1use std::{hash::Hash, io::Cursor, path::Path};
31
32use animation::Animation;
33use binrw::{BinRead, BinReaderExt};
34use error::{LoadModelError, LoadModelLegacyError};
35use glam::{Mat4, Vec3};
36use indexmap::IndexMap;
37use material::{create_materials, create_materials_samplers_legacy};
38use shader_database::ShaderDatabase;
39use skinning::Skinning;
40use vertex::ModelBuffers;
41use xc3_lib::{
42 apmd::Apmd,
43 bc::Bc,
44 error::{DecompressStreamError, ReadFileError},
45 evpa::Evpa,
46 hkt::Hkt,
47 msrd::streaming::chr_folder,
48 mxmd::{Mxmd, legacy::MxmdLegacy},
49 sar1::Sar1,
50 xbc1::MaybeXbc1,
51};
52
53pub use collision::load_collisions;
54pub use map::load_map;
55use material::{Material, Texture};
56pub use sampler::{AddressMode, FilterMode, Sampler};
57pub use skeleton::{Bone, Skeleton};
58pub use texture::{ExtractedTextures, ImageFormat, ImageTexture, ViewDimension};
59pub use transform::Transform;
60pub use xc3_lib::mxmd::{MeshRenderFlags2, MeshRenderPass};
61
62#[cfg(feature = "gltf")]
63pub mod gltf;
64
65pub mod animation;
66pub mod collision;
67pub mod error;
68mod map;
69pub mod material;
70pub mod model;
71pub mod monolib;
72mod sampler;
73pub mod shader_database;
74mod skeleton;
75pub mod skinning;
76mod texture;
77mod transform;
78pub mod vertex;
79
80#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
83#[derive(Debug, PartialEq, Clone)]
84pub struct ModelRoot {
85 pub models: Models,
86 pub buffers: ModelBuffers,
88
89 pub image_textures: Vec<ImageTexture>,
93
94 pub skeleton: Option<Skeleton>,
96}
97
98#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
99#[derive(Debug, PartialEq, Clone)]
100pub struct MapRoot {
101 pub groups: Vec<ModelGroup>,
102
103 pub image_textures: Vec<ImageTexture>,
107}
108
109#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
110#[derive(Debug, PartialEq, Clone)]
111pub struct ModelGroup {
112 pub models: Vec<Models>,
113 pub buffers: Vec<ModelBuffers>,
115}
116
117#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
121#[derive(Debug, PartialEq, Clone)]
122pub struct Models {
123 pub models: Vec<Model>,
124 pub materials: Vec<Material>,
125 pub samplers: Vec<Sampler>,
126
127 pub skinning: Option<Skinning>,
129
130 pub lod_data: Option<LodData>,
132
133 pub morph_controller_names: Vec<String>,
136
137 pub animation_morph_names: Vec<String>,
139
140 pub max_xyz: Vec3,
143
144 pub min_xyz: Vec3,
146}
147
148#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
150#[derive(Debug, PartialEq, Clone)]
151pub struct Model {
152 pub meshes: Vec<Mesh>,
153 pub instances: Vec<Mat4>,
155 pub model_buffers_index: usize,
158
159 pub max_xyz: Vec3,
160 pub min_xyz: Vec3,
161 pub bounding_radius: f32,
162}
163
164#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
166#[derive(Debug, PartialEq, Clone)]
167pub struct Mesh {
168 pub flags1: u32,
169 pub flags2: MeshRenderFlags2,
170 pub vertex_buffer_index: usize,
171 pub index_buffer_index: usize,
172 pub index_buffer_index2: usize,
173 pub material_index: usize,
174 pub ext_mesh_index: Option<usize>,
175 pub lod_item_index: Option<usize>,
176 pub base_mesh_index: Option<usize>,
177}
178
179#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
181#[derive(Debug, PartialEq, Clone)]
182pub struct LodData {
183 pub unk1: u32,
184 pub items: Vec<LodItem>,
185 pub groups: Vec<LodGroup>,
186}
187
188#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
190#[derive(Debug, PartialEq, Clone)]
191pub struct LodItem {
192 pub unk2: f32,
193 pub index: u8,
194}
195
196#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
198#[derive(Debug, PartialEq, Clone)]
199pub struct LodGroup {
200 pub base_lod_index: usize,
201 pub lod_count: usize,
202}
203
204impl LodData {
205 pub fn is_base_lod(&self, lod_item_index: Option<usize>) -> bool {
208 match lod_item_index {
209 Some(i) => self.groups.iter().any(|g| g.base_lod_index == i),
210 None => true,
211 }
212 }
213}
214
215#[tracing::instrument(skip_all)]
270pub fn load_model<P: AsRef<Path>>(
271 wimdo_path: P,
272 shader_database: Option<&ShaderDatabase>,
273) -> Result<ModelRoot, LoadModelError> {
274 let wimdo_path = wimdo_path.as_ref();
275
276 let mxmd = load_wimdo(wimdo_path)?;
277 let chr = chr_folder(wimdo_path);
278
279 let is_pc = wimdo_path.extension().and_then(|e| e.to_str()) == Some("pcmdo");
281 let wismt_path = if is_pc {
282 wimdo_path.with_extension("pcsmt")
283 } else {
284 wimdo_path.with_extension("wismt")
285 };
286
287 let model_name = model_name(wimdo_path);
288 let skel = load_skel(wimdo_path, &model_name);
289
290 ModelRoot::from_mxmd(
291 &mxmd,
292 &wismt_path,
293 chr.as_deref(),
294 skel,
295 shader_database,
296 is_pc,
297 )
298}
299
300pub fn load_skel(wimdo: &Path, model_name: &str) -> Option<xc3_lib::bc::skel::Skel> {
301 load_chr(wimdo, model_name)
302 .and_then(|chr| {
303 chr.entries
305 .iter()
306 .find_map(|e| match e.read_data::<xc3_lib::bc::Bc>() {
307 Ok(bc) => match bc.data {
308 xc3_lib::bc::BcData::Skel(skel) => Some(skel),
309 _ => None,
310 },
311 _ => None,
312 })
313 })
314 .or_else(|| {
315 Bc::from_file(wimdo.with_file_name(format!("{model_name}_rig.skl")))
318 .ok()
319 .or_else(|| {
320 let model_name = model_name.trim_end_matches("_us").trim_end_matches("_eu");
321 Bc::from_file(wimdo.with_file_name(format!("{model_name}_rig.skl"))).ok()
322 })
323 .and_then(|bc| match bc.data {
324 xc3_lib::bc::BcData::Skel(skel) => Some(skel),
325 _ => None,
326 })
327 })
328}
329
330fn load_chr(wimdo: &Path, model_name: &str) -> Option<Sar1> {
331 let base_name = base_chr_name(model_name);
338 Sar1::from_file(wimdo.with_file_name(&base_name).with_extension("chr"))
339 .ok()
340 .or_else(|| Sar1::from_file(wimdo.with_file_name(&base_name).with_extension("arc")).ok())
341 .or_else(|| Sar1::from_file(wimdo.with_extension("chr")).ok())
342 .or_else(|| Sar1::from_file(wimdo.with_extension("arc")).ok())
343 .or_else(|| {
344 (0..model_name.len()).find_map(|i| {
348 let mut chr_name = model_name.to_string();
349 chr_name.replace_range(chr_name.len() - i.., &"0".repeat(i));
350 let chr_path = wimdo.with_file_name(chr_name).with_extension("chr");
351 Sar1::from_file(chr_path).ok()
352 })
353 })
354}
355
356fn base_chr_name(model_name: &str) -> String {
357 let mut chr_name = model_name.to_string();
358 chr_name.replace_range(chr_name.len() - 3.., "000");
359 chr_name
360}
361
362#[tracing::instrument(skip_all)]
378pub fn load_model_legacy<P: AsRef<Path>>(
379 camdo_path: P,
380 shader_database: Option<&ShaderDatabase>,
381) -> Result<ModelRoot, LoadModelLegacyError> {
382 let camdo_path = camdo_path.as_ref();
383 let mxmd = MxmdLegacy::from_file(camdo_path).map_err(LoadModelLegacyError::Camdo)?;
384
385 let casmt = mxmd
386 .streaming
387 .as_ref()
388 .map(|_| {
389 std::fs::read(camdo_path.with_extension("casmt")).map_err(LoadModelLegacyError::Casmt)
390 })
391 .transpose()?;
392
393 let model_name = model_name(camdo_path);
394 let hkt_path = camdo_path.with_file_name(format!("{model_name}_rig.hkt"));
395 let hkt = Hkt::from_file(hkt_path).ok();
396
397 ModelRoot::from_mxmd_model_legacy(&mxmd, casmt, hkt.as_ref(), shader_database)
398}
399
400#[derive(BinRead)]
402enum Wimdo {
403 Mxmd(Box<Mxmd>),
404 Apmd(Apmd),
405}
406
407fn load_wimdo(wimdo_path: &Path) -> Result<Mxmd, LoadModelError> {
408 let mut reader = Cursor::new(std::fs::read(wimdo_path).map_err(|e| {
409 LoadModelError::Wimdo(ReadFileError {
410 path: wimdo_path.to_owned(),
411 source: e.into(),
412 })
413 })?);
414 let wimdo: Wimdo = reader.read_le().map_err(|e| {
415 LoadModelError::Wimdo(ReadFileError {
416 path: wimdo_path.to_owned(),
417 source: e,
418 })
419 })?;
420 match wimdo {
421 Wimdo::Mxmd(mxmd) => Ok(*mxmd),
422 Wimdo::Apmd(apmd) => apmd
423 .entries
424 .iter()
425 .find_map(|e| {
426 if e.entry_type == xc3_lib::apmd::EntryType::Mxmd {
427 Some(Mxmd::from_bytes(&e.entry_data))
428 } else {
429 None
430 }
431 })
432 .map_or(Err(LoadModelError::MissingApmdMxmdEntry), |r| {
433 r.map_err(|e| {
434 LoadModelError::Wimdo(ReadFileError {
435 path: wimdo_path.to_owned(),
436 source: e,
437 })
438 })
439 }),
440 }
441}
442
443#[tracing::instrument(skip_all)]
463pub fn load_animations<P: AsRef<Path>>(
464 anim_path: P,
465) -> Result<Vec<Animation>, DecompressStreamError> {
466 let anim_file = <MaybeXbc1<AnimFile>>::from_file(anim_path)?;
471
472 let mut animations = Vec::new();
473 match anim_file {
474 MaybeXbc1::Uncompressed(anim) => add_anim_file(&mut animations, anim),
475 MaybeXbc1::Xbc1(xbc1) => {
476 if let Ok(anim) = xbc1.extract() {
477 add_anim_file(&mut animations, anim);
478 }
479 }
480 }
481
482 Ok(animations)
483}
484
485#[derive(BinRead)]
486enum AnimFile {
487 Sar1(Sar1),
488 Bc(Bc),
489 Evpa(Evpa),
490}
491
492fn add_anim_file(animations: &mut Vec<Animation>, anim: AnimFile) {
493 match anim {
494 AnimFile::Sar1(sar1) => {
495 for entry in &sar1.entries {
496 if let Ok(bc) = entry.read_data() {
497 add_bc_animations(animations, bc);
498 }
499 }
500 }
501 AnimFile::Bc(bc) => {
502 add_bc_animations(animations, bc);
503 }
504 AnimFile::Evpa(evpa) => {
505 for e in &evpa.entries {
506 if let Ok(bc) = Bc::from_bytes(&e.entry_data) {
507 add_bc_animations(animations, bc);
508 }
509 }
510 }
511 }
512}
513
514fn add_bc_animations(animations: &mut Vec<Animation>, bc: Bc) {
515 if let xc3_lib::bc::BcData::Anim(anim) = bc.data {
516 let animation = Animation::from_anim(&anim);
517 animations.push(animation);
518 }
519}
520
521fn model_name(model_path: &Path) -> String {
523 model_path
524 .file_stem()
525 .unwrap_or_default()
526 .to_string_lossy()
527 .to_string()
528}
529
530#[cfg(feature = "arbitrary")]
531fn arbitrary_smolstr(u: &mut arbitrary::Unstructured) -> arbitrary::Result<smol_str::SmolStr> {
532 let text: String = u.arbitrary()?;
533 Ok(text.into())
534}
535
536#[cfg(test)]
537#[macro_export]
538macro_rules! assert_hex_eq {
539 ($a:expr, $b:expr) => {
540 pretty_assertions::assert_str_eq!(hex::encode($a), hex::encode($b))
541 };
542}
543
544pub trait IndexMapExt<T> {
546 fn entry_index(&mut self, key: T) -> usize;
549}
550
551impl<T> IndexMapExt<T> for IndexMap<T, usize>
552where
553 T: Hash + Eq,
554{
555 fn entry_index(&mut self, key: T) -> usize {
556 let new_value = self.len();
557 *self.entry(key).or_insert(new_value)
558 }
559}
560
561fn get_bytes(bytes: &[u8], offset: u32, size: Option<u32>) -> std::io::Result<&[u8]> {
562 let start = offset as usize;
563
564 match size {
565 Some(size) => {
566 let end = start + size as usize;
567 bytes.get(start..end).ok_or_else(|| {
568 std::io::Error::new(
569 std::io::ErrorKind::UnexpectedEof,
570 format!(
571 "byte range {start}..{end} out of range for length {}",
572 bytes.len()
573 ),
574 )
575 })
576 }
577 None => bytes.get(start..).ok_or_else(|| {
578 std::io::Error::new(
579 std::io::ErrorKind::UnexpectedEof,
580 format!(
581 "byte offset {start} out of range for length {}",
582 bytes.len()
583 ),
584 )
585 }),
586 }
587}