Skip to main content

rakata_formats/bwm/
mod.rs

1//! BWM/WOK binary reader and writer.
2//!
3//! BWM is KotOR's binary walkmesh container format. The same binary layout is
4//! used by `.wok` (area walkmesh), `.pwk` (placeable walkmesh), and `.dwk`
5//! (door walkmesh) resources.
6//!
7//! This module provides lossless table-level parsing and deterministic
8//! serialization for the binary `BWM V1.0` layout.
9//!
10//! ## Shape of the container
11//!
12//! A 136-byte header and nine data blocks, each located by its own offset out
13//! of that header rather than by following on from the block before it.
14//!
15//! One `face_count` sizes four of the nine: indices, materials, normals and
16//! planar distances are parallel arrays over the same faces, not independent
17//! tables. Vertices, AABB nodes, adjacency, edges and perimeters each carry
18//! their own count.
19//!
20//! ## What is and is not verified here
21//!
22//! The counts and offsets from `0x48` onward are confirmed against a real file:
23//! the block chain closes exactly, each block starting where the last ends and
24//! the final one reaching the file's exact length.
25//!
26//! The header before `0x48` is confirmed too, though not by the fixtures, which
27//! zero the whole region and are unrepresentative: across 1,554 vanilla
28//! walkmeshes the hook fields are populated in 81% of them, and `0x08` turns
29//! out to be a kind discriminator rather than any kind of count. The format
30//! page's engine audit reads these bytes differently and is refuted by that
31//! measurement; see `docs/src/formats/models/walkmesh.md`.
32//!
33//! What remains open is the tail: every file is contiguous, but a fifth of
34//! `.wok` and a third of `.pwk` carry bytes past the last populated block, and
35//! nobody has established whether that is padding or content. Preserve it.
36
37use num_enum::{IntoPrimitive, TryFromPrimitive};
38use thiserror::Error;
39
40use crate::binary::{self, DecodeBinary, EncodeBinary};
41
42/// ASCII reader.
43pub mod ascii_reader;
44/// ASCII writer.
45pub mod ascii_writer;
46mod reader;
47mod writer;
48
49pub use ascii_reader::{read_bwm_ascii, BwmAsciiError};
50pub use ascii_writer::write_bwm_ascii;
51pub use reader::{read_bwm, read_bwm_from_bytes};
52pub use writer::{write_bwm, write_bwm_to_vec};
53
54/// Binary BWM header size.
55pub(super) const FILE_HEADER_SIZE: usize = 136;
56/// Vertex row size (`x, y, z` float32).
57pub(super) const VERTEX_ENTRY_SIZE: usize = 12;
58/// Face-index row size (`i1, i2, i3` u32).
59pub(super) const FACE_INDEX_ENTRY_SIZE: usize = 12;
60/// Face-material row size (`material_id` u32).
61pub(super) const MATERIAL_ENTRY_SIZE: usize = 4;
62/// Face-normal row size (`x, y, z` float32).
63pub(super) const NORMAL_ENTRY_SIZE: usize = 12;
64/// Planar-distance row size (`distance` float32).
65pub(super) const DISTANCE_ENTRY_SIZE: usize = 4;
66/// AABB-node row size.
67pub(super) const AABB_ENTRY_SIZE: usize = 44;
68/// Adjacency row size (`edge_ref[3]` i32).
69pub(super) const ADJACENCY_ENTRY_SIZE: usize = 12;
70/// Edge row size (`edge_index`, `transition`) i32.
71pub(super) const EDGE_ENTRY_SIZE: usize = 8;
72/// Perimeter row size (`edge_table_index`) u32.
73pub(super) const PERIMETER_ENTRY_SIZE: usize = 4;
74/// BWM magic.
75pub(super) const BWM_MAGIC: [u8; 4] = *b"BWM ";
76/// BWM version used by KotOR.
77pub(super) const BWM_VERSION_V10: [u8; 4] = *b"V1.0";
78
79/// Surface material IDs used in walkmeshes.
80///
81/// These correspond to row indices in `surfacemat.2DA`.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
83#[repr(u32)]
84pub enum SurfaceMaterial {
85    /// Undefined material.
86    Undefined = 0,
87    /// Dirt.
88    Dirt = 1,
89    /// Obscuring (Grass).
90    Obscuring = 2,
91    /// Stone.
92    Stone = 3,
93    /// Wood.
94    Wood = 4,
95    /// Water.
96    Water = 5,
97    /// Non-walkable.
98    NonWalk = 6,
99    /// Transparent.
100    Transparent = 7,
101    /// Carpet.
102    Carpet = 8,
103    /// Metal.
104    Metal = 9,
105    /// Puddles.
106    Puddles = 10,
107    /// Swamp.
108    Swamp = 11,
109    /// Mud.
110    Mud = 12,
111    /// Leaves.
112    Leaves = 13,
113    /// Lava.
114    Lava = 14,
115    /// Bottomless Pit.
116    BottomlessPit = 15,
117    /// Deep Water.
118    DeepWater = 16,
119    /// Door.
120    Door = 17,
121    /// Non-walkable (Grass).
122    NonWalkGrass = 18,
123    /// Non-walkable (Stone).
124    NonWalkStone = 19,
125    /// Non-walkable (Wood).
126    NonWalkWood = 20,
127    /// Non-walkable (Water).
128    NonWalkWater = 21,
129    /// Non-walkable (Glass).
130    NonWalkGlass = 22,
131    /// Non-walkable (Carpet).
132    NonWalkCarpet = 23,
133    /// Non-walkable (Metal).
134    NonWalkMetal = 24,
135    /// Non-walkable (Puddles).
136    NonWalkPuddles = 25,
137    /// Non-walkable (Swamp).
138    NonWalkSwamp = 26,
139    /// Non-walkable (Mud).
140    NonWalkMud = 27,
141    /// Non-walkable (Leaves).
142    NonWalkLeaves = 28,
143    /// Non-walkable (Lava).
144    NonWalkLava = 29,
145    /// Non-walkable (Bottomless Pit).
146    NonWalkBottomlessPit = 30,
147}
148
149impl SurfaceMaterial {
150    /// Returns true if this material is typically walkable.
151    ///
152    /// This mimics the `Walk` column in `surfacemat.2DA`.
153    pub fn is_walkable(self) -> bool {
154        !matches!(
155            self,
156            Self::NonWalk
157                | Self::BottomlessPit
158                | Self::DeepWater
159                | Self::NonWalkGrass
160                | Self::NonWalkStone
161                | Self::NonWalkWood
162                | Self::NonWalkWater
163                | Self::NonWalkGlass
164                | Self::NonWalkCarpet
165                | Self::NonWalkMetal
166                | Self::NonWalkPuddles
167                | Self::NonWalkSwamp
168                | Self::NonWalkMud
169                | Self::NonWalkLeaves
170                | Self::NonWalkLava
171                | Self::NonWalkBottomlessPit
172        )
173    }
174}
175
176/// Known walkmesh type values.
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
178#[repr(u32)]
179pub enum BwmType {
180    /// Placeable/door walkmesh (`PWK`/`DWK`) in local coordinates.
181    PlaceableOrDoor = 0,
182    /// Area walkmesh (`WOK`) in world coordinates.
183    AreaModel = 1,
184}
185
186/// Lossless walkmesh-type wrapper.
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
188pub struct BwmTypeCode(u32);
189
190impl BwmTypeCode {
191    /// Creates a type code from a raw on-disk value.
192    pub const fn from_raw(raw: u32) -> Self {
193        Self(raw)
194    }
195
196    /// Returns the raw on-disk value.
197    pub const fn raw(self) -> u32 {
198        self.0
199    }
200
201    /// Returns the known walkmesh type for this code when available.
202    pub fn known(self) -> Option<BwmType> {
203        BwmType::try_from(self.0).ok()
204    }
205}
206
207impl From<BwmType> for BwmTypeCode {
208    fn from(value: BwmType) -> Self {
209        Self(u32::from(value))
210    }
211}
212
213/// Three-dimensional vector value.
214#[derive(Debug, Clone, Copy, PartialEq)]
215pub struct BwmVec3 {
216    /// X component.
217    pub x: f32,
218    /// Y component.
219    pub y: f32,
220    /// Z component.
221    pub z: f32,
222}
223
224impl BwmVec3 {
225    /// Creates a vector from components.
226    pub const fn new(x: f32, y: f32, z: f32) -> Self {
227        Self { x, y, z }
228    }
229}
230
231/// One face row assembled from index/material/normal/distance tables.
232#[derive(Debug, Clone, PartialEq)]
233pub struct BwmFace {
234    /// Vertex indices into [`Bwm::vertices`].
235    pub vertex_indices: [u32; 3],
236    /// Surface material ID.
237    pub material_id: u32,
238    /// Face normal vector.
239    pub normal: BwmVec3,
240    /// Plane distance coefficient.
241    pub planar_distance: f32,
242}
243
244/// One AABB-node row.
245#[derive(Debug, Clone, PartialEq)]
246pub struct BwmAabbNode {
247    /// Bounding-box minimum.
248    pub bb_min: BwmVec3,
249    /// Bounding-box maximum.
250    pub bb_max: BwmVec3,
251    /// Face index or `0xFFFF_FFFF` when this node is interior-only.
252    pub face_index: u32,
253    /// Unknown node field (commonly `4`).
254    pub unknown: u32,
255    /// Split-plane axis/value identifier.
256    pub split_axis: u32,
257    /// Left-child node index or `0xFFFF_FFFF`.
258    pub left_child: u32,
259    /// Right-child node index or `0xFFFF_FFFF`.
260    pub right_child: u32,
261}
262
263/// One adjacency row.
264#[derive(Debug, Clone, PartialEq, Eq)]
265pub struct BwmAdjacency {
266    /// Edge references (`face_index * 3 + edge_index`), or `-1` for none.
267    pub edge_refs: [i32; 3],
268}
269
270/// One edge-transition row.
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct BwmEdge {
273    /// Edge index (`face_index * 3 + edge_index`).
274    pub edge_index: i32,
275    /// Transition index or `-1` when absent.
276    pub transition: i32,
277}
278
279/// In-memory binary BWM container.
280#[derive(Debug, Clone, PartialEq)]
281pub struct Bwm {
282    /// Walkmesh type code.
283    pub walkmesh_type: BwmTypeCode,
284    /// Relative hook position #1.
285    pub relative_hook1: BwmVec3,
286    /// Relative hook position #2.
287    pub relative_hook2: BwmVec3,
288    /// Absolute hook position #1.
289    pub absolute_hook1: BwmVec3,
290    /// Absolute hook position #2.
291    pub absolute_hook2: BwmVec3,
292    /// Walkmesh position.
293    pub position: BwmVec3,
294    /// Unknown header field at offset `0x6C`.
295    pub unknown: u32,
296    /// Vertex array.
297    pub vertices: Vec<BwmVec3>,
298    /// Face rows.
299    pub faces: Vec<BwmFace>,
300    /// AABB nodes.
301    pub aabb_nodes: Vec<BwmAabbNode>,
302    /// Adjacency rows.
303    pub adjacencies: Vec<BwmAdjacency>,
304    /// Edge rows.
305    pub edges: Vec<BwmEdge>,
306    /// Perimeter entries (1-based edge-table indexes in canonical files).
307    pub perimeters: Vec<u32>,
308}
309
310impl Default for Bwm {
311    fn default() -> Self {
312        Self {
313            walkmesh_type: BwmTypeCode::from(BwmType::AreaModel),
314            relative_hook1: BwmVec3::new(0.0, 0.0, 0.0),
315            relative_hook2: BwmVec3::new(0.0, 0.0, 0.0),
316            absolute_hook1: BwmVec3::new(0.0, 0.0, 0.0),
317            absolute_hook2: BwmVec3::new(0.0, 0.0, 0.0),
318            position: BwmVec3::new(0.0, 0.0, 0.0),
319            unknown: 0,
320            vertices: Vec::new(),
321            faces: Vec::new(),
322            aabb_nodes: Vec::new(),
323            adjacencies: Vec::new(),
324            edges: Vec::new(),
325            perimeters: Vec::new(),
326        }
327    }
328}
329
330impl Bwm {
331    /// Creates an empty walkmesh container.
332    pub fn new() -> Self {
333        Self::default()
334    }
335}
336
337impl DecodeBinary for Bwm {
338    type Error = BwmBinaryError;
339
340    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
341        read_bwm_from_bytes(bytes)
342    }
343}
344
345impl EncodeBinary for Bwm {
346    type Error = BwmBinaryError;
347
348    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
349        write_bwm_to_vec(self)
350    }
351}
352
353/// Errors produced while parsing or serializing BWM binary data.
354#[derive(Debug, Error)]
355pub enum BwmBinaryError {
356    /// I/O read/write failure.
357    #[error(transparent)]
358    Io(#[from] std::io::Error),
359    /// Header signature is not `BWM `.
360    #[error("invalid BWM magic: {0:?}")]
361    InvalidMagic([u8; 4]),
362    /// Header version is unsupported.
363    #[error("invalid BWM version: {0:?}")]
364    InvalidVersion([u8; 4]),
365    /// Header/body layout is invalid or truncated.
366    #[error("invalid BWM header: {0}")]
367    InvalidHeader(String),
368    /// Walkmesh content is structurally invalid.
369    #[error("invalid BWM data: {0}")]
370    InvalidData(String),
371    /// Value cannot fit on-disk integer width.
372    #[error("value overflow while writing field `{0}`")]
373    ValueOverflow(&'static str),
374}
375
376impl From<binary::BinaryLayoutError> for BwmBinaryError {
377    fn from(error: binary::BinaryLayoutError) -> Self {
378        Self::InvalidHeader(error.to_string())
379    }
380}
381
382pub(super) fn checked_mul(
383    lhs: usize,
384    rhs: usize,
385    field: &'static str,
386) -> Result<usize, BwmBinaryError> {
387    lhs.checked_mul(rhs)
388        .ok_or(BwmBinaryError::ValueOverflow(field))
389}
390
391pub(super) fn checked_add(
392    lhs: usize,
393    rhs: usize,
394    field: &'static str,
395) -> Result<usize, BwmBinaryError> {
396    lhs.checked_add(rhs)
397        .ok_or(BwmBinaryError::ValueOverflow(field))
398}
399
400pub(super) fn usize_to_u32(value: usize, field: &'static str) -> Result<u32, BwmBinaryError> {
401    u32::try_from(value).map_err(|_| BwmBinaryError::ValueOverflow(field))
402}