mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 06:16:04 +00:00
Add mining to the pathfinder (#122)
* basic pathfinder mining poc * mining descending and autotool * pathfinder mining descending * pathfinder fixes * allow disabling pathfinder miner and other fixes * small optimization to avoid chunk vec iter lookup sometimes * seeded rng in pathfinder bench * consistently use f32::INFINITY this brings performance much closer to how it was before * astar heuristic optimization from baritone * add downward_move * fix downward move execute * avoid liquids and falling blocks when mining * fix COST_HEURISTIC * fix to not path through flowing liquids * only reset pathfinder timeout while mining if the block is close enough * cache mining costs of block positions * fix mine_while_at_start and move PathfinderDebugParticles to its own module * add ReachBlockPosGoal in other news: azalea's sin/cos functions were broken this whole time and i never noticed * clippy and add things that i accidentally didn't commit * improve wording on doc for azalea::pathfinder
This commit is contained in:
parent
59e140ddd6
commit
a707e2eb82
22 changed files with 1456 additions and 358 deletions
|
@ -1,4 +1,7 @@
|
|||
use std::{collections::HashSet, ops::RangeInclusive};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
ops::{Add, RangeInclusive},
|
||||
};
|
||||
|
||||
use crate::BlockState;
|
||||
|
||||
|
@ -31,3 +34,13 @@ impl BlockStates {
|
|||
self.set.contains(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for BlockStates {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
set: self.set.union(&rhs.set).copied().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -193,7 +193,7 @@ pub fn update_hit_result_component(
|
|||
};
|
||||
let instance = instance_lock.read();
|
||||
|
||||
let hit_result = pick(look_direction, &eye_position, &instance, 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 {
|
||||
|
@ -212,13 +212,13 @@ pub fn update_hit_result_component(
|
|||
pub fn pick(
|
||||
look_direction: &LookDirection,
|
||||
eye_position: &Vec3,
|
||||
instance: &Instance,
|
||||
chunks: &azalea_world::ChunkStorage,
|
||||
pick_range: f64,
|
||||
) -> BlockHitResult {
|
||||
let view_vector = view_vector(look_direction);
|
||||
let end_position = eye_position + &(view_vector * pick_range);
|
||||
azalea_physics::clip::clip(
|
||||
&instance.chunks,
|
||||
chunks,
|
||||
ClipContext {
|
||||
from: *eye_position,
|
||||
to: end_position,
|
||||
|
|
|
@ -12,6 +12,7 @@ use azalea_inventory::{
|
|||
use azalea_protocol::packets::game::{
|
||||
serverbound_container_click_packet::ServerboundContainerClickPacket,
|
||||
serverbound_container_close_packet::ServerboundContainerClosePacket,
|
||||
serverbound_set_carried_item_packet::ServerboundSetCarriedItemPacket,
|
||||
};
|
||||
use azalea_registry::MenuKind;
|
||||
use bevy_app::{App, Plugin, Update};
|
||||
|
@ -40,9 +41,11 @@ impl Plugin for InventoryPlugin {
|
|||
.add_event::<CloseContainerEvent>()
|
||||
.add_event::<ContainerClickEvent>()
|
||||
.add_event::<SetContainerContentEvent>()
|
||||
.add_event::<SetSelectedHotbarSlotEvent>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
handle_set_selected_hotbar_slot_event,
|
||||
handle_menu_opened_event,
|
||||
handle_set_container_content_event,
|
||||
handle_container_click_event,
|
||||
|
@ -734,3 +737,33 @@ fn handle_set_container_content_event(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Event)]
|
||||
pub struct SetSelectedHotbarSlotEvent {
|
||||
pub entity: Entity,
|
||||
/// The hotbar slot to select. This should be in the range 0..=8.
|
||||
pub slot: u8,
|
||||
}
|
||||
fn handle_set_selected_hotbar_slot_event(
|
||||
mut events: EventReader<SetSelectedHotbarSlotEvent>,
|
||||
mut send_packet_events: EventWriter<SendPacketEvent>,
|
||||
mut query: Query<&mut InventoryComponent>,
|
||||
) {
|
||||
for event in events.read() {
|
||||
let mut inventory = query.get_mut(event.entity).unwrap();
|
||||
|
||||
// if the slot is already selected, don't send a packet
|
||||
if inventory.selected_hotbar_slot == event.slot {
|
||||
continue;
|
||||
}
|
||||
|
||||
inventory.selected_hotbar_slot = event.slot;
|
||||
send_packet_events.send(SendPacketEvent {
|
||||
entity: event.entity,
|
||||
packet: ServerboundSetCarriedItemPacket {
|
||||
slot: event.slot as u16,
|
||||
}
|
||||
.get(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -200,7 +200,20 @@ fn handle_start_mining_block_with_direction_event(
|
|||
.get_block_state(&event.position)
|
||||
.unwrap_or_default();
|
||||
*sequence_number += 1;
|
||||
let block_is_solid = !target_block_state.is_air();
|
||||
let target_registry_block = azalea_registry::Block::from(target_block_state);
|
||||
|
||||
// we can't break blocks if they don't have a bounding box
|
||||
|
||||
// TODO: So right now azalea doesn't differenciate between different types of
|
||||
// bounding boxes. See ClipContext::block_shape for more info. Ideally this
|
||||
// should just call ClipContext::block_shape and check if it's empty.
|
||||
let block_is_solid = !target_block_state.is_air()
|
||||
// this is a hack to make sure we can't break water or lava
|
||||
&& !matches!(
|
||||
target_registry_block,
|
||||
azalea_registry::Block::Water | azalea_registry::Block::Lava
|
||||
);
|
||||
|
||||
if block_is_solid && **mine_progress == 0. {
|
||||
// interact with the block (like note block left click) here
|
||||
attack_block_events.send(AttackBlockEvent {
|
||||
|
|
|
@ -13,15 +13,15 @@ pub static SIN: LazyLock<[f32; 65536]> = LazyLock::new(|| {
|
|||
/// A sine function that uses a lookup table.
|
||||
pub fn sin(x: f32) -> f32 {
|
||||
let x = x * 10430.378;
|
||||
let x = x as usize;
|
||||
SIN[x & 65535]
|
||||
let x = x as i32 as usize & 65535;
|
||||
SIN[x]
|
||||
}
|
||||
|
||||
/// A cosine function that uses a lookup table.
|
||||
pub fn cos(x: f32) -> f32 {
|
||||
let x = x * 10430.378 + 16384.0;
|
||||
let x = x as usize;
|
||||
SIN[x & 65535]
|
||||
let x = x as i32 as usize & 65535;
|
||||
SIN[x]
|
||||
}
|
||||
|
||||
// TODO: make this generic
|
||||
|
@ -83,4 +83,24 @@ mod tests {
|
|||
assert_eq!(gcd(12, 7), 1);
|
||||
assert_eq!(gcd(7, 12), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sin() {
|
||||
const PI: f32 = std::f32::consts::PI;
|
||||
// check that they're close enough
|
||||
fn assert_sin_eq_enough(number: f32) {
|
||||
let a = sin(number);
|
||||
let b = f32::sin(number);
|
||||
assert!((a - b).abs() < 0.01, "sin({number}) failed, {a} != {b}");
|
||||
}
|
||||
assert_sin_eq_enough(0.0);
|
||||
assert_sin_eq_enough(PI / 2.0);
|
||||
assert_sin_eq_enough(PI);
|
||||
assert_sin_eq_enough(PI * 2.0);
|
||||
assert_sin_eq_enough(PI * 3.0 / 2.0);
|
||||
assert_sin_eq_enough(-PI / 2.0);
|
||||
assert_sin_eq_enough(-PI);
|
||||
assert_sin_eq_enough(-PI * 2.0);
|
||||
assert_sin_eq_enough(-PI * 3.0 / 2.0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
use azalea_buf::{BufReadError, McBuf, McBufReadable, McBufWritable};
|
||||
use std::{
|
||||
fmt,
|
||||
hash::Hash,
|
||||
io::{Cursor, Write},
|
||||
ops::{Add, AddAssign, Mul, Rem, Sub},
|
||||
|
@ -65,6 +66,43 @@ macro_rules! vec3_impl {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return a new instance of this position with the z coordinate subtracted
|
||||
/// by the given number.
|
||||
pub fn north(&self, z: $type) -> Self {
|
||||
Self {
|
||||
x: self.x,
|
||||
y: self.y,
|
||||
z: self.z - z,
|
||||
}
|
||||
}
|
||||
/// Return a new instance of this position with the x coordinate increased
|
||||
/// by the given number.
|
||||
pub fn east(&self, x: $type) -> Self {
|
||||
Self {
|
||||
x: self.x + x,
|
||||
y: self.y,
|
||||
z: self.z,
|
||||
}
|
||||
}
|
||||
/// Return a new instance of this position with the z coordinate increased
|
||||
/// by the given number.
|
||||
pub fn south(&self, z: $type) -> Self {
|
||||
Self {
|
||||
x: self.x,
|
||||
y: self.y,
|
||||
z: self.z + z,
|
||||
}
|
||||
}
|
||||
/// Return a new instance of this position with the x coordinate subtracted
|
||||
/// by the given number.
|
||||
pub fn west(&self, x: $type) -> Self {
|
||||
Self {
|
||||
x: self.x - x,
|
||||
y: self.y,
|
||||
z: self.z,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn dot(&self, other: Self) -> $type {
|
||||
self.x * other.x + self.y * other.y + self.z * other.z
|
||||
|
@ -501,6 +539,13 @@ impl From<Vec3> for ChunkBlockPos {
|
|||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for BlockPos {
|
||||
/// Display a block position as `x y z`.
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{} {} {}", self.x, self.y, self.z)
|
||||
}
|
||||
}
|
||||
|
||||
const PACKED_X_LENGTH: u64 = 1 + 25; // minecraft does something a bit more complicated to get this 25
|
||||
const PACKED_Z_LENGTH: u64 = PACKED_X_LENGTH;
|
||||
const PACKED_Y_LENGTH: u64 = 64 - PACKED_X_LENGTH - PACKED_Z_LENGTH;
|
||||
|
|
|
@ -34,7 +34,7 @@ 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.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChunkStorage {
|
||||
pub height: u32,
|
||||
pub min_y: i32,
|
||||
|
@ -514,7 +514,12 @@ impl Default for ChunkStorage {
|
|||
/// and the minimum y coordinate of the world.
|
||||
#[inline]
|
||||
pub fn section_index(y: i32, min_y: i32) -> u32 {
|
||||
assert!(y >= min_y, "y ({y}) must be at least {min_y}");
|
||||
if y < min_y {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("y ({y}) must be at most {min_y}");
|
||||
#[cfg(not(debug_assertions))]
|
||||
tracing::error!("y ({y}) must be at least {min_y}")
|
||||
};
|
||||
let min_section_index = min_y >> 4;
|
||||
((y >> 4) - min_section_index) as u32
|
||||
}
|
||||
|
|
306
azalea-world/src/find_blocks.rs
Normal file
306
azalea-world/src/find_blocks.rs
Normal file
|
@ -0,0 +1,306 @@
|
|||
use azalea_block::{BlockState, BlockStates};
|
||||
use azalea_core::position::{BlockPos, ChunkPos};
|
||||
|
||||
use crate::{iterators::ChunkIterator, palette::Palette, ChunkStorage, Instance};
|
||||
|
||||
fn palette_maybe_has_block(palette: &Palette, block_states: &BlockStates) -> bool {
|
||||
match &palette {
|
||||
Palette::SingleValue(id) => block_states.contains(&BlockState { id: *id }),
|
||||
Palette::Linear(ids) => ids
|
||||
.iter()
|
||||
.any(|&id| block_states.contains(&BlockState { id })),
|
||||
Palette::Hashmap(ids) => ids
|
||||
.iter()
|
||||
.any(|&id| block_states.contains(&BlockState { id })),
|
||||
Palette::Global => true,
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
///
|
||||
/// ```
|
||||
/// # fn example(client: &azalea_client::Client) {
|
||||
/// client.world().read().find_block(client.position(), &azalea_registry::Block::Chest.into());
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn find_block(
|
||||
&self,
|
||||
nearest_to: impl Into<BlockPos>,
|
||||
block_states: &BlockStates,
|
||||
) -> Option<BlockPos> {
|
||||
// iterate over every chunk in a 3d spiral pattern
|
||||
// and then check the palette for the block state
|
||||
|
||||
let nearest_to: BlockPos = nearest_to.into();
|
||||
let start_chunk: ChunkPos = (&nearest_to).into();
|
||||
let mut iter = ChunkIterator::new(start_chunk, 32);
|
||||
|
||||
let mut nearest_found_pos: Option<BlockPos> = None;
|
||||
let mut nearest_found_distance = 0;
|
||||
|
||||
// we do `while` instead of `for` so we can access iter later
|
||||
while let Some(chunk_pos) = iter.next() {
|
||||
let Some(chunk) = self.chunks.get(&chunk_pos) else {
|
||||
// if the chunk isn't loaded then we skip it.
|
||||
// we don't just return since it *could* cause issues if there's a random
|
||||
// unloaded chunk and then more that are loaded.
|
||||
// unlikely but still something to consider, and it's not like this slows it
|
||||
// down much anyways.
|
||||
continue;
|
||||
};
|
||||
|
||||
for (section_index, section) in chunk.read().sections.iter().enumerate() {
|
||||
let maybe_has_block =
|
||||
palette_maybe_has_block(§ion.states.palette, block_states);
|
||||
if !maybe_has_block {
|
||||
continue;
|
||||
}
|
||||
|
||||
for i in 0..4096 {
|
||||
let block_state = section.states.get_at_index(i);
|
||||
let block_state = BlockState { id: block_state };
|
||||
|
||||
if block_states.contains(&block_state) {
|
||||
let (section_x, section_y, section_z) = section.states.coords_from_index(i);
|
||||
let (x, y, z) = (
|
||||
chunk_pos.x * 16 + (section_x as i32),
|
||||
self.chunks.min_y + (section_index * 16) as i32 + section_y as i32,
|
||||
chunk_pos.z * 16 + (section_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(nearest_found_pos) = nearest_found_pos {
|
||||
// this is required because find_block searches chunk-by-chunk, which can cause
|
||||
// us to find blocks first that aren't actually the closest
|
||||
let required_chunk_distance = u32::max(
|
||||
u32::max(
|
||||
(chunk_pos.x - start_chunk.x).unsigned_abs(),
|
||||
(chunk_pos.z - start_chunk.z).unsigned_abs(),
|
||||
),
|
||||
(nearest_to.y - nearest_found_pos.y)
|
||||
.unsigned_abs()
|
||||
.div_ceil(16),
|
||||
) + 1;
|
||||
let nearest_chunk_distance = iter.layer;
|
||||
|
||||
// if we found the position and there's no chance there's something closer,
|
||||
// return it
|
||||
if nearest_chunk_distance > required_chunk_distance {
|
||||
return Some(nearest_found_pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if nearest_found_pos.is_some() {
|
||||
nearest_found_pos
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Find all the coordinates of a block in the world.
|
||||
///
|
||||
/// This returns an iterator that yields the [`BlockPos`]s of blocks that
|
||||
/// are in the given block states. It's sorted by `x+y+z`.
|
||||
pub fn find_blocks<'a>(
|
||||
&'a self,
|
||||
nearest_to: impl Into<BlockPos>,
|
||||
block_states: &'a BlockStates,
|
||||
) -> FindBlocks<'a> {
|
||||
FindBlocks::new(nearest_to.into(), &self.chunks, block_states)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FindBlocks<'a> {
|
||||
nearest_to: BlockPos,
|
||||
start_chunk: ChunkPos,
|
||||
chunk_iterator: ChunkIterator,
|
||||
chunks: &'a ChunkStorage,
|
||||
block_states: &'a BlockStates,
|
||||
|
||||
queued: Vec<BlockPos>,
|
||||
}
|
||||
|
||||
impl<'a> FindBlocks<'a> {
|
||||
pub fn new(
|
||||
nearest_to: BlockPos,
|
||||
chunks: &'a ChunkStorage,
|
||||
block_states: &'a BlockStates,
|
||||
) -> Self {
|
||||
let start_chunk: ChunkPos = (&nearest_to).into();
|
||||
Self {
|
||||
nearest_to,
|
||||
start_chunk,
|
||||
chunk_iterator: ChunkIterator::new(start_chunk, 32),
|
||||
chunks,
|
||||
block_states,
|
||||
|
||||
queued: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for FindBlocks<'a> {
|
||||
type Item = BlockPos;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(queued) = self.queued.pop() {
|
||||
return Some(queued);
|
||||
}
|
||||
|
||||
let mut found = Vec::new();
|
||||
|
||||
let mut nearest_found_pos: Option<BlockPos> = None;
|
||||
let mut nearest_found_distance = 0;
|
||||
|
||||
while let Some(chunk_pos) = self.chunk_iterator.next() {
|
||||
let Some(chunk) = self.chunks.get(&chunk_pos) else {
|
||||
// if the chunk isn't loaded then we skip it.
|
||||
// we don't just return since it *could* cause issues if there's a random
|
||||
// unloaded chunk and then more that are loaded.
|
||||
// unlikely but still something to consider, and it's not like this slows it
|
||||
// down much anyways.
|
||||
continue;
|
||||
};
|
||||
|
||||
for (section_index, section) in chunk.read().sections.iter().enumerate() {
|
||||
let maybe_has_block =
|
||||
palette_maybe_has_block(§ion.states.palette, self.block_states);
|
||||
if !maybe_has_block {
|
||||
continue;
|
||||
}
|
||||
|
||||
for i in 0..4096 {
|
||||
let block_state = section.states.get_at_index(i);
|
||||
let block_state = BlockState { id: block_state };
|
||||
|
||||
if self.block_states.contains(&block_state) {
|
||||
let (section_x, section_y, section_z) = section.states.coords_from_index(i);
|
||||
let (x, y, z) = (
|
||||
chunk_pos.x * 16 + (section_x as i32),
|
||||
self.chunks.min_y + (section_index * 16) as i32 + section_y as i32,
|
||||
chunk_pos.z * 16 + (section_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 let Some(nearest_found_pos) = nearest_found_pos {
|
||||
// this is required because find_block searches chunk-by-chunk, which can cause
|
||||
// us to find blocks first that aren't actually the closest
|
||||
let required_chunk_distance = u32::max(
|
||||
u32::max(
|
||||
(chunk_pos.x - self.start_chunk.x).unsigned_abs(),
|
||||
(chunk_pos.z - self.start_chunk.z).unsigned_abs(),
|
||||
),
|
||||
(self.nearest_to.y - nearest_found_pos.y)
|
||||
.unsigned_abs()
|
||||
.div_ceil(16),
|
||||
) + 1;
|
||||
let nearest_chunk_distance = self.chunk_iterator.layer;
|
||||
|
||||
// if we found the position and there's no chance there's something closer,
|
||||
// return it
|
||||
if nearest_chunk_distance > required_chunk_distance {
|
||||
// sort so nearest is at the end
|
||||
found.sort_unstable_by_key(|(_, distance)| u32::MAX - distance);
|
||||
|
||||
self.queued = found.into_iter().map(|(pos, _)| pos).collect();
|
||||
return self.queued.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use azalea_registry::Block;
|
||||
|
||||
use crate::{Chunk, PartialChunkStorage};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn find_block() {
|
||||
let mut instance = Instance::default();
|
||||
|
||||
let chunk_storage = &mut instance.chunks;
|
||||
let mut partial_chunk_storage = PartialChunkStorage::default();
|
||||
|
||||
// block at (17, 0, 0) and (0, 18, 0)
|
||||
|
||||
partial_chunk_storage.set(
|
||||
&ChunkPos { x: 0, z: 0 },
|
||||
Some(Chunk::default()),
|
||||
chunk_storage,
|
||||
);
|
||||
partial_chunk_storage.set(
|
||||
&ChunkPos { x: 1, z: 0 },
|
||||
Some(Chunk::default()),
|
||||
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());
|
||||
|
||||
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 }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_block_next_to_chunk_border() {
|
||||
let mut instance = Instance::default();
|
||||
|
||||
let chunk_storage = &mut instance.chunks;
|
||||
let mut partial_chunk_storage = PartialChunkStorage::default();
|
||||
|
||||
// block at (-1, 0, 0) and (15, 0, 0)
|
||||
|
||||
partial_chunk_storage.set(
|
||||
&ChunkPos { x: -1, z: 0 },
|
||||
Some(Chunk::default()),
|
||||
chunk_storage,
|
||||
);
|
||||
partial_chunk_storage.set(
|
||||
&ChunkPos { x: 0, z: 0 },
|
||||
Some(Chunk::default()),
|
||||
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());
|
||||
|
||||
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 }));
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
mod bit_storage;
|
||||
pub mod chunk_storage;
|
||||
mod container;
|
||||
pub mod find_blocks;
|
||||
pub mod heightmap;
|
||||
pub mod iterators;
|
||||
pub mod palette;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{iterators::ChunkIterator, palette::Palette, ChunkStorage, PartialChunkStorage};
|
||||
use azalea_block::{BlockState, BlockStates, FluidState};
|
||||
use crate::{ChunkStorage, PartialChunkStorage};
|
||||
use azalea_block::{BlockState, FluidState};
|
||||
use azalea_core::position::{BlockPos, ChunkPos};
|
||||
use azalea_core::registry_holder::RegistryHolder;
|
||||
use bevy_ecs::{component::Component, entity::Entity};
|
||||
|
@ -104,110 +104,6 @@ impl Instance {
|
|||
pub fn set_block_state(&self, pos: &BlockPos, state: BlockState) -> Option<BlockState> {
|
||||
self.chunks.set_block_state(pos, state)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// ```
|
||||
/// # fn example(client: &azalea_client::Client) {
|
||||
/// client.world().read().find_block(client.position(), &azalea_registry::Block::Chest.into());
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn find_block(
|
||||
&self,
|
||||
nearest_to: impl Into<BlockPos>,
|
||||
block_states: &BlockStates,
|
||||
) -> Option<BlockPos> {
|
||||
// iterate over every chunk in a 3d spiral pattern
|
||||
// and then check the palette for the block state
|
||||
|
||||
let nearest_to: BlockPos = nearest_to.into();
|
||||
let start_chunk: ChunkPos = (&nearest_to).into();
|
||||
let mut iter = ChunkIterator::new(start_chunk, 32);
|
||||
|
||||
let mut nearest_found_pos: Option<BlockPos> = None;
|
||||
let mut nearest_found_distance = 0;
|
||||
|
||||
// we do `while` instead of `for` so we can access iter later
|
||||
while let Some(chunk_pos) = iter.next() {
|
||||
let Some(chunk) = self.chunks.get(&chunk_pos) else {
|
||||
// if the chunk isn't loaded then we skip it.
|
||||
// we don't just return since it *could* cause issues if there's a random
|
||||
// unloaded chunk and then more that are loaded.
|
||||
// unlikely but still something to consider, and it's not like this slows it
|
||||
// down much anyways.
|
||||
continue;
|
||||
};
|
||||
|
||||
for (section_index, section) in chunk.read().sections.iter().enumerate() {
|
||||
let maybe_has_block = match §ion.states.palette {
|
||||
Palette::SingleValue(id) => block_states.contains(&BlockState { id: *id }),
|
||||
Palette::Linear(ids) => ids
|
||||
.iter()
|
||||
.any(|&id| block_states.contains(&BlockState { id })),
|
||||
Palette::Hashmap(ids) => ids
|
||||
.iter()
|
||||
.any(|&id| block_states.contains(&BlockState { id })),
|
||||
Palette::Global => true,
|
||||
};
|
||||
if !maybe_has_block {
|
||||
continue;
|
||||
}
|
||||
|
||||
for i in 0..4096 {
|
||||
let block_state = section.states.get_at_index(i);
|
||||
let block_state = BlockState { id: block_state };
|
||||
|
||||
if block_states.contains(&block_state) {
|
||||
let (section_x, section_y, section_z) = section.states.coords_from_index(i);
|
||||
let (x, y, z) = (
|
||||
chunk_pos.x * 16 + (section_x as i32),
|
||||
self.chunks.min_y + (section_index * 16) as i32 + section_y as i32,
|
||||
chunk_pos.z * 16 + (section_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(nearest_found_pos) = nearest_found_pos {
|
||||
// this is required because find_block searches chunk-by-chunk, which can cause
|
||||
// us to find blocks first that aren't actually the closest
|
||||
let required_chunk_distance = u32::max(
|
||||
u32::max(
|
||||
(chunk_pos.x - start_chunk.x).unsigned_abs(),
|
||||
(chunk_pos.z - start_chunk.z).unsigned_abs(),
|
||||
),
|
||||
(nearest_to.y - nearest_found_pos.y)
|
||||
.unsigned_abs()
|
||||
.div_ceil(16),
|
||||
);
|
||||
let nearest_chunk_distance = iter.layer;
|
||||
|
||||
// if we found the position and there's no chance there's something closer,
|
||||
// return it
|
||||
if nearest_chunk_distance >= required_chunk_distance {
|
||||
return Some(nearest_found_pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if nearest_found_pos.is_some() {
|
||||
nearest_found_pos
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for PartialInstance {
|
||||
|
@ -244,39 +140,3 @@ impl From<ChunkStorage> for Instance {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use azalea_registry::Block;
|
||||
|
||||
use crate::Chunk;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn find_block() {
|
||||
let mut instance = Instance::default();
|
||||
|
||||
let chunk_storage = &mut instance.chunks;
|
||||
let mut partial_chunk_storage = PartialChunkStorage::default();
|
||||
|
||||
// block at (17, 0, 0) and (0, 18, 0)
|
||||
|
||||
partial_chunk_storage.set(
|
||||
&ChunkPos { x: 0, z: 0 },
|
||||
Some(Chunk::default()),
|
||||
chunk_storage,
|
||||
);
|
||||
partial_chunk_storage.set(
|
||||
&ChunkPos { x: 1, z: 0 },
|
||||
Some(Chunk::default()),
|
||||
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());
|
||||
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,17 +3,16 @@ use std::{hint::black_box, sync::Arc, time::Duration};
|
|||
use azalea::{
|
||||
pathfinder::{
|
||||
astar::{self, a_star},
|
||||
goals::BlockPosGoal,
|
||||
goals::{BlockPosGoal, Goal},
|
||||
mining::MiningCache,
|
||||
world::CachedWorld,
|
||||
Goal,
|
||||
},
|
||||
BlockPos,
|
||||
};
|
||||
use azalea_core::position::{ChunkBlockPos, ChunkPos};
|
||||
use azalea_inventory::Menu;
|
||||
use azalea_world::{Chunk, ChunkStorage, PartialChunkStorage};
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use criterion::{criterion_group, criterion_main, Bencher, Criterion};
|
||||
use parking_lot::RwLock;
|
||||
use rand::{rngs::StdRng, Rng, SeedableRng};
|
||||
|
||||
|
@ -58,14 +57,14 @@ fn generate_bedrock_world(
|
|||
}
|
||||
|
||||
let mut start = BlockPos::new(-64, 4, -64);
|
||||
// move start down until it's on bedrock
|
||||
// move start down until it's on a solid block
|
||||
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 bedrock
|
||||
// move end down until it's on a solid block
|
||||
while chunks.get_block_state(&end).unwrap().is_air() {
|
||||
end = end.down(1);
|
||||
}
|
||||
|
@ -74,37 +73,90 @@ fn generate_bedrock_world(
|
|||
(chunks, start, end)
|
||||
}
|
||||
|
||||
fn generate_mining_world(
|
||||
partial_chunks: &mut PartialChunkStorage,
|
||||
size: u32,
|
||||
) -> (ChunkStorage, BlockPos, BlockPos) {
|
||||
let size = size as i32;
|
||||
|
||||
let mut chunks = ChunkStorage::default();
|
||||
for chunk_x in -size..size {
|
||||
for chunk_z in -size..size {
|
||||
let chunk_pos = ChunkPos::new(chunk_x, chunk_z);
|
||||
partial_chunks.set(&chunk_pos, Some(Chunk::default()), &mut chunks);
|
||||
}
|
||||
}
|
||||
|
||||
let mut rng = StdRng::seed_from_u64(0);
|
||||
|
||||
for chunk_x in -size..size {
|
||||
for chunk_z in -size..size {
|
||||
let chunk_pos = ChunkPos::new(chunk_x, chunk_z);
|
||||
let chunk = chunks.get(&chunk_pos).unwrap();
|
||||
let mut chunk = chunk.write();
|
||||
for y in chunks.min_y..(chunks.min_y + chunks.height as i32) {
|
||||
for x in 0..16_u8 {
|
||||
for z in 0..16_u8 {
|
||||
chunk.set(
|
||||
&ChunkBlockPos::new(x, y, z),
|
||||
azalea_registry::Block::Stone.into(),
|
||||
chunks.min_y,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let start = BlockPos::new(-64, 4, -64);
|
||||
let end = BlockPos::new(0, 4, 0);
|
||||
|
||||
(chunks, start, end)
|
||||
}
|
||||
|
||||
fn run_pathfinder_benchmark(
|
||||
b: &mut Bencher<'_>,
|
||||
generate_world: fn(&mut PartialChunkStorage, u32) -> (ChunkStorage, BlockPos, BlockPos),
|
||||
) {
|
||||
let mut partial_chunks = PartialChunkStorage::new(32);
|
||||
let successors_fn = azalea::pathfinder::moves::default_move;
|
||||
|
||||
let (world, start, end) = generate_world(&mut partial_chunks, 4);
|
||||
|
||||
b.iter(|| {
|
||||
let cached_world = CachedWorld::new(Arc::new(RwLock::new(world.clone().into())));
|
||||
let mining_cache =
|
||||
MiningCache::new(Some(Menu::Player(azalea_inventory::Player::default())));
|
||||
let goal = BlockPosGoal(end);
|
||||
|
||||
let successors = |pos: BlockPos| {
|
||||
azalea::pathfinder::call_successors_fn(&cached_world, &mining_cache, successors_fn, pos)
|
||||
};
|
||||
|
||||
let astar::Path { movements, partial } = a_star(
|
||||
start,
|
||||
|n| goal.heuristic(n),
|
||||
successors,
|
||||
|n| goal.success(n),
|
||||
Duration::MAX,
|
||||
);
|
||||
|
||||
assert!(!partial);
|
||||
|
||||
black_box((movements, partial));
|
||||
})
|
||||
}
|
||||
|
||||
fn bench_pathfinder(c: &mut Criterion) {
|
||||
c.bench_function("bedrock", |b| {
|
||||
let mut partial_chunks = PartialChunkStorage::new(32);
|
||||
let successors_fn = azalea::pathfinder::moves::default_move;
|
||||
|
||||
b.iter(|| {
|
||||
let (world, start, end) = generate_bedrock_world(&mut partial_chunks, 4);
|
||||
let cached_world = CachedWorld::new(Arc::new(RwLock::new(world.into())));
|
||||
let mining_cache = MiningCache::new(Menu::Player(azalea_inventory::Player::default()));
|
||||
let goal = BlockPosGoal(end);
|
||||
|
||||
let successors = |pos: BlockPos| {
|
||||
azalea::pathfinder::call_successors_fn(
|
||||
&cached_world,
|
||||
&mining_cache,
|
||||
successors_fn,
|
||||
pos,
|
||||
)
|
||||
};
|
||||
|
||||
let astar::Path { movements, partial } = a_star(
|
||||
start,
|
||||
|n| goal.heuristic(n),
|
||||
successors,
|
||||
|n| goal.success(n),
|
||||
Duration::MAX,
|
||||
);
|
||||
|
||||
black_box((movements, partial));
|
||||
})
|
||||
// c.bench_function("bedrock", |b| {
|
||||
// run_pathfinder_benchmark(b, generate_bedrock_world);
|
||||
// });
|
||||
let mut slow_group = c.benchmark_group("slow");
|
||||
slow_group.sample_size(10);
|
||||
slow_group.bench_function("mining", |b| {
|
||||
run_pathfinder_benchmark(b, generate_mining_world);
|
||||
});
|
||||
slow_group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_pathfinder);
|
||||
|
|
|
@ -4,6 +4,7 @@ use azalea_entity::{FluidOnEyes, Physics};
|
|||
use azalea_inventory::{ItemSlot, Menu};
|
||||
use azalea_registry::Fluid;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BestToolResult {
|
||||
pub index: usize,
|
||||
pub percentage_per_tick: f32,
|
||||
|
@ -62,7 +63,57 @@ pub fn accurate_best_tool_in_hotbar_for_block(
|
|||
let mut best_slot = None;
|
||||
|
||||
let block = Box::<dyn Block>::from(block);
|
||||
let registry_block = block.as_registry_block();
|
||||
|
||||
if matches!(
|
||||
registry_block,
|
||||
azalea_registry::Block::Water | azalea_registry::Block::Lava
|
||||
) {
|
||||
// can't mine fluids
|
||||
return BestToolResult {
|
||||
index: 0,
|
||||
percentage_per_tick: 0.,
|
||||
};
|
||||
}
|
||||
|
||||
// find the first slot that has an item without durability
|
||||
for (i, item_slot) in hotbar_slots.iter().enumerate() {
|
||||
let this_item_speed;
|
||||
match item_slot {
|
||||
ItemSlot::Empty => {
|
||||
this_item_speed = Some(azalea_entity::mining::get_mine_progress(
|
||||
block.as_ref(),
|
||||
azalea_registry::Item::Air,
|
||||
menu,
|
||||
fluid_on_eyes,
|
||||
physics,
|
||||
));
|
||||
}
|
||||
ItemSlot::Present(item_slot) => {
|
||||
// lazy way to avoid checking durability since azalea doesn't have durability
|
||||
// data yet
|
||||
if item_slot.nbt.is_none() {
|
||||
this_item_speed = Some(azalea_entity::mining::get_mine_progress(
|
||||
block.as_ref(),
|
||||
item_slot.kind,
|
||||
menu,
|
||||
fluid_on_eyes,
|
||||
physics,
|
||||
));
|
||||
} else {
|
||||
this_item_speed = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(this_item_speed) = this_item_speed {
|
||||
if this_item_speed > best_speed {
|
||||
best_slot = Some(i);
|
||||
best_speed = this_item_speed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now check every item
|
||||
for (i, item_slot) in hotbar_slots.iter().enumerate() {
|
||||
if let ItemSlot::Present(item_slot) = item_slot {
|
||||
let this_item_speed = azalea_entity::mining::get_mine_progress(
|
||||
|
|
|
@ -170,27 +170,35 @@ fn look_at_listener(
|
|||
) {
|
||||
for event in events.read() {
|
||||
if let Ok((position, eye_height, mut look_direction)) = query.get_mut(event.entity) {
|
||||
let (y_rot, x_rot) =
|
||||
let new_look_direction =
|
||||
direction_looking_at(&position.up(eye_height.into()), &event.position);
|
||||
trace!(
|
||||
"look at {:?} (currently at {:?})",
|
||||
event.position,
|
||||
**position
|
||||
);
|
||||
(look_direction.y_rot, look_direction.x_rot) = (y_rot, x_rot);
|
||||
*look_direction = new_look_direction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the (`y_rot`, `x_rot`) that would make a client at `current` be
|
||||
/// Return the look direction that would make a client at `current` be
|
||||
/// looking at `target`.
|
||||
fn direction_looking_at(current: &Vec3, target: &Vec3) -> (f32, f32) {
|
||||
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);
|
||||
let ground_distance = f64::sqrt(delta.x * delta.x + delta.z * delta.z);
|
||||
let x_rot = f64::atan2(delta.y, ground_distance) * -(180.0 / PI);
|
||||
(y_rot as f32, x_rot as f32)
|
||||
|
||||
// clamp
|
||||
let y_rot = y_rot.rem_euclid(360.0);
|
||||
let x_rot = x_rot.clamp(-90.0, 90.0) % 360.0;
|
||||
|
||||
LookDirection {
|
||||
x_rot: x_rot as f32,
|
||||
y_rot: y_rot as f32,
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`PluginGroup`] for the plugins that add extra bot functionality to the
|
||||
|
|
|
@ -48,7 +48,7 @@ where
|
|||
movement_data: None,
|
||||
came_from: None,
|
||||
g_score: f32::default(),
|
||||
f_score: f32::MAX,
|
||||
f_score: f32::INFINITY,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -70,14 +70,14 @@ where
|
|||
let current_g_score = nodes
|
||||
.get(¤t_node)
|
||||
.map(|n| n.g_score)
|
||||
.unwrap_or(f32::MAX);
|
||||
.unwrap_or(f32::INFINITY);
|
||||
|
||||
for neighbor in successors(current_node) {
|
||||
let tentative_g_score = current_g_score + neighbor.cost;
|
||||
let neighbor_g_score = nodes
|
||||
.get(&neighbor.movement.target)
|
||||
.map(|n| n.g_score)
|
||||
.unwrap_or(f32::MAX);
|
||||
.unwrap_or(f32::INFINITY);
|
||||
if tentative_g_score - neighbor_g_score < MIN_IMPROVEMENT {
|
||||
let heuristic = heuristic(neighbor.movement.target);
|
||||
let f_score = tentative_g_score + heuristic;
|
||||
|
|
|
@ -10,6 +10,15 @@ pub const SPRINT_MULTIPLIER: f32 = SPRINT_ONE_BLOCK_COST / WALK_ONE_BLOCK_COST;
|
|||
pub const JUMP_PENALTY: f32 = 2.;
|
||||
pub const CENTER_AFTER_FALL_COST: f32 = WALK_ONE_BLOCK_COST - WALK_OFF_BLOCK_COST; // 0.927
|
||||
|
||||
// explanation here:
|
||||
// https://github.com/cabaletta/baritone/blob/f147519a5c291015d4f18c94558a3f1bdcdb9588/src/api/java/baritone/api/Settings.java#L405
|
||||
// it's basically just the heuristic multiplier
|
||||
pub const COST_HEURISTIC: f32 = 3.563;
|
||||
|
||||
// this one is also from baritone, it's helpful as a tiebreaker to avoid
|
||||
// breaking blocks if it can be avoided
|
||||
pub const BLOCK_BREAK_ADDITIONAL_PENALTY: f32 = 2.;
|
||||
|
||||
pub static FALL_1_25_BLOCKS_COST: LazyLock<f32> = LazyLock::new(|| distance_to_ticks(1.25));
|
||||
pub static FALL_0_25_BLOCKS_COST: LazyLock<f32> = LazyLock::new(|| distance_to_ticks(0.25));
|
||||
pub static JUMP_ONE_BLOCK_COST: LazyLock<f32> =
|
||||
|
|
113
azalea/src/pathfinder/debug.rs
Normal file
113
azalea/src/pathfinder/debug.rs
Normal file
|
@ -0,0 +1,113 @@
|
|||
use azalea_client::{chat::SendChatEvent, InstanceHolder};
|
||||
use azalea_core::position::Vec3;
|
||||
use bevy_ecs::prelude::*;
|
||||
|
||||
use super::ExecutingPath;
|
||||
|
||||
/// A component that makes bots run /particle commands while pathfinding to show
|
||||
/// where they're going. This requires the bots to have server operator
|
||||
/// permissions, and it'll make them spam *a lot* of commands.
|
||||
///
|
||||
/// ```
|
||||
/// # use azalea::prelude::*;
|
||||
/// # use azalea::pathfinder::PathfinderDebugParticles;
|
||||
/// # #[derive(Component, Clone, Default)]
|
||||
/// # pub struct State;
|
||||
///
|
||||
/// async fn handle(mut bot: Client, event: azalea::Event, state: State) -> anyhow::Result<()> {
|
||||
/// match event {
|
||||
/// azalea::Event::Init => {
|
||||
/// bot.ecs
|
||||
/// .lock()
|
||||
/// .entity_mut(bot.entity)
|
||||
/// .insert(PathfinderDebugParticles);
|
||||
/// }
|
||||
/// _ => {}
|
||||
/// }
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Component)]
|
||||
pub struct PathfinderDebugParticles;
|
||||
|
||||
pub fn debug_render_path_with_particles(
|
||||
mut query: Query<(Entity, &ExecutingPath, &InstanceHolder), With<PathfinderDebugParticles>>,
|
||||
// chat_events is Option because the tests don't have SendChatEvent
|
||||
// and we have to use ResMut<Events> because bevy doesn't support Option<EventWriter>
|
||||
chat_events: Option<ResMut<Events<SendChatEvent>>>,
|
||||
mut tick_count: Local<usize>,
|
||||
) {
|
||||
let Some(mut chat_events) = chat_events else {
|
||||
return;
|
||||
};
|
||||
if *tick_count >= 2 {
|
||||
*tick_count = 0;
|
||||
} else {
|
||||
*tick_count += 1;
|
||||
return;
|
||||
}
|
||||
for (entity, executing_path, instance_holder) in &mut query {
|
||||
if executing_path.path.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let chunks = &instance_holder.instance.read().chunks;
|
||||
|
||||
let mut start = executing_path.last_reached_node;
|
||||
for (i, movement) in executing_path.path.iter().enumerate() {
|
||||
// /particle dust 0 1 1 1 ~ ~ ~ 0 0 0.2 0 100
|
||||
|
||||
let end = movement.target;
|
||||
|
||||
let start_vec3 = start.center();
|
||||
let end_vec3 = end.center();
|
||||
|
||||
let step_count = (start_vec3.distance_to_sqr(&end_vec3).sqrt() * 4.0) as usize;
|
||||
|
||||
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))
|
||||
.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
|
||||
// purposes
|
||||
let is_mining = !super::world::is_block_state_passable(target_block_state)
|
||||
|| !super::world::is_block_state_passable(above_target_block_state);
|
||||
|
||||
let (r, g, b): (f64, f64, f64) = if i == 0 {
|
||||
(0., 1., 0.)
|
||||
} else if is_mining {
|
||||
(1., 0., 0.)
|
||||
} else {
|
||||
(0., 1., 1.)
|
||||
};
|
||||
|
||||
// interpolate between the start and end positions
|
||||
for i in 0..step_count {
|
||||
let percent = i as f64 / step_count as f64;
|
||||
let pos = Vec3 {
|
||||
x: start_vec3.x + (end_vec3.x - start_vec3.x) * percent,
|
||||
y: start_vec3.y + (end_vec3.y - start_vec3.y) * percent,
|
||||
z: start_vec3.z + (end_vec3.z - start_vec3.z) * percent,
|
||||
};
|
||||
let particle_command = format!(
|
||||
"/particle dust {r} {g} {b} {size} {start_x} {start_y} {start_z} {delta_x} {delta_y} {delta_z} 0 {count}",
|
||||
size = 1,
|
||||
start_x = pos.x,
|
||||
start_y = pos.y,
|
||||
start_z = pos.z,
|
||||
delta_x = 0,
|
||||
delta_y = 0,
|
||||
delta_z = 0,
|
||||
count = 1
|
||||
);
|
||||
chat_events.send(SendChatEvent {
|
||||
entity,
|
||||
content: particle_command,
|
||||
});
|
||||
}
|
||||
|
||||
start = movement.target;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,21 @@
|
|||
//! The goals that a pathfinder can try to reach.
|
||||
|
||||
use std::f32::consts::SQRT_2;
|
||||
|
||||
use azalea_core::position::{BlockPos, Vec3};
|
||||
use azalea_world::ChunkStorage;
|
||||
|
||||
use super::{
|
||||
costs::{FALL_N_BLOCKS_COST, JUMP_ONE_BLOCK_COST},
|
||||
Goal,
|
||||
};
|
||||
use super::costs::{COST_HEURISTIC, FALL_N_BLOCKS_COST, JUMP_ONE_BLOCK_COST};
|
||||
|
||||
pub trait Goal {
|
||||
#[must_use]
|
||||
fn heuristic(&self, n: BlockPos) -> f32;
|
||||
#[must_use]
|
||||
fn success(&self, n: BlockPos) -> bool;
|
||||
}
|
||||
|
||||
/// Move to the given block position.
|
||||
#[derive(Debug)]
|
||||
pub struct BlockPosGoal(pub BlockPos);
|
||||
impl Goal for BlockPosGoal {
|
||||
fn heuristic(&self, n: BlockPos) -> f32 {
|
||||
|
@ -36,9 +45,11 @@ fn xz_heuristic(dx: f32, dz: f32) -> f32 {
|
|||
diagonal = z;
|
||||
}
|
||||
|
||||
diagonal * SQRT_2 + straight
|
||||
(diagonal * SQRT_2 + straight) * COST_HEURISTIC
|
||||
}
|
||||
|
||||
/// Move to the given block position, ignoring the y axis.
|
||||
#[derive(Debug)]
|
||||
pub struct XZGoal {
|
||||
pub x: i32,
|
||||
pub z: i32,
|
||||
|
@ -62,6 +73,8 @@ fn y_heuristic(dy: f32) -> f32 {
|
|||
}
|
||||
}
|
||||
|
||||
/// Move to the given y coordinate.
|
||||
#[derive(Debug)]
|
||||
pub struct YGoal {
|
||||
pub y: i32,
|
||||
}
|
||||
|
@ -75,6 +88,8 @@ impl Goal for YGoal {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get within the given radius of the given position.
|
||||
#[derive(Debug)]
|
||||
pub struct RadiusGoal {
|
||||
pub pos: Vec3,
|
||||
pub radius: f32,
|
||||
|
@ -96,6 +111,8 @@ impl Goal for RadiusGoal {
|
|||
}
|
||||
}
|
||||
|
||||
/// Do the opposite of the given goal.
|
||||
#[derive(Debug)]
|
||||
pub struct InverseGoal<T: Goal>(pub T);
|
||||
impl<T: Goal> Goal for InverseGoal<T> {
|
||||
fn heuristic(&self, n: BlockPos) -> f32 {
|
||||
|
@ -106,6 +123,8 @@ impl<T: Goal> Goal for InverseGoal<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Do either of the given goals, whichever is closer.
|
||||
#[derive(Debug)]
|
||||
pub struct OrGoal<T: Goal, U: Goal>(pub T, pub U);
|
||||
impl<T: Goal, U: Goal> Goal for OrGoal<T, U> {
|
||||
fn heuristic(&self, n: BlockPos) -> f32 {
|
||||
|
@ -116,6 +135,24 @@ impl<T: Goal, U: Goal> Goal for OrGoal<T, U> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Do any of the given goals, whichever is closest.
|
||||
#[derive(Debug)]
|
||||
pub struct OrGoals<T: Goal>(pub Vec<T>);
|
||||
impl<T: Goal> Goal for OrGoals<T> {
|
||||
fn heuristic(&self, n: BlockPos) -> f32 {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|goal| goal.heuristic(n))
|
||||
.min_by(|a, b| a.partial_cmp(b).unwrap())
|
||||
.unwrap_or(f32::INFINITY)
|
||||
}
|
||||
fn success(&self, n: BlockPos) -> bool {
|
||||
self.0.iter().any(|goal| goal.success(n))
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to reach both of the given goals.
|
||||
#[derive(Debug)]
|
||||
pub struct AndGoal<T: Goal, U: Goal>(pub T, pub U);
|
||||
impl<T: Goal, U: Goal> Goal for AndGoal<T, U> {
|
||||
fn heuristic(&self, n: BlockPos) -> f32 {
|
||||
|
@ -125,3 +162,52 @@ impl<T: Goal, U: Goal> Goal for AndGoal<T, U> {
|
|||
self.0.success(n) && self.1.success(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to reach all of the given goals.
|
||||
#[derive(Debug)]
|
||||
pub struct AndGoals<T: Goal>(pub Vec<T>);
|
||||
impl<T: Goal> Goal for AndGoals<T> {
|
||||
fn heuristic(&self, n: BlockPos) -> f32 {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|goal| goal.heuristic(n))
|
||||
.max_by(|a, b| a.partial_cmp(b).unwrap())
|
||||
.unwrap_or(f32::INFINITY)
|
||||
}
|
||||
fn success(&self, n: BlockPos) -> bool {
|
||||
self.0.iter().all(|goal| goal.success(n))
|
||||
}
|
||||
}
|
||||
|
||||
/// Move to a position where we can reach the given block.
|
||||
#[derive(Debug)]
|
||||
pub struct ReachBlockPosGoal {
|
||||
pub pos: BlockPos,
|
||||
pub chunk_storage: ChunkStorage,
|
||||
}
|
||||
impl Goal for ReachBlockPosGoal {
|
||||
fn heuristic(&self, n: BlockPos) -> f32 {
|
||||
BlockPosGoal(self.pos).heuristic(n)
|
||||
}
|
||||
fn success(&self, n: BlockPos) -> bool {
|
||||
// only do the expensive check if we're close enough
|
||||
let max_pick_range = 6;
|
||||
let actual_pick_range = 4.5;
|
||||
|
||||
let distance = (self.pos - n).length_sqr();
|
||||
if distance > max_pick_range * max_pick_range {
|
||||
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 block_hit_result = azalea_client::interact::pick(
|
||||
&look_direction,
|
||||
&eye_position,
|
||||
&self.chunk_storage,
|
||||
actual_pick_range,
|
||||
);
|
||||
|
||||
block_hit_result.block_pos == self.pos
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,109 @@
|
|||
use azalea_block::BlockState;
|
||||
use std::{cell::UnsafeCell, ops::RangeInclusive};
|
||||
|
||||
use azalea_block::{BlockState, BlockStates};
|
||||
use azalea_inventory::Menu;
|
||||
use nohash_hasher::IntMap;
|
||||
|
||||
use crate::auto_tool::best_tool_in_hotbar_for_block;
|
||||
|
||||
use super::costs::BLOCK_BREAK_ADDITIONAL_PENALTY;
|
||||
|
||||
pub struct MiningCache {
|
||||
block_state_id_costs: IntMap<u32, f32>,
|
||||
inventory_menu: Menu,
|
||||
block_state_id_costs: UnsafeCell<IntMap<u32, f32>>,
|
||||
inventory_menu: Option<Menu>,
|
||||
|
||||
water_block_state_range: RangeInclusive<u32>,
|
||||
lava_block_state_range: RangeInclusive<u32>,
|
||||
|
||||
falling_blocks: Vec<BlockState>,
|
||||
}
|
||||
|
||||
impl MiningCache {
|
||||
pub fn new(inventory_menu: Menu) -> Self {
|
||||
pub fn new(inventory_menu: Option<Menu>) -> Self {
|
||||
let water_block_states = BlockStates::from(azalea_registry::Block::Water);
|
||||
let lava_block_states = BlockStates::from(azalea_registry::Block::Lava);
|
||||
|
||||
let mut water_block_state_range_min = u32::MAX;
|
||||
let mut water_block_state_range_max = u32::MIN;
|
||||
for state in water_block_states {
|
||||
water_block_state_range_min = water_block_state_range_min.min(state.id);
|
||||
water_block_state_range_max = water_block_state_range_max.max(state.id);
|
||||
}
|
||||
let water_block_state_range = water_block_state_range_min..=water_block_state_range_max;
|
||||
|
||||
let mut lava_block_state_range_min = u32::MAX;
|
||||
let mut lava_block_state_range_max = u32::MIN;
|
||||
for state in lava_block_states {
|
||||
lava_block_state_range_min = lava_block_state_range_min.min(state.id);
|
||||
lava_block_state_range_max = lava_block_state_range_max.max(state.id);
|
||||
}
|
||||
let lava_block_state_range = lava_block_state_range_min..=lava_block_state_range_max;
|
||||
|
||||
let mut falling_blocks: Vec<BlockState> = vec![
|
||||
azalea_registry::Block::Sand.into(),
|
||||
azalea_registry::Block::RedSand.into(),
|
||||
azalea_registry::Block::Gravel.into(),
|
||||
azalea_registry::Block::Anvil.into(),
|
||||
azalea_registry::Block::ChippedAnvil.into(),
|
||||
azalea_registry::Block::DamagedAnvil.into(),
|
||||
// concrete powders
|
||||
azalea_registry::Block::WhiteConcretePowder.into(),
|
||||
azalea_registry::Block::OrangeConcretePowder.into(),
|
||||
azalea_registry::Block::MagentaConcretePowder.into(),
|
||||
azalea_registry::Block::LightBlueConcretePowder.into(),
|
||||
azalea_registry::Block::YellowConcretePowder.into(),
|
||||
azalea_registry::Block::LimeConcretePowder.into(),
|
||||
azalea_registry::Block::PinkConcretePowder.into(),
|
||||
azalea_registry::Block::GrayConcretePowder.into(),
|
||||
azalea_registry::Block::LightGrayConcretePowder.into(),
|
||||
azalea_registry::Block::CyanConcretePowder.into(),
|
||||
azalea_registry::Block::PurpleConcretePowder.into(),
|
||||
azalea_registry::Block::BlueConcretePowder.into(),
|
||||
azalea_registry::Block::BrownConcretePowder.into(),
|
||||
azalea_registry::Block::GreenConcretePowder.into(),
|
||||
azalea_registry::Block::RedConcretePowder.into(),
|
||||
azalea_registry::Block::BlackConcretePowder.into(),
|
||||
];
|
||||
falling_blocks.sort_unstable_by_key(|block| block.id);
|
||||
|
||||
Self {
|
||||
block_state_id_costs: IntMap::default(),
|
||||
block_state_id_costs: UnsafeCell::new(IntMap::default()),
|
||||
inventory_menu,
|
||||
water_block_state_range,
|
||||
lava_block_state_range,
|
||||
falling_blocks,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cost_for(&mut self, block: BlockState) -> f32 {
|
||||
if let Some(cost) = self.block_state_id_costs.get(&block.id) {
|
||||
pub fn cost_for(&self, block: BlockState) -> f32 {
|
||||
let Some(inventory_menu) = &self.inventory_menu else {
|
||||
return f32::INFINITY;
|
||||
};
|
||||
|
||||
// SAFETY: mining is single-threaded, so this is safe
|
||||
let block_state_id_costs = unsafe { &mut *self.block_state_id_costs.get() };
|
||||
|
||||
if let Some(cost) = block_state_id_costs.get(&block.id) {
|
||||
*cost
|
||||
} else {
|
||||
let best_tool_result = best_tool_in_hotbar_for_block(block, &self.inventory_menu);
|
||||
let cost = 1. / best_tool_result.percentage_per_tick;
|
||||
self.block_state_id_costs.insert(block.id, cost);
|
||||
let best_tool_result = best_tool_in_hotbar_for_block(block, inventory_menu);
|
||||
let mut cost = 1. / best_tool_result.percentage_per_tick;
|
||||
|
||||
cost += BLOCK_BREAK_ADDITIONAL_PENALTY;
|
||||
|
||||
block_state_id_costs.insert(block.id, cost);
|
||||
cost
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_liquid(&self, block: BlockState) -> bool {
|
||||
self.water_block_state_range.contains(&block.id)
|
||||
|| self.lava_block_state_range.contains(&block.id)
|
||||
}
|
||||
|
||||
pub fn is_falling_block(&self, block: BlockState) -> bool {
|
||||
self.falling_blocks
|
||||
.binary_search_by_key(&block.id, |block| block.id)
|
||||
.is_ok()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
//! A pathfinding plugin to make bots navigate the world. A lot of this code is
|
||||
//! based on [Baritone](https://github.com/cabaletta/baritone).
|
||||
//! A pathfinding plugin to make bots able to traverse the world.
|
||||
//!
|
||||
//! Much of this code is based on [Baritone](https://github.com/cabaletta/baritone).
|
||||
|
||||
pub mod astar;
|
||||
pub mod costs;
|
||||
mod debug;
|
||||
pub mod goals;
|
||||
pub mod mining;
|
||||
pub mod moves;
|
||||
|
@ -23,11 +25,11 @@ use crate::ecs::{
|
|||
};
|
||||
use crate::pathfinder::moves::PathfinderCtx;
|
||||
use crate::pathfinder::world::CachedWorld;
|
||||
use azalea_client::chat::SendChatEvent;
|
||||
use azalea_client::inventory::{InventoryComponent, InventorySet};
|
||||
use azalea_client::inventory::{InventoryComponent, InventorySet, SetSelectedHotbarSlotEvent};
|
||||
use azalea_client::mining::{Mining, StartMiningBlockEvent};
|
||||
use azalea_client::movement::MoveEventsSet;
|
||||
use azalea_client::{StartSprintEvent, StartWalkEvent};
|
||||
use azalea_core::position::{BlockPos, Vec3};
|
||||
use azalea_client::{InstanceHolder, StartSprintEvent, StartWalkEvent};
|
||||
use azalea_core::position::BlockPos;
|
||||
use azalea_core::tick::GameTick;
|
||||
use azalea_entity::metadata::Player;
|
||||
use azalea_entity::LocalEntity;
|
||||
|
@ -35,11 +37,9 @@ use azalea_entity::{Physics, Position};
|
|||
use azalea_physics::PhysicsSet;
|
||||
use azalea_world::{InstanceContainer, InstanceName};
|
||||
use bevy_app::{PreUpdate, Update};
|
||||
use bevy_ecs::event::Events;
|
||||
use bevy_ecs::prelude::Event;
|
||||
use bevy_ecs::query::Changed;
|
||||
use bevy_ecs::schedule::IntoSystemConfigs;
|
||||
use bevy_ecs::system::{Local, ResMut};
|
||||
use bevy_tasks::{AsyncComputeTaskPool, Task};
|
||||
use futures_lite::future;
|
||||
use std::collections::VecDeque;
|
||||
|
@ -48,6 +48,9 @@ use std::sync::Arc;
|
|||
use std::time::{Duration, Instant};
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
|
||||
use self::debug::debug_render_path_with_particles;
|
||||
pub use self::debug::PathfinderDebugParticles;
|
||||
use self::goals::Goal;
|
||||
use self::mining::MiningCache;
|
||||
use self::moves::{ExecuteCtx, IsReachedCtx, SuccessorsFn};
|
||||
|
||||
|
@ -93,11 +96,12 @@ impl Plugin for PathfinderPlugin {
|
|||
}
|
||||
|
||||
/// A component that makes this client able to pathfind.
|
||||
#[derive(Component, Default)]
|
||||
#[derive(Component, Default, Clone)]
|
||||
pub struct Pathfinder {
|
||||
pub goal: Option<Arc<dyn Goal + Send + Sync>>,
|
||||
pub successors_fn: Option<SuccessorsFn>,
|
||||
pub is_calculating: bool,
|
||||
pub allow_mining: bool,
|
||||
|
||||
pub goto_id: Arc<AtomicUsize>,
|
||||
}
|
||||
|
@ -120,6 +124,9 @@ pub struct GotoEvent {
|
|||
/// The function that's used for checking what moves are possible. Usually
|
||||
/// `pathfinder::moves::default_move`
|
||||
pub successors_fn: SuccessorsFn,
|
||||
|
||||
/// Whether the bot is allowed to break blocks while pathfinding.
|
||||
pub allow_mining: bool,
|
||||
}
|
||||
#[derive(Event, Clone)]
|
||||
pub struct PathFoundEvent {
|
||||
|
@ -128,6 +135,7 @@ pub struct PathFoundEvent {
|
|||
pub path: Option<VecDeque<astar::Movement<BlockPos, moves::MoveData>>>,
|
||||
pub is_partial: bool,
|
||||
pub successors_fn: SuccessorsFn,
|
||||
pub allow_mining: bool,
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
|
@ -142,6 +150,7 @@ fn add_default_pathfinder(
|
|||
|
||||
pub trait PathfinderClientExt {
|
||||
fn goto(&self, goal: impl Goal + Send + Sync + 'static);
|
||||
fn goto_without_mining(&self, goal: impl Goal + Send + Sync + 'static);
|
||||
fn stop_pathfinding(&self);
|
||||
}
|
||||
|
||||
|
@ -158,6 +167,18 @@ impl PathfinderClientExt for azalea_client::Client {
|
|||
entity: self.entity,
|
||||
goal: Arc::new(goal),
|
||||
successors_fn: moves::default_move,
|
||||
allow_mining: true,
|
||||
});
|
||||
}
|
||||
|
||||
/// Same as [`goto`](Self::goto). but the bot won't break any blocks while
|
||||
/// executing the path.
|
||||
fn goto_without_mining(&self, goal: impl Goal + Send + Sync + 'static) {
|
||||
self.ecs.lock().send_event(GotoEvent {
|
||||
entity: self.entity,
|
||||
goal: Arc::new(goal),
|
||||
successors_fn: moves::default_move,
|
||||
allow_mining: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -191,10 +212,19 @@ fn goto_listener(
|
|||
.get_mut(event.entity)
|
||||
.expect("Called goto on an entity that's not in the world");
|
||||
|
||||
if event.goal.success(BlockPos::from(position)) {
|
||||
// we're already at the goal, nothing to do
|
||||
pathfinder.goal = None;
|
||||
pathfinder.successors_fn = None;
|
||||
pathfinder.is_calculating = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// we store the goal so it can be recalculated later if necessary
|
||||
pathfinder.goal = Some(event.goal.clone());
|
||||
pathfinder.successors_fn = Some(event.successors_fn);
|
||||
pathfinder.is_calculating = true;
|
||||
pathfinder.allow_mining = event.allow_mining;
|
||||
|
||||
let start = if let Some(executing_path) = executing_path
|
||||
&& let Some(final_node) = executing_path.path.back()
|
||||
|
@ -220,7 +250,13 @@ fn goto_listener(
|
|||
|
||||
let goto_id_atomic = pathfinder.goto_id.clone();
|
||||
let goto_id = goto_id_atomic.fetch_add(1, atomic::Ordering::Relaxed) + 1;
|
||||
let mining_cache = MiningCache::new(inventory.inventory_menu.clone());
|
||||
|
||||
let allow_mining = event.allow_mining;
|
||||
let mining_cache = MiningCache::new(if allow_mining {
|
||||
Some(inventory.inventory_menu.clone())
|
||||
} else {
|
||||
None
|
||||
});
|
||||
|
||||
let task = thread_pool.spawn(async move {
|
||||
debug!("start: {start:?}");
|
||||
|
@ -248,7 +284,11 @@ fn goto_listener(
|
|||
debug!("partial: {partial:?}");
|
||||
let duration = end_time - start_time;
|
||||
if partial {
|
||||
info!("Pathfinder took {duration:?} (incomplete path)");
|
||||
if movements.is_empty() {
|
||||
info!("Pathfinder took {duration:?} (empty path)");
|
||||
} else {
|
||||
info!("Pathfinder took {duration:?} (incomplete path)");
|
||||
}
|
||||
// wait a bit so it's not a busy loop
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
} else {
|
||||
|
@ -289,6 +329,7 @@ fn goto_listener(
|
|||
path: Some(path),
|
||||
is_partial,
|
||||
successors_fn,
|
||||
allow_mining,
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -342,7 +383,11 @@ fn path_found_listener(
|
|||
.expect("Entity tried to pathfind but the entity isn't in a valid world");
|
||||
let successors_fn: moves::SuccessorsFn = event.successors_fn;
|
||||
let cached_world = CachedWorld::new(world_lock);
|
||||
let mining_cache = MiningCache::new(inventory.inventory_menu.clone());
|
||||
let mining_cache = MiningCache::new(if event.allow_mining {
|
||||
Some(inventory.inventory_menu.clone())
|
||||
} else {
|
||||
None
|
||||
});
|
||||
let successors = |pos: BlockPos| {
|
||||
call_successors_fn(&cached_world, &mining_cache, successors_fn, pos)
|
||||
};
|
||||
|
@ -399,8 +444,21 @@ fn path_found_listener(
|
|||
}
|
||||
}
|
||||
|
||||
fn timeout_movement(mut query: Query<(&Pathfinder, &mut ExecutingPath, &Position)>) {
|
||||
for (pathfinder, mut executing_path, position) in &mut query {
|
||||
fn timeout_movement(
|
||||
mut query: Query<(&Pathfinder, &mut ExecutingPath, &Position, Option<&Mining>)>,
|
||||
) {
|
||||
for (pathfinder, mut executing_path, position, mining) in &mut query {
|
||||
// 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_to_sqr(&BlockPos::from(position)) < 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();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if executing_path.last_node_reached_at.elapsed() > Duration::from_secs(2)
|
||||
&& !pathfinder.is_calculating
|
||||
&& !executing_path.path.is_empty()
|
||||
|
@ -535,7 +593,11 @@ fn check_for_path_obstruction(
|
|||
|
||||
// obstruction check (the path we're executing isn't possible anymore)
|
||||
let cached_world = CachedWorld::new(world_lock);
|
||||
let mining_cache = MiningCache::new(inventory.inventory_menu.clone());
|
||||
let mining_cache = MiningCache::new(if pathfinder.allow_mining {
|
||||
Some(inventory.inventory_menu.clone())
|
||||
} else {
|
||||
None
|
||||
});
|
||||
let successors =
|
||||
|pos: BlockPos| call_successors_fn(&cached_world, &mining_cache, successors_fn, pos);
|
||||
|
||||
|
@ -580,6 +642,7 @@ fn recalculate_near_end_of_path(
|
|||
entity,
|
||||
goal,
|
||||
successors_fn,
|
||||
allow_mining: pathfinder.allow_mining,
|
||||
});
|
||||
pathfinder.is_calculating = true;
|
||||
|
||||
|
@ -614,14 +677,27 @@ fn recalculate_near_end_of_path(
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn tick_execute_path(
|
||||
mut query: Query<(Entity, &mut ExecutingPath, &Position, &Physics)>,
|
||||
mut query: Query<(
|
||||
Entity,
|
||||
&mut ExecutingPath,
|
||||
&Position,
|
||||
&Physics,
|
||||
Option<&Mining>,
|
||||
&InstanceHolder,
|
||||
&InventoryComponent,
|
||||
)>,
|
||||
mut look_at_events: EventWriter<LookAtEvent>,
|
||||
mut sprint_events: EventWriter<StartSprintEvent>,
|
||||
mut walk_events: EventWriter<StartWalkEvent>,
|
||||
mut jump_events: EventWriter<JumpEvent>,
|
||||
mut start_mining_events: EventWriter<StartMiningBlockEvent>,
|
||||
mut set_selected_hotbar_slot_events: EventWriter<SetSelectedHotbarSlotEvent>,
|
||||
) {
|
||||
for (entity, executing_path, position, physics) in &mut query {
|
||||
for (entity, executing_path, position, physics, mining, instance_holder, inventory_component) in
|
||||
&mut query
|
||||
{
|
||||
if let Some(movement) = executing_path.path.front() {
|
||||
let ctx = ExecuteCtx {
|
||||
entity,
|
||||
|
@ -629,10 +705,16 @@ fn tick_execute_path(
|
|||
position: **position,
|
||||
start: executing_path.last_reached_node,
|
||||
physics,
|
||||
is_currently_mining: mining.is_some(),
|
||||
instance: instance_holder.instance.clone(),
|
||||
menu: inventory_component.inventory_menu.clone(),
|
||||
|
||||
look_at_events: &mut look_at_events,
|
||||
sprint_events: &mut sprint_events,
|
||||
walk_events: &mut walk_events,
|
||||
jump_events: &mut jump_events,
|
||||
start_mining_events: &mut start_mining_events,
|
||||
set_selected_hotbar_slot_events: &mut set_selected_hotbar_slot_events,
|
||||
};
|
||||
trace!("executing move");
|
||||
(movement.data.execute)(ctx);
|
||||
|
@ -652,6 +734,7 @@ fn recalculate_if_has_goal_but_no_path(
|
|||
entity,
|
||||
goal,
|
||||
successors_fn: pathfinder.successors_fn.unwrap(),
|
||||
allow_mining: pathfinder.allow_mining,
|
||||
});
|
||||
pathfinder.is_calculating = true;
|
||||
}
|
||||
|
@ -718,101 +801,6 @@ fn stop_pathfinding_on_instance_change(
|
|||
}
|
||||
}
|
||||
|
||||
/// A component that makes bots run /particle commands while pathfinding to show
|
||||
/// where they're going. This requires the bots to have server operator
|
||||
/// permissions, and it'll make them spam *a lot* of commands.
|
||||
///
|
||||
/// ```
|
||||
/// # use azalea::prelude::*;
|
||||
/// # use azalea::pathfinder::PathfinderDebugParticles;
|
||||
/// # #[derive(Component, Clone, Default)]
|
||||
/// # pub struct State;
|
||||
///
|
||||
/// async fn handle(mut bot: Client, event: azalea::Event, state: State) -> anyhow::Result<()> {
|
||||
/// match event {
|
||||
/// azalea::Event::Init => {
|
||||
/// bot.ecs
|
||||
/// .lock()
|
||||
/// .entity_mut(bot.entity)
|
||||
/// .insert(PathfinderDebugParticles);
|
||||
/// }
|
||||
/// _ => {}
|
||||
/// }
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Component)]
|
||||
pub struct PathfinderDebugParticles;
|
||||
|
||||
fn debug_render_path_with_particles(
|
||||
mut query: Query<(Entity, &ExecutingPath), With<PathfinderDebugParticles>>,
|
||||
// chat_events is Option because the tests don't have SendChatEvent
|
||||
// and we have to use ResMut<Events> because bevy doesn't support Option<EventWriter>
|
||||
chat_events: Option<ResMut<Events<SendChatEvent>>>,
|
||||
mut tick_count: Local<usize>,
|
||||
) {
|
||||
let Some(mut chat_events) = chat_events else {
|
||||
return;
|
||||
};
|
||||
if *tick_count >= 2 {
|
||||
*tick_count = 0;
|
||||
} else {
|
||||
*tick_count += 1;
|
||||
return;
|
||||
}
|
||||
for (entity, executing_path) in &mut query {
|
||||
if executing_path.path.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut start = executing_path.last_reached_node;
|
||||
for (i, movement) in executing_path.path.iter().enumerate() {
|
||||
// /particle dust 0 1 1 1 ~ ~ ~ 0 0 0.2 0 100
|
||||
|
||||
let end = movement.target;
|
||||
|
||||
let start_vec3 = start.center();
|
||||
let end_vec3 = end.center();
|
||||
|
||||
let step_count = (start_vec3.distance_to_sqr(&end_vec3).sqrt() * 4.0) as usize;
|
||||
|
||||
let (r, g, b): (f64, f64, f64) = if i == 0 { (0., 1., 0.) } else { (0., 1., 1.) };
|
||||
|
||||
// interpolate between the start and end positions
|
||||
for i in 0..step_count {
|
||||
let percent = i as f64 / step_count as f64;
|
||||
let pos = Vec3 {
|
||||
x: start_vec3.x + (end_vec3.x - start_vec3.x) * percent,
|
||||
y: start_vec3.y + (end_vec3.y - start_vec3.y) * percent,
|
||||
z: start_vec3.z + (end_vec3.z - start_vec3.z) * percent,
|
||||
};
|
||||
let particle_command = format!(
|
||||
"/particle dust {r} {g} {b} {size} {start_x} {start_y} {start_z} {delta_x} {delta_y} {delta_z} 0 {count}",
|
||||
size = 1,
|
||||
start_x = pos.x,
|
||||
start_y = pos.y,
|
||||
start_z = pos.z,
|
||||
delta_x = 0,
|
||||
delta_y = 0,
|
||||
delta_z = 0,
|
||||
count = 1
|
||||
);
|
||||
chat_events.send(SendChatEvent {
|
||||
entity,
|
||||
content: particle_command,
|
||||
});
|
||||
}
|
||||
|
||||
start = movement.target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Goal {
|
||||
fn heuristic(&self, n: BlockPos) -> f32;
|
||||
fn success(&self, n: BlockPos) -> bool;
|
||||
}
|
||||
|
||||
/// Checks whether the path has been obstructed, and returns Some(index) if it
|
||||
/// has been. The index is of the first obstructed node.
|
||||
fn check_path_obstructed<SuccessorsFn>(
|
||||
|
@ -911,6 +899,7 @@ mod tests {
|
|||
entity: simulation.entity,
|
||||
goal: Arc::new(BlockPosGoal(end_pos)),
|
||||
successors_fn: moves::default_move,
|
||||
allow_mining: false,
|
||||
});
|
||||
simulation
|
||||
}
|
||||
|
|
|
@ -16,17 +16,20 @@ pub fn basic_move(ctx: &mut PathfinderCtx, node: BlockPos) {
|
|||
descend_move(ctx, node);
|
||||
diagonal_move(ctx, node);
|
||||
descend_forward_1_move(ctx, node);
|
||||
downward_move(ctx, node);
|
||||
}
|
||||
|
||||
fn forward_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
|
||||
for dir in CardinalDirection::iter() {
|
||||
let offset = BlockPos::new(dir.x(), 0, dir.z());
|
||||
|
||||
if !ctx.world.is_standable(pos + offset) {
|
||||
let mut cost = SPRINT_ONE_BLOCK_COST;
|
||||
|
||||
let break_cost = ctx.world.cost_for_standing(pos + offset, ctx.mining_cache);
|
||||
if break_cost == f32::INFINITY {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cost = SPRINT_ONE_BLOCK_COST;
|
||||
cost += break_cost;
|
||||
|
||||
ctx.edges.push(Edge {
|
||||
movement: astar::Movement {
|
||||
|
@ -43,6 +46,14 @@ fn forward_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
|
|||
|
||||
fn execute_forward_move(mut ctx: ExecuteCtx) {
|
||||
let center = ctx.target.center();
|
||||
|
||||
if ctx.mine_while_at_start(ctx.target.up(1)) {
|
||||
return;
|
||||
}
|
||||
if ctx.mine_while_at_start(ctx.target) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.look_at(center);
|
||||
ctx.sprint(SprintDirection::Forward);
|
||||
}
|
||||
|
@ -51,14 +62,22 @@ fn ascend_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
|
|||
for dir in CardinalDirection::iter() {
|
||||
let offset = BlockPos::new(dir.x(), 1, dir.z());
|
||||
|
||||
if !ctx.world.is_block_passable(pos.up(2)) {
|
||||
let break_cost_1 = ctx
|
||||
.world
|
||||
.cost_for_breaking_block(pos.up(2), ctx.mining_cache);
|
||||
if break_cost_1 == f32::INFINITY {
|
||||
continue;
|
||||
}
|
||||
if !ctx.world.is_standable(pos + offset) {
|
||||
let break_cost_2 = ctx.world.cost_for_standing(pos + offset, ctx.mining_cache);
|
||||
if break_cost_2 == f32::INFINITY {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cost = SPRINT_ONE_BLOCK_COST + JUMP_PENALTY + *JUMP_ONE_BLOCK_COST;
|
||||
let cost = SPRINT_ONE_BLOCK_COST
|
||||
+ JUMP_PENALTY
|
||||
+ *JUMP_ONE_BLOCK_COST
|
||||
+ break_cost_1
|
||||
+ break_cost_2;
|
||||
|
||||
ctx.edges.push(Edge {
|
||||
movement: astar::Movement {
|
||||
|
@ -81,6 +100,16 @@ fn execute_ascend_move(mut ctx: ExecuteCtx) {
|
|||
..
|
||||
} = ctx;
|
||||
|
||||
if ctx.mine_while_at_start(start.up(2)) {
|
||||
return;
|
||||
}
|
||||
if ctx.mine_while_at_start(target) {
|
||||
return;
|
||||
}
|
||||
if ctx.mine_while_at_start(target.up(1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let target_center = target.center();
|
||||
|
||||
ctx.look_at(target_center);
|
||||
|
@ -123,19 +152,39 @@ fn descend_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
|
|||
for dir in CardinalDirection::iter() {
|
||||
let dir_delta = BlockPos::new(dir.x(), 0, dir.z());
|
||||
let new_horizontal_position = pos + dir_delta;
|
||||
let fall_distance = ctx.world.fall_distance(new_horizontal_position);
|
||||
if fall_distance == 0 || fall_distance > 3 {
|
||||
|
||||
let break_cost_1 = ctx
|
||||
.world
|
||||
.cost_for_passing(new_horizontal_position, ctx.mining_cache);
|
||||
if break_cost_1 == f32::INFINITY {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut fall_distance = ctx.world.fall_distance(new_horizontal_position);
|
||||
if fall_distance > 3 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if fall_distance == 0 {
|
||||
// if the fall distance is 0, set it to 1 so we try mining
|
||||
fall_distance = 1
|
||||
}
|
||||
|
||||
let new_position = new_horizontal_position.down(fall_distance as i32);
|
||||
|
||||
// check whether 3 blocks vertically forward are passable
|
||||
if !ctx.world.is_passable(new_horizontal_position) {
|
||||
continue;
|
||||
}
|
||||
// check whether we can stand on the target position
|
||||
if !ctx.world.is_standable(new_position) {
|
||||
continue;
|
||||
// only mine if we're descending 1 block
|
||||
let break_cost_2;
|
||||
if fall_distance == 1 {
|
||||
break_cost_2 = ctx.world.cost_for_standing(new_position, ctx.mining_cache);
|
||||
if break_cost_2 == f32::INFINITY {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// check whether we can stand on the target position
|
||||
if !ctx.world.is_standable(new_position) {
|
||||
continue;
|
||||
}
|
||||
break_cost_2 = 0.;
|
||||
}
|
||||
|
||||
let cost = WALK_OFF_BLOCK_COST
|
||||
|
@ -145,9 +194,11 @@ fn descend_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
|
|||
.copied()
|
||||
// avoid panicking if we fall more than the size of FALL_N_BLOCKS_COST
|
||||
// probably not possible but just in case
|
||||
.unwrap_or(f32::MAX),
|
||||
.unwrap_or(f32::INFINITY),
|
||||
CENTER_AFTER_FALL_COST,
|
||||
);
|
||||
)
|
||||
+ break_cost_1
|
||||
+ break_cost_2;
|
||||
|
||||
ctx.edges.push(Edge {
|
||||
movement: astar::Movement {
|
||||
|
@ -169,6 +220,12 @@ fn execute_descend_move(mut ctx: ExecuteCtx) {
|
|||
..
|
||||
} = ctx;
|
||||
|
||||
for i in (0..=(start.y - target.y + 1)).rev() {
|
||||
if ctx.mine_while_at_start(target.up(i)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let start_center = start.center();
|
||||
let center = target.center();
|
||||
|
||||
|
@ -249,7 +306,7 @@ fn descend_forward_1_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
|
|||
.copied()
|
||||
// avoid panicking if we fall more than the size of FALL_N_BLOCKS_COST
|
||||
// probably not possible but just in case
|
||||
.unwrap_or(f32::MAX),
|
||||
.unwrap_or(f32::INFINITY),
|
||||
CENTER_AFTER_FALL_COST,
|
||||
);
|
||||
|
||||
|
@ -310,3 +367,53 @@ fn execute_diagonal_move(mut ctx: ExecuteCtx) {
|
|||
ctx.look_at(target_center);
|
||||
ctx.sprint(SprintDirection::Forward);
|
||||
}
|
||||
|
||||
/// Go directly down, usually by mining.
|
||||
fn downward_move(ctx: &mut PathfinderCtx, pos: BlockPos) {
|
||||
// make sure we land on a solid block after breaking the one below us
|
||||
if !ctx.world.is_block_solid(pos.down(2)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let break_cost = ctx
|
||||
.world
|
||||
.cost_for_breaking_block(pos.down(1), ctx.mining_cache);
|
||||
if break_cost == f32::INFINITY {
|
||||
return;
|
||||
}
|
||||
|
||||
let cost = FALL_N_BLOCKS_COST[1] + break_cost;
|
||||
|
||||
ctx.edges.push(Edge {
|
||||
movement: astar::Movement {
|
||||
target: pos.down(1),
|
||||
data: MoveData {
|
||||
execute: &execute_downward_move,
|
||||
is_reached: &default_is_reached,
|
||||
},
|
||||
},
|
||||
cost,
|
||||
})
|
||||
}
|
||||
fn execute_downward_move(mut ctx: ExecuteCtx) {
|
||||
let ExecuteCtx {
|
||||
target, position, ..
|
||||
} = ctx;
|
||||
|
||||
let target_center = target.center();
|
||||
|
||||
let horizontal_distance_from_target =
|
||||
(target_center - position).horizontal_distance_sqr().sqrt();
|
||||
|
||||
if horizontal_distance_from_target > 0.25 {
|
||||
ctx.look_at(target_center);
|
||||
ctx.walk(WalkDirection::Forward);
|
||||
} else if ctx.mine_while_at_start(target) {
|
||||
ctx.walk(WalkDirection::None);
|
||||
} else if BlockPos::from(position) != target {
|
||||
ctx.look_at(target_center);
|
||||
ctx.walk(WalkDirection::Forward);
|
||||
} else {
|
||||
ctx.walk(WalkDirection::None);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
pub mod basic;
|
||||
pub mod parkour;
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use crate::{JumpEvent, LookAtEvent};
|
||||
use crate::{auto_tool::best_tool_in_hotbar_for_block, JumpEvent, LookAtEvent};
|
||||
|
||||
use super::{astar, mining::MiningCache, world::CachedWorld};
|
||||
use azalea_client::{SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection};
|
||||
use super::{
|
||||
astar,
|
||||
mining::MiningCache,
|
||||
world::{is_block_state_passable, CachedWorld},
|
||||
};
|
||||
use azalea_client::{
|
||||
inventory::SetSelectedHotbarSlotEvent, mining::StartMiningBlockEvent, SprintDirection,
|
||||
StartSprintEvent, StartWalkEvent, WalkDirection,
|
||||
};
|
||||
use azalea_core::position::{BlockPos, Vec3};
|
||||
use azalea_inventory::Menu;
|
||||
use azalea_world::Instance;
|
||||
use bevy_ecs::{entity::Entity, event::EventWriter};
|
||||
use parking_lot::RwLock;
|
||||
|
||||
type Edge = astar::Edge<BlockPos, MoveData>;
|
||||
|
||||
|
@ -35,7 +45,7 @@ impl Debug for MoveData {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct ExecuteCtx<'w1, 'w2, 'w3, 'w4, 'a> {
|
||||
pub struct ExecuteCtx<'w1, 'w2, 'w3, 'w4, 'w5, 'w6, 'a> {
|
||||
pub entity: Entity,
|
||||
/// The node that we're trying to reach.
|
||||
pub target: BlockPos,
|
||||
|
@ -43,14 +53,19 @@ pub struct ExecuteCtx<'w1, 'w2, 'w3, 'w4, 'a> {
|
|||
pub start: BlockPos,
|
||||
pub position: Vec3,
|
||||
pub physics: &'a azalea_entity::Physics,
|
||||
pub is_currently_mining: bool,
|
||||
pub instance: Arc<RwLock<Instance>>,
|
||||
pub menu: Menu,
|
||||
|
||||
pub look_at_events: &'a mut EventWriter<'w1, LookAtEvent>,
|
||||
pub sprint_events: &'a mut EventWriter<'w2, StartSprintEvent>,
|
||||
pub walk_events: &'a mut EventWriter<'w3, StartWalkEvent>,
|
||||
pub jump_events: &'a mut EventWriter<'w4, JumpEvent>,
|
||||
pub start_mining_events: &'a mut EventWriter<'w5, StartMiningBlockEvent>,
|
||||
pub set_selected_hotbar_slot_events: &'a mut EventWriter<'w6, SetSelectedHotbarSlotEvent>,
|
||||
}
|
||||
|
||||
impl ExecuteCtx<'_, '_, '_, '_, '_> {
|
||||
impl ExecuteCtx<'_, '_, '_, '_, '_, '_, '_> {
|
||||
pub fn look_at(&mut self, position: Vec3) {
|
||||
self.look_at_events.send(LookAtEvent {
|
||||
entity: self.entity,
|
||||
|
@ -63,6 +78,13 @@ impl ExecuteCtx<'_, '_, '_, '_, '_> {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn look_at_exact(&mut self, position: Vec3) {
|
||||
self.look_at_events.send(LookAtEvent {
|
||||
entity: self.entity,
|
||||
position,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn sprint(&mut self, direction: SprintDirection) {
|
||||
self.sprint_events.send(StartSprintEvent {
|
||||
entity: self.entity,
|
||||
|
@ -82,6 +104,76 @@ impl ExecuteCtx<'_, '_, '_, '_, '_> {
|
|||
entity: self.entity,
|
||||
});
|
||||
}
|
||||
|
||||
/// 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)
|
||||
.unwrap_or_default();
|
||||
if is_block_state_passable(block_state) {
|
||||
// block is already passable, no need to mine it
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Mine the block at the given position. Returns whether the block is being
|
||||
/// mined.
|
||||
pub fn mine(&mut self, block: BlockPos) -> bool {
|
||||
let block_state = self
|
||||
.instance
|
||||
.read()
|
||||
.get_block_state(&block)
|
||||
.unwrap_or_default();
|
||||
if is_block_state_passable(block_state) {
|
||||
// block is already passable, no need to mine it
|
||||
return false;
|
||||
}
|
||||
|
||||
let best_tool_result = best_tool_in_hotbar_for_block(block_state, &self.menu);
|
||||
|
||||
self.set_selected_hotbar_slot_events
|
||||
.send(SetSelectedHotbarSlotEvent {
|
||||
entity: self.entity,
|
||||
slot: best_tool_result.index as u8,
|
||||
});
|
||||
|
||||
self.is_currently_mining = true;
|
||||
|
||||
self.walk(WalkDirection::None);
|
||||
self.look_at_exact(block.center());
|
||||
self.start_mining_events.send(StartMiningBlockEvent {
|
||||
entity: self.entity,
|
||||
position: block,
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Mine the given block, but make sure the player is standing at the start
|
||||
/// of the current node first.
|
||||
pub fn mine_while_at_start(&mut self, block: BlockPos) -> bool {
|
||||
let horizontal_distance_from_start = (self.start.center() - self.position)
|
||||
.horizontal_distance_sqr()
|
||||
.sqrt();
|
||||
let at_start_position =
|
||||
BlockPos::from(self.position) == self.start && horizontal_distance_from_start < 0.25;
|
||||
|
||||
if self.should_mine(block) {
|
||||
if at_start_position {
|
||||
self.mine(block);
|
||||
} else {
|
||||
self.look_at(self.start.center());
|
||||
self.walk(WalkDirection::Forward);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IsReachedCtx<'a> {
|
||||
|
|
|
@ -11,6 +11,9 @@ use azalea_core::{
|
|||
use azalea_physics::collision::BlockWithShape;
|
||||
use azalea_world::Instance;
|
||||
use parking_lot::RwLock;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use super::mining::MiningCache;
|
||||
|
||||
/// An efficient representation of the world used for the pathfinder.
|
||||
pub struct CachedWorld {
|
||||
|
@ -20,8 +23,11 @@ pub struct CachedWorld {
|
|||
// we store `PalettedContainer`s instead of `Chunk`s or `Section`s because it doesn't contain
|
||||
// any unnecessary data like heightmaps or biomes.
|
||||
cached_chunks: RefCell<Vec<(ChunkPos, Vec<azalea_world::palette::PalettedContainer>)>>,
|
||||
last_chunk_cache_index: RefCell<Option<usize>>,
|
||||
|
||||
cached_blocks: UnsafeCell<CachedSections>,
|
||||
|
||||
cached_mining_costs: RefCell<FxHashMap<BlockPos, f32>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -82,7 +88,9 @@ impl CachedWorld {
|
|||
min_y,
|
||||
world_lock,
|
||||
cached_chunks: Default::default(),
|
||||
last_chunk_cache_index: Default::default(),
|
||||
cached_blocks: Default::default(),
|
||||
cached_mining_costs: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,24 +108,50 @@ impl CachedWorld {
|
|||
section_pos: ChunkSectionPos,
|
||||
f: impl FnOnce(&azalea_world::palette::PalettedContainer) -> T,
|
||||
) -> Option<T> {
|
||||
if section_pos.y * 16 < self.min_y {
|
||||
// y position is out of bounds
|
||||
return None;
|
||||
}
|
||||
|
||||
let chunk_pos = ChunkPos::from(section_pos);
|
||||
let section_index =
|
||||
azalea_world::chunk_storage::section_index(section_pos.y * 16, self.min_y) as usize;
|
||||
|
||||
let mut cached_chunks = self.cached_chunks.borrow_mut();
|
||||
|
||||
// get section from cache
|
||||
if let Some(sections) = cached_chunks.iter().find_map(|(pos, sections)| {
|
||||
if *pos == chunk_pos {
|
||||
Some(sections)
|
||||
} else {
|
||||
None
|
||||
// optimization: avoid doing the iter lookup if the last chunk we looked up is
|
||||
// the same
|
||||
if let Some(last_chunk_cache_index) = *self.last_chunk_cache_index.borrow() {
|
||||
if cached_chunks[last_chunk_cache_index].0 == chunk_pos {
|
||||
// don't bother with the iter lookup
|
||||
let sections = &cached_chunks[last_chunk_cache_index].1;
|
||||
if section_index >= sections.len() {
|
||||
// y position is out of bounds
|
||||
return None;
|
||||
};
|
||||
let section: &azalea_world::palette::PalettedContainer = §ions[section_index];
|
||||
return Some(f(section));
|
||||
}
|
||||
}) {
|
||||
}
|
||||
|
||||
// get section from cache
|
||||
if let Some((chunk_index, sections)) =
|
||||
cached_chunks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(i, (pos, sections))| {
|
||||
if *pos == chunk_pos {
|
||||
Some((i, sections))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
{
|
||||
if section_index >= sections.len() {
|
||||
// y position is out of bounds
|
||||
return None;
|
||||
};
|
||||
*self.last_chunk_cache_index.borrow_mut() = Some(chunk_index);
|
||||
let section: &azalea_world::palette::PalettedContainer = §ions[section_index];
|
||||
return Some(f(section));
|
||||
}
|
||||
|
@ -206,18 +240,189 @@ impl CachedWorld {
|
|||
solid
|
||||
}
|
||||
|
||||
/// Returns how much it costs to break this block. Returns 0 if the block is
|
||||
/// already passable.
|
||||
pub fn cost_for_breaking_block(&self, pos: BlockPos, mining_cache: &MiningCache) -> f32 {
|
||||
let mut cached_mining_costs = self.cached_mining_costs.borrow_mut();
|
||||
|
||||
if let Some(&cost) = cached_mining_costs.get(&pos) {
|
||||
return cost;
|
||||
}
|
||||
|
||||
let cost = self.uncached_cost_for_breaking_block(pos, mining_cache);
|
||||
cached_mining_costs.insert(pos, cost);
|
||||
cost
|
||||
}
|
||||
|
||||
fn uncached_cost_for_breaking_block(&self, pos: BlockPos, mining_cache: &MiningCache) -> f32 {
|
||||
if self.is_block_passable(pos) {
|
||||
// if the block is passable then it doesn't need to be broken
|
||||
return 0.;
|
||||
}
|
||||
|
||||
let (section_pos, section_block_pos) =
|
||||
(ChunkSectionPos::from(pos), ChunkSectionBlockPos::from(pos));
|
||||
|
||||
// we use this as an optimization to avoid getting the section again if the
|
||||
// block is in the same section
|
||||
let up_is_in_same_section = section_block_pos.y != 15;
|
||||
let north_is_in_same_section = section_block_pos.z != 0;
|
||||
let east_is_in_same_section = section_block_pos.x != 15;
|
||||
let south_is_in_same_section = section_block_pos.z != 15;
|
||||
let west_is_in_same_section = section_block_pos.x != 0;
|
||||
|
||||
let Some(mining_cost) = self.with_section(section_pos, |section| {
|
||||
let block_state =
|
||||
BlockState::try_from(section.get_at_index(u16::from(section_block_pos) as usize))
|
||||
.unwrap_or_default();
|
||||
let mining_cost = mining_cache.cost_for(block_state);
|
||||
|
||||
if mining_cost == f32::INFINITY {
|
||||
// the block is unbreakable
|
||||
return f32::INFINITY;
|
||||
}
|
||||
|
||||
// if there's a falling block or liquid above this block, abort
|
||||
if up_is_in_same_section {
|
||||
let up_block = BlockState::try_from(
|
||||
section.get_at_index(u16::from(section_block_pos.up(1)) as usize),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
if mining_cache.is_liquid(up_block) || mining_cache.is_falling_block(up_block) {
|
||||
return f32::INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
// if there's a liquid to the north of this block, abort
|
||||
if north_is_in_same_section {
|
||||
let north_block = BlockState::try_from(
|
||||
section.get_at_index(u16::from(section_block_pos.north(1)) as usize),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
if mining_cache.is_liquid(north_block) {
|
||||
return f32::INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
// liquid to the east
|
||||
if east_is_in_same_section {
|
||||
let east_block = BlockState::try_from(
|
||||
section.get_at_index(u16::from(section_block_pos.east(1)) as usize),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
if mining_cache.is_liquid(east_block) {
|
||||
return f32::INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
// liquid to the south
|
||||
if south_is_in_same_section {
|
||||
let south_block = BlockState::try_from(
|
||||
section.get_at_index(u16::from(section_block_pos.south(1)) as usize),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
if mining_cache.is_liquid(south_block) {
|
||||
return f32::INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
// liquid to the west
|
||||
if west_is_in_same_section {
|
||||
let west_block = BlockState::try_from(
|
||||
section.get_at_index(u16::from(section_block_pos.west(1)) as usize),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
if mining_cache.is_liquid(west_block) {
|
||||
return f32::INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
// the block is probably safe to break, we'll have to check the adjacent blocks
|
||||
// that weren't in the same section next though
|
||||
mining_cost
|
||||
}) else {
|
||||
// the chunk isn't loaded
|
||||
let cost = if self.is_block_solid(pos) {
|
||||
// assume it's unbreakable if it's solid and out of render distance
|
||||
f32::INFINITY
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
return cost;
|
||||
};
|
||||
|
||||
if mining_cost == f32::INFINITY {
|
||||
// the block is unbreakable
|
||||
return f32::INFINITY;
|
||||
}
|
||||
|
||||
let check_should_avoid_this_block = |pos: BlockPos, check: &dyn Fn(BlockState) -> bool| {
|
||||
let block_state = self
|
||||
.with_section(ChunkSectionPos::from(pos), |section| {
|
||||
BlockState::try_from(
|
||||
section.get_at_index(u16::from(ChunkSectionBlockPos::from(pos)) as usize),
|
||||
)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
check(block_state)
|
||||
};
|
||||
|
||||
// check the adjacent blocks that weren't in the same section
|
||||
if !up_is_in_same_section
|
||||
&& check_should_avoid_this_block(pos.up(1), &|b| {
|
||||
mining_cache.is_liquid(b) || mining_cache.is_falling_block(b)
|
||||
})
|
||||
{
|
||||
return f32::INFINITY;
|
||||
}
|
||||
if !north_is_in_same_section
|
||||
&& check_should_avoid_this_block(pos.north(1), &|b| mining_cache.is_liquid(b))
|
||||
{
|
||||
return f32::INFINITY;
|
||||
}
|
||||
if !east_is_in_same_section
|
||||
&& check_should_avoid_this_block(pos.east(1), &|b| mining_cache.is_liquid(b))
|
||||
{
|
||||
return f32::INFINITY;
|
||||
}
|
||||
if !south_is_in_same_section
|
||||
&& check_should_avoid_this_block(pos.south(1), &|b| mining_cache.is_liquid(b))
|
||||
{
|
||||
return f32::INFINITY;
|
||||
}
|
||||
if !west_is_in_same_section
|
||||
&& check_should_avoid_this_block(pos.west(1), &|b| mining_cache.is_liquid(b))
|
||||
{
|
||||
return f32::INFINITY;
|
||||
}
|
||||
|
||||
mining_cost
|
||||
}
|
||||
|
||||
/// Whether this block and the block above are passable
|
||||
pub fn is_passable(&self, pos: BlockPos) -> bool {
|
||||
self.is_block_passable(pos) && self.is_block_passable(pos.up(1))
|
||||
}
|
||||
|
||||
pub fn cost_for_passing(&self, pos: BlockPos, mining_cache: &MiningCache) -> f32 {
|
||||
self.cost_for_breaking_block(pos, mining_cache)
|
||||
+ self.cost_for_breaking_block(pos.up(1), mining_cache)
|
||||
}
|
||||
|
||||
/// Whether we can stand in this position. Checks if the block below is
|
||||
/// solid, and that the two blocks above that are passable.
|
||||
|
||||
pub fn is_standable(&self, pos: BlockPos) -> bool {
|
||||
self.is_block_solid(pos.down(1)) && self.is_passable(pos)
|
||||
}
|
||||
|
||||
pub fn cost_for_standing(&self, pos: BlockPos, mining_cache: &MiningCache) -> f32 {
|
||||
if !self.is_block_solid(pos.down(1)) {
|
||||
return f32::INFINITY;
|
||||
}
|
||||
self.cost_for_passing(pos, mining_cache)
|
||||
}
|
||||
|
||||
/// Get the amount of air blocks until the next solid block below this one.
|
||||
pub fn fall_distance(&self, pos: BlockPos) -> u32 {
|
||||
let mut distance = 0;
|
||||
|
@ -235,7 +440,10 @@ impl CachedWorld {
|
|||
}
|
||||
|
||||
/// whether this block is passable
|
||||
fn is_block_state_passable(block: BlockState) -> bool {
|
||||
pub fn is_block_state_passable(block: BlockState) -> bool {
|
||||
// i already tried optimizing this by having it cache in an IntMap/FxHashMap but
|
||||
// it wasn't measurably faster
|
||||
|
||||
if block.is_air() {
|
||||
// fast path
|
||||
return true;
|
||||
|
@ -243,7 +451,8 @@ fn is_block_state_passable(block: BlockState) -> bool {
|
|||
if !block.is_shape_empty() {
|
||||
return false;
|
||||
}
|
||||
if block == azalea_registry::Block::Water.into() {
|
||||
let registry_block = azalea_registry::Block::from(block);
|
||||
if registry_block == azalea_registry::Block::Water {
|
||||
return false;
|
||||
}
|
||||
if block
|
||||
|
@ -252,7 +461,7 @@ fn is_block_state_passable(block: BlockState) -> bool {
|
|||
{
|
||||
return false;
|
||||
}
|
||||
if block == azalea_registry::Block::Lava.into() {
|
||||
if registry_block == azalea_registry::Block::Lava {
|
||||
return false;
|
||||
}
|
||||
// block.waterlogged currently doesn't account for seagrass and some other water
|
||||
|
@ -261,11 +470,18 @@ fn is_block_state_passable(block: BlockState) -> bool {
|
|||
return false;
|
||||
}
|
||||
|
||||
// don't walk into fire
|
||||
if registry_block == azalea_registry::Block::Fire
|
||||
|| registry_block == azalea_registry::Block::SoulFire
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// whether this block has a solid hitbox (i.e. we can stand on it)
|
||||
fn is_block_state_solid(block: BlockState) -> bool {
|
||||
pub fn is_block_state_solid(block: BlockState) -> bool {
|
||||
if block.is_air() {
|
||||
// fast path
|
||||
return false;
|
||||
|
|
Loading…
Add table
Reference in a new issue