use winnow::{Parser, ascii};
use crate::{Aabb, Obb, ParseError, Vec3};
use super::raw::{
CategoryRecord, HouseRecord, LevelRecord, Mp3dRecord, ObjectRecord, RegionRecord, SegmentRecord,
};
pub fn parse_record(line: &str) -> Result<Mp3dRecord, ParseError> {
let tokens: Vec<&str> = line.split_whitespace().collect();
let tag = token(&tokens, 0)?.chars().next().ok_or_else(|| {
ParseError::BadLine("expected a record tag in the first field".to_string())
})?;
match tag {
'H' => parse_house(&tokens),
'L' => parse_level(&tokens),
'R' => parse_region(&tokens),
'C' => parse_category(&tokens),
'O' => parse_object(&tokens),
'E' => parse_segment(&tokens),
_ => Ok(Mp3dRecord::Ignored),
}
}
fn parse_house(tokens: &[&str]) -> Result<Mp3dRecord, ParseError> {
Ok(Mp3dRecord::House(HouseRecord {
name: token(tokens, 1)?.to_string(),
label: token(tokens, 2)?.to_string(),
images: parse_usize(tokens, 3)?,
panoramas: parse_usize(tokens, 4)?,
vertices: parse_usize(tokens, 5)?,
surfaces: parse_usize(tokens, 6)?,
segments: parse_usize(tokens, 7)?,
objects: parse_usize(tokens, 8)?,
categories: parse_usize(tokens, 9)?,
regions: parse_usize(tokens, 10)?,
portals: parse_usize(tokens, 11)?,
levels: parse_usize(tokens, 12)?,
aabb: parse_aabb(tokens, 18)?,
}))
}
fn parse_level(tokens: &[&str]) -> Result<Mp3dRecord, ParseError> {
Ok(Mp3dRecord::Level(LevelRecord {
index: parse_i32(tokens, 1)?,
label: token(tokens, 3)?.to_string(),
position: parse_vec3(tokens, 4)?,
aabb: parse_aabb(tokens, 7)?,
}))
}
fn parse_region(tokens: &[&str]) -> Result<Mp3dRecord, ParseError> {
let category_code = token(tokens, 5)?
.chars()
.next()
.ok_or_else(|| ParseError::BadField {
field: 5,
value: String::new(),
})?;
Ok(Mp3dRecord::Region(RegionRecord {
index: parse_i32(tokens, 1)?,
level_index: parse_i32(tokens, 2)?,
category_code,
position: parse_vec3(tokens, 6)?,
aabb: parse_aabb(tokens, 9)?,
}))
}
fn parse_category(tokens: &[&str]) -> Result<Mp3dRecord, ParseError> {
Ok(Mp3dRecord::Category(CategoryRecord {
index: parse_i32(tokens, 1)?,
raw_index: parse_i32(tokens, 2)?,
raw_name: token(tokens, 3)?.replace('#', " "),
mpcat40_index: parse_i32(tokens, 4)?,
mpcat40_name: token(tokens, 5)?.to_string(),
}))
}
fn parse_object(tokens: &[&str]) -> Result<Mp3dRecord, ParseError> {
Ok(Mp3dRecord::Object(ObjectRecord {
index: parse_i32(tokens, 1)?,
region_index: parse_i32(tokens, 2)?,
category_index: parse_i32(tokens, 3)?,
obb: parse_obb(tokens, 4)?,
}))
}
fn parse_segment(tokens: &[&str]) -> Result<Mp3dRecord, ParseError> {
Ok(Mp3dRecord::Segment(SegmentRecord {
object_index: parse_i32(tokens, 2)?,
segment_id: parse_i32(tokens, 3)?,
}))
}
fn token<'a>(tokens: &'a [&str], field: usize) -> Result<&'a str, ParseError> {
tokens
.get(field)
.copied()
.ok_or(ParseError::MissingField { field })
}
fn parse_i32(tokens: &[&str], field: usize) -> Result<i32, ParseError> {
let value = token(tokens, field)?;
let mut input = value;
let parsed = ascii::dec_int::<_, i32, winnow::error::ContextError>.parse_next(&mut input);
match parsed {
Ok(parsed) if input.is_empty() => Ok(parsed),
_ => Err(ParseError::BadField {
field,
value: value.to_string(),
}),
}
}
fn parse_usize(tokens: &[&str], field: usize) -> Result<usize, ParseError> {
let parsed = parse_i32(tokens, field)?;
usize::try_from(parsed).map_err(|_| ParseError::BadField {
field,
value: parsed.to_string(),
})
}
fn parse_f32(tokens: &[&str], field: usize) -> Result<f32, ParseError> {
let value = token(tokens, field)?;
let mut input = value;
let parsed = ascii::float::<_, f32, winnow::error::ContextError>.parse_next(&mut input);
match parsed {
Ok(parsed) if input.is_empty() => Ok(parsed),
_ => Err(ParseError::BadField {
field,
value: value.to_string(),
}),
}
}
fn parse_vec3(tokens: &[&str], offset: usize) -> Result<Vec3, ParseError> {
Ok(Vec3(
parse_f32(tokens, offset)?,
parse_f32(tokens, offset + 1)?,
parse_f32(tokens, offset + 2)?,
))
}
fn parse_aabb(tokens: &[&str], offset: usize) -> Result<Aabb, ParseError> {
Ok(Aabb {
min: parse_vec3(tokens, offset)?,
max: parse_vec3(tokens, offset + 3)?,
})
}
fn parse_obb(tokens: &[&str], offset: usize) -> Result<Obb, ParseError> {
let center = parse_vec3(tokens, offset)?;
let axis0 = parse_vec3(tokens, offset + 3)?;
let axis1 = parse_vec3(tokens, offset + 6)?;
let axis2 = axis0.cross(axis1);
let half_extents = parse_vec3(tokens, offset + 9)?;
Ok(Obb {
center,
axis0,
axis1,
axis2,
half_extents,
})
}
#[cfg(test)]
mod tests {
use crate::{Vec3, mp3d::raw::Mp3dRecord};
use super::parse_record;
#[test]
fn parses_mp3d_records() {
let house =
parse_record("H test label 1 2 3 4 5 6 7 8 9 10 0 0 0 0 0 -1 -2 -3 4 5 6 0 0 0 0 0")
.unwrap();
let Mp3dRecord::House(house) = house else {
panic!("expected house record");
};
assert_eq!(house.name, "test");
assert_eq!(house.levels, 10);
assert_eq!(house.aabb.min, Vec3(-1.0, -2.0, -3.0));
assert_eq!(house.aabb.max, Vec3(4.0, 5.0, 6.0));
let level = parse_record("L 2 1 lvl 1 2 3 0 0 0 10 20 30 0 0 0 0 0").unwrap();
let Mp3dRecord::Level(level) = level else {
panic!("expected level record");
};
assert_eq!(level.index, 2);
assert_eq!(level.label, "lvl");
assert_eq!(level.position, Vec3(1.0, 2.0, 3.0));
let region = parse_record("R 3 2 0 0 k 4 5 6 1 2 3 7 8 9 0 0 0 0 0").unwrap();
let Mp3dRecord::Region(region) = region else {
panic!("expected region record");
};
assert_eq!(region.index, 3);
assert_eq!(region.level_index, 2);
assert_eq!(region.category_code, 'k');
let category = parse_record("C 4 40 raw#chair 5 chair 0 0 0 0 0").unwrap();
let Mp3dRecord::Category(category) = category else {
panic!("expected category record");
};
assert_eq!(category.index, 4);
assert_eq!(category.raw_name, "raw chair");
assert_eq!(category.mpcat40_name, "chair");
let object = parse_record("O 5 3 4 1 2 3 1 0 0 0 1 0 2 3 4 0 0 0 0 0 0 0 0").unwrap();
let Mp3dRecord::Object(object) = object else {
panic!("expected object record");
};
assert_eq!(object.index, 5);
assert_eq!(object.region_index, 3);
assert_eq!(object.category_index, 4);
assert_eq!(object.obb.axis2, Vec3(0.0, 0.0, 1.0));
assert_eq!(object.obb.half_extents, Vec3(2.0, 3.0, 4.0));
let segment = parse_record("E 0 5 123 1.0 0 0 0 0 0 0 1 1 1 0 0 0 0 0").unwrap();
let Mp3dRecord::Segment(segment) = segment else {
panic!("expected segment record");
};
assert_eq!(segment.object_index, 5);
assert_eq!(segment.segment_id, 123);
assert!(matches!(
parse_record("P ignored").unwrap(),
Mp3dRecord::Ignored
));
}
}