diff --git a/Cargo.lock b/Cargo.lock index 4549452b..521dad86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -375,14 +375,15 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb2a21c9f3306676077a88700bb8f354be779cf9caba9c21e94da9e696751af4" dependencies = [ + "bevy_dylib", "bevy_internal", ] [[package]] name = "bevy-inspector-egui" -version = "0.28.1" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36172627eb6fd8586600972bcbba2880ed6f59e4e243dcf2ed7ff68d987577ce" +checksum = "b3d3ea87310d78bacc94471bcf5a8b63ead43e7263d404571832c2297458b856" dependencies = [ "bevy-inspector-egui-derive", "bevy_app", @@ -409,14 +410,15 @@ dependencies = [ "fuzzy-matcher", "image", "smallvec", + "uuid", "winit", ] [[package]] name = "bevy-inspector-egui-derive" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3afc67826e0a4347414545e022e748f42550a577a502b26af44e6d03742c9266" +checksum = "a7259e525c7844b23f10fd2b2efaa3eea57996f101cc30e833070d139e2b4e4d" dependencies = [ "proc-macro2", "quote", @@ -639,6 +641,15 @@ dependencies = [ "sysinfo", ] +[[package]] +name = "bevy_dylib" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b120f96b1f18f3aac28729b6d7765114486b64df4c2af3314fe2bf30d4eb11c3" +dependencies = [ + "bevy_internal", +] + [[package]] name = "bevy_ecs" version = "0.15.1" @@ -676,9 +687,9 @@ dependencies = [ [[package]] name = "bevy_egui" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "954fbe8551af4b40767ea9390ec7d32fe1070a6ab55d524cf0868c17f8469a55" +checksum = "6a4b8df063d7c4d4171bc853e5ea0d67c7f1b5edd3b014d43acbfe3042dd6cf4" dependencies = [ "arboard", "bevy_app", @@ -689,6 +700,7 @@ dependencies = [ "bevy_input", "bevy_log", "bevy_math", + "bevy_picking", "bevy_reflect", "bevy_render", "bevy_time", @@ -700,7 +712,6 @@ dependencies = [ "egui", "encase", "js-sys", - "log", "thread_local", "wasm-bindgen", "wasm-bindgen-futures", @@ -1235,8 +1246,7 @@ dependencies = [ [[package]] name = "bevy_renet" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eef73d8b44abe4852fe772f30eb7215beeac60e53ef94b3cb5a63e162bda4f51" +source = "git+https://github.com/CuddlyBunion341/renet.git#01fce875fab05aabebcb6d1e8725bf15424883a0" dependencies = [ "bevy_app", "bevy_ecs", @@ -2271,9 +2281,9 @@ dependencies = [ [[package]] name = "ecolor" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775cfde491852059e386c4e1deb4aef381c617dc364184c6f6afee99b87c402b" +checksum = "7d72e9c39f6e11a2e922d04a34ec5e7ef522ea3f5a1acfca7a19d16ad5fe50f5" dependencies = [ "bytemuck", "emath", @@ -2281,14 +2291,26 @@ dependencies = [ [[package]] name = "egui" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53eafabcce0cb2325a59a98736efe0bf060585b437763f8c476957fb274bb974" +checksum = "252d52224d35be1535d7fd1d6139ce071fb42c9097773e79f7665604f5596b5e" dependencies = [ "ahash", "emath", "epaint", "nohash-hasher", + "profiling", +] + +[[package]] +name = "egui_plot" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c226cae80a6ee10c4d3aaf9e33bd9e9b2f1c0116b6036bdc2a1cfc9d2d0dcc10" +dependencies = [ + "ahash", + "egui", + "emath", ] [[package]] @@ -2299,9 +2321,9 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1fe0049ce51d0fb414d029e668dd72eb30bc2b739bf34296ed97bd33df544f3" +checksum = "c4fe73c1207b864ee40aa0b0c038d6092af1030744678c60188a05c28553515d" dependencies = [ "bytemuck", ] @@ -2349,9 +2371,9 @@ dependencies = [ [[package]] name = "epaint" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a32af8da821bd4f43f2c137e295459ee2e1661d87ca8779dfa0eaf45d870e20f" +checksum = "5666f8d25236293c966fbb3635eac18b04ad1914e3bab55bc7d44b9980cafcac" dependencies = [ "ab_glyph", "ahash", @@ -2361,13 +2383,14 @@ dependencies = [ "epaint_default_fonts", "nohash-hasher", "parking_lot", + "profiling", ] [[package]] name = "epaint_default_fonts" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483440db0b7993cf77a20314f08311dbe95675092405518c0677aa08c151a3ea" +checksum = "66f6ddac3e6ac6fd4c3d48bb8b1943472f8da0f43a4303bcd8a18aa594401c80" [[package]] name = "equivalent" @@ -4330,8 +4353,7 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "renet" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b4ff6af5dc5497f1155a2856c2a8db3cf4f94c0e2ed83614e3cef2fde48c0f9" +source = "git+https://github.com/CuddlyBunion341/renet.git#01fce875fab05aabebcb6d1e8725bf15424883a0" dependencies = [ "bevy_ecs", "bytes", @@ -4342,8 +4364,7 @@ dependencies = [ [[package]] name = "renet_netcode" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e34e6acd4d01155558d24609c1d0e8f8eafaf58bf805ecf1ecd7ac613d6528a" +source = "git+https://github.com/CuddlyBunion341/renet.git#01fce875fab05aabebcb6d1e8725bf15424883a0" dependencies = [ "bevy_ecs", "log", @@ -4354,8 +4375,7 @@ dependencies = [ [[package]] name = "renet_visualizer" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3000d08a1d1682a01bc76a7b8d4d8172f5e7ea3eee18546d7c22c76eb36c609b" +source = "git+https://github.com/CuddlyBunion341/renet.git#01fce875fab05aabebcb6d1e8725bf15424883a0" dependencies = [ "bevy_ecs", "egui", @@ -4365,8 +4385,7 @@ dependencies = [ [[package]] name = "renetcode" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "118d456f815f7fd5bd12713a9e69a0b0f8b45806bd515e05bb60146f1867310d" +source = "git+https://github.com/CuddlyBunion341/renet.git#01fce875fab05aabebcb6d1e8725bf15424883a0" dependencies = [ "chacha20poly1305", "log", @@ -4421,6 +4440,7 @@ dependencies = [ "bincode", "cgmath", "chrono", + "egui_plot", "iyes_perf_ui", "noise", "rand", diff --git a/Cargo.toml b/Cargo.toml index 5e90fafc..c2a4528a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,17 +12,20 @@ bevy_rapier3d = "0.28.0" cgmath = "0.18.0" iyes_perf_ui = { git = "https://github.com/IyesGames/iyes_perf_ui.git", branch = "main" } noise = "0.9.0" -bevy_renet = { version = "1.0.0" } +bevy_renet = { git = "https://github.com/CuddlyBunion341/renet.git" } bincode = "1.3.3" rand = "0.8.5" -renet = "1.0.0" +renet = { git = "https://github.com/CuddlyBunion341/renet.git" } serde = { version = "1.0.203", features = ["derive"] } serde-big-array = "0.5.1" chrono = "0.4.38" rayon = "1.10.0" bevy_flair = "0.1.0" -bevy-inspector-egui = "0.28.1" -renet_visualizer = { version = "1.0.0", features = ["bevy"] } +bevy-inspector-egui = "0.29" +renet_visualizer = { git = "https://github.com/CuddlyBunion341/renet.git", features = [ + "bevy", +] } +egui_plot = "0.30.0" [profile.dev.package."*"] opt-level = 3 @@ -39,10 +42,23 @@ name = "server" path = "src/server/main.rs" [features] +default = ["chat"] +dynamic_linking = ["bevy/dynamic_linking"] + +# both +chat = ["dynamic_linking"] + +# server +renet_visualizer = ["egui_layer"] +generator_visualizer = ["egui_layer"] +egui_layer = [] +skip_chunk_padding = [] + +# client wireframe = [] debug_ui = [] -renet_visualizer = [] +ortho_camera = [] +lock_player = [] physics_debug = [] raycast_debug = [] visual_debug = ["wireframe", "physics_debug", "raycast_debug"] - diff --git a/README.md b/README.md index 45dc6877..05f66f87 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,7 @@ Strongly inspired by the [Bevy NixOS installation guide](https://github.com/bevy nix-shell --run "cargo run --bin server" nix-shell --run "cargo run --bin client" ``` + +## Notes + +Checkout the [Wiki](https://github.com/CuddlyBunion341/rsmc/wiki) for additional project information. diff --git a/src/client/main.rs b/src/client/main.rs index 08c89f58..258ef725 100644 --- a/src/client/main.rs +++ b/src/client/main.rs @@ -62,6 +62,7 @@ fn main() { collider::ColliderPlugin, player::PlayerPlugin, remote_player::RemotePlayerPlugin, + #[cfg(feature = "chat")] chat::ChatPlugin, )); app.insert_state(GameState::Playing); diff --git a/src/client/networking/systems.rs b/src/client/networking/systems.rs index 1b6cbaa4..10ee0fc7 100644 --- a/src/client/networking/systems.rs +++ b/src/client/networking/systems.rs @@ -9,8 +9,11 @@ pub fn receive_message_system( mut block_update_events: ResMut>, mut chunk_manager: ResMut, mut chunk_mesh_events: ResMut>, - mut chat_events: ResMut>, - mut single_chat_events: ResMut>, + mut world_regenerate_events: ResMut>, + #[cfg(feature = "chat")] mut chat_events: ResMut>, + #[cfg(feature = "chat")] mut single_chat_events: ResMut< + Events, + >, mut spawn_area_loaded: ResMut, ) { while let Some(message) = client.receive_message(DefaultChannel::ReliableOrdered) { @@ -35,10 +38,12 @@ pub fn receive_message_system( from_network: true, }); } + #[cfg(feature = "chat")] NetworkingMessage::ChatMessageSync(messages) => { info!("Client received {} chat messages", messages.len()); chat_events.send(chat_events::ChatSyncEvent(messages)); } + #[cfg(feature = "chat")] NetworkingMessage::SingleChatMessageSync(message) => { info!("Client received chat message {}", message.message); single_chat_events.send(chat_events::SingleChatSendEvent(message)); @@ -91,6 +96,10 @@ pub fn receive_message_system( player_sync_events .send(remote_player_events::RemotePlayerSyncEvent { players: event }); } + NetworkingMessage::ServerAsksClientNicelyToRerequestChunkBatch() => { + info!("Client asked for chunk batch."); + world_regenerate_events.send(terrain_events::WorldRegenerateEvent); + } _ => { warn!("Received unknown message type. (ReliableUnordered)"); } diff --git a/src/client/player/systems/controller.rs b/src/client/player/systems/controller.rs index 9a39c41a..e0a34bf4 100644 --- a/src/client/player/systems/controller.rs +++ b/src/client/player/systems/controller.rs @@ -1,14 +1,28 @@ use crate::prelude::*; -const SPAWN_POINT: Vec3 = Vec3::new(0.0, 32.0, 0.0); +#[cfg(not(feature = "lock_player"))] +const SPAWN_POINT: Vec3 = Vec3::new(0.0, 64.0, 0.0); +#[cfg(feature = "lock_player")] +const SPAWN_POINT: Vec3 = Vec3::new(128.0, 96.0, -128.0); pub fn setup_player_camera(mut commands: Commands) { commands.spawn(( + Name::new("Player cam?"), Camera3d::default(), + #[cfg(not(feature = "ortho_camera"))] Projection::Perspective(PerspectiveProjection { fov: TAU / 5.0, ..default() }), + #[cfg(feature = "ortho_camera")] + Projection::Orthographic(OrthographicProjection { + scale: 0.125, + near: 0.0001, + far: 1000.0, + viewport_origin: Vec2::new(0.5, 0.5), + scaling_mode: ScalingMode::WindowSize, + area: Rect::new(-1.0, -1.0, 1.0, 1.0), + }), RenderPlayer { logical_entity: Entity::from_raw(0), }, @@ -36,6 +50,9 @@ pub fn setup_controller_on_area_ready_system( }, ActiveEvents::COLLISION_EVENTS, Velocity::zero(), + #[cfg(feature = "lock_player")] + RigidBody::Fixed, + #[cfg(not(feature = "lock_player"))] RigidBody::Dynamic, Sleeping::disabled(), LockedAxes::ROTATION_LOCKED, @@ -44,9 +61,16 @@ pub fn setup_controller_on_area_ready_system( Ccd { enabled: true }, // Prevent clipping when going fast Transform::from_translation(SPAWN_POINT), LogicalPlayer, + #[cfg(not(feature = "lock_player"))] + FpsControllerInput { + pitch: -TAU / 20.0, + yaw: TAU * 5.0 / 12.0, + ..default() + }, + #[cfg(feature = "lock_player")] FpsControllerInput { - pitch: -TAU / 12.0, - yaw: TAU * 5.0 / 8.0, + pitch: 0.0, + yaw: 0.0, ..default() }, FpsController { diff --git a/src/client/terrain/events.rs b/src/client/terrain/events.rs index 5b3fb25e..9280405e 100644 --- a/src/client/terrain/events.rs +++ b/src/client/terrain/events.rs @@ -11,3 +11,6 @@ pub struct BlockUpdateEvent { pub block: BlockId, pub from_network: bool, } + +#[derive(Event)] +pub struct WorldRegenerateEvent; diff --git a/src/client/terrain/mod.rs b/src/client/terrain/mod.rs index 0660635e..04588a01 100644 --- a/src/client/terrain/mod.rs +++ b/src/client/terrain/mod.rs @@ -16,8 +16,10 @@ impl Plugin for TerrainPlugin { app.insert_resource(util::TextureManager::new()); app.add_event::(); app.add_event::(); + app.add_event::(); app.add_systems(Startup, terrain_systems::prepare_spawn_area_system); app.add_systems(Startup, terrain_systems::generate_world_system); app.add_systems(Update, terrain_systems::handle_chunk_mesh_update_events); + app.add_systems(Update, terrain_systems::handle_terrain_regeneration_events); } } diff --git a/src/client/terrain/systems.rs b/src/client/terrain/systems.rs index 478299ad..0c94f998 100644 --- a/src/client/terrain/systems.rs +++ b/src/client/terrain/systems.rs @@ -162,3 +162,17 @@ fn spawn_chunk( }, )); } + +pub fn handle_terrain_regeneration_events( + mut client: ResMut, + mut world_regenerate_events: EventReader, + chunk_manager: ResMut, +) { + for _ in world_regenerate_events.read() { + info!("Rerequesting all chunks from server"); + let all_chunk_positions = chunk_manager.get_all_chunk_positions(); + let message = + bincode::serialize(&NetworkingMessage::ChunkBatchRequest(all_chunk_positions)); + client.send_message(DefaultChannel::ReliableUnordered, message.unwrap()); + } +} diff --git a/src/server/main.rs b/src/server/main.rs index deffdd63..9f1c01fc 100644 --- a/src/server/main.rs +++ b/src/server/main.rs @@ -4,28 +4,35 @@ pub mod player; pub mod prelude; pub mod terrain; -#[cfg(feature = "renet_visualizer")] +#[cfg(feature = "egui_layer")] use bevy::DefaultPlugins; -#[cfg(not(feature = "renet_visualizer"))] +#[cfg(not(feature = "egui_layer"))] use bevy::log::LogPlugin; use crate::prelude::*; fn main() { let mut app = App::new(); - #[cfg(not(feature = "renet_visualizer"))] + #[cfg(not(feature = "egui_layer"))] { app.add_plugins(MinimalPlugins); app.add_plugins(LogPlugin::default()); } - #[cfg(feature = "renet_visualizer")] - app.add_plugins(DefaultPlugins); + #[cfg(feature = "egui_layer")] + { + use bevy_inspector_egui::bevy_egui::EguiPlugin; + app.add_plugins(DefaultPlugins); + app.add_plugins(EguiPlugin); + } app.add_plugins(player::PlayerPlugin); app.add_plugins(networking::NetworkingPlugin); app.add_plugins(terrain::TerrainPlugin); + + #[cfg(feature = "chat")] app.add_plugins(chat::ChatPlugin); + app.run(); } diff --git a/src/server/networking/mod.rs b/src/server/networking/mod.rs index 0cd5df05..f97d39c0 100644 --- a/src/server/networking/mod.rs +++ b/src/server/networking/mod.rs @@ -14,10 +14,8 @@ impl Plugin for NetworkingPlugin { #[cfg(feature = "renet_visualizer")] { - use bevy_inspector_egui::bevy_egui::EguiPlugin; use renet_visualizer::RenetServerVisualizer; - app.add_plugins(EguiPlugin); app.insert_resource(RenetServerVisualizer::<200>::default()); app.add_systems( Update, diff --git a/src/server/networking/systems.rs b/src/server/networking/systems.rs index c0e009f1..cccb61b1 100644 --- a/src/server/networking/systems.rs +++ b/src/server/networking/systems.rs @@ -5,7 +5,10 @@ pub fn receive_message_system( mut player_states: ResMut, mut past_block_updates: ResMut, chunk_manager: ResMut, - mut chat_message_events: EventWriter, + #[cfg(feature = "chat")] mut chat_message_events: EventWriter< + chat_events::PlayerChatMessageSendEvent, + >, + generator: Res, ) { for client_id in server.clients_id() { while let Some(message) = server.receive_message(client_id, DefaultChannel::ReliableOrdered) @@ -29,6 +32,7 @@ pub fn receive_message_system( .unwrap(), ); } + #[cfg(feature = "chat")] NetworkingMessage::ChatMessageSend(message) => { info!("Received chat message from {}", client_id); chat_message_events @@ -69,11 +73,7 @@ pub fn receive_message_system( Some(chunk) => *chunk, None => { let mut chunk = Chunk::new(position); - - let generator = terrain_util::generator::Generator::new(0); - generator.generate_chunk(&mut chunk); - chunk } } @@ -102,8 +102,12 @@ pub fn handle_events_system( mut server_events: EventReader, mut player_states: ResMut, past_block_updates: Res, - mut chat_message_events: EventWriter, - mut chat_sync_events: EventWriter, + #[cfg(feature = "chat")] mut chat_message_events: EventWriter< + chat_events::PlayerChatMessageSendEvent, + >, + #[cfg(feature = "chat")] mut chat_sync_events: EventWriter< + chat_events::SyncPlayerChatMessagesEvent, + >, ) { for event in server_events.read() { match event { @@ -117,10 +121,12 @@ pub fn handle_events_system( }, ); + #[cfg(feature = "chat")] chat_sync_events.send(chat_events::SyncPlayerChatMessagesEvent { client_id: *client_id, }); + #[cfg(feature = "chat")] chat_message_events.send(chat_events::PlayerChatMessageSendEvent { client_id: SERVER_MESSAGE_ID, message: format!("Player {} joined the game", *client_id), @@ -147,6 +153,7 @@ pub fn handle_events_system( println!("Client {client_id} disconnected: {reason}"); player_states.players.remove(client_id); + #[cfg(feature = "chat")] chat_message_events.send(chat_events::PlayerChatMessageSendEvent { client_id: SERVER_MESSAGE_ID, message: format!("Player {} left the game", client_id), @@ -165,6 +172,7 @@ pub use server_visualizer::*; #[cfg(feature = "renet_visualizer")] pub mod server_visualizer { + use crate::prelude::*; use bevy_inspector_egui::bevy_egui::EguiContexts; use renet_visualizer::RenetServerVisualizer; diff --git a/src/server/terrain/events.rs b/src/server/terrain/events.rs index 872816c7..987f4b98 100644 --- a/src/server/terrain/events.rs +++ b/src/server/terrain/events.rs @@ -5,3 +5,17 @@ pub struct BlockUpdateEvent { pub position: Vec3, pub block: BlockId, } + +#[cfg(feature = "generator_visualizer")] +pub use visualizer::*; +#[cfg(feature = "generator_visualizer")] +mod visualizer { + use super::*; + use terrain_resources::TextureType; + + #[derive(Event)] + pub struct RegenerateHeightMapEvent(pub TextureType); + + #[derive(Event)] + pub struct WorldRegenerateEvent; +} diff --git a/src/server/terrain/mod.rs b/src/server/terrain/mod.rs index ae05e3c0..2983cf47 100644 --- a/src/server/terrain/mod.rs +++ b/src/server/terrain/mod.rs @@ -13,5 +13,18 @@ impl Plugin for TerrainPlugin { app.add_event::(); app.insert_resource(resources::PastBlockUpdates::new()); app.add_systems(Startup, terrain_systems::setup_world_system); + app.insert_resource(resources::Generator::default()); + + #[cfg(feature = "generator_visualizer")] + { + app.insert_resource(resources::NoiseTextureList::default()); + app.add_systems(Startup, terrain_systems::prepare_visualizer_texture_system); + app.add_systems(Update, terrain_systems::render_visualizer_system); + app.add_systems(Update, terrain_systems::regenerate_heightmap_system); + app.add_systems(Update, terrain_systems::handle_regenerate_event_system); + + app.add_event::(); + app.add_event::(); + } } } diff --git a/src/server/terrain/resources.rs b/src/server/terrain/resources.rs index c817bb53..0bd1ad9e 100644 --- a/src/server/terrain/resources.rs +++ b/src/server/terrain/resources.rs @@ -20,3 +20,162 @@ impl PastBlockUpdates { } } } + +#[derive(Resource)] +pub struct Generator { + pub seed: u32, + pub perlin: Perlin, + pub params: TerrainGeneratorParams, +} + +pub struct HeightParams { + pub noise: NoiseFunctionParams, + pub splines: Vec, +} + +pub struct DensityParams { + pub noise: NoiseFunctionParams, + pub squash_factor: f64, + pub height_offset: f64, +} + +pub struct CaveParams { + pub noise: NoiseFunctionParams, + pub base_value: f64, + pub threshold: f64, +} + +pub struct HeightAdjustParams { + pub noise: NoiseFunctionParams, +} + +#[derive(Debug)] +pub struct NoiseFunctionParams { + pub octaves: u32, + pub height: f64, + pub lacuranity: f64, + pub frequency: f64, + pub amplitude: f64, + pub persistence: f64, +} + +impl Default for Generator { + fn default() -> Self { + Self::new(0) + } +} + +pub struct TerrainGeneratorParams { + pub height: HeightParams, + pub height_adjust: HeightAdjustParams, + pub density: DensityParams, + pub cave: CaveParams, +} + +impl Default for TerrainGeneratorParams { + fn default() -> Self { + Self { + height: HeightParams { + splines: vec![ + Vec2::new(-1.0, 4.0), + Vec2::new(0.0, 0.0), + Vec2::new(0.0, 0.0), + Vec2::new(0.05, 20.0), + Vec2::new(1.0, 35.0), + ], + noise: NoiseFunctionParams { + octaves: 4, + height: 0.0, + lacuranity: 2.0, + frequency: 1.0 / 300.0, + amplitude: 30.0, + persistence: 0.5, + }, + }, + height_adjust: HeightAdjustParams { + noise: NoiseFunctionParams { + octaves: 4, + height: 0.0, + lacuranity: 2.0, + frequency: 1.0 / 120.0, + amplitude: 30.0, + persistence: 0.5, + }, + }, + density: DensityParams { + squash_factor: 1.0 / 100.0, + height_offset: -20.0, + noise: NoiseFunctionParams { + octaves: 4, + height: 0.0, + lacuranity: 2.0, + frequency: 1.0 / 60.0, + amplitude: 10.0, + persistence: 0.5, + }, + }, + cave: CaveParams { + noise: NoiseFunctionParams { + octaves: 2, + height: 0.0, + lacuranity: 0.03, + frequency: 1.0 / 20.0, + amplitude: 30.0, + persistence: 0.59, + }, + base_value: 0.0, + threshold: 0.25, + }, + } + } +} + +#[cfg(feature = "generator_visualizer")] +pub use visualizer::*; + +#[cfg(feature = "generator_visualizer")] +mod visualizer { + use super::*; + use bevy::utils::HashMap; + use bevy_inspector_egui::egui::TextureHandle; + + #[derive(PartialEq, Hash, Eq, Clone, Debug)] + pub enum TextureType { + Height, + HeightAdjust, + Density, + Cave, + } + + #[derive(Resource)] + pub struct NoiseTextureList { + pub noise_textures: HashMap, + } + + impl Default for NoiseTextureList { + fn default() -> Self { + let mut noise_textures = HashMap::new(); + + noise_textures.insert(TextureType::Height, NoiseTexture::default()); + noise_textures.insert(TextureType::HeightAdjust, NoiseTexture::default()); + noise_textures.insert(TextureType::Density, NoiseTexture::default()); + noise_textures.insert(TextureType::Cave, NoiseTexture::default()); + + NoiseTextureList { noise_textures } + } + } + + pub struct NoiseTexture { + pub texture: Option, + pub size: Vec2, + } + + impl Default for NoiseTexture { + fn default() -> Self { + NoiseTexture { + texture: None, + size: Vec2::ZERO, + } + } + } +} diff --git a/src/server/terrain/systems.rs b/src/server/terrain/systems.rs index 83708e96..f6af669d 100644 --- a/src/server/terrain/systems.rs +++ b/src/server/terrain/systems.rs @@ -1,9 +1,10 @@ use crate::prelude::*; -pub fn setup_world_system(mut chunk_manager: ResMut) { - let generator = terrain_util::generator::Generator::new(0); - - let render_distance = Vec3::new(12.0, 2.0, 12.0); +pub fn setup_world_system( + mut chunk_manager: ResMut, + generator: Res, +) { + let render_distance = Vec3::new(8.0, 3.0, 8.0); info!("Generating chunks"); @@ -16,3 +17,347 @@ pub fn setup_world_system(mut chunk_manager: ResMut) { chunk_manager.insert_chunks(chunks); } + +#[cfg(feature = "generator_visualizer")] +pub use visualizer::*; + +#[cfg(feature = "generator_visualizer")] +mod visualizer { + use bevy::{ + log::{info, warn}, + math::{Vec2, Vec3}, + prelude::{EventReader, EventWriter, ResMut}, + }; + use bevy_inspector_egui::{ + bevy_egui::EguiContexts, + egui::{self, Color32, ColorImage, ImageData, TextureOptions}, + }; + use egui_plot::{Line, PlotPoint, PlotPoints}; + use rayon::iter::IntoParallelIterator; + + use rayon::iter::ParallelIterator; + use renet::{DefaultChannel, RenetServer}; + use rsmc::{Chunk, ChunkManager, NetworkingMessage, CHUNK_SIZE}; + + use super::{ + terrain_events, + terrain_resources::{self, NoiseFunctionParams, TextureType}, + }; + + fn map_range(value: f64, min: f64, max: f64, new_min: f64, new_max: f64) -> f64 { + ((value - min) / (max - min)) * (new_max - new_min) + new_min + } + + fn generate_terrain_heightmap( + generator: &terrain_resources::Generator, + texture_type: &TextureType, + size: Vec3, + draw_chunk_border: bool, + ) -> ImageData { + let mut data = vec![0; (size.x * size.z) as usize]; + + let width = size.x as usize; + let height = size.z as usize; + + for x in 0..width { + for z in 0..height { + let index = x + z * width; + + if draw_chunk_border && (x % CHUNK_SIZE == 0 || z % CHUNK_SIZE == 0) { + data[index] = 255; + continue; + } + + match texture_type { + TextureType::Height => { + let sample_position = Vec2::new(x as f32, z as f32); + let value = generator.normalized_spline_terrain_sample(sample_position); + let value = (value * size.y as f64) / 2.0 + 0.5; + + data[index] = value as u8; + } + TextureType::HeightAdjust => { + let sample_position = Vec2::new(x as f32, z as f32); + let value = generator + .sample_2d(sample_position, &generator.params.height_adjust.noise); + let value = map_range(value, -1.0, 1.0, 0.0, 255.0); + + data[index] = value as u8; + } + TextureType::Density => { + let pos = Vec3::new(x as f32, z as f32, 0.0); + let value = generator.sample_3d(pos, &generator.params.density.noise); + let value = map_range(value, -1.0, 1.0, 0.0, 255.0); + + data[index] = value as u8; + } + TextureType::Cave => { + let pos = Vec3::new(x as f32, z as f32, 0.0); + let mut value = generator.sample_3d(pos, &generator.params.cave.noise); + + let base = generator.params.cave.base_value; + let upper_bound = base + generator.params.cave.threshold; + let lower_bound = base - generator.params.cave.threshold; + + if lower_bound <= value && value >= upper_bound { + value = -1.0; + } + + let value = map_range(value, -1.0, 1.0, 0.0, 255.0); + + data[index] = value as u8; + } + }; + } + } + + let color_data: Vec = data + .iter() + .map(|&value| Color32::from_gray(value)) + .collect(); + + let color_image: ColorImage = ColorImage { + size: [width, height], + pixels: color_data, + }; + + ImageData::Color(color_image.into()) + } + + pub fn handle_regenerate_event_system( + mut events: EventReader, + mut chunk_manager: ResMut, + generator: ResMut, + mut server: ResMut, + ) { + for _ in events.read() { + info!("Regenerating world"); + let existing_chunk_positions = chunk_manager.get_all_chunk_positions(); + + let new_chunks: Vec = existing_chunk_positions + .into_par_iter() + .map(|chunk_position| { + let mut chunk = Chunk::new(chunk_position); + info!("Generating chunk at {:?}", chunk_position); + generator.generate_chunk(&mut chunk); + chunk + }) + .collect(); + + new_chunks.into_iter().for_each(|chunk| { + chunk_manager.insert_chunk(chunk); + }); + + info!("Successfully regenerated world"); + info!("Sending chunk requests for all chunks"); + + server.broadcast_message( + DefaultChannel::ReliableUnordered, + bincode::serialize( + &NetworkingMessage::ServerAsksClientNicelyToRerequestChunkBatch(), + ) + .unwrap(), + ); + } + } + + pub fn regenerate_heightmap_system( + mut events: EventReader, + generator: ResMut, + mut noise_texture_list: ResMut, + mut contexts: EguiContexts, + ) { + for event in events.read() { + let texture_type = event.0.clone(); + + info!("Regenerating noise preview for {:?}", texture_type); + + let width = 512; + let height = 512; + let depth = 512; + + let image_data = generate_terrain_heightmap( + &generator, + &texture_type, + Vec3::new(width as f32, height as f32, depth as f32), + true, + ); + + let entry = noise_texture_list + .noise_textures + .get_mut(&texture_type) + .expect("Noise texture not loaded, please initialize the resource properly."); + + entry.texture = Some(contexts.ctx_mut().load_texture( + "terrain-texture", + image_data, + TextureOptions::default(), + )); + entry.size = Vec2::new(width as f32, height as f32); + } + } + + #[rustfmt::skip] + pub fn prepare_visualizer_texture_system( + mut event_writer: EventWriter, + ) { + event_writer.send(terrain_events::RegenerateHeightMapEvent(TextureType::Height)); + event_writer.send(terrain_events::RegenerateHeightMapEvent(TextureType::HeightAdjust)); + event_writer.send(terrain_events::RegenerateHeightMapEvent(TextureType::Density)); + event_writer.send(terrain_events::RegenerateHeightMapEvent(TextureType::Cave)); + } + + macro_rules! add_slider { + ($ui:expr, $changed:expr, $value:expr, $range:expr, $text:expr) => {{ + $changed = $changed + || $ui + .add(egui::widgets::Slider::new($value, $range).text($text)) + .changed(); + }}; + } + + macro_rules! add_noise_sliders { + ($ui:expr, $changed:expr, $params:expr) => { + add_slider!($ui, $changed, &mut $params.octaves, 1..=8, "octaves"); + add_slider!( + $ui, + $changed, + &mut $params.lacuranity, + 0.001..=4.0, + "lacuranity" + ); + add_slider!( + $ui, + $changed, + &mut $params.frequency, + 10.0..=800.0, + "frequency" + ); + add_slider!( + $ui, + $changed, + &mut $params.persistence, + 0.001..=1.0, + "persistence" + ); + }; + } + + macro_rules! add_sliders_for_noise_params { + ($ui:expr, $changed:expr, $params:expr) => { + $params.frequency = 1.0 / $params.frequency; + add_noise_sliders!($ui, *$changed, $params); + $params.frequency = 1.0 / $params.frequency; + }; + } + + #[rustfmt::skip] + pub fn render_visualizer_system( + mut contexts: EguiContexts, + noise_texture_list: ResMut, + mut generator: ResMut, + mut event_writer: EventWriter, + mut world_regenerate_event_writer: EventWriter, + ) { + let noise_textures = &noise_texture_list.noise_textures; + + egui::Window::new("Terrain Generator").show(contexts.ctx_mut(), |ui| { + + ui.horizontal(|ui| { + + egui::Grid::new("Terrain gen").show(ui, |ui| { + ui.group(|ui| { + ui.vertical(|ui| { + ui.label("Splines"); + + let mut changed = false; + + let length = generator.params.height.splines.len(); + + for index in 0..length { + if index != 0 && index != length - 1 { + // Ensure range from 0 to 1 by locking the first and last splines + add_slider!(ui, changed, &mut generator.params.height.splines[index].x, -1.0..=1.0, format!("x{}", index)); + } + add_slider!(ui, changed, &mut generator.params.height.splines[index].y, -40.0..=80.0, format!("y{}", index)); + } + + if changed { + event_writer.send(terrain_events::RegenerateHeightMapEvent(TextureType::Height)); + } + + if ui.button("Regenerate world").clicked() { + world_regenerate_event_writer.send(terrain_events::WorldRegenerateEvent); + } + + egui_plot::Plot::new("splines") + .show(ui, |plot_ui| { + let plot_points: Vec = generator.params.height.splines.iter().map(|spline| PlotPoint {x: spline.x as f64, y: spline.y as f64}).collect(); + let line_chart = Line::new(PlotPoints::Owned(plot_points)); + plot_ui.line(line_chart); + }); + }) + + + + }); + for (texture_type, noise_texture) in noise_textures { + let texture_handle = noise_texture.texture.as_ref(); + + match texture_handle { + None => { + warn!("Noise texture handle could not be borrowed") + }, + Some(texture_handle) => { + let window_name = match texture_type { + TextureType::Height => "Base Height", + TextureType::HeightAdjust => "Height adjustment", + TextureType::Density => "Density", + TextureType::Cave => "Cave", + }; + + ui.group(|ui| { + ui.vertical(|ui| { + ui.label(window_name); + + let mut changed = false; + + let params: &mut NoiseFunctionParams = match texture_type { + TextureType::Height => &mut generator.params.height.noise, + TextureType::HeightAdjust => &mut generator.params.height_adjust.noise, + TextureType::Density => { + generator.params.density.squash_factor = 1.0 / generator.params.density.squash_factor; + add_slider!(ui, changed, &mut generator.params.density.squash_factor, 10.0..=500.0, "squash factor"); + add_slider!(ui, changed, &mut generator.params.density.height_offset, -50.0..=50.0, "height offset"); + generator.params.density.squash_factor = 1.0 / generator.params.density.squash_factor; + &mut generator.params.density.noise + } + TextureType::Cave => { + add_slider!(ui, changed, &mut generator.params.cave.base_value, -1.0..=1.0, "base value"); + add_slider!(ui, changed, &mut generator.params.cave.threshold, -1.0..=1.0, "treshold"); + &mut generator.params.cave.noise + }, + }; + + add_sliders_for_noise_params!(ui, &mut changed, params); + + if changed { + event_writer.send(terrain_events::RegenerateHeightMapEvent(texture_type.clone())); + }; + + ui.add(egui::widgets::Image::new(egui::load::SizedTexture::new( + texture_handle.id(), + texture_handle.size_vec2(), + ))); + + }) + }); + } + } + } + }); + }) + + }); + } +} diff --git a/src/server/terrain/util/generator.rs b/src/server/terrain/util/generator.rs index 18953009..c7156d0c 100644 --- a/src/server/terrain/util/generator.rs +++ b/src/server/terrain/util/generator.rs @@ -1,116 +1,243 @@ +use terrain_resources::{Generator, NoiseFunctionParams, TerrainGeneratorParams}; + use crate::prelude::*; -pub struct Generator { - pub seed: u32, - perlin: Perlin, +macro_rules! for_each_chunk_coordinate { + ($chunk:expr, $body:expr) => { + for x in 0..CHUNK_SIZE + 2 { + for y in 0..CHUNK_SIZE + 2 { + for z in 0..CHUNK_SIZE + 2 { + #[cfg(feature = "skip_chunk_padding")] + if x == 0 + || x == CHUNK_SIZE + 1 + || y == 0 + || y == CHUNK_SIZE + 1 + || z == 0 + || z == CHUNK_SIZE + 1 + { + continue; + } + + let chunk_origin = $chunk.position * CHUNK_SIZE as f32; + let local_position = Vec3::new(x as f32, y as f32, z as f32); + let world_position = chunk_origin + local_position; + + $body(x, y, z, world_position); + } + } + } + }; } impl Generator { pub fn new(seed: u32) -> Generator { + Self::new_with_params(seed, TerrainGeneratorParams::default()) + } + + pub fn new_with_params(seed: u32, params: TerrainGeneratorParams) -> Generator { Generator { seed, perlin: Perlin::new(seed), + params, } } pub fn generate_chunk(&self, chunk: &mut Chunk) { - let chunk_origin = chunk.position * CHUNK_SIZE as f32; - if chunk_origin.y < 0.0 { + if chunk.position.y < 0.0 { return; } - for x in 0..CHUNK_SIZE + 2 { - for y in 0..CHUNK_SIZE + 2 { - for z in 0..CHUNK_SIZE + 2 { - let local_position = Vec3::new(x as f32, y as f32, z as f32); - let block_position = chunk_origin + local_position; - let block = self.generate_block(block_position); - chunk.set_unpadded(x, y, z, block); - } + for_each_chunk_coordinate!(chunk, |x, y, z, world_position| { + let block = self.generate_block(world_position); + chunk.set_unpadded(x, y, z, block); + }); + + for_each_chunk_coordinate!(chunk, |x, y, z, _| { + let pos = Vec3 { + x: x as f32, + y: y as f32, + z: z as f32, + }; + + let block = self.decorate_block(chunk, pos); + chunk.set_unpadded(x, y, z, block); + }); + } + + fn decorate_block(&self, chunk: &Chunk, position: Vec3) -> BlockId { + let x = position.x as usize; + let y = position.y as usize; + let z = position.z as usize; + + let block = chunk.get_unpadded(x, y, z); + if block == BlockId::Air { + return block; + } + + let mut depth_below_nearest_air = 0; + let depth_check = 3; + + for delta_height in 0..depth_check { + if !Chunk::valid_unpadded(x, y + delta_height, z) { + break; } + + let block = chunk.get_unpadded(x, y + delta_height, z); + + if block == BlockId::Air { + break; + } + + depth_below_nearest_air += 1; + } + + match depth_below_nearest_air { + 0_i32..=1_i32 => BlockId::Grass, + 2..3 => BlockId::Dirt, + _ => BlockId::Stone, } } fn generate_block(&self, position: Vec3) -> BlockId { - let base_height = -50.0; - - let mut density = self.sample_3d( - Vec3 { - x: position.x, - y: position.y + base_height, - z: position.z, - }, - 4, - ); - density -= position.y as f64 * 0.02; - if density > 0.7 { - BlockId::Stone - } else if density > 0.40 { - BlockId::Dirt - } else if density > 0.0 { - if self.generate_block(position + Vec3::new(0.0, 1.0, 0.0)) == BlockId::Air { - BlockId::Grass - } else { - BlockId::Dirt + if self.is_inside_cave(position) { + return BlockId::Air; + } + + if (position.y as f64) < self.determine_terrain_height(position) { + return BlockId::Stone; + } + + if self.determine_terrain_density(position) > 0.0 { + return BlockId::Stone; + } + + BlockId::Air + } + + fn is_inside_cave(&self, position: Vec3) -> bool { + let density = self.sample_3d(position, &self.params.cave.noise); + + let upper_bound = self.params.cave.base_value - self.params.cave.threshold; + let lower_bound = self.params.cave.base_value + self.params.cave.threshold; + + lower_bound <= density && density >= upper_bound + } + + fn determine_terrain_height(&self, position: Vec3) -> f64 { + let noise_value = self + .sample_2d( + Vec2 { + x: position.x, + y: position.z, + }, + &self.params.height.noise, + ) + .abs(); + + self.spline_lerp(noise_value) + } + + fn determine_terrain_density(&self, position: Vec3) -> f64 { + let density = self.sample_3d(position, &self.params.density.noise); + let density_falloff = (position.y as f64 + self.params.density.height_offset) + * self.params.density.squash_factor; + + density - density_falloff + } + + pub fn normalized_spline_terrain_sample(&self, position: Vec2) -> f64 { + let noise_value = self.sample_2d(position, &self.params.height.noise); + + let min_height = self.params.height.splines[0].y as f64; + let max_height = self.params.height.splines[self.params.height.splines.len() - 1].y as f64; + + let splined_value = self.spline_lerp(noise_value); + + (splined_value - min_height) / (max_height - min_height) + } + + fn spline_lerp(&self, x: f64) -> f64 { + let x: f32 = x as f32; + + assert!(self.params.height.splines.len() >= 2); + + let min_x = self.params.height.splines[0].x; + let max_x = self.params.height.splines[self.params.height.splines.len() - 1].x; + + assert!(min_x == -1.0); + assert!(max_x == 1.0); + + for i in 0..self.params.height.splines.len() - 1 { + let current = self.params.height.splines[i]; + let next = self.params.height.splines[i + 1]; + + if x >= current.x && x <= next.x { + return self.lerp(current, x, next); } - } else { - BlockId::Air } + + panic!("Could not find matching spline points for x value {}", x); + } + + fn lerp(&self, point0: Vec2, x: f32, point1: Vec2) -> f64 { + ((point0.y * (point1.x - x) + point1.y * (x - point0.x)) / (point1.x - point0.x)) as f64 + } + + pub fn sample_2d(&self, position: Vec2, params: &NoiseFunctionParams) -> f64 { + let mut sample = 0.0; + let mut frequency = params.frequency; + let mut weight = 1.0; + let mut weight_sum = 0.0; + + for _ in 0..params.octaves { + let new_sample = self + .perlin + .get([position.x as f64 * frequency, position.y as f64 * frequency]); + + frequency *= params.lacuranity; + sample += new_sample * weight; + weight_sum += weight; + weight *= params.persistence; + } + + sample / weight_sum } - fn sample_3d(&self, position: Vec3, octaves: i32) -> f64 { - let mut density = 0.0; - let lacuranity = 2.0; - let mut frequency = 0.04; - let mut amplitude = 1.0; - let mut persistence = 0.5; + pub fn sample_3d(&self, position: Vec3, params: &NoiseFunctionParams) -> f64 { + let mut sample = 0.0; + let mut frequency = params.frequency; + let mut weight = 1.0; + let mut weight_sum = 0.0; - for _ in 0..octaves { - density += self.perlin.get([ + for _ in 0..params.octaves { + let new_sample = self.perlin.get([ position.x as f64 * frequency, position.y as f64 * frequency, position.z as f64 * frequency, - ]) * amplitude; + ]); - amplitude *= persistence; - frequency *= lacuranity; - persistence *= 0.5; + frequency *= params.lacuranity; + sample += new_sample * weight; + weight_sum += weight; + weight *= params.persistence; } - density + sample / weight_sum } } #[cfg(test)] mod tests { use super::*; - - #[test] - fn test_generator_new() { - let seed = 42; - let generator = Generator::new(seed); - assert_eq!(generator.seed, seed); - } + use terrain_resources::Generator; #[test] fn test_generate_chunk() { - let seed = 42; - let generator = Generator::new(seed); + let generator = Generator::default(); let mut chunk = Chunk::new(Vec3::new(0.0, 0.0, 0.0)); generator.generate_chunk(&mut chunk); assert_ne!(chunk.get(0, 0, 0), BlockId::Air); } - - #[test] - fn test_sample_3d() { - let seed = 42; - let generator = Generator::new(seed); - - let position = Vec3::new(0.0, 0.0, 0.0); - let density = generator.sample_3d(position, 4); - - assert!((0.0..=1.0).contains(&density)); - } } diff --git a/src/server/terrain/util/mod.rs b/src/server/terrain/util/mod.rs index 48962a8a..37fda818 100644 --- a/src/server/terrain/util/mod.rs +++ b/src/server/terrain/util/mod.rs @@ -2,4 +2,3 @@ pub mod blocks; pub mod generator; pub use blocks::*; -pub use generator::*; diff --git a/src/shared/networking.rs b/src/shared/networking.rs index 5857faef..e5b68349 100644 --- a/src/shared/networking.rs +++ b/src/shared/networking.rs @@ -50,6 +50,7 @@ pub enum NetworkingMessage { SingleChatMessageSync(ChatMessage), ChatMessageSync(Vec), BlockUpdate { position: Vec3, block: BlockId }, + ServerAsksClientNicelyToRerequestChunkBatch(), } const CHANNELS: [ChannelConfig; 3] = [ diff --git a/src/shared/terrain.rs b/src/shared/terrain.rs index 8437828a..5f532c56 100644 --- a/src/shared/terrain.rs +++ b/src/shared/terrain.rs @@ -24,6 +24,14 @@ impl Chunk { } } + pub fn valid_padded(x: usize, y: usize, z: usize) -> bool { + (1..CHUNK_SIZE).contains(&x) && (1..CHUNK_SIZE).contains(&y) && (1..CHUNK_SIZE).contains(&z) + } + + pub fn valid_unpadded(x: usize, y: usize, z: usize) -> bool { + x < PADDED_CHUNK_SIZE && y < PADDED_CHUNK_SIZE && z < PADDED_CHUNK_SIZE + } + pub fn get(&self, x: usize, y: usize, z: usize) -> BlockId { self.get_unpadded(x + 1, y + 1, z + 1) } @@ -195,6 +203,13 @@ impl ChunkManager { let chunk_position = position / CHUNK_SIZE as f32; self.get_chunk_mut(chunk_position) } + + pub fn get_all_chunk_positions(&self) -> Vec { + self.chunks + .keys() + .map(|key| Vec3::new(key[0] as f32, key[1] as f32, key[2] as f32)) + .collect() + } } #[cfg(test)] @@ -269,4 +284,15 @@ mod tests { let retrieved_block = chunk_manager.get_block(block_position).unwrap(); assert_eq!(retrieved_block, block_id); } + + #[test] + fn test_get_all_chunk_positions() { + let mut chunk_manager = ChunkManager::new(); + chunk_manager.set_chunk(Vec3::new(0.0, 0.0, 0.0), Chunk::default()); + chunk_manager.set_chunk(Vec3::new(2.0, 0.0, 0.0), Chunk::default()); + chunk_manager.set_chunk(Vec3::new(1.0, 0.0, 3.0), Chunk::default()); + + let retrieved_chunk_positions = chunk_manager.get_all_chunk_positions(); + assert_eq!(retrieved_chunk_positions.len(), 3); + } }