Skip to main content

rakata_formats/bwm/
ascii_reader.rs

1//! BWM ASCII reader implementation.
2//!
3//! This module provides a line-based parser for the KotOR ASCII walkmesh format,
4//! mirroring the logic of the game's `LoadMeshText` function.
5
6use std::io::{BufRead, Cursor, Read};
7use std::str::FromStr;
8
9use super::{Bwm, BwmAabbNode, BwmFace, BwmType, BwmTypeCode, BwmVec3, SurfaceMaterial};
10
11/// Errors specific to ASCII BWM parsing.
12#[derive(Debug, thiserror::Error)]
13pub enum BwmAsciiError {
14    /// I/O error.
15    #[error(transparent)]
16    Io(#[from] std::io::Error),
17    /// Parse error (float/int conversion).
18    #[error("parse error: {0}")]
19    Parse(String),
20    /// Structure error (missing keywords, invalid counts).
21    #[error("structure error: {0}")]
22    Structure(String),
23    /// Invalid data (indices out of bounds, etc).
24    #[error("invalid data: {0}")]
25    InvalidData(String),
26}
27
28/// Reads an ASCII BWM from a reader.
29///
30/// # Errors
31///
32/// [`BwmAsciiError::Io`] when the reader fails, [`BwmAsciiError::Parse`] for a
33/// number that will not parse, and [`BwmAsciiError::Structure`] when the
34/// blocks the file declares do not describe a walkmesh.
35pub fn read_bwm_ascii<R: Read>(reader: &mut R) -> Result<Bwm, BwmAsciiError> {
36    let mut buffer = Vec::new();
37    reader.read_to_end(&mut buffer)?;
38    let cursor = Cursor::new(buffer);
39
40    // Use a Peekable iterator to handle nested parsing (e.g. verts/faces counts)
41    let mut lines = cursor
42        .lines()
43        .map(|l| l.map_err(BwmAsciiError::Io))
44        .peekable();
45
46    let mut bwm = Bwm::new();
47    bwm.walkmesh_type = BwmTypeCode::from(BwmType::PlaceableOrDoor); // Default
48
49    let mut in_aabb_node = false;
50    let mut vertices = Vec::new();
51    let mut faces_raw = Vec::new(); // (v1, v2, v3, adj1, adj2, adj3, adj4, mat)
52    let mut aabb_nodes = Vec::new();
53
54    // Loop through lines
55    while let Some(line_res) = lines.next() {
56        let line = line_res?;
57        let trimmed = line.trim_start();
58
59        if trimmed.is_empty() {
60            continue;
61        }
62
63        // Block Structure
64
65        if trimmed.starts_with("node") {
66            if trimmed.contains("aabb") {
67                in_aabb_node = true;
68            }
69            continue;
70        }
71
72        if trimmed.starts_with("endnode") {
73            in_aabb_node = false;
74            continue;
75        }
76
77        if !in_aabb_node {
78            continue;
79        }
80
81        // Fields
82
83        if trimmed.starts_with("position") {
84            let parts: Vec<&str> = trimmed.split_whitespace().collect();
85            if parts.len() >= 4 {
86                bwm.position = parse_vec3(&parts[1..4])?;
87            }
88        } else if trimmed.starts_with("orientation") {
89            // Orientation is parsed for compatibility but not stored in the binary BWM model.
90        } else if trimmed.starts_with("verts") {
91            let parts: Vec<&str> = trimmed.split_whitespace().collect();
92            if parts.len() >= 2 {
93                let count: usize = parts[1]
94                    .parse()
95                    .map_err(|e| BwmAsciiError::Parse(format!("vertex count: {}", e)))?;
96                for _ in 0..count {
97                    if let Some(v_line_res) = lines.next() {
98                        let v_line = v_line_res?;
99                        let v_parts: Vec<&str> = v_line.split_whitespace().collect();
100                        if v_parts.len() >= 3 {
101                            vertices.push(parse_vec3(&v_parts[0..3])?);
102                        }
103                    } else {
104                        return Err(BwmAsciiError::Structure("unexpected EOF in verts".into()));
105                    }
106                }
107            }
108        } else if trimmed.starts_with("faces") {
109            let parts: Vec<&str> = trimmed.split_whitespace().collect();
110            if parts.len() >= 2 {
111                let count: usize = parts[1]
112                    .parse()
113                    .map_err(|e| BwmAsciiError::Parse(format!("face count: {}", e)))?;
114                for _ in 0..count {
115                    if let Some(f_line_res) = lines.next() {
116                        let f_line = f_line_res?;
117                        let f_parts: Vec<&str> = f_line.split_whitespace().collect();
118                        if f_parts.len() >= 8 {
119                            let v1: u32 = f_parts[0]
120                                .parse()
121                                .map_err(|_| BwmAsciiError::Parse("face v1".into()))?;
122                            let v2: u32 = f_parts[1]
123                                .parse()
124                                .map_err(|_| BwmAsciiError::Parse("face v2".into()))?;
125                            let v3: u32 = f_parts[2]
126                                .parse()
127                                .map_err(|_| BwmAsciiError::Parse("face v3".into()))?;
128
129                            // Adjacency indices are skipped as they are not stored in BwmFace.
130                            let mat: u32 = f_parts[7]
131                                .parse()
132                                .map_err(|_| BwmAsciiError::Parse("face mat".into()))?;
133
134                            faces_raw.push((v1, v2, v3, mat));
135                        }
136                    } else {
137                        return Err(BwmAsciiError::Structure("unexpected EOF in faces".into()));
138                    }
139                }
140            }
141        } else if let Some(stripped) = trimmed.strip_prefix("aabb") {
142            // Check if the `aabb` keyword line also contains the first data entry.
143            let remainder = stripped.trim();
144            if !remainder.is_empty() {
145                if let Ok(node) = parse_aabb_node(remainder) {
146                    aabb_nodes.push(node);
147                }
148            }
149
150            // Consume subsequent lines until a keyword or the end of the block
151            loop {
152                let should_break = if let Some(Ok(next_line)) = lines.peek() {
153                    let next_trimmed = next_line.trim_start();
154                    next_trimmed.starts_with("node")
155                        || next_trimmed.starts_with("endnode")
156                        || next_trimmed.starts_with("position")
157                        || next_trimmed.starts_with("orientation")
158                        || next_trimmed.starts_with("verts")
159                        || next_trimmed.starts_with("faces")
160                } else {
161                    true // EOF or error
162                };
163
164                if should_break {
165                    break;
166                }
167
168                // Consume line
169                if let Some(line_res) = lines.next() {
170                    let line = line_res?;
171                    let trimmed = line.trim_start();
172                    if trimmed.is_empty() {
173                        continue;
174                    }
175
176                    if let Ok(node) = parse_aabb_node(trimmed) {
177                        aabb_nodes.push(node);
178                    }
179                }
180            }
181        }
182    }
183
184    // Post-Processing
185    bwm.vertices = vertices;
186
187    // Sort faces: Walkable first, Unwalkable second.
188    // The game engine requires walkable faces to appear first in the list.
189    // `adjacency_count` in the binary header corresponds to the number of walkable faces.
190    // Unwalkable faces follow and do not have adjacency table entries.
191    let mut walkable = Vec::new();
192    let mut unwalkable = Vec::new();
193
194    for (v1, v2, v3, mat_id) in faces_raw {
195        let (normal, planar_distance) = match (
196            bwm.vertices
197                .get(usize::try_from(v1).expect("vertex index fits in usize"))
198                .copied(),
199            bwm.vertices
200                .get(usize::try_from(v2).expect("vertex index fits in usize"))
201                .copied(),
202            bwm.vertices
203                .get(usize::try_from(v3).expect("vertex index fits in usize"))
204                .copied(),
205        ) {
206            (Some(a), Some(b), Some(c)) => face_normal_and_distance(a, b, c),
207            _ => (BwmVec3::new(0.0, 0.0, 1.0), 0.0),
208        };
209        let face = BwmFace {
210            vertex_indices: [v1, v2, v3],
211            material_id: mat_id,
212            normal,
213            planar_distance,
214        };
215
216        let is_walkable = SurfaceMaterial::try_from(mat_id)
217            .map(|m| m.is_walkable())
218            .unwrap_or(true); // Default to walkable if unknown ID
219
220        if is_walkable {
221            walkable.push(face);
222        } else {
223            unwalkable.push(face);
224        }
225    }
226
227    // Combine faces (walkable then unwalkable).
228    bwm.faces = walkable;
229    let walkable_count = bwm.faces.len();
230    bwm.faces.extend(unwalkable);
231
232    // Populate default adjacencies for walkable faces.
233    // ASCII input does not provide reliable adjacency data; populate with default (no neighbors) for now.
234    for _ in 0..walkable_count {
235        bwm.adjacencies.push(super::BwmAdjacency {
236            edge_refs: [-1, -1, -1],
237        });
238    }
239
240    // AABB Nodes
241    for (min, max, face_idx) in aabb_nodes {
242        bwm.aabb_nodes.push(BwmAabbNode {
243            bb_min: min,
244            bb_max: max,
245            face_index: u32::try_from(face_idx).expect("AABB face index is non-negative"),
246            unknown: 4,
247            split_axis: 0,
248            left_child: 0xFFFFFFFF,
249            right_child: 0xFFFFFFFF,
250        });
251    }
252
253    if !bwm.aabb_nodes.is_empty() {
254        bwm.walkmesh_type = BwmTypeCode::from(BwmType::AreaModel);
255    }
256
257    Ok(bwm)
258}
259
260/// Computes the unit normal and planar distance for a triangle, matching the
261/// values the binary BWM format stores for each face.
262fn face_normal_and_distance(a: BwmVec3, b: BwmVec3, c: BwmVec3) -> (BwmVec3, f32) {
263    let ab = BwmVec3::new(b.x - a.x, b.y - a.y, b.z - a.z);
264    let ac = BwmVec3::new(c.x - a.x, c.y - a.y, c.z - a.z);
265    let nx = ab.y * ac.z - ab.z * ac.y;
266    let ny = ab.z * ac.x - ab.x * ac.z;
267    let nz = ab.x * ac.y - ab.y * ac.x;
268    let len = (nx * nx + ny * ny + nz * nz).sqrt();
269    if len > 0.0 {
270        let n = BwmVec3::new(nx / len, ny / len, nz / len);
271        let d = n.x * a.x + n.y * a.y + n.z * a.z;
272        (n, d)
273    } else {
274        // Degenerate triangle: fall back to up-facing normal.
275        (BwmVec3::new(0.0, 0.0, 1.0), 0.0)
276    }
277}
278
279fn parse_vec3(parts: &[&str]) -> Result<BwmVec3, BwmAsciiError> {
280    let x = f32::from_str(parts[0]).map_err(|_| BwmAsciiError::Parse("x".into()))?;
281    let y = f32::from_str(parts[1]).map_err(|_| BwmAsciiError::Parse("y".into()))?;
282    let z = f32::from_str(parts[2]).map_err(|_| BwmAsciiError::Parse("z".into()))?;
283    Ok(BwmVec3::new(x, y, z))
284}
285
286fn parse_aabb_node(line: &str) -> Result<(BwmVec3, BwmVec3, i32), BwmAsciiError> {
287    let parts: Vec<&str> = line.split_whitespace().collect();
288    if parts.len() < 7 {
289        return Err(BwmAsciiError::Parse("aabb fields".into()));
290    }
291    let min = parse_vec3(&parts[0..3])?;
292    let max = parse_vec3(&parts[3..6])?;
293    let face: i32 = parts[6]
294        .parse()
295        .map_err(|_| BwmAsciiError::Parse("aabb face".into()))?;
296
297    // Apply 0.01 epsilon expansion to bounding box, mirroring game engine behavior.
298    let epsilon = 0.01;
299    let bb_min = BwmVec3::new(min.x - epsilon, min.y - epsilon, min.z - epsilon);
300    let bb_max = BwmVec3::new(max.x + epsilon, max.y + epsilon, max.z + epsilon);
301
302    Ok((bb_min, bb_max, face))
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use crate::bwm::ascii_writer::write_bwm_ascii;
309    use std::io::Cursor;
310
311    #[test]
312    fn test_roundtrip_ascii() {
313        let mut bwm = Bwm::new();
314        bwm.walkmesh_type = BwmTypeCode::from(BwmType::AreaModel);
315        bwm.vertices.push(BwmVec3::new(0.0, 0.0, 0.0));
316        bwm.vertices.push(BwmVec3::new(10.0, 0.0, 0.0));
317        bwm.vertices.push(BwmVec3::new(0.0, 10.0, 0.0));
318        bwm.faces.push(BwmFace {
319            vertex_indices: [0, 1, 2],
320            material_id: 1, // Dirt (Walkable)
321            normal: BwmVec3::new(0.0, 0.0, 1.0),
322            planar_distance: 0.0,
323        });
324
325        let mut buffer = Vec::new();
326        write_bwm_ascii(&mut buffer, &bwm).expect("write failed");
327
328        let text = String::from_utf8(buffer.clone()).expect("utf8");
329        println!("Generated ASCII:\n{}", text);
330
331        let mut cursor = Cursor::new(buffer);
332        let parsed = read_bwm_ascii(&mut cursor).expect("read failed");
333
334        assert_eq!(parsed.vertices.len(), 3);
335        assert_eq!(parsed.faces.len(), 1);
336        assert_eq!(parsed.faces[0].material_id, 1);
337
338        // Float comparison for vertices
339        assert!((parsed.vertices[1].x - 10.0).abs() < 0.001);
340    }
341}