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
SamB440 f8a5315fff We do not need high bound if player ticks reliably. 2025-10-28 13:27:54 +00:00
AnnotationVisitor 102b5842e7 Add copper golem statue, shelf collision boxes and other missing 1.21.10 stuff (#2315)
* add copper golem statue

* Include ViaVersion substitution

* add shelves

* also add copper armor

* add mannequin box sizes

* Add copper golem size
2025-10-27 11:17:09 -06:00
ManInMyVan a76bf9ce09 add copper golem statue collision box 2025-10-26 19:01:29 -06:00
ManInMyVan 4f3e715f94 add shelf collision box 2025-10-26 18:41:25 -06:00
ManInMyVan 1b47e31776 allow up to one PLAYER_INPUT per tick (#2306) 2025-10-26 11:54:15 -06:00
BT (calcastor/mame) b69906f809 Clear active item before updating inventory on legacy (#2317)
* Apply spotless

* Clear active item before updating inventory on legacy
2025-10-23 22:56:12 -06:00
ManInMyVan 9ba4052c35 possibly fix build 2025-10-23 00:51:27 -06:00
ManInMyVan a86bf57a39 mark MultiActionsD as stable 2025-10-23 00:49:50 -06:00
ManInMyVan a82f2b888a mark MultiActionsC as stable 2025-10-23 00:49:23 -06:00
ManInMyVan 89cf181772 mark PositionBreakB as stable 2025-10-23 00:43:47 -06:00
ManInMyVan 6e5ec332dd remove double space in ChatB 2025-10-23 00:40:59 -06:00
ManInMyVan 79bf7e4fd1 mark ElytraF as stable 2025-10-23 00:39:32 -06:00
ManInMyVan d0aafdd9f7 mark ElytraA as stable 2025-10-22 18:06:48 -06:00
ManInMyVan 3f19abc859 mark ElytraC as stable 2025-10-22 18:06:29 -06:00
ManInMyVan 8a6863b9d3 mark ElytraB as stable 2025-10-22 17:47:05 -06:00
Axionize 26492fd8e3 Optimize new Vector3dm() multiply calls 2025-10-20 23:23:39 -04:00
Axionize 15e809835b Optimize new object creation on vector add() calls 2025-10-20 23:15:23 -04:00
Axionize 1eae5484c7 Micro optimize DefaultResyncHandler 2025-10-20 22:56:11 -04:00
Axionize 90036e2bf2 Optimize Messages (#2309)
* Optimize MessageUtil.replacePlaceHolders()

* Send user messages at packet level bypassing backend server for performance

* Stash temporary changes

* Make Grim send fast chat messages bypasses platform by default
2025-10-20 22:16:21 -04:00
179 changed files with 3324 additions and 800 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 {
@@ -6,6 +6,8 @@ import ac.grim.grimac.platform.bukkit.utils.convert.BukkitConversionUtils;
import ac.grim.grimac.platform.bukkit.utils.reflection.PaperUtils;
import ac.grim.grimac.platform.bukkit.world.BukkitPlatformWorld;
import ac.grim.grimac.utils.math.Location;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import org.bukkit.entity.Entity;
import org.jetbrains.annotations.NotNull;
@@ -15,6 +17,8 @@ import java.util.concurrent.CompletableFuture;
public class BukkitGrimEntity implements GrimEntity {
protected static final boolean CAN_USE_DIRECT_GETTERS = PacketEvents.getAPI().getServerManager().getVersion().isNewerThan(ServerVersion.V_1_20_1) && PaperUtils.PAPER;
private final Entity entity;
private BukkitPlatformWorld bukkitPlatformWorld;
@@ -59,7 +63,6 @@ public class BukkitGrimEntity implements GrimEntity {
if (bukkitPlatformWorld == null || !bukkitPlatformWorld.getBukkitWorld().equals(entity.getWorld())) {
bukkitPlatformWorld = new BukkitPlatformWorld(entity.getWorld());
}
return bukkitPlatformWorld;
}
@@ -75,4 +78,26 @@ public class BukkitGrimEntity implements GrimEntity {
location.getPitch()
);
}
@Override
public double distanceSquared(double oX, double oY, double oZ) {
if (CAN_USE_DIRECT_GETTERS) {
double x = this.entity.getX();
double y = this.entity.getY();
double z = this.entity.getZ();
double distX = (x - oX) * (x - oX);
double distY = (y - oY) * (y - oY);
double distZ = (z - oZ) * (z - oZ);
return distX + distY + distZ;
} else {
org.bukkit.Location location = this.entity.getLocation();
double x = location.getX();
double y = location.getY();
double z = location.getZ();
double distX = (x - oX) * (x - oX);
double distY = (y - oY) * (y - oY);
double distZ = (z - oZ) * (z - oZ);
return distX + distY + distZ;
}
}
}
@@ -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);
});
}
}
}
@@ -177,10 +177,11 @@ public class BukkitItemResetHandler implements ItemResetHandler {
if (legacy) { // 1.8.8
resetItemUsage = player -> {
try {
method.invoke(getHandle.invoke(player));
// in 1.8 we need to resync item usage manually,
// only do so if the player is using an item
if (isUsingItem.test(player)) player.updateInventory();
method.invoke(getHandle.invoke(player));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
@@ -9,8 +9,11 @@ import ac.grim.grimac.platform.bukkit.entity.BukkitGrimEntity;
import ac.grim.grimac.platform.bukkit.utils.anticheat.MultiLibUtil;
import ac.grim.grimac.platform.bukkit.utils.convert.BukkitConversionUtils;
import ac.grim.grimac.platform.bukkit.utils.reflection.PaperUtils;
import ac.grim.grimac.utils.common.arguments.CommonGrimArguments;
import ac.grim.grimac.utils.math.Location;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.protocol.player.User;
import com.github.retrooper.packetevents.util.Vector3d;
import io.github.retrooper.packetevents.util.SpigotConversionUtil;
import lombok.Getter;
@@ -27,6 +30,7 @@ import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class BukkitPlatformPlayer extends BukkitGrimEntity implements PlatformPlayer {
private static final BukkitAudiences audiences = BukkitAudiences.create(GrimACBukkitLoaderPlugin.LOADER);
@Getter
@@ -34,10 +38,18 @@ public class BukkitPlatformPlayer extends BukkitGrimEntity implements PlatformPl
@Getter
private final PlatformInventory inventory;
private final @Nullable User user;
public BukkitPlatformPlayer(Player bukkitPlayer) {
super(bukkitPlayer);
this.bukkitPlayer = bukkitPlayer;
this.inventory = new BukkitPlatformInventory(bukkitPlayer);
if (CommonGrimArguments.USE_CHAT_FAST_BYPASS.value()) {
Object channel = PacketEvents.getAPI().getProtocolManager().getChannel(bukkitPlayer.getUniqueId());
this.user = PacketEvents.getAPI().getProtocolManager().getUser(channel);
} else {
this.user = null;
}
}
@Override
@@ -67,12 +79,20 @@ public class BukkitPlatformPlayer extends BukkitGrimEntity implements PlatformPl
@Override
public void sendMessage(String message) {
bukkitPlayer.sendMessage(message);
if (CommonGrimArguments.USE_CHAT_FAST_BYPASS.value() && user != null) {
user.sendMessage(message);
} else {
bukkitPlayer.sendMessage(message);
}
}
@Override
public void sendMessage(Component message) {
audiences.player(bukkitPlayer).sendMessage(message);
if (CommonGrimArguments.USE_CHAT_FAST_BYPASS.value() && user != null) {
user.sendMessage(message);
} else {
audiences.player(bukkitPlayer).sendMessage(message);
}
}
@Override
@@ -92,8 +112,12 @@ public class BukkitPlatformPlayer extends BukkitGrimEntity implements PlatformPl
@Override
public Vector3d getPosition() {
org.bukkit.Location location = this.bukkitPlayer.getLocation();
return new Vector3d(location.getX(), location.getY(), location.getZ());
if (CAN_USE_DIRECT_GETTERS) {
return new Vector3d(this.bukkitPlayer.getX(), this.bukkitPlayer.getY(), this.bukkitPlayer.getZ());
} else {
org.bukkit.Location location = this.bukkitPlayer.getLocation();
return new Vector3d(location.getX(), location.getY(), location.getZ());
}
}
@Override
@@ -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());
}
}
@@ -0,0 +1,32 @@
package ac.grim.grimac.checks.impl.badpackets;
import ac.grim.grimac.checks.Check;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.PacketCheck;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
@CheckData(name = "BadPacketsZ", experimental = true)
public class BadPacketsZ extends Check implements PacketCheck {
private boolean sent;
public BadPacketsZ(GrimPlayer player) {
super(player);
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (event.getPacketType() == PacketType.Play.Client.CLIENT_TICK_END) {
sent = false;
}
if (event.getPacketType() == PacketType.Play.Client.PLAYER_INPUT) {
if (sent) {
flagAndAlert();
}
sent = true;
}
}
}
@@ -26,9 +26,8 @@ public class FarBreak extends Check implements BlockBreakCheck {
double min = Double.MAX_VALUE;
for (double d : player.getPossibleEyeHeights()) {
SimpleCollisionBox box = new SimpleCollisionBox(blockBreak.position);
Vector3dm eyes = new Vector3dm(player.x, player.y + d, player.z);
Vector3dm best = VectorUtils.cutBoxToVector(eyes, box);
min = Math.min(min, eyes.distanceSquared(best));
Vector3dm best = VectorUtils.cutBoxToVector(player.x, player.y + d, player.z, box);
min = Math.min(min, best.distanceSquared(player.x, player.y + d, player.z));
}
// getPickRange() determines this?
@@ -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;
@@ -9,7 +9,7 @@ import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.protocol.player.DiggingAction;
import com.github.retrooper.packetevents.protocol.world.BlockFace;
@CheckData(name = "PositionBreakB", experimental = true)
@CheckData(name = "PositionBreakB")
public class PositionBreakB extends Check implements BlockBreakCheck {
private final int releaseFace = player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_8) ? 0 : 255;
private BlockFace lastFace;
@@ -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;
@@ -14,7 +12,6 @@ import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientCh
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientChatCommandUnsigned;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientChatMessage;
// this can false from click events, but I doubt this would actually
// happen unless they're trying to flag, or if the server is set up badly
@CheckData(name = "ChatB", description = "Invalid chat message")
@@ -64,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,33 +234,20 @@ 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) {
eyePos.setY(from.getY() + eye);
Vector3dm endReachPos = eyePos.clone().add(new Vector3dm(lookVec.getX() * distance, lookVec.getY() * distance, lookVec.getZ() * distance));
Vector3dm endReachPos = eyePos.clone().add(lookVec.getX() * distance, lookVec.getY() * distance, lookVec.getZ() * distance);
Vector3dm intercept = ReachUtils.calculateIntercept(targetBox, eyePos, endReachPos).first();
@@ -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);
}
}
@@ -10,7 +10,7 @@ import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientEntityAction;
@CheckData(name = "ElytraA", description = "Started gliding while already gliding", experimental = true)
@CheckData(name = "ElytraA", description = "Started gliding while already gliding")
public class ElytraA extends Check implements PostPredictionCheck {
private boolean setback;
@@ -9,7 +9,7 @@ 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 = "ElytraB", description = "Started gliding without jumping", experimental = true)
@CheckData(name = "ElytraB", description = "Started gliding without jumping")
public class ElytraB extends Check implements PostPredictionCheck {
private boolean glide;
private boolean setback;
@@ -11,7 +11,7 @@ import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientEntityAction;
@CheckData(name = "ElytraC", description = "Started gliding too frequently", experimental = true)
@CheckData(name = "ElytraC", description = "Started gliding too frequently")
public class ElytraC extends Check implements PostPredictionCheck {
private boolean glideThisTick, glideLastTick, setback;
private int flags;
@@ -10,7 +10,7 @@ import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientEntityAction;
@CheckData(name = "ElytraF", description = "Started gliding while on ground", experimental = true)
@CheckData(name = "ElytraF", description = "Started gliding while on ground")
public class ElytraF extends Check implements PostPredictionCheck {
private boolean setback;
@@ -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();
}
}
}
}
@@ -12,12 +12,13 @@ import org.jetbrains.annotations.NotNull;
import java.util.StringJoiner;
@CheckData(name = "MultiActionsC", description = "Clicked in inventory while moving", experimental = true)
@CheckData(name = "MultiActionsC", description = "Clicked in inventory while moving")
public class MultiActionsC extends Check implements PacketCheck {
public MultiActionsC(GrimPlayer player) {
super(player);
}
// TODO: move this to a bett spot? not sure where to put this
@Contract(pure = true)
public static String getVerbose(@NotNull GrimPlayer player) {
StringJoiner verbose = new StringJoiner(", ");
@@ -7,7 +7,7 @@ import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
@CheckData(name = "MultiActionsD", description = "Closed inventory while moving", experimental = true)
@CheckData(name = "MultiActionsD", description = "Closed inventory while moving")
public class MultiActionsD extends Check implements PacketCheck {
public MultiActionsD(GrimPlayer player) {
super(player);
@@ -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
@@ -30,9 +30,8 @@ public class FarPlace extends BlockPlaceCheck {
final double[] possibleEyeHeights = player.getPossibleEyeHeights();
for (double d : possibleEyeHeights) {
SimpleCollisionBox box = new SimpleCollisionBox(blockPos);
Vector3dm eyes = new Vector3dm(player.x, player.y + d, player.z);
Vector3dm best = VectorUtils.cutBoxToVector(eyes, box);
min = Math.min(min, eyes.distanceSquared(best));
Vector3dm best = VectorUtils.cutBoxToVector(player.x, player.y + d, player.z, box);
min = Math.min(min, best.distanceSquared(player.x, player.y + d, player.z));
}
// getPickRange() determines this?
@@ -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;
@@ -116,6 +115,12 @@ public class CheckManager {
.put(BadPacketsU.class, new BadPacketsU(player))
.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))
@@ -124,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))
@@ -200,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))
@@ -249,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
@@ -131,9 +138,9 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
} else if (player.wasTouchingLava) {
vector.multiply(0.5D);
if (player.hasGravity)
vector.add(new Vector3dm(0.0D, -player.gravity / 4.0D, 0.0D));
vector.add(0.0D, -player.gravity / 4.0D, 0.0D);
} else if (player.isGliding) {
PredictionEngineElytra.getElytraMovement(player, vector, ReachUtils.getLook(player, player.yaw, player.pitch)).multiply(player.stuckSpeedMultiplier).multiply(new Vector3dm(0.99F, 0.98F, 0.99F));
PredictionEngineElytra.getElytraMovement(player, vector, ReachUtils.getLook(player, player.yaw, player.pitch)).multiply(player.stuckSpeedMultiplier).multiply(0.99F, 0.98F, 0.99F);
vector.setY(vector.getY() - 0.05); // Make the player fall a bit
} else { // Gliding doesn't have friction, we handle it differently
PredictionEngineNormal.staticVectorEndOfTick(player, vector); // Lava and normal movement
@@ -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();
@@ -7,7 +7,6 @@ import ac.grim.grimac.platform.api.world.PlatformWorld;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.github.retrooper.packetevents.util.Vector3d;
import com.github.retrooper.packetevents.util.Vector3i;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerAcknowledgeBlockChanges;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerBlockChange;
@@ -116,7 +115,7 @@ public class DefaultResyncHandler implements ResyncHandler {
GrimAPI.INSTANCE.getScheduler().getRegionScheduler().execute(GrimAPI.INSTANCE.getGrimPlugin(), world, chunkX, chunkZ, () -> {
if (!player.platformPlayer.isOnline() || !player.getSetbackTeleportUtil().hasAcceptedSpawnTeleport)
return;
if (player.platformPlayer.getPosition().distance(new Vector3d(x, y, z)) >= 64)
if (player.platformPlayer.distanceSquared(x, y, z) >= 64 * 64)
return;
if (!world.isChunkLoaded(chunkX, chunkZ)) return; // Don't load chunks sync
@@ -25,4 +25,6 @@ public interface GrimEntity extends GrimIdentity {
PlatformWorld getWorld();
Location getLocation();
double distanceSquared(double x, double y, double z);
}
@@ -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();
}
}
@@ -196,7 +197,6 @@ public class MovementCheckRunner extends Check implements PositionCheck {
player.vehicleData.wasVehicleSwitch = false;
if (riding != null) {
Vector3dm pos = new Vector3dm(player.x, player.y, player.z);
SimpleCollisionBox interTruePositions = riding.getPossibleCollisionBoxes();
// We shrink the expanded bounding box to what the packet positions can be, for a smaller box
@@ -206,7 +206,7 @@ public class MovementCheckRunner extends Check implements PositionCheck {
interTruePositions.expand(-width, 0, -width);
interTruePositions.expandMax(0, -height, 0);
Vector3dm cutTo = VectorUtils.cutBoxToVector(pos, interTruePositions);
Vector3dm cutTo = VectorUtils.cutBoxToVector(player.x, player.y, player.z, interTruePositions);
// Now we need to simulate a tick starting at the most optimal position
// The start position is never sent, so we assume the most optimal start position
@@ -567,7 +567,7 @@ public class MovementCheckRunner extends Check implements PositionCheck {
// Patch sprint jumping with elytra exploit
if (player.platformPlayer != null && player.isGliding && player.predictedVelocity.isJump() && player.isSprinting && !allowSprintJumpingWithElytra) {
SetbackTeleportUtil.SetbackPosWithVector lastKnownGoodPosition = player.getSetbackTeleportUtil().lastKnownGoodPosition;
lastKnownGoodPosition.setVector(lastKnownGoodPosition.getVector().multiply(new Vector3dm(0.6 * 0.91, 1, 0.6 * 0.91)));
lastKnownGoodPosition.setVector(lastKnownGoodPosition.getVector().multiply(0.6 * 0.91, 1, 0.6 * 0.91));
player.getSetbackTeleportUtil().executeNonSimulatingSetback();
}
@@ -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;
});
}
@@ -157,12 +157,12 @@ public class UncertaintyHandler {
Vector3dm maxLocation = new Vector3dm(entityBox.maxX, entityBox.maxY, entityBox.maxZ);
Vector3dm minLocation = new Vector3dm(entityBox.minX, entityBox.minY, entityBox.minZ);
Vector3dm diff = minLocation.subtract(new Vector3dm(player.lastX, player.lastY + 0.8 * 1.8, player.lastZ)).multiply(0.1);
Vector3dm diff = minLocation.subtract(player.lastX, player.lastY + 0.8 * 1.8, player.lastZ).multiply(0.1);
fishingRodPullBox.minX = Math.min(0, diff.getX());
fishingRodPullBox.minY = Math.min(0, diff.getY());
fishingRodPullBox.minZ = Math.min(0, diff.getZ());
diff = maxLocation.subtract(new Vector3dm(player.lastX, player.lastY + 0.8 * 1.8, player.lastZ)).multiply(0.1);
diff = maxLocation.subtract(player.lastX, player.lastY + 0.8 * 1.8, player.lastZ).multiply(0.1);
fishingRodPullBox.maxX = Math.max(0, diff.getX());
fishingRodPullBox.maxY = Math.max(0, diff.getY());
fishingRodPullBox.maxZ = Math.max(0, diff.getZ());
@@ -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;
@@ -233,7 +233,7 @@ public class MovementTicker {
player.predictedVelocity = new VectorData(collide.clone(), player.predictedVelocity.lastVector, player.predictedVelocity.vectorType);
float f = BlockProperties.getBlockSpeedFactor(player, player.mainSupportingBlockData, new Vector3d(player.x, player.y, player.z));
player.clientVelocity.multiply(new Vector3dm(f, 1, f));
player.clientVelocity.multiply(f, 1, f);
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_21_2)) {
return;
@@ -446,14 +446,14 @@ public class MovementTicker {
// Lava movement changed in 1.16
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_16) && player.slightlyTouchingLava) {
player.clientVelocity = player.clientVelocity.multiply(new Vector3dm(0.5D, 0.800000011920929D, 0.5D));
player.clientVelocity = player.clientVelocity.multiply(0.5D, 0.800000011920929D, 0.5D);
player.clientVelocity = FluidFallingAdjustedMovement.getFluidFallingAdjustedMovement(player, playerGravity, isFalling, player.clientVelocity);
} else {
player.clientVelocity.multiply(0.5D);
}
if (player.hasGravity)
player.clientVelocity.add(new Vector3dm(0.0D, -playerGravity / 4.0D, 0.0D));
player.clientVelocity.add(0.0D, -playerGravity / 4.0D, 0.0D);
} else if (player.isGliding) {
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_21_5) && Collisions.onClimbable(player, player.lastX, player.lastY, player.lastZ)) {
@@ -30,7 +30,7 @@ public class MovementTickerStrider extends MovementTickerRideable {
if (isAbove(player) && player.compensatedWorld.getLavaFluidLevelAt((int) Math.floor(player.x), (int) Math.floor(player.y + 1), (int) Math.floor(player.z)) == 0) {
player.onGround = true;
} else {
player.clientVelocity.multiply(0.5).add(new Vector3dm(0, 0.05, 0));
player.clientVelocity.multiply(0.5).add(0, 0.05, 0);
}
}
}
@@ -417,7 +417,7 @@ public class PredictionEngine {
data.vector.setZ(data.vector.getZ() * 0.6);
data.addVectorType(VectorData.VectorType.AttackSlow);
} else {
velocities.add(data.returnNewModified(data.vector.clone().multiply(new Vector3dm(0.6, 1, 0.6)), VectorData.VectorType.AttackSlow));
velocities.add(data.returnNewModified(data.vector.clone().multiply(0.6, 1, 0.6), VectorData.VectorType.AttackSlow));
}
}
@@ -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);
@@ -31,24 +31,24 @@ public class PredictionEngineElytra extends PredictionEngine {
recalculatedGravity = player.getClientVersion().isOlderThan(ClientVersion.V_1_20_5) ? 0.01 : Math.min(recalculatedGravity, 0.01);
}
vector.add(new Vector3dm(0.0D, recalculatedGravity * (-1.0D + vertCosRotation * 0.75D), 0.0D));
vector.add(0.0D, recalculatedGravity * (-1.0D + vertCosRotation * 0.75D), 0.0D);
double d5;
// Handle slowing the player down when falling
if (vector.getY() < 0.0D && horizontalSqrt > 0.0D) {
d5 = vector.getY() * -0.1D * vertCosRotation;
vector.add(new Vector3dm(lookVector.getX() * d5 / horizontalSqrt, d5, lookVector.getZ() * d5 / horizontalSqrt));
vector.add(lookVector.getX() * d5 / horizontalSqrt, d5, lookVector.getZ() * d5 / horizontalSqrt);
}
// Handle accelerating the player when they are looking down
if (pitchRadians < 0.0F && horizontalSqrt > 0.0D) {
d5 = horizontalLength * (double) (-player.trigHandler.sin(pitchRadians)) * 0.04D;
vector.add(new Vector3dm(-lookVector.getX() * d5 / horizontalSqrt, d5 * 3.2D, -lookVector.getZ() * d5 / horizontalSqrt));
vector.add(-lookVector.getX() * d5 / horizontalSqrt, d5 * 3.2D, -lookVector.getZ() * d5 / horizontalSqrt);
}
// Handle accelerating the player sideways
if (horizontalSqrt > 0) {
vector.add(new Vector3dm((lookVector.getX() / horizontalSqrt * horizontalLength - vector.getX()) * 0.1D, 0.0D, (lookVector.getZ() / horizontalSqrt * horizontalLength - vector.getZ()) * 0.1D));
vector.add((lookVector.getX() / horizontalSqrt * horizontalLength - vector.getX()) * 0.1D, 0.0D, (lookVector.getZ() / horizontalSqrt * horizontalLength - vector.getZ()) * 0.1D);
}
return vector;
@@ -67,7 +67,7 @@ public class PredictionEngineElytra extends PredictionEngine {
for (VectorData data : possibleVectors) {
Vector3dm elytraResult = getElytraMovement(player, data.vector.clone(), currentLook);
if (applyStuckSpeed != 0) elytraResult.multiply(player.stuckSpeedMultiplier);
elytraResult.multiply(new Vector3dm(0.99F, 0.98F, 0.99F));
elytraResult.multiply(0.99F, 0.98F, 0.99F);
VectorData modified = data.returnNewModified(elytraResult, VectorData.VectorType.InputResult);
modified.input = new Vector3dm(0, 0, 0);
results.add(modified);
@@ -16,7 +16,7 @@ public class PredictionEngineLava extends PredictionEngine {
double extraVelFromVertTickSkipUpwards = GrimMath.clamp(player.actualMovement.getY(), vector.vector.clone().getY(), vector.vector.clone().getY() + 0.05f);
existingVelocities.add(new VectorData(vector.vector.clone().setY(extraVelFromVertTickSkipUpwards), vector, VectorData.VectorType.Jump));
} else {
existingVelocities.add(new VectorData(vector.vector.clone().add(new Vector3dm(0, 0.04f, 0)), vector, VectorData.VectorType.Jump));
existingVelocities.add(new VectorData(vector.vector.clone().add(0, 0.04f, 0), vector, VectorData.VectorType.Jump));
}
if (player.slightlyTouchingLava && player.lastOnGround && !player.onGround) {
@@ -56,7 +56,7 @@ public class PredictionEngineNormal extends PredictionEngine {
JumpPower.jumpFromGround(player, jump);
} else {
jump.add(new Vector3dm(0, player.flySpeed * 3, 0));
jump.add(0, player.flySpeed * 3, 0);
if (!player.wasFlying) {
Vector3dm edgeCaseJump = jump.clone();
JumpPower.jumpFromGround(player, edgeCaseJump);
@@ -20,7 +20,7 @@ public class PredictionEngineWater extends PredictionEngine {
private float swimmingFriction;
public static void staticVectorEndOfTick(GrimPlayer player, Vector3dm vector, float swimmingFriction, double playerGravity, boolean isFalling) {
vector.multiply(new Vector3dm(swimmingFriction, 0.8F, swimmingFriction));
vector.multiply(swimmingFriction, 0.8F, swimmingFriction);
Vector3dm fluidVector = FluidFallingAdjustedMovement.getFluidFallingAdjustedMovement(player, playerGravity, isFalling, vector);
vector.setX(fluidVector.getX());
vector.setY(fluidVector.getY());
@@ -76,7 +76,7 @@ public class PredictionEngineWater extends PredictionEngine {
double extraVelFromVertTickSkipUpwards = GrimMath.clamp(player.actualMovement.getY(), vector.vector.clone().getY(), vector.vector.clone().getY() + 0.05f);
existingVelocities.add(new VectorData(vector.vector.clone().setY(extraVelFromVertTickSkipUpwards), vector, VectorData.VectorType.Jump));
} else {
existingVelocities.add(new VectorData(vector.vector.clone().add(new Vector3dm(0, 0.04f, 0)), vector, VectorData.VectorType.Jump));
existingVelocities.add(new VectorData(vector.vector.clone().add(0, 0.04f, 0), vector, VectorData.VectorType.Jump));
}
if (player.slightlyTouchingWater && player.lastOnGround && !player.onGround) {
@@ -47,10 +47,10 @@ public class PredictionEngineWaterLegacy extends PredictionEngine {
@Override
public void addJumpsToPossibilities(GrimPlayer player, Set<VectorData> existingVelocities) {
for (VectorData vector : new HashSet<>(existingVelocities)) {
existingVelocities.add(new VectorData(vector.vector.clone().add(new Vector3dm(0, 0.04f, 0)), vector, VectorData.VectorType.Jump));
existingVelocities.add(new VectorData(vector.vector.clone().add(0, 0.04f, 0), vector, VectorData.VectorType.Jump));
if (player.skippedTickInActualMovement) {
existingVelocities.add(new VectorData(vector.vector.clone().add(new Vector3dm(0, 0.02f, 0)), vector, VectorData.VectorType.Jump));
existingVelocities.add(new VectorData(vector.vector.clone().add(0, 0.02f, 0), vector, VectorData.VectorType.Jump));
}
}
}
@@ -60,7 +60,7 @@ public class PredictionEngineWaterLegacy extends PredictionEngine {
super.endOfTick(player, playerGravity);
for (VectorData vector : player.getPossibleVelocitiesMinusKnockback()) {
vector.vector.multiply(new Vector3dm(swimmingFriction, 0.8F, swimmingFriction));
vector.vector.multiply(swimmingFriction, 0.8F, swimmingFriction);
// Gravity
vector.vector.setY(vector.vector.getY() - 0.02D);
@@ -59,7 +59,7 @@ public final class PredictionEngineRideableUtils {
}
final double multiplier = (double) (22.2222F * player.vehicleData.horseJump) * camel.getAttributeValue(Attributes.MOVEMENT_SPEED) * (double) BlockProperties.getBlockSpeedFactor(player, player.mainSupportingBlockData, new Vector3d(player.lastX, player.lastY, player.lastZ));
Vector3dm jumpVelocity = ReachUtils.getLook(player, player.yaw, player.pitch).multiply(new Vector3dm(1.0, 0.0, 1.0)).normalize().multiply(multiplier).add(new Vector3dm(0, (double) (1.4285F * player.vehicleData.horseJump) * jumpYVelocity, 0));
Vector3dm jumpVelocity = ReachUtils.getLook(player, player.yaw, player.pitch).multiply(1.0, 0.0, 1.0).normalize().multiply(multiplier).add(0, (double) (1.4285F * player.vehicleData.horseJump) * jumpYVelocity, 0);
for (VectorData vectorData : possibleVectors) {
vectorData.vector.add(jumpVelocity);
@@ -54,22 +54,85 @@ public class MessageUtil {
return replacePlaceholders(player == null ? null : GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(player.getUniqueId()), player, string, false);
}
private static final Pattern UNIFIED_PLACEHOLDER_PATTERN = Pattern.compile("%([a-zA-Z0-9_]+)%");
@Contract("_, _, null, _ -> null; _, _, !null, _ -> !null")
private @Nullable String replacePlaceholders(@Nullable GrimPlayer grimPlayer, @Nullable PlatformPlayer platformPlayer, @Nullable String string, boolean removeFormatting) {
if (string == null) return null;
for (Map.Entry<String, String> entry : GrimAPI.INSTANCE.getExternalAPI().getStaticReplacements().entrySet()) {
string = string.replace(entry.getKey(), entry.getValue());
// --- OPTIMIZATION 1: THE FAST PATH ---
// If the string contains no '%' characters, it's impossible for it to have placeholders.
// indexOf() is a JVM intrinsic and is magnitudes faster than even creating a Matcher.
if (string.indexOf('%') == -1) {
// Since there are no % signs we can skip calling papi or our own replacement code
return string;
}
if (grimPlayer != null) {
for (Map.Entry<String, Function<GrimUser, String>> entry : GrimAPI.INSTANCE.getExternalAPI().getVariableReplacements().entrySet()) {
String value = entry.getValue().apply(grimPlayer).replace('%', PLACEHOLDER_ESCAPE_CHAR);
if (removeFormatting) value = filterDiscordText(value);
string = string.replace(entry.getKey(), value);
final Matcher matcher = UNIFIED_PLACEHOLDER_PATTERN.matcher(string);
// If matcher.find() is false, it means '%' existed but not in a valid %...% pattern.
// This avoids allocating a StringBuilder unless absolutely necessary.
if (!matcher.find()) {
return GrimAPI.INSTANCE.getMessagePlaceHolderManager().replacePlaceholders(platformPlayer, string);
}
// Get references to the maps once, outside the loop.
final Map<String, String> staticReplacements = GrimAPI.INSTANCE.getExternalAPI().getStaticReplacements();
final Map<String, Function<GrimUser, String>> variableReplacements = GrimAPI.INSTANCE.getExternalAPI().getVariableReplacements();
final StringBuilder sb = new StringBuilder(string.length() + 32); // Pre-size with a little extra room
// --- OPTIMIZATION 2: THE UNIFIED SINGLE-PASS LOOP ---
// We use a do-while loop because we already performed the first matcher.find().
do {
// The full placeholder, e.g., "%tps%" or "%prefix%"
final String keyWithPercent = matcher.group(0);
String value = null;
// --- OPTIMIZATION 3: UNIFIED LOOKUP ---
// We check the static map first. This is a single, O(1) hash map lookup.
String staticValue = staticReplacements.get(keyWithPercent);
if (staticValue != null) {
value = staticValue;
} else if (grimPlayer != null) {
// If it's not a static placeholder, check if it's a dynamic one.
// This is a second, O(1) hash map lookup.
final Function<GrimUser, String> func = variableReplacements.get(keyWithPercent);
if (func != null) {
// LAZY EVALUATION: We only call the expensive function (like getTPS)
// if we actually found its placeholder in the string.
value = func.apply(grimPlayer);
}
}
}
return GrimAPI.INSTANCE.getMessagePlaceHolderManager().replacePlaceholders(platformPlayer, string).replace(PLACEHOLDER_ESCAPE_CHAR, '%');
// If we found no replacement, `value` will be null.
// In that case, we treat the placeholder as literal text by appending the original key.
if (value == null) {
value = keyWithPercent;
}
// --- OPTIMIZATION 4: CONDITIONAL FORMATTING ---
// The check for `removeFormatting` is inside the loop, but it's a simple boolean
// check and the cost is negligible compared to the string operations.
if (removeFormatting) {
// Note: This assumes `filterDiscordText` is reasonably fast.
// If it's slow, there are further micro-optimizations, but this is the right place for it.
value = filterDiscordText(value);
}
// `appendReplacement` efficiently appends the text between matches and our replacement value.
// `Matcher.quoteReplacement` should handle any '$' or '\' in the replacement value.
matcher.appendReplacement(sb, Matcher.quoteReplacement(value));
} while (matcher.find());
// Append the final part of the string after the last found placeholder.
matcher.appendTail(sb);
// Create the final string from our builder.
String grimReplaced = sb.toString();
return GrimAPI.INSTANCE.getMessagePlaceHolderManager().replacePlaceholders(platformPlayer, grimReplaced).replace(PLACEHOLDER_ESCAPE_CHAR, '%');
}
public static String filterDiscordText(String message) {
@@ -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;
@@ -1018,6 +1018,30 @@ public enum CollisionData implements CollisionFactory {
}
}, StateTypes.DRIED_GHAST),
SHELF((player, version, data, x, y, z) -> {
if (version.isOlderThan(ClientVersion.V_1_21_9)) {
// ViaVersion replacement block (planks)
return new SimpleCollisionBox(0, 0, 0, 1, 1, 1, true);
}
return switch (data.getFacing()) {
case NORTH -> new ComplexCollisionBox(3, new HexCollisionBox(0, 12, 11, 16, 16, 13), new HexCollisionBox(0, 0, 13, 16, 16, 16), new HexCollisionBox(0, 0, 11, 16, 4, 13));
case SOUTH -> new ComplexCollisionBox(3, new HexCollisionBox(0, 12, 3, 16, 16, 5), new HexCollisionBox(0, 0, 0, 16, 16, 3), new HexCollisionBox(0, 0, 3, 16, 4, 5));
case WEST -> new ComplexCollisionBox(3, new HexCollisionBox(11, 12, 0, 13, 16, 16), new HexCollisionBox(13, 0, 0, 16, 16, 16), new HexCollisionBox(11, 0, 0, 13, 4, 16));
case EAST -> new ComplexCollisionBox(3, new HexCollisionBox(3, 12, 0, 5, 16, 16), new HexCollisionBox(0, 0, 0, 3, 16, 16), new HexCollisionBox(3, 0, 0, 5, 4, 16));
default -> throw new IllegalStateException("Unexpected value: " + data.getFacing());
};
}, BlockTags.WOODEN_SHELVES.getStates().toArray(new StateType[0])),
COPPER_GOLEM_STATUE((player, version, data, x, y, z) -> {
if (version.isOlderThan(ClientVersion.V_1_21_9)) {
// ViaVersion replacement block (copper block)
return new SimpleCollisionBox(0, 0, 0, 1, 1, 1, true);
}
return new HexCollisionBox(3, 0, 3, 13, 14, 13);
}, BlockTags.COPPER_GOLEM_STATUES.getStates().toArray(new StateType[0])),
DEFAULT(new SimpleCollisionBox(0, 0, 0, 1, 1, 1, true), StateTypes.STONE);
// This should be an array... but a hashmap will do for now...
@@ -1049,7 +1073,7 @@ public enum CollisionData implements CollisionFactory {
this.materials = mList.toArray(new StateType[0]);
}
private static CollisionBox getAmethystBox(ClientVersion version, com.github.retrooper.packetevents.protocol.world.BlockFace facing, int param_0, int param_1) {
private static CollisionBox getAmethystBox(ClientVersion version, BlockFace facing, int param_0, int param_1) {
if (version.isOlderThanOrEquals(ClientVersion.V_1_16_4))
return NoCollisionBox.INSTANCE;
@@ -1146,7 +1170,11 @@ public enum CollisionData implements CollisionFactory {
// Would pre-computing all states be worth the memory cost? I doubt it
public static CollisionData getData(StateType state) { // TODO: Find a better hack for lava and scaffolding
// What the fuck mojang, why put noCollision() and then give PITCHER_CROP collision?
return state.isSolid() || state == StateTypes.LAVA || state == StateTypes.SCAFFOLDING || state == StateTypes.PITCHER_CROP || state == StateTypes.HEAVY_CORE || state == StateTypes.PALE_MOSS_CARPET || BlockTags.WALL_HANGING_SIGNS.contains(state) ? rawLookupMap.getOrDefault(state, DEFAULT) : NO_COLLISION;
return state.isSolid() || state == StateTypes.LAVA || state == StateTypes.SCAFFOLDING
|| state == StateTypes.PITCHER_CROP || state == StateTypes.HEAVY_CORE
|| state == StateTypes.PALE_MOSS_CARPET || BlockTags.WALL_HANGING_SIGNS.contains(state)
|| BlockTags.COPPER_GOLEM_STATUES.contains(state)
? rawLookupMap.getOrDefault(state, DEFAULT) : NO_COLLISION;
}
// TODO: This is wrong if a block doesn't have any hitbox and isn't specified, light block?
@@ -17,4 +17,16 @@ public class CommonGrimArguments {
public final static SystemArgument<String> PASTE_URL = FACTORY.create(string("PasteUrl", "https://paste.grim.ac/"));
public final static SystemArgument<Platform> PLATFORM_OVERRIDE = FACTORY.create(platform("PlatformOverride"));
/**
* Enables "Fast Bypass" mode for chat messages sent by GrimAC.
* <p>
* <b>BENEFIT:</b> Messages are sent directly as packets, significantly improving
* performance and reducing server overhead especially when lots of alerts are being sent.
* <p>
* <b>TRADE-OFF:</b> This completely bypasses the platform's event system (e.g., Bukkit's chat events).
* Other plugins will NOT be able to see, format, or cancel these messages.
* <p>
* This setting is opt-in (default: false) and requires a server restart to change.
*/
public final static SystemArgument<Boolean> USE_CHAT_FAST_BYPASS = FACTORY.create(string("ChatFastBypass", true));
}
@@ -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;
}
}
@@ -40,7 +40,7 @@ public class ReachInterpolationData {
private int cancelledLerpInterpolationStepsLowBound = Integer.MAX_VALUE;
public ReachInterpolationData(GrimPlayer player, SimpleCollisionBox startingLocation, TrackedPosition position, PacketEntity entity) {
final boolean isPointNine = !player.inVehicle() && player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9);
final boolean unreliableTicking = !player.inVehicle() && player.canSkipTicks();
this.startingLocation = startingLocation;
final Vector3d pos = position.getPos();
@@ -50,7 +50,7 @@ public class ReachInterpolationData {
// 1.9 -> 1.8 precision loss in packets
// (ViaVersion is doing some stuff that makes this code difficult)
if (!isPointNine && PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_9)) {
if (player.getClientVersion().isOlderThan(ClientVersion.V_1_9) && PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_9)) {
targetLocation.expand(0.03125);
}
@@ -66,7 +66,8 @@ public class ReachInterpolationData {
interpolationSteps = 1;
}
if (isPointNine) interpolationStepsHighBound = getInterpolationSteps();
// If the player doesn't tick reliably, their interpolation is anywhere between min and max steps.
if (unreliableTicking) interpolationStepsHighBound = getInterpolationSteps();
}
// While riding entities, there is no interpolation.
@@ -200,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;
}
}
@@ -33,23 +33,26 @@ public enum EquipmentType {
}
if (item == ItemTypes.LEATHER_BOOTS || item == ItemTypes.CHAINMAIL_BOOTS
|| item == ItemTypes.IRON_BOOTS || item == ItemTypes.DIAMOND_BOOTS
|| item == ItemTypes.GOLDEN_BOOTS || item == ItemTypes.NETHERITE_BOOTS) {
|| item == ItemTypes.GOLDEN_BOOTS || item == ItemTypes.NETHERITE_BOOTS
|| item == ItemTypes.COPPER_BOOTS) {
return FEET;
}
if (item == ItemTypes.LEATHER_LEGGINGS || item == ItemTypes.CHAINMAIL_LEGGINGS
|| item == ItemTypes.IRON_LEGGINGS || item == ItemTypes.DIAMOND_LEGGINGS
|| item == ItemTypes.GOLDEN_LEGGINGS || item == ItemTypes.NETHERITE_LEGGINGS) {
|| item == ItemTypes.GOLDEN_LEGGINGS || item == ItemTypes.NETHERITE_LEGGINGS
|| item == ItemTypes.COPPER_LEGGINGS) {
return LEGS;
}
if (item == ItemTypes.LEATHER_CHESTPLATE || item == ItemTypes.CHAINMAIL_CHESTPLATE
|| item == ItemTypes.IRON_CHESTPLATE || item == ItemTypes.DIAMOND_CHESTPLATE
|| item == ItemTypes.GOLDEN_CHESTPLATE || item == ItemTypes.NETHERITE_CHESTPLATE) {
|| item == ItemTypes.GOLDEN_CHESTPLATE || item == ItemTypes.NETHERITE_CHESTPLATE
|| item == ItemTypes.COPPER_CHESTPLATE) {
return CHEST;
}
if (item == ItemTypes.LEATHER_HELMET || item == ItemTypes.CHAINMAIL_HELMET
|| item == ItemTypes.IRON_HELMET || item == ItemTypes.DIAMOND_HELMET
|| item == ItemTypes.GOLDEN_HELMET || item == ItemTypes.NETHERITE_HELMET
|| item == ItemTypes.TURTLE_HELMET) {
|| item == ItemTypes.COPPER_HELMET || item == ItemTypes.TURTLE_HELMET) {
return HEAD;
}
return ItemTypes.SHIELD == item ? OFFHAND : MAINHAND;
@@ -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())));
}
}
}
@@ -66,23 +66,35 @@ public class Vector3dm implements Cloneable, Serializable {
}
public @NotNull Vector3dm add(@NotNull Vector3dm vec) {
this.x += vec.x;
this.y += vec.y;
this.z += vec.z;
return add(vec.x, vec.y, vec.z);
}
public @NotNull Vector3dm add(double x, double y, double z) {
this.x += x;
this.y += y;
this.z += z;
return this;
}
public @NotNull Vector3dm subtract(@NotNull Vector3dm vec) {
this.x -= vec.x;
this.y -= vec.y;
this.z -= vec.z;
return subtract(vec.x, vec.y, vec.z);
}
public @NotNull Vector3dm subtract(double x, double y, double z) {
this.x -= x;
this.y -= y;
this.z -= z;
return this;
}
public @NotNull Vector3dm multiply(@NotNull Vector3dm vec) {
this.x *= vec.x;
this.y *= vec.y;
this.z *= vec.z;
return multiply(vec.x, vec.y, vec.z);
}
public @NotNull Vector3dm multiply(double x, double y, double z) {
this.x *= x;
this.y *= y;
this.z *= z;
return this;
}
@@ -109,11 +121,19 @@ public class Vector3dm implements Cloneable, Serializable {
}
public double distance(@NotNull Vector3dm o) {
return Math.sqrt(GrimMath.square(this.x - o.x) + GrimMath.square(this.y - o.y) + GrimMath.square(this.z - o.z));
return Math.sqrt(distanceSquared(o));
}
public double distanceSquared(@NotNull Vector3dm o) {
return GrimMath.square(this.x - o.x) + GrimMath.square(this.y - o.y) + GrimMath.square(this.z - o.z);
return distanceSquared(o.x, o.y, o.z);
}
public double distance(double oX, double oY, double oZ) {
return Math.sqrt(distanceSquared(oX, oY, oZ));
}
public double distanceSquared(double oX, double oY, double oZ) {
return GrimMath.square(this.x - oX) + GrimMath.square(this.y - oY) + GrimMath.square(this.z - oZ);
}
public @NotNull Vector3dm midpoint(@NotNull Vector3dm other) {
@@ -15,9 +15,13 @@ public class VectorUtils {
@Contract("_, _ -> new")
public static @NotNull Vector3dm cutBoxToVector(@NotNull Vector3dm vectorCutTo, @NotNull SimpleCollisionBox box) {
return new Vector3dm(GrimMath.clamp(vectorCutTo.getX(), box.minX, box.maxX),
GrimMath.clamp(vectorCutTo.getY(), box.minY, box.maxY),
GrimMath.clamp(vectorCutTo.getZ(), box.minZ, box.maxZ));
return cutBoxToVector(vectorCutTo.getX(), vectorCutTo.getY(), vectorCutTo.getZ(), box);
}
public static @NotNull Vector3dm cutBoxToVector(double x, double y, double z, @NotNull SimpleCollisionBox box) {
return new Vector3dm(GrimMath.clamp(x, box.minX, box.maxX),
GrimMath.clamp(y, box.minY, box.maxY),
GrimMath.clamp(z, box.minZ, box.maxZ));
}
@Contract("_ -> new")
@@ -31,7 +31,7 @@ public final class BoundingBoxSize {
return 0.75f;
} else if (EntityTypes.PANDA.equals(type)) {
return 1.3f;
} else if (EntityTypes.BAT.equals(type) || EntityTypes.PARROT.equals(type) || EntityTypes.COD.equals(type) || EntityTypes.EVOKER_FANGS.equals(type) || EntityTypes.TROPICAL_FISH.equals(type) || EntityTypes.FROG.equals(type)) {
} else if (EntityTypes.BAT.equals(type) || EntityTypes.PARROT.equals(type) || EntityTypes.COD.equals(type) || EntityTypes.EVOKER_FANGS.equals(type) || EntityTypes.TROPICAL_FISH.equals(type) || EntityTypes.FROG.equals(type) || EntityTypes.COPPER_GOLEM.equals(type)) {
return 0.5f;
} else if (EntityTypes.ARMADILLO.equals(type) || EntityTypes.BEE.equals(type) || EntityTypes.PUFFERFISH.equals(type) || EntityTypes.SALMON.equals(type) || EntityTypes.SNOW_GOLEM.equals(type) || EntityTypes.CAVE_SPIDER.equals(type)) {
return 0.7f;
@@ -87,7 +87,7 @@ public final class BoundingBoxSize {
return 0.98f;
} else if (EntityTypes.isTypeInstanceOf(type, EntityTypes.MINECART_ABSTRACT)) {
return 0.98f;
} else if (EntityTypes.PLAYER.equals(type)) {
} else if (EntityTypes.PLAYER.equals(type) || EntityTypes.MANNEQUIN.equals(type)) {
return 0.6f;
} else if (EntityTypes.POLAR_BEAR.equals(type)) {
return 1.4f;
@@ -199,7 +199,7 @@ public final class BoundingBoxSize {
return 0.1;
} else if (EntityTypes.EVOKER.equals(type) || EntityTypes.ILLUSIONER.equals(type) || EntityTypes.PILLAGER.equals(type) || EntityTypes.RAVAGER.equals(type) || EntityTypes.VINDICATOR.equals(type) || EntityTypes.WITCH.equals(type)) {
return -0.45;
} else if (EntityTypes.PLAYER.equals(type)) {
} else if (EntityTypes.PLAYER.equals(type) || EntityTypes.MANNEQUIN.equals(type)) {
return -0.35;
}
@@ -337,7 +337,7 @@ public final class BoundingBoxSize {
}
return 1.8f;
} else if (EntityTypes.PLAYER.equals(type)) {
} else if (EntityTypes.PLAYER.equals(type) || EntityTypes.MANNEQUIN.equals(type)) {
return 1.8f;
} else if (EntityTypes.POLAR_BEAR.equals(type)) {
return 1.4f;
@@ -405,6 +405,8 @@ public final class BoundingBoxSize {
return 1.8F;
} else if (EntityTypes.FIREWORK_ROCKET.equals(type)) {
return 0.25F;
} else if (EntityTypes.COPPER_GOLEM.equals(type)) {
return 1.0F;
}
return 1.95f;
}
@@ -63,7 +63,7 @@ public class FluidTypeFlowing {
if ((state.getType() == StateTypes.WATER || state.getType() == StateTypes.LAVA) && state.getLevel() >= 8) {
for (BlockFace enumdirection : new BlockFace[]{BlockFace.NORTH, BlockFace.EAST, BlockFace.SOUTH, BlockFace.WEST}) {
if (isSolidFace(player, originalX, originalY, originalZ, enumdirection) || isSolidFace(player, originalX, originalY + 1, originalZ, enumdirection)) {
vec3d = normalizeVectorWithoutNaN(vec3d).add(new Vector3dm(0.0D, -6.0D, 0.0D));
vec3d = normalizeVectorWithoutNaN(vec3d).add(0.0D, -6.0D, 0.0D);
break;
}
}
@@ -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);
}
}
@@ -30,9 +30,9 @@ public class JumpPower {
if (player.isSprinting) {
float radRotation = GrimMath.radians(player.yaw);
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_20_5)) {
vector.add(new Vector3dm(-player.trigHandler.sin(radRotation) * 0.2, 0.0, player.trigHandler.cos(radRotation) * 0.2));
vector.add(-player.trigHandler.sin(radRotation) * 0.2, 0.0, player.trigHandler.cos(radRotation) * 0.2);
} else {
vector.add(new Vector3dm(-player.trigHandler.sin(radRotation) * 0.2F, 0.0, player.trigHandler.cos(radRotation) * 0.2F));
vector.add(-player.trigHandler.sin(radRotation) * 0.2F, 0.0, player.trigHandler.cos(radRotation) * 0.2F);
}
}
}
@@ -179,9 +179,8 @@ public class ReachUtils {
if (giveMovementThresholdLenience) targetBox.expand(player.getMovementThreshold());
final double[] possibleEyeHeights = player.getPossibleEyeHeights();
for (double eyes : possibleEyeHeights) {
Vector3dm from = new Vector3dm(player.x, player.y + eyes, player.z);
Vector3dm closestPoint = VectorUtils.cutBoxToVector(from, targetBox);
lowest = Math.min(lowest, closestPoint.distance(from));
Vector3dm closestPoint = VectorUtils.cutBoxToVector(player.x, player.y + eyes, player.z, targetBox);
lowest = Math.min(lowest, closestPoint.distance(player.x, player.y + eyes, player.z));
}
return lowest;
@@ -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));
}
}
Loaded 100 of 179 files, more files were not shown because too many files have changed in this diff. Show more