Client Component Division (#266)

## Description

Divides the `Client` component into a set of smaller components as
described by #199 (with many deviations). `McEntity` will be dealt with
in a future PR.

- Divide `Client` into smaller components (There's a lot to look at).
- Move common components to `component` module.
- Remove `Username` type from `valence_protocol` because the added
complexity wasn't adding much benefit.
- Clean up the inventory module.

I've stopped worrying about the "Effect When Added" and "Effect When
Removed" behavior of components so much, and instead assume that all
components of a particular thing are required unless otherwise stated.

## Test Plan

Steps:
1. Run examples and tests.

A large number of tweaks have been made to the inventory module. I tried
to preserve semantics but I could have made a mistake there.

---------

Co-authored-by: Carson McManus <dyc3@users.noreply.github.com>
Co-authored-by: Carson McManus <carson.mcmanus1@gmail.com>
This commit is contained in:
Ryan Johnson 2023-03-11 06:04:14 -08:00 committed by GitHub
parent 8bd20e964e
commit b46cc502aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 2320 additions and 1906 deletions

View file

@ -7,18 +7,17 @@ use valence::prelude::*;
/// Toggles client's game mode between survival and creative when they start /// Toggles client's game mode between survival and creative when they start
/// sneaking. /// sneaking.
pub fn toggle_gamemode_on_sneak( pub fn toggle_gamemode_on_sneak(
mut clients: Query<&mut Client>, mut clients: Query<&mut GameMode>,
mut events: EventReader<StartSneaking>, mut events: EventReader<StartSneaking>,
) { ) {
for event in events.iter() { for event in events.iter() {
let Ok(mut client) = clients.get_component_mut::<Client>(event.client) else { let Ok(mut mode) = clients.get_component_mut::<GameMode>(event.client) else {
continue; continue;
}; };
let mode = client.game_mode(); *mode = match *mode {
client.set_game_mode(match mode {
GameMode::Survival => GameMode::Creative, GameMode::Survival => GameMode::Creative,
GameMode::Creative => GameMode::Survival, GameMode::Creative => GameMode::Survival,
_ => GameMode::Creative, _ => GameMode::Creative,
}); };
} }
} }

View file

@ -9,8 +9,8 @@ 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(default_event_handler.in_schedule(EventLoopSchedule))
.add_startup_system(setup) .add_startup_system(setup)
.add_system(default_event_handler.in_schedule(EventLoopSchedule))
.add_system(init_clients) .add_system(init_clients)
.add_system(despawn_disconnected_clients); .add_system(despawn_disconnected_clients);
} }
@ -34,15 +34,12 @@ fn setup(mut commands: Commands, server: Res<Server>) {
} }
fn init_clients( fn init_clients(
mut clients: Query<&mut Client, Added<Client>>, mut clients: Query<(&mut Position, &mut Location), Added<Client>>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
) { ) {
let instance = instances.get_single().unwrap(); for (mut pos, mut loc) in &mut clients {
pos.0 = [0.5, SPAWN_Y as f64 + 1.0, 0.5].into();
for mut client in &mut clients { loc.0 = instances.single();
client.set_position([0.5, SPAWN_Y as f64 + 1.0, 0.5]);
client.set_instance(instance);
client.set_game_mode(GameMode::Survival);
} }
} }

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use std::time::Instant; use std::time::Instant;
use valence::client::despawn_disconnected_clients; use valence::client::despawn_disconnected_clients;
@ -65,19 +67,26 @@ fn setup(mut commands: Commands, server: Res<Server>) {
} }
fn init_clients( fn init_clients(
mut clients: Query<(Entity, &mut Client), Added<Client>>, mut clients: Query<
(
Entity,
&UniqueId,
&mut Position,
&mut Location,
&mut GameMode,
),
Added<Client>,
>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
mut commands: Commands, mut commands: Commands,
) { ) {
let instance = instances.single(); for (entity, unique_id, mut pos, mut loc, mut game_mode) in &mut clients {
pos.0 = [0.0, SPAWN_Y as f64 + 1.0, 0.0].into();
loc.0 = instances.single();
*game_mode = GameMode::Creative;
for (client_entity, mut client) in &mut clients { commands
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); .entity(entity)
client.set_instance(instance); .insert(McEntity::with_uuid(EntityKind::Player, loc.0, unique_id.0));
client.set_game_mode(GameMode::Creative);
let player_entity = McEntity::with_uuid(EntityKind::Player, instance, client.uuid());
commands.entity(client_entity).insert(player_entity);
} }
} }

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
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::prelude::*; use valence::prelude::*;
@ -77,14 +79,12 @@ fn setup(mut commands: Commands, server: Res<Server>) {
} }
fn init_clients( fn init_clients(
mut clients: Query<&mut Client, Added<Client>>, mut clients: Query<(&mut Position, &mut Location, &mut GameMode), Added<Client>>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
) { ) {
for mut client in &mut clients { for (mut pos, mut loc, mut game_mode) in &mut clients {
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
client.set_respawn_screen(true); loc.0 = instances.single();
client.set_instance(instances.single()); *game_mode = GameMode::Creative;
client.set_game_mode(GameMode::Creative);
client.send_message("Welcome to Valence!".italic());
} }
} }

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use valence::client::despawn_disconnected_clients; use valence::client::despawn_disconnected_clients;
use valence::client::event::{default_event_handler, ChatMessage, PlayerInteractBlock}; use valence::client::event::{default_event_handler, ChatMessage, PlayerInteractBlock};
use valence::nbt::{compound, List}; use valence::nbt::{compound, List};
@ -61,19 +63,34 @@ fn setup(mut commands: Commands, server: Res<Server>) {
} }
fn init_clients( fn init_clients(
mut clients: Query<&mut Client, Added<Client>>, mut clients: Query<
(
Entity,
&UniqueId,
&mut Position,
&mut Look,
&mut Location,
&mut GameMode,
),
Added<Client>,
>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) { ) {
for mut client in &mut clients { for (entity, uuid, mut pos, mut look, mut loc, mut game_mode) in &mut clients {
client.set_position([1.5, FLOOR_Y as f64 + 1.0, 1.5]); pos.set([1.5, FLOOR_Y as f64 + 1.0, 1.5]);
client.set_yaw(-90.0); look.yaw = -90.0;
client.set_instance(instances.single()); loc.0 = instances.single();
client.set_game_mode(GameMode::Creative); *game_mode = GameMode::Creative;
commands
.entity(entity)
.insert(McEntity::with_uuid(EntityKind::Player, loc.0, uuid.0));
} }
} }
fn event_handler( fn event_handler(
clients: Query<&Client>, clients: Query<(&Username, &Properties, &UniqueId)>,
mut messages: EventReader<ChatMessage>, mut messages: EventReader<ChatMessage>,
mut block_interacts: EventReader<PlayerInteractBlock>, mut block_interacts: EventReader<PlayerInteractBlock>,
mut instances: Query<&mut Instance>, mut instances: Query<&mut Instance>,
@ -83,14 +100,14 @@ fn event_handler(
client, message, .. client, message, ..
} in messages.iter() } in messages.iter()
{ {
let Ok(client) = clients.get(*client) else { let Ok((username, _, _)) = clients.get(*client) else {
continue continue
}; };
let mut sign = instance.block_mut(SIGN_POS).unwrap(); let mut sign = instance.block_mut(SIGN_POS).unwrap();
let nbt = sign.nbt_mut().unwrap(); let nbt = sign.nbt_mut().unwrap();
nbt.insert("Text2", message.to_string().color(Color::DARK_GREEN)); nbt.insert("Text2", message.to_string().color(Color::DARK_GREEN));
nbt.insert("Text3", format!("~{}", client.username()).italic()); nbt.insert("Text3", format!("~{}", username).italic());
} }
for PlayerInteractBlock { for PlayerInteractBlock {
@ -101,19 +118,19 @@ fn event_handler(
} in block_interacts.iter() } in block_interacts.iter()
{ {
if *hand == Hand::Main && *position == SKULL_POS { if *hand == Hand::Main && *position == SKULL_POS {
let Ok(client) = clients.get(*client) else { let Ok((_, properties, uuid)) = clients.get(*client) else {
continue continue
}; };
let Some(textures) = client.properties().iter().find(|prop| prop.name == "textures") else { let Some(textures) = properties.textures() else {
continue continue;
}; };
let mut skull = instance.block_mut(SKULL_POS).unwrap(); let mut skull = instance.block_mut(SKULL_POS).unwrap();
let nbt = skull.nbt_mut().unwrap(); let nbt = skull.nbt_mut().unwrap();
*nbt = compound! { *nbt = compound! {
"SkullOwner" => compound! { "SkullOwner" => compound! {
"Id" => client.uuid(), "Id" => uuid.0,
"Properties" => compound! { "Properties" => compound! {
"textures" => List::Compound(vec![compound! { "textures" => List::Compound(vec![compound! {
"Value" => textures.value.clone(), "Value" => textures.value.clone(),

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use valence::client::despawn_disconnected_clients; use valence::client::despawn_disconnected_clients;
use valence::client::event::{ use valence::client::event::{
default_event_handler, PlayerInteractBlock, StartDigging, StartSneaking, StopDestroyBlock, default_event_handler, PlayerInteractBlock, StartDigging, StartSneaking, StopDestroyBlock,
@ -48,77 +50,90 @@ fn setup(mut commands: Commands, server: Res<Server>) {
} }
fn init_clients( fn init_clients(
mut clients: Query<&mut Client, Added<Client>>, mut clients: Query<
(
Entity,
&UniqueId,
&mut Client,
&mut Position,
&mut Location,
&mut GameMode,
),
Added<Client>,
>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) { ) {
for mut client in &mut clients { for (entity, uuid, mut client, mut pos, mut loc, mut game_mode) in &mut clients {
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); pos.0 = [0.0, SPAWN_Y as f64 + 1.0, 0.0].into();
client.set_instance(instances.single()); loc.0 = instances.single();
client.set_game_mode(GameMode::Creative); *game_mode = GameMode::Creative;
client.send_message("Welcome to Valence! Build something cool.".italic()); client.send_message("Welcome to Valence! Build something cool.".italic());
commands
.entity(entity)
.insert(McEntity::with_uuid(EntityKind::Player, loc.0, uuid.0));
} }
} }
fn toggle_gamemode_on_sneak( fn toggle_gamemode_on_sneak(
mut clients: Query<&mut Client>, mut clients: Query<&mut GameMode>,
mut events: EventReader<StartSneaking>, mut events: EventReader<StartSneaking>,
) { ) {
for event in events.iter() { for event in events.iter() {
let Ok(mut client) = clients.get_component_mut::<Client>(event.client) else { let Ok(mut mode) = clients.get_component_mut::<GameMode>(event.client) else {
continue; continue;
}; };
let mode = client.game_mode(); *mode = match *mode {
client.set_game_mode(match mode {
GameMode::Survival => GameMode::Creative, GameMode::Survival => GameMode::Creative,
GameMode::Creative => GameMode::Survival, GameMode::Creative => GameMode::Survival,
_ => GameMode::Creative, _ => GameMode::Creative,
}); };
} }
} }
fn digging_creative_mode( fn digging_creative_mode(
clients: Query<&Client>, clients: Query<&GameMode>,
mut instances: Query<&mut Instance>, mut instances: Query<&mut Instance>,
mut events: EventReader<StartDigging>, mut events: EventReader<StartDigging>,
) { ) {
let mut instance = instances.single_mut(); let mut instance = instances.single_mut();
for event in events.iter() { for event in events.iter() {
let Ok(client) = clients.get_component::<Client>(event.client) else { let Ok(game_mode) = clients.get(event.client) else {
continue; continue;
}; };
if client.game_mode() == GameMode::Creative { if *game_mode == GameMode::Creative {
instance.set_block(event.position, BlockState::AIR); instance.set_block(event.position, BlockState::AIR);
} }
} }
} }
fn digging_survival_mode( fn digging_survival_mode(
clients: Query<&Client>, clients: Query<&GameMode>,
mut instances: Query<&mut Instance>, mut instances: Query<&mut Instance>,
mut events: EventReader<StopDestroyBlock>, mut events: EventReader<StopDestroyBlock>,
) { ) {
let mut instance = instances.single_mut(); let mut instance = instances.single_mut();
for event in events.iter() { for event in events.iter() {
let Ok(client) = clients.get_component::<Client>(event.client) else { let Ok(game_mode) = clients.get(event.client) else {
continue; continue;
}; };
if client.game_mode() == GameMode::Survival { if *game_mode == GameMode::Survival {
instance.set_block(event.position, BlockState::AIR); instance.set_block(event.position, BlockState::AIR);
} }
} }
} }
fn place_blocks( fn place_blocks(
mut clients: Query<(&Client, &mut Inventory)>, mut clients: Query<(&mut Inventory, &GameMode, &PlayerInventoryState)>,
mut instances: Query<&mut Instance>, mut instances: Query<&mut Instance>,
mut events: EventReader<PlayerInteractBlock>, mut events: EventReader<PlayerInteractBlock>,
) { ) {
let mut instance = instances.single_mut(); let mut instance = instances.single_mut();
for event in events.iter() { for event in events.iter() {
let Ok((client, mut inventory)) = clients.get_mut(event.client) else { let Ok((mut inventory, game_mode, inv_state)) = clients.get_mut(event.client) else {
continue; continue;
}; };
if event.hand != Hand::Main { if event.hand != Hand::Main {
@ -126,7 +141,7 @@ fn place_blocks(
} }
// get the held item // get the held item
let slot_id = client.held_item_slot(); let slot_id = inv_state.held_item_slot();
let Some(stack) = inventory.slot(slot_id) else { let Some(stack) = inventory.slot(slot_id) else {
// no item in the slot // no item in the slot
continue; continue;
@ -137,7 +152,7 @@ fn place_blocks(
continue; continue;
}; };
if client.game_mode() == GameMode::Survival { if *game_mode == GameMode::Survival {
// check if the player has the item in their inventory and remove // check if the player has the item in their inventory and remove
// it. // it.
if stack.count() > 1 { if stack.count() > 1 {

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use bevy_app::App; use bevy_app::App;
use tracing::warn; use tracing::warn;
use valence::client::despawn_disconnected_clients; use valence::client::despawn_disconnected_clients;
@ -37,7 +39,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::BEDROCK); instance.set_block([x, SPAWN_Y, z], BlockState::OAK_PLANKS);
} }
} }
@ -45,33 +47,49 @@ fn setup(mut commands: Commands, server: Res<Server>) {
} }
fn init_clients( fn init_clients(
mut clients: Query<&mut Client, Added<Client>>, mut clients: Query<
(
Entity,
&UniqueId,
&mut Client,
&mut Position,
&mut Location,
&mut GameMode,
),
Added<Client>,
>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) { ) {
for mut client in &mut clients { for (entity, uuid, mut client, mut pos, mut loc, mut game_mode) in &mut clients {
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
client.set_instance(instances.single()); loc.0 = instances.single();
client.set_game_mode(GameMode::Adventure); *game_mode = GameMode::Adventure;
client.send_message("Welcome to Valence! Talk about something.".italic()); client.send_message("Welcome to Valence! Talk about something.".italic());
commands
.entity(entity)
.insert(McEntity::with_uuid(EntityKind::Player, loc.0, uuid.0));
} }
} }
fn handle_message_events(mut clients: Query<&mut Client>, mut messages: EventReader<ChatMessage>) { fn handle_message_events(
mut clients: Query<(&mut Client, &Username)>,
mut messages: EventReader<ChatMessage>,
) {
for message in messages.iter() { for message in messages.iter() {
let Ok(client) = clients.get_component::<Client>(message.client) else { let Ok(username) = clients.get_component::<Username>(message.client) else {
warn!("Unable to find client for message: {:?}", message); warn!("Unable to find client for message: {:?}", message);
continue; continue;
}; };
let message = message.message.to_string(); let message = message.message.to_string();
let formatted = format!("<{}>: ", client.username()) let formatted = format!("<{}>: ", username.0).bold().color(Color::YELLOW)
.bold() + message.not_bold().color(Color::WHITE);
.color(Color::YELLOW)
+ message.into_text().not_bold().color(Color::WHITE);
// TODO: write message to instance buffer. // TODO: write message to instance buffer.
for mut client in &mut clients { for (mut client, _) in &mut clients {
client.send_message(formatted.clone()); client.send_message(formatted.clone());
} }
} }

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use tracing::warn; use tracing::warn;
use valence::client::despawn_disconnected_clients; use valence::client::despawn_disconnected_clients;
use valence::client::event::{default_event_handler, PlayerInteractBlock, StartSneaking}; use valence::client::event::{default_event_handler, PlayerInteractBlock, StartSneaking};
@ -48,30 +50,42 @@ fn setup(mut commands: Commands, server: Res<Server>) {
} }
fn init_clients( fn init_clients(
mut clients: Query<&mut Client, Added<Client>>, mut clients: Query<
(
Entity,
&UniqueId,
&mut Position,
&mut Location,
&mut GameMode,
),
Added<Client>,
>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) { ) {
for mut client in &mut clients { for (entity, uuid, mut pos, mut loc, mut game_mode) in &mut clients {
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); pos.0 = [0.5, SPAWN_Y as f64 + 1.0, 0.5].into();
client.set_instance(instances.single()); loc.0 = instances.single();
client.set_game_mode(GameMode::Creative); *game_mode = GameMode::Creative;
commands
.entity(entity)
.insert(McEntity::with_uuid(EntityKind::Player, loc.0, uuid.0));
} }
} }
fn toggle_gamemode_on_sneak( fn toggle_gamemode_on_sneak(
mut clients: Query<&mut Client>, mut clients: Query<&mut GameMode>,
mut events: EventReader<StartSneaking>, mut events: EventReader<StartSneaking>,
) { ) {
for event in events.iter() { for event in events.iter() {
let Ok(mut client) = clients.get_component_mut::<Client>(event.client) else { let Ok(mut mode) = clients.get_component_mut::<GameMode>(event.client) else {
continue; continue;
}; };
let mode = client.game_mode(); *mode = match *mode {
client.set_game_mode(match mode {
GameMode::Survival => GameMode::Creative, GameMode::Survival => GameMode::Creative,
GameMode::Creative => GameMode::Survival, GameMode::Creative => GameMode::Survival,
_ => GameMode::Creative, _ => GameMode::Creative,
}); };
} }
} }

View file

@ -1,3 +1,6 @@
#![allow(clippy::type_complexity)]
use bevy_ecs::query::WorldQuery;
use glam::Vec3Swizzles; use glam::Vec3Swizzles;
use valence::client::despawn_disconnected_clients; use valence::client::despawn_disconnected_clients;
use valence::client::event::{ use valence::client::event::{
@ -64,43 +67,50 @@ fn setup(mut commands: Commands, server: Res<Server>) {
} }
fn init_clients( fn init_clients(
mut commands: Commands, mut clients: Query<(Entity, &UniqueId, &mut Position, &mut Location), Added<Client>>,
mut clients: Query<(Entity, &mut Client), Added<Client>>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) { ) {
let instance = instances.single(); for (entity, uuid, mut pos, mut loc) in &mut clients {
pos.set([0.0, SPAWN_Y as f64, 0.0]);
for (entity, mut client) in &mut clients { loc.0 = instances.single();
client.set_position([0.0, SPAWN_Y as f64, 0.0]);
client.set_instance(instance);
commands.entity(entity).insert(( commands.entity(entity).insert((
CombatState { CombatState {
last_attacked_tick: 0, last_attacked_tick: 0,
has_bonus_knockback: false, has_bonus_knockback: false,
}, },
McEntity::with_uuid(EntityKind::Player, instance, client.uuid()), McEntity::with_uuid(EntityKind::Player, loc.0, uuid.0),
)); ));
} }
} }
#[derive(WorldQuery)]
#[world_query(mutable)]
struct CombatQuery {
client: &'static mut Client,
pos: &'static Position,
state: &'static mut CombatState,
entity: &'static mut McEntity,
}
fn handle_combat_events( fn handle_combat_events(
manager: Res<McEntityManager>, manager: Res<McEntityManager>,
server: Res<Server>, server: Res<Server>,
mut clients: Query<CombatQuery>,
mut start_sprinting: EventReader<StartSprinting>, mut start_sprinting: EventReader<StartSprinting>,
mut stop_sprinting: EventReader<StopSprinting>, mut stop_sprinting: EventReader<StopSprinting>,
mut interact_with_entity: EventReader<PlayerInteract>, mut interact_with_entity: EventReader<PlayerInteract>,
mut clients: Query<(&mut Client, &mut CombatState, &mut McEntity)>,
) { ) {
for &StartSprinting { client } in start_sprinting.iter() { for &StartSprinting { client } in start_sprinting.iter() {
if let Ok((_, mut state, _)) = clients.get_mut(client) { if let Ok(mut client) = clients.get_mut(client) {
state.has_bonus_knockback = true; client.state.has_bonus_knockback = true;
} }
} }
for &StopSprinting { client } in stop_sprinting.iter() { for &StopSprinting { client } in stop_sprinting.iter() {
if let Ok((_, mut state, _)) = clients.get_mut(client) { if let Ok(mut client) = clients.get_mut(client) {
state.has_bonus_knockback = false; client.state.has_bonus_knockback = false;
} }
} }
@ -115,49 +125,53 @@ fn handle_combat_events(
continue continue
}; };
let Ok([(attacker_client, mut attacker_state, _), (mut victim_client, mut victim_state, mut victim_entity)]) = let Ok([mut attacker, mut victim]) = clients.get_many_mut([attacker_client, victim_client]) else {
clients.get_many_mut([attacker_client, victim_client])
else {
// Victim or attacker does not exist, or the attacker is attacking itself. // Victim or attacker does not exist, or the attacker is attacking itself.
continue continue
}; };
if server.current_tick() - victim_state.last_attacked_tick < 10 { if server.current_tick() - victim.state.last_attacked_tick < 10 {
// Victim is still on attack cooldown. // Victim is still on attack cooldown.
continue; continue;
} }
victim_state.last_attacked_tick = server.current_tick(); victim.state.last_attacked_tick = server.current_tick();
let victim_pos = victim_client.position().xz(); let victim_pos = victim.pos.0.xz();
let attacker_pos = attacker_client.position().xz(); let attacker_pos = attacker.pos.0.xz();
let dir = (victim_pos - attacker_pos).normalize().as_vec2(); let dir = (victim_pos - attacker_pos).normalize().as_vec2();
let knockback_xz = if attacker_state.has_bonus_knockback { let knockback_xz = if attacker.state.has_bonus_knockback {
18.0 18.0
} else { } else {
8.0 8.0
}; };
let knockback_y = if attacker_state.has_bonus_knockback { let knockback_y = if attacker.state.has_bonus_knockback {
8.432 8.432
} else { } else {
6.432 6.432
}; };
victim_client.set_velocity([dir.x * knockback_xz, knockback_y, dir.y * knockback_xz]); victim
.client
.set_velocity([dir.x * knockback_xz, knockback_y, dir.y * knockback_xz]);
attacker_state.has_bonus_knockback = false; attacker.state.has_bonus_knockback = false;
victim_client.trigger_status(EntityStatus::DamageFromGenericSource); victim
victim_entity.trigger_status(EntityStatus::DamageFromGenericSource); .client
.trigger_status(EntityStatus::DamageFromGenericSource);
victim
.entity
.trigger_status(EntityStatus::DamageFromGenericSource);
} }
} }
fn teleport_oob_clients(mut clients: Query<&mut Client>) { fn teleport_oob_clients(mut clients: Query<&mut Position, With<Client>>) {
for mut client in &mut clients { for mut pos in &mut clients {
if client.position().y < 0.0 { if pos.0.y < 0.0 {
client.set_position([0.0, SPAWN_Y as _, 0.0]); pos.set([0.0, SPAWN_Y as _, 0.0]);
} }
} }
} }

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use std::mem; use std::mem;
use valence::client::despawn_disconnected_clients; use valence::client::despawn_disconnected_clients;
@ -65,13 +67,24 @@ fn setup(mut commands: Commands, server: Res<Server>) {
} }
fn init_clients( fn init_clients(
mut clients: Query<&mut Client, Added<Client>>, mut clients: Query<
(
Entity,
&UniqueId,
&mut Client,
&mut Position,
&mut Location,
&mut GameMode,
),
Added<Client>,
>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) { ) {
for mut client in &mut clients { for (entity, uuid, mut client, mut pos, mut loc, mut game_mode) in &mut clients {
client.set_position(SPAWN_POS); pos.0 = SPAWN_POS;
client.set_instance(instances.single()); loc.0 = instances.single();
client.set_game_mode(GameMode::Survival); *game_mode = GameMode::Survival;
client.send_message("Welcome to Conway's game of life in Minecraft!".italic()); client.send_message("Welcome to Conway's game of life in Minecraft!".italic());
client.send_message( client.send_message(
@ -79,6 +92,9 @@ fn init_clients(
life." life."
.italic(), .italic(),
); );
commands
.entity(entity)
.insert(McEntity::with_uuid(EntityKind::Player, loc.0, uuid.0));
} }
} }
@ -195,10 +211,13 @@ fn pause_on_crouch(
} }
} }
fn reset_oob_clients(mut clients: Query<&mut Client>, mut board: ResMut<LifeBoard>) { fn reset_oob_clients(
for mut client in &mut clients { mut clients: Query<&mut Position, With<Client>>,
if client.position().y < 0.0 { mut board: ResMut<LifeBoard>,
client.set_position(SPAWN_POS); ) {
for mut pos in &mut clients {
if pos.0.y < 0.0 {
pos.0 = SPAWN_POS;
board.clear(); board.clear();
} }
} }

View file

@ -1,10 +1,12 @@
#![allow(clippy::type_complexity)]
use std::f64::consts::TAU; use std::f64::consts::TAU;
use glam::{DQuat, EulerRot}; use glam::{DQuat, EulerRot};
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::math::to_yaw_and_pitch;
use valence::prelude::*; use valence::prelude::*;
use valence::util::to_yaw_and_pitch;
const SPHERE_CENTER: DVec3 = DVec3::new(0.5, SPAWN_POS.y as f64 + 2.0, 0.5); const SPHERE_CENTER: DVec3 = DVec3::new(0.5, SPAWN_POS.y as f64 + 2.0, 0.5);
const SPHERE_AMOUNT: usize = 200; const SPHERE_AMOUNT: usize = 200;
@ -52,17 +54,31 @@ fn setup(mut commands: Commands, server: Res<Server>) {
} }
fn init_clients( fn init_clients(
mut clients: Query<&mut Client, Added<Client>>, mut clients: Query<
(
Entity,
&UniqueId,
&mut Position,
&mut Location,
&mut GameMode,
),
Added<Client>,
>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) { ) {
for mut client in &mut clients { for (entity, uuid, mut pos, mut loc, mut game_mode) in &mut clients {
client.set_position([ pos.set([
SPAWN_POS.x as f64 + 0.5, SPAWN_POS.x as f64 + 0.5,
SPAWN_POS.y as f64 + 1.0, SPAWN_POS.y as f64 + 1.0,
SPAWN_POS.z as f64 + 0.5, SPAWN_POS.z as f64 + 0.5,
]); ]);
client.set_instance(instances.single()); loc.0 = instances.single();
client.set_game_mode(GameMode::Creative); *game_mode = GameMode::Creative;
commands
.entity(entity)
.insert(McEntity::with_uuid(EntityKind::Player, loc.0, uuid.0));
} }
} }

View file

@ -1,4 +1,5 @@
use tracing::warn; #![allow(clippy::type_complexity)]
use valence::client::despawn_disconnected_clients; use valence::client::despawn_disconnected_clients;
use valence::client::event::{default_event_handler, PerformRespawn, StartSneaking}; use valence::client::event::{default_event_handler, PerformRespawn, StartSneaking};
use valence::prelude::*; use valence::prelude::*;
@ -9,7 +10,7 @@ pub fn main() {
tracing_subscriber::fmt().init(); tracing_subscriber::fmt().init();
App::new() App::new()
.add_plugin(ServerPlugin::new(())) .add_plugin(ServerPlugin::new(()).with_connection_mode(ConnectionMode::Offline))
.add_startup_system(setup) .add_startup_system(setup)
.add_system(init_clients) .add_system(init_clients)
.add_systems( .add_systems(
@ -41,51 +42,59 @@ fn setup(mut commands: Commands, server: Res<Server>) {
} }
fn init_clients( fn init_clients(
mut clients: Query<&mut Client, Added<Client>>, mut clients: Query<
(
Entity,
&UniqueId,
&mut Client,
&mut Position,
&mut HasRespawnScreen,
&mut Location,
),
Added<Client>,
>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) { ) {
let instance = instances.into_iter().next().unwrap(); for (entity, uuid, mut client, mut pos, mut has_respawn_screen, mut loc) in &mut clients {
pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
for mut client in &mut clients { has_respawn_screen.0 = true;
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); loc.0 = instances.iter().next().unwrap();
client.set_respawn_screen(true);
client.set_instance(instance);
client.send_message( client.send_message(
"Welcome to Valence! Press shift to die in the game (but not in real life).".italic(), "Welcome to Valence! Sneak to die in the game (but not in real life).".italic(),
); );
commands
.entity(entity)
.insert(McEntity::with_uuid(EntityKind::Player, loc.0, uuid.0));
} }
} }
fn squat_and_die(mut clients: Query<&mut Client>, mut events: EventReader<StartSneaking>) { fn squat_and_die(mut clients: Query<&mut Client>, mut events: EventReader<StartSneaking>) {
for event in events.iter() { for event in events.iter() {
let Ok(mut client) = clients.get_component_mut::<Client>(event.client) else { if let Ok(mut client) = clients.get_mut(event.client) {
warn!("Client {:?} not found", event.client); client.kill(None, "Squatted too hard.");
continue; }
};
client.kill(None, "Squatted too hard.");
} }
} }
fn necromancy( fn necromancy(
mut clients: Query<&mut Client>, mut clients: Query<(&mut Position, &mut Look, &mut Location)>,
mut events: EventReader<PerformRespawn>, mut events: EventReader<PerformRespawn>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
) { ) {
for event in events.iter() { for event in events.iter() {
let Ok(mut client) = clients.get_component_mut::<Client>(event.client) else { if let Ok((mut pos, mut look, mut loc)) = clients.get_mut(event.client) {
continue; pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
}; look.yaw = 0.0;
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); look.pitch = 0.0;
client.set_velocity([0.0, 0.0, 0.0]);
client.set_yaw(0.0); // make the client respawn in another instance
client.set_pitch(0.0); let idx = instances.iter().position(|i| i == loc.0).unwrap();
// make the client respawn in another instance
let idx = instances let count = instances.iter().count();
.iter()
.position(|i| i == client.instance()) loc.0 = instances.into_iter().nth((idx + 1) % count).unwrap();
.unwrap(); }
let count = instances.iter().count();
client.set_instance(instances.into_iter().nth((idx + 1) % count).unwrap());
} }
} }

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use valence::client::despawn_disconnected_clients; use valence::client::despawn_disconnected_clients;
use valence::client::event::{default_event_handler, CommandExecution}; use valence::client::event::{default_event_handler, CommandExecution};
use valence::prelude::*; use valence::prelude::*;
@ -36,28 +38,47 @@ fn setup(mut commands: Commands, server: Res<Server>) {
} }
fn init_clients( fn init_clients(
mut clients: Query<&mut Client, Added<Client>>, mut clients: Query<
(
Entity,
&UniqueId,
&mut Client,
&mut Position,
&mut Location,
&mut GameMode,
&mut OpLevel,
),
Added<Client>,
>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) { ) {
for mut client in &mut clients { for (entity, uuid, mut client, mut pos, mut loc, mut game_mode, mut op_level) in &mut clients {
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
client.set_instance(instances.single()); loc.0 = instances.single();
client.set_game_mode(GameMode::Creative); *game_mode = GameMode::Creative;
client.set_op_level(2); // required to use F3+F4, eg /gamemode op_level.set(2); // required to use F3+F4, eg /gamemode
client.send_message("Welcome to Valence! Use F3+F4 to change gamemode.".italic()); client.send_message("Welcome to Valence! Use F3+F4 to change gamemode.".italic());
commands
.entity(entity)
.insert(McEntity::with_uuid(EntityKind::Player, loc.0, uuid.0));
} }
} }
fn interpret_command(mut clients: Query<&mut Client>, mut events: EventReader<CommandExecution>) { fn interpret_command(
mut clients: Query<(&mut Client, &OpLevel, &mut GameMode)>,
mut events: EventReader<CommandExecution>,
) {
for event in events.iter() { for event in events.iter() {
let Ok(mut client) = clients.get_component_mut::<Client>(event.client) else { let Ok((mut client, op_level, mut game_mode)) = clients.get_mut(event.client) else {
continue; continue;
}; };
let mut args = event.command.split_whitespace(); let mut args = event.command.split_whitespace();
if args.next() == Some("gamemode") { if args.next() == Some("gamemode") {
if client.op_level() < 2 { if op_level.get() < 2 {
// not enough permissions to use gamemode command // not enough permissions to use gamemode command
continue; continue;
} }
@ -72,7 +93,7 @@ fn interpret_command(mut clients: Query<&mut Client>, mut events: EventReader<Co
continue; continue;
} }
}; };
client.set_game_mode(mode); *game_mode = mode;
client.send_message(format!("Set gamemode to {mode:?}.").italic()); client.send_message(format!("Set gamemode to {mode:?}.").italic());
} }
} }

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use std::collections::VecDeque; use std::collections::VecDeque;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
@ -37,7 +39,6 @@ pub fn main() {
manage_blocks, manage_blocks,
despawn_disconnected_clients, despawn_disconnected_clients,
)) ))
.add_system(despawn_disconnected_clients)
.run(); .run();
} }
@ -51,28 +52,27 @@ struct GameState {
} }
fn init_clients( fn init_clients(
mut commands: Commands, mut clients: Query<
(
Entity,
&mut Client,
&UniqueId,
&mut IsFlat,
&mut Location,
&mut GameMode,
),
Added<Client>,
>,
server: Res<Server>, server: Res<Server>,
mut clients: Query<(Entity, &mut Client), Added<Client>>, mut commands: Commands,
) { ) {
for (ent, mut client) in clients.iter_mut() { for (entity, mut client, uuid, mut is_flat, mut loc, mut game_mode) in clients.iter_mut() {
let mut instance = server.new_instance(DimensionId::default()); is_flat.0 = true;
loc.0 = entity;
for pos in client.view().with_dist(VIEW_DIST).iter() { *game_mode = GameMode::Adventure;
assert!(instance.insert_chunk(pos, Chunk::default()).is_none());
}
client.set_position([
START_POS.x as f64 + 0.5,
START_POS.y as f64 + 1.0,
START_POS.z as f64 + 0.5,
]);
client.set_flat(true);
client.set_instance(ent);
client.set_game_mode(GameMode::Adventure);
client.send_message("Welcome to epic infinite parkour game!".italic()); client.send_message("Welcome to epic infinite parkour game!".italic());
let mut state = GameState { let state = GameState {
blocks: VecDeque::new(), blocks: VecDeque::new(),
score: 0, score: 0,
combo: 0, combo: 0,
@ -80,39 +80,75 @@ fn init_clients(
last_block_timestamp: 0, last_block_timestamp: 0,
}; };
reset(&mut client, &mut state, &mut instance); let instance = server.new_instance(DimensionId::default());
commands.entity(ent).insert(state); let mcentity = McEntity::with_uuid(EntityKind::Player, entity, uuid.0);
commands.entity(ent).insert(instance);
commands.entity(entity).insert((state, instance, mcentity));
} }
} }
fn reset_clients( fn reset_clients(
mut clients: Query<(&mut Client, &mut GameState, &mut Instance), With<GameState>>, mut clients: Query<(
&mut Client,
&mut Position,
&mut Look,
&mut GameState,
&mut Instance,
)>,
) { ) {
for (mut client, mut state, mut instance) in clients.iter_mut() { for (mut client, mut pos, mut look, mut state, mut instance) in clients.iter_mut() {
if (client.position().y as i32) < START_POS.y - 32 { let out_of_bounds = (pos.0.y as i32) < START_POS.y - 32;
client.send_message(
"Your score was ".italic()
+ state
.score
.to_string()
.color(Color::GOLD)
.bold()
.not_italic(),
);
reset(&mut client, &mut state, &mut instance); if out_of_bounds || state.is_added() {
if out_of_bounds && !state.is_added() {
client.send_message(
"Your score was ".italic()
+ state
.score
.to_string()
.color(Color::GOLD)
.bold()
.not_italic(),
);
}
// Init chunks.
for pos in ChunkView::new(ChunkPos::from_block_pos(START_POS), VIEW_DIST).iter() {
instance.insert_chunk(pos, Chunk::default());
}
state.score = 0;
state.combo = 0;
for block in &state.blocks {
instance.set_block(*block, BlockState::AIR);
}
state.blocks.clear();
state.blocks.push_back(START_POS);
instance.set_block(START_POS, BlockState::STONE);
for _ in 0..10 {
generate_next_block(&mut state, &mut instance, false);
}
pos.set([
START_POS.x as f64 + 0.5,
START_POS.y as f64 + 1.0,
START_POS.z as f64 + 0.5,
]);
look.yaw = 0.0;
look.pitch = 0.0;
} }
} }
} }
fn manage_blocks(mut clients: Query<(&mut Client, &mut GameState, &mut Instance)>) { fn manage_blocks(mut clients: Query<(&mut Client, &Position, &mut GameState, &mut Instance)>) {
for (mut client, mut state, mut instance) in clients.iter_mut() { for (mut client, pos, mut state, mut instance) in clients.iter_mut() {
let pos_under_player = BlockPos::new( let pos_under_player = BlockPos::new(
(client.position().x - 0.5).round() as i32, (pos.0.x - 0.5).round() as i32,
client.position().y as i32 - 1, pos.0.y as i32 - 1,
(client.position().z - 0.5).round() as i32, (pos.0.z - 0.5).round() as i32,
); );
if let Some(index) = state if let Some(index) = state
@ -140,11 +176,10 @@ fn manage_blocks(mut clients: Query<(&mut Client, &mut GameState, &mut Instance)
} }
let pitch = 0.9 + ((state.combo as f32) - 1.0) * 0.05; let pitch = 0.9 + ((state.combo as f32) - 1.0) * 0.05;
let pos = client.position();
client.play_sound( client.play_sound(
Sound::BlockNoteBlockBass, Sound::BlockNoteBlockBass,
SoundCategory::Master, SoundCategory::Master,
pos, pos.0,
1.0, 1.0,
pitch, pitch,
); );
@ -163,14 +198,14 @@ fn manage_blocks(mut clients: Query<(&mut Client, &mut GameState, &mut Instance)
} }
} }
fn manage_chunks(mut clients: Query<(&mut Client, &mut Instance)>) { fn manage_chunks(mut clients: Query<(&Position, &OldPosition, &mut Instance), With<Client>>) {
for (client, mut instance) in &mut clients { for (pos, old_pos, mut instance) in &mut clients {
let old_view = client.old_view().with_dist(VIEW_DIST); let old_view = ChunkView::new(old_pos.chunk_pos(), VIEW_DIST);
let view = client.view().with_dist(VIEW_DIST); let view = ChunkView::new(pos.chunk_pos(), VIEW_DIST);
if old_view != view { if old_view != view {
for pos in old_view.diff(view) { for pos in old_view.diff(view) {
instance.chunk_entry(pos).or_default(); instance.remove_chunk(pos);
} }
for pos in view.diff(old_view) { for pos in view.diff(old_view) {
@ -180,38 +215,6 @@ fn manage_chunks(mut clients: Query<(&mut Client, &mut Instance)>) {
} }
} }
fn reset(client: &mut Client, state: &mut GameState, instance: &mut Instance) {
// Load chunks around spawn to avoid double void reset
for z in -1..3 {
for x in -2..2 {
instance.insert_chunk([x, z], Chunk::default());
}
}
state.score = 0;
state.combo = 0;
for block in &state.blocks {
instance.set_block(*block, BlockState::AIR);
}
state.blocks.clear();
state.blocks.push_back(START_POS);
instance.set_block(START_POS, BlockState::STONE);
for _ in 0..10 {
generate_next_block(state, instance, false);
}
client.set_position([
START_POS.x as f64 + 0.5,
START_POS.y as f64 + 1.0,
START_POS.z as f64 + 0.5,
]);
client.set_velocity([0f32, 0f32, 0f32]);
client.set_yaw(0f32);
client.set_pitch(0f32)
}
fn generate_next_block(state: &mut GameState, instance: &mut Instance, in_game: bool) { fn generate_next_block(state: &mut GameState, instance: &mut Instance, in_game: bool) {
if in_game { if in_game {
let removed_block = state.blocks.pop_front().unwrap(); let removed_block = state.blocks.pop_front().unwrap();

View file

@ -1,3 +1,7 @@
#![allow(clippy::type_complexity)]
use std::fmt;
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::prelude::*; use valence::prelude::*;
@ -19,23 +23,7 @@ pub fn main() {
} }
#[derive(Resource)] #[derive(Resource)]
struct ParticleSpawner { struct ParticleVec(Vec<Particle>);
particles: Vec<Particle>,
index: usize,
}
impl ParticleSpawner {
pub fn new() -> Self {
Self {
particles: create_particle_vec(),
index: 0,
}
}
pub fn next(&mut self) {
self.index = (self.index + 1) % self.particles.len();
}
}
fn setup(mut commands: Commands, server: Res<Server>) { fn setup(mut commands: Commands, server: Res<Server>) {
let mut instance = server.new_instance(DimensionId::default()); let mut instance = server.new_instance(DimensionId::default());
@ -50,35 +38,48 @@ fn setup(mut commands: Commands, server: Res<Server>) {
commands.spawn(instance); commands.spawn(instance);
let spawner = ParticleSpawner::new(); commands.insert_resource(ParticleVec(create_particle_vec()));
commands.insert_resource(spawner)
} }
fn init_clients( fn init_clients(
mut clients: Query<&mut Client, Added<Client>>, mut clients: Query<
(
Entity,
&UniqueId,
&mut Position,
&mut Location,
&mut GameMode,
),
Added<Client>,
>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) { ) {
for mut client in &mut clients { for (entity, uuid, mut pos, mut loc, mut game_mode) in &mut clients {
client.set_position([0.5, SPAWN_Y as f64 + 1.0, 0.5]); pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]);
client.set_instance(instances.single()); loc.0 = instances.single();
client.set_game_mode(GameMode::Creative); *game_mode = GameMode::Creative;
commands
.entity(entity)
.insert(McEntity::with_uuid(EntityKind::Player, loc.0, uuid.0));
} }
} }
fn manage_particles( fn manage_particles(
mut spawner: ResMut<ParticleSpawner>, particles: Res<ParticleVec>,
server: Res<Server>, server: Res<Server>,
mut instances: Query<&mut Instance>, mut instances: Query<&mut Instance>,
mut particle_idx: Local<usize>,
) { ) {
if server.current_tick() % 20 == 0 { if server.current_tick() % 20 != 0 {
spawner.next();
}
if server.current_tick() % 5 != 0 {
return; return;
} }
let particle = &spawner.particles[spawner.index]; let particle = &particles.0[*particle_idx];
*particle_idx = (*particle_idx + 1) % particles.0.len();
let name = dbg_name(particle); let name = dbg_name(particle);
let pos = [0.5, SPAWN_Y as f64 + 2.0, 5.0]; let pos = [0.5, SPAWN_Y as f64 + 2.0, 5.0];
@ -90,7 +91,7 @@ fn manage_particles(
instance.set_action_bar(name.bold()); instance.set_action_bar(name.bold());
} }
fn dbg_name(dbg: &impl std::fmt::Debug) -> String { fn dbg_name(dbg: &impl fmt::Debug) -> String {
let string = format!("{dbg:?}"); let string = format!("{dbg:?}");
string string

View file

@ -1,5 +1,6 @@
#![allow(clippy::type_complexity)]
use rand::Rng; use rand::Rng;
use valence::client::despawn_disconnected_clients;
use valence::client::event::default_event_handler; use valence::client::event::default_event_handler;
use valence::player_list::Entry; use valence::player_list::Entry;
use valence::prelude::*; use valence::prelude::*;
@ -49,14 +50,25 @@ fn setup(mut commands: Commands, server: Res<Server>, mut player_list: ResMut<Pl
} }
fn init_clients( fn init_clients(
mut clients: Query<&mut Client, Added<Client>>, mut clients: Query<
(
&mut Client,
&mut Position,
&mut Location,
&mut GameMode,
&Username,
&Properties,
&UniqueId,
),
Added<Client>,
>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
mut player_list: ResMut<PlayerList>, mut player_list: ResMut<PlayerList>,
) { ) {
for mut client in &mut clients { for (mut client, mut pos, mut loc, mut game_mode, username, props, uuid) in &mut clients {
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
client.set_instance(instances.single()); loc.0 = instances.single();
client.set_game_mode(GameMode::Creative); *game_mode = GameMode::Creative;
client.send_message( client.send_message(
"Please open your player list (tab key)." "Please open your player list (tab key)."
@ -65,13 +77,13 @@ fn init_clients(
); );
let entry = PlayerListEntry::new() let entry = PlayerListEntry::new()
.with_username(client.username()) .with_username(&username.0)
.with_properties(client.properties()) // For the player's skin and cape. .with_properties(props.0.clone()) // For the player's skin and cape.
.with_game_mode(client.game_mode()) .with_game_mode(*game_mode)
.with_ping(0) // Use negative values to indicate missing. .with_ping(0) // Use negative values to indicate missing.
.with_display_name(Some("".color(Color::new(255, 87, 66)))); .with_display_name(Some("".color(Color::new(255, 87, 66))));
player_list.insert(client.uuid(), entry); player_list.insert(uuid.0, entry);
} }
} }
@ -108,12 +120,13 @@ fn update_player_list(mut player_list: ResMut<PlayerList>, server: Res<Server>)
} }
fn remove_disconnected_clients_from_player_list( fn remove_disconnected_clients_from_player_list(
clients: Query<&mut Client>, mut clients: RemovedComponents<Client>,
mut player_list: ResMut<PlayerList>, mut player_list: ResMut<PlayerList>,
uuids: Query<&UniqueId>,
) { ) {
for client in &clients { for client in clients.iter() {
if client.is_disconnected() { if let Ok(UniqueId(uuid)) = uuids.get(client) {
player_list.remove(client.uuid()); player_list.remove(*uuid);
} }
} }
} }

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use valence::client::despawn_disconnected_clients; use valence::client::despawn_disconnected_clients;
use valence::client::event::{ use valence::client::event::{
default_event_handler, PlayerInteract, ResourcePackStatus, ResourcePackStatusChange, default_event_handler, PlayerInteract, ResourcePackStatus, ResourcePackStatusChange,
@ -52,15 +54,30 @@ fn setup(mut commands: Commands, server: Res<Server>) {
} }
fn init_clients( fn init_clients(
mut clients: Query<&mut Client, Added<Client>>, mut clients: Query<
(
Entity,
&UniqueId,
&mut Client,
&mut Position,
&mut Location,
&mut GameMode,
),
Added<Client>,
>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) { ) {
for mut client in &mut clients { for (entity, uuid, mut client, mut pos, mut loc, mut game_mode) in &mut clients {
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
client.set_instance(instances.single()); loc.0 = instances.single();
client.set_game_mode(GameMode::Creative); *game_mode = GameMode::Creative;
client.send_message("Hit the sheep to prompt for the resource pack.".italic()); client.send_message("Hit the sheep to prompt for the resource pack.".italic());
commands
.entity(entity)
.insert(McEntity::with_uuid(EntityKind::Player, loc.0, uuid.0));
} }
} }

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use std::net::SocketAddr; use std::net::SocketAddr;
use valence::prelude::*; use valence::prelude::*;

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
@ -106,23 +108,31 @@ fn setup(mut commands: Commands, server: Res<Server>) {
} }
fn init_clients( fn init_clients(
mut clients: Query<&mut Client, Added<Client>>, mut clients: Query<
(
Entity,
&UniqueId,
&mut IsFlat,
&mut GameMode,
&mut Position,
&mut Location,
),
Added<Client>,
>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
mut commands: Commands, mut commands: Commands,
) { ) {
for mut client in &mut clients { for (entity, uuid, mut is_flat, mut game_mode, mut pos, mut loc) in &mut clients {
let instance = instances.single(); let instance = instances.single();
client.set_flat(true); is_flat.0 = true;
client.set_game_mode(GameMode::Creative); *game_mode = GameMode::Creative;
client.set_position(SPAWN_POS); pos.0 = SPAWN_POS;
client.set_instance(instance); loc.0 = instance;
commands.spawn(McEntity::with_uuid( commands
EntityKind::Player, .entity(entity)
instance, .insert(McEntity::with_uuid(EntityKind::Player, loc.0, uuid.0));
client.uuid(),
));
} }
} }
@ -134,13 +144,13 @@ fn remove_unviewed_chunks(mut instances: Query<&mut Instance>) {
fn update_client_views( fn update_client_views(
mut instances: Query<&mut Instance>, mut instances: Query<&mut Instance>,
mut clients: Query<&mut Client>, mut clients: Query<(&mut Client, View, OldView)>,
mut state: ResMut<GameState>, mut state: ResMut<GameState>,
) { ) {
let instance = instances.single_mut(); let instance = instances.single_mut();
for client in &mut clients { for (client, view, old_view) in &mut clients {
let view = client.view(); let view = view.get();
let queue_pos = |pos| { let queue_pos = |pos| {
if instance.chunk(pos).is_none() { if instance.chunk(pos).is_none() {
match state.pending.entry(pos) { match state.pending.entry(pos) {
@ -162,7 +172,7 @@ fn update_client_views(
if client.is_added() { if client.is_added() {
view.iter().for_each(queue_pos); view.iter().for_each(queue_pos);
} else { } else {
let old_view = client.old_view(); let old_view = old_view.get();
if old_view != view { if old_view != view {
view.diff(old_view).for_each(queue_pos); view.diff(old_view).for_each(queue_pos);
} }

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
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::prelude::*; use valence::prelude::*;
@ -39,13 +41,13 @@ fn setup(world: &mut World) {
} }
fn init_clients( fn init_clients(
mut clients: Query<&mut Client, Added<Client>>, mut clients: Query<(&mut Client, &mut Position, &mut Location, &mut GameMode), Added<Client>>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
) { ) {
for mut client in &mut clients { for (mut client, mut pos, mut loc, mut game_mode) in &mut clients {
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]); pos.0 = [0.0, SPAWN_Y as f64 + 1.0, 0.0].into();
client.set_instance(instances.single()); loc.0 = instances.single();
client.set_game_mode(GameMode::Creative); *game_mode = GameMode::Creative;
client.send_message("Welcome to the text example.".bold()); client.send_message("Welcome to the text example.".bold());
client client

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@ use std::cmp;
use anyhow::bail; use anyhow::bail;
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_ecs::query::WorldQuery;
use bevy_ecs::system::{SystemParam, SystemState}; use bevy_ecs::system::{SystemParam, SystemState};
use glam::{DVec3, Vec3}; use glam::{DVec3, Vec3};
use paste::paste; use paste::paste;
@ -30,7 +31,12 @@ use valence_protocol::packet::C2sPlayPacket;
use valence_protocol::tracked_data::Pose; use valence_protocol::tracked_data::Pose;
use valence_protocol::types::{Difficulty, Direction, Hand}; use valence_protocol::types::{Difficulty, Direction, Hand};
use super::{
CursorItem, KeepaliveState, PlayerActionSequence, PlayerInventoryState, TeleportState,
ViewDistance,
};
use crate::client::Client; use crate::client::Client;
use crate::component::{Look, OnGround, Ping, Position};
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; use crate::server::EventLoopSchedule;
@ -635,24 +641,37 @@ events! {
} }
} }
#[derive(WorldQuery)]
#[world_query(mutable)]
pub(crate) struct EventLoopQuery {
entity: Entity,
client: &'static mut Client,
teleport_state: &'static mut TeleportState,
keepalive_state: &'static mut KeepaliveState,
cursor_item: &'static mut CursorItem,
inventory: &'static mut Inventory,
position: &'static mut Position,
look: &'static mut Look,
on_ground: &'static mut OnGround,
ping: &'static mut Ping,
player_action_sequence: &'static mut PlayerActionSequence,
player_inventory_state: &'static mut PlayerInventoryState,
}
/// An exclusive system for running the event loop schedule. /// An exclusive system for running the event loop schedule.
#[allow(clippy::type_complexity)]
pub(crate) fn run_event_loop( pub(crate) fn run_event_loop(
world: &mut World, world: &mut World,
state: &mut SystemState<(Query<(Entity, &mut Client, &mut Inventory)>, ClientEvents)>, state: &mut SystemState<(Query<EventLoopQuery>, ClientEvents, Commands)>,
mut clients_to_check: Local<Vec<Entity>>, mut clients_to_check: Local<Vec<Entity>>,
) { ) {
let (mut clients, mut events) = state.get_mut(world); let (mut clients, mut events, mut commands) = state.get_mut(world);
update_all_event_buffers(&mut events); update_all_event_buffers(&mut events);
for (entity, client, inventory) in &mut clients { for mut q in &mut clients {
let client = client.into_inner(); let Ok(bytes) = q.client.conn.try_recv() else {
let inventory = inventory.into_inner();
let Ok(bytes) = client.conn.try_recv() else {
// Client is disconnected. // Client is disconnected.
client.is_disconnected = true; commands.entity(q.entity).remove::<Client>();
continue; continue;
}; };
@ -661,81 +680,75 @@ pub(crate) fn run_event_loop(
continue; continue;
} }
client.dec.queue_bytes(bytes); q.client.dec.queue_bytes(bytes);
match handle_one_packet(client, inventory, entity, &mut events) { match handle_one_packet(&mut q, &mut events) {
Ok(had_packet) => { Ok(had_packet) => {
if had_packet { if had_packet {
// We decoded one packet, but there might be more. // We decoded one packet, but there might be more.
clients_to_check.push(entity); clients_to_check.push(q.entity);
} }
} }
Err(e) => { Err(e) => {
warn!( warn!("failed to dispatch events for client {:?}: {e:?}", q.entity);
username = %client.username, commands.entity(q.entity).remove::<Client>();
uuid = %client.uuid,
ip = %client.ip,
"failed to dispatch events: {e:#}"
);
client.is_disconnected = true;
} }
} }
} }
state.apply(world);
// Keep looping until all serverbound packets are decoded. // Keep looping until all serverbound packets are decoded.
while !clients_to_check.is_empty() { while !clients_to_check.is_empty() {
world.run_schedule(EventLoopSchedule); world.run_schedule(EventLoopSchedule);
let (mut clients, mut events) = state.get_mut(world); let (mut clients, mut events, mut commands) = 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 q) = clients.get_mut(entity) else {
// Client must have been deleted during the last run of the schedule. // Client must have been deleted during the last run of the schedule.
return false; return false;
}; };
match handle_one_packet(&mut client, &mut inventory, entity, &mut events) { match handle_one_packet(&mut q, &mut events) {
Ok(had_packet) => had_packet, Ok(had_packet) => had_packet,
Err(e) => { Err(e) => {
warn!( warn!("failed to dispatch events for client {:?}: {e:?}", q.entity);
username = %client.username, commands.entity(entity).remove::<Client>();
uuid = %client.uuid,
ip = %client.ip,
"failed to dispatch events: {e:#}"
);
client.is_disconnected = true;
false false
} }
} }
}); });
state.apply(world);
} }
} }
fn handle_one_packet( fn handle_one_packet(
client: &mut Client, q: &mut EventLoopQueryItem,
inventory: &mut Inventory,
entity: Entity,
events: &mut ClientEvents, events: &mut ClientEvents,
) -> anyhow::Result<bool> { ) -> anyhow::Result<bool> {
let Some(pkt) = client.dec.try_next_packet::<C2sPlayPacket>()? else { let Some(pkt) = q.client.dec.try_next_packet::<C2sPlayPacket>()? else {
// No packets to decode. // No packets to decode.
return Ok(false); return Ok(false);
}; };
let entity = q.entity;
match pkt { match pkt {
C2sPlayPacket::TeleportConfirmC2s(p) => { C2sPlayPacket::TeleportConfirmC2s(p) => {
if client.pending_teleports == 0 { if q.teleport_state.pending_teleports == 0 {
bail!("unexpected teleport confirmation"); bail!("unexpected teleport confirmation");
} }
let got = p.teleport_id.0 as u32; let got = p.teleport_id.0 as u32;
let expected = client let expected = q
.teleport_state
.teleport_id_counter .teleport_id_counter
.wrapping_sub(client.pending_teleports); .wrapping_sub(q.teleport_state.pending_teleports);
if got == expected { if got == expected {
client.pending_teleports -= 1; q.teleport_state.pending_teleports -= 1;
} else { } else {
bail!("unexpected teleport ID (expected {expected}, got {got}"); bail!("unexpected teleport ID (expected {expected}, got {got}");
} }
@ -814,7 +827,7 @@ fn handle_one_packet(
} }
C2sPlayPacket::ClickSlotC2s(p) => { C2sPlayPacket::ClickSlotC2s(p) => {
if p.slot_idx < 0 { if p.slot_idx < 0 {
if let Some(stack) = client.cursor_item.take() { if let Some(stack) = q.cursor_item.0.take() {
events.2.drop_item_stack.send(DropItemStack { events.2.drop_item_stack.send(DropItemStack {
client: entity, client: entity,
from_slot: None, from_slot: None,
@ -823,13 +836,13 @@ fn handle_one_packet(
} }
} else if p.mode == ClickMode::DropKey { } else if p.mode == ClickMode::DropKey {
let entire_stack = p.button == 1; let entire_stack = p.button == 1;
if let Some(stack) = inventory.slot(p.slot_idx as u16) { if let Some(stack) = q.inventory.slot(p.slot_idx as u16) {
let dropped = if entire_stack || stack.count() == 1 { let dropped = if entire_stack || stack.count() == 1 {
inventory.replace_slot(p.slot_idx as u16, None) q.inventory.replace_slot(p.slot_idx as u16, None)
} else { } else {
let mut stack = stack.clone(); let mut stack = stack.clone();
stack.set_count(stack.count() - 1); stack.set_count(stack.count() - 1);
let mut old_slot = inventory.replace_slot(p.slot_idx as u16, Some(stack)); let mut old_slot = q.inventory.replace_slot(p.slot_idx as u16, Some(stack));
// we already checked that the slot was not empty and that the // we already checked that the slot was not empty and that the
// stack count is > 1 // stack count is > 1
old_slot.as_mut().unwrap().set_count(1); old_slot.as_mut().unwrap().set_count(1);
@ -899,17 +912,17 @@ fn handle_one_packet(
}); });
} }
C2sPlayPacket::KeepAliveC2s(p) => { C2sPlayPacket::KeepAliveC2s(p) => {
if client.got_keepalive { if q.keepalive_state.got_keepalive {
bail!("unexpected keepalive"); bail!("unexpected keepalive");
} else if p.id != client.last_keepalive_id { } else if p.id != q.keepalive_state.last_keepalive_id {
bail!( bail!(
"keepalive IDs don't match (expected {}, got {})", "keepalive IDs don't match (expected {}, got {})",
client.last_keepalive_id, q.keepalive_state.last_keepalive_id,
p.id p.id
); );
} else { } else {
client.got_keepalive = true; q.keepalive_state.got_keepalive = true;
client.ping = client.keepalive_sent_time.elapsed().as_millis() as i32; q.ping.0 = q.keepalive_state.keepalive_sent_time.elapsed().as_millis() as i32;
} }
} }
C2sPlayPacket::UpdateDifficultyLockC2s(p) => { C2sPlayPacket::UpdateDifficultyLockC2s(p) => {
@ -919,23 +932,24 @@ fn handle_one_packet(
}); });
} }
C2sPlayPacket::PositionAndOnGroundC2s(p) => { C2sPlayPacket::PositionAndOnGroundC2s(p) => {
if client.pending_teleports != 0 { if q.teleport_state.pending_teleports != 0 {
return Ok(false); return Ok(false);
} }
events.1.player_move.send(PlayerMove { events.1.player_move.send(PlayerMove {
client: entity, client: entity,
position: p.position.into(), position: p.position.into(),
yaw: client.yaw, yaw: q.look.yaw,
pitch: client.pitch, pitch: q.look.pitch,
on_ground: client.on_ground, on_ground: q.on_ground.0,
}); });
client.position = p.position.into(); q.position.0 = p.position.into();
client.on_ground = p.on_ground; q.teleport_state.synced_pos = p.position.into();
q.on_ground.0 = p.on_ground;
} }
C2sPlayPacket::FullC2s(p) => { C2sPlayPacket::FullC2s(p) => {
if client.pending_teleports != 0 { if q.teleport_state.pending_teleports != 0 {
return Ok(false); return Ok(false);
} }
@ -947,45 +961,50 @@ fn handle_one_packet(
on_ground: p.on_ground, on_ground: p.on_ground,
}); });
client.position = p.position.into(); q.position.0 = p.position.into();
client.yaw = p.yaw; q.teleport_state.synced_pos = p.position.into();
client.pitch = p.pitch; q.look.yaw = p.yaw;
client.on_ground = p.on_ground; q.teleport_state.synced_look.yaw = p.yaw;
q.look.pitch = p.pitch;
q.teleport_state.synced_look.pitch = p.pitch;
q.on_ground.0 = p.on_ground;
} }
C2sPlayPacket::LookAndOnGroundC2s(p) => { C2sPlayPacket::LookAndOnGroundC2s(p) => {
if client.pending_teleports != 0 { if q.teleport_state.pending_teleports != 0 {
return Ok(false); return Ok(false);
} }
events.1.player_move.send(PlayerMove { events.1.player_move.send(PlayerMove {
client: entity, client: entity,
position: client.position, position: q.position.0,
yaw: p.yaw, yaw: p.yaw,
pitch: p.pitch, pitch: p.pitch,
on_ground: p.on_ground, on_ground: p.on_ground,
}); });
client.yaw = p.yaw; q.look.yaw = p.yaw;
client.pitch = p.pitch; q.teleport_state.synced_look.yaw = p.yaw;
client.on_ground = p.on_ground; q.look.pitch = p.pitch;
q.teleport_state.synced_look.pitch = p.pitch;
q.on_ground.0 = p.on_ground;
} }
C2sPlayPacket::OnGroundOnlyC2s(p) => { C2sPlayPacket::OnGroundOnlyC2s(p) => {
if client.pending_teleports != 0 { if q.teleport_state.pending_teleports != 0 {
return Ok(false); return Ok(false);
} }
events.1.player_move.send(PlayerMove { events.1.player_move.send(PlayerMove {
client: entity, client: entity,
position: client.position, position: q.position.0,
yaw: client.yaw, yaw: q.look.yaw,
pitch: client.pitch, pitch: q.look.pitch,
on_ground: p.on_ground, on_ground: p.on_ground,
}); });
client.on_ground = p.on_ground; q.on_ground.0 = p.on_ground;
} }
C2sPlayPacket::VehicleMoveC2s(p) => { C2sPlayPacket::VehicleMoveC2s(p) => {
if client.pending_teleports != 0 { if q.teleport_state.pending_teleports != 0 {
return Ok(false); return Ok(false);
} }
@ -996,9 +1015,12 @@ fn handle_one_packet(
pitch: p.pitch, pitch: p.pitch,
}); });
client.position = p.position.into(); q.position.0 = p.position.into();
client.yaw = p.yaw; q.teleport_state.synced_pos = p.position.into();
client.pitch = p.pitch; q.look.yaw = p.yaw;
q.teleport_state.synced_look.yaw = p.yaw;
q.look.pitch = p.pitch;
q.teleport_state.synced_look.pitch = p.pitch;
} }
C2sPlayPacket::BoatPaddleStateC2s(p) => { C2sPlayPacket::BoatPaddleStateC2s(p) => {
events.2.boat_paddle_state.send(BoatPaddleState { events.2.boat_paddle_state.send(BoatPaddleState {
@ -1031,7 +1053,7 @@ fn handle_one_packet(
}, },
C2sPlayPacket::PlayerActionC2s(p) => { C2sPlayPacket::PlayerActionC2s(p) => {
if p.sequence.0 != 0 { if p.sequence.0 != 0 {
client.block_change_sequence = cmp::max(p.sequence.0, client.block_change_sequence); q.player_action_sequence.0 = cmp::max(p.sequence.0, q.player_action_sequence.0);
} }
match p.action { match p.action {
@ -1058,31 +1080,41 @@ fn handle_one_packet(
}) })
} }
PlayerAction::DropAllItems => { PlayerAction::DropAllItems => {
if let Some(stack) = inventory.replace_slot(client.held_item_slot(), None) { if let Some(stack) = q
client.inventory_slots_modified |= 1 << client.held_item_slot(); .inventory
.replace_slot(q.player_inventory_state.held_item_slot(), None)
{
q.player_inventory_state.slots_changed |=
1 << q.player_inventory_state.held_item_slot();
events.2.drop_item_stack.send(DropItemStack { events.2.drop_item_stack.send(DropItemStack {
client: entity, client: entity,
from_slot: Some(client.held_item_slot()), from_slot: Some(q.player_inventory_state.held_item_slot()),
stack, stack,
}); });
} }
} }
PlayerAction::DropItem => { PlayerAction::DropItem => {
if let Some(stack) = inventory.slot(client.held_item_slot()) { if let Some(stack) = q.inventory.slot(q.player_inventory_state.held_item_slot())
{
let mut old_slot = if stack.count() == 1 { let mut old_slot = if stack.count() == 1 {
inventory.replace_slot(client.held_item_slot(), None) q.inventory
.replace_slot(q.player_inventory_state.held_item_slot(), None)
} else { } else {
let mut stack = stack.clone(); let mut stack = stack.clone();
stack.set_count(stack.count() - 1); stack.set_count(stack.count() - 1);
inventory.replace_slot(client.held_item_slot(), Some(stack.clone())) q.inventory.replace_slot(
q.player_inventory_state.held_item_slot(),
Some(stack.clone()),
)
} }
.expect("old slot should exist"); // we already checked that the slot was not empty .expect("old slot should exist"); // we already checked that the slot was not empty
client.inventory_slots_modified |= 1 << client.held_item_slot(); q.player_inventory_state.slots_changed |=
1 << q.player_inventory_state.held_item_slot();
old_slot.set_count(1); old_slot.set_count(1);
events.2.drop_item_stack.send(DropItemStack { events.2.drop_item_stack.send(DropItemStack {
client: entity, client: entity,
from_slot: Some(client.held_item_slot()), from_slot: Some(q.player_inventory_state.held_item_slot()),
stack: old_slot, stack: old_slot,
}); });
} }
@ -1310,7 +1342,7 @@ fn handle_one_packet(
} }
C2sPlayPacket::PlayerInteractBlockC2s(p) => { C2sPlayPacket::PlayerInteractBlockC2s(p) => {
if p.sequence.0 != 0 { if p.sequence.0 != 0 {
client.block_change_sequence = cmp::max(p.sequence.0, client.block_change_sequence); q.player_action_sequence.0 = cmp::max(p.sequence.0, q.player_action_sequence.0);
} }
events.4.player_interact_block.send(PlayerInteractBlock { events.4.player_interact_block.send(PlayerInteractBlock {
@ -1325,7 +1357,7 @@ fn handle_one_packet(
} }
C2sPlayPacket::PlayerInteractItemC2s(p) => { C2sPlayPacket::PlayerInteractItemC2s(p) => {
if p.sequence.0 != 0 { if p.sequence.0 != 0 {
client.block_change_sequence = cmp::max(p.sequence.0, client.block_change_sequence); q.player_action_sequence.0 = cmp::max(p.sequence.0, q.player_action_sequence.0);
} }
events.4.player_interact_item.send(PlayerInteractItem { events.4.player_interact_item.send(PlayerInteractItem {
@ -1356,7 +1388,7 @@ fn handle_one_packet(
/// not 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>, &mut ViewDistance)>,
mut update_settings: EventReader<ClientSettings>, mut update_settings: EventReader<ClientSettings>,
mut player_move: EventReader<PlayerMove>, mut player_move: EventReader<PlayerMove>,
mut start_sneaking: EventReader<StartSneaking>, mut start_sneaking: EventReader<StartSneaking>,
@ -1373,33 +1405,20 @@ pub fn default_event_handler(
.. ..
} in update_settings.iter() } in update_settings.iter()
{ {
let Ok((mut client, entity)) = clients.get_mut(*client) else { if let Ok((_, mcentity, mut view_dist)) = clients.get_mut(*client) {
continue view_dist.set(*view_distance);
};
client.set_view_distance(*view_distance); if let Some(mut entity) = mcentity {
if let TrackedData::Player(player) = entity.data_mut() {
let player = client.player_mut(); player.set_cape(displayed_skin_parts.cape());
player.set_jacket(displayed_skin_parts.jacket());
player.set_cape(displayed_skin_parts.cape()); player.set_left_sleeve(displayed_skin_parts.left_sleeve());
player.set_jacket(displayed_skin_parts.jacket()); player.set_right_sleeve(displayed_skin_parts.right_sleeve());
player.set_left_sleeve(displayed_skin_parts.left_sleeve()); player.set_left_pants_leg(displayed_skin_parts.left_pants_leg());
player.set_right_sleeve(displayed_skin_parts.right_sleeve()); player.set_right_pants_leg(displayed_skin_parts.right_pants_leg());
player.set_left_pants_leg(displayed_skin_parts.left_pants_leg()); player.set_hat(displayed_skin_parts.hat());
player.set_right_pants_leg(displayed_skin_parts.right_pants_leg()); player.set_main_arm(*main_hand as u8);
player.set_hat(displayed_skin_parts.hat()); }
player.set_main_arm(*main_hand as u8);
if let Some(mut entity) = entity {
if let TrackedData::Player(player) = entity.data_mut() {
player.set_cape(displayed_skin_parts.cape());
player.set_jacket(displayed_skin_parts.jacket());
player.set_left_sleeve(displayed_skin_parts.left_sleeve());
player.set_right_sleeve(displayed_skin_parts.right_sleeve());
player.set_left_pants_leg(displayed_skin_parts.left_pants_leg());
player.set_right_pants_leg(displayed_skin_parts.right_pants_leg());
player.set_hat(displayed_skin_parts.hat());
player.set_main_arm(*main_hand as u8);
} }
} }
} }
@ -1413,67 +1432,55 @@ pub fn default_event_handler(
.. ..
} in player_move.iter() } in player_move.iter()
{ {
let Ok((_, Some(mut entity))) = clients.get_mut(*client) else { if let Ok((_, Some(mut mcentity), _)) = clients.get_mut(*client) {
continue mcentity.set_position(*position);
}; mcentity.set_yaw(*yaw);
mcentity.set_head_yaw(*yaw);
entity.set_position(*position); mcentity.set_pitch(*pitch);
entity.set_yaw(*yaw); mcentity.set_on_ground(*on_ground);
entity.set_head_yaw(*yaw); }
entity.set_pitch(*pitch);
entity.set_on_ground(*on_ground);
} }
for StartSneaking { client } in start_sneaking.iter() { for StartSneaking { client } in start_sneaking.iter() {
let Ok((_, Some(mut entity))) = clients.get_mut(*client) else { if let Ok((_, Some(mut entity), _)) = clients.get_mut(*client) {
continue if let TrackedData::Player(player) = entity.data_mut() {
player.set_pose(Pose::Sneaking);
}
}; };
if let TrackedData::Player(player) = entity.data_mut() {
player.set_pose(Pose::Sneaking);
}
} }
for StopSneaking { client } in stop_sneaking.iter() { for StopSneaking { client } in stop_sneaking.iter() {
let Ok((_, Some(mut entity))) = clients.get_mut(*client) else { if let Ok((_, Some(mut entity), _)) = clients.get_mut(*client) {
continue if let TrackedData::Player(player) = entity.data_mut() {
player.set_pose(Pose::Standing);
}
}; };
if let TrackedData::Player(player) = entity.data_mut() {
player.set_pose(Pose::Standing);
}
} }
for StartSprinting { client } in start_sprinting.iter() { for StartSprinting { client } in start_sprinting.iter() {
let Ok((_, Some(mut entity))) = clients.get_mut(*client) else { if let Ok((_, Some(mut entity), _)) = clients.get_mut(*client) {
continue if let TrackedData::Player(player) = entity.data_mut() {
player.set_sprinting(true);
}
}; };
if let TrackedData::Player(player) = entity.data_mut() {
player.set_sprinting(true);
}
} }
for StopSprinting { client } in stop_sprinting.iter() { for StopSprinting { client } in stop_sprinting.iter() {
let Ok((_, Some(mut entity))) = clients.get_mut(*client) else { if let Ok((_, Some(mut entity), _)) = clients.get_mut(*client) {
continue if let TrackedData::Player(player) = entity.data_mut() {
player.set_sprinting(false);
}
}; };
if let TrackedData::Player(player) = entity.data_mut() {
player.set_sprinting(false);
}
} }
for HandSwing { client, hand } in swing_arm.iter() { for HandSwing { client, hand } in swing_arm.iter() {
let Ok((_, Some(mut entity))) = clients.get_mut(*client) else { if let Ok((_, Some(mut entity), _)) = clients.get_mut(*client) {
continue if entity.kind() == EntityKind::Player {
entity.trigger_animation(match hand {
Hand::Main => EntityAnimation::SwingMainHand,
Hand::Off => EntityAnimation::SwingOffHand,
});
}
}; };
if entity.kind() == EntityKind::Player {
entity.trigger_animation(match hand {
Hand::Main => EntityAnimation::SwingMainHand,
Hand::Off => EntityAnimation::SwingOffHand,
});
}
} }
} }

View file

@ -0,0 +1,204 @@
use std::fmt;
/// Contains shared components and world queries.
use bevy_ecs::prelude::*;
use glam::{DVec3, Vec3};
use uuid::Uuid;
use valence_protocol::types::{GameMode as ProtocolGameMode, Property};
use crate::util::{from_yaw_and_pitch, to_yaw_and_pitch};
use crate::view::ChunkPos;
use crate::NULL_ENTITY;
/// A [`Component`] for marking entities that should be despawned at the end of
/// the tick.
///
/// In Valence, some built-in components such as [`McEntity`] are not allowed to
/// be removed from the [`World`] directly. Instead, you must give the entities
/// you wish to despawn the `Despawned` component. At the end of the tick,
/// Valence will despawn all entities with this component for you.
///
/// It is legal to remove components or delete entities that Valence does not
/// know about at any time.
///
/// [`McEntity`]: crate::entity::McEntity
#[derive(Component, Copy, Clone, Default, PartialEq, Eq, Debug)]
pub struct Despawned;
#[derive(Component, Default, Clone, PartialEq, Eq, Debug)]
pub struct UniqueId(pub Uuid);
#[derive(Component, Clone, PartialEq, Eq, Debug)]
pub struct Username(pub String);
impl fmt::Display for Username {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Component, Clone, PartialEq, Eq, Debug)]
pub struct Properties(pub Vec<Property>);
impl Properties {
/// Finds the property with the name "textures".
pub fn textures(&self) -> Option<&Property> {
self.0.iter().find(|prop| prop.name == "textures")
}
/// Finds the property with the name "textures".
pub fn textures_mut(&mut self) -> Option<&mut Property> {
self.0.iter_mut().find(|prop| prop.name == "textures")
}
}
#[derive(Component, Copy, Clone, PartialEq, Eq, Debug, Default)]
pub enum GameMode {
#[default]
Survival,
Creative,
Adventure,
Spectator,
}
impl From<GameMode> for ProtocolGameMode {
fn from(gm: GameMode) -> Self {
match gm {
GameMode::Survival => ProtocolGameMode::Survival,
GameMode::Creative => ProtocolGameMode::Creative,
GameMode::Adventure => ProtocolGameMode::Adventure,
GameMode::Spectator => ProtocolGameMode::Spectator,
}
}
}
impl From<ProtocolGameMode> for GameMode {
fn from(gm: ProtocolGameMode) -> Self {
match gm {
ProtocolGameMode::Survival => GameMode::Survival,
ProtocolGameMode::Creative => GameMode::Creative,
ProtocolGameMode::Adventure => GameMode::Adventure,
ProtocolGameMode::Spectator => GameMode::Spectator,
}
}
}
/// Delay measured in milliseconds. Negative values indicate absence.
#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct Ping(pub i32);
impl Default for Ping {
fn default() -> Self {
Self(-1)
}
}
/// Contains the [`Instance`] an entity is located in. For the coordinates
/// within the instance, see [`Position`].
///
/// [`Instance`]: crate::instance::Instance
#[derive(Component, Copy, Clone, PartialEq, Eq, Debug)]
pub struct Location(pub Entity);
impl Default for Location {
fn default() -> Self {
Self(NULL_ENTITY)
}
}
#[derive(Component, Copy, Clone, PartialEq, Eq, Debug)]
pub struct OldLocation(Entity);
impl OldLocation {
pub(crate) fn update(mut query: Query<(&Location, &mut OldLocation), Changed<Location>>) {
for (loc, mut old_loc) in &mut query {
old_loc.0 = loc.0;
}
}
pub fn new(instance: Entity) -> Self {
Self(instance)
}
pub fn get(&self) -> Entity {
self.0
}
}
impl Default for OldLocation {
fn default() -> Self {
Self(NULL_ENTITY)
}
}
#[derive(Component, Copy, Clone, PartialEq, Default, Debug)]
pub struct Position(pub DVec3);
impl Position {
pub fn chunk_pos(&self) -> ChunkPos {
ChunkPos::from_dvec3(self.0)
}
pub fn get(&self) -> DVec3 {
self.0
}
pub fn set(&mut self, pos: impl Into<DVec3>) {
self.0 = pos.into();
}
}
#[derive(Component, Copy, Clone, PartialEq, Default, Debug)]
pub struct OldPosition(DVec3);
impl OldPosition {
pub(crate) fn update(mut query: Query<(&Position, &mut OldPosition), Changed<Position>>) {
for (pos, mut old_pos) in &mut query {
old_pos.0 = pos.0;
}
}
pub fn new(pos: DVec3) -> Self {
Self(pos)
}
pub fn get(&self) -> DVec3 {
self.0
}
pub fn chunk_pos(&self) -> ChunkPos {
ChunkPos::from_dvec3(self.0)
}
}
/// Velocity in m/s.
#[derive(Component, Copy, Clone, PartialEq, Default, Debug)]
pub struct Velocity(pub Vec3);
/// Describes the direction an entity is looking using pitch and yaw angles.
#[derive(Component, Copy, Clone, PartialEq, Default, Debug)]
pub struct Look {
/// The yaw angle in degrees.
pub yaw: f32,
/// The pitch angle in degrees.
pub pitch: f32,
}
impl Look {
/// Gets a normalized direction vector from the yaw and pitch.
pub fn vec(&self) -> Vec3 {
from_yaw_and_pitch(self.yaw, self.pitch)
}
/// Sets the yaw and pitch using a normalized direction vector.
pub fn set_vec(&mut self, vec: Vec3) {
(self.yaw, self.pitch) = to_yaw_and_pitch(vec);
}
}
#[derive(Component, Copy, Clone, PartialEq, Eq, Default, Debug)]
pub struct OnGround(pub bool);
#[derive(Component, Default, Debug)]
pub struct ScratchBuffer(pub Vec<u8>);

View file

@ -8,7 +8,6 @@ use tokio::runtime::Handle;
use tracing::error; use tracing::error;
use uuid::Uuid; use uuid::Uuid;
use valence_protocol::text::Text; use valence_protocol::text::Text;
use valence_protocol::username::Username;
use crate::biome::Biome; use crate::biome::Biome;
use crate::dimension::Dimension; use crate::dimension::Dimension;
@ -307,7 +306,7 @@ pub trait AsyncCallbacks: Send + Sync + 'static {
async fn session_server( async fn session_server(
&self, &self,
shared: &SharedServer, shared: &SharedServer,
username: Username<&str>, username: &str,
auth_digest: &str, auth_digest: &str,
player_ip: &IpAddr, player_ip: &IpAddr,
) -> String { ) -> String {

View file

@ -18,10 +18,11 @@ use valence_protocol::packet::s2c::play::{
use valence_protocol::tracked_data::{Facing, PaintingKind, Pose}; use valence_protocol::tracked_data::{Facing, PaintingKind, Pose};
use valence_protocol::var_int::VarInt; use valence_protocol::var_int::VarInt;
use crate::component::Despawned;
use crate::config::DEFAULT_TPS; use crate::config::DEFAULT_TPS;
use crate::math::Aabb;
use crate::packet::WritePacket; use crate::packet::WritePacket;
use crate::{Despawned, NULL_ENTITY}; use crate::util::Aabb;
use crate::NULL_ENTITY;
pub mod data; pub mod data;
@ -29,28 +30,28 @@ include!(concat!(env!("OUT_DIR"), "/entity_event.rs"));
/// A [`Resource`] which maintains information about all the [`McEntity`] /// A [`Resource`] which maintains information about all the [`McEntity`]
/// components on the server. /// components on the server.
#[derive(Resource)] #[derive(Resource, Debug)]
pub struct McEntityManager { pub struct McEntityManager {
protocol_id_to_entity: FxHashMap<i32, Entity>, protocol_id_to_mcentity: FxHashMap<i32, Entity>,
next_protocol_id: i32, next_protocol_id: i32,
} }
impl McEntityManager { impl McEntityManager {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
Self { Self {
protocol_id_to_entity: HashMap::default(), protocol_id_to_mcentity: HashMap::default(),
next_protocol_id: 1, next_protocol_id: 1,
} }
} }
/// Gets the [`Entity`] of the [`McEntity`] with the given protocol ID. /// Gets the [`Entity`] of the [`McEntity`] with the given protocol ID.
pub fn get_with_protocol_id(&self, id: i32) -> Option<Entity> { pub fn get_with_protocol_id(&self, id: i32) -> Option<Entity> {
self.protocol_id_to_entity.get(&id).cloned() self.protocol_id_to_mcentity.get(&id).cloned()
} }
} }
/// Sets the protocol ID of new entities. /// Sets the protocol ID of new mcentities.
pub(crate) fn init_entities( pub(crate) fn init_mcentities(
mut entities: Query<(Entity, &mut McEntity), Added<McEntity>>, mut entities: Query<(Entity, &mut McEntity), Added<McEntity>>,
mut manager: ResMut<McEntityManager>, mut manager: ResMut<McEntityManager>,
) { ) {
@ -65,31 +66,31 @@ pub(crate) fn init_entities(
manager.next_protocol_id = manager.next_protocol_id.wrapping_add(1); manager.next_protocol_id = manager.next_protocol_id.wrapping_add(1);
manager manager
.protocol_id_to_entity .protocol_id_to_mcentity
.insert(mc_entity.protocol_id, entity); .insert(mc_entity.protocol_id, entity);
} }
} }
/// Removes despawned entities from the entity manager. /// Removes despawned mcentities from the mcentity manager.
pub(crate) fn deinit_despawned_entities( pub(crate) fn deinit_despawned_mcentities(
entities: Query<&mut McEntity, With<Despawned>>, entities: Query<&mut McEntity, With<Despawned>>,
mut manager: ResMut<McEntityManager>, mut manager: ResMut<McEntityManager>,
) { ) {
for entity in &entities { for entity in &entities {
manager.protocol_id_to_entity.remove(&entity.protocol_id); manager.protocol_id_to_mcentity.remove(&entity.protocol_id);
} }
} }
pub(crate) fn update_entities(mut entities: Query<&mut McEntity, Changed<McEntity>>) { pub(crate) fn update_mcentities(mut mcentities: Query<&mut McEntity, Changed<McEntity>>) {
for mut entity in &mut entities { for mut ent in &mut mcentities {
entity.data.clear_modifications(); ent.data.clear_modifications();
entity.old_position = entity.position; ent.old_position = ent.position;
entity.old_instance = entity.instance; ent.old_instance = ent.instance;
entity.statuses = 0; ent.statuses = 0;
entity.animations = 0; ent.animations = 0;
entity.yaw_or_pitch_modified = false; ent.yaw_or_pitch_modified = false;
entity.head_yaw_modified = false; ent.head_yaw_modified = false;
entity.velocity_modified = false; ent.velocity_modified = false;
} }
} }
@ -105,7 +106,7 @@ pub(crate) fn update_entities(mut entities: Query<&mut McEntity, Changed<McEntit
/// not common to every kind of entity, see [`Self::data`]. /// not common to every kind of entity, see [`Self::data`].
#[derive(Component)] #[derive(Component)]
pub struct McEntity { pub struct McEntity {
data: TrackedData, pub(crate) data: TrackedData,
protocol_id: i32, protocol_id: i32,
uuid: Uuid, uuid: Uuid,
/// The range of bytes in the partition cell containing this entity's update /// The range of bytes in the partition cell containing this entity's update

View file

@ -18,12 +18,12 @@ use valence_protocol::text::Text;
use valence_protocol::types::SoundCategory; use valence_protocol::types::SoundCategory;
use valence_protocol::Packet; use valence_protocol::Packet;
use crate::component::Despawned;
use crate::dimension::DimensionId; use crate::dimension::DimensionId;
use crate::entity::McEntity; use crate::entity::McEntity;
use crate::packet::{PacketWriter, WritePacket}; use crate::packet::{PacketWriter, WritePacket};
use crate::server::{Server, SharedServer}; use crate::server::{Server, SharedServer};
use crate::view::ChunkPos; use crate::view::ChunkPos;
use crate::Despawned;
mod chunk; mod chunk;
mod chunk_entry; mod chunk_entry;

View file

@ -19,8 +19,8 @@ use valence_protocol::Encode;
use crate::biome::BiomeId; use crate::biome::BiomeId;
use crate::instance::paletted_container::PalettedContainer; use crate::instance::paletted_container::PalettedContainer;
use crate::instance::InstanceInfo; use crate::instance::InstanceInfo;
use crate::math::bit_width;
use crate::packet::{PacketWriter, WritePacket}; use crate::packet::{PacketWriter, WritePacket};
use crate::util::bit_width;
use crate::view::ChunkPos; use crate::view::ChunkPos;
/// A chunk is a 16x16-meter segment of a world with a variable height. Chunks /// A chunk is a 16x16-meter segment of a world with a variable height. Chunks
@ -333,31 +333,35 @@ impl Chunk<true> {
self.write_init_packets(info, pos, writer, scratch) self.write_init_packets(info, pos, writer, scratch)
} else { } else {
for (sect_y, sect) in &mut self.sections.iter_mut().enumerate() { for (sect_y, sect) in &mut self.sections.iter_mut().enumerate() {
if sect.section_updates.len() == 1 { match sect.section_updates.len() {
let packed = sect.section_updates[0].0 as u64; 0 => {}
let offset_y = packed & 0b1111; 1 => {
let offset_z = (packed >> 4) & 0b1111; let packed = sect.section_updates[0].0 as u64;
let offset_x = (packed >> 8) & 0b1111; let offset_y = packed & 0b1111;
let block = packed >> 12; let offset_z = (packed >> 4) & 0b1111;
let offset_x = (packed >> 8) & 0b1111;
let block = packed >> 12;
let global_x = pos.x * 16 + offset_x as i32; let global_x = pos.x * 16 + offset_x as i32;
let global_y = info.min_y + sect_y as i32 * 16 + offset_y as i32; let global_y = info.min_y + sect_y as i32 * 16 + offset_y as i32;
let global_z = pos.z * 16 + offset_z as i32; let global_z = pos.z * 16 + offset_z as i32;
writer.write_packet(&BlockUpdateS2c { writer.write_packet(&BlockUpdateS2c {
position: BlockPos::new(global_x, global_y, global_z), position: BlockPos::new(global_x, global_y, global_z),
block_id: VarInt(block as i32), block_id: VarInt(block as i32),
}) })
} else if sect.section_updates.len() > 1 { }
let chunk_section_position = (pos.x as i64) << 42 _ => {
| (pos.z as i64 & 0x3fffff) << 20 let chunk_section_position = (pos.x as i64) << 42
| (sect_y as i64 + info.min_y.div_euclid(16) as i64) & 0xfffff; | (pos.z as i64 & 0x3fffff) << 20
| (sect_y as i64 + info.min_y.div_euclid(16) as i64) & 0xfffff;
writer.write_packet(&ChunkDeltaUpdateS2c { writer.write_packet(&ChunkDeltaUpdateS2c {
chunk_section_position, chunk_section_position,
invert_trust_edges: false, invert_trust_edges: false,
blocks: Cow::Borrowed(&sect.section_updates), blocks: Cow::Borrowed(&sect.section_updates),
}); });
}
} }
} }
for idx in &self.modified_block_entities { for idx in &self.modified_block_entities {

View file

@ -5,7 +5,7 @@ use arrayvec::ArrayVec;
use valence_protocol::var_int::VarInt; use valence_protocol::var_int::VarInt;
use valence_protocol::Encode; use valence_protocol::Encode;
use crate::math::bit_width; use crate::util::bit_width;
/// `HALF_LEN` must be equal to `ceil(LEN / 2)`. /// `HALF_LEN` must be equal to `ceil(LEN / 2)`.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]

View file

@ -38,31 +38,15 @@ use valence_protocol::packet::s2c::play::{
CloseScreenS2c, InventoryS2c, OpenScreenS2c, ScreenHandlerSlotUpdateS2c, CloseScreenS2c, InventoryS2c, OpenScreenS2c, ScreenHandlerSlotUpdateS2c,
}; };
use valence_protocol::text::Text; use valence_protocol::text::Text;
use valence_protocol::types::{GameMode, WindowType}; use valence_protocol::types::WindowType;
use valence_protocol::var_int::VarInt; use valence_protocol::var_int::VarInt;
use crate::client::event::{ use crate::client::event::{
ClickSlot, CloseHandledScreen, CreativeInventoryAction, UpdateSelectedSlot, ClickSlot, CloseHandledScreen, CreativeInventoryAction, UpdateSelectedSlot,
}; };
use crate::client::Client; use crate::client::{Client, CursorItem, PlayerInventoryState};
use crate::component::GameMode;
/// The systems needed for updating the inventories. use crate::packet::WritePacket;
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 {
@ -70,7 +54,7 @@ pub struct Inventory {
kind: InventoryKind, kind: InventoryKind,
slots: Box<[Option<ItemStack>]>, slots: Box<[Option<ItemStack>]>,
/// Contains a set bit for each modified slot in `slots`. /// Contains a set bit for each modified slot in `slots`.
modified: u64, changed: u64,
} }
impl Inventory { impl Inventory {
@ -84,7 +68,7 @@ impl Inventory {
title: title.into(), title: title.into(),
kind, kind,
slots: vec![None; kind.slot_count()].into(), slots: vec![None; kind.slot_count()].into(),
modified: 0, changed: 0,
} }
} }
@ -131,13 +115,13 @@ impl Inventory {
idx: u16, idx: u16,
item: impl Into<Option<ItemStack>>, item: impl Into<Option<ItemStack>>,
) -> Option<ItemStack> { ) -> Option<ItemStack> {
assert!(idx < self.slot_count(), "slot index out of range"); assert!(idx < self.slot_count(), "slot index of {idx} out of bounds");
let new = item.into(); let new = item.into();
let old = &mut self.slots[idx as usize]; let old = &mut self.slots[idx as usize];
if new != *old { if new != *old {
self.modified |= 1 << idx; self.changed |= 1 << idx;
} }
std::mem::replace(old, new) std::mem::replace(old, new)
@ -156,16 +140,22 @@ impl Inventory {
/// ``` /// ```
#[track_caller] #[track_caller]
pub fn swap_slot(&mut self, idx_a: u16, idx_b: u16) { pub fn swap_slot(&mut self, idx_a: u16, idx_b: u16) {
assert!(idx_a < self.slot_count(), "slot index out of range"); assert!(
assert!(idx_b < self.slot_count(), "slot index out of range"); idx_a < self.slot_count(),
"slot index of {idx_a} out of bounds"
);
assert!(
idx_b < self.slot_count(),
"slot index of {idx_b} out of bounds"
);
if idx_a == idx_b || self.slots[idx_a as usize] == self.slots[idx_b as usize] { if idx_a == idx_b || self.slots[idx_a as usize] == self.slots[idx_b as usize] {
// Nothing to do here, ignore. // Nothing to do here, ignore.
return; return;
} }
self.modified |= 1 << idx_a; self.changed |= 1 << idx_a;
self.modified |= 1 << idx_b; self.changed |= 1 << idx_b;
self.slots.swap(idx_a as usize, idx_b as usize); self.slots.swap(idx_a as usize, idx_b as usize);
} }
@ -190,7 +180,7 @@ impl Inventory {
return; return;
} }
item.set_count(amount); item.set_count(amount);
self.modified |= 1 << idx; self.changed |= 1 << idx;
} }
} }
@ -289,87 +279,21 @@ impl Inventory {
} }
} }
/// Send updates for each client's player inventory.
fn update_player_inventories(
mut query: Query<(&mut Inventory, &mut Client), Without<OpenInventory>>,
) {
for (mut inventory, mut client) in query.iter_mut() {
if inventory.kind != InventoryKind::Player {
warn!("Inventory on client entity is not a player inventory");
}
if inventory.modified != 0 {
if inventory.modified == u64::MAX {
// Update the whole inventory.
client.inventory_state_id += 1;
let cursor_item = client.cursor_item.clone();
let state_id = client.inventory_state_id.0;
client.write_packet(&InventoryS2c {
window_id: 0,
state_id: VarInt(state_id),
slots: Cow::Borrowed(inventory.slot_slice()),
carried_item: Cow::Borrowed(&cursor_item),
});
client.cursor_item_modified = false;
} else {
// send the modified slots
// The slots that were NOT modified by this client, and they need to be sent
let modified_filtered = inventory.modified & !client.inventory_slots_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: 0,
state_id: VarInt(state_id),
slot_idx: i as i16,
slot_data: Cow::Borrowed(slot),
});
}
}
}
}
inventory.modified = 0;
client.inventory_slots_modified = 0;
}
if client.cursor_item_modified {
client.inventory_state_id += 1;
client.cursor_item_modified = false;
// TODO: eliminate clone?
let cursor_item = client.cursor_item.clone();
let state_id = client.inventory_state_id.0;
client.write_packet(&ScreenHandlerSlotUpdateS2c {
window_id: -1,
state_id: VarInt(state_id),
slot_idx: -1,
slot_data: Cow::Borrowed(&cursor_item),
});
}
}
}
/// Used to indicate that the client with this component is currently viewing /// Used to indicate that the client with this component is currently viewing
/// an inventory. /// an inventory.
#[derive(Debug, Clone, Component)] #[derive(Component, Clone, Debug)]
pub struct OpenInventory { pub struct OpenInventory {
/// The Entity with the `Inventory` component that the client is currently /// The Entity with the `Inventory` component that the client is currently
/// viewing. /// viewing.
pub(crate) entity: Entity, pub(crate) entity: Entity,
client_modified: u64, client_changed: u64,
} }
impl OpenInventory { impl OpenInventory {
pub fn new(entity: Entity) -> Self { pub fn new(entity: Entity) -> Self {
OpenInventory { OpenInventory {
entity, entity,
client_modified: 0, client_changed: 0,
} }
} }
@ -378,75 +302,174 @@ impl OpenInventory {
} }
} }
/// The systems needed for updating the inventories.
pub(crate) fn update_inventories() -> SystemConfigs {
(
handle_set_held_item,
handle_click_container
.before(update_open_inventories)
.before(update_player_inventories),
handle_set_slot_creative
.before(update_open_inventories)
.before(update_player_inventories),
update_open_inventories,
handle_close_container,
update_client_on_close_inventory.after(update_open_inventories),
update_player_inventories,
)
.into_configs()
}
/// Send updates for each client's player inventory.
fn update_player_inventories(
mut query: Query<
(
&mut Inventory,
&mut Client,
&mut PlayerInventoryState,
Ref<CursorItem>,
),
Without<OpenInventory>,
>,
) {
for (mut inventory, mut client, mut inv_state, cursor_item) in &mut query {
if inventory.kind != InventoryKind::Player {
warn!("Inventory on client entity is not a player inventory");
}
if inventory.changed == u64::MAX {
// Update the whole inventory.
inv_state.state_id += 1;
client.write_packet(&InventoryS2c {
window_id: 0,
state_id: VarInt(inv_state.state_id.0),
slots: Cow::Borrowed(inventory.slot_slice()),
carried_item: Cow::Borrowed(&cursor_item.0),
});
inventory.changed = 0;
inv_state.slots_changed = 0;
// Skip updating the cursor item because we just updated the whole inventory.
continue;
} else if inventory.changed != 0 {
// Send the modified slots.
// The slots that were NOT modified by this client, and they need to be sent
let changed_filtered = inventory.changed & !inv_state.slots_changed;
if changed_filtered != 0 {
inv_state.state_id += 1;
for (i, slot) in inventory.slots.iter().enumerate() {
if ((changed_filtered >> i) & 1) == 1 {
client.write_packet(&ScreenHandlerSlotUpdateS2c {
window_id: 0,
state_id: VarInt(inv_state.state_id.0),
slot_idx: i as i16,
slot_data: Cow::Borrowed(slot),
});
}
}
}
inventory.changed = 0;
inv_state.slots_changed = 0;
}
if cursor_item.is_changed() && !inv_state.client_updated_cursor_item {
inv_state.state_id += 1;
client.write_packet(&ScreenHandlerSlotUpdateS2c {
window_id: -1,
state_id: VarInt(inv_state.state_id.0),
slot_idx: -1,
slot_data: Cow::Borrowed(&cursor_item.0),
});
}
inv_state.client_updated_cursor_item = false;
}
}
/// Handles the `OpenInventory` component being added to a client, which /// Handles the `OpenInventory` component being added to a client, which
/// indicates that the client is now viewing an inventory, and sends inventory /// indicates that the client is now viewing an inventory, and sends inventory
/// updates to the client when the inventory is modified. /// updates to the client when the inventory is modified.
fn update_open_inventories( fn update_open_inventories(
mut commands: Commands, mut clients: Query<(
mut clients: Query<(Entity, &mut Client, &mut OpenInventory)>, Entity,
&mut Client,
&mut PlayerInventoryState,
&CursorItem,
&mut OpenInventory,
)>,
mut inventories: Query<&mut Inventory>, mut inventories: Query<&mut Inventory>,
mut commands: Commands,
) { ) {
// These operations need to happen in this order. // These operations need to happen in this order.
// send the inventory contents to all clients that are viewing an inventory // Send the inventory contents to all clients that are viewing an inventory.
for (client_entity, mut client, mut open_inventory) in clients.iter_mut() { for (client_entity, mut client, mut inv_state, cursor_item, mut open_inventory) in &mut clients
// validate that the inventory exists {
let Ok(inventory) = inventories.get_component::<Inventory>(open_inventory.entity) else { // Validate that the inventory exists.
// the inventory no longer exists, so close the inventory let Ok(mut inventory) = inventories.get_mut(open_inventory.entity) else {
// The inventory no longer exists, so close the inventory.
commands.entity(client_entity).remove::<OpenInventory>(); commands.entity(client_entity).remove::<OpenInventory>();
let window_id = client.window_id;
client.write_packet(&CloseScreenS2c { client.write_packet(&CloseScreenS2c {
window_id, window_id: inv_state.window_id,
}); });
continue; continue;
}; };
if open_inventory.is_added() { if open_inventory.is_added() {
// send the inventory to the client if the client just opened the inventory // Send the inventory to the client if the client just opened the inventory.
client.window_id = client.window_id % 100 + 1; inv_state.window_id = inv_state.window_id % 100 + 1;
open_inventory.client_modified = 0; open_inventory.client_changed = 0;
let packet = OpenScreenS2c { client.write_packet(&OpenScreenS2c {
window_id: VarInt(client.window_id.into()), window_id: VarInt(inv_state.window_id.into()),
window_type: WindowType::from(inventory.kind), window_type: WindowType::from(inventory.kind),
window_title: (&inventory.title).into(), window_title: Cow::Borrowed(&inventory.title),
}; });
client.write_packet(&packet);
let packet = InventoryS2c { client.write_packet(&InventoryS2c {
window_id: client.window_id, window_id: inv_state.window_id,
state_id: VarInt(client.inventory_state_id.0), state_id: VarInt(inv_state.state_id.0),
slots: Cow::Borrowed(inventory.slot_slice()), slots: Cow::Borrowed(inventory.slot_slice()),
// TODO: eliminate clone? carried_item: Cow::Borrowed(&cursor_item.0),
carried_item: Cow::Owned(client.cursor_item.clone()), });
};
client.write_packet(&packet);
} else { } else {
// the client is already viewing the inventory // The client is already viewing the inventory.
if inventory.modified == u64::MAX {
// send the entire inventory if inventory.changed == u64::MAX {
client.inventory_state_id += 1; // Send the entire inventory.
let packet = InventoryS2c {
window_id: client.window_id, inv_state.state_id += 1;
state_id: VarInt(client.inventory_state_id.0),
client.write_packet(&InventoryS2c {
window_id: inv_state.window_id,
state_id: VarInt(inv_state.state_id.0),
slots: Cow::Borrowed(inventory.slot_slice()), slots: Cow::Borrowed(inventory.slot_slice()),
// TODO: eliminate clone? carried_item: Cow::Borrowed(&cursor_item.0),
carried_item: Cow::Owned(client.cursor_item.clone()), })
};
client.write_packet(&packet);
} else { } else {
// send the modified slots // Send the changed slots.
let window_id = client.window_id as i8;
// The slots that were NOT modified by this client, and they need to be sent // The slots that were NOT changed by this client, and they need to be sent
let modified_filtered = inventory.modified & !open_inventory.client_modified; let changed_filtered = inventory.changed & !open_inventory.client_changed;
if modified_filtered != 0 {
client.inventory_state_id += 1; if changed_filtered != 0 {
let state_id = client.inventory_state_id.0; inv_state.state_id += 1;
for (i, slot) in inventory.slots.iter().enumerate() { for (i, slot) in inventory.slots.iter().enumerate() {
if (modified_filtered >> i) & 1 == 1 { if (changed_filtered >> i) & 1 == 1 {
client.write_packet(&ScreenHandlerSlotUpdateS2c { client.write_packet(&ScreenHandlerSlotUpdateS2c {
window_id, window_id: inv_state.window_id as i8,
state_id: VarInt(state_id), state_id: VarInt(inv_state.state_id.0),
slot_idx: i as i16, slot_idx: i as i16,
slot_data: Cow::Borrowed(slot), slot_data: Cow::Borrowed(slot),
}); });
@ -456,24 +479,19 @@ fn update_open_inventories(
} }
} }
open_inventory.client_modified = 0; open_inventory.client_changed = 0;
client.inventory_slots_modified = 0; inv_state.slots_changed = 0;
} inv_state.client_updated_cursor_item = false;
inventory.changed = 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. /// Handles clients telling the server that they are closing an inventory.
fn handle_close_container(mut commands: Commands, mut events: EventReader<CloseHandledScreen>) { fn handle_close_container(mut events: EventReader<CloseHandledScreen>, mut commands: Commands) {
for event in events.iter() { for event in events.iter() {
commands.entity(event.client).remove::<OpenInventory>(); if let Some(mut entity) = commands.get_entity(event.client) {
entity.remove::<OpenInventory>();
}
} }
} }
@ -481,29 +499,39 @@ fn handle_close_container(mut commands: Commands, mut events: EventReader<CloseH
/// indicates that the client is no longer viewing an inventory. /// indicates that the client is no longer viewing an inventory.
fn update_client_on_close_inventory( fn update_client_on_close_inventory(
mut removals: RemovedComponents<OpenInventory>, mut removals: RemovedComponents<OpenInventory>,
mut clients: Query<&mut Client>, mut clients: Query<(&mut Client, &PlayerInventoryState)>,
) { ) {
for entity in &mut removals { for entity in &mut removals {
if let Ok(mut client) = clients.get_component_mut::<Client>(entity) { if let Ok((mut client, inv_state)) = clients.get_mut(entity) {
let window_id = client.window_id; client.write_packet(&CloseScreenS2c {
client.write_packet(&CloseScreenS2c { window_id }); window_id: inv_state.window_id,
})
} }
} }
} }
// TODO: Do this logic in c2s packet handler?
fn handle_click_container( fn handle_click_container(
mut clients: Query<(&mut Client, &mut Inventory, Option<&mut OpenInventory>)>, mut clients: Query<(
&mut Client,
&mut Inventory,
&mut PlayerInventoryState,
Option<&mut OpenInventory>,
&mut CursorItem,
)>,
// TODO: this query matches disconnected clients. Define client marker component to avoid
// problem?
mut inventories: Query<&mut Inventory, Without<Client>>, mut inventories: Query<&mut Inventory, Without<Client>>,
mut events: EventReader<ClickSlot>, mut events: EventReader<ClickSlot>,
) { ) {
for event in events.iter() { for event in events.iter() {
let Ok((mut client, mut client_inventory, mut open_inventory)) = let Ok((mut client, mut client_inventory, mut inv_state, open_inventory, mut cursor_item)) =
clients.get_mut(event.client) else { clients.get_mut(event.client) else {
// the client does not exist, ignore // The client does not exist, ignore.
continue; continue;
}; };
// validate the window id // Validate the window id.
if (event.window_id == 0) != open_inventory.is_none() { if (event.window_id == 0) != open_inventory.is_none() {
warn!( warn!(
"Client sent a click with an invalid window id for current state: window_id = {}, \ "Client sent a click with an invalid window id for current state: window_id = {}, \
@ -514,68 +542,77 @@ fn handle_click_container(
continue; continue;
} }
if let Some(open_inventory) = open_inventory.as_mut() { if let Some(mut open_inventory) = open_inventory {
// the player is interacting with an inventory that is open // 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 let Ok(mut target_inventory) = inventories.get_mut(open_inventory.entity) else {
// The inventory does not exist, ignore.
continue; continue;
}; };
if client.inventory_state_id.0 != event.state_id {
// client is out of sync, resync, ignore click if inv_state.state_id.0 != event.state_id {
// Client is out of sync. Resync and ignore click.
debug!("Client state id mismatch, resyncing"); debug!("Client state id mismatch, resyncing");
client.inventory_state_id += 1;
let packet = InventoryS2c { inv_state.state_id += 1;
window_id: client.window_id,
state_id: VarInt(client.inventory_state_id.0), client.write_packet(&InventoryS2c {
window_id: inv_state.window_id,
state_id: VarInt(inv_state.state_id.0),
slots: Cow::Borrowed(target_inventory.slot_slice()), slots: Cow::Borrowed(target_inventory.slot_slice()),
// TODO: eliminate clone? carried_item: Cow::Borrowed(&cursor_item.0),
carried_item: Cow::Owned(client.cursor_item.clone()), });
};
client.write_packet(&packet);
continue; continue;
} }
client.cursor_item = event.carried_item.clone(); cursor_item.0 = event.carried_item.clone();
for slot in event.slot_changes.clone() { for slot in event.slot_changes.clone() {
if (0i16..target_inventory.slot_count() as i16).contains(&slot.idx) { if (0i16..target_inventory.slot_count() as i16).contains(&slot.idx) {
// the client is interacting with a slot in the target inventory // The client is interacting with a slot in the target inventory.
target_inventory.set_slot(slot.idx as u16, slot.item); target_inventory.set_slot(slot.idx as u16, slot.item);
open_inventory.client_modified |= 1 << slot.idx; open_inventory.client_changed |= 1 << slot.idx;
} else { } else {
// the client is interacting with a slot in their own inventory // 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); let slot_id = convert_to_player_slot_id(target_inventory.kind, slot.idx as u16);
client_inventory.set_slot(slot_id, slot.item); client_inventory.set_slot(slot_id, slot.item);
client.inventory_slots_modified |= 1 << slot_id; inv_state.slots_changed |= 1 << slot_id;
} }
} }
} else { } else {
// the client is interacting with their own inventory // The client is interacting with their own inventory.
if inv_state.state_id.0 != event.state_id {
// Client is out of sync. Resync and ignore the click.
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"); debug!("Client state id mismatch, resyncing");
client.inventory_state_id += 1;
let packet = InventoryS2c { inv_state.state_id += 1;
window_id: client.window_id,
state_id: VarInt(client.inventory_state_id.0), client.write_packet(&InventoryS2c {
window_id: inv_state.window_id,
state_id: VarInt(inv_state.state_id.0),
slots: Cow::Borrowed(client_inventory.slot_slice()), slots: Cow::Borrowed(client_inventory.slot_slice()),
// TODO: eliminate clone? carried_item: Cow::Borrowed(&cursor_item.0),
carried_item: Cow::Owned(client.cursor_item.clone()), });
};
client.write_packet(&packet);
continue; continue;
} }
// TODO: do more validation on the click // TODO: do more validation on the click
client.cursor_item = event.carried_item.clone();
cursor_item.set_if_neq(CursorItem(event.carried_item.clone()));
inv_state.client_updated_cursor_item = true;
for slot in event.slot_changes.clone() { for slot in event.slot_changes.clone() {
if (0i16..client_inventory.slot_count() as i16).contains(&slot.idx) { if (0i16..client_inventory.slot_count() as i16).contains(&slot.idx) {
client_inventory.set_slot(slot.idx as u16, slot.item); client_inventory.set_slot(slot.idx as u16, slot.item);
client.inventory_slots_modified |= 1 << slot.idx; inv_state.slots_changed |= 1 << slot.idx;
} else { } else {
// the client is trying to interact with a slot that does not exist, // The client is trying to interact with a slot that does not exist,
// ignore // ignore.
warn!( warn!(
"Client attempted to interact with slot {} which does not exist", "Client attempted to interact with slot {} which does not exist",
slot.idx slot.idx
@ -587,30 +624,40 @@ fn handle_click_container(
} }
fn handle_set_slot_creative( fn handle_set_slot_creative(
mut clients: Query<(&mut Client, &mut Inventory)>, mut clients: Query<(
&mut Client,
&mut Inventory,
&mut PlayerInventoryState,
&GameMode,
)>,
mut events: EventReader<CreativeInventoryAction>, mut events: EventReader<CreativeInventoryAction>,
) { ) {
for event in events.iter() { for event in events.iter() {
if let Ok((mut client, mut inventory)) = clients.get_mut(event.client) { if let Ok((mut client, mut inventory, mut inv_state, game_mode)) =
if client.game_mode() != GameMode::Creative { clients.get_mut(event.client)
// the client is not in creative mode, ignore {
if *game_mode != GameMode::Creative {
// The client is not in creative mode, ignore.
continue; continue;
} }
if event.slot < 0 || event.slot >= inventory.slot_count() as i16 { 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 // The client is trying to interact with a slot that does not exist, ignore.
continue; continue;
} }
inventory.set_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 // Set the slot without marking it as changed.
client.inventory_state_id += 1; inventory.slots[event.slot as usize] = event.clicked_item.clone();
let state_id = client.inventory_state_id.0;
inv_state.state_id += 1;
// HACK: notchian clients rely on the server to send the slot update when in // 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 // creative mode. Simply marking the slot as changed is not enough. This was
// discovered because shift-clicking the destroy item slot in creative mode does // discovered because shift-clicking the destroy item slot in creative mode does
// not work without this hack. // not work without this hack.
client.write_packet(&ScreenHandlerSlotUpdateS2c { client.write_packet(&ScreenHandlerSlotUpdateS2c {
window_id: 0, window_id: 0,
state_id: VarInt(state_id), state_id: VarInt(inv_state.state_id.0),
slot_idx: event.slot, slot_idx: event.slot,
slot_data: Cow::Borrowed(&event.clicked_item), slot_data: Cow::Borrowed(&event.clicked_item),
}); });
@ -619,12 +666,12 @@ fn handle_set_slot_creative(
} }
fn handle_set_held_item( fn handle_set_held_item(
mut clients: Query<&mut Client>, mut clients: Query<&mut PlayerInventoryState>,
mut events: EventReader<UpdateSelectedSlot>, mut events: EventReader<UpdateSelectedSlot>,
) { ) {
for event in events.iter() { for event in events.iter() {
if let Ok(mut client) = clients.get_mut(event.client) { if let Ok(mut inv_state) = clients.get_mut(event.client) {
client.held_item_slot = convert_hotbar_slot_id(event.slot as u16); inv_state.held_item_slot = convert_hotbar_slot_id(event.slot as u16);
} }
} }
} }
@ -919,9 +966,9 @@ mod test {
// Make the client click the slot and pick up the item. // Make the client click the slot and pick up the item.
let state_id = app let state_id = app
.world .world
.get::<Client>(client_ent) .get::<PlayerInventoryState>(client_ent)
.unwrap() .unwrap()
.inventory_state_id; .state_id;
client_helper.send(&valence_protocol::packet::c2s::play::ClickSlotC2s { client_helper.send(&valence_protocol::packet::c2s::play::ClickSlotC2s {
window_id: 0, window_id: 0,
button: 0, button: 0,
@ -940,7 +987,7 @@ mod test {
// Make assertions // Make assertions
let sent_packets = client_helper.collect_sent()?; let sent_packets = client_helper.collect_sent()?;
// because the inventory was modified as a result of the client's click, the // because the inventory was changed as a result of the client's click, the
// server should not send any packets to the client because the client // server should not send any packets to the client because the client
// already knows about the change. // already knows about the change.
assert_packet_count!( assert_packet_count!(
@ -953,12 +1000,12 @@ mod test {
.get::<Inventory>(client_ent) .get::<Inventory>(client_ent)
.expect("could not find inventory for client"); .expect("could not find inventory for client");
assert_eq!(inventory.slot(20), None); assert_eq!(inventory.slot(20), None);
let client = app let cursor_item = app
.world .world
.get::<Client>(client_ent) .get::<CursorItem>(client_ent)
.expect("could not find client"); .expect("could not find client");
assert_eq!( assert_eq!(
client.cursor_item, cursor_item.0,
Some(ItemStack::new(ItemKind::Diamond, 2, None)) Some(ItemStack::new(ItemKind::Diamond, 2, None))
); );
@ -1014,7 +1061,7 @@ mod test {
.world .world
.get_mut::<Inventory>(client_ent) .get_mut::<Inventory>(client_ent)
.expect("could not find inventory for client"); .expect("could not find inventory for client");
inventory.modified = u64::MAX; inventory.changed = u64::MAX;
app.update(); app.update();
@ -1050,12 +1097,9 @@ mod test {
client_helper.clear_sent(); client_helper.clear_sent();
// Make the client click the slot and pick up the item. // Make the client click the slot and pick up the item.
let state_id = app let inv_state = app.world.get::<PlayerInventoryState>(client_ent).unwrap();
.world let state_id = inv_state.state_id;
.get::<Client>(client_ent) let window_id = inv_state.window_id;
.unwrap()
.inventory_state_id;
let window_id = app.world.get::<Client>(client_ent).unwrap().window_id;
client_helper.send(&valence_protocol::packet::c2s::play::ClickSlotC2s { client_helper.send(&valence_protocol::packet::c2s::play::ClickSlotC2s {
window_id, window_id,
button: 0, button: 0,
@ -1087,12 +1131,12 @@ mod test {
.get::<Inventory>(inventory_ent) .get::<Inventory>(inventory_ent)
.expect("could not find inventory"); .expect("could not find inventory");
assert_eq!(inventory.slot(20), None); assert_eq!(inventory.slot(20), None);
let client = app let cursor_item = app
.world .world
.get::<Client>(client_ent) .get::<CursorItem>(client_ent)
.expect("could not find client"); .expect("could not find client");
assert_eq!( assert_eq!(
client.cursor_item, cursor_item.0,
Some(ItemStack::new(ItemKind::Diamond, 2, None)) Some(ItemStack::new(ItemKind::Diamond, 2, None))
); );
@ -1154,7 +1198,7 @@ mod test {
.world .world
.get_mut::<Inventory>(inventory_ent) .get_mut::<Inventory>(inventory_ent)
.expect("could not find inventory"); .expect("could not find inventory");
inventory.modified = u64::MAX; inventory.changed = u64::MAX;
app.update(); app.update();
@ -1169,11 +1213,11 @@ mod test {
fn test_set_creative_mode_slot_handling() { fn test_set_creative_mode_slot_handling() {
let mut app = App::new(); let mut app = App::new();
let (client_ent, mut client_helper) = scenario_single_client(&mut app); let (client_ent, mut client_helper) = scenario_single_client(&mut app);
let mut client = app let mut game_mode = app
.world .world
.get_mut::<Client>(client_ent) .get_mut::<GameMode>(client_ent)
.expect("could not find client"); .expect("could not find client");
client.set_game_mode(GameMode::Creative); *game_mode.as_mut() = GameMode::Creative;
// Process a tick to get past the "on join" logic. // Process a tick to get past the "on join" logic.
app.update(); app.update();
@ -1203,11 +1247,11 @@ mod test {
fn test_ignore_set_creative_mode_slot_if_not_creative() { fn test_ignore_set_creative_mode_slot_if_not_creative() {
let mut app = App::new(); let mut app = App::new();
let (client_ent, mut client_helper) = scenario_single_client(&mut app); let (client_ent, mut client_helper) = scenario_single_client(&mut app);
let mut client = app let mut game_mode = app
.world .world
.get_mut::<Client>(client_ent) .get_mut::<GameMode>(client_ent)
.expect("could not find client"); .expect("could not find client");
client.set_game_mode(GameMode::Survival); *game_mode.as_mut() = GameMode::Survival;
// Process a tick to get past the "on join" logic. // Process a tick to get past the "on join" logic.
app.update(); app.update();
@ -1259,11 +1303,11 @@ mod test {
} }
// Make assertions // Make assertions
let client = app let inv_state = app
.world .world
.get::<Client>(client_ent) .get::<PlayerInventoryState>(client_ent)
.expect("could not find client"); .expect("could not find client");
assert_eq!(client.window_id, 3); assert_eq!(inv_state.window_id, 3);
} }
#[test] #[test]
@ -1280,11 +1324,11 @@ mod test {
app.update(); app.update();
// Make assertions // Make assertions
let client = app let inv_state = app
.world .world
.get::<Client>(client_ent) .get::<PlayerInventoryState>(client_ent)
.expect("could not find client"); .expect("could not find client");
assert_eq!(client.held_item_slot, 40); assert_eq!(inv_state.held_item_slot, 40);
Ok(()) Ok(())
} }
@ -1377,11 +1421,11 @@ mod test {
app.update(); app.update();
// Make assertions // Make assertions
let client = app let inv_state = app
.world .world
.get::<Client>(client_ent) .get::<PlayerInventoryState>(client_ent)
.expect("could not find client"); .expect("could not find client");
assert_eq!(client.held_item_slot(), 36); assert_eq!(inv_state.held_item_slot, 36);
let inventory = app let inventory = app
.world .world
.get::<Inventory>(client_ent) .get::<Inventory>(client_ent)
@ -1442,12 +1486,16 @@ mod test {
fn should_drop_item_stack_click_container_outside() -> anyhow::Result<()> { fn should_drop_item_stack_click_container_outside() -> anyhow::Result<()> {
let mut app = App::new(); let mut app = App::new();
let (client_ent, mut client_helper) = scenario_single_client(&mut app); let (client_ent, mut client_helper) = scenario_single_client(&mut app);
let mut client = app let mut cursor_item = app
.world .world
.get_mut::<Client>(client_ent) .get_mut::<CursorItem>(client_ent)
.expect("could not find client"); .expect("could not find client");
client.cursor_item = Some(ItemStack::new(ItemKind::IronIngot, 32, None)); cursor_item.0 = Some(ItemStack::new(ItemKind::IronIngot, 32, None));
let state_id = client.inventory_state_id.0; let inv_state = app
.world
.get_mut::<PlayerInventoryState>(client_ent)
.expect("could not find client");
let state_id = inv_state.state_id.0;
// Process a tick to get past the "on join" logic. // Process a tick to get past the "on join" logic.
app.update(); app.update();
@ -1466,11 +1514,11 @@ mod test {
app.update(); app.update();
// Make assertions // Make assertions
let client = app let cursor_item = app
.world .world
.get::<Client>(client_ent) .get::<CursorItem>(client_ent)
.expect("could not find client"); .expect("could not find client");
assert_eq!(client.cursor_item(), None); assert_eq!(cursor_item.0, None);
let events = app let events = app
.world .world
.get_resource::<Events<DropItemStack>>() .get_resource::<Events<DropItemStack>>()
@ -1491,11 +1539,11 @@ mod test {
fn should_drop_item_click_container_with_dropkey_single() -> anyhow::Result<()> { fn should_drop_item_click_container_with_dropkey_single() -> anyhow::Result<()> {
let mut app = App::new(); let mut app = App::new();
let (client_ent, mut client_helper) = scenario_single_client(&mut app); let (client_ent, mut client_helper) = scenario_single_client(&mut app);
let client = app let inv_state = app
.world .world
.get_mut::<Client>(client_ent) .get_mut::<PlayerInventoryState>(client_ent)
.expect("could not find client"); .expect("could not find client");
let state_id = client.inventory_state_id.0; let state_id = inv_state.state_id.0;
let mut inventory = app let mut inventory = app
.world .world
.get_mut::<Inventory>(client_ent) .get_mut::<Inventory>(client_ent)
@ -1539,11 +1587,11 @@ mod test {
fn should_drop_item_stack_click_container_with_dropkey() -> anyhow::Result<()> { fn should_drop_item_stack_click_container_with_dropkey() -> anyhow::Result<()> {
let mut app = App::new(); let mut app = App::new();
let (client_ent, mut client_helper) = scenario_single_client(&mut app); let (client_ent, mut client_helper) = scenario_single_client(&mut app);
let client = app let inv_state = app
.world .world
.get_mut::<Client>(client_ent) .get_mut::<PlayerInventoryState>(client_ent)
.expect("could not find client"); .expect("could not find client");
let state_id = client.inventory_state_id.0; let state_id = inv_state.state_id.0;
let mut inventory = app let mut inventory = app
.world .world
.get_mut::<Inventory>(client_ent) .get_mut::<Inventory>(client_ent)

View file

@ -20,11 +20,7 @@
unused_import_braces, unused_import_braces,
clippy::dbg_macro clippy::dbg_macro
)] )]
#![allow( #![allow(clippy::type_complexity)] // ECS queries are often complicated.
clippy::derive_partial_eq_without_eq,
clippy::unusual_byte_groupings,
clippy::comparison_chain
)]
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
pub use { pub use {
@ -33,18 +29,19 @@ pub use {
pub mod biome; pub mod biome;
pub mod client; pub mod client;
pub mod component;
pub mod config; pub mod config;
pub mod dimension; pub mod dimension;
pub mod entity; pub mod entity;
pub mod instance; pub mod instance;
pub mod inventory; pub mod inventory;
pub mod math; pub mod packet;
mod packet;
pub mod player_list; pub mod player_list;
pub mod player_textures; pub mod player_textures;
pub mod server; pub mod server;
#[cfg(any(test, doctest))] #[cfg(any(test, doctest))]
mod unit_test; mod unit_test;
pub mod util;
pub mod view; pub mod view;
pub mod prelude { pub mod prelude {
@ -52,7 +49,8 @@ pub mod prelude {
pub use bevy_app::prelude::*; 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::*;
pub use component::*;
pub use config::{ pub use config::{
AsyncCallbacks, ConnectionMode, PlayerSampleEntry, ServerListPing, ServerPlugin, AsyncCallbacks, ConnectionMode, PlayerSampleEntry, ServerListPing, ServerPlugin,
}; };
@ -68,8 +66,6 @@ pub mod prelude {
pub use protocol::ident::Ident; pub use protocol::ident::Ident;
pub use protocol::item::{ItemKind, ItemStack}; pub use protocol::item::{ItemKind, ItemStack};
pub use protocol::text::{Color, Text, TextFormat}; pub use protocol::text::{Color, Text, TextFormat};
pub use protocol::types::GameMode;
pub use protocol::username::Username;
pub use server::{EventLoopSchedule, EventLoopSet, 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;
@ -82,21 +78,6 @@ pub mod prelude {
use super::*; use super::*;
} }
/// A [`Component`] for marking entities that should be despawned at the end of
/// the tick.
///
/// In Valence, some built-in components such as [`McEntity`] are not allowed to
/// be removed from the [`World`] directly. Instead, you must give the entities
/// you wish to despawn the `Despawned` component. At the end of the tick,
/// Valence will despawn all entities with this component for you.
///
/// It is legal to remove components or delete entities that Valence does not
/// know about at any time.
///
/// [`McEntity`]: entity::McEntity
#[derive(Copy, Clone, Component)]
pub struct Despawned;
/// Let's pretend that [`NULL_ENTITY`] was created by spawning an entity, /// Let's pretend that [`NULL_ENTITY`] was created by spawning an entity,
/// immediately despawning it, and then stealing its [`Entity`] ID. The user /// immediately despawning it, and then stealing its [`Entity`] ID. The user
/// doesn't need to know about this. /// doesn't need to know about this.

View file

@ -4,19 +4,18 @@ use tracing::warn;
use valence_protocol::codec::{encode_packet, encode_packet_compressed, PacketEncoder}; use valence_protocol::codec::{encode_packet, encode_packet_compressed, PacketEncoder};
use valence_protocol::Packet; use valence_protocol::Packet;
/// Types that can have packets written to them.
pub(crate) trait WritePacket { pub(crate) trait WritePacket {
fn write_packet<'a, P>(&mut self, packet: &P) /// Writes a packet to this object. Encoding errors are typically logged and
where /// discarded.
P: Packet<'a>; fn write_packet<'a>(&mut self, packet: &impl Packet<'a>);
/// Copies raw packet data directly into this object. Don't use this unless
/// you know what you're doing.
fn write_packet_bytes(&mut self, bytes: &[u8]); fn write_packet_bytes(&mut self, bytes: &[u8]);
} }
impl<W: WritePacket> WritePacket for &mut W { impl<W: WritePacket> WritePacket for &mut W {
fn write_packet<'a, P>(&mut self, packet: &P) fn write_packet<'a>(&mut self, packet: &impl Packet<'a>) {
where
P: Packet<'a>,
{
(*self).write_packet(packet) (*self).write_packet(packet)
} }
@ -25,6 +24,7 @@ impl<W: WritePacket> WritePacket for &mut W {
} }
} }
/// An implementor of [`WritePacket`] backed by a `Vec` reference.
pub(crate) struct PacketWriter<'a> { pub(crate) struct PacketWriter<'a> {
buf: &'a mut Vec<u8>, buf: &'a mut Vec<u8>,
threshold: Option<u32>, threshold: Option<u32>,
@ -42,10 +42,7 @@ impl<'a> PacketWriter<'a> {
} }
impl WritePacket for PacketWriter<'_> { impl WritePacket for PacketWriter<'_> {
fn write_packet<'a, P>(&mut self, pkt: &P) fn write_packet<'a>(&mut self, pkt: &impl Packet<'a>) {
where
P: Packet<'a>,
{
let res = if let Some(threshold) = self.threshold { let res = if let Some(threshold) = self.threshold {
encode_packet_compressed(self.buf, pkt, threshold, self.scratch) encode_packet_compressed(self.buf, pkt, threshold, self.scratch)
} else { } else {
@ -65,10 +62,7 @@ impl WritePacket for PacketWriter<'_> {
} }
impl WritePacket for PacketEncoder { impl WritePacket for PacketEncoder {
fn write_packet<'a, P>(&mut self, packet: &P) fn write_packet<'a>(&mut self, packet: &impl Packet<'a>) {
where
P: Packet<'a>,
{
if let Err(e) = self.append_packet(packet) { if let Err(e) = self.append_packet(packet) {
warn!("failed to write packet: {e:#}"); warn!("failed to write packet: {e:#}");
} }

View file

@ -13,9 +13,10 @@ use valence_protocol::packet::s2c::play::player_list::{
}; };
use valence_protocol::packet::s2c::play::{PlayerListHeaderS2c, PlayerRemoveS2c}; use valence_protocol::packet::s2c::play::{PlayerListHeaderS2c, PlayerRemoveS2c};
use valence_protocol::text::Text; use valence_protocol::text::Text;
use valence_protocol::types::{GameMode, Property}; use valence_protocol::types::Property;
use crate::client::Client; use crate::client::Client;
use crate::component::{GameMode, Ping, Properties, UniqueId, Username};
use crate::packet::{PacketWriter, WritePacket}; use crate::packet::{PacketWriter, WritePacket};
use crate::server::Server; use crate::server::Server;
@ -55,27 +56,28 @@ impl PlayerList {
/// When clients disconnect, they are removed from the player list. /// When clients disconnect, they are removed from the player list.
pub fn default_systems() -> SystemConfigs { 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<(&Username, &Properties, &GameMode, &Ping, &UniqueId), Added<Client>>,
mut player_list: ResMut<PlayerList>, mut player_list: ResMut<PlayerList>,
) { ) {
for client in &clients { for (username, properties, game_mode, ping, uuid) in &clients {
let entry = PlayerListEntry::new() let entry = PlayerListEntry::new()
.with_username(client.username()) .with_username(&username.0)
.with_properties(client.properties()) .with_properties(properties.0.clone())
.with_game_mode(client.game_mode()) .with_game_mode(*game_mode)
.with_ping(client.ping()); .with_ping(ping.0);
player_list.insert(client.uuid(), entry); player_list.insert(uuid.0, entry);
} }
} }
fn remove_disconnected_clients_from_player_list( fn remove_disconnected_clients_from_player_list(
clients: Query<&mut Client>, mut clients: RemovedComponents<Client>,
uuids: Query<&UniqueId>,
mut player_list: ResMut<PlayerList>, mut player_list: ResMut<PlayerList>,
) { ) {
for client in &clients { for entity in clients.iter() {
if client.is_disconnected() { if let Ok(UniqueId(uuid)) = uuids.get(entity) {
player_list.remove(client.uuid()); player_list.remove(*uuid);
} }
} }
} }
@ -238,7 +240,7 @@ impl PlayerList {
chat_data: None, chat_data: None,
listed: entry.listed, listed: entry.listed,
ping: entry.ping, ping: entry.ping,
game_mode: entry.game_mode, game_mode: entry.game_mode.into(),
display_name: entry.display_name.as_ref().map(|t| t.into()), display_name: entry.display_name.as_ref().map(|t| t.into()),
}) })
}) })
@ -271,7 +273,7 @@ impl PlayerList {
/// ``` /// ```
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct PlayerListEntry { pub struct PlayerListEntry {
username: String, // TODO: Username<String>? username: String,
properties: Vec<Property>, properties: Vec<Property>,
game_mode: GameMode, game_mode: GameMode,
old_game_mode: GameMode, old_game_mode: GameMode,
@ -609,7 +611,7 @@ pub(crate) fn update_player_list(
chat_data: None, chat_data: None,
listed: entry.listed, listed: entry.listed,
ping: entry.ping, ping: entry.ping,
game_mode: entry.game_mode, game_mode: entry.game_mode.into(),
display_name: entry.display_name.as_ref().map(|t| t.into()), display_name: entry.display_name.as_ref().map(|t| t.into()),
}; };
@ -650,7 +652,7 @@ pub(crate) fn update_player_list(
chat_data: None, chat_data: None,
listed: entry.listed, listed: entry.listed,
ping: entry.ping, ping: entry.ping,
game_mode: entry.game_mode, game_mode: entry.game_mode.into(),
display_name: entry.display_name.as_ref().map(|t| t.into()), display_name: entry.display_name.as_ref().map(|t| t.into()),
}]), }]),
}); });
@ -676,7 +678,7 @@ pub(crate) fn update_player_list(
} }
for mut client in &mut clients { for mut client in &mut clients {
if client.is_new() { if client.is_added() {
pl.write_init_packets(client.into_inner()); pl.write_init_packets(client.into_inner());
} else { } else {
client.write_packet_bytes(&pl.cached_update_packets); client.write_packet_bytes(&pl.cached_update_packets);

View file

@ -18,22 +18,22 @@ use uuid::Uuid;
use valence_nbt::{compound, Compound, List}; use valence_nbt::{compound, Compound, List};
use valence_protocol::ident; use valence_protocol::ident;
use valence_protocol::types::Property; use valence_protocol::types::Property;
use valence_protocol::username::Username;
use crate::biome::{validate_biomes, Biome, BiomeId}; use crate::biome::{validate_biomes, Biome, BiomeId};
use crate::client::event::{register_client_events, run_event_loop}; use crate::client::event::{register_client_events, run_event_loop};
use crate::client::{update_clients, Client}; use crate::client::{update_clients, ClientBundle};
use crate::component::{Despawned, OldLocation, OldPosition};
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::{deinit_despawned_entities, init_entities, update_entities, McEntityManager}; use crate::entity::{
deinit_despawned_mcentities, init_mcentities, update_mcentities, 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::update_inventories; use crate::inventory::update_inventories;
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;
mod byte_channel; mod byte_channel;
mod connect; mod connect;
@ -92,9 +92,9 @@ struct SharedServerInner {
/// Sent to all clients when joining. /// Sent to all clients when joining.
registry_codec: Compound, registry_codec: Compound,
/// 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<ClientBundle>,
/// Receiver for new clients past the login stage. /// Receiver for new clients past the login stage.
new_clients_recv: Receiver<Client>, new_clients_recv: Receiver<ClientBundle>,
/// A semaphore used to limit the number of simultaneous connections to the /// A semaphore used to limit the number of simultaneous connections to the
/// server. Closing this semaphore stops new connections. /// server. Closing this semaphore stops new connections.
connection_sema: Arc<Semaphore>, connection_sema: Arc<Semaphore>,
@ -202,7 +202,7 @@ impl SharedServer {
#[non_exhaustive] #[non_exhaustive]
pub struct NewClientInfo { pub struct NewClientInfo {
/// The username of the new client. /// The username of the new client.
pub username: Username<String>, pub username: String,
/// The UUID of the new client. /// The UUID of the new client.
pub uuid: Uuid, pub uuid: Uuid,
/// The remote address of the new client. /// The remote address of the new client.
@ -298,7 +298,7 @@ pub fn build_plugin(
break break
}; };
world.spawn((client, Inventory::new(InventoryKind::Player))); world.spawn(client);
} }
}; };
@ -341,22 +341,29 @@ pub fn build_plugin(
// Add internal valence systems that run after `CoreSet::Update`. // Add internal valence systems that run after `CoreSet::Update`.
app.add_systems( app.add_systems(
( (
init_entities, init_mcentities,
check_instance_invariants, check_instance_invariants,
update_player_list.before(update_instances_pre_client), update_player_list.before(update_instances_pre_client),
update_instances_pre_client.after(init_entities), update_instances_pre_client.after(init_mcentities),
update_clients.after(update_instances_pre_client), update_instances_post_client.after(update_instances_pre_client),
update_instances_post_client.after(update_clients), deinit_despawned_mcentities.after(update_instances_post_client),
deinit_despawned_entities.after(update_instances_post_client), despawn_marked_entities.after(deinit_despawned_mcentities),
despawn_marked_entities.after(deinit_despawned_entities), update_mcentities.after(despawn_marked_entities),
update_entities.after(despawn_marked_entities), OldPosition::update.after(despawn_marked_entities),
OldLocation::update.after(despawn_marked_entities),
) )
.in_base_set(CoreSet::PostUpdate), .in_base_set(CoreSet::PostUpdate),
) )
.add_systems( .add_systems(
update_inventories() update_inventories()
.in_base_set(CoreSet::PostUpdate) .in_base_set(CoreSet::PostUpdate)
.before(init_entities), .before(init_mcentities),
)
.add_systems(
update_clients()
.in_base_set(CoreSet::PostUpdate)
.after(update_instances_pre_client)
.before(update_instances_post_client),
) )
.add_system(increment_tick_counter.in_base_set(CoreSet::Last)); .add_system(increment_tick_counter.in_base_set(CoreSet::Last));
@ -367,7 +374,7 @@ pub fn build_plugin(
#[derive(ScheduleLabel, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] #[derive(ScheduleLabel, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
pub struct EventLoopSchedule; pub struct EventLoopSchedule;
/// The default base set for the event loop [`Schedule`]. /// The default base set for [`EventLoopSchedule`].
#[derive(SystemSet, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] #[derive(SystemSet, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
pub struct EventLoopSet; pub struct EventLoopSet;

View file

@ -34,13 +34,13 @@ use valence_protocol::packet::s2c::status::{QueryPongS2c, QueryResponseS2c};
use valence_protocol::raw::RawBytes; use valence_protocol::raw::RawBytes;
use valence_protocol::text::Text; use valence_protocol::text::Text;
use valence_protocol::types::Property; use valence_protocol::types::Property;
use valence_protocol::username::Username;
use valence_protocol::var_int::VarInt; use valence_protocol::var_int::VarInt;
use valence_protocol::{translation_key, Decode, MINECRAFT_VERSION, PROTOCOL_VERSION}; use valence_protocol::{translation_key, Decode, MINECRAFT_VERSION, PROTOCOL_VERSION};
use crate::config::{AsyncCallbacks, ConnectionMode, ServerListPing}; use crate::config::{AsyncCallbacks, ConnectionMode, ServerListPing};
use crate::server::connection::InitialConnection; use crate::server::connection::InitialConnection;
use crate::server::{NewClientInfo, SharedServer}; use crate::server::{NewClientInfo, SharedServer};
use crate::util::is_valid_username;
/// Accepts new connections to the server as they occur. /// Accepts new connections to the server as they occur.
#[instrument(skip_all)] #[instrument(skip_all)]
@ -150,7 +150,7 @@ async fn handle_handshake(
.context("error handling login")? .context("error handling login")?
{ {
Some(info) => { Some(info) => {
let client = conn.into_client( let client = conn.into_client_bundle(
info, info,
shared.0.incoming_capacity, shared.0.incoming_capacity,
shared.0.outgoing_capacity, shared.0.outgoing_capacity,
@ -238,7 +238,9 @@ async fn handle_login(
profile_id: _, // TODO profile_id: _, // TODO
} = conn.recv_packet().await?; } = conn.recv_packet().await?;
let username = username.to_owned_username(); ensure!(is_valid_username(username), "invalid username");
let username = username.to_owned();
let info = match shared.connection_mode() { let info = match shared.connection_mode() {
ConnectionMode::Online { .. } => { ConnectionMode::Online { .. } => {
@ -269,7 +271,7 @@ async fn handle_login(
conn.send_packet(&LoginSuccessS2c { conn.send_packet(&LoginSuccessS2c {
uuid: info.uuid, uuid: info.uuid,
username: info.username.as_str_username(), username: &info.username,
properties: Default::default(), properties: Default::default(),
}) })
.await?; .await?;
@ -283,7 +285,7 @@ pub(super) async fn login_online(
callbacks: &Arc<impl AsyncCallbacks>, callbacks: &Arc<impl AsyncCallbacks>,
conn: &mut InitialConnection<OwnedReadHalf, OwnedWriteHalf>, conn: &mut InitialConnection<OwnedReadHalf, OwnedWriteHalf>,
remote_addr: SocketAddr, remote_addr: SocketAddr,
username: Username<String>, username: String,
) -> anyhow::Result<NewClientInfo> { ) -> anyhow::Result<NewClientInfo> {
let my_verify_token: [u8; 16] = rand::random(); let my_verify_token: [u8; 16] = rand::random();
@ -331,7 +333,7 @@ pub(super) async fn login_online(
let url = callbacks let url = callbacks
.session_server( .session_server(
shared, shared,
username.as_str_username(), username.as_str(),
&auth_digest(&hash), &auth_digest(&hash),
&remote_addr.ip(), &remote_addr.ip(),
) )
@ -360,12 +362,17 @@ pub(super) async fn login_online(
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct GameProfile { struct GameProfile {
id: Uuid, id: Uuid,
name: Username<String>, name: String,
properties: Vec<Property>, properties: Vec<Property>,
} }
let profile: GameProfile = resp.json().await.context("parsing game profile")?; let profile: GameProfile = resp.json().await.context("parsing game profile")?;
ensure!(
is_valid_username(&profile.name),
"invalid game profile username"
);
ensure!(profile.name == username, "usernames do not match"); ensure!(profile.name == username, "usernames do not match");
Ok(NewClientInfo { Ok(NewClientInfo {
@ -383,7 +390,7 @@ fn auth_digest(bytes: &[u8]) -> String {
/// Login procedure for offline mode. /// Login procedure for offline mode.
pub(super) fn login_offline( pub(super) fn login_offline(
remote_addr: SocketAddr, remote_addr: SocketAddr,
username: Username<String>, username: String,
) -> anyhow::Result<NewClientInfo> { ) -> anyhow::Result<NewClientInfo> {
Ok(NewClientInfo { Ok(NewClientInfo {
// Derive the client's UUID from a hash of their username. // Derive the client's UUID from a hash of their username.
@ -397,7 +404,7 @@ pub(super) fn login_offline(
/// Login procedure for BungeeCord. /// Login procedure for BungeeCord.
pub(super) fn login_bungeecord( pub(super) fn login_bungeecord(
server_address: &str, server_address: &str,
username: Username<String>, username: String,
) -> anyhow::Result<NewClientInfo> { ) -> anyhow::Result<NewClientInfo> {
// Get data from server_address field of the handshake // Get data from server_address field of the handshake
let [_, client_ip, uuid, properties]: [&str; 4] = server_address let [_, client_ip, uuid, properties]: [&str; 4] = server_address
@ -422,7 +429,7 @@ pub(super) fn login_bungeecord(
/// Login procedure for Velocity. /// Login procedure for Velocity.
pub(super) async fn login_velocity( pub(super) async fn login_velocity(
conn: &mut InitialConnection<OwnedReadHalf, OwnedWriteHalf>, conn: &mut InitialConnection<OwnedReadHalf, OwnedWriteHalf>,
username: Username<String>, username: String,
velocity_secret: &str, velocity_secret: &str,
) -> anyhow::Result<NewClientInfo> { ) -> anyhow::Result<NewClientInfo> {
const VELOCITY_MIN_SUPPORTED_VERSION: u8 = 1; const VELOCITY_MIN_SUPPORTED_VERSION: u8 = 1;
@ -473,7 +480,7 @@ pub(super) async fn login_velocity(
// Get username and validate // Get username and validate
ensure!( ensure!(
username == Username::decode(&mut data_without_signature)?, username == <&str>::decode(&mut data_without_signature)?,
"mismatched usernames" "mismatched usernames"
); );

View file

@ -12,7 +12,7 @@ use tracing::debug;
use valence_protocol::codec::{PacketDecoder, PacketEncoder}; use valence_protocol::codec::{PacketDecoder, PacketEncoder};
use valence_protocol::Packet; use valence_protocol::Packet;
use crate::client::{Client, ClientConnection}; use crate::client::{ClientBundle, ClientConnection};
use crate::server::byte_channel::{ use crate::server::byte_channel::{
byte_channel, ByteReceiver, ByteSender, TryRecvError, TrySendError, byte_channel, ByteReceiver, ByteSender, TryRecvError, TrySendError,
}; };
@ -120,12 +120,12 @@ where
self.dec.enable_encryption(key); self.dec.enable_encryption(key);
} }
pub fn into_client( pub fn into_client_bundle(
mut self, mut self,
info: NewClientInfo, info: NewClientInfo,
incoming_limit: usize, incoming_limit: usize,
outgoing_limit: usize, outgoing_limit: usize,
) -> Client ) -> ClientBundle
where where
R: Send + 'static, R: Send + 'static,
W: Send + 'static, W: Send + 'static,
@ -171,7 +171,7 @@ where
} }
}); });
Client::new( ClientBundle::new(
info, info,
Box::new(RealClientConnection { Box::new(RealClientConnection {
send: outgoing_sender, send: outgoing_sender,

View file

@ -57,6 +57,7 @@ mod tests {
use super::*; use super::*;
use crate::client::Client; use crate::client::Client;
use crate::component::Position;
use crate::inventory::{Inventory, InventoryKind, OpenInventory}; use crate::inventory::{Inventory, InventoryKind, OpenInventory};
use crate::{assert_packet_count, assert_packet_order}; use crate::{assert_packet_count, assert_packet_order};
@ -90,8 +91,8 @@ mod tests {
app.update(); app.update();
// Make assertions // Make assertions
let client: &Client = app.world.get(client_ent).expect("client not found"); let pos = app.world.get::<Position>(client_ent).unwrap();
assert_eq!(client.position(), [12.0, 64.0, 0.0].into()); assert_eq!(pos.0, [12.0, 64.0, 0.0].into());
} }
/// A unit test where we want to test what packets are sent to the client. /// A unit test where we want to test what packets are sent to the client.

View file

@ -1,37 +1,36 @@
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use bevy_app::{App, CoreSchedule}; use bevy_app::prelude::*;
use bevy_ecs::prelude::Entity; use bevy_ecs::prelude::*;
use bevy_ecs::schedule::{LogLevel, ScheduleBuildSettings}; 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;
use valence_protocol::username::Username;
use valence_protocol::Packet; use valence_protocol::Packet;
use crate::client::{Client, ClientConnection}; use crate::client::{ClientBundle, ClientConnection};
use crate::component::Location;
use crate::config::{ConnectionMode, ServerPlugin}; use crate::config::{ConnectionMode, ServerPlugin};
use crate::dimension::DimensionId; use crate::dimension::DimensionId;
use crate::inventory::{Inventory, InventoryKind};
use crate::server::{NewClientInfo, Server}; use crate::server::{NewClientInfo, Server};
/// Creates a mock client that can be used for unit testing. /// Creates a mock client bundle that can be used for unit testing.
/// ///
/// Returns the client, and a helper to inject packets as if the client sent /// Returns the client, and a helper to inject packets as if the client sent
/// them and receive packets as if the client received them. /// them and receive packets as if the client received them.
pub fn create_mock_client(client_info: NewClientInfo) -> (Client, MockClientHelper) { pub(crate) fn create_mock_client(client_info: NewClientInfo) -> (ClientBundle, MockClientHelper) {
let mock_connection = MockClientConnection::new(); let mock_connection = MockClientConnection::new();
let enc = PacketEncoder::new(); let enc = PacketEncoder::new();
let dec = PacketDecoder::new(); let dec = PacketDecoder::new();
let client = Client::new(client_info, Box::new(mock_connection.clone()), enc, dec); let bundle = ClientBundle::new(client_info, Box::new(mock_connection.clone()), enc, dec);
(client, MockClientHelper::new(mock_connection))
(bundle, MockClientHelper::new(mock_connection))
} }
/// Creates a `NewClientInfo` with the given username and a random UUID. /// Creates a `NewClientInfo` with the given username and a random UUID.
/// Panics if the username is invalid. pub fn gen_client_info(username: impl Into<String>) -> NewClientInfo {
pub fn gen_client_info(username: &str) -> NewClientInfo {
NewClientInfo { NewClientInfo {
username: Username::new(username.to_owned()).unwrap(), username: username.into(),
uuid: uuid::Uuid::new_v4(), uuid: uuid::Uuid::new_v4(),
ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
properties: vec![], properties: vec![],
@ -169,15 +168,12 @@ pub fn scenario_single_client(app: &mut App) -> (Entity, MockClientHelper) {
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 (client, client_helper) = create_mock_client(gen_client_info("test"));
let (mut client, client_helper) = create_mock_client(info);
// HACK: needed so client does not get disconnected on first update let client_ent = app.world.spawn(client).id();
client.set_instance(instance_ent);
let client_ent = app // Set initial location.
.world app.world.get_mut::<Location>(client_ent).unwrap().0 = instance_ent;
.spawn((client, Inventory::new(InventoryKind::Player)))
.id();
// Print warnings if there are ambiguities in the schedule. // Print warnings if there are ambiguities in the schedule.
app.edit_schedule(CoreSchedule::Main, |schedule| { app.edit_schedule(CoreSchedule::Main, |schedule| {
@ -209,10 +205,15 @@ macro_rules! assert_packet_count {
assert_eq!( assert_eq!(
count, count,
$count, $count,
"expected {} {} packets, got {}", "expected {} {} packets, got {}\nPackets actually found:\n[\n\t{}\n]\n",
$count, $count,
stringify!($packet), stringify!($packet),
count count,
sent_packets
.iter()
.map(|p| format!("{:?}", p))
.collect::<Vec<_>>()
.join(",\n\t")
); );
}}; }};
} }

View file

@ -67,6 +67,30 @@ pub(crate) const fn bit_width(n: usize) -> usize {
(usize::BITS - n.leading_zeros()) as _ (usize::BITS - n.leading_zeros()) as _
} }
/// Returns whether or not the given string is a valid Minecraft username.
///
/// A valid username is 3 to 16 characters long with only ASCII alphanumeric
/// characters. The username must match the regex `^[a-zA-Z0-9_]{3,16}$` to be
/// considered valid.
///
/// # Examples
///
/// ```
/// use valence::util::is_valid_username;
///
/// assert!(is_valid_username("00a"));
/// assert!(is_valid_username("jeb_"));
///
/// assert!(!is_valid_username("notavalidusername"));
/// assert!(!is_valid_username("NotValid!"));
/// ```
pub fn is_valid_username(username: &str) -> bool {
(3..=16).contains(&username.len())
&& username
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use approx::assert_relative_eq; use approx::assert_relative_eq;

View file

@ -90,23 +90,28 @@ fn setup(world: &mut World) {
} }
fn init_clients( fn init_clients(
mut clients: Query<&mut Client, Added<Client>>, mut clients: Query<
(
&mut Position,
&mut Location,
&mut GameMode,
&mut IsFlat,
&UniqueId,
),
Added<Client>,
>,
instances: Query<Entity, With<Instance>>, instances: Query<Entity, With<Instance>>,
mut commands: Commands, mut commands: Commands,
) { ) {
for mut client in &mut clients { for (mut pos, mut loc, mut game_mode, mut is_flat, uuid) in &mut clients {
let instance = instances.single(); let instance = instances.single();
client.set_flat(true); pos.0 = SPAWN_POS;
client.set_game_mode(GameMode::Creative); loc.0 = instance;
client.set_position(SPAWN_POS); *game_mode = GameMode::Creative;
client.set_instance(instance); is_flat.0 = true;
commands.spawn(McEntity::with_uuid( commands.spawn(McEntity::with_uuid(EntityKind::Player, instance, uuid.0));
EntityKind::Player,
instance,
client.uuid(),
));
} }
} }
@ -118,13 +123,13 @@ fn remove_unviewed_chunks(mut instances: Query<&mut Instance>) {
fn update_client_views( fn update_client_views(
mut instances: Query<&mut Instance>, mut instances: Query<&mut Instance>,
mut clients: Query<&mut Client>, mut clients: Query<(&mut Client, View, OldView)>,
mut state: ResMut<GameState>, mut state: ResMut<GameState>,
) { ) {
let instance = instances.single_mut(); let instance = instances.single_mut();
for client in &mut clients { for (client, view, old_view) in &mut clients {
let view = client.view(); let view = view.get();
let queue_pos = |pos| { let queue_pos = |pos| {
if instance.chunk(pos).is_none() { if instance.chunk(pos).is_none() {
match state.pending.entry(pos) { match state.pending.entry(pos) {
@ -146,7 +151,7 @@ fn update_client_views(
if client.is_added() { if client.is_added() {
view.iter().for_each(queue_pos); view.iter().for_each(queue_pos);
} else { } else {
let old_view = client.old_view(); let old_view = old_view.get();
if old_view != view { if old_view != view {
view.diff(old_view).for_each(queue_pos); view.diff(old_view).for_each(queue_pos);
} }

View file

@ -505,7 +505,6 @@ mod tests {
use crate::item::{ItemKind, ItemStack}; use crate::item::{ItemKind, ItemStack};
use crate::text::{Text, TextFormat}; use crate::text::{Text, TextFormat};
use crate::tracked_data::PaintingKind; use crate::tracked_data::PaintingKind;
use crate::username::Username;
use crate::var_long::VarLong; use crate::var_long::VarLong;
use crate::Decode; use crate::Decode;
@ -525,12 +524,11 @@ mod tests {
h: Ident<&'a str>, h: Ident<&'a str>,
i: Option<ItemStack>, i: Option<ItemStack>,
j: Text, j: Text,
k: Username<&'a str>, k: VarInt,
l: VarInt, l: VarLong,
m: VarLong, m: &'a str,
n: &'a str, n: &'a [u8; 10],
o: &'a [u8; 10], o: [u128; 3],
p: [u128; 3],
} }
impl<'a> TestPacket<'a> { impl<'a> TestPacket<'a> {
@ -546,12 +544,11 @@ mod tests {
h: Ident::new("minecraft:whatever").unwrap(), h: Ident::new("minecraft:whatever").unwrap(),
i: Some(ItemStack::new(ItemKind::WoodenSword, 12, None)), i: Some(ItemStack::new(ItemKind::WoodenSword, 12, None)),
j: "my ".into_text() + "fancy".italic() + " text", j: "my ".into_text() + "fancy".italic() + " text",
k: Username::new("00a").unwrap(), k: VarInt(123),
l: VarInt(123), l: VarLong(456),
m: VarLong(456), m: n,
n, n: &[7; 10],
o: &[7; 10], o: [123456789; 3],
p: [123456789; 3],
} }
} }

View file

@ -62,11 +62,7 @@
unused_import_braces, unused_import_braces,
clippy::dbg_macro clippy::dbg_macro
)] )]
#![allow( #![allow(clippy::unusual_byte_groupings)]
clippy::derive_partial_eq_without_eq,
clippy::unusual_byte_groupings,
clippy::comparison_chain
)]
// Allows us to use our own proc macros internally. // Allows us to use our own proc macros internally.
extern crate self as valence_protocol; extern crate self as valence_protocol;
@ -101,7 +97,6 @@ pub mod text;
pub mod tracked_data; pub mod tracked_data;
pub mod translation_key; pub mod translation_key;
pub mod types; pub mod types;
pub mod username;
pub mod var_int; pub mod var_int;
pub mod var_long; pub mod var_long;

View file

@ -1,10 +1,9 @@
use uuid::Uuid; use uuid::Uuid;
use crate::username::Username;
use crate::{Decode, Encode}; use crate::{Decode, Encode};
#[derive(Clone, Debug, Encode, Decode)] #[derive(Clone, Debug, Encode, Decode)]
pub struct LoginHelloC2s<'a> { pub struct LoginHelloC2s<'a> {
pub username: Username<&'a str>, pub username: &'a str, // TODO: bound this
pub profile_id: Option<Uuid>, pub profile_id: Option<Uuid>,
} }

View file

@ -3,12 +3,11 @@ use std::borrow::Cow;
use uuid::Uuid; use uuid::Uuid;
use crate::types::Property; use crate::types::Property;
use crate::username::Username;
use crate::{Decode, Encode}; use crate::{Decode, Encode};
#[derive(Clone, Debug, Encode, Decode)] #[derive(Clone, Debug, Encode, Decode)]
pub struct LoginSuccessS2c<'a> { pub struct LoginSuccessS2c<'a> {
pub uuid: Uuid, pub uuid: Uuid,
pub username: Username<&'a str>, pub username: &'a str, // TODO: bound this.
pub properties: Cow<'a, [Property]>, pub properties: Cow<'a, [Property]>,
} }

View file

@ -1,194 +0,0 @@
use std::borrow::Borrow;
use std::error::Error;
use std::fmt;
use std::fmt::Formatter;
use std::io::Write;
use std::str::FromStr;
use anyhow::anyhow;
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize};
use crate::text::Text;
use crate::{Decode, Encode, Result};
/// A newtype wrapper around a string type `S` which guarantees the wrapped
/// string meets the criteria for a valid Minecraft username.
///
/// A valid username is 3 to 16 characters long with only ASCII alphanumeric
/// characters. The username must match the regex `^[a-zA-Z0-9_]{3,16}$` to be
/// considered valid.
///
/// # Contract
///
/// The type `S` must meet the following criteria:
/// - All calls to [`AsRef::as_ref`] and [`Borrow::borrow`] while the string is
/// wrapped in `Username` must return the same value.
///
/// # Examples
///
/// ```
/// use valence_protocol::username::Username;
///
/// assert!(Username::new("00a").is_ok());
/// assert!(Username::new("jeb_").is_ok());
///
/// assert!(Username::new("notavalidusername").is_err());
/// assert!(Username::new("NotValid!").is_err());
/// ```
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize)]
#[repr(transparent)]
#[serde(transparent)]
pub struct Username<S>(S);
impl<S: AsRef<str>> Username<S> {
pub fn new(string: S) -> Result<Self, UsernameError<S>> {
let s = string.as_ref();
if (3..=16).contains(&s.len()) && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
Ok(Self(string))
} else {
Err(UsernameError(string))
}
}
pub fn as_str(&self) -> &str {
self.0.as_ref()
}
pub fn as_str_username(&self) -> Username<&str> {
Username(self.0.as_ref())
}
pub fn into_inner(self) -> S {
self.0
}
}
impl<'a, S: ?Sized> Username<&'a S> {
pub fn to_owned_username(&self) -> Username<S::Owned>
where
S: ToOwned,
S::Owned: AsRef<str>,
{
Username(self.0.to_owned())
}
}
impl<S> AsRef<str> for Username<S>
where
S: AsRef<str>,
{
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl<S> Borrow<str> for Username<S>
where
S: Borrow<str>,
{
fn borrow(&self) -> &str {
self.0.borrow()
}
}
impl FromStr for Username<String> {
type Err = UsernameError<String>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Username::new(s.to_owned())
}
}
impl TryFrom<String> for Username<String> {
type Error = UsernameError<String>;
fn try_from(value: String) -> Result<Self, Self::Error> {
Username::new(value)
}
}
impl<S> From<Username<S>> for String
where
S: Into<String> + AsRef<str>,
{
fn from(value: Username<S>) -> Self {
value.0.into()
}
}
impl<S> From<Username<S>> for Text
where
S: AsRef<str>,
{
fn from(value: Username<S>) -> Self {
Text::text(value.as_str().to_owned())
}
}
impl<S> fmt::Display for Username<S>
where
S: AsRef<str>,
{
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.0.as_ref().fmt(f)
}
}
impl<S> Encode for Username<S>
where
S: Encode,
{
fn encode(&self, w: impl Write) -> Result<()> {
self.0.encode(w)
}
}
impl<'a, S> Decode<'a> for Username<S>
where
S: Decode<'a> + AsRef<str>,
{
fn decode(r: &mut &'a [u8]) -> Result<Self> {
Username::new(S::decode(r)?).map_err(|e| anyhow!("{e:#}"))
}
}
impl<'de, S> Deserialize<'de> for Username<S>
where
S: Deserialize<'de> + AsRef<str>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Username::new(S::deserialize(deserializer)?).map_err(D::Error::custom)
}
}
/// The error type created when a [`Username`] cannot be parsed from a string.
/// Contains the offending string.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct UsernameError<S>(pub S);
impl<S> fmt::Debug for UsernameError<S>
where
S: AsRef<str>,
{
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_tuple("UsernameError")
.field(&self.0.as_ref())
.finish()
}
}
impl<S> fmt::Display for UsernameError<S>
where
S: AsRef<str>,
{
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "invalid username \"{}\"", self.0.as_ref())
}
}
impl<S> Error for UsernameError<S> where S: AsRef<str> {}

View file

@ -13,7 +13,6 @@ use valence_protocol::packet::c2s::play::{
KeepAliveC2s, PositionAndOnGroundC2s, TeleportConfirmC2s, KeepAliveC2s, PositionAndOnGroundC2s, TeleportConfirmC2s,
}; };
use valence_protocol::packet::{C2sHandshakePacket, S2cLoginPacket, S2cPlayPacket}; use valence_protocol::packet::{C2sHandshakePacket, S2cLoginPacket, S2cPlayPacket};
use valence_protocol::username::Username;
use valence_protocol::var_int::VarInt; use valence_protocol::var_int::VarInt;
use valence_protocol::PROTOCOL_VERSION; use valence_protocol::PROTOCOL_VERSION;
@ -56,7 +55,7 @@ pub async fn make_session<'a>(params: &SessionParams<'a>) -> anyhow::Result<()>
enc.append_packet(&handshake_pkt)?; enc.append_packet(&handshake_pkt)?;
enc.append_packet(&LoginHelloC2s { enc.append_packet(&LoginHelloC2s {
username: Username::new(sess_name).unwrap(), username: sess_name,
profile_id: Some(Uuid::new_v4()), profile_id: Some(Uuid::new_v4()),
})?; })?;