Compare commits

...
Author SHA1 Message Date
Axionize 49742a9ddc Fix build increment conditional 2025-11-08 18:32:22 -05:00
Axionize 612e0f8f67 Fix permissions for increment build number creating tags/refs 2025-11-08 18:18:52 -05:00
Axionize 5d4af9365b Forget to make workflows run on lightning branch 2025-11-08 17:41:56 -05:00
Axionize 5489f118c1 Fix buildscripts again 2025-11-08 17:40:38 -05:00
Axionize ded5c0f320 Restructure to help upstream forks.
People have started building forks of LightningGrim...

I never intended for this but regardless to help them I'll stop rebasing and use merges only.
2025-11-08 17:37:59 -05:00
Axionize 238b77c002 Fix buildscripts 2025-11-08 17:18:09 -05:00
Axionize 71d93c97cf Minor build fixes after rebase on upstream 2025-11-08 17:12:49 -05:00
Axionize 43f4c7fcd7 README updates for LightningGrim 2025-11-08 17:12:49 -05:00
Axionize 03714c9b97 Fix compile issues from rebasing upstream 2025-11-08 17:12:49 -05:00
Axionize 004804c09e Potentially fix LightningGrim-specific vector culling Reach False? 2025-11-08 17:12:49 -05:00
Axionize 853d02b9c8 Temp stash 2025-11-08 17:12:49 -05:00
Axionize 2272d4df16 Optimize Bukkit PistonEvent() usage 2025-11-08 17:12:49 -05:00
Axionize fa2dc7b00b * Remove space in names of new checks:
* "Wall Hit" -> "WallHit"
    * "Entity Pierce" -> "EntityPierce"
* Fix modeling experience orb as unhittable entity
* Fix sending hitbox with /grim hitboxdebug in the hitboxdebug handler
* Update upstream to 35811fe551
2025-11-08 17:12:49 -05:00
Axionize c8347ed146 Heavy memory use optimization
- 2.2x reduction in queued task overhead
- reduction in size of queued single block changes
- up to 8x reduction in size of queued multi block changes
    - fixed blocks disappearing in B73 caused by incorrect shifting in new block udpate handler
2025-11-08 17:12:49 -05:00
Axionize c94260bea6 Buildscript Update & Dirty Painting Hack
- Update gradle buildscript to not include branch-name if branch is called "lightning"
 - Update GitHub Actions to automatically include changelog and to rename the jar LightningGrim-${platform}-${version}.jar
 - Mark Paintings as entities that cannot be hit to prevent EntityPierce (formally HitboxEntity) falses.
2025-11-08 17:12:48 -05:00
Axionize bde2328511 Update buildscript
Notes:
- Mark actions builds automatically as alpha on Modrinth
- Shorten Fabric version string to be same as Bukkit on Modrinth
2025-11-08 17:12:48 -05:00
Axionize b87eeaf490 Cull dead entities from canHit() 2025-11-08 17:12:48 -05:00
Axionize 3abc904590 Add support for Hitbox Debugging 2025-11-08 17:12:48 -05:00
Axionize 1b416fc841 - Re-add HitboxBlock and HitboxEntity (renamed to WallHit and EntityPierce)
- Reduce unnecessary reach vectors used for calculations in versions
2025-11-08 17:12:48 -05:00
Axionize 224f464eff Add Inventory checks 2025-11-08 17:12:48 -05:00
Axionize ac35735fff Runtime startup check of PE version 2025-11-08 17:12:48 -05:00
ManInMyVan def21633e2 disable ExploitB "invalid page characters" check 2025-11-03 19:54:02 -07:00
ItzBloxxy 918e6575e8 New translation ro.yml (#2285)
Config Romanian translation for grim ac.
2025-11-03 17:02:42 +00:00
SamB440 ea1f1615b6 Disable PacketOrderP, fixes #2327 2025-11-03 16:59:53 +00:00
SamB440 c323cf06ce Bump cloud to beta.13, fixes #2318 2025-11-03 16:30:53 +00:00
SamB440 0ba331780e Don't simulate movement in unloaded chunks?
Who knows what other problems this could cause...
2025-11-03 16:26:30 +00:00
ManInMyVan fc47c5b663 refactor via 1.11->1.10 chat hook 2025-11-01 17:10:11 -06:00
ManInMyVan 17cf067d58 fix #2330 2025-11-01 16:05:03 -06:00
ManInMyVan 6280b28e16 show size for applicable reach/hitbox flags 2025-11-01 12:23:17 -06:00
Axionize bfd2bd0e22 Reorder publication of artifacts to Modrinth and update supported versions 2025-10-31 22:29:16 -04:00
ManInMyVan 0df77dd32a slight cleanup of fabric code 2025-10-31 14:59:26 -06:00
ManInMyVan b373e5f1c4 switch to mojmap for fabric
see https://fabricmc.net/2025/10/31/obfuscation.html
2025-10-31 14:38:23 -06:00
Szypkoo 115c32916f fix: useless player fetch (#2329) 2025-10-31 11:03:27 +00:00
ManInMyVan 5f8692deda refactor plugin message wrappers 2025-10-30 17:32:01 -06:00
148 changed files with 2992 additions and 712 deletions

No files matched your search

+26 -17
View File
@@ -38,7 +38,8 @@ jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
# Grant permission to write contents (for creating tags/refs)
contents: write
packages: write
env:
@@ -85,7 +86,7 @@ jobs:
run: |
# If any command in a pipeline fails, treat the pipeline as failed.
set -o pipefail
BUILD_ARGS="build"
if [[ "${{ inputs.is-release-build }}" == "true" ]]; then
BUILD_ARGS="$BUILD_ARGS -Prelease=true"
@@ -94,12 +95,12 @@ jobs:
if [[ "${{ inputs.run-build-scan }}" == "true" ]]; then
BUILD_ARGS="$BUILD_ARGS --scan"
fi
echo "Running: ./gradlew printVersion $BUILD_ARGS"
# Use 'tee' to both display the output live and save it to a file for parsing
VERSION=$(./gradlew printVersion $BUILD_ARGS | tee build-output.log | grep '^VERSION=' | tail -n1 | cut -d'=' -f2)
echo "build_version=$VERSION" >> $GITHUB_OUTPUT
echo "BUILD_VERSION=$VERSION" >> $GITHUB_ENV
echo "Build version: $VERSION"
@@ -110,7 +111,7 @@ jobs:
run: |
# Search the log file for the build scan URL and extract it
SCAN_URL=$(grep -o 'https://gradle.com/s/[^ ]*' build-output.log | head -n 1 || true)
if [[ -n "$SCAN_URL" ]]; then
echo "Found build scan URL: $SCAN_URL"
# Post a highly visible link to the Job Summary page
@@ -165,17 +166,25 @@ jobs:
# Use the environment variable for the dev changelog.
NOTES="$DEV_NOTES"
fi
if [ -z "$NOTES" ]; then
NOTES="* No new code changes in this build."
fi
# This is the correct way to set a multi-line output variable in Actions
{
echo "notes<<EOF"
echo "$NOTES"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Generate build number
if: contains(fromJSON('["merge","release","main","lightning"]'), github.ref_name) && inputs.publish-artifacts == true && env.MODRINTH_ID != '' && env.MODRINTH_TOKEN != ''
id: buildnumber
uses: onyxmueller/build-tag-number@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
#------------------------------------------------------------------
# Publish to Modrinth & API
#------------------------------------------------------------------
@@ -188,17 +197,17 @@ jobs:
modrinth-featured: ${{ inputs.is-release-build }} # Only feature release builds
files: bukkit/build/libs/grimac-bukkit-${{ env.BUILD_VERSION }}.jar
name: Grim Anticheat (Bukkit) ${{ env.BUILD_VERSION }}
name: LightningGrim Anticheat (Bukkit) ${{ env.BUILD_VERSION }}-b${{ steps.buildnumber.outputs.build_number }}
version: ${{ env.BUILD_VERSION }}
version-type: ${{ inputs.release-version-type }}
changelog: ${{ steps.changelog.outputs.notes }}
loaders: |
bukkit
spigot
paper
folia
purpur
game-versions: '>=1.7 <=1.21.8'
bukkit
spigot
paper
folia
purpur
game-versions: '>=1.7 <=1.21.10'
- name: Publish Fabric to Modrinth
if: ${{ inputs.publish-artifacts == true && env.MODRINTH_ID != '' && env.MODRINTH_TOKEN != '' }}
@@ -209,13 +218,13 @@ jobs:
modrinth-featured: ${{ inputs.is-release-build }} # Only feature release builds
files: fabric/build/libs/grimac-fabric-${{ env.BUILD_VERSION }}.jar
name: Grim Anticheat (Fabric) ${{ env.BUILD_VERSION }}
name: LightningGrim Anticheat (Fabric) ${{ env.BUILD_VERSION }}-b${{ steps.buildnumber.outputs.build_number }}
version: ${{ env.BUILD_VERSION }}
version-type: ${{ inputs.release-version-type }}
changelog: ${{ steps.changelog.outputs.notes }}
loaders: |
fabric
game-versions: '>=1.16.1 <=1.21.8'
game-versions: '>=1.16.1 <=1.21.10'
dependencies: 'fabric-api'
- name: Publish to API
+50 -50
View File
@@ -12,64 +12,64 @@
name: "CodeQL"
on:
workflow_dispatch:
push:
branches: [ dev ]
workflow_dispatch:
push:
branches: [ dev ]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'java-kotlin' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
strategy:
fail-fast: false
matrix:
language: [ 'java-kotlin' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v5
steps:
- name: Checkout repository
uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
server-id: github # Value of the distributionManagement/repository/id field of the pom.xml
settings-path: ${{ github.workspace }} # location for the settings.xml file
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
server-id: github # Value of the distributionManagement/repository/id field of the pom.xml
settings-path: ${{ github.workspace }} # location for the settings.xml file
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
+1
View File
@@ -19,6 +19,7 @@ on:
- 'dev'
- 'release'
- 'release/**'
- 'lightning'
paths:
- '.github/workflows/**'
- 'buildSrc/**'
+2 -2
View File
@@ -65,8 +65,8 @@ information.
## Compiling From Source
1. `git clone https://github.com/GrimAnticheat/Grim.git`
2. `cd Grim`
1. `git clone https://github.com/Axionize/LightningGrim`
2. `cd LightningGrim`
3. `./gradlew build`
4. The final jars will compile into the `<platform>/build/libs` folders
@@ -91,6 +91,7 @@ object VersionUtil {
val mainBranch = System.getenv("GRIM_MAIN_BRANCH") ?: "2.0"
return when (branch) {
"lightning" -> null
"main", mainBranch -> null // ← ignore these branches
else -> branch
}
+1 -1
View File
@@ -195,7 +195,7 @@ publishing.publications.create<MavenPublication>("maven") {
tasks {
runServer {
minecraftVersion("1.21.4")
minecraftVersion("1.21.10")
}
shadowJar {
@@ -63,7 +63,6 @@ public class BukkitGrimEntity implements GrimEntity {
if (bukkitPlatformWorld == null || !bukkitPlatformWorld.getBukkitWorld().equals(entity.getWorld())) {
bukkitPlatformWorld = new BukkitPlatformWorld(entity.getWorld());
}
return bukkitPlatformWorld;
}
@@ -4,10 +4,8 @@ import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.platform.bukkit.utils.convert.BukkitConversionUtils;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
import ac.grim.grimac.utils.data.PistonData;
import ac.grim.grimac.utils.data.PistonTemplate;
import com.github.retrooper.packetevents.protocol.world.BlockFace;
import com.github.retrooper.packetevents.util.Vector3d;
import com.github.retrooper.packetevents.util.Vector3i;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.event.EventHandler;
@@ -28,10 +26,10 @@ public class PistonEvent implements Listener {
private static final double MAX_VERTICAL_DISTANCE = 64.0;
// accuracy isn't that important, it's close enough and performant
private static boolean isCloseEnough(Vector3i vectorA, Vector3d vectorB) {
return Math.abs(vectorA.getX() - vectorB.getX()) <= MAX_HORIZONTAL_DISTANCE
&& Math.abs(vectorA.getY() - vectorB.getY()) <= MAX_VERTICAL_DISTANCE
&& Math.abs(vectorA.getZ() - vectorB.getZ()) <= MAX_HORIZONTAL_DISTANCE;
private static boolean isCloseEnough(int x, int y, int z, double posX, double posY, double posZ) {
return Math.abs(x - posX) <= MAX_HORIZONTAL_DISTANCE
&& Math.abs(y - posY) <= MAX_VERTICAL_DISTANCE
&& Math.abs(z - posZ) <= MAX_HORIZONTAL_DISTANCE;
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
@@ -68,18 +66,8 @@ public class PistonEvent implements Listener {
piston.getY() + event.getDirection().getModY(),
piston.getZ() + event.getDirection().getModZ()));
final int chunkX = event.getBlock().getX() >> 4;
final int chunkZ = event.getBlock().getZ() >> 4;
final BlockFace blockFace = BukkitConversionUtils.fromBukkitFace(event.getDirection());
final Vector3i sourcePos = new Vector3i(piston.getX(), piston.getY(), piston.getZ());
for (GrimPlayer player : GrimAPI.INSTANCE.getPlayerDataManager().getEntries()) {
if (player.compensatedWorld.isChunkLoaded(chunkX, chunkZ) && isCloseEnough(sourcePos, player.compensatedEntities.self.trackedServerPosition.getPos())) {
final int lastTrans = player.lastTransactionSent.get();
PistonData data = new PistonData(blockFace, boxes, lastTrans, true, hasSlimeBlock, hasHoneyBlock);
player.latencyUtils.addRealTimeTaskAsync(lastTrans, () -> player.compensatedWorld.activePistons.add(data));
}
}
PistonTemplate data = new PistonTemplate(BukkitConversionUtils.fromBukkitFace(event.getDirection()), boxes, true, hasSlimeBlock, hasHoneyBlock);
addPistonData(data, piston.getX(), piston.getY(), piston.getZ());
}
// For some unknown reason, bukkit handles this stupidly
@@ -101,9 +89,8 @@ public class PistonEvent implements Listener {
BlockFace face = BukkitConversionUtils.fromBukkitFace(event.getDirection());
// The event was called without blocks and is therefore in the right direction
Block piston = event.getBlock();
if (event.getBlocks().isEmpty()) {
Block piston = event.getBlock();
// Add bounding box of the actual piston head pushing
boxes.add(new SimpleCollisionBox(0, 0, 0, 1, 1, 1, true)
.offset(piston.getX() + face.getModX(),
@@ -127,16 +114,19 @@ public class PistonEvent implements Listener {
}
}
final int chunkX = event.getBlock().getX() >> 4;
final int chunkZ = event.getBlock().getZ() >> 4;
Vector3i sourcePos = new Vector3i(event.getBlock().getX(), event.getBlock().getY(), event.getBlock().getZ());
PistonTemplate data = new PistonTemplate(face, boxes, false, hasSlimeBlock, hasHoneyBlock);
addPistonData(data, piston.getX(), piston.getY(), piston.getZ());
}
private void addPistonData(PistonTemplate pistonTemplate, int blockX, int blockY, int blockZ) {
for (GrimPlayer player : GrimAPI.INSTANCE.getPlayerDataManager().getEntries()) {
if (player.compensatedWorld.isChunkLoaded(chunkX, chunkZ) && isCloseEnough(sourcePos, player.compensatedEntities.self.trackedServerPosition.getPos())) {
int lastTrans = player.lastTransactionSent.get();
PistonData data = new PistonData(face, boxes, lastTrans, false, hasSlimeBlock, hasHoneyBlock);
player.latencyUtils.addRealTimeTaskAsync(lastTrans, () -> player.compensatedWorld.activePistons.add(data));
}
if (player.compensatedWorld.isChunkLoaded(blockX >> 4, blockZ >> 4) || !isCloseEnough(blockX, blockY, blockZ, player.x, player.y, player.z)) continue;
int lastTrans = player.lastTransactionSent.get();
player.latencyUtils.addRealTimeTaskAsync(lastTrans, () -> {
player.compensatedWorld.addPiston(pistonTemplate, lastTrans);
});
}
}
}
@@ -31,7 +31,6 @@ import lombok.Getter;
import org.incendo.cloud.CommandManager;
import org.jetbrains.annotations.NotNull;
@Getter
public final class GrimAPI {
public static final GrimAPI INSTANCE = new GrimAPI();
@@ -170,7 +170,8 @@ public class Check extends GrimProcessor implements AbstractCheck {
}
public static boolean isAsync(PacketTypeCommon packetType) {
return packetType == PacketType.Play.Client.KEEP_ALIVE;
return packetType == PacketType.Play.Client.KEEP_ALIVE
|| packetType == PacketType.Play.Client.CHUNK_BATCH_ACK;
}
public boolean isUpdate(PacketTypeCommon packetType) {
@@ -8,7 +8,7 @@ public abstract class AbstractDebugHandler extends Check {
super(player);
}
public abstract void toggleListener(GrimPlayer player);
public abstract boolean toggleListener(GrimPlayer player);
public abstract boolean toggleConsoleOutput();
}
@@ -0,0 +1,205 @@
package ac.grim.grimac.checks.debug;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.collisions.datatypes.CollisionBox;
import ac.grim.grimac.utils.collisions.datatypes.NoCollisionBox;
import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
import ac.grim.grimac.utils.data.Pair;
import ac.grim.grimac.utils.math.Vector3dm;
import com.github.retrooper.packetevents.netty.buffer.ByteBufHelper;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPluginMessage;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* Handles debug visualization of hitboxes and reach calculations for GrimAC.
* Sends debug data to clients through plugin messages that can be visualized by compatible clients.
*/
public class HitboxDebugHandler extends AbstractDebugHandler {
/**
* Set of players currently listening to hitbox debug data
*/
private final Set<GrimPlayer> listeners = new CopyOnWriteArraySet<>(new HashSet<>());
/**
* Creates a new HitboxDebugHandler for the specified player
*
* @param grimPlayer The GrimAC player instance to debug
*/
public HitboxDebugHandler(GrimPlayer grimPlayer) {
super(grimPlayer);
}
/**
* Toggles whether a player receives hitbox debug visualization data.
* If the player is already listening, they will be removed. If not, they will be added.
*
* @param player The player to toggle debug visualization for
* @return {@code true} if the player is now listening (was added),
* {@code false} if the player is no longer listening (was removed).
*/
@Override
public boolean toggleListener(GrimPlayer player) {
boolean wasPresent = listeners.remove(player);
if (wasPresent) {
return false;
} else {
listeners.add(player);
return true;
}
}
@Override
public boolean toggleConsoleOutput() {
throw new UnsupportedOperationException();
}
/**
* Sends debug visualization data for reach calculations to all listening players.
* The data includes hitboxes, target entities, look vectors, and eye heights used in reach calculations.
*
* @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 isPrediction Whether these hitboxes are from a prediction calculation
*
* Packet Format (Version 1):
* - Byte: Version (0)
* - Byte: Global flags
* - Bit 0: isPrediction
* - Bits 1-7: Reserved
* - Double: Player reach/interact distance
* - Vector3d: Base position (3 doubles)
* - VarInt: Number of ray traces
* - For each ray trace:
* - Double: Eye height delta
* - Vector3d: Look vector (3 doubles)
* - VarInt: Number of hitboxes
* - For each hitbox:
* - VarInt: Entity ID
* - Byte: Flags
* - Bit 0: Is target entity
* - Bit 1: Is no collision
* - Bits 2-7: Reserved
* - If not NoCollisionBox:
* - Double: minX
* - Double: minY
* - Double: minZ
* - Double: maxX
* - Double: maxY
* - Double: maxZ
*/
public void sendHitboxData(Map<Integer, CollisionBox> hitboxes, Set<Integer> targetEntities,
List<Pair<Vector3dm, Double>> lookVecsAndEyeHeights, Vector3dm basePos,
boolean isPrediction, double reachDistance) {
if (!isEnabled()) return;
ByteBuf buffer = Unpooled.buffer();
try {
// Version Header
buffer.writeByte(1);
// Global Flags Header
// Pack boolean flags into a single byte
byte global_flags = 0;
global_flags |= (isPrediction ? 1 : 0); // Bit 0: are the hitboxes from a prediction?
// Bits 2-7 reserved for future use
buffer.writeByte(global_flags);
// Write reach distance
buffer.writeDouble(reachDistance);
// Write base position
writeVector(buffer, basePos);
// Write number of ray traces
ByteBufHelper.writeVarInt(buffer, lookVecsAndEyeHeights.size());
// Write all possible ray traces
for (Pair<Vector3dm, Double> pair : lookVecsAndEyeHeights) {
Vector3dm lookVec = pair.first();
double eyeHeight = pair.second();
// Write eye height delta and look vector
// 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);
}
// Write number of hitboxes
ByteBufHelper.writeVarInt(buffer, hitboxes.size());
// Write hitbox data
for (Map.Entry<Integer, CollisionBox> entry : hitboxes.entrySet()) {
int entityId = entry.getKey();
CollisionBox box = entry.getValue();
// Write entity ID
ByteBufHelper.writeVarInt(buffer, entityId);
// Pack boolean flags into a single byte
byte flags = 0;
flags |= (targetEntities.contains(entityId) ? 1 : 0); // Bit 0: Is target
flags |= (box == NoCollisionBox.INSTANCE ? 2 : 0); // Bit 1: Is no collision
// Bits 2-7 reserved for future use
buffer.writeByte(flags);
// Write box coordinates if it's not a NoCollisionBox
if ((flags & 2) == 0) {
SimpleCollisionBox simpleCollisionBox = (SimpleCollisionBox) box;
buffer.writeDouble(simpleCollisionBox.minX);
buffer.writeDouble(simpleCollisionBox.minY);
buffer.writeDouble(simpleCollisionBox.minZ);
buffer.writeDouble(simpleCollisionBox.maxX);
buffer.writeDouble(simpleCollisionBox.maxY);
buffer.writeDouble(simpleCollisionBox.maxZ);
}
}
// Convert buffer to byte array
byte[] data = new byte[buffer.readableBytes()];
buffer.readBytes(data);
// Create and send packet
WrapperPlayServerPluginMessage packet = new WrapperPlayServerPluginMessage(
"grim:debug_hitbox",
data
);
// Send to all listeners
for (GrimPlayer listener : listeners) {
if (listener != null) {
listener.user.sendPacket(packet);
}
}
} finally {
// Release buffer to prevent memory leaks
buffer.release();
}
}
public boolean isEnabled() {
return !listeners.isEmpty();
}
/**
* Helper method to write a Vector to the ByteBuf
* @param buffer The buffer to write to
* @param vector The vector to write
*/
private void writeVector(ByteBuf buffer, Vector3dm vector) {
buffer.writeDouble(vector.getX());
buffer.writeDouble(vector.getY());
buffer.writeDouble(vector.getZ());
}
}
@@ -7,7 +7,7 @@ import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.update.BlockBreak;
import ac.grim.grimac.utils.math.GrimMath;
import ac.grim.grimac.utils.nmsutil.BlockBreakSpeed;
import ac.grim.grimac.utils.reflection.ViaVersionUtil;
import ac.grim.grimac.utils.viaversion.ViaVersionUtil;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
@@ -4,8 +4,6 @@ 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 ac.grim.grimac.utils.reflection.ViaVersionUtil;
import ac.grim.grimac.utils.via.ViaHooks;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
@@ -63,10 +61,4 @@ public class ChatB extends Check implements PacketCheck {
}
return false;
}
static {
if (ViaVersionUtil.isAvailable && PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_11)) {
ViaHooks.injectVia();
}
}
}
@@ -0,0 +1,13 @@
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;
@CheckData(name = "EntityPierce", configName = "EntityPierce", setback = 30)
public class EntityPierce extends Check implements PacketCheck {
public EntityPierce(GrimPlayer player) {
super(player);
}
}
@@ -18,13 +18,22 @@ package ac.grim.grimac.checks.impl.combat;
import ac.grim.grimac.api.config.ConfigManager;
import ac.grim.grimac.checks.Check;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.debug.HitboxDebugHandler;
import ac.grim.grimac.checks.type.PacketCheck;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.collisions.datatypes.CollisionBox;
import ac.grim.grimac.utils.collisions.datatypes.NoCollisionBox;
import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
import ac.grim.grimac.utils.data.BlockHitData;
import ac.grim.grimac.utils.data.EntityHitData;
import ac.grim.grimac.utils.data.HitData;
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.Vector3dm;
import ac.grim.grimac.utils.nmsutil.ReachUtils;
import ac.grim.grimac.utils.nmsutil.WorldRayTrace;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.attribute.Attributes;
import com.github.retrooper.packetevents.protocol.entity.type.EntityType;
@@ -32,16 +41,17 @@ 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.protocol.player.GameMode;
import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState;
import com.github.retrooper.packetevents.util.Vector3d;
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 it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.*;
// You may not copy the check unless you are licensed under GPL
@CheckData(name = "Reach", setback = 10)
@@ -55,8 +65,14 @@ public class Reach extends Check implements PacketCheck {
// Only one flag per reach attack, per entity, per tick.
// We store position because lastX isn't reliable on teleports.
private final Int2ObjectMap<Vector3d> playerAttackQueue = new Int2ObjectOpenHashMap<>();
// temporarily used to prevent falses in the wall hit check
private final Set<Vector3i> blocksChangedThisTick = new HashSet<>();
// extra distance to raytrace beyond player reach distance so we know how far beyond the legit distance a cheater hit
public static final double extraSearchDistance = 3;
private boolean ignoreNonPlayerTargets;
private boolean cancelImpossibleHits;
private double threshold;
public double threshold;
private double cancelBuffer; // For the next 4 hits after using reach, we aggressively cancel reach
public Reach(GrimPlayer player) {
@@ -88,6 +104,10 @@ public class Reach extends Check implements PacketCheck {
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;
@@ -114,8 +134,9 @@ public class Reach extends Check implements PacketCheck {
}
// If the player set their look, or we know they have a new tick
final boolean isFlying = WrapperPlayClientPlayerFlying.isFlying(event.getPacketType());
if (isUpdate(event.getPacketType())) {
tickBetterReachCheckWithAngle();
tickBetterReachCheckWithAngle(isFlying);
}
}
@@ -149,7 +170,7 @@ public class Reach extends Check implements PacketCheck {
}
}
private void tickBetterReachCheckWithAngle() {
private void tickBetterReachCheckWithAngle(boolean isFlying) {
for (Int2ObjectMap.Entry<Vector3d> attack : playerAttackQueue.int2ObjectEntrySet()) {
PacketEntity reachEntity = player.compensatedEntities.entityMap.get(attack.getIntKey());
if (reachEntity == null) continue;
@@ -158,16 +179,33 @@ public class Reach extends Check implements PacketCheck {
switch (result.type()) {
case REACH -> {
String added = ", type=" + reachEntity.type.getName().getKey();
if (reachEntity instanceof PacketEntitySizeable sizeable) {
added += ", size=" + sizeable.size;
}
flagAndAlert(result.verbose() + added);
}
case HITBOX -> {
String added = "type=" + reachEntity.type.getName().getKey();
if (reachEntity instanceof PacketEntitySizeable sizeable) {
added += ", size=" + sizeable.size;
}
player.checkManager.getCheck(Hitboxes.class).flagAndAlert(result.verbose() + added);
}
case WALL_HIT -> {
String added = reachEntity.type == EntityTypes.PLAYER ? "" : "type=" + reachEntity.type.getName().getKey();
player.checkManager.getCheck(WallHit.class).flagAndAlert(result.verbose() + added);
}
case ENTITY_PIERCE -> {
String added = reachEntity.type == EntityTypes.PLAYER ? "" : "type=" + reachEntity.type.getName().getKey();
player.checkManager.getCheck(EntityPierce.class).flagAndAlert(result.verbose() + added);
}
}
}
playerAttackQueue.clear();
// We can't use transactions for this because of this problem:
// transaction -> block changed applied -> 2nd transaction -> list cleared -> attack packet -> flying -> reach block hit checked, falses
if (isFlying) blocksChangedThisTick.clear();
}
@NotNull
@@ -196,28 +234,15 @@ public class Reach extends Check implements PacketCheck {
double minDistance = Double.MAX_VALUE;
// https://bugs.mojang.com/browse/MC-67665
List<Vector3dm> possibleLookDirs = new ArrayList<>(Collections.singletonList(ReachUtils.getLook(player, player.yaw, player.pitch)));
// If we are a tick behind, we don't know their next look so don't bother doing this
if (!isPrediction) {
possibleLookDirs.add(ReachUtils.getLook(player, player.lastYaw, player.pitch));
// 1.9+ players could be a tick behind because we don't get skipped ticks
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9)) {
possibleLookDirs.add(ReachUtils.getLook(player, player.lastYaw, player.lastPitch));
}
// 1.7 players do not have any of these issues! They are always on the latest look vector
if (player.getClientVersion().isOlderThan(ClientVersion.V_1_8)) {
possibleLookDirs = Collections.singletonList(ReachUtils.getLook(player, player.yaw, player.pitch));
}
}
// will store all lookVecsAndEyeHeight pairs that landed a hit on the target entity
// We only need to check for blocking intersections for these
List<Pair<Vector3dm, Double>> lookVecsAndEyeHeights = new ArrayList<>();
final double maxReach = player.compensatedEntities.self.getAttributeValue(Attributes.ENTITY_INTERACTION_RANGE);
// +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());
for (Vector3dm lookVec : possibleLookDirs) {
for (double eye : possibleEyeHeights) {
@@ -233,13 +258,44 @@ public class Reach extends Check implements PacketCheck {
if (intercept != null) {
minDistance = Math.min(eyePos.distance(intercept), minDistance);
lookVecsAndEyeHeights.add(new Pair<>(lookVec, eye));
}
}
}
if (hitboxDebuggingEnabled())
sendHitboxDebugData(reachEntity, from, 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)
// Ignore when could be hitting through a moving shulker, piston blocks. They are just too glitchy/uncertain to check.
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);
HitData hitData = hitResult.second();
// If the returned hit result was NOT the target entity we flag the check
if (hitData instanceof EntityHitData &&
player.compensatedEntities.getPacketEntityID(((EntityHitData) hitData).getEntity()) != player.compensatedEntities.getPacketEntityID(reachEntity)) {
minDistance = Double.MIN_VALUE;
foundHitData = hitData;
// until I fix block modeling exempt any blocks changed this tick
} else if (hitData instanceof BlockHitData && !blocksChangedThisTick.contains(((BlockHitData) hitData).position())) {
minDistance = Double.MIN_VALUE;
foundHitData = hitData;
}
}
// if the entity is not exempt and the entity is alive
if ((!blacklisted.contains(reachEntity.type) && reachEntity.isLivingEntity) || reachEntity.type == EntityTypes.END_CRYSTAL) {
if (minDistance == Double.MAX_VALUE) {
if (minDistance == Double.MIN_VALUE && foundHitData != null) {
cancelBuffer = 1;
if (foundHitData instanceof BlockHitData) {
return new CheckResult(ResultType.WALL_HIT, "Hit block=" + ((BlockHitData) foundHitData).state().getType().getName() + " ");
} else { // entity hit data
return new CheckResult(ResultType.ENTITY_PIERCE, "Hit entity=" + ((EntityHitData) foundHitData).getEntity().type.getName() + " ");
}
} else if (minDistance == Double.MAX_VALUE) {
cancelBuffer = 1;
return new CheckResult(ResultType.HITBOX, "");
} else if (minDistance > maxReach) {
@@ -255,12 +311,13 @@ public class Reach extends Check implements PacketCheck {
@Override
public void onReload(ConfigManager config) {
this.ignoreNonPlayerTargets = config.getBooleanElse("Reach.ignore-non-player-targets", false);
this.cancelImpossibleHits = config.getBooleanElse("Reach.block-impossible-hits", true);
this.threshold = config.getDoubleElse("Reach.threshold", 0.0005);
}
private enum ResultType {
REACH, HITBOX, NONE
REACH, HITBOX, WALL_HIT, ENTITY_PIERCE, NONE
}
private record CheckResult(ResultType type, String verbose) {
@@ -268,4 +325,68 @@ public class Reach extends Check implements PacketCheck {
return type != ResultType.NONE;
}
}
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;
// Only do this if the state really had any world impact
if (state.equals(player.compensatedWorld.getBlock(vector3i))) return;
blocksChangedThisTick.add(vector3i);
}
private boolean hitboxDebuggingEnabled() {
return player.checkManager.getCheck(HitboxDebugHandler.class).isEnabled();
}
private void sendHitboxDebugData(PacketEntity reachEntity, Vector3d from, 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();
if (!entity.canHit()) continue;
CollisionBox box;
if (entity.equals(reachEntity)) {
// Target entity gets expanded hitbox
box = entity.getPossibleCollisionBoxes();
SimpleCollisionBox sBox = (SimpleCollisionBox) box;
sBox.expand(threshold);
// Add movement threshold uncertainty for 1.9+ or non-position updates
if (!player.packetStateData.didLastLastMovementIncludePosition
|| player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9)) {
sBox.expand(player.getMovementThreshold());
}
} else {
// Non-target entities
box = entity.getMinimumPossibleCollisionBoxes();
if (box instanceof NoCollisionBox) {
hitboxes.put(entry.getIntKey(), NoCollisionBox.INSTANCE);
continue;
} else if (box instanceof SimpleCollisionBox sBox) {
sBox.expand(-threshold);
// Shrink non-target entities by movement threshold when applicable
if (!player.packetStateData.didLastLastMovementIncludePosition
|| player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9)) {
sBox.expand(-player.getMovementThreshold());
}
}
}
// Add 1.8 and below extra hitbox size
if (player.getClientVersion().isOlderThan(ClientVersion.V_1_9)
&& box instanceof SimpleCollisionBox) {
((SimpleCollisionBox) box).expand(0.1f);
}
hitboxes.put(entry.getIntKey(), box);
}
player.checkManager.getCheck(HitboxDebugHandler.class).sendHitboxData(hitboxes,
Collections.singleton(player.compensatedEntities.getPacketEntityID(reachEntity)),
lookVecsAndEyeHeights,
new Vector3dm(from.getX(), from.getY(), from.getZ()),
isPrediction, player.compensatedEntities.self.getAttributeValue(Attributes.ENTITY_INTERACTION_RANGE));
}
}
@@ -0,0 +1,13 @@
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;
@CheckData(name = "WallHit", configName = "WallHit", setback = 20)
public class WallHit extends Check implements PacketCheck {
public WallHit(GrimPlayer player) {
super(player);
}
}
@@ -29,7 +29,7 @@ public class ExploitA extends Check implements PacketCheck {
if (event.getPacketType() == PacketType.Play.Client.PLUGIN_MESSAGE && PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_13)) {
WrapperPlayClientPluginMessage wrapper = new WrapperPlayClientPluginMessage(event);
if (wrapper.getChannelName().equals("MC|ItemName")) {
check(PayloadItemName.CODEC.read(wrapper.getData()).itemName(), event);
check(new PayloadItemName(wrapper.getData()).itemName(), event);
}
}
}
@@ -72,7 +72,7 @@ public class ExploitB extends Check implements PacketCheck {
}
}
ItemStack newBook = PayloadBookEdit.CODEC.read(wrapper.getData()).itemStack();
ItemStack newBook = new PayloadBookEdit(wrapper.getData()).itemStack();
checkItemStack(newBook, signing, event);
oldBook = null;
}
@@ -287,13 +287,14 @@ public class ExploitB extends Check implements PacketCheck {
return true;
}
if (containsInvalidCharacters(page, true)) {
if (flagAndAlert("invalid page characters") && shouldModifyPackets()) {
event.setCancelled(true);
player.onPacketCancel();
}
return true;
}
// commented out because existing characters are not considered
// if (containsInvalidCharacters(page, true)) {
// if (flagAndAlert("invalid page characters") && shouldModifyPackets()) {
// event.setCancelled(true);
// player.onPacketCancel();
// }
// return true;
// }
return false;
}
@@ -0,0 +1,42 @@
package ac.grim.grimac.checks.impl.inventory;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.InventoryCheck;
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.WrapperPlayClientInteractEntity.InteractAction;
@CheckData(name = "InventoryA", setback = 3, description = "Attacked an entity while inventory is open")
public class InventoryA extends InventoryCheck {
public InventoryA(GrimPlayer player) {
super(player);
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
super.onPacketReceive(event);
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) {
WrapperPlayClientInteractEntity wrapper = new WrapperPlayClientInteractEntity(event);
if (wrapper.getAction() != InteractAction.ATTACK) return;
// Is not possible to attack while the inventory is open.
if (player.hasInventoryOpen) {
if (flagAndAlert()) {
// Cancel the packet
if (shouldModifyPackets()) {
event.setCancelled(true);
player.onPacketCancel();
}
if (!isNoSetbackPermission())
closeInventory();
}
} else {
reward();
}
}
}
}
@@ -0,0 +1,35 @@
package ac.grim.grimac.checks.impl.inventory;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.InventoryCheck;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.player.DiggingAction;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerDigging;
@CheckData(name = "InventoryB", setback = 3, description = "Started digging blocks while inventory is open")
public class InventoryB extends InventoryCheck {
public InventoryB(GrimPlayer player) {
super(player);
}
public void handle(PacketReceiveEvent event, WrapperPlayClientPlayerDigging wrapper) {
if (wrapper.getAction() != DiggingAction.START_DIGGING) return;
// Is not possible to start digging a block while the inventory is open.
if (player.hasInventoryOpen) {
if (flagAndAlert()) {
// Cancel the packet
if (shouldModifyPackets()) {
event.setCancelled(true);
player.onPacketCancel();
}
if (!isNoSetbackPermission()) {
closeInventory();
}
}
} else {
reward();
}
}
}
@@ -0,0 +1,30 @@
package ac.grim.grimac.checks.impl.inventory;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.InventoryCheck;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.update.BlockPlace;
@CheckData(name = "InventoryC", setback = 3, description = "Placed a block while inventory is open")
public class InventoryC extends InventoryCheck {
public InventoryC(GrimPlayer player) {
super(player);
}
public void onBlockPlace(final BlockPlace place) {
// It is not possible to place a block while the inventory is open
if (player.hasInventoryOpen) {
if (flagAndAlert()) {
if (shouldModifyPackets()) {
place.resync();
}
if (!isNoSetbackPermission()) {
closeInventory();
}
}
} else {
reward();
}
}
}
@@ -0,0 +1,85 @@
package ac.grim.grimac.checks.impl.inventory;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.InventoryCheck;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.update.PredictionComplete;
import ac.grim.grimac.utils.data.VectorData;
import ac.grim.grimac.utils.data.VectorData.MoveVectorData;
import ac.grim.grimac.utils.data.VehicleData;
import java.util.StringJoiner;
@CheckData(name = "InventoryD", setback = 1, decay = 0.25)
public class InventoryD extends InventoryCheck {
private int horseJumpVerbose;
public InventoryD(GrimPlayer player) {
super(player);
}
@Override
public void onPredictionComplete(final PredictionComplete predictionComplete) {
if (!predictionComplete.isChecked() ||
predictionComplete.getData().isTeleport() ||
player.getSetbackTeleportUtil().blockOffsets ||
player.packetStateData.lastPacketWasTeleport ||
player.packetStateData.isSlowedByUsingItem() ||
System.currentTimeMillis() - player.lastBlockPlaceUseItem < 50L) {
return;
}
if (player.hasInventoryOpen) {
boolean inVehicle = player.inVehicle();
boolean isJumping, isMoving;
if (inVehicle) {
VehicleData vehicle = player.vehicleData;
// Will flag once if player open anything with pressed space bar
isJumping = vehicle.nextHorseJump > 0 && horseJumpVerbose++ >= 1;
isMoving = vehicle.nextVehicleForward != 0 || vehicle.nextVehicleHorizontal != 0;
} else {
MoveVectorData move = findMovement(player.predictedVelocity);
isJumping = player.predictedVelocity.isJump();
isMoving = move != null && (move.x != 0 || move.z != 0);
}
if (!isMoving && !isJumping) {
reward();
return;
}
if (flag()) {
if (!isNoSetbackPermission())
closeInventory();
StringJoiner joiner = new StringJoiner(" ");
if (isMoving) joiner.add("moving");
if (isJumping) joiner.add("jumping");
if (inVehicle) joiner.add("inVehicle");
alert(joiner.toString());
}
} else {
horseJumpVerbose = 0;
}
}
private MoveVectorData findMovement(VectorData vectorData) {
if (vectorData instanceof MoveVectorData) {
return (MoveVectorData) vectorData;
}
while (vectorData != null) {
vectorData = vectorData.lastVector;
if (vectorData instanceof MoveVectorData) {
return (MoveVectorData) vectorData;
}
}
return null;
}
}
@@ -0,0 +1,50 @@
package ac.grim.grimac.checks.impl.inventory;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.InventoryCheck;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
@CheckData(name = "InventoryE", setback = 3, description = "Sent a held item change packet while inventory is open")
public class InventoryE extends InventoryCheck {
private long lastTransaction = Long.MAX_VALUE; // Impossible transaction ID
public InventoryE(GrimPlayer player) {
super(player);
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
super.onPacketReceive(event);
if (event.getPacketType() == PacketType.Play.Client.HELD_ITEM_CHANGE) {
// It is not possible to change hotbar slots with held item change while the inventory is open
// A container click packet would be sent instead
if (player.hasInventoryOpen) {
if (this.lastTransaction < player.lastTransactionReceived.get()
&& flagAndAlert()) {
// Cancel the packet
if (shouldModifyPackets()) {
event.setCancelled(true);
player.onPacketCancel();
player.inventory.needResend = true;
}
if (!isNoSetbackPermission()) {
closeInventory();
}
}
} else {
reward();
}
}
}
@Override
public void onPacketSend(PacketSendEvent event) {
if (event.getPacketType() == PacketType.Play.Server.HELD_ITEM_CHANGE) {
this.lastTransaction = player.lastTransactionSent.get();
}
}
}
@@ -0,0 +1,46 @@
package ac.grim.grimac.checks.impl.inventory;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.InventoryCheck;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.inventory.InventoryDesyncStatus;
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.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
@CheckData(name = "InventoryF", setback = 3, description = "Sent a click window packet without a open inventory", experimental = true)
public class InventoryF extends InventoryCheck {
public InventoryF(GrimPlayer player) {
super(player);
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
// Exempt on 1.9+ server version due to the Via hack done in PacketPlayerWindow, the exemption can be deleted
// once we are ahead of ViaVersion
if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_9)
|| player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9)) return;
super.onPacketReceive(event);
if (event.getPacketType() == PacketType.Play.Client.CLICK_WINDOW) {
if (!player.hasInventoryOpen && player.inventoryDesyncStatus == InventoryDesyncStatus.NOT_DESYNCED) {
if (flagAndAlert()) {
// Cancel the packet
if (shouldModifyPackets()) {
event.setCancelled(true);
player.onPacketCancel();
}
if (!isNoSetbackPermission()) {
closeInventory();
}
}
} else {
reward();
}
}
}
}
@@ -0,0 +1,40 @@
package ac.grim.grimac.checks.impl.inventory;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.InventoryCheck;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientEntityAction;
@CheckData(name = "InventoryG", setback = 3, description = "Sent a entity action packet while inventory is open", experimental = true)
public class InventoryG extends InventoryCheck {
public InventoryG(GrimPlayer player) {
super(player);
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (player.packetStateData.lastPacketWasTeleport) return;
super.onPacketReceive(event);
if (event.getPacketType() == PacketType.Play.Client.ENTITY_ACTION) {
WrapperPlayClientEntityAction wrapper = new WrapperPlayClientEntityAction(event);
WrapperPlayClientEntityAction.Action action = wrapper.getAction();
if (action == WrapperPlayClientEntityAction.Action.STOP_SNEAKING
|| action == WrapperPlayClientEntityAction.Action.STOP_SPRINTING) {
return;
}
if (player.hasInventoryOpen) {
if (flagAndAlert() && !isNoSetbackPermission()) {
closeInventory();
}
} else {
reward();
}
}
}
}
@@ -133,8 +133,14 @@ public class DebugHandler extends AbstractDebugHandler implements PostPrediction
}
@Override
public void toggleListener(GrimPlayer player) {
if (!listeners.remove(player)) listeners.add(player);
public boolean toggleListener(GrimPlayer player) {
boolean wasPresent = listeners.remove(player);
if (wasPresent) {
return false;
} else {
listeners.add(player);
return true;
}
}
@Override
@@ -1,6 +1,5 @@
package ac.grim.grimac.checks.impl.velocity;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.api.config.ConfigManager;
import ac.grim.grimac.checks.Check;
import ac.grim.grimac.checks.CheckData;
@@ -46,9 +45,6 @@ public class KnockbackHandler extends Check implements PostPredictionCheck {
WrapperPlayServerEntityVelocity velocity = new WrapperPlayServerEntityVelocity(event);
int entityId = velocity.getEntityId();
GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser());
if (player == null) return;
// Detect whether this knockback packet affects the player or if it is useless
// Mojang sends extra useless knockback packets for no apparent reason
if (player.compensatedEntities.serverPlayerVehicle != null && entityId != player.compensatedEntities.serverPlayerVehicle) {
@@ -0,0 +1,67 @@
package ac.grim.grimac.checks.type;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientCloseWindow;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerCloseWindow;
import org.jetbrains.annotations.MustBeInvokedByOverriders;
public class InventoryCheck extends BlockPlaceCheck implements PacketCheck {
// Impossible transaction ID
protected static final long NONE = Long.MAX_VALUE;
protected long closeTransaction = NONE;
protected int closePacketsToSkip;
public InventoryCheck(GrimPlayer player) {
super(player);
}
@Override
@MustBeInvokedByOverriders
public void onPacketReceive(final PacketReceiveEvent event) {
if (event.getPacketType() == PacketType.Play.Client.CLICK_WINDOW) {
// Disallow any clicks if inventory is closing
if (closeTransaction != NONE && shouldModifyPackets()) {
event.setCancelled(true);
player.onPacketCancel();
player.inventory.needResend = true;
}
} else if (event.getPacketType() == PacketType.Play.Client.CLOSE_WINDOW) {
// Players with high ping can close inventory faster than send transaction back
if (closeTransaction != NONE && closePacketsToSkip-- <= 0) {
closeTransaction = NONE;
}
}
}
public void closeInventory() {
if (closeTransaction != NONE) {
return;
}
int windowId = player.inventory.openWindowID;
player.user.writePacket(new WrapperPlayServerCloseWindow(windowId));
// Force close inventory on server side
closePacketsToSkip = 1; // Sending close packet to itself, so skip it
PacketEvents.getAPI().getProtocolManager().receivePacket(
player.user.getChannel(), new WrapperPlayClientCloseWindow(windowId)
);
player.sendTransaction();
int transaction = player.lastTransactionSent.get();
closeTransaction = transaction;
player.latencyUtils.addRealTimeTask(transaction, () -> {
if (closeTransaction == transaction) {
closeTransaction = NONE;
}
});
player.user.flushPackets();
}
}
@@ -1,6 +1,7 @@
package ac.grim.grimac.command.commands;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.checks.debug.HitboxDebugHandler;
import ac.grim.grimac.command.BuildableCommand;
import ac.grim.grimac.platform.api.command.PlayerSelector;
import ac.grim.grimac.platform.api.sender.Sender;
@@ -36,9 +37,16 @@ public class GrimDebug implements BuildableCommand {
.required("target", GrimAPI.INSTANCE.getCommandAdapter().singlePlayerSelectorParser())
.handler(this::handleConsoleDebug);
Command.Builder<Sender> hitboxDebugCommand = grimCommand
.literal("hitboxdebug", Description.of("Toggle hitbox debug visualization"))
.permission("grim.hitboxdebug")
.optional("target", GrimAPI.INSTANCE.getCommandAdapter().singlePlayerSelectorParser(), Description.of("Player to debug (defaults to self if sender is player)"))
.handler(this::handleHitboxDebug);
// Register command
commandManager.command(debugCommand);
commandManager.command(consoleDebugCommand);
commandManager.command(hitboxDebugCommand);
}
private void handleDebug(@NotNull CommandContext<Sender> context) {
@@ -88,6 +96,48 @@ public class GrimDebug implements BuildableCommand {
sender.sendMessage(message);
}
private void handleHitboxDebug(@NotNull CommandContext<Sender> context) {
Sender sender = context.sender();
PlayerSelector playerSelector = context.getOrDefault("target", null);
// Hitbox debug requires a *player* to be the listener (the one seeing the boxes)
if (!sender.isPlayer()) {
sender.sendMessage(MessageUtil.getParsedComponent(sender,
"hitboxdebug-player-only",
"%prefix% &cHitbox debug can only be toggled by players.")
);
return;
}
// Determine the target player whose hitboxes are being debugged
GrimPlayer targetGrimPlayer = parseTarget(sender, playerSelector == null ? sender : playerSelector.getSinglePlayer());
if (targetGrimPlayer == null) return;
// Get the sender's GrimPlayer data, as they are the listener
GrimPlayer senderGrimPlayer = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(sender.getUniqueId());
if (senderGrimPlayer == null) {
sender.sendMessage(Component.text("Could not find your player data to register as a listener.", NamedTextColor.RED));
return;
}
// Get the HitboxDebugHandler check instance and toggle the listener
HitboxDebugHandler hitboxHandler = targetGrimPlayer.checkManager.getCheck(HitboxDebugHandler.class);
if (hitboxHandler == null) {
sender.sendMessage(Component.text("HitboxDebugHandler check not found for target player.", NamedTextColor.RED));
return;
}
boolean enabled = hitboxHandler.toggleListener(senderGrimPlayer); // Pass the sender/listener
// Send feedback message
Component message = Component.text()
.append(Component.text("Hitbox debug listener for ", NamedTextColor.GRAY))
.append(Component.text(targetGrimPlayer.getName(), NamedTextColor.WHITE))
.append(Component.text(enabled ? " enabled." : " disabled.", NamedTextColor.GRAY))
.build();
sender.sendMessage(message);
}
private @Nullable GrimPlayer parseTarget(@NotNull Sender sender, @Nullable Sender t) {
if (sender.isConsole() && t == null) {
sender.sendMessage(MessageUtil.getParsedComponent(sender, "console-specify-target", "%prefix% &cYou must specify a target as the console!"));
@@ -7,7 +7,7 @@ import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import ac.grim.grimac.utils.common.PropertiesUtil;
import ac.grim.grimac.utils.reflection.ReflectionUtils;
import ac.grim.grimac.utils.reflection.ViaVersionUtil;
import ac.grim.grimac.utils.viaversion.ViaVersionUtil;
import com.github.retrooper.packetevents.PacketEvents;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -5,6 +5,8 @@ import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.update.*;
import ac.grim.grimac.utils.blockplace.BlockPlaceResult;
import ac.grim.grimac.utils.blockplace.ConsumesBlockPlace;
import ac.grim.grimac.utils.data.BlockHitData;
import ac.grim.grimac.utils.data.BlockPlaceSnapshot;
import ac.grim.grimac.utils.change.BlockModification;
import ac.grim.grimac.utils.data.*;
import ac.grim.grimac.utils.inventory.Inventory;
@@ -56,7 +58,7 @@ public class CheckManagerListener extends PacketListenerAbstract {
}
private static void placeWaterLavaSnowBucket(GrimPlayer player, ItemStack held, StateType toPlace, InteractionHand hand, int sequence) {
HitData data = WorldRayTrace.getNearestBlockHitResult(player, StateTypes.AIR, false, true, true);
BlockHitData data = WorldRayTrace.getNearestBlockHitResult(player, StateTypes.AIR, false, true, true);
if (data != null) {
BlockPlace blockPlace = new BlockPlace(player, hand, data.position(), data.closestDirection().getFaceValue(), data.closestDirection(), held, data, sequence);
@@ -272,7 +274,7 @@ public class CheckManagerListener extends PacketListenerAbstract {
}
private static void placeBucket(GrimPlayer player, InteractionHand hand, int sequence) {
HitData data = WorldRayTrace.getNearestBlockHitResult(player, null, true, false, true);
BlockHitData data = WorldRayTrace.getNearestBlockHitResult(player, null, true, false, true);
if (data != null) {
BlockPlace blockPlace = new BlockPlace(player, hand, data.position(), data.closestDirection().getFaceValue(), data.closestDirection(), ItemStack.EMPTY, data, sequence);
@@ -351,7 +353,7 @@ public class CheckManagerListener extends PacketListenerAbstract {
}
private static void placeLilypad(GrimPlayer player, InteractionHand hand, int sequence) {
HitData data = WorldRayTrace.getNearestBlockHitResult(player, null, true, false, true);
BlockHitData data = WorldRayTrace.getNearestBlockHitResult(player, null, true, false, true);
if (data != null) {
// A lilypad cannot replace a fluid
@@ -10,7 +10,7 @@ import ac.grim.grimac.utils.data.TrackerData;
import ac.grim.grimac.utils.data.packetentity.PacketEntity;
import ac.grim.grimac.utils.data.packetentity.PacketEntityHook;
import ac.grim.grimac.utils.data.packetentity.PacketEntityTrackXRot;
import ac.grim.grimac.utils.reflection.ViaVersionUtil;
import ac.grim.grimac.utils.viaversion.ViaVersionUtil;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.event.PacketSendEvent;
@@ -0,0 +1,163 @@
package ac.grim.grimac.events.packets;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.data.packetentity.PacketEntitySelf;
import ac.grim.grimac.utils.inventory.InventoryDesyncStatus;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketListenerPriority;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.event.PacketSendEvent;
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.wrapper.play.client.WrapperPlayClientClientStatus;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientClientStatus.Action;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerFlying;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerOpenWindow;
public class PacketPlayerWindow extends PacketListenerAbstract {
public PacketPlayerWindow() {
super(PacketListenerPriority.LOWEST);
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (WrapperPlayClientPlayerFlying.isFlying(event.getPacketType()) && !event.isCancelled()) {
GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser());
if (player == null) return;
if (player.hasInventoryOpen && isNearNetherPortal(player)) {
handleInventoryClose(player, InventoryDesyncStatus.NETHER_PORTAL);
}
}
// Client Status is sent in 1.7-1.8
if (event.getPacketType() == PacketType.Play.Client.CLIENT_STATUS) {
WrapperPlayClientClientStatus wrapper = new WrapperPlayClientClientStatus(event);
if (wrapper.getAction() == Action.OPEN_INVENTORY_ACHIEVEMENT) {
GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser());
if (player == null) return;
handleInventoryOpen(player);
}
}
// We need to do this due to 1.9 not sending anymore the inventory action in the Client Status
if (event.getPacketType() == PacketType.Play.Client.CLICK_WINDOW) {
GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser());
if (player == null) return;
// TODO: Remove this check after we finish the before ViaVersion injection
// Explanation: On 1.7 and 1.8 we have OPEN_INVENTORY_ACHIEVEMENT on CLIENT_STATUS packet
// but after Via translation this information gets lost
// This is a workaround to atleast make our inventory checks work "decently" in 1.8 clients for 1.9+ servers
if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_9)
&& player.getClientVersion().isOlderThanOrEquals(ClientVersion.V_1_8)) {
handleInventoryOpen(player);
}
if (player.getClientVersion().isNewerThan(ClientVersion.V_1_8)) {
handleInventoryOpen(player);
}
}
if (event.getPacketType() == PacketType.Play.Client.CLOSE_WINDOW) {
GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser());
if (player == null) return;
handleInventoryClose(player, InventoryDesyncStatus.NOT_DESYNCED);
}
}
@Override
public void onPacketSend(PacketSendEvent event) {
if (event.getPacketType() == PacketType.Play.Server.RESPAWN) {
GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser());
if (player == null) return;
player.sendTransaction();
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(),
() -> handleInventoryClose(player, InventoryDesyncStatus.NOT_DESYNCED));
} else if (event.getPacketType() == PacketType.Play.Server.OPEN_WINDOW) {
WrapperPlayServerOpenWindow wrapper = new WrapperPlayServerOpenWindow(event);
GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser());
if (player == null) return;
player.sendTransaction();
String legacyType = wrapper.getLegacyType();
int modernType = wrapper.getType();
InventoryDesyncStatus inventoryDesyncStatus = getContainerDesyncStatus(player, legacyType, modernType);
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(),
() -> {
if (inventoryDesyncStatus == InventoryDesyncStatus.NOT_DESYNCED) {
handleInventoryOpen(player);
} else {
handleInventoryClose(player, inventoryDesyncStatus);
}
});
} else if (event.getPacketType() == PacketType.Play.Server.OPEN_HORSE_WINDOW) {
GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser());
if (player == null) return;
player.sendTransaction();
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(),
() -> handleInventoryOpen(player));
} else if (event.getPacketType() == PacketType.Play.Server.CLOSE_WINDOW) {
GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser());
if (player == null) return;
player.sendTransaction();
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(),
() -> handleInventoryClose(player, InventoryDesyncStatus.NOT_DESYNCED));
}
}
private void handleInventoryOpen(GrimPlayer player) {
if (!player.hasInventoryOpen) {
player.lastInventoryOpen = System.currentTimeMillis();
}
player.hasInventoryOpen = true;
}
private void handleInventoryClose(GrimPlayer player, InventoryDesyncStatus desyncStatus) {
player.hasInventoryOpen = false;
player.inventoryDesyncStatus = desyncStatus;
}
public InventoryDesyncStatus getContainerDesyncStatus(GrimPlayer player, String legacyType, int modernType) {
// Closing beacon with the cross button cause desync in 1.7-1.8
if (player.getClientVersion().isOlderThanOrEquals(ClientVersion.V_1_8) &&
("minecraft:beacon".equals(legacyType) || modernType == 8)) {
return player.inventoryDesyncStatus = InventoryDesyncStatus.BEACON;
}
if (isNearNetherPortal(player)) {
return player.inventoryDesyncStatus = InventoryDesyncStatus.NETHER_PORTAL;
}
return player.inventoryDesyncStatus = InventoryDesyncStatus.NOT_DESYNCED;
}
public boolean isNearNetherPortal(GrimPlayer player) {
// Going inside nether portal with opened inventory cause desync, fixed in 1.12.2
if (player.getClientVersion().isOlderThanOrEquals(ClientVersion.V_1_12_1) &&
player.pointThreeEstimator.isNearNetherPortal) {
PacketEntitySelf playerEntity = player.compensatedEntities.self;
// Client ignore nether portal if player has passengers or riding an entity
return !playerEntity.inVehicle() && playerEntity.passengers.isEmpty();
}
return false;
}
}
@@ -1,12 +1,17 @@
package ac.grim.grimac.events.packets.worldreader;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.events.packets.worldreader.multiblockchange.LegacyMultiBlockChangeHandler;
import ac.grim.grimac.events.packets.worldreader.multiblockchange.V1160MultiBlockChangeBitRepackHandler;
import ac.grim.grimac.events.packets.worldreader.multiblockchange.VersionedMultiBlockChangeHandler;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.chunks.Column;
import ac.grim.grimac.utils.data.TeleportData;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketListenerPriority;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.world.chunk.BaseChunk;
import com.github.retrooper.packetevents.util.Vector3i;
@@ -16,11 +21,15 @@ import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerBl
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChangeGameState;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChunkData;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChunkDataBulk;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerMultiBlockChange;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerUnloadChunk;
public class BasePacketWorldReader extends PacketListenerAbstract {
private final static VersionedMultiBlockChangeHandler versionedMultiBlockChangeHandler
= PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_16)
? new V1160MultiBlockChangeBitRepackHandler()
: new LegacyMultiBlockChangeHandler();
public BasePacketWorldReader() {
super(PacketListenerPriority.HIGH);
}
@@ -161,28 +170,15 @@ public class BasePacketWorldReader extends PacketListenerAbstract {
player.lastTransSent + 2 < System.currentTimeMillis())
player.sendTransaction();
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> player.compensatedWorld.updateBlock(blockPosition.getX(), blockPosition.getY(), blockPosition.getZ(), blockChange.getBlockId()));
int x = blockPosition.getX();
int y = blockPosition.getY();
int z = blockPosition.getZ();
int blockId = blockChange.getBlockId();
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> player.compensatedWorld.updateBlock(x, y, z, blockId));
}
public void handleMultiBlockChange(GrimPlayer player, PacketSendEvent event) {
WrapperPlayServerMultiBlockChange multiBlockChange = new WrapperPlayServerMultiBlockChange(event);
int range = 16;
final var blocks = multiBlockChange.getBlocks();
for (WrapperPlayServerMultiBlockChange.EncodedBlock blockChange : blocks) {
// Don't send a transaction unless it's within 16 blocks of the player
if (Math.abs(blockChange.getX() - player.x) < range && Math.abs(blockChange.getY() - player.y) < range && Math.abs(blockChange.getZ() - player.z) < range && player.lastTransSent + 2 < System.currentTimeMillis()) {
player.sendTransaction();
break;
}
}
// Add a single runnable to prevent excessive memory use when there are lots of block changes
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> {
for (WrapperPlayServerMultiBlockChange.EncodedBlock blockChange : blocks) {
player.compensatedWorld.updateBlock(blockChange.getX(), blockChange.getY(), blockChange.getZ(), blockChange.getBlockId());
}
});
versionedMultiBlockChangeHandler.handleMultiBlockChange(player, event);
}
}
@@ -0,0 +1,30 @@
package ac.grim.grimac.events.packets.worldreader.multiblockchange;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerMultiBlockChange;
public class LegacyMultiBlockChangeHandler implements VersionedMultiBlockChangeHandler {
// TODO hande optimize Pre 1.16 code to also reduce memory usage and not use wrapper
@Override
public void handleMultiBlockChange(GrimPlayer player, PacketSendEvent event) {
WrapperPlayServerMultiBlockChange multiBlockChange = new WrapperPlayServerMultiBlockChange(event);
final var blocks = multiBlockChange.getBlocks();
for (WrapperPlayServerMultiBlockChange.EncodedBlock blockChange : blocks) {
// Don't send a transaction unless it's within 16 blocks of the player
if (Math.abs(blockChange.getX() - player.x) < RANGE && Math.abs(blockChange.getY() - player.y) < RANGE && Math.abs(blockChange.getZ() - player.z) < RANGE && player.lastTransSent + TRANSACTION_COOLDOWN_MS < System.currentTimeMillis()) {
player.sendTransaction();
break;
}
}
// Add a single runnable to prevent excessive memory use when there are lots of block changes
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> {
for (WrapperPlayServerMultiBlockChange.EncodedBlock blockChange : blocks) {
player.compensatedWorld.updateBlock(blockChange.getX(), blockChange.getY(), blockChange.getZ(), blockChange.getBlockId());
}
});
}
}
@@ -0,0 +1,175 @@
package ac.grim.grimac.events.packets.worldreader.multiblockchange;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.github.retrooper.packetevents.netty.buffer.ByteBufHelper;
import io.netty.buffer.ByteBuf;
/**
* <p>
* Minecraft’s MultiBlockChange packet batches many block updates in one shot:
* the server sends a 64-bit section coordinate, an optional trustEdges flag,
* a VarInt count, then that many VarLong-encoded block changes
* (<em>52 bits</em> of global blockStateId + <em>12 bits</em> of local X/Z/Y
* within the 16×16×16 section).
* </p>
*
* <p>
* To shrink our on-heap footprint for deferred tasks, we immediately repack
* each VarLong into one 32-bit <code>int</code>, while still using the
* vanilla 64-bit header for coords.
* </p>
*
* <h3>Vanilla “on-the-wire” format</h3>
* <pre>
* 1) sectionEncodedPosition : Long
* 2) [trustEdges? : Boolean] // only on protocol ≤1.19.4
* 3) recordCount : VarInt
* 4) records[] : recordCount × VarLong
*
* Each VarLong is bits:
* [63……12] blockStateId (52 bits)
* [11……8] localX (4 bits, 0–15)
* [7……4] localZ (4 bits, 0–15)
* [3……0] localY (4 bits, 0–15)
* </pre>
*
* <h3>Vanilla 64-bit section header “encodedPosition”</h3>
* <pre>
* bits 63……42 41……20 19……0
* ┌────────┬─────────┬────────┐
* │ secX │ secZ │ secY │
* │ (22b) │ (22b) │ (20b) │
* └────────┴─────────┴────────┘
*
* secX = encoded >> 42
* secZ = encoded << 22 >> 42
* secY = encoded << 44 >> 44
* </pre>
*
* <h3>Our 32-bit repacked block record (MSB → LSB)</h3>
* <pre>
* bits 31…17 16…12 11…8 7…4 3…0
* ┌───────┬───────┬───────┬──────┬─────┐
* │ state │ spare │ lX │ lZ │ lY │
* │ (15b) │ (5b) │ (4b) │ (4b) │(4b) │
* └───────┴───────┴───────┴──────┴─────┘
*
* • state = (data >>> 12) & 0x7FFF
* • spare = bits 12–16 (unused, reserved for flags)
* • lX = (data >>> 8) & 0xF
* • lZ = (data >>> 4) & 0xF
* • lY = data & 0xF
*
* Then pack: packed = (state << 17) | ((lX<<8)|(lZ<<4)|lY)
* </pre>
*/
public final class V1160MultiBlockChangeBitRepackHandler
implements ac.grim.grimac.events.packets.worldreader.multiblockchange.VersionedMultiBlockChangeHandler {
/* ---------- bit masks / shifts for the packed int ---------- */
private static final int SHIFT_STATE = 17; // 32-15 = 17
private static final int MASK_STATE = 0x7FFF; // 15 bits
static final int MASK_LOCAL = 0xFFF; // 12 bits
/* ---------- does this protocol still have trustEdges ? ----- */
private static final boolean HAS_TRUST_EDGES =
PacketEvents.getAPI().getServerManager().getVersion().isOlderThanOrEquals(ServerVersion.V_1_19_4);
@Override
public void handleMultiBlockChange(GrimPlayer player, PacketSendEvent event) {
// PE resets writer index for us, we don't have to call buffer.writerIndex(originalWriterIndex)
ByteBuf buf = (ByteBuf) event.getByteBuf();
/* 1. Section-position header (64 bits) ------------------ */
long sectionEncodedPosition = ByteBufHelper.readLong(buf);
if (HAS_TRUST_EDGES) { // skip only when it really exists
buf.skipBytes(1);
}
/* 2. Record count + packed-int array ------------------- */
int recordCount = ByteBufHelper.readVarInt(buf);
int[] packed = new int[recordCount];
/* 3. Decode packet (still comes as varLong per record) */
// Unpack section coords once for the “near player” test
int secX = (int) (sectionEncodedPosition >> 42);
int secZ = (int) (sectionEncodedPosition << 22 >> 42);
int secY = (int) (sectionEncodedPosition << 44 >> 44);
int baseX = secX << 4;
int baseY = secY << 4;
int baseZ = secZ << 4;
boolean sendTx = false;
// Use this to not spam the player with transactions if one has been sent within COOLDOWN
long now = System.currentTimeMillis();
for (int i = 0; i < recordCount; i++) {
long data = readVarLong(buf); // 52+12 bits
int local = (int) (data & 0xFFFL); // 12-bit pos
packed[i] = repackFromLong(data);
/* -------- near-player distance test -------------- */
if (!sendTx) {
int lx = (local >>> 8) & 0xF;
int lz = (local >>> 4) & 0xF;
int ly = local & 0xF;
int wx = baseX + lx, wy = baseY + ly, wz = baseZ + lz;
if (Math.abs(wx - player.x) < RANGE &&
Math.abs(wy - player.y) < RANGE &&
Math.abs(wz - player.z) < RANGE &&
player.lastTransSent + TRANSACTION_COOLDOWN_MS < now) {
sendTx = true;
}
}
}
if (sendTx)
player.sendTransaction();
/* 4. Queue runnable – captures only int[] + sectionPos */
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> {
// unpack section once per execution
int sX = (int) (sectionEncodedPosition >> 42);
int sY = (int) (sectionEncodedPosition << 44 >> 44);
int sZ = (int) (sectionEncodedPosition << 22 >> 42);
int bx = sX << 4, by = sY << 4, bz = sZ << 4;
for (int rec : packed) {
int stateId = (rec >>> SHIFT_STATE) & MASK_STATE;
int lx = (rec >>> 8) & 0xF;
int lz = (rec >>> 4) & 0xF;
int ly = rec & 0xF;
int wx = bx + lx;
int wy = by + ly;
int wz = bz + lz;
player.compensatedWorld.updateBlock(wx, wy, wz, stateId);
}
});
}
public int repackFromLong(long data) {
// 1) extract the 15-bit state from bits 12.. (original >> 12)
int blockState = (int)((data >>> 12) & MASK_STATE);
// 2) extract the 12-bit local from bits 0..11
int local = (int)( data & MASK_LOCAL);
// 3) glue them together
return (blockState << SHIFT_STATE) | local;
}
}
@@ -0,0 +1,22 @@
package ac.grim.grimac.events.packets.worldreader.multiblockchange;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import io.netty.buffer.ByteBuf;
public interface VersionedMultiBlockChangeHandler {
int RANGE = 16;
long TRANSACTION_COOLDOWN_MS = 2; // In milliseconds
void handleMultiBlockChange(GrimPlayer player, PacketSendEvent event);
default long readVarLong(ByteBuf buf) {
long value = 0;
int size = 0;
int b;
while (((b = buf.readByte()) & 0x80) == 0x80) {
value |= (long) (b & 0x7F) << (size++ * 7);
}
return value | ((long) (b & 0x7F) << (size * 7));
}
}
@@ -2,6 +2,7 @@ package ac.grim.grimac.manager;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.api.AbstractCheck;
import ac.grim.grimac.checks.debug.HitboxDebugHandler;
import ac.grim.grimac.checks.impl.aim.AimDuplicateLook;
import ac.grim.grimac.checks.impl.aim.AimModulo360;
import ac.grim.grimac.checks.impl.aim.processor.AimProcessor;
@@ -11,15 +12,13 @@ import ac.grim.grimac.checks.impl.chat.ChatA;
import ac.grim.grimac.checks.impl.chat.ChatB;
import ac.grim.grimac.checks.impl.chat.ChatC;
import ac.grim.grimac.checks.impl.chat.ChatD;
import ac.grim.grimac.checks.impl.combat.Hitboxes;
import ac.grim.grimac.checks.impl.combat.MultiInteractA;
import ac.grim.grimac.checks.impl.combat.MultiInteractB;
import ac.grim.grimac.checks.impl.combat.Reach;
import ac.grim.grimac.checks.impl.combat.*;
import ac.grim.grimac.checks.impl.crash.*;
import ac.grim.grimac.checks.impl.elytra.*;
import ac.grim.grimac.checks.impl.exploit.ExploitA;
import ac.grim.grimac.checks.impl.exploit.ExploitB;
import ac.grim.grimac.checks.impl.groundspoof.NoFall;
import ac.grim.grimac.checks.impl.inventory.*;
import ac.grim.grimac.checks.impl.misc.ClientBrand;
import ac.grim.grimac.checks.impl.misc.GhostBlockMitigation;
import ac.grim.grimac.checks.impl.misc.Post;
@@ -117,6 +116,11 @@ public class CheckManager {
.put(BadPacketsV.class, new BadPacketsV(player))
.put(BadPacketsY.class, new BadPacketsY(player))
.put(BadPacketsZ.class, new BadPacketsZ(player))
.put(InventoryA.class, new InventoryA(player))
.put(InventoryB.class, new InventoryB(player))
.put(InventoryE.class, new InventoryE(player))
.put(InventoryF.class, new InventoryF(player))
.put(InventoryG.class, new InventoryG(player))
.put(MultiActionsA.class, new MultiActionsA(player))
.put(MultiActionsC.class, new MultiActionsC(player))
.put(MultiActionsD.class, new MultiActionsD(player))
@@ -125,7 +129,7 @@ public class CheckManager {
.put(PacketOrderC.class, new PacketOrderC(player))
.put(PacketOrderD.class, new PacketOrderD(player))
.put(PacketOrderO.class, new PacketOrderO(player))
.put(PacketOrderP.class, new PacketOrderP(player))
// .put(PacketOrderP.class, new PacketOrderP(player))
.put(SprintA.class, new SprintA(player))
.put(VehicleA.class, new VehicleA(player))
.put(VehicleB.class, new VehicleB(player))
@@ -201,6 +205,7 @@ public class CheckManager {
.build();
blockPlaceChecks = new ImmutableClassToInstanceMap.Builder<BlockPlaceCheck>()
.put(InventoryC.class, new InventoryC(player))
.put(InvalidPlaceA.class, new InvalidPlaceA(player))
.put(InvalidPlaceB.class, new InvalidPlaceB(player))
.put(AirLiquidPlace.class, new AirLiquidPlace(player))
@@ -250,6 +255,9 @@ public class CheckManager {
.put(TransactionOrder.class, new TransactionOrder(player))
.put(VehicleC.class, new VehicleC(player))
.put(Hitboxes.class, new Hitboxes(player)) // Hitboxes is invoked by Reach
.put(WallHit.class, new WallHit(player))
.put(EntityPierce.class, new EntityPierce(player))
.put(HitboxDebugHandler.class, new HitboxDebugHandler(player))
.build();
allChecks = new ImmutableClassToInstanceMap.Builder<AbstractCheck>()
@@ -100,6 +100,13 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
blockMovementsUntilResync(true, true);
}
public void executeForceResyncNoSimulation() {
if (player.gamemode == GameMode.SPECTATOR || player.disableGrim)
return; // We don't care about spectators, they don't flag
if (lastKnownGoodPosition == null) return; // Player hasn't spawned yet
blockMovementsUntilResync(false, true);
}
public void executeNonSimulatingSetback() {
if (player.gamemode == GameMode.SPECTATOR || player.disableGrim)
return; // We don't care about spectators, they don't flag
@@ -10,12 +10,14 @@ import com.github.retrooper.packetevents.protocol.item.enchantment.type.Enchantm
import com.github.retrooper.packetevents.protocol.item.type.ItemTypes;
import com.github.retrooper.packetevents.protocol.particle.type.ParticleTypes;
import com.github.retrooper.packetevents.protocol.world.states.type.StateTypes;
import com.github.retrooper.packetevents.util.PEVersion;
import java.util.concurrent.Executors;
public class PacketEventsInit implements LoadableInitable {
private final PacketEventsAPI<?> packetEventsAPI;
PEVersion MINIMUM_REQUIRED_PE_VERSION = new PEVersion(2, 8, 0, true);
public PacketEventsInit(PacketEventsAPI<?> packetEventsAPI) {
this.packetEventsAPI = packetEventsAPI;
@@ -25,6 +27,17 @@ public class PacketEventsInit implements LoadableInitable {
public void load() {
LogUtil.info("Loading PacketEvents...");
PacketEvents.setAPI(packetEventsAPI);
if (!checkPacketEventsVersion()) {
LogUtil.error("\n" +
"******************************************************\n" +
"GrimAC requires PacketEvents >= " + MINIMUM_REQUIRED_PE_VERSION +
(MINIMUM_REQUIRED_PE_VERSION.snapshot() ? "-SNAPSHOT" : "") + "\n" +
"Current version: " + PacketEvents.getAPI().getVersion() + "\n" +
"Please update PacketEvents to a compatible version.\n" +
"*****************************************************");
}
PacketEvents.getAPI().getSettings()
.fullStackTrace(true)
.kickOnPacketException(true)
@@ -44,4 +57,33 @@ public class PacketEventsInit implements LoadableInitable {
ParticleTypes.DUST.getName();
}).start();
}
private boolean checkPacketEventsVersion() {
PEVersion current = PacketEvents.getAPI().getVersion();
PEVersion required = MINIMUM_REQUIRED_PE_VERSION;
// If current version is newer, always accept
if (current.isNewerThan(required)) {
return true;
}
// If current version is exactly equal to required (including snapshot status), accept
if (current.major() == required.major()
&& current.minor() == required.minor()
&& current.patch() == required.patch()
&& current.snapshot() == required.snapshot()) {
return true;
}
// If required is a snapshot, accept matching release or snapshot
if (required.snapshot()
&& current.major() == required.major()
&& current.minor() == required.minor()
&& current.patch() == required.patch()) {
return true;
}
// Otherwise, reject
return false;
}
}
@@ -15,6 +15,7 @@ public class PacketManager implements StartableInitable {
PacketEvents.getAPI().getEventManager().registerListener(new PacketPlayerJoinQuit());
PacketEvents.getAPI().getEventManager().registerListener(new PacketPingListener());
PacketEvents.getAPI().getEventManager().registerListener(new PacketPlayerWindow());
PacketEvents.getAPI().getEventManager().registerListener(new PacketPlayerDigging());
PacketEvents.getAPI().getEventManager().registerListener(new PacketPlayerAttack());
PacketEvents.getAPI().getEventManager().registerListener(new PacketEntityAction());
@@ -2,7 +2,7 @@ package ac.grim.grimac.manager.init.start;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.utils.anticheat.LogUtil;
import ac.grim.grimac.utils.reflection.ViaVersionUtil;
import ac.grim.grimac.utils.viaversion.ViaVersionUtil;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
@@ -2,7 +2,7 @@ package ac.grim.grimac.manager.init.start;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.utils.anticheat.LogUtil;
import ac.grim.grimac.utils.reflection.ViaVersionUtil;
import ac.grim.grimac.utils.viaversion.ViaVersionUtil;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.viaversion.viaversion.api.Via;
@@ -12,6 +12,7 @@ public class ViaVersion implements StartableInitable {
@Override
public void start() {
if (!ViaVersionUtil.isAvailable) return;
ViaVersionUtil.injectHooks();
ServerVersion serverVersion = PacketEvents.getAPI().getServerManager().getVersion();
@@ -30,6 +30,7 @@ import ac.grim.grimac.utils.data.packetentity.PacketEntitySelf;
import ac.grim.grimac.utils.data.tags.SyncedTags;
import ac.grim.grimac.utils.enums.FluidTag;
import ac.grim.grimac.utils.enums.Pose;
import ac.grim.grimac.utils.inventory.InventoryDesyncStatus;
import ac.grim.grimac.utils.latency.*;
import ac.grim.grimac.utils.math.GrimMath;
import ac.grim.grimac.utils.math.Location;
@@ -38,8 +39,9 @@ import ac.grim.grimac.utils.math.Vector3dm;
import ac.grim.grimac.utils.nmsutil.BlockProperties;
import ac.grim.grimac.utils.nmsutil.Collisions;
import ac.grim.grimac.utils.nmsutil.GetBoundingBox;
import ac.grim.grimac.utils.nmsutil.ReachUtils;
import ac.grim.grimac.utils.nmsutil.Materials;
import ac.grim.grimac.utils.reflection.ViaVersionUtil;
import ac.grim.grimac.utils.viaversion.ViaVersionUtil;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
@@ -209,7 +211,7 @@ public class GrimPlayer implements GrimUser {
public final CompensatedWorld compensatedWorld;
public final CompensatedEntities compensatedEntities;
public final CompensatedInventory inventory;
public final LatencyUtils latencyUtils = new LatencyUtils(this);
public final ILatencyUtils latencyUtils = new LatencyUtils(this);
public final PointThreeEstimator pointThreeEstimator;
public final TrigHandler trigHandler = new TrigHandler(this);
public final PacketStateData packetStateData = new PacketStateData();
@@ -236,6 +238,9 @@ public class GrimPlayer implements GrimUser {
// 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;
public final Queue<BlockPlaceSnapshot> placeUseItemPackets = new LinkedBlockingQueue<>();
public final Queue<BlockBreak> queuedBreaks = new LinkedBlockingQueue<>();
public final PlayerBlockHistory blockHistory = new PlayerBlockHistory();
@@ -658,6 +663,33 @@ public class GrimPlayer implements GrimUser {
}
}
// 1.8-1.10.2 specific mouse delay fix (MC-67665)
// https://bugs.mojang.com/browse/MC-67665
// 1.9-1.21.1 specific desync due to skipped ticks
// Players can be a tick behind on both pitch and yaw together
// 1.21.2+ added end tick input packet, fixing skipped tick issues
public Vector3dm[] getPossibleLookVectors(boolean isPrediction) {
// https://bugs.mojang.com/browse/MC-67665
List<Vector3dm> possibleLookDirs = new ArrayList<>(Collections.singletonList(ReachUtils.getLook(this, this.yaw, this.pitch)));
// If we are a tick behind, we don't know their next look so don't bother doing this
if (!isPrediction) {
possibleLookDirs.add(ReachUtils.getLook(this, this.lastYaw, this.pitch));
// 1.9+ players could be a tick behind because we don't get skipped ticks
if (this.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9)) {
possibleLookDirs.add(ReachUtils.getLook(this, this.lastYaw, this.lastPitch));
}
// 1.7 players do not have any of these issues! They are always on the latest look vector
if (this.getClientVersion().isOlderThan(ClientVersion.V_1_8)) {
possibleLookDirs = Collections.singletonList(ReachUtils.getLook(this, this.yaw, this.pitch));
}
}
return possibleLookDirs.toArray(new Vector3dm[0]);
}
@Override
public int getTransactionPing() {
return GrimMath.floor(transactionPing / 1e6);
@@ -70,7 +70,8 @@ public class MovementCheckRunner extends Check implements PositionCheck {
if (!invalidVehicle && !data.isTeleport()) {
// Teleport the player back to avoid players being able to simply ignore transactions
player.getSetbackTeleportUtil().executeForceResync();
// We shouldn't simulate movement in unloaded chunks
player.getSetbackTeleportUtil().executeForceResyncNoSimulation();
}
}
@@ -1,5 +1,6 @@
package ac.grim.grimac.predictionengine;
import ac.grim.grimac.checks.impl.combat.Reach;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.predictionengine.predictions.PredictionEngine;
import ac.grim.grimac.utils.collisions.CollisionData;
@@ -21,6 +22,7 @@ import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState
import com.github.retrooper.packetevents.protocol.world.states.defaulttags.BlockTags;
import com.github.retrooper.packetevents.protocol.world.states.type.StateType;
import com.github.retrooper.packetevents.protocol.world.states.type.StateTypes;
import com.github.retrooper.packetevents.util.Vector3i;
import lombok.Getter;
import lombok.Setter;
@@ -107,6 +109,7 @@ public class PointThreeEstimator {
private boolean isNearHorizontalFlowingLiquid = false; // We can't calculate the direction, only a toggle
private boolean isNearVerticalFlowingLiquid = false; // We can't calculate exact values, once again a toggle
private boolean isNearBubbleColumn = false; // We can't calculate exact values once again
public boolean isNearNetherPortal = false; // We can't calculate exact values once again
private int maxPositiveLevitation = Integer.MIN_VALUE; // Positive potion effects [0, 128]
private int minNegativeLevitation = Integer.MAX_VALUE; // Negative potion effects [-127, -1]r
@@ -156,6 +159,8 @@ public class PointThreeEstimator {
isNearFluid = true;
}
player.checkManager.getPacketCheck(Reach.class).handleBlockChange(new Vector3i(x, y, z), state);
if (pointThreeBox.isIntersected(new SimpleCollisionBox(x, y, z))) {
// https://github.com/MWHunter/Grim/issues/613
int controllingEntityId = player.inVehicle() ? player.getRidingVehicleId() : player.entityID;
@@ -266,6 +271,7 @@ public class PointThreeEstimator {
isNearClimbable = false;
isNearBubbleColumn = false;
isNearFluid = false;
isNearNetherPortal = false;
// Check for flowing water
Collisions.hasMaterial(player, pointThreeBox, (pair) -> {
@@ -287,6 +293,10 @@ public class PointThreeEstimator {
isNearFluid = true;
}
if (state.getType() == StateTypes.NETHER_PORTAL) {
isNearNetherPortal = true;
}
return false;
});
}
@@ -15,7 +15,7 @@ import ac.grim.grimac.utils.nmsutil.Collisions;
import ac.grim.grimac.utils.nmsutil.FluidFallingAdjustedMovement;
import ac.grim.grimac.utils.nmsutil.GetBoundingBox;
import ac.grim.grimac.utils.nmsutil.MainSupportingBlockPosFinder;
import ac.grim.grimac.utils.reflection.ViaVersionUtil;
import ac.grim.grimac.utils.viaversion.ViaVersionUtil;
import ac.grim.grimac.utils.team.EntityPredicates;
import ac.grim.grimac.utils.team.EntityTeam;
import ac.grim.grimac.utils.team.TeamHandler;
@@ -492,52 +492,63 @@ public class PredictionEngine {
// Order priority (to avoid false positives and false flagging future predictions):
// Knockback and explosions
// 0.03 ticks
// Movement without input
// Normal movement
// First bread knockback and explosions
// Flagging groundspoof
// Flagging flip items
if (a.isExplosion())
aScore -= 5;
aScore -= 10;
if (a.isKnockback())
aScore -= 5;
aScore -= 10;
if (b.isExplosion())
bScore -= 5;
bScore -= 10;
if (b.isKnockback())
bScore -= 5;
bScore -= 10;
if (a.isFirstBreadExplosion())
aScore += 1;
aScore += 2;
if (b.isFirstBreadExplosion())
bScore += 1;
bScore += 2;
if (a.isFirstBreadKb())
aScore += 1;
aScore += 2;
if (b.isFirstBreadKb())
bScore += 1;
bScore += 2;
if (a.isFlipItem())
aScore += 3;
aScore += 6;
if (b.isFlipItem())
bScore += 3;
bScore += 6;
if (a.isZeroPointZeroThree())
aScore -= 1;
aScore -= 2;
if (b.isZeroPointZeroThree())
bScore -= 2;
if (a.isWithInput() || a.isJump())
aScore += 1;
else
aScore -= 1;
if (b.isWithInput() || b.isJump())
bScore += 1;
else
bScore -= 1;
// If the player is on the ground but the vector leads the player off the ground
if ((player.inVehicle() ? player.clientControlledVerticalCollision : player.onGround) && a.vector.getY() >= 0)
aScore += 2;
aScore += 4;
if ((player.inVehicle() ? player.clientControlledVerticalCollision : player.onGround) && b.vector.getY() >= 0)
bScore += 2;
bScore += 4;
if (aScore != bScore)
return Integer.compare(aScore, bScore);
@@ -829,9 +840,9 @@ public class PredictionEngine {
if (applyStuckSpeed == 0 && player.isForceStuckSpeed()) break;
Vector3dm input = transformInputsToVector(player, new Vector3dm(strafe, 0, forward));
VectorData result = new VectorData(possibleLastTickOutput.vector.clone()
VectorData result = new VectorData.MoveVectorData(possibleLastTickOutput.vector.clone()
.add(getMovementResultFromInput(player, input, speed, player.yaw)),
possibleLastTickOutput, VectorData.VectorType.InputResult);
possibleLastTickOutput, VectorData.VectorType.InputResult, forward, strafe);
result.input = input;
if (applyStuckSpeed != 0) {
result = result.returnNewModified(result.vector.clone().multiply(player.stuckSpeedMultiplier), VectorData.VectorType.StuckMultiplier);
@@ -8,7 +8,7 @@ import ac.grim.grimac.utils.collisions.blocks.DoorHandler;
import ac.grim.grimac.utils.collisions.datatypes.CollisionBox;
import ac.grim.grimac.utils.collisions.datatypes.ComplexCollisionBox;
import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
import ac.grim.grimac.utils.data.HitData;
import ac.grim.grimac.utils.data.BlockHitData;
import ac.grim.grimac.utils.data.packetentity.PacketEntity;
import ac.grim.grimac.utils.latency.CompensatedWorld;
import ac.grim.grimac.utils.math.GrimMath;
@@ -52,14 +52,14 @@ public class BlockPlace {
private final GrimPlayer player;
public final ItemStack itemStack;
public final StateType material;
public final @Nullable HitData hitData;
public final @Nullable BlockHitData hitData;
@Getter private int faceId;
@Getter private BlockFace face;
public boolean isInside;
public Vector3f cursor;
public final int sequence;
public BlockPlace(GrimPlayer player, InteractionHand hand, Vector3i position, int faceId, BlockFace face, ItemStack itemStack, @Nullable HitData hitData, int sequence) {
public BlockPlace(GrimPlayer player, InteractionHand hand, Vector3i position, int faceId, BlockFace face, ItemStack itemStack, @Nullable BlockHitData hitData, int sequence) {
this.player = player;
this.hand = hand;
this.position = position;
@@ -22,7 +22,7 @@ import ac.grim.grimac.utils.collisions.datatypes.NoCollisionBox;
import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
import ac.grim.grimac.utils.data.packetentity.PacketEntityStrider;
import ac.grim.grimac.utils.nmsutil.Materials;
import ac.grim.grimac.utils.reflection.ViaVersionUtil;
import ac.grim.grimac.utils.viaversion.ViaVersionUtil;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.github.retrooper.packetevents.protocol.item.ItemStack;
@@ -0,0 +1,34 @@
package ac.grim.grimac.utils.data;
import ac.grim.grimac.utils.math.Vector3dm;
import com.github.retrooper.packetevents.protocol.world.BlockFace;
import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState;
import com.github.retrooper.packetevents.util.Vector3d;
import com.github.retrooper.packetevents.util.Vector3i;
import lombok.Getter;
import lombok.ToString;
import lombok.experimental.Accessors;
@Getter
@Accessors(fluent = true)
@ToString
public class BlockHitData extends HitData {
Vector3i position;
WrappedBlockState state;
BlockFace closestDirection;
// public Boolean success;
public BlockHitData(Vector3i position, Vector3dm blockHitLocation, BlockFace closestDirection, WrappedBlockState state
// , Boolean success
) {
super(blockHitLocation);
this.position = position;
this.closestDirection = closestDirection;
this.state = state;
// this.success = success;
}
public Vector3d getRelativeBlockHitLocation() {
return new Vector3d(blockHitLocation.getX() - position.getX(), blockHitLocation.getY() - position.getY(), blockHitLocation.getZ() - position.getZ());
}
}
@@ -0,0 +1,23 @@
package ac.grim.grimac.utils.data;
import ac.grim.grimac.utils.data.packetentity.PacketEntity;
import ac.grim.grimac.utils.math.Vector3dm;
import lombok.Getter;
import lombok.ToString;
@Getter
@ToString
public class EntityHitData extends HitData {
private final PacketEntity entity;
public EntityHitData(PacketEntity packetEntity) {
this(packetEntity, new Vector3dm(packetEntity.trackedServerPosition.getPos().x,
packetEntity.trackedServerPosition.getPos().y,
packetEntity.trackedServerPosition.getPos().z));
}
public EntityHitData(PacketEntity packetEntity, Vector3dm intersectionPoint) {
super(intersectionPoint); // Use actual intersection point
this.entity = packetEntity;
}
}
@@ -1,18 +1,15 @@
package ac.grim.grimac.utils.data;
import ac.grim.grimac.utils.math.Vector3dm;
import com.github.retrooper.packetevents.protocol.world.BlockFace;
import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState;
import com.github.retrooper.packetevents.util.Vector3d;
import com.github.retrooper.packetevents.util.Vector3i;
public record HitData(
Vector3i position,
Vector3dm blockHitLocation,
BlockFace closestDirection,
WrappedBlockState state
) {
public Vector3d getRelativeBlockHitLocation() {
return new Vector3d(blockHitLocation.getX() - position.getX(), blockHitLocation.getY() - position.getY(), blockHitLocation.getZ() - position.getZ());
public class HitData {
Vector3dm blockHitLocation;
public HitData(Vector3dm blockHitLocation) {
this.blockHitLocation = blockHitLocation;
}
public ac.grim.grimac.utils.math.Vector3dm getBlockHitLocation() {
return this.blockHitLocation;
}
}
@@ -0,0 +1,11 @@
package ac.grim.grimac.utils.data;
import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
import com.github.retrooper.packetevents.protocol.world.BlockFace;
import java.util.List;
public record PistonTemplate(BlockFace dir,
List<SimpleCollisionBox> boxes,
boolean push, boolean slime, boolean honey) {
}
@@ -0,0 +1,23 @@
package ac.grim.grimac.utils.data;
public class PlayerPistonData {
public final PistonTemplate pistonTemplate;
public final int lastTransactionSent;
// Calculate if the player has no-push, and when to end the possibility of applying piston
int ticksOfPistonBeingAlive = 0;
public PlayerPistonData(PistonTemplate playerPistonData, int lastTransactionSent) {
this.pistonTemplate = playerPistonData;
this.lastTransactionSent = lastTransactionSent;
}
// We don't know when the piston has applied, or what stage of pushing it is on
// Therefore, we need to use what we have - the number of movement packets.
// 10 is a very cautious number
public boolean tickIfGuaranteedFinished() {
return ++ticksOfPistonBeingAlive >= 10;
}
}
@@ -201,6 +201,133 @@ public class ReachInterpolationData {
return minimumInterpLocation;
}
public CollisionBox getOverlapHitboxCombined() {
int interpSteps = getInterpolationSteps();
// Calculate step increments for each axis
double stepMinX = (targetLocation.minX - startingLocation.minX) / (double) interpSteps;
double stepMaxX = (targetLocation.maxX - startingLocation.maxX) / (double) interpSteps;
double stepMinY = (targetLocation.minY - startingLocation.minY) / (double) interpSteps;
double stepMaxY = (targetLocation.maxY - startingLocation.maxY) / (double) interpSteps;
double stepMinZ = (targetLocation.minZ - startingLocation.minZ) / (double) interpSteps;
double stepMaxZ = (targetLocation.maxZ - startingLocation.maxZ) / (double) interpSteps;
// Track the intersection of all expanded hitboxes
double overallMinX = Double.NEGATIVE_INFINITY;
double overallMaxX = Double.POSITIVE_INFINITY;
double overallMinY = Double.NEGATIVE_INFINITY;
double overallMaxY = Double.POSITIVE_INFINITY;
double overallMinZ = Double.NEGATIVE_INFINITY;
double overallMaxZ = Double.POSITIVE_INFINITY;
boolean isFirstStep = true;
for (int step = interpolationStepsLowBound; step <= interpolationStepsHighBound; step++) {
// Compute interpolated position for this step
double currentMinX = startingLocation.minX + (step * stepMinX);
double currentMaxX = startingLocation.maxX + (step * stepMaxX);
double currentMinY = startingLocation.minY + (step * stepMinY);
double currentMaxY = startingLocation.maxY + (step * stepMaxY);
double currentMinZ = startingLocation.minZ + (step * stepMinZ);
double currentMaxZ = startingLocation.maxZ + (step * stepMaxZ);
// Create the collision box for this step's position
// Create boxes for each bottom corner
SimpleCollisionBox[] cornerBoxes = new SimpleCollisionBox[4];
// Bottom corners: (minX,minY,minZ), (maxX,minY,minZ), (minX,minY,maxZ), (maxX,minY,maxZ)
cornerBoxes[0] = new SimpleCollisionBox(currentMinX, currentMinY, currentMinZ,
currentMinX, currentMinY, currentMinZ);
cornerBoxes[1] = new SimpleCollisionBox(currentMaxX, currentMinY, currentMinZ,
currentMaxX, currentMinY, currentMinZ);
cornerBoxes[2] = new SimpleCollisionBox(currentMinX, currentMinY, currentMaxZ,
currentMinX, currentMinY, currentMaxZ);
cornerBoxes[3] = new SimpleCollisionBox(currentMaxX, currentMinY, currentMaxZ,
currentMaxX, currentMinY, currentMaxZ);
// Expand each corner box by entity dimensions
for (SimpleCollisionBox cornerBox : cornerBoxes) {
GetBoundingBox.expandBoundingBoxByEntityDimensions(cornerBox, player, entity);
}
// Get the overlap of the 4 corner boxes
CollisionBox stepOverlap = getOverlapOfBoxes(cornerBoxes);
if (stepOverlap == NoCollisionBox.INSTANCE)
return NoCollisionBox.INSTANCE;
SimpleCollisionBox stepBox = (SimpleCollisionBox) stepOverlap;
// Initialize overall bounds with the first expanded box
if (isFirstStep) {
overallMinX = stepBox.minX;
overallMaxX = stepBox.maxX;
overallMinY = stepBox.minY;
overallMaxY = stepBox.maxY;
overallMinZ = stepBox.minZ;
overallMaxZ = stepBox.maxZ;
isFirstStep = false;
} else {
// Update bounds to the intersection of all expanded boxes
overallMinX = Math.max(overallMinX, stepBox.minX);
overallMaxX = Math.min(overallMaxX, stepBox.maxX);
overallMinY = Math.max(overallMinY, stepBox.minY);
overallMaxY = Math.min(overallMaxY, stepBox.maxY);
overallMinZ = Math.max(overallMinZ, stepBox.minZ);
overallMaxZ = Math.min(overallMaxZ, stepBox.maxZ);
}
// Early exit if the intersection becomes empty
if (overallMinX > overallMaxX || overallMinY > overallMaxY || overallMinZ > overallMaxZ) {
return NoCollisionBox.INSTANCE;
}
}
// Check if the final intersection is valid
if (overallMinX > overallMaxX || overallMinY > overallMaxY || overallMinZ > overallMaxZ) {
return NoCollisionBox.INSTANCE;
}
return new SimpleCollisionBox(
overallMinX, overallMinY, overallMinZ,
overallMaxX, overallMaxY, overallMaxZ
);
}
private CollisionBox getOverlapOfBoxes(SimpleCollisionBox[] boxes) {
double minX = Double.NEGATIVE_INFINITY;
double maxX = Double.POSITIVE_INFINITY;
double minY = Double.NEGATIVE_INFINITY;
double maxY = Double.POSITIVE_INFINITY;
double minZ = Double.NEGATIVE_INFINITY;
double maxZ = Double.POSITIVE_INFINITY;
boolean first = true;
for (SimpleCollisionBox box : boxes) {
if (first) {
minX = box.minX;
maxX = box.maxX;
minY = box.minY;
maxY = box.maxY;
minZ = box.minZ;
maxZ = box.maxZ;
first = false;
} else {
minX = Math.max(minX, box.minX);
maxX = Math.min(maxX, box.maxX);
minY = Math.max(minY, box.minY);
maxY = Math.min(maxY, box.maxY);
minZ = Math.max(minZ, box.minZ);
maxZ = Math.min(maxZ, box.maxZ);
}
if (minX > maxX || minY > maxY || minZ > maxZ) {
return NoCollisionBox.INSTANCE;
}
}
return new SimpleCollisionBox(minX, minY, minZ, maxX, maxY, maxZ);
}
public void updatePossibleStartingLocation(SimpleCollisionBox possibleLocationCombined) {
//GrimACBukkitLoaderPlugin.staticGetLogger().info(ChatColor.BLUE + "Updated new starting location as second trans hasn't arrived " + startingLocation);
this.startingLocation = combineCollisionBox(startingLocation, possibleLocationCombined);
@@ -7,6 +7,31 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import java.util.Objects;
public class VectorData {
public static final class MoveVectorData extends VectorData {
public int x;
public int z;
public MoveVectorData(Vector3dm vector, VectorData lastVector, VectorType vectorType, int x, int z) {
super(vector, lastVector, vectorType);
this.x = x;
this.z = z;
if (x != 0 || z != 0) {
addVectorType(VectorType.WithInput);
}
}
public MoveVectorData(Vector3dm vector, VectorType vectorType, int x, int z) {
super(vector, vectorType);
this.x = x;
this.z = z;
if (x != 0 || z != 0) {
addVectorType(VectorType.WithInput);
}
}
}
public final VectorType vectorType;
public VectorData lastVector;
public VectorData preUncertainty;
@@ -14,7 +39,7 @@ public class VectorData {
public @MonotonicNonNull Vector3dm input;
@Getter
private boolean isKnockback, firstBreadKb, isExplosion, firstBreadExplosion, isTrident, isZeroPointZeroThree, isSwimHop, isFlipSneaking, isFlipItem, isJump, isAttackSlow = false;
private boolean isKnockback, firstBreadKb, isExplosion, firstBreadExplosion, isTrident, isZeroPointZeroThree, isSwimHop, isFlipSneaking, isFlipItem, isJump, isAttackSlow = false, isWithInput = false;
// For handling replacing the type of vector it is while keeping data
public VectorData(Vector3dm vector, VectorData lastVector, VectorType vectorType) {
@@ -36,6 +61,7 @@ public class VectorData {
preUncertainty = lastVector.preUncertainty;
isAttackSlow = lastVector.isAttackSlow;
input = lastVector.input;
isWithInput = lastVector.isWithInput;
}
addVectorType(vectorType);
@@ -81,6 +107,7 @@ public class VectorData {
case Flip_Use_Item -> isFlipItem = true;
case Jump -> isJump = true;
case AttackSlow -> isAttackSlow = true;
case WithInput -> isWithInput = true;
}
}
@@ -107,6 +134,7 @@ public class VectorData {
Explosion,
FirstBreadExplosion,
InputResult,
WithInput,
StuckMultiplier,
Spectator,
Dead,
@@ -16,6 +16,7 @@
package ac.grim.grimac.utils.data.packetentity;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.collisions.datatypes.CollisionBox;
import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
import ac.grim.grimac.utils.data.ReachInterpolationData;
import ac.grim.grimac.utils.data.TrackedPosition;
@@ -229,6 +230,14 @@ public class PacketEntity extends TypedPacketEntity {
return ReachInterpolationData.combineCollisionBox(oldPacketLocation.getPossibleHitboxCombined(), newPacketLocation.getPossibleHitboxCombined());
}
public CollisionBox getMinimumPossibleCollisionBoxes() {
if (oldPacketLocation == null) {
return newPacketLocation.getOverlapHitboxCombined();
}
return ReachInterpolationData.getOverlapHitbox(oldPacketLocation.getOverlapHitboxCombined(), newPacketLocation.getOverlapHitboxCombined());
}
public OptionalInt getPotionEffectLevel(PotionType effect) {
final int amplifier = potionsMap == null ? -1 : potionsMap.getInt(effect);
return amplifier == -1 ? OptionalInt.empty() : OptionalInt.of(amplifier);
@@ -16,4 +16,11 @@ public class PacketEntityPainting extends PacketEntity {
super(player, uuid, EntityTypes.PAINTING, x, y, z);
this.direction = direction;
}
// This is incorrect, temporary measure to exempt paintings from HitboxEntity
// Will properly model later
@Override
public boolean canHit() {
return false;
}
}
@@ -0,0 +1,7 @@
package ac.grim.grimac.utils.inventory;
public enum InventoryDesyncStatus {
BEACON,
NETHER_PORTAL,
NOT_DESYNCED
}
@@ -287,9 +287,9 @@ public class CompensatedEntities {
if (sizeObject != null) {
Object value = sizeObject.getValue();
if (value instanceof Integer) {
sizeable.size = Math.max((int) value, 1);
sizeable.size = (int) value;
} else if (value instanceof Byte) {
sizeable.size = Math.max((byte) value, 1);
sizeable.size = (byte) value;
}
}
}
@@ -50,7 +50,7 @@ public class CompensatedInventory extends Check implements PacketCheck {
public boolean isPacketInventoryActive = true;
public boolean needResend = false;
public int stateID = 0; // Don't mess up the last sent state ID by changing it
private int openWindowID = 0;
public int openWindowID = 0;
// Special values:
// Player inventory is -1
// Unsupported inventory is -2
@@ -6,10 +6,7 @@ import ac.grim.grimac.utils.change.BlockModification;
import ac.grim.grimac.utils.chunks.Column;
import ac.grim.grimac.utils.collisions.CollisionData;
import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
import ac.grim.grimac.utils.data.BlockPrediction;
import ac.grim.grimac.utils.data.Pair;
import ac.grim.grimac.utils.data.PistonData;
import ac.grim.grimac.utils.data.ShulkerData;
import ac.grim.grimac.utils.data.*;
import ac.grim.grimac.utils.data.packetentity.PacketEntity;
import ac.grim.grimac.utils.data.packetentity.PacketEntityShulker;
import ac.grim.grimac.utils.math.GrimMath;
@@ -69,7 +66,7 @@ public class CompensatedWorld {
public final GrimPlayer player;
public final Long2ObjectMap<Column> chunks;
// Packet locations for blocks
public final Set<PistonData> activePistons = new HashSet<>();
public final Set<PlayerPistonData> activePistons = new HashSet<>();
public final Set<ShulkerData> openShulkerBoxes = new HashSet<>();
// 1.17 with datapacks, and 1.18, have negative world offset values
@Getter
@@ -238,8 +235,8 @@ public class CompensatedWorld {
}
// Pistons are a block entity.
for (PistonData data : activePistons) {
for (SimpleCollisionBox box : data.boxes) {
for (PlayerPistonData data : activePistons) {
for (SimpleCollisionBox box : data.pistonTemplate.boxes()) {
if (playerBox.isCollided(box)) {
return true;
}
@@ -358,18 +355,18 @@ public class CompensatedWorld {
double modY = 0;
double modZ = 0;
for (PistonData data : activePistons) {
for (SimpleCollisionBox box : data.boxes) {
for (PlayerPistonData data : activePistons) {
for (SimpleCollisionBox box : data.pistonTemplate.boxes()) {
if (playerBox.isCollided(box)) {
modX = Math.max(modX, Math.abs(data.direction.getModX() * 0.51D));
modY = Math.max(modY, Math.abs(data.direction.getModY() * 0.51D));
modZ = Math.max(modZ, Math.abs(data.direction.getModZ() * 0.51D));
modX = Math.max(modX, Math.abs(data.pistonTemplate.dir().getModX() * 0.51D));
modY = Math.max(modY, Math.abs(data.pistonTemplate.dir().getModY() * 0.51D));
modZ = Math.max(modZ, Math.abs(data.pistonTemplate.dir().getModZ() * 0.51D));
playerBox.expandMax(modX, modY, modZ);
playerBox.expandMin(modX * -1, modY * -1, modZ * -1);
if (data.hasSlimeBlock || (data.hasHoneyBlock && player.getClientVersion().isOlderThan(ClientVersion.V_1_15_2))) {
player.uncertaintyHandler.slimePistonBounces.add(data.direction);
if (data.pistonTemplate.slime() || (data.pistonTemplate.honey() && player.getClientVersion().isOlderThan(ClientVersion.V_1_15_2))) {
player.uncertaintyHandler.slimePistonBounces.add(data.pistonTemplate.dir());
}
break;
@@ -424,7 +421,7 @@ public class CompensatedWorld {
activePistons.removeIf(data -> data.lastTransactionSent < transactionId);
openShulkerBoxes.removeIf(data -> data.isClosing && data.lastTransactionSent < transactionId);
} else {
activePistons.removeIf(PistonData::tickIfGuaranteedFinished);
activePistons.removeIf(PlayerPistonData::tickIfGuaranteedFinished);
openShulkerBoxes.removeIf(ShulkerData::tickIfGuaranteedFinished);
}
// Remove if a shulker is not in this block position anymore
@@ -697,4 +694,8 @@ public class CompensatedWorld {
public WrappedBlockState getBlock(Vector3dm aboveCCWPos) {
return getBlock(aboveCCWPos.getX(), aboveCCWPos.getY(), aboveCCWPos.getZ());
}
public void addPiston(PistonTemplate pistonTemplate, int transactionID) {
activePistons.add(new PlayerPistonData(pistonTemplate, transactionID));
}
}
@@ -0,0 +1,28 @@
package ac.grim.grimac.utils.latency;
public interface ILatencyUtils {
/**
* Adds a task to be executed when the corresponding transaction ACK is received.
*
* @param transaction The transaction ID this task is associated with.
* @param runnable The task to execute.
*/
void addRealTimeTask(int transaction, Runnable runnable);
/**
* Adds a task to be executed asynchronously via the player's event loop
* when the corresponding transaction ACK is received.
* (Note: Benchmark might simplify/ignore the async part unless specifically testing event loop contention)
*
* @param transaction The transaction ID this task is associated with.
* @param runnable The task to execute.
*/
void addRealTimeTaskAsync(int transaction, Runnable runnable);
/**
* Processes received transaction ACKs and runs associated tasks.
*
* @param receivedTransactionId The ID of the transaction ACK received from the client.
*/
void handleNettySyncTransaction(int receivedTransactionId);
}
@@ -5,18 +5,17 @@ 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.common.arguments.CommonGrimArguments;
import ac.grim.grimac.utils.data.Pair;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.*;
public class LatencyUtils implements ILatencyUtils {
// Record to replace Pair with primitive int
private record TransactionTask(int transactionId, Runnable task) {}
private final ArrayDeque<TransactionTask> transactionMap = new ArrayDeque<>();
public class LatencyUtils {
private final LinkedList<Pair<Integer, Runnable>> transactionMap = new LinkedList<>();
private final GrimPlayer player;
// Built from transactionMap and cleared at start of every handleNettySyncTransaction() call
// The actual usage scope of this variable's use is limited to within the synchronized block of handleNettySyncTransaction
private final ArrayList<Runnable> tasksToRun = new ArrayList<>();
public LatencyUtils(GrimPlayer player) {
@@ -24,15 +23,15 @@ public class LatencyUtils {
}
public void addRealTimeTask(int transaction, Runnable runnable) {
addRealTimeTask(transaction, false, runnable);
addRealTimeTaskInternal(transaction, false, runnable);
}
public void addRealTimeTaskAsync(int transaction, Runnable runnable) {
addRealTimeTask(transaction, true, runnable);
addRealTimeTaskInternal(transaction, true, runnable);
}
public void addRealTimeTask(int transaction, boolean async, Runnable runnable) {
if (player.lastTransactionReceived.get() >= transaction) { // If the player already responded to this transaction
private void addRealTimeTaskInternal(int transactionId, boolean async, Runnable runnable) {
if (player.lastTransactionReceived.get() >= transactionId) {
if (async) {
player.runSafely(runnable);
} else {
@@ -40,62 +39,54 @@ public class LatencyUtils {
}
return;
}
synchronized (this) {
transactionMap.add(new Pair<>(transaction, runnable));
synchronized (transactionMap) {
transactionMap.add(new TransactionTask(transactionId, runnable));
}
}
public void handleNettySyncTransaction(int transaction) {
/*
* This code uses a two-pass approach within the synchronized block to prevent CMEs.
* First we collect and remove tasks using the iterator, then execute all collected tasks.
*
* The issue:
* We cannot execute tasks during iteration because if a runnable modifies transactionMap
* or calls addRealTimeTask, it will cause a ConcurrentModificationException.
* While only seen on Folia servers, this is theoretically possible everywhere.
*
* Why this solution:
* Rather than documenting "don't modify transactionMap in runnables" and risking subtle
* bugs from future contributions or Check API usage, we prevent the issue entirely
* at a small performance cost.
*
* Future considerations:
* If this becomes a performance bottleneck, we may revisit using a single-pass approach
* on non-Folia servers. We could also explore concurrent data structures or parallel
* execution, but this would lose the guarantee that transactions are processed in order.
*/
synchronized (this) {
@Override
public void handleNettySyncTransaction(int receivedTransactionId) {
synchronized (transactionMap) {
tasksToRun.clear();
// First pass: collect tasks and mark them for removal
ListIterator<Pair<Integer, Runnable>> iterator = transactionMap.listIterator();
Iterator<TransactionTask> iterator = transactionMap.iterator();
while (iterator.hasNext()) {
Pair<Integer, Runnable> pair = iterator.next();
TransactionTask taskEntry = iterator.next();
int taskTransactionId = taskEntry.transactionId();
// We are at most a tick ahead when running tasks based on transactions, meaning this is too far
if (transaction + 1 < pair.first())
// If tasks are added with monotonically increasing IDs,
// once we find one that's too far ahead, all subsequent ones
// will also be too far ahead.
if (receivedTransactionId + 1 < taskTransactionId) {
break;
}
// This is at most tick ahead of what we want
if (transaction == pair.first() - 1)
continue;
if (receivedTransactionId == taskTransactionId - 1) {
continue; // Skip this specific task
}
tasksToRun.add(pair.second());
iterator.remove();
// If we didn't break or continue, the task is eligible
tasksToRun.add(taskEntry.task());
iterator.remove(); // Remove using the iterator
}
// Task execution loop
for (Runnable runnable : tasksToRun) {
try {
runnable.run();
} catch (Exception e) {
LogUtil.error("An error has occurred when running transactions for player: " + player.user.getName(), e);
// Kick the player SO PEOPLE ACTUALLY REPORT PROBLEMS AND KNOW WHEN THEY HAPPEN
if (CommonGrimArguments.KICK_ON_TRANSACTION_ERRORS.value()) {
player.disconnect(MessageUtil.miniMessage(MessageUtil.replacePlaceholders(player, GrimAPI.INSTANCE.getConfigManager().getDisconnectPacketError())));
}
handleRunnableError(e);
}
}
}
}
private void handleRunnableError(Exception e) {
LogUtil.error("An error has occurred when running transactions for player: " + player.user.getName(), e);
// Kick the player SO PEOPLE ACTUALLY REPORT PROBLEMS AND KNOW WHEN THEY HAPPEN
if (CommonGrimArguments.KICK_ON_TRANSACTION_ERRORS.value()) {
player.disconnect(MessageUtil.miniMessage(MessageUtil.replacePlaceholders(player, GrimAPI.INSTANCE.getConfigManager().getDisconnectPacketError())));
}
}
}
@@ -51,7 +51,17 @@ public class GetBoundingBox {
double minZ = centerZ - (width / 2f);
double maxZ = centerZ + (width / 2f);
return new SimpleCollisionBox(minX, minY, minZ, maxX, maxY, maxZ, false);
// it's possible for width and/or height to be negative,
// correct the order of min and max
return new SimpleCollisionBox(
Math.min(minX, maxX),
Math.min(minY, maxY),
Math.min(minZ, maxZ),
Math.max(minX, maxX),
Math.max(minY, maxY),
Math.max(minZ, maxZ),
false
);
}
public static double @NotNull [] getEntityDimensions(GrimPlayer player, @NotNull PacketEntity entity) {
@@ -67,11 +77,20 @@ public class GetBoundingBox {
double height = dimensions[1];
double halfDepth = dimensions[2] / 2.0;
box.minX -= halfWidth;
box.minY -= 0; // No downward expansion
box.minZ -= halfDepth;
box.maxX += halfWidth;
box.maxY += height;
box.maxZ += halfDepth;
double minX = box.minX - halfWidth;
double minY = box.minY; // No downward expansion
double minZ = box.minZ - halfDepth;
double maxX = box.maxX + halfWidth;
double maxY = box.maxY + height;
double maxZ = box.maxZ + halfDepth;
// it's possible for width and/or height to be negative,
// correct the order of min and max
box.minX = Math.min(minX, maxX);
box.minY = Math.min(minY, maxY);
box.minZ = Math.min(minZ, maxZ);
box.maxX = Math.max(minX, maxX);
box.maxY = Math.max(minY, maxY);
box.maxZ = Math.max(minZ, maxZ);
}
}
@@ -1,12 +1,18 @@
package ac.grim.grimac.utils.nmsutil;
import ac.grim.grimac.checks.impl.combat.Reach;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.collisions.CollisionData;
import ac.grim.grimac.utils.collisions.HitboxData;
import ac.grim.grimac.utils.collisions.datatypes.CollisionBox;
import ac.grim.grimac.utils.collisions.datatypes.ComplexCollisionBox;
import ac.grim.grimac.utils.collisions.datatypes.NoCollisionBox;
import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
import ac.grim.grimac.utils.data.BlockHitData;
import ac.grim.grimac.utils.data.EntityHitData;
import ac.grim.grimac.utils.data.HitData;
import ac.grim.grimac.utils.data.Pair;
import ac.grim.grimac.utils.data.packetentity.PacketEntity;
import ac.grim.grimac.utils.math.GrimMath;
import ac.grim.grimac.utils.math.Vector3dm;
import com.github.retrooper.packetevents.protocol.attribute.Attributes;
@@ -16,6 +22,8 @@ import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState
import com.github.retrooper.packetevents.protocol.world.states.type.StateType;
import com.github.retrooper.packetevents.util.Vector3d;
import com.github.retrooper.packetevents.util.Vector3i;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import lombok.experimental.UtilityClass;
import java.util.ArrayList;
@@ -24,7 +32,7 @@ import java.util.function.BiFunction;
@UtilityClass
public class WorldRayTrace {
public static HitData getNearestBlockHitResult(GrimPlayer player, StateType heldItem, boolean sourcesHaveHitbox, boolean fluidPlacement, boolean itemUsePlacement) {
public static BlockHitData getNearestBlockHitResult(GrimPlayer player, StateType heldItem, boolean sourcesHaveHitbox, boolean fluidPlacement, boolean itemUsePlacement) {
Vector3d startingPos = new Vector3d(player.x, player.y + player.getEyeHeight(), player.z);
Vector3dm startingVec = new Vector3dm(startingPos.getX(), startingPos.getY(), startingPos.getZ());
Ray trace = new Ray(player, startingPos.getX(), startingPos.getY(), startingPos.getZ(), player.yaw, player.pitch);
@@ -59,7 +67,7 @@ public class WorldRayTrace {
}
}
if (bestHitLoc != null) {
return new HitData(vector3i, bestHitLoc, bestFace, block);
return new BlockHitData(vector3i, bestHitLoc, bestFace, block);
}
if (sourcesHaveHitbox &&
@@ -72,7 +80,7 @@ public class WorldRayTrace {
Pair<Vector3dm, BlockFace> intercept = ReachUtils.calculateIntercept(box, trace.getOrigin(), trace.getPointAtDistance(distance));
if (intercept.first() != null) {
return new HitData(vector3i, intercept.first(), intercept.second(), block);
return new BlockHitData(vector3i, intercept.first(), intercept.second(), block);
}
}
@@ -85,7 +93,7 @@ public class WorldRayTrace {
//
// I do have to admit that I'm starting to like bifunctions/new java 8 things more than I originally did.
// although I still don't understand Mojang's obsession with streams in some of the hottest methods... that kills performance
public static HitData traverseBlocks(GrimPlayer player, Vector3d start, Vector3d end, BiFunction<WrappedBlockState, Vector3i, HitData> predicate) {
public static BlockHitData traverseBlocks(GrimPlayer player, Vector3d start, Vector3d end, BiFunction<WrappedBlockState, Vector3i, BlockHitData> predicate) {
// I guess go back by the collision epsilon?
double endX = GrimMath.lerp(-1.0E-7D, end.x, start.x);
double endY = GrimMath.lerp(-1.0E-7D, end.y, start.y);
@@ -101,7 +109,7 @@ public class WorldRayTrace {
if (start.equals(end)) return null;
WrappedBlockState state = player.compensatedWorld.getBlock(floorStartX, floorStartY, floorStartZ);
HitData apply = predicate.apply(state, new Vector3i(floorStartX, floorStartY, floorStartZ));
BlockHitData apply = predicate.apply(state, new Vector3i(floorStartX, floorStartY, floorStartZ));
if (apply != null) {
return apply;
@@ -150,4 +158,162 @@ public class WorldRayTrace {
return null;
}
@Nullable
public static HitData getNearestHitResult(GrimPlayer player, PacketEntity targetEntity, Vector3dm eyePos, Vector3dm lookVec) {
double maxAttackDistance = player.compensatedEntities.self.getAttributeValue(Attributes.ENTITY_INTERACTION_RANGE);
double maxBlockDistance = player.compensatedEntities.self.getAttributeValue(Attributes.BLOCK_INTERACTION_RANGE);
Vector3d startingPos = new Vector3d(eyePos.getX(), eyePos.getY(), eyePos.getZ());
Vector3dm startingVec = new Vector3dm(startingPos.getX(), startingPos.getY(), startingPos.getZ());
Ray trace = new Ray(eyePos, lookVec);
Vector3dm endVec = trace.getPointAtDistance(maxBlockDistance);
Vector3d endPos = new Vector3d(endVec.getX(), endVec.getY(), endVec.getZ());
// Get block hit
BlockHitData blockHitData = getTraverseResult(player, null, startingPos, startingVec, trace, endPos, false, true, maxBlockDistance, true);
Vector3dm closestHitVec = null;
PacketEntity closestEntity = null;
double closestDistanceSquared = blockHitData != null ? blockHitData.getBlockHitLocation().distanceSquared(startingVec) : maxAttackDistance * maxAttackDistance;
for (PacketEntity entity : player.compensatedEntities.entityMap.values().stream().filter(PacketEntity::canHit).toList()) {
SimpleCollisionBox box = null;
// 1.7 and 1.8 players get a bit of extra hitbox (this is why you should use 1.8 on cross version servers)
// Yes, this is vanilla and not uncertainty. All reach checks have this or they are wrong.
if (entity.equals(targetEntity)) {
box = entity.getPossibleCollisionBoxes();
box.expand(player.checkManager.getPacketCheck(Reach.class).threshold);
// This is better than adding to the reach, as 0.03 can cause a player to miss their target
// Adds some more than 0.03 uncertainty in some cases, but a good trade off for simplicity
//
// Just give the uncertainty on 1.9+ clients as we have no way of knowing whether they had 0.03 movement
if (!player.packetStateData.didLastLastMovementIncludePosition || player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9))
box.expand(player.getMovementThreshold());
if (ReachUtils.isVecInside(box, eyePos)) {
return new EntityHitData(entity, eyePos);
}
} else {
CollisionBox b = entity.getMinimumPossibleCollisionBoxes();
if (b instanceof NoCollisionBox) {
continue;
}
box = (SimpleCollisionBox) b;
box.expand(-player.checkManager.getPacketCheck(Reach.class).threshold);
// todo, shrink by reachThreshold as well for non-target entities?
if (!player.packetStateData.didLastLastMovementIncludePosition || player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9))
box.expand(-player.getMovementThreshold());
}
if (player.getClientVersion().isOlderThan(ClientVersion.V_1_9)) {
box.expand(0.1f);
}
Pair<Vector3dm, BlockFace> intercept = ReachUtils.calculateIntercept(box, trace.getOrigin(), trace.getPointAtDistance(Math.sqrt(closestDistanceSquared)));
if (intercept.first() != null) {
double distSquared = intercept.first().distanceSquared(startingVec);
if (distSquared < closestDistanceSquared) {
closestDistanceSquared = distSquared;
closestHitVec = intercept.first();
closestEntity = entity;
}
}
}
return closestEntity == null ? blockHitData : new EntityHitData(closestEntity, closestHitVec);
}
// Checks if it was possible to hit a target entity
// TODO refactor to return list of rays and why each of them didn't hit instead of closest obstruction
// NOTE: It should be impossible for the returned Pair to be null
// because all of the possibleLookVecsAndEyeHeights passed in should be ones that hit the target entity
// 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) {
HitData firstObstruction = null;
double firstObstructionDistanceSq = 0;
// Check every possible look direction and every possible eye height
for (Pair<Vector3dm, Double> vectorDoublePair : possibleLookVecsAndEyeHeights) {
Vector3dm lookVec = vectorDoublePair.first();
double eye = vectorDoublePair.second();
Vector3dm eyes = new Vector3dm(from.getX(), from.getY() + eye, from.getZ());
// this function is completely 0.03 aware
final HitData hitResult = WorldRayTrace.getNearestHitResult(player, targetEntity, eyes, lookVec);
// If we hit the target entity, it's a valid hit
if (hitResult instanceof EntityHitData && ((EntityHitData) hitResult).getEntity().equals(targetEntity)) {
double distanceSquared = eyes.distanceSquared(hitResult.getBlockHitLocation());
return new Pair<>(distanceSquared, hitResult); // Legitimate hit
} else if (hitResult != null && firstObstruction == null) {
// Store the first obstruction only
firstObstruction = hitResult;
firstObstructionDistanceSq = eyes.distanceSquared(hitResult.getBlockHitLocation());
}
}
// Return the first obstruction if no valid hit found
// Since we sort eye heights by likeniness, we should in effect return the most likely (first) obstruction
assert firstObstruction != null;
return new Pair<>(firstObstructionDistanceSq, firstObstruction);
}
// TODO replace shrinkBlocks boolean with a data structure/better way to represent
// 1. We have a target block. Shrink everything by movementThreshold except expand target block (we are checking to see if it matches the target block)
// 2. We do not have a target block. Shrink everything by movementThreshold()
// 3. Do not expand or shrink everything, we do not expect 0.03/0.002 or we legacy example where we want to keep old behaviour
private static BlockHitData getTraverseResult(GrimPlayer player, @Nullable StateType heldItem, Vector3d startingPos, Vector3dm startingVec, Ray trace, Vector3d endPos, boolean sourcesHaveHitbox, boolean checkInside, double knownDistance, boolean shrinkBlocks) {
return traverseBlocks(player, startingPos, endPos, (block, vector3i) -> {
// even though sometimes we are raytracing against a block that is the target block, we pass false to this function because it only applies a change for brewing stands in 1.8
CollisionBox data = HitboxData.getBlockHitbox(player, heldItem, player.getClientVersion(), block, false, vector3i.getX(), vector3i.getY(), vector3i.getZ());
SimpleCollisionBox[] boxes = new SimpleCollisionBox[ComplexCollisionBox.DEFAULT_MAX_COLLISION_BOX_SIZE];
int size = data.downCast(boxes);
double bestHitResult = Double.MAX_VALUE;
Vector3dm bestHitLoc = null;
BlockFace bestFace = null;
for (int i = 0; i < size; i++) {
if (shrinkBlocks) boxes[i].expand(-player.getMovementThreshold());
Pair<Vector3dm, BlockFace> intercept = ReachUtils.calculateIntercept(boxes[i], trace.getOrigin(), trace.getPointAtDistance(knownDistance));
if (intercept.first() == null) continue; // No intercept
Vector3dm hitLoc = intercept.first();
// If inside a block, return empty result for reach check (don't bother checking this?)
if (checkInside && ReachUtils.isVecInside(boxes[i], trace.getOrigin())) {
return null;
}
if (hitLoc.distanceSquared(startingVec) < bestHitResult) {
bestHitResult = hitLoc.distanceSquared(startingVec);
bestHitLoc = hitLoc;
bestFace = intercept.second();
}
}
if (bestHitLoc != null) {
return new BlockHitData(vector3i, bestHitLoc, bestFace, block);
}
if (sourcesHaveHitbox &&
(player.compensatedWorld.isWaterSourceBlock(vector3i.getX(), vector3i.getY(), vector3i.getZ())
|| player.compensatedWorld.getLavaFluidLevelAt(vector3i.getX(), vector3i.getY(), vector3i.getZ()) == (8 / 9f))) {
double waterHeight = player.compensatedWorld.getFluidLevelAt(vector3i.getX(), vector3i.getY(), vector3i.getZ());
SimpleCollisionBox box = new SimpleCollisionBox(vector3i.getX(), vector3i.getY(), vector3i.getZ(), vector3i.getX() + 1, vector3i.getY() + waterHeight, vector3i.getZ() + 1);
Pair<Vector3dm, BlockFace> intercept = ReachUtils.calculateIntercept(box, trace.getOrigin(), trace.getPointAtDistance(knownDistance));
if (intercept.first() != null) {
return new BlockHitData(vector3i, intercept.first(), intercept.second(), block);
}
}
return null;
});
}
}
@@ -0,0 +1,27 @@
package ac.grim.grimac.utils.payload;
import com.github.retrooper.packetevents.netty.buffer.ByteBufHelper;
import com.github.retrooper.packetevents.wrapper.PacketWrapper;
import org.jetbrains.annotations.ApiStatus.NonExtendable;
import org.jetbrains.annotations.ApiStatus.OverrideOnly;
import org.jetbrains.annotations.NotNull;
import static com.github.retrooper.packetevents.netty.buffer.UnpooledByteBufAllocationHelper.buffer;
import static com.github.retrooper.packetevents.netty.buffer.UnpooledByteBufAllocationHelper.wrappedBuffer;
import static com.github.retrooper.packetevents.wrapper.PacketWrapper.createUniversalPacketWrapper;
public interface Payload {
@NonExtendable
default byte[] write() {
Object buffer = buffer();
write(createUniversalPacketWrapper(buffer));
return ByteBufHelper.array(buffer);
}
@OverrideOnly
void write(PacketWrapper<?> wrapper);
static @NotNull PacketWrapper<?> wrapper(byte[] data) {
return createUniversalPacketWrapper(wrappedBuffer(data));
}
}
@@ -1,13 +1,20 @@
package ac.grim.grimac.utils.payload;
import com.github.retrooper.packetevents.protocol.item.ItemStack;
import com.github.retrooper.packetevents.wrapper.PacketWrapper;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientEditBook;
import org.jetbrains.annotations.NotNull;
/**
* Payload wrapper for {@code MC|BEdit} and {@code MC|BSign}
* Payload wrapper for serverbound {@code MC|BEdit} and {@code MC|BSign}, replaced by the {@link WrapperPlayClientEditBook EDIT_BOOK} packet in 1.13
*/
public record PayloadBookEdit(ItemStack itemStack) {
public static final PayloadCodec<PayloadBookEdit> CODEC = new PayloadCodec<>(
wrapper -> new PayloadBookEdit(wrapper.readItemStack()),
(wrapper, payload) -> wrapper.writeItemStack(payload.itemStack)
);
public record PayloadBookEdit(@NotNull ItemStack itemStack) implements Payload {
public PayloadBookEdit(byte[] data) {
this(Payload.wrapper(data).readItemStack());
}
@Override
public void write(PacketWrapper<?> wrapper) {
wrapper.writeItemStack(itemStack);
}
}
@@ -1,39 +0,0 @@
package ac.grim.grimac.utils.payload;
import com.github.retrooper.packetevents.netty.buffer.ByteBufHelper;
import com.github.retrooper.packetevents.wrapper.PacketWrapper;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.util.function.BiConsumer;
import java.util.function.Function;
import static com.github.retrooper.packetevents.netty.buffer.UnpooledByteBufAllocationHelper.buffer;
import static com.github.retrooper.packetevents.netty.buffer.UnpooledByteBufAllocationHelper.copiedBuffer;
import static com.github.retrooper.packetevents.wrapper.PacketWrapper.createUniversalPacketWrapper;
public final class PayloadCodec<P> {
private final Function<PacketWrapper<?>, P> reader;
private final BiConsumer<PacketWrapper<?>, P> writer;
@Contract(pure = true)
public PayloadCodec(Function<PacketWrapper<?>, P> reader, BiConsumer<PacketWrapper<?>, P> writer) {
this.reader = reader;
this.writer = writer;
}
public P read(byte[] data) {
Object buffer = copiedBuffer(data);
P payload = reader.apply(createUniversalPacketWrapper(buffer));
ByteBufHelper.release(buffer);
return payload;
}
public byte @NotNull [] write(P payload) {
Object buffer = buffer();
writer.accept(createUniversalPacketWrapper(buffer), payload);
byte[] bytes = ByteBufHelper.array(buffer);
ByteBufHelper.release(buffer);
return bytes;
}
}
@@ -1,10 +1,19 @@
package ac.grim.grimac.utils.payload;
import com.github.retrooper.packetevents.wrapper.PacketWrapper;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientNameItem;
import org.jetbrains.annotations.NotNull;
public record PayloadItemName(@NotNull String itemName) {
public static final PayloadCodec<PayloadItemName> CODEC = new PayloadCodec<>(
wrapper -> new PayloadItemName(wrapper.readString()),
(wrapper, payload) -> wrapper.writeString(payload.itemName)
);
/**
* Payload wrapper for serverbound {@code MC|ItemName}, replaced by the {@link WrapperPlayClientNameItem NAME_ITEM} packet in 1.13
*/
public record PayloadItemName(@NotNull String itemName) implements Payload {
public PayloadItemName(byte[] data) {
this(Payload.wrapper(data).readString());
}
@Override
public void write(PacketWrapper<?> wrapper) {
wrapper.writeString(itemName);
}
}
@@ -1,19 +1,34 @@
package ac.grim.grimac.utils.via;
package ac.grim.grimac.utils.viaversion;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.checks.impl.chat.ChatB;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.LogUtil;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.viaversion.viaversion.api.Via;
import com.viaversion.viaversion.api.type.Types;
import com.viaversion.viaversion.protocols.v1_10to1_11.Protocol1_10To1_11;
import com.viaversion.viaversion.protocols.v1_9_1to1_9_3.packet.ServerboundPackets1_9_3;
import lombok.experimental.UtilityClass;
import java.util.UUID;
public class ViaHooks {
public static void injectVia() {
final var protocol = Via.getManager().getProtocolManager().getProtocol(Protocol1_10To1_11.class);
@UtilityClass
class ViaVersionHooks {
// this method's only purpose is getting <clinit> called
static void load() {}
static {
inject1_11ChatHook();
}
private static void inject1_11ChatHook() {
if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_11)) {
return; // not needed
}
final Protocol1_10To1_11 protocol = Via.getManager().getProtocolManager().getProtocol(Protocol1_10To1_11.class);
if (protocol == null) {
LogUtil.warn("Failed to inject ViaVersion message hook for 1.11+ clients: Protocol1_10To1_11 isn't registered!");
return;
@@ -1,6 +1,7 @@
package ac.grim.grimac.utils.reflection;
package ac.grim.grimac.utils.viaversion;
import ac.grim.grimac.utils.anticheat.LogUtil;
import ac.grim.grimac.utils.reflection.ReflectionUtils;
import lombok.experimental.UtilityClass;
@UtilityClass
@@ -12,4 +13,8 @@ public class ViaVersionUtil {
LogUtil.error("Using unsupported ViaVersion 4.0 API, update ViaVersion to 5.0");
}
}
public static void injectHooks() {
if (isAvailable) ViaVersionHooks.load();
}
}
+229
View File
@@ -0,0 +1,229 @@
# Configurația principală GrimAC
# Cod sursă: https://github.com/MWHunter/Grim
# Drepturi de autor 2025 DefineOutside și colaboratorii, licențiat sub GPLv3.
# Binarele modificate sau pluginurile cu cod Grim copiat trebuie să fie private,
# sau cu întregul cod sursă disponibil cumpărătorilor fără costuri suplimentare.
alerts:
# Pe lângă transmiterea alertelor către jucători, ar trebui să fie trimise și în consolă?
print-to-console: true
# Aceasta controlează dacă/și cum sunt partajate alertele între serverele conectate la proxy.
# Trebuie să ai 'bungee-plugin-message-channel' activat în configurația Velocity dacă Velocity este folosit.
proxy:
# Ar trebui ca alertele să fie trimise către alte servere conectate la proxy?
send: false
# Ar trebui ca alertele primite de la alte servere să fie anunțate pe acest server?
receive: false
verbose:
print-to-console: false
check-for-updates: true
client-brand:
# Aceasta înseamnă că nu va transmite operatorilor brandul clientului dacă brandul se potrivește cu regex-urile de mai jos
ignored-clients:
- "^vanilla$"
- "^fabric$"
- "^lunarclient:v\\d+\\.\\d+\\.\\d+-\\d{4}$"
- "^Feather Fabric$"
- "^labymod$"
# Grim va bloca anumite versiuni Forge care includ hackuri Reach integrate (Forge 1.18.2 până la 1.19.3).
# Setarea acestei opțiuni la false va permite acestor clienți să se conecteze la server. Dezactivează pe propriul risc.
disconnect-blacklisted-forge-versions: true
spectators:
# Ascunde toți spectatorii cu permisiunea grim.spectator indiferent dacă spectează activ sau nu
hide-regardless: false
# Va face spectatorii invizibili doar în aceste lumi, lasă gol pentru a permite în toate lumile
allowed-worlds:
- ""
# Cât timp au jucătorii până să fie deconectați pentru timeout? Implicit = 60 secunde
max-transaction-time: 60
# Activează pentru a opri Grim din a anula pongs primite de la jucători.
# Poate îmbunătăți compatibilitatea cu alte anticheaturi. Poate cauza probleme cu limitatoarele de pachete.
# Nu activa dacă nu ești sigur ce faci.
disable-pong-cancelling: false
# Ar trebui să fie anulat pachetul de mișcare duplicat?
# Mojang a reparat această problemă în 1.21. A fost încercarea lor de a rezolva „bucket desync”. https://bugs.mojang.com/browse/MC-12363
# Această setare se aplică doar clienților 1.17-1.20.5 pe servere 1.8.
cancel-duplicate-packet: true
# Dacă să se ignore rotația în pachetele duplicate
ignore-duplicate-packet-rotation: false
Simulation:
# Cu cât ar trebui să înmulțim avantajul total atunci când jucătorul este legitim
# Așa arată configurația implicită (axa X = secunde, axa Y = 1/1000 bloc): https://www.desmos.com/calculator/d4ufgxrxer
setback-decay-multiplier: 0.999
# Cât de mare trebuie să fie abaterea de la mișcarea jucătorului pentru a crea o încălcare?
# Măsurat în blocuri față de mișcarea posibilă
# Luăm în considerare Optifine schimbând tabelele trig, dar scăderea la 0.001 va reduce FastMath
# și ar putea marca anticheatul dacă această compensație nu funcționează...
threshold: 0.001
# Cât de mare trebuie să fie încălcarea într-un tick pentru a da setback imediat?
# -1 pentru a dezactiva
immediate-setback-threshold: 0.1
# Cât de mare avantaj acumulat înainte să începem setback?
# -1 pentru a dezactiva
max-advantage: 1
# După 50 de secunde cu setările implicite, jucătorul va trece de la 4 blocuri -> 1 bloc avantaj
# Aceasta previne acumularea prea multor încălcări fără șansă de reset
# Grafic implicit avantaj (axa X = secunde, axa Y = 1/1000 bloc): https://www.desmos.com/calculator/4lovswdarj
max-ceiling: 4
# Pragul de încălcare pentru setback
# 1 pentru comportamentul vechi
setback-violation-threshold: 1
# Verifică dacă un jucător a intrat într-un bloc în timpul mișcării
Phase:
setbackvl: 1 # Glitch-ul în blocuri poate permite escaladarea pereților, iar acest check e relativ stabil
decay: 0.005
AirLiquidPlace:
cancelvl: 0
FabricatedPlace:
cancelvl: 5
FarPlace:
cancelvl: 5
PositionPlace:
cancelvl: 5
RotationPlace:
cancelvl: 5
# Verificare NoSlow bazată pe predicție
# Grim ține cont de netcode-ul buguit aici... greu de fals chiar și când spamezi click dreapta și butonul offhand
# Mult mai stabil decât alte anticheaturi, dar raportează orice falsuri... am reparat multe probleme de netcode aici.
NoSlow:
# Cât de mare abatere e considerată „cheating”
# Implicit este mai mică decât alte offs
# Marchează între 0.03-0.2 constant cu NoSlow activ
threshold: 0.001
# Setback rapid la primul obiect pentru a elimina orice avantaj dat de NoSlow
setbackvl: 5
# Scade atunci când jucătorul folosește un item ȘI este încetinit de acesta
decay: 0.05
Knockback:
# Cu cât ar trebui să înmulțim avantajul total atunci când jucătorul este legitim
setback-decay-multiplier: 0.999
# Cât de mare trebuie să fie abaterea față de viteza jucătorului pentru a crea o încălcare?
# Măsurat în blocuri față de viteza posibilă
threshold: 0.001
# Cât de mare trebuie să fie încălcarea într-un tick pentru setback imediat?
# -1 pentru a dezactiva
immediate-setback-threshold: 0.1
# Cât de mare avantaj acumulat înainte să începem setback?
# -1 pentru a dezactiva
max-advantage: 1
# Previne acumularea prea multor încălcări imposibil de resetat
max-ceiling: 4
Explosion:
threshold: 0.001
setbackvl: 3
TimerA:
setbackvl: 10
# Milisecunde pe care jucătorul le poate acumula pentru folosire ulterioară când rămâne în urmă
# Poate permite bypass-uri fast use/fast heal/fast bow pe 1.8 dacă e setat prea mare, 120 ms e un echilibru bun
drift: 120
# Acest check limitează abuzul TimerA prevenind mișcarea jucătorului să rămână prea mult în urmă față de timpul real
TimerLimit:
# Ping la care check-ul începe să limiteze balanța timer, pentru a preveni abuzul.
# Poate cauza setback pentru jucători legitimi doar dacă sunt peste acest prag de ping.
# -1 pentru a dezactiva
ping-abuse-limit-threshold: 1000
NegativeTimer:
# Număr de milisecunde pierdute în timp ce se mișcă înainte de a începe să marcheze
drift: 1200
# Aceeași metodă de verificare ca TimerA, dar pentru vehicule
VehicleTimer:
# Țintă timer 1.005
setbackvl: 10
PacketOrderI:
# activează dacă jucătorii sunt marcați folosind moduri de animații 1.7
exempt-placing-while-digging: false
Reach:
# Cu cât să mărim hitboxurile? 0.0005 ar trebui să detecteze reach 3.0005+
#
# Există o extindere forțată de 0.03 cu clienți 1.9-1.18.1 (nu 1.18.2), sau unele combinații client/server datorită
# schimbărilor și limitărilor protocolului. Acest check este cel mai puternic cu clienți 1.7/1.8 pe servere 1.8.
threshold: 0.0005
# Ar trebui să anulăm loviturile imposibile?
# Lovituri de 3.00-3.03 pot trece dar tot vor fi marcate, din cauza limitărilor de ordine a pachetelor
block-impossible-hits: true
# Va trimite un pachet suplimentar la sfârșitul fiecărui tick pentru a crește șansele de a prinde cheat-uri
# Injectează în lista de conexiuni a serverului pentru a trimite un pachet final chiar înainte ca serverul să facă flush
# Activarea va crește traficul pentru toți jucătorii
# Nu va scădea performanța serverului
# Activarea va prinde mai mulți cheateri.
# Lăsarea dezactivată va prinde totuși cheateri și nu va cauza false positive
# Dacă nu ești server PvP 1.8, acest pachet suplimentar nu este recomandat
enable-post-packet: false
exploit:
# Poți obține viteze mari sărind sprint cu elytra, aceasta previne exploitul dacă e setat pe false
# Mojang a stricat netcode-ul făcând startul elytra client-side și finalul server-side
# Elytra are fricțiune orizontală 0.99, așa că adăugând constant 0.2 viteză rezultă în viteze foarte mari.
allow-sprint-jumping-when-using-elytra: true
# Această opțiune reduce efectul ghostblocks prin resincronizarea jucătorului când se întâmplă
allow-building-on-ghostblocks: true
# Această setare, influențată de booleanul de mai sus, definește distanța verificată pentru ghostblocks
# Interval valid: 2 până la 4
distance-to-check-if-ghostblocks: 2
# Activează logarea pluginurilor care s-au injectat în netty la intrare pentru debugging de compatibilitate
debug-pipeline-on-join: false
# Activează verificări experimentale
experimental-checks: false
reset-item-usage-on-item-update: true
reset-item-usage-on-attack: true
reset-item-usage-on-slot-change: true
# Grim uneori anulează pachete ilegale precum timer, după X pachete anulate într-o secundă, când să
# deconectăm jucătorul? Este necesar deoarece unele limitatoare de pachete nu numără pachetele anulate de Grim.
packet-spam-threshold: 100
# Activează pentru a afișa stacktrace când un jucător e deconectat din cauza packet-spam-threshold
debug-packet-cancel: false
# Grim poate aplica ca un jucător scos din starea de zbor să nu aibă mai mult de X ms ping
# Deoarece Grim nu verifică în prezent jucătorii care zboară
# Pentru dezactivare, folosește -1
max-ping-out-of-flying: 1000
# Ping maxim când boost-ul cu artificii este eliminat de la jucător.
# Previne jucătorii cu latență mare să folosească un boost cu artificii la infinit cu elytra.
max-ping-firework-boost: 1000
history:
enabled: true
# Câte înregistrări să fie afișate pe pagină cu /grim history <player>
entries-per-page: 15
# Ce nume de server să fie inserat pentru comanda history? Util dacă folosești aceeași bază de date pentru mai multe servere
server-name: Prison
database:
# Folosește SQLITE pentru stocare locală, MYSQL dacă ai bază de date externă. Se actualizează doar la restart server
type: SQLITE
# Detalii conexiune MySQL
host: localhost
port: 3306
database: grim
username: root
password: ""
config-version: 9
@@ -63,6 +63,15 @@ Punishments:
- "20:20 [log]"
- "40:40 [webhook]"
- "40:40 [proxy]"
Inventory:
remove-violations-after: 300
checks:
- "Inventory"
commands:
- "10:10 [alert]"
- "10:10 [log]"
- "20:20 [webhook]"
- "20:20 [proxy]"
Reach:
remove-violations-after: 300
checks:
@@ -81,6 +90,24 @@ Punishments:
- "5:3 [log]"
- "5:3 [webhook]"
- "5:3 [proxy]"
WallHit:
remove-violations-after: 300
checks:
- "WallHit"
commands:
- "10:5 [alert]"
- "10:5 [webhook]"
- "10:5 [proxy]"
- "10:5 [log]"
EntityPierce:
remove-violations-after: 300
checks:
- "EntityPierce"
commands:
- "15:10 [alert]"
- "15:10 [webhook]"
- "15:10 [proxy]"
- "15:10 [log]"
Misc:
remove-violations-after: 300
checks:
@@ -63,6 +63,15 @@ Punishments:
- "20:20 [log]"
- "40:40 [webhook]"
- "40:40 [proxy]"
Inventory:
remove-violations-after: 300
checks:
- "Inventory"
commands:
- "10:10 [alert]"
- "10:10 [log]"
- "20:20 [webhook]"
- "20:20 [proxy]"
Reach:
remove-violations-after: 300
checks:
@@ -81,6 +90,24 @@ Punishments:
- "5:3 [log]"
- "5:3 [webhook]"
- "5:3 [proxy]"
WallHit:
remove-violations-after: 300
checks:
- "WallHit"
commands:
- "10:5 [alert]"
- "10:5 [webhook]"
- "10:5 [proxy]"
- "10:5 [log]"
EntityPierce:
remove-violations-after: 300
checks:
- "EntityPierce"
commands:
- "15:10 [alert]"
- "15:10 [webhook]"
- "15:10 [proxy]"
- "15:10 [log]"
Misc:
remove-violations-after: 300
checks:
@@ -63,6 +63,15 @@ Punishments:
- "20:20 [log]"
- "40:40 [webhook]"
- "40:40 [proxy]"
Inventory:
remove-violations-after: 300
checks:
- "Inventory"
commands:
- "10:10 [alert]"
- "10:10 [log]"
- "20:20 [webhook]"
- "20:20 [proxy]"
Reach:
remove-violations-after: 300
checks:
@@ -81,6 +90,24 @@ Punishments:
- "5:3 [log]"
- "5:3 [webhook]"
- "5:3 [proxy]"
WallHit:
remove-violations-after: 300
checks:
- "WallHit"
commands:
- "10:5 [alert]"
- "10:5 [webhook]"
- "10:5 [proxy]"
- "10:5 [log]"
EntityPierce:
remove-violations-after: 300
checks:
- "EntityPierce"
commands:
- "15:10 [alert]"
- "15:10 [webhook]"
- "15:10 [proxy]"
- "15:10 [log]"
Misc:
remove-violations-after: 300
checks:
@@ -63,6 +63,15 @@ Punishments:
- "20:20 [log]"
- "40:40 [webhook]"
- "40:40 [proxy]"
Inventory:
remove-violations-after: 300
checks:
- "Inventory"
commands:
- "10:10 [alert]"
- "10:10 [log]"
- "20:20 [webhook]"
- "20:20 [proxy]"
Reach:
remove-violations-after: 300
checks:
@@ -81,6 +90,24 @@ Punishments:
- "5:3 [log]"
- "5:3 [webhook]"
- "5:3 [proxy]"
WallHit:
remove-violations-after: 300
checks:
- "WallHit"
commands:
- "10:5 [alert]"
- "10:5 [webhook]"
- "10:5 [proxy]"
- "10:5 [log]"
EntityPierce:
remove-violations-after: 300
checks:
- "EntityPierce"
commands:
- "15:10 [alert]"
- "15:10 [webhook]"
- "15:10 [proxy]"
- "15:10 [log]"
Misc:
remove-violations-after: 300
checks:
@@ -50,6 +50,15 @@ Punishments:
- "20:20 [log]"
- "40:40 [webhook]"
- "40:40 [proxy]"
Inventory:
remove-violations-after: 300
checks:
- "Inventory"
commands:
- "10:10 [alert]"
- "10:10 [log]"
- "20:20 [webhook]"
- "20:20 [proxy]"
Reach:
remove-violations-after: 300
checks:
@@ -68,6 +77,24 @@ Punishments:
- "5:3 [log]"
- "5:3 [webhook]"
- "5:3 [proxy]"
WallHit:
remove-violations-after: 300
checks:
- "WallHit"
commands:
- "10:5 [alert]"
- "10:5 [webhook]"
- "10:5 [proxy]"
- "10:5 [log]"
EntityPierce:
remove-violations-after: 300
checks:
- "EntityPierce"
commands:
- "15:10 [alert]"
- "15:10 [webhook]"
- "15:10 [proxy]"
- "15:10 [log]"
Misc:
remove-violations-after: 300
checks:
@@ -63,6 +63,15 @@ Punishments:
- "20:20 [log]"
- "40:40 [webhook]"
- "40:40 [proxy]"
Inventory:
remove-violations-after: 300
checks:
- "Inventory"
commands:
- "10:10 [alert]"
- "10:10 [log]"
- "20:20 [webhook]"
- "20:20 [proxy]"
Reach:
remove-violations-after: 300
checks:
@@ -81,6 +90,24 @@ Punishments:
- "5:3 [log]"
- "5:3 [webhook]"
- "5:3 [proxy]"
WallHit:
remove-violations-after: 300
checks:
- "WallHit"
commands:
- "10:5 [alert]"
- "10:5 [webhook]"
- "10:5 [proxy]"
- "10:5 [log]"
EntityPierce:
remove-violations-after: 300
checks:
- "EntityPierce"
commands:
- "15:10 [alert]"
- "15:10 [webhook]"
- "15:10 [proxy]"
- "15:10 [log]"
Misc:
remove-violations-after: 300
checks:
@@ -63,6 +63,15 @@ Punishments:
- "20:20 [log]"
- "40:40 [webhook]"
- "40:40 [proxy]"
Inventory:
remove-violations-after: 300
checks:
- "Inventory"
commands:
- "10:10 [alert]"
- "10:10 [log]"
- "20:20 [webhook]"
- "20:20 [proxy]"
Reach:
remove-violations-after: 300
checks:
@@ -81,6 +90,24 @@ Punishments:
- "5:3 [log]"
- "5:3 [webhook]"
- "5:3 [proxy]"
WallHit:
remove-violations-after: 300
checks:
- "WallHit"
commands:
- "10:5 [alert]"
- "10:5 [webhook]"
- "10:5 [proxy]"
- "10:5 [log]"
EntityPierce:
remove-violations-after: 300
checks:
- "EntityPierce"
commands:
- "15:10 [alert]"
- "15:10 [webhook]"
- "15:10 [proxy]"
- "15:10 [log]"
Misc:
remove-violations-after: 300
checks:
@@ -63,6 +63,15 @@ Punishments:
- "20:20 [log]"
- "40:40 [webhook]"
- "40:40 [proxy]"
Inventory:
remove-violations-after: 300
checks:
- "Inventory"
commands:
- "10:10 [alert]"
- "10:10 [log]"
- "20:20 [webhook]"
- "20:20 [proxy]"
Reach:
remove-violations-after: 300
checks:
@@ -81,6 +90,24 @@ Punishments:
- "5:3 [log]"
- "5:3 [webhook]"
- "5:3 [proxy]"
WallHit:
remove-violations-after: 300
checks:
- "WallHit"
commands:
- "10:5 [alert]"
- "10:5 [webhook]"
- "10:5 [proxy]"
- "10:5 [log]"
EntityPierce:
remove-violations-after: 300
checks:
- "EntityPierce"
commands:
- "15:10 [alert]"
- "15:10 [webhook]"
- "15:10 [proxy]"
- "15:10 [log]"
Misc:
remove-violations-after: 300
checks:
@@ -63,6 +63,15 @@ Punishments:
- "20:20 [log]"
- "40:40 [webhook]"
- "40:40 [proxy]"
Inventory:
remove-violations-after: 300
checks:
- "Inventory"
commands:
- "10:10 [alert]"
- "10:10 [log]"
- "20:20 [webhook]"
- "20:20 [proxy]"
Reach:
remove-violations-after: 300
checks:
@@ -81,6 +90,24 @@ Punishments:
- "5:3 [log]"
- "5:3 [webhook]"
- "5:3 [proxy]"
WallHit:
remove-violations-after: 300
checks:
- "WallHit"
commands:
- "10:5 [alert]"
- "10:5 [webhook]"
- "10:5 [proxy]"
- "10:5 [log]"
EntityPierce:
remove-violations-after: 300
checks:
- "EntityPierce"
commands:
- "15:10 [alert]"
- "15:10 [webhook]"
- "15:10 [proxy]"
- "15:10 [log]"
Misc:
remove-violations-after: 300
checks:
@@ -61,6 +61,15 @@ Punishments:
- "20:20 [log]"
- "40:40 [webhook]"
- "40:40 [proxy]"
Inventory:
remove-violations-after: 300
checks:
- "Inventory"
commands:
- "10:10 [alert]"
- "10:10 [log]"
- "20:20 [webhook]"
- "20:20 [proxy]"
Reach:
remove-violations-after: 300
checks:
@@ -79,6 +88,24 @@ Punishments:
- "5:3 [log]"
- "5:3 [webhook]"
- "5:3 [proxy]"
WallHit:
remove-violations-after: 300
checks:
- "WallHit"
commands:
- "10:5 [alert]"
- "10:5 [webhook]"
- "10:5 [proxy]"
- "10:5 [log]"
EntityPierce:
remove-violations-after: 300
checks:
- "EntityPierce"
commands:
- "15:10 [alert]"
- "15:10 [webhook]"
- "15:10 [proxy]"
- "15:10 [log]"
Misc:
remove-violations-after: 300
checks:
@@ -63,6 +63,15 @@ Punishments:
- "20:20 [log]"
- "40:40 [webhook]"
- "40:40 [proxy]"
Inventory:
remove-violations-after: 300
checks:
- "Inventory"
commands:
- "10:10 [alert]"
- "10:10 [log]"
- "20:20 [webhook]"
- "20:20 [proxy]"
Reach:
remove-violations-after: 300
checks:
@@ -81,6 +90,24 @@ Punishments:
- "5:3 [log]"
- "5:3 [webhook]"
- "5:3 [proxy]"
WallHit:
remove-violations-after: 300
checks:
- "WallHit"
commands:
- "10:5 [alert]"
- "10:5 [webhook]"
- "10:5 [proxy]"
- "10:5 [log]"
EntityPierce:
remove-violations-after: 300
checks:
- "EntityPierce"
commands:
- "15:10 [alert]"
- "15:10 [webhook]"
- "15:10 [proxy]"
- "15:10 [log]"
Misc:
remove-violations-after: 300
checks:
+1 -1
View File
@@ -13,7 +13,7 @@ plugins {
dependencies {
minecraft("com.mojang:minecraft:$minecraft_version")
mappings("net.fabricmc:yarn:$yarn_mappings")
mappings(loom.officialMojangMappings())
modImplementation(fabricApi.module("fabric-lifecycle-events-v1", fabric_version))
modImplementation("me.lucko:fabric-permissions-api:0.3.1")
+1 -1
View File
@@ -1,6 +1,6 @@
dependencies {
minecraft("com.mojang:minecraft:1.16.1")
mappings("net.fabricmc:yarn:1.16.1+build.21:v2")
mappings(loom.officialMojangMappings())
modImplementation(fabricApi.module("fabric-lifecycle-events-v1", "0.42.0+1.16"))
modImplementation("me.lucko:fabric-permissions-api:0.1-SNAPSHOT")
@@ -3,19 +3,19 @@ package ac.grim.grimac.platform.fabric.mc1161;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.platform.fabric.AbstractFabricPlatformServer;
import ac.grim.grimac.platform.fabric.GrimACFabricLoaderPlugin;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.commands.CommandSourceStack;
public class Fabric1140PlatformServer extends AbstractFabricPlatformServer {
@Override
public void dispatchCommand(Sender sender, String command) {
ServerCommandSource commandSource = GrimACFabricLoaderPlugin.LOADER.getFabricSenderFactory().reverse(sender);
GrimACFabricLoaderPlugin.FABRIC_SERVER.getCommandManager().execute(commandSource, command);
CommandSourceStack commandSource = GrimACFabricLoaderPlugin.LOADER.getFabricSenderFactory().reverse(sender);
GrimACFabricLoaderPlugin.FABRIC_SERVER.getCommands().performCommand(commandSource, command);
}
// TODO (Cross-platform) implement proper bukkit equivalent for getting TPS over time
@Override
public double getTPS() {
return Math.min(1000.0 / GrimACFabricLoaderPlugin.FABRIC_SERVER.getTickTime(), 20.0);
return Math.min(1000.0 / GrimACFabricLoaderPlugin.FABRIC_SERVER.getAverageTickTime(), 20.0);
}
}
@@ -1,6 +1,5 @@
package ac.grim.grimac.platform.fabric.mc1161.command;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.platform.api.command.PlayerSelector;
import ac.grim.grimac.platform.api.sender.Sender;
@@ -24,7 +23,7 @@ public class Fabric1161PlayerSelectorAdapter implements PlayerSelector {
@Override
public Sender getSinglePlayer() {
return ((FabricSenderFactory) GrimAPI.INSTANCE.getSenderFactory()).map(fabricSelector.single().getCommandSource());
return ((FabricSenderFactory) GrimAPI.INSTANCE.getSenderFactory()).map(fabricSelector.single().createCommandSourceStack());
}
@Override
@@ -3,11 +3,10 @@ package ac.grim.grimac.platform.fabric.mc1161.entity;
import ac.grim.grimac.platform.fabric.entity.AbstractFabricGrimEntity;
import ac.grim.grimac.platform.fabric.utils.thread.FabricFutureUtil;
import ac.grim.grimac.utils.math.Location;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.server.world.ServerWorld;
import java.util.concurrent.CompletableFuture;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
public class Fabric1161GrimEntity extends AbstractFabricGrimEntity {
@@ -18,8 +17,8 @@ public class Fabric1161GrimEntity extends AbstractFabricGrimEntity {
@Override
public CompletableFuture<Boolean> teleportAsync(Location location) {
return FabricFutureUtil.supplySync(() -> {
if (entity.getEntityWorld() instanceof ServerWorld) {
entity.teleport(
if (entity.getCommandSenderWorld() instanceof ServerLevel) {
entity.teleportToWithTicket(
location.getX(),
location.getY(),
location.getZ()
@@ -32,6 +31,6 @@ public class Fabric1161GrimEntity extends AbstractFabricGrimEntity {
@Override
public boolean isDead() {
return entity instanceof LivingEntity living ? living.isDead() : entity.removed;
return entity instanceof LivingEntity living ? living.isDeadOrDying() : entity.removed;
}
}
@@ -1,16 +1,16 @@
package ac.grim.grimac.platform.fabric.mc1161.player;
import ac.grim.grimac.platform.fabric.player.AbstractFabricPlatformInventory;
import net.minecraft.screen.PlayerScreenHandler;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.screen.ScreenHandlerType;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
import net.minecraft.util.registry.Registry;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.InventoryMenu;
import net.minecraft.world.inventory.MenuType;
import org.jetbrains.annotations.Nullable;
public class Fabric1161PlatformInventory extends AbstractFabricPlatformInventory {
public Fabric1161PlatformInventory(ServerPlayerEntity player) {
public Fabric1161PlatformInventory(ServerPlayer player) {
super(player);
}
@@ -23,13 +23,13 @@ public class Fabric1161PlatformInventory extends AbstractFabricPlatformInventory
// And is slated to be replaced by packet based behaviour, this should do for now
@Override
public String getOpenInventoryKey() {
ScreenHandler handler = fabricPlayer.currentScreenHandler;
ScreenHandlerType<?> type = getSafeType(handler);
AbstractContainerMenu handler = fabricPlayer.containerMenu;
MenuType<?> type = getSafeType(handler);
// Handle null types (player crafting and creative)
if (type == null) {
// 4x4 CRAFTING -> CRAFTING
if (handler instanceof PlayerScreenHandler) {
if (handler instanceof InventoryMenu) {
return "CRAFTING";
// Not sure if creative mode check here is correct
} else if (this.isPlayerCreative()) {
@@ -39,23 +39,23 @@ public class Fabric1161PlatformInventory extends AbstractFabricPlatformInventory
// should we handle crafters here also??
// CRAFTING -> CRAFTING
if (type == ScreenHandlerType.CRAFTING) {
if (type == MenuType.CRAFTING) {
return "CRAFTING";
// PLAYER -> PLAYER
} else if (type == ScreenHandlerType.GENERIC_9X4) {
} else if (type == MenuType.GENERIC_9x4) {
return "PLAYER";
// CHEST, ENDER_CHEST, or BARREL -> CHEST
} else if (type == ScreenHandlerType.GENERIC_9X3) {
} else if (type == MenuType.GENERIC_9x3) {
return "CHEST";
// DISPENSER, DROPPER -> DISPENSER
} else if (type == ScreenHandlerType.GENERIC_3X3) {
} else if (type == MenuType.GENERIC_3x3) {
return "DISPENSER";
} else {
// Registry handles:
// SHULKER_BOX -> SHULKER_BOX
// CRAFTIING -> CRAFTING
Identifier registryKey = this.getScreenID(type);
ResourceLocation registryKey = this.getScreenID(type);
if (registryKey != null) {
return registryKey.getPath();
}
@@ -64,15 +64,15 @@ public class Fabric1161PlatformInventory extends AbstractFabricPlatformInventory
}
}
protected Identifier getScreenID(ScreenHandlerType<?> type) {
return Registry.SCREEN_HANDLER.getId(type);
protected ResourceLocation getScreenID(MenuType<?> type) {
return Registry.MENU.getKey(type);
}
protected boolean isPlayerCreative() {
return fabricPlayer.isCreative();
}
protected @Nullable ScreenHandlerType<?> getSafeType(ScreenHandler handler) {
protected @Nullable MenuType<?> getSafeType(AbstractContainerMenu handler) {
try {
return handler.getType();
} catch (UnsupportedOperationException e) {
@@ -5,36 +5,25 @@ import ac.grim.grimac.platform.fabric.GrimACFabricLoaderPlugin;
import ac.grim.grimac.platform.fabric.player.AbstractFabricPlatformPlayer;
import ac.grim.grimac.platform.fabric.utils.thread.FabricFutureUtil;
import ac.grim.grimac.utils.math.Location;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import java.util.concurrent.CompletableFuture;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
public class Fabric1161PlatformPlayer extends AbstractFabricPlatformPlayer {
public Fabric1161PlatformPlayer(ServerPlayerEntity player) {
public Fabric1161PlatformPlayer(ServerPlayer player) {
super(player);
}
@Override
public boolean hasPermission(String permission) {
return GrimACFabricLoaderPlugin.LOADER.getFabricSenderFactory().map(entity.getCommandSource()).hasPermission(permission);
}
@Override
public boolean hasPermission(String s, boolean defaultIfUnset) {
return GrimACFabricLoaderPlugin.LOADER.getFabricSenderFactory().map(entity.getCommandSource()).hasPermission(s, defaultIfUnset);
}
@Override
public Sender getSender() {
return GrimACFabricLoaderPlugin.LOADER.getFabricSenderFactory().map(entity.getCommandSource());
return GrimACFabricLoaderPlugin.LOADER.getFabricSenderFactory().map(entity.createCommandSourceStack());
}
@Override
public CompletableFuture<Boolean> teleportAsync(Location location) {
return FabricFutureUtil.supplySync(() -> {
fabricPlayer.teleport(
(ServerWorld) location.getWorld(),
fabricPlayer.teleportTo(
(ServerLevel) location.getWorld(),
location.getX(),
location.getY(),
location.getZ(),
@@ -9,19 +9,18 @@ import io.github.retrooper.packetevents.adventure.serializer.gson.GsonComponentS
import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
import net.kyori.adventure.text.Component;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.text.Text;
import net.minecraft.network.FriendlyByteBuf;
public class Fabric1140ConversionUtil implements IFabricConversionUtil {
public ItemStack fromFabricItemStack(net.minecraft.item.ItemStack fabricStack) {
public ItemStack fromFabricItemStack(net.minecraft.world.item.ItemStack fabricStack) {
if (fabricStack.isEmpty()) {
return ItemStack.EMPTY;
}
ByteBuf buffer = PooledByteBufAllocator.DEFAULT.buffer();
try {
PacketByteBuf packetByteBuf = new PacketByteBuf(buffer);
packetByteBuf.writeItemStack(fabricStack);
FriendlyByteBuf packetByteBuf = new FriendlyByteBuf(buffer);
packetByteBuf.writeItem(fabricStack);
PacketWrapper<?> wrapper = PacketWrapper.createUniversalPacketWrapper(buffer);
return wrapper.readItemStack();
} catch (Exception e) {
@@ -32,7 +31,7 @@ public class Fabric1140ConversionUtil implements IFabricConversionUtil {
}
}
public Text toNativeText(Component component) {
return Text.Serializer.fromJson(GsonComponentSerializer.gson().serializeToTree(component));
public net.minecraft.network.chat.Component toNativeText(Component component) {
return net.minecraft.network.chat.Component.Serializer.fromJson(GsonComponentSerializer.gson().serializeToTree(component));
}
}
@@ -1,18 +1,18 @@
package ac.grim.grimac.platform.fabric.mc1161.util.convert;
import ac.grim.grimac.platform.fabric.utils.message.IFabricMessageUtil;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.LiteralText;
import net.minecraft.text.Text;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TextComponent;
public class Fabric1161MessageUtil implements IFabricMessageUtil {
@Override
public Text textLiteral(String message) {
return new LiteralText(message);
public Component textLiteral(String message) {
return new TextComponent(message);
}
@Override
public void sendMessage(ServerCommandSource target, Text message, boolean overlay) {
target.sendFeedback(message, overlay);
public void sendMessage(CommandSourceStack target, Component message, boolean overlay) {
target.sendSuccess(message, overlay);
}
}
@@ -1,3 +1,3 @@
accessWidener v2 named
accessible field net/minecraft/server/world/ServerWorld worldProperties Lnet/minecraft/world/level/ServerWorldProperties;
accessible field net/minecraft/server/command/ServerCommandSource output Lnet/minecraft/server/command/CommandOutput;
accessible field net/minecraft/server/level/ServerLevel serverLevelData Lnet/minecraft/world/level/storage/ServerLevelData;
accessible field net/minecraft/commands/CommandSourceStack source Lnet/minecraft/commands/CommandSource;
+1 -1
View File
@@ -1,6 +1,6 @@
dependencies {
minecraft("com.mojang:minecraft:1.17.1")
mappings("net.fabricmc:yarn:1.17.1+build.65:v2")
mappings(loom.officialMojangMappings())
compileOnly(project(":fabric:mc1161", configuration = "namedElements"))
modImplementation(fabricApi.module("fabric-lifecycle-events-v1", "0.46.1+1.17"))
@@ -7,12 +7,10 @@ import org.jetbrains.annotations.Nullable;
import java.util.Optional;
public class Fabric1171PlatformServer extends Fabric1140PlatformServer {
@Override @Nullable
public GameProfile getProfileByName(String name) {
Optional<GameProfile> gameProfile = GrimACFabricLoaderPlugin.FABRIC_SERVER.getUserCache().findByName(name);
@Override
public @Nullable GameProfile getProfileByName(String name) {
Optional<GameProfile> gameProfile = GrimACFabricLoaderPlugin.FABRIC_SERVER.getProfileCache().get(name);
return gameProfile.orElse(null);
}
}
@@ -1,9 +1,8 @@
package ac.grim.grimac.platform.fabric.mc1171.entity;
import ac.grim.grimac.platform.fabric.mc1161.entity.Fabric1161GrimEntity;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
public class Fabric1170GrimEntity extends Fabric1161GrimEntity {
@@ -13,8 +12,6 @@ public class Fabric1170GrimEntity extends Fabric1161GrimEntity {
@Override
public boolean isDead() {
if (this.entity instanceof LivingEntity)
return ((LivingEntity) entity).isDead();
return this.entity.isRemoved();
return this.entity instanceof LivingEntity living ? living.isDeadOrDying() : this.entity.isRemoved();
}
}
@@ -3,16 +3,15 @@ package ac.grim.grimac.platform.fabric.mc1171.player;
import ac.grim.grimac.platform.fabric.mc1161.player.Fabric1161PlatformPlayer;
import ac.grim.grimac.platform.fabric.utils.convert.FabricConversionUtil;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.level.ServerPlayer;
public class Fabric1170PlatformPlayer extends Fabric1161PlatformPlayer {
public Fabric1170PlatformPlayer(ServerPlayerEntity player) {
public Fabric1170PlatformPlayer(ServerPlayer player) {
super(player);
}
@Override
public void setGameMode(GameMode gameMode) {
fabricPlayer.changeGameMode(FabricConversionUtil.toFabricGameMode(gameMode));
fabricPlayer.setGameMode(FabricConversionUtil.toFabricGameMode(gameMode));
}
}
Loaded 100 of 148 files, more files were not shown because too many files have changed in this diff. Show more