From f034296f2850673240b8d1102fe603152f2aefae Mon Sep 17 00:00:00 2001 From: DeidaraMC <117625071+DeidaraMC@users.noreply.github.com> Date: Wed, 27 Mar 2024 15:21:07 -0400 Subject: [PATCH] feat: add aerodynamics record and the capability to set custom horizontal air resistance (#2053) * feat: add aerodynamics record and the ability to set horizontal drag * feat: entity physics simulation overhaul * fix: made physics utils private, renamed to match other utils * chore: separate concept of chunks and tps from PhysicsUtils, remove bad PhysicsResult constants * chore: remove synchronization from PhysicsUtils, SYNCHRONIZE_ONLY_ENTITIES collection > set * chore: remove extra vec allocations * chore: improved flyingVelocity test * chore: add all entities with client side prediction to SYNCRHONIZE_ONLY_ENTITIES, refactor velocity --------- Co-authored-by: iam --- .../server/collision/Aerodynamics.java | 46 ++++ .../server/collision/BlockCollision.java | 3 +- .../server/collision/CollisionUtils.java | 37 +++- .../server/collision/PhysicsUtils.java | 65 ++++++ .../net/minestom/server/entity/Entity.java | 200 +++++------------- .../server/entity/EntityProjectile.java | 9 +- .../entity/EntityVelocityIntegrationTest.java | 7 +- 7 files changed, 206 insertions(+), 161 deletions(-) create mode 100644 src/main/java/net/minestom/server/collision/Aerodynamics.java create mode 100644 src/main/java/net/minestom/server/collision/PhysicsUtils.java diff --git a/src/main/java/net/minestom/server/collision/Aerodynamics.java b/src/main/java/net/minestom/server/collision/Aerodynamics.java new file mode 100644 index 000000000..954b8cdbc --- /dev/null +++ b/src/main/java/net/minestom/server/collision/Aerodynamics.java @@ -0,0 +1,46 @@ +package net.minestom.server.collision; + +import it.unimi.dsi.fastutil.doubles.DoubleUnaryOperator; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +/** + * Represents the aerodynamic properties of an entity + * + * @param gravity the entity's downward acceleration per tick + * @param horizontalAirResistance the horizontal drag coefficient; the entity's current horizontal + * velocity is multiplied by this every tick + * @param verticalAirResistance the vertical drag coefficient; the entity's current vertical + * * velocity is multiplied by this every tick + */ +public record Aerodynamics(double gravity, double horizontalAirResistance, double verticalAirResistance) { + @Contract(pure = true) + public @NotNull Aerodynamics withGravity(double gravity) { + return new Aerodynamics(gravity, horizontalAirResistance, verticalAirResistance); + } + + @Contract(pure = true) + public @NotNull Aerodynamics withHorizontalAirResistance(double horizontalAirResistance) { + return new Aerodynamics(gravity, horizontalAirResistance, verticalAirResistance); + } + + @Contract(pure = true) + public @NotNull Aerodynamics withHorizontalAirResistance(@NotNull DoubleUnaryOperator operator) { + return withHorizontalAirResistance(operator.apply(horizontalAirResistance)); + } + + @Contract(pure = true) + public @NotNull Aerodynamics withVerticalAirResistance(double verticalAirResistance) { + return new Aerodynamics(gravity, horizontalAirResistance, verticalAirResistance); + } + + @Contract(pure = true) + public @NotNull Aerodynamics withVerticalAirResistance(@NotNull DoubleUnaryOperator operator) { + return withVerticalAirResistance(operator.apply(verticalAirResistance)); + } + + @Contract(pure = true) + public @NotNull Aerodynamics withAirResistance(double horizontal, double vertical) { + return new Aerodynamics(gravity, horizontalAirResistance, verticalAirResistance); + } +} diff --git a/src/main/java/net/minestom/server/collision/BlockCollision.java b/src/main/java/net/minestom/server/collision/BlockCollision.java index 3eae94f3d..3d3595421 100644 --- a/src/main/java/net/minestom/server/collision/BlockCollision.java +++ b/src/main/java/net/minestom/server/collision/BlockCollision.java @@ -28,7 +28,8 @@ final class BlockCollision { boolean singleCollision) { if (velocity.isZero()) { // TODO should return a constant - return new PhysicsResult(entityPosition, Vec.ZERO, false, false, false, false, velocity, new Point[3], new Shape[3], false, SweepResult.NO_COLLISION); + return new PhysicsResult(entityPosition, Vec.ZERO, false, false, false, false, + velocity, new Point[3], new Shape[3], false, SweepResult.NO_COLLISION); } // Fast-exit using cache final PhysicsResult cachedResult = cachedPhysics(velocity, entityPosition, getter, lastPhysicsResult); diff --git a/src/main/java/net/minestom/server/collision/CollisionUtils.java b/src/main/java/net/minestom/server/collision/CollisionUtils.java index e68e0b8e5..00533def4 100644 --- a/src/main/java/net/minestom/server/collision/CollisionUtils.java +++ b/src/main/java/net/minestom/server/collision/CollisionUtils.java @@ -102,9 +102,26 @@ public final class CollisionUtils { @NotNull Pos position, @NotNull Vec velocity, @Nullable PhysicsResult lastPhysicsResult, boolean singleCollision) { final Block.Getter getter = new ChunkCache(instance, chunk != null ? chunk : instance.getChunkAt(position), Block.STONE); + return handlePhysics(getter, boundingBox, position, velocity, lastPhysicsResult, singleCollision); + } + + /** + * Moves bounding box with physics applied (ie checking against blocks) + *

+ * Works by getting all the full blocks that a bounding box could interact with. + * All bounding boxes inside the full blocks are checked for collisions with the given bounding box. + * + * @param blockGetter the block getter to check collisions against, ensure block access is synchronized + * @return the result of physics simulation + */ + @ApiStatus.Internal + public static PhysicsResult handlePhysics(@NotNull Block.Getter blockGetter, + @NotNull BoundingBox boundingBox, + @NotNull Pos position, @NotNull Vec velocity, + @Nullable PhysicsResult lastPhysicsResult, boolean singleCollision) { return BlockCollision.handlePhysics(boundingBox, velocity, position, - getter, lastPhysicsResult, singleCollision); + blockGetter, lastPhysicsResult, singleCollision); } /** @@ -140,14 +157,13 @@ public final class CollisionUtils { /** * Applies world border collision. * - * @param instance the instance where the world border is + * @param worldBorder the world border * @param currentPosition the current position * @param newPosition the future target position * @return the position with the world border collision applied (can be {@code newPosition} if not changed) */ - public static @NotNull Pos applyWorldBorder(@NotNull Instance instance, + public static @NotNull Pos applyWorldBorder(@NotNull WorldBorder worldBorder, @NotNull Pos currentPosition, @NotNull Pos newPosition) { - final WorldBorder worldBorder = instance.getWorldBorder(); final WorldBorder.CollisionAxis collisionAxis = worldBorder.getCollisionAxis(newPosition); return switch (collisionAxis) { case NONE -> @@ -168,4 +184,17 @@ public final class CollisionUtils { public static Shape parseBlockShape(String collision, String occlusion, Registry.BlockEntry blockEntry) { return ShapeImpl.parseBlockFromRegistry(collision, occlusion, blockEntry); } + + /** + * Simulate the entity's collision physics as if the world had no blocks + * + * @param entityPosition the position of the entity + * @param entityVelocity the velocity of the entity + * @return the result of physics simulation + */ + public static PhysicsResult blocklessCollision(@NotNull Pos entityPosition, @NotNull Vec entityVelocity) { + return new PhysicsResult(entityPosition.add(entityVelocity), entityVelocity, false, + false, false, false, entityVelocity, new Point[3], + new Shape[3], false, SweepResult.NO_COLLISION); + } } diff --git a/src/main/java/net/minestom/server/collision/PhysicsUtils.java b/src/main/java/net/minestom/server/collision/PhysicsUtils.java new file mode 100644 index 000000000..c428cdccc --- /dev/null +++ b/src/main/java/net/minestom/server/collision/PhysicsUtils.java @@ -0,0 +1,65 @@ +package net.minestom.server.collision; + +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.WorldBorder; +import net.minestom.server.instance.block.Block; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class PhysicsUtils { + /** + * Simulate the entity's movement physics + *

+ * This is done by first attempting to move the entity forward with the + * current velocity passed in. Then adjusting the velocity by applying + * air resistance and friction. + * + * @param entityPosition the current entity position + * @param entityVelocityPerTick the current entity velocity in blocks/tick + * @param entityBoundingBox the current entity bounding box + * @param worldBorder the world border to test bounds against + * @param blockGetter the block getter to test block collisions against + * @param aerodynamics the current entity aerodynamics + * @param entityNoGravity whether the entity has gravity + * @param entityHasPhysics whether the entity has physics + * @param entityOnGround whether the entity is on the ground + * @param entityFlying whether the entity is flying + * @param previousPhysicsResult the physics result from the previous simulation or null + * @return a {@link PhysicsResult} containing the resulting physics state of this simulation + */ + public static @NotNull PhysicsResult simulateMovement(@NotNull Pos entityPosition, @NotNull Vec entityVelocityPerTick, @NotNull BoundingBox entityBoundingBox, + @NotNull WorldBorder worldBorder, @NotNull Block.Getter blockGetter, @NotNull Aerodynamics aerodynamics, boolean entityNoGravity, + boolean entityHasPhysics, boolean entityOnGround, boolean entityFlying, @Nullable PhysicsResult previousPhysicsResult) { + final PhysicsResult physicsResult = entityHasPhysics ? + CollisionUtils.handlePhysics(blockGetter, entityBoundingBox, entityPosition, entityVelocityPerTick, previousPhysicsResult, false) : + CollisionUtils.blocklessCollision(entityPosition, entityVelocityPerTick); + + Pos newPosition = physicsResult.newPosition(); + Vec newVelocity = physicsResult.newVelocity(); + + Pos positionWithinBorder = CollisionUtils.applyWorldBorder(worldBorder, entityPosition, newPosition); + newVelocity = updateVelocity(entityPosition, newVelocity, blockGetter, aerodynamics, !positionWithinBorder.samePoint(entityPosition), entityFlying, entityOnGround, entityNoGravity); + return new PhysicsResult(positionWithinBorder, newVelocity, physicsResult.isOnGround(), physicsResult.collisionX(), physicsResult.collisionY(), physicsResult.collisionZ(), + physicsResult.originalDelta(), physicsResult.collisionPoints(), physicsResult.collisionShapes(), physicsResult.hasCollision(), physicsResult.res()); + } + + private static @NotNull Vec updateVelocity(@NotNull Pos entityPosition, @NotNull Vec currentVelocity, @NotNull Block.Getter blockGetter, @NotNull Aerodynamics aerodynamics, + boolean positionChanged, boolean entityFlying, boolean entityOnGround, boolean entityNoGravity) { + if (!positionChanged) { + if (entityOnGround || entityFlying) return Vec.ZERO; + return new Vec(0, entityNoGravity ? 0 : -aerodynamics.gravity() * aerodynamics.verticalAirResistance(), 0); + } + + double drag = entityOnGround ? blockGetter.getBlock(entityPosition.sub(0, 0.5000001, 0)).registry().friction() * aerodynamics.horizontalAirResistance() : + aerodynamics.horizontalAirResistance(); + double gravity = entityFlying ? 0 : aerodynamics.gravity(); + double gravityDrag = entityFlying ? 0.6 : aerodynamics.verticalAirResistance(); + + double x = currentVelocity.x() * drag, z = currentVelocity.z() * drag; + double y = !entityNoGravity ? ((currentVelocity.y() - gravity) * gravityDrag) : currentVelocity.y(); + return new Vec(Math.abs(x) < Vec.EPSILON ? 0 : x, Math.abs(y) < Vec.EPSILON ? 0 : y, Math.abs(z) < Vec.EPSILON ? 0 : z); + } + + private PhysicsUtils() {} +} diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index bcc7c4abc..a315431bb 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -79,13 +79,17 @@ import java.util.function.UnaryOperator; */ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, EventHandler, Taggable, PermissionHandler, HoverEventSource, Sound.Emitter, Shape { - - private static final int VELOCITY_UPDATE_INTERVAL = 1; - private static final Int2ObjectSyncMap ENTITY_BY_ID = Int2ObjectSyncMap.hashmap(); private static final Map ENTITY_BY_UUID = new ConcurrentHashMap<>(); private static final AtomicInteger LAST_ENTITY_ID = new AtomicInteger(); + // Certain entities should only have their position packets sent during synchronization + private static final Set SYNCHRONIZE_ONLY_ENTITIES = Set.of(EntityType.ITEM, EntityType.FALLING_BLOCK, + EntityType.ARROW, EntityType.SPECTRAL_ARROW, EntityType.TRIDENT, EntityType.LLAMA_SPIT, EntityType.WIND_CHARGE, + EntityType.FISHING_BOBBER, EntityType.SNOWBALL, EntityType.EGG, EntityType.ENDER_PEARL, EntityType.POTION, + EntityType.EYE_OF_ENDER, EntityType.DRAGON_FIREBALL, EntityType.FIREBALL, EntityType.SMALL_FIREBALL, + EntityType.TNT); + private final CachedPacket destroyPacketCache = new CachedPacket(() -> new DestroyEntitiesPacket(getEntityId())); protected Instance instance; @@ -96,7 +100,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev protected boolean onGround; protected BoundingBox boundingBox; - private PhysicsResult lastPhysicsResult = null; + private PhysicsResult previousPhysicsResult = null; protected Entity vehicle; @@ -106,18 +110,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev protected boolean hasPhysics = true; protected boolean hasCollision = true; - /** - * The amount of drag applied on the Y axle. - *

- * Unit: 1/tick - */ - protected double gravityDragPerTick; - /** - * Acceleration on the Y axle due to gravity - *

- * Unit: blocks/tick - */ - protected double gravityAcceleration; + private Aerodynamics aerodynamics; protected int gravityTickCount; // Number of tick where gravity tick was applied private final int id; @@ -192,8 +185,9 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev Entity.ENTITY_BY_ID.put(id, this); Entity.ENTITY_BY_UUID.put(uuid, this); - this.gravityAcceleration = entityType.registry().acceleration(); - this.gravityDragPerTick = entityType.registry().drag(); + EntitySpawnType type = entityType.registry().spawnType(); + this.aerodynamics = new Aerodynamics(entityType.registry().acceleration(), + type == EntitySpawnType.LIVING || type == EntitySpawnType.PLAYER ? 0.91 : 0.98, 1 - entityType.registry().drag()); final ServerProcess process = MinecraftServer.process(); if (process != null) { @@ -529,7 +523,9 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev this.entityType = entityType; this.metadata = new Metadata(this); this.entityMeta = EntityTypeImpl.createMeta(entityType, this, this.metadata); - + EntitySpawnType type = entityType.registry().spawnType(); + this.aerodynamics = aerodynamics.withAirResistance(type == EntitySpawnType.LIVING || + type == EntitySpawnType.PLAYER ? 0.91 : 0.98, 1 - entityType.registry().drag()); Set viewers = new HashSet<>(getViewers()); getViewers().forEach(this::updateOldViewer); viewers.forEach(this::updateNewViewer); @@ -559,8 +555,8 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev // Entity tick { - // Cache the number of "gravity tick" - velocityTick(); + // handle position and velocity updates + movementTick(); // handle block contacts touchTick(); @@ -580,117 +576,28 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev } } - private void velocityTick() { + @ApiStatus.Internal + protected void movementTick() { this.gravityTickCount = onGround ? 0 : gravityTickCount + 1; if (vehicle != null) return; - final boolean noGravity = hasNoGravity(); - final boolean hasVelocity = hasVelocity(); - if (!hasVelocity && noGravity) { - return; + boolean entityIsPlayer = this instanceof Player; + boolean entityFlying = entityIsPlayer && ((Player) this).isFlying(); + PhysicsResult physicsResult = PhysicsUtils.simulateMovement(position, velocity.div(ServerFlag.SERVER_TICKS_PER_SECOND), boundingBox, + instance.getWorldBorder(), instance, aerodynamics, hasNoGravity(), hasPhysics, onGround, entityFlying, previousPhysicsResult); + this.previousPhysicsResult = physicsResult; + + Chunk finalChunk = ChunkUtils.retrieve(instance, currentChunk, physicsResult.newPosition()); + if (!ChunkUtils.isLoaded(finalChunk)) return; + + velocity = physicsResult.newVelocity().mul(ServerFlag.SERVER_TICKS_PER_SECOND); + onGround = physicsResult.isOnGround(); + boolean shouldSendVelocity = !entityIsPlayer && hasVelocity(); + + if (!PlayerUtils.isSocketClient(this)) { + refreshPosition(physicsResult.newPosition(), true, !SYNCHRONIZE_ONLY_ENTITIES.contains(entityType)); + if (shouldSendVelocity) sendPacketToViewers(getVelocityPacket()); } - final float tps = MinecraftServer.TICK_PER_SECOND; - final Pos positionBeforeMove = getPosition(); - final Vec currentVelocity = getVelocity(); - final boolean wasOnGround = this.onGround; - final Vec deltaPos = currentVelocity.div(tps); - - final Pos newPosition; - final Vec newVelocity; - if (this.hasPhysics) { - final var physicsResult = CollisionUtils.handlePhysics(this, deltaPos, lastPhysicsResult); - this.lastPhysicsResult = physicsResult; - if (!PlayerUtils.isSocketClient(this)) - this.onGround = physicsResult.isOnGround(); - - newPosition = physicsResult.newPosition(); - newVelocity = physicsResult.newVelocity(); - } else { - newVelocity = deltaPos; - newPosition = position.add(currentVelocity.div(20)); - } - - // World border collision - final Pos finalVelocityPosition = CollisionUtils.applyWorldBorder(instance, position, newPosition); - final boolean positionChanged = !finalVelocityPosition.samePoint(position); - final boolean isPlayer = this instanceof Player; - final boolean flying = isPlayer && ((Player) this).isFlying(); - if (!positionChanged) { - if (flying) { - this.velocity = Vec.ZERO; - return; - } else if (hasVelocity || newVelocity.isZero()) { - this.velocity = noGravity ? Vec.ZERO : new Vec( - 0, - -gravityAcceleration * tps * (1 - gravityDragPerTick), - 0 - ); - if (this.ticks % VELOCITY_UPDATE_INTERVAL == 0) { - if (!isPlayer && !this.lastVelocityWasZero) { - sendPacketToViewers(getVelocityPacket()); - this.lastVelocityWasZero = !hasVelocity; - } - } - return; - } - } - final Chunk finalChunk = ChunkUtils.retrieve(instance, currentChunk, finalVelocityPosition); - if (!ChunkUtils.isLoaded(finalChunk)) { - // Entity shouldn't be updated when moving in an unloaded chunk - return; - } - - if (positionChanged) { - if (entityType == EntityTypes.ITEM || entityType == EntityType.FALLING_BLOCK) { - // TODO find other exceptions - this.previousPosition = this.position; - this.position = finalVelocityPosition; - refreshCoordinate(finalVelocityPosition); - } else { - if (!PlayerUtils.isSocketClient(this)) - refreshPosition(finalVelocityPosition, true); - } - } - - // Update velocity - if (!noGravity && (hasVelocity || !newVelocity.isZero())) { - updateVelocity(wasOnGround, flying, positionBeforeMove, newVelocity); - } - - // Verify if velocity packet has to be sent - if (this.ticks % VELOCITY_UPDATE_INTERVAL == 0) { - if (!isPlayer && (hasVelocity || !lastVelocityWasZero)) { - sendPacketToViewers(getVelocityPacket()); - this.lastVelocityWasZero = !hasVelocity; - } - } - } - - protected void updateVelocity(boolean wasOnGround, boolean flying, Pos positionBeforeMove, Vec newVelocity) { - EntitySpawnType type = entityType.registry().spawnType(); - final double airDrag = type == EntitySpawnType.LIVING || type == EntitySpawnType.PLAYER ? 0.91 : 0.98; - final double drag; - if (wasOnGround) { - final Chunk chunk = ChunkUtils.retrieve(instance, currentChunk, position); - synchronized (chunk) { - drag = chunk.getBlock(positionBeforeMove.sub(0, 0.5000001, 0)).registry().friction() * airDrag; - } - } else drag = airDrag; - - double gravity = flying ? 0 : gravityAcceleration; - double gravityDrag = flying ? 0.6 : (1 - gravityDragPerTick); - - this.velocity = newVelocity - // Apply gravity and drag - .apply((x, y, z) -> new Vec( - x * drag, - !hasNoGravity() ? (y - gravity) * gravityDrag : y, - z * drag - )) - // Convert from block/tick to block/sec - .mul(MinecraftServer.TICK_PER_SECOND) - // Prevent infinitely decreasing velocity - .apply(Vec.Operator.EPSILON); } private void touchTick() { @@ -972,21 +879,21 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev } /** - * Gets the gravity drag per tick. + * Gets the aerodynamics; how the entity behaves in the air. * - * @return the gravity drag per tick in block + * @return the aerodynamic properties this entity is using */ - public double getGravityDragPerTick() { - return gravityDragPerTick; + public @NotNull Aerodynamics getAerodynamics() { + return aerodynamics; } /** - * Gets the gravity acceleration. + * Sets the aerodynamics; how the entity behaves in the air. * - * @return the gravity acceleration in block + * @param aerodynamics the new aerodynamic properties */ - public double getGravityAcceleration() { - return gravityAcceleration; + public void setAerodynamics(@NotNull Aerodynamics aerodynamics) { + this.aerodynamics = aerodynamics; } /** @@ -998,18 +905,6 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev return gravityTickCount; } - /** - * Changes the gravity of the entity. - * - * @param gravityDragPerTick the gravity drag per tick in block - * @param gravityAcceleration the gravity acceleration in block - * @see Entities motion - */ - public void setGravity(double gravityDragPerTick, double gravityAcceleration) { - this.gravityDragPerTick = gravityDragPerTick; - this.gravityAcceleration = gravityAcceleration; - } - public double getDistance(@NotNull Point point) { return getPosition().distance(point); } @@ -1357,14 +1252,14 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev * @param newPosition the new position */ @ApiStatus.Internal - public void refreshPosition(@NotNull final Pos newPosition, boolean ignoreView) { + public void refreshPosition(@NotNull final Pos newPosition, boolean ignoreView, boolean sendPackets) { final var previousPosition = this.position; final Pos position = ignoreView ? previousPosition.withCoord(newPosition) : newPosition; if (position.equals(lastSyncedPosition)) return; this.position = position; this.previousPosition = previousPosition; if (!position.samePoint(previousPosition)) refreshCoordinate(position); - if (nextSynchronizationTick <= ticks + 1) { + if (nextSynchronizationTick <= ticks + 1 || !sendPackets) { // The entity will be synchronized at the end of its tick // not returning here will duplicate position packets return; @@ -1399,6 +1294,11 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev this.lastSyncedPosition = position; } + @ApiStatus.Internal + public void refreshPosition(@NotNull final Pos newPosition, boolean ignoreView) { + refreshPosition(newPosition, ignoreView, true); + } + @ApiStatus.Internal public void refreshPosition(@NotNull final Pos newPosition) { refreshPosition(newPosition, false); diff --git a/src/main/java/net/minestom/server/entity/EntityProjectile.java b/src/main/java/net/minestom/server/entity/EntityProjectile.java index eda26624a..19e25a928 100644 --- a/src/main/java/net/minestom/server/entity/EntityProjectile.java +++ b/src/main/java/net/minestom/server/entity/EntityProjectile.java @@ -29,6 +29,7 @@ import java.util.stream.Stream; public class EntityProjectile extends Entity { private final Entity shooter; + private boolean wasStuck; public EntityProjectile(@Nullable Entity shooter, @NotNull EntityType entityType) { super(entityType); @@ -99,12 +100,12 @@ public class EntityProjectile extends Entity { this.velocity = Vec.ZERO; sendPacketToViewersAndSelf(getVelocityPacket()); setNoGravity(true); + wasStuck = true; } else { - if (!super.onGround) { - return; - } + if (!wasStuck) return; + wasStuck = false; + setNoGravity(super.onGround); super.onGround = false; - setNoGravity(false); EventDispatcher.call(new ProjectileUncollideEvent(this)); } } diff --git a/src/test/java/net/minestom/server/entity/EntityVelocityIntegrationTest.java b/src/test/java/net/minestom/server/entity/EntityVelocityIntegrationTest.java index 2ee0bd429..05ad4bd13 100644 --- a/src/test/java/net/minestom/server/entity/EntityVelocityIntegrationTest.java +++ b/src/test/java/net/minestom/server/entity/EntityVelocityIntegrationTest.java @@ -1,5 +1,6 @@ package net.minestom.server.entity; +import net.minestom.server.instance.block.Block; import net.minestom.testing.Env; import net.minestom.testing.EnvTest; import net.minestom.server.coordinate.Pos; @@ -109,7 +110,7 @@ public class EntityVelocityIntegrationTest { var player = env.createPlayer(instance, new Pos(0, 42, 0)); env.tick(); - final double epsilon = 0.00001; + final double epsilon = 0.000001; assertEquals(player.getVelocity().y(), -1.568, epsilon); double previousVelocity = player.getVelocity().y(); @@ -118,7 +119,7 @@ public class EntityVelocityIntegrationTest { env.tick(); // Every tick, the y velocity is multiplied by 0.6, and after 27 ticks it should be 0 - for (int i = 0; i < 27; i++) { + for (int i = 0; i < 22; i++) { assertEquals(player.getVelocity().y(), previousVelocity * 0.6, epsilon); previousVelocity = player.getVelocity().y(); env.tick(); @@ -172,6 +173,8 @@ public class EntityVelocityIntegrationTest { viewerConnection.connect(instance, new Pos(1, 40, 1)).join(); var entity = new Entity(EntityType.ZOMBIE); entity.setInstance(instance, new Pos(0,40,0)).join(); + instance.setBlock(new Vec(0, 39, 0), Block.STONE); + env.tick(); // Tick because the entity is in the air, they'll send velocity from gravity AtomicInteger i = new AtomicInteger(); BooleanSupplier tickLoopCondition = () -> i.getAndIncrement() < Math.max(VELOCITY_UPDATE_INTERVAL, 1);