use std::{hash::Hash, io::Cursor, path::Path};
use animation::Animation;
use binrw::{BinRead, BinReaderExt};
use error::{LoadModelError, LoadModelLegacyError};
use glam::{Mat4, Vec3};
use indexmap::IndexMap;
use material::{create_materials, create_materials_samplers_legacy};
use model::import::{ModelFilesV40, ModelFilesV111, ModelFilesV112};
use shader_database::ShaderDatabase;
use skinning::Skinning;
use vertex::ModelBuffers;
use xc3_lib::{
apmd::Apmd,
bc::Bc,
error::{DecompressStreamError, ReadFileError},
hkt::Hkt,
msrd::streaming::chr_folder,
mxmd::{Mxmd, legacy::MxmdLegacy},
sar1::Sar1,
xbc1::MaybeXbc1,
};
pub use collision::load_collisions;
pub use map::load_map;
use material::{Material, Texture};
pub use sampler::{AddressMode, FilterMode, Sampler};
pub use skeleton::{Bone, Skeleton};
pub use texture::{ExtractedTextures, ImageFormat, ImageTexture, ViewDimension};
pub use transform::Transform;
pub use xc3_lib::mxmd::{MeshRenderFlags2, MeshRenderPass};
#[cfg(feature = "gltf")]
pub mod gltf;
pub mod animation;
pub mod collision;
pub mod error;
mod map;
pub mod material;
pub mod model;
pub mod monolib;
mod sampler;
pub mod shader_database;
mod skeleton;
pub mod skinning;
mod texture;
mod transform;
pub mod vertex;
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone)]
pub struct ModelRoot {
pub models: Models,
pub buffers: ModelBuffers,
pub image_textures: Vec<ImageTexture>,
pub skeleton: Option<Skeleton>,
}
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone)]
pub struct MapRoot {
pub groups: Vec<ModelGroup>,
pub image_textures: Vec<ImageTexture>,
}
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone)]
pub struct ModelGroup {
pub models: Vec<Models>,
pub buffers: Vec<ModelBuffers>,
}
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone)]
pub struct Models {
pub models: Vec<Model>,
pub materials: Vec<Material>,
pub samplers: Vec<Sampler>,
pub skinning: Option<Skinning>,
pub lod_data: Option<LodData>,
pub morph_controller_names: Vec<String>,
pub animation_morph_names: Vec<String>,
pub max_xyz: Vec3,
pub min_xyz: Vec3,
}
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone)]
pub struct Model {
pub meshes: Vec<Mesh>,
pub instances: Vec<Mat4>,
pub model_buffers_index: usize,
pub max_xyz: Vec3,
pub min_xyz: Vec3,
pub bounding_radius: f32,
}
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone)]
pub struct Mesh {
pub flags1: u32,
pub flags2: MeshRenderFlags2,
pub vertex_buffer_index: usize,
pub index_buffer_index: usize,
pub index_buffer_index2: usize,
pub material_index: usize,
pub ext_mesh_index: Option<usize>,
pub lod_item_index: Option<usize>,
pub base_mesh_index: Option<usize>,
}
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone)]
pub struct LodData {
pub unk1: u32,
pub items: Vec<LodItem>,
pub groups: Vec<LodGroup>,
}
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone)]
pub struct LodItem {
pub unk2: f32,
pub index: u8,
}
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone)]
pub struct LodGroup {
pub base_lod_index: usize,
pub lod_count: usize,
}
impl LodData {
pub fn is_base_lod(&self, lod_item_index: Option<usize>) -> bool {
match lod_item_index {
Some(i) => self.groups.iter().any(|g| g.base_lod_index == i),
None => true,
}
}
}
#[tracing::instrument(skip_all)]
pub fn load_model<P: AsRef<Path>>(
wimdo_path: P,
shader_database: Option<&ShaderDatabase>,
) -> Result<ModelRoot, LoadModelError> {
let wimdo_path = wimdo_path.as_ref();
let mxmd = load_wimdo(wimdo_path)?;
let chr = chr_folder(wimdo_path);
let is_pc = wimdo_path.extension().and_then(|e| e.to_str()) == Some("pcmdo");
let wismt_path = if is_pc {
wimdo_path.with_extension("pcsmt")
} else {
wimdo_path.with_extension("wismt")
};
let model_name = model_name(wimdo_path);
let skel = load_skel(wimdo_path, &model_name);
match mxmd.inner {
xc3_lib::mxmd::MxmdInner::V40(mxmd) => {
let files = ModelFilesV40::from_files(&mxmd, &wismt_path, chr.as_deref())?;
ModelRoot::from_mxmd_v40(&files, skel, shader_database)
}
xc3_lib::mxmd::MxmdInner::V111(mxmd) => {
let files = ModelFilesV111::from_files(&mxmd, &wismt_path, chr.as_deref(), is_pc)?;
ModelRoot::from_mxmd_v111(&files, skel, shader_database)
}
xc3_lib::mxmd::MxmdInner::V112(mxmd) => {
let files = ModelFilesV112::from_files(&mxmd, &wismt_path, chr.as_deref(), is_pc)?;
ModelRoot::from_mxmd_v112(&files, skel, shader_database)
}
}
}
pub fn load_skel(wimdo: &Path, model_name: &str) -> Option<xc3_lib::bc::skel::Skel> {
load_chr(wimdo, model_name)
.and_then(|chr| {
chr.entries
.iter()
.find_map(|e| match e.read_data::<xc3_lib::bc::Bc>() {
Ok(bc) => match bc.data {
xc3_lib::bc::BcData::Skel(skel) => Some(skel),
_ => None,
},
_ => None,
})
})
.or_else(|| {
Bc::from_file(wimdo.with_file_name(format!("{model_name}_rig.skl")))
.ok()
.or_else(|| {
let model_name = model_name.trim_end_matches("_us").trim_end_matches("_eu");
Bc::from_file(wimdo.with_file_name(format!("{model_name}_rig.skl"))).ok()
})
.and_then(|bc| match bc.data {
xc3_lib::bc::BcData::Skel(skel) => Some(skel),
_ => None,
})
})
}
fn load_chr(wimdo: &Path, model_name: &str) -> Option<Sar1> {
let base_name = base_chr_name(model_name);
Sar1::from_file(wimdo.with_file_name(&base_name).with_extension("chr"))
.ok()
.or_else(|| Sar1::from_file(wimdo.with_file_name(&base_name).with_extension("arc")).ok())
.or_else(|| Sar1::from_file(wimdo.with_extension("chr")).ok())
.or_else(|| Sar1::from_file(wimdo.with_extension("arc")).ok())
.or_else(|| {
(0..model_name.len()).find_map(|i| {
let mut chr_name = model_name.to_string();
chr_name.replace_range(chr_name.len() - i.., &"0".repeat(i));
let chr_path = wimdo.with_file_name(chr_name).with_extension("chr");
Sar1::from_file(chr_path).ok()
})
})
}
fn base_chr_name(model_name: &str) -> String {
let mut chr_name = model_name.to_string();
chr_name.replace_range(chr_name.len() - 3.., "000");
chr_name
}
#[tracing::instrument(skip_all)]
pub fn load_model_legacy<P: AsRef<Path>>(
camdo_path: P,
shader_database: Option<&ShaderDatabase>,
) -> Result<ModelRoot, LoadModelLegacyError> {
let camdo_path = camdo_path.as_ref();
let mxmd = MxmdLegacy::from_file(camdo_path).map_err(LoadModelLegacyError::Camdo)?;
let casmt = mxmd
.streaming
.as_ref()
.map(|_| {
std::fs::read(camdo_path.with_extension("casmt")).map_err(LoadModelLegacyError::Casmt)
})
.transpose()?;
let model_name = model_name(camdo_path);
let hkt_path = camdo_path.with_file_name(format!("{model_name}_rig.hkt"));
let hkt = Hkt::from_file(hkt_path).ok();
ModelRoot::from_mxmd_model_legacy(&mxmd, casmt, hkt.as_ref(), shader_database)
}
#[derive(BinRead)]
enum Wimdo {
Mxmd(Box<Mxmd>),
Apmd(Apmd),
}
fn load_wimdo(wimdo_path: &Path) -> Result<Mxmd, LoadModelError> {
let mut reader = Cursor::new(std::fs::read(wimdo_path).map_err(|e| {
LoadModelError::Wimdo(ReadFileError {
path: wimdo_path.to_owned(),
source: e.into(),
})
})?);
let wimdo: Wimdo = reader.read_le().map_err(|e| {
LoadModelError::Wimdo(ReadFileError {
path: wimdo_path.to_owned(),
source: e,
})
})?;
match wimdo {
Wimdo::Mxmd(mxmd) => Ok(*mxmd),
Wimdo::Apmd(apmd) => apmd
.entries
.iter()
.find_map(|e| {
if e.entry_type == xc3_lib::apmd::EntryType::Mxmd {
Some(Mxmd::from_bytes(&e.entry_data))
} else {
None
}
})
.map_or(Err(LoadModelError::MissingApmdMxmdEntry), |r| {
r.map_err(|e| {
LoadModelError::Wimdo(ReadFileError {
path: wimdo_path.to_owned(),
source: e,
})
})
}),
}
}
#[tracing::instrument(skip_all)]
pub fn load_animations<P: AsRef<Path>>(
anim_path: P,
) -> Result<Vec<Animation>, DecompressStreamError> {
let anim_file = <MaybeXbc1<AnimFile>>::from_file(anim_path)?;
let mut animations = Vec::new();
match anim_file {
MaybeXbc1::Uncompressed(anim) => add_anim_file(&mut animations, anim),
MaybeXbc1::Xbc1(xbc1) => {
if let Ok(anim) = xbc1.extract() {
add_anim_file(&mut animations, anim);
}
}
}
Ok(animations)
}
#[derive(BinRead)]
enum AnimFile {
Sar1(Sar1),
Bc(Bc),
}
fn add_anim_file(animations: &mut Vec<Animation>, anim: AnimFile) {
match anim {
AnimFile::Sar1(sar1) => {
for entry in &sar1.entries {
if let Ok(bc) = entry.read_data() {
add_bc_animations(animations, bc);
}
}
}
AnimFile::Bc(bc) => {
add_bc_animations(animations, bc);
}
}
}
fn add_bc_animations(animations: &mut Vec<Animation>, bc: Bc) {
if let xc3_lib::bc::BcData::Anim(anim) = bc.data {
let animation = Animation::from_anim(&anim);
animations.push(animation);
}
}
fn model_name(model_path: &Path) -> String {
model_path
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string()
}
#[cfg(feature = "arbitrary")]
fn arbitrary_smolstr(u: &mut arbitrary::Unstructured) -> arbitrary::Result<smol_str::SmolStr> {
let text: String = u.arbitrary()?;
Ok(text.into())
}
#[cfg(test)]
#[macro_export]
macro_rules! assert_hex_eq {
($a:expr, $b:expr) => {
pretty_assertions::assert_str_eq!(hex::encode($a), hex::encode($b))
};
}
pub trait IndexMapExt<T> {
fn entry_index(&mut self, key: T) -> usize;
}
impl<T> IndexMapExt<T> for IndexMap<T, usize>
where
T: Hash + Eq,
{
fn entry_index(&mut self, key: T) -> usize {
let new_value = self.len();
*self.entry(key).or_insert(new_value)
}
}
fn get_bytes(bytes: &[u8], offset: u32, size: Option<u32>) -> std::io::Result<&[u8]> {
let start = offset as usize;
match size {
Some(size) => {
let end = start + size as usize;
bytes.get(start..end).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!(
"byte range {start}..{end} out of range for length {}",
bytes.len()
),
)
})
}
None => bytes.get(start..).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!(
"byte offset {start} out of range for length {}",
bytes.len()
),
)
}),
}
}