From 61f2279831920dcbc78566f68e16965ae9f07572 Mon Sep 17 00:00:00 2001
From: Tachibana Yui <33594017+tachibanayui@users.noreply.github.com>
Date: Thu, 15 Jun 2023 12:15:47 +0700
Subject: [PATCH] Implement world border (#364)
## Description
Basic implementation of world border
World border is not enabled by default. It can be enabled by inserting
`WorldBorderBundle` bundle. Currently, this PR only implements world
borders per instance, I'm considering expanding this per client.
However, the same functionality can be achieved by Visibility Layers
#362
Playground:
```rust
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] {
"add" => {
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 };
}
_ => (),
}
}
}
```
example: `cargo run --package valence --example world_border`
tests: `cargo test --package valence --lib -- tests::world_border`
**Related**
part of #210
---
Cargo.toml | 2 +
crates/README.md | 1 +
crates/valence/Cargo.toml | 5 +-
crates/valence/examples/world_border.rs | 161 +++++++++
crates/valence/src/lib.rs | 7 +
crates/valence/src/tests.rs | 1 +
crates/valence/src/tests/world_border.rs | 133 ++++++++
crates/valence_instance/src/packet.rs | 46 ---
crates/valence_world_border/Cargo.toml | 14 +
crates/valence_world_border/src/lib.rs | 397 ++++++++++++++++++++++
crates/valence_world_border/src/packet.rs | 49 +++
11 files changed, 769 insertions(+), 47 deletions(-)
create mode 100644 crates/valence/examples/world_border.rs
create mode 100644 crates/valence/src/tests/world_border.rs
create mode 100644 crates/valence_world_border/Cargo.toml
create mode 100644 crates/valence_world_border/src/lib.rs
create mode 100644 crates/valence_world_border/src/packet.rs
diff --git a/Cargo.toml b/Cargo.toml
index c53446b..e35e93c 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/README.md b/crates/README.md
index f17011e..7c4f8c6 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/Cargo.toml b/crates/valence/Cargo.toml
index a69c885..b754089 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
new file mode 100644
index 0000000..52f71b8
--- /dev/null
+++ b/crates/valence/examples/world_border.rs
@@ -0,0 +1,161 @@
+use std::time::Duration;
+
+use bevy_app::App;
+use valence::client::chat::ChatMessageEvent;
+use valence::client::despawn_disconnected_clients;
+use valence::inventory::HeldItem;
+use valence::prelude::*;
+use valence::world_border::*;
+
+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_center_avg)
+ .add_system(border_expand)
+ .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::MOSSY_COBBLESTONE);
+ }
+ }
+
+ commands
+ .spawn(instance)
+ .insert(WorldBorderBundle::new([0.0, 0.0], 1.0));
+}
+
+fn init_clients(
+ mut clients: Query<
+ (
+ &mut Client,
+ &mut Location,
+ &mut Position,
+ &mut Inventory,
+ &HeldItem,
+ ),
+ Added,
+ >,
+ instances: Query>,
+) {
+ 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]);
+ 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>,
+ mut event_writer: EventWriter,
+) {
+ for x in events.iter() {
+ let parts: Vec<&str> = x.message.split(' ').collect();
+ match parts[0] {
+ "add" => {
+ 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.get() + value,
+ duration: Duration::from_millis(speed as u64),
+ })
+ }
+ "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/lib.rs b/crates/valence/src/lib.rs
index a7ba2c1..a33b35e 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.rs b/crates/valence/src/tests.rs
index 86e5cbd..9f88b6b 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 0000000..41d90bc
--- /dev/null
+++ b/crates/valence/src/tests/world_border.rs
@@ -0,0 +1,133 @@
+use std::time::Duration;
+
+use bevy_app::App;
+use valence_entity::Location;
+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};
+
+#[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,
+ duration: Duration::ZERO,
+ 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(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();
+ 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();
+ (client_helper, instance_ent)
+}
diff --git a/crates/valence_instance/src/packet.rs b/crates/valence_instance/src/packet.rs
index 6cc8f70..650b2e8 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 0000000..c2e5e27
--- /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_world_border/src/lib.rs b/crates/valence_world_border/src/lib.rs
new file mode 100644
index 0000000..2c4cce1
--- /dev/null
+++ b/crates/valence_world_border/src/lib.rs
@@ -0,0 +1,397 @@
+//! # 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 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,
+//! })
+//! }
+//! ```
+//!
+//! 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::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 straightforward
+//! by querying their respective component. [`WorldBorderBundle`] contains
+//! 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;
+
+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::{Instance, WriteUpdatePacketsToInstancesSet};
+use valence_registry::*;
+
+// 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 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),
+ )
+ .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,
+ pub diameter: WorldBorderDiameter,
+ pub portal_teleport_boundary: WorldBorderPortalTpBoundary,
+ pub warning_time: WorldBorderWarnTime,
+ pub warning_blocks: WorldBorderWarnBlocks,
+ pub moving: MovingWorldBorder,
+}
+
+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()),
+ 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,
+ duration: 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);
+
+/// The world border diameter can be read by calling
+/// [`WorldBorderDiameter::get()`]. If you want to modify the diameter
+/// size, do not modify the value directly! Use [`SetWorldBorderSizeEvent`]
+/// instead.
+#[derive(Component)]
+pub struct WorldBorderDiameter(f64);
+
+impl WorldBorderDiameter {
+ 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 diameter.
+/// 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,
+ /// equivalent to `speed` on wiki.vg
+ pub duration: i64,
+ pub timestamp: Instant,
+}
+
+impl MovingWorldBorder {
+ pub fn current_diameter(&self) -> f64 {
+ 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 {
+ let speed = self.duration - self.timestamp.elapsed().as_millis() as i64;
+ speed.max(0)
+ }
+}
+
+/// 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
+ 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 duration: Duration,
+}
+
+fn handle_wb_size_change(
+ mut events: EventReader,
+ mut instances: Query<(&WorldBorderDiameter, Option<&mut MovingWorldBorder>)>,
+) {
+ for SetWorldBorderSizeEvent {
+ instance,
+ new_diameter,
+ duration,
+ } in events.iter()
+ {
+ 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.get();
+ mvb.duration = duration.as_millis() as i64;
+ mvb.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_duration())
+ } 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.duration == 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_duration()),
+ });
+ }
+ }
+}
+
+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>,
+ ),
+ Changed,
+ >,
+) {
+ 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_duration())
+ } 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
+}
diff --git a/crates/valence_world_border/src/packet.rs b/crates/valence_world_border/src/packet.rs
new file mode 100644
index 0000000..30465ab
--- /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,
+}