mirror of
https://github.com/PrimeDecomp/retrotool.git
synced 2026-07-12 18:18:58 -07:00
Split into lib, CLI, GUI
This commit is contained in:
Generated
+3650
-11
File diff suppressed because it is too large
Load Diff
+13
-27
@@ -1,33 +1,19 @@
|
||||
[package]
|
||||
name = "retrotool"
|
||||
description = "Tools for working with Retro game formats."
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
repository = "https://github.com/PrimeDecomp/retrotool"
|
||||
readme = "README.md"
|
||||
categories = ["command-line-utilities"]
|
||||
[workspace]
|
||||
members = [
|
||||
"lib",
|
||||
"retrotool",
|
||||
"retrotool-gui",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
panic = "abort"
|
||||
strip = "debuginfo"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.69"
|
||||
argh = "0.1.10"
|
||||
# astc-decode = "0.3.1"
|
||||
binrw = "0.11.1"
|
||||
binrw_derive = "0.11.1"
|
||||
ddsfile = { git = "https://github.com/encounter/ddsfile", rev = "880f04c1dffa680eab0e9e09cfa58591fe186a31" }
|
||||
env_logger = "0.10.0"
|
||||
gltf-json = { version = "1.1.0", features = ["names", "extras"] }
|
||||
half = "2.2.1"
|
||||
# image = "0.24.5"
|
||||
log = "0.4.17"
|
||||
memmap2 = "0.5.8"
|
||||
serde_json = "1.0.93"
|
||||
tegra_swizzle = "0.3.0"
|
||||
uuid = "1.3.0"
|
||||
[profile.dev.package]
|
||||
bevy = { opt-level = 3 }
|
||||
egui = { opt-level = 3 }
|
||||
naga = { opt-level = 3 }
|
||||
wgpu = { opt-level = 3 }
|
||||
winit = { opt-level = 3 }
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "retrolib"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.69"
|
||||
binrw = "0.11.1"
|
||||
binrw_derive = "0.11.1"
|
||||
ddsfile = { git = "https://github.com/encounter/ddsfile", rev = "880f04c1dffa680eab0e9e09cfa58591fe186a31" }
|
||||
log = "0.4.17"
|
||||
memmap2 = "0.5.9"
|
||||
tegra_swizzle = "0.3.0"
|
||||
uuid = "1.3.0"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
use anyhow::{anyhow, ensure, Result};
|
||||
use binrw::Endian;
|
||||
|
||||
use crate::format::{chunk::ChunkDescriptor, pack::K_CHUNK_META, rfrm::FormDescriptor, FourCC};
|
||||
|
||||
// Custom footer for extracted files
|
||||
pub const K_FORM_FOOT: FourCC = FourCC(*b"FOOT");
|
||||
// Custom footer asset information
|
||||
pub const K_CHUNK_AINF: FourCC = FourCC(*b"AINF");
|
||||
// Custom footer asset name
|
||||
pub const K_CHUNK_NAME: FourCC = FourCC(*b"NAME");
|
||||
|
||||
/// Locate the meta section in extracted files
|
||||
pub fn locate_meta(file_data: &[u8], e: Endian) -> Result<&[u8]> {
|
||||
let (_, _, remain) = FormDescriptor::slice(file_data, e)?;
|
||||
let (foot_desc, mut foot_data, remain) = FormDescriptor::slice(remain, Endian::Little)?;
|
||||
ensure!(foot_desc.id == K_FORM_FOOT);
|
||||
ensure!(foot_desc.version_a == 1);
|
||||
ensure!(foot_desc.version_b == 1);
|
||||
ensure!(remain.is_empty());
|
||||
|
||||
while !foot_data.is_empty() {
|
||||
let (desc, data, remain) = ChunkDescriptor::slice(foot_data, e)?;
|
||||
if desc.id == K_CHUNK_META {
|
||||
return Ok(data);
|
||||
}
|
||||
foot_data = remain;
|
||||
}
|
||||
Err(anyhow!("Failed to locate META chunk"))
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
pub mod chunk;
|
||||
pub mod cmdl;
|
||||
pub mod foot;
|
||||
pub mod pack;
|
||||
pub mod rfrm;
|
||||
pub mod txtr;
|
||||
|
||||
use std::fmt::{Debug, Display, Formatter, Write};
|
||||
use std::{
|
||||
fmt::{Debug, Display, Formatter, Write},
|
||||
string::FromUtf8Error,
|
||||
};
|
||||
|
||||
use binrw::binrw;
|
||||
|
||||
@@ -54,3 +59,60 @@ impl PartialEq<[u8; 4]> for FourCC {
|
||||
|
||||
#[inline]
|
||||
pub fn peek_four_cc(data: &[u8]) -> FourCC { FourCC(*array_ref!(data, 0, 4)) }
|
||||
|
||||
#[binrw]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CVector3f {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub z: f32,
|
||||
}
|
||||
|
||||
#[binrw]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CColor4f {
|
||||
pub r: f32,
|
||||
pub g: f32,
|
||||
pub b: f32,
|
||||
pub a: f32,
|
||||
}
|
||||
|
||||
#[binrw]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CVector4i {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub z: i32,
|
||||
pub w: i32,
|
||||
}
|
||||
|
||||
#[binrw]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CMatrix4f {
|
||||
pub m: [f32; 16],
|
||||
}
|
||||
|
||||
#[binrw]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CAABox {
|
||||
pub min: CVector3f,
|
||||
pub max: CVector3f,
|
||||
}
|
||||
|
||||
#[binrw]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CStringFixedName {
|
||||
#[bw(try_calc = text.len().try_into())]
|
||||
pub size: u32,
|
||||
#[br(count = size)]
|
||||
pub text: Vec<u8>,
|
||||
}
|
||||
|
||||
impl CStringFixedName {
|
||||
fn from_string(str: &String) -> Self {
|
||||
#[allow(clippy::needless_update)]
|
||||
Self { text: str.as_bytes().to_vec(), ..Default::default() }
|
||||
}
|
||||
|
||||
fn into_string(self) -> Result<String, FromUtf8Error> { String::from_utf8(self.text) }
|
||||
}
|
||||
@@ -4,13 +4,18 @@ use std::{
|
||||
io::{Cursor, Seek, SeekFrom, Write},
|
||||
};
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use anyhow::{anyhow, bail, ensure, Result};
|
||||
use binrw::{binrw, BinReaderExt, BinWriterExt, Endian};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
format::{chunk::ChunkDescriptor, rfrm::FormDescriptor, FourCC},
|
||||
util::lzss::decompress_buffer,
|
||||
format::{
|
||||
chunk::ChunkDescriptor,
|
||||
foot::{K_CHUNK_AINF, K_CHUNK_NAME, K_FORM_FOOT},
|
||||
rfrm::FormDescriptor,
|
||||
FourCC,
|
||||
},
|
||||
util::compression::decompress_buffer,
|
||||
};
|
||||
|
||||
// Package file
|
||||
@@ -24,13 +29,6 @@ pub const K_CHUNK_STRG: FourCC = FourCC(*b"STRG");
|
||||
// Asset directory
|
||||
pub const K_CHUNK_ADIR: FourCC = FourCC(*b"ADIR");
|
||||
|
||||
// Custom footer for extracted files
|
||||
pub const K_FORM_FOOT: FourCC = FourCC(*b"FOOT");
|
||||
// Custom footer asset information
|
||||
pub const K_CHUNK_AINF: FourCC = FourCC(*b"AINF");
|
||||
// Custom footer asset name
|
||||
pub const K_CHUNK_NAME: FourCC = FourCC(*b"NAME");
|
||||
|
||||
/// PACK::TOCC::ADIR chunk
|
||||
#[binrw]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -134,8 +132,201 @@ pub struct Package<'a> {
|
||||
pub assets: Vec<Asset<'a>>,
|
||||
}
|
||||
|
||||
/// Asset header information
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SparsePackageEntry {
|
||||
pub id: Uuid,
|
||||
pub kind: FourCC,
|
||||
pub name: Option<String>,
|
||||
pub reader_version: u32,
|
||||
pub writer_version: u32,
|
||||
}
|
||||
|
||||
impl Package<'_> {
|
||||
pub fn read(data: &[u8], e: Endian) -> Result<Package> {
|
||||
pub fn read_header(data: &[u8], e: Endian) -> Result<Vec<u8>> {
|
||||
let (mut pack, pack_data, _) = FormDescriptor::slice(data, e)?;
|
||||
ensure!(pack.id == K_FORM_PACK);
|
||||
ensure!(pack.version_a == 1);
|
||||
let (mut tocc, tocc_data, _) = FormDescriptor::slice(pack_data, e)?;
|
||||
ensure!(tocc.id == K_FORM_TOCC);
|
||||
ensure!(tocc.version_a == 3);
|
||||
|
||||
// Rewrite PACK with only TOCC chunk
|
||||
let mut out = Cursor::new(Vec::new());
|
||||
pack.write(&mut out, e, |w| {
|
||||
tocc.write(w, e, |w| {
|
||||
w.write_all(tocc_data)?;
|
||||
Ok(())
|
||||
})
|
||||
})?;
|
||||
Ok(out.into_inner())
|
||||
}
|
||||
|
||||
pub fn read_sparse(data: &[u8], e: Endian) -> Result<Vec<SparsePackageEntry>> {
|
||||
let (pack, pack_data, _) = FormDescriptor::slice(data, e)?;
|
||||
ensure!(pack.id == K_FORM_PACK);
|
||||
ensure!(pack.version_a == 1);
|
||||
let (tocc, mut tocc_data, _) = FormDescriptor::slice(pack_data, e)?;
|
||||
ensure!(tocc.id == K_FORM_TOCC);
|
||||
ensure!(tocc.version_a == 3);
|
||||
let mut adir: Option<AssetDirectory> = None;
|
||||
let mut strg: HashMap<Uuid, String> = HashMap::new();
|
||||
while !tocc_data.is_empty() {
|
||||
let (desc, chunk_data, remain) = ChunkDescriptor::slice(tocc_data, e)?;
|
||||
let mut reader = Cursor::new(chunk_data);
|
||||
match desc.id {
|
||||
K_CHUNK_ADIR => {
|
||||
adir = Some(reader.read_type(e)?);
|
||||
}
|
||||
K_CHUNK_META => {}
|
||||
K_CHUNK_STRG => {
|
||||
let chunk: StringTable = reader.read_type(e)?;
|
||||
for entry in chunk.entries {
|
||||
strg.insert(entry.asset_id, String::from_utf8(entry.name)?);
|
||||
}
|
||||
}
|
||||
kind => bail!("Unhandled TOCC chunk {:?}", kind),
|
||||
}
|
||||
tocc_data = remain;
|
||||
}
|
||||
|
||||
let Some(adir) = adir else {
|
||||
bail!("Failed to locate asset directory");
|
||||
};
|
||||
let entries = adir
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|asset_entry| SparsePackageEntry {
|
||||
id: asset_entry.asset_id,
|
||||
kind: asset_entry.asset_type,
|
||||
name: strg.get(&asset_entry.asset_id).cloned(),
|
||||
reader_version: asset_entry.version,
|
||||
writer_version: asset_entry.other_version,
|
||||
})
|
||||
.collect();
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub fn read_asset(data: &[u8], id: Uuid, e: Endian) -> Result<Vec<u8>> {
|
||||
let (pack, pack_data, _) = FormDescriptor::slice(data, e)?;
|
||||
ensure!(pack.id == K_FORM_PACK);
|
||||
ensure!(pack.version_a == 1);
|
||||
let (tocc, mut tocc_data, _) = FormDescriptor::slice(pack_data, e)?;
|
||||
ensure!(tocc.id == K_FORM_TOCC);
|
||||
ensure!(tocc.version_a == 3);
|
||||
|
||||
let mut asset: Option<AssetDirectoryEntry> = None;
|
||||
let mut meta: Option<&[u8]> = None;
|
||||
let mut name: Option<String> = None;
|
||||
while !tocc_data.is_empty() {
|
||||
let (desc, chunk_data, remain) = ChunkDescriptor::slice(tocc_data, e)?;
|
||||
let mut reader = Cursor::new(chunk_data);
|
||||
match desc.id {
|
||||
K_CHUNK_ADIR => {
|
||||
let adir: AssetDirectory = reader.read_type(e)?;
|
||||
asset = Some(
|
||||
adir.entries
|
||||
.into_iter()
|
||||
.find(|asset| asset.asset_id == id)
|
||||
.ok_or_else(|| anyhow!("Failed to locate asset {}", id))?,
|
||||
);
|
||||
}
|
||||
K_CHUNK_META => {
|
||||
let chunk: MetadataTable = reader.read_type(e)?;
|
||||
for entry in chunk.entries {
|
||||
if entry.asset_id != id {
|
||||
continue;
|
||||
}
|
||||
let meta_size = u32::from_le_bytes(
|
||||
chunk_data[entry.offset as usize..entry.offset as usize + 4]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
);
|
||||
let meta_data = &chunk_data
|
||||
[entry.offset as usize + 4..(entry.offset + 4 + meta_size) as usize];
|
||||
meta = Some(meta_data);
|
||||
}
|
||||
}
|
||||
K_CHUNK_STRG => {
|
||||
let chunk: StringTable = reader.read_type(e)?;
|
||||
for entry in chunk.entries {
|
||||
if entry.asset_id != id {
|
||||
continue;
|
||||
}
|
||||
name = Some(String::from_utf8(entry.name)?);
|
||||
break;
|
||||
}
|
||||
}
|
||||
kind => bail!("Unhandled TOCC chunk {:?}", kind),
|
||||
}
|
||||
tocc_data = remain;
|
||||
}
|
||||
|
||||
let Some(asset) = asset else {
|
||||
bail!("Failed to locate asset directory");
|
||||
};
|
||||
let compressed_data = &data[asset.offset as usize..(asset.offset + asset.size) as usize];
|
||||
let (compression_mode, data) = if asset.size != asset.decompressed_size {
|
||||
decompress_buffer(compressed_data, asset.decompressed_size)?
|
||||
} else {
|
||||
(0, Cow::Borrowed(compressed_data))
|
||||
};
|
||||
|
||||
// Validate RFRM
|
||||
{
|
||||
let (form, _, _) = FormDescriptor::slice(&data, Endian::Little)?;
|
||||
ensure!(asset.asset_type == form.id);
|
||||
ensure!(asset.version == form.version_a);
|
||||
ensure!(asset.other_version == form.version_b);
|
||||
ensure!(asset.decompressed_size == form.size + 32 /* RFRM */);
|
||||
}
|
||||
|
||||
let len = data.len() as u64;
|
||||
let mut w = Cursor::new(data.into_owned());
|
||||
w.set_position(len); // set to append
|
||||
|
||||
// Write custom footer
|
||||
FormDescriptor { size: 0, unk: 0, id: K_FORM_FOOT, version_a: 1, version_b: 1 }.write(
|
||||
&mut w,
|
||||
Endian::Little,
|
||||
|w| {
|
||||
ChunkDescriptor { id: K_CHUNK_AINF, size: 0, unk: 0, skip: 0 }.write(
|
||||
w,
|
||||
Endian::Little,
|
||||
|w| {
|
||||
w.write_le(&AssetInfo { id, compression_mode, orig_offset: asset.offset })?;
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
if let Some(meta) = meta {
|
||||
let meta_chunk = ChunkDescriptor {
|
||||
id: K_CHUNK_META,
|
||||
size: meta.len() as u64,
|
||||
unk: 0,
|
||||
skip: 0,
|
||||
};
|
||||
w.write_le(&meta_chunk)?;
|
||||
w.write_all(meta)?;
|
||||
}
|
||||
if let Some(name) = &name {
|
||||
let bytes = name.as_bytes();
|
||||
let name_chunk = ChunkDescriptor {
|
||||
id: K_CHUNK_NAME,
|
||||
size: bytes.len() as u64,
|
||||
unk: 0,
|
||||
skip: 0,
|
||||
};
|
||||
w.write_le(&name_chunk)?;
|
||||
w.write_all(bytes)?;
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(w.into_inner())
|
||||
}
|
||||
|
||||
pub fn read_full(data: &[u8], e: Endian) -> Result<Package> {
|
||||
let (pack, pack_data, _) = FormDescriptor::slice(data, e)?;
|
||||
ensure!(pack.id == K_FORM_PACK);
|
||||
ensure!(pack.version_a == 1);
|
||||
@@ -1,9 +1,21 @@
|
||||
use std::num::NonZeroUsize;
|
||||
use std::{io::Cursor, num::NonZeroUsize};
|
||||
|
||||
use anyhow::{ensure, Result};
|
||||
use binrw::binrw;
|
||||
use anyhow::{anyhow, ensure, Result};
|
||||
use binrw::{binrw, BinReaderExt, Endian};
|
||||
use tegra_swizzle::surface::BlockDim;
|
||||
|
||||
use crate::{
|
||||
format::{chunk::ChunkDescriptor, rfrm::FormDescriptor, FourCC},
|
||||
util::compression::decompress_into,
|
||||
};
|
||||
|
||||
// Texture
|
||||
pub const K_FORM_TXTR: FourCC = FourCC(*b"TXTR");
|
||||
// Texture header
|
||||
pub const K_CHUNK_HEAD: FourCC = FourCC(*b"HEAD");
|
||||
// GPU data
|
||||
pub const K_CHUNK_GPU: FourCC = FourCC(*b"GPU ");
|
||||
|
||||
#[binrw]
|
||||
#[repr(u32)]
|
||||
#[brw(repr(u32))]
|
||||
@@ -370,7 +382,7 @@ impl ETextureFormat {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deswizzle(header: &STextureHeader, data: &[u8]) -> Result<Vec<u8>> {
|
||||
fn deswizzle(header: &STextureHeader, data: &[u8]) -> Result<Vec<u8>> {
|
||||
let (bw, bh, bd) = header.format.block_size();
|
||||
let block_dim = BlockDim {
|
||||
width: NonZeroUsize::new(bw as usize).unwrap(),
|
||||
@@ -401,3 +413,43 @@ pub fn deswizzle(header: &STextureHeader, data: &[u8]) -> Result<Vec<u8>> {
|
||||
header.layers as usize,
|
||||
)?)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextureData {
|
||||
pub head: STextureHeader,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl TextureData {
|
||||
pub fn slice(data: &[u8], meta: &[u8], e: Endian) -> Result<TextureData> {
|
||||
let (txtr_desc, txtr_data, _) = FormDescriptor::slice(data, e)?;
|
||||
ensure!(txtr_desc.id == K_FORM_TXTR);
|
||||
ensure!(txtr_desc.version_a == 47);
|
||||
ensure!(txtr_desc.version_b == 51);
|
||||
|
||||
let (head_desc, head_data, _) = ChunkDescriptor::slice(txtr_data, Endian::Little)?;
|
||||
ensure!(head_desc.id == K_CHUNK_HEAD);
|
||||
let head: STextureHeader = Cursor::new(head_data).read_type(Endian::Little)?;
|
||||
|
||||
// log::debug!("META: {meta:#?}");
|
||||
// log::debug!("HEAD: {head:#?}");
|
||||
|
||||
let meta: STextureMetaData = Cursor::new(meta).read_type(e)?;
|
||||
let mut buffer = vec![0u8; meta.decompressed_size as usize];
|
||||
for info in &meta.buffers {
|
||||
let read =
|
||||
meta.info.iter().find(|i| i.index as u32 == info.index).ok_or_else(|| {
|
||||
anyhow!("Failed to locate read info for buffer {}", info.index)
|
||||
})?;
|
||||
let read_buf = &data[read.offset as usize..(read.offset + read.size) as usize];
|
||||
let comp_buf = &read_buf[info.offset as usize..(info.offset + info.size) as usize];
|
||||
decompress_into(
|
||||
comp_buf,
|
||||
&mut buffer
|
||||
[info.dest_offset as usize..(info.dest_offset + info.dest_size) as usize],
|
||||
)?;
|
||||
}
|
||||
let deswizzled = deswizzle(&head, &buffer)?;
|
||||
Ok(TextureData { head, data: deswizzled })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod format;
|
||||
pub mod util;
|
||||
@@ -0,0 +1,46 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use crate::util::lzss;
|
||||
|
||||
pub fn decompress_buffer(
|
||||
compressed_data: &[u8],
|
||||
decompressed_size: u64,
|
||||
) -> Result<(u32, Cow<[u8]>)> {
|
||||
if compressed_data.len() < 4 {
|
||||
bail!("Invalid compressed data size: {}", compressed_data.len());
|
||||
}
|
||||
if compressed_data[0..4] == [0u8; 4] {
|
||||
// Shortcut for uncompressed data
|
||||
return Ok((0, Cow::Borrowed(&compressed_data[4..])));
|
||||
}
|
||||
let mut out = vec![0u8; decompressed_size as usize];
|
||||
let mode = decompress_into(compressed_data, &mut out)?;
|
||||
Ok((mode, Cow::Owned(out)))
|
||||
}
|
||||
|
||||
pub fn decompress_into(compressed_data: &[u8], out: &mut [u8]) -> Result<u32> {
|
||||
if compressed_data.len() < 4 {
|
||||
bail!("Invalid compressed data size: {}", compressed_data.len());
|
||||
}
|
||||
let mode = u32::from_le_bytes(compressed_data[0..4].try_into().unwrap());
|
||||
let data = &compressed_data[4..];
|
||||
if !match mode {
|
||||
0 => {
|
||||
if data.len() == out.len() {
|
||||
out.copy_from_slice(data);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
1 => lzss::decompress::<1>(data, out),
|
||||
2 => lzss::decompress::<2>(data, out),
|
||||
3 => lzss::decompress::<3>(data, out),
|
||||
_ => bail!("Unsupported compression mode {}", mode),
|
||||
} {
|
||||
bail!("Decompression failed");
|
||||
}
|
||||
Ok(mode)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/// https://wiki.axiodl.com/w/LZSS_Compression
|
||||
pub fn decompress<const M: u8>(mut input: &[u8], output: &mut [u8]) -> bool {
|
||||
let group_len = 2usize.pow(M as u32 - 1);
|
||||
let mut out_cur = 0usize;
|
||||
|
||||
let mut header_byte = 0u8;
|
||||
let mut group = 0u8;
|
||||
while !input.is_empty() {
|
||||
if group == 0 {
|
||||
header_byte = input[0];
|
||||
input = &input[1..];
|
||||
group = 8;
|
||||
}
|
||||
|
||||
if header_byte & 0x80 == 0 {
|
||||
output[out_cur..group_len + out_cur].copy_from_slice(&input[..group_len]);
|
||||
input = &input[group_len..];
|
||||
out_cur += group_len;
|
||||
} else {
|
||||
let count = (input[0] as usize >> 4) + (4 - M as usize);
|
||||
let length = (((input[0] as usize & 0xF) << 0x8) | input[1] as usize) << (M - 1);
|
||||
input = &input[2..];
|
||||
|
||||
let seek = out_cur - length;
|
||||
for n in 0..count * group_len {
|
||||
output[out_cur + n] = output[seek + n];
|
||||
}
|
||||
out_cur += count * group_len;
|
||||
}
|
||||
|
||||
header_byte <<= 1;
|
||||
group -= 1;
|
||||
}
|
||||
|
||||
out_cur == output.len()
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod astc;
|
||||
pub mod compression;
|
||||
pub mod dds;
|
||||
pub mod file;
|
||||
pub mod lzss;
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "retrotool-gui"
|
||||
description = "Tools for working with Retro game formats."
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
repository = "https://github.com/PrimeDecomp/retrotool"
|
||||
readme = "README.md"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
dynamic = ["bevy/dynamic"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.69"
|
||||
astc-decode = "0.3.1"
|
||||
bevy = "0.9.1"
|
||||
bevy_egui = { git = "https://github.com/paul-hansen/bevy_egui", branch = "egui-0.21" }
|
||||
binrw = "0.11.1"
|
||||
egui = "0.21.0"
|
||||
egui_dock = "0.4.0"
|
||||
image = "0.24.5"
|
||||
log = "0.4.17"
|
||||
retrolib = { path = "../lib" }
|
||||
uuid = "1.3.0"
|
||||
walkdir = "2.3.2"
|
||||
@@ -0,0 +1,229 @@
|
||||
use std::{
|
||||
io::Cursor,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use anyhow::Error;
|
||||
use astc_decode::{astc_decode, Footprint};
|
||||
use bevy::{
|
||||
app::{App, Plugin},
|
||||
asset::{
|
||||
AddAsset, AssetIo, AssetIoError, AssetLoader, BoxedFuture, LoadContext, LoadedAsset,
|
||||
Metadata,
|
||||
},
|
||||
prelude::*,
|
||||
};
|
||||
use binrw::Endian;
|
||||
use image::{ImageBuffer, RgbaImage};
|
||||
use retrolib::{
|
||||
format::{
|
||||
foot::locate_meta,
|
||||
pack::{Package, SparsePackageEntry},
|
||||
txtr::TextureData,
|
||||
},
|
||||
util::file::map_file,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Resource)]
|
||||
pub struct SharedPackageInfo {
|
||||
packages: Arc<RwLock<Vec<PackageDirectory>>>,
|
||||
}
|
||||
|
||||
struct RetroAssetIo {
|
||||
default: Box<dyn AssetIo>,
|
||||
packages: SharedPackageInfo,
|
||||
}
|
||||
|
||||
fn read_pak_header(path: &Path) -> anyhow::Result<Vec<u8>> {
|
||||
let data = map_file(path)?;
|
||||
Package::read_header(&data, Endian::Little)
|
||||
}
|
||||
|
||||
fn read_asset(path: &Path, id: Uuid) -> anyhow::Result<Vec<u8>> {
|
||||
let data = map_file(path)?;
|
||||
Package::read_asset(&data, id, Endian::Little)
|
||||
}
|
||||
|
||||
impl AssetIo for RetroAssetIo {
|
||||
fn load_path<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, anyhow::Result<Vec<u8>, AssetIoError>> {
|
||||
if let Some(id) =
|
||||
path.file_stem().and_then(|name| Uuid::try_parse(&name.to_string_lossy()).ok())
|
||||
{
|
||||
// Load pak header only
|
||||
Box::pin(async move {
|
||||
let mut package_path: Option<PathBuf> = None;
|
||||
if let Ok(packages) = self.packages.packages.read() {
|
||||
if let Some(package) =
|
||||
packages.iter().find(|p| p.entries.iter().any(|e| e.id == id))
|
||||
{
|
||||
package_path = Some(package.path.clone());
|
||||
}
|
||||
}
|
||||
let Some(package_path) = package_path else {
|
||||
return Err(AssetIoError::NotFound(path.to_owned()));
|
||||
};
|
||||
read_asset(&package_path, id).map_err(|e| {
|
||||
AssetIoError::Io(std::io::Error::new(std::io::ErrorKind::Other, e))
|
||||
})
|
||||
})
|
||||
} else if path.extension() == Some("pak".as_ref()) {
|
||||
// Load pak header only
|
||||
Box::pin(async move {
|
||||
read_pak_header(path).map_err(|e| {
|
||||
AssetIoError::Io(std::io::Error::new(std::io::ErrorKind::Other, e))
|
||||
})
|
||||
})
|
||||
} else {
|
||||
self.default.load_path(path)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_directory(
|
||||
&self,
|
||||
path: &Path,
|
||||
) -> anyhow::Result<Box<dyn Iterator<Item = PathBuf>>, AssetIoError> {
|
||||
self.default.read_directory(path)
|
||||
}
|
||||
|
||||
fn get_metadata(&self, path: &Path) -> anyhow::Result<Metadata, AssetIoError> {
|
||||
self.default.get_metadata(path)
|
||||
}
|
||||
|
||||
fn watch_path_for_changes(&self, path: &Path) -> anyhow::Result<(), AssetIoError> {
|
||||
self.default.watch_path_for_changes(path)
|
||||
}
|
||||
|
||||
fn watch_for_changes(&self) -> anyhow::Result<(), AssetIoError> {
|
||||
self.default.watch_for_changes()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RetroAssetIoPlugin;
|
||||
|
||||
impl Plugin for RetroAssetIoPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
let default = AssetPlugin::default().create_platform_default_asset_io();
|
||||
let shared_package_info = SharedPackageInfo { packages: Arc::new(Default::default()) };
|
||||
let asset_io = RetroAssetIo { default, packages: shared_package_info.clone() };
|
||||
app.insert_resource(shared_package_info);
|
||||
app.insert_resource(AssetServer::new(asset_io));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn package_loader_system(
|
||||
mut ev_asset: EventReader<AssetEvent<PackageDirectory>>,
|
||||
assets: Res<Assets<PackageDirectory>>,
|
||||
package_info: Res<SharedPackageInfo>,
|
||||
) {
|
||||
for ev in ev_asset.iter() {
|
||||
match ev {
|
||||
AssetEvent::Created { handle } => {
|
||||
let package = assets.get(handle).unwrap();
|
||||
println!("Loaded package {}", package.path.display());
|
||||
let mut package_info =
|
||||
package_info.packages.write().expect("Failed to lock shared package info");
|
||||
package_info.push(package.clone());
|
||||
}
|
||||
AssetEvent::Modified { .. } => {}
|
||||
AssetEvent::Removed { handle } => {
|
||||
let package = assets.get(handle).unwrap();
|
||||
let mut package_info =
|
||||
package_info.packages.write().expect("Failed to lock shared package info");
|
||||
package_info.retain(|p| p.path != package.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, bevy::reflect::TypeUuid)]
|
||||
#[uuid = "83269869-1209-408e-8835-bc6f2496e827"]
|
||||
pub struct PackageDirectory {
|
||||
pub path: PathBuf,
|
||||
pub name: String,
|
||||
pub entries: Vec<SparsePackageEntry>,
|
||||
}
|
||||
|
||||
pub struct PackageAssetLoader;
|
||||
|
||||
impl Plugin for PackageAssetLoader {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_asset::<PackageDirectory>().add_asset_loader(PackageAssetLoader);
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetLoader for PackageAssetLoader {
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
bytes: &'a [u8],
|
||||
load_context: &'a mut LoadContext,
|
||||
) -> BoxedFuture<'a, anyhow::Result<(), Error>> {
|
||||
Box::pin(async move {
|
||||
load_context.set_default_asset(LoadedAsset::new(PackageDirectory {
|
||||
path: load_context.path().to_owned(),
|
||||
name: load_context
|
||||
.path()
|
||||
.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
entries: Package::read_sparse(bytes, Endian::Little)?,
|
||||
}));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn extensions(&self) -> &[&str] { &["pak"] }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, bevy::reflect::TypeUuid)]
|
||||
#[uuid = "83269869-1209-408e-8835-bc6f2496e828"]
|
||||
pub struct TxtrData {
|
||||
pub data: TextureData,
|
||||
pub rgba: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
pub struct TxtrAssetLoader;
|
||||
|
||||
impl Plugin for TxtrAssetLoader {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_asset::<TxtrData>().add_asset_loader(TxtrAssetLoader);
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetLoader for TxtrAssetLoader {
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
bytes: &'a [u8],
|
||||
load_context: &'a mut LoadContext,
|
||||
) -> BoxedFuture<'a, anyhow::Result<(), Error>> {
|
||||
Box::pin(async move {
|
||||
let meta = locate_meta(bytes, Endian::Little)?;
|
||||
let data = TextureData::slice(bytes, meta, Endian::Little)?;
|
||||
let mut rgba = None;
|
||||
if data.head.format.is_astc() {
|
||||
let mut image = RgbaImage::new(data.head.width, data.head.height);
|
||||
let (bx, by, _) = data.head.format.block_size();
|
||||
astc_decode(
|
||||
Cursor::new(&data.data),
|
||||
data.head.width,
|
||||
data.head.height,
|
||||
Footprint::new(bx as u32, by as u32),
|
||||
|x, y, texel| {
|
||||
image.put_pixel(x, y, texel.into());
|
||||
},
|
||||
)?;
|
||||
rgba = Some(image.into_raw());
|
||||
}
|
||||
println!("Loaded texture {:?}", data.head);
|
||||
load_context.set_default_asset(LoadedAsset::new(TxtrData { data, rgba }));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn extensions(&self) -> &[&str] { &["txtr"] }
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
mod loaders;
|
||||
|
||||
use std::fs::FileType;
|
||||
|
||||
use bevy::{
|
||||
asset::LoadState,
|
||||
prelude::*,
|
||||
render::render_resource::{
|
||||
Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
|
||||
},
|
||||
};
|
||||
use bevy_egui::{
|
||||
egui,
|
||||
egui::{text::LayoutJob, Color32, FontId, TextFormat, Widget},
|
||||
EguiContext, EguiPlugin, EguiSettings,
|
||||
};
|
||||
use egui::TextureId;
|
||||
use retrolib::format::txtr::K_FORM_TXTR;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
use crate::loaders::{
|
||||
package_loader_system, PackageAssetLoader, PackageDirectory, RetroAssetIoPlugin,
|
||||
TxtrAssetLoader, TxtrData,
|
||||
};
|
||||
|
||||
struct Images {
|
||||
bevy_icon: Handle<Image>,
|
||||
bevy_icon_inverted: Handle<Image>,
|
||||
}
|
||||
|
||||
impl FromWorld for Images {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let asset_server = world.get_resource_mut::<AssetServer>().unwrap();
|
||||
Self {
|
||||
bevy_icon: asset_server.load("icon.png"),
|
||||
bevy_icon_inverted: asset_server.load("icon_inverted.png"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This example demonstrates the following functionality and use-cases of bevy_egui:
|
||||
/// - rendering loaded assets;
|
||||
/// - toggling hidpi scaling (by pressing '/' button);
|
||||
/// - configuring egui contexts during the startup.
|
||||
fn main() {
|
||||
App::new()
|
||||
.insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0)))
|
||||
.insert_resource(Msaa { samples: 1 })
|
||||
.init_resource::<UiState>()
|
||||
.init_resource::<Packages>()
|
||||
.init_resource::<Textures>()
|
||||
.init_resource::<OpenAsset>()
|
||||
.add_plugins(
|
||||
DefaultPlugins
|
||||
.build()
|
||||
// the custom asset io plugin must be inserted in-between the
|
||||
// `CorePlugin' and `AssetPlugin`. It needs to be after the
|
||||
// CorePlugin, so that the IO task pool has already been constructed.
|
||||
// And it must be before the `AssetPlugin` so that the asset plugin
|
||||
// doesn't create another instance of an asset server. In general,
|
||||
// the AssetPlugin should still run so that other aspects of the
|
||||
// asset system are initialized correctly.
|
||||
.add_before::<AssetPlugin, _>(RetroAssetIoPlugin),
|
||||
)
|
||||
.add_plugin(PackageAssetLoader)
|
||||
.add_plugin(TxtrAssetLoader)
|
||||
.add_plugin(EguiPlugin)
|
||||
.add_startup_system(configure_visuals)
|
||||
.add_startup_system(preload_package)
|
||||
.add_system(update_ui_scale_factor)
|
||||
.add_system(ui_example)
|
||||
.add_system(package_loader_system)
|
||||
.add_system(check_assets_ready)
|
||||
.run();
|
||||
}
|
||||
|
||||
enum TabType {
|
||||
Directory,
|
||||
Asset(HandleUntyped, Option<TextureId>),
|
||||
Empty,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct UiState {
|
||||
tree: egui_dock::Tree<TabType>,
|
||||
}
|
||||
|
||||
impl Default for UiState {
|
||||
fn default() -> Self {
|
||||
let mut tree = egui_dock::Tree::new(vec![TabType::Empty]);
|
||||
tree.split_left(egui_dock::NodeIndex::root(), 0.3, vec![TabType::Directory]);
|
||||
Self { tree }
|
||||
}
|
||||
}
|
||||
|
||||
struct TabViewer<'a> {
|
||||
server: Res<'a, AssetServer>,
|
||||
packages: Res<'a, Assets<PackageDirectory>>,
|
||||
textures: Res<'a, Assets<TxtrData>>,
|
||||
open_asset: ResMut<'a, OpenAsset>,
|
||||
}
|
||||
|
||||
impl egui_dock::TabViewer for TabViewer<'_> {
|
||||
type Tab = TabType;
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, tab: &mut Self::Tab) {
|
||||
match tab {
|
||||
TabType::Directory => {
|
||||
for (_, package) in self.packages.iter() {
|
||||
egui::CollapsingHeader::new(&package.name).show(ui, |ui| {
|
||||
for entry in &package.entries {
|
||||
let mut job = LayoutJob::simple(
|
||||
format!("{} {}", entry.kind, entry.id),
|
||||
FontId::monospace(12.0),
|
||||
Color32::GRAY,
|
||||
0.0,
|
||||
);
|
||||
if let Some(name) = &entry.name {
|
||||
job.append(
|
||||
&format!("\n{name}"),
|
||||
0.0,
|
||||
TextFormat::simple(FontId::monospace(12.0), Color32::WHITE),
|
||||
);
|
||||
}
|
||||
if egui::SelectableLabel::new(false, job).ui(ui).clicked() {
|
||||
self.open_asset.0 = match entry.kind {
|
||||
K_FORM_TXTR => Some(
|
||||
self.server
|
||||
.load::<TxtrData, _>(format!(
|
||||
"{}.{}",
|
||||
entry.id, entry.kind
|
||||
))
|
||||
.into(),
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
TabType::Asset(handle, image) => {
|
||||
if let Some(txtr) = self.textures.get(&handle.typed_weak::<TxtrData>()) {
|
||||
if let Some(image) = image {
|
||||
ui.add(egui::widgets::Image::new(*image, egui::Vec2 {
|
||||
x: txtr.data.head.width as f32,
|
||||
y: txtr.data.head.height as f32,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
TabType::Empty => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn title(&mut self, tab: &mut Self::Tab) -> egui::WidgetText {
|
||||
match tab {
|
||||
TabType::Directory => "Directory".into(),
|
||||
TabType::Asset(_, _) => "Asset view".into(),
|
||||
TabType::Empty => "Placeholder".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_visuals(mut egui_ctx: ResMut<EguiContext>) {
|
||||
// egui_ctx
|
||||
// .ctx_mut()
|
||||
// .set_visuals(egui::Visuals { window_rounding: 0.0.into(), ..Default::default() });
|
||||
}
|
||||
|
||||
#[derive(Default, Resource)]
|
||||
struct Packages(Vec<Handle<PackageDirectory>>);
|
||||
#[derive(Default, Resource)]
|
||||
struct Textures(Vec<Handle<TxtrData>>);
|
||||
|
||||
fn is_hidden(entry: &DirEntry) -> bool {
|
||||
entry.file_name().to_str().map(|s| s.starts_with('.')).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn preload_package(server: Res<AssetServer>, mut loading: ResMut<Packages>) {
|
||||
let walker = WalkDir::new("/home/lstreet/Development/mpr/extract/romfs").into_iter();
|
||||
for entry in walker.filter_entry(|e| !is_hidden(e)).filter_map(|e| e.ok()) {
|
||||
if entry.file_type().is_file() && entry.path().extension() == Some("pak".as_ref()) {
|
||||
loading.0.push(server.load(entry.path()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_assets_ready(
|
||||
server: Res<AssetServer>,
|
||||
mut loading: ResMut<OpenAsset>,
|
||||
mut ui_state: ResMut<UiState>,
|
||||
textures: Res<Assets<TxtrData>>,
|
||||
mut egui_ctx: ResMut<EguiContext>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
) {
|
||||
if let Some(handle) = &loading.0 {
|
||||
let state = server.get_load_state(handle);
|
||||
match state {
|
||||
LoadState::NotLoaded => {}
|
||||
LoadState::Loading => {}
|
||||
LoadState::Loaded => {
|
||||
let handle = std::mem::take(&mut loading.0).unwrap();
|
||||
let txtr = textures.get(&handle.typed_weak::<TxtrData>()).unwrap();
|
||||
let image = if let Some(rgba) = &txtr.rgba {
|
||||
let image_handle = images.add(Image {
|
||||
data: rgba.clone(),
|
||||
texture_descriptor: TextureDescriptor {
|
||||
label: None,
|
||||
size: Extent3d {
|
||||
width: txtr.data.head.width,
|
||||
height: txtr.data.head.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
|
||||
},
|
||||
sampler_descriptor: Default::default(),
|
||||
texture_view_descriptor: None,
|
||||
});
|
||||
Some(egui_ctx.add_image(image_handle))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
ui_state.tree.push_to_first_leaf(TabType::Asset(handle, image));
|
||||
}
|
||||
LoadState::Failed => {}
|
||||
LoadState::Unloaded => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_ui_scale_factor(
|
||||
keyboard_input: Res<Input<KeyCode>>,
|
||||
mut toggle_scale_factor: Local<Option<bool>>,
|
||||
mut egui_settings: ResMut<EguiSettings>,
|
||||
windows: Res<Windows>,
|
||||
) {
|
||||
if keyboard_input.just_pressed(KeyCode::Slash) || toggle_scale_factor.is_none() {
|
||||
*toggle_scale_factor = Some(!toggle_scale_factor.unwrap_or(true));
|
||||
|
||||
if let Some(window) = windows.get_primary() {
|
||||
let scale_factor =
|
||||
if toggle_scale_factor.unwrap() { 1.0 } else { 1.0 / window.scale_factor() };
|
||||
egui_settings.scale_factor = scale_factor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Resource)]
|
||||
struct OpenAsset(Option<HandleUntyped>);
|
||||
|
||||
fn ui_example(
|
||||
mut egui_ctx: ResMut<EguiContext>,
|
||||
mut ui_state: ResMut<UiState>,
|
||||
packages: Res<Assets<PackageDirectory>>,
|
||||
textures: Res<Assets<TxtrData>>,
|
||||
server: Res<AssetServer>,
|
||||
open_asset: ResMut<OpenAsset>,
|
||||
) {
|
||||
egui::TopBottomPanel::top("top_panel").show(egui_ctx.ctx_mut(), |ui| {
|
||||
// The top panel is often a good place for a menu bar:
|
||||
egui::menu::bar(ui, |ui| {
|
||||
egui::menu::menu_button(ui, "File", |ui| {
|
||||
if ui.button("Quit").clicked() {
|
||||
std::process::exit(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
egui_dock::DockArea::new(&mut ui_state.tree)
|
||||
.style(egui_dock::Style::from_egui(egui_ctx.ctx_mut().style().as_ref()))
|
||||
.show(egui_ctx.ctx_mut(), &mut TabViewer { server, packages, textures, open_asset });
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user