From 5c1acde1548e23acb9402e3c313f441b279d6e3a Mon Sep 17 00:00:00 2001 From: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> Date: Mon, 5 May 2025 08:25:37 +1400 Subject: [PATCH 01/12] Raytrace Entity Tracker --- .../features/0162-paw-optimization.patch | 8 +- .../0165-Raytrace-Entity-Tracker.patch | 157 ++++++ .../occlusionculling/DataProvider.java | 34 ++ .../OcclusionCullingInstance.java | 511 ++++++++++++++++++ .../cache/ArrayOcclusionCache.java | 57 ++ .../cache/OcclusionCache.java | 17 + .../occlusionculling/util/MathUtilities.java | 25 + .../occlusionculling/util/Vec3d.java | 86 +++ .../dev/tr7zw/entityculling/CullTask.java | 134 +++++ .../DefaultChunkDataProvider.java | 42 ++ .../versionless/access/Cullable.java | 17 + .../config/modules/misc/RaytraceTracker.java | 92 ++++ 12 files changed, 1176 insertions(+), 4 deletions(-) create mode 100644 leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch create mode 100644 leaf-server/src/main/java/com/logisticscraft/occlusionculling/DataProvider.java create mode 100644 leaf-server/src/main/java/com/logisticscraft/occlusionculling/OcclusionCullingInstance.java create mode 100644 leaf-server/src/main/java/com/logisticscraft/occlusionculling/cache/ArrayOcclusionCache.java create mode 100644 leaf-server/src/main/java/com/logisticscraft/occlusionculling/cache/OcclusionCache.java create mode 100644 leaf-server/src/main/java/com/logisticscraft/occlusionculling/util/MathUtilities.java create mode 100644 leaf-server/src/main/java/com/logisticscraft/occlusionculling/util/Vec3d.java create mode 100644 leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java create mode 100644 leaf-server/src/main/java/dev/tr7zw/entityculling/DefaultChunkDataProvider.java create mode 100644 leaf-server/src/main/java/dev/tr7zw/entityculling/versionless/access/Cullable.java create mode 100644 leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java diff --git a/leaf-server/minecraft-patches/features/0162-paw-optimization.patch b/leaf-server/minecraft-patches/features/0162-paw-optimization.patch index f3e9616610..aa6414378b 100644 --- a/leaf-server/minecraft-patches/features/0162-paw-optimization.patch +++ b/leaf-server/minecraft-patches/features/0162-paw-optimization.patch @@ -77,7 +77,7 @@ index 4535858701b2bb232b9d2feb2af6551526232ddc..e65c62dbe4c1560ae153e4c4344e9194 - // Paper end - detailed watchdog information } diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java -index d75f85208da0c7424fc95ae0d8ebb0a725dda0a7..4c0a6dcee5e63f788e532208ef3485726eedf8cc 100644 +index 0860a700106e8c1afe58c77150a0f3aee8393fdd..8e5414becccc921db39e4e6eebeb054d3af94291 100644 --- a/net/minecraft/server/level/ServerChunkCache.java +++ b/net/minecraft/server/level/ServerChunkCache.java @@ -504,9 +504,10 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon @@ -126,7 +126,7 @@ index fbfb35dad8b07c31f967d33fb04cfcfc94557d72..ba1dd51e7187a80e8438e46383257c22 private void tickPassenger(Entity ridingEntity, Entity passengerEntity, final boolean isActive) { // Paper - EAR 2 diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java -index a81983182ee3e3b874ba83ddf9bbc6ea772a2997..4221e5322fa3a3ff6ab53946aa71d54144d2c4b2 100644 +index 2c15f8f0be8ba03f2c9481bed0d46aad738848a0..124624d9c8cc292fcedb5652542b7d9da4e5d228 100644 --- a/net/minecraft/world/entity/Entity.java +++ b/net/minecraft/world/entity/Entity.java @@ -1143,31 +1143,6 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess @@ -192,7 +192,7 @@ index a81983182ee3e3b874ba83ddf9bbc6ea772a2997..4221e5322fa3a3ff6ab53946aa71d541 } private void applyMovementEmissionAndPlaySound(Entity.MovementEmission movementEmission, Vec3 movement, BlockPos pos, BlockState state) { -@@ -4833,9 +4792,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -4832,9 +4791,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } public void setDeltaMovement(Vec3 deltaMovement) { @@ -202,7 +202,7 @@ index a81983182ee3e3b874ba83ddf9bbc6ea772a2997..4221e5322fa3a3ff6ab53946aa71d541 } public void addDeltaMovement(Vec3 addend) { -@@ -4941,9 +4898,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -4940,9 +4897,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } // Paper end - Fix MC-4 if (this.position.x != x || this.position.y != y || this.position.z != z) { diff --git a/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch b/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch new file mode 100644 index 0000000000..3b49be255b --- /dev/null +++ b/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch @@ -0,0 +1,157 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> +Date: Sun, 7 Nov 2077 00:00:00 +1400 +Subject: [PATCH] Raytrace Entity Tracker + + +diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java +index 546b20f8998c71ca1a701de7efcedd8d821105e4..3585f941d3edd15276967ca09e83a635760d2cae 100644 +--- a/net/minecraft/server/level/ChunkMap.java ++++ b/net/minecraft/server/level/ChunkMap.java +@@ -1281,7 +1281,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + double d1 = vec3_dx * vec3_dx + vec3_dz * vec3_dz; // Paper + double d2 = d * d; + // Paper start - Configurable entity tracking range by Y +- boolean flag = d1 <= d2; ++ boolean flag = d1 <= d2 && !entity.isCulled(); // Leaf - Raytrace Entity Tracker + if (flag && level.paperConfig().entities.trackingRangeY.enabled) { + double rangeY = level.paperConfig().entities.trackingRangeY.get(this.entity, -1); + if (rangeY != -1) { +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 2786fbc43f238be058b153ba551f0a616c4a18f4..fa97af63223cd0988b395be7a82f9b1d3b3cfdf1 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -133,7 +133,7 @@ import net.minecraft.world.scores.ScoreHolder; + import net.minecraft.world.scores.Team; + import org.slf4j.Logger; + +-public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, ScoreHolder, ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity, ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity { // Paper - rewrite chunk system // Paper - optimise entity tracker ++public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, ScoreHolder, ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity, ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity, dev.tr7zw.entityculling.versionless.access.Cullable { // Paper - rewrite chunk system // Paper - optimise entity tracker // Leaf - Raytrace entity tracker + public static javax.script.ScriptEngine scriptEngine = new javax.script.ScriptEngineManager().getEngineByName("rhino"); // Purpur - Configurable entity base attributes + // CraftBukkit start + private static final int CURRENT_LEVEL = 2; +@@ -5291,4 +5291,45 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return this.distanceToSqr(entity.position()); + } + // Leaf end - Optimize Entity distanceToSqr ++ ++ // Leaf start - Raytrace entity tracker ++ private long lastTime = 0; ++ private volatile boolean culled = false; ++ private boolean outOfCamera = false; ++ ++ @Override ++ public void setTimeout() { ++ this.lastTime = System.currentTimeMillis() + 1000; ++ } ++ ++ @Override ++ public boolean isForcedVisible() { ++ return this.lastTime > System.currentTimeMillis(); ++ } ++ ++ @Override ++ public void setCulled(boolean value) { ++ this.culled = value; ++ if (!value) { ++ setTimeout(); ++ } ++ } ++ ++ @Override ++ public boolean isCulled() { ++ if (!org.dreeam.leaf.config.modules.misc.RaytraceTracker.enabled) return false; ++ return this.culled; ++ } ++ ++ @Override ++ public void setOutOfCamera(boolean value) { ++ this.outOfCamera = value; ++ } ++ ++ @Override ++ public boolean isOutOfCamera() { ++ if (!org.dreeam.leaf.config.modules.misc.RaytraceTracker.enabled) return false; ++ return this.outOfCamera; ++ } ++ // Leaf end - Raytrace entity tracker + } +diff --git a/net/minecraft/world/entity/EntityType.java b/net/minecraft/world/entity/EntityType.java +index adeab8980b590e4a8b64b62cb60f623b2a842982..67ec36f61184f1a7f2304ba860982cd12f0139a7 100644 +--- a/net/minecraft/world/entity/EntityType.java ++++ b/net/minecraft/world/entity/EntityType.java +@@ -1064,6 +1064,7 @@ public class EntityType implements FeatureElement, EntityTypeT + private final int clientTrackingRange; + private final int updateInterval; + public boolean dabEnabled = false; // Pufferfish ++ public boolean skipRaytraceCheck = false; // Leaf - Raytrace Entity Tracker + private final String descriptionId; + @Nullable + private Component description; +diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java +index 477455fdfcc591a89823e88983eb12dabb078d9b..24f6188f8996a41be5bbedbebabe392f99c5d41c 100644 +--- a/net/minecraft/world/entity/player/Player.java ++++ b/net/minecraft/world/entity/player/Player.java +@@ -204,6 +204,11 @@ public abstract class Player extends LivingEntity { + public int burpDelay = 0; // Purpur - Burp delay + public boolean canPortalInstant = false; // Purpur - Add portal permission bypass + public int sixRowEnderchestSlotCount = -1; // Purpur - Barrels and enderchests 6 rows ++ // Leaf start - Raytrace Entity Tracker ++ @Nullable private dev.tr7zw.entityculling.CullTask cullTask; ++ @Nullable private Level lastCullWorld; ++ private final boolean needToCull = !(this instanceof org.leavesmc.leaves.replay.ServerPhotographer); ++ // Leaf end - Raytrace Entity Tracker + + // CraftBukkit start + public boolean fauxSleeping; +@@ -296,6 +301,37 @@ public abstract class Player extends LivingEntity { + } + // Purpur end - Burp delay + ++ // Leaf start - Raytrace Entity Tracker ++ if (this.needToCull) { ++ if (!org.dreeam.leaf.config.modules.misc.RaytraceTracker.enabled) { ++ if (this.cullTask != null) { ++ this.cullTask.signalStop(); ++ this.cullTask = null; ++ } ++ } else { ++ Level currLevel = this.level(); ++ boolean needsUpdate = this.cullTask == null || this.lastCullWorld != currLevel; ++ ++ if (needsUpdate) { ++ if (this.cullTask != null) { ++ this.cullTask.signalStop(); ++ } ++ final com.logisticscraft.occlusionculling.OcclusionCullingInstance culling = new com.logisticscraft.occlusionculling.OcclusionCullingInstance( ++ org.dreeam.leaf.config.modules.misc.RaytraceTracker.maxTraceDistance, ++ new dev.tr7zw.entityculling.DefaultChunkDataProvider(currLevel) ++ ); ++ this.cullTask = new dev.tr7zw.entityculling.CullTask( ++ culling, ++ this, ++ org.dreeam.leaf.config.modules.misc.RaytraceTracker.boundingBoxLimit, ++ org.dreeam.leaf.config.modules.misc.RaytraceTracker.traceInterval ++ ); ++ this.cullTask.setup(); ++ this.lastCullWorld = currLevel; ++ } ++ } ++ } ++ // Leaf end - Raytrace Entity Tracker + this.noPhysics = this.isSpectator(); + if (this.isSpectator() || this.isPassenger()) { + this.setOnGround(false); +@@ -1553,6 +1589,12 @@ public abstract class Player extends LivingEntity { + if (this.containerMenu != null && this.hasContainerOpen()) { + this.doCloseContainer(); + } ++ // Leaf start - Raytrace Entity Tracker ++ if (this.cullTask != null) { ++ this.cullTask.signalStop(); ++ this.cullTask = null; ++ } ++ // Leaf end - Raytrace Entity Tracker + } + + public boolean isLocalPlayer() { diff --git a/leaf-server/src/main/java/com/logisticscraft/occlusionculling/DataProvider.java b/leaf-server/src/main/java/com/logisticscraft/occlusionculling/DataProvider.java new file mode 100644 index 0000000000..5ab4600554 --- /dev/null +++ b/leaf-server/src/main/java/com/logisticscraft/occlusionculling/DataProvider.java @@ -0,0 +1,34 @@ +package com.logisticscraft.occlusionculling; + +import com.logisticscraft.occlusionculling.util.Vec3d; + +public interface DataProvider { + + /** + * Prepares the requested chunk. Returns true if the chunk is ready, false when + * not loaded. Should not reload the chunk when the x and y are the same as the + * last request! + * + * @param chunkX + * @param chunkZ + * @return + */ + boolean prepareChunk(int chunkX, int chunkZ); + + /** + * Location is inside the chunk. + * + * @param x + * @param y + * @param z + * @return + */ + boolean isOpaqueFullCube(int x, int y, int z); + + default void cleanup() { + } + + default void checkingPosition(Vec3d[] targetPoints, int size, Vec3d viewerPosition) { + } + +} diff --git a/leaf-server/src/main/java/com/logisticscraft/occlusionculling/OcclusionCullingInstance.java b/leaf-server/src/main/java/com/logisticscraft/occlusionculling/OcclusionCullingInstance.java new file mode 100644 index 0000000000..a7ef3dc563 --- /dev/null +++ b/leaf-server/src/main/java/com/logisticscraft/occlusionculling/OcclusionCullingInstance.java @@ -0,0 +1,511 @@ +package com.logisticscraft.occlusionculling; + +import com.logisticscraft.occlusionculling.cache.ArrayOcclusionCache; +import com.logisticscraft.occlusionculling.cache.OcclusionCache; +import com.logisticscraft.occlusionculling.util.MathUtilities; +import com.logisticscraft.occlusionculling.util.Vec3d; + +import java.util.Arrays; +import java.util.BitSet; + +public class OcclusionCullingInstance { + + private static final int ON_MIN_X = 0x01; + private static final int ON_MAX_X = 0x02; + private static final int ON_MIN_Y = 0x04; + private static final int ON_MAX_Y = 0x08; + private static final int ON_MIN_Z = 0x10; + private static final int ON_MAX_Z = 0x20; + + private final int reach; + private final double aabbExpansion; + private final DataProvider provider; + private final OcclusionCache cache; + + // Reused allocated data structures + private final BitSet skipList = new BitSet(); // Grows bigger in case some mod introduces giant hitboxes + private final Vec3d[] targetPoints = new Vec3d[15]; + private final Vec3d targetPos = new Vec3d(0, 0, 0); + private final int[] cameraPos = new int[3]; + private final boolean[] dotselectors = new boolean[14]; + private boolean allowRayChecks = false; + private final int[] lastHitBlock = new int[3]; + private boolean allowWallClipping = false; + + + public OcclusionCullingInstance(int maxDistance, DataProvider provider) { + this(maxDistance, provider, new ArrayOcclusionCache(maxDistance), 0.5); + } + + public OcclusionCullingInstance(int maxDistance, DataProvider provider, OcclusionCache cache, double aabbExpansion) { + this.reach = maxDistance; + this.provider = provider; + this.cache = cache; + this.aabbExpansion = aabbExpansion; + for (int i = 0; i < targetPoints.length; i++) { + targetPoints[i] = new Vec3d(0, 0, 0); + } + } + + public boolean isAABBVisible(Vec3d aabbMin, Vec3d aabbMax, Vec3d viewerPosition) { + try { + int maxX = MathUtilities.floor(aabbMax.x + + aabbExpansion); + int maxY = MathUtilities.floor(aabbMax.y + + aabbExpansion); + int maxZ = MathUtilities.floor(aabbMax.z + + aabbExpansion); + int minX = MathUtilities.floor(aabbMin.x + - aabbExpansion); + int minY = MathUtilities.floor(aabbMin.y + - aabbExpansion); + int minZ = MathUtilities.floor(aabbMin.z + - aabbExpansion); + + cameraPos[0] = MathUtilities.floor(viewerPosition.x); + cameraPos[1] = MathUtilities.floor(viewerPosition.y); + cameraPos[2] = MathUtilities.floor(viewerPosition.z); + + Relative relX = Relative.from(minX, maxX, cameraPos[0]); + Relative relY = Relative.from(minY, maxY, cameraPos[1]); + Relative relZ = Relative.from(minZ, maxZ, cameraPos[2]); + + if (relX == Relative.INSIDE && relY == Relative.INSIDE && relZ == Relative.INSIDE) { + return true; // We are inside of the AABB, don't cull + } + + skipList.clear(); + + // Just check the cache first + int id = 0; + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + for (int z = minZ; z <= maxZ; z++) { + int cachedValue = getCacheValue(x, y, z); + + if (cachedValue == 1) { + // non-occluding + return true; + } + + if (cachedValue != 0) { + // was checked and it wasn't visible + skipList.set(id); + } + id++; + } + } + } + + // only after the first hit wall the cache becomes valid. + allowRayChecks = false; + + // since the cache wasn't helpfull + id = 0; + for (int x = minX; x <= maxX; x++) { + byte visibleOnFaceX = 0; + byte faceEdgeDataX = 0; + faceEdgeDataX |= (x == minX) ? ON_MIN_X : 0; + faceEdgeDataX |= (x == maxX) ? ON_MAX_X : 0; + visibleOnFaceX |= (x == minX && relX == Relative.POSITIVE) ? ON_MIN_X : 0; + visibleOnFaceX |= (x == maxX && relX == Relative.NEGATIVE) ? ON_MAX_X : 0; + for (int y = minY; y <= maxY; y++) { + byte faceEdgeDataY = faceEdgeDataX; + byte visibleOnFaceY = visibleOnFaceX; + faceEdgeDataY |= (y == minY) ? ON_MIN_Y : 0; + faceEdgeDataY |= (y == maxY) ? ON_MAX_Y : 0; + visibleOnFaceY |= (y == minY && relY == Relative.POSITIVE) ? ON_MIN_Y : 0; + visibleOnFaceY |= (y == maxY && relY == Relative.NEGATIVE) ? ON_MAX_Y : 0; + for (int z = minZ; z <= maxZ; z++) { + byte faceEdgeData = faceEdgeDataY; + byte visibleOnFace = visibleOnFaceY; + faceEdgeData |= (z == minZ) ? ON_MIN_Z : 0; + faceEdgeData |= (z == maxZ) ? ON_MAX_Z : 0; + visibleOnFace |= (z == minZ && relZ == Relative.POSITIVE) ? ON_MIN_Z : 0; + visibleOnFace |= (z == maxZ && relZ == Relative.NEGATIVE) ? ON_MAX_Z : 0; + if (skipList.get(id)) { // was checked and it wasn't visible + id++; + continue; + } + + if (visibleOnFace != 0) { + targetPos.set(x, y, z); + if (isVoxelVisible(viewerPosition, targetPos, faceEdgeData, visibleOnFace)) { + return true; + } + } + id++; + } + } + } + + return false; + } catch (Throwable t) { + // Failsafe + t.printStackTrace(); + } + return true; + } + + /** + * @param viewerPosition + * @param position + * @param faceData contains rather this Block is on the outside for a given face + * @param visibleOnFace contains rather a face should be concidered + * @return + */ + private boolean isVoxelVisible(Vec3d viewerPosition, Vec3d position, byte faceData, byte visibleOnFace) { + int targetSize = 0; + Arrays.fill(dotselectors, false); + if ((visibleOnFace & ON_MIN_X) == ON_MIN_X) { + dotselectors[0] = true; + if ((faceData & ~ON_MIN_X) != 0) { + dotselectors[1] = true; + dotselectors[4] = true; + dotselectors[5] = true; + } + dotselectors[8] = true; + } + if ((visibleOnFace & ON_MIN_Y) == ON_MIN_Y) { + dotselectors[0] = true; + if ((faceData & ~ON_MIN_Y) != 0) { + dotselectors[3] = true; + dotselectors[4] = true; + dotselectors[7] = true; + } + dotselectors[9] = true; + } + if ((visibleOnFace & ON_MIN_Z) == ON_MIN_Z) { + dotselectors[0] = true; + if ((faceData & ~ON_MIN_Z) != 0) { + dotselectors[1] = true; + dotselectors[4] = true; + dotselectors[5] = true; + } + dotselectors[10] = true; + } + if ((visibleOnFace & ON_MAX_X) == ON_MAX_X) { + dotselectors[4] = true; + if ((faceData & ~ON_MAX_X) != 0) { + dotselectors[5] = true; + dotselectors[6] = true; + dotselectors[7] = true; + } + dotselectors[11] = true; + } + if ((visibleOnFace & ON_MAX_Y) == ON_MAX_Y) { + dotselectors[1] = true; + if ((faceData & ~ON_MAX_Y) != 0) { + dotselectors[2] = true; + dotselectors[5] = true; + dotselectors[6] = true; + } + dotselectors[12] = true; + } + if ((visibleOnFace & ON_MAX_Z) == ON_MAX_Z) { + dotselectors[2] = true; + if ((faceData & ~ON_MAX_Z) != 0) { + dotselectors[3] = true; + dotselectors[6] = true; + dotselectors[7] = true; + } + dotselectors[13] = true; + } + + if (dotselectors[0]) targetPoints[targetSize++].setAdd(position, 0.05, 0.05, 0.05); + if (dotselectors[1]) targetPoints[targetSize++].setAdd(position, 0.05, 0.95, 0.05); + if (dotselectors[2]) targetPoints[targetSize++].setAdd(position, 0.05, 0.95, 0.95); + if (dotselectors[3]) targetPoints[targetSize++].setAdd(position, 0.05, 0.05, 0.95); + if (dotselectors[4]) targetPoints[targetSize++].setAdd(position, 0.95, 0.05, 0.05); + if (dotselectors[5]) targetPoints[targetSize++].setAdd(position, 0.95, 0.95, 0.05); + if (dotselectors[6]) targetPoints[targetSize++].setAdd(position, 0.95, 0.95, 0.95); + if (dotselectors[7]) targetPoints[targetSize++].setAdd(position, 0.95, 0.05, 0.95); + // middle points + if (dotselectors[8]) targetPoints[targetSize++].setAdd(position, 0.05, 0.5, 0.5); + if (dotselectors[9]) targetPoints[targetSize++].setAdd(position, 0.5, 0.05, 0.5); + if (dotselectors[10]) targetPoints[targetSize++].setAdd(position, 0.5, 0.5, 0.05); + if (dotselectors[11]) targetPoints[targetSize++].setAdd(position, 0.95, 0.5, 0.5); + if (dotselectors[12]) targetPoints[targetSize++].setAdd(position, 0.5, 0.95, 0.5); + if (dotselectors[13]) targetPoints[targetSize++].setAdd(position, 0.5, 0.5, 0.95); + + return isVisible(viewerPosition, targetPoints, targetSize); + } + + private boolean rayIntersection(int[] b, Vec3d rayOrigin, Vec3d rayDir) { + Vec3d rInv = new Vec3d(1, 1, 1).div(rayDir); + + double t1 = (b[0] - rayOrigin.x) * rInv.x; + double t2 = (b[0] + 1 - rayOrigin.x) * rInv.x; + double t3 = (b[1] - rayOrigin.y) * rInv.y; + double t4 = (b[1] + 1 - rayOrigin.y) * rInv.y; + double t5 = (b[2] - rayOrigin.z) * rInv.z; + double t6 = (b[2] + 1 - rayOrigin.z) * rInv.z; + + double tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)), Math.min(t5, t6)); + double tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)), Math.max(t5, t6)); + + // if tmax > 0, ray (line) is intersecting AABB, but the whole AABB is behind us + if (tmax > 0) { + return false; + } + + // if tmin > tmax, ray doesn't intersect AABB + return !(tmin > tmax); + } + + /** + * returns the grid cells that intersect with this Vec3d
+ * http://playtechs.blogspot.de/2007/03/raytracing-on-grid.html + *

+ * Caching assumes that all Vec3d's are inside the same block + */ + private boolean isVisible(Vec3d start, Vec3d[] targets, int size) { + // start cell coordinate + int x = cameraPos[0]; + int y = cameraPos[1]; + int z = cameraPos[2]; + + for (int v = 0; v < size; v++) { + // ray-casting target + Vec3d target = targets[v]; + + double relativeX = start.x - target.getX(); + double relativeY = start.y - target.getY(); + double relativeZ = start.z - target.getZ(); + + if (allowRayChecks && rayIntersection(lastHitBlock, start, new Vec3d(relativeX, relativeY, relativeZ).normalize())) { + continue; + } + + // horizontal and vertical cell amount spanned + double dimensionX = Math.abs(relativeX); + double dimensionY = Math.abs(relativeY); + double dimensionZ = Math.abs(relativeZ); + + // distance between horizontal intersection points with cell border as a + // fraction of the total Vec3d length + double dimFracX = 1f / dimensionX; + // distance between vertical intersection points with cell border as a fraction + // of the total Vec3d length + double dimFracY = 1f / dimensionY; + double dimFracZ = 1f / dimensionZ; + + // total amount of intersected cells + int intersectCount = 1; + + // 1, 0 or -1 + // determines the direction of the next cell (horizontally / vertically) + int x_inc, y_inc, z_inc; + + // the distance to the next horizontal / vertical intersection point with a cell + // border as a fraction of the total Vec3d length + double t_next_y, t_next_x, t_next_z; + + if (dimensionX == 0f) { + x_inc = 0; + t_next_x = dimFracX; // don't increment horizontally because the Vec3d is perfectly vertical + } else if (target.x > start.x) { + x_inc = 1; // target point is horizontally greater than starting point so increment every + // step by 1 + intersectCount += MathUtilities.floor(target.x) - x; // increment total amount of intersecting cells + t_next_x = (float) ((x + 1 - start.x) * dimFracX); // calculate the next horizontal + // intersection + // point based on the position inside + // the first cell + } else { + x_inc = -1; // target point is horizontally smaller than starting point so reduce every step + // by 1 + intersectCount += x - MathUtilities.floor(target.x); // increment total amount of intersecting cells + t_next_x = (float) ((start.x - x) + * dimFracX); // calculate the next horizontal + // intersection point + // based on the position inside + // the first cell + } + + if (dimensionY == 0f) { + y_inc = 0; + t_next_y = dimFracY; // don't increment vertically because the Vec3d is perfectly horizontal + } else if (target.y > start.y) { + y_inc = 1; // target point is vertically greater than starting point so increment every + // step by 1 + intersectCount += MathUtilities.floor(target.y) - y; // increment total amount of intersecting cells + t_next_y = (float) ((y + 1 - start.y) + * dimFracY); // calculate the next vertical + // intersection + // point based on the position inside + // the first cell + } else { + y_inc = -1; // target point is vertically smaller than starting point so reduce every step + // by 1 + intersectCount += y - MathUtilities.floor(target.y); // increment total amount of intersecting cells + t_next_y = (float) ((start.y - y) + * dimFracY); // calculate the next vertical intersection + // point + // based on the position inside + // the first cell + } + + if (dimensionZ == 0f) { + z_inc = 0; + t_next_z = dimFracZ; // don't increment vertically because the Vec3d is perfectly horizontal + } else if (target.z > start.z) { + z_inc = 1; // target point is vertically greater than starting point so increment every + // step by 1 + intersectCount += MathUtilities.floor(target.z) - z; // increment total amount of intersecting cells + t_next_z = (float) ((z + 1 - start.z) + * dimFracZ); // calculate the next vertical + // intersection + // point based on the position inside + // the first cell + } else { + z_inc = -1; // target point is vertically smaller than starting point so reduce every step + // by 1 + intersectCount += z - MathUtilities.floor(target.z); // increment total amount of intersecting cells + t_next_z = (float) ((start.z - z) + * dimFracZ); // calculate the next vertical intersection + // point + // based on the position inside + // the first cell + } + + boolean finished = stepRay(start, x, y, z, + dimFracX, dimFracY, dimFracZ, intersectCount, x_inc, y_inc, + z_inc, t_next_y, t_next_x, t_next_z); + provider.cleanup(); + if (finished) { + cacheResult(targets[0], true); + return true; + } else { + allowRayChecks = true; + } + } + cacheResult(targets[0], false); + return false; + } + + private boolean stepRay(Vec3d start, int currentX, int currentY, + int currentZ, double distInX, double distInY, + double distInZ, int n, int x_inc, int y_inc, + int z_inc, double t_next_y, double t_next_x, + double t_next_z) { + allowWallClipping = true; // initially allow rays to go through walls till they are on the outside + // iterate through all intersecting cells (n times) + for (; n > 1; n--) { // n-1 times because we don't want to check the last block + // towards - where from + + + // get cached value, 0 means uncached (default) + int cVal = getCacheValue(currentX, currentY, currentZ); + + if (cVal == 2 && !allowWallClipping) { + // block cached as occluding, stop ray + lastHitBlock[0] = currentX; + lastHitBlock[1] = currentY; + lastHitBlock[2] = currentZ; + return false; + } + + if (cVal == 0) { + // save current cell + int chunkX = currentX >> 4; + int chunkZ = currentZ >> 4; + + if (!provider.prepareChunk(chunkX, chunkZ)) { // Chunk not ready + return false; + } + + if (provider.isOpaqueFullCube(currentX, currentY, currentZ)) { + if (!allowWallClipping) { + cache.setLastHidden(); + lastHitBlock[0] = currentX; + lastHitBlock[1] = currentY; + lastHitBlock[2] = currentZ; + return false; + } + } else { + // outside of wall, now clipping is not allowed + allowWallClipping = false; + cache.setLastVisible(); + } + } + + if (cVal == 1) { + // outside of wall, now clipping is not allowed + allowWallClipping = false; + } + + + if (t_next_y < t_next_x && t_next_y < t_next_z) { // next cell is upwards/downwards because the distance to + // the next vertical + // intersection point is smaller than to the next horizontal intersection point + currentY += y_inc; // move up/down + t_next_y += distInY; // update next vertical intersection point + } else if (t_next_x < t_next_y && t_next_x < t_next_z) { // next cell is right/left + currentX += x_inc; // move right/left + t_next_x += distInX; // update next horizontal intersection point + } else { + currentZ += z_inc; // move right/left + t_next_z += distInZ; // update next horizontal intersection point + } + + } + return true; + } + + // -1 = invalid location, 0 = not checked yet, 1 = visible, 2 = occluding + private int getCacheValue(int x, int y, int z) { + x -= cameraPos[0]; + y -= cameraPos[1]; + z -= cameraPos[2]; + if (Math.abs(x) > reach - 2 || Math.abs(y) > reach - 2 + || Math.abs(z) > reach - 2) { + return -1; + } + + // check if target is already known + return cache.getState(x + reach, y + reach, z + reach); + } + + + private void cacheResult(int x, int y, int z, boolean result) { + int cx = x - cameraPos[0] + reach; + int cy = y - cameraPos[1] + reach; + int cz = z - cameraPos[2] + reach; + if (result) { + cache.setVisible(cx, cy, cz); + } else { + cache.setHidden(cx, cy, cz); + } + } + + private void cacheResult(Vec3d vector, boolean result) { + int cx = MathUtilities.floor(vector.x) - cameraPos[0] + reach; + int cy = MathUtilities.floor(vector.y) - cameraPos[1] + reach; + int cz = MathUtilities.floor(vector.z) - cameraPos[2] + reach; + if (result) { + cache.setVisible(cx, cy, cz); + } else { + cache.setHidden(cx, cy, cz); + } + } + + public void resetCache() { + this.cache.resetCache(); + } + + private enum Relative { + INSIDE, POSITIVE, NEGATIVE; + + public static Relative from(int min, int max, int pos) { + if (max > pos && min > pos) { + return POSITIVE; + } else if (min < pos && max < pos) { + return NEGATIVE; + } + return INSIDE; + } + } + +} diff --git a/leaf-server/src/main/java/com/logisticscraft/occlusionculling/cache/ArrayOcclusionCache.java b/leaf-server/src/main/java/com/logisticscraft/occlusionculling/cache/ArrayOcclusionCache.java new file mode 100644 index 0000000000..08f0ddb343 --- /dev/null +++ b/leaf-server/src/main/java/com/logisticscraft/occlusionculling/cache/ArrayOcclusionCache.java @@ -0,0 +1,57 @@ +package com.logisticscraft.occlusionculling.cache; + +import java.util.Arrays; + +public class ArrayOcclusionCache implements OcclusionCache { + + private final int reachX2; + private final byte[] cache; + private int positionKey; + private int entry; + private int offset; + + public ArrayOcclusionCache(int reach) { + this.reachX2 = reach * 2; + this.cache = new byte[(reachX2 * reachX2 * reachX2) / 4]; + } + + @Override + public void resetCache() { + Arrays.fill(cache, (byte) 0); + } + + @Override + public void setVisible(int x, int y, int z) { + positionKey = x + y * reachX2 + z * reachX2 * reachX2; + entry = positionKey / 4; + offset = (positionKey % 4) * 2; + cache[entry] |= 1 << offset; + } + + @Override + public void setHidden(int x, int y, int z) { + positionKey = x + y * reachX2 + z * reachX2 * reachX2; + entry = positionKey / 4; + offset = (positionKey % 4) * 2; + cache[entry] |= 1 << offset + 1; + } + + @Override + public int getState(int x, int y, int z) { + positionKey = x + y * reachX2 + z * reachX2 * reachX2; + entry = positionKey / 4; + offset = (positionKey % 4) * 2; + return cache[entry] >> offset & 3; + } + + @Override + public void setLastVisible() { + cache[entry] |= 1 << offset; + } + + @Override + public void setLastHidden() { + cache[entry] |= 1 << offset + 1; + } + +} diff --git a/leaf-server/src/main/java/com/logisticscraft/occlusionculling/cache/OcclusionCache.java b/leaf-server/src/main/java/com/logisticscraft/occlusionculling/cache/OcclusionCache.java new file mode 100644 index 0000000000..2a2e5620fb --- /dev/null +++ b/leaf-server/src/main/java/com/logisticscraft/occlusionculling/cache/OcclusionCache.java @@ -0,0 +1,17 @@ +package com.logisticscraft.occlusionculling.cache; + +public interface OcclusionCache { + + void resetCache(); + + void setVisible(int x, int y, int z); + + void setHidden(int x, int y, int z); + + int getState(int x, int y, int z); + + void setLastHidden(); + + void setLastVisible(); + +} diff --git a/leaf-server/src/main/java/com/logisticscraft/occlusionculling/util/MathUtilities.java b/leaf-server/src/main/java/com/logisticscraft/occlusionculling/util/MathUtilities.java new file mode 100644 index 0000000000..fb28265631 --- /dev/null +++ b/leaf-server/src/main/java/com/logisticscraft/occlusionculling/util/MathUtilities.java @@ -0,0 +1,25 @@ +package com.logisticscraft.occlusionculling.util; + +/** + * Contains MathHelper methods + */ +public final class MathUtilities { + + private MathUtilities() { + } + + public static int floor(double d) { + int i = (int) d; + return d < (double) i ? i - 1 : i; + } + + public static int fastFloor(double d) { + return (int) (d + 1024.0) - 1024; + } + + public static int ceil(double d) { + int i = (int) d; + return d > (double) i ? i + 1 : i; + } + +} diff --git a/leaf-server/src/main/java/com/logisticscraft/occlusionculling/util/Vec3d.java b/leaf-server/src/main/java/com/logisticscraft/occlusionculling/util/Vec3d.java new file mode 100644 index 0000000000..879499768d --- /dev/null +++ b/leaf-server/src/main/java/com/logisticscraft/occlusionculling/util/Vec3d.java @@ -0,0 +1,86 @@ +package com.logisticscraft.occlusionculling.util; + +public class Vec3d { + + public double x; + public double y; + public double z; + + public Vec3d(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public double getZ() { + return z; + } + + public void set(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public void setAdd(Vec3d vec, double x, double y, double z) { + this.x = vec.x + x; + this.y = vec.y + y; + this.z = vec.z + z; + } + + public Vec3d div(Vec3d rayDir) { + this.x /= rayDir.x; + this.z /= rayDir.z; + this.y /= rayDir.y; + return this; + } + + public Vec3d normalize() { + double mag = Math.sqrt(x * x + y * y + z * z); + this.x /= mag; + this.y /= mag; + this.z /= mag; + return this; + } + + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Vec3d vec3d)) { + return false; + } + if (Double.compare(vec3d.x, x) != 0) { + return false; + } + if (Double.compare(vec3d.y, y) != 0) { + return false; + } + return Double.compare(vec3d.z, z) == 0; + } + + @Override + public int hashCode() { + long l = Double.doubleToLongBits(x); + int i = (int) (l ^ l >>> 32); + l = Double.doubleToLongBits(y); + i = 31 * i + (int) (l ^ l >>> 32); + l = Double.doubleToLongBits(z); + i = 31 * i + (int) (l ^ l >>> 32); + return i; + } + + @Override + public String toString() { + return "(" + x + ", " + y + ", " + z + ")"; + } + +} diff --git a/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java b/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java new file mode 100644 index 0000000000..abb6f7dcfd --- /dev/null +++ b/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java @@ -0,0 +1,134 @@ +package dev.tr7zw.entityculling; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.logisticscraft.occlusionculling.OcclusionCullingInstance; +import com.logisticscraft.occlusionculling.util.Vec3d; +import dev.tr7zw.entityculling.versionless.access.Cullable; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.decoration.ArmorStand; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.dreeam.leaf.config.modules.misc.RaytraceTracker; + +import java.util.concurrent.*; + +public class CullTask implements Runnable { + + private static final String THREAD_PREFIX = "Leaf Raytrace Tracker"; + private volatile boolean scheduleNext = true; + private volatile boolean isInit = false; + + private final OcclusionCullingInstance culling; + private final Player checkTarget; + + private final int hitboxLimit; + + public long lastCheckedTime = 0; + + private final Vec3d lastPos = new Vec3d(0, 0, 0); + private final Vec3d aabbMin = new Vec3d(0, 0, 0); + private final Vec3d aabbMax = new Vec3d(0, 0, 0); + + private static final Executor backgroundWorker = Executors.newCachedThreadPool( + new ThreadFactoryBuilder() + .setNameFormat(THREAD_PREFIX + " Thread - %d") + .setDaemon(true) + .setPriority(Thread.NORM_PRIORITY - 1) + .build() + ); + + private final Executor worker; + + public CullTask( + OcclusionCullingInstance culling, + Player checkTarget, + int hitboxLimit, + long checkIntervalMs + ) { + this.culling = culling; + this.checkTarget = checkTarget; + this.hitboxLimit = hitboxLimit; + this.worker = CompletableFuture.delayedExecutor(checkIntervalMs, TimeUnit.MILLISECONDS, backgroundWorker); + } + + public void signalStop() { + this.scheduleNext = false; + } + + public void setup() { + if (!this.isInit) { + this.isInit = true; + } else { + return; + } + this.worker.execute(this); + } + + @Override + public synchronized void run() { + try { + if (this.checkTarget.tickCount > 10) { + Vec3 cameraMC = this.checkTarget.getEyePosition(0); + long start = System.currentTimeMillis(); + if (!(cameraMC.x == lastPos.x && cameraMC.y == lastPos.y && cameraMC.z == lastPos.z) || (start - lastCheckedTime) > 2500) { + + lastPos.set(cameraMC.x, cameraMC.y, cameraMC.z); + culling.resetCache(); + + cullEntities(cameraMC, lastPos); + + lastCheckedTime = System.currentTimeMillis() - start; + } + } + } finally { + if (this.scheduleNext) { + this.worker.execute(this); + } + } + } + + private void cullEntities(Vec3 cameraMC, Vec3d camera) { + for (Entity entity : this.checkTarget.level().getEntities().getAll()) { // This one's safe here; moonrise returns an array for us to iterate + if (!(entity instanceof Cullable cullable) || entity == this.checkTarget) { + continue; + } + + if (entity.getType().skipRaytraceCheck) { + continue; + } + + if (!cullable.isForcedVisible()) { + if (entity.isCurrentlyGlowing() || isSkippableArmorstand(entity)) { + cullable.setCulled(false); + continue; + } + + if (!entity.position().closerThan(cameraMC, RaytraceTracker.maxTraceDistance)) { + cullable.setCulled(false); // If your entity view distance is larger than tracingDistance just + // render it + continue; + } + + AABB boundingBox = entity.getBoundingBox(); + if (boundingBox.getXsize() > hitboxLimit || boundingBox.getYsize() > hitboxLimit + || boundingBox.getZsize() > hitboxLimit) { + cullable.setCulled(false); // Too big to bother to cull + continue; + } + + aabbMin.set(boundingBox.minX, boundingBox.minY, boundingBox.minZ); + aabbMax.set(boundingBox.maxX, boundingBox.maxY, boundingBox.maxZ); + + boolean visible = culling.isAABBVisible(aabbMin, aabbMax, camera); + + cullable.setCulled(!visible); + } + } + } + + private boolean isSkippableArmorstand(Entity entity) { + if (!RaytraceTracker.skipMarkerArmorStand) return false; + return entity instanceof ArmorStand && entity.isInvisible(); + } +} diff --git a/leaf-server/src/main/java/dev/tr7zw/entityculling/DefaultChunkDataProvider.java b/leaf-server/src/main/java/dev/tr7zw/entityculling/DefaultChunkDataProvider.java new file mode 100644 index 0000000000..e780575325 --- /dev/null +++ b/leaf-server/src/main/java/dev/tr7zw/entityculling/DefaultChunkDataProvider.java @@ -0,0 +1,42 @@ +package dev.tr7zw.entityculling; + +import com.logisticscraft.occlusionculling.DataProvider; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.chunk.ChunkAccess; + +public class DefaultChunkDataProvider implements DataProvider { + private final Level level; + + public DefaultChunkDataProvider(Level level) { + this.level = level; + } + + @Override + public boolean prepareChunk(int chunkX, int chunkZ) { + return this.level.getChunkIfLoaded(chunkX, chunkZ) != null; + } + + @Override + public boolean isOpaqueFullCube(int x, int y, int z) { + BlockPos pos = new BlockPos(x, y, z); + + final ChunkAccess access = this.level.getChunkIfLoaded(pos); + if (access == null) { + return false; + } + + if (this.level.isOutsideBuildHeight(pos)) { + return Blocks.VOID_AIR.defaultBlockState().isSolidRender(); + } else { + return access.getBlockState(pos).isSolidRender(); + } + } + + @Override + public void cleanup() { + DataProvider.super.cleanup(); + } + +} diff --git a/leaf-server/src/main/java/dev/tr7zw/entityculling/versionless/access/Cullable.java b/leaf-server/src/main/java/dev/tr7zw/entityculling/versionless/access/Cullable.java new file mode 100644 index 0000000000..e0f88b3ce5 --- /dev/null +++ b/leaf-server/src/main/java/dev/tr7zw/entityculling/versionless/access/Cullable.java @@ -0,0 +1,17 @@ +package dev.tr7zw.entityculling.versionless.access; + +public interface Cullable { + + void setTimeout(); + + boolean isForcedVisible(); + + void setCulled(boolean value); + + boolean isCulled(); + + void setOutOfCamera(boolean value); + + boolean isOutOfCamera(); + +} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java new file mode 100644 index 0000000000..f8ea33f52c --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java @@ -0,0 +1,92 @@ +package org.dreeam.leaf.config.modules.misc; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.EntityType; +import org.dreeam.leaf.config.ConfigModules; +import org.dreeam.leaf.config.EnumConfigCategory; +import org.dreeam.leaf.config.LeafConfig; +import org.dreeam.leaf.config.annotations.Experimental; + +import java.util.*; + +public class RaytraceTracker extends ConfigModules { + + public String getBasePath() { + return EnumConfigCategory.MISC.getBaseKeyName() + ".raytrace-entity-tracker"; + } + + @Experimental + public static boolean enabled = false; + + public static int maxTraceDistance = 64; + public static boolean skipMarkerArmorStand = false; + public static int boundingBoxLimit = 20; + public static int traceInterval = 50; + public static List skippedEntities = List.of("PLAYER"); + + @Override + public void onLoaded() { + config.addCommentRegionBased(getBasePath(), """ + *** EXPERIMENTAL FEATURE *** + Raytrace Entity Tracker uses async ray-tracing to untrack entities players cannot see, + which can reduce bandwidth usage significantly, + especially in some massive entities in small area situations. + Also it provides a way to guard against Entity ESP hacks.""", + """ + *** 实验性功能 *** + 使用异步射线追踪来动态取消跟踪玩家看不见的实体, + 可以显著降低带宽使用, + 在实体数量多且密集的情况下效果明显. + 也提供了一种对抗 Entity ESP 作弊的方案."""); + + enabled = config.getBoolean(getBasePath() + ".enabled", enabled); + maxTraceDistance = config.getInt(getBasePath() + ".max-trace-distance", maxTraceDistance, config.pickStringRegionBased( + """ + The maximum distance to trace entities in blocks.""", + """ + 最大追踪实体距离, 单位: 方块.""")); + skipMarkerArmorStand = config.getBoolean(getBasePath() + ".skip-marker-armor-stand", skipMarkerArmorStand, config.pickStringRegionBased( + """ + Whether to skip tracing entities with marker armor stand.""", + """ + 是否跳过追踪带标记盔甲架的实体.""")); + boundingBoxLimit = config.getInt(getBasePath() + ".bounding-box-limit", boundingBoxLimit, config.pickStringRegionBased( + """ + The maximum size of bounding box to trace. + Entities with bounding box larger than this value will be skipped.""", + """ + 碰撞箱大小限制, + 实体碰撞箱大于该值将被跳过.""")); + traceInterval = config.getInt(getBasePath() + ".trace-interval", traceInterval, config.pickStringRegionBased( + """ + The interval between each trace in milliseconds. + Lower value means more frequent trace.""", + """ + 追踪间隔(单位: 毫秒), 越小越频繁.""")); + skippedEntities = config.getList(getBasePath() + ".skipped-entities", skippedEntities, config.pickStringRegionBased( + """ + The entities to skip tracing.""", + """ + 跳过追踪的实体.""")); + } + + @Override + public void onPostLoaded() { + for (EntityType entityType : BuiltInRegistries.ENTITY_TYPE) { + entityType.skipRaytraceCheck = false; + } + + final String DEFAULT_PREFIX = ResourceLocation.DEFAULT_NAMESPACE + ResourceLocation.NAMESPACE_SEPARATOR; + + for (String name : skippedEntities) { + String lowerName = name.toLowerCase(Locale.ROOT); + String typeId = lowerName.startsWith(DEFAULT_PREFIX) ? lowerName : DEFAULT_PREFIX + lowerName; + + EntityType.byString(typeId).ifPresentOrElse(entityType -> + entityType.skipRaytraceCheck = true, + () -> LeafConfig.LOGGER.warn("Skip unknown entity {}, in {}", name, getBasePath() + ".skipped-entities") + ); + } + } +} From 7b1a274c32fc170889a9efe63b3bdc9f0c09898d Mon Sep 17 00:00:00 2001 From: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> Date: Mon, 5 May 2025 08:32:50 +1400 Subject: [PATCH 02/12] Server doesn't have a camera Or yes? *Thinking --- .../features/0165-Raytrace-Entity-Tracker.patch | 15 ++------------- .../versionless/access/Cullable.java | 4 ---- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch b/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch index 3b49be255b..5d94c9f21d 100644 --- a/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch +++ b/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch @@ -18,7 +18,7 @@ index 546b20f8998c71ca1a701de7efcedd8d821105e4..3585f941d3edd15276967ca09e83a635 double rangeY = level.paperConfig().entities.trackingRangeY.get(this.entity, -1); if (rangeY != -1) { diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java -index 2786fbc43f238be058b153ba551f0a616c4a18f4..fa97af63223cd0988b395be7a82f9b1d3b3cfdf1 100644 +index 2786fbc43f238be058b153ba551f0a616c4a18f4..f69565586ef795daff48a1b75119db1d86d66b2e 100644 --- a/net/minecraft/world/entity/Entity.java +++ b/net/minecraft/world/entity/Entity.java @@ -133,7 +133,7 @@ import net.minecraft.world.scores.ScoreHolder; @@ -30,7 +30,7 @@ index 2786fbc43f238be058b153ba551f0a616c4a18f4..fa97af63223cd0988b395be7a82f9b1d public static javax.script.ScriptEngine scriptEngine = new javax.script.ScriptEngineManager().getEngineByName("rhino"); // Purpur - Configurable entity base attributes // CraftBukkit start private static final int CURRENT_LEVEL = 2; -@@ -5291,4 +5291,45 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -5291,4 +5291,34 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess return this.distanceToSqr(entity.position()); } // Leaf end - Optimize Entity distanceToSqr @@ -63,17 +63,6 @@ index 2786fbc43f238be058b153ba551f0a616c4a18f4..fa97af63223cd0988b395be7a82f9b1d + if (!org.dreeam.leaf.config.modules.misc.RaytraceTracker.enabled) return false; + return this.culled; + } -+ -+ @Override -+ public void setOutOfCamera(boolean value) { -+ this.outOfCamera = value; -+ } -+ -+ @Override -+ public boolean isOutOfCamera() { -+ if (!org.dreeam.leaf.config.modules.misc.RaytraceTracker.enabled) return false; -+ return this.outOfCamera; -+ } + // Leaf end - Raytrace entity tracker } diff --git a/net/minecraft/world/entity/EntityType.java b/net/minecraft/world/entity/EntityType.java diff --git a/leaf-server/src/main/java/dev/tr7zw/entityculling/versionless/access/Cullable.java b/leaf-server/src/main/java/dev/tr7zw/entityculling/versionless/access/Cullable.java index e0f88b3ce5..667a2f0b09 100644 --- a/leaf-server/src/main/java/dev/tr7zw/entityculling/versionless/access/Cullable.java +++ b/leaf-server/src/main/java/dev/tr7zw/entityculling/versionless/access/Cullable.java @@ -10,8 +10,4 @@ public interface Cullable { boolean isCulled(); - void setOutOfCamera(boolean value); - - boolean isOutOfCamera(); - } From 0a849de9d49a73c242de5ae81b817020ae2be7e5 Mon Sep 17 00:00:00 2001 From: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> Date: Mon, 5 May 2025 08:42:12 +1400 Subject: [PATCH 03/12] Some patch info --- .../features/0165-Raytrace-Entity-Tracker.patch | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch b/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch index 5d94c9f21d..9347f84093 100644 --- a/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch +++ b/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch @@ -3,6 +3,14 @@ From: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> Date: Sun, 7 Nov 2077 00:00:00 +1400 Subject: [PATCH] Raytrace Entity Tracker +Original project: https://github.com/tr7zw/EntityCulling +Original license: Custom License + +Original project: https://github.com/LogisticsCraft/OcclusionCulling +Original license: MIT + +Ported by: https://github.com/LuminolMC/Luminol +Fixed and updated by: https://github.com/Winds-Studio/Leaf diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java index 546b20f8998c71ca1a701de7efcedd8d821105e4..3585f941d3edd15276967ca09e83a635760d2cae 100644 From 3a68bc5a87cd705c8947f516738cd7d440328cd5 Mon Sep 17 00:00:00 2001 From: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> Date: Mon, 5 May 2025 09:00:45 +1400 Subject: [PATCH 04/12] expose AABB expansion to config --- .../occlusionculling/OcclusionCullingInstance.java | 2 +- .../dreeam/leaf/config/modules/misc/RaytraceTracker.java | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/leaf-server/src/main/java/com/logisticscraft/occlusionculling/OcclusionCullingInstance.java b/leaf-server/src/main/java/com/logisticscraft/occlusionculling/OcclusionCullingInstance.java index a7ef3dc563..921f905d00 100644 --- a/leaf-server/src/main/java/com/logisticscraft/occlusionculling/OcclusionCullingInstance.java +++ b/leaf-server/src/main/java/com/logisticscraft/occlusionculling/OcclusionCullingInstance.java @@ -34,7 +34,7 @@ public class OcclusionCullingInstance { public OcclusionCullingInstance(int maxDistance, DataProvider provider) { - this(maxDistance, provider, new ArrayOcclusionCache(maxDistance), 0.5); + this(maxDistance, provider, new ArrayOcclusionCache(maxDistance), org.dreeam.leaf.config.modules.misc.RaytraceTracker.boundingBoxExpansion); } public OcclusionCullingInstance(int maxDistance, DataProvider provider, OcclusionCache cache, double aabbExpansion) { diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java index f8ea33f52c..d129b3282f 100644 --- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java +++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java @@ -23,6 +23,7 @@ public String getBasePath() { public static boolean skipMarkerArmorStand = false; public static int boundingBoxLimit = 20; public static int traceInterval = 50; + public static double boundingBoxExpansion = 0.5D; public static List skippedEntities = List.of("PLAYER"); @Override @@ -64,6 +65,13 @@ public void onLoaded() { Lower value means more frequent trace.""", """ 追踪间隔(单位: 毫秒), 越小越频繁.""")); + boundingBoxExpansion = config.getDouble(getBasePath() + ".bounding-box-expansion", boundingBoxExpansion, config.pickStringRegionBased( + """ + The expansion of bounding box. + This modifier will be added to the actual bounding box when tracing.""", + """ + 碰撞箱扩大量. + 此值将会在射线追踪时添加到实际碰撞箱上.""")); skippedEntities = config.getList(getBasePath() + ".skipped-entities", skippedEntities, config.pickStringRegionBased( """ The entities to skip tracing.""", From 92e04aae908179ff6365141f18d7ea8fb453da45 Mon Sep 17 00:00:00 2001 From: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> Date: Mon, 5 May 2025 09:55:24 +1400 Subject: [PATCH 05/12] Option to invert entity skip list --- .../leaf/config/modules/misc/RaytraceTracker.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java index d129b3282f..7b68fd9261 100644 --- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java +++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java @@ -25,6 +25,7 @@ public String getBasePath() { public static int traceInterval = 50; public static double boundingBoxExpansion = 0.5D; public static List skippedEntities = List.of("PLAYER"); + public static boolean invertSkipEntities = false; @Override public void onLoaded() { @@ -77,12 +78,17 @@ public void onLoaded() { The entities to skip tracing.""", """ 跳过追踪的实体.""")); + invertSkipEntities = config.getBoolean(getBasePath() + ".invert-skip-entities", invertSkipEntities, config.pickStringRegionBased( + """ + Whether to invert the skip entities list.""", + """ + 是否反转跳过实体列表.""")); } @Override public void onPostLoaded() { for (EntityType entityType : BuiltInRegistries.ENTITY_TYPE) { - entityType.skipRaytraceCheck = false; + entityType.skipRaytraceCheck = invertSkipEntities; } final String DEFAULT_PREFIX = ResourceLocation.DEFAULT_NAMESPACE + ResourceLocation.NAMESPACE_SEPARATOR; @@ -92,7 +98,7 @@ public void onPostLoaded() { String typeId = lowerName.startsWith(DEFAULT_PREFIX) ? lowerName : DEFAULT_PREFIX + lowerName; EntityType.byString(typeId).ifPresentOrElse(entityType -> - entityType.skipRaytraceCheck = true, + entityType.skipRaytraceCheck = !invertSkipEntities, () -> LeafConfig.LOGGER.warn("Skip unknown entity {}, in {}", name, getBasePath() + ".skipped-entities") ); } From b64326bdd7531977118d76fd61fe9350ddd98710 Mon Sep 17 00:00:00 2001 From: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> Date: Mon, 5 May 2025 10:10:49 +1400 Subject: [PATCH 06/12] Force visible radius --- .../main/java/dev/tr7zw/entityculling/CullTask.java | 8 +++++++- .../leaf/config/modules/misc/RaytraceTracker.java | 10 ++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java b/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java index abb6f7dcfd..f190a00c26 100644 --- a/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java +++ b/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java @@ -104,7 +104,13 @@ private void cullEntities(Vec3 cameraMC, Vec3d camera) { continue; } - if (!entity.position().closerThan(cameraMC, RaytraceTracker.maxTraceDistance)) { + final double distanceSqr = entity.position().distanceToSqr(cameraMC); + if (distanceSqr < RaytraceTracker.forceVisibleRadius * RaytraceTracker.forceVisibleRadius) { + cullable.setCulled(false); + continue; + } + + if (distanceSqr >= RaytraceTracker.maxTraceDistance * RaytraceTracker.maxTraceDistance) { cullable.setCulled(false); // If your entity view distance is larger than tracingDistance just // render it continue; diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java index 7b68fd9261..4a07f63a45 100644 --- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java +++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java @@ -23,6 +23,7 @@ public String getBasePath() { public static boolean skipMarkerArmorStand = false; public static int boundingBoxLimit = 20; public static int traceInterval = 50; + public static double forceVisibleRadius = 2.0D; public static double boundingBoxExpansion = 0.5D; public static List skippedEntities = List.of("PLAYER"); public static boolean invertSkipEntities = false; @@ -66,6 +67,15 @@ public void onLoaded() { Lower value means more frequent trace.""", """ 追踪间隔(单位: 毫秒), 越小越频繁.""")); + forceVisibleRadius = config.getDouble(getBasePath() + ".force-visible-radius", forceVisibleRadius, config.pickStringRegionBased( + """ + The radius to force visible entities. + Entities within this radius will be forced visible. + Set to values less than or equal to zero to disable.""", + """ + 强制可见半径, + 实体在范围内将被强制可见. + 设置为小于等于 0 的值来禁用强制可见.""")); boundingBoxExpansion = config.getDouble(getBasePath() + ".bounding-box-expansion", boundingBoxExpansion, config.pickStringRegionBased( """ The expansion of bounding box. From 12f4dc333d69a394d04168f77e17a5c7a2f61d43 Mon Sep 17 00:00:00 2001 From: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> Date: Mon, 5 May 2025 16:34:29 +1400 Subject: [PATCH 07/12] Only check for canOcclude --- .../dev/tr7zw/entityculling/DefaultChunkDataProvider.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/leaf-server/src/main/java/dev/tr7zw/entityculling/DefaultChunkDataProvider.java b/leaf-server/src/main/java/dev/tr7zw/entityculling/DefaultChunkDataProvider.java index e780575325..8744a5d591 100644 --- a/leaf-server/src/main/java/dev/tr7zw/entityculling/DefaultChunkDataProvider.java +++ b/leaf-server/src/main/java/dev/tr7zw/entityculling/DefaultChunkDataProvider.java @@ -4,6 +4,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.ChunkAccess; public class DefaultChunkDataProvider implements DataProvider { @@ -28,9 +29,11 @@ public boolean isOpaqueFullCube(int x, int y, int z) { } if (this.level.isOutsideBuildHeight(pos)) { - return Blocks.VOID_AIR.defaultBlockState().isSolidRender(); + BlockState bs = Blocks.VOID_AIR.defaultBlockState(); + return !bs.canOcclude(); } else { - return access.getBlockState(pos).isSolidRender(); + BlockState bs = access.getBlockState(pos); + return !bs.canOcclude(); } } From dbfa70f21242b176a8cd775e1b08502f85cb34b3 Mon Sep 17 00:00:00 2001 From: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> Date: Mon, 5 May 2025 23:33:16 +1400 Subject: [PATCH 08/12] Remove default skip player --- .../org/dreeam/leaf/config/modules/misc/RaytraceTracker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java index 4a07f63a45..c30da035ff 100644 --- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java +++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/misc/RaytraceTracker.java @@ -25,7 +25,7 @@ public String getBasePath() { public static int traceInterval = 50; public static double forceVisibleRadius = 2.0D; public static double boundingBoxExpansion = 0.5D; - public static List skippedEntities = List.of("PLAYER"); + public static List skippedEntities = List.of(); public static boolean invertSkipEntities = false; @Override From 6ae90711c470ecc3a59ccc6b3d2f621a840c28d8 Mon Sep 17 00:00:00 2001 From: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> Date: Tue, 6 May 2025 04:18:07 +1400 Subject: [PATCH 09/12] Change DefaultChunkDataProvider check logic --- .../dev/tr7zw/entityculling/DefaultChunkDataProvider.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/leaf-server/src/main/java/dev/tr7zw/entityculling/DefaultChunkDataProvider.java b/leaf-server/src/main/java/dev/tr7zw/entityculling/DefaultChunkDataProvider.java index 8744a5d591..a4960b1ee2 100644 --- a/leaf-server/src/main/java/dev/tr7zw/entityculling/DefaultChunkDataProvider.java +++ b/leaf-server/src/main/java/dev/tr7zw/entityculling/DefaultChunkDataProvider.java @@ -30,10 +30,10 @@ public boolean isOpaqueFullCube(int x, int y, int z) { if (this.level.isOutsideBuildHeight(pos)) { BlockState bs = Blocks.VOID_AIR.defaultBlockState(); - return !bs.canOcclude(); + return !bs.canOcclude() && bs.isSolidRender(); } else { BlockState bs = access.getBlockState(pos); - return !bs.canOcclude(); + return !bs.canOcclude() && bs.isSolidRender(); } } From 49957ffcc3bfb74156160255083eca4972371e99 Mon Sep 17 00:00:00 2001 From: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> Date: Thu, 8 May 2025 04:36:47 +1400 Subject: [PATCH 10/12] Block change hook --- .../0165-Raytrace-Entity-Tracker.patch | 16 +++++- .../dev/tr7zw/entityculling/CullTask.java | 55 +++++++++++++++---- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch b/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch index 9347f84093..c90c6a0a88 100644 --- a/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch +++ b/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch @@ -86,7 +86,7 @@ index adeab8980b590e4a8b64b62cb60f623b2a842982..67ec36f61184f1a7f2304ba860982cd1 @Nullable private Component description; diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java -index 477455fdfcc591a89823e88983eb12dabb078d9b..24f6188f8996a41be5bbedbebabe392f99c5d41c 100644 +index 477455fdfcc591a89823e88983eb12dabb078d9b..b92f485c88e698fbf8cde5e6fc96dfba001cdc63 100644 --- a/net/minecraft/world/entity/player/Player.java +++ b/net/minecraft/world/entity/player/Player.java @@ -204,6 +204,11 @@ public abstract class Player extends LivingEntity { @@ -94,7 +94,7 @@ index 477455fdfcc591a89823e88983eb12dabb078d9b..24f6188f8996a41be5bbedbebabe392f public boolean canPortalInstant = false; // Purpur - Add portal permission bypass public int sixRowEnderchestSlotCount = -1; // Purpur - Barrels and enderchests 6 rows + // Leaf start - Raytrace Entity Tracker -+ @Nullable private dev.tr7zw.entityculling.CullTask cullTask; ++ @Nullable public dev.tr7zw.entityculling.CullTask cullTask; + @Nullable private Level lastCullWorld; + private final boolean needToCull = !(this instanceof org.leavesmc.leaves.replay.ServerPhotographer); + // Leaf end - Raytrace Entity Tracker @@ -152,3 +152,15 @@ index 477455fdfcc591a89823e88983eb12dabb078d9b..24f6188f8996a41be5bbedbebabe392f } public boolean isLocalPlayer() { +diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java +index 2518ea7a9d2122ff55ab6546d21d7ed6cc933090..e1f8d97fcb3cb38275b7d0c1b1db65d2f8742fe4 100644 +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -1199,6 +1199,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + // Imanity end - AntiXraySDK integration + this.chunkPacketBlockController.onBlockChange(this, pos, state, blockState, flags, recursionLeft); // Paper - Anti-Xray + ++ dev.tr7zw.entityculling.CullTask.onBlockChange(this, pos); // Leaf - Raytrace Entity Tracker + if (blockState == null) { + // CraftBukkit start - remove blockstate if failed (or the same) + if (this.captureBlockStates && captured) { diff --git a/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java b/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java index f190a00c26..6cdeec26c5 100644 --- a/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java +++ b/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java @@ -4,9 +4,13 @@ import com.logisticscraft.occlusionculling.OcclusionCullingInstance; import com.logisticscraft.occlusionculling.util.Vec3d; import dev.tr7zw.entityculling.versionless.access.Cullable; +import net.minecraft.core.BlockPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.players.PlayerList; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.decoration.ArmorStand; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; import org.dreeam.leaf.config.modules.misc.RaytraceTracker; @@ -24,8 +28,6 @@ public class CullTask implements Runnable { private final int hitboxLimit; - public long lastCheckedTime = 0; - private final Vec3d lastPos = new Vec3d(0, 0, 0); private final Vec3d aabbMin = new Vec3d(0, 0, 0); private final Vec3d aabbMax = new Vec3d(0, 0, 0); @@ -70,16 +72,13 @@ public synchronized void run() { try { if (this.checkTarget.tickCount > 10) { Vec3 cameraMC = this.checkTarget.getEyePosition(0); - long start = System.currentTimeMillis(); - if (!(cameraMC.x == lastPos.x && cameraMC.y == lastPos.y && cameraMC.z == lastPos.z) || (start - lastCheckedTime) > 2500) { - + if (!(cameraMC.x == lastPos.x && cameraMC.y == lastPos.y && cameraMC.z == lastPos.z)) { lastPos.set(cameraMC.x, cameraMC.y, cameraMC.z); - culling.resetCache(); - - cullEntities(cameraMC, lastPos); - - lastCheckedTime = System.currentTimeMillis() - start; + synchronized (culling) { + culling.resetCache(); + } } + cullEntities(cameraMC, lastPos); } } finally { if (this.scheduleNext) { @@ -126,10 +125,42 @@ private void cullEntities(Vec3 cameraMC, Vec3d camera) { aabbMin.set(boundingBox.minX, boundingBox.minY, boundingBox.minZ); aabbMax.set(boundingBox.maxX, boundingBox.maxY, boundingBox.maxZ); - boolean visible = culling.isAABBVisible(aabbMin, aabbMax, camera); + synchronized (culling) { + boolean visible = culling.isAABBVisible(aabbMin, aabbMax, camera); + + cullable.setCulled(!visible); + } + } + } + } - cullable.setCulled(!visible); + public static void onBlockChange(Level level, BlockPos pos) { + if (RaytraceTracker.enabled) { + MinecraftServer server = level.getServer(); + if (server == null) { // tbh this cant be null + return; } + PlayerList playerList = server.getPlayerList(); + CompletableFuture.runAsync(() -> { + for (Player player : playerList.realPlayers) { + CullTask cullTask = player.cullTask; + if (cullTask == null) continue; + if (player.level() == level) { + int posX = pos.getX(); + int posY = pos.getY(); + int posZ = pos.getZ(); + BlockPos playerPos = player.blockPosition(); + final int playerX = playerPos.getX(), playerY = playerPos.getY(), playerZ = playerPos.getZ(); + if (Math.abs(posX - playerX) < RaytraceTracker.maxTraceDistance + && Math.abs(posY - playerY) < RaytraceTracker.maxTraceDistance + && Math.abs(posZ - playerZ) < RaytraceTracker.maxTraceDistance) { + synchronized (cullTask.culling) { + cullTask.culling.resetCache(); + } + } + } + } + }, backgroundWorker); } } From ef697d5d525156c9cf7ff6bf22005e38474e5d8a Mon Sep 17 00:00:00 2001 From: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> Date: Fri, 9 May 2025 04:25:09 +1400 Subject: [PATCH 11/12] Per player tracing --- .../0165-Raytrace-Entity-Tracker.patch | 22 ++++++++------- .../dev/tr7zw/entityculling/CullTask.java | 28 +++++++++++++++---- .../versionless/access/Cullable.java | 6 ++-- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch b/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch index c90c6a0a88..34c4afd687 100644 --- a/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch +++ b/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch @@ -13,7 +13,7 @@ Ported by: https://github.com/LuminolMC/Luminol Fixed and updated by: https://github.com/Winds-Studio/Leaf diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java -index 546b20f8998c71ca1a701de7efcedd8d821105e4..3585f941d3edd15276967ca09e83a635760d2cae 100644 +index 546b20f8998c71ca1a701de7efcedd8d821105e4..b7a1e05d210068dc08889c1128497aa4b5fbddfd 100644 --- a/net/minecraft/server/level/ChunkMap.java +++ b/net/minecraft/server/level/ChunkMap.java @@ -1281,7 +1281,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider @@ -21,12 +21,12 @@ index 546b20f8998c71ca1a701de7efcedd8d821105e4..3585f941d3edd15276967ca09e83a635 double d2 = d * d; // Paper start - Configurable entity tracking range by Y - boolean flag = d1 <= d2; -+ boolean flag = d1 <= d2 && !entity.isCulled(); // Leaf - Raytrace Entity Tracker ++ boolean flag = d1 <= d2 && !entity.isCulled(player); // Leaf - Raytrace Entity Tracker if (flag && level.paperConfig().entities.trackingRangeY.enabled) { double rangeY = level.paperConfig().entities.trackingRangeY.get(this.entity, -1); if (rangeY != -1) { diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java -index 2786fbc43f238be058b153ba551f0a616c4a18f4..f69565586ef795daff48a1b75119db1d86d66b2e 100644 +index 2786fbc43f238be058b153ba551f0a616c4a18f4..5d8a32632905eef6f09a45615ef3e54f2bb91a44 100644 --- a/net/minecraft/world/entity/Entity.java +++ b/net/minecraft/world/entity/Entity.java @@ -133,7 +133,7 @@ import net.minecraft.world.scores.ScoreHolder; @@ -38,15 +38,13 @@ index 2786fbc43f238be058b153ba551f0a616c4a18f4..f69565586ef795daff48a1b75119db1d public static javax.script.ScriptEngine scriptEngine = new javax.script.ScriptEngineManager().getEngineByName("rhino"); // Purpur - Configurable entity base attributes // CraftBukkit start private static final int CURRENT_LEVEL = 2; -@@ -5291,4 +5291,34 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -5291,4 +5291,36 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess return this.distanceToSqr(entity.position()); } // Leaf end - Optimize Entity distanceToSqr + + // Leaf start - Raytrace entity tracker + private long lastTime = 0; -+ private volatile boolean culled = false; -+ private boolean outOfCamera = false; + + @Override + public void setTimeout() { @@ -59,17 +57,21 @@ index 2786fbc43f238be058b153ba551f0a616c4a18f4..f69565586ef795daff48a1b75119db1d + } + + @Override -+ public void setCulled(boolean value) { -+ this.culled = value; ++ public void setCulled(boolean value, Player player) { ++ dev.tr7zw.entityculling.CullTask task = player.cullTask; ++ if (task == null) return; ++ task.setCulled(this, value); + if (!value) { + setTimeout(); + } + } + + @Override -+ public boolean isCulled() { ++ public boolean isCulled(Player player) { + if (!org.dreeam.leaf.config.modules.misc.RaytraceTracker.enabled) return false; -+ return this.culled; ++ dev.tr7zw.entityculling.CullTask task = player.cullTask; ++ if (task == null) return false; ++ return task.isEntityCulled(this); + } + // Leaf end - Raytrace entity tracker } diff --git a/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java b/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java index 6cdeec26c5..10405d4fe1 100644 --- a/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java +++ b/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java @@ -15,6 +15,7 @@ import net.minecraft.world.phys.Vec3; import org.dreeam.leaf.config.modules.misc.RaytraceTracker; +import java.util.Set; import java.util.concurrent.*; public class CullTask implements Runnable { @@ -40,6 +41,8 @@ public class CullTask implements Runnable { .build() ); + private final Set culledEntities = ConcurrentHashMap.newKeySet(); + private final Executor worker; public CullTask( @@ -96,21 +99,22 @@ private void cullEntities(Vec3 cameraMC, Vec3d camera) { if (entity.getType().skipRaytraceCheck) { continue; } + Player player = this.checkTarget; - if (!cullable.isForcedVisible()) { + if (!cullable.isForcedVisible() || true) { // TODO if (entity.isCurrentlyGlowing() || isSkippableArmorstand(entity)) { - cullable.setCulled(false); + cullable.setCulled(false, player); continue; } final double distanceSqr = entity.position().distanceToSqr(cameraMC); if (distanceSqr < RaytraceTracker.forceVisibleRadius * RaytraceTracker.forceVisibleRadius) { - cullable.setCulled(false); + cullable.setCulled(false, player); continue; } if (distanceSqr >= RaytraceTracker.maxTraceDistance * RaytraceTracker.maxTraceDistance) { - cullable.setCulled(false); // If your entity view distance is larger than tracingDistance just + cullable.setCulled(false, player); // If your entity view distance is larger than tracingDistance just // render it continue; } @@ -118,7 +122,7 @@ private void cullEntities(Vec3 cameraMC, Vec3d camera) { AABB boundingBox = entity.getBoundingBox(); if (boundingBox.getXsize() > hitboxLimit || boundingBox.getYsize() > hitboxLimit || boundingBox.getZsize() > hitboxLimit) { - cullable.setCulled(false); // Too big to bother to cull + cullable.setCulled(false, player); // Too big to bother to cull continue; } @@ -128,7 +132,7 @@ private void cullEntities(Vec3 cameraMC, Vec3d camera) { synchronized (culling) { boolean visible = culling.isAABBVisible(aabbMin, aabbMax, camera); - cullable.setCulled(!visible); + cullable.setCulled(!visible, player); } } } @@ -168,4 +172,16 @@ private boolean isSkippableArmorstand(Entity entity) { if (!RaytraceTracker.skipMarkerArmorStand) return false; return entity instanceof ArmorStand && entity.isInvisible(); } + + public boolean isEntityCulled(Entity entity) { + return culledEntities.contains(entity.getId()); + } + + public void setCulled(Entity entity, boolean value) { + if (value) { + culledEntities.add(entity.getId()); + } else { + culledEntities.remove(entity.getId()); + } + } } diff --git a/leaf-server/src/main/java/dev/tr7zw/entityculling/versionless/access/Cullable.java b/leaf-server/src/main/java/dev/tr7zw/entityculling/versionless/access/Cullable.java index 667a2f0b09..3b0d09a905 100644 --- a/leaf-server/src/main/java/dev/tr7zw/entityculling/versionless/access/Cullable.java +++ b/leaf-server/src/main/java/dev/tr7zw/entityculling/versionless/access/Cullable.java @@ -1,13 +1,15 @@ package dev.tr7zw.entityculling.versionless.access; +import net.minecraft.world.entity.player.Player; + public interface Cullable { void setTimeout(); boolean isForcedVisible(); - void setCulled(boolean value); + void setCulled(boolean value, Player player); - boolean isCulled(); + boolean isCulled(Player player); } From 2f96972c552e63141f59db1b036d3b5c29b347a6 Mon Sep 17 00:00:00 2001 From: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> Date: Sat, 10 May 2025 04:15:18 +1400 Subject: [PATCH 12/12] Batch entity removal --- .../0165-Raytrace-Entity-Tracker.patch | 12 +++++++-- .../dev/tr7zw/entityculling/CullTask.java | 27 ++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch b/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch index 34c4afd687..50a03119b8 100644 --- a/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch +++ b/leaf-server/minecraft-patches/features/0165-Raytrace-Entity-Tracker.patch @@ -26,7 +26,7 @@ index 546b20f8998c71ca1a701de7efcedd8d821105e4..b7a1e05d210068dc08889c1128497aa4 double rangeY = level.paperConfig().entities.trackingRangeY.get(this.entity, -1); if (rangeY != -1) { diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java -index 2786fbc43f238be058b153ba551f0a616c4a18f4..5d8a32632905eef6f09a45615ef3e54f2bb91a44 100644 +index 2786fbc43f238be058b153ba551f0a616c4a18f4..5640069f67afa59d8238960258a549c6c84e6698 100644 --- a/net/minecraft/world/entity/Entity.java +++ b/net/minecraft/world/entity/Entity.java @@ -133,7 +133,7 @@ import net.minecraft.world.scores.ScoreHolder; @@ -38,7 +38,15 @@ index 2786fbc43f238be058b153ba551f0a616c4a18f4..5d8a32632905eef6f09a45615ef3e54f public static javax.script.ScriptEngine scriptEngine = new javax.script.ScriptEngineManager().getEngineByName("rhino"); // Purpur - Configurable entity base attributes // CraftBukkit start private static final int CURRENT_LEVEL = 2; -@@ -5291,4 +5291,36 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -5040,6 +5040,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return; + } + // Paper end - rewrite chunk system ++ dev.tr7zw.entityculling.CullTask.onEntityRemoval(this); // Leaf - Raytrace entity tracker + org.bukkit.craftbukkit.event.CraftEventFactory.callEntityRemoveEvent(this, cause); + // CraftBukkit end + final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers +@@ -5291,4 +5292,36 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess return this.distanceToSqr(entity.position()); } // Leaf end - Optimize Entity distanceToSqr diff --git a/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java b/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java index 10405d4fe1..3ec24488dd 100644 --- a/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java +++ b/leaf-server/src/main/java/dev/tr7zw/entityculling/CullTask.java @@ -4,6 +4,8 @@ import com.logisticscraft.occlusionculling.OcclusionCullingInstance; import com.logisticscraft.occlusionculling.util.Vec3d; import dev.tr7zw.entityculling.versionless.access.Cullable; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSets; import net.minecraft.core.BlockPos; import net.minecraft.server.MinecraftServer; import net.minecraft.server.players.PlayerList; @@ -15,6 +17,7 @@ import net.minecraft.world.phys.Vec3; import org.dreeam.leaf.config.modules.misc.RaytraceTracker; +import java.util.Queue; import java.util.Set; import java.util.concurrent.*; @@ -41,7 +44,9 @@ public class CullTask implements Runnable { .build() ); - private final Set culledEntities = ConcurrentHashMap.newKeySet(); + private static final Set tasks = ConcurrentHashMap.newKeySet(); + private final Set culledEntities = IntSets.synchronize(new IntOpenHashSet()); + private final Queue removalQueue = new ConcurrentLinkedQueue<>(); private final Executor worker; @@ -59,6 +64,7 @@ public CullTask( public void signalStop() { this.scheduleNext = false; + tasks.remove(this); } public void setup() { @@ -68,12 +74,17 @@ public void setup() { return; } this.worker.execute(this); + tasks.add(this); } @Override public synchronized void run() { try { if (this.checkTarget.tickCount > 10) { + while (!removalQueue.isEmpty()) { + int entityId = removalQueue.poll(); + culledEntities.remove(entityId); + } Vec3 cameraMC = this.checkTarget.getEyePosition(0); if (!(cameraMC.x == lastPos.x && cameraMC.y == lastPos.y && cameraMC.z == lastPos.z)) { lastPos.set(cameraMC.x, cameraMC.y, cameraMC.z); @@ -168,6 +179,20 @@ public static void onBlockChange(Level level, BlockPos pos) { } } + public static void onEntityRemoval(Entity entity) { + if (!RaytraceTracker.enabled) return; + int id = entity.getId(); + CompletableFuture.runAsync(() -> { + for (CullTask cullTask : tasks) { + cullTask.scheduleForRemoval(id); + } + }, backgroundWorker); + } + + public void scheduleForRemoval(int entityId) { + removalQueue.offer(entityId); + } + private boolean isSkippableArmorstand(Entity entity) { if (!RaytraceTracker.skipMarkerArmorStand) return false; return entity instanceof ArmorStand && entity.isInvisible();