From b3ee3e234527a8a44c25c36456cfdc8d2550e41e Mon Sep 17 00:00:00 2001 From: Konstantin Shandurenko Date: Mon, 28 Mar 2022 23:06:25 +0300 Subject: [PATCH] EntityProjectile fixes and optimizations (#807) --- .../net/minestom/server/entity/Entity.java | 32 ++-- .../server/entity/EntityProjectile.java | 117 +++++++-------- .../projectile/ProjectileCollideEvent.java | 39 +++++ .../ProjectileCollideWithBlockEvent.java | 24 +++ .../ProjectileCollideWithEntityEvent.java | 23 +++ .../projectile/ProjectileUncollideEvent.java | 20 +++ ...ityProjectileCollisionIntegrationTest.java | 139 ++++++++++++++++++ 7 files changed, 319 insertions(+), 75 deletions(-) create mode 100644 src/main/java/net/minestom/server/event/entity/projectile/ProjectileCollideEvent.java create mode 100644 src/main/java/net/minestom/server/event/entity/projectile/ProjectileCollideWithBlockEvent.java create mode 100644 src/main/java/net/minestom/server/event/entity/projectile/ProjectileCollideWithEntityEvent.java create mode 100644 src/main/java/net/minestom/server/event/entity/projectile/ProjectileUncollideEvent.java create mode 100644 src/test/java/net/minestom/server/collision/EntityProjectileCollisionIntegrationTest.java diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index 1231a2985..302e8c074 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -578,13 +578,17 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev } // World border collision - final var finalVelocityPosition = CollisionUtils.applyWorldBorder(instance, position, newPosition); - if (finalVelocityPosition.samePoint(position)) { - this.velocity = Vec.ZERO; - if (hasVelocity) { - sendPacketToViewers(getVelocityPacket()); + final Pos finalVelocityPosition = CollisionUtils.applyWorldBorder(instance, position, newPosition); + final boolean positionChanged = !finalVelocityPosition.samePoint(position); + if (!positionChanged) { + if (!hasVelocity && newVelocity.isZero()) { + return; + } + if (hasVelocity) { + this.velocity = Vec.ZERO; + sendPacketToViewers(getVelocityPacket()); + return; } - return; } final Chunk finalChunk = ChunkUtils.retrieve(instance, currentChunk, finalVelocityPosition); if (!ChunkUtils.isLoaded(finalChunk)) { @@ -592,13 +596,15 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev return; } - if (entityType == EntityTypes.ITEM || entityType == EntityType.FALLING_BLOCK) { - // TODO find other exceptions - this.previousPosition = this.position; - this.position = finalVelocityPosition; - refreshCoordinate(finalVelocityPosition); - } else { - refreshPosition(finalVelocityPosition, true); + if (positionChanged) { + if (entityType == EntityTypes.ITEM || entityType == EntityType.FALLING_BLOCK) { + // TODO find other exceptions + this.previousPosition = this.position; + this.position = finalVelocityPosition; + refreshCoordinate(finalVelocityPosition); + } else { + refreshPosition(finalVelocityPosition, true); + } } // Update velocity diff --git a/src/main/java/net/minestom/server/entity/EntityProjectile.java b/src/main/java/net/minestom/server/entity/EntityProjectile.java index 4702d3674..7cc2266e4 100644 --- a/src/main/java/net/minestom/server/entity/EntityProjectile.java +++ b/src/main/java/net/minestom/server/entity/EntityProjectile.java @@ -1,15 +1,18 @@ package net.minestom.server.entity; -import net.minestom.server.entity.metadata.ProjectileMeta; -import net.minestom.server.event.EventDispatcher; -import net.minestom.server.event.entity.EntityAttackEvent; -import net.minestom.server.event.entity.EntityShootEvent; -import net.minestom.server.instance.Chunk; -import net.minestom.server.instance.Instance; -import net.minestom.server.instance.block.Block; +import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.metadata.ProjectileMeta; +import net.minestom.server.event.EventDispatcher; +import net.minestom.server.event.entity.EntityShootEvent; +import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent; +import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent; +import net.minestom.server.event.entity.projectile.ProjectileUncollideEvent; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.Block; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -18,6 +21,7 @@ import java.util.Optional; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Class that allows to instantiate entities with projectile-like physics handling. @@ -44,30 +48,14 @@ public class EntityProjectile extends Entity { return this.shooter; } - /** - * Called when this projectile is stuck in blocks. - * Probably you want to do nothing with arrows in such case and to remove other types of projectiles. - */ - public void onStuck() { - - } - - /** - * Called when this projectile unstucks. - * Probably you want to add some random velocity to arrows in such case. - */ - public void onUnstuck() { - - } - public void shoot(Point to, double power, double spread) { - EntityShootEvent shootEvent = new EntityShootEvent(this.shooter, this, to, power, spread); + final EntityShootEvent shootEvent = new EntityShootEvent(this.shooter, this, to, power, spread); EventDispatcher.call(shootEvent); if (shootEvent.isCancelled()) { remove(); return; } - final var from = this.shooter.getPosition().add(0D, this.shooter.getEyeHeight(), 0D); + final Pos from = this.shooter.getPosition().add(0D, this.shooter.getEyeHeight(), 0D); shoot(from, to, shootEvent.getPower(), shootEvent.getSpread()); } @@ -75,10 +63,10 @@ public class EntityProjectile extends Entity { double dx = to.x() - from.x(); double dy = to.y() - from.y(); double dz = to.z() - from.z(); - double xzLength = Math.sqrt(dx * dx + dz * dz); + final double xzLength = Math.sqrt(dx * dx + dz * dz); dy += xzLength * 0.20000000298023224D; - double length = Math.sqrt(dx * dx + dy * dy + dz * dz); + final double length = Math.sqrt(dx * dx + dy * dy + dz * dz); dx /= length; dy /= length; dz /= length; @@ -98,9 +86,9 @@ public class EntityProjectile extends Entity { @Override public void tick(long time) { - final var posBefore = getPosition(); + final Pos posBefore = getPosition(); super.tick(time); - final var posNow = getPosition(); + final Pos posNow = getPosition(); if (isStuck(posBefore, posNow)) { if (super.onGround) { return; @@ -109,14 +97,13 @@ public class EntityProjectile extends Entity { this.velocity = Vec.ZERO; sendPacketToViewersAndSelf(getVelocityPacket()); setNoGravity(true); - onStuck(); } else { if (!super.onGround) { return; } super.onGround = false; setNoGravity(false); - onUnstuck(); + EventDispatcher.call(new ProjectileUncollideEvent(this)); } } @@ -129,60 +116,66 @@ public class EntityProjectile extends Entity { */ @SuppressWarnings("ConstantConditions") private boolean isStuck(Pos pos, Pos posNow) { + final Instance instance = getInstance(); if (pos.samePoint(posNow)) { - return true; + return instance.getBlock(pos).isSolid(); } - Instance instance = getInstance(); Chunk chunk = null; - Collection entities = null; + Collection entities = null; + final BoundingBox bb = getBoundingBox(); /* - What we're about to do is to discretely jump from the previous position to the new one. + What we're about to do is to discretely jump from a previous position to the new one. For each point we will be checking blocks and entities we're in. */ - double part = .25D; // half of the bounding box - final var dir = posNow.sub(pos).asVec(); - int parts = (int) Math.ceil(dir.length() / part); - final var direction = dir.normalize().mul(part).asPosition(); + final double part = bb.width() / 2; + final Vec dir = posNow.sub(pos).asVec(); + final int parts = (int) Math.ceil(dir.length() / part); + final Pos direction = dir.normalize().mul(part).asPosition(); + final long aliveTicks = getAliveTicks(); + Block block = null; + Point blockPos = null; for (int i = 0; i < parts; ++i) { - // If we're at last part, we can't just add another direction-vector, because we can exceed end point. - if (i == parts - 1) { - pos = posNow; - } else { - pos = pos.add(direction); + // If we're at last part, we can't just add another direction-vector, because we can exceed the end point. + pos = (i == parts - 1) ? posNow : pos.add(direction); + if (block == null || !pos.sameBlock(blockPos)) { + block = instance.getBlock(pos); + blockPos = pos; } - Block block = instance.getBlock(pos); - if (!block.isAir() && !block.isLiquid()) { - teleport(pos); - return true; + if (block.isSolid()) { + final ProjectileCollideWithBlockEvent event = new ProjectileCollideWithBlockEvent(this, pos, block); + EventDispatcher.call(event); + if (!event.isCancelled()) { + teleport(pos); + return true; + } } - Chunk currentChunk = instance.getChunkAt(pos); if (currentChunk != chunk) { chunk = currentChunk; entities = instance.getChunkEntities(chunk) .stream() .filter(entity -> entity instanceof LivingEntity) + .map(entity -> (LivingEntity) entity) .collect(Collectors.toSet()); } + Stream victimsStream = entities.stream() + .filter(entity -> bb.intersectEntity(getPosition(), entity)); /* - We won't check collisions with entities for first ticks of arrow's life, because it spawns in the - shooter and will immediately damage him. + We won't check collisions with a shooter for first ticks of arrow's life, because it spawns in him + and will immediately deal damage. */ - if (getAliveTicks() < 3) { - continue; + if (aliveTicks < 3 && shooter != null) { + victimsStream = victimsStream.filter(entity -> entity != shooter); } - Optional victimOptional = entities.stream() - .filter(entity -> getBoundingBox().intersectEntity(getPosition(), entity)) - .findAny(); + final Optional victimOptional = victimsStream.findAny(); if (victimOptional.isPresent()) { - LivingEntity victim = (LivingEntity) victimOptional.get(); - if(entityType == EntityTypes.ARROW || entityType == EntityTypes.SPECTRAL_ARROW) { - victim.setArrowCount(victim.getArrowCount() + 1); + final LivingEntity target = victimOptional.get(); + final ProjectileCollideWithEntityEvent event = new ProjectileCollideWithEntityEvent(this, pos, target); + EventDispatcher.call(event); + if (!event.isCancelled()) { + return super.onGround; } - EventDispatcher.call(new EntityAttackEvent(this, victim)); - remove(); - return super.onGround; } } return false; diff --git a/src/main/java/net/minestom/server/event/entity/projectile/ProjectileCollideEvent.java b/src/main/java/net/minestom/server/event/entity/projectile/ProjectileCollideEvent.java new file mode 100644 index 000000000..782debff0 --- /dev/null +++ b/src/main/java/net/minestom/server/event/entity/projectile/ProjectileCollideEvent.java @@ -0,0 +1,39 @@ +package net.minestom.server.event.entity.projectile; + +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Entity; +import net.minestom.server.event.trait.CancellableEvent; +import net.minestom.server.event.trait.EntityInstanceEvent; +import net.minestom.server.event.trait.RecursiveEvent; +import org.jetbrains.annotations.NotNull; + +class ProjectileCollideEvent implements EntityInstanceEvent, CancellableEvent, RecursiveEvent { + + private final @NotNull Entity projectile; + private final @NotNull Pos position; + private boolean cancelled; + + protected ProjectileCollideEvent(@NotNull Entity projectile, @NotNull Pos position) { + this.projectile = projectile; + this.position = position; + } + + @Override + public @NotNull Entity getEntity() { + return projectile; + } + + public @NotNull Pos getCollisionPosition() { + return position; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + cancelled = cancel; + } +} diff --git a/src/main/java/net/minestom/server/event/entity/projectile/ProjectileCollideWithBlockEvent.java b/src/main/java/net/minestom/server/event/entity/projectile/ProjectileCollideWithBlockEvent.java new file mode 100644 index 000000000..e5b213b7a --- /dev/null +++ b/src/main/java/net/minestom/server/event/entity/projectile/ProjectileCollideWithBlockEvent.java @@ -0,0 +1,24 @@ +package net.minestom.server.event.entity.projectile; + +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Entity; +import net.minestom.server.instance.block.Block; +import org.jetbrains.annotations.NotNull; + +public final class ProjectileCollideWithBlockEvent extends ProjectileCollideEvent { + + private final @NotNull Block block; + + public ProjectileCollideWithBlockEvent( + @NotNull Entity projectile, + @NotNull Pos position, + @NotNull Block block + ) { + super(projectile, position); + this.block = block; + } + + public @NotNull Block getBlock() { + return block; + } +} diff --git a/src/main/java/net/minestom/server/event/entity/projectile/ProjectileCollideWithEntityEvent.java b/src/main/java/net/minestom/server/event/entity/projectile/ProjectileCollideWithEntityEvent.java new file mode 100644 index 000000000..faf794966 --- /dev/null +++ b/src/main/java/net/minestom/server/event/entity/projectile/ProjectileCollideWithEntityEvent.java @@ -0,0 +1,23 @@ +package net.minestom.server.event.entity.projectile; + +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Entity; +import org.jetbrains.annotations.NotNull; + +public final class ProjectileCollideWithEntityEvent extends ProjectileCollideEvent { + + private final @NotNull Entity target; + + public ProjectileCollideWithEntityEvent( + @NotNull Entity projectile, + @NotNull Pos position, + @NotNull Entity target + ) { + super(projectile, position); + this.target = target; + } + + public @NotNull Entity getTarget() { + return target; + } +} diff --git a/src/main/java/net/minestom/server/event/entity/projectile/ProjectileUncollideEvent.java b/src/main/java/net/minestom/server/event/entity/projectile/ProjectileUncollideEvent.java new file mode 100644 index 000000000..8dfddb5e0 --- /dev/null +++ b/src/main/java/net/minestom/server/event/entity/projectile/ProjectileUncollideEvent.java @@ -0,0 +1,20 @@ +package net.minestom.server.event.entity.projectile; + +import net.minestom.server.entity.Entity; +import net.minestom.server.event.trait.EntityInstanceEvent; +import org.jetbrains.annotations.NotNull; + +public final class ProjectileUncollideEvent implements EntityInstanceEvent { + + private final @NotNull Entity projectile; + + public ProjectileUncollideEvent(@NotNull Entity projectile) { + this.projectile = projectile; + } + + @Override + public @NotNull Entity getEntity() { + return projectile; + } + +} diff --git a/src/test/java/net/minestom/server/collision/EntityProjectileCollisionIntegrationTest.java b/src/test/java/net/minestom/server/collision/EntityProjectileCollisionIntegrationTest.java new file mode 100644 index 000000000..1ad786b94 --- /dev/null +++ b/src/test/java/net/minestom/server/collision/EntityProjectileCollisionIntegrationTest.java @@ -0,0 +1,139 @@ +package net.minestom.server.collision; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.api.Env; +import net.minestom.server.api.EnvTest; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityProjectile; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.LivingEntity; +import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent; +import net.minestom.server.event.entity.projectile.ProjectileCollideWithEntityEvent; +import net.minestom.server.event.entity.projectile.ProjectileUncollideEvent; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.Block; +import net.minestom.server.utils.time.TimeUnit; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; + +@EnvTest +public class EntityProjectileCollisionIntegrationTest { + + @Test + public void blockShootAndBlockRemoval(Env env) { + final Instance instance = env.createFlatInstance(); + instance.getWorldBorder().setDiameter(1000.0); + + final Entity shooter = new Entity(EntityType.SKELETON); + shooter.setInstance(instance, new Pos(0, 40, 0)).join(); + + final EntityProjectile projectile = new EntityProjectile(shooter, EntityType.ARROW); + projectile.setInstance(instance, shooter.getPosition().withY(y -> y + shooter.getEyeHeight())).join(); + + final Point blockPosition = new Vec(5, 40, 0); + final Block block = Block.GRASS_BLOCK; + instance.setBlock(blockPosition, block); + projectile.shoot(blockPosition, 1, 0); + + final var eventRef = new AtomicReference(); + MinecraftServer.getGlobalEventHandler().addListener(ProjectileCollideWithBlockEvent.class, eventRef::set); + + final long tick = TimeUnit.SERVER_TICK.getDuration().toMillis(); + for (int i = 0; i < MinecraftServer.TICK_PER_SECOND; ++i) { + projectile.tick(i * tick); + } + + var event = eventRef.get(); + assertNotNull(event); + assertEquals(blockPosition, new Vec(event.getCollisionPosition().blockX(), event.getCollisionPosition().blockY(), event.getCollisionPosition().blockZ())); + assertEquals(block, event.getBlock()); + + final var eventRef2 = new AtomicReference(); + MinecraftServer.getGlobalEventHandler().addListener(ProjectileUncollideEvent.class, eventRef2::set); + eventRef.set(null); + instance.setBlock(blockPosition, Block.AIR); + + for (int i = 0; i < MinecraftServer.TICK_PER_SECOND; ++i) { + projectile.tick((MinecraftServer.TICK_PER_SECOND + i) * tick); + } + event = eventRef.get(); + final var event2 = eventRef2.get(); + assertNotNull(event); + assertNotNull(event2); + assertEquals(blockPosition.withY(y -> y - 1), new Vec(event.getCollisionPosition().blockX(), event.getCollisionPosition().blockY(), event.getCollisionPosition().blockZ())); + } + + @Test + public void entityShoot(Env env) { + final Instance instance = env.createFlatInstance(); + instance.getWorldBorder().setDiameter(1000.0); + + final Entity shooter = new Entity(EntityType.SKELETON); + shooter.setInstance(instance, new Pos(0, 40, 0)).join(); + + final EntityProjectile projectile = new EntityProjectile(shooter, EntityType.ARROW); + projectile.setInstance(instance, shooter.getPosition().withY(y -> y + shooter.getEyeHeight())).join(); + + final Point targetPosition = new Vec(5, 40, 0); + final LivingEntity target = new LivingEntity(EntityType.ZOMBIE); + target.setInstance(instance, Pos.fromPoint(targetPosition)).join(); + projectile.shoot(targetPosition, 1, 0); + + final var eventRef = new AtomicReference(); + MinecraftServer.getGlobalEventHandler().addListener(ProjectileCollideWithEntityEvent.class, event -> { + event.getEntity().remove(); + eventRef.set(event); + }); + + final long tick = TimeUnit.SERVER_TICK.getDuration().toMillis(); + for (int i = 0; i < MinecraftServer.TICK_PER_SECOND; ++i) { + if (!projectile.isRemoved()) { + projectile.tick(i * tick); + } + } + + final var event = eventRef.get(); + assertNotNull(event); + assertSame(target, event.getTarget()); + assertTrue(target.getBoundingBox().intersectEntity(target.getPosition(), projectile)); + } + + @Test + public void entitySelfShoot(Env env) { + final Instance instance = env.createFlatInstance(); + instance.getWorldBorder().setDiameter(1000.0); + + final LivingEntity shooter = new LivingEntity(EntityType.SKELETON); + shooter.setInstance(instance, new Pos(0, 40, 0)).join(); + + final EntityProjectile projectile = new EntityProjectile(shooter, EntityType.ARROW); + projectile.setInstance(instance, shooter.getPosition().withY(y -> y + shooter.getEyeHeight())).join(); + + projectile.shoot(new Vec(0, 60, 0), 1, 0); + + final var eventRef = new AtomicReference(); + MinecraftServer.getGlobalEventHandler().addListener(ProjectileCollideWithEntityEvent.class, event -> { + event.getEntity().remove(); + eventRef.set(event); + }); + + final long tick = TimeUnit.SERVER_TICK.getDuration().toMillis(); + for (int i = 0; i < MinecraftServer.TICK_PER_SECOND * 5; ++i) { + if (!projectile.isRemoved()) { + projectile.tick(i * tick); + } + } + + final var event = eventRef.get(); + assertNotNull(event); + assertSame(shooter, event.getTarget()); + assertTrue(shooter.getBoundingBox().intersectEntity(shooter.getPosition(), projectile)); + } + +}