1use core::fmt;
43
44use crate::bytes::{Cursor, OutOfBounds};
45use crate::kfa::{Hinge, Kfa, KfaSprite, Point3, Seq};
46use crate::kv6::{self, Kv6};
47use crate::sprite::Sprite;
48use crate::voxel_clip::{self, VoxelClip};
49use crate::xform::{BoneXform, Quat};
50
51const MAGIC: [u8; 4] = *b"RKCH";
52const VERSION: u16 = 3;
57
58const TAG_META: [u8; 4] = *b"META";
59const TAG_MSHS: [u8; 4] = *b"MSHS";
60const TAG_BONS: [u8; 4] = *b"BONS";
61const TAG_CLPS: [u8; 4] = *b"CLPS";
62const TAG_VCLP: [u8; 4] = *b"VCLP";
64
65const HINGE_SIZE: usize = 64;
66
67const MESH_KIND_STATIC: u16 = 0;
69const MESH_KIND_CLIP: u16 = 1;
72
73const CLIP_KIND_SKELETAL: u16 = 0;
77
78#[derive(Debug, Clone)]
86pub struct Character {
87 pub name: String,
89 pub root: [f32; 3],
91 pub meshes: Vec<Kv6>,
93 pub bones: Vec<Bone>,
95 pub clips: Vec<Clip>,
98 pub voxel_clips: Vec<VoxelClip>,
101 pub extra_chunks: Vec<([u8; 4], Vec<u8>)>,
105}
106
107#[derive(Debug, Clone)]
114pub struct Bone {
115 pub name: String,
116 pub attachments: Vec<Attachment>,
119 pub hinge: Hinge,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq)]
127pub struct Attachment {
128 pub target: MeshRef,
130 pub local_offset: BoneXform,
134 pub playback: ClipPlayback,
136}
137
138impl Attachment {
139 #[must_use]
142 pub fn static_mesh(mesh_id: usize) -> Self {
143 Self {
144 target: MeshRef::Static(mesh_id),
145 local_offset: BoneXform::IDENTITY,
146 playback: ClipPlayback::default(),
147 }
148 }
149
150 #[must_use]
152 pub fn clip(clip_id: usize) -> Self {
153 Self {
154 target: MeshRef::Clip(clip_id),
155 local_offset: BoneXform::IDENTITY,
156 playback: ClipPlayback::default(),
157 }
158 }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub struct ClipPlayback {
167 pub speed_q8: i32,
170 pub start_phase_ms: u32,
173}
174
175impl Default for ClipPlayback {
176 fn default() -> Self {
177 Self {
178 speed_q8: 256,
179 start_phase_ms: 0,
180 }
181 }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub enum MeshRef {
188 Static(usize),
190 Clip(usize),
192}
193
194#[derive(Debug, Clone)]
196pub struct Clip {
197 pub name: String,
198 pub data: ClipData,
199}
200
201#[derive(Debug, Clone, PartialEq)]
203pub enum ClipData {
204 Skeletal {
209 frmval: Vec<Vec<BoneXform>>,
210 seq: Vec<Seq>,
211 },
212 Unknown { kind: u16, bytes: Vec<u8> },
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
219pub enum ParseError {
220 BadMagic { got: [u8; 4] },
222 UnsupportedVersion(u16),
224 Truncated { at: usize, need: usize },
226 UnsupportedMeshKind(u16),
229 ClipBoneCountMismatch,
231 MissingChunk([u8; 4]),
233 BadMesh(kv6::ParseError),
235 BadClip(voxel_clip::ParseError),
237}
238
239impl fmt::Display for ParseError {
240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241 match self {
242 Self::BadMagic { got } => {
243 write!(
244 f,
245 "character bad magic: got {got:02x?}, expected {MAGIC:02x?}"
246 )
247 }
248 Self::UnsupportedVersion(v) => {
249 write!(
250 f,
251 "character unsupported version {v} (this build reads {VERSION})"
252 )
253 }
254 Self::Truncated { at, need } => {
255 write!(f, "character truncated: need {need} bytes at offset {at}")
256 }
257 Self::UnsupportedMeshKind(k) => {
258 write!(f, "character bone references unsupported mesh_kind {k}")
259 }
260 Self::ClipBoneCountMismatch => {
261 write!(f, "character skeletal clip numhin != bones.len()")
262 }
263 Self::MissingChunk(tag) => {
264 write!(
265 f,
266 "character missing required chunk {:?}",
267 String::from_utf8_lossy(tag)
268 )
269 }
270 Self::BadMesh(e) => write!(f, "character embedded kv6 mesh: {e}"),
271 Self::BadClip(e) => write!(f, "character embedded voxel clip: {e:?}"),
272 }
273 }
274}
275
276impl std::error::Error for ParseError {}
277
278impl From<OutOfBounds> for ParseError {
279 fn from(e: OutOfBounds) -> Self {
280 Self::Truncated {
281 at: e.at,
282 need: e.need,
283 }
284 }
285}
286
287pub fn parse(bytes: &[u8]) -> Result<Character, ParseError> {
301 let mut cur = Cursor::new(bytes);
302 let magic = cur.read_bytes(4)?;
303 if magic != MAGIC {
304 return Err(ParseError::BadMagic {
305 got: [magic[0], magic[1], magic[2], magic[3]],
306 });
307 }
308 let version = cur.read_u16()?;
309 if version != VERSION {
310 return Err(ParseError::UnsupportedVersion(version));
311 }
312
313 let mut meta: Option<&[u8]> = None;
318 let mut mshs: Option<&[u8]> = None;
319 let mut bons: Option<&[u8]> = None;
320 let mut clps: Option<&[u8]> = None;
321 let mut vclp: Option<&[u8]> = None;
322 let mut extra_chunks = Vec::new();
323 while cur.remaining() > 0 {
324 let tag_buf = cur.read_bytes(4)?;
325 let tag = [tag_buf[0], tag_buf[1], tag_buf[2], tag_buf[3]];
326 let len = cur.read_u32()? as usize;
327 let payload = cur.read_bytes(len)?;
328 match tag {
329 TAG_META => meta = Some(payload),
330 TAG_MSHS => mshs = Some(payload),
331 TAG_BONS => bons = Some(payload),
332 TAG_CLPS => clps = Some(payload),
333 TAG_VCLP => vclp = Some(payload),
334 _ => extra_chunks.push((tag, payload.to_vec())),
335 }
336 }
337
338 let meta = meta.ok_or(ParseError::MissingChunk(TAG_META))?;
339 let mshs = mshs.ok_or(ParseError::MissingChunk(TAG_MSHS))?;
340 let bons = bons.ok_or(ParseError::MissingChunk(TAG_BONS))?;
341
342 let (name, root) = parse_meta(meta)?;
343 let meshes = parse_mshs(mshs)?;
344 let bones = parse_bons(bons)?;
345 let clips = match clps {
346 Some(p) => parse_clps(p, bones.len())?,
347 None => Vec::new(),
348 };
349 let voxel_clips = match vclp {
350 Some(p) => parse_vclp(p)?,
351 None => Vec::new(),
352 };
353
354 Ok(Character {
355 name,
356 root,
357 meshes,
358 bones,
359 clips,
360 voxel_clips,
361 extra_chunks,
362 })
363}
364
365#[must_use]
376pub fn serialize(c: &Character) -> Vec<u8> {
377 let mut out = Vec::new();
378 out.extend_from_slice(&MAGIC);
379 out.extend_from_slice(&VERSION.to_le_bytes());
380
381 write_chunk(&mut out, TAG_META, |b| write_meta(b, c));
382 write_chunk(&mut out, TAG_MSHS, |b| write_mshs(b, c));
383 write_chunk(&mut out, TAG_BONS, |b| write_bons(b, c));
384 write_chunk(&mut out, TAG_CLPS, |b| write_clps(b, c));
385 write_chunk(&mut out, TAG_VCLP, |b| write_vclp(b, c));
386
387 for (tag, payload) in &c.extra_chunks {
388 write_chunk(&mut out, *tag, |b| b.extend_from_slice(payload));
389 }
390
391 out
392}
393
394impl Character {
395 #[must_use]
410 pub fn to_kfa_sprite(&self, clip: Option<usize>) -> KfaSprite {
411 let limbs = self
419 .bones
420 .iter()
421 .map(|b| {
422 let kv6 = b
423 .attachments
424 .iter()
425 .find_map(|a| match a.target {
426 MeshRef::Static(i) => Some(self.meshes[i].clone()),
427 MeshRef::Clip(_) => None,
428 })
429 .unwrap_or_else(|| Kv6::from_fn(1, 1, 1, |_, _, _| None));
430 Sprite::axis_aligned(kv6, self.root)
431 })
432 .collect();
433 let hinges = self.bones.iter().map(|b| b.hinge).collect();
434 let mut k = KfaSprite::new(limbs, hinges, self.root);
435 if let Some(ci) = clip {
436 if let ClipData::Skeletal { frmval, seq } = &self.clips[ci].data {
437 k.set_animation(frmval.clone(), seq.clone());
440 }
441 }
442 k
443 }
444
445 #[must_use]
464 pub fn to_kfa(&self, clip: Option<usize>, kv6_name: impl Into<Vec<u8>>) -> Kfa {
465 let hinges = self.bones.iter().map(|b| b.hinge).collect();
466 let (frmval, seq) = match clip.and_then(|ci| self.clips.get(ci)) {
470 Some(Clip {
471 data: ClipData::Skeletal { frmval, seq },
472 ..
473 }) => {
474 let angles = frmval
475 .iter()
476 .map(|row| {
477 row.iter()
478 .enumerate()
479 .map(|(bone, x)| {
480 let v = self.bones[bone].hinge.v[0];
481 x.hinge_angle([v.x, v.y, v.z])
482 })
483 .collect()
484 })
485 .collect();
486 (angles, seq.clone())
487 }
488 _ => (Vec::new(), Vec::new()),
489 };
490 Kfa {
491 kv6_name: kv6_name.into(),
492 hinges,
493 frmval,
494 seq,
495 }
496 }
497}
498
499fn parse_meta(payload: &[u8]) -> Result<(String, [f32; 3]), ParseError> {
502 let mut cur = Cursor::new(payload);
503 let name_len = cur.read_u16()? as usize;
504 let name = String::from_utf8_lossy(cur.read_bytes(name_len)?).into_owned();
505 let root = [cur.read_f32()?, cur.read_f32()?, cur.read_f32()?];
506 Ok((name, root))
507}
508
509fn parse_mshs(payload: &[u8]) -> Result<Vec<Kv6>, ParseError> {
510 let mut cur = Cursor::new(payload);
511 let count = cur.read_u32()? as usize;
512 let mut meshes = Vec::with_capacity(count);
513 for _ in 0..count {
514 let blob_len = cur.read_u32()? as usize;
515 let blob = cur.read_bytes(blob_len)?;
516 meshes.push(kv6::parse(blob).map_err(ParseError::BadMesh)?);
517 }
518 Ok(meshes)
519}
520
521fn parse_bons(payload: &[u8]) -> Result<Vec<Bone>, ParseError> {
522 let mut cur = Cursor::new(payload);
523 let count = cur.read_u32()? as usize;
524 let mut bones = Vec::with_capacity(count);
525 for _ in 0..count {
526 let name_len = cur.read_u16()? as usize;
527 let name = String::from_utf8_lossy(cur.read_bytes(name_len)?).into_owned();
528 let attach_count = cur.read_u32()? as usize;
529 let mut attachments = Vec::with_capacity(attach_count);
530 for _ in 0..attach_count {
531 attachments.push(read_attachment(&mut cur)?);
532 }
533 let hinge = read_hinge(&mut cur)?;
534 bones.push(Bone {
535 name,
536 attachments,
537 hinge,
538 });
539 }
540 Ok(bones)
541}
542
543fn read_attachment(cur: &mut Cursor<'_>) -> Result<Attachment, ParseError> {
546 let mesh_kind = cur.read_u16()?;
547 let index = cur.read_u32()? as usize;
548 let target = match mesh_kind {
549 MESH_KIND_STATIC => MeshRef::Static(index),
550 MESH_KIND_CLIP => MeshRef::Clip(index),
551 other => return Err(ParseError::UnsupportedMeshKind(other)),
552 };
553 let local_offset = read_bonexform(cur)?;
554 let speed_q8 = cur.read_i32()?;
555 let start_phase_ms = cur.read_u32()?;
556 Ok(Attachment {
557 target,
558 local_offset,
559 playback: ClipPlayback {
560 speed_q8,
561 start_phase_ms,
562 },
563 })
564}
565
566fn parse_vclp(payload: &[u8]) -> Result<Vec<VoxelClip>, ParseError> {
567 let mut cur = Cursor::new(payload);
568 let count = cur.read_u32()? as usize;
569 let mut clips = Vec::with_capacity(count);
570 for _ in 0..count {
571 let blob_len = cur.read_u32()? as usize;
572 let blob = cur.read_bytes(blob_len)?;
573 clips.push(VoxelClip::parse(blob).map_err(ParseError::BadClip)?);
574 }
575 Ok(clips)
576}
577
578fn parse_clps(payload: &[u8], numbone: usize) -> Result<Vec<Clip>, ParseError> {
579 let mut cur = Cursor::new(payload);
580 let count = cur.read_u32()? as usize;
581 let mut clips = Vec::with_capacity(count);
582 for _ in 0..count {
583 let name_len = cur.read_u16()? as usize;
584 let name = String::from_utf8_lossy(cur.read_bytes(name_len)?).into_owned();
585 let kind = cur.read_u16()?;
586 let payload_len = cur.read_u32()? as usize;
587 let body = cur.read_bytes(payload_len)?;
588 let data = if kind == CLIP_KIND_SKELETAL {
589 parse_skeletal(body, numbone)?
590 } else {
591 ClipData::Unknown {
592 kind,
593 bytes: body.to_vec(),
594 }
595 };
596 clips.push(Clip { name, data });
597 }
598 Ok(clips)
599}
600
601fn parse_skeletal(body: &[u8], numbone: usize) -> Result<ClipData, ParseError> {
602 let mut cur = Cursor::new(body);
603 let numfrm = cur.read_u32()? as usize;
604 let numhin = cur.read_u32()? as usize;
605 if numhin != numbone {
606 return Err(ParseError::ClipBoneCountMismatch);
607 }
608 let mut frmval = Vec::with_capacity(numfrm);
609 for _ in 0..numfrm {
610 let mut row = Vec::with_capacity(numhin);
611 for _ in 0..numhin {
612 row.push(read_bonexform(&mut cur)?);
613 }
614 frmval.push(row);
615 }
616 let seqcount = cur.read_u32()? as usize;
617 let mut seq = Vec::with_capacity(seqcount);
618 for _ in 0..seqcount {
619 let tim = cur.read_i32()?;
620 let frm = cur.read_i32()?;
621 seq.push(Seq { tim, frm });
622 }
623 Ok(ClipData::Skeletal { frmval, seq })
624}
625
626fn read_hinge(cur: &mut Cursor<'_>) -> Result<Hinge, OutOfBounds> {
627 let parent = cur.read_i32()?;
628 let p0 = read_point3(cur)?;
629 let p1 = read_point3(cur)?;
630 let v0 = read_point3(cur)?;
631 let v1 = read_point3(cur)?;
632 let vmin = cur.read_i16()?;
633 let vmax = cur.read_i16()?;
634 let htype = cur.read_u8()?;
635 let filler_buf = cur.read_bytes(7)?;
636 let mut filler = [0u8; 7];
637 filler.copy_from_slice(filler_buf);
638 Ok(Hinge {
639 parent,
640 p: [p0, p1],
641 v: [v0, v1],
642 vmin,
643 vmax,
644 htype,
645 filler,
646 })
647}
648
649fn read_point3(cur: &mut Cursor<'_>) -> Result<Point3, OutOfBounds> {
650 Ok(Point3 {
651 x: cur.read_f32()?,
652 y: cur.read_f32()?,
653 z: cur.read_f32()?,
654 })
655}
656
657fn read_bonexform(cur: &mut Cursor<'_>) -> Result<BoneXform, OutOfBounds> {
660 let t = [cur.read_f32()?, cur.read_f32()?, cur.read_f32()?];
661 let r = Quat {
662 x: cur.read_f32()?,
663 y: cur.read_f32()?,
664 z: cur.read_f32()?,
665 w: cur.read_f32()?,
666 };
667 let s = [cur.read_f32()?, cur.read_f32()?, cur.read_f32()?];
668 Ok(BoneXform { t, r, s })
669}
670
671fn write_bonexform(out: &mut Vec<u8>, x: &BoneXform) {
672 for v in [
673 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],
674 ] {
675 out.extend_from_slice(&v.to_le_bytes());
676 }
677}
678
679fn write_chunk(out: &mut Vec<u8>, tag: [u8; 4], body: impl FnOnce(&mut Vec<u8>)) {
683 out.extend_from_slice(&tag);
684 let len_pos = out.len();
685 out.extend_from_slice(&0u32.to_le_bytes()); let start = out.len();
687 body(out);
688 let len = u32::try_from(out.len() - start).expect("chunk payload length must fit in u32");
689 out[len_pos..len_pos + 4].copy_from_slice(&len.to_le_bytes());
690}
691
692fn write_meta(out: &mut Vec<u8>, c: &Character) {
693 let name = c.name.as_bytes();
694 let name_len = u16::try_from(name.len()).expect("character name length must fit in u16");
695 out.extend_from_slice(&name_len.to_le_bytes());
696 out.extend_from_slice(name);
697 for v in c.root {
698 out.extend_from_slice(&v.to_le_bytes());
699 }
700}
701
702fn write_mshs(out: &mut Vec<u8>, c: &Character) {
703 let count = u32::try_from(c.meshes.len()).expect("mesh count must fit in u32");
704 out.extend_from_slice(&count.to_le_bytes());
705 for mesh in &c.meshes {
706 let blob = kv6::serialize(mesh);
707 let blob_len = u32::try_from(blob.len()).expect("kv6 blob length must fit in u32");
708 out.extend_from_slice(&blob_len.to_le_bytes());
709 out.extend_from_slice(&blob);
710 }
711}
712
713fn write_bons(out: &mut Vec<u8>, c: &Character) {
714 let count = u32::try_from(c.bones.len()).expect("bone count must fit in u32");
715 out.extend_from_slice(&count.to_le_bytes());
716 for bone in &c.bones {
717 let name = bone.name.as_bytes();
718 let name_len = u16::try_from(name.len()).expect("bone name length must fit in u16");
719 out.extend_from_slice(&name_len.to_le_bytes());
720 out.extend_from_slice(name);
721 let attach_count =
722 u32::try_from(bone.attachments.len()).expect("attachment count must fit in u32");
723 out.extend_from_slice(&attach_count.to_le_bytes());
724 for a in &bone.attachments {
725 write_attachment(out, a);
726 }
727 write_hinge(out, &bone.hinge);
728 }
729}
730
731fn write_attachment(out: &mut Vec<u8>, a: &Attachment) {
732 let (kind, index) = match a.target {
733 MeshRef::Static(i) => (MESH_KIND_STATIC, i),
734 MeshRef::Clip(i) => (MESH_KIND_CLIP, i),
735 };
736 out.extend_from_slice(&kind.to_le_bytes());
737 let idx = u32::try_from(index).expect("attachment index must fit in u32");
738 out.extend_from_slice(&idx.to_le_bytes());
739 write_bonexform(out, &a.local_offset);
740 out.extend_from_slice(&a.playback.speed_q8.to_le_bytes());
741 out.extend_from_slice(&a.playback.start_phase_ms.to_le_bytes());
742}
743
744fn write_vclp(out: &mut Vec<u8>, c: &Character) {
745 let count = u32::try_from(c.voxel_clips.len()).expect("voxel clip count must fit in u32");
746 out.extend_from_slice(&count.to_le_bytes());
747 for clip in &c.voxel_clips {
748 let blob = clip.serialize();
749 let blob_len = u32::try_from(blob.len()).expect("voxel clip blob length must fit in u32");
750 out.extend_from_slice(&blob_len.to_le_bytes());
751 out.extend_from_slice(&blob);
752 }
753}
754
755fn write_clps(out: &mut Vec<u8>, c: &Character) {
756 let count = u32::try_from(c.clips.len()).expect("clip count must fit in u32");
757 out.extend_from_slice(&count.to_le_bytes());
758 for clip in &c.clips {
759 let name = clip.name.as_bytes();
760 let name_len = u16::try_from(name.len()).expect("clip name length must fit in u16");
761 out.extend_from_slice(&name_len.to_le_bytes());
762 out.extend_from_slice(name);
763 match &clip.data {
764 ClipData::Skeletal { frmval, seq } => {
765 out.extend_from_slice(&CLIP_KIND_SKELETAL.to_le_bytes());
766 write_chunk_body(out, |b| write_skeletal(b, frmval, seq));
767 }
768 ClipData::Unknown { kind, bytes } => {
769 out.extend_from_slice(&kind.to_le_bytes());
770 write_chunk_body(out, |b| b.extend_from_slice(bytes));
771 }
772 }
773 }
774}
775
776fn write_chunk_body(out: &mut Vec<u8>, body: impl FnOnce(&mut Vec<u8>)) {
780 let len_pos = out.len();
781 out.extend_from_slice(&0u32.to_le_bytes());
782 let start = out.len();
783 body(out);
784 let len = u32::try_from(out.len() - start).expect("clip payload length must fit in u32");
785 out[len_pos..len_pos + 4].copy_from_slice(&len.to_le_bytes());
786}
787
788fn write_skeletal(out: &mut Vec<u8>, frmval: &[Vec<BoneXform>], seq: &[Seq]) {
789 let numhin = frmval.first().map_or(0, Vec::len);
790 for (i, row) in frmval.iter().enumerate() {
791 assert!(
792 row.len() == numhin,
793 "skeletal frmval[{i}].len() = {} != numhin {numhin}",
794 row.len(),
795 );
796 }
797 let numfrm = u32::try_from(frmval.len()).expect("numfrm must fit in u32");
798 let numhin_u32 = u32::try_from(numhin).expect("numhin must fit in u32");
799 out.extend_from_slice(&numfrm.to_le_bytes());
800 out.extend_from_slice(&numhin_u32.to_le_bytes());
801 for row in frmval {
802 for v in row {
803 write_bonexform(out, v);
804 }
805 }
806 let seqcount = u32::try_from(seq.len()).expect("seqcount must fit in u32");
807 out.extend_from_slice(&seqcount.to_le_bytes());
808 for s in seq {
809 out.extend_from_slice(&s.tim.to_le_bytes());
810 out.extend_from_slice(&s.frm.to_le_bytes());
811 }
812}
813
814fn write_hinge(out: &mut Vec<u8>, h: &Hinge) {
815 out.extend_from_slice(&h.parent.to_le_bytes());
816 for p in h.p {
817 write_point3(out, p);
818 }
819 for v in h.v {
820 write_point3(out, v);
821 }
822 out.extend_from_slice(&h.vmin.to_le_bytes());
823 out.extend_from_slice(&h.vmax.to_le_bytes());
824 out.push(h.htype);
825 out.extend_from_slice(&h.filler);
826}
827
828fn write_point3(out: &mut Vec<u8>, p: Point3) {
829 out.extend_from_slice(&p.x.to_le_bytes());
830 out.extend_from_slice(&p.y.to_le_bytes());
831 out.extend_from_slice(&p.z.to_le_bytes());
832}
833
834const _: () = assert!(HINGE_SIZE == 64);
836
837#[cfg(test)]
840mod tests {
841 use super::*;
842
843 fn unit_kv6(fill: u32) -> Kv6 {
844 Kv6 {
846 xsiz: 1,
847 ysiz: 1,
848 zsiz: 1,
849 xpiv: 0.5,
850 ypiv: 0.5,
851 zpiv: 0.5,
852 voxels: vec![kv6::Voxel {
853 col: fill,
854 z: 0,
855 vis: 0x3f,
856 dir: 0,
857 }],
858 xlen: vec![1],
859 ylen: vec![vec![1]],
860 palette: None,
861 }
862 }
863
864 fn hinge(parent: i32) -> Hinge {
865 let zero = Point3 {
866 x: 0.0,
867 y: 0.0,
868 z: 0.0,
869 };
870 let axis = Point3 {
871 x: 0.0,
872 y: 0.0,
873 z: 1.0,
874 };
875 Hinge {
876 parent,
877 p: [zero, zero],
878 v: [axis, axis],
879 vmin: 0,
880 vmax: 0,
881 htype: 0,
882 filler: [0; 7],
883 }
884 }
885
886 fn synthetic_character() -> Character {
887 Character {
888 name: "anasaur".to_string(),
889 root: [70.0, -75.0, 50.0],
890 meshes: vec![unit_kv6(0x00ff_8040), unit_kv6(0x0010_2030)],
891 bones: vec![
892 Bone {
893 name: "body".to_string(),
894 attachments: vec![Attachment::static_mesh(0)],
895 hinge: hinge(-1),
896 },
897 Bone {
898 name: "arm".to_string(),
899 attachments: vec![Attachment::static_mesh(1)],
900 hinge: hinge(0),
901 },
902 ],
903 clips: vec![Clip {
904 name: "wave".to_string(),
905 data: ClipData::Skeletal {
908 frmval: [[0i16, 0], [0, 16000], [0, 0], [0, -16000]]
909 .iter()
910 .map(|[r, a]| {
911 let z = [0.0, 0.0, 1.0];
912 vec![
913 BoneXform::from_hinge_angle(z, *r),
914 BoneXform::from_hinge_angle(z, *a),
915 ]
916 })
917 .collect(),
918 seq: vec![
919 Seq { tim: 0, frm: 0 },
920 Seq { tim: 500, frm: 1 },
921 Seq { tim: 1000, frm: 2 },
922 Seq { tim: 1500, frm: 3 },
923 Seq { tim: 2000, frm: !0 },
924 ],
925 },
926 }],
927 voxel_clips: Vec::new(),
928 extra_chunks: Vec::new(),
929 }
930 }
931
932 #[test]
933 fn roundtrips_byte_equal() {
934 let c = synthetic_character();
935 let bytes = serialize(&c);
936 let parsed = parse(&bytes).expect("parse synthetic");
937 let bytes2 = serialize(&parsed);
938 assert_eq!(bytes, bytes2, "byte-level round-trip failed");
939 assert_eq!(parsed.name, c.name);
940 assert_eq!(parsed.root, c.root);
941 assert_eq!(parsed.meshes.len(), c.meshes.len());
942 assert_eq!(parsed.bones.len(), c.bones.len());
943 assert_eq!(parsed.bones[1].name, "arm");
944 assert_eq!(parsed.bones[1].attachments[0].target, MeshRef::Static(1));
945 assert_eq!(parsed.bones[1].hinge.parent, 0);
946 assert_eq!(parsed.clips.len(), 1);
947 assert_eq!(parsed.clips[0].data, c.clips[0].data);
948 assert!(parsed.voxel_clips.is_empty());
949 }
950
951 #[test]
952 fn roundtrips_with_clips_and_multi_attachment() {
953 use crate::voxel_clip::{LoopMode, VoxelClip, VoxelFrame};
954 let frame = VoxelFrame {
956 occupancy: vec![1u32],
957 colors: vec![0x8011_2233],
958 color_offsets: vec![0, 1],
959 };
960 let clip = VoxelClip::from_frames(
961 [1, 1, 4],
962 [0.5, 0.5, 2.0],
963 1.0,
964 LoopMode::Loop,
965 &[frame],
966 &[],
967 33,
968 0,
969 );
970
971 let mut c = synthetic_character();
972 c.voxel_clips = vec![clip];
973 c.bones[0].attachments.push(Attachment {
976 target: MeshRef::Clip(0),
977 local_offset: BoneXform {
978 t: [1.0, 2.0, 3.0],
979 r: Quat::IDENTITY,
980 s: [1.0, 1.0, 1.0],
981 },
982 playback: ClipPlayback {
983 speed_q8: 512,
984 start_phase_ms: 100,
985 },
986 });
987
988 let bytes = serialize(&c);
989 let parsed = parse(&bytes).expect("parse v3 with clips");
990 assert_eq!(serialize(&parsed), bytes, "byte round-trip");
991
992 assert_eq!(parsed.voxel_clips.len(), 1);
993 assert_eq!(parsed.voxel_clips[0], c.voxel_clips[0]);
994
995 let body = &parsed.bones[0].attachments;
997 assert_eq!(body.len(), 2);
998 assert_eq!(body[0].target, MeshRef::Static(0));
999 assert_eq!(body[1].target, MeshRef::Clip(0));
1000 assert_eq!(body[1].local_offset.t, [1.0, 2.0, 3.0]);
1001 assert_eq!(
1002 body[1].playback,
1003 ClipPlayback {
1004 speed_q8: 512,
1005 start_phase_ms: 100,
1006 }
1007 );
1008
1009 let k = parsed.to_kfa_sprite(None);
1012 assert_eq!(k.limbs.len(), parsed.bones.len());
1013 }
1014
1015 #[test]
1016 fn to_kfa_sprite_builds_renderable() {
1017 let c = synthetic_character();
1018 let mut k = c.to_kfa_sprite(Some(0));
1019 assert_eq!(k.limbs.len(), 2);
1020 assert_eq!(k.hinges.len(), 2);
1021 assert_eq!(k.p, c.root);
1022 k.animsprite(500);
1024 assert_ne!(
1025 k.kfaval[1],
1026 crate::xform::BoneXform::IDENTITY,
1027 "baked clip should move the arm bone"
1028 );
1029
1030 let mut rest = c.to_kfa_sprite(None);
1032 rest.animsprite(500);
1033 assert_eq!(rest.kfaval[1], crate::xform::BoneXform::IDENTITY);
1034 }
1035
1036 #[test]
1037 fn to_kfa_export_is_lossy_but_valid() {
1038 let c = synthetic_character();
1039 let kfa = c.to_kfa(Some(0), "coco.kv6");
1040 assert_eq!(kfa.kv6_name, b"coco.kv6");
1042 assert_eq!(kfa.hinges.len(), 2);
1043 assert_eq!(kfa.hinges[1].parent, 0);
1044 if let ClipData::Skeletal { frmval, seq } = &c.clips[0].data {
1045 let z = [0.0, 0.0, 1.0];
1048 let expected: Vec<Vec<i16>> = frmval
1049 .iter()
1050 .map(|row| row.iter().map(|x| x.hinge_angle(z)).collect())
1051 .collect();
1052 assert_eq!(kfa.frmval, expected);
1053 assert_eq!(&kfa.seq, seq);
1054 } else {
1055 panic!("clip 0 should be skeletal");
1056 }
1057 let bytes = crate::kfa::serialize(&kfa);
1060 let reparsed = crate::kfa::parse(&bytes).expect("export round-trips through kfa");
1061 assert_eq!(reparsed.kv6_name, kfa.kv6_name);
1062 assert_eq!(reparsed.frmval, kfa.frmval);
1063 assert_eq!(reparsed.seq, kfa.seq);
1064 }
1065
1066 #[test]
1067 fn to_kfa_without_clip_is_posable() {
1068 let c = synthetic_character();
1069 let kfa = c.to_kfa(None, b"x.kv6".to_vec());
1071 assert_eq!(kfa.hinges.len(), 2);
1072 assert!(kfa.frmval.is_empty());
1073 assert!(kfa.seq.is_empty());
1074 let bytes = crate::kfa::serialize(&kfa);
1076 assert!(crate::kfa::parse(&bytes).is_ok());
1077 }
1078
1079 #[test]
1080 fn clips_may_be_empty() {
1081 let mut c = synthetic_character();
1082 c.clips.clear();
1083 let bytes = serialize(&c);
1084 let parsed = parse(&bytes).expect("parse");
1085 assert!(parsed.clips.is_empty());
1086 let k = parsed.to_kfa_sprite(None);
1088 assert_eq!(k.limbs.len(), 2);
1089 }
1090
1091 #[test]
1092 fn unknown_top_level_chunk_is_skipped_and_preserved() {
1093 let mut bytes = serialize(&synthetic_character());
1094 bytes.extend_from_slice(b"ZZZZ");
1096 bytes.extend_from_slice(&3u32.to_le_bytes());
1097 bytes.extend_from_slice(&[1, 2, 3]);
1098 let parsed = parse(&bytes).expect("parse with unknown chunk");
1099 assert_eq!(parsed.bones.len(), 2, "known chunks still parse");
1100 assert_eq!(parsed.extra_chunks, vec![(*b"ZZZZ", vec![1u8, 2, 3])]);
1101 let bytes2 = serialize(&parsed);
1103 assert_eq!(
1104 bytes2, bytes,
1105 "unknown chunk preserved byte-equal on re-save"
1106 );
1107 }
1108
1109 #[test]
1110 fn unknown_clip_kind_preserved() {
1111 let mut c = synthetic_character();
1112 c.clips.push(Clip {
1113 name: "mystery".to_string(),
1114 data: ClipData::Unknown {
1115 kind: 7,
1116 bytes: vec![9, 8, 7, 6],
1117 },
1118 });
1119 let bytes = serialize(&c);
1120 let parsed = parse(&bytes).expect("parse");
1121 assert!(matches!(parsed.clips[0].data, ClipData::Skeletal { .. }));
1123 assert_eq!(
1124 parsed.clips[1].data,
1125 ClipData::Unknown {
1126 kind: 7,
1127 bytes: vec![9, 8, 7, 6]
1128 }
1129 );
1130 assert_eq!(serialize(&parsed), bytes);
1132 }
1133
1134 #[test]
1135 fn bad_magic_errors() {
1136 let mut bytes = serialize(&synthetic_character());
1137 bytes[0] ^= 0xff;
1138 assert!(matches!(parse(&bytes), Err(ParseError::BadMagic { .. })));
1139 }
1140
1141 #[test]
1142 fn version_mismatch_errors() {
1143 let mut bytes = serialize(&synthetic_character());
1144 bytes[4] = 0xff;
1146 bytes[5] = 0xff;
1147 assert!(matches!(
1148 parse(&bytes),
1149 Err(ParseError::UnsupportedVersion(0xffff))
1150 ));
1151 }
1152
1153 #[test]
1154 fn truncated_errors() {
1155 let bytes = serialize(&synthetic_character());
1156 assert!(matches!(
1157 parse(&bytes[..bytes.len() - 4]),
1158 Err(ParseError::Truncated { .. })
1159 ));
1160 }
1161
1162 #[test]
1163 fn missing_required_chunk_errors() {
1164 let mut bytes = Vec::new();
1166 bytes.extend_from_slice(&MAGIC);
1167 bytes.extend_from_slice(&VERSION.to_le_bytes());
1168 assert!(matches!(
1169 parse(&bytes),
1170 Err(ParseError::MissingChunk(TAG_META))
1171 ));
1172 }
1173
1174 #[test]
1175 fn unsupported_mesh_kind_errors() {
1176 let mut bytes = serialize(&synthetic_character());
1177 let pos = find_tag(&bytes, *b"BONS");
1182 let kind_off = pos + 8 + 4 + 2 + 4 + 4;
1183 bytes[kind_off] = 2;
1184 bytes[kind_off + 1] = 0;
1185 assert!(matches!(
1186 parse(&bytes),
1187 Err(ParseError::UnsupportedMeshKind(2))
1188 ));
1189 }
1190
1191 #[test]
1192 fn clip_bone_count_mismatch_errors() {
1193 let mut c = synthetic_character();
1196 c.bones.pop(); c.bones[0].attachments = vec![Attachment::static_mesh(0)];
1198 let bytes = serialize(&c);
1200 assert!(matches!(
1201 parse(&bytes),
1202 Err(ParseError::ClipBoneCountMismatch)
1203 ));
1204 }
1205
1206 #[test]
1207 fn bad_embedded_mesh_errors() {
1208 let mut bytes = serialize(&synthetic_character());
1209 let pos = find_tag(&bytes, *b"MSHS");
1212 let kv6_magic_off = pos + 8 + 4 + 4;
1213 bytes[kv6_magic_off] ^= 0xff;
1214 assert!(matches!(parse(&bytes), Err(ParseError::BadMesh(_))));
1215 }
1216
1217 fn find_tag(bytes: &[u8], tag: [u8; 4]) -> usize {
1220 let mut pos = 6; while pos + 8 <= bytes.len() {
1222 let here = &bytes[pos..pos + 4];
1223 let len = u32::from_le_bytes([
1224 bytes[pos + 4],
1225 bytes[pos + 5],
1226 bytes[pos + 6],
1227 bytes[pos + 7],
1228 ]) as usize;
1229 if here == tag {
1230 return pos;
1231 }
1232 pos += 8 + len;
1233 }
1234 panic!("tag {tag:?} not found");
1235 }
1236}