feat: initialise workspace — Milestone 1 asset explorer

Three-crate Cargo workspace structured per PROJECT.md spec:
- crates/sylpheed-formats  — Xbox 360 format parsers (no Bevy)
- crates/sylpheed-viewer   — Bevy 0.15 asset viewer + egui UI
- crates/sylpheed-cli      — CLI tools (extract/list/sniff/texture)

Milestone 1 features:
- XISO disc image reading via xdvdfs 0.8
- XPR2 texture container parsing + Morton de-tiling
- D3DFORMAT → wgpu TextureFormat mapping (DXT1/3/5, DXN, ARGB)
- Custom Bevy AssetLoader for .xpr files
- Orbit camera (LMB orbit, RMB pan, scroll zoom)
- egui file browser + RE notes panel
- CLI: extract / list / sniff / texture info / texture export
- GitHub Actions CI (Linux, macOS, Windows, WASM)
- Trunk WASM build config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-03-25 21:04:07 +01:00
commit f8127e73b0
23 changed files with 8107 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
//! Mesh format parsing — TO BE REVERSE ENGINEERED.
//!
//! Project Sylpheed uses a completely custom engine with unknown mesh formats.
//! This module is a scaffold: populate these structs as you discover the
//! actual binary layout using a hex editor (010 Editor) and Ghidra.
//!
//! ## RE Strategy for Meshes
//!
//! 1. Extract the game files using xdvdfs
//! 2. Look for files with extensions like .mdl, .mesh, .geo, .obj, .pak
//! 3. Open them in a hex editor — look for:
//! - Repeating patterns of 12 bytes (XYZ float vertices)
//! - Groups of 3 uint16s (triangle indices)
//! - Header magic bytes
//! 4. Cross-reference with Ghidra's XEX analysis to find the load functions
//!
//! ## Useful tools
//! - 010 Editor with binary templates
//! - Noesis (can preview many console formats)
//! - binrw (this project) for writing the parser once the format is known
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MeshError {
#[error("Unknown mesh magic: {0:?}")]
UnknownMagic([u8; 4]),
#[error("Unsupported mesh version: {0}")]
UnsupportedVersion(u32),
#[error("Parse error: {0}")]
Parse(String),
}
/// A decoded 3D mesh ready for Bevy.
/// Vertex positions, normals, UVs, and indices.
#[derive(Debug, Default, Clone)]
pub struct GameMesh {
/// Interleaved vertex positions [x, y, z, x, y, z, ...]
pub positions: Vec<[f32; 3]>,
/// Vertex normals [nx, ny, nz, ...]
pub normals: Vec<[f32; 3]>,
/// UV texture coordinates [u, v, ...]
pub uvs: Vec<[f32; 2]>,
/// Triangle list indices
pub indices: Vec<u32>,
/// Name of this mesh (if available in the file)
pub name: Option<String>,
}
impl GameMesh {
/// Parse a mesh from raw bytes.
///
/// TODO: implement once the actual file format is identified.
/// Currently returns an error until the format is reverse engineered.
pub fn from_bytes(_bytes: &[u8]) -> Result<Vec<Self>, MeshError> {
// ┌──────────────────────────────────────────────────────────────┐
// │ REVERSE ENGINEERING TODO │
// │ │
// │ Steps to implement this: │
// │ 1. Find mesh files in the extraction (look for .mdl etc.) │
// │ 2. Identify the file format using identify_format() in vfs │
// │ 3. Use 010 Editor to map the binary structure │
// │ 4. Add binrw #[derive(BinRead)] structs above │
// │ 5. Implement this function to parse and return meshes │
// └──────────────────────────────────────────────────────────────┘
Err(MeshError::Parse(
"Mesh format not yet reverse engineered. \
See RE TODO in src/mesh.rs".to_string()
))
}
}