From 7ce9781f86c56f932dabc3bc84b8d21d7be8fecc Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Tue, 11 Aug 2026 20:41:41 -0700 Subject: refactor sim loop --- src/camera.rs | 6 +- src/sim/materials.rs | 41 ------------- src/sim/materials/mod.rs | 60 ++++++++++++++++++ src/sim/materials/sand.rs | 10 +++ src/sim/materials/water.rs | 78 ++++++++++++++++++++++++ src/sim/overlay.rs | 2 - src/sim/sim.rs | 149 ++++++++++++--------------------------------- src/ui.rs | 34 +++++------ 8 files changed, 206 insertions(+), 174 deletions(-) delete mode 100644 src/sim/materials.rs create mode 100644 src/sim/materials/mod.rs create mode 100644 src/sim/materials/sand.rs create mode 100644 src/sim/materials/water.rs (limited to 'src') diff --git a/src/camera.rs b/src/camera.rs index d10c92e..604cb0d 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,7 +1,7 @@ use crate::{ Input, config::{CAMERA_MOVEMENT_SPEED, PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH}, - sim::{board::Board, materials::MATERIALS}, + sim::board::Board, }; pub struct Camera { @@ -71,8 +71,8 @@ impl Camera { let cell = board.cell_at_position(x_coord as i32, y_coord as i32); let cell_color: Option<(u8, u8, u8, u8)> = cell.map(|c| { - let m = MATERIALS[c.material as usize]; - (m.r, m.g, m.b, 0xFF) + let m = c.material.def(); + (m.color[0], m.color[1], m.color[2], 0xFF) }); let off_grid_color: (u8, u8, u8, u8) = (0x00, 0x00, 0x00, 0xFF); diff --git a/src/sim/materials.rs b/src/sim/materials.rs deleted file mode 100644 index 8b21618..0000000 --- a/src/sim/materials.rs +++ /dev/null @@ -1,41 +0,0 @@ -#[derive(Clone, Copy)] -pub struct Material<'a> { - pub name: &'a str, - pub r: u8, - pub g: u8, - pub b: u8, - - pub density: u8, -} - -#[repr(u8)] -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum MaterialId { - Void, - Sand, - Water, -} - -pub static MATERIALS: [Material; 3] = [ - Material { - name: "Void", - r: 0x00, - g: 0x00, - b: 0x00, - density: 0, - }, - Material { - name: "Sand", - r: 0xDE, - g: 0xCB, - b: 0x85, - density: 50, - }, - Material { - name: "Water", - r: 0x38, - g: 0xA9, - b: 0xFF, - density: 40, - }, -]; diff --git a/src/sim/materials/mod.rs b/src/sim/materials/mod.rs new file mode 100644 index 0000000..410236d --- /dev/null +++ b/src/sim/materials/mod.rs @@ -0,0 +1,60 @@ +use crate::sim::sim::UpdateCtx; + +mod sand; +mod water; + +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum MaterialId { + Void = 0, + Sand, + Wood, + Water, +} + +pub struct MaterialDef { + pub name: &'static str, + pub color: [u8; 4], + pub density: u8, + pub sim_update: Option ()>, +} + +static MATERIALS: [MaterialDef; 4] = [ + MaterialDef { + name: "Void", + color: [0x00, 0x00, 0x00, 0xFF], + 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), + }, +]; + +impl MaterialId { + pub const ALL: [MaterialId; 4] = [ + MaterialId::Void, + MaterialId::Sand, + MaterialId::Wood, + MaterialId::Water, + ]; + #[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 new file mode 100644 index 0000000..6a780f3 --- /dev/null +++ b/src/sim/materials/sand.rs @@ -0,0 +1,10 @@ +use crate::sim::sim::UpdateCtx; + +#[inline] +pub fn sim_update(ctx: &mut UpdateCtx) { + ctx.candidates_swap(&[ + (ctx.self_x, ctx.self_y + 1), + (ctx.self_x - 1 + 2 * ctx.seqno_parity as i32, ctx.self_y + 1), + (ctx.self_x + 1 - 2 * ctx.seqno_parity as i32, ctx.self_y + 1), + ]); +} diff --git a/src/sim/materials/water.rs b/src/sim/materials/water.rs new file mode 100644 index 0000000..d875aee --- /dev/null +++ b/src/sim/materials/water.rs @@ -0,0 +1,78 @@ +use crate::sim::sim::UpdateCtx; + +#[inline] +pub fn sim_update(ctx: &mut UpdateCtx) { + // if the water can fall, do so + if ctx.candidates_swap(&[ + (ctx.self_x, ctx.self_y + 1), + (ctx.self_x - 1 + 2 * ctx.seqno_parity as i32, ctx.self_y + 1), + (ctx.self_x + 1 - 2 * ctx.seqno_parity as i32, ctx.self_y + 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.board.cell_at_position(ctx.self_x - 1, ctx.self_y); + let can_move_left = left_target + .is_some_and(|c| c.material.def().density < ctx.self_cell.material.def().density); + let right_target = ctx.board.cell_at_position(ctx.self_x + 1, ctx.self_y); + let can_move_right = right_target + .is_some_and(|c| c.material.def().density < ctx.self_cell.material.def().density); + + // we can't move down or to other side, so we're stuck + if !can_move_left && !can_move_right { + 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 + }; + if (side == 1 && !can_move_right) || (side == -1 && !can_move_left) { + continue; + } + + let offset = side * (1 + i / 2); + let hole_target = ctx + .board + .cell_at_position(ctx.self_x + offset, ctx.self_y + 1); + if let Some(target) = hole_target + && target.material.def().density < ctx.self_cell.material.def().density + { + // we identified a hole and we know that the space on this side is open + // move toward the hole + let move_target = if side == 1 { right_target } else { left_target }.clone(); + // new_target.flags = new_target.flags ^ 0b1; + // safe to unwrap + ctx.board + .set_cell_at_position(ctx.self_x, ctx.self_y, move_target.unwrap()); + ctx.board + .set_cell_at_position(ctx.self_x + side, ctx.self_y, ctx.self_cell); + return; + } + } + + // we didn't find a hole, so just move "randomly" on the same surface + // TODO when to settle? + let (target, target_x) = if !can_move_left { + (right_target, 1) + } else if !can_move_right { + (left_target, -1) + } else if ctx.seqno_parity % 2 == 1 { + (right_target, 1) + } else { + (left_target, -1) + }; + + ctx.board + .set_cell_at_position(ctx.self_x, ctx.self_y, target.unwrap()); + ctx.board + .set_cell_at_position(ctx.self_x + target_x, ctx.self_y, ctx.self_cell); +} diff --git a/src/sim/overlay.rs b/src/sim/overlay.rs index 7c5d5a4..e0560b5 100644 --- a/src/sim/overlay.rs +++ b/src/sim/overlay.rs @@ -1,5 +1,3 @@ -use core::range::Range; - use crate::{Config, Input, sim::board::Board}; pub fn create_compute_combined_overlay_offset( diff --git a/src/sim/sim.rs b/src/sim/sim.rs index 186483d..350b823 100644 --- a/src/sim/sim.rs +++ b/src/sim/sim.rs @@ -1,9 +1,32 @@ -use core::range::Range; +use crate::{Board, sim::board::Cell}; + +pub struct UpdateCtx<'a> { + pub self_x: i32, + pub self_y: i32, + pub self_cell: Cell, + pub delta_time: f32, + pub seqno_parity: u8, + pub board: &'a mut Board, +} -use crate::{ - Board, - sim::materials::{MATERIALS, MaterialId}, -}; +impl UpdateCtx<'_> { + pub fn candidates_swap(&mut self, candidates: &[(i32, i32)]) -> bool { + for candidate in candidates { + let target = self.board.cell_at_position(candidate.0, candidate.1); + if let Some(target) = target + && target.material.def().density < self.self_cell.material.def().density + { + // swap the cells + self.board + .set_cell_at_position(self.self_x, self.self_y, target); + self.board + .set_cell_at_position(candidate.0, candidate.1, self.self_cell); + return true; + } + } + false + } +} // TODO: chunks pub fn sim_tick(board: &mut Board, seqno: u64, delta_time: f32) { @@ -18,115 +41,21 @@ pub fn sim_tick(board: &mut Board, seqno: u64, delta_time: f32) { let x = if seqno_parity == 0 { col } else { -col }; let cell = board.cell_at_position(x, y); - if let Some(cell) = cell - && cell.flags & 0b1 == seqno_parity + if let Some(mut cur) = cell + && cur.flags & 0b1 == seqno_parity { - let mut cur = cell.clone(); // flip the parity bit cur.flags = cur.flags ^ 0b1; - let material = &MATERIALS[cur.material as usize]; - - match cur.material { - MaterialId::Void => {} - // TODO abstract density based movement - MaterialId::Sand => { - for candidate in [ - (x, y + 1), - (x - 1 + 2 * seqno_parity as i32, y + 1), - (x + 1 - 2 * seqno_parity as i32, y + 1), - ] { - let target = board.cell_at_position(candidate.0, candidate.1); - if let Some(target) = target - && MATERIALS[target.material as usize].density < material.density - { - // swap the cells - board.set_cell_at_position(x, y, target); - board.set_cell_at_position(candidate.0, candidate.1, cur); - break; - } - } - } - MaterialId::Water => 'water: { - // if the water can fall, do so - for candidate in [ - (x, y + 1), - (x - 1 + 2 * seqno_parity as i32, y + 1), - (x + 1 - 2 * seqno_parity as i32, y + 1), - ] { - let target = board.cell_at_position(candidate.0, candidate.1); - if let Some(target) = target - && MATERIALS[target.material as usize].density < material.density - { - // swap the cells - board.set_cell_at_position(x, y, target); - board.set_cell_at_position(candidate.0, candidate.1, cur); - break 'water; - } - } - // 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 = board.cell_at_position(x - 1, y); - let can_move_left = left_target.is_some_and(|c| { - MATERIALS[c.material as usize].density < material.density - }); - let right_target = board.cell_at_position(x + 1, y); - let can_move_right = right_target.is_some_and(|c| { - MATERIALS[c.material as usize].density < material.density - }); - - // we can't move down or to other side, so we're stuck - if !can_move_left && !can_move_right { - break 'water; - } - - // 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 seqno_parity == 0 { 1 } else { -1 }; - for i in 0..20 { - let side = if i % 2 == 0 { - starting_side - } else { - -starting_side - }; - if (side == 1 && !can_move_right) || (side == -1 && !can_move_left) { - continue; - } - - let offset = side * (1 + i / 2); - - let target = board.cell_at_position(x + offset, y + 1); - if let Some(target) = target - && MATERIALS[target.material as usize].density < material.density - { - // we identified a hole and we know that the space on this side is open - // move toward the hole - let mut new_target = - if side == 1 { right_target } else { left_target }.clone(); - // new_target.flags = new_target.flags ^ 0b1; - // safe to unwrap - board.set_cell_at_position(x, y, new_target.unwrap()); - board.set_cell_at_position(x + side, y, cur); - break 'water; - } - } - - // we didn't find a hole, so just move "randomly" on the same surface - // TODO when to settle? - let (target, target_x) = if !can_move_left { - (right_target, 1) - } else if !can_move_right { - (left_target, -1) - } else if seqno_parity % 2 == 1 { - (right_target, 1) - } else { - (left_target, -1) - }; - - board.set_cell_at_position(x, y, target.unwrap()); - board.set_cell_at_position(x + target_x, y, cur); - } + if let Some(update) = cur.material.def().sim_update { + update(&mut UpdateCtx { + self_x: x, + self_y: y, + self_cell: cur, + board, + delta_time, + seqno_parity, + }); } } } diff --git a/src/ui.rs b/src/ui.rs index dbdfa69..e55aff1 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,10 +1,6 @@ -use egui::{Color32, Stroke, Ui, accesskit::ListStyle::Circle, epaint::CircleShape}; +use egui::{Color32, Stroke, Ui, epaint::CircleShape}; -use crate::{ - Config, Diagnostics, Input, - camera::Camera, - sim::materials::{MATERIALS, MaterialId}, -}; +use crate::{Config, Diagnostics, Input, camera::Camera, sim::materials::MaterialId}; pub fn draw_egui<'a>( ui: &mut Ui, @@ -16,7 +12,7 @@ pub fn draw_egui<'a>( ui.heading("Config"); ui.add(egui::Slider::new(&mut config.brush_radius, 1..=100).text("Brush radius")); - let material = MATERIALS[config.brush_material as usize]; + let material = config.brush_material.def(); // material combobox egui::ComboBox::from_label("Select a material") @@ -25,19 +21,21 @@ pub fn draw_egui<'a>( material.name, material.density, )) .icon(move |ui, rect, _, _| { - ui.painter().add(egui::Shape::Circle( - (CircleShape { - center: rect.center(), - radius: rect.width() / 2.5, - stroke: Stroke::NONE, - fill: Color32::from_rgb(material.r, material.g, material.b), - }), - )); + ui.painter().add(egui::Shape::Circle(CircleShape { + center: rect.center(), + radius: rect.width() / 2.5, + stroke: Stroke::NONE, + fill: Color32::from_rgb(material.color[0], material.color[1], material.color[2]), + })); }) .show_ui(ui, |ui| { - ui.selectable_value(&mut config.brush_material, MaterialId::Void, "Void"); - ui.selectable_value(&mut config.brush_material, MaterialId::Sand, "Sand"); - ui.selectable_value(&mut config.brush_material, MaterialId::Water, "Water"); + for material_id in MaterialId::ALL { + ui.selectable_value( + &mut config.brush_material, + material_id, + material_id.def().name, + ); + } }); ui.add(egui::Slider::new(&mut config.fps, 1..=1000).text("Max FPS")); -- cgit v1.3.1