1use core::fmt;
44
45use crate::bytes::{Cursor, OutOfBounds};
46use crate::kfa::{Hinge, Kfa, KfaSprite, Point3, Seq};
47use crate::kv6::{self, Kv6};
48use crate::sprite::Sprite;
49use crate::voxel_clip::{self, VoxelClip};
50use crate::xform::{BoneXform, Quat};
51
52const MAGIC: [u8; 4] = *b"RKCH";
53const VERSION: u16 = 3;
58
59const TAG_META: [u8; 4] = *b"META";
60const TAG_MSHS: [u8; 4] = *b"MSHS";
61const TAG_BONS: [u8; 4] = *b"BONS";
62const TAG_CLPS: [u8; 4] = *b"CLPS";
63const TAG_VCLP: [u8; 4] = *b"VCLP";
65
66const HINGE_SIZE: usize = 64;
67
68const MESH_KIND_STATIC: u16 = 0;
70const MESH_KIND_CLIP: u16 = 1;
73
74const CLIP_KIND_SKELETAL: u16 = 0;
78
79#[derive(Debug, Clone)]
87pub struct Character {
88 pub name: String,
90 pub root: [f32; 3],
92 pub meshes: Vec<Kv6>,
94 pub bones: Vec<Bone>,
96 pub clips: Vec<Clip>,
99 pub voxel_clips: Vec<VoxelClip>,
102 pub extra_chunks: Vec<([u8; 4], Vec<u8>)>,
106}
107
108#[derive(Debug, Clone)]
115pub struct Bone {
116 pub name: String,
119 pub attachments: Vec<Attachment>,
122 pub hinge: Hinge,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq)]
130pub struct Attachment {
131 pub target: MeshRef,
133 pub local_offset: BoneXform,
137 pub playback: ClipPlayback,
139}
140
141impl Attachment {
142 #[must_use]
145 pub fn static_mesh(mesh_id: usize) -> Self {
146 Self {
147 target: MeshRef::Static(mesh_id),
148 local_offset: BoneXform::IDENTITY,
149 playback: ClipPlayback::default(),
150 }
151 }
152
153 #[must_use]
155 pub fn clip(clip_id: usize) -> Self {
156 Self {
157 target: MeshRef::Clip(clip_id),
158 local_offset: BoneXform::IDENTITY,
159 playback: ClipPlayback::default(),
160 }
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub struct ClipPlayback {
170 pub speed_q8: i32,
173 pub start_phase_ms: u32,
176}
177
178impl Default for ClipPlayback {
179 fn default() -> Self {
180 Self {
181 speed_q8: 256,
182 start_phase_ms: 0,
183 }
184 }
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum MeshRef {
191 Static(usize),
193 Clip(usize),
195}
196
197#[derive(Debug, Clone)]
199pub struct Clip {
200 pub name: String,
203 pub data: ClipData,
206}
207
208#[derive(Debug, Clone, PartialEq)]
210pub enum ClipData {
211 Skeletal {
216 frmval: Vec<Vec<BoneXform>>,
219 seq: Vec<Seq>,
222 },
223 Unknown {
226 kind: u16,
228 bytes: Vec<u8>,
230 },
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
235pub enum ParseError {
236 BadMagic {
238 got: [u8; 4],
240 },
241 UnsupportedVersion(u16),
243 Truncated {
245 at: usize,
247 need: usize,
249 },
250 UnsupportedMeshKind(u16),
253 ClipBoneCountMismatch,
255 MissingChunk([u8; 4]),
257 BadMesh(kv6::ParseError),
259 BadClip(voxel_clip::ParseError),
261 BadAttachmentIndex {
266 bone: usize,
268 index: usize,
270 table_len: usize,
272 },
273 BadSkeleton {
277 bone: usize,
279 },
280}
281
282impl fmt::Display for ParseError {
283 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284 match self {
285 Self::BadMagic { got } => {
286 write!(
287 f,
288 "character bad magic: got {got:02x?}, expected {MAGIC:02x?}"
289 )
290 }
291 Self::UnsupportedVersion(v) => {
292 write!(
293 f,
294 "character unsupported version {v} (this build reads {VERSION})"
295 )
296 }
297 Self::Truncated { at, need } => {
298 write!(f, "character truncated: need {need} bytes at offset {at}")
299 }
300 Self::UnsupportedMeshKind(k) => {
301 write!(f, "character bone references unsupported mesh_kind {k}")
302 }
303 Self::ClipBoneCountMismatch => {
304 write!(f, "character skeletal clip numhin != bones.len()")
305 }
306 Self::MissingChunk(tag) => {
307 write!(
308 f,
309 "character missing required chunk {:?}",
310 String::from_utf8_lossy(tag)
311 )
312 }
313 Self::BadMesh(e) => write!(f, "character embedded kv6 mesh: {e}"),
314 Self::BadClip(e) => write!(f, "character embedded voxel clip: {e:?}"),
315 Self::BadAttachmentIndex {
316 bone,
317 index,
318 table_len,
319 } => write!(
320 f,
321 "character bone {bone}: attachment index {index} past table of {table_len}"
322 ),
323 Self::BadSkeleton { bone } => {
324 write!(f, "character bone {bone}: parent out of range or cyclic")
325 }
326 }
327 }
328}
329
330impl std::error::Error for ParseError {}
331
332impl From<OutOfBounds> for ParseError {
333 fn from(e: OutOfBounds) -> Self {
334 Self::Truncated {
335 at: e.at,
336 need: e.need,
337 }
338 }
339}
340
341pub fn parse(bytes: &[u8]) -> Result<Character, ParseError> {
355 let mut cur = Cursor::new(bytes);
356 let magic = cur.read_bytes(4)?;
357 if magic != MAGIC {
358 return Err(ParseError::BadMagic {
359 got: [magic[0], magic[1], magic[2], magic[3]],
360 });
361 }
362 let version = cur.read_u16()?;
363 if version != VERSION {
364 return Err(ParseError::UnsupportedVersion(version));
365 }
366
367 let mut meta: Option<&[u8]> = None;
372 let mut mshs: Option<&[u8]> = None;
373 let mut bons: Option<&[u8]> = None;
374 let mut clps: Option<&[u8]> = None;
375 let mut vclp: Option<&[u8]> = None;
376 let mut extra_chunks = Vec::new();
377 while cur.remaining() > 0 {
378 let tag_buf = cur.read_bytes(4)?;
379 let tag = [tag_buf[0], tag_buf[1], tag_buf[2], tag_buf[3]];
380 let len = cur.read_u32()? as usize;
381 let payload = cur.read_bytes(len)?;
382 match tag {
383 TAG_META => meta = Some(payload),
384 TAG_MSHS => mshs = Some(payload),
385 TAG_BONS => bons = Some(payload),
386 TAG_CLPS => clps = Some(payload),
387 TAG_VCLP => vclp = Some(payload),
388 _ => extra_chunks.push((tag, payload.to_vec())),
389 }
390 }
391
392 let meta = meta.ok_or(ParseError::MissingChunk(TAG_META))?;
393 let mshs = mshs.ok_or(ParseError::MissingChunk(TAG_MSHS))?;
394 let bons = bons.ok_or(ParseError::MissingChunk(TAG_BONS))?;
395
396 let (name, root) = parse_meta(meta)?;
397 let meshes = parse_mshs(mshs)?;
398 let bones = parse_bons(bons)?;
399 let clips = match clps {
400 Some(p) => parse_clps(p, bones.len())?,
401 None => Vec::new(),
402 };
403 let voxel_clips = match vclp {
404 Some(p) => parse_vclp(p)?,
405 None => Vec::new(),
406 };
407
408 for (bi, bone) in bones.iter().enumerate() {
413 for att in &bone.attachments {
414 let (index, table_len) = match att.target {
415 MeshRef::Static(i) => (i, meshes.len()),
416 MeshRef::Clip(i) => (i, voxel_clips.len()),
417 };
418 if index >= table_len {
419 return Err(ParseError::BadAttachmentIndex {
420 bone: bi,
421 index,
422 table_len,
423 });
424 }
425 }
426 if bone.hinge.parent != -1
428 && usize::try_from(bone.hinge.parent).map_or(true, |p| p >= bones.len())
429 {
430 return Err(ParseError::BadSkeleton { bone: bi });
431 }
432 }
433 for start in 0..bones.len() {
434 let mut cur_parent = bones[start].hinge.parent;
435 let mut hops = 0usize;
436 while cur_parent != -1 {
437 hops += 1;
438 if hops > bones.len() {
439 return Err(ParseError::BadSkeleton { bone: start });
440 }
441 cur_parent = bones[usize::try_from(cur_parent).expect("validated non-negative")]
442 .hinge
443 .parent;
444 }
445 }
446
447 Ok(Character {
448 name,
449 root,
450 meshes,
451 bones,
452 clips,
453 voxel_clips,
454 extra_chunks,
455 })
456}
457
458#[must_use]
469pub fn serialize(c: &Character) -> Vec<u8> {
470 let mut out = Vec::new();
471 out.extend_from_slice(&MAGIC);
472 out.extend_from_slice(&VERSION.to_le_bytes());
473
474 write_chunk(&mut out, TAG_META, |b| write_meta(b, c));
475 write_chunk(&mut out, TAG_MSHS, |b| write_mshs(b, c));
476 write_chunk(&mut out, TAG_BONS, |b| write_bons(b, c));
477 write_chunk(&mut out, TAG_CLPS, |b| write_clps(b, c));
478 write_chunk(&mut out, TAG_VCLP, |b| write_vclp(b, c));
479
480 for (tag, payload) in &c.extra_chunks {
481 write_chunk(&mut out, *tag, |b| b.extend_from_slice(payload));
482 }
483
484 out
485}
486
487impl Character {
488 #[must_use]
503 pub fn to_kfa_sprite(&self, clip: Option<usize>) -> KfaSprite {
504 let limbs = self
512 .bones
513 .iter()
514 .map(|b| {
515 let kv6 = b
516 .attachments
517 .iter()
518 .find_map(|a| match a.target {
519 MeshRef::Static(i) => Some(self.meshes[i].clone()),
520 MeshRef::Clip(_) => None,
521 })
522 .unwrap_or_else(|| Kv6::from_fn(1, 1, 1, |_, _, _| None));
523 Sprite::axis_aligned(kv6, self.root)
524 })
525 .collect();
526 let hinges = self.bones.iter().map(|b| b.hinge).collect();
527 let mut k = KfaSprite::new(limbs, hinges, self.root);
528 if let Some(ci) = clip {
529 if let ClipData::Skeletal { frmval, seq } = &self.clips[ci].data {
530 k.set_animation(frmval.clone(), seq.clone());
533 }
534 }
535 k
536 }
537
538 #[must_use]
557 pub fn to_kfa(&self, clip: Option<usize>, kv6_name: impl Into<Vec<u8>>) -> Kfa {
558 let hinges = self.bones.iter().map(|b| b.hinge).collect();
559 let (frmval, seq) = match clip.and_then(|ci| self.clips.get(ci)) {
563 Some(Clip {
564 data: ClipData::Skeletal { frmval, seq },
565 ..
566 }) => {
567 let angles = frmval
568 .iter()
569 .map(|row| {
570 row.iter()
571 .enumerate()
572 .map(|(bone, x)| {
573 let v = self.bones[bone].hinge.v[0];
574 x.hinge_angle([v.x, v.y, v.z])
575 })
576 .collect()
577 })
578 .collect();
579 (angles, seq.clone())
580 }
581 _ => (Vec::new(), Vec::new()),
582 };
583 Kfa {
584 kv6_name: kv6_name.into(),
585 hinges,
586 frmval,
587 seq,
588 }
589 }
590}
591
592fn parse_meta(payload: &[u8]) -> Result<(String, [f32; 3]), ParseError> {
595 let mut cur = Cursor::new(payload);
596 let name_len = cur.read_u16()? as usize;
597 let name = String::from_utf8_lossy(cur.read_bytes(name_len)?).into_owned();
598 let root = [cur.read_f32()?, cur.read_f32()?, cur.read_f32()?];
599 Ok((name, root))
600}
601
602fn parse_mshs(payload: &[u8]) -> Result<Vec<Kv6>, ParseError> {
603 let mut cur = Cursor::new(payload);
604 let count = cur.read_u32()? as usize;
605 let mut meshes = Vec::with_capacity(cur.clamped_capacity(count, 4));
608 for _ in 0..count {
609 let blob_len = cur.read_u32()? as usize;
610 let blob = cur.read_bytes(blob_len)?;
611 meshes.push(kv6::parse(blob).map_err(ParseError::BadMesh)?);
612 }
613 Ok(meshes)
614}
615
616fn parse_bons(payload: &[u8]) -> Result<Vec<Bone>, ParseError> {
617 let mut cur = Cursor::new(payload);
618 let count = cur.read_u32()? as usize;
619 let mut bones = Vec::with_capacity(cur.clamped_capacity(count, 70));
621 for _ in 0..count {
622 let name_len = cur.read_u16()? as usize;
623 let name = String::from_utf8_lossy(cur.read_bytes(name_len)?).into_owned();
624 let attach_count = cur.read_u32()? as usize;
625 let mut attachments = Vec::with_capacity(cur.clamped_capacity(attach_count, 54));
627 for _ in 0..attach_count {
628 attachments.push(read_attachment(&mut cur)?);
629 }
630 let hinge = read_hinge(&mut cur)?;
631 bones.push(Bone {
632 name,
633 attachments,
634 hinge,
635 });
636 }
637 Ok(bones)
638}
639
640fn read_attachment(cur: &mut Cursor<'_>) -> Result<Attachment, ParseError> {
643 let mesh_kind = cur.read_u16()?;
644 let index = cur.read_u32()? as usize;
645 let target = match mesh_kind {
646 MESH_KIND_STATIC => MeshRef::Static(index),
647 MESH_KIND_CLIP => MeshRef::Clip(index),
648 other => return Err(ParseError::UnsupportedMeshKind(other)),
649 };
650 let local_offset = read_bonexform(cur)?;
651 let speed_q8 = cur.read_i32()?;
652 let start_phase_ms = cur.read_u32()?;
653 Ok(Attachment {
654 target,
655 local_offset,
656 playback: ClipPlayback {
657 speed_q8,
658 start_phase_ms,
659 },
660 })
661}
662
663fn parse_vclp(payload: &[u8]) -> Result<Vec<VoxelClip>, ParseError> {
664 let mut cur = Cursor::new(payload);
665 let count = cur.read_u32()? as usize;
666 let mut clips = Vec::with_capacity(cur.clamped_capacity(count, 4));
668 for _ in 0..count {
669 let blob_len = cur.read_u32()? as usize;
670 let blob = cur.read_bytes(blob_len)?;
671 clips.push(VoxelClip::parse(blob).map_err(ParseError::BadClip)?);
672 }
673 Ok(clips)
674}
675
676fn parse_clps(payload: &[u8], numbone: usize) -> Result<Vec<Clip>, ParseError> {
677 let mut cur = Cursor::new(payload);
678 let count = cur.read_u32()? as usize;
679 let mut clips = Vec::with_capacity(cur.clamped_capacity(count, 8));
681 for _ in 0..count {
682 let name_len = cur.read_u16()? as usize;
683 let name = String::from_utf8_lossy(cur.read_bytes(name_len)?).into_owned();
684 let kind = cur.read_u16()?;
685 let payload_len = cur.read_u32()? as usize;
686 let body = cur.read_bytes(payload_len)?;
687 let data = if kind == CLIP_KIND_SKELETAL {
688 parse_skeletal(body, numbone)?
689 } else {
690 ClipData::Unknown {
691 kind,
692 bytes: body.to_vec(),
693 }
694 };
695 clips.push(Clip { name, data });
696 }
697 Ok(clips)
698}
699
700fn parse_skeletal(body: &[u8], numbone: usize) -> Result<ClipData, ParseError> {
701 let mut cur = Cursor::new(body);
702 let numfrm = cur.read_u32()? as usize;
703 let numhin = cur.read_u32()? as usize;
704 if numhin != numbone {
705 return Err(ParseError::ClipBoneCountMismatch);
706 }
707 let mut frmval = Vec::with_capacity(cur.clamped_capacity(numfrm, numhin.saturating_mul(40)));
709 for _ in 0..numfrm {
710 let mut row = Vec::with_capacity(cur.clamped_capacity(numhin, 40));
711 for _ in 0..numhin {
712 row.push(read_bonexform(&mut cur)?);
713 }
714 frmval.push(row);
715 }
716 let seqcount = cur.read_u32()? as usize;
717 let mut seq = Vec::with_capacity(cur.clamped_capacity(seqcount, 8));
718 for _ in 0..seqcount {
719 let tim = cur.read_i32()?;
720 let frm = cur.read_i32()?;
721 seq.push(Seq { tim, frm });
722 }
723 Ok(ClipData::Skeletal { frmval, seq })
724}
725
726fn read_hinge(cur: &mut Cursor<'_>) -> Result<Hinge, OutOfBounds> {
727 let parent = cur.read_i32()?;
728 let p0 = read_point3(cur)?;
729 let p1 = read_point3(cur)?;
730 let v0 = read_point3(cur)?;
731 let v1 = read_point3(cur)?;
732 let vmin = cur.read_i16()?;
733 let vmax = cur.read_i16()?;
734 let htype = cur.read_u8()?;
735 let filler_buf = cur.read_bytes(7)?;
736 let mut filler = [0u8; 7];
737 filler.copy_from_slice(filler_buf);
738 Ok(Hinge {
739 parent,
740 p: [p0, p1],
741 v: [v0, v1],
742 vmin,
743 vmax,
744 htype,
745 filler,
746 })
747}
748
749fn read_point3(cur: &mut Cursor<'_>) -> Result<Point3, OutOfBounds> {
750 Ok(Point3 {
751 x: cur.read_f32()?,
752 y: cur.read_f32()?,
753 z: cur.read_f32()?,
754 })
755}
756
757fn read_bonexform(cur: &mut Cursor<'_>) -> Result<BoneXform, OutOfBounds> {
760 let t = [cur.read_f32()?, cur.read_f32()?, cur.read_f32()?];
761 let r = Quat {
762 x: cur.read_f32()?,
763 y: cur.read_f32()?,
764 z: cur.read_f32()?,
765 w: cur.read_f32()?,
766 };
767 let s = [cur.read_f32()?, cur.read_f32()?, cur.read_f32()?];
768 Ok(BoneXform { t, r, s })
769}
770
771fn write_bonexform(out: &mut Vec<u8>, x: &BoneXform) {
772 for v in [
773 x.t[0], x.t[1], x.t[2], x.r.x, x.r.y, x.r.z, x.r.w, x.s[0], x.s[1], x.s[2],
774 ] {
775 out.extend_from_slice(&v.to_le_bytes());
776 }
777}
778
779fn write_chunk(out: &mut Vec<u8>, tag: [u8; 4], body: impl FnOnce(&mut Vec<u8>)) {
783 out.extend_from_slice(&tag);
784 let len_pos = out.len();
785 out.extend_from_slice(&0u32.to_le_bytes()); let start = out.len();
787 body(out);
788 let len = u32::try_from(out.len() - start).expect("chunk payload length must fit in u32");
789 out[len_pos..len_pos + 4].copy_from_slice(&len.to_le_bytes());
790}
791
792fn write_meta(out: &mut Vec<u8>, c: &Character) {
793 let name = c.name.as_bytes();
794 let name_len = u16::try_from(name.len()).expect("character name length must fit in u16");
795 out.extend_from_slice(&name_len.to_le_bytes());
796 out.extend_from_slice(name);
797 for v in c.root {
798 out.extend_from_slice(&v.to_le_bytes());
799 }
800}
801
802fn write_mshs(out: &mut Vec<u8>, c: &Character) {
803 let count = u32::try_from(c.meshes.len()).expect("mesh count must fit in u32");
804 out.extend_from_slice(&count.to_le_bytes());
805 for mesh in &c.meshes {
806 let blob = kv6::serialize(mesh);
807 let blob_len = u32::try_from(blob.len()).expect("kv6 blob length must fit in u32");
808 out.extend_from_slice(&blob_len.to_le_bytes());
809 out.extend_from_slice(&blob);
810 }
811}
812
813fn write_bons(out: &mut Vec<u8>, c: &Character) {
814 let count = u32::try_from(c.bones.len()).expect("bone count must fit in u32");
815 out.extend_from_slice(&count.to_le_bytes());
816 for bone in &c.bones {
817 let name = bone.name.as_bytes();
818 let name_len = u16::try_from(name.len()).expect("bone name length must fit in u16");
819 out.extend_from_slice(&name_len.to_le_bytes());
820 out.extend_from_slice(name);
821 let attach_count =
822 u32::try_from(bone.attachments.len()).expect("attachment count must fit in u32");
823 out.extend_from_slice(&attach_count.to_le_bytes());
824 for a in &bone.attachments {
825 write_attachment(out, a);
826 }
827 write_hinge(out, &bone.hinge);
828 }
829}
830
831fn write_attachment(out: &mut Vec<u8>, a: &Attachment) {
832 let (kind, index) = match a.target {
833 MeshRef::Static(i) => (MESH_KIND_STATIC, i),
834 MeshRef::Clip(i) => (MESH_KIND_CLIP, i),
835 };
836 out.extend_from_slice(&kind.to_le_bytes());
837 let idx = u32::try_from(index).expect("attachment index must fit in u32");
838 out.extend_from_slice(&idx.to_le_bytes());
839 write_bonexform(out, &a.local_offset);
840 out.extend_from_slice(&a.playback.speed_q8.to_le_bytes());
841 out.extend_from_slice(&a.playback.start_phase_ms.to_le_bytes());
842}
843
844fn write_vclp(out: &mut Vec<u8>, c: &Character) {
845 let count = u32::try_from(c.voxel_clips.len()).expect("voxel clip count must fit in u32");
846 out.extend_from_slice(&count.to_le_bytes());
847 for clip in &c.voxel_clips {
848 let blob = clip.serialize();
849 let blob_len = u32::try_from(blob.len()).expect("voxel clip blob length must fit in u32");
850 out.extend_from_slice(&blob_len.to_le_bytes());
851 out.extend_from_slice(&blob);
852 }
853}
854
855fn write_clps(out: &mut Vec<u8>, c: &Character) {
856 let count = u32::try_from(c.clips.len()).expect("clip count must fit in u32");
857 out.extend_from_slice(&count.to_le_bytes());
858 for clip in &c.clips {
859 let name = clip.name.as_bytes();
860 let name_len = u16::try_from(name.len()).expect("clip name length must fit in u16");
861 out.extend_from_slice(&name_len.to_le_bytes());
862 out.extend_from_slice(name);
863 match &clip.data {
864 ClipData::Skeletal { frmval, seq } => {
865 out.extend_from_slice(&CLIP_KIND_SKELETAL.to_le_bytes());
866 write_chunk_body(out, |b| write_skeletal(b, frmval, seq));
867 }
868 ClipData::Unknown { kind, bytes } => {
869 out.extend_from_slice(&kind.to_le_bytes());
870 write_chunk_body(out, |b| b.extend_from_slice(bytes));
871 }
872 }
873 }
874}
875
876fn write_chunk_body(out: &mut Vec<u8>, body: impl FnOnce(&mut Vec<u8>)) {
880 let len_pos = out.len();
881 out.extend_from_slice(&0u32.to_le_bytes());
882 let start = out.len();
883 body(out);
884 let len = u32::try_from(out.len() - start).expect("clip payload length must fit in u32");
885 out[len_pos..len_pos + 4].copy_from_slice(&len.to_le_bytes());
886}
887
888fn write_skeletal(out: &mut Vec<u8>, frmval: &[Vec<BoneXform>], seq: &[Seq]) {
889 let numhin = frmval.first().map_or(0, Vec::len);
890 for (i, row) in frmval.iter().enumerate() {
891 assert!(
892 row.len() == numhin,
893 "skeletal frmval[{i}].len() = {} != numhin {numhin}",
894 row.len(),
895 );
896 }
897 let numfrm = u32::try_from(frmval.len()).expect("numfrm must fit in u32");
898 let numhin_u32 = u32::try_from(numhin).expect("numhin must fit in u32");
899 out.extend_from_slice(&numfrm.to_le_bytes());
900 out.extend_from_slice(&numhin_u32.to_le_bytes());
901 for row in frmval {
902 for v in row {
903 write_bonexform(out, v);
904 }
905 }
906 let seqcount = u32::try_from(seq.len()).expect("seqcount must fit in u32");
907 out.extend_from_slice(&seqcount.to_le_bytes());
908 for s in seq {
909 out.extend_from_slice(&s.tim.to_le_bytes());
910 out.extend_from_slice(&s.frm.to_le_bytes());
911 }
912}
913
914fn write_hinge(out: &mut Vec<u8>, h: &Hinge) {
915 out.extend_from_slice(&h.parent.to_le_bytes());
916 for p in h.p {
917 write_point3(out, p);
918 }
919 for v in h.v {
920 write_point3(out, v);
921 }
922 out.extend_from_slice(&h.vmin.to_le_bytes());
923 out.extend_from_slice(&h.vmax.to_le_bytes());
924 out.push(h.htype);
925 out.extend_from_slice(&h.filler);
926}
927
928fn write_point3(out: &mut Vec<u8>, p: Point3) {
929 out.extend_from_slice(&p.x.to_le_bytes());
930 out.extend_from_slice(&p.y.to_le_bytes());
931 out.extend_from_slice(&p.z.to_le_bytes());
932}
933
934const _: () = assert!(HINGE_SIZE == 64);
936
937#[cfg(test)]
940mod tests {
941 use super::*;
942
943 fn unit_kv6(fill: u32) -> Kv6 {
944 Kv6 {
946 xsiz: 1,
947 ysiz: 1,
948 zsiz: 1,
949 xpiv: 0.5,
950 ypiv: 0.5,
951 zpiv: 0.5,
952 voxels: vec![kv6::Voxel {
953 col: fill,
954 z: 0,
955 vis: 0x3f,
956 dir: 0,
957 }],
958 xlen: vec![1],
959 ylen: vec![vec![1]],
960 palette: None,
961 }
962 }
963
964 fn hinge(parent: i32) -> Hinge {
965 let zero = Point3 {
966 x: 0.0,
967 y: 0.0,
968 z: 0.0,
969 };
970 let axis = Point3 {
971 x: 0.0,
972 y: 0.0,
973 z: 1.0,
974 };
975 Hinge {
976 parent,
977 p: [zero, zero],
978 v: [axis, axis],
979 vmin: 0,
980 vmax: 0,
981 htype: 0,
982 filler: [0; 7],
983 }
984 }
985
986 fn synthetic_character() -> Character {
987 Character {
988 name: "anasaur".to_string(),
989 root: [70.0, -75.0, 50.0],
990 meshes: vec![unit_kv6(0x00ff_8040), unit_kv6(0x0010_2030)],
991 bones: vec![
992 Bone {
993 name: "body".to_string(),
994 attachments: vec![Attachment::static_mesh(0)],
995 hinge: hinge(-1),
996 },
997 Bone {
998 name: "arm".to_string(),
999 attachments: vec![Attachment::static_mesh(1)],
1000 hinge: hinge(0),
1001 },
1002 ],
1003 clips: vec![Clip {
1004 name: "wave".to_string(),
1005 data: ClipData::Skeletal {
1008 frmval: [[0i16, 0], [0, 16000], [0, 0], [0, -16000]]
1009 .iter()
1010 .map(|[r, a]| {
1011 let z = [0.0, 0.0, 1.0];
1012 vec![
1013 BoneXform::from_hinge_angle(z, *r),
1014 BoneXform::from_hinge_angle(z, *a),
1015 ]
1016 })
1017 .collect(),
1018 seq: vec![
1019 Seq { tim: 0, frm: 0 },
1020 Seq { tim: 500, frm: 1 },
1021 Seq { tim: 1000, frm: 2 },
1022 Seq { tim: 1500, frm: 3 },
1023 Seq { tim: 2000, frm: !0 },
1024 ],
1025 },
1026 }],
1027 voxel_clips: Vec::new(),
1028 extra_chunks: Vec::new(),
1029 }
1030 }
1031
1032 #[test]
1033 fn roundtrips_byte_equal() {
1034 let c = synthetic_character();
1035 let bytes = serialize(&c);
1036 let parsed = parse(&bytes).expect("parse synthetic");
1037 let bytes2 = serialize(&parsed);
1038 assert_eq!(bytes, bytes2, "byte-level round-trip failed");
1039 assert_eq!(parsed.name, c.name);
1040 assert_eq!(parsed.root, c.root);
1041 assert_eq!(parsed.meshes.len(), c.meshes.len());
1042 assert_eq!(parsed.bones.len(), c.bones.len());
1043 assert_eq!(parsed.bones[1].name, "arm");
1044 assert_eq!(parsed.bones[1].attachments[0].target, MeshRef::Static(1));
1045 assert_eq!(parsed.bones[1].hinge.parent, 0);
1046 assert_eq!(parsed.clips.len(), 1);
1047 assert_eq!(parsed.clips[0].data, c.clips[0].data);
1048 assert!(parsed.voxel_clips.is_empty());
1049 }
1050
1051 #[test]
1052 fn roundtrips_with_clips_and_multi_attachment() {
1053 use crate::voxel_clip::{LoopMode, VoxelClip, VoxelFrame};
1054 let frame = VoxelFrame {
1056 occupancy: vec![1u32],
1057 colors: vec![0x8011_2233],
1058 color_offsets: vec![0, 1],
1059 };
1060 let clip = VoxelClip::from_frames(
1061 [1, 1, 4],
1062 [0.5, 0.5, 2.0],
1063 1.0,
1064 LoopMode::Loop,
1065 &[frame],
1066 &[],
1067 33,
1068 0,
1069 );
1070
1071 let mut c = synthetic_character();
1072 c.voxel_clips = vec![clip];
1073 c.bones[0].attachments.push(Attachment {
1076 target: MeshRef::Clip(0),
1077 local_offset: BoneXform {
1078 t: [1.0, 2.0, 3.0],
1079 r: Quat::IDENTITY,
1080 s: [1.0, 1.0, 1.0],
1081 },
1082 playback: ClipPlayback {
1083 speed_q8: 512,
1084 start_phase_ms: 100,
1085 },
1086 });
1087
1088 let bytes = serialize(&c);
1089 let parsed = parse(&bytes).expect("parse v3 with clips");
1090 assert_eq!(serialize(&parsed), bytes, "byte round-trip");
1091
1092 assert_eq!(parsed.voxel_clips.len(), 1);
1093 assert_eq!(parsed.voxel_clips[0], c.voxel_clips[0]);
1094
1095 let body = &parsed.bones[0].attachments;
1097 assert_eq!(body.len(), 2);
1098 assert_eq!(body[0].target, MeshRef::Static(0));
1099 assert_eq!(body[1].target, MeshRef::Clip(0));
1100 assert_eq!(body[1].local_offset.t, [1.0, 2.0, 3.0]);
1101 assert_eq!(
1102 body[1].playback,
1103 ClipPlayback {
1104 speed_q8: 512,
1105 start_phase_ms: 100,
1106 }
1107 );
1108
1109 let k = parsed.to_kfa_sprite(None);
1112 assert_eq!(k.limbs.len(), parsed.bones.len());
1113 }
1114
1115 #[test]
1116 fn to_kfa_sprite_builds_renderable() {
1117 let c = synthetic_character();
1118 let mut k = c.to_kfa_sprite(Some(0));
1119 assert_eq!(k.limbs.len(), 2);
1120 assert_eq!(k.hinges.len(), 2);
1121 assert_eq!(k.p, c.root);
1122 k.animsprite(500);
1124 assert_ne!(
1125 k.kfaval[1],
1126 crate::xform::BoneXform::IDENTITY,
1127 "baked clip should move the arm bone"
1128 );
1129
1130 let mut rest = c.to_kfa_sprite(None);
1132 rest.animsprite(500);
1133 assert_eq!(rest.kfaval[1], crate::xform::BoneXform::IDENTITY);
1134 }
1135
1136 #[test]
1137 fn to_kfa_export_is_lossy_but_valid() {
1138 let c = synthetic_character();
1139 let kfa = c.to_kfa(Some(0), "coco.kv6");
1140 assert_eq!(kfa.kv6_name, b"coco.kv6");
1142 assert_eq!(kfa.hinges.len(), 2);
1143 assert_eq!(kfa.hinges[1].parent, 0);
1144 if let ClipData::Skeletal { frmval, seq } = &c.clips[0].data {
1145 let z = [0.0, 0.0, 1.0];
1148 let expected: Vec<Vec<i16>> = frmval
1149 .iter()
1150 .map(|row| row.iter().map(|x| x.hinge_angle(z)).collect())
1151 .collect();
1152 assert_eq!(kfa.frmval, expected);
1153 assert_eq!(&kfa.seq, seq);
1154 } else {
1155 panic!("clip 0 should be skeletal");
1156 }
1157 let bytes = crate::kfa::serialize(&kfa);
1160 let reparsed = crate::kfa::parse(&bytes).expect("export round-trips through kfa");
1161 assert_eq!(reparsed.kv6_name, kfa.kv6_name);
1162 assert_eq!(reparsed.frmval, kfa.frmval);
1163 assert_eq!(reparsed.seq, kfa.seq);
1164 }
1165
1166 #[test]
1167 fn to_kfa_without_clip_is_posable() {
1168 let c = synthetic_character();
1169 let kfa = c.to_kfa(None, b"x.kv6".to_vec());
1171 assert_eq!(kfa.hinges.len(), 2);
1172 assert!(kfa.frmval.is_empty());
1173 assert!(kfa.seq.is_empty());
1174 let bytes = crate::kfa::serialize(&kfa);
1176 assert!(crate::kfa::parse(&bytes).is_ok());
1177 }
1178
1179 #[test]
1180 fn clips_may_be_empty() {
1181 let mut c = synthetic_character();
1182 c.clips.clear();
1183 let bytes = serialize(&c);
1184 let parsed = parse(&bytes).expect("parse");
1185 assert!(parsed.clips.is_empty());
1186 let k = parsed.to_kfa_sprite(None);
1188 assert_eq!(k.limbs.len(), 2);
1189 }
1190
1191 #[test]
1192 fn unknown_top_level_chunk_is_skipped_and_preserved() {
1193 let mut bytes = serialize(&synthetic_character());
1194 bytes.extend_from_slice(b"ZZZZ");
1196 bytes.extend_from_slice(&3u32.to_le_bytes());
1197 bytes.extend_from_slice(&[1, 2, 3]);
1198 let parsed = parse(&bytes).expect("parse with unknown chunk");
1199 assert_eq!(parsed.bones.len(), 2, "known chunks still parse");
1200 assert_eq!(parsed.extra_chunks, vec![(*b"ZZZZ", vec![1u8, 2, 3])]);
1201 let bytes2 = serialize(&parsed);
1203 assert_eq!(
1204 bytes2, bytes,
1205 "unknown chunk preserved byte-equal on re-save"
1206 );
1207 }
1208
1209 #[test]
1210 fn unknown_clip_kind_preserved() {
1211 let mut c = synthetic_character();
1212 c.clips.push(Clip {
1213 name: "mystery".to_string(),
1214 data: ClipData::Unknown {
1215 kind: 7,
1216 bytes: vec![9, 8, 7, 6],
1217 },
1218 });
1219 let bytes = serialize(&c);
1220 let parsed = parse(&bytes).expect("parse");
1221 assert!(matches!(parsed.clips[0].data, ClipData::Skeletal { .. }));
1223 assert_eq!(
1224 parsed.clips[1].data,
1225 ClipData::Unknown {
1226 kind: 7,
1227 bytes: vec![9, 8, 7, 6]
1228 }
1229 );
1230 assert_eq!(serialize(&parsed), bytes);
1232 }
1233
1234 #[test]
1235 fn bad_magic_errors() {
1236 let mut bytes = serialize(&synthetic_character());
1237 bytes[0] ^= 0xff;
1238 assert!(matches!(parse(&bytes), Err(ParseError::BadMagic { .. })));
1239 }
1240
1241 #[test]
1242 fn version_mismatch_errors() {
1243 let mut bytes = serialize(&synthetic_character());
1244 bytes[4] = 0xff;
1246 bytes[5] = 0xff;
1247 assert!(matches!(
1248 parse(&bytes),
1249 Err(ParseError::UnsupportedVersion(0xffff))
1250 ));
1251 }
1252
1253 #[test]
1254 fn truncated_errors() {
1255 let bytes = serialize(&synthetic_character());
1256 assert!(matches!(
1257 parse(&bytes[..bytes.len() - 4]),
1258 Err(ParseError::Truncated { .. })
1259 ));
1260 }
1261
1262 #[test]
1263 fn missing_required_chunk_errors() {
1264 let mut bytes = Vec::new();
1266 bytes.extend_from_slice(&MAGIC);
1267 bytes.extend_from_slice(&VERSION.to_le_bytes());
1268 assert!(matches!(
1269 parse(&bytes),
1270 Err(ParseError::MissingChunk(TAG_META))
1271 ));
1272 }
1273
1274 #[test]
1275 fn unsupported_mesh_kind_errors() {
1276 let mut bytes = serialize(&synthetic_character());
1277 let pos = find_tag(&bytes, *b"BONS");
1282 let kind_off = pos + 8 + 4 + 2 + 4 + 4;
1283 bytes[kind_off] = 2;
1284 bytes[kind_off + 1] = 0;
1285 assert!(matches!(
1286 parse(&bytes),
1287 Err(ParseError::UnsupportedMeshKind(2))
1288 ));
1289 }
1290
1291 #[test]
1292 fn clip_bone_count_mismatch_errors() {
1293 let mut c = synthetic_character();
1296 c.bones.pop(); c.bones[0].attachments = vec![Attachment::static_mesh(0)];
1298 let bytes = serialize(&c);
1300 assert!(matches!(
1301 parse(&bytes),
1302 Err(ParseError::ClipBoneCountMismatch)
1303 ));
1304 }
1305
1306 #[test]
1307 fn bad_embedded_mesh_errors() {
1308 let mut bytes = serialize(&synthetic_character());
1309 let pos = find_tag(&bytes, *b"MSHS");
1312 let kv6_magic_off = pos + 8 + 4 + 4;
1313 bytes[kv6_magic_off] ^= 0xff;
1314 assert!(matches!(parse(&bytes), Err(ParseError::BadMesh(_))));
1315 }
1316
1317 fn find_tag(bytes: &[u8], tag: [u8; 4]) -> usize {
1320 let mut pos = 6; while pos + 8 <= bytes.len() {
1322 let here = &bytes[pos..pos + 4];
1323 let len = u32::from_le_bytes([
1324 bytes[pos + 4],
1325 bytes[pos + 5],
1326 bytes[pos + 6],
1327 bytes[pos + 7],
1328 ]) as usize;
1329 if here == tag {
1330 return pos;
1331 }
1332 pos += 8 + len;
1333 }
1334 panic!("tag {tag:?} not found");
1335 }
1336
1337 #[test]
1340 fn parse_rejects_out_of_range_attachment_index() {
1341 let mut c = synthetic_character();
1344 c.bones[1].attachments = vec![Attachment::static_mesh(99)];
1345 let bytes = serialize(&c);
1346 assert!(matches!(
1347 parse(&bytes),
1348 Err(ParseError::BadAttachmentIndex {
1349 bone: 1,
1350 index: 99,
1351 table_len: 2,
1352 })
1353 ));
1354 }
1355
1356 #[test]
1357 fn parse_rejects_bad_or_cyclic_bone_skeleton() {
1358 let mut c = synthetic_character();
1360 c.bones[1].hinge.parent = 42;
1361 assert!(matches!(
1362 parse(&serialize(&c)),
1363 Err(ParseError::BadSkeleton { bone: 1 })
1364 ));
1365 let mut c = synthetic_character();
1367 c.bones[0].hinge.parent = 1;
1368 assert!(matches!(
1369 parse(&serialize(&c)),
1370 Err(ParseError::BadSkeleton { .. })
1371 ));
1372 }
1373}