Update to Bevy 0.10 (#265)

## Description

Fix #264

Bevy version is pinned to a recent commit on the main branch until 0.10
is released.

## Test Plan

Run examples and tests. Behavior should be the same.

scheduling is a bit tricky so I may have made some subtle mistakes.
This commit is contained in:
Ryan Johnson 2023-03-04 03:35:11 -08:00 committed by GitHub
parent cc2571d7e6
commit 62f882eec7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 591 additions and 571 deletions

View file

@ -9,7 +9,7 @@ const SPAWN_Y: i32 = 64;
pub fn build_app(app: &mut App) { pub fn build_app(app: &mut App) {
app.add_plugin(ServerPlugin::new(())) 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_startup_system(setup)
.add_system(init_clients) .add_system(init_clients)
.add_system(despawn_disconnected_clients); .add_system(despawn_disconnected_clients);

View file

@ -16,8 +16,8 @@ anyhow = "1.0.65"
arrayvec = "0.7.2" arrayvec = "0.7.2"
async-trait = "0.1.60" async-trait = "0.1.60"
base64 = "0.21.0" base64 = "0.21.0"
bevy_app = "0.9.1" bevy_app = { git = "https://github.com/bevyengine/bevy/", rev = "2ea00610188dce1eba1172a3ded8244570189230" }
bevy_ecs = "0.9.1" bevy_ecs = { git = "https://github.com/bevyengine/bevy/", rev = "2ea00610188dce1eba1172a3ded8244570189230" }
bitfield-struct = "0.3.1" bitfield-struct = "0.3.1"
bytes = "1.2.1" bytes = "1.2.1"
flume = "0.10.14" flume = "0.10.14"

View file

@ -1,6 +1,5 @@
use std::time::Instant; use std::time::Instant;
use bevy_app::{App, CoreStage};
use valence::client::despawn_disconnected_clients; use valence::client::despawn_disconnected_clients;
use valence::client::event::default_event_handler; use valence::client::event::default_event_handler;
use valence::instance::{Chunk, Instance}; use valence::instance::{Chunk, Instance};
@ -22,12 +21,14 @@ fn main() {
.with_max_connections(50_000), .with_max_connections(50_000),
) )
.add_startup_system(setup) .add_startup_system(setup)
.add_system_to_stage(EventLoop, default_event_handler) .add_systems((
.add_system_to_stage(CoreStage::First, record_tick_start_time) default_event_handler.in_schedule(EventLoopSchedule),
.add_system_to_stage(CoreStage::Last, print_tick_time) record_tick_start_time.in_base_set(CoreSet::First),
.add_system(init_clients) print_tick_time.in_base_set(CoreSet::Last),
.add_system(despawn_disconnected_clients) init_clients,
.add_system_set(PlayerList::default_system_set()) despawn_disconnected_clients,
))
.add_systems(PlayerList::default_systems())
.run(); .run();
} }

View file

@ -32,11 +32,13 @@ pub fn main() {
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
), ),
) )
.add_system_to_stage(EventLoop, default_event_handler)
.add_startup_system(setup) .add_startup_system(setup)
.add_system(init_clients) .add_systems((
.add_system(despawn_disconnected_clients) default_event_handler.in_schedule(EventLoopSchedule),
.add_system_set(PlayerList::default_system_set()) init_clients,
despawn_disconnected_clients,
))
.add_systems(PlayerList::default_systems())
.run(); .run();
} }

View file

@ -13,12 +13,14 @@ pub fn main() {
App::new() App::new()
.add_plugin(ServerPlugin::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_startup_system(setup)
.add_system(init_clients) .add_systems((
.add_system(despawn_disconnected_clients) default_event_handler.in_schedule(EventLoopSchedule),
event_handler.in_schedule(EventLoopSchedule),
init_clients,
despawn_disconnected_clients,
))
.add_systems(PlayerList::default_systems())
.run(); .run();
} }

View file

@ -12,15 +12,20 @@ pub fn main() {
App::new() App::new()
.add_plugin(ServerPlugin::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_startup_system(setup)
.add_system(init_clients) .add_system(init_clients)
.add_system(despawn_disconnected_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(); .run();
} }

View file

@ -12,12 +12,17 @@ pub fn main() {
App::new() App::new()
.add_plugin(ServerPlugin::new(()).with_connection_mode(ConnectionMode::Offline)) .add_plugin(ServerPlugin::new(()).with_connection_mode(ConnectionMode::Offline))
.add_startup_system(setup) .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_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(despawn_disconnected_clients)
.add_system_set(PlayerList::default_system_set())
.run(); .run();
} }
@ -65,9 +70,10 @@ fn handle_message_events(mut clients: Query<&mut Client>, mut messages: EventRea
.color(Color::YELLOW) .color(Color::YELLOW)
+ message.into_text().not_bold().color(Color::WHITE); + 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()); client.send_message(formatted.clone());
}) }
} }
} }

View file

@ -11,12 +11,13 @@ pub fn main() {
App::new() App::new()
.add_plugin(ServerPlugin::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_startup_system(setup)
.add_system(init_clients) .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) .add_system(despawn_disconnected_clients)
.run(); .run();
} }

View file

@ -22,11 +22,10 @@ pub fn main() {
App::new() App::new()
.add_plugin(ServerPlugin::new(())) .add_plugin(ServerPlugin::new(()))
.add_startup_system(setup) .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_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(despawn_disconnected_clients)
.add_system_set(PlayerList::default_system_set())
.add_system(teleport_oob_clients) .add_system(teleport_oob_clients)
.run(); .run();
} }

View file

@ -27,15 +27,16 @@ pub fn main() {
grass_color: Some(0x00ff00), grass_color: Some(0x00ff00),
..Default::default() ..Default::default()
}])) }]))
.add_system_to_stage(EventLoop, default_event_handler)
.add_system_set(PlayerList::default_system_set())
.add_startup_system(setup) .add_startup_system(setup)
.add_system(init_clients) .add_system(init_clients)
.add_system(despawn_disconnected_clients) .add_systems((default_event_handler, toggle_cell_on_dig).in_schedule(EventLoopSchedule))
.add_system_to_stage(EventLoop, toggle_cell_on_dig) .add_systems(PlayerList::default_systems())
.add_system(update_board) .add_systems((
.add_system(pause_on_crouch) despawn_disconnected_clients,
.add_system(reset_oob_clients) update_board,
pause_on_crouch,
reset_oob_clients,
))
.run(); .run();
} }

View file

@ -24,12 +24,12 @@ fn main() {
App::new() App::new()
.add_plugin(ServerPlugin::new(())) .add_plugin(ServerPlugin::new(()))
.add_system_to_stage(EventLoop, default_event_handler)
.add_startup_system(setup) .add_startup_system(setup)
.add_system(init_clients) .add_system(init_clients)
.add_system(default_event_handler.in_schedule(EventLoopSchedule))
.add_systems(PlayerList::default_systems())
.add_system(update_sphere) .add_system(update_sphere)
.add_system(despawn_disconnected_clients) .add_system(despawn_disconnected_clients)
.add_system_set(PlayerList::default_system_set())
.run(); .run();
} }

View file

@ -10,12 +10,12 @@ pub fn main() {
App::new() App::new()
.add_plugin(ServerPlugin::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_startup_system(setup)
.add_system(init_clients) .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) .add_system(despawn_disconnected_clients)
.run(); .run();
} }

View file

@ -9,11 +9,10 @@ pub fn main() {
App::new() App::new()
.add_plugin(ServerPlugin::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_startup_system(setup)
.add_system(init_clients) .add_system(init_clients)
.add_systems((default_event_handler, interpret_command).in_schedule(EventLoopSchedule))
.add_systems(PlayerList::default_systems())
.add_system(despawn_disconnected_clients) .add_system(despawn_disconnected_clients)
.run(); .run();
} }

View file

@ -28,13 +28,16 @@ pub fn main() {
App::new() App::new()
.add_plugin(ServerPlugin::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(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(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(); .run();
} }

View file

@ -9,10 +9,10 @@ pub fn main() {
App::new() App::new()
.add_plugin(ServerPlugin::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_startup_system(setup)
.add_system(init_clients) .add_system(init_clients)
.add_system(default_event_handler.in_schedule(EventLoopSchedule))
.add_systems(PlayerList::default_systems())
.add_system(despawn_disconnected_clients) .add_system(despawn_disconnected_clients)
.add_system(manage_particles) .add_system(manage_particles)
.run(); .run();

View file

@ -14,11 +14,14 @@ fn main() {
App::new() App::new()
.add_plugin(ServerPlugin::new(())) .add_plugin(ServerPlugin::new(()))
.add_startup_system(setup) .add_startup_system(setup)
.add_system_to_stage(EventLoop, default_event_handler) .add_system(default_event_handler.in_schedule(EventLoopSchedule))
.add_system(init_clients) .add_systems(PlayerList::default_systems())
.add_system(update_player_list) .add_systems((
.add_system(despawn_disconnected_clients) init_clients,
.add_system(remove_disconnected_clients_from_player_list) update_player_list,
remove_disconnected_clients_from_player_list,
despawn_disconnected_clients,
))
.run(); .run();
} }

View file

@ -12,12 +12,17 @@ pub fn main() {
App::new() App::new()
.add_plugin(ServerPlugin::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_startup_system(setup)
.add_system(init_clients) .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) .add_system(despawn_disconnected_clients)
.run(); .run();
} }
@ -33,7 +38,7 @@ fn setup(mut commands: Commands, server: Res<Server>) {
for z in -25..25 { for z in -25..25 {
for x 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<Server>) {
let mut sheep = McEntity::new(EntityKind::Sheep, instance_ent); let mut sheep = McEntity::new(EntityKind::Sheep, instance_ent);
sheep.set_position([0.0, SPAWN_Y as f64 + 1.0, 2.0]); 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); commands.spawn(sheep);
} }

View file

@ -43,14 +43,19 @@ pub fn main() {
App::new() App::new()
.add_plugin(ServerPlugin::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_startup_system(setup)
.add_system(init_clients) .add_system(default_event_handler.in_schedule(EventLoopSchedule))
.add_system(remove_unviewed_chunks.after(init_clients)) .add_systems(
.add_system(update_client_views.after(remove_unviewed_chunks)) (
.add_system(send_recv_chunks.after(update_client_views)) init_clients,
remove_unviewed_chunks,
update_client_views,
send_recv_chunks,
)
.chain(),
)
.add_system(despawn_disconnected_clients) .add_system(despawn_disconnected_clients)
.add_systems(PlayerList::default_systems())
.run(); .run();
} }

View file

@ -10,10 +10,10 @@ pub fn main() {
App::new() App::new()
.add_plugin(ServerPlugin::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_startup_system(setup)
.add_system(init_clients) .add_system(init_clients)
.add_system(default_event_handler.in_schedule(EventLoopSchedule))
.add_systems(PlayerList::default_systems())
.add_system(despawn_disconnected_clients) .add_system(despawn_disconnected_clients)
.run(); .run();
} }

View file

@ -633,32 +633,33 @@ pub(crate) fn update_clients(
instances: Query<&Instance>, instances: Query<&Instance>,
entities: Query<&McEntity>, entities: Query<&McEntity>,
) { ) {
// TODO: what batch size to use? clients
clients.par_for_each_mut(16, |(entity_id, mut client, self_entity)| { .par_iter_mut()
if !client.is_disconnected() { .for_each_mut(|(entity_id, mut client, self_entity)| {
if let Err(e) = update_one_client( if !client.is_disconnected() {
&mut client, if let Err(e) = update_one_client(
self_entity, &mut client,
entity_id, self_entity,
&instances, entity_id,
&entities, &instances,
&server, &entities,
) { &server,
client.write_packet(&DisconnectS2c { ) {
reason: Text::from("").into(), client.write_packet(&DisconnectS2c {
}); reason: Text::from("").into(),
client.is_disconnected = true; });
warn!( client.is_disconnected = true;
username = %client.username, warn!(
uuid = %client.uuid, username = %client.username,
ip = %client.ip, uuid = %client.uuid,
"error updating client: {e:#}" ip = %client.ip,
); "error updating client: {e:#}"
);
}
} }
}
client.is_new = false; client.is_new = false;
}); });
} }
#[inline] #[inline]

View file

@ -2,8 +2,7 @@ use std::cmp;
use anyhow::bail; use anyhow::bail;
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_ecs::schedule::ShouldRun; use bevy_ecs::system::{SystemParam, SystemState};
use bevy_ecs::system::SystemParam;
use glam::{DVec3, Vec3}; use glam::{DVec3, Vec3};
use paste::paste; use paste::paste;
use tracing::warn; use tracing::warn;
@ -34,6 +33,7 @@ use valence_protocol::types::{Difficulty, Direction, Hand};
use crate::client::Client; use crate::client::Client;
use crate::entity::{EntityAnimation, EntityKind, McEntity, TrackedData}; use crate::entity::{EntityAnimation, EntityKind, McEntity, TrackedData};
use crate::inventory::Inventory; use crate::inventory::Inventory;
use crate::server::EventLoopSchedule;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct QueryBlockNbt { pub struct QueryBlockNbt {
@ -635,57 +635,62 @@ events! {
} }
} }
pub(crate) fn event_loop_run_criteria( /// An exclusive system for running the event loop schedule.
mut clients: Query<(Entity, &mut Client, &mut Inventory)>, #[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<Vec<Entity>>, mut clients_to_check: Local<Vec<Entity>>,
mut events: ClientEvents, ) {
) -> ShouldRun { let (mut clients, mut events) = state.get_mut(world);
if clients_to_check.is_empty() {
// First run of the criteria. Prepare packets.
update_all_event_buffers(&mut events); update_all_event_buffers(&mut events);
for (entity, client, inventory) in &mut clients { for (entity, client, inventory) in &mut clients {
let client = client.into_inner(); let client = client.into_inner();
let inventory = inventory.into_inner(); let inventory = inventory.into_inner();
let Ok(bytes) = client.conn.try_recv() else { let Ok(bytes) = client.conn.try_recv() else {
// Client is disconnected. // Client is disconnected.
client.is_disconnected = true; client.is_disconnected = true;
continue; continue;
}; };
if bytes.is_empty() { if bytes.is_empty() {
// No data was received. // No data was received.
continue; 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);
}
} }
Err(e) => {
client.dec.queue_bytes(bytes); warn!(
username = %client.username,
match handle_one_packet(client, inventory, entity, &mut events) { uuid = %client.uuid,
Ok(had_packet) => { ip = %client.ip,
if had_packet { "failed to dispatch events: {e:#}"
// We decoded one packet, but there might be more. );
clients_to_check.push(entity); 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| { clients_to_check.retain(|&entity| {
let Ok((_, mut client, mut inventory)) = clients.get_mut(entity) else { 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; 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( fn handle_one_packet(
@ -1353,8 +1352,8 @@ fn handle_one_packet(
/// is subject to change. /// is subject to change.
/// ///
/// This system must be scheduled to run in the /// This system must be scheduled to run in the
/// [`EventLoop`](crate::server::EventLoop) stage. Otherwise, it may not /// [`EventLoopSchedule`](crate::server::EventLoopSchedule). Otherwise, it may
/// function correctly. /// not function correctly.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn default_event_handler( pub fn default_event_handler(
mut clients: Query<(&mut Client, Option<&mut McEntity>)>, mut clients: Query<(&mut Client, Option<&mut McEntity>)>,

View file

@ -93,16 +93,6 @@ pub(crate) fn update_entities(mut entities: Query<&mut McEntity, Changed<McEntit
} }
} }
pub(crate) fn check_entity_invariants(removed: RemovedComponents<McEntity>) {
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 /// A component for Minecraft entities. For Valence to recognize a
/// Minecraft entity, it must have this component attached. /// Minecraft entity, it must have this component attached.
/// ///

View file

@ -2,6 +2,7 @@ use std::borrow::Cow;
use std::iter::FusedIterator; use std::iter::FusedIterator;
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_ecs::schedule::SystemConfigs;
use tracing::{debug, warn}; use tracing::{debug, warn};
use valence_protocol::item::ItemStack; use valence_protocol::item::ItemStack;
use valence_protocol::packet::s2c::play::{ use valence_protocol::packet::s2c::play::{
@ -16,6 +17,24 @@ use crate::client::event::{
}; };
use crate::client::Client; 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)] #[derive(Debug, Clone, Component)]
pub struct Inventory { pub struct Inventory {
title: Text, title: Text,
@ -115,7 +134,7 @@ impl Inventory {
} }
/// Send updates for each client's player 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<OpenInventory>>, mut query: Query<(&mut Inventory, &mut Client), Without<OpenInventory>>,
) { ) {
for (mut inventory, mut client) in query.iter_mut() { 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::<Inventory>(open_inventory.entity) else {
// the inventory no longer exists, so close the inventory
commands.entity(client_entity).remove::<OpenInventory>();
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::<Inventory>(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<CloseHandledScreen>) {
for event in events.iter() {
commands.entity(event.client).remove::<OpenInventory>();
}
}
/// 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<OpenInventory>,
mut clients: Query<&mut Client>,
) {
for entity in &mut removals {
if let Ok(mut client) = clients.get_component_mut::<Client>(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<Client>>,
mut events: EventReader<ClickSlot>,
) {
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::<Inventory>(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<CreativeInventoryAction>,
) {
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<UpdateSelectedSlot>,
) {
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)] #[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum InventoryKind { pub enum InventoryKind {
Generic9x1, Generic9x1,
@ -308,295 +613,6 @@ impl From<WindowType> 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::<Inventory>(open_inventory.entity) else {
// the inventory no longer exists, so close the inventory
commands.entity(client_entity).remove::<OpenInventory>();
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::<Inventory>(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<CloseHandledScreen>,
) {
for event in events.iter() {
commands.entity(event.client).remove::<OpenInventory>();
}
}
/// 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<OpenInventory>,
mut clients: Query<&mut Client>,
) {
for entity in removals.iter() {
if let Ok(mut client) = clients.get_component_mut::<Client>(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<Client>>,
mut events: EventReader<ClickSlot>,
) {
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::<Inventory>(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<CreativeInventoryAction>,
) {
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<UpdateSelectedSlot>,
) {
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)] #[cfg(test)]
mod test { mod test {
use bevy_app::App; use bevy_app::App;

View file

@ -49,7 +49,7 @@ pub mod view;
pub mod prelude { pub mod prelude {
pub use async_trait::async_trait; pub use async_trait::async_trait;
pub use bevy_app::App; pub use bevy_app::prelude::*;
pub use bevy_ecs::prelude::*; pub use bevy_ecs::prelude::*;
pub use biome::{Biome, BiomeId}; pub use biome::{Biome, BiomeId};
pub use client::Client; pub use client::Client;
@ -70,7 +70,7 @@ pub mod prelude {
pub use protocol::text::{Color, Text, TextFormat}; pub use protocol::text::{Color, Text, TextFormat};
pub use protocol::types::GameMode; pub use protocol::types::GameMode;
pub use protocol::username::Username; 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 uuid::Uuid;
pub use valence_nbt::Compound; pub use valence_nbt::Compound;
pub use valence_protocol::block::BlockKind; pub use valence_protocol::block::BlockKind;

View file

@ -5,6 +5,7 @@ use std::iter::FusedIterator;
use std::mem; use std::mem;
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_ecs::schedule::SystemConfigs;
use tracing::warn; use tracing::warn;
use uuid::Uuid; use uuid::Uuid;
use valence_protocol::packet::s2c::play::player_list::{ 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 /// 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. /// default way. When clients connect, they are added to the player list.
/// When clients disconnect, they are removed from 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( fn add_new_clients_to_player_list(
clients: Query<&Client, Added<Client>>, clients: Query<&Client, Added<Client>>,
mut player_list: ResMut<PlayerList>, mut player_list: ResMut<PlayerList>,
@ -79,9 +80,11 @@ impl PlayerList {
} }
} }
SystemSet::new() (
.with_system(add_new_clients_to_player_list) add_new_clients_to_player_list,
.with_system(remove_disconnected_clients_from_player_list) remove_disconnected_clients_from_player_list,
)
.into_configs()
} }
} }

View file

@ -2,14 +2,13 @@ use std::iter::FusedIterator;
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::time::Duration;
use std::time::{Duration, Instant};
use anyhow::ensure; use anyhow::ensure;
use bevy_app::prelude::*; use bevy_app::prelude::*;
use bevy_app::AppExit; use bevy_app::{ScheduleRunnerPlugin, ScheduleRunnerSettings};
use bevy_ecs::event::ManualEventReader;
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_ecs::schedule::ScheduleLabel;
use flume::{Receiver, Sender}; use flume::{Receiver, Sender};
use rand::rngs::OsRng; use rand::rngs::OsRng;
use rsa::{PublicKeyParts, RsaPrivateKey}; use rsa::{PublicKeyParts, RsaPrivateKey};
@ -22,23 +21,17 @@ use valence_protocol::types::Property;
use valence_protocol::username::Username; use valence_protocol::username::Username;
use crate::biome::{validate_biomes, Biome, BiomeId}; 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::client::{update_clients, Client};
use crate::config::{AsyncCallbacks, ConnectionMode, ServerPlugin}; use crate::config::{AsyncCallbacks, ConnectionMode, ServerPlugin};
use crate::dimension::{validate_dimensions, Dimension, DimensionId}; use crate::dimension::{validate_dimensions, Dimension, DimensionId};
use crate::entity::{ use crate::entity::{deinit_despawned_entities, init_entities, update_entities, McEntityManager};
check_entity_invariants, deinit_despawned_entities, init_entities, update_entities,
McEntityManager,
};
use crate::instance::{ use crate::instance::{
check_instance_invariants, update_instances_post_client, update_instances_pre_client, Instance, check_instance_invariants, update_instances_post_client, update_instances_pre_client, Instance,
}; };
use crate::inventory::{ use crate::inventory::update_inventories;
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::player_list::{update_player_list, PlayerList}; use crate::player_list::{update_player_list, PlayerList};
use crate::prelude::{Inventory, InventoryKind};
use crate::server::connect::do_accept_loop; use crate::server::connect::do_accept_loop;
use crate::Despawned; use crate::Despawned;
@ -98,8 +91,6 @@ struct SharedServerInner {
/// Contains info about dimensions, biomes, and chats. /// Contains info about dimensions, biomes, and chats.
/// Sent to all clients when joining. /// Sent to all clients when joining.
registry_codec: Compound, registry_codec: Compound,
/// The instant the server was started.
start_instant: Instant,
/// Sender for new clients past the login stage. /// Sender for new clients past the login stage.
new_clients_send: Sender<Client>, new_clients_send: Sender<Client>,
/// Receiver for new clients past the login stage. /// Receiver for new clients past the login stage.
@ -205,11 +196,6 @@ impl SharedServer {
pub(crate) fn registry_codec(&self) -> &Compound { pub(crate) fn registry_codec(&self) -> &Compound {
&self.0.registry_codec &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. /// Contains information about a new client joining the server.
@ -280,7 +266,6 @@ pub fn build_plugin(
dimensions: plugin.dimensions.clone(), dimensions: plugin.dimensions.clone(),
biomes: plugin.biomes.clone(), biomes: plugin.biomes.clone(),
registry_codec, registry_codec,
start_instant: Instant::now(),
new_clients_send, new_clients_send,
new_clients_recv, new_clients_recv,
connection_sema: Arc::new(Semaphore::new(plugin.max_connections)), connection_sema: Arc::new(Semaphore::new(plugin.max_connections)),
@ -306,7 +291,7 @@ pub fn build_plugin(
let shared = server.shared.clone(); 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| { let spawn_new_clients = move |world: &mut World| {
for _ in 0..shared.0.new_clients_recv.len() { for _ in 0..shared.0.new_clients_recv.len() {
let Ok(client) = shared.0.new_clients_recv.try_recv() else { let Ok(client) = shared.0.new_clients_recv.try_recv() else {
@ -319,96 +304,72 @@ pub fn build_plugin(
let shared = server.shared.clone(); 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. // Insert resources.
app.insert_resource(server) app.insert_resource(server)
.insert_resource(McEntityManager::new()) .insert_resource(McEntityManager::new())
.insert_resource(PlayerList::new()); .insert_resource(PlayerList::new());
register_client_events(&mut app.world); register_client_events(&mut app.world);
// Add core systems and stages. User code is expected to run in // Add the event loop schedule.
// `CoreStage::Update` and `EventLoop`. let mut event_loop = Schedule::new();
app.add_system_to_stage(CoreStage::PreUpdate, spawn_new_clients) event_loop.configure_set(EventLoopSet);
.add_stage_before( event_loop.set_default_base_set(EventLoopSet);
CoreStage::Update,
EventLoop, app.add_schedule(EventLoopSchedule, event_loop);
SystemStage::parallel().with_run_criteria(event_loop_run_criteria),
// 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( .in_base_set(CoreSet::PostUpdate),
CoreStage::PostUpdate, )
SystemSet::new() .add_systems(
.label("valence_core") update_inventories()
.with_system(init_entities) .in_base_set(CoreSet::PostUpdate)
.with_system(check_entity_invariants) .before(init_entities),
.with_system(check_instance_invariants.after(check_entity_invariants)) )
.with_system(update_player_list.before(update_instances_pre_client)) .add_system(increment_tick_counter.in_base_set(CoreSet::Last));
.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::<AppExit>::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::<Events<AppExit>>() {
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()));
}
});
Ok(()) Ok(())
} }
/// The stage label for the special "event loop" stage. /// The [`ScheduleLabel`] for the event loop [`Schedule`].
#[derive(StageLabel)] #[derive(ScheduleLabel, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
pub struct EventLoop; 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`] /// Despawns all the entities marked as despawned with the [`Despawned`]
/// component. /// component.
@ -418,7 +379,7 @@ fn despawn_marked_entities(mut commands: Commands, entities: Query<Entity, With<
} }
} }
fn inc_current_tick(mut server: ResMut<Server>) { fn increment_tick_counter(mut server: ResMut<Server>) {
server.current_tick += 1; server.current_tick += 1;
} }

View file

@ -1,7 +1,8 @@
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use bevy_app::App; use bevy_app::{App, CoreSchedule};
use bevy_ecs::prelude::Entity; use bevy_ecs::prelude::Entity;
use bevy_ecs::schedule::{LogLevel, ScheduleBuildSettings};
use bytes::BytesMut; use bytes::BytesMut;
use valence_protocol::codec::{PacketDecoder, PacketEncoder}; use valence_protocol::codec::{PacketDecoder, PacketEncoder};
use valence_protocol::packet::S2cPlayPacket; use valence_protocol::packet::S2cPlayPacket;
@ -164,17 +165,28 @@ pub fn scenario_single_client(app: &mut App) -> (Entity, MockClientHelper) {
.with_compression_threshold(None) .with_compression_threshold(None)
.with_connection_mode(ConnectionMode::Offline), .with_connection_mode(ConnectionMode::Offline),
); );
let server = app.world.resource::<Server>(); let server = app.world.resource::<Server>();
let instance = server.new_instance(DimensionId::default()); let instance = server.new_instance(DimensionId::default());
let instance_ent = app.world.spawn(instance).id(); let instance_ent = app.world.spawn(instance).id();
let info = gen_client_info("test"); let info = gen_client_info("test");
let (mut client, client_helper) = create_mock_client(info); let (mut client, client_helper) = create_mock_client(info);
// HACK: needed so client does not get disconnected on first update // HACK: needed so client does not get disconnected on first update
client.set_instance(instance_ent); client.set_instance(instance_ent);
let client_ent = app let client_ent = app
.world .world
.spawn((client, Inventory::new(InventoryKind::Player))) .spawn((client, Inventory::new(InventoryKind::Player)))
.id(); .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) (client_ent, client_helper)
} }

View file

@ -20,7 +20,7 @@ valence_nbt = { version = "0.5.0", path = "../valence_nbt" }
[dev-dependencies] [dev-dependencies]
anyhow = "1.0.68" anyhow = "1.0.68"
bevy_ecs = "0.9.1" bevy_ecs = { git = "https://github.com/bevyengine/bevy/", rev = "2ea00610188dce1eba1172a3ded8244570189230" }
clap = "4.1.4" clap = "4.1.4"
criterion = "0.4.0" criterion = "0.4.0"
flume = "0.10.14" flume = "0.10.14"

View file

@ -40,13 +40,17 @@ pub fn main() {
App::new() App::new()
.add_plugin(ServerPlugin::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_startup_system(setup)
.add_system(init_clients) .add_system(default_event_handler.in_schedule(EventLoopSchedule))
.add_system(remove_unviewed_chunks.after(init_clients)) .add_systems(
.add_system(update_client_views.after(remove_unviewed_chunks)) (
.add_system(send_recv_chunks.after(update_client_views)) init_clients,
remove_unviewed_chunks,
update_client_views,
send_recv_chunks,
)
.chain(),
)
.add_system(despawn_disconnected_clients) .add_system(despawn_disconnected_clients)
.run(); .run();
} }