diff options
Diffstat (limited to 'azalea-client/src/plugins/interact.rs')
| -rw-r--r-- | azalea-client/src/plugins/interact.rs | 370 |
1 files changed, 370 insertions, 0 deletions
diff --git a/azalea-client/src/plugins/interact.rs b/azalea-client/src/plugins/interact.rs new file mode 100644 index 00000000..1a344cc8 --- /dev/null +++ b/azalea-client/src/plugins/interact.rs @@ -0,0 +1,370 @@ +use std::ops::AddAssign; + +use azalea_block::BlockState; +use azalea_core::{ + block_hit_result::BlockHitResult, + direction::Direction, + game_type::GameMode, + position::{BlockPos, Vec3}, +}; +use azalea_entity::{ + Attributes, EyeHeight, LocalEntity, LookDirection, Position, clamp_look_direction, view_vector, +}; +use azalea_inventory::{ItemStack, ItemStackData, components}; +use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType}; +use azalea_protocol::packets::game::{ + s_interact::InteractionHand, + s_swing::ServerboundSwing, + s_use_item_on::{BlockHit, ServerboundUseItemOn}, +}; +use azalea_world::{Instance, InstanceContainer, InstanceName}; +use bevy_app::{App, Plugin, Update}; +use bevy_ecs::{ + component::Component, + entity::Entity, + event::{Event, EventReader, EventWriter}, + query::{Changed, With}, + schedule::IntoSystemConfigs, + system::{Commands, Query, Res}, +}; +use derive_more::{Deref, DerefMut}; +use tracing::warn; + +use super::packet::game::handle_outgoing_packets; +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::<BlockInteractEvent>() + .add_event::<SwingArmEvent>() + .add_systems( + Update, + ( + ( + update_hit_result_component.after(clamp_look_direction), + handle_block_interact_event, + handle_swing_arm_event, + ) + .before(handle_outgoing_packets) + .after(InventorySet) + .after(perform_respawn) + .after(handle_attack_event) + .chain(), + update_modifiers_for_held_item + .after(InventorySet) + .after(MoveEventsSet), + ), + ); + } +} + +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). +#[derive(Event)] +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)] +pub struct CurrentSequenceNumber(u32); + +impl AddAssign<u32> for CurrentSequenceNumber { + fn add_assign(&mut self, rhs: u32) { + self.0 += rhs; + } +} + +/// A component that contains the block that the player is currently looking at. +#[derive(Component, Clone, Debug, Deref, DerefMut)] +pub struct HitResultComponent(BlockHitResult); + +pub fn handle_block_interact_event( + mut events: EventReader<BlockInteractEvent>, + mut query: Query<(Entity, &mut CurrentSequenceNumber, &HitResultComponent)>, + mut send_packet_events: EventWriter<SendPacketEvent>, +) { + for event in events.read() { + let Ok((entity, mut sequence_number, hit_result)) = query.get_mut(event.entity) else { + warn!("Sent BlockInteractEvent for entity that doesn't have the required components"); + 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, + world_border: hit_result.world_border, + } + } 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, + world_border: false, + } + }; + + send_packet_events.send(SendPacketEvent::new( + entity, + ServerboundUseItemOn { + hand: InteractionHand::MainHand, + block_hit, + sequence: sequence_number.0, + }, + )); + } +} + +#[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 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, + 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)] +pub struct SwingArmEvent { + pub entity: Entity, +} +pub fn handle_swing_arm_event( + mut events: EventReader<SwingArmEvent>, + mut send_packet_events: EventWriter<SendPacketEvent>, +) { + for event in events.read() { + send_packet_events.send(SendPacketEvent::new( + event.entity, + ServerboundSwing { + hand: InteractionHand::MainHand, + }, + )); + } +} + +#[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, + )); + } +} |
