diff options
Diffstat (limited to 'azalea-client/src/plugins/interact.rs')
| -rw-r--r-- | azalea-client/src/plugins/interact.rs | 560 |
1 files changed, 0 insertions, 560 deletions
diff --git a/azalea-client/src/plugins/interact.rs b/azalea-client/src/plugins/interact.rs deleted file mode 100644 index da1fa78e..00000000 --- a/azalea-client/src/plugins/interact.rs +++ /dev/null @@ -1,560 +0,0 @@ -use std::collections::HashMap; - -use azalea_block::BlockState; -use azalea_core::{ - direction::Direction, - game_type::GameMode, - hit_result::{BlockHitResult, HitResult}, - position::{BlockPos, Vec3}, - tick::GameTick, -}; -use azalea_entity::{ - Attributes, EyeHeight, LocalEntity, LookDirection, Position, clamp_look_direction, view_vector, -}; -use azalea_inventory::{ItemStack, ItemStackData, components}; -use azalea_physics::{ - PhysicsSet, - clip::{BlockShapeType, ClipContext, FluidPickType}, -}; -use azalea_protocol::packets::game::{ - ServerboundUseItem, s_interact::InteractionHand, s_swing::ServerboundSwing, - s_use_item_on::ServerboundUseItemOn, -}; -use azalea_world::{Instance, InstanceContainer, InstanceName}; -use bevy_app::{App, Plugin, Update}; -use bevy_ecs::prelude::*; -use derive_more::{Deref, DerefMut}; -use tracing::warn; - -use super::mining::Mining; -use crate::{ - Client, - attack::handle_attack_event, - inventory::{Inventory, InventorySet}, - local_player::{LocalGameMode, PermissionLevel, PlayerAbilities}, - movement::MoveEventsSet, - packet::game::SendPacketEvent, - respawn::perform_respawn, -}; - -/// 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::<StartUseItemEvent>() - .add_event::<SwingArmEvent>() - .add_systems( - Update, - ( - ( - handle_start_use_item_event, - update_hit_result_component.after(clamp_look_direction), - handle_swing_arm_event, - ) - .after(InventorySet) - .after(perform_respawn) - .after(handle_attack_event) - .chain(), - update_modifiers_for_held_item - .after(InventorySet) - .after(MoveEventsSet), - ), - ) - .add_systems(GameTick, handle_start_use_item_queued.before(PhysicsSet)) - .add_observer(handle_swing_arm_trigger); - } -} - -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(&self, position: BlockPos) { - self.ecs.lock().send_event(StartUseItemEvent { - entity: self.entity, - hand: InteractionHand::MainHand, - force_block: Some(position), - }); - } - - /// Right-click the currently held item. - /// - /// If the item is consumable, then it'll act as if right-click was held - /// until the item finishes being consumed. You can use this to eat food. - /// - /// If we're looking at a block or entity, then it will be clicked. Also see - /// [`Client::block_interact`]. - pub fn start_use_item(&self) { - self.ecs.lock().send_event(StartUseItemEvent { - entity: self.entity, - hand: InteractionHand::MainHand, - force_block: None, - }); - } -} - -/// A component that contains information about our local block state -/// predictions. -#[derive(Component, Clone, Debug, Default)] -pub struct BlockStatePredictionHandler { - /// The total number of changes that this client has made to blocks. - seq: u32, - server_state: HashMap<BlockPos, ServerVerifiedState>, -} -#[derive(Clone, Debug)] -struct ServerVerifiedState { - seq: u32, - block_state: BlockState, - /// Used for teleporting the player back if we're colliding with the block - /// that got placed back. - #[allow(unused)] - player_pos: Vec3, -} - -impl BlockStatePredictionHandler { - /// Get the next sequence number that we're going to use and increment the - /// value. - pub fn start_predicting(&mut self) -> u32 { - self.seq += 1; - self.seq - } - - /// Should be called right before the client updates a block with its - /// prediction. - /// - /// This is used to make sure that we can rollback to this state if the - /// server acknowledges the sequence number (with - /// [`ClientboundBlockChangedAck`]) without having sent a block update. - /// - /// [`ClientboundBlockChangedAck`]: azalea_protocol::packets::game::ClientboundBlockChangedAck - pub fn retain_known_server_state( - &mut self, - pos: BlockPos, - old_state: BlockState, - player_pos: Vec3, - ) { - self.server_state - .entry(pos) - .and_modify(|s| s.seq = self.seq) - .or_insert(ServerVerifiedState { - seq: self.seq, - block_state: old_state, - player_pos, - }); - } - - /// Save this update as the correct server state so when the server sends a - /// [`ClientboundBlockChangedAck`] we don't roll back this new update. - /// - /// This should be used when we receive a block update from the server. - /// - /// [`ClientboundBlockChangedAck`]: azalea_protocol::packets::game::ClientboundBlockChangedAck - pub fn update_known_server_state(&mut self, pos: BlockPos, state: BlockState) -> bool { - if let Some(s) = self.server_state.get_mut(&pos) { - s.block_state = state; - true - } else { - false - } - } - - pub fn end_prediction_up_to(&mut self, seq: u32, world: &Instance) { - let mut to_remove = Vec::new(); - for (pos, state) in &self.server_state { - if state.seq > seq { - continue; - } - to_remove.push(*pos); - - // syncBlockState - let client_block_state = world.get_block_state(*pos).unwrap_or_default(); - let server_block_state = state.block_state; - if client_block_state == server_block_state { - continue; - } - world.set_block_state(*pos, server_block_state); - // TODO: implement these two functions - // if is_colliding(player, *pos, server_block_state) { - // abs_snap_to(state.player_pos); - // } - } - - for pos in to_remove { - self.server_state.remove(&pos); - } - } -} - -/// A component that contains the block or entity that the player is currently -/// looking at. -#[doc(alias("looking at", "looking at block", "crosshair"))] -#[derive(Component, Clone, Debug, Deref, DerefMut)] -pub struct HitResultComponent(HitResult); - -/// An event that makes one of our clients simulate a right-click. -/// -/// This event just inserts the [`StartUseItemQueued`] component on the given -/// entity. -#[doc(alias("right click"))] -#[derive(Event)] -pub struct StartUseItemEvent { - pub entity: Entity, - pub hand: InteractionHand, - /// See [`StartUseItemQueued::force_block`]. - pub force_block: Option<BlockPos>, -} -pub fn handle_start_use_item_event( - mut commands: Commands, - mut events: EventReader<StartUseItemEvent>, -) { - for event in events.read() { - commands.entity(event.entity).insert(StartUseItemQueued { - hand: event.hand, - force_block: event.force_block, - }); - } -} - -/// A component that makes our client simulate a right-click on the next -/// [`GameTick`]. It's removed after that tick. -/// -/// You may find it more convenient to use [`StartUseItemEvent`] instead, which -/// just inserts this component for you. -/// -/// [`GameTick`]: azalea_core::tick::GameTick -#[derive(Component, Debug)] -pub struct StartUseItemQueued { - pub hand: InteractionHand, - /// Optionally force us to send a [`ServerboundUseItemOn`] on the given - /// block. - /// - /// This is useful if you want to interact with a block without looking at - /// it, but should be avoided to stay compatible with anticheats. - pub force_block: Option<BlockPos>, -} -#[allow(clippy::type_complexity)] -pub fn handle_start_use_item_queued( - mut commands: Commands, - query: Query<( - Entity, - &StartUseItemQueued, - &mut BlockStatePredictionHandler, - &HitResultComponent, - &LookDirection, - Option<&Mining>, - )>, -) { - for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in - query - { - commands.entity(entity).remove::<StartUseItemQueued>(); - - if mining.is_some() { - warn!("Got a StartUseItemEvent for a client that was mining"); - } - - // TODO: this also skips if LocalPlayer.handsBusy is true, which is used when - // rowing a boat - - let mut hit_result = hit_result.0.clone(); - - if let Some(force_block) = start_use_item.force_block { - let hit_result_matches = if let HitResult::Block(block_hit_result) = &hit_result { - block_hit_result.block_pos == force_block - } else { - false - }; - - if !hit_result_matches { - // we're not looking at the block, so make up some numbers - hit_result = HitResult::Block(BlockHitResult { - location: force_block.center(), - direction: Direction::Up, - block_pos: force_block, - inside: false, - world_border: false, - miss: false, - }); - } - } - - match &hit_result { - HitResult::Block(block_hit_result) => { - let seq = prediction_handler.start_predicting(); - if block_hit_result.miss { - commands.trigger(SendPacketEvent::new( - entity, - ServerboundUseItem { - hand: start_use_item.hand, - seq, - x_rot: look_direction.x_rot, - y_rot: look_direction.y_rot, - }, - )); - } else { - commands.trigger(SendPacketEvent::new( - entity, - ServerboundUseItemOn { - hand: start_use_item.hand, - block_hit: block_hit_result.into(), - seq, - }, - )); - // TODO: depending on the result of useItemOn, this might - // also need to send a SwingArmEvent. - // basically, this TODO is for - // simulating block interactions/placements on the - // client-side. - } - } - HitResult::Entity => { - // TODO: implement HitResult::Entity - - // TODO: worldborder check - - // commands.trigger(SendPacketEvent::new( - // entity, - // ServerboundInteract { - // entity_id: todo!(), - // action: todo!(), - // using_secondary_action: todo!(), - // }, - // )); - } - } - } -} - -#[allow(clippy::type_complexity)] -pub fn update_hit_result_component( - mut commands: Commands, - mut query: Query<( - Entity, - Option<&mut HitResultComponent>, - &LocalGameMode, - &Position, - &EyeHeight, - &LookDirection, - &InstanceName, - )>, - 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 Some(instance_lock) = instance_container.get(world_name) else { - continue; - }; - let instance = instance_lock.read(); - - let hit_result = pick(*look_direction, eye_position, &instance.chunks, 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 or entity that a player would be looking at if their eyes were -/// at the given direction and position. -/// -/// If you need to get the block/entity the player is looking at right now, use -/// [`HitResultComponent`]. -/// -/// Also see [`pick_block`]. -/// -/// TODO: does not currently check for entities -pub fn pick( - look_direction: LookDirection, - eye_position: Vec3, - chunks: &azalea_world::ChunkStorage, - pick_range: f64, -) -> HitResult { - // TODO - // let entity_hit_result = ; - - HitResult::Block(pick_block(look_direction, eye_position, chunks, pick_range)) -} - -/// Get the block that a player would be looking at if their eyes were at the -/// given direction and position. -/// -/// Also see [`pick`]. -pub fn pick_block( - look_direction: LookDirection, - eye_position: Vec3, - chunks: &azalea_world::ChunkStorage, - pick_range: f64, -) -> BlockHitResult { - let view_vector = view_vector(look_direction); - let end_position = eye_position + (view_vector * pick_range); - - azalea_physics::clip::clip( - chunks, - ClipContext { - from: eye_position, - to: end_position, - block_shape_type: BlockShapeType::Outline, - fluid_pick_type: FluidPickType::None, - }, - ) -} - -/// Whether we can't interact with the block, based on your gamemode. If -/// this is false, then we can interact with the block. -/// -/// Passing the inventory, block position, and instance is necessary for the -/// adventure mode check. -pub fn check_is_interaction_restricted( - instance: &Instance, - block_pos: BlockPos, - game_mode: &GameMode, - inventory: &Inventory, -) -> bool { - match game_mode { - GameMode::Adventure => { - // vanilla checks for abilities.mayBuild here but servers have no - // way of modifying that - - let held_item = inventory.held_item(); - match &held_item { - ItemStack::Present(item) => { - let block = instance.chunks.get_block_state(block_pos); - let Some(block) = block else { - // block isn't loaded so just say that it is restricted - return true; - }; - check_block_can_be_broken_by_item_in_adventure_mode(item, &block) - } - _ => true, - } - } - GameMode::Spectator => true, - _ => false, - } -} - -/// Check if the item has the `CanDestroy` tag for the block. -pub fn check_block_can_be_broken_by_item_in_adventure_mode( - item: &ItemStackData, - _block: &BlockState, -) -> bool { - // minecraft caches the last checked block but that's kind of an unnecessary - // optimization and makes the code too complicated - - if !item.components.has::<components::CanBreak>() { - // no CanDestroy tag - return false; - }; - - false - - // for block_predicate in can_destroy { - // // TODO - // // defined in BlockPredicateArgument.java - // } - - // true -} - -pub fn can_use_game_master_blocks( - abilities: &PlayerAbilities, - permission_level: &PermissionLevel, -) -> bool { - abilities.instant_break && **permission_level >= 2 -} - -/// Swing your arm. This is purely a visual effect and won't interact with -/// anything in the world. -#[derive(Event, Clone, Debug)] -pub struct SwingArmEvent { - pub entity: Entity, -} -pub fn handle_swing_arm_trigger(trigger: Trigger<SwingArmEvent>, mut commands: Commands) { - commands.trigger(SendPacketEvent::new( - trigger.event().entity, - ServerboundSwing { - hand: InteractionHand::MainHand, - }, - )); -} -pub fn handle_swing_arm_event(mut events: EventReader<SwingArmEvent>, mut commands: Commands) { - for event in events.read() { - commands.trigger(event.clone()); - } -} - -#[allow(clippy::type_complexity)] -fn update_modifiers_for_held_item( - mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>, -) { - for (mut attributes, inventory) in &mut query { - let held_item = inventory.held_item(); - - use azalea_registry::Item; - let added_attack_speed = match held_item.kind() { - Item::WoodenSword => -2.4, - Item::WoodenShovel => -3.0, - Item::WoodenPickaxe => -2.8, - Item::WoodenAxe => -3.2, - Item::WoodenHoe => -3.0, - - Item::StoneSword => -2.4, - Item::StoneShovel => -3.0, - Item::StonePickaxe => -2.8, - Item::StoneAxe => -3.2, - Item::StoneHoe => -2.0, - - Item::GoldenSword => -2.4, - Item::GoldenShovel => -3.0, - Item::GoldenPickaxe => -2.8, - Item::GoldenAxe => -3.0, - Item::GoldenHoe => -3.0, - - Item::IronSword => -2.4, - Item::IronShovel => -3.0, - Item::IronPickaxe => -2.8, - Item::IronAxe => -3.1, - Item::IronHoe => -1.0, - - Item::DiamondSword => -2.4, - Item::DiamondShovel => -3.0, - Item::DiamondPickaxe => -2.8, - Item::DiamondAxe => -3.0, - Item::DiamondHoe => 0.0, - - Item::NetheriteSword => -2.4, - Item::NetheriteShovel => -3.0, - Item::NetheritePickaxe => -2.8, - Item::NetheriteAxe => -3.0, - Item::NetheriteHoe => 0.0, - - Item::Trident => -2.9, - _ => 0., - }; - attributes - .attack_speed - .insert(azalea_entity::attributes::base_attack_speed_modifier( - added_attack_speed, - )); - } -} |
