feat: implement chunk unloading infrastructure

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 ().
This commit is contained in:
2026-03-20 10:30:33 +00:00
parent a0eb608d73
commit d7cbbaa04b
7 changed files with 237 additions and 63 deletions
+31 -2
View File
@@ -12,7 +12,8 @@ use crate::{
constants::{SEED, TILE_SIZE},
world::{
tiles::{FixtureTileData, TileMap},
ChunkForrestryEvent, FixtureTilePrefab, TextureIDs, Textures, VisibleGameEntity,
ChunkForrestryEvent, ChunkMap, ChunkOwner, FixtureTilePrefab, TextureIDs, Textures,
VisibleGameEntity,
},
};
@@ -22,11 +23,14 @@ pub fn generate_chunk_forrestry(
mut tilemap: ResMut<TileMap>,
texture_ids: Res<TextureIDs>,
textures: Res<Textures>,
mut chunk_map: ResMut<ChunkMap>,
) {
let start = Instant::now();
let count = events.len();
let collected_tilemap_updates: Mutex<Vec<(IVec3, FixtureTileData)>> = Mutex::new(Vec::new());
// Collect (chunk_pos, entity) pairs from parallel section for chunk_entity_index
let collected_entities: Mutex<Vec<(IVec2, Entity)>> = Mutex::new(Vec::new());
events.par_read().for_each(|event| {
let floor_positions = &event.floor_tiles;
@@ -71,6 +75,14 @@ pub fn generate_chunk_forrestry(
let trunk_entity =
FixtureTilePrefab::log(trunk_pos).spawn(&mut commands);
tree_positions.push(trunk_pos);
commands
.entity(trunk_entity)
.insert(ChunkOwner(event.chunk_position));
collected_entities
.lock()
.unwrap()
.push((event.chunk_position, trunk_entity));
// Add sprite component to the same entity if texture exists
if let Some(texture_id) = texture_ids.refs.get(&500004) {
if let Some(texture) = textures.handles.get(texture_id) {
@@ -142,7 +154,14 @@ pub fn generate_chunk_forrestry(
),
))
.id();
commands.entity(leaf).insert(VisibleGameEntity);
commands.entity(leaf).insert((
VisibleGameEntity,
ChunkOwner(event.chunk_position),
));
collected_entities
.lock()
.unwrap()
.push((event.chunk_position, leaf));
collected_tilemap_updates.lock().unwrap().push(
(
ivec,
@@ -169,6 +188,16 @@ pub fn generate_chunk_forrestry(
for (ivec, data) in collected_updates {
tilemap.insert_fixture(ivec, data);
}
// Populate chunk_entity_index from collected entities (after parallel section)
let collected = collected_entities.into_inner().unwrap();
for (chunk_pos, entity) in collected {
chunk_map
.chunk_entity_index
.entry(chunk_pos)
.or_default()
.push(entity);
}
if count > 0 {
println!(
"Forrestry update for {:?} chunks in {:.2?}",
+9 -2
View File
@@ -10,7 +10,7 @@ use crate::{
world::{
tiles::{ChunkData, FloorTileData, TileMap, TerrainSpriteState, CurrentWorldSpriteState},
ChunkForrestryEvent, ChunkTerrainEvent, FloorTilePrefab, TileOcclusionEvent, CHUNK_SIZE,
Z_ABOVE, Z_BELOW,
Z_ABOVE, Z_BELOW, ChunkMap, ChunkOwner,
},
};
@@ -297,6 +297,7 @@ 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>,
) {
@@ -318,7 +319,13 @@ pub fn apply_terrain_blobs(
tilemap.chunks.insert(blob.chunk_pos, blob.chunk_data);
for (_position, prefab) in blob.tile_spawns {
prefab.spawn(&mut commands);
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 {