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