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