diff --git a/Cargo.lock b/Cargo.lock index 7f16037e5..4d557afc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,11 @@ name = "bitflags" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "bitvec" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "block-cipher-trait" version = "0.6.2" @@ -653,6 +658,7 @@ dependencies = [ "backtrace 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bitvec 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)", "bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", "crossbeam 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -682,12 +688,15 @@ dependencies = [ "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand-legacy 0.1.0", + "rand_xorshift 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "rayon 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "rsa 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "rsa-der 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", "shrev 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "simdeez 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", + "simdnoise 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "simple_logger 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", "specs 0.15.0 (git+https://github.com/slide-rs/specs)", @@ -1448,6 +1457,26 @@ dependencies = [ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "paste" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "paste-impl 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-hack 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "paste-impl" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro-hack 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "percent-encoding" version = "1.0.1" @@ -1703,6 +1732,14 @@ dependencies = [ "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rand_xorshift" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rand_xoshiro" version = "0.3.1" @@ -1984,6 +2021,22 @@ name = "shrev" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "simdeez" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "paste 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "simdnoise" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "simdeez 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "simple_asn1" version = "0.4.0" @@ -2530,6 +2583,7 @@ dependencies = [ "checksum backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "82a830b4ef2d1124a711c71d263c5abdc710ef8e907bd508c88be475cebc422b" "checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" "checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd" +"checksum bitvec 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)" = "461d7d0e952343f575470daeb04d38aad19675b4f170e122c6b5dd618612c8a8" "checksum block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774" "checksum bstr 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "94cdf78eb7e94c566c1f5dbe2abf8fc70a548fc902942a48c4b3a98b48ca9ade" "checksum bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ad807f2fc2bf185eeb98ff3a901bd46dc5ad58163d0fa4577ba0d25674d71708" @@ -2655,6 +2709,8 @@ dependencies = [ "checksum parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" "checksum parking_lot_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9" "checksum parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" +"checksum paste 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "423a519e1c6e828f1e73b720f9d9ed2fa643dce8a7737fb43235ce0b41eeaa49" +"checksum paste-impl 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "4214c9e912ef61bf42b81ba9a47e8aad1b2ffaf739ab162bf96d1e011f54e6c5" "checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" "checksum petgraph 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)" = "9c3659d1ee90221741f65dd128d9998311b0e40c5d3c23a62445938214abce4f" "checksum pkg-config 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c1d2cfa5a714db3b5f24f0915e74fcdf91d09d496ba61329705dda7774d2af" @@ -2683,6 +2739,7 @@ dependencies = [ "checksum rand_os 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a788ae3edb696cfcba1c19bfd388cc4b8c21f8a408432b199c072825084da58a" "checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" "checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +"checksum rand_xorshift 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "77d416b86801d23dde1aa643023b775c3a462efc0ed96443add11546cdf1dca8" "checksum rand_xoshiro 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0e18c91676f670f6f0312764c759405f13afb98d5d73819840cf72a518487bff" "checksum rawpointer 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ebac11a9d2e11f2af219b8b8d833b76b1ea0e054aa0e8d8e9e4cbde353bdf019" "checksum rayon 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "83a27732a533a1be0a0035a111fe76db89ad312f6f0347004c220c57f209a123" @@ -2715,6 +2772,8 @@ dependencies = [ "checksum sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" "checksum shred 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "034e46a7afee5639940a76f42f578d64f7d814a7e4bebe258b1ab23fed04474e" "checksum shrev 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b5752e017e03af9d735b4b069f53b7a7fd90fefafa04d8bd0c25581b0bff437f" +"checksum simdeez 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4204ae48b2a871f428dc20426f005be250413fa6263e6f3d93094a36b29504dc" +"checksum simdnoise 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "86a9e4c1c3369eab7105ac7e1582a601942fed0a63877cb2e1afcf57f34ed7b3" "checksum simple_asn1 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2b25ecba7165254f0c97d6c22a64b1122a03634b18d20a34daf21e18f892e618" "checksum simple_logger 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3a4756ecc75607ba957820ac0a2413a6c27e6c61191cda0c62c6dcea4da88870" "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" diff --git a/core/src/biomes.rs b/core/src/biomes.rs index 8bd200a79..dc0677d3a 100644 --- a/core/src/biomes.rs +++ b/core/src/biomes.rs @@ -1,4 +1,4 @@ -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumCount)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumCount, FromPrimitive, ToPrimitive)] pub enum Biome { Badlands, BadlandsPlateau, diff --git a/core/src/save/level.rs b/core/src/save/level.rs index d043b7ae1..cc44a60d2 100644 --- a/core/src/save/level.rs +++ b/core/src/save/level.rs @@ -54,7 +54,7 @@ pub struct LevelData { #[serde(rename = "rainTime")] pub rain_time: i32, #[serde(rename = "RandomSeed")] - pub random_seed: i64, + pub seed: i64, #[serde(rename = "SpawnX")] pub spawn_x: i32, diff --git a/core/src/world/chunk.rs b/core/src/world/chunk.rs index 458dae7fd..58f7c4c48 100644 --- a/core/src/world/chunk.rs +++ b/core/src/world/chunk.rs @@ -155,6 +155,12 @@ impl Chunk { self.sections.iter().map(|sec| sec.as_ref()).collect() } + /// Returns a mutable slice of the 16 sections + /// in this chunk. + pub fn sections_mut(&mut self) -> Vec> { + self.sections.iter_mut().map(|sec| sec.as_mut()).collect() + } + /// Returns the position in chunk coordinates /// of this chunk. pub fn position(&self) -> ChunkPosition { @@ -524,6 +530,14 @@ impl ChunkSection { pub fn block_light(&self) -> &BitArray { &self.block_light } + + pub fn sky_light_mut(&mut self) -> &mut BitArray { + &mut self.sky_light + } + + pub fn block_light_mut(&mut self) -> &mut BitArray { + &mut self.block_light + } } impl Default for ChunkSection { diff --git a/generator/src/biome.rs b/generator/src/biome.rs index f0966d771..8b32d75a4 100644 --- a/generator/src/biome.rs +++ b/generator/src/biome.rs @@ -84,7 +84,7 @@ pub fn generate_rust(input: &str, output: &str) -> Result<(), Error> { } let code = quote! { - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumCount)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumCount, FromPrimitive, ToPrimitive)] pub enum Biome { #(#enum_variants)* } diff --git a/server/Cargo.toml b/server/Cargo.toml index 85a47f456..c65b840cd 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -27,6 +27,7 @@ rsa = "0.1.3" num-bigint = { version = "0.4", features = ["rand", "i128", "u64_digit", "prime", "zeroize"], package = "num-bigint-dig" } rsa-der = "0.2.1" rand = "0.7.0" +rand_xorshift = "0.2.0" rand-legacy = { path = "../util/rand-legacy" } bytes = "0.4.12" hashbrown = { version = "0.6.0", features = ["rayon"] } @@ -60,6 +61,9 @@ thread_local = "0.3.6" parking_lot = "0.9.0" heapless = "0.5.1" strum = "0.15.0" +simdnoise = "3.1.1" +simdeez = "0.6.4" +bitvec = "0.15.1" [features] nightly = ["specs/nightly", "parking_lot/nightly"] diff --git a/server/src/main.rs b/server/src/main.rs index 54c3461ac..3d6ae56ca 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -2,7 +2,6 @@ // tuples as their SystemData, and Clippy // doesn't seem to like this. #![allow(clippy::type_complexity)] -#![forbid(unsafe_code)] #[macro_use] extern crate log; @@ -26,7 +25,8 @@ extern crate feather_codegen; extern crate bitflags; #[macro_use] extern crate feather_core; -extern crate feather_blocks; +#[macro_use] +extern crate bitvec; extern crate nalgebra_glm as glm; @@ -46,7 +46,9 @@ use crate::network::send_packet_to_player; use crate::player::PlayerDisconnectEvent; use crate::systems::{BROADCASTER, ITEM_SPAWN, JOIN_HANDLER, NETWORK, PLAYER_INIT, SPAWNER}; use crate::util::Util; -use crate::worldgen::{EmptyWorldGenerator, SuperflatWorldGenerator, WorldGenerator}; +use crate::worldgen::{ + ComposableGenerator, EmptyWorldGenerator, SuperflatWorldGenerator, WorldGenerator, +}; use backtrace::Backtrace; use feather_core::level; use feather_core::level::{LevelData, LevelGeneratorType}; @@ -258,6 +260,9 @@ fn init_world<'a, 'b>( LevelGeneratorType::Flat => Arc::new(SuperflatWorldGenerator { options: level.clone().generator_options.unwrap_or_default(), }), + LevelGeneratorType::Default => { + Arc::new(ComposableGenerator::default_with_seed(level.seed as u64)) + } _ => Arc::new(EmptyWorldGenerator {}), }; world.insert(level); diff --git a/server/src/physics/entity.rs b/server/src/physics/entity.rs index 329a22352..f1b4711d8 100644 --- a/server/src/physics/entity.rs +++ b/server/src/physics/entity.rs @@ -43,7 +43,6 @@ impl<'a> System<'a> for EntityPhysicsSystem { chunk_map, entities, ) = data; - // Go through entities and update their positions according // to their velocities. diff --git a/server/src/worldgen/biomes/distorted_voronoi.rs b/server/src/worldgen/biomes/distorted_voronoi.rs new file mode 100644 index 000000000..e8fda1ba7 --- /dev/null +++ b/server/src/worldgen/biomes/distorted_voronoi.rs @@ -0,0 +1,129 @@ +use crate::worldgen::voronoi::VoronoiGrid; +use crate::worldgen::{BiomeGenerator, ChunkBiomes}; +use feather_core::{Biome, ChunkPosition}; +use num_traits::FromPrimitive; +use rand::{Rng, SeedableRng}; +use rand_xorshift::XorShiftRng; +use strum::EnumCount; + +/// Biome grid generator based on a distorted Voronoi +/// noise. +#[derive(Default)] +pub struct DistortedVoronoiBiomeGenerator; + +impl BiomeGenerator for DistortedVoronoiBiomeGenerator { + fn generate_for_chunk(&self, chunk: ChunkPosition, seed: u64) -> ChunkBiomes { + let mut voronoi = VoronoiGrid::new(384, seed); + + let mut biomes = ChunkBiomes::from_array([Biome::Plains; 16 * 16]); // Will be overridden + + // Noise is used to distort each coordinate. + /*let x_noise = + NoiseBuilder::gradient_2d_offset(chunk.x as f32 * 16.0, 16, chunk.z as f32 * 16.0, 16) + .with_seed(seed as i32 + 1) + .generate_scaled(-4.0, 4.0); + let z_noise = + NoiseBuilder::gradient_2d_offset(chunk.x as f32 * 16.0, 16, chunk.z as f32 * 16.0, 16) + .with_seed(seed as i32 + 2) + .generate_scaled(-4.0, 4.0);*/ + + for x in 0..16 { + for z in 0..16 { + // Apply distortion to coordinate before passing to voronoi + // generator. + //let distort_x = x_noise[(z << 4) | x] as i32 * 8; + //let distort_z = z_noise[(z << 4) | x] as i32 * 8; + + let distort_x = 0; + let distort_z = 0; + + let (closest_x, closest_y) = voronoi.get( + (chunk.x * 16) + x as i32 + distort_x, + (chunk.z * 16) + z as i32 + distort_z, + ); + + // Shift around the closest_x and closest_y values + // and deterministically select a biome based on the + // computed value. Continue shifting the value until + // a valid biome is computed. + let combined = (i64::from(closest_x) << 32) | i64::from(closest_y); + let mut rng = XorShiftRng::seed_from_u64(combined as u64); + + loop { + let shifted: u64 = rng.gen(); + + let biome = Biome::from_u64(shifted % Biome::count() as u64).unwrap(); + if is_biome_allowed(biome) { + biomes.set_biome_at(x, z, biome); + break; + } + } + } + } + + biomes + } +} + +/// Returns whether the given biome is allowed in the overworld. +fn is_biome_allowed(biome: Biome) -> bool { + match biome { + Biome::TheEnd + | Biome::TheVoid + | Biome::Nether + | Biome::SmallEndIslands + | Biome::EndBarrens + | Biome::EndHighlands + | Biome::EndMidlands => false, + _ => true, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_not_all_plains() { + // Check that the `ChunkBiomes` was overridden correctly. + let gen = DistortedVoronoiBiomeGenerator::default(); + + let chunk = ChunkPosition::new(5433, 132); + + let biomes = gen.generate_for_chunk(chunk, 8344); + + println!("{:?}", biomes); + + let mut num_plains = 0; + for x in 0..16 { + for z in 0..16 { + if biomes.biome_at(x, z) == Biome::Plains { + num_plains += 1; + } + } + } + + assert_ne!(num_plains, 16 * 16); + } + + #[test] + fn test_deterministic() { + // Check that the result is always deterministic. + let gen = DistortedVoronoiBiomeGenerator::default(); + + let chunk = ChunkPosition::new(0, 0); + + let seed = 52; + let first = gen.generate_for_chunk(chunk, seed); + + for _ in 0..5 { + let next = gen.generate_for_chunk(chunk, seed); + + for x in 0..16 { + for z in 0..16 { + assert_eq!(first.biome_at(x, z), next.biome_at(x, z)); + } + } + } + } +} diff --git a/server/src/worldgen/biomes/mod.rs b/server/src/worldgen/biomes/mod.rs new file mode 100644 index 000000000..666e9fd5e --- /dev/null +++ b/server/src/worldgen/biomes/mod.rs @@ -0,0 +1,7 @@ +//! Biome grid creation. + +mod distorted_voronoi; +mod two_level; + +pub use distorted_voronoi::DistortedVoronoiBiomeGenerator; +pub use two_level::TwoLevelBiomeGenerator; diff --git a/server/src/worldgen/biomes/two_level.rs b/server/src/worldgen/biomes/two_level.rs new file mode 100644 index 000000000..0581d887d --- /dev/null +++ b/server/src/worldgen/biomes/two_level.rs @@ -0,0 +1,68 @@ +use crate::worldgen::voronoi::VoronoiGrid; +use crate::worldgen::{voronoi, BiomeGenerator, ChunkBiomes}; +use feather_core::{Biome, ChunkPosition}; + +lazy_static! { + /// Array of biome groups, each containing biomes + /// which may appear next to each other. This is used in the + /// two-level biome generator. + static ref BIOME_GROUPS: Vec> = { + vec![ + vec![Biome::SnowyTundra, Biome::SnowyTaiga], + vec![Biome::Plains, Biome::BirchForest, Biome::Forest, Biome::Taiga, Biome::Mountains, Biome::Swamp, Biome::DarkForest], + vec![Biome::Savanna, Biome::Desert], + ] + }; +} + +/// Biome grid generator which works using two layers +/// of Voronoi. The first layer defines the biome group, +/// and the second determines which biome inside that group +/// to use. This technique allows similar biomes to be grouped +/// together and prevents unrelated biomes from being neighbors. +#[derive(Default)] +pub struct TwoLevelBiomeGenerator; + +impl BiomeGenerator for TwoLevelBiomeGenerator { + fn generate_for_chunk(&self, chunk: ChunkPosition, seed: u64) -> ChunkBiomes { + // Voronoi used to determine biome group + let mut group_voronoi = VoronoiGrid::new(1024, seed); + // Voronoi used to determine biome within group + let mut local_voronoi = VoronoiGrid::new(256, seed + 1); + + let mut biomes = ChunkBiomes::from_array([Biome::Plains; 16 * 16]); // Will be overridden + + let num_groups = BIOME_GROUPS.len(); + + // TODO: distort voronoi + + for x in 0..16 { + for z in 0..16 { + // Compute biome group + let possible_biomes = { + let (closest_x, closest_z) = + group_voronoi.get(chunk.x * 16 + x, chunk.z * 16 + z); + + let group_index = voronoi::shuffle(closest_x, closest_z, 0, num_groups); + + &BIOME_GROUPS[group_index] + }; + + // Compute biome within group + let biome = { + let (closest_x, closest_z) = + local_voronoi.get(chunk.x * 16 + x, chunk.z * 16 + z); + + let biome_index = + voronoi::shuffle(closest_x, closest_z, 0, possible_biomes.len()); + + possible_biomes[biome_index] + }; + + biomes.set_biome_at(x as usize, z as usize, biome); + } + } + + biomes + } +} diff --git a/server/src/worldgen/composition.rs b/server/src/worldgen/composition.rs new file mode 100644 index 000000000..865f85cc8 --- /dev/null +++ b/server/src/worldgen/composition.rs @@ -0,0 +1,200 @@ +//! Composition generator, used to populate chunks with blocks +//! based on the density and biome values. + +use crate::worldgen::{block_index, util, ChunkBiomes, CompositionGenerator, SEA_LEVEL}; +use bitvec::slice::BitSlice; +use feather_blocks::{GrassBlockData, MyceliumData, WaterData}; +use feather_core::{Biome, Block, Chunk, ChunkPosition}; +use rand::{Rng, SeedableRng}; +use rand_xorshift::XorShiftRng; +use std::cmp::min; + +/// A composition generator which generates basic +/// terrain based on biome values. +#[derive(Debug, Default)] +pub struct BasicCompositionGenerator; + +impl CompositionGenerator for BasicCompositionGenerator { + fn generate_for_chunk( + &self, + chunk: &mut Chunk, + _pos: ChunkPosition, + biomes: &ChunkBiomes, + density: &BitSlice, + seed: u64, + ) { + // For each column in the chunk, go from top to + // bottom. The first time a block density value is set to `true`, + // set it and the next three blocks to dirt. After that, use + // stone. + for x in 0..16 { + for z in 0..16 { + basic_composition_for_column(x, z, chunk, density, seed, biomes.biome_at(x, z)); + } + } + } +} + +fn basic_composition_for_column( + x: usize, + z: usize, + chunk: &mut Chunk, + density: &BitSlice, + seed: u64, + biome: Biome, +) { + basic_composition_for_solid_biome(x, z, chunk, density, seed, biome); +} + +fn basic_composition_for_solid_biome( + x: usize, + z: usize, + chunk: &mut Chunk, + density: &BitSlice, + seed: u64, + biome: Biome, +) { + let mut rng = + XorShiftRng::seed_from_u64(util::shuffle_seed_for_column(seed, chunk.position(), x, z)); + + let top_soil = top_soil_block(biome); + + let mut topsoil_remaining = -1; + let mut water_level = 0; // `level` block data starts at 0 and skips to min(8+n, 15) for each level of water downward + for y in (0..256).rev() { + let mut block = Block::Air; + + let is_solid = density[block_index(x, y, z)]; + + let mut skip = false; + + if biome == Biome::Ocean { + if y <= SEA_LEVEL && !is_solid { + block = Block::Water(WaterData { level: water_level }); + if water_level == 0 { + water_level = 8; + } else { + water_level = min(water_level + 1, 15); + } + skip = true; + } else if y >= SEA_LEVEL { + continue; // Leave at air - no blocks above sea level in ocean + } + } + + if !skip { + if y <= rng.gen_range(0, 4) { + block = Block::Bedrock; + } else { + block = if is_solid { + if topsoil_remaining == -1 { + topsoil_remaining = 3; + top_soil + } else if topsoil_remaining > 0 { + let block = underneath_top_soil_block(biome); + topsoil_remaining -= 1; + block + } else { + Block::Stone + } + } else { + topsoil_remaining = -1; + Block::Air + }; + } + } + + if block != Block::Air { + chunk.set_block_at(x, y, z, block); + } + } +} + +/// Returns the top soil block for the given biome. +fn top_soil_block(biome: Biome) -> Block { + match biome { + Biome::SnowyTundra + | Biome::IceSpikes + | Biome::SnowyTaiga + | Biome::SnowyTaigaMountains + | Biome::SnowyBeach => Block::GrassBlock(GrassBlockData { snowy: true }), + Biome::GravellyMountains | Biome::ModifiedGravellyMountains => Block::Gravel, + Biome::StoneShore => Block::Stone, + Biome::Beach | Biome::Desert | Biome::DesertHills | Biome::DesertLakes => Block::Sand, + Biome::MushroomFields | Biome::MushroomFieldShore => { + Block::Mycelium(MyceliumData { snowy: false }) + } + Biome::Badlands + | Biome::ErodedBadlands + | Biome::WoodedBadlandsPlateau + | Biome::BadlandsPlateau + | Biome::ModifiedBadlandsPlateau + | Biome::ModifiedWoodedBadlandsPlateau => Block::RedSand, + Biome::Ocean => Block::Sand, + _ => Block::GrassBlock(GrassBlockData::default()), + } +} + +/// Returns the block under the top soil block for the given biome. +fn underneath_top_soil_block(biome: Biome) -> Block { + match biome { + Biome::SnowyBeach => Block::SnowBlock, + Biome::GravellyMountains | Biome::ModifiedGravellyMountains => Block::Gravel, + Biome::StoneShore => Block::Stone, + Biome::Beach | Biome::Desert | Biome::DesertHills | Biome::DesertLakes => Block::Sandstone, + Biome::MushroomFields | Biome::MushroomFieldShore => Block::Dirt, + Biome::Badlands + | Biome::ErodedBadlands + | Biome::WoodedBadlandsPlateau + | Biome::BadlandsPlateau + | Biome::ModifiedBadlandsPlateau + | Biome::ModifiedWoodedBadlandsPlateau => Block::RedSandstone, + Biome::Ocean => Block::Sand, + _ => Block::Dirt, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_composition_for_column() { + let mut density = bitvec![0; 16 * 256 * 16]; + + let x = 0; + let z = 0; + + for y in 0..=32 { + density.set(block_index(x, y, z), true); + } + + for y in 40..=64 { + density.set(block_index(x, y, z), true); + } + + let mut chunk = Chunk::new(ChunkPosition::new(0, 0)); + basic_composition_for_column(x, z, &mut chunk, &density[..], 435, Biome::Plains); + + for y in 4..=28 { + assert_eq!(chunk.block_at(x, y, z), Block::Stone); + } + + for y in 29..=31 { + assert_eq!(chunk.block_at(x, y, z), Block::Dirt); + } + + for y in 33..40 { + assert_eq!(chunk.block_at(x, y, z), Block::Air); + } + + for y in 40..=60 { + assert_eq!(chunk.block_at(x, y, z), Block::Stone); + } + + assert_eq!( + chunk.block_at(x, 64, z), + Block::GrassBlock(GrassBlockData::default()) + ); + } +} diff --git a/server/src/worldgen/density_map/density.rs b/server/src/worldgen/density_map/density.rs new file mode 100644 index 000000000..f559a3aa8 --- /dev/null +++ b/server/src/worldgen/density_map/density.rs @@ -0,0 +1,256 @@ +//! Implements a density map generator using 3D Perlin noise. +//! +//! Over the 2D height map generator, this has the advantage that terrain +//! is more interesting; overhangs and the like will be able to generate. + +use crate::worldgen::{block_index, noise, DensityMapGenerator, NearbyBiomes, NoiseLerper}; +use bitvec::vec::BitVec; +use feather_core::{Biome, ChunkPosition}; +use simdnoise::NoiseBuilder; + +/// A density map generator using 3D Perlin noise. +/// +/// This generator should be used over the height map generator +/// when seeking correct-looking worlds; +/// +/// # Implementation +/// Density calculation works as follows: +/// * Generate a base 3D Perlin nosie with settings depending +/// on the biome. Use linear interpolation on noise (this +/// is handled by `Wrapped3DPerlinNoise`)`. +/// * Depending on the density value from the noise, decide +/// whether the position is solid or air. +#[derive(Debug, Default)] +pub struct DensityMapGeneratorImpl; + +impl DensityMapGenerator for DensityMapGeneratorImpl { + fn generate_for_chunk(&self, chunk: ChunkPosition, biomes: &NearbyBiomes, seed: u64) -> BitVec { + let mut density = bitvec![0; 16 * 256 * 16]; + + let uninterpolated_densities = generate_density(chunk, &biomes, seed); + let noise = NoiseLerper::new(&uninterpolated_densities) + .with_offset(chunk.x, chunk.z) + .generate(); + + for x in 0..16 { + for y in 0..256 { + for z in 0..16 { + let value = noise[noise::index(x, y, z)]; + + let is_solid = value < 0.0; + let index = block_index(x, y, z); + density.set(index, is_solid); + } + } + } + + density + } +} + +const DENSITY_WIDTH: usize = 5; +const DENSITY_HEIGHT: usize = 33; + +/// Generates a 5x33x5 density array to pass to `NoiseLerper`. +/// +/// This is based on Cuberite's implementation of the same function. +/// It works by having two 3D density noises, with another noise +/// to interpolate between the values of each. It then uses +/// a vertical linear gradient to determine densities of the +/// subchunks at each given Y level for a column. +/// +/// # Notes +/// The density values emitted from this function should +/// be considered solid if less than 0 and air if greater +/// than 0. This is contrary to what might seem logical. +fn generate_density(chunk: ChunkPosition, biomes: &NearbyBiomes, seed: u64) -> Vec { + // TODO: generate based on biome + + let x_offset = (chunk.x * (DENSITY_WIDTH as i32 - 1)) as f32; + let y_offset = 0.0; + let z_offset = (chunk.z * (DENSITY_WIDTH as i32 - 1)) as f32; + let len = DENSITY_WIDTH; + let height = DENSITY_HEIGHT; + + let noise_seed = seed as i32; + + // Generate various noises. + let choice_noise = NoiseBuilder::fbm_3d_offset(x_offset, len, y_offset, height, z_offset, len) + .with_seed(noise_seed) + .with_octaves(2) + .with_freq(0.001) + .generate() + .0; + let density_noise_1 = + NoiseBuilder::fbm_3d_offset(x_offset, len, y_offset, height, z_offset, len) + .with_seed(noise_seed + 1) + .with_octaves(2) + .with_freq(0.2) + .generate() + .0; + let density_noise_2 = + NoiseBuilder::fbm_3d_offset(x_offset, len, y_offset, height, z_offset, len) + .with_seed(noise_seed + 2) + .with_octaves(2) + .with_freq(0.2) + .generate() + .0; + // Additional 2D height noise for extra detail. + let height_noise = NoiseBuilder::fbm_2d_offset(x_offset, len, z_offset, len) + .with_seed(noise_seed + 3) + .with_octaves(2) + .with_freq(0.001) + .generate() + .0; + + let mut result = vec![0.0; DENSITY_WIDTH * DENSITY_HEIGHT * DENSITY_WIDTH]; + + // Loop through subchunks and generate density for each. + for subx in 0..DENSITY_WIDTH { + for subz in 0..DENSITY_WIDTH { + // TODO: average nearby biome parameters + let (amplitude, midpoint) = column_parameters(&biomes, subx, subz); + + let height = height_noise[(subz * len) + subx] * 25.0; + + // Loop through Y axis of this subchunk column. + for suby in 0..DENSITY_HEIGHT { + // Linear gradient used to offset based on height. + let mut height_offset = ((suby as f32 * 8.0) - midpoint) * amplitude; + + // If we are below the midpoint, increase the slope of the gradient. + // This creates smoother terrain. + if height_offset < 0.0 { + height_offset *= 4.0; + } + + // When we are near sky limit, decrease + // the slope. This ensures that very tall + // mountains don't artificially cut off + // at Y=256. + if suby > 26 { + height_offset += (suby as f32 - 28.0) / 4.0; + } + + let index = DENSITY_WIDTH * suby + subx + DENSITY_WIDTH * DENSITY_HEIGHT * subz; + + let choice = choice_noise[index] * 100.0; + let density_1 = density_noise_1[index] * 50.0; + let density_2 = density_noise_2[index] * 50.0; + + // Average between two density values based on choice weight. + result[index] = lerp(density_1, density_2, choice) + height_offset + height; + } + } + } + + result +} + +lazy_static! { + /// Elevation height field, used to weight + /// the averaging of nearby biome heights. + static ref ELEVATION_WEIGHT: [[f32; 19]; 19] = { + let mut array = [[0.0; 19]; 19]; + for (x, values) in array.iter_mut().enumerate() { + for (z, value) in values.iter_mut().enumerate() { + let mut x_squared = x as i32 - 9; + x_squared *= x_squared; + let mut z_sqaured = z as i32 - 9; + z_sqaured *= z_sqaured; + *value = 10.0 / (x_squared as f32 + z_sqaured as f32 + 0.2).sqrt(); + } + } + array + }; +} + +/// Computes the target amplitude and midpoint for the +/// given column, using a 9x9 grid of biomes +/// around the column to determine a weighted average. +/// +/// The X and Z parameters are the coordinates of the subchunk +/// within the chunk, not the block coordinate. +fn column_parameters(biomes: &NearbyBiomes, x: usize, z: usize) -> (f32, f32) { + let x = x as i32 * (DENSITY_WIDTH as i32 - 1); + let z = z as i32 * (DENSITY_WIDTH as i32 - 1); + + let mut sum_amplitudes = 0.0; + let mut sum_midpoints = 0.0; + let mut sum_weights = 0.0; + + // Loop through columns in 9x9 grid and compute weighted average of amplitudes + // and midpoints. + for block_x in -9..=9 { + for block_z in -9..=9 { + let abs_x = x + block_x; + let abs_z = z + block_z; + + let biome = biomes.biome_at(abs_x, abs_z); + let (amplitude, midpoint) = biome_parameters(biome); + + let weight = ELEVATION_WEIGHT[(block_x + 9) as usize][(block_z + 9) as usize]; + + sum_amplitudes += amplitude * weight; + sum_midpoints += midpoint * weight; + sum_weights += weight; + } + } + + sum_amplitudes /= sum_weights; + sum_midpoints /= sum_weights; + + (sum_amplitudes, sum_midpoints) +} + +/// Returns the amplitude and midpoint for a given biome +/// type as a tuple in that order. +/// +/// All original values were taken from Cuberite's source, +/// so all credit for this function goes to their team +/// for the presumably highly laborious effort +/// involved in finding these values. +fn biome_parameters(biome: Biome) -> (f32, f32) { + match biome { + Biome::Beach => (0.2, 60.0), + Biome::BirchForest => (0.1, 64.0), + Biome::BirchForestHills => (0.075, 64.0), + Biome::TallBirchHills => (0.075, 68.0), + Biome::TallBirchForest => (0.1, 64.0), + Biome::SnowyBeach => (0.3, 62.0), + Biome::Taiga => (0.3, 62.0), + Biome::TaigaHills => (0.075, 68.0), + Biome::DesertHills => (0.075, 68.0), + Biome::DeepOcean => (0.17, 35.0), + Biome::Desert => (0.15, 62.0), + Biome::Mountains => (0.045, 75.0), + Biome::MountainEdge => (0.1, 70.0), + Biome::WoodedMountains => (0.04, 80.0), + Biome::FlowerForest => (0.1, 64.0), + Biome::Forest => (0.1, 64.0), + Biome::Jungle => (0.1, 63.0), + Biome::Ocean => (0.12, 45.0), + Biome::Plains => (0.3, 62.0), + Biome::Savanna => (0.3, 62.0), + Biome::SavannaPlateau => (0.3, 85.0), + Biome::StoneShore => (0.075, 60.0), + Biome::SunflowerPlains => (0.3, 62.0), + Biome::Swamp => (0.25, 59.0), + // TODO: finish this list + _ => (0.3, 62.0), + } +} + +/// Interpolates between two values based on the given +/// weight. +/// +/// The weight is clamped to [0.0, 1.0]. +fn lerp(a: f32, b: f32, weight: f32) -> f32 { + if weight < 0.0 { + return a; + } else if weight > 1.0 { + return b; + } + + a + (b - a) * weight +} diff --git a/server/src/worldgen/density_map/height.rs b/server/src/worldgen/density_map/height.rs new file mode 100644 index 000000000..5ffdc3e93 --- /dev/null +++ b/server/src/worldgen/density_map/height.rs @@ -0,0 +1,51 @@ +//! Implements a basic height map generator using 2D Perlin noise. +//! A superior generator would use 3D noise to allow for overhangs. + +use crate::worldgen::{block_index, DensityMapGenerator, NearbyBiomes, OCEAN_DEPTH, SKY_LIMIT}; +use bitvec::vec::BitVec; +use feather_core::{Biome, ChunkPosition}; +use simdnoise::NoiseBuilder; +use std::cmp::min; + +/// Density map generator which simply uses a height map +/// using two-dimensional Perlin noise. +#[derive(Debug, Default)] +pub struct HeightMapGenerator; + +impl DensityMapGenerator for HeightMapGenerator { + fn generate_for_chunk(&self, chunk: ChunkPosition, biomes: &NearbyBiomes, seed: u64) -> BitVec { + let x_offset = (chunk.x * 16) as f32; + let y_offset = (chunk.z * 16) as f32; + + let dim = 16; + let (elevation, _, _) = NoiseBuilder::gradient_2d_offset(x_offset, dim, y_offset, dim) + .with_seed(seed as i32) + .with_freq(0.01) + .generate(); + let (detail, _, _) = NoiseBuilder::gradient_2d_offset(x_offset, dim, y_offset, dim) + .with_seed(seed as i32 + 1) + .generate(); + + let mut density_map = bitvec![0; 16 * 256 * 16]; + for x in 0..16 { + for z in 0..16 { + let biome = biomes.biome_at(x, z); + let index = (z << 4) | x; + let mut elevation = elevation[index].abs() * 400.0; + let detail = detail[index] * 50.0; + + if biome == Biome::Ocean { + elevation -= OCEAN_DEPTH as f32; + } + + let height = (elevation + detail + 64.0) as usize; + + for y in 0..min(height, SKY_LIMIT) { + density_map.set(block_index(x, y, z), true); + } + } + } + + density_map + } +} diff --git a/server/src/worldgen/density_map/mod.rs b/server/src/worldgen/density_map/mod.rs new file mode 100644 index 000000000..3a24e4bd8 --- /dev/null +++ b/server/src/worldgen/density_map/mod.rs @@ -0,0 +1,5 @@ +mod density; +mod height; + +pub use density::DensityMapGeneratorImpl; +pub use height::HeightMapGenerator; diff --git a/server/src/worldgen/finishers/clumped.rs b/server/src/worldgen/finishers/clumped.rs new file mode 100644 index 000000000..74e48a6c0 --- /dev/null +++ b/server/src/worldgen/finishers/clumped.rs @@ -0,0 +1,76 @@ +use crate::worldgen::util::shuffle_seed_for_chunk; +use crate::worldgen::{ChunkBiomes, FinishingGenerator, TopBlocks}; +use feather_blocks::Block; +use feather_core::{Biome, Chunk}; +use rand::{Rng, SeedableRng}; +use rand_xorshift::XorShiftRng; +use std::{cmp, iter}; + +/// Clumped foliage generator. +#[derive(Default)] +pub struct ClumpedFoliageFinisher; + +impl FinishingGenerator for ClumpedFoliageFinisher { + fn generate_for_chunk( + &self, + chunk: &mut Chunk, + biomes: &ChunkBiomes, + top_blocks: &TopBlocks, + seed: u64, + ) { + // Generate clumps of foliage for the biome. + // Note that we currently use a hack + // to ensure that clumps are within one + // chunk. + // The algorithm should be changed in the future + // to allow for cross-chunk clumps. + + let mut rng = XorShiftRng::seed_from_u64(shuffle_seed_for_chunk(seed, chunk.position())); + + for x in 0..16 { + for z in 0..16 { + let biome = biomes.biome_at(x, z); + + if let Some(block) = biome_clump_block(biome) { + if rng.gen_range(0, 48) == 0 { + // Generate clump with center at this position. + iter::repeat(()).take(rng.gen_range(3, 6)).for_each(|_| { + let offset_x = rng.gen_range(-2, 3); + let offset_z = rng.gen_range(-2, 3); + + // Clamp value within chunk border + let pos_x = cmp::max(0, cmp::min(x as i32 + offset_x, 15)) as usize; + let pos_z = cmp::max(0, cmp::min(z as i32 + offset_z, 15)) as usize; + + if chunk.biome_at(pos_x, pos_z) != biome { + return; // Don't generate block outside this biome + } + + let top = top_blocks.top_block_at(pos_x, pos_z); + chunk.set_block_at(pos_x, top + 1, pos_z, block); + }); + } + } + } + } + } +} + +fn biome_clump_block(biome: Biome) -> Option { + match biome { + Biome::Plains + | Biome::SunflowerPlains + | Biome::WoodedMountains + | Biome::Mountains + | Biome::Savanna + | Biome::SavannaPlateau + | Biome::Forest + | Biome::DarkForest + | Biome::DarkForestHills + | Biome::BirchForest + | Biome::TallBirchForest + | Biome::BirchForestHills + | Biome::Swamp => Some(Block::Grass), + _ => None, + } +} diff --git a/server/src/worldgen/finishers/mod.rs b/server/src/worldgen/finishers/mod.rs new file mode 100644 index 000000000..d9bfdb0c5 --- /dev/null +++ b/server/src/worldgen/finishers/mod.rs @@ -0,0 +1,9 @@ +//! Various finishers for world generation, such as grass, snow, and trees. + +mod clumped; +mod single; +mod snow; + +pub use clumped::ClumpedFoliageFinisher; +pub use single::SingleFoliageFinisher; +pub use snow::SnowFinisher; diff --git a/server/src/worldgen/finishers/single.rs b/server/src/worldgen/finishers/single.rs new file mode 100644 index 000000000..dc313c2c8 --- /dev/null +++ b/server/src/worldgen/finishers/single.rs @@ -0,0 +1,60 @@ +use crate::worldgen::util::shuffle_seed_for_chunk; +use crate::worldgen::{ChunkBiomes, FinishingGenerator, TopBlocks}; +use feather_blocks::{Block, WaterData}; +use feather_core::{Biome, Chunk}; +use rand::{Rng, SeedableRng}; +use rand_xorshift::XorShiftRng; + +/// Foliage including shrubs and lilypads. +#[derive(Default)] +pub struct SingleFoliageFinisher; + +impl FinishingGenerator for SingleFoliageFinisher { + fn generate_for_chunk( + &self, + chunk: &mut Chunk, + biomes: &ChunkBiomes, + top_blocks: &TopBlocks, + seed: u64, + ) { + let mut rng = XorShiftRng::seed_from_u64(shuffle_seed_for_chunk(seed, chunk.position())); + for x in 0..16 { + for z in 0..16 { + let biome = biomes.biome_at(x, z); + + if let Some(foliage) = biome_foliage(biome) { + if chunk.block_at(x, top_blocks.top_block_at(x, z), z) == foliage.required + && rng.gen_range(0, 192) == 0 + { + chunk.set_block_at(x, top_blocks.top_block_at(x, z) + 1, z, foliage.block); + } + } + } + } + } +} + +struct Foliage { + /// The block required at the top of the column + /// for the foliage to generate. + required: Block, + /// The foliage block. + block: Block, +} + +impl Foliage { + fn new(required: Block, block: Block) -> Self { + Self { required, block } + } +} + +fn biome_foliage(biome: Biome) -> Option { + match biome { + Biome::Desert => Some(Foliage::new(Block::Sand, Block::DeadBush)), + Biome::Swamp => Some(Foliage::new( + Block::Water(WaterData::default()), + Block::LilyPad, + )), + _ => None, + } +} diff --git a/server/src/worldgen/finishers/snow.rs b/server/src/worldgen/finishers/snow.rs new file mode 100644 index 000000000..aa0ff6f68 --- /dev/null +++ b/server/src/worldgen/finishers/snow.rs @@ -0,0 +1,43 @@ +use crate::worldgen::{ChunkBiomes, FinishingGenerator, TopBlocks}; +use feather_blocks::{Block, SnowData}; +use feather_core::{Biome, Chunk}; + +/// Finisher for generating snow on top of snow biomes. +#[derive(Default)] +pub struct SnowFinisher; + +impl FinishingGenerator for SnowFinisher { + fn generate_for_chunk( + &self, + chunk: &mut Chunk, + biomes: &ChunkBiomes, + top_blocks: &TopBlocks, + _seed: u64, + ) { + for x in 0..16 { + for z in 0..16 { + if !is_snowy_biome(biomes.biome_at(x, z)) { + continue; + } + + chunk.set_block_at( + x, + top_blocks.top_block_at(x, z) + 1, + z, + Block::Snow(SnowData { layers: 1 }), + ) + } + } + } +} + +fn is_snowy_biome(biome: Biome) -> bool { + match biome { + Biome::SnowyTundra + | Biome::IceSpikes + | Biome::SnowyTaiga + | Biome::SnowyTaigaMountains + | Biome::SnowyBeach => true, + _ => false, + } +} diff --git a/server/src/worldgen/mod.rs b/server/src/worldgen/mod.rs new file mode 100644 index 000000000..169d3f561 --- /dev/null +++ b/server/src/worldgen/mod.rs @@ -0,0 +1,489 @@ +//! World generation for Feather. +//! +//! Generation is primarily based around the `ComposableGenerator`, +//! which allows configuration of a world generator pipeline. + +use feather_core::{Biome, Block, Chunk, ChunkPosition}; + +mod biomes; +mod composition; +mod density_map; +mod finishers; +pub mod noise; +mod superflat; +mod util; +pub mod voronoi; + +use crate::worldgen::finishers::{ClumpedFoliageFinisher, SingleFoliageFinisher, SnowFinisher}; +pub use biomes::{DistortedVoronoiBiomeGenerator, TwoLevelBiomeGenerator}; +use bitvec::slice::BitSlice; +use bitvec::vec::BitVec; +pub use composition::BasicCompositionGenerator; +pub use density_map::{DensityMapGeneratorImpl, HeightMapGenerator}; +pub use noise::NoiseLerper; +use num_traits::ToPrimitive; +use rand::{Rng, SeedableRng}; +use rand_xorshift::XorShiftRng; +use smallvec::SmallVec; +use std::fmt; +pub use superflat::SuperflatWorldGenerator; + +/// Sea-level height. +pub const SEA_LEVEL: usize = 64; +/// Sky limit. +pub const SKY_LIMIT: usize = 255; +/// Depth of an ocean. +const OCEAN_DEPTH: usize = 30; + +pub trait WorldGenerator: Send + Sync { + /// Generates the chunk at the given position. + fn generate_chunk(&self, position: ChunkPosition) -> Chunk; +} + +pub struct EmptyWorldGenerator {} + +impl WorldGenerator for EmptyWorldGenerator { + fn generate_chunk(&self, position: ChunkPosition) -> Chunk { + Chunk::new(position) + } +} + +/// A "composable" world generator. +/// +/// This generator will generate the world based +/// on a pipeline, and each step in the pipeline passes +/// data to the next stage. +/// +/// The pipeline stages are as follows: +/// * Biomes - generates a biome grid. +/// * Terrain density - generates the terrain density values using Perlin noise. +/// * Terrain composition - sets the correct block types based on the biome and terrain density. +/// * Finishing generators - generates final elements, such as grass, snow, and trees. +/// +/// This generator is based on [this document](http://cuberite.xoft.cz/docs/Generator.html). +pub struct ComposableGenerator { + /// The biome generator. + biome: Box, + /// The height map generator. + density_map: Box, + /// The composition generator. + composition: Box, + /// A vector of finishing generators used + /// by this composable generator. + finishers: SmallVec<[Box; 8]>, + /// The world seed. + seed: u64, +} + +impl ComposableGenerator { + /// Creates a new `ComposableGenerator` with the given stages. + pub fn new( + biome: B, + density_map: D, + composition: C, + finishers: F, + seed: u64, + ) -> Self + where + B: BiomeGenerator + 'static, + D: DensityMapGenerator + 'static, + C: CompositionGenerator + 'static, + F: IntoIterator>, + { + Self { + biome: Box::new(biome), + density_map: Box::new(density_map), + composition: Box::new(composition), + finishers: finishers.into_iter().collect(), + seed, + } + } + + /// A default composable generator, used + /// for worlds with "default" world type. + pub fn default_with_seed(seed: u64) -> Self { + let finishers: Vec> = vec![ + Box::new(SnowFinisher::default()), + Box::new(SingleFoliageFinisher::default()), + Box::new(ClumpedFoliageFinisher::default()), + ]; + Self::new( + TwoLevelBiomeGenerator::default(), + DensityMapGeneratorImpl::default(), + BasicCompositionGenerator::default(), + finishers, + seed, + ) + } +} + +impl WorldGenerator for ComposableGenerator { + fn generate_chunk(&self, position: ChunkPosition) -> Chunk { + let mut seed_shuffler = XorShiftRng::seed_from_u64(self.seed); + + // Generate biomes for 3x3 grid of chunks around current chunk. + let biome_seed = seed_shuffler.gen(); + + let mut biomes = vec![]; + + for z in -1..=1 { + for x in -1..=1 { + let pos = ChunkPosition::new(position.x + x, position.z + z); + biomes.push(self.biome.generate_for_chunk(pos, biome_seed)); + } + } + let biomes = NearbyBiomes::from_vec(biomes); + + let density_map = + self.density_map + .generate_for_chunk(position, &biomes, seed_shuffler.gen()); + + let mut chunk = Chunk::new(position); + + for x in 0..16 { + for z in 0..16 { + chunk.set_biome_at(x, z, biomes.biome_at(x, z)); + } + } + + self.composition.generate_for_chunk( + &mut chunk, + position, + &biomes.biomes[4], // Center chunk + density_map.as_bitslice(), + seed_shuffler.gen(), + ); + + // Calculate top blocks in chunk. + // TODO: perhaps this should be moved to `Chunk`? + let mut top_blocks = TopBlocks::new(); + for x in 0..16 { + for z in 0..16 { + for y in (0..256).rev() { + if chunk.block_at(x, y, z) != Block::Air { + top_blocks.set_top_block_at(x, z, y); + break; + } + } + } + } + + // Finishers. + for finisher in &self.finishers { + finisher.generate_for_chunk( + &mut chunk, + &biomes.biomes[4], + &top_blocks, + seed_shuffler.gen(), + ); + } + + // TODO: correct lighting. + // Fill chunk with 15 light levels. + chunk + .sections_mut() + .into_iter() + .filter(|section| section.is_some()) + .map(|section| section.unwrap()) + .for_each(|section| { + let sky_light = section.sky_light_mut(); + + (0..4096).for_each(|index| { + sky_light.set(index, 15); + }) + }); + + chunk + } +} + +/// A generator which generates the biome grid for a `ComposableGenerator`. +pub trait BiomeGenerator: Send + Sync { + /// Generates the biomes for a given chunk. + /// This function should be deterministic. + fn generate_for_chunk(&self, chunk: ChunkPosition, seed: u64) -> ChunkBiomes; +} + +/// A generator which generates the density map for a chunk. +/// Used in the `ComposableGenerator` pipeline. +pub trait DensityMapGenerator: Send + Sync { + /// Generates the density map for a given chunk. + /// A compact array of booleans is returned, indexable + /// by (y << 8) | (x << 4) | z. Those set to `true` will + /// contain solid blacks; those set to `false` will be air. + fn generate_for_chunk(&self, chunk: ChunkPosition, biomes: &NearbyBiomes, seed: u64) -> BitVec; +} + +/// A generator which populates the given chunk using blocks +/// based on the given density map and biomes. +pub trait CompositionGenerator: Send + Sync { + /// Populates the given chunk with blocks based on the given + /// biomes and density map. + fn generate_for_chunk( + &self, + chunk: &mut Chunk, + pos: ChunkPosition, + biomes: &ChunkBiomes, + density: &BitSlice, + seed: u64, + ); +} + +/// A generator, run after composition, +/// which can add finishing elements to chunks, +/// such as grass, trees, and snow. +pub trait FinishingGenerator: Send + Sync { + /// Populates the given chunk with any + /// finishing blocks. + fn generate_for_chunk( + &self, + chunk: &mut Chunk, + biomes: &ChunkBiomes, + top_blocks: &TopBlocks, + seed: u64, + ); +} + +/// Returns an index into a one-dimensional array +/// for the given x, y, and z values. +pub fn block_index(x: usize, y: usize, z: usize) -> usize { + assert!(x < 16 && y < 256 && z < 16); + (y << 8) | (x << 4) | z +} + +/// Represents the highest solid blocks in a chunk. +#[derive(Default)] +pub struct TopBlocks { + top_blocks: Vec, +} + +impl TopBlocks { + pub fn new() -> Self { + Self { + top_blocks: vec![0; 16 * 16], + } + } + + /// Fetches the highest solid blocks for the + /// given column coordinates (chunk-local). + pub fn top_block_at(&self, x: usize, z: usize) -> usize { + self.top_blocks[x + (z << 4)] as usize + } + + pub fn set_top_block_at(&mut self, x: usize, z: usize, top: usize) { + self.top_blocks[x + (z << 4)] = top as u8; + } +} + +/// Represents the biomes in a 3x3 grid of chunks, +/// centered on the chunk currently being generated. +pub struct NearbyBiomes { + /// 2D array of chunk biomes. The chunk biomes + /// for a given chunk position relative to the center + /// chunk can be obtained using (x + 1) + (z + 1) * 3. + biomes: Vec, +} + +impl NearbyBiomes { + pub fn from_vec(biomes: Vec) -> Self { + Self { biomes } + } + + /// Gets the biome at the given column position. + /// + /// The column position is an offset from the + /// bottom-left corner of the center chunk. + pub fn biome_at(&self, x: N, z: N) -> Biome { + let (index, local_x, local_z) = self.index(x, z); + + self.biomes[index].biome_at(local_x, local_z) + } + + pub fn set_biome_at(&mut self, x: N, z: N, biome: Biome) { + let (index, local_x, local_z) = self.index(x, z); + + self.biomes[index].set_biome_at(local_x, local_z, biome); + } + + fn index(&self, ox: N, oz: N) -> (usize, usize, usize) { + let ox = ox.to_isize().unwrap(); + let oz = oz.to_isize().unwrap(); + let x = ox + 16; + let z = oz + 16; + + let chunk_x = (x / 16) as usize; + let chunk_z = (z / 16) as usize; + + let mut local_x = (ox % 16).abs() as usize; + let mut local_z = (oz % 16).abs() as usize; + + if ox < 0 { + local_x = 16 - local_x; + } + if oz < 0 { + local_z = 16 - local_z; + } + + (chunk_x + chunk_z * 3, local_x, local_z) + } +} + +/// Represents the biomes of a chunk. +pub struct ChunkBiomes { + /// 2D array of biome values. The biome for a given + /// column local to the chunk can be indexed using + /// (x << 4) | z. + biomes: [Biome; 16 * 16], +} + +impl ChunkBiomes { + /// Creates a `ChunkBiomes` wrapping the given array of biomes. + #[inline] + pub fn from_array(biomes: [Biome; 16 * 16]) -> Self { + Self { biomes } + } + + /// Gets the biome for the given column index. + /// + /// # Panics + /// Panics if `x >= 16 | z >= 16`. + pub fn biome_at(&self, x: usize, z: usize) -> Biome { + assert!(x < 16 && z < 16); + + let index = Self::index(x, z); + self.biomes[index] + } + + /// Sets the biome for the given column index. + /// + /// # Panics + /// Panics if `x >= 16 | z >= 16`. + pub fn set_biome_at(&mut self, x: usize, z: usize, biome: Biome) { + assert!(x < 16 && z < 16); + + let index = Self::index(x, z); + self.biomes[index] = biome; + } + + fn index(x: usize, z: usize) -> usize { + (x << 4) | z + } +} + +impl fmt::Debug for ChunkBiomes { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + for i in 0..256 { + write!(f, "{:?}, ", self.biomes[i])?; + } + + Ok(()) + } +} + +/// A biome generator which always generates plains. +#[derive(Debug, Default)] +pub struct StaticBiomeGenerator; + +impl BiomeGenerator for StaticBiomeGenerator { + fn generate_for_chunk(&self, _chunk: ChunkPosition, _seed: u64) -> ChunkBiomes { + ChunkBiomes::from_array([Biome::Plains; 16 * 16]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_reproducability() { + let seeds: [u64; 4] = [std::u64::MAX, 3243, 0, 100]; + + let chunks = [ + ChunkPosition::new(0, 0), + ChunkPosition::new(-1, -1), + ChunkPosition::new(1, 1), + ]; + + for seed in seeds.iter() { + let gen = ComposableGenerator::default_with_seed(*seed); + for chunk in chunks.iter() { + let first = gen.generate_chunk(*chunk); + + let second = gen.generate_chunk(*chunk); + + test_chunks_eq(&first, &second); + } + } + } + + fn test_chunks_eq(a: &Chunk, b: &Chunk) { + for x in 0..16 { + for z in 0..16 { + assert_eq!(a.biome_at(x, z), b.biome_at(x, z)); + for y in 0..256 { + assert_eq!(a.block_at(x, y, z), b.block_at(x, y, z)); + } + } + } + } + + #[test] + pub fn test_worldgen_empty() { + let chunk_pos = ChunkPosition { x: 1, z: 2 }; + let generator = EmptyWorldGenerator {}; + let chunk = generator.generate_chunk(chunk_pos); + + // No sections have been generated + assert!(chunk.sections().iter().all(|sec| sec.is_none())); + assert_eq!(chunk_pos, chunk.position()); + } + + #[test] + fn test_chunk_biomes() { + let biomes = [Biome::Plains; 16 * 16]; + + let mut biomes = ChunkBiomes::from_array(biomes); + + for x in 0..16 { + for z in 0..16 { + assert_eq!(biomes.biome_at(x, z), Biome::Plains); + biomes.set_biome_at(x, z, Biome::Ocean); + assert_eq!(biomes.biome_at(x, z), Biome::Ocean); + } + } + } + + #[test] + fn test_static_biome_generator() { + let gen = StaticBiomeGenerator::default(); + + let biomes = gen.generate_for_chunk(ChunkPosition::new(0, 0), 0); + + for x in 0..16 { + for z in 0..16 { + assert_eq!(biomes.biome_at(x, z), Biome::Plains); + } + } + } + + #[test] + fn test_nearby_biomes() { + let biomes = vec![ + ChunkBiomes::from_array([Biome::Plains; 256]), + ChunkBiomes::from_array([Biome::Swamp; 256]), + ChunkBiomes::from_array([Biome::Savanna; 256]), + ChunkBiomes::from_array([Biome::BirchForest; 256]), + ChunkBiomes::from_array([Biome::DarkForest; 256]), + ChunkBiomes::from_array([Biome::Mountains; 256]), + ChunkBiomes::from_array([Biome::Ocean; 256]), + ChunkBiomes::from_array([Biome::Desert; 256]), + ChunkBiomes::from_array([Biome::Taiga; 256]), + ]; + let biomes = NearbyBiomes::from_vec(biomes); + + assert_eq!(biomes.biome_at(0, 0), Biome::DarkForest); + assert_eq!(biomes.biome_at(16, 16), Biome::Taiga); + assert_eq!(biomes.biome_at(-1, -1), Biome::Plains); + assert_eq!(biomes.biome_at(-1, 0), Biome::BirchForest); + } +} diff --git a/server/src/worldgen/noise.rs b/server/src/worldgen/noise.rs new file mode 100644 index 000000000..d12e45ec4 --- /dev/null +++ b/server/src/worldgen/noise.rs @@ -0,0 +1,225 @@ +use num_traits::ToPrimitive; + +/// Struct for applying linear interpolation to a 3D +/// density array. +pub struct NoiseLerper<'a> { + /// The density values. + densities: &'a [f32], + /// The size of the chunk to generate along X and Z axes. + size_horizontal: u32, + /// The size of the chunk to generate along the Y axis. + size_vertical: u32, + /// The offset along the X axis to generate. + offset_x: i32, + /// The offset along the Z axis to generate. + offset_z: i32, + /// The scale along the X and Z axes. Must be a divisor of size_horizontal. + scale_horizontal: u32, + /// The scale along the Y axis. Must be a divisor of size_vertical. + scale_vertical: u32, +} + +impl<'a> NoiseLerper<'a> { + /// Initializes with default settings and the given + /// density values. + /// + /// Default settings are intended to match the size + /// of chunks. Horizontal and vertical size and scale + /// are initialized to sane defaults. + pub fn new(densities: &'a [f32]) -> Self { + Self { + densities, + size_horizontal: 16, + size_vertical: 256, + offset_x: 0, + offset_z: 0, + scale_horizontal: 4, + scale_vertical: 8, + } + } + + /// Sets the size of the chunk to be generated. + pub fn with_size(mut self, xz: u32, y: u32) -> Self { + self.size_horizontal = xz; + self.size_vertical = y; + self + } + + /// Sets the X and Z offsets. + /// + /// # Notes + /// * The X and Z offsets are multiplied by the horizontal and vertical + /// sizes, respectively, to obtain the offset in absolute coordinates. + /// (This means there is no need to multiply the chunk coordinate by 16.) + pub fn with_offset(mut self, x: i32, z: i32) -> Self { + self.offset_x = x; + self.offset_z = z; + self + } + + /// Sets the scale of the noise. Linear interpolation + /// is used between values based on this scale. + pub fn with_scale(mut self, horizontal: u32, vertical: u32) -> Self { + self.scale_horizontal = horizontal; + self.size_vertical = vertical; + self + } + + /// Generates a linear-interpolated block of noise. + /// The returned vector will have length `size_horizontal^2 * size_vertical`, + /// indexable by `((y << 12) | z << 4) | x`. + pub fn generate(&self) -> Vec { + // If AVX2 is available, use it. Otherwise, + // default to a scalar impl. + // TODO: support SSE41, other SIMD instruction sets + + if is_x86_feature_detected!("avx2") { + self.generate_avx2() + } else { + self.generate_fallback() + } + } + + fn generate_avx2(&self) -> Vec { + // TODO: implement this. (Premature optimization is bad!) + self.generate_fallback() + } + + fn generate_fallback(&self) -> Vec { + // Loop through values offsetted by the scale. + // Then, loop through all coordinates inside + // that subchunk and apply linear interpolation. + + // This is based on Glowstone's OverworldGenerator.generateRawTerrain + // with a few modifications and superior variable names. + + // Number of subchunks in a chunk along each axis. + let subchunk_horizontal = self.size_horizontal / self.scale_horizontal; + let subchunk_vertical = self.size_vertical / self.scale_vertical; + + // Density noise, with one value every `scale` blocks along each axis. + // Indexing into this vector is done using `self.uninterpolated_index(x, y, z)`. + let densities = self.densities; + + // Buffer to emit final noise into. + // TODO: consider using Vec::set_len to avoid zeroing it out + let mut buf = + vec![0.0; (self.size_horizontal * self.size_horizontal * self.size_vertical) as usize]; + + let scale_vertical = self.scale_vertical as f32; + let scale_horizontal = self.scale_horizontal as f32; + + // Coordinates of the subchunk. The subchunk + // is the chunk within the chunk in which we + // only find the noise value for the corners + // and then apply interpolation in between. + + // Here, we loop through the subchunks and interpolate + // noise for each block within it. + for subx in 0..subchunk_horizontal { + for suby in 0..subchunk_vertical { + for subz in 0..subchunk_horizontal { + // Two grids of noise values: + // one for the four bottom corners + // of the subchunk, and one for the + // offsets along the Y axis to apply + // to those base corners each block increment. + + // These are mutated so that they are at the + // current Y position. + let mut base1 = densities[self.uninterpolated_index(subx, suby, subz)]; + let mut base2 = densities[self.uninterpolated_index(subx + 1, suby, subz)]; + let mut base3 = densities[self.uninterpolated_index(subx, suby, subz + 1)]; + let mut base4 = densities[self.uninterpolated_index(subx + 1, suby, subz + 1)]; + + // Offsets for each block along the Y axis from each corner above. + let offset1 = (densities[self.uninterpolated_index(subx, suby + 1, subz)] + - base1) + / scale_vertical; + let offset2 = (densities[self.uninterpolated_index(subx + 1, suby + 1, subz)] + - base2) + / scale_vertical; + let offset3 = (densities[self.uninterpolated_index(subx, suby + 1, subz + 1)] + - base3) + / scale_vertical; + let offset4 = (densities + [self.uninterpolated_index(subx + 1, suby + 1, subz + 1)] + - base4) + / scale_vertical; + + // Iterate through the blocks in this subchunk + // and apply interpolation before setting the + // noise value in the final buffer. + for blocky in 0..self.scale_vertical { + let mut z_base = base1; + let mut z_corner = base3; + for blockx in 0..self.scale_horizontal { + let mut density = z_base; + for blockz in 0..self.scale_horizontal { + // Set interpolated value in buffer. + buf[index( + blockx + (self.scale_horizontal * subx), + blocky + (self.scale_vertical * suby), + blockz + (self.scale_horizontal * subz), + )] = density; + + // Apply Z interpolation. + density += (z_corner - z_base) / scale_horizontal; + } + // Interpolation along X. + z_base += (base2 - base1) / scale_horizontal; + // Along Z again. + z_corner += (base4 - base3) / scale_horizontal; + } + + // Interpolation along Y. + base1 += offset1; + base2 += offset2; + base3 += offset3; + base4 += offset4; + } + } + } + } + + buf + } + + fn uninterpolated_index(&self, x: N, y: N, z: N) -> usize { + let length = (self.size_horizontal / self.scale_horizontal + 1) as usize; + let height = (self.size_vertical / self.scale_vertical + 1) as usize; + + let x = x.to_usize().unwrap(); + let y = y.to_usize().unwrap(); + let z = z.to_usize().unwrap(); + + y * length + x + height * length * z + } +} + +pub fn index(x: N, y: N, z: N) -> usize { + let x = x.to_usize().unwrap(); + let y = y.to_usize().unwrap(); + let z = z.to_usize().unwrap(); + + ((y << 8) | z << 4) | x +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn basic_test() { + let densities = [0.0; 5 * 33 * 5]; + let noise = NoiseLerper::new(&densities).with_offset(10, 16); + + let chunk = noise.generate(); + + assert_eq!(chunk.len(), 16 * 256 * 16); + + for x in chunk { + assert_float_eq!(x, 0.0); + } + } +} diff --git a/server/src/worldgen.rs b/server/src/worldgen/superflat.rs similarity index 78% rename from server/src/worldgen.rs rename to server/src/worldgen/superflat.rs index b16c04172..c9039c5af 100644 --- a/server/src/worldgen.rs +++ b/server/src/worldgen/superflat.rs @@ -1,24 +1,12 @@ +use crate::worldgen::WorldGenerator; use feather_blocks::Block; use feather_core::level::SuperflatGeneratorOptions; use feather_core::{Biome, Chunk, ChunkPosition}; -pub trait WorldGenerator: Send + Sync { - /// Generates the chunk at the given position. - fn generate_chunk(&self, position: ChunkPosition) -> Chunk; -} - pub struct SuperflatWorldGenerator { pub options: SuperflatGeneratorOptions, } -pub struct EmptyWorldGenerator {} - -impl WorldGenerator for EmptyWorldGenerator { - fn generate_chunk(&self, position: ChunkPosition) -> Chunk { - Chunk::new(position) - } -} - impl WorldGenerator for SuperflatWorldGenerator { fn generate_chunk(&self, position: ChunkPosition) -> Chunk { let biome = Biome::from_identifier(self.options.biome.as_str()).unwrap_or(Biome::Plains); @@ -55,17 +43,6 @@ mod tests { use super::*; use feather_blocks::GrassBlockData; - #[test] - pub fn test_worldgen_empty() { - let chunk_pos = ChunkPosition { x: 1, z: 2 }; - let generator = EmptyWorldGenerator {}; - let chunk = generator.generate_chunk(chunk_pos); - - // No sections have been generated - assert!(chunk.sections().iter().all(|sec| sec.is_none())); - assert_eq!(chunk_pos, chunk.position()); - } - #[test] pub fn test_worldgen_flat() { let mut options = SuperflatGeneratorOptions::default(); diff --git a/server/src/worldgen/util.rs b/server/src/worldgen/util.rs new file mode 100644 index 000000000..ff4072a4d --- /dev/null +++ b/server/src/worldgen/util.rs @@ -0,0 +1,18 @@ +//! Utilities for world generation. + +use feather_core::ChunkPosition; + +/// Deterministically a seed for the given chunk. This allows +/// different seeds to be used for different chunk. +pub fn shuffle_seed_for_chunk(seed: u64, chunk: ChunkPosition) -> u64 { + seed.wrapping_mul((chunk.x as u64).wrapping_add(1)) + .wrapping_add((chunk.z as u64).wrapping_add(1)) +} + +/// Deterministically shuffles a seed for the given chunk and chunk column. +pub fn shuffle_seed_for_column(seed: u64, chunk: ChunkPosition, col_x: usize, col_z: usize) -> u64 { + shuffle_seed_for_chunk(seed, chunk) + .wrapping_add(2) + .wrapping_mul(((col_x as u64) << 4) + 4) + .wrapping_mul(col_z as u64 + 4) +} diff --git a/server/src/worldgen/voronoi.rs b/server/src/worldgen/voronoi.rs new file mode 100644 index 000000000..839dcb909 --- /dev/null +++ b/server/src/worldgen/voronoi.rs @@ -0,0 +1,144 @@ +//! Basic Voronoi implementation. + +use rand::{Rng, SeedableRng}; +use rand_xorshift::XorShiftRng; + +/// Position of a cell. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct CellPos { + x: i32, + y: i32, +} + +/// Position of a seed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct SeedPos { + x: i32, + y: i32, +} + +/// Representation of a Voronoi grid. +/// +/// Seeds around the most recently requested cell are cached. +/// +/// # Implementation +/// This Voronoi implementation works using a grid with jitter +/// offsets. A seed is allocated for each cell in the grid with +/// a random offset from the center of the cell based on a hash +/// function of the cell position. +/// +/// This allows the grid to be deterministic and efficient compared +/// to when using random cell positions. +pub struct VoronoiGrid { + /// Length and width of each grid square. + length: u32, + /// The seed used for generation. + seed: u64, + /// The currently cached cell position. + cached: CellPos, + /// The positions of the seeds around the cached cell position. + cached_seeds: [[SeedPos; 5]; 5], +} + +impl VoronoiGrid { + /// Creates a new voronoi grid with the given + /// length and seed. + /// + /// This function does not + /// actually compute any values. + pub fn new(length: u32, seed: u64) -> Self { + Self { + length, + seed, + cached: CellPos { + x: 999_999_999, + y: 999_999_999, + }, // Use values so that this will be replaced + cached_seeds: [[SeedPos { x: 0, y: 0 }; 5]; 5], + } + } + + /// Returns the position of the seed closest to the given + /// position. + pub fn get(&mut self, x: i32, y: i32) -> (i32, i32) { + let cell_pos = CellPos { + x: x / self.length as i32, + y: y / self.length as i32, + }; + + self.update_cache(cell_pos); + + // TODO: this is fairly inefficient. There is + // probably a way to optimize this. + let closest_seed = self + .cached_seeds + .iter() + .flatten() + .min_by_key(|seed| { + // Distance squared to cell position + square(seed.x - x) + square(seed.y - y) + }) + .unwrap(); // Safe - iterator is never empty + + (closest_seed.x, closest_seed.y) + } + + /// Updates the currently cached seed positions. + /// + /// If the given cell position is equal to the cached + /// cell position, this is a no-op. + fn update_cache(&mut self, cell: CellPos) { + if cell == self.cached { + return; + } + + self.cached = cell; + + let half_length = (self.length / 2) as i32; + + for x in -2..=2 { + for y in -2..=2 { + // Calculate center of grid position and then + // apply an offset based on a hash of the cell position. + + let cell_x = cell.x + x; + let cell_y = cell.y + y; + + let pos_x = cell_x * self.length as i32; + let pos_y = cell_y * self.length as i32; + + let mut rng = XorShiftRng::seed_from_u64( + self.seed ^ (((i64::from(cell_x)) << 32) | (i64::from(cell_y))) as u64, + ); + let offset = rng.gen_range(-half_length, half_length); + + let center_x = pos_x + half_length as i32; + let center_y = pos_y + half_length as i32; + + let offsetted_pos = SeedPos { + x: center_x + offset, + y: center_y + offset, + }; + self.cached_seeds[(x + 2) as usize][(y + 2) as usize] = offsetted_pos; + } + } + } +} + +/// Shuffles the given closest_x and closest_y values +/// and returns a deterministic random value in the given range based +/// on those values. +/// +/// This can be used to determine a value corresponding to a voronoi seed, +/// for example. +pub fn shuffle(closest_x: i32, closest_y: i32, min: usize, max: usize) -> usize { + let combined = ((closest_x as u64) << 32) | closest_y as u64; + + let mut rng = XorShiftRng::seed_from_u64(combined); + + rng.gen_range(min, max) +} + +fn square(x: i32) -> i32 { + x * x +}