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 5 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
103 changes: 103 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,103 @@
use std::time::Duration;

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

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

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

fn init_clients(
mut clients: Query<(&mut Client, &mut Location, &mut Position), Added<Client>>,
instances: Query<Entity, With<Instance>>,
) {
for (mut client, mut loc, mut pos) in &mut clients {
loc.0 = instances.single();
pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]);
client.send_message("Border control: ");
client.send_message("- Modify size: add <amount(can be negative)> <time(ms)>");
client.send_message("- Change center: center <x> <z>");
}
}

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] {
"resize" => {
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,
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 };
}
_ => (),
}
}
}
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;
139 changes: 139 additions & 0 deletions crates/valence/src/tests/world_border.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use std::time::Duration;

use bevy_app::App;
use valence_client::world_border::{
SetWorldBorderSizeEvent, WorldBorderBundle, WorldBorderCenter, WorldBorderPortalTpBoundary,
WorldBorderWarnBlocks, WorldBorderWarnTime,
};
use valence_entity::Location;
use valence_instance::packet::{
WorldBorderCenterChangedS2c, WorldBorderInitializeS2c, WorldBorderSizeChangedS2c,
WorldBorderWarningBlocksChangedS2c, WorldBorderWarningTimeChangedS2c,
};
use valence_instance::Instance;
use valence_registry::{Entity, Mut};

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)
}
2 changes: 2 additions & 0 deletions crates/valence_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ pub mod status;
pub mod teleport;
pub mod title;
pub mod weather;
pub mod world_border;

pub struct ClientPlugin;

Expand Down Expand Up @@ -150,6 +151,7 @@ impl Plugin for ClientPlugin {
action::build(app);
teleport::build(app);
weather::build(app);
world_border::build(app);
chat::build(app);
custom_payload::build(app);
hand_swing::build(app);
Expand Down
Loading