Files
Verbatim/src/world/chunked_grid.rs
T
Emil 0d54d987ac feat: explosions, particle system, electricity, structural integrity
Explosions:
- trigger_explosion(x, y, radius, damage) in Game
- Destroys non-static solids, ignites empty cells, pressure spike
- Knockback to entities (velocity away from center + upward pop)
- Particle burst on explosion (fire + smoke + debris)

Particle system:
- ParticleManager in src/physics/particle.rs (capacity 2000)
- CPU-side: position, velocity, life, color, gravity, size
- spawn_burst() for radial bursts, individual spawn() for ambient
- Rendered as third draw call in graphics.rs (is_ui: 2 in shader)
- ParticleInstance = ColorInstance compatible (12 bytes)
- upload_particles() via GpuRenderer trait
- Ambient: fire sparks, lava bubbles — scanned around player

Electricity:
- chunk.electricity: Vec<u8> (4KB/chunk) — current strength 0-255
- Material.conductive + conductivity fields (water = conductive)
- electricity_step() in CA: propagates current through conductive
  neighbors, decays over time, skips non-conductive cells
- cells_swap swaps electricity, serialization saves/loads it

Structural integrity:
- Material.structural: bool (stone, wood = true)
- structural_step() in CA (every 30 ticks, infinite mode only):
  checks if structural cells have support (below, below-left, below-right)
  converts unsupported to Sand (falls)
- Only active in infinite mode to avoid breaking test grids

Graphics shader: is_ui==2 branch renders particles in world-space
All 185 tests + 14 scenarios pass. 143 FPS benchmark.
2026-06-22 20:13:07 +03:00

1056 lines
35 KiB
Rust

use crate::world::cell::{default_temp, Cell, MaterialId};
use crate::world::chunk::{Chunk, CHUNK_SIZE};
use std::collections::HashMap;
use std::io;
use std::path::{Path, PathBuf};
pub struct ChunkedGrid {
pub chunk_size: usize,
pub chunks: HashMap<(i64, i64), Chunk>,
pub chunks_vec: Vec<Chunk>,
pub bounds: Option<(i64, i64, i64, i64)>,
pub seed: u64,
pub cache_dir: Option<String>,
pub width: usize,
pub height: usize,
pub chunks_x: usize,
pub chunks_y: usize,
}
impl ChunkedGrid {
pub fn with_size(width: usize, height: usize) -> Self {
let chunk_size = CHUNK_SIZE;
let chunks_x = (width + chunk_size - 1) / chunk_size;
let chunks_y = (height + chunk_size - 1) / chunk_size;
let mut chunks_vec = Vec::with_capacity(chunks_x * chunks_y);
for _ in 0..chunks_x * chunks_y {
let mut chunk = Chunk::new();
chunk.active = true;
chunks_vec.push(chunk);
}
Self {
chunk_size,
chunks: HashMap::new(),
chunks_vec,
bounds: Some((0, 0, width as i64, height as i64)),
seed: 0,
cache_dir: None,
width,
height,
chunks_x,
chunks_y,
}
}
pub fn infinite(seed: u64, cache_dir: Option<String>) -> Self {
Self {
chunk_size: CHUNK_SIZE,
chunks: HashMap::new(),
chunks_vec: Vec::new(),
bounds: None,
seed,
cache_dir,
width: i64::MAX as usize,
height: i64::MAX as usize,
chunks_x: 0,
chunks_y: 0,
}
}
#[inline]
fn chunk_index(&self, cx: i32, cy: i32) -> Option<usize> {
if cx < 0 || cy < 0 || cx >= self.chunks_x as i32 || cy >= self.chunks_y as i32 {
return None;
}
Some((cy as usize) * self.chunks_x + (cx as usize))
}
#[inline]
fn is_bounded(&self) -> bool {
self.bounds.is_some()
}
#[inline]
pub fn chunk_at(&self, x: i32, y: i32) -> (i32, i32, i32, i32) {
let cs = self.chunk_size as i32;
let cx = x.div_euclid(cs);
let cy = y.div_euclid(cs);
let lx = x.rem_euclid(cs);
let ly = y.rem_euclid(cs);
(cx, cy, lx, ly)
}
#[inline]
pub fn in_bounds(&self, x: i32, y: i32) -> bool {
match self.bounds {
Some((x0, y0, x1, y1)) => {
let wx = x as i64;
let wy = y as i64;
wx >= x0 && wx < x1 && wy >= y0 && wy < y1
}
None => true,
}
}
#[inline]
pub fn get_chunk(&self, cx: i32, cy: i32) -> Option<&Chunk> {
if self.is_bounded() {
self.chunk_index(cx, cy)
.and_then(|idx| self.chunks_vec.get(idx))
} else {
self.chunks.get(&(cx as i64, cy as i64))
}
}
#[inline]
pub fn get_chunk_mut(&mut self, cx: i32, cy: i32) -> Option<&mut Chunk> {
if self.is_bounded() {
self.chunk_index(cx, cy)
.and_then(|idx| self.chunks_vec.get_mut(idx))
} else {
self.chunks.get_mut(&(cx as i64, cy as i64))
}
}
pub fn ensure_chunk(&mut self, cx: i32, cy: i32) -> Option<&mut Chunk> {
let origin_x = cx * self.chunk_size as i32;
let origin_y = cy * self.chunk_size as i32;
if !self.in_bounds(origin_x, origin_y) {
return None;
}
if self.is_bounded() {
return self.get_chunk_mut(cx, cy);
}
let key = (cx as i64, cy as i64);
if !self.chunks.contains_key(&key) {
let chunk = Chunk::new();
if let Some(ref dir) = self.cache_dir {
let path = chunk_path(dir, self.seed, cx, cy);
if path.exists() {
if let Err(e) = self.load_chunk_from_path(&path, cx, cy) {
eprintln!("Chunk load failed {} {}: {}", cx, cy, e);
} else {
return self.chunks.get_mut(&key);
}
}
}
self.chunks.insert(key, chunk);
}
self.chunks.get_mut(&key)
}
pub fn get_or_create_chunk(&mut self, cx: i32, cy: i32) -> &mut Chunk {
if self.is_bounded() {
let idx = self.chunk_index(cx, cy).unwrap();
return &mut self.chunks_vec[idx];
}
let key = (cx as i64, cy as i64);
if !self.chunks.contains_key(&key) {
self.chunks.insert(key, Chunk::new());
}
self.chunks.get_mut(&key).unwrap()
}
#[inline]
pub fn get(&self, x: i32, y: i32) -> Cell {
if !self.in_bounds(x, y) {
return Cell::new(MaterialId::Stone);
}
let (cx, cy, lx, ly) = self.chunk_at(x, y);
if self.is_bounded() {
if let Some(idx) = self.chunk_index(cx, cy) {
if let Some(chunk) = self.chunks_vec.get(idx) {
return chunk.get(lx, ly);
}
}
} else if let Some(chunk) = self.chunks.get(&(cx as i64, cy as i64)) {
return chunk.get(lx, ly);
}
Cell::new(MaterialId::Stone)
}
#[inline]
pub fn set(&mut self, x: i32, y: i32, cell: Cell) {
if !self.in_bounds(x, y) {
return;
}
let (cx, cy, lx, ly) = self.chunk_at(x, y);
if self.is_bounded() {
if let Some(idx) = self.chunk_index(cx, cy) {
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
chunk.set(lx, ly, cell);
chunk.mark_dirty(lx, ly);
}
}
} else {
let chunk = self.get_or_create_chunk(cx, cy);
chunk.set(lx, ly, cell);
chunk.mark_dirty(lx, ly);
}
}
#[inline]
pub fn set_material(&mut self, x: i32, y: i32, mat: MaterialId) {
if !self.in_bounds(x, y) {
return;
}
let (cx, cy, lx, ly) = self.chunk_at(x, y);
let t = default_temp(mat);
if self.is_bounded() {
if let Some(idx) = self.chunk_index(cx, cy) {
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
chunk.set_material(lx, ly, mat);
chunk.set_temp(lx, ly, t);
chunk.mark_dirty(lx, ly);
}
}
} else {
let chunk = self.get_or_create_chunk(cx, cy);
chunk.set_material(lx, ly, mat);
chunk.set_temp(lx, ly, t);
chunk.mark_dirty(lx, ly);
}
}
#[inline]
pub fn get_temp(&self, x: i32, y: i32) -> f32 {
if !self.in_bounds(x, y) {
return 20.0;
}
let (cx, cy, lx, ly) = self.chunk_at(x, y);
if self.is_bounded() {
if let Some(idx) = self.chunk_index(cx, cy) {
if let Some(chunk) = self.chunks_vec.get(idx) {
return chunk.get_temp(lx, ly);
}
}
} else if let Some(chunk) = self.chunks.get(&(cx as i64, cy as i64)) {
return chunk.get_temp(lx, ly);
}
20.0
}
#[inline]
pub fn set_temp(&mut self, x: i32, y: i32, t: f32) {
if !self.in_bounds(x, y) {
return;
}
let (cx, cy, lx, ly) = self.chunk_at(x, y);
if self.is_bounded() {
if let Some(idx) = self.chunk_index(cx, cy) {
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
chunk.set_temp(lx, ly, t);
}
}
} else {
let chunk = self.get_or_create_chunk(cx, cy);
chunk.set_temp(lx, ly, t);
}
}
#[inline]
pub fn get_pressure(&self, x: i32, y: i32) -> u8 {
if !self.in_bounds(x, y) {
return 128;
}
let (cx, cy, lx, ly) = self.chunk_at(x, y);
if self.is_bounded() {
if let Some(idx) = self.chunk_index(cx, cy) {
if let Some(chunk) = self.chunks_vec.get(idx) {
return chunk.get_pressure(lx, ly);
}
}
} else if let Some(chunk) = self.chunks.get(&(cx as i64, cy as i64)) {
return chunk.get_pressure(lx, ly);
}
128
}
#[inline]
pub fn set_pressure(&mut self, x: i32, y: i32, p: u8) {
if !self.in_bounds(x, y) {
return;
}
let (cx, cy, lx, ly) = self.chunk_at(x, y);
if self.is_bounded() {
if let Some(idx) = self.chunk_index(cx, cy) {
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
chunk.set_pressure(lx, ly, p);
chunk.mark_dirty(lx, ly);
}
}
} else {
let chunk = self.get_or_create_chunk(cx, cy);
chunk.set_pressure(lx, ly, p);
chunk.mark_dirty(lx, ly);
}
}
#[inline]
pub fn get_gas(&self, x: i32, y: i32) -> (u8, u8) {
if !self.in_bounds(x, y) {
return (0, 0);
}
let (cx, cy, lx, ly) = self.chunk_at(x, y);
if self.is_bounded() {
if let Some(idx) = self.chunk_index(cx, cy) {
if let Some(chunk) = self.chunks_vec.get(idx) {
return chunk.get_gas(lx, ly);
}
}
} else if let Some(chunk) = self.chunks.get(&(cx as i64, cy as i64)) {
return chunk.get_gas(lx, ly);
}
(0, 0)
}
#[inline]
pub fn set_gas(&mut self, x: i32, y: i32, gas_type: u8, density: u8) {
if !self.in_bounds(x, y) {
return;
}
let (cx, cy, lx, ly) = self.chunk_at(x, y);
if self.is_bounded() {
if let Some(idx) = self.chunk_index(cx, cy) {
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
chunk.set_gas(lx, ly, gas_type, density);
chunk.mark_dirty(lx, ly);
}
}
} else {
let chunk = self.get_or_create_chunk(cx, cy);
chunk.set_gas(lx, ly, gas_type, density);
chunk.mark_dirty(lx, ly);
}
}
#[inline]
pub fn get_light(&self, x: i32, y: i32) -> [u8; 3] {
if !self.in_bounds(x, y) {
return [0, 0, 0];
}
let (cx, cy, lx, ly) = self.chunk_at(x, y);
if self.is_bounded() {
if let Some(idx) = self.chunk_index(cx, cy) {
if let Some(chunk) = self.chunks_vec.get(idx) {
return chunk.get_light(lx, ly);
}
}
} else if let Some(chunk) = self.chunks.get(&(cx as i64, cy as i64)) {
return chunk.get_light(lx, ly);
}
[0, 0, 0]
}
#[inline]
pub fn set_light(&mut self, x: i32, y: i32, rgb: [u8; 3]) {
if !self.in_bounds(x, y) {
return;
}
let (cx, cy, lx, ly) = self.chunk_at(x, y);
if self.is_bounded() {
if let Some(idx) = self.chunk_index(cx, cy) {
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
chunk.set_light(lx, ly, rgb);
}
}
} else {
let chunk = self.get_or_create_chunk(cx, cy);
chunk.set_light(lx, ly, rgb);
}
}
#[inline]
pub fn mark_dirty(&mut self, x: i32, y: i32) {
if !self.in_bounds(x, y) {
return;
}
let (cx, cy, lx, ly) = self.chunk_at(x, y);
let cs = self.chunk_size as i32;
if self.is_bounded() {
if let Some(idx) = self.chunk_index(cx, cy) {
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
chunk.mark_dirty(lx, ly);
}
}
if lx <= 1 {
if let Some(idx) = self.chunk_index(cx - 1, cy) {
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
chunk.mark_dirty(cs - 1, ly);
}
}
}
if lx >= cs - 2 {
if let Some(idx) = self.chunk_index(cx + 1, cy) {
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
chunk.mark_dirty(0, ly);
}
}
}
if ly <= 1 {
if let Some(idx) = self.chunk_index(cx, cy - 1) {
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
chunk.mark_dirty(lx, cs - 1);
}
}
}
if ly >= cs - 2 {
if let Some(idx) = self.chunk_index(cx, cy + 1) {
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
chunk.mark_dirty(lx, 0);
}
}
}
} else {
let cx64 = cx as i64;
let cy64 = cy as i64;
if let Some(chunk) = self.chunks.get_mut(&(cx64, cy64)) {
chunk.mark_dirty(lx, ly);
}
if lx <= 1 {
if let Some(chunk) = self.chunks.get_mut(&(cx64 - 1, cy64)) {
chunk.mark_dirty(cs - 1, ly);
}
}
if lx >= cs - 2 {
if let Some(chunk) = self.chunks.get_mut(&(cx64 + 1, cy64)) {
chunk.mark_dirty(0, ly);
}
}
if ly <= 1 {
if let Some(chunk) = self.chunks.get_mut(&(cx64, cy64 - 1)) {
chunk.mark_dirty(lx, cs - 1);
}
}
if ly >= cs - 2 {
if let Some(chunk) = self.chunks.get_mut(&(cx64, cy64 + 1)) {
chunk.mark_dirty(lx, 0);
}
}
}
}
#[inline]
pub fn cells_swap(&mut self, x1: i32, y1: i32, x2: i32, y2: i32) {
if !self.in_bounds(x1, y1) || !self.in_bounds(x2, y2) {
return;
}
let (cx1, cy1, lx1, ly1) = self.chunk_at(x1, y1);
let (cx2, cy2, lx2, ly2) = self.chunk_at(x2, y2);
if self.is_bounded() {
let idx1 = self.chunk_index(cx1, cy1);
let idx2 = self.chunk_index(cx2, cy2);
match (idx1, idx2) {
(Some(i1), Some(i2)) if i1 == i2 => {
if let Some(chunk) = self.chunks_vec.get_mut(i1) {
let ci1 = (ly1 as usize) * self.chunk_size + (lx1 as usize);
let ci2 = (ly2 as usize) * self.chunk_size + (lx2 as usize);
chunk.cells.swap(ci1, ci2);
chunk.temps.swap(ci1, ci2);
chunk.pressure.swap(ci1, ci2);
chunk.gas_type.swap(ci1, ci2);
chunk.gas_density.swap(ci1, ci2);
chunk.electricity.swap(ci1, ci2);
chunk.cells[ci2].updated_this_tick = true;
chunk.modified = true;
chunk.mark_dirty(lx1, ly1);
chunk.mark_dirty(lx2, ly2);
}
}
(Some(i1), Some(i2)) => {
let (lo, hi) = if i1 < i2 { (i1, i2) } else { (i2, i1) };
let (left, right) = self.chunks_vec.split_at_mut(hi);
let (ch_lo, ch_hi) = if i1 < i2 {
(left.get_mut(lo), right.first_mut())
} else {
(right.first_mut(), left.get_mut(lo))
};
if let (Some(ch1), Some(ch2)) = (ch_lo, ch_hi) {
let ci1 = (ly1 as usize) * self.chunk_size + (lx1 as usize);
let ci2 = (ly2 as usize) * self.chunk_size + (lx2 as usize);
let (c1, t1, p1, gt1, gd1) = (
ch1.cells[ci1],
ch1.temps[ci1],
ch1.pressure[ci1],
ch1.gas_type[ci1],
ch1.gas_density[ci1],
);
ch1.cells[ci1] = ch2.cells[ci2];
ch1.temps[ci1] = ch2.temps[ci2];
ch1.pressure[ci1] = ch2.pressure[ci2];
ch1.gas_type[ci1] = ch2.gas_type[ci2];
ch1.gas_density[ci1] = ch2.gas_density[ci2];
ch1.cells[ci1].updated_this_tick = true;
ch1.modified = true;
ch1.mark_dirty(lx1, ly1);
ch2.cells[ci2] = c1;
ch2.temps[ci2] = t1;
ch2.pressure[ci2] = p1;
ch2.gas_type[ci2] = gt1;
ch2.gas_density[ci2] = gd1;
ch2.modified = true;
ch2.mark_dirty(lx2, ly2);
}
}
_ => {}
}
} else if cx1 == cx2 && cy1 == cy2 {
let cs = self.chunk_size;
let chunk = self.get_or_create_chunk(cx1, cy1);
let i1 = (ly1 as usize) * cs + (lx1 as usize);
let i2 = (ly2 as usize) * cs + (lx2 as usize);
chunk.cells.swap(i1, i2);
chunk.temps.swap(i1, i2);
chunk.pressure.swap(i1, i2);
chunk.gas_type.swap(i1, i2);
chunk.gas_density.swap(i1, i2);
chunk.electricity.swap(i1, i2);
chunk.cells[i2].updated_this_tick = true;
chunk.modified = true;
chunk.mark_dirty(lx1, ly1);
chunk.mark_dirty(lx2, ly2);
} else {
let c1 = self.get(x1, y1);
let c2 = self.get(x2, y2);
let t1 = self.get_temp(x1, y1);
let t2 = self.get_temp(x2, y2);
let p1 = self.get_pressure(x1, y1);
let p2 = self.get_pressure(x2, y2);
let g1 = self.get_gas(x1, y1);
let g2 = self.get_gas(x2, y2);
self.set(x1, y1, c2);
self.set_temp(x1, y1, t2);
self.set_pressure(x1, y1, p2);
self.set_gas(x1, y1, g2.0, g2.1);
self.set(x2, y2, c1);
self.set_temp(x2, y2, t1);
self.set_pressure(x2, y2, p1);
self.set_gas(x2, y2, g1.0, g1.1);
let cs = self.chunk_size;
let chunk = self.get_or_create_chunk(cx1, cy1);
let i = (ly1 as usize) * cs + (lx1 as usize);
chunk.cells[i].updated_this_tick = true;
}
}
pub fn set_cell_index(&mut self, i: usize, cell: Cell) {
let x = (i % self.chunk_size) as i32;
let y = (i / self.chunk_size) as i32;
self.set(x, y, cell);
}
pub fn reset_tick_flags(&mut self) {
if self.is_bounded() {
for chunk in &mut self.chunks_vec {
if !chunk.active {
continue;
}
for c in &mut chunk.cells {
c.updated_this_tick = false;
}
}
} else {
for chunk in self.chunks.values_mut() {
if !chunk.active {
continue;
}
for c in &mut chunk.cells {
c.updated_this_tick = false;
}
}
}
}
pub fn swap_modified_flags(&mut self) {
if self.is_bounded() {
for chunk in &mut self.chunks_vec {
chunk.swap_modified_flags();
}
} else {
for chunk in self.chunks.values_mut() {
chunk.swap_modified_flags();
}
}
}
pub fn any_modified(&self) -> bool {
if self.is_bounded() {
self.chunks_vec.iter().any(|c| c.modified)
} else {
self.chunks.values().any(|c| c.modified)
}
}
pub fn any_was_modified(&self) -> bool {
if self.is_bounded() {
self.chunks_vec.iter().any(|c| c.was_modified)
} else {
self.chunks.values().any(|c| c.was_modified)
}
}
pub fn active_chunks(&self) -> Vec<(i32, i32)> {
let mut out = Vec::new();
if self.is_bounded() {
for cy in 0..self.chunks_y as i32 {
for cx in 0..self.chunks_x as i32 {
if let Some(idx) = self.chunk_index(cx, cy) {
if self.chunks_vec[idx].active {
out.push((cx, cy));
}
}
}
}
} else {
for (&(cx, cy), chunk) in &self.chunks {
if chunk.active {
out.push((cx as i32, cy as i32));
}
}
}
out
}
pub fn all_chunk_coords(&self) -> Vec<(i32, i32)> {
let mut out = Vec::new();
if self.is_bounded() {
for cy in 0..self.chunks_y as i32 {
for cx in 0..self.chunks_x as i32 {
out.push((cx, cy));
}
}
} else {
for (&(cx, cy), _) in &self.chunks {
out.push((cx as i32, cy as i32));
}
}
out
}
pub fn is_chunk_modified(&self, cx: i32, cy: i32) -> bool {
if self.is_bounded() {
self.chunk_index(cx, cy)
.and_then(|idx| self.chunks_vec.get(idx))
.map(|c| c.modified || c.was_modified)
.unwrap_or(false)
} else {
self.chunks
.get(&(cx as i64, cy as i64))
.map(|c| c.modified || c.was_modified)
.unwrap_or(false)
}
}
pub fn is_chunk_generated(&self, cx: i32, cy: i32) -> bool {
if self.is_bounded() {
self.chunk_index(cx, cy)
.and_then(|idx| self.chunks_vec.get(idx))
.map(|c| c.generated)
.unwrap_or(false)
} else {
self.chunks
.get(&(cx as i64, cy as i64))
.map(|c| c.generated)
.unwrap_or(false)
}
}
pub fn is_chunk_empty(&self, cx: i32, cy: i32) -> bool {
if self.is_bounded() {
self.chunk_index(cx, cy)
.and_then(|idx| self.chunks_vec.get(idx))
.map(|c| c.is_empty())
.unwrap_or(true)
} else {
self.chunks
.get(&(cx as i64, cy as i64))
.map(|c| c.is_empty())
.unwrap_or(true)
}
}
pub fn unload_chunk(&mut self, cx: i32, cy: i32) {
if self.is_bounded() {
return;
}
self.chunks.remove(&(cx as i64, cy as i64));
}
pub fn chunk_bounds(&self, cx: i32, cy: i32) -> (i32, i32, i32, i32) {
let cs = self.chunk_size as i32;
let x0 = cx * cs;
let y0 = cy * cs;
let x1 = x0 + cs;
let y1 = y0 + cs;
match self.bounds {
Some((bx0, by0, bx1, by1)) => {
let bx0_i = bx0 as i32;
let by0_i = by0 as i32;
let bx1_i = bx1 as i32;
let by1_i = by1 as i32;
(x0.max(bx0_i), y0.max(by0_i), x1.min(bx1_i), y1.min(by1_i))
}
None => (x0, y0, x1, y1),
}
}
pub fn is_chunk_active(&self, cx: i32, cy: i32) -> bool {
if self.is_bounded() {
self.chunk_index(cx, cy)
.and_then(|idx| self.chunks_vec.get(idx))
.map(|c| c.active)
.unwrap_or(false)
} else {
self.chunks
.get(&(cx as i64, cy as i64))
.map(|c| c.active)
.unwrap_or(false)
}
}
pub fn set_chunk_active(&mut self, cx: i32, cy: i32, active: bool) {
if self.is_bounded() {
if let Some(idx) = self.chunk_index(cx, cy) {
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
chunk.active = active;
}
}
} else if let Some(chunk) = self.chunks.get_mut(&(cx as i64, cy as i64)) {
chunk.active = active;
}
}
pub fn get_chunk_dirty(&self, cx: i32, cy: i32) -> Option<(i32, i32, i32, i32)> {
let ox = cx * self.chunk_size as i32;
let oy = cy * self.chunk_size as i32;
let dirty = if self.is_bounded() {
self.chunk_index(cx, cy)
.and_then(|idx| self.chunks_vec.get(idx))
.and_then(|c| c.dirty)
} else {
self.chunks
.get(&(cx as i64, cy as i64))
.and_then(|c| c.dirty)
};
dirty.map(|(x0, y0, x1, y1)| (x0 + ox, y0 + oy, x1 + ox, y1 + oy))
}
pub fn set_chunk_dirty(&mut self, cx: i32, cy: i32, dirty: Option<(i32, i32, i32, i32)>) {
let ox = cx * self.chunk_size as i32;
let oy = cy * self.chunk_size as i32;
let local = dirty.map(|(x0, y0, x1, y1)| (x0 - ox, y0 - oy, x1 - ox, y1 - oy));
if self.is_bounded() {
if let Some(idx) = self.chunk_index(cx, cy) {
if let Some(chunk) = self.chunks_vec.get_mut(idx) {
chunk.dirty = local;
}
}
} else if let Some(chunk) = self.chunks.get_mut(&(cx as i64, cy as i64)) {
chunk.dirty = local;
}
}
pub fn activate_around(&mut self, x: i32, y: i32, radius: i32) {
let (cx, cy, _, _) = self.chunk_at(x, y);
for dy in -radius..=radius {
for dx in -radius..=radius {
self.set_chunk_active(cx + dx, cy + dy, true);
}
}
}
pub fn deactivate_all(&mut self) {
if self.is_bounded() {
for chunk in &mut self.chunks_vec {
chunk.active = false;
}
} else {
for chunk in self.chunks.values_mut() {
chunk.active = false;
}
}
}
pub fn is_infinite(&self) -> bool {
self.bounds.is_none()
}
pub fn cell_active(&self, x: i32, y: i32) -> bool {
if !self.in_bounds(x, y) {
return false;
}
let (cx, cy, _, _) = self.chunk_at(x, y);
self.is_chunk_active(cx, cy)
}
pub fn chunk_cells(&self, cx: i32, cy: i32) -> Vec<(i32, i32, Cell)> {
let (x0, y0, x1, y1) = self.chunk_bounds(cx, cy);
let mut out = Vec::with_capacity(((x1 - x0) as usize) * ((y1 - y0) as usize));
for y in y0..y1 {
for x in x0..x1 {
out.push((x, y, self.get(x, y)));
}
}
out
}
pub fn load_chunk_cells(&mut self, cx: i32, cy: i32, cells: &[Cell]) {
let cs = self.chunk_size as i32;
let (x0, y0, x1, y1) = self.chunk_bounds(cx, cy);
let chunk = self.get_or_create_chunk(cx, cy);
let w = (x1 - x0) as usize;
for y in y0..y1 {
for x in x0..x1 {
let i = ((y - y0) as usize) * w + (x - x0) as usize;
if let Some(cell) = cells.get(i) {
let lx = x - cx * cs;
let ly = y - cy * cs;
chunk.set(lx, ly, *cell);
}
}
}
chunk.active = true;
}
pub fn fill_border(&mut self, mat: MaterialId) {
let (x0, y0, x1, y1) = match self.bounds {
Some(b) => b,
None => return,
};
for x in x0..x1 {
self.set_material(x as i32, y0 as i32, mat);
self.set_material(x as i32, (y1 - 1) as i32, mat);
}
for y in y0..y1 {
self.set_material(x0 as i32, y as i32, mat);
self.set_material((x1 - 1) as i32, y as i32, mat);
}
}
pub fn save_chunk(&self, path: &str, cx: i32, cy: i32) -> io::Result<()> {
let chunk = if self.is_bounded() {
match self
.chunk_index(cx, cy)
.and_then(|idx| self.chunks_vec.get(idx))
{
Some(c) => c,
None => return Ok(()),
}
} else {
match self.chunks.get(&(cx as i64, cy as i64)) {
Some(c) => c,
None => return Ok(()),
}
};
let area = self.chunk_size * self.chunk_size;
let mut bytes = Vec::with_capacity(5 + area * 8 + area * 4 + area * 2 + area + area * 3);
bytes.extend_from_slice(b"VWM1");
bytes.push(1);
for c in &chunk.cells {
bytes.extend_from_slice(&c.to_bytes());
}
for t in &chunk.temps {
bytes.extend_from_slice(&t.to_le_bytes());
}
for i in 0..area {
bytes.push(chunk.gas_type[i]);
bytes.push(chunk.gas_density[i]);
}
for &p in &chunk.pressure {
bytes.push(p);
}
for l in &chunk.light {
bytes.extend_from_slice(&l[..]);
}
for &e in &chunk.electricity {
bytes.push(e);
}
let dir = Path::new(path);
if let Some(parent) = dir.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, bytes)
}
pub fn load_chunk(&mut self, path: &str, cx: i32, cy: i32) -> io::Result<()> {
self.load_chunk_from_path(Path::new(path), cx, cy)
}
fn load_chunk_from_path(&mut self, path: &Path, cx: i32, cy: i32) -> io::Result<()> {
let data = std::fs::read(path)?;
let cs = self.chunk_size;
let area = cs * cs;
let bounds = self.chunk_bounds(cx, cy);
let chunk = self.get_or_create_chunk(cx, cy);
if data.len() >= 5 && &data[0..4] == b"VWM1" {
let mut off = 5;
for i in 0..area {
if off + 8 > data.len() {
break;
}
let cell = Cell::from_bytes(&data[off..off + 8]);
let lx = (i % cs) as i32;
let ly = (i / cs) as i32;
chunk.set(lx, ly, cell);
off += 8;
}
for i in 0..area {
if off + 4 > data.len() {
break;
}
chunk.temps[i] =
f32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]);
off += 4;
}
for i in 0..area {
if off + 2 > data.len() {
break;
}
chunk.gas_type[i] = data[off];
chunk.gas_density[i] = data[off + 1];
off += 2;
}
for i in 0..area {
if off >= data.len() {
break;
}
chunk.pressure[i] = data[off];
off += 1;
}
for i in 0..area {
if off + 3 > data.len() {
break;
}
chunk.light[i] = [data[off], data[off + 1], data[off + 2]];
off += 3;
}
for i in 0..area {
if off >= data.len() {
break;
}
chunk.electricity[i] = data[off];
off += 1;
}
} else {
let (x0, y0, x1, y1) = bounds;
let w = (x1 - x0) as usize;
let h = (y1 - y0) as usize;
let expected = w * h * 12;
if data.len() != expected {
return Err(io::Error::other("chunk file size mismatch"));
}
let mut i = 0usize;
for y in y0..y1 {
for x in x0..x1 {
let lx = x - cx * cs as i32;
let ly = y - cy * cs as i32;
let cell = Cell::from_bytes(&data[i * 12..(i + 1) * 12]);
chunk.set(lx, ly, cell);
if cell.material != MaterialId::Empty {
chunk.set_temp(lx, ly, default_temp(cell.material));
}
i += 1;
}
}
}
chunk.active = true;
chunk.generated = true;
Ok(())
}
pub fn load_all_modified(&mut self) -> io::Result<()> {
let cache_dir = match self.cache_dir {
Some(ref dir) => dir,
None => return Ok(()),
};
let base = Path::new(cache_dir).join(format!("seed_{}", self.seed));
if !base.exists() {
return Ok(());
}
for entry in std::fs::read_dir(base)? {
let entry = entry?;
let name = entry.file_name();
let name = name.to_string_lossy();
if let Some(rest) = name.strip_prefix("chunk_") {
let rest = rest.strip_suffix(".bin").unwrap_or(rest);
let parts: Vec<&str> = rest.split('_').collect();
if parts.len() == 2 {
if let (Ok(cx), Ok(cy)) = (parts[0].parse::<i32>(), parts[1].parse::<i32>()) {
let path = entry.path();
self.load_chunk_from_path(&path, cx, cy)?;
}
}
}
}
Ok(())
}
pub fn save_all_modified(&self) -> io::Result<()> {
let cache_dir = match self.cache_dir {
Some(ref dir) => dir,
None => return Ok(()),
};
if self.is_bounded() {
for cy in 0..self.chunks_y as i32 {
for cx in 0..self.chunks_x as i32 {
if let Some(idx) = self.chunk_index(cx, cy) {
let chunk = &self.chunks_vec[idx];
if chunk.modified || chunk.was_modified {
let path = chunk_path(cache_dir, self.seed, cx, cy);
self.save_chunk(path.to_str().unwrap(), cx, cy)?;
}
}
}
}
} else {
for (&(cx, cy), chunk) in &self.chunks {
if chunk.modified || chunk.was_modified {
let path = chunk_path(cache_dir, self.seed, cx as i32, cy as i32);
self.save_chunk(path.to_str().unwrap(), cx as i32, cy as i32)?;
}
}
}
Ok(())
}
pub fn unload_distant(&mut self, px: i32, py: i32, radius: i32) {
if self.is_bounded() {
return;
}
let (pcx, pcy, _, _) = self.chunk_at(px, py);
let mut to_remove = Vec::new();
for (&(cx, cy), chunk) in &self.chunks {
if (cx - pcx as i64).abs() > radius as i64 || (cy - pcy as i64).abs() > radius as i64 {
if chunk.modified || chunk.was_modified {
if let Some(ref dir) = self.cache_dir {
let path = chunk_path(dir, self.seed, cx as i32, cy as i32);
let _ = self.save_chunk(path.to_str().unwrap(), cx as i32, cy as i32);
}
}
to_remove.push((cx, cy));
}
}
for key in to_remove {
self.chunks.remove(&key);
}
}
pub fn ensure_loaded(&mut self, px: i32, py: i32, radius: i32) {
let (pcx, pcy, _, _) = self.chunk_at(px, py);
for dy in -radius..=radius {
for dx in -radius..=radius {
let cx = pcx + dx;
let cy = pcy + dy;
let _ = self.ensure_chunk(cx, cy);
self.set_chunk_active(cx, cy, true);
}
}
}
}
pub fn chunk_path(root: &str, seed: u64, cx: i32, cy: i32) -> PathBuf {
Path::new(root)
.join(format!("seed_{}", seed))
.join(format!("chunk_{}_{}.bin", cx, cy))
}