cs2_nav/
spread.rs

1/// Module for modelling the spread of players after the round starts and when they can see each other.
2use crate::nav::{AreaIdent, AreaLike, GroupId, Nav, NavArea, PathResult, areas_audible};
3use crate::position::Position;
4use crate::utils::create_file_with_parents;
5use core::f64;
6use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
7use rayon::prelude::ParallelSliceMut;
8use rustc_hash::FxHashMap as HashMap;
9use rustc_hash::FxHashSet as HashSet;
10use serde::{Deserialize, Serialize};
11use simple_tqdm::{Config, ParTqdm, Tqdm};
12use std::fs::File;
13use std::iter;
14use std::mem;
15use std::path::Path;
16
17/// Struct for the positions of CT and T spawn points.
18#[derive(Debug, Clone, Deserialize, Serialize)]
19#[allow(non_snake_case)]
20pub struct Spawns {
21    CT: Vec<Position>,
22    T: Vec<Position>,
23}
24
25pub enum Perceivability {
26    Visibility(HashMap<(u32, u32), bool>),
27    Audibility,
28}
29
30impl Spawns {
31    /// Read the spawn points from a JSON file.
32    ///
33    /// # Panics
34    ///
35    /// Will panic if the file cannot be opened or the JSON cannot be deserialized.
36    #[must_use]
37    pub fn from_json(filename: &Path) -> Self {
38        let file = File::open(filename).unwrap();
39        serde_json::from_reader(&file).unwrap()
40    }
41}
42
43/// Struct for the distance of an area from a collection of spawn points.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct SpawnDistance {
46    /// Nav area for which this represents the distance.
47    area: NavArea,
48    /// Distance to the closest spawn point in the considered collection.
49    distance: f64,
50    /// Path to the closest spawn point in the considered collection.
51    path: Vec<u32>,
52}
53
54/// Only the parts of the `SpawnDistance` struct that are needed for the spread plotting.
55#[derive(Clone, Debug, Serialize, Deserialize)]
56pub struct ReducedSpawnDistance {
57    area: u32,
58    path: Vec<u32>,
59}
60
61impl From<&SpawnDistance> for ReducedSpawnDistance {
62    fn from(spawn_distance: &SpawnDistance) -> Self {
63        Self {
64            area: spawn_distance.area.area_id,
65            path: spawn_distance.path.clone(),
66        }
67    }
68}
69
70/// Struct for the distances of all areas from CT and T spawn points.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72#[allow(non_snake_case)]
73pub struct SpawnDistances {
74    pub CT: Vec<SpawnDistance>,
75    pub T: Vec<SpawnDistance>,
76}
77
78impl SpawnDistances {
79    /// Read the spawn distances from a JSON file.
80    ///
81    /// # Panics
82    ///
83    /// Will panic if a centroid comparison returns `None`. Basically if there is a NaN somewhere.
84    #[must_use]
85    pub fn from_json(filename: &Path) -> Self {
86        let file = File::open(filename).unwrap();
87        serde_json::from_reader(&file).unwrap()
88    }
89
90    /// Save the spawn distances to a JSON file.
91    ///
92    /// # Panics
93    ///
94    /// Will panic if the file cannot be created or the JSON cannot be serialized.
95    pub fn save_to_json(self, filename: &Path) {
96        let mut file = create_file_with_parents(filename);
97        serde_json::to_writer(&mut file, &self).unwrap();
98    }
99}
100
101/// For each area in `map_areas`, find the distances and paths to CT and T spawns.
102///
103/// The contents of each vector are sorted by distance.
104///
105/// # Panics
106///
107/// Will panic if any pathfinding yields a NaN distance.
108#[must_use]
109pub fn get_distances_from_spawns(map_areas: &Nav, spawns: &Spawns) -> SpawnDistances {
110    println!("Getting distances from spawns.");
111    let tqdm_config = Config::new().with_leave(true);
112
113    let distances: Vec<(SpawnDistance, SpawnDistance)> = map_areas
114        .areas
115        .values()
116        .collect::<Vec<_>>()
117        .par_iter()
118        .tqdm_config(tqdm_config.with_desc("Getting distances per spawn."))
119        .map(|area| {
120            let ct_path = spawns
121                .CT
122                .iter()
123                .map(|&spawn_point| {
124                    map_areas.find_path(AreaIdent::Pos(spawn_point), AreaIdent::Id(area.area_id))
125                })
126                .min_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap())
127                .unwrap_or(PathResult {
128                    distance: f64::MAX,
129                    path: Vec::new(),
130                });
131
132            let t_path = spawns
133                .T
134                .iter()
135                .map(|&spawn_point| {
136                    map_areas.find_path(AreaIdent::Pos(spawn_point), AreaIdent::Id(area.area_id))
137                })
138                .min_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap())
139                .unwrap_or(PathResult {
140                    distance: f64::MAX,
141                    path: Vec::new(),
142                });
143
144            (
145                SpawnDistance {
146                    area: (*area).clone(),
147                    distance: ct_path.distance,
148                    path: ct_path.path.iter().map(|a| a.area_id).collect(),
149                },
150                SpawnDistance {
151                    area: (*area).clone(),
152                    distance: t_path.distance,
153                    path: t_path.path.iter().map(|a| a.area_id).collect(),
154                },
155            )
156        })
157        .collect();
158    println!(); // Newline after tqdm so bars dont override each other.
159
160    let mut ct_distances: Vec<SpawnDistance> = Vec::new();
161    let mut t_distances: Vec<SpawnDistance> = Vec::new();
162
163    for (ct, t) in distances {
164        ct_distances.push(ct);
165        t_distances.push(t);
166    }
167
168    ct_distances.par_sort_unstable_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap());
169    t_distances.par_sort_unstable_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap());
170
171    SpawnDistances {
172        CT: ct_distances,
173        T: t_distances,
174    }
175}
176
177/// Result of one spread step.
178#[derive(Clone, Debug, Serialize, Deserialize)]
179pub struct SpreadResult {
180    /// New areas that are reachable by CTs.
181    new_marked_areas_ct: HashSet<u32>,
182    /// New areas that are reachable by Ts.
183    new_marked_areas_t: HashSet<u32>,
184
185    /// New connections between areas that are visible to each other.
186    visibility_connections: Vec<(ReducedSpawnDistance, ReducedSpawnDistance)>,
187}
188
189/// Save the spread results to a JSON file.
190///
191/// # Panics
192///
193/// Will panic if the file cannot be created or the JSON cannot be serialized.
194pub fn save_spreads_to_json(spreads: &[SpreadResult], filename: &Path) {
195    let mut file = create_file_with_parents(filename);
196    serde_json::to_writer(&mut file, &spreads).unwrap();
197}
198
199fn assert_sorted(spawn_distances: &[SpawnDistance]) {
200    assert!(
201        spawn_distances
202            .windows(2)
203            .all(|w| w[0].distance <= w[1].distance)
204    );
205}
206
207/// Generate spread steps from the distances of areas to CT and T spawns.
208///
209/// The slices of `spawn_distances_ct` and `spawn_distances_t` need to be pre-sorted by distance.
210/// Also requires a mapping of areas to close groups to ensure a reduced number of spread points.
211/// In the extreme case this can just be `AreaID` -> `AreaID` to indicate no grouping at all.
212/// Usually a 4x4 to 10x10 grid structure is sensible. See `group_nav_areas`.
213///
214/// Logic wise we iterate over the T and CT distances in parallel and always take the closest distance to the spawn points.
215/// We keep track of all of the processed (reachable) areas for both sides.
216///
217/// Then we try to check which reachable areas of the other side can be seen from the new area.
218/// We only consider that have not already been spotted. We determine this "spottednes" by whether it or its last path step
219/// are in a group that has been spotted. We only consider the parent and not the full path to not mark areas
220/// that have separated from that group before it was spotted.
221/// For example if you could have a look at the spawn of the other side after 2 seconds, then you would not have seen
222/// player that moved away from the spawn and behind walls within that time.
223///
224/// There is some significant difficulty here with getting this perfect because paths do not go from one area to the next
225/// but skip over them or take one every so slightly at an angle. This means that we do not mark areas as spotted that
226/// sensibly already have.
227///
228/// Meanwhile the grouping can cause areas to be declared spotted that were separated by small walls or other obstacles
229/// from the actually spotted one. Without any grouping we get >1000 spread points for each map which is excssive.
230/// With grouping 5x5 or 10x10 we get around 200-300 spread points which is much more manageable.
231#[allow(clippy::too_many_lines)]
232#[must_use]
233pub fn generate_spreads(
234    spawn_distances_ct: &[SpawnDistance],
235    spawn_distances_t: &[SpawnDistance],
236    area_to_group: &HashMap<u32, GroupId>,
237    perceiption: &Perceivability,
238) -> Vec<SpreadResult> {
239    assert_sorted(spawn_distances_ct);
240    assert_sorted(spawn_distances_t);
241    println!("Generating spreads");
242
243    let mut ct_index = 0;
244    let mut t_index = 0;
245
246    let mut new_marked_areas_ct: HashSet<u32> = HashSet::default();
247    let mut new_marked_areas_t: HashSet<u32> = HashSet::default();
248
249    let mut previous_areas_ct: Vec<&SpawnDistance> = Vec::with_capacity(spawn_distances_ct.len());
250    let mut previous_areas_t: Vec<&SpawnDistance> = Vec::with_capacity(spawn_distances_t.len());
251
252    let mut spotted_groups_ct: HashSet<GroupId> = HashSet::default();
253    let mut spotted_groups_t: HashSet<GroupId> = HashSet::default();
254    let mut visibility_connections: Vec<(ReducedSpawnDistance, ReducedSpawnDistance)> = Vec::new();
255
256    let mut last_plotted: f64 = 0.0;
257
258    let mut result = Vec::with_capacity(spawn_distances_ct.len() + spawn_distances_t.len());
259
260    let n_iterations = spawn_distances_ct
261        .iter()
262        .chain(spawn_distances_t.iter())
263        .filter(|a| a.distance < f64::MAX)
264        .count();
265
266    let tqdm_config = Config::new()
267        .with_leave(true)
268        .with_desc("Generating spreads".to_string());
269    let mut p_bar = iter::repeat_n((), n_iterations).tqdm_config(tqdm_config);
270
271    loop {
272        p_bar.next();
273        // Get the next T or CT area based on the distance from the spawns.
274        let (current_area, opposing_spotted_groups, own_spotted_groups, opposing_previous_areas) =
275            if ct_index < spawn_distances_ct.len()
276                && (t_index >= spawn_distances_t.len()
277                    || spawn_distances_ct[ct_index].distance < spawn_distances_t[t_index].distance)
278            {
279                let current = &spawn_distances_ct[ct_index];
280                new_marked_areas_ct.insert(current.area.area_id);
281                previous_areas_ct.push(current);
282
283                ct_index += 1;
284                (
285                    current,
286                    &mut spotted_groups_t,
287                    &mut spotted_groups_ct,
288                    &mut previous_areas_t,
289                )
290            } else if t_index < spawn_distances_t.len() {
291                let current = &spawn_distances_t[t_index];
292                new_marked_areas_t.insert(current.area.area_id);
293                previous_areas_t.push(current);
294
295                t_index += 1;
296                (
297                    current,
298                    &mut spotted_groups_ct,
299                    &mut spotted_groups_t,
300                    &mut previous_areas_ct,
301                )
302            } else {
303                result.push(SpreadResult {
304                    new_marked_areas_ct: mem::take(&mut new_marked_areas_ct),
305                    new_marked_areas_t: mem::take(&mut new_marked_areas_t),
306                    visibility_connections: mem::take(&mut visibility_connections),
307                });
308                break;
309            };
310
311        // Spot when only unreachable areas are left.
312        if current_area.distance == f64::MAX {
313            result.push(SpreadResult {
314                new_marked_areas_ct: mem::take(&mut new_marked_areas_ct),
315                new_marked_areas_t: mem::take(&mut new_marked_areas_t),
316                visibility_connections: mem::take(&mut visibility_connections),
317            });
318            break;
319        }
320
321        // Set spottednes based on the spottedness of the group of the last path element.
322        if current_area.path.len() >= 2
323            && own_spotted_groups
324                .contains(&area_to_group[&current_area.path[current_area.path.len() - 2]])
325        {
326            own_spotted_groups.insert(area_to_group[&current_area.area.area_id]);
327        }
328
329        // Get new areas that are visible from the current area.
330        let visible_areas = newly_perceivable(
331            current_area,
332            opposing_previous_areas,
333            own_spotted_groups,
334            opposing_spotted_groups,
335            area_to_group,
336            perceiption,
337        );
338
339        // If there are any newly visible areas that declare this one and the opposing one as spotted.
340        if !visible_areas.is_empty() {
341            own_spotted_groups.insert(area_to_group[&current_area.area.area_id]);
342            for spotted_by_area in &visible_areas {
343                opposing_spotted_groups.insert(area_to_group[&spotted_by_area.area.area_id]);
344                visibility_connections.push((
345                    Into::<ReducedSpawnDistance>::into(current_area),
346                    Into::<ReducedSpawnDistance>::into(*spotted_by_area),
347                ));
348            }
349        }
350
351        // Save a spread point either after a fixed distance or whenever a new area has been spotted.
352        if visible_areas.is_empty() && current_area.distance <= last_plotted + 100.0 {
353            continue;
354        }
355
356        result.push(SpreadResult {
357            new_marked_areas_ct: mem::take(&mut new_marked_areas_ct),
358            new_marked_areas_t: mem::take(&mut new_marked_areas_t),
359            visibility_connections: mem::take(&mut visibility_connections),
360        });
361
362        last_plotted = round_up_to_next_100(current_area.distance);
363    }
364    p_bar.for_each(|()| {});
365    println!(); // Newline after tqdm so bars dont override each other.
366    result
367}
368
369/// Get the new areas that can be seen from `current_area`.
370///
371/// This is the fine version where we only skip areas that are marked as spotted explicitly.
372/// This has previously been set based on the spottedness of the group of the last path element
373/// for the `current_area` itself.
374fn newly_perceivable<'a>(
375    current_area: &SpawnDistance,
376    previous_opposing_areas: &'a [&'a SpawnDistance],
377    own_spotted_groups: &HashSet<GroupId>,
378    opposing_spotted_groups: &HashSet<GroupId>,
379    area_to_group: &HashMap<u32, GroupId>,
380    perceiption: &Perceivability,
381) -> Vec<&'a SpawnDistance> {
382    previous_opposing_areas
383        .par_iter()
384        .filter(|opposing_area| {
385            !(own_spotted_groups.contains(&area_to_group[&current_area.area.area_id])
386                && opposing_spotted_groups.contains(&area_to_group[&opposing_area.area.area_id]))
387                && match perceiption {
388                    Perceivability::Visibility(visibility_cache) => {
389                        visibility_cache
390                            [&(current_area.area.area_id(), opposing_area.area.area_id())]
391                    }
392                    Perceivability::Audibility => {
393                        areas_audible(&current_area.area, &opposing_area.area)
394                    }
395                }
396        })
397        .copied()
398        .collect()
399}
400
401fn round_up_to_next_100(value: f64) -> f64 {
402    (value / 100.0).ceil() * 100.0
403}