This commit is contained in:
Robin Appelman 2025-05-30 17:23:40 +02:00
commit 89ab56a12c
5 changed files with 115 additions and 175 deletions

View file

@ -4,8 +4,9 @@ parsing of SourceEngine `.nav` files
## Usage ## Usage
This library is currently focused on getting the z-height from an x/y coordinate in a map and the api is tailored towards 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. in a map and the api is tailored towards that usage. For other usages the raw
navigation areas are exposed.
```rust ```rust
use sourcenav::get_quad_tree; use sourcenav::get_quad_tree;
@ -23,5 +24,5 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
## Credits and Licence ## Credits and Licence
This library is largely based on [gonav](https://github.com/mrazza/gonav), a parser for `.nav` files written in Go This library is largely based on [gonav](https://github.com/mrazza/gonav), a
and is licenced under AGPL-3.0. parser for `.nav` files written in Go and is licenced under AGPL-3.0.

View file

@ -6,7 +6,7 @@ pub use crate::navmesh::{
use crate::parser::read_quads; use crate::parser::read_quads;
pub use crate::parser::{read_areas, 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 binread::io::{Read, Seek};
use euclid::{TypedPoint2D, TypedRect, TypedSize2D}; use euclid::{TypedPoint2D, TypedRect, TypedSize2D};
mod navmesh; mod navmesh;
@ -30,10 +30,8 @@ pub struct NavQuadTree(QuadTree<NavQuad, HammerUnit, [(ItemId, Rect); 4]>);
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn get_quad_tree( pub fn get_quad_tree<R: Read + Seek>(data: &mut R) -> Result<NavQuadTree, ParseError> {
data: impl Into<BitReadStream<LittleEndian>>, let areas = read_quads(data)?;
) -> 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),
@ -142,8 +140,11 @@ impl NavQuadTree {
#[test] #[test]
fn test_tree() { fn test_tree() {
use std::io::Cursor;
let file = std::fs::read("data/pl_badwater.nav").unwrap(); let file = std::fs::read("data/pl_badwater.nav").unwrap();
let tree = get_quad_tree(file).unwrap(); let mut cursor = Cursor::new(&file);
let tree = get_quad_tree(&mut cursor).unwrap();
// single flat plane // single flat plane
let point1 = (1600.0, -1300.0); let point1 = (1600.0, -1300.0);

View file

@ -1,17 +1,18 @@
use crate::Rect; use crate::Rect;
use aabb_quadtree::Spatial; use aabb_quadtree::Spatial;
use bitbuffer::{BitRead, BitReadStream, Endianness, ReadError}; use binread::io::{Read, Seek};
use binread::{BinRead, BinReaderExt, BinResult, ReadOptions};
use euclid::{TypedPoint2D, TypedSize2D}; use euclid::{TypedPoint2D, TypedSize2D};
use std::fmt; use std::fmt;
use std::fmt::Debug; use std::fmt::Debug;
use std::ops::Index; use std::ops::Index;
/// A 3 dimensional coordinate /// A 3 dimensional coordinate
#[derive(Debug, BitRead)] #[derive(Debug, BinRead)]
pub struct Vector3(pub f32, pub f32, pub f32); pub struct Vector3(pub f32, pub f32, pub f32);
/// A unique identifier for a navigation area /// A unique identifier for a navigation area
#[derive(Debug, BitRead, Clone, Copy, Eq, PartialEq)] #[derive(Debug, BinRead, Clone, Copy, Eq, PartialEq)]
pub struct NavAreaId(u32); pub struct NavAreaId(u32);
impl fmt::Display for NavAreaId { impl fmt::Display for NavAreaId {
@ -21,22 +22,35 @@ impl fmt::Display for NavAreaId {
} }
/// A navigation area from the nav file /// A navigation area from the nav file
#[derive(Debug)] #[derive(Debug, BinRead)]
#[br(import(major_version: u32))]
pub struct NavArea { pub struct NavArea {
pub id: NavAreaId, pub id: NavAreaId,
pub quad: NavQuad,
pub flags: u32, pub flags: u32,
pub quad: NavQuad,
pub connections: Connections, pub connections: Connections,
pub hiding_spots_count: u8,
#[br(count = hiding_spots_count)]
pub hiding_spots: Vec<NavHidingSpot>, pub hiding_spots: Vec<NavHidingSpot>,
#[br(if(major_version < 15))]
pub approach_areas_count: u8,
#[br(count = approach_areas_count)]
pub approach_areas: Vec<ApproachArea>, pub approach_areas: Vec<ApproachArea>,
pub encounter_paths_count: u32,
#[br(count = encounter_paths_count)]
pub encounter_paths: Vec<EncounterPath>, pub encounter_paths: Vec<EncounterPath>,
pub place: u16, pub place: u16,
pub light_intensity: LightIntensity,
pub ladder_connections: LadderConnections, pub ladder_connections: LadderConnections,
pub earliest_occupy_first_team: f32, pub earliest_occupy_first_team: f32,
pub earliest_occupy_second_team: f32, pub earliest_occupy_second_team: f32,
#[br(if(major_version >= 11))]
pub light_intensity: LightIntensity,
#[br(if(major_version >= 16))]
pub visible_areas_count: u32,
#[br(count = visible_areas_count)]
pub visible_areas: Vec<VisibleArea>, pub visible_areas: Vec<VisibleArea>,
pub inherit_visibility_from_area_id: u32, pub inherit_visibility_from_area_id: u32,
pub padding: u32,
} }
pub(crate) struct HammerUnit; pub(crate) struct HammerUnit;
@ -62,28 +76,26 @@ pub(crate) struct HammerUnit;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Connections([Vec<NavAreaId>; 4]); pub struct Connections([Vec<NavAreaId>; 4]);
impl<E: Endianness> BitRead<E> for Connections { impl BinRead for Connections {
fn read(stream: &mut BitReadStream<E>) -> Result<Self, ReadError> { type Args = ();
fn read_options<R: Read + Seek>(
reader: &mut R,
options: &ReadOptions,
args: Self::Args,
) -> BinResult<Self> {
let mut connections = [Vec::new(), Vec::new(), Vec::new(), Vec::new()]; let mut connections = [Vec::new(), Vec::new(), Vec::new(), Vec::new()];
for direction in connections.iter_mut() { for direction in connections.iter_mut() {
let connection_count: u32 = stream.read()?; let connection_count: u32 = reader.read_le()?;
direction.reserve(connection_count as usize); direction.reserve(connection_count as usize);
for _ in 0..connection_count { for _ in 0..connection_count {
direction.push(stream.read()?); direction.push(reader.read_le()?);
} }
} }
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 {
@ -115,28 +127,26 @@ impl Index<NavDirection> for Connections {
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct LadderConnections([Vec<NavAreaId>; 2]); pub struct LadderConnections([Vec<NavAreaId>; 2]);
impl<E: Endianness> BitRead<E> for LadderConnections { impl BinRead for LadderConnections {
fn read(stream: &mut BitReadStream<E>) -> Result<Self, ReadError> { type Args = ();
fn read_options<R: Read + Seek>(
reader: &mut R,
options: &ReadOptions,
args: Self::Args,
) -> BinResult<Self> {
let mut connections = [Vec::new(), Vec::new()]; let mut connections = [Vec::new(), Vec::new()];
for direction in connections.iter_mut() { for direction in connections.iter_mut() {
let connection_count: u32 = stream.read()?; let connection_count: u32 = reader.read_le()?;
direction.reserve(connection_count as usize); direction.reserve(connection_count as usize);
for _ in 0..connection_count { for _ in 0..connection_count {
direction.push(stream.read()?); direction.push(reader.read_le()?);
} }
} }
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 {
@ -148,9 +158,8 @@ impl Index<LadderDirection> for LadderConnections {
} }
/// The directions in which two areas can be connected /// The directions in which two areas can be connected
#[derive(Debug, BitRead)] #[derive(Debug, BinRead)]
#[repr(u8)] #[repr(u8)]
#[discriminant_bits = 8]
pub enum NavDirection { pub enum NavDirection {
North, North,
East, East,
@ -159,16 +168,15 @@ pub enum NavDirection {
} }
/// The directions in which two areas can be connected by ladder /// The directions in which two areas can be connected by ladder
#[derive(Debug, BitRead)] #[derive(Debug, BinRead)]
#[repr(u8)] #[repr(u8)]
#[discriminant_bits = 8]
pub enum LadderDirection { pub enum LadderDirection {
Up, Up,
Down, Down,
} }
/// A hiding spot within an area /// A hiding spot within an area
#[derive(Debug, BitRead)] #[derive(Debug, BinRead)]
pub struct NavHidingSpot { pub struct NavHidingSpot {
id: u32, id: u32,
location: Vector3, location: Vector3,
@ -176,7 +184,7 @@ pub struct NavHidingSpot {
} }
/// An area that can be used for approach, no longer used in newer nav files /// An area that can be used for approach, no longer used in newer nav files
#[derive(Debug, BitRead)] #[derive(Debug, BinRead)]
pub struct ApproachArea { pub struct ApproachArea {
approach_here: u32, approach_here: u32,
approach_pre: u32, approach_pre: u32,
@ -186,24 +194,25 @@ pub struct ApproachArea {
} }
/// A path that can be used to approach an area /// A path that can be used to approach an area
#[derive(Debug, BitRead)] #[derive(Debug, BinRead)]
pub struct EncounterPath { pub struct EncounterPath {
from_area_id: NavAreaId, from_area_id: NavAreaId,
from_direction: u8, from_direction: u8,
to_area_id: NavAreaId, to_area_id: NavAreaId,
to_direction: u8, to_direction: u8,
#[size_bits = 8] spot_count: u8,
#[br(count = spot_count)]
spots: Vec<EncounterSpot>, spots: Vec<EncounterSpot>,
} }
#[derive(Debug, BitRead)] #[derive(Debug, BinRead)]
pub struct EncounterSpot { pub struct EncounterSpot {
order: u32, order: u32,
distance: u8, // divide by 255 distance: u8, // divide by 255
} }
/// The light intensity at the four corners of an area /// The light intensity at the four corners of an area
#[derive(Debug, BitRead, Default)] #[derive(Debug, BinRead, Default)]
pub struct LightIntensity { pub struct LightIntensity {
pub north_west: f32, pub north_west: f32,
pub north_east: f32, pub north_east: f32,
@ -212,7 +221,7 @@ pub struct LightIntensity {
} }
/// An area that is visible /// An area that is visible
#[derive(Debug, BitRead)] #[derive(Debug, BinRead)]
pub struct VisibleArea { pub struct VisibleArea {
id: u32, id: u32,
attributes: u8, attributes: u8,
@ -225,7 +234,7 @@ pub struct NavPlace {
} }
/// A navigation area from the nav file /// A navigation area from the nav file
#[derive(Debug)] #[derive(Debug, BinRead)]
pub struct NavQuad { pub struct NavQuad {
pub north_west: Vector3, pub north_west: Vector3,
pub south_east: Vector3, pub south_east: Vector3,

View file

@ -1,15 +1,17 @@
pub use crate::navmesh::NavArea; pub use crate::navmesh::NavArea;
use crate::navmesh::NavQuad; use crate::navmesh::NavQuad;
use crate::{Connections, EncounterPath, LadderConnections, NavHidingSpot, VisibleArea}; use crate::{Connections, EncounterPath, LadderConnections, NavHidingSpot, VisibleArea};
use bitbuffer::{BitRead, BitReadStream, LittleEndian}; use binread::io::{Read, Seek, SeekFrom};
use binread::{BinRead, BinReaderExt, ReadOptions};
use err_derive::Error; use err_derive::Error;
use std::mem::size_of;
/// Errors that can occur when parsing the binary nav file /// Errors that can occur when parsing the binary nav file
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ParseError { pub enum ParseError {
/// An error ocured when reading from the source binary data /// An error ocured when reading from the source binary data
#[error(display = "Error while reading from data: {}", _0)] #[error(display = "Error while reading from data: {}", _0)]
ReadError(#[error(source)] bitbuffer::ReadError), ReadError(#[error(source)] binread::Error),
#[error( #[error(
display = "Invalid magic number ({:#8X}), not a nav file or corrupted", display = "Invalid magic number ({:#8X}), not a nav file or corrupted",
_0 _0
@ -34,127 +36,53 @@ pub enum ParseError {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn read_areas( pub fn read_areas<R: Read + Seek>(data: &mut R) -> Result<Vec<NavArea>, ParseError> {
data: impl Into<BitReadStream<LittleEndian>>, let magic = data.read_le()?;
) -> Result<Vec<NavArea>, ParseError> {
let mut data = data.into();
let magic = data.read()?;
if magic != 0xFEED_FACE { if magic != 0xFEED_FACE {
return Err(ParseError::InvalidMagicNumber(magic)); return Err(ParseError::InvalidMagicNumber(magic));
} }
let major_version: u32 = data.read()?; let major_version: u32 = data.read_le()?;
if major_version < 6 || major_version > 16 { if major_version < 6 || major_version > 16 {
return Err(ParseError::UnsupportedVersion(major_version)); return Err(ParseError::UnsupportedVersion(major_version));
} }
let _minor_version: u32 = if major_version >= 10 { data.read()? } else { 0 }; let _minor_version: u32 = if major_version >= 10 {
data.read_le()?
} else {
0
};
let _size: u32 = data.read()?; let _size: u32 = data.read_le()?;
let _is_analysed = if major_version >= 14 { let _is_analysed = if major_version >= 14 {
data.read_int::<u8>(8)? == 1 data.read_le::<u8>(8)? == 1
} else { } else {
false false
}; };
let place_count: u16 = data.read()?; let place_count: u16 = data.read_le()?;
// let places = Vec::with_capacity(place_count as usize); // let places = Vec::with_capacity(place_count as usize);
for _id in 1..=place_count { for _id in 1..=place_count {
let name_length: u16 = data.read()?; let name_length: u16 = data.read_le()?;
let _name = data.read_string(Some(name_length as usize))?; let _name = data.read_string(Some(name_length as usize))?;
// TODO // TODO
} }
let _has_unnamed_areas = if major_version >= 12 { let _has_unnamed_areas = if major_version >= 12 {
data.read_int::<u8>(8)? == 1 data.read_le::<u8>(8)? == 1
} else { } else {
false false
}; };
let area_count: u32 = data.read()?; let area_count: u32 = data.read_le()?;
let mut areas = Vec::with_capacity(area_count as usize); let mut areas = Vec::with_capacity(area_count as usize);
for _ in 0..area_count { for _ in 0..area_count {
let id = data.read()?; areas.push(data.read_le()?);
let flags = if major_version <= 8 {
data.read_int(8)?
} else if major_version <= 12 {
data.read_int(16)?
} else {
data.read_int(32)?
};
let north_west = data.read()?;
let south_east = data.read()?;
let north_east_z = data.read()?;
let south_west_z = data.read()?;
let connections = data.read()?;
let hiding_spots_count: u8 = data.read()?;
let hiding_spots = data.read_sized(hiding_spots_count as usize)?;
let approach_areas = if major_version < 15 {
let approach_area_count: u8 = data.read()?;
data.read_sized(approach_area_count as usize)?
} else {
Vec::new()
};
let encounter_paths_count: u32 = data.read()?;
let encounter_paths = data.read_sized(encounter_paths_count as usize)?;
let place = data.read()?;
let ladder_connections = data.read()?;
let earliest_occupy_first_team = data.read()?;
let earliest_occupy_second_team = data.read()?;
let light_intensity = if major_version >= 11 {
data.read()?
} else {
Default::default()
};
let visible_areas = if major_version >= 16 {
let visible_areas_count: u32 = data.read()?;
data.read_sized(visible_areas_count as usize)?
} else {
Vec::new()
};
let inherit_visibility_from_area_id = data.read()?;
data.skip_bits(32)?;
areas.push(NavArea {
id,
quad: NavQuad {
north_west,
south_east,
north_east_z,
south_west_z,
},
flags,
connections,
hiding_spots,
approach_areas,
encounter_paths,
place,
ladder_connections,
earliest_occupy_first_team,
earliest_occupy_second_team,
light_intensity,
visible_areas,
inherit_visibility_from_area_id,
});
} }
debug_assert!(data.bits_left() <= 32); debug_assert!(data.bits_left() <= 32);
@ -162,83 +90,83 @@ pub fn read_areas(
Ok(areas) Ok(areas)
} }
pub(crate) fn read_quads( pub(crate) fn read_quads<R: Read + Seek>(data: &mut R) -> Result<Vec<NavQuad>, ParseError> {
mut data: BitReadStream<LittleEndian>, let magic = data.read_le()?;
) -> Result<Vec<NavQuad>, ParseError> {
let magic = data.read()?;
if magic != 0xFEED_FACE { if magic != 0xFEED_FACE {
return Err(ParseError::InvalidMagicNumber(magic)); return Err(ParseError::InvalidMagicNumber(magic));
} }
let major_version: u32 = data.read()?; let major_version: u32 = data.read_le()?;
if major_version != 16 { if major_version != 16 {
return Err(ParseError::UnsupportedVersion(major_version)); return Err(ParseError::UnsupportedVersion(major_version));
} }
let _minor_version: u32 = if major_version >= 10 { data.read()? } else { 0 }; let _minor_version: u32 = if major_version >= 10 {
data.read_le()?
} else {
0
};
let _size: u32 = data.read()?; let _size: u32 = data.read_le()?;
let _is_analysed = if major_version >= 14 { let _is_analysed = if major_version >= 14 {
data.read_int::<u8>(8)? == 1 data.read_le::<u8>(8)? == 1
} else { } else {
false false
}; };
let place_count: u16 = data.read()?; let place_count: u16 = data.read_le()?;
// let places = Vec::with_capacity(place_count as usize); // let places = Vec::with_capacity(place_count as usize);
for _id in 1..=place_count { for _id in 1..=place_count {
let name_length: u16 = data.read()?; let name_length: u16 = data.read_le()?;
let _name = data.read_string(Some(name_length as usize))?; let _name = data.read_string(Some(name_length as usize))?;
// TODO // TODO
} }
let _has_unnamed_areas = if major_version >= 12 { let _has_unnamed_areas = if major_version >= 12 {
data.read_int::<u8>(8)? == 1 data.read_le::<u8>(8)? == 1
} else { } else {
false false
}; };
let area_count: u32 = data.read()?; let area_count: u32 = data.read_le()?;
let mut areas = Vec::with_capacity(area_count as usize); let mut areas = Vec::with_capacity(area_count as usize);
for _ in 0..area_count { for _ in 0..area_count {
data.skip_bits(32 * 2)?; // id and flags data.seek(SeekFrom::Current(4 * 2))?; // id and flags
let north_west = data.read()?; let north_west = data.read_le()?;
let south_east = data.read()?; let south_east = data.read_le()?;
let north_east_z = data.read()?; let north_east_z = data.read_le()?;
let south_west_z = data.read()?; let south_west_z = data.read_le()?;
Connections::skip(&mut data)?; Connections::skip(&mut data)?;
let hiding_spots_count: u8 = data.read()?; let hiding_spots_count: u8 = data.read_le()?;
data.skip_bits( data.seek(SeekFrom::Current(
<NavHidingSpot as BitRead<LittleEndian>>::bit_size().unwrap() size_of::<NavHidingSpot>() as i64 * hiding_spots_count as i64,
* hiding_spots_count as usize, ))?;
)?;
let encounter_paths_count: u32 = data.read()?; let encounter_paths_count: u32 = data.read_le()?;
for _ in 0..encounter_paths_count { for _ in 0..encounter_paths_count {
EncounterPath::skip(&mut data)?; EncounterPath::skip(&mut data)?;
} }
data.skip_bits(16)?; // place data.seek(SeekFrom::Current(2))?;
LadderConnections::skip(&mut data)?; let _: LadderConnections = data.read_le()?;
data.skip_bits((32 * 2) + (32 * 4))?; // occupy time, light intensity data.seek(SeekFrom::Current((4 * 2) + (4 * 4)))?; // occupy time, light intensity
let visible_areas_count: u32 = data.read()?; let visible_areas_count: u32 = data.read_le()?;
data.skip_bits( data.seek(SeekFrom::Current(
<VisibleArea as BitRead<LittleEndian>>::bit_size().unwrap() size_of::<VisibleArea>() as i64 * visible_areas_count as i64,
* visible_areas_count as usize, ))?;
)?;
data.skip_bits(32 * 2)?; // inherit visible, garbage data.seek(SeekFrom::Current(4 * 2))?; // inherit visible, garbage
areas.push(NavQuad { areas.push(NavQuad {
north_west, north_west,

View file

@ -0,0 +1 @@