aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2023-07-14 22:20:40 -0500
committerGitHub <noreply@github.com>2023-07-14 22:20:40 -0500
commit7405427199e5a994d4a6a706f84434a69cb7a7d9 (patch)
treeca537e5d761bc053187d952fced0915c850b92aa /azalea-client/src
parentd1afd02aa84e7b4450c1607277f078eb2a0f1bf3 (diff)
downloadazalea-drasl-7405427199e5a994d4a6a706f84434a69cb7a7d9.tar.xz
Mining (#95)
* more mining stuff * initialize azalea-tags crate * more mining stuff 2 * mining in ecs * well technically mining works but no codegen for how long it takes to mine each block yet * rename downloads to __cache__ it was bothering me since it's not *just* downloads * codegen block behavior * fix not sending packet to finish breaking block * mining animation 🎉 * clippy * cleanup, move Client::mine into a client extension * add azalea/src/mining.rs --------- Co-authored-by: mat <git@matdoes.dev>
Diffstat (limited to 'azalea-client/src')
-rwxr-xr-xazalea-client/src/account.rs5
-rw-r--r--azalea-client/src/client.rs28
-rw-r--r--azalea-client/src/entity_query.rs6
-rw-r--r--azalea-client/src/events.rs2
-rw-r--r--azalea-client/src/interact.rs88
-rw-r--r--azalea-client/src/inventory.rs25
-rw-r--r--azalea-client/src/lib.rs2
-rw-r--r--azalea-client/src/local_player.rs10
-rw-r--r--azalea-client/src/mining.rs537
-rw-r--r--azalea-client/src/movement.rs31
-rw-r--r--azalea-client/src/packet_handling.rs41
-rwxr-xr-xazalea-client/src/player.rs2
12 files changed, 669 insertions, 108 deletions
diff --git a/azalea-client/src/account.rs b/azalea-client/src/account.rs
index 2d1b766c..0e67a79a 100755
--- a/azalea-client/src/account.rs
+++ b/azalea-client/src/account.rs
@@ -187,7 +187,10 @@ impl Account {
*access_token_mutex.lock() = new_access_token;
let AccountOpts::MicrosoftWithAccessToken { msa: new_msa } =
- new_account.account_opts else { unreachable!() };
+ new_account.account_opts
+ else {
+ unreachable!()
+ };
*msa.lock() = new_msa.lock().clone();
Ok(())
diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs
index 725cd9f3..0b1fccc1 100644
--- a/azalea-client/src/client.rs
+++ b/azalea-client/src/client.rs
@@ -8,6 +8,7 @@ use crate::{
death_event, handle_send_packet_event, update_in_loaded_chunk, GameProfileComponent,
LocalPlayer, PhysicsState, SendPacketEvent,
},
+ mining::{self, MinePlugin},
movement::{LastSentLookDirection, PlayerMovePlugin},
packet_handling::{self, PacketHandlerPlugin, PacketReceiver},
player::retroactively_add_game_profile_component,
@@ -19,6 +20,7 @@ use crate::{
use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError};
use azalea_chat::FormattedText;
use azalea_core::Vec3;
+use azalea_entity::{EntityPlugin, EntityUpdateSet, Local, Position};
use azalea_physics::{PhysicsPlugin, PhysicsSet};
use azalea_protocol::{
connect::{Connection, ConnectionError},
@@ -41,10 +43,7 @@ use azalea_protocol::{
},
resolver, ServerAddress,
};
-use azalea_world::{
- entity::{EntityPlugin, EntityUpdateSet, Local, Position, WorldName},
- Instance, InstanceContainer, PartialInstance,
-};
+use azalea_world::{Instance, InstanceContainer, InstanceName, PartialInstance};
use bevy_app::{App, FixedUpdate, Main, Plugin, PluginGroup, PluginGroupBuilder, Update};
use bevy_ecs::{
bundle::Bundle,
@@ -132,6 +131,10 @@ impl From<ClientboundPlayerAbilitiesPacket> for PlayerAbilities {
}
}
+/// Level must be 0..=4
+#[derive(Component, Clone, Default, Deref, DerefMut)]
+pub struct PermissionLevel(pub u8);
+
/// A component that contains a map of player UUIDs to their information in the
/// tab list.
///
@@ -301,6 +304,8 @@ impl Client {
current_sequence_number: CurrentSequenceNumber::default(),
last_sent_direction: LastSentLookDirection::default(),
abilities: PlayerAbilities::default(),
+ permission_level: PermissionLevel::default(),
+ mining: mining::MineBundle::default(),
_local: Local,
});
@@ -466,9 +471,9 @@ impl Client {
/// # Examples
///
/// ```
- /// # use azalea_world::entity::WorldName;
+ /// # use azalea_world::InstanceName;
/// # fn example(client: &azalea_client::Client) {
- /// let world_name = client.component::<WorldName>();
+ /// let world_name = client.component::<InstanceName>();
/// # }
pub fn component<T: Component + Clone>(&self) -> T {
self.query::<&T>(&mut self.ecs.lock()).clone()
@@ -486,7 +491,7 @@ impl Client {
/// If the client using a shared world, then the shared world will be a
/// superset of the client's world.
pub fn world(&self) -> Arc<RwLock<Instance>> {
- let world_name = self.component::<WorldName>();
+ let world_name = self.component::<InstanceName>();
let ecs = self.ecs.lock();
let instance_container = ecs.resource::<InstanceContainer>();
instance_container.get(&world_name).unwrap()
@@ -495,7 +500,7 @@ impl Client {
/// Returns whether we have a received the login packet yet.
pub fn logged_in(&self) -> bool {
// the login packet tells us the world name
- self.query::<Option<&WorldName>>(&mut self.ecs.lock())
+ self.query::<Option<&InstanceName>>(&mut self.ecs.lock())
.is_some()
}
@@ -560,6 +565,10 @@ pub struct JoinedClientBundle {
pub current_sequence_number: CurrentSequenceNumber,
pub last_sent_direction: LastSentLookDirection,
pub abilities: PlayerAbilities,
+ pub permission_level: PermissionLevel,
+
+ pub mining: mining::MineBundle,
+
pub _local: Local,
}
@@ -660,7 +669,7 @@ pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::UnboundedSender<(
#[derive(Resource, Deref)]
pub struct TickBroadcast(broadcast::Sender<()>);
-fn send_tick_broadcast(tick_broadcast: ResMut<TickBroadcast>) {
+pub fn send_tick_broadcast(tick_broadcast: ResMut<TickBroadcast>) {
let _ = tick_broadcast.0.send(());
}
/// A plugin that makes the [`RanScheduleBroadcast`] resource available.
@@ -706,6 +715,7 @@ impl PluginGroup for DefaultPlugins {
.add(PlayerMovePlugin)
.add(InteractPlugin)
.add(RespawnPlugin)
+ .add(MinePlugin)
.add(TickBroadcastPlugin);
#[cfg(feature = "log")]
{
diff --git a/azalea-client/src/entity_query.rs b/azalea-client/src/entity_query.rs
index 8fe94659..0320457f 100644
--- a/azalea-client/src/entity_query.rs
+++ b/azalea-client/src/entity_query.rs
@@ -15,10 +15,10 @@ impl Client {
///
/// # Examples
/// ```
- /// # use azalea_world::entity::WorldName;
+ /// # use azalea_world::InstanceName;
/// # fn example(mut client: azalea_client::Client) {
/// let is_logged_in = client
- /// .query::<Option<&WorldName>>(&mut client.ecs.lock())
+ /// .query::<Option<&InstanceName>>(&mut client.ecs.lock())
/// .is_some();
/// # }
/// ```
@@ -39,7 +39,7 @@ impl Client {
/// ```
/// use azalea_client::{Client, GameProfileComponent};
/// use bevy_ecs::query::With;
- /// use azalea_world::entity::{Position, metadata::Player};
+ /// use azalea_entity::{Position, metadata::Player};
///
/// # fn example(mut bot: Client, sender_name: String) {
/// let entity = bot.entity_by::<With<Player>, (&GameProfileComponent,)>(
diff --git a/azalea-client/src/events.rs b/azalea-client/src/events.rs
index e97f3477..900f559f 100644
--- a/azalea-client/src/events.rs
+++ b/azalea-client/src/events.rs
@@ -6,7 +6,7 @@ use std::sync::Arc;
use azalea_protocol::packets::game::{
clientbound_player_combat_kill_packet::ClientboundPlayerCombatKillPacket, ClientboundGamePacket,
};
-use azalea_world::entity::MinecraftEntityId;
+use azalea_world::MinecraftEntityId;
use bevy_app::{App, FixedUpdate, Plugin, Update};
use bevy_ecs::{component::Component, event::EventReader, query::Added, system::Query};
use derive_more::{Deref, DerefMut};
diff --git a/azalea-client/src/interact.rs b/azalea-client/src/interact.rs
index 45b845f9..dc0213b0 100644
--- a/azalea-client/src/interact.rs
+++ b/azalea-client/src/interact.rs
@@ -1,22 +1,22 @@
+use std::ops::AddAssign;
+
use azalea_block::BlockState;
use azalea_core::{BlockHitResult, BlockPos, Direction, GameMode, Vec3};
+use azalea_entity::{clamp_look_direction, view_vector, EyeHeight, LookDirection, Position};
use azalea_inventory::{ItemSlot, ItemSlotData};
use azalea_nbt::NbtList;
use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType};
use azalea_protocol::packets::game::{
serverbound_interact_packet::InteractionHand,
+ serverbound_swing_packet::ServerboundSwingPacket,
serverbound_use_item_on_packet::{BlockHit, ServerboundUseItemOnPacket},
};
-use azalea_world::{
- entity::{clamp_look_direction, view_vector, EyeHeight, LookDirection, Position, WorldName},
- Instance, InstanceContainer,
-};
+use azalea_world::{Instance, InstanceContainer, InstanceName};
use bevy_app::{App, Plugin, Update};
use bevy_ecs::{
component::Component,
entity::Entity,
- event::EventReader,
- prelude::Event,
+ event::{Event, EventReader, EventWriter},
schedule::IntoSystemConfigs,
system::{Commands, Query, Res},
};
@@ -24,8 +24,9 @@ use derive_more::{Deref, DerefMut};
use log::warn;
use crate::{
+ client::{PermissionLevel, PlayerAbilities},
inventory::InventoryComponent,
- local_player::{handle_send_packet_event, LocalGameMode},
+ local_player::{handle_send_packet_event, LocalGameMode, SendPacketEvent},
Client, LocalPlayer,
};
@@ -33,15 +34,18 @@ use crate::{
pub struct InteractPlugin;
impl Plugin for InteractPlugin {
fn build(&self, app: &mut App) {
- app.add_event::<BlockInteractEvent>().add_systems(
- Update,
- (
- update_hit_result_component.after(clamp_look_direction),
- handle_block_interact_event,
- )
- .before(handle_send_packet_event)
- .chain(),
- );
+ 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_send_packet_event)
+ .chain(),
+ );
}
}
@@ -73,9 +77,15 @@ pub struct BlockInteractEvent {
/// A component that contains the number of changes this client has made to
/// blocks.
-#[derive(Component, Copy, Clone, Debug, Default, Deref, DerefMut)]
+#[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);
@@ -89,14 +99,15 @@ pub fn handle_block_interact_event(
)>,
) {
for event in events.iter() {
- let Ok((local_player, mut sequence_number, hit_result)) = query.get_mut(event.entity) else {
+ 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;
+ *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
@@ -143,7 +154,7 @@ fn update_hit_result_component(
&Position,
&EyeHeight,
&LookDirection,
- &WorldName,
+ &InstanceName,
)>,
instance_container: Res<InstanceContainer>,
) {
@@ -246,10 +257,11 @@ pub fn check_block_can_be_broken_by_item_in_adventure_mode(
.nbt
.as_compound()
.and_then(|nbt| nbt.get("tag").and_then(|nbt| nbt.as_compound()))
- .and_then(|nbt| nbt.get("CanDestroy").and_then(|nbt| nbt.as_list())) else {
- // no CanDestroy tag
- return false;
- };
+ .and_then(|nbt| nbt.get("CanDestroy").and_then(|nbt| nbt.as_list()))
+ else {
+ // no CanDestroy tag
+ return false;
+ };
let NbtList::String(_can_destroy) = can_destroy else {
// CanDestroy tag must be a list of strings
@@ -265,3 +277,31 @@ pub fn check_block_can_be_broken_by_item_in_adventure_mode(
// 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,
+}
+fn handle_swing_arm_event(
+ mut events: EventReader<SwingArmEvent>,
+ mut send_packet_events: EventWriter<SendPacketEvent>,
+) {
+ for event in events.iter() {
+ send_packet_events.send(SendPacketEvent {
+ entity: event.entity,
+ packet: ServerboundSwingPacket {
+ hand: InteractionHand::MainHand,
+ }
+ .get(),
+ });
+ }
+}
diff --git a/azalea-client/src/inventory.rs b/azalea-client/src/inventory.rs
index 25ce157e..6f7829de 100644
--- a/azalea-client/src/inventory.rs
+++ b/azalea-client/src/inventory.rs
@@ -101,6 +101,7 @@ pub struct InventoryComponent {
/// the scroll wheel.
pub selected_hotbar_slot: u8,
}
+
impl InventoryComponent {
/// Returns a reference to the currently active menu. If a container is open
/// it'll return [`Self::container_menu`], otherwise
@@ -220,10 +221,10 @@ impl InventoryComponent {
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();
- };
+ 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;
@@ -244,8 +245,8 @@ impl InventoryComponent {
// get the ItemSlotData for the slot
let ItemSlot::Present(slot) = slot else {
- unreachable!("the loop above requires the slot to be present to break")
- };
+ unreachable!("the loop above requires the slot to be present to break")
+ };
// if self.can_drag_to(slot) {
let mut new_carried = carried.clone();
@@ -480,8 +481,8 @@ impl InventoryComponent {
// 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");
- };
+ unreachable!("target slot is not empty but is not present");
+ };
target_slot_item.count += taken_item.count();
}
}
@@ -512,13 +513,13 @@ fn can_item_quick_replace(
ignore_item_count: bool,
) -> bool {
let ItemSlot::Present(target_slot) = target_slot else {
- return false;
- };
+ 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;
- };
+ return false;
+ };
if !item.is_same_item_and_nbt(target_slot) {
return false;
diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs
index c47c5e29..e36cb846 100644
--- a/azalea-client/src/lib.rs
+++ b/azalea-client/src/lib.rs
@@ -21,7 +21,7 @@ mod get_mc_dir;
pub mod interact;
pub mod inventory;
mod local_player;
-mod mining;
+pub mod mining;
mod movement;
pub mod packet_handling;
pub mod ping;
diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs
index 4368c47c..594513db 100644
--- a/azalea-client/src/local_player.rs
+++ b/azalea-client/src/local_player.rs
@@ -2,11 +2,9 @@ use std::{io, sync::Arc};
use azalea_auth::game_profile::GameProfile;
use azalea_core::{ChunkPos, GameMode};
+use azalea_entity::{Dead, Position};
use azalea_protocol::packets::game::ServerboundGamePacket;
-use azalea_world::{
- entity::{self, Dead, WorldName},
- Instance, InstanceContainer, PartialInstance,
-};
+use azalea_world::{Instance, InstanceContainer, InstanceName, PartialInstance};
use bevy_ecs::{
component::Component,
entity::Entity,
@@ -32,7 +30,7 @@ use crate::{
/// You can also use the [`Local`] marker component for queries if you're only
/// checking for a local player and don't need the contents of this component.
///
-/// [`Local`]: azalea_world::entity::Local
+/// [`Local`]: azalea_entity::Local
/// [`Client`]: crate::Client
#[derive(Component)]
pub struct LocalPlayer {
@@ -135,7 +133,7 @@ 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, &WorldName, &entity::Position)>,
+ query: Query<(Entity, &InstanceName, &Position)>,
instance_container: Res<InstanceContainer>,
) {
for (entity, local_player, position) in &query {
diff --git a/azalea-client/src/mining.rs b/azalea-client/src/mining.rs
index 241c4fde..049bc859 100644
--- a/azalea-client/src/mining.rs
+++ b/azalea-client/src/mining.rs
@@ -1,36 +1,543 @@
-use azalea_core::BlockPos;
-use bevy_app::{App, Plugin, Update};
+use azalea_block::{Block, BlockState, FluidState};
+use azalea_core::{BlockPos, Direction, GameMode};
+use azalea_entity::{mining::get_mine_progress, FluidOnEyes, Physics};
+use azalea_inventory::ItemSlot;
+use azalea_protocol::packets::game::serverbound_player_action_packet::{
+ self, ServerboundPlayerActionPacket,
+};
+use azalea_world::{InstanceContainer, InstanceName};
+use bevy_app::{App, FixedUpdate, Plugin, Update};
use bevy_ecs::prelude::*;
+use derive_more::{Deref, DerefMut};
-use crate::Client;
+use crate::{
+ client::{PermissionLevel, PlayerAbilities},
+ interact::{
+ can_use_game_master_blocks, check_is_interaction_restricted, CurrentSequenceNumber,
+ HitResultComponent, SwingArmEvent,
+ },
+ inventory::InventoryComponent,
+ local_player::{LocalGameMode, SendPacketEvent},
+};
/// 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_systems(Update, handle_start_mining_block_event);
+ .add_event::<StartMiningBlockWithDirectionEvent>()
+ .add_event::<FinishMiningBlockEvent>()
+ .add_event::<StopMiningBlockEvent>()
+ .add_event::<MineBlockProgressEvent>()
+ .add_event::<AttackBlockEvent>()
+ .add_systems(FixedUpdate, continue_mining_block)
+ .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(),
+ );
}
}
-impl Client {
- /// Start mining a block.
- pub fn start_mining_block(&self, position: BlockPos) {
- self.ecs.lock().send_event(StartMiningBlockEvent {
- entity: self.entity,
- position,
+/// 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.iter() {
+ 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 StartMiningBlockEvent {
+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,
+ &InventoryComponent,
+ &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.iter() {
+ 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,
+ &current_mining_pos,
+ &current_mining_item,
+ )
+ {
+ if mining.is_some() {
+ // send a packet to stop mining since we just changed target
+ send_packet_events.send(SendPacketEvent {
+ entity: event.entity,
+ packet: ServerboundPlayerActionPacket {
+ action: serverbound_player_action_packet::Action::AbortDestroyBlock,
+ pos: current_mining_pos
+ .expect("IsMining is true so MineBlockPos must be present"),
+ direction: event.direction,
+ sequence: 0,
+ }
+ .get(),
+ });
+ }
+
+ let target_block_state = instance
+ .get_block_state(&event.position)
+ .unwrap_or_default();
+ *sequence_number += 1;
+ let block_is_solid = !target_block_state.is_air();
+ 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 {
+ entity: event.entity,
+ packet: ServerboundPlayerActionPacket {
+ action: serverbound_player_action_packet::Action::StartDestroyBlock,
+ pos: event.position,
+ direction: event.direction,
+ sequence: **sequence_number,
+ }
+ .get(),
+ });
+ }
+ }
+}
+
+#[derive(Event)]
+pub struct MineBlockProgressEvent {
pub entity: Entity,
pub position: BlockPos,
+ pub destroy_stage: Option<u32>,
}
-fn handle_start_mining_block_event(mut _events: EventReader<StartMiningBlockEvent>) {
- // for event in events.iter() {
- // //
- // }
+/// 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: &InventoryComponent,
+ 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, Debug, Default, Deref, DerefMut)]
+pub struct MineTicks(pub f32);
+
+/// A component that stores the position of the block we're currently mining.
+#[derive(Component, 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 [`ItemSlot::Empty`].
+#[derive(Component, Debug, Default, Deref, DerefMut)]
+pub struct MineItem(pub ItemSlot);
+
+/// Sent when we completed mining a block.
+#[derive(Event)]
+pub struct FinishMiningBlockEvent {
+ pub entity: Entity,
+ pub position: BlockPos,
+}
+
+fn handle_finish_mining_block_event(
+ mut events: EventReader<FinishMiningBlockEvent>,
+ mut query: Query<(
+ &InstanceName,
+ &LocalGameMode,
+ &InventoryComponent,
+ &PlayerAbilities,
+ &PermissionLevel,
+ &mut CurrentSequenceNumber,
+ )>,
+ instances: Res<InstanceContainer>,
+) {
+ for event in events.iter() {
+ 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,
+}
+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.iter() {
+ 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 {
+ entity: event.entity,
+ packet: ServerboundPlayerActionPacket {
+ action: serverbound_player_action_packet::Action::AbortDestroyBlock,
+ pos: mine_block_pos,
+ direction: Direction::Down,
+ sequence: 0,
+ }
+ .get(),
+ });
+ 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)]
+fn continue_mining_block(
+ mut query: Query<(
+ Entity,
+ &InstanceName,
+ &LocalGameMode,
+ &InventoryComponent,
+ &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 {
+ entity,
+ packet: ServerboundPlayerActionPacket {
+ action: serverbound_player_action_packet::Action::StartDestroyBlock,
+ pos: mining.pos,
+ direction: mining.dir,
+ sequence: **sequence_number,
+ }
+ .get(),
+ });
+ 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 {
+ entity,
+ packet: ServerboundPlayerActionPacket {
+ action: serverbound_player_action_packet::Action::StopDestroyBlock,
+ pos: mining.pos,
+ direction: mining.dir,
+ sequence: **sequence_number,
+ }
+ .get(),
+ });
+ **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 });
+ }
}
diff --git a/azalea-client/src/movement.rs b/azalea-client/src/movement.rs
index 2db392b4..0bda9b15 100644
--- a/azalea-client/src/movement.rs
+++ b/azalea-client/src/movement.rs
@@ -2,6 +2,8 @@ use crate::client::Client;
use crate::local_player::{
update_in_loaded_chunk, LocalPlayer, LocalPlayerInLoadedChunk, PhysicsState,
};
+use azalea_entity::{metadata::Sprinting, Attributes, Jumping};
+use azalea_entity::{LastSentPosition, LookDirection, Physics, Position};
use azalea_physics::{force_jump_listener, PhysicsSet};
use azalea_protocol::packets::game::serverbound_player_command_packet::ServerboundPlayerCommandPacket;
use azalea_protocol::packets::game::{
@@ -10,10 +12,7 @@ use azalea_protocol::packets::game::{
serverbound_move_player_rot_packet::ServerboundMovePlayerRotPacket,
serverbound_move_player_status_only_packet::ServerboundMovePlayerStatusOnlyPacket,
};
-use azalea_world::{
- entity::{self, metadata::Sprinting, Attributes, Jumping, MinecraftEntityId},
- MoveEntityError,
-};
+use azalea_world::{MinecraftEntityId, MoveEntityError};
use bevy_app::{App, FixedUpdate, Plugin, Update};
use bevy_ecs::prelude::Event;
use bevy_ecs::{
@@ -89,7 +88,7 @@ impl Client {
/// `y_rot` goes from -180 to 180, and `x_rot` goes from -90 to 90.
pub fn set_direction(&mut self, y_rot: f32, x_rot: f32) {
let mut ecs = self.ecs.lock();
- let mut look_direction = self.query::<&mut entity::LookDirection>(&mut ecs);
+ let mut look_direction = self.query::<&mut LookDirection>(&mut ecs);
(look_direction.y_rot, look_direction.x_rot) = (y_rot, x_rot);
}
@@ -110,12 +109,12 @@ pub(crate) fn send_position(
&MinecraftEntityId,
&mut LocalPlayer,
&mut PhysicsState,
- &entity::Position,
- &mut entity::LastSentPosition,
- &mut entity::Physics,
- &entity::LookDirection,
+ &Position,
+ &mut LastSentPosition,
+ &mut Physics,
+ &LookDirection,
&mut LastSentLookDirection,
- &entity::metadata::Sprinting,
+ &Sprinting,
),
&LocalPlayerInLoadedChunk,
>,
@@ -223,7 +222,7 @@ impl LocalPlayer {
fn send_sprinting_if_needed(
&mut self,
id: &MinecraftEntityId,
- sprinting: &entity::metadata::Sprinting,
+ sprinting: &Sprinting,
physics_state: &mut PhysicsState,
) {
let was_sprinting = physics_state.was_sprinting;
@@ -287,9 +286,9 @@ pub fn local_player_ai_step(
mut query: Query<
(
&mut PhysicsState,
- &mut entity::Physics,
- &mut entity::metadata::Sprinting,
- &mut entity::Attributes,
+ &mut Physics,
+ &mut Sprinting,
+ &mut Attributes,
),
With<LocalPlayerInLoadedChunk>,
>,
@@ -431,12 +430,12 @@ fn set_sprinting(
if sprinting {
attributes
.speed
- .insert(entity::attributes::sprinting_modifier())
+ .insert(azalea_entity::attributes::sprinting_modifier())
.is_ok()
} else {
attributes
.speed
- .remove(&entity::attributes::sprinting_modifier().uuid)
+ .remove(&azalea_entity::attributes::sprinting_modifier().uuid)
.is_none()
}
}
diff --git a/azalea-client/src/packet_handling.rs b/azalea-client/src/packet_handling.rs
index 2371d834..fd8d77e5 100644
--- a/azalea-client/src/packet_handling.rs
+++ b/azalea-client/src/packet_handling.rs
@@ -1,6 +1,11 @@
use std::{collections::HashSet, io::Cursor, sync::Arc};
use azalea_core::{ChunkPos, GameMode, ResourceLocation, Vec3};
+use azalea_entity::{
+ metadata::{apply_metadata, Health, PlayerMetadataBundle},
+ Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition, LoadedBy, LookDirection,
+ Physics, PlayerBundle, Position, RelativeEntityUpdate,
+};
use azalea_protocol::{
connect::{ReadConnection, WriteConnection},
packets::game::{
@@ -15,15 +20,7 @@ use azalea_protocol::{
},
read::ReadPacketError,
};
-use azalea_world::{
- entity::{
- metadata::{apply_metadata, Health, PlayerMetadataBundle},
- Dead, EntityBundle, EntityKind, EntityUpdateSet, LastSentPosition, LookDirection,
- MinecraftEntityId, Physics, PlayerBundle, Position, WorldName,
- },
- entity::{LoadedBy, RelativeEntityUpdate},
- InstanceContainer, PartialInstance,
-};
+use azalea_world::{InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance};
use bevy_app::{App, First, Plugin, PreUpdate};
use bevy_ecs::{
component::Component,
@@ -195,7 +192,7 @@ fn process_packet_events(ecs: &mut World) {
Commands,
Query<(
&mut LocalPlayer,
- Option<&mut WorldName>,
+ Option<&mut InstanceName>,
&GameProfileComponent,
&ClientInformation,
)>,
@@ -225,7 +222,7 @@ fn process_packet_events(ecs: &mut World) {
} else {
commands
.entity(player_entity)
- .insert(WorldName(new_world_name.clone()));
+ .insert(InstanceName(new_world_name.clone()));
}
// add this world to the instance_container (or don't if it's already
// there)
@@ -348,10 +345,16 @@ fn process_packet_events(ecs: &mut World) {
)>,
> = SystemState::new(ecs);
let mut query = system_state.get_mut(ecs);
- let Ok((local_player, mut physics, mut direction, mut position, mut last_sent_position)) =
- query.get_mut(player_entity) else {
- continue;
- };
+ let Ok((
+ local_player,
+ mut physics,
+ mut direction,
+ mut position,
+ mut last_sent_position,
+ )) = query.get_mut(player_entity)
+ else {
+ continue;
+ };
let delta_movement = physics.delta;
@@ -555,12 +558,12 @@ fn process_packet_events(ecs: &mut World) {
ClientboundGamePacket::AddEntity(p) => {
debug!("Got add entity packet {:?}", p);
- let mut system_state: SystemState<(Commands, Query<Option<&WorldName>>)> =
+ let mut system_state: SystemState<(Commands, Query<Option<&InstanceName>>)> =
SystemState::new(ecs);
let (mut commands, mut query) = system_state.get_mut(ecs);
let world_name = query.get_mut(player_entity).unwrap();
- if let Some(WorldName(world_name)) = world_name {
+ if let Some(InstanceName(world_name)) = world_name {
let bundle = p.as_entity_bundle(world_name.clone());
let mut entity_commands = commands.spawn((
MinecraftEntityId(p.id),
@@ -622,12 +625,12 @@ fn process_packet_events(ecs: &mut World) {
#[allow(clippy::type_complexity)]
let mut system_state: SystemState<(
Commands,
- Query<(&TabList, Option<&WorldName>)>,
+ Query<(&TabList, Option<&InstanceName>)>,
)> = SystemState::new(ecs);
let (mut commands, mut query) = system_state.get_mut(ecs);
let (tab_list, world_name) = query.get_mut(player_entity).unwrap();
- if let Some(WorldName(world_name)) = world_name {
+ if let Some(InstanceName(world_name)) = world_name {
let bundle = p.as_player_bundle(world_name.clone());
let mut spawned = commands.spawn((
MinecraftEntityId(p.id),
diff --git a/azalea-client/src/player.rs b/azalea-client/src/player.rs
index 999f2490..25ba0d8c 100755
--- a/azalea-client/src/player.rs
+++ b/azalea-client/src/player.rs
@@ -1,7 +1,7 @@
use azalea_auth::game_profile::GameProfile;
use azalea_chat::FormattedText;
use azalea_core::GameMode;
-use azalea_world::entity::EntityInfos;
+use azalea_entity::EntityInfos;
use bevy_ecs::{
event::EventReader,
system::{Commands, Res},