1use crate::collisions::CollisionChecker;
5use crate::constants::{
6 CROUCHING_ATTRIBUTE_FLAG, CROUCHING_SPEED, JUMP_HEIGHT, LADDER_SPEED, PLAYER_CROUCH_HEIGHT,
7 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
940fn areas_visible<T: AreaLike>(area1: &T, area2: &T, vis_checker: &CollisionChecker) -> bool {
946 let height_correction = PLAYER_EYE_LEVEL;
947
948 let area1_centroid = area1.centroid();
949 let area2_centroid = area2.centroid();
950
951 let used_centroid1 = Position::new(
952 area1_centroid.x,
953 area1_centroid.y,
954 area1_centroid.z + height_correction,
955 );
956 let used_centroid2 = Position::new(
957 area2_centroid.x,
958 area2_centroid.y,
959 area2_centroid.z + height_correction,
960 );
961
962 vis_checker.connection_unobstructed(used_centroid1, used_centroid2)
963}
964
965#[must_use]
972pub fn get_visibility_cache(
973 map_name: &str,
974 granularity: usize,
975 nav: &Nav,
976 vis_checker: &CollisionChecker,
977 safe_to_file: bool,
978) -> HashMap<(u32, u32), bool> {
979 let tqdm_config = Config::new().with_leave(true);
980 let cache_path_str =
981 format!("./data/collisions/{map_name}_{granularity}_visibility_cache.vis_cache");
982 let cache_path = Path::new(&cache_path_str);
983 if cache_path.exists() {
984 println!("Loading visibility cache from binary.");
985 let file = File::open(cache_path).unwrap();
986 deserialize_from(file).unwrap()
987 } else {
988 println!("Building visibility cache from scratch.");
989 let visibility_cache = iproduct!(&nav.areas, &nav.areas)
990 .collect::<Vec<_>>()
991 .par_iter()
992 .tqdm_config(tqdm_config.with_desc("Building visibility cache"))
993 .map(|((area_id, area), (other_area_id, other_area))| {
994 let visible = areas_visible(*area, *other_area, vis_checker);
995 ((**area_id, **other_area_id), visible)
996 })
997 .collect();
998 if safe_to_file {
999 let mut file = create_file_with_parents(cache_path);
1000 serialize_into(&mut file, &visibility_cache).unwrap();
1001 }
1002 visibility_cache
1003 }
1004}
1005
1006fn areas_walkable<T: AreaLike>(area1: &T, area2: &T, walk_checker: &CollisionChecker) -> bool {
1012 let height = if area1.requires_crouch() || area2.requires_crouch() {
1013 PLAYER_CROUCH_HEIGHT
1014 } else {
1015 PLAYER_HEIGHT
1016 };
1017 let width = 0.9 * PLAYER_WIDTH;
1019
1020 let area1_centroid = area1.centroid();
1021 let area2_centroid = area2.centroid();
1022
1023 let dx = area2_centroid.x - area1_centroid.x;
1024 let dy = area2_centroid.y - area1_centroid.y;
1025 let angle = dx.atan2(dy);
1026
1027 for (width_correction, height_correction) in iproduct!([width / 2.0, -width / 2.0], [height]) {
1028 let dx_corr = width_correction * angle.cos();
1029 let dy_corr = width_correction * angle.sin();
1030
1031 let used_centroid1 = Position::new(
1032 area1_centroid.x + dx_corr,
1033 area1_centroid.y + dy_corr,
1034 area1_centroid.z + height_correction,
1035 );
1036 let used_centroid2 = Position::new(
1037 area2_centroid.x + dx_corr,
1038 area2_centroid.y + dy_corr,
1039 area2_centroid.z + height_correction,
1040 );
1041 if !walk_checker.connection_unobstructed(used_centroid1, used_centroid2) {
1042 return false;
1043 }
1044 }
1045 true
1046}
1047
1048#[allow(dead_code)]
1055#[must_use]
1056fn get_walkability_cache(
1057 map_name: &str,
1058 granularity: usize,
1059 nav: &Nav,
1060 walk_checker: &CollisionChecker,
1061) -> HashMap<(u32, u32), bool> {
1062 let tqdm_config = Config::new().with_leave(true);
1063 let cache_path_str =
1064 format!("./data/collisions/{map_name}_{granularity}_walkability_cache.json");
1065 let cache_path = Path::new(&cache_path_str);
1066 if cache_path.exists() {
1067 let file = File::open(cache_path).unwrap();
1068 serde_json::from_reader(file).unwrap()
1069 } else {
1070 let mut file = create_file_with_parents(cache_path);
1071 let mut walkability_cache = HashMap::default();
1072 for ((area_id, area), (other_area_id, other_area)) in iproduct!(&nav.areas, &nav.areas)
1073 .tqdm_config(tqdm_config.with_desc("Building walkability cache"))
1074 {
1075 let visible = areas_walkable(area, other_area, walk_checker);
1076 walkability_cache.insert((*area_id, *other_area_id), visible);
1077 }
1078 serde_json::to_writer(&mut file, &walkability_cache).unwrap();
1079 walkability_cache
1080 }
1081}
1082
1083#[derive(Debug, Clone, Deserialize, Serialize)]
1085struct NewNavArea {
1086 pub area_id: u32,
1087 pub dynamic_attribute_flags: DynamicAttributeFlags,
1088 pub corners: Vec<Position>,
1089 pub connections: HashSet<u32>,
1090 pub ladders_above: HashSet<u32>,
1091 pub ladders_below: HashSet<u32>,
1092 pub orig_ids: HashSet<u32>,
1093 centroid: Position,
1094}
1095
1096impl NewNavArea {
1097 pub fn new(
1098 corners: Vec<Position>,
1099 orig_ids: HashSet<u32>,
1100 ladders_above: HashSet<u32>,
1101 ladders_below: HashSet<u32>,
1102 dynamic_attribute_flags: DynamicAttributeFlags,
1103 connections: HashSet<u32>,
1104 ) -> Self {
1105 let centroid = centroid(&corners);
1106 Self {
1107 area_id: 0,
1108 dynamic_attribute_flags,
1109 corners,
1110 connections,
1111 ladders_above,
1112 ladders_below,
1113 orig_ids,
1114 centroid,
1115 }
1116 }
1117}
1118
1119impl AreaLike for NewNavArea {
1120 fn centroid(&self) -> Position {
1121 self.centroid
1122 }
1123 fn requires_crouch(&self) -> bool {
1124 self.dynamic_attribute_flags == CROUCHING_ATTRIBUTE_FLAG
1125 }
1126
1127 fn area_id(&self) -> u32 {
1128 self.area_id
1129 }
1130}
1131
1132#[derive(Debug, Clone)]
1133struct AdditionalNavAreaInfo {
1134 pub polygon: Polygon,
1135 pub z_level: f64,
1136}
1137
1138#[allow(clippy::cast_precision_loss)]
1140fn create_new_nav_areas(
1141 nav_areas: &HashMap<u32, NavArea>,
1142 grid_granularity: usize,
1143 xs: &[f64],
1144 ys: &[f64],
1145 area_extra_info: &HashMap<u32, AdditionalNavAreaInfo>,
1146 tqdm_config: Config,
1147) -> (Vec<NewNavArea>, HashMap<u32, HashSet<u32>>) {
1148 let min_x = *xs.iter().min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
1150 let max_x = *xs.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
1151 let min_y = *ys.iter().min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
1152 let max_y = *ys.iter().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
1153
1154 let cell_width = (max_x - min_x) / grid_granularity as f64;
1156 let cell_height = (max_y - min_y) / grid_granularity as f64;
1157
1158 let mut new_cells: Vec<NewNavArea> = Vec::new();
1159
1160 for (i, j) in iproduct!(0..grid_granularity, 0..grid_granularity)
1162 .tqdm_config(tqdm_config.with_desc("Creating grid cell"))
1163 {
1164 let cell_min_x = (j as f64).mul_add(cell_width, min_x);
1166 let cell_min_y = (i as f64).mul_add(cell_height, min_y);
1167 let cell_max_x = cell_min_x + cell_width;
1168 let cell_max_y = cell_min_y + cell_height;
1169 let center_x = (cell_min_x + cell_max_x) / 2.0;
1170 let center_y = (cell_min_y + cell_max_y) / 2.0;
1171 let center_point = Point::new(center_x, center_y);
1172
1173 let cell_poly = Polygon::new(
1174 LineString::from(vec![
1175 (cell_min_x, cell_min_y),
1176 (cell_max_x, cell_min_y),
1177 (cell_max_x, cell_max_y),
1178 (cell_min_x, cell_max_y),
1179 ]),
1180 vec![],
1181 );
1182
1183 let mut primary_origs: HashSet<u32> = HashSet::default();
1188 let mut extra_orig_ids: HashSet<u32> = HashSet::default();
1189 for (area_id, info) in area_extra_info {
1190 if info.polygon.contains(¢er_point) {
1191 primary_origs.insert(*area_id);
1192 } else if info.polygon.intersects(&cell_poly) {
1193 extra_orig_ids.insert(*area_id);
1194 }
1195 }
1196
1197 if primary_origs.is_empty() && extra_orig_ids.is_empty() {
1199 continue;
1200 }
1201
1202 let primary_origs = if primary_origs.is_empty() {
1204 let min_id = extra_orig_ids.iter().min_by(|a, b| {
1205 let distance_a = Euclidean::distance(
1206 &area_extra_info[*a].polygon.centroid().unwrap(),
1207 ¢er_point,
1208 );
1209
1210 let distance_b = Euclidean::distance(
1211 &area_extra_info[*b].polygon.centroid().unwrap(),
1212 ¢er_point,
1213 );
1214 distance_a
1215 .partial_cmp(&distance_b)
1216 .unwrap_or(Ordering::Equal)
1217 });
1218 HashSet::from_iter([*min_id.unwrap()])
1219 } else {
1220 primary_origs
1221 };
1222
1223 for primary in primary_origs {
1225 let mut cell_orig_ids = HashSet::from_iter([primary]);
1226
1227 let primary_z =
1230 inverse_distance_weighting(&nav_areas[&primary].corners, (center_x, center_y));
1231
1232 for other in &extra_orig_ids {
1233 if *other != primary
1234 && (primary_z - area_extra_info[other].z_level).abs() <= JUMP_HEIGHT
1235 {
1236 cell_orig_ids.insert(*other);
1237 }
1238 }
1239
1240 let rep_level = (primary_z * 100.0).round() / 100.0;
1241 let corners = vec![
1242 Position::new(cell_min_x, cell_min_y, rep_level),
1243 Position::new(cell_max_x, cell_min_y, rep_level),
1244 Position::new(cell_max_x, cell_max_y, rep_level),
1245 Position::new(cell_min_x, cell_max_y, rep_level),
1246 ];
1247
1248 let primary_area = &nav_areas[&primary];
1249 new_cells.push(NewNavArea::new(
1250 corners,
1251 cell_orig_ids,
1252 HashSet::from_iter(primary_area.ladders_above.clone()),
1253 HashSet::from_iter(primary_area.ladders_below.clone()),
1254 primary_area.dynamic_attribute_flags,
1255 HashSet::default(),
1256 ));
1257 }
1258 }
1259 println!(); let old_to_new_children = build_old_to_new_mapping(&mut new_cells);
1262
1263 (new_cells, old_to_new_children)
1264}
1265
1266#[allow(clippy::cast_possible_truncation)]
1268fn build_old_to_new_mapping(new_cells: &mut [NewNavArea]) -> HashMap<u32, HashSet<u32>> {
1269 let mut old_to_new_children: HashMap<u32, HashSet<u32>> = HashMap::default();
1270
1271 for (idx, new_cell) in new_cells.iter_mut().enumerate() {
1272 new_cell.area_id = idx as u32;
1273 for orig_id in &new_cell.orig_ids {
1274 old_to_new_children
1275 .entry(*orig_id)
1276 .or_default()
1277 .insert(new_cell.area_id);
1278 }
1279 }
1280 old_to_new_children
1281}
1282
1283#[allow(clippy::cast_possible_truncation)]
1290#[allow(clippy::cast_precision_loss)]
1291#[must_use]
1292pub fn regularize_nav_areas(
1293 nav_areas: &HashMap<u32, NavArea>,
1294 grid_granularity: usize,
1295 walk_checker: &CollisionChecker,
1296) -> HashMap<u32, NavArea> {
1297 let tqdm_config = Config::new().with_leave(true);
1298
1299 let mut xs: Vec<f64> = Vec::new();
1300 let mut ys: Vec<f64> = Vec::new();
1301 let mut area_extra_info: HashMap<u32, AdditionalNavAreaInfo> = HashMap::default();
1302
1303 for (area_id, area) in nav_areas {
1305 let coords: Vec<(f64, f64)> = area.corners.iter().map(|c| (c.x, c.y)).collect();
1306 let poly = Polygon::new(LineString::from(coords), vec![]);
1307 let avg_z: f64 =
1308 area.corners.iter().map(|corner| corner.z).sum::<f64>() / area.corners.len() as f64;
1309 area_extra_info.insert(
1310 *area_id,
1311 AdditionalNavAreaInfo {
1312 polygon: poly,
1313 z_level: avg_z,
1314 },
1315 );
1316
1317 for corner in &area.corners {
1318 xs.push(corner.x);
1319 ys.push(corner.y);
1320 }
1321 }
1322
1323 if xs.is_empty() || ys.is_empty() {
1324 return HashMap::default();
1325 }
1326
1327 let (mut new_nav_areas, old_to_new_children) = create_new_nav_areas(
1329 nav_areas,
1330 grid_granularity,
1331 &xs,
1332 &ys,
1333 &area_extra_info,
1334 tqdm_config.clone(),
1335 );
1336
1337 add_connections_by_reachability(&mut new_nav_areas, walk_checker, tqdm_config.clone());
1344
1345 ensure_inter_area_connections(
1346 &mut new_nav_areas,
1347 nav_areas,
1348 &old_to_new_children,
1349 tqdm_config,
1350 );
1351
1352 new_nav_areas
1353 .into_iter()
1354 .enumerate()
1355 .map(|(idx, area)| (idx as u32, area.into()))
1356 .collect()
1357}
1358
1359fn ensure_inter_area_connections(
1362 new_nav_areas: &mut [NewNavArea],
1363 nav_areas: &HashMap<u32, NavArea>,
1364 old_to_new_children: &HashMap<u32, HashSet<u32>>,
1365 tqdm_config: Config,
1366) {
1367 for (a_idx, area_a) in nav_areas
1369 .iter()
1370 .tqdm_config(tqdm_config.with_desc("Ensuring old connections"))
1371 {
1372 let Some(children_of_a) = old_to_new_children.get(a_idx) else {
1376 continue;
1377 };
1378 for neighbor_of_a_idx in &area_a.connections {
1379 let Some(children_of_neighbor_of_a) = old_to_new_children.get(neighbor_of_a_idx) else {
1380 continue;
1381 };
1382
1383 let mut neighbors_of_children_of_a: HashSet<&u32> = HashSet::from_iter(children_of_a);
1384 for child_of_a in children_of_a {
1385 neighbors_of_children_of_a.extend(&new_nav_areas[*child_of_a as usize].connections);
1386 }
1387
1388 if children_of_neighbor_of_a
1389 .iter()
1390 .any(|x| neighbors_of_children_of_a.contains(x))
1391 {
1392 continue;
1394 }
1395
1396 let pairs_of_children =
1397 iproduct!(children_of_a.iter(), children_of_neighbor_of_a.iter());
1398
1399 let pairs_of_children = pairs_of_children.sorted_by(|pair_a, pair_b| {
1400 new_nav_areas[*pair_a.0 as usize]
1401 .centroid()
1402 .distance_2d(&new_nav_areas[*pair_a.1 as usize].centroid())
1403 .partial_cmp(
1404 &new_nav_areas[*pair_b.0 as usize]
1405 .centroid()
1406 .distance_2d(&new_nav_areas[*pair_b.1 as usize].centroid()),
1407 )
1408 .unwrap()
1409 });
1410
1411 for pair_of_children in pairs_of_children.take(3) {
1416 new_nav_areas
1417 .get_mut(*pair_of_children.0 as usize)
1418 .unwrap()
1419 .connections
1420 .insert(*pair_of_children.1);
1421 }
1422 }
1423 }
1424 println!();
1425 }
1427
1428fn add_connections_by_reachability(
1432 new_nav_areas: &mut Vec<NewNavArea>,
1433 walk_checker: &CollisionChecker,
1434 tqdm_config: Config,
1435) {
1436 let new_connections: Vec<HashSet<u32>> = new_nav_areas
1437 .par_iter()
1438 .tqdm_config(tqdm_config.with_desc("Connections from reachability"))
1439 .map(|area| {
1440 let mut conns = HashSet::default();
1441 for other_area in &*new_nav_areas {
1442 if area.area_id == other_area.area_id
1443 || area.connections.contains(&other_area.area_id)
1444 {
1445 continue;
1446 }
1447
1448 if (!area.ladders_above.is_disjoint(&other_area.ladders_below))
1449 || (!area.ladders_below.is_disjoint(&other_area.ladders_above))
1450 || (area.centroid().can_jump_to(&other_area.centroid())
1451 && areas_walkable(area, other_area, walk_checker))
1452 {
1453 conns.insert(other_area.area_id);
1454 }
1455 }
1456 conns
1457 })
1458 .collect();
1459 for (area, conns) in new_nav_areas.iter_mut().zip(new_connections) {
1460 area.connections.extend(conns);
1461 }
1462 println!();
1463 }
1465
1466#[allow(dead_code)]
1472fn add_intra_area_connections(
1473 new_nav_areas: &mut [NewNavArea],
1474 old_to_new_children: &HashMap<u32, HashSet<u32>>,
1475 tqdm_config: Config,
1476) {
1477 for new_area in &mut new_nav_areas
1478 .iter_mut()
1479 .tqdm_config(tqdm_config.with_desc("Connections from inheritance"))
1480 {
1481 let parent_areas = &new_area.orig_ids;
1482 for parent_area in parent_areas {
1483 let siblings = &old_to_new_children[parent_area];
1484
1485 for sibling in siblings {
1486 if *sibling != new_area.area_id {
1487 new_area.connections.insert(*sibling);
1488 }
1489 }
1490 }
1491 }
1492 println!(); }
1494
1495#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash, Copy, IntoPyObject)]
1496pub struct GroupId(u32);
1497
1498#[allow(clippy::cast_possible_truncation)]
1514#[allow(clippy::cast_sign_loss)]
1515pub fn group_nav_areas(nav_areas: &[&NavArea], group_size: usize) -> HashMap<u32, GroupId> {
1516 println!("Grouping areas");
1517 let mut block_map: HashMap<(usize, usize), Vec<&NavArea>> = HashMap::default();
1518
1519 let min_x = nav_areas
1522 .iter()
1523 .min_by(|a, b| a.centroid.x.partial_cmp(&b.centroid.x).unwrap())
1524 .unwrap()
1525 .centroid
1526 .x;
1527 let min_y = nav_areas
1528 .iter()
1529 .min_by(|a, b| a.centroid.y.partial_cmp(&b.centroid.y).unwrap())
1530 .unwrap()
1531 .centroid
1532 .y;
1533
1534 let first_area = nav_areas.first().unwrap();
1537 let tile_min_x = first_area
1538 .corners
1539 .iter()
1540 .map(|c| c.x)
1541 .fold(f64::INFINITY, f64::min);
1542 let tile_min_y = first_area
1543 .corners
1544 .iter()
1545 .map(|c| c.y)
1546 .fold(f64::INFINITY, f64::min);
1547 let tile_max_x = first_area
1548 .corners
1549 .iter()
1550 .map(|c| c.x)
1551 .fold(f64::NEG_INFINITY, f64::max);
1552 let tile_max_y = first_area
1553 .corners
1554 .iter()
1555 .map(|c| c.y)
1556 .fold(f64::NEG_INFINITY, f64::max);
1557
1558 let delta_x = tile_max_x - tile_min_x;
1559 let delta_y = tile_max_y - tile_min_y;
1560
1561 for area in nav_areas {
1563 let cell_x = ((area.centroid.x - min_x) / delta_x).round() as usize;
1564 let cell_y = ((area.centroid.y - min_y) / delta_y).round() as usize;
1565 block_map
1566 .entry((cell_x / group_size, cell_y / group_size))
1567 .or_default()
1568 .push(area);
1569 }
1570
1571 let sorted_blocks: Vec<Vec<&NavArea>> = block_map
1573 .into_iter()
1574 .sorted_by_key(|(k, _v)| *k)
1575 .map(|(_, v)| v)
1576 .collect();
1577
1578 let mut area_to_group: HashMap<u32, GroupId> = HashMap::default();
1580 let mut next_group_id: u32 = 0;
1581
1582 for mut areas in sorted_blocks {
1584 areas.sort_by_key(|a| {
1585 let cell_x = ((a.centroid.x - min_x) / delta_x).round() as usize;
1586 let cell_y = ((a.centroid.y - min_y) / delta_y).round() as usize;
1587 (cell_x, cell_y, a.area_id)
1588 });
1589
1590 let mut z_groups: Vec<Vec<&NavArea>> = Vec::new();
1592 for area in areas {
1593 let cell_coord = (
1594 ((area.centroid.x - min_x) / delta_x).round() as usize,
1595 ((area.centroid.y - min_y) / delta_y).round() as usize,
1596 );
1597 let mut found = false;
1598
1599 for group in &mut z_groups {
1600 if group.iter().any(|a| {
1603 let ax = ((a.centroid.x - min_x) / delta_x).round() as usize;
1604 let ay = ((a.centroid.y - min_y) / delta_y).round() as usize;
1605 (ax, ay) == cell_coord
1606 }) {
1607 continue;
1608 }
1609
1610 if group
1612 .iter()
1613 .all(|a| (a.centroid.z - area.centroid.z).abs() <= JUMP_HEIGHT)
1614 {
1615 group.push(area);
1616 found = true;
1617 break;
1618 }
1619 }
1620
1621 if !found {
1623 z_groups.push(vec![area]);
1624 }
1625 }
1626
1627 for group in z_groups {
1629 let group_id = next_group_id;
1630 next_group_id += 1;
1631 for area in group {
1633 area_to_group.insert(area.area_id, GroupId(group_id));
1634 }
1635 }
1636 }
1637
1638 area_to_group
1639}
1640
1641#[pyfunction]
1642#[allow(clippy::needless_pass_by_value)]
1643#[pyo3(name = "regularize_nav_areas")]
1644#[must_use]
1645pub fn py_regularize_nav_areas(
1646 nav_areas: HashMap<u32, NavArea>,
1647 grid_granularity: usize,
1648 walk_checker: &CollisionChecker,
1649) -> HashMap<u32, NavArea> {
1650 regularize_nav_areas(&nav_areas, grid_granularity, walk_checker)
1651}
1652
1653#[pyfunction]
1654#[allow(clippy::needless_pass_by_value)]
1655#[pyo3(name = "group_nav_areas")]
1656#[must_use]
1657pub fn py_group_nav_areas(nav_areas: Vec<NavArea>, group_size: usize) -> HashMap<u32, GroupId> {
1658 let nav_refs: Vec<&NavArea> = nav_areas.iter().collect();
1659 group_nav_areas(&nav_refs, group_size)
1660}