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,
117 pub attachments: Vec<Attachment>,
120 pub hinge: Hinge,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq)]
128pub struct Attachment {
129 pub target: MeshRef,
131 pub local_offset: BoneXform,
135 pub playback: ClipPlayback,
137}
138
139impl Attachment {
140 #[must_use]
143 pub fn static_mesh(mesh_id: usize) -> Self {
144 Self {
145 target: MeshRef::Static(mesh_id),
146 local_offset: BoneXform::IDENTITY,
147 playback: ClipPlayback::default(),
148 }
149 }
150
151 #[must_use]
153 pub fn clip(clip_id: usize) -> Self {
154 Self {
155 target: MeshRef::Clip(clip_id),
156 local_offset: BoneXform::IDENTITY,
157 playback: ClipPlayback::default(),
158 }
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub struct ClipPlayback {
168 pub speed_q8: i32,
171 pub start_phase_ms: u32,
174}
175
176impl Default for ClipPlayback {
177 fn default() -> Self {
178 Self {
179 speed_q8: 256,
180 start_phase_ms: 0,
181 }
182 }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum MeshRef {
189 Static(usize),
191 Clip(usize),
193}
194
195#[derive(Debug, Clone)]
197pub struct Clip {
198 pub name: String,
199 pub data: ClipData,
200}
201
202#[derive(Debug, Clone, PartialEq)]
204pub enum ClipData {
205 Skeletal {
210 frmval: Vec<Vec<BoneXform>>,
211 seq: Vec<Seq>,
212 },
213 Unknown { kind: u16, bytes: Vec<u8> },
216}
217
218#[derive(Debug, Clone, PartialEq, Eq)]
220pub enum ParseError {
221 BadMagic { got: [u8; 4] },
223 UnsupportedVersion(u16),
225 Truncated { at: usize, need: usize },
227 UnsupportedMeshKind(u16),
230 ClipBoneCountMismatch,
232 MissingChunk([u8; 4]),
234 BadMesh(kv6::ParseError),
236 BadClip(voxel_clip::ParseError),
238 BadAttachmentIndex {
243 bone: usize,
245 index: usize,
247 table_len: usize,
249 },
250 BadSkeleton {
254 bone: usize,
256 },
257}
258
259impl fmt::Display for ParseError {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 match self {
262 Self::BadMagic { got } => {
263 write!(
264 f,
265 "character bad magic: got {got:02x?}, expected {MAGIC:02x?}"
266 )
267 }
268 Self::UnsupportedVersion(v) => {
269 write!(
270 f,
271 "character unsupported version {v} (this build reads {VERSION})"
272 )
273 }
274 Self::Truncated { at, need } => {
275 write!(f, "character truncated: need {need} bytes at offset {at}")
276 }
277 Self::UnsupportedMeshKind(k) => {
278 write!(f, "character bone references unsupported mesh_kind {k}")
279 }
280 Self::ClipBoneCountMismatch => {
281 write!(f, "character skeletal clip numhin != bones.len()")
282 }
283 Self::MissingChunk(tag) => {
284 write!(
285 f,
286 "character missing required chunk {:?}",
287 String::from_utf8_lossy(tag)
288 )
289 }
290 Self::BadMesh(e) => write!(f, "character embedded kv6 mesh: {e}"),
291 Self::BadClip(e) => write!(f, "character embedded voxel clip: {e:?}"),
292 Self::BadAttachmentIndex {
293 bone,
294 index,
295 table_len,
296 } => write!(
297 f,
298 "character bone {bone}: attachment index {index} past table of {table_len}"
299 ),
300 Self::BadSkeleton { bone } => {
301 write!(f, "character bone {bone}: parent out of range or cyclic")
302 }
303 }
304 }
305}
306
307impl std::error::Error for ParseError {}
308
309impl From<OutOfBounds> for ParseError {
310 fn from(e: OutOfBounds) -> Self {
311 Self::Truncated {
312 at: e.at,
313 need: e.need,
314 }
315 }
316}
317
318pub fn parse(bytes: &[u8]) -> Result<Character, ParseError> {
332 let mut cur = Cursor::new(bytes);
333 let magic = cur.read_bytes(4)?;
334 if magic != MAGIC {
335 return Err(ParseError::BadMagic {
336 got: [magic[0], magic[1], magic[2], magic[3]],
337 });
338 }
339 let version = cur.read_u16()?;
340 if version != VERSION {
341 return Err(ParseError::UnsupportedVersion(version));
342 }
343
344 let mut meta: Option<&[u8]> = None;
349 let mut mshs: Option<&[u8]> = None;
350 let mut bons: Option<&[u8]> = None;
351 let mut clps: Option<&[u8]> = None;
352 let mut vclp: Option<&[u8]> = None;
353 let mut extra_chunks = Vec::new();
354 while cur.remaining() > 0 {
355 let tag_buf = cur.read_bytes(4)?;
356 let tag = [tag_buf[0], tag_buf[1], tag_buf[2], tag_buf[3]];
357 let len = cur.read_u32()? as usize;
358 let payload = cur.read_bytes(len)?;
359 match tag {
360 TAG_META => meta = Some(payload),
361 TAG_MSHS => mshs = Some(payload),
362 TAG_BONS => bons = Some(payload),
363 TAG_CLPS => clps = Some(payload),
364 TAG_VCLP => vclp = Some(payload),
365 _ => extra_chunks.push((tag, payload.to_vec())),
366 }
367 }
368
369 let meta = meta.ok_or(ParseError::MissingChunk(TAG_META))?;
370 let mshs = mshs.ok_or(ParseError::MissingChunk(TAG_MSHS))?;
371 let bons = bons.ok_or(ParseError::MissingChunk(TAG_BONS))?;
372
373 let (name, root) = parse_meta(meta)?;
374 let meshes = parse_mshs(mshs)?;
375 let bones = parse_bons(bons)?;
376 let clips = match clps {
377 Some(p) => parse_clps(p, bones.len())?,
378 None => Vec::new(),
379 };
380 let voxel_clips = match vclp {
381 Some(p) => parse_vclp(p)?,
382 None => Vec::new(),
383 };
384
385 for (bi, bone) in bones.iter().enumerate() {
390 for att in &bone.attachments {
391 let (index, table_len) = match att.target {
392 MeshRef::Static(i) => (i, meshes.len()),
393 MeshRef::Clip(i) => (i, voxel_clips.len()),
394 };
395 if index >= table_len {
396 return Err(ParseError::BadAttachmentIndex {
397 bone: bi,
398 index,
399 table_len,
400 });
401 }
402 }
403 if bone.hinge.parent != -1
405 && usize::try_from(bone.hinge.parent).map_or(true, |p| p >= bones.len())
406 {
407 return Err(ParseError::BadSkeleton { bone: bi });
408 }
409 }
410 for start in 0..bones.len() {
411 let mut cur_parent = bones[start].hinge.parent;
412 let mut hops = 0usize;
413 while cur_parent != -1 {
414 hops += 1;
415 if hops > bones.len() {
416 return Err(ParseError::BadSkeleton { bone: start });
417 }
418 cur_parent = bones[usize::try_from(cur_parent).expect("validated non-negative")]
419 .hinge
420 .parent;
421 }
422 }
423
424 Ok(Character {
425 name,
426 root,
427 meshes,
428 bones,
429 clips,
430 voxel_clips,
431 extra_chunks,
432 })
433}
434
435#[must_use]
446pub fn serialize(c: &Character) -> Vec<u8> {
447 let mut out = Vec::new();
448 out.extend_from_slice(&MAGIC);
449 out.extend_from_slice(&VERSION.to_le_bytes());
450
451 write_chunk(&mut out, TAG_META, |b| write_meta(b, c));
452 write_chunk(&mut out, TAG_MSHS, |b| write_mshs(b, c));
453 write_chunk(&mut out, TAG_BONS, |b| write_bons(b, c));
454 write_chunk(&mut out, TAG_CLPS, |b| write_clps(b, c));
455 write_chunk(&mut out, TAG_VCLP, |b| write_vclp(b, c));
456
457 for (tag, payload) in &c.extra_chunks {
458 write_chunk(&mut out, *tag, |b| b.extend_from_slice(payload));
459 }
460
461 out
462}
463
464impl Character {
465 #[must_use]
480 pub fn to_kfa_sprite(&self, clip: Option<usize>) -> KfaSprite {
481 let limbs = self
489 .bones
490 .iter()
491 .map(|b| {
492 let kv6 = b
493 .attachments
494 .iter()
495 .find_map(|a| match a.target {
496 MeshRef::Static(i) => Some(self.meshes[i].clone()),
497 MeshRef::Clip(_) => None,
498 })
499 .unwrap_or_else(|| Kv6::from_fn(1, 1, 1, |_, _, _| None));
500 Sprite::axis_aligned(kv6, self.root)
501 })
502 .collect();
503 let hinges = self.bones.iter().map(|b| b.hinge).collect();
504 let mut k = KfaSprite::new(limbs, hinges, self.root);
505 if let Some(ci) = clip {
506 if let ClipData::Skeletal { frmval, seq } = &self.clips[ci].data {
507 k.set_animation(frmval.clone(), seq.clone());
510 }
511 }
512 k
513 }
514
515 #[must_use]
534 pub fn to_kfa(&self, clip: Option<usize>, kv6_name: impl Into<Vec<u8>>) -> Kfa {
535 let hinges = self.bones.iter().map(|b| b.hinge).collect();
536 let (frmval, seq) = match clip.and_then(|ci| self.clips.get(ci)) {
540 Some(Clip {
541 data: ClipData::Skeletal { frmval, seq },
542 ..
543 }) => {
544 let angles = frmval
545 .iter()
546 .map(|row| {
547 row.iter()
548 .enumerate()
549 .map(|(bone, x)| {
550 let v = self.bones[bone].hinge.v[0];
551 x.hinge_angle([v.x, v.y, v.z])
552 })
553 .collect()
554 })
555 .collect();
556 (angles, seq.clone())
557 }
558 _ => (Vec::new(), Vec::new()),
559 };
560 Kfa {
561 kv6_name: kv6_name.into(),
562 hinges,
563 frmval,
564 seq,
565 }
566 }
567}
568
569fn parse_meta(payload: &[u8]) -> Result<(String, [f32; 3]), ParseError> {
572 let mut cur = Cursor::new(payload);
573 let name_len = cur.read_u16()? as usize;
574 let name = String::from_utf8_lossy(cur.read_bytes(name_len)?).into_owned();
575 let root = [cur.read_f32()?, cur.read_f32()?, cur.read_f32()?];
576 Ok((name, root))
577}
578
579fn parse_mshs(payload: &[u8]) -> Result<Vec<Kv6>, ParseError> {
580 let mut cur = Cursor::new(payload);
581 let count = cur.read_u32()? as usize;
582 let mut meshes = Vec::with_capacity(cur.clamped_capacity(count, 4));
585 for _ in 0..count {
586 let blob_len = cur.read_u32()? as usize;
587 let blob = cur.read_bytes(blob_len)?;
588 meshes.push(kv6::parse(blob).map_err(ParseError::BadMesh)?);
589 }
590 Ok(meshes)
591}
592
593fn parse_bons(payload: &[u8]) -> Result<Vec<Bone>, ParseError> {
594 let mut cur = Cursor::new(payload);
595 let count = cur.read_u32()? as usize;
596 let mut bones = Vec::with_capacity(cur.clamped_capacity(count, 70));
598 for _ in 0..count {
599 let name_len = cur.read_u16()? as usize;
600 let name = String::from_utf8_lossy(cur.read_bytes(name_len)?).into_owned();
601 let attach_count = cur.read_u32()? as usize;
602 let mut attachments = Vec::with_capacity(cur.clamped_capacity(attach_count, 54));
604 for _ in 0..attach_count {
605 attachments.push(read_attachment(&mut cur)?);
606 }
607 let hinge = read_hinge(&mut cur)?;
608 bones.push(Bone {
609 name,
610 attachments,
611 hinge,
612 });
613 }
614 Ok(bones)
615}
616
617fn read_attachment(cur: &mut Cursor<'_>) -> Result<Attachment, ParseError> {
620 let mesh_kind = cur.read_u16()?;
621 let index = cur.read_u32()? as usize;
622 let target = match mesh_kind {
623 MESH_KIND_STATIC => MeshRef::Static(index),
624 MESH_KIND_CLIP => MeshRef::Clip(index),
625 other => return Err(ParseError::UnsupportedMeshKind(other)),
626 };
627 let local_offset = read_bonexform(cur)?;
628 let speed_q8 = cur.read_i32()?;
629 let start_phase_ms = cur.read_u32()?;
630 Ok(Attachment {
631 target,
632 local_offset,
633 playback: ClipPlayback {
634 speed_q8,
635 start_phase_ms,
636 },
637 })
638}
639
640fn parse_vclp(payload: &[u8]) -> Result<Vec<VoxelClip>, ParseError> {
641 let mut cur = Cursor::new(payload);
642 let count = cur.read_u32()? as usize;
643 let mut clips = Vec::with_capacity(cur.clamped_capacity(count, 4));
645 for _ in 0..count {
646 let blob_len = cur.read_u32()? as usize;
647 let blob = cur.read_bytes(blob_len)?;
648 clips.push(VoxelClip::parse(blob).map_err(ParseError::BadClip)?);
649 }
650 Ok(clips)
651}
652
653fn parse_clps(payload: &[u8], numbone: usize) -> Result<Vec<Clip>, ParseError> {
654 let mut cur = Cursor::new(payload);
655 let count = cur.read_u32()? as usize;
656 let mut clips = Vec::with_capacity(cur.clamped_capacity(count, 8));
658 for _ in 0..count {
659 let name_len = cur.read_u16()? as usize;
660 let name = String::from_utf8_lossy(cur.read_bytes(name_len)?).into_owned();
661 let kind = cur.read_u16()?;
662 let payload_len = cur.read_u32()? as usize;
663 let body = cur.read_bytes(payload_len)?;
664 let data = if kind == CLIP_KIND_SKELETAL {
665 parse_skeletal(body, numbone)?
666 } else {
667 ClipData::Unknown {
668 kind,
669 bytes: body.to_vec(),
670 }
671 };
672 clips.push(Clip { name, data });
673 }
674 Ok(clips)
675}
676
677fn parse_skeletal(body: &[u8], numbone: usize) -> Result<ClipData, ParseError> {
678 let mut cur = Cursor::new(body);
679 let numfrm = cur.read_u32()? as usize;
680 let numhin = cur.read_u32()? as usize;
681 if numhin != numbone {
682 return Err(ParseError::ClipBoneCountMismatch);
683 }
684 let mut frmval = Vec::with_capacity(cur.clamped_capacity(numfrm, numhin.saturating_mul(40)));
686 for _ in 0..numfrm {
687 let mut row = Vec::with_capacity(cur.clamped_capacity(numhin, 40));
688 for _ in 0..numhin {
689 row.push(read_bonexform(&mut cur)?);
690 }
691 frmval.push(row);
692 }
693 let seqcount = cur.read_u32()? as usize;
694 let mut seq = Vec::with_capacity(cur.clamped_capacity(seqcount, 8));
695 for _ in 0..seqcount {
696 let tim = cur.read_i32()?;
697 let frm = cur.read_i32()?;
698 seq.push(Seq { tim, frm });
699 }
700 Ok(ClipData::Skeletal { frmval, seq })
701}
702
703fn read_hinge(cur: &mut Cursor<'_>) -> Result<Hinge, OutOfBounds> {
704 let parent = cur.read_i32()?;
705 let p0 = read_point3(cur)?;
706 let p1 = read_point3(cur)?;
707 let v0 = read_point3(cur)?;
708 let v1 = read_point3(cur)?;
709 let vmin = cur.read_i16()?;
710 let vmax = cur.read_i16()?;
711 let htype = cur.read_u8()?;
712 let filler_buf = cur.read_bytes(7)?;
713 let mut filler = [0u8; 7];
714 filler.copy_from_slice(filler_buf);
715 Ok(Hinge {
716 parent,
717 p: [p0, p1],
718 v: [v0, v1],
719 vmin,
720 vmax,
721 htype,
722 filler,
723 })
724}
725
726fn read_point3(cur: &mut Cursor<'_>) -> Result<Point3, OutOfBounds> {
727 Ok(Point3 {
728 x: cur.read_f32()?,
729 y: cur.read_f32()?,
730 z: cur.read_f32()?,
731 })
732}
733
734fn read_bonexform(cur: &mut Cursor<'_>) -> Result<BoneXform, OutOfBounds> {
737 let t = [cur.read_f32()?, cur.read_f32()?, cur.read_f32()?];
738 let r = Quat {
739 x: cur.read_f32()?,
740 y: cur.read_f32()?,
741 z: cur.read_f32()?,
742 w: cur.read_f32()?,
743 };
744 let s = [cur.read_f32()?, cur.read_f32()?, cur.read_f32()?];
745 Ok(BoneXform { t, r, s })
746}
747
748fn write_bonexform(out: &mut Vec<u8>, x: &BoneXform) {
749 for v in [
750 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],
751 ] {
752 out.extend_from_slice(&v.to_le_bytes());
753 }
754}
755
756fn write_chunk(out: &mut Vec<u8>, tag: [u8; 4], body: impl FnOnce(&mut Vec<u8>)) {
760 out.extend_from_slice(&tag);
761 let len_pos = out.len();
762 out.extend_from_slice(&0u32.to_le_bytes()); let start = out.len();
764 body(out);
765 let len = u32::try_from(out.len() - start).expect("chunk payload length must fit in u32");
766 out[len_pos..len_pos + 4].copy_from_slice(&len.to_le_bytes());
767}
768
769fn write_meta(out: &mut Vec<u8>, c: &Character) {
770 let name = c.name.as_bytes();
771 let name_len = u16::try_from(name.len()).expect("character name length must fit in u16");
772 out.extend_from_slice(&name_len.to_le_bytes());
773 out.extend_from_slice(name);
774 for v in c.root {
775 out.extend_from_slice(&v.to_le_bytes());
776 }
777}
778
779fn write_mshs(out: &mut Vec<u8>, c: &Character) {
780 let count = u32::try_from(c.meshes.len()).expect("mesh count must fit in u32");
781 out.extend_from_slice(&count.to_le_bytes());
782 for mesh in &c.meshes {
783 let blob = kv6::serialize(mesh);
784 let blob_len = u32::try_from(blob.len()).expect("kv6 blob length must fit in u32");
785 out.extend_from_slice(&blob_len.to_le_bytes());
786 out.extend_from_slice(&blob);
787 }
788}
789
790fn write_bons(out: &mut Vec<u8>, c: &Character) {
791 let count = u32::try_from(c.bones.len()).expect("bone count must fit in u32");
792 out.extend_from_slice(&count.to_le_bytes());
793 for bone in &c.bones {
794 let name = bone.name.as_bytes();
795 let name_len = u16::try_from(name.len()).expect("bone name length must fit in u16");
796 out.extend_from_slice(&name_len.to_le_bytes());
797 out.extend_from_slice(name);
798 let attach_count =
799 u32::try_from(bone.attachments.len()).expect("attachment count must fit in u32");
800 out.extend_from_slice(&attach_count.to_le_bytes());
801 for a in &bone.attachments {
802 write_attachment(out, a);
803 }
804 write_hinge(out, &bone.hinge);
805 }
806}
807
808fn write_attachment(out: &mut Vec<u8>, a: &Attachment) {
809 let (kind, index) = match a.target {
810 MeshRef::Static(i) => (MESH_KIND_STATIC, i),
811 MeshRef::Clip(i) => (MESH_KIND_CLIP, i),
812 };
813 out.extend_from_slice(&kind.to_le_bytes());
814 let idx = u32::try_from(index).expect("attachment index must fit in u32");
815 out.extend_from_slice(&idx.to_le_bytes());
816 write_bonexform(out, &a.local_offset);
817 out.extend_from_slice(&a.playback.speed_q8.to_le_bytes());
818 out.extend_from_slice(&a.playback.start_phase_ms.to_le_bytes());
819}
820
821fn write_vclp(out: &mut Vec<u8>, c: &Character) {
822 let count = u32::try_from(c.voxel_clips.len()).expect("voxel clip count must fit in u32");
823 out.extend_from_slice(&count.to_le_bytes());
824 for clip in &c.voxel_clips {
825 let blob = clip.serialize();
826 let blob_len = u32::try_from(blob.len()).expect("voxel clip blob length must fit in u32");
827 out.extend_from_slice(&blob_len.to_le_bytes());
828 out.extend_from_slice(&blob);
829 }
830}
831
832fn write_clps(out: &mut Vec<u8>, c: &Character) {
833 let count = u32::try_from(c.clips.len()).expect("clip count must fit in u32");
834 out.extend_from_slice(&count.to_le_bytes());
835 for clip in &c.clips {
836 let name = clip.name.as_bytes();
837 let name_len = u16::try_from(name.len()).expect("clip name length must fit in u16");
838 out.extend_from_slice(&name_len.to_le_bytes());
839 out.extend_from_slice(name);
840 match &clip.data {
841 ClipData::Skeletal { frmval, seq } => {
842 out.extend_from_slice(&CLIP_KIND_SKELETAL.to_le_bytes());
843 write_chunk_body(out, |b| write_skeletal(b, frmval, seq));
844 }
845 ClipData::Unknown { kind, bytes } => {
846 out.extend_from_slice(&kind.to_le_bytes());
847 write_chunk_body(out, |b| b.extend_from_slice(bytes));
848 }
849 }
850 }
851}
852
853fn write_chunk_body(out: &mut Vec<u8>, body: impl FnOnce(&mut Vec<u8>)) {
857 let len_pos = out.len();
858 out.extend_from_slice(&0u32.to_le_bytes());
859 let start = out.len();
860 body(out);
861 let len = u32::try_from(out.len() - start).expect("clip payload length must fit in u32");
862 out[len_pos..len_pos + 4].copy_from_slice(&len.to_le_bytes());
863}
864
865fn write_skeletal(out: &mut Vec<u8>, frmval: &[Vec<BoneXform>], seq: &[Seq]) {
866 let numhin = frmval.first().map_or(0, Vec::len);
867 for (i, row) in frmval.iter().enumerate() {
868 assert!(
869 row.len() == numhin,
870 "skeletal frmval[{i}].len() = {} != numhin {numhin}",
871 row.len(),
872 );
873 }
874 let numfrm = u32::try_from(frmval.len()).expect("numfrm must fit in u32");
875 let numhin_u32 = u32::try_from(numhin).expect("numhin must fit in u32");
876 out.extend_from_slice(&numfrm.to_le_bytes());
877 out.extend_from_slice(&numhin_u32.to_le_bytes());
878 for row in frmval {
879 for v in row {
880 write_bonexform(out, v);
881 }
882 }
883 let seqcount = u32::try_from(seq.len()).expect("seqcount must fit in u32");
884 out.extend_from_slice(&seqcount.to_le_bytes());
885 for s in seq {
886 out.extend_from_slice(&s.tim.to_le_bytes());
887 out.extend_from_slice(&s.frm.to_le_bytes());
888 }
889}
890
891fn write_hinge(out: &mut Vec<u8>, h: &Hinge) {
892 out.extend_from_slice(&h.parent.to_le_bytes());
893 for p in h.p {
894 write_point3(out, p);
895 }
896 for v in h.v {
897 write_point3(out, v);
898 }
899 out.extend_from_slice(&h.vmin.to_le_bytes());
900 out.extend_from_slice(&h.vmax.to_le_bytes());
901 out.push(h.htype);
902 out.extend_from_slice(&h.filler);
903}
904
905fn write_point3(out: &mut Vec<u8>, p: Point3) {
906 out.extend_from_slice(&p.x.to_le_bytes());
907 out.extend_from_slice(&p.y.to_le_bytes());
908 out.extend_from_slice(&p.z.to_le_bytes());
909}
910
911const _: () = assert!(HINGE_SIZE == 64);
913
914#[cfg(test)]
917mod tests {
918 use super::*;
919
920 fn unit_kv6(fill: u32) -> Kv6 {
921 Kv6 {
923 xsiz: 1,
924 ysiz: 1,
925 zsiz: 1,
926 xpiv: 0.5,
927 ypiv: 0.5,
928 zpiv: 0.5,
929 voxels: vec![kv6::Voxel {
930 col: fill,
931 z: 0,
932 vis: 0x3f,
933 dir: 0,
934 }],
935 xlen: vec![1],
936 ylen: vec![vec![1]],
937 palette: None,
938 }
939 }
940
941 fn hinge(parent: i32) -> Hinge {
942 let zero = Point3 {
943 x: 0.0,
944 y: 0.0,
945 z: 0.0,
946 };
947 let axis = Point3 {
948 x: 0.0,
949 y: 0.0,
950 z: 1.0,
951 };
952 Hinge {
953 parent,
954 p: [zero, zero],
955 v: [axis, axis],
956 vmin: 0,
957 vmax: 0,
958 htype: 0,
959 filler: [0; 7],
960 }
961 }
962
963 fn synthetic_character() -> Character {
964 Character {
965 name: "anasaur".to_string(),
966 root: [70.0, -75.0, 50.0],
967 meshes: vec![unit_kv6(0x00ff_8040), unit_kv6(0x0010_2030)],
968 bones: vec![
969 Bone {
970 name: "body".to_string(),
971 attachments: vec![Attachment::static_mesh(0)],
972 hinge: hinge(-1),
973 },
974 Bone {
975 name: "arm".to_string(),
976 attachments: vec![Attachment::static_mesh(1)],
977 hinge: hinge(0),
978 },
979 ],
980 clips: vec![Clip {
981 name: "wave".to_string(),
982 data: ClipData::Skeletal {
985 frmval: [[0i16, 0], [0, 16000], [0, 0], [0, -16000]]
986 .iter()
987 .map(|[r, a]| {
988 let z = [0.0, 0.0, 1.0];
989 vec![
990 BoneXform::from_hinge_angle(z, *r),
991 BoneXform::from_hinge_angle(z, *a),
992 ]
993 })
994 .collect(),
995 seq: vec![
996 Seq { tim: 0, frm: 0 },
997 Seq { tim: 500, frm: 1 },
998 Seq { tim: 1000, frm: 2 },
999 Seq { tim: 1500, frm: 3 },
1000 Seq { tim: 2000, frm: !0 },
1001 ],
1002 },
1003 }],
1004 voxel_clips: Vec::new(),
1005 extra_chunks: Vec::new(),
1006 }
1007 }
1008
1009 #[test]
1010 fn roundtrips_byte_equal() {
1011 let c = synthetic_character();
1012 let bytes = serialize(&c);
1013 let parsed = parse(&bytes).expect("parse synthetic");
1014 let bytes2 = serialize(&parsed);
1015 assert_eq!(bytes, bytes2, "byte-level round-trip failed");
1016 assert_eq!(parsed.name, c.name);
1017 assert_eq!(parsed.root, c.root);
1018 assert_eq!(parsed.meshes.len(), c.meshes.len());
1019 assert_eq!(parsed.bones.len(), c.bones.len());
1020 assert_eq!(parsed.bones[1].name, "arm");
1021 assert_eq!(parsed.bones[1].attachments[0].target, MeshRef::Static(1));
1022 assert_eq!(parsed.bones[1].hinge.parent, 0);
1023 assert_eq!(parsed.clips.len(), 1);
1024 assert_eq!(parsed.clips[0].data, c.clips[0].data);
1025 assert!(parsed.voxel_clips.is_empty());
1026 }
1027
1028 #[test]
1029 fn roundtrips_with_clips_and_multi_attachment() {
1030 use crate::voxel_clip::{LoopMode, VoxelClip, VoxelFrame};
1031 let frame = VoxelFrame {
1033 occupancy: vec![1u32],
1034 colors: vec![0x8011_2233],
1035 color_offsets: vec![0, 1],
1036 };
1037 let clip = VoxelClip::from_frames(
1038 [1, 1, 4],
1039 [0.5, 0.5, 2.0],
1040 1.0,
1041 LoopMode::Loop,
1042 &[frame],
1043 &[],
1044 33,
1045 0,
1046 );
1047
1048 let mut c = synthetic_character();
1049 c.voxel_clips = vec![clip];
1050 c.bones[0].attachments.push(Attachment {
1053 target: MeshRef::Clip(0),
1054 local_offset: BoneXform {
1055 t: [1.0, 2.0, 3.0],
1056 r: Quat::IDENTITY,
1057 s: [1.0, 1.0, 1.0],
1058 },
1059 playback: ClipPlayback {
1060 speed_q8: 512,
1061 start_phase_ms: 100,
1062 },
1063 });
1064
1065 let bytes = serialize(&c);
1066 let parsed = parse(&bytes).expect("parse v3 with clips");
1067 assert_eq!(serialize(&parsed), bytes, "byte round-trip");
1068
1069 assert_eq!(parsed.voxel_clips.len(), 1);
1070 assert_eq!(parsed.voxel_clips[0], c.voxel_clips[0]);
1071
1072 let body = &parsed.bones[0].attachments;
1074 assert_eq!(body.len(), 2);
1075 assert_eq!(body[0].target, MeshRef::Static(0));
1076 assert_eq!(body[1].target, MeshRef::Clip(0));
1077 assert_eq!(body[1].local_offset.t, [1.0, 2.0, 3.0]);
1078 assert_eq!(
1079 body[1].playback,
1080 ClipPlayback {
1081 speed_q8: 512,
1082 start_phase_ms: 100,
1083 }
1084 );
1085
1086 let k = parsed.to_kfa_sprite(None);
1089 assert_eq!(k.limbs.len(), parsed.bones.len());
1090 }
1091
1092 #[test]
1093 fn to_kfa_sprite_builds_renderable() {
1094 let c = synthetic_character();
1095 let mut k = c.to_kfa_sprite(Some(0));
1096 assert_eq!(k.limbs.len(), 2);
1097 assert_eq!(k.hinges.len(), 2);
1098 assert_eq!(k.p, c.root);
1099 k.animsprite(500);
1101 assert_ne!(
1102 k.kfaval[1],
1103 crate::xform::BoneXform::IDENTITY,
1104 "baked clip should move the arm bone"
1105 );
1106
1107 let mut rest = c.to_kfa_sprite(None);
1109 rest.animsprite(500);
1110 assert_eq!(rest.kfaval[1], crate::xform::BoneXform::IDENTITY);
1111 }
1112
1113 #[test]
1114 fn to_kfa_export_is_lossy_but_valid() {
1115 let c = synthetic_character();
1116 let kfa = c.to_kfa(Some(0), "coco.kv6");
1117 assert_eq!(kfa.kv6_name, b"coco.kv6");
1119 assert_eq!(kfa.hinges.len(), 2);
1120 assert_eq!(kfa.hinges[1].parent, 0);
1121 if let ClipData::Skeletal { frmval, seq } = &c.clips[0].data {
1122 let z = [0.0, 0.0, 1.0];
1125 let expected: Vec<Vec<i16>> = frmval
1126 .iter()
1127 .map(|row| row.iter().map(|x| x.hinge_angle(z)).collect())
1128 .collect();
1129 assert_eq!(kfa.frmval, expected);
1130 assert_eq!(&kfa.seq, seq);
1131 } else {
1132 panic!("clip 0 should be skeletal");
1133 }
1134 let bytes = crate::kfa::serialize(&kfa);
1137 let reparsed = crate::kfa::parse(&bytes).expect("export round-trips through kfa");
1138 assert_eq!(reparsed.kv6_name, kfa.kv6_name);
1139 assert_eq!(reparsed.frmval, kfa.frmval);
1140 assert_eq!(reparsed.seq, kfa.seq);
1141 }
1142
1143 #[test]
1144 fn to_kfa_without_clip_is_posable() {
1145 let c = synthetic_character();
1146 let kfa = c.to_kfa(None, b"x.kv6".to_vec());
1148 assert_eq!(kfa.hinges.len(), 2);
1149 assert!(kfa.frmval.is_empty());
1150 assert!(kfa.seq.is_empty());
1151 let bytes = crate::kfa::serialize(&kfa);
1153 assert!(crate::kfa::parse(&bytes).is_ok());
1154 }
1155
1156 #[test]
1157 fn clips_may_be_empty() {
1158 let mut c = synthetic_character();
1159 c.clips.clear();
1160 let bytes = serialize(&c);
1161 let parsed = parse(&bytes).expect("parse");
1162 assert!(parsed.clips.is_empty());
1163 let k = parsed.to_kfa_sprite(None);
1165 assert_eq!(k.limbs.len(), 2);
1166 }
1167
1168 #[test]
1169 fn unknown_top_level_chunk_is_skipped_and_preserved() {
1170 let mut bytes = serialize(&synthetic_character());
1171 bytes.extend_from_slice(b"ZZZZ");
1173 bytes.extend_from_slice(&3u32.to_le_bytes());
1174 bytes.extend_from_slice(&[1, 2, 3]);
1175 let parsed = parse(&bytes).expect("parse with unknown chunk");
1176 assert_eq!(parsed.bones.len(), 2, "known chunks still parse");
1177 assert_eq!(parsed.extra_chunks, vec![(*b"ZZZZ", vec![1u8, 2, 3])]);
1178 let bytes2 = serialize(&parsed);
1180 assert_eq!(
1181 bytes2, bytes,
1182 "unknown chunk preserved byte-equal on re-save"
1183 );
1184 }
1185
1186 #[test]
1187 fn unknown_clip_kind_preserved() {
1188 let mut c = synthetic_character();
1189 c.clips.push(Clip {
1190 name: "mystery".to_string(),
1191 data: ClipData::Unknown {
1192 kind: 7,
1193 bytes: vec![9, 8, 7, 6],
1194 },
1195 });
1196 let bytes = serialize(&c);
1197 let parsed = parse(&bytes).expect("parse");
1198 assert!(matches!(parsed.clips[0].data, ClipData::Skeletal { .. }));
1200 assert_eq!(
1201 parsed.clips[1].data,
1202 ClipData::Unknown {
1203 kind: 7,
1204 bytes: vec![9, 8, 7, 6]
1205 }
1206 );
1207 assert_eq!(serialize(&parsed), bytes);
1209 }
1210
1211 #[test]
1212 fn bad_magic_errors() {
1213 let mut bytes = serialize(&synthetic_character());
1214 bytes[0] ^= 0xff;
1215 assert!(matches!(parse(&bytes), Err(ParseError::BadMagic { .. })));
1216 }
1217
1218 #[test]
1219 fn version_mismatch_errors() {
1220 let mut bytes = serialize(&synthetic_character());
1221 bytes[4] = 0xff;
1223 bytes[5] = 0xff;
1224 assert!(matches!(
1225 parse(&bytes),
1226 Err(ParseError::UnsupportedVersion(0xffff))
1227 ));
1228 }
1229
1230 #[test]
1231 fn truncated_errors() {
1232 let bytes = serialize(&synthetic_character());
1233 assert!(matches!(
1234 parse(&bytes[..bytes.len() - 4]),
1235 Err(ParseError::Truncated { .. })
1236 ));
1237 }
1238
1239 #[test]
1240 fn missing_required_chunk_errors() {
1241 let mut bytes = Vec::new();
1243 bytes.extend_from_slice(&MAGIC);
1244 bytes.extend_from_slice(&VERSION.to_le_bytes());
1245 assert!(matches!(
1246 parse(&bytes),
1247 Err(ParseError::MissingChunk(TAG_META))
1248 ));
1249 }
1250
1251 #[test]
1252 fn unsupported_mesh_kind_errors() {
1253 let mut bytes = serialize(&synthetic_character());
1254 let pos = find_tag(&bytes, *b"BONS");
1259 let kind_off = pos + 8 + 4 + 2 + 4 + 4;
1260 bytes[kind_off] = 2;
1261 bytes[kind_off + 1] = 0;
1262 assert!(matches!(
1263 parse(&bytes),
1264 Err(ParseError::UnsupportedMeshKind(2))
1265 ));
1266 }
1267
1268 #[test]
1269 fn clip_bone_count_mismatch_errors() {
1270 let mut c = synthetic_character();
1273 c.bones.pop(); c.bones[0].attachments = vec![Attachment::static_mesh(0)];
1275 let bytes = serialize(&c);
1277 assert!(matches!(
1278 parse(&bytes),
1279 Err(ParseError::ClipBoneCountMismatch)
1280 ));
1281 }
1282
1283 #[test]
1284 fn bad_embedded_mesh_errors() {
1285 let mut bytes = serialize(&synthetic_character());
1286 let pos = find_tag(&bytes, *b"MSHS");
1289 let kv6_magic_off = pos + 8 + 4 + 4;
1290 bytes[kv6_magic_off] ^= 0xff;
1291 assert!(matches!(parse(&bytes), Err(ParseError::BadMesh(_))));
1292 }
1293
1294 fn find_tag(bytes: &[u8], tag: [u8; 4]) -> usize {
1297 let mut pos = 6; while pos + 8 <= bytes.len() {
1299 let here = &bytes[pos..pos + 4];
1300 let len = u32::from_le_bytes([
1301 bytes[pos + 4],
1302 bytes[pos + 5],
1303 bytes[pos + 6],
1304 bytes[pos + 7],
1305 ]) as usize;
1306 if here == tag {
1307 return pos;
1308 }
1309 pos += 8 + len;
1310 }
1311 panic!("tag {tag:?} not found");
1312 }
1313
1314 #[test]
1317 fn parse_rejects_out_of_range_attachment_index() {
1318 let mut c = synthetic_character();
1321 c.bones[1].attachments = vec![Attachment::static_mesh(99)];
1322 let bytes = serialize(&c);
1323 assert!(matches!(
1324 parse(&bytes),
1325 Err(ParseError::BadAttachmentIndex {
1326 bone: 1,
1327 index: 99,
1328 table_len: 2,
1329 })
1330 ));
1331 }
1332
1333 #[test]
1334 fn parse_rejects_bad_or_cyclic_bone_skeleton() {
1335 let mut c = synthetic_character();
1337 c.bones[1].hinge.parent = 42;
1338 assert!(matches!(
1339 parse(&serialize(&c)),
1340 Err(ParseError::BadSkeleton { bone: 1 })
1341 ));
1342 let mut c = synthetic_character();
1344 c.bones[0].hinge.parent = 1;
1345 assert!(matches!(
1346 parse(&serialize(&c)),
1347 Err(ParseError::BadSkeleton { .. })
1348 ));
1349 }
1350}