wip: current project state
This commit is contained in:
+5
-5
@@ -11,10 +11,10 @@ pub use action::AiAction;
|
||||
pub use protocol::run_pipe_protocol;
|
||||
pub use replay::{ReplayPlayer, ReplayRecorder, ReplayRecording};
|
||||
pub use scenario::{
|
||||
format_results, load_scenario, run_all_scenarios, run_scenario, Assertion, AssertionResult,
|
||||
Scenario,
|
||||
Assertion, AssertionResult, Scenario, format_results, load_scenario, run_all_scenarios,
|
||||
run_scenario,
|
||||
};
|
||||
pub use session::GameSession;
|
||||
pub use spectrum::{format_all_spectrums, render_all_spectrums, render_spectrum, Spectrum};
|
||||
pub use state::{entity_kind_name, render_view, CellInfo, EntityInfo, GameState, SubBodyInfo};
|
||||
pub use tape::{run_tape_mode, TapeFrame, TapeRecorder};
|
||||
pub use spectrum::{Spectrum, format_all_spectrums, render_all_spectrums, render_spectrum};
|
||||
pub use state::{CellInfo, EntityInfo, GameState, SubBodyInfo, entity_kind_name, render_view};
|
||||
pub use tape::{TapeFrame, TapeRecorder, run_tape_mode};
|
||||
|
||||
+5
-1
@@ -289,7 +289,11 @@ fn handle_command(cmd: Command, session: &mut Option<GameSession>) -> Response {
|
||||
"entities" => crate::ai::spectrum::Spectrum::Entities,
|
||||
"density" => crate::ai::spectrum::Spectrum::Density,
|
||||
"velocity" => crate::ai::spectrum::Spectrum::Velocity,
|
||||
_ => return Response::err("Unknown spectrum. Use: materials, temperature, light, entities, density, velocity"),
|
||||
_ => {
|
||||
return Response::err(
|
||||
"Unknown spectrum. Use: materials, temperature, light, entities, density, velocity",
|
||||
);
|
||||
}
|
||||
};
|
||||
let view = s.get_spectrum(&spec, vw, vh);
|
||||
Response {
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ use crate::ai::replay::ReplayRecorder;
|
||||
use crate::ai::state::{build_game_state, render_view, CellInfo, EntityInfo, GameState};
|
||||
use crate::game::Game;
|
||||
use crate::world::cell::MaterialId;
|
||||
use crate::world::grid::Grid;
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
|
||||
pub struct GameSession {
|
||||
pub game: Game,
|
||||
@@ -223,7 +223,7 @@ impl GameSession {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn grid(&self) -> &Grid {
|
||||
pub fn grid(&self) -> &ChunkedGrid {
|
||||
&self.game.grid
|
||||
}
|
||||
|
||||
|
||||
+10
-10
@@ -1,5 +1,5 @@
|
||||
use crate::entity::{EntityKind, EntityManager};
|
||||
use crate::world::grid::Grid;
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
|
||||
pub enum Spectrum {
|
||||
Materials,
|
||||
@@ -36,7 +36,7 @@ impl Spectrum {
|
||||
|
||||
pub fn render_spectrum(
|
||||
spectrum: &Spectrum,
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &EntityManager,
|
||||
light: Option<&crate::render::lighting::LightGrid>,
|
||||
cam_x: i32,
|
||||
@@ -55,7 +55,7 @@ pub fn render_spectrum(
|
||||
}
|
||||
|
||||
fn render_materials(
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &EntityManager,
|
||||
cam_x: i32,
|
||||
cam_y: i32,
|
||||
@@ -100,7 +100,7 @@ fn render_materials(
|
||||
buf
|
||||
}
|
||||
|
||||
fn render_temperature(grid: &Grid, cam_x: i32, cam_y: i32, vw: usize, vh: usize) -> String {
|
||||
fn render_temperature(grid: &ChunkedGrid, cam_x: i32, cam_y: i32, vw: usize, vh: usize) -> String {
|
||||
let mut buf = String::with_capacity(vw * vh + vh);
|
||||
for dy in 0..vh {
|
||||
for dx in 0..vw {
|
||||
@@ -141,7 +141,7 @@ fn render_temperature(grid: &Grid, cam_x: i32, cam_y: i32, vw: usize, vh: usize)
|
||||
}
|
||||
|
||||
fn render_light(
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &EntityManager,
|
||||
light: Option<&crate::render::lighting::LightGrid>,
|
||||
cam_x: i32,
|
||||
@@ -188,7 +188,7 @@ fn render_light(
|
||||
}
|
||||
|
||||
fn render_entities(
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &EntityManager,
|
||||
cam_x: i32,
|
||||
cam_y: i32,
|
||||
@@ -236,7 +236,7 @@ fn render_entities(
|
||||
buf
|
||||
}
|
||||
|
||||
fn render_density(grid: &Grid, cam_x: i32, cam_y: i32, vw: usize, vh: usize) -> String {
|
||||
fn render_density(grid: &ChunkedGrid, cam_x: i32, cam_y: i32, vw: usize, vh: usize) -> String {
|
||||
let reg = crate::world::material::MaterialRegistry::instance();
|
||||
let mut buf = String::with_capacity(vw * vh + vh);
|
||||
for dy in 0..vh {
|
||||
@@ -275,7 +275,7 @@ fn render_density(grid: &Grid, cam_x: i32, cam_y: i32, vw: usize, vh: usize) ->
|
||||
}
|
||||
|
||||
fn render_velocity(
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &EntityManager,
|
||||
cam_x: i32,
|
||||
cam_y: i32,
|
||||
@@ -327,7 +327,7 @@ fn render_velocity(
|
||||
}
|
||||
|
||||
pub fn render_all_spectrums(
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &EntityManager,
|
||||
light: Option<&crate::render::lighting::LightGrid>,
|
||||
cam_x: i32,
|
||||
@@ -347,7 +347,7 @@ pub fn render_all_spectrums(
|
||||
}
|
||||
|
||||
pub fn format_all_spectrums(
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &EntityManager,
|
||||
light: Option<&crate::render::lighting::LightGrid>,
|
||||
cam_x: i32,
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
use crate::entity::{EntityKind, EntityManager};
|
||||
use crate::game::Game;
|
||||
use crate::world::cell::MaterialId;
|
||||
use crate::world::grid::Grid;
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
use crate::world::material::MaterialRegistry;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -60,7 +60,7 @@ pub struct CellInfo {
|
||||
}
|
||||
|
||||
impl CellInfo {
|
||||
pub fn from_grid(grid: &Grid, x: i32, y: i32) -> Self {
|
||||
pub fn from_grid(grid: &ChunkedGrid, x: i32, y: i32) -> Self {
|
||||
if !grid.in_bounds(x, y) {
|
||||
return Self {
|
||||
x,
|
||||
@@ -157,7 +157,7 @@ pub fn build_game_state(game: &Game, view_w: usize, view_h: usize) -> GameState
|
||||
}
|
||||
|
||||
pub fn render_view(
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &EntityManager,
|
||||
cam_x: i32,
|
||||
cam_y: i32,
|
||||
|
||||
@@ -211,6 +211,24 @@ impl Item {
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemType {
|
||||
pub fn from_name(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
"Dagger" => Some(Self::Dagger),
|
||||
"Sword" => Some(Self::Sword),
|
||||
"Bow" => Some(Self::Bow),
|
||||
"Leather Armor" => Some(Self::LeatherArmor),
|
||||
"Plate Armor" => Some(Self::PlateArmor),
|
||||
"Shield" => Some(Self::Shield),
|
||||
"Health Potion" => Some(Self::HealthPotion),
|
||||
"Mana Potion" => Some(Self::ManaPotion),
|
||||
"Food" => Some(Self::Food),
|
||||
"Scroll" => Some(Self::Scroll),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ItemManager {
|
||||
items: Vec<Item>,
|
||||
next_id: u32,
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ pub mod entity;
|
||||
pub mod item;
|
||||
pub mod player;
|
||||
|
||||
pub use body_template::{template_for_kind, BodyPart, BodyTemplate};
|
||||
pub use body_template::{BodyPart, BodyTemplate, template_for_kind};
|
||||
pub use entity::{EntityKind, EntityManager};
|
||||
pub use item::{Item, ItemManager, ItemType};
|
||||
pub use player::Player;
|
||||
|
||||
@@ -31,6 +31,13 @@ impl Player {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_position(&self, manager: &mut EntityManager, cx: f32, cy: f32) {
|
||||
if let Some(e) = manager.get_mut(self.entity_id) {
|
||||
e.cx = cx;
|
||||
e.cy = cy;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_left(&mut self, manager: &mut EntityManager) {
|
||||
if let Some(e) = manager.get_mut(self.entity_id) {
|
||||
e.set_horizontal_vel(-self.move_speed);
|
||||
|
||||
+270
-226
@@ -1,7 +1,7 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::entity::player::Player;
|
||||
use crate::entity::{EntityKind, EntityManager, ItemManager, ItemType};
|
||||
use crate::entity::{EntityKind, EntityManager, ItemManager};
|
||||
use crate::input::{Action, InputHandler};
|
||||
use crate::physics::collision::resolve_grid_collision;
|
||||
use crate::physics::projectile::{ProjectileManager, ProjectileType};
|
||||
@@ -9,12 +9,15 @@ use crate::physics::verlet::VerletSolver;
|
||||
use crate::render::lighting;
|
||||
use crate::render::Renderer;
|
||||
use crate::ui::UiLayer;
|
||||
use crate::world::cache::WorldCache;
|
||||
use crate::world::cell::MaterialId;
|
||||
use crate::world::cellular::CellularAutomaton;
|
||||
use crate::world::grid::Grid;
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
use crate::world::grid::{WORLD_H, WORLD_W};
|
||||
use crate::world::worldgen::WorldGenerator;
|
||||
|
||||
pub struct Game {
|
||||
pub grid: Grid,
|
||||
pub grid: ChunkedGrid,
|
||||
pub ca: CellularAutomaton,
|
||||
pub verlet: VerletSolver,
|
||||
pub entities: EntityManager,
|
||||
@@ -43,14 +46,20 @@ pub struct Game {
|
||||
pub inventory_open: bool,
|
||||
pub inventory_mouse_x: i32,
|
||||
pub inventory_mouse_y: i32,
|
||||
pub seed: u64,
|
||||
pub cache_dir: Option<String>,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
pub fn new() -> Self {
|
||||
Self::new_with_size(WORLD_W, WORLD_H)
|
||||
}
|
||||
|
||||
fn new_with_size(width: usize, height: usize) -> Self {
|
||||
let mut entities = EntityManager::new();
|
||||
let player = Player::new(&mut entities);
|
||||
Self {
|
||||
grid: Grid::new(),
|
||||
grid: ChunkedGrid::with_size(width, height),
|
||||
ca: CellularAutomaton::new(),
|
||||
verlet: VerletSolver::new(),
|
||||
entities,
|
||||
@@ -79,184 +88,98 @@ impl Game {
|
||||
inventory_open: false,
|
||||
inventory_mouse_x: 0,
|
||||
inventory_mouse_y: 0,
|
||||
seed: 0x1234567890ABCDEF,
|
||||
cache_dir: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_random() -> Self {
|
||||
let seed = crate::world::cellular::random_seed();
|
||||
let cache_dir = Some("cache/worlds".to_string());
|
||||
let mut entities = EntityManager::new();
|
||||
let player = Player::new(&mut entities);
|
||||
let mut ca = CellularAutomaton::new();
|
||||
ca.seed(seed);
|
||||
Self {
|
||||
grid: ChunkedGrid::infinite(seed, cache_dir.clone()),
|
||||
ca,
|
||||
verlet: VerletSolver::new(),
|
||||
entities,
|
||||
projectiles: ProjectileManager::new(),
|
||||
items: ItemManager::new(),
|
||||
player,
|
||||
input: InputHandler::new(),
|
||||
ui: UiLayer::new(),
|
||||
cam_x: 0,
|
||||
cam_y: 0,
|
||||
cam_offset_x: 0,
|
||||
cam_offset_y: 0,
|
||||
running: true,
|
||||
tick: 0,
|
||||
fixed_dt: Duration::from_millis(16),
|
||||
accumulator: Duration::ZERO,
|
||||
last_time: Instant::now(),
|
||||
last_shot_tick: 0,
|
||||
shot_cooldown: 8,
|
||||
fireball_mode: false,
|
||||
corpse_decomp_timer: 0,
|
||||
kills: 0,
|
||||
score: 0,
|
||||
depth: 1,
|
||||
fps: 0.0,
|
||||
inventory_open: false,
|
||||
inventory_mouse_x: 0,
|
||||
inventory_mouse_y: 0,
|
||||
seed,
|
||||
cache_dir,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_world(&mut self) {
|
||||
let w = self.grid.width;
|
||||
let h = self.grid.height;
|
||||
|
||||
for x in 0..w {
|
||||
self.grid
|
||||
.set_material(x as i32, (h - 1) as i32, MaterialId::Stone);
|
||||
self.grid
|
||||
.set_material(x as i32, (h - 2) as i32, MaterialId::Dirt);
|
||||
}
|
||||
|
||||
let surface_noise = |x: i32| -> i32 {
|
||||
let base = (h as i32 - 3) - ((x as f32 * 0.08).sin() * 4.0) as i32;
|
||||
let detail = ((x as f32 * 0.23).sin() * 2.0) as i32;
|
||||
(base + detail).max(10).min(h as i32 - 3)
|
||||
};
|
||||
|
||||
for x in 0..w {
|
||||
let surface = surface_noise(x as i32);
|
||||
let biome = x / (w / 4);
|
||||
|
||||
for y in surface..(h as i32 - 2) {
|
||||
if y == surface {
|
||||
let mat = match biome {
|
||||
0 => MaterialId::Grass,
|
||||
1 => MaterialId::Grass,
|
||||
2 => MaterialId::Dirt,
|
||||
_ => MaterialId::Stone,
|
||||
};
|
||||
self.grid.set_material(x as i32, y, mat);
|
||||
} else if y > surface + 8 {
|
||||
self.grid.set_material(x as i32, y, MaterialId::Stone);
|
||||
let cache_dir = self.cache_dir.clone();
|
||||
let (px, py) = if let Some(ref root) = cache_dir {
|
||||
let has_meta = WorldCache::meta_exists(root, self.seed);
|
||||
if has_meta {
|
||||
if let Err(e) = WorldCache::load_meta(
|
||||
root,
|
||||
self.seed,
|
||||
&mut self.player,
|
||||
&mut self.entities,
|
||||
&mut self.items,
|
||||
) {
|
||||
eprintln!("World cache meta load failed: {}", e);
|
||||
} else {
|
||||
self.grid.set_material(x as i32, y, MaterialId::Dirt);
|
||||
let (px, py) = self.player.center(&self.entities);
|
||||
self.grid.ensure_loaded(px as i32, py as i32, 3);
|
||||
self.center_camera_on(px, py);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _ in 0..8 {
|
||||
let cave_x = (self.ca.random_u32() % (w as u32 - 20) + 10) as i32;
|
||||
let cave_y = (self.ca.random_u32() % (h as u32 / 3) + (h as u32 / 3) * 2) as i32;
|
||||
let cave_r = (self.ca.random_u32() % 4 + 3) as i32;
|
||||
for dy in -cave_r..=cave_r {
|
||||
for dx in -cave_r..=cave_r {
|
||||
if dx * dx + dy * dy <= cave_r * cave_r {
|
||||
let cx = cave_x + dx;
|
||||
let cy = cave_y + dy;
|
||||
if cx > 1 && cx < w as i32 - 2 && cy > 1 && cy < h as i32 - 2 {
|
||||
self.grid.set(cx, cy, crate::world::cell::Cell::empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for tree_x in [60, 75, 130, 145, 220] {
|
||||
let s = surface_noise(tree_x);
|
||||
for y in (s - 6)..s {
|
||||
if y > 5 {
|
||||
self.grid.set_material(tree_x, y, MaterialId::Wood);
|
||||
}
|
||||
}
|
||||
for dy in -2..=0 {
|
||||
for dx in -2..=2 {
|
||||
if dx * dx + dy * dy <= 5 {
|
||||
let cx = tree_x + dx;
|
||||
let cy = s - 6 + dy;
|
||||
if cx > 1 && cx < w as i32 - 2 && cy > 1 {
|
||||
if self.grid.get(cx, cy).is_empty() {
|
||||
self.grid.set_material(cx, cy, MaterialId::Grass);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let water_x = 40;
|
||||
for x in water_x - 10..=water_x + 10 {
|
||||
let s = surface_noise(x);
|
||||
for y in s - 6..s {
|
||||
if self.grid.get(x, y).is_empty() {
|
||||
self.grid.set_material(x, y, MaterialId::Water);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let lava_x = 200;
|
||||
for x in lava_x - 8..=lava_x + 8 {
|
||||
let s = surface_noise(x);
|
||||
for y in s - 4..s {
|
||||
if self.grid.get(x, y).is_empty() {
|
||||
self.grid.set_material(x, y, MaterialId::Lava);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sand_x = 160;
|
||||
for dx in -10..=10 {
|
||||
let s = surface_noise(sand_x + dx);
|
||||
let pile_h = (10.0 - (dx as f32).abs() * 0.8) as i32;
|
||||
for dy in 0..pile_h {
|
||||
let y = s - 1 - dy;
|
||||
if y > 5 && self.grid.get(sand_x + dx, y).is_empty() {
|
||||
self.grid.set_material(sand_x + dx, y, MaterialId::Sand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let acid_x = 20;
|
||||
for x in acid_x - 4..=acid_x + 4 {
|
||||
let s = surface_noise(x);
|
||||
for y in s - 3..s {
|
||||
if self.grid.get(x, y).is_empty() {
|
||||
self.grid.set_material(x, y, MaterialId::Acid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let wall_x = 110;
|
||||
let wall_s = surface_noise(wall_x);
|
||||
for y in (wall_s - 5)..wall_s {
|
||||
self.grid.set_material(wall_x, y, MaterialId::Stone);
|
||||
self.grid.set_material(wall_x + 1, y, MaterialId::Stone);
|
||||
}
|
||||
|
||||
for _ in 0..6 {
|
||||
let px = (self.ca.random_u32() % (w as u32 - 20) + 10) as i32;
|
||||
let py = (self.ca.random_u32() % (h as u32 / 3) + (h as u32 / 3) * 2) as i32;
|
||||
for dy in 0..8 {
|
||||
for dx in -1..=1 {
|
||||
let cx = px + dx;
|
||||
let cy = py + dy;
|
||||
if cx > 1 && cx < w as i32 - 2 && cy < h as i32 - 3 {
|
||||
self.grid.set_material(cx, cy, MaterialId::Stone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.grid.fill_border(MaterialId::Stone);
|
||||
|
||||
let cx = (w / 2) as f32;
|
||||
let surface_x = cx as i32;
|
||||
let mut surface_y = h as i32 - 3;
|
||||
for y in 0..h as i32 {
|
||||
if self.grid.get(surface_x, y).is_solid()
|
||||
&& self.grid.get(surface_x, y).material != MaterialId::Stone
|
||||
let (px, py) = WorldGenerator::new(&mut self.ca).generate(
|
||||
&mut self.grid,
|
||||
&mut self.items,
|
||||
&mut self.player,
|
||||
&mut self.entities,
|
||||
self.depth,
|
||||
);
|
||||
self.center_camera_on(px, py);
|
||||
if let Err(e) = WorldCache::save_meta(root, self.seed, px, py, self.depth, &self.items)
|
||||
{
|
||||
surface_y = y;
|
||||
break;
|
||||
eprintln!("World cache meta save failed: {}", e);
|
||||
}
|
||||
}
|
||||
let stair_y = (h as i32 - 2).max(surface_y + 2);
|
||||
self.grid
|
||||
.set_material(surface_x, stair_y, MaterialId::Stairs);
|
||||
|
||||
let cy = (surface_y as f32) - 5.0;
|
||||
self.player.spawn_at(&mut self.entities, cx, cy);
|
||||
|
||||
let (px, py) = self.player.center(&self.entities);
|
||||
self.center_camera_on(px, py);
|
||||
|
||||
self.items
|
||||
.spawn(ItemType::Sword, px as i32 - 6, py as i32 + 1);
|
||||
self.items
|
||||
.spawn(ItemType::HealthPotion, px as i32 + 6, py as i32 + 1);
|
||||
self.items
|
||||
.spawn(ItemType::LeatherArmor, px as i32 - 3, py as i32 - 8);
|
||||
self.items
|
||||
.spawn(ItemType::Bow, px as i32 + 10, py as i32 + 1);
|
||||
self.items
|
||||
.spawn(ItemType::Shield, px as i32 - 10, py as i32 + 1);
|
||||
self.items
|
||||
.spawn(ItemType::ManaPotion, px as i32 + 3, py as i32 - 6);
|
||||
(px, py)
|
||||
} else {
|
||||
let (px, py) = WorldGenerator::new(&mut self.ca).generate(
|
||||
&mut self.grid,
|
||||
&mut self.items,
|
||||
&mut self.player,
|
||||
&mut self.entities,
|
||||
self.depth,
|
||||
);
|
||||
self.center_camera_on(px, py);
|
||||
(px, py)
|
||||
};
|
||||
let _ = (px, py);
|
||||
}
|
||||
|
||||
pub fn center_camera_on(&mut self, px: f32, py: f32) {
|
||||
@@ -525,6 +448,7 @@ impl Game {
|
||||
pub fn fixed_update(&mut self) {
|
||||
self.tick += 1;
|
||||
|
||||
self.stream_chunks();
|
||||
self.update_active_chunks();
|
||||
self.ca.step(&mut self.grid);
|
||||
|
||||
@@ -604,7 +528,7 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
self.depth += 1;
|
||||
self.grid = Grid::new();
|
||||
self.grid = ChunkedGrid::with_size(self.grid.width, self.grid.height);
|
||||
self.entities = EntityManager::new();
|
||||
self.player = Player::new(&mut self.entities);
|
||||
self.projectiles = ProjectileManager::new();
|
||||
@@ -655,12 +579,80 @@ impl Game {
|
||||
self.ui.add_message(&format!("Dropped {}", item.name()));
|
||||
}
|
||||
|
||||
fn stream_chunks(&mut self) {
|
||||
if !self.grid.is_infinite() && self.grid.width <= 2048 {
|
||||
return;
|
||||
}
|
||||
let (px, py) = self.player.center(&self.entities);
|
||||
let px = px as i32;
|
||||
let py = py as i32;
|
||||
let (pcx, pcy, _, _) = self.grid.chunk_at(px, py);
|
||||
let radius = 3;
|
||||
let chunk_size = self.grid.chunk_size as i32;
|
||||
|
||||
for dy in -radius..=radius {
|
||||
for dx in -radius..=radius {
|
||||
let cx = pcx + dx;
|
||||
let cy = pcy + dy;
|
||||
let ox = cx * chunk_size;
|
||||
let oy = cy * chunk_size;
|
||||
if !self.grid.in_bounds(ox, oy) {
|
||||
continue;
|
||||
}
|
||||
self.grid.ensure_chunk(cx, cy);
|
||||
if !self.grid.is_chunk_generated(cx, cy) {
|
||||
WorldGenerator::new(&mut self.ca).generate_chunk(&mut self.grid, cx, cy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref dir) = self.cache_dir {
|
||||
if self.tick % 60 == 0 {
|
||||
let save_radius = radius + 2;
|
||||
for (cx, cy) in self.grid.all_chunk_coords() {
|
||||
let dx = (cx - pcx).abs();
|
||||
let dy = (cy - pcy).abs();
|
||||
if dx > save_radius || dy > save_radius {
|
||||
if self.grid.is_chunk_modified(cx, cy) {
|
||||
let path =
|
||||
crate::world::chunked_grid::chunk_path(dir, self.seed, cx, cy);
|
||||
let _ = self.grid.save_chunk(path.to_str().unwrap(), cx, cy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.tick % 60 == 0 {
|
||||
let unload_radius = radius + 4;
|
||||
let to_unload: Vec<(i32, i32)> = self
|
||||
.grid
|
||||
.all_chunk_coords()
|
||||
.into_iter()
|
||||
.filter(|(cx, cy)| {
|
||||
let dx = (cx - pcx).abs();
|
||||
let dy = (cy - pcy).abs();
|
||||
dx > unload_radius || dy > unload_radius
|
||||
})
|
||||
.collect();
|
||||
for (cx, cy) in to_unload {
|
||||
if self.grid.is_chunk_modified(cx, cy) {
|
||||
if let Some(ref dir) = self.cache_dir {
|
||||
let path = crate::world::chunked_grid::chunk_path(dir, self.seed, cx, cy);
|
||||
let _ = self.grid.save_chunk(path.to_str().unwrap(), cx, cy);
|
||||
}
|
||||
}
|
||||
self.grid.unload_chunk(cx, cy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_active_chunks(&mut self) {
|
||||
self.grid.deactivate_all();
|
||||
|
||||
for e in self.entities.all() {
|
||||
let (cx, cy) = e.center();
|
||||
self.grid.activate_around(cx as i32, cy as i32, 2);
|
||||
self.grid.activate_around(cx as i32, cy as i32, 1);
|
||||
}
|
||||
|
||||
for p in self.projectiles.all() {
|
||||
@@ -672,14 +664,23 @@ impl Game {
|
||||
}
|
||||
|
||||
let chunk_size = self.grid.chunk_size as i32;
|
||||
for cy in 0..self.grid.chunks_y as i32 {
|
||||
for cx in 0..self.grid.chunks_x as i32 {
|
||||
let idx = self.grid.chunk_index(cx, cy);
|
||||
if self.grid.chunks[idx].modified || self.grid.chunks[idx].was_modified {
|
||||
let (px, py) = self.player.center(&self.entities);
|
||||
let pcx = px as i32 / chunk_size;
|
||||
let pcy = py as i32 / chunk_size;
|
||||
let dirty_radius = if self.grid.is_infinite() { 1 } else { 100000 };
|
||||
|
||||
for (cx, cy) in self.grid.all_chunk_coords() {
|
||||
if self.grid.is_chunk_modified(cx, cy) {
|
||||
if (cx - pcx).abs() <= dirty_radius && (cy - pcy).abs() <= dirty_radius {
|
||||
self.grid
|
||||
.activate_around(cx * chunk_size, cy * chunk_size, 1);
|
||||
}
|
||||
}
|
||||
if self.grid.get_chunk_dirty(cx, cy).is_some() {
|
||||
if (cx - pcx).abs() <= dirty_radius && (cy - pcy).abs() <= dirty_radius {
|
||||
self.grid.set_chunk_active(cx, cy, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1386,6 +1387,69 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
fn find_spawn_location(&self, near_x: i32, near_y: i32, radius: i32) -> Option<(i32, i32)> {
|
||||
for r in 0..=radius {
|
||||
for dy in -r..=r {
|
||||
for dx in -r..=r {
|
||||
if dx.abs() + dy.abs() != r {
|
||||
continue;
|
||||
}
|
||||
let x = near_x + dx;
|
||||
let y = near_y + dy;
|
||||
if !self.grid.in_bounds(x, y) || !self.grid.in_bounds(x, y - 3) {
|
||||
continue;
|
||||
}
|
||||
if !self.grid.get(x, y).is_empty() || !self.grid.get(x, y + 1).is_solid() {
|
||||
continue;
|
||||
}
|
||||
let mut clear = true;
|
||||
for k in -3..=0 {
|
||||
if !self.grid.get(x, y + k).is_empty() {
|
||||
clear = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if clear {
|
||||
return Some((x, y - 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_surface_spawn(
|
||||
&self,
|
||||
px: f32,
|
||||
py: f32,
|
||||
offset: i32,
|
||||
height_offset: i32,
|
||||
) -> Option<(i32, i32)> {
|
||||
let spawn_x = px as i32 + offset;
|
||||
if !self.grid.in_bounds(spawn_x, 0) {
|
||||
return None;
|
||||
}
|
||||
let search_top = 0;
|
||||
let search_bottom = if self.grid.is_infinite() {
|
||||
py as i32 + 50
|
||||
} else {
|
||||
self.grid.height as i32 - 3
|
||||
};
|
||||
let mut surface_y = search_bottom;
|
||||
for y in search_top..=search_bottom {
|
||||
let cell = self.grid.get(spawn_x, y);
|
||||
if cell.is_solid() && cell.material != MaterialId::Stone {
|
||||
surface_y = y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let spawn_y = surface_y - height_offset;
|
||||
if !self.grid.in_bounds(spawn_x, spawn_y) {
|
||||
return None;
|
||||
}
|
||||
Some((spawn_x, spawn_y))
|
||||
}
|
||||
|
||||
fn try_spawn_goblin(&mut self) {
|
||||
let max_goblins = 3usize + self.depth.min(5) as usize;
|
||||
let alive_goblins = self
|
||||
@@ -1398,33 +1462,23 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
|
||||
let (px, _py) = self.player.center(&self.entities);
|
||||
let spawn_x = px as i32 + if px as i32 % 2 == 0 { 15 } else { -15 };
|
||||
if !self.grid.in_bounds(spawn_x, 0) {
|
||||
return;
|
||||
}
|
||||
let (px, py) = self.player.center(&self.entities);
|
||||
let offset = if px as i32 % 2 == 0 { 15 } else { -15 };
|
||||
let spawn = if self.depth <= 3 {
|
||||
self.find_surface_spawn(px, py, offset, 5)
|
||||
} else {
|
||||
self.find_spawn_location(px as i32 + offset, py as i32, 3)
|
||||
};
|
||||
|
||||
let mut surface_y = self.grid.height as i32 - 3;
|
||||
for y in 0..self.grid.height as i32 {
|
||||
let cell = self.grid.get(spawn_x, y);
|
||||
if cell.is_solid() && cell.material != MaterialId::Stone {
|
||||
surface_y = y;
|
||||
break;
|
||||
if let Some((spawn_x, spawn_y)) = spawn {
|
||||
let id = self.entities.spawn(EntityKind::Goblin);
|
||||
if let Some(g) = self.entities.get_mut(id) {
|
||||
g.build_humanoid(spawn_x as f32, spawn_y as f32);
|
||||
g.health += self.depth as f32 * 5.0;
|
||||
g.max_health += self.depth as f32 * 5.0;
|
||||
g.strength += self.depth;
|
||||
}
|
||||
}
|
||||
let spawn_y = surface_y - 5;
|
||||
|
||||
if !self.grid.in_bounds(spawn_x, spawn_y) {
|
||||
return;
|
||||
}
|
||||
|
||||
let id = self.entities.spawn(EntityKind::Goblin);
|
||||
if let Some(g) = self.entities.get_mut(id) {
|
||||
g.build_humanoid(spawn_x as f32, spawn_y as f32);
|
||||
g.health += self.depth as f32 * 5.0;
|
||||
g.max_health += self.depth as f32 * 5.0;
|
||||
g.strength += self.depth;
|
||||
}
|
||||
}
|
||||
|
||||
fn try_spawn_slime(&mut self) {
|
||||
@@ -1439,31 +1493,21 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
|
||||
let (px, _py) = self.player.center(&self.entities);
|
||||
let spawn_x = px as i32 + if px as i32 % 2 == 0 { -18 } else { 18 };
|
||||
if !self.grid.in_bounds(spawn_x, 0) {
|
||||
return;
|
||||
}
|
||||
let (px, py) = self.player.center(&self.entities);
|
||||
let offset = if px as i32 % 2 == 0 { -18 } else { 18 };
|
||||
let spawn = if self.depth <= 3 {
|
||||
self.find_surface_spawn(px, py, offset, 3)
|
||||
} else {
|
||||
self.find_spawn_location(px as i32 + offset, py as i32, 3)
|
||||
};
|
||||
|
||||
let mut surface_y = self.grid.height as i32 - 3;
|
||||
for y in 0..self.grid.height as i32 {
|
||||
let cell = self.grid.get(spawn_x, y);
|
||||
if cell.is_solid() && cell.material != MaterialId::Stone {
|
||||
surface_y = y;
|
||||
break;
|
||||
if let Some((spawn_x, spawn_y)) = spawn {
|
||||
let id = self.entities.spawn(EntityKind::Slime);
|
||||
if let Some(s) = self.entities.get_mut(id) {
|
||||
s.build_humanoid(spawn_x as f32, spawn_y as f32);
|
||||
s.health += self.depth as f32 * 3.0;
|
||||
s.max_health += self.depth as f32 * 3.0;
|
||||
}
|
||||
}
|
||||
let spawn_y = surface_y - 3;
|
||||
|
||||
if !self.grid.in_bounds(spawn_x, spawn_y) {
|
||||
return;
|
||||
}
|
||||
|
||||
let id = self.entities.spawn(EntityKind::Slime);
|
||||
if let Some(s) = self.entities.get_mut(id) {
|
||||
s.build_humanoid(spawn_x as f32, spawn_y as f32);
|
||||
s.health += self.depth as f32 * 3.0;
|
||||
s.max_health += self.depth as f32 * 3.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-6
@@ -82,14 +82,16 @@ impl InputHandler {
|
||||
pub fn start(&mut self) {
|
||||
let (tx, rx) = mpsc::channel::<Event>();
|
||||
self.receiver = Some(rx);
|
||||
self.input_thread = Some(thread::spawn(move || loop {
|
||||
match event::read() {
|
||||
Ok(ev) => {
|
||||
if tx.send(ev).is_err() {
|
||||
break;
|
||||
self.input_thread = Some(thread::spawn(move || {
|
||||
loop {
|
||||
match event::read() {
|
||||
Ok(ev) => {
|
||||
if tx.send(ev).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
+44
-13
@@ -8,6 +8,7 @@ use verbatim::render::lighting;
|
||||
use verbatim::render::terminal::TerminalRenderer;
|
||||
use verbatim::render::window_input::WindowInput;
|
||||
use verbatim::world::cell::MaterialId;
|
||||
use verbatim::world::chunked_grid::ChunkedGrid;
|
||||
use winit::event::{Event, WindowEvent};
|
||||
use winit::event_loop::{ControlFlow, EventLoop};
|
||||
use winit::window::Window;
|
||||
@@ -42,6 +43,9 @@ struct Cli {
|
||||
#[arg(long, default_value = "benchmark_results.json")]
|
||||
benchmark_output: String,
|
||||
|
||||
#[arg(long, default_value = "surface")]
|
||||
benchmark_biome: String,
|
||||
|
||||
#[arg(long, default_value_t = 5)]
|
||||
tape_interval: u32,
|
||||
|
||||
@@ -58,7 +62,7 @@ trait GpuRenderer {
|
||||
Self: Sized;
|
||||
fn render(
|
||||
&mut self,
|
||||
grid: &verbatim::world::grid::Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &verbatim::entity::EntityManager,
|
||||
items: &verbatim::entity::item::ItemManager,
|
||||
ui: &verbatim::ui::UiLayer,
|
||||
@@ -76,7 +80,7 @@ impl GpuRenderer for verbatim::render::vulkan::VulkanRenderer {
|
||||
}
|
||||
fn render(
|
||||
&mut self,
|
||||
grid: &verbatim::world::grid::Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &verbatim::entity::EntityManager,
|
||||
items: &verbatim::entity::item::ItemManager,
|
||||
ui: &verbatim::ui::UiLayer,
|
||||
@@ -102,7 +106,7 @@ impl GpuRenderer for verbatim::render::graphics::GraphicsRenderer {
|
||||
}
|
||||
fn render(
|
||||
&mut self,
|
||||
grid: &verbatim::world::grid::Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &verbatim::entity::EntityManager,
|
||||
items: &verbatim::entity::item::ItemManager,
|
||||
ui: &verbatim::ui::UiLayer,
|
||||
@@ -139,7 +143,7 @@ fn main() {
|
||||
eprintln!("PANIC: {}", info);
|
||||
}));
|
||||
let mut renderer = TerminalRenderer::new();
|
||||
let mut game = Game::new();
|
||||
let mut game = Game::new_random();
|
||||
game.run(&mut renderer);
|
||||
}
|
||||
"ascii" => {
|
||||
@@ -216,13 +220,13 @@ fn run_gpu_mode<R: GpuRenderer>(title: &str) {
|
||||
eprintln!("Vulkan init failed: {e}");
|
||||
eprintln!("Falling back to terminal mode...");
|
||||
let mut renderer = TerminalRenderer::new();
|
||||
let mut game = Game::new();
|
||||
let mut game = Game::new_random();
|
||||
game.run(&mut renderer);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut game = Game::new();
|
||||
let mut game = Game::new_random();
|
||||
game.init_world();
|
||||
|
||||
let mut input = WindowInput::new();
|
||||
@@ -533,19 +537,25 @@ fn run_benchmark_mode(cli: &Cli) {
|
||||
let ticks = cli.benchmark_ticks;
|
||||
let renderer_type = cli.benchmark_renderer.as_str();
|
||||
let output_path = cli.benchmark_output.as_str();
|
||||
let biome = cli.benchmark_biome.as_str();
|
||||
|
||||
eprintln!("Benchmark: {} ticks, renderer={}", ticks, renderer_type);
|
||||
eprintln!(
|
||||
"Benchmark: {} ticks, renderer={}, biome={}",
|
||||
ticks, renderer_type, biome
|
||||
);
|
||||
|
||||
match renderer_type {
|
||||
"ascii" => run_benchmark_inner::<verbatim::render::vulkan::VulkanRenderer>(
|
||||
ticks,
|
||||
output_path,
|
||||
"ascii",
|
||||
biome,
|
||||
),
|
||||
"graphics" => run_benchmark_inner::<verbatim::render::graphics::GraphicsRenderer>(
|
||||
ticks,
|
||||
output_path,
|
||||
"graphics",
|
||||
biome,
|
||||
),
|
||||
_ => {
|
||||
eprintln!(
|
||||
@@ -557,7 +567,12 @@ fn run_benchmark_mode(cli: &Cli) {
|
||||
}
|
||||
}
|
||||
|
||||
fn run_benchmark_inner<R: GpuRenderer>(ticks: u32, output_path: &str, mode_name: &str) {
|
||||
fn run_benchmark_inner<R: GpuRenderer>(
|
||||
ticks: u32,
|
||||
output_path: &str,
|
||||
mode_name: &str,
|
||||
biome: &str,
|
||||
) {
|
||||
let event_loop = EventLoop::new().expect("Failed to create event loop");
|
||||
let window = event_loop
|
||||
.create_window(
|
||||
@@ -576,9 +591,25 @@ fn run_benchmark_inner<R: GpuRenderer>(ticks: u32, output_path: &str, mode_name:
|
||||
}
|
||||
};
|
||||
|
||||
let mut game = Game::new();
|
||||
let mut game = Game::new_random();
|
||||
game.init_world();
|
||||
|
||||
let chunk_size = game.grid.chunk_size as i32;
|
||||
let (px, _py) = game.player.center(&game.entities);
|
||||
match biome {
|
||||
"caves" => {
|
||||
let cave_y = 2 * chunk_size + chunk_size / 2;
|
||||
game.player
|
||||
.set_position(&mut game.entities, px, cave_y as f32);
|
||||
}
|
||||
"dungeon" => {
|
||||
let dungeon_y = 6 * chunk_size + chunk_size / 2;
|
||||
game.player
|
||||
.set_position(&mut game.entities, px, dungeon_y as f32);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut tick_count = 0u32;
|
||||
let mut ca_times_us: Vec<u64> = Vec::with_capacity(ticks as usize);
|
||||
let mut render_times_us: Vec<u64> = Vec::with_capacity(ticks as usize);
|
||||
@@ -908,14 +939,14 @@ fn run_capture(ticks: u32) {
|
||||
}
|
||||
|
||||
fn dump_view(
|
||||
grid: &verbatim::world::grid::Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &verbatim::entity::EntityManager,
|
||||
cam_x: i32,
|
||||
cam_y: i32,
|
||||
vw: usize,
|
||||
vh: usize,
|
||||
w: usize,
|
||||
h: usize,
|
||||
) -> String {
|
||||
ai::render_view(grid, entities, cam_x, cam_y, vw, vh)
|
||||
ai::render_view(grid, entities, cam_x, cam_y, w, h)
|
||||
.lines()
|
||||
.enumerate()
|
||||
.map(|(i, line)| format!("{:2}{}", (cam_y + i as i32) % 100, line))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::world::cell::MaterialId;
|
||||
use crate::world::grid::Grid;
|
||||
use crate::physics::verlet::SubBody;
|
||||
use crate::world::cell::MaterialId;
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
|
||||
pub struct CollisionResult {
|
||||
pub on_ground: bool,
|
||||
@@ -24,7 +24,7 @@ impl CollisionResult {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_grid_collision(grid: &Grid, body: &mut SubBody) -> CollisionResult {
|
||||
pub fn resolve_grid_collision(grid: &ChunkedGrid, body: &mut SubBody) -> CollisionResult {
|
||||
let mut result = CollisionResult::none();
|
||||
|
||||
let r = body.radius;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::entity::entity::{Entity, EntityId};
|
||||
use crate::world::cell::{Cell, MaterialId};
|
||||
use crate::world::grid::Grid;
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ProjectileType {
|
||||
@@ -65,7 +65,7 @@ impl Projectile {
|
||||
self.damage + self.damage_bonus
|
||||
}
|
||||
|
||||
pub fn update(&mut self, grid: &Grid) {
|
||||
pub fn update(&mut self, grid: &ChunkedGrid) {
|
||||
if !self.alive {
|
||||
return;
|
||||
}
|
||||
@@ -118,7 +118,12 @@ impl Projectile {
|
||||
dx.abs() < hit_w && dy.abs() < hit_h
|
||||
}
|
||||
|
||||
pub fn apply_impact(&self, grid: &mut Grid, entity: &mut Entity, ui: &mut crate::ui::UiLayer) {
|
||||
pub fn apply_impact(
|
||||
&self,
|
||||
grid: &mut ChunkedGrid,
|
||||
entity: &mut Entity,
|
||||
ui: &mut crate::ui::UiLayer,
|
||||
) {
|
||||
if self.typ == ProjectileType::Fireball {
|
||||
let min_x = (self.x - 1.5).floor() as i32;
|
||||
let max_x = (self.x + 1.5).ceil() as i32;
|
||||
@@ -226,7 +231,7 @@ impl ProjectileManager {
|
||||
self.spawn(ProjectileType::Arrow, x, y, vx, vy, owner, 0.0)
|
||||
}
|
||||
|
||||
pub fn update(&mut self, grid: &Grid) {
|
||||
pub fn update(&mut self, grid: &ChunkedGrid) {
|
||||
for p in &mut self.projectiles {
|
||||
p.update(grid);
|
||||
}
|
||||
@@ -234,7 +239,7 @@ impl ProjectileManager {
|
||||
|
||||
pub fn resolve_hits(
|
||||
&mut self,
|
||||
grid: &mut Grid,
|
||||
grid: &mut ChunkedGrid,
|
||||
entities: &mut [Entity],
|
||||
ui: &mut crate::ui::UiLayer,
|
||||
) {
|
||||
|
||||
@@ -3,14 +3,14 @@ use crate::entity::EntityManager;
|
||||
use crate::render::lighting;
|
||||
use crate::ui::UiLayer;
|
||||
use crate::world::cell::MaterialId;
|
||||
use crate::world::grid::Grid;
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
use image::{ImageBuffer, RgbImage};
|
||||
|
||||
pub const CELL_SIZE: u32 = 8;
|
||||
pub const UI_CELL_SIZE: u32 = 2;
|
||||
|
||||
pub fn capture_frame(
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &EntityManager,
|
||||
items: &ItemManager,
|
||||
ui: &UiLayer,
|
||||
@@ -145,7 +145,7 @@ fn entity_positions(
|
||||
|
||||
fn shadow_positions(
|
||||
entity_positions: &std::collections::HashMap<(i32, i32), [u8; 3]>,
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
cam_x: i32,
|
||||
cam_y: i32,
|
||||
view_w: u32,
|
||||
@@ -203,7 +203,7 @@ fn draw_cell(img: &mut RgbImage, vx: u32, vy: u32, color: [u8; 3]) {
|
||||
|
||||
pub fn save_capture(
|
||||
path: &str,
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &EntityManager,
|
||||
items: &ItemManager,
|
||||
ui: &UiLayer,
|
||||
@@ -221,7 +221,7 @@ pub fn save_capture(
|
||||
}
|
||||
|
||||
pub fn capture_from_state(
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &EntityManager,
|
||||
items: &ItemManager,
|
||||
ui: &UiLayer,
|
||||
|
||||
+15
-17
@@ -5,7 +5,8 @@ use std::sync::Arc;
|
||||
use crate::entity::EntityManager;
|
||||
use crate::render::lighting;
|
||||
use crate::world::cell::MaterialId;
|
||||
use crate::world::grid::{Grid, WORLD_H, WORLD_W};
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
use crate::world::grid::{MAX_WORLD_H, MAX_WORLD_W};
|
||||
|
||||
const CHAR_W: u32 = 8;
|
||||
const CHAR_H: u32 = 8;
|
||||
@@ -552,13 +553,14 @@ impl GraphicsRenderer {
|
||||
Ok((buf, mem))
|
||||
};
|
||||
|
||||
let grid_data = vec![0u32; WORLD_W * WORLD_H];
|
||||
let grid_count = MAX_WORLD_W * MAX_WORLD_H;
|
||||
let grid_data = vec![0u32; grid_count];
|
||||
let (grid_buffer, grid_memory) = make_buf(
|
||||
bytemuck::cast_slice(&grid_data),
|
||||
vk::BufferUsageFlags::STORAGE_BUFFER,
|
||||
)?;
|
||||
let grid_ptr = unsafe {
|
||||
let sz = (WORLD_W * WORLD_H * std::mem::size_of::<u32>()) as vk::DeviceSize;
|
||||
let sz = (grid_count * std::mem::size_of::<u32>()) as vk::DeviceSize;
|
||||
let ptr = device
|
||||
.map_memory(grid_memory, 0, sz, vk::MemoryMapFlags::default())
|
||||
.map_err(|e| format!("map grid: {e:?}"))?;
|
||||
@@ -602,7 +604,7 @@ impl GraphicsRenderer {
|
||||
let grid_info = vk::DescriptorBufferInfo::default()
|
||||
.buffer(grid_buffer)
|
||||
.offset(0)
|
||||
.range((WORLD_W * WORLD_H * std::mem::size_of::<u32>()) as vk::DeviceSize);
|
||||
.range((MAX_WORLD_W * MAX_WORLD_H * std::mem::size_of::<u32>()) as vk::DeviceSize);
|
||||
let light_info = vk::DescriptorBufferInfo::default()
|
||||
.buffer(light_buffer)
|
||||
.offset(0)
|
||||
@@ -764,7 +766,7 @@ impl GraphicsRenderer {
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &EntityManager,
|
||||
items: &crate::entity::item::ItemManager,
|
||||
ui: &crate::ui::UiLayer,
|
||||
@@ -921,16 +923,12 @@ impl GraphicsRenderer {
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let margin = 30i32;
|
||||
let x_min = (cam_x - margin).max(0) as usize;
|
||||
let x_max = (cam_x + self.grid_w as i32 + margin).min(WORLD_W as i32) as usize;
|
||||
let y_min = (cam_y - margin).max(0) as usize;
|
||||
let y_max = (cam_y + self.grid_h as i32 + margin).min(WORLD_H as i32) as usize;
|
||||
for y in y_min..y_max {
|
||||
let row_offset = y * WORLD_W;
|
||||
for x in x_min..x_max {
|
||||
let i = row_offset + x;
|
||||
*self.grid_ptr.add(i) = grid.cells[i].material as u32;
|
||||
for dy in 0..self.grid_h {
|
||||
for dx in 0..self.grid_w {
|
||||
let wx = cam_x + dx as i32;
|
||||
let wy = cam_y + dy as i32;
|
||||
let idx = dy * self.grid_w + dx;
|
||||
*self.grid_ptr.add(idx) = grid.get(wx, wy).material as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1033,7 +1031,7 @@ impl GraphicsRenderer {
|
||||
self.swapchain_extent.height as f32,
|
||||
],
|
||||
cell_size: [CHAR_W as f32, CHAR_H as f32],
|
||||
world_size: [WORLD_W as i32, WORLD_H as i32],
|
||||
world_size: [self.grid_w as i32, self.grid_h as i32],
|
||||
cam_pos: [cam_x, cam_y],
|
||||
ambient,
|
||||
is_ui: 0,
|
||||
@@ -1062,7 +1060,7 @@ impl GraphicsRenderer {
|
||||
self.swapchain_extent.height as f32,
|
||||
],
|
||||
cell_size: [UI_CELL_SIZE as f32, UI_CELL_SIZE as f32],
|
||||
world_size: [WORLD_W as i32, WORLD_H as i32],
|
||||
world_size: [self.grid_w as i32, self.grid_h as i32],
|
||||
cam_pos: [0, 0],
|
||||
ambient: [1.0, 1.0, 1.0],
|
||||
is_ui: 1,
|
||||
|
||||
+19
-11
@@ -1,5 +1,5 @@
|
||||
use crate::world::cell::MaterialId;
|
||||
use crate::world::grid::Grid;
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct LightSource {
|
||||
@@ -67,7 +67,7 @@ pub fn material_light(material: MaterialId) -> Option<LightSource> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gather_sources(grid: &Grid) -> Vec<LightSource> {
|
||||
pub fn gather_sources(grid: &ChunkedGrid) -> Vec<LightSource> {
|
||||
let mut sources = Vec::new();
|
||||
let w = grid.width;
|
||||
let h = grid.height;
|
||||
@@ -85,7 +85,7 @@ pub fn gather_sources(grid: &Grid) -> Vec<LightSource> {
|
||||
}
|
||||
|
||||
pub fn gather_sources_in_range(
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
cam_x: i32,
|
||||
cam_y: i32,
|
||||
view_w: usize,
|
||||
@@ -94,9 +94,17 @@ pub fn gather_sources_in_range(
|
||||
) -> Vec<LightSource> {
|
||||
let mut sources = Vec::new();
|
||||
let min_x = (cam_x - margin).max(0);
|
||||
let max_x = (cam_x + view_w as i32 + margin).min(grid.width as i32);
|
||||
let max_x = if grid.is_infinite() {
|
||||
cam_x + view_w as i32 + margin
|
||||
} else {
|
||||
(cam_x + view_w as i32 + margin).min(grid.width as i32)
|
||||
};
|
||||
let min_y = (cam_y - margin).max(0);
|
||||
let max_y = (cam_y + view_h as i32 + margin).min(grid.height as i32);
|
||||
let max_y = if grid.is_infinite() {
|
||||
cam_y + view_h as i32 + margin
|
||||
} else {
|
||||
(cam_y + view_h as i32 + margin).min(grid.height as i32)
|
||||
};
|
||||
for y in min_y..max_y {
|
||||
for x in min_x..max_x {
|
||||
let cell = grid.get(x, y);
|
||||
@@ -111,7 +119,7 @@ pub fn gather_sources_in_range(
|
||||
}
|
||||
|
||||
pub fn compute_lighting(
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
cam_x: i32,
|
||||
cam_y: i32,
|
||||
view_w: usize,
|
||||
@@ -176,7 +184,7 @@ pub fn compute_lighting(
|
||||
grid_light
|
||||
}
|
||||
|
||||
pub fn line_of_sight(grid: &Grid, x0: i32, y0: i32, x1: i32, y1: i32) -> bool {
|
||||
pub fn line_of_sight(grid: &ChunkedGrid, x0: i32, y0: i32, x1: i32, y1: i32) -> bool {
|
||||
let mut x = x0;
|
||||
let mut y = y0;
|
||||
let dx = (x1 - x0).abs();
|
||||
@@ -236,10 +244,10 @@ pub fn ambient_light() -> [u8; 3] {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::world::grid::Grid;
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
|
||||
fn grid_with_lava() -> (Grid, i32, i32) {
|
||||
let mut grid = Grid::new();
|
||||
fn grid_with_lava() -> (ChunkedGrid, i32, i32) {
|
||||
let mut grid = ChunkedGrid::with_size(250, 250);
|
||||
grid.set_material(10, 10, MaterialId::Lava);
|
||||
(grid, 10, 10)
|
||||
}
|
||||
@@ -267,7 +275,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn walls_block_light() {
|
||||
let mut grid = Grid::new();
|
||||
let mut grid = ChunkedGrid::with_size(250, 250);
|
||||
grid.set_material(5, 10, MaterialId::Lava);
|
||||
for y in 7..13 {
|
||||
grid.set_material(8, y, MaterialId::Stone);
|
||||
|
||||
+2
-2
@@ -8,13 +8,13 @@ pub mod window_input;
|
||||
use crate::entity::item::ItemManager;
|
||||
use crate::entity::EntityManager;
|
||||
use crate::ui::UiLayer;
|
||||
use crate::world::grid::Grid;
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
|
||||
pub trait Renderer {
|
||||
fn init(&mut self) -> std::io::Result<()>;
|
||||
fn render(
|
||||
&mut self,
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &EntityManager,
|
||||
items: &ItemManager,
|
||||
ui: &UiLayer,
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::entity::EntityManager;
|
||||
use crate::render::lighting::{self, apply_light_tuple, LightGrid};
|
||||
use crate::render::Renderer;
|
||||
use crate::world::cell::MaterialId;
|
||||
use crate::world::grid::Grid;
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
|
||||
fn entity_priority(kind: crate::entity::EntityKind) -> u32 {
|
||||
match kind {
|
||||
@@ -96,7 +96,7 @@ impl Renderer for TerminalRenderer {
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &EntityManager,
|
||||
items: &crate::entity::item::ItemManager,
|
||||
ui: &crate::ui::UiLayer,
|
||||
|
||||
+14
-17
@@ -6,7 +6,8 @@ use std::sync::Arc;
|
||||
use crate::entity::{EntityKind, EntityManager};
|
||||
use crate::render::lighting;
|
||||
use crate::world::cell::MaterialId;
|
||||
use crate::world::grid::{Grid, WORLD_H, WORLD_W};
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
use crate::world::grid::{MAX_WORLD_H, MAX_WORLD_W};
|
||||
|
||||
const CHAR_W: u32 = 8;
|
||||
const CHAR_H: u32 = 8;
|
||||
@@ -225,7 +226,7 @@ impl VulkanRenderer {
|
||||
let (ui_instance_buffer, ui_instance_memory, ui_instance_ptr) =
|
||||
create_instance_buffer(&device, &instance, physical_device, ui_instance_capacity)?;
|
||||
|
||||
let grid_data = vec![0u32; WORLD_W * WORLD_H];
|
||||
let grid_data = vec![0u32; MAX_WORLD_W * MAX_WORLD_H];
|
||||
let (grid_buffer, grid_memory) = create_buffer_with_data(
|
||||
&device,
|
||||
&instance,
|
||||
@@ -234,7 +235,7 @@ impl VulkanRenderer {
|
||||
vk::BufferUsageFlags::STORAGE_BUFFER,
|
||||
)?;
|
||||
let grid_ptr = unsafe {
|
||||
let sz = (WORLD_W * WORLD_H * std::mem::size_of::<u32>()) as vk::DeviceSize;
|
||||
let sz = (MAX_WORLD_W * MAX_WORLD_H * std::mem::size_of::<u32>()) as vk::DeviceSize;
|
||||
let ptr = device
|
||||
.map_memory(grid_memory, 0, sz, vk::MemoryMapFlags::default())
|
||||
.map_err(|e| format!("map grid: {e:?}"))?;
|
||||
@@ -339,7 +340,7 @@ impl VulkanRenderer {
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
grid: &Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &EntityManager,
|
||||
items: &crate::entity::item::ItemManager,
|
||||
ui: &crate::ui::UiLayer,
|
||||
@@ -441,16 +442,12 @@ impl VulkanRenderer {
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let margin = 30i32;
|
||||
let x_min = (cam_x - margin).max(0) as usize;
|
||||
let x_max = (cam_x + self.grid_w as i32 + margin).min(WORLD_W as i32) as usize;
|
||||
let y_min = (cam_y - margin).max(0) as usize;
|
||||
let y_max = (cam_y + self.grid_h as i32 + margin).min(WORLD_H as i32) as usize;
|
||||
for y in y_min..y_max {
|
||||
let row_offset = y * WORLD_W;
|
||||
for x in x_min..x_max {
|
||||
let i = row_offset + x;
|
||||
*self.grid_ptr.add(i) = grid.cells[i].material as u32;
|
||||
for dy in 0..self.grid_h {
|
||||
for dx in 0..self.grid_w {
|
||||
let wx = cam_x + dx as i32;
|
||||
let wy = cam_y + dy as i32;
|
||||
let idx = dy * self.grid_w + dx;
|
||||
*self.grid_ptr.add(idx) = grid.get(wx, wy).material as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -627,7 +624,7 @@ impl VulkanRenderer {
|
||||
self.swapchain_extent.height as f32,
|
||||
],
|
||||
cell_size: [CHAR_W as f32, CHAR_H as f32],
|
||||
world_size: [WORLD_W as i32, WORLD_H as i32],
|
||||
world_size: [self.grid_w as i32, self.grid_h as i32],
|
||||
cam_pos: [cam_x, cam_y],
|
||||
ambient: [
|
||||
ambient[0] as f32 / 255.0,
|
||||
@@ -660,7 +657,7 @@ impl VulkanRenderer {
|
||||
self.swapchain_extent.height as f32,
|
||||
],
|
||||
cell_size: [UI_CELL_SIZE as f32, UI_CELL_SIZE as f32],
|
||||
world_size: [WORLD_W as i32, WORLD_H as i32],
|
||||
world_size: [self.grid_w as i32, self.grid_h as i32],
|
||||
cam_pos: [0, 0],
|
||||
ambient: [0.0, 0.0, 0.0],
|
||||
is_ui: 1,
|
||||
@@ -1824,7 +1821,7 @@ fn update_descriptor_set(
|
||||
let bi = vk::DescriptorBufferInfo::default()
|
||||
.buffer(grid_buffer)
|
||||
.offset(0)
|
||||
.range((WORLD_W * WORLD_H * std::mem::size_of::<u32>()) as vk::DeviceSize);
|
||||
.range((MAX_WORLD_W * MAX_WORLD_H * std::mem::size_of::<u32>()) as vk::DeviceSize);
|
||||
let li = vk::DescriptorBufferInfo::default()
|
||||
.buffer(light_buffer)
|
||||
.offset(0)
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::entity::entity::{Entity, EntityKind};
|
||||
use crate::world::cell::MaterialId;
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
|
||||
pub const UI_SCALE: i32 = 4;
|
||||
|
||||
@@ -241,7 +242,7 @@ impl UiLayer {
|
||||
&mut self,
|
||||
screen_w: usize,
|
||||
screen_h: usize,
|
||||
grid: &crate::world::grid::Grid,
|
||||
grid: &ChunkedGrid,
|
||||
entities: &[Entity],
|
||||
cam_x: i32,
|
||||
cam_y: i32,
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
use crate::entity::item::{ItemManager, ItemType};
|
||||
use crate::entity::player::Player;
|
||||
use crate::entity::EntityManager;
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const CACHE_VERSION: u32 = 2;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct CacheMeta {
|
||||
version: u32,
|
||||
seed: u64,
|
||||
player_x: f32,
|
||||
player_y: f32,
|
||||
depth: u32,
|
||||
items: Vec<CachedItem>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct CachedItem {
|
||||
typ: String,
|
||||
x: i32,
|
||||
y: i32,
|
||||
}
|
||||
|
||||
pub struct WorldCache;
|
||||
|
||||
impl WorldCache {
|
||||
pub fn path(root: &str, seed: u64) -> PathBuf {
|
||||
PathBuf::from(root).join(format!("seed_{}", seed))
|
||||
}
|
||||
|
||||
pub fn meta_path(root: &str, seed: u64) -> PathBuf {
|
||||
Self::path(root, seed).join("meta.json")
|
||||
}
|
||||
|
||||
pub fn chunk_path(root: &str, seed: u64, cx: i32, cy: i32) -> PathBuf {
|
||||
Self::path(root, seed).join(format!("chunk_{}_{}.bin", cx, cy))
|
||||
}
|
||||
|
||||
pub fn meta_exists(root: &str, seed: u64) -> bool {
|
||||
Self::meta_path(root, seed).exists()
|
||||
}
|
||||
|
||||
pub fn chunk_exists(root: &str, seed: u64, cx: i32, cy: i32) -> bool {
|
||||
Self::chunk_path(root, seed, cx, cy).exists()
|
||||
}
|
||||
|
||||
pub fn save_meta(
|
||||
root: &str,
|
||||
seed: u64,
|
||||
player_x: f32,
|
||||
player_y: f32,
|
||||
depth: u32,
|
||||
items: &ItemManager,
|
||||
) -> io::Result<()> {
|
||||
let path = Self::path(root, seed);
|
||||
std::fs::create_dir_all(&path)?;
|
||||
let meta = CacheMeta {
|
||||
version: CACHE_VERSION,
|
||||
seed,
|
||||
player_x,
|
||||
player_y,
|
||||
depth,
|
||||
items: items
|
||||
.all()
|
||||
.iter()
|
||||
.map(|i| CachedItem {
|
||||
typ: i.name().to_string(),
|
||||
x: i.x,
|
||||
y: i.y,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let meta_json = serde_json::to_string_pretty(&meta)?;
|
||||
std::fs::write(path.join("meta.json"), meta_json)
|
||||
}
|
||||
|
||||
pub fn load_meta(
|
||||
root: &str,
|
||||
seed: u64,
|
||||
player: &mut Player,
|
||||
entities: &mut EntityManager,
|
||||
items: &mut ItemManager,
|
||||
) -> io::Result<u32> {
|
||||
let path = Self::path(root, seed);
|
||||
let meta_json = std::fs::read_to_string(path.join("meta.json"))?;
|
||||
let meta: CacheMeta = serde_json::from_str(&meta_json)
|
||||
.map_err(|e| io::Error::other(format!("cache meta parse: {}", e)))?;
|
||||
if meta.version != CACHE_VERSION {
|
||||
return Err(io::Error::other("cache version mismatch"));
|
||||
}
|
||||
player.spawn_at(entities, meta.player_x, meta.player_y);
|
||||
items.all_mut().clear();
|
||||
for ci in meta.items {
|
||||
if let Some(typ) = ItemType::from_name(&ci.typ) {
|
||||
items.spawn(typ, ci.x, ci.y);
|
||||
}
|
||||
}
|
||||
Ok(meta.depth)
|
||||
}
|
||||
|
||||
pub fn save_chunk(
|
||||
root: &str,
|
||||
seed: u64,
|
||||
cx: i32,
|
||||
cy: i32,
|
||||
grid: &ChunkedGrid,
|
||||
) -> io::Result<()> {
|
||||
let path = Self::chunk_path(root, seed, cx, cy);
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
grid.save_chunk(path.to_str().unwrap(), cx, cy)
|
||||
}
|
||||
|
||||
pub fn load_chunk(
|
||||
root: &str,
|
||||
seed: u64,
|
||||
cx: i32,
|
||||
cy: i32,
|
||||
grid: &mut ChunkedGrid,
|
||||
) -> io::Result<()> {
|
||||
let path = Self::chunk_path(root, seed, cx, cy);
|
||||
grid.load_chunk(path.to_str().unwrap(), cx, cy)
|
||||
}
|
||||
|
||||
pub fn save_all_loaded(root: &str, seed: u64, grid: &ChunkedGrid) -> io::Result<()> {
|
||||
let path = Self::path(root, seed);
|
||||
std::fs::create_dir_all(&path)?;
|
||||
for (&(cx, cy), chunk) in &grid.chunks {
|
||||
if chunk.modified || chunk.was_modified {
|
||||
let file = Self::chunk_path(root, seed, cx as i32, cy as i32);
|
||||
grid.save_chunk(file.to_str().unwrap(), cx as i32, cy as i32)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+214
-200
@@ -1,5 +1,5 @@
|
||||
use crate::world::cell::{Cell, MaterialId};
|
||||
use crate::world::grid::Grid;
|
||||
use crate::world::chunked_grid::ChunkedGrid;
|
||||
|
||||
pub struct CellularAutomaton {
|
||||
tick: u64,
|
||||
@@ -16,6 +16,10 @@ impl CellularAutomaton {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn seed(&mut self, state: u64) {
|
||||
self.rng_state = state;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn rand(&mut self) -> u32 {
|
||||
self.rng_state ^= self.rng_state << 13;
|
||||
@@ -30,19 +34,37 @@ impl CellularAutomaton {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn apply_cell_rule(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
fn apply_cell_rule(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.updated_this_tick || cell.is_empty() || cell.is_static() {
|
||||
return;
|
||||
}
|
||||
match cell.material {
|
||||
MaterialId::Sand => self.update_sand(grid, x, y),
|
||||
MaterialId::Water => self.update_water(grid, x, y),
|
||||
MaterialId::Lava => self.update_lava(grid, x, y),
|
||||
MaterialId::Steam => self.update_steam(grid, x, y),
|
||||
MaterialId::Fire => self.update_fire(grid, x, y),
|
||||
MaterialId::Smoke => self.update_smoke(grid, x, y),
|
||||
MaterialId::Acid => self.update_acid(grid, x, y),
|
||||
MaterialId::Water => {
|
||||
self.update_water(grid, x, y);
|
||||
grid.mark_dirty(x, y);
|
||||
}
|
||||
MaterialId::Lava => {
|
||||
self.update_lava(grid, x, y);
|
||||
grid.mark_dirty(x, y);
|
||||
}
|
||||
MaterialId::Steam => {
|
||||
self.update_steam(grid, x, y);
|
||||
grid.mark_dirty(x, y);
|
||||
}
|
||||
MaterialId::Fire => {
|
||||
self.update_fire(grid, x, y);
|
||||
grid.mark_dirty(x, y);
|
||||
}
|
||||
MaterialId::Smoke => {
|
||||
self.update_smoke(grid, x, y);
|
||||
grid.mark_dirty(x, y);
|
||||
}
|
||||
MaterialId::Acid => {
|
||||
self.update_acid(grid, x, y);
|
||||
grid.mark_dirty(x, y);
|
||||
}
|
||||
MaterialId::Flesh => self.update_flesh(grid, x, y),
|
||||
MaterialId::Grass => self.update_grass(grid, x, y),
|
||||
MaterialId::Dirt => self.update_dirt(grid, x, y),
|
||||
@@ -61,40 +83,38 @@ impl CellularAutomaton {
|
||||
(self.rand() as usize) % max
|
||||
}
|
||||
|
||||
pub fn step(&mut self, grid: &mut Grid) {
|
||||
grid.reset_tick_flags();
|
||||
pub fn step(&mut self, grid: &mut ChunkedGrid) {
|
||||
let flip = self.rand_bool();
|
||||
let chunk_w = grid.chunk_size;
|
||||
let chunks_x = grid.chunks_x;
|
||||
let chunks_y = grid.chunks_y;
|
||||
let grid_w = grid.width;
|
||||
|
||||
for cy in (0..chunks_y).rev() {
|
||||
let y0 = cy * chunk_w;
|
||||
let y1 = ((cy + 1) * chunk_w).min(grid.height);
|
||||
for y_idx in (y0..y1).rev() {
|
||||
let y = y_idx as i32;
|
||||
let mut active = grid.active_chunks();
|
||||
active.sort_by(|(ax, ay), (bx, by)| by.cmp(ay).then(bx.cmp(ax)));
|
||||
|
||||
for (cx, cy) in active {
|
||||
let dirty = grid.get_chunk_dirty(cx, cy);
|
||||
if dirty.is_none() {
|
||||
continue;
|
||||
}
|
||||
let (min_x, min_y, max_x, max_y) = dirty.unwrap();
|
||||
|
||||
grid.set_chunk_dirty(cx, cy, None);
|
||||
|
||||
for y in min_y..=max_y {
|
||||
for x in min_x..=max_x {
|
||||
let mut cell = grid.get(x, y);
|
||||
cell.updated_this_tick = false;
|
||||
grid.set(x, y, cell);
|
||||
}
|
||||
}
|
||||
grid.set_chunk_dirty(cx, cy, None);
|
||||
|
||||
for y in (min_y..=max_y).rev() {
|
||||
if flip {
|
||||
for cx in 0..chunks_x {
|
||||
if !grid.is_chunk_active(cx as i32, cy as i32) {
|
||||
continue;
|
||||
}
|
||||
let x0 = cx * chunk_w;
|
||||
let x1 = ((cx + 1) * chunk_w).min(grid_w);
|
||||
for x in x0..x1 {
|
||||
self.apply_cell_rule(grid, x as i32, y);
|
||||
}
|
||||
for x in min_x..=max_x {
|
||||
self.apply_cell_rule(grid, x, y);
|
||||
}
|
||||
} else {
|
||||
for cx in (0..chunks_x).rev() {
|
||||
if !grid.is_chunk_active(cx as i32, cy as i32) {
|
||||
continue;
|
||||
}
|
||||
let x0 = cx * chunk_w;
|
||||
let x1 = ((cx + 1) * chunk_w).min(grid_w);
|
||||
for x in (x0..x1).rev() {
|
||||
self.apply_cell_rule(grid, x as i32, y);
|
||||
}
|
||||
for x in (min_x..=max_x).rev() {
|
||||
self.apply_cell_rule(grid, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,15 +124,17 @@ impl CellularAutomaton {
|
||||
self.tick += 1;
|
||||
}
|
||||
|
||||
fn try_move_down(&mut self, grid: &mut Grid, x: i32, y: i32, _mat: MaterialId, density: f32) {
|
||||
fn try_move_down(
|
||||
&mut self,
|
||||
grid: &mut ChunkedGrid,
|
||||
x: i32,
|
||||
y: i32,
|
||||
_mat: MaterialId,
|
||||
density: f32,
|
||||
) {
|
||||
let below = grid.get(x, y + 1);
|
||||
if below.is_empty() || (below.is_liquid() && below.density() < density) {
|
||||
let src = grid.get(x, y);
|
||||
let i_dst = grid.idx(x, y + 1);
|
||||
let i_src = grid.idx(x, y);
|
||||
grid.cells[i_dst] = src;
|
||||
grid.cells[i_dst].updated_this_tick = true;
|
||||
grid.cells[i_src] = Cell::empty();
|
||||
grid.cells_swap(x, y, x, y + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -127,36 +149,25 @@ impl CellularAutomaton {
|
||||
|
||||
if can_left && can_right {
|
||||
if self.rand_bool() {
|
||||
self.do_swap(grid, x, y, x - dir, y + 1);
|
||||
grid.cells_swap(x, y, x - dir, y + 1);
|
||||
} else {
|
||||
self.do_swap(grid, x, y, x + dir, y + 1);
|
||||
grid.cells_swap(x, y, x + dir, y + 1);
|
||||
}
|
||||
} else if can_left {
|
||||
self.do_swap(grid, x, y, x - dir, y + 1);
|
||||
grid.cells_swap(x, y, x - dir, y + 1);
|
||||
} else if can_right {
|
||||
self.do_swap(grid, x, y, x + dir, y + 1);
|
||||
grid.cells_swap(x, y, x + dir, y + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn do_swap(&self, grid: &mut Grid, x1: i32, y1: i32, x2: i32, y2: i32) {
|
||||
let a = grid.get(x1, y1);
|
||||
let b = grid.get(x2, y2);
|
||||
let i1 = grid.idx(x1, y1);
|
||||
let i2 = grid.idx(x2, y2);
|
||||
grid.cells[i1] = b;
|
||||
grid.cells[i2] = a;
|
||||
grid.cells[i2].updated_this_tick = true;
|
||||
}
|
||||
|
||||
fn update_sand(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
fn update_sand(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
self.try_move_down(grid, x, y, MaterialId::Sand, 1.5);
|
||||
}
|
||||
|
||||
fn update_water(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
fn update_water(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
let below = grid.get(x, y + 1);
|
||||
if below.is_empty() || (below.is_liquid() && below.density() < 1.0) {
|
||||
self.do_swap(grid, x, y, x, y + 1);
|
||||
grid.cells_swap(x, y, x, y + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -171,27 +182,27 @@ impl CellularAutomaton {
|
||||
|
||||
if can_dl && can_dr {
|
||||
if self.rand_bool() {
|
||||
self.do_swap(grid, x, y, x - dir, y + 1);
|
||||
grid.cells_swap(x, y, x - dir, y + 1);
|
||||
} else {
|
||||
self.do_swap(grid, x, y, x + dir, y + 1);
|
||||
grid.cells_swap(x, y, x + dir, y + 1);
|
||||
}
|
||||
} else if can_dl {
|
||||
self.do_swap(grid, x, y, x - dir, y + 1);
|
||||
grid.cells_swap(x, y, x - dir, y + 1);
|
||||
} else if can_dr {
|
||||
self.do_swap(grid, x, y, x + dir, y + 1);
|
||||
grid.cells_swap(x, y, x + dir, y + 1);
|
||||
} else {
|
||||
let can_l = grid.in_bounds(x - dir, y) && grid.get(x - dir, y).is_empty();
|
||||
let can_r = grid.in_bounds(x + dir, y) && grid.get(x + dir, y).is_empty();
|
||||
if can_l && can_r {
|
||||
if self.rand_bool() {
|
||||
self.do_swap(grid, x, y, x - dir, y);
|
||||
grid.cells_swap(x, y, x - dir, y);
|
||||
} else {
|
||||
self.do_swap(grid, x, y, x + dir, y);
|
||||
grid.cells_swap(x, y, x + dir, y);
|
||||
}
|
||||
} else if can_l {
|
||||
self.do_swap(grid, x, y, x - dir, y);
|
||||
grid.cells_swap(x, y, x - dir, y);
|
||||
} else if can_r {
|
||||
self.do_swap(grid, x, y, x + dir, y);
|
||||
grid.cells_swap(x, y, x + dir, y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,49 +211,47 @@ impl CellularAutomaton {
|
||||
let mut new = cell;
|
||||
new.material = MaterialId::Steam;
|
||||
new.temp = 110.0;
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = new;
|
||||
grid.set(x, y, new);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_lava(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
fn update_lava(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp < 400.0 {
|
||||
let mut new = cell;
|
||||
new.material = MaterialId::Stone;
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = new;
|
||||
grid.set(x, y, new);
|
||||
return;
|
||||
}
|
||||
|
||||
let below = grid.get(x, y + 1);
|
||||
if below.is_empty() {
|
||||
self.do_swap(grid, x, y, x, y + 1);
|
||||
grid.cells_swap(x, y, x, y + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = if self.rand_bool() { 1 } else { -1 };
|
||||
if grid.in_bounds(x - dir, y + 1) && grid.get(x - dir, y + 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x - dir, y + 1);
|
||||
grid.cells_swap(x, y, x - dir, y + 1);
|
||||
return;
|
||||
}
|
||||
if grid.in_bounds(x + dir, y + 1) && grid.get(x + dir, y + 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x + dir, y + 1);
|
||||
grid.cells_swap(x, y, x + dir, y + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.rand() % 10 == 0 {
|
||||
if grid.in_bounds(x - dir, y) && grid.get(x - dir, y).is_empty() {
|
||||
self.do_swap(grid, x, y, x - dir, y);
|
||||
grid.cells_swap(x, y, x - dir, y);
|
||||
} else if grid.in_bounds(x + dir, y) && grid.get(x + dir, y).is_empty() {
|
||||
self.do_swap(grid, x, y, x + dir, y);
|
||||
grid.cells_swap(x, y, x + dir, y);
|
||||
}
|
||||
}
|
||||
|
||||
self.lava_interact(grid, x, y);
|
||||
}
|
||||
|
||||
fn lava_interact(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
fn lava_interact(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
for &(dx, dy) in &NEIGHBORS4 {
|
||||
let nx = x + dx;
|
||||
let ny = y + dy;
|
||||
@@ -252,84 +261,61 @@ impl CellularAutomaton {
|
||||
let neighbor = grid.get(nx, ny);
|
||||
match neighbor.material {
|
||||
MaterialId::Water => {
|
||||
let i_n = grid.idx(nx, ny);
|
||||
grid.cells[i_n] = Cell::new(MaterialId::Steam);
|
||||
grid.set(nx, ny, Cell::new(MaterialId::Steam));
|
||||
let lava = grid.get(x, y);
|
||||
let mut new_lava = lava;
|
||||
new_lava.temp -= 50.0;
|
||||
let i_l = grid.idx(x, y);
|
||||
grid.cells[i_l] = new_lava;
|
||||
grid.set(x, y, new_lava);
|
||||
}
|
||||
MaterialId::Wood | MaterialId::Grass | MaterialId::Flesh
|
||||
if neighbor.temp < 300.0 =>
|
||||
{
|
||||
let i_n = grid.idx(nx, ny);
|
||||
grid.cells[i_n] = Cell::new(MaterialId::Fire);
|
||||
grid.set(nx, ny, Cell::new(MaterialId::Fire));
|
||||
}
|
||||
MaterialId::Sand if neighbor.temp > 1700.0 => {
|
||||
let i_n = grid.idx(nx, ny);
|
||||
grid.cells[i_n] = Cell::new(MaterialId::Stone);
|
||||
grid.set(nx, ny, Cell::new(MaterialId::Stone));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_steam(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
fn update_steam(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp < 80.0 {
|
||||
let mut new = cell;
|
||||
new.material = MaterialId::Water;
|
||||
new.temp = 50.0;
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = new;
|
||||
grid.set(x, y, new);
|
||||
return;
|
||||
}
|
||||
|
||||
if y > 0 && grid.get(x, y - 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x, y - 1);
|
||||
grid.cells_swap(x, y, x, y - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = if self.rand_bool() { 1 } else { -1 };
|
||||
if grid.in_bounds(x - dir, y - 1) && grid.get(x - dir, y - 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x - dir, y - 1);
|
||||
grid.cells_swap(x, y, x - dir, y - 1);
|
||||
return;
|
||||
}
|
||||
if grid.in_bounds(x + dir, y - 1) && grid.get(x + dir, y - 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x + dir, y - 1);
|
||||
grid.cells_swap(x, y, x + dir, y - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.rand() % 3 == 0 {
|
||||
if grid.in_bounds(x - dir, y) && grid.get(x - dir, y).is_empty() {
|
||||
self.do_swap(grid, x, y, x - dir, y);
|
||||
grid.cells_swap(x, y, x - dir, y);
|
||||
} else if grid.in_bounds(x + dir, y) && grid.get(x + dir, y).is_empty() {
|
||||
self.do_swap(grid, x, y, x + dir, y);
|
||||
grid.cells_swap(x, y, x + dir, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_fire(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
fn update_fire(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp < 100.0 || self.rand() % 20 == 0 {
|
||||
let i = grid.idx(x, y);
|
||||
if self.rand() % 3 == 0 {
|
||||
grid.cells[i] = Cell::new(MaterialId::Smoke);
|
||||
} else {
|
||||
grid.cells[i] = Cell::empty();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut new = cell;
|
||||
new.temp -= 15.0;
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = new;
|
||||
|
||||
if y > 0 && grid.get(x, y - 1).is_empty() && self.rand() % 2 == 0 {
|
||||
self.do_swap(grid, x, y, x, y - 1);
|
||||
}
|
||||
|
||||
for &(dx, dy) in &NEIGHBORS4 {
|
||||
let nx = x + dx;
|
||||
@@ -344,37 +330,52 @@ impl CellularAutomaton {
|
||||
let mut new_n = neighbor;
|
||||
new_n.material = MaterialId::Fire;
|
||||
new_n.temp = 400.0;
|
||||
let i_n = grid.idx(nx, ny);
|
||||
grid.cells[i_n] = new_n;
|
||||
grid.set(nx, ny, new_n);
|
||||
}
|
||||
}
|
||||
|
||||
if cell.temp < 100.0 || self.rand() % 20 == 0 {
|
||||
if self.rand() % 3 == 0 {
|
||||
grid.set(x, y, Cell::new(MaterialId::Smoke));
|
||||
} else {
|
||||
grid.set(x, y, Cell::empty());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut new = cell;
|
||||
new.temp -= 15.0;
|
||||
grid.set(x, y, new);
|
||||
|
||||
if y > 0 && grid.get(x, y - 1).is_empty() && self.rand() % 2 == 0 {
|
||||
grid.cells_swap(x, y, x, y - 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_smoke(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
fn update_smoke(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
if self.rand() % 60 == 0 {
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = Cell::empty();
|
||||
grid.set(x, y, Cell::empty());
|
||||
return;
|
||||
}
|
||||
|
||||
if y > 0 && grid.get(x, y - 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x, y - 1);
|
||||
grid.cells_swap(x, y, x, y - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = if self.rand_bool() { 1 } else { -1 };
|
||||
if grid.in_bounds(x - dir, y - 1) && grid.get(x - dir, y - 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x - dir, y - 1);
|
||||
grid.cells_swap(x, y, x - dir, y - 1);
|
||||
} else if grid.in_bounds(x + dir, y - 1) && grid.get(x + dir, y - 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x + dir, y - 1);
|
||||
grid.cells_swap(x, y, x + dir, y - 1);
|
||||
} else if grid.in_bounds(x - dir, y) && grid.get(x - dir, y).is_empty() {
|
||||
self.do_swap(grid, x, y, x - dir, y);
|
||||
grid.cells_swap(x, y, x - dir, y);
|
||||
} else if grid.in_bounds(x + dir, y) && grid.get(x + dir, y).is_empty() {
|
||||
self.do_swap(grid, x, y, x + dir, y);
|
||||
grid.cells_swap(x, y, x + dir, y);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_acid(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
fn update_acid(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
for &(dx, dy) in &NEIGHBORS4 {
|
||||
let nx = x + dx;
|
||||
let ny = y + dy;
|
||||
@@ -387,11 +388,9 @@ impl CellularAutomaton {
|
||||
&& neighbor.material != MaterialId::Stone
|
||||
&& self.rand() % 4 == 0
|
||||
{
|
||||
let i_n = grid.idx(nx, ny);
|
||||
grid.cells[i_n] = Cell::empty();
|
||||
grid.set(nx, ny, Cell::empty());
|
||||
if self.rand() % 2 == 0 {
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = Cell::empty();
|
||||
grid.set(x, y, Cell::empty());
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -399,113 +398,120 @@ impl CellularAutomaton {
|
||||
|
||||
let below = grid.get(x, y + 1);
|
||||
if below.is_empty() || (below.is_liquid() && below.density() < 1.2) {
|
||||
self.do_swap(grid, x, y, x, y + 1);
|
||||
grid.cells_swap(x, y, x, y + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = if self.rand_bool() { 1 } else { -1 };
|
||||
if grid.in_bounds(x - dir, y + 1) && grid.get(x - dir, y + 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x - dir, y + 1);
|
||||
grid.cells_swap(x, y, x - dir, y + 1);
|
||||
} else if grid.in_bounds(x + dir, y + 1) && grid.get(x + dir, y + 1).is_empty() {
|
||||
self.do_swap(grid, x, y, x + dir, y + 1);
|
||||
grid.cells_swap(x, y, x + dir, y + 1);
|
||||
} else if grid.in_bounds(x - dir, y) && grid.get(x - dir, y).is_empty() {
|
||||
self.do_swap(grid, x, y, x - dir, y);
|
||||
grid.cells_swap(x, y, x - dir, y);
|
||||
} else if grid.in_bounds(x + dir, y) && grid.get(x + dir, y).is_empty() {
|
||||
self.do_swap(grid, x, y, x + dir, y);
|
||||
grid.cells_swap(x, y, x + dir, y);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_flesh(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
fn update_flesh(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp > 200.0 {
|
||||
let mut new = cell;
|
||||
new.material = MaterialId::Fire;
|
||||
new.temp = 400.0;
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = new;
|
||||
grid.set(x, y, new);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_grass(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
fn update_grass(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp > 250.0 {
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = Cell::new(MaterialId::Fire);
|
||||
grid.set(x, y, Cell::new(MaterialId::Fire));
|
||||
}
|
||||
}
|
||||
|
||||
fn update_dirt(&mut self, grid: &mut Grid, x: i32, y: i32) {
|
||||
fn update_dirt(&mut self, grid: &mut ChunkedGrid, x: i32, y: i32) {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.temp < 0.0 {
|
||||
let mut new = cell;
|
||||
new.material = MaterialId::Stone;
|
||||
let i = grid.idx(x, y);
|
||||
grid.cells[i] = new;
|
||||
grid.set(x, y, new);
|
||||
}
|
||||
}
|
||||
|
||||
fn heat_transfer(&mut self, grid: &mut Grid) {
|
||||
let w = grid.width;
|
||||
let size = w * grid.height;
|
||||
self.temps.resize(size, 0.0);
|
||||
for i in 0..size {
|
||||
self.temps[i] = grid.cells[i].temp;
|
||||
}
|
||||
fn heat_transfer(&mut self, grid: &mut ChunkedGrid) {
|
||||
let reg = crate::world::material::MaterialRegistry::instance();
|
||||
let gw = grid.width as i32;
|
||||
let gh = grid.height as i32;
|
||||
|
||||
let chunk_w = grid.chunk_size;
|
||||
for cy in 0..grid.chunks_y {
|
||||
if !grid.chunks[grid.chunk_index(0, cy as i32)].active {
|
||||
let mut any_active = false;
|
||||
for cx in 0..grid.chunks_x {
|
||||
if grid.is_chunk_active(cx as i32, cy as i32) {
|
||||
any_active = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !any_active {
|
||||
continue;
|
||||
let active = grid.active_chunks();
|
||||
for (cx, cy) in active {
|
||||
let dirty = grid.get_chunk_dirty(cx, cy);
|
||||
if dirty.is_none() {
|
||||
continue;
|
||||
}
|
||||
let (min_x, min_y, max_x, max_y) = dirty.unwrap();
|
||||
|
||||
let ex_min_x = (min_x - 1).max(0);
|
||||
let ex_min_y = (min_y - 1).max(0);
|
||||
let (ex_max_x, ex_max_y) = if grid.is_infinite() {
|
||||
((max_x + 1), (max_y + 1))
|
||||
} else {
|
||||
((max_x + 1).min(gw - 1), (max_y + 1).min(gh - 1))
|
||||
};
|
||||
|
||||
let ew = (ex_max_x - ex_min_x + 1) as usize;
|
||||
let eh = (ex_max_y - ex_min_y + 1) as usize;
|
||||
let ecount = ew.saturating_mul(eh);
|
||||
if ecount > 10000 {
|
||||
eprintln!(
|
||||
"heat_transfer skipping huge dirty rect: chunk=({}, {}) dirty=({},{},{},{}) ew={} eh={}",
|
||||
cx, cy, min_x, min_y, max_x, max_y, ew, eh
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if self.temps.len() < ecount {
|
||||
self.temps.resize(ecount, 0.0);
|
||||
}
|
||||
|
||||
for y in ex_min_y..=ex_max_y {
|
||||
for x in ex_min_x..=ex_max_x {
|
||||
let idx = ((y - ex_min_y) as usize) * ew + (x - ex_min_x) as usize;
|
||||
self.temps[idx] = grid.get(x, y).temp;
|
||||
}
|
||||
}
|
||||
let y0 = cy * chunk_w;
|
||||
let y1 = ((cy + 1) * chunk_w).min(grid.height);
|
||||
for y in y0..y1 {
|
||||
for cx in 0..grid.chunks_x {
|
||||
if !grid.is_chunk_active(cx as i32, cy as i32) {
|
||||
|
||||
for y in min_y..=max_y {
|
||||
for x in min_x..=max_x {
|
||||
let cell = grid.get(x, y);
|
||||
if cell.is_empty() || cell.is_static() {
|
||||
continue;
|
||||
}
|
||||
let mat = reg.get(cell.material);
|
||||
let k = mat.heat_conductivity;
|
||||
if k == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let x0 = cx * chunk_w;
|
||||
let x1 = ((cx + 1) * chunk_w).min(grid.width);
|
||||
for x in x0..x1 {
|
||||
let i = y * w + x;
|
||||
let cell = grid.cells[i];
|
||||
if cell.is_empty() || cell.is_static() {
|
||||
continue;
|
||||
}
|
||||
let reg = crate::world::material::MaterialRegistry::instance();
|
||||
let mat = reg.get(cell.material);
|
||||
let k = mat.heat_conductivity;
|
||||
if k == 0.0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut sum = 0.0;
|
||||
let mut count = 0;
|
||||
for &(dx, dy) in &NEIGHBORS4 {
|
||||
let nx = x as i32 + dx;
|
||||
let ny = y as i32 + dy;
|
||||
if nx < 0 || nx >= w as i32 || ny < 0 || ny >= grid.height as i32 {
|
||||
continue;
|
||||
}
|
||||
let ni = ny as usize * w + nx as usize;
|
||||
sum += self.temps[ni];
|
||||
count += 1;
|
||||
}
|
||||
if count > 0 {
|
||||
let avg = sum / count as f32;
|
||||
let mut new = cell;
|
||||
new.temp += (avg - cell.temp) * k * 0.1;
|
||||
grid.cells[i] = new;
|
||||
let mut sum = 0.0;
|
||||
let mut count = 0;
|
||||
for &(dx, dy) in &NEIGHBORS4 {
|
||||
let nx = x + dx;
|
||||
let ny = y + dy;
|
||||
if nx < ex_min_x || nx > ex_max_x || ny < ex_min_y || ny > ex_max_y {
|
||||
continue;
|
||||
}
|
||||
let ni = ((ny - ex_min_y) as usize) * ew + (nx - ex_min_x) as usize;
|
||||
sum += self.temps[ni];
|
||||
count += 1;
|
||||
}
|
||||
if count > 0 {
|
||||
let avg = sum / count as f32;
|
||||
let mut new = cell;
|
||||
new.temp += (avg - cell.temp) * k * 0.1;
|
||||
grid.set(x, y, new);
|
||||
grid.mark_dirty(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -514,3 +520,11 @@ impl CellularAutomaton {
|
||||
}
|
||||
|
||||
const NEIGHBORS4: [(i32, i32); 4] = [(0, -1), (0, 1), (-1, 0), (1, 0)];
|
||||
|
||||
pub fn random_seed() -> u64 {
|
||||
let nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos();
|
||||
(nanos ^ (nanos >> 32)) as u64
|
||||
}
|
||||
|
||||
+40
-3
@@ -6,6 +6,9 @@ pub struct Chunk {
|
||||
pub cells: Vec<Cell>,
|
||||
pub active: bool,
|
||||
pub modified: bool,
|
||||
pub was_modified: bool,
|
||||
pub generated: bool,
|
||||
pub dirty: Option<(i32, i32, i32, i32)>,
|
||||
}
|
||||
|
||||
impl Chunk {
|
||||
@@ -15,6 +18,20 @@ impl Chunk {
|
||||
cells: vec![Cell::empty(); size],
|
||||
active: false,
|
||||
modified: false,
|
||||
was_modified: false,
|
||||
generated: false,
|
||||
dirty: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn swap_modified_flags(&mut self) {
|
||||
self.was_modified = self.modified;
|
||||
self.modified = false;
|
||||
}
|
||||
|
||||
pub fn reset_tick_flags(&mut self) {
|
||||
for c in &mut self.cells {
|
||||
c.updated_this_tick = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +66,29 @@ impl Chunk {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_tick_flags(&mut self) {
|
||||
for c in &mut self.cells {
|
||||
c.updated_this_tick = false;
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.cells.iter().all(|c| c.is_empty())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mark_dirty(&mut self, x: i32, y: i32) {
|
||||
if !Self::in_bounds(x, y) {
|
||||
return;
|
||||
}
|
||||
let min_x = (x - 1).max(0);
|
||||
let min_y = (y - 1).max(0);
|
||||
let max_x = (x + 1).min(CHUNK_SIZE as i32 - 1);
|
||||
let max_y = (y + 1).min(CHUNK_SIZE as i32 - 1);
|
||||
match self.dirty {
|
||||
None => self.dirty = Some((min_x, min_y, max_x, max_y)),
|
||||
Some((dx0, dy0, dx1, dy1)) => {
|
||||
self.dirty = Some((
|
||||
dx0.min(min_x),
|
||||
dy0.min(min_y),
|
||||
dx1.max(max_x),
|
||||
dy1.max(max_y),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,798 @@
|
||||
use crate::world::cell::{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 cx64 = cx as i64;
|
||||
let cy64 = cy as i64;
|
||||
if !self.chunks.contains_key(&(cx64, cy64)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.chunks.insert((cx64, cy64), Chunk::new());
|
||||
}
|
||||
self.chunks.get_mut(&(cx64, cy64))
|
||||
}
|
||||
|
||||
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);
|
||||
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.mark_dirty(lx, ly);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let chunk = self.get_or_create_chunk(cx, cy);
|
||||
chunk.set_material(lx, ly, mat);
|
||||
chunk.mark_dirty(lx, ly);
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
let tmp = chunk.cells[ci1];
|
||||
chunk.cells[ci1] = chunk.cells[ci2];
|
||||
chunk.cells[ci2] = tmp;
|
||||
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 c1 = self.get(x1, y1);
|
||||
let c2 = self.get(x2, y2);
|
||||
if let Some(chunk) = self.chunks_vec.get_mut(i1) {
|
||||
let ci = (ly1 as usize) * self.chunk_size + (lx1 as usize);
|
||||
chunk.cells[ci] = c2;
|
||||
chunk.cells[ci].updated_this_tick = true;
|
||||
chunk.modified = true;
|
||||
chunk.mark_dirty(lx1, ly1);
|
||||
}
|
||||
if let Some(chunk) = self.chunks_vec.get_mut(i2) {
|
||||
let ci = (ly2 as usize) * self.chunk_size + (lx2 as usize);
|
||||
chunk.cells[ci] = c1;
|
||||
chunk.modified = true;
|
||||
chunk.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);
|
||||
let tmp = chunk.cells[i1];
|
||||
chunk.cells[i1] = chunk.cells[i2];
|
||||
chunk.cells[i2] = tmp;
|
||||
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);
|
||||
self.set(x1, y1, c2);
|
||||
self.set(x2, y2, c1);
|
||||
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 (x0, y0, x1, y1) = self.chunk_bounds(cx, cy);
|
||||
let w = (x1 - x0) as usize;
|
||||
let h = (y1 - y0) as usize;
|
||||
let mut bytes = Vec::with_capacity(w * h * 12);
|
||||
let cs = self.chunk_size as i32;
|
||||
for y in y0..y1 {
|
||||
for x in x0..x1 {
|
||||
let lx = x - cx * cs;
|
||||
let ly = y - cy * cs;
|
||||
bytes.extend_from_slice(&chunk.get(lx, ly).to_bytes());
|
||||
}
|
||||
}
|
||||
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 (x0, y0, x1, y1) = self.chunk_bounds(cx, cy);
|
||||
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 cs = self.chunk_size as i32;
|
||||
let chunk = self.get_or_create_chunk(cx, cy);
|
||||
let mut i = 0usize;
|
||||
for y in y0..y1 {
|
||||
for x in x0..x1 {
|
||||
let lx = x - cx * cs;
|
||||
let ly = y - cy * cs;
|
||||
let cell = Cell::from_bytes(&data[i * 12..(i + 1) * 12]);
|
||||
chunk.set(lx, ly, cell);
|
||||
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 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))
|
||||
}
|
||||
+103
-7
@@ -6,10 +6,14 @@ use std::path::Path;
|
||||
pub const WORLD_W: usize = 250;
|
||||
pub const WORLD_H: usize = 250;
|
||||
|
||||
pub const MAX_WORLD_W: usize = 2048;
|
||||
pub const MAX_WORLD_H: usize = 2048;
|
||||
|
||||
pub struct ChunkMeta {
|
||||
pub active: bool,
|
||||
pub modified: bool,
|
||||
pub was_modified: bool,
|
||||
pub dirty: Option<(i32, i32, i32, i32)>,
|
||||
}
|
||||
|
||||
pub struct Grid {
|
||||
@@ -24,22 +28,27 @@ pub struct Grid {
|
||||
|
||||
impl Grid {
|
||||
pub fn new() -> Self {
|
||||
let size = WORLD_W * WORLD_H;
|
||||
Self::with_size(WORLD_W, WORLD_H)
|
||||
}
|
||||
|
||||
pub fn with_size(width: usize, height: usize) -> Self {
|
||||
let size = width * height;
|
||||
let chunk_size = CHUNK_SIZE;
|
||||
let chunks_x = (WORLD_W + chunk_size - 1) / chunk_size;
|
||||
let chunks_y = (WORLD_H + chunk_size - 1) / chunk_size;
|
||||
let chunks_x = (width + chunk_size - 1) / chunk_size;
|
||||
let chunks_y = (height + chunk_size - 1) / chunk_size;
|
||||
let mut chunks = Vec::with_capacity(chunks_x * chunks_y);
|
||||
for _ in 0..chunks_x * chunks_y {
|
||||
chunks.push(ChunkMeta {
|
||||
active: true,
|
||||
modified: false,
|
||||
was_modified: false,
|
||||
dirty: None,
|
||||
});
|
||||
}
|
||||
Self {
|
||||
cells: vec![Cell::empty(); size],
|
||||
width: WORLD_W,
|
||||
height: WORLD_H,
|
||||
width,
|
||||
height,
|
||||
chunk_size,
|
||||
chunks_x,
|
||||
chunks_y,
|
||||
@@ -185,6 +194,7 @@ impl Grid {
|
||||
if let Some(c) = self.chunks.get_mut(idx) {
|
||||
c.modified = true;
|
||||
}
|
||||
self.mark_dirty(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +208,7 @@ impl Grid {
|
||||
if let Some(c) = self.chunks.get_mut(idx) {
|
||||
c.modified = true;
|
||||
}
|
||||
self.mark_dirty(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,9 +223,94 @@ impl Grid {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mark_dirty(&mut self, x: i32, y: i32) {
|
||||
if !self.in_bounds(x, y) {
|
||||
return;
|
||||
}
|
||||
let (cx, cy, _, _) = self.chunk_at(x, y);
|
||||
self.expand_chunk_dirty(cx, cy, x, y);
|
||||
let cs = self.chunk_size as i32;
|
||||
let lx = x - cx * cs;
|
||||
let ly = y - cy * cs;
|
||||
if lx <= 1 {
|
||||
self.expand_chunk_dirty(cx - 1, cy, x - 1, y);
|
||||
}
|
||||
if lx >= cs - 2 {
|
||||
self.expand_chunk_dirty(cx + 1, cy, x + 1, y);
|
||||
}
|
||||
if ly <= 1 {
|
||||
self.expand_chunk_dirty(cx, cy - 1, x, y - 1);
|
||||
}
|
||||
if ly >= cs - 2 {
|
||||
self.expand_chunk_dirty(cx, cy + 1, x, y + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn expand_chunk_dirty(&mut self, cx: i32, cy: i32, x: i32, y: i32) {
|
||||
if cx < 0 || cy < 0 || cx >= self.chunks_x as i32 || cy >= self.chunks_y as i32 {
|
||||
return;
|
||||
}
|
||||
if !self.in_bounds(x, y) {
|
||||
return;
|
||||
}
|
||||
let idx = self.chunk_index(cx, cy);
|
||||
let min_x = (x - 1).max(0);
|
||||
let min_y = (y - 1).max(0);
|
||||
let max_x = (x + 1).min(self.width as i32 - 1);
|
||||
let max_y = (y + 1).min(self.height as i32 - 1);
|
||||
let chunk = &mut self.chunks[idx];
|
||||
match chunk.dirty {
|
||||
None => chunk.dirty = Some((min_x, min_y, max_x, max_y)),
|
||||
Some((dx0, dy0, dx1, dy1)) => {
|
||||
chunk.dirty = Some((
|
||||
dx0.min(min_x),
|
||||
dy0.min(min_y),
|
||||
dx1.max(max_x),
|
||||
dy1.max(max_y),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 i1 = self.idx(x1, y1);
|
||||
let i2 = self.idx(x2, y2);
|
||||
let tmp = self.cells[i1];
|
||||
self.cells[i1] = self.cells[i2];
|
||||
self.cells[i2] = tmp;
|
||||
self.cells[i2].updated_this_tick = true;
|
||||
self.mark_dirty(x1, y1);
|
||||
self.mark_dirty(x2, y2);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_cell_index(&mut self, i: usize, cell: Cell) {
|
||||
self.cells[i] = cell;
|
||||
let x = (i % self.width) as i32;
|
||||
let y = (i / self.width) as i32;
|
||||
self.mark_dirty(x, y);
|
||||
}
|
||||
|
||||
pub fn reset_tick_flags(&mut self) {
|
||||
for c in &mut self.cells {
|
||||
c.updated_this_tick = false;
|
||||
for cy in 0..self.chunks_y {
|
||||
for cx in 0..self.chunks_x {
|
||||
if !self.chunks[cy * self.chunks_x + cx].active {
|
||||
continue;
|
||||
}
|
||||
let (x0, y0, x1, y1) = self.chunk_bounds(cx, cy);
|
||||
for y in y0..y1 {
|
||||
let row = y as usize * self.width;
|
||||
for x in x0..x1 {
|
||||
self.cells[row + x as usize].updated_this_tick = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
pub mod cache;
|
||||
pub mod cell;
|
||||
pub mod cellular;
|
||||
pub mod chunk;
|
||||
pub mod chunked_grid;
|
||||
pub mod grid;
|
||||
pub mod material;
|
||||
pub mod worldgen;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user