Chunk unloading system with cleanly abstracted decision layer. - ChunkMap: replace (bool, i32) timer tuple with () presence marker; remove FIFO cycle fields pending_unload/unload_cycle/unload_cursor/ unload_countdown. loaded_chunks is now HashMap<IVec2, ()>. - handle_chunk_events: simplify re-entrancy check to contains_key; remove parallel Mutex dedup machinery — load detection is now trivial. - is_standable: single HashMap get; return false for unloaded chunks instead of falling back to is_standable_slow (pathfinding correctness). - remove_chunk_data: bounds-iteration tile cleanup for chunk unload. - ChunkOwner component + chunk_entity_index: static terrain entities (floor tiles, fixtures, trees) tagged at spawn and despawned by chunk. Mobile entities (dorfs, pigs, rabbits) not tracked — safe by design since they were never indexed. - unload_chunk: canonical single-chunk unload function; cleans loaded_chunks, despawns entities, removes tilemap data, marks render chunk dirty via ChunkZKey::from_world, updates connectivity, fires rebake. Full docstring with step-by-step state map, working example for dynamic unload, and "what it does NOT handle" section. - dynamic_chunk_unloading_system: no-op stub. Re-enable by writing a system that diffs wanted vs loaded chunks and calls unload_chunk. Wired out of FixedUpdate during foundation work. - render chunk fix: ChunkZKey::from_world now pub(crate) so unload can derive the correct render chunk key (spans 4x4 world chunks) matching what build_quilted_terrain_sprites uses at spawn. - FloorTilePrefab::spawn: pre-existing bug — return Entity not ().
355 lines
13 KiB
Rust
355 lines
13 KiB
Rust
use bevy::prelude::*;
|
|
use bevy::tasks::AsyncComputeTaskPool;
|
|
use bevy_platform::time::Instant;
|
|
use noise::{NoiseFn, Perlin};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use crate::{
|
|
config::TileRegistry,
|
|
constants::{SEED, TILE_SIZE},
|
|
world::{
|
|
tiles::{ChunkData, FloorTileData, TileMap, TerrainSpriteState, CurrentWorldSpriteState},
|
|
ChunkForrestryEvent, ChunkTerrainEvent, FloorTilePrefab, TileOcclusionEvent, CHUNK_SIZE,
|
|
Z_ABOVE, Z_BELOW, ChunkMap, ChunkOwner,
|
|
},
|
|
};
|
|
|
|
/// Thread-safe storage for completed terrain blobs.
|
|
/// Uses type erasure to avoid Debug bounds on TerrainBlob.
|
|
type BlobStorage = Arc<Mutex<Box<dyn Send + Sync>>>;
|
|
|
|
/// Typed wrapper for terrain blob storage.
|
|
#[derive(Resource)]
|
|
pub struct TerrainBlobStorage {
|
|
pub blobs: Arc<Mutex<Vec<TerrainBlob>>>,
|
|
}
|
|
|
|
impl Default for TerrainBlobStorage {
|
|
fn default() -> Self {
|
|
Self {
|
|
blobs: Arc::new(Mutex::new(Vec::new())),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Clone for TerrainBlobStorage {
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
blobs: self.blobs.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Result of async terrain generation for a single chunk.
|
|
/// Contains all data needed to spawn entities and update TileMap on main thread.
|
|
pub struct TerrainBlob {
|
|
pub chunk_pos: IVec2,
|
|
pub chunk_data: ChunkData,
|
|
pub tile_updates: Vec<(IVec3, FloorTileData)>,
|
|
pub surface_positions: Vec<(Vec3, String)>,
|
|
pub tile_spawns: Vec<(Vec3, FloorTilePrefab)>,
|
|
}
|
|
|
|
pub fn generate_surface_terrain(x: i32, y: i32) -> f32 {
|
|
let noise = Perlin::new(SEED);
|
|
let mut noise_value = 0.0;
|
|
let mut amplitude = 1.0;
|
|
let mut frequency = 0.008;
|
|
|
|
for _ in 0..6 {
|
|
noise_value += noise.get([x as f64 * frequency, y as f64 * frequency]) * amplitude;
|
|
amplitude *= 0.6;
|
|
frequency *= 1.8;
|
|
}
|
|
(noise_value * 2.5) as f32
|
|
}
|
|
|
|
/// Async terrain generation - runs on AsyncComputeTaskPool.
|
|
/// Computes all terrain data without ECS access, returns blob for main thread.
|
|
fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
|
let cave_noise = Perlin::new(SEED);
|
|
let start_x = chunk_pos.x * CHUNK_SIZE;
|
|
let start_y = chunk_pos.y * CHUNK_SIZE;
|
|
let registry = TileRegistry::global();
|
|
|
|
let mut chunk_data = ChunkData::new(chunk_pos);
|
|
let mut tile_updates: Vec<(IVec3, FloorTileData)> = Vec::new();
|
|
let mut surface_positions: Vec<(Vec3, String)> = Vec::new();
|
|
let mut tile_spawns: Vec<(Vec3, FloorTilePrefab)> = Vec::new();
|
|
|
|
for local_y in 0..CHUNK_SIZE {
|
|
for local_x in 0..CHUNK_SIZE {
|
|
let world_x = start_x + local_x;
|
|
let world_y = start_y + local_y;
|
|
|
|
let noise_position = Vec3::new(
|
|
(world_x as f32 * TILE_SIZE).round(),
|
|
(world_y as f32 * TILE_SIZE).round(),
|
|
(generate_surface_terrain(world_x, world_y) * TILE_SIZE).round(),
|
|
);
|
|
|
|
for z in -Z_BELOW as isize..=Z_ABOVE as isize {
|
|
let position = Vec3::new(
|
|
(world_x as f32 * TILE_SIZE).round(),
|
|
(world_y as f32 * TILE_SIZE).round(),
|
|
(z as f32 * TILE_SIZE).round(),
|
|
);
|
|
let pos_ivec = position.as_ivec3();
|
|
let local_z = z as i32;
|
|
let surface_height =
|
|
(generate_surface_terrain(world_x, world_y) * TILE_SIZE).round();
|
|
|
|
if z < -5 {
|
|
let cave_value = cave_noise.get([
|
|
world_x as f64 * 0.05,
|
|
world_y as f64 * 0.05,
|
|
z as f64 * 0.05,
|
|
]);
|
|
if cave_value < -0.75 {
|
|
let tile = registry.floor("air");
|
|
tile_spawns.push((position, FloorTilePrefab::air(position)));
|
|
tile_updates.push((
|
|
pos_ivec,
|
|
FloorTileData::new(
|
|
tile.id,
|
|
tile.can_stand_in,
|
|
tile.can_stand_on,
|
|
tile.transparent,
|
|
tile.astar_weight,
|
|
[0; 8],
|
|
),
|
|
));
|
|
chunk_data.set_floor_tile(
|
|
local_x,
|
|
local_y,
|
|
local_z,
|
|
tile.id,
|
|
tile.can_stand_in,
|
|
tile.can_stand_on,
|
|
tile.astar_weight,
|
|
);
|
|
} else if cave_value < 0.8 {
|
|
let tile = registry.floor("rock");
|
|
tile_spawns.push((position, FloorTilePrefab::rock(position)));
|
|
tile_updates.push((
|
|
pos_ivec,
|
|
FloorTileData::new(
|
|
tile.id,
|
|
tile.can_stand_in,
|
|
tile.can_stand_on,
|
|
tile.transparent,
|
|
tile.astar_weight,
|
|
[0; 8],
|
|
),
|
|
));
|
|
chunk_data.set_floor_tile(
|
|
local_x,
|
|
local_y,
|
|
local_z,
|
|
tile.id,
|
|
tile.can_stand_in,
|
|
tile.can_stand_on,
|
|
tile.astar_weight,
|
|
);
|
|
} else {
|
|
let tile = registry.floor("dirt");
|
|
tile_spawns.push((position, FloorTilePrefab::dirt(position)));
|
|
tile_updates.push((
|
|
pos_ivec,
|
|
FloorTileData::new(
|
|
tile.id,
|
|
tile.can_stand_in,
|
|
tile.can_stand_on,
|
|
tile.transparent,
|
|
tile.astar_weight,
|
|
[0; 8],
|
|
),
|
|
));
|
|
chunk_data.set_floor_tile(
|
|
local_x,
|
|
local_y,
|
|
local_z,
|
|
tile.id,
|
|
tile.can_stand_in,
|
|
tile.can_stand_on,
|
|
tile.astar_weight,
|
|
);
|
|
}
|
|
} else if noise_position.z > position.z {
|
|
if surface_height <= position.z + TILE_SIZE {
|
|
let tile = registry.floor("grass");
|
|
tile_spawns.push((position, FloorTilePrefab::grass(position)));
|
|
tile_updates.push((
|
|
pos_ivec,
|
|
FloorTileData::new(
|
|
tile.id,
|
|
tile.can_stand_in,
|
|
tile.can_stand_on,
|
|
tile.transparent,
|
|
tile.astar_weight,
|
|
[0; 8],
|
|
),
|
|
));
|
|
chunk_data.set_floor_tile(
|
|
local_x,
|
|
local_y,
|
|
local_z,
|
|
tile.id,
|
|
tile.can_stand_in,
|
|
tile.can_stand_on,
|
|
tile.astar_weight,
|
|
);
|
|
surface_positions.push((position, "grass".to_string()));
|
|
} else {
|
|
let tile = registry.floor("dirt");
|
|
tile_spawns.push((position, FloorTilePrefab::dirt(position)));
|
|
tile_updates.push((
|
|
pos_ivec,
|
|
FloorTileData::new(
|
|
tile.id,
|
|
tile.can_stand_in,
|
|
tile.can_stand_on,
|
|
tile.transparent,
|
|
tile.astar_weight,
|
|
[0; 8],
|
|
),
|
|
));
|
|
chunk_data.set_floor_tile(
|
|
local_x,
|
|
local_y,
|
|
local_z,
|
|
tile.id,
|
|
tile.can_stand_in,
|
|
tile.can_stand_on,
|
|
tile.astar_weight,
|
|
);
|
|
}
|
|
} else {
|
|
let tile = registry.floor("air");
|
|
tile_spawns.push((position, FloorTilePrefab::air(position)));
|
|
tile_updates.push((
|
|
pos_ivec,
|
|
FloorTileData::new(
|
|
tile.id,
|
|
tile.can_stand_in,
|
|
tile.can_stand_on,
|
|
tile.transparent,
|
|
tile.astar_weight,
|
|
[0; 8],
|
|
),
|
|
));
|
|
chunk_data.set_floor_tile(
|
|
local_x,
|
|
local_y,
|
|
local_z,
|
|
tile.id,
|
|
tile.can_stand_in,
|
|
tile.can_stand_on,
|
|
tile.astar_weight,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TerrainBlob {
|
|
chunk_pos,
|
|
chunk_data,
|
|
tile_updates,
|
|
surface_positions,
|
|
tile_spawns,
|
|
}
|
|
}
|
|
|
|
/// Spawns async terrain generation tasks on AsyncComputeTaskPool.
|
|
/// Fast - just reads events and spawns tasks.
|
|
pub fn spawn_terrain_tasks(
|
|
mut events: MessageReader<ChunkTerrainEvent>,
|
|
mut cwss: ResMut<CurrentWorldSpriteState>,
|
|
blob_storage: Res<TerrainBlobStorage>,
|
|
) {
|
|
let start = Instant::now();
|
|
let count = events.len();
|
|
|
|
if count == 0 {
|
|
return;
|
|
}
|
|
|
|
let task_pool = AsyncComputeTaskPool::get();
|
|
let blobs = blob_storage.blobs.clone();
|
|
|
|
for event in events.read() {
|
|
let chunk_pos = event.chunk_position;
|
|
let blobs_clone = blobs.clone();
|
|
task_pool.spawn(async move {
|
|
let blob = generate_terrain_blob(chunk_pos);
|
|
blobs_clone.lock().unwrap().push(blob);
|
|
}).detach();
|
|
}
|
|
|
|
cwss.state = TerrainSpriteState::WaitingForRender;
|
|
println!("{} terrain tasks spawned in {:.2?}", count, start.elapsed());
|
|
}
|
|
|
|
/// Applies completed terrain blobs on main thread.
|
|
/// Spawns entities, updates TileMap, sends occlusion events.
|
|
pub fn apply_terrain_blobs(
|
|
mut commands: Commands,
|
|
blob_storage: Res<TerrainBlobStorage>,
|
|
mut tilemap: ResMut<TileMap>,
|
|
mut chunk_map: ResMut<ChunkMap>,
|
|
mut forrestry_event_writer: MessageWriter<ChunkForrestryEvent>,
|
|
mut occlusion_event_writer: MessageWriter<TileOcclusionEvent>,
|
|
) {
|
|
let start = Instant::now();
|
|
let mut applied_count = 0;
|
|
|
|
let completed: Vec<TerrainBlob> = blob_storage.blobs.lock().unwrap().drain(..).collect();
|
|
|
|
for blob in completed {
|
|
let new_positions: Vec<IVec3> = blob
|
|
.tile_updates
|
|
.into_iter()
|
|
.map(|(pos, data)| {
|
|
tilemap.insert_floor(pos, data);
|
|
pos
|
|
})
|
|
.collect();
|
|
|
|
tilemap.chunks.insert(blob.chunk_pos, blob.chunk_data);
|
|
|
|
for (_position, prefab) in blob.tile_spawns {
|
|
let entity = prefab.spawn(&mut commands);
|
|
commands.entity(entity).insert(ChunkOwner(blob.chunk_pos));
|
|
chunk_map
|
|
.chunk_entity_index
|
|
.entry(blob.chunk_pos)
|
|
.or_default()
|
|
.push(entity);
|
|
}
|
|
|
|
for pos in new_positions {
|
|
occlusion_event_writer.write(TileOcclusionEvent { tile_position: pos });
|
|
}
|
|
|
|
forrestry_event_writer.write(ChunkForrestryEvent {
|
|
chunk_position: blob.chunk_pos,
|
|
floor_tiles: blob.surface_positions,
|
|
});
|
|
|
|
applied_count += 1;
|
|
}
|
|
|
|
if applied_count > 0 {
|
|
println!("{} terrain blobs applied in {:.2?}", applied_count, start.elapsed());
|
|
}
|
|
}
|
|
|
|
pub fn generate_chunk_weathering_and_precipitation() {
|
|
// TODO: Generate weathering and precipitation
|
|
// Temperature and humidity
|
|
// Erosion
|
|
// Hadley lines/cells etc
|
|
// Biomes
|
|
}
|