pub mod pick; 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, LocalEntity, LookDirection, PlayerAbilities, Position, attributes::{ creative_block_interaction_range_modifier, creative_entity_interaction_range_modifier, }, clamp_look_direction, indexing::EntityIdIndex, inventory::Inventory, }; use azalea_inventory::{ItemStack, ItemStackData, components}; use azalea_physics::{ PhysicsSystems, collision::entity_collisions::update_last_bounding_box, local_player::PhysicsState, }; use azalea_protocol::packets::game::{ ServerboundInteract, ServerboundUseItem, s_interact::{self, InteractionHand}, s_swing::ServerboundSwing, s_use_item_on::ServerboundUseItemOn, }; use azalea_registry::builtin::ItemKind; use azalea_world::Instance; use bevy_app::{App, Plugin, Update}; use bevy_ecs::prelude::*; use tracing::warn; use super::mining::Mining; use crate::{ attack::handle_attack_event, interact::pick::{HitResultComponent, update_hit_result_component}, inventory::InventorySystems, local_player::{LocalGameMode, PermissionLevel}, movement::MoveEventsSystems, packet::game::SendGamePacketEvent, 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_message::() .add_systems( Update, ( ( update_attributes_for_held_item, update_attributes_for_gamemode, ) .in_set(UpdateAttributesSystems) .chain(), handle_start_use_item_event, update_hit_result_component .after(clamp_look_direction) .after(update_last_bounding_box), ) .after(InventorySystems) .after(MoveEventsSystems) .after(perform_respawn) .after(handle_attack_event) .chain(), ) .add_systems( GameTick, handle_start_use_item_queued.before(PhysicsSystems), ) .add_observer(handle_entity_interact) .add_observer(handle_swing_arm_trigger); } } #[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)] pub struct UpdateAttributesSystems; /// A component that contains information about our local block state /// predictions. #[derive(Clone, Component, Debug, Default)] pub struct BlockStatePredictionHandler { /// The total number of changes that this client has made to blocks. seq: u32, server_state: HashMap, } #[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); } } } /// 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(Message)] pub struct StartUseItemEvent { pub entity: Entity, pub hand: InteractionHand, /// See [`StartUseItemQueued::force_block`]. pub force_block: Option, } pub fn handle_start_use_item_event( mut commands: Commands, mut events: MessageReader, ) { 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, } #[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::(); 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).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(r) => { let seq = prediction_handler.start_predicting(); if r.miss { commands.trigger(SendGamePacketEvent::new( entity, ServerboundUseItem { hand: start_use_item.hand, seq, x_rot: look_direction.x_rot(), y_rot: look_direction.y_rot(), }, )); } else { commands.trigger(SendGamePacketEvent::new( entity, ServerboundUseItemOn { hand: start_use_item.hand, block_hit: r.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(r) => { commands.trigger(EntityInteractEvent { client: entity, target: r.entity, location: Some(r.location), }); } } } } /// An ECS `Event` that makes the client tell the server that we right-clicked /// an entity. #[derive(Clone, Debug, EntityEvent)] pub struct EntityInteractEvent { #[event_target] pub client: Entity, pub target: Entity, /// The position on the entity that we'll tell the server that we clicked /// on. /// /// This doesn't matter for most entities. If it's set to `None` but we're /// looking at the target, it'll use the correct value. If it's `None` and /// we're not looking at the entity, then it'll arbitrary send the target's /// exact position. pub location: Option, } pub fn handle_entity_interact( trigger: On, mut commands: Commands, client_query: Query<(&PhysicsState, &EntityIdIndex, &HitResultComponent)>, target_query: Query<&Position>, ) { let Some((physics_state, entity_id_index, hit_result)) = client_query.get(trigger.client).ok() else { warn!( "tried to interact with an entity but the client didn't have the required components" ); return; }; // TODO: worldborder check let Some(entity_id) = entity_id_index.get_by_ecs_entity(trigger.target) else { warn!("tried to interact with an entity that isn't known by the client"); return; }; let location = if let Some(l) = trigger.location { l } else { // if we're looking at the entity, use that if let Some(entity_hit_result) = hit_result.as_entity_hit_result() && entity_hit_result.entity == trigger.target { entity_hit_result.location } else { // if we're not looking at the entity, make up a value that's good enough by // using the entity's position let Ok(target_position) = target_query.get(trigger.target) else { warn!("tried to look at an entity without the entity having a position"); return; }; **target_position } }; let mut interact = ServerboundInteract { entity_id, action: s_interact::ActionType::InteractAt { location, hand: InteractionHand::MainHand, }, using_secondary_action: physics_state.trying_to_crouch, }; commands.trigger(SendGamePacketEvent::new(trigger.client, interact.clone())); // TODO: this is true if the interaction failed, which i think can only happen // in certain cases when interacting with armor stands let consumes_action = false; if !consumes_action { // but yes, most of the time vanilla really does send two interact packets like // this interact.action = s_interact::ActionType::Interact { hand: InteractionHand::MainHand, }; commands.trigger(SendGamePacketEvent::new(trigger.client, interact)); } } /// 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.get_component::().is_none() { // 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(Clone, Debug, EntityEvent)] pub struct SwingArmEvent { pub entity: Entity, } pub fn handle_swing_arm_trigger(swing_arm: On, mut commands: Commands) { commands.trigger(SendGamePacketEvent::new( swing_arm.entity, ServerboundSwing { hand: InteractionHand::MainHand, }, )); } #[allow(clippy::type_complexity)] fn update_attributes_for_held_item( mut query: Query<(&mut Attributes, &Inventory), (With, Changed)>, ) { for (mut attributes, inventory) in &mut query { let held_item = inventory.held_item(); let added_attack_speed = added_attack_speed_for_item(held_item.kind()); attributes .attack_speed .insert(azalea_entity::attributes::base_attack_speed_modifier( added_attack_speed, )); } } fn added_attack_speed_for_item(item: ItemKind) -> f64 { match item { ItemKind::WoodenSword => -2.4, ItemKind::WoodenShovel => -3.0, ItemKind::WoodenPickaxe => -2.8, ItemKind::WoodenAxe => -3.2, ItemKind::WoodenHoe => -3.0, ItemKind::StoneSword => -2.4, ItemKind::StoneShovel => -3.0, ItemKind::StonePickaxe => -2.8, ItemKind::StoneAxe => -3.2, ItemKind::StoneHoe => -2.0, ItemKind::GoldenSword => -2.4, ItemKind::GoldenShovel => -3.0, ItemKind::GoldenPickaxe => -2.8, ItemKind::GoldenAxe => -3.0, ItemKind::GoldenHoe => -3.0, ItemKind::IronSword => -2.4, ItemKind::IronShovel => -3.0, ItemKind::IronPickaxe => -2.8, ItemKind::IronAxe => -3.1, ItemKind::IronHoe => -1.0, ItemKind::DiamondSword => -2.4, ItemKind::DiamondShovel => -3.0, ItemKind::DiamondPickaxe => -2.8, ItemKind::DiamondAxe => -3.0, ItemKind::DiamondHoe => 0.0, ItemKind::NetheriteSword => -2.4, ItemKind::NetheriteShovel => -3.0, ItemKind::NetheritePickaxe => -2.8, ItemKind::NetheriteAxe => -3.0, ItemKind::NetheriteHoe => 0.0, ItemKind::Trident => -2.9, _ => 0., } } #[allow(clippy::type_complexity)] fn update_attributes_for_gamemode( query: Query<(&mut Attributes, &LocalGameMode), (With, Changed)>, ) { for (mut attributes, game_mode) in query { if game_mode.current == GameMode::Creative { attributes .block_interaction_range .insert(creative_block_interaction_range_modifier()); attributes .entity_interaction_range .insert(creative_entity_interaction_range_modifier()); } else { attributes .block_interaction_range .remove(&creative_block_interaction_range_modifier().id); attributes .entity_interaction_range .remove(&creative_entity_interaction_range_modifier().id); } } }