1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 06:16:04 +00:00

Merge branch 'main' into 1.21.6

This commit is contained in:
mat 2025-06-15 06:01:50 -13:45
commit 6495a58812
100 changed files with 2376 additions and 1344 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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<dyn Block>`.
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<dyn BlockTrait>`.
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::<dyn Block>::from(block_state);
let block = Box::<dyn BlockTrait>::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::<dyn Block>::from(block_state).downcast_ref::<azalea_block::blocks::Jukebox>() {
if let Some(jukebox) = Box::<dyn BlockTrait>::from(block_state).downcast_ref::<azalea_block::blocks::Jukebox>() {
// ...
}
```

View file

@ -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<BlockState> for Box<dyn Block> {
impl From<BlockState> for Box<dyn BlockTrait> {
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<azalea_registry::Block> for Box<dyn Block> {
impl From<azalea_registry::Block> for Box<dyn BlockTrait> {
fn from(block: azalea_registry::Block) -> Self {
match block {
#from_registry_block_to_block_match

View file

@ -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::<dyn Block>::from(*self)
Box::<dyn BlockTrait>::from(*self)
)
}
}
impl From<BlockState> for azalea_registry::Block {
fn from(value: BlockState) -> Self {
Box::<dyn Block>::from(value).as_registry_block()
Box::<dyn BlockTrait>::from(value).as_registry_block()
}
}
@ -149,11 +149,11 @@ mod tests {
#[test]
fn test_from_blockstate() {
let block: Box<dyn Block> = Box::<dyn Block>::from(BlockState::AIR);
let block: Box<dyn BlockTrait> = Box::<dyn BlockTrait>::from(BlockState::AIR);
assert_eq!(block.id(), "air");
let block: Box<dyn Block> =
Box::<dyn Block>::from(BlockState::from(azalea_registry::Block::FloweringAzalea));
let block: Box<dyn BlockTrait> =
Box::<dyn BlockTrait>::from(BlockState::from(azalea_registry::Block::FloweringAzalea));
assert_eq!(block.id(), "flowering_azalea");
}

View file

@ -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,

View file

@ -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 => {

View file

@ -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<T: Block>(&self) -> Option<&T> {
impl dyn BlockTrait {
pub fn downcast_ref<T: BlockTrait>(&self) -> Option<&T> {
(self as &dyn Any).downcast_ref::<T>()
}
}

View file

@ -3,6 +3,8 @@ use std::{
ops::{Add, RangeInclusive},
};
use azalea_registry::Block;
use crate::{BlockState, block_state::BlockStateIntegerRepr};
#[derive(Debug, Clone)]
@ -45,18 +47,33 @@ impl Add for BlockStates {
}
}
impl From<HashSet<azalea_registry::Block>> for BlockStates {
fn from(set: HashSet<azalea_registry::Block>) -> Self {
Self {
set: set.into_iter().map(|b| b.into()).collect(),
}
impl From<HashSet<Block>> for BlockStates {
fn from(set: HashSet<Block>) -> Self {
Self::from(&set)
}
}
impl From<&HashSet<azalea_registry::Block>> for BlockStates {
fn from(set: &HashSet<azalea_registry::Block>) -> Self {
Self {
set: set.iter().map(|&b| b.into()).collect(),
impl From<&HashSet<Block>> for BlockStates {
fn from(set: &HashSet<Block>) -> Self {
let mut block_states = HashSet::with_capacity(set.len());
for &block in set {
block_states.extend(BlockStates::from(block));
}
Self { set: block_states }
}
}
impl<const N: usize> 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 }
}
}

View file

@ -55,7 +55,7 @@ impl<S> CommandDispatcher<S> {
build
}
pub fn parse(&self, command: StringReader, source: S) -> ParseResults<S> {
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());

View file

@ -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},
@ -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<dyn std::error::Error>> {
/// let account = Account::offline("bot");
/// let (client, rx) = Client::join(account, "localhost").await?;
@ -338,17 +338,10 @@ 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::<Mining, _>(|m| m.is_some());
/// # }
/// ```
pub fn map_get_component<T: Component, R>(&self, f: impl FnOnce(Option<&T>) -> R) -> R {
pub fn map_get_component<T: Component, R>(&self, f: impl FnOnce(&T) -> R) -> Option<R> {
let mut ecs = self.ecs.lock();
let value = self.query::<Option<&T>>(&mut ecs);
f(value)
value.map(f)
}
/// Get an `RwLock` with a reference to our (potentially shared) world.
@ -579,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,
@ -594,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,

View file

@ -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<Q, F>,
) -> Option<Entity> {
let instance_name = self.get_component::<InstanceName>()?;
predicate.find(self.ecs.clone(), &instance_name)
predicate.find_any(self.ecs.clone(), &instance_name)
}
/// Same as [`Self::entity_by`] but returns a `Vec<Entity>` of all entities
/// in our instance that match the predicate.
/// Similar to [`Self::entity_by`] but returns a `Vec<Entity>` 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<F: QueryFilter, Q: QueryData>(
&self,
predicate: impl EntityPredicate<Q, F>,
@ -77,7 +85,10 @@ impl Client {
let Some(instance_name) = self.get_component::<InstanceName>() else {
return vec![];
};
predicate.find_all(self.ecs.clone(), &instance_name)
let Some(position) = self.get_component::<Position>() 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<Q: QueryData, Filter: QueryFilter> {
fn find(&self, ecs_lock: Arc<Mutex<World>>, instance_name: &InstanceName) -> Option<Entity>;
fn find_all(&self, ecs_lock: Arc<Mutex<World>>, instance_name: &InstanceName) -> Vec<Entity>;
fn find_any(&self, ecs_lock: Arc<Mutex<World>>, instance_name: &InstanceName)
-> Option<Entity>;
fn find_all_sorted(
&self,
ecs_lock: Arc<Mutex<World>>,
instance_name: &InstanceName,
nearest_to: Vec3,
) -> Vec<Entity>;
}
impl<F, Q: QueryData, Filter: QueryFilter> EntityPredicate<Q, Filter> for F
where
F: Fn(&ROQueryItem<Q>) -> bool,
{
fn find(&self, ecs_lock: Arc<Mutex<World>>, instance_name: &InstanceName) -> Option<Entity> {
fn find_any(
&self,
ecs_lock: Arc<Mutex<World>>,
instance_name: &InstanceName,
) -> Option<Entity> {
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<Mutex<World>>, instance_name: &InstanceName) -> Vec<Entity> {
fn find_all_sorted(
&self,
ecs_lock: Arc<Mutex<World>>,
instance_name: &InstanceName,
nearest_to: Vec3,
) -> Vec<Entity> {
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::<Vec<_>>()
.filter(|(_, e_instance_name, _, q)| *e_instance_name == instance_name && (self)(q))
.map(|(e, _, position, _)| (e, Vec3::from(position)))
.collect::<Vec<(Entity, Vec3)>>();
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::<Vec<Entity>>()
}
}

View file

@ -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;

View file

@ -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::<AttackStrengthScale>()
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::<AttackStrengthScale>() 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
}
}

View file

@ -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

View file

@ -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<FormattedText>),
ReceiveChunk(ChunkPos),
}
/// A component that contains an event sender for events that are only
@ -145,6 +147,7 @@ impl Plugin for EventsPlugin {
keepalive_listener,
death_listener,
disconnect_listener,
receive_chunk_listener,
),
)
.add_systems(
@ -294,3 +297,17 @@ pub fn disconnect_listener(
}
}
}
pub fn receive_chunk_listener(
query: Query<&LocalPlayerEvents>,
mut events: EventReader<ReceiveChunkEvent>,
) {
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,
)));
}
}
}

View file

@ -1,3 +1,5 @@
use std::collections::HashMap;
use azalea_block::BlockState;
use azalea_core::{
direction::Direction,
@ -96,18 +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<BlockPos, ServerVerifiedState>,
}
#[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_and_increment(&mut self) -> u32 {
let cur = self.0;
self.0 += 1;
cur
pub fn start_predicting(&mut self) -> u32 {
self.seq += 1;
self.seq
}
/// Should be called right before the client updates a block with its
/// prediction.
///
/// This is used to make sure that we can rollback to this state if the
/// server acknowledges the sequence number (with
/// [`ClientboundBlockChangedAck`]) without having sent a block update.
///
/// [`ClientboundBlockChangedAck`]: azalea_protocol::packets::game::ClientboundBlockChangedAck
pub fn retain_known_server_state(
&mut self,
pos: BlockPos,
old_state: BlockState,
player_pos: Vec3,
) {
self.server_state
.entry(pos)
.and_modify(|s| s.seq = self.seq)
.or_insert(ServerVerifiedState {
seq: self.seq,
block_state: old_state,
player_pos,
});
}
/// Save this update as the correct server state so when the server sends a
/// [`ClientboundBlockChangedAck`] we don't roll back this new update.
///
/// This should be used when we receive a block update from the server.
///
/// [`ClientboundBlockChangedAck`]: azalea_protocol::packets::game::ClientboundBlockChangedAck
pub fn update_known_server_state(&mut self, pos: BlockPos, state: BlockState) -> bool {
if let Some(s) = self.server_state.get_mut(&pos) {
s.block_state = state;
true
} else {
false
}
}
pub fn end_prediction_up_to(&mut self, seq: u32, world: &Instance) {
let mut to_remove = Vec::new();
for (pos, state) in &self.server_state {
if state.seq > seq {
continue;
}
to_remove.push(*pos);
// syncBlockState
let client_block_state = world.get_block_state(*pos).unwrap_or_default();
let server_block_state = state.block_state;
if client_block_state == server_block_state {
continue;
}
world.set_block_state(*pos, server_block_state);
// TODO: implement these two functions
// if is_colliding(player, *pos, server_block_state) {
// abs_snap_to(state.player_pos);
// }
}
for pos in to_remove {
self.server_state.remove(&pos);
}
}
}
@ -148,7 +227,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
@ -164,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::<StartUseItemQueued>();
if mining.is_some() {
@ -204,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_and_increment(),
seq,
x_rot: look_direction.x_rot,
y_rot: look_direction.y_rot,
},
@ -220,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_and_increment(),
seq,
},
));
// TODO: depending on the result of useItemOn, this might
@ -281,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 {
@ -302,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 {
@ -318,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,
@ -344,7 +426,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 {

View file

@ -1,4 +1,7 @@
use std::collections::{HashMap, HashSet};
use std::{
cmp,
collections::{HashMap, HashSet},
};
use azalea_chat::FormattedText;
pub use azalea_inventory::*;
@ -60,13 +63,42 @@ 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.
#[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.
@ -312,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;
}
}
@ -361,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) {
@ -378,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);
@ -499,12 +598,74 @@ 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()];
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<ItemStack> {
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(
@ -637,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<Mutex<ItemStack>>` 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;
}
@ -650,12 +835,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<ContainerClickEvent>,
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.",
@ -664,16 +849,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<u16, HashedStack> = 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));
}
@ -754,3 +941,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
);
}
}

View file

@ -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,
@ -128,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
@ -215,7 +218,6 @@ pub fn poll_create_connection_task(
write_conn,
ConnectionProtocol::Login,
),
client_information: crate::ClientInformation::default(),
instance_holder,
metadata: azalea_entity::metadata::PlayerMetadataBundle::default(),
},

View file

@ -0,0 +1,48 @@
use azalea_core::tick::GameTick;
use azalea_entity::{InLoadedChunk, LocalEntity};
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;
#[allow(clippy::type_complexity)]
pub fn player_loaded_packet(
mut commands: Commands,
query: Query<
Entity,
(
With<LocalEntity>,
With<InLoadedChunk>,
Without<HasClientLoaded>,
),
>,
) {
for entity in query.iter() {
commands.trigger(SendPacketEvent::new(entity, ServerboundPlayerLoaded));
commands.entity(entity).insert(HasClientLoaded);
}
}

View file

@ -1,6 +1,6 @@
use azalea_block::{Block, 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_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};
@ -8,12 +8,12 @@ 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,
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,
@ -241,13 +241,12 @@ fn handle_mining_queued(
mut current_mining_pos,
) in query
{
info!("mining_queued: {mining_queued:?}");
commands.entity(entity).remove::<MiningQueued>();
let instance = instance_holder.instance.read();
if check_is_interaction_restricted(
&instance,
&mining_queued.position,
mining_queued.position,
&game_mode.current,
inventory,
) {
@ -281,13 +280,13 @@ 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,
},
));
}
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
@ -301,7 +300,7 @@ fn handle_mining_queued(
});
}
let block = Box::<dyn Block>::from(target_block_state);
let block = Box::<dyn BlockTrait>::from(target_block_state);
let held_item = inventory.held_item();
@ -314,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,
@ -346,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(),
seq: sequence_number.start_predicting(),
},
));
// vanilla really does send two swing arm packets
@ -441,17 +440,25 @@ pub fn handle_finish_mining_block_observer(
&Inventory,
&PlayerAbilities,
&PermissionLevel,
&mut CurrentSequenceNumber,
&Position,
&mut BlockStatePredictionHandler,
)>,
instances: Res<InstanceContainer>,
) {
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) {
if check_is_interaction_restricted(&instance, event.position, &game_mode.current, inventory) {
return;
}
@ -466,11 +473,12 @@ 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;
};
let registry_block = Box::<dyn Block>::from(block_state).as_registry_block();
let registry_block: azalea_registry::Block =
Box::<dyn BlockTrait>::from(block_state).as_registry_block();
if !can_use_game_master_blocks(abilities, permission_level)
&& matches!(
registry_block,
@ -486,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.
@ -498,10 +509,10 @@ pub fn handle_stop_mining_block_event(
mut events: EventReader<StopMiningBlockEvent>,
mut commands: Commands,
mut mine_block_progress_events: EventWriter<MineBlockProgressEvent>,
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");
@ -511,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::<Mining>();
@ -539,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<MineBlockProgressEvent>,
@ -558,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 {
@ -581,7 +592,7 @@ pub fn continue_mining_block(
action: s_player_action::Action::StartDestroyBlock,
pos: mining.pos,
direction: mining.dir,
sequence: sequence_number.get_and_increment(),
seq: prediction_handler.start_predicting(),
},
));
commands.trigger(SwingArmEvent { entity });
@ -596,7 +607,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:?}");
@ -604,7 +615,7 @@ pub fn continue_mining_block(
commands.entity(entity).remove::<Mining>();
continue;
}
let block = Box::<dyn Block>::from(target_block_state);
let block = Box::<dyn BlockTrait>::from(target_block_state);
**mine_progress += get_mine_progress(
block.as_ref(),
current_mining_item.kind(),
@ -635,12 +646,12 @@ pub fn continue_mining_block(
action: s_player_action::Action::StopDestroyBlock,
pos: mining.pos,
direction: mining.dir,
sequence: sequence_number.get_and_increment(),
seq: prediction_handler.start_predicting(),
},
));
**mine_progress = 0.;
**mine_ticks = 0.;
**mine_delay = 0;
**mine_delay = 5;
}
mine_block_progress_events.write(MineBlockProgressEvent {

View file

@ -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)

View file

@ -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);
}
}

View file

@ -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::{
@ -26,9 +25,11 @@ use crate::{
connection::RawConnection,
declare_packet_handlers,
disconnect::DisconnectEvent,
interact::BlockStatePredictionHandler,
inventory::{
ClientSideCloseContainerEvent, Inventory, MenuOpenedEvent, SetContainerContentEvent,
},
loading::HasClientLoaded,
local_player::{Hunger, InstanceHolder, LocalGameMode, PlayerAbilities, TabList},
movement::{KnockbackEvent, KnockbackType},
packet::as_system,
@ -427,68 +428,12 @@ impl GamePacketHandler<'_> {
**last_sent_position = **position;
fn apply_change<T: Add<Output = T>>(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);
physics.set_old_pos(*position);
// send the relevant packets
commands.trigger(SendPacketEvent::new(
self.player,
ServerboundAcceptTeleportation { id: p.id },
@ -496,8 +441,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,
},
@ -854,6 +799,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)| {
@ -864,26 +811,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::<Position>().unwrap();
if new_pos != **position {
**position = new_pos;
}
let position = *position;
let mut look_direction = entity.get_mut::<LookDirection>().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::<Physics>().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);
});
},
));
},
@ -916,11 +865,7 @@ impl GamePacketHandler<'_> {
instance_holder.partial_instance.clone(),
move |entity_mut| {
let mut physics = entity_mut.get_mut::<Physics>().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);
@ -970,17 +915,13 @@ impl GamePacketHandler<'_> {
instance_holder.partial_instance.clone(),
move |entity_mut| {
let mut physics = entity_mut.get_mut::<Physics>().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::<Position>().unwrap();
if new_pos != **position {
**position = new_pos;
if new_position != **position {
**position = new_position;
}
let mut look_direction = entity_mut.get_mut::<LookDirection>().unwrap();
@ -1124,13 +1065,17 @@ impl GamePacketHandler<'_> {
pub fn block_update(&mut self, p: &ClientboundBlockUpdate) {
debug!("Got block update packet {p:?}");
as_system::<Query<&InstanceHolder>>(self.ecs, |mut query| {
let local_player = query.get_mut(self.player).unwrap();
as_system::<Query<(&InstanceHolder, &mut BlockStatePredictionHandler)>>(
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) {
@ -1140,15 +1085,19 @@ impl GamePacketHandler<'_> {
pub fn section_blocks_update(&mut self, p: &ClientboundSectionBlocksUpdate) {
debug!("Got section blocks update packet {p:?}");
as_system::<Query<&InstanceHolder>>(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::<Query<(&InstanceHolder, &mut BlockStatePredictionHandler)>>(
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) {
@ -1188,7 +1137,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::<Query<(&InstanceHolder, &mut BlockStatePredictionHandler)>>(
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) {}
@ -1493,8 +1451,9 @@ impl GamePacketHandler<'_> {
entity_bundle,
));
// Remove the Dead marker component from the player.
commands.entity(self.player).remove::<Dead>();
commands
.entity(self.player)
.remove::<(Dead, HasClientLoaded)>();
},
)
}
@ -1609,7 +1568,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::<Query<&mut Inventory>>(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) {}

View file

@ -0,0 +1,6 @@
pub mod simulation;
pub mod tracing;
pub mod prelude {
pub use super::{simulation::*, tracing::*};
}

View file

@ -6,19 +6,22 @@ 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,
};
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();
@ -99,10 +102,16 @@ 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);
}
pub fn update(&mut self) {
self.app.update();
}
pub fn minecraft_entity_id(&self) -> MinecraftEntityId {
self.component::<MinecraftEntityId>()
@ -145,6 +154,12 @@ impl Simulation {
.chunks
.get(&chunk_pos)
}
pub fn get_block_state(&self, pos: BlockPos) -> Option<BlockState> {
self.component::<InstanceHolder>()
.instance
.read()
.get_block_state(pos)
}
pub fn disconnect(&mut self) {
// send DisconnectEvent
@ -171,7 +186,6 @@ fn create_local_player_bundle(
let local_player_bundle = LocalPlayerBundle {
raw_connection,
client_information: ClientInformation::default(),
instance_holder,
metadata: PlayerMetadataBundle::default(),
};

View file

@ -0,0 +1,37 @@
use bevy_log::tracing_subscriber::{
self, EnvFilter, Layer,
layer::{Context, SubscriberExt},
util::SubscriberInitExt,
};
use tracing::{Event, Level, Subscriber};
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<S: Subscriber> Layer<S> 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}");
}
}
}

View file

@ -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::<InConfigState>());
assert!(!simulation.has_component::<InGameState>());

View file

@ -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);

View file

@ -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 {

View file

@ -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::<InConfigState>());

View file

@ -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::<InConfigState>());
assert!(!simulation.has_component::<InGameState>());
@ -128,9 +127,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();
}

View file

@ -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");
}

View file

@ -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()));
}

View file

@ -0,0 +1,46 @@
use azalea_client::test_utils::prelude::*;
use azalea_core::{
position::{ChunkPos, Vec3},
resource_location::ResourceLocation,
};
use azalea_protocol::{
common::movements::{PositionMoveRotation, RelativeMovements},
packets::{
ConnectionProtocol,
game::{ClientboundRemoveEntities, ClientboundTeleportEntity},
},
};
use azalea_registry::{DataRegistry, DimensionType, EntityKind};
use azalea_world::MinecraftEntityId;
#[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"),
));
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)));
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();
}

View file

@ -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<Cow>>();
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(

View file

@ -0,0 +1,39 @@
use azalea_client::{InConfigState, test_utils::prelude::*};
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 uuid::Uuid;
#[test]
fn test_receive_spawn_entity_and_start_config_packet() {
init_tracing();
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::<InstanceName>());
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::<InConfigState>());
}

View file

@ -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(

View file

@ -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);

View file

@ -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::<InConfigState>());

View file

@ -15,14 +15,16 @@ pub struct AABB {
pub struct ClipPointOpts<'a> {
pub t: &'a mut f64,
pub approach_dir: Option<Direction>,
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,25 +153,25 @@ impl AABB {
self.deflate(Vec3::new(amount, amount, amount))
}
pub fn clip(&self, min: &Vec3, max: &Vec3) -> Option<Vec3> {
pub fn clip(&self, min: Vec3, max: Vec3) -> Option<Vec3> {
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<Vec3> {
pub fn clip_with_from_and_to(min: Vec3, max: Vec3, from: Vec3, to: Vec3) -> Option<Vec3> {
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<AABB>,
from: &Vec3,
to: &Vec3,
pos: &BlockPos,
boxes: &[AABB],
from: Vec3,
to: Vec3,
pos: BlockPos,
) -> Option<BlockHitResult> {
let mut t = 1.0;
let mut dir = None;
@ -223,14 +183,14 @@ 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,
block_pos: pos,
inside: false,
miss: false,
world_border: false,
@ -239,32 +199,34 @@ impl AABB {
fn get_direction_aabb(
&self,
from: &Vec3,
from: Vec3,
t: &mut f64,
dir: Option<Direction>,
delta: &Vec3,
delta: Vec3,
) -> Option<Direction> {
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<Direction>,
delta: &Vec3,
delta: Vec3,
) -> Option<Direction> {
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<AABB>) -> 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(&center) {
if inflated.contains(new_center) || inflated.contains(center) {
return true;
}
if inflated.clip(&center, &new_center).is_some() {
if inflated.clip(center, new_center).is_some() {
return true;
}
}
@ -494,13 +458,13 @@ 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.),
&BlockPos::new(0, 0, 0),
Vec3::new(-1., -1., -1.),
Vec3::new(1., 1., 1.),
BlockPos::new(0, 0, 0),
),
None
);

View file

@ -162,6 +162,16 @@ impl CardinalDirection {
}
}
}
impl From<CardinalDirection> 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.

View file

@ -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()
}
@ -335,8 +352,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 {
@ -357,6 +375,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 {
@ -371,43 +399,25 @@ 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()
}
/// 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
@ -444,6 +454,17 @@ impl Add<ChunkPos> for ChunkPos {
}
}
}
impl Add<ChunkBlockPos> 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
// ........
@ -567,6 +588,12 @@ impl From<&ChunkBiomePos> for ChunkSectionBiomePos {
}
}
}
impl From<ChunkBiomePos> 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.
@ -582,6 +609,12 @@ impl From<&BlockPos> for ChunkBiomePos {
ChunkBiomePos::from(&ChunkBlockPos::from(pos))
}
}
impl From<BlockPos> for ChunkBiomePos {
#[inline]
fn from(pos: BlockPos) -> Self {
ChunkBiomePos::from(&ChunkBlockPos::from(pos))
}
}
impl From<&ChunkBlockPos> for ChunkBiomePos {
#[inline]
fn from(pos: &ChunkBlockPos) -> Self {

View file

@ -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 {

View file

@ -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;
@ -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()
@ -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,
@ -321,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,
@ -373,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;
}
}
@ -389,6 +391,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 {
@ -453,7 +457,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),
@ -487,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 {
@ -499,5 +508,5 @@ impl FluidOnEyes {
}
}
#[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)]
#[derive(Component, Clone, Copy, Debug, PartialEq, Deref, DerefMut)]
pub struct OnClimbable(bool);

View file

@ -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;
}

View file

@ -132,16 +132,17 @@ pub fn update_entity_chunk_positions(
instance_container: Res<InstanceContainer>,
) {
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<InstanceContainer>,
) {
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

View file

@ -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,8 +134,8 @@ 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_at_feet = Box::<dyn azalea_block::Block>::from(block_state_at_feet);
let block_state_at_feet = instance.get_block_state(block_pos).unwrap_or_default();
let block_at_feet = Box::<dyn azalea_block::BlockTrait>::from(block_state_at_feet);
let registry_block_at_feet = block_at_feet.as_registry_block();
**on_climbable = azalea_registry::tags::blocks::CLIMBABLE.contains(&registry_block_at_feet)
@ -159,10 +159,10 @@ 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::<dyn azalea_block::Block>::from(block_below).as_registry_block();
Box::<dyn azalea_block::BlockTrait>::from(block_below).as_registry_block();
if registry_block_below != azalea_registry::Block::Ladder {
return false;
}
@ -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<Position>>) {
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;
}
}
@ -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,

View file

@ -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;
}

View file

@ -48,6 +48,7 @@ pub fn generate(input: &DeclareMenus) -> TokenStream {
}
quote! {
#[derive(Debug)]
pub enum MenuLocation {
#menu_location_variants
}

View file

@ -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;
@ -32,6 +33,11 @@ impl<const N: usize> Default for SlotList<N> {
SlotList([(); N].map(|_| ItemStack::Empty))
}
}
impl<const N: usize> SlotList<N> {
pub fn new(items: [ItemStack; N]) -> Self {
SlotList(items)
}
}
impl Menu {
/// Get the [`Player`] from this [`Menu`].
@ -46,6 +52,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

View file

@ -61,6 +61,8 @@ impl From<QuickMoveClick> 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,
}
@ -615,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
}
@ -655,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
@ -675,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();
@ -686,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();
}
}
}

View file

@ -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<ItemStackData> 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

View file

@ -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)
@ -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,
block_pos: &BlockPos,
from: Vec3,
to: Vec3,
block_pos: BlockPos,
block_shape: &VoxelShape,
_block_state: &BlockState,
_block_state: BlockState,
) -> Option<BlockHitResult> {
let block_hit_result = block_shape.clip(from, to, block_pos);
@ -168,7 +168,7 @@ pub fn traverse_blocks<C, T>(
from: Vec3,
to: Vec3,
context: C,
get_hit_result: impl Fn(&C, &BlockPos) -> Option<T>,
get_hit_result: impl Fn(&C, BlockPos) -> Option<T>,
get_miss_result: impl Fn(&C) -> T,
) -> T {
if from == to {
@ -188,7 +188,7 @@ pub fn traverse_blocks<C, T>(
};
let mut current_block = BlockPos::from(right_before_start);
if let Some(data) = get_hit_result(&context, &current_block) {
if let Some(data) = get_hit_result(&context, current_block) {
return data;
}
@ -249,13 +249,13 @@ pub fn traverse_blocks<C, T>(
percentage.z += percentage_step.z;
}
if let Some(data) = get_hit_result(&context, &current_block) {
if let Some(data) = get_hit_result(&context, current_block) {
return data;
}
}
}
pub fn box_traverse_blocks(from: &Vec3, to: &Vec3, aabb: &AABB) -> HashSet<BlockPos> {
pub fn box_traverse_blocks(from: Vec3, to: Vec3, aabb: &AABB) -> HashSet<BlockPos> {
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;
};

View file

@ -35,7 +35,7 @@ pub enum MoverType {
// Entity.collide
fn collide(
movement: &Vec3,
movement: Vec3,
world: &Instance,
physics: &azalea_entity::Physics,
source_entity: Option<Entity>,
@ -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<azalea_entity::Position>,
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,76 +259,53 @@ fn collide_bounding_box(
}
fn collide_with_shapes(
movement: &Vec3,
mut movement: Vec3,
mut entity_box: AABB,
collision_boxes: &Vec<VoxelShape>,
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.
///
/// 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;
}
@ -379,7 +356,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::<dyn azalea_block::Block>::from(block);
let block_trait = Box::<dyn azalea_block::BlockTrait>::from(block);
if let Some(solid) = block_trait.behavior().force_solid {
return solid;
}

View file

@ -98,9 +98,9 @@ impl Shapes {
}
pub fn collide(
axis: &Axis,
axis: Axis,
entity_box: &AABB,
collision_boxes: &Vec<VoxelShape>,
collision_boxes: &[VoxelShape],
mut movement: f64,
) -> f64 {
for shape in collision_boxes {
@ -399,10 +399,6 @@ 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),
_ => {
@ -412,7 +408,7 @@ impl VoxelShape {
}
}
pub fn clip(&self, from: &Vec3, to: &Vec3, block_pos: &BlockPos) -> Option<BlockHitResult> {
pub fn clip(&self, from: Vec3, to: Vec3, block_pos: BlockPos) -> Option<BlockHitResult> {
if self.is_empty() {
return None;
}
@ -420,7 +416,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),
@ -428,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,
@ -440,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() {
@ -645,7 +641,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);
@ -681,7 +676,7 @@ impl CubeVoxelShape {
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
f64::floor(f64::clamp(coord * (n as f64), -1f64, n as f64)) as i32
}
}
@ -752,4 +747,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
}
);
}
}

View file

@ -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 {
@ -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;
}

View file

@ -8,7 +8,7 @@ pub mod travel;
use std::collections::HashSet;
use azalea_block::{Block, BlockState, fluid_state::FluidState, properties};
use azalea_block::{BlockState, BlockTrait, fluid_state::FluidState, properties};
use azalea_core::{
math,
position::{BlockPos, Vec3},
@ -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,17 +188,17 @@ 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;
// }
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;
}
@ -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,
@ -268,7 +269,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
@ -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<Pose>,
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())
.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<Pose>,
) -> Vec3 {
if !**on_climbable {
if !*on_climbable {
return velocity;
}
@ -450,11 +451,11 @@ 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
.get_block_state(&position.into())
.get_block_state(position.into())
.unwrap_or_default(),
) != azalea_registry::Block::Scaffolding
{
@ -485,14 +486,14 @@ 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());
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
.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::<dyn Block>::from(block).behavior().jump_factor
Box::<dyn BlockTrait>::from(block).behavior().jump_factor
} else {
1.
};
@ -501,7 +502,7 @@ fn block_jump_factor(world: &Instance, position: &Position) -> f32 {
}
if let Some(block) = block_below {
Box::<dyn Block>::from(block).behavior().jump_factor
Box::<dyn BlockTrait>::from(block).behavior().jump_factor
} else {
1.
}
@ -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)
}

View file

@ -1,4 +1,4 @@
use azalea_block::{Block, BlockState, fluid_state::FluidState};
use azalea_block::{BlockState, BlockTrait, fluid_state::FluidState};
use azalea_core::{
aabb::AABB,
position::{BlockPos, Vec3},
@ -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,25 +106,25 @@ fn travel_in_air(
world: &Instance,
entity: Entity,
physics: &mut Physics,
direction: &LookDirection,
direction: LookDirection,
position: Mut<Position>,
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
.get_block_state(&block_pos_below)
.get_block_state(block_pos_below)
.unwrap_or(BlockState::AIR);
let block_below: Box<dyn Block> = block_state_below.into();
let block_below: Box<dyn BlockTrait> = block_state_below.into();
let block_friction = block_below.behavior().friction;
let inertia = if physics.on_ground() {
@ -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<Position>,
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,
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,
@ -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;

View file

@ -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();

View file

@ -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.
///
@ -28,6 +31,65 @@ pub struct RelativeMovements {
pub delta_z: bool,
pub rotate_delta: bool,
}
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<T: Add<Output = T>>(base: T, condition: bool, change: T) -> T {
if condition { base + change } else { change }
}
impl AzaleaRead for RelativeMovements {
fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {

View file

@ -4,5 +4,5 @@ use azalea_protocol_macros::ClientboundGamePacket;
#[derive(Clone, Debug, AzBuf, ClientboundGamePacket)]
pub struct ClientboundBlockChangedAck {
#[var]
pub sequence: i32,
pub seq: u32,
}

View file

@ -9,6 +9,6 @@ pub struct ClientboundTeleportEntity {
#[var]
pub id: MinecraftEntityId,
pub change: PositionMoveRotation,
pub relatives: RelativeMovements,
pub relative: RelativeMovements,
pub on_ground: bool,
}

View file

@ -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)]

View file

@ -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,
}

View file

@ -15,7 +15,7 @@ pub struct ServerboundUseItemOn {
pub hand: InteractionHand,
pub block_hit: BlockHit,
#[var]
pub sequence: u32,
pub seq: u32,
}
#[derive(Clone, Debug)]

View file

@ -24,7 +24,8 @@ impl<T: DataRegistry> 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]
@ -57,7 +58,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 {

View file

@ -281,10 +281,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",
@ -1891,6 +1891,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",

View file

@ -219,7 +219,7 @@ impl BitStorage {
self.size
}
pub fn iter(&self) -> BitStorageIter {
pub fn iter(&self) -> BitStorageIter<'_> {
BitStorageIter {
storage: self,
index: 0,

View file

@ -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,
@ -152,7 +155,7 @@ impl PartialChunkStorage {
pub fn set_block_state(
&self,
pos: &BlockPos,
pos: BlockPos,
state: BlockState,
chunk_storage: &ChunkStorage,
) -> Option<BlockState> {
@ -290,26 +293,26 @@ impl ChunkStorage {
self.map.get(pos).and_then(|chunk| chunk.upgrade())
}
pub fn get_block_state(&self, pos: &BlockPos) -> Option<BlockState> {
pub fn get_block_state(&self, pos: BlockPos) -> Option<BlockState> {
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<FluidState> {
pub fn get_fluid_state(&self, pos: BlockPos) -> Option<FluidState> {
let block_state = self.get_block_state(pos)?;
Some(FluidState::from(block_state))
}
pub fn get_biome(&self, pos: &BlockPos) -> Option<Biome> {
pub fn get_biome(&self, pos: BlockPos) -> Option<Biome> {
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<BlockState> {
pub fn set_block_state(&self, pos: BlockPos, state: BlockState) -> Option<BlockState> {
if pos.y < self.min_y || pos.y >= (self.min_y + self.height as i32) {
return None;
}
@ -401,7 +404,7 @@ impl Chunk {
}
}
pub fn get_biome(&self, pos: &ChunkBiomePos, min_y: i32) -> Option<Biome> {
pub fn get_biome(&self, pos: ChunkBiomePos, min_y: i32) -> Option<Biome> {
if pos.y < min_y {
// y position is out of bounds
return None;
@ -577,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()
);
}

View file

@ -1,22 +1,13 @@
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<BlockState>, 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.
///
/// 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) {
@ -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(&section.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
@ -117,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<BlockPos>,
@ -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(&section.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(&section.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<BlockState>, 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;
@ -269,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 }));
@ -296,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 }));

View file

@ -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::<dyn azalea_block::Block>::from(block_state);
let block = Box::<dyn azalea_block::BlockTrait>::from(block_state);
let registry_block = block.as_registry_block();
match self {
HeightmapKind::WorldSurfaceWg => !block_state.is_air(),

View file

@ -192,19 +192,10 @@ impl<S: PalletedContainerKind> PalettedContainer<S> {
/// 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

View file

@ -171,19 +171,27 @@ pub struct Instance {
}
impl Instance {
pub fn get_block_state(&self, pos: &BlockPos) -> Option<BlockState> {
pub fn get_block_state(&self, pos: BlockPos) -> Option<BlockState> {
self.chunks.get_block_state(pos)
}
pub fn get_fluid_state(&self, pos: &BlockPos) -> Option<FluidState> {
pub fn get_fluid_state(&self, pos: BlockPos) -> Option<FluidState> {
self.chunks.get_block_state(pos).map(FluidState::from)
}
pub fn get_biome(&self, pos: &BlockPos) -> Option<Biome> {
/// 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<Biome> {
self.chunks.get_biome(pos)
}
pub fn set_block_state(&self, pos: &BlockPos, state: BlockState) -> Option<BlockState> {
pub fn set_block_state(&self, pos: BlockPos, state: BlockState) -> Option<BlockState> {
self.chunks.set_block_state(pos, state)
}
}

View file

@ -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
@ -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

View file

@ -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);

View file

@ -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();

View file

@ -12,7 +12,7 @@ use bevy_ecs::{
system::Query,
};
#[tokio::main]
#[tokio::main(flavor = "current_thread")]
async fn main() {
let account = Account::offline("bot");

View file

@ -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();

View file

@ -110,7 +110,7 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) {
};
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<Mutex<CommandSource>>) {
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<Mutex<CommandSource>>) {
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
})),

View file

@ -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;

View file

@ -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();

View file

@ -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<Mutex<bool>>,
}
#[tokio::main]
#[tokio::main(flavor = "current_thread")]
async fn main() {
let account = Account::offline("bot");
// or let bot = Account::microsoft("email").await;

View file

@ -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();

View file

@ -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();

View file

@ -1,8 +1,11 @@
use azalea_block::{Block, BlockState, fluid_state::FluidKind};
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<Output = ()> + 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.
@ -52,7 +67,7 @@ pub fn accurate_best_tool_in_hotbar_for_block(
let mut best_speed = 0.;
let mut best_slot = None;
let block = Box::<dyn Block>::from(block);
let block = Box::<dyn BlockTrait>::from(block);
let registry_block = block.as_registry_block();
if matches!(

View file

@ -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<Output = ()> + Send;
/// Wait for one ECS Update.
fn wait_one_update(&self) -> impl Future<Output = ()> + Send;
/// Wait for the specified number of game ticks.
fn wait_ticks(&self, n: usize) -> impl Future<Output = ()> + Send;
/// Wait for the specified number of ECS `Update`s.
fn wait_updates(&self, n: usize) -> impl Future<Output = ()> + 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) {
@ -220,11 +229,8 @@ 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);
trace!(
"look at {:?} (currently at {:?})",
event.position, **position
);
direction_looking_at(position.up(eye_height.into()), event.position);
trace!("look at {} (currently at {})", event.position, **position);
*look_direction = new_look_direction;
}
}
@ -232,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);

View file

@ -6,10 +6,15 @@ 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_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;
@ -22,15 +27,6 @@ impl Plugin for ContainerPlugin {
}
pub trait ContainerClientExt {
fn open_container_at(
&self,
pos: BlockPos,
) -> impl Future<Output = Option<ContainerHandle>> + Send;
fn open_inventory(&self) -> Option<ContainerHandle>;
fn get_open_container(&self) -> Option<ContainerHandleRef>;
}
impl ContainerClientExt for Client {
/// Open a container in the world, like a chest. Use
/// [`Client::open_inventory`] to open your own inventory.
///
@ -48,15 +44,62 @@ impl ContainerClientExt for Client {
/// let container = bot.open_container_at(target_pos).await;
/// # }
/// ```
fn open_container_at(
&self,
pos: BlockPos,
) -> impl Future<Output = Option<ContainerHandle>> + 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<ContainerHandle>;
/// 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<ContainerHandle> {
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::<WaitingForInventoryOpen>(self.entity).is_none() {
break;
@ -72,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<ContainerHandle> {
let ecs = self.ecs.lock();
let inventory = ecs.get::<Inventory>(self.entity).expect("no inventory");
if inventory.id == 0 {
Some(ContainerHandle::new(0, self.clone()))
} else {
@ -93,23 +125,15 @@ impl ContainerClientExt for Client {
}
}
/// 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<ContainerHandleRef> {
fn get_inventory(&self) -> ContainerHandleRef {
let ecs = self.ecs.lock();
let inventory = ecs.get::<Inventory>(self.entity).expect("no inventory");
ContainerHandleRef::new(inventory.id, self.clone())
}
if inventory.id == 0 {
None
} else {
Some(ContainerHandleRef {
id: inventory.id,
client: self.clone(),
})
}
fn get_held_item(&self) -> ItemStack {
self.map_get_component::<Inventory, _>(|inventory| inventory.held_item())
.expect("no inventory")
}
}
@ -127,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,
@ -171,6 +199,31 @@ 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<Vec<ItemStack>> {
self.menu().map(|menu| menu.slots())
}
/// A shortcut for [`Self::click`] with `PickupClick::Left`.
pub fn left_click(&self, slot: impl Into<usize>) {
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<usize>) {
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<usize>) {
self.click(PickupClick::Right {
slot: Some(slot.into() as u16),
});
}
pub fn click(&self, operation: impl Into<ClickOperation>) {
let operation = operation.into();
self.client.ecs.lock().send_event(ContainerClickEvent {
@ -183,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 {
@ -202,31 +256,9 @@ 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<Menu> {
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<Vec<ItemStack>> {
self.0.contents()
}
pub fn click(&self, operation: impl Into<ClickOperation>) {
self.0.click(operation);
/// Closes the inventory by dropping the handle.
pub fn close(self) {
// implicitly calls drop
}
}

View file

@ -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)

View file

@ -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<Entity> {
@ -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 {

View file

@ -65,11 +65,11 @@ 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 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

View file

@ -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;
@ -29,7 +32,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
}
}
@ -111,14 +116,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)
}
}
@ -191,7 +196,7 @@ impl<T: Goal> Goal for AndGoals<T> {
}
/// 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,
@ -218,17 +223,23 @@ 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 - n).length_squared();
if distance > self.max_check_distance * self.max_check_distance {
let distance_squared = self.pos.distance_squared_to(n);
if distance_squared > self.max_check_distance.pow(2) {
return false;
}
let eye_position = n.to_vec3_floored() + Vec3::new(0.5, 1.62, 0.5);
let look_direction = crate::direction_looking_at(&eye_position, &self.pos.center());
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,
&eye_position,
look_direction,
eye_position,
&self.chunk_storage,
self.distance,
);
@ -236,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()
}
}

View file

@ -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<dyn Goal>,
/// 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
}
}

View file

@ -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<Arc<dyn Goal>>,
pub successors_fn: Option<SuccessorsFn>,
pub is_calculating: bool,
pub allow_mining: bool,
pub retry_on_no_path: bool,
pub min_timeout: Option<PathfinderTimeout>,
pub max_timeout: Option<PathfinderTimeout>,
@ -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<dyn Goal>,
/// 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) {
@ -260,7 +223,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() {
@ -274,10 +237,8 @@ impl PathfinderClientExt for azalea_client::Client {
}
fn is_goto_target_reached(&self) -> bool {
self.map_get_component::<Pathfinder, _>(|p| {
p.map(|p| p.goal.is_none() && !p.is_calculating)
.unwrap_or(true)
})
self.map_get_component::<Pathfinder, _>(|p| p.goal.is_none() && !p.is_calculating)
.unwrap_or(true)
}
}
@ -361,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 {
@ -382,6 +344,7 @@ pub fn goto_listener(
goto_id_atomic,
allow_mining,
mining_cache,
retry_on_no_path,
custom_state,
min_timeout,
max_timeout,
@ -401,10 +364,14 @@ pub struct CalculatePathOpts {
pub goto_id_atomic: Arc<AtomicUsize>,
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.
@ -618,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(),
@ -670,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();
@ -689,7 +660,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 +725,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
@ -934,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 {
@ -952,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),
@ -1026,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
@ -1137,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"),
});
@ -1263,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, 70, 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));
}
}

View file

@ -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.

View file

@ -111,12 +111,18 @@ 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
.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
@ -132,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
@ -185,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()
}
}

View file

@ -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(),
)

View file

@ -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));
}

View file

@ -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
}
@ -615,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)));
@ -636,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)));
@ -657,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)));

View file

@ -636,7 +636,7 @@ pub type BoxSwarmHandleFn<SS, R> =
/// #[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();