diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2023-05-03 20:57:27 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-05-03 20:57:27 -0500 |
| commit | 634cb8d72c6608512aedba19e5cd669104bc35ea (patch) | |
| tree | f8e76ce9eb43403d29cc0cbcf9a4f51522419dc2 /azalea-client | |
| parent | 1fb4418f2c9cbd004c64c2f23d2d0352ee12c0e5 (diff) | |
| download | azalea-drasl-634cb8d72c6608512aedba19e5cd669104bc35ea.tar.xz | |
Inventory (#48)
* start adding azalea-inventory
* design more of how inventories are defined
* start working on az-inv-macros
* inventory macro works
* start adding inventory codegen
* update some deps
* add inventory codegen
* manually write inventory menus
* put the inventories in Client
* start on containersetcontent
* inventory menu should hopefully work
* checks in containersetcontent
* format a comment
* move some variant matches
* inventory.rs
* inventory stuff
* more inventory stuff
* inventory/container tracking works
* start adding interact function
* sequence number
* start adding HitResultComponent
* implement traverse_blocks
* start adding clip
* add clip function
* update_hit_result_component
* start trying to fix
* fix
* make some stuff simpler
* clippy
* lever
* chest
* container handle
* fix ambiguity
* fix some doc tests
* move some container stuff from az-client to azalea
* clicking container
* start implementing simulate_click
* keep working on simulate click
* implement more of simulate_click
this is really boring
* inventory fixes
* start implementing shift clicking
* fix panic in azalea-chat i hope
* shift clicking implemented
* more inventory stuff
* fix items not showing in containers sometimes
* fix test
* fix all warnings
* remove a println
---------
Co-authored-by: mat <git@matdoes.dev>
Diffstat (limited to 'azalea-client')
| -rw-r--r-- | azalea-client/Cargo.toml | 1 | ||||
| -rw-r--r-- | azalea-client/src/client.rs | 127 | ||||
| -rw-r--r-- | azalea-client/src/interact.rs | 200 | ||||
| -rw-r--r-- | azalea-client/src/inventory.rs | 721 | ||||
| -rw-r--r-- | azalea-client/src/lib.rs | 3 | ||||
| -rw-r--r-- | azalea-client/src/local_player.rs | 39 | ||||
| -rw-r--r-- | azalea-client/src/movement.rs | 49 | ||||
| -rw-r--r-- | azalea-client/src/packet_handling.rs | 171 | ||||
| -rwxr-xr-x | azalea-client/src/player.rs | 7 |
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 |
