Skip to content

Commit

Permalink
Implement world border (#364)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
tachibanayui authored Jun 15, 2023
1 parent 09fbd9b commit 61f2279
Show file tree
Hide file tree
Showing 11 changed files with 769 additions and 47 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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."*"]
Expand Down
1 change: 1 addition & 0 deletions crates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ graph TD
anvil --> instance
entity --> block
advancement --> client
world_border --> client
```
5 changes: 4 additions & 1 deletion crates/valence/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
161 changes: 161 additions & 0 deletions crates/valence/examples/world_border.rs
Original file line number Diff line number Diff line change
@@ -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 };
}
_ => (),
}
}
}
7 changes: 7 additions & 0 deletions crates/valence/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
}
1 change: 1 addition & 0 deletions crates/valence/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,4 @@ mod client;
mod example;
mod inventory;
mod weather;
mod world_border;
133 changes: 133 additions & 0 deletions crates/valence/src/tests/world_border.rs
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 61f2279

Please sign in to comment.