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        // A synchronous manager loads chunks during update. (The old
988        // assertion `... || pending_count >= 0` was always true.)
989        assert!(mgr.cache_size() > 0);
990    }
991
992    #[test]
993    fn test_streaming_manager_force_load() {
994        let config = test_config();
995        let mut mgr = StreamingManager::new_synchronous(config);
996        let chunk = mgr.force_load(ChunkCoord(0, 0));
997        assert_eq!(chunk.coord, ChunkCoord(0, 0));
998        assert_eq!(mgr.cache_size(), 1);
999    }
1000}
1001
1002// ── Extended Streaming Utilities ──────────────────────────────────────────────
1003
1004/// Serialization format options.
1005#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1006pub enum SerializationFormat {
1007    /// Raw binary (no compression).
1008    Raw,
1009    /// Run-length encoded (good for uniform terrain).
1010    RLE,
1011    /// Delta-encoded (good for slowly varying terrain).
1012    Delta,
1013}
1014
1015/// Extended chunk serializer with format support.
1016pub struct ExtendedChunkSerializer;
1017
1018impl ExtendedChunkSerializer {
1019    /// Serialize with the given format.
1020    pub fn serialize_with_format(
1021        chunk: &crate::terrain::mod_types::TerrainChunk,
1022        format: SerializationFormat,
1023    ) -> Vec<u8> {
1024        let base = ChunkSerializer::serialize(chunk);
1025        match format {
1026            SerializationFormat::Raw   => base,
1027            SerializationFormat::RLE   => Self::rle_encode(&base),
1028            SerializationFormat::Delta => Self::delta_encode_bytes(&base),
1029        }
1030    }
1031
1032    /// Deserialize with the given format.
1033    pub fn deserialize_with_format(
1034        bytes: &[u8],
1035        format: SerializationFormat,
1036    ) -> Option<crate::terrain::mod_types::TerrainChunk> {
1037        let decoded = match format {
1038            SerializationFormat::Raw   => bytes.to_vec(),
1039            SerializationFormat::RLE   => Self::rle_decode(bytes)?,
1040            SerializationFormat::Delta => Self::delta_decode_bytes(bytes)?,
1041        };
1042        ChunkSerializer::deserialize(&decoded)
1043    }
1044
1045    /// Simple run-length encoding for byte streams.
1046    pub fn rle_encode(data: &[u8]) -> Vec<u8> {
1047        if data.is_empty() { return Vec::new(); }
1048        let mut out = Vec::new();
1049        let mut i = 0;
1050        while i < data.len() {
1051            let val = data[i];
1052            let mut run = 1usize;
1053            while i + run < data.len() && data[i + run] == val && run < 255 {
1054                run += 1;
1055            }
1056            out.push(run as u8);
1057            out.push(val);
1058            i += run;
1059        }
1060        out
1061    }
1062
1063    /// Decode run-length encoded data.
1064    pub fn rle_decode(data: &[u8]) -> Option<Vec<u8>> {
1065        if data.len() % 2 != 0 { return None; }
1066        let mut out = Vec::new();
1067        let mut i = 0;
1068        while i + 1 < data.len() {
1069            let count = data[i] as usize;
1070            let val   = data[i + 1];
1071            for _ in 0..count { out.push(val); }
1072            i += 2;
1073        }
1074        Some(out)
1075    }
1076
1077    /// Delta encode: store first byte raw, then differences.
1078    pub fn delta_encode_bytes(data: &[u8]) -> Vec<u8> {
1079        if data.is_empty() { return Vec::new(); }
1080        let mut out = Vec::with_capacity(data.len());
1081        out.push(data[0]);
1082        for i in 1..data.len() {
1083            out.push(data[i].wrapping_sub(data[i - 1]));
1084        }
1085        out
1086    }
1087
1088    /// Decode delta-encoded bytes.
1089    pub fn delta_decode_bytes(data: &[u8]) -> Option<Vec<u8>> {
1090        if data.is_empty() { return Some(Vec::new()); }
1091        let mut out = Vec::with_capacity(data.len());
1092        out.push(data[0]);
1093        for i in 1..data.len() {
1094            out.push(data[i].wrapping_add(*out.last().unwrap()));
1095        }
1096        Some(out)
1097    }
1098}
1099
1100// ── Chunk Event System ────────────────────────────────────────────────────────
1101
1102/// Events emitted by the streaming system.
1103#[derive(Clone, Debug)]
1104pub enum ChunkEvent {
1105    Loaded(ChunkCoord),
1106    Unloaded(ChunkCoord),
1107    LodChanged { coord: ChunkCoord, old_lod: u8, new_lod: u8 },
1108    GenerationStarted(ChunkCoord),
1109    GenerationFailed { coord: ChunkCoord, reason: String },
1110}
1111
1112/// Simple event queue for chunk events.
1113#[derive(Debug, Default)]
1114pub struct ChunkEventQueue {
1115    events: VecDeque<ChunkEvent>,
1116    max_size: usize,
1117}
1118
1119impl ChunkEventQueue {
1120    pub fn new(max_size: usize) -> Self {
1121        Self { events: VecDeque::new(), max_size }
1122    }
1123
1124    pub fn push(&mut self, event: ChunkEvent) {
1125        if self.events.len() >= self.max_size {
1126            self.events.pop_front();
1127        }
1128        self.events.push_back(event);
1129    }
1130
1131    pub fn drain(&mut self) -> Vec<ChunkEvent> {
1132        self.events.drain(..).collect()
1133    }
1134
1135    pub fn len(&self) -> usize { self.events.len() }
1136    pub fn is_empty(&self) -> bool { self.events.is_empty() }
1137}
1138
1139// ── Memory Budget Tracker ─────────────────────────────────────────────────────
1140
1141/// Tracks and enforces memory budget for the streaming system.
1142#[derive(Debug, Clone)]
1143pub struct MemoryBudget {
1144    /// Maximum allowed memory in bytes.
1145    pub max_bytes:      usize,
1146    /// Currently used memory in bytes.
1147    pub current_bytes:  usize,
1148    /// Reserved headroom (keep this much free).
1149    pub headroom_bytes: usize,
1150    /// Peak memory usage seen.
1151    pub peak_bytes:     usize,
1152}
1153
1154impl MemoryBudget {
1155    pub fn new(max_bytes: usize) -> Self {
1156        Self {
1157            max_bytes,
1158            current_bytes: 0,
1159            headroom_bytes: max_bytes / 10,
1160            peak_bytes: 0,
1161        }
1162    }
1163
1164    pub fn allocate(&mut self, bytes: usize) -> bool {
1165        if self.current_bytes + bytes + self.headroom_bytes > self.max_bytes {
1166            return false;
1167        }
1168        self.current_bytes += bytes;
1169        if self.current_bytes > self.peak_bytes {
1170            self.peak_bytes = self.current_bytes;
1171        }
1172        true
1173    }
1174
1175    pub fn free(&mut self, bytes: usize) {
1176        self.current_bytes = self.current_bytes.saturating_sub(bytes);
1177    }
1178
1179    pub fn utilization(&self) -> f32 {
1180        self.current_bytes as f32 / self.max_bytes as f32
1181    }
1182
1183    pub fn available(&self) -> usize {
1184        self.max_bytes.saturating_sub(self.current_bytes + self.headroom_bytes)
1185    }
1186
1187    pub fn is_over_budget(&self) -> bool {
1188        self.current_bytes + self.headroom_bytes > self.max_bytes
1189    }
1190}
1191
1192// ── Terrain World Map ─────────────────────────────────────────────────────────
1193
1194/// Low-resolution overview map of the entire world, used for minimap and LOD hints.
1195#[derive(Debug)]
1196pub struct WorldMap {
1197    /// Low-resolution height overview.
1198    pub height_overview:  crate::terrain::heightmap::HeightMap,
1199    /// Biome overview (compressed to u8 per cell).
1200    pub biome_overview:   Vec<u8>,
1201    /// Overview resolution.
1202    pub resolution:       usize,
1203    /// World size in chunks.
1204    pub world_size_chunks: usize,
1205}
1206
1207impl WorldMap {
1208    /// Generate a world map by sampling the global noise function.
1209    pub fn generate(world_size_chunks: usize, resolution: usize, config: &TerrainConfig) -> Self {
1210        let scale = world_size_chunks as f32 / resolution as f32;
1211        let height_overview = crate::terrain::heightmap::FractalNoise::generate(
1212            resolution, resolution, 6, 2.0, 0.5, 2.0, config.seed,
1213        );
1214        let biome_overview: Vec<u8> = height_overview.data.iter()
1215            .map(|&h| {
1216                let biome = if h < 0.1 { crate::terrain::biome::BiomeType::Ocean }
1217                    else if h < 0.2 { crate::terrain::biome::BiomeType::Beach }
1218                    else if h < 0.5 { crate::terrain::biome::BiomeType::Grassland }
1219                    else if h < 0.7 { crate::terrain::biome::BiomeType::TemperateForest }
1220                    else if h < 0.85 { crate::terrain::biome::BiomeType::Mountain }
1221                    else { crate::terrain::biome::BiomeType::AlpineGlacier };
1222                biome as u8
1223            })
1224            .collect();
1225        Self { height_overview, biome_overview, resolution, world_size_chunks }
1226    }
1227
1228    /// Sample height at normalized world position (0..1).
1229    pub fn sample_height(&self, nx: f32, ny: f32) -> f32 {
1230        let x = (nx * (self.resolution - 1) as f32).clamp(0.0, (self.resolution - 1) as f32);
1231        let y = (ny * (self.resolution - 1) as f32).clamp(0.0, (self.resolution - 1) as f32);
1232        self.height_overview.sample_bilinear(x, y)
1233    }
1234
1235    /// Get biome at normalized world position.
1236    pub fn sample_biome(&self, nx: f32, ny: f32) -> crate::terrain::biome::BiomeType {
1237        let x = (nx * (self.resolution - 1) as f32) as usize;
1238        let y = (ny * (self.resolution - 1) as f32) as usize;
1239        let idx = y.min(self.resolution - 1) * self.resolution + x.min(self.resolution - 1);
1240        crate::terrain::biome::biome_from_index(self.biome_overview[idx] as usize)
1241    }
1242
1243    /// Convert chunk coord to normalized world position.
1244    pub fn chunk_to_normalized(&self, coord: ChunkCoord) -> (f32, f32) {
1245        (
1246            (coord.0 as f32 + 0.5) / self.world_size_chunks as f32,
1247            (coord.1 as f32 + 0.5) / self.world_size_chunks as f32,
1248        )
1249    }
1250}
1251
1252// ── Chunk Diff ────────────────────────────────────────────────────────────────
1253
1254/// Records the difference between two chunk heightmaps (for terrain editing).
1255#[derive(Clone, Debug)]
1256pub struct ChunkDiff {
1257    pub coord:   ChunkCoord,
1258    /// Sparse list of (x, y, old_height, new_height).
1259    pub changes: Vec<(usize, usize, f32, f32)>,
1260}
1261
1262impl ChunkDiff {
1263    pub fn new(coord: ChunkCoord) -> Self {
1264        Self { coord, changes: Vec::new() }
1265    }
1266
1267    /// Record a height change.
1268    pub fn record(&mut self, x: usize, y: usize, old_h: f32, new_h: f32) {
1269        if (old_h - new_h).abs() > 1e-6 {
1270            self.changes.push((x, y, old_h, new_h));
1271        }
1272    }
1273
1274    /// Apply this diff to a heightmap.
1275    pub fn apply(&self, hm: &mut crate::terrain::heightmap::HeightMap) {
1276        for &(x, y, _, new_h) in &self.changes {
1277            hm.set(x, y, new_h);
1278        }
1279    }
1280
1281    /// Reverse (undo) this diff.
1282    pub fn undo(&self, hm: &mut crate::terrain::heightmap::HeightMap) {
1283        for &(x, y, old_h, _) in &self.changes {
1284            hm.set(x, y, old_h);
1285        }
1286    }
1287
1288    pub fn is_empty(&self) -> bool { self.changes.is_empty() }
1289    pub fn len(&self) -> usize { self.changes.len() }
1290}
1291
1292// ── Streaming Profiler ────────────────────────────────────────────────────────
1293
1294/// Profiling data for the streaming system.
1295#[derive(Debug, Default, Clone)]
1296pub struct StreamingProfiler {
1297    pub frame_count:         u64,
1298    pub total_generate_ms:   f64,
1299    pub total_serialize_ms:  f64,
1300    pub total_deserialize_ms: f64,
1301    pub max_generate_ms:     f64,
1302    pub min_generate_ms:     f64,
1303    pub chunks_per_second:   f32,
1304    pub last_frame_ms:       f64,
1305}
1306
1307impl StreamingProfiler {
1308    pub fn new() -> Self {
1309        Self { min_generate_ms: f64::INFINITY, ..Default::default() }
1310    }
1311
1312    pub fn record_generate(&mut self, ms: f64) {
1313        self.total_generate_ms += ms;
1314        self.frame_count += 1;
1315        if ms > self.max_generate_ms { self.max_generate_ms = ms; }
1316        if ms < self.min_generate_ms { self.min_generate_ms = ms; }
1317    }
1318
1319    pub fn average_generate_ms(&self) -> f64 {
1320        if self.frame_count == 0 { 0.0 } else { self.total_generate_ms / self.frame_count as f64 }
1321    }
1322
1323    pub fn reset(&mut self) { *self = Self::new(); }
1324}
1325
1326// ── Priority Zones ────────────────────────────────────────────────────────────
1327
1328/// Defines priority zones that affect chunk loading order.
1329/// E.g., player start location, POIs, scripted events.
1330#[derive(Clone, Debug)]
1331pub struct PriorityZone {
1332    /// World-space center of the zone.
1333    pub center: Vec3,
1334    /// Radius of influence (world units).
1335    pub radius: f32,
1336    /// Priority bonus applied to chunks in this zone.
1337    pub priority_bonus: i64,
1338    /// Optional name for debugging.
1339    pub name: String,
1340}
1341
1342impl PriorityZone {
1343    pub fn new(center: Vec3, radius: f32, priority_bonus: i64) -> Self {
1344        Self { center, radius, priority_bonus, name: String::new() }
1345    }
1346
1347    pub fn named(mut self, name: &str) -> Self {
1348        self.name = name.to_string();
1349        self
1350    }
1351
1352    pub fn contains_world_pos(&self, pos: Vec3) -> bool {
1353        let dx = pos.x - self.center.x;
1354        let dz = pos.z - self.center.z;
1355        dx * dx + dz * dz <= self.radius * self.radius
1356    }
1357
1358    pub fn chunk_priority(&self, coord: ChunkCoord, chunk_size: f32) -> i64 {
1359        let world_pos = coord.to_world_pos(chunk_size);
1360        if self.contains_world_pos(world_pos) {
1361            self.priority_bonus
1362        } else {
1363            let dx = world_pos.x - self.center.x;
1364            let dz = world_pos.z - self.center.z;
1365            let dist = (dx * dx + dz * dz).sqrt();
1366            let falloff = (1.0 - (dist / self.radius).min(2.0)) * 0.5;
1367            (self.priority_bonus as f32 * falloff.max(0.0)) as i64
1368        }
1369    }
1370}
1371
1372/// Manages multiple priority zones and computes combined priority bonuses.
1373#[derive(Debug, Default)]
1374pub struct PriorityZoneManager {
1375    pub zones: Vec<PriorityZone>,
1376}
1377
1378impl PriorityZoneManager {
1379    pub fn new() -> Self { Self::default() }
1380
1381    pub fn add_zone(&mut self, zone: PriorityZone) {
1382        self.zones.push(zone);
1383    }
1384
1385    pub fn remove_zone_by_name(&mut self, name: &str) {
1386        self.zones.retain(|z| z.name != name);
1387    }
1388
1389    pub fn total_priority_bonus(&self, coord: ChunkCoord, chunk_size: f32) -> i64 {
1390        self.zones.iter().map(|z| z.chunk_priority(coord, chunk_size)).sum()
1391    }
1392}
1393
1394// ── Chunk Hitlist ─────────────────────────────────────────────────────────────
1395
1396/// A list of chunks that must be loaded before play can begin.
1397#[derive(Debug)]
1398pub struct ChunkHitlist {
1399    required: std::collections::HashSet<ChunkCoord>,
1400    loaded:   std::collections::HashSet<ChunkCoord>,
1401}
1402
1403impl ChunkHitlist {
1404    pub fn new() -> Self {
1405        Self {
1406            required: std::collections::HashSet::new(),
1407            loaded:   std::collections::HashSet::new(),
1408        }
1409    }
1410
1411    pub fn require(&mut self, coord: ChunkCoord) {
1412        self.required.insert(coord);
1413    }
1414
1415    pub fn mark_loaded(&mut self, coord: ChunkCoord) {
1416        if self.required.contains(&coord) {
1417            self.loaded.insert(coord);
1418        }
1419    }
1420
1421    pub fn is_complete(&self) -> bool {
1422        self.required.iter().all(|c| self.loaded.contains(c))
1423    }
1424
1425    pub fn completion_fraction(&self) -> f32 {
1426        if self.required.is_empty() { return 1.0; }
1427        self.loaded.len() as f32 / self.required.len() as f32
1428    }
1429
1430    pub fn pending_coords(&self) -> Vec<ChunkCoord> {
1431        self.required.iter().filter(|c| !self.loaded.contains(*c)).copied().collect()
1432    }
1433}
1434
1435impl Default for ChunkHitlist {
1436    fn default() -> Self { Self::new() }
1437}
1438
1439// ── Distance-based LOD Bias ───────────────────────────────────────────────────
1440
1441/// Adjusts LOD thresholds based on terrain importance (e.g., near a city).
1442#[derive(Clone, Debug)]
1443pub struct LodBias {
1444    /// LOD multiplier: > 1.0 means higher quality (further LOD0 distance).
1445    pub quality_multiplier: f32,
1446    /// Region center.
1447    pub center: Vec3,
1448    /// Radius of effect.
1449    pub radius: f32,
1450}
1451
1452impl LodBias {
1453    pub fn new(center: Vec3, radius: f32, quality_multiplier: f32) -> Self {
1454        Self { quality_multiplier, center, radius }
1455    }
1456
1457    pub fn lod_multiplier_at(&self, pos: Vec3) -> f32 {
1458        let dx = pos.x - self.center.x;
1459        let dz = pos.z - self.center.z;
1460        let dist = (dx * dx + dz * dz).sqrt();
1461        if dist < self.radius {
1462            let t = 1.0 - dist / self.radius;
1463            1.0 + (self.quality_multiplier - 1.0) * t
1464        } else {
1465            1.0
1466        }
1467    }
1468}
1469
1470// ── Extended Streaming Tests ──────────────────────────────────────────────────
1471
1472#[cfg(test)]
1473mod extended_streaming_tests {
1474    use super::*;
1475
1476    fn test_config() -> TerrainConfig {
1477        TerrainConfig { chunk_size: 16, view_distance: 2, lod_levels: 3, seed: 42 }
1478    }
1479
1480    #[test]
1481    fn test_rle_roundtrip() {
1482        let data: Vec<u8> = vec![1, 1, 1, 2, 3, 3, 4, 4, 4, 4];
1483        let encoded = ExtendedChunkSerializer::rle_encode(&data);
1484        let decoded = ExtendedChunkSerializer::rle_decode(&encoded).unwrap();
1485        assert_eq!(data, decoded);
1486    }
1487
1488    #[test]
1489    fn test_delta_encode_roundtrip() {
1490        let data: Vec<u8> = vec![10, 12, 11, 15, 14, 20, 18];
1491        let enc = ExtendedChunkSerializer::delta_encode_bytes(&data);
1492        let dec = ExtendedChunkSerializer::delta_decode_bytes(&enc).unwrap();
1493        assert_eq!(data, dec);
1494    }
1495
1496    #[test]
1497    fn test_chunk_event_queue() {
1498        let mut q = ChunkEventQueue::new(100);
1499        q.push(ChunkEvent::Loaded(ChunkCoord(0, 0)));
1500        q.push(ChunkEvent::Unloaded(ChunkCoord(1, 0)));
1501        assert_eq!(q.len(), 2);
1502        let events = q.drain();
1503        assert_eq!(events.len(), 2);
1504        assert!(q.is_empty());
1505    }
1506
1507    #[test]
1508    fn test_memory_budget() {
1509        let mut budget = MemoryBudget::new(1024 * 1024);
1510        assert!(budget.allocate(100_000));
1511        budget.free(100_000);
1512        assert_eq!(budget.current_bytes, 0);
1513        assert!(!budget.is_over_budget());
1514    }
1515
1516    #[test]
1517    fn test_memory_budget_over() {
1518        let mut budget = MemoryBudget::new(1000);
1519        assert!(!budget.allocate(1000)); // headroom would be exceeded
1520    }
1521
1522    #[test]
1523    fn test_world_map_generation() {
1524        let config = test_config();
1525        let wm = WorldMap::generate(64, 32, &config);
1526        assert_eq!(wm.height_overview.data.len(), 32 * 32);
1527        assert_eq!(wm.biome_overview.len(), 32 * 32);
1528    }
1529
1530    #[test]
1531    fn test_world_map_sample() {
1532        let config = test_config();
1533        let wm = WorldMap::generate(64, 32, &config);
1534        let h = wm.sample_height(0.5, 0.5);
1535        assert!(h >= 0.0 && h <= 1.0);
1536    }
1537
1538    #[test]
1539    fn test_chunk_diff_apply_undo() {
1540        let config = test_config();
1541        let gen = ChunkGenerator::new(config);
1542        let mut chunk = gen.generate(ChunkCoord(0, 0));
1543        let old_h = chunk.heightmap.get(5, 5);
1544        let new_h = 0.9f32;
1545        let mut diff = ChunkDiff::new(ChunkCoord(0, 0));
1546        diff.record(5, 5, old_h, new_h);
1547        diff.apply(&mut chunk.heightmap);
1548        assert!((chunk.heightmap.get(5, 5) - new_h).abs() < 1e-6);
1549        diff.undo(&mut chunk.heightmap);
1550        assert!((chunk.heightmap.get(5, 5) - old_h).abs() < 1e-6);
1551    }
1552
1553    #[test]
1554    fn test_priority_zone() {
1555        let zone = PriorityZone::new(Vec3::new(0.0, 0.0, 0.0), 100.0, 500_000);
1556        assert!(zone.contains_world_pos(Vec3::new(50.0, 0.0, 50.0)));
1557        assert!(!zone.contains_world_pos(Vec3::new(200.0, 0.0, 200.0)));
1558        let bonus = zone.chunk_priority(ChunkCoord(0, 0), 16.0);
1559        assert!(bonus > 0);
1560    }
1561
1562    #[test]
1563    fn test_priority_zone_manager() {
1564        let mut mgr = PriorityZoneManager::new();
1565        mgr.add_zone(PriorityZone::new(Vec3::ZERO, 100.0, 100_000).named("start"));
1566        let bonus = mgr.total_priority_bonus(ChunkCoord(0, 0), 16.0);
1567        assert!(bonus > 0);
1568        mgr.remove_zone_by_name("start");
1569        assert!(mgr.zones.is_empty());
1570    }
1571
1572    #[test]
1573    fn test_chunk_hitlist() {
1574        let mut hl = ChunkHitlist::new();
1575        hl.require(ChunkCoord(0, 0));
1576        hl.require(ChunkCoord(1, 0));
1577        assert!(!hl.is_complete());
1578        assert!((hl.completion_fraction() - 0.0).abs() < 1e-5);
1579        hl.mark_loaded(ChunkCoord(0, 0));
1580        assert!((hl.completion_fraction() - 0.5).abs() < 1e-5);
1581        hl.mark_loaded(ChunkCoord(1, 0));
1582        assert!(hl.is_complete());
1583    }
1584
1585    #[test]
1586    fn test_lod_bias() {
1587        let bias = LodBias::new(Vec3::ZERO, 100.0, 2.0);
1588        let at_center = bias.lod_multiplier_at(Vec3::ZERO);
1589        assert!((at_center - 2.0).abs() < 1e-4);
1590        let far_away = bias.lod_multiplier_at(Vec3::new(200.0, 0.0, 0.0));
1591        assert!((far_away - 1.0).abs() < 1e-4);
1592    }
1593
1594    #[test]
1595    fn test_streaming_profiler() {
1596        let mut prof = StreamingProfiler::new();
1597        prof.record_generate(5.0);
1598        prof.record_generate(10.0);
1599        prof.record_generate(3.0);
1600        assert!((prof.average_generate_ms() - 6.0).abs() < 1e-4);
1601        assert!((prof.max_generate_ms - 10.0).abs() < 1e-4);
1602        assert!((prof.min_generate_ms - 3.0).abs() < 1e-4);
1603    }
1604}
1605
1606// ── Terrain Patch System ──────────────────────────────────────────────────────
1607
1608/// A small editable terrain patch (sub-chunk resolution editing).
1609#[derive(Clone, Debug)]
1610pub struct TerrainPatch {
1611    pub coord:       ChunkCoord,
1612    pub offset_x:    usize,
1613    pub offset_z:    usize,
1614    pub width:       usize,
1615    pub height:      usize,
1616    pub data:        Vec<f32>,
1617    pub dirty:       bool,
1618}
1619
1620impl TerrainPatch {
1621    pub fn new(coord: ChunkCoord, offset_x: usize, offset_z: usize, width: usize, height: usize) -> Self {
1622        Self {
1623            coord, offset_x, offset_z, width, height,
1624            data: vec![0.0f32; width * height],
1625            dirty: false,
1626        }
1627    }
1628
1629    pub fn get(&self, x: usize, z: usize) -> f32 {
1630        if x < self.width && z < self.height { self.data[z * self.width + x] } else { 0.0 }
1631    }
1632
1633    pub fn set(&mut self, x: usize, z: usize, v: f32) {
1634        if x < self.width && z < self.height {
1635            self.data[z * self.width + x] = v.clamp(0.0, 1.0);
1636            self.dirty = true;
1637        }
1638    }
1639
1640    /// Apply this patch to the corresponding chunk heightmap.
1641    pub fn apply_to_chunk(&self, chunk: &mut crate::terrain::mod_types::TerrainChunk) {
1642        for z in 0..self.height {
1643            for x in 0..self.width {
1644                let cx = self.offset_x + x;
1645                let cz = self.offset_z + z;
1646                chunk.heightmap.set(cx, cz, self.get(x, z));
1647            }
1648        }
1649    }
1650
1651    /// Read current values from a chunk into this patch.
1652    pub fn read_from_chunk(&mut self, chunk: &crate::terrain::mod_types::TerrainChunk) {
1653        for z in 0..self.height {
1654            for x in 0..self.width {
1655                let cx = self.offset_x + x;
1656                let cz = self.offset_z + z;
1657                self.set(x, z, chunk.heightmap.get(cx, cz));
1658            }
1659        }
1660        self.dirty = false;
1661    }
1662}
1663
1664// ── Neighbor Stitching ────────────────────────────────────────────────────────
1665
1666/// Stitches chunk borders to eliminate seams between adjacent chunks.
1667pub struct ChunkStitcher;
1668
1669impl ChunkStitcher {
1670    /// Blend the border rows of two adjacent chunks for seamless transitions.
1671    /// `primary` is the chunk to modify; `neighbor` provides the reference values.
1672    /// `edge` is which edge of `primary` borders `neighbor`.
1673    pub fn stitch_edge(
1674        primary:  &mut crate::terrain::mod_types::TerrainChunk,
1675        neighbor: &crate::terrain::mod_types::TerrainChunk,
1676        edge:     StitchEdge,
1677        blend_width: usize,
1678    ) {
1679        let pw = primary.heightmap.width;
1680        let ph = primary.heightmap.height;
1681        let nw = neighbor.heightmap.width;
1682        let nh = neighbor.heightmap.height;
1683
1684        match edge {
1685            StitchEdge::East => {
1686                for z in 0..ph {
1687                    let nz = (z as f32 / ph as f32 * nh as f32) as usize;
1688                    for bx in 0..blend_width {
1689                        let px = pw - 1 - bx;
1690                        let nx = bx;
1691                        let t = bx as f32 / blend_width as f32;
1692                        let p_val = primary.heightmap.get(px, z);
1693                        let n_val = neighbor.heightmap.get(nx, nz.min(nh - 1));
1694                        let blended = p_val + (n_val - p_val) * t;
1695                        primary.heightmap.set(px, z, blended);
1696                    }
1697                }
1698            }
1699            StitchEdge::West => {
1700                for z in 0..ph {
1701                    let nz = (z as f32 / ph as f32 * nh as f32) as usize;
1702                    for bx in 0..blend_width {
1703                        let px = bx;
1704                        let nx = nw - 1 - bx;
1705                        let t = bx as f32 / blend_width as f32;
1706                        let p_val = primary.heightmap.get(px, z);
1707                        let n_val = neighbor.heightmap.get(nx.min(nw - 1), nz.min(nh - 1));
1708                        let blended = n_val + (p_val - n_val) * (1.0 - t);
1709                        primary.heightmap.set(px, z, blended);
1710                    }
1711                }
1712            }
1713            StitchEdge::North => {
1714                for x in 0..pw {
1715                    let nx = (x as f32 / pw as f32 * nw as f32) as usize;
1716                    for bz in 0..blend_width {
1717                        let pz = bz;
1718                        let nz = nh - 1 - bz;
1719                        let t = bz as f32 / blend_width as f32;
1720                        let p_val = primary.heightmap.get(x, pz);
1721                        let n_val = neighbor.heightmap.get(nx.min(nw - 1), nz.min(nh - 1));
1722                        let blended = n_val + (p_val - n_val) * (1.0 - t);
1723                        primary.heightmap.set(x, pz, blended);
1724                    }
1725                }
1726            }
1727            StitchEdge::South => {
1728                for x in 0..pw {
1729                    let nx = (x as f32 / pw as f32 * nw as f32) as usize;
1730                    for bz in 0..blend_width {
1731                        let pz = ph - 1 - bz;
1732                        let nz = bz;
1733                        let t = bz as f32 / blend_width as f32;
1734                        let p_val = primary.heightmap.get(x, pz);
1735                        let n_val = neighbor.heightmap.get(nx.min(nw - 1), nz.min(nh - 1));
1736                        let blended = p_val + (n_val - p_val) * t;
1737                        primary.heightmap.set(x, pz, blended);
1738                    }
1739                }
1740            }
1741        }
1742    }
1743}
1744
1745/// Which edge of a chunk to stitch.
1746#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1747pub enum StitchEdge {
1748    North,
1749    South,
1750    East,
1751    West,
1752}
1753
1754// ── Chunk Compression ─────────────────────────────────────────────────────────
1755
1756/// Quantizes a heightmap for compact storage.
1757pub struct HeightmapQuantizer;
1758
1759impl HeightmapQuantizer {
1760    /// Quantize to 8-bit precision (256 levels).
1761    pub fn quantize_8bit(heights: &[f32]) -> Vec<u8> {
1762        heights.iter().map(|&h| (h.clamp(0.0, 1.0) * 255.0) as u8).collect()
1763    }
1764
1765    /// Dequantize from 8-bit.
1766    pub fn dequantize_8bit(bytes: &[u8]) -> Vec<f32> {
1767        bytes.iter().map(|&b| b as f32 / 255.0).collect()
1768    }
1769
1770    /// Quantize to 16-bit precision (65536 levels).
1771    pub fn quantize_16bit(heights: &[f32]) -> Vec<u16> {
1772        heights.iter().map(|&h| (h.clamp(0.0, 1.0) * 65535.0) as u16).collect()
1773    }
1774
1775    /// Dequantize from 16-bit.
1776    pub fn dequantize_16bit(shorts: &[u16]) -> Vec<f32> {
1777        shorts.iter().map(|&s| s as f32 / 65535.0).collect()
1778    }
1779
1780    /// Compute quantization error (mean absolute error).
1781    pub fn quantization_error(original: &[f32], quantized_8bit: &[u8]) -> f32 {
1782        if original.len() != quantized_8bit.len() { return f32::INFINITY; }
1783        original.iter().zip(quantized_8bit.iter())
1784            .map(|(&orig, &q)| (orig - q as f32 / 255.0).abs())
1785            .sum::<f32>() / original.len() as f32
1786    }
1787}
1788
1789// ── Streaming Telemetry ────────────────────────────────────────────────────────
1790
1791/// Detailed telemetry for streaming system performance analysis.
1792#[derive(Debug, Default, Clone)]
1793pub struct StreamingTelemetry {
1794    pub frame_number:        u64,
1795    pub visible_chunk_count: usize,
1796    pub loaded_chunk_count:  usize,
1797    pub pending_chunk_count: usize,
1798    pub cache_hit_rate:      f32,
1799    pub memory_usage_mb:     f32,
1800    pub generation_rate:     f32, // chunks per second
1801    pub eviction_count:      usize,
1802    pub last_update_ms:      f32,
1803}
1804
1805impl StreamingTelemetry {
1806    pub fn update_from_stats(&mut self, stats: &StreamingStats) {
1807        self.frame_number      += 1;
1808        self.loaded_chunk_count = stats.chunks_loaded;
1809        self.pending_chunk_count = stats.pending_count;
1810        self.cache_hit_rate    = stats.cache_hit_rate();
1811        self.memory_usage_mb   = stats.memory_bytes as f32 / (1024.0 * 1024.0);
1812    }
1813
1814    pub fn to_display_string(&self) -> String {
1815        format!(
1816            "Frame:{} | Chunks:{} Pending:{} | Cache:{:.0}% | Mem:{:.1}MB",
1817            self.frame_number,
1818            self.loaded_chunk_count,
1819            self.pending_chunk_count,
1820            self.cache_hit_rate * 100.0,
1821            self.memory_usage_mb,
1822        )
1823    }
1824}
1825
1826// ── Chunk Repair ──────────────────────────────────────────────────────────────
1827
1828/// Repairs corrupted or invalid chunk data.
1829pub struct ChunkRepair;
1830
1831impl ChunkRepair {
1832    /// Fix out-of-range height values.
1833    pub fn clamp_heights(chunk: &mut crate::terrain::mod_types::TerrainChunk) {
1834        for v in chunk.heightmap.data.iter_mut() {
1835            *v = v.clamp(0.0, 1.0);
1836        }
1837    }
1838
1839    /// Fix NaN values in heightmap by replacing with neighbors' average.
1840    pub fn fix_nans(chunk: &mut crate::terrain::mod_types::TerrainChunk) {
1841        let w = chunk.heightmap.width;
1842        let h = chunk.heightmap.height;
1843        let data = chunk.heightmap.data.clone();
1844        for y in 0..h {
1845            for x in 0..w {
1846                if chunk.heightmap.get(x, y).is_nan() {
1847                    let mut sum = 0.0f32;
1848                    let mut count = 0;
1849                    for (dx, dy) in &[(-1i32,0),(1,0),(0,-1i32),(0,1)] {
1850                        let nx = x as i32 + dx;
1851                        let ny = y as i32 + dy;
1852                        if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
1853                            let v = data[ny as usize * w + nx as usize];
1854                            if !v.is_nan() { sum += v; count += 1; }
1855                        }
1856                    }
1857                    let replacement = if count > 0 { sum / count as f32 } else { 0.5 };
1858                    chunk.heightmap.set(x, y, replacement);
1859                }
1860            }
1861        }
1862    }
1863
1864    /// Check if a chunk has any issues.
1865    pub fn is_valid(chunk: &crate::terrain::mod_types::TerrainChunk) -> bool {
1866        chunk.heightmap.data.iter().all(|&v| !v.is_nan() && v >= 0.0 && v <= 1.0)
1867    }
1868}
1869
1870// ── More Streaming Tests ──────────────────────────────────────────────────────
1871
1872#[cfg(test)]
1873mod more_streaming_tests {
1874    use super::*;
1875
1876    fn test_config() -> TerrainConfig {
1877        TerrainConfig { chunk_size: 16, view_distance: 1, lod_levels: 2, seed: 42 }
1878    }
1879
1880    #[test]
1881    fn test_terrain_patch_apply() {
1882        let config = test_config();
1883        let gen = ChunkGenerator::new(config);
1884        let mut chunk = gen.generate(ChunkCoord(0, 0));
1885        let mut patch = TerrainPatch::new(ChunkCoord(0, 0), 0, 0, 4, 4);
1886        for v in patch.data.iter_mut() { *v = 0.99; }
1887        patch.dirty = true;
1888        patch.apply_to_chunk(&mut chunk);
1889        assert!((chunk.heightmap.get(0, 0) - 0.99).abs() < 1e-5);
1890    }
1891
1892    #[test]
1893    fn test_terrain_patch_read_write() {
1894        let config = test_config();
1895        let gen = ChunkGenerator::new(config);
1896        let chunk = gen.generate(ChunkCoord(0, 0));
1897        let mut patch = TerrainPatch::new(ChunkCoord(0, 0), 0, 0, 4, 4);
1898        patch.read_from_chunk(&chunk);
1899        assert!(!patch.dirty);
1900        assert!(patch.data.iter().any(|&v| v > 0.0));
1901    }
1902
1903    #[test]
1904    fn test_heightmap_quantizer_8bit() {
1905        let heights: Vec<f32> = (0..256).map(|i| i as f32 / 255.0).collect();
1906        let quantized = HeightmapQuantizer::quantize_8bit(&heights);
1907        let deq = HeightmapQuantizer::dequantize_8bit(&quantized);
1908        let err = HeightmapQuantizer::quantization_error(&heights, &quantized);
1909        assert!(err < 0.005, "8-bit quantization error should be small");
1910    }
1911
1912    #[test]
1913    fn test_heightmap_quantizer_16bit() {
1914        let heights: Vec<f32> = (0..1024).map(|i| i as f32 / 1023.0).collect();
1915        let quantized = HeightmapQuantizer::quantize_16bit(&heights);
1916        let deq = HeightmapQuantizer::dequantize_16bit(&quantized);
1917        let max_err = heights.iter().zip(deq.iter())
1918            .map(|(&a, &b)| (a - b).abs())
1919            .fold(0.0f32, f32::max);
1920        assert!(max_err < 0.0001, "16-bit quantization should be very accurate");
1921    }
1922
1923    #[test]
1924    fn test_chunk_repair_clamp() {
1925        let config = test_config();
1926        let gen = ChunkGenerator::new(config);
1927        let mut chunk = gen.generate(ChunkCoord(0, 0));
1928        chunk.heightmap.data[0] = 2.0; // invalid
1929        chunk.heightmap.data[1] = -1.0; // invalid
1930        ChunkRepair::clamp_heights(&mut chunk);
1931        assert!(ChunkRepair::is_valid(&chunk));
1932    }
1933
1934    #[test]
1935    fn test_chunk_repair_nan() {
1936        let config = test_config();
1937        let gen = ChunkGenerator::new(config);
1938        let mut chunk = gen.generate(ChunkCoord(0, 0));
1939        chunk.heightmap.data[16] = f32::NAN;
1940        ChunkRepair::fix_nans(&mut chunk);
1941        assert!(ChunkRepair::is_valid(&chunk));
1942    }
1943
1944    #[test]
1945    fn test_streaming_telemetry() {
1946        let stats = StreamingStats {
1947            chunks_loaded: 10, chunks_unloaded: 2,
1948            cache_hits: 80, cache_misses: 20,
1949            pending_count: 3, memory_bytes: 2 * 1024 * 1024,
1950            generate_time_ms: 100.0,
1951        };
1952        let mut tel = StreamingTelemetry::default();
1953        tel.update_from_stats(&stats);
1954        assert_eq!(tel.loaded_chunk_count, 10);
1955        assert!((tel.cache_hit_rate - 0.8).abs() < 1e-4);
1956        assert!((tel.memory_usage_mb - 2.0).abs() < 1e-4);
1957        let s = tel.to_display_string();
1958        assert!(s.contains("Chunks:10"));
1959    }
1960
1961    #[test]
1962    fn test_extended_serializer_rle_chunk() {
1963        let config = test_config();
1964        let gen = ChunkGenerator::new(config);
1965        let chunk = gen.generate(ChunkCoord(0, 0));
1966        let bytes = ExtendedChunkSerializer::serialize_with_format(&chunk, SerializationFormat::Raw);
1967        let restored = ExtendedChunkSerializer::deserialize_with_format(
1968            &bytes, SerializationFormat::Raw
1969        );
1970        assert!(restored.is_some());
1971    }
1972}