aboutsummaryrefslogtreecommitdiff
path: root/azalea-client
diff options
context:
space:
mode:
Diffstat (limited to 'azalea-client')
-rw-r--r--azalea-client/Cargo.toml1
-rw-r--r--azalea-client/src/client.rs127
-rw-r--r--azalea-client/src/interact.rs200
-rw-r--r--azalea-client/src/inventory.rs721
-rw-r--r--azalea-client/src/lib.rs3
-rw-r--r--azalea-client/src/local_player.rs39
-rw-r--r--azalea-client/src/movement.rs49
-rw-r--r--azalea-client/src/packet_handling.rs171
-rwxr-xr-xazalea-client/src/player.rs7
9 files changed, 1248 insertions, 70 deletions
diff --git a/azalea-client/Cargo.toml b/azalea-client/Cargo.toml
index 6bc07def..2a80467a 100644
--- a/azalea-client/Cargo.toml
+++ b/azalea-client/Cargo.toml
@@ -25,6 +25,7 @@ bevy_ecs = "0.10.0"
bevy_log = "0.10.0"
bevy_tasks = "0.10.0"
bevy_time = "0.10.0"
+azalea-inventory = { path = "../azalea-inventory", version = "0.1.0" }
derive_more = { version = "0.99.17", features = ["deref", "deref_mut"] }
futures = "0.3.25"
log = "0.4.17"
diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs
index 47cc7235..7a4285e6 100644
--- a/azalea-client/src/client.rs
+++ b/azalea-client/src/client.rs
@@ -2,11 +2,13 @@ use crate::{
chat::ChatPlugin,
disconnect::{DisconnectEvent, DisconnectPlugin},
events::{Event, EventPlugin, LocalPlayerEvents},
+ interact::{CurrentSequenceNumber, InteractPlugin},
+ inventory::{InventoryComponent, InventoryPlugin},
local_player::{
death_event, handle_send_packet_event, update_in_loaded_chunk, GameProfileComponent,
LocalPlayer, PhysicsState, SendPacketEvent,
},
- movement::PlayerMovePlugin,
+ movement::{LastSentLookDirection, PlayerMovePlugin},
packet_handling::{self, PacketHandlerPlugin, PacketReceiver},
player::retroactively_add_game_profile_component,
task_pool::TaskPoolPlugin,
@@ -15,11 +17,13 @@ use crate::{
use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError};
use azalea_chat::FormattedText;
+use azalea_core::Vec3;
use azalea_physics::{PhysicsPlugin, PhysicsSet};
use azalea_protocol::{
connect::{Connection, ConnectionError},
packets::{
game::{
+ clientbound_player_abilities_packet::ClientboundPlayerAbilitiesPacket,
serverbound_client_information_packet::ServerboundClientInformationPacket,
ClientboundGamePacket, ServerboundGamePacket,
},
@@ -37,16 +41,17 @@ use azalea_protocol::{
resolver, ServerAddress,
};
use azalea_world::{
- entity::{EntityPlugin, EntityUpdateSet, Local, WorldName},
+ entity::{EntityPlugin, EntityUpdateSet, Local, Position, WorldName},
Instance, InstanceContainer, PartialInstance,
};
-use bevy_app::{App, CoreSchedule, Plugin, PluginGroup, PluginGroupBuilder};
+use bevy_app::{App, CoreSchedule, IntoSystemAppConfig, Plugin, PluginGroup, PluginGroupBuilder};
use bevy_ecs::{
bundle::Bundle,
component::Component,
entity::Entity,
schedule::IntoSystemConfig,
schedule::{LogLevel, ScheduleBuildSettings, ScheduleLabel},
+ system::{ResMut, Resource},
world::World,
};
use bevy_log::LogPlugin;
@@ -56,7 +61,10 @@ use log::{debug, error};
use parking_lot::{Mutex, RwLock};
use std::{collections::HashMap, fmt::Debug, io, net::SocketAddr, sync::Arc, time::Duration};
use thiserror::Error;
-use tokio::{sync::mpsc, time};
+use tokio::{
+ sync::{broadcast, mpsc},
+ time,
+};
use uuid::Uuid;
/// `Client` has the things that a user interacting with the library will want.
@@ -93,11 +101,50 @@ pub struct Client {
}
/// A component that contains some of the "settings" for this client that are
-/// sent to the server, such as render distance.
+/// sent to the server, such as render distance. This is only present on local
+/// players.
pub type ClientInformation = ServerboundClientInformationPacket;
+/// A component that contains the abilities the player has, like flying
+/// or instantly breaking blocks. This is only present on local players.
+#[derive(Clone, Debug, Component, Default)]
+pub struct PlayerAbilities {
+ pub invulnerable: bool,
+ pub flying: bool,
+ pub can_fly: bool,
+ /// Whether the player can instantly break blocks and can duplicate blocks
+ /// in their inventory.
+ pub instant_break: bool,
+
+ pub flying_speed: f32,
+ /// Used for the fov
+ pub walking_speed: f32,
+}
+impl From<ClientboundPlayerAbilitiesPacket> for PlayerAbilities {
+ fn from(packet: ClientboundPlayerAbilitiesPacket) -> Self {
+ Self {
+ invulnerable: packet.flags.invulnerable,
+ flying: packet.flags.flying,
+ can_fly: packet.flags.can_fly,
+ instant_break: packet.flags.instant_break,
+ flying_speed: packet.flying_speed,
+ walking_speed: packet.walking_speed,
+ }
+ }
+}
+
/// A component that contains a map of player UUIDs to their information in the
-/// tab list
+/// tab list.
+///
+/// ```
+/// # use azalea_client::TabList;
+/// # fn example(client: &azalea_client::Client) {
+/// let tab_list = client.component::<TabList>();
+/// println!("Online players:");
+/// for (uuid, player_info) in tab_list.iter() {
+/// println!("- {} ({}ms)", player_info.profile.name, player_info.latency);
+/// }
+/// # }
#[derive(Component, Clone, Debug, Deref, DerefMut, Default)]
pub struct TabList(HashMap<Uuid, PlayerInfo>);
@@ -246,8 +293,12 @@ impl Client {
game_profile: GameProfileComponent(game_profile),
physics_state: PhysicsState::default(),
local_player_events: LocalPlayerEvents(tx),
+ inventory: InventoryComponent::default(),
client_information: ClientInformation::default(),
tab_list: TabList::default(),
+ current_sequence_number: CurrentSequenceNumber::default(),
+ last_sent_direction: LastSentLookDirection::default(),
+ abilities: PlayerAbilities::default(),
_local: Local,
});
@@ -421,6 +472,11 @@ impl Client {
self.query::<&T>(&mut self.ecs.lock()).clone()
}
+ /// Get a component from this client, or `None` if it doesn't exist.
+ pub fn get_component<T: Component + Clone>(&self) -> Option<T> {
+ self.query::<Option<&T>>(&mut self.ecs.lock()).cloned()
+ }
+
/// Get a reference to our (potentially shared) world.
///
/// This gets the [`Instance`] from our world container. If it's a normal
@@ -430,8 +486,8 @@ impl Client {
pub fn world(&self) -> Arc<RwLock<Instance>> {
let world_name = self.component::<WorldName>();
let ecs = self.ecs.lock();
- let world_container = ecs.resource::<InstanceContainer>();
- world_container.get(&world_name).unwrap()
+ let instance_container = ecs.resource::<InstanceContainer>();
+ instance_container.get(&world_name).unwrap()
}
/// Returns whether we have a received the login packet yet.
@@ -478,6 +534,15 @@ impl Client {
}
}
+impl Client {
+ /// Get the position of this client.
+ ///
+ /// This is a shortcut for `Vec3::from(&bot.component::<Position>())`.
+ pub fn position(&self) -> Vec3 {
+ Vec3::from(&self.component::<Position>())
+ }
+}
+
/// A bundle for the components that are present on a local player that received
/// a login packet. If you want to filter for this, just use [`Local`].
#[derive(Bundle)]
@@ -487,8 +552,12 @@ pub struct JoinedClientBundle {
pub game_profile: GameProfileComponent,
pub physics_state: PhysicsState,
pub local_player_events: LocalPlayerEvents,
+ pub inventory: InventoryComponent,
pub client_information: ClientInformation,
pub tab_list: TabList,
+ pub current_sequence_number: CurrentSequenceNumber,
+ pub last_sent_direction: LastSentLookDirection,
+ pub abilities: PlayerAbilities,
pub _local: Local,
}
@@ -498,11 +567,7 @@ impl Plugin for AzaleaPlugin {
// Minecraft ticks happen every 50ms
app.insert_resource(FixedTime::new(Duration::from_millis(50)));
- app.add_system(
- update_in_loaded_chunk
- .after(PhysicsSet)
- .after(handle_send_packet_event),
- );
+ app.add_system(update_in_loaded_chunk.after(PhysicsSet));
// fire the Death event when the player dies.
app.add_system(death_event);
@@ -599,6 +664,39 @@ pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::UnboundedSender<(
}
}
+/// A resource that contains a [`broadcast::Sender`] that will be sent every
+/// Minecraft tick.
+///
+/// This is useful for running code every schedule from async user code.
+///
+/// ```
+/// use azalea_client::TickBroadcast;
+/// # async fn example(client: azalea_client::Client) {
+/// let mut receiver = {
+/// let ecs = client.ecs.lock();
+/// let tick_broadcast = ecs.resource::<TickBroadcast>();
+/// tick_broadcast.subscribe()
+/// };
+/// while receiver.recv().await.is_ok() {
+/// // do something
+/// }
+/// # }
+/// ```
+#[derive(Resource, Deref)]
+pub struct TickBroadcast(broadcast::Sender<()>);
+
+fn send_tick_broadcast(tick_broadcast: ResMut<TickBroadcast>) {
+ let _ = tick_broadcast.0.send(());
+}
+/// A plugin that makes the [`RanScheduleBroadcast`] resource available.
+pub struct TickBroadcastPlugin;
+impl Plugin for TickBroadcastPlugin {
+ fn build(&self, app: &mut App) {
+ app.insert_resource(TickBroadcast(broadcast::channel(1).0))
+ .add_system(send_tick_broadcast.in_schedule(CoreSchedule::FixedUpdate));
+ }
+}
+
/// This plugin group will add all the default plugins necessary for Azalea to
/// work.
pub struct DefaultPlugins;
@@ -614,8 +712,11 @@ impl PluginGroup for DefaultPlugins {
.add(PhysicsPlugin)
.add(EventPlugin)
.add(TaskPoolPlugin::default())
+ .add(InventoryPlugin)
.add(ChatPlugin)
.add(DisconnectPlugin)
.add(PlayerMovePlugin)
+ .add(InteractPlugin)
+ .add(TickBroadcastPlugin)
}
}
diff --git a/azalea-client/src/interact.rs b/azalea-client/src/interact.rs
new file mode 100644
index 00000000..ec5ed87b
--- /dev/null
+++ b/azalea-client/src/interact.rs
@@ -0,0 +1,200 @@
+use azalea_core::{BlockHitResult, BlockPos, Direction, GameMode, Vec3};
+use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType};
+use azalea_protocol::packets::game::{
+ serverbound_interact_packet::InteractionHand,
+ serverbound_use_item_on_packet::{BlockHit, ServerboundUseItemOnPacket},
+};
+use azalea_world::{
+ entity::{clamp_look_direction, view_vector, EyeHeight, LookDirection, Position, WorldName},
+ InstanceContainer,
+};
+use bevy_app::{App, Plugin};
+use bevy_ecs::{
+ component::Component,
+ entity::Entity,
+ event::EventReader,
+ schedule::{IntoSystemConfig, IntoSystemConfigs},
+ system::{Commands, Query, Res},
+};
+use derive_more::{Deref, DerefMut};
+use log::warn;
+
+use crate::{
+ local_player::{handle_send_packet_event, LocalGameMode},
+ Client, LocalPlayer,
+};
+
+/// A plugin that allows clients to interact with blocks in the world.
+pub struct InteractPlugin;
+impl Plugin for InteractPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_event::<BlockInteractEvent>().add_systems(
+ (
+ update_hit_result_component.after(clamp_look_direction),
+ handle_block_interact_event,
+ )
+ .before(handle_send_packet_event)
+ .chain(),
+ );
+ }
+}
+
+impl Client {
+ /// Right click a block. The behavior of this depends on the target block,
+ /// and it'll either place the block you're holding in your hand or use the
+ /// block you clicked (like toggling a lever).
+ ///
+ /// Note that this may trigger anticheats as it doesn't take into account
+ /// whether you're actually looking at the block.
+ pub fn block_interact(&mut self, position: BlockPos) {
+ self.ecs.lock().send_event(BlockInteractEvent {
+ entity: self.entity,
+ position,
+ });
+ }
+}
+
+/// Right click a block. The behavior of this depends on the target block,
+/// and it'll either place the block you're holding in your hand or use the
+/// block you clicked (like toggling a lever).
+pub struct BlockInteractEvent {
+ /// The local player entity that's opening the container.
+ pub entity: Entity,
+ /// The coordinates of the container.
+ pub position: BlockPos,
+}
+
+/// A component that contains the number of changes this client has made to
+/// blocks.
+#[derive(Component, Copy, Clone, Debug, Default, Deref, DerefMut)]
+pub struct CurrentSequenceNumber(u32);
+
+/// A component that contains the block that the player is currently looking at.
+#[derive(Component, Clone, Debug, Deref, DerefMut)]
+pub struct HitResultComponent(BlockHitResult);
+
+fn handle_block_interact_event(
+ mut events: EventReader<BlockInteractEvent>,
+ mut query: Query<(
+ &LocalPlayer,
+ &mut CurrentSequenceNumber,
+ &HitResultComponent,
+ )>,
+) {
+ for event in events.iter() {
+ let Ok((local_player, mut sequence_number, hit_result)) = query.get_mut(event.entity) else {
+ warn!("Sent BlockInteractEvent for entity that isn't LocalPlayer");
+ continue;
+ };
+
+ // TODO: check to make sure we're within the world border
+
+ **sequence_number += 1;
+
+ // minecraft also does the interaction client-side (so it looks like clicking a
+ // button is instant) but we don't really need that
+
+ // the block_hit data will depend on whether we're looking at the block and
+ // whether we can reach it
+
+ let block_hit = if hit_result.block_pos == event.position {
+ // we're looking at the block :)
+ BlockHit {
+ block_pos: hit_result.block_pos,
+ direction: hit_result.direction,
+ location: hit_result.location,
+ inside: hit_result.inside,
+ }
+ } else {
+ // we're not looking at the block, so make up some numbers
+ BlockHit {
+ block_pos: event.position,
+ direction: Direction::Up,
+ location: event.position.center(),
+ inside: false,
+ }
+ };
+
+ local_player.write_packet(
+ ServerboundUseItemOnPacket {
+ hand: InteractionHand::MainHand,
+ block_hit,
+ sequence: sequence_number.0,
+ }
+ .get(),
+ )
+ }
+}
+
+#[allow(clippy::type_complexity)]
+fn update_hit_result_component(
+ mut commands: Commands,
+ mut query: Query<(
+ Entity,
+ Option<&mut HitResultComponent>,
+ &LocalGameMode,
+ &Position,
+ &EyeHeight,
+ &LookDirection,
+ &WorldName,
+ )>,
+ instance_container: Res<InstanceContainer>,
+) {
+ for (entity, hit_result_ref, game_mode, position, eye_height, look_direction, world_name) in
+ &mut query
+ {
+ let pick_range = if game_mode.current == GameMode::Creative {
+ 6.
+ } else {
+ 4.5
+ };
+ let eye_position = Vec3 {
+ x: position.x,
+ y: position.y + **eye_height as f64,
+ z: position.z,
+ };
+ let hit_result = pick(
+ look_direction,
+ &eye_position,
+ world_name,
+ &instance_container,
+ pick_range,
+ );
+ if let Some(mut hit_result_ref) = hit_result_ref {
+ **hit_result_ref = hit_result;
+ } else {
+ commands
+ .entity(entity)
+ .insert(HitResultComponent(hit_result));
+ }
+ }
+}
+
+/// Get the block that a player would be looking at if their eyes were at the
+/// given direction and position.
+///
+/// If you need to get the block the player is looking at right now, use
+/// [`HitResultComponent`].
+pub fn pick(
+ look_direction: &LookDirection,
+ eye_position: &Vec3,
+ world_name: &WorldName,
+ instance_container: &InstanceContainer,
+ pick_range: f64,
+) -> BlockHitResult {
+ let view_vector = view_vector(look_direction);
+ let end_position = eye_position + &(view_vector * pick_range);
+ let instance_lock = instance_container
+ .get(world_name)
+ .expect("entities must always be in a valid world");
+ let instance = instance_lock.read();
+ azalea_physics::clip::clip(
+ &instance.chunks,
+ ClipContext {
+ from: *eye_position,
+ to: end_position,
+ block_shape_type: BlockShapeType::Outline,
+ fluid_pick_type: FluidPickType::None,
+ },
+ )
+}
diff --git a/azalea-client/src/inventory.rs b/azalea-client/src/inventory.rs
new file mode 100644
index 00000000..d6f909a7
--- /dev/null
+++ b/azalea-client/src/inventory.rs
@@ -0,0 +1,721 @@
+use std::collections::{HashMap, HashSet};
+
+use azalea_chat::FormattedText;
+pub use azalea_inventory::*;
+use azalea_inventory::{
+ item::MaxStackSizeExt,
+ operations::{
+ ClickOperation, CloneClick, PickupAllClick, PickupClick, QuickCraftKind, QuickCraftStatus,
+ QuickCraftStatusKind, QuickMoveClick, ThrowClick,
+ },
+};
+use azalea_protocol::packets::game::{
+ serverbound_container_click_packet::ServerboundContainerClickPacket,
+ serverbound_container_close_packet::ServerboundContainerClosePacket,
+};
+use azalea_registry::MenuKind;
+use bevy_app::{App, Plugin};
+use bevy_ecs::{
+ component::Component,
+ entity::Entity,
+ event::EventReader,
+ prelude::EventWriter,
+ schedule::{IntoSystemConfig, IntoSystemConfigs},
+ system::Query,
+};
+use log::warn;
+
+use crate::{client::PlayerAbilities, local_player::handle_send_packet_event, Client, LocalPlayer};
+
+pub struct InventoryPlugin;
+impl Plugin for InventoryPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_event::<ClientSideCloseContainerEvent>()
+ .add_event::<MenuOpenedEvent>()
+ .add_event::<CloseContainerEvent>()
+ .add_event::<ContainerClickEvent>()
+ .add_event::<SetContainerContentEvent>()
+ .add_systems(
+ (
+ handle_menu_opened_event,
+ handle_set_container_content_event,
+ handle_container_click_event,
+ handle_container_close_event.before(handle_send_packet_event),
+ handle_client_side_close_container_event,
+ )
+ .chain(),
+ );
+ }
+}
+
+impl Client {
+ /// Return the menu that is currently open. If no menu is open, this will
+ /// have the player's inventory.
+ pub fn menu(&self) -> Menu {
+ let mut ecs = self.ecs.lock();
+ let inventory = self.query::<&InventoryComponent>(&mut ecs);
+ inventory.menu().clone()
+ }
+}
+
+/// A component present on all local players that have an inventory.
+#[derive(Component, Debug)]
+pub struct InventoryComponent {
+ /// A component that contains the player's inventory menu. This is
+ /// guaranteed to be a `Menu::Player`.
+ ///
+ /// We keep it as a [`Menu`] since `Menu` has some useful functions that
+ /// bare [`azalea_inventory::Player`] doesn't have.
+ pub inventory_menu: azalea_inventory::Menu,
+
+ /// The ID of the container that's currently open. Its value is not
+ /// guaranteed to be anything specific, and may change every time you open a
+ /// container (unless it's 0, in which case it means that no container is
+ /// open).
+ pub id: u8,
+ /// The current container menu that the player has open. If no container is
+ /// open, this will be `None`.
+ pub container_menu: Option<azalea_inventory::Menu>,
+ /// The item that is currently held by the cursor. `Slot::Empty` if nothing
+ /// is currently being held.
+ pub carried: ItemSlot,
+ /// An identifier used by the server to track client inventory desyncs. This
+ /// is sent on every container click, and it's only ever updated when the
+ /// server sends a new container update.
+ pub state_id: u32,
+
+ pub quick_craft_status: QuickCraftStatusKind,
+ pub quick_craft_kind: QuickCraftKind,
+ /// A set of the indexes of the slots that have been right clicked in
+ /// this "quick craft".
+ pub quick_craft_slots: HashSet<u16>,
+ // minecraft also has these fields, but i don't
+ // think they're necessary?:
+ // private final NonNullList<ItemStack>
+ // remoteSlots;
+ // private final IntList remoteDataSlots;
+ // private ItemStack remoteCarried;
+}
+impl InventoryComponent {
+ /// Returns a reference to the currently active menu. If a container is open
+ /// it'll return [`Self::container_menu`], otherwise
+ /// [`Self::inventory_menu`].
+ ///
+ /// Use [`Self::menu_mut`] if you need a mutable reference.
+ pub fn menu(&self) -> &azalea_inventory::Menu {
+ if let Some(menu) = &self.container_menu {
+ menu
+ } else {
+ &self.inventory_menu
+ }
+ }
+
+ /// Returns a mutable reference to the currently active menu. If a container
+ /// is open it'll return [`Self::container_menu`], otherwise
+ /// [`Self::inventory_menu`].
+ ///
+ /// Use [`Self::menu`] if you don't need a mutable reference.
+ pub fn menu_mut(&mut self) -> &mut azalea_inventory::Menu {
+ if let Some(menu) = &mut self.container_menu {
+ menu
+ } else {
+ &mut self.inventory_menu
+ }
+ }
+
+ /// Modify the inventory as if the given operation was performed on it.
+ pub fn simulate_click(
+ &mut self,
+ operation: &ClickOperation,
+ player_abilities: &PlayerAbilities,
+ ) {
+ if let ClickOperation::QuickCraft(quick_craft) = operation {
+ let last_quick_craft_status_tmp = self.quick_craft_status.clone();
+ self.quick_craft_status = last_quick_craft_status_tmp.clone();
+ let last_quick_craft_status = last_quick_craft_status_tmp;
+
+ // no carried item, reset
+ if self.carried.is_empty() {
+ return self.reset_quick_craft();
+ }
+ // if we were starting or ending, or now we aren't ending and the status
+ // changed, reset
+ if (last_quick_craft_status == QuickCraftStatusKind::Start
+ || last_quick_craft_status == QuickCraftStatusKind::End
+ || self.quick_craft_status != QuickCraftStatusKind::End)
+ && (self.quick_craft_status != last_quick_craft_status)
+ {
+ return self.reset_quick_craft();
+ }
+ if self.quick_craft_status == QuickCraftStatusKind::Start {
+ self.quick_craft_kind = quick_craft.kind.clone();
+ if self.quick_craft_kind == QuickCraftKind::Middle && player_abilities.instant_break
+ {
+ self.quick_craft_status = QuickCraftStatusKind::Add;
+ self.quick_craft_slots.clear();
+ } else {
+ self.reset_quick_craft();
+ }
+ return;
+ }
+ if let QuickCraftStatus::Add { slot } = quick_craft.status {
+ let slot_item = self.menu().slot(slot as usize);
+ if let Some(slot_item) = slot_item {
+ if let ItemSlot::Present(carried) = &self.carried {
+ // minecraft also checks slot.may_place(carried) and
+ // menu.can_drag_to(slot)
+ // but they always return true so they're not relevant for us
+ if can_item_quick_replace(slot_item, &self.carried, true)
+ && (self.quick_craft_kind == QuickCraftKind::Right
+ || carried.count as usize > self.quick_craft_slots.len())
+ {
+ self.quick_craft_slots.insert(slot);
+ }
+ }
+ }
+ return;
+ }
+ if self.quick_craft_status == QuickCraftStatusKind::End {
+ if !self.quick_craft_slots.is_empty() {
+ if self.quick_craft_slots.len() == 1 {
+ // if we only clicked one slot, then turn this
+ // QuickCraftClick into a PickupClick
+ let slot = *self.quick_craft_slots.iter().next().unwrap();
+ self.reset_quick_craft();
+ self.simulate_click(
+ &match self.quick_craft_kind {
+ QuickCraftKind::Left => {
+ PickupClick::Left { slot: Some(slot) }.into()
+ }
+ QuickCraftKind::Right => {
+ PickupClick::Left { slot: Some(slot) }.into()
+ }
+ QuickCraftKind::Middle => {
+ // idk just do nothing i guess
+ return;
+ }
+ },
+ player_abilities,
+ );
+ return;
+ }
+
+ let ItemSlot::Present(mut carried) = self.carried.clone() else {
+ // this should never happen
+ return self.reset_quick_craft();
+ };
+
+ let mut carried_count = carried.count;
+ let mut quick_craft_slots_iter = self.quick_craft_slots.iter();
+
+ loop {
+ let mut slot: &ItemSlot;
+ let mut slot_index: u16;
+ let mut item_stack: &ItemSlot;
+
+ loop {
+ let Some(&next_slot) = quick_craft_slots_iter.next() else {
+ carried.count = carried_count;
+ self.carried = ItemSlot::Present(carried);
+ return self.reset_quick_craft();
+ };
+
+ slot = self.menu().slot(next_slot as usize).unwrap();
+ slot_index = next_slot;
+ item_stack = &self.carried;
+
+ if slot.is_present()
+ && can_item_quick_replace(slot, item_stack, true)
+ // this always returns true in most cases
+ // && slot.may_place(item_stack)
+ && (
+ self.quick_craft_kind == QuickCraftKind::Middle
+ || item_stack.count() as i32 >= self.quick_craft_slots.len() as i32
+ )
+ {
+ break;
+ }
+ }
+
+ // get the ItemSlotData for the slot
+ let ItemSlot::Present(slot) = slot else {
+ unreachable!("the loop above requires the slot to be present to break")
+ };
+
+ // if self.can_drag_to(slot) {
+ let mut new_carried = carried.clone();
+ let slot_item_count = slot.count;
+ get_quick_craft_slot_count(
+ &self.quick_craft_slots,
+ &self.quick_craft_kind,
+ &mut new_carried,
+ slot_item_count,
+ );
+ let max_stack_size = i8::min(
+ new_carried.kind.max_stack_size(),
+ i8::min(
+ new_carried.kind.max_stack_size(),
+ slot.kind.max_stack_size(),
+ ),
+ );
+ if new_carried.count > max_stack_size {
+ new_carried.count = max_stack_size;
+ }
+
+ carried_count -= new_carried.count - slot_item_count;
+ // we have to inline self.menu_mut() here to avoid the borrow checker
+ // complaining
+ let menu = if let Some(menu) = &mut self.container_menu {
+ menu
+ } else {
+ &mut self.inventory_menu
+ };
+ *menu.slot_mut(slot_index as usize).unwrap() =
+ ItemSlot::Present(new_carried);
+ // }
+ }
+ }
+ } else {
+ return self.reset_quick_craft();
+ }
+ }
+ // the quick craft status should always be in start if we're not in quick craft
+ // mode
+ if self.quick_craft_status != QuickCraftStatusKind::Start {
+ return self.reset_quick_craft();
+ }
+
+ match operation {
+ // left clicking outside inventory
+ ClickOperation::Pickup(PickupClick::Left { slot: None }) => {
+ if self.carried.is_present() {
+ // vanilla has `player.drop`s but they're only used
+ // server-side
+ // they're included as comments here in case you want to adapt this for a server
+ // implementation
+
+ // player.drop(self.carried, true);
+ self.carried = ItemSlot::Empty;
+ }
+ }
+ ClickOperation::Pickup(PickupClick::Right { slot: None }) => {
+ if self.carried.is_present() {
+ let _item = self.carried.split(1);
+ // player.drop(item, true);
+ }
+ }
+ ClickOperation::Pickup(
+ PickupClick::Left { slot: Some(slot) } | PickupClick::Right { slot: Some(slot) },
+ ) => {
+ let Some(slot_item) = self.menu().slot(*slot as usize) else {
+ return;
+ };
+ let carried = &self.carried;
+ // vanilla does a check called tryItemClickBehaviourOverride
+ // here
+ // i don't understand it so i didn't implement it
+ match slot_item {
+ ItemSlot::Empty => if carried.is_present() {},
+ ItemSlot::Present(_) => todo!(),
+ }
+ }
+ ClickOperation::QuickMove(
+ QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot },
+ ) => {
+ // in vanilla it also tests if QuickMove has a slot index of -999
+ // but i don't think that's ever possible so it's not covered here
+ loop {
+ let new_slot_item = self.menu_mut().quick_move_stack(*slot as usize);
+ let slot_item = self.menu().slot(*slot as usize).unwrap();
+ if new_slot_item.is_empty() || slot_item != &new_slot_item {
+ break;
+ }
+ }
+ }
+ ClickOperation::Swap(s) => {
+ let source_slot_index = s.source_slot as usize;
+ let target_slot_index = s.target_slot as usize;
+
+ let Some(source_slot) = self.menu().slot(source_slot_index) else {
+ return;
+ };
+ let Some(target_slot) = self.menu().slot(target_slot_index) else {
+ return;
+ };
+ if source_slot.is_empty() && target_slot.is_empty() {
+ return;
+ }
+
+ if target_slot.is_empty() {
+ if self.menu().may_pickup(source_slot_index) {
+ let source_slot = source_slot.clone();
+ let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
+ *target_slot = source_slot;
+ }
+ } else if source_slot.is_empty() {
+ let ItemSlot::Present(target_item) = target_slot else {
+ unreachable!("target slot is not empty but is not present");
+ };
+ if self.menu().may_place(source_slot_index, target_item) {
+ // get the target_item but mutable
+ let source_max_stack_size = self.menu().max_stack_size(source_slot_index);
+
+ let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
+ let new_source_slot = target_slot.split(source_max_stack_size);
+ *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
+ }
+ } else if self.menu().may_pickup(source_slot_index) {
+ let ItemSlot::Present(target_item) = target_slot else {
+ unreachable!("target slot is not empty but is not present");
+ };
+ if self.menu().may_place(source_slot_index, target_item) {
+ let source_max_stack = self.menu().max_stack_size(source_slot_index);
+ if target_slot.count() > source_max_stack as i8 {
+ // if there's more than the max stack size in the target slot
+
+ let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
+ let new_source_slot = target_slot.split(source_max_stack);
+ *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot;
+ // if !self.inventory_menu.add(new_source_slot) {
+ // player.drop(new_source_slot, true);
+ // }
+ } else {
+ // normal swap
+ let new_target_slot = source_slot.clone();
+ let new_source_slot = target_slot.clone();
+
+ let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap();
+ *target_slot = new_target_slot;
+
+ let source_slot = self.menu_mut().slot_mut(source_slot_index).unwrap();
+ *source_slot = new_source_slot;
+ }
+ }
+ }
+ }
+ ClickOperation::Clone(CloneClick { slot }) => {
+ if !player_abilities.instant_break || self.carried.is_present() {
+ return;
+ }
+ let Some(source_slot) = self.menu().slot(*slot as usize) else {
+ return;
+ };
+ let ItemSlot::Present(source_item) = source_slot else {
+ return;
+ };
+ let mut new_carried = source_item.clone();
+ new_carried.count = new_carried.kind.max_stack_size();
+ self.carried = ItemSlot::Present(new_carried);
+ }
+ ClickOperation::Throw(c) => {
+ if self.carried.is_present() {
+ return;
+ }
+
+ let (ThrowClick::Single { slot: slot_index }
+ | ThrowClick::All { slot: slot_index }) = c;
+ let slot_index = *slot_index as usize;
+
+ let Some(slot) = self.menu_mut().slot_mut(slot_index) else {
+ return;
+ };
+ let ItemSlot::Present(slot_item) = slot else {
+ return;
+ };
+
+ let dropping_count = match c {
+ ThrowClick::Single { .. } => 1,
+ ThrowClick::All { .. } => slot_item.count,
+ };
+
+ let _dropping = slot_item.split(dropping_count as u8);
+ // player.drop(dropping, true);
+ }
+ ClickOperation::PickupAll(PickupAllClick {
+ slot: source_slot_index,
+ reversed,
+ }) => {
+ let source_slot_index = *source_slot_index as usize;
+
+ let source_slot = self.menu().slot(source_slot_index).unwrap();
+ let target_slot = self.carried.clone();
+
+ if target_slot.is_empty()
+ || (source_slot.is_present() && self.menu().may_pickup(source_slot_index))
+ {
+ return;
+ }
+
+ let ItemSlot::Present(target_slot_item) = &target_slot else {
+ unreachable!("target slot is not empty but is not present");
+ };
+
+ for round in 0..2 {
+ let iterator: Box<dyn Iterator<Item = usize>> = if *reversed {
+ Box::new((0..self.menu().len()).rev())
+ } else {
+ Box::new(0..self.menu().len())
+ };
+
+ for i in iterator {
+ if target_slot_item.count < target_slot_item.kind.max_stack_size() {
+ let checking_slot = self.menu().slot(i).unwrap();
+ if let ItemSlot::Present(checking_item) = checking_slot {
+ if can_item_quick_replace(checking_slot, &target_slot, true)
+ && self.menu().may_pickup(i)
+ && (round != 0
+ || checking_item.count
+ != checking_item.kind.max_stack_size())
+ {
+ // get the checking_slot and checking_item again but mutable
+ let checking_slot = self.menu_mut().slot_mut(i).unwrap();
+
+ let taken_item =
+ checking_slot.split(checking_slot.count() as u8);
+
+ // now extend the carried item
+ let target_slot = &mut self.carried;
+ let ItemSlot::Present(target_slot_item) = target_slot else {
+ unreachable!("target slot is not empty but is not present");
+ };
+ target_slot_item.count += taken_item.count();
+ }
+ }
+ }
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+
+ fn reset_quick_craft(&mut self) {
+ self.quick_craft_status = QuickCraftStatusKind::Start;
+ self.quick_craft_slots.clear();
+ }
+}
+
+fn can_item_quick_replace(
+ target_slot: &ItemSlot,
+ item: &ItemSlot,
+ ignore_item_count: bool,
+) -> bool {
+ let ItemSlot::Present(target_slot) = target_slot else {
+ return false;
+ };
+ let ItemSlot::Present(item) = item else {
+ // i *think* this is what vanilla does
+ // not 100% sure lol probably doesn't matter though
+ return false;
+ };
+
+ if !item.is_same_item_and_nbt(target_slot) {
+ return false;
+ }
+ let count = target_slot.count as u16
+ + if ignore_item_count {
+ 0
+ } else {
+ item.count as u16
+ };
+ count <= item.kind.max_stack_size() as u16
+}
+
+// public static void getQuickCraftSlotCount(Set<Slot> quickCraftSlots, int
+// quickCraftType, ItemStack itemStack, int var3) {
+// switch (quickCraftType) {
+// case 0:
+// itemStack.setCount(Mth.floor((float) itemStack.getCount() / (float)
+// quickCraftSlots.size())); break;
+// case 1:
+// itemStack.setCount(1);
+// break;
+// case 2:
+// itemStack.setCount(itemStack.getItem().getMaxStackSize());
+// }
+
+// itemStack.grow(var3);
+// }
+fn get_quick_craft_slot_count(
+ quick_craft_slots: &HashSet<u16>,
+ quick_craft_kind: &QuickCraftKind,
+ item: &mut ItemSlotData,
+ slot_item_count: i8,
+) {
+ item.count = match quick_craft_kind {
+ QuickCraftKind::Left => item.count / quick_craft_slots.len() as i8,
+ QuickCraftKind::Right => 1,
+ QuickCraftKind::Middle => item.kind.max_stack_size(),
+ };
+ item.count += slot_item_count;
+}
+
+impl Default for InventoryComponent {
+ fn default() -> Self {
+ InventoryComponent {
+ inventory_menu: Menu::Player(azalea_inventory::Player::default()),
+ id: 0,
+ container_menu: None,
+ carried: ItemSlot::Empty,
+ state_id: 0,
+ quick_craft_status: QuickCraftStatusKind::Start,
+ quick_craft_kind: QuickCraftKind::Middle,
+ quick_craft_slots: HashSet::new(),
+ }
+ }
+}
+
+/// Sent from the server when a menu (like a chest or crafting table) was
+/// opened by the client.
+#[derive(Debug)]
+pub struct MenuOpenedEvent {
+ pub entity: Entity,
+ pub window_id: u32,
+ pub menu_type: MenuKind,
+ pub title: FormattedText,
+}
+fn handle_menu_opened_event(
+ mut events: EventReader<MenuOpenedEvent>,
+ mut query: Query<&mut InventoryComponent>,
+) {
+ for event in events.iter() {
+ let mut inventory = query.get_mut(event.entity).unwrap();
+ inventory.id = event.window_id as u8;
+ inventory.container_menu = Some(Menu::from_kind(event.menu_type));
+ }
+}
+
+/// Tell the server that we want to close a container.
+///
+/// Note that this is also sent when the client closes its own inventory, even
+/// though there is no packet for opening its inventory.
+pub struct CloseContainerEvent {
+ pub entity: Entity,
+ /// The ID of the container to close. 0 for the player's inventory. If this
+ /// is not the same as the currently open inventory, nothing will happen.
+ pub id: u8,
+}
+fn handle_container_close_event(
+ mut events: EventReader<CloseContainerEvent>,
+ mut client_side_events: EventWriter<ClientSideCloseContainerEvent>,
+ query: Query<(&LocalPlayer, &InventoryComponent)>,
+) {
+ for event in events.iter() {
+ let (local_player, inventory) = query.get(event.entity).unwrap();
+ if event.id != inventory.id {
+ warn!(
+ "Tried to close container with ID {}, but the current container ID is {}",
+ event.id, inventory.id
+ );
+ continue;
+ }
+
+ local_player.write_packet(
+ ServerboundContainerClosePacket {
+ container_id: inventory.id,
+ }
+ .get(),
+ );
+ client_side_events.send(ClientSideCloseContainerEvent {
+ entity: event.entity,
+ });
+ }
+}
+
+/// Close a container without notifying the server.
+///
+/// Note that this also gets fired when we get a [`CloseContainerEvent`].
+pub struct ClientSideCloseContainerEvent {
+ pub entity: Entity,
+}
+fn handle_client_side_close_container_event(
+ mut events: EventReader<ClientSideCloseContainerEvent>,
+ mut query: Query<&mut InventoryComponent>,
+) {
+ for event in events.iter() {
+ let mut inventory = query.get_mut(event.entity).unwrap();
+ inventory.container_menu = None;
+ inventory.id = 0;
+ }
+}
+
+#[derive(Debug)]
+pub struct ContainerClickEvent {
+ pub entity: Entity,
+ pub window_id: u8,
+ pub operation: ClickOperation,
+}
+fn handle_container_click_event(
+ mut events: EventReader<ContainerClickEvent>,
+ mut query: Query<(&mut InventoryComponent, &LocalPlayer)>,
+) {
+ for event in events.iter() {
+ let (mut inventory, local_player) = query.get_mut(event.entity).unwrap();
+ if inventory.id != event.window_id {
+ warn!(
+ "Tried to click container with ID {}, but the current container ID is {}",
+ event.window_id, inventory.id
+ );
+ continue;
+ }
+
+ let menu = inventory.menu_mut();
+ let old_slots = menu.slots().clone();
+
+ // menu.click(&event.operation);
+
+ // see which slots changed after clicking and put them in the hashmap
+ // the server uses this to check if we desynced
+ let mut changed_slots: HashMap<u16, ItemSlot> = HashMap::new();
+ for (slot_index, old_slot) in old_slots.iter().enumerate() {
+ let new_slot = &menu.slots()[slot_index];
+ if old_slot != new_slot {
+ changed_slots.insert(slot_index as u16, new_slot.clone());
+ }
+ }
+
+ local_player.write_packet(
+ ServerboundContainerClickPacket {
+ container_id: event.window_id,
+ state_id: inventory.state_id,
+ slot_num: event.operation.slot_num().map(|n| n as i16).unwrap_or(-999),
+ button_num: event.operation.button_num(),
+ click_type: event.operation.click_type(),
+ changed_slots,
+ carried_item: inventory.carried.clone(),
+ }
+ .get(),
+ )
+ }
+}
+
+/// Sent from the server when the contents of a container are replaced. Usually
+/// triggered by the `ContainerSetContent` packet.
+pub struct SetContainerContentEvent {
+ pub entity: Entity,
+ pub slots: Vec<ItemSlot>,
+ pub container_id: u8,
+}
+fn handle_set_container_content_event(
+ mut events: EventReader<SetContainerContentEvent>,
+ mut query: Query<&mut InventoryComponent>,
+) {
+ for event in events.iter() {
+ let mut inventory = query.get_mut(event.entity).unwrap();
+
+ if event.container_id != inventory.id {
+ warn!(
+ "Tried to set container content with ID {}, but the current container ID is {}",
+ event.container_id, inventory.id
+ );
+ continue;
+ }
+
+ let menu = inventory.menu_mut();
+ for (i, slot) in event.slots.iter().enumerate() {
+ if let Some(slot_mut) = menu.slot_mut(i) {
+ *slot_mut = slot.clone();
+ }
+ }
+ }
+}
diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs
index 04fec604..c198ced3 100644
--- a/azalea-client/src/lib.rs
+++ b/azalea-client/src/lib.rs
@@ -18,6 +18,8 @@ pub mod disconnect;
mod entity_query;
mod events;
mod get_mc_dir;
+pub mod interact;
+pub mod inventory;
mod local_player;
mod movement;
pub mod packet_handling;
@@ -28,6 +30,7 @@ pub mod task_pool;
pub use account::{Account, AccountOpts};
pub use client::{
init_ecs_app, start_ecs, Client, ClientInformation, JoinError, JoinedClientBundle, TabList,
+ TickBroadcast,
};
pub use events::Event;
pub use local_player::{GameProfileComponent, LocalPlayer};
diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs
index 540ef3b4..423b4308 100644
--- a/azalea-client/src/local_player.rs
+++ b/azalea-client/src/local_player.rs
@@ -1,14 +1,18 @@
use std::{io, sync::Arc};
use azalea_auth::game_profile::GameProfile;
-use azalea_core::ChunkPos;
+use azalea_core::{ChunkPos, GameMode};
use azalea_protocol::packets::game::ServerboundGamePacket;
use azalea_world::{
- entity::{self, Dead},
- Instance, PartialInstance,
+ entity::{self, Dead, WorldName},
+ Instance, InstanceContainer, PartialInstance,
};
use bevy_ecs::{
- component::Component, entity::Entity, event::EventReader, query::Added, system::Query,
+ component::Component,
+ entity::Entity,
+ event::EventReader,
+ query::Added,
+ system::{Query, Res},
};
use derive_more::{Deref, DerefMut};
use parking_lot::RwLock;
@@ -75,9 +79,17 @@ pub struct GameProfileComponent(pub GameProfile);
/// Marks a [`LocalPlayer`] that's in a loaded chunk. This is updated at the
/// beginning of every tick.
-#[derive(Component)]
+#[derive(Component, Clone, Debug, Copy)]
pub struct LocalPlayerInLoadedChunk;
+/// The gamemode of a local player. For a non-local player, you can look up the
+/// player in the [`TabList`].
+#[derive(Component, Clone, Debug, Copy)]
+pub struct LocalGameMode {
+ pub current: GameMode,
+ pub previous: Option<GameMode>,
+}
+
impl LocalPlayer {
/// Create a new `LocalPlayer`.
pub fn new(
@@ -104,7 +116,7 @@ impl LocalPlayer {
}
/// Write a packet directly to the server.
- pub fn write_packet(&mut self, packet: ServerboundGamePacket) {
+ pub fn write_packet(&self, packet: ServerboundGamePacket) {
self.packet_writer
.send(packet)
.expect("write_packet shouldn't be able to be called if the connection is closed");
@@ -122,16 +134,15 @@ impl Drop for LocalPlayer {
/// Update the [`LocalPlayerInLoadedChunk`] component for all [`LocalPlayer`]s.
pub fn update_in_loaded_chunk(
mut commands: bevy_ecs::system::Commands,
- query: Query<(Entity, &LocalPlayer, &entity::Position)>,
+ query: Query<(Entity, &WorldName, &entity::Position)>,
+ instance_container: Res<InstanceContainer>,
) {
for (entity, local_player, position) in &query {
let player_chunk_pos = ChunkPos::from(position);
- let in_loaded_chunk = local_player
- .world
- .read()
- .chunks
- .get(&player_chunk_pos)
- .is_some();
+ let instance_lock = instance_container
+ .get(local_player)
+ .expect("local player should always be in an instance");
+ let in_loaded_chunk = instance_lock.read().chunks.get(&player_chunk_pos).is_some();
if in_loaded_chunk {
commands.entity(entity).insert(LocalPlayerInLoadedChunk);
} else {
@@ -176,7 +187,7 @@ pub fn handle_send_packet_event(
mut query: Query<&mut LocalPlayer>,
) {
for event in send_packet_events.iter() {
- if let Ok(mut local_player) = query.get_mut(event.entity) {
+ if let Ok(local_player) = query.get_mut(event.entity) {
local_player.write_packet(event.packet.clone());
}
}
diff --git a/azalea-client/src/movement.rs b/azalea-client/src/movement.rs
index f6123c70..d68be8b8 100644
--- a/azalea-client/src/movement.rs
+++ b/azalea-client/src/movement.rs
@@ -16,6 +16,7 @@ use azalea_world::{
};
use bevy_app::{App, CoreSchedule, IntoSystemAppConfigs, Plugin};
use bevy_ecs::{
+ component::Component,
entity::Entity,
event::EventReader,
query::With,
@@ -84,18 +85,26 @@ impl Client {
**jumping_ref
}
- /// Sets your rotation. `y_rot` is yaw (looking to the side), `x_rot` is
- /// pitch (looking up and down). You can get these numbers from the vanilla
- /// f3 screen.
+ /// Sets the direction the client is looking. `y_rot` is yaw (looking to the
+ /// side), `x_rot` is pitch (looking up and down). You can get these
+ /// numbers from the vanilla f3 screen.
/// `y_rot` goes from -180 to 180, and `x_rot` goes from -90 to 90.
- pub fn set_rotation(&mut self, y_rot: f32, x_rot: f32) {
+ pub fn set_direction(&mut self, y_rot: f32, x_rot: f32) {
let mut ecs = self.ecs.lock();
- let mut physics = self.query::<&mut entity::Physics>(&mut ecs);
+ let mut look_direction = self.query::<&mut entity::LookDirection>(&mut ecs);
- entity::set_rotation(&mut physics, y_rot, x_rot);
+ (look_direction.y_rot, look_direction.x_rot) = (y_rot, x_rot);
}
}
+/// A component that contains the look direction that was last sent over the
+/// network.
+#[derive(Debug, Component, Clone, Default)]
+pub struct LastSentLookDirection {
+ pub x_rot: f32,
+ pub y_rot: f32,
+}
+
#[allow(clippy::type_complexity)]
pub(crate) fn send_position(
mut query: Query<
@@ -106,6 +115,8 @@ pub(crate) fn send_position(
&entity::Position,
&mut entity::LastSentPosition,
&mut entity::Physics,
+ &entity::LookDirection,
+ &mut LastSentLookDirection,
&entity::metadata::Sprinting,
),
&LocalPlayerInLoadedChunk,
@@ -118,6 +129,8 @@ pub(crate) fn send_position(
position,
mut last_sent_position,
mut physics,
+ direction,
+ mut last_direction,
sprinting,
) in query.iter_mut()
{
@@ -130,8 +143,8 @@ pub(crate) fn send_position(
let x_delta = position.x - last_sent_position.x;
let y_delta = position.y - last_sent_position.y;
let z_delta = position.z - last_sent_position.z;
- let y_rot_delta = (physics.y_rot - physics.y_rot_last) as f64;
- let x_rot_delta = (physics.x_rot - physics.x_rot_last) as f64;
+ let y_rot_delta = (direction.y_rot - last_direction.y_rot) as f64;
+ let x_rot_delta = (direction.x_rot - last_direction.x_rot) as f64;
physics_state.position_remainder += 1;
@@ -140,19 +153,19 @@ pub(crate) fn send_position(
let sending_position = ((x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2))
> 2.0e-4f64.powi(2))
|| physics_state.position_remainder >= 20;
- let sending_rotation = y_rot_delta != 0.0 || x_rot_delta != 0.0;
+ let sending_direction = y_rot_delta != 0.0 || x_rot_delta != 0.0;
// if self.is_passenger() {
// TODO: posrot packet for being a passenger
// }
- let packet = if sending_position && sending_rotation {
+ let packet = if sending_position && sending_direction {
Some(
ServerboundMovePlayerPosRotPacket {
x: position.x,
y: position.y,
z: position.z,
- x_rot: physics.x_rot,
- y_rot: physics.y_rot,
+ x_rot: direction.x_rot,
+ y_rot: direction.y_rot,
on_ground: physics.on_ground,
}
.get(),
@@ -167,11 +180,11 @@ pub(crate) fn send_position(
}
.get(),
)
- } else if sending_rotation {
+ } else if sending_direction {
Some(
ServerboundMovePlayerRotPacket {
- x_rot: physics.x_rot,
- y_rot: physics.y_rot,
+ x_rot: direction.x_rot,
+ y_rot: direction.y_rot,
on_ground: physics.on_ground,
}
.get(),
@@ -191,9 +204,9 @@ pub(crate) fn send_position(
**last_sent_position = **position;
physics_state.position_remainder = 0;
}
- if sending_rotation {
- physics.y_rot_last = physics.y_rot;
- physics.x_rot_last = physics.x_rot;
+ if sending_direction {
+ last_direction.y_rot = direction.y_rot;
+ last_direction.x_rot = direction.x_rot;
}
physics.last_on_ground = physics.on_ground;
diff --git a/azalea-client/src/packet_handling.rs b/azalea-client/src/packet_handling.rs
index b9837dba..8ffff870 100644
--- a/azalea-client/src/packet_handling.rs
+++ b/azalea-client/src/packet_handling.rs
@@ -1,6 +1,6 @@
use std::{collections::HashSet, io::Cursor, sync::Arc};
-use azalea_core::{ChunkPos, ResourceLocation, Vec3};
+use azalea_core::{ChunkPos, GameMode, ResourceLocation, Vec3};
use azalea_protocol::{
connect::{ReadConnection, WriteConnection},
packets::game::{
@@ -16,7 +16,7 @@ use azalea_protocol::{
use azalea_world::{
entity::{
metadata::{apply_metadata, Health, PlayerMetadataBundle},
- set_rotation, Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition,
+ Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition, LookDirection,
MinecraftEntityId, Physics, PlayerBundle, Position, WorldName,
},
entity::{LoadedBy, RelativeEntityUpdate},
@@ -37,9 +37,13 @@ use tokio::sync::mpsc;
use crate::{
chat::{ChatPacket, ChatReceivedEvent},
- client::TabList,
+ client::{PlayerAbilities, TabList},
disconnect::DisconnectEvent,
- local_player::{GameProfileComponent, LocalPlayer},
+ inventory::{
+ ClientSideCloseContainerEvent, InventoryComponent, MenuOpenedEvent,
+ SetContainerContentEvent,
+ },
+ local_player::{GameProfileComponent, LocalGameMode, LocalPlayer},
ClientInformation, PlayerInfo,
};
@@ -194,7 +198,7 @@ fn process_packet_events(ecs: &mut World) {
)>,
ResMut<InstanceContainer>,
)> = SystemState::new(ecs);
- let (mut commands, mut query, mut world_container) = system_state.get_mut(ecs);
+ let (mut commands, mut query, mut instance_container) = system_state.get_mut(ecs);
let (mut local_player, world_name, game_profile, client_information) =
query.get_mut(player_entity).unwrap();
@@ -220,16 +224,16 @@ fn process_packet_events(ecs: &mut World) {
.entity(player_entity)
.insert(WorldName(new_world_name.clone()));
}
- // add this world to the world_container (or don't if it's already
+ // add this world to the instance_container (or don't if it's already
// there)
- let weak_world = world_container.insert(
+ let weak_world = instance_container.insert(
new_world_name.clone(),
dimension.height,
dimension.min_y,
);
// set the partial_world to an empty world
// (when we add chunks or entities those will be in the
- // world_container)
+ // instance_container)
*local_player.partial_instance.write() = PartialInstance::new(
client_information.view_distance.into(),
@@ -250,9 +254,14 @@ fn process_packet_events(ecs: &mut World) {
metadata: PlayerMetadataBundle::default(),
};
// insert our components into the ecs :)
- commands
- .entity(player_entity)
- .insert((MinecraftEntityId(p.player_id), player_bundle));
+ commands.entity(player_entity).insert((
+ MinecraftEntityId(p.player_id),
+ LocalGameMode {
+ current: p.game_type,
+ previous: p.previous_game_type.into(),
+ },
+ player_bundle,
+ ));
}
// send the client information that we have set
@@ -288,6 +297,12 @@ fn process_packet_events(ecs: &mut World) {
}
ClientboundGamePacket::PlayerAbilities(p) => {
debug!("Got player abilities packet {:?}", p);
+ let mut system_state: SystemState<Query<&mut PlayerAbilities>> =
+ SystemState::new(ecs);
+ let mut query = system_state.get_mut(ecs);
+ let mut player_abilities = query.get_mut(player_entity).unwrap();
+
+ *player_abilities = PlayerAbilities::from(p);
}
ClientboundGamePacket::SetCarriedItem(p) => {
debug!("Got set carried item packet {:?}", p);
@@ -319,16 +334,18 @@ fn process_packet_events(ecs: &mut World) {
// TODO: reply with teleport confirm
debug!("Got player position packet {:?}", p);
+ #[allow(clippy::type_complexity)]
let mut system_state: SystemState<
Query<(
&mut LocalPlayer,
&mut Physics,
+ &mut LookDirection,
&mut Position,
&mut LastSentPosition,
)>,
> = SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
- let Ok((mut local_player, mut physics, mut position, mut last_sent_position)) =
+ let Ok((local_player, mut physics, mut direction, mut position, mut last_sent_position)) =
query.get_mut(player_entity) else {
continue;
};
@@ -364,10 +381,10 @@ fn process_packet_events(ecs: &mut World) {
let mut y_rot = p.y_rot;
let mut x_rot = p.x_rot;
if p.relative_arguments.x_rot {
- x_rot += physics.x_rot;
+ x_rot += direction.x_rot;
}
if p.relative_arguments.y_rot {
- y_rot += physics.y_rot;
+ y_rot += direction.y_rot;
}
physics.delta = Vec3 {
@@ -378,7 +395,7 @@ fn process_packet_events(ecs: &mut World) {
// we call a function instead of setting the fields ourself since the
// function makes sure the rotations stay in their
// ranges
- set_rotation(&mut physics, y_rot, x_rot);
+ (direction.y_rot, direction.x_rot) = (y_rot, x_rot);
// TODO: minecraft sets "xo", "yo", and "zo" here but idk what that means
// so investigate that ig
let new_pos = Vec3 {
@@ -633,9 +650,6 @@ fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::SetDefaultSpawnPosition(p) => {
debug!("Got set default spawn position packet {:?}", p);
}
- ClientboundGamePacket::ContainerSetContent(p) => {
- debug!("Got container set content packet {:?}", p);
- }
ClientboundGamePacket::SetHealth(p) => {
debug!("Got set health packet {:?}", p);
@@ -765,7 +779,7 @@ fn process_packet_events(ecs: &mut World) {
id: p.id,
});
- let mut local_player = query.get_mut(player_entity).unwrap();
+ let local_player = query.get_mut(player_entity).unwrap();
local_player.write_packet(ServerboundKeepAlivePacket { id: p.id }.get());
debug!("Sent keep alive packet {p:?} for {player_entity:?}");
}
@@ -831,7 +845,23 @@ fn process_packet_events(ecs: &mut World) {
}
}
ClientboundGamePacket::GameEvent(p) => {
+ use azalea_protocol::packets::game::clientbound_game_event_packet::EventType;
+
debug!("Got game event packet {:?}", p);
+
+ #[allow(clippy::single_match)]
+ match p.event {
+ EventType::ChangeGameMode => {
+ let mut system_state: SystemState<Query<&mut LocalGameMode>> =
+ SystemState::new(ecs);
+ let mut query = system_state.get_mut(ecs);
+ let mut local_game_mode = query.get_mut(player_entity).unwrap();
+ if let Some(new_game_mode) = GameMode::from_id(p.param as u8) {
+ local_game_mode.current = new_game_mode;
+ }
+ }
+ _ => {}
+ }
}
ClientboundGamePacket::LevelParticles(p) => {
debug!("Got level particles packet {:?}", p);
@@ -855,8 +885,93 @@ fn process_packet_events(ecs: &mut World) {
}
ClientboundGamePacket::BossEvent(_) => {}
ClientboundGamePacket::CommandSuggestions(_) => {}
- ClientboundGamePacket::ContainerSetData(_) => {}
- ClientboundGamePacket::ContainerSetSlot(_) => {}
+ ClientboundGamePacket::ContainerSetContent(p) => {
+ debug!("Got container set content packet {:?}", p);
+
+ let mut system_state: SystemState<(
+ Query<&mut InventoryComponent>,
+ EventWriter<SetContainerContentEvent>,
+ )> = SystemState::new(ecs);
+ let (mut query, mut events) = system_state.get_mut(ecs);
+ let mut inventory = query.get_mut(player_entity).unwrap();
+
+ // container id 0 is always the player's inventory
+ if p.container_id == 0 {
+ // this is just so it has the same type as the `else` block
+ for (i, slot) in p.items.iter().enumerate() {
+ if let Some(slot_mut) = inventory.inventory_menu.slot_mut(i) {
+ *slot_mut = slot.clone();
+ }
+ }
+ } else {
+ events.send(SetContainerContentEvent {
+ entity: player_entity,
+ slots: p.items.clone(),
+ container_id: p.container_id as u8,
+ });
+ }
+ }
+ ClientboundGamePacket::ContainerSetData(p) => {
+ debug!("Got container set data packet {:?}", p);
+ // let mut system_state: SystemState<Query<&mut
+ // InventoryComponent>> =
+ // SystemState::new(ecs);
+ // let mut query = system_state.get_mut(ecs);
+ // let mut inventory =
+ // query.get_mut(player_entity).unwrap();
+
+ // TODO: handle ContainerSetData packet
+ // this is used for various things like the furnace progress
+ // bar
+ // see https://wiki.vg/Protocol#Set_Container_Property
+ }
+ ClientboundGamePacket::ContainerSetSlot(p) => {
+ debug!("Got container set slot packet {:?}", p);
+
+ let mut system_state: SystemState<Query<&mut InventoryComponent>> =
+ SystemState::new(ecs);
+ let mut query = system_state.get_mut(ecs);
+ let mut inventory = query.get_mut(player_entity).unwrap();
+
+ if p.container_id == -1 {
+ // -1 means carried item
+ inventory.carried = p.item_stack.clone();
+ } else if p.container_id == -2 {
+ if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) {
+ *slot = p.item_stack.clone();
+ }
+ } else {
+ let is_creative_mode_and_inventory_closed = false;
+ // technically minecraft has slightly different behavior here if you're in
+ // creative mode and have your inventory open
+ if p.container_id == 0
+ && azalea_inventory::Player::is_hotbar_slot(p.slot.into())
+ {
+ // minecraft also sets a "pop time" here which is used for an animation
+ // but that's not really necessary
+ if let Some(slot) = inventory.inventory_menu.slot_mut(p.slot.into()) {
+ *slot = p.item_stack.clone();
+ }
+ } else if p.container_id == (inventory.id as i8)
+ && (p.container_id != 0 || !is_creative_mode_and_inventory_closed)
+ {
+ // var2.containerMenu.setItem(var4, var1.getStateId(), var3);
+ if let Some(slot) = inventory.menu_mut().slot_mut(p.slot.into()) {
+ *slot = p.item_stack.clone();
+ inventory.state_id = p.state_id;
+ }
+ }
+ }
+ }
+ ClientboundGamePacket::ContainerClose(_p) => {
+ // there's p.container_id but minecraft doesn't actually check it
+ let mut system_state: SystemState<EventWriter<ClientSideCloseContainerEvent>> =
+ SystemState::new(ecs);
+ let mut client_side_close_container_events = system_state.get_mut(ecs);
+ client_side_close_container_events.send(ClientSideCloseContainerEvent {
+ entity: player_entity,
+ })
+ }
ClientboundGamePacket::Cooldown(_) => {}
ClientboundGamePacket::CustomChatCompletions(_) => {}
ClientboundGamePacket::DeleteChat(_) => {}
@@ -867,7 +982,18 @@ fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::MerchantOffers(_) => {}
ClientboundGamePacket::MoveVehicle(_) => {}
ClientboundGamePacket::OpenBook(_) => {}
- ClientboundGamePacket::OpenScreen(_) => {}
+ ClientboundGamePacket::OpenScreen(p) => {
+ debug!("Got open screen packet {:?}", p);
+ let mut system_state: SystemState<EventWriter<MenuOpenedEvent>> =
+ SystemState::new(ecs);
+ let mut menu_opened_events = system_state.get_mut(ecs);
+ menu_opened_events.send(MenuOpenedEvent {
+ entity: player_entity,
+ window_id: p.container_id,
+ menu_type: p.menu_type,
+ title: p.title,
+ })
+ }
ClientboundGamePacket::OpenSignEditor(_) => {}
ClientboundGamePacket::Ping(_) => {}
ClientboundGamePacket::PlaceGhostRecipe(_) => {}
@@ -935,7 +1061,6 @@ fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::TakeItemEntity(_) => {}
ClientboundGamePacket::DisguisedChat(_) => {}
ClientboundGamePacket::UpdateEnabledFeatures(_) => {}
- ClientboundGamePacket::ContainerClose(_) => {}
ClientboundGamePacket::Bundle(_) => {}
ClientboundGamePacket::DamageEvent(_) => {}
ClientboundGamePacket::HurtAnimation(_) => {}
diff --git a/azalea-client/src/player.rs b/azalea-client/src/player.rs
index c2c8a94e..999f2490 100755
--- a/azalea-client/src/player.rs
+++ b/azalea-client/src/player.rs
@@ -1,6 +1,6 @@
use azalea_auth::game_profile::GameProfile;
use azalea_chat::FormattedText;
-use azalea_core::GameType;
+use azalea_core::GameMode;
use azalea_world::entity::EntityInfos;
use bevy_ecs::{
event::EventReader,
@@ -18,7 +18,10 @@ pub struct PlayerInfo {
pub profile: GameProfile,
/// The player's UUID.
pub uuid: Uuid,
- pub gamemode: GameType,
+ /// The current gamemode of the player, like survival or creative.
+ pub gamemode: GameMode,
+ /// The player's latency in milliseconds. The bars in the tab screen depend
+ /// on this.
pub latency: i32,
/// The player's display name in the tab list, but only if it's different
/// from the player's normal username. Use `player_info.profile.name` to get