From 2c5f293210a09c99577a6999afd52357c898eaeb Mon Sep 17 00:00:00 2001 From: mat Date: Mon, 2 Jun 2025 06:55:38 -1030 Subject: [PATCH 01/36] add Event::ReceiveChunk and find_blocks_in_chunk function --- azalea-client/src/plugins/events.rs | 18 +++- azalea-world/src/find_blocks.rs | 139 ++++++++++++++-------------- 2 files changed, 89 insertions(+), 68 deletions(-) diff --git a/azalea-client/src/plugins/events.rs b/azalea-client/src/plugins/events.rs index d9cbf912..36f48a05 100644 --- a/azalea-client/src/plugins/events.rs +++ b/azalea-client/src/plugins/events.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use azalea_chat::FormattedText; -use azalea_core::tick::GameTick; +use azalea_core::{position::ChunkPos, tick::GameTick}; use azalea_entity::{Dead, InLoadedChunk}; use azalea_protocol::packets::game::c_player_combat_kill::ClientboundPlayerCombatKill; use azalea_world::{InstanceName, MinecraftEntityId}; @@ -15,6 +15,7 @@ use tokio::sync::mpsc; use crate::{ chat::{ChatPacket, ChatReceivedEvent}, + chunks::ReceiveChunkEvent, disconnect::DisconnectEvent, packet::game::{ AddPlayerEvent, DeathEvent, KeepAliveEvent, RemovePlayerEvent, UpdatePlayerEvent, @@ -118,6 +119,7 @@ pub enum Event { KeepAlive(u64), /// The client disconnected from the server. Disconnect(Option), + ReceiveChunk(ChunkPos), } /// A component that contains an event sender for events that are only @@ -294,3 +296,17 @@ pub fn disconnect_listener( } } } + +pub fn receive_chunk_listener( + query: Query<&LocalPlayerEvents>, + mut events: EventReader, +) { + for event in events.read() { + if let Ok(local_player_events) = query.get(event.entity) { + let _ = local_player_events.send(Event::ReceiveChunk(ChunkPos::new( + event.packet.x, + event.packet.z, + ))); + } + } +} diff --git a/azalea-world/src/find_blocks.rs b/azalea-world/src/find_blocks.rs index b266d799..10068243 100644 --- a/azalea-world/src/find_blocks.rs +++ b/azalea-world/src/find_blocks.rs @@ -1,16 +1,7 @@ use azalea_block::{BlockState, BlockStates}; use azalea_core::position::{BlockPos, ChunkPos}; -use crate::{ChunkStorage, Instance, iterators::ChunkIterator, palette::Palette}; - -fn palette_maybe_has_block(palette: &Palette, block_states: &BlockStates) -> bool { - match &palette { - Palette::SingleValue(id) => block_states.contains(id), - Palette::Linear(ids) => ids.iter().any(|id| block_states.contains(id)), - Palette::Hashmap(ids) => ids.iter().any(|id| block_states.contains(id)), - Palette::Global => true, - } -} +use crate::{Chunk, ChunkStorage, Instance, iterators::ChunkIterator, palette::Palette}; impl Instance { /// Find the coordinates of a block in the world. @@ -52,35 +43,20 @@ impl Instance { continue; }; - for (section_index, section) in chunk.read().sections.iter().enumerate() { - let maybe_has_block = - palette_maybe_has_block(§ion.states.palette, block_states); - if !maybe_has_block { - continue; - } - - for i in 0..4096 { - let block_state = section.states.get_at_index(i); - - if block_states.contains(&block_state) { - let section_pos = section.states.coords_from_index(i); - let (x, y, z) = ( - chunk_pos.x * 16 + (section_pos.x as i32), - self.chunks.min_y + (section_index * 16) as i32 + section_pos.y as i32, - chunk_pos.z * 16 + (section_pos.z as i32), - ); - let this_block_pos = BlockPos { x, y, z }; - let this_block_distance = (nearest_to - this_block_pos).length_manhattan(); - // only update if it's closer - if nearest_found_pos.is_none() - || this_block_distance < nearest_found_distance - { - nearest_found_pos = Some(this_block_pos); - nearest_found_distance = this_block_distance; - } + find_blocks_in_chunk( + block_states, + chunk_pos, + &chunk.read(), + self.chunks.min_y, + |this_block_pos| { + let this_block_distance = (nearest_to - this_block_pos).length_manhattan(); + // only update if it's closer + if nearest_found_pos.is_none() || this_block_distance < nearest_found_distance { + nearest_found_pos = Some(this_block_pos); + nearest_found_distance = this_block_distance; } - } - } + }, + ); if let Some(nearest_found_pos) = nearest_found_pos { // this is required because find_block searches chunk-by-chunk, which can cause @@ -179,38 +155,22 @@ impl Iterator for FindBlocks<'_> { continue; }; - for (section_index, section) in chunk.read().sections.iter().enumerate() { - let maybe_has_block = - palette_maybe_has_block(§ion.states.palette, self.block_states); - if !maybe_has_block { - continue; - } + find_blocks_in_chunk( + self.block_states, + chunk_pos, + &chunk.read(), + self.chunks.min_y, + |this_block_pos| { + let this_block_distance = (self.nearest_to - this_block_pos).length_manhattan(); - for i in 0..4096 { - let block_state = section.states.get_at_index(i); + found.push((this_block_pos, this_block_distance)); - if self.block_states.contains(&block_state) { - let section_pos = section.states.coords_from_index(i); - let (x, y, z) = ( - chunk_pos.x * 16 + (section_pos.x as i32), - self.chunks.min_y + (section_index * 16) as i32 + section_pos.y as i32, - chunk_pos.z * 16 + (section_pos.z as i32), - ); - let this_block_pos = BlockPos { x, y, z }; - let this_block_distance = - (self.nearest_to - this_block_pos).length_manhattan(); - - found.push((this_block_pos, this_block_distance)); - - if nearest_found_pos.is_none() - || this_block_distance < nearest_found_distance - { - nearest_found_pos = Some(this_block_pos); - nearest_found_distance = this_block_distance; - } + if nearest_found_pos.is_none() || this_block_distance < nearest_found_distance { + nearest_found_pos = Some(this_block_pos); + nearest_found_distance = this_block_distance; } - } - } + }, + ); if let Some(nearest_found_pos) = nearest_found_pos { // this is required because find_block searches chunk-by-chunk, which can cause @@ -242,6 +202,51 @@ impl Iterator for FindBlocks<'_> { } } +/// An optimized function for finding the block positions in a chunk that match +/// the given block states. +/// +/// This is used internally by [`Instance::find_block`] and +/// [`Instance::find_blocks`]. +pub fn find_blocks_in_chunk( + block_states: &BlockStates, + chunk_pos: ChunkPos, + chunk: &Chunk, + min_y: i32, + mut cb: impl FnMut(BlockPos), +) { + for (section_index, section) in chunk.sections.iter().enumerate() { + let maybe_has_block = palette_maybe_has_block(§ion.states.palette, block_states); + if !maybe_has_block { + continue; + } + + for i in 0..4096 { + let block_state = section.states.get_at_index(i); + + if block_states.contains(&block_state) { + let section_pos = section.states.coords_from_index(i); + let (x, y, z) = ( + chunk_pos.x * 16 + (section_pos.x as i32), + min_y + (section_index * 16) as i32 + section_pos.y as i32, + chunk_pos.z * 16 + (section_pos.z as i32), + ); + let this_block_pos = BlockPos { x, y, z }; + + cb(this_block_pos); + } + } + } +} + +fn palette_maybe_has_block(palette: &Palette, block_states: &BlockStates) -> bool { + match &palette { + Palette::SingleValue(id) => block_states.contains(id), + Palette::Linear(ids) => ids.iter().any(|id| block_states.contains(id)), + Palette::Hashmap(ids) => ids.iter().any(|id| block_states.contains(id)), + Palette::Global => true, + } +} + #[cfg(test)] mod tests { use azalea_registry::Block; From c5ddae58a172289fffde991830bb79860421a961 Mon Sep 17 00:00:00 2001 From: mat Date: Mon, 2 Jun 2025 11:51:03 -0600 Subject: [PATCH 02/36] improve docs for biome code --- azalea-registry/src/data.rs | 13 +++++++++++-- azalea-world/src/world.rs | 8 ++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/azalea-registry/src/data.rs b/azalea-registry/src/data.rs index 91e7106f..011835ed 100644 --- a/azalea-registry/src/data.rs +++ b/azalea-registry/src/data.rs @@ -24,7 +24,8 @@ impl Registry for T { } macro_rules! data_registry { - ($name:ident, $registry_name:expr) => { + ($(#[$doc:meta])* $name:ident, $registry_name:expr) => { + $(#[$doc])* #[derive(Debug, Clone, Copy, AzBuf, PartialEq, Eq, Hash)] pub struct $name { #[var] @@ -54,7 +55,15 @@ data_registry! {PigVariant, "pig_variant"} data_registry! {PaintingVariant, "painting_variant"} data_registry! {WolfVariant, "wolf_variant"} -data_registry! {Biome, "biome"} +data_registry! { + /// An opaque biome identifier. + /// + /// You'll probably want to resolve this into its name before using it, by + /// using `Client::with_resolved_registry` or a similar function. + Biome, + "worldgen/biome" +} + // these extra traits are required for Biome to be allowed to be palletable impl Default for Biome { fn default() -> Self { diff --git a/azalea-world/src/world.rs b/azalea-world/src/world.rs index 47804dcc..3e6359ff 100644 --- a/azalea-world/src/world.rs +++ b/azalea-world/src/world.rs @@ -179,6 +179,14 @@ impl Instance { self.chunks.get_block_state(pos).map(FluidState::from) } + /// Get the biome at the given position. + /// + /// You can then use `Client::with_resolved_registry` to get the name and + /// data from the biome. + /// + /// Note that biomes are internally stored as 4x4x4 blocks, so if you're + /// writing code that searches for a specific biome it'll probably be more + /// efficient to avoid scanning every single block. pub fn get_biome(&self, pos: &BlockPos) -> Option { self.chunks.get_biome(pos) } From cee64cece3ddbe4c0a41f0761d69b7c490d85040 Mon Sep 17 00:00:00 2001 From: mat Date: Mon, 2 Jun 2025 15:55:39 -0300 Subject: [PATCH 03/36] actually send Event::ReceiveChunk --- azalea-client/src/plugins/events.rs | 1 + azalea-client/src/plugins/mining.rs | 1 - azalea-core/src/position.rs | 11 +++++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/azalea-client/src/plugins/events.rs b/azalea-client/src/plugins/events.rs index 36f48a05..581a3e81 100644 --- a/azalea-client/src/plugins/events.rs +++ b/azalea-client/src/plugins/events.rs @@ -147,6 +147,7 @@ impl Plugin for EventsPlugin { keepalive_listener, death_listener, disconnect_listener, + receive_chunk_listener, ), ) .add_systems( diff --git a/azalea-client/src/plugins/mining.rs b/azalea-client/src/plugins/mining.rs index 204b482c..8d466328 100644 --- a/azalea-client/src/plugins/mining.rs +++ b/azalea-client/src/plugins/mining.rs @@ -241,7 +241,6 @@ fn handle_mining_queued( mut current_mining_pos, ) in query { - info!("mining_queued: {mining_queued:?}"); commands.entity(entity).remove::(); let instance = instance_holder.instance.read(); diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs index 5932cb5b..357e9b39 100644 --- a/azalea-core/src/position.rs +++ b/azalea-core/src/position.rs @@ -444,6 +444,17 @@ impl Add for ChunkPos { } } } +impl Add for ChunkPos { + type Output = BlockPos; + + fn add(self, rhs: ChunkBlockPos) -> Self::Output { + BlockPos { + x: self.x * 16 + rhs.x as i32, + y: rhs.y, + z: self.z * 16 + rhs.z as i32, + } + } +} // reading ChunkPos is done in reverse, so z first and then x // ........ From 5c0e5b1eb3edfdbf1542173f3cc9efcff18be511 Mon Sep 17 00:00:00 2001 From: mat Date: Tue, 3 Jun 2025 07:40:53 +1200 Subject: [PATCH 04/36] sort entities_by by distance and improve some docs --- azalea-client/src/entity_query.rs | 64 ++++++++++++++++++++++------- azalea-client/src/plugins/mining.rs | 2 +- azalea-entity/src/lib.rs | 2 + azalea-registry/src/lib.rs | 10 +++++ azalea/src/pathfinder/world.rs | 10 +++++ 5 files changed, 73 insertions(+), 15 deletions(-) diff --git a/azalea-client/src/entity_query.rs b/azalea-client/src/entity_query.rs index ad8a87a6..5b19bb24 100644 --- a/azalea-client/src/entity_query.rs +++ b/azalea-client/src/entity_query.rs @@ -1,5 +1,7 @@ use std::{any, sync::Arc}; +use azalea_core::position::Vec3; +use azalea_entity::Position; use azalea_world::InstanceName; use bevy_ecs::{ component::Component, @@ -34,13 +36,16 @@ impl Client { }) } - /// Return a lightweight [`Entity`] for the first entity that matches the + /// Return a lightweight [`Entity`] for an arbitrary entity that matches the /// given predicate function that is in the same [`Instance`] as the /// client. /// /// You can then use [`Self::entity_component`] to get components from this /// entity. /// + /// Also see [`Self::entities_by`] which will return all entities that match + /// the predicate and sorts them by distance (unlike `entity_by`). + /// /// # Example /// ``` /// use azalea_client::{Client, player::GameProfileComponent}; @@ -65,11 +70,14 @@ impl Client { predicate: impl EntityPredicate, ) -> Option { let instance_name = self.get_component::()?; - predicate.find(self.ecs.clone(), &instance_name) + predicate.find_any(self.ecs.clone(), &instance_name) } - /// Same as [`Self::entity_by`] but returns a `Vec` of all entities - /// in our instance that match the predicate. + /// Similar to [`Self::entity_by`] but returns a `Vec` of all + /// entities in our instance that match the predicate. + /// + /// Unlike `entity_by`, the result is sorted by distance to our client's + /// position, so the closest entity is first. pub fn entities_by( &self, predicate: impl EntityPredicate, @@ -77,7 +85,10 @@ impl Client { let Some(instance_name) = self.get_component::() else { return vec![]; }; - predicate.find_all(self.ecs.clone(), &instance_name) + let Some(position) = self.get_component::() else { + return vec![]; + }; + predicate.find_all_sorted(self.ecs.clone(), &instance_name, (&position).into()) } /// Get a component from an entity. Note that this will return an owned type @@ -109,14 +120,24 @@ impl Client { } pub trait EntityPredicate { - fn find(&self, ecs_lock: Arc>, instance_name: &InstanceName) -> Option; - fn find_all(&self, ecs_lock: Arc>, instance_name: &InstanceName) -> Vec; + fn find_any(&self, ecs_lock: Arc>, instance_name: &InstanceName) + -> Option; + fn find_all_sorted( + &self, + ecs_lock: Arc>, + instance_name: &InstanceName, + nearest_to: Vec3, + ) -> Vec; } impl EntityPredicate for F where F: Fn(&ROQueryItem) -> bool, { - fn find(&self, ecs_lock: Arc>, instance_name: &InstanceName) -> Option { + fn find_any( + &self, + ecs_lock: Arc>, + instance_name: &InstanceName, + ) -> Option { let mut ecs = ecs_lock.lock(); let mut query = ecs.query_filtered::<(Entity, &InstanceName, Q), Filter>(); query @@ -125,13 +146,28 @@ where .map(|(e, _, _)| e) } - fn find_all(&self, ecs_lock: Arc>, instance_name: &InstanceName) -> Vec { + fn find_all_sorted( + &self, + ecs_lock: Arc>, + instance_name: &InstanceName, + nearest_to: Vec3, + ) -> Vec { let mut ecs = ecs_lock.lock(); - let mut query = ecs.query_filtered::<(Entity, &InstanceName, Q), Filter>(); - query + let mut query = ecs.query_filtered::<(Entity, &InstanceName, &Position, Q), Filter>(); + let mut entities = query .iter(&ecs) - .filter(|(_, e_instance_name, q)| *e_instance_name == instance_name && (self)(q)) - .map(|(e, _, _)| e) - .collect::>() + .filter(|(_, e_instance_name, _, q)| *e_instance_name == instance_name && (self)(q)) + .map(|(e, _, position, _)| (e, Vec3::from(position))) + .collect::>(); + + entities.sort_by_cached_key(|(_, position)| { + // to_bits is fine here as long as the number is positive + position.distance_squared_to(&nearest_to).to_bits() + }); + + entities + .into_iter() + .map(|(e, _)| e) + .collect::>() } } diff --git a/azalea-client/src/plugins/mining.rs b/azalea-client/src/plugins/mining.rs index 8d466328..4f076129 100644 --- a/azalea-client/src/plugins/mining.rs +++ b/azalea-client/src/plugins/mining.rs @@ -8,7 +8,7 @@ use azalea_world::{InstanceContainer, InstanceName}; use bevy_app::{App, Plugin, Update}; use bevy_ecs::prelude::*; use derive_more::{Deref, DerefMut}; -use tracing::{info, trace}; +use tracing::trace; use crate::{ Client, diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs index 44a4a9ee..ad225400 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -389,6 +389,8 @@ pub struct Dead; /// /// This is used to calculate the camera position for players, when spectating /// an entity, and when raycasting from the entity. +/// +/// The default eye height for a player is 1.62 blocks. #[derive(Component, Clone, Copy, Debug, PartialEq, Deref, DerefMut)] pub struct EyeHeight(f32); impl EyeHeight { diff --git a/azalea-registry/src/lib.rs b/azalea-registry/src/lib.rs index 56e20028..6fb089d2 100644 --- a/azalea-registry/src/lib.rs +++ b/azalea-registry/src/lib.rs @@ -1883,6 +1883,16 @@ enum IntProviderKind { } registry! { +/// Every type of item in the game. +/// +/// You might find it useful in some cases to check for categories of items +/// with [`azalea_registry::tags::items`](crate::tags::items), like this +/// +/// ``` +/// let item = azalea_registry::Item::OakLog; +/// let is_log = azalea_registry::tags::items::LOGS.contains(&item); +/// assert!(is_log); +/// ``` enum Item { Air => "minecraft:air", Stone => "minecraft:stone", diff --git a/azalea/src/pathfinder/world.rs b/azalea/src/pathfinder/world.rs index b77183e8..3ec95136 100644 --- a/azalea/src/pathfinder/world.rs +++ b/azalea/src/pathfinder/world.rs @@ -551,6 +551,16 @@ pub fn is_block_state_passable(block: BlockState) -> bool { return false; } + if registry_block == azalea_registry::Block::PowderSnow { + // we can't jump out of powder snow + return false; + } + + if registry_block == azalea_registry::Block::SweetBerryBush { + // these hurt us + return false; + } + true } From 04dd6dd0a44faa760906bd60c9861a1b06a664a1 Mon Sep 17 00:00:00 2001 From: mat Date: Mon, 2 Jun 2025 13:18:42 -0630 Subject: [PATCH 05/36] fix login_to_dimension_with_same_name test logging error --- azalea-client/tests/login_to_dimension_with_same_name.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/azalea-client/tests/login_to_dimension_with_same_name.rs b/azalea-client/tests/login_to_dimension_with_same_name.rs index 24637693..59d3123d 100644 --- a/azalea-client/tests/login_to_dimension_with_same_name.rs +++ b/azalea-client/tests/login_to_dimension_with_same_name.rs @@ -128,9 +128,4 @@ fn generic_test_login_to_dimension_with_same_name(using_respawn: bool) { simulation .chunk(ChunkPos::new(0, 0)) .expect("chunk should exist"); - simulation.receive_packet(make_basic_login_or_respawn_packet( - DimensionType::new_raw(2), // nether - ResourceLocation::new("minecraft:nether"), - )); - simulation.tick(); } From abf995a70245028f9cc860ee231dc671f14adfcc Mon Sep 17 00:00:00 2001 From: mat Date: Tue, 3 Jun 2025 03:48:36 +0500 Subject: [PATCH 06/36] replace wait_one_tick with wait_ticks and some other api improvements --- azalea-block/src/range.rs | 1 + azalea-client/src/plugins/inventory.rs | 33 +++++++++++++++++++- azalea-core/src/direction.rs | 10 ++++++ azalea-inventory/src/operations.rs | 2 ++ azalea/src/bot.rs | 42 +++++++++++++++----------- azalea/src/container.rs | 33 +++++++++++++++++++- azalea/src/pathfinder/mod.rs | 2 +- 7 files changed, 102 insertions(+), 21 deletions(-) diff --git a/azalea-block/src/range.rs b/azalea-block/src/range.rs index cbe77284..183c3cc0 100644 --- a/azalea-block/src/range.rs +++ b/azalea-block/src/range.rs @@ -1,6 +1,7 @@ use std::{ collections::{HashSet, hash_set}, ops::{Add, RangeInclusive}, + sync::LazyLock, }; use crate::{BlockState, block_state::BlockStateIntegerRepr}; diff --git a/azalea-client/src/plugins/inventory.rs b/azalea-client/src/plugins/inventory.rs index 829b37f8..23651db3 100644 --- a/azalea-client/src/plugins/inventory.rs +++ b/azalea-client/src/plugins/inventory.rs @@ -60,6 +60,36 @@ impl Client { let inventory = self.query::<&Inventory>(&mut ecs); inventory.menu().clone() } + + /// Returns the index of the hotbar slot that's currently selected. + /// + /// If you want to access the actual held item, you can get the current menu + /// with [`Client::menu`] and then get the slot index by offsetting from + /// the start of [`azalea_inventory::Menu::hotbar_slots_range`]. + /// + /// You can use [`Self::set_selected_hotbar_slot`] to change it. + pub fn selected_hotbar_slot(&self) -> u8 { + let mut ecs = self.ecs.lock(); + let inventory = self.query::<&Inventory>(&mut ecs); + inventory.selected_hotbar_slot + } + + /// Update the selected hotbar slot index. + /// + /// This will run next `Update`, so you might want to call + /// `bot.wait_updates(1)` after calling this if you're using `azalea`. + pub fn set_selected_hotbar_slot(&self, new_hotbar_slot_index: u8) { + assert!( + new_hotbar_slot_index < 9, + "Hotbar slot index must be in the range 0..=8" + ); + + let mut ecs = self.ecs.lock(); + ecs.send_event(SetSelectedHotbarSlotEvent { + entity: self.entity, + slot: new_hotbar_slot_index, + }); + } } /// A component present on all local players that have an inventory. @@ -499,7 +529,8 @@ impl Inventory { self.quick_craft_slots.clear(); } - /// Get the item in the player's hotbar that is currently being held. + /// Get the item in the player's hotbar that is currently being held in its + /// main hand. pub fn held_item(&self) -> ItemStack { let inventory = &self.inventory_menu; let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()]; diff --git a/azalea-core/src/direction.rs b/azalea-core/src/direction.rs index 656cab1b..9f51ceaf 100644 --- a/azalea-core/src/direction.rs +++ b/azalea-core/src/direction.rs @@ -162,6 +162,16 @@ impl CardinalDirection { } } } +impl From for Direction { + fn from(value: CardinalDirection) -> Self { + match value { + CardinalDirection::North => Direction::North, + CardinalDirection::South => Direction::South, + CardinalDirection::West => Direction::West, + CardinalDirection::East => Direction::East, + } + } +} impl Axis { /// Pick x, y, or z from the arguments depending on the axis. diff --git a/azalea-inventory/src/operations.rs b/azalea-inventory/src/operations.rs index 90ad2403..f410c2c5 100644 --- a/azalea-inventory/src/operations.rs +++ b/azalea-inventory/src/operations.rs @@ -61,6 +61,8 @@ impl From for ClickOperation { #[derive(Debug, Clone)] pub struct SwapClick { pub source_slot: u16, + /// 0-8 for hotbar slots, 40 for offhand, everything else is treated as a + /// slot index. pub target_slot: u8, } diff --git a/azalea/src/bot.rs b/azalea/src/bot.rs index 8bc9d594..9e8566bf 100644 --- a/azalea/src/bot.rs +++ b/azalea/src/bot.rs @@ -92,10 +92,10 @@ pub trait BotClientExt { fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()>; /// Get a receiver that will receive a message every ECS Update. fn get_update_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()>; - /// Wait for one tick. - fn wait_one_tick(&self) -> impl Future + Send; - /// Wait for one ECS Update. - fn wait_one_update(&self) -> impl Future + Send; + /// Wait for the specified number of game ticks. + fn wait_ticks(&self, n: usize) -> impl Future + Send; + /// Wait for the specified number of ECS `Update`s. + fn wait_updates(&self, n: usize) -> impl Future + Send; /// Mine a block. This won't turn the bot's head towards the block, so if /// that's necessary you'll have to do that yourself with [`look_at`]. /// @@ -156,23 +156,32 @@ impl BotClientExt for azalea_client::Client { update_broadcast.subscribe() } - /// Wait for one tick using [`Self::get_tick_broadcaster`]. + /// Wait for the specified number of ticks using + /// [`Self::get_tick_broadcaster`]. /// /// If you're going to run this in a loop, you may want to use that function - /// instead and use the `Receiver` from it as it'll be more efficient. - async fn wait_one_tick(&self) { + /// instead and use the `Receiver` from it to avoid accidentally skipping + /// ticks and having to wait longer. + async fn wait_ticks(&self, n: usize) { let mut receiver = self.get_tick_broadcaster(); - // wait for the next tick - let _ = receiver.recv().await; + for _ in 0..n { + let _ = receiver.recv().await; + } } - /// Waits for one ECS Update using [`Self::get_update_broadcaster`]. + /// Waits for the specified number of ECS `Update`s using + /// [`Self::get_update_broadcaster`]. + /// + /// These are basically equivalent to frames because even though we have no + /// rendering, some game mechanics depend on frames. /// /// If you're going to run this in a loop, you may want to use that function - /// instead and use the `Receiver` from it as it'll be more efficient. - async fn wait_one_update(&self) { + /// instead and use the `Receiver` from it to avoid accidentally skipping + /// ticks and having to wait longer. + async fn wait_updates(&self, n: usize) { let mut receiver = self.get_update_broadcaster(); - // wait for the next tick - let _ = receiver.recv().await; + for _ in 0..n { + let _ = receiver.recv().await; + } } async fn mine(&self, position: BlockPos) { @@ -221,10 +230,7 @@ fn look_at_listener( if let Ok((position, eye_height, mut look_direction)) = query.get_mut(event.entity) { let new_look_direction = direction_looking_at(&position.up(eye_height.into()), &event.position); - trace!( - "look at {:?} (currently at {:?})", - event.position, **position - ); + trace!("look at {} (currently at {})", event.position, **position); *look_direction = new_look_direction; } } diff --git a/azalea/src/container.rs b/azalea/src/container.rs index 6715cd63..e5896d8a 100644 --- a/azalea/src/container.rs +++ b/azalea/src/container.rs @@ -6,7 +6,10 @@ use azalea_client::{ packet::game::ReceiveGamePacketEvent, }; use azalea_core::position::BlockPos; -use azalea_inventory::{ItemStack, Menu, operations::ClickOperation}; +use azalea_inventory::{ + ItemStack, Menu, + operations::{ClickOperation, PickupClick, QuickMoveClick}, +}; use azalea_protocol::packets::game::ClientboundGamePacket; use bevy_app::{App, Plugin, Update}; use bevy_ecs::{component::Component, prelude::EventReader, system::Commands}; @@ -27,6 +30,7 @@ pub trait ContainerClientExt { pos: BlockPos, ) -> impl Future> + Send; fn open_inventory(&self) -> Option; + fn get_held_item(&self) -> ItemStack; fn get_open_container(&self) -> Option; } @@ -93,6 +97,14 @@ impl ContainerClientExt for Client { } } + /// Get the item in the bot's hotbar that is currently being held in its + /// main hand. + fn get_held_item(&self) -> ItemStack { + let ecs = self.ecs.lock(); + let inventory = ecs.get::(self.entity).expect("no inventory"); + inventory.held_item() + } + /// Get a handle to the open container. This will return None if no /// container is open. This will not close the container when it's dropped. /// @@ -228,6 +240,25 @@ impl ContainerHandle { pub fn click(&self, operation: impl Into) { self.0.click(operation); } + + /// A shortcut for [`Self::click`] with `PickupClick::Left`. + pub fn left_click(&self, slot: impl Into) { + self.click(PickupClick::Left { + slot: Some(slot.into() as u16), + }); + } + /// A shortcut for [`Self::click`] with `QuickMoveClick::Left`. + pub fn shift_click(&self, slot: impl Into) { + self.click(QuickMoveClick::Left { + slot: slot.into() as u16, + }); + } + /// A shortcut for [`Self::click`] with `PickupClick::Right`. + pub fn right_click(&self, slot: impl Into) { + self.click(PickupClick::Right { + slot: Some(slot.into() as u16), + }); + } } #[derive(Component, Debug)] diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index 08c72f9a..5ee56643 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -260,7 +260,7 @@ impl PathfinderClientExt for azalea_client::Client { async fn wait_until_goto_target_reached(&self) { // we do this to make sure the event got handled before we start checking // is_goto_target_reached - self.wait_one_update().await; + self.wait_updates(1).await; let mut tick_broadcaster = self.get_tick_broadcaster(); while !self.is_goto_target_reached() { From 7517a207db658c98d5b97b3b3f44df6725c025a2 Mon Sep 17 00:00:00 2001 From: mat Date: Mon, 2 Jun 2025 19:23:59 -0330 Subject: [PATCH 07/36] rename the Block trait to BlockTrait to disambiguate with azalea_registry::Block --- azalea-block/README.md | 14 +++++++------- azalea-block/azalea-block-macros/src/lib.rs | 6 +++--- azalea-block/src/block_state.rs | 12 ++++++------ azalea-block/src/generated.rs | 2 +- azalea-block/src/lib.rs | 6 +++--- azalea-block/src/range.rs | 1 - azalea-client/src/plugins/mining.rs | 8 ++++---- azalea-entity/src/mining.rs | 6 +++--- azalea-entity/src/plugin/mod.rs | 4 ++-- azalea-physics/src/collision/mod.rs | 2 +- azalea-physics/src/lib.rs | 6 +++--- azalea-physics/src/travel.rs | 4 ++-- azalea-registry/src/lib.rs | 4 ++-- azalea-world/src/heightmap.rs | 2 +- azalea/src/auto_tool.rs | 4 ++-- 15 files changed, 40 insertions(+), 41 deletions(-) diff --git a/azalea-block/README.md b/azalea-block/README.md index ae3b8c5f..8eca79ee 100644 --- a/azalea-block/README.md +++ b/azalea-block/README.md @@ -23,20 +23,20 @@ let block_state: BlockState = azalea_block::blocks::CobblestoneWall { let block_state: BlockState = azalea_registry::Block::Jukebox.into(); ``` -## Block trait +## BlockTrait -The [`Block`] trait represents a type of a block. With the the [`Block`] trait, you can get some extra things like the string block ID and some information about the block's behavior. Also, the structs that implement the trait contain the block attributes as fields so it's more convenient to get them. Note that this is often used as `Box`. -If for some reason you don't want the `Block` trait, set default-features to false. +The [`BlockTrait`] trait represents a type of a block. With [`BlockTrait`], you can get some extra things like the string block ID and some information about the block's behavior. Also, the structs that implement the trait contain the block attributes as fields so it's more convenient to get them. Note that this is often used as `Box`. +If for some reason you don't want `BlockTrait`, set `default-features = false`. ``` -# use azalea_block::{Block, BlockState}; +# use azalea_block::{BlockTrait, BlockState}; # let block_state = BlockState::from(azalea_registry::Block::Jukebox); -let block = Box::::from(block_state); +let block = Box::::from(block_state); ``` ``` -# use azalea_block::{Block, BlockState}; +# use azalea_block::{BlockTrait, BlockState}; # let block_state: BlockState = azalea_registry::Block::Jukebox.into(); -if let Some(jukebox) = Box::::from(block_state).downcast_ref::() { +if let Some(jukebox) = Box::::from(block_state).downcast_ref::() { // ... } ``` diff --git a/azalea-block/azalea-block-macros/src/lib.rs b/azalea-block/azalea-block-macros/src/lib.rs index 3d09b018..4174ed41 100644 --- a/azalea-block/azalea-block-macros/src/lib.rs +++ b/azalea-block/azalea-block-macros/src/lib.rs @@ -660,7 +660,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream { #block_struct_fields } - impl Block for #block_struct_name { + impl BlockTrait for #block_struct_name { fn behavior(&self) -> BlockBehavior { #block_behavior } @@ -785,7 +785,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream { #block_structs - impl From for Box { + impl From for Box { fn from(block_state: BlockState) -> Self { let b = block_state.id(); match b { @@ -794,7 +794,7 @@ pub fn make_block_states(input: TokenStream) -> TokenStream { } } } - impl From for Box { + impl From for Box { fn from(block: azalea_registry::Block) -> Self { match block { #from_registry_block_to_block_match diff --git a/azalea-block/src/block_state.rs b/azalea-block/src/block_state.rs index dfa2a9b2..a954c333 100644 --- a/azalea-block/src/block_state.rs +++ b/azalea-block/src/block_state.rs @@ -5,7 +5,7 @@ use std::{ use azalea_buf::{AzaleaRead, AzaleaReadVar, AzaleaWrite, AzaleaWriteVar, BufReadError}; -use crate::Block; +use crate::BlockTrait; /// The type that's used internally to represent a block state ID. /// @@ -121,14 +121,14 @@ impl Debug for BlockState { f, "BlockState(id: {}, {:?})", self.id, - Box::::from(*self) + Box::::from(*self) ) } } impl From for azalea_registry::Block { fn from(value: BlockState) -> Self { - Box::::from(value).as_registry_block() + Box::::from(value).as_registry_block() } } @@ -149,11 +149,11 @@ mod tests { #[test] fn test_from_blockstate() { - let block: Box = Box::::from(BlockState::AIR); + let block: Box = Box::::from(BlockState::AIR); assert_eq!(block.id(), "air"); - let block: Box = - Box::::from(BlockState::from(azalea_registry::Block::FloweringAzalea)); + let block: Box = + Box::::from(BlockState::from(azalea_registry::Block::FloweringAzalea)); assert_eq!(block.id(), "flowering_azalea"); } diff --git a/azalea-block/src/generated.rs b/azalea-block/src/generated.rs index 28dfe4f9..7c26291f 100644 --- a/azalea-block/src/generated.rs +++ b/azalea-block/src/generated.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use azalea_block_macros::make_block_states; -use crate::{Block, BlockBehavior, BlockState, BlockStates, Property}; +use crate::{BlockTrait, BlockBehavior, BlockState, BlockStates, Property}; make_block_states! { Properties => { diff --git a/azalea-block/src/lib.rs b/azalea-block/src/lib.rs index fdffe372..4f929cd3 100644 --- a/azalea-block/src/lib.rs +++ b/azalea-block/src/lib.rs @@ -15,7 +15,7 @@ pub use block_state::BlockState; pub use generated::{blocks, properties}; pub use range::BlockStates; -pub trait Block: Debug + Any { +pub trait BlockTrait: Debug + Any { fn behavior(&self) -> BlockBehavior; /// Get the Minecraft ID for this block. For example `stone` or /// `grass_block`. @@ -27,8 +27,8 @@ pub trait Block: Debug + Any { /// `azalea_registry::Block` doesn't contain any state data. fn as_registry_block(&self) -> azalea_registry::Block; } -impl dyn Block { - pub fn downcast_ref(&self) -> Option<&T> { +impl dyn BlockTrait { + pub fn downcast_ref(&self) -> Option<&T> { (self as &dyn Any).downcast_ref::() } } diff --git a/azalea-block/src/range.rs b/azalea-block/src/range.rs index 183c3cc0..cbe77284 100644 --- a/azalea-block/src/range.rs +++ b/azalea-block/src/range.rs @@ -1,7 +1,6 @@ use std::{ collections::{HashSet, hash_set}, ops::{Add, RangeInclusive}, - sync::LazyLock, }; use crate::{BlockState, block_state::BlockStateIntegerRepr}; diff --git a/azalea-client/src/plugins/mining.rs b/azalea-client/src/plugins/mining.rs index 4f076129..6372e6c8 100644 --- a/azalea-client/src/plugins/mining.rs +++ b/azalea-client/src/plugins/mining.rs @@ -1,4 +1,4 @@ -use azalea_block::{Block, BlockState, fluid_state::FluidState}; +use azalea_block::{BlockTrait, BlockState, fluid_state::FluidState}; use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick}; use azalea_entity::{FluidOnEyes, Physics, mining::get_mine_progress}; use azalea_inventory::ItemStack; @@ -300,7 +300,7 @@ fn handle_mining_queued( }); } - let block = Box::::from(target_block_state); + let block = Box::::from(target_block_state); let held_item = inventory.held_item(); @@ -469,7 +469,7 @@ pub fn handle_finish_mining_block_observer( return; }; - let registry_block = Box::::from(block_state).as_registry_block(); + let registry_block = Box::::from(block_state).as_registry_block(); if !can_use_game_master_blocks(abilities, permission_level) && matches!( registry_block, @@ -603,7 +603,7 @@ pub fn continue_mining_block( commands.entity(entity).remove::(); continue; } - let block = Box::::from(target_block_state); + let block = Box::::from(target_block_state); **mine_progress += get_mine_progress( block.as_ref(), current_mining_item.kind(), diff --git a/azalea-entity/src/mining.rs b/azalea-entity/src/mining.rs index 370478ee..fbe7f525 100644 --- a/azalea-entity/src/mining.rs +++ b/azalea-entity/src/mining.rs @@ -1,4 +1,4 @@ -use azalea_block::{Block, BlockBehavior}; +use azalea_block::{BlockTrait, BlockBehavior}; use azalea_core::tier::get_item_tier; use azalea_registry as registry; @@ -13,7 +13,7 @@ use crate::{FluidOnEyes, Physics, effects}; /// The player inventory is needed to check your armor and offhand for modifiers /// to your mining speed. pub fn get_mine_progress( - block: &dyn Block, + block: &dyn BlockTrait, held_item: registry::Item, player_inventory: &azalea_inventory::Menu, fluid_on_eyes: &FluidOnEyes, @@ -41,7 +41,7 @@ pub fn get_mine_progress( / divider as f32 } -fn has_correct_tool_for_drops(block: &dyn Block, tool: registry::Item) -> bool { +fn has_correct_tool_for_drops(block: &dyn BlockTrait, tool: registry::Item) -> bool { if !block.behavior().requires_correct_tool_for_drops { return true; } diff --git a/azalea-entity/src/plugin/mod.rs b/azalea-entity/src/plugin/mod.rs index 6e716d15..03afe7cd 100644 --- a/azalea-entity/src/plugin/mod.rs +++ b/azalea-entity/src/plugin/mod.rs @@ -135,7 +135,7 @@ pub fn update_on_climbable( let block_pos = BlockPos::from(position); let block_state_at_feet = instance.get_block_state(&block_pos).unwrap_or_default(); - let block_at_feet = Box::::from(block_state_at_feet); + let block_at_feet = Box::::from(block_state_at_feet); let registry_block_at_feet = block_at_feet.as_registry_block(); **on_climbable = azalea_registry::tags::blocks::CLIMBABLE.contains(®istry_block_at_feet) @@ -162,7 +162,7 @@ fn is_trapdoor_useable_as_ladder( .get_block_state(&block_pos.down(1)) .unwrap_or_default(); let registry_block_below = - Box::::from(block_below).as_registry_block(); + Box::::from(block_below).as_registry_block(); if registry_block_below != azalea_registry::Block::Ladder { return false; } diff --git a/azalea-physics/src/collision/mod.rs b/azalea-physics/src/collision/mod.rs index 56491e2d..2e46970d 100644 --- a/azalea-physics/src/collision/mod.rs +++ b/azalea-physics/src/collision/mod.rs @@ -379,7 +379,7 @@ pub fn legacy_blocks_motion(block: BlockState) -> bool { pub fn legacy_calculate_solid(block: BlockState) -> bool { // force_solid has to be checked before anything else - let block_trait = Box::::from(block); + let block_trait = Box::::from(block); if let Some(solid) = block_trait.behavior().force_solid { return solid; } diff --git a/azalea-physics/src/lib.rs b/azalea-physics/src/lib.rs index 6c9fe096..f384a90f 100644 --- a/azalea-physics/src/lib.rs +++ b/azalea-physics/src/lib.rs @@ -8,7 +8,7 @@ pub mod travel; use std::collections::HashSet; -use azalea_block::{Block, BlockState, fluid_state::FluidState, properties}; +use azalea_block::{BlockTrait, BlockState, fluid_state::FluidState, properties}; use azalea_core::{ math, position::{BlockPos, Vec3}, @@ -492,7 +492,7 @@ fn block_jump_factor(world: &Instance, position: &Position) -> f32 { .get_block_state(&get_block_pos_below_that_affects_movement(position)); let block_at_pos_jump_factor = if let Some(block) = block_at_pos { - Box::::from(block).behavior().jump_factor + Box::::from(block).behavior().jump_factor } else { 1. }; @@ -501,7 +501,7 @@ fn block_jump_factor(world: &Instance, position: &Position) -> f32 { } if let Some(block) = block_below { - Box::::from(block).behavior().jump_factor + Box::::from(block).behavior().jump_factor } else { 1. } diff --git a/azalea-physics/src/travel.rs b/azalea-physics/src/travel.rs index 8f89de25..741267c2 100644 --- a/azalea-physics/src/travel.rs +++ b/azalea-physics/src/travel.rs @@ -1,4 +1,4 @@ -use azalea_block::{Block, BlockState, fluid_state::FluidState}; +use azalea_block::{BlockTrait, BlockState, fluid_state::FluidState}; use azalea_core::{ aabb::AABB, position::{BlockPos, Vec3}, @@ -124,7 +124,7 @@ fn travel_in_air( .chunks .get_block_state(&block_pos_below) .unwrap_or(BlockState::AIR); - let block_below: Box = block_state_below.into(); + let block_below: Box = block_state_below.into(); let block_friction = block_below.behavior().friction; let inertia = if physics.on_ground() { diff --git a/azalea-registry/src/lib.rs b/azalea-registry/src/lib.rs index 6fb089d2..69ac72b0 100644 --- a/azalea-registry/src/lib.rs +++ b/azalea-registry/src/lib.rs @@ -278,10 +278,10 @@ enum Attribute { registry! { /// An enum of every type of block in the game. To represent a block *state*, -/// use [`azalea_block::BlockState`] or the [`azalea_block::Block`] trait. +/// use [`azalea_block::BlockState`] or [`azalea_block::BlockTrait`]. /// /// [`azalea_block::BlockState`]: https://docs.rs/azalea-block/latest/azalea_block/struct.BlockState.html -/// [`azalea_block::Block`]: https://docs.rs/azalea-block/latest/azalea_block/trait.Block.html +/// [`azalea_block::BlockTrait`]: https://docs.rs/azalea-block/latest/azalea_block/trait.BlockTrait.html enum Block { Air => "minecraft:air", Stone => "minecraft:stone", diff --git a/azalea-world/src/heightmap.rs b/azalea-world/src/heightmap.rs index 462d5b09..dbe7d78f 100644 --- a/azalea-world/src/heightmap.rs +++ b/azalea-world/src/heightmap.rs @@ -44,7 +44,7 @@ fn motion_blocking(block_state: BlockState) -> bool { impl HeightmapKind { pub fn is_opaque(self, block_state: BlockState) -> bool { - let block = Box::::from(block_state); + let block = Box::::from(block_state); let registry_block = block.as_registry_block(); match self { HeightmapKind::WorldSurfaceWg => !block_state.is_air(), diff --git a/azalea/src/auto_tool.rs b/azalea/src/auto_tool.rs index ace032d9..0f33dd09 100644 --- a/azalea/src/auto_tool.rs +++ b/azalea/src/auto_tool.rs @@ -1,4 +1,4 @@ -use azalea_block::{Block, BlockState, fluid_state::FluidKind}; +use azalea_block::{BlockState, BlockTrait, fluid_state::FluidKind}; use azalea_client::{Client, inventory::Inventory}; use azalea_entity::{FluidOnEyes, Physics}; use azalea_inventory::{ItemStack, Menu, components}; @@ -52,7 +52,7 @@ pub fn accurate_best_tool_in_hotbar_for_block( let mut best_speed = 0.; let mut best_slot = None; - let block = Box::::from(block); + let block = Box::::from(block); let registry_block = block.as_registry_block(); if matches!( From f3a5e91a8ccbcd03a239aa3565dbfddabb26fa76 Mon Sep 17 00:00:00 2001 From: mat Date: Tue, 3 Jun 2025 09:52:30 +0930 Subject: [PATCH 08/36] fix issues when pathfinding to non-full blocks and add Client::view_inventory --- azalea-client/src/client.rs | 6 +++--- azalea-client/src/plugins/inventory.rs | 3 +-- azalea-core/src/position.rs | 5 +++-- azalea-world/src/chunk_storage.rs | 3 +++ azalea/src/container.rs | 15 ++++++++++++--- azalea/src/pathfinder/goals.rs | 8 +++++--- azalea/src/pathfinder/mod.rs | 16 ++++++++++------ 7 files changed, 37 insertions(+), 19 deletions(-) diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 7b63ff12..02625326 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -342,13 +342,13 @@ impl Client { /// ``` /// # use azalea_client::{Client, mining::Mining}; /// # fn example(bot: &Client) { - /// let is_mining = bot.map_get_component::(|m| m.is_some()); + /// let is_mining = bot.map_get_component::(|m| m).is_some(); /// # } /// ``` - pub fn map_get_component(&self, f: impl FnOnce(Option<&T>) -> R) -> R { + pub fn map_get_component(&self, f: impl FnOnce(&T) -> R) -> Option { let mut ecs = self.ecs.lock(); let value = self.query::>(&mut ecs); - f(value) + value.map(f) } /// Get an `RwLock` with a reference to our (potentially shared) world. diff --git a/azalea-client/src/plugins/inventory.rs b/azalea-client/src/plugins/inventory.rs index 23651db3..a7e45ffb 100644 --- a/azalea-client/src/plugins/inventory.rs +++ b/azalea-client/src/plugins/inventory.rs @@ -95,8 +95,7 @@ impl Client { /// A component present on all local players that have an inventory. #[derive(Component, Debug, Clone)] pub struct Inventory { - /// A component that contains the player's inventory menu. This is - /// guaranteed to be a `Menu::Player`. + /// The player's inventory menu. This is guaranteed to be a `Menu::Player`. /// /// We keep it as a [`Menu`] since `Menu` has some useful functions that /// bare [`azalea_inventory::Player`] doesn't have. diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs index 357e9b39..beb8eedb 100644 --- a/azalea-core/src/position.rs +++ b/azalea-core/src/position.rs @@ -335,8 +335,9 @@ impl Vec3 { } } -/// The coordinates of a block in the world. For entities (if the coordinate -/// have decimals), use [`Vec3`] instead. +/// The coordinates of a block in the world. +/// +/// For entities (if the coordinates are floating-point), use [`Vec3`] instead. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct BlockPos { diff --git a/azalea-world/src/chunk_storage.rs b/azalea-world/src/chunk_storage.rs index 23d1bb89..a6e33739 100644 --- a/azalea-world/src/chunk_storage.rs +++ b/azalea-world/src/chunk_storage.rs @@ -41,6 +41,9 @@ pub struct PartialChunkStorage { /// A storage for chunks where they're only stored weakly, so if they're not /// actively being used somewhere else they'll be forgotten. This is used for /// shared worlds. +/// +/// This is relatively cheap to clone since it's just an `IntMap` with `Weak` +/// pointers. #[derive(Debug, Clone)] pub struct ChunkStorage { pub height: u32, diff --git a/azalea/src/container.rs b/azalea/src/container.rs index e5896d8a..da3ddb8a 100644 --- a/azalea/src/container.rs +++ b/azalea/src/container.rs @@ -32,6 +32,7 @@ pub trait ContainerClientExt { fn open_inventory(&self) -> Option; fn get_held_item(&self) -> ItemStack; fn get_open_container(&self) -> Option; + fn view_inventory(&self) -> Menu; } impl ContainerClientExt for Client { @@ -100,9 +101,8 @@ impl ContainerClientExt for Client { /// Get the item in the bot's hotbar that is currently being held in its /// main hand. fn get_held_item(&self) -> ItemStack { - let ecs = self.ecs.lock(); - let inventory = ecs.get::(self.entity).expect("no inventory"); - inventory.held_item() + self.map_get_component::(|inventory| inventory.held_item()) + .expect("no inventory") } /// Get a handle to the open container. This will return None if no @@ -123,6 +123,15 @@ impl ContainerClientExt for Client { }) } } + + /// Returns the player's inventory menu. + /// + /// This is a shortcut for accessing the client's + /// [`Inventory::inventory_menu`]. + fn view_inventory(&self) -> Menu { + self.map_get_component::(|inventory| inventory.inventory_menu.clone()) + .expect("no inventory") + } } /// A handle to a container that may be open. This does not close the container diff --git a/azalea/src/pathfinder/goals.rs b/azalea/src/pathfinder/goals.rs index 5ab969c9..36fca762 100644 --- a/azalea/src/pathfinder/goals.rs +++ b/azalea/src/pathfinder/goals.rs @@ -29,7 +29,9 @@ impl Goal for BlockPosGoal { xz_heuristic(dx, dz) + y_heuristic(dy) } fn success(&self, n: BlockPos) -> bool { - n == self.0 + // the second half of this condition is intended to fix issues when pathing to + // non-full blocks + n == self.0 || n.down(1) == self.0 } } @@ -219,8 +221,8 @@ impl Goal for ReachBlockPosGoal { } fn success(&self, n: BlockPos) -> bool { // only do the expensive check if we're close enough - let distance = (self.pos - n).length_squared(); - if distance > self.max_check_distance * self.max_check_distance { + let distance = self.pos.distance_squared_to(&n); + if distance > self.max_check_distance.pow(2) { return false; } diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index 5ee56643..b05d2aab 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -274,10 +274,8 @@ impl PathfinderClientExt for azalea_client::Client { } fn is_goto_target_reached(&self) -> bool { - self.map_get_component::(|p| { - p.map(|p| p.goal.is_none() && !p.is_calculating) - .unwrap_or(true) - }) + self.map_get_component::(|p| p.goal.is_none() && !p.is_calculating) + .unwrap_or(true) } } @@ -689,7 +687,12 @@ pub fn timeout_movement( let world_lock = instance_container .get(instance_name) .expect("Entity tried to pathfind but the entity isn't in a valid world"); - let successors_fn: moves::SuccessorsFn = pathfinder.successors_fn.unwrap(); + let Some(successors_fn) = pathfinder.successors_fn else { + warn!( + "pathfinder was going to patch path because of timeout, but there was no successors_fn" + ); + return; + }; let custom_state = custom_state.cloned().unwrap_or_default(); @@ -749,7 +752,8 @@ pub fn check_node_reached( let z_difference_from_center = position.z - (movement.target.z as f64 + 0.5); // this is to make sure we don't fall off immediately after finishing the path physics.on_ground() - && BlockPos::from(position) == movement.target + // 0.5 to handle non-full blocks + && BlockPos::from(position.up(0.5)) == movement.target // adding the delta like this isn't a perfect solution but it helps to make // sure we don't keep going if our delta is high && (x_difference_from_center + physics.velocity.x).abs() < 0.2 From 1edb9d34486b432c84351692aa82a3c0328a7d69 Mon Sep 17 00:00:00 2001 From: mat Date: Mon, 2 Jun 2025 17:56:16 -0800 Subject: [PATCH 09/36] add BlockPos::center_bottom --- azalea-core/src/position.rs | 10 ++++++++++ azalea/src/pathfinder/goals.rs | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs index beb8eedb..7cb8b143 100644 --- a/azalea-core/src/position.rs +++ b/azalea-core/src/position.rs @@ -358,6 +358,16 @@ impl BlockPos { } } + /// Get the center of the bottom of a block position by adding 0.5 to the x + /// and z coordinates. + pub fn center_bottom(&self) -> Vec3 { + Vec3 { + x: self.x as f64 + 0.5, + y: self.y as f64, + z: self.z as f64 + 0.5, + } + } + /// Convert the block position into a Vec3 without centering it. pub fn to_vec3_floored(&self) -> Vec3 { Vec3 { diff --git a/azalea/src/pathfinder/goals.rs b/azalea/src/pathfinder/goals.rs index 36fca762..aa6f357a 100644 --- a/azalea/src/pathfinder/goals.rs +++ b/azalea/src/pathfinder/goals.rs @@ -113,14 +113,14 @@ impl Goal for RadiusGoal { let dx = (self.pos.x - n.x) as f32; let dy = (self.pos.y - n.y) as f32; let dz = (self.pos.z - n.z) as f32; - dx * dx + dy * dy + dz * dz + dx.powi(2) + dy.powi(2) + dz.powi(2) } fn success(&self, n: BlockPos) -> bool { let n = n.center(); let dx = (self.pos.x - n.x) as f32; let dy = (self.pos.y - n.y) as f32; let dz = (self.pos.z - n.z) as f32; - dx * dx + dy * dy + dz * dz <= self.radius * self.radius + dx.powi(2) + dy.powi(2) + dz.powi(2) <= self.radius.powi(2) } } @@ -226,7 +226,7 @@ impl Goal for ReachBlockPosGoal { return false; } - let eye_position = n.to_vec3_floored() + Vec3::new(0.5, 1.62, 0.5); + let eye_position = n.center_bottom().up(1.62); let look_direction = crate::direction_looking_at(&eye_position, &self.pos.center()); let block_hit_result = azalea_client::interact::pick_block( &look_direction, From 61443fa481aeb6f9b488bf9475c512554a58473d Mon Sep 17 00:00:00 2001 From: mat Date: Mon, 2 Jun 2025 21:51:08 -0930 Subject: [PATCH 10/36] fix wrong sequence number being sent --- azalea-client/src/plugins/interact.rs | 11 +++++------ azalea-client/src/plugins/mining.rs | 8 ++++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/azalea-client/src/plugins/interact.rs b/azalea-client/src/plugins/interact.rs index 712e3242..95f7c17c 100644 --- a/azalea-client/src/plugins/interact.rs +++ b/azalea-client/src/plugins/interact.rs @@ -104,10 +104,9 @@ pub struct CurrentSequenceNumber(u32); impl CurrentSequenceNumber { /// Get the next sequence number that we're going to use and increment the /// value. - pub fn get_and_increment(&mut self) -> u32 { - let cur = self.0; + pub fn get_next(&mut self) -> u32 { self.0 += 1; - cur + self.0 } } @@ -148,7 +147,7 @@ pub fn handle_start_use_item_event( /// just inserts this component for you. /// /// [`GameTick`]: azalea_core::tick::GameTick -#[derive(Component)] +#[derive(Component, Debug)] pub struct StartUseItemQueued { pub hand: InteractionHand, /// Optionally force us to send a [`ServerboundUseItemOn`] on the given @@ -209,7 +208,7 @@ pub fn handle_start_use_item_queued( entity, ServerboundUseItem { hand: start_use_item.hand, - sequence: sequence_number.get_and_increment(), + sequence: sequence_number.get_next(), x_rot: look_direction.x_rot, y_rot: look_direction.y_rot, }, @@ -220,7 +219,7 @@ pub fn handle_start_use_item_queued( ServerboundUseItemOn { hand: start_use_item.hand, block_hit: block_hit_result.into(), - sequence: sequence_number.get_and_increment(), + sequence: sequence_number.get_next(), }, )); // TODO: depending on the result of useItemOn, this might diff --git a/azalea-client/src/plugins/mining.rs b/azalea-client/src/plugins/mining.rs index 6372e6c8..f69c30f0 100644 --- a/azalea-client/src/plugins/mining.rs +++ b/azalea-client/src/plugins/mining.rs @@ -1,4 +1,4 @@ -use azalea_block::{BlockTrait, BlockState, fluid_state::FluidState}; +use azalea_block::{BlockState, BlockTrait, fluid_state::FluidState}; use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick}; use azalea_entity::{FluidOnEyes, Physics, mining::get_mine_progress}; use azalea_inventory::ItemStack; @@ -345,7 +345,7 @@ fn handle_mining_queued( action: s_player_action::Action::StartDestroyBlock, pos: mining_queued.position, direction: mining_queued.direction, - sequence: sequence_number.get_and_increment(), + sequence: sequence_number.get_next(), }, )); // vanilla really does send two swing arm packets @@ -580,7 +580,7 @@ pub fn continue_mining_block( action: s_player_action::Action::StartDestroyBlock, pos: mining.pos, direction: mining.dir, - sequence: sequence_number.get_and_increment(), + sequence: sequence_number.get_next(), }, )); commands.trigger(SwingArmEvent { entity }); @@ -634,7 +634,7 @@ pub fn continue_mining_block( action: s_player_action::Action::StopDestroyBlock, pos: mining.pos, direction: mining.dir, - sequence: sequence_number.get_and_increment(), + sequence: sequence_number.get_next(), }, )); **mine_progress = 0.; From cc3e64a3151398046408a7b97c339d32700cc541 Mon Sep 17 00:00:00 2001 From: mat Date: Mon, 2 Jun 2025 19:24:39 -1300 Subject: [PATCH 11/36] fix collisions bugs --- azalea-client/src/client.rs | 7 ---- azalea-entity/src/lib.rs | 7 +++- azalea-physics/src/collision/shape.rs | 53 ++++++++++++++++++--------- azalea/src/pathfinder/mod.rs | 2 +- 4 files changed, 43 insertions(+), 26 deletions(-) diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 02625326..f73e939b 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -338,13 +338,6 @@ impl Client { /// Similar to [`Self::get_component`], but doesn't clone the component /// since it's passed as a reference. [`Self::ecs`] will remain locked /// while the callback is being run. - /// - /// ``` - /// # use azalea_client::{Client, mining::Mining}; - /// # fn example(bot: &Client) { - /// let is_mining = bot.map_get_component::(|m| m).is_some(); - /// # } - /// ``` pub fn map_get_component(&self, f: impl FnOnce(&T) -> R) -> Option { let mut ecs = self.ecs.lock(); let value = self.query::>(&mut ecs); diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs index ad225400..0f33e01e 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -455,7 +455,12 @@ impl EntityBundle { world_name: ResourceLocation, ) -> Self { let dimensions = EntityDimensions::from(kind); - let eye_height = dimensions.height * 0.85; + let eye_height = match kind { + // TODO: codegen hardcoded eye heights, search withEyeHeight with mojmap + // also, eye height should change depending on pose (like sneaking, swimming, etc) + azalea_registry::EntityKind::Player => 1.62, + _ => dimensions.height * 0.85, + }; Self { kind: EntityKind(kind), diff --git a/azalea-physics/src/collision/shape.rs b/azalea-physics/src/collision/shape.rs index e27e4c2a..902ae20c 100644 --- a/azalea-physics/src/collision/shape.rs +++ b/azalea-physics/src/collision/shape.rs @@ -399,17 +399,8 @@ impl VoxelShape { } pub fn find_index(&self, axis: Axis, coord: f64) -> i32 { - // let r = binary_search(0, (self.shape().size(axis) + 1) as i32, &|t| { - // coord < self.get(axis, t as usize) - // }) - 1; - // r - match self { - VoxelShape::Cube(s) => s.find_index(axis, coord), - _ => { - let upper_limit = (self.shape().size(axis) + 1) as i32; - binary_search(0, upper_limit, |t| coord < self.get(axis, t as usize)) - 1 - } - } + let upper_limit = (self.shape().size(axis) + 1) as i32; + binary_search(0, upper_limit, |t| coord < self.get(axis, t as usize)) - 1 } pub fn clip(&self, from: &Vec3, to: &Vec3, block_pos: &BlockPos) -> Option { @@ -420,7 +411,7 @@ impl VoxelShape { if vector.length_squared() < EPSILON { return None; } - let right_after_start = from + &(vector * 0.0001); + let right_after_start = from + &(vector * 0.001); if self.shape().is_full_wide( self.find_index(Axis::X, right_after_start.x - block_pos.x as f64), @@ -645,7 +636,6 @@ impl ArrayVoxelShape { impl CubeVoxelShape { pub fn new(shape: DiscreteVoxelShape) -> Self { - // pre-calculate the coor let x_coords = Self::calculate_coords(&shape, Axis::X); let y_coords = Self::calculate_coords(&shape, Axis::Y); let z_coords = Self::calculate_coords(&shape, Axis::Z); @@ -679,10 +669,11 @@ impl CubeVoxelShape { axis.choose(&self.x_coords, &self.y_coords, &self.z_coords) } - fn find_index(&self, axis: Axis, coord: f64) -> i32 { - let n = self.shape().size(axis); - (f64::clamp(coord * (n as f64), -1f64, n as f64)) as i32 - } + // unused + // fn find_index(&self, axis: Axis, coord: f64) -> i32 { + // let n = self.shape().size(axis); + // (f64::clamp(coord * (n as f64), -1f64, n as f64)) as i32 + // } } #[derive(Debug)] @@ -752,4 +743,32 @@ mod tests { let joined = Shapes::matches_anywhere(&shape, &shape2, |a, b| a && b); assert!(joined, "Shapes should intersect"); } + + #[test] + fn clip_in_front_of_block() { + let block_shape = &*BLOCK_SHAPE; + let block_hit_result = block_shape + .clip( + &Vec3::new(-0.3, 0.5, 0.), + &Vec3::new(5.3, 0.5, 0.), + &BlockPos::new(0, 0, 0), + ) + .unwrap(); + + assert_eq!( + block_hit_result, + BlockHitResult { + location: Vec3 { + x: 0.0, + y: 0.5, + z: 0.0 + }, + direction: Direction::West, + block_pos: BlockPos { x: 0, y: 0, z: 0 }, + inside: false, + world_border: false, + miss: false + } + ); + } } diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index b05d2aab..c4586d29 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -1569,7 +1569,7 @@ mod tests { simulation.app.world_mut().send_event(GotoEvent { entity: simulation.entity, - goal: Arc::new(BlockPosGoal(BlockPos::new(0, 70, 0))), + goal: Arc::new(BlockPosGoal(BlockPos::new(0, 69, 0))), successors_fn: moves::default_move, allow_mining: true, min_timeout: PathfinderTimeout::Nodes(1_000_000), From 415c0d873e7e793bbc8304247b828355d3ea8118 Mon Sep 17 00:00:00 2001 From: mat Date: Mon, 2 Jun 2025 23:44:49 -0900 Subject: [PATCH 12/36] fix CubeVoxelShape::find_index --- azalea-physics/src/collision/shape.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/azalea-physics/src/collision/shape.rs b/azalea-physics/src/collision/shape.rs index 902ae20c..59671622 100644 --- a/azalea-physics/src/collision/shape.rs +++ b/azalea-physics/src/collision/shape.rs @@ -399,8 +399,13 @@ impl VoxelShape { } pub fn find_index(&self, axis: Axis, coord: f64) -> i32 { - let upper_limit = (self.shape().size(axis) + 1) as i32; - binary_search(0, upper_limit, |t| coord < self.get(axis, t as usize)) - 1 + match self { + VoxelShape::Cube(s) => s.find_index(axis, coord), + _ => { + let upper_limit = (self.shape().size(axis) + 1) as i32; + binary_search(0, upper_limit, |t| coord < self.get(axis, t as usize)) - 1 + } + } } pub fn clip(&self, from: &Vec3, to: &Vec3, block_pos: &BlockPos) -> Option { @@ -669,11 +674,10 @@ impl CubeVoxelShape { axis.choose(&self.x_coords, &self.y_coords, &self.z_coords) } - // unused - // fn find_index(&self, axis: Axis, coord: f64) -> i32 { - // let n = self.shape().size(axis); - // (f64::clamp(coord * (n as f64), -1f64, n as f64)) as i32 - // } + fn find_index(&self, axis: Axis, coord: f64) -> i32 { + let n = self.shape().size(axis); + f64::floor(f64::clamp(coord * (n as f64), -1f64, n as f64)) as i32 + } } #[derive(Debug)] From f311ac27d47c43eb4c33d760f3e1d1f2b8008a4f Mon Sep 17 00:00:00 2001 From: mat Date: Tue, 3 Jun 2025 22:01:50 +0330 Subject: [PATCH 13/36] send ServerboundPlayerLoaded on join and respawn --- azalea-client/src/plugins/disconnect.rs | 4 +- azalea-client/src/plugins/loading.rs | 40 +++++++++++++++++++ azalea-client/src/plugins/mod.rs | 2 + .../src/plugins/packet/game/events.rs | 5 ++- azalea-client/src/plugins/packet/game/mod.rs | 6 ++- 5 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 azalea-client/src/plugins/loading.rs diff --git a/azalea-client/src/plugins/disconnect.rs b/azalea-client/src/plugins/disconnect.rs index c6c01e35..8dddff09 100644 --- a/azalea-client/src/plugins/disconnect.rs +++ b/azalea-client/src/plugins/disconnect.rs @@ -10,7 +10,7 @@ use tracing::info; use super::login::IsAuthenticated; use crate::{ - chat_signing, client::JoinedClientBundle, connection::RawConnection, + chat_signing, client::JoinedClientBundle, connection::RawConnection, loading::HasClientLoaded, local_player::InstanceHolder, }; @@ -69,6 +69,8 @@ pub struct RemoveOnDisconnectBundle { pub chat_signing_session: chat_signing::ChatSigningSession, /// They're not authenticated anymore if they disconnected. pub is_authenticated: IsAuthenticated, + // send ServerboundPlayerLoaded next time we join + pub has_client_loaded: HasClientLoaded, } /// A system that removes the several components from our clients when they get diff --git a/azalea-client/src/plugins/loading.rs b/azalea-client/src/plugins/loading.rs new file mode 100644 index 00000000..33290f39 --- /dev/null +++ b/azalea-client/src/plugins/loading.rs @@ -0,0 +1,40 @@ +use azalea_core::tick::GameTick; +use azalea_entity::InLoadedChunk; +use azalea_physics::PhysicsSet; +use azalea_protocol::packets::game::ServerboundPlayerLoaded; +use bevy_app::{App, Plugin}; +use bevy_ecs::prelude::*; + +use crate::{mining::MiningSet, packet::game::SendPacketEvent}; + +pub struct PlayerLoadedPlugin; +impl Plugin for PlayerLoadedPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + GameTick, + player_loaded_packet + .after(PhysicsSet) + .after(MiningSet) + .after(crate::movement::send_position), + ); + } +} + +// this component is removed on respawn or disconnect +// (notably, it's not removed on login) + +// mojmap interchangably calls it 'has client loaded' and 'has player loaded', i +// prefer the client one because it makes it clear that the component is only +// present on our own clients + +#[derive(Component)] +pub struct HasClientLoaded; +pub fn player_loaded_packet( + mut commands: Commands, + query: Query, Without)>, +) { + for entity in query.iter() { + commands.trigger(SendPacketEvent::new(entity, ServerboundPlayerLoaded)); + commands.entity(entity).insert(HasClientLoaded); + } +} diff --git a/azalea-client/src/plugins/mod.rs b/azalea-client/src/plugins/mod.rs index dea2fdb8..6f003b01 100644 --- a/azalea-client/src/plugins/mod.rs +++ b/azalea-client/src/plugins/mod.rs @@ -12,6 +12,7 @@ pub mod events; pub mod interact; pub mod inventory; pub mod join; +pub mod loading; pub mod login; pub mod mining; pub mod movement; @@ -48,6 +49,7 @@ impl PluginGroup for DefaultPlugins { .add(attack::AttackPlugin) .add(chunks::ChunksPlugin) .add(tick_end::TickEndPlugin) + .add(loading::PlayerLoadedPlugin) .add(brand::BrandPlugin) .add(tick_broadcast::TickBroadcastPlugin) .add(pong::PongPlugin) diff --git a/azalea-client/src/plugins/packet/game/events.rs b/azalea-client/src/plugins/packet/game/events.rs index 7134a2f2..e341db3e 100644 --- a/azalea-client/src/plugins/packet/game/events.rs +++ b/azalea-client/src/plugins/packet/game/events.rs @@ -57,7 +57,6 @@ pub fn handle_outgoing_packets_observer( mut query: Query<(&mut RawConnection, Option<&InGameState>)>, ) { let event = trigger.event(); - trace!("Sending game packet: {:?}", event.packet); if let Ok((mut raw_connection, in_game_state)) = query.get_mut(event.sent_by) { if in_game_state.is_none() { @@ -68,10 +67,12 @@ pub fn handle_outgoing_packets_observer( return; } - // debug!("Sending game packet: {:?}", event.packet); + trace!("Sending game packet: {:?}", event.packet); if let Err(e) = raw_connection.write(event.packet.clone()) { error!("Failed to send packet: {e}"); } + } else { + trace!("Not sending game packet: {:?}", event.packet); } } diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs index 63c1bafa..670058c3 100644 --- a/azalea-client/src/plugins/packet/game/mod.rs +++ b/azalea-client/src/plugins/packet/game/mod.rs @@ -29,6 +29,7 @@ use crate::{ inventory::{ ClientSideCloseContainerEvent, Inventory, MenuOpenedEvent, SetContainerContentEvent, }, + loading::HasClientLoaded, local_player::{Hunger, InstanceHolder, LocalGameMode, PlayerAbilities, TabList}, movement::{KnockbackEvent, KnockbackType}, packet::as_system, @@ -1490,8 +1491,9 @@ impl GamePacketHandler<'_> { entity_bundle, )); - // Remove the Dead marker component from the player. - commands.entity(self.player).remove::(); + commands + .entity(self.player) + .remove::<(Dead, HasClientLoaded)>(); }, ) } From f5f50b85e5d427aab6a0ef00570b4076b61babe8 Mon Sep 17 00:00:00 2001 From: mat Date: Wed, 4 Jun 2025 01:53:24 -0330 Subject: [PATCH 14/36] re-enable click prediction and fix related issues --- azalea-client/src/plugins/inventory.rs | 233 ++++++++++++++++-- azalea-client/src/plugins/loading.rs | 12 +- .../src/location_enum.rs | 1 + azalea-inventory/src/operations.rs | 39 ++- azalea-inventory/src/slot.rs | 44 ++-- azalea/src/container.rs | 37 ++- 6 files changed, 305 insertions(+), 61 deletions(-) diff --git a/azalea-client/src/plugins/inventory.rs b/azalea-client/src/plugins/inventory.rs index a7e45ffb..29a81410 100644 --- a/azalea-client/src/plugins/inventory.rs +++ b/azalea-client/src/plugins/inventory.rs @@ -1,4 +1,7 @@ -use std::collections::{HashMap, HashSet}; +use std::{ + cmp, + collections::{HashMap, HashSet}, +}; use azalea_chat::FormattedText; pub use azalea_inventory::*; @@ -341,30 +344,95 @@ impl Inventory { // player.drop(item, true); } } - ClickOperation::Pickup( - PickupClick::Left { slot: Some(slot) } | PickupClick::Right { slot: Some(slot) }, + &ClickOperation::Pickup( + // lol + ref pickup @ (PickupClick::Left { slot: Some(slot) } + | PickupClick::Right { slot: Some(slot) }), ) => { - let Some(slot_item) = self.menu().slot(*slot as usize) else { + let slot = slot as usize; + let Some(slot_item) = self.menu().slot(slot) else { return; }; - let carried = &self.carried; - // vanilla does a check called tryItemClickBehaviourOverride - // here - // i don't understand it so i didn't implement it + + if self.try_item_click_behavior_override(operation, slot) { + return; + } + + let is_left_click = matches!(pickup, PickupClick::Left { .. }); + match slot_item { - ItemStack::Empty => if carried.is_present() {}, - ItemStack::Present(_) => todo!(), + ItemStack::Empty => { + if self.carried.is_present() { + let place_count = if is_left_click { + self.carried.count() + } else { + 1 + }; + self.carried = + self.safe_insert(slot, self.carried.clone(), place_count); + } + } + ItemStack::Present(_) => { + if !self.menu().may_pickup(slot) { + return; + } + if let ItemStack::Present(carried) = self.carried.clone() { + let slot_is_same_item_as_carried = slot_item + .as_present() + .is_some_and(|s| carried.is_same_item_and_components(s)); + + if self.menu().may_place(slot, &carried) { + if slot_is_same_item_as_carried { + let place_count = if is_left_click { carried.count } else { 1 }; + self.carried = + self.safe_insert(slot, self.carried.clone(), place_count); + } else if carried.count + <= self + .menu() + .max_stack_size(slot) + .min(carried.kind.max_stack_size()) + { + // swap slot_item and carried + self.carried = slot_item.clone(); + let slot_item = self.menu_mut().slot_mut(slot).unwrap(); + *slot_item = carried.into(); + } + } else if slot_is_same_item_as_carried + && let Some(removed) = self.try_remove( + slot, + slot_item.count(), + carried.kind.max_stack_size() - carried.count, + ) + { + self.carried.as_present_mut().unwrap().count += removed.count(); + // slot.onTake(player, removed); + } + } else { + let pickup_count = if is_left_click { + slot_item.count() + } else { + (slot_item.count() + 1) / 2 + }; + if let Some(new_slot_item) = + self.try_remove(slot, pickup_count, i32::MAX) + { + self.carried = new_slot_item; + // slot.onTake(player, newSlot); + } + } + } } } - ClickOperation::QuickMove( + &ClickOperation::QuickMove( QuickMoveClick::Left { slot } | QuickMoveClick::Right { slot }, ) => { // in vanilla it also tests if QuickMove has a slot index of -999 // but i don't think that's ever possible so it's not covered here + let slot = slot as usize; loop { - let new_slot_item = self.menu_mut().quick_move_stack(*slot as usize); - let slot_item = self.menu().slot(*slot as usize).unwrap(); - if new_slot_item.is_empty() || slot_item != &new_slot_item { + let new_slot_item = self.menu_mut().quick_move_stack(slot); + let slot_item = self.menu().slot(slot).unwrap(); + if new_slot_item.is_empty() || slot_item.kind() != new_slot_item.kind() { break; } } @@ -390,15 +458,16 @@ impl Inventory { *target_slot = source_slot; } } else if source_slot.is_empty() { - let ItemStack::Present(target_item) = target_slot else { - unreachable!("target slot is not empty but is not present"); - }; + let target_item = target_slot + .as_present() + .expect("target slot was already checked to not be empty"); if self.menu().may_place(source_slot_index, target_item) { // get the target_item but mutable let source_max_stack_size = self.menu().max_stack_size(source_slot_index); let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); - let new_source_slot = target_slot.split(source_max_stack_size); + let new_source_slot = + target_slot.split(source_max_stack_size.try_into().unwrap()); *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot; } } else if self.menu().may_pickup(source_slot_index) { @@ -407,11 +476,12 @@ impl Inventory { }; if self.menu().may_place(source_slot_index, target_item) { let source_max_stack = self.menu().max_stack_size(source_slot_index); - if target_slot.count() > source_max_stack as i32 { + if target_slot.count() > source_max_stack { // if there's more than the max stack size in the target slot let target_slot = self.menu_mut().slot_mut(target_slot_index).unwrap(); - let new_source_slot = target_slot.split(source_max_stack); + let new_source_slot = + target_slot.split(source_max_stack.try_into().unwrap()); *self.menu_mut().slot_mut(source_slot_index).unwrap() = new_source_slot; // if !self.inventory_menu.add(new_source_slot) { // player.drop(new_source_slot, true); @@ -535,6 +605,67 @@ impl Inventory { let hotbar_items = &inventory.slots()[inventory.hotbar_slots_range()]; hotbar_items[self.selected_hotbar_slot as usize].clone() } + + /// TODO: implement bundles + fn try_item_click_behavior_override( + &self, + _operation: &ClickOperation, + _slot_item_index: usize, + ) -> bool { + false + } + + fn safe_insert(&mut self, slot: usize, src_item: ItemStack, take_count: i32) -> ItemStack { + let Some(slot_item) = self.menu_mut().slot_mut(slot) else { + return src_item; + }; + let ItemStack::Present(mut src_item) = src_item else { + return src_item; + }; + + let take_count = cmp::min( + cmp::min(take_count, src_item.count), + src_item.kind.max_stack_size() - slot_item.count(), + ); + if take_count <= 0 { + return src_item.into(); + } + let take_count = take_count as u32; + + if slot_item.is_empty() { + *slot_item = src_item.split(take_count).into(); + } else if let ItemStack::Present(slot_item) = slot_item + && slot_item.is_same_item_and_components(&src_item) + { + src_item.count -= take_count as i32; + slot_item.count += take_count as i32; + } + + src_item.into() + } + + fn try_remove(&mut self, slot: usize, count: i32, limit: i32) -> Option { + if !self.menu().may_pickup(slot) { + return None; + } + let mut slot_item = self.menu().slot(slot)?.clone(); + if !self.menu().allow_modification(slot) && limit < slot_item.count() { + return None; + } + + let count = count.min(limit); + if count <= 0 { + return None; + } + // vanilla calls .remove here but i think it has the same behavior as split? + let removed = slot_item.split(count as u32); + + if removed.is_present() && slot_item.is_empty() { + *self.menu_mut().slot_mut(slot).unwrap() = ItemStack::Empty; + } + + Some(removed) + } } fn can_item_quick_replace( @@ -680,12 +811,12 @@ pub struct ContainerClickEvent { pub operation: ClickOperation, } pub fn handle_container_click_event( - mut query: Query<(Entity, &mut Inventory)>, + mut query: Query<(Entity, &mut Inventory, Option<&PlayerAbilities>)>, mut events: EventReader, mut commands: Commands, ) { for event in events.read() { - let (entity, mut inventory) = query.get_mut(event.entity).unwrap(); + let (entity, mut inventory, player_abilities) = query.get_mut(event.entity).unwrap(); if inventory.id != event.window_id { error!( "Tried to click container with ID {}, but the current container ID is {}. Click packet won't be sent.", @@ -694,16 +825,18 @@ pub fn handle_container_click_event( continue; } - let menu = inventory.menu_mut(); - let old_slots = menu.slots().clone(); - - // menu.click(&event.operation); + let old_slots = inventory.menu().slots(); + inventory.simulate_click( + &event.operation, + player_abilities.unwrap_or(&PlayerAbilities::default()), + ); + let new_slots = inventory.menu().slots(); // see which slots changed after clicking and put them in the hashmap // the server uses this to check if we desynced let mut changed_slots: HashMap = HashMap::new(); for (slot_index, old_slot) in old_slots.iter().enumerate() { - let new_slot = &menu.slots()[slot_index]; + let new_slot = &new_slots[slot_index]; if old_slot != new_slot { changed_slots.insert(slot_index as u16, HashedStack::from(new_slot)); } @@ -784,3 +917,49 @@ fn handle_set_selected_hotbar_slot_event( )); } } + +#[cfg(test)] +mod tests { + use azalea_registry::Item; + + use super::*; + + #[test] + fn test_simulate_shift_click_in_crafting_table() { + let spruce_planks = ItemStack::Present(ItemStackData { + count: 4, + kind: Item::SprucePlanks, + components: Default::default(), + }); + + let mut inventory = Inventory { + inventory_menu: Menu::Player(azalea_inventory::Player::default()), + id: 1, + container_menu: Some(Menu::Crafting { + result: spruce_planks.clone(), + // simulate_click won't delete the items from here + grid: SlotList::default(), + player: SlotList::default(), + }), + container_menu_title: None, + carried: ItemStack::Empty, + state_id: 0, + quick_craft_status: QuickCraftStatusKind::Start, + quick_craft_kind: QuickCraftKind::Middle, + quick_craft_slots: HashSet::new(), + selected_hotbar_slot: 0, + }; + + inventory.simulate_click( + &ClickOperation::QuickMove(QuickMoveClick::Left { slot: 0 }), + &PlayerAbilities::default(), + ); + + let new_slots = inventory.menu().slots(); + assert_eq!(&new_slots[0], &ItemStack::Empty); + assert_eq!( + &new_slots[*Menu::CRAFTING_PLAYER_SLOTS.start()], + &spruce_planks + ); + } +} diff --git a/azalea-client/src/plugins/loading.rs b/azalea-client/src/plugins/loading.rs index 33290f39..217d6f75 100644 --- a/azalea-client/src/plugins/loading.rs +++ b/azalea-client/src/plugins/loading.rs @@ -1,5 +1,5 @@ use azalea_core::tick::GameTick; -use azalea_entity::InLoadedChunk; +use azalea_entity::{InLoadedChunk, LocalEntity}; use azalea_physics::PhysicsSet; use azalea_protocol::packets::game::ServerboundPlayerLoaded; use bevy_app::{App, Plugin}; @@ -29,9 +29,17 @@ impl Plugin for PlayerLoadedPlugin { #[derive(Component)] pub struct HasClientLoaded; +#[allow(clippy::type_complexity)] pub fn player_loaded_packet( mut commands: Commands, - query: Query, Without)>, + query: Query< + Entity, + ( + With, + With, + Without, + ), + >, ) { for entity in query.iter() { commands.trigger(SendPacketEvent::new(entity, ServerboundPlayerLoaded)); diff --git a/azalea-inventory/azalea-inventory-macros/src/location_enum.rs b/azalea-inventory/azalea-inventory-macros/src/location_enum.rs index 615f07e5..46db3e76 100644 --- a/azalea-inventory/azalea-inventory-macros/src/location_enum.rs +++ b/azalea-inventory/azalea-inventory-macros/src/location_enum.rs @@ -48,6 +48,7 @@ pub fn generate(input: &DeclareMenus) -> TokenStream { } quote! { + #[derive(Debug)] pub enum MenuLocation { #menu_location_variants } diff --git a/azalea-inventory/src/operations.rs b/azalea-inventory/src/operations.rs index f410c2c5..e7668ab5 100644 --- a/azalea-inventory/src/operations.rs +++ b/azalea-inventory/src/operations.rs @@ -617,13 +617,26 @@ impl Menu { } /// Whether the item in the given slot could be clicked and picked up. + /// /// TODO: right now this always returns true pub fn may_pickup(&self, _source_slot_index: usize) -> bool { true } + /// Whether the item in the slot can be picked up and placed. + pub fn allow_modification(&self, target_slot_index: usize) -> bool { + if !self.may_pickup(target_slot_index) { + return false; + } + let item = self.slot(target_slot_index).unwrap(); + // the default here probably doesn't matter since we should only be calling this + // if we already checked that the slot isn't empty + item.as_present() + .is_some_and(|item| self.may_place(target_slot_index, item)) + } + /// Get the maximum number of items that can be placed in this slot. - pub fn max_stack_size(&self, _target_slot_index: usize) -> u32 { + pub fn max_stack_size(&self, _target_slot_index: usize) -> i32 { 64 } @@ -657,7 +670,10 @@ impl Menu { } } - item_slot.is_empty() + let is_source_slot_now_empty = item_slot.is_empty(); + + *self.slot_mut(item_slot_index).unwrap() = item_slot; + is_source_slot_now_empty } /// Merge this item slot into the target item slot, only if the target item @@ -677,7 +693,7 @@ impl Menu { && target_item.is_same_item_and_components(item) { let slot_item_limit = self.max_stack_size(target_slot_index); - let new_target_slot_data = item.split(u32::min(slot_item_limit, item.count as u32)); + let new_target_slot_data = item.split(i32::min(slot_item_limit, item.count) as u32); // get the target slot again but mut this time so we can update it let target_slot = self.slot_mut(target_slot_index).unwrap(); @@ -688,18 +704,23 @@ impl Menu { } } - fn move_item_to_slot_if_empty(&mut self, item_slot: &mut ItemStack, target_slot_index: usize) { - let ItemStack::Present(item) = item_slot else { + fn move_item_to_slot_if_empty( + &mut self, + source_item: &mut ItemStack, + target_slot_index: usize, + ) { + let ItemStack::Present(source_item_data) = source_item else { return; }; let target_slot = self.slot(target_slot_index).unwrap(); - if target_slot.is_empty() && self.may_place(target_slot_index, item) { + if target_slot.is_empty() && self.may_place(target_slot_index, source_item_data) { let slot_item_limit = self.max_stack_size(target_slot_index); - let new_target_slot_data = item.split(u32::min(slot_item_limit, item.count as u32)); + let new_target_slot_data = + source_item_data.split(i32::min(slot_item_limit, source_item_data.count) as u32); + source_item.update_empty(); let target_slot = self.slot_mut(target_slot_index).unwrap(); - *target_slot = ItemStack::Present(new_target_slot_data); - item_slot.update_empty(); + *target_slot = new_target_slot_data.into(); } } } diff --git a/azalea-inventory/src/slot.rs b/azalea-inventory/src/slot.rs index 66f8bf50..2d00236f 100644 --- a/azalea-inventory/src/slot.rs +++ b/azalea-inventory/src/slot.rs @@ -87,6 +87,13 @@ impl ItemStack { ItemStack::Present(i) => Some(i), } } + + pub fn as_present_mut(&mut self) -> Option<&mut ItemStackData> { + match self { + ItemStack::Empty => None, + ItemStack::Present(i) => Some(i), + } + } } /// An item in an inventory, with a count and NBT. Usually you want @@ -172,6 +179,16 @@ impl AzaleaWrite for ItemStack { } } +impl From for ItemStack { + fn from(item: ItemStackData) -> Self { + if item.is_empty() { + ItemStack::Empty + } else { + ItemStack::Present(item) + } + } +} + /// An update to an item's data components. /// /// Note that in vanilla items come with their own set of default components, @@ -311,24 +328,19 @@ impl PartialEq for DataComponentPatch { return false; } for (kind, component) in &self.components { - match other.components.get(kind) { - Some(other_component) => { - // we can't use PartialEq, but we can use our own eq method - if let Some(component) = component { - if let Some(other_component) = other_component { - if !component.eq((*other_component).clone()) { - return false; - } - } else { - return false; - } - } else if other_component.is_some() { - return false; - } - } - _ => { + let Some(other_component) = other.components.get(kind) else { + return false; + }; + // we can't use PartialEq, but we can use our own eq method + if let Some(component) = component { + let Some(other_component) = other_component else { + return false; + }; + if !component.eq((*other_component).clone()) { return false; } + } else if other_component.is_some() { + return false; } } true diff --git a/azalea/src/container.rs b/azalea/src/container.rs index da3ddb8a..3521c06d 100644 --- a/azalea/src/container.rs +++ b/azalea/src/container.rs @@ -32,7 +32,7 @@ pub trait ContainerClientExt { fn open_inventory(&self) -> Option; fn get_held_item(&self) -> ItemStack; fn get_open_container(&self) -> Option; - fn view_inventory(&self) -> Menu; + fn view_container_or_inventory(&self) -> Menu; } impl ContainerClientExt for Client { @@ -124,13 +124,19 @@ impl ContainerClientExt for Client { } } - /// Returns the player's inventory menu. + /// Returns the player's currently open container menu, or their inventory + /// if no container is open. /// - /// This is a shortcut for accessing the client's - /// [`Inventory::inventory_menu`]. - fn view_inventory(&self) -> Menu { - self.map_get_component::(|inventory| inventory.inventory_menu.clone()) - .expect("no inventory") + /// This tries to access the client's [`Inventory::container_menu`] and + /// falls back to [`Inventory::inventory_menu`]. + fn view_container_or_inventory(&self) -> Menu { + self.map_get_component::(|inventory| { + inventory + .container_menu + .clone() + .unwrap_or(inventory.inventory_menu.clone()) + }) + .expect("no inventory") } } @@ -192,6 +198,12 @@ impl ContainerHandleRef { self.menu().map(|menu| menu.contents()) } + /// Return the contents of the menu, including the player's inventory. If + /// the container is closed, this will return `None`. + pub fn slots(&self) -> Option> { + self.menu().map(|menu| menu.slots()) + } + pub fn click(&self, operation: impl Into) { let operation = operation.into(); self.client.ecs.lock().send_event(ContainerClickEvent { @@ -246,6 +258,17 @@ impl ContainerHandle { self.0.contents() } + /// Return the contents of the menu, including the player's inventory. If + /// the container is closed, this will return `None`. + pub fn slots(&self) -> Option> { + self.0.slots() + } + + /// Closes the inventory by dropping the handle. + pub fn close(self) { + // implicitly calls drop + } + pub fn click(&self, operation: impl Into) { self.0.click(operation); } From bbfca34133fdb97a732a60a0a11ca4ba3276a63a Mon Sep 17 00:00:00 2001 From: mat Date: Tue, 3 Jun 2025 17:10:34 -1300 Subject: [PATCH 15/36] copy player part of container_menu to inventory_menu on close --- azalea-client/src/plugins/inventory.rs | 26 +++++++++++++++++++++++++- azalea-inventory/src/lib.rs | 19 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/azalea-client/src/plugins/inventory.rs b/azalea-client/src/plugins/inventory.rs index 29a81410..8efc0e71 100644 --- a/azalea-client/src/plugins/inventory.rs +++ b/azalea-client/src/plugins/inventory.rs @@ -798,7 +798,31 @@ pub fn handle_client_side_close_container_event( ) { for event in events.read() { let mut inventory = query.get_mut(event.entity).unwrap(); - inventory.container_menu = None; + + // copy the Player part of the container_menu to the inventory_menu + if let Some(inventory_menu) = inventory.container_menu.take() { + // this isn't the same as what vanilla does. i believe vanilla synchronizes the + // slots between inventoryMenu and containerMenu by just having the player slots + // point to the same ItemStack in memory, but emulating this in rust would + // require us to wrap our `ItemStack`s as `Arc>` which would + // have kinda terrible ergonomics. + + // the simpler solution i chose to go with here is to only copy the player slots + // when the container is closed. this is perfectly fine for vanilla, but it + // might cause issues if a server modifies id 0 while we have a container + // open... + + // if we do encounter this issue in the wild then the simplest solution would + // probably be to just add logic for updating the container_menu when the server + // tries to modify id 0 for slots within `inventory`. not implemented for now + // because i'm not sure if that's worth worrying about. + + let new_inventory = + inventory_menu.slots()[inventory_menu.player_slots_range()].to_vec(); + let new_inventory = <[ItemStack; 36]>::try_from(new_inventory).unwrap(); + *inventory.inventory_menu.as_player_mut().inventory = new_inventory; + } + inventory.id = 0; inventory.container_menu_title = None; } diff --git a/azalea-inventory/src/lib.rs b/azalea-inventory/src/lib.rs index 91d9f48d..0f74ba3a 100644 --- a/azalea-inventory/src/lib.rs +++ b/azalea-inventory/src/lib.rs @@ -32,6 +32,11 @@ impl Default for SlotList { SlotList([(); N].map(|_| ItemStack::Empty)) } } +impl SlotList { + pub fn new(items: [ItemStack; N]) -> Self { + SlotList(items) + } +} impl Menu { /// Get the [`Player`] from this [`Menu`]. @@ -46,6 +51,20 @@ impl Menu { unreachable!("Called `Menu::as_player` on a menu that wasn't `Player`.") } } + + /// Same as [`Menu::as_player`], but returns a mutable reference to the + /// [`Player`]. + /// + /// # Panics + /// + /// Will panic if the menu isn't `Menu::Player`. + pub fn as_player_mut(&mut self) -> &mut Player { + if let Menu::Player(player) = self { + player + } else { + unreachable!("Called `Menu::as_player_mut` on a menu that wasn't `Player`.") + } + } } // the player inventory part is always the last 36 slots (except in the Player From 123c15a2936639244c3485c5db789c845f7c2e43 Mon Sep 17 00:00:00 2001 From: mat Date: Wed, 4 Jun 2025 12:16:12 +0600 Subject: [PATCH 16/36] fix hashset of blockstate into blockstates impl --- azalea-block/src/range.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/azalea-block/src/range.rs b/azalea-block/src/range.rs index cbe77284..7960b5c4 100644 --- a/azalea-block/src/range.rs +++ b/azalea-block/src/range.rs @@ -47,16 +47,16 @@ impl Add for BlockStates { impl From> for BlockStates { fn from(set: HashSet) -> Self { - Self { - set: set.into_iter().map(|b| b.into()).collect(), - } + Self::from(&set) } } impl From<&HashSet> for BlockStates { fn from(set: &HashSet) -> Self { - Self { - set: set.iter().map(|&b| b.into()).collect(), + let mut block_states = HashSet::with_capacity(set.len()); + for &block in set { + block_states.extend(BlockStates::from(block)); } + Self { set: block_states } } } From 93a96786a8a46068f5d9d7d3e503645d7fc4b830 Mon Sep 17 00:00:00 2001 From: mat Date: Wed, 4 Jun 2025 10:26:32 +0330 Subject: [PATCH 17/36] handle set_held_slot packet and add more Into BlockStates impls --- azalea-block/src/range.rs | 25 ++++++++++++++++---- azalea-client/src/plugins/packet/game/mod.rs | 11 ++++++++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/azalea-block/src/range.rs b/azalea-block/src/range.rs index 7960b5c4..76b18079 100644 --- a/azalea-block/src/range.rs +++ b/azalea-block/src/range.rs @@ -3,6 +3,8 @@ use std::{ ops::{Add, RangeInclusive}, }; +use azalea_registry::Block; + use crate::{BlockState, block_state::BlockStateIntegerRepr}; #[derive(Debug, Clone)] @@ -45,14 +47,14 @@ impl Add for BlockStates { } } -impl From> for BlockStates { - fn from(set: HashSet) -> Self { +impl From> for BlockStates { + fn from(set: HashSet) -> Self { Self::from(&set) } } -impl From<&HashSet> for BlockStates { - fn from(set: &HashSet) -> Self { +impl From<&HashSet> for BlockStates { + fn from(set: &HashSet) -> Self { let mut block_states = HashSet::with_capacity(set.len()); for &block in set { block_states.extend(BlockStates::from(block)); @@ -60,3 +62,18 @@ impl From<&HashSet> for BlockStates { Self { set: block_states } } } + +impl From<[Block; N]> for BlockStates { + fn from(arr: [Block; N]) -> Self { + Self::from(&arr[..]) + } +} +impl From<&[Block]> for BlockStates { + fn from(arr: &[Block]) -> Self { + let mut block_states = HashSet::with_capacity(arr.len()); + for &block in arr { + block_states.extend(BlockStates::from(block)); + } + Self { set: block_states } + } +} diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs index 670058c3..2fb9e1cd 100644 --- a/azalea-client/src/plugins/packet/game/mod.rs +++ b/azalea-client/src/plugins/packet/game/mod.rs @@ -1608,7 +1608,16 @@ impl GamePacketHandler<'_> { pub fn store_cookie(&mut self, _p: &ClientboundStoreCookie) {} pub fn transfer(&mut self, _p: &ClientboundTransfer) {} pub fn move_minecart_along_track(&mut self, _p: &ClientboundMoveMinecartAlongTrack) {} - pub fn set_held_slot(&mut self, _p: &ClientboundSetHeldSlot) {} + pub fn set_held_slot(&mut self, p: &ClientboundSetHeldSlot) { + debug!("Got set held slot packet {p:?}"); + + as_system::>(self.ecs, |mut query| { + let mut inventory = query.get_mut(self.player).unwrap(); + if p.slot <= 8 { + inventory.selected_hotbar_slot = p.slot as u8; + } + }); + } pub fn set_player_inventory(&mut self, _p: &ClientboundSetPlayerInventory) {} pub fn projectile_power(&mut self, _p: &ClientboundProjectilePower) {} pub fn custom_report_details(&mut self, _p: &ClientboundCustomReportDetails) {} From be81877137df57de26030bceecbef68b9f05b3e5 Mon Sep 17 00:00:00 2001 From: mat Date: Wed, 4 Jun 2025 21:53:06 -0600 Subject: [PATCH 18/36] fix panic when receiving add_entity and start_configuration in the same update --- ...ve_spawn_entity_and_start_config_packet.rs | 42 +++++++++++++++++++ azalea-entity/src/plugin/mod.rs | 5 +++ 2 files changed, 47 insertions(+) create mode 100644 azalea-client/tests/receive_spawn_entity_and_start_config_packet.rs diff --git a/azalea-client/tests/receive_spawn_entity_and_start_config_packet.rs b/azalea-client/tests/receive_spawn_entity_and_start_config_packet.rs new file mode 100644 index 00000000..d64d2209 --- /dev/null +++ b/azalea-client/tests/receive_spawn_entity_and_start_config_packet.rs @@ -0,0 +1,42 @@ +use azalea_client::{InConfigState, test_simulation::*}; +use azalea_core::{position::Vec3, resource_location::ResourceLocation}; +use azalea_protocol::packets::{ + ConnectionProtocol, + game::{ClientboundAddEntity, ClientboundStartConfiguration}, +}; +use azalea_registry::{DataRegistry, DimensionType, EntityKind}; +use azalea_world::InstanceName; +use bevy_log::tracing_subscriber; +use uuid::Uuid; + +#[test] +fn test_receive_spawn_entity_and_start_config_packet() { + let _ = tracing_subscriber::fmt::try_init(); + + let mut simulation = Simulation::new(ConnectionProtocol::Game); + simulation.receive_packet(make_basic_login_packet( + DimensionType::new_raw(0), + ResourceLocation::new("minecraft:overworld"), + )); + simulation.tick(); + assert!(simulation.has_component::()); + simulation.tick(); + + simulation.receive_packet(ClientboundAddEntity { + id: 123.into(), + uuid: Uuid::new_v4(), + entity_type: EntityKind::ArmorStand, + position: Vec3::ZERO, + x_rot: 0, + y_rot: 0, + y_head_rot: 0, + data: 0, + velocity: Default::default(), + }); + simulation.receive_packet(ClientboundStartConfiguration); + + simulation.tick(); + assert!(simulation.has_component::()); + + // make sure that the entity is despawned +} diff --git a/azalea-entity/src/plugin/mod.rs b/azalea-entity/src/plugin/mod.rs index 03afe7cd..6a6c9615 100644 --- a/azalea-entity/src/plugin/mod.rs +++ b/azalea-entity/src/plugin/mod.rs @@ -46,6 +46,11 @@ impl Plugin for EntityPlugin { Update, ( ( + // remove_despawned_entities_from_indexes is done again here to correctly + // handle the case where an entity is spawned and then the world is removed at + // the same time (like with ClientboundStartConfiguration). + indexing::remove_despawned_entities_from_indexes + .in_set(EntityUpdateSet::Deindex), indexing::update_entity_chunk_positions, indexing::insert_entity_chunk_position, ) From 874f05181028af233b81b1d7d3322a39a0421f33 Mon Sep 17 00:00:00 2001 From: mat Date: Wed, 4 Jun 2025 20:55:57 -0700 Subject: [PATCH 19/36] remove comment about a part of a test that i decided not to implement --- .../tests/receive_spawn_entity_and_start_config_packet.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/azalea-client/tests/receive_spawn_entity_and_start_config_packet.rs b/azalea-client/tests/receive_spawn_entity_and_start_config_packet.rs index d64d2209..0090e37e 100644 --- a/azalea-client/tests/receive_spawn_entity_and_start_config_packet.rs +++ b/azalea-client/tests/receive_spawn_entity_and_start_config_packet.rs @@ -37,6 +37,4 @@ fn test_receive_spawn_entity_and_start_config_packet() { simulation.tick(); assert!(simulation.has_component::()); - - // make sure that the entity is despawned } From 338f931c51ab3665c7c18124ca3e35bfd2ff5ca0 Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 5 Jun 2025 00:59:14 -0400 Subject: [PATCH 20/36] wait for block to exist when calling open_container_at --- azalea/src/container.rs | 19 +++++++++++++++++-- azalea/src/pathfinder/goals.rs | 6 ++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/azalea/src/container.rs b/azalea/src/container.rs index 3521c06d..6c4e86cc 100644 --- a/azalea/src/container.rs +++ b/azalea/src/container.rs @@ -10,6 +10,7 @@ use azalea_inventory::{ ItemStack, Menu, operations::{ClickOperation, PickupClick, QuickMoveClick}, }; +use azalea_physics::collision::BlockWithShape; use azalea_protocol::packets::game::ClientboundGamePacket; use bevy_app::{App, Plugin, Update}; use bevy_ecs::{component::Component, prelude::EventReader, system::Commands}; @@ -54,14 +55,28 @@ impl ContainerClientExt for Client { /// # } /// ``` async fn open_container_at(&self, pos: BlockPos) -> Option { + let mut ticks = self.get_tick_broadcaster(); + // wait until it's not air (up to 10 ticks) + for _ in 0..10 { + if !self + .world() + .read() + .get_block_state(&pos) + .unwrap_or_default() + .is_collision_shape_empty() + { + break; + } + let _ = ticks.recv().await; + } + self.ecs .lock() .entity_mut(self.entity) .insert(WaitingForInventoryOpen); self.block_interact(pos); - let mut receiver = self.get_tick_broadcaster(); - while receiver.recv().await.is_ok() { + while ticks.recv().await.is_ok() { let ecs = self.ecs.lock(); if ecs.get::(self.entity).is_none() { break; diff --git a/azalea/src/pathfinder/goals.rs b/azalea/src/pathfinder/goals.rs index aa6f357a..4c0dbafa 100644 --- a/azalea/src/pathfinder/goals.rs +++ b/azalea/src/pathfinder/goals.rs @@ -220,6 +220,12 @@ impl Goal for ReachBlockPosGoal { BlockPosGoal(self.pos).heuristic(n) } fn success(&self, n: BlockPos) -> bool { + if n.up(1) == self.pos { + // our head is in the block, assume it's always reachable (to reduce the amount + // of impossible goals) + return true; + } + // only do the expensive check if we're close enough let distance = self.pos.distance_squared_to(&n); if distance > self.max_check_distance.pow(2) { From 3087b0c996dbd3fb9a1dbcac4bf5c32f992c2e5e Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 8 Jun 2025 22:46:26 -0330 Subject: [PATCH 21/36] add support for panicking on warn/error in simulation tests --- azalea-client/src/lib.rs | 2 +- azalea-client/src/plugins/join.rs | 3 +- azalea-client/src/test_utils/mod.rs | 6 +++ .../simulation.rs} | 0 azalea-client/src/test_utils/tracing.rs | 38 +++++++++++++++++++ .../change_dimension_to_nether_and_back.rs | 7 +--- azalea-client/tests/client_disconnect.rs | 5 +-- ...espawn_entities_when_changing_dimension.rs | 5 +-- azalea-client/tests/fast_login.rs | 5 +-- .../login_to_dimension_with_same_name.rs | 9 ++--- azalea-client/tests/move_despawned_entity.rs | 8 ++-- ...ve_spawn_entity_and_start_config_packet.rs | 5 +-- .../tests/receive_start_config_packet.rs | 5 +-- .../tests/reply_to_ping_with_pong.rs | 5 +-- .../tests/set_health_before_login.rs | 5 +-- azalea-protocol/src/common/movements.rs | 5 +++ 16 files changed, 76 insertions(+), 37 deletions(-) create mode 100644 azalea-client/src/test_utils/mod.rs rename azalea-client/src/{test_simulation.rs => test_utils/simulation.rs} (100%) create mode 100644 azalea-client/src/test_utils/tracing.rs diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index 6bff353e..df04a606 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -17,7 +17,7 @@ pub mod player; mod plugins; #[doc(hidden)] -pub mod test_simulation; +pub mod test_utils; pub use account::{Account, AccountOpts}; pub use azalea_protocol::common::client_information::ClientInformation; diff --git a/azalea-client/src/plugins/join.rs b/azalea-client/src/plugins/join.rs index 09eeff59..8d094b7d 100644 --- a/azalea-client/src/plugins/join.rs +++ b/azalea-client/src/plugins/join.rs @@ -3,6 +3,7 @@ use std::{net::SocketAddr, sync::Arc}; use azalea_entity::{LocalEntity, indexing::EntityUuidIndex}; use azalea_protocol::{ ServerAddress, + common::client_information::ClientInformation, connect::{Connection, ConnectionError, Proxy}, packets::{ ClientIntention, ConnectionProtocol, PROTOCOL_VERSION, @@ -215,7 +216,7 @@ pub fn poll_create_connection_task( write_conn, ConnectionProtocol::Login, ), - client_information: crate::ClientInformation::default(), + client_information: ClientInformation::default(), instance_holder, metadata: azalea_entity::metadata::PlayerMetadataBundle::default(), }, diff --git a/azalea-client/src/test_utils/mod.rs b/azalea-client/src/test_utils/mod.rs new file mode 100644 index 00000000..9e640908 --- /dev/null +++ b/azalea-client/src/test_utils/mod.rs @@ -0,0 +1,6 @@ +pub mod simulation; +pub mod tracing; + +pub mod prelude { + pub use super::{simulation::*, tracing::*}; +} diff --git a/azalea-client/src/test_simulation.rs b/azalea-client/src/test_utils/simulation.rs similarity index 100% rename from azalea-client/src/test_simulation.rs rename to azalea-client/src/test_utils/simulation.rs diff --git a/azalea-client/src/test_utils/tracing.rs b/azalea-client/src/test_utils/tracing.rs new file mode 100644 index 00000000..85ac4bd6 --- /dev/null +++ b/azalea-client/src/test_utils/tracing.rs @@ -0,0 +1,38 @@ +use bevy_log::tracing_subscriber::{ + self, EnvFilter, Layer, + layer::{Context, SubscriberExt}, + registry::LookupSpan, + util::SubscriberInitExt, +}; +use tracing::{Event, Level, Subscriber, level_filters::LevelFilter}; + +pub fn init_tracing() { + init_tracing_with_level(Level::WARN); +} + +pub fn init_tracing_with_level(max_level: Level) { + tracing_subscriber::registry() + .with( + tracing_subscriber::fmt::layer().with_filter( + EnvFilter::builder() + .with_default_directive(max_level.into()) + .from_env_lossy(), + ), + ) + .with(TestTracingLayer { + panic_on_level: max_level, + }) + .init(); +} + +struct TestTracingLayer { + panic_on_level: Level, +} +impl Layer for TestTracingLayer { + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + let level = *event.metadata().level(); + if level <= self.panic_on_level { + panic!("logged on level {level}"); + } + } +} diff --git a/azalea-client/tests/change_dimension_to_nether_and_back.rs b/azalea-client/tests/change_dimension_to_nether_and_back.rs index fddbccde..e3b5d96e 100644 --- a/azalea-client/tests/change_dimension_to_nether_and_back.rs +++ b/azalea-client/tests/change_dimension_to_nether_and_back.rs @@ -1,4 +1,4 @@ -use azalea_client::{InConfigState, InGameState, test_simulation::*}; +use azalea_client::{InConfigState, InGameState, test_utils::prelude::*}; use azalea_core::{position::ChunkPos, resource_location::ResourceLocation}; use azalea_entity::LocalEntity; use azalea_protocol::packets::{ @@ -7,12 +7,11 @@ use azalea_protocol::packets::{ }; use azalea_registry::{DataRegistry, DimensionType}; use azalea_world::InstanceName; -use bevy_log::tracing_subscriber; use simdnbt::owned::{NbtCompound, NbtTag}; #[test] fn test_change_dimension_to_nether_and_back() { - let _ = tracing_subscriber::fmt().try_init(); + init_tracing(); generic_test_change_dimension_to_nether_and_back(true); generic_test_change_dimension_to_nether_and_back(false); @@ -29,8 +28,6 @@ fn generic_test_change_dimension_to_nether_and_back(using_respawn: bool) { } }; - let _ = tracing_subscriber::fmt::try_init(); - let mut simulation = Simulation::new(ConnectionProtocol::Configuration); assert!(simulation.has_component::()); assert!(!simulation.has_component::()); diff --git a/azalea-client/tests/client_disconnect.rs b/azalea-client/tests/client_disconnect.rs index 354ac788..fc17da0c 100644 --- a/azalea-client/tests/client_disconnect.rs +++ b/azalea-client/tests/client_disconnect.rs @@ -1,11 +1,10 @@ -use azalea_client::test_simulation::*; +use azalea_client::test_utils::prelude::*; use azalea_protocol::packets::ConnectionProtocol; use azalea_world::InstanceName; -use bevy_log::tracing_subscriber; #[test] fn test_client_disconnect() { - let _ = tracing_subscriber::fmt::try_init(); + init_tracing(); let mut simulation = Simulation::new(ConnectionProtocol::Game); diff --git a/azalea-client/tests/despawn_entities_when_changing_dimension.rs b/azalea-client/tests/despawn_entities_when_changing_dimension.rs index 967e0aac..9143b3eb 100644 --- a/azalea-client/tests/despawn_entities_when_changing_dimension.rs +++ b/azalea-client/tests/despawn_entities_when_changing_dimension.rs @@ -1,4 +1,4 @@ -use azalea_client::test_simulation::*; +use azalea_client::test_utils::prelude::*; use azalea_core::{position::ChunkPos, resource_location::ResourceLocation}; use azalea_entity::metadata::Cow; use azalea_protocol::packets::{ @@ -7,12 +7,11 @@ use azalea_protocol::packets::{ }; use azalea_registry::{DataRegistry, DimensionType, EntityKind}; use bevy_ecs::query::With; -use bevy_log::tracing_subscriber; use simdnbt::owned::{NbtCompound, NbtTag}; #[test] fn test_despawn_entities_when_changing_dimension() { - let _ = tracing_subscriber::fmt::try_init(); + init_tracing(); let mut simulation = Simulation::new(ConnectionProtocol::Configuration); simulation.receive_packet(ClientboundRegistryData { diff --git a/azalea-client/tests/fast_login.rs b/azalea-client/tests/fast_login.rs index bc26079a..f11ada9c 100644 --- a/azalea-client/tests/fast_login.rs +++ b/azalea-client/tests/fast_login.rs @@ -1,4 +1,4 @@ -use azalea_client::{InConfigState, test_simulation::*}; +use azalea_client::{InConfigState, test_utils::prelude::*}; use azalea_core::resource_location::ResourceLocation; use azalea_entity::metadata::Health; use azalea_protocol::packets::{ @@ -6,12 +6,11 @@ use azalea_protocol::packets::{ config::{ClientboundFinishConfiguration, ClientboundRegistryData}, game::ClientboundSetHealth, }; -use bevy_log::tracing_subscriber; use simdnbt::owned::{NbtCompound, NbtTag}; #[test] fn test_fast_login() { - let _ = tracing_subscriber::fmt::try_init(); + init_tracing(); let mut simulation = Simulation::new(ConnectionProtocol::Configuration); assert!(simulation.has_component::()); diff --git a/azalea-client/tests/login_to_dimension_with_same_name.rs b/azalea-client/tests/login_to_dimension_with_same_name.rs index 59d3123d..eb1db2a8 100644 --- a/azalea-client/tests/login_to_dimension_with_same_name.rs +++ b/azalea-client/tests/login_to_dimension_with_same_name.rs @@ -1,4 +1,6 @@ -use azalea_client::{InConfigState, InGameState, local_player::InstanceHolder, test_simulation::*}; +use azalea_client::{ + InConfigState, InGameState, local_player::InstanceHolder, test_utils::prelude::*, +}; use azalea_core::{position::ChunkPos, resource_location::ResourceLocation}; use azalea_entity::LocalEntity; use azalea_protocol::packets::{ @@ -8,12 +10,11 @@ use azalea_protocol::packets::{ }; use azalea_registry::{DataRegistry, DimensionType}; use azalea_world::InstanceName; -use bevy_log::tracing_subscriber; use simdnbt::owned::{NbtCompound, NbtTag}; #[test] fn test_login_to_dimension_with_same_name() { - let _ = tracing_subscriber::fmt().try_init(); + init_tracing(); generic_test_login_to_dimension_with_same_name(true); generic_test_login_to_dimension_with_same_name(false); @@ -30,8 +31,6 @@ fn generic_test_login_to_dimension_with_same_name(using_respawn: bool) { } }; - let _ = tracing_subscriber::fmt::try_init(); - let mut simulation = Simulation::new(ConnectionProtocol::Configuration); assert!(simulation.has_component::()); assert!(!simulation.has_component::()); diff --git a/azalea-client/tests/move_despawned_entity.rs b/azalea-client/tests/move_despawned_entity.rs index d590cb39..8c33566b 100644 --- a/azalea-client/tests/move_despawned_entity.rs +++ b/azalea-client/tests/move_despawned_entity.rs @@ -1,15 +1,15 @@ -use azalea_client::test_simulation::*; +use azalea_client::test_utils::prelude::*; use azalea_core::{position::ChunkPos, resource_location::ResourceLocation}; use azalea_entity::metadata::Cow; use azalea_protocol::packets::{ConnectionProtocol, game::ClientboundMoveEntityRot}; use azalea_registry::{DataRegistry, DimensionType, EntityKind}; use azalea_world::MinecraftEntityId; use bevy_ecs::query::With; -use bevy_log::tracing_subscriber; +use tracing::Level; #[test] fn test_move_despawned_entity() { - let _ = tracing_subscriber::fmt::try_init(); + init_tracing_with_level(Level::ERROR); // a warning is expected here let mut simulation = Simulation::new(ConnectionProtocol::Game); simulation.receive_packet(make_basic_login_packet( @@ -26,7 +26,7 @@ fn test_move_despawned_entity() { // make sure it's spawned let mut cow_query = simulation.app.world_mut().query_filtered::<(), With>(); let cow_iter = cow_query.iter(simulation.app.world()); - assert_eq!(cow_iter.count(), 1, "cow should be despawned"); + assert_eq!(cow_iter.count(), 1, "cow should be spawned"); // despawn the cow by receiving a login packet simulation.receive_packet(make_basic_login_packet( diff --git a/azalea-client/tests/receive_spawn_entity_and_start_config_packet.rs b/azalea-client/tests/receive_spawn_entity_and_start_config_packet.rs index 0090e37e..8e14f731 100644 --- a/azalea-client/tests/receive_spawn_entity_and_start_config_packet.rs +++ b/azalea-client/tests/receive_spawn_entity_and_start_config_packet.rs @@ -1,4 +1,4 @@ -use azalea_client::{InConfigState, test_simulation::*}; +use azalea_client::{InConfigState, test_utils::prelude::*}; use azalea_core::{position::Vec3, resource_location::ResourceLocation}; use azalea_protocol::packets::{ ConnectionProtocol, @@ -6,12 +6,11 @@ use azalea_protocol::packets::{ }; use azalea_registry::{DataRegistry, DimensionType, EntityKind}; use azalea_world::InstanceName; -use bevy_log::tracing_subscriber; use uuid::Uuid; #[test] fn test_receive_spawn_entity_and_start_config_packet() { - let _ = tracing_subscriber::fmt::try_init(); + init_tracing(); let mut simulation = Simulation::new(ConnectionProtocol::Game); simulation.receive_packet(make_basic_login_packet( diff --git a/azalea-client/tests/receive_start_config_packet.rs b/azalea-client/tests/receive_start_config_packet.rs index 9b0c734d..485204e4 100644 --- a/azalea-client/tests/receive_start_config_packet.rs +++ b/azalea-client/tests/receive_start_config_packet.rs @@ -1,14 +1,13 @@ -use azalea_client::{InConfigState, packet::game::SendPacketEvent, test_simulation::*}; +use azalea_client::{InConfigState, packet::game::SendPacketEvent, test_utils::prelude::*}; use azalea_core::resource_location::ResourceLocation; use azalea_protocol::packets::{ConnectionProtocol, game::ClientboundStartConfiguration}; use azalea_registry::{DataRegistry, DimensionType}; use azalea_world::InstanceName; use bevy_ecs::event::Events; -use bevy_log::tracing_subscriber; #[test] fn test_receive_start_config_packet() { - let _ = tracing_subscriber::fmt::try_init(); + init_tracing(); let mut simulation = Simulation::new(ConnectionProtocol::Game); simulation.receive_packet(make_basic_login_packet( diff --git a/azalea-client/tests/reply_to_ping_with_pong.rs b/azalea-client/tests/reply_to_ping_with_pong.rs index bc8bccd4..ac09336c 100644 --- a/azalea-client/tests/reply_to_ping_with_pong.rs +++ b/azalea-client/tests/reply_to_ping_with_pong.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use azalea_client::{ packet::{config::SendConfigPacketEvent, game::SendPacketEvent}, - test_simulation::*, + test_utils::prelude::*, }; use azalea_core::resource_location::ResourceLocation; use azalea_protocol::packets::{ @@ -13,13 +13,12 @@ use azalea_protocol::packets::{ game::{self, ServerboundGamePacket}, }; use bevy_ecs::observer::Trigger; -use bevy_log::tracing_subscriber; use parking_lot::Mutex; use simdnbt::owned::{NbtCompound, NbtTag}; #[test] fn reply_to_ping_with_pong() { - let _ = tracing_subscriber::fmt::try_init(); + init_tracing(); let mut simulation = Simulation::new(ConnectionProtocol::Configuration); diff --git a/azalea-client/tests/set_health_before_login.rs b/azalea-client/tests/set_health_before_login.rs index d81dd48f..60735781 100644 --- a/azalea-client/tests/set_health_before_login.rs +++ b/azalea-client/tests/set_health_before_login.rs @@ -1,4 +1,4 @@ -use azalea_client::{InConfigState, test_simulation::*}; +use azalea_client::{InConfigState, test_utils::prelude::*}; use azalea_core::resource_location::ResourceLocation; use azalea_entity::{LocalEntity, metadata::Health}; use azalea_protocol::packets::{ @@ -7,12 +7,11 @@ use azalea_protocol::packets::{ game::ClientboundSetHealth, }; use azalea_registry::{DataRegistry, DimensionType}; -use bevy_log::tracing_subscriber; use simdnbt::owned::{NbtCompound, NbtTag}; #[test] fn test_set_health_before_login() { - let _ = tracing_subscriber::fmt::try_init(); + init_tracing(); let mut simulation = Simulation::new(ConnectionProtocol::Configuration); assert!(simulation.has_component::()); diff --git a/azalea-protocol/src/common/movements.rs b/azalea-protocol/src/common/movements.rs index 7ab7fc2f..ffc3452f 100644 --- a/azalea-protocol/src/common/movements.rs +++ b/azalea-protocol/src/common/movements.rs @@ -28,6 +28,11 @@ pub struct RelativeMovements { pub delta_z: bool, pub rotate_delta: bool, } +impl RelativeMovements { + pub fn all_absolute() -> Self { + RelativeMovements::default() + } +} impl AzaleaRead for RelativeMovements { fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result { From 45d73712746fbfd365e8a68a75dfad6ae2e0d174 Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 8 Jun 2025 13:37:23 -1300 Subject: [PATCH 22/36] insert ClientInformation earlier --- azalea-brigadier/src/command_dispatcher.rs | 2 +- azalea-client/src/client.rs | 1 - azalea-client/src/plugins/join.rs | 3 ++- azalea-client/src/test_utils/simulation.rs | 26 ++++++++++++---------- azalea-client/src/test_utils/tracing.rs | 3 +-- azalea-world/src/bit_storage.rs | 2 +- 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/azalea-brigadier/src/command_dispatcher.rs b/azalea-brigadier/src/command_dispatcher.rs index b27df6d1..d9a45132 100644 --- a/azalea-brigadier/src/command_dispatcher.rs +++ b/azalea-brigadier/src/command_dispatcher.rs @@ -55,7 +55,7 @@ impl CommandDispatcher { build } - pub fn parse(&self, command: StringReader, source: S) -> ParseResults { + pub fn parse(&self, command: StringReader, source: S) -> ParseResults<'_, S> { let source = Arc::new(source); let context = CommandContextBuilder::new(self, source, self.root.clone(), command.cursor()); diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index f73e939b..0304e7b0 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -572,7 +572,6 @@ impl Client { #[derive(Bundle)] pub struct LocalPlayerBundle { pub raw_connection: RawConnection, - pub client_information: ClientInformation, pub instance_holder: InstanceHolder, pub metadata: azalea_entity::metadata::PlayerMetadataBundle, diff --git a/azalea-client/src/plugins/join.rs b/azalea-client/src/plugins/join.rs index 8d094b7d..ed2d6ed3 100644 --- a/azalea-client/src/plugins/join.rs +++ b/azalea-client/src/plugins/join.rs @@ -129,6 +129,8 @@ pub fn handle_start_join_server_event( // localentity is always present for our clients, even if we're not actually logged // in LocalEntity, + // this is inserted early so the user can always access and modify it + ClientInformation::default(), // ConnectOpts is inserted as a component here event.connect_opts.clone(), // we don't insert InLoginState until we actually create the connection. note that @@ -216,7 +218,6 @@ pub fn poll_create_connection_task( write_conn, ConnectionProtocol::Login, ), - client_information: ClientInformation::default(), instance_holder, metadata: azalea_entity::metadata::PlayerMetadataBundle::default(), }, diff --git a/azalea-client/src/test_utils/simulation.rs b/azalea-client/src/test_utils/simulation.rs index c53a624a..00b35dee 100644 --- a/azalea-client/src/test_utils/simulation.rs +++ b/azalea-client/src/test_utils/simulation.rs @@ -11,14 +11,17 @@ use azalea_core::{ tick::GameTick, }; use azalea_entity::metadata::PlayerMetadataBundle; -use azalea_protocol::packets::{ - ConnectionProtocol, Packet, ProtocolPacket, - common::CommonPlayerSpawnInfo, - config::{ClientboundFinishConfiguration, ClientboundRegistryData}, - game::{ - ClientboundAddEntity, ClientboundLevelChunkWithLight, ClientboundLogin, ClientboundRespawn, - c_level_chunk_with_light::ClientboundLevelChunkPacketData, - c_light_update::ClientboundLightUpdatePacketData, +use azalea_protocol::{ + common::client_information::ClientInformation, + packets::{ + ConnectionProtocol, Packet, ProtocolPacket, + common::CommonPlayerSpawnInfo, + config::{ClientboundFinishConfiguration, ClientboundRegistryData}, + game::{ + ClientboundAddEntity, ClientboundLevelChunkWithLight, ClientboundLogin, + ClientboundRespawn, c_level_chunk_with_light::ClientboundLevelChunkPacketData, + c_light_update::ClientboundLightUpdatePacketData, + }, }, }; use azalea_registry::{Biome, DimensionType, EntityKind}; @@ -30,8 +33,8 @@ use simdnbt::owned::{NbtCompound, NbtTag}; use uuid::Uuid; use crate::{ - ClientInformation, InConfigState, LocalPlayerBundle, connection::RawConnection, - disconnect::DisconnectEvent, local_player::InstanceHolder, player::GameProfileComponent, + InConfigState, LocalPlayerBundle, connection::RawConnection, disconnect::DisconnectEvent, + local_player::InstanceHolder, player::GameProfileComponent, }; /// A way to simulate a client in a server, used for some internal tests. @@ -49,7 +52,7 @@ impl Simulation { let mut entity = app.world_mut().spawn_empty(); let (player, rt) = create_local_player_bundle(entity.id(), ConnectionProtocol::Configuration); - entity.insert(player); + entity.insert((player, ClientInformation::default())); let entity = entity.id(); @@ -171,7 +174,6 @@ fn create_local_player_bundle( let local_player_bundle = LocalPlayerBundle { raw_connection, - client_information: ClientInformation::default(), instance_holder, metadata: PlayerMetadataBundle::default(), }; diff --git a/azalea-client/src/test_utils/tracing.rs b/azalea-client/src/test_utils/tracing.rs index 85ac4bd6..207a4625 100644 --- a/azalea-client/src/test_utils/tracing.rs +++ b/azalea-client/src/test_utils/tracing.rs @@ -1,10 +1,9 @@ use bevy_log::tracing_subscriber::{ self, EnvFilter, Layer, layer::{Context, SubscriberExt}, - registry::LookupSpan, util::SubscriberInitExt, }; -use tracing::{Event, Level, Subscriber, level_filters::LevelFilter}; +use tracing::{Event, Level, Subscriber}; pub fn init_tracing() { init_tracing_with_level(Level::WARN); diff --git a/azalea-world/src/bit_storage.rs b/azalea-world/src/bit_storage.rs index 7d1b72e1..01e9f349 100644 --- a/azalea-world/src/bit_storage.rs +++ b/azalea-world/src/bit_storage.rs @@ -219,7 +219,7 @@ impl BitStorage { self.size } - pub fn iter(&self) -> BitStorageIter { + pub fn iter(&self) -> BitStorageIter<'_> { BitStorageIter { storage: self, index: 0, From 4a4de819616d620d15680e71fb32390e28ab07cd Mon Sep 17 00:00:00 2001 From: mat Date: Mon, 9 Jun 2025 17:15:07 +0900 Subject: [PATCH 23/36] handle relative teleports correctly and fix entity chunk indexing warnings --- azalea-client/src/plugins/packet/game/mod.rs | 123 +++++------------- azalea-client/src/test_utils/simulation.rs | 3 + .../tests/move_and_despawn_entity.rs | 57 ++++++++ azalea-entity/src/plugin/indexing.rs | 36 +++-- azalea-entity/src/plugin/mod.rs | 5 - azalea-entity/src/vec_delta_codec.rs | 8 +- azalea-protocol/src/common/movements.rs | 63 ++++++++- .../src/packets/game/c_teleport_entity.rs | 2 +- 8 files changed, 184 insertions(+), 113 deletions(-) create mode 100644 azalea-client/tests/move_and_despawn_entity.rs diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs index 2fb9e1cd..1307473d 100644 --- a/azalea-client/src/plugins/packet/game/mod.rs +++ b/azalea-client/src/plugins/packet/game/mod.rs @@ -1,10 +1,9 @@ mod events; -use std::{collections::HashSet, ops::Add, sync::Arc}; +use std::{collections::HashSet, sync::Arc}; use azalea_core::{ game_type::GameMode, - math, position::{ChunkPos, Vec3}, }; use azalea_entity::{ @@ -425,68 +424,12 @@ impl GamePacketHandler<'_> { **last_sent_position = **position; - fn apply_change>(base: T, condition: bool, change: T) -> T { - if condition { base + change } else { change } - } - - let new_x = apply_change(position.x, p.relative.x, p.change.pos.x); - let new_y = apply_change(position.y, p.relative.y, p.change.pos.y); - let new_z = apply_change(position.z, p.relative.z, p.change.pos.z); - - let new_y_rot = apply_change( - direction.y_rot, - p.relative.y_rot, - p.change.look_direction.y_rot, - ); - let new_x_rot = apply_change( - direction.x_rot, - p.relative.x_rot, - p.change.look_direction.x_rot, - ); - - let mut new_delta_from_rotations = physics.velocity; - if p.relative.rotate_delta { - let y_rot_delta = direction.y_rot - new_y_rot; - let x_rot_delta = direction.x_rot - new_x_rot; - new_delta_from_rotations = new_delta_from_rotations - .x_rot(math::to_radians(x_rot_delta as f64) as f32) - .y_rot(math::to_radians(y_rot_delta as f64) as f32); - } - - let new_delta = Vec3::new( - apply_change( - new_delta_from_rotations.x, - p.relative.delta_x, - p.change.delta.x, - ), - apply_change( - new_delta_from_rotations.y, - p.relative.delta_y, - p.change.delta.y, - ), - apply_change( - new_delta_from_rotations.z, - p.relative.delta_z, - p.change.delta.z, - ), - ); - - // apply the updates - - physics.velocity = new_delta; - - (direction.y_rot, direction.x_rot) = (new_y_rot, new_x_rot); - - let new_pos = Vec3::new(new_x, new_y, new_z); - if new_pos != **position { - **position = new_pos; - } - + p.relative + .apply(&p.change, &mut position, &mut direction, &mut physics); // old_pos is set to the current position when we're teleported physics.set_old_pos(&position); // send the relevant packets - commands.trigger(SendPacketEvent::new( self.player, ServerboundAcceptTeleportation { id: p.id }, @@ -494,8 +437,8 @@ impl GamePacketHandler<'_> { commands.trigger(SendPacketEvent::new( self.player, ServerboundMovePlayerPosRot { - pos: new_pos, - look_direction: LookDirection::new(new_y_rot, new_x_rot), + pos: **position, + look_direction: *direction, // this is always false on_ground: false, }, @@ -852,6 +795,8 @@ impl GamePacketHandler<'_> { } pub fn teleport_entity(&mut self, p: &ClientboundTeleportEntity) { + debug!("Got teleport entity packet {p:?}"); + as_system::<(Commands, Query<(&EntityIdIndex, &InstanceHolder)>)>( self.ecs, |(mut commands, mut query)| { @@ -862,26 +807,28 @@ impl GamePacketHandler<'_> { return; }; - let new_pos = p.change.pos; - let new_look_direction = LookDirection { - x_rot: (p.change.look_direction.x_rot as i32 * 360) as f32 / 256., - y_rot: (p.change.look_direction.y_rot as i32 * 360) as f32 / 256., - }; + let relative = p.relative.clone(); + let change = p.change.clone(); + commands.entity(entity).queue(RelativeEntityUpdate::new( instance_holder.partial_instance.clone(), move |entity| { - let mut position = entity.get_mut::().unwrap(); - if new_pos != **position { - **position = new_pos; - } - let position = *position; - let mut look_direction = entity.get_mut::().unwrap(); - if new_look_direction != *look_direction { - *look_direction = new_look_direction; - } - // old_pos is set to the current position when we're teleported - let mut physics = entity.get_mut::().unwrap(); - physics.set_old_pos(&position); + let entity_id = entity.id(); + entity.world_scope(move |world| { + let mut query = + world.query::<(&mut Physics, &mut LookDirection, &mut Position)>(); + let (mut physics, mut look_direction, mut position) = + query.get_mut(world, entity_id).unwrap(); + let old_position = *position; + relative.apply( + &change, + &mut position, + &mut look_direction, + &mut physics, + ); + // old_pos is set to the current position when we're teleported + physics.set_old_pos(&old_position); + }); }, )); }, @@ -914,11 +861,7 @@ impl GamePacketHandler<'_> { instance_holder.partial_instance.clone(), move |entity_mut| { let mut physics = entity_mut.get_mut::().unwrap(); - let new_pos = physics.vec_delta_codec.decode( - new_delta.xa as i64, - new_delta.ya as i64, - new_delta.za as i64, - ); + let new_pos = physics.vec_delta_codec.decode(&new_delta); physics.vec_delta_codec.set_base(new_pos); physics.set_on_ground(new_on_ground); @@ -968,17 +911,13 @@ impl GamePacketHandler<'_> { instance_holder.partial_instance.clone(), move |entity_mut| { let mut physics = entity_mut.get_mut::().unwrap(); - let new_pos = physics.vec_delta_codec.decode( - new_delta.xa as i64, - new_delta.ya as i64, - new_delta.za as i64, - ); - physics.vec_delta_codec.set_base(new_pos); + let new_position = physics.vec_delta_codec.decode(&new_delta); + physics.vec_delta_codec.set_base(new_position); physics.set_on_ground(new_on_ground); let mut position = entity_mut.get_mut::().unwrap(); - if new_pos != **position { - **position = new_pos; + if new_position != **position { + **position = new_position; } let mut look_direction = entity_mut.get_mut::().unwrap(); diff --git a/azalea-client/src/test_utils/simulation.rs b/azalea-client/src/test_utils/simulation.rs index 00b35dee..caf63113 100644 --- a/azalea-client/src/test_utils/simulation.rs +++ b/azalea-client/src/test_utils/simulation.rs @@ -106,6 +106,9 @@ impl Simulation { pub fn tick(&mut self) { tick_app(&mut self.app); } + pub fn update(&mut self) { + self.app.update(); + } pub fn minecraft_entity_id(&self) -> MinecraftEntityId { self.component::() diff --git a/azalea-client/tests/move_and_despawn_entity.rs b/azalea-client/tests/move_and_despawn_entity.rs new file mode 100644 index 00000000..080ca903 --- /dev/null +++ b/azalea-client/tests/move_and_despawn_entity.rs @@ -0,0 +1,57 @@ +use azalea_client::test_utils::prelude::*; +use azalea_core::{ + position::{ChunkPos, Vec3}, + resource_location::ResourceLocation, +}; +use azalea_entity::metadata::Cow; +use azalea_protocol::{ + common::movements::{PositionMoveRotation, RelativeMovements}, + packets::{ + ConnectionProtocol, + game::{ClientboundRemoveEntities, ClientboundTeleportEntity}, + }, +}; +use azalea_registry::{DataRegistry, DimensionType, EntityKind}; +use azalea_world::MinecraftEntityId; +use bevy_ecs::query::With; + +#[test] +fn test_move_and_despawn_entity() { + init_tracing(); + + let mut simulation = Simulation::new(ConnectionProtocol::Game); + simulation.receive_packet(make_basic_login_packet( + DimensionType::new_raw(0), + ResourceLocation::new("azalea:overworld"), + )); + + for x in 0..=10 { + simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(x, 0), (384 + 64) / 16)); + } + simulation.tick(); + + simulation.receive_packet(make_basic_add_entity(EntityKind::Cow, 123, (0.5, 64., 0.5))); + simulation.tick(); + + simulation.receive_packet(ClientboundTeleportEntity { + id: MinecraftEntityId(123), + change: PositionMoveRotation { + pos: Vec3::new(16., 0., 0.), + delta: Vec3::ZERO, + look_direction: Default::default(), + }, + relative: RelativeMovements::all_relative(), + on_ground: true, + }); + simulation.receive_packet(ClientboundRemoveEntities { + entity_ids: vec![MinecraftEntityId(123)], + }); + simulation.tick(); + + // make sure it's despawned + let mut cow_query = simulation.app.world_mut().query_filtered::<(), With>(); + let cow_iter = cow_query.iter(simulation.app.world()); + assert_eq!(cow_iter.count(), 0, "cow should be despawned"); + + simulation.tick(); +} diff --git a/azalea-entity/src/plugin/indexing.rs b/azalea-entity/src/plugin/indexing.rs index 2fc89e84..f1926286 100644 --- a/azalea-entity/src/plugin/indexing.rs +++ b/azalea-entity/src/plugin/indexing.rs @@ -132,16 +132,17 @@ pub fn update_entity_chunk_positions( instance_container: Res, ) { for (entity, pos, instance_name, mut entity_chunk_pos) in query.iter_mut() { - // TODO: move this inside of the if statement so it's not called as often - let instance_lock = instance_container.get(instance_name).unwrap(); - let mut instance = instance_lock.write(); - let old_chunk = **entity_chunk_pos; let new_chunk = ChunkPos::from(*pos); if old_chunk != new_chunk { **entity_chunk_pos = new_chunk; if old_chunk != new_chunk { + let Some(instance_lock) = instance_container.get(instance_name) else { + continue; + }; + let mut instance = instance_lock.write(); + // move the entity from the old chunk to the new one if let Some(entities) = instance.entities_by_chunk.get_mut(&old_chunk) { entities.remove(&entity); @@ -163,7 +164,10 @@ pub fn insert_entity_chunk_position( instance_container: Res, ) { for (entity, pos, world_name) in query.iter() { - let instance_lock = instance_container.get(world_name).unwrap(); + let Some(instance_lock) = instance_container.get(world_name) else { + // entity must've been despawned already + continue; + }; let mut instance = instance_lock.write(); let chunk = ChunkPos::from(*pos); @@ -213,13 +217,13 @@ pub fn remove_despawned_entities_from_indexes( let mut instance = instance_lock.write(); - // if the entity has no references left, despawn it + // if the entity is being loaded by any of our clients, don't despawn it if !loaded_by.is_empty() { continue; } // remove the entity from the chunk index - let chunk = ChunkPos::from(*position); + let chunk = ChunkPos::from(position); match instance.entities_by_chunk.get_mut(&chunk) { Some(entities_in_chunk) => { if entities_in_chunk.remove(&entity) { @@ -247,9 +251,21 @@ pub fn remove_despawned_entities_from_indexes( } } _ => { - debug!( - "Tried to remove entity {entity:?} from chunk {chunk:?} but the chunk was not found." - ); + let mut found_in_other_chunks = HashSet::new(); + for (other_chunk, entities_in_other_chunk) in &mut instance.entities_by_chunk { + if entities_in_other_chunk.remove(&entity) { + found_in_other_chunks.insert(other_chunk); + } + } + if found_in_other_chunks.is_empty() { + warn!( + "Tried to remove entity {entity:?} from chunk {chunk:?} but the chunk was not found and the entity wasn't in any other chunks." + ); + } else { + warn!( + "Tried to remove entity {entity:?} from chunk {chunk:?} but the chunk was not found. Entity found in and removed from other chunk(s): {found_in_other_chunks:?}" + ); + } } } // remove it from the uuid index diff --git a/azalea-entity/src/plugin/mod.rs b/azalea-entity/src/plugin/mod.rs index 6a6c9615..03afe7cd 100644 --- a/azalea-entity/src/plugin/mod.rs +++ b/azalea-entity/src/plugin/mod.rs @@ -46,11 +46,6 @@ impl Plugin for EntityPlugin { Update, ( ( - // remove_despawned_entities_from_indexes is done again here to correctly - // handle the case where an entity is spawned and then the world is removed at - // the same time (like with ClientboundStartConfiguration). - indexing::remove_despawned_entities_from_indexes - .in_set(EntityUpdateSet::Deindex), indexing::update_entity_chunk_positions, indexing::insert_entity_chunk_position, ) diff --git a/azalea-entity/src/vec_delta_codec.rs b/azalea-entity/src/vec_delta_codec.rs index 51aa7cea..270daff2 100644 --- a/azalea-entity/src/vec_delta_codec.rs +++ b/azalea-entity/src/vec_delta_codec.rs @@ -1,4 +1,4 @@ -use azalea_core::position::Vec3; +use azalea_core::{delta::PositionDelta8, position::Vec3}; #[derive(Debug, Clone, Default)] pub struct VecDeltaCodec { @@ -10,7 +10,11 @@ impl VecDeltaCodec { Self { base } } - pub fn decode(&self, x: i64, y: i64, z: i64) -> Vec3 { + pub fn decode(&self, delta: &PositionDelta8) -> Vec3 { + let x = delta.xa as i64; + let y = delta.ya as i64; + let z = delta.za as i64; + if x == 0 && y == 0 && z == 0 { return self.base; } diff --git a/azalea-protocol/src/common/movements.rs b/azalea-protocol/src/common/movements.rs index ffc3452f..a70342b3 100644 --- a/azalea-protocol/src/common/movements.rs +++ b/azalea-protocol/src/common/movements.rs @@ -1,8 +1,11 @@ -use std::io::{self, Cursor, Write}; +use std::{ + io::{self, Cursor, Write}, + ops::Add, +}; use azalea_buf::{AzBuf, AzaleaRead, AzaleaWrite, BufReadError}; -use azalea_core::{bitset::FixedBitSet, position::Vec3}; -use azalea_entity::LookDirection; +use azalea_core::{bitset::FixedBitSet, math, position::Vec3}; +use azalea_entity::{LookDirection, Physics, Position}; /// The updated position, velocity, and rotations for an entity. /// @@ -32,6 +35,60 @@ impl RelativeMovements { pub fn all_absolute() -> Self { RelativeMovements::default() } + pub fn all_relative() -> Self { + RelativeMovements { + x: true, + y: true, + z: true, + y_rot: true, + x_rot: true, + delta_x: true, + delta_y: true, + delta_z: true, + rotate_delta: true, + } + } + + pub fn apply( + &self, + change: &PositionMoveRotation, + position: &mut Position, + direction: &mut LookDirection, + physics: &mut Physics, + ) { + let new_position = Vec3::new( + apply_change(position.x, self.x, change.pos.x), + apply_change(position.y, self.y, change.pos.y), + apply_change(position.z, self.z, change.pos.z), + ); + + let new_look_direction = LookDirection::new( + apply_change(direction.y_rot, self.y_rot, change.look_direction.y_rot), + apply_change(direction.x_rot, self.x_rot, change.look_direction.x_rot), + ); + + let mut new_delta = physics.velocity; + if self.rotate_delta { + let y_rot_delta = direction.y_rot - new_look_direction.y_rot; + let x_rot_delta = direction.x_rot - new_look_direction.x_rot; + new_delta = new_delta + .x_rot(math::to_radians(x_rot_delta as f64) as f32) + .y_rot(math::to_radians(y_rot_delta as f64) as f32); + } + let new_delta = Vec3::new( + apply_change(new_delta.x, self.delta_x, change.delta.x), + apply_change(new_delta.y, self.delta_y, change.delta.y), + apply_change(new_delta.z, self.delta_z, change.delta.z), + ); + + **position = new_position; + *direction = new_look_direction; + physics.velocity = new_delta; + } +} + +fn apply_change>(base: T, condition: bool, change: T) -> T { + if condition { base + change } else { change } } impl AzaleaRead for RelativeMovements { diff --git a/azalea-protocol/src/packets/game/c_teleport_entity.rs b/azalea-protocol/src/packets/game/c_teleport_entity.rs index 92b8f1eb..e19ded58 100644 --- a/azalea-protocol/src/packets/game/c_teleport_entity.rs +++ b/azalea-protocol/src/packets/game/c_teleport_entity.rs @@ -9,6 +9,6 @@ pub struct ClientboundTeleportEntity { #[var] pub id: MinecraftEntityId, pub change: PositionMoveRotation, - pub relatives: RelativeMovements, + pub relative: RelativeMovements, pub on_ground: bool, } From 40bcb62a777fca73a5b887af4d9a3fce01ee9053 Mon Sep 17 00:00:00 2001 From: mat Date: Mon, 9 Jun 2025 14:29:44 +0600 Subject: [PATCH 24/36] update config_fast_builds --- ...ig_fast_builds => config_fast_builds.toml} | 45 ++++++++++++------- .../tests/move_and_despawn_entity.rs | 11 +---- azalea/README.md | 2 +- 3 files changed, 31 insertions(+), 27 deletions(-) rename .cargo/{config_fast_builds => config_fast_builds.toml} (52%) diff --git a/.cargo/config_fast_builds b/.cargo/config_fast_builds.toml similarity index 52% rename from .cargo/config_fast_builds rename to .cargo/config_fast_builds.toml index 9f370141..06620164 100644 --- a/.cargo/config_fast_builds +++ b/.cargo/config_fast_builds.toml @@ -1,33 +1,46 @@ -# This file was borrowed from Bevy: https://github.com/bevyengine/bevy/blob/main/.cargo/config_fast_builds +# This file is based on Bevy's fast builds config: https://github.com/bevyengine/bevy/blob/main/.cargo/config_fast_builds.toml # Add the contents of this file to `config.toml` to enable "fast build" configuration. Please read the notes below. -# NOTE: For maximum performance, build using a nightly compiler -# If you are using rust stable, remove the "-Zshare-generics=y" below. - [target.x86_64-unknown-linux-gnu] linker = "clang" -rustflags = ["-Clink-arg=-fuse-ld=lld", "-Zshare-generics=y"] +rustflags = [ + # LLD linker + # + # You may need to install it: + # + # - Ubuntu: `sudo apt-get install lld clang` + # - Fedora: `sudo dnf install lld clang` + # - Arch: `sudo pacman -S lld clang` + "-Clink-arg=-fuse-ld=lld", + + # Mold linker + # + # You may need to install it: + # + # - Ubuntu: `sudo apt-get install mold clang` + # - Fedora: `sudo dnf install mold clang` + # - Arch: `sudo pacman -S mold clang` + # "-Clink-arg=-fuse-ld=mold", + + # Nightly + "-Zshare-generics=y", +] # NOTE: you must install [Mach-O LLD Port](https://lld.llvm.org/MachO/index.html) on mac. you can easily do this by installing llvm which includes lld with the "brew" package manager: # `brew install llvm` [target.x86_64-apple-darwin] -rustflags = [ - "-C", - "link-arg=-fuse-ld=/usr/local/opt/llvm/bin/ld64.lld", - "-Zshare-generics=y", -] +rustflags = ["-Zshare-generics=y"] [target.aarch64-apple-darwin] -rustflags = [ - "-C", - "link-arg=-fuse-ld=/opt/homebrew/opt/llvm/bin/ld64.lld", - "-Zshare-generics=y", -] +rustflags = ["-Zshare-generics=y"] [target.x86_64-pc-windows-msvc] linker = "rust-lld.exe" -rustflags = ["-Zshare-generics=n"] +rustflags = [ + # This needs to be off if you use dynamic linking on Windows. + "-Zshare-generics=n", +] # Optional: Uncommenting the following improves compile times, but reduces the amount of debug info to 'line number tables only' # In most cases the gains are negligible, but if you are on macos and have slow compile times you should see significant gains. diff --git a/azalea-client/tests/move_and_despawn_entity.rs b/azalea-client/tests/move_and_despawn_entity.rs index 080ca903..09d901bc 100644 --- a/azalea-client/tests/move_and_despawn_entity.rs +++ b/azalea-client/tests/move_and_despawn_entity.rs @@ -25,9 +25,7 @@ fn test_move_and_despawn_entity() { ResourceLocation::new("azalea:overworld"), )); - for x in 0..=10 { - simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(x, 0), (384 + 64) / 16)); - } + simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16)); simulation.tick(); simulation.receive_packet(make_basic_add_entity(EntityKind::Cow, 123, (0.5, 64., 0.5))); @@ -47,11 +45,4 @@ fn test_move_and_despawn_entity() { entity_ids: vec![MinecraftEntityId(123)], }); simulation.tick(); - - // make sure it's despawned - let mut cow_query = simulation.app.world_mut().query_filtered::<(), With>(); - let cow_iter = cow_query.iter(simulation.app.world()); - assert_eq!(cow_iter.count(), 0, "cow should be despawned"); - - simulation.tick(); } diff --git a/azalea/README.md b/azalea/README.md index 83ff0d49..26a06dde 100644 --- a/azalea/README.md +++ b/azalea/README.md @@ -15,7 +15,7 @@ Then, use one of the following commands to add Azalea to your project: ## Optimization For faster compile times, create a `.cargo/config.toml` file in your project and copy -[this file](https://github.com/azalea-rs/azalea/blob/main/.cargo/config_fast_builds) +[this file](https://github.com/azalea-rs/azalea/blob/main/.cargo/config_fast_builds.toml) into it. You may have to install the LLD linker. For faster performance in debug mode, add the following code to your From 086f979a2895e7f15ec61e5704869a0cfc94d16f Mon Sep 17 00:00:00 2001 From: mat Date: Mon, 9 Jun 2025 13:14:20 -1100 Subject: [PATCH 25/36] replace Client::get_open_container and view_container_or_inventory with get_inventory --- .../tests/move_and_despawn_entity.rs | 2 - azalea-inventory/src/lib.rs | 3 +- azalea/src/container.rs | 174 +++++++----------- 3 files changed, 66 insertions(+), 113 deletions(-) diff --git a/azalea-client/tests/move_and_despawn_entity.rs b/azalea-client/tests/move_and_despawn_entity.rs index 09d901bc..d8298ff2 100644 --- a/azalea-client/tests/move_and_despawn_entity.rs +++ b/azalea-client/tests/move_and_despawn_entity.rs @@ -3,7 +3,6 @@ use azalea_core::{ position::{ChunkPos, Vec3}, resource_location::ResourceLocation, }; -use azalea_entity::metadata::Cow; use azalea_protocol::{ common::movements::{PositionMoveRotation, RelativeMovements}, packets::{ @@ -13,7 +12,6 @@ use azalea_protocol::{ }; use azalea_registry::{DataRegistry, DimensionType, EntityKind}; use azalea_world::MinecraftEntityId; -use bevy_ecs::query::With; #[test] fn test_move_and_despawn_entity() { diff --git a/azalea-inventory/src/lib.rs b/azalea-inventory/src/lib.rs index 0f74ba3a..3bcb0019 100644 --- a/azalea-inventory/src/lib.rs +++ b/azalea-inventory/src/lib.rs @@ -1,4 +1,5 @@ -/// Representations of various inventory data structures in Minecraft. +//! Representations of various inventory data structures in Minecraft. + pub mod components; pub mod item; pub mod operations; diff --git a/azalea/src/container.rs b/azalea/src/container.rs index 6c4e86cc..f452b1be 100644 --- a/azalea/src/container.rs +++ b/azalea/src/container.rs @@ -14,6 +14,7 @@ use azalea_physics::collision::BlockWithShape; use azalea_protocol::packets::game::ClientboundGamePacket; use bevy_app::{App, Plugin, Update}; use bevy_ecs::{component::Component, prelude::EventReader, system::Commands}; +use derive_more::Deref; use futures_lite::Future; use crate::bot::BotClientExt; @@ -26,17 +27,6 @@ impl Plugin for ContainerPlugin { } pub trait ContainerClientExt { - fn open_container_at( - &self, - pos: BlockPos, - ) -> impl Future> + Send; - fn open_inventory(&self) -> Option; - fn get_held_item(&self) -> ItemStack; - fn get_open_container(&self) -> Option; - fn view_container_or_inventory(&self) -> Menu; -} - -impl ContainerClientExt for Client { /// Open a container in the world, like a chest. Use /// [`Client::open_inventory`] to open your own inventory. /// @@ -54,6 +44,39 @@ impl ContainerClientExt for Client { /// let container = bot.open_container_at(target_pos).await; /// # } /// ``` + fn open_container_at( + &self, + pos: BlockPos, + ) -> impl Future> + Send; + /// Open the player's inventory. This will return None if another + /// container is open. + /// + /// Note that this will send a packet to the server once it's dropped. Also, + /// due to how it's implemented, you could call this function multiple times + /// while another inventory handle already exists (but you shouldn't). + /// + /// If you just want to get the items in the player's inventory without + /// sending any packets, use [`Client::menu`], [`Menu::player_slots_range`], + /// and [`Menu::slots`]. + fn open_inventory(&self) -> Option; + /// Returns a [`ContainerHandleRef`] to the client's currently open + /// container, or their inventory. + /// + /// This will not send a packet to close the container when it's dropped, + /// which may cause anticheat compatibility issues if you modify your + /// inventory without closing it afterwards. + /// + /// To simulate opening your own inventory (like pressing 'e') in a way that + /// won't trigger anticheats, use [`Client::open_inventory`]. + /// + /// To open a container in the world, use [`Client::open_container_at`]. + fn get_inventory(&self) -> ContainerHandleRef; + /// Get the item in the bot's hotbar that is currently being held in its + /// main hand. + fn get_held_item(&self) -> ItemStack; +} + +impl ContainerClientExt for Client { async fn open_container_at(&self, pos: BlockPos) -> Option { let mut ticks = self.get_tick_broadcaster(); // wait until it's not air (up to 10 ticks) @@ -92,20 +115,9 @@ impl ContainerClientExt for Client { } } - /// Open the player's inventory. This will return None if another - /// container is open. - /// - /// Note that this will send a packet to the server once it's dropped. Also, - /// due to how it's implemented, you could call this function multiple times - /// while another inventory handle already exists (but you shouldn't). - /// - /// If you just want to get the items in the player's inventory without - /// sending any packets, use [`Client::menu`], [`Menu::player_slots_range`], - /// and [`Menu::slots`]. fn open_inventory(&self) -> Option { let ecs = self.ecs.lock(); let inventory = ecs.get::(self.entity).expect("no inventory"); - if inventory.id == 0 { Some(ContainerHandle::new(0, self.clone())) } else { @@ -113,46 +125,16 @@ impl ContainerClientExt for Client { } } - /// Get the item in the bot's hotbar that is currently being held in its - /// main hand. + fn get_inventory(&self) -> ContainerHandleRef { + let ecs = self.ecs.lock(); + let inventory = ecs.get::(self.entity).expect("no inventory"); + ContainerHandleRef::new(inventory.id, self.clone()) + } + fn get_held_item(&self) -> ItemStack { self.map_get_component::(|inventory| inventory.held_item()) .expect("no inventory") } - - /// Get a handle to the open container. This will return None if no - /// container is open. This will not close the container when it's dropped. - /// - /// See [`Client::open_inventory`] or [`Client::menu`] if you want to open - /// your own inventory. - fn get_open_container(&self) -> Option { - let ecs = self.ecs.lock(); - let inventory = ecs.get::(self.entity).expect("no inventory"); - - if inventory.id == 0 { - None - } else { - Some(ContainerHandleRef { - id: inventory.id, - client: self.clone(), - }) - } - } - - /// Returns the player's currently open container menu, or their inventory - /// if no container is open. - /// - /// This tries to access the client's [`Inventory::container_menu`] and - /// falls back to [`Inventory::inventory_menu`]. - fn view_container_or_inventory(&self) -> Menu { - self.map_get_component::(|inventory| { - inventory - .container_menu - .clone() - .unwrap_or(inventory.inventory_menu.clone()) - }) - .expect("no inventory") - } } /// A handle to a container that may be open. This does not close the container @@ -169,6 +151,10 @@ impl Debug for ContainerHandleRef { } } impl ContainerHandleRef { + pub fn new(id: i32, client: Client) -> Self { + Self { id, client } + } + pub fn close(&self) { self.client.ecs.lock().send_event(CloseContainerEvent { entity: self.client.entity, @@ -219,6 +205,25 @@ impl ContainerHandleRef { self.menu().map(|menu| menu.slots()) } + /// A shortcut for [`Self::click`] with `PickupClick::Left`. + pub fn left_click(&self, slot: impl Into) { + self.click(PickupClick::Left { + slot: Some(slot.into() as u16), + }); + } + /// A shortcut for [`Self::click`] with `QuickMoveClick::Left`. + pub fn shift_click(&self, slot: impl Into) { + self.click(QuickMoveClick::Left { + slot: slot.into() as u16, + }); + } + /// A shortcut for [`Self::click`] with `PickupClick::Right`. + pub fn right_click(&self, slot: impl Into) { + self.click(PickupClick::Right { + slot: Some(slot.into() as u16), + }); + } + pub fn click(&self, operation: impl Into) { let operation = operation.into(); self.client.ecs.lock().send_event(ContainerClickEvent { @@ -231,6 +236,7 @@ impl ContainerHandleRef { /// A handle to the open container. The container will be closed once this is /// dropped. +#[derive(Deref)] pub struct ContainerHandle(ContainerHandleRef); impl Drop for ContainerHandle { @@ -250,62 +256,10 @@ impl ContainerHandle { Self(ContainerHandleRef { id, client }) } - /// Get the id of the container. If this is 0, that means it's the player's - /// inventory. Otherwise, the number isn't really meaningful since only one - /// container can be open at a time. - pub fn id(&self) -> i32 { - self.0.id() - } - - /// Returns the menu of the container. If the container is closed, this - /// will return `None`. - /// - /// Note that any modifications you make to the `Menu` you're given will not - /// actually cause any packets to be sent. If you're trying to modify your - /// inventory, use [`ContainerHandle::click`] instead - pub fn menu(&self) -> Option { - self.0.menu() - } - - /// Returns the item slots in the container, not including the player's - /// inventory. If the container is closed, this will return `None`. - pub fn contents(&self) -> Option> { - self.0.contents() - } - - /// Return the contents of the menu, including the player's inventory. If - /// the container is closed, this will return `None`. - pub fn slots(&self) -> Option> { - self.0.slots() - } - /// Closes the inventory by dropping the handle. pub fn close(self) { // implicitly calls drop } - - pub fn click(&self, operation: impl Into) { - self.0.click(operation); - } - - /// A shortcut for [`Self::click`] with `PickupClick::Left`. - pub fn left_click(&self, slot: impl Into) { - self.click(PickupClick::Left { - slot: Some(slot.into() as u16), - }); - } - /// A shortcut for [`Self::click`] with `QuickMoveClick::Left`. - pub fn shift_click(&self, slot: impl Into) { - self.click(QuickMoveClick::Left { - slot: slot.into() as u16, - }); - } - /// A shortcut for [`Self::click`] with `PickupClick::Right`. - pub fn right_click(&self, slot: impl Into) { - self.click(PickupClick::Right { - slot: Some(slot.into() as u16), - }); - } } #[derive(Component, Debug)] From e4ead93f195d3d91fd809dfc8cfee9cc36aabcbc Mon Sep 17 00:00:00 2001 From: mat Date: Mon, 9 Jun 2025 21:33:21 -0330 Subject: [PATCH 26/36] jump if in water while executing pathfinder path --- azalea-client/src/plugins/mining.rs | 8 ++++---- azalea/src/pathfinder/goals.rs | 16 ++++++++++++++-- azalea/src/pathfinder/moves/basic.rs | 3 +++ azalea/src/pathfinder/moves/mod.rs | 6 ++++++ 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/azalea-client/src/plugins/mining.rs b/azalea-client/src/plugins/mining.rs index f69c30f0..89e3d0e2 100644 --- a/azalea-client/src/plugins/mining.rs +++ b/azalea-client/src/plugins/mining.rs @@ -313,7 +313,7 @@ fn handle_mining_queued( physics, ) >= 1. { - // block was broken instantly + // block was broken instantly (instamined) commands.trigger_targets( FinishMiningBlockEvent { position: mining_queued.position, @@ -497,10 +497,10 @@ pub fn handle_stop_mining_block_event( mut events: EventReader, mut commands: Commands, mut mine_block_progress_events: EventWriter, - mut query: Query<(&mut Mining, &MineBlockPos, &mut MineProgress)>, + mut query: Query<(&MineBlockPos, &mut MineProgress)>, ) { for event in events.read() { - let (mut _mining, mine_block_pos, mut mine_progress) = query.get_mut(event.entity).unwrap(); + let (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"); @@ -639,7 +639,7 @@ pub fn continue_mining_block( )); **mine_progress = 0.; **mine_ticks = 0.; - **mine_delay = 0; + **mine_delay = 5; } mine_block_progress_events.write(MineBlockProgressEvent { diff --git a/azalea/src/pathfinder/goals.rs b/azalea/src/pathfinder/goals.rs index 4c0dbafa..c19bf504 100644 --- a/azalea/src/pathfinder/goals.rs +++ b/azalea/src/pathfinder/goals.rs @@ -1,6 +1,9 @@ //! The goals that a pathfinder can try to reach. -use std::{f32::consts::SQRT_2, fmt::Debug}; +use std::{ + f32::consts::SQRT_2, + fmt::{self, Debug}, +}; use azalea_core::position::{BlockPos, Vec3}; use azalea_world::ChunkStorage; @@ -193,7 +196,7 @@ impl Goal for AndGoals { } /// Move to a position where we can reach the given block. -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct ReachBlockPosGoal { pub pos: BlockPos, pub distance: f64, @@ -244,3 +247,12 @@ impl Goal for ReachBlockPosGoal { block_hit_result.block_pos == self.pos } } +impl Debug for ReachBlockPosGoal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ReachBlockPosGoal") + .field("pos", &self.pos) + .field("distance", &self.distance) + .field("max_check_distance", &self.max_check_distance) + .finish() + } +} diff --git a/azalea/src/pathfinder/moves/basic.rs b/azalea/src/pathfinder/moves/basic.rs index fe0d81f5..e352f385 100644 --- a/azalea/src/pathfinder/moves/basic.rs +++ b/azalea/src/pathfinder/moves/basic.rs @@ -55,6 +55,7 @@ fn execute_forward_move(mut ctx: ExecuteCtx) { } ctx.look_at(center); + ctx.jump_if_in_water(); ctx.sprint(SprintDirection::Forward); } @@ -141,6 +142,7 @@ fn execute_ascend_move(mut ctx: ExecuteCtx) { ctx.look_at(target_center); ctx.walk(WalkDirection::Forward); + ctx.jump_if_in_water(); // these checks are to make sure we don't fall if our velocity is too high in // the wrong direction @@ -439,6 +441,7 @@ fn execute_diagonal_move(mut ctx: ExecuteCtx) { ctx.look_at(target_center); ctx.sprint(SprintDirection::Forward); + ctx.jump_if_in_water(); } /// Go directly down, usually by mining. diff --git a/azalea/src/pathfinder/moves/mod.rs b/azalea/src/pathfinder/moves/mod.rs index 6c26a507..44d31bfb 100644 --- a/azalea/src/pathfinder/moves/mod.rs +++ b/azalea/src/pathfinder/moves/mod.rs @@ -111,6 +111,12 @@ impl ExecuteCtx<'_, '_, '_, '_, '_, '_, '_> { }); } + pub fn jump_if_in_water(&mut self) { + if self.physics.is_in_water() { + self.jump(); + } + } + /// Returns whether this block could be mined. pub fn should_mine(&mut self, block: BlockPos) -> bool { let block_state = self From 2a6ac0764fe9975f9b16d495ce773e4ae1f097e0 Mon Sep 17 00:00:00 2001 From: mat Date: Tue, 10 Jun 2025 19:47:42 -0100 Subject: [PATCH 27/36] add mine_with_auto_tool --- azalea/src/auto_tool.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/azalea/src/auto_tool.rs b/azalea/src/auto_tool.rs index 0f33dd09..63561772 100644 --- a/azalea/src/auto_tool.rs +++ b/azalea/src/auto_tool.rs @@ -1,8 +1,11 @@ use azalea_block::{BlockState, BlockTrait, fluid_state::FluidKind}; use azalea_client::{Client, inventory::Inventory}; +use azalea_core::position::BlockPos; use azalea_entity::{FluidOnEyes, Physics}; use azalea_inventory::{ItemStack, Menu, components}; +use crate::BotClientExt; + #[derive(Debug)] pub struct BestToolResult { pub index: usize, @@ -11,6 +14,7 @@ pub struct BestToolResult { pub trait AutoToolClientExt { fn best_tool_in_hotbar_for_block(&self, block: BlockState) -> BestToolResult; + fn mine_with_auto_tool(&self, block_pos: BlockPos) -> impl Future + Send; } impl AutoToolClientExt for Client { @@ -22,6 +26,17 @@ impl AutoToolClientExt for Client { accurate_best_tool_in_hotbar_for_block(block, menu, physics, fluid_on_eyes) } + + async fn mine_with_auto_tool(&self, block_pos: BlockPos) { + let block_state = self + .world() + .read() + .get_block_state(&block_pos) + .unwrap_or_default(); + let best_tool_result = self.best_tool_in_hotbar_for_block(block_state); + self.set_selected_hotbar_slot(best_tool_result.index as u8); + self.mine(block_pos).await; + } } /// Returns the best tool in the hotbar for the given block. From 9b0bd29db4faa9d94df0cec472346b814e7efcb9 Mon Sep 17 00:00:00 2001 From: mat Date: Wed, 11 Jun 2025 16:55:33 +1100 Subject: [PATCH 28/36] take BlockPos instead of &BlockPos in all function arguments --- azalea-client/src/plugins/interact.rs | 2 +- azalea-client/src/plugins/mining.rs | 12 +++++----- azalea-client/src/plugins/packet/game/mod.rs | 4 ++-- azalea-core/src/aabb.rs | 6 ++--- azalea-core/src/position.rs | 12 ++++++++++ azalea-entity/src/lib.rs | 4 ++-- azalea-entity/src/plugin/mod.rs | 14 ++++++------ azalea-physics/src/clip.rs | 10 ++++---- azalea-physics/src/collision/mod.rs | 8 ++----- azalea-physics/src/collision/shape.rs | 6 ++--- azalea-physics/src/fluids.rs | 10 ++++---- azalea-physics/src/lib.rs | 14 ++++++------ azalea-physics/src/travel.rs | 6 ++--- azalea-physics/tests/physics.rs | 12 +++++----- azalea-world/src/chunk_storage.rs | 24 ++++++++++---------- azalea-world/src/find_blocks.rs | 12 +++++----- azalea-world/src/world.rs | 8 +++---- azalea/examples/testbot/commands/debug.rs | 6 ++--- azalea/src/auto_tool.rs | 2 +- azalea/src/container.rs | 2 +- azalea/src/pathfinder/debug.rs | 4 ++-- azalea/src/pathfinder/mod.rs | 4 ++-- azalea/src/pathfinder/moves/mod.rs | 6 ++--- azalea/src/pathfinder/world.rs | 16 ++++++------- 24 files changed, 106 insertions(+), 98 deletions(-) diff --git a/azalea-client/src/plugins/interact.rs b/azalea-client/src/plugins/interact.rs index 95f7c17c..2e2b039f 100644 --- a/azalea-client/src/plugins/interact.rs +++ b/azalea-client/src/plugins/interact.rs @@ -343,7 +343,7 @@ pub fn pick_block( /// adventure mode check. pub fn check_is_interaction_restricted( instance: &Instance, - block_pos: &BlockPos, + block_pos: BlockPos, game_mode: &GameMode, inventory: &Inventory, ) -> bool { diff --git a/azalea-client/src/plugins/mining.rs b/azalea-client/src/plugins/mining.rs index 89e3d0e2..51bb5529 100644 --- a/azalea-client/src/plugins/mining.rs +++ b/azalea-client/src/plugins/mining.rs @@ -246,7 +246,7 @@ fn handle_mining_queued( let instance = instance_holder.instance.read(); if check_is_interaction_restricted( &instance, - &mining_queued.position, + mining_queued.position, &game_mode.current, inventory, ) { @@ -286,7 +286,7 @@ fn handle_mining_queued( } let target_block_state = instance - .get_block_state(&mining_queued.position) + .get_block_state(mining_queued.position) .unwrap_or_default(); // we can't break blocks if they don't have a bounding box @@ -450,7 +450,7 @@ pub fn handle_finish_mining_block_observer( query.get_mut(trigger.target()).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) { + if check_is_interaction_restricted(&instance, event.position, &game_mode.current, inventory) { return; } @@ -465,7 +465,7 @@ pub fn handle_finish_mining_block_observer( } } - let Some(block_state) = instance.get_block_state(&event.position) else { + let Some(block_state) = instance.get_block_state(event.position) else { return; }; @@ -485,7 +485,7 @@ pub fn handle_finish_mining_block_observer( // 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); + instance.set_block_state(event.position, block_state_for_fluid); } /// Abort mining a block. @@ -595,7 +595,7 @@ pub fn continue_mining_block( trace!("continue mining block at {:?}", mining.pos); 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(); + let target_block_state = instance.get_block_state(mining.pos).unwrap_or_default(); trace!("target_block_state: {target_block_state:?}"); diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs index 1307473d..a49a0209 100644 --- a/azalea-client/src/plugins/packet/game/mod.rs +++ b/azalea-client/src/plugins/packet/game/mod.rs @@ -1066,7 +1066,7 @@ impl GamePacketHandler<'_> { let world = local_player.instance.write(); - world.chunks.set_block_state(&p.pos, p.block_state); + world.chunks.set_block_state(p.pos, p.block_state); }); } @@ -1083,7 +1083,7 @@ impl GamePacketHandler<'_> { for state in &p.states { world .chunks - .set_block_state(&(p.section_pos + state.pos), state.state); + .set_block_state(p.section_pos + state.pos, state.state); } }); } diff --git a/azalea-core/src/aabb.rs b/azalea-core/src/aabb.rs index 42ae797d..03a754f0 100644 --- a/azalea-core/src/aabb.rs +++ b/azalea-core/src/aabb.rs @@ -211,7 +211,7 @@ impl AABB { boxes: &Vec, from: &Vec3, to: &Vec3, - pos: &BlockPos, + pos: BlockPos, ) -> Option { let mut t = 1.0; let mut dir = None; @@ -230,7 +230,7 @@ impl AABB { Some(BlockHitResult { location: from + &(delta * t), direction: dir, - block_pos: *pos, + block_pos: pos, inside: false, miss: false, world_border: false, @@ -500,7 +500,7 @@ mod tests { }], &Vec3::new(-1., -1., -1.), &Vec3::new(1., 1., 1.), - &BlockPos::new(0, 0, 0), + BlockPos::new(0, 0, 0), ), None ); diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs index 7cb8b143..b1560577 100644 --- a/azalea-core/src/position.rs +++ b/azalea-core/src/position.rs @@ -589,6 +589,12 @@ impl From<&ChunkBiomePos> for ChunkSectionBiomePos { } } } +impl From for ChunkSectionBiomePos { + #[inline] + fn from(pos: ChunkBiomePos) -> Self { + Self::from(&pos) + } +} vec3_impl!(ChunkSectionBiomePos, u8); /// The coordinates of a biome inside a chunk. Biomes are 4x4 blocks. @@ -604,6 +610,12 @@ impl From<&BlockPos> for ChunkBiomePos { ChunkBiomePos::from(&ChunkBlockPos::from(pos)) } } +impl From for ChunkBiomePos { + #[inline] + fn from(pos: BlockPos) -> Self { + ChunkBiomePos::from(&ChunkBlockPos::from(pos)) + } +} impl From<&ChunkBlockPos> for ChunkBiomePos { #[inline] fn from(pos: &ChunkBlockPos) -> Self { diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs index 0f33e01e..7b971c27 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -106,10 +106,10 @@ pub fn on_pos(offset: f32, chunk_storage: &ChunkStorage, pos: &Position) -> Bloc // TODO: check if block below is a fence, wall, or fence gate let block_pos = pos.down(1); - let block_state = chunk_storage.get_block_state(&block_pos); + let block_state = chunk_storage.get_block_state(block_pos); if block_state == Some(BlockState::AIR) { let block_pos_below = block_pos.down(1); - let block_state_below = chunk_storage.get_block_state(&block_pos_below); + let block_state_below = chunk_storage.get_block_state(block_pos_below); if let Some(_block_state_below) = block_state_below { // if block_state_below.is_fence() // || block_state_below.is_wall() diff --git a/azalea-entity/src/plugin/mod.rs b/azalea-entity/src/plugin/mod.rs index 03afe7cd..e64f9823 100644 --- a/azalea-entity/src/plugin/mod.rs +++ b/azalea-entity/src/plugin/mod.rs @@ -103,7 +103,7 @@ pub fn update_fluid_on_eyes( let eye_block_pos = BlockPos::from(Vec3::new(position.x, adjusted_eye_y, position.z)); let fluid_at_eye = instance .read() - .get_fluid_state(&eye_block_pos) + .get_fluid_state(eye_block_pos) .unwrap_or_default(); let fluid_cutoff_y = (eye_block_pos.y as f32 + fluid_at_eye.height()) as f64; if fluid_cutoff_y > adjusted_eye_y { @@ -134,7 +134,7 @@ pub fn update_on_climbable( let instance = instance.read(); let block_pos = BlockPos::from(position); - let block_state_at_feet = instance.get_block_state(&block_pos).unwrap_or_default(); + let block_state_at_feet = instance.get_block_state(block_pos).unwrap_or_default(); let block_at_feet = Box::::from(block_state_at_feet); let registry_block_at_feet = block_at_feet.as_registry_block(); @@ -159,7 +159,7 @@ fn is_trapdoor_useable_as_ladder( // block below must be a ladder let block_below = instance - .get_block_state(&block_pos.down(1)) + .get_block_state(block_pos.down(1)) .unwrap_or_default(); let registry_block_below = Box::::from(block_below).as_registry_block(); @@ -255,7 +255,7 @@ mod tests { &mut chunks, ); partial_instance.chunks.set_block_state( - &BlockPos::new(0, 0, 0), + BlockPos::new(0, 0, 0), azalea_registry::Block::Stone.into(), &chunks, ); @@ -266,7 +266,7 @@ mod tests { }; partial_instance .chunks - .set_block_state(&BlockPos::new(0, 0, 0), ladder.into(), &chunks); + .set_block_state(BlockPos::new(0, 0, 0), ladder.into(), &chunks); let trapdoor = OakTrapdoor { facing: FacingCardinal::East, @@ -277,12 +277,12 @@ mod tests { }; partial_instance .chunks - .set_block_state(&BlockPos::new(0, 1, 0), trapdoor.into(), &chunks); + .set_block_state(BlockPos::new(0, 1, 0), trapdoor.into(), &chunks); let instance = Instance::from(chunks); let trapdoor_matches_ladder = is_trapdoor_useable_as_ladder( instance - .get_block_state(&BlockPos::new(0, 1, 0)) + .get_block_state(BlockPos::new(0, 1, 0)) .unwrap_or_default(), BlockPos::new(0, 1, 0), &instance, diff --git a/azalea-physics/src/clip.rs b/azalea-physics/src/clip.rs index e7d203d8..2cef15c4 100644 --- a/azalea-physics/src/clip.rs +++ b/azalea-physics/src/clip.rs @@ -49,7 +49,7 @@ impl ClipContext { &self, fluid_state: FluidState, world: &ChunkStorage, - pos: &BlockPos, + pos: BlockPos, ) -> &VoxelShape { if self.fluid_pick_type.can_pick(&fluid_state) { crate::collision::fluid_shape(&fluid_state, world, pos) @@ -139,7 +139,7 @@ pub fn clip(chunk_storage: &ChunkStorage, context: ClipContext) -> BlockHitResul fn clip_with_interaction_override( from: &Vec3, to: &Vec3, - block_pos: &BlockPos, + block_pos: BlockPos, block_shape: &VoxelShape, _block_state: &BlockState, ) -> Option { @@ -168,7 +168,7 @@ pub fn traverse_blocks( from: Vec3, to: Vec3, context: C, - get_hit_result: impl Fn(&C, &BlockPos) -> Option, + get_hit_result: impl Fn(&C, BlockPos) -> Option, get_miss_result: impl Fn(&C) -> T, ) -> T { if from == to { @@ -188,7 +188,7 @@ pub fn traverse_blocks( }; let mut current_block = BlockPos::from(right_before_start); - if let Some(data) = get_hit_result(&context, ¤t_block) { + if let Some(data) = get_hit_result(&context, current_block) { return data; } @@ -249,7 +249,7 @@ pub fn traverse_blocks( percentage.z += percentage_step.z; } - if let Some(data) = get_hit_result(&context, ¤t_block) { + if let Some(data) = get_hit_result(&context, current_block) { return data; } } diff --git a/azalea-physics/src/collision/mod.rs b/azalea-physics/src/collision/mod.rs index 2e46970d..ef994deb 100644 --- a/azalea-physics/src/collision/mod.rs +++ b/azalea-physics/src/collision/mod.rs @@ -322,13 +322,9 @@ fn collide_with_shapes( /// /// The instance and position are required so it can check if the block above is /// also the same fluid type. -pub fn fluid_shape( - fluid: &FluidState, - world: &ChunkStorage, - pos: &BlockPos, -) -> &'static VoxelShape { +pub fn fluid_shape(fluid: &FluidState, world: &ChunkStorage, pos: BlockPos) -> &'static VoxelShape { if fluid.amount == 9 { - let fluid_state_above = world.get_fluid_state(&pos.up(1)).unwrap_or_default(); + let fluid_state_above = world.get_fluid_state(pos.up(1)).unwrap_or_default(); if fluid_state_above.kind == fluid.kind { return &BLOCK_SHAPE; } diff --git a/azalea-physics/src/collision/shape.rs b/azalea-physics/src/collision/shape.rs index 59671622..4d430ee7 100644 --- a/azalea-physics/src/collision/shape.rs +++ b/azalea-physics/src/collision/shape.rs @@ -408,7 +408,7 @@ impl VoxelShape { } } - pub fn clip(&self, from: &Vec3, to: &Vec3, block_pos: &BlockPos) -> Option { + pub fn clip(&self, from: &Vec3, to: &Vec3, block_pos: BlockPos) -> Option { if self.is_empty() { return None; } @@ -424,7 +424,7 @@ impl VoxelShape { self.find_index(Axis::Z, right_after_start.z - block_pos.z as f64), ) { Some(BlockHitResult { - block_pos: *block_pos, + block_pos, direction: Direction::nearest(vector).opposite(), location: right_after_start, inside: true, @@ -755,7 +755,7 @@ mod tests { .clip( &Vec3::new(-0.3, 0.5, 0.), &Vec3::new(5.3, 0.5, 0.), - &BlockPos::new(0, 0, 0), + BlockPos::new(0, 0, 0), ) .unwrap(); diff --git a/azalea-physics/src/fluids.rs b/azalea-physics/src/fluids.rs index c4716a27..5ea6194a 100644 --- a/azalea-physics/src/fluids.rs +++ b/azalea-physics/src/fluids.rs @@ -116,7 +116,7 @@ fn update_fluid_height_and_do_fluid_pushing( for cur_y in min_y..max_y { for cur_z in min_z..max_z { let cur_pos = BlockPos::new(cur_x, cur_y, cur_z); - let Some(fluid_at_cur_pos) = world.get_fluid_state(&cur_pos) else { + let Some(fluid_at_cur_pos) = world.get_fluid_state(cur_pos) else { continue; }; if fluid_at_cur_pos.kind != checking_fluid { @@ -192,7 +192,7 @@ pub fn get_fluid_flow(fluid: &FluidState, world: &Instance, pos: BlockPos) -> Ve let adjacent_block_pos = pos.offset_with_direction(direction); let adjacent_block_state = world - .get_block_state(&adjacent_block_pos) + .get_block_state(adjacent_block_pos) .unwrap_or_default(); let adjacent_fluid_state = FluidState::from(adjacent_block_state); @@ -206,7 +206,7 @@ pub fn get_fluid_flow(fluid: &FluidState, world: &Instance, pos: BlockPos) -> Ve if !legacy_blocks_motion(adjacent_block_state) { let block_pos_below_adjacent = adjacent_block_pos.down(1); let fluid_below_adjacent = world - .get_fluid_state(&block_pos_below_adjacent) + .get_fluid_state(block_pos_below_adjacent) .unwrap_or_default(); if fluid.affects_flow(&fluid_below_adjacent) { @@ -250,8 +250,8 @@ fn is_solid_face( adjacent_pos: BlockPos, direction: Direction, ) -> bool { - let block_state = world.get_block_state(&adjacent_pos).unwrap_or_default(); - let fluid_state = world.get_fluid_state(&adjacent_pos).unwrap_or_default(); + let block_state = world.get_block_state(adjacent_pos).unwrap_or_default(); + let fluid_state = world.get_fluid_state(adjacent_pos).unwrap_or_default(); if fluid_state.is_same_kind(fluid) { return false; } diff --git a/azalea-physics/src/lib.rs b/azalea-physics/src/lib.rs index f384a90f..1f381174 100644 --- a/azalea-physics/src/lib.rs +++ b/azalea-physics/src/lib.rs @@ -8,7 +8,7 @@ pub mod travel; use std::collections::HashSet; -use azalea_block::{BlockTrait, BlockState, fluid_state::FluidState, properties}; +use azalea_block::{BlockState, BlockTrait, fluid_state::FluidState, properties}; use azalea_core::{ math, position::{BlockPos, Vec3}, @@ -197,7 +197,7 @@ fn check_inside_blocks( // return; // } - let traversed_block_state = world.get_block_state(&traversed_block).unwrap_or_default(); + let traversed_block_state = world.get_block_state(traversed_block).unwrap_or_default(); if traversed_block_state.is_air() { continue; } @@ -268,7 +268,7 @@ fn handle_entity_inside_block( #[allow(clippy::single_match)] match registry_block { azalea_registry::Block::BubbleColumn => { - let block_above = world.get_block_state(&block_pos.up(1)).unwrap_or_default(); + let block_above = world.get_block_state(block_pos.up(1)).unwrap_or_default(); let is_block_above_empty = block_above.is_collision_shape_empty() && FluidState::from(block_above).is_empty(); let drag_down = block @@ -417,7 +417,7 @@ fn handle_relative_friction_and_calculate_movement( if physics.horizontal_collision || **jumping { let block_at_feet: azalea_registry::Block = world .chunks - .get_block_state(&(*position).into()) + .get_block_state((*position).into()) .unwrap_or_default() .into(); @@ -454,7 +454,7 @@ fn handle_on_climbable( && azalea_registry::Block::from( world .chunks - .get_block_state(&position.into()) + .get_block_state(position.into()) .unwrap_or_default(), ) != azalea_registry::Block::Scaffolding { @@ -486,10 +486,10 @@ fn get_friction_influenced_speed( /// Returns the what the entity's jump should be multiplied by based on the /// block they're standing on. fn block_jump_factor(world: &Instance, position: &Position) -> f32 { - let block_at_pos = world.chunks.get_block_state(&position.into()); + let block_at_pos = world.chunks.get_block_state(position.into()); let block_below = world .chunks - .get_block_state(&get_block_pos_below_that_affects_movement(position)); + .get_block_state(get_block_pos_below_that_affects_movement(position)); let block_at_pos_jump_factor = if let Some(block) = block_at_pos { Box::::from(block).behavior().jump_factor diff --git a/azalea-physics/src/travel.rs b/azalea-physics/src/travel.rs index 741267c2..a442f629 100644 --- a/azalea-physics/src/travel.rs +++ b/azalea-physics/src/travel.rs @@ -1,4 +1,4 @@ -use azalea_block::{BlockTrait, BlockState, fluid_state::FluidState}; +use azalea_block::{BlockState, BlockTrait, fluid_state::FluidState}; use azalea_core::{ aabb::AABB, position::{BlockPos, Vec3}, @@ -122,7 +122,7 @@ fn travel_in_air( let block_state_below = world .chunks - .get_block_state(&block_pos_below) + .get_block_state(block_pos_below) .unwrap_or(BlockState::AIR); let block_below: Box = block_state_below.into(); let block_friction = block_below.behavior().friction; @@ -392,7 +392,7 @@ fn contains_any_liquid(world: &Instance, bounding_box: AABB) -> bool { for z in min.z..max.z { let block_state = world .chunks - .get_block_state(&BlockPos::new(x, y, z)) + .get_block_state(BlockPos::new(x, y, z)) .unwrap_or_default(); if !FluidState::from(block_state).is_empty() { return true; diff --git a/azalea-physics/tests/physics.rs b/azalea-physics/tests/physics.rs index ebe1cfad..8150e5b0 100644 --- a/azalea-physics/tests/physics.rs +++ b/azalea-physics/tests/physics.rs @@ -121,7 +121,7 @@ fn test_collision() { )) .id(); let block_state = partial_world.chunks.set_block_state( - &BlockPos { x: 0, y: 69, z: 0 }, + BlockPos { x: 0, y: 69, z: 0 }, azalea_registry::Block::Stone.into(), &world_lock.write().chunks, ); @@ -177,7 +177,7 @@ fn test_slab_collision() { )) .id(); let block_state = partial_world.chunks.set_block_state( - &BlockPos { x: 0, y: 69, z: 0 }, + BlockPos { x: 0, y: 69, z: 0 }, azalea_block::blocks::StoneSlab { kind: azalea_block::properties::Type::Bottom, waterlogged: false, @@ -227,7 +227,7 @@ fn test_top_slab_collision() { )) .id(); let block_state = world_lock.write().chunks.set_block_state( - &BlockPos { x: 0, y: 69, z: 0 }, + BlockPos { x: 0, y: 69, z: 0 }, azalea_block::blocks::StoneSlab { kind: azalea_block::properties::Type::Top, waterlogged: false, @@ -284,7 +284,7 @@ fn test_weird_wall_collision() { )) .id(); let block_state = world_lock.write().chunks.set_block_state( - &BlockPos { x: 0, y: 69, z: 0 }, + BlockPos { x: 0, y: 69, z: 0 }, azalea_block::blocks::CobblestoneWall { east: azalea_block::properties::WallEast::Low, north: azalea_block::properties::WallNorth::Low, @@ -346,7 +346,7 @@ fn test_negative_coordinates_weird_wall_collision() { )) .id(); let block_state = world_lock.write().chunks.set_block_state( - &BlockPos { + BlockPos { x: -8, y: 69, z: -8, @@ -440,7 +440,7 @@ fn test_afk_pool() { world_lock .write() .chunks - .set_block_state(&BlockPos { x, y, z }, b); + .set_block_state(BlockPos { x, y, z }, b); }; let stone = azalea_block::blocks::Stone {}.into(); diff --git a/azalea-world/src/chunk_storage.rs b/azalea-world/src/chunk_storage.rs index a6e33739..2efd002d 100644 --- a/azalea-world/src/chunk_storage.rs +++ b/azalea-world/src/chunk_storage.rs @@ -155,7 +155,7 @@ impl PartialChunkStorage { pub fn set_block_state( &self, - pos: &BlockPos, + pos: BlockPos, state: BlockState, chunk_storage: &ChunkStorage, ) -> Option { @@ -293,26 +293,26 @@ impl ChunkStorage { self.map.get(pos).and_then(|chunk| chunk.upgrade()) } - pub fn get_block_state(&self, pos: &BlockPos) -> Option { + pub fn get_block_state(&self, pos: BlockPos) -> Option { let chunk_pos = ChunkPos::from(pos); let chunk = self.get(&chunk_pos)?; let chunk = chunk.read(); chunk.get_block_state(&ChunkBlockPos::from(pos), self.min_y) } - pub fn get_fluid_state(&self, pos: &BlockPos) -> Option { + pub fn get_fluid_state(&self, pos: BlockPos) -> Option { let block_state = self.get_block_state(pos)?; Some(FluidState::from(block_state)) } - pub fn get_biome(&self, pos: &BlockPos) -> Option { + pub fn get_biome(&self, pos: BlockPos) -> Option { let chunk_pos = ChunkPos::from(pos); let chunk = self.get(&chunk_pos)?; let chunk = chunk.read(); - chunk.get_biome(&ChunkBiomePos::from(pos), self.min_y) + chunk.get_biome(ChunkBiomePos::from(pos), self.min_y) } - pub fn set_block_state(&self, pos: &BlockPos, state: BlockState) -> Option { + pub fn set_block_state(&self, pos: BlockPos, state: BlockState) -> Option { if pos.y < self.min_y || pos.y >= (self.min_y + self.height as i32) { return None; } @@ -404,7 +404,7 @@ impl Chunk { } } - pub fn get_biome(&self, pos: &ChunkBiomePos, min_y: i32) -> Option { + pub fn get_biome(&self, pos: ChunkBiomePos, min_y: i32) -> Option { if pos.y < min_y { // y position is out of bounds return None; @@ -580,27 +580,27 @@ mod tests { ); assert!( chunk_storage - .get_block_state(&BlockPos { x: 0, y: 319, z: 0 }) + .get_block_state(BlockPos { x: 0, y: 319, z: 0 }) .is_some() ); assert!( chunk_storage - .get_block_state(&BlockPos { x: 0, y: 320, z: 0 }) + .get_block_state(BlockPos { x: 0, y: 320, z: 0 }) .is_none() ); assert!( chunk_storage - .get_block_state(&BlockPos { x: 0, y: 338, z: 0 }) + .get_block_state(BlockPos { x: 0, y: 338, z: 0 }) .is_none() ); assert!( chunk_storage - .get_block_state(&BlockPos { x: 0, y: -64, z: 0 }) + .get_block_state(BlockPos { x: 0, y: -64, z: 0 }) .is_some() ); assert!( chunk_storage - .get_block_state(&BlockPos { x: 0, y: -65, z: 0 }) + .get_block_state(BlockPos { x: 0, y: -65, z: 0 }) .is_none() ); } diff --git a/azalea-world/src/find_blocks.rs b/azalea-world/src/find_blocks.rs index 10068243..4086358f 100644 --- a/azalea-world/src/find_blocks.rs +++ b/azalea-world/src/find_blocks.rs @@ -7,7 +7,7 @@ impl Instance { /// Find the coordinates of a block in the world. /// /// Note that this is sorted by `x+y+z` and not `x^2+y^2+z^2` for - /// optimization purposes. + /// performance purposes. /// /// ``` /// # fn example(client: &azalea_client::Client) { @@ -93,7 +93,7 @@ impl Instance { /// are in the given block states. /// /// Note that this is sorted by `x+y+z` and not `x^2+y^2+z^2` for - /// optimization purposes. + /// performance purposes. pub fn find_blocks<'a>( &'a self, nearest_to: impl Into, @@ -274,8 +274,8 @@ mod tests { chunk_storage, ); - chunk_storage.set_block_state(&BlockPos { x: 17, y: 0, z: 0 }, Block::Stone.into()); - chunk_storage.set_block_state(&BlockPos { x: 0, y: 18, z: 0 }, Block::Stone.into()); + chunk_storage.set_block_state(BlockPos { x: 17, y: 0, z: 0 }, Block::Stone.into()); + chunk_storage.set_block_state(BlockPos { x: 0, y: 18, z: 0 }, Block::Stone.into()); let pos = instance.find_block(BlockPos { x: 0, y: 0, z: 0 }, &Block::Stone.into()); assert_eq!(pos, Some(BlockPos { x: 17, y: 0, z: 0 })); @@ -301,8 +301,8 @@ mod tests { chunk_storage, ); - chunk_storage.set_block_state(&BlockPos { x: -1, y: 0, z: 0 }, Block::Stone.into()); - chunk_storage.set_block_state(&BlockPos { x: 15, y: 0, z: 0 }, Block::Stone.into()); + chunk_storage.set_block_state(BlockPos { x: -1, y: 0, z: 0 }, Block::Stone.into()); + chunk_storage.set_block_state(BlockPos { x: 15, y: 0, z: 0 }, Block::Stone.into()); let pos = instance.find_block(BlockPos { x: 0, y: 0, z: 0 }, &Block::Stone.into()); assert_eq!(pos, Some(BlockPos { x: -1, y: 0, z: 0 })); diff --git a/azalea-world/src/world.rs b/azalea-world/src/world.rs index 3e6359ff..89132a73 100644 --- a/azalea-world/src/world.rs +++ b/azalea-world/src/world.rs @@ -171,11 +171,11 @@ pub struct Instance { } impl Instance { - pub fn get_block_state(&self, pos: &BlockPos) -> Option { + pub fn get_block_state(&self, pos: BlockPos) -> Option { self.chunks.get_block_state(pos) } - pub fn get_fluid_state(&self, pos: &BlockPos) -> Option { + pub fn get_fluid_state(&self, pos: BlockPos) -> Option { self.chunks.get_block_state(pos).map(FluidState::from) } @@ -187,11 +187,11 @@ impl Instance { /// Note that biomes are internally stored as 4x4x4 blocks, so if you're /// writing code that searches for a specific biome it'll probably be more /// efficient to avoid scanning every single block. - pub fn get_biome(&self, pos: &BlockPos) -> Option { + pub fn get_biome(&self, pos: BlockPos) -> Option { self.chunks.get_biome(pos) } - pub fn set_block_state(&self, pos: &BlockPos, state: BlockState) -> Option { + pub fn set_block_state(&self, pos: BlockPos, state: BlockState) -> Option { self.chunks.set_block_state(pos, state) } } diff --git a/azalea/examples/testbot/commands/debug.rs b/azalea/examples/testbot/commands/debug.rs index b97ad71d..ea5dbe6f 100644 --- a/azalea/examples/testbot/commands/debug.rs +++ b/azalea/examples/testbot/commands/debug.rs @@ -110,7 +110,7 @@ pub fn register(commands: &mut CommandDispatcher>) { }; let block_pos = hit_result.block_pos; - let block = source.bot.world().read().get_block_state(&block_pos); + let block = source.bot.world().read().get_block_state(block_pos); source.reply(&format!("I'm looking at {block:?} at {block_pos:?}")); @@ -125,7 +125,7 @@ pub fn register(commands: &mut CommandDispatcher>) { let z = get_integer(ctx, "z").unwrap(); println!("getblock xyz {x} {y} {z}"); let block_pos = BlockPos::new(x, y, z); - let block = source.bot.world().read().get_block_state(&block_pos); + let block = source.bot.world().read().get_block_state(block_pos); source.reply(&format!("Block at {block_pos} is {block:?}")); 1 })), @@ -138,7 +138,7 @@ pub fn register(commands: &mut CommandDispatcher>) { let z = get_integer(ctx, "z").unwrap(); println!("getfluid xyz {x} {y} {z}"); let block_pos = BlockPos::new(x, y, z); - let block = source.bot.world().read().get_fluid_state(&block_pos); + let block = source.bot.world().read().get_fluid_state(block_pos); source.reply(&format!("Fluid at {block_pos} is {block:?}")); 1 })), diff --git a/azalea/src/auto_tool.rs b/azalea/src/auto_tool.rs index 63561772..1f339c85 100644 --- a/azalea/src/auto_tool.rs +++ b/azalea/src/auto_tool.rs @@ -31,7 +31,7 @@ impl AutoToolClientExt for Client { let block_state = self .world() .read() - .get_block_state(&block_pos) + .get_block_state(block_pos) .unwrap_or_default(); let best_tool_result = self.best_tool_in_hotbar_for_block(block_state); self.set_selected_hotbar_slot(best_tool_result.index as u8); diff --git a/azalea/src/container.rs b/azalea/src/container.rs index f452b1be..ee618df2 100644 --- a/azalea/src/container.rs +++ b/azalea/src/container.rs @@ -84,7 +84,7 @@ impl ContainerClientExt for Client { if !self .world() .read() - .get_block_state(&pos) + .get_block_state(pos) .unwrap_or_default() .is_collision_shape_empty() { diff --git a/azalea/src/pathfinder/debug.rs b/azalea/src/pathfinder/debug.rs index b00e4272..6b319531 100644 --- a/azalea/src/pathfinder/debug.rs +++ b/azalea/src/pathfinder/debug.rs @@ -67,9 +67,9 @@ pub fn debug_render_path_with_particles( let step_count = (start_vec3.distance_squared_to(&end_vec3).sqrt() * 4.0) as usize; - let target_block_state = chunks.get_block_state(&movement.target).unwrap_or_default(); + let target_block_state = chunks.get_block_state(movement.target).unwrap_or_default(); let above_target_block_state = chunks - .get_block_state(&movement.target.up(1)) + .get_block_state(movement.target.up(1)) .unwrap_or_default(); // this isn't foolproof, there might be another block that could be mined // depending on the move, but it's good enough for debugging diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index c4586d29..c72573f5 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -1334,10 +1334,10 @@ mod tests { partial_chunks.set(&chunk_pos, Some(Chunk::default()), &mut chunks); } for block_pos in solid_blocks { - chunks.set_block_state(block_pos, azalea_registry::Block::Stone.into()); + chunks.set_block_state(*block_pos, azalea_registry::Block::Stone.into()); } for (block_pos, block_state) in extra_blocks { - chunks.set_block_state(block_pos, *block_state); + chunks.set_block_state(*block_pos, *block_state); } let player = SimulatedPlayerBundle::new(Vec3::new( diff --git a/azalea/src/pathfinder/moves/mod.rs b/azalea/src/pathfinder/moves/mod.rs index 44d31bfb..6d1c7c55 100644 --- a/azalea/src/pathfinder/moves/mod.rs +++ b/azalea/src/pathfinder/moves/mod.rs @@ -122,7 +122,7 @@ impl ExecuteCtx<'_, '_, '_, '_, '_, '_, '_> { let block_state = self .instance .read() - .get_block_state(&block) + .get_block_state(block) .unwrap_or_default(); if is_block_state_passable(block_state) { // block is already passable, no need to mine it @@ -138,7 +138,7 @@ impl ExecuteCtx<'_, '_, '_, '_, '_, '_, '_> { let block_state = self .instance .read() - .get_block_state(&block) + .get_block_state(block) .unwrap_or_default(); if is_block_state_passable(block_state) { // block is already passable, no need to mine it @@ -191,7 +191,7 @@ impl ExecuteCtx<'_, '_, '_, '_, '_, '_, '_> { pub fn get_block_state(&self, block: BlockPos) -> BlockState { self.instance .read() - .get_block_state(&block) + .get_block_state(block) .unwrap_or_default() } } diff --git a/azalea/src/pathfinder/world.rs b/azalea/src/pathfinder/world.rs index 3ec95136..e9337503 100644 --- a/azalea/src/pathfinder/world.rs +++ b/azalea/src/pathfinder/world.rs @@ -625,13 +625,13 @@ mod tests { .chunks .set(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()), &mut world); partial_world.chunks.set_block_state( - &BlockPos::new(0, 0, 0), + BlockPos::new(0, 0, 0), azalea_registry::Block::Stone.into(), &world, ); partial_world .chunks - .set_block_state(&BlockPos::new(0, 1, 0), BlockState::AIR, &world); + .set_block_state(BlockPos::new(0, 1, 0), BlockState::AIR, &world); let ctx = CachedWorld::new(Arc::new(RwLock::new(world.into())), BlockPos::default()); assert!(!ctx.is_block_pos_passable(BlockPos::new(0, 0, 0))); @@ -646,13 +646,13 @@ mod tests { .chunks .set(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()), &mut world); partial_world.chunks.set_block_state( - &BlockPos::new(0, 0, 0), + BlockPos::new(0, 0, 0), azalea_registry::Block::Stone.into(), &world, ); partial_world .chunks - .set_block_state(&BlockPos::new(0, 1, 0), BlockState::AIR, &world); + .set_block_state(BlockPos::new(0, 1, 0), BlockState::AIR, &world); let ctx = CachedWorld::new(Arc::new(RwLock::new(world.into())), BlockPos::default()); assert!(ctx.is_block_pos_solid(BlockPos::new(0, 0, 0))); @@ -667,19 +667,19 @@ mod tests { .chunks .set(&ChunkPos { x: 0, z: 0 }, Some(Chunk::default()), &mut world); partial_world.chunks.set_block_state( - &BlockPos::new(0, 0, 0), + BlockPos::new(0, 0, 0), azalea_registry::Block::Stone.into(), &world, ); partial_world .chunks - .set_block_state(&BlockPos::new(0, 1, 0), BlockState::AIR, &world); + .set_block_state(BlockPos::new(0, 1, 0), BlockState::AIR, &world); partial_world .chunks - .set_block_state(&BlockPos::new(0, 2, 0), BlockState::AIR, &world); + .set_block_state(BlockPos::new(0, 2, 0), BlockState::AIR, &world); partial_world .chunks - .set_block_state(&BlockPos::new(0, 3, 0), BlockState::AIR, &world); + .set_block_state(BlockPos::new(0, 3, 0), BlockState::AIR, &world); let ctx = CachedWorld::new(Arc::new(RwLock::new(world.into())), BlockPos::default()); assert!(ctx.is_standable_at_block_pos(BlockPos::new(0, 1, 0))); From ab05e7bdae43de3595718fee877c0fbcc6368555 Mon Sep 17 00:00:00 2001 From: mat Date: Wed, 11 Jun 2025 02:55:30 -0530 Subject: [PATCH 29/36] add Client::attack_cooldown_remaining_ticks --- azalea-client/src/plugins/attack.rs | 30 +++++++++++++++++++++++------ azalea-entity/src/lib.rs | 6 ++++-- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/azalea-client/src/plugins/attack.rs b/azalea-client/src/plugins/attack.rs index cac2a021..2409dacc 100644 --- a/azalea-client/src/plugins/attack.rs +++ b/azalea-client/src/plugins/attack.rs @@ -53,15 +53,33 @@ impl Client { } /// Whether the player has an attack cooldown. + /// + /// Also see [`Client::attack_cooldown_remaining_ticks`]. pub fn has_attack_cooldown(&self) -> bool { - let Some(AttackStrengthScale(ticks_since_last_attack)) = - self.get_component::() - else { - // they don't even have an AttackStrengthScale so they probably can't attack - // lmao, just return false + let Some(attack_strength_scale) = self.get_component::() else { + // they don't even have an AttackStrengthScale so they probably can't even + // attack? whatever, just return false return false; }; - ticks_since_last_attack < 1.0 + *attack_strength_scale < 1.0 + } + + /// Returns the number of ticks until we can attack at full strength again. + /// + /// Also see [`Client::has_attack_cooldown`]. + pub fn attack_cooldown_remaining_ticks(&self) -> usize { + let mut ecs = self.ecs.lock(); + let Ok((attributes, ticks_since_last_attack)) = ecs + .query::<(&Attributes, &TicksSinceLastAttack)>() + .get(&ecs, self.entity) + else { + return 0; + }; + + let attack_strength_delay = get_attack_strength_delay(attributes); + let remaining_ticks = attack_strength_delay - **ticks_since_last_attack as f32; + + remaining_ticks.max(0.).ceil() as usize } } diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs index 7b971c27..0058708f 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -253,9 +253,11 @@ impl Eq for LookDirection {} /// bounding box. #[derive(Debug, Component, Clone, Default)] pub struct Physics { - /// How fast the entity is moving. + /// How fast the entity is moving. Sometimes referred to as the delta + /// movement. /// - /// Sometimes referred to as the delta movement. + /// Note that our Y velocity will be approximately -0.0784 when we're on the + /// ground due to how Minecraft applies gravity. pub velocity: Vec3, pub vec_delta_codec: VecDeltaCodec, From 067ec06f26ecaf7a319eb3ce61307b9730176313 Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 12 Jun 2025 05:13:58 +1100 Subject: [PATCH 30/36] add BlockPos::distance_to and length --- azalea-core/src/position.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs index b1560577..d6e67dc2 100644 --- a/azalea-core/src/position.rs +++ b/azalea-core/src/position.rs @@ -419,6 +419,22 @@ impl BlockPos { pub fn offset_with_direction(self, direction: Direction) -> Self { self + direction.normal() } + + /// Get the distance (as an f64) of this BlockPos to the origin by + /// doing `sqrt(x^2 + y^2 + z^2)`. + pub fn length(&self) -> f64 { + f64::sqrt((self.x * self.x + self.y * self.y + self.z * self.z) as f64) + } + + /// Get the distance (as an f64) from this position to another position. + /// Equivalent to `(self - other).length()`. + /// + /// Note that if you're using this in a hot path, it may be more performant + /// to use [`BlockPos::distance_squared_to`] instead (by squaring the other + /// side in the comparison). + pub fn distance_to(self, other: Self) -> f64 { + (self - other).length() + } } /// Similar to [`BlockPos`] but it's serialized as 3 varints instead of one From 1b348ceeffc61e49b19f2982e7a9de479c1678de Mon Sep 17 00:00:00 2001 From: mat Date: Wed, 11 Jun 2025 22:22:26 +0000 Subject: [PATCH 31/36] implement reverting block state predictions on ack --- azalea-client/src/client.rs | 4 +- azalea-client/src/plugins/interact.rs | 107 ++++++++++++++++-- azalea-client/src/plugins/mining.rs | 42 ++++--- azalea-client/src/plugins/packet/game/mod.rs | 50 +++++--- azalea-client/src/test_utils/simulation.rs | 11 +- azalea-client/tests/mine_block_rollback.rs | 49 ++++++++ .../tests/mine_block_without_rollback.rs | 51 +++++++++ .../src/packets/game/c_block_changed_ack.rs | 2 +- .../src/packets/game/s_player_action.rs | 5 +- .../src/packets/game/s_use_item.rs | 2 +- .../src/packets/game/s_use_item_on.rs | 2 +- azalea-world/src/palette/container.rs | 13 +-- azalea/src/pathfinder/simulation.rs | 4 +- 13 files changed, 277 insertions(+), 65 deletions(-) create mode 100644 azalea-client/tests/mine_block_rollback.rs create mode 100644 azalea-client/tests/mine_block_without_rollback.rs diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 0304e7b0..d9fec523 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -51,7 +51,7 @@ use crate::{ connection::RawConnection, disconnect::DisconnectEvent, events::Event, - interact::CurrentSequenceNumber, + interact::BlockStatePredictionHandler, inventory::Inventory, join::{ConnectOpts, StartJoinServerEvent}, local_player::{Hunger, InstanceHolder, PermissionLevel, PlayerAbilities, TabList}, @@ -586,7 +586,7 @@ pub struct JoinedClientBundle { pub physics_state: PhysicsState, pub inventory: Inventory, pub tab_list: TabList, - pub current_sequence_number: CurrentSequenceNumber, + pub current_sequence_number: BlockStatePredictionHandler, pub last_sent_direction: LastSentLookDirection, pub abilities: PlayerAbilities, pub permission_level: PermissionLevel, diff --git a/azalea-client/src/plugins/interact.rs b/azalea-client/src/plugins/interact.rs index 2e2b039f..31b5acf4 100644 --- a/azalea-client/src/plugins/interact.rs +++ b/azalea-client/src/plugins/interact.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use azalea_block::BlockState; use azalea_core::{ direction::Direction, @@ -96,17 +98,95 @@ impl Client { } } -/// 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); +/// 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, +} +#[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 CurrentSequenceNumber { +impl BlockStatePredictionHandler { /// Get the next sequence number that we're going to use and increment the /// value. - pub fn get_next(&mut self) -> u32 { - self.0 += 1; - self.0 + 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: 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); + } } } @@ -163,13 +243,15 @@ pub fn handle_start_use_item_queued( query: Query<( Entity, &StartUseItemQueued, - &mut CurrentSequenceNumber, + &mut BlockStatePredictionHandler, &HitResultComponent, &LookDirection, Option<&Mining>, )>, ) { - for (entity, start_use_item, mut sequence_number, hit_result, look_direction, mining) in query { + for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in + query + { commands.entity(entity).remove::(); if mining.is_some() { @@ -203,12 +285,13 @@ pub fn handle_start_use_item_queued( 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, - sequence: sequence_number.get_next(), + seq, x_rot: look_direction.x_rot, y_rot: look_direction.y_rot, }, @@ -219,7 +302,7 @@ pub fn handle_start_use_item_queued( ServerboundUseItemOn { hand: start_use_item.hand, block_hit: block_hit_result.into(), - sequence: sequence_number.get_next(), + seq, }, )); // TODO: depending on the result of useItemOn, this might diff --git a/azalea-client/src/plugins/mining.rs b/azalea-client/src/plugins/mining.rs index 51bb5529..88bd3be8 100644 --- a/azalea-client/src/plugins/mining.rs +++ b/azalea-client/src/plugins/mining.rs @@ -1,6 +1,6 @@ use azalea_block::{BlockState, BlockTrait, fluid_state::FluidState}; use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick}; -use azalea_entity::{FluidOnEyes, Physics, mining::get_mine_progress}; +use azalea_entity::{FluidOnEyes, Physics, Position, mining::get_mine_progress}; use azalea_inventory::ItemStack; use azalea_physics::{PhysicsSet, collision::BlockWithShape}; use azalea_protocol::packets::game::s_player_action::{self, ServerboundPlayerAction}; @@ -13,7 +13,7 @@ use tracing::trace; use crate::{ Client, interact::{ - CurrentSequenceNumber, HitResultComponent, SwingArmEvent, can_use_game_master_blocks, + BlockStatePredictionHandler, HitResultComponent, SwingArmEvent, can_use_game_master_blocks, check_is_interaction_restricted, }, inventory::{Inventory, InventorySet}, @@ -216,7 +216,7 @@ fn handle_mining_queued( &FluidOnEyes, &Physics, Option<&Mining>, - &mut CurrentSequenceNumber, + &mut BlockStatePredictionHandler, &mut MineDelay, &mut MineProgress, &mut MineTicks, @@ -280,7 +280,7 @@ fn handle_mining_queued( pos: current_mining_pos .expect("IsMining is true so MineBlockPos must be present"), direction: mining_queued.direction, - sequence: 0, + seq: 0, }, )); } @@ -345,7 +345,7 @@ fn handle_mining_queued( action: s_player_action::Action::StartDestroyBlock, pos: mining_queued.position, direction: mining_queued.direction, - sequence: sequence_number.get_next(), + seq: sequence_number.start_predicting(), }, )); // vanilla really does send two swing arm packets @@ -440,14 +440,22 @@ pub fn handle_finish_mining_block_observer( &Inventory, &PlayerAbilities, &PermissionLevel, - &mut CurrentSequenceNumber, + &Position, + &mut BlockStatePredictionHandler, )>, instances: Res, ) { let event = trigger.event(); - let (instance_name, game_mode, inventory, abilities, permission_level, _sequence_number) = - query.get_mut(trigger.target()).unwrap(); + let ( + instance_name, + game_mode, + inventory, + abilities, + permission_level, + player_pos, + mut prediction_handler, + ) = query.get_mut(trigger.target()).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) { @@ -469,7 +477,8 @@ pub fn handle_finish_mining_block_observer( return; }; - let registry_block = Box::::from(block_state).as_registry_block(); + let registry_block: azalea_registry::Block = + Box::::from(block_state).as_registry_block(); if !can_use_game_master_blocks(abilities, permission_level) && matches!( registry_block, @@ -485,7 +494,10 @@ pub fn handle_finish_mining_block_observer( // 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); + let old_state = instance + .set_block_state(event.position, block_state_for_fluid) + .unwrap_or_default(); + prediction_handler.retain_known_server_state(event.position, old_state, **player_pos); } /// Abort mining a block. @@ -510,7 +522,7 @@ pub fn handle_stop_mining_block_event( action: s_player_action::Action::AbortDestroyBlock, pos: mine_block_pos, direction: Direction::Down, - sequence: 0, + seq: 0, }, )); commands.entity(event.entity).remove::(); @@ -538,7 +550,7 @@ pub fn continue_mining_block( &mut MineDelay, &mut MineProgress, &mut MineTicks, - &mut CurrentSequenceNumber, + &mut BlockStatePredictionHandler, )>, mut commands: Commands, mut mine_block_progress_events: EventWriter, @@ -557,7 +569,7 @@ pub fn continue_mining_block( mut mine_delay, mut mine_progress, mut mine_ticks, - mut sequence_number, + mut prediction_handler, ) in query.iter_mut() { if **mine_delay > 0 { @@ -580,7 +592,7 @@ pub fn continue_mining_block( action: s_player_action::Action::StartDestroyBlock, pos: mining.pos, direction: mining.dir, - sequence: sequence_number.get_next(), + seq: prediction_handler.start_predicting(), }, )); commands.trigger(SwingArmEvent { entity }); @@ -634,7 +646,7 @@ pub fn continue_mining_block( action: s_player_action::Action::StopDestroyBlock, pos: mining.pos, direction: mining.dir, - sequence: sequence_number.get_next(), + seq: prediction_handler.start_predicting(), }, )); **mine_progress = 0.; diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs index a49a0209..b2a4abc4 100644 --- a/azalea-client/src/plugins/packet/game/mod.rs +++ b/azalea-client/src/plugins/packet/game/mod.rs @@ -25,6 +25,7 @@ use crate::{ connection::RawConnection, declare_packet_handlers, disconnect::DisconnectEvent, + interact::BlockStatePredictionHandler, inventory::{ ClientSideCloseContainerEvent, Inventory, MenuOpenedEvent, SetContainerContentEvent, }, @@ -1061,13 +1062,17 @@ impl GamePacketHandler<'_> { pub fn block_update(&mut self, p: &ClientboundBlockUpdate) { debug!("Got block update packet {p:?}"); - as_system::>(self.ecs, |mut query| { - let local_player = query.get_mut(self.player).unwrap(); + as_system::>( + self.ecs, + |mut query| { + let (local_player, mut prediction_handler) = query.get_mut(self.player).unwrap(); - let world = local_player.instance.write(); - - world.chunks.set_block_state(p.pos, p.block_state); - }); + let world = local_player.instance.read(); + if !prediction_handler.update_known_server_state(p.pos, p.block_state) { + world.chunks.set_block_state(p.pos, p.block_state); + } + }, + ); } pub fn animate(&mut self, p: &ClientboundAnimate) { @@ -1077,15 +1082,19 @@ impl GamePacketHandler<'_> { pub fn section_blocks_update(&mut self, p: &ClientboundSectionBlocksUpdate) { debug!("Got section blocks update packet {p:?}"); - as_system::>(self.ecs, |mut query| { - let local_player = query.get_mut(self.player).unwrap(); - let world = local_player.instance.write(); - for state in &p.states { - world - .chunks - .set_block_state(p.section_pos + state.pos, state.state); - } - }); + as_system::>( + self.ecs, + |mut query| { + let (local_player, mut prediction_handler) = query.get_mut(self.player).unwrap(); + let world = local_player.instance.read(); + for new_state in &p.states { + let pos = p.section_pos + new_state.pos; + if !prediction_handler.update_known_server_state(pos, new_state.state) { + world.chunks.set_block_state(pos, new_state.state); + } + } + }, + ); } pub fn game_event(&mut self, p: &ClientboundGameEvent) { @@ -1125,7 +1134,16 @@ impl GamePacketHandler<'_> { pub fn award_stats(&mut self, _p: &ClientboundAwardStats) {} - pub fn block_changed_ack(&mut self, _p: &ClientboundBlockChangedAck) {} + pub fn block_changed_ack(&mut self, p: &ClientboundBlockChangedAck) { + as_system::>( + self.ecs, + |mut query| { + let (local_player, mut prediction_handler) = query.get_mut(self.player).unwrap(); + let world = local_player.instance.read(); + prediction_handler.end_prediction_up_to(p.seq, &world); + }, + ); + } pub fn block_destruction(&mut self, _p: &ClientboundBlockDestruction) {} diff --git a/azalea-client/src/test_utils/simulation.rs b/azalea-client/src/test_utils/simulation.rs index caf63113..e7ac8d5b 100644 --- a/azalea-client/src/test_utils/simulation.rs +++ b/azalea-client/src/test_utils/simulation.rs @@ -6,7 +6,7 @@ use azalea_buf::AzaleaWrite; use azalea_core::{ delta::PositionDelta8, game_type::{GameMode, OptionalGameType}, - position::{ChunkPos, Vec3}, + position::{BlockPos, ChunkPos, Vec3}, resource_location::ResourceLocation, tick::GameTick, }; @@ -102,6 +102,9 @@ impl Simulation { raw_conn.injected_clientbound_packets.push(buf); }); } + pub fn send_event(&mut self, event: impl bevy_ecs::event::Event) { + self.app.world_mut().send_event(event); + } pub fn tick(&mut self) { tick_app(&mut self.app); @@ -151,6 +154,12 @@ impl Simulation { .chunks .get(&chunk_pos) } + pub fn get_block_state(&self, pos: BlockPos) -> Option { + self.component::() + .instance + .read() + .get_block_state(pos) + } pub fn disconnect(&mut self) { // send DisconnectEvent diff --git a/azalea-client/tests/mine_block_rollback.rs b/azalea-client/tests/mine_block_rollback.rs new file mode 100644 index 00000000..e9b46d13 --- /dev/null +++ b/azalea-client/tests/mine_block_rollback.rs @@ -0,0 +1,49 @@ +use azalea_client::{mining::StartMiningBlockEvent, test_utils::prelude::*}; +use azalea_core::{ + position::{BlockPos, ChunkPos}, + resource_location::ResourceLocation, +}; +use azalea_protocol::packets::{ + ConnectionProtocol, + game::{ClientboundBlockChangedAck, ClientboundBlockUpdate}, +}; +use azalea_registry::{Block, DataRegistry, DimensionType}; + +#[test] +fn test_mine_block_rollback() { + init_tracing(); + + let mut simulation = Simulation::new(ConnectionProtocol::Game); + simulation.receive_packet(make_basic_login_packet( + DimensionType::new_raw(0), + ResourceLocation::new("azalea:overworld"), + )); + + simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16)); + simulation.tick(); + + let pos = BlockPos::new(1, 2, 3); + simulation.receive_packet(ClientboundBlockUpdate { + pos, + // tnt is used for this test because it's insta-mineable so we don't have to waste ticks + // waiting + block_state: Block::Tnt.into(), + }); + simulation.tick(); + assert_eq!(simulation.get_block_state(pos), Some(Block::Tnt.into())); + println!("set serverside tnt"); + + simulation.send_event(StartMiningBlockEvent { + entity: simulation.entity, + position: pos, + }); + simulation.tick(); + assert_eq!(simulation.get_block_state(pos), Some(Block::Air.into())); + println!("set clientside air"); + + // server didn't send the new block, so the change should be rolled back + simulation.receive_packet(ClientboundBlockChangedAck { seq: 1 }); + simulation.tick(); + assert_eq!(simulation.get_block_state(pos), Some(Block::Tnt.into())); + println!("reset serverside tnt"); +} diff --git a/azalea-client/tests/mine_block_without_rollback.rs b/azalea-client/tests/mine_block_without_rollback.rs new file mode 100644 index 00000000..02fb1a77 --- /dev/null +++ b/azalea-client/tests/mine_block_without_rollback.rs @@ -0,0 +1,51 @@ +use azalea_client::{mining::StartMiningBlockEvent, test_utils::prelude::*}; +use azalea_core::{ + position::{BlockPos, ChunkPos}, + resource_location::ResourceLocation, +}; +use azalea_protocol::packets::{ + ConnectionProtocol, + game::{ClientboundBlockChangedAck, ClientboundBlockUpdate}, +}; +use azalea_registry::{Block, DataRegistry, DimensionType}; + +#[test] +fn test_mine_block_without_rollback() { + init_tracing(); + + let mut simulation = Simulation::new(ConnectionProtocol::Game); + simulation.receive_packet(make_basic_login_packet( + DimensionType::new_raw(0), + ResourceLocation::new("azalea:overworld"), + )); + + simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16)); + simulation.tick(); + + let pos = BlockPos::new(1, 2, 3); + simulation.receive_packet(ClientboundBlockUpdate { + pos, + // tnt is used for this test because it's insta-mineable so we don't have to waste ticks + // waiting + block_state: Block::Tnt.into(), + }); + simulation.tick(); + assert_eq!(simulation.get_block_state(pos), Some(Block::Tnt.into())); + + simulation.send_event(StartMiningBlockEvent { + entity: simulation.entity, + position: pos, + }); + simulation.tick(); + assert_eq!(simulation.get_block_state(pos), Some(Block::Air.into())); + + // server acknowledged our change by sending a BlockUpdate + BlockChangedAck, so + // no rollback + simulation.receive_packet(ClientboundBlockUpdate { + pos, + block_state: Block::Air.into(), + }); + simulation.receive_packet(ClientboundBlockChangedAck { seq: 1 }); + simulation.tick(); + assert_eq!(simulation.get_block_state(pos), Some(Block::Air.into())); +} diff --git a/azalea-protocol/src/packets/game/c_block_changed_ack.rs b/azalea-protocol/src/packets/game/c_block_changed_ack.rs index ebb303c0..c477c1f2 100644 --- a/azalea-protocol/src/packets/game/c_block_changed_ack.rs +++ b/azalea-protocol/src/packets/game/c_block_changed_ack.rs @@ -4,5 +4,5 @@ use azalea_protocol_macros::ClientboundGamePacket; #[derive(Clone, Debug, AzBuf, ClientboundGamePacket)] pub struct ClientboundBlockChangedAck { #[var] - pub sequence: i32, + pub seq: u32, } diff --git a/azalea-protocol/src/packets/game/s_player_action.rs b/azalea-protocol/src/packets/game/s_player_action.rs index 5b65b746..cffec2bd 100644 --- a/azalea-protocol/src/packets/game/s_player_action.rs +++ b/azalea-protocol/src/packets/game/s_player_action.rs @@ -1,6 +1,5 @@ use azalea_buf::AzBuf; -use azalea_core::direction::Direction; -use azalea_core::position::BlockPos; +use azalea_core::{direction::Direction, position::BlockPos}; use azalea_protocol_macros::ServerboundGamePacket; #[derive(Clone, Debug, AzBuf, ServerboundGamePacket)] @@ -9,7 +8,7 @@ pub struct ServerboundPlayerAction { pub pos: BlockPos, pub direction: Direction, #[var] - pub sequence: u32, + pub seq: u32, } #[derive(AzBuf, Clone, Copy, Debug)] diff --git a/azalea-protocol/src/packets/game/s_use_item.rs b/azalea-protocol/src/packets/game/s_use_item.rs index d6ccca87..3e9e1a2f 100644 --- a/azalea-protocol/src/packets/game/s_use_item.rs +++ b/azalea-protocol/src/packets/game/s_use_item.rs @@ -7,7 +7,7 @@ use crate::packets::game::s_interact::InteractionHand; pub struct ServerboundUseItem { pub hand: InteractionHand, #[var] - pub sequence: u32, + pub seq: u32, pub y_rot: f32, pub x_rot: f32, } diff --git a/azalea-protocol/src/packets/game/s_use_item_on.rs b/azalea-protocol/src/packets/game/s_use_item_on.rs index 11e32b2c..1c08112d 100644 --- a/azalea-protocol/src/packets/game/s_use_item_on.rs +++ b/azalea-protocol/src/packets/game/s_use_item_on.rs @@ -15,7 +15,7 @@ pub struct ServerboundUseItemOn { pub hand: InteractionHand, pub block_hit: BlockHit, #[var] - pub sequence: u32, + pub seq: u32, } #[derive(Clone, Debug)] diff --git a/azalea-world/src/palette/container.rs b/azalea-world/src/palette/container.rs index 2f3d9238..0ddac6c2 100644 --- a/azalea-world/src/palette/container.rs +++ b/azalea-world/src/palette/container.rs @@ -192,19 +192,10 @@ impl PalettedContainer { /// Sets the id at the given coordinates and return the previous id pub fn get_and_set(&mut self, pos: S::SectionPos, value: S) -> S { let paletted_value = self.id_for(value); - let block_state_id = self + let old_paletted_value = self .storage .get_and_set(self.index_from_coords(pos), paletted_value as u64); - // error in debug mode - #[cfg(debug_assertions)] - if block_state_id > BlockState::MAX_STATE.into() { - warn!( - "Old block state from get_and_set {block_state_id} was greater than max state {}", - BlockState::MAX_STATE - ); - } - - S::try_from(block_state_id as u32).unwrap_or_default() + self.palette.value_for(old_paletted_value as usize) } /// Sets the id at the given index and return the previous id. You probably diff --git a/azalea/src/pathfinder/simulation.rs b/azalea/src/pathfinder/simulation.rs index 337efda7..5c548aa2 100644 --- a/azalea/src/pathfinder/simulation.rs +++ b/azalea/src/pathfinder/simulation.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use azalea_client::{ - PhysicsState, interact::CurrentSequenceNumber, inventory::Inventory, + PhysicsState, interact::BlockStatePredictionHandler, inventory::Inventory, local_player::LocalGameMode, mining::MineBundle, packet::game::SendPacketEvent, }; use azalea_core::{ @@ -113,7 +113,7 @@ fn create_simulation_player_complete_bundle( Inventory::default(), LocalGameMode::from(GameMode::Survival), MineBundle::default(), - CurrentSequenceNumber::default(), + BlockStatePredictionHandler::default(), azalea_client::local_player::PermissionLevel::default(), azalea_client::local_player::PlayerAbilities::default(), ) From df092f25ec928a3ff6fb9c023d5cb66cd53d6196 Mon Sep 17 00:00:00 2001 From: mat Date: Wed, 11 Jun 2025 16:02:08 -0900 Subject: [PATCH 32/36] add pathfinder retry_on_no_path and rework GotoEvent constructor --- azalea/src/pathfinder/goto_event.rs | 84 ++++++ azalea/src/pathfinder/mod.rs | 396 +++------------------------- azalea/src/pathfinder/tests.rs | 311 ++++++++++++++++++++++ 3 files changed, 426 insertions(+), 365 deletions(-) create mode 100644 azalea/src/pathfinder/goto_event.rs create mode 100644 azalea/src/pathfinder/tests.rs diff --git a/azalea/src/pathfinder/goto_event.rs b/azalea/src/pathfinder/goto_event.rs new file mode 100644 index 00000000..dfd89122 --- /dev/null +++ b/azalea/src/pathfinder/goto_event.rs @@ -0,0 +1,84 @@ +use std::{sync::Arc, time::Duration}; + +use bevy_ecs::{entity::Entity, event::Event}; + +use crate::pathfinder::{ + astar::PathfinderTimeout, + goals::Goal, + moves::{self, SuccessorsFn}, +}; + +/// Send this event to start pathfinding to the given goal. +/// +/// Also see [`PathfinderClientExt::goto`]. +/// +/// This event is read by [`goto_listener`]. +#[derive(Event)] +#[non_exhaustive] +pub struct GotoEvent { + /// The local bot entity that will do the pathfinding and execute the path. + pub entity: Entity, + pub goal: Arc, + /// The function that's used for checking what moves are possible. Usually + /// [`moves::default_move`]. + pub successors_fn: SuccessorsFn, + + /// Whether the bot is allowed to break blocks while pathfinding. + pub allow_mining: bool, + + /// Whether we should recalculate the path when the pathfinder timed out and + /// there's no partial path to try. + /// + /// Should usually be set to true. + pub retry_on_no_path: bool, + + /// The minimum amount of time that should pass before the A* pathfinder + /// function can return a timeout. It may take up to [`Self::max_timeout`] + /// if it can't immediately find a usable path. + /// + /// A good default value for this is + /// `PathfinderTimeout::Time(Duration::from_secs(1))`. + /// + /// Also see [`PathfinderTimeout::Nodes`] + pub min_timeout: PathfinderTimeout, + /// The absolute maximum amount of time that the pathfinder function can + /// take to find a path. If it takes this long, it means no usable path was + /// found (so it might be impossible). + /// + /// A good default value for this is + /// `PathfinderTimeout::Time(Duration::from_secs(5))`. + pub max_timeout: PathfinderTimeout, +} +impl GotoEvent { + pub fn new(entity: Entity, goal: impl Goal + 'static) -> Self { + Self { + entity, + goal: Arc::new(goal), + successors_fn: moves::default_move, + allow_mining: true, + retry_on_no_path: true, + min_timeout: PathfinderTimeout::Time(Duration::from_secs(1)), + max_timeout: PathfinderTimeout::Time(Duration::from_secs(5)), + } + } + pub fn with_successors_fn(mut self, successors_fn: SuccessorsFn) -> Self { + self.successors_fn = successors_fn; + self + } + pub fn with_allow_mining(mut self, allow_mining: bool) -> Self { + self.allow_mining = allow_mining; + self + } + pub fn with_retry_on_no_path(mut self, retry_on_no_path: bool) -> Self { + self.retry_on_no_path = retry_on_no_path; + self + } + pub fn with_min_timeout(mut self, min_timeout: PathfinderTimeout) -> Self { + self.min_timeout = min_timeout; + self + } + pub fn with_max_timeout(mut self, max_timeout: PathfinderTimeout) -> Self { + self.max_timeout = max_timeout; + self + } +} diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index c72573f5..e75c99c4 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -7,10 +7,13 @@ pub mod costs; pub mod custom_state; pub mod debug; pub mod goals; +mod goto_event; pub mod mining; pub mod moves; pub mod rel_block_pos; pub mod simulation; +#[cfg(test)] +mod tests; pub mod world; use std::{ @@ -43,6 +46,7 @@ use bevy_tasks::{AsyncComputeTaskPool, Task}; use custom_state::{CustomPathfinderState, CustomPathfinderStateRef}; use futures_lite::future; use goals::BlockPosGoal; +pub use goto_event::GotoEvent; use parking_lot::RwLock; use rel_block_pos::RelBlockPos; use tokio::sync::broadcast::error::RecvError; @@ -112,11 +116,13 @@ impl Plugin for PathfinderPlugin { /// A component that makes this client able to pathfind. #[derive(Component, Default, Clone)] +#[non_exhaustive] pub struct Pathfinder { pub goal: Option>, pub successors_fn: Option, pub is_calculating: bool, pub allow_mining: bool, + pub retry_on_no_path: bool, pub min_timeout: Option, pub max_timeout: Option, @@ -135,41 +141,8 @@ pub struct ExecutingPath { pub is_path_partial: bool, } -/// Send this event to start pathfinding to the given goal. -/// -/// Also see [`PathfinderClientExt::goto`]. -/// -/// This event is read by [`goto_listener`]. -#[derive(Event)] -pub struct GotoEvent { - /// The local bot entity that will do the pathfinding and execute the path. - pub entity: Entity, - pub goal: Arc, - /// The function that's used for checking what moves are possible. Usually - /// [`moves::default_move`]. - pub successors_fn: SuccessorsFn, - - /// Whether the bot is allowed to break blocks while pathfinding. - pub allow_mining: bool, - - /// The minimum amount of time that should pass before the A* pathfinder - /// function can return a timeout. It may take up to [`Self::max_timeout`] - /// if it can't immediately find a usable path. - /// - /// A good default value for this is - /// `PathfinderTimeout::Time(Duration::from_secs(1))`. - /// - /// Also see [`PathfinderTimeout::Nodes`] - pub min_timeout: PathfinderTimeout, - /// The absolute maximum amount of time that the pathfinder function can - /// take to find a path. If it takes this long, it means no usable path was - /// found (so it might be impossible). - /// - /// A good default value for this is - /// `PathfinderTimeout::Time(Duration::from_secs(5))`. - pub max_timeout: PathfinderTimeout, -} #[derive(Event, Clone, Debug)] +#[non_exhaustive] pub struct PathFoundEvent { pub entity: Entity, pub start: BlockPos, @@ -226,27 +199,17 @@ impl PathfinderClientExt for azalea_client::Client { /// # } /// ``` fn start_goto(&self, goal: impl Goal + 'static) { - self.ecs.lock().send_event(GotoEvent { - entity: self.entity, - goal: Arc::new(goal), - successors_fn: moves::default_move, - allow_mining: true, - min_timeout: PathfinderTimeout::Time(Duration::from_secs(1)), - max_timeout: PathfinderTimeout::Time(Duration::from_secs(5)), - }); + self.ecs + .lock() + .send_event(GotoEvent::new(self.entity, goal)); } /// Same as [`start_goto`](Self::start_goto). but the bot won't break any /// blocks while executing the path. fn start_goto_without_mining(&self, goal: impl Goal + 'static) { - self.ecs.lock().send_event(GotoEvent { - entity: self.entity, - goal: Arc::new(goal), - successors_fn: moves::default_move, - allow_mining: false, - min_timeout: PathfinderTimeout::Time(Duration::from_secs(1)), - max_timeout: PathfinderTimeout::Time(Duration::from_secs(5)), - }); + self.ecs + .lock() + .send_event(GotoEvent::new(self.entity, goal).with_allow_mining(false)); } fn stop_pathfinding(&self) { @@ -359,6 +322,7 @@ pub fn goto_listener( let goto_id_atomic = pathfinder.goto_id.clone(); let allow_mining = event.allow_mining; + let retry_on_no_path = event.retry_on_no_path; let mining_cache = MiningCache::new(if allow_mining { Some(inventory.inventory_menu.clone()) } else { @@ -380,6 +344,7 @@ pub fn goto_listener( goto_id_atomic, allow_mining, mining_cache, + retry_on_no_path, custom_state, min_timeout, max_timeout, @@ -399,10 +364,14 @@ pub struct CalculatePathOpts { pub goto_id_atomic: Arc, pub allow_mining: bool, pub mining_cache: MiningCache, - pub custom_state: CustomPathfinderState, - /// Also see [`GotoEvent::min_timeout`]. + /// See [`GotoEvent::retry_on_no_path`]. + pub retry_on_no_path: bool, + + /// See [`GotoEvent::min_timeout`]. pub min_timeout: PathfinderTimeout, pub max_timeout: PathfinderTimeout, + + pub custom_state: CustomPathfinderState, } /// Calculate the [`PathFoundEvent`] for the given pathfinder options. @@ -616,6 +585,10 @@ pub fn path_found_listener( executing_path.is_path_partial = event.is_partial; } else if path.is_empty() { debug!("calculated path is empty, so didn't add ExecutingPath"); + if !pathfinder.retry_on_no_path { + debug!("retry_on_no_path is set to false, removing goal"); + pathfinder.goal = None; + } } else { commands.entity(event.entity).insert(ExecutingPath { path: path.to_owned(), @@ -938,8 +911,9 @@ fn patch_path( let goal = Arc::new(BlockPosGoal(patch_end)); let goto_id_atomic = pathfinder.goto_id.clone(); - let allow_mining = pathfinder.allow_mining; + let retry_on_no_path = pathfinder.retry_on_no_path; + let mining_cache = MiningCache::new(if allow_mining { Some(inventory.inventory_menu.clone()) } else { @@ -956,6 +930,8 @@ fn patch_path( goto_id_atomic, allow_mining, mining_cache, + retry_on_no_path, + custom_state, min_timeout: PathfinderTimeout::Nodes(10_000), max_timeout: PathfinderTimeout::Nodes(10_000), @@ -1030,6 +1006,7 @@ pub fn recalculate_near_end_of_path( goal, successors_fn, allow_mining: pathfinder.allow_mining, + retry_on_no_path: pathfinder.retry_on_no_path, min_timeout: if executing_path.path.len() == 50 { // we have quite some time until the node is reached, soooo we might as // well burn some cpu cycles to get a good path @@ -1141,6 +1118,7 @@ pub fn recalculate_if_has_goal_but_no_path( goal, successors_fn: pathfinder.successors_fn.unwrap(), allow_mining: pathfinder.allow_mining, + retry_on_no_path: pathfinder.retry_on_no_path, min_timeout: pathfinder.min_timeout.expect("min_timeout should be set"), max_timeout: pathfinder.max_timeout.expect("max_timeout should be set"), }); @@ -1267,315 +1245,3 @@ pub fn call_successors_fn( successors_fn(&mut ctx, pos); edges } - -#[cfg(test)] -mod tests { - use std::{ - collections::HashSet, - sync::Arc, - thread, - time::{Duration, Instant}, - }; - - use azalea_block::BlockState; - use azalea_core::position::{BlockPos, ChunkPos, Vec3}; - use azalea_world::{Chunk, ChunkStorage, PartialChunkStorage}; - - use super::{ - GotoEvent, - astar::PathfinderTimeout, - goals::BlockPosGoal, - moves, - simulation::{SimulatedPlayerBundle, Simulation}, - }; - - fn setup_blockposgoal_simulation( - partial_chunks: &mut PartialChunkStorage, - start_pos: BlockPos, - end_pos: BlockPos, - solid_blocks: &[BlockPos], - ) -> Simulation { - let mut simulation = setup_simulation_world(partial_chunks, start_pos, solid_blocks, &[]); - - // you can uncomment this while debugging tests to get trace logs - // simulation.app.add_plugins(bevy_log::LogPlugin { - // level: bevy_log::Level::TRACE, - // filter: "".to_string(), - // ..Default::default() - // }); - - simulation.app.world_mut().send_event(GotoEvent { - entity: simulation.entity, - goal: Arc::new(BlockPosGoal(end_pos)), - successors_fn: moves::default_move, - allow_mining: false, - min_timeout: PathfinderTimeout::Nodes(1_000_000), - max_timeout: PathfinderTimeout::Nodes(5_000_000), - }); - simulation - } - - fn setup_simulation_world( - partial_chunks: &mut PartialChunkStorage, - start_pos: BlockPos, - solid_blocks: &[BlockPos], - extra_blocks: &[(BlockPos, BlockState)], - ) -> Simulation { - let mut chunk_positions = HashSet::new(); - for block_pos in solid_blocks { - chunk_positions.insert(ChunkPos::from(block_pos)); - } - for (block_pos, _) in extra_blocks { - chunk_positions.insert(ChunkPos::from(block_pos)); - } - - let mut chunks = ChunkStorage::default(); - for chunk_pos in chunk_positions { - partial_chunks.set(&chunk_pos, Some(Chunk::default()), &mut chunks); - } - for block_pos in solid_blocks { - chunks.set_block_state(*block_pos, azalea_registry::Block::Stone.into()); - } - for (block_pos, block_state) in extra_blocks { - chunks.set_block_state(*block_pos, *block_state); - } - - let player = SimulatedPlayerBundle::new(Vec3::new( - start_pos.x as f64 + 0.5, - start_pos.y as f64, - start_pos.z as f64 + 0.5, - )); - Simulation::new(chunks, player) - } - - pub fn assert_simulation_reaches(simulation: &mut Simulation, ticks: usize, end_pos: BlockPos) { - wait_until_bot_starts_moving(simulation); - for _ in 0..ticks { - simulation.tick(); - } - assert_eq!(BlockPos::from(simulation.position()), end_pos); - } - - pub fn wait_until_bot_starts_moving(simulation: &mut Simulation) { - let start_pos = simulation.position(); - let start_time = Instant::now(); - while simulation.position() == start_pos - && !simulation.is_mining() - && start_time.elapsed() < Duration::from_millis(500) - { - simulation.tick(); - thread::yield_now(); - } - } - - #[test] - fn test_simple_forward() { - let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_blockposgoal_simulation( - &mut partial_chunks, - BlockPos::new(0, 71, 0), - BlockPos::new(0, 71, 1), - &[BlockPos::new(0, 70, 0), BlockPos::new(0, 70, 1)], - ); - assert_simulation_reaches(&mut simulation, 20, BlockPos::new(0, 71, 1)); - } - - #[test] - fn test_double_diagonal_with_walls() { - let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_blockposgoal_simulation( - &mut partial_chunks, - BlockPos::new(0, 71, 0), - BlockPos::new(2, 71, 2), - &[ - BlockPos::new(0, 70, 0), - BlockPos::new(1, 70, 1), - BlockPos::new(2, 70, 2), - BlockPos::new(1, 72, 0), - BlockPos::new(2, 72, 1), - ], - ); - assert_simulation_reaches(&mut simulation, 30, BlockPos::new(2, 71, 2)); - } - - #[test] - fn test_jump_with_sideways_momentum() { - let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_blockposgoal_simulation( - &mut partial_chunks, - BlockPos::new(0, 71, 3), - BlockPos::new(5, 76, 0), - &[ - BlockPos::new(0, 70, 3), - BlockPos::new(0, 70, 2), - BlockPos::new(0, 70, 1), - BlockPos::new(0, 70, 0), - BlockPos::new(1, 71, 0), - BlockPos::new(2, 72, 0), - BlockPos::new(3, 73, 0), - BlockPos::new(4, 74, 0), - BlockPos::new(5, 75, 0), - ], - ); - assert_simulation_reaches(&mut simulation, 120, BlockPos::new(5, 76, 0)); - } - - #[test] - fn test_parkour_2_block_gap() { - let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_blockposgoal_simulation( - &mut partial_chunks, - BlockPos::new(0, 71, 0), - BlockPos::new(0, 71, 3), - &[BlockPos::new(0, 70, 0), BlockPos::new(0, 70, 3)], - ); - assert_simulation_reaches(&mut simulation, 40, BlockPos::new(0, 71, 3)); - } - - #[test] - fn test_descend_and_parkour_2_block_gap() { - let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_blockposgoal_simulation( - &mut partial_chunks, - BlockPos::new(0, 71, 0), - BlockPos::new(3, 67, 4), - &[ - BlockPos::new(0, 70, 0), - BlockPos::new(0, 69, 1), - BlockPos::new(0, 68, 2), - BlockPos::new(0, 67, 3), - BlockPos::new(0, 66, 4), - BlockPos::new(3, 66, 4), - ], - ); - assert_simulation_reaches(&mut simulation, 100, BlockPos::new(3, 67, 4)); - } - - #[test] - fn test_small_descend_and_parkour_2_block_gap() { - let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_blockposgoal_simulation( - &mut partial_chunks, - BlockPos::new(0, 71, 0), - BlockPos::new(0, 70, 5), - &[ - BlockPos::new(0, 70, 0), - BlockPos::new(0, 70, 1), - BlockPos::new(0, 69, 2), - BlockPos::new(0, 69, 5), - ], - ); - assert_simulation_reaches(&mut simulation, 40, BlockPos::new(0, 70, 5)); - } - - #[test] - fn test_quickly_descend() { - let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_blockposgoal_simulation( - &mut partial_chunks, - BlockPos::new(0, 71, 0), - BlockPos::new(0, 68, 3), - &[ - BlockPos::new(0, 70, 0), - BlockPos::new(0, 69, 1), - BlockPos::new(0, 68, 2), - BlockPos::new(0, 67, 3), - ], - ); - assert_simulation_reaches(&mut simulation, 60, BlockPos::new(0, 68, 3)); - } - - #[test] - fn test_2_gap_ascend_thrice() { - let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_blockposgoal_simulation( - &mut partial_chunks, - BlockPos::new(0, 71, 0), - BlockPos::new(3, 74, 0), - &[ - BlockPos::new(0, 70, 0), - BlockPos::new(0, 71, 3), - BlockPos::new(3, 72, 3), - BlockPos::new(3, 73, 0), - ], - ); - assert_simulation_reaches(&mut simulation, 60, BlockPos::new(3, 74, 0)); - } - - #[test] - fn test_consecutive_3_gap_parkour() { - let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_blockposgoal_simulation( - &mut partial_chunks, - BlockPos::new(0, 71, 0), - BlockPos::new(4, 71, 12), - &[ - BlockPos::new(0, 70, 0), - BlockPos::new(0, 70, 4), - BlockPos::new(0, 70, 8), - BlockPos::new(0, 70, 12), - BlockPos::new(4, 70, 12), - ], - ); - assert_simulation_reaches(&mut simulation, 80, BlockPos::new(4, 71, 12)); - } - - #[test] - fn test_jumps_with_more_sideways_momentum() { - let mut partial_chunks = PartialChunkStorage::default(); - let mut simulation = setup_blockposgoal_simulation( - &mut partial_chunks, - BlockPos::new(0, 71, 0), - BlockPos::new(4, 74, 9), - &[ - BlockPos::new(0, 70, 0), - BlockPos::new(0, 70, 1), - BlockPos::new(0, 70, 2), - BlockPos::new(0, 71, 3), - BlockPos::new(0, 72, 6), - BlockPos::new(0, 73, 9), - // this is the point where the bot might fall if it has too much momentum - BlockPos::new(2, 73, 9), - BlockPos::new(4, 73, 9), - ], - ); - assert_simulation_reaches(&mut simulation, 80, BlockPos::new(4, 74, 9)); - } - - #[test] - fn test_mine_through_non_colliding_block() { - let mut partial_chunks = PartialChunkStorage::default(); - - let mut simulation = setup_simulation_world( - &mut partial_chunks, - // the pathfinder can't actually dig straight down, so we start a block to the side so - // it can descend correctly - BlockPos::new(0, 72, 1), - &[BlockPos::new(0, 71, 1)], - &[ - ( - BlockPos::new(0, 71, 0), - azalea_registry::Block::SculkVein.into(), - ), - ( - BlockPos::new(0, 70, 0), - azalea_registry::Block::GrassBlock.into(), - ), - // this is an extra check to make sure that we don't accidentally break the block - // below (since tnt will break instantly) - (BlockPos::new(0, 69, 0), azalea_registry::Block::Tnt.into()), - ], - ); - - simulation.app.world_mut().send_event(GotoEvent { - entity: simulation.entity, - goal: Arc::new(BlockPosGoal(BlockPos::new(0, 69, 0))), - successors_fn: moves::default_move, - allow_mining: true, - min_timeout: PathfinderTimeout::Nodes(1_000_000), - max_timeout: PathfinderTimeout::Nodes(5_000_000), - }); - - assert_simulation_reaches(&mut simulation, 200, BlockPos::new(0, 70, 0)); - } -} diff --git a/azalea/src/pathfinder/tests.rs b/azalea/src/pathfinder/tests.rs new file mode 100644 index 00000000..782899e8 --- /dev/null +++ b/azalea/src/pathfinder/tests.rs @@ -0,0 +1,311 @@ + +use std::{ + collections::HashSet, + sync::Arc, + thread, + time::{Duration, Instant}, +}; + +use azalea_block::BlockState; +use azalea_core::position::{BlockPos, ChunkPos, Vec3}; +use azalea_world::{Chunk, ChunkStorage, PartialChunkStorage}; + +use super::{ + GotoEvent, + astar::PathfinderTimeout, + goals::BlockPosGoal, + moves, + simulation::{SimulatedPlayerBundle, Simulation}, +}; + +fn setup_blockposgoal_simulation( + partial_chunks: &mut PartialChunkStorage, + start_pos: BlockPos, + end_pos: BlockPos, + solid_blocks: &[BlockPos], +) -> Simulation { + let mut simulation = setup_simulation_world(partial_chunks, start_pos, solid_blocks, &[]); + + // you can uncomment this while debugging tests to get trace logs + // simulation.app.add_plugins(bevy_log::LogPlugin { + // level: bevy_log::Level::TRACE, + // filter: "".to_string(), + // ..Default::default() + // }); + + simulation.app.world_mut().send_event(GotoEvent { + entity: simulation.entity, + goal: Arc::new(BlockPosGoal(end_pos)), + successors_fn: moves::default_move, + allow_mining: false, + retry_on_no_path: true, + min_timeout: PathfinderTimeout::Nodes(1_000_000), + max_timeout: PathfinderTimeout::Nodes(5_000_000), + }); + simulation +} + +fn setup_simulation_world( + partial_chunks: &mut PartialChunkStorage, + start_pos: BlockPos, + solid_blocks: &[BlockPos], + extra_blocks: &[(BlockPos, BlockState)], +) -> Simulation { + let mut chunk_positions = HashSet::new(); + for block_pos in solid_blocks { + chunk_positions.insert(ChunkPos::from(block_pos)); + } + for (block_pos, _) in extra_blocks { + chunk_positions.insert(ChunkPos::from(block_pos)); + } + + let mut chunks = ChunkStorage::default(); + for chunk_pos in chunk_positions { + partial_chunks.set(&chunk_pos, Some(Chunk::default()), &mut chunks); + } + for block_pos in solid_blocks { + chunks.set_block_state(*block_pos, azalea_registry::Block::Stone.into()); + } + for (block_pos, block_state) in extra_blocks { + chunks.set_block_state(*block_pos, *block_state); + } + + let player = SimulatedPlayerBundle::new(Vec3::new( + start_pos.x as f64 + 0.5, + start_pos.y as f64, + start_pos.z as f64 + 0.5, + )); + Simulation::new(chunks, player) +} + +pub fn assert_simulation_reaches(simulation: &mut Simulation, ticks: usize, end_pos: BlockPos) { + wait_until_bot_starts_moving(simulation); + for _ in 0..ticks { + simulation.tick(); + } + assert_eq!(BlockPos::from(simulation.position()), end_pos); +} + +pub fn wait_until_bot_starts_moving(simulation: &mut Simulation) { + let start_pos = simulation.position(); + let start_time = Instant::now(); + while simulation.position() == start_pos + && !simulation.is_mining() + && start_time.elapsed() < Duration::from_millis(500) + { + simulation.tick(); + thread::yield_now(); + } +} + +#[test] +fn test_simple_forward() { + let mut partial_chunks = PartialChunkStorage::default(); + let mut simulation = setup_blockposgoal_simulation( + &mut partial_chunks, + BlockPos::new(0, 71, 0), + BlockPos::new(0, 71, 1), + &[BlockPos::new(0, 70, 0), BlockPos::new(0, 70, 1)], + ); + assert_simulation_reaches(&mut simulation, 20, BlockPos::new(0, 71, 1)); +} + +#[test] +fn test_double_diagonal_with_walls() { + let mut partial_chunks = PartialChunkStorage::default(); + let mut simulation = setup_blockposgoal_simulation( + &mut partial_chunks, + BlockPos::new(0, 71, 0), + BlockPos::new(2, 71, 2), + &[ + BlockPos::new(0, 70, 0), + BlockPos::new(1, 70, 1), + BlockPos::new(2, 70, 2), + BlockPos::new(1, 72, 0), + BlockPos::new(2, 72, 1), + ], + ); + assert_simulation_reaches(&mut simulation, 30, BlockPos::new(2, 71, 2)); +} + +#[test] +fn test_jump_with_sideways_momentum() { + let mut partial_chunks = PartialChunkStorage::default(); + let mut simulation = setup_blockposgoal_simulation( + &mut partial_chunks, + BlockPos::new(0, 71, 3), + BlockPos::new(5, 76, 0), + &[ + BlockPos::new(0, 70, 3), + BlockPos::new(0, 70, 2), + BlockPos::new(0, 70, 1), + BlockPos::new(0, 70, 0), + BlockPos::new(1, 71, 0), + BlockPos::new(2, 72, 0), + BlockPos::new(3, 73, 0), + BlockPos::new(4, 74, 0), + BlockPos::new(5, 75, 0), + ], + ); + assert_simulation_reaches(&mut simulation, 120, BlockPos::new(5, 76, 0)); +} + +#[test] +fn test_parkour_2_block_gap() { + let mut partial_chunks = PartialChunkStorage::default(); + let mut simulation = setup_blockposgoal_simulation( + &mut partial_chunks, + BlockPos::new(0, 71, 0), + BlockPos::new(0, 71, 3), + &[BlockPos::new(0, 70, 0), BlockPos::new(0, 70, 3)], + ); + assert_simulation_reaches(&mut simulation, 40, BlockPos::new(0, 71, 3)); +} + +#[test] +fn test_descend_and_parkour_2_block_gap() { + let mut partial_chunks = PartialChunkStorage::default(); + let mut simulation = setup_blockposgoal_simulation( + &mut partial_chunks, + BlockPos::new(0, 71, 0), + BlockPos::new(3, 67, 4), + &[ + BlockPos::new(0, 70, 0), + BlockPos::new(0, 69, 1), + BlockPos::new(0, 68, 2), + BlockPos::new(0, 67, 3), + BlockPos::new(0, 66, 4), + BlockPos::new(3, 66, 4), + ], + ); + assert_simulation_reaches(&mut simulation, 100, BlockPos::new(3, 67, 4)); +} + +#[test] +fn test_small_descend_and_parkour_2_block_gap() { + let mut partial_chunks = PartialChunkStorage::default(); + let mut simulation = setup_blockposgoal_simulation( + &mut partial_chunks, + BlockPos::new(0, 71, 0), + BlockPos::new(0, 70, 5), + &[ + BlockPos::new(0, 70, 0), + BlockPos::new(0, 70, 1), + BlockPos::new(0, 69, 2), + BlockPos::new(0, 69, 5), + ], + ); + assert_simulation_reaches(&mut simulation, 40, BlockPos::new(0, 70, 5)); +} + +#[test] +fn test_quickly_descend() { + let mut partial_chunks = PartialChunkStorage::default(); + let mut simulation = setup_blockposgoal_simulation( + &mut partial_chunks, + BlockPos::new(0, 71, 0), + BlockPos::new(0, 68, 3), + &[ + BlockPos::new(0, 70, 0), + BlockPos::new(0, 69, 1), + BlockPos::new(0, 68, 2), + BlockPos::new(0, 67, 3), + ], + ); + assert_simulation_reaches(&mut simulation, 60, BlockPos::new(0, 68, 3)); +} + +#[test] +fn test_2_gap_ascend_thrice() { + let mut partial_chunks = PartialChunkStorage::default(); + let mut simulation = setup_blockposgoal_simulation( + &mut partial_chunks, + BlockPos::new(0, 71, 0), + BlockPos::new(3, 74, 0), + &[ + BlockPos::new(0, 70, 0), + BlockPos::new(0, 71, 3), + BlockPos::new(3, 72, 3), + BlockPos::new(3, 73, 0), + ], + ); + assert_simulation_reaches(&mut simulation, 60, BlockPos::new(3, 74, 0)); +} + +#[test] +fn test_consecutive_3_gap_parkour() { + let mut partial_chunks = PartialChunkStorage::default(); + let mut simulation = setup_blockposgoal_simulation( + &mut partial_chunks, + BlockPos::new(0, 71, 0), + BlockPos::new(4, 71, 12), + &[ + BlockPos::new(0, 70, 0), + BlockPos::new(0, 70, 4), + BlockPos::new(0, 70, 8), + BlockPos::new(0, 70, 12), + BlockPos::new(4, 70, 12), + ], + ); + assert_simulation_reaches(&mut simulation, 80, BlockPos::new(4, 71, 12)); +} + +#[test] +fn test_jumps_with_more_sideways_momentum() { + let mut partial_chunks = PartialChunkStorage::default(); + let mut simulation = setup_blockposgoal_simulation( + &mut partial_chunks, + BlockPos::new(0, 71, 0), + BlockPos::new(4, 74, 9), + &[ + BlockPos::new(0, 70, 0), + BlockPos::new(0, 70, 1), + BlockPos::new(0, 70, 2), + BlockPos::new(0, 71, 3), + BlockPos::new(0, 72, 6), + BlockPos::new(0, 73, 9), + // this is the point where the bot might fall if it has too much momentum + BlockPos::new(2, 73, 9), + BlockPos::new(4, 73, 9), + ], + ); + assert_simulation_reaches(&mut simulation, 80, BlockPos::new(4, 74, 9)); +} + +#[test] +fn test_mine_through_non_colliding_block() { + let mut partial_chunks = PartialChunkStorage::default(); + + let mut simulation = setup_simulation_world( + &mut partial_chunks, + // the pathfinder can't actually dig straight down, so we start a block to the side so + // it can descend correctly + BlockPos::new(0, 72, 1), + &[BlockPos::new(0, 71, 1)], + &[ + ( + BlockPos::new(0, 71, 0), + azalea_registry::Block::SculkVein.into(), + ), + ( + BlockPos::new(0, 70, 0), + azalea_registry::Block::GrassBlock.into(), + ), + // this is an extra check to make sure that we don't accidentally break the block + // below (since tnt will break instantly) + (BlockPos::new(0, 69, 0), azalea_registry::Block::Tnt.into()), + ], + ); + + simulation.app.world_mut().send_event(GotoEvent { + entity: simulation.entity, + goal: Arc::new(BlockPosGoal(BlockPos::new(0, 69, 0))), + successors_fn: moves::default_move, + allow_mining: true, + retry_on_no_path: true, + min_timeout: PathfinderTimeout::Nodes(1_000_000), + max_timeout: PathfinderTimeout::Nodes(5_000_000), + }); + + assert_simulation_reaches(&mut simulation, 200, BlockPos::new(0, 70, 0)); +} From 89ddd5e85f4f2fb98697df15528df6e07a3ddd07 Mon Sep 17 00:00:00 2001 From: mat Date: Wed, 11 Jun 2025 15:50:45 -1245 Subject: [PATCH 33/36] fix bench --- azalea/benches/pathfinder.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azalea/benches/pathfinder.rs b/azalea/benches/pathfinder.rs index cddaee2c..48736cef 100644 --- a/azalea/benches/pathfinder.rs +++ b/azalea/benches/pathfinder.rs @@ -61,14 +61,14 @@ fn generate_bedrock_world( let mut start = BlockPos::new(-64, 4, -64); // move start down until it's on a solid block - while chunks.get_block_state(&start).unwrap().is_air() { + while chunks.get_block_state(start).unwrap().is_air() { start = start.down(1); } start = start.up(1); let mut end = BlockPos::new(63, 4, 63); // move end down until it's on a solid block - while chunks.get_block_state(&end).unwrap().is_air() { + while chunks.get_block_state(end).unwrap().is_air() { end = end.down(1); } end = end.up(1); From a2606569bb79867d07a075bcf7b05730e4264d72 Mon Sep 17 00:00:00 2001 From: mat Date: Wed, 11 Jun 2025 22:58:41 -0630 Subject: [PATCH 34/36] use owned instead of borrowed Vec3 more --- azalea-block/src/fluid_state.rs | 2 +- azalea-client/src/entity_query.rs | 2 +- azalea-client/src/plugins/interact.rs | 14 +- azalea-client/src/plugins/packet/game/mod.rs | 4 +- azalea-core/src/aabb.rs | 166 ++++++++----------- azalea-core/src/position.rs | 57 +++---- azalea-entity/src/dimensions.rs | 2 +- azalea-entity/src/lib.rs | 26 +-- azalea-entity/src/plugin/mod.rs | 2 +- azalea-physics/src/clip.rs | 28 ++-- azalea-physics/src/collision/mod.rs | 77 ++++----- azalea-physics/src/collision/shape.rs | 16 +- azalea-physics/src/fluids.rs | 4 +- azalea-physics/src/lib.rs | 73 ++++---- azalea-physics/src/travel.rs | 28 ++-- azalea/examples/testbot/killaura.rs | 2 +- azalea/src/bot.rs | 4 +- azalea/src/nearest_entity.rs | 10 +- azalea/src/pathfinder/debug.rs | 2 +- azalea/src/pathfinder/goals.rs | 10 +- azalea/src/pathfinder/mod.rs | 2 +- 21 files changed, 230 insertions(+), 301 deletions(-) diff --git a/azalea-block/src/fluid_state.rs b/azalea-block/src/fluid_state.rs index edceac05..cfddcc7f 100644 --- a/azalea-block/src/fluid_state.rs +++ b/azalea-block/src/fluid_state.rs @@ -20,7 +20,7 @@ pub struct FluidState { /// set (see FlowingFluid.getFlowing) pub falling: bool, } -#[derive(Default, Clone, Debug, PartialEq, Eq)] +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)] pub enum FluidKind { #[default] Empty, diff --git a/azalea-client/src/entity_query.rs b/azalea-client/src/entity_query.rs index 5b19bb24..ee99d0f4 100644 --- a/azalea-client/src/entity_query.rs +++ b/azalea-client/src/entity_query.rs @@ -162,7 +162,7 @@ where entities.sort_by_cached_key(|(_, position)| { // to_bits is fine here as long as the number is positive - position.distance_squared_to(&nearest_to).to_bits() + position.distance_squared_to(nearest_to).to_bits() }); entities diff --git a/azalea-client/src/plugins/interact.rs b/azalea-client/src/plugins/interact.rs index 31b5acf4..d91597d6 100644 --- a/azalea-client/src/plugins/interact.rs +++ b/azalea-client/src/plugins/interact.rs @@ -363,7 +363,7 @@ pub fn update_hit_result_component( }; let instance = instance_lock.read(); - let hit_result = pick(look_direction, &eye_position, &instance.chunks, pick_range); + 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 { @@ -384,8 +384,8 @@ pub fn update_hit_result_component( /// /// TODO: does not currently check for entities pub fn pick( - look_direction: &LookDirection, - eye_position: &Vec3, + look_direction: LookDirection, + eye_position: Vec3, chunks: &azalea_world::ChunkStorage, pick_range: f64, ) -> HitResult { @@ -400,18 +400,18 @@ pub fn pick( /// /// Also see [`pick`]. pub fn pick_block( - look_direction: &LookDirection, - eye_position: &Vec3, + 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); + let end_position = eye_position + (view_vector * pick_range); azalea_physics::clip::clip( chunks, ClipContext { - from: *eye_position, + from: eye_position, to: end_position, block_shape_type: BlockShapeType::Outline, fluid_pick_type: FluidPickType::None, diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs index b2a4abc4..e1477d21 100644 --- a/azalea-client/src/plugins/packet/game/mod.rs +++ b/azalea-client/src/plugins/packet/game/mod.rs @@ -428,7 +428,7 @@ impl GamePacketHandler<'_> { p.relative .apply(&p.change, &mut position, &mut direction, &mut physics); // old_pos is set to the current position when we're teleported - physics.set_old_pos(&position); + physics.set_old_pos(*position); // send the relevant packets commands.trigger(SendPacketEvent::new( @@ -828,7 +828,7 @@ impl GamePacketHandler<'_> { &mut physics, ); // old_pos is set to the current position when we're teleported - physics.set_old_pos(&old_position); + physics.set_old_pos(old_position); }); }, )); diff --git a/azalea-core/src/aabb.rs b/azalea-core/src/aabb.rs index 03a754f0..09abf047 100644 --- a/azalea-core/src/aabb.rs +++ b/azalea-core/src/aabb.rs @@ -15,14 +15,16 @@ pub struct AABB { pub struct ClipPointOpts<'a> { pub t: &'a mut f64, pub approach_dir: Option, - pub delta: &'a Vec3, + pub delta: Vec3, pub begin: f64, + pub min_x: f64, pub min_z: f64, pub max_x: f64, pub max_z: f64, + pub result_dir: Direction, - pub start: &'a Vec3, + pub start: Vec3, } impl AABB { @@ -51,85 +53,51 @@ impl AABB { AABB { min, max } } - pub fn expand_towards(&self, other: &Vec3) -> AABB { - let mut min_x = self.min.x; - let mut min_y = self.min.y; - let mut min_z = self.min.z; - - let mut max_x = self.max.x; - let mut max_y = self.max.y; - let mut max_z = self.max.z; + pub fn expand_towards(&self, other: Vec3) -> AABB { + let mut min = self.min; + let mut max = self.max; if other.x < 0.0 { - min_x += other.x; + min.x += other.x; } else if other.x > 0.0 { - max_x += other.x; + max.x += other.x; } if other.y < 0.0 { - min_y += other.y; + min.y += other.y; } else if other.y > 0.0 { - max_y += other.y; + max.y += other.y; } if other.z < 0.0 { - min_z += other.z; + min.z += other.z; } else if other.z > 0.0 { - max_z += other.z; + max.z += other.z; } - AABB { - min: Vec3::new(min_x, min_y, min_z), - max: Vec3::new(max_x, max_y, max_z), - } + AABB { min, max } } pub fn inflate(&self, amount: Vec3) -> AABB { - let min_x = self.min.x - amount.x; - let min_y = self.min.y - amount.y; - let min_z = self.min.z - amount.z; + let min = self.min - amount; + let max = self.max + amount; - let max_x = self.max.x + amount.x; - let max_y = self.max.y + amount.y; - let max_z = self.max.z + amount.z; - - AABB { - min: Vec3::new(min_x, min_y, min_z), - max: Vec3::new(max_x, max_y, max_z), - } + AABB { min, max } } pub fn inflate_all(&self, amount: f64) -> AABB { self.inflate(Vec3::new(amount, amount, amount)) } pub fn intersect(&self, other: &AABB) -> AABB { - let min_x = self.min.x.max(other.min.x); - let min_y = self.min.y.max(other.min.y); - let min_z = self.min.z.max(other.min.z); - - let max_x = self.max.x.min(other.max.x); - let max_y = self.max.y.min(other.max.y); - let max_z = self.max.z.min(other.max.z); - - AABB { - min: Vec3::new(min_x, min_y, min_z), - max: Vec3::new(max_x, max_y, max_z), - } + let min = self.min.max(other.min); + let max = self.max.min(other.max); + AABB { min, max } } pub fn minmax(&self, other: &AABB) -> AABB { - let min_x = self.min.x.min(other.min.x); - let min_y = self.min.y.min(other.min.y); - let min_z = self.min.z.min(other.min.z); - - let max_x = self.max.x.max(other.max.x); - let max_y = self.max.y.max(other.max.y); - let max_z = self.max.z.max(other.max.z); - - AABB { - min: Vec3::new(min_x, min_y, min_z), - max: Vec3::new(max_x, max_y, max_z), - } + let min = self.min.min(other.min); + let max = self.max.max(other.max); + AABB { min, max } } pub fn move_relative(&self, delta: Vec3) -> AABB { @@ -147,22 +115,13 @@ impl AABB { && self.min.z < other.max.z && self.max.z > other.min.z } - pub fn intersects_vec3(&self, corner1: &Vec3, corner2: &Vec3) -> bool { - self.intersects_aabb(&AABB { - min: Vec3::new( - corner1.x.min(corner2.x), - corner1.y.min(corner2.y), - corner1.z.min(corner2.z), - ), - max: Vec3::new( - corner1.x.max(corner2.x), - corner1.y.max(corner2.y), - corner1.z.max(corner2.z), - ), - }) + pub fn intersects_vec3(&self, corner1: Vec3, corner2: Vec3) -> bool { + let min = corner1.min(corner2); + let max = corner1.max(corner2); + self.intersects_aabb(&AABB { min, max }) } - pub fn contains(&self, point: &Vec3) -> bool { + pub fn contains(&self, point: Vec3) -> bool { point.x >= self.min.x && point.x < self.max.x && point.y >= self.min.y @@ -178,6 +137,7 @@ impl AABB { (x + y + z) / 3.0 } + #[inline] pub fn get_size(&self, axis: Axis) -> f64 { axis.choose( self.max.x - self.min.x, @@ -193,24 +153,24 @@ impl AABB { self.deflate(Vec3::new(amount, amount, amount)) } - pub fn clip(&self, min: &Vec3, max: &Vec3) -> Option { + pub fn clip(&self, min: Vec3, max: Vec3) -> Option { let mut t = 1.0; let delta = max - min; - let _dir = Self::get_direction_aabb(self, min, &mut t, None, &delta)?; - Some(min + &(delta * t)) + let _dir = Self::get_direction_aabb(self, min, &mut t, None, delta)?; + Some(min + (delta * t)) } - pub fn clip_with_from_and_to(min: &Vec3, max: &Vec3, from: &Vec3, to: &Vec3) -> Option { + pub fn clip_with_from_and_to(min: Vec3, max: Vec3, from: Vec3, to: Vec3) -> Option { let mut t = 1.0; let delta = to - from; - let _dir = Self::get_direction(min, max, from, &mut t, None, &delta)?; - Some(from + &(delta * t)) + let _dir = Self::get_direction(min, max, from, &mut t, None, delta)?; + Some(from + (delta * t)) } pub fn clip_iterable( - boxes: &Vec, - from: &Vec3, - to: &Vec3, + boxes: &[AABB], + from: Vec3, + to: Vec3, pos: BlockPos, ) -> Option { let mut t = 1.0; @@ -223,12 +183,12 @@ impl AABB { from, &mut t, dir, - &delta, + delta, ); } let dir = dir?; Some(BlockHitResult { - location: from + &(delta * t), + location: from + (delta * t), direction: dir, block_pos: pos, inside: false, @@ -239,32 +199,34 @@ impl AABB { fn get_direction_aabb( &self, - from: &Vec3, + from: Vec3, t: &mut f64, dir: Option, - delta: &Vec3, + delta: Vec3, ) -> Option { - AABB::get_direction(&self.min, &self.max, from, t, dir, delta) + AABB::get_direction(self.min, self.max, from, t, dir, delta) } fn get_direction( - min: &Vec3, - max: &Vec3, - from: &Vec3, + min: Vec3, + max: Vec3, + from: Vec3, t: &mut f64, mut dir: Option, - delta: &Vec3, + delta: Vec3, ) -> Option { if delta.x > EPSILON { dir = Self::clip_point(ClipPointOpts { t, approach_dir: dir, delta, + begin: min.x, min_x: min.y, max_x: max.y, min_z: min.z, max_z: max.z, + result_dir: Direction::West, start: from, }); @@ -273,11 +235,13 @@ impl AABB { t, approach_dir: dir, delta, + begin: max.x, min_x: min.y, max_x: max.y, min_z: min.z, max_z: max.z, + result_dir: Direction::East, start: from, }); @@ -287,7 +251,7 @@ impl AABB { dir = Self::clip_point(ClipPointOpts { t, approach_dir: dir, - delta: &Vec3 { + delta: Vec3 { x: delta.y, y: delta.z, z: delta.x, @@ -298,7 +262,7 @@ impl AABB { min_z: min.x, max_z: max.x, result_dir: Direction::Down, - start: &Vec3 { + start: Vec3 { x: from.y, y: from.z, z: from.x, @@ -308,7 +272,7 @@ impl AABB { dir = Self::clip_point(ClipPointOpts { t, approach_dir: dir, - delta: &Vec3 { + delta: Vec3 { x: delta.y, y: delta.z, z: delta.x, @@ -319,7 +283,7 @@ impl AABB { min_z: min.x, max_z: max.x, result_dir: Direction::Up, - start: &Vec3 { + start: Vec3 { x: from.y, y: from.z, z: from.x, @@ -331,7 +295,7 @@ impl AABB { dir = Self::clip_point(ClipPointOpts { t, approach_dir: dir, - delta: &Vec3 { + delta: Vec3 { x: delta.z, y: delta.x, z: delta.y, @@ -342,7 +306,7 @@ impl AABB { min_z: min.y, max_z: max.y, result_dir: Direction::North, - start: &Vec3 { + start: Vec3 { x: from.z, y: from.x, z: from.y, @@ -352,7 +316,7 @@ impl AABB { dir = Self::clip_point(ClipPointOpts { t, approach_dir: dir, - delta: &Vec3 { + delta: Vec3 { x: delta.z, y: delta.x, z: delta.y, @@ -363,7 +327,7 @@ impl AABB { min_z: min.y, max_z: max.y, result_dir: Direction::South, - start: &Vec3 { + start: Vec3 { x: from.z, y: from.x, z: from.y, @@ -431,7 +395,7 @@ impl AABB { axis.choose(self.min.x, self.min.y, self.min.z) } - pub fn collided_along_vector(&self, vector: Vec3, boxes: &Vec) -> bool { + pub fn collided_along_vector(&self, vector: Vec3, boxes: &[AABB]) -> bool { let center = self.get_center(); let new_center = center + vector; @@ -441,11 +405,11 @@ impl AABB { self.get_size(Axis::Y) * 0.5, self.get_size(Axis::Z) * 0.5, )); - if inflated.contains(&new_center) || inflated.contains(¢er) { + if inflated.contains(new_center) || inflated.contains(center) { return true; } - if inflated.clip(¢er, &new_center).is_some() { + if inflated.clip(center, new_center).is_some() { return true; } } @@ -494,12 +458,12 @@ mod tests { fn test_aabb_clip_iterable() { assert_ne!( AABB::clip_iterable( - &vec![AABB { + &[AABB { min: Vec3::new(0., 0., 0.), max: Vec3::new(1., 1., 1.), }], - &Vec3::new(-1., -1., -1.), - &Vec3::new(1., 1., 1.), + Vec3::new(-1., -1., -1.), + Vec3::new(1., 1., 1.), BlockPos::new(0, 0, 0), ), None diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs index d6e67dc2..5a8d3e0c 100644 --- a/azalea-core/src/position.rs +++ b/azalea-core/src/position.rs @@ -34,7 +34,7 @@ macro_rules! vec3_impl { /// Get the squared distance from this position to another position. /// Equivalent to `(self - other).length_squared()`. #[inline] - pub fn distance_squared_to(&self, other: &Self) -> $type { + pub fn distance_squared_to(self, other: Self) -> $type { (self - other).length_squared() } @@ -44,7 +44,7 @@ macro_rules! vec3_impl { } #[inline] - pub fn horizontal_distance_squared_to(&self, other: &Self) -> $type { + pub fn horizontal_distance_squared_to(self, other: Self) -> $type { (self - other).horizontal_distance_squared() } @@ -115,6 +115,23 @@ macro_rules! vec3_impl { self.x * other.x + self.y * other.y + self.z * other.z } + /// Make a new position with the lower coordinates for each axis. + pub fn min(&self, other: Self) -> Self { + Self { + x: self.x.min(other.x), + y: self.x.min(other.y), + z: self.x.min(other.z), + } + } + /// Make a new position with the higher coordinates for each axis. + pub fn max(&self, other: Self) -> Self { + Self { + x: self.x.max(other.x), + y: self.x.max(other.y), + z: self.x.max(other.z), + } + } + /// Replace the Y with 0. #[inline] pub fn xz(&self) -> Self { @@ -298,7 +315,7 @@ impl Vec3 { /// Get the distance from this position to another position. /// Equivalent to `(self - other).length()`. - pub fn distance_to(&self, other: &Self) -> f64 { + pub fn distance_to(self, other: Self) -> f64 { (self - other).length() } @@ -382,40 +399,6 @@ impl BlockPos { (self.x.abs() + self.y.abs() + self.z.abs()) as u32 } - /// Make a new BlockPos with the lower coordinates for each axis. - /// - /// ``` - /// # use azalea_core::position::BlockPos; - /// assert_eq!( - /// BlockPos::min(&BlockPos::new(1, 20, 300), &BlockPos::new(50, 40, 30),), - /// BlockPos::new(1, 20, 30), - /// ); - /// ``` - pub fn min(&self, other: &Self) -> Self { - Self { - x: self.x.min(other.x), - y: self.y.min(other.y), - z: self.z.min(other.z), - } - } - - /// Make a new BlockPos with the higher coordinates for each axis. - /// - /// ``` - /// # use azalea_core::position::BlockPos; - /// assert_eq!( - /// BlockPos::max(&BlockPos::new(1, 20, 300), &BlockPos::new(50, 40, 30),), - /// BlockPos::new(50, 40, 300), - /// ); - /// ``` - pub fn max(&self, other: &Self) -> Self { - Self { - x: self.x.max(other.x), - y: self.y.max(other.y), - z: self.z.max(other.z), - } - } - pub fn offset_with_direction(self, direction: Direction) -> Self { self + direction.normal() } diff --git a/azalea-entity/src/dimensions.rs b/azalea-entity/src/dimensions.rs index 8d9a1eae..8770fa94 100644 --- a/azalea-entity/src/dimensions.rs +++ b/azalea-entity/src/dimensions.rs @@ -12,7 +12,7 @@ impl EntityDimensions { Self { width, height } } - pub fn make_bounding_box(&self, pos: &Vec3) -> AABB { + pub fn make_bounding_box(&self, pos: Vec3) -> AABB { let radius = (self.width / 2.0) as f64; let height = self.height as f64; AABB { diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs index 0058708f..cf2222d4 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -39,15 +39,15 @@ pub use crate::plugin::*; pub fn move_relative( physics: &mut Physics, - direction: &LookDirection, + direction: LookDirection, speed: f32, - acceleration: &Vec3, + acceleration: Vec3, ) { let input_vector = input_vector(direction, speed, acceleration); physics.velocity += input_vector; } -pub fn input_vector(direction: &LookDirection, speed: f32, acceleration: &Vec3) -> Vec3 { +pub fn input_vector(direction: LookDirection, speed: f32, acceleration: Vec3) -> Vec3 { let distance = acceleration.length_squared(); if distance < 1.0E-7 { return Vec3::ZERO; @@ -55,7 +55,7 @@ pub fn input_vector(direction: &LookDirection, speed: f32, acceleration: &Vec3) let acceleration = if distance > 1.0 { acceleration.normalize() } else { - *acceleration + acceleration } .scale(speed as f64); let y_rot = math::sin(direction.y_rot * 0.017453292f32); @@ -67,7 +67,7 @@ pub fn input_vector(direction: &LookDirection, speed: f32, acceleration: &Vec3) } } -pub fn view_vector(look_direction: &LookDirection) -> Vec3 { +pub fn view_vector(look_direction: LookDirection) -> Vec3 { let x_rot = look_direction.x_rot * 0.017453292; let y_rot = -look_direction.y_rot * 0.017453292; let y_rot_cos = math::cos(y_rot); @@ -82,7 +82,7 @@ pub fn view_vector(look_direction: &LookDirection) -> Vec3 { } /// Get the position of the block below the entity, but a little lower. -pub fn on_pos_legacy(chunk_storage: &ChunkStorage, position: &Position) -> BlockPos { +pub fn on_pos_legacy(chunk_storage: &ChunkStorage, position: Position) -> BlockPos { on_pos(0.2, chunk_storage, position) } @@ -98,7 +98,7 @@ pub fn on_pos_legacy(chunk_storage: &ChunkStorage, position: &Position) -> Block // } // } // return var5; -pub fn on_pos(offset: f32, chunk_storage: &ChunkStorage, pos: &Position) -> BlockPos { +pub fn on_pos(offset: f32, chunk_storage: &ChunkStorage, pos: Position) -> BlockPos { let x = pos.x.floor() as i32; let y = (pos.y - offset as f64).floor() as i32; let z = pos.z.floor() as i32; @@ -323,7 +323,7 @@ impl Physics { no_jump_delay: 0, - bounding_box: dimensions.make_bounding_box(&pos), + bounding_box: dimensions.make_bounding_box(pos), dimensions, has_impulse: false, @@ -375,8 +375,8 @@ impl Physics { self.lava_fluid_height > 0. } - pub fn set_old_pos(&mut self, pos: &Position) { - self.old_position = **pos; + pub fn set_old_pos(&mut self, pos: Position) { + self.old_position = *pos; } } @@ -496,10 +496,10 @@ impl EntityBundle { /// If this is for a client then all of our clients will have this. /// /// This component is not removed from clients when they disconnect. -#[derive(Component, Clone, Debug, Default)] +#[derive(Component, Clone, Copy, Debug, Default)] pub struct LocalEntity; -#[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)] +#[derive(Component, Clone, Copy, Debug, PartialEq, Deref, DerefMut)] pub struct FluidOnEyes(FluidKind); impl FluidOnEyes { @@ -508,5 +508,5 @@ impl FluidOnEyes { } } -#[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)] +#[derive(Component, Clone, Copy, Debug, PartialEq, Deref, DerefMut)] pub struct OnClimbable(bool); diff --git a/azalea-entity/src/plugin/mod.rs b/azalea-entity/src/plugin/mod.rs index e64f9823..65b28a59 100644 --- a/azalea-entity/src/plugin/mod.rs +++ b/azalea-entity/src/plugin/mod.rs @@ -199,7 +199,7 @@ pub fn clamp_look_direction(mut query: Query<&mut LookDirection>) { /// Cached position in the world must be updated. pub fn update_bounding_box(mut query: Query<(&Position, &mut Physics), Changed>) { for (position, mut physics) in query.iter_mut() { - let bounding_box = physics.dimensions.make_bounding_box(position); + let bounding_box = physics.dimensions.make_bounding_box(**position); physics.bounding_box = bounding_box; } } diff --git a/azalea-physics/src/clip.rs b/azalea-physics/src/clip.rs index 2cef15c4..8d2b5dd1 100644 --- a/azalea-physics/src/clip.rs +++ b/azalea-physics/src/clip.rs @@ -101,22 +101,22 @@ pub fn clip(chunk_storage: &ChunkStorage, context: ClipContext) -> BlockHitResul let block_shape = ctx.block_shape(block_state); let interaction_clip = clip_with_interaction_override( - &ctx.from, - &ctx.to, + ctx.from, + ctx.to, block_pos, block_shape, - &block_state, + block_state, ); let fluid_shape = ctx.fluid_shape(fluid_state, chunk_storage, block_pos); - let fluid_clip = fluid_shape.clip(&ctx.from, &ctx.to, block_pos); + let fluid_clip = fluid_shape.clip(ctx.from, ctx.to, block_pos); let distance_to_interaction = interaction_clip .as_ref() - .map(|hit| ctx.from.distance_squared_to(&hit.location)) + .map(|hit| ctx.from.distance_squared_to(hit.location)) .unwrap_or(f64::MAX); let distance_to_fluid = fluid_clip .as_ref() - .map(|hit| ctx.from.distance_squared_to(&hit.location)) + .map(|hit| ctx.from.distance_squared_to(hit.location)) .unwrap_or(f64::MAX); if distance_to_interaction <= distance_to_fluid { @@ -137,11 +137,11 @@ pub fn clip(chunk_storage: &ChunkStorage, context: ClipContext) -> BlockHitResul } fn clip_with_interaction_override( - from: &Vec3, - to: &Vec3, + from: Vec3, + to: Vec3, block_pos: BlockPos, block_shape: &VoxelShape, - _block_state: &BlockState, + _block_state: BlockState, ) -> Option { let block_hit_result = block_shape.clip(from, to, block_pos); @@ -255,7 +255,7 @@ pub fn traverse_blocks( } } -pub fn box_traverse_blocks(from: &Vec3, to: &Vec3, aabb: &AABB) -> HashSet { +pub fn box_traverse_blocks(from: Vec3, to: Vec3, aabb: &AABB) -> HashSet { let delta = to - from; let traversed_blocks = BlockPos::between_closed_aabb(aabb); if delta.length_squared() < (0.99999_f32 * 0.99999) as f64 { @@ -346,10 +346,10 @@ pub fn add_collisions_along_travel( step_count += 1; let Some(clip_location) = AABB::clip_with_from_and_to( - &Vec3::new(min_x as f64, min_y as f64, min_z as f64), - &Vec3::new((min_x + 1) as f64, (min_y + 1) as f64, (min_z + 1) as f64), - &from, - &to, + Vec3::new(min_x as f64, min_y as f64, min_z as f64), + Vec3::new((min_x + 1) as f64, (min_y + 1) as f64, (min_z + 1) as f64), + from, + to, ) else { continue; }; diff --git a/azalea-physics/src/collision/mod.rs b/azalea-physics/src/collision/mod.rs index ef994deb..41fc6c85 100644 --- a/azalea-physics/src/collision/mod.rs +++ b/azalea-physics/src/collision/mod.rs @@ -35,7 +35,7 @@ pub enum MoverType { // Entity.collide fn collide( - movement: &Vec3, + movement: Vec3, world: &Instance, physics: &azalea_entity::Physics, source_entity: Option, @@ -51,7 +51,7 @@ fn collide( collidable_entity_query, ); let collided_delta = if movement.length_squared() == 0.0 { - *movement + movement } else { collide_bounding_box(movement, &entity_bounding_box, world, &entity_collisions) }; @@ -65,20 +65,20 @@ fn collide( let max_up_step = 0.6; if max_up_step > 0. && on_ground && (x_collision || z_collision) { let mut step_to_delta = collide_bounding_box( - &movement.with_y(max_up_step), + movement.with_y(max_up_step), &entity_bounding_box, world, &entity_collisions, ); let directly_up_delta = collide_bounding_box( - &Vec3::ZERO.with_y(max_up_step), - &entity_bounding_box.expand_towards(&Vec3::new(movement.x, 0., movement.z)), + Vec3::ZERO.with_y(max_up_step), + &entity_bounding_box.expand_towards(Vec3::new(movement.x, 0., movement.z)), world, &entity_collisions, ); if directly_up_delta.y < max_up_step { let target_movement = collide_bounding_box( - &movement.with_y(0.), + movement.with_y(0.), &entity_bounding_box.move_relative(directly_up_delta), world, &entity_collisions, @@ -95,7 +95,7 @@ fn collide( > collided_delta.horizontal_distance_squared() { return step_to_delta.add(collide_bounding_box( - &Vec3::ZERO.with_y(-step_to_delta.y + movement.y), + Vec3::ZERO.with_y(-step_to_delta.y + movement.y), &entity_bounding_box.move_relative(step_to_delta), world, &entity_collisions, @@ -112,7 +112,7 @@ fn collide( #[allow(clippy::too_many_arguments)] pub fn move_colliding( _mover_type: MoverType, - movement: &Vec3, + movement: Vec3, world: &Instance, position: &mut Mut, physics: &mut azalea_entity::Physics, @@ -180,7 +180,7 @@ pub fn move_colliding( // TODO: minecraft checks for a "minor" horizontal collision here - let _block_pos_below = azalea_entity::on_pos_legacy(&world.chunks, position); + let _block_pos_below = azalea_entity::on_pos_legacy(&world.chunks, **position); // let _block_state_below = self // .world // .get_block_state(&block_pos_below) @@ -239,7 +239,7 @@ pub fn move_colliding( } fn collide_bounding_box( - movement: &Vec3, + movement: Vec3, entity_bounding_box: &AABB, world: &Instance, entity_collisions: &[VoxelShape], @@ -259,63 +259,44 @@ fn collide_bounding_box( } fn collide_with_shapes( - movement: &Vec3, + mut movement: Vec3, mut entity_box: AABB, - collision_boxes: &Vec, + collision_boxes: &[VoxelShape], ) -> Vec3 { if collision_boxes.is_empty() { - return *movement; + return movement; } - let mut x_movement = movement.x; - let mut y_movement = movement.y; - let mut z_movement = movement.z; - if y_movement != 0. { - y_movement = Shapes::collide(&Axis::Y, &entity_box, collision_boxes, y_movement); - if y_movement != 0. { - entity_box = entity_box.move_relative(Vec3 { - x: 0., - y: y_movement, - z: 0., - }); + if movement.y != 0. { + movement.y = Shapes::collide(Axis::Y, &entity_box, collision_boxes, movement.y); + if movement.y != 0. { + entity_box = entity_box.move_relative(Vec3::new(0., movement.y, 0.)); } } // whether the player is moving more in the z axis than x // this is done to fix a movement bug, minecraft does this too - let more_z_movement = x_movement.abs() < z_movement.abs(); + let more_z_movement = movement.x.abs() < movement.z.abs(); - if more_z_movement && z_movement != 0. { - z_movement = Shapes::collide(&Axis::Z, &entity_box, collision_boxes, z_movement); - if z_movement != 0. { - entity_box = entity_box.move_relative(Vec3 { - x: 0., - y: 0., - z: z_movement, - }); + if more_z_movement && movement.z != 0. { + movement.z = Shapes::collide(Axis::Z, &entity_box, collision_boxes, movement.z); + if movement.z != 0. { + entity_box = entity_box.move_relative(Vec3::new(0., 0., movement.z)); } } - if x_movement != 0. { - x_movement = Shapes::collide(&Axis::X, &entity_box, collision_boxes, x_movement); - if x_movement != 0. { - entity_box = entity_box.move_relative(Vec3 { - x: x_movement, - y: 0., - z: 0., - }); + if movement.x != 0. { + movement.x = Shapes::collide(Axis::X, &entity_box, collision_boxes, movement.x); + if movement.x != 0. { + entity_box = entity_box.move_relative(Vec3::new(movement.x, 0., 0.)); } } - if !more_z_movement && z_movement != 0. { - z_movement = Shapes::collide(&Axis::Z, &entity_box, collision_boxes, z_movement); + if !more_z_movement && movement.z != 0. { + movement.z = Shapes::collide(Axis::Z, &entity_box, collision_boxes, movement.z); } - Vec3 { - x: x_movement, - y: y_movement, - z: z_movement, - } + movement } /// Get the [`VoxelShape`] for the given fluid state. diff --git a/azalea-physics/src/collision/shape.rs b/azalea-physics/src/collision/shape.rs index 4d430ee7..9caae590 100644 --- a/azalea-physics/src/collision/shape.rs +++ b/azalea-physics/src/collision/shape.rs @@ -98,9 +98,9 @@ impl Shapes { } pub fn collide( - axis: &Axis, + axis: Axis, entity_box: &AABB, - collision_boxes: &Vec, + collision_boxes: &[VoxelShape], mut movement: f64, ) -> f64 { for shape in collision_boxes { @@ -408,7 +408,7 @@ impl VoxelShape { } } - pub fn clip(&self, from: &Vec3, to: &Vec3, block_pos: BlockPos) -> Option { + pub fn clip(&self, from: Vec3, to: Vec3, block_pos: BlockPos) -> Option { if self.is_empty() { return None; } @@ -416,7 +416,7 @@ impl VoxelShape { if vector.length_squared() < EPSILON { return None; } - let right_after_start = from + &(vector * 0.001); + let right_after_start = from + (vector * 0.001); if self.shape().is_full_wide( self.find_index(Axis::X, right_after_start.x - block_pos.x as f64), @@ -436,8 +436,8 @@ impl VoxelShape { } } - pub fn collide(&self, axis: &Axis, entity_box: &AABB, movement: f64) -> f64 { - self.collide_x(AxisCycle::between(*axis, Axis::X), entity_box, movement) + pub fn collide(&self, axis: Axis, entity_box: &AABB, movement: f64) -> f64 { + self.collide_x(AxisCycle::between(axis, Axis::X), entity_box, movement) } pub fn collide_x(&self, axis_cycle: AxisCycle, entity_box: &AABB, mut movement: f64) -> f64 { if self.shape().is_empty() { @@ -753,8 +753,8 @@ mod tests { let block_shape = &*BLOCK_SHAPE; let block_hit_result = block_shape .clip( - &Vec3::new(-0.3, 0.5, 0.), - &Vec3::new(5.3, 0.5, 0.), + Vec3::new(-0.3, 0.5, 0.), + Vec3::new(5.3, 0.5, 0.), BlockPos::new(0, 0, 0), ) .unwrap(); diff --git a/azalea-physics/src/fluids.rs b/azalea-physics/src/fluids.rs index 5ea6194a..ea0f12e2 100644 --- a/azalea-physics/src/fluids.rs +++ b/azalea-physics/src/fluids.rs @@ -32,7 +32,7 @@ pub fn update_in_water_state_and_do_fluid_pushing( physics.water_fluid_height = 0.; physics.lava_fluid_height = 0.; - update_in_water_state_and_do_water_current_pushing(&mut physics, &world, position); + update_in_water_state_and_do_water_current_pushing(&mut physics, &world, *position); // right now doing registries.dimension_type() clones the entire registry which // is very inefficient, so for now we're doing this instead @@ -63,7 +63,7 @@ pub fn update_in_water_state_and_do_fluid_pushing( fn update_in_water_state_and_do_water_current_pushing( physics: &mut Physics, world: &Instance, - _position: &Position, + _position: Position, ) { // TODO: implement vehicles and boats // if vehicle == AbstractBoat { diff --git a/azalea-physics/src/lib.rs b/azalea-physics/src/lib.rs index 1f381174..d0db3c22 100644 --- a/azalea-physics/src/lib.rs +++ b/azalea-physics/src/lib.rs @@ -18,6 +18,7 @@ use azalea_entity::{ Attributes, InLoadedChunk, Jumping, LocalEntity, LookDirection, OnClimbable, Physics, Pose, Position, metadata::Sprinting, move_relative, }; +use azalea_registry::Block; use azalea_world::{Instance, InstanceContainer, InstanceName}; use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; @@ -110,9 +111,9 @@ pub fn ai_step( { jump_from_ground( &mut physics, - position, - look_direction, - sprinting, + *position, + *look_direction, + *sprinting, instance_name, &instance_container, ); @@ -187,11 +188,11 @@ fn check_inside_blocks( for movement in movements { let bounding_box_at_target = physics .dimensions - .make_bounding_box(&movement.to) + .make_bounding_box(movement.to) .deflate_all(1.0E-5); for traversed_block in - box_traverse_blocks(&movement.from, &movement.to, &bounding_box_at_target) + box_traverse_blocks(movement.from, movement.to, &bounding_box_at_target) { // if (!this.isAlive()) { // return; @@ -221,8 +222,8 @@ fn check_inside_blocks( if entity_inside_collision_shape != &*BLOCK_SHAPE && !collided_with_shape_moving_from( - &movement.from, - &movement.to, + movement.from, + movement.to, traversed_block, entity_inside_collision_shape, physics, @@ -241,8 +242,8 @@ fn check_inside_blocks( } fn collided_with_shape_moving_from( - from: &Vec3, - to: &Vec3, + from: Vec3, + to: Vec3, traversed_block: BlockPos, entity_inside_collision_shape: &VoxelShape, physics: &Physics, @@ -304,9 +305,9 @@ pub struct EntityMovement { pub fn jump_from_ground( physics: &mut Physics, - position: &Position, - look_direction: &LookDirection, - sprinting: &Sprinting, + position: Position, + look_direction: LookDirection, + sprinting: Sprinting, instance_name: &InstanceName, instance_container: &InstanceContainer, ) { @@ -322,7 +323,7 @@ pub fn jump_from_ground( y: jump_power, z: old_delta_movement.z, }; - if **sprinting { + if *sprinting { // sprint jumping gives some extra velocity let y_rot = look_direction.y_rot * 0.017453292; physics.velocity += Vec3 { @@ -337,11 +338,11 @@ pub fn jump_from_ground( pub fn update_old_position(mut query: Query<(&mut Physics, &Position)>) { for (mut physics, position) in &mut query { - physics.set_old_pos(position); + physics.set_old_pos(*position); } } -fn get_block_pos_below_that_affects_movement(position: &Position) -> BlockPos { +fn get_block_pos_below_that_affects_movement(position: Position) -> BlockPos { BlockPos::new( position.x.floor() as i32, // TODO: this uses bounding_box.min_y instead of position.y @@ -355,13 +356,13 @@ struct HandleRelativeFrictionAndCalculateMovementOpts<'a, 'b, 'world, 'state> { block_friction: f32, world: &'a Instance, physics: &'a mut Physics, - direction: &'a LookDirection, + direction: LookDirection, position: Mut<'a, Position>, attributes: &'a Attributes, is_sprinting: bool, - on_climbable: &'a OnClimbable, - pose: Option<&'a Pose>, - jumping: &'a Jumping, + on_climbable: OnClimbable, + pose: Option, + jumping: Jumping, entity: Entity, physics_query: &'a PhysicsQuery<'world, 'state, 'b>, collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>, @@ -387,18 +388,18 @@ fn handle_relative_friction_and_calculate_movement( physics, direction, get_friction_influenced_speed(physics, attributes, block_friction, is_sprinting), - &Vec3 { - x: physics.x_acceleration as f64, - y: physics.y_acceleration as f64, - z: physics.z_acceleration as f64, - }, + Vec3::new( + physics.x_acceleration as f64, + physics.y_acceleration as f64, + physics.z_acceleration as f64, + ), ); - physics.velocity = handle_on_climbable(physics.velocity, on_climbable, &position, world, pose); + physics.velocity = handle_on_climbable(physics.velocity, on_climbable, *position, world, pose); move_colliding( MoverType::Own, - &physics.velocity.clone(), + physics.velocity, world, &mut position, physics, @@ -414,14 +415,14 @@ fn handle_relative_friction_and_calculate_movement( // PowderSnowBlock.canEntityWalkOnPowderSnow(entity))) { var3 = new // Vec3(var3.x, 0.2D, var3.z); } - if physics.horizontal_collision || **jumping { - let block_at_feet: azalea_registry::Block = world + if physics.horizontal_collision || *jumping { + let block_at_feet: Block = world .chunks .get_block_state((*position).into()) .unwrap_or_default() .into(); - if **on_climbable || block_at_feet == azalea_registry::Block::PowderSnow { + if *on_climbable || block_at_feet == Block::PowderSnow { physics.velocity.y = 0.2; } } @@ -431,12 +432,12 @@ fn handle_relative_friction_and_calculate_movement( fn handle_on_climbable( velocity: Vec3, - on_climbable: &OnClimbable, - position: &Position, + on_climbable: OnClimbable, + position: Position, world: &Instance, - pose: Option<&Pose>, + pose: Option, ) -> Vec3 { - if !**on_climbable { + if !*on_climbable { return velocity; } @@ -450,7 +451,7 @@ fn handle_on_climbable( // sneaking on ladders/vines if y < 0.0 - && pose.copied() == Some(Pose::Sneaking) + && pose == Some(Pose::Sneaking) && azalea_registry::Block::from( world .chunks @@ -485,7 +486,7 @@ fn get_friction_influenced_speed( /// Returns the what the entity's jump should be multiplied by based on the /// block they're standing on. -fn block_jump_factor(world: &Instance, position: &Position) -> f32 { +fn block_jump_factor(world: &Instance, position: Position) -> f32 { let block_at_pos = world.chunks.get_block_state(position.into()); let block_below = world .chunks @@ -513,7 +514,7 @@ fn block_jump_factor(world: &Instance, position: &Position) -> f32 { // public double getJumpBoostPower() { // return this.hasEffect(MobEffects.JUMP) ? (double)(0.1F * // (float)(this.getEffect(MobEffects.JUMP).getAmplifier() + 1)) : 0.0D; } -fn jump_power(world: &Instance, position: &Position) -> f32 { +fn jump_power(world: &Instance, position: Position) -> f32 { 0.42 * block_jump_factor(world, position) } diff --git a/azalea-physics/src/travel.rs b/azalea-physics/src/travel.rs index a442f629..dba991d5 100644 --- a/azalea-physics/src/travel.rs +++ b/azalea-physics/src/travel.rs @@ -73,7 +73,7 @@ pub fn travel( &world, entity, &mut physics, - &direction, + *direction, position, attributes, sprinting, @@ -86,13 +86,13 @@ pub fn travel( &world, entity, &mut physics, - &direction, + *direction, position, attributes, sprinting, - on_climbable, + *on_climbable, pose, - jumping, + *jumping, &physics_query, &collidable_entity_query, ); @@ -106,19 +106,19 @@ fn travel_in_air( world: &Instance, entity: Entity, physics: &mut Physics, - direction: &LookDirection, + direction: LookDirection, position: Mut, attributes: &Attributes, sprinting: Sprinting, - on_climbable: &OnClimbable, + on_climbable: OnClimbable, pose: Option<&Pose>, - jumping: &Jumping, + jumping: Jumping, physics_query: &PhysicsQuery, collidable_entity_query: &CollidableEntityQuery, ) { let gravity = get_effective_gravity(); - let block_pos_below = get_block_pos_below_that_affects_movement(&position); + let block_pos_below = get_block_pos_below_that_affects_movement(*position); let block_state_below = world .chunks @@ -144,7 +144,7 @@ fn travel_in_air( attributes, is_sprinting: *sprinting, on_climbable, - pose, + pose: pose.copied(), jumping, entity, physics_query, @@ -177,7 +177,7 @@ fn travel_in_fluid( world: &Instance, entity: Entity, physics: &mut Physics, - direction: &LookDirection, + direction: LookDirection, mut position: Mut, attributes: &Attributes, sprinting: Sprinting, @@ -212,10 +212,10 @@ fn travel_in_fluid( // waterMovementSpeed = 0.96F; // } - move_relative(physics, direction, speed, &acceleration); + move_relative(physics, direction, speed, acceleration); move_colliding( MoverType::Own, - &physics.velocity.clone(), + physics.velocity.clone(), world, &mut position, physics, @@ -236,10 +236,10 @@ fn travel_in_fluid( physics.velocity = get_fluid_falling_adjusted_movement(gravity, moving_down, new_velocity, sprinting); } else { - move_relative(physics, direction, 0.02, &acceleration); + move_relative(physics, direction, 0.02, acceleration); move_colliding( MoverType::Own, - &physics.velocity.clone(), + physics.velocity, world, &mut position, physics, diff --git a/azalea/examples/testbot/killaura.rs b/azalea/examples/testbot/killaura.rs index 5458ea4e..586d90b2 100644 --- a/azalea/examples/testbot/killaura.rs +++ b/azalea/examples/testbot/killaura.rs @@ -31,7 +31,7 @@ pub fn tick(bot: Client, state: State) -> anyhow::Result<()> { continue; } - let distance = bot_position.distance_to(position); + let distance = bot_position.distance_to(**position); if distance < 4. && distance < nearest_distance { nearest_entity = Some(entity_id); nearest_distance = distance; diff --git a/azalea/src/bot.rs b/azalea/src/bot.rs index 9e8566bf..745f3fdb 100644 --- a/azalea/src/bot.rs +++ b/azalea/src/bot.rs @@ -229,7 +229,7 @@ fn look_at_listener( for event in events.read() { if let Ok((position, eye_height, mut look_direction)) = query.get_mut(event.entity) { let new_look_direction = - direction_looking_at(&position.up(eye_height.into()), &event.position); + direction_looking_at(position.up(eye_height.into()), event.position); trace!("look at {} (currently at {})", event.position, **position); *look_direction = new_look_direction; } @@ -238,7 +238,7 @@ fn look_at_listener( /// Return the look direction that would make a client at `current` be /// looking at `target`. -pub fn direction_looking_at(current: &Vec3, target: &Vec3) -> LookDirection { +pub fn direction_looking_at(current: Vec3, target: Vec3) -> LookDirection { // borrowed from mineflayer's Bot.lookAt because i didn't want to do math let delta = target - current; let y_rot = (PI - f64::atan2(-delta.x, -delta.z)) * (180.0 / PI); diff --git a/azalea/src/nearest_entity.rs b/azalea/src/nearest_entity.rs index 7ac4fff0..df54273a 100644 --- a/azalea/src/nearest_entity.rs +++ b/azalea/src/nearest_entity.rs @@ -69,7 +69,7 @@ where /// multiple entities are within range, only the closest one is returned. pub fn nearest_to_position( &'a self, - position: &Position, + position: Position, instance_name: &InstanceName, max_distance: f64, ) -> Option { @@ -81,7 +81,7 @@ where continue; } - let target_distance = position.distance_to(e_pos); + let target_distance = position.distance_to(**e_pos); if target_distance < min_distance { nearest_entity = Some(target_entity); min_distance = target_distance; @@ -111,7 +111,7 @@ where continue; } - let target_distance = position.distance_to(e_pos); + let target_distance = position.distance_to(**e_pos); if target_distance < min_distance { nearest_entity = Some(target_entity); min_distance = target_distance; @@ -140,7 +140,7 @@ where return None; } - let distance = position.distance_to(e_pos); + let distance = position.distance_to(**e_pos); if distance < max_distance { Some((target_entity, distance)) } else { @@ -181,7 +181,7 @@ where return None; } - let distance = position.distance_to(e_pos); + let distance = position.distance_to(**e_pos); if distance < max_distance { Some((target_entity, distance)) } else { diff --git a/azalea/src/pathfinder/debug.rs b/azalea/src/pathfinder/debug.rs index 6b319531..d0d264d3 100644 --- a/azalea/src/pathfinder/debug.rs +++ b/azalea/src/pathfinder/debug.rs @@ -65,7 +65,7 @@ pub fn debug_render_path_with_particles( let start_vec3 = start.center(); let end_vec3 = end.center(); - let step_count = (start_vec3.distance_squared_to(&end_vec3).sqrt() * 4.0) as usize; + let step_count = (start_vec3.distance_to(end_vec3) * 4.0) as usize; let target_block_state = chunks.get_block_state(movement.target).unwrap_or_default(); let above_target_block_state = chunks diff --git a/azalea/src/pathfinder/goals.rs b/azalea/src/pathfinder/goals.rs index c19bf504..95786561 100644 --- a/azalea/src/pathfinder/goals.rs +++ b/azalea/src/pathfinder/goals.rs @@ -230,16 +230,16 @@ impl Goal for ReachBlockPosGoal { } // only do the expensive check if we're close enough - let distance = self.pos.distance_squared_to(&n); - if distance > self.max_check_distance.pow(2) { + let distance_squared = self.pos.distance_squared_to(n); + if distance_squared > self.max_check_distance.pow(2) { return false; } let eye_position = n.center_bottom().up(1.62); - let look_direction = crate::direction_looking_at(&eye_position, &self.pos.center()); + let look_direction = crate::direction_looking_at(eye_position, self.pos.center()); let block_hit_result = azalea_client::interact::pick_block( - &look_direction, - &eye_position, + look_direction, + eye_position, &self.chunk_storage, self.distance, ); diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index e75c99c4..9786e1de 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -641,7 +641,7 @@ pub fn timeout_movement( // don't timeout if we're mining if let Some(mining) = mining { // also make sure we're close enough to the block that's being mined - if mining.pos.distance_squared_to(&BlockPos::from(position)) < 6_i32.pow(2) { + if mining.pos.distance_squared_to(position.into()) < 6_i32.pow(2) { // also reset the last_node_reached_at so we don't timeout after we finish // mining executing_path.last_node_reached_at = Instant::now(); From 5e81d85d7e8eeca1b6c86ea028353d7c55361961 Mon Sep 17 00:00:00 2001 From: mat Date: Sat, 14 Jun 2025 20:33:22 -1030 Subject: [PATCH 35/36] add note about current_thread to azalea readme --- azalea-client/src/client.rs | 2 +- azalea-client/src/plugins/interact.rs | 2 +- azalea-physics/src/travel.rs | 2 +- azalea/README.md | 6 +++++- azalea/examples/echo.rs | 2 +- azalea/examples/nearest_entity.rs | 2 +- azalea/examples/steal.rs | 2 +- azalea/examples/testbot/main.rs | 4 +--- azalea/examples/todo/craft_dig_straight_down.rs | 5 ++--- azalea/examples/todo/mine_a_chunk.rs | 2 +- azalea/examples/todo/pvp.rs | 10 +++++----- azalea/src/lib.rs | 2 +- azalea/src/swarm/mod.rs | 2 +- 13 files changed, 22 insertions(+), 21 deletions(-) diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index d9fec523..c9cc5259 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -153,7 +153,7 @@ impl Client { /// ```rust,no_run /// use azalea_client::{Account, Client}; /// - /// #[tokio::main] + /// #[tokio::main(flavor = "current_thread")] /// async fn main() -> Result<(), Box> { /// let account = Account::offline("bot"); /// let (client, rx) = Client::join(account, "localhost").await?; diff --git a/azalea-client/src/plugins/interact.rs b/azalea-client/src/plugins/interact.rs index d91597d6..da1fa78e 100644 --- a/azalea-client/src/plugins/interact.rs +++ b/azalea-client/src/plugins/interact.rs @@ -144,7 +144,7 @@ impl BlockStatePredictionHandler { .or_insert(ServerVerifiedState { seq: self.seq, block_state: old_state, - player_pos: player_pos, + player_pos, }); } diff --git a/azalea-physics/src/travel.rs b/azalea-physics/src/travel.rs index dba991d5..9af3ed27 100644 --- a/azalea-physics/src/travel.rs +++ b/azalea-physics/src/travel.rs @@ -215,7 +215,7 @@ fn travel_in_fluid( move_relative(physics, direction, speed, acceleration); move_colliding( MoverType::Own, - physics.velocity.clone(), + physics.velocity, world, &mut position, physics, diff --git a/azalea/README.md b/azalea/README.md index 26a06dde..4f7506d1 100644 --- a/azalea/README.md +++ b/azalea/README.md @@ -46,7 +46,7 @@ use std::sync::Arc; use azalea::prelude::*; use parking_lot::Mutex; -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() { let account = Account::offline("bot"); // or Account::microsoft("example@example.com").await.unwrap(); @@ -110,5 +110,9 @@ If your code is simply hanging, it might be a deadlock. Enable `parking_lot`'s ` Backtraces are also useful, though they're sometimes hard to read and don't always contain the actual location of the error. Run your code with `RUST_BACKTRACE=1` to enable full backtraces. If it's very long, often searching for the keyword "azalea" will help you filter out unrelated things and find the actual source of the issue. +# Using a single-threaded Tokio runtime + +Due to the fact that Azalea clients store the ECS in a Mutex that's frequently locked and unlocked, bots that rely on the `Client` or `Swarm` types may run into race condition bugs (like out-of-order events and ticks happening at suboptimal moments) if they do not set Tokio to use a single thread with `#[tokio::main(flavor = "current_thread")]`. This may change in a future version of Azalea. Setting this option will usually not result in a performance hit, and Azalea internally will keep using multiple threads for running the ECS itself (because Tokio is not used for this). + [`azalea_client`]: https://docs.rs/azalea-client [`bevy_log`]: https://docs.rs/bevy_log diff --git a/azalea/examples/echo.rs b/azalea/examples/echo.rs index 80b0cb15..09c3d5d3 100644 --- a/azalea/examples/echo.rs +++ b/azalea/examples/echo.rs @@ -2,7 +2,7 @@ use azalea::prelude::*; -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() { let account = Account::offline("bot"); // or let account = Account::microsoft("email").await.unwrap(); diff --git a/azalea/examples/nearest_entity.rs b/azalea/examples/nearest_entity.rs index 2e6973cf..8774829e 100644 --- a/azalea/examples/nearest_entity.rs +++ b/azalea/examples/nearest_entity.rs @@ -12,7 +12,7 @@ use bevy_ecs::{ system::Query, }; -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() { let account = Account::offline("bot"); diff --git a/azalea/examples/steal.rs b/azalea/examples/steal.rs index 899c2568..87a1561b 100644 --- a/azalea/examples/steal.rs +++ b/azalea/examples/steal.rs @@ -6,7 +6,7 @@ use azalea::{BlockPos, pathfinder::goals::RadiusGoal, prelude::*}; use azalea_inventory::{ItemStack, operations::QuickMoveClick}; use parking_lot::Mutex; -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() { let account = Account::offline("bot"); // or let bot = Account::microsoft("email").await.unwrap(); diff --git a/azalea/examples/testbot/main.rs b/azalea/examples/testbot/main.rs index 8a35a281..8fb96411 100644 --- a/azalea/examples/testbot/main.rs +++ b/azalea/examples/testbot/main.rs @@ -20,8 +20,6 @@ //! only have this on if the bot has operator permissions, otherwise it'll //! just spam the server console unnecessarily. -#![feature(trivial_bounds)] - mod commands; pub mod killaura; @@ -34,7 +32,7 @@ use azalea::{ use commands::{CommandSource, register_commands}; use parking_lot::Mutex; -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() { let args = parse_args(); diff --git a/azalea/examples/todo/craft_dig_straight_down.rs b/azalea/examples/todo/craft_dig_straight_down.rs index 0dc8e16d..bf312331 100644 --- a/azalea/examples/todo/craft_dig_straight_down.rs +++ b/azalea/examples/todo/craft_dig_straight_down.rs @@ -1,7 +1,6 @@ use std::sync::Arc; -use azalea::pathfinder; -use azalea::prelude::*; +use azalea::{pathfinder, prelude::*}; use parking_lot::Mutex; #[derive(Default, Clone, Component)] @@ -9,7 +8,7 @@ struct State { pub started: Arc>, } -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() { let account = Account::offline("bot"); // or let bot = Account::microsoft("email").await; diff --git a/azalea/examples/todo/mine_a_chunk.rs b/azalea/examples/todo/mine_a_chunk.rs index 0c439f26..eb7fafd4 100644 --- a/azalea/examples/todo/mine_a_chunk.rs +++ b/azalea/examples/todo/mine_a_chunk.rs @@ -1,6 +1,6 @@ use azalea::{prelude::*, swarm::prelude::*}; -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() { let mut accounts = Vec::new(); let mut states = Vec::new(); diff --git a/azalea/examples/todo/pvp.rs b/azalea/examples/todo/pvp.rs index fb5a768d..0639d86b 100644 --- a/azalea/examples/todo/pvp.rs +++ b/azalea/examples/todo/pvp.rs @@ -1,11 +1,11 @@ use std::time::Duration; -use azalea::ecs::query::With; -use azalea::entity::metadata::Player; -use azalea::{pathfinder, Account, Client, Event, GameProfileComponent}; -use azalea::{prelude::*, swarm::prelude::*}; +use azalea::{ + Account, Client, Event, GameProfileComponent, ecs::query::With, entity::metadata::Player, + pathfinder, prelude::*, swarm::prelude::*, +}; -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() { let mut accounts = Vec::new(); let mut states = Vec::new(); diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index b3e8a7d9..cdf0afbf 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -59,7 +59,7 @@ pub enum StartError { /// /// ```no_run /// # use azalea::prelude::*; -/// # #[tokio::main] +/// # #[tokio::main(flavor = "current_thread")] /// # async fn main() { /// ClientBuilder::new() /// .set_handler(handle) diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs index 35007b9e..ff85e2c1 100644 --- a/azalea/src/swarm/mod.rs +++ b/azalea/src/swarm/mod.rs @@ -636,7 +636,7 @@ pub type BoxSwarmHandleFn = /// #[derive(Default, Clone, Resource)] /// struct SwarmState {} /// -/// #[tokio::main] +/// #[tokio::main(flavor = "current_thread")] /// async fn main() { /// let mut accounts = Vec::new(); /// let mut states = Vec::new(); From 1a983beec10fe80d3bca1e5a9ee0b63c0de24117 Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 15 Jun 2025 14:45:56 -0500 Subject: [PATCH 36/36] update changelog --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b060cb..8eda3843 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,28 +17,53 @@ is breaking anyways, semantic versioning is not followed. - Non-standard legacy hex colors like `ยง#ff0000` are now supported in azalea-chat. - Chat signing. - Add auto-reconnecting which is enabled by default. -- `client.start_use_item()`. +- `Client::start_use_item`. - The pathfinder no longer avoids slabs, stairs, and dirt path blocks. - The pathfinder now immediately recalculates if blocks are placed in its path. +- Bots that use custom pathfinder moves can now keep arbitrary persistent state by using the `CustomPathfinderState` component and `PathfinderCtx::custom_state`. - The reach distance for the pathfinder `ReachBlockPosGoal` is now configurable. (@x-osc) +- There is now a `retry_on_no_path` option in `GotoEvent` that can be set to false to make the pathfinder give up if no path could be found. - azalea-brigadier now supports suggestions, command contexts, result consumers, and returning errors with `ArgumentBuilder::executes_result`. +- Proper support for getting biomes at coordinates. +- Add a new `Client::entities_by` which sorts entities that match a criteria by their distance to the client. +- New client event `Event::ReceiveChunk`. +- Several new functions for interacting with inventories. +- Add `Client::set_selected_hotbar_slot` and `Client::selected_hotbar_slot`. +- Add `Client::attack_cooldown_remaining_ticks` to complement `has_attack_cooldown`. ### Changed -- [BREAKING] `Client::goto` is now async and completes when the client reaches its destination. `Client::start_goto` should be used if the old behavior is desired. -- [BREAKING] The `BlockState::id` field is now private, use `.id()` instead. -- [BREAKING] Update to [Bevy 0.16](https://bevyengine.org/news/bevy-0-16/). -- [BREAKING] Rename `InstanceContainer::insert` to `get_or_insert`. -- [BREAKING] Replace `BlockInteractEvent` with the more general-purpose `StartUseItemEvent`. +- `Client::goto` is now async and completes when the client reaches its destination. `Client::start_goto` should be used if the old behavior is desired. +- The `BlockState::id` field is now private, use `.id()` instead. +- Update to [Bevy 0.16](https://bevyengine.org/news/bevy-0-16/). +- Rename `InstanceContainer::insert` to `get_or_insert`. +- Replace `BlockInteractEvent` with the more general-purpose `StartUseItemEvent`. - `ClientBuilder` and `SwarmBuilder` are now Send. +- Replace `wait_one_tick` and `wait_one_update` with `wait_ticks` and `wait_updates`. +- Functions that took `&Vec3` or `&BlockPos` as arguments now only take them as owned types. +- Rename `azalea_block::Block` to `BlockTrait` to disambiguate with `azalea_registry::Block`. +- `GotoEvent` is now non-enhaustive, it should be constructed by calling its methods now. ### Fixed - Clients now validate incoming packets using the correct `MAXIMUM_UNCOMPRESSED_LENGTH` value. -- Several protocol fixes, including for ClientboundSetPlayerTeam and a few data components. +- Several protocol fixes, including for `ClientboundSetPlayerTeam` and a few data components. - No more chunk errors when the client joins another world with the same name but different height. -- Mining now aborts correctly and doesn't flag Grim. - Update the `InstanceName` component correctly when we receive a respawn or second login packet. - azalea-chat now handles legacy color codes correctly when parsing from NBT. - Send the correct UUID to servers in `ClientboundHello` when we're joining in offline-mode. - Block shapes and some properties were using data from `1.20.3-pre4` due to using an old data generator (Pixlyzer), which has now been replaced with the data generator from [Pumpkin](https://github.com/Pumpkin-MC/Extractor). +- When patching the path, don't replace the move we're currently executing. +- The correct sequence number is now sent when interacting with blocks. +- Mining is now generally more reliable and doesn't flag Grim. +- Ghost blocks are now handled correctly due to implementing `ClientboundBlockChangedAck`. +- Player eye height was wrong due to being calculated from height instead of being a special case (was 1.53, should've been 1.62). +- The player inventory is now correctly updated when we close a container. +- Inventory interactions are now predicted on the client-side again, and the remaining click operations were implemented. +- `Client::open_container_at` now waits up to 10 ticks for the block to exist if you try to click air. +- Wrong physics collision code resulted in `HitResult` sometimes containing the wrong coordinates and `inside` value. +- Fix the client being unresponsive for a few seconds after joining due to not sending `ServerboundPlayerLoaded`. +- Fix panic when a client received `ClientboundAddEntity` and `ClientboundStartConfiguration` at the same time. +- Fix panic due to `ClientInformation` being inserted too late. +- `ClientboundTeleportEntity` did not handle relative teleports correctly. +- Pathfinder now gets stuck in water less by automatically trying to jump if it's in water.