physis/
pbd.rs

1// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use std::io::{Cursor, SeekFrom};
5
6use crate::ByteSpan;
7use crate::common_file_operations::strings_parser;
8use binrw::BinRead;
9use binrw::binread;
10
11#[binread]
12#[derive(Debug)]
13#[br(import { data_offset: i32 })]
14#[brw(little)]
15#[allow(unused)]
16struct RacialDeformer {
17    bone_count: i32,
18
19    #[br(count = bone_count)]
20    bone_name_offsets: Vec<u16>,
21
22    #[br(args(data_offset as u64, &bone_name_offsets), parse_with = strings_parser)]
23    #[br(restore_position)]
24    bone_names: Vec<String>,
25
26    #[br(if((bone_count & 1) != 0))]
27    #[br(temp)]
28    _padding: u16,
29
30    /// 4x3 matrix
31    #[br(count = bone_count)]
32    #[br(err_context("offset = {} bone count = {}", data_offset, bone_count))]
33    transform: Vec<[f32; 12]>,
34}
35
36#[binread]
37#[derive(Debug)]
38#[brw(little)]
39struct PreBoneDeformerItem {
40    body_id: u16, // the combined body id like 0101
41    link_index: i16,
42    #[br(pad_after = 4)]
43    #[br(temp)]
44    data_offset: i32,
45
46    #[br(args { data_offset: data_offset })]
47    #[br(seek_before = SeekFrom::Start(data_offset as u64))]
48    #[br(restore_position)]
49    deformer: RacialDeformer,
50}
51
52#[binread]
53#[derive(Debug)]
54#[brw(little)]
55#[allow(dead_code)]
56struct PreBoneDeformerLink {
57    parent_index: i16,
58    first_child_index: i16,
59    next_sibling_index: i16,
60    deformer_index: u16,
61}
62
63#[binread]
64#[derive(Debug)]
65#[brw(little)]
66#[allow(dead_code)]
67struct PreBoneDeformerHeader {
68    count: i32,
69
70    #[br(count = count)]
71    items: Vec<PreBoneDeformerItem>,
72
73    #[br(count = count)]
74    links: Vec<PreBoneDeformerLink>,
75}
76
77pub struct PreBoneDeformer {
78    header: PreBoneDeformerHeader,
79}
80
81#[derive(Debug)]
82pub struct PreBoneDeformBone {
83    /// Name of the affected bone
84    pub name: String,
85    /// The deform matrix
86    pub deform: [f32; 12],
87}
88
89#[derive(Debug)]
90pub struct PreBoneDeformMatrices {
91    /// The prebone deform bones
92    pub bones: Vec<PreBoneDeformBone>,
93}
94
95impl PreBoneDeformer {
96    /// Reads an existing PBD file
97    pub fn from_existing(buffer: ByteSpan) -> Option<PreBoneDeformer> {
98        let mut cursor = Cursor::new(buffer);
99        let header = PreBoneDeformerHeader::read(&mut cursor).ok()?;
100
101        Some(PreBoneDeformer { header })
102    }
103
104    /// Calculates the deform matrices between two races
105    pub fn get_deform_matrices(
106        &self,
107        from_body_id: u16,
108        to_body_id: u16,
109    ) -> Option<PreBoneDeformMatrices> {
110        if from_body_id == to_body_id {
111            return None;
112        }
113
114        let mut item = self
115            .header
116            .items
117            .iter()
118            .find(|x| x.body_id == from_body_id)?;
119        let mut next = &self.header.links[item.link_index as usize];
120
121        if next.next_sibling_index == -1 {
122            return None;
123        }
124
125        let mut bones = vec![];
126
127        loop {
128            for i in 0..item.deformer.bone_count {
129                bones.push(PreBoneDeformBone {
130                    name: item.deformer.bone_names[i as usize].clone(),
131                    deform: item.deformer.transform[i as usize],
132                })
133            }
134
135            if next.parent_index == -1 {
136                break;
137            }
138
139            next = &self.header.links[next.parent_index as usize];
140            item = &self.header.items[next.deformer_index as usize];
141
142            if item.body_id == to_body_id {
143                break;
144            }
145        }
146
147        Some(PreBoneDeformMatrices { bones })
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use std::fs::read;
154    use std::path::PathBuf;
155
156    use super::*;
157
158    #[test]
159    fn test_invalid() {
160        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
161        d.push("resources/tests");
162        d.push("random");
163
164        // Feeding it invalid data should not panic
165        PreBoneDeformer::from_existing(&read(d).unwrap());
166    }
167}