From f177cc716c5f2aa5b50b14ccbb421de89e3a7854 Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Sun, 16 Aug 2026 17:29:42 -0700 Subject: wip --- src/sim/cell.rs | 38 ------- src/sim/cell/cell.rs | 38 +++++++ src/sim/cell/materials/fire.rs | 79 ++++++++++++++ src/sim/cell/materials/gas.rs | 76 +++++++++++++ src/sim/cell/materials/mod.rs | 88 +++++++++++++++ src/sim/cell/materials/sand.rs | 10 ++ src/sim/cell/materials/smoke.rs | 20 ++++ src/sim/cell/materials/water.rs | 76 +++++++++++++ src/sim/cell/mod.rs | 2 + src/sim/cell_sim/chunk.rs | 29 +++++ src/sim/cell_sim/mod.rs | 4 + src/sim/cell_sim/overlay.rs | 50 +++++++++ src/sim/cell_sim/sim.rs | 234 ++++++++++++++++++++++++++++++++++++++++ src/sim/cell_sim/world.rs | 69 ++++++++++++ src/sim/chunk.rs | 29 ----- src/sim/materials/fire.rs | 76 ------------- src/sim/materials/gas.rs | 76 ------------- src/sim/materials/mod.rs | 88 --------------- src/sim/materials/sand.rs | 10 -- src/sim/materials/smoke.rs | 20 ---- src/sim/materials/water.rs | 76 ------------- src/sim/mod.rs | 7 +- src/sim/overlay.rs | 50 --------- src/sim/rb_sim/mod.rs | 131 ++++++++++++++++++++++ src/sim/rb_sim/rb_entity.rs | 24 +++++ src/sim/sim.rs | 231 --------------------------------------- src/sim/world.rs | 69 ------------ 27 files changed, 932 insertions(+), 768 deletions(-) delete mode 100644 src/sim/cell.rs create mode 100644 src/sim/cell/cell.rs create mode 100644 src/sim/cell/materials/fire.rs create mode 100644 src/sim/cell/materials/gas.rs create mode 100644 src/sim/cell/materials/mod.rs create mode 100644 src/sim/cell/materials/sand.rs create mode 100644 src/sim/cell/materials/smoke.rs create mode 100644 src/sim/cell/materials/water.rs create mode 100644 src/sim/cell/mod.rs create mode 100644 src/sim/cell_sim/chunk.rs create mode 100644 src/sim/cell_sim/mod.rs create mode 100644 src/sim/cell_sim/overlay.rs create mode 100644 src/sim/cell_sim/sim.rs create mode 100644 src/sim/cell_sim/world.rs delete mode 100644 src/sim/chunk.rs delete mode 100644 src/sim/materials/fire.rs delete mode 100644 src/sim/materials/gas.rs delete mode 100644 src/sim/materials/mod.rs delete mode 100644 src/sim/materials/sand.rs delete mode 100644 src/sim/materials/smoke.rs delete mode 100644 src/sim/materials/water.rs delete mode 100644 src/sim/overlay.rs create mode 100644 src/sim/rb_sim/mod.rs create mode 100644 src/sim/rb_sim/rb_entity.rs delete mode 100644 src/sim/sim.rs delete mode 100644 src/sim/world.rs (limited to 'src/sim') diff --git a/src/sim/cell.rs b/src/sim/cell.rs deleted file mode 100644 index d3ca302..0000000 --- a/src/sim/cell.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::sim::materials::MaterialId; - -#[derive(Clone, Copy)] -pub struct Cell { - pub material: MaterialId, - pub flags: u8, - pub data: u16, -} - -impl Cell { - const FLAG_PARITY: u8 = 0b0000_0001; - - #[inline] - pub fn parity(self) -> u8 { - self.flags & Self::FLAG_PARITY - } - #[inline] - pub fn flip_parity(&mut self) { - self.flags ^= Self::FLAG_PARITY; - } -} - -impl Cell { - pub fn void() -> Cell { - Cell { - material: MaterialId::Void, - flags: 0, - data: 0, - } - } - pub fn from_material(material: MaterialId) -> Cell { - Cell { - material, - flags: 0, - data: 0, - } - } -} diff --git a/src/sim/cell/cell.rs b/src/sim/cell/cell.rs new file mode 100644 index 0000000..d35fa76 --- /dev/null +++ b/src/sim/cell/cell.rs @@ -0,0 +1,38 @@ +use crate::sim::cell::materials::MaterialId; + +#[derive(Clone, Copy)] +pub struct Cell { + pub material: MaterialId, + pub flags: u8, + pub data: u16, +} + +impl Cell { + const FLAG_PARITY: u8 = 0b0000_0001; + + #[inline] + pub fn parity(self) -> u8 { + self.flags & Self::FLAG_PARITY + } + #[inline] + pub fn flip_parity(&mut self) { + self.flags ^= Self::FLAG_PARITY; + } +} + +impl Cell { + pub fn void() -> Cell { + Cell { + material: MaterialId::Void, + flags: 0, + data: 0, + } + } + pub fn from_material(material: MaterialId) -> Cell { + Cell { + material, + flags: 0, + data: 0, + } + } +} diff --git a/src/sim/cell/materials/fire.rs b/src/sim/cell/materials/fire.rs new file mode 100644 index 0000000..e0416b0 --- /dev/null +++ b/src/sim/cell/materials/fire.rs @@ -0,0 +1,79 @@ +use rand::RngExt; + +use crate::sim::{ + cell::{cell::Cell, materials::MaterialId}, + cell_sim::sim::UpdateCtx, +}; + +trait FireCellView { + fn get_ticks_lived(self) -> u16; + fn set_ticks_lived(&mut self, ticks: u16) -> (); + fn is_flammable(self) -> bool; +} + +impl FireCellView for Cell { + fn get_ticks_lived(self) -> u16 { + self.data + } + fn set_ticks_lived(&mut self, ticks: u16) { + self.data = ticks; + } + // this could be a property of the material def, I think it's better here for now + fn is_flammable(self) -> bool { + [MaterialId::Wood].contains(&self.material) + } +} + +// TODO optimize number of rng calls? +#[inline] +pub fn sim_update(ctx: &mut UpdateCtx) { + let ticks_lived = ctx.cell.get_ticks_lived(); + // 2 seconds + if ticks_lived > 240 { + // we have a chance to live longer--roughly 50% chance of living an extra second + if ctx.rng.random_range(0.0..1.0) > 0.995 { + // kill ourselves, with a chance to turn into ash + if ctx.rng.random_range(0.0..1.0) > 0.8 { + // TODO ash material + ctx.set_cell(0, 0, Cell::from_material(MaterialId::Sand)); + } else { + ctx.set_cell(0, 0, Cell::void()); + } + return; + } + } + + // at an average of 4 times per lifespan, try to spread + // 1/60 * 240 = 4 + if ctx.rng.random_range(0.0..1.0) > (59.0 / 60.0) { + let (dx, dy) = (ctx.rng.random_range(-1..=1), ctx.rng.random_range(-1..=1)); + if let Some(target) = ctx.get_cell(dx, dy) + && target.is_flammable() + { + ctx.set_cell(dx, dy, Cell::from_material(MaterialId::Fire)); + } + } + + // 8 times in our lifespan, emit smoke + if ticks_lived.is_multiple_of(30) + && let Some(target) = ctx.get_cell(0, -1) + && target.material == MaterialId::Void + { + for (dx, dy) in [ + (0, -1), + (1 - ctx.seqno_parity as i32 * 2, 0), + (-1 + ctx.seqno_parity as i32 * 2, 0), + (0, 1), + ] { + if let Some(target) = ctx.get_cell(dx, dy) + && target.material == MaterialId::Void + { + ctx.set_cell(dx, dy, Cell::from_material(MaterialId::Smoke)); + break; + } + } + } + + ctx.cell.set_ticks_lived(ticks_lived + 1); + ctx.set_cell(0, 0, *ctx.cell); +} diff --git a/src/sim/cell/materials/gas.rs b/src/sim/cell/materials/gas.rs new file mode 100644 index 0000000..c0cd9f9 --- /dev/null +++ b/src/sim/cell/materials/gas.rs @@ -0,0 +1,76 @@ +use crate::sim::cell_sim::sim::UpdateCtx; + +#[inline] +pub fn sim_update(ctx: &mut UpdateCtx) { + // if the water can fall, do so + if ctx.candidates_swap(&[ + (0, -1), + (-1 + 2 * ctx.seqno_parity as i32, -1), + (1 - 2 * ctx.seqno_parity as i32, -1), + ]) { + return; + } + + // if the water can't fall, check if we can move left or right + // these are inverted on parity so that we don't preference a direction + let left_target = ctx.get_cell(-1, 0); + let can_move_left = + left_target.is_some_and(|c| c.material.def().density < ctx.material.density); + let right_target = ctx.get_cell(1, 0); + let can_move_right = + right_target.is_some_and(|c| c.material.def().density < ctx.material.density); + + // we can't move down or to other side, so we're stuck + if !can_move_left && !can_move_right { + return; + } + + // if we can't move left, just move right + if !can_move_left { + ctx.candidates_swap(&[(1, 0)]); + return; + } + // and vice versa + if !can_move_right { + ctx.candidates_swap(&[(-1, 0)]); + return; + } + + // find the closest hole within 20 pixels (TODO optimize) + // a hole is any space below us with a lesser density + // prevents equidistance stuck state + let starting_side = if ctx.seqno_parity == 0 { 1 } else { -1 }; + for i in 0..20 { + let side = if i % 2 == 0 { + starting_side + } else { + -starting_side + }; + + let offset = side * (1 + i / 2); + let hole_target = ctx.get_cell(offset, -1); + if let Some(target) = hole_target + && target.material.def().density < ctx.material.density + { + // we identified a hole and we know that the space on this side is open + // move toward the hole + // new_target.flags = new_target.flags ^ 0b1; + ctx.candidates_swap(&[(side, 0)]); + return; + } + } + + // we didn't find a hole, so just move "randomly" on the same surface + // TODO when to settle? + let target_x = if !can_move_left { + 1 + } else if !can_move_right { + -1 + } else if ctx.seqno_parity % 2 == 1 { + 1 + } else { + -1 + }; + + ctx.candidates_swap(&[(target_x, 0)]); +} diff --git a/src/sim/cell/materials/mod.rs b/src/sim/cell/materials/mod.rs new file mode 100644 index 0000000..a55ed51 --- /dev/null +++ b/src/sim/cell/materials/mod.rs @@ -0,0 +1,88 @@ +use crate::sim::cell_sim::sim::UpdateCtx; + +mod fire; +mod gas; +mod sand; +mod smoke; +mod water; + +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum MaterialId { + Void = 0, + Sand, + Wood, + Water, + Gas, + Fire, + Smoke, +} + +pub struct MaterialDef { + pub name: &'static str, + pub color: (u8, u8, u8, u8), + pub density: u8, + pub sim_update: Option ()>, +} + +static MATERIALS: [MaterialDef; 7] = [ + MaterialDef { + name: "Void", + color: (0x00, 0x00, 0x00, 0x00), + density: 0, + sim_update: None, + }, + MaterialDef { + name: "Sand", + color: (0xDE, 0xCB, 0x85, 0xFF), + density: 50, + sim_update: Some(sand::sim_update), + }, + MaterialDef { + name: "Wood", + color: (0x85, 0x56, 0x1D, 0xFF), + density: 50, + sim_update: None, + }, + MaterialDef { + name: "Water", + color: (0x38, 0xA9, 0xFF, 0xFF), + density: 40, + sim_update: Some(water::sim_update), + }, + MaterialDef { + name: "Gas", + color: (0xBD, 0xFF, 0xE4, 0xAA), + density: 10, + sim_update: Some(gas::sim_update), + }, + MaterialDef { + name: "Fire", + color: (0xFC, 0x66, 0x00, 0xAA), + // for now this matches wood + density: 50, + sim_update: Some(fire::sim_update), + }, + MaterialDef { + name: "Smoke", + color: (0x32, 0x35, 0x36, 0xAA), + density: 15, + sim_update: Some(smoke::sim_update), + }, +]; + +impl MaterialId { + pub const ALL: [MaterialId; 7] = [ + MaterialId::Void, + MaterialId::Sand, + MaterialId::Wood, + MaterialId::Water, + MaterialId::Gas, + MaterialId::Fire, + MaterialId::Smoke, + ]; + #[inline] + pub fn def(self) -> &'static MaterialDef { + &MATERIALS[self as usize] + } +} diff --git a/src/sim/cell/materials/sand.rs b/src/sim/cell/materials/sand.rs new file mode 100644 index 0000000..d2d1265 --- /dev/null +++ b/src/sim/cell/materials/sand.rs @@ -0,0 +1,10 @@ +use crate::sim::cell_sim::sim::UpdateCtx; + +#[inline] +pub fn sim_update(ctx: &mut UpdateCtx) { + ctx.candidates_swap(&[ + (0, 1), + (-1 + 2 * ctx.seqno_parity as i32, 1), + (1 - 2 * ctx.seqno_parity as i32, 1), + ]); +} diff --git a/src/sim/cell/materials/smoke.rs b/src/sim/cell/materials/smoke.rs new file mode 100644 index 0000000..2ee7c00 --- /dev/null +++ b/src/sim/cell/materials/smoke.rs @@ -0,0 +1,20 @@ +use rand::RngExt; + +use crate::sim::cell_sim::sim::UpdateCtx; + +#[inline] +pub fn sim_update(ctx: &mut UpdateCtx) { + // only allow upward movement some of the time to limit movement speed + if ctx.rng.random_range(0.0..1.0) > 0.8 && ctx.candidates_swap(&[(0, -1)]) { + return; + } + // same for each horizontal direction + if ctx.rng.random_range(0.0..1.0) > 0.8 + && ctx.candidates_swap(&[(1 - ctx.seqno_parity as i32 * 2, 0)]) + { + return; + } + if ctx.rng.random_range(0.0..1.0) > 0.8 + && ctx.candidates_swap(&[(-1 + ctx.seqno_parity as i32 * 2, 0)]) + {} +} diff --git a/src/sim/cell/materials/water.rs b/src/sim/cell/materials/water.rs new file mode 100644 index 0000000..4b848fb --- /dev/null +++ b/src/sim/cell/materials/water.rs @@ -0,0 +1,76 @@ +use crate::sim::cell_sim::sim::UpdateCtx; + +#[inline] +pub fn sim_update(ctx: &mut UpdateCtx) { + // if the water can fall, do so + if ctx.candidates_swap(&[ + (0, 1), + (-1 + 2 * ctx.seqno_parity as i32, 1), + (1 - 2 * ctx.seqno_parity as i32, 1), + ]) { + return; + } + + // if the water can't fall, check if we can move left or right + // these are inverted on parity so that we don't preference a direction + let left_target = ctx.get_cell(-1, 0); + let can_move_left = + left_target.is_some_and(|c| c.material.def().density < ctx.material.density); + let right_target = ctx.get_cell(1, 0); + let can_move_right = + right_target.is_some_and(|c| c.material.def().density < ctx.material.density); + + // we can't move down or to other side, so we're stuck + if !can_move_left && !can_move_right { + return; + } + + // if we can't move left, just move right + if !can_move_left { + ctx.candidates_swap(&[(1, 0)]); + return; + } + // and vice versa + if !can_move_right { + ctx.candidates_swap(&[(-1, 0)]); + return; + } + + // find the closest hole within 20 pixels (TODO optimize) + // a hole is any space below us with a lesser density + // prevents equidistance stuck state + let starting_side = if ctx.seqno_parity == 0 { 1 } else { -1 }; + for i in 0..20 { + let side = if i % 2 == 0 { + starting_side + } else { + -starting_side + }; + + let offset = side * (1 + i / 2); + let hole_target = ctx.get_cell(offset, 1); + if let Some(target) = hole_target + && target.material.def().density < ctx.material.density + { + // we identified a hole and we know that the space on this side is open + // move toward the hole + // new_target.flags = new_target.flags ^ 0b1; + ctx.candidates_swap(&[(side, 0)]); + return; + } + } + + // we didn't find a hole, so just move "randomly" on the same surface + // TODO when to settle? + // let target_x = if !can_move_left { + // 1 + // } else if !can_move_right { + // -1 + // } else if ctx.seqno_parity % 2 == 1 { + // 1 + // } else { + // -1 + // }; + + // ctx.candidates_swap(&[(target_x, 0)]); +} diff --git a/src/sim/cell/mod.rs b/src/sim/cell/mod.rs new file mode 100644 index 0000000..2d2175c --- /dev/null +++ b/src/sim/cell/mod.rs @@ -0,0 +1,2 @@ +pub mod cell; +pub mod materials; diff --git a/src/sim/cell_sim/chunk.rs b/src/sim/cell_sim/chunk.rs new file mode 100644 index 0000000..116a38a --- /dev/null +++ b/src/sim/cell_sim/chunk.rs @@ -0,0 +1,29 @@ +use crate::{ + config::{CELLS_IN_CHUNK, CHUNK_SIZE}, + sim::cell::cell::Cell, +}; + +pub struct Chunk { + pub cells: Box<[Cell; CELLS_IN_CHUNK]>, + pub sleeping: bool, + pub needs_texture_update: bool, +} + +impl Chunk { + #[inline] + pub fn get_cell_at_local_position(&self, x: u8, y: u8) -> Cell { + self.cells[x as usize + y as usize * CHUNK_SIZE as usize] + } + #[inline] + pub fn set_cell_at_local_position(&mut self, x: u8, y: u8, cell: Cell) { + self.cells[x as usize + y as usize * CHUNK_SIZE as usize] = cell; + } + + pub fn void() -> Self { + Chunk { + cells: Box::new([Cell::void(); CELLS_IN_CHUNK]), + sleeping: true, + needs_texture_update: true, + } + } +} diff --git a/src/sim/cell_sim/mod.rs b/src/sim/cell_sim/mod.rs new file mode 100644 index 0000000..a1db2a1 --- /dev/null +++ b/src/sim/cell_sim/mod.rs @@ -0,0 +1,4 @@ +pub mod chunk; +pub mod overlay; +pub mod sim; +pub mod world; diff --git a/src/sim/cell_sim/overlay.rs b/src/sim/cell_sim/overlay.rs new file mode 100644 index 0000000..ee494ae --- /dev/null +++ b/src/sim/cell_sim/overlay.rs @@ -0,0 +1,50 @@ +use crate::{Config, Input, sim::cell_sim::world::World}; + +pub fn create_compute_combined_overlay_offset( + world: &World, + config: &Config, + input: &Input, +) -> impl Fn(i32, i32) -> (u8, u8, u8, u8) { + puffin::profile_function!(); + // TODO fix this move? + move |pixel_x: i32, pixel_y: i32| { + // could allow negative offsets too + let mut offset: (u8, u8, u8, u8) = (0x00, 0x00, 0x00, 0x00); + + // // bounds + // // left + // let xl = -((board.get_game_width() / 2 + 1) as i32); + // // right + // let xu = (board.get_game_width() / 2 + 1) as i32; + // // bottom + // let yl = -((board.get_game_height() / 2 + 1) as i32); + // // top + // let yu = (board.get_game_height() / 2 + 1) as i32; + + // if ((pixel_x == xl || pixel_x == xu) && (pixel_y <= yu && pixel_y >= yl)) + // || (pixel_y == yl || pixel_y == yu) && (pixel_x <= xu && pixel_x >= xl) + // { + // offset.0 = offset.0.saturating_add(0xFF); + // offset.1 = offset.1.saturating_add(0xFF); + // offset.2 = offset.2.saturating_add(0xFF); + // } + + // // grid + // if pixel_x % 30 == 0 || pixel_y % 30 == 0 { + // offset.0 = offset.0.saturating_add(0x10); + // offset.1 = offset.1.saturating_add(0x10); + // offset.2 = offset.2.saturating_add(0x10); + // } + + // // brush/selection + // if input.last_mouse_pos_on_board.is_some_and(|p| { + // ((pixel_x - p.0).pow(2) + (pixel_y - p.1).pow(2)) < (config.brush_radius as i32).pow(2) + // }) { + // offset.0 = offset.0.saturating_add(0x82); + // offset.1 = offset.1.saturating_add(0xA1); + // offset.2 = offset.2.saturating_add(0xAD); + // } + + return offset; + } +} diff --git a/src/sim/cell_sim/sim.rs b/src/sim/cell_sim/sim.rs new file mode 100644 index 0000000..fd9b6c9 --- /dev/null +++ b/src/sim/cell_sim/sim.rs @@ -0,0 +1,234 @@ +use std::marker::PhantomData; + +use fxhash::FxHashMap; +use rand::{Rng, SeedableRng, rngs::SmallRng}; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; + +use crate::{ + config::CHUNK_SIZE, + sim::{ + cell::{cell::Cell, materials::MaterialDef}, + cell_sim::{chunk::Chunk, world::World}, + }, +}; + +struct ChunkAccess<'a> { + ptr: *mut Chunk, + len: usize, + _marker: PhantomData<&'a mut [Chunk]>, +} + +impl<'a> ChunkAccess<'a> { + pub fn new(chunks: &'a mut [Chunk]) -> Self { + Self { + ptr: chunks.as_mut_ptr(), + len: chunks.len(), + _marker: PhantomData, + } + } + unsafe fn get(&self, i: usize) -> &'a mut Chunk { + debug_assert!(i < self.len); + unsafe { &mut *self.ptr.add(i) } + } +} + +unsafe impl Sync for ChunkAccess<'_> {} + +fn get_cell(chunks: &[Option<&mut Chunk>; 9], x: i32, y: i32) -> Option { + let dcx = x.div_euclid(CHUNK_SIZE); + let dcy = y.div_euclid(CHUNK_SIZE); + if dcx != 0 || dcy != 0 { + // in a different chunk + let nc_x = x.rem_euclid(CHUNK_SIZE) as u8; + let nc_y = y.rem_euclid(CHUNK_SIZE) as u8; + + chunks[(dcx + 1 + (dcy + 1) * 3) as usize] + .as_ref() + .map(|chunk| chunk.get_cell_at_local_position(nc_x, nc_y)) + } else { + chunks[4] + .as_ref() + .map(|target| target.get_cell_at_local_position(x as u8, y as u8)) + } +} + +pub fn set_cell(chunks: &mut [Option<&mut Chunk>; 9], x: i32, y: i32, cell: Cell) { + let dcx = x.div_euclid(CHUNK_SIZE); + let dcy = y.div_euclid(CHUNK_SIZE); + if dcx != 0 || dcy != 0 { + // in a different chunk + let nc_x = x.rem_euclid(CHUNK_SIZE) as u8; + let nc_y = y.rem_euclid(CHUNK_SIZE) as u8; + + if let Some(chunk) = &mut chunks[(dcx + 1 + (dcy + 1) * 3) as usize] { + chunk.set_cell_at_local_position(nc_x, nc_y, cell); + chunk.needs_texture_update = true; + } + } else { + if let Some(target) = &mut chunks[4] { + target.set_cell_at_local_position(x as u8, y as u8, cell); + target.needs_texture_update = true; + } + } +} + +pub struct UpdateCtx<'a, 'b, 'c> { + pub chunks: &'a mut [Option<&'b mut Chunk>; 9], + pub seqno: u64, + pub seqno_parity: u8, + + pub x: i32, + pub y: i32, + pub cell: &'c mut Cell, + pub material: &'c MaterialDef, + + pub rng: &'c mut dyn Rng, +} + +impl UpdateCtx<'_, '_, '_> { + pub fn get_cell(&self, dx: i32, dy: i32) -> Option { + let x = self.x + dx; + let y = self.y + dy; + get_cell(self.chunks, x, y) + } + + pub fn set_cell(&mut self, dx: i32, dy: i32, cell: Cell) { + // cannot move out of the neighbourhood, but also cannot move to the edge of the neighbourhood + // as this would wake a chunk outside of the neighbourhood + debug_assert!(dx > -15 && dx < 15); + debug_assert!(dy > -15 && dy < 15); + let x = self.x + dx; + let y = self.y + dy; + set_cell(self.chunks, x, y, cell); + self.chunks.iter_mut().for_each(|c| { + if let Some(chunk) = c { + chunk.sleeping = false; + } + }) + } + + pub fn candidates_swap(&mut self, candidates: &[(i32, i32)]) -> bool { + for &(dx, dy) in candidates { + let candidate_cell = self.get_cell(dx, dy); + if candidate_cell.is_some_and(|c| c.material.def().density < self.material.density) { + self.set_cell(0, 0, candidate_cell.unwrap()); + self.set_cell(dx, dy, *self.cell); + return true; + } + } + false + } +} + +pub fn sim_tick_chunk(chunks: &mut [Option<&mut Chunk>; 9], seqno: u64) { + puffin::profile_function!(); + let seqno_parity = (seqno as u8) & 0b1; + let mut rng = SmallRng::seed_from_u64(seqno); + + if chunks[4].is_some() { + for y in (0..CHUNK_SIZE).rev() { + for i in 0..CHUNK_SIZE { + let x = if seqno_parity == 0 { + i + } else { + (CHUNK_SIZE) - i - 1 + }; + + let mut cell = get_cell(chunks, x, y).unwrap(); + let material = cell.material.def(); + + if let Some(update) = material.sim_update + && cell.parity() == seqno_parity + { + cell.flip_parity(); + // apply the flipped parity in case the sim target doesn't + set_cell(chunks, x, y, cell); + + let mut update_ctx = UpdateCtx { + chunks, + seqno, + seqno_parity, + + x, + y, + cell: &mut cell, + material, + + // TODO this is platform-dependent, will break for multiplayer + rng: &mut rng, + }; + + update(&mut update_ctx); + } + } + } + } +} + +const NEIGHBORHOOD_OFFSETS: [(i32, i32); 9] = [ + (-1, -1), + (0, -1), + (1, -1), + (-1, 0), + (0, 0), + (1, 0), + (-1, 1), + (0, 1), + (1, 1), +]; + +pub fn sim_tick(world: &mut World, seqno: u64, use_threading: bool) { + puffin::profile_function!(); + + let mut columns: FxHashMap> = FxHashMap::default(); + for &(cx, cy) in world.chunk_position_to_chunk_idx.keys() { + columns.entry(cx).or_default().push(cy); + } + + // color columns s.t. columns of same color are separated by two columns + // and sort the column bottom-to-top + // -------------------- + // | 0, 1, 2, 0, 1, 2 | + // | 0, 1, 2, 0, 1, 2 | + // | 0, 1, 2, 0, 1, 2 | + // -------------------- + let mut columns_by_color: [Vec<(i32, Vec)>; 3] = Default::default(); + for (cx, mut cys) in columns { + cys.sort_unstable_by(|a, b| b.cmp(a)); + columns_by_color[cx.rem_euclid(3) as usize].push((cx, cys)); + } + + let access = ChunkAccess::new(&mut world.chunks); + + for color in &columns_by_color { + puffin::profile_scope!("chunk_color"); + + let chunk_closure = |(cx, cys): &(i32, Vec)| { + let cx = *cx; + for &cy in cys { + let mut chunks: [Option<&mut Chunk>; 9] = NEIGHBORHOOD_OFFSETS.map(|(dx, dy)| { + world + .chunk_position_to_chunk_idx + .get(&(cx + dx, cy + dy)) + .map(|&idx| unsafe { access.get(idx) }) + }); + + if let Some(target) = &mut chunks[4] { + if target.sleeping { + continue; + } + target.sleeping = true; + } + + sim_tick_chunk(&mut chunks, seqno); + } + }; + + if use_threading { + // TODO use forte + color.par_iter().for_each(chunk_closure); + } else { + color.iter().for_each(chunk_closure); + }; + } +} diff --git a/src/sim/cell_sim/world.rs b/src/sim/cell_sim/world.rs new file mode 100644 index 0000000..924639a --- /dev/null +++ b/src/sim/cell_sim/world.rs @@ -0,0 +1,69 @@ +use fxhash::FxHashMap; + +use crate::{ + config::CHUNK_SIZE, + sim::{cell::cell::Cell, cell_sim::chunk::Chunk}, +}; + +pub struct World { + pub chunks: Vec, + // TODO FxFxHashMap? + pub chunk_position_to_chunk_idx: FxHashMap<(i32, i32), usize>, +} + +impl World { + #[inline] + pub fn split_game_position(x: i32, y: i32) -> ((i32, i32), (u8, u8)) { + ( + ( + // TODO is this cast expensive? + x.div_euclid(CHUNK_SIZE), + y.div_euclid(CHUNK_SIZE), + ), + ( + x.rem_euclid(CHUNK_SIZE) as u8, + y.rem_euclid(CHUNK_SIZE) as u8, + ), + ) + } + + // VERY EXPENSIVE + pub fn get_cell_from_game_position(&self, x: i32, y: i32) -> Option { + let ((cx, cy), (dx, dy)) = World::split_game_position(x, y); + self.chunk_position_to_chunk_idx + .get(&(cx, cy)) + .map(|&idx| self.chunks[idx].get_cell_at_local_position(dx, dy)) + } + + // VERY EXPENSIVE + pub fn set_cell_from_game_position(&mut self, x: i32, y: i32, cell: Cell, sleeping: bool) { + let ((cx, cy), (dx, dy)) = World::split_game_position(x, y); + if let Some(&idx) = self.chunk_position_to_chunk_idx.get(&(cx, cy)) { + self.chunks[idx].set_cell_at_local_position(dx, dy, cell); + // this is a temporary hack + self.chunks[idx].sleeping = sleeping; + self.chunks[idx].needs_texture_update = true; + } + } + + pub fn insert(&mut self, x: i32, y: i32, chunk: Chunk) { + self.chunk_position_to_chunk_idx + .insert((x, y), self.chunks.len()); + self.chunks.push(chunk); + } + + pub fn from_default_size() -> Self { + let mut world = World { + chunks: Vec::new(), + chunk_position_to_chunk_idx: FxHashMap::default(), + }; + + for y in -10..1 { + for x in -100..100 { + world.insert(x, y, Chunk::void()); + } + } + + world + } +} diff --git a/src/sim/chunk.rs b/src/sim/chunk.rs deleted file mode 100644 index 478af12..0000000 --- a/src/sim/chunk.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::{ - config::{CELLS_IN_CHUNK, CHUNK_SIZE}, - sim::cell::Cell, -}; - -pub struct Chunk { - pub cells: Box<[Cell; CELLS_IN_CHUNK]>, - pub sleeping: bool, - pub needs_texture_update: bool, -} - -impl Chunk { - #[inline] - pub fn get_cell_at_local_position(&self, x: u8, y: u8) -> Cell { - self.cells[x as usize + y as usize * CHUNK_SIZE as usize] - } - #[inline] - pub fn set_cell_at_local_position(&mut self, x: u8, y: u8, cell: Cell) { - self.cells[x as usize + y as usize * CHUNK_SIZE as usize] = cell; - } - - pub fn void() -> Self { - Chunk { - cells: Box::new([Cell::void(); CELLS_IN_CHUNK]), - sleeping: true, - needs_texture_update: true, - } - } -} diff --git a/src/sim/materials/fire.rs b/src/sim/materials/fire.rs deleted file mode 100644 index 97027d5..0000000 --- a/src/sim/materials/fire.rs +++ /dev/null @@ -1,76 +0,0 @@ -use rand::RngExt; - -use crate::sim::{cell::Cell, materials::MaterialId, sim::UpdateCtx}; - -trait FireCellView { - fn get_ticks_lived(self) -> u16; - fn set_ticks_lived(&mut self, ticks: u16) -> (); - fn is_flammable(self) -> bool; -} - -impl FireCellView for Cell { - fn get_ticks_lived(self) -> u16 { - self.data - } - fn set_ticks_lived(&mut self, ticks: u16) { - self.data = ticks; - } - // this could be a property of the material def, I think it's better here for now - fn is_flammable(self) -> bool { - [MaterialId::Wood].contains(&self.material) - } -} - -// TODO optimize number of rng calls? -#[inline] -pub fn sim_update(ctx: &mut UpdateCtx) { - let ticks_lived = ctx.cell.get_ticks_lived(); - // 2 seconds - if ticks_lived > 240 { - // we have a chance to live longer--roughly 50% chance of living an extra second - if ctx.rng.random_range(0.0..1.0) > 0.995 { - // kill ourselves, with a chance to turn into ash - if ctx.rng.random_range(0.0..1.0) > 0.8 { - // TODO ash material - ctx.set_cell(0, 0, Cell::from_material(MaterialId::Sand)); - } else { - ctx.set_cell(0, 0, Cell::void()); - } - return; - } - } - - // at an average of 4 times per lifespan, try to spread - // 1/60 * 240 = 4 - if ctx.rng.random_range(0.0..1.0) > (59.0 / 60.0) { - let (dx, dy) = (ctx.rng.random_range(-1..=1), ctx.rng.random_range(-1..=1)); - if let Some(target) = ctx.get_cell(dx, dy) - && target.is_flammable() - { - ctx.set_cell(dx, dy, Cell::from_material(MaterialId::Fire)); - } - } - - // 8 times in our lifespan, emit smoke - if ticks_lived.is_multiple_of(30) - && let Some(target) = ctx.get_cell(0, -1) - && target.material == MaterialId::Void - { - for (dx, dy) in [ - (0, -1), - (1 - ctx.seqno_parity as i32 * 2, 0), - (-1 + ctx.seqno_parity as i32 * 2, 0), - (0, 1), - ] { - if let Some(target) = ctx.get_cell(dx, dy) - && target.material == MaterialId::Void - { - ctx.set_cell(dx, dy, Cell::from_material(MaterialId::Smoke)); - break; - } - } - } - - ctx.cell.set_ticks_lived(ticks_lived + 1); - ctx.set_cell(0, 0, *ctx.cell); -} diff --git a/src/sim/materials/gas.rs b/src/sim/materials/gas.rs deleted file mode 100644 index 8d9f42b..0000000 --- a/src/sim/materials/gas.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::sim::sim::UpdateCtx; - -#[inline] -pub fn sim_update(ctx: &mut UpdateCtx) { - // if the water can fall, do so - if ctx.candidates_swap(&[ - (0, -1), - (-1 + 2 * ctx.seqno_parity as i32, -1), - (1 - 2 * ctx.seqno_parity as i32, -1), - ]) { - return; - } - - // if the water can't fall, check if we can move left or right - // these are inverted on parity so that we don't preference a direction - let left_target = ctx.get_cell(-1, 0); - let can_move_left = - left_target.is_some_and(|c| c.material.def().density < ctx.material.density); - let right_target = ctx.get_cell(1, 0); - let can_move_right = - right_target.is_some_and(|c| c.material.def().density < ctx.material.density); - - // we can't move down or to other side, so we're stuck - if !can_move_left && !can_move_right { - return; - } - - // if we can't move left, just move right - if !can_move_left { - ctx.candidates_swap(&[(1, 0)]); - return; - } - // and vice versa - if !can_move_right { - ctx.candidates_swap(&[(-1, 0)]); - return; - } - - // find the closest hole within 20 pixels (TODO optimize) - // a hole is any space below us with a lesser density - // prevents equidistance stuck state - let starting_side = if ctx.seqno_parity == 0 { 1 } else { -1 }; - for i in 0..20 { - let side = if i % 2 == 0 { - starting_side - } else { - -starting_side - }; - - let offset = side * (1 + i / 2); - let hole_target = ctx.get_cell(offset, -1); - if let Some(target) = hole_target - && target.material.def().density < ctx.material.density - { - // we identified a hole and we know that the space on this side is open - // move toward the hole - // new_target.flags = new_target.flags ^ 0b1; - ctx.candidates_swap(&[(side, 0)]); - return; - } - } - - // we didn't find a hole, so just move "randomly" on the same surface - // TODO when to settle? - let target_x = if !can_move_left { - 1 - } else if !can_move_right { - -1 - } else if ctx.seqno_parity % 2 == 1 { - 1 - } else { - -1 - }; - - ctx.candidates_swap(&[(target_x, 0)]); -} diff --git a/src/sim/materials/mod.rs b/src/sim/materials/mod.rs deleted file mode 100644 index 10f0a06..0000000 --- a/src/sim/materials/mod.rs +++ /dev/null @@ -1,88 +0,0 @@ -use crate::sim::sim::UpdateCtx; - -mod fire; -mod gas; -mod sand; -mod smoke; -mod water; - -#[repr(u8)] -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum MaterialId { - Void = 0, - Sand, - Wood, - Water, - Gas, - Fire, - Smoke, -} - -pub struct MaterialDef { - pub name: &'static str, - pub color: (u8, u8, u8, u8), - pub density: u8, - pub sim_update: Option ()>, -} - -static MATERIALS: [MaterialDef; 7] = [ - MaterialDef { - name: "Void", - color: (0x00, 0x00, 0x00, 0x00), - density: 0, - sim_update: None, - }, - MaterialDef { - name: "Sand", - color: (0xDE, 0xCB, 0x85, 0xFF), - density: 50, - sim_update: Some(sand::sim_update), - }, - MaterialDef { - name: "Wood", - color: (0x85, 0x56, 0x1D, 0xFF), - density: 50, - sim_update: None, - }, - MaterialDef { - name: "Water", - color: (0x38, 0xA9, 0xFF, 0xFF), - density: 40, - sim_update: Some(water::sim_update), - }, - MaterialDef { - name: "Gas", - color: (0xBD, 0xFF, 0xE4, 0xAA), - density: 10, - sim_update: Some(gas::sim_update), - }, - MaterialDef { - name: "Fire", - color: (0xFC, 0x66, 0x00, 0xAA), - // for now this matches wood - density: 50, - sim_update: Some(fire::sim_update), - }, - MaterialDef { - name: "Smoke", - color: (0x32, 0x35, 0x36, 0xAA), - density: 15, - sim_update: Some(smoke::sim_update), - }, -]; - -impl MaterialId { - pub const ALL: [MaterialId; 7] = [ - MaterialId::Void, - MaterialId::Sand, - MaterialId::Wood, - MaterialId::Water, - MaterialId::Gas, - MaterialId::Fire, - MaterialId::Smoke, - ]; - #[inline] - pub fn def(self) -> &'static MaterialDef { - &MATERIALS[self as usize] - } -} diff --git a/src/sim/materials/sand.rs b/src/sim/materials/sand.rs deleted file mode 100644 index ac8889f..0000000 --- a/src/sim/materials/sand.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::sim::sim::UpdateCtx; - -#[inline] -pub fn sim_update(ctx: &mut UpdateCtx) { - ctx.candidates_swap(&[ - (0, 1), - (-1 + 2 * ctx.seqno_parity as i32, 1), - (1 - 2 * ctx.seqno_parity as i32, 1), - ]); -} diff --git a/src/sim/materials/smoke.rs b/src/sim/materials/smoke.rs deleted file mode 100644 index 5240bc9..0000000 --- a/src/sim/materials/smoke.rs +++ /dev/null @@ -1,20 +0,0 @@ -use rand::RngExt; - -use crate::sim::sim::UpdateCtx; - -#[inline] -pub fn sim_update(ctx: &mut UpdateCtx) { - // only allow upward movement some of the time to limit movement speed - if ctx.rng.random_range(0.0..1.0) > 0.8 && ctx.candidates_swap(&[(0, -1)]) { - return; - } - // same for each horizontal direction - if ctx.rng.random_range(0.0..1.0) > 0.8 - && ctx.candidates_swap(&[(1 - ctx.seqno_parity as i32 * 2, 0)]) - { - return; - } - if ctx.rng.random_range(0.0..1.0) > 0.8 - && ctx.candidates_swap(&[(-1 + ctx.seqno_parity as i32 * 2, 0)]) - {} -} diff --git a/src/sim/materials/water.rs b/src/sim/materials/water.rs deleted file mode 100644 index 1ba8eb4..0000000 --- a/src/sim/materials/water.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::sim::sim::UpdateCtx; - -#[inline] -pub fn sim_update(ctx: &mut UpdateCtx) { - // if the water can fall, do so - if ctx.candidates_swap(&[ - (0, 1), - (-1 + 2 * ctx.seqno_parity as i32, 1), - (1 - 2 * ctx.seqno_parity as i32, 1), - ]) { - return; - } - - // if the water can't fall, check if we can move left or right - // these are inverted on parity so that we don't preference a direction - let left_target = ctx.get_cell(-1, 0); - let can_move_left = - left_target.is_some_and(|c| c.material.def().density < ctx.material.density); - let right_target = ctx.get_cell(1, 0); - let can_move_right = - right_target.is_some_and(|c| c.material.def().density < ctx.material.density); - - // we can't move down or to other side, so we're stuck - if !can_move_left && !can_move_right { - return; - } - - // if we can't move left, just move right - if !can_move_left { - ctx.candidates_swap(&[(1, 0)]); - return; - } - // and vice versa - if !can_move_right { - ctx.candidates_swap(&[(-1, 0)]); - return; - } - - // find the closest hole within 20 pixels (TODO optimize) - // a hole is any space below us with a lesser density - // prevents equidistance stuck state - let starting_side = if ctx.seqno_parity == 0 { 1 } else { -1 }; - for i in 0..20 { - let side = if i % 2 == 0 { - starting_side - } else { - -starting_side - }; - - let offset = side * (1 + i / 2); - let hole_target = ctx.get_cell(offset, 1); - if let Some(target) = hole_target - && target.material.def().density < ctx.material.density - { - // we identified a hole and we know that the space on this side is open - // move toward the hole - // new_target.flags = new_target.flags ^ 0b1; - ctx.candidates_swap(&[(side, 0)]); - return; - } - } - - // we didn't find a hole, so just move "randomly" on the same surface - // TODO when to settle? - // let target_x = if !can_move_left { - // 1 - // } else if !can_move_right { - // -1 - // } else if ctx.seqno_parity % 2 == 1 { - // 1 - // } else { - // -1 - // }; - - // ctx.candidates_swap(&[(target_x, 0)]); -} diff --git a/src/sim/mod.rs b/src/sim/mod.rs index 7553f89..63a9c56 100644 --- a/src/sim/mod.rs +++ b/src/sim/mod.rs @@ -1,6 +1,3 @@ pub mod cell; -pub mod chunk; -pub mod materials; -pub mod overlay; -pub mod sim; -pub mod world; +pub mod cell_sim; +pub mod rb_sim; diff --git a/src/sim/overlay.rs b/src/sim/overlay.rs deleted file mode 100644 index c9fdf17..0000000 --- a/src/sim/overlay.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::{Config, Input, sim::world::World}; - -pub fn create_compute_combined_overlay_offset( - world: &World, - config: &Config, - input: &Input, -) -> impl Fn(i32, i32) -> (u8, u8, u8, u8) { - puffin::profile_function!(); - // TODO fix this move? - move |pixel_x: i32, pixel_y: i32| { - // could allow negative offsets too - let mut offset: (u8, u8, u8, u8) = (0x00, 0x00, 0x00, 0x00); - - // // bounds - // // left - // let xl = -((board.get_game_width() / 2 + 1) as i32); - // // right - // let xu = (board.get_game_width() / 2 + 1) as i32; - // // bottom - // let yl = -((board.get_game_height() / 2 + 1) as i32); - // // top - // let yu = (board.get_game_height() / 2 + 1) as i32; - - // if ((pixel_x == xl || pixel_x == xu) && (pixel_y <= yu && pixel_y >= yl)) - // || (pixel_y == yl || pixel_y == yu) && (pixel_x <= xu && pixel_x >= xl) - // { - // offset.0 = offset.0.saturating_add(0xFF); - // offset.1 = offset.1.saturating_add(0xFF); - // offset.2 = offset.2.saturating_add(0xFF); - // } - - // // grid - // if pixel_x % 30 == 0 || pixel_y % 30 == 0 { - // offset.0 = offset.0.saturating_add(0x10); - // offset.1 = offset.1.saturating_add(0x10); - // offset.2 = offset.2.saturating_add(0x10); - // } - - // // brush/selection - // if input.last_mouse_pos_on_board.is_some_and(|p| { - // ((pixel_x - p.0).pow(2) + (pixel_y - p.1).pow(2)) < (config.brush_radius as i32).pow(2) - // }) { - // offset.0 = offset.0.saturating_add(0x82); - // offset.1 = offset.1.saturating_add(0xA1); - // offset.2 = offset.2.saturating_add(0xAD); - // } - - return offset; - } -} diff --git a/src/sim/rb_sim/mod.rs b/src/sim/rb_sim/mod.rs new file mode 100644 index 0000000..1818052 --- /dev/null +++ b/src/sim/rb_sim/mod.rs @@ -0,0 +1,131 @@ +pub mod rb_entity; + +use fxhash::FxHashMap; +use rapier2d::{ + dynamics::{self}, + geometry, + glamx::vec2, + math, prelude, +}; + +use crate::{ + config::{CELLS_IN_CHUNK, PHYSICS_DELTA_TIME}, + sim::{ + cell::{cell::Cell, materials::MaterialId}, + rb_sim::rb_entity::RbEntity, + }, +}; + +pub struct PhysicsManager { + rigid_body_set: prelude::RigidBodySet, + collider_set: prelude::ColliderSet, + physics_pipeline: prelude::PhysicsPipeline, + integration_parameters: prelude::IntegrationParameters, + island_manager: prelude::IslandManager, + broad_phase: prelude::DefaultBroadPhase, + narrow_phase: prelude::NarrowPhase, + impulse_joint_set: prelude::ImpulseJointSet, + multibody_joint_set: prelude::MultibodyJointSet, + ccd_solver: prelude::CCDSolver, +} + +impl PhysicsManager { + pub fn new() -> Self { + PhysicsManager { + rigid_body_set: prelude::RigidBodySet::new(), + collider_set: prelude::ColliderSet::new(), + physics_pipeline: prelude::PhysicsPipeline::new(), + integration_parameters: prelude::IntegrationParameters { + // 20 pixels = 1 meter + length_unit: 20.0, + dt: PHYSICS_DELTA_TIME, + ..prelude::IntegrationParameters::default() + }, + island_manager: prelude::IslandManager::new(), + broad_phase: prelude::DefaultBroadPhase::new(), + narrow_phase: prelude::NarrowPhase::new(), + impulse_joint_set: prelude::ImpulseJointSet::new(), + multibody_joint_set: prelude::MultibodyJointSet::new(), + ccd_solver: prelude::CCDSolver::new(), + } + } +} + +pub struct RbSimManager { + physics_manager: PhysicsManager, + pub rb_entities: FxHashMap, +} + +impl RbSimManager { + pub fn rb_tick(&mut self, delta_time: f32) { + let gravity = vec2(0.0, -9.81); + + self.physics_manager.physics_pipeline.step( + gravity, + &self.physics_manager.integration_parameters, + &mut self.physics_manager.island_manager, + &mut self.physics_manager.broad_phase, + &mut self.physics_manager.narrow_phase, + &mut self.physics_manager.rigid_body_set, + &mut self.physics_manager.collider_set, + &mut self.physics_manager.impulse_joint_set, + &mut self.physics_manager.multibody_joint_set, + &mut self.physics_manager.ccd_solver, + &(), + &(), + ); + } + + pub fn get_rb_entity_position(&self, entity_id: u32) -> Option<(f32, f32)> { + let entity = self.rb_entities.get(&entity_id); + match entity { + Some(entity) => { + let rb = self.physics_manager.rigid_body_set.get(entity.rb_parent); + rb.map(|rb| (rb.position().translation.x, rb.position().translation.y)) + } + None => return None, + } + } + + pub fn test(&mut self) { + /* Create the ground. */ + let collider = geometry::ColliderBuilder::cuboid(100.0, 0.1).build(); + self.physics_manager.collider_set.insert(collider); + + /* Create the bouncing ball. */ + let rigid_body = dynamics::RigidBodyBuilder::dynamic() + .translation(math::Vector::new(0.0, 10.0)) + .build(); + let collider = geometry::ColliderBuilder::ball(0.5) + .restitution(0.7) + .build(); + let ball_body_handle = self.physics_manager.rigid_body_set.insert(rigid_body); + self.physics_manager.collider_set.insert_with_parent( + collider, + ball_body_handle, + &mut self.physics_manager.rigid_body_set, + ); + + let test_cells = Box::new([Cell::void(); CELLS_IN_CHUNK]); + let mut rb_entity = RbEntity { + id: 0, + cells: test_cells, + rb_parent: ball_body_handle, + }; + + for x in 0..10 { + for y in 0..10 { + rb_entity.set_cell_at_local_position(x, y, Cell::from_material(MaterialId::Wood)); + } + } + + self.rb_entities.insert(0, rb_entity); + } + + pub fn new() -> Self { + RbSimManager { + physics_manager: PhysicsManager::new(), + rb_entities: FxHashMap::default(), + } + } +} diff --git a/src/sim/rb_sim/rb_entity.rs b/src/sim/rb_sim/rb_entity.rs new file mode 100644 index 0000000..50243bc --- /dev/null +++ b/src/sim/rb_sim/rb_entity.rs @@ -0,0 +1,24 @@ +use rapier2d::prelude; + +use crate::{ + config::{CELLS_IN_CHUNK, CHUNK_SIZE}, + sim::cell::cell::Cell, +}; + +pub struct RbEntity { + pub id: u32, + // TODO resizable + pub cells: Box<[Cell; CELLS_IN_CHUNK]>, + pub rb_parent: prelude::RigidBodyHandle, +} + +impl RbEntity { + #[inline] + pub fn get_cell_at_local_position(&self, x: u8, y: u8) -> Cell { + self.cells[x as usize + y as usize * CHUNK_SIZE as usize] + } + #[inline] + pub fn set_cell_at_local_position(&mut self, x: u8, y: u8, cell: Cell) { + self.cells[x as usize + y as usize * CHUNK_SIZE as usize] = cell; + } +} diff --git a/src/sim/sim.rs b/src/sim/sim.rs deleted file mode 100644 index b9f4b76..0000000 --- a/src/sim/sim.rs +++ /dev/null @@ -1,231 +0,0 @@ -use std::marker::PhantomData; - -use fxhash::FxHashMap; -use rand::{Rng, SeedableRng, rngs::SmallRng}; -use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; - -use crate::{ - config::CHUNK_SIZE, - sim::{cell::Cell, chunk::Chunk, materials::MaterialDef, world::World}, -}; - -struct ChunkAccess<'a> { - ptr: *mut Chunk, - len: usize, - _marker: PhantomData<&'a mut [Chunk]>, -} - -impl<'a> ChunkAccess<'a> { - pub fn new(chunks: &'a mut [Chunk]) -> Self { - Self { - ptr: chunks.as_mut_ptr(), - len: chunks.len(), - _marker: PhantomData, - } - } - unsafe fn get(&self, i: usize) -> &'a mut Chunk { - debug_assert!(i < self.len); - unsafe { &mut *self.ptr.add(i) } - } -} - -unsafe impl Sync for ChunkAccess<'_> {} - -fn get_cell(chunks: &[Option<&mut Chunk>; 9], x: i32, y: i32) -> Option { - let dcx = x.div_euclid(CHUNK_SIZE); - let dcy = y.div_euclid(CHUNK_SIZE); - if dcx != 0 || dcy != 0 { - // in a different chunk - let nc_x = x.rem_euclid(CHUNK_SIZE) as u8; - let nc_y = y.rem_euclid(CHUNK_SIZE) as u8; - - chunks[(dcx + 1 + (dcy + 1) * 3) as usize] - .as_ref() - .map(|chunk| chunk.get_cell_at_local_position(nc_x, nc_y)) - } else { - chunks[4] - .as_ref() - .map(|target| target.get_cell_at_local_position(x as u8, y as u8)) - } -} - -pub fn set_cell(chunks: &mut [Option<&mut Chunk>; 9], x: i32, y: i32, cell: Cell) { - let dcx = x.div_euclid(CHUNK_SIZE); - let dcy = y.div_euclid(CHUNK_SIZE); - if dcx != 0 || dcy != 0 { - // in a different chunk - let nc_x = x.rem_euclid(CHUNK_SIZE) as u8; - let nc_y = y.rem_euclid(CHUNK_SIZE) as u8; - - if let Some(chunk) = &mut chunks[(dcx + 1 + (dcy + 1) * 3) as usize] { - chunk.set_cell_at_local_position(nc_x, nc_y, cell); - chunk.needs_texture_update = true; - } - } else { - if let Some(target) = &mut chunks[4] { - target.set_cell_at_local_position(x as u8, y as u8, cell); - target.needs_texture_update = true; - } - } -} - -pub struct UpdateCtx<'a, 'b, 'c> { - pub chunks: &'a mut [Option<&'b mut Chunk>; 9], - pub seqno: u64, - pub seqno_parity: u8, - - pub x: i32, - pub y: i32, - pub cell: &'c mut Cell, - pub material: &'c MaterialDef, - - pub rng: &'c mut dyn Rng, -} - -impl UpdateCtx<'_, '_, '_> { - pub fn get_cell(&self, dx: i32, dy: i32) -> Option { - let x = self.x + dx; - let y = self.y + dy; - get_cell(self.chunks, x, y) - } - - pub fn set_cell(&mut self, dx: i32, dy: i32, cell: Cell) { - // cannot move out of the neighbourhood, but also cannot move to the edge of the neighbourhood - // as this would wake a chunk outside of the neighbourhood - debug_assert!(dx > -15 && dx < 15); - debug_assert!(dy > -15 && dy < 15); - let x = self.x + dx; - let y = self.y + dy; - set_cell(self.chunks, x, y, cell); - self.chunks.iter_mut().for_each(|c| { - if let Some(chunk) = c { - chunk.sleeping = false; - } - }) - } - - pub fn candidates_swap(&mut self, candidates: &[(i32, i32)]) -> bool { - for &(dx, dy) in candidates { - let candidate_cell = self.get_cell(dx, dy); - if candidate_cell.is_some_and(|c| c.material.def().density < self.material.density) { - self.set_cell(0, 0, candidate_cell.unwrap()); - self.set_cell(dx, dy, *self.cell); - return true; - } - } - false - } -} - -pub fn sim_tick_chunk(chunks: &mut [Option<&mut Chunk>; 9], seqno: u64) { - puffin::profile_function!(); - let seqno_parity = (seqno as u8) & 0b1; - let mut rng = SmallRng::seed_from_u64(seqno); - - if chunks[4].is_some() { - for y in (0..CHUNK_SIZE).rev() { - for i in 0..CHUNK_SIZE { - let x = if seqno_parity == 0 { - i - } else { - (CHUNK_SIZE) - i - 1 - }; - - let mut cell = get_cell(chunks, x, y).unwrap(); - let material = cell.material.def(); - - if let Some(update) = material.sim_update - && cell.parity() == seqno_parity - { - cell.flip_parity(); - // apply the flipped parity in case the sim target doesn't - set_cell(chunks, x, y, cell); - - let mut update_ctx = UpdateCtx { - chunks, - seqno, - seqno_parity, - - x, - y, - cell: &mut cell, - material, - - // TODO this is platform-dependent, will break for multiplayer - rng: &mut rng, - }; - - update(&mut update_ctx); - } - } - } - } -} - -const NEIGHBORHOOD_OFFSETS: [(i32, i32); 9] = [ - (-1, -1), - (0, -1), - (1, -1), - (-1, 0), - (0, 0), - (1, 0), - (-1, 1), - (0, 1), - (1, 1), -]; - -pub fn sim_tick(world: &mut World, seqno: u64, use_threading: bool) { - puffin::profile_function!(); - - let mut columns: FxHashMap> = FxHashMap::default(); - for &(cx, cy) in world.chunk_position_to_chunk_idx.keys() { - columns.entry(cx).or_default().push(cy); - } - - // color columns s.t. columns of same color are separated by two columns - // and sort the column bottom-to-top - // -------------------- - // | 0, 1, 2, 0, 1, 2 | - // | 0, 1, 2, 0, 1, 2 | - // | 0, 1, 2, 0, 1, 2 | - // -------------------- - let mut columns_by_color: [Vec<(i32, Vec)>; 3] = Default::default(); - for (cx, mut cys) in columns { - cys.sort_unstable_by(|a, b| b.cmp(a)); - columns_by_color[cx.rem_euclid(3) as usize].push((cx, cys)); - } - - let access = ChunkAccess::new(&mut world.chunks); - - for color in &columns_by_color { - puffin::profile_scope!("chunk_color"); - - let chunk_closure = |(cx, cys): &(i32, Vec)| { - let cx = *cx; - for &cy in cys { - let mut chunks: [Option<&mut Chunk>; 9] = NEIGHBORHOOD_OFFSETS.map(|(dx, dy)| { - world - .chunk_position_to_chunk_idx - .get(&(cx + dx, cy + dy)) - .map(|&idx| unsafe { access.get(idx) }) - }); - - if let Some(target) = &mut chunks[4] { - if target.sleeping { - continue; - } - target.sleeping = true; - } - - sim_tick_chunk(&mut chunks, seqno); - } - }; - - if use_threading { - // TODO use forte - color.par_iter().for_each(chunk_closure); - } else { - color.iter().for_each(chunk_closure); - }; - } -} diff --git a/src/sim/world.rs b/src/sim/world.rs deleted file mode 100644 index d54906c..0000000 --- a/src/sim/world.rs +++ /dev/null @@ -1,69 +0,0 @@ -use fxhash::FxHashMap; - -use crate::{ - config::CHUNK_SIZE, - sim::{cell::Cell, chunk::Chunk}, -}; - -pub struct World { - pub chunks: Vec, - // TODO FxFxHashMap? - pub chunk_position_to_chunk_idx: FxHashMap<(i32, i32), usize>, -} - -impl World { - #[inline] - pub fn split_game_position(x: i32, y: i32) -> ((i32, i32), (u8, u8)) { - ( - ( - // TODO is this cast expensive? - x.div_euclid(CHUNK_SIZE), - y.div_euclid(CHUNK_SIZE), - ), - ( - x.rem_euclid(CHUNK_SIZE) as u8, - y.rem_euclid(CHUNK_SIZE) as u8, - ), - ) - } - - // VERY EXPENSIVE - pub fn get_cell_from_game_position(&self, x: i32, y: i32) -> Option { - let ((cx, cy), (dx, dy)) = World::split_game_position(x, y); - self.chunk_position_to_chunk_idx - .get(&(cx, cy)) - .map(|&idx| self.chunks[idx].get_cell_at_local_position(dx, dy)) - } - - // VERY EXPENSIVE - pub fn set_cell_from_game_position(&mut self, x: i32, y: i32, cell: Cell, sleeping: bool) { - let ((cx, cy), (dx, dy)) = World::split_game_position(x, y); - if let Some(&idx) = self.chunk_position_to_chunk_idx.get(&(cx, cy)) { - self.chunks[idx].set_cell_at_local_position(dx, dy, cell); - // this is a temporary hack - self.chunks[idx].sleeping = sleeping; - self.chunks[idx].needs_texture_update = true; - } - } - - pub fn insert(&mut self, x: i32, y: i32, chunk: Chunk) { - self.chunk_position_to_chunk_idx - .insert((x, y), self.chunks.len()); - self.chunks.push(chunk); - } - - pub fn from_default_size() -> Self { - let mut world = World { - chunks: Vec::new(), - chunk_position_to_chunk_idx: FxHashMap::default(), - }; - - for y in -10..1 { - for x in -100..100 { - world.insert(x, y, Chunk::void()); - } - } - - world - } -} -- cgit v1.3.1