Skip to main content

proof_engine/terrain/
streaming.rs

1//! Terrain streaming — async chunk loading/unloading, LRU cache, priority queue.
2//!
3//! Uses thread pools (std threads + channels) rather than async/await.
4//! Manages chunk lifecycle: generation → cache → serialization → eviction.
5
6use std::collections::{HashMap, BinaryHeap, VecDeque};
7use std::sync::{Arc, Mutex, atomic::{AtomicUsize, Ordering}};
8use std::thread;
9use glam::Vec3;
10
11use crate::terrain::mod_types::{TerrainChunk, TerrainConfig, ChunkCoord, ChunkState};
12use crate::terrain::heightmap::{HeightMap, FractalNoise, DiamondSquare, PerlinTerrain};
13use crate::terrain::biome::{BiomeMap, ClimateSimulator};
14use crate::terrain::vegetation::VegetationSystem;
15
16// ── ChunkCache ────────────────────────────────────────────────────────────────
17
18/// Key for cache entries: maps ChunkCoord → access_order (used for LRU).
19struct CacheEntry {
20    chunk:        TerrainChunk,
21    access_order: u64,
22}
23
24/// LRU cache for terrain chunks.
25pub struct ChunkCache {
26    entries:   HashMap<ChunkCoord, CacheEntry>,
27    max_size:  usize,
28    /// Monotonically increasing access counter for LRU tracking.
29    clock:     u64,
30    /// Memory budget in bytes (approximate).
31    memory_budget: usize,
32    current_memory: usize,
33}
34
35impl ChunkCache {
36    pub fn new(max_size: usize) -> Self {
37        Self {
38            entries: HashMap::new(),
39            max_size,
40            clock: 0,
41            memory_budget: max_size * 1024 * 1024, // default: max_size MB
42            current_memory: 0,
43        }
44    }
45
46    pub fn with_memory_budget(max_size: usize, budget_bytes: usize) -> Self {
47        let mut c = Self::new(max_size);
48        c.memory_budget = budget_bytes;
49        c
50    }
51
52    /// Insert or replace a chunk. Evicts LRU if over capacity.
53    pub fn insert(&mut self, coord: ChunkCoord, chunk: TerrainChunk) {
54        let mem = Self::estimate_chunk_memory(&chunk);
55        // Evict while over budget or count limit
56        while (self.entries.len() >= self.max_size || self.current_memory + mem > self.memory_budget)
57            && !self.entries.is_empty()
58        {
59            self.evict_lru();
60        }
61        self.clock += 1;
62        let old_mem = self.entries.get(&coord).map(|e| Self::estimate_chunk_memory(&e.chunk)).unwrap_or(0);
63        self.current_memory = self.current_memory.saturating_sub(old_mem) + mem;
64        self.entries.insert(coord, CacheEntry { chunk, access_order: self.clock });
65    }
66
67    /// Get a chunk by coord, updating access time.
68    pub fn get(&mut self, coord: ChunkCoord) -> Option<&TerrainChunk> {
69        if let Some(entry) = self.entries.get_mut(&coord) {
70            self.clock += 1;
71            entry.access_order = self.clock;
72            Some(&entry.chunk)
73        } else {
74            None
75        }
76    }
77
78    /// Check whether the cache contains a coord without updating LRU.
79    pub fn contains(&self, coord: ChunkCoord) -> bool {
80        self.entries.contains_key(&coord)
81    }
82
83    /// Remove a specific coord from the cache.
84    pub fn remove(&mut self, coord: ChunkCoord) -> Option<TerrainChunk> {
85        if let Some(entry) = self.entries.remove(&coord) {
86            self.current_memory = self.current_memory
87                .saturating_sub(Self::estimate_chunk_memory(&entry.chunk));
88            Some(entry.chunk)
89        } else {
90            None
91        }
92    }
93
94    /// Evict the least-recently-used entry.
95    fn evict_lru(&mut self) {
96        let lru_key = self.entries.iter()
97            .min_by_key(|(_, e)| e.access_order)
98            .map(|(k, _)| *k);
99        if let Some(key) = lru_key {
100            if let Some(entry) = self.entries.remove(&key) {
101                self.current_memory = self.current_memory
102                    .saturating_sub(Self::estimate_chunk_memory(&entry.chunk));
103            }
104        }
105    }
106
107    /// Estimate memory usage of a chunk in bytes.
108    fn estimate_chunk_memory(chunk: &TerrainChunk) -> usize {
109        let heightmap_bytes = chunk.heightmap.data.len() * 4;
110        let base = std::mem::size_of::<TerrainChunk>();
111        base + heightmap_bytes + 512 // approximate
112    }
113
114    pub fn len(&self) -> usize { self.entries.len() }
115    pub fn is_empty(&self) -> bool { self.entries.is_empty() }
116    pub fn current_memory_bytes(&self) -> usize { self.current_memory }
117}
118
119// ── Priority Queue for chunk loading ─────────────────────────────────────────
120
121/// A load request with priority.
122#[derive(Clone, Debug)]
123struct LoadRequest {
124    coord:    ChunkCoord,
125    priority: i64,   // higher = load first
126}
127
128impl PartialEq for LoadRequest {
129    fn eq(&self, other: &Self) -> bool { self.priority == other.priority }
130}
131impl Eq for LoadRequest {}
132impl PartialOrd for LoadRequest {
133    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
134        Some(self.cmp(other))
135    }
136}
137impl Ord for LoadRequest {
138    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
139        self.priority.cmp(&other.priority)
140    }
141}
142
143/// Priority queue for chunks to be loaded.
144pub struct LoadQueue {
145    heap:    BinaryHeap<LoadRequest>,
146    in_queue: std::collections::HashSet<ChunkCoord>,
147}
148
149impl LoadQueue {
150    pub fn new() -> Self {
151        Self { heap: BinaryHeap::new(), in_queue: std::collections::HashSet::new() }
152    }
153
154    /// Push a coord with given priority (higher = sooner).
155    pub fn push(&mut self, coord: ChunkCoord, priority: i64) {
156        if self.in_queue.insert(coord) {
157            self.heap.push(LoadRequest { coord, priority });
158        }
159    }
160
161    /// Pop the highest-priority coord.
162    pub fn pop(&mut self) -> Option<ChunkCoord> {
163        while let Some(req) = self.heap.pop() {
164            if self.in_queue.remove(&req.coord) {
165                return Some(req.coord);
166            }
167        }
168        None
169    }
170
171    pub fn len(&self) -> usize { self.in_queue.len() }
172    pub fn is_empty(&self) -> bool { self.in_queue.is_empty() }
173
174    /// Recompute priorities based on new camera position. Clears and rebuilds.
175    pub fn reprioritize(&mut self, camera: Vec3, config: &TerrainConfig) {
176        let coords: Vec<ChunkCoord> = self.in_queue.drain().collect();
177        self.heap.clear();
178        for coord in coords {
179            let dist = coord.distance_to_world_pos(camera, config.chunk_size as f32);
180            let priority = -(dist as i64);
181            self.heap.push(LoadRequest { coord, priority });
182            self.in_queue.insert(coord);
183        }
184    }
185}
186
187impl Default for LoadQueue {
188    fn default() -> Self { Self::new() }
189}
190
191// ── ChunkGenerator ────────────────────────────────────────────────────────────
192
193/// Generates terrain chunks from scratch for a given coord.
194pub struct ChunkGenerator {
195    pub config: TerrainConfig,
196}
197
198impl ChunkGenerator {
199    pub fn new(config: TerrainConfig) -> Self { Self { config } }
200
201    /// Generate a fully-initialized chunk for the given coordinate.
202    pub fn generate(&self, coord: ChunkCoord) -> TerrainChunk {
203        let size = self.config.chunk_size;
204        let seed = self.chunk_seed(coord);
205
206        // Generate heightmap using a mix of algorithms
207        let mut heightmap = self.generate_heightmap(coord, size, seed);
208        heightmap.island_mask(2.0);
209        heightmap.normalize();
210
211        // Apply erosion for naturalistic terrain
212        crate::terrain::heightmap::HydraulicErosion::erode(
213            &mut heightmap, 1000, 1.0, 8.0, 0.05, seed,
214        );
215        crate::terrain::heightmap::ThermalErosion::erode(&mut heightmap, 10, 0.04);
216        heightmap.normalize();
217
218        // Biome classification
219        let sim = ClimateSimulator::default();
220        let climate = sim.simulate(&heightmap);
221        let biome_map = BiomeMap::from_heightmap(&heightmap, &climate);
222
223        // Vegetation
224        let vegetation = VegetationSystem::generate(&heightmap, &biome_map, 0.8, seed);
225
226        TerrainChunk {
227            coord,
228            heightmap,
229            biome_map: Some(biome_map),
230            vegetation: Some(vegetation),
231            lod_level: 0,
232            state: ChunkState::Ready,
233            last_used: std::time::Instant::now(),
234            seed,
235        }
236    }
237
238    /// Generate a LOD-reduced chunk (lower resolution heightmap).
239    pub fn generate_lod(&self, coord: ChunkCoord, lod: u8) -> TerrainChunk {
240        let scale = 1usize << lod as usize;
241        let size = (self.config.chunk_size / scale).max(4);
242        let seed = self.chunk_seed(coord);
243        let mut heightmap = self.generate_heightmap(coord, size, seed);
244        heightmap.normalize();
245
246        TerrainChunk {
247            coord,
248            heightmap,
249            biome_map: None,
250            vegetation: None,
251            lod_level: lod,
252            state: ChunkState::Ready,
253            last_used: std::time::Instant::now(),
254            seed,
255        }
256    }
257
258    fn generate_heightmap(&self, coord: ChunkCoord, size: usize, seed: u64) -> HeightMap {
259        // Use coord to offset the noise sampling for seamless tiling
260        let offset_x = coord.0 as f32 * size as f32;
261        let offset_z = coord.1 as f32 * size as f32;
262        let world_scale = 256.0f32;
263
264        // Base layer: fractal noise
265        let mut hm = HeightMap::new(size, size);
266        let noise = crate::terrain::heightmap::FractalNoise::generate(
267            size, size, 6, 2.0, 0.5, 4.0, seed,
268        );
269        // Offset coordinates for seamless tiling
270        for y in 0..size {
271            for x in 0..size {
272                let nx = (x as f32 + offset_x) / world_scale;
273                let ny = (y as f32 + offset_z) / world_scale;
274                let idx = y * size + x;
275                hm.data[idx] = noise.data[idx];
276            }
277        }
278        hm
279    }
280
281    fn chunk_seed(&self, coord: ChunkCoord) -> u64 {
282        let base = self.config.seed;
283        let cx = coord.0 as u64;
284        let cz = coord.1 as u64;
285        base.wrapping_add(cx.wrapping_mul(0x9e3779b97f4a7c15))
286            .wrapping_add(cz.wrapping_mul(0x6c62272e07bb0142))
287    }
288}
289
290// ── ChunkSerializer ───────────────────────────────────────────────────────────
291
292/// Magic bytes for chunk file format.
293const CHUNK_MAGIC: u32 = 0x43484E4B; // "CHNK"
294const CHUNK_VERSION: u16 = 1;
295
296/// Serializes/deserializes chunks to/from a binary format.
297///
298/// Format:
299/// ```text
300/// [magic: u32][version: u16][coord_x: i32][coord_z: i32]
301/// [timestamp: u64][seed: u64][lod: u8]
302/// [heightmap_width: u32][heightmap_height: u32][heightmap_data: f32 * w * h]
303/// [checksum: u32]
304/// ```
305pub struct ChunkSerializer;
306
307impl ChunkSerializer {
308    /// Serialize a chunk to bytes.
309    pub fn serialize(chunk: &TerrainChunk) -> Vec<u8> {
310        let mut out = Vec::new();
311        // Header
312        out.extend_from_slice(&CHUNK_MAGIC.to_le_bytes());
313        out.extend_from_slice(&CHUNK_VERSION.to_le_bytes());
314        out.extend_from_slice(&chunk.coord.0.to_le_bytes());
315        out.extend_from_slice(&chunk.coord.1.to_le_bytes());
316        // Timestamp (seconds since epoch — use 0 since no std::time::SystemTime)
317        out.extend_from_slice(&0u64.to_le_bytes());
318        out.extend_from_slice(&chunk.seed.to_le_bytes());
319        out.push(chunk.lod_level);
320        // Heightmap
321        let hm_bytes = chunk.heightmap.to_raw_bytes();
322        let hm_len = hm_bytes.len() as u32;
323        out.extend_from_slice(&hm_len.to_le_bytes());
324        out.extend_from_slice(&hm_bytes);
325        // Simple checksum: sum of all bytes so far mod 2^32
326        let checksum: u32 = out.iter().fold(0u32, |acc, &b| acc.wrapping_add(b as u32));
327        out.extend_from_slice(&checksum.to_le_bytes());
328        out
329    }
330
331    /// Deserialize a chunk from bytes. Returns None if format is invalid.
332    pub fn deserialize(bytes: &[u8]) -> Option<TerrainChunk> {
333        if bytes.len() < 4 + 2 + 4 + 4 + 8 + 8 + 1 + 4 { return None; }
334        let mut pos = 0;
335
336        let magic = u32::from_le_bytes(bytes[pos..pos+4].try_into().ok()?); pos += 4;
337        if magic != CHUNK_MAGIC { return None; }
338
339        let version = u16::from_le_bytes(bytes[pos..pos+2].try_into().ok()?); pos += 2;
340        if version != CHUNK_VERSION { return None; }
341
342        let coord_x = i32::from_le_bytes(bytes[pos..pos+4].try_into().ok()?); pos += 4;
343        let coord_z = i32::from_le_bytes(bytes[pos..pos+4].try_into().ok()?); pos += 4;
344        let _timestamp = u64::from_le_bytes(bytes[pos..pos+8].try_into().ok()?); pos += 8;
345        let seed = u64::from_le_bytes(bytes[pos..pos+8].try_into().ok()?); pos += 8;
346        let lod_level = bytes[pos]; pos += 1;
347
348        let hm_len = u32::from_le_bytes(bytes[pos..pos+4].try_into().ok()?) as usize; pos += 4;
349        if bytes.len() < pos + hm_len + 4 { return None; }
350
351        let hm_bytes = &bytes[pos..pos + hm_len]; pos += hm_len;
352        let heightmap = HeightMap::from_raw_bytes(hm_bytes)?;
353
354        // Verify checksum
355        let stored_checksum = u32::from_le_bytes(bytes[pos..pos+4].try_into().ok()?);
356        let computed: u32 = bytes[..pos].iter().fold(0u32, |acc, &b| acc.wrapping_add(b as u32));
357        if stored_checksum != computed { return None; }
358
359        Some(TerrainChunk {
360            coord: ChunkCoord(coord_x, coord_z),
361            heightmap,
362            biome_map: None,
363            vegetation: None,
364            lod_level,
365            state: ChunkState::Ready,
366            last_used: std::time::Instant::now(),
367            seed,
368        })
369    }
370
371    /// Save chunk to a file path (returns error string on failure).
372    pub fn save_to_file(chunk: &TerrainChunk, path: &str) -> Result<(), String> {
373        let bytes = Self::serialize(chunk);
374        std::fs::write(path, &bytes).map_err(|e| e.to_string())
375    }
376
377    /// Load chunk from a file path.
378    pub fn load_from_file(path: &str) -> Result<TerrainChunk, String> {
379        let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
380        Self::deserialize(&bytes).ok_or_else(|| "Invalid chunk format".to_string())
381    }
382}
383
384// ── StreamingStats ────────────────────────────────────────────────────────────
385
386/// Statistics for the streaming system.
387#[derive(Debug, Default, Clone)]
388pub struct StreamingStats {
389    pub chunks_loaded:   usize,
390    pub chunks_unloaded: usize,
391    pub cache_hits:      usize,
392    pub cache_misses:    usize,
393    pub pending_count:   usize,
394    pub memory_bytes:    usize,
395    pub generate_time_ms: f64,
396}
397
398impl StreamingStats {
399    pub fn cache_hit_rate(&self) -> f32 {
400        let total = self.cache_hits + self.cache_misses;
401        if total == 0 { 0.0 } else { self.cache_hits as f32 / total as f32 }
402    }
403}
404
405// ── VisibilitySet ─────────────────────────────────────────────────────────────
406
407/// Tracks which chunk coords are currently in the view frustum.
408#[derive(Debug, Default)]
409pub struct VisibilitySet {
410    visible: std::collections::HashSet<ChunkCoord>,
411    previous: std::collections::HashSet<ChunkCoord>,
412}
413
414impl VisibilitySet {
415    pub fn new() -> Self { Self::default() }
416
417    /// Update the visible set from new camera parameters.
418    pub fn update(&mut self, camera_pos: Vec3, config: &TerrainConfig) {
419        self.previous = std::mem::take(&mut self.visible);
420        let chunk_world = config.chunk_size as f32;
421        let view_chunks = config.view_distance as i32;
422        let cam_cx = (camera_pos.x / chunk_world).floor() as i32;
423        let cam_cz = (camera_pos.z / chunk_world).floor() as i32;
424        for dz in -view_chunks..=view_chunks {
425            for dx in -view_chunks..=view_chunks {
426                let dist = ((dx * dx + dz * dz) as f32).sqrt();
427                if dist <= config.view_distance as f32 {
428                    self.visible.insert(ChunkCoord(cam_cx + dx, cam_cz + dz));
429                }
430            }
431        }
432    }
433
434    /// Chunks that just became visible.
435    pub fn newly_visible(&self) -> impl Iterator<Item = ChunkCoord> + '_ {
436        self.visible.iter().copied().filter(|c| !self.previous.contains(c))
437    }
438
439    /// Chunks that just became invisible.
440    pub fn newly_hidden(&self) -> impl Iterator<Item = ChunkCoord> + '_ {
441        self.previous.iter().copied().filter(|c| !self.visible.contains(c))
442    }
443
444    pub fn is_visible(&self, coord: ChunkCoord) -> bool { self.visible.contains(&coord) }
445    pub fn visible_count(&self) -> usize { self.visible.len() }
446    pub fn visible_coords(&self) -> impl Iterator<Item = ChunkCoord> + '_ { self.visible.iter().copied() }
447}
448
449// ── LodScheduler ─────────────────────────────────────────────────────────────
450
451/// Decides when to upgrade/downgrade chunk LOD.
452///
453/// Uses hysteresis to prevent LOD thrashing: upgrade requires distance < threshold,
454/// downgrade requires distance > threshold + hysteresis_margin.
455pub struct LodScheduler {
456    /// Distance thresholds for each LOD level. `lod_thresholds[n]` = max dist for LOD n.
457    pub lod_thresholds:     Vec<f32>,
458    /// Hysteresis margin to prevent thrashing.
459    pub hysteresis_margin:  f32,
460    /// Pending LOD changes (coord → target_lod).
461    pending: HashMap<ChunkCoord, u8>,
462}
463
464impl LodScheduler {
465    pub fn new(lod_levels: usize, chunk_size: f32) -> Self {
466        let thresholds: Vec<f32> = (0..lod_levels)
467            .map(|l| chunk_size * 2.0 * (1 << l) as f32)
468            .collect();
469        Self {
470            lod_thresholds: thresholds,
471            hysteresis_margin: chunk_size * 0.5,
472            pending: HashMap::new(),
473        }
474    }
475
476    /// Compute the desired LOD for a chunk at a given world distance.
477    pub fn desired_lod(&self, dist: f32, current_lod: u8) -> u8 {
478        let mut target = (self.lod_thresholds.len() - 1) as u8;
479        for (l, &threshold) in self.lod_thresholds.iter().enumerate() {
480            if dist < threshold {
481                target = l as u8;
482                break;
483            }
484        }
485        // Apply hysteresis: only change if difference exceeds margin
486        if target > current_lod {
487            // Downgrading: require dist > current threshold + margin
488            let cur_thresh = self.lod_thresholds.get(current_lod as usize).copied().unwrap_or(0.0);
489            if dist < cur_thresh + self.hysteresis_margin {
490                return current_lod; // Stay at current LOD
491            }
492        }
493        // Upgrading: immediate (close objects need more detail)
494        target
495    }
496
497    /// Update LOD decisions for all visible chunks.
498    pub fn update(&mut self, camera_pos: Vec3, chunks: &HashMap<ChunkCoord, u8>, config: &TerrainConfig) {
499        self.pending.clear();
500        for (&coord, &current_lod) in chunks {
501            let dist = coord.distance_to_world_pos(camera_pos, config.chunk_size as f32);
502            let desired = self.desired_lod(dist, current_lod);
503            if desired != current_lod {
504                self.pending.insert(coord, desired);
505            }
506        }
507    }
508
509    /// Get the pending LOD changes.
510    pub fn pending_changes(&self) -> &HashMap<ChunkCoord, u8> { &self.pending }
511
512    /// Compute priority for loading a chunk (used by LoadQueue).
513    /// In-frustum and close chunks have highest priority.
514    pub fn load_priority(
515        coord: ChunkCoord,
516        camera_pos: Vec3,
517        chunk_size: f32,
518        in_frustum: bool,
519    ) -> i64 {
520        let dist = coord.distance_to_world_pos(camera_pos, chunk_size);
521        let dist_score = -(dist as i64);
522        let frustum_bonus: i64 = if in_frustum { 1_000_000 } else { 0 };
523        dist_score + frustum_bonus
524    }
525}
526
527// ── Prefetcher ────────────────────────────────────────────────────────────────
528
529/// Predicts movement direction and prefetches chunks ahead of the camera.
530pub struct Prefetcher {
531    /// Recent camera positions (ring buffer for velocity estimation).
532    history: VecDeque<Vec3>,
533    /// Number of frames to look ahead.
534    lookahead_frames: usize,
535    /// Estimated velocity.
536    velocity: Vec3,
537}
538
539impl Prefetcher {
540    pub fn new(lookahead_frames: usize) -> Self {
541        Self {
542            history: VecDeque::with_capacity(16),
543            lookahead_frames,
544            velocity: Vec3::ZERO,
545        }
546    }
547
548    /// Record a new camera position.
549    pub fn push_position(&mut self, pos: Vec3) {
550        if self.history.len() >= 16 { self.history.pop_front(); }
551        self.history.push_back(pos);
552        self.estimate_velocity();
553    }
554
555    fn estimate_velocity(&mut self) {
556        if self.history.len() < 2 {
557            self.velocity = Vec3::ZERO;
558            return;
559        }
560        let recent = self.history.back().copied().unwrap_or(Vec3::ZERO);
561        let old = self.history.front().copied().unwrap_or(Vec3::ZERO);
562        let n = self.history.len() as f32;
563        self.velocity = (recent - old) / n;
564    }
565
566    /// Predict where camera will be after `lookahead_frames` frames.
567    pub fn predicted_position(&self) -> Vec3 {
568        self.history.back().copied().unwrap_or(Vec3::ZERO)
569            + self.velocity * self.lookahead_frames as f32
570    }
571
572    /// Generate coords to prefetch based on predicted movement.
573    pub fn prefetch_coords(&self, config: &TerrainConfig) -> Vec<ChunkCoord> {
574        let pred = self.predicted_position();
575        let chunk_world = config.chunk_size as f32;
576        let pred_cx = (pred.x / chunk_world).floor() as i32;
577        let pred_cz = (pred.z / chunk_world).floor() as i32;
578        let prefetch_radius = 2i32;
579        let mut coords = Vec::new();
580        for dz in -prefetch_radius..=prefetch_radius {
581            for dx in -prefetch_radius..=prefetch_radius {
582                coords.push(ChunkCoord(pred_cx + dx, pred_cz + dz));
583            }
584        }
585        coords
586    }
587}
588
589// ── Worker Thread Pool ─────────────────────────────────────────────────────────
590
591type GenerateResult = (ChunkCoord, TerrainChunk);
592
593/// A fixed-size thread pool for chunk generation.
594pub struct GeneratorPool {
595    workers:    Vec<thread::JoinHandle<()>>,
596    tx:         std::sync::mpsc::Sender<ChunkCoord>,
597    rx_results: Arc<Mutex<Vec<GenerateResult>>>,
598    active:     Arc<AtomicUsize>,
599    config:     TerrainConfig,
600}
601
602impl GeneratorPool {
603    pub fn new(num_workers: usize, config: TerrainConfig) -> Self {
604        let (tx, rx) = std::sync::mpsc::channel::<ChunkCoord>();
605        let rx = Arc::new(Mutex::new(rx));
606        let results = Arc::new(Mutex::new(Vec::new()));
607        let active = Arc::new(AtomicUsize::new(0));
608        let mut workers = Vec::new();
609
610        for _ in 0..num_workers {
611            let rx2 = Arc::clone(&rx);
612            let res2 = Arc::clone(&results);
613            let act2 = Arc::clone(&active);
614            let cfg2 = config.clone();
615            let handle = thread::spawn(move || {
616                let gen = ChunkGenerator::new(cfg2);
617                loop {
618                    let coord = {
619                        let lock = rx2.lock().unwrap();
620                        match lock.recv() {
621                            Ok(c) => c,
622                            Err(_) => break,
623                        }
624                    };
625                    act2.fetch_add(1, Ordering::Relaxed);
626                    let chunk = gen.generate(coord);
627                    {
628                        let mut lock = res2.lock().unwrap();
629                        lock.push((coord, chunk));
630                    }
631                    act2.fetch_sub(1, Ordering::Relaxed);
632                }
633            });
634            workers.push(handle);
635        }
636
637        Self { workers, tx, rx_results: results, active, config }
638    }
639
640    /// Submit a coord for generation.
641    pub fn submit(&self, coord: ChunkCoord) {
642        let _ = self.tx.send(coord);
643    }
644
645    /// Drain completed results.
646    pub fn drain_results(&self) -> Vec<GenerateResult> {
647        let mut lock = self.rx_results.lock().unwrap();
648        std::mem::take(&mut *lock)
649    }
650
651    /// Number of workers currently generating.
652    pub fn active_count(&self) -> usize { self.active.load(Ordering::Relaxed) }
653}
654
655// ── StreamingManager ─────────────────────────────────────────────────────────
656
657/// Top-level coordinator for terrain chunk streaming.
658///
659/// Uses a thread pool for generation, an LRU cache, a priority load queue,
660/// a LOD scheduler, and a prefetcher to manage the full chunk lifecycle.
661pub struct StreamingManager {
662    pub config:       TerrainConfig,
663    pub cache:        ChunkCache,
664    pub load_queue:   LoadQueue,
665    pub lod_scheduler: LodScheduler,
666    pub prefetcher:   Prefetcher,
667    pub visibility:   VisibilitySet,
668    pub stats:        StreamingStats,
669    /// Coords currently being generated (submitted to pool).
670    in_flight: std::collections::HashSet<ChunkCoord>,
671    /// Generator pool.
672    pool:      Option<GeneratorPool>,
673    /// Camera position from last update.
674    camera_pos: Vec3,
675    /// Current LOD for each cached chunk.
676    chunk_lods: HashMap<ChunkCoord, u8>,
677}
678
679impl StreamingManager {
680    pub fn new(config: TerrainConfig) -> Self {
681        let lod_levels = config.lod_levels;
682        let chunk_size = config.chunk_size as f32;
683        let max_cache = (config.view_distance * 2 + 1).pow(2) as usize * 2;
684        let pool = GeneratorPool::new(4, config.clone());
685
686        Self {
687            lod_scheduler: LodScheduler::new(lod_levels, chunk_size),
688            cache:        ChunkCache::new(max_cache),
689            load_queue:   LoadQueue::new(),
690            prefetcher:   Prefetcher::new(8),
691            visibility:   VisibilitySet::new(),
692            stats:        StreamingStats::default(),
693            in_flight:    std::collections::HashSet::new(),
694            pool:         Some(pool),
695            camera_pos:   Vec3::ZERO,
696            chunk_lods:   HashMap::new(),
697            config,
698        }
699    }
700
701    /// Create without a thread pool (synchronous generation for testing).
702    pub fn new_synchronous(config: TerrainConfig) -> Self {
703        let lod_levels = config.lod_levels;
704        let chunk_size = config.chunk_size as f32;
705        let max_cache = (config.view_distance * 2 + 1).pow(2) as usize * 2;
706        Self {
707            lod_scheduler: LodScheduler::new(lod_levels, chunk_size),
708            cache:        ChunkCache::new(max_cache),
709            load_queue:   LoadQueue::new(),
710            prefetcher:   Prefetcher::new(8),
711            visibility:   VisibilitySet::new(),
712            stats:        StreamingStats::default(),
713            in_flight:    std::collections::HashSet::new(),
714            pool:         None,
715            camera_pos:   Vec3::ZERO,
716            chunk_lods:   HashMap::new(),
717            config,
718        }
719    }
720
721    /// Update the streaming system with a new camera position.
722    /// Call once per frame.
723    pub fn update(&mut self, camera_pos: Vec3) {
724        self.camera_pos = camera_pos;
725        self.prefetcher.push_position(camera_pos);
726
727        // Update visibility
728        self.visibility.update(camera_pos, &self.config);
729
730        // Queue newly visible chunks
731        for coord in self.visibility.newly_visible() {
732            if !self.cache.contains(coord) && !self.in_flight.contains(&coord) {
733                let priority = LodScheduler::load_priority(
734                    coord, camera_pos, self.config.chunk_size as f32, true,
735                );
736                self.load_queue.push(coord, priority);
737            }
738        }
739
740        // Queue prefetch coords
741        let prefetch = self.prefetcher.prefetch_coords(&self.config);
742        for coord in prefetch {
743            if !self.cache.contains(coord) && !self.in_flight.contains(&coord) {
744                let priority = LodScheduler::load_priority(
745                    coord, camera_pos, self.config.chunk_size as f32, false,
746                ) - 500_000; // lower priority than visible
747                self.load_queue.push(coord, priority);
748            }
749        }
750
751        // Reprioritize queue
752        self.load_queue.reprioritize(camera_pos, &self.config);
753
754        // Submit chunks to pool (limit in-flight)
755        let max_in_flight = 8usize;
756        while self.in_flight.len() < max_in_flight {
757            if let Some(coord) = self.load_queue.pop() {
758                if self.pool.is_some() {
759                    self.pool.as_ref().unwrap().submit(coord);
760                    self.in_flight.insert(coord);
761                } else {
762                    // Synchronous mode: generate immediately
763                    let t0 = std::time::Instant::now();
764                    let gen = ChunkGenerator::new(self.config.clone());
765                    let chunk = gen.generate(coord);
766                    self.stats.generate_time_ms += t0.elapsed().as_secs_f64() * 1000.0;
767                    self.chunk_lods.insert(coord, chunk.lod_level);
768                    self.cache.insert(coord, chunk);
769                    self.stats.chunks_loaded += 1;
770                    self.stats.cache_misses += 1;
771                }
772            } else {
773                break;
774            }
775        }
776
777        // Drain completed results from pool
778        if let Some(ref pool) = self.pool {
779            for (coord, chunk) in pool.drain_results() {
780                self.in_flight.remove(&coord);
781                self.chunk_lods.insert(coord, chunk.lod_level);
782                self.cache.insert(coord, chunk);
783                self.stats.chunks_loaded += 1;
784                self.stats.cache_misses += 1;
785            }
786        }
787
788        // Unload invisible chunks that exceed cache budget
789        for coord in self.visibility.newly_hidden() {
790            if self.cache.contains(coord) {
791                self.cache.remove(coord);
792                self.chunk_lods.remove(&coord);
793                self.stats.chunks_unloaded += 1;
794            }
795        }
796
797        // LOD scheduler update
798        self.lod_scheduler.update(camera_pos, &self.chunk_lods, &self.config);
799
800        self.stats.pending_count = self.load_queue.len() + self.in_flight.len();
801        self.stats.memory_bytes = self.cache.current_memory_bytes();
802    }
803
804    /// Get a chunk by coordinate. Returns None if not yet loaded.
805    pub fn get_chunk(&mut self, coord: ChunkCoord) -> Option<&TerrainChunk> {
806        if let Some(chunk) = self.cache.get(coord) {
807            self.stats.cache_hits += 1;
808            Some(chunk)
809        } else {
810            self.stats.cache_misses += 1;
811            None
812        }
813    }
814
815    /// Force-load a chunk synchronously (blocks until generated).
816    pub fn force_load(&mut self, coord: ChunkCoord) -> &TerrainChunk {
817        if !self.cache.contains(coord) {
818            let gen = ChunkGenerator::new(self.config.clone());
819            let chunk = gen.generate(coord);
820            self.chunk_lods.insert(coord, chunk.lod_level);
821            self.cache.insert(coord, chunk);
822            self.stats.chunks_loaded += 1;
823        }
824        self.cache.get(coord).unwrap()
825    }
826
827    /// Sample height at world coordinates (interpolates between chunks if needed).
828    pub fn sample_height_world(&mut self, world_x: f32, world_z: f32) -> f32 {
829        let chunk_world = self.config.chunk_size as f32;
830        let cx = (world_x / chunk_world).floor() as i32;
831        let cz = (world_z / chunk_world).floor() as i32;
832        let coord = ChunkCoord(cx, cz);
833        let local_x = world_x - cx as f32 * chunk_world;
834        let local_z = world_z - cz as f32 * chunk_world;
835        if let Some(chunk) = self.cache.get(coord) {
836            let hm = &chunk.heightmap;
837            let sx = (local_x / chunk_world * hm.width as f32).clamp(0.0, hm.width as f32 - 1.0);
838            let sz = (local_z / chunk_world * hm.height as f32).clamp(0.0, hm.height as f32 - 1.0);
839            hm.sample_bilinear(sx, sz)
840        } else {
841            0.0
842        }
843    }
844
845    pub fn stats(&self) -> &StreamingStats { &self.stats }
846    pub fn visible_chunk_count(&self) -> usize { self.visibility.visible_count() }
847    pub fn cache_size(&self) -> usize { self.cache.len() }
848    pub fn in_flight_count(&self) -> usize { self.in_flight.len() }
849}
850
851// ── Tests ─────────────────────────────────────────────────────────────────────
852
853#[cfg(test)]
854mod tests {
855    use super::*;
856
857    fn test_config() -> TerrainConfig {
858        TerrainConfig {
859            chunk_size: 16,
860            view_distance: 2,
861            lod_levels: 3,
862            seed: 42,
863        }
864    }
865
866    #[test]
867    fn test_chunk_cache_insert_get() {
868        let config = test_config();
869        let gen = ChunkGenerator::new(config.clone());
870        let chunk = gen.generate(ChunkCoord(0, 0));
871        let mut cache = ChunkCache::new(10);
872        cache.insert(ChunkCoord(0, 0), chunk);
873        assert!(cache.contains(ChunkCoord(0, 0)));
874        assert!(cache.get(ChunkCoord(0, 0)).is_some());
875        assert!(!cache.contains(ChunkCoord(1, 0)));
876    }
877
878    #[test]
879    fn test_chunk_cache_lru_eviction() {
880        let config = test_config();
881        let gen = ChunkGenerator::new(config.clone());
882        let mut cache = ChunkCache::new(3);
883        for i in 0..4i32 {
884            let chunk = gen.generate(ChunkCoord(i, 0));
885            cache.insert(ChunkCoord(i, 0), chunk);
886        }
887        assert!(cache.len() <= 3);
888    }
889
890    #[test]
891    fn test_load_queue_priority() {
892        let mut q = LoadQueue::new();
893        q.push(ChunkCoord(5, 0), -50);
894        q.push(ChunkCoord(1, 0), -10);
895        q.push(ChunkCoord(3, 0), -30);
896        let first = q.pop().unwrap();
897        assert_eq!(first, ChunkCoord(1, 0)); // highest priority = -10
898    }
899
900    #[test]
901    fn test_load_queue_no_duplicates() {
902        let mut q = LoadQueue::new();
903        q.push(ChunkCoord(0, 0), 100);
904        q.push(ChunkCoord(0, 0), 200); // duplicate
905        assert_eq!(q.len(), 1);
906    }
907
908    #[test]
909    fn test_chunk_generator() {
910        let config = test_config();
911        let gen = ChunkGenerator::new(config);
912        let chunk = gen.generate(ChunkCoord(0, 0));
913        assert_eq!(chunk.coord, ChunkCoord(0, 0));
914        assert!(chunk.heightmap.width > 0);
915        assert_eq!(chunk.heightmap.data.len(), chunk.heightmap.width * chunk.heightmap.height);
916    }
917
918    #[test]
919    fn test_chunk_serializer_roundtrip() {
920        let config = test_config();
921        let gen = ChunkGenerator::new(config);
922        let chunk = gen.generate(ChunkCoord(2, -3));
923        let bytes = ChunkSerializer::serialize(&chunk);
924        let restored = ChunkSerializer::deserialize(&bytes).expect("deserialize failed");
925        assert_eq!(restored.coord, chunk.coord);
926        assert_eq!(restored.seed, chunk.seed);
927        assert_eq!(restored.heightmap.width, chunk.heightmap.width);
928        for (a, b) in chunk.heightmap.data.iter().zip(restored.heightmap.data.iter()) {
929            assert!((a - b).abs() < 1e-6);
930        }
931    }
932
933    #[test]
934    fn test_chunk_serializer_invalid_magic() {
935        let mut bytes = vec![0u8; 64];
936        bytes[0] = 0xFF; // wrong magic
937        assert!(ChunkSerializer::deserialize(&bytes).is_none());
938    }
939
940    #[test]
941    fn test_visibility_set_update() {
942        let config = test_config();
943        let mut vis = VisibilitySet::new();
944        vis.update(Vec3::new(0.0, 0.0, 0.0), &config);
945        assert!(vis.visible_count() > 0);
946        assert!(vis.is_visible(ChunkCoord(0, 0)));
947    }
948
949    #[test]
950    fn test_lod_scheduler_desired_lod() {
951        let sched = LodScheduler::new(3, 32.0);
952        assert_eq!(sched.desired_lod(10.0, 0), 0);  // close → LOD 0
953        let far = sched.lod_thresholds[1] + 1.0;
954        // With hysteresis: starting from LOD 0, need to exceed threshold + margin
955        let lod = sched.desired_lod(far, 0);
956        assert!(lod >= 1);
957    }
958
959    #[test]
960    fn test_lod_scheduler_hysteresis() {
961        let sched = LodScheduler::new(3, 32.0);
962        let threshold = sched.lod_thresholds[0];
963        let margin = sched.hysteresis_margin;
964        // Just beyond threshold but within margin: should stay at LOD 0
965        let dist = threshold + margin * 0.5;
966        assert_eq!(sched.desired_lod(dist, 0), 0);
967        // Beyond threshold + margin: should upgrade
968        let dist2 = threshold + margin + 1.0;
969        assert!(sched.desired_lod(dist2, 0) >= 1);
970    }
971
972    #[test]
973    fn test_prefetcher() {
974        let mut pf = Prefetcher::new(8);
975        pf.push_position(Vec3::new(0.0, 0.0, 0.0));
976        pf.push_position(Vec3::new(10.0, 0.0, 0.0));
977        let pred = pf.predicted_position();
978        // Predicted position should be ahead of current
979        assert!(pred.x >= 10.0);
980    }
981
982    #[test]
983    fn test_streaming_manager_synchronous() {
984        let config = test_config();
985        let mut mgr = StreamingManager::new_synchronous(config);
986        mgr.update(Vec3::new(0.0, 0.0, 0.0));
987        // After update, some chunks should be loaded
988        assert!(mgr.cache_size() > 0 || mgr.stats.pending_count >= 0);
989    }
990
991    #[test]
992    fn test_streaming_manager_force_load() {
993        let config = test_config();
994        let mut mgr = StreamingManager::new_synchronous(config);
995        let chunk = mgr.force_load(ChunkCoord(0, 0));
996        assert_eq!(chunk.coord, ChunkCoord(0, 0));
997        assert_eq!(mgr.cache_size(), 1);
998    }
999}
1000
1001// ── Extended Streaming Utilities ──────────────────────────────────────────────
1002
1003/// Serialization format options.
1004#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1005pub enum SerializationFormat {
1006    /// Raw binary (no compression).
1007    Raw,
1008    /// Run-length encoded (good for uniform terrain).
1009    RLE,
1010    /// Delta-encoded (good for slowly varying terrain).
1011    Delta,
1012}
1013
1014/// Extended chunk serializer with format support.
1015pub struct ExtendedChunkSerializer;
1016
1017impl ExtendedChunkSerializer {
1018    /// Serialize with the given format.
1019    pub fn serialize_with_format(
1020        chunk: &crate::terrain::mod_types::TerrainChunk,
1021        format: SerializationFormat,
1022    ) -> Vec<u8> {
1023        let base = ChunkSerializer::serialize(chunk);
1024        match format {
1025            SerializationFormat::Raw   => base,
1026            SerializationFormat::RLE   => Self::rle_encode(&base),
1027            SerializationFormat::Delta => Self::delta_encode_bytes(&base),
1028        }
1029    }
1030
1031    /// Deserialize with the given format.
1032    pub fn deserialize_with_format(
1033        bytes: &[u8],
1034        format: SerializationFormat,
1035    ) -> Option<crate::terrain::mod_types::TerrainChunk> {
1036        let decoded = match format {
1037            SerializationFormat::Raw   => bytes.to_vec(),
1038            SerializationFormat::RLE   => Self::rle_decode(bytes)?,
1039            SerializationFormat::Delta => Self::delta_decode_bytes(bytes)?,
1040        };
1041        ChunkSerializer::deserialize(&decoded)
1042    }
1043
1044    /// Simple run-length encoding for byte streams.
1045    pub fn rle_encode(data: &[u8]) -> Vec<u8> {
1046        if data.is_empty() { return Vec::new(); }
1047        let mut out = Vec::new();
1048        let mut i = 0;
1049        while i < data.len() {
1050            let val = data[i];
1051            let mut run = 1usize;
1052            while i + run < data.len() && data[i + run] == val && run < 255 {
1053                run += 1;
1054            }
1055            out.push(run as u8);
1056            out.push(val);
1057            i += run;
1058        }
1059        out
1060    }
1061
1062    /// Decode run-length encoded data.
1063    pub fn rle_decode(data: &[u8]) -> Option<Vec<u8>> {
1064        if data.len() % 2 != 0 { return None; }
1065        let mut out = Vec::new();
1066        let mut i = 0;
1067        while i + 1 < data.len() {
1068            let count = data[i] as usize;
1069            let val   = data[i + 1];
1070            for _ in 0..count { out.push(val); }
1071            i += 2;
1072        }
1073        Some(out)
1074    }
1075
1076    /// Delta encode: store first byte raw, then differences.
1077    pub fn delta_encode_bytes(data: &[u8]) -> Vec<u8> {
1078        if data.is_empty() { return Vec::new(); }
1079        let mut out = Vec::with_capacity(data.len());
1080        out.push(data[0]);
1081        for i in 1..data.len() {
1082            out.push(data[i].wrapping_sub(data[i - 1]));
1083        }
1084        out
1085    }
1086
1087    /// Decode delta-encoded bytes.
1088    pub fn delta_decode_bytes(data: &[u8]) -> Option<Vec<u8>> {
1089        if data.is_empty() { return Some(Vec::new()); }
1090        let mut out = Vec::with_capacity(data.len());
1091        out.push(data[0]);
1092        for i in 1..data.len() {
1093            out.push(data[i].wrapping_add(*out.last().unwrap()));
1094        }
1095        Some(out)
1096    }
1097}
1098
1099// ── Chunk Event System ────────────────────────────────────────────────────────
1100
1101/// Events emitted by the streaming system.
1102#[derive(Clone, Debug)]
1103pub enum ChunkEvent {
1104    Loaded(ChunkCoord),
1105    Unloaded(ChunkCoord),
1106    LodChanged { coord: ChunkCoord, old_lod: u8, new_lod: u8 },
1107    GenerationStarted(ChunkCoord),
1108    GenerationFailed { coord: ChunkCoord, reason: String },
1109}
1110
1111/// Simple event queue for chunk events.
1112#[derive(Debug, Default)]
1113pub struct ChunkEventQueue {
1114    events: VecDeque<ChunkEvent>,
1115    max_size: usize,
1116}
1117
1118impl ChunkEventQueue {
1119    pub fn new(max_size: usize) -> Self {
1120        Self { events: VecDeque::new(), max_size }
1121    }
1122
1123    pub fn push(&mut self, event: ChunkEvent) {
1124        if self.events.len() >= self.max_size {
1125            self.events.pop_front();
1126        }
1127        self.events.push_back(event);
1128    }
1129
1130    pub fn drain(&mut self) -> Vec<ChunkEvent> {
1131        self.events.drain(..).collect()
1132    }
1133
1134    pub fn len(&self) -> usize { self.events.len() }
1135    pub fn is_empty(&self) -> bool { self.events.is_empty() }
1136}
1137
1138// ── Memory Budget Tracker ─────────────────────────────────────────────────────
1139
1140/// Tracks and enforces memory budget for the streaming system.
1141#[derive(Debug, Clone)]
1142pub struct MemoryBudget {
1143    /// Maximum allowed memory in bytes.
1144    pub max_bytes:      usize,
1145    /// Currently used memory in bytes.
1146    pub current_bytes:  usize,
1147    /// Reserved headroom (keep this much free).
1148    pub headroom_bytes: usize,
1149    /// Peak memory usage seen.
1150    pub peak_bytes:     usize,
1151}
1152
1153impl MemoryBudget {
1154    pub fn new(max_bytes: usize) -> Self {
1155        Self {
1156            max_bytes,
1157            current_bytes: 0,
1158            headroom_bytes: max_bytes / 10,
1159            peak_bytes: 0,
1160        }
1161    }
1162
1163    pub fn allocate(&mut self, bytes: usize) -> bool {
1164        if self.current_bytes + bytes + self.headroom_bytes > self.max_bytes {
1165            return false;
1166        }
1167        self.current_bytes += bytes;
1168        if self.current_bytes > self.peak_bytes {
1169            self.peak_bytes = self.current_bytes;
1170        }
1171        true
1172    }
1173
1174    pub fn free(&mut self, bytes: usize) {
1175        self.current_bytes = self.current_bytes.saturating_sub(bytes);
1176    }
1177
1178    pub fn utilization(&self) -> f32 {
1179        self.current_bytes as f32 / self.max_bytes as f32
1180    }
1181
1182    pub fn available(&self) -> usize {
1183        self.max_bytes.saturating_sub(self.current_bytes + self.headroom_bytes)
1184    }
1185
1186    pub fn is_over_budget(&self) -> bool {
1187        self.current_bytes + self.headroom_bytes > self.max_bytes
1188    }
1189}
1190
1191// ── Terrain World Map ─────────────────────────────────────────────────────────
1192
1193/// Low-resolution overview map of the entire world, used for minimap and LOD hints.
1194#[derive(Debug)]
1195pub struct WorldMap {
1196    /// Low-resolution height overview.
1197    pub height_overview:  crate::terrain::heightmap::HeightMap,
1198    /// Biome overview (compressed to u8 per cell).
1199    pub biome_overview:   Vec<u8>,
1200    /// Overview resolution.
1201    pub resolution:       usize,
1202    /// World size in chunks.
1203    pub world_size_chunks: usize,
1204}
1205
1206impl WorldMap {
1207    /// Generate a world map by sampling the global noise function.
1208    pub fn generate(world_size_chunks: usize, resolution: usize, config: &TerrainConfig) -> Self {
1209        let scale = world_size_chunks as f32 / resolution as f32;
1210        let height_overview = crate::terrain::heightmap::FractalNoise::generate(
1211            resolution, resolution, 6, 2.0, 0.5, 2.0, config.seed,
1212        );
1213        let biome_overview: Vec<u8> = height_overview.data.iter()
1214            .map(|&h| {
1215                let biome = if h < 0.1 { crate::terrain::biome::BiomeType::Ocean }
1216                    else if h < 0.2 { crate::terrain::biome::BiomeType::Beach }
1217                    else if h < 0.5 { crate::terrain::biome::BiomeType::Grassland }
1218                    else if h < 0.7 { crate::terrain::biome::BiomeType::TemperateForest }
1219                    else if h < 0.85 { crate::terrain::biome::BiomeType::Mountain }
1220                    else { crate::terrain::biome::BiomeType::AlpineGlacier };
1221                biome as u8
1222            })
1223            .collect();
1224        Self { height_overview, biome_overview, resolution, world_size_chunks }
1225    }
1226
1227    /// Sample height at normalized world position (0..1).
1228    pub fn sample_height(&self, nx: f32, ny: f32) -> f32 {
1229        let x = (nx * (self.resolution - 1) as f32).clamp(0.0, (self.resolution - 1) as f32);
1230        let y = (ny * (self.resolution - 1) as f32).clamp(0.0, (self.resolution - 1) as f32);
1231        self.height_overview.sample_bilinear(x, y)
1232    }
1233
1234    /// Get biome at normalized world position.
1235    pub fn sample_biome(&self, nx: f32, ny: f32) -> crate::terrain::biome::BiomeType {
1236        let x = (nx * (self.resolution - 1) as f32) as usize;
1237        let y = (ny * (self.resolution - 1) as f32) as usize;
1238        let idx = y.min(self.resolution - 1) * self.resolution + x.min(self.resolution - 1);
1239        crate::terrain::biome::biome_from_index(self.biome_overview[idx] as usize)
1240    }
1241
1242    /// Convert chunk coord to normalized world position.
1243    pub fn chunk_to_normalized(&self, coord: ChunkCoord) -> (f32, f32) {
1244        (
1245            (coord.0 as f32 + 0.5) / self.world_size_chunks as f32,
1246            (coord.1 as f32 + 0.5) / self.world_size_chunks as f32,
1247        )
1248    }
1249}
1250
1251// ── Chunk Diff ────────────────────────────────────────────────────────────────
1252
1253/// Records the difference between two chunk heightmaps (for terrain editing).
1254#[derive(Clone, Debug)]
1255pub struct ChunkDiff {
1256    pub coord:   ChunkCoord,
1257    /// Sparse list of (x, y, old_height, new_height).
1258    pub changes: Vec<(usize, usize, f32, f32)>,
1259}
1260
1261impl ChunkDiff {
1262    pub fn new(coord: ChunkCoord) -> Self {
1263        Self { coord, changes: Vec::new() }
1264    }
1265
1266    /// Record a height change.
1267    pub fn record(&mut self, x: usize, y: usize, old_h: f32, new_h: f32) {
1268        if (old_h - new_h).abs() > 1e-6 {
1269            self.changes.push((x, y, old_h, new_h));
1270        }
1271    }
1272
1273    /// Apply this diff to a heightmap.
1274    pub fn apply(&self, hm: &mut crate::terrain::heightmap::HeightMap) {
1275        for &(x, y, _, new_h) in &self.changes {
1276            hm.set(x, y, new_h);
1277        }
1278    }
1279
1280    /// Reverse (undo) this diff.
1281    pub fn undo(&self, hm: &mut crate::terrain::heightmap::HeightMap) {
1282        for &(x, y, old_h, _) in &self.changes {
1283            hm.set(x, y, old_h);
1284        }
1285    }
1286
1287    pub fn is_empty(&self) -> bool { self.changes.is_empty() }
1288    pub fn len(&self) -> usize { self.changes.len() }
1289}
1290
1291// ── Streaming Profiler ────────────────────────────────────────────────────────
1292
1293/// Profiling data for the streaming system.
1294#[derive(Debug, Default, Clone)]
1295pub struct StreamingProfiler {
1296    pub frame_count:         u64,
1297    pub total_generate_ms:   f64,
1298    pub total_serialize_ms:  f64,
1299    pub total_deserialize_ms: f64,
1300    pub max_generate_ms:     f64,
1301    pub min_generate_ms:     f64,
1302    pub chunks_per_second:   f32,
1303    pub last_frame_ms:       f64,
1304}
1305
1306impl StreamingProfiler {
1307    pub fn new() -> Self {
1308        Self { min_generate_ms: f64::INFINITY, ..Default::default() }
1309    }
1310
1311    pub fn record_generate(&mut self, ms: f64) {
1312        self.total_generate_ms += ms;
1313        self.frame_count += 1;
1314        if ms > self.max_generate_ms { self.max_generate_ms = ms; }
1315        if ms < self.min_generate_ms { self.min_generate_ms = ms; }
1316    }
1317
1318    pub fn average_generate_ms(&self) -> f64 {
1319        if self.frame_count == 0 { 0.0 } else { self.total_generate_ms / self.frame_count as f64 }
1320    }
1321
1322    pub fn reset(&mut self) { *self = Self::new(); }
1323}
1324
1325// ── Priority Zones ────────────────────────────────────────────────────────────
1326
1327/// Defines priority zones that affect chunk loading order.
1328/// E.g., player start location, POIs, scripted events.
1329#[derive(Clone, Debug)]
1330pub struct PriorityZone {
1331    /// World-space center of the zone.
1332    pub center: Vec3,
1333    /// Radius of influence (world units).
1334    pub radius: f32,
1335    /// Priority bonus applied to chunks in this zone.
1336    pub priority_bonus: i64,
1337    /// Optional name for debugging.
1338    pub name: String,
1339}
1340
1341impl PriorityZone {
1342    pub fn new(center: Vec3, radius: f32, priority_bonus: i64) -> Self {
1343        Self { center, radius, priority_bonus, name: String::new() }
1344    }
1345
1346    pub fn named(mut self, name: &str) -> Self {
1347        self.name = name.to_string();
1348        self
1349    }
1350
1351    pub fn contains_world_pos(&self, pos: Vec3) -> bool {
1352        let dx = pos.x - self.center.x;
1353        let dz = pos.z - self.center.z;
1354        dx * dx + dz * dz <= self.radius * self.radius
1355    }
1356
1357    pub fn chunk_priority(&self, coord: ChunkCoord, chunk_size: f32) -> i64 {
1358        let world_pos = coord.to_world_pos(chunk_size);
1359        if self.contains_world_pos(world_pos) {
1360            self.priority_bonus
1361        } else {
1362            let dx = world_pos.x - self.center.x;
1363            let dz = world_pos.z - self.center.z;
1364            let dist = (dx * dx + dz * dz).sqrt();
1365            let falloff = (1.0 - (dist / self.radius).min(2.0)) * 0.5;
1366            (self.priority_bonus as f32 * falloff.max(0.0)) as i64
1367        }
1368    }
1369}
1370
1371/// Manages multiple priority zones and computes combined priority bonuses.
1372#[derive(Debug, Default)]
1373pub struct PriorityZoneManager {
1374    pub zones: Vec<PriorityZone>,
1375}
1376
1377impl PriorityZoneManager {
1378    pub fn new() -> Self { Self::default() }
1379
1380    pub fn add_zone(&mut self, zone: PriorityZone) {
1381        self.zones.push(zone);
1382    }
1383
1384    pub fn remove_zone_by_name(&mut self, name: &str) {
1385        self.zones.retain(|z| z.name != name);
1386    }
1387
1388    pub fn total_priority_bonus(&self, coord: ChunkCoord, chunk_size: f32) -> i64 {
1389        self.zones.iter().map(|z| z.chunk_priority(coord, chunk_size)).sum()
1390    }
1391}
1392
1393// ── Chunk Hitlist ─────────────────────────────────────────────────────────────
1394
1395/// A list of chunks that must be loaded before play can begin.
1396#[derive(Debug)]
1397pub struct ChunkHitlist {
1398    required: std::collections::HashSet<ChunkCoord>,
1399    loaded:   std::collections::HashSet<ChunkCoord>,
1400}
1401
1402impl ChunkHitlist {
1403    pub fn new() -> Self {
1404        Self {
1405            required: std::collections::HashSet::new(),
1406            loaded:   std::collections::HashSet::new(),
1407        }
1408    }
1409
1410    pub fn require(&mut self, coord: ChunkCoord) {
1411        self.required.insert(coord);
1412    }
1413
1414    pub fn mark_loaded(&mut self, coord: ChunkCoord) {
1415        if self.required.contains(&coord) {
1416            self.loaded.insert(coord);
1417        }
1418    }
1419
1420    pub fn is_complete(&self) -> bool {
1421        self.required.iter().all(|c| self.loaded.contains(c))
1422    }
1423
1424    pub fn completion_fraction(&self) -> f32 {
1425        if self.required.is_empty() { return 1.0; }
1426        self.loaded.len() as f32 / self.required.len() as f32
1427    }
1428
1429    pub fn pending_coords(&self) -> Vec<ChunkCoord> {
1430        self.required.iter().filter(|c| !self.loaded.contains(*c)).copied().collect()
1431    }
1432}
1433
1434impl Default for ChunkHitlist {
1435    fn default() -> Self { Self::new() }
1436}
1437
1438// ── Distance-based LOD Bias ───────────────────────────────────────────────────
1439
1440/// Adjusts LOD thresholds based on terrain importance (e.g., near a city).
1441#[derive(Clone, Debug)]
1442pub struct LodBias {
1443    /// LOD multiplier: > 1.0 means higher quality (further LOD0 distance).
1444    pub quality_multiplier: f32,
1445    /// Region center.
1446    pub center: Vec3,
1447    /// Radius of effect.
1448    pub radius: f32,
1449}
1450
1451impl LodBias {
1452    pub fn new(center: Vec3, radius: f32, quality_multiplier: f32) -> Self {
1453        Self { quality_multiplier, center, radius }
1454    }
1455
1456    pub fn lod_multiplier_at(&self, pos: Vec3) -> f32 {
1457        let dx = pos.x - self.center.x;
1458        let dz = pos.z - self.center.z;
1459        let dist = (dx * dx + dz * dz).sqrt();
1460        if dist < self.radius {
1461            let t = 1.0 - dist / self.radius;
1462            1.0 + (self.quality_multiplier - 1.0) * t
1463        } else {
1464            1.0
1465        }
1466    }
1467}
1468
1469// ── Extended Streaming Tests ──────────────────────────────────────────────────
1470
1471#[cfg(test)]
1472mod extended_streaming_tests {
1473    use super::*;
1474
1475    fn test_config() -> TerrainConfig {
1476        TerrainConfig { chunk_size: 16, view_distance: 2, lod_levels: 3, seed: 42 }
1477    }
1478
1479    #[test]
1480    fn test_rle_roundtrip() {
1481        let data: Vec<u8> = vec![1, 1, 1, 2, 3, 3, 4, 4, 4, 4];
1482        let encoded = ExtendedChunkSerializer::rle_encode(&data);
1483        let decoded = ExtendedChunkSerializer::rle_decode(&encoded).unwrap();
1484        assert_eq!(data, decoded);
1485    }
1486
1487    #[test]
1488    fn test_delta_encode_roundtrip() {
1489        let data: Vec<u8> = vec![10, 12, 11, 15, 14, 20, 18];
1490        let enc = ExtendedChunkSerializer::delta_encode_bytes(&data);
1491        let dec = ExtendedChunkSerializer::delta_decode_bytes(&enc).unwrap();
1492        assert_eq!(data, dec);
1493    }
1494
1495    #[test]
1496    fn test_chunk_event_queue() {
1497        let mut q = ChunkEventQueue::new(100);
1498        q.push(ChunkEvent::Loaded(ChunkCoord(0, 0)));
1499        q.push(ChunkEvent::Unloaded(ChunkCoord(1, 0)));
1500        assert_eq!(q.len(), 2);
1501        let events = q.drain();
1502        assert_eq!(events.len(), 2);
1503        assert!(q.is_empty());
1504    }
1505
1506    #[test]
1507    fn test_memory_budget() {
1508        let mut budget = MemoryBudget::new(1024 * 1024);
1509        assert!(budget.allocate(100_000));
1510        budget.free(100_000);
1511        assert_eq!(budget.current_bytes, 0);
1512        assert!(!budget.is_over_budget());
1513    }
1514
1515    #[test]
1516    fn test_memory_budget_over() {
1517        let mut budget = MemoryBudget::new(1000);
1518        assert!(!budget.allocate(1000)); // headroom would be exceeded
1519    }
1520
1521    #[test]
1522    fn test_world_map_generation() {
1523        let config = test_config();
1524        let wm = WorldMap::generate(64, 32, &config);
1525        assert_eq!(wm.height_overview.data.len(), 32 * 32);
1526        assert_eq!(wm.biome_overview.len(), 32 * 32);
1527    }
1528
1529    #[test]
1530    fn test_world_map_sample() {
1531        let config = test_config();
1532        let wm = WorldMap::generate(64, 32, &config);
1533        let h = wm.sample_height(0.5, 0.5);
1534        assert!(h >= 0.0 && h <= 1.0);
1535    }
1536
1537    #[test]
1538    fn test_chunk_diff_apply_undo() {
1539        let config = test_config();
1540        let gen = ChunkGenerator::new(config);
1541        let mut chunk = gen.generate(ChunkCoord(0, 0));
1542        let old_h = chunk.heightmap.get(5, 5);
1543        let new_h = 0.9f32;
1544        let mut diff = ChunkDiff::new(ChunkCoord(0, 0));
1545        diff.record(5, 5, old_h, new_h);
1546        diff.apply(&mut chunk.heightmap);
1547        assert!((chunk.heightmap.get(5, 5) - new_h).abs() < 1e-6);
1548        diff.undo(&mut chunk.heightmap);
1549        assert!((chunk.heightmap.get(5, 5) - old_h).abs() < 1e-6);
1550    }
1551
1552    #[test]
1553    fn test_priority_zone() {
1554        let zone = PriorityZone::new(Vec3::new(0.0, 0.0, 0.0), 100.0, 500_000);
1555        assert!(zone.contains_world_pos(Vec3::new(50.0, 0.0, 50.0)));
1556        assert!(!zone.contains_world_pos(Vec3::new(200.0, 0.0, 200.0)));
1557        let bonus = zone.chunk_priority(ChunkCoord(0, 0), 16.0);
1558        assert!(bonus > 0);
1559    }
1560
1561    #[test]
1562    fn test_priority_zone_manager() {
1563        let mut mgr = PriorityZoneManager::new();
1564        mgr.add_zone(PriorityZone::new(Vec3::ZERO, 100.0, 100_000).named("start"));
1565        let bonus = mgr.total_priority_bonus(ChunkCoord(0, 0), 16.0);
1566        assert!(bonus > 0);
1567        mgr.remove_zone_by_name("start");
1568        assert!(mgr.zones.is_empty());
1569    }
1570
1571    #[test]
1572    fn test_chunk_hitlist() {
1573        let mut hl = ChunkHitlist::new();
1574        hl.require(ChunkCoord(0, 0));
1575        hl.require(ChunkCoord(1, 0));
1576        assert!(!hl.is_complete());
1577        assert!((hl.completion_fraction() - 0.0).abs() < 1e-5);
1578        hl.mark_loaded(ChunkCoord(0, 0));
1579        assert!((hl.completion_fraction() - 0.5).abs() < 1e-5);
1580        hl.mark_loaded(ChunkCoord(1, 0));
1581        assert!(hl.is_complete());
1582    }
1583
1584    #[test]
1585    fn test_lod_bias() {
1586        let bias = LodBias::new(Vec3::ZERO, 100.0, 2.0);
1587        let at_center = bias.lod_multiplier_at(Vec3::ZERO);
1588        assert!((at_center - 2.0).abs() < 1e-4);
1589        let far_away = bias.lod_multiplier_at(Vec3::new(200.0, 0.0, 0.0));
1590        assert!((far_away - 1.0).abs() < 1e-4);
1591    }
1592
1593    #[test]
1594    fn test_streaming_profiler() {
1595        let mut prof = StreamingProfiler::new();
1596        prof.record_generate(5.0);
1597        prof.record_generate(10.0);
1598        prof.record_generate(3.0);
1599        assert!((prof.average_generate_ms() - 6.0).abs() < 1e-4);
1600        assert!((prof.max_generate_ms - 10.0).abs() < 1e-4);
1601        assert!((prof.min_generate_ms - 3.0).abs() < 1e-4);
1602    }
1603}
1604
1605// ── Terrain Patch System ──────────────────────────────────────────────────────
1606
1607/// A small editable terrain patch (sub-chunk resolution editing).
1608#[derive(Clone, Debug)]
1609pub struct TerrainPatch {
1610    pub coord:       ChunkCoord,
1611    pub offset_x:    usize,
1612    pub offset_z:    usize,
1613    pub width:       usize,
1614    pub height:      usize,
1615    pub data:        Vec<f32>,
1616    pub dirty:       bool,
1617}
1618
1619impl TerrainPatch {
1620    pub fn new(coord: ChunkCoord, offset_x: usize, offset_z: usize, width: usize, height: usize) -> Self {
1621        Self {
1622            coord, offset_x, offset_z, width, height,
1623            data: vec![0.0f32; width * height],
1624            dirty: false,
1625        }
1626    }
1627
1628    pub fn get(&self, x: usize, z: usize) -> f32 {
1629        if x < self.width && z < self.height { self.data[z * self.width + x] } else { 0.0 }
1630    }
1631
1632    pub fn set(&mut self, x: usize, z: usize, v: f32) {
1633        if x < self.width && z < self.height {
1634            self.data[z * self.width + x] = v.clamp(0.0, 1.0);
1635            self.dirty = true;
1636        }
1637    }
1638
1639    /// Apply this patch to the corresponding chunk heightmap.
1640    pub fn apply_to_chunk(&self, chunk: &mut crate::terrain::mod_types::TerrainChunk) {
1641        for z in 0..self.height {
1642            for x in 0..self.width {
1643                let cx = self.offset_x + x;
1644                let cz = self.offset_z + z;
1645                chunk.heightmap.set(cx, cz, self.get(x, z));
1646            }
1647        }
1648    }
1649
1650    /// Read current values from a chunk into this patch.
1651    pub fn read_from_chunk(&mut self, chunk: &crate::terrain::mod_types::TerrainChunk) {
1652        for z in 0..self.height {
1653            for x in 0..self.width {
1654                let cx = self.offset_x + x;
1655                let cz = self.offset_z + z;
1656                self.set(x, z, chunk.heightmap.get(cx, cz));
1657            }
1658        }
1659        self.dirty = false;
1660    }
1661}
1662
1663// ── Neighbor Stitching ────────────────────────────────────────────────────────
1664
1665/// Stitches chunk borders to eliminate seams between adjacent chunks.
1666pub struct ChunkStitcher;
1667
1668impl ChunkStitcher {
1669    /// Blend the border rows of two adjacent chunks for seamless transitions.
1670    /// `primary` is the chunk to modify; `neighbor` provides the reference values.
1671    /// `edge` is which edge of `primary` borders `neighbor`.
1672    pub fn stitch_edge(
1673        primary:  &mut crate::terrain::mod_types::TerrainChunk,
1674        neighbor: &crate::terrain::mod_types::TerrainChunk,
1675        edge:     StitchEdge,
1676        blend_width: usize,
1677    ) {
1678        let pw = primary.heightmap.width;
1679        let ph = primary.heightmap.height;
1680        let nw = neighbor.heightmap.width;
1681        let nh = neighbor.heightmap.height;
1682
1683        match edge {
1684            StitchEdge::East => {
1685                for z in 0..ph {
1686                    let nz = (z as f32 / ph as f32 * nh as f32) as usize;
1687                    for bx in 0..blend_width {
1688                        let px = pw - 1 - bx;
1689                        let nx = bx;
1690                        let t = bx as f32 / blend_width as f32;
1691                        let p_val = primary.heightmap.get(px, z);
1692                        let n_val = neighbor.heightmap.get(nx, nz.min(nh - 1));
1693                        let blended = p_val + (n_val - p_val) * t;
1694                        primary.heightmap.set(px, z, blended);
1695                    }
1696                }
1697            }
1698            StitchEdge::West => {
1699                for z in 0..ph {
1700                    let nz = (z as f32 / ph as f32 * nh as f32) as usize;
1701                    for bx in 0..blend_width {
1702                        let px = bx;
1703                        let nx = nw - 1 - bx;
1704                        let t = bx as f32 / blend_width as f32;
1705                        let p_val = primary.heightmap.get(px, z);
1706                        let n_val = neighbor.heightmap.get(nx.min(nw - 1), nz.min(nh - 1));
1707                        let blended = n_val + (p_val - n_val) * (1.0 - t);
1708                        primary.heightmap.set(px, z, blended);
1709                    }
1710                }
1711            }
1712            StitchEdge::North => {
1713                for x in 0..pw {
1714                    let nx = (x as f32 / pw as f32 * nw as f32) as usize;
1715                    for bz in 0..blend_width {
1716                        let pz = bz;
1717                        let nz = nh - 1 - bz;
1718                        let t = bz as f32 / blend_width as f32;
1719                        let p_val = primary.heightmap.get(x, pz);
1720                        let n_val = neighbor.heightmap.get(nx.min(nw - 1), nz.min(nh - 1));
1721                        let blended = n_val + (p_val - n_val) * (1.0 - t);
1722                        primary.heightmap.set(x, pz, blended);
1723                    }
1724                }
1725            }
1726            StitchEdge::South => {
1727                for x in 0..pw {
1728                    let nx = (x as f32 / pw as f32 * nw as f32) as usize;
1729                    for bz in 0..blend_width {
1730                        let pz = ph - 1 - bz;
1731                        let nz = bz;
1732                        let t = bz as f32 / blend_width as f32;
1733                        let p_val = primary.heightmap.get(x, pz);
1734                        let n_val = neighbor.heightmap.get(nx.min(nw - 1), nz.min(nh - 1));
1735                        let blended = p_val + (n_val - p_val) * t;
1736                        primary.heightmap.set(x, pz, blended);
1737                    }
1738                }
1739            }
1740        }
1741    }
1742}
1743
1744/// Which edge of a chunk to stitch.
1745#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1746pub enum StitchEdge {
1747    North,
1748    South,
1749    East,
1750    West,
1751}
1752
1753// ── Chunk Compression ─────────────────────────────────────────────────────────
1754
1755/// Quantizes a heightmap for compact storage.
1756pub struct HeightmapQuantizer;
1757
1758impl HeightmapQuantizer {
1759    /// Quantize to 8-bit precision (256 levels).
1760    pub fn quantize_8bit(heights: &[f32]) -> Vec<u8> {
1761        heights.iter().map(|&h| (h.clamp(0.0, 1.0) * 255.0) as u8).collect()
1762    }
1763
1764    /// Dequantize from 8-bit.
1765    pub fn dequantize_8bit(bytes: &[u8]) -> Vec<f32> {
1766        bytes.iter().map(|&b| b as f32 / 255.0).collect()
1767    }
1768
1769    /// Quantize to 16-bit precision (65536 levels).
1770    pub fn quantize_16bit(heights: &[f32]) -> Vec<u16> {
1771        heights.iter().map(|&h| (h.clamp(0.0, 1.0) * 65535.0) as u16).collect()
1772    }
1773
1774    /// Dequantize from 16-bit.
1775    pub fn dequantize_16bit(shorts: &[u16]) -> Vec<f32> {
1776        shorts.iter().map(|&s| s as f32 / 65535.0).collect()
1777    }
1778
1779    /// Compute quantization error (mean absolute error).
1780    pub fn quantization_error(original: &[f32], quantized_8bit: &[u8]) -> f32 {
1781        if original.len() != quantized_8bit.len() { return f32::INFINITY; }
1782        original.iter().zip(quantized_8bit.iter())
1783            .map(|(&orig, &q)| (orig - q as f32 / 255.0).abs())
1784            .sum::<f32>() / original.len() as f32
1785    }
1786}
1787
1788// ── Streaming Telemetry ────────────────────────────────────────────────────────
1789
1790/// Detailed telemetry for streaming system performance analysis.
1791#[derive(Debug, Default, Clone)]
1792pub struct StreamingTelemetry {
1793    pub frame_number:        u64,
1794    pub visible_chunk_count: usize,
1795    pub loaded_chunk_count:  usize,
1796    pub pending_chunk_count: usize,
1797    pub cache_hit_rate:      f32,
1798    pub memory_usage_mb:     f32,
1799    pub generation_rate:     f32, // chunks per second
1800    pub eviction_count:      usize,
1801    pub last_update_ms:      f32,
1802}
1803
1804impl StreamingTelemetry {
1805    pub fn update_from_stats(&mut self, stats: &StreamingStats) {
1806        self.frame_number      += 1;
1807        self.loaded_chunk_count = stats.chunks_loaded;
1808        self.pending_chunk_count = stats.pending_count;
1809        self.cache_hit_rate    = stats.cache_hit_rate();
1810        self.memory_usage_mb   = stats.memory_bytes as f32 / (1024.0 * 1024.0);
1811    }
1812
1813    pub fn to_display_string(&self) -> String {
1814        format!(
1815            "Frame:{} | Chunks:{} Pending:{} | Cache:{:.0}% | Mem:{:.1}MB",
1816            self.frame_number,
1817            self.loaded_chunk_count,
1818            self.pending_chunk_count,
1819            self.cache_hit_rate * 100.0,
1820            self.memory_usage_mb,
1821        )
1822    }
1823}
1824
1825// ── Chunk Repair ──────────────────────────────────────────────────────────────
1826
1827/// Repairs corrupted or invalid chunk data.
1828pub struct ChunkRepair;
1829
1830impl ChunkRepair {
1831    /// Fix out-of-range height values.
1832    pub fn clamp_heights(chunk: &mut crate::terrain::mod_types::TerrainChunk) {
1833        for v in chunk.heightmap.data.iter_mut() {
1834            *v = v.clamp(0.0, 1.0);
1835        }
1836    }
1837
1838    /// Fix NaN values in heightmap by replacing with neighbors' average.
1839    pub fn fix_nans(chunk: &mut crate::terrain::mod_types::TerrainChunk) {
1840        let w = chunk.heightmap.width;
1841        let h = chunk.heightmap.height;
1842        let data = chunk.heightmap.data.clone();
1843        for y in 0..h {
1844            for x in 0..w {
1845                if chunk.heightmap.get(x, y).is_nan() {
1846                    let mut sum = 0.0f32;
1847                    let mut count = 0;
1848                    for (dx, dy) in &[(-1i32,0),(1,0),(0,-1i32),(0,1)] {
1849                        let nx = x as i32 + dx;
1850                        let ny = y as i32 + dy;
1851                        if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
1852                            let v = data[ny as usize * w + nx as usize];
1853                            if !v.is_nan() { sum += v; count += 1; }
1854                        }
1855                    }
1856                    let replacement = if count > 0 { sum / count as f32 } else { 0.5 };
1857                    chunk.heightmap.set(x, y, replacement);
1858                }
1859            }
1860        }
1861    }
1862
1863    /// Check if a chunk has any issues.
1864    pub fn is_valid(chunk: &crate::terrain::mod_types::TerrainChunk) -> bool {
1865        chunk.heightmap.data.iter().all(|&v| !v.is_nan() && v >= 0.0 && v <= 1.0)
1866    }
1867}
1868
1869// ── More Streaming Tests ──────────────────────────────────────────────────────
1870
1871#[cfg(test)]
1872mod more_streaming_tests {
1873    use super::*;
1874
1875    fn test_config() -> TerrainConfig {
1876        TerrainConfig { chunk_size: 16, view_distance: 1, lod_levels: 2, seed: 42 }
1877    }
1878
1879    #[test]
1880    fn test_terrain_patch_apply() {
1881        let config = test_config();
1882        let gen = ChunkGenerator::new(config);
1883        let mut chunk = gen.generate(ChunkCoord(0, 0));
1884        let mut patch = TerrainPatch::new(ChunkCoord(0, 0), 0, 0, 4, 4);
1885        for v in patch.data.iter_mut() { *v = 0.99; }
1886        patch.dirty = true;
1887        patch.apply_to_chunk(&mut chunk);
1888        assert!((chunk.heightmap.get(0, 0) - 0.99).abs() < 1e-5);
1889    }
1890
1891    #[test]
1892    fn test_terrain_patch_read_write() {
1893        let config = test_config();
1894        let gen = ChunkGenerator::new(config);
1895        let chunk = gen.generate(ChunkCoord(0, 0));
1896        let mut patch = TerrainPatch::new(ChunkCoord(0, 0), 0, 0, 4, 4);
1897        patch.read_from_chunk(&chunk);
1898        assert!(!patch.dirty);
1899        assert!(patch.data.iter().any(|&v| v > 0.0));
1900    }
1901
1902    #[test]
1903    fn test_heightmap_quantizer_8bit() {
1904        let heights: Vec<f32> = (0..256).map(|i| i as f32 / 255.0).collect();
1905        let quantized = HeightmapQuantizer::quantize_8bit(&heights);
1906        let deq = HeightmapQuantizer::dequantize_8bit(&quantized);
1907        let err = HeightmapQuantizer::quantization_error(&heights, &quantized);
1908        assert!(err < 0.005, "8-bit quantization error should be small");
1909    }
1910
1911    #[test]
1912    fn test_heightmap_quantizer_16bit() {
1913        let heights: Vec<f32> = (0..1024).map(|i| i as f32 / 1023.0).collect();
1914        let quantized = HeightmapQuantizer::quantize_16bit(&heights);
1915        let deq = HeightmapQuantizer::dequantize_16bit(&quantized);
1916        let max_err = heights.iter().zip(deq.iter())
1917            .map(|(&a, &b)| (a - b).abs())
1918            .fold(0.0f32, f32::max);
1919        assert!(max_err < 0.0001, "16-bit quantization should be very accurate");
1920    }
1921
1922    #[test]
1923    fn test_chunk_repair_clamp() {
1924        let config = test_config();
1925        let gen = ChunkGenerator::new(config);
1926        let mut chunk = gen.generate(ChunkCoord(0, 0));
1927        chunk.heightmap.data[0] = 2.0; // invalid
1928        chunk.heightmap.data[1] = -1.0; // invalid
1929        ChunkRepair::clamp_heights(&mut chunk);
1930        assert!(ChunkRepair::is_valid(&chunk));
1931    }
1932
1933    #[test]
1934    fn test_chunk_repair_nan() {
1935        let config = test_config();
1936        let gen = ChunkGenerator::new(config);
1937        let mut chunk = gen.generate(ChunkCoord(0, 0));
1938        chunk.heightmap.data[16] = f32::NAN;
1939        ChunkRepair::fix_nans(&mut chunk);
1940        assert!(ChunkRepair::is_valid(&chunk));
1941    }
1942
1943    #[test]
1944    fn test_streaming_telemetry() {
1945        let stats = StreamingStats {
1946            chunks_loaded: 10, chunks_unloaded: 2,
1947            cache_hits: 80, cache_misses: 20,
1948            pending_count: 3, memory_bytes: 2 * 1024 * 1024,
1949            generate_time_ms: 100.0,
1950        };
1951        let mut tel = StreamingTelemetry::default();
1952        tel.update_from_stats(&stats);
1953        assert_eq!(tel.loaded_chunk_count, 10);
1954        assert!((tel.cache_hit_rate - 0.8).abs() < 1e-4);
1955        assert!((tel.memory_usage_mb - 2.0).abs() < 1e-4);
1956        let s = tel.to_display_string();
1957        assert!(s.contains("Chunks:10"));
1958    }
1959
1960    #[test]
1961    fn test_extended_serializer_rle_chunk() {
1962        let config = test_config();
1963        let gen = ChunkGenerator::new(config);
1964        let chunk = gen.generate(ChunkCoord(0, 0));
1965        let bytes = ExtendedChunkSerializer::serialize_with_format(&chunk, SerializationFormat::Raw);
1966        let restored = ExtendedChunkSerializer::deserialize_with_format(
1967            &bytes, SerializationFormat::Raw
1968        );
1969        assert!(restored.is_some());
1970    }
1971}