diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2022-08-29 20:41:01 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-08-29 20:41:01 -0500 |
| commit | f42d630544165d11a544224ac273d6aaf89d8095 (patch) | |
| tree | 94bd73771ecb582d89a87cdca8e21b2d6573ef12 /azalea-physics/src | |
| parent | 2ea804401f54a45765860201d10d0569d07862ec (diff) | |
| download | azalea-drasl-f42d630544165d11a544224ac273d6aaf89d8095.tar.xz | |
Physics (#11)
* Put physics module in azalea-entity
* port aabb
* add more stuff to PositionXYZ
* azalea-physics
* important collision things
* more physics stuff
* backup because i'm about to delete shapes
* more shape stuff
* CubeVoxelShape
* no compile errors???
insane
* impl VoxelShape for ArrayVoxelShape
* Shapes stuff
* collide_x but it doesn't work yet
* binary_search
* it compiles
* Entity has bounding box
* Update discrete_voxel_shape.rs
* Entity::make_bounding_box
* ok i'm about to merge az-entity and az-world
might be a terrible idea which is why i'm committing first
* ok so i moved entity to world
* on_pos and move_entity compiles
* add send_position
* move collision stuff to collision module in az-physics
* dimension is no longer an Option
* start trying to do collision for the client
* collision works :tada:
* start adding palette resizing
* get_and_set (pain)
* it compiles but probably won't work
* add a test
* remove printlns
* add more tests for palette stuff
* ClientboundMoveVec3Packet -> ClientboundMoveEntityPosPacket
i think i changed this on accident once
* palette resizing works
todo: remove the printlns
* Remove printlns in palette.rs
* fix issues from merge
* fixes + work a bit more on physics
* Better entities (#19)
* well it compiles
* add tests to entity storage
* add suggestions in azalea-brigadier
* this probably causes ub
* fix brigadiersuggestions
* get rid of entityid
* test From<EntityMut> for EntityRef
* don't mention other libraries since there's too many
* fix warnings
* do todos in brigadier suggestions
* work on physics
* more physics stuff
* remove trait feature on az-block
i think rust gets confused and compiles the macro without the feature
* bump ahash
* aes tests in az-crypto
* optimize aes's deps
* fix crashes
* fix section_index for negative numbers and test
* fix BlockPos protocol implementation
* remove some debug prints
* prepare to add ai_step
* make ai step work
* clippy
Diffstat (limited to 'azalea-physics/src')
| -rw-r--r-- | azalea-physics/src/collision/dimension_collisions.rs | 137 | ||||
| -rw-r--r-- | azalea-physics/src/collision/discrete_voxel_shape.rs | 156 | ||||
| -rw-r--r-- | azalea-physics/src/collision/mod.rs | 273 | ||||
| -rw-r--r-- | azalea-physics/src/collision/shape.rs | 254 | ||||
| -rw-r--r-- | azalea-physics/src/lib.rs | 127 |
5 files changed, 947 insertions, 0 deletions
diff --git a/azalea-physics/src/collision/dimension_collisions.rs b/azalea-physics/src/collision/dimension_collisions.rs new file mode 100644 index 00000000..59514fda --- /dev/null +++ b/azalea-physics/src/collision/dimension_collisions.rs @@ -0,0 +1,137 @@ +use crate::collision::{VoxelShape, AABB}; +use azalea_block::BlockState; +use azalea_core::{ChunkPos, ChunkSectionPos, Cursor3d, CursorIterationType, EPSILON}; +use azalea_world::entity::EntityData; +use azalea_world::{Chunk, Dimension}; +use std::sync::{Arc, Mutex}; + +pub trait CollisionGetter { + fn get_block_collisions<'a>( + &'a self, + entity: Option<&EntityData>, + aabb: AABB, + ) -> BlockCollisions<'a>; +} + +impl CollisionGetter for Dimension { + fn get_block_collisions<'a>( + &'a self, + entity: Option<&EntityData>, + aabb: AABB, + ) -> BlockCollisions<'a> { + BlockCollisions::new(self, entity, aabb) + } +} + +pub struct BlockCollisions<'a> { + pub dimension: &'a Dimension, + // context: CollisionContext, + pub aabb: AABB, + + pub cursor: Cursor3d, + pub only_suffocating_blocks: bool, +} + +impl<'a> BlockCollisions<'a> { + pub fn new(dimension: &'a Dimension, _entity: Option<&EntityData>, aabb: AABB) -> Self { + let origin_x = (aabb.min_x - EPSILON) as i32 - 1; + let origin_y = (aabb.min_y - EPSILON) as i32 - 1; + let origin_z = (aabb.min_z - EPSILON) as i32 - 1; + + let end_x = (aabb.max_x + EPSILON) as i32 + 1; + let end_y = (aabb.max_y + EPSILON) as i32 + 1; + let end_z = (aabb.max_z + EPSILON) as i32 + 1; + + let cursor = Cursor3d::new(origin_x, origin_y, origin_z, end_x, end_y, end_z); + + Self { + dimension, + aabb, + cursor, + only_suffocating_blocks: false, + } + } + + fn get_chunk(&self, block_x: i32, block_z: i32) -> Option<&Arc<Mutex<Chunk>>> { + let chunk_x = ChunkSectionPos::block_to_section_coord(block_x); + let chunk_z = ChunkSectionPos::block_to_section_coord(block_z); + let chunk_pos = ChunkPos::new(chunk_x, chunk_z); + + // TODO: minecraft caches chunk here + // int chunkX = SectionPos.blockToSectionCoord(blockX); + // int chunkZ = SectionPos.blockToSectionCoord(blockZ); + // long chunkPosLong = ChunkPos.asLong(chunkX, chunkZ); + // if (this.cachedBlockGetter != null && this.cachedBlockGetterPos == var5) { + // return this.cachedBlockGetter; + // } else { + // BlockGetter var7 = this.collisionGetter.getChunkForCollisions(chunkX, chunkZ); + // this.cachedBlockGetter = var7; + // this.cachedBlockGetterPos = chunkPosLong; + // return var7; + // } + + self.dimension[&chunk_pos].as_ref() + } +} + +impl<'a> Iterator for BlockCollisions<'a> { + type Item = Box<dyn VoxelShape>; + + fn next(&mut self) -> Option<Self::Item> { + while let Some(item) = self.cursor.next() { + if item.iteration_type == CursorIterationType::Corner { + continue; + } + + let chunk = self.get_chunk(item.pos.x, item.pos.z); + let chunk = match chunk { + Some(chunk) => chunk, + None => continue, + }; + let chunk_lock = chunk.lock().unwrap(); + + let pos = item.pos; + println!("getting block at {:?}", pos); + let block_state: BlockState = chunk_lock.get(&(&pos).into(), self.dimension.min_y()); + // let block: Box<dyn Block> = block_state.into(); + + // TODO: continue if self.only_suffocating_blocks and the block is not suffocating + + let block_shape = if block_state == BlockState::Air { + crate::collision::empty_shape() + } else { + crate::collision::block_shape() + }; + // let block_shape = block.get_collision_shape(); + // if block_shape == Shapes::block() { + if true { + // TODO: this can be optimized + if !self.aabb.intersects_aabb(&AABB { + min_x: item.pos.x as f64, + min_y: item.pos.y as f64, + min_z: item.pos.z as f64, + max_x: (item.pos.x + 1) as f64, + max_y: (item.pos.y + 1) as f64, + max_z: (item.pos.z + 1) as f64, + }) { + continue; + } + + return Some(block_shape.move_relative( + item.pos.x as f64, + item.pos.y as f64, + item.pos.z as f64, + )); + } + + // let block_shape = block_shape.move_relative(item.pos.x, item.pos.y, item.pos.z); + // if (!Shapes.joinIsNotEmpty(block_shape, this.entityShape, BooleanOp.AND)) { + // continue; + // } + + // return block_shape; + } + + None + } +} diff --git a/azalea-physics/src/collision/discrete_voxel_shape.rs b/azalea-physics/src/collision/discrete_voxel_shape.rs new file mode 100644 index 00000000..6eb425ce --- /dev/null +++ b/azalea-physics/src/collision/discrete_voxel_shape.rs @@ -0,0 +1,156 @@ +use azalea_core::{Axis, AxisCycle, BitSet}; + +// TODO: every impl of DiscreteVoxelShape could be turned into a single enum as an optimization + +pub trait DiscreteVoxelShape { + fn size(&self, axis: Axis) -> u32; + + fn first_full_x(&self) -> u32; + fn first_full_y(&self) -> u32; + fn first_full_z(&self) -> u32; + + fn last_full_x(&self) -> u32; + fn last_full_y(&self) -> u32; + fn last_full_z(&self) -> u32; + + fn is_empty(&self) -> bool { + if self.first_full_x() >= self.last_full_x() { + return true; + } + if self.first_full_y() >= self.last_full_y() { + return true; + } + if self.first_full_x() >= self.last_full_x() { + return true; + } + false + } + + fn is_full_wide(&self, x: u32, y: u32, z: u32) -> bool { + (x < self.size(Axis::X) && y < self.size(Axis::Y) && z < self.size(Axis::Z)) + && (self.is_full(x, y, z)) + } + fn is_full_wide_axis_cycle(&self, axis_cycle: AxisCycle, x: u32, y: u32, z: u32) -> bool { + self.is_full_wide( + axis_cycle.cycle_xyz(x, y, z, Axis::X), + axis_cycle.cycle_xyz(x, y, z, Axis::Y), + axis_cycle.cycle_xyz(x, y, z, Axis::Z), + ) + } + + fn is_full(&self, x: u32, y: u32, z: u32) -> bool; + + // i don't know how to do this properly + fn clone(&self) -> Box<dyn DiscreteVoxelShape>; +} + +#[derive(Default, Clone)] +pub struct BitSetDiscreteVoxelShape { + x_size: u32, + y_size: u32, + z_size: u32, + + storage: BitSet, + x_min: u32, + y_min: u32, + z_min: u32, + x_max: u32, + y_max: u32, + z_max: u32, +} + +impl BitSetDiscreteVoxelShape { + // public BitSetDiscreteVoxelShape(int var1, int var2, int var3) { + // super(var1, var2, var3); + // this.storage = new BitSet(var1 * var2 * var3); + // this.xMin = var1; + // this.yMin = var2; + // this.zMin = var3; + // } + pub fn new(x_min: u32, y_min: u32, z_min: u32) -> Self { + BitSetDiscreteVoxelShape { + x_size: x_min, + y_size: y_min, + z_size: z_min, + + storage: BitSet::new((x_min * y_min * z_min).try_into().unwrap()), + x_min, + y_min, + z_min, + x_max: 0, + y_max: 0, + z_max: 0, + } + } + + // private void fillUpdateBounds(int var1, int var2, int var3, boolean var4) { + // this.storage.set(this.getIndex(var1, var2, var3)); + // if (var4) { + // this.xMin = Math.min(this.xMin, var1); + // this.yMin = Math.min(this.yMin, var2); + // this.zMin = Math.min(this.zMin, var3); + // this.xMax = Math.max(this.xMax, var1 + 1); + // this.yMax = Math.max(this.yMax, var2 + 1); + // this.zMax = Math.max(this.zMax, var3 + 1); + // } + // } + fn fill_update_bounds(&mut self, x: u32, y: u32, z: u32, update: bool) { + self.storage.set(self.get_index(x, y, z)); + if update { + self.x_min = std::cmp::min(self.x_min, x); + self.y_min = std::cmp::min(self.y_min, y); + self.z_min = std::cmp::min(self.z_min, z); + self.x_max = std::cmp::max(self.x_max, x + 1); + self.y_max = std::cmp::max(self.y_max, y + 1); + self.z_max = std::cmp::max(self.z_max, z + 1); + } + } + + // public void fill(int var1, int var2, int var3) { + // this.fillUpdateBounds(var1, var2, var3, true); + // } + pub fn fill(&mut self, x: u32, y: u32, z: u32) { + self.fill_update_bounds(x, y, z, true); + } + + // protected int getIndex(int var1, int var2, int var3) { + // return (var1 * this.ySize + var2) * this.zSize + var3; + // } + fn get_index(&self, x: u32, y: u32, z: u32) -> usize { + ((x * self.y_size + y) * self.z_size + z) as usize + } +} + +impl DiscreteVoxelShape for BitSetDiscreteVoxelShape { + fn size(&self, axis: Axis) -> u32 { + axis.choose(self.x_size, self.y_size, self.z_size) + } + + fn first_full_x(&self) -> u32 { + self.x_min + } + fn first_full_y(&self) -> u32 { + self.y_min + } + fn first_full_z(&self) -> u32 { + self.z_min + } + + fn last_full_x(&self) -> u32 { + self.x_max + } + fn last_full_y(&self) -> u32 { + self.y_max + } + fn last_full_z(&self) -> u32 { + self.z_max + } + + fn clone(&self) -> Box<dyn DiscreteVoxelShape> { + Box::new(Clone::clone(self)) + } + + fn is_full(&self, x: u32, y: u32, z: u32) -> bool { + self.storage.index(self.get_index(x, y, z)) + } +} diff --git a/azalea-physics/src/collision/mod.rs b/azalea-physics/src/collision/mod.rs new file mode 100644 index 00000000..465698b2 --- /dev/null +++ b/azalea-physics/src/collision/mod.rs @@ -0,0 +1,273 @@ +mod dimension_collisions; +mod discrete_voxel_shape; +mod shape; + +use azalea_core::{Axis, PositionXYZ, Vec3, AABB, EPSILON}; +use azalea_world::entity::{EntityData, EntityMut}; +use azalea_world::{Dimension, MoveEntityError}; +use dimension_collisions::CollisionGetter; +pub use discrete_voxel_shape::*; +pub use shape::*; + +pub enum MoverType { + Own, + Player, + Piston, + ShulkerBox, + Shulker, +} + +pub trait HasCollision { + fn collide(&self, movement: &Vec3, entity: &EntityData) -> Vec3; +} + +pub trait MovableEntity { + fn move_colliding( + &mut self, + mover_type: &MoverType, + movement: &Vec3, + ) -> Result<(), MoveEntityError>; +} + +impl HasCollision for Dimension { + // private Vec3 collide(Vec3 var1) { + // AABB var2 = this.getBoundingBox(); + // List var3 = this.level.getEntityCollisions(this, var2.expandTowards(var1)); + // Vec3 var4 = var1.lengthSqr() == 0.0D ? var1 : collideBoundingBox(this, var1, var2, this.level, var3); + // boolean var5 = var1.x != var4.x; + // boolean var6 = var1.y != var4.y; + // boolean var7 = var1.z != var4.z; + // boolean var8 = this.onGround || var6 && var1.y < 0.0D; + // if (this.maxUpStep > 0.0F && var8 && (var5 || var7)) { + // Vec3 var9 = collideBoundingBox(this, new Vec3(var1.x, (double)this.maxUpStep, var1.z), var2, this.level, var3); + // Vec3 var10 = collideBoundingBox(this, new Vec3(0.0D, (double)this.maxUpStep, 0.0D), var2.expandTowards(var1.x, 0.0D, var1.z), this.level, var3); + // if (var10.y < (double)this.maxUpStep) { + // Vec3 var11 = collideBoundingBox(this, new Vec3(var1.x, 0.0D, var1.z), var2.move(var10), this.level, var3).add(var10); + // if (var11.horizontalDistanceSqr() > var9.horizontalDistanceSqr()) { + // var9 = var11; + // } + // } + + // if (var9.horizontalDistanceSqr() > var4.horizontalDistanceSqr()) { + // return var9.add(collideBoundingBox(this, new Vec3(0.0D, -var9.y + var1.y, 0.0D), var2.move(var9), this.level, var3)); + // } + // } + + // return var4; + // } + fn collide(&self, movement: &Vec3, entity: &EntityData) -> Vec3 { + let entity_bounding_box = entity.bounding_box; + println!("collide: entity_bounding_box: {:?}", entity_bounding_box); + // TODO: get_entity_collisions + // let entity_collisions = dimension.get_entity_collisions(self, entity_bounding_box.expand_towards(movement)); + let entity_collisions = Vec::new(); + if movement.length_sqr() == 0.0 { + *movement + } else { + collide_bounding_box( + Some(entity), + movement, + &entity_bounding_box, + self, + entity_collisions, + ) + } + + // TODO: stepping (for stairs and stuff) + + // collided_movement + } +} + +impl MovableEntity for EntityMut<'_> { + /// Move an entity by a given delta, checking for collisions. + fn move_colliding( + &mut self, + _mover_type: &MoverType, + movement: &Vec3, + ) -> Result<(), MoveEntityError> { + // TODO: do all these + + // if self.no_physics { + // return; + // }; + + // if (var1 == MoverType.PISTON) { + // var2 = this.limitPistonMovement(var2); + // if (var2.equals(Vec3.ZERO)) { + // return; + // } + // } + + // if (this.stuckSpeedMultiplier.lengthSqr() > 1.0E-7D) { + // var2 = var2.multiply(this.stuckSpeedMultiplier); + // this.stuckSpeedMultiplier = Vec3.ZERO; + // this.setDeltaMovement(Vec3.ZERO); + // } + + // movement = this.maybeBackOffFromEdge(movement, moverType); + + println!("move_entity {:?}", movement); + + let collide_result = { self.dimension.collide(movement, self) }; + + let move_distance = collide_result.length_sqr(); + + println!("move_entity move_distance: {}", move_distance); + + if move_distance > EPSILON { + // TODO: fall damage + + let new_pos = { + let entity_pos = self.pos(); + Vec3 { + x: entity_pos.x + collide_result.x, + y: entity_pos.y + collide_result.y, + z: entity_pos.z + collide_result.z, + } + }; + + self.dimension.set_entity_pos(self.id, new_pos)?; + + println!("move_entity set_entity_pos {:?}", new_pos) + } + + let x_collision = movement.x != collide_result.x; + let z_collision = movement.z != collide_result.z; + let horizontal_collision = x_collision || z_collision; + let vertical_collision = movement.y != collide_result.y; + let on_ground = vertical_collision && movement.y < 0.; + // self.on_ground = on_ground; + + println!( + "move_entity {} {} {}", + x_collision, z_collision, vertical_collision + ); + + // TODO: minecraft checks for a "minor" horizontal collision here + + let block_pos_below = { self.on_pos_legacy() }; + let _block_state_below = self + .dimension + .get_block_state(&block_pos_below) + .expect("Couldn't get block state below"); + + println!("move_entity 4"); + // self.check_fall_damage(collide_result.y, on_ground, block_state_below, block_pos_below); + + // if self.isRemoved() { return; } + + if horizontal_collision { + let delta_movement = &self.delta; + self.delta = Vec3 { + x: if x_collision { 0. } else { delta_movement.x }, + y: delta_movement.y, + z: if z_collision { 0. } else { delta_movement.z }, + } + } + + if vertical_collision { + // blockBelow.updateEntityAfterFallOn(this.level, this); + } + + if on_ground { + // blockBelow.stepOn(this.level, blockPosBelow, blockStateBelow, this); + } + + // sounds + + // this.tryCheckInsideBlocks(); + + // float var25 = this.getBlockSpeedFactor(); + // this.setDeltaMovement(this.getDeltaMovement().multiply((double)var25, 1.0D, (double)var25)); + // if (this.level.getBlockStatesIfLoaded(this.getBoundingBox().deflate(1.0E-6D)).noneMatch((var0) -> { + // return var0.is(BlockTags.FIRE) || var0.is(Blocks.LAVA); + // })) { + // if (this.remainingFireTicks <= 0) { + // this.setRemainingFireTicks(-this.getFireImmuneTicks()); + // } + + // if (this.wasOnFire && (this.isInPowderSnow || this.isInWaterRainOrBubble())) { + // this.playEntityOnFireExtinguishedSound(); + // } + // } + + // if (this.isOnFire() && (this.isInPowderSnow || this.isInWaterRainOrBubble())) { + // this.setRemainingFireTicks(-this.getFireImmuneTicks()); + // } + + println!("move_entity 5"); + + Ok(()) + } +} + +fn collide_bounding_box( + entity: Option<&EntityData>, + movement: &Vec3, + entity_bounding_box: &AABB, + dimension: &Dimension, + entity_collisions: Vec<Box<dyn VoxelShape>>, +) -> Vec3 { + let mut collision_boxes: Vec<Box<dyn VoxelShape>> = + Vec::with_capacity(entity_collisions.len() + 1); + + if !entity_collisions.is_empty() { + collision_boxes.extend(entity_collisions); + } + + // TODO: world border + + let block_collisions = + dimension.get_block_collisions(entity, entity_bounding_box.expand_towards(movement)); + collision_boxes.extend(block_collisions); + collide_with_shapes(movement, *entity_bounding_box, &collision_boxes) +} + +fn collide_with_shapes( + movement: &Vec3, + mut entity_box: AABB, + collision_boxes: &Vec<Box<dyn VoxelShape>>, +) -> Vec3 { + if collision_boxes.is_empty() { + return *movement; + } + + let mut x_movement = movement.x; + let mut y_movement = movement.y; + let mut z_movement = movement.z; + if y_movement != 0. { + y_movement = Shapes::collide(&Axis::Y, &entity_box, collision_boxes, y_movement); + if y_movement != 0. { + entity_box = entity_box.move_relative(0., y_movement, 0.); + } + } + + // whether the player is moving more in the z axis than x + // this is done to fix a movement bug, minecraft does this too + let more_z_movement = x_movement.abs() < z_movement.abs(); + + if more_z_movement && z_movement != 0. { + z_movement = Shapes::collide(&Axis::Z, &entity_box, collision_boxes, z_movement); + if z_movement != 0. { + entity_box = entity_box.move_relative(0., 0., z_movement); + } + } + + if x_movement != 0. { + x_movement = Shapes::collide(&Axis::X, &entity_box, collision_boxes, x_movement); + if x_movement != 0. { + entity_box = entity_box.move_relative(x_movement, 0., 0.); + } + } + + if !more_z_movement && z_movement != 0. { + z_movement = Shapes::collide(&Axis::Z, &entity_box, collision_boxes, z_movement); + } + + Vec3 { + x: x_movement, + y: y_movement, + z: z_movement, + } +} diff --git a/azalea-physics/src/collision/shape.rs b/azalea-physics/src/collision/shape.rs new file mode 100644 index 00000000..45822d07 --- /dev/null +++ b/azalea-physics/src/collision/shape.rs @@ -0,0 +1,254 @@ +use crate::collision::{BitSetDiscreteVoxelShape, DiscreteVoxelShape, AABB}; +use azalea_core::{binary_search, Axis, AxisCycle, EPSILON}; +use std::cmp; + +pub struct Shapes {} + +pub fn block_shape() -> Box<dyn VoxelShape> { + let mut shape = BitSetDiscreteVoxelShape::new(1, 1, 1); + shape.fill(0, 0, 0); + Box::new(CubeVoxelShape::new(Box::new(shape))) +} +pub fn empty_shape() -> Box<dyn VoxelShape> { + Box::new(ArrayVoxelShape::new( + Box::new(BitSetDiscreteVoxelShape::new(0, 0, 0)), + vec![0.], + vec![0.], + vec![0.], + )) +} + +impl Shapes { + pub fn collide( + axis: &Axis, + entity_box: &AABB, + collision_boxes: &Vec<Box<dyn VoxelShape>>, + mut movement: f64, + ) -> f64 { + for shape in collision_boxes { + if movement.abs() < EPSILON { + return 0.; + } + movement = shape.collide(axis, entity_box, movement); + } + movement + } +} + +pub trait VoxelShape { + fn shape(&self) -> Box<dyn DiscreteVoxelShape>; + + fn get_coords(&self, axis: Axis) -> Vec<f64>; + + // TODO: optimization: should this be changed to return ArrayVoxelShape? + // i might change the implementation of empty_shape in the future so not 100% sure + fn move_relative(&self, x: f64, y: f64, z: f64) -> Box<dyn VoxelShape> { + if self.shape().is_empty() { + return empty_shape(); + } + + println!( + "making new voxel shape {:?} {:?} {:?}", + self.get_coords(Axis::X), + self.get_coords(Axis::Y), + self.get_coords(Axis::Z) + ); + + Box::new(ArrayVoxelShape::new( + self.shape(), + self.get_coords(Axis::X).iter().map(|c| c + x).collect(), + self.get_coords(Axis::Y).iter().map(|c| c + y).collect(), + self.get_coords(Axis::Z).iter().map(|c| c + z).collect(), + )) + } + + fn get(&self, axis: Axis, index: usize) -> f64 { + self.get_coords(axis)[index] + } + + fn find_index(&self, axis: Axis, coord: f64) -> u32 { + binary_search(0, self.shape().size(axis) + 1, &|t| { + coord < self.get(axis, t as usize) + }) - 1 + } + + fn collide(&self, axis: &Axis, entity_box: &AABB, movement: f64) -> f64 { + self.collide_x(AxisCycle::between(*axis, Axis::X), entity_box, movement) + } + fn collide_x(&self, axis_cycle: AxisCycle, entity_box: &AABB, mut movement: f64) -> f64 { + if self.shape().is_empty() { + return movement; + } + if movement.abs() < EPSILON { + return 0.; + } + + let inverse_axis_cycle = axis_cycle.inverse(); + + // probably not good names but idk what this does + let x_axis = inverse_axis_cycle.cycle(Axis::X); + let y_axis = inverse_axis_cycle.cycle(Axis::Y); + let z_axis = inverse_axis_cycle.cycle(Axis::Z); + + // i gave up on names at this point (these are the obfuscated names from fernflower) + let var9 = entity_box.max(&x_axis); + let var11 = entity_box.min(&x_axis); + + let var13 = self.find_index(x_axis, var11 + EPSILON); + let var14 = self.find_index(x_axis, var9 - EPSILON); + + let var15 = cmp::max( + 0, + self.find_index(y_axis, entity_box.min(&y_axis) + EPSILON), + ); + let var16 = cmp::min( + self.shape().size(y_axis), + self.find_index(y_axis, entity_box.max(&y_axis) - EPSILON) + 1, + ); + + let var17 = cmp::max( + 0, + self.find_index(z_axis, entity_box.min(&z_axis) + EPSILON), + ); + let var18 = cmp::min( + self.shape().size(z_axis), + self.find_index(z_axis, entity_box.max(&z_axis) - EPSILON) + 1, + ); + + let var19 = self.shape().size(x_axis); + + if movement > 0. { + for var20 in var14 + 1..var19 { + for var21 in var15..var16 { + for var22 in var17..var18 { + if self.shape().is_full_wide_axis_cycle( + inverse_axis_cycle, + var20, + var21, + var22, + ) { + let var23 = self.get(x_axis, var20 as usize) - var9; + if var23 >= -EPSILON { + movement = f64::min(movement, var23); + } + return movement; + } + } + } + } + } else if movement < 0. { + for var20 in (var13 - 1)..=0 { + for var21 in var15..var16 { + for var22 in var17..var18 { + if self.shape().is_full_wide_axis_cycle( + inverse_axis_cycle, + var20, + var21, + var22, + ) { + let var23 = self.get(x_axis, (var20 + 1) as usize) - var11; + if var23 <= EPSILON { + movement = f64::max(movement, var23); + } + return movement; + } + } + } + } + } + + movement + } +} + +pub struct ArrayVoxelShape { + shape: Box<dyn DiscreteVoxelShape>, + // TODO: check where faces is used in minecraft + #[allow(dead_code)] + faces: Option<Vec<Box<dyn VoxelShape>>>, + + pub xs: Vec<f64>, + pub ys: Vec<f64>, + pub zs: Vec<f64>, +} + +pub struct CubeVoxelShape { + shape: Box<dyn DiscreteVoxelShape>, + // TODO: check where faces is used in minecraft + #[allow(dead_code)] + faces: Option<Vec<Box<dyn VoxelShape>>>, +} + +impl ArrayVoxelShape { + pub fn new( + shape: Box<dyn DiscreteVoxelShape>, + xs: Vec<f64>, + ys: Vec<f64>, + zs: Vec<f64>, + ) -> Self { + let x_size = shape.size(Axis::X) + 1; + let y_size = shape.size(Axis::Y) + 1; + let z_size = shape.size(Axis::Z) + 1; + + // Lengths of point arrays must be consistent with the size of the VoxelShape. + assert_eq!(x_size, xs.len() as u32); + assert_eq!(y_size, ys.len() as u32); + assert_eq!(z_size, zs.len() as u32); + + Self { + faces: None, + shape, + xs, + ys, + zs, + } + } +} + +impl CubeVoxelShape { + pub fn new(shape: Box<dyn DiscreteVoxelShape>) -> Self { + Self { shape, faces: None } + } +} + +impl VoxelShape for ArrayVoxelShape { + fn shape(&self) -> Box<dyn DiscreteVoxelShape> { + self.shape.clone() + } + + fn get_coords(&self, axis: Axis) -> Vec<f64> { + axis.choose(self.xs.clone(), self.ys.clone(), self.zs.clone()) + } +} + +impl VoxelShape for CubeVoxelShape { + fn shape(&self) -> Box<dyn DiscreteVoxelShape> { + self.shape.clone() + } + + fn get_coords(&self, axis: Axis) -> Vec<f64> { + let size = self.shape.size(axis); + let mut parts = Vec::with_capacity(size as usize); + for i in 0..=size { + parts.push(i as f64 / size as f64); + } + parts + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_block_shape() { + let shape = block_shape(); + assert_eq!(shape.shape().size(Axis::X), 1); + assert_eq!(shape.shape().size(Axis::Y), 1); + assert_eq!(shape.shape().size(Axis::Z), 1); + + assert_eq!(shape.get_coords(Axis::X).len(), 2); + assert_eq!(shape.get_coords(Axis::Y).len(), 2); + assert_eq!(shape.get_coords(Axis::Z).len(), 2); + } +} diff --git a/azalea-physics/src/lib.rs b/azalea-physics/src/lib.rs new file mode 100644 index 00000000..8842727b --- /dev/null +++ b/azalea-physics/src/lib.rs @@ -0,0 +1,127 @@ +pub mod collision; + +use azalea_block::Block; +use azalea_core::{BlockPos, Vec3}; +use azalea_world::entity::{EntityData, EntityMut}; +use collision::{MovableEntity, MoverType}; + +pub trait HasPhysics { + fn travel(&mut self, acceleration: &Vec3); + fn ai_step(&mut self); +} + +impl HasPhysics for EntityMut<'_> { + /// Move the entity with the given acceleration while handling friction, + /// gravity, collisions, and some other stuff. + fn travel(&mut self, acceleration: &Vec3) { + // if !self.is_effective_ai() && !self.is_controlled_by_local_instance() { + // // this.calculateEntityAnimation(this, this instanceof FlyingAnimal); + // return; + // } + + let gravity: f64 = 0.08; + + // TODO: slow falling effect + // let is_falling = self.delta.y <= 0.; + + // TODO: fluids + + // TODO: elytra + + let block_pos_below = get_block_pos_below_that_affects_movement(self); + let block_friction = + if let Some(block_state_below) = self.dimension.get_block_state(&block_pos_below) { + let block_below: Box<dyn Block> = block_state_below.into(); + block_below.behavior().friction + } else { + unreachable!("Block below should be a real block.") + }; + + let inertia = if self.on_ground { + block_friction * 0.91 + } else { + 0.91 + }; + let mut movement = + handle_relative_friction_and_calculate_movement(self, acceleration, block_friction); + + movement.y -= gravity; + + // if (this.shouldDiscardFriction()) { + // this.setDeltaMovement(movement.x, yMovement, movement.z); + // } else { + // this.setDeltaMovement(movement.x * (double)inertia, yMovement * 0.9800000190734863D, movement.z * (double)inertia); + // } + + // if should_discard_friction(self) { + if false { + self.delta = movement; + } else { + self.delta = Vec3 { + x: movement.x * inertia as f64, + y: movement.y * 0.98f64, + z: movement.z * inertia as f64, + }; + } + } + + /// applies air resistance, calls self.travel(), and some other random + /// stuff. + fn ai_step(&mut self) { + // vanilla does movement interpolation here, doesn't really matter much for a bot though + + self.xxa *= 0.98; + self.zza *= 0.98; + + self.travel(&Vec3 { + x: self.xxa as f64, + y: self.yya as f64, + z: self.zza as f64, + }); + // freezing + // pushEntities + // drowning damage + } +} + +fn get_block_pos_below_that_affects_movement(entity: &EntityData) -> BlockPos { + BlockPos::new( + entity.pos().x as i32, + // TODO: this uses bounding_box.min_y instead of position.y + (entity.pos().y - 0.5f64) as i32, + entity.pos().z as i32, + ) +} + +fn handle_relative_friction_and_calculate_movement( + entity: &mut EntityMut, + acceleration: &Vec3, + block_friction: f32, +) -> Vec3 { + entity.move_relative(get_speed(&*entity, block_friction), acceleration); + // entity.delta = entity.handle_on_climbable(entity.delta); + entity + .move_colliding(&MoverType::Own, &entity.delta.clone()) + .expect("Entity should exist."); + // let delta_movement = entity.delta; + // if ((entity.horizontalCollision || entity.jumping) && (entity.onClimbable() || entity.getFeetBlockState().is(Blocks.POWDER_SNOW) && PowderSnowBlock.canEntityWalkOnPowderSnow(entity))) { + // var3 = new Vec3(var3.x, 0.2D, var3.z); + // } + // TODO: powdered snow + + entity.delta +} + +// private float getFrictionInfluencedSpeed(float friction) { +// return this.onGround ? this.getSpeed() * (0.21600002F / (friction * friction * friction)) : this.flyingSpeed; +// } +fn get_speed(entity: &EntityData, friction: f32) -> f32 { + // TODO: have speed & flying_speed fields in entity + if entity.on_ground { + let speed: f32 = 0.7; + speed * (0.216f32 / (friction * friction * friction)) + } else { + // entity.flying_speed + 0.02 + } +} |
