diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3a69f38 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'soiboy'", + "cargo": { + "args": [ + "test", + "--no-run", + // "--lib", + "--package=soiboy" + ], + "filter": { + "name": "soiboy", + // "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index e3f16bf..5cf52dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,13 @@ name = "soiboy" version = "0.1.0" edition = "2021" +[lib] +crate-type = ["cdylib", "rlib"] + [dependencies] modular-bitfield = "0.11" flate2 = "1.0" -binrw = "0.11" +binrw = "0.13" [dev-dependencies] x-flipper-360 = { git = "https://github.com/offsetting/x-flipper-360" } diff --git a/src/collision.rs b/src/collision.rs new file mode 100644 index 0000000..0cff265 --- /dev/null +++ b/src/collision.rs @@ -0,0 +1,839 @@ +use binrw::{BinRead, BinResult, BinWrite, Endian}; +use std::io::{Seek, Write}; + +use crate::utils::*; + +// Members that are commented out are part of the streaming data section, and need to be merged into the contents of the header data after extraction. + +#[derive(BinRead, BinWrite, PartialEq, Debug)] +#[brw(repr = i32)] +enum CollisionType { + Soultree = 0, + SoultreeHeirarchy, + Rays, + DynamicRays, + RadiusedLine, + Sphere, + Box, + Ecosystem, + FinitePlane, + + StreamingSoultree, + StreamingHeirarchy, + StreamingFinitePlane, +} + +impl std::fmt::Display for CollisionType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CollisionType::Soultree => write!(f, "Soultree"), + CollisionType::SoultreeHeirarchy => write!(f, "SoultreeHeirarchy"), + CollisionType::Rays => write!(f, "Rays"), + CollisionType::DynamicRays => write!(f, "DynamicRays"), + CollisionType::RadiusedLine => write!(f, "RadiusedLine"), + CollisionType::Sphere => write!(f, "Sphere"), + CollisionType::Box => write!(f, "Box"), + CollisionType::Ecosystem => write!(f, "Ecosystem"), + CollisionType::FinitePlane => write!(f, "FinitePlane"), + CollisionType::StreamingSoultree => write!(f, "StreamingSoultree"), + CollisionType::StreamingHeirarchy => write!(f, "StreamingHeirarchy"), + CollisionType::StreamingFinitePlane => write!(f, "StreamingFinitePlane"), + } + } +} + +#[derive(Default, BinRead, BinWrite, Debug)] +struct TreeFace { + volume: f32, + vectors: [Vector3; 2], + type_indices: [i16; 2], +} + +#[derive(Default, BinRead, BinWrite, Debug)] +struct RaceORamaStreamingDataTreeFace { + vectors: [Vector4; 2], + type_indices: [i16; 2], + volume: f32, + #[brw(pad_after = 20)] + radius: f32, +} + +impl RaceORamaStreamingDataTreeFace { + pub fn to_tree_face(&self) -> TreeFace { + TreeFace { + volume: self.volume, + vectors: [ + Vector3 { + x: self.vectors[0].x, + y: self.vectors[0].y, + z: self.vectors[0].z, + }, + Vector3 { + x: self.vectors[1].x, + y: self.vectors[1].y, + z: self.vectors[1].z, + }, + ], + type_indices: self.type_indices, + } + } +} + +#[derive(Default, BinRead, BinWrite, Debug)] +struct StreamingDataTreeFace { + volume: f32, + radius: f32, + type_indices: [i16; 2], + vectors: [Vector3; 2], +} + +impl StreamingDataTreeFace { + pub fn to_tree_face(&self) -> TreeFace { + TreeFace { + volume: self.volume, + vectors: self.vectors, + type_indices: self.type_indices, + } + } +} + +#[derive(BinRead, BinWrite, Debug)] +struct TreeFaceLeaf { + dvalue: f32, + vector: Vector3, + vertices: [i16; 3], +} + +#[derive(BinRead, BinWrite, Debug)] +struct RaceORamaStreamingDataTreeFaceLeaf { + vector: Vector4, + dvalue: f32, + #[brw(pad_after = 6)] + vertices: [i16; 3], +} + +impl RaceORamaStreamingDataTreeFaceLeaf { + pub fn to_tree_face_leaf(&self) -> TreeFaceLeaf { + TreeFaceLeaf { + dvalue: self.dvalue, + vector: Vector3 { + x: self.vector.x, + y: self.vector.y, + z: self.vector.z, + }, + vertices: self.vertices, + } + } +} + +#[derive(BinRead, BinWrite, Debug)] +struct StreamingDataTreeFaceLeaf { + vertices: [i16; 3], + unknown1: f32, + unknown2: u16, + vector: Vector3i16, + unknown1_bits: u16, +} +// assert_eq!(std::mem::size_of, 20); + +impl StreamingDataTreeFaceLeaf { + pub fn to_tree_face_leaf(&self, global_vertices: &[Vector3]) -> TreeFaceLeaf { + let vertex = global_vertices.get(self.vertices[0] as usize).unwrap(); + let normal = Vector3 { + x: self.vector.x as f32 / 16384.0_f32, + y: self.vector.y as f32 / 16384.0_f32, + z: self.vector.z as f32 / 16384.0_f32, + }; + let dvalue = -1.0_f32 * (vertex.x * normal.x + vertex.y * normal.y + vertex.z * normal.z); + TreeFaceLeaf { + dvalue, + vector: normal, + vertices: [ + self.vertices[0] as i16, + self.vertices[1] as i16, + self.vertices[2] as i16, + ], + } + } +} + +#[derive(Default, BinRead, BinWrite, Debug)] +struct SoultreeCollisionObject { + temp_cmt: i32, + + obb_data: [f32; 12], + reverse_collision_mode: i32, + vertex_count: u32, + tree_face_count: u32, + tree_face_leaf_count: u32, + quantized: i32, + + // #[br(if(quantized == 1), count = vertex_count)] + // quantized_tree_vertices: Vec, + + // #[br(if(quantized == 0), count = vertex_count)] + // tree_vertices: Vec, + load_normals: i32, + + // #[br(if(load_normals == 1 && quantized == 1), count = vertex_count)] + // quantized_tree_normals: Vec, + + // #[br(if(load_normals == 1 && quantized == 0), count = vertex_count)] + // tree_normals: Vec, + + // #[br(count = tree_face_count)] + // tree_faces: Vec, + top_tree_face: TreeFace, + // #[br(count = tree_face_leaf_count)] + // tree_face_leaves: Vec, +} + +#[derive(BinRead, BinWrite, Debug)] +struct FinitePlaneStruct { + local_vertex_bl: Vector3, + local_vertex_br: Vector3, + local_vertex_tl: Vector3, + local_vertex_tr: Vector3, + plane_normal: Vector3, +} + +#[derive(BinRead, BinWrite, Debug)] +struct StreamingHeirarchyEntry { + object_id: i32, + object: SoultreeCollisionObject, +} + +#[derive(BinRead, Debug)] +pub struct CollisionModel { + magic: u32, + col_type: [u8; 4], + version: i32, + collision_type: CollisionType, + + #[br(if (collision_type == CollisionType::StreamingSoultree))] + object: SoultreeCollisionObject, + + #[br(if (collision_type == CollisionType::StreamingHeirarchy))] + object_count: i32, + #[br(if (collision_type == CollisionType::StreamingHeirarchy))] + reverse_collision_mode: i32, + #[br(if (collision_type == CollisionType::StreamingHeirarchy), count = object_count)] + objects: Vec, + + #[br(if (collision_type == CollisionType::StreamingFinitePlane))] + plane_count: i32, + #[br(if (collision_type == CollisionType::StreamingFinitePlane))] + half: Vector3, + // #[br(if(collision_type == CollisionType::FinitePlane), count = plane_count)] + // planes: Vec, +} + +#[derive(BinRead, Debug)] +pub struct StreamingCollisionModel { + pub model_info: crate::ModelInfo, + + #[br(count = model_info.parameter_count)] + pub parameters: Vec, + + pub collision_model: CollisionModel, +} + +impl std::fmt::Display for StreamingCollisionModel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.model_info.zone != -1 { + write!( + f, + "COL={}.col\nPosition={}\nLookVector={}\nUpVector={}\nZone={}\n", + clean_string(&self.model_info.name), + self.model_info.position, + self.model_info.look_vector, + self.model_info.up_vector, + self.model_info.zone + )?; + } else { + write!( + f, + "COL={}.col\nPosition={}\nLookVector={}\nUpVector={}\n", + clean_string(&self.model_info.name), + self.model_info.position, + self.model_info.look_vector, + self.model_info.up_vector, + )?; + } + Ok(for param in self.parameters.iter() { + write!(f, "{}\n", param)?; + }) + } +} + +// Derive BinrwNamedArgs +#[derive(Clone, Debug)] +pub struct CollisionModelArgs { + pub ror: bool, + pub streaming_data: Vec, +} + +// This BinWrite implementation actually restructures the streaming component data plus the header data in the SOI to form a proper GOL file. +// As such, the streaming data must be passed to write_options. +impl BinWrite for CollisionModel { + type Args<'a> = &'a CollisionModelArgs; + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + args: Self::Args<'_>, + ) -> BinResult<()> { + u32::write_options(&1234, writer, endian, ())?; + self.col_type.write_options(writer, endian, ())?; + i32::write_options(&self.version, writer, endian, ())?; + + let mut offset_in_data: usize = 0; + + let mut cursor = std::io::Cursor::new(&args.streaming_data); + + match self.collision_type { + CollisionType::Soultree => panic!("Unsupported collision type!"), + CollisionType::SoultreeHeirarchy => panic!("Unsupported collision type!"), + CollisionType::Rays => panic!("Unsupported collision type!"), + CollisionType::DynamicRays => panic!("Unsupported collision type!"), + CollisionType::RadiusedLine => panic!("Unsupported collision type!"), + CollisionType::Sphere => panic!("Unsupported collision type!"), + CollisionType::Box => panic!("Unsupported collision type!"), + CollisionType::Ecosystem => panic!("Unsupported collision type!"), + CollisionType::FinitePlane => panic!("Unsupported collision type!"), + CollisionType::StreamingSoultree => { + CollisionType::write_options(&CollisionType::Soultree, writer, endian, ())?; + + i32::write_options(&self.object.temp_cmt, writer, endian, ())?; + self.object.obb_data.write_options(writer, endian, ())?; + i32::write_options(&self.object.reverse_collision_mode, writer, endian, ())?; + u32::write_options(&self.object.vertex_count, writer, endian, ())?; + u32::write_options(&self.object.tree_face_count, writer, endian, ())?; + u32::write_options(&self.object.tree_face_leaf_count, writer, endian, ())?; + i32::write_options(&self.object.quantized, writer, endian, ())?; + + // Conversion from MN's streaming format requires the vertices be cached and used later for DValue calculation. + let mut global_vertices = Vec::new(); + + if self.object.quantized == 0 { + if args.ror { + // Read Vector4s from the data and write out Vector3s, truncating the W component. + cursor.set_position(offset_in_data as u64); + let vertices = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(self.object.vertex_count as usize) + .finalize(), + ) + .unwrap(); + + let mut truncated_vertices = Vec::new(); + for vec in vertices.iter() { + truncated_vertices.push(Vector3 { + x: vec.x, + y: vec.y, + z: vec.z, + }); + } + + Vec::::write_options(&truncated_vertices, writer, endian, ())?; + offset_in_data += 16 * self.object.vertex_count as usize; + } else { + cursor.set_position(offset_in_data as u64); + global_vertices = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(self.object.vertex_count as usize) + .finalize(), + ) + .unwrap(); + + Vec::::write_options(&global_vertices, writer, endian, ())?; + offset_in_data += 12 * self.object.vertex_count as usize; + + // let data = (&args.streaming_data[offset_in_data..offset_in_data + 12 * self.object.vertex_count as usize]).to_vec(); + // Vec::::write_options(&data, writer, options, ())?; + // offset_in_data += 12 * self.object.vertex_count as usize; + } + } + if self.object.quantized == 1 { + if args.ror { + // Read Vector4i16s from the data and write out Vector3i16s, truncating the W component. + cursor.set_position(offset_in_data as u64); + let vertices = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(self.object.vertex_count as usize) + .finalize(), + ) + .unwrap(); + + let mut truncated_vertices = Vec::new(); + for vec in vertices.iter() { + truncated_vertices.push(Vector3i16 { + x: vec.x, + y: vec.y, + z: vec.z, + }); + } + + Vec::::write_options(&truncated_vertices, writer, endian, ())?; + offset_in_data += 8 * self.object.vertex_count as usize; + } else { + cursor.set_position(offset_in_data as u64); + let quantized_vertices = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(self.object.vertex_count as usize) + .finalize(), + ) + .unwrap(); + + Vec::::write_options(&quantized_vertices, writer, endian, ())?; + offset_in_data += 6 * self.object.vertex_count as usize; + + for vec in quantized_vertices.iter() { + global_vertices.push(Vector3 { + x: vec.x as f32 / 16384.0_f32, + y: vec.y as f32 / 16384.0_f32, + z: vec.z as f32 / 16384.0_f32, + }); + } + + // let data = (&args.streaming_data[offset_in_data..offset_in_data + 6 * self.object.vertex_count as usize]).to_vec(); + // Vec::::write_options(&data, writer, options, ())?; + // offset_in_data += 6 * self.object.vertex_count as usize; + } + } + i32::write_options(&self.object.load_normals, writer, endian, ())?; + if self.object.load_normals == 1 { + if self.object.quantized == 0 { + if args.ror { + // Read Vector4s from the data and write out Vector3s, truncating the W component. + cursor.set_position(offset_in_data as u64); + let normals = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(self.object.vertex_count as usize) + .finalize(), + ) + .unwrap(); + + let mut truncated_normals = Vec::new(); + for vec in normals.iter() { + truncated_normals.push(Vector3 { + x: vec.x, + y: vec.y, + z: vec.z, + }); + } + + Vec::::write_options(&truncated_normals, writer, endian, ())?; + offset_in_data += 16 * self.object.vertex_count as usize; + } else { + let data = (&args.streaming_data + [offset_in_data..offset_in_data + 12 * self.object.vertex_count as usize]) + .to_vec(); + Vec::::write_options(&data, writer, endian, ())?; + offset_in_data += 12 * self.object.vertex_count as usize; + } + } + if self.object.quantized == 1 { + if args.ror { + // Read Vector4i16s from the data and write out Vector3i16s, truncating the W component. + cursor.set_position(offset_in_data as u64); + let normals = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(self.object.vertex_count as usize) + .finalize(), + ) + .unwrap(); + + let mut truncated_normals = Vec::new(); + for vec in normals.iter() { + truncated_normals.push(Vector3i16 { + x: vec.x, + y: vec.y, + z: vec.z, + }); + } + + Vec::::write_options(&truncated_normals, writer, endian, ())?; + offset_in_data += 8 * self.object.vertex_count as usize; + } else { + let data = (&args.streaming_data + [offset_in_data..offset_in_data + 6 * self.object.vertex_count as usize]) + .to_vec(); + Vec::::write_options(&data, writer, endian, ())?; + offset_in_data += 6 * self.object.vertex_count as usize; + } + } + } + // Nasty hack... + if args.ror && self.object.vertex_count % 2 == 1 { + offset_in_data += 16; + } + if args.ror { + // Read RaceORamaStreamingDataTreeFace from the data and write out TreeFaces. + cursor.set_position(offset_in_data as u64); + let ror_tree_faces = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(self.object.tree_face_count as usize) + .finalize(), + ) + .unwrap(); + + let mut tree_faces = Vec::new(); + for vec in ror_tree_faces.iter() { + tree_faces.push(vec.to_tree_face()); + } + + Vec::::write_options(&tree_faces, writer, endian, ())?; + offset_in_data += 64 * self.object.tree_face_count as usize; + } else { + // Read StreamingDataTreeFaces from the data and write out TreeFaces. + cursor.set_position(offset_in_data as u64); + let mn_tree_faces = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(self.object.tree_face_count as usize) + .finalize(), + ) + .unwrap(); + + let mut tree_faces = Vec::new(); + for vec in mn_tree_faces.iter() { + tree_faces.push(vec.to_tree_face()); + } + + Vec::::write_options(&tree_faces, writer, endian, ())?; + offset_in_data += 36 * self.object.tree_face_count as usize; + } + if args.ror { + // Read StreamingDataTreeFaceLeaves from the data and write out TreeFaceLeaves. + cursor.set_position(offset_in_data as u64); + let ror_face_leaves = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(self.object.tree_face_leaf_count as usize) + .finalize(), + ) + .unwrap(); + + let mut tree_face_leaves = Vec::new(); + for vec in ror_face_leaves.iter() { + tree_face_leaves.push(vec.to_tree_face_leaf()); + } + + Vec::::write_options(&tree_face_leaves, writer, endian, ())?; + offset_in_data += 32 * self.object.tree_face_leaf_count as usize; + } else { + // Read StreamingDataTreeFaceLeaves from the data and write out TreeFaceLeaves. + cursor.set_position(offset_in_data as u64); + let mn_face_leaves = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(self.object.tree_face_leaf_count as usize) + .finalize(), + ) + .unwrap(); + + let mut tree_face_leaves = Vec::new(); + for vec in mn_face_leaves.iter() { + tree_face_leaves.push(vec.to_tree_face_leaf(&global_vertices)); + } + + Vec::::write_options(&tree_face_leaves, writer, endian, ())?; + offset_in_data += 20 * self.object.tree_face_leaf_count as usize; + } + assert_eq!(offset_in_data, args.streaming_data.len()); + } + CollisionType::StreamingHeirarchy => { + CollisionType::write_options(&CollisionType::SoultreeHeirarchy, writer, endian, ())?; + + i32::write_options(&self.object_count, writer, endian, ())?; + i32::write_options(&self.reverse_collision_mode, writer, endian, ())?; + for object in &self.objects { + i32::write_options(&object.object.temp_cmt, writer, endian, ())?; + object.object.obb_data.write_options(writer, endian, ())?; + i32::write_options(&object.object.reverse_collision_mode, writer, endian, ())?; + u32::write_options(&object.object.vertex_count, writer, endian, ())?; + u32::write_options(&object.object.tree_face_count, writer, endian, ())?; + u32::write_options(&object.object.tree_face_leaf_count, writer, endian, ())?; + i32::write_options(&object.object.quantized, writer, endian, ())?; + + // Conversion from MN's streaming format requires the vertices be cached and used later for DValue calculation. + let mut global_vertices = Vec::new(); + + if object.object.quantized == 0 { + if args.ror { + // Read Vector4s from the data and write out Vector3s, truncating the W component. + cursor.set_position(offset_in_data as u64); + let vertices = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(object.object.vertex_count as usize) + .finalize(), + ) + .unwrap(); + + let mut truncated_vertices = Vec::new(); + for vec in vertices.iter() { + truncated_vertices.push(Vector3 { + x: vec.x, + y: vec.y, + z: vec.z, + }); + } + + Vec::::write_options(&truncated_vertices, writer, endian, ())?; + offset_in_data += 16 * object.object.vertex_count as usize; + } else { + cursor.set_position(offset_in_data as u64); + global_vertices = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(object.object.vertex_count as usize) + .finalize(), + ) + .unwrap(); + + Vec::::write_options(&global_vertices, writer, endian, ())?; + offset_in_data += 12 * object.object.vertex_count as usize; + + // let data = (&args.streaming_data[offset_in_data..offset_in_data + 12 * object.object.vertex_count as usize]).to_vec(); + // Vec::::write_options(&data, writer, options, ())?; + // offset_in_data += 12 * object.object.vertex_count as usize; + } + } + if object.object.quantized == 1 { + if args.ror { + // Read Vector4i16s from the data and write out Vector3i16s, truncating the W component. + cursor.set_position(offset_in_data as u64); + let vertices = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(object.object.vertex_count as usize) + .finalize(), + ) + .unwrap(); + + let mut truncated_vertices = Vec::new(); + for vec in vertices.iter() { + truncated_vertices.push(Vector3i16 { + x: vec.x, + y: vec.y, + z: vec.z, + }); + } + + Vec::::write_options(&truncated_vertices, writer, endian, ())?; + offset_in_data += 8 * object.object.vertex_count as usize; + } else { + cursor.set_position(offset_in_data as u64); + let quantized_vertices = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(object.object.vertex_count as usize) + .finalize(), + ) + .unwrap(); + + Vec::::write_options(&quantized_vertices, writer, endian, ())?; + offset_in_data += 6 * object.object.vertex_count as usize; + + for vec in quantized_vertices.iter() { + global_vertices.push(Vector3 { + x: vec.x as f32 / 16384.0_f32, + y: vec.y as f32 / 16384.0_f32, + z: vec.z as f32 / 16384.0_f32, + }); + } + + // let data = (&args.streaming_data[offset_in_data..offset_in_data + 6 * object.object.vertex_count as usize]).to_vec(); + // Vec::::write_options(&data, writer, options, ())?; + // offset_in_data += 6 * object.object.vertex_count as usize; + } + } + i32::write_options(&object.object.load_normals, writer, endian, ())?; + if object.object.load_normals == 1 { + if object.object.quantized == 0 { + if args.ror { + // Read Vector4s from the data and write out Vector3s, truncating the W component. + cursor.set_position(offset_in_data as u64); + let normals = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(object.object.vertex_count as usize) + .finalize(), + ) + .unwrap(); + + let mut truncated_normals = Vec::new(); + for vec in normals.iter() { + truncated_normals.push(Vector3 { + x: vec.x, + y: vec.y, + z: vec.z, + }); + } + + Vec::::write_options(&truncated_normals, writer, endian, ())?; + offset_in_data += 16 * object.object.vertex_count as usize; + } else { + let data = (&args.streaming_data + [offset_in_data..offset_in_data + 12 * object.object.vertex_count as usize]) + .to_vec(); + Vec::::write_options(&data, writer, endian, ())?; + offset_in_data += 12 * object.object.vertex_count as usize; + } + } + if object.object.quantized == 1 { + if args.ror { + // Read Vector4i16s from the data and write out Vector3i16s, truncating the W component. + cursor.set_position(offset_in_data as u64); + let normals = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(object.object.vertex_count as usize) + .finalize(), + ) + .unwrap(); + + let mut truncated_normals = Vec::new(); + for vec in normals.iter() { + truncated_normals.push(Vector3i16 { + x: vec.x, + y: vec.y, + z: vec.z, + }); + } + + Vec::::write_options(&truncated_normals, writer, endian, ())?; + offset_in_data += 8 * object.object.vertex_count as usize; + } else { + let data = (&args.streaming_data + [offset_in_data..offset_in_data + 6 * object.object.vertex_count as usize]) + .to_vec(); + Vec::::write_options(&data, writer, endian, ())?; + offset_in_data += 6 * object.object.vertex_count as usize; + } + } + } + // Nasty hack... + if args.ror && object.object.vertex_count % 2 == 1 { + offset_in_data += 16; + } + if args.ror { + // Read RaceORamaStreamingDataTreeFace from the data and write out TreeFaces. + cursor.set_position(offset_in_data as u64); + let ror_tree_faces = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(object.object.tree_face_count as usize) + .finalize(), + ) + .unwrap(); + + let mut tree_faces = Vec::new(); + for vec in ror_tree_faces.iter() { + tree_faces.push(vec.to_tree_face()); + } + + Vec::::write_options(&tree_faces, writer, endian, ())?; + offset_in_data += 64 * object.object.tree_face_count as usize; + } else { + // Read StreamingDataTreeFaces from the data and write out TreeFaces. + cursor.set_position(offset_in_data as u64); + let mn_tree_faces = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(object.object.tree_face_count as usize) + .finalize(), + ) + .unwrap(); + + let mut tree_faces = Vec::new(); + for vec in mn_tree_faces.iter() { + tree_faces.push(vec.to_tree_face()); + } + + Vec::::write_options(&tree_faces, writer, endian, ())?; + offset_in_data += 36 * object.object.tree_face_count as usize; + } + if args.ror { + // Read StreamingDataTreeFaceLeaves from the data and write out TreeFaceLeaves. + cursor.set_position(offset_in_data as u64); + let ror_face_leaves = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(object.object.tree_face_leaf_count as usize) + .finalize(), + ) + .unwrap(); + + let mut tree_face_leaves = Vec::new(); + for vec in ror_face_leaves.iter() { + tree_face_leaves.push(vec.to_tree_face_leaf()); + } + + Vec::::write_options(&tree_face_leaves, writer, endian, ())?; + offset_in_data += 32 * object.object.tree_face_leaf_count as usize; + } else { + // Read StreamingDataTreeFaceLeaves from the data and write out TreeFaceLeaves. + cursor.set_position(offset_in_data as u64); + let mn_face_leaves = Vec::::read_options( + &mut cursor, + endian, + binrw::VecArgs::builder() + .count(object.object.tree_face_leaf_count as usize) + .finalize(), + ) + .unwrap(); + + let mut tree_face_leaves = Vec::new(); + for vec in mn_face_leaves.iter() { + tree_face_leaves.push(vec.to_tree_face_leaf(&global_vertices)); + } + + Vec::::write_options(&tree_face_leaves, writer, endian, ())?; + offset_in_data += 20 * object.object.tree_face_leaf_count as usize; + } + assert_eq!(offset_in_data, args.streaming_data.len()); + } + } + CollisionType::StreamingFinitePlane => { + CollisionType::write_options(&CollisionType::FinitePlane, writer, endian, ())?; + + i32::write_options(&self.plane_count, writer, endian, ())?; + Vector3::write_options(&self.half, writer, endian, ())?; + + Vec::::write_options(&args.streaming_data, writer, endian, ())?; + // assert_eq!(self.plane_count as usize * 60, args.streaming_data.len()); + } + } + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 03c18dd..4965899 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,22 @@ +pub use crate::collision::*; +pub use crate::models::*; +pub use crate::motion::*; +pub use crate::res::*; pub use crate::soi::*; pub use crate::soi_soup::*; pub use crate::str::*; +pub use crate::textures::*; pub use crate::toc::*; +pub use crate::utils::*; +mod collision; +mod models; +mod motion; +mod res; mod soi; mod soi_soup; mod str; +mod textures; mod toc; mod utils; diff --git a/src/models/dxg.rs b/src/models/dxg.rs new file mode 100644 index 0000000..7521930 --- /dev/null +++ b/src/models/dxg.rs @@ -0,0 +1,283 @@ +use binrw::{BinRead, BinResult, BinWrite, Endian}; +use std::io::{Seek, Write}; + +use crate::{utils::*, Bone, MeshName}; + +// https://github.com/leeao/carsraceorama/blob/master/carsraceorama/CarsTypes.h#L43 +const D3DVSDT_FLOAT1: u8 = 0x12; // 1D float expanded to (value, 0., 0., 1.) +const D3DVSDT_FLOAT2: u8 = 0x22; // 2D float expanded to (value, value, 0., 1.) +const D3DVSDT_FLOAT3: u8 = 0x32; // 3D float expanded to (value, value, value, 1.) +const D3DVSDT_FLOAT4: u8 = 0x42; // 4D float +const D3DVSDT_D3DCOLOR: u8 = 0x40; // 4D packed unsigned bytes mapped to 0. to 1. range + // Input is in D3DCOLOR format (ARGB) expanded to (R, G, B, A) +const D3DVSDT_SHORT2: u8 = 0x25; // 2D signed short expanded to (value, value, 0., 1.) +const D3DVSDT_SHORT4: u8 = 0x45; // 4D signed short + +// The following are Xbox extensions +const D3DVSDT_NORMSHORT1: u8 = 0x11; // 1D signed, normalized short expanded to (value, 0, 0., 1.) + +// (signed, normalized short maps from -1.0 to 1.0) +const D3DVSDT_NORMSHORT2: u8 = 0x21; // 2D signed, normalized short expanded to (value, value, 0., 1.) +const D3DVSDT_NORMSHORT3: u8 = 0x31; // 3D signed, normalized short expanded to (value, value, value, 1.) +const D3DVSDT_NORMSHORT4: u8 = 0x41; // 4D signed, normalized short expanded to (value, value, value, value) +const D3DVSDT_NORMPACKED3: u8 = 0x16; // 3 signed, normalized components packed in 32-bits. (11,11,10). + +// Each component ranges from -1.0 to 1.0. +// Expanded to (value, value, value, 1.) +const D3DVSDT_SHORT1: u8 = 0x15; // 1D signed short expanded to (value, 0., 0., 1.) + +// Signed shorts map to the range [-32768, 32767] +const D3DVSDT_SHORT3: u8 = 0x35; // 3D signed short expanded to (value, value, value, 1.) +const D3DVSDT_PBYTE1: u8 = 0x14; // 1D packed byte expanded to (value, 0., 0., 1.) + +// Packed bytes map to the range [0, 1] +const D3DVSDT_PBYTE2: u8 = 0x24; // 2D packed byte expanded to (value, value, 0., 1.) +const D3DVSDT_PBYTE3: u8 = 0x34; // 3D packed byte expanded to (value, value, value, 1.) +const D3DVSDT_PBYTE4: u8 = 0x44; // 4D packed byte expanded to (value, value, value, value) +const D3DVSDT_FLOAT2H: u8 = 0x72; // 2D homogeneous float expanded to (value, value,0., value.) + +// Useful for projective texture coordinates. +const D3DVSDT_NONE: u8 = 0x02; // No stream data + +const DEFAULT_VERTEX_FORMATS: [u8; 16] = [ + D3DVSDT_FLOAT3, + D3DVSDT_FLOAT3, + D3DVSDT_D3DCOLOR, + D3DVSDT_FLOAT2, + D3DVSDT_FLOAT1, + D3DVSDT_NONE, + D3DVSDT_NONE, + D3DVSDT_NONE, + D3DVSDT_FLOAT4, + D3DVSDT_FLOAT4, + D3DVSDT_FLOAT2, + D3DVSDT_NONE, + D3DVSDT_NONE, + D3DVSDT_NONE, + D3DVSDT_NONE, + D3DVSDT_NONE, +]; + +#[derive(Default, BinRead, BinWrite, Debug)] +#[brw(little)] +pub struct DXGDeltaBlock { + num_channels: u32, + + #[br(count = 64)] + controller_name: Vec, + + num_vertices: u32, + xyz_bits: u32, + force_unique: u8, + unk: u32, + unk2: u32, + + delta_count: u32, + + #[br(count = delta_count)] + delta_positions: Vec, + + #[br(count = delta_count)] + delta_normals: Vec, + + #[br(count = delta_count)] + delta_indices: Vec, + + #[br(count = num_vertices)] + positions: Vec, + + #[br(count = num_vertices)] + normals: Vec, +} + +#[derive(BinRead, BinWrite, Debug)] +#[brw(little)] +pub struct StreamingDXGMesh { + pub surface_index: u32, + pub vertex_type: u32, + + #[br(if ((vertex_type & 0x2000) == 0x2000))] + pub compression_stuff: Option<[f32; 8]>, + + #[br(if ((vertex_type & 0x2000) == 0x2000))] + pub vertex_formats: Option<[u8; 16]>, + + pub num_texture_coordinate_sets: u8, + pub compressed: u8, + pub streaming: u8, + pub unk: u8, + pub unk2: u8, + pub unk3: u8, + + #[br(count = num_texture_coordinate_sets)] + pub texture_coordinate_sets: Vec, + + pub num_vertices: u16, + pub num_face_indices: u16, + + #[br(if ((vertex_type & 0x100) == 0x100))] + pub delta_block: Option, +} + +#[derive(BinRead, BinWrite, Debug)] +#[brw(little)] +pub struct DXGLod { + pub auto_lod_value: f32, + pub num_meshes: u32, + + #[br(count = num_meshes)] + pub meshes: Vec, +} + +#[derive(BinRead, Debug)] +#[brw(little)] +#[br(magic = b"dgs\0")] +pub struct DXGHeader { + version: i32, + num_bones: u32, + + #[br(count = num_bones)] + bones: Vec, + + num_mesh_names: i32, + + #[br(count = num_mesh_names)] + pub mesh_names: Vec, + + pub num_lod: u8, + skin_animates_flag: u8, + has_weight: u8, + unused: u8, + + #[br(count = num_lod)] + pub lods: Vec, +} + +// BinrwNamedArgs +#[derive(Clone, Debug)] +pub struct DXGHeaderArgs { + pub streaming_data: Vec, +} + +// This BinWrite implementation actually restructures the streaming component data plus the header data in the SOI to form a proper DXG file. +// As such, the streaming data must be passed to write_options. +impl BinWrite for DXGHeader { + type Args<'a> = &'a DXGHeaderArgs; + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + args: Self::Args<'_>, + ) -> BinResult<()> { + let magic = b"dxg\0".to_vec(); + Vec::::write_options(&magic, writer, endian, ())?; + i32::write_options(&self.version, writer, endian, ())?; + u32::write_options(&self.num_bones, writer, endian, ())?; + Vec::::write_options(&self.bones, writer, endian, ())?; + i32::write_options(&self.num_mesh_names, writer, endian, ())?; + Vec::::write_options(&self.mesh_names, writer, endian, ())?; + u8::write_options(&self.num_lod, writer, endian, ())?; + u8::write_options(&self.skin_animates_flag, writer, endian, ())?; + u8::write_options(&self.has_weight, writer, endian, ())?; + u8::write_options(&self.unused, writer, endian, ())?; + + let mut offset_in_data: usize = 0; + + for lod in &self.lods { + f32::write_options(&lod.auto_lod_value, writer, endian, ())?; + u32::write_options(&lod.num_meshes, writer, endian, ())?; + for mesh in &lod.meshes { + u32::write_options(&mesh.surface_index, writer, endian, ())?; + u32::write_options(&mesh.vertex_type, writer, endian, ())?; + if let Some(compression_stuff) = mesh.compression_stuff { + compression_stuff.write_options(writer, endian, ())?; + } + + let vertex_formats = if let Some(vertex_formats) = mesh.vertex_formats { + vertex_formats + } else { + DEFAULT_VERTEX_FORMATS.clone() + }; + if mesh.vertex_formats.is_some() { + vertex_formats.write_options(writer, endian, ())?; + } + + u8::write_options(&mesh.num_texture_coordinate_sets, writer, endian, ())?; + u8::write_options(&mesh.compressed, writer, endian, ())?; + u8::write_options(&0, writer, endian, ())?; + u8::write_options(&mesh.unk, writer, endian, ())?; + u8::write_options(&mesh.unk2, writer, endian, ())?; + u8::write_options(&mesh.unk3, writer, endian, ())?; + Vec::::write_options(&mesh.texture_coordinate_sets, writer, endian, ())?; + + u16::write_options(&mesh.num_vertices, writer, endian, ())?; + u16::write_options(&mesh.num_face_indices, writer, endian, ())?; + + assert!(mesh.streaming == 1); + + let ty = mesh.vertex_type; + let mut offset: usize = mesh.num_face_indices as usize * 2; + + if (ty & 0x01) == 0x01 { + let attribute_size = if vertex_formats[0] == D3DVSDT_FLOAT3 { + mesh.num_vertices as usize * 12 + } else if vertex_formats[0] == D3DVSDT_SHORT3 { + mesh.num_vertices as usize * 6 + } else if vertex_formats[0] == D3DVSDT_PBYTE3 { + mesh.num_vertices as usize * 3 + } else { + 0 + }; + offset += attribute_size; + } + if (ty & 0x02) == 0x02 { + let attribute_size = if vertex_formats[1] == D3DVSDT_FLOAT3 { + mesh.num_vertices as usize * 12 + } else if vertex_formats[1] == D3DVSDT_SHORT3 { + mesh.num_vertices as usize * 6 + } else if vertex_formats[1] == D3DVSDT_PBYTE3 { + mesh.num_vertices as usize * 3 + } else { + 0 + }; + offset += attribute_size; + } + if (ty & 0x08) == 0x08 { + offset += mesh.num_vertices as usize * 4; + } + if (ty & 0x04) == 0x04 { + let attribute_size = if vertex_formats[3] == D3DVSDT_FLOAT2 { + mesh.num_vertices as usize * 8 + } else if vertex_formats[3] == D3DVSDT_SHORT2 { + mesh.num_vertices as usize * 4 + } else if vertex_formats[3] == D3DVSDT_PBYTE2 { + mesh.num_vertices as usize * 2 + } else { + 0 + }; + offset += attribute_size; + } + if (ty & 0x10) == 0x10 { + offset += mesh.num_vertices as usize * 8; + } + if (ty & 0x40) == 0x40 { + offset += mesh.num_vertices as usize * 4; + } + if (ty & 0x1000) == 0x1000 { + offset += mesh.num_vertices as usize * 32; + } + + let data = (&args.streaming_data[offset_in_data..offset_in_data + offset]).to_vec(); + Vec::::write_options(&data, writer, endian, ())?; + + offset_in_data += offset; + + if let Some(delta_block) = &mesh.delta_block { + delta_block.write_options(writer, endian, ())?; + } + } + } + Ok(()) + } +} diff --git a/src/models/gcg.rs b/src/models/gcg.rs new file mode 100644 index 0000000..8990b17 --- /dev/null +++ b/src/models/gcg.rs @@ -0,0 +1,246 @@ +use std::io::{Seek, Write}; + +use binrw::{BinRead, BinResult, BinWrite, Endian}; + +use crate::{Bone, MeshName}; + +#[derive(BinRead, BinWrite, PartialEq, Debug, Clone, Copy)] +#[brw(repr = u8)] +pub enum GXCompType { + GxU8 = 0, + GxS8 = 1, + GxU16 = 2, + GxS16 = 3, + GxF32 = 4, + GxU32 = 5, +} + +#[derive(BinRead, BinWrite, PartialEq, Debug, Clone, Copy)] +#[brw(repr = u8)] +pub enum GXAttrType { + GxNone = 0, + GxDirect = 1, + GxIndex8 = 2, + GxIndex16 = 3, +} + +#[derive(BinRead, BinWrite, Debug)] +#[brw(big)] +pub struct StreamingGCGMesh { + pub surface_index: u32, + pub vertex_type: u8, + + pub vertex_attr_type: GXAttrType, + pub vertex_data_type: GXCompType, + pub xyz_frac_bits: u8, + + #[br(if ((vertex_type & 0x1) == 0x1 && (vertex_type & 0x8) == 0x8))] + pub normal_attr_type: Option, + #[br(if ((vertex_type & 0x1) == 0x1 && (vertex_type & 0x8) == 0x8))] + pub normal_data_type: Option, + + #[br(if ((vertex_type & 0x1) == 0x0))] + pub color_attr_type: Option, + #[br(if ((vertex_type & 0x1) == 0x0))] + pub color_data_type: Option, + + pub uv_attr_type: GXAttrType, + pub uv_data_type: GXCompType, + pub tex_frac_bits: u8, + + pub vertex_count: u16, + pub byte_stride: u8, + + #[br(if ((vertex_type & 0x1) == 0x1 && (vertex_type & 0x8) == 0x8))] + pub normal_count: u16, + #[br(if ((vertex_type & 0x1) == 0x1 && (vertex_type & 0x8) == 0x8))] + pub normal_stride: u8, + + #[br(if ((vertex_type & 0x1) == 0x0))] + pub color_count: u16, + #[br(if ((vertex_type & 0x1) == 0x0))] + pub color_stride: u8, + + pub uv_count: u16, + pub uv_stride: u8, + pub face_chunk_size: u32, +} + +#[derive(BinRead, BinWrite, Debug)] +#[brw(big)] +pub struct GCGLod { + pub auto_lod_value: f32, + pub num_meshes: u32, + + #[br(count = num_meshes)] + pub meshes: Vec, +} + +#[derive(BinRead, BinWrite, Debug, Clone, Copy)] +#[br(big)] +pub struct GCGWeight { + pub bone_id: u16, + pub weight: f32, +} + +#[derive(BinRead, Debug)] +#[br(big)] +#[br(magic = b"ggs\0")] +pub struct GCGHeader { + version: i32, + num_bones: u32, + + #[br(count = num_bones)] + bones: Vec, + + num_mesh_names: i32, + + #[br(count = num_mesh_names)] + pub mesh_names: Vec, + + pub num_lod: u8, + skin_animates_flag: u8, + has_weight: u8, + unused: u8, + + #[br(if(has_weight != 0))] + weight_count: u16, + + #[br(if(has_weight != 0), count = weight_count)] + pub weights: Option>, + + #[br(count = num_lod)] + pub lods: Vec, +} + +// BinrwNamedArgs +#[derive(Clone, Debug)] +pub struct GCGHeaderArgs { + pub streaming_data: Vec, +} + +// This BinWrite implementation actually restructures the streaming component data plus the header data in the SOI to form a proper XNG file. +// As such, the streaming data must be passed to write_options. +impl BinWrite for GCGHeader { + type Args<'a> = &'a GCGHeaderArgs; + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + args: Self::Args<'_>, + ) -> BinResult<()> { + let magic = b"gcg\0".to_vec(); + Vec::::write_options(&magic, writer, endian, ())?; + i32::write_options(&self.version, writer, endian, ())?; + u32::write_options(&self.num_bones, writer, endian, ())?; + Vec::::write_options(&self.bones, writer, endian, ())?; + i32::write_options(&self.num_mesh_names, writer, endian, ())?; + Vec::::write_options(&self.mesh_names, writer, endian, ())?; + u8::write_options(&self.num_lod, writer, endian, ())?; + u8::write_options(&self.skin_animates_flag, writer, endian, ())?; + u8::write_options(&self.has_weight, writer, endian, ())?; + u8::write_options(&self.unused, writer, endian, ())?; + if self.has_weight != 0 { + u16::write_options(&self.weight_count, writer, endian, ())?; + Vec::::write_options(&self.weights.clone().unwrap(), writer, endian, ())?; + } + + let mut offset_in_data: usize = 0; + + for lod in &self.lods { + f32::write_options(&lod.auto_lod_value, writer, endian, ())?; + u32::write_options(&lod.num_meshes, writer, endian, ())?; + for mesh in &lod.meshes { + u32::write_options(&mesh.surface_index, writer, endian, ())?; + u8::write_options(&(mesh.vertex_type - 0x80), writer, endian, ())?; + GXAttrType::write_options(&mesh.vertex_attr_type, writer, endian, ())?; + GXCompType::write_options(&mesh.vertex_data_type, writer, endian, ())?; + u8::write_options(&mesh.xyz_frac_bits, writer, endian, ())?; + + if (mesh.vertex_type & 0x1) == 0x1 && (mesh.vertex_type & 0x8) == 0x8 { + GXAttrType::write_options(&mesh.normal_attr_type.unwrap(), writer, endian, ())?; + GXCompType::write_options(&mesh.normal_data_type.unwrap(), writer, endian, ())?; + } + + if (mesh.vertex_type & 0x1) == 0x0 { + GXAttrType::write_options(&mesh.color_attr_type.unwrap(), writer, endian, ())?; + GXCompType::write_options(&mesh.color_data_type.unwrap(), writer, endian, ())?; + } + + GXAttrType::write_options(&mesh.uv_attr_type, writer, endian, ())?; + GXCompType::write_options(&mesh.uv_data_type, writer, endian, ())?; + u8::write_options(&mesh.tex_frac_bits, writer, endian, ())?; + + u16::write_options(&mesh.vertex_count, writer, endian, ())?; + u8::write_options(&mesh.byte_stride, writer, endian, ())?; + + if (mesh.vertex_type & 0x1) == 0x1 && (mesh.vertex_type & 0x8) == 0x8 { + u16::write_options(&mesh.normal_count, writer, endian, ())?; + u8::write_options(&mesh.normal_stride, writer, endian, ())?; + } + + if (mesh.vertex_type & 0x1) == 0x0 { + u16::write_options(&mesh.color_count, writer, endian, ())?; + u8::write_options(&mesh.color_stride, writer, endian, ())?; + } + + u16::write_options(&mesh.uv_count, writer, endian, ())?; + u8::write_options(&mesh.uv_stride, writer, endian, ())?; + + u32::write_options(&mesh.face_chunk_size, writer, endian, ())?; + let mut vertex_block_size = 0; + + vertex_block_size += match mesh.vertex_data_type { + GXCompType::GxU8 => mesh.vertex_count as usize * 3, + GXCompType::GxS8 => mesh.vertex_count as usize * 3, + GXCompType::GxU16 => mesh.vertex_count as usize * 6, + GXCompType::GxS16 => mesh.vertex_count as usize * 6, + GXCompType::GxF32 => mesh.vertex_count as usize * 12, + GXCompType::GxU32 => mesh.vertex_count as usize * 12, + }; + + if (mesh.vertex_type & 0x1) == 0x1 && (mesh.vertex_type & 0x8) == 0x8 { + vertex_block_size += match mesh.normal_data_type { + Some(GXCompType::GxU8) => mesh.normal_count as usize * 3, + Some(GXCompType::GxS8) => mesh.normal_count as usize * 3, + Some(GXCompType::GxU16) => mesh.normal_count as usize * 6, + Some(GXCompType::GxS16) => mesh.normal_count as usize * 6, + Some(GXCompType::GxF32) => mesh.normal_count as usize * 12, + Some(GXCompType::GxU32) => mesh.normal_count as usize * 12, + None => 0, + }; + } + + if (mesh.vertex_type & 0x1) == 0x0 { + vertex_block_size += 4 * mesh.color_count as usize; + } + + if (mesh.vertex_type & 0x2) == 0x2 { + vertex_block_size += match mesh.uv_data_type { + GXCompType::GxU8 => mesh.uv_count as usize * 2, + GXCompType::GxS8 => mesh.uv_count as usize * 2, + GXCompType::GxU16 => mesh.uv_count as usize * 4, + GXCompType::GxS16 => mesh.uv_count as usize * 4, + GXCompType::GxF32 => mesh.uv_count as usize * 8, + GXCompType::GxU32 => mesh.uv_count as usize * 8, + }; + } + + // write the vertex block (positions, normals or vertex colors, uvs) + let vertex_block = + (&args.streaming_data[offset_in_data..offset_in_data + vertex_block_size]).to_vec(); + Vec::::write_options(&vertex_block, writer, endian, ())?; + offset_in_data = crate::round_up(offset_in_data + vertex_block_size, 32); + + let face_chunk: Vec = (&args.streaming_data + [offset_in_data..offset_in_data + mesh.face_chunk_size as usize]) + .to_vec(); + Vec::::write_options(&face_chunk, writer, endian, ())?; + offset_in_data += mesh.face_chunk_size as usize; + } + assert_eq!(offset_in_data, args.streaming_data.len()); + } + Ok(()) + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..6f3ffb8 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,65 @@ +mod dxg; +mod gcg; +mod xng; +use binrw::BinRead; +use binrw::BinWrite; + +use crate::clean_string; + +pub use self::dxg::*; +pub use self::gcg::*; +pub use self::xng::*; + +#[derive(BinRead, Debug)] +pub struct StreamingRenderableModel = ()> + 'static> { + pub model_info: crate::ModelInfo, + + #[br(count = model_info.parameter_count)] + pub parameters: Vec, + + pub streaming_model_header: MH, +} + +impl = ()>> std::fmt::Display for StreamingRenderableModel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.model_info.zone != -1 { + write!( + f, + "SLT={}\nPosition={}\nLookVector={}\nUpVector={}\nZone={}\n", + clean_string(&self.model_info.name), + self.model_info.position, + self.model_info.look_vector, + self.model_info.up_vector, + self.model_info.zone + )?; + } else { + write!( + f, + "SLT={}\nPosition={}\nLookVector={}\nUpVector={}\n", + clean_string(&self.model_info.name), + self.model_info.position, + self.model_info.look_vector, + self.model_info.up_vector, + )?; + } + Ok(for param in self.parameters.iter() { + write!(f, "{}\n", param)?; + }) + } +} + +#[derive(BinRead, BinWrite, Debug)] +pub struct MeshName { + #[br(count = 64)] + name: Vec, +} + +#[derive(BinRead, BinWrite, Debug)] +pub struct Bone { + name: [u8; 128], + matrix: [f32; 16], + bounding_box_center: [f32; 3], + bounding_box_half: [f32; 3], + bounding_box_radius: f32, + parent_index: u32, +} diff --git a/src/models/xng.rs b/src/models/xng.rs new file mode 100644 index 0000000..5f68a30 --- /dev/null +++ b/src/models/xng.rs @@ -0,0 +1,199 @@ +use binrw::{BinRead, BinResult, BinWrite, Endian}; +use std::io::{Seek, Write}; + +use crate::{utils::*, Bone, MeshName}; + +#[derive(Default, BinRead, BinWrite, Debug)] +#[brw(big)] +pub struct XNGDeltaBlock { + num_channels: u32, + + #[br(count = 64)] + controller_name: Vec, + + num_vertices: u32, + xyz_bits: u32, + force_unique: u8, + unk: u32, + unk2: u32, + + delta_count: u32, + + #[br(count = delta_count)] + delta_positions: Vec, + + #[br(count = delta_count)] + delta_normals: Vec, + + #[br(count = delta_count)] + delta_indices: Vec, + + #[br(count = num_vertices)] + positions: Vec, + + #[br(count = num_vertices)] + normals: Vec, +} + +#[derive(BinRead, BinWrite, Debug)] +#[brw(big)] +pub struct StreamingXNGMesh { + pub surface_index: u32, + pub vertex_type: u32, + + #[br(if ((vertex_type & 0x2000) == 0x2000))] + pub compression_stuff: Option<[f32; 8]>, + + pub num_texture_coordinate_sets: u8, + pub compressed: u8, + pub streaming: u8, + pub unk: u8, + pub unk2: u8, + pub unk3: u8, + + #[br(count = num_texture_coordinate_sets)] + pub texture_coordinate_sets: Vec, + + pub num_vertices: u16, + pub num_face_indices: u16, + + #[br(if ((vertex_type & 0x100) == 0x100))] + pub delta_block: Option, +} + +#[derive(BinRead, BinWrite, Debug)] +#[brw(big)] +pub struct XNGLod { + pub auto_lod_value: f32, + pub num_meshes: u32, + + #[br(count = num_meshes)] + pub meshes: Vec, +} + +#[derive(BinRead, Debug)] +#[br(big)] +#[br(magic = b"xgs\0")] +pub struct XNGHeader { + version: i32, + num_bones: u32, + + #[br(count = num_bones)] + bones: Vec, + + num_mesh_names: i32, + + #[br(count = num_mesh_names)] + pub mesh_names: Vec, + + pub num_lod: u8, + skin_animates_flag: u8, + has_weight: u8, + unused: u8, + + #[br(count = num_lod)] + pub lods: Vec, +} + +// BinrwNamedArgs +#[derive(Clone, Debug)] +pub struct XNGHeaderArgs { + pub streaming_data: Vec, +} + +// This BinWrite implementation actually restructures the streaming component data plus the header data in the SOI to form a proper XNG file. +// As such, the streaming data must be passed to write_options. +impl BinWrite for XNGHeader { + type Args<'a> = &'a XNGHeaderArgs; + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + args: Self::Args<'_>, + ) -> BinResult<()> { + let magic = b"xng\0".to_vec(); + Vec::::write_options(&magic, writer, endian, ())?; + i32::write_options(&self.version, writer, endian, ())?; + u32::write_options(&self.num_bones, writer, endian, ())?; + Vec::::write_options(&self.bones, writer, endian, ())?; + i32::write_options(&self.num_mesh_names, writer, endian, ())?; + Vec::::write_options(&self.mesh_names, writer, endian, ())?; + u8::write_options(&self.num_lod, writer, endian, ())?; + u8::write_options(&self.skin_animates_flag, writer, endian, ())?; + u8::write_options(&self.has_weight, writer, endian, ())?; + u8::write_options(&self.unused, writer, endian, ())?; + + let mut offset_in_data: usize = 0; + + for lod in &self.lods { + f32::write_options(&lod.auto_lod_value, writer, endian, ())?; + u32::write_options(&lod.num_meshes, writer, endian, ())?; + for mesh in &lod.meshes { + u32::write_options(&mesh.surface_index, writer, endian, ())?; + u32::write_options(&mesh.vertex_type, writer, endian, ())?; + if let Some(compression_stuff) = mesh.compression_stuff { + compression_stuff.write_options(writer, endian, ())?; + } + u8::write_options(&mesh.num_texture_coordinate_sets, writer, endian, ())?; + u8::write_options(&mesh.compressed, writer, endian, ())?; + u8::write_options(&0, writer, endian, ())?; + u8::write_options(&mesh.unk, writer, endian, ())?; + u8::write_options(&mesh.unk2, writer, endian, ())?; + u8::write_options(&mesh.unk3, writer, endian, ())?; + Vec::::write_options(&mesh.texture_coordinate_sets, writer, endian, ())?; + + u16::write_options(&mesh.num_vertices, writer, endian, ())?; + u16::write_options(&mesh.num_face_indices, writer, endian, ())?; + + assert!(mesh.streaming == 1); + + let ty = mesh.vertex_type; + let mut offset: usize = mesh.num_face_indices as usize * 2; + if mesh.num_face_indices % 2 == 1 { + offset += 2; + } + if (ty & 0x01) == 0x01 { + offset += mesh.num_vertices as usize * 12; + } + if (ty & 0x02) == 0x02 { + offset += mesh.num_vertices as usize * 12; + } + if (ty & 0x08) == 0x08 { + offset += mesh.num_vertices as usize * 4; + } + if (ty & 0x04) == 0x04 { + offset += mesh.num_vertices as usize * 8; + } + if (ty & 0x40) == 0x40 { + offset += mesh.num_vertices as usize * 4; + } + if (ty & 0x1000) == 0x1000 { + offset += mesh.num_vertices as usize * 32; + } + if (ty & 0x10) == 0x10 { + offset += mesh.num_vertices as usize * 8; + } + if (ty & 0x4000) == 0x4000 { + offset += mesh.num_vertices as usize * 8; + } + if (ty & 0x8000) == 0x8000 { + offset += mesh.num_vertices as usize * 8; + } + if (ty & 0x20) == 0x20 { + offset += mesh.num_vertices as usize * 12; + } + + let data = (&args.streaming_data[offset_in_data..offset_in_data + offset]).to_vec(); + Vec::::write_options(&data, writer, endian, ())?; + + offset_in_data += offset; + + if let Some(delta_block) = &mesh.delta_block { + delta_block.write_options(writer, endian, ())?; + } + } + } + Ok(()) + } +} diff --git a/src/motion.rs b/src/motion.rs new file mode 100644 index 0000000..add503e --- /dev/null +++ b/src/motion.rs @@ -0,0 +1,42 @@ +use binrw::{BinRead, BinWrite}; + +#[derive(BinRead, BinWrite, Debug)] +struct MotionPackString { + len: u32, + + #[br(count = len)] + bone_name: Vec, +} + +#[derive(BinRead, BinWrite, Debug)] +pub struct StreamingMotionPackHeader { + version: i32, + motion_type: i32, + frame_count: u32, + object_count: u32, + + #[br(count = object_count)] + bone_targets: Vec, + + duration: f32, + rotation_type: u32, + position_type: u32, + num_positions: u32, + num_rotations: u32, + num_camera_infos: u32, + padding: u32, + + #[br(if (version == - 8))] + #[bw(ignore)] + unk_bpb_size: u32, + + #[br(if (version == - 8), count = unk_bpb_size)] + #[bw(ignore)] + unk_bpb: Vec, +} + +#[derive(BinRead, Debug)] +pub struct StreamingMotionPack { + pub model_info: crate::ModelInfo, + pub header: StreamingMotionPackHeader, +} diff --git a/src/res.rs b/src/res.rs new file mode 100644 index 0000000..26787ff --- /dev/null +++ b/src/res.rs @@ -0,0 +1,99 @@ +use std::{collections::HashMap, fs::File, io::Read, path::Path}; + +use binrw::{BinRead, BinReaderExt, BinResult}; +use flate2::read::ZlibDecoder; + +use crate::clean_path; + +#[derive(BinRead)] +pub struct OffsetEntry { + pub name_len: u32, + + #[br(count = name_len)] + pub name: Vec, + + pub start_offset: u32, + pub size: u32, +} + +#[derive(BinRead)] +pub struct Header { + pub version: u32, + pub header_size: u32, + + #[br(if(version == 3))] + pub resource_file_type_flags: Option, + + pub user_data_type: u32, + + /// Size in bytes + pub sector_list_size: u32, + + #[br(count = sector_list_size / 2)] + pub sector_list: Vec, + + #[br(count = header_size - (8 + sector_list_size + (resource_file_type_flags.is_some() as u32 * 4)))] + pub user_data: Vec, + + /// Actual length + pub offset_table_len: u32, + /// Size in bytes + pub offset_table_size: u32, + + #[br(count = offset_table_len)] + pub offset_table: Vec, +} + +pub struct Res { + res_file_header: Header, + files: HashMap>, +} + +impl Res { + pub fn read(path: &Path) -> BinResult { + let mut file = File::open(path)?; + Self::read_file(&mut file) + } + + pub fn read_file(file: &mut File) -> BinResult { + let res_file_header: Header = file.read_le()?; + let mut files = HashMap::new(); + let mut compressed_data = Vec::new(); + file.read_to_end(&mut compressed_data).unwrap(); + + let mut current_offset = 0; + while compressed_data[current_offset] == 0 { + current_offset += 1; + } + + for offset_entry in res_file_header.offset_table.iter() { + let path = clean_path(&offset_entry.name); + + let mut decompressed_data = Vec::new(); + let mut decoder = ZlibDecoder::new(&compressed_data[current_offset..]); + decoder.read_to_end(&mut decompressed_data)?; + + // advance the offset in the compressed data buffer by the size that the decoder read while decompressing this specific file. + current_offset += decoder.total_in() as usize; + + // a dummy 0xFF byte is sometimes placed so the game knows when a file has been read, so we check for it and advance the offset accordingly. + if compressed_data[current_offset] == 0xFF { + current_offset += 1; + } + + files.insert(path, decompressed_data); + } + Ok(Self { + res_file_header, + files, + }) + } + + pub fn get_file(&self, path: String) -> Option<&[u8]> { + if self.files.contains_key(&path) { + Some(&self.files[&path]) + } else { + None + } + } +} diff --git a/src/soi.rs b/src/soi.rs index cb14898..c8b00f8 100644 --- a/src/soi.rs +++ b/src/soi.rs @@ -1,8 +1,14 @@ use std::fs::File; use std::path::Path; +use binrw::Endian; use binrw::{BinRead, BinReaderExt, BinResult}; +use crate::collision::*; +use crate::models::*; +use crate::motion::*; +use crate::utils::*; + #[derive(BinRead, PartialEq, Debug)] #[br(repr = i32)] enum StreamingMode { @@ -13,8 +19,8 @@ enum StreamingMode { } #[derive(BinRead, Debug)] -struct Header { - version: i32, +pub struct Header { + pub version: i32, flags: i32, sections: i32, @@ -37,32 +43,60 @@ struct Header { } #[derive(BinRead, Debug)] -struct ModelInfo { +pub struct ModelInfo { flags: i32, - position: [f32; 4], - look_vector: [f32; 4], - up_vector: [f32; 4], - is_animated: i32, + pub position: Vector4, + pub look_vector: Vector4, + pub up_vector: Vector4, + pub is_animated: i32, section_id: i32, component_id: i32, + pub name: [u8; 260], + + pub zone: i32, + pub parameter_count: i32, +} + +#[derive(BinRead, Debug)] +pub struct StreamingParameter { name: [u8; 260], + value: [u8; 260], +} - zone: i32, - parameter_count: i32, +impl std::fmt::Display for StreamingParameter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}={}", + clean_string(&self.name), + clean_string(&self.value) + ) + } } #[derive(BinRead, Debug)] -struct StreamingTexture = ()>> { - model_info: ModelInfo, - // might be something, currently only padding - padding: u32, - header: TH, +pub struct StreamingTexture = ()> + 'static> { + pub model_info: ModelInfo, + // padding on xbox, version on wii + pub version: u32, + pub header: StreamingTH, } #[derive(BinRead, Debug)] -pub struct Soi = ()> + 'static> { - header: Header, +pub struct StaticTexture = ()> + 'static> { + pub model_info: ModelInfo, + + pub static_texture_header: StaticTH, +} + +#[derive(BinRead, Debug)] +pub struct Soi< + StreamingTH: BinRead = ()> + 'static, + StaticTH: BinRead = ()> + 'static, + MH: BinRead = ()> + 'static, +> { + pub header: Header, #[br(count = header.uncached_pages)] uncached_page_sizes: Vec, @@ -71,31 +105,136 @@ pub struct Soi = ()> + 'static> { cached_page_sizes: Vec, #[br(count = header.streaming_textures)] - streaming_textures: Vec>, - // #[br(count = header.static_textures)] - // static_textures: Vec, + streaming_textures: Vec>, + + #[br(count = header.static_textures)] + static_textures: Vec>, - // #[br(count = header.motion_packs)] - // motion_packs: Vec, + #[br(count = header.motion_packs)] + motion_packs: Vec, + // #[br(if(header.flags & 64 == 1))] + // collision_grid_info: StreamingCollisionGridInfo, + #[br(count = header.renderable_models)] + renderable_models: Vec>, + + #[br(count = header.collision_models)] + collision_models: Vec, } -impl = ()>> Soi { - pub fn read(path: &Path) -> BinResult { +impl< + StreamingTH: BinRead = ()>, + StaticTH: BinRead = ()>, + MH: BinRead = ()>, + > Soi +{ + pub fn read(path: &Path, endian: Endian) -> BinResult { let mut file = File::open(path)?; - Self::read_file(&mut file) + Self::read_file(&mut file, endian) + } + + pub fn read_file(file: &mut File, endian: Endian) -> BinResult { + file.read_type(endian) } - pub fn read_file(file: &mut File) -> BinResult { - file.read_be() + pub fn get_streaming_textures(&self) -> &[StreamingTexture] { + return &self.streaming_textures; + } + + pub fn get_static_textures(&self) -> &[StaticTexture] { + return &self.static_textures; + } + + pub fn get_motion_packs(&self) -> &[StreamingMotionPack] { + return &self.motion_packs; + } + + pub fn get_renderable_models(&self) -> &[StreamingRenderableModel] { + return &self.renderable_models; + } + + pub fn get_collision_models(&self) -> &[StreamingCollisionModel] { + return &self.collision_models; + } + + pub fn find_static_texture( + &self, + section_id: u32, + component_id: u32, + ) -> Option<&StaticTexture> { + for texture in &self.static_textures { + let model_info = &texture.model_info; + if model_info.section_id == section_id as i32 + && model_info.component_id == component_id as i32 + { + return Some(&texture); + } + } + + None } - pub fn find_texture_header(&self, section_id: u32, component_id: u32) -> Option<&TH> { + pub fn find_streaming_texture( + &self, + section_id: u32, + component_id: u32, + ) -> Option<&StreamingTexture> { for texture in &self.streaming_textures { let model_info = &texture.model_info; if model_info.section_id == section_id as i32 && model_info.component_id == component_id as i32 { - return Some(&texture.header); + return Some(&texture); + } + } + + None + } + + pub fn find_motion_pack( + &self, + section_id: u32, + component_id: u32, + ) -> Option<&StreamingMotionPack> { + for motion_pack in &self.motion_packs { + let model_info = &motion_pack.model_info; + if model_info.section_id == section_id as i32 + && model_info.component_id == component_id as i32 + { + return Some(&motion_pack); + } + } + + None + } + + pub fn find_collision_model( + &self, + section_id: u32, + component_id: u32, + ) -> Option<&StreamingCollisionModel> { + for collision_model in &self.collision_models { + let model_info = &collision_model.model_info; + if model_info.section_id == section_id as i32 + && model_info.component_id == component_id as i32 + { + return Some(&collision_model); + } + } + + None + } + + pub fn find_model( + &self, + section_id: u32, + component_id: u32, + ) -> Option<&StreamingRenderableModel> { + for model in &self.renderable_models { + let model_info = &model.model_info; + if model_info.section_id == section_id as i32 + && model_info.component_id == component_id as i32 + { + return Some(&model); } } diff --git a/src/soi_soup.rs b/src/soi_soup.rs index fe28eba..f275099 100644 --- a/src/soi_soup.rs +++ b/src/soi_soup.rs @@ -1,18 +1,30 @@ use std::path::Path; -use binrw::{BinRead, BinResult}; - -use crate::{ComponentHeader, Section, Soi, Toc}; - -pub struct SoiSoup = ()> + 'static> { +use binrw::{BinRead, BinResult, Endian}; + +use crate::{ + ComponentHeader, Section, Soi, StaticTexture, StreamingCollisionModel, StreamingMotionPack, + StreamingRenderableModel, StreamingTexture, Toc, X360StaticTextureHeader, XNGHeader, +}; + +pub struct SoiSoup< + StreamingTH: BinRead = ()> + 'static, + StaticTH: BinRead = ()> + 'static, + MH: BinRead = ()> + 'static, +> { toc: Toc, - soi: Soi, + soi: Soi, } -impl = ()>> SoiSoup { - pub fn cook(toc_path: &Path, soi_path: &Path) -> BinResult { - let toc = Toc::read(toc_path)?; - let soi = Soi::read(soi_path)?; +impl< + StreamingTH: BinRead = ()>, + StaticTH: BinRead = ()>, + MH: BinRead = ()>, + > SoiSoup +{ + pub fn cook(toc_path: &Path, soi_path: &Path, endian: Endian) -> BinResult { + let soi = Soi::read(soi_path, endian)?; + let toc = Toc::read(toc_path, endian, soi.header.version == 0x101)?; Ok(Self { toc, soi }) } @@ -39,6 +51,26 @@ impl = ()>> SoiSoup { components } + pub fn streaming_textures(&self) -> &[StreamingTexture] { + self.soi.get_streaming_textures() + } + + pub fn static_textures(&self) -> &[StaticTexture] { + self.soi.get_static_textures() + } + + pub fn motion_packs(&self) -> &[StreamingMotionPack] { + self.soi.get_motion_packs() + } + + pub fn renderable_models(&self) -> &[StreamingRenderableModel] { + self.soi.get_renderable_models() + } + + pub fn collision_models(&self) -> &[StreamingCollisionModel] { + self.soi.get_collision_models() + } + pub fn component_count(&self) -> u32 { let mut sum = 0; @@ -49,17 +81,73 @@ impl = ()>> SoiSoup { sum as u32 } - pub fn find_texture_header( + pub fn find_static_texture( + &self, + section_id: u32, + component_id: u32, + instance_id: u32, + ) -> Option<&StaticTexture> { + if let Some(header) = self.soi.find_static_texture(section_id, component_id) { + return Some(header); + } + + let (section_id, component_id) = self.toc.find_ids(instance_id)?; + self.soi.find_static_texture(section_id, component_id) + } + + pub fn find_streaming_texture( + &self, + section_id: u32, + component_id: u32, + instance_id: u32, + ) -> Option<&StreamingTexture> { + if let Some(header) = self.soi.find_streaming_texture(section_id, component_id) { + return Some(header); + } + + let (section_id, component_id) = self.toc.find_ids(instance_id)?; + self.soi.find_streaming_texture(section_id, component_id) + } + + pub fn find_motion_pack( + &self, + section_id: u32, + component_id: u32, + instance_id: u32, + ) -> Option<&StreamingMotionPack> { + if let Some(header) = self.soi.find_motion_pack(section_id, component_id) { + return Some(header); + } + + let (section_id, component_id) = self.toc.find_ids(instance_id)?; + self.soi.find_motion_pack(section_id, component_id) + } + + pub fn find_collision_model( + &self, + section_id: u32, + component_id: u32, + instance_id: u32, + ) -> Option<&StreamingCollisionModel> { + if let Some(header) = self.soi.find_collision_model(section_id, component_id) { + return Some(header); + } + + let (section_id, component_id) = self.toc.find_ids(instance_id)?; + self.soi.find_collision_model(section_id, component_id) + } + + pub fn find_model( &self, section_id: u32, component_id: u32, instance_id: u32, - ) -> Option<&TH> { - if let Some(header) = self.soi.find_texture_header(section_id, component_id) { + ) -> Option<&StreamingRenderableModel> { + if let Some(header) = self.soi.find_model(section_id, component_id) { return Some(header); } let (section_id, component_id) = self.toc.find_ids(instance_id)?; - self.soi.find_texture_header(section_id, component_id) + self.soi.find_model(section_id, component_id) } } diff --git a/src/str.rs b/src/str.rs index 8d006b3..f077dc2 100644 --- a/src/str.rs +++ b/src/str.rs @@ -40,22 +40,48 @@ impl Str { pub fn read_section_data(&mut self, section: &Section) -> io::Result { let header = §ion.header; - let zlib = &header.zlib_header; - - let section_offset = header.memory_entry.offset as u64; - self.file.seek(SeekFrom::Start(section_offset))?; - - let uncached = { - let data = self.decode_zlib_data(header.uncached_data_size as usize, &zlib.uncached_sizes)?; - extract_components(§ion.uncached_components, data) - }; - - let cached = { - let data = self.decode_zlib_data(header.cached_data_size as usize, &zlib.cached_sizes)?; - extract_components(§ion.cached_components, data) - }; - - Ok(SectionData { uncached, cached }) + if let Some(zlib) = &header.zlib_header { + let section_offset = header.memory_entry.offset as u64; + self.file.seek(SeekFrom::Start(section_offset))?; + + if !zlib.cached_sizes.is_empty() && !zlib.uncached_sizes.is_empty() { + let uncached = { + let data = + self.decode_zlib_data(header.uncached_data_size as usize, &zlib.uncached_sizes)?; + extract_components(§ion.uncached_components, data) + }; + + let cached = { + let data = self.decode_zlib_data(header.cached_data_size as usize, &zlib.cached_sizes)?; + extract_components(§ion.cached_components, data) + }; + + Ok(SectionData { uncached, cached }) + } else { + let mut uncached_data = vec![0u8; header.uncached_data_size as usize]; + self.file.read(&mut uncached_data)?; + let uncached = extract_components(§ion.uncached_components, uncached_data); + + let mut cached_data = vec![0u8; header.cached_data_size as usize]; + self.file.read(&mut cached_data)?; + let cached = extract_components(§ion.cached_components, cached_data); + + Ok(SectionData { uncached, cached }) + } + } else { + let section_offset = header.memory_entry.offset as u64; + self.file.seek(SeekFrom::Start(section_offset))?; + + let mut uncached_data = vec![0u8; header.uncached_data_size as usize]; + self.file.read(&mut uncached_data)?; + let uncached = extract_components(§ion.uncached_components, uncached_data); + + let mut cached_data = vec![0u8; header.cached_data_size as usize]; + self.file.read(&mut cached_data)?; + let cached = extract_components(§ion.cached_components, cached_data); + + Ok(SectionData { uncached, cached }) + } } pub fn decode_zlib_data( diff --git a/src/test.rs b/src/test.rs index 2824227..ef5401a 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,72 +1,549 @@ -use std::fs::{create_dir_all, File}; +use std::io::Write; use std::path::{Path, PathBuf}; -use x_flipper_360::{Config, Format, TextureFormat, TextureHeader, TextureSize2D}; +use binrw::BinWrite; +use x_flipper_360::*; -use crate::ComponentKind::Texture; -use crate::{ComponentData, SoiSoup, Str}; +use crate::textures::{GCNTextureHeader, GCTSurfaceHeader}; +use crate::ComponentKind::{self, *}; +use crate::{ + utils, DXGHeader, DXGHeaderArgs, DXTStaticTextureHeader, DXTTextureHeader, GCGHeader, + GCGHeaderArgs, GCNStaticTextureHeader, Res, X360StaticTextureHeader, XNGHeader, +}; +use crate::{CollisionModelArgs, ComponentData, SoiSoup, Str, XNGHeaderArgs}; + +pub type XboxSoiSoup = SoiSoup; +pub type X360SoiSoup = SoiSoup; +pub type WiiSoiSoup = SoiSoup; #[test] fn extract() { - let toc_path = Path::new("data/CR_03.x360.toc"); - let soi_path = Path::new("data/CR_03.x360.soi"); - let str_path = Path::new("data/CR_03.x360.str"); + let res_path = Path::new("./data/FE.xbox.res"); + let str_path = Path::new("./data/FE.xbox.str"); + + let res = Res::read(res_path).unwrap(); + + let toc_path = Path::new("./data/FE.xbox.toc"); + let soi_path = Path::new("./data/FE.xbox.soi"); - let soup = SoiSoup::cook(toc_path, soi_path).unwrap(); + let toc_bytes = res.get_file("FE.xbox.toc".to_owned()).unwrap(); + std::fs::File::create(toc_path) + .unwrap() + .write_all(toc_bytes) + .unwrap(); + let toc_bytes = res.get_file("FE.xbox.soi".to_owned()).unwrap(); + std::fs::File::create(soi_path) + .unwrap() + .write_all(toc_bytes) + .unwrap(); + + let soup = XboxSoiSoup::cook(toc_path, soi_path, binrw::Endian::Little).unwrap(); let mut str = Str::read(str_path).unwrap(); for (id, section) in soup.find_sections().iter().enumerate() { let section_data = str.read_section_data(section).unwrap(); for component in section_data.uncached { - process_component(&soup, id as u32, component); + process_component_xbox(&soup, id as u32, component); } for component in section_data.cached { - process_component(&soup, id as u32, component); + process_component_xbox(&soup, id as u32, component); + } + } +} + +#[test] +fn dump_scn() { + let res_path = Path::new("./data/FE.xbox.res"); + let str_path = Path::new("./data/FE.xbox.str"); + + let res = Res::read(res_path).unwrap(); + + let toc_path = Path::new("./data/FE.xbox.toc"); + let soi_path = Path::new("./data/FE.xbox.soi"); + + let toc_bytes = res.get_file("FE.xbox.toc".to_owned()).unwrap(); + std::fs::File::create(toc_path) + .unwrap() + .write_all(toc_bytes) + .unwrap(); + let toc_bytes = res.get_file("FE.xbox.soi".to_owned()).unwrap(); + std::fs::File::create(soi_path) + .unwrap() + .write_all(toc_bytes) + .unwrap(); + + let soup = XboxSoiSoup::cook(toc_path, soi_path, binrw::Endian::Little).unwrap(); + let mut str = Str::read(str_path).unwrap(); + let mut num_anim_models = 1; + let mut num_static_models = 1; + let mut num_objects = 1; + for (id, section) in soup.find_sections().iter().enumerate() { + let section_data = str.read_section_data(section).unwrap(); + + for component in section_data.uncached { + print_component( + &soup, + id as u32, + component, + &mut num_anim_models, + &mut num_static_models, + &mut num_objects, + ); + } + + for component in section_data.cached { + print_component( + &soup, + id as u32, + component, + &mut num_anim_models, + &mut num_static_models, + &mut num_objects, + ); + } + } +} + +fn print_component< + StreamingTH: binrw::BinRead = ()> + 'static, + StaticTH: binrw::BinRead = ()> + 'static, + MH: binrw::BinRead = ()> + 'static, +>( + soup: &SoiSoup, + section_id: u32, + component: ComponentData, + num_anim_models: &mut i32, + num_static_models: &mut i32, + num_objects: &mut i32, +) { + match component.kind { + RenderableModel => { + let header = soup + .find_model(section_id, component.id, component.instance_id) + .unwrap(); + if header.model_info.is_animated == 1 { + println!("[AnimatedModel{}]\n{}", num_anim_models, header); + *num_anim_models = *num_anim_models + 1; + } else { + println!("[Model{}]\n{}", num_static_models, header); + *num_static_models = *num_static_models + 1; + } + } + Texture => { + // println!("found Texture component kind; skipping..."); + } + CollisionModel => { + let header = soup + .find_collision_model(section_id, component.id, component.instance_id) + .unwrap(); + println!("[Object{}]\n{}", num_objects, header); + *num_objects = *num_objects + 1; + } + UserData => { + // println!("found UserData component kind; skipping..."); + } + MotionPack => { + // println!("found MotionPack component kind; skipping..."); + } + CollisionGrid => { + // println!("found CollisionGrid component kind; skipping..."); + } + } +} + +fn process_component_wii(soup: &WiiSoiSoup, section_id: u32, component: ComponentData) { + if component.kind == ComponentKind::MotionPack { + let header = soup + .find_motion_pack(section_id, component.id, component.instance_id) + .unwrap(); + + let path = PathBuf::from(format!("./data/FE/{}.got", component.path)); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut out = std::fs::File::create(path).unwrap(); + header.header.write_be(&mut out).unwrap(); + out.write_all(&component.data).unwrap(); + } + + if component.kind == ComponentKind::RenderableModel { + let header = soup + .find_model(section_id, component.id, component.instance_id) + .unwrap(); + + let path = PathBuf::from(format!("./data/FE/{}.gcg", component.path)); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut out = std::fs::File::create(path).unwrap(); + header + .streaming_model_header + .write_options( + &mut out, + binrw::Endian::Big, + &GCGHeaderArgs { + streaming_data: component.data.clone(), + }, + ) + .unwrap(); + } + if component.kind == ComponentKind::CollisionModel { + let header = soup + .find_collision_model(section_id, component.id, component.instance_id) + .unwrap(); + + let path = PathBuf::from(format!("./data/FE/{}.gol", component.path)); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut out = std::fs::File::create(path).unwrap(); + header + .collision_model + .write_options( + &mut out, + binrw::Endian::Big, + &CollisionModelArgs { + ror: true, + streaming_data: component.data.clone(), + }, + ) + .unwrap(); + } + if component.kind == ComponentKind::Texture { + match soup.find_streaming_texture(section_id, component.id, component.instance_id) { + Some(streaming_texture) => { + let path = PathBuf::from(format!("./data/FE/{}.gct", component.path)); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut out = std::fs::File::create(path).unwrap(); + + // Write the version out (streamed GCTs have a flag OR'd on the version, so we set it manually to 2 [otherwise we would use streaming_texture.version]) + u32::write_options(&2, &mut out, binrw::Endian::Big, ()).unwrap(); + streaming_texture.header.write_be(&mut out).unwrap(); + + // Keeps track of our position in the streaming data. + let mut offset = 0; + let mip_count = streaming_texture.header.mip_count; + + // Holds the offsets and sizes (in a Range) for each mip so we can iterate backwards over this later + let mut mips = Vec::with_capacity(mip_count as usize); + + // Since mips are stored in forwards order (biggest to smallest) in the streaming data we need to first collect them in the vec, + for i in 0..mip_count { + let mip_width = 1.max(streaming_texture.header.width as usize / (2 as usize).pow(i)); + let mip_height = 1.max(streaming_texture.header.height as usize / (2 as usize).pow(i)); + + let mip_size = streaming_texture + .header + .format + .calculate_mip_size(mip_width, mip_height); + + mips.push(offset..offset + mip_size); + + offset += mip_size as usize; + } + + // (At this point, we should have read through the entire streaming data) + assert_eq!(offset, streaming_texture.header.calculate_image_size()); + assert_eq!(offset, component.data.len()); + + // and we can then terate over the mips in backwards order and write them to the GCT. + for i in (0..mip_count).rev() { + let mip_width = 1.max(streaming_texture.header.width as usize / (2 as usize).pow(i)); + let mip_height = 1.max(streaming_texture.header.height as usize / (2 as usize).pow(i)); + + let surface_header = GCTSurfaceHeader { + width: mip_width as u32, + height: mip_height as u32, + size: streaming_texture + .header + .format + .calculate_mip_size(mip_width, mip_height) as u32, + }; + surface_header.write_be(&mut out).unwrap(); + + component.data[mips.get(i as usize).unwrap().clone()] + .write(&mut out) + .unwrap(); + } + } + None => { + panic!("Failed to find texture header."); + } } } } -fn process_component(soup: &SoiSoup, section_id: u32, component: ComponentData) { - if component.kind != Texture { - return; +fn process_component_xbox(soup: &XboxSoiSoup, section_id: u32, component: ComponentData) { + if component.kind == ComponentKind::MotionPack { + let header = soup + .find_motion_pack(section_id, component.id, component.instance_id) + .unwrap(); + + let path = PathBuf::from(format!("./data/FE/{}.mot", component.path)); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut out = std::fs::File::create(path).unwrap(); + header.header.write_le(&mut out).unwrap(); + out.write_all(&component.data).unwrap(); + } + + if component.kind == ComponentKind::RenderableModel { + let header = soup + .find_model(section_id, component.id, component.instance_id) + .unwrap(); + + let path = PathBuf::from(format!("./data/FE/{}.dxg", component.path)); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut out = std::fs::File::create(path).unwrap(); + header + .streaming_model_header + .write_options( + &mut out, + binrw::Endian::Little, + &DXGHeaderArgs { + streaming_data: component.data.clone(), + }, + ) + .unwrap(); } + if component.kind == ComponentKind::CollisionModel { + let header = soup + .find_collision_model(section_id, component.id, component.instance_id) + .unwrap(); + + let path = PathBuf::from(format!("./data/FE/{}.col", component.path)); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut out = std::fs::File::create(path).unwrap(); + header + .collision_model + .write_options( + &mut out, + binrw::Endian::Little, + &CollisionModelArgs { + ror: false, + streaming_data: component.data.clone(), + }, + ) + .unwrap(); + } + if component.kind == ComponentKind::Texture { + match soup.find_streaming_texture(section_id, component.id, component.instance_id) { + Some(streaming_texture) => { + let path = PathBuf::from(format!("./data/FE/{}.dxt", component.path)); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut out = std::fs::File::create(path).unwrap(); + + // Write the version out (streamed GCTs have a flag OR'd on the version, so we set it manually to 2 [otherwise we would use streaming_texture.version]) + u32::write_options(&2, &mut out, binrw::Endian::Little, ()).unwrap(); + streaming_texture.header.write_le(&mut out).unwrap(); + + // Keeps track of our position in the streaming data. + let mut offset = 0; + let mip_count = streaming_texture.header.mip_count; + + // Holds the offsets and sizes (in a Range) for each mip so we can iterate backwards over this later + let mut mips = Vec::with_capacity(mip_count as usize); + + // Since mips are stored in forwards order (biggest to smallest) in the streaming data we need to first collect them in the vec, + for i in 0..mip_count { + let mip_width = 1.max(streaming_texture.header.width as usize / (2 as usize).pow(i)); + let mip_height = 1.max(streaming_texture.header.height as usize / (2 as usize).pow(i)); + + let mip_size = streaming_texture + .header + .format + .calculate_mip_size(mip_width, mip_height); + + mips.push(offset..offset + mip_size); + + offset += mip_size as usize; + } + + // (At this point, we should have read through the entire streaming data) + assert_eq!(offset, streaming_texture.header.calculate_image_size()); + assert_eq!(offset, component.data.len()); + + // and we can then terate over the mips in backwards order and write them to the GCT. + for i in (0..mip_count).rev() { + let mip_width = 1.max(streaming_texture.header.width as usize / (2 as usize).pow(i)); + let mip_height = 1.max(streaming_texture.header.height as usize / (2 as usize).pow(i)); + + let surface_header = GCTSurfaceHeader { + width: mip_width as u32, + height: mip_height as u32, + size: streaming_texture + .header + .format + .calculate_mip_size(mip_width, mip_height) as u32, + }; + surface_header.write_le(&mut out).unwrap(); - let header = match soup.find_texture_header(section_id, component.id, component.instance_id) { - Some(header) => header, - None => panic!("can not find texture header by section and component id nor instance id"), - }; - - let metadata = header.metadata(); - println!("{} {:?}", component.path, metadata.format()); - - let texture_size: TextureSize2D = - TextureSize2D::from_bytes(metadata.texture_size().to_le_bytes()); - - let config = Config { - width: texture_size.width() as u32 + 1, - height: texture_size.height() as u32 + 1, - depth: None, - pitch: metadata.pitch() as u32, - tiled: metadata.tiled(), - packed_mips: metadata.packed_mips(), - format: match metadata.format() { - TextureFormat::Dxt1 => Format::Dxt1, - TextureFormat::Dxt4_5 => Format::Dxt5, - _ => Format::RGBA8, - }, - mipmap_levels: Some(1.max(metadata.max_mip_level() - metadata.min_mip_level()) as u32), - base_address: metadata.base_address(), - mip_address: metadata.mip_address(), - }; - - let path = PathBuf::from(format!( - "./data/out/{}.dds", - component.path.replace("\\", "/") - )); - create_dir_all(path.parent().unwrap()).unwrap(); - let mut out = File::create(path).unwrap(); - - x_flipper_360::convert_to_dds(&config, &component.data, &mut out).unwrap(); + component.data[mips.get(i as usize).unwrap().clone()] + .write(&mut out) + .unwrap(); + } + } + None => { + panic!("Failed to find texture header."); + } + } + } +} + +fn process_component_xbox360(soup: &X360SoiSoup, section_id: u32, component: ComponentData) { + if component.kind == ComponentKind::MotionPack { + let header = soup + .find_motion_pack(section_id, component.id, component.instance_id) + .unwrap(); + + let path = PathBuf::from(format!("./data/FE/{}.got", component.path)); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut out = std::fs::File::create(path).unwrap(); + header.header.write_be(&mut out).unwrap(); + out.write_all(&component.data).unwrap(); + } + + if component.kind == ComponentKind::RenderableModel { + let header = soup + .find_model(section_id, component.id, component.instance_id) + .unwrap(); + + let path = PathBuf::from(format!("./data/FE/{}.xng", component.path)); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut out = std::fs::File::create(path).unwrap(); + header + .streaming_model_header + .write_options( + &mut out, + binrw::Endian::Big, + &XNGHeaderArgs { + streaming_data: component.data.clone(), + }, + ) + .unwrap(); + } + if component.kind == ComponentKind::CollisionModel { + let header = soup + .find_collision_model(section_id, component.id, component.instance_id) + .unwrap(); + + let path = PathBuf::from(format!("./data/FE/{}.gol", component.path)); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut out = std::fs::File::create(path).unwrap(); + header + .collision_model + .write_options( + &mut out, + binrw::Endian::Big, + &CollisionModelArgs { + ror: false, + streaming_data: component.data.clone(), + }, + ) + .unwrap(); + } + if component.kind == ComponentKind::Texture { + match soup.find_streaming_texture(section_id, component.id, component.instance_id) { + Some(header) => { + let metadata = header.header.metadata(); + + match metadata.format() { + TextureFormat::Dxt1 => { + let texture_size: TextureSize2D = + TextureSize2D::from_bytes(metadata.texture_size().to_le_bytes()); + + let config = Config { + width: texture_size.width() as u32 + 1, + height: texture_size.height() as u32 + 1, + depth: None, + pitch: metadata.pitch() as u32, + tiled: metadata.tiled(), + packed_mips: metadata.packed_mips(), + format: Format::Dxt1, + mipmap_levels: Some(1.max(metadata.max_mip_level() - metadata.min_mip_level()) as u32), + base_address: metadata.base_address(), + mip_address: metadata.mip_address(), + }; + + let path = PathBuf::from(format!("./data/FE/{}.dds", component.path)); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut out = std::fs::File::create(path).unwrap(); + x_flipper_360::convert_to_dds(&config, &component.data, &mut out).unwrap(); + } + TextureFormat::Dxt4_5 => { + let texture_size: TextureSize2D = + TextureSize2D::from_bytes(metadata.texture_size().to_le_bytes()); + + let config = Config { + width: texture_size.width() as u32 + 1, + height: texture_size.height() as u32 + 1, + depth: None, + pitch: metadata.pitch() as u32, + tiled: metadata.tiled(), + packed_mips: metadata.packed_mips(), + format: Format::Dxt5, + mipmap_levels: Some(1.max(metadata.max_mip_level() - metadata.min_mip_level()) as u32), + base_address: metadata.base_address(), + mip_address: metadata.mip_address(), + }; + + let path = PathBuf::from(format!("./data/FE/{}.dds", component.path)); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut out = std::fs::File::create(path).unwrap(); + x_flipper_360::convert_to_dds(&config, &component.data, &mut out).unwrap(); + } + TextureFormat::Dxt2_3 => { + let texture_size: TextureSize2D = + TextureSize2D::from_bytes(metadata.texture_size().to_le_bytes()); + + let config = Config { + width: texture_size.width() as u32 + 1, + height: texture_size.height() as u32 + 1, + depth: None, + pitch: metadata.pitch() as u32, + tiled: metadata.tiled(), + packed_mips: metadata.packed_mips(), + format: Format::Dxt3, + mipmap_levels: Some(1.max(metadata.max_mip_level() - metadata.min_mip_level()) as u32), + base_address: metadata.base_address(), + mip_address: metadata.mip_address(), + }; + + let path = PathBuf::from(format!("./data/FE/{}.dds", component.path)); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut out = std::fs::File::create(path).unwrap(); + x_flipper_360::convert_to_dds(&config, &component.data, &mut out).unwrap(); + } + _ => { + let texture_size: TextureSize2D = + TextureSize2D::from_bytes(metadata.texture_size().to_le_bytes()); + + let config = Config { + width: texture_size.width() as u32 + 1, + height: texture_size.height() as u32 + 1, + depth: None, + pitch: metadata.pitch() as u32, + tiled: metadata.tiled(), + packed_mips: metadata.packed_mips(), + format: Format::RGBA8, + mipmap_levels: Some(1.max(metadata.max_mip_level() - metadata.min_mip_level()) as u32), + base_address: metadata.base_address(), + mip_address: metadata.mip_address(), + }; + + let path = PathBuf::from(format!("./data/FE/{}.dds", component.path)); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut out = std::fs::File::create(path).unwrap(); + x_flipper_360::convert_to_dds(&config, &component.data, &mut out).unwrap(); + } + } + } + None => match soup.find_static_texture(section_id, component.id, component.instance_id) { + Some(static_texture) => { + let path = PathBuf::from(format!("./data/FE/{}.dds", component.path)); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut out = std::fs::File::create(path).unwrap(); + out + .write_all(&static_texture.static_texture_header.header_file) + .unwrap(); + } + None => println!("Failed to find texture header."), + }, + } + } } diff --git a/src/textures/dds.rs b/src/textures/dds.rs new file mode 100644 index 0000000..7a86759 --- /dev/null +++ b/src/textures/dds.rs @@ -0,0 +1,8 @@ +use binrw::BinRead; + +#[derive(BinRead, Debug)] +pub struct X360StaticTextureHeader { + pub dds_size: u32, + #[br(count = dds_size)] + pub header_file: Vec, +} diff --git a/src/textures/dxt.rs b/src/textures/dxt.rs new file mode 100644 index 0000000..c94a937 --- /dev/null +++ b/src/textures/dxt.rs @@ -0,0 +1,140 @@ +use binrw::*; + +use crate::div_round_up; + +#[derive(BinRead, BinWrite, PartialEq, Debug, Clone)] +#[brw(repr = u32)] +pub enum DXTFormat { + Dxt1 = 37, + Dxt1Mm = 38, + Dxt2 = 47, + Dxt2Mm = 48, + Dxt3 = 49, + Dxt3Mm = 50, + Dxt4 = 51, + Dxt4Mm = 52, + Dxt5 = 53, + Dxt5Mm = 54, + Pal8 = 55, + Pal8Mm = 56, + Lum8 = 57, +} + +impl DXTFormat { + pub fn get_block_dim(&self) -> (usize, usize) { + match self { + DXTFormat::Pal8 => (1, 1), + DXTFormat::Pal8Mm => (1, 1), + DXTFormat::Lum8 => (1, 1), + _ => (4, 4), + } + } + pub fn get_bits_per_pixel(&self) -> usize { + match self { + DXTFormat::Dxt1 => 4, + DXTFormat::Dxt1Mm => 4, + DXTFormat::Dxt2 => 8, + DXTFormat::Dxt2Mm => 8, + DXTFormat::Dxt3 => 8, + DXTFormat::Dxt3Mm => 8, + DXTFormat::Dxt4 => 4, + DXTFormat::Dxt4Mm => 4, + DXTFormat::Dxt5 => 8, + DXTFormat::Dxt5Mm => 8, + DXTFormat::Pal8 => 8, + DXTFormat::Pal8Mm => 8, + DXTFormat::Lum8 => 8, + } + } + pub fn calculate_mip_size(&self, width: usize, height: usize) -> usize { + let (blk_width_pixels, blk_height_pixels) = self.get_block_dim(); + let bits_per_pixel = self.get_bits_per_pixel(); + let blk_size_bytes = ((blk_width_pixels * blk_height_pixels) * bits_per_pixel) / 8; + let size_bytes = div_round_up(width, blk_width_pixels) + * div_round_up(height, blk_height_pixels) + * blk_size_bytes; + + size_bytes + } +} + +impl std::fmt::Display for DXTFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DXTFormat::Dxt1 | DXTFormat::Dxt1Mm => write!(f, "Dxt1"), + DXTFormat::Dxt2 | DXTFormat::Dxt2Mm => write!(f, "Dxt2"), + DXTFormat::Dxt3 | DXTFormat::Dxt3Mm => write!(f, "Dxt3"), + DXTFormat::Dxt4 | DXTFormat::Dxt4Mm => write!(f, "Dxt4"), + DXTFormat::Dxt5 | DXTFormat::Dxt5Mm => write!(f, "Dxt5"), + DXTFormat::Pal8 | DXTFormat::Pal8Mm => write!(f, "Pal8"), + DXTFormat::Lum8 => write!(f, "Lum8"), + } + } +} + +#[derive(BinRead, BinWrite, Debug, Clone)] +pub struct DXTTextureHeader { + pub format: DXTFormat, + pub palette_size: u32, + + #[br(count = palette_size * 2)] + pub palette: Vec, + + pub mip_count: u32, + pub width: u32, + pub height: u32, +} + +#[derive(BinRead, BinWrite, Debug)] +pub struct DXTSurfaceHeader { + pub width: u32, + pub height: u32, + pub size: u32, +} + +impl DXTTextureHeader { + pub fn calculate_image_size(&self) -> usize { + let (blk_width_pixels, blk_height_pixels) = self.format.get_block_dim(); + let bits_per_pixel = self.format.get_bits_per_pixel(); + let blk_size_bytes = ((blk_width_pixels * blk_height_pixels) * bits_per_pixel) / 8; + let mut size_bytes = div_round_up(self.width as usize, blk_width_pixels) + * div_round_up(self.height as usize, blk_height_pixels) + * blk_size_bytes; + + for i in 1..self.mip_count { + let mip_width = self.width as usize / (2 as usize).pow(i); + let mip_height = self.height as usize / (2 as usize).pow(i); + + size_bytes += div_round_up(mip_width as usize, blk_width_pixels) + * div_round_up(mip_height as usize, blk_height_pixels) + * blk_size_bytes; + } + + size_bytes + } +} + +#[derive(BinRead, BinWrite)] +pub struct DXTSurface { + pub header: DXTSurfaceHeader, + + #[br(count = header.size)] + pub data: Vec, +} + +#[derive(BinRead, BinWrite)] +pub struct DXTStaticTextureHeader { + pub version: u32, + pub format: DXTFormat, + pub palette_size: u32, + + #[br(count = palette_size * 2)] + pub palette: Vec, + + pub mip_count: u32, + pub width: u32, + pub height: u32, + + #[br(count = mip_count)] + pub mips: Vec, +} diff --git a/src/textures/gct.rs b/src/textures/gct.rs new file mode 100644 index 0000000..7e0eadb --- /dev/null +++ b/src/textures/gct.rs @@ -0,0 +1,125 @@ +use binrw::*; + +use crate::div_round_up; + +#[derive(BinRead, BinWrite, PartialEq, Debug, Clone)] +#[brw(repr = u32)] +pub enum GCTFormat { + Rgba8 = 0x0F, + Cmpr = 0x29, + CmprMm = 0x2A, + Ci8 = 0x3A, + Ci8Mm = 0x3B, + I8 = 0x3C, +} + +impl GCTFormat { + pub fn get_block_dim(&self) -> (usize, usize) { + match self { + GCTFormat::Rgba8 => (4, 4), + GCTFormat::Cmpr => (8, 8), + GCTFormat::CmprMm => (8, 8), + GCTFormat::Ci8 => (8, 4), + GCTFormat::Ci8Mm => (8, 4), + GCTFormat::I8 => (8, 4), + } + } + pub fn get_bits_per_pixel(&self) -> usize { + match self { + GCTFormat::Rgba8 => 32, + GCTFormat::Cmpr => 4, + GCTFormat::CmprMm => 4, + GCTFormat::Ci8 => 8, + GCTFormat::Ci8Mm => 8, + GCTFormat::I8 => 8, + } + } + pub fn calculate_mip_size(&self, width: usize, height: usize) -> usize { + let (blk_width_pixels, blk_height_pixels) = self.get_block_dim(); + let bits_per_pixel = self.get_bits_per_pixel(); + let blk_size_bytes = ((blk_width_pixels * blk_height_pixels) * bits_per_pixel) / 8; + let size_bytes = div_round_up(width, blk_width_pixels) + * div_round_up(height, blk_height_pixels) + * blk_size_bytes; + + size_bytes + } +} + +impl std::fmt::Display for GCTFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GCTFormat::Rgba8 => write!(f, "Rgba8"), + GCTFormat::Cmpr | GCTFormat::CmprMm => write!(f, "Cmpr"), + GCTFormat::Ci8 | GCTFormat::Ci8Mm => write!(f, "Ci8"), + GCTFormat::I8 => write!(f, "I8"), + } + } +} + +#[derive(BinRead, BinWrite, Debug, Clone)] +pub struct GCNTextureHeader { + pub format: GCTFormat, + pub palette_size: u32, + + #[br(count = palette_size * 2)] + pub palette: Vec, + + pub mip_count: u32, + pub width: u32, + pub height: u32, +} + +#[derive(BinRead, BinWrite, Debug)] +pub struct GCTSurfaceHeader { + pub width: u32, + pub height: u32, + pub size: u32, +} + +impl GCNTextureHeader { + pub fn calculate_image_size(&self) -> usize { + let (blk_width_pixels, blk_height_pixels) = self.format.get_block_dim(); + let bits_per_pixel = self.format.get_bits_per_pixel(); + let blk_size_bytes = ((blk_width_pixels * blk_height_pixels) * bits_per_pixel) / 8; + let mut size_bytes = div_round_up(self.width as usize, blk_width_pixels) + * div_round_up(self.height as usize, blk_height_pixels) + * blk_size_bytes; + + for i in 1..self.mip_count { + let mip_width = self.width as usize / (2 as usize).pow(i); + let mip_height = self.height as usize / (2 as usize).pow(i); + + size_bytes += div_round_up(mip_width as usize, blk_width_pixels) + * div_round_up(mip_height as usize, blk_height_pixels) + * blk_size_bytes; + } + + size_bytes + } +} + +#[derive(BinRead, BinWrite)] +pub struct GCTSurface { + pub header: GCTSurfaceHeader, + + #[br(count = header.size)] + pub data: Vec, +} + +#[derive(BinRead, BinWrite)] +pub struct GCNStaticTextureHeader { + pub version: u32, + pub format: GCTFormat, + pub palette_size: u32, + + #[br(count = palette_size * 2)] + pub palette: Vec, + + pub mip_count: u32, + pub width: u32, + pub height: u32, + + #[br(count = mip_count)] + pub mips: Vec, +} diff --git a/src/textures/mod.rs b/src/textures/mod.rs new file mode 100644 index 0000000..b6ab637 --- /dev/null +++ b/src/textures/mod.rs @@ -0,0 +1,6 @@ +mod dds; +mod dxt; +mod gct; +pub use self::dds::*; +pub use self::dxt::*; +pub use self::gct::*; diff --git a/src/toc.rs b/src/toc.rs index 5a527ff..4e978d0 100644 --- a/src/toc.rs +++ b/src/toc.rs @@ -2,7 +2,7 @@ use std::fs::File; use std::io::Seek; use std::path::Path; -use binrw::{BinRead, BinReaderExt, BinResult}; +use binrw::{BinRead, BinResult, Endian}; use crate::utils::clean_path; @@ -35,7 +35,7 @@ pub enum ComponentKind { CollisionGrid, } -#[derive(BinRead, Debug)] +#[derive(Default, BinRead, Debug)] pub(crate) struct ZlibHeader { pub(crate) uncached_total_size: i32, pub(crate) cached_total_size: i32, @@ -51,6 +51,7 @@ pub(crate) struct ZlibHeader { } #[derive(BinRead, Debug)] +#[br(import{read_zlib_header: bool})] pub struct SectionHeader { pub name: [u8; 260], @@ -70,7 +71,8 @@ pub struct SectionHeader { pub(crate) uncached_data_size: i32, pub(crate) cached_data_size: i32, - pub(crate) zlib_header: ZlibHeader, + #[br(if(read_zlib_header))] + pub(crate) zlib_header: Option, } #[derive(BinRead, Debug)] @@ -84,7 +86,9 @@ pub struct ComponentHeader { } #[derive(BinRead, Debug)] +#[br(import{read_zlib_header: bool})] pub struct Section { + #[br(args { read_zlib_header: read_zlib_header })] pub header: SectionHeader, #[br(count = header.uncached_component_count)] @@ -100,24 +104,26 @@ pub struct Toc { } impl Toc { - pub fn read(path: &Path) -> BinResult { + pub fn read(path: &Path, endian: Endian, is_new: bool) -> BinResult { let mut file = File::open(path)?; - Self::read_file(&mut file) + Self::read_file(&mut file, endian, is_new) } - pub fn read_file(file: &mut File) -> BinResult { + pub fn read_file(file: &mut File, endian: Endian, is_new: bool) -> BinResult { let mut sections = Vec::new(); let file_size = file.metadata()?.len(); // read sections until the end of the file is reached while file.stream_position()? < file_size { - let section = file.read_be()?; + // hack that allows newer SOI packages to load + let read_zlib_header = !is_new; + let section = Section::read_options(file, endian, binrw::args! {read_zlib_header})?; sections.push(section); } Ok(Self { sections }) } - + pub fn find_section(&self, id: u32) -> Option<&Section> { self.sections.get(id as usize) } diff --git a/src/utils.rs b/src/utils.rs index 1c4fab6..b3121a4 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,8 +1,75 @@ +use binrw::{BinRead, BinWrite}; + +#[derive(Default, BinRead, BinWrite, Debug)] +pub struct Vector4 { + pub x: f32, + pub y: f32, + pub z: f32, + pub w: f32, +} + const NULL_BYTE: u8 = b'\0'; const BACKSLASH: u8 = b'\\'; const SLASH: char = '/'; -pub(crate) fn clean_path(input: &[u8]) -> String { +impl std::fmt::Display for Vector4 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{},{},{},{}", self.x, self.y, self.z, self.w) + } +} + +#[derive(Default, BinRead, BinWrite, Debug, Clone, Copy)] +pub struct Vector3 { + pub x: f32, + pub y: f32, + pub z: f32, +} + +impl std::fmt::Display for Vector3 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{},{},{}", self.x, self.y, self.z) + } +} + +#[derive(Default, BinRead, BinWrite, Debug)] +#[brw(big)] +pub struct Vector2 { + pub x: f32, + pub y: f32, +} + +impl std::fmt::Display for Vector2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{},{}", self.x, self.y) + } +} + +#[derive(BinRead, BinWrite, Debug)] +#[brw(big)] +pub struct Vector3i16 { + pub x: i16, + pub y: i16, + pub z: i16, +} + +#[derive(BinRead, BinWrite, Debug)] +#[brw(big)] +pub struct Vector4i16 { + pub x: i16, + pub y: i16, + pub z: i16, + pub w: i16, +} + +pub fn round_up(num_to_round: usize, round_to: usize) -> usize { + return ((num_to_round + round_to - 1) / round_to) * round_to; +} + +pub fn div_round_up(num_to_round: usize, round_to: usize) -> usize { + return (num_to_round + round_to - 1) / round_to; +} + +pub fn clean_path(input: &[u8]) -> String { let mut output = String::new(); for c in input { @@ -19,3 +86,16 @@ pub(crate) fn clean_path(input: &[u8]) -> String { output } + +pub fn clean_string(input: &[u8]) -> String { + let mut output = Vec::new(); + + for c in input { + if c == &NULL_BYTE { + return std::str::from_utf8(&output).unwrap().to_owned(); + } + output.push(*c) + } + + std::str::from_utf8(&output).unwrap().to_owned() +}