Merge remote-tracking branch 'origin/2.0' into lightning
# Conflicts: # common/src/main/java/ac/grim/grimac/checks/impl/combat/Reach.java
This commit is contained in:
commit
65815fc73b
54 files changed
+660
-437
No files matched your search
@@ -233,7 +233,7 @@ jobs:
|
||||
dependencies: 'fabric-api'
|
||||
|
||||
- name: Publish to API
|
||||
if: ${{ inputs.publish-artifacts == true && env.BUILD_UPDATE_TOKEN != '' }}
|
||||
if: ${{ inputs.publish-artifacts == true && env.BUILD_UPDATE_TOKEN != '' && inputs.is-release-build == false }}
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
@@ -241,3 +241,13 @@ jobs:
|
||||
-H "Authorization: Bearer ${{ secrets.BUILD_UPDATE_TOKEN }}" \
|
||||
-d '{"org": "${{ github.repository_owner }}", "repository": "${{ github.event.repository.name }}", "branch": "${{ github.ref_name }}", "version": "${{ env.BUILD_VERSION }}"}' \
|
||||
"${{ secrets.BUILD_UPDATE_URL }}"
|
||||
|
||||
- name: Publish Release to API
|
||||
if: ${{ inputs.publish-artifacts == true && env.BUILD_UPDATE_TOKEN != '' && inputs.is-release-build == true }}
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "User-Agent: GrimACBuilder/v1" \
|
||||
-H "Authorization: Bearer ${{ secrets.BUILD_UPDATE_TOKEN }}" \
|
||||
-d '{"org": "${{ github.repository_owner }}", "repository": "${{ github.event.repository.name }}", "branch": "release", "version": "${{ env.BUILD_VERSION }}"}' \
|
||||
"${{ secrets.BUILD_UPDATE_URL }}"
|
||||
@@ -176,7 +176,8 @@ public class Check extends GrimProcessor implements AbstractCheck {
|
||||
|
||||
public static boolean isAsync(PacketTypeCommon packetType) {
|
||||
return packetType == PacketType.Play.Client.KEEP_ALIVE
|
||||
|| packetType == PacketType.Play.Client.CHUNK_BATCH_ACK;
|
||||
|| packetType == PacketType.Play.Client.CHUNK_BATCH_ACK
|
||||
|| packetType == PacketType.Play.Client.RESOURCE_PACK_STATUS;
|
||||
}
|
||||
|
||||
public boolean isUpdate(PacketTypeCommon packetType) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package ac.grim.grimac.checks.impl.badpackets;
|
||||
|
||||
import ac.grim.grimac.checks.Check;
|
||||
import ac.grim.grimac.checks.CheckData;
|
||||
import ac.grim.grimac.checks.type.PacketCheck;
|
||||
import ac.grim.grimac.player.GrimPlayer;
|
||||
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientEntityAction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@CheckData(name = "BadPacketsC", description = "Tried to wake up while not sleeping", experimental = true)
|
||||
public class BadPacketsC extends Check implements PacketCheck {
|
||||
public BadPacketsC(@NotNull GrimPlayer player) {
|
||||
super(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.ENTITY_ACTION
|
||||
&& new WrapperPlayClientEntityAction(event).getAction() == WrapperPlayClientEntityAction.Action.LEAVE_BED
|
||||
&& !player.isInBed) {
|
||||
flagAndAlert();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import com.github.retrooper.packetevents.protocol.attribute.Attributes;
|
||||
import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
|
||||
import com.github.retrooper.packetevents.util.Vector3d;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
|
||||
|
||||
@CheckData(name = "BadPacketsT")
|
||||
@@ -35,37 +36,39 @@ public class BadPacketsT extends Check implements PacketCheck {
|
||||
if (event.getPacketType().equals(PacketType.Play.Client.INTERACT_ENTITY)) {
|
||||
final WrapperPlayClientInteractEntity wrapper = new WrapperPlayClientInteractEntity(event);
|
||||
// Only INTERACT_AT actually has an interaction vector
|
||||
wrapper.getTarget().ifPresent(targetVector -> {
|
||||
final PacketEntity packetEntity = player.compensatedEntities.getEntity(wrapper.getEntityId());
|
||||
// Don't continue if the compensated entity hasn't been resolved
|
||||
if (packetEntity == null) {
|
||||
return;
|
||||
}
|
||||
if (wrapper.getAction() != WrapperPlayClientInteractEntity.InteractAction.INTERACT_AT) return;
|
||||
Vector3d targetVector = wrapper.getLocation();
|
||||
if (targetVector == null) return; // shouldn't ever happen, but whatever
|
||||
|
||||
// Make sure our target entity is actually a player (Player NPCs work too)
|
||||
if (!EntityTypes.PLAYER.equals(packetEntity.type)) {
|
||||
// We can't check for any entity that is not a player
|
||||
return;
|
||||
}
|
||||
final PacketEntity packetEntity = player.compensatedEntities.getEntity(wrapper.getEntityId());
|
||||
// Don't continue if the compensated entity hasn't been resolved
|
||||
if (packetEntity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform the interaction vector check
|
||||
// TODO:
|
||||
// 27/12/2023 - Dynamic values for more than just one entity type?
|
||||
// 28/12/2023 - Player-only is fine
|
||||
// 30/12/2023 - Expansions differ in 1.9+
|
||||
final float scale = (float) packetEntity.getAttributeValue(Attributes.SCALE);
|
||||
if (targetVector.y > (minVerticalDisplacement * scale) && targetVector.y < (maxVerticalDisplacement * scale)
|
||||
&& Math.abs(targetVector.x) < (maxHorizontalDisplacement * scale)
|
||||
&& Math.abs(targetVector.z) < (maxHorizontalDisplacement * scale)) {
|
||||
return;
|
||||
}
|
||||
// Make sure our target entity is actually a player (Player NPCs work too)
|
||||
if (!EntityTypes.PLAYER.equals(packetEntity.type)) {
|
||||
// We can't check for any entity that is not a player
|
||||
return;
|
||||
}
|
||||
|
||||
// Log the vector
|
||||
final String verbose = String.format("%.5f/%.5f/%.5f",
|
||||
targetVector.x, targetVector.y, targetVector.z);
|
||||
// We could pretty much ban the player at this point
|
||||
flagAndAlert(verbose);
|
||||
});
|
||||
// Perform the interaction vector check
|
||||
// TODO:
|
||||
// 27/12/2023 - Dynamic values for more than just one entity type?
|
||||
// 28/12/2023 - Player-only is fine
|
||||
// 30/12/2023 - Expansions differ in 1.9+
|
||||
final float scale = (float) packetEntity.getAttributeValue(Attributes.SCALE);
|
||||
if (targetVector.y > (minVerticalDisplacement * scale) && targetVector.y < (maxVerticalDisplacement * scale)
|
||||
&& Math.abs(targetVector.x) < (maxHorizontalDisplacement * scale)
|
||||
&& Math.abs(targetVector.z) < (maxHorizontalDisplacement * scale)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Log the vector
|
||||
final String verbose = String.format("%.5f/%.5f/%.5f",
|
||||
targetVector.x, targetVector.y, targetVector.z);
|
||||
// We could pretty much ban the player at this point
|
||||
flagAndAlert(verbose);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,9 @@ import ac.grim.grimac.player.GrimPlayer;
|
||||
import ac.grim.grimac.utils.anticheat.update.PredictionComplete;
|
||||
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientAttack;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientSpectateEntity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@@ -28,23 +30,17 @@ public class MultiInteractA extends Check implements PostPredictionCheck {
|
||||
WrapperPlayClientInteractEntity packet = new WrapperPlayClientInteractEntity(event);
|
||||
int entity = packet.getEntityId();
|
||||
boolean sneaking = packet.isSneaking().orElse(false);
|
||||
onInteract(event, entity, sneaking);
|
||||
}
|
||||
|
||||
if (hasInteracted && entity != lastEntity) {
|
||||
String verbose = "lastEntity=" + lastEntity + ", entity=" + entity
|
||||
+ ", lastSneaking=" + lastSneaking + ", sneaking=" + sneaking;
|
||||
if (!player.canSkipTicks()) {
|
||||
if (flagAndAlert(verbose) && shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
} else {
|
||||
flags.add(verbose);
|
||||
}
|
||||
}
|
||||
if (event.getPacketType() == PacketType.Play.Client.ATTACK) {
|
||||
WrapperPlayClientAttack packet = new WrapperPlayClientAttack(event);
|
||||
onInteract(event, packet.getEntityId(), lastSneaking);
|
||||
}
|
||||
|
||||
lastEntity = entity;
|
||||
lastSneaking = sneaking;
|
||||
hasInteracted = true;
|
||||
if (event.getPacketType() == PacketType.Play.Client.SPECTATE_ENTITY) {
|
||||
WrapperPlayClientSpectateEntity packet = new WrapperPlayClientSpectateEntity(event);
|
||||
onInteract(event, packet.getEntityId(), lastSneaking);
|
||||
}
|
||||
|
||||
if (!player.cameraEntity.isSelf() || isTickPacket(event.getPacketType())) {
|
||||
@@ -52,6 +48,25 @@ public class MultiInteractA extends Check implements PostPredictionCheck {
|
||||
}
|
||||
}
|
||||
|
||||
private void onInteract(PacketReceiveEvent event, int entity, boolean sneaking) {
|
||||
if (hasInteracted && (entity != lastEntity || sneaking != lastSneaking)) {
|
||||
String verbose = "lastEntity=" + lastEntity + ", entity=" + entity
|
||||
+ ", lastSneaking=" + lastSneaking + ", sneaking=" + sneaking;
|
||||
if (!player.canSkipTicks()) {
|
||||
if (flagAndAlert(verbose) && shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
} else {
|
||||
flags.add(verbose);
|
||||
}
|
||||
}
|
||||
|
||||
lastEntity = entity;
|
||||
lastSneaking = sneaking;
|
||||
hasInteracted = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPredictionComplete(PredictionComplete predictionComplete) {
|
||||
if (!player.canSkipTicks()) return;
|
||||
|
||||
@@ -8,7 +8,7 @@ import ac.grim.grimac.utils.anticheat.MessageUtil;
|
||||
import ac.grim.grimac.utils.anticheat.update.PredictionComplete;
|
||||
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.github.retrooper.packetevents.util.Vector3f;
|
||||
import com.github.retrooper.packetevents.util.Vector3d;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -16,8 +16,8 @@ import java.util.ArrayList;
|
||||
@CheckData(name = "MultiInteractB", experimental = true)
|
||||
public class MultiInteractB extends Check implements PostPredictionCheck {
|
||||
private final ArrayList<String> flags = new ArrayList<>();
|
||||
private Vector3f lastPos;
|
||||
private boolean hasInteracted = false;
|
||||
private Vector3d lastPos;
|
||||
private boolean hasInteracted;
|
||||
|
||||
public MultiInteractB(final GrimPlayer player) {
|
||||
super(player);
|
||||
@@ -26,11 +26,11 @@ public class MultiInteractB extends Check implements PostPredictionCheck {
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) {
|
||||
Vector3f pos = new WrapperPlayClientInteractEntity(event).getTarget().orElse(null);
|
||||
WrapperPlayClientInteractEntity packet = new WrapperPlayClientInteractEntity(event);
|
||||
if (packet.getAction() != WrapperPlayClientInteractEntity.InteractAction.INTERACT_AT) return;
|
||||
|
||||
if (pos == null) {
|
||||
return;
|
||||
}
|
||||
Vector3d pos = packet.getLocation();
|
||||
if (pos == null) return; // shouldn't ever happen, but whatever
|
||||
|
||||
if (hasInteracted && !pos.equals(lastPos)) {
|
||||
String verbose = "pos=" + MessageUtil.toUnlabledString(pos) + ", lastPos=" + MessageUtil.toUnlabledString(lastPos);
|
||||
|
||||
@@ -50,6 +50,7 @@ import com.github.retrooper.packetevents.protocol.player.ClientVersion;
|
||||
import com.github.retrooper.packetevents.protocol.player.GameMode;
|
||||
import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState;
|
||||
import com.github.retrooper.packetevents.protocol.player.InteractionHand;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientAttack;
|
||||
import com.github.retrooper.packetevents.util.Vector3d;
|
||||
import com.github.retrooper.packetevents.util.Vector3i;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
|
||||
@@ -90,114 +91,14 @@ public class Reach extends Check implements PacketCheck {
|
||||
|
||||
@Override
|
||||
public void onPacketReceive(final PacketReceiveEvent event) {
|
||||
if (!player.disableGrim && event.getPacketType() == PacketType.Play.Client.ATTACK) {
|
||||
WrapperPlayClientAttack packet = new WrapperPlayClientAttack(event);
|
||||
onInteract(event, packet.getEntityId(), InteractionHand.MAIN_HAND);
|
||||
}
|
||||
|
||||
if (!player.disableGrim && event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) {
|
||||
WrapperPlayClientInteractEntity action = new WrapperPlayClientInteractEntity(event);
|
||||
|
||||
// Don't let the player teleport to bypass reach
|
||||
if (player.getSetbackTeleportUtil().shouldBlockMovement()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
PacketEntity entity = player.compensatedEntities.entityMap.get(action.getEntityId());
|
||||
// Stop people from freezing transactions before an entity spawns to bypass reach
|
||||
// TODO: implement dragon parts?
|
||||
if (entity == null || entity instanceof PacketEntityEnderDragonPart) {
|
||||
// Only cancel if and only if we are tracking this entity
|
||||
// This is because we don't track paintings.
|
||||
if (shouldModifyPackets() && player.compensatedEntities.serverPositionsMap.containsKey(action.getEntityId())) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (ignoreNonPlayerTargets && !entity.type.equals(EntityTypes.PLAYER)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dead entities cause false flags (https://github.com/GrimAnticheat/Grim/issues/546)
|
||||
if (entity.isDead) return;
|
||||
|
||||
// TODO: Remove when in front of via
|
||||
if (entity.type == EntityTypes.ARMOR_STAND && player.getClientVersion().isOlderThan(ClientVersion.V_1_8))
|
||||
return;
|
||||
//Prevents Happy Ghast Reach false on 1.21.6+ servers with ViaBackwards set up
|
||||
if (entity.type == EntityTypes.HAPPY_GHAST && player.getClientVersion().isOlderThan(ClientVersion.V_1_21_6)) {
|
||||
return;
|
||||
}
|
||||
if (player.gamemode == GameMode.CREATIVE || player.gamemode == GameMode.SPECTATOR)
|
||||
return;
|
||||
if (player.inVehicle()) return;
|
||||
if (entity.riding != null) return;
|
||||
|
||||
InteractionHand hand = action.getAction() == WrapperPlayClientInteractEntity.InteractAction.ATTACK ?
|
||||
InteractionHand.MAIN_HAND : action.getHand(); // attacks can be only performed with the main hand
|
||||
|
||||
ItemStack currentStack = player.inventory.getItemInHand(hand);
|
||||
ItemStack startStack = player.inventory.getStartOfTickStack();
|
||||
|
||||
boolean hasRange = false;
|
||||
float maxReach = 0f;
|
||||
float hitboxMargin = 0f;
|
||||
|
||||
boolean clientAttackRangeExists = player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_21_11);
|
||||
boolean clientAndServerAgrees = clientAttackRangeExists && ATTACK_RANGE_COMPONENT_EXISTS;
|
||||
|
||||
boolean viaVersionAvailable = false;
|
||||
if (USE_1_8_HITBOX_MARGIN && ViaVersionUtil.isAvailable) {
|
||||
viaVersionAvailable = Via.getConfig().getValues().containsKey("use-1_8-hitbox-margin") && Via.getConfig().use1_8HitboxMargin();
|
||||
}
|
||||
|
||||
boolean clientAndViaVersion = clientAttackRangeExists && viaVersionAvailable;
|
||||
if (clientAndServerAgrees || clientAndViaVersion) {
|
||||
ItemAttackRange startRange = startStack.getComponentOr(ComponentTypes.ATTACK_RANGE, null);
|
||||
ItemAttackRange currentRange = currentStack.getComponentOr(ComponentTypes.ATTACK_RANGE, null);
|
||||
|
||||
if (clientAndViaVersion) {
|
||||
if (startStack != ItemStack.EMPTY) {
|
||||
startRange = new ItemAttackRange(0F, 3F, 0F, 4F, 0.1F, 1F);
|
||||
}
|
||||
|
||||
if (currentStack != ItemStack.EMPTY) {
|
||||
currentRange = new ItemAttackRange(0F, 3F, 0F, 4F, 0.1F, 1F);
|
||||
}
|
||||
}
|
||||
|
||||
// If the start stack has no range component, the client defaults to vanilla reach behavior,
|
||||
// regardless of what the current stack is (No Range -> X = No Range used).
|
||||
if (startRange != null) {
|
||||
if (currentRange == null) {
|
||||
// Range (Start) -> No Range (Current)
|
||||
// Client logic uses Start Range
|
||||
hasRange = true;
|
||||
maxReach = startRange.getMaxRange();
|
||||
hitboxMargin = startRange.getHitboxMargin();
|
||||
} else {
|
||||
// Range (Start) -> Range (Current)
|
||||
// Client logic requires satisfying BOTH constraints
|
||||
hasRange = true;
|
||||
maxReach = Math.min(startRange.getMaxRange(), currentRange.getMaxRange());
|
||||
hitboxMargin = Math.min(startRange.getHitboxMargin(), currentRange.getHitboxMargin());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean tooManyAttacks = playerAttackQueue.size() > 10;
|
||||
if (!tooManyAttacks) {
|
||||
playerAttackQueue.put(action.getEntityId(), new InteractionData(
|
||||
player.x, player.y, player.z,
|
||||
hasRange, maxReach, hitboxMargin
|
||||
)); // Queue for next tick for very precise check
|
||||
}
|
||||
|
||||
boolean knownInvalid = isKnownInvalid(entity, hasRange, maxReach, hitboxMargin);
|
||||
|
||||
if ((shouldModifyPackets() && cancelImpossibleHits && knownInvalid) || tooManyAttacks) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
WrapperPlayClientInteractEntity packet = new WrapperPlayClientInteractEntity(event);
|
||||
onInteract(event, packet.getEntityId(), packet.getHand());
|
||||
}
|
||||
|
||||
// If the player set their look, or we know they have a new tick
|
||||
@@ -207,6 +108,109 @@ public class Reach extends Check implements PacketCheck {
|
||||
}
|
||||
}
|
||||
|
||||
private void onInteract(PacketReceiveEvent event, int entityId, InteractionHand hand) {
|
||||
// Don't let the player teleport to bypass reach
|
||||
if (player.getSetbackTeleportUtil().shouldBlockMovement()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
PacketEntity entity = player.compensatedEntities.entityMap.get(entityId);
|
||||
// Stop people from freezing transactions before an entity spawns to bypass reach
|
||||
// TODO: implement dragon parts?
|
||||
if (entity == null || entity instanceof PacketEntityEnderDragonPart) {
|
||||
// Only cancel if and only if we are tracking this entity
|
||||
// This is because we don't track paintings.
|
||||
if (shouldModifyPackets() && player.compensatedEntities.serverPositionsMap.containsKey(entityId)) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (ignoreNonPlayerTargets && !entity.type.equals(EntityTypes.PLAYER)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dead entities cause false flags (https://github.com/GrimAnticheat/Grim/issues/546)
|
||||
if (entity.isDead) return;
|
||||
|
||||
// TODO: Remove when in front of via
|
||||
if (entity.type == EntityTypes.ARMOR_STAND && player.getClientVersion().isOlderThan(ClientVersion.V_1_8))
|
||||
return;
|
||||
// Prevents Happy Ghast Reach false on 1.21.6+ servers with ViaBackwards set up
|
||||
if (entity.type == EntityTypes.HAPPY_GHAST && player.getClientVersion().isOlderThan(ClientVersion.V_1_21_6))
|
||||
return;
|
||||
if (player.gamemode == GameMode.CREATIVE || player.gamemode == GameMode.SPECTATOR)
|
||||
return;
|
||||
if (player.inVehicle()) return;
|
||||
if (entity.riding != null) return;
|
||||
|
||||
ItemStack currentStack = player.inventory.getItemInHand(hand);
|
||||
ItemStack startStack = player.inventory.getStartOfTickStack();
|
||||
|
||||
boolean hasRange = false;
|
||||
float maxReach = 0f;
|
||||
float hitboxMargin = 0f;
|
||||
|
||||
boolean clientAttackRangeExists = player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_21_11);
|
||||
boolean clientAndServerAgrees = clientAttackRangeExists && ATTACK_RANGE_COMPONENT_EXISTS;
|
||||
|
||||
boolean viaVersionAvailable = false;
|
||||
if (USE_1_8_HITBOX_MARGIN && ViaVersionUtil.isAvailable) {
|
||||
viaVersionAvailable = Via.getConfig().getValues().containsKey("use-1_8-hitbox-margin") && Via.getConfig().use1_8HitboxMargin();
|
||||
}
|
||||
|
||||
boolean clientAndViaVersion = clientAttackRangeExists && viaVersionAvailable;
|
||||
if (clientAndServerAgrees || clientAndViaVersion) {
|
||||
ItemAttackRange startRange = startStack.getComponentOr(ComponentTypes.ATTACK_RANGE, null);
|
||||
ItemAttackRange currentRange = currentStack.getComponentOr(ComponentTypes.ATTACK_RANGE, null);
|
||||
|
||||
if (clientAndViaVersion) {
|
||||
if (startStack != ItemStack.EMPTY) {
|
||||
startRange = new ItemAttackRange(0F, 3F, 0F, 4F, 0.1F, 1F);
|
||||
}
|
||||
|
||||
if (currentStack != ItemStack.EMPTY) {
|
||||
currentRange = new ItemAttackRange(0F, 3F, 0F, 4F, 0.1F, 1F);
|
||||
}
|
||||
}
|
||||
|
||||
// If the start stack has no range component, the client defaults to vanilla reach behavior,
|
||||
// regardless of what the current stack is (No Range -> X = No Range used).
|
||||
if (startRange != null) {
|
||||
hasRange = true;
|
||||
if (currentRange == null) {
|
||||
// Range (Start) -> No Range (Current)
|
||||
// Client logic uses Start Range
|
||||
maxReach = startRange.getMaxRange();
|
||||
hitboxMargin = startRange.getHitboxMargin();
|
||||
} else {
|
||||
// Range (Start) -> Range (Current)
|
||||
// Client logic requires satisfying BOTH constraints
|
||||
maxReach = Math.min(startRange.getMaxRange(), currentRange.getMaxRange());
|
||||
hitboxMargin = Math.min(startRange.getHitboxMargin(), currentRange.getHitboxMargin());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean tooManyAttacks = playerAttackQueue.size() > 10;
|
||||
if (!tooManyAttacks) {
|
||||
playerAttackQueue.put(entityId, new InteractionData(
|
||||
player.x, player.y, player.z,
|
||||
hasRange, maxReach, hitboxMargin
|
||||
)); // Queue for next tick for very precise check
|
||||
}
|
||||
|
||||
boolean knownInvalid = isKnownInvalid(entity, hasRange, maxReach, hitboxMargin);
|
||||
|
||||
if ((shouldModifyPackets() && cancelImpossibleHits && knownInvalid) || tooManyAttacks) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
}
|
||||
|
||||
// This method finds the most optimal point at which the user should be aiming at
|
||||
// and then measures the distance between the player's eyes and this target point
|
||||
//
|
||||
|
||||
@@ -6,7 +6,9 @@ import ac.grim.grimac.checks.type.PacketCheck;
|
||||
import ac.grim.grimac.player.GrimPlayer;
|
||||
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientAttack;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientSpectateEntity;
|
||||
|
||||
@CheckData(name = "SelfInteract", description = "Interacted with self")
|
||||
public class SelfInteract extends Check implements PacketCheck {
|
||||
@@ -16,11 +18,26 @@ public class SelfInteract extends Check implements PacketCheck {
|
||||
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY
|
||||
&& player.cameraEntity.isSelf() // TODO: should check for camera entity id?
|
||||
&& new WrapperPlayClientInteractEntity(event).getEntityId() == player.entityID
|
||||
&& flagAndAlert() && shouldModifyPackets() // Instant ban
|
||||
) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) {
|
||||
WrapperPlayClientInteractEntity packet = new WrapperPlayClientInteractEntity(event);
|
||||
onInteract(event, packet.getEntityId());
|
||||
}
|
||||
|
||||
if (event.getPacketType() == PacketType.Play.Client.ATTACK) {
|
||||
WrapperPlayClientAttack packet = new WrapperPlayClientAttack(event);
|
||||
onInteract(event, packet.getEntityId());
|
||||
}
|
||||
|
||||
if (event.getPacketType() == PacketType.Play.Client.SPECTATE_ENTITY) {
|
||||
WrapperPlayClientSpectateEntity packet = new WrapperPlayClientSpectateEntity(event);
|
||||
onInteract(event, packet.getEntityId());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: should check for camera entity id instead of player entity id?
|
||||
private void onInteract(PacketReceiveEvent event, int entityId) {
|
||||
if (player.cameraEntity.isSelf() && entityId == player.entityID
|
||||
&& flagAndAlert() && shouldModifyPackets()) { // Instant ban
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ import ac.grim.grimac.checks.type.PostPredictionCheck;
|
||||
import ac.grim.grimac.player.GrimPlayer;
|
||||
import ac.grim.grimac.utils.anticheat.update.PredictionComplete;
|
||||
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientEntityAction;
|
||||
|
||||
@CheckData(name = "ElytraA", description = "Started gliding while already gliding")
|
||||
public class ElytraA extends Check implements PostPredictionCheck {
|
||||
@@ -33,13 +31,6 @@ public class ElytraA extends Check implements PostPredictionCheck {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if (player.getClientVersion().isOlderThan(ClientVersion.V_1_15) && event.getPacketType() == PacketType.Play.Client.ENTITY_ACTION
|
||||
&& new WrapperPlayClientEntityAction(event).getAction() == WrapperPlayClientEntityAction.Action.START_FLYING_WITH_ELYTRA
|
||||
) onStartGliding(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPredictionComplete(PredictionComplete predictionComplete) {
|
||||
if (setback) {
|
||||
|
||||
@@ -21,15 +21,7 @@ import java.util.ArrayDeque;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static com.github.retrooper.packetevents.protocol.packettype.PacketType.Play.Client.ANIMATION;
|
||||
import static com.github.retrooper.packetevents.protocol.packettype.PacketType.Play.Client.CLICK_WINDOW;
|
||||
import static com.github.retrooper.packetevents.protocol.packettype.PacketType.Play.Client.ENTITY_ACTION;
|
||||
import static com.github.retrooper.packetevents.protocol.packettype.PacketType.Play.Client.HELD_ITEM_CHANGE;
|
||||
import static com.github.retrooper.packetevents.protocol.packettype.PacketType.Play.Client.INTERACT_ENTITY;
|
||||
import static com.github.retrooper.packetevents.protocol.packettype.PacketType.Play.Client.PLAYER_ABILITIES;
|
||||
import static com.github.retrooper.packetevents.protocol.packettype.PacketType.Play.Client.PLAYER_BLOCK_PLACEMENT;
|
||||
import static com.github.retrooper.packetevents.protocol.packettype.PacketType.Play.Client.PLAYER_DIGGING;
|
||||
import static com.github.retrooper.packetevents.protocol.packettype.PacketType.Play.Client.USE_ITEM;
|
||||
import static com.github.retrooper.packetevents.protocol.packettype.PacketType.Play.Client.*;
|
||||
|
||||
@CheckData(name = "Post")
|
||||
public class Post extends Check implements PacketCheck, PostPredictionCheck {
|
||||
@@ -90,6 +82,7 @@ public class Post extends Check implements PacketCheck, PostPredictionCheck {
|
||||
} else if (PLAYER_ABILITIES.equals(packetType)
|
||||
|| (HELD_ITEM_CHANGE.equals(packetType) && player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_8))
|
||||
|| INTERACT_ENTITY.equals(packetType) || PLAYER_BLOCK_PLACEMENT.equals(packetType)
|
||||
|| ATTACK.equals(packetType) || SPECTATE_ENTITY.equals(packetType)
|
||||
|| USE_ITEM.equals(packetType) || PLAYER_DIGGING.equals(packetType)) {
|
||||
if (sentFlying) post.add(event.getPacketType());
|
||||
} else if (CLICK_WINDOW.equals(packetType) && player.getClientVersion().isOlderThan(ClientVersion.V_1_13)) {
|
||||
@@ -107,6 +100,7 @@ public class Post extends Check implements PacketCheck, PostPredictionCheck {
|
||||
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_19_3) && player.inVehicle()) {
|
||||
return;
|
||||
}
|
||||
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_13) && new WrapperPlayClientEntityAction(event).getAction() == WrapperPlayClientEntityAction.Action.LEAVE_BED) return;
|
||||
if (sentFlying) post.add(event.getPacketType());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ public class SetbackBlocker extends Check implements PacketCheck {
|
||||
if (player.disableGrim)
|
||||
return; // Let's avoid letting people disable grim with grim.nomodifypackets
|
||||
|
||||
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY
|
||||
|| event.getPacketType() == PacketType.Play.Client.ATTACK
|
||||
|| event.getPacketType() == PacketType.Play.Client.SPECTATE_ENTITY) {
|
||||
if (player.getSetbackTeleportUtil().cheatVehicleInterpolationDelay > 0) {
|
||||
event.setCancelled(true); // Player is in the vehicle
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ import ac.grim.grimac.checks.type.PacketCheck;
|
||||
import ac.grim.grimac.player.GrimPlayer;
|
||||
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.github.retrooper.packetevents.protocol.player.DiggingAction;
|
||||
import com.github.retrooper.packetevents.protocol.player.InteractionHand;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerDigging;
|
||||
|
||||
@CheckData(name = "MultiActionsA", description = "Attacked while using an item", experimental = true)
|
||||
public class MultiActionsA extends Check implements PacketCheck {
|
||||
@@ -17,8 +19,10 @@ public class MultiActionsA extends Check implements PacketCheck {
|
||||
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if (player.packetStateData.isSlowedByUsingItem() && (player.packetStateData.lastSlotSelected == player.packetStateData.getSlowedByUsingItemSlot() || player.packetStateData.itemInUseHand == InteractionHand.OFF_HAND) && event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) {
|
||||
if (new WrapperPlayClientInteractEntity(event).getAction() == WrapperPlayClientInteractEntity.InteractAction.ATTACK) {
|
||||
if (player.packetStateData.isSlowedByUsingItem() && (player.packetStateData.lastSlotSelected == player.packetStateData.getSlowedByUsingItemSlot() || player.packetStateData.itemInUseHand == InteractionHand.OFF_HAND)) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY && new WrapperPlayClientInteractEntity(event).getAction() == WrapperPlayClientInteractEntity.InteractAction.ATTACK
|
||||
|| event.getPacketType() == PacketType.Play.Client.ATTACK || event.getPacketType() == PacketType.Play.Client.SPECTATE_ENTITY
|
||||
|| event.getPacketType() == PacketType.Play.Client.PLAYER_DIGGING && new WrapperPlayClientPlayerDigging(event).getAction() == DiggingAction.STAB) {
|
||||
if (flagAndAlert() && shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
|
||||
@@ -39,12 +39,15 @@ public class MultiActionsC extends Check implements PacketCheck {
|
||||
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.CLICK_WINDOW && !player.serverOpenedInventoryThisTick) {
|
||||
String verbose = getVerbose(player);
|
||||
if (!verbose.isEmpty() && flagAndAlert(verbose) && shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
if (event.getPacketType() != PacketType.Play.Client.CLICK_WINDOW) return;
|
||||
if (player.serverOpenedInventoryThisTick) return;
|
||||
|
||||
String verbose = getVerbose(player);
|
||||
if (verbose.isEmpty()) return;
|
||||
|
||||
if (flagAndAlert(verbose) && shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,15 @@ public class MultiActionsD extends Check implements PacketCheck {
|
||||
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.CLOSE_WINDOW) {
|
||||
String verbose = MultiActionsC.getVerbose(player);
|
||||
if (!verbose.isEmpty() && flagAndAlert(verbose) && shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
}
|
||||
if (event.getPacketType() != PacketType.Play.Client.CLOSE_WINDOW) return;
|
||||
if (player.serverOpenedInventoryThisTick) return;
|
||||
|
||||
String verbose = MultiActionsC.getVerbose(player);
|
||||
if (verbose.isEmpty()) return;
|
||||
|
||||
// Don't cancel this packet, because it won't do anything except for making chests
|
||||
// look like they are still open (desynced),
|
||||
// and it can cause incompatibility issues with plugins
|
||||
flagAndAlert(verbose);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,9 @@ public class MultiActionsF extends BlockPlaceCheck {
|
||||
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY
|
||||
|| event.getPacketType() == PacketType.Play.Client.ATTACK
|
||||
|| event.getPacketType() == PacketType.Play.Client.SPECTATE_ENTITY) {
|
||||
entity = true;
|
||||
if (block) {
|
||||
if (!player.canSkipTicks()) {
|
||||
|
||||
@@ -24,6 +24,18 @@ public class MultiActionsG extends BlockPlaceCheck {
|
||||
player.onPacketCancel();
|
||||
}
|
||||
|
||||
if (event.getPacketType() == PacketType.Play.Client.ATTACK && isCheckActive()
|
||||
&& flagAndAlert("attack") && shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
|
||||
if (event.getPacketType() == PacketType.Play.Client.SPECTATE_ENTITY && isCheckActive()
|
||||
&& flagAndAlert("spectateEntity") && shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
|
||||
if (event.getPacketType() == PacketType.Play.Client.USE_ITEM && isCheckActive()
|
||||
&& flagAndAlert("use") && shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
|
||||
@@ -9,10 +9,12 @@ import com.github.retrooper.packetevents.event.PacketReceiveEvent;
|
||||
import com.github.retrooper.packetevents.manager.server.ServerVersion;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
|
||||
import com.github.retrooper.packetevents.protocol.player.DiggingAction;
|
||||
import com.github.retrooper.packetevents.protocol.player.GameMode;
|
||||
import com.github.retrooper.packetevents.protocol.player.InteractionHand;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientAnimation;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerDigging;
|
||||
|
||||
@CheckData(name = "PacketOrderB", description = "Did not swing for attack")
|
||||
public class PacketOrderB extends Check implements PacketCheck {
|
||||
@@ -51,20 +53,23 @@ public class PacketOrderB extends Check implements PacketCheck {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY && (player.gamemode != GameMode.SPECTATOR || player.getClientVersion().isOlderThan(ClientVersion.V_1_21_11))) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) {
|
||||
WrapperPlayClientInteractEntity packet = new WrapperPlayClientInteractEntity(event);
|
||||
if (packet.getAction() == WrapperPlayClientInteractEntity.InteractAction.ATTACK) {
|
||||
sentAttack = true;
|
||||
onAttack(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (is1_9 ? !sentAnimationSinceLastAttack : !sentAnimation) {
|
||||
sentAttack = false; // don't flag twice
|
||||
if (flagAndAlert("pre-attack") && shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
}
|
||||
if (event.getPacketType() == PacketType.Play.Client.ATTACK) {
|
||||
onAttack(event);
|
||||
return;
|
||||
}
|
||||
|
||||
sentAnimationSinceLastAttack = sentAnimation = sentSlotSwitch = false;
|
||||
if (event.getPacketType() == PacketType.Play.Client.PLAYER_DIGGING) {
|
||||
WrapperPlayClientPlayerDigging packet = new WrapperPlayClientPlayerDigging(event);
|
||||
if (packet.getAction() == DiggingAction.STAB) {
|
||||
onAttack(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -82,4 +87,20 @@ public class PacketOrderB extends Check implements PacketCheck {
|
||||
sentAttack = sentAnimation = sentSlotSwitch = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void onAttack(PacketReceiveEvent event) {
|
||||
if (player.gamemode == GameMode.SPECTATOR && player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_21_11)) return;
|
||||
|
||||
sentAttack = true;
|
||||
|
||||
if (is1_9 ? !sentAnimationSinceLastAttack : !sentAnimation) {
|
||||
sentAttack = false; // don't flag twice
|
||||
if (flagAndAlert("pre-attack") && shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
}
|
||||
|
||||
sentAnimationSinceLastAttack = sentAnimation = sentSlotSwitch = false;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@ import ac.grim.grimac.checks.CheckData;
|
||||
import ac.grim.grimac.checks.type.PacketCheck;
|
||||
import ac.grim.grimac.player.GrimPlayer;
|
||||
import ac.grim.grimac.utils.data.packetentity.PacketEntity;
|
||||
import com.github.retrooper.packetevents.PacketEvents;
|
||||
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
|
||||
import com.github.retrooper.packetevents.manager.server.ServerVersion;
|
||||
import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
|
||||
@@ -15,8 +17,9 @@ import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPl
|
||||
|
||||
@CheckData(name = "PacketOrderC")
|
||||
public class PacketOrderC extends Check implements PacketCheck {
|
||||
// 1.7 players do not send INTERACT_AT, so we cannot check them
|
||||
private final boolean exempt = player.getClientVersion().isOlderThanOrEquals(ClientVersion.V_1_7_10);
|
||||
private final boolean exempt = player.getClientVersion().isOlderThanOrEquals(ClientVersion.V_1_7_10) // 1.7 players do not send INTERACT_AT
|
||||
|| player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_26_1) // 26.1 players do not send INTERACT
|
||||
|| PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_26_1); // ViaBackwards drops INTERACT on 26.1 servers
|
||||
private boolean sentInteractAt = false;
|
||||
private int requiredEntity;
|
||||
private InteractionHand requiredHand;
|
||||
|
||||
@@ -30,8 +30,11 @@ public class PacketOrderD extends Check implements PacketCheck {
|
||||
final boolean sneaking = packet.isSneaking().orElse(false);
|
||||
final int entity = packet.getEntityId();
|
||||
|
||||
// via inserts these wrong...
|
||||
if (action == InteractAction.INTERACT && player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_26_1)) return;
|
||||
|
||||
if (packet.getHand() == InteractionHand.OFF_HAND) {
|
||||
if (action == InteractAction.INTERACT) {
|
||||
if (action == InteractAction.INTERACT || player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_26_1)) {
|
||||
if (!sentMainhand) {
|
||||
if (flagAndAlert("Skipped Mainhand") && shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
@@ -39,12 +42,16 @@ public class PacketOrderD extends Check implements PacketCheck {
|
||||
}
|
||||
}
|
||||
sentMainhand = false;
|
||||
} else if (sneaking != requiredSneaking || entity != requiredEntity) {
|
||||
String verbose = "requiredEntity=" + requiredEntity + ", entity=" + entity
|
||||
+ ", requiredSneaking=" + requiredSneaking + ", sneaking=" + sneaking;
|
||||
if (flagAndAlert(verbose) && shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
|
||||
if (action == InteractAction.INTERACT_AT) {
|
||||
if (sneaking != requiredSneaking || entity != requiredEntity) {
|
||||
String verbose = "requiredEntity=" + requiredEntity + ", entity=" + entity
|
||||
+ ", requiredSneaking=" + requiredSneaking + ", sneaking=" + sneaking;
|
||||
if (flagAndAlert(verbose) && shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -22,7 +22,7 @@ public class PacketOrderE extends Check implements PostPredictionCheck {
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.HELD_ITEM_CHANGE) {
|
||||
if (player.packetOrderProcessor.isAttacking()
|
||||
if (player.packetOrderProcessor.isAttackingOrStabbing()
|
||||
|| player.packetOrderProcessor.isRightClicking()
|
||||
|| player.packetOrderProcessor.isOpeningInventory()
|
||||
|| player.packetOrderProcessor.isReleasing()
|
||||
@@ -32,7 +32,7 @@ public class PacketOrderE extends Check implements PostPredictionCheck {
|
||||
|| player.packetOrderProcessor.isStartingToGlide()
|
||||
|| player.packetOrderProcessor.isJumpingWithMount()
|
||||
) {
|
||||
String verbose = "attacking=" + player.packetOrderProcessor.isAttacking()
|
||||
String verbose = "attacking=" + player.packetOrderProcessor.isAttackingOrStabbing()
|
||||
+ ", rightClicking=" + player.packetOrderProcessor.isRightClicking()
|
||||
+ ", openingInventory=" + player.packetOrderProcessor.isOpeningInventory()
|
||||
+ ", releasing=" + player.packetOrderProcessor.isReleasing()
|
||||
|
||||
@@ -23,6 +23,8 @@ public class PacketOrderF extends Check implements PostPredictionCheck {
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY
|
||||
|| event.getPacketType() == PacketType.Play.Client.ATTACK
|
||||
|| event.getPacketType() == PacketType.Play.Client.SPECTATE_ENTITY
|
||||
|| event.getPacketType() == PacketType.Play.Client.PLAYER_BLOCK_PLACEMENT
|
||||
|| event.getPacketType() == PacketType.Play.Client.USE_ITEM
|
||||
|| event.getPacketType() == PacketType.Play.Client.PICK_ITEM
|
||||
@@ -31,6 +33,8 @@ public class PacketOrderF extends Check implements PostPredictionCheck {
|
||||
&& new WrapperPlayClientClientStatus(event).getAction() == WrapperPlayClientClientStatus.Action.OPEN_INVENTORY_ACHIEVEMENT)
|
||||
) if (player.packetOrderProcessor.isSprinting() || player.packetOrderProcessor.isSneaking()) {
|
||||
String verbose = "action=" + (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY ? "interact"
|
||||
: event.getPacketType() == PacketType.Play.Client.ATTACK ? "attack"
|
||||
: event.getPacketType() == PacketType.Play.Client.SPECTATE_ENTITY ? "spectateEntity"
|
||||
: event.getPacketType() == PacketType.Play.Client.PLAYER_BLOCK_PLACEMENT ? "place"
|
||||
: event.getPacketType() == PacketType.Play.Client.USE_ITEM ? "use"
|
||||
: event.getPacketType() == PacketType.Play.Client.PICK_ITEM ? "pick"
|
||||
|
||||
@@ -28,21 +28,20 @@ public class PacketOrderG extends Check implements PostPredictionCheck {
|
||||
DiggingAction action = null;
|
||||
if (event.getPacketType() == PacketType.Play.Client.PLAYER_DIGGING) {
|
||||
action = new WrapperPlayClientPlayerDigging(event).getAction();
|
||||
if (action == DiggingAction.RELEASE_USE_ITEM
|
||||
|| action == DiggingAction.START_DIGGING
|
||||
|| action == DiggingAction.CANCELLED_DIGGING
|
||||
|| action == DiggingAction.FINISHED_DIGGING
|
||||
if (action != DiggingAction.SWAP_ITEM_WITH_OFFHAND
|
||||
&& action != DiggingAction.DROP_ITEM
|
||||
&& action != DiggingAction.DROP_ITEM_STACK
|
||||
) return;
|
||||
}
|
||||
|
||||
if (player.packetOrderProcessor.isAttacking()
|
||||
if (player.packetOrderProcessor.isAttackingOrStabbing()
|
||||
|| player.packetOrderProcessor.isReleasing()
|
||||
|| player.packetOrderProcessor.isRightClicking()
|
||||
|| player.packetOrderProcessor.isPicking()
|
||||
|| player.packetOrderProcessor.isDigging()
|
||||
) {
|
||||
String verbose = "action=" + (action == null ? "openInventory" : action == DiggingAction.SWAP_ITEM_WITH_OFFHAND ? "swap" : "drop")
|
||||
+ ", attacking=" + player.packetOrderProcessor.isAttacking()
|
||||
+ ", attacking=" + player.packetOrderProcessor.isAttackingOrStabbing()
|
||||
+ ", releasing=" + player.packetOrderProcessor.isReleasing()
|
||||
+ ", rightClicking=" + player.packetOrderProcessor.isRightClicking()
|
||||
+ ", picking=" + player.packetOrderProcessor.isPicking()
|
||||
|
||||
@@ -32,20 +32,7 @@ public class PacketOrderI extends Check implements PostPredictionCheck {
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) {
|
||||
if (new WrapperPlayClientInteractEntity(event).getAction() == WrapperPlayClientInteractEntity.InteractAction.ATTACK) {
|
||||
if (player.packetOrderProcessor.isRightClicking() || player.packetOrderProcessor.isPicking() || player.packetOrderProcessor.isReleasing() || player.packetOrderProcessor.isDigging()) {
|
||||
String verbose = "type=attack, rightClicking=" + player.packetOrderProcessor.isRightClicking()
|
||||
+ ", picking=" + player.packetOrderProcessor.isPicking()
|
||||
+ ", releasing=" + player.packetOrderProcessor.isReleasing()
|
||||
+ ", digging=" + player.packetOrderProcessor.isDigging();
|
||||
if (!player.canSkipTicks()) {
|
||||
if (flagAndAlert(verbose) && shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
} else {
|
||||
flags.add(verbose);
|
||||
}
|
||||
}
|
||||
onAttack(event);
|
||||
} else if (player.packetOrderProcessor.isReleasing() || player.packetOrderProcessor.isDigging()) {
|
||||
String verbose = "type=interact, releasing=" + player.packetOrderProcessor.isReleasing() + ", digging=" + player.packetOrderProcessor.isDigging();
|
||||
if (!player.canSkipTicks()) {
|
||||
@@ -73,13 +60,20 @@ public class PacketOrderI extends Check implements PostPredictionCheck {
|
||||
}
|
||||
}
|
||||
|
||||
if (event.getPacketType() == PacketType.Play.Client.ATTACK || event.getPacketType() == PacketType.Play.Client.SPECTATE_ENTITY) {
|
||||
onAttack(event);
|
||||
}
|
||||
|
||||
if (event.getPacketType() == PacketType.Play.Client.PLAYER_DIGGING) {
|
||||
WrapperPlayClientPlayerDigging packet = new WrapperPlayClientPlayerDigging(event);
|
||||
|
||||
switch (packet.getAction()) {
|
||||
case STAB:
|
||||
onAttack(event);
|
||||
break;
|
||||
case RELEASE_USE_ITEM:
|
||||
if (player.packetOrderProcessor.isAttacking() || player.packetOrderProcessor.isRightClicking() || player.packetOrderProcessor.isPicking() || player.packetOrderProcessor.isDigging()) {
|
||||
String verbose = "type=release, attacking=" + player.packetOrderProcessor.isAttacking()
|
||||
if (player.packetOrderProcessor.isAttackingOrStabbing() || player.packetOrderProcessor.isRightClicking() || player.packetOrderProcessor.isPicking() || player.packetOrderProcessor.isDigging()) {
|
||||
String verbose = "type=release, attacking=" + player.packetOrderProcessor.isAttackingOrStabbing()
|
||||
+ ", rightClicking=" + player.packetOrderProcessor.isRightClicking()
|
||||
+ ", picking=" + player.packetOrderProcessor.isPicking()
|
||||
+ ", digging=" + player.packetOrderProcessor.isDigging();
|
||||
@@ -134,6 +128,23 @@ public class PacketOrderI extends Check implements PostPredictionCheck {
|
||||
setback = false;
|
||||
}
|
||||
|
||||
private void onAttack(PacketReceiveEvent event) {
|
||||
if (player.packetOrderProcessor.isRightClicking() || player.packetOrderProcessor.isPicking() || player.packetOrderProcessor.isReleasing() || player.packetOrderProcessor.isDigging()) {
|
||||
String verbose = "type=attack" + ", rightClicking=" + player.packetOrderProcessor.isRightClicking()
|
||||
+ ", picking=" + player.packetOrderProcessor.isPicking()
|
||||
+ ", releasing=" + player.packetOrderProcessor.isReleasing()
|
||||
+ ", digging=" + player.packetOrderProcessor.isDigging();
|
||||
if (!player.canSkipTicks()) {
|
||||
if (flagAndAlert(verbose) && shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
} else {
|
||||
flags.add(verbose);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReload(ConfigManager config) {
|
||||
exemptPlacingWhileDigging = config.getBooleanElse(getConfigName() + ".exempt-placing-while-digging", false);
|
||||
|
||||
@@ -19,6 +19,7 @@ public class PacketOrderJ extends Check implements PostPredictionCheck {
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.PLAYER_BLOCK_PLACEMENT || event.getPacketType() == PacketType.Play.Client.USE_ITEM) {
|
||||
// we don't check stabbing here because you don't need to target an entity to stab
|
||||
if (player.packetOrderProcessor.isAttacking() && !player.packetOrderProcessor.isInteracting()) {
|
||||
if (!player.canSkipTicks()) {
|
||||
if (flagAndAlert() && shouldModifyPackets()) {
|
||||
|
||||
@@ -38,6 +38,7 @@ public final class PacketOrderProcessor extends Check implements PacketCheck {
|
||||
private boolean leavingBed;
|
||||
private boolean startingToGlide;
|
||||
private boolean jumpingWithMount;
|
||||
private boolean stabbing;
|
||||
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
@@ -57,12 +58,17 @@ public final class PacketOrderProcessor extends Check implements PacketCheck {
|
||||
}
|
||||
}
|
||||
|
||||
if (packetType == PacketType.Play.Client.ATTACK || packetType == PacketType.Play.Client.SPECTATE_ENTITY) {
|
||||
attacking = true;
|
||||
}
|
||||
|
||||
if (packetType == PacketType.Play.Client.PLAYER_DIGGING) {
|
||||
switch (new WrapperPlayClientPlayerDigging(event).getAction()) {
|
||||
case SWAP_ITEM_WITH_OFFHAND -> swapping = true;
|
||||
case DROP_ITEM, DROP_ITEM_STACK -> dropping = true;
|
||||
case RELEASE_USE_ITEM -> releasing = true;
|
||||
case FINISHED_DIGGING, CANCELLED_DIGGING, START_DIGGING -> digging = true;
|
||||
case STAB -> stabbing = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +138,7 @@ public final class PacketOrderProcessor extends Check implements PacketCheck {
|
||||
leavingBed = false;
|
||||
startingToGlide = false;
|
||||
jumpingWithMount = false;
|
||||
stabbing = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,4 +146,9 @@ public final class PacketOrderProcessor extends Check implements PacketCheck {
|
||||
public boolean isRightClicking() {
|
||||
return placing || using || interacting;
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
public boolean isAttackingOrStabbing() {
|
||||
return attacking || stabbing;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ 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;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon;
|
||||
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
|
||||
import com.github.retrooper.packetevents.protocol.player.DiggingAction;
|
||||
import com.github.retrooper.packetevents.protocol.player.GameMode;
|
||||
@@ -576,7 +577,7 @@ public class CheckManagerListener extends PacketListenerAbstract {
|
||||
player.packetStateData.cancelDuplicatePacket = false;
|
||||
}
|
||||
|
||||
if (event.getPacketType() == PacketType.Play.Client.CLIENT_TICK_END) {
|
||||
if (event.getPacketType() == PacketType.Play.Client.CLIENT_TICK_END && player.supportsEndTick()) {
|
||||
player.serverOpenedInventoryThisTick = false;
|
||||
if (!player.packetStateData.didSendMovementBeforeTickEnd) {
|
||||
// The player didn't send a movement packet, so we can predict this like we had idle tick on 1.8
|
||||
@@ -602,11 +603,12 @@ public class CheckManagerListener extends PacketListenerAbstract {
|
||||
GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser());
|
||||
if (player == null) return;
|
||||
|
||||
if (event.getPacketType() == PacketType.Play.Server.OPEN_WINDOW) {
|
||||
final PacketTypeCommon packetType = event.getPacketType();
|
||||
if (packetType == PacketType.Play.Server.OPEN_WINDOW || packetType == PacketType.Play.Server.OPEN_HORSE_WINDOW) {
|
||||
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> player.serverOpenedInventoryThisTick = true);
|
||||
}
|
||||
|
||||
if (event.getPacketType() == PacketType.Play.Server.BUNDLE) {
|
||||
if (packetType == PacketType.Play.Server.BUNDLE) {
|
||||
player.packetStateData.sendingBundlePacket = !player.packetStateData.sendingBundlePacket;
|
||||
}
|
||||
|
||||
@@ -627,7 +629,7 @@ public class CheckManagerListener extends PacketListenerAbstract {
|
||||
//
|
||||
// removed a large rant, but I'm keeping this out of context insult below
|
||||
// EVEN A BUNCH OF MONKEYS ON A TYPEWRITER COULDNT WRITE WORSE NETCODE THAN MOJANG
|
||||
if (!player.packetStateData.lastPacketWasTeleport && flying.hasPositionChanged() && flying.hasRotationChanged() &&
|
||||
if (flying.hasPositionChanged() && flying.hasRotationChanged() &&
|
||||
// Ground status will never change in this stupidity packet
|
||||
((flying.isOnGround() == player.packetStateData.packetPlayerOnGround
|
||||
// Mojang added this stupid mechanic in 1.17
|
||||
|
||||
@@ -62,9 +62,11 @@ public class PacketEntityAction extends PacketListenerAbstract {
|
||||
player.onPacketCancel();
|
||||
break;
|
||||
}
|
||||
|
||||
player.checkManager.getPostPredictionCheck(ElytraA.class).onStartGliding(event);
|
||||
|
||||
// Starting fall flying is server sided on 1.14 and below
|
||||
if (player.getClientVersion().isOlderThan(ClientVersion.V_1_15)) return;
|
||||
player.checkManager.getPostPredictionCheck(ElytraA.class).onStartGliding(event);
|
||||
|
||||
// This shouldn't be needed with latency compensated inventories
|
||||
// TODO: Remove this?
|
||||
|
||||
@@ -26,7 +26,7 @@ public class PacketHidePlayerInfo extends PacketListenerAbstract {
|
||||
@Override
|
||||
public void onPacketSend(PacketSendEvent event) {
|
||||
if (event.getPacketType() == PacketType.Play.Server.PLAYER_INFO) {
|
||||
//iterate through players and fake their game mode if they are spectating via grim spectate
|
||||
// iterate through players and fake their game mode if they are spectating via grim spectate
|
||||
if (PacketEvents.getAPI().getServerManager().getVersion().isOlderThanOrEquals(ServerVersion.V_1_12_2))
|
||||
return;
|
||||
|
||||
@@ -50,7 +50,7 @@ public class PacketHidePlayerInfo extends PacketListenerAbstract {
|
||||
}
|
||||
}
|
||||
|
||||
//if amount of hidden players is the amount of players updated & is an update game mode action just cancel it
|
||||
// if amount of hidden players is the amount of players updated & is an update game mode action just cancel it
|
||||
if (hideCount == nmsPlayerInfoDataList.size() && info.getAction() == WrapperPlayServerPlayerInfo.Action.UPDATE_GAME_MODE) {
|
||||
event.setCancelled(true);
|
||||
} else if (hideCount > 0) {
|
||||
@@ -60,55 +60,55 @@ public class PacketHidePlayerInfo extends PacketListenerAbstract {
|
||||
} else if (event.getPacketType() == PacketType.Play.Server.PLAYER_INFO_UPDATE) {
|
||||
GrimPlayer receiver = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser());
|
||||
if (receiver == null) return;
|
||||
//create wrappers
|
||||
|
||||
WrapperPlayServerPlayerInfoUpdate wrapper = new WrapperPlayServerPlayerInfoUpdate(event);
|
||||
EnumSet<WrapperPlayServerPlayerInfoUpdate.Action> actions = wrapper.getActions();
|
||||
//player's game mode updated
|
||||
if (actions.contains(WrapperPlayServerPlayerInfoUpdate.Action.UPDATE_GAME_MODE)) {
|
||||
boolean onlyGameMode = actions.size() == 1; // packet is being sent to only update game modes
|
||||
int hideCount = 0;
|
||||
List<WrapperPlayServerPlayerInfoUpdate.PlayerInfo> modified = new ArrayList<>(wrapper.getEntries().size());
|
||||
//iterate through the player entries
|
||||
for (WrapperPlayServerPlayerInfoUpdate.PlayerInfo entry : wrapper.getEntries()) {
|
||||
//check if the player should be hidden
|
||||
WrapperPlayServerPlayerInfoUpdate.PlayerInfo modifiedPacket = null;
|
||||
final UserProfile gameProfile = entry.getGameProfile();
|
||||
if (GrimAPI.INSTANCE.getSpectateManager().shouldHidePlayer(receiver, gameProfile.getUUID())) {
|
||||
hideCount++;
|
||||
//modify & create a new packet from pre-existing one if they are a spectator
|
||||
if (entry.getGameMode() == GameMode.SPECTATOR) {
|
||||
modifiedPacket = new WrapperPlayServerPlayerInfoUpdate.PlayerInfo(
|
||||
gameProfile,
|
||||
entry.isListed(),
|
||||
entry.getLatency(),
|
||||
GameMode.SURVIVAL,
|
||||
entry.getDisplayName(),
|
||||
entry.getChatSession()
|
||||
);
|
||||
modified.add(modifiedPacket);
|
||||
}
|
||||
}
|
||||
if (!actions.contains(WrapperPlayServerPlayerInfoUpdate.Action.UPDATE_GAME_MODE)) return;
|
||||
|
||||
if (modifiedPacket == null) { //if the packet wasn't modified, send original
|
||||
modified.add(entry);
|
||||
} else if (!onlyGameMode) { //if more than just the game mode updated, modify the packet
|
||||
boolean onlyGameMode = actions.size() == 1; // packet is being sent to only update game modes
|
||||
int hideCount = 0;
|
||||
List<WrapperPlayServerPlayerInfoUpdate.PlayerInfo> modified = new ArrayList<>(wrapper.getEntries().size());
|
||||
// iterate through the player entries
|
||||
for (WrapperPlayServerPlayerInfoUpdate.PlayerInfo entry : wrapper.getEntries()) {
|
||||
// check if the player should be hidden
|
||||
WrapperPlayServerPlayerInfoUpdate.PlayerInfo modifiedPacket = null;
|
||||
final UserProfile gameProfile = entry.getGameProfile();
|
||||
if (GrimAPI.INSTANCE.getSpectateManager().shouldHidePlayer(receiver, gameProfile.getUUID())) {
|
||||
hideCount++;
|
||||
// modify & create a new packet from pre-existing one if they are a spectator
|
||||
if (entry.getGameMode() == GameMode.SPECTATOR) {
|
||||
modifiedPacket = new WrapperPlayServerPlayerInfoUpdate.PlayerInfo(
|
||||
gameProfile,
|
||||
entry.isListed(),
|
||||
entry.getLatency(),
|
||||
GameMode.SURVIVAL,
|
||||
entry.getDisplayName(),
|
||||
entry.getChatSession(),
|
||||
entry.getListOrder(),
|
||||
entry.isShowHat()
|
||||
);
|
||||
modified.add(modifiedPacket);
|
||||
} //if only the game mode was updated and the packet was modified, don't send anything
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// if the amount of hidden players & modified entries are the same
|
||||
if (hideCount == modified.size()) {
|
||||
if (onlyGameMode) { // if only the game mode changed, cancel
|
||||
event.setCancelled(true);
|
||||
} else { //if more than the game mode changed, remove the action
|
||||
wrapper.getActions().remove(WrapperPlayServerPlayerInfoUpdate.Action.UPDATE_GAME_MODE);
|
||||
event.markForReEncode(true);
|
||||
}
|
||||
} else { //modify entries
|
||||
wrapper.setEntries(modified);
|
||||
if (modifiedPacket == null) { // if the packet wasn't modified, send original
|
||||
modified.add(entry);
|
||||
} else if (!onlyGameMode) { // if more than just the game mode updated, modify the packet
|
||||
modified.add(modifiedPacket);
|
||||
} // if only the game mode was updated and the packet was modified, don't send anything
|
||||
}
|
||||
|
||||
// if the amount of hidden players & modified entries are the same
|
||||
if (hideCount == modified.size()) {
|
||||
if (onlyGameMode) { // if only the game mode changed, cancel
|
||||
event.setCancelled(true);
|
||||
} else { // if more than the game mode changed, remove the action
|
||||
wrapper.getActions().remove(WrapperPlayServerPlayerInfoUpdate.Action.UPDATE_GAME_MODE);
|
||||
event.markForReEncode(true);
|
||||
}
|
||||
} else { // modify entries
|
||||
wrapper.setEntries(modified);
|
||||
event.markForReEncode(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,12 @@ import com.github.retrooper.packetevents.protocol.item.ItemStack;
|
||||
import com.github.retrooper.packetevents.protocol.item.enchantment.type.EnchantmentTypes;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
|
||||
import com.github.retrooper.packetevents.protocol.player.DiggingAction;
|
||||
import com.github.retrooper.packetevents.protocol.player.GameMode;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientAttack;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerDigging;
|
||||
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientSpectateEntity;
|
||||
|
||||
public class PacketPlayerAttack extends PacketListenerAbstract {
|
||||
|
||||
@@ -33,81 +37,124 @@ public class PacketPlayerAttack extends PacketListenerAbstract {
|
||||
|
||||
if (player == null) return;
|
||||
|
||||
// The entity does not exist
|
||||
if (!player.compensatedEntities.entityMap.containsKey(interact.getEntityId()) && !player.compensatedEntities.serverPositionsMap.containsKey(interact.getEntityId())
|
||||
// the list of entities used to raytrace isn't the same as the list of entities in the world in pre-1.14 (wtf mojang)
|
||||
&& (!player.compensatedEntities.entitiesRemovedThisTick.contains(interact.getEntityId()) || player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_14))) {
|
||||
final BadPacketsW badPacketsW = player.checkManager.getCheck(BadPacketsW.class);
|
||||
if (badPacketsW.flagAndAlert("entityId=" + interact.getEntityId()) && badPacketsW.shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
return;
|
||||
}
|
||||
int entityId = interact.getEntityId();
|
||||
|
||||
if (interact.getAction() == WrapperPlayClientInteractEntity.InteractAction.ATTACK) {
|
||||
if (player.isResetItemUsageOnAttack()) {
|
||||
GrimAPI.INSTANCE.getItemResetHandler().resetItemUsage(player.platformPlayer);
|
||||
}
|
||||
onAttack(event, player, entityId);
|
||||
} else {
|
||||
if (isInvalidEntity(event, player, entityId)) return;
|
||||
|
||||
// This is not vanilla behaviour as the attack damage attribute is marked as not synced to the client
|
||||
// However, plugins can still set this by sending an attributes packet
|
||||
if (player.compensatedEntities.self.getAttributeValue(Attributes.ATTACK_DAMAGE) <= 0) return;
|
||||
|
||||
ItemStack heldItem = player.inventory.getHeldItem();
|
||||
PacketEntity entity = player.compensatedEntities.getEntity(interact.getEntityId());
|
||||
|
||||
if (entity != null && (!entity.isLivingEntity || entity.type == EntityTypes.PLAYER || entity.type == EntityTypes.PAINTING
|
||||
|| entity.type == EntityTypes.ENDER_DRAGON && player.getClientVersion().isOlderThan(ClientVersion.V_1_21_2))) {
|
||||
int knockbackLevel = player.getClientVersion().isOlderThan(ClientVersion.V_1_21) && heldItem != null
|
||||
? heldItem.getEnchantmentLevel(EnchantmentTypes.KNOCKBACK)
|
||||
: 0;
|
||||
final boolean hasNegativeKB = knockbackLevel < 0;
|
||||
|
||||
final boolean isLegacyPlayer = player.getClientVersion().isOlderThanOrEquals(ClientVersion.V_1_8);
|
||||
// assume cooldown is full on 1.8 servers
|
||||
final boolean sufficientCooldownProgress = isLegacyPlayer || player.attackCooldown.getMinimumProgress() > 0.9F;
|
||||
|
||||
if (!isLegacyPlayer) {
|
||||
knockbackLevel = Math.max(knockbackLevel, 0);
|
||||
if (interact.getAction() == WrapperPlayClientInteractEntity.InteractAction.INTERACT) {
|
||||
// Interacting with a horse in versions 1.13- will cause the client to
|
||||
// set the player's rotation to the horse's rotation
|
||||
if (player.compensatedEntities.getEntity(entityId) instanceof PacketEntityHorse
|
||||
&& player.getClientVersion().isOlderThanOrEquals(ClientVersion.V_1_13)) {
|
||||
player.packetStateData.horseInteractCausedForcedRotation = true;
|
||||
}
|
||||
|
||||
// 1.8 players who are packet sprinting WILL get slowed
|
||||
// 1.9+ players who are packet sprinting might not, based on attack cooldown
|
||||
// Players with knockback enchantments always get slowed
|
||||
|
||||
if (player.lastSprinting && !hasNegativeKB && sufficientCooldownProgress || knockbackLevel > 0) {
|
||||
player.minAttackSlow++;
|
||||
player.maxAttackSlow++;
|
||||
|
||||
// Players cannot slow themselves twice in one tick without a knockback sword
|
||||
if (knockbackLevel == 0) {
|
||||
player.maxAttackSlow = player.minAttackSlow = 1;
|
||||
}
|
||||
} else if (!isLegacyPlayer && player.lastSprinting) {
|
||||
// 1.9+ players who have attack speed cannot slow themselves twice in one tick because their attack cooldown gets reset on swing.
|
||||
if (player.maxAttackSlow > 0
|
||||
&& PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_9)
|
||||
&& player.compensatedEntities.self.getAttributeValue(Attributes.ATTACK_SPEED) < 16) { // 16 is a reasonable limit
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.9+ player who might have been slowed, but we can't be sure
|
||||
player.maxAttackSlow++;
|
||||
}
|
||||
}
|
||||
|
||||
if (player.gamemode != GameMode.SPECTATOR) {
|
||||
player.attackCooldown.reset();
|
||||
}
|
||||
} else if (interact.getAction() == WrapperPlayClientInteractEntity.InteractAction.INTERACT) {
|
||||
// Interacting with a horse in versions 1.13- will cause the client to
|
||||
// set the player's rotation to the horse's rotation
|
||||
if (player.compensatedEntities.getEntity(interact.getEntityId()) instanceof PacketEntityHorse
|
||||
&& player.getClientVersion().isOlderThanOrEquals(ClientVersion.V_1_13)) {
|
||||
player.packetStateData.horseInteractCausedForcedRotation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event.getPacketType() == PacketType.Play.Client.PLAYER_DIGGING) {
|
||||
WrapperPlayClientPlayerDigging packet = new WrapperPlayClientPlayerDigging(event);
|
||||
if (packet.getAction() == DiggingAction.STAB) {
|
||||
GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser());
|
||||
if (player == null) return;
|
||||
|
||||
if (player.isResetItemUsageOnAttack()) {
|
||||
GrimAPI.INSTANCE.getItemResetHandler().resetItemUsage(player.platformPlayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event.getPacketType() == PacketType.Play.Client.ATTACK) {
|
||||
GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser());
|
||||
if (player == null) return;
|
||||
|
||||
WrapperPlayClientAttack packet = new WrapperPlayClientAttack(event);
|
||||
onAttack(event, player, packet.getEntityId());
|
||||
}
|
||||
|
||||
if (event.getPacketType() == PacketType.Play.Client.SPECTATE_ENTITY) {
|
||||
GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser());
|
||||
if (player == null) return;
|
||||
|
||||
WrapperPlayClientSpectateEntity packet = new WrapperPlayClientSpectateEntity(event);
|
||||
onAttack(event, player, packet.getEntityId());
|
||||
}
|
||||
}
|
||||
|
||||
private void onAttack(PacketReceiveEvent event, GrimPlayer player, int entityId) {
|
||||
if (isInvalidEntity(event, player, entityId)) return;
|
||||
|
||||
if (player.isResetItemUsageOnAttack()) {
|
||||
GrimAPI.INSTANCE.getItemResetHandler().resetItemUsage(player.platformPlayer);
|
||||
}
|
||||
|
||||
// This is not vanilla behaviour as the attack damage attribute is marked as not synced to the client
|
||||
// However, plugins can still set this by sending an attributes packet
|
||||
if (player.compensatedEntities.self.getAttributeValue(Attributes.ATTACK_DAMAGE) <= 0) return;
|
||||
|
||||
ItemStack heldItem = player.inventory.getHeldItem();
|
||||
PacketEntity entity = player.compensatedEntities.getEntity(entityId);
|
||||
|
||||
if (entity != null && (!entity.isLivingEntity || entity.type == EntityTypes.PLAYER || entity.type == EntityTypes.PAINTING
|
||||
|| entity.type == EntityTypes.ENDER_DRAGON && player.getClientVersion().isOlderThan(ClientVersion.V_1_21_2))) {
|
||||
int knockbackLevel = player.getClientVersion().isOlderThan(ClientVersion.V_1_21) && heldItem != null
|
||||
? heldItem.getEnchantmentLevel(EnchantmentTypes.KNOCKBACK)
|
||||
: 0;
|
||||
final boolean hasNegativeKB = knockbackLevel < 0;
|
||||
|
||||
final boolean isLegacyPlayer = player.getClientVersion().isOlderThanOrEquals(ClientVersion.V_1_8);
|
||||
// assume cooldown is full on 1.8 servers
|
||||
final boolean sufficientCooldownProgress = isLegacyPlayer || player.attackCooldown.getMinimumProgress() > 0.9F;
|
||||
|
||||
if (!isLegacyPlayer) {
|
||||
knockbackLevel = Math.max(knockbackLevel, 0);
|
||||
}
|
||||
|
||||
// 1.8 players who are packet sprinting WILL get slowed
|
||||
// 1.9+ players who are packet sprinting might not, based on attack cooldown
|
||||
// Players with knockback enchantments always get slowed
|
||||
|
||||
if (player.lastSprinting && !hasNegativeKB && sufficientCooldownProgress || knockbackLevel > 0) {
|
||||
player.minAttackSlow++;
|
||||
player.maxAttackSlow++;
|
||||
|
||||
// Players cannot slow themselves twice in one tick without a knockback sword
|
||||
if (knockbackLevel == 0) {
|
||||
player.maxAttackSlow = player.minAttackSlow = 1;
|
||||
}
|
||||
} else if (!isLegacyPlayer && player.lastSprinting) {
|
||||
// 1.9+ players who have attack speed cannot slow themselves twice in one tick because their attack cooldown gets reset on swing.
|
||||
if (player.maxAttackSlow > 0
|
||||
&& PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_9)
|
||||
&& player.compensatedEntities.self.getAttributeValue(Attributes.ATTACK_SPEED) < 16) { // 16 is a reasonable limit
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.9+ player who might have been slowed, but we can't be sure
|
||||
player.maxAttackSlow++;
|
||||
}
|
||||
}
|
||||
|
||||
if (player.gamemode != GameMode.SPECTATOR) {
|
||||
player.attackCooldown.reset();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInvalidEntity(PacketReceiveEvent event, GrimPlayer player, int entityId) {
|
||||
// The entity does not exist
|
||||
if (!player.compensatedEntities.entityMap.containsKey(entityId) && !player.compensatedEntities.serverPositionsMap.containsKey(entityId)
|
||||
// the list of entities used to raytrace isn't the same as the list of entities in the world in pre-1.14 (wtf mojang)
|
||||
&& (!player.compensatedEntities.entitiesRemovedThisTick.contains(entityId) || player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_14))) {
|
||||
final BadPacketsW badPacketsW = player.checkManager.getCheck(BadPacketsW.class);
|
||||
if (badPacketsW.flagAndAlert("entityId=" + entityId) && badPacketsW.shouldModifyPackets()) {
|
||||
event.setCancelled(true);
|
||||
player.onPacketCancel();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,7 @@ public class CheckManager {
|
||||
.put(ExploitB.class, new ExploitB(player))
|
||||
.put(BadPacketsA.class, new BadPacketsA(player))
|
||||
.put(BadPacketsB.class, new BadPacketsB(player))
|
||||
.put(BadPacketsC.class, new BadPacketsC(player))
|
||||
.put(BadPacketsD.class, new BadPacketsD(player))
|
||||
.put(BadPacketsE.class, new BadPacketsE(player))
|
||||
.put(BadPacketsF.class, new BadPacketsF(player))
|
||||
|
||||
@@ -30,6 +30,30 @@ public class DatabaseUtils {
|
||||
return new UUID(msb, lsb);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves a UUID from a ResultSet.
|
||||
* This handles both PostgreSQL (which returns a native {@link UUID} object)
|
||||
* and MySQL/SQLite (which return a {@code byte[]}).
|
||||
*
|
||||
* @param resultSet The ResultSet to read from.
|
||||
* @param columnName The name of the UUID column.
|
||||
* @return The UUID.
|
||||
* @throws SQLException If a database access error occurs, or if the UUID column
|
||||
* contains an unexpected type or invalid data.
|
||||
*/
|
||||
public static UUID getUuid(ResultSet resultSet, String columnName) throws SQLException {
|
||||
Object uuidObject = resultSet.getObject(columnName);
|
||||
|
||||
if (uuidObject instanceof byte[] uuidBytes) {
|
||||
return bytesToUuid(uuidBytes);
|
||||
} else if (uuidObject instanceof UUID uuid) {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
throw new SQLException("Unexpected UUID type: " + (uuidObject == null ? "null" : uuidObject.getClass().getName()));
|
||||
}
|
||||
|
||||
// --- Generic Deduplication Lookup (uses DatabaseDialect) ---
|
||||
public static long getOrCreateId(Connection connection, DatabaseDialect dialect, String tableName, String stringColumnName, String value) throws SQLException {
|
||||
// Step 1: Attempt to insert the string.
|
||||
|
||||
@@ -13,8 +13,7 @@ public record Violation(String server, UUID uuid, String checkName, String verbo
|
||||
List<Violation> violations = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
String server = resultSet.getString(DatabaseConstants.SERVERS_STRING_COLUMN);
|
||||
byte[] uuidBytes = resultSet.getBytes(DatabaseConstants.VIOLATIONS_UUID_COLUMN);
|
||||
UUID uuid = DatabaseUtils.bytesToUuid(uuidBytes);
|
||||
UUID uuid = DatabaseUtils.getUuid(resultSet, DatabaseConstants.VIOLATIONS_UUID_COLUMN);
|
||||
String checkName = resultSet.getString(DatabaseConstants.CHECK_NAMES_STRING_COLUMN);
|
||||
String verbose = resultSet.getString(DatabaseConstants.VIOLATIONS_VERBOSE_COLUMN);
|
||||
int vl = resultSet.getInt(DatabaseConstants.VIOLATIONS_VL_COLUMN);
|
||||
|
||||
+19
-51
@@ -56,9 +56,6 @@ public class MySQLViolationDatabase implements ViolationDatabase {
|
||||
DatabaseConstants.SERVERS_STRING_COLUMN + " VARCHAR(255) NOT NULL UNIQUE" +
|
||||
")"
|
||||
).execute();
|
||||
connection.prepareStatement(
|
||||
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.SERVERS_TABLE + "_name ON " + DatabaseConstants.SERVERS_TABLE + "(" + DatabaseConstants.SERVERS_STRING_COLUMN + ");"
|
||||
).execute();
|
||||
|
||||
// 2. Create Lookup Table for Check Names
|
||||
connection.prepareStatement(
|
||||
@@ -67,9 +64,6 @@ public class MySQLViolationDatabase implements ViolationDatabase {
|
||||
DatabaseConstants.CHECK_NAMES_STRING_COLUMN + " VARCHAR(255) NOT NULL UNIQUE" +
|
||||
")"
|
||||
).execute();
|
||||
connection.prepareStatement(
|
||||
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.CHECK_NAMES_TABLE + "_string ON " + DatabaseConstants.CHECK_NAMES_TABLE + "(" + DatabaseConstants.CHECK_NAMES_STRING_COLUMN + ");"
|
||||
).execute();
|
||||
|
||||
// --- NEW LOOKUP TABLES ---
|
||||
// 3. Create Lookup Table for Grim Versions
|
||||
@@ -79,9 +73,6 @@ public class MySQLViolationDatabase implements ViolationDatabase {
|
||||
DatabaseConstants.GRIM_VERSIONS_STRING_COLUMN + " VARCHAR(255) NOT NULL UNIQUE" +
|
||||
")"
|
||||
).execute();
|
||||
connection.prepareStatement(
|
||||
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.GRIM_VERSIONS_TABLE + "_string ON " + DatabaseConstants.GRIM_VERSIONS_TABLE + "(" + DatabaseConstants.GRIM_VERSIONS_STRING_COLUMN + ");"
|
||||
).execute();
|
||||
|
||||
// 4. Create Lookup Table for Client Brands
|
||||
connection.prepareStatement(
|
||||
@@ -90,9 +81,6 @@ public class MySQLViolationDatabase implements ViolationDatabase {
|
||||
DatabaseConstants.CLIENT_BRANDS_STRING_COLUMN + " VARCHAR(255) NOT NULL UNIQUE" +
|
||||
")"
|
||||
).execute();
|
||||
connection.prepareStatement(
|
||||
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.CLIENT_BRANDS_TABLE + "_string ON " + DatabaseConstants.CLIENT_BRANDS_TABLE + "(" + DatabaseConstants.CLIENT_BRANDS_STRING_COLUMN + ");"
|
||||
).execute();
|
||||
|
||||
// 5. Create Lookup Table for Client Versions
|
||||
connection.prepareStatement(
|
||||
@@ -101,9 +89,6 @@ public class MySQLViolationDatabase implements ViolationDatabase {
|
||||
DatabaseConstants.CLIENT_VERSIONS_STRING_COLUMN + " VARCHAR(255) NOT NULL UNIQUE" +
|
||||
")"
|
||||
).execute();
|
||||
connection.prepareStatement(
|
||||
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.CLIENT_VERSIONS_TABLE + "_string ON " + DatabaseConstants.CLIENT_VERSIONS_TABLE + "(" + DatabaseConstants.CLIENT_VERSIONS_STRING_COLUMN + ");"
|
||||
).execute();
|
||||
|
||||
// 6. Create Lookup Table for Server Versions
|
||||
connection.prepareStatement(
|
||||
@@ -112,9 +97,6 @@ public class MySQLViolationDatabase implements ViolationDatabase {
|
||||
DatabaseConstants.SERVER_VERSIONS_STRING_COLUMN + " VARCHAR(255) NOT NULL UNIQUE" +
|
||||
")"
|
||||
).execute();
|
||||
connection.prepareStatement(
|
||||
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.SERVER_VERSIONS_TABLE + "_string ON " + DatabaseConstants.SERVER_VERSIONS_TABLE + "(" + DatabaseConstants.SERVER_VERSIONS_STRING_COLUMN + ");"
|
||||
).execute();
|
||||
// --- END NEW LOOKUP TABLES ---
|
||||
|
||||
// 7. Create Main Violations Table with ALL Foreign Keys and optimized UUID
|
||||
@@ -127,44 +109,30 @@ public class MySQLViolationDatabase implements ViolationDatabase {
|
||||
DatabaseConstants.VIOLATIONS_VERBOSE_COLUMN + " TEXT NOT NULL, " +
|
||||
DatabaseConstants.VIOLATIONS_VL_COLUMN + " INT NOT NULL, " +
|
||||
DatabaseConstants.VIOLATIONS_CREATED_AT_COLUMN + " BIGINT NOT NULL, " +
|
||||
DatabaseConstants.VIOLATIONS_GRIM_VERSION_ID_COLUMN + " BIGINT NOT NULL, " + // NEW
|
||||
DatabaseConstants.VIOLATIONS_CLIENT_BRAND_ID_COLUMN + " BIGINT NOT NULL, " + // NEW
|
||||
DatabaseConstants.VIOLATIONS_CLIENT_VERSION_ID_COLUMN + " BIGINT NOT NULL, " + // NEW
|
||||
DatabaseConstants.VIOLATIONS_SERVER_VERSION_ID_COLUMN + " BIGINT NOT NULL, " + // NEW
|
||||
DatabaseConstants.VIOLATIONS_GRIM_VERSION_ID_COLUMN + " BIGINT NOT NULL, " +
|
||||
DatabaseConstants.VIOLATIONS_CLIENT_BRAND_ID_COLUMN + " BIGINT NOT NULL, " +
|
||||
DatabaseConstants.VIOLATIONS_CLIENT_VERSION_ID_COLUMN + " BIGINT NOT NULL, " +
|
||||
DatabaseConstants.VIOLATIONS_SERVER_VERSION_ID_COLUMN + " BIGINT NOT NULL, " +
|
||||
|
||||
"FOREIGN KEY (" + DatabaseConstants.VIOLATIONS_SERVER_ID_COLUMN + ") REFERENCES " + DatabaseConstants.SERVERS_TABLE + "(id), " +
|
||||
"FOREIGN KEY (" + DatabaseConstants.VIOLATIONS_CHECK_NAME_ID_COLUMN + ") REFERENCES " + DatabaseConstants.CHECK_NAMES_TABLE + "(id), " +
|
||||
"FOREIGN KEY (" + DatabaseConstants.VIOLATIONS_GRIM_VERSION_ID_COLUMN + ") REFERENCES " + DatabaseConstants.GRIM_VERSIONS_TABLE + "(id), " + // NEW
|
||||
"FOREIGN KEY (" + DatabaseConstants.VIOLATIONS_CLIENT_BRAND_ID_COLUMN + ") REFERENCES " + DatabaseConstants.CLIENT_BRANDS_TABLE + "(id), " + // NEW
|
||||
"FOREIGN KEY (" + DatabaseConstants.VIOLATIONS_CLIENT_VERSION_ID_COLUMN + ") REFERENCES " + DatabaseConstants.CLIENT_VERSIONS_TABLE + "(id), " + // NEW
|
||||
"FOREIGN KEY (" + DatabaseConstants.VIOLATIONS_SERVER_VERSION_ID_COLUMN + ") REFERENCES " + DatabaseConstants.SERVER_VERSIONS_TABLE + "(id)" + // NEW
|
||||
"FOREIGN KEY (" + DatabaseConstants.VIOLATIONS_GRIM_VERSION_ID_COLUMN + ") REFERENCES " + DatabaseConstants.GRIM_VERSIONS_TABLE + "(id), " +
|
||||
"FOREIGN KEY (" + DatabaseConstants.VIOLATIONS_CLIENT_BRAND_ID_COLUMN + ") REFERENCES " + DatabaseConstants.CLIENT_BRANDS_TABLE + "(id), " +
|
||||
"FOREIGN KEY (" + DatabaseConstants.VIOLATIONS_CLIENT_VERSION_ID_COLUMN + ") REFERENCES " + DatabaseConstants.CLIENT_VERSIONS_TABLE + "(id), " +
|
||||
"FOREIGN KEY (" + DatabaseConstants.VIOLATIONS_SERVER_VERSION_ID_COLUMN + ") REFERENCES " + DatabaseConstants.SERVER_VERSIONS_TABLE + "(id)" +
|
||||
")"
|
||||
).execute();
|
||||
|
||||
// 8. Create Indexes for efficient querying on main table (includes new FKs)
|
||||
connection.prepareStatement(
|
||||
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_uuid ON " + DatabaseConstants.VIOLATIONS_TABLE + "(" + DatabaseConstants.VIOLATIONS_UUID_COLUMN + ");"
|
||||
).execute();
|
||||
connection.prepareStatement(
|
||||
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_created_at ON " + DatabaseConstants.VIOLATIONS_TABLE + "(" + DatabaseConstants.VIOLATIONS_CREATED_AT_COLUMN + ");"
|
||||
).execute();
|
||||
connection.prepareStatement(
|
||||
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_server_id ON " + DatabaseConstants.VIOLATIONS_TABLE + "(" + DatabaseConstants.VIOLATIONS_SERVER_ID_COLUMN + ");"
|
||||
).execute();
|
||||
connection.prepareStatement(
|
||||
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_check_name_id ON " + DatabaseConstants.VIOLATIONS_TABLE + "(" + DatabaseConstants.VIOLATIONS_CHECK_NAME_ID_COLUMN + ");"
|
||||
).execute();
|
||||
connection.prepareStatement(
|
||||
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_grim_version_id ON " + DatabaseConstants.VIOLATIONS_TABLE + "(" + DatabaseConstants.VIOLATIONS_GRIM_VERSION_ID_COLUMN + ");" // NEW
|
||||
).execute();
|
||||
connection.prepareStatement(
|
||||
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_client_brand_id ON " + DatabaseConstants.VIOLATIONS_TABLE + "(" + DatabaseConstants.VIOLATIONS_CLIENT_BRAND_ID_COLUMN + ");" // NEW
|
||||
).execute();
|
||||
connection.prepareStatement(
|
||||
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_client_version_id ON " + DatabaseConstants.VIOLATIONS_TABLE + "(" + DatabaseConstants.VIOLATIONS_CLIENT_VERSION_ID_COLUMN + ");" // NEW
|
||||
).execute();
|
||||
connection.prepareStatement(
|
||||
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_server_version_id ON " + DatabaseConstants.VIOLATIONS_TABLE + "(" + DatabaseConstants.VIOLATIONS_SERVER_VERSION_ID_COLUMN + ");" // NEW
|
||||
).execute();
|
||||
try {
|
||||
connection.prepareStatement(
|
||||
"CREATE INDEX idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_uuid_created_at ON " +
|
||||
DatabaseConstants.VIOLATIONS_TABLE + "(" +
|
||||
DatabaseConstants.VIOLATIONS_UUID_COLUMN + ", " +
|
||||
DatabaseConstants.VIOLATIONS_CREATED_AT_COLUMN +
|
||||
")"
|
||||
).execute();
|
||||
} catch (SQLException ignored) {}
|
||||
|
||||
|
||||
} catch (SQLException ex) {
|
||||
LogUtil.error("Failed to generate violations database:", ex);
|
||||
|
||||
@@ -6,6 +6,7 @@ import ac.grim.grimac.platform.api.player.PlatformPlayer;
|
||||
import ac.grim.grimac.platform.api.sender.Sender;
|
||||
import ac.grim.grimac.player.GrimPlayer;
|
||||
import ac.grim.grimac.utils.data.webhook.discord.CompiledDiscordTemplate;
|
||||
import com.github.retrooper.packetevents.util.Vector3d;
|
||||
import com.github.retrooper.packetevents.util.Vector3f;
|
||||
import com.github.retrooper.packetevents.util.Vector3i;
|
||||
import lombok.experimental.UtilityClass;
|
||||
@@ -27,7 +28,7 @@ public class MessageUtil {
|
||||
private final Pattern HEX_PATTERN = Pattern.compile("([&§]#[A-Fa-f0-9]{6})|([&§]x([&§][A-Fa-f0-9]){6})");
|
||||
private final char PLACEHOLDER_ESCAPE_CHAR = '\uFFFF'; // this specific character holds no significance
|
||||
|
||||
public @NotNull String toUnlabledString(@Nullable Vector3i vec) {
|
||||
public @NotNull String toUnlabledString(@Nullable Vector3d vec) {
|
||||
return vec == null ? "null" : vec.x + ", " + vec.y + ", " + vec.z;
|
||||
}
|
||||
|
||||
@@ -35,6 +36,10 @@ public class MessageUtil {
|
||||
return vec == null ? "null" : vec.x + ", " + vec.y + ", " + vec.z;
|
||||
}
|
||||
|
||||
public @NotNull String toUnlabledString(@Nullable Vector3i vec) {
|
||||
return vec == null ? "null" : vec.x + ", " + vec.y + ", " + vec.z;
|
||||
}
|
||||
|
||||
@Contract("_, null, _ -> null; _, !null, _ -> !null")
|
||||
public @Nullable String replacePlaceholders(@Nullable GrimPlayer player, @Nullable String string, boolean removeFormatting) {
|
||||
return replacePlaceholders(player, player == null ? null : player.platformPlayer, string, removeFormatting);
|
||||
|
||||
@@ -816,7 +816,7 @@ public enum CollisionData implements CollisionFactory {
|
||||
}, StateTypes.TRIPWIRE_HOOK),
|
||||
|
||||
TORCH(new HexCollisionBox(6.0D, 0.0D, 6.0D, 10.0D, 10.0D, 10.0D),
|
||||
StateTypes.TORCH, StateTypes.REDSTONE_TORCH, StateTypes.COPPER_TORCH),
|
||||
StateTypes.TORCH, StateTypes.REDSTONE_TORCH, StateTypes.COPPER_TORCH, StateTypes.SOUL_TORCH),
|
||||
|
||||
WALL_TORCH((player, version, data, x, y, z) -> switch (data.getFacing()) {
|
||||
case NORTH -> new HexCollisionBox(5.5D, 3.0D, 11.0D, 10.5D, 13.0D, 16.0D);
|
||||
@@ -825,7 +825,7 @@ public enum CollisionData implements CollisionFactory {
|
||||
case EAST -> new HexCollisionBox(0.0D, 3.0D, 5.5D, 5.0D, 13.0D, 10.5D);
|
||||
// 1.13 separates wall and normal torches, 1.12 does not
|
||||
default -> new HexCollisionBox(6.0D, 0.0D, 6.0D, 10.0D, 10.0D, 10.0D);
|
||||
}, StateTypes.WALL_TORCH, StateTypes.REDSTONE_WALL_TORCH, StateTypes.COPPER_WALL_TORCH),
|
||||
}, StateTypes.WALL_TORCH, StateTypes.REDSTONE_WALL_TORCH, StateTypes.COPPER_WALL_TORCH, StateTypes.SOUL_WALL_TORCH),
|
||||
|
||||
// 1.17 blocks
|
||||
CANDLE((player, version, data, x, y, z) -> {
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import java.util.UUID;
|
||||
|
||||
public class PacketEntityRideable extends PacketEntity {
|
||||
|
||||
public boolean hasSaddle = false;
|
||||
public boolean hasSaddle = false; // We dont actually use this anywhere, why is this here?
|
||||
public int boostTimeMax = 0;
|
||||
public int currentBoostTime = 0;
|
||||
|
||||
|
||||
@@ -242,6 +242,11 @@ public class CompensatedEntities {
|
||||
PacketEntity entity = player.compensatedEntities.getEntity(entityID);
|
||||
if (entity == null) return;
|
||||
|
||||
// 26.1 added AGE_LOCKED to AgeableMob after BABY, shifting all descendant field indices +1.
|
||||
// Only affects entities extending AgeableMob (horses, pigs, striders, tamables, etc.)
|
||||
final int ageableOffset = PacketEvents.getAPI().getServerManager().getVersion()
|
||||
.isNewerThanOrEquals(ServerVersion.V_26_1) ? 1 : 0;
|
||||
|
||||
if (entity.isAgeable) {
|
||||
int id;
|
||||
if (PacketEvents.getAPI().getServerManager().getVersion().isOlderThanOrEquals(ServerVersion.V_1_8_8)) {
|
||||
@@ -255,7 +260,7 @@ public class CompensatedEntities {
|
||||
} else if (PacketEvents.getAPI().getServerManager().getVersion().isOlderThanOrEquals(ServerVersion.V_1_16_5)) {
|
||||
id = 15;
|
||||
} else {
|
||||
id = 16;
|
||||
id = 16; // BABY is still at 16 in 26.1 (AGE_LOCKED is 17, after BABY)
|
||||
}
|
||||
|
||||
// 1.14 good
|
||||
@@ -320,7 +325,10 @@ public class CompensatedEntities {
|
||||
shulker.facing = BlockFace.valueOf(shulkerAttached.getValue().toString().toUpperCase());
|
||||
}
|
||||
|
||||
EntityData<?> height = WatchableIndexUtil.getIndex(watchableObjects, id + 2);
|
||||
// ATTACH_POS was removed in 1.17, shifting PEEK from id+2 to id+1
|
||||
int peekOffset = PacketEvents.getAPI().getServerManager().getVersion()
|
||||
.isNewerThanOrEquals(ServerVersion.V_1_17) ? 1 : 2;
|
||||
EntityData<?> height = WatchableIndexUtil.getIndex(watchableObjects, id + peekOffset);
|
||||
if (height != null) {
|
||||
if ((byte) height.getValue() == 0) {
|
||||
ShulkerData data = new ShulkerData(shulker, player.lastTransactionSent.get(), true);
|
||||
@@ -354,34 +362,42 @@ public class CompensatedEntities {
|
||||
}
|
||||
|
||||
if (entity.type == EntityTypes.PIG) {
|
||||
// SADDLE removed in 1.21.5, shifting BOOST_TIME from 18→17.
|
||||
// ageableOffset naturally cancels this out in 26.1 (BOOST_TIME back at 18).
|
||||
if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_21_5))
|
||||
offset = 1;
|
||||
|
||||
EntityData<?> pigSaddle = WatchableIndexUtil.getIndex(watchableObjects, 17 - offset);
|
||||
if (pigSaddle != null) {
|
||||
rideable.hasSaddle = (boolean) pigSaddle.getValue();
|
||||
// SADDLE removed in 1.21.5; now handled via equipment
|
||||
if (PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_21_5)) {
|
||||
EntityData<?> pigSaddle = WatchableIndexUtil.getIndex(watchableObjects, 17 - offset + ageableOffset);
|
||||
if (pigSaddle != null) {
|
||||
rideable.hasSaddle = (boolean) pigSaddle.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
EntityData<?> pigBoost = WatchableIndexUtil.getIndex(watchableObjects, 18 - offset);
|
||||
EntityData<?> pigBoost = WatchableIndexUtil.getIndex(watchableObjects, 18 - offset + ageableOffset);
|
||||
if (pigBoost != null) { // What does 1.9-1.10 do here? Is this feature even here?
|
||||
rideable.boostTimeMax = (int) pigBoost.getValue();
|
||||
rideable.currentBoostTime = 0;
|
||||
}
|
||||
} else if (entity instanceof PacketEntityStrider) {
|
||||
EntityData<?> striderBoost = WatchableIndexUtil.getIndex(watchableObjects, 17 - offset);
|
||||
EntityData<?> striderBoost = WatchableIndexUtil.getIndex(watchableObjects, 17 - offset + ageableOffset);
|
||||
if (striderBoost != null) {
|
||||
rideable.boostTimeMax = (int) striderBoost.getValue();
|
||||
rideable.currentBoostTime = 0;
|
||||
}
|
||||
|
||||
EntityData<?> striderShaking = WatchableIndexUtil.getIndex(watchableObjects, 18 - offset);
|
||||
EntityData<?> striderShaking = WatchableIndexUtil.getIndex(watchableObjects, 18 - offset + ageableOffset);
|
||||
if (striderShaking != null) {
|
||||
((PacketEntityStrider) rideable).isShaking = (boolean) striderShaking.getValue();
|
||||
}
|
||||
|
||||
EntityData<?> striderSaddle = WatchableIndexUtil.getIndex(watchableObjects, 19 - offset);
|
||||
if (striderSaddle != null) {
|
||||
rideable.hasSaddle = (boolean) striderSaddle.getValue();
|
||||
// SADDLE removed in 1.21.5; now handled via equipment
|
||||
if (PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_21_5)) {
|
||||
EntityData<?> striderSaddle = WatchableIndexUtil.getIndex(watchableObjects, 19 - offset + ageableOffset);
|
||||
if (striderSaddle != null) {
|
||||
rideable.hasSaddle = (boolean) striderSaddle.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -400,7 +416,7 @@ public class CompensatedEntities {
|
||||
offset = 1;
|
||||
}
|
||||
|
||||
EntityData<?> horseByte = WatchableIndexUtil.getIndex(watchableObjects, 17 - offset);
|
||||
EntityData<?> horseByte = WatchableIndexUtil.getIndex(watchableObjects, 17 - offset + ageableOffset);
|
||||
if (horseByte != null) {
|
||||
byte info = (byte) horseByte.getValue();
|
||||
|
||||
@@ -412,7 +428,7 @@ public class CompensatedEntities {
|
||||
// track camel dashing
|
||||
if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_20)) {
|
||||
if (entity instanceof PacketEntityCamel camel) {
|
||||
EntityData<?> entityData = WatchableIndexUtil.getIndex(watchableObjects, 18);
|
||||
EntityData<?> entityData = WatchableIndexUtil.getIndex(watchableObjects, 18 + ageableOffset);
|
||||
if (entityData != null) {
|
||||
camel.setDashing((boolean) entityData.getValue());
|
||||
|
||||
@@ -437,7 +453,7 @@ public class CompensatedEntities {
|
||||
}
|
||||
|
||||
if (entity instanceof PacketEntityNautilus nautilus) {
|
||||
EntityData<?> entityData = WatchableIndexUtil.getIndex(watchableObjects, 19);
|
||||
EntityData<?> entityData = WatchableIndexUtil.getIndex(watchableObjects, 19 + ageableOffset);
|
||||
if (entityData != null) {
|
||||
nautilus.setDashing((boolean) entityData.getValue());
|
||||
|
||||
|
||||
@@ -403,7 +403,8 @@ public class CompensatedInventory extends Check implements PacketCheck {
|
||||
// 01/07/2025: Somehow, the server sends a window id 0 update when the player is not in their inventory?
|
||||
// I guess just revert isPacketInventoryActive if the player has a NotImplementedMenu open?
|
||||
// Regardless, the client does accept this packet and update its inventory, so we must do the same.
|
||||
if (slots.size() == cachedPacketInvSize || items.getWindowId() == 0) {
|
||||
boolean forceUpdate = slots.size() == cachedPacketInvSize || items.getWindowId() == 0;
|
||||
if (!isPacketInventoryActive && forceUpdate) {
|
||||
isPacketInventoryActive = true;
|
||||
updatedValue.set(true);
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ public final class BoundingBoxSize {
|
||||
return 0.3125f;
|
||||
} else if (type == EntityTypes.SPIDER) {
|
||||
return 1.4f;
|
||||
} else if (type == EntityTypes.SQUID) {
|
||||
} else if (type == EntityTypes.SQUID || type == EntityTypes.GLOW_SQUID) {
|
||||
return player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9) ? 0.8f : 0.95f;
|
||||
} else if (type == EntityTypes.TURTLE) {
|
||||
return 1.2f;
|
||||
@@ -251,7 +251,7 @@ public final class BoundingBoxSize {
|
||||
return 0.6f;
|
||||
} else if (type == EntityTypes.EVOKER_FANGS || type == EntityTypes.VEX) {
|
||||
return 0.8f;
|
||||
} else if (type == EntityTypes.SQUID) {
|
||||
} else if (type == EntityTypes.SQUID || type == EntityTypes.GLOW_SQUID) {
|
||||
return player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9) ? 0.8f : 0.95f;
|
||||
} else if (type == EntityTypes.PARROT || type == EntityTypes.BAT || type == EntityTypes.PIG || type == EntityTypes.SPIDER) {
|
||||
return 0.9f;
|
||||
|
||||
@@ -25,6 +25,7 @@ client-brand:
|
||||
ignored-clients:
|
||||
- "^vanilla$"
|
||||
- "^fabric$"
|
||||
- "^quilt$"
|
||||
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
|
||||
- "^Feather Fabric$"
|
||||
- "^labymod$"
|
||||
|
||||
@@ -25,6 +25,7 @@ client-brand:
|
||||
ignored-clients:
|
||||
- "^vanilla$"
|
||||
- "^fabric$"
|
||||
- "^quilt$"
|
||||
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
|
||||
- "^Feather Fabric$"
|
||||
- "^labymod$"
|
||||
|
||||
@@ -25,6 +25,7 @@ client-brand:
|
||||
ignored-clients:
|
||||
- "^vanilla$"
|
||||
- "^fabric$"
|
||||
- "^quilt$"
|
||||
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
|
||||
- "^Feather Fabric$"
|
||||
- "^labymod$"
|
||||
|
||||
@@ -25,6 +25,7 @@ client-brand:
|
||||
ignored-clients:
|
||||
- "^vanilla$"
|
||||
- "^fabric$"
|
||||
- "^quilt$"
|
||||
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
|
||||
- "^Feather Fabric$"
|
||||
- "^labymod$"
|
||||
|
||||
@@ -26,6 +26,7 @@ client-brand:
|
||||
ignored-clients:
|
||||
- "^vanilla$"
|
||||
- "^fabric$"
|
||||
- "^quilt$"
|
||||
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
|
||||
- "^Feather Fabric$"
|
||||
- "^labymod$"
|
||||
|
||||
@@ -27,6 +27,7 @@ client-brand:
|
||||
ignored-clients:
|
||||
- "^vanilla$"
|
||||
- "^fabric$"
|
||||
- "^quilt$"
|
||||
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
|
||||
- "^Feather Fabric$"
|
||||
- "^labymod$"
|
||||
|
||||
@@ -25,6 +25,7 @@ client-brand:
|
||||
ignored-clients:
|
||||
- "^vanilla$"
|
||||
- "^fabric$"
|
||||
- "^quilt$"
|
||||
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
|
||||
- "^Feather Fabric$"
|
||||
- "^labymod$"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
alerts:
|
||||
# Czy oprócz wysyłania alertów do graczy, powinny być one również wysyłane na konsolę?
|
||||
print-to-console: true
|
||||
# Określa, czy i w jaki sposób alerty są udostępniane pomiędzy serwerami połączonymi z Twoim serwerem proxy.
|
||||
# Określa, czy i w jaki sposób alerty są udostępniane pomiędzy serwerami połączonymi z Twoim serwerem proxy.
|
||||
# Jeśli korzystasz z Velocity, musisz włączyć opcję „bungee-plugin-message-channel” w konfiguracji Velocity.
|
||||
proxy:
|
||||
# Czy alerty powinny być wysyłane do innych serwerów podłączonych do Twojego serwera proxy?
|
||||
@@ -25,6 +25,7 @@ client-brand:
|
||||
ignored-clients:
|
||||
- "^vanilla$"
|
||||
- "^fabric$"
|
||||
- "^quilt$"
|
||||
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
|
||||
- "^Feather Fabric$"
|
||||
- "^labymod$"
|
||||
|
||||
@@ -26,6 +26,7 @@ client-brand:
|
||||
ignored-clients:
|
||||
- "^vanilla$"
|
||||
- "^fabric$"
|
||||
- "^quilt$"
|
||||
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
|
||||
- "^Feather Fabric$"
|
||||
- "^labymod$"
|
||||
|
||||
@@ -25,6 +25,7 @@ client-brand:
|
||||
ignored-clients:
|
||||
- "^vanilla$"
|
||||
- "^fabric$"
|
||||
- "^quilt$"
|
||||
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
|
||||
- "^Feather Fabric$"
|
||||
- "^labymod$"
|
||||
|
||||
@@ -25,6 +25,7 @@ client-brand:
|
||||
ignored-clients:
|
||||
- "^vanilla$"
|
||||
- "^fabric$"
|
||||
- "^quilt$"
|
||||
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
|
||||
- "^Feather Fabric$"
|
||||
- "^labymod$"
|
||||
|
||||
@@ -25,6 +25,7 @@ client-brand:
|
||||
ignored-clients:
|
||||
- "^vanilla$"
|
||||
- "^fabric$"
|
||||
- "^quilt$"
|
||||
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
|
||||
- "^Feather Fabric$"
|
||||
- "^labymod$"
|
||||
|
||||
@@ -26,6 +26,7 @@ client-brand:
|
||||
ignored-clients:
|
||||
- "^vanilla$"
|
||||
- "^fabric$"
|
||||
- "^quilt$"
|
||||
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
|
||||
- "^Feather Fabric$"
|
||||
- "^labymod$"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ cloud-processors-requirements = "1.0.0-rc.1"
|
||||
floodgate-api = "2.0-SNAPSHOT"
|
||||
geyser-base-api = "1.0.2"
|
||||
grim-api = "1.2.4.0"
|
||||
packetevents = "2.11.2+675bc5c-SNAPSHOT"
|
||||
packetevents = "2.12.1+d82ae73-SNAPSHOT"
|
||||
placeholderapi = "2.11.6"
|
||||
viaversion = "5.7.1"
|
||||
|
||||
@@ -26,7 +26,7 @@ configuralize = "1.4.1" # :slim classifier is applied in build.gradle.kts
|
||||
|
||||
# --- Build & Tooling ---
|
||||
fabric-loader = "0.16.14"
|
||||
fabric-loom = "1.14.6"
|
||||
fabric-loom = "1.15.5"
|
||||
lombok = "9.0.0"
|
||||
shadow = "9.3.0"
|
||||
spotless = "6.25.0"
|
||||
|
||||
Reference in new issue
Block a user