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