use core::fmt;
use crate::bytes::{Cursor, OutOfBounds};
const MAGIC: u32 = 0x6b6c_774b; const HINGE_SIZE: usize = 64;
const SEQ_SIZE: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point3 {
pub x: f32,
pub y: f32,
pub z: f32,
}
#[derive(Debug, Clone, Copy)]
pub struct Hinge {
pub parent: i32,
pub p: [Point3; 2],
pub v: [Point3; 2],
pub vmin: i16,
pub vmax: i16,
pub htype: u8,
pub filler: [u8; 7],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Seq {
pub tim: i32,
pub frm: i32,
}
#[derive(Debug, Clone)]
pub struct Kfa {
pub kv6_name: Vec<u8>,
pub hinges: Vec<Hinge>,
pub frmval: Vec<Vec<i16>>,
pub seq: Vec<Seq>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
BadMagic { got: u32 },
Truncated { at: usize, need: usize },
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::BadMagic { got } => {
write!(f, "kfa bad magic: got {got:#010x}, expected 0x6b6c774b")
}
Self::Truncated { at, need } => {
write!(f, "kfa truncated: need {need} bytes at offset {at}")
}
}
}
}
impl std::error::Error for ParseError {}
impl From<OutOfBounds> for ParseError {
fn from(e: OutOfBounds) -> Self {
Self::Truncated {
at: e.at,
need: e.need,
}
}
}
pub fn parse(bytes: &[u8]) -> Result<Kfa, ParseError> {
let mut cur = Cursor::new(bytes);
let magic = cur.read_u32()?;
if magic != MAGIC {
return Err(ParseError::BadMagic { got: magic });
}
let name_len = cur.read_u32()? as usize;
let kv6_name = cur.read_bytes(name_len)?.to_vec();
let numhin = cur.read_u32()? as usize;
let mut hinges = Vec::with_capacity(numhin);
for _ in 0..numhin {
hinges.push(read_hinge(&mut cur)?);
}
let numfrm = cur.read_u32()? as usize;
let mut frmval = Vec::with_capacity(numfrm);
for _ in 0..numfrm {
let mut row = Vec::with_capacity(numhin);
for _ in 0..numhin {
row.push(cur.read_i16()?);
}
frmval.push(row);
}
let seqnum = cur.read_u32()? as usize;
let mut seq = Vec::with_capacity(seqnum);
for _ in 0..seqnum {
let tim = cur.read_i32()?;
let frm = cur.read_i32()?;
seq.push(Seq { tim, frm });
}
Ok(Kfa {
kv6_name,
hinges,
frmval,
seq,
})
}
#[must_use]
pub fn serialize(kfa: &Kfa) -> Vec<u8> {
let numhin = kfa.hinges.len();
for (i, row) in kfa.frmval.iter().enumerate() {
assert!(
row.len() == numhin,
"kfa frmval[{}].len() = {}, expected numhin = {}",
i,
row.len(),
numhin,
);
}
let name_len = u32::try_from(kfa.kv6_name.len()).expect("kv6_name length must fit in u32");
let numhin_u32 = u32::try_from(numhin).expect("numhin must fit in u32");
let numfrm_u32 = u32::try_from(kfa.frmval.len()).expect("numfrm must fit in u32");
let seqnum_u32 = u32::try_from(kfa.seq.len()).expect("seqnum must fit in u32");
let total = 4
+ 4
+ kfa.kv6_name.len()
+ 4
+ numhin * HINGE_SIZE
+ 4
+ (kfa.frmval.len() * numhin) * 2
+ 4
+ kfa.seq.len() * SEQ_SIZE;
let mut out = Vec::with_capacity(total);
out.extend_from_slice(&MAGIC.to_le_bytes());
out.extend_from_slice(&name_len.to_le_bytes());
out.extend_from_slice(&kfa.kv6_name);
out.extend_from_slice(&numhin_u32.to_le_bytes());
for h in &kfa.hinges {
write_hinge(&mut out, h);
}
out.extend_from_slice(&numfrm_u32.to_le_bytes());
for row in &kfa.frmval {
for v in row {
out.extend_from_slice(&v.to_le_bytes());
}
}
out.extend_from_slice(&seqnum_u32.to_le_bytes());
for s in &kfa.seq {
out.extend_from_slice(&s.tim.to_le_bytes());
out.extend_from_slice(&s.frm.to_le_bytes());
}
out
}
fn read_point3(cur: &mut Cursor<'_>) -> Result<Point3, OutOfBounds> {
let x = cur.read_f32()?;
let y = cur.read_f32()?;
let z = cur.read_f32()?;
Ok(Point3 { x, y, z })
}
fn write_point3(out: &mut Vec<u8>, p: Point3) {
out.extend_from_slice(&p.x.to_le_bytes());
out.extend_from_slice(&p.y.to_le_bytes());
out.extend_from_slice(&p.z.to_le_bytes());
}
fn read_hinge(cur: &mut Cursor<'_>) -> Result<Hinge, OutOfBounds> {
let parent = cur.read_i32()?;
let p0 = read_point3(cur)?;
let p1 = read_point3(cur)?;
let v0 = read_point3(cur)?;
let v1 = read_point3(cur)?;
let vmin = cur.read_i16()?;
let vmax = cur.read_i16()?;
let htype = cur.read_u8()?;
let filler_buf = cur.read_bytes(7)?;
let mut filler = [0u8; 7];
filler.copy_from_slice(filler_buf);
Ok(Hinge {
parent,
p: [p0, p1],
v: [v0, v1],
vmin,
vmax,
htype,
filler,
})
}
fn write_hinge(out: &mut Vec<u8>, h: &Hinge) {
out.extend_from_slice(&h.parent.to_le_bytes());
write_point3(out, h.p[0]);
write_point3(out, h.p[1]);
write_point3(out, h.v[0]);
write_point3(out, h.v[1]);
out.extend_from_slice(&h.vmin.to_le_bytes());
out.extend_from_slice(&h.vmax.to_le_bytes());
out.push(h.htype);
out.extend_from_slice(&h.filler);
}
#[derive(Clone)]
pub struct KfaSprite {
pub limbs: Vec<crate::sprite::Sprite>,
pub hinges: Vec<Hinge>,
pub hinge_sort: Vec<usize>,
pub kfaval: Vec<i16>,
pub p: [f32; 3],
pub s: [f32; 3],
pub h: [f32; 3],
pub f: [f32; 3],
}
impl KfaSprite {
#[must_use]
pub fn new(limbs: Vec<crate::sprite::Sprite>, hinges: Vec<Hinge>, root_pos: [f32; 3]) -> Self {
assert_eq!(
limbs.len(),
hinges.len(),
"limbs ({}) and hinges ({}) length mismatch",
limbs.len(),
hinges.len()
);
let n = hinges.len();
let hinge_sort = sort_hinges(&hinges);
Self {
limbs,
hinges,
hinge_sort,
kfaval: vec![0i16; n],
p: root_pos,
s: [1.0, 0.0, 0.0],
h: [0.0, 1.0, 0.0],
f: [0.0, 0.0, 1.0],
}
}
}
#[must_use]
#[allow(clippy::cast_sign_loss)] pub fn sort_hinges(hinges: &[Hinge]) -> Vec<usize> {
let n = hinges.len();
let mut hsort = vec![0usize; n];
let mut head = 0usize;
let mut tail = n;
for i in (0..n).rev() {
if hinges[i].parent < 0 {
tail -= 1;
hsort[tail] = i;
} else {
hsort[head] = i;
head += 1;
}
}
let mut solved = vec![false; n];
for i in (tail..n).rev() {
solved[hsort[i]] = true;
}
let mut idx = head; while tail > 0 {
if idx == 0 {
idx = head;
}
idx -= 1;
let j = hsort[idx];
let parent = hinges[j].parent;
if parent < 0 {
continue;
}
if solved[parent as usize] {
solved[j] = true;
tail -= 1;
hsort[idx] = hsort[tail];
hsort[tail] = j;
head -= 1;
}
if head == 0 {
break;
}
}
hsort
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic_kfa() -> Kfa {
Kfa {
kv6_name: b"anasaur.kv6".to_vec(),
hinges: vec![
Hinge {
parent: -1,
p: [
Point3 {
x: 0.0,
y: 0.0,
z: 0.0,
},
Point3 {
x: 1.0,
y: 0.0,
z: 0.0,
},
],
v: [
Point3 {
x: 0.0,
y: 1.0,
z: 0.0,
},
Point3 {
x: 0.0,
y: 0.0,
z: 1.0,
},
],
vmin: -180,
vmax: 180,
htype: 0,
filler: [0; 7],
},
Hinge {
parent: 0,
p: [
Point3 {
x: 0.5,
y: 0.0,
z: 0.0,
},
Point3 {
x: 0.5,
y: 1.0,
z: 0.0,
},
],
v: [
Point3 {
x: 1.0,
y: 0.0,
z: 0.0,
},
Point3 {
x: 0.0,
y: 1.0,
z: 0.0,
},
],
vmin: -90,
vmax: 90,
htype: 1,
filler: [0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba],
},
],
frmval: vec![vec![0, 0], vec![45, -30], vec![90, -60], vec![135, -90]],
seq: vec![
Seq { tim: 0, frm: 0 },
Seq { tim: 100, frm: 1 },
Seq { tim: 200, frm: 2 },
Seq { tim: 300, frm: 3 },
],
}
}
#[test]
fn synthetic_roundtrips_byte_equal() {
let kfa = synthetic_kfa();
let bytes = serialize(&kfa);
let parsed = parse(&bytes).expect("parse synthetic");
let bytes2 = serialize(&parsed);
assert_eq!(bytes, bytes2, "byte-level round-trip failed");
assert_eq!(parsed.kv6_name, kfa.kv6_name);
assert_eq!(parsed.hinges.len(), kfa.hinges.len());
assert_eq!(parsed.frmval, kfa.frmval);
assert_eq!(parsed.seq, kfa.seq);
}
#[test]
fn hinge_size_matches_voxlap_packed_layout() {
assert_eq!(HINGE_SIZE, 64);
let kfa = synthetic_kfa();
let bytes = serialize(&kfa);
let header = 4 + 4 + kfa.kv6_name.len() + 4;
let after_hinges = header + kfa.hinges.len() * HINGE_SIZE;
let parsed = parse(&bytes).expect("parse synthetic");
assert_eq!(
parsed.hinges[1].filler,
[0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba]
);
assert!(bytes.len() > after_hinges + 4);
}
#[test]
fn parse_bad_magic_fails() {
let mut bytes = serialize(&synthetic_kfa());
bytes[0] ^= 0xff;
let r = parse(&bytes);
assert!(matches!(r, Err(ParseError::BadMagic { .. })));
}
#[test]
fn parse_truncated_in_hinge_table_fails() {
let bytes = serialize(&synthetic_kfa());
let truncated = &bytes[..30];
let r = parse(truncated);
assert!(matches!(r, Err(ParseError::Truncated { .. })));
}
#[test]
#[allow(clippy::cast_sign_loss)] fn sort_hinges_three_bone_chain() {
let axis = |x: f32, y: f32, z: f32| Point3 { x, y, z };
let h = |parent: i32| Hinge {
parent,
p: [axis(0.0, 0.0, 0.0); 2],
v: [axis(1.0, 0.0, 0.0); 2],
vmin: 0,
vmax: 0,
htype: 0,
filler: [0; 7],
};
let hinges = vec![h(-1), h(0), h(1)];
let sort = sort_hinges(&hinges);
let mut seen = [false; 3];
for k in (0..3).rev() {
let j = sort[k];
seen[j] = true;
let p = hinges[j].parent;
if p >= 0 {
assert!(
seen[p as usize],
"bone {j}'s parent {p} not yet visited at descent step k={k}"
);
}
}
}
}