) {
- {
- let millis = server.last_tick_duration().as_secs_f64() * 1000.0;
- let tick = server.current_tick();
- let players = server.clients.len();
- let delay = 20;
-
- server.millis_sum += millis;
-
- if tick % delay == 0 {
- let avg = server.millis_sum / delay as f64;
- println!("Avg MSPT: {avg:.3}ms, Tick={tick}, Players={players}");
- server.millis_sum = 0.0;
- }
- }
-
- let (world_id, _) = server.worlds.iter_mut().next().unwrap();
-
- server.clients.retain(|_, client| {
- if client.created_this_tick() {
- client.respawn(world_id);
- client.set_flat(true);
- client.teleport([0.0, 1.0, 0.0], 0.0, 0.0);
- client.set_game_mode(GameMode::Creative);
-
- if WITH_PLAYER_ENTITIES {
- client.set_player_list(server.state.player_list.clone());
- if let Some(id) = &server.state.player_list {
- server.player_lists[id].insert(
- client.uuid(),
- client.username(),
- client.textures().cloned(),
- client.game_mode(),
- 0,
- None,
- true,
- );
- }
-
- match server
- .entities
- .insert_with_uuid(EntityKind::Player, client.uuid(), ())
- {
- Some((id, entity)) => {
- entity.set_world(world_id);
- client.state = id
- }
- None => {
- client.disconnect("Conflicting UUID");
- return false;
- }
- }
- }
- }
-
- if client.is_disconnected() {
- if WITH_PLAYER_ENTITIES {
- if let Some(id) = &server.state.player_list {
- server.player_lists[id].remove(client.uuid());
- }
- server.entities[client.state].set_deleted(true);
- }
-
- return false;
- }
-
- if WITH_PLAYER_ENTITIES {
- if let Some(player) = server.entities.get_mut(client.state) {
- while let Some(event) = client.next_event() {
- event.handle_default(client, player);
- }
- }
- } else {
- while let Some(event) = client.next_event() {
- if let ClientEvent::UpdateSettings { view_distance, .. } = event {
- client.set_view_distance(view_distance);
- }
- }
- }
-
- true
- });
- }
-}
diff --git a/crates/packet_inspector/src/main.rs b/crates/packet_inspector/src/main.rs
index f82aaf7..0446a94 100644
--- a/crates/packet_inspector/src/main.rs
+++ b/crates/packet_inspector/src/main.rs
@@ -1,9 +1,9 @@
use std::error::Error;
use std::fmt::Write;
+use std::io;
use std::io::ErrorKind;
use std::net::SocketAddr;
use std::sync::Arc;
-use std::{fmt, io};
use anyhow::bail;
use clap::Parser;
@@ -63,7 +63,7 @@ struct State {
impl State {
pub async fn rw_packet<'a, P>(&'a mut self) -> anyhow::Result
where
- P: DecodePacket<'a> + EncodePacket + fmt::Debug,
+ P: DecodePacket<'a> + EncodePacket,
{
while !self.dec.has_next_packet()? {
self.dec.reserve(4096);
diff --git a/crates/bench_players/Cargo.toml b/crates/playground/Cargo.toml
similarity index 63%
rename from crates/bench_players/Cargo.toml
rename to crates/playground/Cargo.toml
index 00141e0..e47b736 100644
--- a/crates/bench_players/Cargo.toml
+++ b/crates/playground/Cargo.toml
@@ -1,8 +1,11 @@
[package]
-name = "bench_players"
+name = "playground"
version = "0.1.0"
edition = "2021"
[dependencies]
+anyhow = "1.0.65"
+glam = "0.22.0"
+tracing = "0.1.37"
tracing-subscriber = "0.3.16"
valence = { path = "../valence" }
diff --git a/crates/playground/build.rs b/crates/playground/build.rs
new file mode 100644
index 0000000..4f75cfb
--- /dev/null
+++ b/crates/playground/build.rs
@@ -0,0 +1,21 @@
+use std::path::Path;
+
+fn main() {
+ let current = std::env::current_dir().unwrap();
+ println!("current directory: {}", current.display());
+
+ let src = current.join(Path::new("src/playground.template.rs"));
+ let dst = current.join(Path::new("src/playground.rs"));
+
+ if dst.exists() {
+ println!("{dst:?} already exists, skipping");
+ return;
+ }
+
+ if !src.exists() {
+ println!("{src:?} does not exist, skipping");
+ return;
+ }
+
+ std::fs::copy(src, dst).unwrap();
+}
diff --git a/crates/playground/src/.gitignore b/crates/playground/src/.gitignore
new file mode 100644
index 0000000..68aefd1
--- /dev/null
+++ b/crates/playground/src/.gitignore
@@ -0,0 +1 @@
+playground.rs
\ No newline at end of file
diff --git a/crates/playground/src/extras.rs b/crates/playground/src/extras.rs
new file mode 100644
index 0000000..7a43fdc
--- /dev/null
+++ b/crates/playground/src/extras.rs
@@ -0,0 +1,2 @@
+//! Put stuff in here if you find that you have to write the same code for
+//! multiple playgrounds.
diff --git a/crates/playground/src/main.rs b/crates/playground/src/main.rs
new file mode 100644
index 0000000..47dac45
--- /dev/null
+++ b/crates/playground/src/main.rs
@@ -0,0 +1,12 @@
+use valence::bevy_app::App;
+
+mod extras;
+mod playground;
+
+fn main() {
+ tracing_subscriber::fmt().init();
+
+ let mut app = App::new();
+ playground::build_app(&mut app);
+ app.run();
+}
diff --git a/crates/playground/src/playground.template.rs b/crates/playground/src/playground.template.rs
new file mode 100644
index 0000000..7ba539c
--- /dev/null
+++ b/crates/playground/src/playground.template.rs
@@ -0,0 +1,51 @@
+use valence::client::despawn_disconnected_clients;
+use valence::client::event::default_event_handler;
+use valence::prelude::*;
+
+#[allow(unused_imports)]
+use crate::extras::*;
+
+const SPAWN_Y: i32 = 64;
+
+pub fn build_app(app: &mut App) {
+ app.add_plugin(ServerPlugin::new(()))
+ .add_system_to_stage(EventLoop, default_event_handler)
+ .add_startup_system(setup)
+ .add_system(init_clients)
+ .add_system(despawn_disconnected_clients);
+}
+
+fn setup(world: &mut World) {
+ let mut instance = world
+ .resource::()
+ .new_instance(DimensionId::default());
+
+ for z in -5..5 {
+ for x in -5..5 {
+ instance.insert_chunk([x, z], Chunk::default());
+ }
+ }
+
+ for z in -25..25 {
+ for x in -25..25 {
+ instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
+ }
+ }
+
+ world.spawn(instance);
+}
+
+fn init_clients(
+ mut clients: Query<&mut Client, Added>,
+ instances: Query>,
+) {
+ let instance = instances.get_single().unwrap();
+
+ for mut client in &mut clients {
+ client.set_position([0.5, SPAWN_Y as f64 + 1.0, 0.5]);
+ client.set_instance(instance);
+ client.set_game_mode(GameMode::Survival);
+ }
+}
+
+// Add new systems here!
diff --git a/crates/valence/Cargo.toml b/crates/valence/Cargo.toml
index 5eb9d17..766d61a 100644
--- a/crates/valence/Cargo.toml
+++ b/crates/valence/Cargo.toml
@@ -6,7 +6,7 @@ description = "A framework for building Minecraft servers in Rust."
repository = "https://github.com/rj00a/valence"
readme = "README.md"
license = "MIT"
-keywords = ["minecraft", "gamedev", "server"]
+keywords = ["minecraft", "gamedev", "server", "ecs"]
categories = ["game-engines"]
build = "build/main.rs"
authors = ["Ryan Johnson "]
@@ -14,18 +14,20 @@ authors = ["Ryan Johnson "]
[dependencies]
anyhow = "1.0.65"
arrayvec = "0.7.2"
-async-trait = "0.1.57"
-base64 = "0.13.0"
-bitfield-struct = "0.1.8"
+async-trait = "0.1.60"
+base64 = "0.21.0"
+bevy_app = "0.9.1"
+bevy_ecs = "0.9.1"
+bitfield-struct = "0.3.1"
bytes = "1.2.1"
flume = "0.10.14"
-futures = "0.3.24"
+glam = "0.22.0"
hmac = "0.12.1"
num = "0.4.0"
-paste = "1.0.9"
+parking_lot = "0.12.1"
+paste = "1.0.11"
rand = "0.8.5"
-rayon = "1.5.3"
-rsa = "0.6.1"
+rsa = "0.7.2"
rsa-der = "0.3.0"
rustc-hash = "1.1.0"
serde = { version = "1.0.145", features = ["derive"] }
@@ -33,16 +35,12 @@ serde_json = "1.0.85"
sha1 = "0.10.5"
sha2 = "0.10.6"
thiserror = "1.0.35"
+tokio = { version = "1.25.0", features = ["full"] }
tracing = "0.1.37"
url = { version = "2.2.2", features = ["serde"] }
uuid = { version = "1.1.2", features = ["serde"] }
valence_nbt = { version = "0.5.0", path = "../valence_nbt" }
valence_protocol = { version = "0.1.0", path = "../valence_protocol", features = ["encryption", "compression"] }
-vek = "0.15.8"
-
-[dependencies.tokio]
-version = "1.21.2"
-features = ["macros", "rt-multi-thread", "net", "io-util", "sync", "time"]
[dependencies.reqwest]
version = "0.11.12"
@@ -52,9 +50,9 @@ features = ["rustls-tls", "json"]
[dev-dependencies]
approx = "0.5.1"
-tracing-subscriber = "0.3.16"
+glam = { version = "0.22.0", features = ["approx"] }
noise = "0.8.2"
-valence_spatial_index = { path = "../valence_spatial_index", version = "0.1.0" }
+tracing-subscriber = "0.3.16"
[build-dependencies]
anyhow = "1.0.65"
diff --git a/crates/valence/build/entity.rs b/crates/valence/build/entity.rs
index 7f8bd77..3972f97 100644
--- a/crates/valence/build/entity.rs
+++ b/crates/valence/build/entity.rs
@@ -128,7 +128,7 @@ impl Value {
Value::Facing(_) => quote!(Facing),
Value::OptionalUuid(_) => quote!(Option),
Value::OptionalBlockState(_) => quote!(BlockState),
- Value::NbtCompound(_) => quote!(crate::nbt::Compound),
+ Value::NbtCompound(_) => quote!(valence_nbt::Compound),
Value::Particle(_) => quote!(Particle),
Value::VillagerData { .. } => quote!(VillagerData),
Value::OptionalInt(_) => quote!(OptionalInt),
@@ -145,7 +145,7 @@ impl Value {
Value::String(_) => quote!(&str),
Value::TextComponent(_) => quote!(&Text),
Value::OptionalTextComponent(_) => quote!(Option<&Text>),
- Value::NbtCompound(_) => quote!(&crate::nbt::Compound),
+ Value::NbtCompound(_) => quote!(&valence_nbt::Compound),
_ => self.field_type(),
}
}
@@ -191,7 +191,7 @@ impl Value {
}
Value::OptionalUuid(_) => quote!(None), // TODO
Value::OptionalBlockState(_) => quote!(BlockState::default()), // TODO
- Value::NbtCompound(_) => quote!(crate::nbt::Compound::default()), // TODO
+ Value::NbtCompound(_) => quote!(valence_nbt::Compound::default()), // TODO
Value::Particle(p) => {
let variant = ident(p.to_pascal_case());
quote!(Particle::#variant)
diff --git a/crates/valence/build/entity_event.rs b/crates/valence/build/entity_event.rs
index eeae2a6..01eb982 100644
--- a/crates/valence/build/entity_event.rs
+++ b/crates/valence/build/entity_event.rs
@@ -8,13 +8,13 @@ use serde::Deserialize;
use crate::ident;
#[derive(Deserialize, Clone, Debug)]
-struct EntityData {
+struct EntityEvents {
statuses: BTreeMap,
animations: BTreeMap,
}
pub fn build() -> anyhow::Result {
- let entity_data: EntityData =
+ let entity_data: EntityEvents =
serde_json::from_str(include_str!("../../../extracted/entity_data.json"))?;
let mut statuses: Vec<_> = entity_data.statuses.into_iter().collect();
@@ -23,44 +23,39 @@ pub fn build() -> anyhow::Result {
let mut animations: Vec<_> = entity_data.animations.into_iter().collect();
animations.sort_by_key(|(_, id)| *id);
- let event_variants = statuses
+ let entity_status_variants: Vec<_> = statuses
.iter()
- .chain(animations.iter())
- .map(|(name, _)| ident(name.to_pascal_case()));
+ .map(|(name, code)| {
+ let name = ident(name.to_pascal_case());
+ let code = *code as isize;
- let status_arms = statuses.iter().map(|(name, code)| {
- let name = ident(name.to_pascal_case());
- quote! {
- Self::#name => StatusOrAnimation::Status(#code),
- }
- });
+ quote! {
+ #name = #code,
+ }
+ })
+ .collect();
- let animation_arms = animations.iter().map(|(name, code)| {
- let name = ident(name.to_pascal_case());
- quote! {
- Self::#name => StatusOrAnimation::Animation(#code),
- }
- });
+ let entity_animation_variants: Vec<_> = animations
+ .iter()
+ .map(|(name, code)| {
+ let name = ident(name.to_pascal_case());
+ let code = *code as isize;
+
+ quote! {
+ #name = #code,
+ }
+ })
+ .collect();
Ok(quote! {
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
- pub enum EntityEvent {
- #(#event_variants,)*
+ pub enum EntityStatus {
+ #(#entity_status_variants)*
}
- impl EntityEvent {
- pub(crate) fn status_or_animation(self) -> StatusOrAnimation {
- match self {
- #(#status_arms)*
- #(#animation_arms)*
- }
- }
- }
-
- #[derive(Clone, Copy, PartialEq, Eq, Debug)]
- pub(crate) enum StatusOrAnimation {
- Status(u8),
- Animation(u8),
+ #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
+ pub enum EntityAnimation {
+ #(#entity_animation_variants)*
}
})
}
diff --git a/crates/valence/examples/bench_players.rs b/crates/valence/examples/bench_players.rs
new file mode 100644
index 0000000..84d7fd3
--- /dev/null
+++ b/crates/valence/examples/bench_players.rs
@@ -0,0 +1,86 @@
+use std::time::Instant;
+
+use bevy_app::{App, CoreStage};
+use valence::client::despawn_disconnected_clients;
+use valence::client::event::default_event_handler;
+use valence::instance::{Chunk, Instance};
+use valence::prelude::*;
+
+const SPAWN_Y: i32 = 64;
+
+#[derive(Resource)]
+struct TickStart(Instant);
+
+fn main() {
+ tracing_subscriber::fmt().init();
+
+ App::new()
+ .add_plugin(
+ ServerPlugin::new(())
+ .with_connection_mode(ConnectionMode::Offline)
+ .with_compression_threshold(None)
+ .with_max_connections(50_000),
+ )
+ .add_startup_system(setup)
+ .add_system_to_stage(EventLoop, default_event_handler)
+ .add_system_to_stage(CoreStage::First, record_tick_start_time)
+ .add_system_to_stage(CoreStage::Last, print_tick_time)
+ .add_system(init_clients)
+ .add_system(despawn_disconnected_clients)
+ .add_system_set(PlayerList::default_system_set())
+ .run();
+}
+
+fn record_tick_start_time(world: &mut World) {
+ world
+ .get_resource_or_insert_with(|| TickStart(Instant::now()))
+ .0 = Instant::now();
+}
+
+fn print_tick_time(server: Res, time: Res, clients: Query<(), With>) {
+ let tick = server.current_tick();
+ if tick % (server.tps() / 2) == 0 {
+ let client_count = clients.iter().count();
+
+ let millis = time.0.elapsed().as_secs_f32() * 1000.0;
+ println!("Tick={tick}, MSPT={millis:.04}ms, Clients={client_count}");
+ }
+}
+
+fn setup(world: &mut World) {
+ let mut instance = world
+ .resource::()
+ .new_instance(DimensionId::default());
+
+ for z in -5..5 {
+ for x in -5..5 {
+ instance.insert_chunk([x, z], Chunk::default());
+ }
+ }
+
+ for z in -50..50 {
+ for x in -50..50 {
+ instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
+ }
+ }
+
+ world.spawn(instance);
+}
+
+fn init_clients(
+ mut clients: Query<(Entity, &mut Client), Added>,
+ instances: Query>,
+ mut commands: Commands,
+) {
+ let instance = instances.get_single().unwrap();
+
+ for (client_entity, mut client) in &mut clients {
+ client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
+ client.set_instance(instance);
+ client.set_game_mode(GameMode::Creative);
+
+ let player_entity = McEntity::with_uuid(EntityKind::Player, instance, client.uuid());
+
+ commands.entity(client_entity).insert(player_entity);
+ }
+}
diff --git a/crates/valence/examples/biomes.rs b/crates/valence/examples/biomes.rs
index a8172c8..4a2ad72 100644
--- a/crates/valence/examples/biomes.rs
+++ b/crates/valence/examples/biomes.rs
@@ -1,208 +1,89 @@
-use std::iter;
-use std::net::SocketAddr;
-use std::sync::atomic::{AtomicUsize, Ordering};
-
+use valence::client::despawn_disconnected_clients;
+use valence::client::event::default_event_handler;
use valence::prelude::*;
-pub fn main() -> ShutdownResult {
+const SPAWN_Y: i32 = 0;
+const BIOME_COUNT: usize = 10;
+
+pub fn main() {
tracing_subscriber::fmt().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 = 10;
-const MIN_Y: i32 = -64;
-
-#[async_trait]
-impl Config for Game {
- type ServerState = ServerState;
- type ClientState = ClientState;
- type EntityState = ();
- type WorldState = ();
- type ChunkState = ();
- type PlayerListState = ();
- type InventoryState = ();
-
- fn dimensions(&self) -> Vec {
- vec![Dimension {
- fixed_time: Some(6000),
- ..Dimension::default()
- }]
- }
-
- fn biomes(&self) -> Vec {
- (1..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()
- }
- })
- .chain(iter::once(Biome {
- name: ident!("plains"),
- ..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 = world.chunks.height();
- assert_eq!(world.chunks.min_y(), MIN_Y);
-
- 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, 1, z, BlockState::GRASS_BLOCK);
+ App::new()
+ .add_plugin(
+ ServerPlugin::new(()).with_biomes(
+ (1..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()
}
- }
-
- // Set chunk biomes
- for z in 0..4 {
- for x in 0..4 {
- for y in 0..height / 4 {
- let biome_id = server
- .shared
- .biomes()
- .nth((x + z * 4 + y * 4 * 4) % BIOME_COUNT)
- .unwrap()
- .0;
-
- 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;
- }
+ .chain(std::iter::once(Biome {
+ name: ident!("plains"),
+ ..Default::default()
+ }))
+ .collect::>(),
+ ),
+ )
+ .add_system_to_stage(EventLoop, default_event_handler)
+ .add_startup_system(setup)
+ .add_system(init_clients)
+ .add_system(despawn_disconnected_clients)
+ .add_system_set(PlayerList::default_system_set())
+ .run();
+}
- match server
- .entities
- .insert_with_uuid(EntityKind::Player, client.uuid(), ())
- {
- Some((id, entity)) => {
- entity.set_world(world_id);
- client.entity_id = id
- }
- None => {
- client.disconnect("Conflicting UUID");
- return false;
+fn setup(world: &mut World) {
+ let server = world.resource::();
+ let mut instance = server.new_instance(DimensionId::default());
+
+ for z in -5..5 {
+ for x in -5..5 {
+ let mut chunk = Chunk::new(4);
+ // Set chunk blocks
+ for z in 0..16 {
+ for x in 0..16 {
+ chunk.set_block_state(x, 63, z, BlockState::GRASS_BLOCK);
+ }
+ }
+
+ // Set the biomes of the chunk to a 4x4x4 grid of biomes
+ for cz in 0..4 {
+ for cx in 0..4 {
+ let height = chunk.section_count() * 16;
+ for cy in 0..height / 4 {
+ let biome_id = server
+ .biomes()
+ .nth((cx + cz * 4 + cy * 4 * 4) % BIOME_COUNT)
+ .unwrap()
+ .0;
+ chunk.set_biome(cx, cy, cz, biome_id);
}
}
-
- client.respawn(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[id].insert(
- client.uuid(),
- client.username(),
- client.textures().cloned(),
- client.game_mode(),
- 0,
- None,
- true,
- );
- }
-
- client.set_game_mode(GameMode::Creative);
}
+ instance.insert_chunk([x, z], chunk);
+ }
+ }
- while client.next_event().is_some() {}
+ world.spawn(instance);
+}
- if client.is_disconnected() {
- self.player_count.fetch_sub(1, Ordering::SeqCst);
- server.entities[client.entity_id].set_deleted(true);
- if let Some(id) = &server.state.player_list {
- server.player_lists[id].remove(client.uuid());
- }
- return false;
- }
-
- if client.position().y < MIN_Y as _ {
- client.teleport(spawn_pos, client.yaw(), client.pitch());
- }
-
- true
- });
+fn init_clients(
+ mut clients: Query<&mut Client, Added>,
+ instances: Query>,
+) {
+ for mut client in &mut clients {
+ client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
+ client.set_respawn_screen(true);
+ client.set_instance(instances.single());
+ client.set_game_mode(GameMode::Creative);
+ client.send_message("Welcome to Valence!".italic());
}
}
diff --git a/crates/valence/examples/building.rs b/crates/valence/examples/building.rs
index 3c1098f..795402f 100644
--- a/crates/valence/examples/building.rs
+++ b/crates/valence/examples/building.rs
@@ -1,201 +1,152 @@
-use std::net::SocketAddr;
-use std::sync::atomic::{AtomicUsize, Ordering};
-
+use valence::client::despawn_disconnected_clients;
+use valence::client::event::{
+ default_event_handler, FinishDigging, StartDigging, StartSneaking, UseItemOnBlock,
+};
use valence::prelude::*;
+use valence_protocol::types::Hand;
-pub fn main() -> ShutdownResult {
+const SPAWN_Y: i32 = 64;
+
+pub fn main() {
tracing_subscriber::fmt().init();
- valence::start_server(
- Game {
- player_count: AtomicUsize::new(0),
- },
- ServerState { player_list: None },
- )
+ App::new()
+ .add_plugin(ServerPlugin::new(()))
+ .add_system_to_stage(EventLoop, default_event_handler)
+ .add_system_to_stage(EventLoop, toggle_gamemode_on_sneak)
+ .add_system_to_stage(EventLoop, digging_creative_mode)
+ .add_system_to_stage(EventLoop, digging_survival_mode)
+ .add_system_to_stage(EventLoop, place_blocks)
+ .add_system_set(PlayerList::default_system_set())
+ .add_startup_system(setup)
+ .add_system(init_clients)
+ .add_system(despawn_disconnected_clients)
+ .run();
}
-struct Game {
- player_count: AtomicUsize,
-}
+fn setup(world: &mut World) {
+ let mut instance = world
+ .resource::()
+ .new_instance(DimensionId::default());
-struct ServerState {
- player_list: Option,
-}
-
-#[derive(Default)]
-struct ClientState {
- entity_id: EntityId,
-}
-
-const MAX_PLAYERS: usize = 10;
-
-const SIZE_X: i32 = 100;
-const SIZE_Z: i32 = 100;
-
-#[async_trait]
-impl Config for Game {
- type ServerState = ServerState;
- type ClientState = ClientState;
- type EntityState = ();
- type WorldState = ();
- type ChunkState = ();
- type PlayerListState = ();
- type InventoryState = ();
-
- fn dimensions(&self) -> Vec {
- vec![Dimension {
- fixed_time: Some(6000),
- ..Dimension::default()
- }]
- }
-
- 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(),
- ),
+ for z in -5..5 {
+ for x in -5..5 {
+ instance.insert_chunk([x, z], Chunk::default());
}
}
- fn init(&self, server: &mut Server) {
- let world = server.worlds.insert(DimensionId::default(), ()).1;
- server.state.player_list = Some(server.player_lists.insert(()).0);
-
- // initialize chunks
- for z in 0..SIZE_Z {
- for x in 0..SIZE_X {
- world
- .chunks
- .set_block_state([x, 0, z], BlockState::GRASS_BLOCK);
- }
+ for z in -25..25 {
+ for x in -25..25 {
+ instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
}
}
- fn update(&self, server: &mut Server) {
- let (world_id, world) = server.worlds.iter_mut().next().unwrap();
+ world.spawn(instance);
+}
- let spawn_pos = [SIZE_X as f64 / 2.0, 1.0, SIZE_Z as f64 / 2.0];
+fn init_clients(
+ mut clients: Query<&mut Client, Added>,
+ instances: Query>,
+) {
+ for mut client in &mut clients {
+ client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
+ client.set_instance(instances.single());
+ client.set_game_mode(GameMode::Creative);
+ client.send_message("Welcome to Valence! Build something cool.".italic());
+ }
+}
- 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, entity)) => {
- entity.set_world(world_id);
- client.entity_id = id
- }
- None => {
- client.disconnect("Conflicting UUID");
- return false;
- }
- }
-
- client.respawn(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[id].insert(
- client.uuid(),
- client.username(),
- client.textures().cloned(),
- client.game_mode(),
- 0,
- None,
- true,
- );
- }
-
- client.set_game_mode(GameMode::Creative);
- client.send_message("Welcome to Valence! Build something cool.".italic());
- }
-
- let player = server.entities.get_mut(client.entity_id).unwrap();
-
- while let Some(event) = client.next_event() {
- event.handle_default(client, player);
- match event {
- ClientEvent::StartDigging { position, .. } => {
- // Allows clients in creative mode to break blocks.
- if client.game_mode() == GameMode::Creative {
- world.chunks.set_block_state(position, BlockState::AIR);
- }
- }
- ClientEvent::FinishDigging { position, .. } => {
- // Allows clients in survival mode to break blocks.
- world.chunks.set_block_state(position, BlockState::AIR);
- }
- ClientEvent::UseItemOnBlock { .. } => {
- // TODO: reimplement when inventories are re-added.
- /*
- if hand == Hand::Main {
- if let Some(stack) = client.held_item() {
- if let Some(held_block_kind) = stack.item.to_block_kind() {
- let block_to_place = BlockState::from_kind(held_block_kind);
-
- if client.game_mode() == GameMode::Creative
- || client.consume_held_item(1).is_ok()
- {
- if world
- .chunks
- .block_state(position)
- .map(|s| s.is_replaceable())
- .unwrap_or(false)
- {
- world.chunks.set_block_state(position, block_to_place);
- } else {
- let place_at = position.get_in_direction(face);
- world.chunks.set_block_state(place_at, block_to_place);
- }
- }
- }
- }
- }
- */
- }
- _ => {}
- }
- }
-
- if client.is_disconnected() {
- self.player_count.fetch_sub(1, Ordering::SeqCst);
- player.set_deleted(true);
- if let Some(id) = &server.state.player_list {
- server.player_lists[id].remove(client.uuid());
- }
- return false;
- }
-
- if client.position().y <= -20.0 {
- client.teleport(spawn_pos, client.yaw(), client.pitch());
- }
-
- true
+fn toggle_gamemode_on_sneak(
+ mut clients: Query<&mut Client>,
+ mut events: EventReader,
+) {
+ for event in events.iter() {
+ let Ok(mut client) = clients.get_component_mut::(event.client) else {
+ continue;
+ };
+ let mode = client.game_mode();
+ client.set_game_mode(match mode {
+ GameMode::Survival => GameMode::Creative,
+ GameMode::Creative => GameMode::Survival,
+ _ => GameMode::Creative,
});
}
}
+
+fn digging_creative_mode(
+ clients: Query<&Client>,
+ mut instances: Query<&mut Instance>,
+ mut events: EventReader,
+) {
+ let mut instance = instances.single_mut();
+
+ for event in events.iter() {
+ let Ok(client) = clients.get_component::(event.client) else {
+ continue;
+ };
+ if client.game_mode() == GameMode::Creative {
+ instance.set_block_state(event.position, BlockState::AIR);
+ }
+ }
+}
+
+fn digging_survival_mode(
+ clients: Query<&Client>,
+ mut instances: Query<&mut Instance>,
+ mut events: EventReader,
+) {
+ let mut instance = instances.single_mut();
+
+ for event in events.iter() {
+ let Ok(client) = clients.get_component::(event.client) else {
+ continue;
+ };
+ if client.game_mode() == GameMode::Survival {
+ instance.set_block_state(event.position, BlockState::AIR);
+ }
+ }
+}
+
+fn place_blocks(
+ mut clients: Query<(&Client, &mut Inventory)>,
+ mut instances: Query<&mut Instance>,
+ mut events: EventReader,
+) {
+ let mut instance = instances.single_mut();
+
+ for event in events.iter() {
+ let Ok((client, mut inventory)) = clients.get_mut(event.client) else {
+ continue;
+ };
+ if event.hand != Hand::Main {
+ continue;
+ }
+
+ // get the held item
+ let slot_id = client.held_item_slot();
+ let Some(stack) = inventory.slot(slot_id) else {
+ // no item in the slot
+ continue;
+ };
+
+ let Some(block_kind) = stack.item.to_block_kind() else {
+ // can't place this item as a block
+ continue;
+ };
+
+ if client.game_mode() == GameMode::Survival {
+ // check if the player has the item in their inventory and remove
+ // it.
+ let slot = if stack.count() > 1 {
+ let mut stack = stack.clone();
+ stack.set_count(stack.count() - 1);
+ Some(stack)
+ } else {
+ None
+ };
+ inventory.replace_slot(slot_id, slot);
+ }
+ let real_pos = event.position.get_in_direction(event.face);
+ instance.set_block_state(real_pos, block_kind.to_state());
+ }
+}
diff --git a/crates/valence/examples/chest.rs b/crates/valence/examples/chest.rs
index 53ca907..d050df8 100644
--- a/crates/valence/examples/chest.rs
+++ b/crates/valence/examples/chest.rs
@@ -1,268 +1,100 @@
-pub fn main() {
- todo!("reimplement when inventories are re-added");
-}
-
-/*
-use std::net::SocketAddr;
-use std::sync::atomic::{AtomicUsize, Ordering};
-
-use num::Integer;
+use tracing::warn;
+use valence::client::despawn_disconnected_clients;
+use valence::client::event::{default_event_handler, StartSneaking, UseItemOnBlock};
use valence::prelude::*;
-use valence::protocol::VarInt;
-pub fn main() -> ShutdownResult {
+const SPAWN_Y: i32 = 64;
+const CHEST_POS: [i32; 3] = [0, SPAWN_Y + 1, 3];
+
+pub fn main() {
tracing_subscriber::fmt().init();
- valence::start_server(
- Game {
- player_count: AtomicUsize::new(0),
- },
- ServerState {
- player_list: None,
- chest: Default::default(),
- tick: 0,
- },
- )
+ App::new()
+ .add_plugin(ServerPlugin::new(()))
+ .add_system_to_stage(EventLoop, default_event_handler)
+ .add_system_to_stage(EventLoop, toggle_gamemode_on_sneak)
+ .add_system_to_stage(EventLoop, open_chest)
+ .add_system_set(PlayerList::default_system_set())
+ .add_startup_system(setup)
+ .add_system(init_clients)
+ .add_system(despawn_disconnected_clients)
+ .run();
}
-struct Game {
- player_count: AtomicUsize,
-}
+fn setup(world: &mut World) {
+ let mut instance = world
+ .resource::()
+ .new_instance(DimensionId::default());
-struct ServerState {
- player_list: Option,
- chest: InventoryId,
- tick: u32,
-}
-
-#[derive(Default)]
-struct ClientState {
- entity_id: EntityId,
- // open_inventory: Option,
-}
-
-const MAX_PLAYERS: usize = 10;
-
-const SIZE_X: usize = 100;
-const SIZE_Z: usize = 100;
-
-#[async_trait]
-impl Config for Game {
- type ServerState = ServerState;
- type ClientState = ClientState;
- type EntityState = ();
- type WorldState = ();
- type ChunkState = ();
- type PlayerListState = ();
- type InventoryState = ();
-
- fn dimensions(&self) -> Vec {
- vec![Dimension {
- fixed_time: Some(6000),
- ..Dimension::default()
- }]
- }
-
- 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()),
+ for z in -5..5 {
+ for x in -5..5 {
+ instance.insert_chunk([x, z], Chunk::default());
}
}
- fn init(&self, server: &mut Server) {
- let world = server.worlds.insert(DimensionId::default(), ()).1;
- server.state.player_list = Some(server.player_lists.insert(()).0);
-
- // initialize chunks
- for chunk_z in -2..Integer::div_ceil(&(SIZE_Z as i32), &16) + 2 {
- for chunk_x in -2..Integer::div_ceil(&(SIZE_X as i32), &16) + 2 {
- world.chunks.insert(
- [chunk_x, chunk_z],
- UnloadedChunk::default(),
- (),
- );
- }
+ for z in -25..25 {
+ for x in -25..25 {
+ instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
}
-
- // initialize blocks in the chunks
- for x in 0..SIZE_X {
- for z in 0..SIZE_Z {
- world
- .chunks
- .set_block_state((x as i32, 0, z as i32), BlockState::GRASS_BLOCK);
- }
- }
-
- world.chunks.set_block_state((50, 0, 54), BlockState::STONE);
- world.chunks.set_block_state((50, 1, 54), BlockState::CHEST);
-
- // create chest inventory
- let inv = ConfigurableInventory::new(27, VarInt(2), None);
- let title = "Extra".italic()
- + " Chesty".not_italic().bold().color(Color::RED)
- + " Chest".not_italic();
-
- let (id, _inv) = server.inventories.insert(inv, title, ());
- server.state.chest = id;
}
+ instance.set_block_state(CHEST_POS, BlockState::CHEST);
+ instance.set_block_state(
+ [CHEST_POS[0], CHEST_POS[1] - 1, CHEST_POS[2]],
+ BlockState::STONE,
+ );
- fn update(&self, server: &mut Server) {
- server.state.tick += 1;
- if server.state.tick > 10 {
- server.state.tick = 0;
- }
- let (world_id, world) = server.worlds.iter_mut().next().unwrap();
+ world.spawn(instance);
- let spawn_pos = [SIZE_X as f64 / 2.0, 1.0, SIZE_Z as f64 / 2.0];
+ let inventory = Inventory::with_title(
+ InventoryKind::Generic9x3,
+ "Extra".italic() + " Chesty".not_italic().bold().color(Color::RED) + " Chest".not_italic(),
+ );
+ world.spawn(inventory);
+}
- if let Some(inv) = server.inventories.get_mut(server.state.chest) {
- if server.state.tick == 0 {
- rotate_items(inv);
- }
- }
+fn init_clients(
+ mut clients: Query<&mut Client, Added>,
+ instances: Query>,
+) {
+ for mut client in &mut clients {
+ client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
+ client.set_instance(instances.single());
+ client.set_game_mode(GameMode::Creative);
+ }
+}
- 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, entity)) => {
- entity.set_world(world_id);
- client.state.entity_id = id
- }
- None => {
- client.disconnect("Conflicting UUID");
- return false;
- }
- }
-
- client.respawn(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[id].insert(
- client.uuid(),
- client.username(),
- client.textures().cloned(),
- client.game_mode(),
- 0,
- None,
- );
- }
-
- client.send_message("Welcome to Valence! Sneak to give yourself an item.".italic());
- }
-
- let player = server.entities.get_mut(client.state.entity_id).unwrap();
-
- while let Some(event) = client.next_event() {
- event.handle_default(client, player);
- match event {
- ClientEvent::UseItemOnBlock { hand, position, .. } => {
- if hand == Hand::Main
- && world.chunks.block_state(position) == Some(BlockState::CHEST)
- {
- client.send_message("Opening chest!");
- client.open_inventory(server.state.chest);
- }
- }
- ClientEvent::CloseScreen { window_id } => {
- if window_id > 0 {
- client.send_message(format!("Window closed: {}", window_id));
- client.send_message(format!("Chest: {:?}", server.state.chest));
- }
- }
- ClientEvent::ClickContainer {
- window_id,
- state_id,
- slot_id,
- mode,
- slot_changes,
- carried_item,
- ..
- } => {
- println!(
- "window_id: {:?}, state_id: {:?}, slot_id: {:?}, mode: {:?}, \
- slot_changes: {:?}, carried_item: {:?}",
- window_id, state_id, slot_id, mode, slot_changes, carried_item
- );
- client.cursor_held_item = carried_item;
- if let Some(window) = client.open_inventory.as_mut() {
- if let Some(obj_inv) =
- server.inventories.get_mut(window.object_inventory)
- {
- for (slot_id, slot) in slot_changes {
- if slot_id < obj_inv.slot_count() as SlotId {
- obj_inv.set_slot(slot_id, slot);
- } else {
- let offset = obj_inv.slot_count() as SlotId;
- client.inventory.set_slot(
- slot_id - offset + PlayerInventory::GENERAL_SLOTS.start,
- slot,
- );
- }
- }
- }
- }
- }
- ClientEvent::StartSneaking => {
- let slot_id: SlotId = PlayerInventory::HOTBAR_SLOTS.start;
- let stack = match client.inventory.slot(slot_id) {
- None => ItemStack::new(ItemKind::Stone, 1, None),
- Some(s) => ItemStack::new(s.item, s.count() + 1, None),
- };
- client.inventory.set_slot(slot_id, Some(stack));
- }
- _ => {}
- }
- }
-
- 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[id].remove(client.uuid());
- }
- return false;
- }
-
- if client.position().y <= -20.0 {
- client.teleport(spawn_pos, client.yaw(), client.pitch());
- }
-
- true
+fn toggle_gamemode_on_sneak(
+ mut clients: Query<&mut Client>,
+ mut events: EventReader,
+) {
+ for event in events.iter() {
+ let Ok(mut client) = clients.get_component_mut::(event.client) else {
+ continue;
+ };
+ let mode = client.game_mode();
+ client.set_game_mode(match mode {
+ GameMode::Survival => GameMode::Creative,
+ GameMode::Creative => GameMode::Survival,
+ _ => GameMode::Creative,
});
}
}
-fn rotate_items(inv: &mut ConfigurableInventory) {
- for i in 1..inv.slot_count() {
- let a = inv.slot((i - 1) as SlotId);
- let b = inv.set_slot(i as SlotId, a.cloned());
- inv.set_slot((i - 1) as SlotId, b);
+fn open_chest(
+ mut commands: Commands,
+ inventories: Query, Without)>,
+ mut events: EventReader,
+) {
+ let Ok(inventory) = inventories.get_single() else {
+ warn!("No inventories");
+ return;
+ };
+
+ for event in events.iter() {
+ if event.position != CHEST_POS.into() {
+ continue;
+ }
+ let open_inventory = OpenInventory::new(inventory);
+ commands.entity(event.client).insert(open_inventory);
}
}
-*/
diff --git a/crates/valence/examples/combat.rs b/crates/valence/examples/combat.rs
index 77311ea..bd578d2 100644
--- a/crates/valence/examples/combat.rs
+++ b/crates/valence/examples/combat.rs
@@ -1,252 +1,166 @@
-use std::net::SocketAddr;
-use std::sync::atomic::{AtomicUsize, Ordering};
-
+use glam::Vec3Swizzles;
+use valence::client::despawn_disconnected_clients;
+use valence::client::event::{
+ default_event_handler, InteractWithEntity, StartSprinting, StopSprinting,
+};
use valence::prelude::*;
-pub fn main() -> ShutdownResult {
+const SPAWN_Y: i32 = 64;
+const ARENA_RADIUS: i32 = 32;
+
+/// Attached to every client.
+#[derive(Component)]
+struct CombatState {
+ /// The tick the client was last attacked.
+ last_attacked_tick: i64,
+ has_bonus_knockback: bool,
+}
+
+pub fn main() {
tracing_subscriber::fmt().init();
- valence::start_server(
- Game {
- player_count: AtomicUsize::new(0),
- },
- None,
- )
+ App::new()
+ .add_plugin(ServerPlugin::new(()))
+ .add_startup_system(setup)
+ .add_system_to_stage(EventLoop, default_event_handler)
+ .add_system_to_stage(EventLoop, handle_combat_events)
+ .add_system(init_clients)
+ .add_system(despawn_disconnected_clients)
+ .add_system_set(PlayerList::default_system_set())
+ .add_system(teleport_oob_clients)
+ .run();
}
-struct Game {
- player_count: AtomicUsize,
-}
+fn setup(world: &mut World) {
+ let mut instance = world
+ .resource::()
+ .new_instance(DimensionId::default());
-#[derive(Default)]
-struct ClientState {
- /// The client's player entity.
- player: EntityId,
- /// The extra knockback on the first hit while sprinting.
- extra_knockback: bool,
-}
-
-#[derive(Default)]
-struct EntityState {
- client: ClientId,
- attacked: bool,
- attacker_pos: Vec3,
- extra_knockback: bool,
- last_attack_time: Ticks,
-}
-
-const MAX_PLAYERS: usize = 10;
-
-const SPAWN_POS: BlockPos = BlockPos::new(0, 20, 0);
-
-#[async_trait]
-impl Config for Game {
- type ServerState = Option;
- type ClientState = ClientState;
- type EntityState = EntityState;
- type WorldState = ();
- type ChunkState = ();
- type PlayerListState = ();
- type InventoryState = ();
-
- 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(),
- ),
+ for z in -5..5 {
+ for x in -5..5 {
+ instance.insert_chunk([x, z], Chunk::default());
}
}
- fn init(&self, server: &mut Server) {
- let (_, world) = server.worlds.insert(DimensionId::default(), ());
- server.state = Some(server.player_lists.insert(()).0);
+ // Create circular arena.
+ for z in -ARENA_RADIUS..ARENA_RADIUS {
+ for x in -ARENA_RADIUS..ARENA_RADIUS {
+ let dist = f64::hypot(x as _, z as _) / ARENA_RADIUS as f64;
- let min_y = world.chunks.min_y();
- let height = world.chunks.height();
+ if dist > 1.0 {
+ continue;
+ }
- // Create circular arena.
- let size = 2;
- for chunk_z in -size - 2..size + 2 {
- for chunk_x in -size - 2..size + 2 {
- let mut chunk = UnloadedChunk::new(height);
+ let block = if rand::random::() < dist {
+ BlockState::STONE
+ } else {
+ BlockState::DEEPSLATE
+ };
- let r = -size..size;
- if r.contains(&chunk_x) && r.contains(&chunk_z) {
- for z in 0..16 {
- for x in 0..16 {
- let block_x = chunk_x * 16 + x as i32;
- let block_z = chunk_z * 16 + z as i32;
- if f64::hypot(block_x as f64, block_z as f64) <= size as f64 * 16.0 {
- for y in 0..(SPAWN_POS.y - min_y + 1) as usize {
- chunk.set_block_state(x, y, z, BlockState::STONE);
- }
- }
- }
- }
- }
-
- world.chunks.insert([chunk_x, chunk_z], chunk, ());
+ for y in 0..SPAWN_Y {
+ instance.set_block_state([x, y, z], block);
}
}
-
- world.chunks.set_block_state(SPAWN_POS, BlockState::BEDROCK);
}
- fn update(&self, server: &mut Server) {
- let current_tick = server.current_tick();
- let (world_id, _) = server.worlds.iter_mut().next().unwrap();
+ world.spawn(instance);
+}
- server.clients.retain(|client_id, 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;
- }
+fn init_clients(
+ mut commands: Commands,
+ mut clients: Query<(Entity, &mut Client), Added>,
+ instances: Query>,
+) {
+ let instance = instances.single();
- let (player_id, player) = match server.entities.insert_with_uuid(
- EntityKind::Player,
- client.uuid(),
- EntityState::default(),
- ) {
- Some(e) => e,
- None => {
- client.disconnect("Conflicting UUID");
- return false;
- }
- };
+ for (entity, mut client) in &mut clients {
+ client.set_position([0.0, SPAWN_Y as f64, 0.0]);
+ client.set_instance(instance);
- player.set_world(world_id);
- player.client = client_id;
+ commands.entity(entity).insert((
+ CombatState {
+ last_attacked_tick: 0,
+ has_bonus_knockback: false,
+ },
+ McEntity::with_uuid(EntityKind::Player, instance, client.uuid()),
+ ));
+ }
+}
- client.player = player_id;
+fn handle_combat_events(
+ manager: Res,
+ server: Res,
+ mut start_sprinting: EventReader,
+ mut stop_sprinting: EventReader,
+ mut interact_with_entity: EventReader,
+ mut clients: Query<(&mut Client, &mut CombatState, &mut McEntity)>,
+) {
+ for &StartSprinting { client } in start_sprinting.iter() {
+ if let Ok((_, mut state, _)) = clients.get_mut(client) {
+ state.has_bonus_knockback = true;
+ }
+ }
- client.respawn(world_id);
- client.set_flat(true);
- client.set_game_mode(GameMode::Survival);
- client.teleport(
- [
- SPAWN_POS.x as f64 + 0.5,
- SPAWN_POS.y as f64 + 1.0,
- SPAWN_POS.z as f64 + 0.5,
- ],
- 0.0,
- 0.0,
- );
- client.set_player_list(server.state.clone());
+ for &StopSprinting { client } in stop_sprinting.iter() {
+ if let Ok((_, mut state, _)) = clients.get_mut(client) {
+ state.has_bonus_knockback = false;
+ }
+ }
- if let Some(id) = &server.state {
- server.player_lists[id].insert(
- client.uuid(),
- client.username(),
- client.textures().cloned(),
- client.game_mode(),
- 0,
- None,
- true,
- );
- }
+ for &InteractWithEntity {
+ client: attacker_client,
+ entity_id,
+ ..
+ } in interact_with_entity.iter()
+ {
+ let Some(victim_client) = manager.get_with_protocol_id(entity_id) else {
+ // Attacked entity doesn't exist.
+ continue
+ };
- client.send_message("Welcome to the arena.".italic());
- if self.player_count.load(Ordering::SeqCst) <= 1 {
- client.send_message("Have another player join the game with you.".italic());
- }
- }
+ let Ok([(attacker_client, mut attacker_state, _), (mut victim_client, mut victim_state, mut victim_entity)]) =
+ clients.get_many_mut([attacker_client, victim_client])
+ else {
+ // Victim or attacker does not exist, or the attacker is attacking itself.
+ continue
+ };
- while let Some(event) = client.next_event() {
- let player = server
- .entities
- .get_mut(client.player)
- .expect("missing player entity");
+ if server.current_tick() - victim_state.last_attacked_tick < 10 {
+ // Victim is still on attack cooldown.
+ continue;
+ }
- event.handle_default(client, player);
- match event {
- ClientEvent::StartSprinting => {
- client.extra_knockback = true;
- }
- ClientEvent::StopSprinting => {
- client.extra_knockback = false;
- }
- ClientEvent::InteractWithEntity { entity_id, .. } => {
- if let Some((id, target)) = server.entities.get_with_raw_id_mut(entity_id) {
- if !target.attacked
- && current_tick - target.last_attack_time >= 10
- && id != client.player
- {
- target.attacked = true;
- target.attacker_pos = client.position();
- target.extra_knockback = client.extra_knockback;
- target.last_attack_time = current_tick;
+ victim_state.last_attacked_tick = server.current_tick();
- client.extra_knockback = false;
- }
- }
- }
- _ => {}
- }
- }
+ let victim_pos = victim_client.position().xz();
+ let attacker_pos = attacker_client.position().xz();
- if client.is_disconnected() {
- self.player_count.fetch_sub(1, Ordering::SeqCst);
- server.entities[client.player].set_deleted(true);
- if let Some(id) = &server.state {
- server.player_lists[id].remove(client.uuid());
- }
- return false;
- }
+ let dir = (victim_pos - attacker_pos).normalize().as_vec2();
- if client.position().y <= 0.0 {
- client.teleport(
- [
- SPAWN_POS.x as f64 + 0.5,
- SPAWN_POS.y as f64 + 1.0,
- SPAWN_POS.z as f64 + 0.5,
- ],
- client.yaw(),
- client.pitch(),
- );
- }
+ let knockback_xz = if attacker_state.has_bonus_knockback {
+ 18.0
+ } else {
+ 8.0
+ };
+ let knockback_y = if attacker_state.has_bonus_knockback {
+ 8.432
+ } else {
+ 6.432
+ };
- true
- });
+ victim_client.set_velocity([dir.x * knockback_xz, knockback_y, dir.y * knockback_xz]);
- for (_, entity) in server.entities.iter_mut() {
- if entity.attacked {
- entity.attacked = false;
- if let Some(victim) = server.clients.get_mut(entity.client) {
- let victim_pos = Vec2::new(victim.position().x, victim.position().z);
- let attacker_pos = Vec2::new(entity.attacker_pos.x, entity.attacker_pos.z);
+ attacker_state.has_bonus_knockback = false;
- let dir = (victim_pos - attacker_pos).normalized();
+ victim_client.trigger_status(EntityStatus::DamageFromGenericSource);
+ victim_entity.trigger_status(EntityStatus::DamageFromGenericSource);
+ }
+}
- let knockback_xz = if entity.extra_knockback { 18.0 } else { 8.0 };
- let knockback_y = if entity.extra_knockback { 8.432 } else { 6.432 };
-
- let vel = Vec3::new(dir.x * knockback_xz, knockback_y, dir.y * knockback_xz);
- victim.set_velocity(vel.as_());
-
- entity.push_event(EntityEvent::DamageFromGenericSource);
- entity.push_event(EntityEvent::Damage);
- victim.send_entity_event(EntityEvent::DamageFromGenericSource);
- victim.send_entity_event(EntityEvent::Damage);
- }
- }
+fn teleport_oob_clients(mut clients: Query<&mut Client>) {
+ for mut client in &mut clients {
+ if client.position().y < 0.0 {
+ client.set_position([0.0, SPAWN_Y as _, 0.0]);
}
}
}
diff --git a/crates/valence/examples/conway.rs b/crates/valence/examples/conway.rs
index aae3263..308bcbe 100644
--- a/crates/valence/examples/conway.rs
+++ b/crates/valence/examples/conway.rs
@@ -1,297 +1,206 @@
use std::mem;
-use std::net::SocketAddr;
-use std::sync::atomic::{AtomicUsize, Ordering};
-use num::Integer;
-use rayon::prelude::*;
+use valence::client::despawn_disconnected_clients;
+use valence::client::event::{default_event_handler, StartDigging, StartSneaking};
use valence::prelude::*;
-pub fn main() -> ShutdownResult {
+const BOARD_MIN_X: i32 = -30;
+const BOARD_MAX_X: i32 = 30;
+const BOARD_MIN_Z: i32 = -30;
+const BOARD_MAX_Z: i32 = 30;
+const BOARD_Y: i32 = 64;
+
+const BOARD_SIZE_X: usize = (BOARD_MAX_X - BOARD_MIN_X + 1) as usize;
+const BOARD_SIZE_Z: usize = (BOARD_MAX_Z - BOARD_MIN_Z + 1) as usize;
+
+const SPAWN_POS: DVec3 = DVec3::new(
+ (BOARD_MIN_X + BOARD_MAX_X) as f64 / 2.0,
+ BOARD_Y as f64 + 1.0,
+ (BOARD_MIN_Z + BOARD_MAX_Z) as f64 / 2.0,
+);
+
+pub fn main() {
tracing_subscriber::fmt().init();
- valence::start_server(
- Game {
- player_count: AtomicUsize::new(0),
- },
- ServerState {
- player_list: None,
- paused: false,
- board: vec![false; SIZE_X * SIZE_Z].into_boxed_slice(),
- board_buf: vec![false; SIZE_X * SIZE_Z].into_boxed_slice(),
- },
- )
+ App::new()
+ .add_plugin(ServerPlugin::new(()).with_biomes(vec![Biome {
+ grass_color: Some(0x00ff00),
+ ..Default::default()
+ }]))
+ .add_system_to_stage(EventLoop, default_event_handler)
+ .add_system_set(PlayerList::default_system_set())
+ .add_startup_system(setup)
+ .add_system(init_clients)
+ .add_system(despawn_disconnected_clients)
+ .add_system_to_stage(EventLoop, toggle_cell_on_dig)
+ .add_system(update_board)
+ .add_system(pause_on_crouch)
+ .add_system(reset_oob_clients)
+ .run();
}
-struct Game {
- player_count: AtomicUsize,
+fn setup(world: &mut World) {
+ let mut instance = world
+ .resource::()
+ .new_instance(DimensionId::default());
+
+ for z in -10..10 {
+ for x in -10..10 {
+ instance.insert_chunk([x, z], Chunk::default());
+ }
+ }
+
+ for z in BOARD_MIN_Z..=BOARD_MAX_Z {
+ for x in BOARD_MIN_X..=BOARD_MAX_X {
+ instance.set_block_state([x, BOARD_Y, z], BlockState::DIRT);
+ }
+ }
+
+ world.spawn(instance);
+
+ world.insert_resource(LifeBoard {
+ paused: true,
+ board: vec![false; BOARD_SIZE_X * BOARD_SIZE_Z].into(),
+ board_buf: vec![false; BOARD_SIZE_X * BOARD_SIZE_Z].into(),
+ });
}
-struct ServerState {
- player_list: Option,
- paused: bool,
+fn init_clients(
+ mut clients: Query<&mut Client, Added>,
+ instances: Query>,
+) {
+ for mut client in &mut clients {
+ client.set_position(SPAWN_POS);
+ client.set_instance(instances.single());
+ client.set_game_mode(GameMode::Survival);
+
+ client.send_message("Welcome to Conway's game of life in Minecraft!".italic());
+ client.send_message(
+ "Sneak to toggle running the simulation and the left mouse button to bring blocks to \
+ life."
+ .italic(),
+ );
+ }
+}
+
+#[derive(Resource)]
+struct LifeBoard {
+ pub paused: bool,
board: Box<[bool]>,
board_buf: Box<[bool]>,
}
-#[derive(Default)]
-struct ClientState {
- entity_id: EntityId,
-}
+impl LifeBoard {
+ pub fn get(&self, x: i32, z: i32) -> bool {
+ if (BOARD_MIN_X..=BOARD_MAX_X).contains(&x) && (BOARD_MIN_Z..=BOARD_MAX_Z).contains(&z) {
+ let x = (x - BOARD_MIN_X) as usize;
+ let z = (z - BOARD_MIN_Z) as usize;
-const MAX_PLAYERS: usize = 10;
-
-const SIZE_X: usize = 100;
-const SIZE_Z: usize = 100;
-const BOARD_Y: i32 = 50;
-
-#[async_trait]
-impl Config for Game {
- type ServerState = ServerState;
- type ClientState = ClientState;
- type EntityState = ();
- type WorldState = ();
- type ChunkState = ();
- type PlayerListState = ();
- type InventoryState = ();
-
- fn dimensions(&self) -> Vec {
- vec![Dimension {
- fixed_time: Some(6000),
- ..Dimension::default()
- }]
- }
-
- fn biomes(&self) -> Vec {
- vec![Biome {
- name: ident!("plains"),
- grass_color: Some(0x00ff00),
- ..Biome::default()
- }]
- }
-
- 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(),
- ),
+ self.board[x + z * BOARD_SIZE_X]
+ } else {
+ false
}
}
- fn init(&self, server: &mut Server) {
- let world = server.worlds.insert(DimensionId::default(), ()).1;
- server.state.player_list = Some(server.player_lists.insert(()).0);
+ pub fn set(&mut self, x: i32, z: i32, value: bool) {
+ if (BOARD_MIN_X..=BOARD_MAX_X).contains(&x) && (BOARD_MIN_Z..=BOARD_MAX_Z).contains(&z) {
+ let x = (x - BOARD_MIN_X) as usize;
+ let z = (z - BOARD_MIN_Z) as usize;
- for chunk_z in -2..Integer::div_ceil(&(SIZE_Z as i32), &16) + 2 {
- for chunk_x in -2..Integer::div_ceil(&(SIZE_X as i32), &16) + 2 {
- world
- .chunks
- .insert([chunk_x, chunk_z], UnloadedChunk::default(), ());
- }
+ self.board[x + z * BOARD_SIZE_X] = value;
}
}
- fn update(&self, server: &mut Server) {
- let current_tick = server.current_tick();
- let (world_id, world) = server.worlds.iter_mut().next().unwrap();
+ pub fn update(&mut self) {
+ for (idx, cell) in self.board_buf.iter_mut().enumerate() {
+ let x = (idx % BOARD_SIZE_X) as i32;
+ let z = (idx / BOARD_SIZE_X) as i32;
- let spawn_pos = [
- SIZE_X as f64 / 2.0,
- BOARD_Y as f64 + 1.0,
- SIZE_Z as f64 / 2.0,
- ];
+ let mut live_neighbors = 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;
- }
+ for cz in z - 1..=z + 1 {
+ for cx in x - 1..=x + 1 {
+ if !(cx == x && cz == z) {
+ let idx = cx.rem_euclid(BOARD_SIZE_X as i32) as usize
+ + cz.rem_euclid(BOARD_SIZE_Z as i32) as usize * BOARD_SIZE_X;
- match server
- .entities
- .insert_with_uuid(EntityKind::Player, client.uuid(), ())
- {
- Some((id, entity)) => {
- entity.set_world(world_id);
- client.entity_id = id
- }
- None => {
- client.disconnect("Conflicting UUID");
- return false;
+ live_neighbors += self.board[idx] as i32;
}
}
-
- client.respawn(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[id].insert(
- client.uuid(),
- client.username(),
- client.textures().cloned(),
- client.game_mode(),
- 0,
- None,
- true,
- );
- }
-
- client.send_message("Welcome to Conway's game of life in Minecraft!".italic());
- client.send_message(
- "Sneak and hold the left mouse button to bring blocks to life.".italic(),
- );
}
- let player = server.entities.get_mut(client.entity_id).unwrap();
-
- while let Some(event) = client.next_event() {
- event.handle_default(client, player);
- match event {
- ClientEvent::StartDigging { position, .. } => {
- if (0..SIZE_X as i32).contains(&position.x)
- && (0..SIZE_Z as i32).contains(&position.z)
- && position.y == BOARD_Y
- {
- let index = position.x as usize + position.z as usize * SIZE_X;
-
- if !server.state.board[index] {
- // client.play_sound(
- // Ident::new("minecraft:block.note_block.
- // banjo").unwrap(),
- // SoundCategory::Block,
- // Vec3::new(position.x, position.y,
- // position.z).as_(),
- // 0.5f32,
- // 1f32,
- // );
- }
-
- server.state.board[index] = true;
- }
- }
- ClientEvent::UseItemOnBlock { hand, .. } => {
- if hand == Hand::Main {
- client.send_message("I said left click, not right click!".italic());
- }
- }
- _ => {}
- }
- }
-
- if client.is_disconnected() {
- self.player_count.fetch_sub(1, Ordering::SeqCst);
- player.set_deleted(true);
- if let Some(id) = &server.state.player_list {
- server.player_lists[id].remove(client.uuid());
- }
- return false;
- }
-
- if client.position().y <= 0.0 {
- client.teleport(spawn_pos, client.yaw(), client.pitch());
- server.state.board.fill(false);
- }
-
- if let TrackedData::Player(data) = player.data() {
- let sneaking = data.get_pose() == Pose::Sneaking;
- if sneaking != server.state.paused {
- server.state.paused = sneaking;
- // client.play_sound(
- // Ident::new("block.note_block.pling").unwrap(),
- // SoundCategory::Block,
- // client.position(),
- // 0.5,
- // if sneaking { 0.5 } else { 1.0 },
- // );
- }
- }
-
- // Display Playing in green or Paused in red
- client.set_action_bar(if server.state.paused {
- "Paused".color(Color::RED)
+ let live = self.board[idx];
+ if live {
+ *cell = (2..=3).contains(&live_neighbors);
} else {
- "Playing".color(Color::GREEN)
- });
-
- true
- });
-
- if !server.state.paused && current_tick % 2 == 0 {
- server
- .state
- .board_buf
- .par_iter_mut()
- .enumerate()
- .for_each(|(i, cell)| {
- let cx = (i % SIZE_X) as i32;
- let cz = (i / SIZE_Z) as i32;
-
- let mut live_count = 0;
- for z in cz - 1..=cz + 1 {
- for x in cx - 1..=cx + 1 {
- if !(x == cx && z == cz) {
- let i = x.rem_euclid(SIZE_X as i32) as usize
- + z.rem_euclid(SIZE_Z as i32) as usize * SIZE_X;
- if server.state.board[i] {
- live_count += 1;
- }
- }
- }
- }
-
- if server.state.board[cx as usize + cz as usize * SIZE_X] {
- *cell = (2..=3).contains(&live_count);
- } else {
- *cell = live_count == 3;
- }
- });
-
- mem::swap(&mut server.state.board, &mut server.state.board_buf);
+ *cell = live_neighbors == 3;
+ }
}
- let min_y = world.chunks.min_y();
+ mem::swap(&mut self.board, &mut self.board_buf);
+ }
- for chunk_x in 0..Integer::div_ceil(&SIZE_X, &16) {
- for chunk_z in 0..Integer::div_ceil(&SIZE_Z, &16) {
- let chunk = world
- .chunks
- .get_mut([chunk_x as i32, chunk_z as i32])
- .unwrap();
- for x in 0..16 {
- for z in 0..16 {
- let cell_x = chunk_x * 16 + x;
- let cell_z = chunk_z * 16 + z;
+ pub fn clear(&mut self) {
+ self.board.fill(false);
+ }
+}
- if cell_x < SIZE_X && cell_z < SIZE_Z {
- let b = if server.state.board[cell_x + cell_z * SIZE_X] {
- BlockState::GRASS_BLOCK
- } else {
- BlockState::DIRT
- };
- chunk.set_block_state(x, (BOARD_Y - min_y) as usize, z, b);
- }
- }
- }
+fn toggle_cell_on_dig(mut events: EventReader, mut board: ResMut) {
+ for event in events.iter() {
+ let (x, z) = (event.position.x, event.position.z);
+
+ let live = board.get(x, z);
+ board.set(x, z, !live);
+ }
+}
+
+fn update_board(
+ mut board: ResMut,
+ mut instances: Query<&mut Instance>,
+ server: Res,
+) {
+ if !board.paused && server.current_tick() % 2 == 0 {
+ board.update();
+ }
+
+ let mut instance = instances.single_mut();
+
+ for z in BOARD_MIN_Z..=BOARD_MAX_Z {
+ for x in BOARD_MIN_X..=BOARD_MAX_X {
+ let block = if board.get(x, z) {
+ BlockState::GRASS_BLOCK
+ } else {
+ BlockState::DIRT
+ };
+
+ instance.set_block_state([x, BOARD_Y, z], block);
+ }
+ }
+}
+
+fn pause_on_crouch(
+ mut events: EventReader,
+ mut board: ResMut,
+ mut clients: Query<&mut Client>,
+) {
+ for _ in events.iter() {
+ board.paused = !board.paused;
+
+ for mut client in clients.iter_mut() {
+ if board.paused {
+ client.set_action_bar("Paused".italic().color(Color::RED));
+ } else {
+ client.set_action_bar("Playing".italic().color(Color::GREEN));
}
}
}
}
+
+fn reset_oob_clients(mut clients: Query<&mut Client>, mut board: ResMut) {
+ for mut client in &mut clients {
+ if client.position().y < 0.0 {
+ client.set_position(SPAWN_POS);
+ board.clear();
+ }
+ }
+}
diff --git a/crates/valence/examples/cow_sphere.rs b/crates/valence/examples/cow_sphere.rs
index df5a787..1c88c16 100644
--- a/crates/valence/examples/cow_sphere.rs
+++ b/crates/valence/examples/cow_sphere.rs
@@ -1,222 +1,100 @@
-use std::borrow::Cow;
use std::f64::consts::TAU;
-use std::net::SocketAddr;
-use std::sync::atomic::{AtomicUsize, Ordering};
+use glam::{DQuat, EulerRot};
+use valence::client::despawn_disconnected_clients;
+use valence::client::event::default_event_handler;
+use valence::math::to_yaw_and_pitch;
use valence::prelude::*;
-pub fn main() -> ShutdownResult {
+const SPHERE_CENTER: DVec3 = DVec3::new(0.5, SPAWN_POS.y as f64 + 2.0, 0.5);
+const SPHERE_AMOUNT: usize = 200;
+const SPHERE_KIND: EntityKind = EntityKind::Cow;
+const SPHERE_MIN_RADIUS: f64 = 6.0;
+const SPHERE_MAX_RADIUS: f64 = 12.0;
+const SPHERE_FREQ: f64 = 0.5;
+
+const SPAWN_POS: BlockPos = BlockPos::new(0, 100, -16);
+
+/// Marker component for entities that are part of the sphere.
+#[derive(Component)]
+struct SpherePart;
+
+fn main() {
tracing_subscriber::fmt().init();
- valence::start_server(
- Game {
- player_count: AtomicUsize::new(0),
- },
- ServerState {
- player_list: None,
- cows: vec![],
- },
- )
+ App::new()
+ .add_plugin(ServerPlugin::new(()))
+ .add_system_to_stage(EventLoop, default_event_handler)
+ .add_startup_system(setup)
+ .add_system(init_clients)
+ .add_system(update_sphere)
+ .add_system(despawn_disconnected_clients)
+ .add_system_set(PlayerList::default_system_set())
+ .run();
}
-struct Game {
- player_count: AtomicUsize,
-}
+fn setup(world: &mut World) {
+ let mut instance = world
+ .resource::()
+ .new_instance(DimensionId::default());
-struct ServerState {
- player_list: Option,
- cows: Vec,
-}
-
-#[derive(Default)]
-struct ClientState {
- entity_id: EntityId,
-}
-
-const MAX_PLAYERS: usize = 10;
-
-const SPAWN_POS: BlockPos = BlockPos::new(0, 100, -25);
-
-#[async_trait]
-impl Config for Game {
- type ServerState = ServerState;
- type ClientState = ClientState;
- type EntityState = ();
- type WorldState = ();
- type ChunkState = ();
- type PlayerListState = ();
- type InventoryState = ();
-
- async fn server_list_ping(
- &self,
- _server: &SharedServer,
- _remote_addr: SocketAddr,
- _protocol_version: i32,
- ) -> ServerListPing {
- const SAMPLE: &[PlayerSampleEntry] = &[
- PlayerSampleEntry {
- name: Cow::Borrowed("§cFirst Entry"),
- id: Uuid::nil(),
- },
- PlayerSampleEntry {
- name: Cow::Borrowed("§6§oSecond Entry"),
- id: Uuid::nil(),
- },
- ];
-
- ServerListPing::Respond {
- online_players: self.player_count.load(Ordering::SeqCst) as i32,
- max_players: MAX_PLAYERS as i32,
- player_sample: SAMPLE.into(),
- description: "Hello Valence!".color(Color::AQUA),
- favicon_png: Some(
- include_bytes!("../../../assets/logo-64x64.png")
- .as_slice()
- .into(),
- ),
+ for z in -5..5 {
+ for x in -5..5 {
+ instance.insert_chunk([x, z], Chunk::default());
}
}
- fn init(&self, server: &mut Server) {
- let (world_id, world) = server.worlds.insert(DimensionId::default(), ());
- server.state.player_list = Some(server.player_lists.insert(()).0);
+ instance.set_block_state(SPAWN_POS, BlockState::BEDROCK);
- let size = 5;
- for z in -size..size {
- for x in -size..size {
- world.chunks.insert([x, z], UnloadedChunk::default(), ());
- }
- }
+ let instance_id = world.spawn(instance).id();
- world.chunks.set_block_state(SPAWN_POS, BlockState::BEDROCK);
+ world.spawn_batch(
+ [0; SPHERE_AMOUNT].map(|_| (McEntity::new(SPHERE_KIND, instance_id), SpherePart)),
+ );
+}
- server.state.cows.extend((0..200).map(|_| {
- let (id, e) = server.entities.insert(EntityKind::Cow, ());
- e.set_world(world_id);
- id
- }));
+fn init_clients(
+ mut clients: Query<&mut Client, Added>,
+ instances: Query>,
+) {
+ for mut client in &mut clients {
+ client.set_position([
+ SPAWN_POS.x as f64 + 0.5,
+ SPAWN_POS.y as f64 + 1.0,
+ SPAWN_POS.z as f64 + 0.5,
+ ]);
+ client.set_instance(instances.single());
+ client.set_game_mode(GameMode::Creative);
}
+}
- fn update(&self, server: &mut Server) {
- let current_tick = server.current_tick();
- let (world_id, _) = server.worlds.iter_mut().next().expect("missing world");
+fn update_sphere(server: Res, mut parts: Query<&mut McEntity, With>) {
+ let time = server.current_tick() as f64 / server.tps() as f64;
- 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;
- }
+ let rot_angles = DVec3::new(0.2, 0.4, 0.6) * SPHERE_FREQ * time * TAU % TAU;
+ let rot = DQuat::from_euler(EulerRot::XYZ, rot_angles.x, rot_angles.y, rot_angles.z);
- match server
- .entities
- .insert_with_uuid(EntityKind::Player, client.uuid(), ())
- {
- Some((id, entity)) => {
- entity.set_world(world_id);
- client.entity_id = id
- }
- None => {
- client.disconnect("Conflicting UUID");
- return false;
- }
- }
+ let radius = lerp(
+ SPHERE_MIN_RADIUS,
+ SPHERE_MAX_RADIUS,
+ ((time * SPHERE_FREQ * TAU).sin() + 1.0) / 2.0,
+ );
- client.respawn(world_id);
- client.set_flat(true);
- client.set_game_mode(GameMode::Creative);
- client.teleport(
- [
- SPAWN_POS.x as f64 + 0.5,
- SPAWN_POS.y as f64 + 1.0,
- SPAWN_POS.z as f64 + 0.5,
- ],
- 0.0,
- 0.0,
- );
- client.set_player_list(server.state.player_list.clone());
+ for (mut entity, p) in parts.iter_mut().zip(fibonacci_spiral(SPHERE_AMOUNT)) {
+ debug_assert!(p.is_normalized());
- if let Some(id) = &server.state.player_list {
- server.player_lists[id].insert(
- client.uuid(),
- client.username(),
- client.textures().cloned(),
- client.game_mode(),
- 0,
- None,
- true,
- );
- }
- }
+ let dir = rot * p;
+ let (yaw, pitch) = to_yaw_and_pitch(dir.as_vec3());
- let entity = &mut server.entities[client.entity_id];
-
- if client.is_disconnected() {
- self.player_count.fetch_sub(1, Ordering::SeqCst);
- if let Some(id) = &server.state.player_list {
- server.player_lists[id].remove(client.uuid());
- }
- entity.set_deleted(true);
-
- return false;
- }
-
- while let Some(event) = client.next_event() {
- event.handle_default(client, entity);
- }
-
- true
- });
-
- let time = current_tick as f64 / server.shared.tick_rate() as f64;
-
- let rot = Mat3::rotation_x(time * TAU * 0.1)
- .rotated_y(time * TAU * 0.2)
- .rotated_z(time * TAU * 0.3);
-
- let radius = 6.0 + ((time * TAU / 2.5).sin() + 1.0) / 2.0 * 10.0;
-
- let player_pos = server
- .clients
- .iter()
- .next()
- .map(|c| c.1.position())
- .unwrap_or_default();
-
- // TODO: remove hardcoded eye pos.
- let eye_pos = Vec3::new(player_pos.x, player_pos.y + 1.6, player_pos.z);
-
- for (cow_id, p) in server
- .state
- .cows
- .iter()
- .cloned()
- .zip(fibonacci_spiral(server.state.cows.len()))
- {
- let cow = server.entities.get_mut(cow_id).expect("missing cow");
- let rotated = p * rot;
- let transformed = rotated * radius + [0.5, SPAWN_POS.y as f64 + 2.0, 0.5];
-
- let yaw = f32::atan2(rotated.z as f32, rotated.x as f32).to_degrees() - 90.0;
- let (looking_yaw, looking_pitch) =
- to_yaw_and_pitch((eye_pos - transformed).normalized());
-
- cow.set_position(transformed);
- cow.set_yaw(yaw);
- cow.set_pitch(looking_pitch as f32);
- cow.set_head_yaw(looking_yaw as f32);
- }
+ entity.set_position(SPHERE_CENTER + dir * radius);
+ entity.set_yaw(yaw);
+ entity.set_head_yaw(yaw);
+ entity.set_pitch(pitch);
}
}
/// Distributes N points on the surface of a unit sphere.
-fn fibonacci_spiral(n: usize) -> impl Iterator- > {
+fn fibonacci_spiral(n: usize) -> impl Iterator
- {
let golden_ratio = (1.0 + 5_f64.sqrt()) / 2.0;
(0..n).map(move |i| {
@@ -227,6 +105,10 @@ fn fibonacci_spiral(n: usize) -> impl Iterator
- > {
// Map from unit square to unit sphere.
let theta = x * TAU;
let phi = (1.0 - 2.0 * y).acos();
- Vec3::new(theta.cos() * phi.sin(), theta.sin() * phi.sin(), phi.cos())
+ DVec3::new(theta.cos() * phi.sin(), theta.sin() * phi.sin(), phi.cos())
})
}
+
+fn lerp(a: f64, b: f64, t: f64) -> f64 {
+ a * (1.0 - t) + b * t
+}
diff --git a/crates/valence/examples/death.rs b/crates/valence/examples/death.rs
index 9f46bc0..e100d6a 100644
--- a/crates/valence/examples/death.rs
+++ b/crates/valence/examples/death.rs
@@ -1,332 +1,93 @@
-use std::net::SocketAddr;
-use std::sync::atomic::{AtomicUsize, Ordering};
-
+use tracing::warn;
+use valence::client::despawn_disconnected_clients;
+use valence::client::event::{default_event_handler, PerformRespawn, StartSneaking};
use valence::prelude::*;
-pub fn main() -> ShutdownResult {
+const SPAWN_Y: i32 = 64;
+
+pub fn main() {
tracing_subscriber::fmt().init();
- valence::start_server(
- Game {
- player_count: AtomicUsize::new(0),
- },
- ServerState::default(),
- )
+ App::new()
+ .add_plugin(ServerPlugin::new(()))
+ .add_system_to_stage(EventLoop, default_event_handler)
+ .add_system_to_stage(EventLoop, squat_and_die)
+ .add_system_to_stage(EventLoop, necromancy)
+ .add_system_set(PlayerList::default_system_set())
+ .add_startup_system(setup)
+ .add_system(init_clients)
+ .add_system(despawn_disconnected_clients)
+ .run();
}
-struct Game {
- player_count: AtomicUsize,
-}
+fn setup(world: &mut World) {
+ for block in [BlockState::GRASS_BLOCK, BlockState::DEEPSLATE] {
+ let mut instance = world
+ .resource::()
+ .new_instance(DimensionId::default());
-#[derive(Default)]
-struct ClientState {
- entity_id: EntityId,
- // World and position to respawn at
- respawn_location: (WorldId, Vec3),
- // Anticheat measure
- can_respawn: bool,
-}
-
-#[derive(Default)]
-struct ServerState {
- player_list: Option,
- first_world: WorldId,
- second_world: WorldId,
- third_world: WorldId,
-}
-
-const MAX_PLAYERS: usize = 10;
-
-const FLOOR_Y: i32 = 64;
-const PLATFORM_X: i32 = 20;
-const PLATFORM_Z: i32 = 20;
-const LEFT_DEATH_LINE: i32 = 16;
-const RIGHT_DEATH_LINE: i32 = 4;
-
-const FIRST_WORLD_SPAWN_BLOCK: BlockPos = BlockPos::new(10, FLOOR_Y, 10);
-const SECOND_WORLD_SPAWN_BLOCK: BlockPos = BlockPos::new(5, FLOOR_Y, 5);
-const THIRD_WORLD_SPAWN_BLOCK: BlockPos = BlockPos::new(5, FLOOR_Y, 5);
-
-#[derive(Clone, Copy, PartialEq, Eq)]
-enum WhichWorld {
- First,
- Second,
- Third,
-}
-
-// Returns position of player standing on `pos` block
-fn block_pos_to_vec(pos: BlockPos) -> Vec3 {
- Vec3::new(
- (pos.x as f64) + 0.5,
- (pos.y as f64) + 1.0,
- (pos.z as f64) + 0.5,
- )
-}
-
-#[async_trait]
-impl Config for Game {
- type ServerState = ServerState;
- type ClientState = ClientState;
- type EntityState = ();
- type WorldState = ();
- type ChunkState = ();
- type PlayerListState = ();
- type InventoryState = ();
-
- fn dimensions(&self) -> Vec {
- vec![
- Dimension {
- fixed_time: Some(6000),
- ..Dimension::default()
- },
- Dimension {
- fixed_time: Some(19000),
- ..Dimension::default()
- },
- ]
- }
-
- 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,
- description: "Hello Valence!".color(Color::AQUA),
- favicon_png: Some(
- include_bytes!("../../../assets/logo-64x64.png")
- .as_slice()
- .into(),
- ),
- player_sample: Default::default(),
+ for z in -5..5 {
+ for x in -5..5 {
+ instance.insert_chunk([x, z], Chunk::default());
+ }
}
- }
- fn init(&self, server: &mut Server) {
- // We created server with meaningless default state.
- // Let's create three worlds and create new ServerState.
- server.state = ServerState {
- player_list: Some(server.player_lists.insert(()).0),
- first_world: create_world(server, FIRST_WORLD_SPAWN_BLOCK, WhichWorld::First),
- second_world: create_world(server, SECOND_WORLD_SPAWN_BLOCK, WhichWorld::Second),
- third_world: create_world(server, THIRD_WORLD_SPAWN_BLOCK, WhichWorld::Third),
+ for z in -25..25 {
+ for x in -25..25 {
+ instance.set_block_state([x, SPAWN_Y, z], block);
+ }
+ }
+
+ world.spawn(instance);
+ }
+}
+
+fn init_clients(
+ mut clients: Query<&mut Client, Added>,
+ instances: Query>,
+) {
+ let instance = instances.into_iter().next().unwrap();
+
+ for mut client in &mut clients {
+ client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
+ client.set_respawn_screen(true);
+ client.set_instance(instance);
+ client.send_message(
+ "Welcome to Valence! Press shift to die in the game (but not in real life).".italic(),
+ );
+ }
+}
+
+fn squat_and_die(mut clients: Query<&mut Client>, mut events: EventReader) {
+ for event in events.iter() {
+ let Ok(mut client) = clients.get_component_mut::(event.client) else {
+ warn!("Client {:?} not found", event.client);
+ continue;
};
- }
- fn update(&self, server: &mut Server) {
- 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, entity)) => {
- entity.set_world(server.state.first_world);
- client.entity_id = id
- }
- None => {
- client.disconnect("Conflicting UUID");
- return false;
- }
- }
-
- client.respawn_location = (
- server.state.first_world,
- block_pos_to_vec(FIRST_WORLD_SPAWN_BLOCK),
- );
-
- // `set_spawn_position` is used for compass _only_
- client.set_spawn_position(FIRST_WORLD_SPAWN_BLOCK, 0.0);
-
- client.set_flat(true);
- client.respawn(server.state.first_world);
- client.teleport(client.respawn_location.1, 0.0, 0.0);
-
- client.set_player_list(server.state.player_list.clone());
-
- server
- .player_lists
- .get_mut(server.state.player_list.as_ref().unwrap())
- .insert(
- client.uuid(),
- client.username(),
- client.textures().cloned(),
- client.game_mode(),
- 0,
- None,
- true,
- );
-
- client.set_respawn_screen(true);
-
- client.send_message("Welcome to the death example!".italic());
- client.send_message("Step over the left line to die. :)");
- client.send_message("Step over the right line to die and respawn in second world.");
- client.send_message("Jumping down kills you and spawns you in another dimension.");
- client.send_message("Sneaking triggers game credits after which you respawn.");
- }
-
- // TODO after inventory support is added, show interaction with compass.
-
- // Handling respawn locations
- if !client.can_respawn {
- if client.position().y < 0.0 {
- client.can_respawn = true;
- client.kill(None, "You fell");
- // You could have also killed the player with `Client::set_health_and_food`,
- // however you cannot send a message to the death screen
- // that way
- if client.world() == server.state.third_world {
- // Falling in third world gets you back to the first world
- client.respawn_location = (
- server.state.first_world,
- block_pos_to_vec(FIRST_WORLD_SPAWN_BLOCK),
- );
- client.set_spawn_position(FIRST_WORLD_SPAWN_BLOCK, 0.0);
- } else {
- // falling in first and second world will cause player to spawn in third
- // world
- client.respawn_location = (
- server.state.third_world,
- block_pos_to_vec(THIRD_WORLD_SPAWN_BLOCK),
- );
- // This is for compass to point at
- client.set_spawn_position(THIRD_WORLD_SPAWN_BLOCK, 0.0);
- }
- }
-
- // Death lanes in the first world
- if client.world() == server.state.first_world {
- let death_msg = "You shouldn't cross suspicious lines";
-
- if client.position().x >= LEFT_DEATH_LINE as f64 {
- // Client went to the left, he dies
- client.can_respawn = true;
- client.kill(None, death_msg);
- }
-
- if client.position().x <= RIGHT_DEATH_LINE as f64 {
- // Client went to the right, he dies and spawns in world2
- client.can_respawn = true;
- client.kill(None, death_msg);
- client.respawn_location = (
- server.state.second_world,
- block_pos_to_vec(SECOND_WORLD_SPAWN_BLOCK),
- );
- client.set_spawn_position(SECOND_WORLD_SPAWN_BLOCK, 0.0);
- }
- }
- }
-
- let player = server.entities.get_mut(client.entity_id).unwrap();
-
- while let Some(event) = client.next_event() {
- event.handle_default(client, player);
- match event {
- ClientEvent::PerformRespawn => {
- if !client.can_respawn {
- client.disconnect("Unexpected PerformRespawn");
- return false;
- }
- // Let's respawn our player. `spawn` will load the world, but we are
- // responsible for teleporting the player.
-
- // You can store respawn however you want, for example in `Client`'s state.
- let spawn = client.respawn_location;
- client.respawn(spawn.0);
- player.set_world(spawn.0);
- client.teleport(spawn.1, 0.0, 0.0);
- client.can_respawn = false;
- }
- ClientEvent::StartSneaking => {
- // Roll the credits, respawn after
- client.can_respawn = true;
- client.win_game(true);
- }
- _ => {}
- }
- }
-
- if client.is_disconnected() {
- self.player_count.fetch_sub(1, Ordering::SeqCst);
- server.entities[client.entity_id].set_deleted(true);
-
- if let Some(list) = client.player_list() {
- server.player_lists.get_mut(list).remove(client.uuid());
- }
-
- return false;
- }
-
- true
- });
+ client.kill(None, "Squatted too hard.");
}
}
-// Boilerplate for creating world
-fn create_world(server: &mut Server, spawn_pos: BlockPos, world_type: WhichWorld) -> WorldId {
- let dimension = match world_type {
- WhichWorld::First => server.shared.dimensions().next().unwrap(),
- WhichWorld::Second => server.shared.dimensions().next().unwrap(),
- WhichWorld::Third => server.shared.dimensions().nth(1).unwrap(),
- };
-
- let (world_id, world) = server.worlds.insert(dimension.0, ());
-
- // Create chunks
- for chunk_z in -3..3 {
- for chunk_x in -3..3 {
- world
- .chunks
- .insert([chunk_x, chunk_z], UnloadedChunk::default(), ());
- }
+fn necromancy(
+ mut clients: Query<&mut Client>,
+ mut events: EventReader,
+ instances: Query>,
+) {
+ for event in events.iter() {
+ let Ok(mut client) = clients.get_component_mut::(event.client) else {
+ continue;
+ };
+ client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
+ client.set_velocity([0.0, 0.0, 0.0]);
+ client.set_yaw(0.0);
+ client.set_pitch(0.0);
+ // make the client respawn in another instance
+ let idx = instances
+ .iter()
+ .position(|i| i == client.instance())
+ .unwrap();
+ let count = instances.iter().count();
+ client.set_instance(instances.into_iter().nth((idx + 1) % count).unwrap());
}
-
- // Create platform
- let platform_block = match world_type {
- WhichWorld::First => BlockState::END_STONE,
- WhichWorld::Second => BlockState::AMETHYST_BLOCK,
- WhichWorld::Third => BlockState::BLACKSTONE,
- };
-
- for z in 0..PLATFORM_Z {
- for x in 0..PLATFORM_X {
- world
- .chunks
- .set_block_state([x, FLOOR_Y, z], platform_block);
- }
- }
-
- // Set death lines
- if world_type == WhichWorld::First {
- for z in 0..PLATFORM_Z {
- world
- .chunks
- .set_block_state([LEFT_DEATH_LINE, FLOOR_Y, z], BlockState::GOLD_BLOCK);
- world
- .chunks
- .set_block_state([RIGHT_DEATH_LINE, FLOOR_Y, z], BlockState::DIAMOND_BLOCK);
- }
- }
-
- // Set spawn block
- world
- .chunks
- .set_block_state(spawn_pos, BlockState::REDSTONE_BLOCK);
-
- world_id
}
diff --git a/crates/valence/examples/entity_raycast.rs b/crates/valence/examples/entity_raycast.rs
deleted file mode 100644
index ce53d09..0000000
--- a/crates/valence/examples/entity_raycast.rs
+++ /dev/null
@@ -1,433 +0,0 @@
-use std::net::SocketAddr;
-use std::sync::atomic::{AtomicUsize, Ordering};
-
-use valence::prelude::*;
-use valence_protocol::entity_meta::{Facing, PaintingKind};
-use valence_spatial_index::bvh::Bvh;
-use valence_spatial_index::{RaycastHit, SpatialIndex, WithAabb};
-
-pub fn main() -> ShutdownResult {
- tracing_subscriber::fmt().init();
-
- valence::start_server(
- Game {
- player_count: AtomicUsize::new(0),
- },
- ServerState {
- player_list: None,
- bvh: Bvh::new(),
- world: WorldId::NULL,
- },
- )
-}
-
-struct Game {
- player_count: AtomicUsize,
-}
-
-#[derive(Default)]
-struct ClientState {
- player: EntityId,
- shulker_bullet: EntityId,
-}
-
-const MAX_PLAYERS: usize = 10;
-
-const SPAWN_POS: BlockPos = BlockPos::new(0, 100, -5);
-
-const PLAYER_EYE_HEIGHT: f64 = 1.62;
-
-// TODO
-// const PLAYER_SNEAKING_EYE_HEIGHT: f64 = 1.495;
-
-struct ServerState {
- player_list: Option,
- bvh: Bvh>,
- world: WorldId,
-}
-
-#[async_trait]
-impl Config for Game {
- type ServerState = ServerState;
- type ClientState = ClientState;
- type EntityState = ();
- type WorldState = ();
- type ChunkState = ();
- type PlayerListState = ();
- type InventoryState = ();
-
- 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_id, world) = server.worlds.insert(DimensionId::default(), ());
- server.state.world = world_id;
-
- let (player_list_id, player_list) = server.player_lists.insert(());
- server.state.player_list = Some(player_list_id);
-
- let size = 5;
- for z in -size..size {
- for x in -size..size {
- world.chunks.insert([x, z], UnloadedChunk::default(), ());
- }
- }
-
- world.chunks.set_block_state(SPAWN_POS, BlockState::BEDROCK);
-
- // ==== Item Frames ==== //
- let (_, e) = server.entities.insert(EntityKind::ItemFrame, ());
- if let TrackedData::ItemFrame(i) = e.data_mut() {
- i.set_rotation(Facing::North as i32);
- }
- e.set_world(world_id);
- e.set_position([2.0, 102.0, 0.0]);
-
- let (_, e) = server.entities.insert(EntityKind::ItemFrame, ());
- if let TrackedData::ItemFrame(i) = e.data_mut() {
- i.set_rotation(Facing::East as i32);
- }
- e.set_world(world_id);
- e.set_position([3.0, 102.0, 0.0]);
-
- let (_, e) = server.entities.insert(EntityKind::GlowItemFrame, ());
- if let TrackedData::GlowItemFrame(i) = e.data_mut() {
- i.set_rotation(Facing::South as i32);
- }
- e.set_world(world_id);
- e.set_position([4.0, 102.0, 0.0]);
-
- let (_, e) = server.entities.insert(EntityKind::GlowItemFrame, ());
- if let TrackedData::GlowItemFrame(i) = e.data_mut() {
- i.set_rotation(Facing::West as i32);
- }
- e.set_world(world_id);
- e.set_position([5.0, 102.0, 0.0]);
-
- // ==== Paintings ==== //
- let (_, e) = server.entities.insert(EntityKind::Painting, ());
- if let TrackedData::Painting(p) = e.data_mut() {
- p.set_variant(PaintingKind::Pigscene);
- }
- e.set_world(world_id);
- e.set_yaw(180.0);
- e.set_position([0.0, 102.0, 0.0]);
-
- let (_, e) = server.entities.insert(EntityKind::Painting, ());
- if let TrackedData::Painting(p) = e.data_mut() {
- p.set_variant(PaintingKind::DonkeyKong);
- }
- e.set_world(world_id);
- e.set_yaw(90.0);
- e.set_position([-4.0, 102.0, 0.0]);
-
- let (_, e) = server.entities.insert(EntityKind::Painting, ());
- if let TrackedData::Painting(p) = e.data_mut() {
- p.set_variant(PaintingKind::Void);
- }
- e.set_world(world_id);
- e.set_position([-6.0, 102.0, 0.0]);
-
- let (_, e) = server.entities.insert(EntityKind::Painting, ());
- if let TrackedData::Painting(p) = e.data_mut() {
- p.set_variant(PaintingKind::Aztec);
- }
- e.set_yaw(270.0);
- e.set_world(world_id);
- e.set_position([-7.0, 102.0, 0.0]);
-
- // ==== Shulkers ==== //
- let (_, e) = server.entities.insert(EntityKind::Shulker, ());
- if let TrackedData::Shulker(s) = e.data_mut() {
- s.set_peek_amount(100);
- s.set_attached_face(Facing::West);
- }
- e.set_world(world_id);
- e.set_position([-4.0, 102.0, -8.0]);
-
- let (_, e) = server.entities.insert(EntityKind::Shulker, ());
- if let TrackedData::Shulker(s) = e.data_mut() {
- s.set_peek_amount(75);
- s.set_attached_face(Facing::Up);
- }
- e.set_world(world_id);
- e.set_position([-1.0, 102.0, -8.0]);
-
- let (_, e) = server.entities.insert(EntityKind::Shulker, ());
- if let TrackedData::Shulker(s) = e.data_mut() {
- s.set_peek_amount(50);
- s.set_attached_face(Facing::Down);
- }
- e.set_world(world_id);
- e.set_position([2.0, 102.0, -8.0]);
-
- let (_, e) = server.entities.insert(EntityKind::Shulker, ());
- if let TrackedData::Shulker(s) = e.data_mut() {
- s.set_peek_amount(25);
- s.set_attached_face(Facing::East);
- }
- e.set_world(world_id);
- e.set_position([5.0, 102.0, -8.0]);
-
- let (_, e) = server.entities.insert(EntityKind::Shulker, ());
- if let TrackedData::Shulker(s) = e.data_mut() {
- s.set_peek_amount(0);
- s.set_attached_face(Facing::North);
- }
- e.set_world(world_id);
- e.set_position([8.0, 102.0, -8.0]);
-
- // ==== Slimes ==== //
- let (_, e) = server.entities.insert(EntityKind::Slime, ());
- if let TrackedData::Slime(s) = e.data_mut() {
- s.set_slime_size(30);
- }
- e.set_world(world_id);
- e.set_yaw(180.0);
- e.set_head_yaw(180.0);
- e.set_position([12.0, 102.0, 10.0]);
-
- let (_, e) = server.entities.insert(EntityKind::MagmaCube, ());
- if let TrackedData::MagmaCube(m) = e.data_mut() {
- m.set_slime_size(30);
- }
- e.set_world(world_id);
- e.set_yaw(180.0);
- e.set_head_yaw(180.0);
- e.set_position([-12.0, 102.0, 10.0]);
-
- // ==== Sheep ==== //
- let (_, e) = server.entities.insert(EntityKind::Sheep, ());
- if let TrackedData::Sheep(s) = e.data_mut() {
- s.set_color(6);
- s.set_child(true);
- }
- e.set_world(world_id);
- e.set_position([-5.0, 101.0, -4.5]);
- e.set_yaw(270.0);
- e.set_head_yaw(270.0);
-
- let (_, e) = server.entities.insert(EntityKind::Sheep, ());
- if let TrackedData::Sheep(s) = e.data_mut() {
- s.set_color(6);
- }
- e.set_world(world_id);
- e.set_position([5.0, 101.0, -4.5]);
- e.set_yaw(90.0);
- e.set_head_yaw(90.0);
-
- // ==== Players ==== //
- let player_poses = [
- Pose::Standing,
- Pose::Sneaking,
- Pose::FallFlying,
- Pose::Sleeping,
- Pose::Swimming,
- Pose::SpinAttack,
- Pose::Dying,
- ];
-
- for (i, pose) in player_poses.into_iter().enumerate() {
- player_list.insert(
- Uuid::from_u128(i as u128),
- format!("fake_player_{i}"),
- None,
- GameMode::Survival,
- 0,
- None,
- true,
- );
-
- let (_, e) = server
- .entities
- .insert_with_uuid(EntityKind::Player, Uuid::from_u128(i as u128), ())
- .unwrap();
- if let TrackedData::Player(p) = e.data_mut() {
- p.set_pose(pose);
- }
- e.set_world(world_id);
- e.set_position([-3.0 + i as f64 * 2.0, 104.0, -9.0]);
- }
-
- // ==== Warden ==== //
- let (_, e) = server.entities.insert(EntityKind::Warden, ());
- e.set_world(world_id);
- e.set_position([-7.0, 102.0, -4.5]);
- e.set_yaw(270.0);
- e.set_head_yaw(270.0);
-
- let (_, e) = server.entities.insert(EntityKind::Warden, ());
- if let TrackedData::Warden(w) = e.data_mut() {
- w.set_pose(Pose::Emerging);
- }
- e.set_world(world_id);
- e.set_position([-7.0, 102.0, -6.5]);
- e.set_yaw(270.0);
- e.set_head_yaw(270.0);
-
- // ==== Goat ==== //
- let (_, e) = server.entities.insert(EntityKind::Goat, ());
- e.set_world(world_id);
- e.set_position([5.0, 103.0, -4.5]);
- e.set_yaw(270.0);
- e.set_head_yaw(90.0);
-
- let (_, e) = server.entities.insert(EntityKind::Goat, ());
- if let TrackedData::Goat(g) = e.data_mut() {
- g.set_pose(Pose::LongJumping);
- }
- e.set_world(world_id);
- e.set_position([5.0, 103.0, -3.5]);
- e.set_yaw(270.0);
- e.set_head_yaw(90.0);
-
- // ==== Giant ==== //
- let (_, e) = server.entities.insert(EntityKind::Giant, ());
- e.set_world(world_id);
- e.set_position([20.0, 101.0, -5.0]);
- e.set_yaw(270.0);
- e.set_head_yaw(90.0);
- }
-
- fn update(&self, server: &mut Server) {
- let world_id = server.state.world;
-
- // Rebuild our BVH every tick. All of the entities are in the same world.
- server.state.bvh.rebuild(
- server
- .entities
- .iter()
- .map(|(id, entity)| WithAabb::new(id, entity.hitbox())),
- );
-
- 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, entity)) => {
- entity.set_world(world_id);
- client.player = id
- }
- None => {
- client.disconnect("Conflicting UUID");
- return false;
- }
- }
-
- client.respawn(world_id);
- client.set_flat(true);
- client.set_game_mode(GameMode::Creative);
- client.teleport(
- [
- SPAWN_POS.x as f64 + 0.5,
- SPAWN_POS.y as f64 + 1.0,
- SPAWN_POS.z as f64 + 0.5,
- ],
- 0.0,
- 0.0,
- );
- client.set_player_list(server.state.player_list.clone());
-
- if let Some(id) = &server.state.player_list {
- server.player_lists[id].insert(
- client.uuid(),
- client.username(),
- client.textures().cloned(),
- client.game_mode(),
- 0,
- None,
- true,
- );
- }
-
- client.send_message(
- "Press ".italic()
- + "F3 + B".italic().color(Color::AQUA)
- + " to show hitboxes.".italic(),
- );
- }
-
- let player = &mut server.entities[client.player];
-
- while let Some(event) = client.next_event() {
- event.handle_default(client, player);
- }
-
- if client.is_disconnected() {
- self.player_count.fetch_sub(1, Ordering::SeqCst);
- if let Some(id) = &server.state.player_list {
- server.player_lists[id].remove(client.uuid());
- }
- player.set_deleted(true);
-
- return false;
- }
-
- let client_pos = client.position();
-
- let origin = Vec3::new(client_pos.x, client_pos.y + PLAYER_EYE_HEIGHT, client_pos.z);
- let direction = from_yaw_and_pitch(client.yaw() as f64, client.pitch() as f64);
- let not_self_or_bullet = |hit: RaycastHit>| {
- hit.object.object != client.player && hit.object.object != client.shulker_bullet
- };
-
- if let Some(hit) = server
- .state
- .bvh
- .raycast(origin, direction, not_self_or_bullet)
- {
- let bullet = if let Some(bullet) = server.entities.get_mut(client.shulker_bullet) {
- bullet
- } else {
- let (id, bullet) = server.entities.insert(EntityKind::ShulkerBullet, ());
- client.shulker_bullet = id;
- bullet.set_world(world_id);
- bullet
- };
-
- let mut hit_pos = origin + direction * hit.near;
- let hitbox = bullet.hitbox();
-
- hit_pos.y -= (hitbox.max.y - hitbox.min.y) / 2.0;
-
- bullet.set_position(hit_pos);
-
- client.set_action_bar("Intersection".color(Color::GREEN));
- } else {
- server.entities.delete(client.shulker_bullet);
- client.set_action_bar("No Intersection".color(Color::RED));
- }
-
- true
- });
- }
-}
diff --git a/crates/valence/examples/gamemode_switcher.rs b/crates/valence/examples/gamemode_switcher.rs
new file mode 100644
index 0000000..cca1aff
--- /dev/null
+++ b/crates/valence/examples/gamemode_switcher.rs
@@ -0,0 +1,82 @@
+use valence::client::despawn_disconnected_clients;
+use valence::client::event::{default_event_handler, ChatCommand};
+use valence::prelude::*;
+
+const SPAWN_Y: i32 = 64;
+
+pub fn main() {
+ tracing_subscriber::fmt().init();
+
+ App::new()
+ .add_plugin(ServerPlugin::new(()))
+ .add_system_to_stage(EventLoop, default_event_handler)
+ .add_system_to_stage(EventLoop, interpret_command)
+ .add_system_set(PlayerList::default_system_set())
+ .add_startup_system(setup)
+ .add_system(init_clients)
+ .add_system(despawn_disconnected_clients)
+ .run();
+}
+
+fn setup(world: &mut World) {
+ let mut instance = world
+ .resource::()
+ .new_instance(DimensionId::default());
+
+ for z in -5..5 {
+ for x in -5..5 {
+ instance.insert_chunk([x, z], Chunk::default());
+ }
+ }
+
+ for z in -25..25 {
+ for x in -25..25 {
+ instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
+ }
+ }
+
+ world.spawn(instance);
+}
+
+fn init_clients(
+ mut clients: Query<&mut Client, Added>,
+ instances: Query>,
+) {
+ for mut client in &mut clients {
+ client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
+ client.set_instance(instances.single());
+ client.set_game_mode(GameMode::Creative);
+ client.set_op_level(2); // required to use F3+F4, eg /gamemode
+ client.send_message("Welcome to Valence! Use F3+F4 to change gamemode.".italic());
+ }
+}
+
+fn interpret_command(mut clients: Query<&mut Client>, mut events: EventReader) {
+ for event in events.iter() {
+ let Ok(mut client) = clients.get_component_mut::(event.client) else {
+ continue;
+ };
+
+ let mut args = event.command.split_whitespace();
+
+ if args.next() == Some("gamemode") {
+ if client.op_level() < 2 {
+ // not enough permissions to use gamemode command
+ continue;
+ }
+
+ let mode = match args.next().unwrap_or_default() {
+ "adventure" => GameMode::Adventure,
+ "creative" => GameMode::Creative,
+ "survival" => GameMode::Survival,
+ "spectator" => GameMode::Spectator,
+ _ => {
+ client.send_message("Invalid gamemode.".italic());
+ continue;
+ }
+ };
+ client.set_game_mode(mode);
+ client.send_message(format!("Set gamemode to {mode:?}.").italic());
+ }
+ }
+}
diff --git a/crates/valence/examples/inventory_piano.rs b/crates/valence/examples/inventory_piano.rs
deleted file mode 100644
index f8b6865..0000000
--- a/crates/valence/examples/inventory_piano.rs
+++ /dev/null
@@ -1,236 +0,0 @@
-use std::net::SocketAddr;
-use std::sync::atomic::{AtomicUsize, Ordering};
-
-use num::Integer;
-pub use valence::prelude::*;
-use valence_protocol::types::ClickContainerMode;
-
-pub fn main() -> ShutdownResult {
- tracing_subscriber::fmt().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 SIZE_X: usize = 100;
-const SIZE_Z: usize = 100;
-
-const SLOT_MIN: i16 = 36;
-const SLOT_MAX: i16 = 43;
-const PITCH_MIN: f32 = 0.5;
-const PITCH_MAX: f32 = 1.0;
-
-#[async_trait]
-impl Config for Game {
- type ServerState = ServerState;
- type ClientState = ClientState;
- type EntityState = ();
- type WorldState = ();
- type ChunkState = ();
- type PlayerListState = ();
- type InventoryState = ();
-
- fn dimensions(&self) -> Vec {
- vec![Dimension {
- fixed_time: Some(6000),
- ..Dimension::default()
- }]
- }
-
- 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);
-
- // initialize chunks
- for chunk_z in -2..Integer::div_ceil(&(SIZE_Z as i32), &16) + 2 {
- for chunk_x in -2..Integer::div_ceil(&(SIZE_X as i32), &16) + 2 {
- world
- .chunks
- .insert([chunk_x, chunk_z], UnloadedChunk::default(), ());
- }
- }
-
- // initialize blocks in the chunks
- for chunk_x in 0..Integer::div_ceil(&SIZE_X, &16) {
- for chunk_z in 0..Integer::div_ceil(&SIZE_Z, &16) {
- let chunk = world
- .chunks
- .get_mut([chunk_x as i32, chunk_z as i32])
- .unwrap();
- for x in 0..16 {
- for z in 0..16 {
- let cell_x = chunk_x * 16 + x;
- let cell_z = chunk_z * 16 + z;
-
- if cell_x < SIZE_X && cell_z < SIZE_Z {
- chunk.set_block_state(x, 63, z, BlockState::GRASS_BLOCK);
- }
- }
- }
- }
- }
- }
-
- fn update(&self, server: &mut Server) {
- let (world_id, _) = server.worlds.iter_mut().next().unwrap();
-
- let spawn_pos = [SIZE_X as f64 / 2.0, 1.0, SIZE_Z as f64 / 2.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.respawn(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[id].insert(
- client.uuid(),
- client.username(),
- client.textures().cloned(),
- client.game_mode(),
- 0,
- None,
- true,
- );
- }
-
- client.set_game_mode(GameMode::Creative);
- client.send_message(
- "Welcome to Valence! Open your inventory, and click on your hotbar to play \
- the piano."
- .italic(),
- );
- client.send_message(
- "Click the rightmost hotbar slot to toggle between creative and survival."
- .italic(),
- );
- }
-
- let player = server.entities.get_mut(client.state.entity_id).unwrap();
-
- if client.is_disconnected() {
- self.player_count.fetch_sub(1, Ordering::SeqCst);
- player.set_deleted(true);
- if let Some(id) = &server.state.player_list {
- server.player_lists[id].remove(client.uuid());
- }
- return false;
- }
-
- if client.position().y <= -20.0 {
- client.teleport(spawn_pos, client.yaw(), client.pitch());
- }
-
- while let Some(event) = client.next_event() {
- match event {
- ClientEvent::CloseContainer { .. } => {
- client.send_message("Done already?");
- }
- ClientEvent::SetCreativeModeSlot { slot, .. } => {
- client.send_message(format!("{event:#?}"));
- // If the user does a double click, 3 notes will be played.
- // This is not possible to fix :(
- play_note(client, player, slot);
- }
- ClientEvent::ClickContainer { slot_id, mode, .. } => {
- client.send_message(format!("{event:#?}"));
- if mode != ClickContainerMode::Click {
- // Prevent notes from being played twice if the user clicks quickly
- continue;
- }
- play_note(client, player, slot_id);
- }
- _ => {}
- }
- }
-
- true
- });
- }
-}
-
-fn play_note(client: &mut Client, player: &mut Entity, clicked_slot: i16) {
- if (SLOT_MIN..=SLOT_MAX).contains(&clicked_slot) {
- let pitch = (clicked_slot - SLOT_MIN) as f32 * (PITCH_MAX - PITCH_MIN)
- / (SLOT_MAX - SLOT_MIN) as f32
- + PITCH_MIN;
-
- client.send_message(format!("playing note with pitch: {pitch}"));
-
- let _ = player;
- // client.play_sound(
- // Ident::new("block.note_block.harp").unwrap(),
- // SoundCategory::Block,
- // player.position(),
- // 10.0,
- // pitch,
- // );
- } else if clicked_slot == 44 {
- client.set_game_mode(match client.game_mode() {
- GameMode::Survival => GameMode::Creative,
- GameMode::Creative => GameMode::Survival,
- _ => GameMode::Creative,
- });
- }
-}
diff --git a/crates/valence/examples/parkour.rs b/crates/valence/examples/parkour.rs
index fc58c7b..cc0cc50 100644
--- a/crates/valence/examples/parkour.rs
+++ b/crates/valence/examples/parkour.rs
@@ -1,45 +1,15 @@
use std::collections::VecDeque;
-use std::net::SocketAddr;
-use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use rand::seq::SliceRandom;
use rand::Rng;
+use valence::client::despawn_disconnected_clients;
+use valence::client::event::default_event_handler;
use valence::prelude::*;
+use valence_protocol::packets::s2c::play::SetTitleAnimationTimes;
-pub fn main() -> ShutdownResult {
- tracing_subscriber::fmt().init();
-
- valence::start_server(
- Game {
- player_count: AtomicUsize::new(0),
- },
- None,
- )
-}
-
-struct Game {
- player_count: AtomicUsize,
-}
-
-#[derive(Default)]
-struct ChunkState {
- keep_loaded: bool,
-}
-
-#[derive(Default)]
-struct ClientState {
- entity_id: EntityId,
- blocks: VecDeque,
- score: u32,
- combo: u32,
- target_y: i32,
- last_block_timestamp: u128,
- world_id: WorldId,
-}
-
-const MAX_PLAYERS: usize = 10;
const START_POS: BlockPos = BlockPos::new(0, 100, 0);
+const VIEW_DIST: u8 = 10;
const BLOCK_TYPES: [BlockState; 7] = [
BlockState::GRASS_BLOCK,
@@ -51,273 +21,217 @@ const BLOCK_TYPES: [BlockState; 7] = [
BlockState::MOSS_BLOCK,
];
-#[async_trait]
-impl Config for Game {
- type ServerState = Option;
- type ClientState = ClientState;
- type EntityState = ();
- type WorldState = ();
- type ChunkState = ChunkState;
- type PlayerListState = ();
- type InventoryState = ();
+pub fn main() {
+ tracing_subscriber::fmt().init();
- 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) {
- server.state = Some(server.player_lists.insert(()).0);
- }
-
- fn update(&self, server: &mut Server) {
- 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;
- }
-
- let (world_id, world) = server.worlds.insert(DimensionId::default(), ());
-
- match server
- .entities
- .insert_with_uuid(EntityKind::Player, client.uuid(), ())
- {
- Some((id, entity)) => {
- entity.set_world(world_id);
-
- // create client state
- client.state = ClientState {
- entity_id: id,
- blocks: VecDeque::new(),
- score: 0,
- combo: 0,
- last_block_timestamp: 0,
- target_y: 0,
- world_id,
- };
- }
- None => {
- client.disconnect("Conflicting UUID");
- server.worlds.remove(world_id);
- return false;
- }
- }
-
- if let Some(id) = &server.state {
- server.player_lists[id].insert(
- client.uuid(),
- client.username(),
- client.textures().cloned(),
- client.game_mode(),
- 0,
- None,
- true,
- );
- }
-
- client.respawn(world_id);
- client.set_flat(true);
- client.set_player_list(server.state.clone());
-
- client.send_message("Welcome to epic infinite parkour game!".italic());
- client.set_game_mode(GameMode::Adventure);
- reset(client, world);
- }
-
- let world_id = client.world_id;
- let world = &mut server.worlds[world_id];
-
- let p = client.position();
- for pos in ChunkPos::at(p.x, p.z).in_view(3) {
- if let Some(chunk) = world.chunks.get_mut(pos) {
- chunk.keep_loaded = true;
- } else {
- world.chunks.insert(
- pos,
- UnloadedChunk::default(),
- ChunkState { keep_loaded: true },
- );
- }
- }
-
- if (client.position().y as i32) < START_POS.y - 32 {
- client.send_message(
- "Your score was ".italic()
- + client
- .score
- .to_string()
- .color(Color::GOLD)
- .bold()
- .not_italic(),
- );
-
- reset(client, world);
- }
-
- let pos_under_player = BlockPos::new(
- (client.position().x - 0.5).round() as i32,
- client.position().y as i32 - 1,
- (client.position().z - 0.5).round() as i32,
- );
-
- if let Some(index) = client
- .blocks
- .iter()
- .position(|block| *block == pos_under_player)
- {
- if index > 0 {
- let power_result = 2.0f32.powf((client.combo as f32) / 45.0);
- let max_time_taken = (1000.0f32 * (index as f32) / power_result) as u128;
-
- let current_time_millis = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_millis();
- if current_time_millis - client.last_block_timestamp < max_time_taken {
- client.combo += index as u32
- } else {
- client.combo = 0
- }
-
- // let pitch = 0.9 + ((client.combo as f32) - 1.0) * 0.05;
-
- for _ in 0..index {
- generate_next_block(client, world, true)
- }
-
- // TODO: add sounds again.
- // client.play_sound(
- // Ident::new("minecraft:block.note_block.bass").unwrap(),
- // SoundCategory::Master,
- // client.position(),
- // 1f32,
- // pitch,
- // );
-
- client.set_title(
- "",
- client.score.to_string().color(Color::LIGHT_PURPLE).bold(),
- SetTitleAnimationTimes {
- fade_in: 0,
- stay: 7,
- fade_out: 4,
- },
- );
- }
- }
-
- let player = server.entities.get_mut(client.entity_id).unwrap();
-
- while let Some(event) = client.next_event() {
- event.handle_default(client, player);
- }
-
- // Remove chunks outside the view distance of players.
- for (_, chunk) in world.chunks.iter_mut() {
- chunk.set_deleted(!chunk.keep_loaded);
- chunk.keep_loaded = false;
- }
-
- if client.is_disconnected() {
- self.player_count.fetch_sub(1, Ordering::SeqCst);
- if let Some(id) = &server.state {
- server.player_lists[id].remove(client.uuid());
- }
- player.set_deleted(true);
- server.worlds.remove(world_id);
- return false;
- }
-
- true
- });
- }
+ App::new()
+ .add_plugin(ServerPlugin::new(()))
+ .add_system_to_stage(EventLoop, default_event_handler)
+ .add_system_set(PlayerList::default_system_set())
+ .add_system(init_clients)
+ .add_system(despawn_disconnected_clients)
+ .add_system(reset_clients.after(init_clients))
+ .add_system(manage_chunks.after(reset_clients).before(manage_blocks))
+ .add_system(manage_blocks)
+ .run();
}
-fn reset(client: &mut Client, world: &mut World) {
- // Load chunks around spawn to avoid double void reset
- for chunk_z in -1..3 {
- for chunk_x in -2..2 {
- world.chunks.insert(
- [chunk_x, chunk_z],
- UnloadedChunk::default(),
- ChunkState { keep_loaded: true },
- );
+#[derive(Component)]
+struct GameState {
+ blocks: VecDeque,
+ score: u32,
+ combo: u32,
+ target_y: i32,
+ last_block_timestamp: u128,
+}
+
+fn init_clients(
+ mut commands: Commands,
+ server: Res,
+ mut clients: Query<(Entity, &mut Client), Added>,
+) {
+ for (ent, mut client) in clients.iter_mut() {
+ let mut instance = server.new_instance(DimensionId::default());
+
+ for pos in client.view().with_dist(VIEW_DIST).iter() {
+ assert!(instance.insert_chunk(pos, Chunk::default()).is_none());
}
- }
- client.score = 0;
- client.combo = 0;
-
- for block in &client.blocks {
- world.chunks.set_block_state(*block, BlockState::AIR);
- }
- client.blocks.clear();
- client.blocks.push_back(START_POS);
- world.chunks.set_block_state(START_POS, BlockState::STONE);
-
- for _ in 0..10 {
- generate_next_block(client, world, false)
- }
-
- client.teleport(
- [
+ client.set_position([
START_POS.x as f64 + 0.5,
START_POS.y as f64 + 1.0,
START_POS.z as f64 + 0.5,
- ],
- 0f32,
- 0f32,
- );
+ ]);
+ client.set_flat(true);
+ client.set_instance(ent);
+ client.set_game_mode(GameMode::Adventure);
+ client.send_message("Welcome to epic infinite parkour game!".italic());
+
+ let mut state = GameState {
+ blocks: VecDeque::new(),
+ score: 0,
+ combo: 0,
+ target_y: 0,
+ last_block_timestamp: 0,
+ };
+
+ reset(&mut client, &mut state, &mut instance);
+
+ commands.entity(ent).insert(state);
+ commands.entity(ent).insert(instance);
+ }
}
-fn generate_next_block(client: &mut Client, world: &mut World, in_game: bool) {
- if in_game {
- let removed_block = client.blocks.pop_front().unwrap();
- world.chunks.set_block_state(removed_block, BlockState::AIR);
+fn reset_clients(
+ mut clients: Query<(&mut Client, &mut GameState, &mut Instance), With>,
+) {
+ for (mut client, mut state, mut instance) in clients.iter_mut() {
+ if (client.position().y as i32) < START_POS.y - 32 {
+ client.send_message(
+ "Your score was ".italic()
+ + state
+ .score
+ .to_string()
+ .color(Color::GOLD)
+ .bold()
+ .not_italic(),
+ );
- client.score += 1
+ reset(&mut client, &mut state, &mut instance);
+ }
+ }
+}
+
+fn manage_blocks(mut clients: Query<(&mut Client, &mut GameState, &mut Instance)>) {
+ for (mut client, mut state, mut instance) in clients.iter_mut() {
+ let pos_under_player = BlockPos::new(
+ (client.position().x - 0.5).round() as i32,
+ client.position().y as i32 - 1,
+ (client.position().z - 0.5).round() as i32,
+ );
+
+ if let Some(index) = state
+ .blocks
+ .iter()
+ .position(|block| *block == pos_under_player)
+ {
+ if index > 0 {
+ let power_result = 2.0f32.powf((state.combo as f32) / 45.0);
+ let max_time_taken = (1000.0f32 * (index as f32) / power_result) as u128;
+
+ let current_time_millis = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_millis();
+
+ if current_time_millis - state.last_block_timestamp < max_time_taken {
+ state.combo += index as u32
+ } else {
+ state.combo = 0
+ }
+
+ // let pitch = 0.9 + ((state.combo as f32) - 1.0) * 0.05;
+
+ for _ in 0..index {
+ generate_next_block(&mut state, &mut instance, true)
+ }
+
+ // TODO: add sounds again.
+ // client.play_sound(
+ // Ident::new("minecraft:block.note_block.bass").unwrap(),
+ // SoundCategory::Master,
+ // client.position(),
+ // 1f32,
+ // pitch,
+ // );
+
+ client.set_title(
+ "",
+ state.score.to_string().color(Color::LIGHT_PURPLE).bold(),
+ SetTitleAnimationTimes {
+ fade_in: 0,
+ stay: 7,
+ fade_out: 4,
+ },
+ );
+ }
+ }
+ }
+}
+
+fn manage_chunks(mut clients: Query<(&mut Client, &mut Instance)>) {
+ for (client, mut instance) in &mut clients {
+ let old_view = client.old_view().with_dist(VIEW_DIST);
+ let view = client.view().with_dist(VIEW_DIST);
+
+ if old_view != view {
+ for pos in old_view.diff(view) {
+ instance.chunk_entry(pos).or_default();
+ }
+
+ for pos in view.diff(old_view) {
+ instance.chunk_entry(pos).or_default();
+ }
+ }
+ }
+}
+
+fn reset(client: &mut Client, state: &mut GameState, instance: &mut Instance) {
+ // Load chunks around spawn to avoid double void reset
+ for z in -1..3 {
+ for x in -2..2 {
+ instance.insert_chunk([x, z], Chunk::default());
+ }
}
- let last_pos = *client.blocks.back().unwrap();
- let block_pos = generate_random_block(last_pos, client.target_y);
+ state.score = 0;
+ state.combo = 0;
+
+ for block in &state.blocks {
+ instance.set_block_state(*block, BlockState::AIR);
+ }
+ state.blocks.clear();
+ state.blocks.push_back(START_POS);
+ instance.set_block_state(START_POS, BlockState::STONE);
+
+ for _ in 0..10 {
+ generate_next_block(state, instance, false);
+ }
+
+ client.set_position([
+ START_POS.x as f64 + 0.5,
+ START_POS.y as f64 + 1.0,
+ START_POS.z as f64 + 0.5,
+ ]);
+ client.set_velocity([0f32, 0f32, 0f32]);
+ client.set_yaw(0f32);
+ client.set_pitch(0f32)
+}
+
+fn generate_next_block(state: &mut GameState, instance: &mut Instance, in_game: bool) {
+ if in_game {
+ let removed_block = state.blocks.pop_front().unwrap();
+ instance.set_block_state(removed_block, BlockState::AIR);
+
+ state.score += 1
+ }
+
+ let last_pos = *state.blocks.back().unwrap();
+ let block_pos = generate_random_block(last_pos, state.target_y);
if last_pos.y == START_POS.y {
- client.target_y = 0
+ state.target_y = 0
} else if last_pos.y < START_POS.y - 30 || last_pos.y > START_POS.y + 30 {
- client.target_y = START_POS.y;
+ state.target_y = START_POS.y;
}
let mut rng = rand::thread_rng();
- world
- .chunks
- .set_block_state(block_pos, *BLOCK_TYPES.choose(&mut rng).unwrap());
- client.blocks.push_back(block_pos);
+ instance.set_block_state(block_pos, *BLOCK_TYPES.choose(&mut rng).unwrap());
+ state.blocks.push_back(block_pos);
// Combo System
- client.last_block_timestamp = SystemTime::now()
+ state.last_block_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
@@ -333,9 +247,9 @@ fn generate_random_block(pos: BlockPos, target_y: i32) -> BlockPos {
_ => -1,
};
let z = match y {
- 1 => rng.gen_range(1..4),
- -1 => rng.gen_range(2..6),
- _ => rng.gen_range(1..5),
+ 1 => rng.gen_range(1..3),
+ -1 => rng.gen_range(2..5),
+ _ => rng.gen_range(1..4),
};
let x = rng.gen_range(-3..4);
diff --git a/crates/valence/examples/particles.rs b/crates/valence/examples/particles.rs
index 1100841..6245349 100644
--- a/crates/valence/examples/particles.rs
+++ b/crates/valence/examples/particles.rs
@@ -1,199 +1,99 @@
-use std::fmt;
-use std::net::SocketAddr;
-use std::sync::atomic::{AtomicUsize, Ordering};
-
+use valence::client::despawn_disconnected_clients;
+use valence::client::event::default_event_handler;
use valence::prelude::*;
+use valence_protocol::packets::s2c::particle::Particle;
-pub fn main() -> ShutdownResult {
+const SPAWN_Y: i32 = 64;
+
+pub fn main() {
tracing_subscriber::fmt().init();
- valence::start_server(
- Game {
- player_count: AtomicUsize::new(0),
- },
- ServerState {
- player_list: None,
- particle_list: create_particle_vec(),
- particle_idx: 0,
- },
- )
+ App::new()
+ .add_plugin(ServerPlugin::new(()))
+ .add_system_to_stage(EventLoop, default_event_handler)
+ .add_system_set(PlayerList::default_system_set())
+ .add_startup_system(setup)
+ .add_system(init_clients)
+ .add_system(despawn_disconnected_clients)
+ .add_system(manage_particles)
+ .run();
}
-struct Game {
- player_count: AtomicUsize,
+#[derive(Resource)]
+struct ParticleSpawner {
+ particles: Vec,
+ index: usize,
}
-struct ServerState {
- player_list: Option,
- particle_list: Vec,
- particle_idx: usize,
-}
-
-const MAX_PLAYERS: usize = 10;
-
-const SPAWN_POS: BlockPos = BlockPos::new(0, 100, 0);
-
-#[async_trait]
-impl Config for Game {
- type ServerState = ServerState;
- type ClientState = EntityId;
- type EntityState = ();
- type WorldState = ();
- type ChunkState = ();
- type PlayerListState = ();
- type InventoryState = ();
-
- 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(),
- ),
+impl ParticleSpawner {
+ pub fn new() -> Self {
+ Self {
+ particles: create_particle_vec(),
+ index: 0,
}
}
- fn init(&self, server: &mut Server) {
- let (_, world) = server.worlds.insert(DimensionId::default(), ());
- server.state.player_list = Some(server.player_lists.insert(()).0);
-
- let size = 5;
- for z in -size..size {
- for x in -size..size {
- world.chunks.insert([x, z], UnloadedChunk::default(), ());
- }
- }
-
- world.chunks.set_block_state(SPAWN_POS, BlockState::BEDROCK);
- }
-
- fn update(&self, server: &mut Server) {
- let (world_id, _) = server.worlds.iter_mut().next().expect("missing world");
-
- 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 = id,
- None => {
- client.disconnect("Conflicting UUID");
- return false;
- }
- }
-
- client.respawn(world_id);
- client.set_flat(true);
- client.set_game_mode(GameMode::Creative);
- client.teleport(
- [
- SPAWN_POS.x as f64 + 0.5,
- SPAWN_POS.y as f64 + 1.0,
- SPAWN_POS.z as f64 + 0.5,
- ],
- 0.0,
- 0.0,
- );
- client.set_player_list(server.state.player_list.clone());
- client.send_message("Sneak to speed up the cycling of particles");
-
- 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,
- true,
- );
- }
- }
-
- if client.is_disconnected() {
- self.player_count.fetch_sub(1, Ordering::SeqCst);
- if let Some(id) = &server.state.player_list {
- server.player_lists[id].remove(client.uuid());
- }
- server.entities[client.state].set_deleted(true);
-
- return false;
- }
-
- let entity = server
- .entities
- .get_mut(client.state)
- .expect("missing player entity");
-
- while let Some(event) = client.next_event() {
- event.handle_default(client, entity);
- }
-
- true
- });
-
- let players_are_sneaking = server.clients.iter().any(|(_, client)| -> bool {
- let player = &server.entities[client.state];
- if let TrackedData::Player(data) = player.data() {
- return data.get_pose() == Pose::Sneaking;
- }
- false
- });
-
- let cycle_time = if players_are_sneaking { 5 } else { 30 };
-
- if !server.clients.is_empty() && server.current_tick() % cycle_time == 0 {
- if server.state.particle_idx == server.state.particle_list.len() {
- server.state.particle_idx = 0;
- }
-
- let pos = [
- SPAWN_POS.x as f64 + 0.5,
- SPAWN_POS.y as f64 + 2.0,
- SPAWN_POS.z as f64 + 5.5,
- ];
- let offset = [0.5, 0.5, 0.5];
- let particle = &server.state.particle_list[server.state.particle_idx];
-
- server.clients.iter_mut().for_each(|(_, client)| {
- client.set_title(
- "",
- dbg_name(particle).bold(),
- SetTitleAnimationTimes {
- fade_in: 0,
- stay: 100,
- fade_out: 2,
- },
- );
- client.play_particle(particle, true, pos, offset, 0.1, 100);
- });
- server.state.particle_idx += 1;
- }
+ pub fn next(&mut self) {
+ self.index = (self.index + 1) % self.particles.len();
}
}
-fn dbg_name(dbg: &impl fmt::Debug) -> String {
+fn setup(world: &mut World) {
+ let mut instance = world
+ .resource::()
+ .new_instance(DimensionId::default());
+
+ for z in -5..5 {
+ for x in -5..5 {
+ instance.insert_chunk([x, z], Chunk::default());
+ }
+ }
+
+ instance.set_block_state([0, SPAWN_Y, 0], BlockState::BEDROCK);
+
+ world.spawn(instance);
+
+ let spawner = ParticleSpawner::new();
+ world.insert_resource(spawner)
+}
+
+fn init_clients(
+ mut clients: Query<&mut Client, Added>,
+ instances: Query>,
+) {
+ for mut client in &mut clients {
+ client.set_position([0.5, SPAWN_Y as f64 + 1.0, 0.5]);
+ client.set_instance(instances.single());
+ client.set_game_mode(GameMode::Creative);
+ }
+}
+
+fn manage_particles(
+ mut spawner: ResMut,
+ server: Res,
+ mut instances: Query<&mut Instance>,
+) {
+ if server.current_tick() % 20 == 0 {
+ spawner.next();
+ }
+
+ if server.current_tick() % 5 != 0 {
+ return;
+ }
+
+ let particle = &spawner.particles[spawner.index];
+ let name = dbg_name(particle);
+
+ let pos = [0.5, SPAWN_Y as f64 + 2.0, 5.0];
+ let offset = [0.5, 0.5, 0.5];
+
+ let mut instance = instances.single_mut();
+
+ instance.play_particle(particle, true, pos, offset, 0.1, 100);
+ instance.set_action_bar(name.bold());
+}
+
+fn dbg_name(dbg: &impl std::fmt::Debug) -> String {
let string = format!("{dbg:?}");
string
@@ -253,7 +153,7 @@ fn create_particle_vec() -> Vec {
Particle::Item(None),
Particle::Item(Some(ItemStack::new(ItemKind::IronPickaxe, 1, None))),
Particle::VibrationBlock {
- block_pos: SPAWN_POS,
+ block_pos: [0, SPAWN_Y, 0].into(),
ticks: 50,
},
Particle::VibrationEntity {
diff --git a/crates/valence/examples/player_list.rs b/crates/valence/examples/player_list.rs
new file mode 100644
index 0000000..9dccb5f
--- /dev/null
+++ b/crates/valence/examples/player_list.rs
@@ -0,0 +1,120 @@
+use rand::Rng;
+use valence::client::despawn_disconnected_clients;
+use valence::client::event::default_event_handler;
+use valence::player_list::Entry;
+use valence::prelude::*;
+
+const SPAWN_Y: i32 = 64;
+const PLAYER_UUID_1: Uuid = Uuid::from_u128(1);
+const PLAYER_UUID_2: Uuid = Uuid::from_u128(2);
+
+fn main() {
+ tracing_subscriber::fmt().init();
+
+ App::new()
+ .add_plugin(ServerPlugin::new(()))
+ .add_startup_system(setup)
+ .add_system_to_stage(EventLoop, default_event_handler)
+ .add_system(init_clients)
+ .add_system(update_player_list)
+ .add_system(despawn_disconnected_clients)
+ .add_system(remove_disconnected_clients_from_player_list)
+ .run();
+}
+
+fn setup(world: &mut World) {
+ let mut instance = world
+ .resource::()
+ .new_instance(DimensionId::default());
+
+ for z in -5..5 {
+ for x in -5..5 {
+ instance.insert_chunk([x, z], Chunk::default());
+ }
+ }
+
+ for z in -25..25 {
+ for x in -25..25 {
+ instance.set_block_state([x, SPAWN_Y, z], BlockState::LIGHT_GRAY_WOOL);
+ }
+ }
+
+ world.spawn(instance);
+
+ let mut player_list = world.resource_mut::();
+
+ player_list.insert(
+ PLAYER_UUID_1,
+ PlayerListEntry::new().with_display_name(Some("persistent entry with no ping")),
+ );
+}
+
+fn init_clients(
+ mut clients: Query<&mut Client, Added>,
+ instances: Query>,
+ mut player_list: ResMut,
+) {
+ for mut client in &mut clients {
+ client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
+ client.set_instance(instances.single());
+ client.set_game_mode(GameMode::Creative);
+
+ client.send_message(
+ "Please open your player list (tab key)."
+ .italic()
+ .color(Color::WHITE),
+ );
+
+ let entry = PlayerListEntry::new()
+ .with_username(client.username())
+ .with_properties(client.properties()) // For the player's skin and cape.
+ .with_game_mode(client.game_mode())
+ .with_ping(0) // Use negative values to indicate missing.
+ .with_display_name(Some("ඞ".color(Color::new(255, 87, 66))));
+
+ player_list.insert(client.uuid(), entry);
+ }
+}
+
+fn update_player_list(mut player_list: ResMut, server: Res) {
+ let tick = server.current_tick();
+
+ player_list.set_header("Current tick: ".into_text() + tick);
+ player_list
+ .set_footer("Current tick but in purple: ".into_text() + tick.color(Color::LIGHT_PURPLE));
+
+ if tick % 5 == 0 {
+ let mut rng = rand::thread_rng();
+ let color = Color::new(rng.gen(), rng.gen(), rng.gen());
+
+ let entry = player_list.get_mut(PLAYER_UUID_1).unwrap();
+ let new_display_name = entry.display_name().unwrap().clone().color(color);
+ entry.set_display_name(Some(new_display_name));
+ }
+
+ if tick % 20 == 0 {
+ match player_list.entry(PLAYER_UUID_2) {
+ Entry::Occupied(oe) => {
+ oe.remove();
+ }
+ Entry::Vacant(ve) => {
+ let entry = PlayerListEntry::new()
+ .with_display_name(Some("Hello!"))
+ .with_ping(300);
+
+ ve.insert(entry);
+ }
+ }
+ }
+}
+
+fn remove_disconnected_clients_from_player_list(
+ clients: Query<&mut Client>,
+ mut player_list: ResMut,
+) {
+ for client in &clients {
+ if client.is_disconnected() {
+ player_list.remove(client.uuid());
+ }
+ }
+}
diff --git a/crates/valence/examples/resource_pack.rs b/crates/valence/examples/resource_pack.rs
index 1c3f51c..f2c3579 100644
--- a/crates/valence/examples/resource_pack.rs
+++ b/crates/valence/examples/resource_pack.rs
@@ -1,207 +1,101 @@
-use std::net::SocketAddr;
-use std::sync::atomic::{AtomicUsize, Ordering};
-
+use valence::client::despawn_disconnected_clients;
+use valence::client::event::{
+ default_event_handler, InteractWithEntity, ResourcePackStatus, ResourcePackStatusChange,
+};
use valence::prelude::*;
use valence_protocol::types::EntityInteraction;
-pub fn main() -> ShutdownResult {
+const SPAWN_Y: i32 = 64;
+
+pub fn main() {
tracing_subscriber::fmt().init();
- valence::start_server(
- Game {
- player_count: AtomicUsize::new(0),
- },
- ServerState {
- player_list: None,
- sheep_id: EntityId::NULL,
- },
- )
+ App::new()
+ .add_plugin(ServerPlugin::new(()))
+ .add_system_to_stage(EventLoop, default_event_handler)
+ .add_system_to_stage(EventLoop, prompt_on_punch)
+ .add_system_to_stage(EventLoop, on_resource_pack_status)
+ .add_system_set(PlayerList::default_system_set())
+ .add_startup_system(setup)
+ .add_system(init_clients)
+ .add_system(despawn_disconnected_clients)
+ .run();
}
-struct Game {
- player_count: AtomicUsize,
-}
+fn setup(world: &mut World) {
+ let mut instance = world
+ .resource::()
+ .new_instance(DimensionId::default());
-struct ServerState {
- player_list: Option,
- sheep_id: EntityId,
-}
-
-#[derive(Default)]
-struct ClientState {
- entity_id: EntityId,
-}
-
-const MAX_PLAYERS: usize = 10;
-
-const SPAWN_POS: BlockPos = BlockPos::new(0, 100, 0);
-
-#[async_trait]
-impl Config for Game {
- type ServerState = ServerState;
- type ClientState = ClientState;
- type EntityState = ();
- type WorldState = ();
- type ChunkState = ();
- type PlayerListState = ();
- type InventoryState = ();
-
- 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(),
- ),
+ for z in -5..5 {
+ for x in -5..5 {
+ instance.insert_chunk([x, z], Chunk::default());
}
}
- fn init(&self, server: &mut Server) {
- let (world_id, world) = server.worlds.insert(DimensionId::default(), ());
- server.state.player_list = Some(server.player_lists.insert(()).0);
-
- let size = 5;
- for z in -size..size {
- for x in -size..size {
- world.chunks.insert([x, z], UnloadedChunk::default(), ());
- }
+ for z in -25..25 {
+ for x in -25..25 {
+ instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
}
-
- let (sheep_id, sheep) = server.entities.insert(EntityKind::Sheep, ());
- server.state.sheep_id = sheep_id;
- sheep.set_world(world_id);
- sheep.set_position([
- SPAWN_POS.x as f64 + 0.5,
- SPAWN_POS.y as f64 + 4.0,
- SPAWN_POS.z as f64 + 0.5,
- ]);
-
- if let TrackedData::Sheep(sheep_data) = sheep.data_mut() {
- sheep_data.set_custom_name("Hit me".color(Color::GREEN));
- }
-
- world.chunks.set_block_state(SPAWN_POS, BlockState::BEDROCK);
}
- fn update(&self, server: &mut Server) {
- let (world_id, _) = server.worlds.iter_mut().next().expect("missing world");
+ let instance_ent = world.spawn(instance).id();
- 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;
- }
+ let mut sheep = McEntity::new(EntityKind::Sheep, instance_ent);
+ sheep.set_position([0.0, SPAWN_Y as f64 + 1.0, 2.0]);
+ world.spawn(sheep);
+}
- match server
- .entities
- .insert_with_uuid(EntityKind::Player, client.uuid(), ())
- {
- Some((id, _)) => client.entity_id = id,
- None => {
- client.disconnect("Conflicting UUID");
- return false;
- }
- }
+fn init_clients(
+ mut clients: Query<&mut Client, Added>,
+ instances: Query>,
+) {
+ for mut client in &mut clients {
+ client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
+ client.set_instance(instances.single());
+ client.set_game_mode(GameMode::Creative);
- client.respawn(world_id);
- client.set_flat(true);
- client.set_game_mode(GameMode::Creative);
- client.teleport(
- [
- SPAWN_POS.x as f64 + 0.5,
- SPAWN_POS.y as f64 + 1.0,
- SPAWN_POS.z as f64 + 0.5,
- ],
- 0.0,
- 0.0,
- );
- client.set_player_list(server.state.player_list.clone());
-
- if let Some(id) = &server.state.player_list {
- server.player_lists[id].insert(
- client.uuid(),
- client.username(),
- client.textures().cloned(),
- client.game_mode(),
- 0,
- None,
- true,
- );
- }
-
- client.send_message(
- "Hit the sheep above you to prompt for the resource pack again.".italic(),
- );
-
- set_example_pack(client);
- }
-
- let player = &mut server.entities[client.entity_id];
-
- if client.is_disconnected() {
- self.player_count.fetch_sub(1, Ordering::SeqCst);
- if let Some(id) = &server.state.player_list {
- server.player_lists[id].remove(client.uuid());
- }
- player.set_deleted(true);
-
- return false;
- }
-
- while let Some(event) = client.next_event() {
- event.handle_default(client, player);
- match event {
- ClientEvent::InteractWithEntity {
- entity_id,
- interact,
- ..
- } => {
- if interact == EntityInteraction::Attack
- && entity_id == server.state.sheep_id.to_raw()
- {
- set_example_pack(client);
- }
- }
- ClientEvent::ResourcePackLoaded => {
- client.send_message("Resource pack loaded!".color(Color::GREEN));
- }
- ClientEvent::ResourcePackDeclined => {
- client.send_message("Resource pack declined.".color(Color::RED));
- }
- ClientEvent::ResourcePackFailedDownload => {
- client.send_message("Resource pack download failed.".color(Color::RED));
- }
- _ => {}
- }
- }
-
- true
- });
+ client.send_message("Hit the sheep to prompt for the resource pack.".italic());
}
}
-/// Sends the resource pack prompt.
-fn set_example_pack(client: &mut Client) {
- client.set_resource_pack(
- "https://github.com/valence-rs/valence/raw/main/assets/example_pack.zip",
- "d7c6108849fb190ec2a49f2d38b7f1f897d9ce9f",
- false,
- None,
- );
+fn prompt_on_punch(mut clients: Query<&mut Client>, mut events: EventReader) {
+ for event in events.iter() {
+ let Ok(mut client) = clients.get_mut(event.client) else {
+ continue;
+ };
+ if event.interact == EntityInteraction::Attack {
+ client.set_resource_pack(
+ "https://github.com/valence-rs/valence/raw/main/assets/example_pack.zip",
+ "d7c6108849fb190ec2a49f2d38b7f1f897d9ce9f",
+ false,
+ None,
+ );
+ }
+ }
+}
+
+fn on_resource_pack_status(
+ mut clients: Query<&mut Client>,
+ mut events: EventReader,
+) {
+ for event in events.iter() {
+ let Ok(mut client) = clients.get_mut(event.client) else {
+ continue;
+ };
+ match event.status {
+ ResourcePackStatus::Accepted => {
+ client.send_message("Resource pack accepted.".color(Color::GREEN));
+ }
+ ResourcePackStatus::Declined => {
+ client.send_message("Resource pack declined.".color(Color::RED));
+ }
+ ResourcePackStatus::FailedDownload => {
+ client.send_message("Resource pack failed to download.".color(Color::RED));
+ }
+ ResourcePackStatus::Loaded => {
+ client.send_message("Resource pack successfully downloaded.".color(Color::BLUE));
+ }
+ }
+ }
}
diff --git a/crates/valence/examples/server_list_ping.rs b/crates/valence/examples/server_list_ping.rs
new file mode 100644
index 0000000..36b42d6
--- /dev/null
+++ b/crates/valence/examples/server_list_ping.rs
@@ -0,0 +1,37 @@
+use std::net::SocketAddr;
+
+use valence::prelude::*;
+
+pub fn main() {
+ App::new()
+ .add_plugin(ServerPlugin::new(MyCallbacks).with_connection_mode(ConnectionMode::Offline))
+ .run();
+}
+
+struct MyCallbacks;
+
+#[async_trait]
+impl AsyncCallbacks for MyCallbacks {
+ async fn server_list_ping(
+ &self,
+ _shared: &SharedServer,
+ remote_addr: SocketAddr,
+ _protocol_version: i32,
+ ) -> ServerListPing {
+ ServerListPing::Respond {
+ online_players: 42,
+ max_players: 420,
+ player_sample: vec![PlayerSampleEntry {
+ name: "foobar".into(),
+ id: Uuid::from_u128(12345),
+ }],
+ description: "Your IP address is ".into_text()
+ + remote_addr.to_string().color(Color::GOLD),
+ favicon_png: include_bytes!("../../../assets/logo-64x64.png"),
+ }
+ }
+
+ async fn login(&self, _shared: &SharedServer, _info: &NewClientInfo) -> Result<(), Text> {
+ Err("You are not meant to join this example".color(Color::RED))
+ }
+}
diff --git a/crates/valence/examples/terrain.rs b/crates/valence/examples/terrain.rs
index 346688b..564969e 100644
--- a/crates/valence/examples/terrain.rs
+++ b/crates/valence/examples/terrain.rs
@@ -1,313 +1,317 @@
-use std::net::SocketAddr;
-use std::sync::atomic::{AtomicUsize, Ordering};
+use std::collections::hash_map::Entry;
+use std::collections::HashMap;
+use std::sync::Arc;
+use std::thread;
use std::time::SystemTime;
+use flume::{Receiver, Sender};
use noise::{NoiseFn, SuperSimplex};
-use rayon::iter::ParallelIterator;
-pub use valence::prelude::*;
-use vek::Lerp;
+use tracing::info;
+use valence::client::despawn_disconnected_clients;
+use valence::client::event::default_event_handler;
+use valence::prelude::*;
-pub fn main() -> ShutdownResult {
+const SPAWN_POS: DVec3 = DVec3::new(0.0, 200.0, 0.0);
+const SECTION_COUNT: usize = 24;
+
+struct ChunkWorkerState {
+ sender: Sender<(ChunkPos, Chunk)>,
+ receiver: Receiver,
+ // Noise functions
+ density: SuperSimplex,
+ hilly: SuperSimplex,
+ stone: SuperSimplex,
+ gravel: SuperSimplex,
+ grass: SuperSimplex,
+}
+
+#[derive(Resource)]
+struct GameState {
+ /// Chunks that need to be generated. Chunks without a priority have already
+ /// been sent to the thread pool.
+ pending: HashMap>,
+ sender: Sender,
+ receiver: Receiver<(ChunkPos, Chunk)>,
+}
+
+/// The order in which chunks should be processed by the thread pool. Smaller
+/// values are sent first.
+type Priority = u64;
+
+pub fn main() {
tracing_subscriber::fmt().init();
+ App::new()
+ .add_plugin(ServerPlugin::new(()))
+ .add_system_to_stage(EventLoop, default_event_handler)
+ .add_system_set(PlayerList::default_system_set())
+ .add_startup_system(setup)
+ .add_system(init_clients)
+ .add_system(remove_unviewed_chunks.after(init_clients))
+ .add_system(update_client_views.after(remove_unviewed_chunks))
+ .add_system(send_recv_chunks.after(update_client_views))
+ .add_system(despawn_disconnected_clients)
+ .run();
+}
+
+fn setup(world: &mut World) {
let seconds_per_day = 86_400;
let seed = (SystemTime::now()
- .duration_since(SystemTime::UNIX_EPOCH)?
+ .duration_since(SystemTime::UNIX_EPOCH)
+ .unwrap()
.as_secs()
/ seconds_per_day) as u32;
- valence::start_server(
- Game {
- player_count: AtomicUsize::new(0),
- density_noise: SuperSimplex::new(seed),
- hilly_noise: SuperSimplex::new(seed.wrapping_add(1)),
- stone_noise: SuperSimplex::new(seed.wrapping_add(2)),
- gravel_noise: SuperSimplex::new(seed.wrapping_add(3)),
- grass_noise: SuperSimplex::new(seed.wrapping_add(4)),
- },
- None,
- )
+ info!("current seed: {seed}");
+
+ let (finished_sender, finished_receiver) = flume::unbounded();
+ let (pending_sender, pending_receiver) = flume::unbounded();
+
+ let state = Arc::new(ChunkWorkerState {
+ sender: finished_sender,
+ receiver: pending_receiver,
+ density: SuperSimplex::new(seed),
+ hilly: SuperSimplex::new(seed.wrapping_add(1)),
+ stone: SuperSimplex::new(seed.wrapping_add(2)),
+ gravel: SuperSimplex::new(seed.wrapping_add(3)),
+ grass: SuperSimplex::new(seed.wrapping_add(4)),
+ });
+
+ // Chunks are generated in a thread pool for parallelism and to avoid blocking
+ // the main tick loop. You can use your thread pool of choice here (rayon,
+ // bevy_tasks, etc). Only the standard library is used in the example for the
+ // sake of simplicity.
+ //
+ // If your chunk generation algorithm is inexpensive then there's no need to do
+ // this.
+ for _ in 0..thread::available_parallelism().unwrap().get() {
+ let state = state.clone();
+ thread::spawn(move || chunk_worker(state));
+ }
+
+ world.insert_resource(GameState {
+ pending: HashMap::new(),
+ sender: pending_sender,
+ receiver: finished_receiver,
+ });
+
+ let instance = world
+ .resource::()
+ .new_instance(DimensionId::default());
+
+ world.spawn(instance);
}
-struct Game {
- player_count: AtomicUsize,
- density_noise: SuperSimplex,
- hilly_noise: SuperSimplex,
- stone_noise: SuperSimplex,
- gravel_noise: SuperSimplex,
- grass_noise: SuperSimplex,
+fn init_clients(
+ mut clients: Query<&mut Client, Added>,
+ instances: Query>,
+ mut commands: Commands,
+) {
+ for mut client in &mut clients {
+ let instance = instances.single();
+
+ client.set_flat(true);
+ client.set_game_mode(GameMode::Creative);
+ client.set_position(SPAWN_POS);
+ client.set_instance(instance);
+
+ commands.spawn(McEntity::with_uuid(
+ EntityKind::Player,
+ instance,
+ client.uuid(),
+ ));
+ }
}
-const MAX_PLAYERS: usize = 10;
+fn remove_unviewed_chunks(mut instances: Query<&mut Instance>) {
+ instances
+ .single_mut()
+ .retain_chunks(|_, chunk| chunk.is_viewed_mut());
+}
-#[async_trait]
-impl Config for Game {
- type ServerState = Option;
- type ClientState = EntityId;
- type EntityState = ();
- type WorldState = ();
- /// If the chunk should stay loaded at the end of the tick.
- type ChunkState = bool;
- type PlayerListState = ();
- type InventoryState = ();
+fn update_client_views(
+ mut instances: Query<&mut Instance>,
+ mut clients: Query<&mut Client>,
+ mut state: ResMut,
+) {
+ let instance = instances.single_mut();
- 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(),
- ),
+ for client in &mut clients {
+ let view = client.view();
+ let queue_pos = |pos| {
+ if instance.chunk(pos).is_none() {
+ match state.pending.entry(pos) {
+ Entry::Occupied(mut oe) => {
+ if let Some(priority) = oe.get_mut() {
+ let dist = view.pos.distance_squared(pos);
+ *priority = (*priority).min(dist);
+ }
+ }
+ Entry::Vacant(ve) => {
+ let dist = view.pos.distance_squared(pos);
+ ve.insert(Some(dist));
+ }
+ }
+ }
+ };
+
+ // Queue all the new chunks in the view to be sent to the thread pool.
+ if client.is_added() {
+ view.iter().for_each(queue_pos);
+ } else {
+ let old_view = client.old_view();
+ if old_view != view {
+ view.diff(old_view).for_each(queue_pos);
+ }
+ }
+ }
+}
+
+fn send_recv_chunks(mut instances: Query<&mut Instance>, state: ResMut) {
+ let mut instance = instances.single_mut();
+ let state = state.into_inner();
+
+ // Insert the chunks that are finished generating into the instance.
+ for (pos, chunk) in state.receiver.drain() {
+ instance.insert_chunk(pos, chunk);
+ assert!(state.pending.remove(&pos).is_some());
+ }
+
+ // Collect all the new chunks that need to be loaded this tick.
+ let mut to_send = vec![];
+
+ for (pos, priority) in &mut state.pending {
+ if let Some(pri) = priority.take() {
+ to_send.push((pri, pos));
}
}
- fn init(&self, server: &mut Server) {
- server.worlds.insert(DimensionId::default(), ());
- server.state = Some(server.player_lists.insert(()).0);
+ // Sort chunks by ascending priority.
+ to_send.sort_unstable_by_key(|(pri, _)| *pri);
+
+ // Send the sorted chunks to be loaded.
+ for (_, pos) in to_send {
+ let _ = state.sender.try_send(*pos);
}
+}
- fn update(&self, server: &mut Server) {
- let (world_id, world) = server.worlds.iter_mut().next().unwrap();
+fn chunk_worker(state: Arc) {
+ while let Ok(pos) = state.receiver.recv() {
+ let mut chunk = Chunk::new(SECTION_COUNT);
- 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;
- }
+ for offset_z in 0..16 {
+ for offset_x in 0..16 {
+ let x = offset_x as i32 + pos.x * 16;
+ let z = offset_z as i32 + pos.z * 16;
- match server
- .entities
- .insert_with_uuid(EntityKind::Player, client.uuid(), ())
- {
- Some((id, entity)) => {
- entity.set_world(world_id);
- client.state = id
- }
- None => {
- client.disconnect("Conflicting UUID");
- return false;
- }
- }
+ let mut in_terrain = false;
+ let mut depth = 0;
- client.respawn(world_id);
- client.set_flat(true);
- client.set_game_mode(GameMode::Creative);
- client.teleport([0.0, 200.0, 0.0], 0.0, 0.0);
- client.set_player_list(server.state.clone());
+ // Fill in the terrain column.
+ for y in (0..chunk.section_count() as i32 * 16).rev() {
+ const WATER_HEIGHT: i32 = 55;
- if let Some(id) = &server.state {
- server.player_lists[id].insert(
- client.uuid(),
- client.username(),
- client.textures().cloned(),
- client.game_mode(),
- 0,
- None,
- true,
- );
- }
+ let p = DVec3::new(x as f64, y as f64, z as f64);
- client.send_message("Welcome to the terrain example!".italic());
- }
+ let block = if has_terrain_at(&state, p) {
+ let gravel_height = WATER_HEIGHT
+ - 1
+ - (fbm(&state.gravel, p / 10.0, 3, 2.0, 0.5) * 6.0).floor() as i32;
- let player = &mut server.entities[client.state];
- while let Some(event) = client.next_event() {
- event.handle_default(client, player);
- }
-
- let dist = client.view_distance();
- let p = client.position();
-
- for pos in ChunkPos::at(p.x, p.z).in_view(dist) {
- if let Some(chunk) = world.chunks.get_mut(pos) {
- chunk.state = true;
- } else {
- world.chunks.insert(pos, UnloadedChunk::default(), true);
- }
- }
-
- if client.is_disconnected() {
- self.player_count.fetch_sub(1, Ordering::SeqCst);
- if let Some(id) = &server.state {
- server.player_lists[id].remove(client.uuid());
- }
- player.set_deleted(true);
-
- return false;
- }
-
- true
- });
-
- // Remove chunks outside the view distance of players.
- for (_, chunk) in world.chunks.iter_mut() {
- chunk.set_deleted(!chunk.state);
- chunk.state = false;
- }
-
- // Generate chunk data for chunks created this tick.
- world.chunks.par_iter_mut().for_each(|(pos, chunk)| {
- if !chunk.created_this_tick() {
- return;
- }
-
- for z in 0..16 {
- for x in 0..16 {
- let block_x = x as i64 + pos.x as i64 * 16;
- let block_z = z as i64 + pos.z as i64 * 16;
-
- let mut in_terrain = false;
- let mut depth = 0;
-
- for y in (0..chunk.section_count() * 16).rev() {
- let b = terrain_column(
- self,
- block_x,
- y as i64,
- block_z,
- &mut in_terrain,
- &mut depth,
- );
- chunk.set_block_state(x, y, z, b);
- }
-
- // Add grass
- for y in (0..chunk.section_count() * 16).rev() {
- if chunk.block_state(x, y, z).is_air()
- && chunk.block_state(x, y - 1, z) == BlockState::GRASS_BLOCK
- {
- let density = fbm(
- &self.grass_noise,
- [block_x, y as i64, block_z].map(|a| a as f64 / 5.0),
- 4,
- 2.0,
- 0.7,
- );
-
- if density > 0.55 {
- if density > 0.7 && chunk.block_state(x, y + 1, z).is_air() {
- let upper = BlockState::TALL_GRASS
- .set(PropName::Half, PropValue::Upper);
- let lower = BlockState::TALL_GRASS
- .set(PropName::Half, PropValue::Lower);
-
- chunk.set_block_state(x, y + 1, z, upper);
- chunk.set_block_state(x, y, z, lower);
+ if in_terrain {
+ if depth > 0 {
+ depth -= 1;
+ if y < gravel_height {
+ BlockState::GRAVEL
} else {
- chunk.set_block_state(x, y, z, BlockState::GRASS);
+ BlockState::DIRT
}
+ } else {
+ BlockState::STONE
+ }
+ } else {
+ in_terrain = true;
+ let n = noise01(&state.stone, p / 15.0);
+
+ depth = (n * 5.0).round() as u32;
+
+ if y < gravel_height {
+ BlockState::GRAVEL
+ } else if y < WATER_HEIGHT - 1 {
+ BlockState::DIRT
+ } else {
+ BlockState::GRASS_BLOCK
+ }
+ }
+ } else {
+ in_terrain = false;
+ depth = 0;
+ if y < WATER_HEIGHT {
+ BlockState::WATER
+ } else {
+ BlockState::AIR
+ }
+ };
+
+ chunk.set_block_state(offset_x, y as usize, offset_z, block);
+ }
+
+ // Add grass on top of grass blocks.
+ for y in (0..chunk.section_count() * 16).rev() {
+ if chunk.block_state(offset_x, y, offset_z).is_air()
+ && chunk.block_state(offset_x, y - 1, offset_z) == BlockState::GRASS_BLOCK
+ {
+ let p = DVec3::new(x as f64, y as f64, z as f64);
+ let density = fbm(&state.grass, p / 5.0, 4, 2.0, 0.7);
+
+ if density > 0.55 {
+ if density > 0.7
+ && chunk.block_state(offset_x, y + 1, offset_z).is_air()
+ {
+ let upper =
+ BlockState::TALL_GRASS.set(PropName::Half, PropValue::Upper);
+ let lower =
+ BlockState::TALL_GRASS.set(PropName::Half, PropValue::Lower);
+
+ chunk.set_block_state(offset_x, y + 1, offset_z, upper);
+ chunk.set_block_state(offset_x, y, offset_z, lower);
+ } else {
+ chunk.set_block_state(offset_x, y, offset_z, BlockState::GRASS);
}
}
}
}
}
- });
+ }
+
+ let _ = state.sender.try_send((pos, chunk));
}
}
-fn terrain_column(
- g: &Game,
- x: i64,
- y: i64,
- z: i64,
- in_terrain: &mut bool,
- depth: &mut u32,
-) -> BlockState {
- const WATER_HEIGHT: i64 = 55;
-
- if has_terrain_at(g, x, y, z) {
- let gravel_height = WATER_HEIGHT
- - 1
- - (fbm(
- &g.gravel_noise,
- [x, y, z].map(|a| a as f64 / 10.0),
- 3,
- 2.0,
- 0.5,
- ) * 6.0)
- .floor() as i64;
-
- if *in_terrain {
- if *depth > 0 {
- *depth -= 1;
- if y < gravel_height {
- BlockState::GRAVEL
- } else {
- BlockState::DIRT
- }
- } else {
- BlockState::STONE
- }
- } else {
- *in_terrain = true;
- let n = noise01(&g.stone_noise, [x, y, z].map(|a| a as f64 / 15.0));
-
- *depth = (n * 5.0).round() as u32;
-
- if y < gravel_height {
- BlockState::GRAVEL
- } else if y < WATER_HEIGHT - 1 {
- BlockState::DIRT
- } else {
- BlockState::GRASS_BLOCK
- }
- }
- } else {
- *in_terrain = false;
- *depth = 0;
- if y < WATER_HEIGHT {
- BlockState::WATER
- } else {
- BlockState::AIR
- }
- }
-}
-
-fn has_terrain_at(g: &Game, x: i64, y: i64, z: i64) -> bool {
- let hilly = Lerp::lerp_unclamped(
- 0.1,
- 1.0,
- noise01(&g.hilly_noise, [x, y, z].map(|a| a as f64 / 400.0)).powi(2),
- );
+fn has_terrain_at(state: &ChunkWorkerState, p: DVec3) -> bool {
+ let hilly = lerp(0.1, 1.0, noise01(&state.hilly, p / 400.0)).powi(2);
let lower = 15.0 + 100.0 * hilly;
let upper = lower + 100.0 * hilly;
- if y as f64 <= lower {
+ if p.y <= lower {
return true;
- } else if y as f64 >= upper {
+ } else if p.y >= upper {
return false;
}
- let density = 1.0 - lerpstep(lower, upper, y as f64);
+ let density = 1.0 - lerpstep(lower, upper, p.y);
+
+ let n = fbm(&state.density, p / 100.0, 4, 2.0, 0.5);
- let n = fbm(
- &g.density_noise,
- [x, y, z].map(|a| a as f64 / 100.0),
- 4,
- 2.0,
- 0.5,
- );
n < density
}
+fn lerp(a: f64, b: f64, t: f64) -> f64 {
+ a * (1.0 - t) + b * t
+}
+
fn lerpstep(edge0: f64, edge1: f64, x: f64) -> f64 {
if x <= edge0 {
0.0
@@ -318,14 +322,14 @@ fn lerpstep(edge0: f64, edge1: f64, x: f64) -> f64 {
}
}
-fn fbm(noise: &SuperSimplex, p: [f64; 3], octaves: u32, lacunarity: f64, persistence: f64) -> f64 {
+fn fbm(noise: &SuperSimplex, p: DVec3, octaves: u32, lacunarity: f64, persistence: f64) -> f64 {
let mut freq = 1.0;
let mut amp = 1.0;
let mut amp_sum = 0.0;
let mut sum = 0.0;
for _ in 0..octaves {
- let n = noise01(noise, p.map(|a| a * freq));
+ let n = noise01(noise, p * freq);
sum += n * amp;
amp_sum += amp;
@@ -337,6 +341,6 @@ fn fbm(noise: &SuperSimplex, p: [f64; 3], octaves: u32, lacunarity: f64, persist
sum / amp_sum
}
-fn noise01(noise: &SuperSimplex, xyz: [f64; 3]) -> f64 {
- (noise.get(xyz) + 1.0) / 2.0
+fn noise01(noise: &SuperSimplex, p: DVec3) -> f64 {
+ (noise.get(p.to_array()) + 1.0) / 2.0
}
diff --git a/crates/valence/examples/text.rs b/crates/valence/examples/text.rs
index a2de3cf..38b9674 100644
--- a/crates/valence/examples/text.rs
+++ b/crates/valence/examples/text.rs
@@ -1,213 +1,125 @@
-use std::net::SocketAddr;
-
+use valence::client::despawn_disconnected_clients;
+use valence::client::event::default_event_handler;
use valence::prelude::*;
+use valence_protocol::translation_key;
-pub fn main() -> ShutdownResult {
+const SPAWN_Y: i32 = 64;
+
+pub fn main() {
tracing_subscriber::fmt().init();
- valence::start_server(Game::default(), ServerState::default())
+ App::new()
+ .add_plugin(ServerPlugin::new(()))
+ .add_system_to_stage(EventLoop, default_event_handler)
+ .add_system_set(PlayerList::default_system_set())
+ .add_startup_system(setup)
+ .add_system(init_clients)
+ .add_system(despawn_disconnected_clients)
+ .run();
}
-#[derive(Default)]
-struct Game {}
+fn setup(world: &mut World) {
+ let mut instance = world
+ .resource::()
+ .new_instance(DimensionId::default());
-#[derive(Default)]
-struct ClientState {
- entity_id: EntityId,
-}
-
-#[derive(Default)]
-struct ServerState {
- world: WorldId,
-}
-
-const FLOOR_Y: i32 = 1;
-const PLATFORM_X: i32 = 3;
-const PLATFORM_Z: i32 = 3;
-const SPAWN_POS: Vec3 = Vec3::new(1.5, 2.0, 1.5);
-
-#[async_trait]
-impl Config for Game {
- type ServerState = ServerState;
- type ClientState = ClientState;
- type EntityState = ();
- type WorldState = ();
- type ChunkState = ();
- type PlayerListState = ();
- type InventoryState = ();
-
- async fn server_list_ping(
- &self,
- _server: &SharedServer,
- _remote_addr: SocketAddr,
- _protocol_version: i32,
- ) -> ServerListPing {
- ServerListPing::Respond {
- online_players: -1,
- max_players: -1,
- description: "Hello Valence! ".into_text() + "Text Example".color(Color::AQUA),
- favicon_png: Some(
- include_bytes!("../../../assets/logo-64x64.png")
- .as_slice()
- .into(),
- ),
- player_sample: Default::default(),
+ for z in -5..5 {
+ for x in -5..5 {
+ instance.insert_chunk([x, z], Chunk::default());
}
}
- fn init(&self, server: &mut Server) {
- server.state = ServerState {
- world: create_world(server),
- };
- }
-
- fn update(&self, server: &mut Server) {
- server.clients.retain(|_, client| {
- if client.created_this_tick() {
- // Boilerplate for client initialization
- match server
- .entities
- .insert_with_uuid(EntityKind::Player, client.uuid(), ())
- {
- Some((id, _)) => client.entity_id = id,
- None => {
- client.disconnect("Conflicting UUID");
- return false;
- }
- }
-
- let world_id = server.state.world;
-
- client.set_flat(true);
- client.respawn(world_id);
- client.teleport(SPAWN_POS, -90.0, 0.0);
- client.set_game_mode(GameMode::Creative);
-
- client.send_message("Welcome to the text example.".bold());
- client.send_message(
- "The following examples show ways to use the different text components.",
- );
-
- // Text examples
- client.send_message("\nText");
- client.send_message(" - ".into_text() + Text::text("Plain text"));
- client.send_message(" - ".into_text() + Text::text("Styled text").italic());
- client.send_message(
- " - ".into_text() + Text::text("Colored text").color(Color::GOLD),
- );
- client.send_message(
- " - ".into_text()
- + Text::text("Colored and styled text")
- .color(Color::GOLD)
- .italic()
- .underlined(),
- );
-
- // Translated text examples
- client.send_message("\nTranslated Text");
- client.send_message(
- " - 'chat.type.advancement.task': ".into_text()
- + Text::translate(translation_key::CHAT_TYPE_ADVANCEMENT_TASK, []),
- );
- client.send_message(
- " - 'chat.type.advancement.task' with slots: ".into_text()
- + Text::translate(
- translation_key::CHAT_TYPE_ADVANCEMENT_TASK,
- ["arg1".into(), "arg2".into()],
- ),
- );
- client.send_message(
- " - 'custom.translation_key': ".into_text()
- + Text::translate("custom.translation_key", []),
- );
-
- // Scoreboard value example
- client.send_message("\nScoreboard Values");
- client.send_message(" - Score: ".into_text() + Text::score("*", "objective", None));
- client.send_message(
- " - Score with custom value: ".into_text()
- + Text::score("*", "objective", Some("value".into())),
- );
-
- // Entity names example
- client.send_message("\nEntity Names (Selector)");
- client.send_message(" - Nearest player: ".into_text() + Text::selector("@p", None));
- client.send_message(" - Random player: ".into_text() + Text::selector("@r", None));
- client.send_message(" - All players: ".into_text() + Text::selector("@a", None));
- client.send_message(" - All entities: ".into_text() + Text::selector("@e", None));
- client.send_message(
- " - All entities with custom separator: ".into_text()
- + Text::selector("@e", Some(", ".into_text().color(Color::GOLD))),
- );
-
- // Keybind example
- client.send_message("\nKeybind");
- client.send_message(
- " - 'key.inventory': ".into_text() + Text::keybind("key.inventory"),
- );
-
- // NBT examples
- client.send_message("\nNBT");
- client.send_message(
- " - Block NBT: ".into_text() + Text::block_nbt("{}", "0 1 0", None, None),
- );
- client.send_message(
- " - Entity NBT: ".into_text() + Text::entity_nbt("{}", "@a", None, None),
- );
- client.send_message(
- " - Storage NBT: ".into_text()
- + Text::storage_nbt(ident!("storage.key"), "@a", None, None),
- );
-
- client.send_message(
- "\n\n↑ ".into_text().bold().color(Color::GOLD)
- + "Scroll up to see the full example!".into_text().not_bold(),
- );
- }
-
- if client.position().y < 0.0 {
- client.teleport(SPAWN_POS, 0.0, 0.0);
- }
-
- let player = server.entities.get_mut(client.entity_id).unwrap();
-
- while let Some(event) = client.next_event() {
- event.handle_default(client, player);
- }
-
- if client.is_disconnected() {
- player.set_deleted(true);
- return false;
- }
-
- true
- });
- }
-}
-
-// Boilerplate for creating world
-fn create_world(server: &mut Server) -> WorldId {
- let dimension = server.shared.dimensions().next().unwrap();
-
- let (world_id, world) = server.worlds.insert(dimension.0, ());
-
- // Create chunks
- for z in -3..3 {
- for x in -3..3 {
- world.chunks.insert([x, z], UnloadedChunk::default(), ());
+ for z in -25..25 {
+ for x in -25..25 {
+ instance.set_block_state([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
}
}
- // Create platform
- let platform_block = BlockState::GLASS;
-
- for z in 0..PLATFORM_Z {
- for x in 0..PLATFORM_X {
- world
- .chunks
- .set_block_state([x, FLOOR_Y, z], platform_block);
- }
- }
-
- world_id
+ world.spawn(instance);
+}
+
+fn init_clients(
+ mut clients: Query<&mut Client, Added>,
+ instances: Query>,
+) {
+ for mut client in &mut clients {
+ client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
+ client.set_instance(instances.single());
+ client.set_game_mode(GameMode::Creative);
+
+ client.send_message("Welcome to the text example.".bold());
+ client
+ .send_message("The following examples show ways to use the different text components.");
+
+ // Text examples
+ client.send_message("\nText");
+ client.send_message(" - ".into_text() + Text::text("Plain text"));
+ client.send_message(" - ".into_text() + Text::text("Styled text").italic());
+ client.send_message(" - ".into_text() + Text::text("Colored text").color(Color::GOLD));
+ client.send_message(
+ " - ".into_text()
+ + Text::text("Colored and styled text")
+ .color(Color::GOLD)
+ .italic()
+ .underlined(),
+ );
+
+ // Translated text examples
+ client.send_message("\nTranslated Text");
+ client.send_message(
+ " - 'chat.type.advancement.task': ".into_text()
+ + Text::translate(translation_key::CHAT_TYPE_ADVANCEMENT_TASK, []),
+ );
+ client.send_message(
+ " - 'chat.type.advancement.task' with slots: ".into_text()
+ + Text::translate(
+ translation_key::CHAT_TYPE_ADVANCEMENT_TASK,
+ ["arg1".into(), "arg2".into()],
+ ),
+ );
+ client.send_message(
+ " - 'custom.translation_key': ".into_text()
+ + Text::translate("custom.translation_key", []),
+ );
+
+ // Scoreboard value example
+ client.send_message("\nScoreboard Values");
+ client.send_message(" - Score: ".into_text() + Text::score("*", "objective", None));
+ client.send_message(
+ " - Score with custom value: ".into_text()
+ + Text::score("*", "objective", Some("value".into())),
+ );
+
+ // Entity names example
+ client.send_message("\nEntity Names (Selector)");
+ client.send_message(" - Nearest player: ".into_text() + Text::selector("@p", None));
+ client.send_message(" - Random player: ".into_text() + Text::selector("@r", None));
+ client.send_message(" - All players: ".into_text() + Text::selector("@a", None));
+ client.send_message(" - All entities: ".into_text() + Text::selector("@e", None));
+ client.send_message(
+ " - All entities with custom separator: ".into_text()
+ + Text::selector("@e", Some(", ".into_text().color(Color::GOLD))),
+ );
+
+ // Keybind example
+ client.send_message("\nKeybind");
+ client.send_message(" - 'key.inventory': ".into_text() + Text::keybind("key.inventory"));
+
+ // NBT examples
+ client.send_message("\nNBT");
+ client.send_message(
+ " - Block NBT: ".into_text() + Text::block_nbt("{}", "0 1 0", None, None),
+ );
+ client
+ .send_message(" - Entity NBT: ".into_text() + Text::entity_nbt("{}", "@a", None, None));
+ client.send_message(
+ " - Storage NBT: ".into_text()
+ + Text::storage_nbt(ident!("storage.key"), "@a", None, None),
+ );
+
+ client.send_message(
+ "\n\n↑ ".into_text().bold().color(Color::GOLD)
+ + "Scroll up to see the full example!".into_text().not_bold(),
+ );
+ }
}
diff --git a/crates/valence/src/biome.rs b/crates/valence/src/biome.rs
index 676a6f3..1b603ca 100644
--- a/crates/valence/src/biome.rs
+++ b/crates/valence/src/biome.rs
@@ -10,18 +10,21 @@ use valence_protocol::ident::Ident;
/// Identifies a particular [`Biome`] on the server.
///
-/// The default biome ID refers to the first biome added in the server's
-/// [configuration](crate::config::Config).
+/// The default biome ID refers to the first biome added in
+/// [`ServerPlugin::biomes`].
///
-/// To obtain biome IDs for other biomes, call
-/// [`biomes`](crate::server::SharedServer::biomes).
+/// To obtain biome IDs for other biomes, see [`ServerPlugin::biomes`].
+///
+/// [`ServerPlugin::biomes`]: crate::config::ServerPlugin::biomes
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct BiomeId(pub(crate) u16);
/// Contains the configuration for a biome.
///
/// Biomes are registered once at startup through
-/// [`biomes`](crate::config::Config::biomes).
+/// [`ServerPlugin::with_biomes`]
+///
+/// [`ServerPlugin::with_biomes`]: crate::config::ServerPlugin::with_biomes
#[derive(Clone, Debug)]
pub struct Biome {
/// The unique name for this biome. The name can be
@@ -40,8 +43,7 @@ pub struct Biome {
pub additions_sound: Option,
pub mood_sound: Option,
pub particle: Option,
- // TODO: The following fields should be added if they can affect the appearance of the biome to
- // clients.
+ // TODO
// * depth: f32
// * temperature: f32
// * scale: f32
diff --git a/crates/valence/src/chunk.rs b/crates/valence/src/chunk.rs
deleted file mode 100644
index 81712be..0000000
--- a/crates/valence/src/chunk.rs
+++ /dev/null
@@ -1,1101 +0,0 @@
-//! Chunks and related types.
-//!
-//! A chunk is a 16x16-block segment of a world with a height determined by the
-//! [`Dimension`](crate::dimension::Dimension) of the world.
-//!
-//! In addition to blocks, chunks also contain [biomes](crate::biome::Biome).
-//! Every 4x4x4 segment of blocks in a chunk corresponds to a biome.
-
-use std::collections::hash_map::Entry;
-use std::io::Write;
-use std::iter::FusedIterator;
-use std::mem;
-use std::ops::{Deref, DerefMut, Index, IndexMut};
-use std::sync::{Mutex, MutexGuard};
-
-use entity_partition::PartitionCell;
-use paletted_container::PalettedContainer;
-pub use pos::ChunkPos;
-use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
-use rustc_hash::FxHashMap;
-use valence_nbt::compound;
-use valence_protocol::packets::s2c::play::{
- BlockUpdate, ChunkDataAndUpdateLightEncode, UpdateSectionBlocksEncode,
-};
-use valence_protocol::{BlockPos, BlockState, Encode, LengthPrefixedArray, VarInt, VarLong};
-
-use crate::biome::BiomeId;
-use crate::config::Config;
-use crate::packet::{PacketWriter, WritePacket};
-use crate::util::bit_width;
-
-pub(crate) mod entity_partition;
-mod paletted_container;
-mod pos;
-
-/// A container for all [`LoadedChunk`]s in a [`World`](crate::world::World).
-pub struct Chunks {
- /// Maps chunk positions to chunks. We store both loaded chunks and
- /// partition cells here so we can get both in a single hashmap lookup
- /// during the client update procedure.
- chunks: FxHashMap>, PartitionCell)>,
- dimension_height: i32,
- dimension_min_y: i32,
- filler_sky_light_mask: Box<[u64]>,
- /// Sending filler light data causes the vanilla client to lag
- /// less. Hopefully we can remove this in the future.
- filler_sky_light_arrays: Box<[LengthPrefixedArray]>,
- biome_registry_len: usize,
- compression_threshold: Option,
-}
-
-impl Chunks {
- pub(crate) fn new(
- dimension_height: i32,
- dimension_min_y: i32,
- biome_registry_len: usize,
- compression_threshold: Option,
- ) -> Self {
- let section_count = (dimension_height / 16 + 2) as usize;
-
- let mut sky_light_mask = vec![0; num::Integer::div_ceil(§ion_count, &16)];
-
- for i in 0..section_count {
- sky_light_mask[i / 64] |= 1 << (i % 64);
- }
-
- Self {
- chunks: FxHashMap::default(),
- dimension_height,
- dimension_min_y,
- filler_sky_light_mask: sky_light_mask.into(),
- filler_sky_light_arrays: vec![LengthPrefixedArray([0xff; 2048]); section_count].into(),
- biome_registry_len,
- compression_threshold,
- }
- }
-
- /// Consumes an [`UnloadedChunk`] and creates a [`LoadedChunk`] at a given
- /// position. An exclusive reference to the new chunk is returned.
- ///
- /// If a chunk at the position already exists, then the old chunk
- /// is overwritten and its contents are dropped.
- ///
- /// The given chunk is resized to match the height of the world as if by
- /// calling [`UnloadedChunk::resize`].
- ///
- /// **Note**: For the vanilla Minecraft client to see a chunk, all chunks
- /// adjacent to it must also be loaded. Clients should not be spawned within
- /// unloaded chunks via [`respawn`](crate::client::Client::respawn).
- pub fn insert(
- &mut self,
- pos: impl Into,
- chunk: UnloadedChunk,
- state: C::ChunkState,
- ) -> &mut LoadedChunk {
- let dimension_section_count = (self.dimension_height / 16) as usize;
- let loaded = LoadedChunk::new(chunk, dimension_section_count, state);
-
- match self.chunks.entry(pos.into()) {
- Entry::Occupied(mut oe) => {
- oe.get_mut().0 = Some(loaded);
- oe.into_mut().0.as_mut().unwrap()
- }
- Entry::Vacant(ve) => ve
- .insert((Some(loaded), PartitionCell::new()))
- .0
- .as_mut()
- .unwrap(),
- }
- }
-
- /// Returns the height of all loaded chunks in the world. This returns the
- /// same value as [`Chunk::section_count`] multiplied by 16 for all loaded
- /// chunks.
- pub fn height(&self) -> usize {
- self.dimension_height as usize
- }
-
- /// The minimum Y coordinate in world space that chunks in this world can
- /// occupy. This is relevant for [`Chunks::block_state`] and
- /// [`Chunks::set_block_state`].
- pub fn min_y(&self) -> i32 {
- self.dimension_min_y
- }
-
- /// Gets a shared reference to the chunk at the provided position.
- ///
- /// If there is no chunk at the position, then `None` is returned.
- pub fn get(&self, pos: impl Into) -> Option<&LoadedChunk> {
- self.chunks.get(&pos.into())?.0.as_ref()
- }
-
- pub(crate) fn chunk_and_cell(
- &self,
- pos: ChunkPos,
- ) -> Option<&(Option>, PartitionCell)> {
- self.chunks.get(&pos)
- }
-
- fn cell_mut(&mut self, pos: ChunkPos) -> Option<&mut PartitionCell> {
- self.chunks.get_mut(&pos).map(|(_, cell)| cell)
- }
-
- /// Gets an exclusive reference to the chunk at the provided position.
- ///
- /// If there is no chunk at the position, then `None` is returned.
- pub fn get_mut(&mut self, pos: impl Into) -> Option<&mut LoadedChunk> {
- self.chunks.get_mut(&pos.into())?.0.as_mut()
- }
-
- /// Returns an iterator over all chunks in the world in an unspecified
- /// order.
- pub fn iter(&self) -> impl FusedIterator
- )> + Clone + '_ {
- self.chunks
- .iter()
- .filter_map(|(&pos, (chunk, _))| chunk.as_ref().map(|c| (pos, c)))
- }
-
- /// Returns a mutable iterator over all chunks in the world in an
- /// unspecified order.
- pub fn iter_mut(&mut self) -> impl FusedIterator
- )> + '_ {
- self.chunks
- .iter_mut()
- .filter_map(|(&pos, (chunk, _))| chunk.as_mut().map(|c| (pos, c)))
- }
-
- fn cells_mut(
- &mut self,
- ) -> impl ExactSizeIterator
- + FusedIterator + '_ {
- self.chunks.iter_mut().map(|(_, (_, cell))| cell)
- }
-
- /// Returns a parallel iterator over all chunks in the world in an
- /// unspecified order.
- pub fn par_iter(
- &self,
- ) -> impl ParallelIterator
- )> + Clone + '_ {
- self.chunks
- .par_iter()
- .filter_map(|(&pos, (chunk, _))| chunk.as_ref().map(|c| (pos, c)))
- }
-
- /// Returns a parallel mutable iterator over all chunks in the world in an
- /// unspecified order.
- pub fn par_iter_mut(
- &mut self,
- ) -> impl ParallelIterator
- )> + '_ {
- self.chunks
- .par_iter_mut()
- .filter_map(|(&pos, (chunk, _))| chunk.as_mut().map(|c| (pos, c)))
- }
-
- /// Gets the block state at an absolute block position in world space.
- ///
- /// If the position is not inside of a chunk, then `None` is returned.
- ///
- /// **Note**: if you need to get a large number of blocks, it is more
- /// efficient to read from the chunks directly with
- /// [`Chunk::block_state`].
- pub fn block_state(&self, pos: impl Into) -> Option {
- let pos = pos.into();
- let chunk_pos = ChunkPos::from(pos);
-
- let chunk = self.get(chunk_pos)?;
-
- let y = pos.y.checked_sub(self.dimension_min_y)?.try_into().ok()?;
-
- if y < chunk.section_count() * 16 {
- Some(chunk.block_state(
- pos.x.rem_euclid(16) as usize,
- y,
- pos.z.rem_euclid(16) as usize,
- ))
- } else {
- None
- }
- }
-
- /// Sets the block state at an absolute block position in world space. The
- /// previous block state at the position is returned.
- ///
- /// If the given position is not inside of a loaded chunk, then a new chunk
- /// is created at the position before the block is set.
- ///
- /// If the position is completely out of bounds, then no new chunk is
- /// created and [`BlockState::AIR`] is returned.
- pub fn set_block_state(&mut self, pos: impl Into, block: BlockState) -> BlockState
- where
- C::ChunkState: Default,
- {
- let pos = pos.into();
-
- let Some(y) = pos.y.checked_sub(self.dimension_min_y).and_then(|y| y.try_into().ok()) else {
- return BlockState::AIR;
- };
-
- if y >= self.dimension_height as usize {
- return BlockState::AIR;
- }
-
- let chunk = match self.chunks.entry(ChunkPos::from(pos)) {
- Entry::Occupied(oe) => oe.into_mut().0.get_or_insert_with(|| {
- let dimension_section_count = (self.dimension_height / 16) as usize;
- LoadedChunk::new(
- UnloadedChunk::default(),
- dimension_section_count,
- Default::default(),
- )
- }),
- Entry::Vacant(ve) => {
- let dimension_section_count = (self.dimension_height / 16) as usize;
- let loaded = LoadedChunk::new(
- UnloadedChunk::default(),
- dimension_section_count,
- Default::default(),
- );
-
- ve.insert((Some(loaded), PartitionCell::new()))
- .0
- .as_mut()
- .unwrap()
- }
- };
-
- chunk.set_block_state(
- pos.x.rem_euclid(16) as usize,
- y,
- pos.z.rem_euclid(16) as usize,
- block,
- )
- }
-
- pub(crate) fn update_caches(&mut self) {
- let min_y = self.dimension_min_y;
-
- self.chunks.par_iter_mut().for_each(|(&pos, (chunk, _))| {
- let Some(chunk) = chunk else {
- // There is no chunk at this position.
- return;
- };
-
- if chunk.deleted {
- // Deleted chunks are not sending packets to anyone.
- return;
- }
-
- let mut compression_scratch = vec![];
- let mut blocks = vec![];
-
- chunk.cached_update_packets.clear();
- let mut any_blocks_modified = false;
-
- for (sect_y, sect) in chunk.sections.iter_mut().enumerate() {
- let modified_blocks_count: u32 = sect
- .modified_blocks
- .iter()
- .map(|&bits| bits.count_ones())
- .sum();
-
- // If the chunk is created this tick, clients are only going to be sent the
- // chunk data packet so there is no need to cache the modified blocks packets.
- if !chunk.created_this_tick {
- if modified_blocks_count == 1 {
- let (i, bits) = sect
- .modified_blocks
- .iter()
- .cloned()
- .enumerate()
- .find(|(_, n)| *n > 0)
- .expect("invalid modified count");
-
- debug_assert_eq!(bits.count_ones(), 1);
-
- let idx = i * USIZE_BITS + bits.trailing_zeros() as usize;
- let block = sect.block_states.get(idx);
-
- let global_x = pos.x * 16 + (idx % 16) as i32;
- let global_y = sect_y as i32 * 16 + (idx / (16 * 16)) as i32 + min_y;
- let global_z = pos.z * 16 + (idx / 16 % 16) as i32;
-
- let mut writer = PacketWriter::new(
- &mut chunk.cached_update_packets,
- self.compression_threshold,
- &mut compression_scratch,
- );
-
- writer
- .write_packet(&BlockUpdate {
- position: BlockPos::new(global_x, global_y, global_z),
- block_id: VarInt(block.to_raw() as _),
- })
- .unwrap();
- } else if modified_blocks_count > 1 {
- blocks.clear();
-
- for y in 0..16 {
- for z in 0..16 {
- for x in 0..16 {
- let idx = x as usize + z as usize * 16 + y as usize * 16 * 16;
-
- if sect.is_block_modified(idx) {
- let block_id = sect.block_states.get(idx).to_raw();
- let compact =
- (block_id as i64) << 12 | (x << 8 | z << 4 | y);
-
- blocks.push(VarLong(compact));
- }
- }
- }
- }
-
- let chunk_section_position = (pos.x as i64) << 42
- | (pos.z as i64 & 0x3fffff) << 20
- | (sect_y as i64 + min_y.div_euclid(16) as i64) & 0xfffff;
-
- let mut writer = PacketWriter::new(
- &mut chunk.cached_update_packets,
- self.compression_threshold,
- &mut compression_scratch,
- );
-
- writer
- .write_packet(&UpdateSectionBlocksEncode {
- chunk_section_position,
- invert_trust_edges: false,
- blocks: &blocks,
- })
- .unwrap();
- }
- }
-
- if modified_blocks_count > 0 {
- any_blocks_modified = true;
- sect.modified_blocks.fill(0);
- }
- }
-
- // Clear the cache if the cache was invalidated.
- if any_blocks_modified || chunk.any_biomes_modified {
- chunk.any_biomes_modified = false;
- chunk.cached_init_packet.get_mut().unwrap().clear();
- }
-
- // Initialize the chunk data cache on new chunks here so this work can be done
- // in parallel.
- if chunk.created_this_tick() {
- debug_assert!(chunk.cached_init_packet.get_mut().unwrap().is_empty());
-
- let _unused: MutexGuard<_> = chunk.get_chunk_data_packet(
- &mut compression_scratch,
- pos,
- self.biome_registry_len,
- &self.filler_sky_light_mask,
- &self.filler_sky_light_arrays,
- self.compression_threshold,
- );
- }
- });
- }
-
- /// Clears changes to partition cells and removes deleted chunks and
- /// partition cells.
- pub(crate) fn update(&mut self) {
- self.chunks.retain(|_, (chunk_opt, cell)| {
- if let Some(chunk) = chunk_opt {
- if chunk.deleted {
- *chunk_opt = None;
- } else {
- chunk.created_this_tick = false;
- }
- }
-
- cell.clear_incoming_outgoing();
-
- chunk_opt.is_some() || cell.entities().len() > 0
- });
- }
-}
-
-impl> Index
for Chunks {
- type Output = LoadedChunk;
-
- fn index(&self, index: P) -> &Self::Output {
- let ChunkPos { x, z } = index.into();
- self.get((x, z))
- .unwrap_or_else(|| panic!("missing chunk at ({x}, {z})"))
- }
-}
-
-impl> IndexMut for Chunks {
- fn index_mut(&mut self, index: P) -> &mut Self::Output {
- let ChunkPos { x, z } = index.into();
- self.get_mut((x, z))
- .unwrap_or_else(|| panic!("missing chunk at ({x}, {z})"))
- }
-}
-
-/// Operations that can be performed on a chunk. [`LoadedChunk`] and
-/// [`UnloadedChunk`] implement this trait.
-pub trait Chunk {
- /// Returns the number of sections in this chunk. To get the height of the
- /// chunk in meters, multiply the result by 16.
- fn section_count(&self) -> usize;
-
- /// Gets the block state at the provided offsets in the chunk.
- ///
- /// **Note**: The arguments to this function are offsets from the minimum
- /// corner of the chunk in _chunk space_ rather than _world space_.
- ///
- /// # Panics
- ///
- /// Panics if the offsets are outside the bounds of the chunk. `x` and `z`
- /// must be less than 16 while `y` must be less than `section_count() * 16`.
- fn block_state(&self, x: usize, y: usize, z: usize) -> BlockState;
-
- /// Sets the block state at the provided offsets in the chunk. The previous
- /// block state at the position is returned.
- ///
- /// **Note**: The arguments to this function are offsets from the minimum
- /// corner of the chunk in _chunk space_ rather than _world space_.
- ///
- /// # Panics
- ///
- /// Panics if the offsets are outside the bounds of the chunk. `x` and `z`
- /// must be less than 16 while `y` must be less than `section_count() * 16`.
- fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) -> BlockState;
-
- /// Sets every block in a section to the given block state.
- ///
- /// This is semantically equivalent to setting every block in the section
- /// with [`set_block_state`]. However, this function may be implemented more
- /// efficiently.
- ///
- /// # Panics
- ///
- /// Panics if `sect_y` is out of bounds. `sect_y` must be less than the
- /// section count.
- ///
- /// [`set_block_state`]: Self::set_block_state
- fn fill_block_states(&mut self, sect_y: usize, block: BlockState);
-
- /// 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`.
- ///
- /// # Panics
- ///
- /// Panics if the offsets are outside the bounds of the chunk. `x` and `z`
- /// must be less than 4 while `y` must be less than `section_count() * 4`.
- fn biome(&self, x: usize, y: usize, z: usize) -> BiomeId;
-
- /// Sets the biome at the provided offsets in the chunk. The previous
- /// biome at the position is returned.
- ///
- /// **Note**: the arguments are **not** block positions. Biomes are 4x4x4
- /// segments of a chunk, so `x` and `z` are in `0..4`.
- ///
- /// # Panics
- ///
- /// Panics if the offsets are outside the bounds of the chunk. `x` and `z`
- /// must be less than 4 while `y` must be less than `section_count() * 4`.
- fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) -> BiomeId;
-
- /// Sets every biome in a section to the given block state.
- ///
- /// This is semantically equivalent to setting every biome in the section
- /// with [`set_biome`]. However, this function may be implemented more
- /// efficiently.
- ///
- /// # Panics
- ///
- /// Panics if `sect_y` is out of bounds. `sect_y` must be less than the
- /// section count.
- ///
- /// [`set_biome`]: Self::set_biome
- fn fill_biomes(&mut self, sect_y: usize, biome: BiomeId);
-
- /// Optimizes this chunk to use the minimum amount of memory possible. It
- /// should have no observable effect on the contents of the chunk.
- ///
- /// This is a potentially expensive operation. The function is most
- /// effective when a large number of blocks and biomes have changed states.
- fn optimize(&mut self);
-}
-
-/// A chunk that is not loaded in any world.
-pub struct UnloadedChunk {
- sections: Vec,
- // TODO: block_entities: BTreeMap,
-}
-
-impl UnloadedChunk {
- /// Constructs a new unloaded chunk containing only [`BlockState::AIR`] and
- /// [`BiomeId::default()`] with the given number of sections. A section is a
- /// 16x16x16 meter volume.
- pub fn new(section_count: usize) -> Self {
- let mut chunk = Self { sections: vec![] };
- chunk.resize(section_count);
- chunk
- }
-
- /// Changes the section count of the chunk to `new_section_count`. This is a
- /// potentially expensive operation that may involve copying.
- ///
- /// The chunk is extended and truncated from the top. New blocks are always
- /// [`BlockState::AIR`] and biomes are [`BiomeId::default()`].
- pub fn resize(&mut self, new_section_count: usize) {
- let old_section_count = self.section_count();
-
- if new_section_count > old_section_count {
- self.sections
- .reserve_exact(new_section_count - old_section_count);
- self.sections
- .resize_with(new_section_count, ChunkSection::default);
- debug_assert_eq!(self.sections.capacity(), self.sections.len());
- } else {
- self.sections.truncate(new_section_count);
- }
- }
-}
-
-/// Constructs a new chunk with height `0`.
-impl Default for UnloadedChunk {
- fn default() -> Self {
- Self::new(0)
- }
-}
-
-impl Chunk for UnloadedChunk {
- fn section_count(&self) -> usize {
- self.sections.len()
- }
-
- fn block_state(&self, x: usize, y: usize, z: usize) -> BlockState {
- assert!(
- x < 16 && y < self.section_count() * 16 && z < 16,
- "chunk block offsets of ({x}, {y}, {z}) are out of bounds"
- );
-
- self.sections[y / 16]
- .block_states
- .get(x + z * 16 + y % 16 * 16 * 16)
- }
-
- fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) -> BlockState {
- assert!(
- x < 16 && y < self.section_count() * 16 && z < 16,
- "chunk block offsets of ({x}, {y}, {z}) are out of bounds"
- );
-
- let mut sect = &mut self.sections[y / 16];
-
- let old_block = sect.block_states.set(x + z * 16 + y % 16 * 16 * 16, block);
-
- match (block.is_air(), old_block.is_air()) {
- (true, false) => sect.non_air_count -= 1,
- (false, true) => sect.non_air_count += 1,
- _ => {}
- }
-
- old_block
- }
-
- fn fill_block_states(&mut self, sect_y: usize, block: BlockState) {
- let Some(sect) = self.sections.get_mut(sect_y) else {
- panic!(
- "section index {sect_y} out of bounds for chunk with {} sections",
- self.section_count()
- )
- };
-
- if block.is_air() {
- sect.non_air_count = 0;
- } else {
- sect.non_air_count = SECTION_BLOCK_COUNT as u16;
- }
-
- sect.block_states.fill(block);
- }
-
- fn biome(&self, x: usize, y: usize, z: usize) -> BiomeId {
- assert!(
- x < 4 && y < self.section_count() * 4 && z < 4,
- "chunk biome offsets of ({x}, {y}, {z}) are out of bounds"
- );
-
- self.sections[y / 4].biomes.get(x + z * 4 + y % 4 * 4 * 4)
- }
-
- fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) -> BiomeId {
- assert!(
- x < 4 && y < self.section_count() * 4 && z < 4,
- "chunk biome offsets of ({x}, {y}, {z}) are out of bounds"
- );
-
- self.sections[y / 4]
- .biomes
- .set(x + z * 4 + y % 4 * 4 * 4, biome)
- }
-
- fn fill_biomes(&mut self, sect_y: usize, biome: BiomeId) {
- let Some(sect) = self.sections.get_mut(sect_y) else {
- panic!(
- "section index {sect_y} out of bounds for chunk with {} sections",
- self.section_count()
- )
- };
-
- sect.biomes.fill(biome);
- }
-
- fn optimize(&mut self) {
- for sect in self.sections.iter_mut() {
- sect.block_states.optimize();
- sect.biomes.optimize();
- }
- }
-}
-
-/// A chunk which is currently loaded in a world.
-pub struct LoadedChunk {
- /// Custom state.
- pub state: C::ChunkState,
- sections: Box<[ChunkSection]>,
- // TODO: block_entities: BTreeMap,
- cached_init_packet: Mutex>,
- cached_update_packets: Vec,
- /// If any of the biomes in this chunk were modified this tick.
- any_biomes_modified: bool,
- created_this_tick: bool,
- deleted: bool,
- /// For debugging purposes.
- #[cfg(debug_assertions)]
- uuid: uuid::Uuid,
-}
-
-impl Deref for LoadedChunk {
- type Target = C::ChunkState;
-
- fn deref(&self) -> &Self::Target {
- &self.state
- }
-}
-
-impl DerefMut for LoadedChunk {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.state
- }
-}
-
-/// A 16x16x16 meter volume of blocks, biomes, and light in a chunk.
-#[derive(Clone, Debug)]
-struct ChunkSection {
- block_states: PalettedContainer,
- /// Contains a set bit for every block that has been modified in this
- /// section this tick. Ignored in unloaded chunks.
- modified_blocks: [usize; SECTION_BLOCK_COUNT / USIZE_BITS],
- /// Number of non-air blocks in this section.
- non_air_count: u16,
- biomes: PalettedContainer,
-}
-
-// [T; 64] Doesn't implement Default so we can't derive :(
-impl Default for ChunkSection {
- fn default() -> Self {
- Self {
- block_states: Default::default(),
- modified_blocks: [0; SECTION_BLOCK_COUNT / USIZE_BITS],
- non_air_count: 0,
- biomes: Default::default(),
- }
- }
-}
-
-const SECTION_BLOCK_COUNT: usize = 4096;
-const USIZE_BITS: usize = usize::BITS as _;
-
-impl ChunkSection {
- fn mark_block_as_modified(&mut self, idx: usize) {
- self.modified_blocks[idx / USIZE_BITS] |= 1 << (idx % USIZE_BITS);
- }
-
- fn mark_all_blocks_as_modified(&mut self) {
- self.modified_blocks.fill(usize::MAX);
- }
-
- fn is_block_modified(&self, idx: usize) -> bool {
- self.modified_blocks[idx / USIZE_BITS] >> (idx % USIZE_BITS) & 1 == 1
- }
-}
-
-impl LoadedChunk {
- fn new(mut chunk: UnloadedChunk, dimension_section_count: usize, state: C::ChunkState) -> Self {
- chunk.resize(dimension_section_count);
-
- Self {
- state,
- sections: chunk.sections.into(),
- cached_init_packet: Mutex::new(vec![]),
- cached_update_packets: vec![],
- any_biomes_modified: false,
- created_this_tick: true,
- deleted: false,
- #[cfg(debug_assertions)]
- uuid: uuid::Uuid::from_u128(rand::random()),
- }
- }
-
- pub fn take(&mut self) -> UnloadedChunk {
- let unloaded = UnloadedChunk {
- sections: mem::take(&mut self.sections).into(),
- };
-
- self.created_this_tick = true;
-
- unloaded
- }
-
- /// Returns `true` if this chunk was created during the current tick.
- pub fn created_this_tick(&self) -> bool {
- self.created_this_tick
- }
-
- pub fn deleted(&self) -> bool {
- self.deleted
- }
-
- pub fn set_deleted(&mut self, deleted: bool) {
- self.deleted = deleted;
- }
-
- /// Queues the chunk data packet for this chunk with the given position.
- /// This will initialize the chunk for the client.
- pub(crate) fn write_chunk_data_packet(
- &self,
- mut writer: impl WritePacket,
- scratch: &mut Vec,
- pos: ChunkPos,
- chunks: &Chunks,
- ) -> anyhow::Result<()> {
- #[cfg(debug_assertions)]
- assert_eq!(
- chunks[pos].uuid, self.uuid,
- "chunks and/or position arguments are incorrect"
- );
-
- let bytes = self.get_chunk_data_packet(
- scratch,
- pos,
- chunks.biome_registry_len,
- &chunks.filler_sky_light_mask,
- &chunks.filler_sky_light_arrays,
- chunks.compression_threshold,
- );
-
- writer.write_bytes(&bytes)
- }
-
- /// Gets the bytes of the cached chunk data packet, initializing the cache
- /// if it is empty.
- fn get_chunk_data_packet(
- &self,
- scratch: &mut Vec,
- pos: ChunkPos,
- biome_registry_len: usize,
- filler_sky_light_mask: &[u64],
- filler_sky_light_arrays: &[LengthPrefixedArray],
- compression_threshold: Option,
- ) -> MutexGuard