From 47b158b67919d694391cafce4458a546a5006ddf Mon Sep 17 00:00:00 2001 From: tachibanayui <33594017+tachibanayui@users.noreply.github.com> Date: Tue, 13 Jun 2023 22:42:20 +0700 Subject: [PATCH 1/6] Implement world border --- crates/valence/examples/world_border.rs | 101 ++++++++ crates/valence/src/tests.rs | 1 + crates/valence/src/tests/world_border.rs | 136 ++++++++++ crates/valence_client/src/lib.rs | 2 + crates/valence_client/src/world_border.rs | 292 ++++++++++++++++++++++ 5 files changed, 532 insertions(+) create mode 100644 crates/valence/examples/world_border.rs create mode 100644 crates/valence/src/tests/world_border.rs create mode 100644 crates/valence_client/src/world_border.rs diff --git a/crates/valence/examples/world_border.rs b/crates/valence/examples/world_border.rs new file mode 100644 index 000000000..57e1e406c --- /dev/null +++ b/crates/valence/examples/world_border.rs @@ -0,0 +1,101 @@ +use valence::client::chat::ChatMessageEvent; +use valence::client::despawn_disconnected_clients; +use valence::client::world_border::{ + SetWorldBorderSizeEvent, WorldBorderBundle, WorldBorderCenter, WorldBorderDiameter, +}; +use valence::prelude::*; + +const SPAWN_Y: i32 = 64; + +fn main() { + tracing_subscriber::fmt().init(); + + App::new() + .add_plugins(DefaultPlugins) + .add_startup_system(setup) + .add_system(init_clients) + .add_system(despawn_disconnected_clients) + .add_system(border_controls) + .run(); +} + +fn setup( + mut commands: Commands, + server: Res, + biomes: Res, + dimensions: Res, +) { + let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + + 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([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + } + } + + commands + .spawn(instance) + .insert(WorldBorderBundle::new([0.0, 0.0], 10.0)); +} + +fn init_clients( + mut clients: Query<(&mut Client, &mut Location, &mut Position), Added>, + instances: Query>, +) { + for (mut client, mut loc, mut pos) in &mut clients { + loc.0 = instances.single(); + pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]); + client.send_message("Border control: "); + client.send_message("- Modify size: add "); + client.send_message("- Change center: center "); + } +} + +fn border_controls( + mut events: EventReader, + mut instances: Query<(Entity, &WorldBorderDiameter, &mut WorldBorderCenter), With>, + mut event_writer: EventWriter, +) { + for x in events.iter() { + let parts: Vec<&str> = x.message.split(' ').collect(); + match parts[0] { + "resize" => { + let Ok(value) = parts[1].parse::() else { + return; + }; + + let Ok(speed) = parts[2].parse::() else { + return; + }; + + let Ok((entity, diameter, _)) = instances.get_single_mut() else { + return; + }; + + event_writer.send(SetWorldBorderSizeEvent { + instance: entity, + new_diameter: diameter.diameter() + value, + speed, + }) + } + "center" => { + let Ok(x) = parts[1].parse::() else { + return; + }; + + let Ok(z) = parts[2].parse::() else { + return; + }; + + instances.single_mut().2 .0 = DVec2 { x, y: z }; + } + _ => (), + } + } +} diff --git a/crates/valence/src/tests.rs b/crates/valence/src/tests.rs index 86e5cbd0c..9f88b6bce 100644 --- a/crates/valence/src/tests.rs +++ b/crates/valence/src/tests.rs @@ -284,3 +284,4 @@ mod client; mod example; mod inventory; mod weather; +mod world_border; diff --git a/crates/valence/src/tests/world_border.rs b/crates/valence/src/tests/world_border.rs new file mode 100644 index 000000000..474fd6c40 --- /dev/null +++ b/crates/valence/src/tests/world_border.rs @@ -0,0 +1,136 @@ +use bevy_app::App; +use valence_client::world_border::{ + SetWorldBorderSizeEvent, WorldBorderBundle, WorldBorderCenter, WorldBorderPortalTpBoundary, + WorldBorderWarnBlocks, WorldBorderWarnTime, +}; +use valence_entity::Location; +use valence_instance::packet::{ + WorldBorderCenterChangedS2c, WorldBorderInitializeS2c, WorldBorderSizeChangedS2c, + WorldBorderWarningBlocksChangedS2c, WorldBorderWarningTimeChangedS2c, +}; +use valence_instance::Instance; +use valence_registry::{Entity, Mut}; + +use super::{scenario_single_client, MockClientHelper, create_mock_client}; + + +#[test] +fn test_intialize_on_join() { + let mut app = App::new(); + let (_, instance_ent) = prepare(&mut app); + + let (client, mut client_helper) = create_mock_client(); + let client_ent = app.world.spawn(client).id(); + + app.world.get_mut::(client_ent).unwrap().0 = instance_ent; + app.update(); + + client_helper.collect_sent().assert_count::(1); +} + +#[test] +fn test_resizing() { + let mut app = App::new(); + let (mut client_helper, instance_ent) = prepare(&mut app); + + app.world.send_event(SetWorldBorderSizeEvent { + new_diameter: 20.0, + speed: 0, + instance: instance_ent, + }); + + app.update(); + let frames = client_helper.collect_sent(); + frames.assert_count::(1); +} + +#[test] +fn test_center() { + let mut app = App::new(); + let (mut client_helper, instance_ent) = prepare(&mut app); + + let mut ins_mut = app.world.entity_mut(instance_ent); + let mut center: Mut = ins_mut + .get_mut() + .expect("Expect world border to be present!"); + center.0 = [10.0, 10.0].into(); + + app.update(); + let frames = client_helper.collect_sent(); + frames.assert_count::(1); +} + +#[test] +fn test_warn_time() { + let mut app = App::new(); + let (mut client_helper, instance_ent) = prepare(&mut app); + + let mut ins_mut = app.world.entity_mut(instance_ent); + let mut wt: Mut = ins_mut + .get_mut() + .expect("Expect world border to be present!"); + wt.0 = 100; + app.update(); + + let frames = client_helper.collect_sent(); + frames.assert_count::(1); +} + +#[test] +fn test_warn_blocks() { + let mut app = App::new(); + let (mut client_helper, instance_ent) = prepare(&mut app); + + let mut ins_mut = app.world.entity_mut(instance_ent); + let mut wb: Mut = ins_mut + .get_mut() + .expect("Expect world border to be present!"); + wb.0 = 100; + app.update(); + + let frames = client_helper.collect_sent(); + frames.assert_count::(1); +} + +#[test] +fn test_portal_tp_boundary() { + let mut app = App::new(); + let (mut client_helper, instance_ent) = prepare(&mut app); + + let mut ins_mut = app.world.entity_mut(instance_ent); + let mut tp: Mut = ins_mut + .get_mut() + .expect("Expect world border to be present!"); + tp.0 = 100; + app.update(); + + let frames = client_helper.collect_sent(); + frames.assert_count::(1); +} + +fn prepare(mut app: &mut App) -> (MockClientHelper, Entity) { + let (_, mut client_helper) = scenario_single_client(&mut app); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + // Get the instance entity. + let instance_ent = app + .world + .iter_entities() + .find(|e| e.contains::()) + .expect("could not find instance") + .id(); + + // Insert a the world border bundle to the instance. + app.world + .entity_mut(instance_ent) + .insert(WorldBorderBundle::new([0.0, 0.0], 10.0)); + for _ in 0..2 { + app.update(); + } + + client_helper.clear_sent(); + return (client_helper, instance_ent); +} diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index caebc3f18..45f6ce841 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -90,6 +90,7 @@ pub mod status; pub mod teleport; pub mod title; pub mod weather; +pub mod world_border; pub struct ClientPlugin; @@ -150,6 +151,7 @@ impl Plugin for ClientPlugin { action::build(app); teleport::build(app); weather::build(app); + world_border::build(app); chat::build(app); custom_payload::build(app); hand_swing::build(app); diff --git a/crates/valence_client/src/world_border.rs b/crates/valence_client/src/world_border.rs new file mode 100644 index 000000000..9377c0731 --- /dev/null +++ b/crates/valence_client/src/world_border.rs @@ -0,0 +1,292 @@ +// Things to update: +// - use system clock as reference time instead of server tick [done] +// - Update portal tp boundary wont send packet to update because there is no +// packet to specifically to update it. Maybe use InitializeWorldBorderS2c? + +use glam::DVec2; +use valence_core::protocol::var_long::VarLong; +use valence_entity::Location; +use valence_instance::packet::*; +use valence_registry::{Component, Query}; + +use crate::*; + +// https://minecraft.fandom.com/wiki/World_border +pub const DEFAULT_PORTAL_LIMIT: i32 = 29999984; +pub const DEFAULT_DIAMETER: f64 = (DEFAULT_PORTAL_LIMIT * 2) as f64; +pub const DEFAULT_WARN_TIME: i32 = 15; +pub const DEFAULT_WARN_BLOCKS: i32 = 5; + +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct UpdateWorldBorderPerInstanceSet; + +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub struct UpdateWorldBorderPerClientSet; + +pub(crate) fn build(app: &mut App) { + app.configure_set( + UpdateWorldBorderPerInstanceSet + .in_base_set(CoreSet::PostUpdate) + .before(WriteUpdatePacketsToInstancesSet), + ) + .configure_set( + UpdateWorldBorderPerClientSet + .in_base_set(CoreSet::PostUpdate) + .before(FlushPacketsSet), + ) + .add_event::() + .add_systems( + ( + handle_wb_size_change.before(handle_diameter_change), + handle_diameter_change, + handle_lerp_transition, + handle_center_change, + handle_warn_time_change, + handle_warn_blocks_change, + handle_portal_teleport_bounary_change, + ) + .in_set(UpdateWorldBorderPerInstanceSet), + ) + .add_system(handle_border_for_player.in_set(UpdateWorldBorderPerClientSet)); +} + +#[derive(Bundle)] +pub struct WorldBorderBundle { + pub center: WorldBorderCenter, + pub diameter: WorldBorderDiameter, + pub portal_teleport_boundary: WorldBorderPortalTpBoundary, + pub warning_time: WorldBorderWarnTime, + pub warning_blocks: WorldBorderWarnBlocks, + pub moving: MovingWorldBorder, +} + +impl WorldBorderBundle { + pub fn new(center: impl Into, diameter: f64) -> Self { + Self { + center: WorldBorderCenter(center.into()), + diameter: WorldBorderDiameter(diameter), + portal_teleport_boundary: WorldBorderPortalTpBoundary(DEFAULT_PORTAL_LIMIT), + warning_time: WorldBorderWarnTime(DEFAULT_WARN_TIME), + warning_blocks: WorldBorderWarnBlocks(DEFAULT_WARN_BLOCKS), + moving: MovingWorldBorder { + old_diameter: diameter, + new_diameter: diameter, + speed: 0, + timestamp: Instant::now(), + }, + } + } +} + +impl Default for WorldBorderBundle { + fn default() -> Self { + Self::new([0.0, 0.0], DEFAULT_DIAMETER) + } +} + +#[derive(Component)] +pub struct WorldBorderCenter(pub DVec2); + +#[derive(Component)] +pub struct WorldBorderWarnTime(pub i32); + +#[derive(Component)] +pub struct WorldBorderWarnBlocks(pub i32); + +#[derive(Component)] +pub struct WorldBorderPortalTpBoundary(pub i32); + +#[derive(Component)] +pub struct WorldBorderDiameter(f64); + +impl WorldBorderDiameter { + pub fn diameter(&self) -> f64 { + self.0 + } +} + +#[derive(Component)] +pub struct MovingWorldBorder { + pub old_diameter: f64, + pub new_diameter: f64, + pub speed: i64, + pub timestamp: Instant, +} + +impl MovingWorldBorder { + pub fn current_diameter(&self) -> f64 { + let t = self.current_speed() as f64 / self.speed as f64; + lerp(self.new_diameter, self.old_diameter, t) + } + + pub fn current_speed(&self) -> i64 { + let speed = self.speed - self.timestamp.elapsed().as_millis() as i64; + speed.max(0) + } +} + +pub struct SetWorldBorderSizeEvent { + pub instance: Entity, + pub new_diameter: f64, + pub speed: i64, +} + +fn handle_wb_size_change( + mut events: EventReader, + mut instances: Query<(Entity, &WorldBorderDiameter, Option<&mut MovingWorldBorder>)>, + mut commands: Commands, +) { + for SetWorldBorderSizeEvent { + instance, + new_diameter, + speed, + } in events.iter() + { + let Ok((entity, diameter, mwb_opt)) = instances.get_mut(instance.clone()) else { + continue; + }; + + if let Some(mut mvb) = mwb_opt { + mvb.new_diameter = *new_diameter; + mvb.old_diameter = diameter.diameter(); + mvb.speed = *speed; + mvb.timestamp = Instant::now(); + } else { + // This might be delayed by 1 tick + commands.entity(entity).insert(MovingWorldBorder { + new_diameter: *new_diameter, + old_diameter: diameter.diameter(), + speed: *speed, + timestamp: Instant::now(), + }); + } + } +} + +fn handle_border_for_player( + mut clients: Query<(&mut Client, &Location), Changed>, + wbs: Query< + ( + &WorldBorderCenter, + &WorldBorderWarnTime, + &WorldBorderWarnBlocks, + &WorldBorderDiameter, + &WorldBorderPortalTpBoundary, + Option<&MovingWorldBorder>, + ), + With, + >, +) { + for (mut client, location) in clients.iter_mut() { + if let Ok((c, wt, wb, diameter, ptb, wbl)) = wbs.get(location.0) { + let (new_diameter, speed) = if let Some(lerping) = wbl { + (lerping.new_diameter, lerping.current_speed()) + } else { + (diameter.0, 0) + }; + + client.write_packet(&WorldBorderInitializeS2c { + x: c.0.x, + z: c.0.y, + old_diameter: diameter.0, + new_diameter, + portal_teleport_boundary: VarInt(ptb.0), + speed: VarLong(speed), + warning_blocks: VarInt(wb.0), + warning_time: VarInt(wt.0), + }); + } + } +} + +fn handle_diameter_change( + mut wbs: Query<(&mut Instance, &MovingWorldBorder), Changed>, +) { + for (mut ins, lerping) in wbs.iter_mut() { + if lerping.speed == 0 { + ins.write_packet(&WorldBorderSizeChangedS2c { + diameter: lerping.new_diameter, + }) + } else { + ins.write_packet(&WorldBorderInterpolateSizeS2c { + old_diameter: lerping.current_diameter(), + new_diameter: lerping.new_diameter, + speed: VarLong(lerping.current_speed()), + }); + } + } +} + +fn handle_lerp_transition(mut wbs: Query<(&mut WorldBorderDiameter, &MovingWorldBorder)>) { + for (mut diameter, moving_wb) in wbs.iter_mut() { + if diameter.0 != moving_wb.new_diameter { + diameter.0 = moving_wb.current_diameter(); + } + } +} + +fn handle_center_change( + mut wbs: Query<(&mut Instance, &WorldBorderCenter), Changed>, +) { + for (mut ins, center) in wbs.iter_mut() { + ins.write_packet(&WorldBorderCenterChangedS2c { + x_pos: center.0.x, + z_pos: center.0.y, + }) + } +} + +fn handle_warn_time_change( + mut wb_query: Query<(&mut Instance, &WorldBorderWarnTime), Changed>, +) { + for (mut ins, wt) in wb_query.iter_mut() { + ins.write_packet(&WorldBorderWarningTimeChangedS2c { + warning_time: VarInt(wt.0), + }) + } +} + +fn handle_warn_blocks_change( + mut wb_query: Query<(&mut Instance, &WorldBorderWarnBlocks), Changed>, +) { + for (mut ins, wb) in wb_query.iter_mut() { + ins.write_packet(&WorldBorderWarningBlocksChangedS2c { + warning_blocks: VarInt(wb.0), + }) + } +} + +fn handle_portal_teleport_bounary_change( + mut wbs: Query<( + &mut Instance, + &WorldBorderCenter, + &WorldBorderWarnTime, + &WorldBorderWarnBlocks, + &WorldBorderDiameter, + &WorldBorderPortalTpBoundary, + Option<&MovingWorldBorder>, + )>, +) { + for (mut ins, c, wt, wb, diameter, ptb, wbl) in wbs.iter_mut() { + let (new_diameter, speed) = if let Some(lerping) = wbl { + (lerping.new_diameter, lerping.current_speed()) + } else { + (diameter.0, 0) + }; + + ins.write_packet(&WorldBorderInitializeS2c { + x: c.0.x, + z: c.0.y, + old_diameter: diameter.0, + new_diameter, + portal_teleport_boundary: VarInt(ptb.0), + speed: VarLong(speed), + warning_blocks: VarInt(wb.0), + warning_time: VarInt(wt.0), + }); + } +} + +fn lerp(start: f64, end: f64, t: f64) -> f64 { + start + (end - start) * t +} From 800c1fd3658b60526e5580f999281f988da70168 Mon Sep 17 00:00:00 2001 From: tachibanayui <33594017+tachibanayui@users.noreply.github.com> Date: Wed, 14 Jun 2023 00:08:48 +0700 Subject: [PATCH 2/6] Add documentation for world border --- crates/valence_client/src/world_border.rs | 54 ++++++++++++++++++++--- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/crates/valence_client/src/world_border.rs b/crates/valence_client/src/world_border.rs index 9377c0731..131e58bef 100644 --- a/crates/valence_client/src/world_border.rs +++ b/crates/valence_client/src/world_border.rs @@ -1,7 +1,44 @@ -// Things to update: -// - use system clock as reference time instead of server tick [done] -// - Update portal tp boundary wont send packet to update because there is no -// packet to specifically to update it. Maybe use InitializeWorldBorderS2c? +//! # World border +//! This module contains Components and Systems needed to handle world border. +//! +//! The world border is the current edge of a Minecraft dimension. It appears as a series of animated, diagonal, narrow stripes. +//! For more information, refer to the [wiki](https://minecraft.fandom.com/wiki/World_border) +//! +//! ## Enable world border per instance +//! By default, world border is not enabled. It can be enabled by inserting the [`WorldBorderBundle`] bundle into a [`Instance`]. +//! Use [`WorldBorderBundle::default()`] to use Minecraft Vanilla border default +//! ``` +//! commands +//! .entity(instance_entity) +//! .insert(WorldBorderBundle::new([0.0, 0.0], 10.0)); +//! ``` +//! +//! +//! ## Modify world border diameter +//! World border diameter can be changed using [`SetWorldBorderSizeEvent`]. +//! Setting speed to 0 will move the border to `new_diameter` immediately, otherwise +//! it will interpolate to `new_diameter` over `speed` milliseconds. +//! ``` +//! fn change_diameter(event_writer: EventWriter) { +//! event_writer.send(SetWorldBorderSizeEvent { +//! instance: entity, +//! new_diameter: diameter, +//! speed, +//! }) +//! } +//! ``` +//! +//! You can also modify the [`MovingWorldBorder`] if you want more control. But it is not recommended. +//! +//! ## Querying world border diameter +//! World border diameter can be read by querying [`WorldBorderDiameter::diameter()`]. +//! Note: If you want to modify the diameter size, do not modify the value directly! Use [`SetWorldBorderSizeEvent`] instead. +//! +//! ## Access other world border properties. +//! Access to the rest of the world border properties is fairly straight forward by querying their respective component. +//! [`WorldBorderBundle`] contains references for all properties of world border and their respective component +//! +#![allow(clippy::type_complexity)] use glam::DVec2; use valence_core::protocol::var_long::VarLong; @@ -50,6 +87,8 @@ pub(crate) fn build(app: &mut App) { .add_system(handle_border_for_player.in_set(UpdateWorldBorderPerClientSet)); } +/// A bundle contains necessary component to enable world border. +/// This struct implements [`Default`] trait that returns a bundle using Minecraft Vanilla defaults. #[derive(Bundle)] pub struct WorldBorderBundle { pub center: WorldBorderCenter, @@ -61,6 +100,7 @@ pub struct WorldBorderBundle { } impl WorldBorderBundle { + /// Create a new world border with specified center and diameter pub fn new(center: impl Into, diameter: f64) -> Self { Self { center: WorldBorderCenter(center.into()), @@ -125,9 +165,13 @@ impl MovingWorldBorder { } } +/// An event for controlling world border diameter. Please refer to the module documentation for example usage. pub struct SetWorldBorderSizeEvent { + /// The instance to change border size. Note that this instance must contain the [`WorldBorderBundle`] bundle pub instance: Entity, + /// The new diameter of the world border pub new_diameter: f64, + /// How long the border takes to reach it new_diameter in millisecond. Set to 0 to move immediately. pub speed: i64, } @@ -142,7 +186,7 @@ fn handle_wb_size_change( speed, } in events.iter() { - let Ok((entity, diameter, mwb_opt)) = instances.get_mut(instance.clone()) else { + let Ok((entity, diameter, mwb_opt)) = instances.get_mut(*instance) else { continue; }; From ece940415ed66914dd8dba9ab9e95352c4368d66 Mon Sep 17 00:00:00 2001 From: tachibanayui <33594017+tachibanayui@users.noreply.github.com> Date: Wed, 14 Jun 2023 02:26:51 +0700 Subject: [PATCH 3/6] rename `speed` to `duration` more documentation --- crates/valence/examples/world_border.rs | 4 +- crates/valence/src/tests/world_border.rs | 11 ++- crates/valence_client/src/world_border.rs | 115 ++++++++++++++-------- 3 files changed, 86 insertions(+), 44 deletions(-) diff --git a/crates/valence/examples/world_border.rs b/crates/valence/examples/world_border.rs index 57e1e406c..113581761 100644 --- a/crates/valence/examples/world_border.rs +++ b/crates/valence/examples/world_border.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use valence::client::chat::ChatMessageEvent; use valence::client::despawn_disconnected_clients; use valence::client::world_border::{ @@ -81,7 +83,7 @@ fn border_controls( event_writer.send(SetWorldBorderSizeEvent { instance: entity, new_diameter: diameter.diameter() + value, - speed, + duration: Duration::from_millis(speed as u64), }) } "center" => { diff --git a/crates/valence/src/tests/world_border.rs b/crates/valence/src/tests/world_border.rs index 474fd6c40..21cf294f3 100644 --- a/crates/valence/src/tests/world_border.rs +++ b/crates/valence/src/tests/world_border.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use bevy_app::App; use valence_client::world_border::{ SetWorldBorderSizeEvent, WorldBorderBundle, WorldBorderCenter, WorldBorderPortalTpBoundary, @@ -11,8 +13,7 @@ use valence_instance::packet::{ use valence_instance::Instance; use valence_registry::{Entity, Mut}; -use super::{scenario_single_client, MockClientHelper, create_mock_client}; - +use super::{create_mock_client, scenario_single_client, MockClientHelper}; #[test] fn test_intialize_on_join() { @@ -25,7 +26,9 @@ fn test_intialize_on_join() { app.world.get_mut::(client_ent).unwrap().0 = instance_ent; app.update(); - client_helper.collect_sent().assert_count::(1); + client_helper + .collect_sent() + .assert_count::(1); } #[test] @@ -35,7 +38,7 @@ fn test_resizing() { app.world.send_event(SetWorldBorderSizeEvent { new_diameter: 20.0, - speed: 0, + duration: Duration::ZERO, instance: instance_ent, }); diff --git a/crates/valence_client/src/world_border.rs b/crates/valence_client/src/world_border.rs index 131e58bef..bf518e0a4 100644 --- a/crates/valence_client/src/world_border.rs +++ b/crates/valence_client/src/world_border.rs @@ -1,45 +1,55 @@ -//! # World border +//! # World border //! This module contains Components and Systems needed to handle world border. -//! -//! The world border is the current edge of a Minecraft dimension. It appears as a series of animated, diagonal, narrow stripes. -//! For more information, refer to the [wiki](https://minecraft.fandom.com/wiki/World_border) -//! +//! +//! The world border is the current edge of a Minecraft dimension. It appears as +//! a series of animated, diagonal, narrow stripes. For more information, refer to the [wiki](https://minecraft.fandom.com/wiki/World_border) +//! //! ## Enable world border per instance -//! By default, world border is not enabled. It can be enabled by inserting the [`WorldBorderBundle`] bundle into a [`Instance`]. +//! By default, world border is not enabled. It can be enabled by inserting the +//! [`WorldBorderBundle`] bundle into a [`Instance`]. //! Use [`WorldBorderBundle::default()`] to use Minecraft Vanilla border default //! ``` //! commands //! .entity(instance_entity) //! .insert(WorldBorderBundle::new([0.0, 0.0], 10.0)); //! ``` -//! -//! +//! +//! //! ## Modify world border diameter -//! World border diameter can be changed using [`SetWorldBorderSizeEvent`]. -//! Setting speed to 0 will move the border to `new_diameter` immediately, otherwise -//! it will interpolate to `new_diameter` over `speed` milliseconds. +//! World border diameter can be changed using [`SetWorldBorderSizeEvent`]. +//! Setting duration to 0 will move the border to `new_diameter` immediately, +//! otherwise it will interpolate to `new_diameter` over `duration` time. //! ``` -//! fn change_diameter(event_writer: EventWriter) { +//! fn change_diameter( +//! event_writer: EventWriter, +//! diameter: f64, +//! duration: Duration, +//! ) { //! event_writer.send(SetWorldBorderSizeEvent { //! instance: entity, //! new_diameter: diameter, -//! speed, +//! duration, //! }) //! } //! ``` -//! -//! You can also modify the [`MovingWorldBorder`] if you want more control. But it is not recommended. -//! +//! +//! You can also modify the [`MovingWorldBorder`] if you want more control. But +//! it is not recommended. +//! //! ## Querying world border diameter -//! World border diameter can be read by querying [`WorldBorderDiameter::diameter()`]. -//! Note: If you want to modify the diameter size, do not modify the value directly! Use [`SetWorldBorderSizeEvent`] instead. -//! +//! World border diameter can be read by querying +//! [`WorldBorderDiameter::diameter()`]. Note: If you want to modify the +//! diameter size, do not modify the value directly! Use +//! [`SetWorldBorderSizeEvent`] instead. +//! //! ## Access other world border properties. -//! Access to the rest of the world border properties is fairly straight forward by querying their respective component. -//! [`WorldBorderBundle`] contains references for all properties of world border and their respective component -//! +//! Access to the rest of the world border properties is fairly straight forward +//! by querying their respective component. [`WorldBorderBundle`] contains +//! references for all properties of world border and their respective component #![allow(clippy::type_complexity)] +use std::time::Duration; + use glam::DVec2; use valence_core::protocol::var_long::VarLong; use valence_entity::Location; @@ -88,7 +98,8 @@ pub(crate) fn build(app: &mut App) { } /// A bundle contains necessary component to enable world border. -/// This struct implements [`Default`] trait that returns a bundle using Minecraft Vanilla defaults. +/// This struct implements [`Default`] trait that returns a bundle using +/// Minecraft Vanilla defaults. #[derive(Bundle)] pub struct WorldBorderBundle { pub center: WorldBorderCenter, @@ -111,7 +122,7 @@ impl WorldBorderBundle { moving: MovingWorldBorder { old_diameter: diameter, new_diameter: diameter, - speed: 0, + duration: 0, timestamp: Instant::now(), }, } @@ -136,6 +147,10 @@ pub struct WorldBorderWarnBlocks(pub i32); #[derive(Component)] pub struct WorldBorderPortalTpBoundary(pub i32); +/// World border diameter can be read by querying +/// [`WorldBorderDiameter::diameter()`]. If you want to modify the diameter +/// size, do not modify the value directly! Use [`SetWorldBorderSizeEvent`] +/// instead. #[derive(Component)] pub struct WorldBorderDiameter(f64); @@ -145,34 +160,56 @@ impl WorldBorderDiameter { } } +/// This component represents the `Set Border Lerp Size` packet with timestamp. +/// It is used for actually lerping the world border diamater. +/// If you need to set the diameter, it is much better to use the +/// [`SetWorldBorderSizeEvent`] event #[derive(Component)] pub struct MovingWorldBorder { pub old_diameter: f64, pub new_diameter: f64, - pub speed: i64, + /// equivalent to `speed` on wiki.vg + pub duration: i64, pub timestamp: Instant, } impl MovingWorldBorder { pub fn current_diameter(&self) -> f64 { - let t = self.current_speed() as f64 / self.speed as f64; + let t = self.current_duration() as f64 / self.duration as f64; lerp(self.new_diameter, self.old_diameter, t) } - pub fn current_speed(&self) -> i64 { - let speed = self.speed - self.timestamp.elapsed().as_millis() as i64; + pub fn current_duration(&self) -> i64 { + let speed = self.duration - self.timestamp.elapsed().as_millis() as i64; speed.max(0) } } -/// An event for controlling world border diameter. Please refer to the module documentation for example usage. +/// An event for controlling world border diameter. +/// Setting duration to 0 will move the border to `new_diameter` immediately, +/// otherwise it will interpolate to `new_diameter` over `duration` time. +/// ``` +/// fn change_diameter( +/// event_writer: EventWriter, +/// diameter: f64, +/// duration: Duration, +/// ) { +/// event_writer.send(SetWorldBorderSizeEvent { +/// instance: entity, +/// new_diameter: diameter, +/// duration, +/// }) +/// } +/// ``` pub struct SetWorldBorderSizeEvent { - /// The instance to change border size. Note that this instance must contain the [`WorldBorderBundle`] bundle + /// The instance to change border size. Note that this instance must contain + /// the [`WorldBorderBundle`] bundle pub instance: Entity, /// The new diameter of the world border pub new_diameter: f64, - /// How long the border takes to reach it new_diameter in millisecond. Set to 0 to move immediately. - pub speed: i64, + /// How long the border takes to reach it new_diameter in millisecond. Set + /// to 0 to move immediately. + pub duration: Duration, } fn handle_wb_size_change( @@ -183,7 +220,7 @@ fn handle_wb_size_change( for SetWorldBorderSizeEvent { instance, new_diameter, - speed, + duration, } in events.iter() { let Ok((entity, diameter, mwb_opt)) = instances.get_mut(*instance) else { @@ -193,14 +230,14 @@ fn handle_wb_size_change( if let Some(mut mvb) = mwb_opt { mvb.new_diameter = *new_diameter; mvb.old_diameter = diameter.diameter(); - mvb.speed = *speed; + mvb.duration = duration.as_millis() as i64; mvb.timestamp = Instant::now(); } else { // This might be delayed by 1 tick commands.entity(entity).insert(MovingWorldBorder { new_diameter: *new_diameter, old_diameter: diameter.diameter(), - speed: *speed, + duration: duration.as_millis() as i64, timestamp: Instant::now(), }); } @@ -224,7 +261,7 @@ fn handle_border_for_player( for (mut client, location) in clients.iter_mut() { if let Ok((c, wt, wb, diameter, ptb, wbl)) = wbs.get(location.0) { let (new_diameter, speed) = if let Some(lerping) = wbl { - (lerping.new_diameter, lerping.current_speed()) + (lerping.new_diameter, lerping.current_duration()) } else { (diameter.0, 0) }; @@ -247,7 +284,7 @@ fn handle_diameter_change( mut wbs: Query<(&mut Instance, &MovingWorldBorder), Changed>, ) { for (mut ins, lerping) in wbs.iter_mut() { - if lerping.speed == 0 { + if lerping.duration == 0 { ins.write_packet(&WorldBorderSizeChangedS2c { diameter: lerping.new_diameter, }) @@ -255,7 +292,7 @@ fn handle_diameter_change( ins.write_packet(&WorldBorderInterpolateSizeS2c { old_diameter: lerping.current_diameter(), new_diameter: lerping.new_diameter, - speed: VarLong(lerping.current_speed()), + speed: VarLong(lerping.current_duration()), }); } } @@ -313,7 +350,7 @@ fn handle_portal_teleport_bounary_change( ) { for (mut ins, c, wt, wb, diameter, ptb, wbl) in wbs.iter_mut() { let (new_diameter, speed) = if let Some(lerping) = wbl { - (lerping.new_diameter, lerping.current_speed()) + (lerping.new_diameter, lerping.current_duration()) } else { (diameter.0, 0) }; From 890331812c1a74bce0bc2ba79e3f71700d15b456 Mon Sep 17 00:00:00 2001 From: tachibanayui <33594017+tachibanayui@users.noreply.github.com> Date: Wed, 14 Jun 2023 02:43:51 +0700 Subject: [PATCH 4/6] fix clippy errors --- crates/valence/src/tests/world_border.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/valence/src/tests/world_border.rs b/crates/valence/src/tests/world_border.rs index 21cf294f3..c69309169 100644 --- a/crates/valence/src/tests/world_border.rs +++ b/crates/valence/src/tests/world_border.rs @@ -111,8 +111,8 @@ fn test_portal_tp_boundary() { frames.assert_count::(1); } -fn prepare(mut app: &mut App) -> (MockClientHelper, Entity) { - let (_, mut client_helper) = scenario_single_client(&mut app); +fn prepare(app: &mut App) -> (MockClientHelper, Entity) { + let (_, mut client_helper) = scenario_single_client(app); // Process a tick to get past the "on join" logic. app.update(); @@ -135,5 +135,5 @@ fn prepare(mut app: &mut App) -> (MockClientHelper, Entity) { } client_helper.clear_sent(); - return (client_helper, instance_ent); + (client_helper, instance_ent) } From 10bf36a2d75f11c2ab9077b8ba8b4e54066411ae Mon Sep 17 00:00:00 2001 From: tachibanayui <33594017+tachibanayui@users.noreply.github.com> Date: Thu, 15 Jun 2023 01:35:29 +0700 Subject: [PATCH 5/6] Move world border to a new crate Change documentation as pointed out by rj Updated examples Fix divide by zero when setting duration of SetWorldBorderSizeEvent to 0 --- Cargo.toml | 2 + crates/valence/Cargo.toml | 5 +- crates/valence/examples/world_border.rs | 82 +++++- crates/valence/src/lib.rs | 7 + crates/valence/src/tests/world_border.rs | 10 +- crates/valence_client/src/lib.rs | 2 - crates/valence_instance/src/packet.rs | 46 ---- crates/valence_world_border/Cargo.toml | 14 + .../src/lib.rs} | 117 ++++---- crates/valence_world_border/src/packet.rs | 49 ++++ crates/valence_world_border/src/tests.rs | 258 ++++++++++++++++++ 11 files changed, 468 insertions(+), 124 deletions(-) create mode 100644 crates/valence_world_border/Cargo.toml rename crates/{valence_client/src/world_border.rs => valence_world_border/src/lib.rs} (81%) create mode 100644 crates/valence_world_border/src/packet.rs create mode 100644 crates/valence_world_border/src/tests.rs diff --git a/Cargo.toml b/Cargo.toml index 424d03b0e..ba01f49f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,7 +89,9 @@ valence_nbt = { path = "crates/valence_nbt", features = ["uuid"] } valence_network.path = "crates/valence_network" valence_player_list.path = "crates/valence_player_list" valence_registry.path = "crates/valence_registry" +valence_world_border.path = "crates/valence_world_border" valence.path = "crates/valence" + zip = "0.6.3" [profile.dev.package."*"] diff --git a/crates/valence/Cargo.toml b/crates/valence/Cargo.toml index a69c885bc..b7540897f 100644 --- a/crates/valence/Cargo.toml +++ b/crates/valence/Cargo.toml @@ -11,12 +11,13 @@ keywords = ["minecraft", "gamedev", "server", "ecs"] categories = ["game-engines"] [features] -default = ["network", "player_list", "inventory", "anvil", "advancement"] +default = ["network", "player_list", "inventory", "anvil", "advancement", "world_border"] network = ["dep:valence_network"] player_list = ["dep:valence_player_list"] inventory = ["dep:valence_inventory"] anvil = ["dep:valence_anvil"] advancement = ["dep:valence_advancement"] +world_border = ["dep:valence_world_border"] [dependencies] bevy_app.workspace = true @@ -37,6 +38,8 @@ valence_player_list = { workspace = true, optional = true } valence_inventory = { workspace = true, optional = true } valence_anvil = { workspace = true, optional = true } valence_advancement = { workspace = true, optional = true } +valence_world_border = { workspace = true, optional = true } + [dev-dependencies] anyhow.workspace = true diff --git a/crates/valence/examples/world_border.rs b/crates/valence/examples/world_border.rs index 113581761..52f71b8ca 100644 --- a/crates/valence/examples/world_border.rs +++ b/crates/valence/examples/world_border.rs @@ -1,11 +1,11 @@ use std::time::Duration; +use bevy_app::App; use valence::client::chat::ChatMessageEvent; use valence::client::despawn_disconnected_clients; -use valence::client::world_border::{ - SetWorldBorderSizeEvent, WorldBorderBundle, WorldBorderCenter, WorldBorderDiameter, -}; +use valence::inventory::HeldItem; use valence::prelude::*; +use valence::world_border::*; const SPAWN_Y: i32 = 64; @@ -17,6 +17,8 @@ fn main() { .add_startup_system(setup) .add_system(init_clients) .add_system(despawn_disconnected_clients) + .add_system(border_center_avg) + .add_system(border_expand) .add_system(border_controls) .run(); } @@ -37,28 +39,84 @@ fn setup( for z in -25..25 { for x in -25..25 { - instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + instance.set_block([x, SPAWN_Y, z], BlockState::MOSSY_COBBLESTONE); } } commands .spawn(instance) - .insert(WorldBorderBundle::new([0.0, 0.0], 10.0)); + .insert(WorldBorderBundle::new([0.0, 0.0], 1.0)); } fn init_clients( - mut clients: Query<(&mut Client, &mut Location, &mut Position), Added>, + mut clients: Query< + ( + &mut Client, + &mut Location, + &mut Position, + &mut Inventory, + &HeldItem, + ), + Added, + >, instances: Query>, ) { - for (mut client, mut loc, mut pos) in &mut clients { + for (mut client, mut loc, mut pos, mut inv, main_slot) in &mut clients { loc.0 = instances.single(); pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]); - client.send_message("Border control: "); - client.send_message("- Modify size: add "); - client.send_message("- Change center: center "); + let pickaxe = Some(ItemStack::new(ItemKind::WoodenPickaxe, 1, None)); + inv.set_slot(main_slot.slot(), pickaxe); + client.send_message("Break block to increase border size!"); } } +fn border_center_avg( + clients: Query<(&Location, &Position)>, + mut instances: Query<(Entity, &mut WorldBorderCenter), With>, +) { + for (entity, mut center) in instances.iter_mut() { + let new_center = { + let (count, x, z) = clients + .iter() + .filter(|(loc, _)| loc.0 == entity) + .fold((0, 0.0, 0.0), |(count, x, z), (_, pos)| { + (count + 1, x + pos.0.x, z + pos.0.z) + }); + + DVec2 { + x: x / count.max(1) as f64, + y: z / count.max(1) as f64, + } + }; + + center.0 = new_center; + } +} + +fn border_expand( + mut events: EventReader, + clients: Query<&Location, With>, + wbs: Query<&WorldBorderDiameter, With>, + mut event_writer: EventWriter, +) { + for digging in events.iter().filter(|d| d.state == DiggingState::Stop) { + let Ok(loc) = clients.get(digging.client) else { + continue; + }; + + let Ok(size) = wbs.get(loc.0) else { + continue; + }; + + event_writer.send(SetWorldBorderSizeEvent { + instance: loc.0, + new_diameter: size.get() + 1.0, + duration: Duration::from_secs(1), + }); + } +} + +// Not needed for this demo, but useful for debugging fn border_controls( mut events: EventReader, mut instances: Query<(Entity, &WorldBorderDiameter, &mut WorldBorderCenter), With>, @@ -67,7 +125,7 @@ fn border_controls( for x in events.iter() { let parts: Vec<&str> = x.message.split(' ').collect(); match parts[0] { - "resize" => { + "add" => { let Ok(value) = parts[1].parse::() else { return; }; @@ -82,7 +140,7 @@ fn border_controls( event_writer.send(SetWorldBorderSizeEvent { instance: entity, - new_diameter: diameter.diameter() + value, + new_diameter: diameter.get() + value, duration: Duration::from_millis(speed as u64), }) } diff --git a/crates/valence/src/lib.rs b/crates/valence/src/lib.rs index a7ba2c142..a33b35e17 100644 --- a/crates/valence/src/lib.rs +++ b/crates/valence/src/lib.rs @@ -37,6 +37,8 @@ pub use valence_inventory as inventory; pub use valence_network as network; #[cfg(feature = "player_list")] pub use valence_player_list as player_list; +#[cfg(feature = "world_border")] +pub use valence_world_border as world_border; pub use { bevy_app as app, bevy_ecs as ecs, glam, valence_biome as biome, valence_block as block, valence_client as client, valence_dimension as dimension, valence_entity as entity, @@ -162,6 +164,11 @@ impl PluginGroup for DefaultPlugins { .add(valence_advancement::bevy_hierarchy::HierarchyPlugin); } + #[cfg(feature = "world_border")] + { + group = group.add(valence_world_border::WorldBorderPlugin); + } + group } } diff --git a/crates/valence/src/tests/world_border.rs b/crates/valence/src/tests/world_border.rs index c69309169..41d90bc88 100644 --- a/crates/valence/src/tests/world_border.rs +++ b/crates/valence/src/tests/world_border.rs @@ -1,17 +1,11 @@ use std::time::Duration; use bevy_app::App; -use valence_client::world_border::{ - SetWorldBorderSizeEvent, WorldBorderBundle, WorldBorderCenter, WorldBorderPortalTpBoundary, - WorldBorderWarnBlocks, WorldBorderWarnTime, -}; use valence_entity::Location; -use valence_instance::packet::{ - WorldBorderCenterChangedS2c, WorldBorderInitializeS2c, WorldBorderSizeChangedS2c, - WorldBorderWarningBlocksChangedS2c, WorldBorderWarningTimeChangedS2c, -}; use valence_instance::Instance; use valence_registry::{Entity, Mut}; +use valence_world_border::packet::*; +use valence_world_border::*; use super::{create_mock_client, scenario_single_client, MockClientHelper}; diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index 45f6ce841..caebc3f18 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -90,7 +90,6 @@ pub mod status; pub mod teleport; pub mod title; pub mod weather; -pub mod world_border; pub struct ClientPlugin; @@ -151,7 +150,6 @@ impl Plugin for ClientPlugin { action::build(app); teleport::build(app); weather::build(app); - world_border::build(app); chat::build(app); custom_payload::build(app); hand_swing::build(app); diff --git a/crates/valence_instance/src/packet.rs b/crates/valence_instance/src/packet.rs index d9b8bef3a..f69e0ec3e 100644 --- a/crates/valence_instance/src/packet.rs +++ b/crates/valence_instance/src/packet.rs @@ -9,52 +9,6 @@ use valence_core::protocol::var_long::VarLong; use valence_core::protocol::{packet_id, Decode, Encode, Packet}; use valence_nbt::Compound; -#[derive(Clone, Debug, Encode, Decode, Packet)] -#[packet(id = packet_id::WORLD_BORDER_CENTER_CHANGED_S2C)] -pub struct WorldBorderCenterChangedS2c { - pub x_pos: f64, - pub z_pos: f64, -} - -#[derive(Copy, Clone, Debug, Encode, Decode, Packet)] -#[packet(id = packet_id::WORLD_BORDER_INITIALIZE_S2C)] -pub struct WorldBorderInitializeS2c { - pub x: f64, - pub z: f64, - pub old_diameter: f64, - pub new_diameter: f64, - pub speed: VarLong, - pub portal_teleport_boundary: VarInt, - pub warning_blocks: VarInt, - pub warning_time: VarInt, -} - -#[derive(Clone, Debug, Encode, Decode, Packet)] -#[packet(id = packet_id::WORLD_BORDER_INTERPOLATE_SIZE_S2C)] -pub struct WorldBorderInterpolateSizeS2c { - pub old_diameter: f64, - pub new_diameter: f64, - pub speed: VarLong, -} - -#[derive(Clone, Debug, Encode, Decode, Packet)] -#[packet(id = packet_id::WORLD_BORDER_SIZE_CHANGED_S2C)] -pub struct WorldBorderSizeChangedS2c { - pub diameter: f64, -} - -#[derive(Clone, Debug, Encode, Decode, Packet)] -#[packet(id = packet_id::WORLD_BORDER_WARNING_BLOCKS_CHANGED_S2C)] -pub struct WorldBorderWarningBlocksChangedS2c { - pub warning_blocks: VarInt, -} - -#[derive(Clone, Debug, Encode, Decode, Packet)] -#[packet(id = packet_id::WORLD_BORDER_WARNING_TIME_CHANGED_S2C)] -pub struct WorldBorderWarningTimeChangedS2c { - pub warning_time: VarInt, -} - #[derive(Clone, Debug, Encode, Decode, Packet)] #[packet(id = packet_id::WORLD_EVENT_S2C)] pub struct WorldEventS2c { diff --git a/crates/valence_world_border/Cargo.toml b/crates/valence_world_border/Cargo.toml new file mode 100644 index 000000000..c2e5e2736 --- /dev/null +++ b/crates/valence_world_border/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "valence_world_border" +version.workspace = true +edition.workspace = true + +[dependencies] +bevy_app.workspace = true +bevy_ecs.workspace = true +glam.workspace = true +valence_client.workspace = true +valence_core.workspace = true +valence_entity.workspace = true +valence_instance.workspace = true +valence_registry.workspace = true diff --git a/crates/valence_client/src/world_border.rs b/crates/valence_world_border/src/lib.rs similarity index 81% rename from crates/valence_client/src/world_border.rs rename to crates/valence_world_border/src/lib.rs index bf518e0a4..3bc2a3d0f 100644 --- a/crates/valence_client/src/world_border.rs +++ b/crates/valence_world_border/src/lib.rs @@ -48,15 +48,20 @@ //! references for all properties of world border and their respective component #![allow(clippy::type_complexity)] -use std::time::Duration; +pub mod packet; +use std::time::{Duration, Instant}; + +use bevy_app::{App, CoreSet, Plugin}; use glam::DVec2; +use packet::*; +use valence_client::{Client, FlushPacketsSet}; +use valence_core::protocol::encode::WritePacket; +use valence_core::protocol::var_int::VarInt; use valence_core::protocol::var_long::VarLong; use valence_entity::Location; -use valence_instance::packet::*; -use valence_registry::{Component, Query}; - -use crate::*; +use valence_instance::{Instance, WriteUpdatePacketsToInstancesSet}; +use valence_registry::*; // https://minecraft.fandom.com/wiki/World_border pub const DEFAULT_PORTAL_LIMIT: i32 = 29999984; @@ -70,31 +75,35 @@ pub struct UpdateWorldBorderPerInstanceSet; #[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct UpdateWorldBorderPerClientSet; -pub(crate) fn build(app: &mut App) { - app.configure_set( - UpdateWorldBorderPerInstanceSet - .in_base_set(CoreSet::PostUpdate) - .before(WriteUpdatePacketsToInstancesSet), - ) - .configure_set( - UpdateWorldBorderPerClientSet - .in_base_set(CoreSet::PostUpdate) - .before(FlushPacketsSet), - ) - .add_event::() - .add_systems( - ( - handle_wb_size_change.before(handle_diameter_change), - handle_diameter_change, - handle_lerp_transition, - handle_center_change, - handle_warn_time_change, - handle_warn_blocks_change, - handle_portal_teleport_bounary_change, +pub struct WorldBorderPlugin; + +impl Plugin for WorldBorderPlugin { + fn build(&self, app: &mut App) { + app.configure_set( + UpdateWorldBorderPerInstanceSet + .in_base_set(CoreSet::PostUpdate) + .before(WriteUpdatePacketsToInstancesSet), + ) + .configure_set( + UpdateWorldBorderPerClientSet + .in_base_set(CoreSet::PostUpdate) + .before(FlushPacketsSet), + ) + .add_event::() + .add_systems( + ( + handle_wb_size_change.before(handle_diameter_change), + handle_diameter_change, + handle_lerp_transition, + handle_center_change, + handle_warn_time_change, + handle_warn_blocks_change, + handle_portal_teleport_bounary_change, + ) + .in_set(UpdateWorldBorderPerInstanceSet), ) - .in_set(UpdateWorldBorderPerInstanceSet), - ) - .add_system(handle_border_for_player.in_set(UpdateWorldBorderPerClientSet)); + .add_system(handle_border_for_player.in_set(UpdateWorldBorderPerClientSet)); + } } /// A bundle contains necessary component to enable world border. @@ -147,7 +156,7 @@ pub struct WorldBorderWarnBlocks(pub i32); #[derive(Component)] pub struct WorldBorderPortalTpBoundary(pub i32); -/// World border diameter can be read by querying +/// The world border diameter can be read by calling /// [`WorldBorderDiameter::diameter()`]. If you want to modify the diameter /// size, do not modify the value directly! Use [`SetWorldBorderSizeEvent`] /// instead. @@ -155,13 +164,13 @@ pub struct WorldBorderPortalTpBoundary(pub i32); pub struct WorldBorderDiameter(f64); impl WorldBorderDiameter { - pub fn diameter(&self) -> f64 { + pub fn get(&self) -> f64 { self.0 } } /// This component represents the `Set Border Lerp Size` packet with timestamp. -/// It is used for actually lerping the world border diamater. +/// It is used for actually lerping the world border diameter. /// If you need to set the diameter, it is much better to use the /// [`SetWorldBorderSizeEvent`] event #[derive(Component)] @@ -175,8 +184,12 @@ pub struct MovingWorldBorder { impl MovingWorldBorder { pub fn current_diameter(&self) -> f64 { - let t = self.current_duration() as f64 / self.duration as f64; - lerp(self.new_diameter, self.old_diameter, t) + if self.duration == 0 { + self.new_diameter + } else { + let t = self.current_duration() as f64 / self.duration as f64; + lerp(self.new_diameter, self.old_diameter, t) + } } pub fn current_duration(&self) -> i64 { @@ -214,8 +227,7 @@ pub struct SetWorldBorderSizeEvent { fn handle_wb_size_change( mut events: EventReader, - mut instances: Query<(Entity, &WorldBorderDiameter, Option<&mut MovingWorldBorder>)>, - mut commands: Commands, + mut instances: Query<(&WorldBorderDiameter, Option<&mut MovingWorldBorder>)>, ) { for SetWorldBorderSizeEvent { instance, @@ -223,23 +235,15 @@ fn handle_wb_size_change( duration, } in events.iter() { - let Ok((entity, diameter, mwb_opt)) = instances.get_mut(*instance) else { + let Ok((diameter, mwb_opt)) = instances.get_mut(*instance) else { continue; }; if let Some(mut mvb) = mwb_opt { mvb.new_diameter = *new_diameter; - mvb.old_diameter = diameter.diameter(); + mvb.old_diameter = diameter.get(); mvb.duration = duration.as_millis() as i64; mvb.timestamp = Instant::now(); - } else { - // This might be delayed by 1 tick - commands.entity(entity).insert(MovingWorldBorder { - new_diameter: *new_diameter, - old_diameter: diameter.diameter(), - duration: duration.as_millis() as i64, - timestamp: Instant::now(), - }); } } } @@ -338,15 +342,18 @@ fn handle_warn_blocks_change( } fn handle_portal_teleport_bounary_change( - mut wbs: Query<( - &mut Instance, - &WorldBorderCenter, - &WorldBorderWarnTime, - &WorldBorderWarnBlocks, - &WorldBorderDiameter, - &WorldBorderPortalTpBoundary, - Option<&MovingWorldBorder>, - )>, + mut wbs: Query< + ( + &mut Instance, + &WorldBorderCenter, + &WorldBorderWarnTime, + &WorldBorderWarnBlocks, + &WorldBorderDiameter, + &WorldBorderPortalTpBoundary, + Option<&MovingWorldBorder>, + ), + Changed, + >, ) { for (mut ins, c, wt, wb, diameter, ptb, wbl) in wbs.iter_mut() { let (new_diameter, speed) = if let Some(lerping) = wbl { diff --git a/crates/valence_world_border/src/packet.rs b/crates/valence_world_border/src/packet.rs new file mode 100644 index 000000000..30465ab54 --- /dev/null +++ b/crates/valence_world_border/src/packet.rs @@ -0,0 +1,49 @@ +use valence_core::protocol::var_int::VarInt; +use valence_core::protocol::var_long::VarLong; +use valence_core::protocol::{packet_id, Decode, Encode, Packet}; + +#[derive(Clone, Debug, Encode, Decode, Packet)] +#[packet(id = packet_id::WORLD_BORDER_CENTER_CHANGED_S2C)] +pub struct WorldBorderCenterChangedS2c { + pub x_pos: f64, + pub z_pos: f64, +} + +#[derive(Copy, Clone, Debug, Encode, Decode, Packet)] +#[packet(id = packet_id::WORLD_BORDER_INITIALIZE_S2C)] +pub struct WorldBorderInitializeS2c { + pub x: f64, + pub z: f64, + pub old_diameter: f64, + pub new_diameter: f64, + pub speed: VarLong, + pub portal_teleport_boundary: VarInt, + pub warning_blocks: VarInt, + pub warning_time: VarInt, +} + +#[derive(Clone, Debug, Encode, Decode, Packet)] +#[packet(id = packet_id::WORLD_BORDER_INTERPOLATE_SIZE_S2C)] +pub struct WorldBorderInterpolateSizeS2c { + pub old_diameter: f64, + pub new_diameter: f64, + pub speed: VarLong, +} + +#[derive(Clone, Debug, Encode, Decode, Packet)] +#[packet(id = packet_id::WORLD_BORDER_SIZE_CHANGED_S2C)] +pub struct WorldBorderSizeChangedS2c { + pub diameter: f64, +} + +#[derive(Clone, Debug, Encode, Decode, Packet)] +#[packet(id = packet_id::WORLD_BORDER_WARNING_BLOCKS_CHANGED_S2C)] +pub struct WorldBorderWarningBlocksChangedS2c { + pub warning_blocks: VarInt, +} + +#[derive(Clone, Debug, Encode, Decode, Packet)] +#[packet(id = packet_id::WORLD_BORDER_WARNING_TIME_CHANGED_S2C)] +pub struct WorldBorderWarningTimeChangedS2c { + pub warning_time: VarInt, +} diff --git a/crates/valence_world_border/src/tests.rs b/crates/valence_world_border/src/tests.rs new file mode 100644 index 000000000..55c5541b3 --- /dev/null +++ b/crates/valence_world_border/src/tests.rs @@ -0,0 +1,258 @@ +/// Sets up valence with a single mock client. Returns the Entity of the client +/// and the corresponding MockClientHelper. +/// +/// Reduces boilerplate in unit tests. +fn scenario_single_client(app: &mut App) -> (Entity, MockClientHelper) { + app.insert_resource(CoreSettings { + compression_threshold: None, + ..Default::default() + }); + + app.insert_resource(NetworkSettings { + connection_mode: ConnectionMode::Offline, + ..Default::default() + }); + + app.add_plugins(DefaultPlugins); + + app.update(); // Initialize plugins. + + let instance = Instance::new( + ident!("overworld"), + app.world.resource::(), + app.world.resource::(), + app.world.resource::(), + ); + + let instance_ent = app.world.spawn(instance).id(); + + let (client, client_helper) = create_mock_client(); + + let client_ent = app.world.spawn(client).id(); + + // Set initial location. + app.world.get_mut::(client_ent).unwrap().0 = instance_ent; + + (client_ent, client_helper) +} + +/// Creates a mock client bundle that can be used for unit testing. +/// +/// Returns the client, and a helper to inject packets as if the client sent +/// them and receive packets as if the client received them. +fn create_mock_client() -> (ClientBundle, MockClientHelper) { + let conn = MockClientConnection::new(); + + let bundle = ClientBundle::new(ClientBundleArgs { + username: "test".into(), + uuid: Uuid::from_bytes(rand::random()), + ip: "127.0.0.1".parse().unwrap(), + properties: vec![], + conn: Box::new(conn.clone()), + enc: PacketEncoder::new(), + }); + + let helper = MockClientHelper::new(conn); + + (bundle, helper) +} + +/// A mock client connection that can be used for testing. +/// +/// Safe to clone, but note that the clone will share the same buffers. +#[derive(Clone)] +struct MockClientConnection { + inner: Arc>, +} + +struct MockClientConnectionInner { + /// The queue of packets to receive from the client to be processed by the + /// server. + recv_buf: VecDeque, + /// The queue of packets to send from the server to the client. + send_buf: BytesMut, +} + +impl MockClientConnection { + fn new() -> Self { + Self { + inner: Arc::new(Mutex::new(MockClientConnectionInner { + recv_buf: VecDeque::new(), + send_buf: BytesMut::new(), + })), + } + } + + /// Injects a (Packet ID + data) frame to be received by the server. + fn inject_recv(&mut self, mut bytes: BytesMut) { + let id = VarInt::decode_partial((&mut bytes).reader()).expect("failed to decode packet ID"); + + self.inner + .lock() + .unwrap() + .recv_buf + .push_back(ReceivedPacket { + timestamp: Instant::now(), + id, + body: bytes.freeze(), + }); + } + + fn take_sent(&mut self) -> BytesMut { + self.inner.lock().unwrap().send_buf.split() + } + + fn clear_sent(&mut self) { + self.inner.lock().unwrap().send_buf.clear(); + } +} + +impl ClientConnection for MockClientConnection { + fn try_send(&mut self, bytes: BytesMut) -> anyhow::Result<()> { + self.inner.lock().unwrap().send_buf.unsplit(bytes); + Ok(()) + } + + fn try_recv(&mut self) -> anyhow::Result> { + Ok(self.inner.lock().unwrap().recv_buf.pop_front()) + } + + fn len(&self) -> usize { + self.inner.lock().unwrap().recv_buf.len() + } +} + +/// Contains the mocked client connection and helper methods to inject packets +/// and read packets from the send stream. +struct MockClientHelper { + conn: MockClientConnection, + dec: PacketDecoder, + scratch: BytesMut, +} + +impl MockClientHelper { + fn new(conn: MockClientConnection) -> Self { + Self { + conn, + dec: PacketDecoder::new(), + scratch: BytesMut::new(), + } + } + + /// Inject a packet to be treated as a packet inbound to the server. Panics + /// if the packet cannot be sent. + #[track_caller] + fn send

(&mut self, packet: &P) + where + P: Packet + Encode, + { + packet + .encode_with_id((&mut self.scratch).writer()) + .expect("failed to encode packet"); + + self.conn.inject_recv(self.scratch.split()); + } + + /// Collect all packets that have been sent to the client. + #[track_caller] + fn collect_sent(&mut self) -> PacketFrames { + self.dec.queue_bytes(self.conn.take_sent()); + + let mut res = vec![]; + + while let Some(frame) = self + .dec + .try_next_packet() + .expect("failed to decode packet frame") + { + res.push(frame); + } + + PacketFrames(res) + } + + fn clear_sent(&mut self) { + self.conn.clear_sent(); + } +} + +struct PacketFrames(Vec); + +impl PacketFrames { + #[track_caller] + fn assert_count(&self, expected_count: usize) { + let actual_count = self.0.iter().filter(|f| f.id == P::ID).count(); + + assert_eq!( + expected_count, + actual_count, + "unexpected packet count for {} (expected {expected_count}, got {actual_count})", + P::NAME + ); + } + + #[track_caller] + fn assert_order(&self) { + let positions: Vec<_> = self + .0 + .iter() + .filter_map(|f| L::packets().iter().position(|(id, _)| f.id == *id)) + .collect(); + + // TODO: replace with slice::is_sorted. + let is_sorted = positions.windows(2).all(|w| w[0] <= w[1]); + + assert!( + is_sorted, + "packets out of order (expected {:?}, got {:?})", + L::packets(), + self.debug::() + ); + } + + fn debug(&self) -> impl std::fmt::Debug { + self.0 + .iter() + .map(|f| { + L::packets() + .iter() + .find(|(id, _)| f.id == *id) + .cloned() + .unwrap_or((f.id, "")) + }) + .collect::>() + } +} + +trait PacketList { + fn packets() -> &'static [(i32, &'static str)]; +} + +macro_rules! impl_packet_list { + ($($ty:ident),*) => { + impl<$($ty: Packet,)*> PacketList for ($($ty,)*) { + fn packets() -> &'static [(i32, &'static str)] { + &[ + $( + ( + $ty::ID, + $ty::NAME + ), + )* + ] + } + } + } +} + +impl_packet_list!(A); +impl_packet_list!(A, B); +impl_packet_list!(A, B, C); +impl_packet_list!(A, B, C, D); +impl_packet_list!(A, B, C, D, E); +impl_packet_list!(A, B, C, D, E, F); +impl_packet_list!(A, B, C, D, E, F, G); +impl_packet_list!(A, B, C, D, E, F, G, H); +impl_packet_list!(A, B, C, D, E, F, G, H, I); +impl_packet_list!(A, B, C, D, E, F, G, H, I, J); +impl_packet_list!(A, B, C, D, E, F, G, H, I, J, K); \ No newline at end of file From ce911b0bf4659f613972301336a278da55299138 Mon Sep 17 00:00:00 2001 From: tachibanayui <33594017+tachibanayui@users.noreply.github.com> Date: Thu, 15 Jun 2023 10:49:07 +0700 Subject: [PATCH 6/6] Update documentation remove tests in world border crate --- crates/README.md | 1 + crates/valence_world_border/src/lib.rs | 27 ++- crates/valence_world_border/src/tests.rs | 258 ----------------------- 3 files changed, 23 insertions(+), 263 deletions(-) delete mode 100644 crates/valence_world_border/src/tests.rs diff --git a/crates/README.md b/crates/README.md index f17011ef7..7c4f8c662 100644 --- a/crates/README.md +++ b/crates/README.md @@ -22,4 +22,5 @@ graph TD anvil --> instance entity --> block advancement --> client + world_border --> client ``` diff --git a/crates/valence_world_border/src/lib.rs b/crates/valence_world_border/src/lib.rs index 3bc2a3d0f..2c4cce173 100644 --- a/crates/valence_world_border/src/lib.rs +++ b/crates/valence_world_border/src/lib.rs @@ -18,7 +18,7 @@ //! ## Modify world border diameter //! World border diameter can be changed using [`SetWorldBorderSizeEvent`]. //! Setting duration to 0 will move the border to `new_diameter` immediately, -//! otherwise it will interpolate to `new_diameter` over `duration` time. +//! otherwise, it will interpolate to `new_diameter` over `duration` time. //! ``` //! fn change_diameter( //! event_writer: EventWriter, @@ -38,15 +38,32 @@ //! //! ## Querying world border diameter //! World border diameter can be read by querying -//! [`WorldBorderDiameter::diameter()`]. Note: If you want to modify the +//! [`WorldBorderDiameter::get()`]. Note: If you want to modify the //! diameter size, do not modify the value directly! Use //! [`SetWorldBorderSizeEvent`] instead. //! //! ## Access other world border properties. -//! Access to the rest of the world border properties is fairly straight forward +//! Access to the rest of the world border properties is fairly straightforward //! by querying their respective component. [`WorldBorderBundle`] contains -//! references for all properties of world border and their respective component +//! references for all properties of the world border and their respective component #![allow(clippy::type_complexity)] +#![deny( + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + rustdoc::missing_crate_level_docs, + rustdoc::invalid_codeblock_attributes, + rustdoc::invalid_rust_codeblocks, + rustdoc::bare_urls, + rustdoc::invalid_html_tags +)] +#![warn( + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_import_braces, + unreachable_pub, + clippy::dbg_macro +)] pub mod packet; @@ -157,7 +174,7 @@ pub struct WorldBorderWarnBlocks(pub i32); pub struct WorldBorderPortalTpBoundary(pub i32); /// The world border diameter can be read by calling -/// [`WorldBorderDiameter::diameter()`]. If you want to modify the diameter +/// [`WorldBorderDiameter::get()`]. If you want to modify the diameter /// size, do not modify the value directly! Use [`SetWorldBorderSizeEvent`] /// instead. #[derive(Component)] diff --git a/crates/valence_world_border/src/tests.rs b/crates/valence_world_border/src/tests.rs deleted file mode 100644 index 55c5541b3..000000000 --- a/crates/valence_world_border/src/tests.rs +++ /dev/null @@ -1,258 +0,0 @@ -/// Sets up valence with a single mock client. Returns the Entity of the client -/// and the corresponding MockClientHelper. -/// -/// Reduces boilerplate in unit tests. -fn scenario_single_client(app: &mut App) -> (Entity, MockClientHelper) { - app.insert_resource(CoreSettings { - compression_threshold: None, - ..Default::default() - }); - - app.insert_resource(NetworkSettings { - connection_mode: ConnectionMode::Offline, - ..Default::default() - }); - - app.add_plugins(DefaultPlugins); - - app.update(); // Initialize plugins. - - let instance = Instance::new( - ident!("overworld"), - app.world.resource::(), - app.world.resource::(), - app.world.resource::(), - ); - - let instance_ent = app.world.spawn(instance).id(); - - let (client, client_helper) = create_mock_client(); - - let client_ent = app.world.spawn(client).id(); - - // Set initial location. - app.world.get_mut::(client_ent).unwrap().0 = instance_ent; - - (client_ent, client_helper) -} - -/// Creates a mock client bundle that can be used for unit testing. -/// -/// Returns the client, and a helper to inject packets as if the client sent -/// them and receive packets as if the client received them. -fn create_mock_client() -> (ClientBundle, MockClientHelper) { - let conn = MockClientConnection::new(); - - let bundle = ClientBundle::new(ClientBundleArgs { - username: "test".into(), - uuid: Uuid::from_bytes(rand::random()), - ip: "127.0.0.1".parse().unwrap(), - properties: vec![], - conn: Box::new(conn.clone()), - enc: PacketEncoder::new(), - }); - - let helper = MockClientHelper::new(conn); - - (bundle, helper) -} - -/// A mock client connection that can be used for testing. -/// -/// Safe to clone, but note that the clone will share the same buffers. -#[derive(Clone)] -struct MockClientConnection { - inner: Arc>, -} - -struct MockClientConnectionInner { - /// The queue of packets to receive from the client to be processed by the - /// server. - recv_buf: VecDeque, - /// The queue of packets to send from the server to the client. - send_buf: BytesMut, -} - -impl MockClientConnection { - fn new() -> Self { - Self { - inner: Arc::new(Mutex::new(MockClientConnectionInner { - recv_buf: VecDeque::new(), - send_buf: BytesMut::new(), - })), - } - } - - /// Injects a (Packet ID + data) frame to be received by the server. - fn inject_recv(&mut self, mut bytes: BytesMut) { - let id = VarInt::decode_partial((&mut bytes).reader()).expect("failed to decode packet ID"); - - self.inner - .lock() - .unwrap() - .recv_buf - .push_back(ReceivedPacket { - timestamp: Instant::now(), - id, - body: bytes.freeze(), - }); - } - - fn take_sent(&mut self) -> BytesMut { - self.inner.lock().unwrap().send_buf.split() - } - - fn clear_sent(&mut self) { - self.inner.lock().unwrap().send_buf.clear(); - } -} - -impl ClientConnection for MockClientConnection { - fn try_send(&mut self, bytes: BytesMut) -> anyhow::Result<()> { - self.inner.lock().unwrap().send_buf.unsplit(bytes); - Ok(()) - } - - fn try_recv(&mut self) -> anyhow::Result> { - Ok(self.inner.lock().unwrap().recv_buf.pop_front()) - } - - fn len(&self) -> usize { - self.inner.lock().unwrap().recv_buf.len() - } -} - -/// Contains the mocked client connection and helper methods to inject packets -/// and read packets from the send stream. -struct MockClientHelper { - conn: MockClientConnection, - dec: PacketDecoder, - scratch: BytesMut, -} - -impl MockClientHelper { - fn new(conn: MockClientConnection) -> Self { - Self { - conn, - dec: PacketDecoder::new(), - scratch: BytesMut::new(), - } - } - - /// Inject a packet to be treated as a packet inbound to the server. Panics - /// if the packet cannot be sent. - #[track_caller] - fn send

(&mut self, packet: &P) - where - P: Packet + Encode, - { - packet - .encode_with_id((&mut self.scratch).writer()) - .expect("failed to encode packet"); - - self.conn.inject_recv(self.scratch.split()); - } - - /// Collect all packets that have been sent to the client. - #[track_caller] - fn collect_sent(&mut self) -> PacketFrames { - self.dec.queue_bytes(self.conn.take_sent()); - - let mut res = vec![]; - - while let Some(frame) = self - .dec - .try_next_packet() - .expect("failed to decode packet frame") - { - res.push(frame); - } - - PacketFrames(res) - } - - fn clear_sent(&mut self) { - self.conn.clear_sent(); - } -} - -struct PacketFrames(Vec); - -impl PacketFrames { - #[track_caller] - fn assert_count(&self, expected_count: usize) { - let actual_count = self.0.iter().filter(|f| f.id == P::ID).count(); - - assert_eq!( - expected_count, - actual_count, - "unexpected packet count for {} (expected {expected_count}, got {actual_count})", - P::NAME - ); - } - - #[track_caller] - fn assert_order(&self) { - let positions: Vec<_> = self - .0 - .iter() - .filter_map(|f| L::packets().iter().position(|(id, _)| f.id == *id)) - .collect(); - - // TODO: replace with slice::is_sorted. - let is_sorted = positions.windows(2).all(|w| w[0] <= w[1]); - - assert!( - is_sorted, - "packets out of order (expected {:?}, got {:?})", - L::packets(), - self.debug::() - ); - } - - fn debug(&self) -> impl std::fmt::Debug { - self.0 - .iter() - .map(|f| { - L::packets() - .iter() - .find(|(id, _)| f.id == *id) - .cloned() - .unwrap_or((f.id, "")) - }) - .collect::>() - } -} - -trait PacketList { - fn packets() -> &'static [(i32, &'static str)]; -} - -macro_rules! impl_packet_list { - ($($ty:ident),*) => { - impl<$($ty: Packet,)*> PacketList for ($($ty,)*) { - fn packets() -> &'static [(i32, &'static str)] { - &[ - $( - ( - $ty::ID, - $ty::NAME - ), - )* - ] - } - } - } -} - -impl_packet_list!(A); -impl_packet_list!(A, B); -impl_packet_list!(A, B, C); -impl_packet_list!(A, B, C, D); -impl_packet_list!(A, B, C, D, E); -impl_packet_list!(A, B, C, D, E, F); -impl_packet_list!(A, B, C, D, E, F, G); -impl_packet_list!(A, B, C, D, E, F, G, H); -impl_packet_list!(A, B, C, D, E, F, G, H, I); -impl_packet_list!(A, B, C, D, E, F, G, H, I, J); -impl_packet_list!(A, B, C, D, E, F, G, H, I, J, K); \ No newline at end of file