Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement world border #364

Merged
merged 8 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The graph in crates/README.md needs an update. Should have an edge going from world_border to client.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to have doc comments instead of an example for this. We're trying to cut down on the number of examples because of the maintenance workload.

Can you update your playground to use the entire template? It makes it easier to switch between playgrounds if we can just copy and paste the entire file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I plan to provide both examples and module-level documentation (which I'm currently writing).

Here is the entire playground:

use valence::client::chat::ChatMessageEvent;
use valence::client::despawn_disconnected_clients;
use valence::client::world_border::{
    SetWorldBorderSizeEvent, WorldBorderBundle, WorldBorderCenter, WorldBorderDiameter,
};
use valence::network::ConnectionMode;
use valence::prelude::*;

#[allow(unused_imports)]
use crate::extras::*;

const SPAWN_Y: i32 = 64;

pub fn build_app(app: &mut App) {
    app.insert_resource(NetworkSettings {
        connection_mode: ConnectionMode::Offline,
        ..Default::default()
    })
    .add_plugins(DefaultPlugins)
    .add_startup_system(setup)
    .add_system(init_clients)
    .add_system(despawn_disconnected_clients)
    .add_system(toggle_gamemode_on_sneak.in_schedule(EventLoopSchedule))
    .add_system(border_controls);
}

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::GRASS_BLOCK);
        }
    }


    commands
        .spawn(instance)
        .insert(WorldBorderBundle::new([0.0, 0.0], 10.0));
}

fn init_clients(
    mut clients: Query<(&mut Location, &mut Position), Added<Client>>,
    instances: Query<Entity, With<Instance>>,
) {
    for (mut loc, mut pos) in &mut clients {
        loc.0 = instances.single();
        pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]);
    }
}

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 };
            }
            _ => (),
        }
    }
}

Copy link
Member

@rj00a rj00a Jun 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know I said I didn't want to have a bunch of small examples, but I've changed my mind on this issue somewhat. Not relevant for this PR, but in the future we should separate "here's how to use this API" examples (like this one) from "here's a complete minigame" examples (like parkour.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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This panics in multiple different ways when the user input is invalid. I suggest demonstrating world border functionality without user input to keep the example focused. Perhaps changing the world border diameter and center on a timer or something.

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),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This disconnected me when I passed in a duration of zero.

failed to write packet: failed to encode field `old_diameter` in `WorldBorderInitializeS2c`: attempt to encode non-finite f64 (NaN)

})
}
"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