mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-23 14:31:30 +11:00
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 <details> <summary>Playground:</summary> ```rust fn border_controls( mut events: EventReader<ChatMessageEvent>, mut instances: Query<(Entity, &WorldBorderDiameter, &mut WorldBorderCenter), With<Instance>>, mut event_writer: EventWriter<SetWorldBorderSizeEvent>, ) { for x in events.iter() { let parts: Vec<&str> = x.message.split(' ').collect(); match parts[0] { "add" => { let Ok(value) = parts[1].parse::<f64>() else { return; }; let Ok(speed) = parts[2].parse::<i64>() 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::<f64>() else { return; }; let Ok(z) = parts[2].parse::<f64>() else { return; }; instances.single_mut().2 .0 = DVec2 { x, y: z }; } _ => (), } } } ``` </details> example: `cargo run --package valence --example world_border` tests: `cargo test --package valence --lib -- tests::world_border` **Related** part of #210
This commit is contained in:
parent
09fbd9b7e7
commit
61f2279831
|
@ -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."*"]
|
||||
|
|
|
@ -22,4 +22,5 @@ graph TD
|
|||
anvil --> instance
|
||||
entity --> block
|
||||
advancement --> client
|
||||
world_border --> client
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
161
crates/valence/examples/world_border.rs
Normal file
161
crates/valence/examples/world_border.rs
Normal file
|
@ -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<Server>,
|
||||
biomes: Res<BiomeRegistry>,
|
||||
dimensions: Res<DimensionTypeRegistry>,
|
||||
) {
|
||||
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<Client>,
|
||||
>,
|
||||
instances: Query<Entity, With<Instance>>,
|
||||
) {
|
||||
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<Instance>>,
|
||||
) {
|
||||
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<DiggingEvent>,
|
||||
clients: Query<&Location, With<Client>>,
|
||||
wbs: Query<&WorldBorderDiameter, With<Instance>>,
|
||||
mut event_writer: EventWriter<SetWorldBorderSizeEvent>,
|
||||
) {
|
||||
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<ChatMessageEvent>,
|
||||
mut instances: Query<(Entity, &WorldBorderDiameter, &mut WorldBorderCenter), With<Instance>>,
|
||||
mut event_writer: EventWriter<SetWorldBorderSizeEvent>,
|
||||
) {
|
||||
for x in events.iter() {
|
||||
let parts: Vec<&str> = x.message.split(' ').collect();
|
||||
match parts[0] {
|
||||
"add" => {
|
||||
let Ok(value) = parts[1].parse::<f64>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(speed) = parts[2].parse::<i64>() 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::<f64>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(z) = parts[2].parse::<f64>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
instances.single_mut().2 .0 = DVec2 { x, y: z };
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -284,3 +284,4 @@ mod client;
|
|||
mod example;
|
||||
mod inventory;
|
||||
mod weather;
|
||||
mod world_border;
|
||||
|
|
133
crates/valence/src/tests/world_border.rs
Normal file
133
crates/valence/src/tests/world_border.rs
Normal file
|
@ -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::<Location>(client_ent).unwrap().0 = instance_ent;
|
||||
app.update();
|
||||
|
||||
client_helper
|
||||
.collect_sent()
|
||||
.assert_count::<WorldBorderInitializeS2c>(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::<WorldBorderSizeChangedS2c>(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<WorldBorderCenter> = 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::<WorldBorderCenterChangedS2c>(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<WorldBorderWarnTime> = 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::<WorldBorderWarningTimeChangedS2c>(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<WorldBorderWarnBlocks> = 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::<WorldBorderWarningBlocksChangedS2c>(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<WorldBorderPortalTpBoundary> = 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::<WorldBorderInitializeS2c>(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::<Instance>())
|
||||
.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)
|
||||
}
|
|
@ -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 {
|
||||
|
|
14
crates/valence_world_border/Cargo.toml
Normal file
14
crates/valence_world_border/Cargo.toml
Normal file
|
@ -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
|
397
crates/valence_world_border/src/lib.rs
Normal file
397
crates/valence_world_border/src/lib.rs
Normal file
|
@ -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<SetWorldBorderSizeEvent>,
|
||||
//! 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::<SetWorldBorderSizeEvent>()
|
||||
.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<DVec2>, 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<SetWorldBorderSizeEvent>,
|
||||
/// 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<SetWorldBorderSizeEvent>,
|
||||
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<Location>>,
|
||||
wbs: Query<
|
||||
(
|
||||
&WorldBorderCenter,
|
||||
&WorldBorderWarnTime,
|
||||
&WorldBorderWarnBlocks,
|
||||
&WorldBorderDiameter,
|
||||
&WorldBorderPortalTpBoundary,
|
||||
Option<&MovingWorldBorder>,
|
||||
),
|
||||
With<Instance>,
|
||||
>,
|
||||
) {
|
||||
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<MovingWorldBorder>>,
|
||||
) {
|
||||
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<WorldBorderCenter>>,
|
||||
) {
|
||||
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<WorldBorderWarnTime>>,
|
||||
) {
|
||||
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<WorldBorderWarnBlocks>>,
|
||||
) {
|
||||
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<WorldBorderPortalTpBoundary>,
|
||||
>,
|
||||
) {
|
||||
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
|
||||
}
|
49
crates/valence_world_border/src/packet.rs
Normal file
49
crates/valence_world_border/src/packet.rs
Normal file
|
@ -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,
|
||||
}
|
Loading…
Reference in a new issue