From 428f213e153d397596f863a2f6ec1467b48d38d6 Mon Sep 17 00:00:00 2001 From: Axionize <154778082+Axionize@users.noreply.github.com> Date: Thu, 3 Oct 2024 01:23:39 -0400 Subject: [PATCH] Migrate reach-related functions to new BlockRayTrace util class --- .../impl/scaffolding/LineOfSightPlace.java | 7 +- .../events/packets/CheckManagerListener.java | 282 +---------------- .../grimac/utils/nmsutil/BlockRayTrace.java | 283 ++++++++++++++++++ 3 files changed, 291 insertions(+), 281 deletions(-) create mode 100644 src/main/java/ac/grim/grimac/utils/nmsutil/BlockRayTrace.java diff --git a/src/main/java/ac/grim/grimac/checks/impl/scaffolding/LineOfSightPlace.java b/src/main/java/ac/grim/grimac/checks/impl/scaffolding/LineOfSightPlace.java index 80af4481fb..4f02f08ca5 100644 --- a/src/main/java/ac/grim/grimac/checks/impl/scaffolding/LineOfSightPlace.java +++ b/src/main/java/ac/grim/grimac/checks/impl/scaffolding/LineOfSightPlace.java @@ -2,20 +2,17 @@ import ac.grim.grimac.checks.CheckData; import ac.grim.grimac.checks.type.BlockPlaceCheck; -import ac.grim.grimac.events.packets.CheckManagerListener; import ac.grim.grimac.player.GrimPlayer; import ac.grim.grimac.utils.anticheat.update.BlockPlace; import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox; -import ac.grim.grimac.utils.data.HitData; import ac.grim.grimac.utils.data.Pair; +import ac.grim.grimac.utils.nmsutil.BlockRayTrace; import com.github.retrooper.packetevents.protocol.attribute.Attributes; import com.github.retrooper.packetevents.protocol.player.ClientVersion; import com.github.retrooper.packetevents.protocol.player.GameMode; import com.github.retrooper.packetevents.protocol.world.BlockFace; import com.github.retrooper.packetevents.protocol.world.states.type.StateType; import com.github.retrooper.packetevents.protocol.world.states.type.StateTypes; -import com.github.retrooper.packetevents.util.Vector3i; -import jdk.internal.util.ArraysSupport; import java.util.*; @@ -171,7 +168,7 @@ private void calculateDirection(double[] result, double xRot, double yRot) { } private boolean getTargetBlock(double[] eyePosition, double[] eyeDirection, double maxDistance, int[] targetBlockVec, BlockFace expectedBlockFace) { - Pair hitData = CheckManagerListener.getNearestReachHitResult(player, eyePosition, eyeDirection, maxDistance, maxDistance, targetBlockVec, expectedBlockFace); + Pair hitData = BlockRayTrace.getNearestReachHitResult(player, eyePosition, eyeDirection, maxDistance, maxDistance, targetBlockVec, expectedBlockFace); // we check for hitdata != null because of being in expanded hitbox, or there was no result, do we still need this? return hitData != null && Arrays.equals(targetBlockVec, hitData.getFirst()) && hitData.getSecond() == expectedBlockFace; } diff --git a/src/main/java/ac/grim/grimac/events/packets/CheckManagerListener.java b/src/main/java/ac/grim/grimac/events/packets/CheckManagerListener.java index fb5cbee8a1..b162a027dd 100644 --- a/src/main/java/ac/grim/grimac/events/packets/CheckManagerListener.java +++ b/src/main/java/ac/grim/grimac/events/packets/CheckManagerListener.java @@ -8,14 +8,9 @@ import ac.grim.grimac.utils.anticheat.update.*; import ac.grim.grimac.utils.blockplace.BlockPlaceResult; import ac.grim.grimac.utils.blockplace.ConsumesBlockPlace; -import ac.grim.grimac.utils.collisions.HitboxData; -import ac.grim.grimac.utils.collisions.datatypes.CollisionBox; -import ac.grim.grimac.utils.collisions.datatypes.NoCollisionBox; -import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox; import ac.grim.grimac.utils.data.*; import ac.grim.grimac.utils.inventory.Inventory; import ac.grim.grimac.utils.latency.CompensatedWorld; -import ac.grim.grimac.utils.math.GrimMath; import ac.grim.grimac.utils.math.VectorUtils; import ac.grim.grimac.utils.nmsutil.*; import com.github.retrooper.packetevents.PacketEvents; @@ -25,7 +20,6 @@ import com.github.retrooper.packetevents.event.PacketSendEvent; import com.github.retrooper.packetevents.manager.server.ServerVersion; import com.github.retrooper.packetevents.protocol.ConnectionState; -import com.github.retrooper.packetevents.protocol.attribute.Attributes; import com.github.retrooper.packetevents.protocol.item.ItemStack; import com.github.retrooper.packetevents.protocol.item.type.ItemType; import com.github.retrooper.packetevents.protocol.item.type.ItemTypes; @@ -49,13 +43,6 @@ import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerAcknowledgeBlockChanges; import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetSlot; import io.github.retrooper.packetevents.util.SpigotConversionUtil; -import jdk.internal.util.ArraysSupport; -import org.bukkit.util.Vector; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.function.BiFunction; public class CheckManagerListener extends PacketListenerAbstract { @@ -63,152 +50,8 @@ public CheckManagerListener() { super(PacketListenerPriority.LOW); } - // Copied from MCP... - // Returns null if there isn't anything. - // - // I do have to admit that I'm starting to like bifunctions/new java 8 things more than I originally did. - // although I still don't understand Mojang's obsession with streams in some of the hottest methods... that kills performance - public static Pair traverseBlocksLOSP(GrimPlayer player, double[] start, double[] end, BiFunction> predicate) { - // I guess go back by the collision epsilon? - double endX = GrimMath.lerp(-1.0E-7D, end[0], start[0]); - double endY = GrimMath.lerp(-1.0E-7D, end[1], start[1]); - double endZ = GrimMath.lerp(-1.0E-7D, end[2], start[2]); - double startX = GrimMath.lerp(-1.0E-7D, start[0], end[0]); - double startY = GrimMath.lerp(-1.0E-7D, start[1], end[1]); - double startZ = GrimMath.lerp(-1.0E-7D, start[2], end[2]); - - int[] floorStart = new int[]{GrimMath.floor(startX), GrimMath.floor(startY), GrimMath.floor(startZ)}; - - if (start[0] == end[0] && start[1] == end[1] && start[2] == end[2]) return null; - - WrappedBlockState state = player.compensatedWorld.getWrappedBlockStateAt(floorStart[0], floorStart[1], floorStart[2]); - Pair apply = predicate.apply(state, floorStart); - - if (apply != null) { - return apply; - } - - double xDiff = endX - startX; - double yDiff = endY - startY; - double zDiff = endZ - startZ; - double xSign = Math.signum(xDiff); - double ySign = Math.signum(yDiff); - double zSign = Math.signum(zDiff); - - double posXInverse = xSign == 0 ? Double.MAX_VALUE : xSign / xDiff; - double posYInverse = ySign == 0 ? Double.MAX_VALUE : ySign / yDiff; - double posZInverse = zSign == 0 ? Double.MAX_VALUE : zSign / zDiff; - - double tMaxX = posXInverse * (xSign > 0 ? 1.0D - GrimMath.frac(startX) : GrimMath.frac(startX)); - double tMaxY = posYInverse * (ySign > 0 ? 1.0D - GrimMath.frac(startY) : GrimMath.frac(startY)); - double tMaxZ = posZInverse * (zSign > 0 ? 1.0D - GrimMath.frac(startZ) : GrimMath.frac(startZ)); - - // tMax represents the maximum distance along each axis before crossing a block boundary - // The loop continues as long as the ray hasn't reached its end point along at least one axis. - // In each iteration, it moves to the next block boundary along the axis with the smallest tMax value, - // updates the corresponding coordinate, and checks for a hit in the new block, Google "3D DDA" for more info - while (tMaxX <= 1.0D || tMaxY <= 1.0D || tMaxZ <= 1.0D) { - if (tMaxX < tMaxY) { - if (tMaxX < tMaxZ) { - floorStart[0] += xSign; - tMaxX += posXInverse; - } else { - floorStart[2] += zSign; - tMaxZ += posZInverse; - } - } else if (tMaxY < tMaxZ) { - floorStart[1] += ySign; - tMaxY += posYInverse; - } else { - floorStart[2] += zSign; - tMaxZ += posZInverse; - } - - state = player.compensatedWorld.getWrappedBlockStateAt(floorStart[0], floorStart[1], floorStart[2]); - apply = predicate.apply(state, floorStart); - - if (apply != null) { - return apply; - } - } - - return null; - } - - public static HitData traverseBlocks(GrimPlayer player, double[] start, double[] end, BiFunction predicate) { - // I guess go back by the collision epsilon? - double endX = GrimMath.lerp(-1.0E-7D, end[0], start[0]); - double endY = GrimMath.lerp(-1.0E-7D, end[1], start[1]); - double endZ = GrimMath.lerp(-1.0E-7D, end[2], start[2]); - double startX = GrimMath.lerp(-1.0E-7D, start[0], end[0]); - double startY = GrimMath.lerp(-1.0E-7D, start[1], end[1]); - double startZ = GrimMath.lerp(-1.0E-7D, start[2], end[2]); - int floorStartX = GrimMath.floor(startX); - int floorStartY = GrimMath.floor(startY); - int floorStartZ = GrimMath.floor(startZ); - - if (start[0] == end[0] && start[1] == end[1] && start[2] == end[2]) return null; - - WrappedBlockState state = player.compensatedWorld.getWrappedBlockStateAt(floorStartX, floorStartY, floorStartZ); - HitData apply = predicate.apply(state, new Vector3i(floorStartX, floorStartY, floorStartZ)); - - if (apply != null) { - return apply; - } - - double xDiff = endX - startX; - double yDiff = endY - startY; - double zDiff = endZ - startZ; - double xSign = Math.signum(xDiff); - double ySign = Math.signum(yDiff); - double zSign = Math.signum(zDiff); - - double posXInverse = xSign == 0 ? Double.MAX_VALUE : xSign / xDiff; - double posYInverse = ySign == 0 ? Double.MAX_VALUE : ySign / yDiff; - double posZInverse = zSign == 0 ? Double.MAX_VALUE : zSign / zDiff; - - double tMaxX = posXInverse * (xSign > 0 ? 1.0D - GrimMath.frac(startX) : GrimMath.frac(startX)); - double tMaxY = posYInverse * (ySign > 0 ? 1.0D - GrimMath.frac(startY) : GrimMath.frac(startY)); - double tMaxZ = posZInverse * (zSign > 0 ? 1.0D - GrimMath.frac(startZ) : GrimMath.frac(startZ)); - - // tMax represents the maximum distance along each axis before crossing a block boundary - // The loop continues as long as the ray hasn't reached its end point along at least one axis. - // In each iteration, it moves to the next block boundary along the axis with the smallest tMax value, - // updates the corresponding coordinate, and checks for a hit in the new block, Google "3D DDA" for more info - while (tMaxX <= 1.0D || tMaxY <= 1.0D || tMaxZ <= 1.0D) { - if (tMaxX < tMaxY) { - if (tMaxX < tMaxZ) { - floorStartX += xSign; - tMaxX += posXInverse; - } else { - floorStartZ += zSign; - tMaxZ += posZInverse; - } - } else if (tMaxY < tMaxZ) { - floorStartY += ySign; - tMaxY += posYInverse; - } else { - floorStartZ += zSign; - tMaxZ += posZInverse; - } - - state = player.compensatedWorld.getWrappedBlockStateAt(floorStartX, floorStartY, floorStartZ); - apply = predicate.apply(state, new Vector3i(floorStartX, floorStartY, floorStartZ)); - - if (apply != null) { - return apply; - } - } - - return null; - } - - public static HitData traverseBlocks(GrimPlayer player, Vector3d start, Vector3d end, BiFunction predicate) { - return traverseBlocks(player, new double[]{start.x, start.y, start.z}, new double[]{end.x, end.y, end.z}, predicate); - } - private static void placeWaterLavaSnowBucket(GrimPlayer player, ItemStack held, StateType toPlace, InteractionHand hand) { - HitData data = getNearestHitResult(player, StateTypes.AIR, false); + HitData data = BlockRayTrace.getNearestHitResult(player, StateTypes.AIR, false); if (data != null) { BlockPlace blockPlace = new BlockPlace(player, hand, data.getPosition(), data.getClosestDirection().getFaceValue(), data.getClosestDirection(), held, data); @@ -345,7 +188,7 @@ private static void handleBlockPlaceOrUseItem(PacketWrapper packet, GrimPlayer p // The offhand is unable to interact with blocks like this... try to stop some desync points before they happen if ((!player.isSneaking || onlyAir) && place.getHand() == InteractionHand.MAIN_HAND) { Vector3i blockPosition = place.getBlockPosition(); - BlockPlace blockPlace = new BlockPlace(player, place.getHand(), blockPosition, place.getFaceId(), place.getFace(), placedWith, getNearestHitResult(player, null, true)); + BlockPlace blockPlace = new BlockPlace(player, place.getHand(), blockPosition, place.getFaceId(), place.getFace(), placedWith, BlockRayTrace.getNearestHitResult(player, null, true)); // Right-clicking a trapdoor/door/etc. StateType placedAgainst = blockPlace.getPlacedAgainstMaterial(); @@ -385,7 +228,7 @@ private static void handleBlockPlaceOrUseItem(PacketWrapper packet, GrimPlayer p placedWith = player.getInventory().getOffHand(); } - BlockPlace blockPlace = new BlockPlace(player, place.getHand(), blockPosition, place.getFaceId(), face, placedWith, getNearestHitResult(player, null, true)); + BlockPlace blockPlace = new BlockPlace(player, place.getHand(), blockPosition, place.getFaceId(), face, placedWith, BlockRayTrace.getNearestHitResult(player, null, true)); // At this point, it is too late to cancel, so we can only flag, and cancel subsequent block places more aggressively if (!player.compensatedEntities.getSelf().inVehicle()) { player.checkManager.onPostFlyingBlockPlace(blockPlace); @@ -585,7 +428,7 @@ public void onPacketReceive(PacketReceiveEvent event) { player.placeUseItemPackets.add(new BlockPlaceSnapshot(packet, player.isSneaking)); } else { // Anti-air place - BlockPlace blockPlace = new BlockPlace(player, packet.getHand(), packet.getBlockPosition(), packet.getFaceId(), packet.getFace(), placedWith, getNearestHitResult(player, null, true)); + BlockPlace blockPlace = new BlockPlace(player, packet.getHand(), packet.getBlockPosition(), packet.getFaceId(), packet.getFace(), placedWith, BlockRayTrace.getNearestHitResult(player, null, true)); blockPlace.setCursor(packet.getCursorPosition()); if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_11) && player.getClientVersion().isOlderThan(ClientVersion.V_1_11)) { @@ -659,7 +502,7 @@ public void onPacketReceive(PacketReceiveEvent event) { } private static void placeBucket(GrimPlayer player, InteractionHand hand) { - HitData data = getNearestHitResult(player, null, true); + HitData data = BlockRayTrace.getNearestHitResult(player, null, true); if (data != null) { BlockPlace blockPlace = new BlockPlace(player, hand, data.getPosition(), data.getClosestDirection().getFaceValue(), data.getClosestDirection(), ItemStack.EMPTY, data); @@ -825,7 +668,7 @@ private void handleFlying(GrimPlayer player, double x, double y, double z, float } private static void placeLilypad(GrimPlayer player, InteractionHand hand) { - HitData data = getNearestHitResult(player, null, true); + HitData data = BlockRayTrace.getNearestHitResult(player, null, true); if (data != null) { // A lilypad cannot replace a fluid @@ -855,119 +698,6 @@ private static void placeLilypad(GrimPlayer player, InteractionHand hand) { } } - public static Pair getNearestReachHitResult(GrimPlayer player, double[] eyePos, double[] lookVec, double currentDistance, double maxDistance, int[] targetBlockVec, BlockFace expectedBlockFace) { - double[] endPos = new double[]{ - eyePos[0] + lookVec[0] * maxDistance, - eyePos[1] + lookVec[1] * maxDistance, - eyePos[2] + lookVec[2] * maxDistance - }; - - double[] currentEnd = new double[]{ - eyePos[0] + lookVec[0] * currentDistance, - eyePos[1] + lookVec[1] * currentDistance, - eyePos[2] + lookVec[2] * currentDistance - }; - - return traverseBlocksLOSP(player, eyePos, endPos, (block, vector3i) -> { - ClientVersion clientVersion = player.getClientVersion(); - CollisionBox data = HitboxData.getBlockHitbox(player, null, clientVersion, block, vector3i[0], vector3i[1], vector3i[2]); - if (data == NoCollisionBox.INSTANCE) return null; - List boxes = new ArrayList<>(); - data.downCast(boxes); - - double bestHitResult = Double.MAX_VALUE; - double[] bestHitLoc = null; - BlockFace bestFace = null; - - // BEWARE OF https://bugs.mojang.com/browse/MC-85109 FOR 1.8 PLAYERS - // 1.8 Brewing Stand hitbox is a fullblock until it is hit sometimes, can be caused be restarting client and joining server - if (block.getType() == StateTypes.BREWING_STAND && clientVersion.equals(ClientVersion.V_1_8) && Arrays.equals(vector3i, targetBlockVec)) { - boxes.add(new SimpleCollisionBox(0, 0, 0, 1, 1, 1, true)); - } - - currentEnd[0] = eyePos[0] + lookVec[0] * currentDistance; - currentEnd[1] = eyePos[1] + lookVec[1] * currentDistance; - currentEnd[2] = eyePos[2] + lookVec[2] * currentDistance; - - for (SimpleCollisionBox box : boxes) { - Pair intercept = ReachUtilsPrimitives.calculateIntercept(box, eyePos, currentEnd); - if (intercept.getFirst() == null) continue; // No intercept or wrong blockFace - - double[] hitLoc = intercept.getFirst(); - - double distSq = distanceSquared(hitLoc, eyePos); - if (distSq < bestHitResult) { - bestHitResult = distSq; - bestHitLoc = hitLoc; - bestFace = intercept.getSecond(); - } - } - - if (bestHitLoc != null) { - return new Pair<>(vector3i, bestFace); - } - - return null; - }); - } - - private static double distanceSquared(double[] vec1, double[] vec2) { - double dx = vec1[0] - vec2[0]; - double dy = vec1[1] - vec2[1]; - double dz = vec1[2] - vec2[2]; - return dx * dx + dy * dy + dz * dz; - } - - private static HitData getNearestHitResult(GrimPlayer player, StateType heldItem, boolean sourcesHaveHitbox) { - Vector3d startingPos = new Vector3d(player.x, player.y + player.getEyeHeight(), player.z); - Vector startingVec = new Vector(startingPos.getX(), startingPos.getY(), startingPos.getZ()); - Ray trace = new Ray(player, startingPos.getX(), startingPos.getY(), startingPos.getZ(), player.xRot, player.yRot); - final double distance = player.compensatedEntities.getSelf().getAttributeValue(Attributes.PLAYER_BLOCK_INTERACTION_RANGE); - Vector endVec = trace.getPointAtDistance(distance); - Vector3d endPos = new Vector3d(endVec.getX(), endVec.getY(), endVec.getZ()); - - return traverseBlocks(player, startingPos, endPos, (block, vector3i) -> { - CollisionBox data = HitboxData.getBlockHitbox(player, heldItem, player.getClientVersion(), block, vector3i.getX(), vector3i.getY(), vector3i.getZ()); - List boxes = new ArrayList<>(); - data.downCast(boxes); - - double bestHitResult = Double.MAX_VALUE; - Vector bestHitLoc = null; - BlockFace bestFace = null; - - for (SimpleCollisionBox box : boxes) { - Pair intercept = ReachUtils.calculateIntercept(box, trace.getOrigin(), trace.getPointAtDistance(distance)); - if (intercept.getFirst() == null) continue; // No intercept - - Vector hitLoc = intercept.getFirst(); - - if (hitLoc.distanceSquared(startingVec) < bestHitResult) { - bestHitResult = hitLoc.distanceSquared(startingVec); - bestHitLoc = hitLoc; - bestFace = intercept.getSecond(); - } - } - if (bestHitLoc != null) { - return new HitData(vector3i, bestHitLoc, bestFace, block); - } - - if (sourcesHaveHitbox && - (player.compensatedWorld.isWaterSourceBlock(vector3i.getX(), vector3i.getY(), vector3i.getZ()) - || player.compensatedWorld.getLavaFluidLevelAt(vector3i.getX(), vector3i.getY(), vector3i.getZ()) == (8 / 9f))) { - double waterHeight = player.compensatedWorld.getFluidLevelAt(vector3i.getX(), vector3i.getY(), vector3i.getZ()); - SimpleCollisionBox box = new SimpleCollisionBox(vector3i.getX(), vector3i.getY(), vector3i.getZ(), vector3i.getX() + 1, vector3i.getY() + waterHeight, vector3i.getZ() + 1); - - Pair intercept = ReachUtils.calculateIntercept(box, trace.getOrigin(), trace.getPointAtDistance(distance)); - - if (intercept.getFirst() != null) { - return new HitData(vector3i, intercept.getFirst(), intercept.getSecond(), block); - } - } - - return null; - }); - } - @Override public void onPacketSend(PacketSendEvent event) { if (event.getConnectionState() != ConnectionState.PLAY) return; diff --git a/src/main/java/ac/grim/grimac/utils/nmsutil/BlockRayTrace.java b/src/main/java/ac/grim/grimac/utils/nmsutil/BlockRayTrace.java new file mode 100644 index 0000000000..5f1cdfe7c3 --- /dev/null +++ b/src/main/java/ac/grim/grimac/utils/nmsutil/BlockRayTrace.java @@ -0,0 +1,283 @@ +package ac.grim.grimac.utils.nmsutil; + +import ac.grim.grimac.player.GrimPlayer; +import ac.grim.grimac.utils.collisions.HitboxData; +import ac.grim.grimac.utils.collisions.datatypes.CollisionBox; +import ac.grim.grimac.utils.collisions.datatypes.NoCollisionBox; +import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox; +import ac.grim.grimac.utils.data.HitData; +import ac.grim.grimac.utils.data.Pair; +import ac.grim.grimac.utils.math.GrimMath; +import com.github.retrooper.packetevents.protocol.attribute.Attributes; +import com.github.retrooper.packetevents.protocol.player.ClientVersion; +import com.github.retrooper.packetevents.protocol.world.BlockFace; +import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState; +import com.github.retrooper.packetevents.protocol.world.states.type.StateType; +import com.github.retrooper.packetevents.protocol.world.states.type.StateTypes; +import com.github.retrooper.packetevents.util.Vector3d; +import com.github.retrooper.packetevents.util.Vector3i; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiFunction; + +public class BlockRayTrace { + // Copied from MCP... + // Returns null if there isn't anything. + // + // I do have to admit that I'm starting to like bifunctions/new java 8 things more than I originally did. + // although I still don't understand Mojang's obsession with streams in some of the hottest methods... that kills performance + public static Pair traverseBlocksLOSP(GrimPlayer player, double[] start, double[] end, BiFunction> predicate) { + // I guess go back by the collision epsilon? + double endX = GrimMath.lerp(-1.0E-7D, end[0], start[0]); + double endY = GrimMath.lerp(-1.0E-7D, end[1], start[1]); + double endZ = GrimMath.lerp(-1.0E-7D, end[2], start[2]); + double startX = GrimMath.lerp(-1.0E-7D, start[0], end[0]); + double startY = GrimMath.lerp(-1.0E-7D, start[1], end[1]); + double startZ = GrimMath.lerp(-1.0E-7D, start[2], end[2]); + + int[] floorStart = new int[]{GrimMath.floor(startX), GrimMath.floor(startY), GrimMath.floor(startZ)}; + + if (start[0] == end[0] && start[1] == end[1] && start[2] == end[2]) return null; + + WrappedBlockState state = player.compensatedWorld.getWrappedBlockStateAt(floorStart[0], floorStart[1], floorStart[2]); + Pair apply = predicate.apply(state, floorStart); + + if (apply != null) { + return apply; + } + + double xDiff = endX - startX; + double yDiff = endY - startY; + double zDiff = endZ - startZ; + double xSign = Math.signum(xDiff); + double ySign = Math.signum(yDiff); + double zSign = Math.signum(zDiff); + + double posXInverse = xSign == 0 ? Double.MAX_VALUE : xSign / xDiff; + double posYInverse = ySign == 0 ? Double.MAX_VALUE : ySign / yDiff; + double posZInverse = zSign == 0 ? Double.MAX_VALUE : zSign / zDiff; + + double tMaxX = posXInverse * (xSign > 0 ? 1.0D - GrimMath.frac(startX) : GrimMath.frac(startX)); + double tMaxY = posYInverse * (ySign > 0 ? 1.0D - GrimMath.frac(startY) : GrimMath.frac(startY)); + double tMaxZ = posZInverse * (zSign > 0 ? 1.0D - GrimMath.frac(startZ) : GrimMath.frac(startZ)); + + // tMax represents the maximum distance along each axis before crossing a block boundary + // The loop continues as long as the ray hasn't reached its end point along at least one axis. + // In each iteration, it moves to the next block boundary along the axis with the smallest tMax value, + // updates the corresponding coordinate, and checks for a hit in the new block, Google "3D DDA" for more info + while (tMaxX <= 1.0D || tMaxY <= 1.0D || tMaxZ <= 1.0D) { + if (tMaxX < tMaxY) { + if (tMaxX < tMaxZ) { + floorStart[0] += xSign; + tMaxX += posXInverse; + } else { + floorStart[2] += zSign; + tMaxZ += posZInverse; + } + } else if (tMaxY < tMaxZ) { + floorStart[1] += ySign; + tMaxY += posYInverse; + } else { + floorStart[2] += zSign; + tMaxZ += posZInverse; + } + + state = player.compensatedWorld.getWrappedBlockStateAt(floorStart[0], floorStart[1], floorStart[2]); + apply = predicate.apply(state, floorStart); + + if (apply != null) { + return apply; + } + } + + return null; + } + + public static HitData traverseBlocks(GrimPlayer player, double[] start, double[] end, BiFunction predicate) { + // I guess go back by the collision epsilon? + double endX = GrimMath.lerp(-1.0E-7D, end[0], start[0]); + double endY = GrimMath.lerp(-1.0E-7D, end[1], start[1]); + double endZ = GrimMath.lerp(-1.0E-7D, end[2], start[2]); + double startX = GrimMath.lerp(-1.0E-7D, start[0], end[0]); + double startY = GrimMath.lerp(-1.0E-7D, start[1], end[1]); + double startZ = GrimMath.lerp(-1.0E-7D, start[2], end[2]); + int floorStartX = GrimMath.floor(startX); + int floorStartY = GrimMath.floor(startY); + int floorStartZ = GrimMath.floor(startZ); + + if (start[0] == end[0] && start[1] == end[1] && start[2] == end[2]) return null; + + WrappedBlockState state = player.compensatedWorld.getWrappedBlockStateAt(floorStartX, floorStartY, floorStartZ); + HitData apply = predicate.apply(state, new Vector3i(floorStartX, floorStartY, floorStartZ)); + + if (apply != null) { + return apply; + } + + double xDiff = endX - startX; + double yDiff = endY - startY; + double zDiff = endZ - startZ; + double xSign = Math.signum(xDiff); + double ySign = Math.signum(yDiff); + double zSign = Math.signum(zDiff); + + double posXInverse = xSign == 0 ? Double.MAX_VALUE : xSign / xDiff; + double posYInverse = ySign == 0 ? Double.MAX_VALUE : ySign / yDiff; + double posZInverse = zSign == 0 ? Double.MAX_VALUE : zSign / zDiff; + + double tMaxX = posXInverse * (xSign > 0 ? 1.0D - GrimMath.frac(startX) : GrimMath.frac(startX)); + double tMaxY = posYInverse * (ySign > 0 ? 1.0D - GrimMath.frac(startY) : GrimMath.frac(startY)); + double tMaxZ = posZInverse * (zSign > 0 ? 1.0D - GrimMath.frac(startZ) : GrimMath.frac(startZ)); + + // tMax represents the maximum distance along each axis before crossing a block boundary + // The loop continues as long as the ray hasn't reached its end point along at least one axis. + // In each iteration, it moves to the next block boundary along the axis with the smallest tMax value, + // updates the corresponding coordinate, and checks for a hit in the new block, Google "3D DDA" for more info + while (tMaxX <= 1.0D || tMaxY <= 1.0D || tMaxZ <= 1.0D) { + if (tMaxX < tMaxY) { + if (tMaxX < tMaxZ) { + floorStartX += xSign; + tMaxX += posXInverse; + } else { + floorStartZ += zSign; + tMaxZ += posZInverse; + } + } else if (tMaxY < tMaxZ) { + floorStartY += ySign; + tMaxY += posYInverse; + } else { + floorStartZ += zSign; + tMaxZ += posZInverse; + } + + state = player.compensatedWorld.getWrappedBlockStateAt(floorStartX, floorStartY, floorStartZ); + apply = predicate.apply(state, new Vector3i(floorStartX, floorStartY, floorStartZ)); + + if (apply != null) { + return apply; + } + } + + return null; + } + + public static HitData traverseBlocks(GrimPlayer player, Vector3d start, Vector3d end, BiFunction predicate) { + return traverseBlocks(player, new double[]{start.x, start.y, start.z}, new double[]{end.x, end.y, end.z}, predicate); + } + + public static Pair getNearestReachHitResult(GrimPlayer player, double[] eyePos, double[] lookVec, double currentDistance, double maxDistance, int[] targetBlockVec, BlockFace expectedBlockFace) { + double[] endPos = new double[]{ + eyePos[0] + lookVec[0] * maxDistance, + eyePos[1] + lookVec[1] * maxDistance, + eyePos[2] + lookVec[2] * maxDistance + }; + + double[] currentEnd = new double[]{ + eyePos[0] + lookVec[0] * currentDistance, + eyePos[1] + lookVec[1] * currentDistance, + eyePos[2] + lookVec[2] * currentDistance + }; + + return traverseBlocksLOSP(player, eyePos, endPos, (block, vector3i) -> { + ClientVersion clientVersion = player.getClientVersion(); + CollisionBox data = HitboxData.getBlockHitbox(player, null, clientVersion, block, vector3i[0], vector3i[1], vector3i[2]); + if (data == NoCollisionBox.INSTANCE) return null; + List boxes = new ArrayList<>(); + data.downCast(boxes); + + double bestHitResult = Double.MAX_VALUE; + double[] bestHitLoc = null; + BlockFace bestFace = null; + + // BEWARE OF https://bugs.mojang.com/browse/MC-85109 FOR 1.8 PLAYERS + // 1.8 Brewing Stand hitbox is a fullblock until it is hit sometimes, can be caused be restarting client and joining server + if (block.getType() == StateTypes.BREWING_STAND && clientVersion.equals(ClientVersion.V_1_8) && Arrays.equals(vector3i, targetBlockVec)) { + boxes.add(new SimpleCollisionBox(0, 0, 0, 1, 1, 1, true)); + } + + currentEnd[0] = eyePos[0] + lookVec[0] * currentDistance; + currentEnd[1] = eyePos[1] + lookVec[1] * currentDistance; + currentEnd[2] = eyePos[2] + lookVec[2] * currentDistance; + + for (SimpleCollisionBox box : boxes) { + Pair intercept = ReachUtilsPrimitives.calculateIntercept(box, eyePos, currentEnd); + if (intercept.getFirst() == null) continue; // No intercept or wrong blockFace + + double[] hitLoc = intercept.getFirst(); + + double distSq = distanceSquared(hitLoc, eyePos); + if (distSq < bestHitResult) { + bestHitResult = distSq; + bestHitLoc = hitLoc; + bestFace = intercept.getSecond(); + } + } + + if (bestHitLoc != null) { + return new Pair<>(vector3i, bestFace); + } + + return null; + }); + } + + private static double distanceSquared(double[] vec1, double[] vec2) { + double dx = vec1[0] - vec2[0]; + double dy = vec1[1] - vec2[1]; + double dz = vec1[2] - vec2[2]; + return dx * dx + dy * dy + dz * dz; + } + + public static HitData getNearestHitResult(GrimPlayer player, StateType heldItem, boolean sourcesHaveHitbox) { + Vector3d startingPos = new Vector3d(player.x, player.y + player.getEyeHeight(), player.z); + Vector startingVec = new Vector(startingPos.getX(), startingPos.getY(), startingPos.getZ()); + Ray trace = new Ray(player, startingPos.getX(), startingPos.getY(), startingPos.getZ(), player.xRot, player.yRot); + final double distance = player.compensatedEntities.getSelf().getAttributeValue(Attributes.PLAYER_BLOCK_INTERACTION_RANGE); + Vector endVec = trace.getPointAtDistance(distance); + Vector3d endPos = new Vector3d(endVec.getX(), endVec.getY(), endVec.getZ()); + + return traverseBlocks(player, startingPos, endPos, (block, vector3i) -> { + CollisionBox data = HitboxData.getBlockHitbox(player, heldItem, player.getClientVersion(), block, vector3i.getX(), vector3i.getY(), vector3i.getZ()); + List boxes = new ArrayList<>(); + data.downCast(boxes); + + double bestHitResult = Double.MAX_VALUE; + Vector bestHitLoc = null; + BlockFace bestFace = null; + + for (SimpleCollisionBox box : boxes) { + Pair intercept = ReachUtils.calculateIntercept(box, trace.getOrigin(), trace.getPointAtDistance(distance)); + if (intercept.getFirst() == null) continue; // No intercept + + Vector hitLoc = intercept.getFirst(); + + if (hitLoc.distanceSquared(startingVec) < bestHitResult) { + bestHitResult = hitLoc.distanceSquared(startingVec); + bestHitLoc = hitLoc; + bestFace = intercept.getSecond(); + } + } + if (bestHitLoc != null) { + return new HitData(vector3i, bestHitLoc, bestFace, block); + } + + if (sourcesHaveHitbox && + (player.compensatedWorld.isWaterSourceBlock(vector3i.getX(), vector3i.getY(), vector3i.getZ()) + || player.compensatedWorld.getLavaFluidLevelAt(vector3i.getX(), vector3i.getY(), vector3i.getZ()) == (8 / 9f))) { + double waterHeight = player.compensatedWorld.getFluidLevelAt(vector3i.getX(), vector3i.getY(), vector3i.getZ()); + SimpleCollisionBox box = new SimpleCollisionBox(vector3i.getX(), vector3i.getY(), vector3i.getZ(), vector3i.getX() + 1, vector3i.getY() + waterHeight, vector3i.getZ() + 1); + + Pair intercept = ReachUtils.calculateIntercept(box, trace.getOrigin(), trace.getPointAtDistance(distance)); + + if (intercept.getFirst() != null) { + return new HitData(vector3i, intercept.getFirst(), intercept.getSecond(), block); + } + } + + return null; + }); + } +}