Skip to main content

waymark_tilecache/
tile_cache.rs

1//! Tile cache implementation for Detour
2//!
3//! This module contains the TileCache structure, which is used to
4//! efficiently store and update navigation mesh tiles.
5
6use std::collections::{HashMap, HashSet};
7use std::f32;
8
9use super::tile_cache_data::{TileCacheBuilderConfig, TileCacheLayer};
10use super::tile_cache_integration::TileCacheNavMeshIntegration;
11use crate::error::TileCacheError;
12use waymark::nav_mesh::TileHeader;
13use waymark::{NavMesh, PolyRef};
14
15/// Maximum number of layers per tile
16const MAX_LAYERS: usize = 32;
17
18/// Tile cache structure
19#[derive(Debug)]
20#[cfg_attr(
21    feature = "serialization",
22    derive(serde::Serialize, serde::Deserialize)
23)]
24pub struct TileCache {
25    /// Parameters for the tile cache
26    params: TileCacheParams,
27    /// Tile width (x-axis)
28    tile_width: f32,
29    /// Tile height (z-axis)
30    tile_height: f32,
31    /// Cached tiles
32    tiles: Vec<Option<TileCacheEntry>>,
33    /// Compressed tile data
34    compressed_tiles: Vec<Vec<u8>>,
35    /// Next free tile index
36    next_free: Option<usize>,
37    /// Tile grid hash lookup
38    pos_lookup: HashMap<(i32, i32, i32), usize>,
39    /// Obstacles in the tile cache
40    obstacles: Vec<Option<Obstacle>>,
41    /// Next free obstacle index
42    next_free_obstacle: Option<usize>,
43    /// Salt for generating unique references
44    salt: u32,
45    /// Navigation mesh integration (not serialized)
46    #[cfg_attr(feature = "serialization", serde(skip))]
47    nav_mesh_integration: Option<TileCacheNavMeshIntegration>,
48}
49
50/// Tile cache parameters
51#[derive(Debug, Clone)]
52#[non_exhaustive]
53#[cfg_attr(
54    feature = "serialization",
55    derive(serde::Serialize, serde::Deserialize)
56)]
57pub struct TileCacheParams {
58    /// Origin of the tile cache
59    pub origin: [f32; 3],
60    /// Cell size (horizontal resolution)
61    pub cs: f32,
62    /// Cell height (vertical resolution)
63    pub ch: f32,
64    /// Width of the tile cache (in tiles)
65    pub width: i32,
66    /// Height of the tile cache (in tiles)
67    pub height: i32,
68    /// Maximum number of obstacles
69    pub max_obstacles: i32,
70}
71
72impl Default for TileCacheParams {
73    fn default() -> Self {
74        Self {
75            origin: [0.0, 0.0, 0.0],
76            cs: 0.3,
77            ch: 0.2,
78            width: 0,
79            height: 0,
80            max_obstacles: 0,
81        }
82    }
83}
84
85/// Tile cache entry
86#[derive(Debug, Clone)]
87#[cfg_attr(
88    feature = "serialization",
89    derive(serde::Serialize, serde::Deserialize)
90)]
91pub struct TileCacheEntry {
92    /// Header of the tile
93    pub header: TileHeader,
94    /// Index of the compressed tile data
95    pub data: usize,
96    /// Indices of obstacles affecting this tile
97    pub obstacles: Vec<usize>,
98    /// Next free tile in the linked list (used for memory management)
99    pub next: Option<usize>,
100}
101
102/// Obstacle type
103#[derive(Debug, Clone, Copy, PartialEq)]
104#[cfg_attr(
105    feature = "serialization",
106    derive(serde::Serialize, serde::Deserialize)
107)]
108pub enum ObstacleType {
109    /// Cylinder obstacle
110    Cylinder,
111    /// Axis-aligned bounding box
112    Box,
113    /// Oriented bounding box (rotated in Y axis)
114    OrientedBox,
115}
116
117/// Obstacle state
118#[derive(Debug, Clone, Copy, PartialEq)]
119#[cfg_attr(
120    feature = "serialization",
121    derive(serde::Serialize, serde::Deserialize)
122)]
123pub enum ObstacleState {
124    /// Empty/uninitialized
125    Empty,
126    /// Being processed
127    Processing,
128    /// Processed and active
129    Processed,
130    /// Being removed
131    Removing,
132}
133
134/// Obstacle data for different types
135#[derive(Debug, Clone)]
136#[cfg_attr(
137    feature = "serialization",
138    derive(serde::Serialize, serde::Deserialize)
139)]
140pub enum ObstacleData {
141    /// Cylinder obstacle
142    Cylinder {
143        /// Center position
144        pos: [f32; 3],
145        /// Radius
146        radius: f32,
147        /// Height
148        height: f32,
149    },
150    /// Axis-aligned box obstacle
151    Box {
152        /// Minimum bounds
153        bmin: [f32; 3],
154        /// Maximum bounds
155        bmax: [f32; 3],
156    },
157    /// Oriented box obstacle (rotated in Y axis)
158    OrientedBox {
159        /// Center position
160        center: [f32; 3],
161        /// Half extents (width/2, height/2, depth/2)
162        half_extents: [f32; 3],
163        /// Rotation auxiliary values [cos(0.5*angle)*sin(-0.5*angle), cos(0.5*angle)*cos(0.5*angle) - 0.5]
164        rot_aux: [f32; 2],
165    },
166}
167
168/// Obstacle in the tile cache
169#[derive(Debug, Clone)]
170#[cfg_attr(
171    feature = "serialization",
172    derive(serde::Serialize, serde::Deserialize)
173)]
174pub struct Obstacle {
175    /// Obstacle data
176    pub data: ObstacleData,
177    /// Obstacle state
178    pub state: ObstacleState,
179    /// Salt value for reference generation
180    pub salt: u16,
181    /// Tiles affected by this obstacle
182    pub touched: Vec<u32>,
183    /// Pending tiles to be updated
184    pub pending: Vec<u32>,
185    /// Next free obstacle in the linked list (used for memory management)
186    pub next: Option<usize>,
187}
188
189impl TileCache {
190    /// Creates a new tile cache
191    pub fn new(params: TileCacheParams) -> Result<Self, TileCacheError> {
192        if params.origin[0].is_infinite()
193            || params.origin[0].is_nan()
194            || params.origin[1].is_infinite()
195            || params.origin[1].is_nan()
196            || params.origin[2].is_infinite()
197            || params.origin[2].is_nan()
198        {
199            return Err(TileCacheError::InvalidParam);
200        }
201
202        if params.cs <= 0.0 || params.ch <= 0.0 {
203            return Err(TileCacheError::InvalidParam);
204        }
205
206        if params.width <= 0 || params.height <= 0 {
207            return Err(TileCacheError::InvalidParam);
208        }
209
210        let max_tiles = params.width * params.height * MAX_LAYERS as i32;
211
212        let mut tiles = Vec::with_capacity(max_tiles as usize);
213        for _i in 0..max_tiles as usize {
214            tiles.push(None);
215        }
216
217        let mut obstacles = Vec::with_capacity(params.max_obstacles as usize);
218        for _i in 0..params.max_obstacles as usize {
219            obstacles.push(None);
220        }
221
222        let next_free = Some(0);
223        let next_free_obstacle = Some(0);
224
225        for (i, tile) in tiles.iter_mut().enumerate().take(max_tiles as usize) {
226            *tile = Some(TileCacheEntry {
227                header: TileHeader::new(0, 0, 0),
228                data: 0,
229                obstacles: Vec::new(),
230                next: if i < max_tiles as usize - 1 {
231                    Some(i + 1)
232                } else {
233                    None
234                },
235            });
236        }
237
238        for (i, obstacle) in obstacles
239            .iter_mut()
240            .enumerate()
241            .take(params.max_obstacles as usize)
242        {
243            *obstacle = Some(Obstacle {
244                data: ObstacleData::Cylinder {
245                    pos: [0.0, 0.0, 0.0],
246                    radius: 0.0,
247                    height: 0.0,
248                },
249                state: ObstacleState::Empty,
250                salt: 0,
251                touched: Vec::new(),
252                pending: Vec::new(),
253                next: if i < params.max_obstacles as usize - 1 {
254                    Some(i + 1)
255                } else {
256                    None
257                },
258            });
259        }
260
261        Ok(Self {
262            params,
263            tile_width: 0.0,  // Will be calculated during initialization
264            tile_height: 0.0, // Will be calculated during initialization
265            tiles,
266            compressed_tiles: Vec::new(),
267            next_free,
268            pos_lookup: HashMap::new(),
269            obstacles,
270            next_free_obstacle,
271            salt: 1,
272            nav_mesh_integration: None,
273        })
274    }
275
276    /// Initializes the tile cache with data
277    pub fn init(&mut self) -> Result<(), TileCacheError> {
278        self.tile_width = self.params.cs * self.params.width as f32;
279        self.tile_height = self.params.cs * self.params.height as f32;
280
281        Ok(())
282    }
283
284    /// Attaches the tile cache to a navigation mesh with the specified builder configuration
285    pub fn attach_to_nav_mesh(
286        &mut self,
287        builder_config: TileCacheBuilderConfig,
288    ) -> Result<(), TileCacheError> {
289        let integration = TileCacheNavMeshIntegration::new(builder_config);
290        self.nav_mesh_integration = Some(integration);
291
292        Ok(())
293    }
294
295    /// Adds a tile to the cache with optional compression
296    pub fn add_tile(
297        &mut self,
298        data: &[u8],
299        flags: u8,
300        result: &mut PolyRef,
301    ) -> Result<(), TileCacheError> {
302        let compressed_data = if (flags & 0x01) != 0 {
303            data.to_vec()
304        } else {
305            self.compress_tile(data)?
306        };
307
308        let free_idx = self
309            .next_free
310            .ok_or(TileCacheError::OutOfMemory { resource: "tiles" })?;
311        let mut tile_entry = self.tiles[free_idx]
312            .take()
313            .ok_or(TileCacheError::InvalidParam)?;
314        self.next_free = tile_entry.next;
315
316        let header = self.parse_tile_header(&compressed_data)?;
317        let header_key = (header.x(), header.y(), header.layer());
318
319        if self.pos_lookup.contains_key(&header_key) {
320            tile_entry.next = self.next_free;
321            self.next_free = Some(free_idx);
322            self.tiles[free_idx] = Some(tile_entry);
323            return Err(TileCacheError::InvalidParam);
324        }
325
326        let data_idx = self.compressed_tiles.len();
327        self.compressed_tiles.push(compressed_data);
328        tile_entry.data = data_idx;
329        tile_entry.header = header;
330
331        self.pos_lookup.insert(header_key, free_idx);
332        self.tiles[free_idx] = Some(tile_entry);
333
334        *result = PolyRef::new((self.salt << 16) | (free_idx as u32));
335        self.salt += 1;
336
337        Ok(())
338    }
339
340    /// Removes a tile from the cache
341    pub fn remove_tile(&mut self, ref_val: PolyRef) -> Result<(), TileCacheError> {
342        let tile_idx = (ref_val.id() & 0xFFFF) as usize;
343
344        if tile_idx >= self.tiles.len() {
345            return Err(TileCacheError::InvalidParam);
346        }
347
348        let mut tile_entry = self.tiles[tile_idx]
349            .take()
350            .ok_or(TileCacheError::InvalidParam)?;
351
352        self.pos_lookup.remove(&(
353            tile_entry.header.x(),
354            tile_entry.header.y(),
355            tile_entry.header.layer(),
356        ));
357
358        tile_entry.obstacles.clear();
359        tile_entry.next = self.next_free;
360        self.next_free = Some(tile_idx);
361
362        self.tiles[tile_idx] = Some(tile_entry);
363
364        Ok(())
365    }
366
367    /// Adds a cylinder obstacle to the cache
368    pub fn add_obstacle(
369        &mut self,
370        pos: [f32; 3],
371        radius: f32,
372        height: f32,
373    ) -> Result<u32, TileCacheError> {
374        let free_idx = self.next_free_obstacle.ok_or(TileCacheError::OutOfMemory {
375            resource: "obstacles",
376        })?;
377        let mut obstacle = self.obstacles[free_idx]
378            .take()
379            .ok_or(TileCacheError::InvalidParam)?;
380        self.next_free_obstacle = obstacle.next;
381
382        obstacle.data = ObstacleData::Cylinder {
383            pos,
384            radius,
385            height,
386        };
387        obstacle.state = ObstacleState::Processing;
388        obstacle.salt += 1;
389        obstacle.touched.clear();
390        obstacle.pending.clear();
391
392        let affected_tiles = self.find_tiles_affected_by_obstacle(pos, radius)?;
393        for &tile_idx in &affected_tiles {
394            obstacle.touched.push(tile_idx as u32);
395            obstacle.pending.push(tile_idx as u32);
396        }
397
398        let salt = obstacle.salt;
399        self.obstacles[free_idx] = Some(obstacle);
400
401        Ok(self.encode_obstacle_ref(salt, free_idx))
402    }
403
404    /// Adds an axis-aligned box obstacle to the cache
405    pub fn add_box_obstacle(
406        &mut self,
407        bmin: [f32; 3],
408        bmax: [f32; 3],
409    ) -> Result<u32, TileCacheError> {
410        let free_idx = self.next_free_obstacle.ok_or(TileCacheError::OutOfMemory {
411            resource: "obstacles",
412        })?;
413        let mut obstacle = self.obstacles[free_idx]
414            .take()
415            .ok_or(TileCacheError::InvalidParam)?;
416        self.next_free_obstacle = obstacle.next;
417
418        obstacle.data = ObstacleData::Box { bmin, bmax };
419        obstacle.state = ObstacleState::Processing;
420        obstacle.salt += 1;
421        obstacle.touched.clear();
422        obstacle.pending.clear();
423
424        let center = [
425            (bmin[0] + bmax[0]) * 0.5,
426            (bmin[1] + bmax[1]) * 0.5,
427            (bmin[2] + bmax[2]) * 0.5,
428        ];
429        let radius = ((bmax[0] - bmin[0]).powi(2) + (bmax[2] - bmin[2]).powi(2)).sqrt() * 0.5;
430
431        let affected_tiles = self.find_tiles_affected_by_obstacle(center, radius)?;
432        for &tile_idx in &affected_tiles {
433            obstacle.touched.push(tile_idx as u32);
434            obstacle.pending.push(tile_idx as u32);
435        }
436
437        let salt = obstacle.salt;
438        self.obstacles[free_idx] = Some(obstacle);
439
440        Ok(self.encode_obstacle_ref(salt, free_idx))
441    }
442
443    /// Adds an oriented box obstacle to the cache
444    pub fn add_oriented_box_obstacle(
445        &mut self,
446        center: [f32; 3],
447        half_extents: [f32; 3],
448        y_radians: f32,
449    ) -> Result<u32, TileCacheError> {
450        let free_idx = self.next_free_obstacle.ok_or(TileCacheError::OutOfMemory {
451            resource: "obstacles",
452        })?;
453        let mut obstacle = self.obstacles[free_idx]
454            .take()
455            .ok_or(TileCacheError::InvalidParam)?;
456        self.next_free_obstacle = obstacle.next;
457
458        let angle = y_radians;
459        let cos_half = (angle * 0.5).cos();
460        let sin_half = (angle * 0.5).sin();
461        let rot_aux = [cos_half * -sin_half, cos_half * cos_half - 0.5];
462
463        obstacle.data = ObstacleData::OrientedBox {
464            center,
465            half_extents,
466            rot_aux,
467        };
468        obstacle.state = ObstacleState::Processing;
469        obstacle.salt += 1;
470        obstacle.touched.clear();
471        obstacle.pending.clear();
472
473        // Conservative bounding radius for oriented box
474        let radius = (half_extents[0].powi(2) + half_extents[2].powi(2)).sqrt();
475
476        let affected_tiles = self.find_tiles_affected_by_obstacle(center, radius)?;
477        for &tile_idx in &affected_tiles {
478            obstacle.touched.push(tile_idx as u32);
479            obstacle.pending.push(tile_idx as u32);
480        }
481
482        let salt = obstacle.salt;
483        self.obstacles[free_idx] = Some(obstacle);
484
485        Ok(self.encode_obstacle_ref(salt, free_idx))
486    }
487
488    /// Removes an obstacle from the cache
489    pub fn remove_obstacle(&mut self, obstacle_ref: u32) -> Result<(), TileCacheError> {
490        let obstacle_idx = self.decode_obstacle_ref_idx(obstacle_ref);
491        let salt = self.decode_obstacle_ref_salt(obstacle_ref);
492
493        if obstacle_idx >= self.obstacles.len() {
494            return Err(TileCacheError::InvalidParam);
495        }
496
497        let stored_salt = self.obstacles[obstacle_idx]
498            .as_ref()
499            .ok_or(TileCacheError::ObstacleNotFound)?
500            .salt;
501        if stored_salt != salt {
502            return Err(TileCacheError::InvalidParam);
503        }
504
505        let mut obstacle = self.obstacles[obstacle_idx]
506            .take()
507            .ok_or(TileCacheError::ObstacleNotFound)?;
508
509        obstacle.state = ObstacleState::Removing;
510        obstacle.pending = obstacle.touched.clone();
511
512        // Will be cleaned up during update
513        self.obstacles[obstacle_idx] = Some(obstacle);
514
515        Ok(())
516    }
517
518    /// Updates the tile cache (processes pending obstacles)
519    pub fn update(&mut self) -> Result<(), TileCacheError> {
520        self.update_with_status(0.0).map(|_| ())
521    }
522
523    /// Updates the tile cache and returns whether it's up to date
524    pub fn update_with_status(&mut self, _dt: f32) -> Result<bool, TileCacheError> {
525        let mut tiles_to_update = HashSet::new();
526        let mut has_pending_work = false;
527
528        for obstacle_idx in 0..self.obstacles.len() {
529            if let Some(obstacle) = &mut self.obstacles[obstacle_idx] {
530                match obstacle.state {
531                    ObstacleState::Processing => {
532                        has_pending_work = true;
533                        for &tile_idx in &obstacle.pending {
534                            tiles_to_update.insert(tile_idx as usize);
535                        }
536                        obstacle.pending.clear();
537                        obstacle.state = ObstacleState::Processed;
538                    }
539                    ObstacleState::Removing => {
540                        has_pending_work = true;
541                        for &tile_idx in &obstacle.pending {
542                            tiles_to_update.insert(tile_idx as usize);
543                        }
544                        obstacle.state = ObstacleState::Empty;
545                        obstacle.touched.clear();
546                        obstacle.pending.clear();
547                    }
548                    _ => {}
549                }
550            }
551        }
552
553        for obstacle_idx in 0..self.obstacles.len() {
554            let is_empty = self.obstacles[obstacle_idx]
555                .as_ref()
556                .is_some_and(|o| o.state == ObstacleState::Empty);
557            if is_empty {
558                if let Some(mut obstacle) = self.obstacles[obstacle_idx].take() {
559                    obstacle.next = self.next_free_obstacle;
560                    self.next_free_obstacle = Some(obstacle_idx);
561                    self.obstacles[obstacle_idx] = Some(obstacle);
562                }
563            }
564        }
565
566        for tile_idx in tiles_to_update {
567            self.rebuild_tile(tile_idx)?;
568        }
569
570        Ok(!has_pending_work)
571    }
572
573    /// Updates the tile cache and rebuilds tiles in the navigation mesh
574    pub fn update_with_nav_mesh(
575        &mut self,
576        _dt: f32,
577        nav_mesh: &mut NavMesh,
578    ) -> Result<bool, TileCacheError> {
579        let mut tiles_to_update = HashSet::new();
580        let mut has_pending_work = false;
581
582        for obstacle_idx in 0..self.obstacles.len() {
583            if let Some(obstacle) = &mut self.obstacles[obstacle_idx] {
584                match obstacle.state {
585                    ObstacleState::Processing => {
586                        has_pending_work = true;
587                        for &tile_idx in &obstacle.pending {
588                            tiles_to_update.insert(tile_idx as usize);
589                        }
590                        obstacle.pending.clear();
591                        obstacle.state = ObstacleState::Processed;
592                    }
593                    ObstacleState::Removing => {
594                        has_pending_work = true;
595                        for &tile_idx in &obstacle.pending {
596                            tiles_to_update.insert(tile_idx as usize);
597                        }
598                        obstacle.state = ObstacleState::Empty;
599                        obstacle.touched.clear();
600                        obstacle.pending.clear();
601                    }
602                    _ => {}
603                }
604            }
605        }
606
607        for obstacle_idx in 0..self.obstacles.len() {
608            let is_empty = self.obstacles[obstacle_idx]
609                .as_ref()
610                .is_some_and(|o| o.state == ObstacleState::Empty);
611            if is_empty {
612                if let Some(mut obstacle) = self.obstacles[obstacle_idx].take() {
613                    obstacle.next = self.next_free_obstacle;
614                    self.next_free_obstacle = Some(obstacle_idx);
615                    self.obstacles[obstacle_idx] = Some(obstacle);
616                }
617            }
618        }
619
620        for tile_idx in tiles_to_update {
621            if let Some(Some(_tile_entry)) = self.tiles.get(tile_idx) {
622                let tile_ref = PolyRef::new((self.salt << 16) | (tile_idx as u32));
623                self.build_nav_mesh_tile(tile_ref, nav_mesh)?;
624            }
625        }
626
627        Ok(!has_pending_work)
628    }
629
630    /// Builds navigation mesh tiles at the specified tile coordinates
631    pub fn build_nav_mesh_tiles_at(
632        &mut self,
633        tx: i32,
634        ty: i32,
635        nav_mesh: &mut NavMesh,
636    ) -> Result<(), TileCacheError> {
637        let tiles = self.get_tiles_at(tx, ty)?;
638
639        for tile_ref in tiles {
640            self.build_nav_mesh_tile(tile_ref, nav_mesh)?;
641        }
642
643        Ok(())
644    }
645
646    /// Builds a navigation mesh tile from a compressed tile reference
647    pub fn build_nav_mesh_tile(
648        &mut self,
649        tile_ref: PolyRef,
650        nav_mesh: &mut NavMesh,
651    ) -> Result<(), TileCacheError> {
652        let integration = self
653            .nav_mesh_integration
654            .as_ref()
655            .ok_or(TileCacheError::InvalidParam)?;
656
657        let tile_idx = (tile_ref.id() & 0xFFFF) as usize;
658        let _salt = tile_ref.id() >> 16;
659
660        if tile_idx >= self.tiles.len() {
661            return Err(TileCacheError::InvalidParam);
662        }
663
664        // Check salt matches (but for now we'll skip this check as our salt handling is different)
665        // In the C++ version, each tile has its own salt
666
667        let tile_entry = match &self.tiles[tile_idx] {
668            Some(entry) => entry,
669            None => return Err(TileCacheError::InvalidParam),
670        };
671
672        let compressed_data = match self.compressed_tiles.get(tile_entry.data) {
673            Some(data) => data,
674            None => return Err(TileCacheError::InvalidParam),
675        };
676
677        let decompressed_data = self.decompress_tile(compressed_data, None)?;
678        let tile_layer = TileCacheLayer::from_bytes(&decompressed_data)?;
679
680        integration.build_nav_mesh_tile_from_layer(self, nav_mesh, &tile_layer, tile_idx)?;
681
682        Ok(())
683    }
684
685    /// Finds the tile index for the given coordinates
686    #[allow(dead_code)]
687    fn find_tile_index(&self, x: i32, y: i32, layer: i32) -> Option<usize> {
688        self.pos_lookup.get(&(x, y, layer)).copied()
689    }
690
691    /// Rebuilds a tile using the attached navigation mesh integration
692    pub fn rebuild_tile_in_nav_mesh(
693        &mut self,
694        nav_mesh: &mut NavMesh,
695        tile_idx: usize,
696    ) -> Result<Option<PolyRef>, TileCacheError> {
697        if tile_idx >= self.tiles.len() {
698            return Err(TileCacheError::InvalidParam);
699        }
700
701        if self.tiles[tile_idx].is_none() {
702            return Err(TileCacheError::InvalidParam);
703        }
704
705        let integration = self
706            .nav_mesh_integration
707            .as_ref()
708            .ok_or(TileCacheError::InvalidParam)?;
709
710        integration.rebuild_tile_in_nav_mesh(self, nav_mesh, tile_idx)
711    }
712
713    /// Rebuilds a tile (legacy method for internal use)
714    fn rebuild_tile(&mut self, tile_idx: usize) -> Result<(), TileCacheError> {
715        if tile_idx >= self.tiles.len() {
716            return Err(TileCacheError::InvalidParam);
717        }
718
719        if self.tiles[tile_idx].is_none() {
720            return Err(TileCacheError::InvalidParam);
721        }
722
723        let tile_entry = match &self.tiles[tile_idx] {
724            Some(entry) => entry,
725            None => return Err(TileCacheError::InvalidParam),
726        };
727
728        let compressed_data = &self.compressed_tiles[tile_entry.data];
729
730        // For this implementation, we'll just mark that the tile has been processed
731        // The real rebuilding happens in rebuild_tile_in_nav_mesh
732        log::debug!(
733            "Rebuilding tile {} with {} bytes of data",
734            tile_idx,
735            compressed_data.len()
736        );
737
738        Ok(())
739    }
740
741    /// Gets a tile by index
742    pub fn get_tile(&self, tile_idx: usize) -> Option<&TileCacheEntry> {
743        if tile_idx >= self.tiles.len() {
744            return None;
745        }
746
747        match &self.tiles[tile_idx] {
748            Some(entry) => Some(entry),
749            None => None,
750        }
751    }
752
753    /// Gets a tile at the specified coordinates
754    pub fn get_tile_at(&self, x: i32, y: i32, layer: i32) -> Option<&TileCacheEntry> {
755        if let Some(&tile_idx) = self.pos_lookup.get(&(x, y, layer)) {
756            self.get_tile(tile_idx)
757        } else {
758            None
759        }
760    }
761
762    /// Gets an obstacle by index
763    pub fn get_obstacle(&self, obstacle_idx: usize) -> Option<&Obstacle> {
764        if obstacle_idx >= self.obstacles.len() {
765            return None;
766        }
767
768        match &self.obstacles[obstacle_idx] {
769            Some(obstacle) => Some(obstacle),
770            None => None,
771        }
772    }
773
774    /// Gets the number of obstacles
775    pub fn get_obstacle_count(&self) -> usize {
776        self.obstacles
777            .iter()
778            .filter_map(|obs| obs.as_ref())
779            .filter(|obs| obs.state != ObstacleState::Empty)
780            .count()
781    }
782
783    /// Gets the parameters of the tile cache
784    pub fn get_params(&self) -> &TileCacheParams {
785        &self.params
786    }
787
788    /// Gets the compressed data for a tile
789    pub fn get_tile_compressed_data(&self, tile_idx: usize) -> Option<&[u8]> {
790        if let Some(Some(tile)) = self.tiles.get(tile_idx) {
791            self.compressed_tiles.get(tile.data).map(|v| v.as_slice())
792        } else {
793            None
794        }
795    }
796
797    /// Gets the decompressed data for a tile
798    pub fn get_tile_data(&self, tile_idx: usize) -> Result<Vec<u8>, TileCacheError> {
799        if let Some(compressed_data) = self.get_tile_compressed_data(tile_idx) {
800            self.decompress_tile(compressed_data, None)
801        } else {
802            Err(TileCacheError::InvalidParam)
803        }
804    }
805
806    /// Compresses tile data using LZ4
807    pub fn compress_tile(&self, data: &[u8]) -> Result<Vec<u8>, TileCacheError> {
808        Ok(lz4_flex::compress_prepend_size(data))
809    }
810
811    /// Decompresses tile data using LZ4
812    pub fn decompress_tile(
813        &self,
814        compressed_data: &[u8],
815        _uncompressed_size: Option<usize>,
816    ) -> Result<Vec<u8>, TileCacheError> {
817        if compressed_data.is_empty() {
818            return Ok(Vec::new());
819        }
820
821        match lz4_flex::decompress_size_prepended(compressed_data) {
822            Ok(decompressed) => Ok(decompressed),
823            Err(e) => {
824                log::error!("LZ4 decompression failed: {:?}", e);
825                Err(TileCacheError::InvalidParam)
826            }
827        }
828    }
829
830    /// Parses a tile header from compressed tile data
831    fn parse_tile_header(&self, compressed_data: &[u8]) -> Result<TileHeader, TileCacheError> {
832        let decompressed = self.decompress_tile(compressed_data, None)?;
833        let tile_layer = TileCacheLayer::from_bytes(&decompressed)?;
834
835        let mut header = TileHeader::new(
836            tile_layer.header.tx,
837            tile_layer.header.ty,
838            tile_layer.header.tlayer,
839        );
840        header.set_data_size(compressed_data.len()); // Store compressed size
841
842        Ok(header)
843    }
844
845    /// Finds tiles affected by an obstacle
846    fn find_tiles_affected_by_obstacle(
847        &self,
848        pos: [f32; 3],
849        radius: f32,
850    ) -> Result<Vec<usize>, TileCacheError> {
851        let mut affected_tiles = Vec::new();
852
853        let min_x = ((pos[0] - radius - self.params.origin[0]) / self.tile_width).floor() as i32;
854        let max_x = ((pos[0] + radius - self.params.origin[0]) / self.tile_width).floor() as i32;
855        let min_z = ((pos[2] - radius - self.params.origin[2]) / self.tile_height).floor() as i32;
856        let max_z = ((pos[2] + radius - self.params.origin[2]) / self.tile_height).floor() as i32;
857
858        for z in min_z..=max_z {
859            for x in min_x..=max_x {
860                for layer in 0..MAX_LAYERS as i32 {
861                    if let Some(&tile_idx) = self.pos_lookup.get(&(x, z, layer)) {
862                        affected_tiles.push(tile_idx);
863                    }
864                }
865            }
866        }
867
868        Ok(affected_tiles)
869    }
870
871    /// Gets the current salt value
872    pub fn get_salt(&self) -> u32 {
873        self.salt
874    }
875
876    /// Encodes an obstacle reference from salt and index
877    pub fn encode_obstacle_ref(&self, salt: u16, idx: usize) -> u32 {
878        ((salt as u32) << 16) | (idx as u32)
879    }
880
881    /// Decodes the salt from an obstacle reference
882    pub fn decode_obstacle_ref_salt(&self, ref_val: u32) -> u16 {
883        ((ref_val >> 16) & 0xFFFF) as u16
884    }
885
886    /// Decodes the index from an obstacle reference
887    pub fn decode_obstacle_ref_idx(&self, ref_val: u32) -> usize {
888        (ref_val & 0xFFFF) as usize
889    }
890
891    /// Gets an obstacle by its reference
892    pub fn get_obstacle_by_ref(&self, ref_val: u32) -> Option<&Obstacle> {
893        let idx = self.decode_obstacle_ref_idx(ref_val);
894        let salt = self.decode_obstacle_ref_salt(ref_val);
895
896        if let Some(obstacle) = self.get_obstacle(idx) {
897            if obstacle.salt == salt {
898                return Some(obstacle);
899            }
900        }
901        None
902    }
903
904    /// Gets the reference for an obstacle at the given index
905    pub fn get_obstacle_ref(&self, obstacle_idx: usize) -> Option<u32> {
906        self.get_obstacle(obstacle_idx)
907            .map(|obstacle| self.encode_obstacle_ref(obstacle.salt, obstacle_idx))
908    }
909
910    /// Queries tiles that overlap with the given bounding box
911    pub fn query_tiles(
912        &self,
913        bmin: &[f32; 3],
914        bmax: &[f32; 3],
915        max_tiles: usize,
916    ) -> Result<Vec<PolyRef>, TileCacheError> {
917        let mut results = Vec::new();
918
919        let tw = self.params.width as f32 * self.params.cs;
920        let th = self.params.height as f32 * self.params.cs;
921
922        let tx0 = ((bmin[0] - self.params.origin[0]) / tw).floor() as i32;
923        let tx1 = ((bmax[0] - self.params.origin[0]) / tw).floor() as i32;
924        let ty0 = ((bmin[2] - self.params.origin[2]) / th).floor() as i32;
925        let ty1 = ((bmax[2] - self.params.origin[2]) / th).floor() as i32;
926
927        for ty in ty0..=ty1 {
928            for tx in tx0..=tx1 {
929                let tiles = self.get_tiles_at(tx, ty)?;
930
931                for &tile_ref in &tiles {
932                    if results.len() >= max_tiles {
933                        break;
934                    }
935
936                    let tile_idx = (tile_ref.id() & 0xFFFF) as usize;
937                    let salt = tile_ref.id() >> 16;
938
939                    if let Some(Some(_tile)) = self.tiles.get(tile_idx) {
940                        if salt != self.salt {
941                            continue;
942                        }
943
944                        if let Some(compressed_data) = self.get_tile_compressed_data(tile_idx) {
945                            let decompressed = self.decompress_tile(compressed_data, None)?;
946                            if let Ok(tile_layer) = TileCacheLayer::from_bytes(&decompressed) {
947                                let (tbmin, tbmax) =
948                                    self.calc_tight_tile_bounds(&tile_layer.header);
949
950                                if Self::overlap_bounds(bmin, bmax, &tbmin, &tbmax) {
951                                    results.push(tile_ref);
952                                }
953                            }
954                        }
955                    }
956                }
957            }
958        }
959
960        Ok(results)
961    }
962
963    /// Gets all tiles at the given tile coordinates
964    pub fn get_tiles_at(&self, tx: i32, ty: i32) -> Result<Vec<PolyRef>, TileCacheError> {
965        let mut tiles = Vec::new();
966
967        for layer in 0..MAX_LAYERS as i32 {
968            if let Some(&tile_idx) = self.pos_lookup.get(&(tx, ty, layer)) {
969                if let Some(Some(_tile)) = self.tiles.get(tile_idx) {
970                    let tile_ref = PolyRef::new((self.salt << 16) | (tile_idx as u32));
971                    tiles.push(tile_ref);
972                }
973            }
974        }
975
976        Ok(tiles)
977    }
978
979    /// Calculates tight bounds for a tile
980    pub fn calc_tight_tile_bounds(
981        &self,
982        header: &crate::tile_cache_data::TileCacheLayerHeader,
983    ) -> ([f32; 3], [f32; 3]) {
984        let cs = self.params.cs;
985
986        let bmin = [
987            header.bmin[0] + header.minx as f32 * cs,
988            header.bmin[1],
989            header.bmin[2] + header.miny as f32 * cs,
990        ];
991
992        let bmax = [
993            header.bmin[0] + (header.maxx + 1) as f32 * cs,
994            header.bmax[1],
995            header.bmin[2] + (header.maxy + 1) as f32 * cs,
996        ];
997
998        (bmin, bmax)
999    }
1000
1001    /// Gets the bounds of an obstacle
1002    pub fn get_obstacle_bounds(&self, obstacle: &Obstacle) -> ([f32; 3], [f32; 3]) {
1003        match &obstacle.data {
1004            ObstacleData::Cylinder {
1005                pos,
1006                radius,
1007                height,
1008            } => {
1009                let bmin = [pos[0] - radius, pos[1], pos[2] - radius];
1010                let bmax = [pos[0] + radius, pos[1] + height, pos[2] + radius];
1011                (bmin, bmax)
1012            }
1013            ObstacleData::Box { bmin, bmax } => (*bmin, *bmax),
1014            ObstacleData::OrientedBox {
1015                center,
1016                half_extents,
1017                ..
1018            } => {
1019                // Conservative bounds for oriented box
1020                let max_r = 1.41 * half_extents[0].max(half_extents[2]);
1021                let bmin = [
1022                    center[0] - max_r,
1023                    center[1] - half_extents[1],
1024                    center[2] - max_r,
1025                ];
1026                let bmax = [
1027                    center[0] + max_r,
1028                    center[1] + half_extents[1],
1029                    center[2] + max_r,
1030                ];
1031                (bmin, bmax)
1032            }
1033        }
1034    }
1035
1036    /// Checks if two bounding boxes overlap
1037    fn overlap_bounds(amin: &[f32; 3], amax: &[f32; 3], bmin: &[f32; 3], bmax: &[f32; 3]) -> bool {
1038        !(amin[0] > bmax[0]
1039            || amax[0] < bmin[0]
1040            || amin[1] > bmax[1]
1041            || amax[1] < bmin[1]
1042            || amin[2] > bmax[2]
1043            || amax[2] < bmin[2])
1044    }
1045
1046    /// Saves the tile cache to a file in JSON format
1047    #[cfg(feature = "serialization")]
1048    pub fn save_to_json<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), TileCacheError> {
1049        let json = serde_json::to_string_pretty(self)
1050            .map_err(|e| TileCacheError::Serialization(Box::new(e)))?;
1051
1052        std::fs::write(path, json).map_err(TileCacheError::Io)?;
1053
1054        Ok(())
1055    }
1056
1057    /// Loads a tile cache from a JSON file
1058    #[cfg(feature = "serialization")]
1059    pub fn load_from_json<P: AsRef<std::path::Path>>(path: P) -> Result<Self, TileCacheError> {
1060        let json = std::fs::read_to_string(path).map_err(TileCacheError::Io)?;
1061
1062        let tile_cache =
1063            serde_json::from_str(&json).map_err(|e| TileCacheError::Serialization(Box::new(e)))?;
1064
1065        Ok(tile_cache)
1066    }
1067
1068    /// Saves the tile cache to a file in binary format
1069    #[cfg(feature = "serialization")]
1070    pub fn save_to_binary<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), TileCacheError> {
1071        let encoded =
1072            postcard::to_allocvec(self).map_err(|e| TileCacheError::Serialization(Box::new(e)))?;
1073
1074        std::fs::write(path, encoded).map_err(TileCacheError::Io)?;
1075
1076        Ok(())
1077    }
1078
1079    /// Loads a tile cache from a binary file
1080    #[cfg(feature = "serialization")]
1081    pub fn load_from_binary<P: AsRef<std::path::Path>>(path: P) -> Result<Self, TileCacheError> {
1082        let data = std::fs::read(path).map_err(TileCacheError::Io)?;
1083
1084        let tile_cache =
1085            postcard::from_bytes(&data).map_err(|e| TileCacheError::Serialization(Box::new(e)))?;
1086
1087        Ok(tile_cache)
1088    }
1089
1090    /// Serializes the tile cache to JSON bytes
1091    #[cfg(feature = "serialization")]
1092    pub fn to_json_bytes(&self) -> Result<Vec<u8>, TileCacheError> {
1093        let json =
1094            serde_json::to_vec(self).map_err(|e| TileCacheError::Serialization(Box::new(e)))?;
1095        Ok(json)
1096    }
1097
1098    /// Deserializes a tile cache from JSON bytes
1099    #[cfg(feature = "serialization")]
1100    pub fn from_json_bytes(data: &[u8]) -> Result<Self, TileCacheError> {
1101        let tile_cache =
1102            serde_json::from_slice(data).map_err(|e| TileCacheError::Serialization(Box::new(e)))?;
1103        Ok(tile_cache)
1104    }
1105
1106    /// Serializes the tile cache to binary bytes
1107    #[cfg(feature = "serialization")]
1108    pub fn to_binary_bytes(&self) -> Result<Vec<u8>, TileCacheError> {
1109        let data =
1110            postcard::to_allocvec(self).map_err(|e| TileCacheError::Serialization(Box::new(e)))?;
1111        Ok(data)
1112    }
1113
1114    /// Deserializes a tile cache from binary bytes
1115    #[cfg(feature = "serialization")]
1116    pub fn from_binary_bytes(data: &[u8]) -> Result<Self, TileCacheError> {
1117        let tile_cache =
1118            postcard::from_bytes(data).map_err(|e| TileCacheError::Serialization(Box::new(e)))?;
1119        Ok(tile_cache)
1120    }
1121}
1122
1123#[cfg(test)]
1124mod tests {
1125    use super::*;
1126    use crate::tile_cache_data::TileCacheLayerHeader;
1127
1128    #[test]
1129    fn test_create_tile_cache() {
1130        let params = TileCacheParams {
1131            origin: [0.0, 0.0, 0.0],
1132            cs: 0.3,
1133            ch: 0.2,
1134            width: 100,
1135            height: 100,
1136            max_obstacles: 1024,
1137        };
1138
1139        let result = TileCache::new(params.clone());
1140
1141        assert!(result.is_ok());
1142
1143        let tile_cache = result.unwrap();
1144
1145        assert_eq!(tile_cache.get_params().width, params.width);
1146        assert_eq!(tile_cache.get_params().height, params.height);
1147        assert_eq!(tile_cache.get_params().cs, params.cs);
1148        assert_eq!(tile_cache.get_params().ch, params.ch);
1149        assert_eq!(tile_cache.get_params().max_obstacles, params.max_obstacles);
1150        assert_eq!(tile_cache.get_obstacle_count(), 0);
1151    }
1152
1153    #[test]
1154    fn test_invalid_params() {
1155        // Test invalid origin
1156        let params = TileCacheParams {
1157            origin: [f32::INFINITY, 0.0, 0.0], // Use INFINITY directly
1158            cs: 0.3,
1159            ch: 0.2,
1160            width: 100,
1161            height: 100,
1162            max_obstacles: 1024,
1163        };
1164
1165        let result = TileCache::new(params);
1166        assert!(result.is_err());
1167
1168        // Test invalid cell size
1169        let params = TileCacheParams {
1170            origin: [0.0, 0.0, 0.0],
1171            cs: -0.3, // Negative cell size
1172            ch: 0.2,
1173            width: 100,
1174            height: 100,
1175            max_obstacles: 1024,
1176        };
1177
1178        let result = TileCache::new(params);
1179        assert!(result.is_err());
1180
1181        // Test invalid width
1182        let params = TileCacheParams {
1183            origin: [0.0, 0.0, 0.0],
1184            cs: 0.3,
1185            ch: 0.2,
1186            width: 0, // Zero width
1187            height: 100,
1188            max_obstacles: 1024,
1189        };
1190
1191        let result = TileCache::new(params);
1192        assert!(result.is_err());
1193    }
1194
1195    #[test]
1196    fn test_obstacle_management() {
1197        let params = TileCacheParams {
1198            origin: [0.0, 0.0, 0.0],
1199            cs: 0.3,
1200            ch: 0.2,
1201            width: 10,
1202            height: 10,
1203            max_obstacles: 64,
1204        };
1205
1206        let mut tile_cache = TileCache::new(params).unwrap();
1207        tile_cache.init().unwrap();
1208
1209        // Add an obstacle
1210        let obstacle_ref = tile_cache.add_obstacle([5.0, 1.0, 5.0], 2.0, 3.0).unwrap();
1211        assert_eq!(tile_cache.get_obstacle_count(), 1);
1212
1213        // Check obstacle properties
1214        let obstacle = tile_cache.get_obstacle_by_ref(obstacle_ref).unwrap();
1215        match &obstacle.data {
1216            ObstacleData::Cylinder {
1217                pos,
1218                radius,
1219                height,
1220            } => {
1221                assert_eq!(*pos, [5.0, 1.0, 5.0]);
1222                assert_eq!(*radius, 2.0);
1223                assert_eq!(*height, 3.0);
1224            }
1225            _ => panic!("Expected cylinder obstacle"),
1226        }
1227        assert_eq!(obstacle.state, ObstacleState::Processing);
1228
1229        // Remove the obstacle
1230        tile_cache.remove_obstacle(obstacle_ref).unwrap();
1231        // Obstacle is marked for removal but not yet removed
1232        assert_eq!(tile_cache.get_obstacle_count(), 1);
1233
1234        // Update to actually remove the obstacle
1235        tile_cache.update().unwrap();
1236        assert_eq!(tile_cache.get_obstacle_count(), 0);
1237    }
1238
1239    #[test]
1240    #[cfg(feature = "serialization")]
1241    fn test_tile_cache_serialization() {
1242        use tempfile::NamedTempFile;
1243
1244        let params = TileCacheParams {
1245            origin: [1.0, 2.0, 3.0],
1246            cs: 0.5,
1247            ch: 0.3,
1248            width: 20,
1249            height: 20,
1250            max_obstacles: 128,
1251        };
1252
1253        let mut tile_cache = TileCache::new(params.clone()).unwrap();
1254        tile_cache.init().unwrap();
1255
1256        // Add some test data
1257        let _obstacle_ref = tile_cache
1258            .add_obstacle([10.0, 2.0, 10.0], 1.5, 2.0)
1259            .unwrap();
1260
1261        // Test JSON serialization
1262        let json_bytes = tile_cache.to_json_bytes().unwrap();
1263        let restored_from_json = TileCache::from_json_bytes(&json_bytes).unwrap();
1264        assert_eq!(restored_from_json.get_params().cs, params.cs);
1265        assert_eq!(restored_from_json.get_params().width, params.width);
1266
1267        // Test binary serialization
1268        let binary_bytes = tile_cache.to_binary_bytes().unwrap();
1269        let restored_from_binary = TileCache::from_binary_bytes(&binary_bytes).unwrap();
1270        assert_eq!(restored_from_binary.get_params().cs, params.cs);
1271        assert_eq!(restored_from_binary.get_params().height, params.height);
1272
1273        // Test file serialization
1274        let json_file = NamedTempFile::new().unwrap();
1275        tile_cache.save_to_json(json_file.path()).unwrap();
1276        let restored_from_json_file = TileCache::load_from_json(json_file.path()).unwrap();
1277        assert_eq!(
1278            restored_from_json_file.get_params().max_obstacles,
1279            params.max_obstacles
1280        );
1281
1282        let binary_file = NamedTempFile::new().unwrap();
1283        tile_cache.save_to_binary(binary_file.path()).unwrap();
1284        let restored_from_binary_file = TileCache::load_from_binary(binary_file.path()).unwrap();
1285        assert_eq!(restored_from_binary_file.get_params().origin, params.origin);
1286    }
1287
1288    #[test]
1289    fn test_tile_management() {
1290        let params = TileCacheParams {
1291            origin: [0.0, 0.0, 0.0],
1292            cs: 1.0,
1293            ch: 0.5,
1294            width: 5,
1295            height: 5,
1296            max_obstacles: 32,
1297        };
1298
1299        let mut tile_cache = TileCache::new(params).unwrap();
1300        tile_cache.init().unwrap();
1301
1302        // Create a proper tile layer
1303        let mut header = TileCacheLayerHeader::new();
1304        header.tx = 1;
1305        header.ty = 2;
1306        header.tlayer = 0;
1307        header.bmin = [0.0, 0.0, 0.0];
1308        header.bmax = [5.0, 5.0, 5.0];
1309        header.width = 5;
1310        header.height = 5;
1311
1312        let layer = TileCacheLayer::new(header);
1313        let tile_data = layer.to_bytes();
1314
1315        // Add a tile
1316        let mut tile_ref = PolyRef::new(0);
1317        tile_cache.add_tile(&tile_data, 0, &mut tile_ref).unwrap();
1318        assert!(tile_ref.is_valid());
1319
1320        // Test salt usage
1321        let salt = tile_cache.get_salt();
1322        assert!(salt > 1);
1323
1324        // Try to get the tile
1325        let tile = tile_cache.get_tile_at(1, 2, 0);
1326        assert!(tile.is_some());
1327
1328        // Remove the tile
1329        tile_cache.remove_tile(tile_ref).unwrap();
1330        let tile_after_removal = tile_cache.get_tile_at(1, 2, 0);
1331        assert!(tile_after_removal.is_none());
1332    }
1333
1334    #[test]
1335    fn test_update_process() {
1336        let params = TileCacheParams {
1337            origin: [0.0, 0.0, 0.0],
1338            cs: 1.0,
1339            ch: 0.5,
1340            width: 10,
1341            height: 10,
1342            max_obstacles: 64,
1343        };
1344
1345        let mut tile_cache = TileCache::new(params).unwrap();
1346        tile_cache.init().unwrap();
1347
1348        // Add an obstacle (it should be marked as pending)
1349        let _obstacle_idx = tile_cache.add_obstacle([5.0, 1.0, 5.0], 2.0, 3.0).unwrap();
1350
1351        // Update should process pending obstacles
1352        tile_cache.update().unwrap();
1353
1354        // After update, obstacle should no longer be pending
1355        // (though in our mock implementation, we don't actually change the pending flag
1356        // since we don't have real tiles to affect)
1357    }
1358}