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