Skip to main content

rakata_formats/bwm/
reader.rs

1//! BWM binary reader.
2
3use std::io::Read;
4
5use super::{
6    binary, checked_mul, Bwm, BwmAabbNode, BwmAdjacency, BwmBinaryError, BwmEdge, BwmFace,
7    BwmTypeCode, BwmVec3, AABB_ENTRY_SIZE, ADJACENCY_ENTRY_SIZE, BWM_MAGIC, BWM_VERSION_V10,
8    DISTANCE_ENTRY_SIZE, EDGE_ENTRY_SIZE, FACE_INDEX_ENTRY_SIZE, FILE_HEADER_SIZE,
9    MATERIAL_ENTRY_SIZE, NORMAL_ENTRY_SIZE, PERIMETER_ENTRY_SIZE, VERTEX_ENTRY_SIZE,
10};
11
12/// Reads BWM data from a reader.
13///
14/// # Errors
15///
16/// [`BwmBinaryError::Io`] when the stream will not read to end, and whatever
17/// [`read_bwm_from_bytes`] reports for the bytes it collected.
18#[cfg_attr(
19    feature = "tracing",
20    tracing::instrument(level = "debug", skip(reader))
21)]
22pub fn read_bwm<R: Read>(reader: &mut R) -> Result<Bwm, BwmBinaryError> {
23    let mut bytes = Vec::new();
24    reader.read_to_end(&mut bytes)?;
25    read_bwm_from_bytes(&bytes)
26}
27
28/// Reads BWM data from bytes.
29///
30/// # Errors
31///
32/// [`BwmBinaryError::InvalidMagic`] and [`BwmBinaryError::InvalidVersion`] for
33/// a header that is not a BWM of a supported version,
34/// [`BwmBinaryError::InvalidHeader`] when a section's offset and count run
35/// past the end of `bytes`, and [`BwmBinaryError::InvalidData`] when the
36/// sections read and do not describe a walkmesh.
37#[cfg_attr(
38    feature = "tracing",
39    tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
40)]
41pub fn read_bwm_from_bytes(bytes: &[u8]) -> Result<Bwm, BwmBinaryError> {
42    if bytes.len() < FILE_HEADER_SIZE {
43        return Err(BwmBinaryError::InvalidHeader(
44            "file smaller than BWM header".into(),
45        ));
46    }
47
48    let magic = binary::read_fourcc(bytes, 0)?;
49    binary::expect_fourcc(magic, BWM_MAGIC).map_err(BwmBinaryError::InvalidMagic)?;
50    let version = binary::read_fourcc(bytes, 4)?;
51    binary::expect_fourcc(version, BWM_VERSION_V10).map_err(BwmBinaryError::InvalidVersion)?;
52
53    // --- Header properties (0x08..0x48) ---
54    let walkmesh_type = BwmTypeCode::from_raw(binary::read_u32(bytes, 8)?); // 0x08
55    let relative_hook1 = read_vec3(bytes, 12)?; // 0x0C
56    let relative_hook2 = read_vec3(bytes, 24)?; // 0x18
57    let absolute_hook1 = read_vec3(bytes, 36)?; // 0x24
58    let absolute_hook2 = read_vec3(bytes, 48)?; // 0x30
59    let position = read_vec3(bytes, 60)?; // 0x3C
60
61    // --- Table counts and offsets (0x48..0x88) ---
62    let vertex_count = binary::checked_to_usize(binary::read_u32(bytes, 72)?, "vertex_count")?; // 0x48
63    let vertex_offset = binary::checked_to_usize(binary::read_u32(bytes, 76)?, "vertex_offset")?; // 0x4C
64    let face_count = binary::checked_to_usize(binary::read_u32(bytes, 80)?, "face_count")?; // 0x50
65    let face_indices_offset =
66        binary::checked_to_usize(binary::read_u32(bytes, 84)?, "face_indices_offset")?; // 0x54
67    let materials_offset =
68        binary::checked_to_usize(binary::read_u32(bytes, 88)?, "materials_offset")?; // 0x58
69    let normals_offset = binary::checked_to_usize(binary::read_u32(bytes, 92)?, "normals_offset")?; // 0x5C
70    let planar_distances_offset =
71        binary::checked_to_usize(binary::read_u32(bytes, 96)?, "planar_distances_offset")?; // 0x60
72    let aabb_count = binary::checked_to_usize(binary::read_u32(bytes, 100)?, "aabb_count")?; // 0x64
73    let aabb_offset = binary::checked_to_usize(binary::read_u32(bytes, 104)?, "aabb_offset")?; // 0x68
74    let unknown = binary::read_u32(bytes, 108)?; // 0x6C
75    let adjacency_count =
76        binary::checked_to_usize(binary::read_u32(bytes, 112)?, "adjacency_count")?; // 0x70
77    let adjacency_offset =
78        binary::checked_to_usize(binary::read_u32(bytes, 116)?, "adjacency_offset")?; // 0x74
79    let edge_count = binary::checked_to_usize(binary::read_u32(bytes, 120)?, "edge_count")?; // 0x78
80    let edges_offset = binary::checked_to_usize(binary::read_u32(bytes, 124)?, "edges_offset")?; // 0x7C
81    let perimeter_count =
82        binary::checked_to_usize(binary::read_u32(bytes, 128)?, "perimeter_count")?; // 0x80
83    let perimeters_offset =
84        binary::checked_to_usize(binary::read_u32(bytes, 132)?, "perimeters_offset")?; // 0x84
85
86    // --- Validate all table extents fit within the file ---
87    let vertices_size = checked_mul(vertex_count, VERTEX_ENTRY_SIZE, "vertices_size")?;
88    binary::check_slice_in_bounds(bytes, vertex_offset, vertices_size, "vertices")?;
89    let face_indices_size = checked_mul(face_count, FACE_INDEX_ENTRY_SIZE, "face_indices_size")?;
90    binary::check_slice_in_bounds(
91        bytes,
92        face_indices_offset,
93        face_indices_size,
94        "face_indices",
95    )?;
96    let materials_size = checked_mul(face_count, MATERIAL_ENTRY_SIZE, "materials_size")?;
97    binary::check_slice_in_bounds(bytes, materials_offset, materials_size, "materials")?;
98    let normals_size = checked_mul(face_count, NORMAL_ENTRY_SIZE, "normals_size")?;
99    binary::check_slice_in_bounds(bytes, normals_offset, normals_size, "normals")?;
100    let distances_size = checked_mul(face_count, DISTANCE_ENTRY_SIZE, "distances_size")?;
101    binary::check_slice_in_bounds(
102        bytes,
103        planar_distances_offset,
104        distances_size,
105        "planar_distances",
106    )?;
107    let aabb_size = checked_mul(aabb_count, AABB_ENTRY_SIZE, "aabb_size")?;
108    binary::check_slice_in_bounds(bytes, aabb_offset, aabb_size, "aabb_nodes")?;
109    let adjacency_size = checked_mul(adjacency_count, ADJACENCY_ENTRY_SIZE, "adjacency_size")?;
110    binary::check_slice_in_bounds(bytes, adjacency_offset, adjacency_size, "adjacencies")?;
111    let edge_size = checked_mul(edge_count, EDGE_ENTRY_SIZE, "edge_size")?;
112    binary::check_slice_in_bounds(bytes, edges_offset, edge_size, "edges")?;
113    let perimeter_size = checked_mul(perimeter_count, PERIMETER_ENTRY_SIZE, "perimeter_size")?;
114    binary::check_slice_in_bounds(bytes, perimeters_offset, perimeter_size, "perimeters")?;
115
116    // --- Read vertex table ---
117    let mut vertices = Vec::with_capacity(vertex_count);
118    for index in 0..vertex_count {
119        let base = vertex_offset + index * VERTEX_ENTRY_SIZE;
120        vertices.push(read_vec3(bytes, base)?);
121    }
122
123    // --- Read face tables (indices, materials, normals, planar distances) ---
124    let mut faces = Vec::with_capacity(face_count);
125    for index in 0..face_count {
126        let indices_base = face_indices_offset + index * FACE_INDEX_ENTRY_SIZE;
127        let i1 = binary::read_u32(bytes, indices_base)?;
128        let i2 = binary::read_u32(bytes, indices_base + 4)?;
129        let i3 = binary::read_u32(bytes, indices_base + 8)?;
130        for (slot, value) in [i1, i2, i3].iter().enumerate() {
131            let vertex_index = binary::checked_to_usize(*value, "face_vertex_index")?;
132            if vertex_index >= vertices.len() {
133                return Err(BwmBinaryError::InvalidData(format!(
134                    "faces[{index}].vertex_indices[{slot}]={value} out of bounds for {} vertices",
135                    vertices.len()
136                )));
137            }
138        }
139
140        let material_base = materials_offset + index * MATERIAL_ENTRY_SIZE;
141        let material_id = binary::read_u32(bytes, material_base)?;
142        let normal_base = normals_offset + index * NORMAL_ENTRY_SIZE;
143        let normal = read_vec3(bytes, normal_base)?;
144        let distance_base = planar_distances_offset + index * DISTANCE_ENTRY_SIZE;
145        let planar_distance = binary::read_f32(bytes, distance_base)?;
146
147        faces.push(BwmFace {
148            vertex_indices: [i1, i2, i3],
149            material_id,
150            normal,
151            planar_distance,
152        });
153    }
154
155    // --- Read AABB tree nodes ---
156    let mut aabb_nodes = Vec::with_capacity(aabb_count);
157    for index in 0..aabb_count {
158        let base = aabb_offset + index * AABB_ENTRY_SIZE;
159        aabb_nodes.push(BwmAabbNode {
160            bb_min: read_vec3(bytes, base)?,
161            bb_max: read_vec3(bytes, base + 12)?,
162            face_index: binary::read_u32(bytes, base + 24)?,
163            unknown: binary::read_u32(bytes, base + 28)?,
164            split_axis: binary::read_u32(bytes, base + 32)?,
165            left_child: binary::read_u32(bytes, base + 36)?,
166            right_child: binary::read_u32(bytes, base + 40)?,
167        });
168    }
169
170    // --- Read adjacency, edge, and perimeter tables ---
171    let mut adjacencies = Vec::with_capacity(adjacency_count);
172    for index in 0..adjacency_count {
173        let base = adjacency_offset + index * ADJACENCY_ENTRY_SIZE;
174        adjacencies.push(BwmAdjacency {
175            edge_refs: [
176                read_i32(bytes, base)?,
177                read_i32(bytes, base + 4)?,
178                read_i32(bytes, base + 8)?,
179            ],
180        });
181    }
182
183    let mut edges = Vec::with_capacity(edge_count);
184    for index in 0..edge_count {
185        let base = edges_offset + index * EDGE_ENTRY_SIZE;
186        edges.push(BwmEdge {
187            edge_index: read_i32(bytes, base)?,
188            transition: read_i32(bytes, base + 4)?,
189        });
190    }
191
192    let mut perimeters = Vec::with_capacity(perimeter_count);
193    for index in 0..perimeter_count {
194        let base = perimeters_offset + index * PERIMETER_ENTRY_SIZE;
195        perimeters.push(binary::read_u32(bytes, base)?);
196    }
197
198    Ok(Bwm {
199        walkmesh_type,
200        relative_hook1,
201        relative_hook2,
202        absolute_hook1,
203        absolute_hook2,
204        position,
205        unknown,
206        vertices,
207        faces,
208        aabb_nodes,
209        adjacencies,
210        edges,
211        perimeters,
212    })
213}
214
215fn read_i32(bytes: &[u8], offset: usize) -> Result<i32, BwmBinaryError> {
216    let raw = binary::read_u32(bytes, offset)?;
217    Ok(i32::from_le_bytes(raw.to_le_bytes()))
218}
219
220fn read_vec3(bytes: &[u8], offset: usize) -> Result<BwmVec3, BwmBinaryError> {
221    Ok(BwmVec3 {
222        x: binary::read_f32(bytes, offset)?,
223        y: binary::read_f32(bytes, offset + 4)?,
224        z: binary::read_f32(bytes, offset + 8)?,
225    })
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::bwm::{write_bwm_to_vec, BwmType};
232
233    const TEST_WOK: &[u8] = include_bytes!(concat!(
234        env!("CARGO_MANIFEST_DIR"),
235        "/../../fixtures/test.wok"
236    ));
237    const TOOLSET_WOK: &[u8] = include_bytes!(concat!(
238        env!("CARGO_MANIFEST_DIR"),
239        "/../../fixtures/zio006j.wok"
240    ));
241
242    fn synthetic_bwm() -> Bwm {
243        Bwm {
244            walkmesh_type: BwmTypeCode::from(BwmType::AreaModel),
245            relative_hook1: BwmVec3::new(1.0, 2.0, 3.0),
246            relative_hook2: BwmVec3::new(4.0, 5.0, 6.0),
247            absolute_hook1: BwmVec3::new(7.0, 8.0, 9.0),
248            absolute_hook2: BwmVec3::new(10.0, 11.0, 12.0),
249            position: BwmVec3::new(13.0, 14.0, 15.0),
250            unknown: 4,
251            vertices: vec![
252                BwmVec3::new(0.0, 0.0, 0.0),
253                BwmVec3::new(1.0, 0.0, 0.0),
254                BwmVec3::new(0.0, 1.0, 0.0),
255            ],
256            faces: vec![BwmFace {
257                vertex_indices: [0, 1, 2],
258                material_id: 1,
259                normal: BwmVec3::new(0.0, 0.0, 1.0),
260                planar_distance: 0.0,
261            }],
262            aabb_nodes: vec![BwmAabbNode {
263                bb_min: BwmVec3::new(0.0, 0.0, 0.0),
264                bb_max: BwmVec3::new(1.0, 1.0, 0.0),
265                face_index: 0,
266                unknown: 4,
267                split_axis: 0,
268                left_child: 0xFFFF_FFFF,
269                right_child: 0xFFFF_FFFF,
270            }],
271            adjacencies: vec![BwmAdjacency {
272                edge_refs: [-1, -1, -1],
273            }],
274            edges: vec![BwmEdge {
275                edge_index: 0,
276                transition: -1,
277            }],
278            perimeters: vec![1],
279        }
280    }
281
282    #[test]
283    fn roundtrip_synthetic_bwm() {
284        let bwm = synthetic_bwm();
285        let bytes = write_bwm_to_vec(&bwm).expect("write should succeed");
286        let parsed = read_bwm_from_bytes(&bytes).expect("read should succeed");
287        assert_eq!(parsed, bwm);
288    }
289
290    #[test]
291    fn writer_is_deterministic_for_synthetic_bwm() {
292        let bwm = synthetic_bwm();
293        let first = write_bwm_to_vec(&bwm).expect("first write should succeed");
294        let second = write_bwm_to_vec(&bwm).expect("second write should succeed");
295        assert_eq!(first, second);
296    }
297
298    #[test]
299    fn parses_wok_fixture() {
300        let bwm = read_bwm_from_bytes(TEST_WOK).expect("fixture should parse");
301        assert_eq!(bwm.walkmesh_type.known(), Some(BwmType::AreaModel));
302        assert_eq!(bwm.vertices.len(), 6);
303        assert_eq!(bwm.faces.len(), 4);
304        assert!(!bwm.aabb_nodes.is_empty());
305    }
306
307    #[test]
308    fn parses_toolset_wok_fixture() {
309        let bwm = read_bwm_from_bytes(TOOLSET_WOK).expect("fixture should parse");
310        assert_eq!(bwm.walkmesh_type.known(), Some(BwmType::AreaModel));
311        assert_eq!(bwm.vertices.len(), 4);
312        assert_eq!(bwm.faces.len(), 2);
313    }
314
315    #[test]
316    fn read_write_roundtrip_preserves_fixture_semantics() {
317        let parsed = read_bwm_from_bytes(TEST_WOK).expect("fixture should parse");
318        let bytes = write_bwm_to_vec(&parsed).expect("write should succeed");
319        let reparsed = read_bwm_from_bytes(&bytes).expect("re-read should succeed");
320        assert_eq!(reparsed, parsed);
321    }
322
323    #[test]
324    fn rejects_invalid_magic() {
325        let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
326        bytes[0..4].copy_from_slice(b"NOPE");
327        bytes[4..8].copy_from_slice(&BWM_VERSION_V10);
328        let err = read_bwm_from_bytes(&bytes).expect_err("must fail");
329        assert!(matches!(err, BwmBinaryError::InvalidMagic(_)));
330    }
331
332    #[test]
333    fn rejects_invalid_version() {
334        let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
335        bytes[0..4].copy_from_slice(&BWM_MAGIC);
336        bytes[4..8].copy_from_slice(b"V9.9");
337        let err = read_bwm_from_bytes(&bytes).expect_err("must fail");
338        assert!(matches!(err, BwmBinaryError::InvalidVersion(_)));
339    }
340
341    #[test]
342    fn rejects_truncated_header() {
343        let bytes = vec![0_u8; FILE_HEADER_SIZE - 1];
344        let err = read_bwm_from_bytes(&bytes).expect_err("must fail");
345        assert!(matches!(err, BwmBinaryError::InvalidHeader(_)));
346    }
347
348    #[test]
349    fn rejects_out_of_bounds_vertex_table() {
350        let mut bytes = write_bwm_to_vec(&synthetic_bwm()).expect("write");
351        bytes[76..80].copy_from_slice(&0xFFFF_FFFF_u32.to_le_bytes()); // vertex_offset
352        let err = read_bwm_from_bytes(&bytes).expect_err("must fail");
353        assert!(matches!(err, BwmBinaryError::InvalidHeader(_)));
354    }
355
356    #[test]
357    fn rejects_face_vertex_index_out_of_bounds() {
358        let mut bytes = write_bwm_to_vec(&synthetic_bwm()).expect("write");
359        // face_indices_offset defaults to 172 for synthetic layout.
360        let face_indices_offset = u32::from_le_bytes([bytes[84], bytes[85], bytes[86], bytes[87]]);
361        let face_indices_offset = usize::try_from(face_indices_offset).expect("offset fits");
362        bytes[face_indices_offset..face_indices_offset + 4].copy_from_slice(&99_u32.to_le_bytes());
363
364        let err = read_bwm_from_bytes(&bytes).expect_err("must fail");
365        assert!(matches!(err, BwmBinaryError::InvalidData(_)));
366    }
367
368    #[test]
369    fn decode_encode_traits_roundtrip() {
370        use crate::binary::{DecodeBinary, EncodeBinary};
371        let bwm = synthetic_bwm();
372        let bytes = bwm.encode_binary().expect("encode");
373        let decoded = Bwm::decode_binary(&bytes).expect("decode");
374        assert_eq!(decoded, bwm);
375    }
376}