diff --git a/src/main/java/net/minestom/server/collision/BlockCollision.java b/src/main/java/net/minestom/server/collision/BlockCollision.java index 537917761..5f44fbdbd 100644 --- a/src/main/java/net/minestom/server/collision/BlockCollision.java +++ b/src/main/java/net/minestom/server/collision/BlockCollision.java @@ -144,24 +144,55 @@ final class BlockCollision { double remainingZ = deltaPosition.z(); // If the movement is small we don't need to run the expensive ray casting. + // Positions of move less than one can have hardcoded blocks to check for every direction if (deltaPosition.length() < 1) { - // Go through all points to check. See if the point after the move will be in a new block - // If the point after is in a new block that new block needs to be checked, otherwise only check the current block for (Vec point : allFaces) { - final Vec pointBefore = point.add(entityPosition); - final Vec pointAfter = pointBefore.add(deltaPosition); - if (!pointAfter.sameBlock(pointBefore)) { - if (pointBefore.blockX() != pointAfter.blockX()) { - checkBoundingBox(pointAfter.blockX(), pointBefore.blockY(), pointBefore.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult); - } - if (pointBefore.blockY() != pointAfter.blockY()) { - checkBoundingBox(pointBefore.blockX(), pointAfter.blockY(), pointBefore.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult); - } - if (pointBefore.blockZ() != pointAfter.blockZ()) { - checkBoundingBox(pointBefore.blockX(), pointBefore.blockY(), pointAfter.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult); - } - } + Vec pointBefore = point.add(entityPosition); + Vec pointAfter = point.add(entityPosition).add(deltaPosition); + + // Entity can pass through up to 4 blocks. Starting block, Two intermediate blocks, and a final block. + // This means we must check every combination of block movements when an entity moves over an axis. + // 000, 001, 010, 011, etc. + // There are 8 of these combinations + // Checks can be limited by checking if we moved across an axis line + + // Pass through (0, 0, 0) checkBoundingBox(pointBefore.blockX(), pointBefore.blockY(), pointBefore.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult); + + if (pointBefore.blockX() != pointAfter.blockX()) { + // Pass through (+1, 0, 0) + checkBoundingBox(pointAfter.blockX(), pointBefore.blockY(), pointBefore.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult); + + // Checks for moving through 4 blocks + if (pointBefore.blockY() != pointAfter.blockY()) + // Pass through (+1, +1, 0) + checkBoundingBox(pointAfter.blockX(), pointAfter.blockY(), pointBefore.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult); + + if (pointBefore.blockZ() != pointAfter.blockZ()) + // Pass through (+1, 0, +1) + checkBoundingBox(pointAfter.blockX(), pointBefore.blockY(), pointAfter.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult); + } + + if (pointBefore.blockY() != pointAfter.blockY()) { + // Pass through (0, +1, 0) + checkBoundingBox(pointBefore.blockX(), pointAfter.blockY(), pointBefore.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult); + + // Checks for moving through 4 blocks + if (pointBefore.blockZ() != pointAfter.blockZ()) + // Pass through (0, +1, +1) + checkBoundingBox(pointBefore.blockX(), pointAfter.blockY(), pointAfter.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult); + } + + if (pointBefore.blockZ() != pointAfter.blockZ()) { + // Pass through (0, 0, +1) + checkBoundingBox(pointBefore.blockX(), pointBefore.blockY(), pointAfter.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult); + } + + // Pass through (+1, +1, +1) + if (pointBefore.blockX() != pointAfter.blockX() + && pointBefore.blockY() != pointAfter.blockY() + && pointBefore.blockZ() != pointAfter.blockZ()) + checkBoundingBox(pointAfter.blockX(), pointAfter.blockY(), pointAfter.blockZ(), deltaPosition, entityPosition, boundingBox, instance, originChunk, finalResult); } } else { // When large moves are done we need to ray-cast to find all blocks that could intersect with the movement diff --git a/src/test/java/net/minestom/server/collision/EntityBlockPhysicsIntegrationTest.java b/src/test/java/net/minestom/server/collision/EntityBlockPhysicsIntegrationTest.java index 31d78f3a7..d840e4cb4 100644 --- a/src/test/java/net/minestom/server/collision/EntityBlockPhysicsIntegrationTest.java +++ b/src/test/java/net/minestom/server/collision/EntityBlockPhysicsIntegrationTest.java @@ -210,6 +210,25 @@ public class EntityBlockPhysicsIntegrationTest { assertEqualsPoint(new Pos(0.7, 42, 0.7), res.newPosition()); } + @Test + public void entityPhysicsCheckEdgeClipSmall(Env env) { + var instance = env.createFlatInstance(); + instance.setBlock(1, 42, 1, Block.STONE); + + var entity = new Entity(EntityType.ZOMBIE); + entity.setInstance(instance, new Pos(0.6999, 42, 0.6999)).join(); + + PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0.702, 0, 0.702)); + + boolean isFirst = checkPoints(new Pos(1.402, 42, 0.7), res.newPosition()); + boolean isSecond = checkPoints(new Pos(0.7, 42, 1.402), res.newPosition()); + + // First and second are both valid, it depends on the implementation + // If x collision is checked first then isFirst will be true + // If z collision is checked first then isSecond will be true + assertTrue(isFirst || isSecond); + } + @Test public void entityPhysicsCheckDoorSubBlockNorth(Env env) { var instance = env.createFlatInstance(); @@ -430,6 +449,178 @@ public class EntityBlockPhysicsIntegrationTest { assertEqualsPoint(new Pos(0.7, 42, 0), res.newPosition()); } + // Checks C include all checks for crossing one intermediate block (3 block checks) + @Test + public void entityPhysicsSmallMoveC0(Env env) { + var instance = env.createFlatInstance(); + instance.setBlock(1, 42, 0, Block.STONE); + + BoundingBox bb = new BoundingBox(0, 0, 0); + + var entity = new Entity(EntityType.ZOMBIE); + entity.setBoundingBox(bb); + + entity.setInstance(instance, new Pos(0.7, 42, 0.5)).join(); + assertEquals(instance, entity.getInstance()); + + PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0.6, 0, 0.6)); + assertEqualsPoint(new Pos(1, 42, 1.1), res.newPosition()); + } + + @Test + public void entityPhysicsSmallMoveC1(Env env) { + var instance = env.createFlatInstance(); + instance.setBlock(0, 42, 1, Block.STONE); + + BoundingBox bb = new BoundingBox(0, 0, 0); + + var entity = new Entity(EntityType.ZOMBIE); + entity.setBoundingBox(bb); + + entity.setInstance(instance, new Pos(0.5, 42, 0.7)).join(); + assertEquals(instance, entity.getInstance()); + + PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0.6, 0, 0.6)); + assertEqualsPoint(new Pos(1.1, 42, 1), res.newPosition()); + } + + @Test + public void entityPhysicsSmallMoveC2(Env env) { + var instance = env.createFlatInstance(); + instance.setBlock(1, 42, 1, Block.STONE); + + BoundingBox bb = new BoundingBox(0, 0, 0); + + var entity = new Entity(EntityType.ZOMBIE); + entity.setBoundingBox(bb); + + entity.setInstance(instance, new Pos(0.8, 42, 1.3)).join(); + assertEquals(instance, entity.getInstance()); + + PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0.6, 0, -0.6)); + assertEqualsPoint(new Pos(1, 42, 0.7), res.newPosition()); + } + + @Test + public void entityPhysicsSmallMoveC3(Env env) { + var instance = env.createFlatInstance(); + instance.setBlock(0, 42, 0, Block.STONE); + + BoundingBox bb = new BoundingBox(0, 0, 0); + + var entity = new Entity(EntityType.ZOMBIE); + entity.setBoundingBox(bb); + + entity.setInstance(instance, new Pos(0.7, 42, 1.1)).join(); + assertEquals(instance, entity.getInstance()); + + PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0.6, 0, -0.6)); + assertEqualsPoint(new Pos(1.3, 42, 1), res.newPosition()); + } + + @Test + public void entityPhysicsSmallMoveC4(Env env) { + var instance = env.createFlatInstance(); + instance.setBlock(0, 42, 1, Block.STONE); + + BoundingBox bb = new BoundingBox(0, 0, 0); + + var entity = new Entity(EntityType.ZOMBIE); + entity.setBoundingBox(bb); + + entity.setInstance(instance, new Pos(1.1, 42, 1.3)).join(); + assertEquals(instance, entity.getInstance()); + + PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(-0.6, 0, -0.6)); + assertEqualsPoint(new Pos(1, 42, 0.7), res.newPosition()); + } + + @Test + public void entityPhysicsSmallMoveC5(Env env) { + var instance = env.createFlatInstance(); + instance.setBlock(1, 42, 0, Block.STONE); + + BoundingBox bb = new BoundingBox(0, 0, 0); + + var entity = new Entity(EntityType.ZOMBIE); + entity.setBoundingBox(bb); + + entity.setInstance(instance, new Pos(1.3, 42, 1.1)).join(); + assertEquals(instance, entity.getInstance()); + + PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(-0.6, 0, -0.6)); + assertEqualsPoint(new Pos(0.7, 42, 1), res.newPosition()); + } + + @Test + public void entityPhysicsSmallMoveC6(Env env) { + var instance = env.createFlatInstance(); + instance.setBlock(0, 42, 0, Block.STONE); + + BoundingBox bb = new BoundingBox(0, 0, 0); + + var entity = new Entity(EntityType.ZOMBIE); + entity.setBoundingBox(bb); + + entity.setInstance(instance, new Pos(1.1, 42, 0.7)).join(); + assertEquals(instance, entity.getInstance()); + + PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(-0.6, 0, 0.6)); + assertEqualsPoint(new Pos(1, 42, 1.3), res.newPosition()); + } + + @Test + public void entityPhysicsSmallMoveC7(Env env) { + var instance = env.createFlatInstance(); + instance.setBlock(1, 42, 1, Block.STONE); + + BoundingBox bb = new BoundingBox(0, 0, 0); + + var entity = new Entity(EntityType.ZOMBIE); + entity.setBoundingBox(bb); + + entity.setInstance(instance, new Pos(1.3, 42, 0.8)).join(); + assertEquals(instance, entity.getInstance()); + + PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(-0.6, 0, 0.6)); + assertEqualsPoint(new Pos(0.7, 42, 1), res.newPosition()); + } + + // Checks CE include checks for crossing two intermediate block (4 block checks) + @Test + public void entityPhysicsSmallMoveC0E(Env env) { + var instance = env.createFlatInstance(); + instance.setBlock(1, 43, 0, Block.STONE); + + BoundingBox bb = new BoundingBox(0, 0, 0); + + var entity = new Entity(EntityType.ZOMBIE); + entity.setBoundingBox(bb); + + entity.setInstance(instance, new Pos(0.51, 42.51, 0.5)).join(); + assertEquals(instance, entity.getInstance()); + + PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0.57, 0.57, 0.57)); + assertEqualsPoint(new Pos(1.08, 43, 1.07), res.newPosition()); + } + + @Test + public void entityPhysicsSmallMoveC1E(Env env) { + var instance = env.createFlatInstance(); + instance.setBlock(0, 43, 1, Block.STONE); + + BoundingBox bb = new BoundingBox(0, 0, 0); + + var entity = new Entity(EntityType.ZOMBIE); + entity.setBoundingBox(bb); + + entity.setInstance(instance, new Pos(0.50, 42.51, 0.51)).join(); + assertEquals(instance, entity.getInstance()); + + PhysicsResult res = CollisionUtils.handlePhysics(entity, new Vec(0.57, 0.57, 0.57)); + assertEqualsPoint(new Pos(1.07, 43, 1.08), res.newPosition()); + } + @Test public void entityPhysicsCheckNoCollision(Env env) { var instance = env.createFlatInstance();