diff --git a/Cargo.lock b/Cargo.lock index d0ffb3b6..173d2d36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5356,6 +5356,7 @@ dependencies = [ "clap", "egui", "egui_plot", + "fastrand", "noise", "rand 0.9.2", "rayon", diff --git a/Cargo.toml b/Cargo.toml index 9be615df..b2801226 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ chrono = "0.4.43" rayon = "1.10.0" clap = { version = "4.5.54", features = ["derive"] } toml = "0.9.11" +fastrand = "2.3.0" [patch.crates-io] # TODO: Remove patch once egui requirement is more flexible. diff --git a/src/client/player/mod.rs b/src/client/player/mod.rs index fe7cbb00..642042f2 100644 --- a/src/client/player/mod.rs +++ b/src/client/player/mod.rs @@ -34,7 +34,7 @@ impl Plugin for PlayerPlugin { player_systems::handle_controller_movement_system, player_systems::handle_player_collider_events_system, ) - .run_if(terrain_resources::SpawnRegionLoaded::is_loaded) // TODO: doublecheck + .run_if(terrain_resources::SpawnRegionLoaded::is_loaded) .run_if(player_resources::PlayerSpawned::is_spawned), ); app.add_systems( diff --git a/src/server/terrain/persistence.rs b/src/server/terrain/persistence.rs index 10a9eb07..a950316f 100644 --- a/src/server/terrain/persistence.rs +++ b/src/server/terrain/persistence.rs @@ -179,7 +179,6 @@ mod tests { let mut actual_new_chunk = Chunk::new(IVec3::new(20, 0, 20)); generator.generate_chunk(&mut actual_new_chunk); - // FIXME: Make Terrain Generation fully deterministic - // assert_eq!(possible_new_chunk.data, actual_new_chunk.data); + assert_eq!(possible_new_chunk.data, actual_new_chunk.data); } } diff --git a/src/server/terrain/util/generator.rs b/src/server/terrain/util/generator.rs index 9cc0d803..0be4e381 100644 --- a/src/server/terrain/util/generator.rs +++ b/src/server/terrain/util/generator.rs @@ -1,3 +1,5 @@ +use fastrand::Rng; + use crate::{ prelude::*, terrain::{ @@ -42,6 +44,9 @@ impl Generator { } pub fn generate_chunk(&self, chunk: &mut Chunk) { + let mut rng = fastrand::Rng::new(); + rng.seed(chunk.rng_seed()); + for_each_chunk_coordinate!(chunk, |x, y, z, world_position| { let block = self.generate_block(world_position); chunk.set_unpadded(x, y, z, block); @@ -49,17 +54,16 @@ impl Generator { for_each_chunk_coordinate!(chunk, |x, y, z, _| { let pos = IVec3::new(x as i32, y as i32, z as i32); - - self.decorate_block(chunk, pos); + self.decorate_block(&mut rng, chunk, pos); }); for _ in 0..self.params.tree.spawn_attempts_per_chunk { - self.attempt_spawn_tree(chunk); + self.attempt_spawn_tree(&mut rng, chunk); } } - fn attempt_spawn_tree(&self, chunk: &mut Chunk) { - let proposal = Self::propose_tree_blocks(self); + fn attempt_spawn_tree(&self, rng: &mut Rng, chunk: &mut Chunk) { + let proposal = self.propose_tree_blocks(rng); struct Bounds { min: IVec3, @@ -85,15 +89,12 @@ impl Generator { }, ); - let sapling_x: i32 = rand::random_range( - proposal_bounds.min.x.abs()..(CHUNK_SIZE as i32 - proposal_bounds.max.x), - ); - let sapling_y: i32 = rand::random_range( - proposal_bounds.min.y.abs()..(CHUNK_SIZE as i32 - proposal_bounds.max.y), - ); - let sapling_z: i32 = rand::random_range( - proposal_bounds.min.z.abs()..(CHUNK_SIZE as i32 - proposal_bounds.max.z), - ); + let sapling_x = + rng.i32(proposal_bounds.min.x.abs()..(CHUNK_SIZE as i32 - proposal_bounds.max.x)); + let sapling_y = + rng.i32(proposal_bounds.min.y.abs()..(CHUNK_SIZE as i32 - proposal_bounds.max.y)); + let sapling_z = + rng.i32(proposal_bounds.min.z.abs()..(CHUNK_SIZE as i32 - proposal_bounds.max.z)); if chunk.get(sapling_x, sapling_y, sapling_z) != BlockId::Grass { return; @@ -102,14 +103,11 @@ impl Generator { let proposal_valid = proposal.iter().all(|(relative_pos, _block)| { let IVec3 { x, y, z } = relative_pos; Chunk::is_within_padded_bounds( - sapling_x as i32 + { *x }, - sapling_y as i32 + { *y }, - sapling_z as i32 + { *z }, - ) && chunk.get( - sapling_x as i32 + { *x }, - sapling_y as i32 + { *y }, - sapling_z as i32 + { *z }, - ) == BlockId::Air + sapling_x + { *x }, + sapling_y + { *y }, + sapling_z + { *z }, + ) && chunk.get(sapling_x + { *x }, sapling_y + { *y }, sapling_z + { *z }) + == BlockId::Air }); if !proposal_valid { @@ -119,26 +117,24 @@ impl Generator { proposal.iter().for_each(|(relative_pos, block_id)| { let IVec3 { x, y, z } = relative_pos; chunk.set( - sapling_x as i32 + { *x }, - sapling_y as i32 + { *y }, - sapling_z as i32 + { *z }, + sapling_x + { *x }, + sapling_y + { *y }, + sapling_z + { *z }, *block_id, ); }); } - fn propose_tree_blocks(&self) -> Vec<(IVec3, BlockId)> { + fn propose_tree_blocks(&self, rng: &mut Rng) -> Vec<(IVec3, BlockId)> { let mut blocks = Vec::new(); let min_tree_stump_height = self.params.tree.min_stump_height; let max_tree_stump_height = self.params.tree.max_stump_height; - let tree_stump_height = - rand::random_range(min_tree_stump_height..max_tree_stump_height) as i32; + let tree_stump_height = rng.u32(min_tree_stump_height..max_tree_stump_height) as i32; let bush_radius: i32 = - rand::random_range(self.params.tree.min_bush_radius..self.params.tree.max_bush_radius) - as i32; + rng.u32(self.params.tree.min_bush_radius..self.params.tree.max_bush_radius) as i32; for dx in -bush_radius..bush_radius { for dz in -bush_radius..bush_radius { @@ -165,7 +161,7 @@ impl Generator { blocks } - fn decorate_block(&self, chunk: &mut Chunk, position: IVec3) { + fn decorate_block(&self, rng: &mut Rng, chunk: &mut Chunk, position: IVec3) { let x = position.x as usize; let y = position.y as usize; let z = position.z as usize; @@ -177,7 +173,7 @@ impl Generator { && Chunk::valid_unpadded(x, y - 1, z) && chunk.get_unpadded(x, y - 1, z) == BlockId::Grass { - let random_number = rand::random_range(0..=self.params.grass.frequency); + let random_number = rng.u32(0..=self.params.grass.frequency); if random_number == 0 { chunk.set_unpadded(x, y, z, BlockId::Tallgrass); } diff --git a/src/shared/chunk/chunk_data.rs b/src/shared/chunk/chunk_data.rs index 43829ec4..74fd7bf7 100644 --- a/src/shared/chunk/chunk_data.rs +++ b/src/shared/chunk/chunk_data.rs @@ -1,3 +1,5 @@ +use std::hash::{DefaultHasher, Hash, Hasher}; + use bevy::math::IVec3; use crate::*; @@ -79,4 +81,10 @@ impl Chunk { pub fn key_eq_pos(key: [i32; 3], position: IVec3) -> bool { position.x == key[0] && position.y == key[1] && position.z == key[2] } + + pub fn rng_seed(&self) -> u64 { + let mut s = DefaultHasher::new(); + self.position.hash(&mut s); + s.finish() + } } diff --git a/src/shared/networking.rs b/src/shared/networking.rs index bb7cf3c6..c1eef07e 100644 --- a/src/shared/networking.rs +++ b/src/shared/networking.rs @@ -149,7 +149,7 @@ impl<'de> Deserialize<'de> for Username { } } -pub const DEFAULT_SPAWN_POINT: IVec3 = IVec3::new(0, 43, 0); // TODO: determine spawn point from terain +pub const DEFAULT_SPAWN_POINT: IVec3 = IVec3::new(0, 43, 0); #[derive(Serialize, Deserialize, Clone, Copy, Debug)] pub struct PlayerState {