Skip to main content

rgpre/import/model3d/
subobjects.rs

1//! Authored collision face subsets; see docs/formats/models/3d.md.
2use super::parser::parse_3d_header;
3use crate::{Result, error::Error};
4use nom::{
5    IResult, Parser,
6    number::complete::{le_f32, le_i32, le_u16, le_u32},
7};
8
9/// One authored subobject. Integer center and radius retain their file units.
10#[derive(serde::Serialize, Debug, PartialEq)]
11pub struct Subobject {
12    pub center: [i32; 3],
13    pub radius: u32,
14    /// v5.0 only; v4.0 has no extent fields.
15    pub extents: Option<[f32; 3]>,
16    /// Original zero-based face indices, preserving order and duplicates.
17    pub faces: Vec<u16>,
18}
19
20/// Decode collision subsets from a complete v4.0/v5.0 model file.
21/// Face references are checked against both the index and original record offset.
22/// Earlier formats have no Section4 and return an empty list.
23pub fn parse_subobjects(bytes: &[u8]) -> Result<Vec<Subobject>> {
24    let (_, header) = parse_3d_header(bytes)?;
25    if header.is_v27_or_earlier() || header.section4_count == 0 {
26        return Ok(Vec::new());
27    }
28    let version5 = header.version == *b"v5.0";
29    if !version5 && header.version != *b"v4.0" {
30        return Err(Error::Parse("model subobjects: unsupported version".into()));
31    }
32    let invalid =
33        || Error::Parse("model subobjects: invalid offset, count or face reference".into());
34    let mut face_offsets = Vec::new();
35    let mut offset = usize::try_from(header.offset_face_data).map_err(|_| invalid())?;
36    for _ in 0..header.num_faces {
37        let count = usize::from(*bytes.get(offset).ok_or_else(invalid)?);
38        face_offsets.push(offset);
39        offset = offset.checked_add(10 + count * 8).ok_or_else(invalid)?;
40        if offset > bytes.len() {
41            return Err(invalid());
42        }
43    }
44    if header.offset_section4 == 0 {
45        return Err(invalid());
46    }
47    let mut input = bytes
48        .get(usize::try_from(header.offset_section4).map_err(|_| invalid())?..)
49        .ok_or_else(invalid)?;
50    let mut nodes = Vec::new();
51    for _ in 0..header.section4_count {
52        let (rest, (x, y, z, radius, count)) = node_header(input)?;
53        input = rest;
54        let extents = if version5 {
55            let (rest, (x, y, z)) = extent_fields(input)?;
56            input = rest;
57            if ![x, y, z].iter().all(|v| v.is_finite()) {
58                return Err(invalid());
59            }
60            Some([x, y, z])
61        } else {
62            None
63        };
64        let mut faces = Vec::new();
65        for _ in 0..count {
66            let (rest, (offset, index)) = face_reference(input)?;
67            input = rest;
68            if index % 4 != 0
69                || face_offsets.get(usize::from(index / 4)).copied() != usize::try_from(offset).ok()
70            {
71                return Err(invalid());
72            }
73            faces.push(index / 4);
74        }
75        nodes.push(Subobject {
76            center: [x, y, z],
77            radius,
78            extents,
79            faces,
80        });
81    }
82    Ok(nodes)
83}
84
85fn node_header(input: &[u8]) -> IResult<&[u8], (i32, i32, i32, u32, u16)> {
86    (le_i32, le_i32, le_i32, le_u32, le_u16).parse(input)
87}
88fn extent_fields(input: &[u8]) -> IResult<&[u8], (f32, f32, f32)> {
89    (le_f32, le_f32, le_f32).parse(input)
90}
91fn face_reference(input: &[u8]) -> IResult<&[u8], (u32, u16)> {
92    (le_u32, le_u16).parse(input)
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    fn fixture(version5: bool) -> Vec<u8> {
100        let mut bytes = vec![0; 74];
101        bytes[..4].copy_from_slice(if version5 { b"v5.0" } else { b"v4.0" });
102        for (offset, value) in [(8, 1u32), (28, 74), (32, 1), (60, 64)] {
103            bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
104        }
105        for value in [1i32, -2, 3, 4] {
106            bytes.extend(value.to_le_bytes());
107        }
108        bytes.extend(2u16.to_le_bytes());
109        if version5 {
110            for value in [1.0f32, 2.0, 3.0] {
111                bytes.extend(value.to_le_bytes());
112            }
113        }
114        for _ in 0..2 {
115            bytes.extend(64u32.to_le_bytes());
116            bytes.extend(0u16.to_le_bytes());
117        }
118        bytes
119    }
120
121    #[test]
122    fn versions_preserve_duplicate_references_and_reject_corruption() {
123        for version5 in [false, true] {
124            let mut bytes = fixture(version5);
125            assert_eq!(
126                parse_subobjects(&bytes).unwrap(),
127                vec![Subobject {
128                    center: [1, -2, 3],
129                    radius: 4,
130                    extents: version5.then_some([1.0, 2.0, 3.0]),
131                    faces: vec![0, 0],
132                }]
133            );
134            let last = bytes.len() - 6;
135            bytes[last] = 65;
136            assert!(parse_subobjects(&bytes).is_err());
137            bytes[last] = 64;
138            bytes.pop();
139            assert!(parse_subobjects(&bytes).is_err());
140        }
141    }
142    #[test]
143    fn installed_subobjects_preserve_authored_face_subsets() {
144        let Some(root) = std::env::var_os("REDGUARD_DIR") else {
145            eprintln!("skipping subobject install check: REDGUARD_DIR absent");
146            return;
147        };
148        let directory = std::path::PathBuf::from(root).join("fxart");
149        let mut models = 0;
150        let mut nodes = 0;
151        let mut references = 0;
152        let mut omitted = 0;
153        for entry in std::fs::read_dir(directory).unwrap() {
154            let path = entry.unwrap().path();
155            if !path
156                .extension()
157                .is_some_and(|v| v.eq_ignore_ascii_case("rob"))
158            {
159                continue;
160            }
161            let archive =
162                crate::import::rob::parse_rob_file(&std::fs::read(&path).unwrap()).unwrap();
163            for segment in archive.segments {
164                if !matches!(segment.data.get(..4), Some(b"v4.0" | b"v5.0")) {
165                    continue;
166                }
167                let parsed = parse_subobjects(&segment.data).unwrap_or_else(|error| {
168                    panic!("{} {}: {error}", path.display(), segment.name())
169                });
170                if parsed.is_empty() {
171                    continue;
172                }
173                let (_, header) = parse_3d_header(&segment.data).unwrap();
174                let unique: std::collections::BTreeSet<_> = parsed
175                    .iter()
176                    .flat_map(|n| n.faces.iter().copied())
177                    .collect();
178                omitted += usize::from(unique.len() < header.num_faces as usize);
179                models += 1;
180                nodes += parsed.len();
181                references += parsed.iter().map(|n| n.faces.len()).sum::<usize>();
182            }
183        }
184        assert_eq!(
185            (models, nodes, references, omitted),
186            (3820, 22931, 222322, 9)
187        );
188    }
189}