Merge PR #3 upstream updates while preserving legacy item bobbing fix

This commit was merged in pull request #3.
This commit is contained in:
NotAlexNoyle committed 2026-06-01 14:57:29 -04:00
commit 67da895f25
101 files changed
+1647 -722

No files matched your search

+11 -1
View File
@@ -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 }}"
+1 -1
View File
@@ -20,7 +20,7 @@ import versioning.VersionUtil
BuildConfig.init(project)
val baseVersion = "2.3.73"
val baseVersion = "2.3.74"
group = "ac.grim.grimac"
version = VersionUtil.computeVersion(baseVersion)
description = "Libre simulation anticheat designed for 1.21 with 1.8–1.21 support, powered by PacketEvents 2.11.2."
+1 -5
View File
@@ -27,12 +27,9 @@ repositories {
includeGroup("me.clip")
}
exclusive("https://repo.codemc.io/repository/maven-releases/", { mavenContent { releasesOnly() } }) {
includeGroup("com.github.retrooper")
}
exclusive("https://repo.grim.ac/snapshots") {
includeGroup("ac.grim.grimac")
includeGroup("com.github.retrooper")
}
exclusive("https://nexus.scarsz.me/content/repositories/releases", { mavenContent { releasesOnly() } }) {
@@ -82,7 +79,6 @@ bukkit {
"floodgate",
"FastLogin",
"PlaceholderAPI",
"ZNPCsPlus",
)
permissions {
@@ -1,6 +1,7 @@
package ac.grim.grimac.platform.bukkit.player;
import ac.grim.grimac.platform.api.entity.GrimEntity;
import ac.grim.grimac.platform.api.player.BlockTranslator;
import ac.grim.grimac.platform.api.player.PlatformInventory;
import ac.grim.grimac.platform.api.player.PlatformPlayer;
import ac.grim.grimac.platform.api.sender.Sender;
@@ -170,6 +171,11 @@ public class BukkitPlatformPlayer extends BukkitGrimEntity implements PlatformPl
return GrimACBukkitLoaderPlugin.LOADER.getBukkitSenderFactory().map(this.bukkitPlayer);
}
@Override
public BlockTranslator getBlockTranslator() {
return BlockTranslator.IDENTITY;
}
@Override
@NotNull
public Player getNative() {
+1 -5
View File
@@ -10,13 +10,9 @@ repositories {
// We still call mavenLocal() conditionally at the top for non-exclusive deps (general fallback)
if (BuildConfig.mavenLocalOverride) mavenLocal()
// Grim API
// Grim API & PacketEvents
exclusive("https://repo.grim.ac/snapshots") {
includeGroup("ac.grim.grimac")
}
// PacketEvents
exclusive("https://repo.codemc.io/repository/maven-releases/", { mavenContent { releasesOnly() } }) {
includeGroup("com.github.retrooper")
}
@@ -8,6 +8,7 @@ import ac.grim.grimac.player.GrimPlayer;
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 lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
@@ -175,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) {
@@ -207,4 +209,10 @@ public class Check extends GrimProcessor implements AbstractCheck {
return isFlying(packetType);
}
// prevent causing exploits with packet cancelling (ie noslow)
public boolean canCancel(DiggingAction action) {
return action != DiggingAction.RELEASE_USE_ITEM
// we check client version here because 1.8- doesn't predict dropping items, so we can cancel them. (see CompensatedInventory)
&& (action != DiggingAction.DROP_ITEM && action != DiggingAction.DROP_ITEM_STACK || player.getClientVersion().isOlderThanOrEquals(ClientVersion.V_1_8));
}
}
@@ -69,7 +69,9 @@ public class HitboxDebugHandler extends AbstractDebugHandler {
* @param hitboxes Map of entity IDs to their collision boxes
* @param targetEntities Set of entity IDs that are considered targets
* @param lookVecsAndEyeHeights List of pairs containing look vectors and their corresponding eye heights
* @param basePos Base position before eye height adjustments
* @param baseX Base x coordinate before eye height adjustments
* @param baseY Base y coordinate before eye height adjustments
* @param baseZ Base z coordinate before eye height adjustments
* @param isPrediction Whether these hitboxes are from a prediction calculation
*
* Packet Format (Version 1):
@@ -99,7 +101,7 @@ public class HitboxDebugHandler extends AbstractDebugHandler {
* - Double: maxZ
*/
public void sendHitboxData(Map<Integer, CollisionBox> hitboxes, Set<Integer> targetEntities,
List<Pair<Vector3dm, Double>> lookVecsAndEyeHeights, Vector3dm basePos,
List<Pair<Vector3dm, Double>> lookVecsAndEyeHeights, double baseX, double baseY, double baseZ,
boolean isPrediction, double reachDistance) {
if (!isEnabled()) return;
@@ -119,7 +121,7 @@ public class HitboxDebugHandler extends AbstractDebugHandler {
buffer.writeDouble(reachDistance);
// Write base position
writeVector(buffer, basePos);
writeVector(buffer, baseX, baseY, baseZ);
// Write number of ray traces
ByteBufHelper.writeVarInt(buffer, lookVecsAndEyeHeights.size());
@@ -133,7 +135,7 @@ public class HitboxDebugHandler extends AbstractDebugHandler {
// we make them 3 meters shorter because all of the vectors passed into here are made 3 meters longer
// then they need to be so grim can report correct reach distance for too far away hits
buffer.writeDouble(eyeHeight);
writeVector(buffer, lookVec);
writeVector(buffer, lookVec.getX(), lookVec.getY(), lookVec.getZ());
}
// Write number of hitboxes
@@ -193,13 +195,15 @@ public class HitboxDebugHandler extends AbstractDebugHandler {
}
/**
* Helper method to write a Vector to the ByteBuf
* Helper method to write vector coordinates to the ByteBuf
* @param buffer The buffer to write to
* @param vector The vector to write
* @param x The x coordinate to write
* @param y The y coordinate to write
* @param z The z coordinate to write
*/
private void writeVector(ByteBuf buffer, Vector3dm vector) {
buffer.writeDouble(vector.getX());
buffer.writeDouble(vector.getY());
buffer.writeDouble(vector.getZ());
private void writeVector(ByteBuf buffer, double x, double y, double z) {
buffer.writeDouble(x);
buffer.writeDouble(y);
buffer.writeDouble(z);
}
}
@@ -6,23 +6,21 @@ 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.WrapperPlayClientInteractEntity;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientEntityAction;
import org.jetbrains.annotations.NotNull;
@CheckData(name = "BadPacketsC", description = "Interacted with self")
@CheckData(name = "BadPacketsC", description = "Tried to wake up while not sleeping", experimental = true)
public class BadPacketsC extends Check implements PacketCheck {
public BadPacketsC(GrimPlayer player) {
public BadPacketsC(@NotNull GrimPlayer player) {
super(player);
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY
&& player.cameraEntity.isSelf()
&& new WrapperPlayClientInteractEntity(event).getEntityId() == player.entityID
&& flagAndAlert() && shouldModifyPackets() // Instant ban
) {
event.setCancelled(true);
player.onPacketCancel();
if (event.getPacketType() == PacketType.Play.Client.ENTITY_ACTION
&& new WrapperPlayClientEntityAction(event).getAction() == WrapperPlayClientEntityAction.Action.LEAVE_BED
&& !player.isInBed) {
flagAndAlert();
}
}
}
@@ -43,7 +43,7 @@ public class BadPacketsL extends Check implements PacketCheck {
+ ", face=" + packet.getBlockFace()
+ ", sequence=" + packet.getSequence()
+ ", action=" + packet.getAction().toString().toLowerCase(Locale.ROOT)
) && shouldModifyPackets() && packet.getAction() != DiggingAction.RELEASE_USE_ITEM) {
) && shouldModifyPackets() && canCancel(packet.getAction())) {
event.setCancelled(true);
player.onPacketCancel();
}
@@ -11,7 +11,7 @@ import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientCl
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientClickWindow.WindowClickType;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerOpenWindow;
@CheckData(name = "BadPacketsP", description = "Invalid click packets", experimental = true)
@CheckData(name = "BadPacketsP", description = "Invalid window click packet", experimental = true)
public class BadPacketsP extends Check implements PacketCheck {
private int containerType = -1;
@@ -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);
@@ -31,8 +31,10 @@ import ac.grim.grimac.utils.data.Pair;
import ac.grim.grimac.utils.data.packetentity.PacketEntity;
import ac.grim.grimac.utils.data.packetentity.PacketEntitySizeable;
import ac.grim.grimac.utils.data.packetentity.dragon.PacketEntityEnderDragonPart;
import ac.grim.grimac.utils.math.GrimMath;
import ac.grim.grimac.utils.math.Vector3dm;
import ac.grim.grimac.utils.nmsutil.ReachUtils;
import ac.grim.grimac.utils.viaversion.ViaVersionUtil;
import ac.grim.grimac.utils.nmsutil.WorldRayTrace;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
@@ -48,10 +50,11 @@ 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.util.Vector3d;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientAttack;
import com.github.retrooper.packetevents.util.Vector3i;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerFlying;
import com.viaversion.viaversion.api.Via;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.jetbrains.annotations.NotNull;
@@ -87,63 +90,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 itemInHand = player.inventory.getItemInHand(hand);
boolean tooManyAttacks = playerAttackQueue.size() > 10;
if (!tooManyAttacks) {
playerAttackQueue.put(action.getEntityId(), new InteractionData(new Vector3d(player.x, player.y, player.z), itemInHand)); // Queue for next tick for very precise check
}
boolean knownInvalid = isKnownInvalid(entity, itemInHand);
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
@@ -153,6 +107,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
//
@@ -161,7 +218,7 @@ public class Reach extends Check implements PacketCheck {
// than this method. If this method flags, the other method WILL flag.
//
// Meaning that the other check should be the only one that flags.
private boolean isKnownInvalid(PacketEntity reachEntity, ItemStack itemInHand) {
private boolean isKnownInvalid(PacketEntity reachEntity, boolean hasAttackRange, float itemMaxReach, float itemHitboxMargin) {
// If the entity doesn't exist, or if it is exempt, or if it is dead
if ((blacklisted.contains(reachEntity.type) || !reachEntity.isLivingEntity) && reachEntity.type != EntityTypes.END_CRYSTAL)
return false; // exempt
@@ -172,13 +229,12 @@ public class Reach extends Check implements PacketCheck {
// Filter out what we assume to be cheats
if (cancelBuffer != 0) {
CheckResult result = checkReach(reachEntity, new Vector3d(player.x, player.y, player.z), itemInHand, true);
CheckResult result = checkReach(reachEntity, player.x, player.y, player.z, hasAttackRange, itemMaxReach, itemHitboxMargin, true);
return result.isFlag(); // If they flagged
} else {
SimpleCollisionBox targetBox = getTargetBox(reachEntity);
double maxReach = applyReachModifiers(targetBox, itemInHand, !player.packetStateData.didLastMovementIncludePosition);
double maxReach = applyReachModifiers(targetBox, hasAttackRange, itemMaxReach, itemHitboxMargin, !player.packetStateData.didLastMovementIncludePosition);
return ReachUtils.getMinReachToBox(player, targetBox) > maxReach;
}
}
@@ -189,7 +245,7 @@ public class Reach extends Check implements PacketCheck {
if (reachEntity == null) continue;
InteractionData interactionData = attack.getValue();
CheckResult result = checkReach(reachEntity, interactionData.vector(), interactionData.itemInHand(), false);
CheckResult result = checkReach(reachEntity, interactionData.x, interactionData.y, interactionData.z, interactionData.hasAttackRange, interactionData.maxReach, interactionData.hitboxMargin, false);
switch (result.type()) {
case REACH -> {
String added = ", type=" + reachEntity.type.getName().getKey();
@@ -223,10 +279,10 @@ public class Reach extends Check implements PacketCheck {
}
@NotNull
private CheckResult checkReach(PacketEntity reachEntity, Vector3d from, ItemStack itemInHand, boolean isPrediction) {
private CheckResult checkReach(PacketEntity reachEntity, double x, double y, double z, boolean hasAttackRange, float itemMaxReach, float itemHitboxMargin, boolean isPrediction) {
SimpleCollisionBox targetBox = getTargetBox(reachEntity);
double maxReach = applyReachModifiers(targetBox, itemInHand, !player.packetStateData.didLastLastMovementIncludePosition);
double maxReach = applyReachModifiers(targetBox, hasAttackRange, itemMaxReach, itemHitboxMargin, !player.packetStateData.didLastLastMovementIncludePosition);
double minDistance = Double.MAX_VALUE;
// will store all lookVecsAndEyeHeight pairs that landed a hit on the target entity
@@ -236,13 +292,12 @@ public class Reach extends Check implements PacketCheck {
// +3 would be 3 + 3 = 6, which is the pre-1.20.5 behaviour, preventing "Missed Hitbox"
final double distance = maxReach + 3;
final double[] possibleEyeHeights = player.getPossibleEyeHeights();
final Vector3dm[] possibleLookDirs = player.getPossibleLookVectors(isPrediction);
final Vector3dm eyePos = new Vector3dm(from.getX(), 0, from.getZ());
final Vector3dm eyePos = new Vector3dm(x, 0, z);
for (Vector3dm lookVec : possibleLookDirs) {
for (double eye : possibleEyeHeights) {
eyePos.setY(from.getY() + eye);
eyePos.setY(y + eye);
Vector3dm endReachPos = eyePos.clone().add(lookVec.getX() * distance, lookVec.getY() * distance, lookVec.getZ() * distance);
Vector3dm intercept = ReachUtils.calculateIntercept(targetBox, eyePos, endReachPos).first();
@@ -260,7 +315,7 @@ public class Reach extends Check implements PacketCheck {
}
if (hitboxDebuggingEnabled())
sendHitboxDebugData(reachEntity, from, lookVecsAndEyeHeights, isPrediction);
sendHitboxDebugData(reachEntity, x, y, z, lookVecsAndEyeHeights, isPrediction);
HitData foundHitData = null;
// If the entity is within range of the player (we'll flag anyway if not, so no point checking blocks in this case)
@@ -268,7 +323,7 @@ public class Reach extends Check implements PacketCheck {
if (minDistance <= distance - extraSearchDistance && !player.compensatedWorld.isNearHardEntity(player.boundingBox.copy().expand(4))) {
// we can optimize didRayTraceHit more to only rayTrace up to the maximize distance of all rays that hit to the target...
// I'm too lazy to do that and we don't need to optimize that much yet so...
final @Nullable Pair<Double, HitData> hitResult = WorldRayTrace.didRayTraceHit(player, reachEntity, lookVecsAndEyeHeights, from);
final @Nullable Pair<Double, HitData> hitResult = WorldRayTrace.didRayTraceHit(player, reachEntity, lookVecsAndEyeHeights, x, y, z);
HitData hitData = hitResult.second();
// If the returned hit result was NOT the target entity we flag the check
if (hitData instanceof EntityHitData &&
@@ -312,20 +367,16 @@ public class Reach extends Check implements PacketCheck {
return reachEntity.getPossibleCollisionBoxes();
}
private double applyReachModifiers(SimpleCollisionBox targetBox, ItemStack itemInHand, boolean giveMovementThreshold) {
private static final boolean ATTACK_RANGE_COMPONENT_EXISTS = PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_21_11);
private static final boolean USE_1_8_HITBOX_MARGIN = PacketEvents.getAPI().getServerManager().getVersion().isOlderThanOrEquals(ServerVersion.V_1_8_8);
private double applyReachModifiers(SimpleCollisionBox targetBox, boolean hasAttackRange, float itemMaxReach, float itemHitboxMargin, boolean giveMovementThreshold) {
double maxReach;
double hitboxMargin = threshold;
ItemAttackRange attackRange = null;
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_21_11) && ATTACK_RANGE_COMPONENT_EXISTS) {
// TODO: ViaVersion support https://github.com/ViaVersion/ViaVersion/pull/4733
attackRange = itemInHand.getComponentOr(ComponentTypes.ATTACK_RANGE, null);
}
if (attackRange != null) {
maxReach = attackRange.getMaxRange();
hitboxMargin += attackRange.getHitboxMargin();
if (hasAttackRange) {
maxReach = itemMaxReach;
hitboxMargin += itemHitboxMargin;
} else {
maxReach = player.compensatedEntities.self.getAttributeValue(Attributes.ENTITY_INTERACTION_RANGE);
// 1.7 and 1.8 players get a bit of extra hitbox (this is why you should use 1.8 on cross version servers)
@@ -349,8 +400,6 @@ public class Reach extends Check implements PacketCheck {
return maxReach;
}
private static final boolean ATTACK_RANGE_COMPONENT_EXISTS = PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_21_11);
@Override
public void onReload(ConfigManager config) {
this.ignoreNonPlayerTargets = config.getBooleanElse("Reach.ignore-non-player-targets", false);
@@ -371,7 +420,7 @@ public class Reach extends Check implements PacketCheck {
public void handleBlockChange(Vector3i vector3i, WrappedBlockState state) {
if (blocksChangedThisTick.size() >= 40) return; // Don't let players freeze movement packets to grow this
// Only do this for nearby blocks
if (new Vector3dm(vector3i.x, vector3i.y, vector3i.z).distanceSquared(new Vector3dm(player.x, player.y, player.z)) > 6) return;
if (GrimMath.distanceSquared(vector3i.x, vector3i.y, vector3i.z, player.x, player.y, player.z) > 36) return;
// Only do this if the state really had any world impact
if (state.equals(player.compensatedWorld.getBlock(vector3i))) return;
blocksChangedThisTick.add(vector3i);
@@ -381,7 +430,7 @@ public class Reach extends Check implements PacketCheck {
return player.checkManager.getCheck(HitboxDebugHandler.class).isEnabled();
}
private void sendHitboxDebugData(PacketEntity reachEntity, Vector3d from, List<Pair<Vector3dm, Double>> lookVecsAndEyeHeights, boolean isPrediction) {
private void sendHitboxDebugData(PacketEntity reachEntity, double x, double y, double z, List<Pair<Vector3dm, Double>> lookVecsAndEyeHeights, boolean isPrediction) {
Map<Integer, CollisionBox> hitboxes = new HashMap<>();
for (Int2ObjectMap.Entry<PacketEntity> entry : player.compensatedEntities.entityMap.int2ObjectEntrySet()) {
PacketEntity entity = entry.getValue();
@@ -428,11 +477,10 @@ public class Reach extends Check implements PacketCheck {
player.checkManager.getCheck(HitboxDebugHandler.class).sendHitboxData(hitboxes,
Collections.singleton(player.compensatedEntities.getPacketEntityID(reachEntity)),
lookVecsAndEyeHeights,
new Vector3dm(from.getX(), from.getY(), from.getZ()),
x, y, z,
isPrediction, player.compensatedEntities.self.getAttributeValue(Attributes.ENTITY_INTERACTION_RANGE));
}
private record InteractionData(Vector3d vector, ItemStack itemInHand) {
private record InteractionData(double x, double y, double z, boolean hasAttackRange, float maxReach, float hitboxMargin) {
}
}
@@ -0,0 +1,45 @@
package ac.grim.grimac.checks.impl.combat;
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.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 {
public SelfInteract(GrimPlayer player) {
super(player);
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
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) {
@@ -41,10 +41,6 @@ public class GhostBlockMitigation extends BlockPlaceCheck {
continue;
}
if (i == xAgainst && j == yAgainst && k == zAgainst) {
continue;
}
if (!world.isChunkLoaded(i >> 4, k >> 4)) {
continue;
}
@@ -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,7 +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 {
@@ -41,7 +46,8 @@ public class PacketOrderB extends Check implements PacketCheck {
public void onPacketReceive(PacketReceiveEvent event) {
if (exempt) return;
if (event.getPacketType() == PacketType.Play.Client.ANIMATION) {
if (event.getPacketType() == PacketType.Play.Client.ANIMATION
&& new WrapperPlayClientAnimation(event).getHand() == InteractionHand.MAIN_HAND) {
sentAnimationSinceLastAttack = sentAnimation = true;
sentAttack = sentSlotSwitch = false;
return;
@@ -50,17 +56,20 @@ public class PacketOrderB extends Check implements PacketCheck {
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;
}
}
@@ -78,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()
@@ -7,7 +7,6 @@ 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.DiggingAction;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientClientStatus;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerDigging;
@@ -24,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
@@ -32,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"
@@ -42,7 +45,7 @@ public class PacketOrderF extends Check implements PostPredictionCheck {
if (!player.canSkipTicks()) {
if (flagAndAlert(verbose) && shouldModifyPackets()) {
if (event.getPacketType() == PacketType.Play.Client.PLAYER_DIGGING
&& new WrapperPlayClientPlayerDigging(event).getAction() == DiggingAction.RELEASE_USE_ITEM
&& !canCancel(new WrapperPlayClientPlayerDigging(event).getAction())
) return; // don't cause a noslow
event.setCancelled(true);
@@ -28,27 +28,26 @@ 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()
+ ", digging=" + player.packetOrderProcessor.isDigging();
if (!player.canSkipTicks()) {
if (flagAndAlert(verbose) && shouldModifyPackets()) {
if (flagAndAlert(verbose) && shouldModifyPackets() && canCancel(action)) {
event.setCancelled(true);
player.onPacketCancel();
}
@@ -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;
}
}
@@ -6,7 +6,7 @@ import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.utils.anticheat.LogUtil;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import ac.grim.grimac.utils.webhook.WebhookMessage;
import ac.grim.grimac.utils.data.webhook.discord.WebhookMessage;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.context.CommandContext;
import org.jetbrains.annotations.NotNull;
@@ -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
@@ -57,9 +57,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,7 +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 {
@@ -32,77 +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 noCooldown = isLegacyPlayer || PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_9);
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 && noCooldown) || 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++;
}
}
} 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;
}
}
@@ -1,7 +1,6 @@
package ac.grim.grimac.events.packets;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.checks.impl.movement.NoSlow;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.item.ItemBehaviour;
import ac.grim.grimac.utils.item.ItemBehaviourRegistry;
@@ -212,7 +211,7 @@ public class PacketPlayerDigging extends PacketListenerAbstract {
&& player.packetStateData.getSlowedByUsingItemSlot() != player.packetStateData.lastSlotSelected;
if (slotChanged || player.inventory.getItemInHand(player.packetStateData.itemInUseHand).isEmpty()) {
player.packetStateData.setSlowedByUsingItem(false);
if (slotChanged) player.checkManager.getPostPredictionCheck(NoSlow.class).didSlotChangeLastTick = true;
if (slotChanged) player.checkManager.getNoSlow().didSlotChangeLastTick = true;
}
}
}
@@ -163,6 +163,7 @@ public class PacketPlayerRespawn extends PacketListenerAbstract {
final boolean keepTrackedData = this.hasFlag(respawn, KEEP_TRACKED_DATA);
if (!keepTrackedData) {
player.food = 20;
player.powderSnowFrozenTicks = 0;
player.compensatedEntities.self.hasGravity = true;
player.playerEntityHasGravity = true;
@@ -113,6 +113,11 @@ public class PacketServerTeleport extends PacketListenerAbstract {
}
}
// 1.21.2+ client ignore teleports if player is inside vehicle, ABSOLUTE CINEMA MOJANG
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_21_2) && player.compensatedEntities.serverPlayerVehicle != null) {
pos = player.getSetbackTeleportUtil().lastKnownGoodPosition.getPos();
}
player.sendTransaction();
final int lastTransactionSent = player.lastTransactionSent.get();
event.getTasksAfterSend().add(player::sendTransaction);
@@ -1,38 +0,0 @@
package ac.grim.grimac.manager;
import ac.grim.grimac.checks.Check;
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.WrapperPlayClientInteractEntity;
import lombok.Getter;
@Getter
public class ActionManager extends Check implements PacketCheck {
private boolean attacking = false;
private long lastAttack = 0;
public ActionManager(GrimPlayer player) {
super(player);
}
@Override
public void onPacketReceive(final PacketReceiveEvent event) {
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) {
WrapperPlayClientInteractEntity action = new WrapperPlayClientInteractEntity(event);
if (action.getAction() == WrapperPlayClientInteractEntity.InteractAction.ATTACK) {
player.totalFlyingPacketsSent = 0;
attacking = true;
lastAttack = System.currentTimeMillis();
}
} else if (isTickPacketIncludingNonMovement(event.getPacketType())) {
player.totalFlyingPacketsSent++;
attacking = false;
}
}
public boolean hasAttackedSince(long time) {
return System.currentTimeMillis() - lastAttack < time;
}
}
@@ -0,0 +1,71 @@
package ac.grim.grimac.manager;
import ac.grim.grimac.checks.Check;
import ac.grim.grimac.checks.type.PacketCheck;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.math.GrimMath;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.attribute.Attributes;
import com.github.retrooper.packetevents.protocol.item.ItemStack;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.DiggingAction;
import com.github.retrooper.packetevents.protocol.world.BlockFace;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerDigging;
public class AttackCooldownHandler extends Check implements PacketCheck {
private int ticksSinceLastSwing;
private ItemStack stack = ItemStack.EMPTY;
// Since we don't know when the client ticks, we call updateHeldItem() when the held item changes,
// but that means ticksSinceLastSwing is incremented after it gets reset. This is wrong, so we
// compensate for that by not incrementing ticksSinceLastSwing if updateHeldItem() was called
// (and the held item changed) before the tick packet.
private boolean stackChanged;
public AttackCooldownHandler(GrimPlayer player) {
super(player);
}
@Override
public void onPacketReceive(final PacketReceiveEvent event) {
if (event.getPacketType() == PacketType.Play.Client.ANIMATION) {
// FIXME: should only run when the click misses
reset();
}
if (event.getPacketType() == PacketType.Play.Client.PLAYER_DIGGING) {
WrapperPlayClientPlayerDigging packet = new WrapperPlayClientPlayerDigging(event);
if (packet.getAction() == DiggingAction.CANCELLED_DIGGING && packet.getBlockFace() == BlockFace.DOWN) {
// FIXME: this could also be triggered by switching targets while looking at the bottom face of a block
reset();
}
}
if (isTickPacket(event.getPacketType())) {
if (!stackChanged) {
++ticksSinceLastSwing;
}
updateHeldItem();
stackChanged = false;
}
}
public void reset() {
ticksSinceLastSwing = 0;
}
// called on client tick and whenever the slot gets updated
public void updateHeldItem() {
ItemStack held = player.inventory.getHeldItem().copy();
if (!(stack.isEmpty() && held.isEmpty() || stack.getType() == held.getType() && (stack.isDamageableItem() || stack.getLegacyData() == held.getLegacyData()))) {
reset();
stackChanged = true;
}
stack = held;
}
public float getMinimumProgress() {
return GrimMath.clamp(((float) ticksSinceLastSwing + 0.5F) / (float) (1d / player.compensatedEntities.self.getAttributeValue(Attributes.ATTACK_SPEED) * 20.0D), 0, 1);
}
}
@@ -59,6 +59,7 @@ import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.google.common.collect.ClassToInstanceMap;
import com.google.common.collect.ImmutableClassToInstanceMap;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
@@ -76,7 +77,8 @@ public class CheckManager {
private final ClassToInstanceMap<BlockBreakCheck> blockBreakChecks;
private final ClassToInstanceMap<BlockPlaceCheck> blockPlaceChecks;
private final ClassToInstanceMap<PostPredictionCheck> postPredictionChecks;
private PacketEntityReplication packetEntityReplication = null;
@Getter
private final PacketEntityReplication packetEntityReplication;
private final List<PacketCheck> packetChecksValues;
private final List<PositionCheck> positionChecksValues;
@@ -88,16 +90,18 @@ public class CheckManager {
private final List<PostPredictionCheck> postPredictionChecksValues;
public CheckManager(GrimPlayer player) {
packetEntityReplication = new PacketEntityReplication(player);
packetChecks = new ImmutableClassToInstanceMap.Builder<PacketCheck>()
.put(CompensatedCameraEntity.class, player.cameraEntity)
.put(PacketOrderProcessor.class, player.packetOrderProcessor)
.put(Reach.class, new Reach(player))
.put(PacketEntityReplication.class, new PacketEntityReplication(player))
.put(PacketEntityReplication.class, packetEntityReplication)
.put(PacketChangeGameState.class, new PacketChangeGameState(player))
.put(CompensatedInventory.class, player.inventory)
.put(PacketPlayerAbilities.class, new PacketPlayerAbilities(player))
.put(PacketWorldBorder.class, new PacketWorldBorder(player))
.put(ActionManager.class, player.actionManager)
.put(AttackCooldownHandler.class, player.attackCooldown)
.put(TeamHandler.class, new TeamHandler(player))
.put(ClientBrand.class, new ClientBrand(player))
.put(NoFall.class, new NoFall(player))
@@ -129,6 +133,7 @@ public class CheckManager {
.put(BadPacketsV.class, new BadPacketsV(player))
.put(BadPacketsY.class, new BadPacketsY(player))
.put(BadPacketsZ.class, new BadPacketsZ(player))
.put(SelfInteract.class, new SelfInteract(player))
.put(InventoryA.class, new InventoryA(player))
.put(InventoryB.class, new InventoryB(player))
.put(InventoryE.class, new InventoryE(player))
@@ -318,6 +323,21 @@ public class CheckManager {
return (T) blockPlaceChecks.get(check);
}
@SuppressWarnings("unchecked")
public <T extends PacketCheck> T getPacketCheck(Class<T> check) {
return (T) packetChecks.get(check);
}
@SuppressWarnings("unchecked")
public <T extends PacketCheck> T getPrePredictionCheck(Class<T> check) {
return (T) prePredictionChecks.get(check);
}
@SuppressWarnings("unchecked")
public <T extends PostPredictionCheck> T getPostPredictionCheck(Class<T> check) {
return (T) postPredictionChecks.get(check);
}
public void onPrePredictionReceivePacket(final PacketReceiveEvent packet) {
for (PacketCheck check : prePredictionChecksValues) {
check.onPacketReceive(packet);
@@ -424,22 +444,6 @@ public class CheckManager {
return getPostPredictionCheck(ExplosionHandler.class);
}
@SuppressWarnings("unchecked")
public <T extends PacketCheck> T getPacketCheck(Class<T> check) {
return (T) packetChecks.get(check);
}
@SuppressWarnings("unchecked")
public <T extends PacketCheck> T getPrePredictionCheck(Class<T> check) {
return (T) prePredictionChecks.get(check);
}
public PacketEntityReplication getEntityReplication() {
if (packetEntityReplication == null)
packetEntityReplication = getPacketCheck(PacketEntityReplication.class);
return packetEntityReplication;
}
public NoFall getNoFall() {
return getPacketCheck(NoFall.class);
}
@@ -464,15 +468,6 @@ public class CheckManager {
return getPostPredictionCheck(DebugHandler.class);
}
public OffsetHandler getOffsetHandler() {
return getPostPredictionCheck(OffsetHandler.class);
}
@SuppressWarnings("unchecked")
public <T extends PostPredictionCheck> T getPostPredictionCheck(Class<T> check) {
return (T) postPredictionChecks.get(check);
}
private void init() {
if (inited || initedAtomic.getAndSet(true)) return;
inited = true;
@@ -1,6 +1,7 @@
package ac.grim.grimac.manager;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.api.GrimUser;
import ac.grim.grimac.api.config.ConfigManager;
import ac.grim.grimac.manager.init.ReloadableInitable;
import ac.grim.grimac.manager.init.start.StartableInitable;
@@ -8,7 +9,11 @@ import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.LogUtil;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import ac.grim.grimac.utils.data.Pair;
import ac.grim.grimac.utils.webhook.*;
import ac.grim.grimac.utils.data.webhook.discord.CompiledDiscordTemplate;
import ac.grim.grimac.utils.data.webhook.discord.Embed;
import ac.grim.grimac.utils.data.webhook.discord.EmbedField;
import ac.grim.grimac.utils.data.webhook.discord.EmbedFooter;
import ac.grim.grimac.utils.data.webhook.discord.WebhookMessage;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -21,15 +26,19 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
public class DiscordManager implements StartableInitable, ReloadableInitable {
private static final Predicate<String> WEBHOOK_REGEX = Pattern.compile("^https://discord\\.com/api(?:/v\\d+)?/webhooks/\\d+/[\\w-]+(\\?thread_id=\\d+)?$").asMatchPredicate();
private static final Predicate<String> HTTPS_URL_REGEX = Pattern.compile("^https://[^/\\s]+/\\S+$").asMatchPredicate();
private static final Duration timeout = Duration.ofSeconds(15);
private static final HttpClient client = HttpClient.newBuilder().connectTimeout(timeout).build();
private static final ConcurrentLinkedDeque<Pair<HttpRequest, CompletableFuture<Boolean>>> requests = new ConcurrentLinkedDeque<>();
@@ -38,7 +47,8 @@ public class DiscordManager implements StartableInitable, ReloadableInitable {
private static long rateLimitedUntil;
private URI url;
private int embedColor;
private String staticContent = "";
private CompiledDiscordTemplate compiledContent;
private char backtickReplacement = '\u02CB';
private String embedTitle = "";
private boolean includeTimestamp;
private boolean includeVerbose;
@@ -68,6 +78,9 @@ public class DiscordManager implements StartableInitable, ReloadableInitable {
@Override
public void reload() {
try {
// Yes all of these fields should technically be volatile so they will be updated correctly on reload for HTTP threads to read
// No we're not going to pay for atomic reads in the hot loop however cheap for a one in a billion chance to read an outdated config
// When your discord webhook settings are changed (who changes them in prod?) that can be fixed with a restart
ConfigManager config = GrimAPI.INSTANCE.getConfigManager().getConfig();
if (!config.getBooleanElse("enabled", false)) {
url = null;
@@ -75,12 +88,29 @@ public class DiscordManager implements StartableInitable, ReloadableInitable {
}
String webhook = config.getStringElse("webhook", "");
boolean strictValidation = !config.getBooleanElse("disable-webhook-validation", false);
if (!WEBHOOK_REGEX.test(webhook)) {
LogUtil.error("Discord webhook url does not follow expected format (https://discord.com/api/webhooks/<id>/<token>): " + webhook);
if (webhook.isEmpty()) {
url = null;
} else if (strictValidation) {
if (!WEBHOOK_REGEX.test(webhook)) {
LogUtil.error("Discord webhook URL does not match expected format"
+ " (https://discord.com/api/webhooks/<id>/<token>): " + webhook);
LogUtil.error("If you are using a proxy or custom endpoint,"
+ " set 'disable-webhook-validation: true' in the Discord config.");
url = null;
} else {
url = new URI(webhook);
}
} else {
url = new URI(webhook);
if (!HTTPS_URL_REGEX.test(webhook)) {
LogUtil.error("Discord webhook URL is not a valid HTTPS URL: " + webhook);
url = null;
} else {
LogUtil.info("Webhook validation disabled — using custom endpoint: "
+ webhook.substring(0, Math.min(webhook.length(), 40)) + "...");
url = new URI(webhook);
}
}
// not adding these to the config since they may change in the future
// mainly for just for allowing more customization
@@ -100,9 +130,11 @@ public class DiscordManager implements StartableInitable, ReloadableInitable {
for (String string : config.getStringListElse("violation-content", getDefaultContents())) {
sb.append(string).append("\n");
}
staticContent = sb.toString();
includeTimestamp = config.getBooleanElse("include-timestamp", true);
includeVerbose = config.getBooleanElse("include-verbose", true);
String btReplace = config.getStringElse("backtick-replacement-char", "\u02CB");
backtickReplacement = (btReplace.isEmpty()) ? '\u02CB' : btReplace.charAt(0);
compiledContent = CompiledDiscordTemplate.compile(sb.toString());
} catch (Exception e) {
LogUtil.error("Failed to load Discord webhook configuration", e);
}
@@ -111,25 +143,29 @@ public class DiscordManager implements StartableInitable, ReloadableInitable {
@Contract(value = " -> new", pure = true)
private @NotNull @Unmodifiable List<@NotNull String> getDefaultContents() {
return List.of(
"**Player**: %player%",
"**Player**: `%player%`",
"**Check**: %check%",
"**Violations**: %violations%",
"**Client Version**: %version%",
"**Brand**: %brand%",
"**Brand**: `%brand%`",
"**Ping**: %ping%",
"**TPS**: %tps%"
);
}
public void sendAlert(GrimPlayer player, String verbose, String checkName, int violations) {
public void sendAlert(@NotNull GrimPlayer player, String verbose, String checkName, int violations) {
if (isDisabled()) {
return;
}
String content = staticContent;
content = content.replace("%check%", checkName.replace("_", "\\_")); // just in case any checks are added with an underscore
content = content.replace("%violations%", Integer.toString(violations));
content = MessageUtil.replacePlaceholders(player, content, true);
// Per-alert overlay — avoids polluting the global static map
Map<String, String> statics = new HashMap<>(GrimAPI.INSTANCE.getExternalAPI().getStaticReplacements());
statics.put("%check%", checkName);
statics.put("%violations%", Integer.toString(violations));
Map<String, Function<GrimUser, String>> dynamics = GrimAPI.INSTANCE.getExternalAPI().getVariableReplacements();
String content = compiledContent.render(player, statics, dynamics, backtickReplacement);
Embed embed = new Embed(content)
.color(embedColor)
@@ -137,14 +173,14 @@ public class DiscordManager implements StartableInitable, ReloadableInitable {
.imageURL(MessageUtil.replacePlaceholders(player, embedImageUrl, false))
.thumbnailURL(MessageUtil.replacePlaceholders(player, embedThumbnailUrl, false))
.footer(new EmbedFooter(
MessageUtil.replacePlaceholders(player, embedFooterText, true),
MessageUtil.replacePlaceholders(player, embedFooterText, false),
MessageUtil.replacePlaceholders(player, embedFooterUrl, false)
));
if (includeTimestamp) embed.timestamp(Instant.now());
if (!verbose.isEmpty() && includeVerbose) {
embed.addFields(new EmbedField("Verbose", MessageUtil.filterDiscordText(verbose), true));
embed.addFields(new EmbedField("Verbose", CompiledDiscordTemplate.escapeMarkdown(verbose), true));
}
sendWebhookMessage(new WebhookMessage().addEmbeds(embed));
@@ -12,7 +12,7 @@ public abstract class AbstractTickEndEvent implements StartableInitable {
}
protected void onEndOfTick(GrimPlayer player) {
player.checkManager.getEntityReplication().onEndOfTickEvent();
player.checkManager.getPacketEntityReplication().onEndOfTickEvent();
}
protected boolean shouldInjectEndTick() {
@@ -2,6 +2,7 @@ package ac.grim.grimac.manager.player.handlers;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.api.handler.ResyncHandler;
import ac.grim.grimac.platform.api.player.BlockTranslator;
import ac.grim.grimac.platform.api.world.PlatformChunk;
import ac.grim.grimac.platform.api.world.PlatformWorld;
import ac.grim.grimac.player.GrimPlayer;
@@ -11,12 +12,8 @@ import com.github.retrooper.packetevents.util.Vector3i;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerAcknowledgeBlockChanges;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerBlockChange;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerMultiBlockChange;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class DefaultResyncHandler implements ResyncHandler {
private final GrimPlayer player;
public record DefaultResyncHandler(GrimPlayer player) implements ResyncHandler {
private static void resyncPositions(GrimPlayer player, int minBlockX, int mY, int minBlockZ, int maxBlockX, int mxY, int maxBlockZ) {
// Check the 4 corners of the player world for loaded chunks before calling event
@@ -58,6 +55,8 @@ public class DefaultResyncHandler implements ResyncHandler {
int minChunkZ = minBlockZ >> 4;
int maxChunkZ = maxBlockZ >> 4;
BlockTranslator translator = player.platformPlayer.getBlockTranslator();
for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
int minZ = currChunkZ == minChunkZ ? minBlockZ & 15 : 0; // coordinate in chunk
int maxZ = currChunkZ == maxChunkZ ? maxBlockZ & 15 : 15; // coordinate in chunk
@@ -81,8 +80,9 @@ public class DefaultResyncHandler implements ResyncHandler {
for (int currZ = minZ; currZ <= maxZ; ++currZ) {
for (int currX = minX; currX <= maxX; ++currX) {
for (int currY = minY; currY <= maxY; ++currY) {
int blockId = chunk.getBlockID(currX, currY | (currChunkY << 4), currZ);
encodedBlocks[blockIndex++] = new WrapperPlayServerMultiBlockChange.EncodedBlock(blockId, currX, currY | (currChunkY << 4), currZ);
int rawId = chunk.getBlockID(currX, currY | (currChunkY << 4), currZ);
int networkId = translator.translate(rawId);
encodedBlocks[blockIndex++] = new WrapperPlayServerMultiBlockChange.EncodedBlock(networkId, currX, currY | (currChunkY << 4), currZ);
}
}
}
@@ -8,7 +8,7 @@ public class ResetTick implements Tickable {
@Override
public void tick() {
for (GrimPlayer player : GrimAPI.INSTANCE.getPlayerDataManager().getEntries()) {
player.checkManager.getEntityReplication().tickStartTick();
player.checkManager.getPacketEntityReplication().tickStartTick();
}
}
}
@@ -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);
@@ -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);
@@ -0,0 +1,7 @@
package ac.grim.grimac.platform.api.player;
public interface BlockTranslator {
BlockTranslator IDENTITY = serverBlockId -> serverBlockId;
int translate(int serverBlockId);
}
@@ -45,4 +45,6 @@ public interface PlatformPlayer extends GrimEntity, OfflinePlatformPlayer {
* Vanilla MC replaces ServerPlayerEntity references on respawn and dimension change
*/
default void replaceNativePlayer(Object nativePlayerObject) {}
BlockTranslator getBlockTranslator();
}
@@ -20,6 +20,7 @@ import ac.grim.grimac.platform.api.player.PlatformPlayer;
import ac.grim.grimac.predictionengine.MovementCheckRunner;
import ac.grim.grimac.predictionengine.PointThreeEstimator;
import ac.grim.grimac.predictionengine.UncertaintyHandler;
import ac.grim.grimac.manager.AttackCooldownHandler;
import ac.grim.grimac.utils.anticheat.LogUtil;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import ac.grim.grimac.utils.anticheat.update.BlockBreak;
@@ -115,7 +116,7 @@ public class GrimPlayer implements GrimUser {
// End transaction handling stuff
// Manager like classes
public final CheckManager checkManager;
public final ActionManager actionManager;
public final AttackCooldownHandler attackCooldown;
public final PunishmentManager punishmentManager;
public final MovementCheckRunner movementCheckRunner;
public final SyncedTags tagManager;
@@ -244,7 +245,6 @@ public class GrimPlayer implements GrimUser {
public final Object2DoubleMap<FluidTag> fluidHeight = new Object2DoubleArrayMap<>(2);
// possibleEyeHeights[0] = Standing eye heights, [1] = Sneaking. [2] = Elytra, Swimming, and Riptide Trident which only exists in 1.9+
public final double[][] possibleEyeHeights = new double[3][];
public int totalFlyingPacketsSent;
public boolean hasInventoryOpen;
public long lastInventoryOpen;
public InventoryDesyncStatus inventoryDesyncStatus;
@@ -296,7 +296,7 @@ public class GrimPlayer implements GrimUser {
cameraEntity = new CompensatedCameraEntity(this);
lastInstanceManager = new LastInstanceManager(this);
actionManager = new ActionManager(this);
attackCooldown = new AttackCooldownHandler(this);
checkManager = new CheckManager(this);
punishmentManager = new PunishmentManager(this);
this.tagManager = new SyncedTags(this); // must be after this.user = user
@@ -414,7 +414,7 @@ public class GrimPlayer implements GrimUser {
if (viaPacketTracker != null) viaPacketTracker.setIntervalPackets(viaPacketTracker.getIntervalPackets() - 1);
if (skipped > 0 && System.currentTimeMillis() - joinTime > 5000)
checkManager.getCheck(TransactionOrder.class).flagAndAlert("skipped: " + skipped);
checkManager.getCheck(TransactionOrder.class).flagAndAlert("skipped=" + skipped);
do {
data = transactionsSent.poll();
@@ -273,13 +273,13 @@ public class PredictionEngine {
}
private Pair<Vector3dm, Vector3dm> doSeekingWallCollisions(GrimPlayer player, Vector3dm primaryPushMovement, Vector3dm originalClientVel, VectorData clientVelAfterInput) {
// TODO: causes falses when riding nautilus on the ground, figure out why this is even here
boolean vehicleKB = player.inVehicle() && clientVelAfterInput.isKnockback() && clientVelAfterInput.vector.getY() == 0;
// Extra collision epsilon required for vehicles to be accurate
double xAdditional = Math.signum(primaryPushMovement.getX()) * SimpleCollisionBox.COLLISION_EPSILON;
// The server likes sending y=0 kb "lifting" the player off the ground.
// The client doesn't send the vehicles onGround status, so we can't check for ground like normal.
double yAdditional = vehicleKB ? 0 : (primaryPushMovement.getY() > 0 ? 1 : -1) * SimpleCollisionBox.COLLISION_EPSILON;
double yAdditional = vehicleKB ? 0 : (player.inVehicle() ? Math.signum(primaryPushMovement.getY()) : (primaryPushMovement.getY() > 0 ? 1 : -1))
* SimpleCollisionBox.COLLISION_EPSILON;
double zAdditional = Math.signum(primaryPushMovement.getZ()) * SimpleCollisionBox.COLLISION_EPSILON;
// Expand by the collision epsilon to test if the player collided with a block (as this resets the velocity in that direction)
@@ -17,8 +17,6 @@ public class PredictionEngineNautilusWater extends PredictionEngine {
@Override
public void endOfTick(GrimPlayer player, double delta) {
super.endOfTick(player, delta);
for (VectorData vector : player.getPossibleVelocitiesMinusKnockback()) {
vector.vector.setX(vector.vector.getX() * multiplier);
vector.vector.setY(vector.vector.getY() * multiplier);
@@ -5,6 +5,8 @@ import ac.grim.grimac.api.GrimUser;
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;
@@ -26,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;
}
@@ -34,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);
@@ -60,9 +66,9 @@ public class MessageUtil {
private @Nullable String replacePlaceholders(@Nullable GrimPlayer grimPlayer, @Nullable PlatformPlayer platformPlayer, @Nullable String string, boolean removeFormatting) {
if (string == null) return null;
// --- OPTIMIZATION 1: THE FAST PATH ---
// If the string contains no '%' characters, it's impossible for it to have placeholders.
// indexOf() is a JVM intrinsic and is magnitudes faster than even creating a Matcher.
// --- PHASE 1: FAST PATH ---
// JVM Intrinsic: Scanning for a char is significantly faster than initializing a Regex Matcher.
// If there is no '%', we can skip all allocation.
if (string.indexOf('%') == -1) {
// Since there are no % signs we can skip calling papi or our own replacement code
return string;
@@ -70,8 +76,8 @@ public class MessageUtil {
final Matcher matcher = UNIFIED_PLACEHOLDER_PATTERN.matcher(string);
// If matcher.find() is false, it means '%' existed but not in a valid %...% pattern.
// This avoids allocating a StringBuilder unless absolutely necessary.
// If '%' exists but doesn't form a valid simple placeholder, skip to the PAPI/Legacy handler.
// This avoids allocating the StringBuilder below.
if (!matcher.find()) {
return GrimAPI.INSTANCE.getMessagePlaceHolderManager().replacePlaceholders(platformPlayer, string);
}
@@ -79,17 +85,17 @@ public class MessageUtil {
// Get references to the maps once, outside the loop.
final Map<String, String> staticReplacements = GrimAPI.INSTANCE.getExternalAPI().getStaticReplacements();
final Map<String, Function<GrimUser, String>> variableReplacements = GrimAPI.INSTANCE.getExternalAPI().getVariableReplacements();
final StringBuilder sb = new StringBuilder(string.length() + 32); // Pre-size with a little extra room
// 32 is a heuristic buffer. It roughly covers the expansion cost of one UUID (36 chars) vs one placeholder (6 chars).
final StringBuilder sb = new StringBuilder(string.length() + 32);
// --- OPTIMIZATION 2: THE UNIFIED SINGLE-PASS LOOP ---
// We use a do-while loop because we already performed the first matcher.find().
// --- PHASE 2: THE REPLACEMENT LOOP ---
// Used do-while because the first `matcher.find()` was already called above.
do {
// The full placeholder, e.g., "%tps%" or "%prefix%"
final String keyWithPercent = matcher.group(0);
String value = null;
// --- OPTIMIZATION 3: UNIFIED LOOKUP ---
// We check the static map first. This is a single, O(1) hash map lookup.
// optimization: We check the static map first. This is a single, O(1) hash map lookup.
String staticValue = staticReplacements.get(keyWithPercent);
if (staticValue != null) {
@@ -109,15 +115,13 @@ public class MessageUtil {
// In that case, we treat the placeholder as literal text by appending the original key.
if (value == null) {
value = keyWithPercent;
}
// --- OPTIMIZATION 4: CONDITIONAL FORMATTING ---
// The check for `removeFormatting` is inside the loop, but it's a simple boolean
// yes the check for `removeFormatting` is inside the loop, but it's a simple boolean
// check and the cost is negligible compared to the string operations.
if (removeFormatting) {
// Note: This assumes `filterDiscordText` is reasonably fast.
// Do NOT escape - this is probably a PAPI key that must remain intact
} else if (removeFormatting) {
// Note: This assumes `escapeMarkdown` is reasonably fast.
// If it's slow, there are further micro-optimizations, but this is the right place for it.
value = filterDiscordText(value);
value = CompiledDiscordTemplate.escapeMarkdown(value);
}
// `appendReplacement` efficiently appends the text between matches and our replacement value.
@@ -135,35 +139,6 @@ public class MessageUtil {
return GrimAPI.INSTANCE.getMessagePlaceHolderManager().replacePlaceholders(platformPlayer, grimReplaced).replace(PLACEHOLDER_ESCAPE_CHAR, '%');
}
public static String filterDiscordText(String message) {
if (message == null || message.isBlank()) return message;
final StringBuilder sb = new StringBuilder(message.length());
for (int i = 0; i < message.length(); ++i) {
final char c = message.charAt(i);
// Escape a newline
if (c == '\n') {
sb.append("\\n");
} // Escape Markdown special characters
else if (c == '`' || c == '*' || c == '_' || c == '~' || c == '|') {
sb.append('\\').append(c);
} else {
// Escape "# ", "> ", etc
if (c == '#' || c == '>' || c == '-') {
// check if there's a space next
if (((i + 1 < message.length()) && (message.charAt(i + 1) == ' '))
&& ((i == 0) || (message.charAt(i - 1) == '\n'))) {
sb.append("\\").append(c);
} else {
sb.append(c);
}
} else {
sb.append(c);
}
}
}
return sb.toString();
}
public @NotNull Component replacePlaceholders(@NotNull GrimPlayer player, @NotNull Component component) {
// Replacement config that forces any placeholder replacement to be pure text
final TextReplacementConfig safeReplacement = TextReplacementConfig.builder()
@@ -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) -> {
@@ -587,7 +587,7 @@ public enum HitboxData implements HitBoxFactory {
new HexCollisionBox(0.0D, 0.0D, 14.0D, 2.0D, 16.0D, 16.0D),
new HexCollisionBox(14.0D, 0.0D, 14.0D, 16.0D, 16.0D, 16.0D));
if (data.getHalf() == Half.LOWER) { // Add the unstable shape to the collision boxes
if (data.isBottom()) { // Add the unstable shape to the collision boxes
box.add(new HexCollisionBox(0.0D, 0.0D, 0.0D, 2.0D, 2.0D, 16.0D));
box.add(new HexCollisionBox(14.0D, 0.0D, 0.0D, 16.0D, 2.0D, 16.0D));
box.add(new HexCollisionBox(0.0D, 0.0D, 14.0D, 16.0D, 2.0D, 16.0D));
@@ -1,4 +1,4 @@
package ac.grim.grimac.utils.webhook;
package ac.grim.grimac.utils.data.json;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
@@ -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;
@@ -70,7 +70,8 @@ public class PacketEntitySelf extends PacketEntity {
trackAttribute(movementSpeed);
trackAttribute(ValuedAttribute.ranged(Attributes.ATTACK_DAMAGE, 2, 0, 2048)); // NOTE: Not synced to client currently.
trackAttribute(ValuedAttribute.ranged(Attributes.ATTACK_SPEED, 4, 0, 1024)
.requiredVersion(player, ClientVersion.V_1_9));
.requiredVersion(player, ClientVersion.V_1_9)
.withGetRewriter(value -> PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_9) ? 20 : value));
trackAttribute(ValuedAttribute.ranged(Attributes.JUMP_STRENGTH, 0.42f, 0, 32)
.requiredVersion(player, ClientVersion.V_1_20_5));
trackAttribute(ValuedAttribute.ranged(Attributes.BLOCK_BREAK_SPEED, 1.0, 0, 1024)
@@ -0,0 +1,250 @@
package ac.grim.grimac.utils.data.webhook.discord;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.api.GrimUser;
import ac.grim.grimac.player.GrimPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public record CompiledDiscordTemplate(Segment[] segments) {
/**
* Markdown context as determined by a state-machine scan of the template.
* Used only at compile time.
*/
private enum MarkdownContext {
NORMAL,
INLINE_CODE,
CODE_BLOCK
}
/**
* Escape strategy assigned to each placeholder at compile time.
*/
public enum EscapeMode {
/**
* Full Markdown escaping: \, `, *, _, ~, |, [], (), line-start chars, newlines
*/
FULL_MARKDOWN,
/**
* Inside a code span: only backtick substitution (backslash escaping doesn't work in Discord code spans)
*/
CODE_SPAN,
/**
* No escaping (placeholder left raw for PAPI)
*/
NONE
}
// Identical to the existing pattern
private static final Pattern PLACEHOLDER = Pattern.compile("%([a-zA-Z0-9_]+)%");
sealed interface Segment permits Literal, Placeholder {
}
record Literal(String text) implements Segment {
}
record Placeholder(String key, EscapeMode mode) implements Segment {
}
// ──────────────────── COMPILE (once per config reload) ────────────────────
public static CompiledDiscordTemplate compile(String template) {
List<Segment> parts = new ArrayList<>();
Matcher m = PLACEHOLDER.matcher(template);
MarkdownContext ctx = MarkdownContext.NORMAL;
int lastEnd = 0;
while (m.find()) {
String gap = template.substring(lastEnd, m.start());
if (!gap.isEmpty()) parts.add(new Literal(gap));
// Advance the context through all the literal text before this placeholder
ctx = advanceContext(ctx, gap);
EscapeMode mode = switch (ctx) {
case NORMAL -> EscapeMode.FULL_MARKDOWN;
case INLINE_CODE,
CODE_BLOCK -> EscapeMode.CODE_SPAN;
};
parts.add(new Placeholder(m.group(0), mode));
lastEnd = m.end();
}
if (lastEnd < template.length()) {
parts.add(new Literal(template.substring(lastEnd)));
}
return new CompiledDiscordTemplate(parts.toArray(Segment[]::new));
}
// ──────────────────── RENDER (once per alert) ────────────────────
/**
* @param player Grim player (nullable)
* @param statics Static replacements (may include per-alert overrides like %check%)
* @param dynamics Dynamic replacements (lazy functions like %tps%)
* @param backtickReplacement Char to substitute for backticks inside code spans (loaded from config)
*/
public String render(@NotNull GrimPlayer player,
@NotNull Map<String, String> statics,
@NotNull Map<String, Function<GrimUser, String>> dynamics,
char backtickReplacement) {
StringBuilder sb = new StringBuilder(segments.length * 32);
for (Segment seg : segments) {
if (seg instanceof Literal l) {
sb.append(l.text);
} else if (seg instanceof Placeholder p) {
// Priority: static → dynamic → external (PAPI)
String val = statics.get(p.key);
if (val == null) {
Function<GrimUser, String> fn = dynamics.get(p.key);
if (fn != null) val = fn.apply(player);
}
if (val == null) {
String resolved = GrimAPI.INSTANCE.getMessagePlaceHolderManager()
.replacePlaceholders(player.platformPlayer, p.key);
if (!resolved.equals(p.key)) val = resolved;
}
if (val != null) {
sb.append(escape(val, p.mode, backtickReplacement));
} else {
sb.append(p.key); // truly unresolved — leave raw
}
}
}
return sb.toString();
}
private static String escape(String value, EscapeMode mode, char backtickReplacement) {
if (mode == EscapeMode.NONE) return value;
if (mode == EscapeMode.CODE_SPAN) return escapeCodeSpan(value, backtickReplacement);
return escapeMarkdown(value);
}
/**
* Full Discord Markdown escaping for untrusted data values.
* <p>
* This method treats input as <b>raw data</b>, not pre-formatted markdown.
* A literal backslash in a player name is data and must render as a visible backslash.
* This means "already escaped" input like {@code \*} correctly becomes {@code \\*}
* (rendering as {@code \*}), which is the intended behavior — identical to SQL parameterization.
* <p>
* All escaped characters ({@code \X}) render identically to their unescaped form ({@code X})
* in Discord's CommonMark variant, so escaping is invisible to end users.
*/
public static String escapeMarkdown(String s) {
if (s == null || s.isEmpty()) return s;
StringBuilder sb = new StringBuilder(s.length() + 16);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
// Backslash MUST be first — prevents our own escape backslashes
// from being re-escaped if the input already contains backslashes.
case '\\' -> sb.append("\\\\");
// Inline code spans (`text`)
case '`' -> sb.append("\\`");
// Bold (**text**) and italic (*text*)
case '*' -> sb.append("\\*");
// Underlined (__text__) and italic (_text_)
case '_' -> sb.append("\\_");
// Strikethrough (~~text~~)
case '~' -> sb.append("\\~");
// Spoiler tags (||text||)
case '|' -> sb.append("\\|");
// Link [text](url) and image ![alt](url) syntax
case '[' -> sb.append("\\[");
case ']' -> sb.append("\\]");
case '(' -> sb.append("\\(");
case ')' -> sb.append("\\)");
// Auto-linking (https://...) and emoji (:name:)
case ':' -> sb.append("\\:");
// Timestamps (<t:...>), mentions (<@id>), custom emoji (<:n:id>),
// embed suppression (<url>)
case '<' -> sb.append("\\<");
// Headers (#, ##, ###), block quotes (>, >>>).
// Always escaped — \# and \> render identically to # and >.
case '#' -> sb.append("\\#");
case '>' -> sb.append("\\>");
// Unordered lists (- item) and subtext (-# text)
case '-' -> sb.append("\\-");
// Ordered lists (1. item). \. renders identically to .
case '.' -> sb.append("\\.");
// Newlines in injected values would break embed layout and enable
// line-start syntax injection (headers, quotes, lists).
// Replaced with literal "\n" text. Template newlines are unaffected
// (they pass through as Literal segments, not through this method).
case '\n' -> sb.append("\\n");
default -> sb.append(c);
}
}
return sb.toString();
}
/**
* Code span escaping: Discord ignores backslash escapes inside code spans,
* so backticks are replaced with a visually-similar substitute character.
* If replacement == '`', this is a no-op (user disabled the feature).
*/
public static String escapeCodeSpan(String s, char replacement) {
if (s == null || s.isEmpty() || replacement == '`') return s;
return s.replace('`', replacement);
}
// ──────────────────── STATE MACHINE ────────────────────
/**
* Advances Markdown context through literal template text.
* Handles: backslash escapes (NORMAL only), single-` inline code, triple-` code blocks.
* <p>
* Edge cases resolved:
* <pre>
* `text %p% text` → INLINE_CODE (` opens before placeholder)
* `a`%p%`b` → NORMAL (` opens then ` closes before placeholder)
* \`%p%` → NORMAL (backtick is backslash-escaped)
* </pre>
* <b>Known limitation:</b> Double-backtick code spans ({@code `` text ``}) are not tracked.
*/
private static MarkdownContext advanceContext(MarkdownContext ctx, String text) {
int i = 0;
while (i < text.length()) {
char c = text.charAt(i);
if (ctx == MarkdownContext.NORMAL) {
if (c == '\\' && i + 1 < text.length()) {
i += 2; // skip escaped char entirely
continue;
}
if (c == '`') {
if (i + 2 < text.length()
&& text.charAt(i + 1) == '`'
&& text.charAt(i + 2) == '`') {
ctx = MarkdownContext.CODE_BLOCK;
i += 3;
continue;
}
ctx = MarkdownContext.INLINE_CODE;
}
} else if (ctx == MarkdownContext.INLINE_CODE) {
if (c == '`') ctx = MarkdownContext.NORMAL;
} else { // CODE_BLOCK
if (c == '`'
&& i + 2 < text.length()
&& text.charAt(i + 1) == '`'
&& text.charAt(i + 2) == '`') {
ctx = MarkdownContext.NORMAL;
i += 3;
continue;
}
}
i++;
}
return ctx;
}
}
@@ -1,5 +1,6 @@
package ac.grim.grimac.utils.webhook;
package ac.grim.grimac.utils.data.webhook.discord;
import ac.grim.grimac.utils.data.json.JsonSerializable;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.Getter;
@@ -12,8 +13,8 @@ import org.jetbrains.annotations.Nullable;
import java.time.Instant;
import java.util.Objects;
import static ac.grim.grimac.utils.webhook.JsonSerializable.deserializeArray;
import static ac.grim.grimac.utils.webhook.JsonSerializable.serializeArray;
import static ac.grim.grimac.utils.data.json.JsonSerializable.deserializeArray;
import static ac.grim.grimac.utils.data.json.JsonSerializable.serializeArray;
import static java.util.Objects.requireNonNull;
@Getter
@@ -1,5 +1,6 @@
package ac.grim.grimac.utils.webhook;
package ac.grim.grimac.utils.data.webhook.discord;
import ac.grim.grimac.utils.data.json.JsonSerializable;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.Getter;
@@ -1,5 +1,6 @@
package ac.grim.grimac.utils.webhook;
package ac.grim.grimac.utils.data.webhook.discord;
import ac.grim.grimac.utils.data.json.JsonSerializable;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.Getter;
@@ -1,5 +1,6 @@
package ac.grim.grimac.utils.webhook;
package ac.grim.grimac.utils.data.webhook.discord;
import ac.grim.grimac.utils.data.json.JsonSerializable;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.Getter;
@@ -1,5 +1,6 @@
package ac.grim.grimac.utils.webhook;
package ac.grim.grimac.utils.data.webhook.discord;
import ac.grim.grimac.utils.data.json.JsonSerializable;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.Getter;
@@ -11,8 +12,8 @@ import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import static ac.grim.grimac.utils.webhook.JsonSerializable.deserializeArray;
import static ac.grim.grimac.utils.webhook.JsonSerializable.serializeArray;
import static ac.grim.grimac.utils.data.json.JsonSerializable.deserializeArray;
import static ac.grim.grimac.utils.data.json.JsonSerializable.serializeArray;
@Getter
@Setter
@@ -21,7 +21,8 @@ public class Inventory extends AbstractContainerMenu {
public static final int SLOT_LEGGINGS = 6;
public static final int SLOT_BOOTS = 7;
private static final int TOTAL_SIZE = 46;
public int selected = 0;
@Getter
private int selected;
@Getter
private final CorrectingPlayerInventoryStorage inventoryStorage;
@@ -68,7 +69,7 @@ public class Inventory extends AbstractContainerMenu {
}
public boolean hasItemType(ItemType item) {
for (int i = 0; i < inventoryStorage.items.length; ++i) {
for (int i = 0; i < inventoryStorage.getSize(); ++i) {
if (inventoryStorage.getItem(i).getType() == item) {
return true;
}
@@ -93,7 +94,7 @@ public class Inventory extends AbstractContainerMenu {
}
public int getFreeSlot() {
for (int i = 0; i < inventoryStorage.items.length; ++i) {
for (int i = 0; i < inventoryStorage.getSize(); ++i) {
if (inventoryStorage.getItem(i).isEmpty()) {
return i;
}
@@ -252,4 +253,9 @@ public class Inventory extends AbstractContainerMenu {
public boolean canTakeItemForPickAll(ItemStack p_38908_, Slot p_38909_) {
return p_38909_.inventoryStorageSlot != 0; // Result slot
}
public void setSelected(int selected) {
this.selected = selected;
this.player.attackCooldown.updateHeldItem();
}
}
@@ -4,7 +4,7 @@ import com.github.retrooper.packetevents.protocol.item.ItemStack;
import lombok.Getter;
public class InventoryStorage {
protected final ItemStack[] items;
private final ItemStack[] items;
@Getter
private final int size;
@@ -26,7 +26,7 @@ public class InventoryStorage {
}
public ItemStack removeItem(int slot, int amount) {
return slot >= 0 && slot < items.length && !items[slot].isEmpty() && amount > 0 ? items[slot].split(amount) : ItemStack.EMPTY;
return slot >= 0 && slot < size && !items[slot].isEmpty() && amount > 0 ? items[slot].split(amount) : ItemStack.EMPTY;
}
public int getMaxStackSize() {
@@ -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());
@@ -56,6 +56,14 @@ public class CompensatedInventory extends Check implements PacketCheck {
// Unsupported inventory is -2
private int packetSendingInventorySize = PLAYER_INVENTORY_CASE;
// The item held at the start of the current client tick (processed at the end of the previous tick)
// Currently only used by 1.21.11+ players to handle attribute swapping items with the ATTACK_RANGE Component
private ItemStack startOfTickStack = ItemStack.EMPTY;
public ItemStack getStartOfTickStack() {
return startOfTickStack;
}
public CompensatedInventory(GrimPlayer playerData) {
super(playerData);
@@ -223,7 +231,7 @@ public class CompensatedInventory extends Check implements PacketCheck {
return;
// 1.19.4+ clients support swapping with non-empty items
int swapItemSlot = item.getHand() == InteractionHand.MAIN_HAND ? inventory.selected + Inventory.HOTBAR_OFFSET : Inventory.SLOT_OFFHAND;
int swapItemSlot = item.getHand() == InteractionHand.MAIN_HAND ? inventory.getSelected() + Inventory.HOTBAR_OFFSET : Inventory.SLOT_OFFHAND;
// Mojang implemented this stupidly, I rewrote their item swap code to make it somewhat cleaner.
// Slot in hotbar
@@ -234,9 +242,7 @@ public class CompensatedInventory extends Check implements PacketCheck {
inventory.getInventoryStorage().handleClientClaimedSlotSet(slot);
inventory.getInventoryStorage().setItem(slot, use);
}
}
if (event.getPacketType() == PacketType.Play.Client.PLAYER_DIGGING) {
} else if (event.getPacketType() == PacketType.Play.Client.PLAYER_DIGGING) {
WrapperPlayClientPlayerDigging dig = new WrapperPlayClientPlayerDigging(event);
// 1.8 clients don't predict dropping items
@@ -258,18 +264,14 @@ public class CompensatedInventory extends Check implements PacketCheck {
inventory.setHeldItem(null);
inventory.getInventoryStorage().handleClientClaimedSlotSet(Inventory.HOTBAR_OFFSET + player.packetStateData.lastSlotSelected);
}
}
if (event.getPacketType() == PacketType.Play.Client.HELD_ITEM_CHANGE) {
} else if (event.getPacketType() == PacketType.Play.Client.HELD_ITEM_CHANGE) {
final int slot = new WrapperPlayClientHeldItemChange(event).getSlot();
// Stop people from spamming the server with an out-of-bounds exception
if (slot > 8 || slot < 0) return;
inventory.selected = slot;
}
if (event.getPacketType() == PacketType.Play.Client.CREATIVE_INVENTORY_ACTION) {
inventory.setSelected(slot);
} else if (event.getPacketType() == PacketType.Play.Client.CREATIVE_INVENTORY_ACTION) {
WrapperPlayClientCreativeInventoryAction action = new WrapperPlayClientCreativeInventoryAction(event);
if (player.gamemode != GameMode.CREATIVE) return;
@@ -281,9 +283,7 @@ public class CompensatedInventory extends Check implements PacketCheck {
inventory.getSlot(action.getSlot()).set(action.getItemStack());
inventory.getInventoryStorage().handleClientClaimedSlotSet(action.getSlot());
}
}
if (event.getPacketType() == PacketType.Play.Client.CLICK_WINDOW && !event.isCancelled()) {
} else if (event.getPacketType() == PacketType.Play.Client.CLICK_WINDOW && !event.isCancelled()) {
WrapperPlayClientClickWindow click = new WrapperPlayClientClickWindow(event);
// How is this possible? Maybe transaction splitting.
@@ -312,10 +312,10 @@ public class CompensatedInventory extends Check implements PacketCheck {
if (slot == -1 || slot == -999 || slot < menu.getSlots().size()) {
menu.doClick(button, slot, clickType);
}
}
if (event.getPacketType() == PacketType.Play.Client.CLOSE_WINDOW) {
} else if (event.getPacketType() == PacketType.Play.Client.CLOSE_WINDOW) {
this.closeActiveInventory();
} else if (event.getPacketType() == PacketType.Play.Client.CLIENT_TICK_END) {
this.startOfTickStack = getHeldItem();
}
}
@@ -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);
}
@@ -90,6 +90,10 @@ public class CorrectingPlayerInventoryStorage extends InventoryStorage {
}
super.setItem(item, stack);
if (item == player.inventory.inventory.getSelected() + Inventory.HOTBAR_OFFSET) {
player.attackCooldown.updateHeldItem();
}
}
/**
@@ -233,4 +233,8 @@ public class GrimMath {
public static double square(double num) {
return num * num;
}
public static double distanceSquared(double x1, double y1, double z1, double x2, double y2, double z2) {
return GrimMath.square(x1 - x2) + GrimMath.square(y1 - y2) + GrimMath.square(z1 - z2);
}
}
@@ -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;
@@ -263,8 +263,8 @@ public final class BoundingBoxSize {
// WHY DOES VIAVERSION OFFSET BOATS? THIS MAKES IT HARD TO SUPPORT, EVEN IF WE INTERPOLATE RIGHT.
// I gave up and just exempted boats from the reach check and gave up with interpolation for collisions
return 0.5625f;
} else if (EntityTypes.isTypeInstanceOf(type, EntityTypes.NAUTILUS)) {
return 0.95F;
} else if (EntityTypes.isTypeInstanceOf(type, EntityTypes.ABSTRACT_NAUTILUS)) {
return 0.95f;
} else if (type == EntityTypes.HAPPY_GHAST) {
return 4.0f;
} else if (type == EntityTypes.CAT) {
@@ -232,7 +232,7 @@ public class WorldRayTrace {
// in previous parts of this check when we didn't check for any obstructions like blocks/entities
public static @NotNull Pair<@NotNull Double, @NotNull HitData> didRayTraceHit(GrimPlayer player, PacketEntity targetEntity,
List<Pair<Vector3dm, Double>> possibleLookVecsAndEyeHeights,
Vector3d from) {
double x, double y, double z) {
HitData firstObstruction = null;
double firstObstructionDistanceSq = 0;
@@ -241,7 +241,7 @@ public class WorldRayTrace {
Vector3dm lookVec = vectorDoublePair.first();
double eye = vectorDoublePair.second();
Vector3dm eyes = new Vector3dm(from.getX(), from.getY() + eye, from.getZ());
Vector3dm eyes = new Vector3dm(x, y + eye, z);
// this function is completely 0.03 aware
final HitData hitResult = WorldRayTrace.getNearestHitResult(player, targetEntity, eyes, lookVec);
+1
View File
@@ -23,6 +23,7 @@ client-brand:
ignored-clients:
- "^vanilla$"
- "^fabric$"
- "^quilt$"
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
- "^Feather Fabric$"
- "^labymod$"
+1
View File
@@ -23,6 +23,7 @@ client-brand:
ignored-clients:
- "^vanilla$"
- "^fabric$"
- "^quilt$"
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
- "^Feather Fabric$"
- "^labymod$"
+1
View File
@@ -23,6 +23,7 @@ client-brand:
ignored-clients:
- "^vanilla$"
- "^fabric$"
- "^quilt$"
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
- "^Feather Fabric$"
- "^labymod$"
+1
View File
@@ -23,6 +23,7 @@ client-brand:
ignored-clients:
- "^vanilla$"
- "^fabric$"
- "^quilt$"
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
- "^Feather Fabric$"
- "^labymod$"
+1
View File
@@ -24,6 +24,7 @@ client-brand:
ignored-clients:
- "^vanilla$"
- "^fabric$"
- "^quilt$"
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
- "^Feather Fabric$"
- "^labymod$"
+1
View File
@@ -25,6 +25,7 @@ client-brand:
ignored-clients:
- "^vanilla$"
- "^fabric$"
- "^quilt$"
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
- "^Feather Fabric$"
- "^labymod$"
+1
View File
@@ -23,6 +23,7 @@ client-brand:
ignored-clients:
- "^vanilla$"
- "^fabric$"
- "^quilt$"
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
- "^Feather Fabric$"
- "^labymod$"
+238
View File
@@ -0,0 +1,238 @@
# GrimAC główna konfiguracja
# Kod źródłowy: https://github.com/MWHunter/Grim
# Prawa autorskie 2025 DefineOutside i współautorzy, licencjonowane na podstawie GPLv3.
# Zmodyfikowane pliki binarne, albo pluginy z skopiowanym kodem grim, muszą być prywatne,
# lub z pełnym kodem źródłowym dostępnym dla kupujących bez dodatkowych kosztów.
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.
# 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?
send: false
# Czy alerty otrzymywane z innych serwerów powinny być przekazywane temu serwerowi?
receive: false
verbose:
print-to-console: false
check-for-updates: true
client-brand:
# Oznacza to, że nie będzie transmitować swojej marki operatorom, jeśli marka będzie pasować do następujących wyrażeń regularnych.
ignored-clients:
- "^vanilla$"
- "^fabric$"
- "^quilt$"
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
- "^Feather Fabric$"
- "^labymod$"
# Grim umieści na czarnej liście konkretne wersje Forge, które zawierają wbudowane modyfikacje Reach (Forge 1.18.2 do 1.19.3).
# Ustawienie tej opcji na fałsz umożliwi tym klientom połączenie się z serwerem. Wyłączasz tę opcję na własne ryzyko.
disconnect-blacklisted-forge-versions: true
spectators:
# Ukryj wszystkich widzów z uprawnieniem grim.spectator, niezależnie od tego, czy są oni faktycznie aktywnymi widzami
hide-regardless: false
# Spowoduje, że widzowie będą ukryci tylko w tych światach, a pozostawienie pustego pola umożliwi dostęp do wszystkich światów
allowed-worlds:
- ""
# Ile czasu powinni mieć gracze, zanim ich wyrzucimy za przekroczenie limitu czasu? Domyślnie = 60 sekund
max-transaction-time: 60
# Włącz tę opcję, aby uniemożliwić Grimowi anulowanie komunikatów Pong otrzymanych od graczy.
# Może to poprawić kompatybilność z innymi programami antycheatowymi. Może to powodować problemy z limiterami pakietów.
# Nie włączaj tej opcji, jeśli nie masz pewności, co robisz.
disable-pong-cancelling: false
# Czy domyślna procedura obsługi resynchronizacji bloków powinna zostać wyłączona?
# Domyślnie Grim odczytuje bloki ze świata serwera i wysyła je do klienta w celu naprawy desynchronizacji.
# Jeśli używasz wtyczek, które tworzą fałszywe bloki oparte na pakietach (które nie istnieją w świecie serwera),
# powinieneś ustawić tę opcję na true, aby uniemożliwić Grimowi nadpisywanie ich blokami air/real.
# Wymusza to użycie procedury obsługi „NoOp” (No Operation), chyba że zostanie ona udostępniona za pośrednictwem API.
disable-default-resync-handler: false
# Czy zduplikowany pakiet ruchu powinien zostać anulowany?
# Mojang naprawił ten problem w wersji 1.21. To była ich próba naprawienia „desynchronizacji kontenera”. https://bugs.mojang.com/browse/MC-12363
# To ustawienie dotyczy tylko klientów 1.17-1.20.5 na serwerach 1.8.
cancel-duplicate-packet: true
# Czy ignorować rotację w zduplikowanych pakietach
ignore-duplicate-packet-rotation: false
Simulation:
# O ile powinniśmy pomnożyć całkowitą przewagę, gdy gracz gra zgodnie z zasadami?
# Tak wygląda domyślna konfiguracja (oś x = sekundy, oś y = 1/1000 bloku): https://www.desmos.com/calculator/d4ufgxrxer
setback-decay-multiplier: 0.999
# Jak duże odchylenie od ruchu gracza powinno skutkować naruszeniem?
# Mierzone w blokach od możliwego ruchu.
# Uwzględniamy Optifine, przełączając tablice trygonometryczne, ale zmniejszenie tej wartości do 0.001 zmniejszy
# liczbę fałszywych alarmów spowodowanych przez FastMath, jeśli ta kompensacja nie zadziała...
threshold: 0.001
# Jak duże naruszenie w jednym ticku spowoduje natychmiastowe cofnięcie gracza?
# -1 aby wyłączyć
immediate-setback-threshold: 0.1
# Jak dużą całkowitą przewagę (we wszystkich tickach) trzeba osiągnąć, zanim zaczniemy cofać gracza?
# -1 aby wyłączyć
max-advantage: 1
# Po 50 sekundach z domyślną konfiguracją, przewaga gracza spadnie z 4 bloków do 1 bloku.
# Ma to na celu zapobieżenie gromadzeniu zbyt wielu naruszeń, których gracz nie mógłby później wyzerować.
# Domyślny sufit przewagi (oś x = sekundy, oś y = 1/1000 bloku): https://www.desmos.com/calculator/4lovswdarj
max-ceiling: 4
# Próg poziomu naruszeń wymagany do cofnięcia.
# 1 dla starego zachowania.
setback-violation-threshold: 1
# Sprawdza, czy gracz wszedł w blok podczas ruchu.
Phase:
setbackvl: 1 # Wchodzenie w bloki może umożliwiać wspinanie się po ścianach, dodatkowo ta kontrola jest względnie stabilna.
decay: 0.005
AirLiquidPlace:
cancelvl: 0
FabricatedPlace:
cancelvl: 5
FarPlace:
cancelvl: 5
PositionPlace:
cancelvl: 5
RotationPlace:
cancelvl: 5
# Oparta na predykcji kontrola NoSlow.
# Grim uwzględnia tutaj problematyczny kod sieciowy... trudno o fałszywe alarmy nawet przy spamowaniu prawym przyciskiem i przyciskiem offhand.
# Znacznie stabilniejsza niż w innych antycheatach, ale prosimy zgłaszać wszelkie fałszywe alarmy... Naprawiłem tu mnóstwo problemów z kodem sieciowym.
NoSlow:
# Jakie odchylenie jest uznawane za "oszukiwanie"?
# Domyślnie jest niższe niż w innych kontrolach.
# Konsekwentnie wykrywa odchylenia o 0.03-0.2 przy włączonym NoSlow.
threshold: 0.001
# Szybkie cofnięcie przy pierwszym użyciu przedmiotu, aby usunąć przewagę dawaną przez NoSlow.
setbackvl: 5
# Spadek naruszeń, gdy gracz używa przedmiotu I jest przez niego spowalniany.
decay: 0.05
Knockback:
# O ile powinniśmy pomnożyć całkowitą przewagę, gdy gracz gra zgodnie z zasadami?
setback-decay-multiplier: 0.999
# Jak duże odchylenie od prędkości nadanej graczowi (velocity) powinno skutkować naruszeniem?
# Mierzone w blokach od możliwej prędkości.
threshold: 0.001
# Jak duże naruszenie w jednym ticku spowoduje natychmiastowe cofnięcie gracza?
# -1 aby wyłączyć
immediate-setback-threshold: 0.1
# Jak dużą całkowitą przewagę (we wszystkich tickach) trzeba osiągnąć, zanim zaczniemy cofać gracza?
# -1 aby wyłączyć
max-advantage: 1
# Ma to na celu zapobieżenie gromadzeniu zbyt wielu naruszeń, których gracz nie mógłby później wyzerować.
max-ceiling: 4
Explosion:
threshold: 0.001
setbackvl: 3
TimerA:
setbackvl: 10
# Liczba milisekund, które gracz może zgromadzić do późniejszego wykorzystania, gdy się opóźni.
# Może potencjalnie umożliwić obejścia fast use/fast heal/fast bow z wersji 1.8, jeśli ustawione zbyt wysoko. 120 ms wydaje się dobrym kompromisem.
drift: 120
# Ta kontrola ogranicza nadużywanie balansu z checku TimerA, zapobiegając zbytniemu opóźnieniu ruchu gracza względem czasu rzeczywistego.
TimerLimit:
# Ping, przy którym kontrola zacznie ograniczać balans timera, aby zapobiec nadużyciom.
# Może powodować pewne cofnięcia u uczciwych graczy, ale tylko jeśli mają ping powyżej tego progu.
# -1 aby wyłączyć
ping-abuse-limit-threshold: 1000
NegativeTimer:
# Liczba milisekund straconych podczas ruchu, zanim zaczniemy wykrywać naruszenia.
drift: 1200
# Ta sama metoda wykrywania co w TimerA, ale dla pojazdów.
VehicleTimer:
# Celuje w timer 1.005
setbackvl: 10
PacketOrderI:
# Włącz, jeśli gracze są oznaczani za używanie modów do animacji z 1.7.
exempt-placing-while-digging: false
Reach:
# O ile powinniśmy zwiększyć hitboxy? 0.0005 powinno wykrywać zasięg 3.0005+.
#
# Wymuszone zwiększenie o 0.03 występuje w wersjach 1.9-1.18.1 (z wyjątkiem 1.18.2) lub w niektórych kombinacjach klient/serwer
# ze względu na zmiany i ograniczenia protokołu. Ta kontrola jest najskuteczniejsza dla klientów 1.7/1.8 na serwerach 1.8.
threshold: 0.0005
# Czy powinniśmy anulować trafienia, które wiemy, że są niemożliwe?
# Trafienia w przedziale 3.00-3.03 mogą przejść, ale nadal będą wykrywane z powodu ograniczeń kolejności pakietów.
block-impossible-hits: true
# Spowoduje to wysłanie dodatkowego pakietu na końcu każdego ticka, aby zwiększyć prawdopodobieństwo wykrycia oszustw.
# Ta metoda wstrzykuje się w listę połączeń serwera, aby wysłać końcowy pakiet tuż przed wysłaniem (flushem) danych przez serwer.
# Włączenie tego zwiększy użycie pasma dla wszystkich graczy.
# Nie wpłynie to negatywnie na ogólną wydajność serwera.
# Włączenie tego pozwoli wykryć więcej oszukujących graczy.
# Pozostawienie tego wyłączonym nadal będzie wykrywać oszustów i nie spowoduje fałszywych alarmów.
# Jeśli nie jesteś serwerem skoncentrowanym na PvP w wersji 1.8, ten dodatkowy pakiet nie jest zalecany.
enable-post-packet: false
exploit:
# Można uzyskać duże prędkości podczas sprintu i skoku z elytrą. To ustawienie zapobiega temu exploitowi, gdy jest ustawione na false.
# Mojang zepsuł kod sieciowy, powodując, że start lotu elytrą jest obsługiwany przez klienta, a koniec przez serwer.
# Elytry mają poziome tarcie 0.99, więc ciągłe dodawanie poziomej prędkości 0.2 skutkuje bardzo dużymi prędkościami.
allow-sprint-jumping-when-using-elytra: true
# Ta opcja łagodzi efekt stawiania bloków na tzw. ghostblockach poprzez resynchronizację gracza, gdy to nastąpi.
allow-building-on-ghostblocks: true
# To ustawienie, zależne od powyższej wartości logicznej, określa dystans do sprawdzania ghostblocków.
# Jego prawidłowy zakres jest ograniczony od 2 do 4.
distance-to-check-if-ghostblocks: 2
# Włącz logowanie pluginów, które wstrzyknęły się do netty podczas dołączania gracza, w celu debugowania problemów z kompatybilnością.
debug-pipeline-on-join: false
# Włącza eksperymentalne checki.
experimental-checks: false
reset-item-usage-on-item-update: true
reset-item-usage-on-attack: true
reset-item-usage-on-slot-change: true
reset-item-usage-on-item-use: true
# Grim czasami anuluje nielegalne pakiety, np. przy użyciu timera. Po anulowaniu X pakietów na sekundę, kiedy powinniśmy
# po prostu wyrzucić gracza? Jest to wymagane, ponieważ niektóre limitery pakietów nie liczą pakietów anulowanych przez Grima.
packet-spam-threshold: 100
# Włącz to, aby wydrukować stack trace, gdy gracz zostanie wyrzucony z powodu przekroczenia packet-spam-threshold.
debug-packet-cancel: false
# Grim jest w stanie egzekwować, że gracz, który nie jest w stanie lotu, nie może mieć więcej niż X milisekund pingu.
# Wynika to z faktu, że Grim obecnie nie sprawdza latających graczy.
# Aby wyłączyć, użyj -1.
max-ping-out-of-flying: 1000
# Maksymalny ping, przy którym boost z fajerwerka jest usuwany graczowi.
# Zapobiega to sytuacji, w której gracze z wysokim opóźnieniem mogą używać jednego boostu z fajerwerka z elytrą w nieskończoność.
max-ping-firework-boost: 1000
history:
enabled: true
# Ile wpisów powinno być pokazywanych na każdej stronie w /grim history <player>
entries-per-page: 15
# Jaka powinna być wstawiona nazwa serwera dla komendy history? Jest to przydatne, jeśli używasz tej samej bazy danych dla wielu serwerów.
server-name: Prison
database:
# Użyj SQLITE dla lokalnego przechowywania, użyj MYSQL lub POSTGRESQL, jeśli masz zewnętrzną bazę danych. To jest aktualizowane tylko przy restarcie serwera.
type: SQLITE
# Szczegóły połączenia z bazą danych
host: localhost
port: 3306
database: grim
username: root
password: ""
config-version: 9
+1
View File
@@ -24,6 +24,7 @@ client-brand:
ignored-clients:
- "^vanilla$"
- "^fabric$"
- "^quilt$"
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
- "^Feather Fabric$"
- "^labymod$"
+1
View File
@@ -23,6 +23,7 @@ client-brand:
ignored-clients:
- "^vanilla$"
- "^fabric$"
- "^quilt$"
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
- "^Feather Fabric$"
- "^labymod$"
+1
View File
@@ -23,6 +23,7 @@ client-brand:
ignored-clients:
- "^vanilla$"
- "^fabric$"
- "^quilt$"
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
- "^Feather Fabric$"
- "^labymod$"
+1
View File
@@ -23,6 +23,7 @@ client-brand:
ignored-clients:
- "^vanilla$"
- "^fabric$"
- "^quilt$"
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
- "^Feather Fabric$"
- "^labymod$"
+71 -52
View File
@@ -2,7 +2,7 @@
# 源代码链接: https://github.com/Axionize/LightningGrim
# 版权所有 © 2025 DefineOutside 及贡献者 / 遵循 GPLv3 开源协议
# 修改过的二进制文件,或带有复制的Grim代码的插件,必须是私有仓库,不得公开售卖此插件。
# 中文翻译提供 Regotly / nuym / Dg32z_ , 如有错误请提交 Issues 并@我
# 中文翻译提供 Regotly / nuym / Dg32z_ / tjshtqwq , 如有错误请提交 Issues 并@我们
alerts:
# 除了向有权限的玩家发送作弊vl外,是否还在控制台中打印作弊vl消息?
@@ -24,6 +24,7 @@ client-brand:
ignored-clients:
- "^vanilla$"
- "^fabric$"
- "^quilt$"
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4},(?:fabric|forge)$"
- "^Feather Fabric$"
- "^labymod$"
@@ -40,7 +41,7 @@ spectators:
# 在玩家无法接收 LightningGrim的 transaction 数据包后,多少秒对玩家进行踢出
# 默认踢出原因为: disconnect.timeout
# 玩家超时踢出时间(默认 = 60 秒)
# 玩家超时踢出时间(单位:秒)(默认 = 60)
max-transaction-time: 60
# 启用此选项可阻止 LightningGrim 取消玩家发送的 pong 信息。
@@ -56,7 +57,7 @@ disable-pong-cancelling: false
disable-default-resync-handler: false
# 是否对重复的移动数据包进行撤销?
# 这可以修复 "bucket desync". https://bugs.mojang.com/browse/MC-12363,不过Mojang已经在1.21修复了这个问题
# 这可以修复 "bucket desync"(桶不同步问题). https://bugs.mojang.com/browse/MC-12363,不过Mojang已经在1.21修复了这个问题
# 这仅仅适用于1.8服务器中使用1.17-1.20.5版本的玩家
cancel-duplicate-packet: true
@@ -64,27 +65,33 @@ cancel-duplicate-packet: true
ignore-duplicate-packet-rotation: false
Simulation:
# 当玩家合法时,我们应该将总优势乘以多少
# 这个检查通过仿真玩家的移动,并与玩家的实际移动进行比较
# 仿真出的位置(玩家理应到达的位置)和玩家实际位置的偏差(距离)即为移动优势。
# 当玩家合法时,我们应该将总移动优势乘以多少
# 这是默认配置的样子(x 轴 = seconds ,y 轴 = 1/1000 方块): https://www.desmos.com/calculator/d4ufgxrxer
setback-decay-multiplier: 0.999
# 玩家移动偏移量判定阈值(单位:方块)
# 已针对 Optifine 优化三角函数表,若设为 0.001 可减少 FastMath 误报
# 如果此补偿不起作用,则反作弊记录此玩家作弊
# 最大允许玩家移动优势的阈值(单位:方块)
# 已经内置了Optifine FastMath的三角函数表,设置为0.001可避免FastMath误判
# 若玩家移动优势大于此阈值,标记玩家作弊
threshold: 0.001
# 在玩家受到回弹之前,1 ticks 的判定阈值
# 假设作弊者与模拟移动的阈值(threshold)是0.0945,该项目设置的是0.07,玩家会立即被回弹到合适的位置
# -1 关闭
# 若玩家的移动优势超过此值,玩家会被强制回弹
# 假设作弊者的移动优势为0.12,且设置的是0.1,0.12 >= 0.1,玩家会立即被回弹到本应到达的位置
# -1 关闭此项
immediate-setback-threshold: 0.1
# 在玩家受到回弹之前,累计回弹的阈值
# 假设作弊者与模拟移动的阈值(threshold)是0.0945,并且触发了10次,应为0.9450
# 而你设置的值为0.94 , 作弊者会受到回弹
# -1 关闭
# 若玩家的总移动优势超过此值,玩家会被强制回弹
# 假设作弊者1个游戏刻的移动优势为0.11,并且违规了10次(10个游戏刻),即0.11*10,为1.1
# 若设置的值为1,1.1 >= 1,那么玩家会立即被回弹到本应到达的位置
# -1 关闭此项
max-advantage: 1
# 在默认配置的50秒后,玩家将从4块跑到1块
# 这是为了防止玩家收集过多的违规行为,并且永远无法清除所有的违规行为
# 在默认配置的50秒后,玩家的总优势将从4个方块变为1个方块
# 这是默认配置的样子(x 轴 = seconds ,y 轴 = 1/1000 方块): https://www.desmos.com/calculator/4lovswdarj
# 若玩家的总移动优势大于此项,则会被重置为此项
# 例如:总移动优势为4.2,会被强制重置到4
# 这是为了防止玩家的总移动优势过大,并且永远无法清除总移动优势
max-ceiling: 4
# 设置回退的违规等级阈值
# 设置回弹的违规等级阈值
# 1 为旧行为
setback-violation-threshold: 1
@@ -112,8 +119,8 @@ RotationPlace:
# 在这里解释错误的服务器代码......即使在发送垃圾数据包时右键单击和副手按钮也很分辨
# 比其他反作弊更稳定,但请报告任何错误......我在这里修复了大量的代码问题。
NoSlow:
# 有多少偏移量是作弊
# 标志 0.03-0.2 与 NoSlow 一致
# 最大允许玩家在使用物品时的移动优势(单位:方块)
# 启用 NoSlow 后,标记量持续为 0.03-0.2
threshold: 0.001
# 到达多少VL时回弹
setbackvl: 5
@@ -123,28 +130,38 @@ NoSlow:
Knockback:
# 当玩家合法时,我们应该将总优势乘以多少
setback-decay-multiplier: 0.999
# 我们应该为玩家的移动创建多大的偏移量?
# 从可能的移动中以方块为单位测量
# 最大允许玩家的移动优势(单位:方块)
threshold: 0.001
# 在玩家受到回弹之前,1 ticks 的违规行为有多大?
# -1 关闭
# 若玩家的总移动优势超过此值,玩家会被强制回弹
# 假设作弊者1个游戏刻的移动优势为0.11,并且违规了10次(10个游戏刻),即0.11*10,为1.1
# 若设置的值为1,1.1 >= 1,那么玩家会立即被回弹到本应到达的位置
# -1 关闭此项
immediate-setback-threshold: 0.1
# 在我们开始遭遇回弹之前,我们的优势有多大?
# -1 关闭
# 若玩家的总移动优势超过此值,玩家会被强制回弹
# 假设作弊者1个游戏刻的移动优势为0.11,并且违规了10次(10个游戏刻),即0.11*10,为1.1
# 若设置的值为1,1.1 >= 1,那么玩家会立即被回弹到本应到达的位置
# -1 关闭此项
max-advantage: 1
# 这是为了防止玩家收集过多的违规行为,并且永远无法清除所有的违规行为
# 若玩家的总移动优势大于此项,则会被重置为此项
# 例如:总移动优势为4.2,会被强制重置到4
# 这是为了防止玩家的总移动优势过大,并且永远无法清除总移动优势
max-ceiling: 4
Explosion:
threshold: 0.001
# 若玩家的总移动优势超过此值,玩家会被强制回弹
# 假设作弊者1个游戏刻的移动优势为0.11,并且违规了10次(10个游戏刻),即0.11*10,为1.1
# 若设置的值为1,1.1 >= 1,那么玩家会立即被回弹到本应到达的位置
# -1 关闭此项
setbackvl: 3
# 到达多少VL时回弹
TimerA:
setbackvl: 10
# 玩家卡顿时可以累积以供以后使用的毫秒数
# 如果设置得太高,可能会允许 1.8 快速使用/快速治疗/快速弓箭绕过,120 毫秒似乎是一个很好的平衡
drift: 120
# 玩家卡顿时,可以累积以供以后使用的时间(单位:毫秒)
# 如果设置得太高,可能会允许 1.8版本的快速使用/快速回血/快速射弓绕过,120毫秒似乎是一个很好的平衡
# 此项是config是用来防止玩家移动与实时差距过大,来限制对 TimerA 平衡机制的滥用。
TimerLimit:
@@ -154,43 +171,45 @@ TimerLimit:
ping-abuse-limit-threshold: 1000
NegativeTimer:
# 在开始检查玩家时,玩家的移动丢失了多少毫秒数
# 玩家的移动丢失了多少毫秒?
drift: 1200
# 与 TimerA 相同的检查方法,但适用于坐骑
VehicleTimer:
# 实体1.0005 检查
# 实体的1.0005Timer 检查
setbackvl: 10
# VL达到多少时回弹?
PacketOrderI:
# 如果玩家因使用 1.7 动画模组而被标记,则启用此项
# 如果玩家因使用1.7动画模组而误判,则启用此项
exempt-placing-while-digging: false
Reach:
# 我们应该将碰撞箱扩大多少? 0.0005 应该检测到 3.0005+ reach
threshold: 0.0005
# 我们应该将实体碰撞箱扩大多少? 0.0005 应该检测到 3.0005+ reach
#
# 在 1.9-1.18.1(不是 1.18.2)或某些客户端/服务器组合中存在 0.03 距离的增加,因为
# 协议更改和限制。 对于 1.8 服务器上的 1.7/1.8 客户端,此检查功能最为强大。
threshold: 0.0005
# 我们应该取消我们知道不可能的命中吗?
# 3.00-3.03 命中可能会通过,但仍会被标记,因为数据包顺序限制
block-impossible-hits: true
# 这将在每个ticks结束时发送一个额外的数据包,以检查作弊的可能性
# 这会注入服务器的连接列表以在服务器刷新之前发送最终数据包
# 启用这将增加所有玩家的带宽使用
# 这不会降低整体服务器性能
# 启用此功能将捕获更多作弊者。
# 禁用此功能仍会捕获作弊者,不会导致误报
# 除非你是专注于 1.8 PvP 的服务器,否则不建议使用这个额外的数据包
# 我们应该取消不合法的攻击吗?
# 攻击距离为3.00-3.03可能不会被取消,但仍会被作弊标记,因为有数据包顺序限制
enable-post-packet: false
# 这将在每个游戏刻(tick)结束时发送一个额外的数据包,以检查更多的作弊
# 这会注入服务器的连接列表以在服务器刷新之前发送最终数据
# 启用这将增加所有玩家的带宽占用
# 但这不会降低整体服务器性能
# 启用此功能将检查到更多作弊者。
# 禁用此功能仍会检查到作弊者,不会导致误报
# 除非你是专注于1.8版本的PvP服务器,否则不建议使用这个额外的数据包
exploit:
#是否允许玩家在使用鞘翅时候进行的疾跑跳跃
allow-sprint-jumping-when-using-elytra: true
# 该选项可在发生鬼块时重新同步玩家,从而减轻玩家在鬼块上的位置。
# 这可以在玩家遇到幽灵方块时进行同步,这可以减少玩家所碰到幽灵方块
# 是否允许玩家在使用鞘翅时候进行的疾跑跳跃
allow-building-on-ghostblocks: true
# 该选项可在发生假方块时重新同步玩家方块,并允许玩家在假方块上继续放置方块
distance-to-check-if-ghostblocks: 2
# 检查假方块的距离。
# 范围限制为 2 到 4
# 启用在加入时注入 netty 的日志插件以调试兼容性问题
debug-pipeline-on-join: false
@@ -204,8 +223,9 @@ reset-item-usage-on-attack: true
reset-item-usage-on-slot-change: true
reset-item-usage-on-item-use: true
# Grim有时会取消非法的数据包,比如用timer,在一秒钟内取消了数个数据包后,我们应该踢掉这个玩家?
# 我们认为是应该的,因为有些数据包限制器并不计算被Grim取消的数据包。
# Grim有时会取消非法的数据包,比如Timer。
# 在一秒钟内取消了多少个数据包后,我们应该踢掉这个玩家?
# 我们认为你应该启用此项,因为有些数据包限制器并不记录被Grim取消的数据包。
packet-spam-threshold: 100
# 启用此项可在玩家因 packet-spam-threshold 被踢时打印堆栈追踪
debug-packet-cancel: false
@@ -214,8 +234,8 @@ debug-packet-cancel: false
# 填写-1则关闭
max-ping-out-of-flying: 1000
# 限制玩家在使用鞘翅飞行时使用烟花加速的延迟
# 这可以解决高延迟的玩家可以使用鞘翅时一直使用 1 个烟花就可以加速
# 当玩家使用鞘翅飞行,且ping(延迟)大于多少毫秒后,Grim会取消这个玩家正在使用的烟花火箭?
# 可以避免高延迟玩家使用一个烟花火箭就可以一直飞行
# 填写-1则关闭
max-ping-firework-boost: 1000
@@ -223,17 +243,16 @@ history:
enabled: true
# 使用 /grim history <player> 时每页显示多少条记录
entries-per-page: 15
# history 指令中插入的服务器名称?如果多个服务器共用同一数据库则很有用
# history命令中应该插入的服务器名称是什么?如果多个服务器共用同一数据库,这个选项会很有用
server-name: Prison
database:
# 本地存储使用SQLITE,如有外部数据库请使用MYSQL或POSTGRESQL。此设置仅在服务器重启时更新
type: SQLITE
# 数据库连接详情
# 数据库连接信息
host: localhost
port: 3306
database: grim
username: root
password: ""
# 不要调整这个
config-version: 9
+9 -2
View File
@@ -1,13 +1,20 @@
# Hilfe zu Webhooks: https://support.discord.com/hc/de/articles/228383668
enabled: false
webhook: ""
# Auf `true` setzen, um die Discord-URL-Formatprüfung zu umgehen.
# Dies erlaubt jede gültige HTTPS-URL (nützlich für Proxys oder Matrix-Forwarder).
disable-webhook-validation: false
embed-title: "**Grim Warnung**"
embed-color: "#00FFFF"
include-timestamp: true
# Ersetzt Backticks (`) in Werten (z. B. Client-Marken), um Fehler in der Discord-Textformatierung zu vermeiden.
# Auf "`" setzen, um diese Ersetzung zu deaktivieren.
backtick-replacement-char: "ˋ"
violation-content:
- "**Spieler**: %player%"
- "**Spieler**: `%player%`"
- "**Überprüfung**: %check%"
- "**Verstöße**: %violations%"
- "**Client-Version**: %version%"
- "**Marke**: %brand%"
- "**Marke**: `%brand%`"
- "**Ping**: %ping%"
- "**TPS**: %tps%"
+9 -2
View File
@@ -1,13 +1,20 @@
# For help with webhooks: https://support.discord.com/hc/en-us/articles/228383668
enabled: false
webhook: ""
# Set to true to bypass Discord's webhook URL format validation.
# This allows any valid HTTPS URL (useful for proxies or Matrix forwarders).
disable-webhook-validation: false
embed-title: "**Grim Alert**"
embed-color: "#00FFFF"
include-timestamp: true
# Replaces backticks in values (like client brands) so they don't break Discord's text formatting.
# Set to "`" to disable this replacement.
backtick-replacement-char: "ˋ" # Defaults to U+02CB
violation-content:
- "**Player**: %player%"
- "**Player**: `%player%`"
- "**Check**: %check%"
- "**Violations**: %violations%"
- "**Client Version**: %version%"
- "**Brand**: %brand%"
- "**Brand**: `%brand%`"
- "**Ping**: %ping%"
- "**TPS**: %tps%"
+11 -3
View File
@@ -7,19 +7,27 @@ enabled: false
# La URL del webhook.
webhook: ""
embed-title: "**Grim Alert**"
# Establécelo en `true` para omitir la validación de formato de URL de Discord.
# Esto permite cualquier URL HTTPS válida (útil para proxies o reenviadores de Matrix).
disable-webhook-validation: false
embed-title: "**Grim Alerta**"
# El color del embed que se mandara por este webhook.
embed-color: "#00FFFF"
include-timestamp: true
# Reemplaza las comillas invertidas (`) en los valores (como las marcas de clientes) para no romper el formato de texto de Discord.
# Establécelo en "`" para desactivar este reemplazo.
backtick-replacement-char: "ˋ"
# El contenido del embed que se mandara por este webhook.
violation-content:
- "**Jugador**: %player%"
- "**Jugador**: `%player%`"
- "**Comprobación**: %check%"
- "**Violaciones**: %violations%"
- "**Version del cliente**: %version%"
- "**Marca del cliente**: %brand%"
- "**Marca del cliente**: `%brand%`"
- "**Latencia**: %ping%"
- "**TPS**: %tps%"
+10 -3
View File
@@ -1,13 +1,20 @@
# Aide sur les webhooks : https://support.discord.com/hc/fr/articles/228383668
enabled: false
webhook: ""
embed-title: "**Grim Alert**"
# Définissez sur `true` pour contourner la validation du format d'URL de Discord.
# Cela autorise n'importe quelle URL HTTPS valide (utile pour les proxys ou redirecteurs Matrix).
disable-webhook-validation: false
embed-title: "**Grim Alerte**"
embed-color: "#00FFFF"
include-timestamp: true
# Remplace les accents graves (`) dans les valeurs (ex. marques de clients) pour éviter de casser le formatage du texte sur Discord.
# Définissez sur "`" pour désactiver ce remplacement.
backtick-replacement-char: "ˋ"
violation-content:
- "**Joueur**: %player%"
- "**Joueur**: `%player%`"
- "**Vérification**: %check%"
- "**Violations**: %violations%"
- "**Version du client**: %version%"
- "**Nature du client**: %brand%"
- "**Marque du client**: `%brand%`"
- "**Latence**: %ping%"
- "**TPS**: %tps%"
+12 -5
View File
@@ -1,13 +1,20 @@
# Guida sui webhook: https://support.discord.com/hc/it/articles/228383668
enabled: false
webhook: ""
embed-title: "**Grim Alert**"
# Imposta su `true` per ignorare la validazione del formato URL di Discord.
# Questo consente qualsiasi URL HTTPS valida (utile per proxy o forwarder Matrix).
disable-webhook-validation: false
embed-title: "**Grim Avviso**"
embed-color: "#00FFFF"
include-timestamp: true
# Sostituisce gli apici inversi (`) nei valori (come i brand dei client) per evitare di rompere la formattazione del testo di Discord.
# Imposta su "`" per disabilitare questa sostituzione.
backtick-replacement-char: "ˋ"
violation-content:
- "**Giocatore**: %player%"
- "**Cheats Rilevati**: %check%"
- "**Giocatore**: `%player%`"
- "**Controllo**: %check%"
- "**Violazioni**: %violations%"
- "**Versione Client**: %version%"
- "**Client**: %brand%"
- "**Versione del client**: %version%"
- "**Marca del client**: `%brand%`"
- "**Ping**: %ping%"
- "**TPS**: %tps%"
+9 -3
View File
@@ -3,14 +3,20 @@
# https://support.discord.com/hc/ja/articles/228383668-Webhooks-%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9
enabled: false
webhook: ""
embed-title: "**Grim アラート**"
# DiscordのURLフォーマット検証をバイパスするには `true` に設定します。
# これにより、任意の有効なHTTPS URLが許可されます(プロキシやMatrixフォワーダーに便利です)。
disable-webhook-validation: false
embed-title: "**Grim 警告**"
embed-color: "#00FFFF"
include-timestamp: true
# 値(クライアントのブランド名など)に含まれるバッククォート (`) を置換し、Discordのテキスト書式が崩れるのを防ぎます。
# この置換を無効にするには "`" を設定してください。
backtick-replacement-char: "ˋ"
violation-content:
- "**プレイヤー**: %player%"
- "**プレイヤー**: `%player%`"
- "**違反項目**: %check%"
- "**違反回数**: %violations%"
- "**クライアントバージョン**: %version%"
- "**ブランド**: %brand%"
- "**ブランド**: `%brand%`"
- "**Ping**: %ping%"
- "**TPS**: %tps%"
+11 -4
View File
@@ -1,13 +1,20 @@
# Hulp bij webhooks: https://support.discord.com/hc/nl/articles/228383668
enabled: false
webhook: ""
embed-title: "**Grim Alert**"
# Stel in op `true` om de URL-formaatcontrole van Discord te omzeilen.
# Dit staat elke geldige HTTPS-URL toe (handig voor proxy's of Matrix-forwarders).
disable-webhook-validation: false
embed-title: "**Grim Waarschuwing**"
embed-color: "#00FFFF"
include-timestamp: true
# Vervangt backticks (`) in waarden (zoals clientmerken) om te voorkomen dat de tekstopmaak van Discord breekt.
# Stel in op "`" om deze vervanging uit te schakelen.
backtick-replacement-char: "ˋ"
violation-content:
- "**Speler**: %player%"
- "**Controleer**: %check%"
- "**Speler**: `%player%`"
- "**Controle**: %check%"
- "**Overtredingen**: %violations%"
- "**Client-versie**: %version%"
- "**Client-merk**: %brand%"
- "**Client-merk**: `%brand%`"
- "**Ping**: %ping%"
- "**TPS**: %tps%"
+10 -3
View File
@@ -1,13 +1,20 @@
# Ajuda com webhooks: https://support.discord.com/hc/pt-br/articles/228383668
enabled: false
webhook: ""
embed-title: "**Grim Alert**"
# Defina como `true` para ignorar a validação de formato de URL do Discord.
# Isso permite qualquer URL HTTPS válida (útil para proxies ou encaminhadores do Matrix).
disable-webhook-validation: false
embed-title: "**Grim Alerta**"
embed-color: "#00FFFF"
include-timestamp: true
# Substitui as aspas invertidas (`) nos valores (como marcas de clientes) para evitar a quebra de formatação de texto do Discord.
# Defina como "`" para desativar esta substituição.
backtick-replacement-char: "ˋ"
violation-content:
- "**Jogador**: %player%"
- "**Jogador**: `%player%`"
- "**Verificação**: %check%"
- "**Violações**: %violations%"
- "**Versão do Cliente**: %version%"
- "**Nome do Cliente**: %brand%"
- "**Marca do Cliente**: `%brand%`"
- "**Ping**: %ping%"
- "**TPS**: %tps%"
+10 -4
View File
@@ -1,14 +1,20 @@
# Включать ли веб-крючок discord
# Справка по вебхукам: https://support.discord.com/hc/ru/articles/228383668
enabled: false
webhook: ""
embed-title: "**Grim Alert**"
# Установите значение `true`, чтобы обойти проверку формата URL Discord.
# Это разрешает любой действительный HTTPS URL (полезно для прокси или Matrix forwarder).
disable-webhook-validation: false
embed-title: "**Grim Оповещение**"
embed-color: "#00FFFF"
include-timestamp: true
# Заменяет обратные кавычки (`) в значениях (например, в названиях клиентов), чтобы не нарушать форматирование текста в Discord.
# Установите значение "`", чтобы отключить эту замену.
backtick-replacement-char: "ˋ"
violation-content:
- "**Игрок**: %player%"
- "**Игрок**: `%player%`"
- "**Проверка**: %check%"
- "**Нарушения**: %violations%"
- "**Версия Клиента**: %version%"
- "**Бренд**: %brand%"
- "**Бренд**: `%brand%`"
- "**Пинг**: %ping%"
- "**ТПС**: %tps%"
+10 -3
View File
@@ -1,13 +1,20 @@
# Webhook yardımı: https://support.discord.com/hc/tr/articles/228383668
enabled: false
webhook: ""
embed-title: "**Grim Alert**"
# Discord'un URL biçimi doğrulamasını atlamak için `true` olarak ayarlayın.
# Bu, geçerli herhangi bir HTTPS URL'sine izin verir (proxy'ler veya Matrix yönlendiricileri için kullanışlıdır).
disable-webhook-validation: false
embed-title: "**Grim Uyarı**"
embed-color: "#00FFFF"
include-timestamp: true
# Discord'un metin biçimlendirmesini bozmasını önlemek için değerlerdeki (istemci markaları gibi) ters tırnak işaretlerini (`) değiştirir.
# Bu değiştirmeyi devre dışı bırakmak için "`" olarak ayarlayın.
backtick-replacement-char: "ˋ"
violation-content:
- "**Oyuncu**: %player%"
- "**Oyuncu**: `%player%`"
- "**Kontrol**: %check%"
- "**İhlaller**: %violations%"
- "**Client Versiyonu**: %version%"
- "**Kullanılan Client**: %brand%"
- "**Client Markası**: `%brand%`"
- "**Gecikme**: %ping%"
- "**TPS**: %tps%"
+12 -6
View File
@@ -1,14 +1,20 @@
# 是否启用discord webhook
# Webhook帮助: https://support.discord.com/hc/zh-cn/articles/228383668
enabled: false
webhook: ""
embed-title: "**Grim Alert**"
# 设置为 `true` 以绕过 Discord 的 URL 格式验证。
# 这将允许任何有效的 HTTPS URL(适用于代理或 Matrix 转发器)。
disable-webhook-validation: false
embed-title: "**Grim 警报**"
embed-color: "#00FFFF"
include-timestamp: true
# 替换变量值(如客户端品牌)中的反引号 (`),以防止破坏 Discord 的文本格式。
# 将其设置为 "`" 即可禁用此替换。
backtick-replacement-char: "ˋ"
violation-content:
- "**玩家名**: %player%"
- "**玩家名**: `%player%`"
- "**检查**: %check%"
- "**违规率**: %violations%"
- "**违规次数**: %violations%"
- "**游戏版本**: %version%"
- "**客户端品牌**: %brand%"
- "**客户端品牌**: `%brand%`"
- "**延迟**: %ping%"
- "**服务器卡顿程度**: %tps%"
- "**TPS**: %tps%"
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff. Show more