1use crate::collisions::CollisionChecker;
5use crate::constants::{
6 CROUCHING_ATTRIBUTE_FLAG, CROUCHING_SPEED, FOOTSTEP_RANGE, JUMP_HEIGHT, LADDER_SPEED,
7 PLAYER_CROUCH_HEIGHT, PLAYER_EYE_LEVEL, PLAYER_HEIGHT, PLAYER_WIDTH, RUNNING_SPEED,
8};
9use crate::position::{Position, inverse_distance_weighting};
10use crate::utils::create_file_with_parents;
11
12use bincode::{deserialize_from, serialize_into};
13use byteorder::{LittleEndian, ReadBytesExt};
14use geo::algorithm::line_measures::metric_spaces::Euclidean;
15use geo::geometry::{LineString, Point, Polygon};
16use geo::{Centroid, Contains, Distance, Intersects};
17use itertools::{Itertools, iproduct};
18use petgraph::algo::astar;
19use petgraph::graphmap::DiGraphMap;
20use petgraph::visit::EdgeRef;
21use pyo3::exceptions::{PyException, PyFileNotFoundError};
22use pyo3::{
23 FromPyObject, IntoPyObject, PyErr, PyResult, create_exception, pyclass, pyfunction, pymethods,
24};
25use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
26use rustc_hash::FxHashMap as HashMap;
27use rustc_hash::FxHashSet as HashSet;
28use serde::de::{self, MapAccess, Visitor};
29use serde::{Deserialize, Deserializer, Serialize};
30use simple_tqdm::{Config, ParTqdm, Tqdm};
31use std::cmp::Ordering;
32use std::f64;
33use std::fmt;
34use std::fs::File;
35use std::hash::Hash;
36use std::io::{BufReader, Read};
37use std::path::{Path, PathBuf};
38
39#[pyclass(eq, module = "cs2_nav")]
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
42pub struct DynamicAttributeFlags(i64);
43
44#[pymethods]
45impl DynamicAttributeFlags {
46 #[must_use]
47 #[new]
48 pub const fn new(value: i64) -> Self {
49 Self(value)
50 }
51}
52
53impl From<DynamicAttributeFlags> for i64 {
54 fn from(flag: DynamicAttributeFlags) -> Self {
55 flag.0
56 }
57}
58
59pub trait AreaLike {
60 fn centroid(&self) -> Position;
61 fn requires_crouch(&self) -> bool;
62 fn area_id(&self) -> u32;
63}
64
65struct NavMeshConnection {
66 area_id: u32,
67 #[allow(dead_code)]
68 edge_id: u32,
69}
70
71impl NavMeshConnection {
72 fn from_binary(reader: &mut BufReader<File>) -> Result<Self, PyErr> {
73 let area_id = reader
74 .read_u32::<LittleEndian>()
75 .map_err(|_| InvalidNavFileError::new_err("Failed to read connection area id."))?;
76 let edge_id = reader
77 .read_u32::<LittleEndian>()
78 .map_err(|_| InvalidNavFileError::new_err("Failed to read connection edge id."))?;
79 Ok(Self { area_id, edge_id })
80 }
81}
82
83#[pyclass(eq, str, module = "cs2_nav")]
84#[derive(Debug, Clone, Serialize)]
86pub struct NavArea {
87 #[pyo3(get)]
91 pub area_id: u32,
92 #[pyo3(get)]
93 pub hull_index: u32,
94 #[pyo3(get)]
95 pub dynamic_attribute_flags: DynamicAttributeFlags,
96 #[pyo3(get)]
98 pub corners: Vec<Position>,
99 #[pyo3(get)]
103 pub connections: Vec<u32>,
104 #[pyo3(get)]
106 pub ladders_above: Vec<u32>,
107 #[pyo3(get)]
109 pub ladders_below: Vec<u32>,
110 #[pyo3(get)]
112 centroid: Position,
113}
114
115impl fmt::Display for NavArea {
116 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
117 write!(
118 f,
119 "NavArea(area_id: {}, hull_index: {}, dynamic_attribute_flags: {:?}, corners: {:?}, connections: {:?}, ladders_above: {:?}, ladders_below: {:?})",
120 self.area_id,
121 self.hull_index,
122 self.dynamic_attribute_flags,
123 self.corners,
124 self.connections,
125 self.ladders_above,
126 self.ladders_below
127 )
128 }
129}
130
131impl PartialEq for NavArea {
133 fn eq(&self, other: &Self) -> bool {
134 self.area_id == other.area_id
135 }
136}
137
138#[allow(clippy::cast_precision_loss)]
139#[must_use]
141pub fn centroid(corners: &[Position]) -> Position {
142 if corners.is_empty() {
143 return Position::new(0.0, 0.0, 0.0);
144 }
145 let (sum_x, sum_y, sum_z) = corners.iter().fold((0.0, 0.0, 0.0), |(sx, sy, sz), c| {
146 (sx + c.x, sy + c.y, sz + c.z)
147 });
148 let count = corners.len() as f64;
149 Position::new(sum_x / count, sum_y / count, sum_z / count)
150}
151
152impl NavArea {
153 #[must_use]
155 pub fn to_polygon_2d(&self) -> Polygon {
156 let coords: Vec<(f64, f64)> = self.corners.iter().map(|c| (c.x, c.y)).collect();
157 Polygon::new(LineString::from(coords), vec![])
158 }
159
160 fn read_connections(reader: &mut BufReader<File>) -> Result<Vec<NavMeshConnection>, PyErr> {
161 let connection_count = reader
162 .read_u32::<LittleEndian>()
163 .map_err(|_| InvalidNavFileError::new_err("Failed to read connection count."))?;
164 let mut connections = Vec::with_capacity(connection_count as usize);
165 for _ in 0..connection_count {
166 connections.push(NavMeshConnection::from_binary(reader)?);
167 }
168 Ok(connections)
169 }
170
171 fn from_data(
172 reader: &mut BufReader<File>,
173 nav_mesh_version: u32,
174 polygons: Option<&Vec<Vec<Position>>>,
175 ) -> Result<Self, PyErr> {
176 let area_id = reader
177 .read_u32::<LittleEndian>()
178 .map_err(|_| InvalidNavFileError::new_err("Failed to read area id."))?;
179
180 let dynamic_attribute_flags =
181 DynamicAttributeFlags::new(reader.read_i64::<LittleEndian>().map_err(|_| {
182 InvalidNavFileError::new_err("Failed to read dynamic attribute flags.")
183 })?);
184
185 let hull_index = u32::from(
186 reader
187 .read_u8()
188 .map_err(|_| InvalidNavFileError::new_err("Failed to read hull index."))?,
189 );
190
191 let corners =
192 if nav_mesh_version >= 31 && polygons.is_some() {
193 let polygon_index = reader
194 .read_u32::<LittleEndian>()
195 .map_err(|_| InvalidNavFileError::new_err("Failed to read polygon index."))?
196 as usize;
197 polygons.as_ref().unwrap()[polygon_index].clone()
198 } else {
199 let corner_count = reader
200 .read_u32::<LittleEndian>()
201 .map_err(|_| InvalidNavFileError::new_err("Failed to read corner count."))?;
202 let mut corners = Vec::with_capacity(corner_count as usize);
203 for _ in 0..corner_count {
204 let x =
205 f64::from(reader.read_f32::<LittleEndian>().map_err(|_| {
206 InvalidNavFileError::new_err("Failed to read corner x.")
207 })?);
208 let y =
209 f64::from(reader.read_f32::<LittleEndian>().map_err(|_| {
210 InvalidNavFileError::new_err("Failed to read corner y.")
211 })?);
212 let z =
213 f64::from(reader.read_f32::<LittleEndian>().map_err(|_| {
214 InvalidNavFileError::new_err("Failed to read corner z.")
215 })?);
216 corners.push(Position { x, y, z });
217 }
218 corners
219 };
220
221 reader
222 .read_u32::<LittleEndian>()
223 .map_err(|_| InvalidNavFileError::new_err("Failed to skip."))?; let mut connections = Vec::new();
226 for _ in 0..corners.len() {
227 for conn in Self::read_connections(reader)? {
228 connections.push(conn.area_id);
229 }
230 }
231
232 reader
233 .read_exact(&mut [0u8; 5])
234 .map_err(|_| InvalidNavFileError::new_err("Failed to skip."))?; let ladder_above_count = reader
237 .read_u32::<LittleEndian>()
238 .map_err(|_| InvalidNavFileError::new_err("Failed to read ladder above count."))?;
239 let mut ladders_above = Vec::with_capacity(ladder_above_count as usize);
240 for _ in 0..ladder_above_count {
241 ladders_above.push(
242 reader
243 .read_u32::<LittleEndian>()
244 .map_err(|_| InvalidNavFileError::new_err("Failed to read ladder above."))?,
245 );
246 }
247
248 let ladder_below_count = reader
249 .read_u32::<LittleEndian>()
250 .map_err(|_| InvalidNavFileError::new_err("Failed to read ladder below count."))?;
251 let mut ladders_below = Vec::with_capacity(ladder_below_count as usize);
252 for _ in 0..ladder_below_count {
253 ladders_below.push(
254 reader
255 .read_u32::<LittleEndian>()
256 .map_err(|_| InvalidNavFileError::new_err("Failed to read ladder below."))?,
257 );
258 }
259
260 Ok(Self::new(
261 area_id,
262 hull_index,
263 dynamic_attribute_flags,
264 corners,
265 connections,
266 ladders_above,
267 ladders_below,
268 ))
269 }
270}
271
272#[pymethods]
273impl NavArea {
274 #[must_use]
275 #[new]
276 pub fn new(
277 area_id: u32,
278 hull_index: u32,
279 dynamic_attribute_flags: DynamicAttributeFlags,
280 corners: Vec<Position>,
281 connections: Vec<u32>,
282 ladders_above: Vec<u32>,
283 ladders_below: Vec<u32>,
284 ) -> Self {
285 let centroid = centroid(&corners);
286 Self {
287 area_id,
288 hull_index,
289 dynamic_attribute_flags,
290 corners,
291 connections,
292 ladders_above,
293 ladders_below,
294 centroid,
295 }
296 }
297
298 #[must_use]
300 #[getter]
301 pub fn size(&self) -> f64 {
302 if self.corners.len() < 3 {
303 return 0.0;
304 }
305 let mut x: Vec<f64> = self.corners.iter().map(|c| c.x).collect();
306 let mut y: Vec<f64> = self.corners.iter().map(|c| c.y).collect();
307 x.push(x[0]);
309 y.push(y[0]);
310
311 let mut area = 0.0;
312 for i in 0..self.corners.len() {
313 area += x[i].mul_add(y[i + 1], -(y[i] * x[i + 1]));
314 }
315 area.abs() / 2.0
316 }
317
318 #[must_use]
320 pub fn contains(&self, point: &Position) -> bool {
321 self.to_polygon_2d().contains(&point.to_point_2d())
322 }
323
324 #[must_use]
325 pub fn centroid_distance(&self, point: &Position) -> f64 {
326 self.centroid().distance(point)
327 }
328}
329
330impl<'de> Deserialize<'de> for NavArea {
334 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
335 where
336 D: Deserializer<'de>,
337 {
338 struct NavAreaVisitor;
339
340 impl<'de> Visitor<'de> for NavAreaVisitor {
341 type Value = NavArea;
342
343 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
344 formatter.write_str("a NavArea struct")
345 }
346
347 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
348 where
349 A: MapAccess<'de>,
350 {
351 let mut area_id = None;
352 let mut hull_index = None;
353 let mut dynamic_attribute_flags = None;
354 let mut corners: Option<Vec<Position>> = None;
355 let mut connections = None;
356 let mut ladders_above = None;
357 let mut ladders_below = None;
358 let mut nav_centroid = None;
359
360 while let Some(key) = map.next_key::<String>()? {
361 match key.as_str() {
362 "area_id" => area_id = Some(map.next_value()?),
363 "hull_index" => hull_index = Some(map.next_value()?),
364 "dynamic_attribute_flags" => {
365 dynamic_attribute_flags = Some(map.next_value()?);
366 }
367 "corners" => corners = Some(map.next_value()?),
368 "connections" => connections = Some(map.next_value()?),
369 "ladders_above" => ladders_above = Some(map.next_value()?),
370 "ladders_below" => ladders_below = Some(map.next_value()?),
371 "centroid" => nav_centroid = Some(map.next_value()?),
372 _ => {
373 let _: serde::de::IgnoredAny = map.next_value()?;
374 }
375 }
376 }
377
378 let area_id = area_id.ok_or_else(|| de::Error::missing_field("area_id"))?;
379 let hull_index = hull_index.unwrap_or(0); let dynamic_attribute_flags = dynamic_attribute_flags
381 .ok_or_else(|| de::Error::missing_field("dynamic_attribute_flags"))?;
382 let corners = corners.ok_or_else(|| de::Error::missing_field("corners"))?;
383 let connections = connections.unwrap_or_default(); let ladders_above = ladders_above.unwrap_or_default(); let ladders_below = ladders_below.unwrap_or_default(); let nav_centroid = nav_centroid.unwrap_or_else(|| centroid(&corners)); Ok(NavArea {
389 area_id,
390 hull_index,
391 dynamic_attribute_flags,
392 corners,
393 connections,
394 ladders_above,
395 ladders_below,
396 centroid: nav_centroid,
397 })
398 }
399 }
400
401 deserializer.deserialize_struct(
402 "NavArea",
403 &[
404 "area_id",
405 "hull_index",
406 "dynamic_attribute_flags",
407 "corners",
408 "connections",
409 "ladders_above",
410 "ladders_below",
411 "centroid",
412 ],
413 NavAreaVisitor,
414 )
415 }
416}
417
418impl AreaLike for NavArea {
419 fn centroid(&self) -> Position {
420 self.centroid
421 }
422 fn requires_crouch(&self) -> bool {
423 self.dynamic_attribute_flags == CROUCHING_ATTRIBUTE_FLAG
424 }
425
426 fn area_id(&self) -> u32 {
427 self.area_id
428 }
429}
430
431impl From<NewNavArea> for NavArea {
432 fn from(item: NewNavArea) -> Self {
433 Self {
434 area_id: item.area_id,
435 hull_index: 0,
436 dynamic_attribute_flags: item.dynamic_attribute_flags,
437 corners: item.corners,
438 connections: Vec::from_iter(item.connections),
439 ladders_above: Vec::from_iter(item.ladders_above),
440 ladders_below: Vec::from_iter(item.ladders_below),
441 centroid: item.centroid,
442 }
443 }
444}
445
446#[pyclass(eq, module = "cs2_nav")]
450#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
451pub struct PathResult {
452 #[pyo3(get, set)]
453 pub path: Vec<NavArea>,
454 #[pyo3(get, set)]
455 pub distance: f64,
456}
457
458#[pymethods]
459impl PathResult {
460 #[must_use]
461 #[new]
462 pub const fn new(path: Vec<NavArea>, distance: f64) -> Self {
463 Self { path, distance }
464 }
465}
466
467#[derive(Debug, Clone, Deserialize, Serialize, FromPyObject)]
471pub enum AreaIdent {
472 #[pyo3(transparent, annotation = "int")]
473 Id(u32),
474 #[pyo3(transparent, annotation = "Position")]
475 Pos(Position),
476}
477
478#[derive(Debug, Clone, Deserialize, Serialize)]
479struct NavSerializationHelperStruct {
480 pub version: u32,
481 pub sub_version: u32,
482 pub is_analyzed: bool,
483 pub areas: HashMap<u32, NavArea>,
484}
485
486#[pyclass(eq, str, module = "cs2_nav")]
487#[derive(Debug, Clone)]
488pub struct Nav {
489 #[pyo3(get)]
490 pub version: u32,
491 #[pyo3(get)]
492 pub sub_version: u32,
493 #[pyo3(get)]
494 pub areas: HashMap<u32, NavArea>,
495 #[pyo3(get)]
496 pub is_analyzed: bool,
497 pub graph: DiGraphMap<u32, f64>,
498}
499
500impl fmt::Display for Nav {
501 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
502 write!(
503 f,
504 "Nav(version: {}, sub_version: {}, areas_count: {}, is_analyzed: {})",
505 self.version,
506 self.sub_version,
507 self.areas.len(), self.is_analyzed
509 )
510 }
511}
512
513impl PartialEq for Nav {
514 fn eq(&self, other: &Self) -> bool {
515 self.version == other.version
516 && self.sub_version == other.sub_version
517 && self.areas == other.areas
518 && self.is_analyzed == other.is_analyzed
519 }
520}
521
522impl Nav {
523 pub const MAGIC: u32 = 0xFEED_FACE;
524
525 #[must_use]
533 pub fn find_area(&self, position: &Position) -> Option<&NavArea> {
534 self.areas
535 .values()
536 .filter(|area| area.contains(position))
537 .min_by(|a, b| {
538 ((a.centroid().z - position.z).abs() - (b.centroid().z - position.z).abs())
539 .partial_cmp(&0.0)
540 .unwrap()
541 })
542 }
543
544 #[must_use]
550 pub fn find_closest_area_centroid(&self, position: &Position) -> &NavArea {
551 self.areas
552 .values()
553 .min_by(|a, b| {
554 a.centroid_distance(position)
555 .partial_cmp(&b.centroid_distance(position))
556 .unwrap()
557 })
558 .unwrap()
559 }
560
561 fn dist_heuristic(&self, node_a: u32, node_b: u32) -> f64 {
563 let a = &self.areas[&node_a].centroid();
564 let b = &self.areas[&node_b].centroid();
565 a.distance_2d(b)
566 }
567
568 fn path_cost(&self, path: &[u32]) -> f64 {
570 path.iter()
571 .tuple_windows()
572 .map(|(u, v)| self.graph.edge_weight(*u, *v).unwrap())
573 .sum()
574 }
575
576 pub fn save_to_json(&self, filename: &Path) {
582 let mut file = create_file_with_parents(filename);
583 let helper = NavSerializationHelperStruct {
584 version: self.version,
585 sub_version: self.sub_version,
586 is_analyzed: self.is_analyzed,
587 areas: self.areas.clone(),
588 };
589 serde_json::to_writer(&mut file, &helper).unwrap();
590 }
591
592 #[must_use]
598 pub fn from_json(filename: &Path) -> Self {
599 let file = File::open(filename).unwrap();
600 let helper: NavSerializationHelperStruct = serde_json::from_reader(file).unwrap();
601 Self::new(
602 helper.version,
603 helper.sub_version,
604 helper.areas,
605 helper.is_analyzed,
606 )
607 }
608
609 fn read_polygons(
610 reader: &mut BufReader<File>,
611 version: u32,
612 ) -> Result<Vec<Vec<Position>>, PyErr> {
613 let corner_count = reader
614 .read_u32::<LittleEndian>()
615 .map_err(|_| InvalidNavFileError::new_err("Could not read corner count."))?;
616
617 let mut corners = Vec::with_capacity(corner_count as usize);
618
619 for _ in 0..corner_count {
620 let x = f64::from(
621 reader
622 .read_f32::<LittleEndian>()
623 .map_err(|_| InvalidNavFileError::new_err("Could not read corner x."))?,
624 );
625 let y = f64::from(
626 reader
627 .read_f32::<LittleEndian>()
628 .map_err(|_| InvalidNavFileError::new_err("Could not read corner y."))?,
629 );
630 let z = f64::from(
631 reader
632 .read_f32::<LittleEndian>()
633 .map_err(|_| InvalidNavFileError::new_err("Could not read corner z."))?,
634 );
635 corners.push(Position { x, y, z });
636 }
637
638 let polygon_count = reader
639 .read_u32::<LittleEndian>()
640 .map_err(|_| InvalidNavFileError::new_err("Could not read polygon count."))?;
641
642 let mut polygons = Vec::with_capacity(polygon_count as usize);
643 for _ in 0..polygon_count {
644 polygons.push(Self::read_polygon(reader, &corners, version)?);
645 }
646
647 Ok(polygons)
648 }
649
650 fn read_polygon(
651 reader: &mut BufReader<File>,
652 corners: &[Position],
653 version: u32,
654 ) -> Result<Vec<Position>, PyErr> {
655 let corner_count = reader
656 .read_u8()
657 .map_err(|_| InvalidNavFileError::new_err("Could not read polygon corner count."))?
658 as usize;
659 let mut polygon = Vec::with_capacity(corner_count);
660 for _ in 0..corner_count {
661 let index = reader
662 .read_u32::<LittleEndian>()
663 .map_err(|_| InvalidNavFileError::new_err("Could not read polygon corner index."))?
664 as usize;
665 polygon.push(corners[index]);
666 }
667 if version >= 35 {
668 reader
669 .read_u32::<LittleEndian>()
670 .map_err(|_| InvalidNavFileError::new_err("Failed to skip unk."))?; }
672 Ok(polygon)
673 }
674
675 fn read_areas(
676 reader: &mut BufReader<File>,
677 polygons: Option<&Vec<Vec<Position>>>,
678 version: u32,
679 ) -> Result<HashMap<u32, NavArea>, PyErr> {
680 let area_count = reader
681 .read_u32::<LittleEndian>()
682 .map_err(|_| InvalidNavFileError::new_err("Failed to read area count."))?;
683 let mut areas = HashMap::default();
684 for _ in 0..area_count {
685 let area = NavArea::from_data(reader, version, polygons)?;
686 areas.insert(area.area_id, area);
687 }
688 Ok(areas)
689 }
690}
691
692fn has_overlap<T: PartialEq>(a: &[T], b: &[T]) -> bool {
693 a.iter().any(|x| b.contains(x))
694}
695
696create_exception!(cs2_nav, InvalidNavFileError, PyException);
697
698#[pymethods]
699impl Nav {
700 #[must_use]
701 #[new]
702 pub fn new(
703 version: u32,
704 sub_version: u32,
705 areas: HashMap<u32, NavArea>,
706 is_analyzed: bool,
707 ) -> Self {
708 let mut graph = DiGraphMap::new();
709
710 for area_id in areas.keys() {
712 graph.add_node(*area_id);
713 }
714
715 for (area_id, area) in &areas {
717 for connected_area_id in &area.connections {
718 let connected_area = &areas[connected_area_id];
719 let dx = area.centroid().x - connected_area.centroid().x;
720 let dy = area.centroid().y - connected_area.centroid().y;
721 let dist_weight = dx.hypot(dy);
722
723 let area_speed = if area.requires_crouch() {
724 CROUCHING_SPEED
725 } else {
726 RUNNING_SPEED
727 };
728
729 let connected_area_speed = if connected_area.requires_crouch() {
730 CROUCHING_SPEED
731 } else {
732 RUNNING_SPEED
733 };
734
735 let area_time_adjusted_distance = dist_weight * (RUNNING_SPEED / area_speed);
736 let connected_area_time_adjusted_distance =
737 dist_weight * (RUNNING_SPEED / connected_area_speed);
738
739 let connected_by_ladder =
742 has_overlap(&area.ladders_above, &connected_area.ladders_below);
743 let ladder_distance = if connected_by_ladder {
744 let dz = connected_area.centroid().z - area.centroid().z - JUMP_HEIGHT;
745 dz.max(0_f64)
746 } else {
747 0.0
748 };
749 let time_adjusted_ladder_distance =
750 ladder_distance * (RUNNING_SPEED / LADDER_SPEED);
751
752 let time_adjusted =
753 ((area_time_adjusted_distance + connected_area_time_adjusted_distance) / 2.0)
754 + time_adjusted_ladder_distance;
755
756 graph.add_edge(*area_id, *connected_area_id, time_adjusted);
757 }
758 }
759
760 Self {
761 version,
762 sub_version,
763 areas,
764 is_analyzed,
765 graph,
766 }
767 }
768
769 #[must_use]
771 pub fn find_path(&self, start: AreaIdent, end: AreaIdent) -> PathResult {
772 let start_area = match start {
774 AreaIdent::Pos(pos) => {
775 self.find_area(&pos)
776 .unwrap_or_else(|| self.find_closest_area_centroid(&pos))
777 .area_id
778 }
779 AreaIdent::Id(id) => id,
780 };
781
782 let end_area = match end {
783 AreaIdent::Pos(pos) => {
784 self.find_area(&pos)
785 .unwrap_or_else(|| self.find_closest_area_centroid(&pos))
786 .area_id
787 }
788
789 AreaIdent::Id(id) => id,
790 };
791
792 let Some((distance, path_ids)) = astar(
794 &self.graph,
795 start_area,
796 |finish| finish == end_area,
797 |e| *e.weight(),
798 |node| self.dist_heuristic(node, end_area),
799 ) else {
800 return PathResult {
801 path: Vec::new(),
802 distance: f64::MAX,
803 };
804 };
805
806 let total_distance = if path_ids.len() <= 2 {
809 match (start, end) {
810 (AreaIdent::Pos(start_pos), AreaIdent::Pos(end_pos)) => {
811 start_pos.distance_2d(&end_pos)
812 }
813 (AreaIdent::Id(_), AreaIdent::Id(_)) => distance,
814 (AreaIdent::Pos(start_pos), AreaIdent::Id(_)) => {
816 start_pos.distance_2d(&self.areas[&end_area].centroid())
817 }
818 (AreaIdent::Id(_), AreaIdent::Pos(end_pos)) => {
819 self.areas[&start_area].centroid().distance_2d(&end_pos)
820 }
821 }
822 } else {
823 let start_distance = match start {
825 AreaIdent::Pos(start_pos) => {
826 start_pos.distance_2d(&self.areas[&path_ids[1]].centroid())
827 }
828 AreaIdent::Id(_) => self.path_cost(&path_ids[0..=1]),
829 };
830
831 let middle_distance: f64 = self.path_cost(&path_ids[1..path_ids.len() - 1]);
832
833 let end_distance = match end {
834 AreaIdent::Pos(end_pos) => self.areas[&path_ids[path_ids.len() - 2]]
835 .centroid()
836 .distance_2d(&end_pos),
837 AreaIdent::Id(_) => {
838 self.path_cost(&path_ids[path_ids.len() - 2..path_ids.len() - 1])
839 }
840 };
841
842 start_distance + middle_distance + end_distance
843 };
844
845 let path = path_ids
847 .iter()
848 .filter_map(|id| self.areas.get(id).cloned())
849 .collect();
850
851 PathResult {
852 path,
853 distance: total_distance,
854 }
855 }
856
857 #[must_use]
861 #[pyo3(name = "find_area")]
862 pub fn find_area_py(&self, position: &Position) -> Option<NavArea> {
863 self.find_area(position).cloned()
864 }
865
866 #[must_use]
868 #[pyo3(name = "find_closest_area_centroid")]
869 pub fn find_closest_area_centroid_py(&self, position: &Position) -> NavArea {
870 self.find_closest_area_centroid(position).clone()
871 }
872
873 #[pyo3(name = "to_json")]
875 #[allow(clippy::needless_pass_by_value)]
876 pub fn save_to_json_py(&self, path: PathBuf) {
877 self.save_to_json(&path);
878 }
879
880 #[must_use]
882 #[pyo3(name = "from_json")]
883 #[allow(clippy::needless_pass_by_value)]
884 #[staticmethod]
885 pub fn from_json_py(path: PathBuf) -> Self {
886 Self::from_json(&path)
887 }
888
889 #[allow(clippy::needless_pass_by_value)]
890 #[staticmethod]
891 fn from_path(path: PathBuf) -> PyResult<Self> {
892 let file = File::open(path).map_err(|_| PyFileNotFoundError::new_err("File not found"))?;
893 let mut reader = BufReader::new(file);
894
895 let magic = reader
896 .read_u32::<LittleEndian>()
897 .map_err(|_| InvalidNavFileError::new_err("Could not read magic number"))?;
898 if magic != Self::MAGIC {
899 return Err(InvalidNavFileError::new_err("Unexpected magic number"));
900 }
901
902 let version = reader
903 .read_u32::<LittleEndian>()
904 .map_err(|_| InvalidNavFileError::new_err("Could not read version number"))?;
905 if !(30..=35).contains(&version) {
906 return Err(InvalidNavFileError::new_err("Unsupported nav version"));
907 }
908
909 let sub_version = reader
910 .read_u32::<LittleEndian>()
911 .map_err(|_| InvalidNavFileError::new_err("Could not read sub version number"))?;
912
913 let unk1 = reader
914 .read_u32::<LittleEndian>()
915 .map_err(|_| InvalidNavFileError::new_err("Could not read unk1"))?;
916 let is_analyzed = (unk1 & 0x0000_0001) > 0;
917
918 let polygons = if version >= 31 {
919 Some(Self::read_polygons(&mut reader, version)?)
920 } else {
921 None
922 };
923
924 if version >= 32 {
925 reader
926 .read_u32::<LittleEndian>()
927 .map_err(|_| InvalidNavFileError::new_err("Failed to skip unk2: {}"))?; }
929 if version >= 35 {
930 reader
931 .read_u32::<LittleEndian>()
932 .map_err(|_| InvalidNavFileError::new_err("Failed to skip unk3: {}"))?; }
934
935 let areas = Self::read_areas(&mut reader, polygons.as_ref(), version)?;
936 Ok(Self::new(version, sub_version, areas, is_analyzed))
937 }
938}
939
940pub fn areas_audible<T: AreaLike>(area1: &T, area2: &T) -> bool {
941 area1.centroid().distance(&area2.centroid()) <= f64::from(FOOTSTEP_RANGE)
942}
943
944pub fn areas_visible<T: AreaLike>(area1: &T, area2: &T, vis_checker: &CollisionChecker) -> bool {
950 let height_correction = PLAYER_EYE_LEVEL;
951
952 let area1_centroid = area1.centroid();
953 let area2_centroid = area2.centroid();
954
955 let used_centroid1 = Position::new(
956 area1_centroid.x,
957 area1_centroid.y,
958 area1_centroid.z + height_correction,
959 );
960 let used_centroid2 = Position::new(
961 area2_centroid.x,
962 area2_centroid.y,
963 area2_centroid.z + height_correction,
964 );
965
966 vis_checker.connection_unobstructed(used_centroid1, used_centroid2)
967}
968
969#[must_use]
976pub fn get_visibility_cache(
977 map_name: &str,
978 granularity: usize,
979 nav: &Nav,
980 vis_checker: &CollisionChecker,
981 safe_to_file: bool,
982) -> HashMap<(u32, u32), bool> {
983 let tqdm_config = Config::new().with_leave(true);
984 let cache_path_str =
985 format!("./data/collisions/{map_name}_{granularity}_visibility_cache.vis_cache");
986 let cache_path = Path::new(&cache_path_str);
987 if cache_path.exists() {
988 println!("Loading visibility cache from binary.");
989 let file = File::open(cache_path).unwrap();
990 deserialize_from(file).unwrap()
991 } else {
992 println!("Building visibility cache from scratch.");
993 let visibility_cache = iproduct!(&nav.areas, &nav.areas)
994 .collect::<Vec<_>>()
995 .par_iter()
996 .tqdm_config(tqdm_config.with_desc("Building visibility cache"))
997 .map(|((area_id, area), (other_area_id, other_area))| {
998 let visible = areas_visible(*area, *other_area, vis_checker);
999 ((**area_id, **other_area_id), visible)
1000 })
1001 .collect();
1002 if safe_to_file {
1003 let mut file = create_file_with_parents(cache_path);
1004 serialize_into(&mut file, &visibility_cache).unwrap();
1005 }
1006 visibility_cache
1007 }
1008}
1009
1010pub fn areas_walkable<T: AreaLike>(area1: &T, area2: &T, walk_checker: &CollisionChecker) -> bool {
1016 let height = if area1.requires_crouch() || area2.requires_crouch() {
1017 PLAYER_CROUCH_HEIGHT
1018 } else {
1019 PLAYER_HEIGHT
1020 };
1021 let width = 0.9 * PLAYER_WIDTH;
1023
1024 let area1_centroid = area1.centroid();
1025 let area2_centroid = area2.centroid();
1026
1027 let dx = area2_centroid.x - area1_centroid.x;
1028 let dy = area2_centroid.y - area1_centroid.y;
1029 let angle = dx.atan2(dy);
1030
1031 for (width_correction, height_correction) in iproduct!([width / 2.0, -width / 2.0], [height]) {
1032 let dx_corr = width_correction * angle.cos();
1033 let dy_corr = width_correction * angle.sin();
1034
1035 let used_centroid1 = Position::new(
1036 area1_centroid.x + dx_corr,
1037 area1_centroid.y + dy_corr,
1038 area1_centroid.z + height_correction,
1039 );
1040 let used_centroid2 = Position::new(
1041 area2_centroid.x + dx_corr,
1042 area2_centroid.y + dy_corr,
1043 area2_centroid.z + height_correction,
1044 );
1045 if !walk_checker.connection_unobstructed(used_centroid1, used_centroid2) {
1046 return false;
1047 }
1048 }
1049 true
1050}
1051
1052#[allow(dead_code)]
1059#[must_use]
1060fn get_walkability_cache(
1061 map_name: &str,
1062 granularity: usize,
1063 nav: &Nav,
1064 walk_checker: &CollisionChecker,
1065) -> HashMap<(u32, u32), bool> {
1066 let tqdm_config = Config::new().with_leave(true);
1067 let cache_path_str =
1068 format!("./data/collisions/{map_name}_{granularity}_walkability_cache.json");
1069 let cache_path = Path::new(&cache_path_str);
1070 if cache_path.exists() {
1071 let file = File::open(cache_path).unwrap();
1072 serde_json::from_reader(file).unwrap()
1073 } else {
1074 let mut file = create_file_with_parents(cache_path);
1075 let mut walkability_cache = HashMap::default();
1076 for ((area_id, area), (other_area_id, other_area)) in iproduct!(&nav.areas, &nav.areas)
1077 .tqdm_config(tqdm_config.with_desc("Building walkability cache"))
1078 {
1079 let visible = areas_walkable(area, other_area, walk_checker);
1080 walkability_cache.insert((*area_id, *other_area_id), visible);
1081 }
1082 serde_json::to_writer(&mut file, &walkability_cache).unwrap();
1083 walkability_cache
1084 }
1085}
1086
1087#[derive(Debug, Clone, Deserialize, Serialize)]
1089struct NewNavArea {
1090 pub area_id: u32,
1091 pub dynamic_attribute_flags: DynamicAttributeFlags,
1092 pub corners: Vec<Position>,
1093 pub connections: HashSet<u32>,
1094 pub ladders_above: HashSet<u32>,
1095 pub ladders_below: HashSet<u32>,
1096 pub orig_ids: HashSet<u32>,
1097 centroid: Position,
1098}
1099
1100impl NewNavArea {
1101 pub fn new(
1102 corners: Vec<Position>,
1103 orig_ids: HashSet<u32>,
1104 ladders_above: HashSet<u32>,
1105 ladders_below: HashSet<u32>,
1106 dynamic_attribute_flags: DynamicAttributeFlags,
1107 connections: HashSet<u32>,
1108 ) -> Self {
1109 let centroid = centroid(&corners);
1110 Self {
1111 area_id: 0,
1112 dynamic_attribute_flags,
1113 corners,
1114 connections,
1115 ladders_above,
1116 ladders_below,
1117 orig_ids,
1118 centroid,
1119 }
1120 }
1121}
1122
1123impl AreaLike for NewNavArea {
1124 fn centroid(&self) -> Position {
1125 self.centroid
1126 }
1127 fn requires_crouch(&self) -> bool {
1128 self.dynamic_attribute_flags == CROUCHING_ATTRIBUTE_FLAG
1129 }
1130
1131 fn area_id(&self) -> u32 {
1132 self.area_id
1133 }
1134}
1135
1136#[derive(Debug, Clone)]
1137struct AdditionalNavAreaInfo {
1138 pub polygon: Polygon,
1139 pub z_level: f64,
1140}
1141
1142#[allow(clippy::cast_precision_loss)]
1144fn create_new_nav_areas(
1145 nav_areas: &HashMap<u32, NavArea>,
1146 grid_granularity: usize,
1147 xs: &[f64],
1148 ys: &[f64],
1149 area_extra_info: &HashMap<u32, AdditionalNavAreaInfo>,
1150 tqdm_config: Config,
1151) -> (Vec<NewNavArea>, HashMap<u32, HashSet<u32>>) {
1152 let min_x = *xs.iter().min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
1154 let max_x = *xs.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
1155 let min_y = *ys.iter().min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
1156 let max_y = *ys.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
1157
1158 let cell_width = (max_x - min_x) / grid_granularity as f64;
1160 let cell_height = (max_y - min_y) / grid_granularity as f64;
1161
1162 let mut new_cells: Vec<NewNavArea> = Vec::new();
1163
1164 for (i, j) in iproduct!(0..grid_granularity, 0..grid_granularity)
1166 .tqdm_config(tqdm_config.with_desc("Creating grid cell"))
1167 {
1168 let cell_min_x = (j as f64).mul_add(cell_width, min_x);
1170 let cell_min_y = (i as f64).mul_add(cell_height, min_y);
1171 let cell_max_x = cell_min_x + cell_width;
1172 let cell_max_y = cell_min_y + cell_height;
1173 let center_x = (cell_min_x + cell_max_x) / 2.0;
1174 let center_y = (cell_min_y + cell_max_y) / 2.0;
1175 let center_point = Point::new(center_x, center_y);
1176
1177 let cell_poly = Polygon::new(
1178 LineString::from(vec![
1179 (cell_min_x, cell_min_y),
1180 (cell_max_x, cell_min_y),
1181 (cell_max_x, cell_max_y),
1182 (cell_min_x, cell_max_y),
1183 ]),
1184 vec![],
1185 );
1186
1187 let mut primary_origs: HashSet<u32> = HashSet::default();
1192 let mut extra_orig_ids: HashSet<u32> = HashSet::default();
1193 for (area_id, info) in area_extra_info {
1194 if info.polygon.contains(¢er_point) {
1195 primary_origs.insert(*area_id);
1196 } else if info.polygon.intersects(&cell_poly) {
1197 extra_orig_ids.insert(*area_id);
1198 }
1199 }
1200
1201 if primary_origs.is_empty() && extra_orig_ids.is_empty() {
1203 continue;
1204 }
1205
1206 let primary_origs = if primary_origs.is_empty() {
1208 let min_id = extra_orig_ids.iter().min_by(|a, b| {
1209 let distance_a = Euclidean.distance(
1210 &area_extra_info[*a].polygon.centroid().unwrap(),
1211 ¢er_point,
1212 );
1213
1214 let distance_b = Euclidean.distance(
1215 &area_extra_info[*b].polygon.centroid().unwrap(),
1216 ¢er_point,
1217 );
1218 distance_a
1219 .partial_cmp(&distance_b)
1220 .unwrap_or(Ordering::Equal)
1221 });
1222 HashSet::from_iter([*min_id.unwrap()])
1223 } else {
1224 primary_origs
1225 };
1226
1227 for primary in primary_origs {
1229 let mut cell_orig_ids = HashSet::from_iter([primary]);
1230
1231 let primary_z =
1234 inverse_distance_weighting(&nav_areas[&primary].corners, (center_x, center_y));
1235
1236 for other in &extra_orig_ids {
1237 if *other != primary
1238 && (primary_z - area_extra_info[other].z_level).abs() <= JUMP_HEIGHT
1239 {
1240 cell_orig_ids.insert(*other);
1241 }
1242 }
1243
1244 let rep_level = (primary_z * 100.0).round() / 100.0;
1245 let corners = vec![
1246 Position::new(cell_min_x, cell_min_y, rep_level),
1247 Position::new(cell_max_x, cell_min_y, rep_level),
1248 Position::new(cell_max_x, cell_max_y, rep_level),
1249 Position::new(cell_min_x, cell_max_y, rep_level),
1250 ];
1251
1252 let primary_area = &nav_areas[&primary];
1253 new_cells.push(NewNavArea::new(
1254 corners,
1255 cell_orig_ids,
1256 HashSet::from_iter(primary_area.ladders_above.clone()),
1257 HashSet::from_iter(primary_area.ladders_below.clone()),
1258 primary_area.dynamic_attribute_flags,
1259 HashSet::default(),
1260 ));
1261 }
1262 }
1263 println!(); let old_to_new_children = build_old_to_new_mapping(&mut new_cells);
1266
1267 (new_cells, old_to_new_children)
1268}
1269
1270#[allow(clippy::cast_possible_truncation)]
1272fn build_old_to_new_mapping(new_cells: &mut [NewNavArea]) -> HashMap<u32, HashSet<u32>> {
1273 let mut old_to_new_children: HashMap<u32, HashSet<u32>> = HashMap::default();
1274
1275 for (idx, new_cell) in new_cells.iter_mut().enumerate() {
1276 new_cell.area_id = idx as u32;
1277 for orig_id in &new_cell.orig_ids {
1278 old_to_new_children
1279 .entry(*orig_id)
1280 .or_default()
1281 .insert(new_cell.area_id);
1282 }
1283 }
1284 old_to_new_children
1285}
1286
1287#[allow(clippy::cast_possible_truncation)]
1294#[allow(clippy::cast_precision_loss)]
1295#[must_use]
1296pub fn regularize_nav_areas(
1297 nav_areas: &HashMap<u32, NavArea>,
1298 grid_granularity: usize,
1299 walk_checker: &CollisionChecker,
1300) -> HashMap<u32, NavArea> {
1301 let tqdm_config = Config::new().with_leave(true);
1302
1303 let mut xs: Vec<f64> = Vec::new();
1304 let mut ys: Vec<f64> = Vec::new();
1305 let mut area_extra_info: HashMap<u32, AdditionalNavAreaInfo> = HashMap::default();
1306
1307 for (area_id, area) in nav_areas {
1309 let coords: Vec<(f64, f64)> = area.corners.iter().map(|c| (c.x, c.y)).collect();
1310 let poly = Polygon::new(LineString::from(coords), vec![]);
1311 let avg_z: f64 =
1312 area.corners.iter().map(|corner| corner.z).sum::<f64>() / area.corners.len() as f64;
1313 area_extra_info.insert(
1314 *area_id,
1315 AdditionalNavAreaInfo {
1316 polygon: poly,
1317 z_level: avg_z,
1318 },
1319 );
1320
1321 for corner in &area.corners {
1322 xs.push(corner.x);
1323 ys.push(corner.y);
1324 }
1325 }
1326
1327 if xs.is_empty() || ys.is_empty() {
1328 return HashMap::default();
1329 }
1330
1331 let (mut new_nav_areas, old_to_new_children) = create_new_nav_areas(
1333 nav_areas,
1334 grid_granularity,
1335 &xs,
1336 &ys,
1337 &area_extra_info,
1338 tqdm_config.clone(),
1339 );
1340
1341 add_connections_by_reachability(&mut new_nav_areas, walk_checker, tqdm_config.clone());
1348
1349 ensure_inter_area_connections(
1350 &mut new_nav_areas,
1351 nav_areas,
1352 &old_to_new_children,
1353 tqdm_config,
1354 );
1355
1356 new_nav_areas
1357 .into_iter()
1358 .enumerate()
1359 .map(|(idx, area)| (idx as u32, area.into()))
1360 .collect()
1361}
1362
1363fn ensure_inter_area_connections(
1366 new_nav_areas: &mut [NewNavArea],
1367 nav_areas: &HashMap<u32, NavArea>,
1368 old_to_new_children: &HashMap<u32, HashSet<u32>>,
1369 tqdm_config: Config,
1370) {
1371 for (a_idx, area_a) in nav_areas
1373 .iter()
1374 .tqdm_config(tqdm_config.with_desc("Ensuring old connections"))
1375 {
1376 let Some(children_of_a) = old_to_new_children.get(a_idx) else {
1380 continue;
1381 };
1382 for neighbor_of_a_idx in &area_a.connections {
1383 let Some(children_of_neighbor_of_a) = old_to_new_children.get(neighbor_of_a_idx) else {
1384 continue;
1385 };
1386
1387 let mut neighbors_of_children_of_a: HashSet<&u32> = HashSet::from_iter(children_of_a);
1388 for child_of_a in children_of_a {
1389 neighbors_of_children_of_a.extend(&new_nav_areas[*child_of_a as usize].connections);
1390 }
1391
1392 if children_of_neighbor_of_a
1393 .iter()
1394 .any(|x| neighbors_of_children_of_a.contains(x))
1395 {
1396 continue;
1398 }
1399
1400 let pairs_of_children =
1401 iproduct!(children_of_a.iter(), children_of_neighbor_of_a.iter());
1402
1403 let pairs_of_children = pairs_of_children.sorted_by(|pair_a, pair_b| {
1404 new_nav_areas[*pair_a.0 as usize]
1405 .centroid()
1406 .distance_2d(&new_nav_areas[*pair_a.1 as usize].centroid())
1407 .partial_cmp(
1408 &new_nav_areas[*pair_b.0 as usize]
1409 .centroid()
1410 .distance_2d(&new_nav_areas[*pair_b.1 as usize].centroid()),
1411 )
1412 .unwrap()
1413 });
1414
1415 for pair_of_children in pairs_of_children.take(3) {
1420 new_nav_areas
1421 .get_mut(*pair_of_children.0 as usize)
1422 .unwrap()
1423 .connections
1424 .insert(*pair_of_children.1);
1425 }
1426 }
1427 }
1428 println!();
1429 }
1431
1432fn add_connections_by_reachability(
1436 new_nav_areas: &mut Vec<NewNavArea>,
1437 walk_checker: &CollisionChecker,
1438 tqdm_config: Config,
1439) {
1440 let new_connections: Vec<HashSet<u32>> = new_nav_areas
1441 .par_iter()
1442 .tqdm_config(tqdm_config.with_desc("Connections from reachability"))
1443 .map(|area| {
1444 let mut conns = HashSet::default();
1445 for other_area in &*new_nav_areas {
1446 if area.area_id == other_area.area_id
1447 || area.connections.contains(&other_area.area_id)
1448 {
1449 continue;
1450 }
1451
1452 if (!area.ladders_above.is_disjoint(&other_area.ladders_below))
1453 || (!area.ladders_below.is_disjoint(&other_area.ladders_above))
1454 || (area.centroid().can_jump_to(&other_area.centroid())
1455 && areas_walkable(area, other_area, walk_checker))
1456 {
1457 conns.insert(other_area.area_id);
1458 }
1459 }
1460 conns
1461 })
1462 .collect();
1463 for (area, conns) in new_nav_areas.iter_mut().zip(new_connections) {
1464 area.connections.extend(conns);
1465 }
1466 println!();
1467 }
1469
1470#[allow(dead_code)]
1476fn add_intra_area_connections(
1477 new_nav_areas: &mut [NewNavArea],
1478 old_to_new_children: &HashMap<u32, HashSet<u32>>,
1479 tqdm_config: Config,
1480) {
1481 for new_area in &mut new_nav_areas
1482 .iter_mut()
1483 .tqdm_config(tqdm_config.with_desc("Connections from inheritance"))
1484 {
1485 let parent_areas = &new_area.orig_ids;
1486 for parent_area in parent_areas {
1487 let siblings = &old_to_new_children[parent_area];
1488
1489 for sibling in siblings {
1490 if *sibling != new_area.area_id {
1491 new_area.connections.insert(*sibling);
1492 }
1493 }
1494 }
1495 }
1496 println!(); }
1498
1499#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash, Copy, IntoPyObject)]
1500pub struct GroupId(u32);
1501
1502#[allow(clippy::cast_possible_truncation)]
1518#[allow(clippy::cast_sign_loss)]
1519pub fn group_nav_areas(nav_areas: &[&NavArea], group_size: usize) -> HashMap<u32, GroupId> {
1520 println!("Grouping areas");
1521 let mut block_map: HashMap<(usize, usize), Vec<&NavArea>> = HashMap::default();
1522
1523 let min_x = nav_areas
1526 .iter()
1527 .min_by(|a, b| a.centroid.x.partial_cmp(&b.centroid.x).unwrap())
1528 .unwrap()
1529 .centroid
1530 .x;
1531 let min_y = nav_areas
1532 .iter()
1533 .min_by(|a, b| a.centroid.y.partial_cmp(&b.centroid.y).unwrap())
1534 .unwrap()
1535 .centroid
1536 .y;
1537
1538 let first_area = nav_areas.first().unwrap();
1541 let tile_min_x = first_area
1542 .corners
1543 .iter()
1544 .map(|c| c.x)
1545 .fold(f64::INFINITY, f64::min);
1546 let tile_min_y = first_area
1547 .corners
1548 .iter()
1549 .map(|c| c.y)
1550 .fold(f64::INFINITY, f64::min);
1551 let tile_max_x = first_area
1552 .corners
1553 .iter()
1554 .map(|c| c.x)
1555 .fold(f64::NEG_INFINITY, f64::max);
1556 let tile_max_y = first_area
1557 .corners
1558 .iter()
1559 .map(|c| c.y)
1560 .fold(f64::NEG_INFINITY, f64::max);
1561
1562 let delta_x = tile_max_x - tile_min_x;
1563 let delta_y = tile_max_y - tile_min_y;
1564
1565 for area in nav_areas {
1567 let cell_x = ((area.centroid.x - min_x) / delta_x).round() as usize;
1568 let cell_y = ((area.centroid.y - min_y) / delta_y).round() as usize;
1569 block_map
1570 .entry((cell_x / group_size, cell_y / group_size))
1571 .or_default()
1572 .push(area);
1573 }
1574
1575 let sorted_blocks: Vec<Vec<&NavArea>> = block_map
1577 .into_iter()
1578 .sorted_by_key(|(k, _v)| *k)
1579 .map(|(_, v)| v)
1580 .collect();
1581
1582 let mut area_to_group: HashMap<u32, GroupId> = HashMap::default();
1584 let mut next_group_id: u32 = 0;
1585
1586 for mut areas in sorted_blocks {
1588 areas.sort_by_key(|a| {
1589 let cell_x = ((a.centroid.x - min_x) / delta_x).round() as usize;
1590 let cell_y = ((a.centroid.y - min_y) / delta_y).round() as usize;
1591 (cell_x, cell_y, a.area_id)
1592 });
1593
1594 let mut z_groups: Vec<Vec<&NavArea>> = Vec::new();
1596 for area in areas {
1597 let cell_coord = (
1598 ((area.centroid.x - min_x) / delta_x).round() as usize,
1599 ((area.centroid.y - min_y) / delta_y).round() as usize,
1600 );
1601 let mut found = false;
1602
1603 for group in &mut z_groups {
1604 if group.iter().any(|a| {
1607 let ax = ((a.centroid.x - min_x) / delta_x).round() as usize;
1608 let ay = ((a.centroid.y - min_y) / delta_y).round() as usize;
1609 (ax, ay) == cell_coord
1610 }) {
1611 continue;
1612 }
1613
1614 if group
1616 .iter()
1617 .all(|a| (a.centroid.z - area.centroid.z).abs() <= JUMP_HEIGHT)
1618 {
1619 group.push(area);
1620 found = true;
1621 break;
1622 }
1623 }
1624
1625 if !found {
1627 z_groups.push(vec![area]);
1628 }
1629 }
1630
1631 for group in z_groups {
1633 let group_id = next_group_id;
1634 next_group_id += 1;
1635 for area in group {
1637 area_to_group.insert(area.area_id, GroupId(group_id));
1638 }
1639 }
1640 }
1641
1642 area_to_group
1643}
1644
1645#[pyfunction]
1646#[allow(clippy::needless_pass_by_value)]
1647#[pyo3(name = "regularize_nav_areas")]
1648#[must_use]
1649pub fn py_regularize_nav_areas(
1650 nav_areas: HashMap<u32, NavArea>,
1651 grid_granularity: usize,
1652 walk_checker: &CollisionChecker,
1653) -> HashMap<u32, NavArea> {
1654 regularize_nav_areas(&nav_areas, grid_granularity, walk_checker)
1655}
1656
1657#[pyfunction]
1658#[allow(clippy::needless_pass_by_value)]
1659#[pyo3(name = "group_nav_areas")]
1660#[must_use]
1661pub fn py_group_nav_areas(nav_areas: Vec<NavArea>, group_size: usize) -> HashMap<u32, GroupId> {
1662 let nav_refs: Vec<&NavArea> = nav_areas.iter().collect();
1663 group_nav_areas(&nav_refs, group_size)
1664}