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) {
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);

View file

@ -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"

View file

@ -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();
}

View file

@ -32,11 +32,13 @@ pub fn main() {
.collect::<Vec<_>>(),
),
)
.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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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());
})
}
}
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();

View file

@ -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();
}

View file

@ -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<Server>) {
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<Server>) {
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);
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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]

View file

@ -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<Vec<Entity>>,
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>)>,

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
/// Minecraft entity, it must have this component attached.
///

View file

@ -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<OpenInventory>>,
) {
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)]
pub enum InventoryKind {
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)]
mod test {
use bevy_app::App;

View file

@ -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;

View file

@ -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<Client>>,
mut player_list: ResMut<PlayerList>,
@ -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()
}
}

View file

@ -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<Client>,
/// 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::<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()));
}
});
.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<Entity, With<
}
}
fn inc_current_tick(mut server: ResMut<Server>) {
fn increment_tick_counter(mut server: ResMut<Server>) {
server.current_tick += 1;
}

View file

@ -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::<Server>();
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)
}

View file

@ -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"

View file

@ -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();
}