steel_core/world/player_index/
area.rs1use std::sync::Arc;
7
8use rustc_hash::FxHashSet;
9use steel_utils::ChunkPos;
10
11use crate::{chunk::player_chunk_view::PlayerChunkView, entity::Entity, player::Player};
12
13pub struct PlayerAreaMap {
29 chunks: scc::HashMap<ChunkPos, FxHashSet<i32>>,
31
32 player_chunks: scc::HashMap<i32, FxHashSet<ChunkPos>>,
34}
35
36impl Default for PlayerAreaMap {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl PlayerAreaMap {
43 #[must_use]
45 pub fn new() -> Self {
46 Self {
47 chunks: scc::HashMap::new(),
48 player_chunks: scc::HashMap::new(),
49 }
50 }
51
52 pub fn on_player_join(&self, player: &Player, view: &PlayerChunkView) {
54 let entity_id = player.id();
55 let mut player_set = FxHashSet::default();
56
57 view.for_each(|chunk| {
58 player_set.insert(chunk);
59 self.add_to_chunk(chunk, entity_id);
60 });
61
62 let _ = self.player_chunks.insert_sync(entity_id, player_set);
63 }
64
65 pub fn on_player_leave(&self, player: &Arc<Player>) {
67 self.remove_by_entity_id(player.id());
68 }
69
70 pub fn remove_by_entity_id(&self, entity_id: i32) {
75 if let Some((_, chunks)) = self.player_chunks.remove_sync(&entity_id) {
76 for chunk in chunks {
77 self.remove_from_chunk(chunk, entity_id);
78 }
79 }
80 }
81
82 pub fn on_player_view_change(
86 &self,
87 entity_id: i32,
88 added_chunks: &[ChunkPos],
89 removed_chunks: &[ChunkPos],
90 ) {
91 if added_chunks.is_empty() && removed_chunks.is_empty() {
92 return;
93 }
94
95 for &chunk in removed_chunks {
96 self.remove_from_chunk(chunk, entity_id);
97 }
98
99 for &chunk in added_chunks {
100 self.add_to_chunk(chunk, entity_id);
101 }
102
103 self.player_chunks.update_sync(&entity_id, |_, set| {
105 for &chunk in removed_chunks {
106 set.remove(&chunk);
107 }
108 for &chunk in added_chunks {
109 set.insert(chunk);
110 }
111 });
112 }
113
114 #[must_use]
116 pub fn get_tracking_players(&self, chunk: ChunkPos) -> Vec<i32> {
117 self.chunks
118 .read_sync(&chunk, |_, set| set.iter().copied().collect())
119 .unwrap_or_default()
120 }
121
122 #[must_use]
124 pub fn len(&self) -> usize {
125 self.player_chunks.len()
126 }
127
128 #[must_use]
130 pub fn is_empty(&self) -> bool {
131 self.player_chunks.is_empty()
132 }
133
134 fn add_to_chunk(&self, chunk: ChunkPos, entity_id: i32) {
135 if self
136 .chunks
137 .update_sync(&chunk, |_, set| {
138 set.insert(entity_id);
139 })
140 .is_none()
141 {
142 let mut set = FxHashSet::default();
143 set.insert(entity_id);
144 let _ = self.chunks.insert_sync(chunk, set);
145 }
146 }
147
148 fn remove_from_chunk(&self, chunk: ChunkPos, entity_id: i32) {
149 let should_remove = self
150 .chunks
151 .update_sync(&chunk, |_, set| {
152 set.remove(&entity_id);
153 set.is_empty()
154 })
155 .unwrap_or(false);
156
157 if should_remove {
158 let _ = self.chunks.remove_if_sync(&chunk, |set| set.is_empty());
159 }
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn test_add_and_get() {
169 let map = PlayerAreaMap::new();
170 let entity_id = 42;
171 let center = ChunkPos::new(0, 0);
172 let view = PlayerChunkView::new(center, 2);
173
174 let mut player_set = FxHashSet::default();
176 view.for_each(|chunk| {
177 player_set.insert(chunk);
178 map.add_to_chunk(chunk, entity_id);
179 });
180 let _ = map.player_chunks.insert_sync(entity_id, player_set);
181
182 assert!(map.get_tracking_players(center).contains(&entity_id));
183 assert!(
184 map.get_tracking_players(ChunkPos::new(1, 1))
185 .contains(&entity_id)
186 );
187 assert!(
189 map.get_tracking_players(ChunkPos::new(3, 0))
190 .contains(&entity_id)
191 );
192 assert!(
194 !map.get_tracking_players(ChunkPos::new(5, 5))
195 .contains(&entity_id)
196 );
197 }
198
199 #[test]
200 fn test_remove() {
201 let map = PlayerAreaMap::new();
202 let entity_id = 42;
203 let center = ChunkPos::new(0, 0);
204 let view = PlayerChunkView::new(center, 2);
205
206 let mut player_set = FxHashSet::default();
208 view.for_each(|chunk| {
209 player_set.insert(chunk);
210 map.add_to_chunk(chunk, entity_id);
211 });
212 let _ = map.player_chunks.insert_sync(entity_id, player_set);
213 assert_eq!(map.len(), 1);
214
215 if let Some((_, chunks)) = map.player_chunks.remove_sync(&entity_id) {
217 for chunk in chunks {
218 map.remove_from_chunk(chunk, entity_id);
219 }
220 }
221 assert_eq!(map.len(), 0);
222 assert!(map.get_tracking_players(center).is_empty());
223 }
224
225 #[test]
226 fn test_view_change() {
227 let map = PlayerAreaMap::new();
228 let entity_id = 42;
229 let old_center = ChunkPos::new(0, 0);
230 let new_center = ChunkPos::new(5, 5);
231 let old_view = PlayerChunkView::new(old_center, 1);
232 let new_view = PlayerChunkView::new(new_center, 1);
233
234 let mut player_set = FxHashSet::default();
236 old_view.for_each(|chunk| {
237 player_set.insert(chunk);
238 map.add_to_chunk(chunk, entity_id);
239 });
240 let _ = map.player_chunks.insert_sync(entity_id, player_set);
241 assert!(map.get_tracking_players(old_center).contains(&entity_id));
242
243 let mut diff = (Vec::new(), Vec::new());
245 PlayerChunkView::difference(
246 &old_view,
247 &new_view,
248 |pos, (added, _): &mut (Vec<ChunkPos>, Vec<ChunkPos>)| added.push(pos),
249 |pos, (_, removed): &mut (Vec<ChunkPos>, Vec<ChunkPos>)| removed.push(pos),
250 &mut diff,
251 );
252 let (added, removed) = diff;
253
254 map.on_player_view_change(entity_id, &added, &removed);
255
256 assert!(!map.get_tracking_players(old_center).contains(&entity_id));
257 assert!(map.get_tracking_players(new_center).contains(&entity_id));
258 }
259
260 #[test]
261 fn test_multiple_players() {
262 let map = PlayerAreaMap::new();
263 let entity_id1 = 42;
264 let entity_id2 = 43;
265
266 let view1 = PlayerChunkView::new(ChunkPos::new(0, 0), 2);
267 let view2 = PlayerChunkView::new(ChunkPos::new(1, 1), 2);
268
269 let mut set1 = FxHashSet::default();
271 view1.for_each(|chunk| {
272 set1.insert(chunk);
273 map.add_to_chunk(chunk, entity_id1);
274 });
275 let _ = map.player_chunks.insert_sync(entity_id1, set1);
276
277 let mut set2 = FxHashSet::default();
278 view2.for_each(|chunk| {
279 set2.insert(chunk);
280 map.add_to_chunk(chunk, entity_id2);
281 });
282 let _ = map.player_chunks.insert_sync(entity_id2, set2);
283
284 let players = map.get_tracking_players(ChunkPos::new(0, 0));
285 assert!(players.contains(&entity_id1));
286 assert!(players.contains(&entity_id2));
287 assert_eq!(map.len(), 2);
288 }
289}