mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 06:16:04 +00:00
* start updating to 1.21.4 * fix block codegen and stop using block data from burger * rename packet related modules and structs to be simpler * ItemSlot -> ItemStack for more consistency with mojmap * .get() -> .into_packet() * simplify declare_state_packets by removing packet ids * rename read_from and write_into to azalea_read and azalea_write * rename McBufReadable and McBufWritable to AzaleaRead and AzaleaWrite * McBuf -> AzBuf * remove most uses of into_variant * update codegen and use resourcelocation names for packets * implement #[limit(i)] attribute for AzBuf derive macro * fixes for 1.21.4 * fix examples * update some physics code and fix ChatType * remove unused imports in codegen * re-add some things to migrate.py and update +mc version numbers automatically * downgrade to 1.21.3 lol
643 lines
21 KiB
Rust
643 lines
21 KiB
Rust
use azalea_block::{Block, BlockState, FluidState};
|
|
use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick};
|
|
use azalea_entity::{mining::get_mine_progress, FluidOnEyes, Physics};
|
|
use azalea_inventory::ItemStack;
|
|
use azalea_physics::PhysicsSet;
|
|
use azalea_protocol::packets::game::s_player_action::{self, ServerboundPlayerAction};
|
|
use azalea_world::{InstanceContainer, InstanceName};
|
|
use bevy_app::{App, Plugin, Update};
|
|
use bevy_ecs::prelude::*;
|
|
use derive_more::{Deref, DerefMut};
|
|
|
|
use crate::{
|
|
interact::{
|
|
can_use_game_master_blocks, check_is_interaction_restricted, CurrentSequenceNumber,
|
|
HitResultComponent, SwingArmEvent,
|
|
},
|
|
inventory::{Inventory, InventorySet},
|
|
local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
|
|
movement::MoveEventsSet,
|
|
packet_handling::game::SendPacketEvent,
|
|
Client,
|
|
};
|
|
|
|
/// A plugin that allows clients to break blocks in the world.
|
|
pub struct MinePlugin;
|
|
impl Plugin for MinePlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.add_event::<StartMiningBlockEvent>()
|
|
.add_event::<StartMiningBlockWithDirectionEvent>()
|
|
.add_event::<FinishMiningBlockEvent>()
|
|
.add_event::<StopMiningBlockEvent>()
|
|
.add_event::<MineBlockProgressEvent>()
|
|
.add_event::<AttackBlockEvent>()
|
|
.add_systems(
|
|
GameTick,
|
|
(continue_mining_block, handle_auto_mine)
|
|
.chain()
|
|
.before(PhysicsSet),
|
|
)
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
handle_start_mining_block_event,
|
|
handle_start_mining_block_with_direction_event,
|
|
handle_finish_mining_block_event,
|
|
handle_stop_mining_block_event,
|
|
)
|
|
.chain()
|
|
.in_set(MiningSet)
|
|
.after(InventorySet)
|
|
.after(MoveEventsSet)
|
|
.before(azalea_entity::update_bounding_box)
|
|
.after(azalea_entity::update_fluid_on_eyes)
|
|
.after(crate::interact::update_hit_result_component)
|
|
.after(crate::attack::handle_attack_event)
|
|
.after(crate::interact::handle_block_interact_event)
|
|
.before(crate::interact::handle_swing_arm_event),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
|
|
pub struct MiningSet;
|
|
|
|
impl Client {
|
|
pub fn start_mining(&mut self, position: BlockPos) {
|
|
self.ecs.lock().send_event(StartMiningBlockEvent {
|
|
entity: self.entity,
|
|
position,
|
|
});
|
|
}
|
|
|
|
/// When enabled, the bot will mine any block that it is looking at if it is
|
|
/// reachable.
|
|
pub fn left_click_mine(&self, enabled: bool) {
|
|
let mut ecs = self.ecs.lock();
|
|
let mut entity_mut = ecs.entity_mut(self.entity);
|
|
|
|
if enabled {
|
|
entity_mut.insert(LeftClickMine);
|
|
} else {
|
|
entity_mut.remove::<LeftClickMine>();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A component that simulates the client holding down left click to mine the
|
|
/// block that it's facing, but this only interacts with blocks and not
|
|
/// entities.
|
|
#[derive(Component)]
|
|
pub struct LeftClickMine;
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
fn handle_auto_mine(
|
|
mut query: Query<
|
|
(
|
|
&HitResultComponent,
|
|
Entity,
|
|
Option<&Mining>,
|
|
&Inventory,
|
|
&MineBlockPos,
|
|
&MineItem,
|
|
),
|
|
With<LeftClickMine>,
|
|
>,
|
|
mut start_mining_block_event: EventWriter<StartMiningBlockEvent>,
|
|
mut stop_mining_block_event: EventWriter<StopMiningBlockEvent>,
|
|
) {
|
|
for (
|
|
hit_result_component,
|
|
entity,
|
|
mining,
|
|
inventory,
|
|
current_mining_pos,
|
|
current_mining_item,
|
|
) in &mut query.iter_mut()
|
|
{
|
|
let block_pos = hit_result_component.block_pos;
|
|
|
|
if (mining.is_none()
|
|
|| !is_same_mining_target(
|
|
block_pos,
|
|
inventory,
|
|
current_mining_pos,
|
|
current_mining_item,
|
|
))
|
|
&& !hit_result_component.miss
|
|
{
|
|
start_mining_block_event.send(StartMiningBlockEvent {
|
|
entity,
|
|
position: block_pos,
|
|
});
|
|
} else if mining.is_some() && hit_result_component.miss {
|
|
stop_mining_block_event.send(StopMiningBlockEvent { entity });
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Information about the block we're currently mining. This is only present if
|
|
/// we're currently mining a block.
|
|
#[derive(Component)]
|
|
pub struct Mining {
|
|
pub pos: BlockPos,
|
|
pub dir: Direction,
|
|
}
|
|
|
|
/// Start mining the block at the given position.
|
|
///
|
|
/// If we're looking at the block then the correct direction will be used,
|
|
/// otherwise it'll be [`Direction::Down`].
|
|
#[derive(Event)]
|
|
pub struct StartMiningBlockEvent {
|
|
pub entity: Entity,
|
|
pub position: BlockPos,
|
|
}
|
|
fn handle_start_mining_block_event(
|
|
mut events: EventReader<StartMiningBlockEvent>,
|
|
mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>,
|
|
mut query: Query<&HitResultComponent>,
|
|
) {
|
|
for event in events.read() {
|
|
let hit_result = query.get_mut(event.entity).unwrap();
|
|
let direction = if hit_result.block_pos == event.position {
|
|
// we're looking at the block
|
|
hit_result.direction
|
|
} else {
|
|
// we're not looking at the block, arbitrary direction
|
|
Direction::Down
|
|
};
|
|
start_mining_events.send(StartMiningBlockWithDirectionEvent {
|
|
entity: event.entity,
|
|
position: event.position,
|
|
direction,
|
|
});
|
|
}
|
|
}
|
|
|
|
#[derive(Event)]
|
|
pub struct StartMiningBlockWithDirectionEvent {
|
|
pub entity: Entity,
|
|
pub position: BlockPos,
|
|
pub direction: Direction,
|
|
}
|
|
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
|
|
fn handle_start_mining_block_with_direction_event(
|
|
mut events: EventReader<StartMiningBlockWithDirectionEvent>,
|
|
mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
|
|
mut send_packet_events: EventWriter<SendPacketEvent>,
|
|
mut attack_block_events: EventWriter<AttackBlockEvent>,
|
|
mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
|
|
mut query: Query<(
|
|
&InstanceName,
|
|
&LocalGameMode,
|
|
&Inventory,
|
|
&FluidOnEyes,
|
|
&Physics,
|
|
Option<&Mining>,
|
|
&mut CurrentSequenceNumber,
|
|
&mut MineDelay,
|
|
&mut MineProgress,
|
|
&mut MineTicks,
|
|
&mut MineItem,
|
|
&mut MineBlockPos,
|
|
)>,
|
|
instances: Res<InstanceContainer>,
|
|
mut commands: Commands,
|
|
) {
|
|
for event in events.read() {
|
|
let (
|
|
instance_name,
|
|
game_mode,
|
|
inventory,
|
|
fluid_on_eyes,
|
|
physics,
|
|
mining,
|
|
mut sequence_number,
|
|
mut mine_delay,
|
|
mut mine_progress,
|
|
mut mine_ticks,
|
|
mut current_mining_item,
|
|
mut current_mining_pos,
|
|
) = query.get_mut(event.entity).unwrap();
|
|
|
|
let instance_lock = instances.get(instance_name).unwrap();
|
|
let instance = instance_lock.read();
|
|
if check_is_interaction_restricted(
|
|
&instance,
|
|
&event.position,
|
|
&game_mode.current,
|
|
inventory,
|
|
) {
|
|
continue;
|
|
}
|
|
// TODO (when world border is implemented): vanilla ignores if the block
|
|
// is outside of the worldborder
|
|
|
|
if game_mode.current == GameMode::Creative {
|
|
*sequence_number += 1;
|
|
finish_mining_events.send(FinishMiningBlockEvent {
|
|
entity: event.entity,
|
|
position: event.position,
|
|
});
|
|
**mine_delay = 5;
|
|
} else if mining.is_none()
|
|
|| !is_same_mining_target(
|
|
event.position,
|
|
inventory,
|
|
¤t_mining_pos,
|
|
¤t_mining_item,
|
|
)
|
|
{
|
|
if mining.is_some() {
|
|
// send a packet to stop mining since we just changed target
|
|
send_packet_events.send(SendPacketEvent::new(
|
|
event.entity,
|
|
ServerboundPlayerAction {
|
|
action: s_player_action::Action::AbortDestroyBlock,
|
|
pos: current_mining_pos
|
|
.expect("IsMining is true so MineBlockPos must be present"),
|
|
direction: event.direction,
|
|
sequence: 0,
|
|
},
|
|
));
|
|
}
|
|
|
|
let target_block_state = instance
|
|
.get_block_state(&event.position)
|
|
.unwrap_or_default();
|
|
*sequence_number += 1;
|
|
let target_registry_block = azalea_registry::Block::from(target_block_state);
|
|
|
|
// we can't break blocks if they don't have a bounding box
|
|
|
|
// TODO: So right now azalea doesn't differenciate between different types of
|
|
// bounding boxes. See ClipContext::block_shape for more info. Ideally this
|
|
// should just call ClipContext::block_shape and check if it's empty.
|
|
let block_is_solid = !target_block_state.is_air()
|
|
// this is a hack to make sure we can't break water or lava
|
|
&& !matches!(
|
|
target_registry_block,
|
|
azalea_registry::Block::Water | azalea_registry::Block::Lava
|
|
);
|
|
|
|
if block_is_solid && **mine_progress == 0. {
|
|
// interact with the block (like note block left click) here
|
|
attack_block_events.send(AttackBlockEvent {
|
|
entity: event.entity,
|
|
position: event.position,
|
|
});
|
|
}
|
|
|
|
let block = Box::<dyn Block>::from(target_block_state);
|
|
|
|
let held_item = inventory.held_item();
|
|
|
|
if block_is_solid
|
|
&& get_mine_progress(
|
|
block.as_ref(),
|
|
held_item.kind(),
|
|
&inventory.inventory_menu,
|
|
fluid_on_eyes,
|
|
physics,
|
|
) >= 1.
|
|
{
|
|
// block was broken instantly
|
|
finish_mining_events.send(FinishMiningBlockEvent {
|
|
entity: event.entity,
|
|
position: event.position,
|
|
});
|
|
} else {
|
|
commands.entity(event.entity).insert(Mining {
|
|
pos: event.position,
|
|
dir: event.direction,
|
|
});
|
|
**current_mining_pos = Some(event.position);
|
|
**current_mining_item = held_item;
|
|
**mine_progress = 0.;
|
|
**mine_ticks = 0.;
|
|
mine_block_progress_events.send(MineBlockProgressEvent {
|
|
entity: event.entity,
|
|
position: event.position,
|
|
destroy_stage: mine_progress.destroy_stage(),
|
|
});
|
|
}
|
|
|
|
send_packet_events.send(SendPacketEvent::new(
|
|
event.entity,
|
|
ServerboundPlayerAction {
|
|
action: s_player_action::Action::StartDestroyBlock,
|
|
pos: event.position,
|
|
direction: event.direction,
|
|
sequence: **sequence_number,
|
|
},
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Event)]
|
|
pub struct MineBlockProgressEvent {
|
|
pub entity: Entity,
|
|
pub position: BlockPos,
|
|
pub destroy_stage: Option<u32>,
|
|
}
|
|
|
|
/// A player left clicked on a block, used for stuff like interacting with note
|
|
/// blocks.
|
|
#[derive(Event)]
|
|
pub struct AttackBlockEvent {
|
|
pub entity: Entity,
|
|
pub position: BlockPos,
|
|
}
|
|
|
|
/// Returns whether the block and item are still the same as when we started
|
|
/// mining.
|
|
fn is_same_mining_target(
|
|
target_block: BlockPos,
|
|
inventory: &Inventory,
|
|
current_mining_pos: &MineBlockPos,
|
|
current_mining_item: &MineItem,
|
|
) -> bool {
|
|
let held_item = inventory.held_item();
|
|
Some(target_block) == current_mining_pos.0 && held_item == current_mining_item.0
|
|
}
|
|
|
|
/// A component bundle for players that can mine blocks.
|
|
#[derive(Bundle, Default)]
|
|
pub struct MineBundle {
|
|
pub delay: MineDelay,
|
|
pub progress: MineProgress,
|
|
pub ticks: MineTicks,
|
|
pub mining_pos: MineBlockPos,
|
|
pub mine_item: MineItem,
|
|
}
|
|
|
|
/// A component that counts down until we start mining the next block.
|
|
#[derive(Component, Debug, Default, Deref, DerefMut)]
|
|
pub struct MineDelay(pub u32);
|
|
|
|
/// A component that stores the progress of the current mining operation. This
|
|
/// is a value between 0 and 1.
|
|
#[derive(Component, Debug, Default, Deref, DerefMut)]
|
|
pub struct MineProgress(pub f32);
|
|
|
|
impl MineProgress {
|
|
pub fn destroy_stage(&self) -> Option<u32> {
|
|
if self.0 > 0. {
|
|
Some((self.0 * 10.) as u32)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A component that stores the number of ticks that we've been mining the same
|
|
/// block for. This is a float even though it should only ever be a round
|
|
/// number.
|
|
#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
|
|
pub struct MineTicks(pub f32);
|
|
|
|
/// A component that stores the position of the block we're currently mining.
|
|
#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
|
|
pub struct MineBlockPos(pub Option<BlockPos>);
|
|
|
|
/// A component that contains the item we're currently using to mine. If we're
|
|
/// not mining anything, it'll be [`ItemStack::Empty`].
|
|
#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
|
|
pub struct MineItem(pub ItemStack);
|
|
|
|
/// Sent when we completed mining a block.
|
|
#[derive(Event)]
|
|
pub struct FinishMiningBlockEvent {
|
|
pub entity: Entity,
|
|
pub position: BlockPos,
|
|
}
|
|
|
|
pub fn handle_finish_mining_block_event(
|
|
mut events: EventReader<FinishMiningBlockEvent>,
|
|
mut query: Query<(
|
|
&InstanceName,
|
|
&LocalGameMode,
|
|
&Inventory,
|
|
&PlayerAbilities,
|
|
&PermissionLevel,
|
|
&mut CurrentSequenceNumber,
|
|
)>,
|
|
instances: Res<InstanceContainer>,
|
|
) {
|
|
for event in events.read() {
|
|
let (instance_name, game_mode, inventory, abilities, permission_level, _sequence_number) =
|
|
query.get_mut(event.entity).unwrap();
|
|
let instance_lock = instances.get(instance_name).unwrap();
|
|
let instance = instance_lock.read();
|
|
if check_is_interaction_restricted(
|
|
&instance,
|
|
&event.position,
|
|
&game_mode.current,
|
|
inventory,
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if game_mode.current == GameMode::Creative {
|
|
let held_item = inventory.held_item().kind();
|
|
if matches!(
|
|
held_item,
|
|
azalea_registry::Item::Trident | azalea_registry::Item::DebugStick
|
|
) || azalea_registry::tags::items::SWORDS.contains(&held_item)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
let Some(block_state) = instance.get_block_state(&event.position) else {
|
|
continue;
|
|
};
|
|
|
|
let registry_block = Box::<dyn Block>::from(block_state).as_registry_block();
|
|
if !can_use_game_master_blocks(abilities, permission_level)
|
|
&& matches!(
|
|
registry_block,
|
|
azalea_registry::Block::CommandBlock | azalea_registry::Block::StructureBlock
|
|
)
|
|
{
|
|
continue;
|
|
}
|
|
if block_state == BlockState::AIR {
|
|
continue;
|
|
}
|
|
|
|
// when we break a waterlogged block we want to keep the water there
|
|
let fluid_state = FluidState::from(block_state);
|
|
let block_state_for_fluid = BlockState::from(fluid_state);
|
|
instance.set_block_state(&event.position, block_state_for_fluid);
|
|
}
|
|
}
|
|
|
|
/// Abort mining a block.
|
|
#[derive(Event)]
|
|
pub struct StopMiningBlockEvent {
|
|
pub entity: Entity,
|
|
}
|
|
pub fn handle_stop_mining_block_event(
|
|
mut events: EventReader<StopMiningBlockEvent>,
|
|
mut send_packet_events: EventWriter<SendPacketEvent>,
|
|
mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
|
|
mut query: Query<(&mut Mining, &MineBlockPos, &mut MineProgress)>,
|
|
mut commands: Commands,
|
|
) {
|
|
for event in events.read() {
|
|
let (mut _mining, mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap();
|
|
|
|
let mine_block_pos =
|
|
mine_block_pos.expect("IsMining is true so MineBlockPos must be present");
|
|
send_packet_events.send(SendPacketEvent::new(
|
|
event.entity,
|
|
ServerboundPlayerAction {
|
|
action: s_player_action::Action::AbortDestroyBlock,
|
|
pos: mine_block_pos,
|
|
direction: Direction::Down,
|
|
sequence: 0,
|
|
},
|
|
));
|
|
commands.entity(event.entity).remove::<Mining>();
|
|
**mine_progress = 0.;
|
|
mine_block_progress_events.send(MineBlockProgressEvent {
|
|
entity: event.entity,
|
|
position: mine_block_pos,
|
|
destroy_stage: None,
|
|
});
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
|
|
pub fn continue_mining_block(
|
|
mut query: Query<(
|
|
Entity,
|
|
&InstanceName,
|
|
&LocalGameMode,
|
|
&Inventory,
|
|
&MineBlockPos,
|
|
&MineItem,
|
|
&FluidOnEyes,
|
|
&Physics,
|
|
&Mining,
|
|
&mut MineDelay,
|
|
&mut MineProgress,
|
|
&mut MineTicks,
|
|
&mut CurrentSequenceNumber,
|
|
)>,
|
|
mut send_packet_events: EventWriter<SendPacketEvent>,
|
|
mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
|
|
mut finish_mining_events: EventWriter<FinishMiningBlockEvent>,
|
|
mut start_mining_events: EventWriter<StartMiningBlockWithDirectionEvent>,
|
|
mut swing_arm_events: EventWriter<SwingArmEvent>,
|
|
instances: Res<InstanceContainer>,
|
|
mut commands: Commands,
|
|
) {
|
|
for (
|
|
entity,
|
|
instance_name,
|
|
game_mode,
|
|
inventory,
|
|
current_mining_pos,
|
|
current_mining_item,
|
|
fluid_on_eyes,
|
|
physics,
|
|
mining,
|
|
mut mine_delay,
|
|
mut mine_progress,
|
|
mut mine_ticks,
|
|
mut sequence_number,
|
|
) in query.iter_mut()
|
|
{
|
|
if **mine_delay > 0 {
|
|
**mine_delay -= 1;
|
|
continue;
|
|
}
|
|
|
|
if game_mode.current == GameMode::Creative {
|
|
// TODO: worldborder check
|
|
**mine_delay = 5;
|
|
finish_mining_events.send(FinishMiningBlockEvent {
|
|
entity,
|
|
position: mining.pos,
|
|
});
|
|
*sequence_number += 1;
|
|
send_packet_events.send(SendPacketEvent::new(
|
|
entity,
|
|
ServerboundPlayerAction {
|
|
action: s_player_action::Action::StartDestroyBlock,
|
|
pos: mining.pos,
|
|
direction: mining.dir,
|
|
sequence: **sequence_number,
|
|
},
|
|
));
|
|
swing_arm_events.send(SwingArmEvent { entity });
|
|
} else if is_same_mining_target(
|
|
mining.pos,
|
|
inventory,
|
|
current_mining_pos,
|
|
current_mining_item,
|
|
) {
|
|
let instance_lock = instances.get(instance_name).unwrap();
|
|
let instance = instance_lock.read();
|
|
let target_block_state = instance.get_block_state(&mining.pos).unwrap_or_default();
|
|
|
|
if target_block_state.is_air() {
|
|
commands.entity(entity).remove::<Mining>();
|
|
continue;
|
|
}
|
|
let block = Box::<dyn Block>::from(target_block_state);
|
|
**mine_progress += get_mine_progress(
|
|
block.as_ref(),
|
|
current_mining_item.kind(),
|
|
&inventory.inventory_menu,
|
|
fluid_on_eyes,
|
|
physics,
|
|
);
|
|
|
|
if **mine_ticks % 4. == 0. {
|
|
// vanilla makes a mining sound here
|
|
}
|
|
**mine_ticks += 1.;
|
|
|
|
if **mine_progress >= 1. {
|
|
commands.entity(entity).remove::<Mining>();
|
|
*sequence_number += 1;
|
|
finish_mining_events.send(FinishMiningBlockEvent {
|
|
entity,
|
|
position: mining.pos,
|
|
});
|
|
send_packet_events.send(SendPacketEvent::new(
|
|
entity,
|
|
ServerboundPlayerAction {
|
|
action: s_player_action::Action::StopDestroyBlock,
|
|
pos: mining.pos,
|
|
direction: mining.dir,
|
|
sequence: **sequence_number,
|
|
},
|
|
));
|
|
**mine_progress = 0.;
|
|
**mine_ticks = 0.;
|
|
**mine_delay = 0;
|
|
}
|
|
|
|
mine_block_progress_events.send(MineBlockProgressEvent {
|
|
entity,
|
|
position: mining.pos,
|
|
destroy_stage: mine_progress.destroy_stage(),
|
|
});
|
|
swing_arm_events.send(SwingArmEvent { entity });
|
|
} else {
|
|
start_mining_events.send(StartMiningBlockWithDirectionEvent {
|
|
entity,
|
|
position: mining.pos,
|
|
direction: mining.dir,
|
|
});
|
|
}
|
|
|
|
swing_arm_events.send(SwingArmEvent { entity });
|
|
}
|
|
}
|