diff --git a/Cargo.lock b/Cargo.lock index 850c4e7..ef609d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-polyfill" version = "1.0.3" @@ -227,30 +236,6 @@ dependencies = [ "objc2", ] -[[package]] -name = "bracket-noise" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b7443d0990c69db7a83f376f0101d684c20a911698e5f932bffbda2c8b08dd" -dependencies = [ - "bracket-random", -] - -[[package]] -name = "bracket-random" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "437be61484077b1ddb57002ce3c96b7d03cbf500b5d15157ee7e67e22332c39b" -dependencies = [ - "getrandom", - "js-sys", - "lazy_static", - "rand", - "rand_xorshift", - "regex", - "wasm-bindgen", -] - [[package]] name = "bumpalo" version = "3.16.0" @@ -262,6 +247,20 @@ name = "bytemuck" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] [[package]] name = "byteorder" @@ -610,6 +609,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fastnoise-lite" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e5f3c3cc9081e5d0e18bcd50e80cd33cba47fc22f88a9da9c33ecd1c87ea5c0" +dependencies = [ + "num-traits", +] + [[package]] name = "fdeflate" version = "0.3.4" @@ -1150,6 +1158,7 @@ version = "0.0.0" dependencies = [ "android-activity", "anyhow", + "atomic", "flume", "gilrs", "glam", @@ -1217,8 +1226,10 @@ name = "kubi-shared" version = "0.0.0" dependencies = [ "anyhow", + "atomic", "bincode", - "bracket-noise", + "bytemuck", + "fastnoise-lite", "glam", "hashbrown 0.14.3", "nohash-hasher", @@ -1662,15 +1673,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_xorshift" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" -dependencies = [ - "rand_core", -] - [[package]] name = "rand_xoshiro" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index d41d6d9..ae628a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,9 @@ opt-level = 3 [profile.dev.package.bracket-noise] opt-level = 3 +[profile.dev.package.fastnoise-lite] +opt-level = 3 + [profile.dev.package.rayon] opt-level = 3 diff --git a/kubi-server/src/world/tasks.rs b/kubi-server/src/world/tasks.rs index 8094632..4e45c92 100644 --- a/kubi-server/src/world/tasks.rs +++ b/kubi-server/src/world/tasks.rs @@ -1,59 +1,61 @@ -use shipyard::{Unique, AllStoragesView}; -use flume::{unbounded, Sender, Receiver}; -use glam::IVec3; -use rayon::{ThreadPool, ThreadPoolBuilder}; -use anyhow::Result; -use kubi_shared::{ - chunk::BlockData, - worldgen::generate_world, - queue::QueuedBlock, -}; - -pub enum ChunkTask { - LoadChunk { - position: IVec3, - seed: u64, - } -} - -pub enum ChunkTaskResponse { - ChunkLoaded { - chunk_position: IVec3, - blocks: BlockData, - queue: Vec - } -} - -#[derive(Unique)] -pub struct ChunkTaskManager { - channel: (Sender, Receiver), - pool: ThreadPool, -} -impl ChunkTaskManager { - pub fn new() -> Result { - Ok(Self { - channel: unbounded(), - pool: ThreadPoolBuilder::new().build()? - }) - } - pub fn spawn_task(&self, task: ChunkTask) { - let sender = self.channel.0.clone(); - self.pool.spawn(move || { - sender.send(match task { - ChunkTask::LoadChunk { position: chunk_position, seed } => { - let (blocks, queue) = generate_world(chunk_position, seed); - ChunkTaskResponse::ChunkLoaded { chunk_position, blocks, queue } - } - }).unwrap() - }) - } - pub fn receive(&self) -> Option { - self.channel.1.try_recv().ok() - } -} - -pub fn init_chunk_task_manager( - storages: AllStoragesView -) { - storages.add_unique(ChunkTaskManager::new().expect("ChunkTaskManager Init failed")); -} +use shipyard::{Unique, AllStoragesView}; +use flume::{unbounded, Sender, Receiver}; +use glam::IVec3; +use rayon::{ThreadPool, ThreadPoolBuilder}; +use anyhow::Result; +use kubi_shared::{ + chunk::BlockData, + worldgen::generate_world, + queue::QueuedBlock, +}; + +pub enum ChunkTask { + LoadChunk { + position: IVec3, + seed: u64, + } +} + +pub enum ChunkTaskResponse { + ChunkLoaded { + chunk_position: IVec3, + blocks: BlockData, + queue: Vec + } +} + +#[derive(Unique)] +pub struct ChunkTaskManager { + channel: (Sender, Receiver), + pool: ThreadPool, +} +impl ChunkTaskManager { + pub fn new() -> Result { + Ok(Self { + channel: unbounded(), + pool: ThreadPoolBuilder::new().build()? + }) + } + pub fn spawn_task(&self, task: ChunkTask) { + let sender = self.channel.0.clone(); + self.pool.spawn(move || { + sender.send(match task { + ChunkTask::LoadChunk { position: chunk_position, seed } => { + let Some((blocks, queue)) = generate_world(chunk_position, seed, None) else { + return + }; + ChunkTaskResponse::ChunkLoaded { chunk_position, blocks, queue } + } + }).unwrap() + }) + } + pub fn receive(&self) -> Option { + self.channel.1.try_recv().ok() + } +} + +pub fn init_chunk_task_manager( + storages: AllStoragesView +) { + storages.add_unique(ChunkTaskManager::new().expect("ChunkTaskManager Init failed")); +} diff --git a/kubi-shared/Cargo.toml b/kubi-shared/Cargo.toml index 2a05a9f..444bc73 100644 --- a/kubi-shared/Cargo.toml +++ b/kubi-shared/Cargo.toml @@ -14,14 +14,15 @@ serde = { version = "1.0", default-features = false, features = ["alloc", "deriv serde_with = "3.4" bincode = "1.3" anyhow = "1.0" -bracket-noise = "0.8" +fastnoise-lite = { version = "1.1", features = ["std", "f64"] } rand = { version = "0.8", default_features = false, features = ["std", "min_const_gen"] } rand_xoshiro = "0.6" hashbrown = { version = "0.14", features = ["serde"] } nohash-hasher = "0.2" -#bytemuck = { version = "1.14", features = ["derive"] } +bytemuck = { version = "1.14", features = ["derive"] } static_assertions = "1.1" nz = "0.3" +atomic = "0.6" [features] default = [] diff --git a/kubi-shared/src/block.rs b/kubi-shared/src/block.rs index b176b0f..9849071 100644 --- a/kubi-shared/src/block.rs +++ b/kubi-shared/src/block.rs @@ -26,9 +26,10 @@ pub enum BlockTexture { Water, } -#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, EnumIter, TryFromPrimitive)] +#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq, Eq, EnumIter, TryFromPrimitive)] #[repr(u8)] pub enum Block { + #[default] Air, Marker, Stone, diff --git a/kubi-shared/src/worldgen.rs b/kubi-shared/src/worldgen.rs index 7ddb678..f5ebba6 100644 --- a/kubi-shared/src/worldgen.rs +++ b/kubi-shared/src/worldgen.rs @@ -1,342 +1,199 @@ -use bracket_noise::prelude::*; -use rand::prelude::*; -use glam::{IVec3, ivec3, Vec3Swizzles, IVec2}; -use rand_xoshiro::Xoshiro256StarStar; +use std::sync::Arc; +use atomic::Atomic; +use bytemuck::{CheckedBitPattern, NoUninit}; +use glam::IVec3; +use static_assertions::const_assert; use crate::{ - chunk::{BlockData, CHUNK_SIZE}, block::Block, + chunk::{BlockData, CHUNK_SIZE}, queue::QueuedBlock, }; -fn mountain_ramp(mut x: f32) -> f32 { - x *= 2.0; - if x < 0.4 { - 0.5 * x - } else if x < 0.55 { - 4. * (x - 0.4) + 0.2 - } else { - 0.4444 * (x - 0.55) + 0.8 +pub mod steps; +pub mod structures; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Default, NoUninit, CheckedBitPattern)] +pub enum AbortState { + #[default] + Continue, + Abort, + Aborted, +} +const_assert!(Atomic::::is_lock_free()); + +pub struct SeedThingy { + pseed: u64, + iseed: i32, + iter: u8, +} + +impl SeedThingy { + pub fn new(seed: u64) -> Self { + Self { + pseed: seed, + iseed: (seed & 0x7fffffffu64) as i32, + iter: 0, + } + } + + pub fn next_seed(&mut self) -> i32 { + self.iter += 1; + self.iseed = ( + self.pseed + .rotate_left((3 * self.iter) as _) + & 0x7fffffff + ) as i32; + self.iseed } } - -fn local_height(height: i32, chunk_position: IVec3) -> usize { - let offset = chunk_position * CHUNK_SIZE as i32; - (height - offset.y).clamp(0, CHUNK_SIZE as i32) as usize +trait WorldGenStep { + fn initialize(generator: &WorldGenerator) -> Self; + fn generate(&mut self, generator: &mut WorldGenerator); } -fn local_y_position(height: i32, chunk_position: IVec3) -> Option { - let offset = chunk_position * CHUNK_SIZE as i32; - let position = height - offset.y; - (0..CHUNK_SIZE as i32).contains(&position).then_some(position as usize) +macro_rules! run_steps { + ($gen: expr, $abort: expr, [$($step:ty),* $(,)?]) => { + (||{ + let _abort: ::std::sync::Arc<::atomic::Atomic<$crate::worldgen::AbortState>> = + $abort.unwrap_or_else(|| ::std::sync::Arc::new(::atomic::Atomic::new($crate::worldgen::AbortState::Continue))); + + let _chkabt = || _abort.compare_exchange( + $crate::worldgen::AbortState::Abort, + $crate::worldgen::AbortState::Aborted, + ::atomic::Ordering::Relaxed, + ::atomic::Ordering::Relaxed + ).is_ok(); + + if _chkabt() { return false } + + $({ + let _ensure_ref: &mut $crate::worldgen::WorldGenerator = $gen; + struct _Ensure0(T); + type _Ensure1 = _Ensure0<$step>; + let mut step: _Ensure1 = _Ensure0(<$step>::initialize(&*_ensure_ref)); + if _chkabt() { return false } + step.0.generate(_ensure_ref); + if _chkabt() { return false } + })* + + true + })() + }; } +#[derive(Default)] +pub struct WorldGeneratorData { + pub master_height_map: Option>>, +} -pub fn generate_world(chunk_position: IVec3, seed: u64) -> (BlockData, Vec) { - let offset = chunk_position * CHUNK_SIZE as i32; - let mut blocks = Box::new([[[Block::Air; CHUNK_SIZE]; CHUNK_SIZE]; CHUNK_SIZE]); - let mut queue = Vec::with_capacity(0); +pub struct WorldGenerator { + seed: u64, + chunk_position: IVec3, + blocks: BlockData, + queue: Vec, + pub data: WorldGeneratorData, +} - let mut smart_place = |blocks: &mut BlockData, position: IVec3, block: Block| { +impl WorldGenerator { + fn offset(&self) -> IVec3 { + self.chunk_position * CHUNK_SIZE as i32 + } + + fn query(&self, position: IVec3) -> Block { + // let offset = self.offset(); + // let event_pos = offset + position; + // if let Some(block) = self.queue.iter().find(|block| block.position == event_pos) { + // block.block_type + // } else { + // self.blocks[position.x as usize][position.y as usize][position.z as usize] + // } + self.blocks[position.x as usize][position.y as usize][position.z as usize] + } + + fn place(&mut self, position: IVec3, block: Block) { + // let offset = self.offset(); + // let event_pos = offset + position; + // self.queue.retain(|block: &QueuedBlock| { + // block.position != event_pos + // }); + self.blocks[position.x as usize][position.y as usize][position.z as usize] = block; + } + + fn place_if_empty(&mut self, position: IVec3, block: Block) { + if self.query(position) == Block::Air { + self.place(position, block); + } + } + + fn place_or_queue(&mut self, position: IVec3, block: Block) { + let offset = self.offset(); if position.to_array().iter().any(|&x| !(0..CHUNK_SIZE).contains(&(x as usize))) { let event_pos = offset + position; - queue.retain(|block: &QueuedBlock| { + self.queue.retain(|block: &QueuedBlock| { block.position != event_pos }); - queue.push(QueuedBlock { - position: event_pos, + self.queue.push(QueuedBlock { + position: event_pos, block_type: block, soft: true }); } else { - blocks[position.x as usize][position.y as usize][position.z as usize] = block; - } - }; - - //STICK - if chunk_position.x == 0 && chunk_position.y == 5 { - for z in 0..CHUNK_SIZE { - blocks[0][0][z] = Block::Stone; - } - } - // - - let mut height_noise = FastNoise::seeded(seed); - height_noise.set_fractal_type(FractalType::FBM); - height_noise.set_fractal_octaves(4); - height_noise.set_frequency(0.003); - - let mut elevation_noise = FastNoise::seeded(seed.rotate_left(1)); - elevation_noise.set_fractal_type(FractalType::FBM); - elevation_noise.set_fractal_octaves(1); - elevation_noise.set_frequency(0.001); - - let mut cave_noise_a = FastNoise::seeded(seed.rotate_left(2)); - cave_noise_a.set_fractal_type(FractalType::FBM); - cave_noise_a.set_fractal_octaves(2); - cave_noise_a.set_frequency(0.01); - - let mut cave_noise_b = FastNoise::seeded(seed.rotate_left(3)); - cave_noise_b.set_fractal_type(FractalType::FBM); - cave_noise_b.set_fractal_octaves(3); - cave_noise_b.set_frequency(0.015); - - let mut cave_noise_holes = FastNoise::seeded(seed.rotate_left(4)); - cave_noise_holes.set_fractal_type(FractalType::FBM); - cave_noise_holes.set_fractal_octaves(2); - cave_noise_holes.set_frequency(0.005); - - let mut ravine_nose_line = FastNoise::seeded(seed.rotate_left(5)); - ravine_nose_line.set_fractal_type(FractalType::Billow); - ravine_nose_line.set_fractal_octaves(2); - ravine_nose_line.set_frequency(0.005); - - let mut ravine_noise_location = FastNoise::seeded(seed.rotate_left(6)); - ravine_noise_location.set_fractal_type(FractalType::FBM); - ravine_noise_location.set_fractal_octaves(1); - ravine_noise_location.set_frequency(0.005); - - let mut river_noise = FastNoise::seeded(seed.rotate_left(7)); - river_noise.set_fractal_type(FractalType::Billow); - river_noise.set_fractal_octaves(2); - river_noise.set_frequency(0.5 * 0.005); - - let mut rng = Xoshiro256StarStar::seed_from_u64( - seed - ^ (chunk_position.x as u32 as u64) - ^ ((chunk_position.z as u32 as u64) << 32) - ); - let rng_map_a: [[f32; CHUNK_SIZE]; CHUNK_SIZE] = rng.gen(); - let rng_map_b: [[f32; CHUNK_SIZE]; CHUNK_SIZE] = rng.gen(); - - //Generate height map - let mut within_heightmap = false; - let mut deco_heightmap = [[None; CHUNK_SIZE]; CHUNK_SIZE]; - - for x in 0..CHUNK_SIZE { - for z in 0..CHUNK_SIZE { - let (noise_x, noise_y) = ((offset.x + x as i32) as f32, (offset.z + z as i32) as f32); - //sample noises (that are needed right now) - let raw_heightmap_value = height_noise.get_noise(noise_x, noise_y); - let raw_elevation_value = elevation_noise.get_noise(noise_x, noise_y); - let raw_ravine_location_value = ravine_noise_location.get_noise(noise_x, noise_y); - //compute height - let mut is_surface = true; - let mut river_fill_height = None; - let height = { - let local_elevation = raw_elevation_value.powi(4).sqrt(); - let mut height = (mountain_ramp(raw_heightmap_value) * local_elevation * 100.) as i32; - //Flatten valleys - if height < 0 { - height /= 2; - } - //Generate rivers - { - let river_width = (height as f32 / -5.).clamp(0.5, 1.); - let river_value = river_noise.get_noise(noise_x, noise_y); - if ((-0.00625 * river_width)..(0.00625 * river_width)).contains(&(river_value.powi(2))) { - is_surface = false; - river_fill_height = Some(height - 1); - //river_fill_height = Some(-3); - height -= (river_width * 15. * ((0.00625 * river_width) - river_value.powi(2)) * (1. / (0.00625 * river_width))).round() as i32; - } - } - //Generate ravines - if height < 0 && raw_ravine_location_value > 0.4 { - let raw_ravine_value = ravine_nose_line.get_noise(noise_x, noise_y); - if (-0.0125..0.0125).contains(&(raw_ravine_value.powi(2))) { - is_surface = false; - height -= (100. * (0.0125 - raw_ravine_value.powi(2)) * (1. / 0.0125)).round() as i32; - } - } - height - }; - //add to heightmap - if is_surface { - deco_heightmap[x][z] = Some(height); - //place dirt - for y in 0..local_height(height, chunk_position) { - blocks[x][y][z] = Block::Dirt; - within_heightmap = true; - } - //place stone - for y in 0..local_height(height - 5 - (raw_heightmap_value * 5.) as i32, chunk_position) { - blocks[x][y][z] = Block::Stone; - within_heightmap = true; - } - //place grass - if let Some(y) = local_y_position(height, chunk_position) { - blocks[x][y][z] = Block::Grass; - within_heightmap = true; - } - } else if let Some(river_fill_height) = river_fill_height { - //Place water - for y in 0..local_height(river_fill_height, chunk_position) { - blocks[x][y][z] = Block::Water; - within_heightmap = true; - } - //Place stone - for y in 0..local_height(height, chunk_position) { - blocks[x][y][z] = Block::Stone; - within_heightmap = true; - } - //Place dirt - if let Some(y) = local_y_position(height, chunk_position) { - blocks[x][y][z] = Block::Dirt; - within_heightmap = true; - } - } else { - //Place stone - for y in 0..local_height(height, chunk_position) { - blocks[x][y][z] = Block::Stone; - within_heightmap = true; - } - } - } - } - - //Carve out caves - if within_heightmap { - for z in 0..CHUNK_SIZE { - for y in 0..CHUNK_SIZE { - for x in 0..CHUNK_SIZE { - if blocks[x][y][z] != Block::Stone { continue } - - let cave_size = ((offset.y + y as i32) as f32 / -100.).clamp(0., 1.); - let inv_cave_size = 1. - cave_size; - if cave_size < 0.1 { continue } - - let position = ivec3(x as i32, y as i32, z as i32) + offset; - - let is_cave = || { - let raw_cavemap_value_a = cave_noise_a.get_noise3d(position.x as f32, position.y as f32, position.z as f32); - let raw_cavemap_value_b = cave_noise_b.get_noise3d(position.x as f32, position.y as f32, position.z as f32); - ((cave_size * -0.15)..=(cave_size * 0.15)).contains(&raw_cavemap_value_a) && - ((cave_size * -0.15)..=(cave_size * 0.15)).contains(&raw_cavemap_value_b) - }; - let is_hole_cave = || { - let raw_cavemap_value_holes = cave_noise_holes.get_noise3d(position.x as f32, position.y as f32, position.z as f32); - ((0.9 + (0.1 * inv_cave_size))..=1.0).contains(&raw_cavemap_value_holes.abs()) - }; - - if is_cave() || is_hole_cave() { - blocks[x][y][z] = Block::Air; - if deco_heightmap[x][z] == Some(y as i32 + offset.y) { - deco_heightmap[x][z] = None - } - } - } - } + self.blocks[position.x as usize][position.y as usize][position.z as usize] = block; } } - //Add decorations - for x in 0..CHUNK_SIZE { - for z in 0..CHUNK_SIZE { - //get height - let Some(height) = deco_heightmap[x][z] else { continue }; - //check for air - // if blocks[x][local_y][z] == Block::Air { - // continue - // } - //place tall grass - if rng_map_a[x][z] < 0.03 { - if let Some(y) = local_y_position(height + 1, chunk_position) { - blocks[x][y][z] = Block::TallGrass; - } - } - //place trees! - if rng_map_a[x][z] < 0.001 { - //Replace grass with dirt under the tree - if let Some(y) = local_y_position(height, chunk_position) { - blocks[x][y][z] = Block::Dirt; - } + fn global_position(&self, position: IVec3) -> IVec3 { + self.offset() + position + } - //Place wood (no smart_place needed here!) - let tree_height = 4 + (rng_map_b[x][z] * 3.).round() as i32; - for tree_y in 0..tree_height { - if let Some(y) = local_y_position(height + 1 + tree_y, chunk_position) { - blocks[x][y][z] = Block::Wood; - } - } + fn local_height(&self, height: i32) -> i32 { + let offset = self.chunk_position * CHUNK_SIZE as i32; + (height - offset.y).clamp(0, CHUNK_SIZE as i32) + } - let tree_height = 4 + (rng_map_b[x][z] * 3.).round() as i32; + fn local_y_position(&self, y: i32) -> Option { + let offset = self.chunk_position * CHUNK_SIZE as i32; + let position = y - offset.y; + (0..CHUNK_SIZE as i32).contains(&position).then_some(position) + } - //Place leaf blocks - if let Some(y) = local_y_position(height + 1, chunk_position) { - let tree_pos = ivec3(x as i32, y as i32, z as i32); - // Place wood (smart_place) - // for tree_y in 0..tree_height { - // smart_place(&mut blocks, tree_pos + tree_y * IVec3::Y, Block::Wood); - // } - // Part that wraps around the tree - { - let tree_leaf_height = tree_height - 3; - let leaf_width = 2; - for tree_y in tree_leaf_height..tree_height { - for tree_x in (-leaf_width)..=leaf_width { - for tree_z in (-leaf_width)..=leaf_width { - let tree_offset = ivec3(tree_x, tree_y, tree_z); - if tree_offset.xz() == IVec2::ZERO { continue } - smart_place(&mut blocks, tree_pos + tree_offset, Block::Leaf); - } - } - } - } - //part above the tree - { - let leaf_above_height = 2; - let leaf_width = 1; - for tree_y in tree_height..(tree_height + leaf_above_height) { - for tree_x in (-leaf_width)..=leaf_width { - for tree_z in (-leaf_width)..=leaf_width { - let tree_offset = ivec3(tree_x, tree_y, tree_z); - smart_place(&mut blocks, tree_pos + tree_offset, Block::Leaf); - } - } - } - } - } - } + /// crude hash of self.seed and x + fn seeded_hash(&self, x: impl std::hash::Hash) -> u64 { + //use std::hash to hash the seed and x + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + x.hash(&mut hasher); + self.seed.hash(&mut hasher); + hasher.finish() + } + + pub fn new(chunk_position: IVec3, seed: u64) -> Self { + Self { + seed, + chunk_position, + blocks: Box::new([[[Block::Air; CHUNK_SIZE]; CHUNK_SIZE]; CHUNK_SIZE]), + queue: Vec::with_capacity(0), + data: Default::default(), } } - (blocks, queue) - - // let mut cave_noise = FastNoise::seeded(seed); - // cave_noise.set_fractal_type(FractalType::FBM); - // cave_noise.set_frequency(0.1); - - // let mut dirt_noise = FastNoise::seeded(seed.rotate_left(1)); - // dirt_noise.set_fractal_type(FractalType::FBM); - // dirt_noise.set_frequency(0.1); - - // - - // if chunk_position.y >= 0 { - // if chunk_position.y == 0 { - // for x in 0..CHUNK_SIZE { - // for z in 0..CHUNK_SIZE { - // blocks[x][0][z] = Block::Dirt; - // blocks[x][1][z] = Block::Grass; - // } - // } - // } - // } else { - // for x in 0..CHUNK_SIZE { - // for y in 0..CHUNK_SIZE { - // for z in 0..CHUNK_SIZE { - // let position = ivec3(x as i32, y as i32, z as i32) + offset; - // let v_cave_noise = cave_noise.get_noise3d(position.x as f32, position.y as f32, position.z as f32) * (-position.y as f32 - 10.0).clamp(0., 1.); - // let v_dirt_noise = dirt_noise.get_noise3d(position.x as f32, position.y as f32, position.z as f32) * (-position.y as f32).clamp(0., 1.); - // if v_cave_noise > 0.5 { - // blocks[x][y][z] = Block::Stone; - // } else if v_dirt_noise > 0.5 { - // blocks[x][y][z] = Block::Dirt; - // } - // } - // } - // } - // } - // blocks - + /// Generate the chunk. + /// + /// Will return `None` only if the generation was aborted. + pub fn generate(mut self, abort: Option>>) -> Option<(BlockData, Vec)> { + run_steps!(&mut self, abort, [ + steps::_01_terrain::TerrainStep, + steps::_02_water::WaterStep, + steps::_03_caves::CaveStep, + steps::_04_layers::LayersStep, + steps::_05_decorate::DecorateStep, + steps::_06_trees::TreesStep, + ]).then_some((self.blocks, self.queue)) + } +} + +pub fn generate_world(chunk_position: IVec3, seed: u64, abort: Option>>) -> Option<(BlockData, Vec)> { + //TODO: pass through None for abort + WorldGenerator::new(chunk_position, seed).generate(abort) } diff --git a/kubi-shared/src/worldgen/steps.rs b/kubi-shared/src/worldgen/steps.rs new file mode 100644 index 0000000..147f85d --- /dev/null +++ b/kubi-shared/src/worldgen/steps.rs @@ -0,0 +1,6 @@ +pub mod _01_terrain; +pub mod _02_water; +pub mod _03_caves; +pub mod _04_layers; +pub mod _05_decorate; +pub mod _06_trees; diff --git a/kubi-shared/src/worldgen/steps/_01_terrain.rs b/kubi-shared/src/worldgen/steps/_01_terrain.rs new file mode 100644 index 0000000..0a0bc36 --- /dev/null +++ b/kubi-shared/src/worldgen/steps/_01_terrain.rs @@ -0,0 +1,34 @@ +use fastnoise_lite::{FastNoiseLite, FractalType}; +use glam::ivec3; +use crate::{block::Block, chunk::CHUNK_SIZE}; +use super::super::{SeedThingy, WorldGenStep, WorldGenerator}; + +pub struct TerrainStep { + noise: FastNoiseLite, +} + +impl WorldGenStep for TerrainStep { + fn initialize(generator: &WorldGenerator) -> Self { + let mut seeder = SeedThingy::new(generator.seed); + let mut noise = FastNoiseLite::with_seed(seeder.next_seed()); + noise.set_fractal_type(Some(FractalType::FBm)); + noise.set_fractal_octaves(Some(4)); + noise.set_frequency(Some(0.003)); + Self { noise } + } + + fn generate(&mut self, gen: &mut WorldGenerator) { + let mut height_map = vec![vec![0; CHUNK_SIZE]; CHUNK_SIZE]; + for x in 0..CHUNK_SIZE as i32 { + for z in 0..CHUNK_SIZE as i32 { + let global_xz = gen.global_position(ivec3(x, 0, z)); + let height = (self.noise.get_noise_2d(global_xz.x as f64, global_xz.z as f64) * 32.0) as i32; + height_map[x as usize][z as usize] = height; + for y in 0..gen.local_height(height) { + gen.place(ivec3(x, y, z), Block::Stone); + } + } + } + gen.data.master_height_map = Some(height_map); + } +} diff --git a/kubi-shared/src/worldgen/steps/_02_water.rs b/kubi-shared/src/worldgen/steps/_02_water.rs new file mode 100644 index 0000000..80c6a3a --- /dev/null +++ b/kubi-shared/src/worldgen/steps/_02_water.rs @@ -0,0 +1,20 @@ +use glam::ivec3; +use crate::{block::Block, chunk::CHUNK_SIZE}; +use super::super::{WorldGenerator, WorldGenStep}; + +pub const WATER_LEVEL: i32 = 0; + +pub struct WaterStep; + +impl WorldGenStep for WaterStep { + fn initialize(_: &WorldGenerator) -> Self { Self } + fn generate(&mut self, gen: &mut WorldGenerator) { + for x in 0..CHUNK_SIZE as i32 { + for z in 0..CHUNK_SIZE as i32 { + for y in 0..gen.local_height(WATER_LEVEL) { + gen.place_if_empty(ivec3(x, y, z), Block::Water); + } + } + } + } +} diff --git a/kubi-shared/src/worldgen/steps/_03_caves.rs b/kubi-shared/src/worldgen/steps/_03_caves.rs new file mode 100644 index 0000000..41d0be2 --- /dev/null +++ b/kubi-shared/src/worldgen/steps/_03_caves.rs @@ -0,0 +1,49 @@ +use fastnoise_lite::{FastNoiseLite, FractalType}; +use glam::{ivec3, FloatExt, IVec3}; +use crate::{block::Block, chunk::CHUNK_SIZE}; +use super::super::{SeedThingy, WorldGenStep, WorldGenerator}; + +pub struct CaveStep { + a: FastNoiseLite, + b: FastNoiseLite, +} + +impl WorldGenStep for CaveStep { + fn initialize(gen: &WorldGenerator) -> Self { + let mut seeder = SeedThingy::new(gen.seed); + + let mut a = FastNoiseLite::with_seed(seeder.next_seed()); + a.set_fractal_type(Some(FractalType::FBm)); + a.set_fractal_octaves(Some(2)); + + let mut b = FastNoiseLite::with_seed(seeder.next_seed()); + b.set_fractal_type(Some(FractalType::FBm)); + b.set_fractal_octaves(Some(2)); + + Self { a, b } + } + + fn generate(&mut self, gen: &mut WorldGenerator) { + for x in 0..CHUNK_SIZE as i32 { + for y in 0..CHUNK_SIZE as i32 { + for z in 0..CHUNK_SIZE as i32 { + let cave_size = ((gen.offset().y + y - 50) as f64 / -200.).clamp(0., 1.) as f32; + let inv_cave_size = 1. - cave_size; + if cave_size < 0.1 { continue } + + let pos = ivec3(x, y, z); + if gen.query(pos) != Block::Stone { continue } + + let pos_global = gen.global_position(pos); + let noise_a = self.a.get_noise_3d(pos_global.x as f64, pos_global.y as f64, pos_global.z as f64) * 0.5 + 0.5; + let noise_b = self.b.get_noise_3d(pos_global.x as f64, pos_global.y as f64, pos_global.z as f64) * 0.5 + 0.5; + + if noise_a.min(noise_b) > (0.6 + 0.4 * inv_cave_size) { + gen.place(pos, Block::Air); + } + //TODO + } + } + } + } +} diff --git a/kubi-shared/src/worldgen/steps/_04_layers.rs b/kubi-shared/src/worldgen/steps/_04_layers.rs new file mode 100644 index 0000000..a80354f --- /dev/null +++ b/kubi-shared/src/worldgen/steps/_04_layers.rs @@ -0,0 +1,36 @@ +use glam::ivec3; +use crate::{block::Block, chunk::CHUNK_SIZE}; +use super::{ + _02_water::WATER_LEVEL, + super::{WorldGenStep, WorldGenerator} +}; + +pub struct LayersStep; + +impl WorldGenStep for LayersStep { + fn initialize(_: &WorldGenerator) -> Self { Self } + + fn generate(&mut self, gen: &mut WorldGenerator) { + for x in 0..CHUNK_SIZE as i32 { + for z in 0..CHUNK_SIZE as i32 { + let terrain_height = gen.data.master_height_map.as_ref().unwrap()[x as usize][z as usize]; + + // Dirt layer height, naturally gets thinner as height gets deeper + let mut dirt_layer_height = (((terrain_height as f32 + 15.) / 20.).clamp(0., 1.) * 8.).round() as i32; + dirt_layer_height -= (gen.seeded_hash((x, z, 1)) & 1) as i32; //+ (gen.seeded_hash((x, z, 0xbau8)) & 1) as i32; + + // Place dirt layer + for y in gen.local_height(terrain_height - dirt_layer_height)..gen.local_height(terrain_height) { + gen.place(ivec3(x, y, z), Block::Dirt); + } + + // If above water level, place grass + if terrain_height >= WATER_LEVEL { + if let Some(local_y) = gen.local_y_position(terrain_height - 1) { + gen.place(ivec3(x, local_y, z), Block::Grass); + } + } + } + } + } +} diff --git a/kubi-shared/src/worldgen/steps/_05_decorate.rs b/kubi-shared/src/worldgen/steps/_05_decorate.rs new file mode 100644 index 0000000..20e9b32 --- /dev/null +++ b/kubi-shared/src/worldgen/steps/_05_decorate.rs @@ -0,0 +1,31 @@ +use glam::ivec3; +use crate::{block::Block, chunk::CHUNK_SIZE}; +use super::{ + _02_water::WATER_LEVEL, + super::{WorldGenStep, WorldGenerator}, +}; + +pub struct DecorateStep; + +impl WorldGenStep for DecorateStep { + fn initialize(_: &WorldGenerator) -> Self { Self } + + fn generate(&mut self, gen: &mut WorldGenerator) { + for x in 0..CHUNK_SIZE as i32 { + for z in 0..CHUNK_SIZE as i32 { + let global_xz = gen.global_position(ivec3(x, 0, z)); + + let terrain_height = gen.data.master_height_map.as_ref().unwrap()[x as usize][z as usize]; + + //Place tall grass + if terrain_height >= WATER_LEVEL { + if let Some(local_y) = gen.local_y_position(terrain_height) { + if (gen.seeded_hash((global_xz.x, global_xz.z)) & 0xf) == 0xf { + gen.place_if_empty(ivec3(x, local_y, z), Block::TallGrass); + } + } + } + } + } + } +} diff --git a/kubi-shared/src/worldgen/steps/_06_trees.rs b/kubi-shared/src/worldgen/steps/_06_trees.rs new file mode 100644 index 0000000..08b547b --- /dev/null +++ b/kubi-shared/src/worldgen/steps/_06_trees.rs @@ -0,0 +1,45 @@ +use bincode::de; +use fastnoise_lite::{FastNoiseLite, NoiseType}; +use glam::ivec3; +use crate::{block::Block, chunk::CHUNK_SIZE, worldgen::SeedThingy}; +use super::_02_water::WATER_LEVEL; +use crate::worldgen::{ + WorldGenStep, WorldGenerator, + structures::{Structure, TreeStructure}, +}; + + +pub struct TreesStep { + density_noise: FastNoiseLite, +} + +impl WorldGenStep for TreesStep { + fn initialize(gen: &WorldGenerator) -> Self { + let mut seeder = SeedThingy::new(gen.seed.rotate_left(5)); + let mut density_noise = FastNoiseLite::with_seed(seeder.next_seed()); + density_noise.set_noise_type(Some(NoiseType::OpenSimplex2)); + density_noise.set_frequency(Some(0.008)); + Self { density_noise } + } + + fn generate(&mut self, gen: &mut WorldGenerator) { + for x in 0..CHUNK_SIZE as i32 { + for z in 0..CHUNK_SIZE as i32 { + let terrain_height = gen.data.master_height_map.as_ref().unwrap()[x as usize][z as usize]; + if terrain_height < WATER_LEVEL { continue } + + let global_xz = gen.global_position(ivec3(x, 0, z)); + let mut density = self.density_noise.get_noise_2d(global_xz.x as f64, global_xz.z as f64) * 0.5 + 0.5; + density = density.powi(3); + if gen.seeded_hash((global_xz.x, global_xz.z, 0xfef)) & 0xff >= (density * 7.).round() as u64 { + continue + } + + let tree = TreeStructure::default(); + if let Some(local_y) = gen.local_y_position(terrain_height) { + tree.place(gen, ivec3(x, local_y, z)); + } + } + } + } +} diff --git a/kubi-shared/src/worldgen/structures.rs b/kubi-shared/src/worldgen/structures.rs new file mode 100644 index 0000000..744cab6 --- /dev/null +++ b/kubi-shared/src/worldgen/structures.rs @@ -0,0 +1,9 @@ +use glam::IVec3; +use super::WorldGenerator; + +mod tree; +pub use tree::TreeStructure; + +pub trait Structure { + fn place(&self, gen: &mut WorldGenerator, root_pos: IVec3); +} diff --git a/kubi-shared/src/worldgen/structures/tree.rs b/kubi-shared/src/worldgen/structures/tree.rs new file mode 100644 index 0000000..78083b3 --- /dev/null +++ b/kubi-shared/src/worldgen/structures/tree.rs @@ -0,0 +1,58 @@ +use glam::IVec3; +use super::Structure; +use crate::{block::Block, worldgen::WorldGenerator}; + +#[derive(Clone, Copy, Debug)] +pub struct TreeStructure { + pub height: i32, +} + +impl Default for TreeStructure { + fn default() -> Self { + Self { height: 5 } + } +} + +impl Structure for TreeStructure { + fn place(&self, gen: &mut WorldGenerator, root: IVec3) { + //check the block below the tree, if it's grass, replace it with dirt + //XXX: This won't work if root.y == 0 + if root.y != 0 && gen.query(root - IVec3::Y) == Block::Grass { + gen.place(root - IVec3::Y, Block::Dirt); + } + + //Tree stem + for y in root.y..root.y + self.height { + gen.place_or_queue(IVec3::new(root.x, y, root.z), Block::Wood); + } + + //Tree leaves + //Try to create the following shape: + //(a 5x2x5 cube that wraps around the stem with a 3x1x3 cube on top) + // xxx + // xx|xx + // xx|xx + // | + + for y in 0..=4_i32 { + for x in -2..=2_i32 { + for z in -2..=2_i32 { + //Do not overwrite the stem + if y < 3 && x == 0 && z == 0 { + continue + } + // Cut off the corners of the top layer + if y >= 3 && (x.abs() > 1 || z.abs() > 1) { + continue + } + let position = IVec3::new( + root.x + x, + root.y + self.height - 3 + y, + root.z + z + ); + gen.place_or_queue(position, Block::Leaf); + } + } + } + } +} diff --git a/kubi/Cargo.toml b/kubi/Cargo.toml index 5b54d87..ed59a2c 100644 --- a/kubi/Cargo.toml +++ b/kubi/Cargo.toml @@ -37,6 +37,7 @@ static_assertions = "1.1" tinyset = "0.4" serde_json = { version = "1.0", optional = true } #only used for `generate_visualizer_data` rand = { version = "0.8", features = ["alloc", "small_rng"]} +atomic = "0.6" [target.'cfg(target_os = "android")'.dependencies] android-activity = "^0.5.2" diff --git a/kubi/src/world/chunk.rs b/kubi/src/world/chunk.rs index 07b2049..e8ddd3a 100644 --- a/kubi/src/world/chunk.rs +++ b/kubi/src/world/chunk.rs @@ -1,5 +1,8 @@ +use std::sync::Arc; use glam::IVec3; +use atomic::Atomic; use glium::{VertexBuffer, IndexBuffer}; +use kubi_shared::worldgen::AbortState; use crate::rendering::world::ChunkVertex; pub use kubi_shared::chunk::{CHUNK_SIZE, BlockData}; @@ -39,7 +42,7 @@ pub enum DesiredChunkState { Nothing, Loaded, Rendered, - ToUnload, + Unloaded, } impl DesiredChunkState { pub fn matches_current(self, current: CurrentChunkState) -> bool { @@ -55,8 +58,10 @@ pub struct Chunk { pub mesh_index: Option, pub current_state: CurrentChunkState, pub desired_state: DesiredChunkState, + pub abortion: Option>>, pub mesh_dirty: bool, } + impl Chunk { pub fn new(position: IVec3) -> Self { Self { @@ -65,6 +70,7 @@ impl Chunk { mesh_index: None, current_state: Default::default(), desired_state: Default::default(), + abortion: None, mesh_dirty: false, } } diff --git a/kubi/src/world/loading.rs b/kubi/src/world/loading.rs index 1d4face..5798408 100644 --- a/kubi/src/world/loading.rs +++ b/kubi/src/world/loading.rs @@ -1,6 +1,8 @@ +use std::sync::Arc; +use atomic::{Atomic, Ordering}; use glam::{IVec3, ivec3}; use glium::{VertexBuffer, IndexBuffer, index::PrimitiveType}; -use kubi_shared::networking::messages::ClientToServerMessage; +use kubi_shared::{networking::messages::ClientToServerMessage, worldgen::AbortState}; use shipyard::{View, UniqueView, UniqueViewMut, IntoIter, Workload, IntoWorkload, NonSendSync, track}; use uflow::SendMode; use crate::{ @@ -15,10 +17,10 @@ use super::{ ChunkStorage, ChunkMeshStorage, chunk::{Chunk, DesiredChunkState, CHUNK_SIZE, ChunkMesh, CurrentChunkState, ChunkData}, tasks::{ChunkTaskManager, ChunkTaskResponse, ChunkTask}, - queue::BlockUpdateQueue + queue::BlockUpdateQueue, }; -const MAX_CHUNK_OPS_INGAME: usize = 6; +const MAX_CHUNK_OPS_INGAME: usize = 8; const MAX_CHUNK_OPS: usize = 32; pub fn update_loaded_world_around_player() -> Workload { @@ -56,7 +58,7 @@ pub fn update_chunks_if_player_moved( //Then, mark *ALL* chunks with ToUnload for (_, chunk) in &mut vm_world.chunks { - chunk.desired_state = DesiredChunkState::ToUnload; + chunk.desired_state = DesiredChunkState::Unloaded; } //Then mark chunks that are near to the player @@ -99,13 +101,27 @@ fn unload_downgrade_chunks( //TODO refactor this //TODO unsubscibe if in multiplayer vm_world.chunks.retain(|_, chunk| { - if chunk.desired_state == DesiredChunkState::ToUnload { + if chunk.desired_state == DesiredChunkState::Unloaded { if let Some(mesh_index) = chunk.mesh_index { vm_meshes.remove(mesh_index).unwrap(); } + if let Some(abortion) = &chunk.abortion { + let _ = abortion.compare_exchange( + AbortState::Continue, AbortState::Abort, + Ordering::Relaxed, Ordering::Relaxed + ); + } false } else { match chunk.desired_state { + DesiredChunkState::Nothing if matches!(chunk.current_state, CurrentChunkState::Loading) => { + if let Some(abortion) = &chunk.abortion { + let _ = abortion.compare_exchange( + AbortState::Continue, AbortState::Abort, + Ordering::Relaxed, Ordering::Relaxed + ); + } + }, DesiredChunkState::Loaded if matches!(chunk.current_state, CurrentChunkState::Rendered | CurrentChunkState::CalculatingMesh | CurrentChunkState::RecalculatingMesh) => { if let Some(mesh_index) = chunk.mesh_index { vm_meshes.remove(mesh_index).unwrap(); @@ -134,6 +150,7 @@ fn start_required_tasks( let chunk = world.chunks.get(&position).unwrap(); match chunk.desired_state { DesiredChunkState::Loaded | DesiredChunkState::Rendered if chunk.current_state == CurrentChunkState::Nothing => { + let mut abortion = None; //start load task if let Some(client) = &mut udp_client { client.0.send( @@ -144,14 +161,18 @@ fn start_required_tasks( SendMode::Reliable ); } else { + let atomic = Arc::new(Atomic::new(AbortState::Continue)); task_manager.spawn_task(ChunkTask::LoadChunk { seed: 0xbeef_face_dead_cafe, - position + position, + abortion: Some(Arc::clone(&atomic)), }); + abortion = Some(atomic); } //Update chunk state let chunk = world.chunks.get_mut(&position).unwrap(); chunk.current_state = CurrentChunkState::Loading; + chunk.abortion = abortion; // =========== //log::trace!("Started loading chunk {position}"); }, @@ -173,6 +194,7 @@ fn start_required_tasks( chunk.current_state = CurrentChunkState::CalculatingMesh; } chunk.mesh_dirty = false; + chunk.abortion = None; //Can never abort at this point // =========== //log::trace!("Started generating mesh for chunk {position}"); } @@ -195,14 +217,15 @@ fn process_completed_tasks( ChunkTaskResponse::LoadedChunk { position, chunk_data, mut queued } => { //check if chunk exists let Some(chunk) = world.chunks.get_mut(&position) else { + //to compensate, actually push the ops counter back by one log::warn!("blocks data discarded: chunk doesn't exist"); - return + continue }; //check if chunk still wants it if !matches!(chunk.desired_state, DesiredChunkState::Loaded | DesiredChunkState::Rendered) { log::warn!("block data discarded: state undesirable: {:?}", chunk.desired_state); - return + continue } //set the block data @@ -228,13 +251,13 @@ fn process_completed_tasks( //check if chunk exists let Some(chunk) = world.chunks.get_mut(&position) else { log::warn!("mesh discarded: chunk doesn't exist"); - return + continue }; //check if chunk still wants it if chunk.desired_state != DesiredChunkState::Rendered { log::warn!("mesh discarded: state undesirable: {:?}", chunk.desired_state); - return + continue } //apply the mesh diff --git a/kubi/src/world/tasks.rs b/kubi/src/world/tasks.rs index 6c22cdf..87bda90 100644 --- a/kubi/src/world/tasks.rs +++ b/kubi/src/world/tasks.rs @@ -1,6 +1,8 @@ +use std::sync::Arc; +use atomic::Atomic; use flume::{Sender, Receiver}; use glam::IVec3; -use kubi_shared::queue::QueuedBlock; +use kubi_shared::{queue::QueuedBlock, worldgen::AbortState}; use shipyard::Unique; use rayon::{ThreadPool, ThreadPoolBuilder}; use super::{ @@ -13,7 +15,8 @@ use crate::rendering::world::ChunkVertex; pub enum ChunkTask { LoadChunk { seed: u64, - position: IVec3 + position: IVec3, + abortion: Option>>, }, GenerateMesh { position: IVec3, @@ -67,8 +70,11 @@ impl ChunkTaskManager { trans_vertices, trans_indices, } }, - ChunkTask::LoadChunk { position, seed } => { - let (chunk_data, queued) = generate_world(position, seed); + ChunkTask::LoadChunk { position, seed, abortion } => { + let Some((chunk_data, queued)) = generate_world(position, seed, abortion) else { + log::warn!("aborted operation"); + return + }; ChunkTaskResponse::LoadedChunk { position, chunk_data, queued } } });