1use 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#[derive(serde::Serialize, Debug, PartialEq)]
11pub struct Subobject {
12 pub center: [i32; 3],
13 pub radius: u32,
14 pub extents: Option<[f32; 3]>,
16 pub faces: Vec<u16>,
18}
19
20pub 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}