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
/// sneaking.
pub fn toggle_gamemode_on_sneak(
mut clients: Query<&mut Client>,
mut clients: Query<&mut GameMode>,
mut events: EventReader<StartSneaking>,
) {
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;
};
let mode = client.game_mode();
client.set_game_mode(match mode {
*mode = match *mode {
GameMode::Survival => GameMode::Creative,
GameMode::Creative => GameMode::Survival,
_ => GameMode::Creative,
});
};
}
}

View file

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

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use std::time::Instant;
use valence::client::despawn_disconnected_clients;
@ -65,19 +67,26 @@ fn setup(mut commands: Commands, server: Res<Server>) {
}
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>>,
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 {
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
client.set_instance(instance);
client.set_game_mode(GameMode::Creative);
let player_entity = McEntity::with_uuid(EntityKind::Player, instance, client.uuid());
commands.entity(client_entity).insert(player_entity);
commands
.entity(entity)
.insert(McEntity::with_uuid(EntityKind::Player, loc.0, unique_id.0));
}
}

View file

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

View file

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

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use valence::client::despawn_disconnected_clients;
use valence::client::event::{
default_event_handler, PlayerInteractBlock, StartDigging, StartSneaking, StopDestroyBlock,
@ -48,77 +50,90 @@ fn setup(mut commands: Commands, server: Res<Server>) {
}
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>>,
mut commands: Commands,
) {
for mut client in &mut clients {
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
client.set_instance(instances.single());
client.set_game_mode(GameMode::Creative);
for (entity, uuid, mut client, 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;
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(
mut clients: Query<&mut Client>,
mut clients: Query<&mut GameMode>,
mut events: EventReader<StartSneaking>,
) {
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;
};
let mode = client.game_mode();
client.set_game_mode(match mode {
*mode = match *mode {
GameMode::Survival => GameMode::Creative,
GameMode::Creative => GameMode::Survival,
_ => GameMode::Creative,
});
};
}
}
fn digging_creative_mode(
clients: Query<&Client>,
clients: Query<&GameMode>,
mut instances: Query<&mut Instance>,
mut events: EventReader<StartDigging>,
) {
let mut instance = instances.single_mut();
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;
};
if client.game_mode() == GameMode::Creative {
if *game_mode == GameMode::Creative {
instance.set_block(event.position, BlockState::AIR);
}
}
}
fn digging_survival_mode(
clients: Query<&Client>,
clients: Query<&GameMode>,
mut instances: Query<&mut Instance>,
mut events: EventReader<StopDestroyBlock>,
) {
let mut instance = instances.single_mut();
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;
};
if client.game_mode() == GameMode::Survival {
if *game_mode == GameMode::Survival {
instance.set_block(event.position, BlockState::AIR);
}
}
}
fn place_blocks(
mut clients: Query<(&Client, &mut Inventory)>,
mut clients: Query<(&mut Inventory, &GameMode, &PlayerInventoryState)>,
mut instances: Query<&mut Instance>,
mut events: EventReader<PlayerInteractBlock>,
) {
let mut instance = instances.single_mut();
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;
};
if event.hand != Hand::Main {
@ -126,7 +141,7 @@ fn place_blocks(
}
// 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 {
// no item in the slot
continue;
@ -137,7 +152,7 @@ fn place_blocks(
continue;
};
if client.game_mode() == GameMode::Survival {
if *game_mode == GameMode::Survival {
// check if the player has the item in their inventory and remove
// it.
if stack.count() > 1 {

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use bevy_app::App;
use tracing::warn;
use valence::client::despawn_disconnected_clients;
@ -37,7 +39,7 @@ fn setup(mut commands: Commands, server: Res<Server>) {
for z in -25..25 {
for x in -25..25 {
instance.set_block([x, SPAWN_Y, z], BlockState::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(
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>>,
mut commands: Commands,
) {
for mut client in &mut clients {
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
client.set_instance(instances.single());
client.set_game_mode(GameMode::Adventure);
for (entity, uuid, mut client, mut pos, mut loc, mut game_mode) in &mut clients {
pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
loc.0 = instances.single();
*game_mode = GameMode::Adventure;
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() {
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);
continue;
};
let message = message.message.to_string();
let formatted = format!("<{}>: ", client.username())
.bold()
.color(Color::YELLOW)
+ message.into_text().not_bold().color(Color::WHITE);
let formatted = format!("<{}>: ", username.0).bold().color(Color::YELLOW)
+ message.not_bold().color(Color::WHITE);
// TODO: write message to instance buffer.
for mut client in &mut clients {
for (mut client, _) in &mut clients {
client.send_message(formatted.clone());
}
}

View file

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

View file

@ -1,3 +1,6 @@
#![allow(clippy::type_complexity)]
use bevy_ecs::query::WorldQuery;
use glam::Vec3Swizzles;
use valence::client::despawn_disconnected_clients;
use valence::client::event::{
@ -64,43 +67,50 @@ fn setup(mut commands: Commands, server: Res<Server>) {
}
fn init_clients(
mut commands: Commands,
mut clients: Query<(Entity, &mut Client), Added<Client>>,
mut clients: Query<(Entity, &UniqueId, &mut Position, &mut Location), Added<Client>>,
instances: Query<Entity, With<Instance>>,
mut commands: Commands,
) {
let instance = instances.single();
for (entity, mut client) in &mut clients {
client.set_position([0.0, SPAWN_Y as f64, 0.0]);
client.set_instance(instance);
for (entity, uuid, mut pos, mut loc) in &mut clients {
pos.set([0.0, SPAWN_Y as f64, 0.0]);
loc.0 = instances.single();
commands.entity(entity).insert((
CombatState {
last_attacked_tick: 0,
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(
manager: Res<McEntityManager>,
server: Res<Server>,
mut clients: Query<CombatQuery>,
mut start_sprinting: EventReader<StartSprinting>,
mut stop_sprinting: EventReader<StopSprinting>,
mut interact_with_entity: EventReader<PlayerInteract>,
mut clients: Query<(&mut Client, &mut CombatState, &mut McEntity)>,
) {
for &StartSprinting { client } in start_sprinting.iter() {
if let Ok((_, mut state, _)) = clients.get_mut(client) {
state.has_bonus_knockback = true;
if let Ok(mut client) = clients.get_mut(client) {
client.state.has_bonus_knockback = true;
}
}
for &StopSprinting { client } in stop_sprinting.iter() {
if let Ok((_, mut state, _)) = clients.get_mut(client) {
state.has_bonus_knockback = false;
if let Ok(mut client) = clients.get_mut(client) {
client.state.has_bonus_knockback = false;
}
}
@ -115,49 +125,53 @@ fn handle_combat_events(
continue
};
let Ok([(attacker_client, mut attacker_state, _), (mut victim_client, mut victim_state, mut victim_entity)]) =
clients.get_many_mut([attacker_client, victim_client])
else {
let Ok([mut attacker, mut victim]) = clients.get_many_mut([attacker_client, victim_client]) else {
// Victim or attacker does not exist, or the attacker is attacking itself.
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.
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 attacker_pos = attacker_client.position().xz();
let victim_pos = victim.pos.0.xz();
let attacker_pos = attacker.pos.0.xz();
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
} else {
8.0
};
let knockback_y = if attacker_state.has_bonus_knockback {
let knockback_y = if attacker.state.has_bonus_knockback {
8.432
} else {
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_entity.trigger_status(EntityStatus::DamageFromGenericSource);
victim
.client
.trigger_status(EntityStatus::DamageFromGenericSource);
victim
.entity
.trigger_status(EntityStatus::DamageFromGenericSource);
}
}
fn teleport_oob_clients(mut clients: Query<&mut Client>) {
for mut client in &mut clients {
if client.position().y < 0.0 {
client.set_position([0.0, SPAWN_Y as _, 0.0]);
fn teleport_oob_clients(mut clients: Query<&mut Position, With<Client>>) {
for mut pos in &mut clients {
if pos.0.y < 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 valence::client::despawn_disconnected_clients;
@ -65,13 +67,24 @@ fn setup(mut commands: Commands, server: Res<Server>) {
}
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>>,
mut commands: Commands,
) {
for mut client in &mut clients {
client.set_position(SPAWN_POS);
client.set_instance(instances.single());
client.set_game_mode(GameMode::Survival);
for (entity, uuid, mut client, mut pos, mut loc, mut game_mode) in &mut clients {
pos.0 = SPAWN_POS;
loc.0 = instances.single();
*game_mode = GameMode::Survival;
client.send_message("Welcome to Conway's game of life in Minecraft!".italic());
client.send_message(
@ -79,6 +92,9 @@ fn init_clients(
life."
.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>) {
for mut client in &mut clients {
if client.position().y < 0.0 {
client.set_position(SPAWN_POS);
fn reset_oob_clients(
mut clients: Query<&mut Position, With<Client>>,
mut board: ResMut<LifeBoard>,
) {
for mut pos in &mut clients {
if pos.0.y < 0.0 {
pos.0 = SPAWN_POS;
board.clear();
}
}

View file

@ -1,10 +1,12 @@
#![allow(clippy::type_complexity)]
use std::f64::consts::TAU;
use glam::{DQuat, EulerRot};
use valence::client::despawn_disconnected_clients;
use valence::client::event::default_event_handler;
use valence::math::to_yaw_and_pitch;
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_AMOUNT: usize = 200;
@ -52,17 +54,31 @@ fn setup(mut commands: Commands, server: Res<Server>) {
}
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>>,
mut commands: Commands,
) {
for mut client in &mut clients {
client.set_position([
for (entity, uuid, mut pos, mut loc, mut game_mode) in &mut clients {
pos.set([
SPAWN_POS.x as f64 + 0.5,
SPAWN_POS.y as f64 + 1.0,
SPAWN_POS.z as f64 + 0.5,
]);
client.set_instance(instances.single());
client.set_game_mode(GameMode::Creative);
loc.0 = instances.single();
*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::event::{default_event_handler, PerformRespawn, StartSneaking};
use valence::prelude::*;
@ -9,7 +10,7 @@ pub fn main() {
tracing_subscriber::fmt().init();
App::new()
.add_plugin(ServerPlugin::new(()))
.add_plugin(ServerPlugin::new(()).with_connection_mode(ConnectionMode::Offline))
.add_startup_system(setup)
.add_system(init_clients)
.add_systems(
@ -41,51 +42,59 @@ fn setup(mut commands: Commands, server: Res<Server>) {
}
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>>,
mut commands: Commands,
) {
let instance = instances.into_iter().next().unwrap();
for mut client in &mut clients {
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
client.set_respawn_screen(true);
client.set_instance(instance);
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]);
has_respawn_screen.0 = true;
loc.0 = instances.iter().next().unwrap();
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>) {
for event in events.iter() {
let Ok(mut client) = clients.get_component_mut::<Client>(event.client) else {
warn!("Client {:?} not found", event.client);
continue;
};
client.kill(None, "Squatted too hard.");
if let Ok(mut client) = clients.get_mut(event.client) {
client.kill(None, "Squatted too hard.");
}
}
}
fn necromancy(
mut clients: Query<&mut Client>,
mut clients: Query<(&mut Position, &mut Look, &mut Location)>,
mut events: EventReader<PerformRespawn>,
instances: Query<Entity, With<Instance>>,
) {
for event in events.iter() {
let Ok(mut client) = clients.get_component_mut::<Client>(event.client) else {
continue;
};
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
client.set_velocity([0.0, 0.0, 0.0]);
client.set_yaw(0.0);
client.set_pitch(0.0);
// make the client respawn in another instance
let idx = instances
.iter()
.position(|i| i == client.instance())
.unwrap();
let count = instances.iter().count();
client.set_instance(instances.into_iter().nth((idx + 1) % count).unwrap());
if let Ok((mut pos, mut look, mut loc)) = clients.get_mut(event.client) {
pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
look.yaw = 0.0;
look.pitch = 0.0;
// make the client respawn in another instance
let idx = instances.iter().position(|i| i == loc.0).unwrap();
let count = instances.iter().count();
loc.0 = 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::event::{default_event_handler, CommandExecution};
use valence::prelude::*;
@ -36,28 +38,47 @@ fn setup(mut commands: Commands, server: Res<Server>) {
}
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>>,
mut commands: Commands,
) {
for mut client in &mut clients {
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
client.set_instance(instances.single());
client.set_game_mode(GameMode::Creative);
client.set_op_level(2); // required to use F3+F4, eg /gamemode
for (entity, uuid, mut client, mut pos, mut loc, mut game_mode, mut op_level) in &mut clients {
pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
loc.0 = instances.single();
*game_mode = GameMode::Creative;
op_level.set(2); // required to use F3+F4, eg /gamemode
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() {
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;
};
let mut args = event.command.split_whitespace();
if args.next() == Some("gamemode") {
if client.op_level() < 2 {
if op_level.get() < 2 {
// not enough permissions to use gamemode command
continue;
}
@ -72,7 +93,7 @@ fn interpret_command(mut clients: Query<&mut Client>, mut events: EventReader<Co
continue;
}
};
client.set_game_mode(mode);
*game_mode = mode;
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::time::{SystemTime, UNIX_EPOCH};
@ -37,7 +39,6 @@ pub fn main() {
manage_blocks,
despawn_disconnected_clients,
))
.add_system(despawn_disconnected_clients)
.run();
}
@ -51,28 +52,27 @@ struct GameState {
}
fn init_clients(
mut commands: Commands,
mut clients: Query<
(
Entity,
&mut Client,
&UniqueId,
&mut IsFlat,
&mut Location,
&mut GameMode,
),
Added<Client>,
>,
server: Res<Server>,
mut clients: Query<(Entity, &mut Client), Added<Client>>,
mut commands: Commands,
) {
for (ent, mut client) in clients.iter_mut() {
let mut instance = server.new_instance(DimensionId::default());
for pos in client.view().with_dist(VIEW_DIST).iter() {
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);
for (entity, mut client, uuid, mut is_flat, mut loc, mut game_mode) in clients.iter_mut() {
is_flat.0 = true;
loc.0 = entity;
*game_mode = GameMode::Adventure;
client.send_message("Welcome to epic infinite parkour game!".italic());
let mut state = GameState {
let state = GameState {
blocks: VecDeque::new(),
score: 0,
combo: 0,
@ -80,39 +80,75 @@ fn init_clients(
last_block_timestamp: 0,
};
reset(&mut client, &mut state, &mut instance);
let instance = server.new_instance(DimensionId::default());
commands.entity(ent).insert(state);
commands.entity(ent).insert(instance);
let mcentity = McEntity::with_uuid(EntityKind::Player, entity, uuid.0);
commands.entity(entity).insert((state, instance, mcentity));
}
}
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() {
if (client.position().y as i32) < START_POS.y - 32 {
client.send_message(
"Your score was ".italic()
+ state
.score
.to_string()
.color(Color::GOLD)
.bold()
.not_italic(),
);
for (mut client, mut pos, mut look, mut state, mut instance) in clients.iter_mut() {
let out_of_bounds = (pos.0.y as i32) < START_POS.y - 32;
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)>) {
for (mut client, mut state, mut instance) in clients.iter_mut() {
fn manage_blocks(mut clients: Query<(&mut Client, &Position, &mut GameState, &mut Instance)>) {
for (mut client, pos, mut state, mut instance) in clients.iter_mut() {
let pos_under_player = BlockPos::new(
(client.position().x - 0.5).round() as i32,
client.position().y as i32 - 1,
(client.position().z - 0.5).round() as i32,
(pos.0.x - 0.5).round() as i32,
pos.0.y as i32 - 1,
(pos.0.z - 0.5).round() as i32,
);
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 pos = client.position();
client.play_sound(
Sound::BlockNoteBlockBass,
SoundCategory::Master,
pos,
pos.0,
1.0,
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)>) {
for (client, mut instance) in &mut clients {
let old_view = client.old_view().with_dist(VIEW_DIST);
let view = client.view().with_dist(VIEW_DIST);
fn manage_chunks(mut clients: Query<(&Position, &OldPosition, &mut Instance), With<Client>>) {
for (pos, old_pos, mut instance) in &mut clients {
let old_view = ChunkView::new(old_pos.chunk_pos(), VIEW_DIST);
let view = ChunkView::new(pos.chunk_pos(), VIEW_DIST);
if old_view != 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) {
@ -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) {
if in_game {
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::event::default_event_handler;
use valence::prelude::*;
@ -19,23 +23,7 @@ pub fn main() {
}
#[derive(Resource)]
struct ParticleSpawner {
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();
}
}
struct ParticleVec(Vec<Particle>);
fn setup(mut commands: Commands, server: Res<Server>) {
let mut instance = server.new_instance(DimensionId::default());
@ -50,35 +38,48 @@ fn setup(mut commands: Commands, server: Res<Server>) {
commands.spawn(instance);
let spawner = ParticleSpawner::new();
commands.insert_resource(spawner)
commands.insert_resource(ParticleVec(create_particle_vec()));
}
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>>,
mut commands: Commands,
) {
for mut client in &mut clients {
client.set_position([0.5, SPAWN_Y as f64 + 1.0, 0.5]);
client.set_instance(instances.single());
client.set_game_mode(GameMode::Creative);
for (entity, uuid, mut pos, mut loc, mut game_mode) in &mut clients {
pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]);
loc.0 = instances.single();
*game_mode = GameMode::Creative;
commands
.entity(entity)
.insert(McEntity::with_uuid(EntityKind::Player, loc.0, uuid.0));
}
}
fn manage_particles(
mut spawner: ResMut<ParticleSpawner>,
particles: Res<ParticleVec>,
server: Res<Server>,
mut instances: Query<&mut Instance>,
mut particle_idx: Local<usize>,
) {
if server.current_tick() % 20 == 0 {
spawner.next();
}
if server.current_tick() % 5 != 0 {
if server.current_tick() % 20 != 0 {
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 pos = [0.5, SPAWN_Y as f64 + 2.0, 5.0];
@ -90,7 +91,7 @@ fn manage_particles(
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:?}");
string

View file

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

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use valence::client::despawn_disconnected_clients;
use valence::client::event::{
default_event_handler, PlayerInteract, ResourcePackStatus, ResourcePackStatusChange,
@ -52,15 +54,30 @@ fn setup(mut commands: Commands, server: Res<Server>) {
}
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>>,
mut commands: Commands,
) {
for mut client in &mut clients {
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
client.set_instance(instances.single());
client.set_game_mode(GameMode::Creative);
for (entity, uuid, mut client, mut pos, mut loc, mut game_mode) in &mut clients {
pos.set([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
loc.0 = instances.single();
*game_mode = GameMode::Creative;
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 valence::prelude::*;

View file

@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::sync::Arc;
@ -106,23 +108,31 @@ fn setup(mut commands: Commands, server: Res<Server>) {
}
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>>,
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();
client.set_flat(true);
client.set_game_mode(GameMode::Creative);
client.set_position(SPAWN_POS);
client.set_instance(instance);
is_flat.0 = true;
*game_mode = GameMode::Creative;
pos.0 = SPAWN_POS;
loc.0 = instance;
commands.spawn(McEntity::with_uuid(
EntityKind::Player,
instance,
client.uuid(),
));
commands
.entity(entity)
.insert(McEntity::with_uuid(EntityKind::Player, loc.0, uuid.0));
}
}
@ -134,13 +144,13 @@ fn remove_unviewed_chunks(mut instances: Query<&mut Instance>) {
fn update_client_views(
mut instances: Query<&mut Instance>,
mut clients: Query<&mut Client>,
mut clients: Query<(&mut Client, View, OldView)>,
mut state: ResMut<GameState>,
) {
let instance = instances.single_mut();
for client in &mut clients {
let view = client.view();
for (client, view, old_view) in &mut clients {
let view = view.get();
let queue_pos = |pos| {
if instance.chunk(pos).is_none() {
match state.pending.entry(pos) {
@ -162,7 +172,7 @@ fn update_client_views(
if client.is_added() {
view.iter().for_each(queue_pos);
} else {
let old_view = client.old_view();
let old_view = old_view.get();
if old_view != view {
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::event::default_event_handler;
use valence::prelude::*;
@ -39,13 +41,13 @@ fn setup(world: &mut World) {
}
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>>,
) {
for mut client in &mut clients {
client.set_position([0.0, SPAWN_Y as f64 + 1.0, 0.0]);
client.set_instance(instances.single());
client.set_game_mode(GameMode::Creative);
for (mut client, 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;
client.send_message("Welcome to the text example.".bold());
client

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@ use std::cmp;
use anyhow::bail;
use bevy_ecs::prelude::*;
use bevy_ecs::query::WorldQuery;
use bevy_ecs::system::{SystemParam, SystemState};
use glam::{DVec3, Vec3};
use paste::paste;
@ -30,7 +31,12 @@ use valence_protocol::packet::C2sPlayPacket;
use valence_protocol::tracked_data::Pose;
use valence_protocol::types::{Difficulty, Direction, Hand};
use super::{
CursorItem, KeepaliveState, PlayerActionSequence, PlayerInventoryState, TeleportState,
ViewDistance,
};
use crate::client::Client;
use crate::component::{Look, OnGround, Ping, Position};
use crate::entity::{EntityAnimation, EntityKind, McEntity, TrackedData};
use crate::inventory::Inventory;
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.
#[allow(clippy::type_complexity)]
pub(crate) fn run_event_loop(
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>>,
) {
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);
for (entity, client, inventory) in &mut clients {
let client = client.into_inner();
let inventory = inventory.into_inner();
let Ok(bytes) = client.conn.try_recv() else {
for mut q in &mut clients {
let Ok(bytes) = q.client.conn.try_recv() else {
// Client is disconnected.
client.is_disconnected = true;
commands.entity(q.entity).remove::<Client>();
continue;
};
@ -661,81 +680,75 @@ pub(crate) fn run_event_loop(
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) => {
if had_packet {
// We decoded one packet, but there might be more.
clients_to_check.push(entity);
clients_to_check.push(q.entity);
}
}
Err(e) => {
warn!(
username = %client.username,
uuid = %client.uuid,
ip = %client.ip,
"failed to dispatch events: {e:#}"
);
client.is_disconnected = true;
warn!("failed to dispatch events for client {:?}: {e:?}", q.entity);
commands.entity(q.entity).remove::<Client>();
}
}
}
state.apply(world);
// Keep looping until all serverbound packets are decoded.
while !clients_to_check.is_empty() {
world.run_schedule(EventLoopSchedule);
let (mut clients, mut events) = state.get_mut(world);
let (mut clients, mut events, mut commands) = state.get_mut(world);
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.
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,
Err(e) => {
warn!(
username = %client.username,
uuid = %client.uuid,
ip = %client.ip,
"failed to dispatch events: {e:#}"
);
client.is_disconnected = true;
warn!("failed to dispatch events for client {:?}: {e:?}", q.entity);
commands.entity(entity).remove::<Client>();
false
}
}
});
state.apply(world);
}
}
fn handle_one_packet(
client: &mut Client,
inventory: &mut Inventory,
entity: Entity,
q: &mut EventLoopQueryItem,
events: &mut ClientEvents,
) -> 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.
return Ok(false);
};
let entity = q.entity;
match pkt {
C2sPlayPacket::TeleportConfirmC2s(p) => {
if client.pending_teleports == 0 {
if q.teleport_state.pending_teleports == 0 {
bail!("unexpected teleport confirmation");
}
let got = p.teleport_id.0 as u32;
let expected = client
let expected = q
.teleport_state
.teleport_id_counter
.wrapping_sub(client.pending_teleports);
.wrapping_sub(q.teleport_state.pending_teleports);
if got == expected {
client.pending_teleports -= 1;
q.teleport_state.pending_teleports -= 1;
} else {
bail!("unexpected teleport ID (expected {expected}, got {got}");
}
@ -814,7 +827,7 @@ fn handle_one_packet(
}
C2sPlayPacket::ClickSlotC2s(p) => {
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 {
client: entity,
from_slot: None,
@ -823,13 +836,13 @@ fn handle_one_packet(
}
} else if p.mode == ClickMode::DropKey {
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 {
inventory.replace_slot(p.slot_idx as u16, None)
q.inventory.replace_slot(p.slot_idx as u16, None)
} else {
let mut stack = stack.clone();
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
// stack count is > 1
old_slot.as_mut().unwrap().set_count(1);
@ -899,17 +912,17 @@ fn handle_one_packet(
});
}
C2sPlayPacket::KeepAliveC2s(p) => {
if client.got_keepalive {
if q.keepalive_state.got_keepalive {
bail!("unexpected keepalive");
} else if p.id != client.last_keepalive_id {
} else if p.id != q.keepalive_state.last_keepalive_id {
bail!(
"keepalive IDs don't match (expected {}, got {})",
client.last_keepalive_id,
q.keepalive_state.last_keepalive_id,
p.id
);
} else {
client.got_keepalive = true;
client.ping = client.keepalive_sent_time.elapsed().as_millis() as i32;
q.keepalive_state.got_keepalive = true;
q.ping.0 = q.keepalive_state.keepalive_sent_time.elapsed().as_millis() as i32;
}
}
C2sPlayPacket::UpdateDifficultyLockC2s(p) => {
@ -919,23 +932,24 @@ fn handle_one_packet(
});
}
C2sPlayPacket::PositionAndOnGroundC2s(p) => {
if client.pending_teleports != 0 {
if q.teleport_state.pending_teleports != 0 {
return Ok(false);
}
events.1.player_move.send(PlayerMove {
client: entity,
position: p.position.into(),
yaw: client.yaw,
pitch: client.pitch,
on_ground: client.on_ground,
yaw: q.look.yaw,
pitch: q.look.pitch,
on_ground: q.on_ground.0,
});
client.position = p.position.into();
client.on_ground = p.on_ground;
q.position.0 = p.position.into();
q.teleport_state.synced_pos = p.position.into();
q.on_ground.0 = p.on_ground;
}
C2sPlayPacket::FullC2s(p) => {
if client.pending_teleports != 0 {
if q.teleport_state.pending_teleports != 0 {
return Ok(false);
}
@ -947,45 +961,50 @@ fn handle_one_packet(
on_ground: p.on_ground,
});
client.position = p.position.into();
client.yaw = p.yaw;
client.pitch = p.pitch;
client.on_ground = p.on_ground;
q.position.0 = p.position.into();
q.teleport_state.synced_pos = p.position.into();
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;
q.on_ground.0 = p.on_ground;
}
C2sPlayPacket::LookAndOnGroundC2s(p) => {
if client.pending_teleports != 0 {
if q.teleport_state.pending_teleports != 0 {
return Ok(false);
}
events.1.player_move.send(PlayerMove {
client: entity,
position: client.position,
position: q.position.0,
yaw: p.yaw,
pitch: p.pitch,
on_ground: p.on_ground,
});
client.yaw = p.yaw;
client.pitch = p.pitch;
client.on_ground = p.on_ground;
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;
q.on_ground.0 = p.on_ground;
}
C2sPlayPacket::OnGroundOnlyC2s(p) => {
if client.pending_teleports != 0 {
if q.teleport_state.pending_teleports != 0 {
return Ok(false);
}
events.1.player_move.send(PlayerMove {
client: entity,
position: client.position,
yaw: client.yaw,
pitch: client.pitch,
position: q.position.0,
yaw: q.look.yaw,
pitch: q.look.pitch,
on_ground: p.on_ground,
});
client.on_ground = p.on_ground;
q.on_ground.0 = p.on_ground;
}
C2sPlayPacket::VehicleMoveC2s(p) => {
if client.pending_teleports != 0 {
if q.teleport_state.pending_teleports != 0 {
return Ok(false);
}
@ -996,9 +1015,12 @@ fn handle_one_packet(
pitch: p.pitch,
});
client.position = p.position.into();
client.yaw = p.yaw;
client.pitch = p.pitch;
q.position.0 = p.position.into();
q.teleport_state.synced_pos = p.position.into();
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) => {
events.2.boat_paddle_state.send(BoatPaddleState {
@ -1031,7 +1053,7 @@ fn handle_one_packet(
},
C2sPlayPacket::PlayerActionC2s(p) => {
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 {
@ -1058,31 +1080,41 @@ fn handle_one_packet(
})
}
PlayerAction::DropAllItems => {
if let Some(stack) = inventory.replace_slot(client.held_item_slot(), None) {
client.inventory_slots_modified |= 1 << client.held_item_slot();
if let Some(stack) = q
.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 {
client: entity,
from_slot: Some(client.held_item_slot()),
from_slot: Some(q.player_inventory_state.held_item_slot()),
stack,
});
}
}
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 {
inventory.replace_slot(client.held_item_slot(), None)
q.inventory
.replace_slot(q.player_inventory_state.held_item_slot(), None)
} else {
let mut stack = stack.clone();
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
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);
events.2.drop_item_stack.send(DropItemStack {
client: entity,
from_slot: Some(client.held_item_slot()),
from_slot: Some(q.player_inventory_state.held_item_slot()),
stack: old_slot,
});
}
@ -1310,7 +1342,7 @@ fn handle_one_packet(
}
C2sPlayPacket::PlayerInteractBlockC2s(p) => {
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 {
@ -1325,7 +1357,7 @@ fn handle_one_packet(
}
C2sPlayPacket::PlayerInteractItemC2s(p) => {
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 {
@ -1356,7 +1388,7 @@ fn handle_one_packet(
/// not function correctly.
#[allow(clippy::too_many_arguments)]
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 player_move: EventReader<PlayerMove>,
mut start_sneaking: EventReader<StartSneaking>,
@ -1373,33 +1405,20 @@ pub fn default_event_handler(
..
} in update_settings.iter()
{
let Ok((mut client, entity)) = clients.get_mut(*client) else {
continue
};
if let Ok((_, mcentity, mut view_dist)) = clients.get_mut(*client) {
view_dist.set(*view_distance);
client.set_view_distance(*view_distance);
let player = client.player_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);
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);
if let Some(mut entity) = mcentity {
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()
{
let Ok((_, Some(mut entity))) = clients.get_mut(*client) else {
continue
};
entity.set_position(*position);
entity.set_yaw(*yaw);
entity.set_head_yaw(*yaw);
entity.set_pitch(*pitch);
entity.set_on_ground(*on_ground);
if let Ok((_, Some(mut mcentity), _)) = clients.get_mut(*client) {
mcentity.set_position(*position);
mcentity.set_yaw(*yaw);
mcentity.set_head_yaw(*yaw);
mcentity.set_pitch(*pitch);
mcentity.set_on_ground(*on_ground);
}
}
for StartSneaking { client } in start_sneaking.iter() {
let Ok((_, Some(mut entity))) = clients.get_mut(*client) else {
continue
if let Ok((_, Some(mut entity), _)) = clients.get_mut(*client) {
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() {
let Ok((_, Some(mut entity))) = clients.get_mut(*client) else {
continue
if let Ok((_, Some(mut entity), _)) = clients.get_mut(*client) {
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() {
let Ok((_, Some(mut entity))) = clients.get_mut(*client) else {
continue
if let Ok((_, Some(mut entity), _)) = clients.get_mut(*client) {
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() {
let Ok((_, Some(mut entity))) = clients.get_mut(*client) else {
continue
if let Ok((_, Some(mut entity), _)) = clients.get_mut(*client) {
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() {
let Ok((_, Some(mut entity))) = clients.get_mut(*client) else {
continue
if let Ok((_, Some(mut entity), _)) = clients.get_mut(*client) {
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 uuid::Uuid;
use valence_protocol::text::Text;
use valence_protocol::username::Username;
use crate::biome::Biome;
use crate::dimension::Dimension;
@ -307,7 +306,7 @@ pub trait AsyncCallbacks: Send + Sync + 'static {
async fn session_server(
&self,
shared: &SharedServer,
username: Username<&str>,
username: &str,
auth_digest: &str,
player_ip: &IpAddr,
) -> String {

View file

@ -18,10 +18,11 @@ use valence_protocol::packet::s2c::play::{
use valence_protocol::tracked_data::{Facing, PaintingKind, Pose};
use valence_protocol::var_int::VarInt;
use crate::component::Despawned;
use crate::config::DEFAULT_TPS;
use crate::math::Aabb;
use crate::packet::WritePacket;
use crate::{Despawned, NULL_ENTITY};
use crate::util::Aabb;
use crate::NULL_ENTITY;
pub mod data;
@ -29,28 +30,28 @@ include!(concat!(env!("OUT_DIR"), "/entity_event.rs"));
/// A [`Resource`] which maintains information about all the [`McEntity`]
/// components on the server.
#[derive(Resource)]
#[derive(Resource, Debug)]
pub struct McEntityManager {
protocol_id_to_entity: FxHashMap<i32, Entity>,
protocol_id_to_mcentity: FxHashMap<i32, Entity>,
next_protocol_id: i32,
}
impl McEntityManager {
pub(crate) fn new() -> Self {
Self {
protocol_id_to_entity: HashMap::default(),
protocol_id_to_mcentity: HashMap::default(),
next_protocol_id: 1,
}
}
/// Gets the [`Entity`] of the [`McEntity`] with the given protocol ID.
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.
pub(crate) fn init_entities(
/// Sets the protocol ID of new mcentities.
pub(crate) fn init_mcentities(
mut entities: Query<(Entity, &mut McEntity), Added<McEntity>>,
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
.protocol_id_to_entity
.protocol_id_to_mcentity
.insert(mc_entity.protocol_id, entity);
}
}
/// Removes despawned entities from the entity manager.
pub(crate) fn deinit_despawned_entities(
/// Removes despawned mcentities from the mcentity manager.
pub(crate) fn deinit_despawned_mcentities(
entities: Query<&mut McEntity, With<Despawned>>,
mut manager: ResMut<McEntityManager>,
) {
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>>) {
for mut entity in &mut entities {
entity.data.clear_modifications();
entity.old_position = entity.position;
entity.old_instance = entity.instance;
entity.statuses = 0;
entity.animations = 0;
entity.yaw_or_pitch_modified = false;
entity.head_yaw_modified = false;
entity.velocity_modified = false;
pub(crate) fn update_mcentities(mut mcentities: Query<&mut McEntity, Changed<McEntity>>) {
for mut ent in &mut mcentities {
ent.data.clear_modifications();
ent.old_position = ent.position;
ent.old_instance = ent.instance;
ent.statuses = 0;
ent.animations = 0;
ent.yaw_or_pitch_modified = false;
ent.head_yaw_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`].
#[derive(Component)]
pub struct McEntity {
data: TrackedData,
pub(crate) data: TrackedData,
protocol_id: i32,
uuid: Uuid,
/// 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::Packet;
use crate::component::Despawned;
use crate::dimension::DimensionId;
use crate::entity::McEntity;
use crate::packet::{PacketWriter, WritePacket};
use crate::server::{Server, SharedServer};
use crate::view::ChunkPos;
use crate::Despawned;
mod chunk;
mod chunk_entry;

View file

@ -19,8 +19,8 @@ use valence_protocol::Encode;
use crate::biome::BiomeId;
use crate::instance::paletted_container::PalettedContainer;
use crate::instance::InstanceInfo;
use crate::math::bit_width;
use crate::packet::{PacketWriter, WritePacket};
use crate::util::bit_width;
use crate::view::ChunkPos;
/// 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)
} else {
for (sect_y, sect) in &mut self.sections.iter_mut().enumerate() {
if sect.section_updates.len() == 1 {
let packed = sect.section_updates[0].0 as u64;
let offset_y = packed & 0b1111;
let offset_z = (packed >> 4) & 0b1111;
let offset_x = (packed >> 8) & 0b1111;
let block = packed >> 12;
match sect.section_updates.len() {
0 => {}
1 => {
let packed = sect.section_updates[0].0 as u64;
let offset_y = packed & 0b1111;
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_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_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_z = pos.z * 16 + offset_z as i32;
writer.write_packet(&BlockUpdateS2c {
position: BlockPos::new(global_x, global_y, global_z),
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
| (sect_y as i64 + info.min_y.div_euclid(16) as i64) & 0xfffff;
writer.write_packet(&BlockUpdateS2c {
position: BlockPos::new(global_x, global_y, global_z),
block_id: VarInt(block as i32),
})
}
_ => {
let chunk_section_position = (pos.x as i64) << 42
| (pos.z as i64 & 0x3fffff) << 20
| (sect_y as i64 + info.min_y.div_euclid(16) as i64) & 0xfffff;
writer.write_packet(&ChunkDeltaUpdateS2c {
chunk_section_position,
invert_trust_edges: false,
blocks: Cow::Borrowed(&sect.section_updates),
});
writer.write_packet(&ChunkDeltaUpdateS2c {
chunk_section_position,
invert_trust_edges: false,
blocks: Cow::Borrowed(&sect.section_updates),
});
}
}
}
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::Encode;
use crate::math::bit_width;
use crate::util::bit_width;
/// `HALF_LEN` must be equal to `ceil(LEN / 2)`.
#[derive(Clone, Debug)]

View file

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

View file

@ -20,11 +20,7 @@
unused_import_braces,
clippy::dbg_macro
)]
#![allow(
clippy::derive_partial_eq_without_eq,
clippy::unusual_byte_groupings,
clippy::comparison_chain
)]
#![allow(clippy::type_complexity)] // ECS queries are often complicated.
use bevy_ecs::prelude::*;
pub use {
@ -33,18 +29,19 @@ pub use {
pub mod biome;
pub mod client;
pub mod component;
pub mod config;
pub mod dimension;
pub mod entity;
pub mod instance;
pub mod inventory;
pub mod math;
mod packet;
pub mod packet;
pub mod player_list;
pub mod player_textures;
pub mod server;
#[cfg(any(test, doctest))]
mod unit_test;
pub mod util;
pub mod view;
pub mod prelude {
@ -52,7 +49,8 @@ pub mod prelude {
pub use bevy_app::prelude::*;
pub use bevy_ecs::prelude::*;
pub use biome::{Biome, BiomeId};
pub use client::Client;
pub use client::*;
pub use component::*;
pub use config::{
AsyncCallbacks, ConnectionMode, PlayerSampleEntry, ServerListPing, ServerPlugin,
};
@ -68,8 +66,6 @@ pub mod prelude {
pub use protocol::ident::Ident;
pub use protocol::item::{ItemKind, ItemStack};
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 uuid::Uuid;
pub use valence_nbt::Compound;
@ -82,21 +78,6 @@ pub mod prelude {
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,
/// immediately despawning it, and then stealing its [`Entity`] ID. The user
/// 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::Packet;
/// Types that can have packets written to them.
pub(crate) trait WritePacket {
fn write_packet<'a, P>(&mut self, packet: &P)
where
P: Packet<'a>;
/// Writes a packet to this object. Encoding errors are typically logged and
/// discarded.
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]);
}
impl<W: WritePacket> WritePacket for &mut W {
fn write_packet<'a, P>(&mut self, packet: &P)
where
P: Packet<'a>,
{
fn write_packet<'a>(&mut self, packet: &impl Packet<'a>) {
(*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> {
buf: &'a mut Vec<u8>,
threshold: Option<u32>,
@ -42,10 +42,7 @@ impl<'a> PacketWriter<'a> {
}
impl WritePacket for PacketWriter<'_> {
fn write_packet<'a, P>(&mut self, pkt: &P)
where
P: Packet<'a>,
{
fn write_packet<'a>(&mut self, pkt: &impl Packet<'a>) {
let res = if let Some(threshold) = self.threshold {
encode_packet_compressed(self.buf, pkt, threshold, self.scratch)
} else {
@ -65,10 +62,7 @@ impl WritePacket for PacketWriter<'_> {
}
impl WritePacket for PacketEncoder {
fn write_packet<'a, P>(&mut self, packet: &P)
where
P: Packet<'a>,
{
fn write_packet<'a>(&mut self, packet: &impl Packet<'a>) {
if let Err(e) = self.append_packet(packet) {
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::text::Text;
use valence_protocol::types::{GameMode, Property};
use valence_protocol::types::Property;
use crate::client::Client;
use crate::component::{GameMode, Ping, Properties, UniqueId, Username};
use crate::packet::{PacketWriter, WritePacket};
use crate::server::Server;
@ -55,27 +56,28 @@ impl PlayerList {
/// When clients disconnect, they are removed from the player list.
pub fn default_systems() -> SystemConfigs {
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>,
) {
for client in &clients {
for (username, properties, game_mode, ping, uuid) in &clients {
let entry = PlayerListEntry::new()
.with_username(client.username())
.with_properties(client.properties())
.with_game_mode(client.game_mode())
.with_ping(client.ping());
.with_username(&username.0)
.with_properties(properties.0.clone())
.with_game_mode(*game_mode)
.with_ping(ping.0);
player_list.insert(client.uuid(), entry);
player_list.insert(uuid.0, entry);
}
}
fn remove_disconnected_clients_from_player_list(
clients: Query<&mut Client>,
mut clients: RemovedComponents<Client>,
uuids: Query<&UniqueId>,
mut player_list: ResMut<PlayerList>,
) {
for client in &clients {
if client.is_disconnected() {
player_list.remove(client.uuid());
for entity in clients.iter() {
if let Ok(UniqueId(uuid)) = uuids.get(entity) {
player_list.remove(*uuid);
}
}
}
@ -238,7 +240,7 @@ impl PlayerList {
chat_data: None,
listed: entry.listed,
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()),
})
})
@ -271,7 +273,7 @@ impl PlayerList {
/// ```
#[derive(Clone, Debug)]
pub struct PlayerListEntry {
username: String, // TODO: Username<String>?
username: String,
properties: Vec<Property>,
game_mode: GameMode,
old_game_mode: GameMode,
@ -609,7 +611,7 @@ pub(crate) fn update_player_list(
chat_data: None,
listed: entry.listed,
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()),
};
@ -650,7 +652,7 @@ pub(crate) fn update_player_list(
chat_data: None,
listed: entry.listed,
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()),
}]),
});
@ -676,7 +678,7 @@ pub(crate) fn update_player_list(
}
for mut client in &mut clients {
if client.is_new() {
if client.is_added() {
pl.write_init_packets(client.into_inner());
} else {
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_protocol::ident;
use valence_protocol::types::Property;
use valence_protocol::username::Username;
use crate::biome::{validate_biomes, Biome, BiomeId};
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::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::{
check_instance_invariants, update_instances_post_client, update_instances_pre_client, Instance,
};
use crate::inventory::update_inventories;
use crate::player_list::{update_player_list, PlayerList};
use crate::prelude::{Inventory, InventoryKind};
use crate::server::connect::do_accept_loop;
use crate::Despawned;
mod byte_channel;
mod connect;
@ -92,9 +92,9 @@ struct SharedServerInner {
/// Sent to all clients when joining.
registry_codec: Compound,
/// 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.
new_clients_recv: Receiver<Client>,
new_clients_recv: Receiver<ClientBundle>,
/// A semaphore used to limit the number of simultaneous connections to the
/// server. Closing this semaphore stops new connections.
connection_sema: Arc<Semaphore>,
@ -202,7 +202,7 @@ impl SharedServer {
#[non_exhaustive]
pub struct NewClientInfo {
/// The username of the new client.
pub username: Username<String>,
pub username: String,
/// The UUID of the new client.
pub uuid: Uuid,
/// The remote address of the new client.
@ -298,7 +298,7 @@ pub fn build_plugin(
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`.
app.add_systems(
(
init_entities,
init_mcentities,
check_instance_invariants,
update_player_list.before(update_instances_pre_client),
update_instances_pre_client.after(init_entities),
update_clients.after(update_instances_pre_client),
update_instances_post_client.after(update_clients),
deinit_despawned_entities.after(update_instances_post_client),
despawn_marked_entities.after(deinit_despawned_entities),
update_entities.after(despawn_marked_entities),
update_instances_pre_client.after(init_mcentities),
update_instances_post_client.after(update_instances_pre_client),
deinit_despawned_mcentities.after(update_instances_post_client),
despawn_marked_entities.after(deinit_despawned_mcentities),
update_mcentities.after(despawn_marked_entities),
OldPosition::update.after(despawn_marked_entities),
OldLocation::update.after(despawn_marked_entities),
)
.in_base_set(CoreSet::PostUpdate),
)
.add_systems(
update_inventories()
.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));
@ -367,7 +374,7 @@ pub fn build_plugin(
#[derive(ScheduleLabel, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
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)]
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::text::Text;
use valence_protocol::types::Property;
use valence_protocol::username::Username;
use valence_protocol::var_int::VarInt;
use valence_protocol::{translation_key, Decode, MINECRAFT_VERSION, PROTOCOL_VERSION};
use crate::config::{AsyncCallbacks, ConnectionMode, ServerListPing};
use crate::server::connection::InitialConnection;
use crate::server::{NewClientInfo, SharedServer};
use crate::util::is_valid_username;
/// Accepts new connections to the server as they occur.
#[instrument(skip_all)]
@ -150,7 +150,7 @@ async fn handle_handshake(
.context("error handling login")?
{
Some(info) => {
let client = conn.into_client(
let client = conn.into_client_bundle(
info,
shared.0.incoming_capacity,
shared.0.outgoing_capacity,
@ -238,7 +238,9 @@ async fn handle_login(
profile_id: _, // TODO
} = 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() {
ConnectionMode::Online { .. } => {
@ -269,7 +271,7 @@ async fn handle_login(
conn.send_packet(&LoginSuccessS2c {
uuid: info.uuid,
username: info.username.as_str_username(),
username: &info.username,
properties: Default::default(),
})
.await?;
@ -283,7 +285,7 @@ pub(super) async fn login_online(
callbacks: &Arc<impl AsyncCallbacks>,
conn: &mut InitialConnection<OwnedReadHalf, OwnedWriteHalf>,
remote_addr: SocketAddr,
username: Username<String>,
username: String,
) -> anyhow::Result<NewClientInfo> {
let my_verify_token: [u8; 16] = rand::random();
@ -331,7 +333,7 @@ pub(super) async fn login_online(
let url = callbacks
.session_server(
shared,
username.as_str_username(),
username.as_str(),
&auth_digest(&hash),
&remote_addr.ip(),
)
@ -360,12 +362,17 @@ pub(super) async fn login_online(
#[derive(Debug, Deserialize)]
struct GameProfile {
id: Uuid,
name: Username<String>,
name: String,
properties: Vec<Property>,
}
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");
Ok(NewClientInfo {
@ -383,7 +390,7 @@ fn auth_digest(bytes: &[u8]) -> String {
/// Login procedure for offline mode.
pub(super) fn login_offline(
remote_addr: SocketAddr,
username: Username<String>,
username: String,
) -> anyhow::Result<NewClientInfo> {
Ok(NewClientInfo {
// Derive the client's UUID from a hash of their username.
@ -397,7 +404,7 @@ pub(super) fn login_offline(
/// Login procedure for BungeeCord.
pub(super) fn login_bungeecord(
server_address: &str,
username: Username<String>,
username: String,
) -> anyhow::Result<NewClientInfo> {
// Get data from server_address field of the handshake
let [_, client_ip, uuid, properties]: [&str; 4] = server_address
@ -422,7 +429,7 @@ pub(super) fn login_bungeecord(
/// Login procedure for Velocity.
pub(super) async fn login_velocity(
conn: &mut InitialConnection<OwnedReadHalf, OwnedWriteHalf>,
username: Username<String>,
username: String,
velocity_secret: &str,
) -> anyhow::Result<NewClientInfo> {
const VELOCITY_MIN_SUPPORTED_VERSION: u8 = 1;
@ -473,7 +480,7 @@ pub(super) async fn login_velocity(
// Get username and validate
ensure!(
username == Username::decode(&mut data_without_signature)?,
username == <&str>::decode(&mut data_without_signature)?,
"mismatched usernames"
);

View file

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

View file

@ -57,6 +57,7 @@ mod tests {
use super::*;
use crate::client::Client;
use crate::component::Position;
use crate::inventory::{Inventory, InventoryKind, OpenInventory};
use crate::{assert_packet_count, assert_packet_order};
@ -90,8 +91,8 @@ mod tests {
app.update();
// Make assertions
let client: &Client = app.world.get(client_ent).expect("client not found");
assert_eq!(client.position(), [12.0, 64.0, 0.0].into());
let pos = app.world.get::<Position>(client_ent).unwrap();
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.

View file

@ -1,37 +1,36 @@
use std::sync::{Arc, Mutex};
use bevy_app::{App, CoreSchedule};
use bevy_ecs::prelude::Entity;
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_ecs::schedule::{LogLevel, ScheduleBuildSettings};
use bytes::BytesMut;
use valence_protocol::codec::{PacketDecoder, PacketEncoder};
use valence_protocol::packet::S2cPlayPacket;
use valence_protocol::username::Username;
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::dimension::DimensionId;
use crate::inventory::{Inventory, InventoryKind};
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
/// 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 enc = PacketEncoder::new();
let dec = PacketDecoder::new();
let client = Client::new(client_info, Box::new(mock_connection.clone()), enc, dec);
(client, MockClientHelper::new(mock_connection))
let bundle = ClientBundle::new(client_info, Box::new(mock_connection.clone()), enc, dec);
(bundle, MockClientHelper::new(mock_connection))
}
/// Creates a `NewClientInfo` with the given username and a random UUID.
/// Panics if the username is invalid.
pub fn gen_client_info(username: &str) -> NewClientInfo {
pub fn gen_client_info(username: impl Into<String>) -> NewClientInfo {
NewClientInfo {
username: Username::new(username.to_owned()).unwrap(),
username: username.into(),
uuid: uuid::Uuid::new_v4(),
ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
properties: vec![],
@ -169,15 +168,12 @@ pub fn scenario_single_client(app: &mut App) -> (Entity, MockClientHelper) {
let server = app.world.resource::<Server>();
let instance = server.new_instance(DimensionId::default());
let instance_ent = app.world.spawn(instance).id();
let info = gen_client_info("test");
let (mut client, client_helper) = create_mock_client(info);
let (client, client_helper) = create_mock_client(gen_client_info("test"));
// HACK: needed so client does not get disconnected on first update
client.set_instance(instance_ent);
let client_ent = app
.world
.spawn((client, Inventory::new(InventoryKind::Player)))
.id();
let client_ent = app.world.spawn(client).id();
// Set initial location.
app.world.get_mut::<Location>(client_ent).unwrap().0 = instance_ent;
// Print warnings if there are ambiguities in the schedule.
app.edit_schedule(CoreSchedule::Main, |schedule| {
@ -209,10 +205,15 @@ macro_rules! assert_packet_count {
assert_eq!(
count,
$count,
"expected {} {} packets, got {}",
"expected {} {} packets, got {}\nPackets actually found:\n[\n\t{}\n]\n",
$count,
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 _
}
/// 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)]
mod tests {
use approx::assert_relative_eq;

View file

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

View file

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

View file

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

View file

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

View file

@ -3,12 +3,11 @@ use std::borrow::Cow;
use uuid::Uuid;
use crate::types::Property;
use crate::username::Username;
use crate::{Decode, Encode};
#[derive(Clone, Debug, Encode, Decode)]
pub struct LoginSuccessS2c<'a> {
pub uuid: Uuid,
pub username: Username<&'a str>,
pub username: &'a str, // TODO: bound this.
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,
};
use valence_protocol::packet::{C2sHandshakePacket, S2cLoginPacket, S2cPlayPacket};
use valence_protocol::username::Username;
use valence_protocol::var_int::VarInt;
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(&LoginHelloC2s {
username: Username::new(sess_name).unwrap(),
username: sess_name,
profile_id: Some(Uuid::new_v4()),
})?;