allow parsing just the nav quads

This commit is contained in:
Robin Appelman 2020-05-02 14:48:17 +02:00
commit 99e24bc9b8
5 changed files with 239 additions and 96 deletions

View file

@ -8,11 +8,11 @@ This library is currently focused on getting the z-height from an x/y coordinate
that usage. For other usages the raw navigation areas are exposed. that usage. For other usages the raw navigation areas are exposed.
```rust ```rust
use sourcenav::get_area_tree; use sourcenav::get_quad_tree;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let file = std::fs::read("data/pl_badwater.nav")?; let file = std::fs::read("data/pl_badwater.nav")?;
let tree = get_area_tree(file)?; let tree = get_quad_tree(file)?;
assert_eq!(220.83125, tree.find_best_height(320.0, -1030.0, 0.0)); assert_eq!(220.83125, tree.find_best_height(320.0, -1030.0, 0.0));

View file

@ -5,16 +5,26 @@ extern crate test;
use std::fs; use std::fs;
use bitbuffer::{BitReadBuffer, BitReadStream, LittleEndian}; use bitbuffer::{BitReadBuffer, BitReadStream, LittleEndian};
use sourcenav::get_area_tree; use sourcenav::{get_quad_tree, read_areas};
use std::fs::read; use std::fs::read;
use test::Bencher; use test::Bencher;
#[bench] #[bench]
fn bench_badwater(b: &mut Bencher) { fn bench_badwater_areas(b: &mut Bencher) {
let file = read("data/pl_badwater.nav").unwrap(); let file = read("data/pl_badwater.nav").unwrap();
let data = BitReadStream::new(BitReadBuffer::new(file, LittleEndian)); let data = BitReadStream::new(BitReadBuffer::new(file, LittleEndian));
b.iter(|| { b.iter(|| {
test::black_box(get_area_tree(data.clone())); test::black_box(read_areas(data.clone()));
})
}
#[bench]
fn bench_badwater_quads(b: &mut Bencher) {
let file = read("data/pl_badwater.nav").unwrap();
let data = BitReadStream::new(BitReadBuffer::new(file, LittleEndian));
b.iter(|| {
test::black_box(get_quad_tree(data.clone()));
}) })
} }

View file

@ -1,10 +1,10 @@
use crate::navmesh::HammerUnit; use crate::navmesh::HammerUnit;
pub use crate::navmesh::{ pub use crate::navmesh::{
ApproachArea, Connections, EncounterPath, LadderConnections, LadderDirection, LightIntensity, ApproachArea, Connections, EncounterPath, LadderConnections, LadderDirection, LightIntensity,
NavDirection, NavHidingSpot, Vector3, VisibleArea, NavDirection, NavHidingSpot, NavQuad, Vector3, VisibleArea,
}; };
use crate::parser::read_areas; use crate::parser::read_quads;
pub use crate::parser::{NavArea, ParseError}; pub use crate::parser::{read_areas, NavArea, ParseError};
use aabb_quadtree::{ItemId, QuadTree}; use aabb_quadtree::{ItemId, QuadTree};
use bitbuffer::{BitReadStream, LittleEndian}; use bitbuffer::{BitReadStream, LittleEndian};
use euclid::{TypedPoint2D, TypedRect, TypedSize2D}; use euclid::{TypedPoint2D, TypedRect, TypedSize2D};
@ -15,23 +15,25 @@ mod parser;
type Rect = TypedRect<f32, HammerUnit>; type Rect = TypedRect<f32, HammerUnit>;
/// A tree of all navigation areas /// A tree of all navigation areas
pub struct NavTree(QuadTree<NavArea, HammerUnit, [(ItemId, Rect); 4]>); pub struct NavQuadTree(QuadTree<NavQuad, HammerUnit, [(ItemId, Rect); 4]>);
/// Parse all navigation areas from a nav file /// Parse all navigation quads from a nav file
/// ///
/// ## Examples /// ## Examples
/// ///
/// ```no_run /// ```no_run
/// use sourcenav::get_area_tree; /// use sourcenav::get_quad_tree;
/// ///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let file = std::fs::read("path/to/navfile.nav")?; /// let file = std::fs::read("path/to/navfile.nav")?;
/// let tree = get_area_tree(file)?; /// let tree = get_quad_tree(file)?;
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn get_area_tree(data: impl Into<BitReadStream<LittleEndian>>) -> Result<NavTree, ParseError> { pub fn get_quad_tree(
let areas = read_areas(data.into())?; data: impl Into<BitReadStream<LittleEndian>>,
) -> Result<NavQuadTree, ParseError> {
let areas = read_quads(data.into())?;
let (min_x, min_y, max_x, max_y) = areas.iter().fold( let (min_x, min_y, max_x, max_y) = areas.iter().fold(
(f32::MAX, f32::MAX, f32::MIN, f32::MIN), (f32::MAX, f32::MAX, f32::MIN, f32::MIN),
@ -57,25 +59,25 @@ pub fn get_area_tree(data: impl Into<BitReadStream<LittleEndian>>) -> Result<Nav
tree.insert(area); tree.insert(area);
} }
Ok(NavTree(tree)) Ok(NavQuadTree(tree))
} }
impl NavTree { impl NavQuadTree {
/// Find the navigation areas at a x/y cooordinate /// Find the navigation areas at a x/y cooordinate
/// ///
/// ## Examples /// ## Examples
/// ///
/// ```no_run /// ```no_run
/// use sourcenav::get_area_tree; /// use sourcenav::get_quad_tree;
/// ///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let file = std::fs::read("path/to/navfile.nav")?; /// let file = std::fs::read("path/to/navfile.nav")?;
/// let tree = get_area_tree(file)?; /// let tree = get_quad_tree(file)?;
/// let areas = tree.query(150.0, -312.0); /// let areas = tree.query(150.0, -312.0);
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn query(&self, x: f32, y: f32) -> impl Iterator<Item = &NavArea> { pub fn query(&self, x: f32, y: f32) -> impl Iterator<Item = &NavQuad> {
let query_box = Rect::new(TypedPoint2D::new(x, y), TypedSize2D::new(1.0, 1.0)); let query_box = Rect::new(TypedPoint2D::new(x, y), TypedSize2D::new(1.0, 1.0));
self.0.query(query_box).into_iter().map(|(area, ..)| area) self.0.query(query_box).into_iter().map(|(area, ..)| area)
@ -88,11 +90,11 @@ impl NavTree {
/// ## Examples /// ## Examples
/// ///
/// ```no_run /// ```no_run
/// use sourcenav::get_area_tree; /// use sourcenav::get_quad_tree;
/// ///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let file = std::fs::read("path/to/navfile.nav")?; /// let file = std::fs::read("path/to/navfile.nav")?;
/// let tree = get_area_tree(file)?; /// let tree = get_quad_tree(file)?;
/// let heights = tree.find_z_height(150.0, -312.0); /// let heights = tree.find_z_height(150.0, -312.0);
/// # Ok(()) /// # Ok(())
/// # } /// # }
@ -122,18 +124,18 @@ impl NavTree {
/// ## Examples /// ## Examples
/// ///
/// ```no_run /// ```no_run
/// use sourcenav::get_area_tree; /// use sourcenav::get_quad_tree;
/// ///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let file = std::fs::read("path/to/navfile.nav")?; /// let file = std::fs::read("path/to/navfile.nav")?;
/// let tree = get_area_tree(file)?; /// let tree = get_quad_tree(file)?;
/// for area in tree.areas() { /// for quad in tree.quads() {
/// println!("area: {}", area.id) /// println!("area: {:?}", quad)
/// } /// }
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn areas(&self) -> impl Iterator<Item = &NavArea> { pub fn quads(&self) -> impl Iterator<Item = &NavQuad> {
self.0.iter().map(|(_, (area, _))| area) self.0.iter().map(|(_, (area, _))| area)
} }
} }
@ -141,7 +143,7 @@ impl NavTree {
#[test] #[test]
fn test_tree() { fn test_tree() {
let file = std::fs::read("data/pl_badwater.nav").unwrap(); let file = std::fs::read("data/pl_badwater.nav").unwrap();
let tree = get_area_tree(file).unwrap(); let tree = get_quad_tree(file).unwrap();
// single flat plane // single flat plane
let point1 = (1600.0, -1300.0); let point1 = (1600.0, -1300.0);
@ -174,11 +176,6 @@ fn test_tree() {
vec![147.23126], vec![147.23126],
tree.find_z_height(point4.0, point4.1).collect::<Vec<f32>>() tree.find_z_height(point4.0, point4.1).collect::<Vec<f32>>()
); );
assert_eq!(
tree.query(point3.0, point3.1).next().map(|area| area.id),
tree.query(point4.0, point4.1).next().map(|area| area.id)
);
} }
#[cfg(doctest)] #[cfg(doctest)]

View file

@ -24,10 +24,7 @@ impl fmt::Display for NavAreaId {
#[derive(Debug)] #[derive(Debug)]
pub struct NavArea { pub struct NavArea {
pub id: NavAreaId, pub id: NavAreaId,
pub north_west: Vector3, pub quad: NavQuad,
pub south_east: Vector3,
pub north_east_z: f32,
pub south_west_z: f32,
pub flags: u32, pub flags: u32,
pub connections: Connections, pub connections: Connections,
pub hiding_spots: Vec<NavHidingSpot>, pub hiding_spots: Vec<NavHidingSpot>,
@ -42,60 +39,8 @@ pub struct NavArea {
pub inherit_visibility_from_area_id: u32, pub inherit_visibility_from_area_id: u32,
} }
impl NavArea {
pub fn width(&self) -> f32 {
self.south_east.0 - self.north_west.0
}
pub fn height(&self) -> f32 {
self.south_east.1 - self.north_west.1
}
/// Get the z height of a x/y point inside the navigation area
///
/// # Examples
///
/// ```no_run
/// use sourcenav::get_area_tree;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let file = std::fs::read("path/to/navfile.nav")?;
/// let tree = get_area_tree(file)?;
/// let area = tree.query(150.0, -312.0).next().unwrap();
///
/// let height = area.get_z_height(150.0, -312.0);
/// # Ok(())
/// # }
/// ```
pub fn get_z_height(&self, x: f32, y: f32) -> f32 {
let from_east = self.south_east.0 - x;
let from_south = self.south_east.0 - y;
let north_slope = (self.north_west.2 - self.north_east_z) / self.width();
let south_slope = (self.south_west_z - self.south_east.2) / self.width();
let north_z = self.north_east_z + north_slope * from_east;
let south_z = self.south_east.2 + south_slope * from_east;
let final_slope = (north_z - south_z) / self.height();
south_z + final_slope * from_south
}
}
pub(crate) struct HammerUnit; pub(crate) struct HammerUnit;
impl Spatial<HammerUnit> for NavArea {
fn aabb(&self) -> Rect {
Rect {
origin: TypedPoint2D::new(self.north_west.0, self.north_west.1),
size: TypedSize2D::new(
self.south_east.0 - self.north_west.0,
self.south_east.1 - self.north_west.1,
),
}
}
}
/// The connections from a navigation area into it's neighbours /// The connections from a navigation area into it's neighbours
/// ///
/// Contains a list of area id's for every [`NavDirection`] /// Contains a list of area id's for every [`NavDirection`]
@ -131,6 +76,14 @@ impl<E: Endianness> BitRead<E> for Connections {
Ok(Connections(connections)) Ok(Connections(connections))
} }
fn skip(stream: &mut BitReadStream<E>) -> Result<(), ReadError> {
for _ in 0..4 {
let connection_count: u32 = stream.read()?;
stream.skip_bits(connection_count as usize * 32)?;
}
Ok(())
}
} }
impl Index<NavDirection> for Connections { impl Index<NavDirection> for Connections {
@ -176,6 +129,14 @@ impl<E: Endianness> BitRead<E> for LadderConnections {
Ok(LadderConnections(connections)) Ok(LadderConnections(connections))
} }
fn skip(stream: &mut BitReadStream<E>) -> Result<(), ReadError> {
for _ in 0..2 {
let connection_count: u32 = stream.read()?;
stream.skip_bits(connection_count as usize * 32)?;
}
Ok(())
}
} }
impl Index<LadderDirection> for LadderConnections { impl Index<LadderDirection> for LadderConnections {
@ -263,5 +224,63 @@ pub struct NavPlace {
name: String, name: String,
} }
/// A navigation area from the nav file
#[derive(Debug)] #[derive(Debug)]
pub struct NavMesh {} pub struct NavQuad {
pub north_west: Vector3,
pub south_east: Vector3,
pub north_east_z: f32,
pub south_west_z: f32,
}
impl NavQuad {
pub fn width(&self) -> f32 {
self.south_east.0 - self.north_west.0
}
pub fn height(&self) -> f32 {
self.south_east.1 - self.north_west.1
}
/// Get the z height of a x/y point inside the navigation area
///
/// # Examples
///
/// ```no_run
/// use sourcenav::get_quad_tree;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let file = std::fs::read("path/to/navfile.nav")?;
/// let tree = get_quad_tree(file)?;
/// let area = tree.query(150.0, -312.0).next().unwrap();
///
/// let height = area.get_z_height(150.0, -312.0);
/// # Ok(())
/// # }
/// ```
pub fn get_z_height(&self, x: f32, y: f32) -> f32 {
let from_east = self.south_east.0 - x;
let from_south = self.south_east.0 - y;
let north_slope = (self.north_west.2 - self.north_east_z) / self.width();
let south_slope = (self.south_west_z - self.south_east.2) / self.width();
let north_z = self.north_east_z + north_slope * from_east;
let south_z = self.south_east.2 + south_slope * from_east;
let final_slope = (north_z - south_z) / self.height();
south_z + final_slope * from_south
}
}
impl Spatial<HammerUnit> for NavQuad {
fn aabb(&self) -> Rect {
Rect {
origin: TypedPoint2D::new(self.north_west.0, self.north_west.1),
size: TypedSize2D::new(
self.south_east.0 - self.north_west.0,
self.south_east.1 - self.north_west.1,
),
}
}
}

View file

@ -1,5 +1,7 @@
pub use crate::navmesh::NavArea; pub use crate::navmesh::NavArea;
use bitbuffer::{BitReadStream, LittleEndian}; use crate::navmesh::NavQuad;
use crate::{Connections, EncounterPath, LadderConnections, NavHidingSpot, VisibleArea};
use bitbuffer::{BitRead, BitReadStream, LittleEndian};
use err_derive::Error; use err_derive::Error;
/// Errors that can occur when parsing the binary nav file /// Errors that can occur when parsing the binary nav file
@ -19,9 +21,23 @@ pub enum ParseError {
UnsupportedVersion(u32), UnsupportedVersion(u32),
} }
pub(crate) fn read_areas( /// Parse all navigation areas from a nav file
mut data: BitReadStream<LittleEndian>, ///
/// ## Examples
///
/// ```no_run
/// use sourcenav::read_areas;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let file = std::fs::read("path/to/navfile.nav")?;
/// let tree = read_areas(file)?;
/// # Ok(())
/// # }
/// ```
pub fn read_areas(
data: impl Into<BitReadStream<LittleEndian>>,
) -> Result<Vec<NavArea>, ParseError> { ) -> Result<Vec<NavArea>, ParseError> {
let mut data = data.into();
let magic = data.read()?; let magic = data.read()?;
if magic != 0xFEED_FACE { if magic != 0xFEED_FACE {
return Err(ParseError::InvalidMagicNumber(magic)); return Err(ParseError::InvalidMagicNumber(magic));
@ -120,10 +136,12 @@ pub(crate) fn read_areas(
areas.push(NavArea { areas.push(NavArea {
id, id,
quad: NavQuad {
north_west, north_west,
south_east, south_east,
north_east_z, north_east_z,
south_west_z, south_west_z,
},
flags, flags,
connections, connections,
hiding_spots, hiding_spots,
@ -144,6 +162,97 @@ pub(crate) fn read_areas(
Ok(areas) Ok(areas)
} }
pub(crate) fn read_quads(
mut data: BitReadStream<LittleEndian>,
) -> Result<Vec<NavQuad>, ParseError> {
let magic = data.read()?;
if magic != 0xFEED_FACE {
return Err(ParseError::InvalidMagicNumber(magic));
}
let major_version: u32 = data.read()?;
if major_version != 16 {
return Err(ParseError::UnsupportedVersion(major_version));
}
let _minor_version: u32 = if major_version >= 10 { data.read()? } else { 0 };
let _size: u32 = data.read()?;
let _is_analysed = if major_version >= 14 {
data.read_int::<u8>(8)? == 1
} else {
false
};
let place_count: u16 = data.read()?;
// let places = Vec::with_capacity(place_count as usize);
for _id in 1..=place_count {
let name_length: u16 = data.read()?;
let _name = data.read_string(Some(name_length as usize))?;
// TODO
}
let _has_unnamed_areas = if major_version >= 12 {
data.read_int::<u8>(8)? == 1
} else {
false
};
let area_count: u32 = data.read()?;
let mut areas = Vec::with_capacity(area_count as usize);
for _ in 0..area_count {
data.skip_bits(32 * 2)?; // id and flags
let north_west = data.read()?;
let south_east = data.read()?;
let north_east_z = data.read()?;
let south_west_z = data.read()?;
Connections::skip(&mut data)?;
let hiding_spots_count: u8 = data.read()?;
data.skip_bits(
<NavHidingSpot as BitRead<LittleEndian>>::bit_size().unwrap()
* hiding_spots_count as usize,
)?;
let encounter_paths_count: u32 = data.read()?;
for _ in 0..encounter_paths_count {
EncounterPath::skip(&mut data)?;
}
data.skip_bits(16)?; // place
LadderConnections::skip(&mut data)?;
data.skip_bits((32 * 2) + (32 * 4))?; // occupy time, light intensity
let visible_areas_count: u32 = data.read()?;
data.skip_bits(
<VisibleArea as BitRead<LittleEndian>>::bit_size().unwrap()
* visible_areas_count as usize,
)?;
data.skip_bits(32 * 2)?; // inherit visible, garbage
areas.push(NavQuad {
north_west,
south_east,
north_east_z,
south_west_z,
});
}
debug_assert!(data.bits_left() <= 32);
Ok(areas)
}
#[test] #[test]
fn test() { fn test() {
let file = std::fs::read("data/pl_badwater.nav").unwrap(); let file = std::fs::read("data/pl_badwater.nav").unwrap();
@ -151,3 +260,11 @@ fn test() {
let areas = read_areas(data).unwrap(); let areas = read_areas(data).unwrap();
assert_eq!(1930, areas.len()); assert_eq!(1930, areas.len());
} }
#[test]
fn test_quads() {
let file = std::fs::read("data/pl_badwater.nav").unwrap();
let data = BitReadStream::new(bitbuffer::BitReadBuffer::new(file, LittleEndian));
let quads = read_quads(data).unwrap();
assert_eq!(1930, quads.len());
}