1pub mod parser;
3pub mod subobjects;
4
5#[derive(serde::Serialize, Debug, Clone, PartialEq, Eq)]
6pub enum ModelVersion {
8 V26,
9 V27,
10 V40,
11 V50,
12 Unknown,
13}
14
15#[derive(serde::Serialize, Debug, Clone)]
16pub enum TextureData {
18 SolidColor(u8),
19 Texture { texture_id: u16, image_id: u8 },
20}
21
22#[derive(serde::Serialize, Debug, Clone)]
23pub struct Model3DHeader {
25 pub version: [u8; 4],
26 pub num_vertices: u32,
27 pub num_faces: u32,
28 pub radius: u32,
29 pub num_frames: u32,
30 pub offset_frame_data: u32,
31 pub total_face_vertices: u32,
32 pub offset_section4: u32,
33 pub section4_count: u32,
34 #[allow(
35 clippy::pub_underscore_fields,
36 reason = "field name mirrors reverse-engineered binary layout"
37 )]
38 pub _unused_24: u32,
41 pub offset_normal_indices: u32,
42 pub offset_vertex_normals: u32,
43 pub offset_vertex_coords: u32,
44 pub offset_face_normals: u32,
45 pub total_face_vertices_dup: u32,
46 pub offset_face_data: u32,
47}
48
49#[derive(serde::Serialize, Debug, Clone)]
50pub struct FaceVertex {
53 pub vertex_index: u32,
54 pub u: i32,
55 pub v: i32,
56}
57
58#[derive(serde::Serialize, Debug, Clone)]
59pub struct FaceData {
61 pub vertex_count: u8,
62 pub tex_hi: u8,
64 pub texture_data: TextureData,
65 pub face_vertices: Vec<FaceVertex>,
66}
67
68impl FaceData {
69 #[must_use]
71 pub const fn size_in_bytes(&self, version: &ModelVersion) -> usize {
72 let texture_header_size = if matches!(version, ModelVersion::V40 | ModelVersion::V50) {
73 1 + 4 } else {
75 1 + 2 };
77 let vertex_data_size = self.face_vertices.len() * (4 + 2 + 2);
78 1 + texture_header_size + 4 + vertex_data_size
79 }
80}
81
82#[derive(serde::Serialize, Debug, Clone, Copy)]
83pub struct VertexCoord {
85 pub x: f32,
86 pub y: f32,
87 pub z: f32,
88}
89
90#[derive(serde::Serialize, Debug, Clone)]
91pub struct FaceNormal {
93 pub x: f32,
94 pub y: f32,
95 pub z: f32,
96}
97
98#[derive(serde::Serialize, Debug, Clone)]
99pub struct VertexNormal {
101 pub x: f32,
102 pub y: f32,
103 pub z: f32,
104}
105
106#[derive(serde::Serialize, Debug, Clone, Copy, PartialEq, Eq)]
107pub enum FrameType {
109 Static3D,
110 AnimatedI16,
111 AnimatedI16Unscaled,
113 AnimatedI32,
114 SoftwareI16 {
116 base_frame: u16,
117 shift: u8,
118 },
119 Static3DC,
120 Unknown(u32),
121}
122
123#[derive(serde::Serialize, Debug, Clone)]
124pub struct FrameDataEntry {
126 pub vertex_offset: u32,
127 pub normal_offset: u32,
128 pub reserved: u32,
129 pub frame_type: FrameType,
130}
131
132#[derive(serde::Serialize, Debug, Clone)]
134pub struct FrameVertexData {
135 pub coords: Vec<VertexCoord>,
136}
137
138#[derive(serde::Serialize, Debug, Clone)]
140pub struct FrameNormalData {
141 pub face_normals: Vec<FaceNormal>,
142}
143
144#[derive(serde::Serialize, Debug, Clone)]
146pub struct Model3DFile {
147 pub header: Model3DHeader,
148 pub version: ModelVersion,
149 pub frame_data: Vec<FrameDataEntry>,
150 pub face_data: Vec<FaceData>,
151 pub vertex_coords: Vec<VertexCoord>,
152 pub face_normals: Vec<FaceNormal>,
153 pub normal_indices: Vec<u32>,
154 pub vertex_normals: Vec<VertexNormal>,
155 pub frame_vertex_data: Vec<FrameVertexData>,
157 pub frame_normal_data: Vec<FrameNormalData>,
159}
160
161impl Model3DHeader {
162 #[must_use]
164 pub fn version_string(&self) -> String {
165 String::from_utf8_lossy(&self.version)
166 .trim_matches('\0')
167 .to_string()
168 }
169
170 #[must_use]
172 pub fn parse_version(&self) -> ModelVersion {
173 let version_str = self.version_string();
174 match version_str.as_str() {
175 "v2.6" => ModelVersion::V26,
176 "v2.7" => ModelVersion::V27,
177 "v4.0" => ModelVersion::V40,
178 "v5.0" => ModelVersion::V50,
179 _ => ModelVersion::Unknown,
180 }
181 }
182
183 #[must_use]
185 pub fn is_v27_or_earlier(&self) -> bool {
186 matches!(self.parse_version(), ModelVersion::V26 | ModelVersion::V27)
187 }
188
189 #[must_use]
191 pub fn is_v40_or_later(&self) -> bool {
192 matches!(self.parse_version(), ModelVersion::V40 | ModelVersion::V50)
193 }
194}
195
196impl Model3DFile {
197 #[must_use]
199 pub fn total_face_vertices(&self) -> usize {
200 self.face_data
201 .iter()
202 .map(|face| usize::from(face.vertex_count))
203 .sum()
204 }
205
206 #[must_use]
208 pub fn bounding_box(&self) -> Option<(VertexCoord, VertexCoord)> {
209 if self.vertex_coords.is_empty() {
210 return None;
211 }
212
213 let mut min = VertexCoord {
214 x: f32::INFINITY,
215 y: f32::INFINITY,
216 z: f32::INFINITY,
217 };
218 let mut max = VertexCoord {
219 x: f32::NEG_INFINITY,
220 y: f32::NEG_INFINITY,
221 z: f32::NEG_INFINITY,
222 };
223
224 for vertex in &self.vertex_coords {
225 min.x = min.x.min(vertex.x);
226 min.y = min.y.min(vertex.y);
227 min.z = min.z.min(vertex.z);
228 max.x = max.x.max(vertex.x);
229 max.y = max.y.max(vertex.y);
230 max.z = max.z.max(vertex.z);
231 }
232
233 Some((min, max))
234 }
235}
236
237use crate::{Result, error::Error};
238use log::warn;
239
240fn parse_data_internal(input: &[u8], kind: &str) -> Result<Model3DFile> {
241 match parser::parse_3d_file(input) {
242 Ok((remaining, model)) => {
243 if !remaining.is_empty() {
244 warn!("{} bytes remaining in {} data", remaining.len(), kind);
245 }
246 Ok(model)
247 }
248 Err(e)
249 if (input.starts_with(b"v2.6") || input.starts_with(b"v2.7")) && input.len() >= 64 =>
250 {
251 Err(Error::Parse(format!(
252 "Failed to parse software {kind}: invalid or incomplete animation directory, positions, normals or faces ({e:?})"
253 )))
254 }
255 Err(e) => Err(Error::Parse(format!("Failed to parse {kind}: {e:?}"))),
256 }
257}
258
259pub fn parse_3d_file(input: &[u8]) -> Result<Model3DFile> {
261 parse_data_internal(input, "3D model file")
262}
263
264impl Model3DFile {
265 pub fn at_frame(&self, frame: usize) -> crate::Result<Self> {
271 use crate::error::Error;
272 let invalid = |message: &str| Error::Conversion(format!("model frame {frame}: {message}"));
273 if frame >= (self.header.num_frames as usize).max(1) {
274 return Err(invalid("outside frame table"));
275 }
276 let mut posed = self.clone();
277 posed.header.num_frames = 1;
278 posed.frame_data.clear();
279 posed.frame_vertex_data.clear();
280 posed.frame_normal_data.clear();
281 if frame == 0 {
282 return Ok(posed);
283 }
284 let encoding = self
285 .frame_data
286 .first()
287 .ok_or_else(|| invalid("missing encoding"))?
288 .frame_type;
289 let positions = &self
290 .frame_vertex_data
291 .get(frame)
292 .ok_or_else(|| invalid("missing positions"))?
293 .coords;
294 let normals = &self
295 .frame_normal_data
296 .get(frame)
297 .ok_or_else(|| invalid("missing normals"))?
298 .face_normals;
299 if positions.len() != self.vertex_coords.len() || normals.len() != self.face_data.len() {
300 return Err(invalid("incomplete geometry"));
301 }
302 posed.vertex_coords = match encoding {
303 FrameType::AnimatedI16
304 | FrameType::AnimatedI16Unscaled
305 | FrameType::AnimatedI32
306 | FrameType::SoftwareI16 { .. } => positions.clone(),
307 FrameType::Static3D
308 if matches!(self.version, ModelVersion::V40 | ModelVersion::V50) =>
309 {
310 positions.clone()
311 }
312 _ => return Err(invalid("unsupported encoding")),
313 };
314 posed.face_normals = normals.clone();
315 Ok(posed)
316 }
317}
318
319#[must_use]
323pub fn export_model_metadata(
324 model: &Model3DFile,
325 subobjects: &[subobjects::Subobject],
326) -> serde_json::Value {
327 serde_json::json!({
328 "model": model,
329 "subobjects": subobjects,
330 "scene_subobjects": subobjects.iter().map(|subobject| serde_json::json!({
331 "center": crate::gltf::model_position(subobject.center.map(|value| value as f32 / 256.0)),
332 "radius": subobject.radius as f32 / 256.0 / crate::gltf::ENGINE_UNIT_SCALE,
333 "extent_factors": subobject.extents,
334 "faces": subobject.faces,
335 })).collect::<Vec<_>>(),
336 "uv_units": "sixteenths_of_a_texel",
337 "bounding_box": model.bounding_box(),
338 })
339}