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],
pub frmval: Vec<Vec<i16>>,
pub seq: Vec<Seq>,
pub kfatim: i32,
pub okfatim: i32,
}
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],
frmval: Vec::new(),
seq: Vec::new(),
kfatim: 0,
okfatim: 0,
}
}
pub fn set_animation(&mut self, frmval: Vec<Vec<i16>>, seq: Vec<Seq>) {
self.frmval = frmval;
self.seq = seq;
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::similar_names
)]
pub fn animsprite(&mut self, mut ti: i32) {
if self.seq.is_empty() || self.frmval.is_empty() {
return;
}
let numhin = self.hinges.len();
let seqnum = self.seq.len();
let mut z = kfatime2seq(&self.seq, self.kfatim) as i32;
while ti > 0 {
z += 1;
if z as usize >= seqnum {
break;
}
let dt = self.seq[z as usize].tim - self.kfatim;
if dt <= 0 {
break;
}
if dt > ti {
self.kfatim += ti;
break;
}
ti -= dt;
let jump = !self.seq[z as usize].frm; if jump >= 0 {
if z == jump {
break;
}
z = jump;
}
self.kfatim = self.seq[z as usize].tim;
}
let z_seq = kfatime2seq(&self.seq, self.kfatim);
let zz_idx = z_seq + 1;
let (trat, zz_frm) = if zz_idx < seqnum && self.seq[zz_idx].frm != !(zz_idx as i32) {
let span = self.seq[zz_idx].tim - self.seq[z_seq].tim;
let trat = if span != 0 {
shldiv16(self.kfatim - self.seq[z_seq].tim, span)
} else {
0
};
let i = self.seq[zz_idx].frm;
let zz_frm = if i < 0 {
self.seq[(!i) as usize].frm
} else {
i
};
(trat, zz_frm)
} else {
(0, 0)
};
let z_frm = self.seq[z_seq].frm;
let mut trat2 = -1i32;
let mut z0_frm = 0i32;
let mut zz0_frm = 0i32;
if z_frm < 0 {
let z0_seq = kfatime2seq(&self.seq, self.okfatim);
let zz0_idx = z0_seq + 1;
if zz0_idx < seqnum && self.seq[zz0_idx].frm != !(zz0_idx as i32) {
let span = self.seq[zz0_idx].tim - self.seq[z0_seq].tim;
trat2 = if span != 0 {
shldiv16(self.okfatim - self.seq[z0_seq].tim, span)
} else {
0
};
let i = self.seq[zz0_idx].frm;
zz0_frm = if i < 0 {
self.seq[(!i) as usize].frm
} else {
i
};
} else {
trat2 = 0;
}
z0_frm = self.seq[z0_seq].frm;
if z0_frm < 0 {
z0_frm = zz0_frm;
trat2 = 0;
}
}
for i in (0..numhin).rev() {
if self.hinges[i].parent < 0 {
continue;
}
let vmin = i32::from(self.hinges[i].vmin);
let vmax = i32::from(self.hinges[i].vmax);
let mut frm0: i32 = if trat2 < 0 {
i32::from(self.frmval[z_frm as usize][i])
} else {
let mut base = i32::from(self.frmval[z0_frm as usize][i]);
if trat2 > 0 {
let target = i32::from(self.frmval[zz0_frm as usize][i]);
base += interp_delta(base, target, vmin, vmax, trat2);
}
base
};
if trat > 0 {
let target = i32::from(self.frmval[zz_frm as usize][i]);
frm0 += interp_delta(frm0, target, vmin, vmax, trat);
}
self.kfaval[i] = frm0 as i16;
}
}
}
#[inline]
#[allow(clippy::cast_possible_truncation)]
fn mulshr16(a: i32, d: i32) -> i32 {
((i64::from(a) * i64::from(d)) >> 16) as i32
}
#[inline]
#[allow(clippy::cast_possible_truncation)]
fn shldiv16(a: i32, b: i32) -> i32 {
((i64::from(a) << 16) / i64::from(b)) as i32
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss
)]
fn kfatime2seq(seq: &[Seq], tim: i32) -> usize {
let mut a: isize = 0;
let mut b: isize = seq.len() as isize - 1;
while b - a >= 2 {
let i = (a + b) >> 1;
if tim >= seq[i as usize].tim {
a = i;
} else {
b = i;
}
}
a as usize
}
#[inline]
fn interp_delta(from: i32, to: i32, vmin: i32, vmax: i32, trat: i32) -> i32 {
let mut x = (to - from) & 65535;
if vmin == vmax {
x = (x << 16) >> 16;
} else if ((to - vmin) & 65535) < ((from - vmin) & 65535) {
x -= 65536;
}
mulshr16(x, trat)
}
#[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}"
);
}
}
}
fn anim_sprite(
child_vmin: i16,
child_vmax: i16,
frmval: Vec<Vec<i16>>,
seq: Vec<Seq>,
) -> KfaSprite {
let zero = Point3 {
x: 0.0,
y: 0.0,
z: 0.0,
};
let axis = Point3 {
x: 1.0,
y: 0.0,
z: 0.0,
};
let hinges = vec![
Hinge {
parent: -1,
p: [zero, zero],
v: [axis, axis],
vmin: 0,
vmax: 0,
htype: 0,
filler: [0; 7],
},
Hinge {
parent: 0,
p: [zero, zero],
v: [axis, axis],
vmin: child_vmin,
vmax: child_vmax,
htype: 0,
filler: [0; 7],
},
];
KfaSprite {
limbs: Vec::new(),
hinge_sort: sort_hinges(&hinges),
kfaval: vec![0i16; hinges.len()],
hinges,
p: [0.0; 3],
s: [1.0, 0.0, 0.0],
h: [0.0, 1.0, 0.0],
f: [0.0, 0.0, 1.0],
frmval,
seq,
kfatim: 0,
okfatim: 0,
}
}
#[test]
fn animsprite_lerps_free_hinge_midpoint() {
let mut kfa = anim_sprite(
0,
0,
vec![vec![0, 0], vec![0, 16384]],
vec![Seq { tim: 0, frm: 0 }, Seq { tim: 1000, frm: 1 }],
);
kfa.animsprite(500);
assert_eq!(kfa.kfatim, 500, "time cursor advanced by ti");
assert_eq!(kfa.kfaval[0], 0, "root bone untouched");
assert_eq!(kfa.kfaval[1], 8192, "child at segment midpoint");
}
#[test]
fn animsprite_free_hinge_takes_shortest_wrap() {
let mut kfa = anim_sprite(
0,
0,
vec![vec![0, 30000], vec![0, -30000]],
vec![Seq { tim: 0, frm: 0 }, Seq { tim: 1000, frm: 1 }],
);
kfa.animsprite(500);
assert_eq!(kfa.kfaval[1], -32768);
}
#[test]
fn animsprite_follows_loop_jump_entry() {
let mut kfa = anim_sprite(
0,
0,
vec![vec![0, 0], vec![0, 16384]],
vec![
Seq { tim: 0, frm: 0 },
Seq { tim: 1000, frm: 1 },
Seq { tim: 2000, frm: !0 },
],
);
kfa.animsprite(2500);
assert_eq!(kfa.kfatim, 500, "looped back and advanced 500 ms");
}
#[test]
fn animsprite_no_curve_is_noop() {
let mut kfa = anim_sprite(0, 0, Vec::new(), Vec::new());
kfa.kfaval[1] = 1234;
kfa.animsprite(500);
assert_eq!(kfa.kfaval[1], 1234);
assert_eq!(kfa.kfatim, 0);
}
#[test]
fn kfatime2seq_brackets_from_below() {
let seq = vec![
Seq { tim: 0, frm: 0 },
Seq { tim: 100, frm: 1 },
Seq { tim: 200, frm: 2 },
Seq { tim: 300, frm: 3 },
];
assert_eq!(kfatime2seq(&seq, 0), 0);
assert_eq!(kfatime2seq(&seq, 99), 0);
assert_eq!(kfatime2seq(&seq, 100), 1);
assert_eq!(kfatime2seq(&seq, 250), 2);
assert_eq!(kfatime2seq(&seq, 9999), 2, "last segment's lower bracket");
}
}