diff --git a/Cargo.toml b/Cargo.toml index c53446b37..e35e93c96 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 f17011ef7..7c4f8c662 100644 --- a/crates/README.md +++ b/crates/README.md @@ -22,4 +22,5 @@ graph TD anvil --> instance entity --> block advancement --> client + world_border --> client ``` diff --git a/crates/valence/Cargo.toml b/crates/valence/Cargo.toml index a69c885bc..b7540897f 100644 --- a/crates/valence/Cargo.toml +++ b/crates/valence/Cargo.toml @@ -11,12 +11,13 @@ keywords = ["minecraft", "gamedev", "server", "ecs"] categories = ["game-engines"] [features] -default = ["network", "player_list", "inventory", "anvil", "advancement"] +default = ["network", "player_list", "inventory", "anvil", "advancement", "world_border"] network = ["dep:valence_network"] player_list = ["dep:valence_player_list"] inventory = ["dep:valence_inventory"] anvil = ["dep:valence_anvil"] advancement = ["dep:valence_advancement"] +world_border = ["dep:valence_world_border"] [dependencies] bevy_app.workspace = true @@ -37,6 +38,8 @@ valence_player_list = { workspace = true, optional = true } valence_inventory = { workspace = true, optional = true } valence_anvil = { workspace = true, optional = true } valence_advancement = { workspace = true, optional = true } +valence_world_border = { workspace = true, optional = true } + [dev-dependencies] anyhow.workspace = true diff --git a/crates/valence/examples/world_border.rs b/crates/valence/examples/world_border.rs new file mode 100644 index 000000000..52f71b8ca --- /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 a7ba2c142..a33b35e17 100644 --- a/crates/valence/src/lib.rs +++ b/crates/valence/src/lib.rs @@ -37,6 +37,8 @@ pub use valence_inventory as inventory; pub use valence_network as network; #[cfg(feature = "player_list")] pub use valence_player_list as player_list; +#[cfg(feature = "world_border")] +pub use valence_world_border as world_border; pub use { bevy_app as app, bevy_ecs as ecs, glam, valence_biome as biome, valence_block as block, valence_client as client, valence_dimension as dimension, valence_entity as entity, @@ -162,6 +164,11 @@ impl PluginGroup for DefaultPlugins { .add(valence_advancement::bevy_hierarchy::HierarchyPlugin); } + #[cfg(feature = "world_border")] + { + group = group.add(valence_world_border::WorldBorderPlugin); + } + group } } diff --git a/crates/valence/src/tests.rs b/crates/valence/src/tests.rs index 86e5cbd0c..9f88b6bce 100644 --- a/crates/valence/src/tests.rs +++ b/crates/valence/src/tests.rs @@ -284,3 +284,4 @@ mod client; mod example; mod inventory; mod weather; +mod world_border; diff --git a/crates/valence/src/tests/world_border.rs b/crates/valence/src/tests/world_border.rs new file mode 100644 index 000000000..41d90bc88 --- /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 6cc8f70ed..650b2e891 100644 --- a/crates/valence_instance/src/packet.rs +++ b/crates/valence_instance/src/packet.rs @@ -9,52 +9,6 @@ use valence_core::protocol::var_long::VarLong; use valence_core::protocol::{packet_id, Decode, Encode, Packet}; use valence_nbt::Compound; -#[derive(Clone, Debug, Encode, Decode, Packet)] -#[packet(id = packet_id::WORLD_BORDER_CENTER_CHANGED_S2C)] -pub struct WorldBorderCenterChangedS2c { - pub x_pos: f64, - pub z_pos: f64, -} - -#[derive(Copy, Clone, Debug, Encode, Decode, Packet)] -#[packet(id = packet_id::WORLD_BORDER_INITIALIZE_S2C)] -pub struct WorldBorderInitializeS2c { - pub x: f64, - pub z: f64, - pub old_diameter: f64, - pub new_diameter: f64, - pub speed: VarLong, - pub portal_teleport_boundary: VarInt, - pub warning_blocks: VarInt, - pub warning_time: VarInt, -} - -#[derive(Clone, Debug, Encode, Decode, Packet)] -#[packet(id = packet_id::WORLD_BORDER_INTERPOLATE_SIZE_S2C)] -pub struct WorldBorderInterpolateSizeS2c { - pub old_diameter: f64, - pub new_diameter: f64, - pub speed: VarLong, -} - -#[derive(Clone, Debug, Encode, Decode, Packet)] -#[packet(id = packet_id::WORLD_BORDER_SIZE_CHANGED_S2C)] -pub struct WorldBorderSizeChangedS2c { - pub diameter: f64, -} - -#[derive(Clone, Debug, Encode, Decode, Packet)] -#[packet(id = packet_id::WORLD_BORDER_WARNING_BLOCKS_CHANGED_S2C)] -pub struct WorldBorderWarningBlocksChangedS2c { - pub warning_blocks: VarInt, -} - -#[derive(Clone, Debug, Encode, Decode, Packet)] -#[packet(id = packet_id::WORLD_BORDER_WARNING_TIME_CHANGED_S2C)] -pub struct WorldBorderWarningTimeChangedS2c { - pub warning_time: VarInt, -} - #[derive(Clone, Debug, Encode, Decode, Packet)] #[packet(id = packet_id::WORLD_EVENT_S2C)] pub struct WorldEventS2c { diff --git a/crates/valence_world_border/Cargo.toml b/crates/valence_world_border/Cargo.toml new file mode 100644 index 000000000..c2e5e2736 --- /dev/null +++ b/crates/valence_world_border/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "valence_world_border" +version.workspace = true +edition.workspace = true + +[dependencies] +bevy_app.workspace = true +bevy_ecs.workspace = true +glam.workspace = true +valence_client.workspace = true +valence_core.workspace = true +valence_entity.workspace = true +valence_instance.workspace = true +valence_registry.workspace = true diff --git a/crates/valence_world_border/src/lib.rs b/crates/valence_world_border/src/lib.rs new file mode 100644 index 000000000..2c4cce173 --- /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 000000000..30465ab54 --- /dev/null +++ b/crates/valence_world_border/src/packet.rs @@ -0,0 +1,49 @@ +use valence_core::protocol::var_int::VarInt; +use valence_core::protocol::var_long::VarLong; +use valence_core::protocol::{packet_id, Decode, Encode, Packet}; + +#[derive(Clone, Debug, Encode, Decode, Packet)] +#[packet(id = packet_id::WORLD_BORDER_CENTER_CHANGED_S2C)] +pub struct WorldBorderCenterChangedS2c { + pub x_pos: f64, + pub z_pos: f64, +} + +#[derive(Copy, Clone, Debug, Encode, Decode, Packet)] +#[packet(id = packet_id::WORLD_BORDER_INITIALIZE_S2C)] +pub struct WorldBorderInitializeS2c { + pub x: f64, + pub z: f64, + pub old_diameter: f64, + pub new_diameter: f64, + pub speed: VarLong, + pub portal_teleport_boundary: VarInt, + pub warning_blocks: VarInt, + pub warning_time: VarInt, +} + +#[derive(Clone, Debug, Encode, Decode, Packet)] +#[packet(id = packet_id::WORLD_BORDER_INTERPOLATE_SIZE_S2C)] +pub struct WorldBorderInterpolateSizeS2c { + pub old_diameter: f64, + pub new_diameter: f64, + pub speed: VarLong, +} + +#[derive(Clone, Debug, Encode, Decode, Packet)] +#[packet(id = packet_id::WORLD_BORDER_SIZE_CHANGED_S2C)] +pub struct WorldBorderSizeChangedS2c { + pub diameter: f64, +} + +#[derive(Clone, Debug, Encode, Decode, Packet)] +#[packet(id = packet_id::WORLD_BORDER_WARNING_BLOCKS_CHANGED_S2C)] +pub struct WorldBorderWarningBlocksChangedS2c { + pub warning_blocks: VarInt, +} + +#[derive(Clone, Debug, Encode, Decode, Packet)] +#[packet(id = packet_id::WORLD_BORDER_WARNING_TIME_CHANGED_S2C)] +pub struct WorldBorderWarningTimeChangedS2c { + pub warning_time: VarInt, +}