From 480a7dcaa2b26a074c97583f8177d2f7a3677876 Mon Sep 17 00:00:00 2001 From: Am Gone <67794767+Am-Gone@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:40:53 +0200 Subject: [PATCH 01/31] Fix entity position desync when spawned in an 8 block radius from world center (0,0,0) (#2299) * fix: entity spawn pos desync * Fix tests --- src/main/java/net/minestom/server/entity/Entity.java | 1 + .../minestom/server/entity/EntityVelocityIntegrationTest.java | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index 0aadbd69c82..187c38a815b 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -763,6 +763,7 @@ public CompletableFuture setInstance(@NotNull Instance instance, @NotNull this.isActive = true; this.position = spawnPosition; this.previousPosition = spawnPosition; + this.lastSyncedPosition = spawnPosition; this.previousPhysicsResult = null; this.instance = instance; return instance.loadOptionalChunk(spawnPosition).thenAccept(chunk -> { diff --git a/src/test/java/net/minestom/server/entity/EntityVelocityIntegrationTest.java b/src/test/java/net/minestom/server/entity/EntityVelocityIntegrationTest.java index c75823d8879..b9605b60d05 100644 --- a/src/test/java/net/minestom/server/entity/EntityVelocityIntegrationTest.java +++ b/src/test/java/net/minestom/server/entity/EntityVelocityIntegrationTest.java @@ -182,8 +182,6 @@ public void countVelocityPackets(Env env) { BooleanSupplier tickLoopCondition = () -> i.getAndIncrement() < Math.max(entity.getSynchronizationTicks() - 1, 19); var tracker = viewerConnection.trackIncoming(EntityVelocityPacket.class); - env.tickWhile(tickLoopCondition, null); - tracker.assertEmpty(); // Verify no updates are sent while the entity is not being synchronized entity.setVelocity(new Vec(0, 5, 0)); tracker = viewerConnection.trackIncoming(EntityVelocityPacket.class); From b0b31d466419da6f5cedf0b57864dfeb4edd6474 Mon Sep 17 00:00:00 2001 From: iam Date: Wed, 21 Aug 2024 17:13:48 -0400 Subject: [PATCH 02/31] Update API for swept entity <-> entity collisions (#2343) * Update API for swept entity <-> entity collisions --- .../server/collision/CollisionUtils.java | 29 +-- .../server/collision/EntityCollision.java | 68 +++---- .../server/entity/PlayerProjectile.java | 190 ------------------ .../EntityEntityCollisionIntegrationTest.java | 58 ++++++ 4 files changed, 107 insertions(+), 238 deletions(-) delete mode 100644 src/main/java/net/minestom/server/entity/PlayerProjectile.java create mode 100644 src/test/java/net/minestom/server/collision/EntityEntityCollisionIntegrationTest.java diff --git a/src/main/java/net/minestom/server/collision/CollisionUtils.java b/src/main/java/net/minestom/server/collision/CollisionUtils.java index 61e2c62cf98..fce1fea1914 100644 --- a/src/main/java/net/minestom/server/collision/CollisionUtils.java +++ b/src/main/java/net/minestom/server/collision/CollisionUtils.java @@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Collection; import java.util.function.Function; @ApiStatus.Internal @@ -42,28 +43,30 @@ public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec e } /** + * Checks for entity collisions * - * @param entity the entity to move - * @param entityVelocity the velocity of the entity - * @return the closest entity we collide with + * @param velocity the velocity of the entity + * @param extendRadius the largest entity bounding box we can collide with + * Measured from bottom center to top corner + * This is used to extend the search radius for entities we collide with + * For players this is (0.3^2 + 0.3^2 + 1.8^2) ^ (1/3) ~= 1.51 */ - public static PhysicsResult checkEntityCollisions(@NotNull Entity entity, @NotNull Vec entityVelocity) { - final Instance instance = entity.getInstance(); - assert instance != null; - return checkEntityCollisions(instance, entity.getBoundingBox(), entity.getPosition(), entityVelocity, 3, e -> true, null); + public static @NotNull Collection checkEntityCollisions(@NotNull Instance instance, @NotNull BoundingBox boundingBox, @NotNull Point pos, @NotNull Vec velocity, double extendRadius, @NotNull Function entityFilter, @Nullable PhysicsResult physicsResult) { + return EntityCollision.checkCollision(instance, boundingBox, pos, velocity, extendRadius, entityFilter, physicsResult); } /** + * Checks for entity collisions * + * @param entity the entity to check collisions for * @param velocity the velocity of the entity * @param extendRadius the largest entity bounding box we can collide with - * Measured from bottom center to top corner - * This is used to extend the search radius for entities we collide with - * For players this is (0.3^2 + 0.3^2 + 1.8^2) ^ (1/3) ~= 1.51 - * @return the closest entity we collide with + * @param entityFilter the entity filter + * @param physicsResult optional physics result + * @return the entity collision results */ - public static PhysicsResult checkEntityCollisions(@NotNull Instance instance, BoundingBox boundingBox, Point pos, @NotNull Vec velocity, double extendRadius, Function entityFilter, PhysicsResult blockResult) { - return EntityCollision.checkCollision(instance, boundingBox, pos, velocity, extendRadius, entityFilter, blockResult); + public static @NotNull Collection checkEntityCollisions(@NotNull Entity entity, @NotNull Vec velocity, double extendRadius, @NotNull Function entityFilter, @Nullable PhysicsResult physicsResult) { + return EntityCollision.checkCollision(entity.getInstance(), entity.getBoundingBox(), entity.getPosition(), velocity, extendRadius, entityFilter, physicsResult); } /** diff --git a/src/main/java/net/minestom/server/collision/EntityCollision.java b/src/main/java/net/minestom/server/collision/EntityCollision.java index dff967b2a8f..f7aaf701570 100644 --- a/src/main/java/net/minestom/server/collision/EntityCollision.java +++ b/src/main/java/net/minestom/server/collision/EntityCollision.java @@ -5,64 +5,62 @@ import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.BlockFace; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; import java.util.function.Function; final class EntityCollision { - public static PhysicsResult checkCollision(Instance instance, BoundingBox boundingBox, Point point, Vec entityVelocity, double extendRadius, Function entityFilter, PhysicsResult res) { - double minimumRes = res != null ? res.res().res : Double.MAX_VALUE; + /** + * Represents the result of a collision with an entity + * @param collisionPoint + * @param entity + * @param face null if the collision is not with a face + */ + public record EntityCollisionResult( + @NotNull Point collisionPoint, + @NotNull Entity entity, + @Nullable BlockFace face, + double percentage + ) implements Comparable { + @Override + public int compareTo(@NotNull EntityCollision.EntityCollisionResult o) { + return Double.compare(percentage, o.percentage); + } + } - if (instance == null) return null; - SweepResult sweepResult = new SweepResult(minimumRes, 0, 0, 0, null, 0, 0, 0); + static @NotNull List checkCollision(@NotNull Instance instance, @NotNull BoundingBox boundingBox, @NotNull Point point, @NotNull Vec entityVelocity, double extendRadius, @NotNull Function entityFilter, @Nullable PhysicsResult physicsResult) { + double minimumRes = physicsResult != null ? physicsResult.res().res : Double.MAX_VALUE; - double closestDistance = minimumRes; - Entity closestEntity = null; + List result = new ArrayList<>(); var maxDistance = Math.pow(boundingBox.height() * boundingBox.height() + boundingBox.depth()/2 * boundingBox.depth()/2 + boundingBox.width()/2 * boundingBox.width()/2, 1/3.0); double projectileDistance = entityVelocity.length(); for (Entity e : instance.getNearbyEntities(point, extendRadius + maxDistance + projectileDistance)) { + SweepResult sweepResult = new SweepResult(minimumRes, 0, 0, 0, null, 0, 0, 0); + if (!entityFilter.apply(e)) continue; if (!e.hasCollision()) continue; // Overlapping with entity, math can't be done we return the entity if (e.getBoundingBox().intersectBox(e.getPosition().sub(point), boundingBox)) { var p = Pos.fromPoint(point); - - return new PhysicsResult(p, - Vec.ZERO, - false, - true, - true, - true, - entityVelocity, - new Pos[] {p, p, p}, - new Shape[] {e, e, e}, - true, - sweepResult); + result.add(new EntityCollisionResult(p, e, null, 0)); } // Check collisions with entity e.getBoundingBox().intersectBoxSwept(point, entityVelocity, e.getPosition(), boundingBox, sweepResult); - if (sweepResult.res < closestDistance && sweepResult.res < 1) { - closestDistance = sweepResult.res; - closestEntity = e; + if (sweepResult.res < 1) { + var p = Pos.fromPoint(point).add(entityVelocity.mul(sweepResult.res)); + result.add(new EntityCollisionResult(p, e, null, sweepResult.res)); } } - Pos[] collisionPoints = new Pos[3]; - - return new PhysicsResult(Pos.fromPoint(point).add(entityVelocity.mul(closestDistance)), - Vec.ZERO, - sweepResult.normalY == -1, - sweepResult.normalX != 0, - sweepResult.normalY != 0, - sweepResult.normalZ != 0, - entityVelocity, - collisionPoints, - new Shape[] {closestEntity, closestEntity, closestEntity}, - sweepResult.normalX != 0 || sweepResult.normalZ != 0 || sweepResult.normalY != 0, - sweepResult - ); + return result; } } diff --git a/src/main/java/net/minestom/server/entity/PlayerProjectile.java b/src/main/java/net/minestom/server/entity/PlayerProjectile.java deleted file mode 100644 index 05ba3e08a1a..00000000000 --- a/src/main/java/net/minestom/server/entity/PlayerProjectile.java +++ /dev/null @@ -1,190 +0,0 @@ -package net.minestom.server.entity; - -import net.minestom.server.MinecraftServer; -import net.minestom.server.collision.CollisionUtils; -import net.minestom.server.collision.PhysicsResult; -import net.minestom.server.collision.ShapeImpl; -import net.minestom.server.coordinate.Point; -import net.minestom.server.coordinate.Pos; -import net.minestom.server.coordinate.Vec; -import net.minestom.server.entity.metadata.projectile.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.instance.Instance; -import net.minestom.server.instance.block.Block; -import net.minestom.server.thread.Acquirable; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -import java.util.Random; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ThreadLocalRandom; - -public class PlayerProjectile extends Entity { - private final Entity shooter; - private long cooldown = 0; - - public PlayerProjectile(Entity shooter, EntityType type) { - super(type); - this.shooter = shooter; - this.hasCollision = false; - setup(); - } - - private void setup() { - super.hasPhysics = false; - if (getEntityMeta() instanceof ProjectileMeta) { - ((ProjectileMeta) getEntityMeta()).setShooter(this.shooter); - } - } - - public void shoot(Point from, double power, double spread) { - var to = from.add(shooter.getPosition().direction()); - shoot(from, to, power, spread); - } - - @Override - public CompletableFuture setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) { - var res = super.setInstance(instance, spawnPosition); - - Pos insideBlock = checkInsideBlock(instance); - // Check if we're inside of a block - if (insideBlock != null) { - var e = new ProjectileCollideWithBlockEvent(this, Pos.fromPoint(spawnPosition), instance.getBlock(spawnPosition)); - MinecraftServer.getGlobalEventHandler().call(e); - } - - return res; - } - - public void shoot(@NotNull Point from, @NotNull Point to, double power, double spread) { - var instance = shooter.getInstance(); - if (instance == null) return; - - float yaw = -shooter.getPosition().yaw(); - float pitch = -shooter.getPosition().pitch(); - - double pitchDiff = pitch - 45; - if (pitchDiff == 0) pitchDiff = 0.0001; - double pitchAdjust = pitchDiff * 0.002145329238474369D; - - double dx = to.x() - from.x(); - double dy = to.y() - from.y() + pitchAdjust; - double dz = to.z() - from.z(); - if (!hasNoGravity()) { - final double xzLength = Math.sqrt(dx * dx + dz * dz); - dy += xzLength * 0.20000000298023224D; - } - - final double length = Math.sqrt(dx * dx + dy * dy + dz * dz); - dx /= length; - dy /= length; - dz /= length; - Random random = ThreadLocalRandom.current(); - spread *= 0.007499999832361937D; - dx += random.nextGaussian() * spread; - dy += random.nextGaussian() * spread; - dz += random.nextGaussian() * spread; - - final EntityShootEvent shootEvent = new EntityShootEvent(this.shooter, this, from, power, spread); - EventDispatcher.call(shootEvent); - if (shootEvent.isCancelled()) { - remove(); - return; - } - - final double mul = 20 * power; - Vec v = new Vec(dx * mul, dy * mul * 0.9, dz * mul); - - this.setInstance(instance, new Pos(from.x(), from.y(), from.z(), yaw, pitch)).whenComplete((result, throwable) -> { - if (throwable != null) { - throwable.printStackTrace(); - } else { - this.setVelocity(v); - } - }); - - cooldown = System.currentTimeMillis(); - } - - private Pos checkInsideBlock(@NotNull Instance instance) { - var iterator = this.getBoundingBox().getBlocks(this.getPosition()); - - while (iterator.hasNext()) { - var block = iterator.next(); - Block b = instance.getBlock(block.blockX(), block.blockY(), block.blockZ()); - var hit = b.registry().collisionShape().intersectBox(this.getPosition().sub(block.x(), block.y(), block.z()), this.getBoundingBox()); - if (hit) return new Pos(block.blockX(), block.blockY(), block.blockZ()); - } - - return null; - } - - @Override - public void refreshPosition(@NotNull Pos newPosition) { - } - - @Override - public void tick(long time) { - final Pos posBefore = getPosition(); - super.tick(time); - if (super.isRemoved()) return; - - final Pos posNow = getPosition(); - - Vec diff = Vec.fromPoint(posNow.sub(posBefore)); - PhysicsResult result = CollisionUtils.handlePhysics( - instance, this.getChunk(), - this.getBoundingBox(), - posBefore, diff, - null, true - ); - - if (cooldown + 500 < System.currentTimeMillis()) { - float yaw = (float) Math.toDegrees(Math.atan2(diff.x(), diff.z())); - float pitch = (float) Math.toDegrees(Math.atan2(diff.y(), Math.sqrt(diff.x() * diff.x() + diff.z() * diff.z()))); - super.refreshPosition(new Pos(posNow.x(), posNow.y(), posNow.z(), yaw, pitch)); - cooldown = System.currentTimeMillis(); - } - - PhysicsResult collided = CollisionUtils.checkEntityCollisions(instance, this.getBoundingBox(), posBefore, diff, 3, (e) -> e != this, result); - if (collided != null && collided.collisionShapes()[0] != shooter) { - if (collided.collisionShapes()[0] instanceof Entity entity) { - var e = new ProjectileCollideWithEntityEvent(this, collided.newPosition(), entity); - MinecraftServer.getGlobalEventHandler().call(e); - return; - } - } - - if (result.hasCollision()) { - Block hitBlock = null; - Point hitPoint = null; - if (result.collisionShapes()[0] instanceof ShapeImpl block) { - hitBlock = block.block(); - hitPoint = result.collisionPoints()[0]; - } - if (result.collisionShapes()[1] instanceof ShapeImpl block) { - hitBlock = block.block(); - hitPoint = result.collisionPoints()[1]; - } - if (result.collisionShapes()[2] instanceof ShapeImpl block) { - hitBlock = block.block(); - hitPoint = result.collisionPoints()[2]; - } - - if (hitBlock == null) return; - - var e = new ProjectileCollideWithBlockEvent(this, Pos.fromPoint(hitPoint), hitBlock); - MinecraftServer.getGlobalEventHandler().call(e); - } - } - - @ApiStatus.Experimental - @SuppressWarnings("unchecked") - @Override - public @NotNull Acquirable acquirable() { - return (Acquirable) super.acquirable(); - } -} diff --git a/src/test/java/net/minestom/server/collision/EntityEntityCollisionIntegrationTest.java b/src/test/java/net/minestom/server/collision/EntityEntityCollisionIntegrationTest.java new file mode 100644 index 00000000000..f879c1ba41a --- /dev/null +++ b/src/test/java/net/minestom/server/collision/EntityEntityCollisionIntegrationTest.java @@ -0,0 +1,58 @@ +package net.minestom.server.collision; + +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; +import net.minestom.testing.Env; +import net.minestom.testing.EnvTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@EnvTest +public class EntityEntityCollisionIntegrationTest { + @Test + public void entitySingleCollisionTest(Env env) { + var instance = env.createFlatInstance(); + + for (int i = -2; i <= 2; ++i) + for (int j = -2; j <= 2; ++j) + instance.loadChunk(i, j).join(); + + var movingEntity = new Entity(EntityType.ZOMBIE); + var stillEntity = new Entity(EntityType.ZOMBIE); + var doNotHitEntity = new Entity(EntityType.ZOMBIE); + + movingEntity.setInstance(instance, new Vec(0, 42, 0)).join(); + stillEntity.setInstance(instance, new Vec(0, 42, 1)).join(); + doNotHitEntity.setInstance(instance, new Vec(0, 42, 2)).join(); + + var result = CollisionUtils.checkEntityCollisions(movingEntity, new Vec(0, 0, 1), 1.51, entity -> entity != movingEntity, null); + + assertEquals(1, result.size()); + assertEquals(stillEntity, result.iterator().next().entity()); + } + + @Test + public void entityMultipleCollisionTest(Env env) { + var instance = env.createFlatInstance(); + + for (int i = -2; i <= 2; ++i) + for (int j = -2; j <= 2; ++j) + instance.loadChunk(i, j).join(); + + var movingEntity = new Entity(EntityType.ZOMBIE); + var stillEntity = new Entity(EntityType.ZOMBIE); + var stillEntity2 = new Entity(EntityType.ZOMBIE); + var doNotHitEntity = new Entity(EntityType.ZOMBIE); + + movingEntity.setInstance(instance, new Vec(0, 42, 0)).join(); + stillEntity.setInstance(instance, new Vec(0, 42, 1)).join(); + stillEntity2.setInstance(instance, new Vec(0, 42, 2)).join(); + doNotHitEntity.setInstance(instance, new Vec(0, 42, 3)).join(); + + var result = CollisionUtils.checkEntityCollisions(movingEntity, new Vec(0, 0, 2), 1.51, entity -> entity != movingEntity, null); + + assertEquals(2, result.size()); + } +} From b05bfd9689870626afa6ceba5a08a7059b35e68a Mon Sep 17 00:00:00 2001 From: iam Date: Wed, 21 Aug 2024 18:08:03 -0400 Subject: [PATCH 03/31] Move EntityCollisionResult to its own class (#2345) * Move EntityCollisionResult to its own class --- .../server/collision/CollisionUtils.java | 4 +-- .../server/collision/EntityCollision.java | 18 ------------- .../collision/EntityCollisionResult.java | 25 +++++++++++++++++++ 3 files changed, 27 insertions(+), 20 deletions(-) create mode 100644 src/main/java/net/minestom/server/collision/EntityCollisionResult.java diff --git a/src/main/java/net/minestom/server/collision/CollisionUtils.java b/src/main/java/net/minestom/server/collision/CollisionUtils.java index fce1fea1914..9adb3c8dd05 100644 --- a/src/main/java/net/minestom/server/collision/CollisionUtils.java +++ b/src/main/java/net/minestom/server/collision/CollisionUtils.java @@ -51,7 +51,7 @@ public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec e * This is used to extend the search radius for entities we collide with * For players this is (0.3^2 + 0.3^2 + 1.8^2) ^ (1/3) ~= 1.51 */ - public static @NotNull Collection checkEntityCollisions(@NotNull Instance instance, @NotNull BoundingBox boundingBox, @NotNull Point pos, @NotNull Vec velocity, double extendRadius, @NotNull Function entityFilter, @Nullable PhysicsResult physicsResult) { + public static @NotNull Collection checkEntityCollisions(@NotNull Instance instance, @NotNull BoundingBox boundingBox, @NotNull Point pos, @NotNull Vec velocity, double extendRadius, @NotNull Function entityFilter, @Nullable PhysicsResult physicsResult) { return EntityCollision.checkCollision(instance, boundingBox, pos, velocity, extendRadius, entityFilter, physicsResult); } @@ -65,7 +65,7 @@ public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec e * @param physicsResult optional physics result * @return the entity collision results */ - public static @NotNull Collection checkEntityCollisions(@NotNull Entity entity, @NotNull Vec velocity, double extendRadius, @NotNull Function entityFilter, @Nullable PhysicsResult physicsResult) { + public static @NotNull Collection checkEntityCollisions(@NotNull Entity entity, @NotNull Vec velocity, double extendRadius, @NotNull Function entityFilter, @Nullable PhysicsResult physicsResult) { return EntityCollision.checkCollision(entity.getInstance(), entity.getBoundingBox(), entity.getPosition(), velocity, extendRadius, entityFilter, physicsResult); } diff --git a/src/main/java/net/minestom/server/collision/EntityCollision.java b/src/main/java/net/minestom/server/collision/EntityCollision.java index f7aaf701570..563bfabfc46 100644 --- a/src/main/java/net/minestom/server/collision/EntityCollision.java +++ b/src/main/java/net/minestom/server/collision/EntityCollision.java @@ -14,24 +14,6 @@ import java.util.function.Function; final class EntityCollision { - /** - * Represents the result of a collision with an entity - * @param collisionPoint - * @param entity - * @param face null if the collision is not with a face - */ - public record EntityCollisionResult( - @NotNull Point collisionPoint, - @NotNull Entity entity, - @Nullable BlockFace face, - double percentage - ) implements Comparable { - @Override - public int compareTo(@NotNull EntityCollision.EntityCollisionResult o) { - return Double.compare(percentage, o.percentage); - } - } - static @NotNull List checkCollision(@NotNull Instance instance, @NotNull BoundingBox boundingBox, @NotNull Point point, @NotNull Vec entityVelocity, double extendRadius, @NotNull Function entityFilter, @Nullable PhysicsResult physicsResult) { double minimumRes = physicsResult != null ? physicsResult.res().res : Double.MAX_VALUE; diff --git a/src/main/java/net/minestom/server/collision/EntityCollisionResult.java b/src/main/java/net/minestom/server/collision/EntityCollisionResult.java new file mode 100644 index 00000000000..57d003b6d23 --- /dev/null +++ b/src/main/java/net/minestom/server/collision/EntityCollisionResult.java @@ -0,0 +1,25 @@ +package net.minestom.server.collision; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.Entity; +import net.minestom.server.instance.block.BlockFace; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Represents the result of a collision with an entity + * @param collisionPoint + * @param entity + * @param face null if the collision is not with a face + */ +public record EntityCollisionResult( + @NotNull Point collisionPoint, + @NotNull Entity entity, + @Nullable BlockFace face, + double percentage +) implements Comparable { + @Override + public int compareTo(@NotNull EntityCollisionResult o) { + return Double.compare(percentage, o.percentage); + } +} From 6b660a4e6a76a8b7999e46563a830c58a58bbbbd Mon Sep 17 00:00:00 2001 From: iam Date: Wed, 21 Aug 2024 19:52:56 -0400 Subject: [PATCH 04/31] Return directional information from entity collision (#2346) --- .../java/net/minestom/server/collision/EntityCollision.java | 5 +++-- .../net/minestom/server/collision/EntityCollisionResult.java | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/minestom/server/collision/EntityCollision.java b/src/main/java/net/minestom/server/collision/EntityCollision.java index 563bfabfc46..e2c1f87ebbf 100644 --- a/src/main/java/net/minestom/server/collision/EntityCollision.java +++ b/src/main/java/net/minestom/server/collision/EntityCollision.java @@ -31,7 +31,7 @@ final class EntityCollision { // Overlapping with entity, math can't be done we return the entity if (e.getBoundingBox().intersectBox(e.getPosition().sub(point), boundingBox)) { var p = Pos.fromPoint(point); - result.add(new EntityCollisionResult(p, e, null, 0)); + result.add(new EntityCollisionResult(p, e, Vec.ZERO, 0)); } // Check collisions with entity @@ -39,7 +39,8 @@ final class EntityCollision { if (sweepResult.res < 1) { var p = Pos.fromPoint(point).add(entityVelocity.mul(sweepResult.res)); - result.add(new EntityCollisionResult(p, e, null, sweepResult.res)); + Vec direction = new Vec(sweepResult.collidedPositionX, sweepResult.collidedPositionY, sweepResult.collidedPositionZ); + result.add(new EntityCollisionResult(p, e, direction, sweepResult.res)); } } diff --git a/src/main/java/net/minestom/server/collision/EntityCollisionResult.java b/src/main/java/net/minestom/server/collision/EntityCollisionResult.java index 57d003b6d23..05648657000 100644 --- a/src/main/java/net/minestom/server/collision/EntityCollisionResult.java +++ b/src/main/java/net/minestom/server/collision/EntityCollisionResult.java @@ -1,6 +1,7 @@ package net.minestom.server.collision; import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; import net.minestom.server.instance.block.BlockFace; import org.jetbrains.annotations.NotNull; @@ -10,12 +11,12 @@ * Represents the result of a collision with an entity * @param collisionPoint * @param entity - * @param face null if the collision is not with a face + * @param direction the direction of the collision. ex. Vec(-1, 0, 0) means the entity collided with the west face of the entity */ public record EntityCollisionResult( @NotNull Point collisionPoint, @NotNull Entity entity, - @Nullable BlockFace face, + @NotNull Vec direction, double percentage ) implements Comparable { @Override From a521c4e7cd1971f95fc7280fc986f663073b18ac Mon Sep 17 00:00:00 2001 From: Lorenz Wrobel <43410952+DasBabyPixel@users.noreply.github.com> Date: Thu, 22 Aug 2024 03:15:19 +0200 Subject: [PATCH 05/31] fix entity collisions (#2348) --- .../java/net/minestom/server/collision/EntityCollision.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/minestom/server/collision/EntityCollision.java b/src/main/java/net/minestom/server/collision/EntityCollision.java index e2c1f87ebbf..e465e89e237 100644 --- a/src/main/java/net/minestom/server/collision/EntityCollision.java +++ b/src/main/java/net/minestom/server/collision/EntityCollision.java @@ -32,12 +32,13 @@ final class EntityCollision { if (e.getBoundingBox().intersectBox(e.getPosition().sub(point), boundingBox)) { var p = Pos.fromPoint(point); result.add(new EntityCollisionResult(p, e, Vec.ZERO, 0)); + continue; } // Check collisions with entity - e.getBoundingBox().intersectBoxSwept(point, entityVelocity, e.getPosition(), boundingBox, sweepResult); + boolean intersected = e.getBoundingBox().intersectBoxSwept(point, entityVelocity, e.getPosition(), boundingBox, sweepResult); - if (sweepResult.res < 1) { + if (intersected && sweepResult.res < 1) { var p = Pos.fromPoint(point).add(entityVelocity.mul(sweepResult.res)); Vec direction = new Vec(sweepResult.collidedPositionX, sweepResult.collidedPositionY, sweepResult.collidedPositionZ); result.add(new EntityCollisionResult(p, e, direction, sweepResult.res)); From b832db7764a3efb1e4d96bc8d4a9a8c4283a8064 Mon Sep 17 00:00:00 2001 From: Lorenz Wrobel <43410952+DasBabyPixel@users.noreply.github.com> Date: Mon, 26 Aug 2024 02:11:29 +0200 Subject: [PATCH 06/31] Ability to get collision shape position from PhysicsResult (#2355) * ability to get collision shape position from PhysicsResult --- .../minestom/server/collision/BlockCollision.java | 12 ++++++++---- .../net/minestom/server/collision/BoundingBox.java | 3 +++ .../minestom/server/collision/CollisionUtils.java | 2 +- .../minestom/server/collision/EntityCollision.java | 2 +- .../net/minestom/server/collision/PhysicsResult.java | 10 ++++++---- .../net/minestom/server/collision/PhysicsUtils.java | 2 +- .../java/net/minestom/server/collision/RayUtils.java | 2 +- .../net/minestom/server/collision/ShapeImpl.java | 4 +++- .../net/minestom/server/collision/SweepResult.java | 8 ++++++-- .../collision/EntityBlockPhysicsIntegrationTest.java | 2 +- 10 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/main/java/net/minestom/server/collision/BlockCollision.java b/src/main/java/net/minestom/server/collision/BlockCollision.java index 698c7ff72c2..248bd9d4a67 100644 --- a/src/main/java/net/minestom/server/collision/BlockCollision.java +++ b/src/main/java/net/minestom/server/collision/BlockCollision.java @@ -29,7 +29,7 @@ static PhysicsResult handlePhysics(@NotNull BoundingBox boundingBox, 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); + velocity, new Point[3], new Shape[3], new Point[3], false, SweepResult.NO_COLLISION); } // Fast-exit using cache final PhysicsResult cachedResult = cachedPhysics(velocity, entityPosition, getter, lastPhysicsResult); @@ -90,12 +90,13 @@ private static PhysicsResult stepPhysics(@NotNull BoundingBox boundingBox, @NotNull Vec velocity, @NotNull Pos entityPosition, @NotNull Block.Getter getter, boolean singleCollision) { // Allocate once and update values - SweepResult finalResult = new SweepResult(1 - Vec.EPSILON, 0, 0, 0, null, 0, 0, 0); + SweepResult finalResult = new SweepResult(1 - Vec.EPSILON, 0, 0, 0, null, 0, 0, 0, 0, 0, 0); boolean foundCollisionX = false, foundCollisionY = false, foundCollisionZ = false; Point[] collidedPoints = new Point[3]; Shape[] collisionShapes = new Shape[3]; + Point[] collisionShapePositions = new Point[3]; boolean hasCollided = false; @@ -114,18 +115,21 @@ private static PhysicsResult stepPhysics(@NotNull BoundingBox boundingBox, if (result.collisionX()) { foundCollisionX = true; collisionShapes[0] = finalResult.collidedShape; + collisionShapePositions[0] = new Vec(finalResult.collidedShapeX, finalResult.collidedShapeY, finalResult.collidedShapeZ); collidedPoints[0] = new Vec(finalResult.collidedPositionX, finalResult.collidedPositionY, finalResult.collidedPositionZ); hasCollided = true; if (singleCollision) break; } else if (result.collisionZ()) { foundCollisionZ = true; collisionShapes[2] = finalResult.collidedShape; + collisionShapePositions[2] = new Vec(finalResult.collidedShapeX, finalResult.collidedShapeY, finalResult.collidedShapeZ); collidedPoints[2] = new Vec(finalResult.collidedPositionX, finalResult.collidedPositionY, finalResult.collidedPositionZ); hasCollided = true; if (singleCollision) break; } else if (result.collisionY()) { foundCollisionY = true; collisionShapes[1] = finalResult.collidedShape; + collisionShapePositions[1] = new Vec(finalResult.collidedShapeX, finalResult.collidedShapeY, finalResult.collidedShapeZ); collidedPoints[1] = new Vec(finalResult.collidedPositionX, finalResult.collidedPositionY, finalResult.collidedPositionZ); hasCollided = true; if (singleCollision) break; @@ -148,7 +152,7 @@ private static PhysicsResult stepPhysics(@NotNull BoundingBox boundingBox, return new PhysicsResult(result.newPosition(), new Vec(newDeltaX, newDeltaY, newDeltaZ), newDeltaY == 0 && velocity.y() < 0, - foundCollisionX, foundCollisionY, foundCollisionZ, velocity, collidedPoints, collisionShapes, hasCollided, finalResult); + foundCollisionX, foundCollisionY, foundCollisionZ, velocity, collidedPoints, collisionShapes, collisionShapePositions, hasCollided, finalResult); } private static PhysicsResult computePhysics(@NotNull BoundingBox boundingBox, @@ -185,7 +189,7 @@ private static PhysicsResult computePhysics(@NotNull BoundingBox boundingBox, return new PhysicsResult(finalPos, new Vec(remainingX, remainingY, remainingZ), collisionY, collisionX, collisionY, collisionZ, - Vec.ZERO, null, null, false, finalResult); + Vec.ZERO, null, null, null, false, finalResult); } private static boolean isDiagonal(Vec velocity) { diff --git a/src/main/java/net/minestom/server/collision/BoundingBox.java b/src/main/java/net/minestom/server/collision/BoundingBox.java index 5500a2290d9..7ab52315e83 100644 --- a/src/main/java/net/minestom/server/collision/BoundingBox.java +++ b/src/main/java/net/minestom/server/collision/BoundingBox.java @@ -53,6 +53,9 @@ public boolean intersectBoxSwept(@NotNull Point rayStart, @NotNull Point rayDire finalResult.collidedPositionX = rayStart.x() + rayDirection.x() * finalResult.res; finalResult.collidedPositionY = rayStart.y() + rayDirection.y() * finalResult.res; finalResult.collidedPositionZ = rayStart.z() + rayDirection.z() * finalResult.res; + finalResult.collidedShapeX = shapePos.x(); + finalResult.collidedShapeY = shapePos.y(); + finalResult.collidedShapeZ = shapePos.z(); finalResult.collidedShape = this; return true; } diff --git a/src/main/java/net/minestom/server/collision/CollisionUtils.java b/src/main/java/net/minestom/server/collision/CollisionUtils.java index 9adb3c8dd05..c73e6150635 100644 --- a/src/main/java/net/minestom/server/collision/CollisionUtils.java +++ b/src/main/java/net/minestom/server/collision/CollisionUtils.java @@ -191,6 +191,6 @@ public static Shape parseBlockShape(String collision, String occlusion, Registry 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); + new Shape[3], new Point[3], false, SweepResult.NO_COLLISION); } } diff --git a/src/main/java/net/minestom/server/collision/EntityCollision.java b/src/main/java/net/minestom/server/collision/EntityCollision.java index e465e89e237..f29229d9f5b 100644 --- a/src/main/java/net/minestom/server/collision/EntityCollision.java +++ b/src/main/java/net/minestom/server/collision/EntityCollision.java @@ -23,7 +23,7 @@ final class EntityCollision { double projectileDistance = entityVelocity.length(); for (Entity e : instance.getNearbyEntities(point, extendRadius + maxDistance + projectileDistance)) { - SweepResult sweepResult = new SweepResult(minimumRes, 0, 0, 0, null, 0, 0, 0); + SweepResult sweepResult = new SweepResult(minimumRes, 0, 0, 0, null, 0, 0, 0, 0, 0, 0); if (!entityFilter.apply(e)) continue; if (!e.hasCollision()) continue; diff --git a/src/main/java/net/minestom/server/collision/PhysicsResult.java b/src/main/java/net/minestom/server/collision/PhysicsResult.java index 06e02dbd85a..b5164fd8bbf 100644 --- a/src/main/java/net/minestom/server/collision/PhysicsResult.java +++ b/src/main/java/net/minestom/server/collision/PhysicsResult.java @@ -4,9 +4,8 @@ import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnknownNullability; -@ApiStatus.Experimental /** * The result of a physics simulation. * @param newPosition the new position of the entity @@ -18,7 +17,9 @@ * @param originalDelta the velocity delta of the entity * @param collisionPoints the points where the entity collided * @param collisionShapes the shapes the entity collided with + * @param collisionShapePositions the positions of the shapes the entity collided with */ +@ApiStatus.Experimental public record PhysicsResult( Pos newPosition, Vec newVelocity, @@ -27,8 +28,9 @@ public record PhysicsResult( boolean collisionY, boolean collisionZ, Vec originalDelta, - @NotNull Point[] collisionPoints, - @NotNull Shape[] collisionShapes, + @UnknownNullability Point @UnknownNullability [] collisionPoints, + @UnknownNullability Shape @UnknownNullability [] collisionShapes, + @UnknownNullability Point @UnknownNullability [] collisionShapePositions, boolean hasCollision, SweepResult res ) { diff --git a/src/main/java/net/minestom/server/collision/PhysicsUtils.java b/src/main/java/net/minestom/server/collision/PhysicsUtils.java index 76089299f52..f5cfebd8f5d 100644 --- a/src/main/java/net/minestom/server/collision/PhysicsUtils.java +++ b/src/main/java/net/minestom/server/collision/PhysicsUtils.java @@ -41,7 +41,7 @@ public final class PhysicsUtils { 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()); + physicsResult.originalDelta(), physicsResult.collisionPoints(), physicsResult.collisionShapes(), physicsResult.collisionShapePositions(), physicsResult.hasCollision(), physicsResult.res()); } private static @NotNull Vec updateVelocity(@NotNull Pos entityPosition, @NotNull Vec currentVelocity, @NotNull Block.Getter blockGetter, @NotNull Aerodynamics aerodynamics, diff --git a/src/main/java/net/minestom/server/collision/RayUtils.java b/src/main/java/net/minestom/server/collision/RayUtils.java index d766f3082b3..6d0767ff60d 100644 --- a/src/main/java/net/minestom/server/collision/RayUtils.java +++ b/src/main/java/net/minestom/server/collision/RayUtils.java @@ -174,6 +174,6 @@ private static double epsilon(double value) { } public static boolean BoundingBoxRayIntersectionCheck(Vec start, Vec direction, BoundingBox boundingBox, Pos position) { - return BoundingBoxIntersectionCheck(BoundingBox.ZERO, start, direction, boundingBox, position, new SweepResult(Double.MAX_VALUE, 0, 0, 0, null, 0, 0, 0)); + return BoundingBoxIntersectionCheck(BoundingBox.ZERO, start, direction, boundingBox, position, new SweepResult(Double.MAX_VALUE, 0, 0, 0, null, 0, 0, 0, 0, 0, 0)); } } diff --git a/src/main/java/net/minestom/server/collision/ShapeImpl.java b/src/main/java/net/minestom/server/collision/ShapeImpl.java index e528797fa4f..1660501e6bf 100644 --- a/src/main/java/net/minestom/server/collision/ShapeImpl.java +++ b/src/main/java/net/minestom/server/collision/ShapeImpl.java @@ -185,7 +185,9 @@ public boolean intersectBoxSwept(@NotNull Point rayStart, @NotNull Point rayDire finalResult.collidedPositionX = rayStart.x() + rayDirection.x() * finalResult.res; finalResult.collidedPositionY = rayStart.y() + rayDirection.y() * finalResult.res; finalResult.collidedPositionZ = rayStart.z() + rayDirection.z() * finalResult.res; - + finalResult.collidedShapeX = shapePos.x(); + finalResult.collidedShapeY = shapePos.y(); + finalResult.collidedShapeZ = shapePos.z(); finalResult.collidedShape = this; hitBlock = true; } diff --git a/src/main/java/net/minestom/server/collision/SweepResult.java b/src/main/java/net/minestom/server/collision/SweepResult.java index 348e41238ff..2bda5073d8b 100644 --- a/src/main/java/net/minestom/server/collision/SweepResult.java +++ b/src/main/java/net/minestom/server/collision/SweepResult.java @@ -1,11 +1,12 @@ package net.minestom.server.collision; public final class SweepResult { - public static SweepResult NO_COLLISION = new SweepResult(Double.MAX_VALUE, 0, 0, 0, null, 0, 0, 0); + public static SweepResult NO_COLLISION = new SweepResult(Double.MAX_VALUE, 0, 0, 0, null, 0, 0, 0, 0, 0, 0); double res; double normalX, normalY, normalZ; double collidedPositionX, collidedPositionY, collidedPositionZ; + double collidedShapeX, collidedShapeY, collidedShapeZ; Shape collidedShape; /** @@ -16,7 +17,7 @@ public final class SweepResult { * @param normalY -1 if intersected on bottom, 1 if intersected on top * @param normalZ -1 if intersected on front, 1 if intersected on back */ - public SweepResult(double res, double normalX, double normalY, double normalZ, Shape collidedShape, double collidedPosX, double collidedPosY, double collidedPosZ) { + public SweepResult(double res, double normalX, double normalY, double normalZ, Shape collidedShape, double collidedPosX, double collidedPosY, double collidedPosZ, double collidedShapeX, double collidedShapeY, double collidedShapeZ) { this.res = res; this.normalX = normalX; this.normalY = normalY; @@ -25,5 +26,8 @@ public SweepResult(double res, double normalX, double normalY, double normalZ, S this.collidedPositionX = collidedPosX; this.collidedPositionY = collidedPosY; this.collidedPositionZ = collidedPosZ; + this.collidedShapeX = collidedShapeX; + this.collidedShapeY = collidedShapeY; + this.collidedShapeZ = collidedShapeZ; } } diff --git a/src/test/java/net/minestom/server/collision/EntityBlockPhysicsIntegrationTest.java b/src/test/java/net/minestom/server/collision/EntityBlockPhysicsIntegrationTest.java index 4f740e47a62..0801f00fe72 100644 --- a/src/test/java/net/minestom/server/collision/EntityBlockPhysicsIntegrationTest.java +++ b/src/test/java/net/minestom/server/collision/EntityBlockPhysicsIntegrationTest.java @@ -428,7 +428,7 @@ public void entityPhysicsCheckEntityHit(Env env) { BoundingBox bb = new Entity(EntityType.ZOMBIE).getBoundingBox(); - SweepResult sweepResultFinal = new SweepResult(1, 0, 0, 0, null, 0, 0, 0); + SweepResult sweepResultFinal = new SweepResult(1, 0, 0, 0, null, 0, 0, 0, 0, 0, 0); bb.intersectBoxSwept(z1, movement, z2, bb, sweepResultFinal); bb.intersectBoxSwept(z1, movement, z3, bb, sweepResultFinal); From 789befee315a8d10ce104a0919c731a7501517fc Mon Sep 17 00:00:00 2001 From: themode Date: Mon, 26 Aug 2024 06:12:32 +0200 Subject: [PATCH 07/31] Coordinate limit to prevent potential overflow --- .../server/listener/PlayerPositionListener.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/net/minestom/server/listener/PlayerPositionListener.java b/src/main/java/net/minestom/server/listener/PlayerPositionListener.java index 3b7d3cdad2b..f58d386ae97 100644 --- a/src/main/java/net/minestom/server/listener/PlayerPositionListener.java +++ b/src/main/java/net/minestom/server/listener/PlayerPositionListener.java @@ -1,5 +1,6 @@ package net.minestom.server.listener; +import net.kyori.adventure.text.Component; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Player; import net.minestom.server.event.EventDispatcher; @@ -11,6 +12,8 @@ import org.jetbrains.annotations.NotNull; public class PlayerPositionListener { + private static final double MAX_COORDINATE = 30_000_000; + private static final Component KICK_MESSAGE = Component.text("You moved too far away!"); public static void playerPacketListener(ClientPlayerPacket packet, Player player) { player.refreshOnGround(packet.onGround()); @@ -33,6 +36,15 @@ public static void teleportConfirmListener(ClientTeleportConfirmPacket packet, P } private static void processMovement(@NotNull Player player, @NotNull Pos packetPosition, boolean onGround) { + // Prevent the player from moving too far + // Doubles close to max size can cause overflow, or simply have precision issues + if (Math.abs(packetPosition.x()) > MAX_COORDINATE || + Math.abs(packetPosition.y()) > MAX_COORDINATE || + Math.abs(packetPosition.z()) > MAX_COORDINATE) { + player.kick(KICK_MESSAGE); + return; + } + final var currentPosition = player.getPosition(); if (currentPosition.equals(packetPosition)) { // For some reason, the position is the same From 8f469134865ac059846eda4a196a4ddf02b19111 Mon Sep 17 00:00:00 2001 From: themode Date: Wed, 28 Aug 2024 20:56:13 +0200 Subject: [PATCH 08/31] fix some jcstress files --- .../java/net/minestom/server/tag/TagPathOverrideTest.java | 7 ++++--- .../jcstress/java/net/minestom/server/tag/TagPathTest.java | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/jcstress-tests/src/jcstress/java/net/minestom/server/tag/TagPathOverrideTest.java b/jcstress-tests/src/jcstress/java/net/minestom/server/tag/TagPathOverrideTest.java index 9686507f20b..dd71b25133d 100644 --- a/jcstress-tests/src/jcstress/java/net/minestom/server/tag/TagPathOverrideTest.java +++ b/jcstress-tests/src/jcstress/java/net/minestom/server/tag/TagPathOverrideTest.java @@ -1,11 +1,12 @@ package net.minestom.server.tag; -import org.jglrxavpok.hephaistos.nbt.NBT; +import net.kyori.adventure.nbt.CompoundBinaryTag; import org.openjdk.jcstress.annotations.*; import org.openjdk.jcstress.infra.results.L_Result; import java.util.Map; +import static net.kyori.adventure.nbt.IntBinaryTag.intBinaryTag; import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; @JCStressTest @@ -30,9 +31,9 @@ public void actor2() { @Arbiter public void arbiter(L_Result r) { var compound = handler.asCompound(); - if (compound.equals(NBT.Compound(Map.of("path", NBT.Compound(Map.of("key", NBT.Int(1))))))) { + if (compound.equals(CompoundBinaryTag.from(Map.of("path", CompoundBinaryTag.from(Map.of("key", intBinaryTag(1))))))) { r.r1 = "actor1"; - } else if (compound.equals(NBT.Compound(Map.of("path", NBT.Compound(Map.of("key", NBT.Int(5))))))) { + } else if (compound.equals(CompoundBinaryTag.from(Map.of("path", CompoundBinaryTag.from(Map.of("key", intBinaryTag(5))))))) { r.r1 = "actor2"; } else { r.r1 = compound; diff --git a/jcstress-tests/src/jcstress/java/net/minestom/server/tag/TagPathTest.java b/jcstress-tests/src/jcstress/java/net/minestom/server/tag/TagPathTest.java index 6f3489c9b39..f42154046ea 100644 --- a/jcstress-tests/src/jcstress/java/net/minestom/server/tag/TagPathTest.java +++ b/jcstress-tests/src/jcstress/java/net/minestom/server/tag/TagPathTest.java @@ -1,11 +1,12 @@ package net.minestom.server.tag; -import org.jglrxavpok.hephaistos.nbt.NBT; +import net.kyori.adventure.nbt.CompoundBinaryTag; import org.openjdk.jcstress.annotations.*; import org.openjdk.jcstress.infra.results.L_Result; import java.util.Map; +import static net.kyori.adventure.nbt.IntBinaryTag.intBinaryTag; import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; @JCStressTest @@ -31,9 +32,9 @@ public void actor2() { @Arbiter public void arbiter(L_Result r) { var compound = handler.asCompound(); - if (compound.equals(NBT.Compound(Map.of("path", NBT.Int(1))))) { + if (compound.equals(CompoundBinaryTag.from(Map.of("path", intBinaryTag(1))))) { r.r1 = "tag"; - } else if (compound.equals(NBT.Compound(Map.of("path", NBT.Compound(Map.of("key", NBT.Int(5))))))) { + } else if (compound.equals(CompoundBinaryTag.from(Map.of("path", CompoundBinaryTag.from(Map.of("key", intBinaryTag(5))))))) { r.r1 = "tag_path"; } else { r.r1 = compound; From 65f75bb0595fc912b28c00be8dd61316d7898513 Mon Sep 17 00:00:00 2001 From: themode Date: Wed, 28 Aug 2024 21:01:41 +0200 Subject: [PATCH 09/31] clear remaining hephaistos usage --- .../minestom/server/tag/TagReadBenchmark.java | 23 +++++++++---------- .../server/tag/TagWriteBenchmark.java | 23 +++++++++---------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/jmh-benchmarks/src/jmh/java/net/minestom/server/tag/TagReadBenchmark.java b/jmh-benchmarks/src/jmh/java/net/minestom/server/tag/TagReadBenchmark.java index fc3d5871cf5..b7a91fe093c 100644 --- a/jmh-benchmarks/src/jmh/java/net/minestom/server/tag/TagReadBenchmark.java +++ b/jmh-benchmarks/src/jmh/java/net/minestom/server/tag/TagReadBenchmark.java @@ -1,11 +1,10 @@ package net.minestom.server.tag; -import org.jglrxavpok.hephaistos.nbt.NBT; -import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound; import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; import java.util.HashMap; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -24,8 +23,8 @@ public class TagReadBenchmark { TagHandler tagHandler; Tag secondTag; - MutableNBTCompound concurrentCompound; - MutableNBTCompound compound; + Map map; + Map concurrentMap; @Setup public void setup() { @@ -34,11 +33,11 @@ public void setup() { if (present) tagHandler.setTag(TAG, "value"); secondTag = Tag.String("key"); // Concurrent map benchmark - this.concurrentCompound = new MutableNBTCompound(new ConcurrentHashMap<>()); - if (present) concurrentCompound.set("key", NBT.String("value")); + map = new HashMap<>(); + if (present) map.put("key", "value"); // Hash map benchmark - this.compound = new MutableNBTCompound(new HashMap<>()); - if (present) compound.set("key", NBT.String("value")); + concurrentMap = new ConcurrentHashMap<>(); + if (present) concurrentMap.put("key", "value"); } @Benchmark @@ -57,12 +56,12 @@ public void readNewTag(Blackhole blackhole) { } @Benchmark - public void readConcurrentCompound(Blackhole blackhole) { - blackhole.consume(concurrentCompound.getString("key")); + public void readConcurrentMap(Blackhole blackhole) { + blackhole.consume(concurrentMap.get("key")); } @Benchmark - public void readCompound(Blackhole blackhole) { - blackhole.consume(compound.getString("key")); + public void readMap(Blackhole blackhole) { + blackhole.consume(map.get("key")); } } diff --git a/jmh-benchmarks/src/jmh/java/net/minestom/server/tag/TagWriteBenchmark.java b/jmh-benchmarks/src/jmh/java/net/minestom/server/tag/TagWriteBenchmark.java index 4da4d4a060b..182e7b3879a 100644 --- a/jmh-benchmarks/src/jmh/java/net/minestom/server/tag/TagWriteBenchmark.java +++ b/jmh-benchmarks/src/jmh/java/net/minestom/server/tag/TagWriteBenchmark.java @@ -1,10 +1,9 @@ package net.minestom.server.tag; -import org.jglrxavpok.hephaistos.nbt.NBT; -import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound; import org.openjdk.jmh.annotations.*; import java.util.HashMap; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -20,8 +19,8 @@ public class TagWriteBenchmark { TagHandler tagHandler; Tag secondTag; - MutableNBTCompound concurrentCompound; - MutableNBTCompound compound; + Map map; + Map concurrentMap; @Setup public void setup() { @@ -30,11 +29,11 @@ public void setup() { tagHandler.setTag(TAG, "value"); secondTag = Tag.String("key"); // Concurrent map benchmark - this.concurrentCompound = new MutableNBTCompound(new ConcurrentHashMap<>()); - concurrentCompound.set("key", NBT.String("value")); + map = new HashMap<>(); + map.put("key", "value"); // Hash map benchmark - this.compound = new MutableNBTCompound(new HashMap<>()); - compound.set("key", NBT.String("value")); + concurrentMap = new ConcurrentHashMap<>(); + concurrentMap.put("key", "value"); } @Benchmark @@ -53,12 +52,12 @@ public void writeNewTag() { } @Benchmark - public void writeConcurrentCompound() { - concurrentCompound.setString("key", "value"); + public void writeConcurrentMap() { + concurrentMap.put("key", "value"); } @Benchmark - public void writeCompound() { - compound.setString("key", "value"); + public void writeMap() { + map.put("key", "value"); } } From b1ad94cd1b0815626890235baf0553917d129564 Mon Sep 17 00:00:00 2001 From: TheMode Date: Tue, 3 Sep 2024 11:01:34 +0200 Subject: [PATCH 10/31] Make BoundingBox/ShapeImpl records (#2371) --- .../server/collision/BlockCollision.java | 8 +- .../server/collision/BoundingBox.java | 90 ++----- .../server/collision/CollisionUtils.java | 26 +- .../minestom/server/collision/ShapeImpl.java | 222 +++++++++--------- 4 files changed, 158 insertions(+), 188 deletions(-) diff --git a/src/main/java/net/minestom/server/collision/BlockCollision.java b/src/main/java/net/minestom/server/collision/BlockCollision.java index 248bd9d4a67..e4392fde37d 100644 --- a/src/main/java/net/minestom/server/collision/BlockCollision.java +++ b/src/main/java/net/minestom/server/collision/BlockCollision.java @@ -70,16 +70,18 @@ static Entity canPlaceBlockAt(Instance instance, Point blockPos, Block b) { private static PhysicsResult cachedPhysics(Vec velocity, Pos entityPosition, Block.Getter getter, PhysicsResult lastPhysicsResult) { if (lastPhysicsResult != null && lastPhysicsResult.collisionShapes()[1] instanceof ShapeImpl shape) { - Block collisionBlockY = shape.block(); + var currentBlock = getter.getBlock(lastPhysicsResult.collisionPoints()[1].sub(0, Vec.EPSILON, 0), Block.Getter.Condition.TYPE); + var lastBlockBoxes = shape.collisionBoundingBoxes(); + var currentBlockBoxes = ((ShapeImpl) currentBlock.registry().collisionShape()).collisionBoundingBoxes(); // Fast exit if entity hasn't moved if (lastPhysicsResult.collisionY() && velocity.y() == lastPhysicsResult.originalDelta().y() // Check block below to fast exit gravity - && getter.getBlock(lastPhysicsResult.collisionPoints()[1].sub(0, Vec.EPSILON, 0), Block.Getter.Condition.TYPE) == collisionBlockY + && currentBlockBoxes.equals(lastBlockBoxes) && velocity.x() == 0 && velocity.z() == 0 && entityPosition.samePoint(lastPhysicsResult.newPosition()) - && collisionBlockY != Block.AIR) { + && !lastBlockBoxes.isEmpty()) { return lastPhysicsResult; } } diff --git a/src/main/java/net/minestom/server/collision/BoundingBox.java b/src/main/java/net/minestom/server/collision/BoundingBox.java index 7ab52315e83..7b1986ac916 100644 --- a/src/main/java/net/minestom/server/collision/BoundingBox.java +++ b/src/main/java/net/minestom/server/collision/BoundingBox.java @@ -13,22 +13,15 @@ /** * See https://wiki.vg/Entity_metadata#Mobs_2 */ -public final class BoundingBox implements Shape { - private static final BoundingBox sleepingBoundingBox = new BoundingBox(0.2, 0.2, 0.2); - private static final BoundingBox sneakingBoundingBox = new BoundingBox(0.6, 1.5, 0.6); - private static final BoundingBox smallBoundingBox = new BoundingBox(0.6, 0.6, 0.6); +public record BoundingBox(Vec relativeStart, Vec relativeEnd) implements Shape { + private static final BoundingBox SLEEPING = new BoundingBox(0.2, 0.2, 0.2); + private static final BoundingBox SNEAKING = new BoundingBox(0.6, 1.5, 0.6); + private static final BoundingBox SMALL = new BoundingBox(0.6, 0.6, 0.6); - final static BoundingBox ZERO = new BoundingBox(0, 0, 0); - - private final double width, height, depth; - private final Point offset; - private Point relativeEnd; + final static BoundingBox ZERO = new BoundingBox(Vec.ZERO, Vec.ZERO); public BoundingBox(double width, double height, double depth, Point offset) { - this.width = width; - this.height = height; - this.depth = depth; - this.offset = offset; + this(Vec.fromPoint(offset), new Vec(width, height, depth).add(offset)); } public BoundingBox(double width, double height, double depth) { @@ -49,7 +42,7 @@ public boolean intersectBox(@NotNull Point positionRelative, @NotNull BoundingBo @Override public boolean intersectBoxSwept(@NotNull Point rayStart, @NotNull Point rayDirection, @NotNull Point shapePos, @NotNull BoundingBox moving, @NotNull SweepResult finalResult) { - if (RayUtils.BoundingBoxIntersectionCheck(moving, rayStart, rayDirection, this, shapePos, finalResult) ) { + if (RayUtils.BoundingBoxIntersectionCheck(moving, rayStart, rayDirection, this, shapePos, finalResult)) { finalResult.collidedPositionX = rayStart.x() + rayDirection.x() * finalResult.res; finalResult.collidedPositionY = rayStart.y() + rayDirection.y() * finalResult.res; finalResult.collidedPositionZ = rayStart.z() + rayDirection.z() * finalResult.res; @@ -67,30 +60,6 @@ public boolean boundingBoxRayIntersectionCheck(Vec start, Vec direction, Pos pos return RayUtils.BoundingBoxRayIntersectionCheck(start, direction, this, position); } - @Override - public @NotNull Point relativeStart() { - return offset; - } - - @Override - public @NotNull Point relativeEnd() { - Point relativeEnd = this.relativeEnd; - if (relativeEnd == null) this.relativeEnd = relativeEnd = offset.add(width, height, depth); - return relativeEnd; - } - - @Override - public String toString() { - String result = "BoundingBox"; - result += "\n"; - result += "[" + minX() + " : " + maxX() + "]"; - result += "\n"; - result += "[" + minY() + " : " + maxY() + "]"; - result += "\n"; - result += "[" + minZ() + " : " + maxZ() + "]"; - return result; - } - /** * Creates a new {@link BoundingBox} with an expanded size. * @@ -100,7 +69,7 @@ public String toString() { * @return a new {@link BoundingBox} expanded */ public @NotNull BoundingBox expand(double x, double y, double z) { - return new BoundingBox(this.width + x, this.height + y, this.depth + z); + return new BoundingBox(width() + x, height() + y, depth() + z); } /** @@ -112,7 +81,7 @@ public String toString() { * @return a new bounding box contracted */ public @NotNull BoundingBox contract(double x, double y, double z) { - return new BoundingBox(this.width - x, this.height - y, this.depth - z); + return new BoundingBox(width() - x, height() - y, depth() - z); } /** @@ -122,43 +91,43 @@ public String toString() { * @return a new bounding box with an offset. */ public @NotNull BoundingBox withOffset(Point offset) { - return new BoundingBox(this.width, this.height, this.depth, offset); + return new BoundingBox(width(), height(), depth(), offset); } public double width() { - return width; + return relativeEnd.x() - relativeStart.x(); } public double height() { - return height; + return relativeEnd.y() - relativeStart.y(); } public double depth() { - return depth; + return relativeEnd.z() - relativeStart.z(); } public double minX() { - return relativeStart().x(); + return relativeStart.x(); } public double maxX() { - return relativeEnd().x(); + return relativeEnd.x(); } public double minY() { - return relativeStart().y(); + return relativeStart.y(); } public double maxY() { - return relativeEnd().y(); + return relativeEnd.y(); } public double minZ() { - return relativeStart().z(); + return relativeStart.z(); } public double maxZ() { - return relativeEnd().z(); + return relativeEnd.z(); } public enum AxisMask { @@ -216,7 +185,9 @@ public static class PointIterator implements Iterator { private double minX, minY, minZ, maxX, maxY, maxZ; private final MutablePoint point = new MutablePoint(); - public PointIterator() {} + public PointIterator() { + } + public PointIterator(BoundingBox boundingBox, Point p, AxisMask axisMask, double axis) { reset(boundingBox, p, axisMask, axis); } @@ -282,22 +253,11 @@ public MutablePoint next() { } } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - BoundingBox that = (BoundingBox) o; - if (Double.compare(that.width, width) != 0) return false; - if (Double.compare(that.height, height) != 0) return false; - if (Double.compare(that.depth, depth) != 0) return false; - return offset.equals(that.offset); - } - public static @Nullable BoundingBox fromPose(@NotNull Entity.Pose pose) { return switch (pose) { - case FALL_FLYING, SWIMMING, SPIN_ATTACK -> smallBoundingBox; - case SLEEPING, DYING -> sleepingBoundingBox; - case SNEAKING -> sneakingBoundingBox; + case FALL_FLYING, SWIMMING, SPIN_ATTACK -> SMALL; + case SLEEPING, DYING -> SLEEPING; + case SNEAKING -> SNEAKING; default -> null; }; } diff --git a/src/main/java/net/minestom/server/collision/CollisionUtils.java b/src/main/java/net/minestom/server/collision/CollisionUtils.java index c73e6150635..7a01355f60d 100644 --- a/src/main/java/net/minestom/server/collision/CollisionUtils.java +++ b/src/main/java/net/minestom/server/collision/CollisionUtils.java @@ -26,10 +26,10 @@ public final class CollisionUtils { * Works by getting all the full blocks that an entity could interact with. * All bounding boxes inside the full blocks are checked for collisions with the entity. * - * @param entity the entity to move - * @param entityVelocity the velocity of the entity + * @param entity the entity to move + * @param entityVelocity the velocity of the entity * @param lastPhysicsResult the last physics result, can be null - * @param singleCollision if the entity should only collide with one block + * @param singleCollision if the entity should only collide with one block * @return the result of physics simulation */ public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec entityVelocity, @@ -45,7 +45,7 @@ public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec e /** * Checks for entity collisions * - * @param velocity the velocity of the entity + * @param velocity the velocity of the entity * @param extendRadius the largest entity bounding box we can collide with * Measured from bottom center to top corner * This is used to extend the search radius for entities we collide with @@ -58,10 +58,10 @@ public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec e /** * Checks for entity collisions * - * @param entity the entity to check collisions for - * @param velocity the velocity of the entity - * @param extendRadius the largest entity bounding box we can collide with - * @param entityFilter the entity filter + * @param entity the entity to check collisions for + * @param velocity the velocity of the entity + * @param extendRadius the largest entity bounding box we can collide with + * @param entityFilter the entity filter * @param physicsResult optional physics result * @return the entity collision results */ @@ -75,8 +75,8 @@ public static PhysicsResult handlePhysics(@NotNull Entity entity, @NotNull Vec e * Works by getting all the full blocks that an entity could interact with. * All bounding boxes inside the full blocks are checked for collisions with the entity. * - * @param entity the entity to move - * @param entityVelocity the velocity of the entity + * @param entity the entity to move + * @param entityVelocity the velocity of the entity * @param lastPhysicsResult the last physics result, can be null * @return the result of physics simulation */ @@ -170,7 +170,7 @@ public static Entity canPlaceBlockAt(Instance instance, Point blockPos, Block b) // from moving forward by supplying their previous position's value boolean xCollision = newPosition.x() > worldBorder.centerX() + radius || newPosition.x() < worldBorder.centerX() - radius; boolean zCollision = newPosition.z() > worldBorder.centerZ() + radius || newPosition.z() < worldBorder.centerZ() - radius; - if (xCollision || zCollision) { + if (xCollision || zCollision) { return newPosition.withCoord(xCollision ? currentPosition.x() : newPosition.x(), newPosition.y(), zCollision ? currentPosition.z() : newPosition.z()); } @@ -178,9 +178,9 @@ public static Entity canPlaceBlockAt(Instance instance, Point blockPos, Block b) } public static Shape parseBlockShape(String collision, String occlusion, Registry.BlockEntry blockEntry) { - return ShapeImpl.parseBlockFromRegistry(collision, occlusion, blockEntry); + return ShapeImpl.parseBlockFromRegistry(collision, occlusion, blockEntry.occludes(), blockEntry.lightEmission()); } - + /** * Simulate the entity's collision physics as if the world had no blocks * diff --git a/src/main/java/net/minestom/server/collision/ShapeImpl.java b/src/main/java/net/minestom/server/collision/ShapeImpl.java index 1660501e6bf..82b1ea497b6 100644 --- a/src/main/java/net/minestom/server/collision/ShapeImpl.java +++ b/src/main/java/net/minestom/server/collision/ShapeImpl.java @@ -4,100 +4,34 @@ import it.unimi.dsi.fastutil.doubles.DoubleList; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; -import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockFace; -import net.minestom.server.registry.Registry; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; -public final class ShapeImpl implements Shape { +public record ShapeImpl(CollisionData collisionData, LightData lightData) implements Shape { private static final Pattern PATTERN = Pattern.compile("\\d.\\d+", Pattern.MULTILINE); - private final List collisionBoundingBoxes; - private final Point relativeStart, relativeEnd; - private final byte fullFaces; - private final List occlusionBoundingBoxes; - private final byte blockOcclusion; - private final byte airOcclusion; - - private final Registry.BlockEntry blockEntry; - private Block block; - - private ShapeImpl(BoundingBox[] boundingBoxes, BoundingBox[] occlusionBoundingBoxes, Registry.BlockEntry blockEntry) { - this.collisionBoundingBoxes = List.of(boundingBoxes); - this.occlusionBoundingBoxes = List.of(occlusionBoundingBoxes); - this.blockEntry = blockEntry; - - // Find bounds of collision - if (!collisionBoundingBoxes.isEmpty()) { - double minX = 1, minY = 1, minZ = 1; - double maxX = 0, maxY = 0, maxZ = 0; - for (BoundingBox blockSection : collisionBoundingBoxes) { - // Min - if (blockSection.minX() < minX) minX = blockSection.minX(); - if (blockSection.minY() < minY) minY = blockSection.minY(); - if (blockSection.minZ() < minZ) minZ = blockSection.minZ(); - // Max - if (blockSection.maxX() > maxX) maxX = blockSection.maxX(); - if (blockSection.maxY() > maxY) maxY = blockSection.maxY(); - if (blockSection.maxZ() > maxZ) maxZ = blockSection.maxZ(); - } - this.relativeStart = new Vec(minX, minY, minZ); - this.relativeEnd = new Vec(maxX, maxY, maxZ); - } else { - this.relativeStart = Vec.ZERO; - this.relativeEnd = Vec.ZERO; - } - - byte fullCollisionFaces = 0; - for (BlockFace f : BlockFace.values()) { - final byte res = isFaceCovered(computeOcclusionSet(f, collisionBoundingBoxes)); - fullCollisionFaces |= ((res == 2) ? 0b1 : 0b0) << (byte) f.ordinal(); - } - this.fullFaces = fullCollisionFaces; - - byte airFaces = 0; - byte fullFaces = 0; - for (BlockFace f : BlockFace.values()) { - final byte res = isFaceCovered(computeOcclusionSet(f, this.occlusionBoundingBoxes)); - airFaces |= ((res == 0) ? 0b1 : 0b0) << (byte) f.ordinal(); - fullFaces |= ((res == 2) ? 0b1 : 0b0) << (byte) f.ordinal(); + record CollisionData(List collisionBoundingBoxes, + Point relativeStart, Point relativeEnd, + byte fullFaces) { + public CollisionData { + collisionBoundingBoxes = List.copyOf(collisionBoundingBoxes); } - - this.airOcclusion = airFaces; - this.blockOcclusion = fullFaces; } - private static BoundingBox[] parseRegistryBoundingBoxString(String str) { - final Matcher matcher = PATTERN.matcher(str); - DoubleList vals = new DoubleArrayList(); - while (matcher.find()) { - double newVal = Double.parseDouble(matcher.group()); - vals.add(newVal); + record LightData(List occlusionBoundingBoxes, + byte blockOcclusion, byte airOcclusion, + int lightEmission) { + public LightData { + occlusionBoundingBoxes = List.copyOf(occlusionBoundingBoxes); } - final int count = vals.size() / 6; - BoundingBox[] boundingBoxes = new BoundingBox[count]; - for (int i = 0; i < count; ++i) { - final double minX = vals.getDouble(0 + 6 * i); - final double minY = vals.getDouble(1 + 6 * i); - final double minZ = vals.getDouble(2 + 6 * i); - - final double boundXSize = vals.getDouble(3 + 6 * i) - minX; - final double boundYSize = vals.getDouble(4 + 6 * i) - minY; - final double boundZSize = vals.getDouble(5 + 6 * i) - minZ; - - final BoundingBox bb = new BoundingBox(boundXSize, boundYSize, boundZSize, new Vec(minX, minY, minZ)); - assert bb.minX() == minX; - assert bb.minY() == minY; - assert bb.minZ() == minZ; - boundingBoxes[i] = bb; - } - return boundingBoxes; } /** @@ -123,53 +57,48 @@ private static byte isFaceCovered(List covering) { return 1; } - static ShapeImpl parseBlockFromRegistry(String collision, String occlusion, Registry.BlockEntry blockEntry) { - BoundingBox[] collisionBoundingBoxes = parseRegistryBoundingBoxString(collision); - BoundingBox[] occlusionBoundingBoxes = blockEntry.occludes() ? parseRegistryBoundingBoxString(occlusion) : new BoundingBox[0]; - return new ShapeImpl(collisionBoundingBoxes, occlusionBoundingBoxes, blockEntry); - } - @Override public @NotNull Point relativeStart() { - return relativeStart; + return collisionData.relativeStart; } @Override public @NotNull Point relativeEnd() { - return relativeEnd; + return collisionData.relativeEnd; } @Override public boolean isOccluded(@NotNull Shape shape, @NotNull BlockFace face) { - final ShapeImpl shapeImpl = ((ShapeImpl) shape); - final boolean hasBlockOcclusion = (((blockOcclusion >> face.ordinal()) & 1) == 1); - final boolean hasBlockOcclusionOther = ((shapeImpl.blockOcclusion >> face.getOppositeFace().ordinal()) & 1) == 1; + final LightData lightData = this.lightData; + final LightData otherLightData = ((ShapeImpl) shape).lightData; + final boolean hasBlockOcclusion = (((lightData.blockOcclusion >> face.ordinal()) & 1) == 1); + final boolean hasBlockOcclusionOther = ((otherLightData.blockOcclusion >> face.getOppositeFace().ordinal()) & 1) == 1; - if (blockEntry.lightEmission() > 0) return hasBlockOcclusionOther; + if (lightData.lightEmission > 0) return hasBlockOcclusionOther; // If either face is full, return true if (hasBlockOcclusion || hasBlockOcclusionOther) return true; - final boolean hasAirOcclusion = (((airOcclusion >> face.ordinal()) & 1) == 1); - final boolean hasAirOcclusionOther = ((shapeImpl.airOcclusion >> face.getOppositeFace().ordinal()) & 1) == 1; + final boolean hasAirOcclusion = (((lightData.airOcclusion >> face.ordinal()) & 1) == 1); + final boolean hasAirOcclusionOther = ((otherLightData.airOcclusion >> face.getOppositeFace().ordinal()) & 1) == 1; // If a single face is air, return false if (hasAirOcclusion || hasAirOcclusionOther) return false; // Comparing two partial faces. Computation needed - List allRectangles = computeOcclusionSet(face.getOppositeFace(), shapeImpl.occlusionBoundingBoxes); - allRectangles.addAll(computeOcclusionSet(face, occlusionBoundingBoxes)); + List allRectangles = computeOcclusionSet(face.getOppositeFace(), otherLightData.occlusionBoundingBoxes); + allRectangles.addAll(computeOcclusionSet(face, lightData.occlusionBoundingBoxes)); return isFaceCovered(allRectangles) == 2; } @Override public boolean isFaceFull(@NotNull BlockFace face) { - return (((fullFaces >> face.ordinal()) & 1) == 1); + return (((collisionData.fullFaces >> face.ordinal()) & 1) == 1); } @Override public boolean intersectBox(@NotNull Point position, @NotNull BoundingBox boundingBox) { - for (BoundingBox blockSection : collisionBoundingBoxes) { + for (BoundingBox blockSection : collisionData.collisionBoundingBoxes) { if (boundingBox.intersectBox(position, blockSection)) return true; } return false; @@ -179,7 +108,7 @@ public boolean intersectBox(@NotNull Point position, @NotNull BoundingBox boundi public boolean intersectBoxSwept(@NotNull Point rayStart, @NotNull Point rayDirection, @NotNull Point shapePos, @NotNull BoundingBox moving, @NotNull SweepResult finalResult) { boolean hitBlock = false; - for (BoundingBox blockSection : collisionBoundingBoxes) { + for (BoundingBox blockSection : collisionData.collisionBoundingBoxes) { // Update final result if the temp result collision is sooner than the current final result if (RayUtils.BoundingBoxIntersectionCheck(moving, rayStart, rayDirection, blockSection, shapePos, finalResult)) { finalResult.collidedPositionX = rayStart.x() + rayDirection.x() * finalResult.res; @@ -195,12 +124,6 @@ public boolean intersectBoxSwept(@NotNull Point rayStart, @NotNull Point rayDire return hitBlock; } - public Block block() { - Block block = this.block; - if (block == null) this.block = block = Block.fromStateId((short) blockEntry.stateId()); - return block; - } - /** * Gets the collision bounding boxes for this block. There will be more than one bounds for more complex shapes e.g. * stairs. @@ -208,7 +131,7 @@ public Block block() { * @return the collision bounding boxes for this block */ public @NotNull @Unmodifiable List collisionBoundingBoxes() { - return collisionBoundingBoxes; + return collisionData.collisionBoundingBoxes; } /** @@ -217,7 +140,91 @@ public Block block() { * @return the occlusion bounding boxes for this block */ public @NotNull @Unmodifiable List occlusionBoundingBoxes() { - return occlusionBoundingBoxes; + return lightData.occlusionBoundingBoxes; + } + + static final Map SHAPES = new ConcurrentHashMap<>(); + + static ShapeImpl parseBlockFromRegistry(String collision, String occlusion, boolean occludes, int lightEmission) { + BoundingBox[] collisionBoundingBoxes = parseRegistryBoundingBoxString(collision); + BoundingBox[] occlusionBoundingBoxes = occludes ? parseRegistryBoundingBoxString(occlusion) : new BoundingBox[0]; + final CollisionData collisionData = collisionData(List.of(collisionBoundingBoxes)); + final LightData lightData = lightData(List.of(occlusionBoundingBoxes), lightEmission); + final ShapeImpl shape = new ShapeImpl(collisionData, lightData); + return SHAPES.computeIfAbsent(shape, k -> k); + } + + private static BoundingBox[] parseRegistryBoundingBoxString(String str) { + final Matcher matcher = PATTERN.matcher(str); + DoubleList vals = new DoubleArrayList(); + while (matcher.find()) { + double newVal = Double.parseDouble(matcher.group()); + vals.add(newVal); + } + final int count = vals.size() / 6; + BoundingBox[] boundingBoxes = new BoundingBox[count]; + for (int i = 0; i < count; ++i) { + final double minX = vals.getDouble(0 + 6 * i); + final double minY = vals.getDouble(1 + 6 * i); + final double minZ = vals.getDouble(2 + 6 * i); + + final double boundXSize = vals.getDouble(3 + 6 * i) - minX; + final double boundYSize = vals.getDouble(4 + 6 * i) - minY; + final double boundZSize = vals.getDouble(5 + 6 * i) - minZ; + + final Vec min = new Vec(minX, minY, minZ); + final Vec max = new Vec(minX + boundXSize, minY + boundYSize, minZ + boundZSize); + final BoundingBox bb = new BoundingBox(min, max); + assert bb.minX() == minX; + assert bb.minY() == minY; + assert bb.minZ() == minZ; + boundingBoxes[i] = bb; + } + return boundingBoxes; + } + + private static CollisionData collisionData(List collisionBoundingBoxes) { + // Find bounds of collision + Vec relativeStart; + Vec relativeEnd; + if (!collisionBoundingBoxes.isEmpty()) { + double minX = 1, minY = 1, minZ = 1; + double maxX = 0, maxY = 0, maxZ = 0; + for (BoundingBox blockSection : collisionBoundingBoxes) { + // Min + if (blockSection.minX() < minX) minX = blockSection.minX(); + if (blockSection.minY() < minY) minY = blockSection.minY(); + if (blockSection.minZ() < minZ) minZ = blockSection.minZ(); + // Max + if (blockSection.maxX() > maxX) maxX = blockSection.maxX(); + if (blockSection.maxY() > maxY) maxY = blockSection.maxY(); + if (blockSection.maxZ() > maxZ) maxZ = blockSection.maxZ(); + } + relativeStart = new Vec(minX, minY, minZ); + relativeEnd = new Vec(maxX, maxY, maxZ); + } else { + relativeStart = Vec.ZERO; + relativeEnd = Vec.ZERO; + } + + byte fullCollisionFaces = 0; + for (BlockFace f : BlockFace.values()) { + final byte res = isFaceCovered(computeOcclusionSet(f, collisionBoundingBoxes)); + fullCollisionFaces |= ((res == 2) ? 0b1 : 0b0) << (byte) f.ordinal(); + } + + return new CollisionData(collisionBoundingBoxes, relativeStart, relativeEnd, fullCollisionFaces); + } + + private static LightData lightData(List occlusionBoundingBoxes, int lightEmission) { + byte fullFaces = 0; + byte airFaces = 0; + for (BlockFace f : BlockFace.values()) { + final byte res = isFaceCovered(computeOcclusionSet(f, occlusionBoundingBoxes)); + fullFaces |= ((res == 2) ? 0b1 : 0b0) << (byte) f.ordinal(); + airFaces |= ((res == 0) ? 0b1 : 0b0) << (byte) f.ordinal(); + } + return new LightData(occlusionBoundingBoxes, fullFaces, airFaces, lightEmission); } private static @NotNull List computeOcclusionSet(BlockFace face, List boundingBoxes) { @@ -289,5 +296,6 @@ private static Rectangle clipRectangle(Rectangle covering, Rectangle toCover) { return new Rectangle(x1, y1, x2, y2); } - private record Rectangle(double x1, double y1, double x2, double y2) { } + private record Rectangle(double x1, double y1, double x2, double y2) { + } } From a3dec124aaf20a0f4dadf0df7caeaa0aee4ff90a Mon Sep 17 00:00:00 2001 From: TheMode Date: Wed, 4 Sep 2024 00:52:01 +0200 Subject: [PATCH 11/31] Use a single int for all block states (#2373) --- .../server/instance/block/BlockImpl.java | 112 +++++++++--------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/src/main/java/net/minestom/server/instance/block/BlockImpl.java b/src/main/java/net/minestom/server/instance/block/BlockImpl.java index 7395633fdad..672dd6c7adc 100644 --- a/src/main/java/net/minestom/server/instance/block/BlockImpl.java +++ b/src/main/java/net/minestom/server/instance/block/BlockImpl.java @@ -1,13 +1,11 @@ package net.minestom.server.instance.block; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; +import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; import it.unimi.dsi.fastutil.objects.Object2ObjectMaps; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.minestom.server.registry.Registry; import net.minestom.server.tag.Tag; -import net.minestom.server.utils.ArrayUtils; import net.minestom.server.utils.block.BlockUtils; import net.minestom.server.utils.collection.MergedMap; import net.minestom.server.utils.collection.ObjectArray; @@ -16,20 +14,30 @@ import org.jetbrains.annotations.UnknownNullability; import org.jetbrains.annotations.Unmodifiable; -import java.time.Duration; -import java.util.*; -import java.util.function.Function; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; record BlockImpl(@NotNull Registry.BlockEntry registry, - byte @NotNull [] propertiesArray, + int propertiesArray, @Nullable CompoundBinaryTag nbt, @Nullable BlockHandler handler) implements Block { + /** + * Number of bits used to store the index of a property value. + *

+ * Block states are all stored within a single number. + */ + private static final int BITS_PER_INDEX = 4; + + private static final int MAX_STATES = Integer.SIZE / BITS_PER_INDEX; + // Block state -> block object private static final ObjectArray BLOCK_STATE_MAP = ObjectArray.singleThread(); // Block id -> valid property keys (order is important for lookup) private static final ObjectArray PROPERTIES_TYPE = ObjectArray.singleThread(); - // Block id -> Map - private static final ObjectArray> POSSIBLE_STATES = ObjectArray.singleThread(); + // Block id -> Map + private static final ObjectArray> POSSIBLE_STATES = ObjectArray.singleThread(); private static final Registry.Container CONTAINER = Registry.createStaticContainer(Registry.Resource.BLOCKS, (namespace, properties) -> { final int blockId = properties.getInt("id"); @@ -41,6 +49,9 @@ record BlockImpl(@NotNull Registry.BlockEntry registry, Registry.Properties stateProperties = properties.section("properties"); if (stateProperties != null) { final int stateCount = stateProperties.size(); + if (stateCount > MAX_STATES) { + throw new IllegalStateException("Too many properties for block " + namespace); + } propertyTypes = new PropertyType[stateCount]; int i = 0; for (var entry : stateProperties) { @@ -57,7 +68,7 @@ record BlockImpl(@NotNull Registry.BlockEntry registry, // Retrieve block states { final int propertiesCount = stateObject.size(); - PropertiesHolder[] propertiesKeys = new PropertiesHolder[propertiesCount]; + int[] propertiesKeys = new int[propertiesCount]; BlockImpl[] blocksValues = new BlockImpl[propertiesCount]; int propertiesOffset = 0; for (var stateEntry : stateObject) { @@ -65,30 +76,26 @@ record BlockImpl(@NotNull Registry.BlockEntry registry, final var stateOverride = (Map) stateEntry.getValue(); final var propertyMap = BlockUtils.parseProperties(query); assert propertyTypes.length == propertyMap.size(); - byte[] propertiesArray = new byte[propertyTypes.length]; - for (var entry : propertyMap.entrySet()) { + int propertiesValue = 0; + for (Map.Entry entry : propertyMap.entrySet()) { final byte keyIndex = findKeyIndex(propertyTypes, entry.getKey(), null); final byte valueIndex = findValueIndex(propertyTypes[keyIndex], entry.getValue(), null); - propertiesArray[keyIndex] = valueIndex; + propertiesValue = updateIndex(propertiesValue, keyIndex, valueIndex); } var mainProperties = Registry.Properties.fromMap(new MergedMap<>(stateOverride, properties.asMap())); final BlockImpl block = new BlockImpl(Registry.block(namespace, mainProperties), - propertiesArray, null, null); + propertiesValue, null, null); BLOCK_STATE_MAP.set(block.stateId(), block); - propertiesKeys[propertiesOffset] = new PropertiesHolder(propertiesArray); + propertiesKeys[propertiesOffset] = propertiesValue; blocksValues[propertiesOffset++] = block; } - POSSIBLE_STATES.set(blockId, ArrayUtils.toMap(propertiesKeys, blocksValues, propertiesOffset)); + POSSIBLE_STATES.set(blockId, new Int2ObjectArrayMap<>(propertiesKeys, blocksValues, propertiesOffset)); } // Register default state final int defaultState = properties.getInt("defaultStateId"); return getState(defaultState); }); - private static final Cache NBT_CACHE = Caffeine.newBuilder() - .expireAfterWrite(Duration.ofMinutes(5)) - .weakValues() - .build(); static { PROPERTIES_TYPE.trim(); @@ -122,9 +129,8 @@ static Collection values() { assert propertyTypes != null; final byte keyIndex = findKeyIndex(propertyTypes, property, this); final byte valueIndex = findValueIndex(propertyTypes[keyIndex], value, this); - var properties = this.propertiesArray.clone(); - properties[keyIndex] = valueIndex; - return compute(properties); + final int updatedProperties = updateIndex(propertiesArray, keyIndex, valueIndex); + return compute(updatedProperties); } @Override @@ -132,13 +138,13 @@ static Collection values() { if (properties.isEmpty()) return this; final PropertyType[] propertyTypes = PROPERTIES_TYPE.get(id()); assert propertyTypes != null; - byte[] result = this.propertiesArray.clone(); - for (var entry : properties.entrySet()) { + int updatedProperties = this.propertiesArray; + for (Map.Entry entry : properties.entrySet()) { final byte keyIndex = findKeyIndex(propertyTypes, entry.getKey(), this); final byte valueIndex = findValueIndex(propertyTypes[keyIndex], entry.getValue(), this); - result[keyIndex] = valueIndex; + updatedProperties = updateIndex(updatedProperties, keyIndex, valueIndex); } - return compute(result); + return compute(updatedProperties); } @Override @@ -146,8 +152,8 @@ static Collection values() { var builder = CompoundBinaryTag.builder(); if (nbt != null) builder.put(nbt); tag.write(builder, value); - var temporaryNbt = builder.build(); - final var finalNbt = temporaryNbt.size() > 0 ? NBT_CACHE.get(temporaryNbt, Function.identity()) : null; + final CompoundBinaryTag temporaryNbt = builder.build(); + final CompoundBinaryTag finalNbt = temporaryNbt.size() > 0 ? temporaryNbt : null; return new BlockImpl(registry, propertiesArray, finalNbt, handler); } @@ -170,9 +176,10 @@ static Collection values() { String[] keys = new String[length]; String[] values = new String[length]; for (int i = 0; i < length; i++) { - var property = propertyTypes[i]; + PropertyType property = propertyTypes[i]; keys[i] = property.key(); - values[i] = property.values().get(propertiesArray[i]); + final int index = extractIndex(propertiesArray, i); + values[i] = property.values().get(index); } return Object2ObjectMaps.unmodifiable(new Object2ObjectArrayMap<>(keys, values, length)); } @@ -192,7 +199,7 @@ static Collection values() { return tag.read(Objects.requireNonNullElse(nbt, CompoundBinaryTag.empty())); } - private Map possibleProperties() { + private Int2ObjectArrayMap possibleProperties() { return POSSIBLE_STATES.get(id()); } @@ -213,11 +220,14 @@ public int hashCode() { return Objects.hash(stateId(), nbt, handler); } - private Block compute(byte[] properties) { - if (Arrays.equals(propertiesArray, properties)) return this; - final BlockImpl block = possibleProperties().get(new PropertiesHolder(properties)); + private Block compute(int updatedProperties) { + if (updatedProperties == this.propertiesArray) return this; + final BlockImpl block = possibleProperties().get(updatedProperties); assert block != null; - return nbt == null && handler == null ? block : new BlockImpl(block.registry(), block.propertiesArray, nbt, handler); + // Reuse the same block instance if possible + if (nbt == null && handler == null) return block; + // Otherwise copy with the nbt and handler + return new BlockImpl(block.registry(), block.propertiesArray, nbt, handler); } private static byte findKeyIndex(PropertyType[] properties, String key, BlockImpl block) { @@ -245,25 +255,17 @@ private static byte findValueIndex(PropertyType propertyType, String value, Bloc private record PropertyType(String key, List values) { } - private static final class PropertiesHolder { - private final byte[] properties; - private final int hashCode; - - public PropertiesHolder(byte[] properties) { - this.properties = properties; - this.hashCode = Arrays.hashCode(properties); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof PropertiesHolder that)) return false; - return Arrays.equals(properties, that.properties); - } + static int updateIndex(int value, int index, byte newValue) { + final int position = index * BITS_PER_INDEX; + final int mask = (1 << BITS_PER_INDEX) - 1; + value &= ~(mask << position); // Clear the bits at the specified position + value |= (newValue & mask) << position; // Set the new bits + return value; + } - @Override - public int hashCode() { - return hashCode; - } + static int extractIndex(int value, int index) { + final int position = index * BITS_PER_INDEX; + final int mask = (1 << BITS_PER_INDEX) - 1; + return ((value >> position) & mask); } } From 5ec42c6ba94fb732de1cded362a4a022339b03ad Mon Sep 17 00:00:00 2001 From: TheMode Date: Wed, 4 Sep 2024 01:05:43 +0200 Subject: [PATCH 12/31] Light cleanup (#2372) --- .../server/instance/LightingChunk.java | 75 +++-- .../net/minestom/server/instance/Section.java | 35 ++- .../server/instance/heightmap/Heightmap.java | 7 +- .../server/instance/light/BlockLight.java | 217 +++++---------- .../minestom/server/instance/light/Light.java | 87 +++--- .../server/instance/light/LightCompute.java | 113 ++++++-- .../server/instance/light/SkyLight.java | 260 ++++++------------ .../server/instance/light/BlockLightTest.java | 36 +-- 8 files changed, 360 insertions(+), 470 deletions(-) diff --git a/src/main/java/net/minestom/server/instance/LightingChunk.java b/src/main/java/net/minestom/server/instance/LightingChunk.java index c95dfc761d3..eefa768500e 100644 --- a/src/main/java/net/minestom/server/instance/LightingChunk.java +++ b/src/main/java/net/minestom/server/instance/LightingChunk.java @@ -9,6 +9,7 @@ import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.instance.heightmap.Heightmap; import net.minestom.server.instance.light.Light; +import net.minestom.server.instance.palette.Palette; import net.minestom.server.network.packet.server.CachedPacket; import net.minestom.server.network.packet.server.play.data.LightData; import net.minestom.server.utils.NamespaceID; @@ -24,12 +25,12 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; -import static net.minestom.server.instance.light.LightCompute.emptyContent; +import static net.minestom.server.instance.light.LightCompute.EMPTY_CONTENT; /** * A chunk which supports lighting computation. *

- * This chunk is used to compute the light data for each block. + * This chunk is used to compute the light data for each block. *

*/ public class LightingChunk extends DynamicChunk { @@ -127,7 +128,8 @@ public void invalidateNeighborsSection(int coordinate) { } for (int k = -1; k <= 1; k++) { - if (k + coordinate < neighborChunk.getMinSection() || k + coordinate >= neighborChunk.getMaxSection()) continue; + if (k + coordinate < neighborChunk.getMinSection() || k + coordinate >= neighborChunk.getMaxSection()) + continue; neighborChunk.getSection(k + coordinate).blockLight().invalidate(); neighborChunk.getSection(k + coordinate).skyLight().invalidate(); } @@ -297,7 +299,7 @@ protected LightData createLightData(boolean requiredFullChunk) { if ((wasUpdatedSky) && this.instance.getCachedDimensionType().hasSkylight() && sectionMinY <= (highestNeighborBlock + 16)) { final byte[] skyLight = section.skyLight().array(); - if (skyLight.length != 0 && skyLight != emptyContent) { + if (skyLight.length != 0 && skyLight != EMPTY_CONTENT) { skyLights.add(skyLight); skyMask.set(index); } else { @@ -308,7 +310,7 @@ protected LightData createLightData(boolean requiredFullChunk) { if (wasUpdatedBlock) { final byte[] blockLight = section.blockLight().array(); - if (blockLight.length != 0 && blockLight != emptyContent) { + if (blockLight.length != 0 && blockLight != EMPTY_CONTENT) { blockLights.add(blockLight); blockMask.set(index); } else { @@ -350,28 +352,54 @@ private static Set flushQueue(Instance instance, Set queue, LightT Set responseChunks = ConcurrentHashMap.newKeySet(); List> tasks = new ArrayList<>(); + Light.LightLookup lightLookup = (x, y, z) -> { + Chunk chunk = instance.getChunk(x, z); + if (chunk == null) return null; + if (!(chunk instanceof LightingChunk lighting)) return null; + if (y - lighting.getMinSection() < 0 || y - lighting.getMaxSection() >= 0) return null; + final Section section = lighting.getSection(y); + return switch (type) { + case BLOCK -> section.blockLight(); + case SKY -> section.skyLight(); + }; + }; + + Light.PaletteLookup paletteLookup = (x, y, z) -> { + Chunk chunk = instance.getChunk(x, z); + if (chunk == null) return null; + if (!(chunk instanceof LightingChunk lighting)) return null; + if (y - lighting.getMinSection() < 0 || y - lighting.getMaxSection() >= 0) return null; + return chunk.getSection(y).blockPalette(); + }; + for (Point point : queue) { Chunk chunk = instance.getChunk(point.blockX(), point.blockZ()); - if (chunk == null) continue; + if (!(chunk instanceof LightingChunk lightingChunk)) continue; - var section = chunk.getSection(point.blockY()); + Section section = chunk.getSection(point.blockY()); responseChunks.add(chunk); - Light light = switch(type) { + Light light = switch (type) { case BLOCK -> section.blockLight(); case SKY -> section.skyLight(); }; + final Palette blockPalette = section.blockPalette(); CompletableFuture task = CompletableFuture.runAsync(() -> { - switch (queueType) { - case INTERNAL -> light.calculateInternal(instance, chunk.getChunkX(), point.blockY(), chunk.getChunkZ()); - case EXTERNAL -> light.calculateExternal(instance, chunk, point.blockY()); - } + final Set toAdd = switch (queueType) { + case INTERNAL -> light.calculateInternal(blockPalette, + chunk.getChunkX(), point.blockY(), chunk.getChunkZ(), + lightingChunk.getOcclusionMap(), chunk.instance.getCachedDimensionType().maxY(), + lightLookup); + case EXTERNAL -> light.calculateExternal(blockPalette, + Light.getNeighbors(chunk, point.blockY()), + lightLookup, paletteLookup); + }; sections.add(light); - var toAdd = light.flip(); - if (toAdd != null) newQueue.addAll(toAdd); + light.flip(); + newQueue.addAll(toAdd); }, pool); tasks.add(task); @@ -403,17 +431,14 @@ public static List relight(Instance instance, Collection chunks) { synchronized (instance) { for (Chunk chunk : chunks) { - if (chunk == null) continue; - if (chunk instanceof LightingChunk lighting) { - for (int section = chunk.minSection; section < chunk.maxSection; section++) { - chunk.getSection(section).blockLight().invalidate(); - chunk.getSection(section).skyLight().invalidate(); - - sections.add(new Vec(chunk.getChunkX(), section, chunk.getChunkZ())); - } - - lighting.invalidate(); + if (!(chunk instanceof LightingChunk lighting)) continue; + for (int sectionIndex = chunk.minSection; sectionIndex < chunk.maxSection; sectionIndex++) { + Section section = chunk.getSection(sectionIndex); + section.blockLight().invalidate(); + section.skyLight().invalidate(); + sections.add(new Vec(chunk.getChunkX(), sectionIndex, chunk.getChunkZ())); } + lighting.invalidate(); } // Expand the sections to include nearby sections @@ -540,4 +565,4 @@ private static Set relight(Instance instance, Set queue, LightType public boolean isLoaded() { return super.isLoaded() && doneInit; } -} \ No newline at end of file +} diff --git a/src/main/java/net/minestom/server/instance/Section.java b/src/main/java/net/minestom/server/instance/Section.java index e951eeb110e..da48e9eb1c3 100644 --- a/src/main/java/net/minestom/server/instance/Section.java +++ b/src/main/java/net/minestom/server/instance/Section.java @@ -7,8 +7,8 @@ import java.util.Arrays; -import static net.minestom.server.instance.light.LightCompute.contentFullyLit; -import static net.minestom.server.instance.light.LightCompute.emptyContent; +import static net.minestom.server.instance.light.LightCompute.CONTENT_FULLY_LIT; +import static net.minestom.server.instance.light.LightCompute.EMPTY_CONTENT; import static net.minestom.server.network.NetworkBuffer.SHORT; public final class Section implements NetworkBuffer.Writer { @@ -17,13 +17,6 @@ public final class Section implements NetworkBuffer.Writer { private final Light skyLight; private final Light blockLight; - private Section(Palette blockPalette, Palette biomePalette) { - this.blockPalette = blockPalette; - this.biomePalette = biomePalette; - this.skyLight = Light.sky(blockPalette); - this.blockLight = Light.block(blockPalette); - } - private Section(Palette blockPalette, Palette biomePalette, Light skyLight, Light blockLight) { this.blockPalette = blockPalette; this.biomePalette = biomePalette; @@ -31,6 +24,10 @@ private Section(Palette blockPalette, Palette biomePalette, Light skyLight, Ligh this.blockLight = blockLight; } + private Section(Palette blockPalette, Palette biomePalette) { + this(blockPalette, biomePalette, Light.sky(), Light.block()); + } + public Section() { this(Palette.blocks(), Palette.biomes()); } @@ -50,11 +47,11 @@ public void clear() { @Override public @NotNull Section clone() { - final Light skyLight = Light.sky(blockPalette); - final Light blockLight = Light.block(blockPalette); + final Light skyLight = Light.sky(); + final Light blockLight = Light.block(); - setSkyLight(this.skyLight.array()); - setBlockLight(this.blockLight.array()); + skyLight.set(this.skyLight.array()); + blockLight.set(this.blockLight.array()); return new Section(this.blockPalette.clone(), this.biomePalette.clone(), skyLight, blockLight); } @@ -67,16 +64,16 @@ public void write(@NotNull NetworkBuffer writer) { } public void setSkyLight(byte[] copyArray) { - if (copyArray == null || copyArray.length == 0) this.skyLight.set(emptyContent); - else if (Arrays.equals(copyArray, emptyContent)) this.skyLight.set(emptyContent); - else if (Arrays.equals(copyArray, contentFullyLit)) this.skyLight.set(contentFullyLit); + if (copyArray == null || copyArray.length == 0) this.skyLight.set(EMPTY_CONTENT); + else if (Arrays.equals(copyArray, EMPTY_CONTENT)) this.skyLight.set(EMPTY_CONTENT); + else if (Arrays.equals(copyArray, CONTENT_FULLY_LIT)) this.skyLight.set(CONTENT_FULLY_LIT); else this.skyLight.set(copyArray); } public void setBlockLight(byte[] copyArray) { - if (copyArray == null || copyArray.length == 0) this.blockLight.set(emptyContent); - else if (Arrays.equals(copyArray, emptyContent)) this.blockLight.set(emptyContent); - else if (Arrays.equals(copyArray, contentFullyLit)) this.blockLight.set(contentFullyLit); + if (copyArray == null || copyArray.length == 0) this.blockLight.set(EMPTY_CONTENT); + else if (Arrays.equals(copyArray, EMPTY_CONTENT)) this.blockLight.set(EMPTY_CONTENT); + else if (Arrays.equals(copyArray, CONTENT_FULLY_LIT)) this.blockLight.set(CONTENT_FULLY_LIT); else this.blockLight.set(copyArray); } diff --git a/src/main/java/net/minestom/server/instance/heightmap/Heightmap.java b/src/main/java/net/minestom/server/instance/heightmap/Heightmap.java index 33d5e730cc7..5b93d50704f 100644 --- a/src/main/java/net/minestom/server/instance/heightmap/Heightmap.java +++ b/src/main/java/net/minestom/server/instance/heightmap/Heightmap.java @@ -20,6 +20,7 @@ public Heightmap(Chunk chunk) { } protected abstract boolean checkBlock(@NotNull Block block); + public abstract String NBTName(); public void refresh(int x, int y, int z, Block block) { @@ -75,7 +76,7 @@ public void loadFrom(long[] data) { for (int i = 0; i < heights.length; i++) { final int indexInContainer = i % entriesPerLong; - heights[i] = (short) ((int)(data[containerIndex] >> (indexInContainer * bitsPerEntry)) & entryMask); + heights[i] = (short) ((int) (data[containerIndex] >> (indexInContainer * bitsPerEntry)) & entryMask); if (indexInContainer == maxPossibleIndexInContainer) containerIndex++; } @@ -109,8 +110,8 @@ public static int getHighestBlockSection(Chunk chunk) { /** * Creates compressed longs array from uncompressed heights array. * - * @param heights array of heights. Note that for this method it doesn't matter what size this array will be. - * But to get correct heights, array must be 256 elements long, and at index `i` must be height of (z=i/16, x=i%16). + * @param heights array of heights. Note that for this method it doesn't matter what size this array will be. + * But to get correct heights, array must be 256 elements long, and at index `i` must be height of (z=i/16, x=i%16). * @param bitsPerEntry bits that each entry from height will take in `long` container. * @return array of encoded heights. */ diff --git a/src/main/java/net/minestom/server/instance/light/BlockLight.java b/src/main/java/net/minestom/server/instance/light/BlockLight.java index a1cc22f64ad..244940fc430 100644 --- a/src/main/java/net/minestom/server/instance/light/BlockLight.java +++ b/src/main/java/net/minestom/server/instance/light/BlockLight.java @@ -3,15 +3,11 @@ import it.unimi.dsi.fastutil.shorts.ShortArrayFIFOQueue; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; -import net.minestom.server.instance.Chunk; -import net.minestom.server.instance.Instance; -import net.minestom.server.instance.Section; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockFace; import net.minestom.server.instance.palette.Palette; import org.jetbrains.annotations.ApiStatus; -import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @@ -19,31 +15,18 @@ import static net.minestom.server.instance.light.LightCompute.*; final class BlockLight implements Light { - private final Palette blockPalette; - private byte[] content; private byte[] contentPropagation; private byte[] contentPropagationSwap; - private final AtomicBoolean isValidBorders = new AtomicBoolean(true); + private volatile boolean isValidBorders = true; private final AtomicBoolean needsSend = new AtomicBoolean(false); - private Set toUpdateSet = new HashSet<>(); - private final Section[] neighborSections = new Section[BlockFace.values().length]; - - BlockLight(Palette blockPalette) { - this.blockPalette = blockPalette; - } - @Override - public Set flip() { + public void flip() { if (this.contentPropagationSwap != null) this.contentPropagation = this.contentPropagationSwap; - this.contentPropagationSwap = null; - - if (toUpdateSet == null) return Set.of(); - return toUpdateSet; } static ShortArrayFIFOQueue buildInternalQueue(Palette blockPalette) { @@ -62,29 +45,19 @@ static ShortArrayFIFOQueue buildInternalQueue(Palette blockPalette) { return lightSources; } - private static Block getBlock(Palette palette, int x, int y, int z) { - return Block.fromStateId((short)palette.get(x, y, z)); - } - - private ShortArrayFIFOQueue buildExternalQueue(Instance instance, Palette blockPalette, Point[] neighbors, byte[] content) { + private ShortArrayFIFOQueue buildExternalQueue(Palette blockPalette, + Point[] neighbors, byte[] content, + LightLookup lightLookup, + PaletteLookup paletteLookup) { ShortArrayFIFOQueue lightSources = new ShortArrayFIFOQueue(); for (int i = 0; i < neighbors.length; i++) { - var face = BlockFace.values()[i]; + final BlockFace face = BlockFace.values()[i]; Point neighborSection = neighbors[i]; if (neighborSection == null) continue; - Section otherSection = neighborSections[face.ordinal()]; - - if (otherSection == null) { - Chunk chunk = instance.getChunk(neighborSection.blockX(), neighborSection.blockZ()); - if (chunk == null) continue; - - otherSection = chunk.getSection(neighborSection.blockY()); - neighborSections[face.ordinal()] = otherSection; - } - - Light otherLight = otherSection.blockLight(); + Palette otherPalette = paletteLookup.palette(neighborSection.blockX(), neighborSection.blockY(), neighborSection.blockZ()); + Light otherLight = lightLookup.light(neighborSection.blockX(), neighborSection.blockY(), neighborSection.blockZ()); for (int bx = 0; bx < 16; bx++) { for (int by = 0; by < 16; by++) { @@ -94,10 +67,10 @@ private ShortArrayFIFOQueue buildExternalQueue(Instance instance, Palette blockP }; final byte lightEmission = (byte) Math.max(switch (face) { - case NORTH, SOUTH -> (byte) otherLight.getLevel(bx, by, 15 - k); - case WEST, EAST -> (byte) otherLight.getLevel(15 - k, bx, by); - default -> (byte) otherLight.getLevel(bx, 15 - k, by); - } - 1, 0); + case NORTH, SOUTH -> (byte) otherLight.getLevel(bx, by, 15 - k); + case WEST, EAST -> (byte) otherLight.getLevel(15 - k, bx, by); + default -> (byte) otherLight.getLevel(bx, 15 - k, by); + } - 1, 0); final int posTo = switch (face) { case NORTH, SOUTH -> bx | (k << 4) | (by << 8); @@ -110,16 +83,16 @@ private ShortArrayFIFOQueue buildExternalQueue(Instance instance, Palette blockP if (lightEmission <= internalEmission) continue; } - final Block blockTo = switch(face) { + final Block blockTo = switch (face) { case NORTH, SOUTH -> getBlock(blockPalette, bx, by, k); case WEST, EAST -> getBlock(blockPalette, k, bx, by); default -> getBlock(blockPalette, bx, k, by); }; final Block blockFrom = (switch (face) { - case NORTH, SOUTH -> getBlock(otherSection.blockPalette(), bx, by, 15 - k); - case WEST, EAST -> getBlock(otherSection.blockPalette(), 15 - k, bx, by); - default -> getBlock(otherSection.blockPalette(), bx, 15 - k, by); + case NORTH, SOUTH -> getBlock(otherPalette, bx, by, 15 - k); + case WEST, EAST -> getBlock(otherPalette, 15 - k, bx, by); + default -> getBlock(otherPalette, bx, 15 - k, by); }); if (blockTo == null && blockFrom != null) { @@ -144,57 +117,16 @@ private ShortArrayFIFOQueue buildExternalQueue(Instance instance, Palette blockP return lightSources; } - @Override - public Light calculateInternal(Instance instance, int chunkX, int sectionY, int chunkZ) { - this.isValidBorders.set(true); - - Chunk chunk = instance.getChunk(chunkX, chunkZ); - if (chunk == null) { - this.toUpdateSet = Set.of(); - return this; - } - - Set toUpdate = new HashSet<>(); - - // Update single section with base lighting changes - ShortArrayFIFOQueue queue = buildInternalQueue(blockPalette); - - Result result = LightCompute.compute(blockPalette, queue); - this.content = result.light(); - - // Propagate changes to neighbors and self - for (int i = -1; i <= 1; i++) { - for (int j = -1; j <= 1; j++) { - Chunk neighborChunk = instance.getChunk(chunkX + i, chunkZ + j); - if (neighborChunk == null) continue; - - for (int k = -1; k <= 1; k++) { - Vec neighborPos = new Vec(chunkX + i, sectionY + k, chunkZ + j); - - if (neighborPos.blockY() >= neighborChunk.getMinSection() && neighborPos.blockY() < neighborChunk.getMaxSection()) { - if (neighborChunk.getSection(neighborPos.blockY()).blockLight() instanceof BlockLight blockLight) - blockLight.contentPropagation = null; - } - } - } - } - - toUpdate.add(new Vec(chunk.getChunkX(), sectionY, chunk.getChunkZ())); - this.toUpdateSet = toUpdate; - - return this; - } - @Override public void invalidate() { this.needsSend.set(true); - this.isValidBorders.set(false); + this.isValidBorders = false; this.contentPropagation = null; } @Override public boolean requiresUpdate() { - return !isValidBorders.get(); + return !isValidBorders; } @Override @@ -202,7 +134,7 @@ public boolean requiresUpdate() { public void set(byte[] copyArray) { this.content = copyArray.clone(); this.contentPropagation = this.content; - this.isValidBorders.set(true); + this.isValidBorders = true; this.needsSend.set(true); } @@ -215,76 +147,67 @@ public boolean requiresSend() { public byte[] array() { if (content == null) return new byte[0]; if (contentPropagation == null) return content; - var res = bake(contentPropagation, content); - if (res == emptyContent) return new byte[0]; + var res = LightCompute.bake(contentPropagation, content); + if (res == EMPTY_CONTENT) return new byte[0]; return res; } @Override - public Light calculateExternal(Instance instance, Chunk chunk, int sectionY) { - if (!isValidBorders.get()) { - this.toUpdateSet = Set.of(); - return this; - } - - Point[] neighbors = Light.getNeighbors(chunk, sectionY); - - ShortArrayFIFOQueue queue = buildExternalQueue(instance, blockPalette, neighbors, content); - LightCompute.Result result = LightCompute.compute(blockPalette, queue); - - byte[] contentPropagationTemp = result.light(); - - this.contentPropagationSwap = bake(contentPropagationSwap, contentPropagationTemp); + public int getLevel(int x, int y, int z) { + if (content == null) return 0; + int index = x | (z << 4) | (y << 8); + if (contentPropagation == null) return LightCompute.getLight(content, index); + return Math.max(LightCompute.getLight(contentPropagation, index), LightCompute.getLight(content, index)); + } + @Override + public Set calculateInternal(Palette blockPalette, + int chunkX, int chunkY, int chunkZ, + int[] heightmap, int maxY, + LightLookup lightLookup) { + this.isValidBorders = true; + // Update single section with base lighting changes + ShortArrayFIFOQueue queue = buildInternalQueue(blockPalette); + this.content = LightCompute.compute(blockPalette, queue); + // Propagate changes to neighbors and self Set toUpdate = new HashSet<>(); + for (int i = -1; i <= 1; i++) { + for (int j = -1; j <= 1; j++) { + for (int k = -1; k <= 1; k++) { + final int neighborX = chunkX + i; + final int neighborY = chunkY + j; + final int neighborZ = chunkZ + k; + if (!(lightLookup.light(neighborX, neighborY, neighborZ) instanceof BlockLight blockLight)) + continue; + blockLight.contentPropagation = null; + } + } + } + toUpdate.add(new Vec(chunkX, chunkY, chunkZ)); + return toUpdate; + } + @Override + public Set calculateExternal(Palette blockPalette, + Point[] neighbors, + LightLookup lightLookup, + PaletteLookup paletteLookup) { + if (!isValidBorders) { + return Set.of(); + } + ShortArrayFIFOQueue queue = buildExternalQueue(blockPalette, neighbors, content, lightLookup, paletteLookup); + final byte[] contentPropagationTemp = LightCompute.compute(blockPalette, queue); + this.contentPropagationSwap = LightCompute.bake(contentPropagationSwap, contentPropagationTemp); // Propagate changes to neighbors and self + Set toUpdate = new HashSet<>(); for (int i = 0; i < neighbors.length; i++) { - var neighbor = neighbors[i]; + final Point neighbor = neighbors[i]; if (neighbor == null) continue; - var face = BlockFace.values()[i]; - - if (!Light.compareBorders(content, contentPropagation, contentPropagationTemp, face)) { + final BlockFace face = BlockFace.values()[i]; + if (!LightCompute.compareBorders(content, contentPropagation, contentPropagationTemp, face)) { toUpdate.add(neighbor); } } - - this.toUpdateSet = toUpdate; - return this; - } - - private byte[] bake(byte[] content1, byte[] content2) { - if (content1 == null && content2 == null) return emptyContent; - if (content1 == emptyContent && content2 == emptyContent) return emptyContent; - - if (content1 == null) return content2; - if (content2 == null) return content1; - - if (Arrays.equals(content1, emptyContent) && Arrays.equals(content2, emptyContent)) return emptyContent; - - byte[] lightMax = new byte[LIGHT_LENGTH]; - for (int i = 0; i < content1.length; i++) { - // Lower - byte l1 = (byte) (content1[i] & 0x0F); - byte l2 = (byte) (content2[i] & 0x0F); - - // Upper - byte u1 = (byte) ((content1[i] >> 4) & 0x0F); - byte u2 = (byte) ((content2[i] >> 4) & 0x0F); - - byte lower = (byte) Math.max(l1, l2); - byte upper = (byte) Math.max(u1, u2); - - lightMax[i] = (byte) (lower | (upper << 4)); - } - return lightMax; - } - - @Override - public int getLevel(int x, int y, int z) { - if (content == null) return 0; - int index = x | (z << 4) | (y << 8); - if (contentPropagation == null) return LightCompute.getLight(content, index); - return Math.max(LightCompute.getLight(contentPropagation, index), LightCompute.getLight(content, index)); + return toUpdate; } -} \ No newline at end of file +} diff --git a/src/main/java/net/minestom/server/instance/light/Light.java b/src/main/java/net/minestom/server/instance/light/Light.java index 9e119f5a9ca..45216c834a7 100644 --- a/src/main/java/net/minestom/server/instance/light/Light.java +++ b/src/main/java/net/minestom/server/instance/light/Light.java @@ -3,25 +3,20 @@ import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.Chunk; -import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.BlockFace; import net.minestom.server.instance.palette.Palette; import net.minestom.server.utils.Direction; import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; import java.util.Set; -import static net.minestom.server.instance.light.LightCompute.SECTION_SIZE; -import static net.minestom.server.instance.light.LightCompute.getLight; - public interface Light { - static Light sky(@NotNull Palette blockPalette) { - return new SkyLight(blockPalette); + static Light sky() { + return new SkyLight(); } - static Light block(@NotNull Palette blockPalette) { - return new BlockLight(blockPalette); + static Light block() { + return new BlockLight(); } boolean requiresSend(); @@ -29,75 +24,57 @@ static Light block(@NotNull Palette blockPalette) { @ApiStatus.Internal byte[] array(); - Set flip(); - - @ApiStatus.Internal - Light calculateExternal(Instance instance, Chunk chunk, int sectionY); + void flip(); int getLevel(int x, int y, int z); - @ApiStatus.Internal - Light calculateInternal(Instance instance, int chunkX, int chunkY, int chunkZ); - void invalidate(); boolean requiresUpdate(); void set(byte[] copyArray); + @ApiStatus.Internal + Set calculateInternal(Palette blockPalette, + int chunkX, int chunkY, int chunkZ, + int[] heightmap, int maxY, + LightLookup lightLookup); + + @ApiStatus.Internal + Set calculateExternal(Palette blockPalette, + Point[] neighbors, + LightLookup lightLookup, + PaletteLookup paletteLookup); + @ApiStatus.Internal static Point[] getNeighbors(Chunk chunk, int sectionY) { - int chunkX = chunk.getChunkX(); - int chunkZ = chunk.getChunkZ(); + final int chunkX = chunk.getChunkX(); + final int chunkZ = chunk.getChunkZ(); Point[] links = new Vec[BlockFace.values().length]; - for (BlockFace face : BlockFace.values()) { - Direction direction = face.toDirection(); - int x = chunkX + direction.normalX(); - int z = chunkZ + direction.normalZ(); - int y = sectionY + direction.normalY(); + final Direction direction = face.toDirection(); + final int x = chunkX + direction.normalX(); + final int z = chunkZ + direction.normalZ(); + final int y = sectionY + direction.normalY(); Chunk foundChunk = chunk.getInstance().getChunk(x, z); - if (foundChunk == null) continue; - if (y - foundChunk.getMinSection() > foundChunk.getMaxSection() || y - foundChunk.getMinSection() < 0) continue; + if (y - foundChunk.getMinSection() > foundChunk.getMaxSection() || y - foundChunk.getMinSection() < 0) + continue; links[face.ordinal()] = new Vec(foundChunk.getChunkX(), y, foundChunk.getChunkZ()); } - return links; } - @ApiStatus.Internal - static boolean compareBorders(byte[] content, byte[] contentPropagation, byte[] contentPropagationTemp, BlockFace face) { - if (content == null && contentPropagation == null && contentPropagationTemp == null) return true; - - final int k = switch (face) { - case WEST, BOTTOM, NORTH -> 0; - case EAST, TOP, SOUTH -> 15; - }; - - for (int bx = 0; bx < SECTION_SIZE; bx++) { - for (int by = 0; by < SECTION_SIZE; by++) { - final int posFrom = switch (face) { - case NORTH, SOUTH -> bx | (k << 4) | (by << 8); - case WEST, EAST -> k | (by << 4) | (bx << 8); - default -> bx | (by << 4) | (k << 8); - }; - - int valueFrom; - - if (content == null && contentPropagation == null) valueFrom = 0; - else if (content != null && contentPropagation == null) valueFrom = getLight(content, posFrom); - else if (content == null && contentPropagation != null) valueFrom = getLight(contentPropagation, posFrom); - else valueFrom = Math.max(getLight(content, posFrom), getLight(contentPropagation, posFrom)); - - int valueTo = getLight(contentPropagationTemp, posFrom); + @FunctionalInterface + interface LightLookup { + Light light(int x, int y, int z); + } - if (valueFrom < valueTo) return false; - } - } - return true; + @FunctionalInterface + interface PaletteLookup { + Palette palette(int x, int y, int z); } } diff --git a/src/main/java/net/minestom/server/instance/light/LightCompute.java b/src/main/java/net/minestom/server/instance/light/LightCompute.java index de8adc79202..591e5d21c0b 100644 --- a/src/main/java/net/minestom/server/instance/light/LightCompute.java +++ b/src/main/java/net/minestom/server/instance/light/LightCompute.java @@ -1,6 +1,7 @@ package net.minestom.server.instance.light; import it.unimi.dsi.fastutil.shorts.ShortArrayFIFOQueue; +import net.minestom.server.collision.Shape; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockFace; import net.minestom.server.instance.palette.Palette; @@ -10,35 +11,30 @@ import java.util.Arrays; import java.util.Objects; -import static net.minestom.server.instance.light.BlockLight.buildInternalQueue; - public final class LightCompute { static final Direction[] DIRECTIONS = Direction.values(); static final int LIGHT_LENGTH = 16 * 16 * 16 / 2; static final int SECTION_SIZE = 16; - public static final byte[] emptyContent = new byte[LIGHT_LENGTH]; - public static final byte[] contentFullyLit = new byte[LIGHT_LENGTH]; + public static final byte[] EMPTY_CONTENT = new byte[LIGHT_LENGTH]; + public static final byte[] CONTENT_FULLY_LIT = new byte[LIGHT_LENGTH]; static { - Arrays.fill(contentFullyLit, (byte) -1); - } - - static @NotNull Result compute(Palette blockPalette) { - return LightCompute.compute(blockPalette, buildInternalQueue(blockPalette)); + Arrays.fill(CONTENT_FULLY_LIT, (byte) -1); } /** * Computes light in one section *

* Takes queue of lights positions and spreads light from this positions in 3d using Breadth-first search + * * @param blockPalette blocks placed in section - * @param lightPre shorts queue in format: [4bit light level][4bit y][4bit z][4bit x] + * @param lightPre shorts queue in format: [4bit light level][4bit y][4bit z][4bit x] * @return lighting wrapped in Result */ - static @NotNull Result compute(Palette blockPalette, ShortArrayFIFOQueue lightPre) { + static byte @NotNull [] compute(Palette blockPalette, ShortArrayFIFOQueue lightPre) { if (lightPre.isEmpty()) { - return new Result(emptyContent); + return EMPTY_CONTENT; } final byte[] lightArray = new byte[LIGHT_LENGTH]; @@ -81,28 +77,22 @@ public final class LightCompute { final int newIndex = xO | (zO << 4) | (yO << 8); if (getLight(lightArray, newIndex) < newLightLevel) { - final Block currentBlock = Objects.requireNonNullElse(Block.fromStateId((short)blockPalette.get(x, y, z)), Block.AIR); - final Block propagatedBlock = Objects.requireNonNullElse(Block.fromStateId((short)blockPalette.get(xO, yO, zO)), Block.AIR); + final Block currentBlock = Objects.requireNonNullElse(getBlock(blockPalette, x, y, z), Block.AIR); + final Block propagatedBlock = Objects.requireNonNullElse(getBlock(blockPalette, xO, yO, zO), Block.AIR); + + final Shape currentShape = currentBlock.registry().collisionShape(); + final Shape propagatedShape = propagatedBlock.registry().collisionShape(); final boolean airAir = currentBlock.isAir() && propagatedBlock.isAir(); - if (!airAir && currentBlock.registry().collisionShape().isOccluded(propagatedBlock.registry().collisionShape(), BlockFace.fromDirection(direction))) continue; + if (!airAir && currentShape.isOccluded(propagatedShape, BlockFace.fromDirection(direction))) + continue; placeLight(lightArray, newIndex, newLightLevel); lightSources.enqueue((short) (newIndex | (newLightLevel << 12))); } } } - return new Result(lightArray); - } - - record Result(byte[] light) { - Result { - assert light.length == LIGHT_LENGTH : "Only 16x16x16 sections are supported: " + light.length; - } - - public byte getLight(int x, int y, int z) { - return (byte) LightCompute.getLight(light, x | (z << 4) | (y << 8)); - } + return lightArray; } private static void placeLight(byte[] light, int index, int value) { @@ -111,9 +101,78 @@ private static void placeLight(byte[] light, int index, int value) { light[i] = (byte) ((light[i] & (0xF0 >>> shift)) | (value << shift)); } + static int getLight(byte[] light, int x, int y, int z) { + return getLight(light, x | (z << 4) | (y << 8)); + } + static int getLight(byte[] light, int index) { if (index >>> 1 >= light.length) return 0; final int value = light[index >>> 1]; return ((value >>> ((index & 1) << 2)) & 0xF); } -} \ No newline at end of file + + public static Block getBlock(Palette palette, int x, int y, int z) { + return Block.fromStateId((short) palette.get(x, y, z)); + } + + public static byte[] bake(byte[] content1, byte[] content2) { + if (content1 == null && content2 == null) return EMPTY_CONTENT; + if (content1 == EMPTY_CONTENT && content2 == EMPTY_CONTENT) return EMPTY_CONTENT; + + if (content1 == null) return content2; + if (content2 == null) return content1; + + if (Arrays.equals(content1, EMPTY_CONTENT) && Arrays.equals(content2, EMPTY_CONTENT)) return EMPTY_CONTENT; + + byte[] lightMax = new byte[LIGHT_LENGTH]; + for (int i = 0; i < content1.length; i++) { + final byte c1 = content1[i]; + final byte c2 = content2[i]; + + // Lower + final byte l1 = (byte) (c1 & 0x0F); + final byte l2 = (byte) (c2 & 0x0F); + + // Upper + final byte u1 = (byte) ((c1 >> 4) & 0x0F); + final byte u2 = (byte) ((c2 >> 4) & 0x0F); + + final byte lower = (byte) Math.max(l1, l2); + final byte upper = (byte) Math.max(u1, u2); + + lightMax[i] = (byte) (lower | (upper << 4)); + } + return lightMax; + } + + public static boolean compareBorders(byte[] content, byte[] contentPropagation, byte[] contentPropagationTemp, BlockFace face) { + if (content == null && contentPropagation == null && contentPropagationTemp == null) return true; + + final int k = switch (face) { + case WEST, BOTTOM, NORTH -> 0; + case EAST, TOP, SOUTH -> 15; + }; + + for (int bx = 0; bx < SECTION_SIZE; bx++) { + for (int by = 0; by < SECTION_SIZE; by++) { + final int posFrom = switch (face) { + case NORTH, SOUTH -> bx | (k << 4) | (by << 8); + case WEST, EAST -> k | (by << 4) | (bx << 8); + default -> bx | (by << 4) | (k << 8); + }; + + int valueFrom; + + if (content == null && contentPropagation == null) valueFrom = 0; + else if (content != null && contentPropagation == null) valueFrom = getLight(content, posFrom); + else if (content == null) valueFrom = getLight(contentPropagation, posFrom); + else valueFrom = Math.max(getLight(content, posFrom), getLight(contentPropagation, posFrom)); + + final int valueTo = getLight(contentPropagationTemp, posFrom); + + if (valueFrom < valueTo) return false; + } + } + return true; + } +} diff --git a/src/main/java/net/minestom/server/instance/light/SkyLight.java b/src/main/java/net/minestom/server/instance/light/SkyLight.java index c569bca92ce..6a79b672048 100644 --- a/src/main/java/net/minestom/server/instance/light/SkyLight.java +++ b/src/main/java/net/minestom/server/instance/light/SkyLight.java @@ -3,16 +3,11 @@ import it.unimi.dsi.fastutil.shorts.ShortArrayFIFOQueue; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; -import net.minestom.server.instance.Chunk; -import net.minestom.server.instance.Instance; -import net.minestom.server.instance.LightingChunk; -import net.minestom.server.instance.Section; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockFace; import net.minestom.server.instance.palette.Palette; import org.jetbrains.annotations.ApiStatus; -import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @@ -20,81 +15,51 @@ import static net.minestom.server.instance.light.LightCompute.*; final class SkyLight implements Light { - private final Palette blockPalette; - private byte[] content; private byte[] contentPropagation; private byte[] contentPropagationSwap; - private final AtomicBoolean isValidBorders = new AtomicBoolean(true); + private volatile boolean isValidBorders = true; private final AtomicBoolean needsSend = new AtomicBoolean(false); - private Set toUpdateSet = new HashSet<>(); - private final Section[] neighborSections = new Section[BlockFace.values().length]; private boolean fullyLit = false; - SkyLight(Palette blockPalette) { - this.blockPalette = blockPalette; - } - @Override - public Set flip() { + public void flip() { if (this.contentPropagationSwap != null) this.contentPropagation = this.contentPropagationSwap; - this.contentPropagationSwap = null; - - if (toUpdateSet == null) return Set.of(); - return toUpdateSet; } - static ShortArrayFIFOQueue buildInternalQueue(Chunk c, int sectionY) { + static ShortArrayFIFOQueue buildInternalQueue(int[] heightmap, int maxY, int sectionY) { ShortArrayFIFOQueue lightSources = new ShortArrayFIFOQueue(); - - if (c instanceof LightingChunk lc) { - int[] heightmap = lc.getOcclusionMap(); - int maxY = c.getInstance().getCachedDimensionType().minY() + c.getInstance().getCachedDimensionType().height(); - int sectionMaxY = (sectionY + 1) * 16 - 1; - int sectionMinY = sectionY * 16; - - for (int x = 0; x < 16; x++) { - for (int z = 0; z < 16; z++) { - int height = heightmap[z << 4 | x]; - - for (int y = Math.min(sectionMaxY, maxY); y >= Math.max(height, sectionMinY); y--) { - int index = x | (z << 4) | ((y % 16) << 8); - lightSources.enqueue((short) (index | (15 << 12))); - } + final int sectionMaxY = (sectionY + 1) * 16 - 1; + final int sectionMinY = sectionY * 16; + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + final int height = heightmap[z << 4 | x]; + for (int y = Math.min(sectionMaxY, maxY); y >= Math.max(height, sectionMinY); y--) { + final int index = x | (z << 4) | ((y % 16) << 8); + lightSources.enqueue((short) (index | (15 << 12))); } } } - return lightSources; } - private static Block getBlock(Palette palette, int x, int y, int z) { - return Block.fromStateId((short)palette.get(x, y, z)); - } - - private ShortArrayFIFOQueue buildExternalQueue(Instance instance, Palette blockPalette, Point[] neighbors, byte[] content) { + private ShortArrayFIFOQueue buildExternalQueue(Palette blockPalette, + Point[] neighbors, byte[] content, + LightLookup lightLookup, + PaletteLookup paletteLookup) { ShortArrayFIFOQueue lightSources = new ShortArrayFIFOQueue(); for (int i = 0; i < neighbors.length; i++) { - var face = BlockFace.values()[i]; + final BlockFace face = BlockFace.values()[i]; Point neighborSection = neighbors[i]; if (neighborSection == null) continue; - Section otherSection = neighborSections[face.ordinal()]; - - if (otherSection == null) { - Chunk chunk = instance.getChunk(neighborSection.blockX(), neighborSection.blockZ()); - if (chunk == null) continue; - - otherSection = chunk.getSection(neighborSection.blockY()); - neighborSections[face.ordinal()] = otherSection; - } - - var otherLight = otherSection.skyLight(); + Palette otherPalette = paletteLookup.palette(neighborSection.blockX(), neighborSection.blockY(), neighborSection.blockZ()); + Light otherLight = lightLookup.light(neighborSection.blockX(), neighborSection.blockY(), neighborSection.blockZ()); for (int bx = 0; bx < 16; bx++) { for (int by = 0; by < 16; by++) { @@ -127,9 +92,9 @@ private ShortArrayFIFOQueue buildExternalQueue(Instance instance, Palette blockP }; final Block blockFrom = (switch (face) { - case NORTH, SOUTH -> getBlock(otherSection.blockPalette(), bx, by, 15 - k); - case WEST, EAST -> getBlock(otherSection.blockPalette(), 15 - k, bx, by); - default -> getBlock(otherSection.blockPalette(), bx, 15 - k, by); + case NORTH, SOUTH -> getBlock(otherPalette, bx, by, 15 - k); + case WEST, EAST -> getBlock(otherPalette, 15 - k, bx, by); + default -> getBlock(otherPalette, bx, 15 - k, by); }); if (blockTo == null && blockFrom != null) { @@ -155,68 +120,16 @@ private ShortArrayFIFOQueue buildExternalQueue(Instance instance, Palette blockP return lightSources; } - @Override - public Light calculateInternal(Instance instance, int chunkX, int sectionY, int chunkZ) { - Chunk chunk = instance.getChunk(chunkX, chunkZ); - if (chunk == null) { - this.toUpdateSet = Set.of(); - return this; - } - this.isValidBorders.set(true); - - // Update single section with base lighting changes - int queueSize = SECTION_SIZE * SECTION_SIZE * SECTION_SIZE; - ShortArrayFIFOQueue queue = new ShortArrayFIFOQueue(0); - if (!fullyLit) { - queue = buildInternalQueue(chunk, sectionY); - queueSize = queue.size(); - } - - if (queueSize == SECTION_SIZE * SECTION_SIZE * SECTION_SIZE) { - this.fullyLit = true; - this.content = contentFullyLit; - } else { - Result result = LightCompute.compute(blockPalette, queue); - this.content = result.light(); - } - - Set toUpdate = new HashSet<>(); - - // Propagate changes to neighbors and self - for (int i = -1; i <= 1; i++) { - for (int j = -1; j <= 1; j++) { - Chunk neighborChunk = instance.getChunk(chunkX + i, chunkZ + j); - if (neighborChunk == null) continue; - - for (int k = -1; k <= 1; k++) { - Vec neighborPos = new Vec(chunkX + i, sectionY + k, chunkZ + j); - - if (neighborPos.blockY() >= neighborChunk.getMinSection() && neighborPos.blockY() < neighborChunk.getMaxSection()) { - if (neighborChunk.getSection(neighborPos.blockY()).skyLight() instanceof SkyLight skyLight) { - skyLight.contentPropagation = null; - toUpdate.add(new Vec(neighborChunk.getChunkX(), neighborPos.blockY(), neighborChunk.getChunkZ())); - } - } - } - } - } - - toUpdate.add(new Vec(chunk.getChunkX(), sectionY, chunk.getChunkZ())); - this.toUpdateSet = toUpdate; - - return this; - } - @Override public void invalidate() { this.needsSend.set(true); - this.isValidBorders.set(false); + this.isValidBorders = false; this.contentPropagation = null; } @Override public boolean requiresUpdate() { - return !isValidBorders.get(); + return !isValidBorders; } @Override @@ -224,7 +137,7 @@ public boolean requiresUpdate() { public void set(byte[] copyArray) { this.content = copyArray.clone(); this.contentPropagation = this.content; - this.isValidBorders.set(true); + this.isValidBorders = true; this.needsSend.set(true); } @@ -237,83 +150,86 @@ public boolean requiresSend() { public byte[] array() { if (content == null) return new byte[0]; if (contentPropagation == null) return content; - var res = bake(contentPropagation, content); - if (res == emptyContent) return new byte[0]; + var res = LightCompute.bake(contentPropagation, content); + if (res == EMPTY_CONTENT) return new byte[0]; return res; } @Override - public Light calculateExternal(Instance instance, Chunk chunk, int sectionY) { - if (!isValidBorders.get()) { - this.toUpdateSet = Set.of(); - return this; - } + public int getLevel(int x, int y, int z) { + if (content == null) return 0; + int index = x | (z << 4) | (y << 8); + if (contentPropagation == null) return LightCompute.getLight(content, index); + return Math.max(LightCompute.getLight(contentPropagation, index), LightCompute.getLight(content, index)); + } - Point[] neighbors = Light.getNeighbors(chunk, sectionY); - Set toUpdate = new HashSet<>(); + @Override + public Set calculateInternal(Palette blockPalette, + int chunkX, int chunkY, int chunkZ, + int[] heightmap, int maxY, + LightLookup lightLookup) { + this.isValidBorders = true; - ShortArrayFIFOQueue queue; + // Update single section with base lighting changes + int queueSize = SECTION_SIZE * SECTION_SIZE * SECTION_SIZE; + ShortArrayFIFOQueue queue = new ShortArrayFIFOQueue(0); + if (!fullyLit) { + queue = buildInternalQueue(heightmap, maxY, chunkY); + queueSize = queue.size(); + } - byte[] contentPropagationTemp = contentFullyLit; + if (queueSize == SECTION_SIZE * SECTION_SIZE * SECTION_SIZE) { + this.fullyLit = true; + this.content = CONTENT_FULLY_LIT; + } else { + this.content = LightCompute.compute(blockPalette, queue); + } - if (!fullyLit) { - queue = buildExternalQueue(instance, blockPalette, neighbors, content); - LightCompute.Result result = LightCompute.compute(blockPalette, queue); + // Propagate changes to neighbors and self + Set toUpdate = new HashSet<>(); + for (int i = -1; i <= 1; i++) { + for (int j = -1; j <= 1; j++) { + for (int k = -1; k <= 1; k++) { + final int neighborX = chunkX + i; + final int neighborY = chunkY + j; + final int neighborZ = chunkZ + k; + if (!(lightLookup.light(neighborX, neighborY, neighborZ) instanceof SkyLight skyLight)) + continue; + skyLight.contentPropagation = null; + toUpdate.add(new Vec(neighborX, neighborY, neighborZ)); + } + } + } + toUpdate.add(new Vec(chunkX, chunkY, chunkZ)); + return toUpdate; + } - contentPropagationTemp = result.light(); - this.contentPropagationSwap = bake(contentPropagationSwap, contentPropagationTemp); + @Override + public Set calculateExternal(Palette blockPalette, + Point[] neighbors, + LightLookup lightLookup, + PaletteLookup paletteLookup) { + if (!isValidBorders) { + return Set.of(); + } + byte[] contentPropagationTemp = CONTENT_FULLY_LIT; + if (!fullyLit) { + ShortArrayFIFOQueue queue = buildExternalQueue(blockPalette, neighbors, content, lightLookup, paletteLookup); + contentPropagationTemp = LightCompute.compute(blockPalette, queue); + this.contentPropagationSwap = LightCompute.bake(contentPropagationSwap, contentPropagationTemp); } else { this.contentPropagationSwap = null; } - // Propagate changes to neighbors and self + Set toUpdate = new HashSet<>(); for (int i = 0; i < neighbors.length; i++) { - var neighbor = neighbors[i]; + final Point neighbor = neighbors[i]; if (neighbor == null) continue; - - var face = BlockFace.values()[i]; - - if (!Light.compareBorders(content, contentPropagation, contentPropagationTemp, face)) { + final BlockFace face = BlockFace.values()[i]; + if (!LightCompute.compareBorders(content, contentPropagation, contentPropagationTemp, face)) { toUpdate.add(neighbor); } } - - this.toUpdateSet = toUpdate; - return this; - } - - private byte[] bake(byte[] content1, byte[] content2) { - if (content1 == null && content2 == null) return emptyContent; - if (content1 == emptyContent && content2 == emptyContent) return emptyContent; - - if (content1 == null) return content2; - if (content2 == null) return content1; - - if (Arrays.equals(content1, emptyContent) && Arrays.equals(content2, emptyContent)) return emptyContent; - - byte[] lightMax = new byte[LIGHT_LENGTH]; - for (int i = 0; i < content1.length; i++) { - // Lower - byte l1 = (byte) (content1[i] & 0x0F); - byte l2 = (byte) (content2[i] & 0x0F); - - // Upper - byte u1 = (byte) ((content1[i] >> 4) & 0x0F); - byte u2 = (byte) ((content2[i] >> 4) & 0x0F); - - byte lower = (byte) Math.max(l1, l2); - byte upper = (byte) Math.max(u1, u2); - - lightMax[i] = (byte) (lower | (upper << 4)); - } - return lightMax; - } - - @Override - public int getLevel(int x, int y, int z) { - if (content == null) return 0; - int index = x | (z << 4) | (y << 8); - if (contentPropagation == null) return LightCompute.getLight(content, index); - return Math.max(LightCompute.getLight(contentPropagation, index), LightCompute.getLight(content, index)); + return toUpdate; } -} \ No newline at end of file +} diff --git a/src/test/java/net/minestom/server/instance/light/BlockLightTest.java b/src/test/java/net/minestom/server/instance/light/BlockLightTest.java index 3f6e054985e..2280d01192b 100644 --- a/src/test/java/net/minestom/server/instance/light/BlockLightTest.java +++ b/src/test/java/net/minestom/server/instance/light/BlockLightTest.java @@ -18,8 +18,8 @@ public class BlockLightTest { @Test public void empty() { var palette = Palette.blocks(); - var result = LightCompute.compute(palette); - for (byte light : result.light()) { + var result = LightCompute.compute(palette, BlockLight.buildInternalQueue(palette)); + for (byte light : result) { assertEquals(0, light); } } @@ -28,8 +28,7 @@ public void empty() { public void glowstone() { var palette = Palette.blocks(); palette.set(0, 1, 0, Block.GLOWSTONE.stateId()); - var result = LightCompute.compute(palette); - assertLight(result, Map.of( + assertLight(palette, Map.of( new Vec(0, 1, 0), 15, new Vec(0, 1, 1), 14, new Vec(0, 1, 2), 13)); @@ -41,8 +40,7 @@ public void doubleGlowstone() { palette.set(0, 1, 0, Block.GLOWSTONE.stateId()); palette.set(4, 1, 4, Block.GLOWSTONE.stateId()); - var result = LightCompute.compute(palette); - assertLight(result, Map.of( + assertLight(palette, Map.of( new Vec(1, 1, 3), 11, new Vec(3, 3, 7), 9, new Vec(1, 1, 1), 13, @@ -53,8 +51,7 @@ public void doubleGlowstone() { public void glowstoneBorder() { var palette = Palette.blocks(); palette.set(0, 1, 0, Block.GLOWSTONE.stateId()); - var result = LightCompute.compute(palette); - assertLight(result, Map.of( + assertLight(palette, Map.of( // X axis new Vec(-1, 0, 0), 13, new Vec(-1, 1, 0), 14, @@ -72,8 +69,7 @@ public void glowstoneBlock() { var palette = Palette.blocks(); palette.set(0, 1, 0, Block.GLOWSTONE.stateId()); palette.set(0, 1, 1, Block.STONE.stateId()); - var result = LightCompute.compute(palette); - assertLight(result, Map.of( + assertLight(palette, Map.of( new Vec(0, 1, 0), 15, new Vec(0, 1, 1), 0, new Vec(0, 1, 2), 11)); @@ -91,8 +87,7 @@ public void isolated() { palette.set(4, 2, 4, Block.STONE.stateId()); palette.set(4, 0, 4, Block.STONE.stateId()); - var result = LightCompute.compute(palette); - assertLight(result, Map.ofEntries( + assertLight(palette, Map.ofEntries( // Glowstone entry(new Vec(4, 1, 4), 15), // Isolation @@ -120,8 +115,7 @@ public void isolatedStair() { palette.set(4, 2, 4, Block.STONE.stateId()); palette.set(4, 0, 4, Block.STONE.stateId()); - var result = LightCompute.compute(palette); - assertLight(result, Map.ofEntries( + assertLight(palette, Map.ofEntries( // Glowstone entry(new Vec(4, 1, 4), 15), // Front of stair @@ -142,8 +136,7 @@ public void isolatedStairOpposite() { palette.set(4, 2, 4, Block.STONE.stateId()); palette.set(4, 0, 4, Block.STONE.stateId()); - var result = LightCompute.compute(palette); - assertLight(result, Map.ofEntries( + assertLight(palette, Map.ofEntries( // Glowstone entry(new Vec(4, 1, 4), 15), // Stair @@ -169,8 +162,7 @@ public void isolatedStairWest() { palette.set(4, 2, 4, Block.STONE.stateId()); palette.set(4, 0, 4, Block.STONE.stateId()); - var result = LightCompute.compute(palette); - assertLight(result, Map.ofEntries( + assertLight(palette, Map.ofEntries( // Glowstone entry(new Vec(4, 1, 4), 15), // Stair @@ -199,8 +191,7 @@ public void isolatedStairSouth() { palette.set(4, 2, 4, Block.STONE.stateId()); palette.set(4, 0, 4, Block.STONE.stateId()); - var result = LightCompute.compute(palette); - assertLight(result, Map.ofEntries( + assertLight(palette, Map.ofEntries( // Glowstone entry(new Vec(4, 1, 4), 15), // Stair @@ -212,14 +203,15 @@ public void isolatedStairSouth() { entry(new Vec(3, 0, 3), 12))); } - void assertLight(LightCompute.Result result, Map expectedLights) { + void assertLight(Palette palette, Map expectedLights) { + byte[] result = LightCompute.compute(palette, BlockLight.buildInternalQueue(palette)); List errors = new ArrayList<>(); for (int x = 0; x < 16; x++) { for (int y = 0; y < 16; y++) { for (int z = 0; z < 16; z++) { var expected = expectedLights.get(new Vec(x, y, z)); if (expected != null) { - final byte light = result.getLight(x, y, z); + final int light = LightCompute.getLight(result, x, y, z); if (light != expected) { errors.add(String.format("Expected %d at [%d,%d,%d] but got %d", expected, x, y, z, light)); } From b1c34cc9d7833039c2fa6ca981826e1d0a90b056 Mon Sep 17 00:00:00 2001 From: TheMode Date: Wed, 4 Sep 2024 01:09:35 +0200 Subject: [PATCH 13/31] record namespace (#2374) --- .../minestom/server/utils/NamespaceID.java | 117 +++++++----------- 1 file changed, 47 insertions(+), 70 deletions(-) diff --git a/src/main/java/net/minestom/server/utils/NamespaceID.java b/src/main/java/net/minestom/server/utils/NamespaceID.java index bd547661889..2bec8358d33 100644 --- a/src/main/java/net/minestom/server/utils/NamespaceID.java +++ b/src/main/java/net/minestom/server/utils/NamespaceID.java @@ -1,7 +1,5 @@ package net.minestom.server.utils; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; import net.kyori.adventure.key.Key; import org.intellij.lang.annotations.Pattern; import org.jetbrains.annotations.NotNull; @@ -12,89 +10,59 @@ * Represents a namespaced ID * https://minecraft.wiki/w/Namespaced_ID */ -public final class NamespaceID implements CharSequence, Key { - private static final String legalLetters = "[0123456789abcdefghijklmnopqrstuvwxyz_-]+"; - private static final String legalPathLetters = "[0123456789abcdefghijklmnopqrstuvwxyz./_-]+"; - private static final Cache CACHE = Caffeine.newBuilder().weakKeys().weakValues().build(); - - private final String domain; - private final String path; - private final String full; +public record NamespaceID(@NotNull String domain, @NotNull String path) implements CharSequence, Key { + private static final String LEGAL_LETTERS = "[0123456789abcdefghijklmnopqrstuvwxyz_-]+"; + private static final String LEGAL_PATH_LETTERS = "[0123456789abcdefghijklmnopqrstuvwxyz./_-]+"; public static @NotNull NamespaceID from(@NotNull String namespace) { - return CACHE.get(namespace, id -> { - final int index = id.indexOf(':'); - final String domain; - final String path; - if (index < 0) { - domain = "minecraft"; - path = id; - id = "minecraft:" + id; - } else { - domain = id.substring(0, index); - path = id.substring(index + 1); - } - return new NamespaceID(id, domain, path); - }); + final int index = namespace.indexOf(Key.DEFAULT_SEPARATOR); + final String domain; + final String path; + if (index == -1) { + domain = Key.MINECRAFT_NAMESPACE; + path = namespace; + } else { + domain = namespace.substring(0, index); + path = namespace.substring(index + 1); + } + return new NamespaceID(domain, path); } public static @NotNull NamespaceID from(@NotNull String domain, @NotNull String path) { - return from(domain + ":" + path); + return new NamespaceID(domain, path); } public static @NotNull NamespaceID from(@NotNull Key key) { - return from(key.asString()); - } - - private NamespaceID(String full, String domain, String path) { - this.full = full; - this.domain = domain; - this.path = path; - assert !domain.contains(".") && !domain.contains("/") : "Domain cannot contain a dot nor a slash character (" + full + ")"; - assert domain.matches(legalLetters) : "Illegal character in domain (" + full + "). Must match " + legalLetters; - assert path.matches(legalPathLetters) : "Illegal character in path (" + full + "). Must match " + legalPathLetters; - } - - public @NotNull String domain() { - return domain; + return new NamespaceID(key.namespace(), key.value()); } - public @NotNull String path() { - return path; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof final Key that)) return false; - return Objects.equals(this.domain, that.namespace()) && Objects.equals(this.path, that.value()); - } - - @Override - public int hashCode() { - int result = this.domain.hashCode(); - result = (31 * result) + this.path.hashCode(); - return result; + public NamespaceID { + domain = domain.intern(); + path = path.intern(); + assert !domain.contains(".") && !domain.contains("/") : "Domain cannot contain a dot nor a slash character (" + asString() + ")"; + assert domain.matches(LEGAL_LETTERS) : "Illegal character in domain (" + asString() + "). Must match " + LEGAL_LETTERS; + assert path.matches(LEGAL_PATH_LETTERS) : "Illegal character in path (" + asString() + "). Must match " + LEGAL_PATH_LETTERS; } @Override public int length() { - return full.length(); + return domain.length() + 1 + path.length(); } @Override public char charAt(int index) { - return full.charAt(index); + if (index < domain.length()) { + return domain.charAt(index); + } else if (index == domain.length()) { + return ':'; + } else { + return path.charAt(index - domain.length() - 1); + } } @Override public @NotNull CharSequence subSequence(int start, int end) { - return full.subSequence(start, end); - } - - @Override - public @NotNull String toString() { - return full; + return asString().subSequence(start, end); } @Override @@ -110,16 +78,25 @@ public char charAt(int index) { @Override public @NotNull String asString() { - return this.full; + return domain + ':' + path; } - @Deprecated - public String getDomain() { - return domain(); + @Override + public @NotNull String toString() { + return asString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof final Key that)) return false; + return Objects.equals(this.domain, that.namespace()) && Objects.equals(this.path, that.value()); } - @Deprecated - public String getPath() { - return path(); + @Override + public int hashCode() { + int result = this.domain.hashCode(); + result = (31 * result) + this.path.hashCode(); + return result; } } From 61f7b61fa9c7a4db7fad27426463ca756bd6fa86 Mon Sep 17 00:00:00 2001 From: TogAr2 <59421074+TogAr2@users.noreply.github.com> Date: Wed, 4 Sep 2024 01:46:04 +0200 Subject: [PATCH 14/31] Add EquipmentSlot.BODY (#2325) * Add EquipmentSlot.BODY * EntityEquipmentPacket should now have 7 entries * Simplify isHand check * Fix tests --------- Co-authored-by: Matt Worzala <35708499+mworzala@users.noreply.github.com> --- .../minestom/server/entity/EquipmentSlot.java | 5 +++-- .../server/entity/EquipmentSlotGroup.java | 2 +- .../minestom/server/entity/LivingEntity.java | 3 +++ .../server/inventory/EquipmentHandler.java | 22 ++++++++++++++++++- .../server/inventory/PlayerInventory.java | 5 +++++ .../PlayerInventoryIntegrationTest.java | 2 +- 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/minestom/server/entity/EquipmentSlot.java b/src/main/java/net/minestom/server/entity/EquipmentSlot.java index 79bd82afe8f..2fe93f3eb4e 100644 --- a/src/main/java/net/minestom/server/entity/EquipmentSlot.java +++ b/src/main/java/net/minestom/server/entity/EquipmentSlot.java @@ -12,7 +12,8 @@ public enum EquipmentSlot { BOOTS(true, BOOTS_SLOT), LEGGINGS(true, LEGGINGS_SLOT), CHESTPLATE(true, CHESTPLATE_SLOT), - HELMET(true, HELMET_SLOT); + HELMET(true, HELMET_SLOT), + BODY(false, -1); private static final List ARMORS = List.of(BOOTS, LEGGINGS, CHESTPLATE, HELMET); @@ -25,7 +26,7 @@ public enum EquipmentSlot { } public boolean isHand() { - return !armor; + return this == MAIN_HAND || this == OFF_HAND; } public boolean isArmor() { diff --git a/src/main/java/net/minestom/server/entity/EquipmentSlotGroup.java b/src/main/java/net/minestom/server/entity/EquipmentSlotGroup.java index 6f2bc2bff12..5e3a1290ccc 100644 --- a/src/main/java/net/minestom/server/entity/EquipmentSlotGroup.java +++ b/src/main/java/net/minestom/server/entity/EquipmentSlotGroup.java @@ -16,7 +16,7 @@ public enum EquipmentSlotGroup { CHEST(EquipmentSlot.CHESTPLATE), HEAD(EquipmentSlot.HELMET), ARMOR(EquipmentSlot.CHESTPLATE, EquipmentSlot.LEGGINGS, EquipmentSlot.BOOTS, EquipmentSlot.HELMET), - BODY(EquipmentSlot.CHESTPLATE, EquipmentSlot.LEGGINGS); + BODY(EquipmentSlot.BODY); public static final NetworkBuffer.Type NETWORK_TYPE = NetworkBuffer.Enum(EquipmentSlotGroup.class); public static final BinaryTagSerializer NBT_TYPE = BinaryTagSerializer.fromEnumStringable(EquipmentSlotGroup.class); diff --git a/src/main/java/net/minestom/server/entity/LivingEntity.java b/src/main/java/net/minestom/server/entity/LivingEntity.java index 83a02db94cf..d416a32234a 100644 --- a/src/main/java/net/minestom/server/entity/LivingEntity.java +++ b/src/main/java/net/minestom/server/entity/LivingEntity.java @@ -91,6 +91,7 @@ public class LivingEntity extends Entity implements EquipmentHandler { private ItemStack chestplate = ItemStack.AIR; private ItemStack leggings = ItemStack.AIR; private ItemStack boots = ItemStack.AIR; + private ItemStack bodyEquipment = ItemStack.AIR; /** * Constructor which allows to specify an UUID. Only use if you know what you are doing! @@ -124,6 +125,7 @@ public void setSprinting(boolean sprinting) { case LEGGINGS -> leggings; case CHESTPLATE -> chestplate; case HELMET -> helmet; + case BODY -> bodyEquipment; }; } @@ -139,6 +141,7 @@ public void setEquipment(@NotNull EquipmentSlot slot, @NotNull ItemStack itemSta case LEGGINGS -> leggings = newItem; case CHESTPLATE -> chestplate = newItem; case HELMET -> helmet = newItem; + case BODY -> bodyEquipment = newItem; } syncEquipment(slot); diff --git a/src/main/java/net/minestom/server/inventory/EquipmentHandler.java b/src/main/java/net/minestom/server/inventory/EquipmentHandler.java index 9488b5a9f70..fc4431fefad 100644 --- a/src/main/java/net/minestom/server/inventory/EquipmentHandler.java +++ b/src/main/java/net/minestom/server/inventory/EquipmentHandler.java @@ -159,6 +159,24 @@ default void setBoots(@NotNull ItemStack itemStack) { setEquipment(EquipmentSlot.BOOTS, itemStack); } + /** + * Gets the body equipment. Used by horses, wolves, and llama's. + * + * @return the body equipment + */ + default @NotNull ItemStack getBodyEquipment() { + return getEquipment(EquipmentSlot.BODY); + } + + /** + * Changes the body equipment. Used by horses, wolves, and llama's. + * + * @param itemStack the body equipment + */ + default void setBodyEquipment(@NotNull ItemStack itemStack) { + setEquipment(EquipmentSlot.BODY, itemStack); + } + default boolean hasEquipment(@NotNull EquipmentSlot slot) { return !getEquipment(slot).isAir(); } @@ -190,7 +208,9 @@ EquipmentSlot.OFF_HAND, getItemInOffHand(), EquipmentSlot.BOOTS, getBoots(), EquipmentSlot.LEGGINGS, getLeggings(), EquipmentSlot.CHESTPLATE, getChestplate(), - EquipmentSlot.HELMET, getHelmet())); + EquipmentSlot.HELMET, getHelmet(), + EquipmentSlot.BODY, getBodyEquipment())); + // Some entities do not allow body equipment, in which case the client will ignore this } } diff --git a/src/main/java/net/minestom/server/inventory/PlayerInventory.java b/src/main/java/net/minestom/server/inventory/PlayerInventory.java index db30d78c6c8..cf229d55e8f 100644 --- a/src/main/java/net/minestom/server/inventory/PlayerInventory.java +++ b/src/main/java/net/minestom/server/inventory/PlayerInventory.java @@ -10,6 +10,7 @@ import net.minestom.server.item.ItemStack; import net.minestom.server.network.packet.server.play.SetSlotPacket; import net.minestom.server.network.packet.server.play.WindowItemsPacket; +import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -54,11 +55,15 @@ private int getSlotId(@NotNull EquipmentSlot slot) { @Override public @NotNull ItemStack getEquipment(@NotNull EquipmentSlot slot) { + if (slot == EquipmentSlot.BODY) return ItemStack.AIR; return getItemStack(getSlotId(slot)); } @Override public void setEquipment(@NotNull EquipmentSlot slot, @NotNull ItemStack itemStack) { + if (slot == EquipmentSlot.BODY) + Check.fail("PlayerInventory does not support body equipment"); + safeItemInsert(getSlotId(slot), itemStack); } diff --git a/src/test/java/net/minestom/server/inventory/PlayerInventoryIntegrationTest.java b/src/test/java/net/minestom/server/inventory/PlayerInventoryIntegrationTest.java index 268a34a2768..d3eb036644e 100644 --- a/src/test/java/net/minestom/server/inventory/PlayerInventoryIntegrationTest.java +++ b/src/test/java/net/minestom/server/inventory/PlayerInventoryIntegrationTest.java @@ -97,7 +97,7 @@ public void clearInventoryTest(Env env) { // Make sure EntityEquipmentPacket is empty equipmentTracker.assertSingle(entityEquipmentPacket -> { - assertEquals(6, entityEquipmentPacket.equipments().size()); + assertEquals(7, entityEquipmentPacket.equipments().size()); for (Map.Entry entry : entityEquipmentPacket.equipments().entrySet()) { assertEquals(ItemStack.AIR, entry.getValue()); } From 2d92fa6657bece1cceb8c69fc6eb4fb79b2e7116 Mon Sep 17 00:00:00 2001 From: themode Date: Wed, 4 Sep 2024 01:46:18 +0200 Subject: [PATCH 15/31] unused cache --- .../minestom/server/command/builder/CommandDispatcher.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/java/net/minestom/server/command/builder/CommandDispatcher.java b/src/main/java/net/minestom/server/command/builder/CommandDispatcher.java index 637dca8ad32..6723b178b07 100644 --- a/src/main/java/net/minestom/server/command/builder/CommandDispatcher.java +++ b/src/main/java/net/minestom/server/command/builder/CommandDispatcher.java @@ -1,7 +1,5 @@ package net.minestom.server.command.builder; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; import net.minestom.server.command.CommandManager; import net.minestom.server.command.CommandParser; import net.minestom.server.command.CommandSender; @@ -9,7 +7,6 @@ import org.jetbrains.annotations.Nullable; import java.util.Set; -import java.util.concurrent.TimeUnit; /** * Class responsible for parsing {@link Command}. @@ -17,10 +14,6 @@ public class CommandDispatcher { private final CommandManager manager; - private final Cache cache = Caffeine.newBuilder() - .expireAfterWrite(30, TimeUnit.SECONDS) - .build(); - public CommandDispatcher(CommandManager manager) { this.manager = manager; } From f59156640754aab97a6d2aa0117009d810f016d5 Mon Sep 17 00:00:00 2001 From: Kerman <46640204+KermanIsPretty@users.noreply.github.com> Date: Tue, 3 Sep 2024 18:54:33 -0500 Subject: [PATCH 16/31] Implement Graph Cache in CommandManager (#2360) --- .../server/command/CommandManager.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/minestom/server/command/CommandManager.java b/src/main/java/net/minestom/server/command/CommandManager.java index b31a4d54ecd..bcb140f5a35 100644 --- a/src/main/java/net/minestom/server/command/CommandManager.java +++ b/src/main/java/net/minestom/server/command/CommandManager.java @@ -32,6 +32,7 @@ public final class CommandManager { private final Set commands = new HashSet<>(); private CommandCallback unknownCommandCallback; + private volatile @Nullable Graph cachedGraph; public CommandManager() { } @@ -55,6 +56,8 @@ public synchronized void register(@NotNull Command command) { for (String name : command.getNames()) { commandMap.put(name, command); } + + invalidateGraphCache(); } /** @@ -68,6 +71,8 @@ public void unregister(@NotNull Command command) { for (String name : command.getNames()) { commandMap.remove(name); } + + invalidateGraphCache(); } /** @@ -188,9 +193,22 @@ public CommandParser.Result parseCommand(@NotNull CommandSender sender, String i return parser.parse(sender, getGraph(), input); } - private Graph getGraph() { - //todo cache - return Graph.merge(commands); + private @NotNull Graph getGraph() { + Graph graph = cachedGraph; + if (graph == null) { + synchronized (this) { + graph = cachedGraph; + if (graph == null) { + graph = cachedGraph = Graph.merge(getCommands()); + } + } + } + + return graph; + } + + private void invalidateGraphCache() { + cachedGraph = null; } private static CommandResult resultConverter(ExecutableCommand executable, From 4553d3c57434a6d263e1584cc7f746389c78b512 Mon Sep 17 00:00:00 2001 From: themode Date: Wed, 4 Sep 2024 02:13:47 +0200 Subject: [PATCH 17/31] update dependencies --- gradle/libs.versions.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ecbea528981..d75cacb1ef2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,15 +5,15 @@ metadata.format.version = "1.1" # Important dependencies data = "1.21-rv3" adventure = "4.17.0" -jetbrainsAnnotations = "23.0.0" +jetbrainsAnnotations = "24.1.0" slf4j = "2.0.7" # Performance / Data Structures -caffeine = "3.1.6" -fastutil = "8.5.12" +caffeine = "3.1.8" +fastutil = "8.5.14" flare = "2.0.1" -gson = "2.10.1" -jcTools = "4.0.1" +gson = "2.11.0" +jcTools = "4.0.5" # Quality junit-jupiter = "5.9.3" From c24f3a5d99f36b2f9526a569209468f14b7ae1d2 Mon Sep 17 00:00:00 2001 From: TheMode Date: Thu, 5 Sep 2024 00:15:56 +0200 Subject: [PATCH 18/31] Remove caffeine dependency from EventNode (#2375) --- .../minestom/server/event/EventNodeImpl.java | 33 +++++++++++++------ .../server/event/EventNodeLazyImpl.java | 7 +++- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/main/java/net/minestom/server/event/EventNodeImpl.java b/src/main/java/net/minestom/server/event/EventNodeImpl.java index a3f643398a5..e6710669f94 100644 --- a/src/main/java/net/minestom/server/event/EventNodeImpl.java +++ b/src/main/java/net/minestom/server/event/EventNodeImpl.java @@ -1,6 +1,5 @@ package net.minestom.server.event; -import com.github.benmanes.caffeine.cache.Caffeine; import net.minestom.server.MinecraftServer; import net.minestom.server.ServerFlag; import net.minestom.server.event.trait.RecursiveEvent; @@ -25,10 +24,15 @@ non-sealed class EventNodeImpl implements EventNode { private final Map> handleMap = new ConcurrentHashMap<>(); final Map, ListenerEntry> listenerMap = new ConcurrentHashMap<>(); final Set> children = new CopyOnWriteArraySet<>(); - final Map> mappedNodeCache = Caffeine.newBuilder() - .weakKeys().weakValues().>build().asMap(); - final Map> registeredMappedNode = Caffeine.newBuilder() - .weakKeys().weakValues().>build().asMap(); + + // Used to store mapped nodes before any listener is added + // Necessary to avoid creating multiple nodes for the same object + // Always accessed through the global lock. + final Map>> mappedNodeCache = new WeakHashMap<>(); + // Store mapped nodes with at least one listener + // Map is copied and mutated for each new active mapped node + // Can be considered immutable. + volatile Map>> registeredMappedNode = new WeakHashMap<>(); final String name; final EventFilter filter; @@ -159,8 +163,10 @@ public void removeChildren(@NotNull String name, @NotNull Class eve node = new EventNodeLazyImpl<>(this, value, filter); Check.stateCondition(node.parent != null, "Node already has a parent"); Check.stateCondition(Objects.equals(parent, node), "Cannot map to self"); - EventNodeImpl previous = this.mappedNodeCache.putIfAbsent(value, (EventNodeImpl) node); - if (previous != null) return (EventNode) previous; + WeakReference> previousRef = this.mappedNodeCache.putIfAbsent(value, + new WeakReference<>((EventNodeLazyImpl) node)); + EventNodeImpl previous; + if (previousRef != null && (previous = previousRef.get()) != null) return (EventNode) previous; node.parent = this; } return node; @@ -169,8 +175,13 @@ public void removeChildren(@NotNull String name, @NotNull Class eve @Override public void unmap(@NotNull Object value) { synchronized (GLOBAL_CHILD_LOCK) { - final var mappedNode = this.registeredMappedNode.remove(value); - if (mappedNode != null) mappedNode.invalidateEventsFor(this); + Map>> registered = new WeakHashMap<>(registeredMappedNode); + final WeakReference> mappedNodeRef = registered.remove(value); + this.registeredMappedNode = registered; + EventNodeLazyImpl mappedNode; + if (mappedNodeRef != null && (mappedNode = mappedNodeRef.get()) != null) { + mappedNode.invalidateEventsFor(this); + } } } @@ -451,7 +462,9 @@ void invalidate() { // Retrieve all filters used to retrieve potential handlers for (var mappedEntry : mappedNodeCache.entrySet()) { - final EventNodeImpl mappedNode = mappedEntry.getValue(); + final WeakReference> mappedNodeRef = mappedEntry.getValue(); + final EventNodeLazyImpl mappedNode = mappedNodeRef.get(); + if (mappedNode == null) continue; // Weak reference collected final Handle handle = (Handle) mappedNode.getHandle(eventType); if (!handle.hasListener()) continue; // Implicit update filters.add(mappedNode.filter); diff --git a/src/main/java/net/minestom/server/event/EventNodeLazyImpl.java b/src/main/java/net/minestom/server/event/EventNodeLazyImpl.java index 6e0c613a711..f8abc333911 100644 --- a/src/main/java/net/minestom/server/event/EventNodeLazyImpl.java +++ b/src/main/java/net/minestom/server/event/EventNodeLazyImpl.java @@ -5,6 +5,8 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; import java.lang.ref.WeakReference; +import java.util.Map; +import java.util.WeakHashMap; import java.util.function.Consumer; final class EventNodeLazyImpl extends EventNodeImpl { @@ -66,7 +68,10 @@ public void register(@NotNull EventBinding binding) { private void ensureMap() { if (MAPPED.compareAndSet(this, false, true)) { synchronized (GLOBAL_CHILD_LOCK) { - var previous = this.holder.registeredMappedNode.putIfAbsent(retrieveOwner(), EventNodeImpl.class.cast(this)); + Map registered = new WeakHashMap<>(this.holder.registeredMappedNode); + var previous = registered.putIfAbsent(retrieveOwner(), + new WeakReference<>(EventNodeLazyImpl.class.cast(this))); + this.holder.registeredMappedNode = registered; if (previous == null) invalidateEventsFor(holder); } } From 6e0ee029a62fe9ce86c13a457bde09125d526dc9 Mon Sep 17 00:00:00 2001 From: themode Date: Thu, 5 Sep 2024 01:33:58 +0200 Subject: [PATCH 19/31] Remove mojang utils cache --- .../server/utils/mojang/MojangUtils.java | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/src/main/java/net/minestom/server/utils/mojang/MojangUtils.java b/src/main/java/net/minestom/server/utils/mojang/MojangUtils.java index 2bf3e9f1691..e4aa269faec 100644 --- a/src/main/java/net/minestom/server/utils/mojang/MojangUtils.java +++ b/src/main/java/net/minestom/server/utils/mojang/MojangUtils.java @@ -1,10 +1,7 @@ package net.minestom.server.utils.mojang; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import net.minestom.server.MinecraftServer; import net.minestom.server.utils.url.URLUtils; import org.jetbrains.annotations.Blocking; import org.jetbrains.annotations.NotNull; @@ -12,7 +9,6 @@ import java.io.IOException; import java.util.UUID; -import java.util.concurrent.TimeUnit; /** * Utils class using mojang API. @@ -20,13 +16,10 @@ public final class MojangUtils { private static final String FROM_UUID_URL = "https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=false"; private static final String FROM_USERNAME_URL = "https://api.mojang.com/users/profiles/minecraft/%s"; - private static final Cache URL_CACHE = Caffeine.newBuilder() - .expireAfterWrite(30, TimeUnit.SECONDS) - .softValues() - .build(); /** * Gets a player's UUID from their username + * * @param username The players username * @return The {@link UUID} * @throws IOException with text detailing the exception @@ -46,6 +39,7 @@ public final class MojangUtils { /** * Gets a player's username from their UUID + * * @param playerUUID The {@link UUID} of the player * @return The player's username * @throws IOException with text detailing the exception @@ -57,6 +51,7 @@ public final class MojangUtils { /** * Gets a {@link JsonObject} with the response from the mojang API + * * @param uuid The UUID as a {@link UUID} * @return The {@link JsonObject} or {@code null} if the mojang API is down or the UUID is invalid */ @@ -67,6 +62,7 @@ public final class MojangUtils { /** * Gets a {@link JsonObject} with the response from the mojang API + * * @param uuid The UUID as a {@link String} * @return The {@link JsonObject} or {@code null} if the mojang API is down or the UUID is invalid */ @@ -81,6 +77,7 @@ public final class MojangUtils { /** * Gets a {@link JsonObject} with the response from the mojang API + * * @param username The username as a {@link String} * @return The {@link JsonObject} or {@code null} if the mojang API is down or the username is invalid */ @@ -95,37 +92,21 @@ public final class MojangUtils { /** * Gets the JsonObject from a URL, expects a mojang player URL so the errors might not make sense if it is not + * * @param url The url to retrieve * @return The {@link JsonObject} of the result * @throws IOException with the text detailing the exception */ private static @NotNull JsonObject retrieve(@NotNull String url) throws IOException { - @Nullable final var cacheResult = URL_CACHE.getIfPresent(url); - - if (cacheResult != null) { - return cacheResult; - } - - final String response; - try { - // Retrieve from the rate-limited Mojang API - response = URLUtils.getText(url); - } catch (IOException e) { - MinecraftServer.getExceptionManager().handleException(e); - throw new RuntimeException(e); - } - + // Retrieve from the rate-limited Mojang API + final String response = URLUtils.getText(url); // If our response is "", that means the url did not get a proper object from the url // So the username or UUID was invalid, and therefore we return null - if (response.isEmpty()) { - throw new IOException("The Mojang API is down"); - } - + if (response.isEmpty()) throw new IOException("The Mojang API is down"); JsonObject jsonObject = JsonParser.parseString(response).getAsJsonObject(); if (jsonObject.has("errorMessage")) { throw new IOException(jsonObject.get("errorMessage").getAsString()); } - URL_CACHE.put(url, jsonObject); return jsonObject; } } From 3172039f39b105c044f6f773ef4f4a6b57d16c74 Mon Sep 17 00:00:00 2001 From: bea4dev <34712108+bea4dev@users.noreply.github.com> Date: Fri, 6 Sep 2024 12:15:41 +0900 Subject: [PATCH 20/31] fix team visibility (#2186) --- src/main/java/net/minestom/server/entity/Player.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index 54104a0b227..f65970e96dd 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -612,15 +612,6 @@ public void remove(boolean permanent) { if (permanent && playerConnection.isOnline()) kick(REMOVE_MESSAGE); } - @Override - public void updateOldViewer(@NotNull Player player) { - super.updateOldViewer(player); - // Team - if (this.getTeam() != null && this.getTeam().getMembers().size() == 1) {// If team only contains "this" player - player.sendPacket(this.getTeam().createTeamDestructionPacket()); - } - } - @Override public void sendPacketToViewersAndSelf(@NotNull SendablePacket packet) { sendPacket(packet); From 1b85254f912ba13946e5a67d6fc8d84956a05e93 Mon Sep 17 00:00:00 2001 From: GreatWyrm Date: Thu, 5 Sep 2024 20:22:24 -0700 Subject: [PATCH 21/31] Fix chunks not being refreshed when view distance updates (#2197) * Fix chunks not being refreshed when view distance updates --- .../net/minestom/server/entity/Player.java | 22 +++++++++++++ .../player/PlayerMovementIntegrationTest.java | 32 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index f65970e96dd..fdb47f8ea34 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -2469,6 +2469,7 @@ public void refresh(String locale, byte viewDistance, ChatMessageType chatMessag byte displayedSkinParts, MainHand mainHand, boolean enableTextFiltering, boolean allowServerListings) { this.locale = locale; // Clamp viewDistance to valid bounds + byte previousViewDistance = this.viewDistance; this.viewDistance = (byte) MathUtils.clamp(viewDistance, 2, 32); this.chatMessageType = chatMessageType; this.chatColors = chatColors; @@ -2477,6 +2478,27 @@ public void refresh(String locale, byte viewDistance, ChatMessageType chatMessag this.enableTextFiltering = enableTextFiltering; this.allowServerListings = allowServerListings; + // Check to see if we're in an instance first, as this method is called when first logging in since the client sends the Settings packet during configuration + if (instance != null) { + // Load/unload chunks if necessary due to view distance changes + if (previousViewDistance < this.viewDistance) { + // View distance expanded, send chunks + ChunkUtils.forChunksInRange(position.chunkX(), position.chunkZ(), this.viewDistance, (chunkX, chunkZ) -> { + if (Math.abs(chunkX - position.chunkX()) > previousViewDistance || Math.abs(chunkZ - position.chunkZ()) > previousViewDistance) { + chunkAdder.accept(chunkX, chunkZ); + } + }); + } else if (previousViewDistance > this.viewDistance) { + // View distance shrunk, unload chunks + ChunkUtils.forChunksInRange(position.chunkX(), position.chunkZ(), previousViewDistance, (chunkX, chunkZ) -> { + if (Math.abs(chunkX - position.chunkX()) > this.viewDistance || Math.abs(chunkZ - position.chunkZ()) > this.viewDistance) { + chunkRemover.accept(chunkX, chunkZ); + } + }); + } + // Else previous and current are equal, do nothing + } + boolean isInPlayState = getPlayerConnection().getConnectionState() == ConnectionState.PLAY; PlayerMeta playerMeta = getPlayerMeta(); if (isInPlayState) playerMeta.setNotifyAboutChanges(false); diff --git a/src/test/java/net/minestom/server/entity/player/PlayerMovementIntegrationTest.java b/src/test/java/net/minestom/server/entity/player/PlayerMovementIntegrationTest.java index 9e69730e35b..713d641483b 100644 --- a/src/test/java/net/minestom/server/entity/player/PlayerMovementIntegrationTest.java +++ b/src/test/java/net/minestom/server/entity/player/PlayerMovementIntegrationTest.java @@ -8,10 +8,12 @@ import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; import net.minestom.server.message.ChatMessageType; +import net.minestom.server.network.packet.client.common.ClientSettingsPacket; import net.minestom.server.network.packet.client.play.ClientPlayerPositionPacket; import net.minestom.server.network.packet.client.play.ClientTeleportConfirmPacket; import net.minestom.server.network.packet.server.play.ChunkDataPacket; import net.minestom.server.network.packet.server.play.EntityPositionPacket; +import net.minestom.server.network.packet.server.play.UnloadChunkPacket; import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.testing.Collector; @@ -21,6 +23,8 @@ import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.HashSet; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -137,4 +141,32 @@ public void testClientViewDistanceSettings(Env env) { player.interpretPacketQueue(); chunkDataPacketCollector.assertCount(MathUtils.square(viewDistance * 2 + 1)); } + + @Test + public void testSettingsViewDistanceExpansionAndShrink(Env env) { + int startingViewDistance = 8; + byte endViewDistance = 12; + byte finalViewDistance = 10; + var instance = env.createFlatInstance(); + var connection = env.createConnection(); + Pos startingPlayerPos = new Pos(0, 42, 0); + var player = connection.connect(instance, startingPlayerPos).join(); + + int chunkDifference = ChunkUtils.getChunkCount(endViewDistance) - ChunkUtils.getChunkCount(startingViewDistance); + + // Preload chunks, otherwise our first tracker.assertCount call will fail randomly due to chunks being loaded off the main thread + ChunkUtils.forChunksInRange(0, 0, endViewDistance, instance::loadChunk); + + var tracker = connection.trackIncoming(ChunkDataPacket.class); + player.addPacketToQueue(new ClientSettingsPacket("en_US", endViewDistance, ChatMessageType.FULL, false, (byte) 0, Player.MainHand.RIGHT, false, true)); + player.interpretPacketQueue(); + tracker.assertCount(chunkDifference); + + var tracker1 = connection.trackIncoming(UnloadChunkPacket.class); + player.addPacketToQueue(new ClientSettingsPacket("en_US", finalViewDistance, ChatMessageType.FULL, false, (byte) 0, Player.MainHand.RIGHT, false, true)); + player.interpretPacketQueue(); + + int chunkDifference1 = ChunkUtils.getChunkCount(endViewDistance) - ChunkUtils.getChunkCount(finalViewDistance); + tracker1.assertCount(chunkDifference1); + } } From b0bad7e1805fb2a4f0c8bca574233a8ad1aa66ff Mon Sep 17 00:00:00 2001 From: Lorenz Wrobel <43410952+DasBabyPixel@users.noreply.github.com> Date: Fri, 6 Sep 2024 07:17:16 +0200 Subject: [PATCH 22/31] give more control of collisions to the entities colliding (#2350) * give more control of collisions to the entities colliding --- .../server/collision/BlockCollision.java | 12 +------- .../server/collision/EntityCollision.java | 3 +- .../net/minestom/server/entity/Entity.java | 28 ++++++++++++++++--- .../net/minestom/server/entity/Player.java | 7 +++++ 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/main/java/net/minestom/server/collision/BlockCollision.java b/src/main/java/net/minestom/server/collision/BlockCollision.java index e4392fde37d..7b03b94525f 100644 --- a/src/main/java/net/minestom/server/collision/BlockCollision.java +++ b/src/main/java/net/minestom/server/collision/BlockCollision.java @@ -4,10 +4,7 @@ import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; -import net.minestom.server.entity.EntityType; -import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; -import net.minestom.server.entity.metadata.other.ArmorStandMeta; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.utils.block.BlockIterator; @@ -42,18 +39,11 @@ static PhysicsResult handlePhysics(@NotNull BoundingBox boundingBox, static Entity canPlaceBlockAt(Instance instance, Point blockPos, Block b) { for (Entity entity : instance.getNearbyEntities(blockPos, 3)) { - final EntityType type = entity.getEntityType(); - if (!entity.hasCollision() || type == EntityType.ITEM || type == EntityType.ARROW) - continue; - // Marker Armor Stands should not prevent block placement - if (entity.getEntityMeta() instanceof ArmorStandMeta armorStandMeta && armorStandMeta.isMarker()) + if (!entity.preventBlockPlacement()) continue; final boolean intersects; if (entity instanceof Player) { - // Ignore spectators - if (((Player) entity).getGameMode() == GameMode.SPECTATOR) - continue; // Need to move player slightly away from block we're placing. // If player is at block 40 we cannot place a block at block 39 with side length 1 because the block will be in [39, 40] // For this reason we subtract a small amount from the player position diff --git a/src/main/java/net/minestom/server/collision/EntityCollision.java b/src/main/java/net/minestom/server/collision/EntityCollision.java index f29229d9f5b..8f78741626c 100644 --- a/src/main/java/net/minestom/server/collision/EntityCollision.java +++ b/src/main/java/net/minestom/server/collision/EntityCollision.java @@ -5,7 +5,6 @@ import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; import net.minestom.server.instance.Instance; -import net.minestom.server.instance.block.BlockFace; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -26,7 +25,7 @@ final class EntityCollision { SweepResult sweepResult = new SweepResult(minimumRes, 0, 0, 0, null, 0, 0, 0, 0, 0, 0); if (!entityFilter.apply(e)) continue; - if (!e.hasCollision()) continue; + if (!e.hasEntityCollision()) continue; // Overlapping with entity, math can't be done we return the entity if (e.getBoundingBox().intersectBox(e.getPosition().sub(point), boundingBox)) { diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index 187c38a815b..f96ed94cbec 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -12,6 +12,7 @@ import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.metadata.EntityMeta; import net.minestom.server.entity.metadata.LivingEntityMeta; +import net.minestom.server.entity.metadata.other.ArmorStandMeta; import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.EventFilter; import net.minestom.server.event.EventHandler; @@ -87,7 +88,11 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev 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 static final Set ALLOW_BLOCK_PLACEMENT_ENTITIES = Set.of(EntityType.ARROW, EntityType.ITEM, + EntityType.SNOWBALL, EntityType.EXPERIENCE_BOTTLE, EntityType.EXPERIENCE_ORB, EntityType.POTION, + EntityType.AREA_EFFECT_CLOUD); + private static final Set NO_ENTITY_COLLISION_ENTITIES = Set.of(EntityType.TEXT_DISPLAY, EntityType.ITEM_DISPLAY, + EntityType.BLOCK_DISPLAY); private final CachedPacket destroyPacketCache = new CachedPacket(() -> new DestroyEntitiesPacket(getEntityId())); protected Instance instance; @@ -106,7 +111,8 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev protected Vec velocity = Vec.ZERO; // Movement in block per second protected boolean lastVelocityWasZero = true; protected boolean hasPhysics = true; - protected boolean hasCollision = true; + protected boolean collidesWithEntities = true; + protected boolean preventBlockPlacement = true; private Aerodynamics aerodynamics; protected int gravityTickCount; // Number of tick where gravity tick was applied @@ -191,6 +197,7 @@ public Entity(@NotNull EntityType entityType, @NotNull UUID uuid) { // Local nodes require a server process this.eventNode = null; } + updateCollisions(); } public Entity(@NotNull EntityType entityType) { @@ -504,6 +511,7 @@ public synchronized void switchEntityType(@NotNull EntityType entityType) { EntitySpawnType type = entityType.registry().spawnType(); this.aerodynamics = aerodynamics.withAirResistance(type == EntitySpawnType.LIVING || type == EntitySpawnType.PLAYER ? 0.91 : 0.98, 1 - entityType.registry().drag()); + updateCollisions(); Set viewers = new HashSet<>(getViewers()); getViewers().forEach(this::updateOldViewer); viewers.forEach(this::updateNewViewer); @@ -1718,8 +1726,20 @@ public boolean intersectBoxSwept(@NotNull Point rayStart, @NotNull Point rayDire return boundingBox.relativeEnd(); } - public boolean hasCollision() { - return hasCollision; + public boolean hasEntityCollision() { + return collidesWithEntities; + } + + public boolean preventBlockPlacement() { + // EntityMeta can change at any time, so initializing this during #initCollisions is not an option + // Can be overridden to allow for custom behaviour + if (entityMeta instanceof ArmorStandMeta armorStandMeta && armorStandMeta.isMarker()) return false; + return preventBlockPlacement; + } + + protected void updateCollisions() { + preventBlockPlacement = !ALLOW_BLOCK_PLACEMENT_ENTITIES.contains(entityType); + collidesWithEntities = !NO_ENTITY_COLLISION_ENTITIES.contains(entityType); } /** diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index fdb47f8ea34..77789101ee7 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -1641,6 +1641,7 @@ public boolean setGameMode(@NotNull GameMode gameMode) { // Make sure that the player is in the PLAY state and synchronize their flight speed. if (isActive()) { refreshAbilities(); + updateCollisions(); } return true; @@ -2333,6 +2334,12 @@ public Player asPlayer() { return this; } + @Override + protected void updateCollisions() { + preventBlockPlacement = gameMode != GameMode.SPECTATOR; + collidesWithEntities = gameMode != GameMode.SPECTATOR; + } + protected void sendChunkUpdates(Chunk newChunk) { if (chunkUpdateLimitChecker.addToHistory(newChunk)) { final int newX = newChunk.getChunkX(); From c509804300b49a1f81f9bac1f9251a35aca00a24 Mon Sep 17 00:00:00 2001 From: Samuel Date: Sat, 7 Sep 2024 18:24:53 -0400 Subject: [PATCH 23/31] Playsound exclude (#2381) * Add Instance::playSoundExcept * Uh... --- .../minestom/server/instance/Instance.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/main/java/net/minestom/server/instance/Instance.java b/src/main/java/net/minestom/server/instance/Instance.java index fd283600ecb..b75c9c74d22 100644 --- a/src/main/java/net/minestom/server/instance/Instance.java +++ b/src/main/java/net/minestom/server/instance/Instance.java @@ -1,13 +1,16 @@ package net.minestom.server.instance; import it.unimi.dsi.fastutil.objects.ObjectArraySet; +import net.kyori.adventure.audience.Audience; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.pointer.Pointers; +import net.kyori.adventure.sound.Sound; import net.minestom.server.MinecraftServer; import net.minestom.server.ServerFlag; import net.minestom.server.ServerProcess; import net.minestom.server.Tickable; +import net.minestom.server.adventure.AdventurePacketConvertor; import net.minestom.server.adventure.audience.PacketGroupingAudience; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; @@ -25,6 +28,7 @@ import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.instance.generator.Generator; import net.minestom.server.instance.light.Light; +import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.play.BlockActionPacket; import net.minestom.server.network.packet.server.play.InitializeWorldBorderPacket; import net.minestom.server.network.packet.server.play.TimeUpdatePacket; @@ -861,6 +865,34 @@ private void sendWeatherPackets(@NotNull Weather previousWeather) { tagHandler.readableCopy()); } + /** + * Plays a {@link Sound} at a given point, except to the excluded player + * @param excludedPlayer The player in the instance who won't receive the sound + * @param sound The sound to play + * @param point The point in this instance at which to play the sound + */ + public void playSoundExcept(@Nullable Player excludedPlayer, @NotNull Sound sound, @NotNull Point point) { + playSoundExcept(excludedPlayer, sound, point.x(), point.y(), point.z()); + } + + public void playSoundExcept(@Nullable Player excludedPlayer, @NotNull Sound sound, double x, double y, double z) { + ServerPacket packet = AdventurePacketConvertor.createSoundPacket(sound, x, y, z); + PacketUtils.sendGroupedPacket(getPlayers(), packet, p -> p != excludedPlayer); + } + + public void playSoundExcept(@Nullable Player excludedPlayer, @NotNull Sound sound, Sound.@NotNull Emitter emitter) { + if (emitter != Sound.Emitter.self()) { + ServerPacket packet = AdventurePacketConvertor.createSoundPacket(sound, emitter); + PacketUtils.sendGroupedPacket(getPlayers(), packet, p -> p != excludedPlayer); + } else { + // if we're playing on self, we need to delegate to each audience member + for (Audience audience : this.audiences()) { + if (audience == excludedPlayer) continue; + audience.playSound(sound, emitter); + } + } + } + /** * Creates an explosion at the given position with the given strength. * The algorithm used to compute damages is provided by {@link #getExplosionSupplier()}. From af7e3571404156fade31846c44b6a7f658965f82 Mon Sep 17 00:00:00 2001 From: SLH <114884726+SLH335@users.noreply.github.com> Date: Sun, 8 Sep 2024 00:27:18 +0200 Subject: [PATCH 24/31] fix: saturation not working in food item component (#2382) --- src/main/java/net/minestom/server/item/component/Food.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/minestom/server/item/component/Food.java b/src/main/java/net/minestom/server/item/component/Food.java index 6901be08ceb..bc978adcfee 100644 --- a/src/main/java/net/minestom/server/item/component/Food.java +++ b/src/main/java/net/minestom/server/item/component/Food.java @@ -42,7 +42,7 @@ public Food read(@NotNull NetworkBuffer buffer) { public static final BinaryTagSerializer NBT_TYPE = BinaryTagSerializer.COMPOUND.map( tag -> new Food( tag.getInt("nutrition"), - tag.getFloat("saturation_modifier"), + tag.getFloat("saturation"), tag.getBoolean("can_always_eat"), tag.getFloat("eat_seconds", DEFAULT_EAT_SECONDS), tag.get("using_converts_to") instanceof BinaryTag usingConvertsTo @@ -51,7 +51,7 @@ public Food read(@NotNull NetworkBuffer buffer) { value -> { CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder() .putInt("nutrition", value.nutrition) - .putFloat("saturation_odifier", value.saturationModifier) + .putFloat("saturation", value.saturationModifier) .putBoolean("can_always_eat", value.canAlwaysEat) .putFloat("eat_seconds", value.eatSeconds) .put("effects", EffectChance.NBT_LIST_TYPE.write(value.effects)); From 11ed85a921f1faa3a3f8f530a8b536f2cf20d11f Mon Sep 17 00:00:00 2001 From: Floweynt <50057682+Floweynt@users.noreply.github.com> Date: Sat, 7 Sep 2024 22:31:42 +0000 Subject: [PATCH 25/31] fix missing inversion in DataComponentMap#diff handling for removed entries (#2359) --- .../java/net/minestom/server/component/DataComponentMap.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/minestom/server/component/DataComponentMap.java b/src/main/java/net/minestom/server/component/DataComponentMap.java index 6bcdc1fddad..b0288816632 100644 --- a/src/main/java/net/minestom/server/component/DataComponentMap.java +++ b/src/main/java/net/minestom/server/component/DataComponentMap.java @@ -72,7 +72,7 @@ public sealed interface DataComponentMap extends DataComponent.Holder permits Da final var protoComp = protoImpl.components().get(entry.getIntKey()); // Entry in prototype if (entry.getValue() == null) { // If the component is removed, remove it from the diff if it is not in the prototype - if (protoImpl.components().containsKey(entry.getIntKey())) { + if (!protoImpl.components().containsKey(entry.getIntKey())) { iter.remove(); } } else if (protoComp != null && protoComp.equals(entry.getValue())) { From 7ce047b22ed7ae7ee01c4d954b2477e87e7e2b24 Mon Sep 17 00:00:00 2001 From: themode Date: Sun, 8 Sep 2024 19:12:32 +0200 Subject: [PATCH 26/31] More Vec operator, more min/max methods --- .../net/minestom/server/coordinate/Vec.java | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/main/java/net/minestom/server/coordinate/Vec.java b/src/main/java/net/minestom/server/coordinate/Vec.java index 4535890c689..5d92c2e34ca 100644 --- a/src/main/java/net/minestom/server/coordinate/Vec.java +++ b/src/main/java/net/minestom/server/coordinate/Vec.java @@ -177,6 +177,11 @@ public Vec(double value) { return new Vec(Math.min(x, point.x()), Math.min(y, point.y()), Math.min(z, point.z())); } + @Contract(pure = true) + public @NotNull Vec min(double x, double y, double z) { + return new Vec(Math.min(this.x, x), Math.min(this.y, y), Math.min(this.z, z)); + } + @Contract(pure = true) public @NotNull Vec min(double value) { return new Vec(Math.min(x, value), Math.min(y, value), Math.min(z, value)); @@ -187,6 +192,11 @@ public Vec(double value) { return new Vec(Math.max(x, point.x()), Math.max(y, point.y()), Math.max(z, point.z())); } + @Contract(pure = true) + public @NotNull Vec max(double x, double y, double z) { + return new Vec(Math.max(this.x, x), Math.max(this.y, y), Math.max(this.z, z)); + } + @Contract(pure = true) public @NotNull Vec max(double value) { return new Vec(Math.max(x, value), Math.max(y, value), Math.max(z, value)); @@ -320,11 +330,11 @@ public double dot(@NotNull Vec vec) { */ @Contract(pure = true) public @NotNull Vec rotateAroundY(double angle) { - double angleCos = Math.cos(angle); - double angleSin = Math.sin(angle); + final double angleCos = Math.cos(angle); + final double angleSin = Math.sin(angle); - double newX = angleCos * x + angleSin * z; - double newZ = -angleSin * x + angleCos * z; + final double newX = angleCos * x + angleSin * z; + final double newZ = -angleSin * x + angleCos * z; return new Vec(newX, y, newZ); } @@ -342,11 +352,11 @@ public double dot(@NotNull Vec vec) { */ @Contract(pure = true) public @NotNull Vec rotateAroundZ(double angle) { - double angleCos = Math.cos(angle); - double angleSin = Math.sin(angle); + final double angleCos = Math.cos(angle); + final double angleSin = Math.sin(angle); - double newX = angleCos * x - angleSin * y; - double newY = angleSin * x + angleCos * y; + final double newX = angleCos * x - angleSin * y; + final double newY = angleSin * x + angleCos * y; return new Vec(newX, newY, z); } @@ -357,11 +367,11 @@ public double dot(@NotNull Vec vec) { @Contract(pure = true) public @NotNull Vec rotateFromView(float yawDegrees, float pitchDegrees) { - double yaw = Math.toRadians(-1 * (yawDegrees + 90)); - double pitch = Math.toRadians(-pitchDegrees); + final double yaw = Math.toRadians(-1 * (yawDegrees + 90)); + final double pitch = Math.toRadians(-pitchDegrees); - double cosYaw = Math.cos(yaw); - double cosPitch = Math.cos(pitch); + final double cosYaw = Math.cos(yaw); + final double cosPitch = Math.cos(pitch); double sinYaw = Math.sin(yaw); double sinPitch = Math.sin(pitch); @@ -432,19 +442,19 @@ public double dot(@NotNull Vec vec) { */ @Contract(pure = true) public @NotNull Vec rotateAroundNonUnitAxis(@NotNull Vec axis, double angle) throws IllegalArgumentException { - double x = x(), y = y(), z = z(); - double x2 = axis.x(), y2 = axis.y(), z2 = axis.z(); + final double x = x(), y = y(), z = z(); + final double x2 = axis.x(), y2 = axis.y(), z2 = axis.z(); double cosTheta = Math.cos(angle); double sinTheta = Math.sin(angle); double dotProduct = this.dot(axis); - double newX = x2 * dotProduct * (1d - cosTheta) + final double newX = x2 * dotProduct * (1d - cosTheta) + x * cosTheta + (-z2 * y + y2 * z) * sinTheta; - double newY = y2 * dotProduct * (1d - cosTheta) + final double newY = y2 * dotProduct * (1d - cosTheta) + y * cosTheta + (z2 * x - x2 * z) * sinTheta; - double newZ = z2 * dotProduct * (1d - cosTheta) + final double newZ = z2 * dotProduct * (1d - cosTheta) + z * cosTheta + (-y2 * x + x2 * y) * sinTheta; @@ -473,26 +483,16 @@ public double dot(@NotNull Vec vec) { @FunctionalInterface public interface Operator { - /** - * Checks each axis' value, if it's below {@code Vec#EPSILON} then it gets replaced with {@code 0} - */ - Operator EPSILON = (x, y, z) -> new Vec( - Math.abs(x) < Vec.EPSILON ? 0 : x, - Math.abs(y) < Vec.EPSILON ? 0 : y, - Math.abs(z) < Vec.EPSILON ? 0 : z - ); - - Operator FLOOR = (x, y, z) -> new Vec( - Math.floor(x), - Math.floor(y), - Math.floor(z) - ); - - Operator SIGNUM = (x, y, z) -> new Vec( - Math.signum(x), - Math.signum(y), - Math.signum(z) - ); + Operator EPSILON = operator(v -> Math.abs(v) < Vec.EPSILON ? 0 : v); + Operator FLOOR = operator(Math::floor); + Operator SIGNUM = operator(Math::signum); + Operator ABS = operator(Math::abs); + Operator NEG = operator(v -> -v); + Operator CEIL = operator(Math::ceil); + + static Operator operator(@NotNull DoubleUnaryOperator operator) { + return (x, y, z) -> new Vec(operator.applyAsDouble(x), operator.applyAsDouble(y), operator.applyAsDouble(z)); + } @NotNull Vec apply(double x, double y, double z); } From 826361dc6a04604189eaad30955877a60d07fda8 Mon Sep 17 00:00:00 2001 From: Phillipp Glanz <6745190+TheMeinerLP@users.noreply.github.com> Date: Mon, 9 Sep 2024 10:41:37 +0200 Subject: [PATCH 27/31] Remove no longer exists methods --- src/main/java/net/minestom/server/entity/Player.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index 77789101ee7..4967615440e 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -2324,16 +2324,6 @@ public void setLocale(@Nullable Locale locale) { return this.pointers; } - @Override - public boolean isPlayer() { - return true; - } - - @Override - public Player asPlayer() { - return this; - } - @Override protected void updateCollisions() { preventBlockPlacement = gameMode != GameMode.SPECTATOR; From 31d44e90daaf713a3906aeea4a9f834cbea93bfb Mon Sep 17 00:00:00 2001 From: Phillipp Glanz <6745190+TheMeinerLP@users.noreply.github.com> Date: Mon, 9 Sep 2024 10:43:53 +0200 Subject: [PATCH 28/31] Disable failed test --- .../server/entity/player/PlayerMovementIntegrationTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/net/minestom/server/entity/player/PlayerMovementIntegrationTest.java b/src/test/java/net/minestom/server/entity/player/PlayerMovementIntegrationTest.java index 8aeea4f6d9d..c1cc5f75a7c 100644 --- a/src/test/java/net/minestom/server/entity/player/PlayerMovementIntegrationTest.java +++ b/src/test/java/net/minestom/server/entity/player/PlayerMovementIntegrationTest.java @@ -20,6 +20,7 @@ import net.minestom.testing.TestConnection; import net.minestom.testing.extension.MicrotusExtension; import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -142,6 +143,7 @@ void testClientViewDistanceSettings(Env env) { chunkDataPacketCollector.assertCount(MathUtils.square(viewDistance * 2 + 1)); } + @Disabled("This test is flaky") @Test public void testSettingsViewDistanceExpansionAndShrink(Env env) { int startingViewDistance = 8; From 5aa77a13fbb36e3809e2a2cd7673224d99b6e925 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 9 Sep 2024 12:49:54 +0200 Subject: [PATCH 29/31] Improve AtomicReference handling --- src/test/java/net/minestom/server/ServerProcessTest.java | 1 + .../server/instance/anvil/AnvilLoaderIntegrationTest.java | 1 + 2 files changed, 2 insertions(+) diff --git a/src/test/java/net/minestom/server/ServerProcessTest.java b/src/test/java/net/minestom/server/ServerProcessTest.java index 07bf2a6fbac..00259a2531b 100644 --- a/src/test/java/net/minestom/server/ServerProcessTest.java +++ b/src/test/java/net/minestom/server/ServerProcessTest.java @@ -21,6 +21,7 @@ void init() { assertDoesNotThrow(() -> process.get().start(new InetSocketAddress("localhost", 25565))); assertThrows(Exception.class, () -> process.get().start(new InetSocketAddress("localhost", 25566))); assertDoesNotThrow(() -> process.get().stop()); + process.set(null); } @Test diff --git a/src/test/java/net/minestom/server/instance/anvil/AnvilLoaderIntegrationTest.java b/src/test/java/net/minestom/server/instance/anvil/AnvilLoaderIntegrationTest.java index bc3fa65a401..efc1dc606d6 100644 --- a/src/test/java/net/minestom/server/instance/anvil/AnvilLoaderIntegrationTest.java +++ b/src/test/java/net/minestom/server/instance/anvil/AnvilLoaderIntegrationTest.java @@ -78,6 +78,7 @@ public void parallelSaveNonexistentFiles(Env env) throws Exception { }); instance.saveChunksToStorage().join(); assertNull(exception.get()); + exception.set(null); } @Test From eaba762f09619a2f9cb8c9a135b7931d80977cf5 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 9 Sep 2024 12:54:28 +0200 Subject: [PATCH 30/31] Fix test execution when loading chunk outside of the main thread --- .../player/PlayerMovementIntegrationTest.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/test/java/net/minestom/server/entity/player/PlayerMovementIntegrationTest.java b/src/test/java/net/minestom/server/entity/player/PlayerMovementIntegrationTest.java index c1cc5f75a7c..60fc66c523b 100644 --- a/src/test/java/net/minestom/server/entity/player/PlayerMovementIntegrationTest.java +++ b/src/test/java/net/minestom/server/entity/player/PlayerMovementIntegrationTest.java @@ -20,12 +20,9 @@ import net.minestom.testing.TestConnection; import net.minestom.testing.extension.MicrotusExtension; import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import java.time.Duration; -import java.time.temporal.ChronoUnit; import java.util.HashSet; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -48,6 +45,8 @@ void teleportConfirm(Env env) { p1.addPacketToQueue(new ClientPlayerPositionPacket(new Pos(0.2, 40, 0), true)); p1.interpretPacketQueue(); assertEquals(new Pos(0.2, 40, 0), p1.getPosition()); + p1.remove(); + env.destroyInstance(instance); } // FIXME @@ -120,6 +119,8 @@ void chunkUpdateDebounceTest(Env env) { player.addPacketToQueue(new ClientPlayerPositionPacket(new Vec(16.5, 40, -16.5), true)); player.interpretPacketQueue(); chunkDataPacketCollector.assertCount(viewDiameter * 2 - 1); + player.remove(); + env.destroyInstance(flatInstance); } @Test @@ -141,9 +142,10 @@ void testClientViewDistanceSettings(Env env) { player.addPacketToQueue(new ClientPlayerPositionPacket(new Vec(160.5, 40, 160.5), true)); player.interpretPacketQueue(); chunkDataPacketCollector.assertCount(MathUtils.square(viewDistance * 2 + 1)); + player.remove(); + env.destroyInstance(flatInstance); } - @Disabled("This test is flaky") @Test public void testSettingsViewDistanceExpansionAndShrink(Env env) { int startingViewDistance = 8; @@ -155,9 +157,10 @@ public void testSettingsViewDistanceExpansionAndShrink(Env env) { var player = connection.connect(instance, startingPlayerPos).join(); int chunkDifference = ChunkUtils.getChunkCount(endViewDistance) - ChunkUtils.getChunkCount(startingViewDistance); - // Preload chunks, otherwise our first tracker.assertCount call will fail randomly due to chunks being loaded off the main thread - ChunkUtils.forChunksInRange(0, 0, endViewDistance, instance::loadChunk); + Set> chunks = new HashSet<>(); + ChunkUtils.forChunksInRange(0, 0, endViewDistance, (v1, v2) -> chunks.add(instance.loadChunk(v1, v2))); + CompletableFuture.allOf(chunks.toArray(CompletableFuture[]::new)).join(); var tracker = connection.trackIncoming(ChunkDataPacket.class); player.addPacketToQueue(new ClientSettingsPacket("en_US", endViewDistance, ChatMessageType.FULL, false, (byte) 0, Player.MainHand.RIGHT, false, true)); @@ -170,5 +173,7 @@ public void testSettingsViewDistanceExpansionAndShrink(Env env) { int chunkDifference1 = ChunkUtils.getChunkCount(endViewDistance) - ChunkUtils.getChunkCount(finalViewDistance); tracker1.assertCount(chunkDifference1); + player.remove(); + env.destroyInstance(instance); } } From 8ad882e9cd94d2f1a0d58e79da373b550524f535 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Mon, 9 Sep 2024 13:03:25 +0200 Subject: [PATCH 31/31] Improve AtomicReference handling in some tests --- .../collision/EntityProjectileCollisionIntegrationTest.java | 4 +++- .../java/net/minestom/server/command/CommandParseTest.java | 1 + .../net/minestom/server/command/CommandSyntaxSingleTest.java | 3 +++ .../minestom/server/instance/GeneratorIntegrationTest.java | 1 + .../server/instance/InstanceBlockIntegrationTest.java | 2 +- .../generator/GeneratorForkConsumerIntegrationTest.java | 1 + 6 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/test/java/net/minestom/server/collision/EntityProjectileCollisionIntegrationTest.java b/src/test/java/net/minestom/server/collision/EntityProjectileCollisionIntegrationTest.java index a045ae71e75..b3f018ab0f5 100644 --- a/src/test/java/net/minestom/server/collision/EntityProjectileCollisionIntegrationTest.java +++ b/src/test/java/net/minestom/server/collision/EntityProjectileCollisionIntegrationTest.java @@ -71,6 +71,8 @@ void blockShootAndBlockRemoval(Env env) { assertNotNull(event); assertNotNull(event2); assertEquals(blockPosition.withY(y -> y - 1), new Vec(event.getCollisionPosition().blockX(), event.getCollisionPosition().blockY(), event.getCollisionPosition().blockZ())); + eventRef.set(null); + eventRef2.set(null); } @Test @@ -151,6 +153,6 @@ void entitySelfShoot(Env env) { assertNotNull(event); assertSame(shooter, event.getTarget()); assertTrue(shooter.getBoundingBox().intersectEntity(shooter.getPosition(), projectile)); + eventRef.set(null); } - } diff --git a/src/test/java/net/minestom/server/command/CommandParseTest.java b/src/test/java/net/minestom/server/command/CommandParseTest.java index 6e9e74f4d86..6f8143715ba 100644 --- a/src/test/java/net/minestom/server/command/CommandParseTest.java +++ b/src/test/java/net/minestom/server/command/CommandParseTest.java @@ -89,6 +89,7 @@ void singleCommandOptionalArgs() { assertValid(foo, "foo T", b); expectedFirstArg.set("A"); assertValid(foo, "foo", b); + expectedFirstArg.set(null); } @Test diff --git a/src/test/java/net/minestom/server/command/CommandSyntaxSingleTest.java b/src/test/java/net/minestom/server/command/CommandSyntaxSingleTest.java index 4380bd5be50..57428a4746b 100644 --- a/src/test/java/net/minestom/server/command/CommandSyntaxSingleTest.java +++ b/src/test/java/net/minestom/server/command/CommandSyntaxSingleTest.java @@ -174,6 +174,9 @@ private static void assertSyntax(List> args, String input, ExpectedE if (expectedValues != null) { assertEquals(expectedValues, values.get()); } + + result.set(null); + values.set(null); } private static void assertSyntax(List> args, String input, ExpectedExecution expectedExecution) { diff --git a/src/test/java/net/minestom/server/instance/GeneratorIntegrationTest.java b/src/test/java/net/minestom/server/instance/GeneratorIntegrationTest.java index 48cd288faa7..3aa407ca42b 100644 --- a/src/test/java/net/minestom/server/instance/GeneratorIntegrationTest.java +++ b/src/test/java/net/minestom/server/instance/GeneratorIntegrationTest.java @@ -47,6 +47,7 @@ void exceptionCatch(Env env) { instance.loadChunk(0, 0).join(); assertSame(exception, ref.get()); + ref.set(null); } @Test diff --git a/src/test/java/net/minestom/server/instance/InstanceBlockIntegrationTest.java b/src/test/java/net/minestom/server/instance/InstanceBlockIntegrationTest.java index a4606fe8687..ae7dad49c2f 100644 --- a/src/test/java/net/minestom/server/instance/InstanceBlockIntegrationTest.java +++ b/src/test/java/net/minestom/server/instance/InstanceBlockIntegrationTest.java @@ -83,7 +83,6 @@ void blockNbt(Env env) { @Test public void handlerPresentInPlacementRuleUpdate(Env env) { - AtomicReference currentBlock = new AtomicReference<>(); env.process().block().registerHandler(SuspiciousGravelBlockHandler.INSTANCE.getNamespaceId(), () -> SuspiciousGravelBlockHandler.INSTANCE); env.process().block().registerBlockPlacementRule(new BlockPlacementRule(Block.SUSPICIOUS_GRAVEL) { @@ -105,5 +104,6 @@ public void handlerPresentInPlacementRuleUpdate(Env env) { instance.setBlock(1, 50, 0, theBlock); assertEquals(theBlock, currentBlock.get()); + currentBlock.set(null); } } diff --git a/src/test/java/net/minestom/server/instance/generator/GeneratorForkConsumerIntegrationTest.java b/src/test/java/net/minestom/server/instance/generator/GeneratorForkConsumerIntegrationTest.java index cfe771a3239..c1447794ac1 100644 --- a/src/test/java/net/minestom/server/instance/generator/GeneratorForkConsumerIntegrationTest.java +++ b/src/test/java/net/minestom/server/instance/generator/GeneratorForkConsumerIntegrationTest.java @@ -32,6 +32,7 @@ void empty(Env env) { }); instance.loadChunk(0, 0).join(); assertNull(failed.get(), "Failed: " + failed.get()); + failed.set(null); } @Test