horizon_event_system/gorc/spatial/
rtree.rs1use super::query::{QueryFilters, QueryResult, SpatialQuery};
8use crate::types::{PlayerId, Position, Vec3};
9use crate::utils::current_timestamp;
10use rstar::{PointDistance, RTree, RTreeObject, AABB};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone)]
15struct SpatialEntry {
16 object: SpatialObject,
17 point: [f64; 3],
18}
19
20impl SpatialEntry {
21 fn new(object: SpatialObject) -> Self {
22 let point = [object.position.x, object.position.y, object.position.z];
23 Self { object, point }
24 }
25}
26
27impl PartialEq for SpatialEntry {
28 fn eq(&self, other: &Self) -> bool {
29 self.object.player_id == other.object.player_id
30 }
31}
32
33impl Eq for SpatialEntry {}
34
35impl RTreeObject for SpatialEntry {
36 type Envelope = AABB<[f64; 3]>;
37
38 fn envelope(&self) -> Self::Envelope {
39 AABB::from_point(self.point)
40 }
41}
42
43impl PointDistance for SpatialEntry {
44 fn distance_2(&self, point: &[f64; 3]) -> f64 {
45 let dx = self.point[0] - point[0];
46 let dy = self.point[1] - point[1];
47 let dz = self.point[2] - point[2];
48 dx * dx + dy * dy + dz * dz
49 }
50
51 fn contains_point(&self, point: &[f64; 3]) -> bool {
52 (self.point[0] - point[0]).abs() < f64::EPSILON
53 && (self.point[1] - point[1]).abs() < f64::EPSILON
54 && (self.point[2] - point[2]).abs() < f64::EPSILON
55 }
56}
57
58#[derive(Debug, Clone)]
60pub struct SpatialObject {
61 pub player_id: PlayerId,
63 pub position: Position,
65 pub last_updated: u64,
67}
68
69impl SpatialObject {
70 pub fn new(player_id: PlayerId, position: Position) -> Self {
72 Self {
73 player_id,
74 position,
75 last_updated: current_timestamp(),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Default)]
82pub struct SpatialIndexStats {
83 pub total_insertions: usize,
84 pub total_queries: usize,
85 pub total_removals: usize,
86 pub total_clears: usize,
87 pub total_rebuilds: usize,
88 pub last_query_result_count: usize,
89 pub current_depth: u8,
90 pub leaf_nodes: usize,
91 pub internal_nodes: usize,
92}
93
94#[derive(Debug, Clone, Default)]
96pub struct NodeStats {
97 pub total_objects: usize,
98 pub max_depth: u8,
99 pub leaf_nodes: usize,
100 pub internal_nodes: usize,
101}
102
103#[derive(Debug)]
105pub struct RegionRTree {
106 bounds: (Vec3, Vec3),
108 tree: RTree<SpatialEntry>,
110 player_entries: HashMap<PlayerId, SpatialEntry>,
112 object_count: usize,
114 stats: SpatialIndexStats,
116}
117
118impl RegionRTree {
119 pub fn new(min: Vec3, max: Vec3) -> Self {
121 Self {
122 bounds: (min, max),
123 tree: RTree::new(),
124 player_entries: HashMap::new(),
125 object_count: 0,
126 stats: SpatialIndexStats::default(),
127 }
128 }
129
130 pub fn insert_player(&mut self, player_id: PlayerId, position: Position) {
132 let object = SpatialObject::new(player_id, position);
133 self.insert_object(object);
134 }
135
136 pub fn insert_object(&mut self, object: SpatialObject) {
138 let player_id = object.player_id;
139 let entry = SpatialEntry::new(object);
140
141 if let Some(existing) = self.player_entries.remove(&player_id) {
142 let _ = self.tree.remove(&existing);
143 self.object_count = self.object_count.saturating_sub(1);
144 }
145
146 self.tree.insert(entry.clone());
147 self.player_entries.insert(player_id, entry);
148 self.object_count += 1;
149 self.stats.total_insertions += 1;
150 }
151
152 pub fn query_radius(&mut self, center: Position, radius: f64) -> Vec<QueryResult> {
154 let query = SpatialQuery {
155 center,
156 radius,
157 filters: QueryFilters::default(),
158 };
159 self.query(query)
160 }
161
162 pub fn query(&mut self, query: SpatialQuery) -> Vec<QueryResult> {
164 let center_point = [query.center.x, query.center.y, query.center.z];
165 let radius_sq = query.radius * query.radius;
166
167 let search_distance = radius_sq;
168
169 let mut results: Vec<QueryResult> = self
170 .tree
171 .locate_within_distance(center_point, search_distance)
172 .filter_map(|entry| {
173 let object = &entry.object;
174
175 if let Some(include) = &query.filters.include_players {
177 if !include.contains(&object.player_id) {
178 return None;
179 }
180 }
181
182 if let Some(exclude) = &query.filters.exclude_players {
183 if exclude.contains(&object.player_id) {
184 return None;
185 }
186 }
187
188 let distance_sq = entry.distance_2(¢er_point);
189 if distance_sq > radius_sq {
190 return None;
191 }
192
193 let distance = distance_sq.sqrt();
194
195 if let Some(min_distance) = query.filters.min_distance {
196 if distance < min_distance {
197 return None;
198 }
199 }
200
201 Some(QueryResult {
202 player_id: object.player_id,
203 position: object.position,
204 distance,
205 metadata: HashMap::new(),
206 })
207 })
208 .collect();
209
210 if let Some(max_results) = query.filters.max_results {
211 results.truncate(max_results);
212 }
213
214 self.stats.total_queries += 1;
215 self.stats.last_query_result_count = results.len();
216 results
217 }
218
219 pub fn remove_player(&mut self, player_id: PlayerId) -> usize {
221 if let Some(existing) = self.player_entries.remove(&player_id) {
222 let removed = self.tree.remove(&existing).is_some();
223 if removed {
224 self.object_count = self.object_count.saturating_sub(1);
225 self.stats.total_removals += 1;
226 return 1;
227 }
228 }
229 0
230 }
231
232 pub fn object_count(&self) -> usize {
234 self.object_count
235 }
236
237 pub fn contains_player(&self, player_id: PlayerId) -> bool {
239 self.player_entries.contains_key(&player_id)
240 }
241
242 pub fn get_stats(&mut self) -> SpatialIndexStats {
244 let mut stats = self.stats.clone();
245 let node_stats = self.get_node_stats();
246 stats.current_depth = node_stats.max_depth;
247 stats.leaf_nodes = node_stats.leaf_nodes;
248 stats.internal_nodes = node_stats.internal_nodes;
249 stats
250 }
251
252 pub fn get_detailed_stats(&mut self) -> (SpatialIndexStats, NodeStats) {
254 let stats = self.get_stats();
255 let node_stats = self.get_node_stats();
256 (stats, node_stats)
257 }
258
259 pub fn estimate_query_efficiency(&self, radius: f64) -> f64 {
261 let total_volume = {
262 let (min, max) = &self.bounds;
263 (max.x - min.x).max(1.0)
264 * (max.y - min.y).max(1.0)
265 * (max.z - min.z).max(1.0)
266 };
267 let query_volume = (4.0 / 3.0) * std::f64::consts::PI * radius * radius * radius;
268 let coverage_ratio = (query_volume / total_volume).min(1.0);
269 1.0 - coverage_ratio
270 }
271
272 pub fn clear(&mut self) {
274 let (min, max) = self.bounds;
275 self.tree = RTree::new();
276 self.player_entries.clear();
277 self.object_count = 0;
278 self.stats.total_clears += 1;
279 self.bounds = (min, max);
280 }
281
282 pub fn rebuild(&mut self) {
284 let entries: Vec<_> = self.player_entries.values().cloned().collect();
285 self.tree = RTree::bulk_load(entries);
286 self.stats.total_rebuilds += 1;
287 }
288
289 pub fn collect_all_objects(&self) -> Vec<SpatialObject> {
291 self.player_entries.values().map(|entry| entry.object.clone()).collect()
292 }
293
294 fn get_node_stats(&self) -> NodeStats {
295 let leaf_nodes = self.tree.size();
296
297 NodeStats {
298 total_objects: self.object_count,
299 max_depth: if leaf_nodes > 0 { 1 } else { 0 },
300 leaf_nodes,
301 internal_nodes: 0,
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use crate::types::Vec3;
310
311 #[test]
312 fn test_insert_and_query() {
313 let mut tree = RegionRTree::new(
314 Vec3::new(-100.0, -100.0, -100.0),
315 Vec3::new(100.0, 100.0, 100.0),
316 );
317
318 let player_a = PlayerId::new();
319 let player_b = PlayerId::new();
320
321 tree.insert_player(player_a, Position::new(0.0, 0.0, 0.0));
322 tree.insert_player(player_b, Position::new(50.0, 0.0, 0.0));
323
324 assert_eq!(tree.object_count(), 2);
325 assert!(tree.contains_player(player_a));
326 assert!(tree.contains_player(player_b));
327
328 let results = tree.query_radius(Position::new(0.0, 0.0, 0.0), 10.0);
329 assert_eq!(results.len(), 1);
330 assert_eq!(results[0].player_id, player_a);
331
332 let wider = tree.query_radius(Position::new(0.0, 0.0, 0.0), 60.0);
333 let ids: Vec<PlayerId> = wider.iter().map(|r| r.player_id).collect();
334 assert!(ids.contains(&player_a));
335 assert!(ids.contains(&player_b));
336 assert_eq!(wider.len(), 2);
337 }
338
339 #[test]
340 fn test_update_player_position() {
341 let mut tree = RegionRTree::new(
342 Vec3::new(-100.0, -100.0, -100.0),
343 Vec3::new(100.0, 100.0, 100.0),
344 );
345
346 let player = PlayerId::new();
347 tree.insert_player(player, Position::new(0.0, 0.0, 0.0));
348 tree.insert_player(player, Position::new(20.0, 0.0, 0.0));
349
350 let results = tree.query_radius(Position::new(0.0, 0.0, 0.0), 5.0);
351 assert!(results.is_empty(), "Player should have moved out of range");
352
353 let results = tree.query_radius(Position::new(20.0, 0.0, 0.0), 5.0);
354 assert_eq!(results.len(), 1);
355 assert_eq!(results[0].player_id, player);
356 }
357
358 #[test]
359 fn test_remove_player() {
360 let mut tree = RegionRTree::new(
361 Vec3::new(-100.0, -100.0, -100.0),
362 Vec3::new(100.0, 100.0, 100.0),
363 );
364
365 let player = PlayerId::new();
366 tree.insert_player(player, Position::new(0.0, 0.0, 0.0));
367 assert_eq!(tree.object_count(), 1);
368
369 let removed = tree.remove_player(player);
370 assert_eq!(removed, 1);
371 assert_eq!(tree.object_count(), 0);
372 }
373}