-
-
Notifications
You must be signed in to change notification settings - Fork 145
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
Changes from 5 commits
47b158b
800c1fd
ece9404
8903318
aa3013d
10bf36a
0aebc87
ce911b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This disconnected me when I passed in a duration of zero.
|
||
}) | ||
} | ||
"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 }; | ||
} | ||
_ => (), | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -284,3 +284,4 @@ mod client; | |
mod example; | ||
mod inventory; | ||
mod weather; | ||
mod world_border; |
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) | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
There was a problem hiding this comment.
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).