Split into lib, CLI, GUI

This commit is contained in:
Luke Street
2023-02-18 10:41:07 -05:00
parent f1a527c491
commit f7f5d9d8a5
30 changed files with 5612 additions and 1178 deletions
Generated
+3650 -11
View File
File diff suppressed because it is too large Load Diff
+13 -27
View File
@@ -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 }
+14
View File
@@ -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
+30
View File
@@ -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"))
}
+63 -1
View File
@@ -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) }
}
+202 -11
View File
@@ -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);
+56 -4
View File
@@ -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 })
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod format;
pub mod util;
+46
View File
@@ -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)
}
+36
View File
@@ -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
View File
@@ -1,4 +1,5 @@
pub mod astc;
pub mod compression;
pub mod dds;
pub mod file;
pub mod lzss;
+28
View File
@@ -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"
+229
View File
@@ -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"] }
}
+278
View File
@@ -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