diff --git a/examples/biomes.rs b/examples/biomes.rs new file mode 100644 index 0000000..c5c8f70 --- /dev/null +++ b/examples/biomes.rs @@ -0,0 +1,204 @@ +use std::net::SocketAddr; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use log::LevelFilter; +use valence::biome::Biome; +use valence::block::BlockState; +use valence::chunk::{Chunk, UnloadedChunk}; +use valence::client::GameMode; +use valence::config::{Config, ServerListPing}; +use valence::dimension::{Dimension, DimensionId}; +use valence::entity::{EntityId, EntityKind}; +use valence::player_list::PlayerListId; +use valence::server::{Server, SharedServer, ShutdownResult}; +use valence::text::{Color, TextFormat}; +use valence::{async_trait, ident}; + +pub fn main() -> ShutdownResult { + env_logger::Builder::new() + .filter_module("valence", LevelFilter::Trace) + .parse_default_env() + .init(); + + valence::start_server( + Game { + player_count: AtomicUsize::new(0), + }, + ServerState { player_list: None }, + ) +} + +struct Game { + player_count: AtomicUsize, +} + +struct ServerState { + player_list: Option, +} + +#[derive(Default)] +struct ClientState { + entity_id: EntityId, +} + +const MAX_PLAYERS: usize = 10; + +const BIOME_COUNT: usize = 4 * 4; + +#[async_trait] +impl Config for Game { + type ServerState = ServerState; + type ClientState = ClientState; + type EntityState = (); + type WorldState = (); + type ChunkState = (); + type PlayerListState = (); + + fn max_connections(&self) -> usize { + // We want status pings to be successful even if the server is full. + MAX_PLAYERS + 64 + } + + fn dimensions(&self) -> Vec { + vec![Dimension { + fixed_time: Some(6000), + ..Dimension::default() + }] + } + + fn biomes(&self) -> Vec { + (0..BIOME_COUNT) + .map(|i| { + let color = (0xffffff / BIOME_COUNT * i) as u32; + Biome { + name: ident!("valence:test_biome_{i}"), + sky_color: color, + water_fog_color: color, + fog_color: color, + water_color: color, + foliage_color: Some(color), + grass_color: Some(color), + ..Default::default() + } + }) + .collect() + } + + async fn server_list_ping( + &self, + _server: &SharedServer, + _remote_addr: SocketAddr, + _protocol_version: i32, + ) -> ServerListPing { + ServerListPing::Respond { + online_players: self.player_count.load(Ordering::SeqCst) as i32, + max_players: MAX_PLAYERS as i32, + player_sample: Default::default(), + description: "Hello Valence!".color(Color::AQUA), + favicon_png: Some(include_bytes!("../assets/logo-64x64.png").as_slice().into()), + } + } + + fn init(&self, server: &mut Server) { + let world = server.worlds.insert(DimensionId::default(), ()).1; + server.state.player_list = Some(server.player_lists.insert(()).0); + let height = server.shared.dimensions().next().unwrap().1.height as usize; + + for chunk_z in 0..3 { + for chunk_x in 0..3 { + let chunk = if chunk_x == 1 && chunk_z == 1 { + let mut chunk = UnloadedChunk::new(height); + + // Set chunk blocks + for z in 0..16 { + for x in 0..16 { + chunk.set_block_state(x, 50, z, BlockState::GRASS_BLOCK); + } + } + + // Set chunk biomes + for z in 0..4 { + for x in 0..4 { + let biome_id = server.shared.biomes().nth(x + z * 4).unwrap().0; + + for y in 0..height / 4 { + chunk.set_biome(x, y, z, biome_id); + } + } + } + + chunk + } else { + UnloadedChunk::default() + }; + + world.chunks.insert([chunk_x, chunk_z], chunk, ()); + } + } + } + + fn update(&self, server: &mut Server) { + let (world_id, _) = server.worlds.iter_mut().next().unwrap(); + + let spawn_pos = [24.0, 50.0, 24.0]; + + server.clients.retain(|_, client| { + if client.created_this_tick() { + if self + .player_count + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { + (count < MAX_PLAYERS).then_some(count + 1) + }) + .is_err() + { + client.disconnect("The server is full!".color(Color::RED)); + return false; + } + + match server + .entities + .insert_with_uuid(EntityKind::Player, client.uuid(), ()) + { + Some((id, _)) => client.state.entity_id = id, + None => { + client.disconnect("Conflicting UUID"); + return false; + } + } + + client.spawn(world_id); + client.set_flat(true); + client.teleport(spawn_pos, 0.0, 0.0); + client.set_player_list(server.state.player_list.clone()); + + if let Some(id) = &server.state.player_list { + server.player_lists.get_mut(id).insert( + client.uuid(), + client.username(), + client.textures().cloned(), + client.game_mode(), + 0, + None, + ); + } + + client.set_game_mode(GameMode::Creative); + } + + if client.is_disconnected() { + self.player_count.fetch_sub(1, Ordering::SeqCst); + server.entities.remove(client.state.entity_id); + if let Some(id) = &server.state.player_list { + server.player_lists.get_mut(id).remove(client.uuid()); + } + return false; + } + + if client.position().y <= -20.0 { + client.teleport(spawn_pos, client.yaw(), client.pitch()); + } + + true + }); + } +} diff --git a/src/chunk.rs b/src/chunk.rs index b837d6e..1ad2ec8 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -274,7 +274,7 @@ pub trait Chunk { /// Gets the biome at the provided biome offsets in the chunk. /// /// **Note**: the arguments are **not** block positions. Biomes are 4x4x4 - /// segments of a chunk, so `x` and `z` are in `0..=4`. + /// segments of a chunk, so `x` and `z` are in `0..4`. /// /// # Panics /// @@ -284,7 +284,7 @@ pub trait Chunk { /// Sets the biome at the provided biome offsets in the chunk. /// /// **Note**: the arguments are **not** block positions. Biomes are 4x4x4 - /// segments of a chunk, so `x` and `z` are in `0..=4`. + /// segments of a chunk, so `x` and `z` are in `0..4`. /// /// # Panics /// diff --git a/src/dimension.rs b/src/dimension.rs index c698436..6ef009c 100644 --- a/src/dimension.rs +++ b/src/dimension.rs @@ -56,7 +56,7 @@ pub struct Dimension { pub fixed_time: Option, /// Determines what skybox/fog effects to use. pub effects: DimensionEffects, - /// The minimum height in which blocks can exist in this dimension. + /// The minimum Y coordinate in which blocks can exist in this dimension. /// /// `min_y` must meet the following conditions: /// * `min_y % 16 == 0` diff --git a/src/server.rs b/src/server.rs index 1907448..64e4a34 100644 --- a/src/server.rs +++ b/src/server.rs @@ -278,7 +278,9 @@ impl SharedServer { /// The function returns once the server has shut down, a runtime error /// occurs, or the configuration is found to be invalid. pub fn start_server(config: C, data: C::ServerState) -> ShutdownResult { - let shared = setup_server(config).map_err(Box::::from)?; + let shared = setup_server(config) + .context("failed to initialize server") + .map_err(Box::::from)?; let _guard = shared.tokio_handle().enter();