steel_core/poi/
poi_set.rs1use rustc_hash::{FxHashMap, FxHashSet};
4use steel_utils::PackedSectionBlockPos;
5
6use super::poi_instance::PointOfInterest;
7use super::poi_storage::OccupationStatus;
8
9pub 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 #[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 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 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 #[must_use]
72 pub fn get(&self, packed_pos: PackedSectionBlockPos) -> Option<&PointOfInterest> {
73 self.pois_by_pos.get(&packed_pos)
74 }
75
76 pub fn get_mut(&mut self, packed_pos: PackedSectionBlockPos) -> Option<&mut PointOfInterest> {
78 self.pois_by_pos.get_mut(&packed_pos)
79 }
80
81 #[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 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 #[must_use]
118 pub const fn is_dirty(&self) -> bool {
119 self.dirty
120 }
121
122 pub const fn clear_dirty(&mut self) {
124 self.dirty = false;
125 }
126
127 #[must_use]
129 pub fn is_empty(&self) -> bool {
130 self.pois_by_pos.is_empty()
131 }
132
133 #[must_use]
135 pub fn len(&self) -> usize {
136 self.pois_by_pos.len()
137 }
138
139 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}