diff --git a/crates/playground/src/playground.template.rs b/crates/playground/src/playground.template.rs index 02e172f..ab77e62 100644 --- a/crates/playground/src/playground.template.rs +++ b/crates/playground/src/playground.template.rs @@ -9,7 +9,7 @@ const SPAWN_Y: i32 = 64; pub fn build_app(app: &mut App) { app.add_plugin(ServerPlugin::new(())) - .add_system_to_stage(EventLoop, default_event_handler) + .add_system(default_event_handler.in_schedule(EventLoopSchedule)) .add_startup_system(setup) .add_system(init_clients) .add_system(despawn_disconnected_clients); diff --git a/crates/valence/Cargo.toml b/crates/valence/Cargo.toml index c2dd506..c849423 100644 --- a/crates/valence/Cargo.toml +++ b/crates/valence/Cargo.toml @@ -16,8 +16,8 @@ anyhow = "1.0.65" arrayvec = "0.7.2" async-trait = "0.1.60" base64 = "0.21.0" -bevy_app = "0.9.1" -bevy_ecs = "0.9.1" +bevy_app = { git = "https://github.com/bevyengine/bevy/", rev = "2ea00610188dce1eba1172a3ded8244570189230" } +bevy_ecs = { git = "https://github.com/bevyengine/bevy/", rev = "2ea00610188dce1eba1172a3ded8244570189230" } bitfield-struct = "0.3.1" bytes = "1.2.1" flume = "0.10.14" diff --git a/crates/valence/examples/bench_players.rs b/crates/valence/examples/bench_players.rs index cac4e03..ad3acda 100644 --- a/crates/valence/examples/bench_players.rs +++ b/crates/valence/examples/bench_players.rs @@ -1,6 +1,5 @@ use std::time::Instant; -use bevy_app::{App, CoreStage}; use valence::client::despawn_disconnected_clients; use valence::client::event::default_event_handler; use valence::instance::{Chunk, Instance}; @@ -22,12 +21,14 @@ fn main() { .with_max_connections(50_000), ) .add_startup_system(setup) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system_to_stage(CoreStage::First, record_tick_start_time) - .add_system_to_stage(CoreStage::Last, print_tick_time) - .add_system(init_clients) - .add_system(despawn_disconnected_clients) - .add_system_set(PlayerList::default_system_set()) + .add_systems(( + default_event_handler.in_schedule(EventLoopSchedule), + record_tick_start_time.in_base_set(CoreSet::First), + print_tick_time.in_base_set(CoreSet::Last), + init_clients, + despawn_disconnected_clients, + )) + .add_systems(PlayerList::default_systems()) .run(); } diff --git a/crates/valence/examples/biomes.rs b/crates/valence/examples/biomes.rs index 3dba08e..a022e87 100644 --- a/crates/valence/examples/biomes.rs +++ b/crates/valence/examples/biomes.rs @@ -32,11 +32,13 @@ pub fn main() { .collect::>(), ), ) - .add_system_to_stage(EventLoop, default_event_handler) .add_startup_system(setup) - .add_system(init_clients) - .add_system(despawn_disconnected_clients) - .add_system_set(PlayerList::default_system_set()) + .add_systems(( + default_event_handler.in_schedule(EventLoopSchedule), + init_clients, + despawn_disconnected_clients, + )) + .add_systems(PlayerList::default_systems()) .run(); } diff --git a/crates/valence/examples/block_entities.rs b/crates/valence/examples/block_entities.rs index 7b756a3..553ea3d 100644 --- a/crates/valence/examples/block_entities.rs +++ b/crates/valence/examples/block_entities.rs @@ -13,12 +13,14 @@ pub fn main() { App::new() .add_plugin(ServerPlugin::new(())) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system_to_stage(EventLoop, event_handler) - .add_system_set(PlayerList::default_system_set()) .add_startup_system(setup) - .add_system(init_clients) - .add_system(despawn_disconnected_clients) + .add_systems(( + default_event_handler.in_schedule(EventLoopSchedule), + event_handler.in_schedule(EventLoopSchedule), + init_clients, + despawn_disconnected_clients, + )) + .add_systems(PlayerList::default_systems()) .run(); } diff --git a/crates/valence/examples/building.rs b/crates/valence/examples/building.rs index 5bc9d04..3c188e9 100644 --- a/crates/valence/examples/building.rs +++ b/crates/valence/examples/building.rs @@ -12,15 +12,20 @@ pub fn main() { App::new() .add_plugin(ServerPlugin::new(())) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system_to_stage(EventLoop, toggle_gamemode_on_sneak) - .add_system_to_stage(EventLoop, digging_creative_mode) - .add_system_to_stage(EventLoop, digging_survival_mode) - .add_system_to_stage(EventLoop, place_blocks) - .add_system_set(PlayerList::default_system_set()) .add_startup_system(setup) .add_system(init_clients) .add_system(despawn_disconnected_clients) + .add_systems( + ( + default_event_handler, + toggle_gamemode_on_sneak, + digging_creative_mode, + digging_survival_mode, + place_blocks, + ) + .in_schedule(EventLoopSchedule), + ) + .add_systems(PlayerList::default_systems()) .run(); } diff --git a/crates/valence/examples/chat.rs b/crates/valence/examples/chat.rs index 6e640ed..b5b6359 100644 --- a/crates/valence/examples/chat.rs +++ b/crates/valence/examples/chat.rs @@ -12,12 +12,17 @@ pub fn main() { App::new() .add_plugin(ServerPlugin::new(()).with_connection_mode(ConnectionMode::Offline)) .add_startup_system(setup) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system_to_stage(EventLoop, handle_message_events) - .add_system_to_stage(EventLoop, handle_command_events) .add_system(init_clients) + .add_systems( + ( + default_event_handler, + handle_message_events, + handle_command_events, + ) + .in_schedule(EventLoopSchedule), + ) + .add_systems(PlayerList::default_systems()) .add_system(despawn_disconnected_clients) - .add_system_set(PlayerList::default_system_set()) .run(); } @@ -65,9 +70,10 @@ fn handle_message_events(mut clients: Query<&mut Client>, mut messages: EventRea .color(Color::YELLOW) + message.into_text().not_bold().color(Color::WHITE); - clients.par_for_each_mut(16, |mut client| { + // TODO: write message to instance buffer. + for mut client in &mut clients { client.send_message(formatted.clone()); - }) + } } } diff --git a/crates/valence/examples/chest.rs b/crates/valence/examples/chest.rs index ee8bcc8..16d26dc 100644 --- a/crates/valence/examples/chest.rs +++ b/crates/valence/examples/chest.rs @@ -11,12 +11,13 @@ pub fn main() { App::new() .add_plugin(ServerPlugin::new(())) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system_to_stage(EventLoop, toggle_gamemode_on_sneak) - .add_system_to_stage(EventLoop, open_chest) - .add_system_set(PlayerList::default_system_set()) .add_startup_system(setup) .add_system(init_clients) + .add_systems( + (default_event_handler, toggle_gamemode_on_sneak, open_chest) + .in_schedule(EventLoopSchedule), + ) + .add_systems(PlayerList::default_systems()) .add_system(despawn_disconnected_clients) .run(); } diff --git a/crates/valence/examples/combat.rs b/crates/valence/examples/combat.rs index 63fcedf..646125c 100644 --- a/crates/valence/examples/combat.rs +++ b/crates/valence/examples/combat.rs @@ -22,11 +22,10 @@ pub fn main() { App::new() .add_plugin(ServerPlugin::new(())) .add_startup_system(setup) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system_to_stage(EventLoop, handle_combat_events) .add_system(init_clients) + .add_systems((default_event_handler, handle_combat_events).in_schedule(EventLoopSchedule)) + .add_systems(PlayerList::default_systems()) .add_system(despawn_disconnected_clients) - .add_system_set(PlayerList::default_system_set()) .add_system(teleport_oob_clients) .run(); } diff --git a/crates/valence/examples/conway.rs b/crates/valence/examples/conway.rs index d73bc73..0c049ff 100644 --- a/crates/valence/examples/conway.rs +++ b/crates/valence/examples/conway.rs @@ -27,15 +27,16 @@ pub fn main() { grass_color: Some(0x00ff00), ..Default::default() }])) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system_set(PlayerList::default_system_set()) .add_startup_system(setup) .add_system(init_clients) - .add_system(despawn_disconnected_clients) - .add_system_to_stage(EventLoop, toggle_cell_on_dig) - .add_system(update_board) - .add_system(pause_on_crouch) - .add_system(reset_oob_clients) + .add_systems((default_event_handler, toggle_cell_on_dig).in_schedule(EventLoopSchedule)) + .add_systems(PlayerList::default_systems()) + .add_systems(( + despawn_disconnected_clients, + update_board, + pause_on_crouch, + reset_oob_clients, + )) .run(); } diff --git a/crates/valence/examples/cow_sphere.rs b/crates/valence/examples/cow_sphere.rs index bc09ef9..6906c36 100644 --- a/crates/valence/examples/cow_sphere.rs +++ b/crates/valence/examples/cow_sphere.rs @@ -24,12 +24,12 @@ fn main() { App::new() .add_plugin(ServerPlugin::new(())) - .add_system_to_stage(EventLoop, default_event_handler) .add_startup_system(setup) .add_system(init_clients) + .add_system(default_event_handler.in_schedule(EventLoopSchedule)) + .add_systems(PlayerList::default_systems()) .add_system(update_sphere) .add_system(despawn_disconnected_clients) - .add_system_set(PlayerList::default_system_set()) .run(); } diff --git a/crates/valence/examples/death.rs b/crates/valence/examples/death.rs index a9e676a..e4811da 100644 --- a/crates/valence/examples/death.rs +++ b/crates/valence/examples/death.rs @@ -10,12 +10,12 @@ pub fn main() { App::new() .add_plugin(ServerPlugin::new(())) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system_to_stage(EventLoop, squat_and_die) - .add_system_to_stage(EventLoop, necromancy) - .add_system_set(PlayerList::default_system_set()) .add_startup_system(setup) .add_system(init_clients) + .add_systems( + (default_event_handler, squat_and_die, necromancy).in_schedule(EventLoopSchedule), + ) + .add_systems(PlayerList::default_systems()) .add_system(despawn_disconnected_clients) .run(); } diff --git a/crates/valence/examples/gamemode_switcher.rs b/crates/valence/examples/gamemode_switcher.rs index 3d93211..86d73d3 100644 --- a/crates/valence/examples/gamemode_switcher.rs +++ b/crates/valence/examples/gamemode_switcher.rs @@ -9,11 +9,10 @@ pub fn main() { App::new() .add_plugin(ServerPlugin::new(())) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system_to_stage(EventLoop, interpret_command) - .add_system_set(PlayerList::default_system_set()) .add_startup_system(setup) .add_system(init_clients) + .add_systems((default_event_handler, interpret_command).in_schedule(EventLoopSchedule)) + .add_systems(PlayerList::default_systems()) .add_system(despawn_disconnected_clients) .run(); } diff --git a/crates/valence/examples/parkour.rs b/crates/valence/examples/parkour.rs index 914ddf3..eecb963 100644 --- a/crates/valence/examples/parkour.rs +++ b/crates/valence/examples/parkour.rs @@ -28,13 +28,16 @@ pub fn main() { App::new() .add_plugin(ServerPlugin::new(())) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system_set(PlayerList::default_system_set()) .add_system(init_clients) + .add_system(default_event_handler.in_schedule(EventLoopSchedule)) + .add_systems(PlayerList::default_systems()) + .add_systems(( + reset_clients.after(init_clients), + manage_chunks.after(reset_clients).before(manage_blocks), + manage_blocks, + despawn_disconnected_clients, + )) .add_system(despawn_disconnected_clients) - .add_system(reset_clients.after(init_clients)) - .add_system(manage_chunks.after(reset_clients).before(manage_blocks)) - .add_system(manage_blocks) .run(); } diff --git a/crates/valence/examples/particles.rs b/crates/valence/examples/particles.rs index bdd3bbf..fd0d3e3 100644 --- a/crates/valence/examples/particles.rs +++ b/crates/valence/examples/particles.rs @@ -9,10 +9,10 @@ pub fn main() { App::new() .add_plugin(ServerPlugin::new(())) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system_set(PlayerList::default_system_set()) .add_startup_system(setup) .add_system(init_clients) + .add_system(default_event_handler.in_schedule(EventLoopSchedule)) + .add_systems(PlayerList::default_systems()) .add_system(despawn_disconnected_clients) .add_system(manage_particles) .run(); diff --git a/crates/valence/examples/player_list.rs b/crates/valence/examples/player_list.rs index faba955..7fc6e1e 100644 --- a/crates/valence/examples/player_list.rs +++ b/crates/valence/examples/player_list.rs @@ -14,11 +14,14 @@ fn main() { App::new() .add_plugin(ServerPlugin::new(())) .add_startup_system(setup) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system(init_clients) - .add_system(update_player_list) - .add_system(despawn_disconnected_clients) - .add_system(remove_disconnected_clients_from_player_list) + .add_system(default_event_handler.in_schedule(EventLoopSchedule)) + .add_systems(PlayerList::default_systems()) + .add_systems(( + init_clients, + update_player_list, + remove_disconnected_clients_from_player_list, + despawn_disconnected_clients, + )) .run(); } diff --git a/crates/valence/examples/resource_pack.rs b/crates/valence/examples/resource_pack.rs index 4ea2cac..20b9d80 100644 --- a/crates/valence/examples/resource_pack.rs +++ b/crates/valence/examples/resource_pack.rs @@ -12,12 +12,17 @@ pub fn main() { App::new() .add_plugin(ServerPlugin::new(())) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system_to_stage(EventLoop, prompt_on_punch) - .add_system_to_stage(EventLoop, on_resource_pack_status) - .add_system_set(PlayerList::default_system_set()) .add_startup_system(setup) .add_system(init_clients) + .add_systems( + ( + default_event_handler, + prompt_on_punch, + on_resource_pack_status, + ) + .in_schedule(EventLoopSchedule), + ) + .add_systems(PlayerList::default_systems()) .add_system(despawn_disconnected_clients) .run(); } @@ -33,7 +38,7 @@ fn setup(mut commands: Commands, server: Res) { for z in -25..25 { for x in -25..25 { - instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + instance.set_block([x, SPAWN_Y, z], BlockState::BEDROCK); } } @@ -41,6 +46,8 @@ fn setup(mut commands: Commands, server: Res) { let mut sheep = McEntity::new(EntityKind::Sheep, instance_ent); sheep.set_position([0.0, SPAWN_Y as f64 + 1.0, 2.0]); + sheep.set_yaw(180.0); + sheep.set_head_yaw(180.0); commands.spawn(sheep); } diff --git a/crates/valence/examples/terrain.rs b/crates/valence/examples/terrain.rs index b0a0d23..e3b3789 100644 --- a/crates/valence/examples/terrain.rs +++ b/crates/valence/examples/terrain.rs @@ -43,14 +43,19 @@ pub fn main() { App::new() .add_plugin(ServerPlugin::new(())) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system_set(PlayerList::default_system_set()) .add_startup_system(setup) - .add_system(init_clients) - .add_system(remove_unviewed_chunks.after(init_clients)) - .add_system(update_client_views.after(remove_unviewed_chunks)) - .add_system(send_recv_chunks.after(update_client_views)) + .add_system(default_event_handler.in_schedule(EventLoopSchedule)) + .add_systems( + ( + init_clients, + remove_unviewed_chunks, + update_client_views, + send_recv_chunks, + ) + .chain(), + ) .add_system(despawn_disconnected_clients) + .add_systems(PlayerList::default_systems()) .run(); } diff --git a/crates/valence/examples/text.rs b/crates/valence/examples/text.rs index ec07246..992d356 100644 --- a/crates/valence/examples/text.rs +++ b/crates/valence/examples/text.rs @@ -10,10 +10,10 @@ pub fn main() { App::new() .add_plugin(ServerPlugin::new(())) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system_set(PlayerList::default_system_set()) .add_startup_system(setup) .add_system(init_clients) + .add_system(default_event_handler.in_schedule(EventLoopSchedule)) + .add_systems(PlayerList::default_systems()) .add_system(despawn_disconnected_clients) .run(); } diff --git a/crates/valence/src/client.rs b/crates/valence/src/client.rs index 8c1b529..eb0cad6 100644 --- a/crates/valence/src/client.rs +++ b/crates/valence/src/client.rs @@ -633,32 +633,33 @@ pub(crate) fn update_clients( instances: Query<&Instance>, entities: Query<&McEntity>, ) { - // TODO: what batch size to use? - clients.par_for_each_mut(16, |(entity_id, mut client, self_entity)| { - if !client.is_disconnected() { - if let Err(e) = update_one_client( - &mut client, - self_entity, - entity_id, - &instances, - &entities, - &server, - ) { - client.write_packet(&DisconnectS2c { - reason: Text::from("").into(), - }); - client.is_disconnected = true; - warn!( - username = %client.username, - uuid = %client.uuid, - ip = %client.ip, - "error updating client: {e:#}" - ); + clients + .par_iter_mut() + .for_each_mut(|(entity_id, mut client, self_entity)| { + if !client.is_disconnected() { + if let Err(e) = update_one_client( + &mut client, + self_entity, + entity_id, + &instances, + &entities, + &server, + ) { + client.write_packet(&DisconnectS2c { + reason: Text::from("").into(), + }); + client.is_disconnected = true; + warn!( + username = %client.username, + uuid = %client.uuid, + ip = %client.ip, + "error updating client: {e:#}" + ); + } } - } - client.is_new = false; - }); + client.is_new = false; + }); } #[inline] diff --git a/crates/valence/src/client/event.rs b/crates/valence/src/client/event.rs index df9d9d1..f2e28ac 100644 --- a/crates/valence/src/client/event.rs +++ b/crates/valence/src/client/event.rs @@ -2,8 +2,7 @@ use std::cmp; use anyhow::bail; use bevy_ecs::prelude::*; -use bevy_ecs::schedule::ShouldRun; -use bevy_ecs::system::SystemParam; +use bevy_ecs::system::{SystemParam, SystemState}; use glam::{DVec3, Vec3}; use paste::paste; use tracing::warn; @@ -34,6 +33,7 @@ use valence_protocol::types::{Difficulty, Direction, Hand}; use crate::client::Client; use crate::entity::{EntityAnimation, EntityKind, McEntity, TrackedData}; use crate::inventory::Inventory; +use crate::server::EventLoopSchedule; #[derive(Clone, Debug)] pub struct QueryBlockNbt { @@ -635,57 +635,62 @@ events! { } } -pub(crate) fn event_loop_run_criteria( - mut clients: Query<(Entity, &mut Client, &mut Inventory)>, +/// An exclusive system for running the event loop schedule. +#[allow(clippy::type_complexity)] +pub(crate) fn run_event_loop( + world: &mut World, + state: &mut SystemState<(Query<(Entity, &mut Client, &mut Inventory)>, ClientEvents)>, mut clients_to_check: Local>, - mut events: ClientEvents, -) -> ShouldRun { - if clients_to_check.is_empty() { - // First run of the criteria. Prepare packets. +) { + let (mut clients, mut events) = state.get_mut(world); - update_all_event_buffers(&mut events); + update_all_event_buffers(&mut events); - for (entity, client, inventory) in &mut clients { - let client = client.into_inner(); - let inventory = inventory.into_inner(); + for (entity, client, inventory) in &mut clients { + let client = client.into_inner(); + let inventory = inventory.into_inner(); - let Ok(bytes) = client.conn.try_recv() else { - // Client is disconnected. - client.is_disconnected = true; - continue; - }; + let Ok(bytes) = client.conn.try_recv() else { + // Client is disconnected. + client.is_disconnected = true; + continue; + }; - if bytes.is_empty() { - // No data was received. - continue; + if bytes.is_empty() { + // No data was received. + continue; + } + + client.dec.queue_bytes(bytes); + + match handle_one_packet(client, inventory, entity, &mut events) { + Ok(had_packet) => { + if had_packet { + // We decoded one packet, but there might be more. + clients_to_check.push(entity); + } } - - client.dec.queue_bytes(bytes); - - match handle_one_packet(client, inventory, entity, &mut events) { - Ok(had_packet) => { - if had_packet { - // We decoded one packet, but there might be more. - clients_to_check.push(entity); - } - } - Err(e) => { - warn!( - username = %client.username, - uuid = %client.uuid, - ip = %client.ip, - "failed to dispatch events: {e:#}" - ); - client.is_disconnected = true; - } + Err(e) => { + warn!( + username = %client.username, + uuid = %client.uuid, + ip = %client.ip, + "failed to dispatch events: {e:#}" + ); + client.is_disconnected = true; } } - } else { - // Continue to filter the list of clients we need to check until there are none - // left. + } + + // Keep looping until all serverbound packets are decoded. + while !clients_to_check.is_empty() { + world.run_schedule(EventLoopSchedule); + + let (mut clients, mut events) = state.get_mut(world); + clients_to_check.retain(|&entity| { let Ok((_, mut client, mut inventory)) = clients.get_mut(entity) else { - // Client was deleted during the last run of the stage. + // Client must have been deleted during the last run of the schedule. return false; }; @@ -705,12 +710,6 @@ pub(crate) fn event_loop_run_criteria( } }); } - - if clients_to_check.is_empty() { - ShouldRun::No - } else { - ShouldRun::YesAndCheckAgain - } } fn handle_one_packet( @@ -1353,8 +1352,8 @@ fn handle_one_packet( /// is subject to change. /// /// This system must be scheduled to run in the -/// [`EventLoop`](crate::server::EventLoop) stage. Otherwise, it may not -/// function correctly. +/// [`EventLoopSchedule`](crate::server::EventLoopSchedule). Otherwise, it may +/// not function correctly. #[allow(clippy::too_many_arguments)] pub fn default_event_handler( mut clients: Query<(&mut Client, Option<&mut McEntity>)>, diff --git a/crates/valence/src/entity.rs b/crates/valence/src/entity.rs index e15697d..eb148e9 100644 --- a/crates/valence/src/entity.rs +++ b/crates/valence/src/entity.rs @@ -93,16 +93,6 @@ pub(crate) fn update_entities(mut entities: Query<&mut McEntity, Changed) { - for entity in &removed { - warn!( - entity = ?entity, - "A `McEntity` component was removed from the world directly. You must use the \ - `Despawned` marker component instead." - ); - } -} - /// A component for Minecraft entities. For Valence to recognize a /// Minecraft entity, it must have this component attached. /// diff --git a/crates/valence/src/inventory.rs b/crates/valence/src/inventory.rs index 9eb1950..bd89733 100644 --- a/crates/valence/src/inventory.rs +++ b/crates/valence/src/inventory.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::iter::FusedIterator; use bevy_ecs::prelude::*; +use bevy_ecs::schedule::SystemConfigs; use tracing::{debug, warn}; use valence_protocol::item::ItemStack; use valence_protocol::packet::s2c::play::{ @@ -16,6 +17,24 @@ use crate::client::event::{ }; use crate::client::Client; +/// The systems needed for updating the inventories. +pub(crate) fn update_inventories() -> SystemConfigs { + ( + handle_set_held_item, + update_open_inventories, + handle_close_container, + update_client_on_close_inventory.after(update_open_inventories), + update_player_inventories, + handle_click_container + .before(update_open_inventories) + .before(update_player_inventories), + handle_set_slot_creative + .before(update_open_inventories) + .before(update_player_inventories), + ) + .into_configs() +} + #[derive(Debug, Clone, Component)] pub struct Inventory { title: Text, @@ -115,7 +134,7 @@ impl Inventory { } /// Send updates for each client's player inventory. -pub(crate) fn update_player_inventories( +fn update_player_inventories( mut query: Query<(&mut Inventory, &mut Client), Without>, ) { for (mut inventory, mut client) in query.iter_mut() { @@ -180,6 +199,292 @@ pub(crate) fn update_player_inventories( } } +/// Used to indicate that the client with this component is currently viewing +/// an inventory. +#[derive(Debug, Clone, Component)] +pub struct OpenInventory { + /// The Entity with the `Inventory` component that the client is currently + /// viewing. + pub(crate) entity: Entity, + client_modified: u64, +} + +impl OpenInventory { + pub fn new(entity: Entity) -> Self { + OpenInventory { + entity, + client_modified: 0, + } + } + + pub fn entity(&self) -> Entity { + self.entity + } +} + +/// Handles the `OpenInventory` component being added to a client, which +/// indicates that the client is now viewing an inventory, and sends inventory +/// updates to the client when the inventory is modified. +fn update_open_inventories( + mut commands: Commands, + mut clients: Query<(Entity, &mut Client, &mut OpenInventory)>, + mut inventories: Query<&mut Inventory>, +) { + // These operations need to happen in this order. + + // send the inventory contents to all clients that are viewing an inventory + for (client_entity, mut client, mut open_inventory) in clients.iter_mut() { + // validate that the inventory exists + let Ok(inventory) = inventories.get_component::(open_inventory.entity) else { + // the inventory no longer exists, so close the inventory + commands.entity(client_entity).remove::(); + let window_id = client.window_id; + client.write_packet(&CloseScreenS2c { + window_id, + }); + continue; + }; + + if open_inventory.is_added() { + // send the inventory to the client if the client just opened the inventory + client.window_id = client.window_id % 100 + 1; + open_inventory.client_modified = 0; + + let packet = OpenScreenS2c { + window_id: VarInt(client.window_id.into()), + window_type: WindowType::from(inventory.kind), + window_title: (&inventory.title).into(), + }; + client.write_packet(&packet); + + let packet = InventoryS2c { + window_id: client.window_id, + state_id: VarInt(client.inventory_state_id.0), + slots: Cow::Borrowed(inventory.slot_slice()), + // TODO: eliminate clone? + carried_item: Cow::Owned(client.cursor_item.clone()), + }; + client.write_packet(&packet); + } else { + // the client is already viewing the inventory + if inventory.modified == u64::MAX { + // send the entire inventory + client.inventory_state_id += 1; + let packet = InventoryS2c { + window_id: client.window_id, + state_id: VarInt(client.inventory_state_id.0), + slots: Cow::Borrowed(inventory.slot_slice()), + // TODO: eliminate clone? + carried_item: Cow::Owned(client.cursor_item.clone()), + }; + client.write_packet(&packet); + } else { + // send the modified slots + let window_id = client.window_id as i8; + // The slots that were NOT modified by this client, and they need to be sent + let modified_filtered = inventory.modified & !open_inventory.client_modified; + if modified_filtered != 0 { + client.inventory_state_id += 1; + let state_id = client.inventory_state_id.0; + for (i, slot) in inventory.slots.iter().enumerate() { + if (modified_filtered >> i) & 1 == 1 { + client.write_packet(&ScreenHandlerSlotUpdateS2c { + window_id, + state_id: VarInt(state_id), + slot_idx: i as i16, + slot_data: Cow::Borrowed(slot), + }); + } + } + } + } + } + + open_inventory.client_modified = 0; + client.inventory_slots_modified = 0; + } + + // reset the modified flag + for (_, _, open_inventory) in clients.iter_mut() { + // validate that the inventory exists + if let Ok(mut inventory) = inventories.get_component_mut::(open_inventory.entity) + { + inventory.modified = 0; + } + } +} + +/// Handles clients telling the server that they are closing an inventory. +fn handle_close_container(mut commands: Commands, mut events: EventReader) { + for event in events.iter() { + commands.entity(event.client).remove::(); + } +} + +/// Detects when a client's `OpenInventory` component is removed, which +/// indicates that the client is no longer viewing an inventory. +fn update_client_on_close_inventory( + mut removals: RemovedComponents, + mut clients: Query<&mut Client>, +) { + for entity in &mut removals { + if let Ok(mut client) = clients.get_component_mut::(entity) { + let window_id = client.window_id; + client.write_packet(&CloseScreenS2c { window_id }); + } + } +} + +fn handle_click_container( + mut clients: Query<(&mut Client, &mut Inventory, Option<&mut OpenInventory>)>, + mut inventories: Query<&mut Inventory, Without>, + mut events: EventReader, +) { + for event in events.iter() { + let Ok((mut client, mut client_inventory, mut open_inventory)) = + clients.get_mut(event.client) else { + // the client does not exist, ignore + continue; + }; + + // validate the window id + if (event.window_id == 0) != open_inventory.is_none() { + warn!( + "Client sent a click with an invalid window id for current state: window_id = {}, \ + open_inventory present = {}", + event.window_id, + open_inventory.is_some() + ); + continue; + } + + if let Some(open_inventory) = open_inventory.as_mut() { + // the player is interacting with an inventory that is open + let Ok(mut target_inventory) = inventories.get_component_mut::(open_inventory.entity) else { + // the inventory does not exist, ignore + continue; + }; + if client.inventory_state_id.0 != event.state_id { + // client is out of sync, resync, ignore click + debug!("Client state id mismatch, resyncing"); + client.inventory_state_id += 1; + let packet = InventoryS2c { + window_id: client.window_id, + state_id: VarInt(client.inventory_state_id.0), + slots: Cow::Borrowed(target_inventory.slot_slice()), + // TODO: eliminate clone? + carried_item: Cow::Owned(client.cursor_item.clone()), + }; + client.write_packet(&packet); + continue; + } + + client.cursor_item = event.carried_item.clone(); + + for slot in event.slot_changes.clone() { + if (0i16..target_inventory.slot_count() as i16).contains(&slot.idx) { + // the client is interacting with a slot in the target inventory + target_inventory.replace_slot(slot.idx as u16, slot.item); + open_inventory.client_modified |= 1 << slot.idx; + } else { + // the client is interacting with a slot in their own inventory + let slot_id = convert_to_player_slot_id(target_inventory.kind, slot.idx as u16); + client_inventory.replace_slot(slot_id, slot.item); + client.inventory_slots_modified |= 1 << slot_id; + } + } + } else { + // the client is interacting with their own inventory + + if client.inventory_state_id.0 != event.state_id { + // client is out of sync, resync, and ignore the click + debug!("Client state id mismatch, resyncing"); + client.inventory_state_id += 1; + let packet = InventoryS2c { + window_id: client.window_id, + state_id: VarInt(client.inventory_state_id.0), + slots: Cow::Borrowed(client_inventory.slot_slice()), + // TODO: eliminate clone? + carried_item: Cow::Owned(client.cursor_item.clone()), + }; + client.write_packet(&packet); + continue; + } + + // TODO: do more validation on the click + client.cursor_item = event.carried_item.clone(); + for slot in event.slot_changes.clone() { + if (0i16..client_inventory.slot_count() as i16).contains(&slot.idx) { + client_inventory.replace_slot(slot.idx as u16, slot.item); + client.inventory_slots_modified |= 1 << slot.idx; + } else { + // the client is trying to interact with a slot that does not exist, + // ignore + warn!( + "Client attempted to interact with slot {} which does not exist", + slot.idx + ); + } + } + } + } +} + +fn handle_set_slot_creative( + mut clients: Query<(&mut Client, &mut Inventory)>, + mut events: EventReader, +) { + for event in events.iter() { + if let Ok((mut client, mut inventory)) = clients.get_mut(event.client) { + if client.game_mode() != GameMode::Creative { + // the client is not in creative mode, ignore + continue; + } + if event.slot < 0 || event.slot >= inventory.slot_count() as i16 { + // the client is trying to interact with a slot that does not exist, ignore + continue; + } + inventory.replace_slot(event.slot as u16, event.clicked_item.clone()); + inventory.modified &= !(1 << event.slot); // clear the modified bit, since we are about to send the update + client.inventory_state_id += 1; + let state_id = client.inventory_state_id.0; + // HACK: notchian clients rely on the server to send the slot update when in + // creative mode Simply marking the slot as modified is not enough. This was + // discovered because shift-clicking the destroy item slot in creative mode does + // not work without this hack. + client.write_packet(&ScreenHandlerSlotUpdateS2c { + window_id: 0, + state_id: VarInt(state_id), + slot_idx: event.slot, + slot_data: Cow::Borrowed(&event.clicked_item), + }); + } + } +} + +fn handle_set_held_item( + mut clients: Query<&mut Client>, + mut events: EventReader, +) { + for event in events.iter() { + if let Ok(mut client) = clients.get_mut(event.client) { + client.held_item_slot = convert_hotbar_slot_id(event.slot as u16); + } + } +} + +/// Convert a slot that is outside a target inventory's range to a slot that is +/// inside the player's inventory. +fn convert_to_player_slot_id(target_kind: InventoryKind, slot_id: u16) -> u16 { + // the first slot in the player's general inventory + let offset = target_kind.slot_count() as u16; + slot_id - offset + 9 +} + +fn convert_hotbar_slot_id(slot_id: u16) -> u16 { + slot_id + 36 +} + #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum InventoryKind { Generic9x1, @@ -308,295 +613,6 @@ impl From for InventoryKind { } } -/// Used to indicate that the client with this component is currently viewing -/// an inventory. -#[derive(Debug, Clone, Component)] -pub struct OpenInventory { - /// The Entity with the `Inventory` component that the client is currently - /// viewing. - pub(crate) entity: Entity, - client_modified: u64, -} - -impl OpenInventory { - pub fn new(entity: Entity) -> Self { - OpenInventory { - entity, - client_modified: 0, - } - } - - pub fn entity(&self) -> Entity { - self.entity - } -} - -/// Handles the `OpenInventory` component being added to a client, which -/// indicates that the client is now viewing an inventory, and sends inventory -/// updates to the client when the inventory is modified. -pub(crate) fn update_open_inventories( - mut commands: Commands, - mut clients: Query<(Entity, &mut Client, &mut OpenInventory)>, - mut inventories: Query<&mut Inventory>, -) { - // These operations need to happen in this order. - - // send the inventory contents to all clients that are viewing an inventory - for (client_entity, mut client, mut open_inventory) in clients.iter_mut() { - // validate that the inventory exists - let Ok(inventory) = inventories.get_component::(open_inventory.entity) else { - // the inventory no longer exists, so close the inventory - commands.entity(client_entity).remove::(); - let window_id = client.window_id; - client.write_packet(&CloseScreenS2c { - window_id, - }); - continue; - }; - - if open_inventory.is_added() { - // send the inventory to the client if the client just opened the inventory - client.window_id = client.window_id % 100 + 1; - open_inventory.client_modified = 0; - - let packet = OpenScreenS2c { - window_id: VarInt(client.window_id.into()), - window_type: WindowType::from(inventory.kind), - window_title: (&inventory.title).into(), - }; - client.write_packet(&packet); - - let packet = InventoryS2c { - window_id: client.window_id, - state_id: VarInt(client.inventory_state_id.0), - slots: Cow::Borrowed(inventory.slot_slice()), - // TODO: eliminate clone? - carried_item: Cow::Owned(client.cursor_item.clone()), - }; - client.write_packet(&packet); - } else { - // the client is already viewing the inventory - if inventory.modified == u64::MAX { - // send the entire inventory - client.inventory_state_id += 1; - let packet = InventoryS2c { - window_id: client.window_id, - state_id: VarInt(client.inventory_state_id.0), - slots: Cow::Borrowed(inventory.slot_slice()), - // TODO: eliminate clone? - carried_item: Cow::Owned(client.cursor_item.clone()), - }; - client.write_packet(&packet); - } else { - // send the modified slots - let window_id = client.window_id as i8; - // The slots that were NOT modified by this client, and they need to be sent - let modified_filtered = inventory.modified & !open_inventory.client_modified; - if modified_filtered != 0 { - client.inventory_state_id += 1; - let state_id = client.inventory_state_id.0; - for (i, slot) in inventory.slots.iter().enumerate() { - if (modified_filtered >> i) & 1 == 1 { - client.write_packet(&ScreenHandlerSlotUpdateS2c { - window_id, - state_id: VarInt(state_id), - slot_idx: i as i16, - slot_data: Cow::Borrowed(slot), - }); - } - } - } - } - } - - open_inventory.client_modified = 0; - client.inventory_slots_modified = 0; - } - - // reset the modified flag - for (_, _, open_inventory) in clients.iter_mut() { - // validate that the inventory exists - if let Ok(mut inventory) = inventories.get_component_mut::(open_inventory.entity) - { - inventory.modified = 0; - } - } -} - -/// Handles clients telling the server that they are closing an inventory. -pub(crate) fn handle_close_container( - mut commands: Commands, - mut events: EventReader, -) { - for event in events.iter() { - commands.entity(event.client).remove::(); - } -} - -/// Detects when a client's `OpenInventory` component is removed, which -/// indicates that the client is no longer viewing an inventory. -pub(crate) fn update_client_on_close_inventory( - removals: RemovedComponents, - mut clients: Query<&mut Client>, -) { - for entity in removals.iter() { - if let Ok(mut client) = clients.get_component_mut::(entity) { - let window_id = client.window_id; - client.write_packet(&CloseScreenS2c { window_id }); - } - } -} - -pub(crate) fn handle_click_container( - mut clients: Query<(&mut Client, &mut Inventory, Option<&mut OpenInventory>)>, - mut inventories: Query<&mut Inventory, Without>, - mut events: EventReader, -) { - for event in events.iter() { - let Ok((mut client, mut client_inventory, mut open_inventory)) = - clients.get_mut(event.client) else { - // the client does not exist, ignore - continue; - }; - - // validate the window id - if (event.window_id == 0) != open_inventory.is_none() { - warn!( - "Client sent a click with an invalid window id for current state: window_id = {}, \ - open_inventory present = {}", - event.window_id, - open_inventory.is_some() - ); - continue; - } - - if let Some(open_inventory) = open_inventory.as_mut() { - // the player is interacting with an inventory that is open - let Ok(mut target_inventory) = inventories.get_component_mut::(open_inventory.entity) else { - // the inventory does not exist, ignore - continue; - }; - if client.inventory_state_id.0 != event.state_id { - // client is out of sync, resync, ignore click - debug!("Client state id mismatch, resyncing"); - client.inventory_state_id += 1; - let packet = InventoryS2c { - window_id: client.window_id, - state_id: VarInt(client.inventory_state_id.0), - slots: Cow::Borrowed(target_inventory.slot_slice()), - // TODO: eliminate clone? - carried_item: Cow::Owned(client.cursor_item.clone()), - }; - client.write_packet(&packet); - continue; - } - - client.cursor_item = event.carried_item.clone(); - - for slot in event.slot_changes.clone() { - if (0i16..target_inventory.slot_count() as i16).contains(&slot.idx) { - // the client is interacting with a slot in the target inventory - target_inventory.replace_slot(slot.idx as u16, slot.item); - open_inventory.client_modified |= 1 << slot.idx; - } else { - // the client is interacting with a slot in their own inventory - let slot_id = convert_to_player_slot_id(target_inventory.kind, slot.idx as u16); - client_inventory.replace_slot(slot_id, slot.item); - client.inventory_slots_modified |= 1 << slot_id; - } - } - } else { - // the client is interacting with their own inventory - - if client.inventory_state_id.0 != event.state_id { - // client is out of sync, resync, and ignore the click - debug!("Client state id mismatch, resyncing"); - client.inventory_state_id += 1; - let packet = InventoryS2c { - window_id: client.window_id, - state_id: VarInt(client.inventory_state_id.0), - slots: Cow::Borrowed(client_inventory.slot_slice()), - // TODO: eliminate clone? - carried_item: Cow::Owned(client.cursor_item.clone()), - }; - client.write_packet(&packet); - continue; - } - - // TODO: do more validation on the click - client.cursor_item = event.carried_item.clone(); - for slot in event.slot_changes.clone() { - if (0i16..client_inventory.slot_count() as i16).contains(&slot.idx) { - client_inventory.replace_slot(slot.idx as u16, slot.item); - client.inventory_slots_modified |= 1 << slot.idx; - } else { - // the client is trying to interact with a slot that does not exist, - // ignore - warn!( - "Client attempted to interact with slot {} which does not exist", - slot.idx - ); - } - } - } - } -} - -pub(crate) fn handle_set_slot_creative( - mut clients: Query<(&mut Client, &mut Inventory)>, - mut events: EventReader, -) { - for event in events.iter() { - if let Ok((mut client, mut inventory)) = clients.get_mut(event.client) { - if client.game_mode() != GameMode::Creative { - // the client is not in creative mode, ignore - continue; - } - if event.slot < 0 || event.slot >= inventory.slot_count() as i16 { - // the client is trying to interact with a slot that does not exist, ignore - continue; - } - inventory.replace_slot(event.slot as u16, event.clicked_item.clone()); - inventory.modified &= !(1 << event.slot); // clear the modified bit, since we are about to send the update - client.inventory_state_id += 1; - let state_id = client.inventory_state_id.0; - // HACK: notchian clients rely on the server to send the slot update when in - // creative mode Simply marking the slot as modified is not enough. This was - // discovered because shift-clicking the destroy item slot in creative mode does - // not work without this hack. - client.write_packet(&ScreenHandlerSlotUpdateS2c { - window_id: 0, - state_id: VarInt(state_id), - slot_idx: event.slot, - slot_data: Cow::Borrowed(&event.clicked_item), - }); - } - } -} - -pub(crate) fn handle_set_held_item( - mut clients: Query<&mut Client>, - mut events: EventReader, -) { - for event in events.iter() { - if let Ok(mut client) = clients.get_mut(event.client) { - client.held_item_slot = convert_hotbar_slot_id(event.slot as u16); - } - } -} - -/// Convert a slot that is outside a target inventory's range to a slot that is -/// inside the player's inventory. -fn convert_to_player_slot_id(target_kind: InventoryKind, slot_id: u16) -> u16 { - // the first slot in the player's general inventory - let offset = target_kind.slot_count() as u16; - slot_id - offset + 9 -} - -fn convert_hotbar_slot_id(slot_id: u16) -> u16 { - slot_id + 36 -} - #[cfg(test)] mod test { use bevy_app::App; diff --git a/crates/valence/src/lib.rs b/crates/valence/src/lib.rs index 218be40..68c10fe 100644 --- a/crates/valence/src/lib.rs +++ b/crates/valence/src/lib.rs @@ -49,7 +49,7 @@ pub mod view; pub mod prelude { pub use async_trait::async_trait; - pub use bevy_app::App; + pub use bevy_app::prelude::*; pub use bevy_ecs::prelude::*; pub use biome::{Biome, BiomeId}; pub use client::Client; @@ -70,7 +70,7 @@ pub mod prelude { pub use protocol::text::{Color, Text, TextFormat}; pub use protocol::types::GameMode; pub use protocol::username::Username; - pub use server::{EventLoop, NewClientInfo, Server, SharedServer}; + pub use server::{EventLoopSchedule, EventLoopSet, NewClientInfo, Server, SharedServer}; pub use uuid::Uuid; pub use valence_nbt::Compound; pub use valence_protocol::block::BlockKind; diff --git a/crates/valence/src/player_list.rs b/crates/valence/src/player_list.rs index ed63873..f5d2693 100644 --- a/crates/valence/src/player_list.rs +++ b/crates/valence/src/player_list.rs @@ -5,6 +5,7 @@ use std::iter::FusedIterator; use std::mem; use bevy_ecs::prelude::*; +use bevy_ecs::schedule::SystemConfigs; use tracing::warn; use uuid::Uuid; use valence_protocol::packet::s2c::play::player_list::{ @@ -52,7 +53,7 @@ impl PlayerList { /// Returns a set of systems for maintaining the player list in a reasonable /// default way. When clients connect, they are added to the player list. /// When clients disconnect, they are removed from the player list. - pub fn default_system_set() -> SystemSet { + pub fn default_systems() -> SystemConfigs { fn add_new_clients_to_player_list( clients: Query<&Client, Added>, mut player_list: ResMut, @@ -79,9 +80,11 @@ impl PlayerList { } } - SystemSet::new() - .with_system(add_new_clients_to_player_list) - .with_system(remove_disconnected_clients_from_player_list) + ( + add_new_clients_to_player_list, + remove_disconnected_clients_from_player_list, + ) + .into_configs() } } diff --git a/crates/valence/src/server.rs b/crates/valence/src/server.rs index 53cd552..3475451 100644 --- a/crates/valence/src/server.rs +++ b/crates/valence/src/server.rs @@ -2,14 +2,13 @@ use std::iter::FusedIterator; use std::net::{IpAddr, SocketAddr}; use std::ops::Deref; use std::sync::Arc; -use std::thread; -use std::time::{Duration, Instant}; +use std::time::Duration; use anyhow::ensure; use bevy_app::prelude::*; -use bevy_app::AppExit; -use bevy_ecs::event::ManualEventReader; +use bevy_app::{ScheduleRunnerPlugin, ScheduleRunnerSettings}; use bevy_ecs::prelude::*; +use bevy_ecs::schedule::ScheduleLabel; use flume::{Receiver, Sender}; use rand::rngs::OsRng; use rsa::{PublicKeyParts, RsaPrivateKey}; @@ -22,23 +21,17 @@ use valence_protocol::types::Property; use valence_protocol::username::Username; use crate::biome::{validate_biomes, Biome, BiomeId}; -use crate::client::event::{event_loop_run_criteria, register_client_events}; +use crate::client::event::{register_client_events, run_event_loop}; use crate::client::{update_clients, Client}; use crate::config::{AsyncCallbacks, ConnectionMode, ServerPlugin}; use crate::dimension::{validate_dimensions, Dimension, DimensionId}; -use crate::entity::{ - check_entity_invariants, deinit_despawned_entities, init_entities, update_entities, - McEntityManager, -}; +use crate::entity::{deinit_despawned_entities, init_entities, update_entities, McEntityManager}; use crate::instance::{ check_instance_invariants, update_instances_post_client, update_instances_pre_client, Instance, }; -use crate::inventory::{ - handle_click_container, handle_close_container, handle_set_held_item, handle_set_slot_creative, - update_client_on_close_inventory, update_open_inventories, update_player_inventories, - Inventory, InventoryKind, -}; +use crate::inventory::update_inventories; use crate::player_list::{update_player_list, PlayerList}; +use crate::prelude::{Inventory, InventoryKind}; use crate::server::connect::do_accept_loop; use crate::Despawned; @@ -98,8 +91,6 @@ struct SharedServerInner { /// Contains info about dimensions, biomes, and chats. /// Sent to all clients when joining. registry_codec: Compound, - /// The instant the server was started. - start_instant: Instant, /// Sender for new clients past the login stage. new_clients_send: Sender, /// Receiver for new clients past the login stage. @@ -205,11 +196,6 @@ impl SharedServer { pub(crate) fn registry_codec(&self) -> &Compound { &self.0.registry_codec } - - /// Returns the instant the server was started. - pub fn start_instant(&self) -> Instant { - self.0.start_instant - } } /// Contains information about a new client joining the server. @@ -280,7 +266,6 @@ pub fn build_plugin( dimensions: plugin.dimensions.clone(), biomes: plugin.biomes.clone(), registry_codec, - start_instant: Instant::now(), new_clients_send, new_clients_recv, connection_sema: Arc::new(Semaphore::new(plugin.max_connections)), @@ -306,7 +291,7 @@ pub fn build_plugin( let shared = server.shared.clone(); - // Exclusive system to spawn new clients. Should run before everything else. + // System to spawn new clients. let spawn_new_clients = move |world: &mut World| { for _ in 0..shared.0.new_clients_recv.len() { let Ok(client) = shared.0.new_clients_recv.try_recv() else { @@ -319,96 +304,72 @@ pub fn build_plugin( let shared = server.shared.clone(); - // Start accepting connections in PostStartup to allow user startup code to run - // first. - app.add_startup_system_to_stage(StartupStage::PostStartup, start_accept_loop); - // Insert resources. app.insert_resource(server) .insert_resource(McEntityManager::new()) .insert_resource(PlayerList::new()); register_client_events(&mut app.world); - // Add core systems and stages. User code is expected to run in - // `CoreStage::Update` and `EventLoop`. - app.add_system_to_stage(CoreStage::PreUpdate, spawn_new_clients) - .add_stage_before( - CoreStage::Update, - EventLoop, - SystemStage::parallel().with_run_criteria(event_loop_run_criteria), + // Add the event loop schedule. + let mut event_loop = Schedule::new(); + event_loop.configure_set(EventLoopSet); + event_loop.set_default_base_set(EventLoopSet); + + app.add_schedule(EventLoopSchedule, event_loop); + + // Make the app loop forever at the configured TPS. + { + let tick_period = Duration::from_secs_f64((shared.tps() as f64).recip()); + + app.insert_resource(ScheduleRunnerSettings::run_loop(tick_period)) + .add_plugin(ScheduleRunnerPlugin); + } + + // Start accepting connections in `PostStartup` to allow user startup code to + // run first. + app.add_system( + start_accept_loop + .in_schedule(CoreSchedule::Startup) + .in_base_set(StartupSet::PostStartup), + ); + + // Add `CoreSet:::PreUpdate` systems. + app.add_systems( + (spawn_new_clients.before(run_event_loop), run_event_loop).in_base_set(CoreSet::PreUpdate), + ); + + // Add internal valence systems that run after `CoreSet::Update`. + app.add_systems( + ( + init_entities, + check_instance_invariants, + update_player_list.before(update_instances_pre_client), + update_instances_pre_client.after(init_entities), + update_clients.after(update_instances_pre_client), + update_instances_post_client.after(update_clients), + deinit_despawned_entities.after(update_instances_post_client), + despawn_marked_entities.after(deinit_despawned_entities), + update_entities.after(despawn_marked_entities), ) - .add_system_set_to_stage( - CoreStage::PostUpdate, - SystemSet::new() - .label("valence_core") - .with_system(init_entities) - .with_system(check_entity_invariants) - .with_system(check_instance_invariants.after(check_entity_invariants)) - .with_system(update_player_list.before(update_instances_pre_client)) - .with_system(update_instances_pre_client.after(init_entities)) - .with_system(update_clients.after(update_instances_pre_client)) - .with_system(update_instances_post_client.after(update_clients)) - .with_system(deinit_despawned_entities.after(update_instances_post_client)) - .with_system(despawn_marked_entities.after(deinit_despawned_entities)) - .with_system(update_entities.after(despawn_marked_entities)), - ) - .add_system_set_to_stage( - CoreStage::PostUpdate, - SystemSet::new() - .label("inventory") - .before("valence_core") - .with_system(handle_set_held_item) - .with_system(update_open_inventories) - .with_system(handle_close_container) - .with_system(update_client_on_close_inventory.after(update_open_inventories)) - .with_system(update_player_inventories) - .with_system( - handle_click_container - .before(update_open_inventories) - .before(update_player_inventories), - ) - .with_system( - handle_set_slot_creative - .before(update_open_inventories) - .before(update_player_inventories), - ), - ) - .add_system_to_stage(CoreStage::Last, inc_current_tick); - - let tick_duration = Duration::from_secs_f64((shared.tps() as f64).recip()); - - // Overwrite the app's runner. - app.set_runner(move |mut app: App| { - let mut app_exit_event_reader = ManualEventReader::::default(); - - loop { - let tick_start = Instant::now(); - - // Stop the server if there was an AppExit event. - if let Some(app_exit_events) = app.world.get_resource_mut::>() { - if app_exit_event_reader - .iter(&app_exit_events) - .last() - .is_some() - { - return; - } - } - - // Run the scheduled stages. - app.update(); - - // Sleep until the next tick. - thread::sleep(tick_duration.saturating_sub(tick_start.elapsed())); - } - }); + .in_base_set(CoreSet::PostUpdate), + ) + .add_systems( + update_inventories() + .in_base_set(CoreSet::PostUpdate) + .before(init_entities), + ) + .add_system(increment_tick_counter.in_base_set(CoreSet::Last)); Ok(()) } -/// The stage label for the special "event loop" stage. -#[derive(StageLabel)] -pub struct EventLoop; +/// The [`ScheduleLabel`] for the event loop [`Schedule`]. +#[derive(ScheduleLabel, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub struct EventLoopSchedule; + +/// The default base set for the event loop [`Schedule`]. +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub struct EventLoopSet; /// Despawns all the entities marked as despawned with the [`Despawned`] /// component. @@ -418,7 +379,7 @@ fn despawn_marked_entities(mut commands: Commands, entities: Query) { +fn increment_tick_counter(mut server: ResMut) { server.current_tick += 1; } diff --git a/crates/valence/src/unit_test/util.rs b/crates/valence/src/unit_test/util.rs index 3cff187..75e4870 100644 --- a/crates/valence/src/unit_test/util.rs +++ b/crates/valence/src/unit_test/util.rs @@ -1,7 +1,8 @@ use std::sync::{Arc, Mutex}; -use bevy_app::App; +use bevy_app::{App, CoreSchedule}; use bevy_ecs::prelude::Entity; +use bevy_ecs::schedule::{LogLevel, ScheduleBuildSettings}; use bytes::BytesMut; use valence_protocol::codec::{PacketDecoder, PacketEncoder}; use valence_protocol::packet::S2cPlayPacket; @@ -164,17 +165,28 @@ pub fn scenario_single_client(app: &mut App) -> (Entity, MockClientHelper) { .with_compression_threshold(None) .with_connection_mode(ConnectionMode::Offline), ); + let server = app.world.resource::(); let instance = server.new_instance(DimensionId::default()); let instance_ent = app.world.spawn(instance).id(); let info = gen_client_info("test"); let (mut client, client_helper) = create_mock_client(info); + // HACK: needed so client does not get disconnected on first update client.set_instance(instance_ent); let client_ent = app .world .spawn((client, Inventory::new(InventoryKind::Player))) .id(); + + // Print warnings if there are ambiguities in the schedule. + app.edit_schedule(CoreSchedule::Main, |schedule| { + schedule.set_build_settings(ScheduleBuildSettings { + ambiguity_detection: LogLevel::Warn, + ..Default::default() + }); + }); + (client_ent, client_helper) } diff --git a/crates/valence_anvil/Cargo.toml b/crates/valence_anvil/Cargo.toml index 0d8e040..5f3d0b5 100644 --- a/crates/valence_anvil/Cargo.toml +++ b/crates/valence_anvil/Cargo.toml @@ -20,7 +20,7 @@ valence_nbt = { version = "0.5.0", path = "../valence_nbt" } [dev-dependencies] anyhow = "1.0.68" -bevy_ecs = "0.9.1" +bevy_ecs = { git = "https://github.com/bevyengine/bevy/", rev = "2ea00610188dce1eba1172a3ded8244570189230" } clap = "4.1.4" criterion = "0.4.0" flume = "0.10.14" diff --git a/crates/valence_anvil/examples/anvil_loading.rs b/crates/valence_anvil/examples/anvil_loading.rs index 322b809..dd860d9 100644 --- a/crates/valence_anvil/examples/anvil_loading.rs +++ b/crates/valence_anvil/examples/anvil_loading.rs @@ -40,13 +40,17 @@ pub fn main() { App::new() .add_plugin(ServerPlugin::new(())) - .add_system_to_stage(EventLoop, default_event_handler) - .add_system_set(PlayerList::default_system_set()) .add_startup_system(setup) - .add_system(init_clients) - .add_system(remove_unviewed_chunks.after(init_clients)) - .add_system(update_client_views.after(remove_unviewed_chunks)) - .add_system(send_recv_chunks.after(update_client_views)) + .add_system(default_event_handler.in_schedule(EventLoopSchedule)) + .add_systems( + ( + init_clients, + remove_unviewed_chunks, + update_client_views, + send_recv_chunks, + ) + .chain(), + ) .add_system(despawn_disconnected_clients) .run(); }