Compare commits
No commits in common. "bd1d3dd8d38dbc40e13a64286d38d98f5dda3e15" and "c1d3cfaf04c3a39c1773893f26894dcccc4ab440" have entirely different histories.
bd1d3dd8d3
...
c1d3cfaf04
|
@ -5,7 +5,3 @@ 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
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
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
|
|
|
@ -1,8 +1,7 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
#resolver = "2"
|
#resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/jmmt",
|
"crates/jmmt",
|
||||||
"crates/jmrenamer",
|
"crates/jmrenamer"
|
||||||
"crates/textool"
|
# "crates/paktool"
|
||||||
# "crates/paktool"
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,5 +5,3 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
binext = "1.0.0"
|
binext = "1.0.0"
|
||||||
image = "0.24.6"
|
|
||||||
thiserror = "1.0.40"
|
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
//! Low-level file format definitions. These are suitable for usage with the [binext] crate,
|
//! Low-level structure definitions
|
||||||
//! 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;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
//! Package file format structures
|
//! Package file format structures
|
||||||
|
|
||||||
use super::Validatable;
|
|
||||||
use crate::lzss::header::LzssHeader;
|
use crate::lzss::header::LzssHeader;
|
||||||
|
use super::Validatable;
|
||||||
|
|
||||||
/// "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,12 +13,11 @@ 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,
|
||||||
|
|
||||||
|
@ -29,12 +28,12 @@ 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 {
|
||||||
/// 'PGRP'
|
/// 'PGRP'
|
||||||
pub const VALID_FOURCC: u32 = 0x50524750;
|
pub const VALID_FOURCC : u32 = 0x50524750;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Validatable for PackageGroup {
|
impl Validatable for PackageGroup {
|
||||||
|
@ -45,7 +44,6 @@ 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,
|
||||||
|
|
||||||
|
@ -87,12 +85,13 @@ 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;
|
||||||
|
|
||||||
pub fn is_compressed(&self) -> bool {
|
pub fn is_compressed(&self) -> bool {
|
||||||
// If the compressed size matches the uncompressed size
|
// If the compressed size matches the uncompressed size
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
//! 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)]
|
||||||
|
@ -16,23 +14,23 @@ pub struct PackageTocEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PackageTocEntry {
|
impl PackageTocEntry {
|
||||||
pub fn file_name(&self) -> Option<String> {
|
fn file_name(&self) -> Option<String> {
|
||||||
make_c_string(&self.file_name)
|
String::from_utf8(self.file_name.to_vec()).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn file_name_hash(&self) -> u32 {
|
fn file_name_hash(&self) -> u32 {
|
||||||
self.file_name_hash
|
self.file_name_hash
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toc_start_offset(&self) -> u32 {
|
fn toc_start_offset(&self) -> u32 {
|
||||||
self.toc_start_offset
|
self.toc_start_offset
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toc_size(&self) -> u32 {
|
fn toc_size(&self) -> u32 {
|
||||||
self.toc_size
|
self.toc_size
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toc_file_count(&self) -> u32 {
|
fn toc_file_count(&self) -> u32 {
|
||||||
self.toc_file_count
|
self.toc_file_count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
//! .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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
//! .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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -65,8 +65,9 @@ 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
|
self.state.set(
|
||||||
.set(CRC32_TABLE[((old ^ (*b & !0x20) as u32) & 0xff) as usize] ^ (old >> 8));
|
CRC32_TABLE[((old ^ (*b & !0x20) as u32) & 0xff) as usize] ^ (old >> 8),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
//! 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)
|
|
||||||
}
|
|
|
@ -1,5 +1,4 @@
|
||||||
//! Hash Algorithms
|
//! Hash Algorithms
|
||||||
|
|
||||||
pub mod crc32;
|
pub mod crc32;
|
||||||
pub mod filename;
|
|
||||||
pub use crc32::*;
|
pub use crc32::*;
|
||||||
|
|
|
@ -7,11 +7,6 @@ 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;
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use crate::format::Validatable;
|
|
||||||
use std::mem::size_of;
|
use std::mem::size_of;
|
||||||
|
use crate::format::Validatable;
|
||||||
|
|
||||||
#[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,
|
||||||
|
|
|
@ -6,3 +6,4 @@
|
||||||
// - 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;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
//! High-level readers
|
|
||||||
|
|
||||||
pub mod package_toc;
|
|
||||||
|
|
||||||
pub mod texture;
|
|
|
@ -1,45 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
|
@ -1,225 +0,0 @@
|
||||||
//! 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
//! 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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,5 +4,4 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.3.8", features = ["derive"] }
|
|
||||||
jmmt = { path = "../jmmt" }
|
jmmt = { path = "../jmmt" }
|
||||||
|
|
|
@ -1,151 +1,74 @@
|
||||||
//! A reimplementation of jmmt_renamer in Rust.
|
//! A reimplemntation 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 jmmt::hash::filename::*;
|
use std::{fs, io, path::Path};
|
||||||
use jmmt::read::package_toc::read_package_toc;
|
use jmmt::hash::hash_string;
|
||||||
use std::{fs, path::Path};
|
|
||||||
|
|
||||||
use clap::{Subcommand, Parser};
|
const FILENAME_TABLE : [&str; 20] = [
|
||||||
|
// First loaded by the game
|
||||||
|
"package.toc",
|
||||||
|
|
||||||
|
// General packs
|
||||||
|
"config.pak",
|
||||||
|
|
||||||
#[derive(Parser)]
|
// This file is referenced in the game files,
|
||||||
#[command(author, version, about, long_about = None)]
|
// but doesn't seem to exist anymore in the final build.
|
||||||
#[command(propagate_version = true)]
|
//"shell.pak",
|
||||||
struct Cli {
|
|
||||||
#[command(subcommand)]
|
"shell_character_select.pak",
|
||||||
command: Commands,
|
"shell_main.pak",
|
||||||
|
"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)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
fn main() -> io::Result<()> {
|
||||||
enum Commands {
|
|
||||||
/// Rename the files in the DATA directory to the original filenames.
|
|
||||||
Clear,
|
|
||||||
|
|
||||||
/// Rename the files to the .DAT filenames that the game expects.
|
// A relatively simple idiot-check.
|
||||||
Unclear,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_clear() {
|
|
||||||
let package_toc_filename = format!("DATA/{}", dat_filename("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_src = format!(
|
|
||||||
"DATA/{}",
|
|
||||||
dat_filename_from_hash(toc_entry.file_name_hash())
|
|
||||||
);
|
|
||||||
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());
|
|
||||||
|
|
||||||
if src_path.exists() {
|
|
||||||
match fs::rename(src_path, dest_path) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(error) => {
|
|
||||||
println!("Error renaming {}: {}", src_path.display(), error);
|
|
||||||
return ();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
println!("Clearnamed {} -> {}", src_path.display(), dest_path.display());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
if !Path::new("DATA").is_dir() {
|
||||||
println!("This program should be run in the root of an extracted copy.");
|
println!("This program should be run in the root of an extracted copy.");
|
||||||
return ();
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let cli = Cli::parse();
|
for clearname in FILENAME_TABLE.iter() {
|
||||||
|
let dat_filename = hashed_dat_filename(clearname);
|
||||||
|
let dat_src = format!("DATA/{}", dat_filename);
|
||||||
|
let dat_clearname = format!("DATA/{}", String::from(*clearname));
|
||||||
|
|
||||||
|
let src_path = Path::new(dat_src.as_str());
|
||||||
|
let dest_path = Path::new(dat_clearname.as_str());
|
||||||
|
|
||||||
match &cli.command {
|
if src_path.exists() {
|
||||||
Commands::Clear =>
|
fs::rename(src_path, dest_path)?;
|
||||||
do_clear(),
|
println!("moved {} -> {}", src_path.display(), dest_path.display());
|
||||||
Commands::Unclear =>
|
}
|
||||||
do_unclear()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "textool"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
clap = { version = "4.3.8", features = ["derive"] }
|
|
||||||
jmmt = { path = "../jmmt" }
|
|
|
@ -1,56 +0,0 @@
|
||||||
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 ();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -30,9 +30,12 @@ struct Lzss_Header {
|
||||||
//}
|
//}
|
||||||
};
|
};
|
||||||
|
|
||||||
// "PGRP" chunk.
|
// Data strutures actually part of the file:
|
||||||
|
|
||||||
|
// "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
|
||||||
|
@ -40,55 +43,52 @@ struct JMMT_PGRP {
|
||||||
u32 pad; // seemingly always 0xCDCDCDCD
|
u32 pad; // seemingly always 0xCDCDCDCD
|
||||||
};
|
};
|
||||||
|
|
||||||
// "PFIL" chunk.
|
// "PFIL" entry.
|
||||||
//
|
//
|
||||||
// This represents a file chunk,
|
// This represents a file block,
|
||||||
// which can itself represent either a whole file (when it is > 65536 bytes),
|
// which can itself represent either a whole file (> 65535 bytes),
|
||||||
// or part of a file (which will need to be stiched together from multiple chunks).
|
// or part of a file (which will need to be stiched together).
|
||||||
struct JMMT_PFIL {
|
struct JMMT_PFIL {
|
||||||
u32 magic;
|
u32 magic;
|
||||||
|
|
||||||
u32 unk; // These two seem to stay the same for every PFIL
|
u32 unk[2]; // Don't know what these are?
|
||||||
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 packaging tool seems to leave files
|
// However the game seems to order chunks for files
|
||||||
// in order, and doesn't start/interleave other files
|
// in order, and doesn't start/interleave other files
|
||||||
// in between. This is definitely still useful, but
|
// in between. So this is a nice waste of 16 bits.
|
||||||
// not quite as much.
|
u16 chunkSequenceNumber;
|
||||||
u16 chunkNumber;
|
|
||||||
|
|
||||||
// Amount of chunks which need to be read
|
// Amount of chunks which need to be read
|
||||||
// from to complete this file.
|
// from to read this file completely.
|
||||||
//
|
//
|
||||||
// 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().
|
// Hashed with jmmt::HashString() (in the jmmt_tools repo).
|
||||||
u32 filenameCrc;
|
u32 filenameCrc;
|
||||||
|
|
||||||
u32 unk3[7]; // These stay the same per file chunk. Could be hashes
|
u32 unk2[7]; // more unknown stuff I don't care/know about
|
||||||
|
|
||||||
// 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 bufferOffset;
|
u32 blockOffset;
|
||||||
|
|
||||||
// Compressed size of the chunk.
|
// ?
|
||||||
u32 compressedSize;
|
u32 unk3;
|
||||||
|
|
||||||
// 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,10 +98,12 @@ 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("Hash: {:0x}, Seqnum: {}, ZOff: {}, FileSize: {}, ChunkSize: {}(z {}), BufferOff: {},",
|
std::print(" Chunk seqNum: {}, Filename CRC: {:0x}, File Size: {}, Chunk Size: {}, Block Offset: {}, Data Offset: {}", chunkSequenceNumber, filenameCrc, fileSize, chunkSize, blockOffset, dataOffset);
|
||||||
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.
|
||||||
|
@ -111,10 +113,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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -124,5 +126,6 @@ struct PFIL_WRAPPER {
|
||||||
// Group header.
|
// Group header.
|
||||||
JMMT_PGRP grp @ 0xd1c30;
|
JMMT_PGRP grp @ 0xd1c30;
|
||||||
|
|
||||||
// All pfil objects. The wrapper expands out other pfil chunks automatically. Pretty cool.
|
// This isn't right (as one PFIL chunk doesn't actually have to mean one file),
|
||||||
|
// but it works for testing and trying to understand the format.
|
||||||
PFIL_WRAPPER files[grp.nrfiles] @ $;
|
PFIL_WRAPPER files[grp.nrfiles] @ $;
|
Loading…
Reference in New Issue