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_network.path = "crates/valence_network"
|
||||||
valence_player_list.path = "crates/valence_player_list"
|
valence_player_list.path = "crates/valence_player_list"
|
||||||
valence_registry.path = "crates/valence_registry"
|
valence_registry.path = "crates/valence_registry"
|
||||||
|
valence_world_border.path = "crates/valence_world_border"
|
||||||
valence.path = "crates/valence"
|
valence.path = "crates/valence"
|
||||||
|
|
||||||
zip = "0.6.3"
|
zip = "0.6.3"
|
||||||
|
|
||||||
[profile.dev.package."*"]
|
[profile.dev.package."*"]
|
||||||
|
|
|
@ -22,4 +22,5 @@ graph TD
|
||||||
anvil --> instance
|
anvil --> instance
|
||||||
entity --> block
|
entity --> block
|
||||||
advancement --> client
|
advancement --> client
|
||||||
|
world_border --> client
|
||||||
```
|
```
|
||||||
|
|
|
@ -11,12 +11,13 @@ keywords = ["minecraft", "gamedev", "server", "ecs"]
|
||||||
categories = ["game-engines"]
|
categories = ["game-engines"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["network", "player_list", "inventory", "anvil", "advancement"]
|
default = ["network", "player_list", "inventory", "anvil", "advancement", "world_border"]
|
||||||
network = ["dep:valence_network"]
|
network = ["dep:valence_network"]
|
||||||
player_list = ["dep:valence_player_list"]
|
player_list = ["dep:valence_player_list"]
|
||||||
inventory = ["dep:valence_inventory"]
|
inventory = ["dep:valence_inventory"]
|
||||||
anvil = ["dep:valence_anvil"]
|
anvil = ["dep:valence_anvil"]
|
||||||
advancement = ["dep:valence_advancement"]
|
advancement = ["dep:valence_advancement"]
|
||||||
|
world_border = ["dep:valence_world_border"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy_app.workspace = true
|
bevy_app.workspace = true
|
||||||
|
@ -37,6 +38,8 @@ valence_player_list = { workspace = true, optional = true }
|
||||||
valence_inventory = { workspace = true, optional = true }
|
valence_inventory = { workspace = true, optional = true }
|
||||||
valence_anvil = { workspace = true, optional = true }
|
valence_anvil = { workspace = true, optional = true }
|
||||||
valence_advancement = { workspace = true, optional = true }
|
valence_advancement = { workspace = true, optional = true }
|
||||||
|
valence_world_border = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow.workspace = true
|
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;
|
pub use valence_network as network;
|
||||||
#[cfg(feature = "player_list")]
|
#[cfg(feature = "player_list")]
|
||||||
pub use valence_player_list as player_list;
|
pub use valence_player_list as player_list;
|
||||||
|
#[cfg(feature = "world_border")]
|
||||||
|
pub use valence_world_border as world_border;
|
||||||
pub use {
|
pub use {
|
||||||
bevy_app as app, bevy_ecs as ecs, glam, valence_biome as biome, valence_block as block,
|
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,
|
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);
|
.add(valence_advancement::bevy_hierarchy::HierarchyPlugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "world_border")]
|
||||||
|
{
|
||||||
|
group = group.add(valence_world_border::WorldBorderPlugin);
|
||||||
|
}
|
||||||
|
|
||||||
group
|
group
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -284,3 +284,4 @@ mod client;
|
||||||
mod example;
|
mod example;
|
||||||
mod inventory;
|
mod inventory;
|
||||||
mod weather;
|
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_core::protocol::{packet_id, Decode, Encode, Packet};
|
||||||
use valence_nbt::Compound;
|
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)]
|
#[derive(Clone, Debug, Encode, Decode, Packet)]
|
||||||
#[packet(id = packet_id::WORLD_EVENT_S2C)]
|
#[packet(id = packet_id::WORLD_EVENT_S2C)]
|
||||||
pub struct WorldEventS2c {
|
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