mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 14:26:04 +00:00
add pathfinder benchmark
This commit is contained in:
parent
37146f46f0
commit
4f6ab28325
9 changed files with 140 additions and 33 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -183,6 +183,7 @@ dependencies = [
|
||||||
"bevy_log",
|
"bevy_log",
|
||||||
"bevy_tasks",
|
"bevy_tasks",
|
||||||
"bevy_time",
|
"bevy_time",
|
||||||
|
"criterion",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
|
@ -191,6 +192,7 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"priority-queue",
|
"priority-queue",
|
||||||
|
"rand",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
|
|
@ -279,6 +279,7 @@ impl Chunk {
|
||||||
get_block_state_from_sections(&self.sections, pos, min_y)
|
get_block_state_from_sections(&self.sections, pos, min_y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use = "Use Chunk::set instead if you don't need the previous state"]
|
||||||
pub fn get_and_set(
|
pub fn get_and_set(
|
||||||
&mut self,
|
&mut self,
|
||||||
pos: &ChunkBlockPos,
|
pos: &ChunkBlockPos,
|
||||||
|
|
|
@ -172,7 +172,7 @@ impl PalettedContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Palette::Linear(palette) => {
|
Palette::Linear(palette) => {
|
||||||
if let Some(index) = palette.iter().position(|v| *v == value) {
|
if let Some(index) = palette.iter().position(|&v| v == value) {
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
let capacity = 2usize.pow(self.bits_per_entry.into());
|
let capacity = 2usize.pow(self.bits_per_entry.into());
|
||||||
|
|
|
@ -43,7 +43,15 @@ bevy_log = "0.11.2"
|
||||||
azalea-entity = { version = "0.8.0", path = "../azalea-entity" }
|
azalea-entity = { version = "0.8.0", path = "../azalea-entity" }
|
||||||
bevy_time = "0.11.2"
|
bevy_time = "0.11.2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
criterion = "0.5.1"
|
||||||
|
rand = "0.8.5"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["log"]
|
default = ["log"]
|
||||||
# enables bevy_log::LogPlugin by default
|
# enables bevy_log::LogPlugin by default
|
||||||
log = ["azalea-client/log"]
|
log = ["azalea-client/log"]
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "pathfinder"
|
||||||
|
harness = false
|
||||||
|
|
98
azalea/benches/pathfinder.rs
Normal file
98
azalea/benches/pathfinder.rs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
use std::{hint::black_box, time::Duration};
|
||||||
|
|
||||||
|
use azalea::{
|
||||||
|
pathfinder::{
|
||||||
|
astar::{self, a_star},
|
||||||
|
goals::BlockPosGoal,
|
||||||
|
Goal,
|
||||||
|
},
|
||||||
|
BlockPos,
|
||||||
|
};
|
||||||
|
use azalea_core::position::{ChunkBlockPos, ChunkPos};
|
||||||
|
use azalea_world::{Chunk, ChunkStorage, PartialChunkStorage};
|
||||||
|
use criterion::{criterion_group, criterion_main, Criterion};
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
fn generate_bedrock_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 = rand::thread_rng();
|
||||||
|
|
||||||
|
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 x in 0..16_u8 {
|
||||||
|
for z in 0..16_u8 {
|
||||||
|
chunk.set(
|
||||||
|
&ChunkBlockPos::new(x, 1, z),
|
||||||
|
azalea_registry::Block::Bedrock.into(),
|
||||||
|
chunks.min_y,
|
||||||
|
);
|
||||||
|
if rng.gen_bool(0.5) {
|
||||||
|
chunk.set(
|
||||||
|
&ChunkBlockPos::new(x, 2, z),
|
||||||
|
azalea_registry::Block::Bedrock.into(),
|
||||||
|
chunks.min_y,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut start = BlockPos::new(-64, 4, -64);
|
||||||
|
// move start down until it's on bedrock
|
||||||
|
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
|
||||||
|
while chunks.get_block_state(&end).unwrap().is_air() {
|
||||||
|
end = end.down(1);
|
||||||
|
}
|
||||||
|
end = end.up(1);
|
||||||
|
|
||||||
|
(chunks, start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_pathfinder(c: &mut Criterion) {
|
||||||
|
c.bench_function("bedrock", |b| {
|
||||||
|
let mut partial_chunks = PartialChunkStorage::new(32);
|
||||||
|
let successors_fn = azalea::pathfinder::moves::parkour::parkour_move;
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
let (world, start, end) = generate_bedrock_world(&mut partial_chunks, 4);
|
||||||
|
let goal = BlockPosGoal(end);
|
||||||
|
|
||||||
|
let successors = |pos: BlockPos| successors_fn(&world, pos);
|
||||||
|
|
||||||
|
let astar::Path { movements, partial } = a_star(
|
||||||
|
start,
|
||||||
|
|n| goal.heuristic(n),
|
||||||
|
successors,
|
||||||
|
|n| goal.success(n),
|
||||||
|
Duration::MAX,
|
||||||
|
);
|
||||||
|
|
||||||
|
black_box((movements, partial));
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, bench_pathfinder);
|
||||||
|
criterion_main!(benches);
|
|
@ -1,10 +1,10 @@
|
||||||
//! A pathfinding plugin to make bots navigate the world. A lot of this code is
|
//! A pathfinding plugin to make bots navigate the world. A lot of this code is
|
||||||
//! based on [Baritone](https://github.com/cabaletta/baritone).
|
//! based on [Baritone](https://github.com/cabaletta/baritone).
|
||||||
|
|
||||||
mod astar;
|
pub mod astar;
|
||||||
pub mod costs;
|
pub mod costs;
|
||||||
pub mod goals;
|
pub mod goals;
|
||||||
mod moves;
|
pub mod moves;
|
||||||
pub mod simulation;
|
pub mod simulation;
|
||||||
|
|
||||||
use crate::bot::{JumpEvent, LookAtEvent};
|
use crate::bot::{JumpEvent, LookAtEvent};
|
||||||
|
@ -177,10 +177,8 @@ fn goto_listener(
|
||||||
let task = thread_pool.spawn(async move {
|
let task = thread_pool.spawn(async move {
|
||||||
debug!("start: {start:?}");
|
debug!("start: {start:?}");
|
||||||
|
|
||||||
let successors = |pos: BlockPos| {
|
let world = &world_lock.read().chunks;
|
||||||
let world = world_lock.read();
|
let successors = |pos: BlockPos| successors_fn(world, pos);
|
||||||
successors_fn(&world, pos)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut attempt_number = 0;
|
let mut attempt_number = 0;
|
||||||
|
|
||||||
|
@ -279,8 +277,8 @@ fn path_found_listener(
|
||||||
);
|
);
|
||||||
let successors_fn: moves::SuccessorsFn = event.successors_fn;
|
let successors_fn: moves::SuccessorsFn = event.successors_fn;
|
||||||
let successors = |pos: BlockPos| {
|
let successors = |pos: BlockPos| {
|
||||||
let world = world_lock.read();
|
let world = &world_lock.read().chunks;
|
||||||
successors_fn(&world, pos)
|
successors_fn(world, pos)
|
||||||
};
|
};
|
||||||
|
|
||||||
if successors(last_node.target)
|
if successors(last_node.target)
|
||||||
|
@ -442,8 +440,8 @@ fn tick_execute_path(
|
||||||
{
|
{
|
||||||
// obstruction check (the path we're executing isn't possible anymore)
|
// obstruction check (the path we're executing isn't possible anymore)
|
||||||
let successors = |pos: BlockPos| {
|
let successors = |pos: BlockPos| {
|
||||||
let world = world_lock.read();
|
let world = &world_lock.read().chunks;
|
||||||
successors_fn(&world, pos)
|
successors_fn(world, pos)
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(last_reached_node) = pathfinder.last_reached_node {
|
if let Some(last_reached_node) = pathfinder.last_reached_node {
|
||||||
|
|
|
@ -5,7 +5,7 @@ use azalea_core::{
|
||||||
direction::CardinalDirection,
|
direction::CardinalDirection,
|
||||||
position::{BlockPos, Vec3},
|
position::{BlockPos, Vec3},
|
||||||
};
|
};
|
||||||
use azalea_world::Instance;
|
use azalea_world::ChunkStorage;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
pathfinder::{astar, costs::*},
|
pathfinder::{astar, costs::*},
|
||||||
|
@ -17,7 +17,7 @@ use super::{
|
||||||
ExecuteCtx, IsReachedCtx, MoveData,
|
ExecuteCtx, IsReachedCtx, MoveData,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn basic_move(world: &Instance, node: BlockPos) -> Vec<Edge> {
|
pub fn basic_move(world: &ChunkStorage, node: BlockPos) -> Vec<Edge> {
|
||||||
let mut edges = Vec::new();
|
let mut edges = Vec::new();
|
||||||
edges.extend(forward_move(world, node));
|
edges.extend(forward_move(world, node));
|
||||||
edges.extend(ascend_move(world, node));
|
edges.extend(ascend_move(world, node));
|
||||||
|
@ -26,7 +26,7 @@ pub fn basic_move(world: &Instance, node: BlockPos) -> Vec<Edge> {
|
||||||
edges
|
edges
|
||||||
}
|
}
|
||||||
|
|
||||||
fn forward_move(world: &Instance, pos: BlockPos) -> Vec<Edge> {
|
fn forward_move(world: &ChunkStorage, pos: BlockPos) -> Vec<Edge> {
|
||||||
let mut edges = Vec::new();
|
let mut edges = Vec::new();
|
||||||
for dir in CardinalDirection::iter() {
|
for dir in CardinalDirection::iter() {
|
||||||
let offset = BlockPos::new(dir.x(), 0, dir.z());
|
let offset = BlockPos::new(dir.x(), 0, dir.z());
|
||||||
|
@ -72,7 +72,7 @@ fn execute_forward_move(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ascend_move(world: &Instance, pos: BlockPos) -> Vec<Edge> {
|
fn ascend_move(world: &ChunkStorage, pos: BlockPos) -> Vec<Edge> {
|
||||||
let mut edges = Vec::new();
|
let mut edges = Vec::new();
|
||||||
for dir in CardinalDirection::iter() {
|
for dir in CardinalDirection::iter() {
|
||||||
let offset = BlockPos::new(dir.x(), 1, dir.z());
|
let offset = BlockPos::new(dir.x(), 1, dir.z());
|
||||||
|
@ -156,7 +156,7 @@ pub fn ascend_is_reached(
|
||||||
BlockPos::from(position) == target || BlockPos::from(position) == target.down(1)
|
BlockPos::from(position) == target || BlockPos::from(position) == target.down(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn descend_move(world: &Instance, pos: BlockPos) -> Vec<Edge> {
|
fn descend_move(world: &ChunkStorage, pos: BlockPos) -> Vec<Edge> {
|
||||||
let mut edges = Vec::new();
|
let mut edges = Vec::new();
|
||||||
for dir in CardinalDirection::iter() {
|
for dir in CardinalDirection::iter() {
|
||||||
let dir_delta = BlockPos::new(dir.x(), 0, dir.z());
|
let dir_delta = BlockPos::new(dir.x(), 0, dir.z());
|
||||||
|
@ -258,7 +258,7 @@ pub fn descend_is_reached(
|
||||||
&& (position.y - target.y as f64) < 0.5
|
&& (position.y - target.y as f64) < 0.5
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diagonal_move(world: &Instance, pos: BlockPos) -> Vec<Edge> {
|
fn diagonal_move(world: &ChunkStorage, pos: BlockPos) -> Vec<Edge> {
|
||||||
let mut edges = Vec::new();
|
let mut edges = Vec::new();
|
||||||
for dir in CardinalDirection::iter() {
|
for dir in CardinalDirection::iter() {
|
||||||
let right = dir.right();
|
let right = dir.right();
|
||||||
|
|
|
@ -9,13 +9,13 @@ use super::astar;
|
||||||
use azalea_client::{StartSprintEvent, StartWalkEvent};
|
use azalea_client::{StartSprintEvent, StartWalkEvent};
|
||||||
use azalea_core::position::{BlockPos, Vec3};
|
use azalea_core::position::{BlockPos, Vec3};
|
||||||
use azalea_physics::collision::{self, BlockWithShape};
|
use azalea_physics::collision::{self, BlockWithShape};
|
||||||
use azalea_world::Instance;
|
use azalea_world::ChunkStorage;
|
||||||
use bevy_ecs::{entity::Entity, event::EventWriter};
|
use bevy_ecs::{entity::Entity, event::EventWriter};
|
||||||
|
|
||||||
type Edge = astar::Edge<BlockPos, MoveData>;
|
type Edge = astar::Edge<BlockPos, MoveData>;
|
||||||
|
|
||||||
pub type SuccessorsFn =
|
pub type SuccessorsFn =
|
||||||
fn(&azalea_world::Instance, BlockPos) -> Vec<astar::Edge<BlockPos, MoveData>>;
|
fn(&azalea_world::ChunkStorage, BlockPos) -> Vec<astar::Edge<BlockPos, MoveData>>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MoveData {
|
pub struct MoveData {
|
||||||
|
@ -34,8 +34,8 @@ impl Debug for MoveData {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// whether this block is passable
|
/// whether this block is passable
|
||||||
fn is_block_passable(pos: &BlockPos, world: &Instance) -> bool {
|
fn is_block_passable(pos: &BlockPos, world: &ChunkStorage) -> bool {
|
||||||
if let Some(block) = world.chunks.get_block_state(pos) {
|
if let Some(block) = world.get_block_state(pos) {
|
||||||
if block.shape() != &collision::empty_shape() {
|
if block.shape() != &collision::empty_shape() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -58,8 +58,8 @@ fn is_block_passable(pos: &BlockPos, world: &Instance) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// whether this block has a solid hitbox (i.e. we can stand on it)
|
/// whether this block has a solid hitbox (i.e. we can stand on it)
|
||||||
fn is_block_solid(pos: &BlockPos, world: &Instance) -> bool {
|
fn is_block_solid(pos: &BlockPos, world: &ChunkStorage) -> bool {
|
||||||
if let Some(block) = world.chunks.get_block_state(pos) {
|
if let Some(block) = world.get_block_state(pos) {
|
||||||
block.shape() == &collision::block_shape()
|
block.shape() == &collision::block_shape()
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -67,26 +67,26 @@ fn is_block_solid(pos: &BlockPos, world: &Instance) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this block and the block above are passable
|
/// Whether this block and the block above are passable
|
||||||
fn is_passable(pos: &BlockPos, world: &Instance) -> bool {
|
fn is_passable(pos: &BlockPos, world: &ChunkStorage) -> bool {
|
||||||
is_block_passable(pos, world) && is_block_passable(&pos.up(1), world)
|
is_block_passable(pos, world) && is_block_passable(&pos.up(1), world)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether we can stand in this position. Checks if the block below is solid,
|
/// Whether we can stand in this position. Checks if the block below is solid,
|
||||||
/// and that the two blocks above that are passable.
|
/// and that the two blocks above that are passable.
|
||||||
|
|
||||||
fn is_standable(pos: &BlockPos, world: &Instance) -> bool {
|
fn is_standable(pos: &BlockPos, world: &ChunkStorage) -> bool {
|
||||||
is_block_solid(&pos.down(1), world) && is_passable(pos, world)
|
is_block_solid(&pos.down(1), world) && is_passable(pos, world)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the amount of air blocks until the next solid block below this one.
|
/// Get the amount of air blocks until the next solid block below this one.
|
||||||
fn fall_distance(pos: &BlockPos, world: &Instance) -> u32 {
|
fn fall_distance(pos: &BlockPos, world: &ChunkStorage) -> u32 {
|
||||||
let mut distance = 0;
|
let mut distance = 0;
|
||||||
let mut current_pos = pos.down(1);
|
let mut current_pos = pos.down(1);
|
||||||
while is_block_passable(¤t_pos, world) {
|
while is_block_passable(¤t_pos, world) {
|
||||||
distance += 1;
|
distance += 1;
|
||||||
current_pos = current_pos.down(1);
|
current_pos = current_pos.down(1);
|
||||||
|
|
||||||
if current_pos.y < world.chunks.min_y {
|
if current_pos.y < world.min_y {
|
||||||
return u32::MAX;
|
return u32::MAX;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,7 +115,7 @@ pub struct IsReachedCtx<'a> {
|
||||||
pub physics: &'a azalea_entity::Physics,
|
pub physics: &'a azalea_entity::Physics,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_move(world: &Instance, node: BlockPos) -> Vec<Edge> {
|
pub fn default_move(world: &ChunkStorage, node: BlockPos) -> Vec<Edge> {
|
||||||
let mut edges = Vec::new();
|
let mut edges = Vec::new();
|
||||||
edges.extend(basic::basic_move(world, node));
|
edges.extend(basic::basic_move(world, node));
|
||||||
edges.extend(parkour::parkour_move(world, node));
|
edges.extend(parkour::parkour_move(world, node));
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use azalea_client::{SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection};
|
use azalea_client::{SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection};
|
||||||
use azalea_core::{direction::CardinalDirection, position::BlockPos};
|
use azalea_core::{direction::CardinalDirection, position::BlockPos};
|
||||||
use azalea_world::Instance;
|
use azalea_world::ChunkStorage;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
pathfinder::{astar, costs::*},
|
pathfinder::{astar, costs::*},
|
||||||
|
@ -12,7 +12,7 @@ use super::{
|
||||||
ExecuteCtx, IsReachedCtx, MoveData,
|
ExecuteCtx, IsReachedCtx, MoveData,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn parkour_move(world: &Instance, node: BlockPos) -> Vec<Edge> {
|
pub fn parkour_move(world: &ChunkStorage, node: BlockPos) -> Vec<Edge> {
|
||||||
let mut edges = Vec::new();
|
let mut edges = Vec::new();
|
||||||
edges.extend(parkour_forward_1_move(world, node));
|
edges.extend(parkour_forward_1_move(world, node));
|
||||||
edges.extend(parkour_headhitter_forward_1_move(world, node));
|
edges.extend(parkour_headhitter_forward_1_move(world, node));
|
||||||
|
@ -20,7 +20,7 @@ pub fn parkour_move(world: &Instance, node: BlockPos) -> Vec<Edge> {
|
||||||
edges
|
edges
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parkour_forward_1_move(world: &Instance, pos: BlockPos) -> Vec<Edge> {
|
fn parkour_forward_1_move(world: &ChunkStorage, pos: BlockPos) -> Vec<Edge> {
|
||||||
let mut edges = Vec::new();
|
let mut edges = Vec::new();
|
||||||
for dir in CardinalDirection::iter() {
|
for dir in CardinalDirection::iter() {
|
||||||
let gap_offset = BlockPos::new(dir.x(), 0, dir.z());
|
let gap_offset = BlockPos::new(dir.x(), 0, dir.z());
|
||||||
|
@ -61,7 +61,7 @@ fn parkour_forward_1_move(world: &Instance, pos: BlockPos) -> Vec<Edge> {
|
||||||
edges
|
edges
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parkour_forward_2_move(world: &Instance, pos: BlockPos) -> Vec<Edge> {
|
fn parkour_forward_2_move(world: &ChunkStorage, pos: BlockPos) -> Vec<Edge> {
|
||||||
let mut edges = Vec::new();
|
let mut edges = Vec::new();
|
||||||
for dir in CardinalDirection::iter() {
|
for dir in CardinalDirection::iter() {
|
||||||
let gap_1_offset = BlockPos::new(dir.x(), 0, dir.z());
|
let gap_1_offset = BlockPos::new(dir.x(), 0, dir.z());
|
||||||
|
@ -112,7 +112,7 @@ fn parkour_forward_2_move(world: &Instance, pos: BlockPos) -> Vec<Edge> {
|
||||||
edges
|
edges
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parkour_headhitter_forward_1_move(world: &Instance, pos: BlockPos) -> Vec<Edge> {
|
fn parkour_headhitter_forward_1_move(world: &ChunkStorage, pos: BlockPos) -> Vec<Edge> {
|
||||||
let mut edges = Vec::new();
|
let mut edges = Vec::new();
|
||||||
for dir in CardinalDirection::iter() {
|
for dir in CardinalDirection::iter() {
|
||||||
let gap_offset = BlockPos::new(dir.x(), 0, dir.z());
|
let gap_offset = BlockPos::new(dir.x(), 0, dir.z());
|
||||||
|
|
Loading…
Add table
Reference in a new issue