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.
```rust
use sourcenav::get_area_tree;
use sourcenav::get_quad_tree;
fn main() -> Result<(), Box<dyn std::error::Error>> {
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));

View file

@ -5,16 +5,26 @@ extern crate test;
use std::fs;
use bitbuffer::{BitReadBuffer, BitReadStream, LittleEndian};
use sourcenav::get_area_tree;
use sourcenav::{get_quad_tree, read_areas};
use std::fs::read;
use test::Bencher;
#[bench]
fn bench_badwater(b: &mut Bencher) {
fn bench_badwater_areas(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_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;
pub use crate::navmesh::{
ApproachArea, Connections, EncounterPath, LadderConnections, LadderDirection, LightIntensity,
NavDirection, NavHidingSpot, Vector3, VisibleArea,
NavDirection, NavHidingSpot, NavQuad, Vector3, VisibleArea,
};
use crate::parser::read_areas;
pub use crate::parser::{NavArea, ParseError};
use crate::parser::read_quads;
pub use crate::parser::{read_areas, NavArea, ParseError};
use aabb_quadtree::{ItemId, QuadTree};
use bitbuffer::{BitReadStream, LittleEndian};
use euclid::{TypedPoint2D, TypedRect, TypedSize2D};
@ -15,23 +15,25 @@ mod parser;
type Rect = TypedRect<f32, HammerUnit>;
/// 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
///
/// ```no_run
/// use sourcenav::get_area_tree;
/// 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_area_tree(file)?;
/// let tree = get_quad_tree(file)?;
/// # Ok(())
/// # }
/// ```
pub fn get_area_tree(data: impl Into<BitReadStream<LittleEndian>>) -> Result<NavTree, ParseError> {
let areas = read_areas(data.into())?;
pub fn get_quad_tree(
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(
(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);
}
Ok(NavTree(tree))
Ok(NavQuadTree(tree))
}
impl NavTree {
impl NavQuadTree {
/// Find the navigation areas at a x/y cooordinate
///
/// ## Examples
///
/// ```no_run
/// use sourcenav::get_area_tree;
/// 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_area_tree(file)?;
/// let tree = get_quad_tree(file)?;
/// let areas = tree.query(150.0, -312.0);
/// # 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));
self.0.query(query_box).into_iter().map(|(area, ..)| area)
@ -88,11 +90,11 @@ impl NavTree {
/// ## Examples
///
/// ```no_run
/// use sourcenav::get_area_tree;
/// 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_area_tree(file)?;
/// let tree = get_quad_tree(file)?;
/// let heights = tree.find_z_height(150.0, -312.0);
/// # Ok(())
/// # }
@ -122,18 +124,18 @@ impl NavTree {
/// ## Examples
///
/// ```no_run
/// use sourcenav::get_area_tree;
/// 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_area_tree(file)?;
/// for area in tree.areas() {
/// println!("area: {}", area.id)
/// let tree = get_quad_tree(file)?;
/// for quad in tree.quads() {
/// println!("area: {:?}", quad)
/// }
/// # Ok(())
/// # }
/// ```
pub fn areas(&self) -> impl Iterator<Item = &NavArea> {
pub fn quads(&self) -> impl Iterator<Item = &NavQuad> {
self.0.iter().map(|(_, (area, _))| area)
}
}
@ -141,7 +143,7 @@ impl NavTree {
#[test]
fn test_tree() {
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
let point1 = (1600.0, -1300.0);
@ -174,11 +176,6 @@ fn test_tree() {
vec![147.23126],
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)]

View file

@ -24,10 +24,7 @@ impl fmt::Display for NavAreaId {
#[derive(Debug)]
pub struct NavArea {
pub id: NavAreaId,
pub north_west: Vector3,
pub south_east: Vector3,
pub north_east_z: f32,
pub south_west_z: f32,
pub quad: NavQuad,
pub flags: u32,
pub connections: Connections,
pub hiding_spots: Vec<NavHidingSpot>,
@ -42,60 +39,8 @@ pub struct NavArea {
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;
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
///
/// Contains a list of area id's for every [`NavDirection`]
@ -131,6 +76,14 @@ impl<E: Endianness> BitRead<E> for 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 {
@ -176,6 +129,14 @@ impl<E: Endianness> BitRead<E> for LadderConnections {
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 {
@ -263,5 +224,63 @@ pub struct NavPlace {
name: String,
}
/// A navigation area from the nav file
#[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;
use bitbuffer::{BitReadStream, LittleEndian};
use crate::navmesh::NavQuad;
use crate::{Connections, EncounterPath, LadderConnections, NavHidingSpot, VisibleArea};
use bitbuffer::{BitRead, BitReadStream, LittleEndian};
use err_derive::Error;
/// Errors that can occur when parsing the binary nav file
@ -19,9 +21,23 @@ pub enum ParseError {
UnsupportedVersion(u32),
}
pub(crate) fn read_areas(
mut data: BitReadStream<LittleEndian>,
/// Parse all navigation areas from a nav file
///
/// ## 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> {
let mut data = data.into();
let magic = data.read()?;
if magic != 0xFEED_FACE {
return Err(ParseError::InvalidMagicNumber(magic));
@ -120,10 +136,12 @@ pub(crate) fn read_areas(
areas.push(NavArea {
id,
quad: NavQuad {
north_west,
south_east,
north_east_z,
south_west_z,
},
flags,
connections,
hiding_spots,
@ -144,6 +162,97 @@ pub(crate) fn read_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]
fn test() {
let file = std::fs::read("data/pl_badwater.nav").unwrap();
@ -151,3 +260,11 @@ fn test() {
let areas = read_areas(data).unwrap();
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());
}