Skip to content

Commit 335c33b

Browse files
committed
Merge reach wall hit check
1 parent 104d1ec commit 335c33b

File tree

4 files changed

+258
-136
lines changed

4 files changed

+258
-136
lines changed

src/main/java/ac/grim/grimac/checks/impl/combat/Reach.java

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
package ac.grim.grimac.checks.impl.combat;
1717

18+
import ac.grim.grimac.GrimAPI;
1819
import ac.grim.grimac.checks.Check;
1920
import ac.grim.grimac.checks.CheckData;
2021
import ac.grim.grimac.checks.type.PacketCheck;
2122
import ac.grim.grimac.player.GrimPlayer;
2223
import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
24+
import ac.grim.grimac.utils.data.HitData;
25+
import ac.grim.grimac.utils.data.Pair;
2326
import ac.grim.grimac.utils.data.packetentity.PacketEntity;
27+
import ac.grim.grimac.utils.nmsutil.BlockRayTrace;
2428
import ac.grim.grimac.utils.data.packetentity.dragon.PacketEntityEnderDragonPart;
2529
import ac.grim.grimac.utils.nmsutil.ReachUtils;
2630
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
@@ -30,19 +34,32 @@
3034
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
3135
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
3236
import com.github.retrooper.packetevents.protocol.player.GameMode;
37+
import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState;
3338
import com.github.retrooper.packetevents.util.Vector3d;
39+
import com.github.retrooper.packetevents.util.Vector3i;
3440
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
3541
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerFlying;
3642
import org.bukkit.util.Vector;
43+
import org.jetbrains.annotations.Nullable;
3744

38-
import java.util.*;
45+
import java.util.ArrayList;
46+
import java.util.Arrays;
47+
import java.util.Collections;
48+
import java.util.HashMap;
49+
import java.util.HashSet;
50+
import java.util.List;
51+
import java.util.Map;
52+
import java.util.Set;
3953

4054
// You may not copy the check unless you are licensed under GPL
4155
@CheckData(name = "Reach", configName = "Reach", setback = 10)
4256
public class Reach extends Check implements PacketCheck {
4357
// Only one flag per reach attack, per entity, per tick.
4458
// We store position because lastX isn't reliable on teleports.
4559
private final Map<Integer, Vector3d> playerAttackQueue = new HashMap<>();
60+
// Used to prevent falses in the wall hit check
61+
private final Set<Vector3i> blocksChangedThisTick = new HashSet<>();
62+
4663
private static final List<EntityType> blacklisted = Arrays.asList(
4764
EntityTypes.BOAT,
4865
EntityTypes.CHEST_BOAT,
@@ -105,10 +122,11 @@ public void onPacketReceive(final PacketReceiveEvent event) {
105122
}
106123

107124
// If the player set their look, or we know they have a new tick
108-
if (WrapperPlayClientPlayerFlying.isFlying(event.getPacketType()) ||
125+
final boolean isFlying = WrapperPlayClientPlayerFlying.isFlying(event.getPacketType());
126+
if (isFlying ||
109127
event.getPacketType() == PacketType.Play.Client.PONG ||
110128
event.getPacketType() == PacketType.Play.Client.WINDOW_CONFIRMATION) {
111-
tickBetterReachCheckWithAngle();
129+
tickBetterReachCheckWithAngle(isFlying);
112130
}
113131
}
114132

@@ -140,7 +158,7 @@ private boolean isKnownInvalid(PacketEntity reachEntity) {
140158
}
141159
}
142160

143-
private void tickBetterReachCheckWithAngle() {
161+
private void tickBetterReachCheckWithAngle(boolean isFlying) {
144162
for (Map.Entry<Integer, Vector3d> attack : playerAttackQueue.entrySet()) {
145163
PacketEntity reachEntity = player.compensatedEntities.entityMap.get(attack.getKey().intValue());
146164
if (reachEntity != null) {
@@ -155,6 +173,9 @@ private void tickBetterReachCheckWithAngle() {
155173
}
156174
}
157175
playerAttackQueue.clear();
176+
// We can't use transactions for this because of this problem:
177+
// transaction -> block changed applied -> 2nd transaction -> list cleared -> attack packet -> flying -> reach block hit checked, falses
178+
if (isFlying) blocksChangedThisTick.clear();
158179
}
159180

160181
private String checkReach(PacketEntity reachEntity, Vector3d from, boolean isPrediction) {
@@ -219,9 +240,26 @@ private String checkReach(PacketEntity reachEntity, Vector3d from, boolean isPre
219240
}
220241
}
221242

243+
final boolean experimentalChecks = true; //GrimAPI.INSTANCE.getConfigManager().isExperimentalChecks(); // TODO fix for undraft
244+
HitData foundHitData = null;
245+
// If the entity is within range of the player (we'll flag anyway if not, so no point checking blocks in this case)
246+
// Ignore when could be hitting through a moving shulker, piston blocks. They are just too glitchy/uncertain to check.
247+
if (experimentalChecks && minDistance <= 3 && !player.compensatedWorld.isNearHardEntity(player.boundingBox.copy().expand(4))) {
248+
final @Nullable Pair<Double, HitData> targetBlock = getTargetBlock(player, possibleLookDirs, from, minDistance);
249+
// And if the target block is closer to the player than the entity box, they should hit the block instead
250+
// So, this hit is invalid.
251+
if (targetBlock != null && targetBlock.getFirst() < (minDistance * minDistance)) { // targetBlock is squared
252+
minDistance = Double.MIN_VALUE;
253+
foundHitData = targetBlock.getSecond();
254+
}
255+
}
256+
222257
// if the entity is not exempt and the entity is alive
223258
if ((!blacklisted.contains(reachEntity.getType()) && reachEntity.isLivingEntity()) || reachEntity.getType() == EntityTypes.END_CRYSTAL) {
224-
if (minDistance == Double.MAX_VALUE) {
259+
if (minDistance == Double.MIN_VALUE && foundHitData != null) {
260+
cancelBuffer = 1;
261+
return "Hit block block=" + foundHitData.getState().getType().getName();
262+
} else if (minDistance == Double.MAX_VALUE) {
225263
cancelBuffer = 1;
226264
return "Missed hitbox";
227265
} else if (minDistance > player.compensatedEntities.getSelf().getAttributeValue(Attributes.PLAYER_ENTITY_INTERACTION_RANGE)) {
@@ -235,6 +273,45 @@ private String checkReach(PacketEntity reachEntity, Vector3d from, boolean isPre
235273
return null;
236274
}
237275

276+
public void handleBlockChange(Vector3i vector3i, WrappedBlockState state) {
277+
if (blocksChangedThisTick.size() >= 40) return; // Don't let players freeze movement packets to grow this
278+
// Only do this for nearby blocks
279+
if (new Vector(vector3i.x, vector3i.y, vector3i.z).distanceSquared(new Vector(player.x, player.y, player.z)) > 6) return;
280+
// Only do this if the state really had any world impact
281+
if (state.equals(player.compensatedWorld.getWrappedBlockStateAt(vector3i))) return;
282+
blocksChangedThisTick.add(vector3i);
283+
}
284+
285+
// Returns a pair so we can check the block type in the flag
286+
@Nullable
287+
private Pair<Double, HitData> getTargetBlock(GrimPlayer player, List<Vector> possibleLookDirs, Vector3d from, double minDistance) {
288+
// Check every possible look direction and every possible eye height
289+
// IF *NONE* of them allow the player to hit the entity, this is an invalid hit
290+
HitData bestHitData = null;
291+
double min = Double.MAX_VALUE;
292+
for (Vector lookVec : possibleLookDirs) {
293+
for (double eye : player.getPossibleEyeHeights()) {
294+
Vector eyes = new Vector(from.getX(), from.getY() + eye, from.getZ());
295+
final double reach = player.compensatedEntities.getSelf().getAttributeValue(Attributes.PLAYER_BLOCK_INTERACTION_RANGE);
296+
final HitData hitResult = BlockRayTrace.getNearestReachHitResult(player, eyes, lookVec, minDistance, reach);
297+
if (hitResult == null) {
298+
return null;
299+
}
300+
301+
final double distance = eyes.distanceSquared(hitResult.getBlockHitLocation());
302+
// Block changes are uncertain, can't check this tick
303+
if (distance < (minDistance * minDistance) && blocksChangedThisTick.contains(hitResult.getPosition())) {
304+
return null;
305+
}
306+
307+
bestHitData = hitResult;
308+
min = Math.min(min, distance);
309+
}
310+
}
311+
312+
return bestHitData == null ? null : Pair.of(min, bestHitData);
313+
}
314+
238315
@Override
239316
public void reload() {
240317
super.reload();

src/main/java/ac/grim/grimac/events/packets/CheckManagerListener.java

Lines changed: 7 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import ac.grim.grimac.utils.data.*;
1515
import ac.grim.grimac.utils.inventory.Inventory;
1616
import ac.grim.grimac.utils.latency.CompensatedWorld;
17-
import ac.grim.grimac.utils.math.GrimMath;
1817
import ac.grim.grimac.utils.math.VectorUtils;
1918
import ac.grim.grimac.utils.nmsutil.*;
2019
import com.github.retrooper.packetevents.PacketEvents;
@@ -47,91 +46,18 @@
4746
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerAcknowledgeBlockChanges;
4847
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetSlot;
4948
import io.github.retrooper.packetevents.util.SpigotConversionUtil;
50-
import org.bukkit.util.Vector;
51-
5249
import java.util.ArrayList;
5350
import java.util.List;
54-
import java.util.function.BiFunction;
51+
import org.bukkit.util.Vector;
5552

5653
public class CheckManagerListener extends PacketListenerAbstract {
5754

5855
public CheckManagerListener() {
5956
super(PacketListenerPriority.LOW);
6057
}
6158

62-
// Copied from MCP...
63-
// Returns null if there isn't anything.
64-
//
65-
// I do have to admit that I'm starting to like bifunctions/new java 8 things more than I originally did.
66-
// although I still don't understand Mojang's obsession with streams in some of the hottest methods... that kills performance
67-
public static HitData traverseBlocks(GrimPlayer player, Vector3d start, Vector3d end, BiFunction<WrappedBlockState, Vector3i, HitData> predicate) {
68-
// I guess go back by the collision epsilon?
69-
double endX = GrimMath.lerp(-1.0E-7D, end.x, start.x);
70-
double endY = GrimMath.lerp(-1.0E-7D, end.y, start.y);
71-
double endZ = GrimMath.lerp(-1.0E-7D, end.z, start.z);
72-
double startX = GrimMath.lerp(-1.0E-7D, start.x, end.x);
73-
double startY = GrimMath.lerp(-1.0E-7D, start.y, end.y);
74-
double startZ = GrimMath.lerp(-1.0E-7D, start.z, end.z);
75-
int floorStartX = GrimMath.floor(startX);
76-
int floorStartY = GrimMath.floor(startY);
77-
int floorStartZ = GrimMath.floor(startZ);
78-
79-
80-
if (start.equals(end)) return null;
81-
82-
WrappedBlockState state = player.compensatedWorld.getWrappedBlockStateAt(floorStartX, floorStartY, floorStartZ);
83-
HitData apply = predicate.apply(state, new Vector3i(floorStartX, floorStartY, floorStartZ));
84-
85-
if (apply != null) {
86-
return apply;
87-
}
88-
89-
double xDiff = endX - startX;
90-
double yDiff = endY - startY;
91-
double zDiff = endZ - startZ;
92-
double xSign = Math.signum(xDiff);
93-
double ySign = Math.signum(yDiff);
94-
double zSign = Math.signum(zDiff);
95-
96-
double posXInverse = xSign == 0 ? Double.MAX_VALUE : xSign / xDiff;
97-
double posYInverse = ySign == 0 ? Double.MAX_VALUE : ySign / yDiff;
98-
double posZInverse = zSign == 0 ? Double.MAX_VALUE : zSign / zDiff;
99-
100-
double d12 = posXInverse * (xSign > 0 ? 1.0D - GrimMath.frac(startX) : GrimMath.frac(startX));
101-
double d13 = posYInverse * (ySign > 0 ? 1.0D - GrimMath.frac(startY) : GrimMath.frac(startY));
102-
double d14 = posZInverse * (zSign > 0 ? 1.0D - GrimMath.frac(startZ) : GrimMath.frac(startZ));
103-
104-
// Can't figure out what this code does currently
105-
while (d12 <= 1.0D || d13 <= 1.0D || d14 <= 1.0D) {
106-
if (d12 < d13) {
107-
if (d12 < d14) {
108-
floorStartX += xSign;
109-
d12 += posXInverse;
110-
} else {
111-
floorStartZ += zSign;
112-
d14 += posZInverse;
113-
}
114-
} else if (d13 < d14) {
115-
floorStartY += ySign;
116-
d13 += posYInverse;
117-
} else {
118-
floorStartZ += zSign;
119-
d14 += posZInverse;
120-
}
121-
122-
state = player.compensatedWorld.getWrappedBlockStateAt(floorStartX, floorStartY, floorStartZ);
123-
apply = predicate.apply(state, new Vector3i(floorStartX, floorStartY, floorStartZ));
124-
125-
if (apply != null) {
126-
return apply;
127-
}
128-
}
129-
130-
return null;
131-
}
132-
13359
private static void placeWaterLavaSnowBucket(GrimPlayer player, ItemStack held, StateType toPlace, InteractionHand hand) {
134-
HitData data = getNearestHitResult(player, StateTypes.AIR, false);
60+
HitData data = BlockRayTrace.getNearestHitResult(player, StateTypes.AIR, false);
13561
if (data != null) {
13662
BlockPlace blockPlace = new BlockPlace(player, hand, data.getPosition(), data.getClosestDirection().getFaceValue(), data.getClosestDirection(), held, data);
13763

@@ -268,7 +194,7 @@ private static void handleBlockPlaceOrUseItem(PacketWrapper packet, GrimPlayer p
268194
// The offhand is unable to interact with blocks like this... try to stop some desync points before they happen
269195
if ((!player.isSneaking || onlyAir) && place.getHand() == InteractionHand.MAIN_HAND) {
270196
Vector3i blockPosition = place.getBlockPosition();
271-
BlockPlace blockPlace = new BlockPlace(player, place.getHand(), blockPosition, place.getFaceId(), place.getFace(), placedWith, getNearestHitResult(player, null, true));
197+
BlockPlace blockPlace = new BlockPlace(player, place.getHand(), blockPosition, place.getFaceId(), place.getFace(), placedWith, BlockRayTrace.getNearestHitResult(player, null, true));
272198

273199
// Right-clicking a trapdoor/door/etc.
274200
StateType placedAgainst = blockPlace.getPlacedAgainstMaterial();
@@ -308,7 +234,7 @@ private static void handleBlockPlaceOrUseItem(PacketWrapper packet, GrimPlayer p
308234
placedWith = player.getInventory().getOffHand();
309235
}
310236

311-
BlockPlace blockPlace = new BlockPlace(player, place.getHand(), blockPosition, place.getFaceId(), face, placedWith, getNearestHitResult(player, null, true));
237+
BlockPlace blockPlace = new BlockPlace(player, place.getHand(), blockPosition, place.getFaceId(), face, placedWith, BlockRayTrace.getNearestHitResult(player, null, true));
312238
// At this point, it is too late to cancel, so we can only flag, and cancel subsequent block places more aggressively
313239
if (!player.compensatedEntities.getSelf().inVehicle()) {
314240
player.checkManager.onPostFlyingBlockPlace(blockPlace);
@@ -508,7 +434,7 @@ public void onPacketReceive(PacketReceiveEvent event) {
508434
player.placeUseItemPackets.add(new BlockPlaceSnapshot(packet, player.isSneaking));
509435
} else {
510436
// Anti-air place
511-
BlockPlace blockPlace = new BlockPlace(player, packet.getHand(), packet.getBlockPosition(), packet.getFaceId(), packet.getFace(), placedWith, getNearestHitResult(player, null, true));
437+
BlockPlace blockPlace = new BlockPlace(player, packet.getHand(), packet.getBlockPosition(), packet.getFaceId(), packet.getFace(), placedWith, BlockRayTrace.getNearestHitResult(player, null, true));
512438
blockPlace.setCursor(packet.getCursorPosition());
513439

514440
if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_11) && player.getClientVersion().isOlderThan(ClientVersion.V_1_11)) {
@@ -582,7 +508,7 @@ public void onPacketReceive(PacketReceiveEvent event) {
582508
}
583509

584510
private static void placeBucket(GrimPlayer player, InteractionHand hand) {
585-
HitData data = getNearestHitResult(player, null, true);
511+
HitData data = BlockRayTrace.getNearestHitResult(player, null, true);
586512

587513
if (data != null) {
588514
BlockPlace blockPlace = new BlockPlace(player, hand, data.getPosition(), data.getClosestDirection().getFaceValue(), data.getClosestDirection(), ItemStack.EMPTY, data);
@@ -748,7 +674,7 @@ private void handleFlying(GrimPlayer player, double x, double y, double z, float
748674
}
749675

750676
private static void placeLilypad(GrimPlayer player, InteractionHand hand) {
751-
HitData data = getNearestHitResult(player, null, true);
677+
HitData data = BlockRayTrace.getNearestHitResult(player, null, true);
752678

753679
if (data != null) {
754680
// A lilypad cannot replace a fluid
@@ -778,56 +704,6 @@ private static void placeLilypad(GrimPlayer player, InteractionHand hand) {
778704
}
779705
}
780706

781-
private static HitData getNearestHitResult(GrimPlayer player, StateType heldItem, boolean sourcesHaveHitbox) {
782-
Vector3d startingPos = new Vector3d(player.x, player.y + player.getEyeHeight(), player.z);
783-
Vector startingVec = new Vector(startingPos.getX(), startingPos.getY(), startingPos.getZ());
784-
Ray trace = new Ray(player, startingPos.getX(), startingPos.getY(), startingPos.getZ(), player.xRot, player.yRot);
785-
final double distance = player.compensatedEntities.getSelf().getAttributeValue(Attributes.PLAYER_BLOCK_INTERACTION_RANGE);
786-
Vector endVec = trace.getPointAtDistance(distance);
787-
Vector3d endPos = new Vector3d(endVec.getX(), endVec.getY(), endVec.getZ());
788-
789-
return traverseBlocks(player, startingPos, endPos, (block, vector3i) -> {
790-
CollisionBox data = HitboxData.getBlockHitbox(player, heldItem, player.getClientVersion(), block, vector3i.getX(), vector3i.getY(), vector3i.getZ());
791-
List<SimpleCollisionBox> boxes = new ArrayList<>();
792-
data.downCast(boxes);
793-
794-
double bestHitResult = Double.MAX_VALUE;
795-
Vector bestHitLoc = null;
796-
BlockFace bestFace = null;
797-
798-
for (SimpleCollisionBox box : boxes) {
799-
Pair<Vector, BlockFace> intercept = ReachUtils.calculateIntercept(box, trace.getOrigin(), trace.getPointAtDistance(distance));
800-
if (intercept.getFirst() == null) continue; // No intercept
801-
802-
Vector hitLoc = intercept.getFirst();
803-
804-
if (hitLoc.distanceSquared(startingVec) < bestHitResult) {
805-
bestHitResult = hitLoc.distanceSquared(startingVec);
806-
bestHitLoc = hitLoc;
807-
bestFace = intercept.getSecond();
808-
}
809-
}
810-
if (bestHitLoc != null) {
811-
return new HitData(vector3i, bestHitLoc, bestFace, block);
812-
}
813-
814-
if (sourcesHaveHitbox &&
815-
(player.compensatedWorld.isWaterSourceBlock(vector3i.getX(), vector3i.getY(), vector3i.getZ())
816-
|| player.compensatedWorld.getLavaFluidLevelAt(vector3i.getX(), vector3i.getY(), vector3i.getZ()) == (8 / 9f))) {
817-
double waterHeight = player.compensatedWorld.getFluidLevelAt(vector3i.getX(), vector3i.getY(), vector3i.getZ());
818-
SimpleCollisionBox box = new SimpleCollisionBox(vector3i.getX(), vector3i.getY(), vector3i.getZ(), vector3i.getX() + 1, vector3i.getY() + waterHeight, vector3i.getZ() + 1);
819-
820-
Pair<Vector, BlockFace> intercept = ReachUtils.calculateIntercept(box, trace.getOrigin(), trace.getPointAtDistance(distance));
821-
822-
if (intercept.getFirst() != null) {
823-
return new HitData(vector3i, intercept.getFirst(), intercept.getSecond(), block);
824-
}
825-
}
826-
827-
return null;
828-
});
829-
}
830-
831707
@Override
832708
public void onPacketSend(PacketSendEvent event) {
833709
if (event.getConnectionState() != ConnectionState.PLAY) return;

0 commit comments

Comments
 (0)