Skip to main content

steel_core/poi/
poi_set.rs

1//! POI set for a single chunk section.
2
3use rustc_hash::{FxHashMap, FxHashSet};
4use steel_utils::PackedSectionBlockPos;
5
6use super::poi_instance::PointOfInterest;
7use super::poi_storage::OccupationStatus;
8
9/// Stores all POIs within a single chunk section (16x16x16).
10///
11/// Uses a section-relative packed block position as the key, with a secondary
12/// index by POI type for type-filtered queries.
13pub struct PointOfInterestSet {
14    pois_by_pos: FxHashMap<PackedSectionBlockPos, PointOfInterest>,
15    pois_by_type: FxHashMap<usize, FxHashSet<PackedSectionBlockPos>>,
16    dirty: bool,
17}
18
19impl Default for PointOfInterestSet {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl PointOfInterestSet {
26    /// Creates an empty POI set.
27    #[must_use]
28    pub fn new() -> Self {
29        Self {
30            pois_by_pos: FxHashMap::default(),
31            pois_by_type: FxHashMap::default(),
32            dirty: false,
33        }
34    }
35
36    /// Returns a reference to the inserted POI, or `None` if one already exists at that position.
37    pub fn add(
38        &mut self,
39        packed_pos: PackedSectionBlockPos,
40        poi: PointOfInterest,
41    ) -> Option<&PointOfInterest> {
42        if self.pois_by_pos.contains_key(&packed_pos) {
43            return None;
44        }
45
46        let type_id = poi.poi_type_id;
47        self.pois_by_pos.insert(packed_pos, poi);
48        self.pois_by_type
49            .entry(type_id)
50            .or_default()
51            .insert(packed_pos);
52        self.dirty = true;
53
54        self.pois_by_pos.get(&packed_pos)
55    }
56
57    /// Removes and returns the POI at the given packed position, if present.
58    pub fn remove(&mut self, packed_pos: PackedSectionBlockPos) -> Option<PointOfInterest> {
59        let poi = self.pois_by_pos.remove(&packed_pos)?;
60        if let Some(positions) = self.pois_by_type.get_mut(&poi.poi_type_id) {
61            positions.remove(&packed_pos);
62            if positions.is_empty() {
63                self.pois_by_type.remove(&poi.poi_type_id);
64            }
65        }
66        self.dirty = true;
67        Some(poi)
68    }
69
70    /// Returns a reference to the POI at the given packed position.
71    #[must_use]
72    pub fn get(&self, packed_pos: PackedSectionBlockPos) -> Option<&PointOfInterest> {
73        self.pois_by_pos.get(&packed_pos)
74    }
75
76    /// Returns a mutable reference to the POI at the given packed position.
77    pub fn get_mut(&mut self, packed_pos: PackedSectionBlockPos) -> Option<&mut PointOfInterest> {
78        self.pois_by_pos.get_mut(&packed_pos)
79    }
80
81    /// Returns all POIs of the given type matching the occupation status.
82    #[must_use]
83    pub fn get_by_type(
84        &self,
85        type_id: usize,
86        status: OccupationStatus,
87        max_tickets: u32,
88    ) -> Vec<&PointOfInterest> {
89        let Some(positions) = self.pois_by_type.get(&type_id) else {
90            return Vec::new();
91        };
92
93        positions
94            .iter()
95            .filter_map(|pos| self.pois_by_pos.get(pos))
96            .filter(|poi| status.matches(poi, max_tickets))
97            .collect()
98    }
99
100    /// Returns all POIs matching the type predicate and occupation status.
101    pub fn get_matching(
102        &self,
103        type_predicate: &impl Fn(usize) -> bool,
104        status: OccupationStatus,
105        max_tickets_fn: &impl Fn(usize) -> u32,
106    ) -> Vec<&PointOfInterest> {
107        self.pois_by_pos
108            .values()
109            .filter(|poi| {
110                type_predicate(poi.poi_type_id)
111                    && status.matches(poi, max_tickets_fn(poi.poi_type_id))
112            })
113            .collect()
114    }
115
116    /// Returns `true` if any POIs have been added or removed since last cleared.
117    #[must_use]
118    pub const fn is_dirty(&self) -> bool {
119        self.dirty
120    }
121
122    /// Clears the dirty flag.
123    pub const fn clear_dirty(&mut self) {
124        self.dirty = false;
125    }
126
127    /// Returns `true` if this set contains no POIs.
128    #[must_use]
129    pub fn is_empty(&self) -> bool {
130        self.pois_by_pos.is_empty()
131    }
132
133    /// Returns the number of POIs in this set.
134    #[must_use]
135    pub fn len(&self) -> usize {
136        self.pois_by_pos.len()
137    }
138
139    /// Iterates over all POIs in this set as `(packed_pos, poi)` pairs.
140    pub fn iter(&self) -> impl Iterator<Item = (PackedSectionBlockPos, &PointOfInterest)> {
141        self.pois_by_pos.iter().map(|(&pos, poi)| (pos, poi))
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use steel_utils::BlockPos;
149
150    fn packed(x: u8, y: u8, z: u8) -> PackedSectionBlockPos {
151        let Some(pos) = PackedSectionBlockPos::from_local_xyz(x, y, z) else {
152            panic!("valid local packed section block position was rejected");
153        };
154        pos
155    }
156
157    #[test]
158    fn test_pack_unpack() {
159        for x in 0..16u8 {
160            for y in 0..16u8 {
161                for z in 0..16u8 {
162                    let pos = packed(x, y, z);
163                    assert_eq!((x, y, z), (pos.x(), pos.y(), pos.z()));
164                }
165            }
166        }
167    }
168
169    #[test]
170    fn test_add_remove_poi() {
171        let mut set = PointOfInterestSet::new();
172        let packed = packed(5, 10, 3);
173        let poi = PointOfInterest::new(BlockPos::new(5, 10, 3), 0, 1);
174
175        assert!(set.add(packed, poi.clone()).is_some());
176        assert!(set.add(packed, poi).is_none());
177
178        assert_eq!(set.len(), 1);
179        assert!(set.get(packed).is_some());
180
181        let removed = set.remove(packed);
182        assert!(removed.is_some());
183        assert!(set.is_empty());
184    }
185
186    #[test]
187    fn test_get_by_type_and_occupation() {
188        let mut set = PointOfInterestSet::new();
189
190        let p1 = packed(0, 0, 0);
191        set.add(p1, PointOfInterest::new(BlockPos::new(0, 0, 0), 0, 1));
192
193        let p2 = packed(1, 0, 0);
194        set.add(p2, PointOfInterest::new(BlockPos::new(1, 0, 0), 0, 1));
195
196        let p3 = packed(2, 0, 0);
197        set.add(p3, PointOfInterest::new(BlockPos::new(2, 0, 0), 1, 1));
198
199        assert_eq!(set.get_by_type(0, OccupationStatus::Any, 1).len(), 2);
200        assert_eq!(set.get_by_type(1, OccupationStatus::Any, 1).len(), 1);
201        assert_eq!(set.get_by_type(0, OccupationStatus::Free, 1).len(), 2);
202
203        set.get_mut(p1).expect("p1 was just added").reserve_ticket();
204        assert_eq!(set.get_by_type(0, OccupationStatus::Free, 1).len(), 1);
205        assert_eq!(set.get_by_type(0, OccupationStatus::Occupied, 1).len(), 1);
206    }
207}