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
189 changed files with 3713 additions and 1873 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 {
@@ -17,7 +17,7 @@ import java.util.concurrent.CompletableFuture;
public class BukkitGrimEntity implements GrimEntity {
protected static final boolean CAN_USE_DIRECT_GETTERS = PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_20_1) && PaperUtils.PAPER;
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;
@@ -63,7 +63,6 @@ public class BukkitGrimEntity implements GrimEntity {
if (bukkitPlatformWorld == null || !bukkitPlatformWorld.getBukkitWorld().equals(entity.getWorld())) {
bukkitPlatformWorld = new BukkitPlatformWorld(entity.getWorld());
}
return bukkitPlatformWorld;
}
@@ -4,8 +4,7 @@ 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.TrackedPosition;
import ac.grim.grimac.utils.data.PistonTemplate;
import com.github.retrooper.packetevents.protocol.world.BlockFace;
import org.bukkit.Material;
import org.bukkit.block.Block;
@@ -27,11 +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(final int aX, final int aY, final int aZ,
final double bX, final double bY, final double bZ) {
return Math.abs(aX - bX) <= MAX_HORIZONTAL_DISTANCE
&& Math.abs(aY - bY) <= MAX_VERTICAL_DISTANCE
&& Math.abs(aZ - bZ) <= 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,21 +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 int sourceX = piston.getX();
final int sourceY = piston.getY();
final int sourceZ = piston.getZ();
for (GrimPlayer player : GrimAPI.INSTANCE.getPlayerDataManager().getEntries()) {
TrackedPosition position = player.compensatedEntities.self.trackedServerPosition;
if (player.compensatedWorld.isChunkLoaded(chunkX, chunkZ) && isCloseEnough(sourceX, sourceY, sourceZ, position.getX(), position.getY(), position.getZ())) {
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
@@ -104,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(),
@@ -130,20 +114,19 @@ public class PistonEvent implements Listener {
}
}
final Block block = event.getBlock();
final int chunkX = block.getX() >> 4;
final int chunkZ = block.getZ() >> 4;
final int sourceX = block.getX();
final int sourceY = block.getY();
final int sourceZ = block.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()) {
TrackedPosition position = player.compensatedEntities.self.trackedServerPosition;
if (player.compensatedWorld.isChunkLoaded(chunkX, chunkZ) && isCloseEnough(sourceX, sourceY, sourceZ, position.getX(), position.getY(), position.getZ())) {
int lastTrans = player.lastTransactionSent.get();
PistonData data = new PistonData(face, boxes, lastTrans, false, hasSlimeBlock, hasHoneyBlock);
player.latencyUtils.addRealTimeTaskAsync(lastTrans, () -> player.compensatedWorld.activePistons.add(data));
}
if (player.compensatedWorld.isChunkLoaded(blockX >> 4, blockZ >> 4) || !isCloseEnough(blockX, blockY, blockZ, player.x, player.y, player.z)) continue;
int lastTrans = player.lastTransactionSent.get();
player.latencyUtils.addRealTimeTaskAsync(lastTrans, () -> {
player.compensatedWorld.addPiston(pistonTemplate, lastTrans);
});
}
}
}
@@ -31,7 +31,6 @@ import lombok.Getter;
import org.incendo.cloud.CommandManager;
import org.jetbrains.annotations.NotNull;
@Getter
public final class GrimAPI {
public static final GrimAPI INSTANCE = new GrimAPI();
@@ -170,7 +170,8 @@ public class Check extends GrimProcessor implements AbstractCheck {
}
public static boolean isAsync(PacketTypeCommon packetType) {
return packetType == PacketType.Play.Client.KEEP_ALIVE;
return packetType == PacketType.Play.Client.KEEP_ALIVE
|| packetType == PacketType.Play.Client.CHUNK_BATCH_ACK;
}
public boolean isUpdate(PacketTypeCommon packetType) {
@@ -8,7 +8,7 @@ public abstract class AbstractDebugHandler extends Check {
super(player);
}
public abstract void toggleListener(GrimPlayer player);
public abstract boolean toggleListener(GrimPlayer player);
public abstract boolean toggleConsoleOutput();
}
@@ -0,0 +1,205 @@
package ac.grim.grimac.checks.debug;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.collisions.datatypes.CollisionBox;
import ac.grim.grimac.utils.collisions.datatypes.NoCollisionBox;
import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
import ac.grim.grimac.utils.data.Pair;
import ac.grim.grimac.utils.math.Vector3dm;
import com.github.retrooper.packetevents.netty.buffer.ByteBufHelper;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPluginMessage;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* Handles debug visualization of hitboxes and reach calculations for GrimAC.
* Sends debug data to clients through plugin messages that can be visualized by compatible clients.
*/
public class HitboxDebugHandler extends AbstractDebugHandler {
/**
* Set of players currently listening to hitbox debug data
*/
private final Set<GrimPlayer> listeners = new CopyOnWriteArraySet<>(new HashSet<>());
/**
* Creates a new HitboxDebugHandler for the specified player
*
* @param grimPlayer The GrimAC player instance to debug
*/
public HitboxDebugHandler(GrimPlayer grimPlayer) {
super(grimPlayer);
}
/**
* Toggles whether a player receives hitbox debug visualization data.
* If the player is already listening, they will be removed. If not, they will be added.
*
* @param player The player to toggle debug visualization for
* @return {@code true} if the player is now listening (was added),
* {@code false} if the player is no longer listening (was removed).
*/
@Override
public boolean toggleListener(GrimPlayer player) {
boolean wasPresent = listeners.remove(player);
if (wasPresent) {
return false;
} else {
listeners.add(player);
return true;
}
}
@Override
public boolean toggleConsoleOutput() {
throw new UnsupportedOperationException();
}
/**
* Sends debug visualization data for reach calculations to all listening players.
* The data includes hitboxes, target entities, look vectors, and eye heights used in reach calculations.
*
* @param hitboxes Map of entity IDs to their collision boxes
* @param targetEntities Set of entity IDs that are considered targets
* @param lookVecsAndEyeHeights List of pairs containing look vectors and their corresponding eye heights
* @param basePos Base position before eye height adjustments
* @param isPrediction Whether these hitboxes are from a prediction calculation
*
* Packet Format (Version 1):
* - Byte: Version (0)
* - Byte: Global flags
* - Bit 0: isPrediction
* - Bits 1-7: Reserved
* - Double: Player reach/interact distance
* - Vector3d: Base position (3 doubles)
* - VarInt: Number of ray traces
* - For each ray trace:
* - Double: Eye height delta
* - Vector3d: Look vector (3 doubles)
* - VarInt: Number of hitboxes
* - For each hitbox:
* - VarInt: Entity ID
* - Byte: Flags
* - Bit 0: Is target entity
* - Bit 1: Is no collision
* - Bits 2-7: Reserved
* - If not NoCollisionBox:
* - Double: minX
* - Double: minY
* - Double: minZ
* - Double: maxX
* - Double: maxY
* - Double: maxZ
*/
public void sendHitboxData(Map<Integer, CollisionBox> hitboxes, Set<Integer> targetEntities,
List<Pair<Vector3dm, Double>> lookVecsAndEyeHeights, Vector3dm basePos,
boolean isPrediction, double reachDistance) {
if (!isEnabled()) return;
ByteBuf buffer = Unpooled.buffer();
try {
// Version Header
buffer.writeByte(1);
// Global Flags Header
// Pack boolean flags into a single byte
byte global_flags = 0;
global_flags |= (isPrediction ? 1 : 0); // Bit 0: are the hitboxes from a prediction?
// Bits 2-7 reserved for future use
buffer.writeByte(global_flags);
// Write reach distance
buffer.writeDouble(reachDistance);
// Write base position
writeVector(buffer, basePos);
// Write number of ray traces
ByteBufHelper.writeVarInt(buffer, lookVecsAndEyeHeights.size());
// Write all possible ray traces
for (Pair<Vector3dm, Double> pair : lookVecsAndEyeHeights) {
Vector3dm lookVec = pair.first();
double eyeHeight = pair.second();
// Write eye height delta and look vector
// we make them 3 meters shorter because all of the vectors passed into here are made 3 meters longer
// then they need to be so grim can report correct reach distance for too far away hits
buffer.writeDouble(eyeHeight);
writeVector(buffer, lookVec);
}
// Write number of hitboxes
ByteBufHelper.writeVarInt(buffer, hitboxes.size());
// Write hitbox data
for (Map.Entry<Integer, CollisionBox> entry : hitboxes.entrySet()) {
int entityId = entry.getKey();
CollisionBox box = entry.getValue();
// Write entity ID
ByteBufHelper.writeVarInt(buffer, entityId);
// Pack boolean flags into a single byte
byte flags = 0;
flags |= (targetEntities.contains(entityId) ? 1 : 0); // Bit 0: Is target
flags |= (box == NoCollisionBox.INSTANCE ? 2 : 0); // Bit 1: Is no collision
// Bits 2-7 reserved for future use
buffer.writeByte(flags);
// Write box coordinates if it's not a NoCollisionBox
if ((flags & 2) == 0) {
SimpleCollisionBox simpleCollisionBox = (SimpleCollisionBox) box;
buffer.writeDouble(simpleCollisionBox.minX);
buffer.writeDouble(simpleCollisionBox.minY);
buffer.writeDouble(simpleCollisionBox.minZ);
buffer.writeDouble(simpleCollisionBox.maxX);
buffer.writeDouble(simpleCollisionBox.maxY);
buffer.writeDouble(simpleCollisionBox.maxZ);
}
}
// Convert buffer to byte array
byte[] data = new byte[buffer.readableBytes()];
buffer.readBytes(data);
// Create and send packet
WrapperPlayServerPluginMessage packet = new WrapperPlayServerPluginMessage(
"grim:debug_hitbox",
data
);
// Send to all listeners
for (GrimPlayer listener : listeners) {
if (listener != null) {
listener.user.sendPacket(packet);
}
}
} finally {
// Release buffer to prevent memory leaks
buffer.release();
}
}
public boolean isEnabled() {
return !listeners.isEmpty();
}
/**
* Helper method to write a Vector to the ByteBuf
* @param buffer The buffer to write to
* @param vector The vector to write
*/
private void writeVector(ByteBuf buffer, Vector3dm vector) {
buffer.writeDouble(vector.getX());
buffer.writeDouble(vector.getY());
buffer.writeDouble(vector.getZ());
}
}
@@ -20,8 +20,8 @@ public class AirLiquidBreak extends Check implements BlockBreakCheck {
private int lastTick;
private boolean didLastFlag;
// Initialize to non-null values to prevent NPE when checking for blockType properties and if position equals old position
private int lastX, lastY, lastZ;
private float lastHardness, lastBlastResistance;
private @NotNull Vector3i lastBreakLoc = new Vector3i();
private @NotNull StateType lastBlockType = StateTypes.AIR;
public AirLiquidBreak(GrimPlayer player) {
super(player);
@@ -40,20 +40,15 @@ public class AirLiquidBreak extends Check implements BlockBreakCheck {
// I am explicitly making this patch as narrow and specific as possible to potentially discover other blocks that exhibit similar behaviour
int newTick = GrimAPI.INSTANCE.getTickManager().currentTick;
if (lastTick == newTick
&& lastX == blockBreak.x
&& lastY == blockBreak.y
&& lastZ == blockBreak.z
&& lastBreakLoc.equals(blockBreak.position)
&& !didLastFlag
&& lastHardness == 0.0F
&& lastBlastResistance == 0.0F
&& lastBlockType.getHardness() == 0.0F
&& lastBlockType.getBlastResistance() == 0.0F
&& block == StateTypes.WATER
) return;
lastTick = newTick;
lastX = blockBreak.x;
lastY = blockBreak.y;
lastZ = blockBreak.z;
lastHardness = block.getHardness();
lastBlastResistance = block.getBlastResistance();
lastBreakLoc = blockBreak.position;
lastBlockType = block;
// the block does not have a hitbox
boolean invalid = (block == StateTypes.LIGHT && !(player.inventory.getHeldItem().is(ItemTypes.LIGHT) || player.inventory.getOffHand().is(ItemTypes.LIGHT)))
@@ -25,7 +25,7 @@ public class FarBreak extends Check implements BlockBreakCheck {
double min = Double.MAX_VALUE;
for (double d : player.getPossibleEyeHeights()) {
SimpleCollisionBox box = new SimpleCollisionBox(blockBreak.x, blockBreak.y, blockBreak.z);
SimpleCollisionBox box = new SimpleCollisionBox(blockBreak.position);
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));
}
@@ -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;
@@ -36,11 +36,8 @@ public class FastBreak extends Check implements BlockBreakCheck {
super(playerData);
}
// The block the player is currently breaking, represented by primitives for performance.
// `targetBlockIsSet` tracks nullness of the position, since primitives can't be null
private boolean targetBlockIsSet;
private int targetBlockX, targetBlockY, targetBlockZ;
// The block the player is currently breaking
Vector3i targetBlockPosition = null;
// The maximum amount of damage the player deals to the block
//
double maximumBlockDamage = 0;
@@ -70,11 +67,8 @@ public class FastBreak extends Check implements BlockBreakCheck {
// * can we translate back "up" to server version and run check against server version to avoid loading older registries?
WrappedBlockState block = clientOlderThanServer ? WrappedBlockState.getByGlobalId(player.getClientVersion(), player.getViaTranslatedClientBlockID(blockBreak.block.getGlobalId())) : blockBreak.block;
startBreak = System.currentTimeMillis() - (!targetBlockIsSet ? 50 : 0); // ???
targetBlockIsSet = true;
targetBlockX = blockBreak.x;
targetBlockY = blockBreak.y;
targetBlockZ = blockBreak.z;
startBreak = System.currentTimeMillis() - (targetBlockPosition == null ? 50 : 0); // ???
targetBlockPosition = blockBreak.position;
maximumBlockDamage = BlockBreakSpeed.getBlockDamage(player, block);
@@ -93,7 +87,9 @@ public class FastBreak extends Check implements BlockBreakCheck {
}
clampBalance();
} else if (blockBreak.action == DiggingAction.FINISHED_DIGGING && targetBlockIsSet) {
}
if (blockBreak.action == DiggingAction.FINISHED_DIGGING && targetBlockPosition != null) {
double predictedTime = Math.ceil(1 / maximumBlockDamage) * 50;
double realTime = System.currentTimeMillis() - startBreak;
double diff = predictedTime - realTime;
@@ -121,8 +117,8 @@ public class FastBreak extends Check implements BlockBreakCheck {
public void onPacketReceive(PacketReceiveEvent event) {
// Find the most optimal block damage using the animation packet, which is sent at least once a tick when breaking blocks
// On 1.8 clients, via screws with this packet meaning we must fall back to the 1.8 idle flying packet
if ((player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9) ? event.getPacketType() == PacketType.Play.Client.ANIMATION : WrapperPlayClientPlayerFlying.isFlying(event.getPacketType())) && targetBlockIsSet) {
maximumBlockDamage = Math.max(maximumBlockDamage, BlockBreakSpeed.getBlockDamage(player, player.compensatedWorld.getBlock(targetBlockX, targetBlockY, targetBlockZ)));
if ((player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9) ? event.getPacketType() == PacketType.Play.Client.ANIMATION : WrapperPlayClientPlayerFlying.isFlying(event.getPacketType())) && targetBlockPosition != null) {
maximumBlockDamage = Math.max(maximumBlockDamage, BlockBreakSpeed.getBlockDamage(player, player.compensatedWorld.getBlock(targetBlockPosition)));
}
}
@@ -21,9 +21,7 @@ public class MultiBreak extends Check implements BlockBreakCheck {
private final List<String> flags = new ArrayList<>();
private boolean hasBroken;
private BlockFace lastFace;
private int lastX;
private int lastY;
private int lastZ;
private Vector3i lastPos;
public MultiBreak(GrimPlayer player) {
super(player);
@@ -35,22 +33,10 @@ public class MultiBreak extends Check implements BlockBreakCheck {
return;
}
final int currentX = blockBreak.x;
final int currentY = blockBreak.y;
final int currentZ = blockBreak.z;
if (hasBroken && (blockBreak.face != lastFace ||
currentX != lastX ||
currentY != lastY ||
currentZ != lastZ)) {
final String currentPosString = MessageUtil.toUnlabeledString(currentX, currentY, currentZ);
final String lastPosString = MessageUtil.toUnlabeledString(lastX, lastY, lastZ);
if (hasBroken && (blockBreak.face != lastFace || !blockBreak.position.equals(lastPos))) {
final String verbose = "face=" + blockBreak.face + ", lastFace=" + lastFace
+ ", pos=" + currentPosString
+ ", lastPos=" + lastPosString;
+ ", pos=" + MessageUtil.toUnlabledString(blockBreak.position)
+ ", lastPos=" + MessageUtil.toUnlabledString(lastPos);
if (!player.canSkipTicks()) {
if (flagAndAlert(verbose) && shouldModifyPackets()) {
blockBreak.cancel();
@@ -61,9 +47,7 @@ public class MultiBreak extends Check implements BlockBreakCheck {
}
lastFace = blockBreak.face;
lastX = currentX;
lastY = currentY;
lastZ = currentZ;
lastPos = blockBreak.position;
hasBroken = true;
}
@@ -7,7 +7,6 @@ import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.update.BlockBreak;
import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
import ac.grim.grimac.utils.data.Pair;
import ac.grim.grimac.utils.data.TeleportData;
import ac.grim.grimac.utils.math.Vector3dm;
import ac.grim.grimac.utils.nmsutil.Ray;
import ac.grim.grimac.utils.nmsutil.ReachUtils;
@@ -15,7 +14,6 @@ import com.github.retrooper.packetevents.protocol.attribute.Attributes;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.protocol.player.DiggingAction;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.protocol.teleport.RelativeFlag;
import com.github.retrooper.packetevents.protocol.world.BlockFace;
import com.github.retrooper.packetevents.util.Vector3d;
import com.github.retrooper.packetevents.util.Vector3f;
@@ -72,7 +70,7 @@ public class RotationBreak extends Check implements BlockBreakCheck {
}
private boolean didRayTraceHit(BlockBreak blockBreak) {
SimpleCollisionBox box = new SimpleCollisionBox(blockBreak.x, blockBreak.y, blockBreak.z);
SimpleCollisionBox box = new SimpleCollisionBox(blockBreak.position);
final double[] possibleEyeHeights = player.getPossibleEyeHeights();
@@ -113,8 +111,7 @@ public class RotationBreak extends Check implements BlockBreakCheck {
for (Vector3f lookDir : possibleLookDirs) {
Vector3d starting = new Vector3d(player.x, player.y + d, player.z);
Ray trace = new Ray(player, starting.getX(), starting.getY(), starting.getZ(), lookDir.getX(), lookDir.getY());
Vector3dm pointAtDistance = trace.getPointAtDistance(distance);
Pair<Vector3dm, BlockFace> intercept = ReachUtils.calculateIntercept(box, trace.getOriginX(), trace.getOriginY(), trace.getOriginZ(), pointAtDistance.getX(), pointAtDistance.getY(), pointAtDistance.getZ());
Pair<Vector3dm, BlockFace> intercept = ReachUtils.calculateIntercept(box, trace.getOrigin(), trace.getPointAtDistance(distance));
if (intercept.first() != null) return true;
}
@@ -19,14 +19,7 @@ import static ac.grim.grimac.utils.nmsutil.BlockBreakSpeed.getBlockDamage;
public class WrongBreak extends Check implements BlockBreakCheck {
private final int exemptedY = player.getClientVersion().isOlderThan(ClientVersion.V_1_8) ? 255 : (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_14) ? -1 : 4095);
private boolean lastBlockWasInstantBreak = false;
private boolean lastBlockIsSet;
private int lastBlockX, lastBlockY, lastBlockZ;
private boolean lastCancelledBlockIsSet;
private int lastCancelledBlockX, lastCancelledBlockY, lastCancelledBlockZ;
private boolean lastLastBlockIsSet;
private Vector3i lastBlock, lastCancelledBlock, lastLastBlock = null;
public WrongBreak(final GrimPlayer player) {
super(player);
@@ -34,7 +27,8 @@ public class WrongBreak extends Check implements BlockBreakCheck {
// The client sometimes sends a wierd cancel packet
private boolean shouldExempt(final WrappedBlockState block, int yPos) {
if (lastLastBlockIsSet || !lastBlockIsSet)
// lastLastBlock is always null when this happens, and lastBlock isn't
if (lastLastBlock != null || lastBlock == null)
return false;
// on pre 1.14.4 clients, the YPos of this packet is always the same
@@ -47,29 +41,22 @@ public class WrongBreak extends Check implements BlockBreakCheck {
@Override
public void onBlockBreak(BlockBreak blockBreak) {
final int x = blockBreak.x;
final int y = blockBreak.y;
final int z = blockBreak.z;
if (blockBreak.action == DiggingAction.START_DIGGING) {
final Vector3i pos = blockBreak.position;
lastBlockWasInstantBreak = getBlockDamage(player, blockBreak.block) >= 1;
lastCancelledBlockIsSet = false;
lastCancelledBlock = null;
lastLastBlock = lastBlock;
lastBlock = pos;
}
lastLastBlockIsSet = lastBlockIsSet;
if (blockBreak.action == DiggingAction.CANCELLED_DIGGING) {
final Vector3i pos = blockBreak.position;
lastBlockIsSet = true;
lastBlockX = x;
lastBlockY = y;
lastBlockZ = z;
} else if (blockBreak.action == DiggingAction.CANCELLED_DIGGING) {
boolean notLastBlock = !lastBlockIsSet || x != lastBlockX || y != lastBlockY || z != lastBlockZ;
if (!shouldExempt(blockBreak.block, y) && notLastBlock) {
if (!shouldExempt(blockBreak.block, pos.y) && !pos.equals(lastBlock)) {
// https://github.com/GrimAnticheat/Grim/issues/1512
boolean isLastCancelled = lastCancelledBlockIsSet && x == lastCancelledBlockX && y == lastCancelledBlockY && z == lastCancelledBlockZ;
if (player.getClientVersion().isOlderThan(ClientVersion.V_1_14_4) || (!lastBlockWasInstantBreak && isLastCancelled)) {
String lastBlockStr = lastBlockIsSet ? MessageUtil.toUnlabeledString(lastBlockX, lastBlockY, lastBlockZ) : "null";
if (flagAndAlert("action=CANCELLED_DIGGING" + ", last=" + lastBlockStr + ", pos=" + MessageUtil.toUnlabeledString(x, y, z))) {
if (player.getClientVersion().isOlderThan(ClientVersion.V_1_14_4) || (!lastBlockWasInstantBreak && pos.equals(lastCancelledBlock))) {
if (flagAndAlert("action=CANCELLED_DIGGING" + ", last=" + MessageUtil.toUnlabledString(lastBlock) + ", pos=" + MessageUtil.toUnlabledString(pos))) {
if (shouldModifyPackets()) {
blockBreak.cancel();
}
@@ -77,20 +64,18 @@ public class WrongBreak extends Check implements BlockBreakCheck {
}
}
lastCancelledBlockIsSet = true;
lastCancelledBlockX = x;
lastCancelledBlockY = y;
lastCancelledBlockZ = z;
lastCancelledBlock = pos;
lastLastBlock = null;
lastBlock = null;
return;
}
lastLastBlockIsSet = lastBlockIsSet = false;
} else if (blockBreak.action == DiggingAction.FINISHED_DIGGING) {
boolean notLastCancelled = !lastCancelledBlockIsSet || x != lastCancelledBlockX || y != lastCancelledBlockY || z != lastCancelledBlockZ;
boolean notLastBlock = !lastBlockIsSet || x != lastBlockX || y != lastBlockY || z != lastBlockZ;
if (blockBreak.action == DiggingAction.FINISHED_DIGGING) {
final Vector3i pos = blockBreak.position;
// when a player looks away from the mined block, they send a cancel, and if they look at it again, they don't send another start. (thanks mojang!)
if (notLastCancelled && (!lastBlockWasInstantBreak || player.getClientVersion().isOlderThan(ClientVersion.V_1_14_4)) && notLastBlock) {
String lastBlockStr = lastBlockIsSet ? MessageUtil.toUnlabeledString(lastBlockX, lastBlockY, lastBlockZ) : "null";
if (flagAndAlert("action=FINISHED_DIGGING" + ", last=" + lastBlockStr + ", pos=" + MessageUtil.toUnlabeledString(x, y, z))) {
if (!pos.equals(lastCancelledBlock) && (!lastBlockWasInstantBreak || player.getClientVersion().isOlderThan(ClientVersion.V_1_14_4)) && !pos.equals(lastBlock)) {
if (flagAndAlert("action=FINISHED_DIGGING" + ", last=" + MessageUtil.toUnlabledString(lastBlock) + ", pos=" + MessageUtil.toUnlabledString(pos))) {
if (shouldModifyPackets()) {
blockBreak.cancel();
}
@@ -99,7 +84,9 @@ public class WrongBreak extends Check implements BlockBreakCheck {
// 1.14.4+ clients don't send another start break in protected regions
if (player.getClientVersion().isOlderThan(ClientVersion.V_1_14_4)) {
lastCancelledBlockIsSet = lastLastBlockIsSet = lastBlockIsSet = false;
lastCancelledBlock = null;
lastLastBlock = null;
lastBlock = null;
}
}
}
@@ -4,8 +4,6 @@ import ac.grim.grimac.checks.Check;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.PacketCheck;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.reflection.ViaVersionUtil;
import ac.grim.grimac.utils.via.ViaHooks;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
@@ -63,10 +61,4 @@ public class ChatB extends Check implements PacketCheck {
}
return false;
}
static {
if (ViaVersionUtil.isAvailable && PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_11)) {
ViaHooks.injectVia();
}
}
}
@@ -0,0 +1,13 @@
package ac.grim.grimac.checks.impl.combat;
import ac.grim.grimac.checks.Check;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.PacketCheck;
import ac.grim.grimac.player.GrimPlayer;
@CheckData(name = "EntityPierce", configName = "EntityPierce", setback = 30)
public class EntityPierce extends Check implements PacketCheck {
public EntityPierce(GrimPlayer player) {
super(player);
}
}
@@ -18,13 +18,22 @@ package ac.grim.grimac.checks.impl.combat;
import ac.grim.grimac.api.config.ConfigManager;
import ac.grim.grimac.checks.Check;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.debug.HitboxDebugHandler;
import ac.grim.grimac.checks.type.PacketCheck;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.collisions.datatypes.CollisionBox;
import ac.grim.grimac.utils.collisions.datatypes.NoCollisionBox;
import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
import ac.grim.grimac.utils.data.BlockHitData;
import ac.grim.grimac.utils.data.EntityHitData;
import ac.grim.grimac.utils.data.HitData;
import ac.grim.grimac.utils.data.Pair;
import ac.grim.grimac.utils.data.packetentity.PacketEntity;
import ac.grim.grimac.utils.data.packetentity.PacketEntitySizeable;
import ac.grim.grimac.utils.data.packetentity.dragon.PacketEntityEnderDragonPart;
import ac.grim.grimac.utils.math.Vector3dm;
import ac.grim.grimac.utils.nmsutil.ReachUtils;
import ac.grim.grimac.utils.nmsutil.WorldRayTrace;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.attribute.Attributes;
import com.github.retrooper.packetevents.protocol.entity.type.EntityType;
@@ -32,16 +41,17 @@ import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState;
import com.github.retrooper.packetevents.util.Vector3d;
import com.github.retrooper.packetevents.util.Vector3i;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerFlying;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.*;
// You may not copy the check unless you are licensed under GPL
@CheckData(name = "Reach", setback = 10)
@@ -55,8 +65,14 @@ public class Reach extends Check implements PacketCheck {
// Only one flag per reach attack, per entity, per tick.
// We store position because lastX isn't reliable on teleports.
private final Int2ObjectMap<Vector3d> playerAttackQueue = new Int2ObjectOpenHashMap<>();
// temporarily used to prevent falses in the wall hit check
private final Set<Vector3i> blocksChangedThisTick = new HashSet<>();
// extra distance to raytrace beyond player reach distance so we know how far beyond the legit distance a cheater hit
public static final double extraSearchDistance = 3;
private boolean ignoreNonPlayerTargets;
private boolean cancelImpossibleHits;
private double threshold;
public double threshold;
private double cancelBuffer; // For the next 4 hits after using reach, we aggressively cancel reach
public Reach(GrimPlayer player) {
@@ -88,6 +104,10 @@ public class Reach extends Check implements PacketCheck {
return;
}
if (ignoreNonPlayerTargets && !entity.type.equals(EntityTypes.PLAYER)) {
return;
}
// Dead entities cause false flags (https://github.com/GrimAnticheat/Grim/issues/546)
if (entity.isDead) return;
@@ -114,8 +134,9 @@ public class Reach extends Check implements PacketCheck {
}
// If the player set their look, or we know they have a new tick
final boolean isFlying = WrapperPlayClientPlayerFlying.isFlying(event.getPacketType());
if (isUpdate(event.getPacketType())) {
tickBetterReachCheckWithAngle();
tickBetterReachCheckWithAngle(isFlying);
}
}
@@ -143,16 +164,13 @@ public class Reach extends Check implements PacketCheck {
} else {
SimpleCollisionBox targetBox = reachEntity.getPossibleCollisionBoxes();
if (reachEntity.type == EntityTypes.END_CRYSTAL) {
final double x = reachEntity.trackedServerPosition.getX();
final double y = reachEntity.trackedServerPosition.getX();
final double z = reachEntity.trackedServerPosition.getX();
targetBox = new SimpleCollisionBox(x - 1, y, z - 1, x + 1, y + 2, z + 1);
targetBox = new SimpleCollisionBox(reachEntity.trackedServerPosition.getPos().subtract(1, 0, 1), reachEntity.trackedServerPosition.getPos().add(1, 2, 1));
}
return ReachUtils.getMinReachToBox(player, targetBox) > player.compensatedEntities.self.getAttributeValue(Attributes.ENTITY_INTERACTION_RANGE);
}
}
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;
@@ -161,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
@@ -178,10 +213,7 @@ public class Reach extends Check implements PacketCheck {
SimpleCollisionBox targetBox = reachEntity.getPossibleCollisionBoxes();
if (reachEntity.type == EntityTypes.END_CRYSTAL) { // Hardcode end crystal box
final double x = reachEntity.trackedServerPosition.getX();
final double y = reachEntity.trackedServerPosition.getX();
final double z = reachEntity.trackedServerPosition.getX();
targetBox = new SimpleCollisionBox(x - 1, y, z - 1, x + 1, y + 2, z + 1);
targetBox = new SimpleCollisionBox(reachEntity.trackedServerPosition.getPos().subtract(1, 0, 1), reachEntity.trackedServerPosition.getPos().add(1, 2, 1));
}
// 1.7 and 1.8 players get a bit of extra hitbox (this is why you should use 1.8 on cross version servers)
@@ -202,56 +234,68 @@ 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 double eyePosX = from.getX();
final double eyePosZ = from.getZ();
final Vector3dm[] possibleLookDirs = player.getPossibleLookVectors(isPrediction);
final Vector3dm eyePos = new Vector3dm(from.getX(), 0, from.getZ());
for (Vector3dm lookVec : possibleLookDirs) {
for (double eye : possibleEyeHeights) {
final double eyePosY = from.getY() + eye;
eyePos.setY(from.getY() + eye);
Vector3dm endReachPos = eyePos.clone().add(lookVec.getX() * distance, lookVec.getY() * distance, lookVec.getZ() * distance);
final double endX = eyePosX + lookVec.getX() * distance;
final double endY = eyePosY + lookVec.getY() * distance;
final double endZ = eyePosZ + lookVec.getZ() * distance;
Vector3dm intercept = ReachUtils.calculateIntercept(targetBox, eyePos, endReachPos).first();
Vector3dm intercept = ReachUtils.calculateIntercept(targetBox, eyePosX, eyePosY, eyePosZ, endX, endY, endZ).first();
if (ReachUtils.isVecInside(targetBox, eyePosX, eyePosY, eyePosZ)) {
if (ReachUtils.isVecInside(targetBox, eyePos)) {
minDistance = 0;
break;
}
if (intercept != null) {
minDistance = Math.min(intercept.distance(eyePosX, eyePosY, eyePosZ), minDistance);
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) {
@@ -267,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) {
@@ -280,4 +325,68 @@ public class Reach extends Check implements PacketCheck {
return type != ResultType.NONE;
}
}
public void handleBlockChange(Vector3i vector3i, WrappedBlockState state) {
if (blocksChangedThisTick.size() >= 40) return; // Don't let players freeze movement packets to grow this
// Only do this for nearby blocks
if (new Vector3dm(vector3i.x, vector3i.y, vector3i.z).distanceSquared(new Vector3dm(player.x, player.y, player.z)) > 6) return;
// Only do this if the state really had any world impact
if (state.equals(player.compensatedWorld.getBlock(vector3i))) return;
blocksChangedThisTick.add(vector3i);
}
private boolean hitboxDebuggingEnabled() {
return player.checkManager.getCheck(HitboxDebugHandler.class).isEnabled();
}
private void sendHitboxDebugData(PacketEntity reachEntity, Vector3d from, List<Pair<Vector3dm, Double>> lookVecsAndEyeHeights, boolean isPrediction) {
Map<Integer, CollisionBox> hitboxes = new HashMap<>();
for (Int2ObjectMap.Entry<PacketEntity> entry : player.compensatedEntities.entityMap.int2ObjectEntrySet()) {
PacketEntity entity = entry.getValue();
if (!entity.canHit()) continue;
CollisionBox box;
if (entity.equals(reachEntity)) {
// Target entity gets expanded hitbox
box = entity.getPossibleCollisionBoxes();
SimpleCollisionBox sBox = (SimpleCollisionBox) box;
sBox.expand(threshold);
// Add movement threshold uncertainty for 1.9+ or non-position updates
if (!player.packetStateData.didLastLastMovementIncludePosition
|| player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9)) {
sBox.expand(player.getMovementThreshold());
}
} else {
// Non-target entities
box = entity.getMinimumPossibleCollisionBoxes();
if (box instanceof NoCollisionBox) {
hitboxes.put(entry.getIntKey(), NoCollisionBox.INSTANCE);
continue;
} else if (box instanceof SimpleCollisionBox sBox) {
sBox.expand(-threshold);
// Shrink non-target entities by movement threshold when applicable
if (!player.packetStateData.didLastLastMovementIncludePosition
|| player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9)) {
sBox.expand(-player.getMovementThreshold());
}
}
}
// Add 1.8 and below extra hitbox size
if (player.getClientVersion().isOlderThan(ClientVersion.V_1_9)
&& box instanceof SimpleCollisionBox) {
((SimpleCollisionBox) box).expand(0.1f);
}
hitboxes.put(entry.getIntKey(), box);
}
player.checkManager.getCheck(HitboxDebugHandler.class).sendHitboxData(hitboxes,
Collections.singleton(player.compensatedEntities.getPacketEntityID(reachEntity)),
lookVecsAndEyeHeights,
new Vector3dm(from.getX(), from.getY(), from.getZ()),
isPrediction, player.compensatedEntities.self.getAttributeValue(Attributes.ENTITY_INTERACTION_RANGE));
}
}
@@ -0,0 +1,13 @@
package ac.grim.grimac.checks.impl.combat;
import ac.grim.grimac.checks.Check;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.PacketCheck;
import ac.grim.grimac.player.GrimPlayer;
@CheckData(name = "WallHit", configName = "WallHit", setback = 20)
public class WallHit extends Check implements PacketCheck {
public WallHit(GrimPlayer player) {
super(player);
}
}
@@ -29,7 +29,7 @@ public class ExploitA extends Check implements PacketCheck {
if (event.getPacketType() == PacketType.Play.Client.PLUGIN_MESSAGE && PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_13)) {
WrapperPlayClientPluginMessage wrapper = new WrapperPlayClientPluginMessage(event);
if (wrapper.getChannelName().equals("MC|ItemName")) {
check(PayloadItemName.CODEC.read(wrapper.getData()).itemName(), event);
check(new PayloadItemName(wrapper.getData()).itemName(), event);
}
}
}
@@ -72,7 +72,7 @@ public class ExploitB extends Check implements PacketCheck {
}
}
ItemStack newBook = PayloadBookEdit.CODEC.read(wrapper.getData()).itemStack();
ItemStack newBook = new PayloadBookEdit(wrapper.getData()).itemStack();
checkItemStack(newBook, signing, event);
oldBook = null;
}
@@ -287,13 +287,14 @@ public class ExploitB extends Check implements PacketCheck {
return true;
}
if (containsInvalidCharacters(page, true)) {
if (flagAndAlert("invalid page characters") && shouldModifyPackets()) {
event.setCancelled(true);
player.onPacketCancel();
}
return true;
}
// commented out because existing characters are not considered
// if (containsInvalidCharacters(page, true)) {
// if (flagAndAlert("invalid page characters") && shouldModifyPackets()) {
// event.setCancelled(true);
// player.onPacketCancel();
// }
// return true;
// }
return false;
}
@@ -0,0 +1,42 @@
package ac.grim.grimac.checks.impl.inventory;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.InventoryCheck;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity.InteractAction;
@CheckData(name = "InventoryA", setback = 3, description = "Attacked an entity while inventory is open")
public class InventoryA extends InventoryCheck {
public InventoryA(GrimPlayer player) {
super(player);
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
super.onPacketReceive(event);
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) {
WrapperPlayClientInteractEntity wrapper = new WrapperPlayClientInteractEntity(event);
if (wrapper.getAction() != InteractAction.ATTACK) return;
// Is not possible to attack while the inventory is open.
if (player.hasInventoryOpen) {
if (flagAndAlert()) {
// Cancel the packet
if (shouldModifyPackets()) {
event.setCancelled(true);
player.onPacketCancel();
}
if (!isNoSetbackPermission())
closeInventory();
}
} else {
reward();
}
}
}
}
@@ -0,0 +1,35 @@
package ac.grim.grimac.checks.impl.inventory;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.InventoryCheck;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.player.DiggingAction;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerDigging;
@CheckData(name = "InventoryB", setback = 3, description = "Started digging blocks while inventory is open")
public class InventoryB extends InventoryCheck {
public InventoryB(GrimPlayer player) {
super(player);
}
public void handle(PacketReceiveEvent event, WrapperPlayClientPlayerDigging wrapper) {
if (wrapper.getAction() != DiggingAction.START_DIGGING) return;
// Is not possible to start digging a block while the inventory is open.
if (player.hasInventoryOpen) {
if (flagAndAlert()) {
// Cancel the packet
if (shouldModifyPackets()) {
event.setCancelled(true);
player.onPacketCancel();
}
if (!isNoSetbackPermission()) {
closeInventory();
}
}
} else {
reward();
}
}
}
@@ -0,0 +1,30 @@
package ac.grim.grimac.checks.impl.inventory;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.InventoryCheck;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.update.BlockPlace;
@CheckData(name = "InventoryC", setback = 3, description = "Placed a block while inventory is open")
public class InventoryC extends InventoryCheck {
public InventoryC(GrimPlayer player) {
super(player);
}
public void onBlockPlace(final BlockPlace place) {
// It is not possible to place a block while the inventory is open
if (player.hasInventoryOpen) {
if (flagAndAlert()) {
if (shouldModifyPackets()) {
place.resync();
}
if (!isNoSetbackPermission()) {
closeInventory();
}
}
} else {
reward();
}
}
}
@@ -0,0 +1,85 @@
package ac.grim.grimac.checks.impl.inventory;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.InventoryCheck;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.update.PredictionComplete;
import ac.grim.grimac.utils.data.VectorData;
import ac.grim.grimac.utils.data.VectorData.MoveVectorData;
import ac.grim.grimac.utils.data.VehicleData;
import java.util.StringJoiner;
@CheckData(name = "InventoryD", setback = 1, decay = 0.25)
public class InventoryD extends InventoryCheck {
private int horseJumpVerbose;
public InventoryD(GrimPlayer player) {
super(player);
}
@Override
public void onPredictionComplete(final PredictionComplete predictionComplete) {
if (!predictionComplete.isChecked() ||
predictionComplete.getData().isTeleport() ||
player.getSetbackTeleportUtil().blockOffsets ||
player.packetStateData.lastPacketWasTeleport ||
player.packetStateData.isSlowedByUsingItem() ||
System.currentTimeMillis() - player.lastBlockPlaceUseItem < 50L) {
return;
}
if (player.hasInventoryOpen) {
boolean inVehicle = player.inVehicle();
boolean isJumping, isMoving;
if (inVehicle) {
VehicleData vehicle = player.vehicleData;
// Will flag once if player open anything with pressed space bar
isJumping = vehicle.nextHorseJump > 0 && horseJumpVerbose++ >= 1;
isMoving = vehicle.nextVehicleForward != 0 || vehicle.nextVehicleHorizontal != 0;
} else {
MoveVectorData move = findMovement(player.predictedVelocity);
isJumping = player.predictedVelocity.isJump();
isMoving = move != null && (move.x != 0 || move.z != 0);
}
if (!isMoving && !isJumping) {
reward();
return;
}
if (flag()) {
if (!isNoSetbackPermission())
closeInventory();
StringJoiner joiner = new StringJoiner(" ");
if (isMoving) joiner.add("moving");
if (isJumping) joiner.add("jumping");
if (inVehicle) joiner.add("inVehicle");
alert(joiner.toString());
}
} else {
horseJumpVerbose = 0;
}
}
private MoveVectorData findMovement(VectorData vectorData) {
if (vectorData instanceof MoveVectorData) {
return (MoveVectorData) vectorData;
}
while (vectorData != null) {
vectorData = vectorData.lastVector;
if (vectorData instanceof MoveVectorData) {
return (MoveVectorData) vectorData;
}
}
return null;
}
}
@@ -0,0 +1,50 @@
package ac.grim.grimac.checks.impl.inventory;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.InventoryCheck;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
@CheckData(name = "InventoryE", setback = 3, description = "Sent a held item change packet while inventory is open")
public class InventoryE extends InventoryCheck {
private long lastTransaction = Long.MAX_VALUE; // Impossible transaction ID
public InventoryE(GrimPlayer player) {
super(player);
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
super.onPacketReceive(event);
if (event.getPacketType() == PacketType.Play.Client.HELD_ITEM_CHANGE) {
// It is not possible to change hotbar slots with held item change while the inventory is open
// A container click packet would be sent instead
if (player.hasInventoryOpen) {
if (this.lastTransaction < player.lastTransactionReceived.get()
&& flagAndAlert()) {
// Cancel the packet
if (shouldModifyPackets()) {
event.setCancelled(true);
player.onPacketCancel();
player.inventory.needResend = true;
}
if (!isNoSetbackPermission()) {
closeInventory();
}
}
} else {
reward();
}
}
}
@Override
public void onPacketSend(PacketSendEvent event) {
if (event.getPacketType() == PacketType.Play.Server.HELD_ITEM_CHANGE) {
this.lastTransaction = player.lastTransactionSent.get();
}
}
}
@@ -0,0 +1,46 @@
package ac.grim.grimac.checks.impl.inventory;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.InventoryCheck;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.inventory.InventoryDesyncStatus;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
@CheckData(name = "InventoryF", setback = 3, description = "Sent a click window packet without a open inventory", experimental = true)
public class InventoryF extends InventoryCheck {
public InventoryF(GrimPlayer player) {
super(player);
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
// Exempt on 1.9+ server version due to the Via hack done in PacketPlayerWindow, the exemption can be deleted
// once we are ahead of ViaVersion
if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_9)
|| player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9)) return;
super.onPacketReceive(event);
if (event.getPacketType() == PacketType.Play.Client.CLICK_WINDOW) {
if (!player.hasInventoryOpen && player.inventoryDesyncStatus == InventoryDesyncStatus.NOT_DESYNCED) {
if (flagAndAlert()) {
// Cancel the packet
if (shouldModifyPackets()) {
event.setCancelled(true);
player.onPacketCancel();
}
if (!isNoSetbackPermission()) {
closeInventory();
}
}
} else {
reward();
}
}
}
}
@@ -0,0 +1,40 @@
package ac.grim.grimac.checks.impl.inventory;
import ac.grim.grimac.checks.CheckData;
import ac.grim.grimac.checks.type.InventoryCheck;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientEntityAction;
@CheckData(name = "InventoryG", setback = 3, description = "Sent a entity action packet while inventory is open", experimental = true)
public class InventoryG extends InventoryCheck {
public InventoryG(GrimPlayer player) {
super(player);
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (player.packetStateData.lastPacketWasTeleport) return;
super.onPacketReceive(event);
if (event.getPacketType() == PacketType.Play.Client.ENTITY_ACTION) {
WrapperPlayClientEntityAction wrapper = new WrapperPlayClientEntityAction(event);
WrapperPlayClientEntityAction.Action action = wrapper.getAction();
if (action == WrapperPlayClientEntityAction.Action.STOP_SNEAKING
|| action == WrapperPlayClientEntityAction.Action.STOP_SPRINTING) {
return;
}
if (player.hasInventoryOpen) {
if (flagAndAlert() && !isNoSetbackPermission()) {
closeInventory();
}
} else {
reward();
}
}
}
}
@@ -15,8 +15,6 @@ public class VehiclePredictionRunner extends Check implements VehicleCheck {
public void process(final VehiclePositionUpdate vehicleUpdate) {
// Vehicle onGround = false always
// We don't do vehicle setbacks because vehicle netcode sucks.
player.movementCheckRunner.processAndCheckMovementPacket(new PositionUpdate(
vehicleUpdate.fromX(), vehicleUpdate.fromY(), vehicleUpdate.fromZ(), vehicleUpdate.toX(), vehicleUpdate.toY(), vehicleUpdate.toZ(),
false, null, null, vehicleUpdate.isTeleport()));
player.movementCheckRunner.processAndCheckMovementPacket(new PositionUpdate(vehicleUpdate.from(), vehicleUpdate.to(), false, null, null, vehicleUpdate.isTeleport()));
}
}
@@ -42,26 +42,24 @@ public class DebugHandler extends AbstractDebugHandler implements PostPrediction
double offset = predictionComplete.getOffset();
if (listeners.isEmpty() && !outputToConsole) return;
final double predictedX = player.predictedVelocity.vectorX;
final double predictedY = player.predictedVelocity.vectorY;
final double predictedZ = player.predictedVelocity.vectorZ;
if (predictedX == 0.0 && predictedY == 0.0 && predictedZ == 0.0 && offset == 0) return;
if (player.predictedVelocity.vector.lengthSquared() == 0 && offset == 0) return;
String color = pickColor(offset, offset);
Vector3dm predicted = player.predictedVelocity.vector;
Vector3dm actually = player.actualMovement;
String xColor = pickColor(Math.abs(predictedX - actually.getX()), offset);
String yColor = pickColor(Math.abs(predictedY - actually.getY()), offset);
String zColor = pickColor(Math.abs(predictedZ - actually.getZ()), offset);
String xColor = pickColor(Math.abs(predicted.getX() - actually.getX()), offset);
String yColor = pickColor(Math.abs(predicted.getY() - actually.getY()), offset);
String zColor = pickColor(Math.abs(predicted.getZ() - actually.getZ()), offset);
Component p = Component.empty()
.append(P_PREFIX.color(NamedTextColor.NAMES.value(color)))
.append(Component.text(predictedX).color(NamedTextColor.NAMES.value(xColor)))
.append(Component.text(predicted.getX()).color(NamedTextColor.NAMES.value(xColor)))
.append(Component.space())
.append(Component.text(predictedY).color(NamedTextColor.NAMES.value(yColor)))
.append(Component.text(predicted.getY()).color(NamedTextColor.NAMES.value(yColor)))
.append(Component.space())
.append(Component.text(predictedZ).color(NamedTextColor.NAMES.value(zColor)));
.append(Component.text(predicted.getZ()).color(NamedTextColor.NAMES.value(zColor)));
Component a = Component.empty()
.append(A_PREFIX.color(NamedTextColor.NAMES.value(color)))
@@ -135,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
@@ -59,16 +59,13 @@ public class AirLiquidPlace extends BlockPlaceCheck {
if (player.gamemode == GameMode.CREATIVE) return;
Vector3i blockPos = place.position;
final int x = blockPos.getX();
final int y = blockPos.getY();
final int z = blockPos.getZ();
StateType placeAgainst = player.compensatedWorld.getBlockType(x, y, z);
StateType placeAgainst = player.compensatedWorld.getBlockType(blockPos.getX(), blockPos.getY(), blockPos.getZ());
int currentTick = GrimAPI.INSTANCE.getTickManager().currentTick;
// this is actual more lenient than we need to be, We can check up to 1 ticks for all changes at location sand up to 0 ticks for first change
// But for such tiny differences in legitness it's not worth it.
Iterable<BlockModification> blockModifications = player.blockHistory.getRecentModifications((blockModification) -> currentTick - blockModification.tick() < 2
&& x == blockModification.x() && y == blockModification.y() && z == blockModification.z()
&& blockPos.equals(blockModification.location())
&& (blockModification.cause() == BlockModification.Cause.START_DIGGING || blockModification.cause() == BlockModification.Cause.HANDLE_NETTY_SYNC_TRANSACTION));
// Check if old block from instant breaking in same tick as the current placement was valid
@@ -112,10 +112,10 @@ public class RotationPlace extends BlockPlaceCheck {
for (double d : possibleEyeHeights) {
for (Vector3f lookDir : possibleLookDirs) {
// x, y, z are correct for the block placement even after post tick because of code elsewhere
Vector3d starting = new Vector3d(player.x, player.y + d, player.z);
// xRot and yRot are a tick behind
Ray trace = new Ray(player, player.x, player.y + d, player.z, lookDir.getX(), lookDir.getY());
Vector3dm pointAtDistance = trace.getPointAtDistance(distance);
Pair<Vector3dm, BlockFace> intercept = ReachUtils.calculateIntercept(box, trace.getOriginX(), trace.getOriginY(), trace.getOriginZ(), pointAtDistance.getX(), pointAtDistance.getY(), pointAtDistance.getZ());
Ray trace = new Ray(player, starting.getX(), starting.getY(), starting.getZ(), lookDir.getX(), lookDir.getY());
Pair<Vector3dm, BlockFace> intercept = ReachUtils.calculateIntercept(box, trace.getOrigin(), trace.getPointAtDistance(distance));
if (intercept.first() != null) return true;
}
@@ -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) {
@@ -64,7 +60,7 @@ public class KnockbackHandler extends Check implements PostPredictionCheck {
// Blacklist problemated vector until mojang fixes a client-sided bug
if (playerVelocity.getY() == -0.04) {
velocity.setVelocity(playerVelocity.add(0, 1 / 8000D, 0));
velocity.setVelocity(playerVelocity.add(new Vector3d(0, 1 / 8000D, 0)));
playerVelocity = velocity.getVelocity();
event.markForReEncode(true);
}
@@ -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
@@ -483,8 +485,7 @@ public class CheckManagerListener extends PacketListenerAbstract {
player.yaw = move.getYaw();
player.pitch = move.getPitch();
final VehiclePositionUpdate update = new VehiclePositionUpdate(clamp.getX(), clamp.getY(), clamp.getZ(),
position.getX(), position.getY(), position.getZ(), move.getYaw(), move.getPitch(), player.packetStateData.lastPacketWasTeleport);
final VehiclePositionUpdate update = new VehiclePositionUpdate(clamp, position, move.getYaw(), move.getPitch(), player.packetStateData.lastPacketWasTeleport);
player.checkManager.onVehiclePositionUpdate(update);
player.packetStateData.receivedSteerVehicle = false;
@@ -533,17 +534,14 @@ public class CheckManagerListener extends PacketListenerAbstract {
player.onPacketCancel();
}
Vector3i facePos = new Vector3i(packet.getBlockPosition().getX() + packet.getFace().getModX(), packet.getBlockPosition().getY() + packet.getFace().getModY(), packet.getBlockPosition().getZ() + packet.getFace().getModZ());
// Ends the client prediction introduced in 1.19+
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_19) && PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_19)) {
player.user.sendPacket(new WrapperPlayServerAcknowledgeBlockChanges(packet.getSequence()));
} else { // The client isn't smart enough to revert changes
final Vector3i blockPosition = packet.getBlockPosition();
final int x = blockPosition.getX();
final int y = blockPosition.getY();
final int z = blockPosition.getZ();
player.resyncPosition(x, y ,z);
final BlockFace face = packet.getFace();
player.resyncPosition(x + face.getModX(), y + face.getModY(), z + face.getModZ());
player.resyncPosition(packet.getBlockPosition());
player.resyncPosition(facePos);
}
// Stop inventory desync from cancelling place
@@ -728,8 +726,9 @@ public class CheckManagerListener extends PacketListenerAbstract {
}
if (hasPosition) {
Vector3d clampVector = VectorUtils.clampVector(x, y, z);
final PositionUpdate update = new PositionUpdate(player.x, player.y, player.z, x, y ,z, onGround, teleportData.getSetback(), teleportData.getTeleportData(), teleportData.isTeleport());
Vector3d position = new Vector3d(x, y, z);
Vector3d clampVector = VectorUtils.clampVector(position);
final PositionUpdate update = new PositionUpdate(new Vector3d(player.x, player.y, player.z), position, onGround, teleportData.getSetback(), teleportData.getTeleportData(), teleportData.isTeleport());
// Stupidity doesn't care about 0.03
if (!player.packetStateData.lastPacketWasOnePointSeventeenDuplicate) {
@@ -773,15 +772,14 @@ public class CheckManagerListener extends PacketListenerAbstract {
return;
}
final Vector3i blockPosition = packet.getBlockPosition();
final BlockBreak blockBreak = new BlockBreak(player, blockPosition.getX(), blockPosition.getY(), blockPosition.getZ(), packet.getBlockFace(), packet.getBlockFaceId(), action, packet.getSequence(), player.compensatedWorld.getBlock(packet.getBlockPosition()));
final BlockBreak blockBreak = new BlockBreak(player, packet.getBlockPosition(), packet.getBlockFace(), packet.getBlockFaceId(), action, packet.getSequence(), player.compensatedWorld.getBlock(packet.getBlockPosition()));
player.checkManager.onBlockBreak(blockBreak);
if (blockBreak.isCancelled()) {
event.setCancelled(true);
player.onPacketCancel();
player.resyncPosition(blockBreak.x, blockBreak.y, blockBreak.z, packet.getSequence());
player.resyncPosition(blockBreak.position, packet.getSequence());
return;
}
@@ -789,7 +787,7 @@ public class CheckManagerListener extends PacketListenerAbstract {
if (action == DiggingAction.FINISHED_DIGGING && BREAKABLE.apply(blockBreak.block.getType())) {
player.compensatedWorld.startPredicting();
player.compensatedWorld.updateBlock(blockBreak.x, blockBreak.y, blockBreak.z, 0);
player.compensatedWorld.updateBlock(blockBreak.position.x, blockBreak.position.y, blockBreak.position.z, 0);
player.compensatedWorld.stopPredicting(packet);
}
@@ -799,14 +797,11 @@ public class CheckManagerListener extends PacketListenerAbstract {
// Instant breaking, no damage means it is unbreakable by creative players (with swords)
if (damage >= 1) {
player.compensatedWorld.startPredicting();
final int x = blockBreak.x;
final int y = blockBreak.y;
final int z = blockBreak.z;
player.blockHistory.add(
new BlockModification(
player.compensatedWorld.getBlock(x, y, z),
player.compensatedWorld.getBlock(blockBreak.position),
WrappedBlockState.getByGlobalId(0),
x, y, z,
blockBreak.position,
GrimAPI.INSTANCE.getTickManager().currentTick,
BlockModification.Cause.START_DIGGING
)
@@ -814,9 +809,9 @@ public class CheckManagerListener extends PacketListenerAbstract {
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_13) && Materials.isWaterSource(player.getClientVersion(), blockBreak.block)) {
// Vanilla uses a method to grab water flowing, but as you can't break flowing water
// We can simply treat all waterlogged blocks or source blocks as source blocks
player.compensatedWorld.updateBlock(x, y, z, StateTypes.WATER.createBlockState(CompensatedWorld.blockVersion).getGlobalId());
player.compensatedWorld.updateBlock(blockBreak.position, StateTypes.WATER.createBlockState(CompensatedWorld.blockVersion));
} else {
player.compensatedWorld.updateBlock(x, y, z, 0);
player.compensatedWorld.updateBlock(blockBreak.position.x, blockBreak.position.y, blockBreak.position.z, 0);
}
player.compensatedWorld.stopPredicting(packet);
}
@@ -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;
}
}
@@ -2,9 +2,10 @@ package ac.grim.grimac.events.packets;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.data.Pair;
import ac.grim.grimac.utils.data.RotationData;
import ac.grim.grimac.utils.data.VehicleData;
import ac.grim.grimac.utils.math.GrimMath;
import ac.grim.grimac.utils.math.Location;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketListenerPriority;
@@ -34,26 +35,22 @@ public class PacketServerTeleport extends PacketListenerAbstract {
WrapperPlayServerPlayerPositionAndLook teleport = new WrapperPlayServerPlayerPositionAndLook(event);
double posX = teleport.getX();
double posY = teleport.getY();
double posZ = teleport.getZ();
final float yaw = teleport.getYaw();
final float pitch = teleport.getPitch();
Vector3d pos = new Vector3d(teleport.getX(), teleport.getY(), teleport.getZ());
// This is the first packet sent to the client which we need to track
if (player.getSetbackTeleportUtil().getRequiredSetBack() == null) {
// Player teleport event gets called AFTER player join event
player.x = posX;
player.y = posY;
player.z = posZ;
player.yaw = yaw;
player.pitch = pitch;
player.x = teleport.getX();
player.y = teleport.getY();
player.z = teleport.getZ();
player.yaw = teleport.getYaw();
player.pitch = teleport.getPitch();
player.lastX = posX;
player.lastY = posY;
player.lastZ = posZ;
player.lastYaw = yaw;
player.lastPitch = pitch;
player.lastX = teleport.getX();
player.lastY = teleport.getY();
player.lastZ = teleport.getZ();
player.lastYaw = teleport.getYaw();
player.lastPitch = teleport.getPitch();
player.pollData();
}
@@ -70,24 +67,24 @@ public class PacketServerTeleport extends PacketListenerAbstract {
relativeZ = teleport.isRelativeFlag(RelativeFlag.Z);
if (relativeX) {
posX += player.x;
pos = pos.add(new Vector3d(player.x, 0, 0));
teleport.setRelative(RelativeFlag.X, false);
}
if (relativeY) {
posY += player.y;
pos = pos.add(new Vector3d(0, player.y, 0));
teleport.setRelative(RelativeFlag.Y, false);
}
if (relativeZ) {
posZ += player.z;
pos = pos.add(new Vector3d(0, 0, player.z));
teleport.setRelative(RelativeFlag.Z, false);
}
if (relativeX || relativeY || relativeZ) {
teleport.setX(posX);
teleport.setY(posY);
teleport.setZ(posZ);
teleport.setX(pos.getX());
teleport.setY(pos.getY());
teleport.setZ(pos.getZ());
event.markForReEncode(true);
}
@@ -127,13 +124,10 @@ public class PacketServerTeleport extends PacketListenerAbstract {
// For some reason teleports on 1.7 servers are offset by 1.62?
if (PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_8))
posY -= 1.62;
pos = pos.withY(pos.getY() - 1.62);
final Vector3d deltaMovement = teleport.getDeltaMovement();
player.getSetbackTeleportUtil().addSentTeleport(posX, posY, posZ,
deltaMovement.getX(), deltaMovement.getY(), deltaMovement.getZ(),
lastTransactionSent, teleport.getRelativeFlags(), true, teleport.getTeleportId()
);
Location target = new Location(null, pos.getX(), pos.getY(), pos.getZ());
player.getSetbackTeleportUtil().addSentTeleport(target, teleport.getDeltaMovement(), lastTransactionSent, teleport.getRelativeFlags(), true, teleport.getTeleportId());
}
if (event.getPacketType() == PacketType.Play.Server.PLAYER_ROTATION) {
@@ -163,12 +157,9 @@ public class PacketServerTeleport extends PacketListenerAbstract {
player.sendTransaction();
event.getTasksAfterSend().add(player::sendTransaction);
Vector3d position = new WrapperPlayServerVehicleMove(event).getPosition();
player.vehicleData.vehicleTeleports.add(new VehicleData.VehicleTeleport(
player.vehicleData.vehicleTeleports.add(new Pair<>(
player.lastTransactionSent.get(),
position.getX(),
position.getY(),
position.getZ()
new WrapperPlayServerVehicleMove(event).getPosition()
));
}
}
@@ -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);
}
@@ -77,11 +86,7 @@ public class BasePacketWorldReader extends PacketListenerAbstract {
if (player == null) return;
WrapperPlayServerAcknowledgePlayerDigging ack = new WrapperPlayServerAcknowledgePlayerDigging(event);
Vector3i blockPosition = ack.getBlockPosition();
final int x = blockPosition.getX();
final int y = blockPosition.getY();
final int z = blockPosition.getZ();
player.compensatedWorld.handleBlockBreakAck(x, y, z, ack.getBlockId(), ack.getAction(), ack.isSuccessful());
player.compensatedWorld.handleBlockBreakAck(ack.getBlockPosition(), ack.getBlockId(), ack.getAction(), ack.isSuccessful());
}
if (event.getPacketType() == PacketType.Play.Server.CHANGE_GAME_STATE) {
@@ -125,7 +130,7 @@ public class BasePacketWorldReader extends PacketListenerAbstract {
if (teleports.getFlags().getMask() != 0) {
continue; // Worse that will happen is people will get an extra setback...
}
shouldPostTrans = shouldPostTrans || (Math.abs(teleports.getLocationX() - chunkCenterX) < 16 && Math.abs(teleports.getLocationZ() - chunkCenterZ) < 16);
shouldPostTrans = shouldPostTrans || (Math.abs(teleports.getLocation().getX() - chunkCenterX) < 16 && Math.abs(teleports.getLocation().getZ() - chunkCenterZ) < 16);
}
if (shouldPostTrans) {
@@ -165,28 +170,15 @@ public class BasePacketWorldReader extends PacketListenerAbstract {
player.lastTransSent + 2 < System.currentTimeMillis())
player.sendTransaction();
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> player.compensatedWorld.updateBlock(blockPosition.getX(), blockPosition.getY(), blockPosition.getZ(), blockChange.getBlockId()));
int x = blockPosition.getX();
int y = blockPosition.getY();
int z = blockPosition.getZ();
int blockId = blockChange.getBlockId();
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> player.compensatedWorld.updateBlock(x, y, z, blockId));
}
public void handleMultiBlockChange(GrimPlayer player, PacketSendEvent event) {
WrapperPlayServerMultiBlockChange multiBlockChange = new WrapperPlayServerMultiBlockChange(event);
int range = 16;
final var blocks = multiBlockChange.getBlocks();
for (WrapperPlayServerMultiBlockChange.EncodedBlock blockChange : blocks) {
// Don't send a transaction unless it's within 16 blocks of the player
if (Math.abs(blockChange.getX() - player.x) < range && Math.abs(blockChange.getY() - player.y) < range && Math.abs(blockChange.getZ() - player.z) < range && player.lastTransSent + 2 < System.currentTimeMillis()) {
player.sendTransaction();
break;
}
}
// Add a single runnable to prevent excessive memory use when there are lots of block changes
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> {
for (WrapperPlayServerMultiBlockChange.EncodedBlock blockChange : blocks) {
player.compensatedWorld.updateBlock(blockChange.getX(), blockChange.getY(), blockChange.getZ(), blockChange.getBlockId());
}
});
versionedMultiBlockChangeHandler.handleMultiBlockChange(player, event);
}
}
@@ -0,0 +1,30 @@
package ac.grim.grimac.events.packets.worldreader.multiblockchange;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerMultiBlockChange;
public class LegacyMultiBlockChangeHandler implements VersionedMultiBlockChangeHandler {
// TODO hande optimize Pre 1.16 code to also reduce memory usage and not use wrapper
@Override
public void handleMultiBlockChange(GrimPlayer player, PacketSendEvent event) {
WrapperPlayServerMultiBlockChange multiBlockChange = new WrapperPlayServerMultiBlockChange(event);
final var blocks = multiBlockChange.getBlocks();
for (WrapperPlayServerMultiBlockChange.EncodedBlock blockChange : blocks) {
// Don't send a transaction unless it's within 16 blocks of the player
if (Math.abs(blockChange.getX() - player.x) < RANGE && Math.abs(blockChange.getY() - player.y) < RANGE && Math.abs(blockChange.getZ() - player.z) < RANGE && player.lastTransSent + TRANSACTION_COOLDOWN_MS < System.currentTimeMillis()) {
player.sendTransaction();
break;
}
}
// Add a single runnable to prevent excessive memory use when there are lots of block changes
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> {
for (WrapperPlayServerMultiBlockChange.EncodedBlock blockChange : blocks) {
player.compensatedWorld.updateBlock(blockChange.getX(), blockChange.getY(), blockChange.getZ(), blockChange.getBlockId());
}
});
}
}
@@ -0,0 +1,175 @@
package ac.grim.grimac.events.packets.worldreader.multiblockchange;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.github.retrooper.packetevents.netty.buffer.ByteBufHelper;
import io.netty.buffer.ByteBuf;
/**
* <p>
* Minecraft’s MultiBlockChange packet batches many block updates in one shot:
* the server sends a 64-bit section coordinate, an optional trustEdges flag,
* a VarInt count, then that many VarLong-encoded block changes
* (<em>52 bits</em> of global blockStateId + <em>12 bits</em> of local X/Z/Y
* within the 16×16×16 section).
* </p>
*
* <p>
* To shrink our on-heap footprint for deferred tasks, we immediately repack
* each VarLong into one 32-bit <code>int</code>, while still using the
* vanilla 64-bit header for coords.
* </p>
*
* <h3>Vanilla “on-the-wire” format</h3>
* <pre>
* 1) sectionEncodedPosition : Long
* 2) [trustEdges? : Boolean] // only on protocol ≤1.19.4
* 3) recordCount : VarInt
* 4) records[] : recordCount × VarLong
*
* Each VarLong is bits:
* [63……12] blockStateId (52 bits)
* [11……8] localX (4 bits, 0–15)
* [7……4] localZ (4 bits, 0–15)
* [3……0] localY (4 bits, 0–15)
* </pre>
*
* <h3>Vanilla 64-bit section header “encodedPosition”</h3>
* <pre>
* bits 63……42 41……20 19……0
* ┌────────┬─────────┬────────┐
* │ secX │ secZ │ secY │
* │ (22b) │ (22b) │ (20b) │
* └────────┴─────────┴────────┘
*
* secX = encoded >> 42
* secZ = encoded << 22 >> 42
* secY = encoded << 44 >> 44
* </pre>
*
* <h3>Our 32-bit repacked block record (MSB → LSB)</h3>
* <pre>
* bits 31…17 16…12 11…8 7…4 3…0
* ┌───────┬───────┬───────┬──────┬─────┐
* │ state │ spare │ lX │ lZ │ lY │
* │ (15b) │ (5b) │ (4b) │ (4b) │(4b) │
* └───────┴───────┴───────┴──────┴─────┘
*
* • state = (data >>> 12) & 0x7FFF
* • spare = bits 12–16 (unused, reserved for flags)
* • lX = (data >>> 8) & 0xF
* • lZ = (data >>> 4) & 0xF
* • lY = data & 0xF
*
* Then pack: packed = (state << 17) | ((lX<<8)|(lZ<<4)|lY)
* </pre>
*/
public final class V1160MultiBlockChangeBitRepackHandler
implements ac.grim.grimac.events.packets.worldreader.multiblockchange.VersionedMultiBlockChangeHandler {
/* ---------- bit masks / shifts for the packed int ---------- */
private static final int SHIFT_STATE = 17; // 32-15 = 17
private static final int MASK_STATE = 0x7FFF; // 15 bits
static final int MASK_LOCAL = 0xFFF; // 12 bits
/* ---------- does this protocol still have trustEdges ? ----- */
private static final boolean HAS_TRUST_EDGES =
PacketEvents.getAPI().getServerManager().getVersion().isOlderThanOrEquals(ServerVersion.V_1_19_4);
@Override
public void handleMultiBlockChange(GrimPlayer player, PacketSendEvent event) {
// PE resets writer index for us, we don't have to call buffer.writerIndex(originalWriterIndex)
ByteBuf buf = (ByteBuf) event.getByteBuf();
/* 1. Section-position header (64 bits) ------------------ */
long sectionEncodedPosition = ByteBufHelper.readLong(buf);
if (HAS_TRUST_EDGES) { // skip only when it really exists
buf.skipBytes(1);
}
/* 2. Record count + packed-int array ------------------- */
int recordCount = ByteBufHelper.readVarInt(buf);
int[] packed = new int[recordCount];
/* 3. Decode packet (still comes as varLong per record) */
// Unpack section coords once for the “near player” test
int secX = (int) (sectionEncodedPosition >> 42);
int secZ = (int) (sectionEncodedPosition << 22 >> 42);
int secY = (int) (sectionEncodedPosition << 44 >> 44);
int baseX = secX << 4;
int baseY = secY << 4;
int baseZ = secZ << 4;
boolean sendTx = false;
// Use this to not spam the player with transactions if one has been sent within COOLDOWN
long now = System.currentTimeMillis();
for (int i = 0; i < recordCount; i++) {
long data = readVarLong(buf); // 52+12 bits
int local = (int) (data & 0xFFFL); // 12-bit pos
packed[i] = repackFromLong(data);
/* -------- near-player distance test -------------- */
if (!sendTx) {
int lx = (local >>> 8) & 0xF;
int lz = (local >>> 4) & 0xF;
int ly = local & 0xF;
int wx = baseX + lx, wy = baseY + ly, wz = baseZ + lz;
if (Math.abs(wx - player.x) < RANGE &&
Math.abs(wy - player.y) < RANGE &&
Math.abs(wz - player.z) < RANGE &&
player.lastTransSent + TRANSACTION_COOLDOWN_MS < now) {
sendTx = true;
}
}
}
if (sendTx)
player.sendTransaction();
/* 4. Queue runnable – captures only int[] + sectionPos */
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> {
// unpack section once per execution
int sX = (int) (sectionEncodedPosition >> 42);
int sY = (int) (sectionEncodedPosition << 44 >> 44);
int sZ = (int) (sectionEncodedPosition << 22 >> 42);
int bx = sX << 4, by = sY << 4, bz = sZ << 4;
for (int rec : packed) {
int stateId = (rec >>> SHIFT_STATE) & MASK_STATE;
int lx = (rec >>> 8) & 0xF;
int lz = (rec >>> 4) & 0xF;
int ly = rec & 0xF;
int wx = bx + lx;
int wy = by + ly;
int wz = bz + lz;
player.compensatedWorld.updateBlock(wx, wy, wz, stateId);
}
});
}
public int repackFromLong(long data) {
// 1) extract the 15-bit state from bits 12.. (original >> 12)
int blockState = (int)((data >>> 12) & MASK_STATE);
// 2) extract the 12-bit local from bits 0..11
int local = (int)( data & MASK_LOCAL);
// 3) glue them together
return (blockState << SHIFT_STATE) | local;
}
}
@@ -0,0 +1,22 @@
package ac.grim.grimac.events.packets.worldreader.multiblockchange;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import io.netty.buffer.ByteBuf;
public interface VersionedMultiBlockChangeHandler {
int RANGE = 16;
long TRANSACTION_COOLDOWN_MS = 2; // In milliseconds
void handleMultiBlockChange(GrimPlayer player, PacketSendEvent event);
default long readVarLong(ByteBuf buf) {
long value = 0;
int size = 0;
int b;
while (((b = buf.readByte()) & 0x80) == 0x80) {
value |= (long) (b & 0x7F) << (size++ * 7);
}
return value | ((long) (b & 0x7F) << (size * 7));
}
}
@@ -2,6 +2,7 @@ package ac.grim.grimac.manager;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.api.AbstractCheck;
import ac.grim.grimac.checks.debug.HitboxDebugHandler;
import ac.grim.grimac.checks.impl.aim.AimDuplicateLook;
import ac.grim.grimac.checks.impl.aim.AimModulo360;
import ac.grim.grimac.checks.impl.aim.processor.AimProcessor;
@@ -11,15 +12,13 @@ import ac.grim.grimac.checks.impl.chat.ChatA;
import ac.grim.grimac.checks.impl.chat.ChatB;
import ac.grim.grimac.checks.impl.chat.ChatC;
import ac.grim.grimac.checks.impl.chat.ChatD;
import ac.grim.grimac.checks.impl.combat.Hitboxes;
import ac.grim.grimac.checks.impl.combat.MultiInteractA;
import ac.grim.grimac.checks.impl.combat.MultiInteractB;
import ac.grim.grimac.checks.impl.combat.Reach;
import ac.grim.grimac.checks.impl.combat.*;
import ac.grim.grimac.checks.impl.crash.*;
import ac.grim.grimac.checks.impl.elytra.*;
import ac.grim.grimac.checks.impl.exploit.ExploitA;
import ac.grim.grimac.checks.impl.exploit.ExploitB;
import ac.grim.grimac.checks.impl.groundspoof.NoFall;
import ac.grim.grimac.checks.impl.inventory.*;
import ac.grim.grimac.checks.impl.misc.ClientBrand;
import ac.grim.grimac.checks.impl.misc.GhostBlockMitigation;
import ac.grim.grimac.checks.impl.misc.Post;
@@ -117,6 +116,11 @@ public class CheckManager {
.put(BadPacketsV.class, new BadPacketsV(player))
.put(BadPacketsY.class, new BadPacketsY(player))
.put(BadPacketsZ.class, new BadPacketsZ(player))
.put(InventoryA.class, new InventoryA(player))
.put(InventoryB.class, new InventoryB(player))
.put(InventoryE.class, new InventoryE(player))
.put(InventoryF.class, new InventoryF(player))
.put(InventoryG.class, new InventoryG(player))
.put(MultiActionsA.class, new MultiActionsA(player))
.put(MultiActionsC.class, new MultiActionsC(player))
.put(MultiActionsD.class, new MultiActionsD(player))
@@ -125,7 +129,7 @@ public class CheckManager {
.put(PacketOrderC.class, new PacketOrderC(player))
.put(PacketOrderD.class, new PacketOrderD(player))
.put(PacketOrderO.class, new PacketOrderO(player))
.put(PacketOrderP.class, new PacketOrderP(player))
// .put(PacketOrderP.class, new PacketOrderP(player))
.put(SprintA.class, new SprintA(player))
.put(VehicleA.class, new VehicleA(player))
.put(VehicleB.class, new VehicleB(player))
@@ -201,6 +205,7 @@ public class CheckManager {
.build();
blockPlaceChecks = new ImmutableClassToInstanceMap.Builder<BlockPlaceCheck>()
.put(InventoryC.class, new InventoryC(player))
.put(InvalidPlaceA.class, new InvalidPlaceA(player))
.put(InvalidPlaceB.class, new InvalidPlaceB(player))
.put(AirLiquidPlace.class, new AirLiquidPlace(player))
@@ -250,6 +255,9 @@ public class CheckManager {
.put(TransactionOrder.class, new TransactionOrder(player))
.put(VehicleC.class, new VehicleC(player))
.put(Hitboxes.class, new Hitboxes(player)) // Hitboxes is invoked by Reach
.put(WallHit.class, new WallHit(player))
.put(EntityPierce.class, new EntityPierce(player))
.put(HitboxDebugHandler.class, new HitboxDebugHandler(player))
.build();
allChecks = new ImmutableClassToInstanceMap.Builder<AbstractCheck>()
@@ -18,9 +18,9 @@ import ac.grim.grimac.utils.data.SetBackData;
import ac.grim.grimac.utils.data.TeleportAcceptData;
import ac.grim.grimac.utils.data.TeleportData;
import ac.grim.grimac.utils.data.VectorData;
import ac.grim.grimac.utils.data.VehicleData;
import ac.grim.grimac.utils.data.VelocityData;
import ac.grim.grimac.utils.math.GrimMath;
import ac.grim.grimac.utils.math.Location;
import ac.grim.grimac.utils.math.Vector3dm;
import ac.grim.grimac.utils.math.VectorUtils;
import ac.grim.grimac.utils.nmsutil.Collisions;
@@ -74,9 +74,7 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
@Override
public void onPredictionComplete(final PredictionComplete predictionComplete) {
// Grab friction now when we know player on ground and other variables
final double afterTickFrictionX = player.clientVelocity.getX();
final double afterTickFrictionY = player.clientVelocity.getY();
final double afterTickFrictionZ = player.clientVelocity.getZ();
Vector3dm afterTickFriction = player.clientVelocity.clone();
// We must first check if the player has accepted their setback
// If the setback isn't complete, then this position is illegitimate
@@ -84,12 +82,12 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
// The player needs to now wait for their vehicle to go into the right place before getting back in
if (cheatVehicleInterpolationDelay > 0) cheatVehicleInterpolationDelay = 10;
// Teleport, let velocity be reset
lastKnownGoodPosition = new SetbackPosWithVector(player.x, player.y, player.z, afterTickFrictionX, afterTickFrictionY, afterTickFrictionZ);
lastKnownGoodPosition = new SetbackPosWithVector(new Vector3d(player.x, player.y, player.z), afterTickFriction);
} else if (requiredSetBack == null || requiredSetBack.isComplete()) {
cheatVehicleInterpolationDelay--;
// No simulation... we can do that later. We just need to know the valid position.
// As we didn't setback here, the new position is known to be safe!
lastKnownGoodPosition = new SetbackPosWithVector(player.x, player.y, player.z, afterTickFrictionX, afterTickFrictionY, afterTickFrictionZ);
lastKnownGoodPosition = new SetbackPosWithVector(new Vector3d(player.x, player.y, player.z), afterTickFriction);
}
if (requiredSetBack != null) requiredSetBack.tick();
@@ -102,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
@@ -125,6 +130,27 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
return player.platformPlayer != null && player.noSetbackPermission;
}
private void simulateFriction(Vector3dm vector) {
// We must always do this before simulating positions, as this is the last actual (safe) movement
// We must not do this for knockback or explosions, as they are at the start of the tick
if (player.wasTouchingWater) {
PredictionEngineWater.staticVectorEndOfTick(player, vector, 0.8F, player.gravity, true);
} else if (player.wasTouchingLava) {
vector.multiply(0.5D);
if (player.hasGravity)
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(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
vector.multiply(player.stuckSpeedMultiplier); // Prevent abusing setbacks to move out of blocks like webs
}
// stop 1.8 players from stepping onto 1.25 high blocks, because why not?
new PredictionEngine().applyMovementThreshold(player, new HashSet<>(Collections.singletonList(new VectorData(vector, VectorData.VectorType.BestVelPicked))));
}
private void blockMovementsUntilResync(boolean simulateNextTickPosition, boolean isResync) {
if (requiredSetBack == null) return; // Hasn't spawned
if (player.platformPlayer != null && player.noSetbackPermission)
@@ -138,115 +164,64 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
lastWorldResync = System.currentTimeMillis();
}
double clientVelX = lastKnownGoodPosition.getVectorX();
double clientVelY = lastKnownGoodPosition.getVectorY();
double clientVelZ = lastKnownGoodPosition.getVectorZ();
Vector3dm clientVel = lastKnownGoodPosition.vector.clone();
Pair<VelocityData, Vector3dm> futureKb = player.checkManager.getKnockbackHandler().getFutureKnockback();
VelocityData futureExplosion = player.checkManager.getExplosionHandler().getFutureExplosion();
// Velocity sets
if (futureKb.first() != null) {
clientVelX = futureKb.second().getX();
clientVelY = futureKb.second().getY();
clientVelZ = futureKb.second().getZ();
clientVel = futureKb.second();
}
// Explosion adds
if (futureExplosion != null && (futureKb.first() == null || futureKb.first().transaction < futureExplosion.transaction)) {
clientVelX += futureExplosion.vector.getX();
clientVelY += futureExplosion.vector.getY();
clientVelZ += futureExplosion.vector.getZ();
clientVel.add(futureExplosion.vector);
}
double x = lastKnownGoodPosition.posX;
double y = lastKnownGoodPosition.posY;
double z = lastKnownGoodPosition.posZ;
Vector3d position = lastKnownGoodPosition.pos;
SimpleCollisionBox oldBB = player.boundingBox;
player.boundingBox = GetBoundingBox.getPlayerBoundingBox(player, x, y, z);
player.boundingBox = GetBoundingBox.getPlayerBoundingBox(player, position.getX(), position.getY(), position.getZ());
// Mini prediction engine - simulate collisions
if (simulateNextTickPosition) {
Vector3dm collide = Collisions.collide(player, clientVelX, clientVelY, clientVelZ);
Vector3dm collide = Collisions.collide(player, clientVel.getX(), clientVel.getY(), clientVel.getZ());
x += collide.getX();
y += collide.getY();
position = position.withX(position.getX() + collide.getX());
position = position.withY(position.getY() + collide.getY());
// TODO: Is this even needed? Can't reproduce any phasing on vanilla 1.8 when being setback.
if (player.getClientVersion().isOlderThan(ClientVersion.V_1_9)) {
// 1.8 players need the collision epsilon to not phase into blocks when being setback
// Due to simulation, this will not allow a flight bypass by sending a billion invalid movements
y += SimpleCollisionBox.COLLISION_EPSILON;
position = position.withY(position.getY() + SimpleCollisionBox.COLLISION_EPSILON);
}
z += collide.getZ();
position = position.withZ(position.getZ() + collide.getZ());
if (clientVelX != collide.getX()) clientVelX = 0;
if (clientVelY != collide.getY()) clientVelY = 0;
if (clientVelZ != collide.getZ()) clientVelZ = 0;
if (clientVel.getX() != collide.getX()) clientVel.setX(0);
if (clientVel.getY() != collide.getY()) clientVel.setY(0);
if (clientVel.getZ() != collide.getZ()) clientVel.setZ(0);
// BEGIN SIMULATING FRICTION
// We must always do this before simulating positions, as this is the last actual (safe) movement
// We must not do this for knockback or explosions, as they are at the start of the tick
double[] vector = {clientVelX, clientVelY, clientVelZ};
if (player.wasTouchingWater) {
PredictionEngineWater.staticVectorEndOfTick(player, vector, 0.8F, player.gravity, true);
} else if (player.wasTouchingLava) {
vector[0] *= 0.5D;
vector[1] *= 0.5D;
vector[2] *= 0.5D;
if (player.hasGravity)
vector[1] += -player.gravity / 4.0D;
} else if (player.isGliding) {
Vector3dm getLook = ReachUtils.getLook(player, player.yaw, player.pitch);
PredictionEngineElytra.applyElytraMovementInPlace(player, vector, getLook.getX(), getLook.getY(), getLook.getZ());
vector[0] *= player.stuckSpeedMultiplier.getX() * 0.99F;
vector[1] *= player.stuckSpeedMultiplier.getY() * 0.98F;
vector[2] *= player.stuckSpeedMultiplier.getZ() * 0.99F;
vector[1] -= 0.05; // Make the player fall a bit
} else { // Gliding doesn't have friction, we handle it differently
// Lava and normal movement
vector[0] *= player.friction;
vector[1] = PredictionEngineNormal.staticVectorEndOfTickY(player, vector[1]);
vector[2] *= player.friction;
// Prevent abusing setbacks to move out of blocks like webs
vector[0] *= player.stuckSpeedMultiplier.getX();
vector[1] *= player.stuckSpeedMultiplier.getY();
vector[2] *= player.stuckSpeedMultiplier.getZ();
}
// stop 1.8 players from stepping onto 1.25 high blocks, because why not?
new PredictionEngine().applyMovementThreshold(player, new HashSet<>(Collections.singletonList(new VectorData(vector[0], vector[1], vector[2], VectorData.VectorType.BestVelPicked))));
// END SIMULATING FRICTION
clientVelX = vector[0];
clientVelY = vector[1];
clientVelZ = vector[2];
simulateFriction(clientVel);
}
player.boundingBox = oldBB; // reset back to the new bounding box
boolean sendVelocity = true;
if (!hasAcceptedSpawnTeleport || player.isFlying)
sendVelocity = false; // if the player is flying or hasn't spawned... don't force kb
clientVel = null; // if the player is flying or hasn't spawned... don't force kb
// Something weird has occurred in the player's movement, block offsets until we resync
if (isResync) {
blockOffsets = true;
}
SetBackData data = new SetBackData(new TeleportData(x, y, z, 0, 0, 0, new RelativeFlag(0b11000), player.lastTransactionSent.get(), 0), player.yaw, player.pitch, clientVelX, clientVelY, clientVelZ, sendVelocity, player.inVehicle(), false);
SetBackData data = new SetBackData(new TeleportData(position, new Vector3d(), new RelativeFlag(0b11000), player.lastTransactionSent.get(), 0), player.yaw, player.pitch, clientVel, player.inVehicle(), false);
sendSetback(data);
}
private void sendSetback(SetBackData data) {
isSendingSetback = true;
double positionX = data.getTeleportData().getLocationX();
double positionY = data.getTeleportData().getLocationY();
double positionZ = data.getTeleportData().getLocationZ();
Vector3d position = data.getTeleportData().getLocation();
try {
// Player is in a vehicle
@@ -262,7 +237,7 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
// Stop the player from being able to teleport vehicles and simply re-enter them to continue,
// therefore, teleport the entity
player.user.sendPacket(new WrapperPlayServerEntityTeleport(vehicleId, new Vector3d(positionX, positionY, positionZ), player.yaw % 360, 0, false));
player.user.sendPacket(new WrapperPlayServerEntityTeleport(vehicleId, new Vector3d(position.getX(), position.getY(), position.getZ()), player.yaw % 360, 0, false));
player.getSetbackTeleportUtil().cheatVehicleInterpolationDelay = Integer.MAX_VALUE; // Set to max until player accepts the new position
// Make sure bukkit also knows the player got teleported out of their vehicle, can't do this async
@@ -277,7 +252,7 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
}
}
double y = positionY;
double y = position.getY();
if (PacketEvents.getAPI().getServerManager().getVersion().isOlderThanOrEquals(ServerVersion.V_1_7_10)) {
y += 1.62; // 1.7 teleport offset if grim ever supports 1.7 again
}
@@ -292,15 +267,15 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
data.getTeleportData().setTransaction(player.lastTransactionSent.get());
// Use provided transaction ID to make sure it can never desync, although there's no reason to do this
addSentTeleport(positionX, y, positionZ, 0, 0, 0, data.getTeleportData().getTransaction(), new RelativeFlag(0b11000), false, teleportId);
addSentTeleport(new Location(null, position.getX(), y, position.getZ(), player.yaw % 360, player.pitch % 360), new Vector3d(), data.getTeleportData().getTransaction(), new RelativeFlag(0b11000), false, teleportId);
// This must be done after setting the sent teleport, otherwise we lose velocity data
requiredSetBack = data;
// Send after tracking to fix race condition
PacketEvents.getAPI().getProtocolManager().sendPacketSilently(player.user.getChannel(), new WrapperPlayServerPlayerPositionAndLook(positionX, positionY, positionZ, 0, 0, data.getTeleportData().getFlags().getMask(), teleportId, false));
PacketEvents.getAPI().getProtocolManager().sendPacketSilently(player.user.getChannel(), new WrapperPlayServerPlayerPositionAndLook(position.getX(), position.getY(), position.getZ(), 0, 0, data.getTeleportData().getFlags().getMask(), teleportId, false));
player.sendTransaction();
if (data.isHasVelocity() && (data.getVelocityX() != 0 || data.getVelocityY() != 0 || data.getVelocityZ() != 0)) {
player.user.sendPacket(new WrapperPlayServerEntityVelocity(player.entityID, new Vector3d(data.getVelocityX(), data.getVelocityY(), data.getVelocityZ())));
if (data.getVelocity() != null && data.getVelocity().lengthSquared() > 0) {
player.user.sendPacket(new WrapperPlayServerEntityVelocity(player.entityID, new Vector3d(data.getVelocity().getX(), data.getVelocity().getY(), data.getVelocity().getZ())));
}
} finally {
isSendingSetback = false;
@@ -320,12 +295,12 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
TeleportData teleportPos;
while ((teleportPos = pendingTeleports.peek()) != null) {
double trueTeleportX = (teleportPos.isRelativeX() ? player.x : 0) + teleportPos.getLocationX();
double trueTeleportY = (teleportPos.isRelativeY() ? player.y : 0) + teleportPos.getLocationY();
double trueTeleportZ = (teleportPos.isRelativeZ() ? player.z : 0) + teleportPos.getLocationZ();
double trueTeleportX = (teleportPos.isRelativeX() ? player.x : 0) + teleportPos.getLocation().getX();
double trueTeleportY = (teleportPos.isRelativeY() ? player.y : 0) + teleportPos.getLocation().getY();
double trueTeleportZ = (teleportPos.isRelativeZ() ? player.z : 0) + teleportPos.getLocation().getZ();
// There seems to be a version difference in teleports past 30 million... just clamp the vector
Vector3d clamped = VectorUtils.clampVector(trueTeleportX, trueTeleportY, trueTeleportZ);
Vector3d clamped = VectorUtils.clampVector(new Vector3d(trueTeleportX, trueTeleportY, trueTeleportZ));
double threshold = teleportPos.isRelativePos() ? player.getMovementThreshold() : 0;
boolean closeEnoughY = Math.abs(clamped.getY() - y) <= 1e-7 + threshold; // 1.7 rounding
@@ -371,17 +346,18 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
int lastTransaction = player.lastTransactionReceived.get();
while (true) {
VehicleData.VehicleTeleport teleportPos = player.vehicleData.vehicleTeleports.peek();
Pair<Integer, Vector3d> teleportPos = player.vehicleData.vehicleTeleports.peek();
if (teleportPos == null) break;
if (lastTransaction < teleportPos.teleportId()) {
if (lastTransaction < teleportPos.first()) {
break;
}
if (teleportPos.x() == x && teleportPos.y() == y && teleportPos.z() == z) {
Vector3d position = teleportPos.second();
if (position.getX() == x && position.getY() == y && position.getZ() == z) {
player.vehicleData.vehicleTeleports.poll();
return true;
} else if (lastTransaction > teleportPos.teleportId() + 1) {
} else if (lastTransaction > teleportPos.first() + 1) {
player.vehicleData.vehicleTeleports.poll();
// Vehicles have terrible netcode so just ignore it if the teleport wasn't from us setting the player back
@@ -427,37 +403,36 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
!player.getSetbackTeleportUtil().hasAcceptedSpawnTeleport);
}
public void addSentTeleport(double x, double y, double z, double velocityX, double velocityY, double velocityZ, int transaction, RelativeFlag flags, boolean plugin, int teleportId) {
TeleportData data = new TeleportData(x, y, z, velocityX, velocityY, velocityZ, flags, transaction, teleportId);
public void addSentTeleport(Location position, Vector3d velocity, int transaction, RelativeFlag flags, boolean plugin, int teleportId) {
TeleportData data = new TeleportData(new Vector3d(position.getX(), position.getY(), position.getZ()), velocity, flags, transaction, teleportId);
pendingTeleports.add(data);
Vector3d safePosition = new Vector3d(position.getX(), position.getY(), position.getZ());
// We must convert relative teleports to avoid them becoming client controlled in the case of setback
if (flags.has(RelativeFlag.X)) {
x += lastKnownGoodPosition.posX;
safePosition = safePosition.withX(safePosition.getX() + lastKnownGoodPosition.pos.getX());
}
if (flags.has(RelativeFlag.Y)) {
y += lastKnownGoodPosition.posY;
safePosition = safePosition.withY(safePosition.getY() + lastKnownGoodPosition.pos.getY());
}
if (flags.has(RelativeFlag.Z)) {
z += lastKnownGoodPosition.posZ;
safePosition = safePosition.withZ(safePosition.getZ() + lastKnownGoodPosition.pos.getZ());
}
data = new TeleportData(x, y, z, velocityX, velocityY, velocityZ, new RelativeFlag(0b11000), transaction, teleportId);
requiredSetBack = new SetBackData(data, player.yaw, player.pitch, false, plugin);
data = new TeleportData(safePosition, velocity, new RelativeFlag(0b11000), transaction, teleportId);
requiredSetBack = new SetBackData(data, player.yaw, player.pitch, null, false, plugin);
this.lastKnownGoodPosition = new SetbackPosWithVector(x, y, z, 0, 0, 0);
this.lastKnownGoodPosition = new SetbackPosWithVector(safePosition, new Vector3dm());
}
@AllArgsConstructor
@Getter
@Setter
public static class SetbackPosWithVector {
private final double posX;
private final double posY;
private final double posZ;
private final double vectorY;
private double vectorX, vectorZ;
private final Vector3d pos;
private Vector3dm vector;
}
}
@@ -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());
@@ -6,12 +6,10 @@ import ac.grim.grimac.checks.type.PostPredictionCheck;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.predictionengine.UncertaintyHandler;
import ac.grim.grimac.predictionengine.predictions.PredictionEngine;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import ac.grim.grimac.utils.anticheat.update.PredictionComplete;
import ac.grim.grimac.utils.data.VectorData;
import ac.grim.grimac.utils.lists.EvictingQueue;
import ac.grim.grimac.utils.math.GrimMath;
import ac.grim.grimac.utils.math.Vec2;
import ac.grim.grimac.utils.math.Vector3dm;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState;
@@ -56,12 +54,7 @@ public final class SuperDebug extends Check implements PostPredictionCheck {
for (Iterator<Object2IntMap.Entry<StringBuilder>> it = continuedDebug.object2IntEntrySet().iterator(); it.hasNext(); ) {
Map.Entry<StringBuilder, Integer> debug = it.next();
appendDebug(debug.getKey(), player.predictedVelocity,
player.actualMovement.getX(), player.actualMovement.getY(), player.actualMovement.getZ(),
location,
player.startTickClientVel.getX(), player.startTickClientVel.getY(), player.startTickClientVel.getZ(),
player.baseTickAddition.getX(), player.baseTickAddition.getY(), player.baseTickAddition.getZ(),
player.baseTickWaterPushing.getX(), player.baseTickWaterPushing.getY(), player.baseTickWaterPushing.getZ());
appendDebug(debug.getKey(), player.predictedVelocity, player.actualMovement, location, player.startTickClientVel, player.baseTickAddition, player.baseTickWaterPushing);
debug.setValue(debug.getValue() - 1);
if (debug.getValue() <= 0) it.remove();
}
@@ -97,14 +90,7 @@ public final class SuperDebug extends Check implements PostPredictionCheck {
Vector3dm startTickVel = startTickClientVel.get(i);
Vector3dm addition = baseTickAddition.get(i);
Vector3dm water = baseTickWater.get(i);
appendDebug(sb,
predict,
actual.getX(), actual.getY(), actual.getZ(),
loc,
startTickVel.getX(), startTickVel.getY(), startTickVel.getZ(),
addition.getX(), addition.getY(), addition.getZ(),
water.getX(), water.getY(), water.getZ()
);
appendDebug(sb, predict, actual, loc, startTickVel, addition, water);
}
UncertaintyHandler uncertaintyHandler = player.uncertaintyHandler;
@@ -208,13 +194,7 @@ public final class SuperDebug extends Check implements PostPredictionCheck {
continuedDebug.put(sb, 40);
}
private void appendDebug(final StringBuilder sb, final VectorData predict,
final double actualX, final double actualY, final double actualZ,
final Location location,
final double startTickX, final double startTickY, final double startTickZ,
final double additionX, final double additionY, final double additionZ,
final double waterX, final double waterY, final double waterZ) {
private void appendDebug(StringBuilder sb, VectorData predict, Vector3dm actual, Location location, Vector3dm startTick, Vector3dm addition, Vector3dm water) {
if (predict.isZeroPointZeroThree()) {
sb.append("Movement threshold/tick skipping\n");
}
@@ -248,45 +228,36 @@ public final class SuperDebug extends Check implements PostPredictionCheck {
}
// Apply 0.003/0.005 to make numbers more accurate
Set<VectorData> set = new HashSet<>(Collections.singletonList(new VectorData(startTickX, startTickY, startTickZ, VectorData.VectorType.BestVelPicked)));
Set<VectorData> set = new HashSet<>(Collections.singletonList(new VectorData(startTick.clone(), VectorData.VectorType.BestVelPicked)));
new PredictionEngine().applyMovementThreshold(player, set);
VectorData tempVectorData = ((VectorData) set.toArray()[0]);
double trueStartVelX = tempVectorData.vectorX;
double trueStartVelY = tempVectorData.vectorY;
double trueStartVelZ = tempVectorData.vectorZ;
Vector3dm trueStartVel = ((VectorData) set.toArray()[0]).vector;
final double offsetX = actualX - trueStartVelX;
final double offsetY = actualY - trueStartVelY;
final double offsetZ = actualZ - trueStartVelZ;
double offsetLength = Math.sqrt(offsetX*offsetX + offsetY*offsetY + offsetZ*offsetZ);
Vec2 clientMovement = getPlayerMathMovement(player, offsetX, offsetZ, location.xRot);
Vec2 simulatedMovement = getPlayerMathMovement(player, predict.vectorX - trueStartVelX, predict.vectorZ - trueStartVelZ, location.xRot);
trueStartVelX += additionX + waterX;
trueStartVelY += additionY + waterY;
trueStartVelZ += additionZ + waterZ;
Vector3dm clientMovement = getPlayerMathMovement(player, actual.clone().subtract(trueStartVel), location.xRot);
Vector3dm simulatedMovement = getPlayerMathMovement(player, predict.vector.clone().subtract(trueStartVel), location.xRot);
Vector3dm offset = actual.clone().subtract(predict.vector);
trueStartVel.add(addition);
trueStartVel.add(water);
sb.append("Simulated: ");
sb.append(MessageUtil.toUnlabeledString(trueStartVelX, trueStartVelY, trueStartVelZ));
sb.append(predict.vector.toString());
sb.append("\nActually: ");
sb.append(MessageUtil.toUnlabeledString(actualX, actualY, actualZ));
sb.append(actual);
sb.append("\nOffset Vector: ");
sb.append(MessageUtil.toUnlabeledString(offsetX, offsetY, offsetZ));
sb.append(offset);
sb.append("\nOffset: ");
sb.append(offsetLength);
sb.append(offset.length());
sb.append("\nLocation: ");
sb.append(location);
sb.append("\nInitial velocity: ");
sb.append(MessageUtil.toUnlabeledString(startTickX, startTickY, startTickZ));
sb.append(startTick);
if (additionX != 0 || additionY != 0 || additionZ != 0) {
if (addition.lengthSquared() > 0) {
sb.append("\nInitial vel addition: ");
sb.append(MessageUtil.toUnlabeledString(additionX, additionY, additionZ));
sb.append(addition);
}
if (waterX != 0 || waterY != 0 || waterZ != 0) {
if (water.lengthSquared() > 0) {
sb.append("\nWater vel addition: ");
sb.append(MessageUtil.toUnlabeledString(waterX, waterY, waterZ));
sb.append(water);
}
sb.append("\nClient input: ");
@@ -302,14 +273,14 @@ public final class SuperDebug extends Check implements PostPredictionCheck {
sb.append("\n\n");
}
private Vec2 getPlayerMathMovement(GrimPlayer player, double x, double z, float f2) {
private Vector3dm getPlayerMathMovement(GrimPlayer player, Vector3dm wantedMovement, float f2) {
float f3 = player.trigHandler.sin(f2 * 0.017453292f);
float f4 = player.trigHandler.cos(f2 * 0.017453292f);
float bestTheoreticalX = (float) (f3 * z + f4 * x) / (f3 * f3 + f4 * f4);
float bestTheoreticalZ = (float) (-f3 * x + f4 * z) / (f3 * f3 + f4 * f4);
float bestTheoreticalX = (float) (f3 * wantedMovement.getZ() + f4 * wantedMovement.getX()) / (f3 * f3 + f4 * f4);
float bestTheoreticalZ = (float) (-f3 * wantedMovement.getX() + f4 * wantedMovement.getZ()) / (f3 * f3 + f4 * f4);
return new Vec2(bestTheoreticalX, bestTheoreticalZ);
return new Vector3dm(bestTheoreticalX, 0, bestTheoreticalZ);
}
private record Location(double x, double y, double z, float xRot, float yRot, String world) {
@@ -2,7 +2,7 @@ package ac.grim.grimac.manager.init.start;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.utils.anticheat.LogUtil;
import ac.grim.grimac.utils.reflection.ViaVersionUtil;
import ac.grim.grimac.utils.viaversion.ViaVersionUtil;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
@@ -2,7 +2,7 @@ package ac.grim.grimac.manager.init.start;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.utils.anticheat.LogUtil;
import ac.grim.grimac.utils.reflection.ViaVersionUtil;
import ac.grim.grimac.utils.viaversion.ViaVersionUtil;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.viaversion.viaversion.api.Via;
@@ -12,6 +12,7 @@ public class ViaVersion implements StartableInitable {
@Override
public void start() {
if (!ViaVersionUtil.isAvailable) return;
ViaVersionUtil.injectHooks();
ServerVersion serverVersion = PacketEvents.getAPI().getServerManager().getVersion();
@@ -30,6 +30,7 @@ import ac.grim.grimac.utils.data.packetentity.PacketEntitySelf;
import ac.grim.grimac.utils.data.tags.SyncedTags;
import ac.grim.grimac.utils.enums.FluidTag;
import ac.grim.grimac.utils.enums.Pose;
import ac.grim.grimac.utils.inventory.InventoryDesyncStatus;
import ac.grim.grimac.utils.latency.*;
import ac.grim.grimac.utils.math.GrimMath;
import ac.grim.grimac.utils.math.Location;
@@ -38,8 +39,9 @@ import ac.grim.grimac.utils.math.Vector3dm;
import ac.grim.grimac.utils.nmsutil.BlockProperties;
import ac.grim.grimac.utils.nmsutil.Collisions;
import ac.grim.grimac.utils.nmsutil.GetBoundingBox;
import ac.grim.grimac.utils.nmsutil.ReachUtils;
import ac.grim.grimac.utils.nmsutil.Materials;
import ac.grim.grimac.utils.reflection.ViaVersionUtil;
import ac.grim.grimac.utils.viaversion.ViaVersionUtil;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
@@ -209,7 +211,7 @@ public class GrimPlayer implements GrimUser {
public final CompensatedWorld compensatedWorld;
public final CompensatedEntities compensatedEntities;
public final CompensatedInventory inventory;
public final LatencyUtils latencyUtils = new LatencyUtils(this);
public final ILatencyUtils latencyUtils = new LatencyUtils(this);
public final PointThreeEstimator pointThreeEstimator;
public final TrigHandler trigHandler = new TrigHandler(this);
public final PacketStateData packetStateData = new PacketStateData();
@@ -236,6 +238,9 @@ public class GrimPlayer implements GrimUser {
// possibleEyeHeights[0] = Standing eye heights, [1] = Sneaking. [2] = Elytra, Swimming, and Riptide Trident which only exists in 1.9+
public final double[][] possibleEyeHeights = new double[3][];
public int totalFlyingPacketsSent;
public boolean hasInventoryOpen;
public long lastInventoryOpen;
public InventoryDesyncStatus inventoryDesyncStatus;
public final Queue<BlockPlaceSnapshot> placeUseItemPackets = new LinkedBlockingQueue<>();
public final Queue<BlockBreak> queuedBreaks = new LinkedBlockingQueue<>();
public final PlayerBlockHistory blockHistory = new PlayerBlockHistory();
@@ -364,15 +369,12 @@ public class GrimPlayer implements GrimUser {
// It's very difficult to test precedence so if there's issues with this bouncy implementation let me know
for (VectorData data : new HashSet<>(possibleMovements)) {
for (BlockFace direction : uncertaintyHandler.slimePistonBounces) {
final double x = data.vectorX;
final double y = data.vectorY;
final double z = data.vectorZ;
if (direction.getModX() != 0) {
possibleMovements.add(data.returnNewModified(direction.getModX(), y, z, VectorData.VectorType.SlimePistonBounce));
possibleMovements.add(data.returnNewModified(data.vector.clone().setX(direction.getModX()), VectorData.VectorType.SlimePistonBounce));
} else if (direction.getModY() != 0) {
possibleMovements.add(data.returnNewModified(x, direction.getModY(), z, VectorData.VectorType.SlimePistonBounce));
possibleMovements.add(data.returnNewModified(data.vector.clone().setY(direction.getModY()), VectorData.VectorType.SlimePistonBounce));
} else if (direction.getModZ() != 0) {
possibleMovements.add(data.returnNewModified(x, y, direction.getModZ(), VectorData.VectorType.SlimePistonBounce));
possibleMovements.add(data.returnNewModified(data.vector.clone().setZ(direction.getModZ()), VectorData.VectorType.SlimePistonBounce));
}
}
}
@@ -661,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);
@@ -799,10 +828,7 @@ public class GrimPlayer implements GrimUser {
}
public boolean isInWaterOrRain() {
return compensatedWorld.isRaining
|| Collisions.hasMaterial(this, boundingBox.copy().expand(0.1f),
(block, x, y, z) -> Materials.isWater(CompensatedWorld.blockVersion, block)
);
return compensatedWorld.isRaining || Collisions.hasMaterial(this, boundingBox.copy().expand(0.1f), (block) -> Materials.isWater(CompensatedWorld.blockVersion, block.first()));
}
@Contract(pure = true)
@@ -931,12 +957,12 @@ public class GrimPlayer implements GrimUser {
if (platformPlayer != null) platformPlayer.sendMessage(message);
}
public void resyncPosition(final int x, final int y, final int z) {
this.resyncHandler.resync(x, y, z, x, y, z);
public void resyncPosition(Vector3i pos) {
this.resyncHandler.resync(pos.getX(), pos.getY(), pos.getZ(), pos.getX(), pos.getY(), pos.getZ());
}
public void resyncPosition(final int x, final int y, final int z, final int sequenceID) {
this.resyncHandler.resyncPosition(x, y, z, sequenceID);
public void resyncPosition(Vector3i pos, int sequenceID) {
this.resyncHandler.resyncPosition(pos.x, pos.y, pos.z, sequenceID);
}
public void resyncPositions(SimpleCollisionBox box) {
@@ -18,7 +18,6 @@ import ac.grim.grimac.predictionengine.predictions.rideable.PredictionEngineBoat
import ac.grim.grimac.utils.anticheat.update.PositionUpdate;
import ac.grim.grimac.utils.anticheat.update.PredictionComplete;
import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
import ac.grim.grimac.utils.data.SetBackData;
import ac.grim.grimac.utils.data.VectorData;
import ac.grim.grimac.utils.data.packetentity.PacketEntity;
import ac.grim.grimac.utils.data.packetentity.PacketEntityCamel;
@@ -71,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();
}
}
@@ -231,7 +231,9 @@ public class MovementCheckRunner extends Check implements PositionCheck {
handleTeleport(update);
if (player.isClimbing) {
player.lastWasClimbing = PredictionEngineNormal.staticVectorEndOfTickY(player, 0.2);
Vector3dm ladder = player.clientVelocity.clone().setY(0.2);
PredictionEngineNormal.staticVectorEndOfTick(player, ladder);
player.lastWasClimbing = ladder.getY();
}
return;
}
@@ -361,7 +363,8 @@ public class MovementCheckRunner extends Check implements PositionCheck {
}
SimpleCollisionBox steppingOnBB = GetBoundingBox.getCollisionBoxForPlayer(player, player.x, player.y, player.z).expand(player.getMovementThreshold()).offset(0, -1, 0);
Collisions.hasMaterial(player, steppingOnBB, (data) -> {
Collisions.hasMaterial(player, steppingOnBB, (pair) -> {
WrappedBlockState data = pair.first();
if (data.getType() == StateTypes.SLIME_BLOCK && player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_8)) {
player.uncertaintyHandler.isSteppingOnSlime = true;
player.uncertaintyHandler.isSteppingOnBouncyBlock = true;
@@ -407,8 +410,8 @@ public class MovementCheckRunner extends Check implements PositionCheck {
player.uncertaintyHandler.isNearGlitchyBlock = player.getClientVersion().isOlderThan(ClientVersion.V_1_9)
&& Collisions.hasMaterial(player, expandedBB.copy().expand(0.2),
checkData -> BlockTags.ANVIL.contains(checkData.getType())
|| checkData.getType() == StateTypes.CHEST || checkData.getType() == StateTypes.TRAPPED_CHEST);
checkData -> BlockTags.ANVIL.contains(checkData.first().getType())
|| checkData.first().getType() == StateTypes.CHEST || checkData.first().getType() == StateTypes.TRAPPED_CHEST);
player.uncertaintyHandler.isOrWasNearGlitchyBlock = isGlitchy || player.uncertaintyHandler.isNearGlitchyBlock;
player.uncertaintyHandler.checkForHardCollision();
@@ -450,9 +453,7 @@ public class MovementCheckRunner extends Check implements PositionCheck {
player.clientVelocity = player.actualMovement.clone();
player.gravity = 0;
player.friction = 0.91f;
player.clientVelocity.setX(player.clientVelocity.getX() * player.friction);
player.clientVelocity.setY(PredictionEngineNormal.staticVectorEndOfTickY(player, player.clientVelocity.getY()));
player.clientVelocity.setZ(player.clientVelocity.getZ() * player.friction);
PredictionEngineNormal.staticVectorEndOfTick(player, player.clientVelocity);
} else if (riding == null) {
wasChecked = true;
@@ -521,7 +522,7 @@ public class MovementCheckRunner extends Check implements PositionCheck {
} // If it isn't any of these cases, the player is on a mob they can't control and therefore is exempt
// No, don't comment about the sqrt call. It doesn't matter unless you run sqrt thousands of times a second.
double offset = player.actualMovement.distance(player.predictedVelocity.vectorX, player.predictedVelocity.vectorY, player.predictedVelocity.vectorZ);
double offset = player.predictedVelocity.vector.distance(player.actualMovement);
offset = player.uncertaintyHandler.reduceOffset(offset);
if (player.packetStateData.tryingToRiptide != clientClaimsRiptide) {
@@ -534,19 +535,19 @@ public class MovementCheckRunner extends Check implements PositionCheck {
//
// Checking for oldClientVel being too high fixes BleachHack vertical scaffold
if (player.getSetbackTeleportUtil().getRequiredSetBack() != null && player.getSetbackTeleportUtil().getRequiredSetBack().getTicksComplete() == 1) {
SetBackData setback = player.getSetbackTeleportUtil().getRequiredSetBack();
Vector3dm setbackVel = player.getSetbackTeleportUtil().getRequiredSetBack().getVelocity();
// A player must have velocity going INTO the ground to be able to jump
// Otherwise they could ignore upwards velocity that isn't useful into more useful upwards velocity (towering)
// So if they are supposed to be going upwards, or are supposed to be off the ground, resync
if (player.predictedVelocity.isJump()
&& !player.wasTouchingLava && !player.wasTouchingWater
&& ((setback.isHasVelocity() && setback.getVelocityY() >= 0) || !Collisions.slowCouldPointThreeHitGround(player, player.lastX, player.lastY, player.lastZ))) {
&& ((setbackVel != null && setbackVel.getY() >= 0) || !Collisions.slowCouldPointThreeHitGround(player, player.lastX, player.lastY, player.lastZ))) {
player.getSetbackTeleportUtil().executeForceResync();
}
boolean lavaBugFix = player.wasTouchingLava && player.predictedVelocity.isJump() &&
player.predictedVelocity.vectorY < 0.06 && player.predictedVelocity.vectorY > -0.02;
player.predictedVelocity.vector.getY() < 0.06 && player.predictedVelocity.vector.getY() > -0.02;
// Player ignored the knockback or is delaying it a tick... bad!
if (!player.predictedVelocity.isKnockback() && !lavaBugFix && player.getSetbackTeleportUtil().getRequiredSetBack().isHasVelocity()) {
if (!player.predictedVelocity.isKnockback() && !lavaBugFix && player.getSetbackTeleportUtil().getRequiredSetBack().getVelocity() != null) {
// And then send it again!
player.getSetbackTeleportUtil().executeForceResync();
}
@@ -566,9 +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.setVectorX(lastKnownGoodPosition.getVectorX() * 0.6 * 0.91);
lastKnownGoodPosition.setVectorZ(lastKnownGoodPosition.getVectorZ() * 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,26 +271,32 @@ public class PointThreeEstimator {
isNearClimbable = false;
isNearBubbleColumn = false;
isNearFluid = false;
isNearNetherPortal = false;
// Check for flowing water
Collisions.hasMaterial(player, pointThreeBox, (state, x, y, z) -> {
Collisions.hasMaterial(player, pointThreeBox, (pair) -> {
final WrappedBlockState state = pair.first();
final StateType stateType = state.getType();
if (player.tagManager.block(SyncedTags.CLIMBABLE).contains(stateType) || (stateType == StateTypes.POWDER_SNOW && !player.inVehicle() && player.inventory.getBoots().getType() == ItemTypes.LEATHER_BOOTS)) {
isNearClimbable = true;
}
if (BlockTags.TRAPDOORS.contains(stateType)) {
isNearClimbable = isNearClimbable || Collisions.trapdoorUsableAsLadder(player, x, y, z, state);
isNearClimbable = isNearClimbable || Collisions.trapdoorUsableAsLadder(player, pair.second().getX(), pair.second().getY(), pair.second().getZ(), state);
}
if (stateType == StateTypes.BUBBLE_COLUMN && player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_13)) {
isNearBubbleColumn = true;
}
if (Materials.isWater(player.getClientVersion(), state) || state.getType() == StateTypes.LAVA) {
if (Materials.isWater(player.getClientVersion(), pair.first()) || pair.first().getType() == StateTypes.LAVA) {
isNearFluid = true;
}
if (state.getType() == StateTypes.NETHER_PORTAL) {
isNearNetherPortal = true;
}
return false;
});
}
@@ -364,7 +375,7 @@ public class PointThreeEstimator {
// If this tick is the tick after y velocity was by 0, a stepping movement is POSSIBLE to have been hidden
// A bit hacky... is there a better way? I'm unsure...
boolean likelyStepSkip = player.isPointThree() && (data.vectorY > -0.08 && data.vectorY < 0.06) && couldStep;
boolean likelyStepSkip = player.isPointThree() && (data.vector.getY() > -0.08 && data.vector.getY() < 0.06) && couldStep;
// We need to do hypot calculations for all 3 axis
// sqrt(sqrt(x^2 + z^2)^2 + y^2) = hypot(x, z, y)
@@ -410,7 +421,7 @@ public class PointThreeEstimator {
wasAlwaysCertain = false;
// Head hitters return the vector to 0, and then apply gravity to it.
// Not much room for abuse for this, so keep it lenient
return -Math.max(0, vector.vectorY) - 0.1 - fluidAddition;
return -Math.max(0, vector.vector.getY()) - 0.1 - fluidAddition;
} else if (player.uncertaintyHandler.wasAffectedByStuckSpeed()) {
wasAlwaysCertain = false;
// This shouldn't be needed but stuck speed can desync very easily with 0.03...
@@ -424,7 +435,7 @@ public class PointThreeEstimator {
double minMovement = player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9) ? 0.003 : 0.005;
// This should likely be refactored, but it works well.
double yVel = vector.vectorY;
double yVel = vector.vector.getY();
double maxYTraveled = 0;
boolean first = true;
do {
@@ -451,7 +462,7 @@ public class PointThreeEstimator {
// We aren't making progress, avoid infinite loop (This can be due to the player not having gravity)
if (yVel == 0) break;
} while (Math.abs(maxYTraveled + vector.vectorY) < player.getMovementThreshold()); // Account for uncertainty, don't stop until we simulate past uncertainty point
} while (Math.abs(maxYTraveled + vector.vector.getY()) < player.getMovementThreshold()); // Account for uncertainty, don't stop until we simulate past uncertainty point
if (maxYTraveled != 0) {
wasAlwaysCertain = false;
@@ -66,13 +66,13 @@ public class SneakingEstimator extends Check implements PostPredictionCheck {
if (data.isJump() == player.predictedVelocity.isJump() && data.isKnockback() == player.predictedVelocity.isKnockback() && data.isExplosion() == player.predictedVelocity.isExplosion()) {
// Fuck, we are compounding this which is very dangerous. After light testing seems fine... can we do better?
if (player.uncertaintyHandler.lastStuckWest.hasOccurredSince(0) || player.uncertaintyHandler.lastStuckNorth.hasOccurredSince(0)) {
sneakingPotentialHiddenVelocity.minX = Math.min(sneakingPotentialHiddenVelocity.minX, data.vectorX);
sneakingPotentialHiddenVelocity.minZ = Math.min(sneakingPotentialHiddenVelocity.minZ, data.vectorZ);
sneakingPotentialHiddenVelocity.minX = Math.min(sneakingPotentialHiddenVelocity.minX, data.vector.getX());
sneakingPotentialHiddenVelocity.minZ = Math.min(sneakingPotentialHiddenVelocity.minZ, data.vector.getZ());
}
if (player.uncertaintyHandler.lastStuckEast.hasOccurredSince(0) || player.uncertaintyHandler.lastStuckSouth.hasOccurredSince(0)) {
sneakingPotentialHiddenVelocity.maxX = Math.max(sneakingPotentialHiddenVelocity.maxX, data.vectorX);
sneakingPotentialHiddenVelocity.maxZ = Math.max(sneakingPotentialHiddenVelocity.maxZ, data.vectorZ);
sneakingPotentialHiddenVelocity.maxX = Math.max(sneakingPotentialHiddenVelocity.maxX, data.vector.getX());
sneakingPotentialHiddenVelocity.maxZ = Math.max(sneakingPotentialHiddenVelocity.maxZ, data.vector.getZ());
}
}
}
@@ -264,7 +264,7 @@ public class UncertaintyHandler {
return 0.06;
// Not worth my time to fix this because checking flying generally sucks - if player was flying in last 2 ticks
if ((lastFlyingTicks.hasOccurredSince(5)) && Math.abs(data.vectorY) < (4.5 * player.flySpeed - 0.25))
if ((lastFlyingTicks.hasOccurredSince(5)) && Math.abs(data.vector.getY()) < (4.5 * player.flySpeed - 0.25))
return 0.06;
double pointThree = player.getMovementThreshold();
@@ -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;
@@ -89,12 +89,12 @@ public class MovementTicker {
player.uncertaintyHandler.collidingEntities.add(possibleCollidingEntities);
}
private boolean isHorizontalCollisionSoft(double x, double y, double z) {
double horizontalLengthSquared = x * x + z * z;
private boolean isHorizontalCollisionSoft(Vector3dm collide) {
double horizontalLengthSquared = collide.getX() * collide.getX() + collide.getZ() * collide.getZ();
if (horizontalLengthSquared < 1E-5F) return false;
float xxa = (float) player.predictedVelocity.input.x();
float zza = (float) player.predictedVelocity.input.z();
float xxa = (float) player.predictedVelocity.input.getX();
float zza = (float) player.predictedVelocity.input.getZ();
float yawInRadians = player.yaw * (float) (Math.PI / 180.0);
double sin = player.trigHandler.sin(yawInRadians);
@@ -102,17 +102,17 @@ public class MovementTicker {
double g = xxa * cos - zza * sin;
double h = zza * cos + xxa * sin;
double i = g * g + h * h;
return i >= 1E-5F && Math.acos((g * x + h * z) / Math.sqrt(i * horizontalLengthSquared)) < 0.13962634F;
return i >= 1E-5F && Math.acos((g * collide.getX() + h * collide.getZ()) / Math.sqrt(i * horizontalLengthSquared)) < 0.13962634F;
}
public void move(Vector3dm inputVel, double x, double y, double z) {
public void move(Vector3dm inputVel, Vector3dm collide) {
if (player.stuckSpeedMultiplier.getX() < 0.99) {
player.clientVelocity = new Vector3dm();
}
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_18_2)) {
boolean xAxis = !GrimMath.equal(inputVel.getX(), x);
boolean zAxis = !GrimMath.equal(inputVel.getZ(), z);
boolean xAxis = !GrimMath.equal(inputVel.getX(), collide.getX());
boolean zAxis = !GrimMath.equal(inputVel.getZ(), collide.getZ());
if (xAxis) {
player.clientVelocity.setX(0);
@@ -123,20 +123,20 @@ public class MovementTicker {
}
player.horizontalCollision = xAxis || zAxis;
player.softHorizontalCollision = player.horizontalCollision && isHorizontalCollisionSoft(x, y, z);
player.softHorizontalCollision = player.horizontalCollision && isHorizontalCollisionSoft(collide);
} else {
if (inputVel.getX() != x) {
if (inputVel.getX() != collide.getX()) {
player.clientVelocity.setX(0);
}
if (inputVel.getZ() != z) {
if (inputVel.getZ() != collide.getZ()) {
player.clientVelocity.setZ(0);
}
player.horizontalCollision = inputVel.getX() != x || inputVel.getZ() != z;
player.horizontalCollision = inputVel.getX() != collide.getX() || inputVel.getZ() != collide.getZ();
}
player.verticalCollision = inputVel.getY() != y;
player.verticalCollision = inputVel.getY() != collide.getY();
// Avoid order of collisions being wrong because 0.03 movements
// Stepping movement USUALLY means the vehicle in on the ground as vehicles can't jump
@@ -145,7 +145,7 @@ public class MovementTicker {
boolean calculatedOnGround = (player.verticalCollision && inputVel.getY() < 0.0D);
// If the player is on the ground with a y velocity of 0, let the player decide (too close to call)
if (inputVel.getY() == -SimpleCollisionBox.COLLISION_EPSILON && y > -SimpleCollisionBox.COLLISION_EPSILON && y <= 0 && !player.inVehicle())
if (inputVel.getY() == -SimpleCollisionBox.COLLISION_EPSILON && collide.getY() > -SimpleCollisionBox.COLLISION_EPSILON && collide.getY() <= 0 && !player.inVehicle())
calculatedOnGround = player.onGround;
player.clientClaimsLastOnGround = player.onGround;
@@ -177,9 +177,9 @@ public class MovementTicker {
if (player.onGround) {
player.fallDistance = 0;
} else if (y < 0) {
player.fallDistance = (player.fallDistance) - y;
player.vehicleData.lastYd = y;
} else if (collide.getY() < 0) {
player.fallDistance = (player.fallDistance) - collide.getY();
player.vehicleData.lastYd = collide.getY();
}
// Striders call the method for inside blocks AGAIN!
@@ -187,11 +187,11 @@ public class MovementTicker {
Collisions.handleInsideBlocks(player);
}
player.mainSupportingBlockData = MainSupportingBlockPosFinder.findMainSupportingBlockPos(player, player.mainSupportingBlockData, new Vector3d(x, y, z), player.boundingBox, player.onGround);
player.mainSupportingBlockData = MainSupportingBlockPosFinder.findMainSupportingBlockPos(player, player.mainSupportingBlockData, new Vector3d(collide.getX(), collide.getY(), collide.getZ()), player.boundingBox, player.onGround);
StateType onBlock = BlockProperties.getOnPos(player, player.mainSupportingBlockData, new Vector3d(player.x, player.y, player.z));
// Hack with 1.14+ poses issue
if (inputVel.getY() != y) {
if (inputVel.getY() != collide.getY()) {
// If the client supports slime blocks
// And the block is a slime block
// Or the block is honey and was replaced by viaversion
@@ -214,15 +214,14 @@ public class MovementTicker {
player.clientVelocity.setY(0);
}
}
// TODO reimplement? the comment about reimplementing clampMovementToHardBorder is 3 years old...
// collide = PredictionEngine.clampMovementToHardBorder(player, collide);
collide = PredictionEngine.clampMovementToHardBorder(player, collide);
// The game disregards movements smaller than 1e-7 (such as in boats)
double collideLengthSquared = GrimMath.lengthSquared(x, y, z);
if (collideLengthSquared <= 1e-7
if (collide.lengthSquared() <= 1e-7
// New condition added in 1.21.2
&& (player.getClientVersion().isOlderThan(ClientVersion.V_1_21_2) || inputVel.lengthSquared() - collideLengthSquared >= 1e-7)) {
x = y = z = 0;
&& (player.getClientVersion().isOlderThan(ClientVersion.V_1_21_2) || inputVel.lengthSquared() - collide.lengthSquared() >= 1e-7)) {
collide = new Vector3dm();
} else if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_21_5)) {
Vector3d from = new Vector3d(player.lastX, player.lastY, player.lastZ);
Vector3d to = new Vector3d(player.x, player.y, player.z);
@@ -231,7 +230,7 @@ public class MovementTicker {
}
// This is where vanilla moves the bounding box and sets it
player.predictedVelocity = new VectorData(x, y, z, player.predictedVelocity.lastVector, player.predictedVelocity.vectorType);
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(f, 1, f);
@@ -307,10 +306,10 @@ public class MovementTicker {
// Meaning we should scan upwards!
oldBB.expand(-SimpleCollisionBox.COLLISION_EPSILON);
double posX = Math.max(0, player.predictedVelocity.vectorX) + SimpleCollisionBox.COLLISION_EPSILON;
double negX = Math.min(0, player.predictedVelocity.vectorX) - SimpleCollisionBox.COLLISION_EPSILON;
double posZ = Math.max(0, player.predictedVelocity.vectorZ) + SimpleCollisionBox.COLLISION_EPSILON;
double negZ = Math.min(0, player.predictedVelocity.vectorZ) - SimpleCollisionBox.COLLISION_EPSILON;
double posX = Math.max(0, player.predictedVelocity.vector.getX()) + SimpleCollisionBox.COLLISION_EPSILON;
double negX = Math.min(0, player.predictedVelocity.vector.getX()) - SimpleCollisionBox.COLLISION_EPSILON;
double posZ = Math.max(0, player.predictedVelocity.vector.getZ()) + SimpleCollisionBox.COLLISION_EPSILON;
double negZ = Math.min(0, player.predictedVelocity.vector.getZ()) - SimpleCollisionBox.COLLISION_EPSILON;
boolean xAxisCollision = !Collisions.isEmpty(player, oldBB.expandMin(negX, 0, 0).expandMax(posX, 0, 0));
boolean zAxisCollision = !Collisions.isEmpty(player, oldBB.expandMin(0, 0, negZ).expandMax(0, 0, posZ));
@@ -436,7 +435,7 @@ public class MovementTicker {
// 1.13 and below players can't climb ladders while touching water
// yes, 1.13 players cannot climb ladders underwater
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_14) && player.isClimbing) {
player.lastWasClimbing = FluidFallingAdjustedMovement.getAdjustedY(player, playerGravity, isFalling, 0.2D * 0.8F);
player.lastWasClimbing = FluidFallingAdjustedMovement.getFluidFallingAdjustedMovement(player, playerGravity, isFalling, player.clientVelocity.clone().setY(0.2D * 0.8F)).getY();
}
} else {
@@ -448,7 +447,7 @@ public class MovementTicker {
// Lava movement changed in 1.16
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_16) && player.slightlyTouchingLava) {
player.clientVelocity = player.clientVelocity.multiply(0.5D, 0.800000011920929D, 0.5D);
player.clientVelocity.setY(FluidFallingAdjustedMovement.getAdjustedY(player, playerGravity, isFalling, player.clientVelocity.getY()));
player.clientVelocity = FluidFallingAdjustedMovement.getFluidFallingAdjustedMovement(player, playerGravity, isFalling, player.clientVelocity);
} else {
player.clientVelocity.multiply(0.5D);
}
@@ -9,7 +9,6 @@ import ac.grim.grimac.utils.data.KnownInput;
import ac.grim.grimac.utils.data.Pair;
import ac.grim.grimac.utils.data.VectorData;
import ac.grim.grimac.utils.math.GrimMath;
import ac.grim.grimac.utils.math.Vec2d;
import ac.grim.grimac.utils.math.Vector3dm;
import ac.grim.grimac.utils.math.VectorUtils;
import ac.grim.grimac.utils.nmsutil.Collisions;
@@ -32,22 +31,22 @@ public class PredictionEngine {
return outputVel;
}
public static Vec2d transformInputsToVector(GrimPlayer player, int strafe, int forward) {
public static Vector3dm transformInputsToVector(GrimPlayer player, Vector3dm theoreticalInput) {
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_21_5)) {
Vec2 moveVector = new Vec2((float) strafe, (float) forward).normalized();
Vec2 moveVector = new Vec2((float) theoreticalInput.getX(), (float) theoreticalInput.getZ()).normalized();
Vec2 input = modifyInput(player, moveVector);
return new Vec2d(input.x(), input.y());
return new Vector3dm(input.x(), 0, input.y());
}
float bestPossibleX;
float bestPossibleZ;
// Slow movement was determined by the previous pose
if (player.isSlowMovement) {
bestPossibleX = (float) (strafe * player.sneakingSpeedMultiplier);
bestPossibleZ = (float) (forward * player.sneakingSpeedMultiplier);
bestPossibleX = (float) (theoreticalInput.getX() * player.sneakingSpeedMultiplier);
bestPossibleZ = (float) (theoreticalInput.getZ() * player.sneakingSpeedMultiplier);
} else {
bestPossibleX = Math.min(Math.max(-1f, strafe), 1f);
bestPossibleZ = Math.min(Math.max(-1f, forward), 1f);
bestPossibleX = Math.min(Math.max(-1f, Math.round(theoreticalInput.getX())), 1f);
bestPossibleZ = Math.min(Math.max(-1f, Math.round(theoreticalInput.getZ())), 1f);
}
if (player.packetStateData.isSlowedByUsingItem()) {
@@ -55,23 +54,18 @@ public class PredictionEngine {
bestPossibleZ *= 0.2F;
}
double inputVectorX = bestPossibleX;
double inputVectorZ = bestPossibleZ;
inputVectorX *= 0.98F;
inputVectorZ *= 0.98F;
Vector3dm inputVector = new Vector3dm(bestPossibleX, 0, bestPossibleZ);
inputVector.multiply(0.98F);
// Simulate float rounding imprecision
inputVectorX = (float) inputVectorX;
inputVectorZ = (float) inputVectorZ;
inputVector = new Vector3dm((float) inputVector.getX(), (float) inputVector.getY(), (float) inputVector.getZ());
double lengthSquared = GrimMath.lengthSquared(inputVectorX, inputVectorZ);
if (lengthSquared > 1) {
double d0 = Math.sqrt(lengthSquared);
inputVectorX = inputVectorX / d0;
inputVectorZ = inputVectorZ / d0;
if (inputVector.lengthSquared() > 1) {
double d0 = Math.sqrt(inputVector.getX() * inputVector.getX() + inputVector.getY() * inputVector.getY() + inputVector.getZ() * inputVector.getZ());
inputVector = new Vector3dm(inputVector.getX() / d0, inputVector.getY() / d0, inputVector.getZ() / d0);
}
return new Vec2d(inputVectorX, inputVectorZ);
return inputVector;
}
public static Vec2 modifyInput(GrimPlayer player, Vec2 moveVector) {
@@ -146,7 +140,7 @@ public class PredictionEngine {
// Client velocity - before collision and carried into the next tick
// Predicted velocity - after collision and not carried into the next tick
new MovementTickerPlayer(player).move(player.clientVelocity.clone(), player.predictedVelocity.vectorX, player.predictedVelocity.vectorY, player.predictedVelocity.vectorZ);
new MovementTickerPlayer(player).move(player.clientVelocity.clone(), player.predictedVelocity.vector);
endOfTick(player, player.gravity);
}
@@ -240,7 +234,7 @@ public class PredictionEngine {
beforeCollisionMovement = primaryPushMovement;
// We basically want to avoid falsing ground spoof, try to find a vector that works
if (player.wouldCollisionResultFlagGroundSpoof(primaryPushMovement.getY(), bestCollisionVel.vectorY))
if (player.wouldCollisionResultFlagGroundSpoof(primaryPushMovement.getY(), bestCollisionVel.vector.getY()))
resultAccuracy += 0.0001 * 0.0001;
bestInput = resultAccuracy;
@@ -265,7 +259,7 @@ public class PredictionEngine {
}
private Pair<Vector3dm, Vector3dm> doSeekingWallCollisions(GrimPlayer player, Vector3dm primaryPushMovement, Vector3dm originalClientVel, VectorData clientVelAfterInput) {
boolean vehicleKB = player.inVehicle() && clientVelAfterInput.isKnockback() && clientVelAfterInput.vectorY == 0;
boolean vehicleKB = player.inVehicle() && clientVelAfterInput.isKnockback() && clientVelAfterInput.vector.getY() == 0;
// Extra collision epsilon required for vehicles to be accurate
double xAdditional = Math.signum(primaryPushMovement.getX()) * SimpleCollisionBox.COLLISION_EPSILON;
// The server likes sending y=0 kb "lifting" the player off the ground.
@@ -335,9 +329,10 @@ public class PredictionEngine {
player.clientVelocity.getX(), 0, player.clientVelocity.getZ()).expand(0.5, -SimpleCollisionBox.COLLISION_EPSILON, 0.5)))) {
// Calculate the Y velocity after friction
double y = PredictionEngineNormal.staticVectorEndOfTickY(player, 0.2);
Vector3dm hackyClimbVector = new Vector3dm(0, 0.2, 0);
PredictionEngineNormal.staticVectorEndOfTick(player, hackyClimbVector);
pointThreePossibilities.add(new VectorData(0, y, 0, VectorData.VectorType.ZeroPointZeroThree));
pointThreePossibilities.add(new VectorData(hackyClimbVector, VectorData.VectorType.ZeroPointZeroThree));
}
// This is a secure method to add jumping vectors to this list
@@ -362,24 +357,19 @@ public class PredictionEngine {
for (VectorData vectorData : data) {
// Sneaking in water
if (vectorData.isKnockback() && player.baseTickAddition.lengthSquared() != 0) {
vectorData.vectorX += player.baseTickAddition.getX();
vectorData.vectorY += player.baseTickAddition.getY();
vectorData.vectorZ += player.baseTickAddition.getZ();
vectorData.vector = vectorData.vector.add(player.baseTickAddition);
}
// Water pushing movement is affected by initial velocity due to 0.003 eating pushing in the past
if (vectorData.isKnockback() && player.baseTickWaterPushing.lengthSquared() != 0) {
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_13)) {
Vector3dm vec3 = player.baseTickWaterPushing.clone();
if (Math.abs(vectorData.vectorX) < 0.003 && Math.abs(vectorData.vectorZ) < 0.003 && player.baseTickWaterPushing.length() < 0.0045000000000000005D) {
if (Math.abs(vectorData.vector.getX()) < 0.003 && Math.abs(vectorData.vector.getZ()) < 0.003 && player.baseTickWaterPushing.length() < 0.0045000000000000005D) {
vec3 = vec3.normalize().multiply(0.0045000000000000005);
}
vectorData.vectorX += vec3.getX();
vectorData.vectorY += vec3.getY();
vectorData.vectorZ += vec3.getZ();
vectorData.vector = vectorData.vector.add(vec3);
} else {
vectorData.vectorX += player.baseTickWaterPushing.getX();
vectorData.vectorY += player.baseTickWaterPushing.getY();
vectorData.vectorZ += player.baseTickWaterPushing.getZ();
vectorData.vector = vectorData.vector.add(player.baseTickWaterPushing);
}
}
}
@@ -415,9 +405,7 @@ public class PredictionEngine {
if (!player.inVehicle() || player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_21_5)) return;
for (VectorData vectorData : data) {
vectorData.vectorX = vectorData.vectorX * 0.98;
vectorData.vectorY = vectorData.vectorY * 0.98;
vectorData.vectorZ = vectorData.vectorZ * 0.98;
vectorData.vector = vectorData.vector.clone().multiply(0.98);
}
}
@@ -425,11 +413,11 @@ public class PredictionEngine {
for (int x = 1; x <= Math.min(player.maxAttackSlow, 5); x++) {
for (VectorData data : new HashSet<>(velocities)) {
if (player.minAttackSlow > 0) {
data.vectorX = data.vectorX * 0.6;
data.vectorZ = data.vectorZ * 0.6;
data.vector.setX(data.vector.getX() * 0.6);
data.vector.setZ(data.vector.getZ() * 0.6);
data.addVectorType(VectorData.VectorType.AttackSlow);
} else {
velocities.add(data.returnNewModified(data.vectorX * 0.6, data.vectorY, data.vectorZ * 0.6, VectorData.VectorType.AttackSlow));
velocities.add(data.returnNewModified(data.vector.clone().multiply(0.6, 1, 0.6), VectorData.VectorType.AttackSlow));
}
}
@@ -454,56 +442,45 @@ public class PredictionEngine {
Set<VectorData> vectors = stupidVectors && stuckOnEdge ? new HashSet<>(velocities) : velocities;
for (VectorData vector : vectors) {
if (stupidVectors) {
if (Collisions.getHorizontalDistanceSqr(vector.vectorX, vector.vectorZ) < 9.0E-6) {
if (Collisions.getHorizontalDistanceSqr(vector.vector) < 9.0E-6) {
if (stuckOnEdge) {
VectorData edgeVector = vector.returnNewModified(vector.vectorX, vector.vectorY, vector.vectorZ, vector.vectorType);
if (Math.abs(edgeVector.vectorY) < minimumMovement) {
edgeVector.setY(0D);
VectorData edgeVector = vector.returnNewModified(vector.vector.clone(), vector.vectorType);
if (Math.abs(edgeVector.vector.getY()) < minimumMovement) {
edgeVector.vector.setY(0D);
}
velocities.add(edgeVector);
}
vector.setX(0D);
vector.setZ(0D);
vector.vector.setX(0D);
vector.vector.setZ(0D);
}
} else {
if (Math.abs(vector.vectorX) < minimumMovement) {
vector.setX(0D);
if (Math.abs(vector.vector.getX()) < minimumMovement) {
vector.vector.setX(0D);
}
if (Math.abs(vector.vectorZ) < minimumMovement) {
vector.setZ(0D);
if (Math.abs(vector.vector.getZ()) < minimumMovement) {
vector.vector.setZ(0D);
}
}
if (Math.abs(vector.vectorY) < minimumMovement) {
vector.setY(0D);
if (Math.abs(vector.vector.getY()) < minimumMovement) {
vector.vector.setY(0D);
}
}
}
public void addExplosionToPossibilities(GrimPlayer player, Set<VectorData> existingVelocities) {
if (player.likelyExplosions == null && player.firstBreadExplosion == null) return;
final VectorData[] vectorsToProcess = existingVelocities.toArray(new VectorData[0]);
for (VectorData vector : vectorsToProcess) {
for (VectorData vector : new HashSet<>(existingVelocities)) {
if (player.likelyExplosions != null) {
final double newX = player.likelyExplosions.vector.getX() + vector.vectorX;
final double newY = player.likelyExplosions.vector.getY() + vector.vectorY;
final double newZ = player.likelyExplosions.vector.getZ() + vector.vectorZ;
existingVelocities.add(new VectorData(newX, newY, newZ, vector, VectorData.VectorType.Explosion));
existingVelocities.add(new VectorData(vector.vector.clone().add(player.likelyExplosions.vector), vector, VectorData.VectorType.Explosion));
}
if (player.firstBreadExplosion != null) {
// Previously we called Vector3dm.add(player.likelyExplosions.vector) after checking (player.firstBreadExplosion != null)
// which would've thrown an NPE if player.likelyExplosions could be null here, we can ignore this
// empirically this means when firstBreadExplosion != null then likelyExplosions is also != null
final double newX = player.likelyExplosions.vector.getX() + vector.vectorX;
final double newY = player.likelyExplosions.vector.getY() + vector.vectorY;
final double newZ = player.likelyExplosions.vector.getZ() + vector.vectorZ;
VectorData intermediateData = new VectorData(newX, newY, newZ, vector, VectorData.VectorType.Explosion);
VectorData finalData = new VectorData(newX, newY, newZ, intermediateData, VectorData.VectorType.FirstBreadExplosion);
existingVelocities.add(finalData);
existingVelocities.add(new VectorData(vector.vector.clone().add(player.firstBreadExplosion.vector), vector, VectorData.VectorType.Explosion)
.returnNewModified(vector.vector.clone().add(player.firstBreadExplosion.vector), VectorData.VectorType.FirstBreadExplosion));
}
}
}
@@ -515,59 +492,68 @@ 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.vectorY >= 0)
aScore += 2;
if ((player.inVehicle() ? player.clientControlledVerticalCollision : player.onGround) && a.vector.getY() >= 0)
aScore += 4;
if ((player.inVehicle() ? player.clientControlledVerticalCollision : player.onGround) && b.vectorY >= 0)
bScore += 2;
if ((player.inVehicle() ? player.clientControlledVerticalCollision : player.onGround) && b.vector.getY() >= 0)
bScore += 4;
if (aScore != bScore)
return Integer.compare(aScore, bScore);
return Double.compare(
GrimMath.distanceSquared(player.actualMovement.getX(), player.actualMovement.getY(), player.actualMovement.getZ(), a.vectorX, a.vectorY, a.vectorZ),
GrimMath.distanceSquared(player.actualMovement.getX(), player.actualMovement.getY(), player.actualMovement.getZ(), b.vectorX, b.vectorY, b.vectorZ));
return Double.compare(a.vector.distanceSquared(player.actualMovement), b.vector.distanceSquared(player.actualMovement));
}
public Vector3dm handleStartingVelocityUncertainty(GrimPlayer player, VectorData vector, Vector3dm targetVec) {
@@ -621,81 +607,69 @@ public class PredictionEngine {
// 0.075 seems safe?
//
// Be somewhat careful as there is an antikb (for horizontal) that relies on this lenience
final double uncertaintyX = avgColliding * 0.08;
final double uncertaintyY = additionVertical;
final double uncertaintyZ = avgColliding * 0.08;
Vector3dm uncertainty = new Vector3dm(avgColliding * 0.08, additionVertical, avgColliding * 0.08);
final double minUncertaintyX = player.uncertaintyHandler.xNegativeUncertainty - additionHorizontal;
final double minUncertaintyY = player.uncertaintyHandler.yNegativeUncertainty - bonusY;
final double minUncertaintyZ = player.uncertaintyHandler.zNegativeUncertainty - additionHorizontal;
Vector3dm min = new Vector3dm(player.uncertaintyHandler.xNegativeUncertainty - additionHorizontal, -bonusY + player.uncertaintyHandler.yNegativeUncertainty, player.uncertaintyHandler.zNegativeUncertainty - additionHorizontal);
Vector3dm max = new Vector3dm(player.uncertaintyHandler.xPositiveUncertainty + additionHorizontal, bonusY + player.uncertaintyHandler.yPositiveUncertainty, player.uncertaintyHandler.zPositiveUncertainty + additionHorizontal);
final double maxUncertaintyX = player.uncertaintyHandler.xPositiveUncertainty + additionHorizontal;
final double maxUncertaintyY = player.uncertaintyHandler.yPositiveUncertainty + bonusY;
final double maxUncertaintyZ = player.uncertaintyHandler.zPositiveUncertainty + additionHorizontal;
double minVecX = vector.vectorX + (minUncertaintyX - uncertaintyX);
double minVecY = vector.vectorY + (minUncertaintyY - uncertaintyY);
double minVecZ = vector.vectorZ + (minUncertaintyZ - uncertaintyZ);
double maxVecX = vector.vectorX + (maxUncertaintyX + uncertaintyX);
double maxVecY = vector.vectorY + (maxUncertaintyY + uncertaintyY);
double maxVecZ = vector.vectorZ + (maxUncertaintyZ + uncertaintyZ);
Vector3dm minVector = vector.vector.clone().add(min.subtract(uncertainty));
Vector3dm maxVector = vector.vector.clone().add(max.add(uncertainty));
// Handle the player landing within 0.03 movement, which resets Y velocity
if (player.uncertaintyHandler.onGroundUncertain && vector.vectorY < 0) {
maxVecY = 0;
if (player.uncertaintyHandler.onGroundUncertain && vector.vector.getY() < 0) {
maxVector.setY(0);
}
// Handles stuff like missing idle packet causing gravity to be missed (plus 0.03 of course)
double gravityOffset = player.pointThreeEstimator.getAdditionalVerticalUncertainty(vector);
if (gravityOffset > 0) {
maxVecY += gravityOffset;
maxVector.setY(maxVector.getY() + gravityOffset);
} else {
minVecY += gravityOffset;
minVector.setY(minVector.getY() + gravityOffset);
}
// Handle vertical fluid pushing within 0.03
double verticalFluid = player.pointThreeEstimator.getVerticalFluidPushingUncertainty(vector);
minVecY -= verticalFluid;
minVector.setY(minVector.getY() - verticalFluid);
// Handle vertical bubble column stupidity within 0.03
double bubbleFluid = player.pointThreeEstimator.getVerticalBubbleUncertainty(vector);
maxVecY += bubbleFluid;
minVecY -= bubbleFluid;
maxVector.setY(maxVector.getY() + bubbleFluid);
minVector.setY(minVector.getY() - bubbleFluid);
// We can't simulate the player's Y velocity, unknown number of ticks with a gravity change
// Feel free to simulate all 104857600000000000000000000 possibilities!
if (!player.pointThreeEstimator.canPredictNextVerticalMovement()) {
minVecY -= player.compensatedEntities.self.getAttributeValue(Attributes.GRAVITY);
minVector.setY(minVector.getY() - player.compensatedEntities.self.getAttributeValue(Attributes.GRAVITY));
}
// Hidden slime block bounces by missing idle tick and 0.03
if (player.actualMovement.getY() >= 0 && player.uncertaintyHandler.influencedByBouncyBlock()) {
if (player.uncertaintyHandler.thisTickSlimeBlockUncertainty != 0 && !vector.isJump()) { // jumping overrides slime block
if (player.uncertaintyHandler.thisTickSlimeBlockUncertainty > maxVecY) {
maxVecY = player.uncertaintyHandler.thisTickSlimeBlockUncertainty;
if (player.uncertaintyHandler.thisTickSlimeBlockUncertainty > maxVector.getY()) {
maxVector.setY(player.uncertaintyHandler.thisTickSlimeBlockUncertainty);
}
if (minVecY > 0) minVecY = 0;
if (minVector.getY() > 0) minVector.setY(0);
}
}
if (vector.isZeroPointZeroThree() && vector.isSwimHop()) {
minVecY -= 0.06; // Fluid pushing downwards hidden by 0.03
minVector.setY(minVector.getY() - 0.06); // Fluid pushing downwards hidden by 0.03
}
SimpleCollisionBox box = new SimpleCollisionBox(minVecX, minVecY, minVecZ, maxVecX, maxVecY, maxVecZ);
SimpleCollisionBox box = new SimpleCollisionBox(minVector, maxVector);
box.sort();
// https://github.com/MWHunter/Grim/issues/398
// Thank mojang for removing the idle packet resulting in this hacky mess
double levitation = player.pointThreeEstimator.positiveLevitation(maxVecY);
double levitation = player.pointThreeEstimator.positiveLevitation(maxVector.getY());
box.combineToMinimum(box.minX, levitation, box.minZ);
levitation = player.pointThreeEstimator.positiveLevitation(minVecY);
levitation = player.pointThreeEstimator.positiveLevitation(minVector.getY());
box.combineToMinimum(box.minX, levitation, box.minZ);
levitation = player.pointThreeEstimator.negativeLevitation(maxVecY);
levitation = player.pointThreeEstimator.negativeLevitation(maxVector.getY());
box.combineToMinimum(box.minX, levitation, box.minZ);
levitation = player.pointThreeEstimator.negativeLevitation(minVecY);
levitation = player.pointThreeEstimator.negativeLevitation(minVector.getY());
box.combineToMinimum(box.minX, levitation, box.minZ);
@@ -706,12 +680,12 @@ public class PredictionEngine {
box.maxZ += sneaking.getSneakingPotentialHiddenVelocity().maxZ;
if (player.uncertaintyHandler.fireworksBox != null) {
double minXdiff = Math.min(0, player.uncertaintyHandler.fireworksBox.minX - originalVec.vectorX);
double minYdiff = Math.min(0, player.uncertaintyHandler.fireworksBox.minY - originalVec.vectorY);
double minZdiff = Math.min(0, player.uncertaintyHandler.fireworksBox.minZ - originalVec.vectorZ);
double maxXdiff = Math.max(0, player.uncertaintyHandler.fireworksBox.maxX - originalVec.vectorX);
double maxYdiff = Math.max(0, player.uncertaintyHandler.fireworksBox.maxY - originalVec.vectorY);
double maxZdiff = Math.max(0, player.uncertaintyHandler.fireworksBox.maxZ - originalVec.vectorZ);
double minXdiff = Math.min(0, player.uncertaintyHandler.fireworksBox.minX - originalVec.vector.getX());
double minYdiff = Math.min(0, player.uncertaintyHandler.fireworksBox.minY - originalVec.vector.getY());
double minZdiff = Math.min(0, player.uncertaintyHandler.fireworksBox.minZ - originalVec.vector.getZ());
double maxXdiff = Math.max(0, player.uncertaintyHandler.fireworksBox.maxX - originalVec.vector.getX());
double maxYdiff = Math.max(0, player.uncertaintyHandler.fireworksBox.maxY - originalVec.vector.getY());
double maxZdiff = Math.max(0, player.uncertaintyHandler.fireworksBox.maxZ - originalVec.vector.getZ());
box.expandMin(minXdiff, minYdiff, minZdiff);
box.expandMax(maxXdiff, maxYdiff, maxZdiff);
@@ -764,7 +738,7 @@ public class PredictionEngine {
//
// Or the player is switching in and out of controlling a vehicle, in which friction messes it up
//
if (player.uncertaintyHandler.lastVehicleSwitch.hasOccurredSince(0) || player.uncertaintyHandler.lastHardCollidingLerpingEntity.hasOccurredSince(3) || (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_13) && vector.vectorY > 0 && vector.isZeroPointZeroThree() && !Collisions.isEmpty(player, GetBoundingBox.getBoundingBoxFromPosAndSize(player, player.lastX, vector.vectorY + player.lastY + 0.6, player.lastZ, 0.6f, 1.26f)))) {
if (player.uncertaintyHandler.lastVehicleSwitch.hasOccurredSince(0) || player.uncertaintyHandler.lastHardCollidingLerpingEntity.hasOccurredSince(3) || (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_13) && vector.vector.getY() > 0 && vector.isZeroPointZeroThree() && !Collisions.isEmpty(player, GetBoundingBox.getBoundingBoxFromPosAndSize(player, player.lastX, vector.vector.getY() + player.lastY + 0.6, player.lastZ, 0.6f, 1.26f)))) {
box.expandToAbsoluteCoordinates(0, 0, 0);
}
@@ -790,26 +764,22 @@ public class PredictionEngine {
box.expand(0.001); // Ignore 1e-3 offsets as we don't know starting vel
}
minVecX = box.minX;
minVecY = box.minY;
minVecZ = box.minZ;
maxVecX = box.maxX;
maxVecY = box.maxY;
maxVecZ = box.maxZ;
minVector = box.min();
maxVector = box.max();
if (pistonX != 0) {
minVecX = Math.min(minVecX - pistonX, pistonX);
maxVecX = Math.max(maxVecX + pistonX, pistonX);
minVector.setX(Math.min(minVector.getX() - pistonX, pistonX));
maxVector.setX(Math.max(maxVector.getX() + pistonX, pistonX));
}
if (pistonY != 0) {
minVecY = Math.min(minVecY - pistonY, pistonY);
maxVecY = Math.max(maxVecY + pistonY, pistonY);
minVector.setY(Math.min(minVector.getY() - pistonY, pistonY));
maxVector.setY(Math.max(maxVector.getY() + pistonY, pistonY));
}
if (pistonZ != 0) {
minVecZ = Math.min(minVecZ - pistonZ, pistonZ);
maxVecZ = Math.max(maxVecZ + pistonZ, pistonZ);
minVector.setZ(Math.min(minVector.getZ() - pistonZ, pistonZ));
maxVector.setZ(Math.max(maxVector.getZ() + pistonZ, pistonZ));
}
return VectorUtils.cutBoxToVector(targetVec, minVecX, minVecY, minVecZ, maxVecX, maxVecY, maxVecZ);
return VectorUtils.cutBoxToVector(targetVec, minVector, maxVector);
}
public void endOfTick(GrimPlayer player, double d) {
@@ -869,20 +839,15 @@ public class PredictionEngine {
for (int applyStuckSpeed = 1; applyStuckSpeed >= 0; applyStuckSpeed--) {
if (applyStuckSpeed == 0 && player.isForceStuckSpeed()) break;
Vec2d input = transformInputsToVector(player, strafe, forward);
Vector3dm movementResultFromInput = getMovementResultFromInput(player, input.x(), 0, input.z(), speed, player.yaw);
VectorData result = new VectorData(possibleLastTickOutput.vectorX + movementResultFromInput.getX(),
possibleLastTickOutput.vectorY + movementResultFromInput.getY(),
possibleLastTickOutput.vectorZ + movementResultFromInput.getZ(),
possibleLastTickOutput, VectorData.VectorType.InputResult);
Vector3dm input = transformInputsToVector(player, new Vector3dm(strafe, 0, forward));
VectorData result = new VectorData.MoveVectorData(possibleLastTickOutput.vector.clone()
.add(getMovementResultFromInput(player, input, speed, player.yaw)),
possibleLastTickOutput, VectorData.VectorType.InputResult, forward, strafe);
result.input = input;
if (applyStuckSpeed != 0) {
result = result.returnNewModified(result.vectorX * player.stuckSpeedMultiplier.getX(),
result.vectorY * player.stuckSpeedMultiplier.getY(),
result.vectorZ * player.stuckSpeedMultiplier.getZ(),
VectorData.VectorType.StuckMultiplier);
result = result.returnNewModified(result.vector.clone().multiply(player.stuckSpeedMultiplier), VectorData.VectorType.StuckMultiplier);
}
result = result.returnNewModified(handleOnClimbable(result.vectorX, result.vectorY, result.vectorZ, player), VectorData.VectorType.Climbable);
result = result.returnNewModified(handleOnClimbable(result.vector.clone(), player), VectorData.VectorType.Climbable);
// Signal that we need to flip sneaking bounding box
if (loopUsingItem == 1)
result = result.returnNewModified(VectorData.VectorType.Flip_Use_Item);
@@ -948,23 +913,25 @@ public class PredictionEngine {
// This is just the vanilla equation, which accepts invalid inputs greater than 1
// We need it because of collision support when a player is using speed
public Vector3dm getMovementResultFromInput(GrimPlayer player, double x, double y, double z, float speed, float yaw) {
float f2InRadians = GrimMath.radians(yaw);
public Vector3dm getMovementResultFromInput(GrimPlayer player, Vector3dm inputVector, float f, float f2) {
float f2InRadians = GrimMath.radians(f2);
float f3 = player.trigHandler.sin(f2InRadians);
float f4 = player.trigHandler.cos(f2InRadians);
double xResult = x * f4 - z * f3;
double zResult = z * f4 + x * f3;
double xResult = inputVector.getX() * f4 - inputVector.getZ() * f3;
double zResult = inputVector.getZ() * f4 + inputVector.getX() * f3;
return new Vector3dm(xResult * speed, 0, zResult * speed);
return new Vector3dm(xResult * f, 0, zResult * f);
}
public Vector3dm handleOnClimbable(double x, double y, double z, GrimPlayer player) {
return new Vector3dm(x, y, z);
public Vector3dm handleOnClimbable(Vector3dm vector, GrimPlayer player) {
return vector;
}
public void doJump(GrimPlayer player, double[] vectorArr) {
if (!player.lastOnGround || player.onGround) return;
JumpPower.jumpFromGround(player, vectorArr);
public void doJump(GrimPlayer player, Vector3dm vector) {
if (!player.lastOnGround || player.onGround)
return;
JumpPower.jumpFromGround(player, vector);
}
}
@@ -3,7 +3,6 @@ package ac.grim.grimac.predictionengine.predictions;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.data.VectorData;
import ac.grim.grimac.utils.math.GrimMath;
import ac.grim.grimac.utils.math.Vec2d;
import ac.grim.grimac.utils.math.Vector3dm;
import ac.grim.grimac.utils.nmsutil.ReachUtils;
import com.github.retrooper.packetevents.protocol.attribute.Attributes;
@@ -14,13 +13,11 @@ import java.util.List;
import java.util.Set;
public class PredictionEngineElytra extends PredictionEngine {
public static void applyElytraMovementInPlace(GrimPlayer player,
double[] vectorArr,
double lookX, double lookY, double lookZ) {
public static Vector3dm getElytraMovement(GrimPlayer player, Vector3dm vector, Vector3dm lookVector) {
float pitchRadians = GrimMath.radians(player.pitch);
double horizontalSqrt = Math.sqrt(lookX * lookX + lookZ * lookZ);
double horizontalLength = GrimMath.length(vectorArr[0], 0, vectorArr[2]);
double length = GrimMath.length(lookX, lookY, lookZ);
double horizontalSqrt = Math.sqrt(lookVector.getX() * lookVector.getX() + lookVector.getZ() * lookVector.getZ());
double horizontalLength = vector.clone().setY(0).length();
double length = lookVector.length();
// Mojang changed from using their math to using regular java math in 1.18.2 elytra movement
double vertCosRotation = player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_18_2) ? Math.cos(pitchRadians) : player.trigHandler.cos(pitchRadians);
@@ -34,30 +31,27 @@ public class PredictionEngineElytra extends PredictionEngine {
recalculatedGravity = player.getClientVersion().isOlderThan(ClientVersion.V_1_20_5) ? 0.01 : Math.min(recalculatedGravity, 0.01);
}
vectorArr[1] += recalculatedGravity * (-1.0D + vertCosRotation * 0.75D);
vector.add(0.0D, recalculatedGravity * (-1.0D + vertCosRotation * 0.75D), 0.0D);
double d5;
// Handle slowing the player down when falling
if (vectorArr[1] < 0.0D && horizontalSqrt > 0.0D) {
d5 = vectorArr[1] * -0.1D * vertCosRotation;
vectorArr[0] += lookX * d5 / horizontalSqrt;
vectorArr[1] += d5;
vectorArr[2] += lookZ * d5 / horizontalSqrt;
if (vector.getY() < 0.0D && horizontalSqrt > 0.0D) {
d5 = vector.getY() * -0.1D * vertCosRotation;
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;
vectorArr[0] += -lookX * d5 / horizontalSqrt;
vectorArr[1] += d5 * 3.2D;
vectorArr[2] += -lookZ * d5 / horizontalSqrt;
vector.add(-lookVector.getX() * d5 / horizontalSqrt, d5 * 3.2D, -lookVector.getZ() * d5 / horizontalSqrt);
}
// Handle accelerating the player sideways
if (horizontalSqrt > 0) {
vectorArr[0] += (lookX / horizontalSqrt * horizontalLength - vectorArr[0]) * 0.1D;
vectorArr[2] += (lookZ / horizontalSqrt * horizontalLength - vectorArr[2]) * 0.1D;
vector.add((lookVector.getX() / horizontalSqrt * horizontalLength - vector.getX()) * 0.1D, 0.0D, (lookVector.getZ() / horizontalSqrt * horizontalLength - vector.getZ()) * 0.1D);
}
return vector;
}
// Inputs have no effect on movement
@@ -71,19 +65,11 @@ public class PredictionEngineElytra extends PredictionEngine {
for (int applyStuckSpeed = 1; applyStuckSpeed >= 0; applyStuckSpeed--) {
if (applyStuckSpeed == 0 && player.isForceStuckSpeed()) break;
for (VectorData data : possibleVectors) {
double[] vector = {data.vectorX, data.vectorY, data.vectorZ};
applyElytraMovementInPlace(player, vector, currentLook.getX(), currentLook.getY(), currentLook.getZ());
if (applyStuckSpeed != 0) {
vector[0] *= player.stuckSpeedMultiplier.getX();
vector[1] *= player.stuckSpeedMultiplier.getY();
vector[2] *= player.stuckSpeedMultiplier.getZ();
}
vector[0] *= 0.99F;
vector[1] *= 0.98F;
vector[2] *= 0.99f;
VectorData modified = data.returnNewModified(vector[0], vector[1], vector[2], VectorData.VectorType.InputResult);
modified.input = new Vec2d(0, 0);
Vector3dm elytraResult = getElytraMovement(player, data.vector.clone(), currentLook);
if (applyStuckSpeed != 0) elytraResult.multiply(player.stuckSpeedMultiplier);
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);
}
}
@@ -11,23 +11,18 @@ import java.util.Set;
public class PredictionEngineLava extends PredictionEngine {
@Override
public void addJumpsToPossibilities(GrimPlayer player, Set<VectorData> existingVelocities) {
final VectorData[] vectorsToProcess = existingVelocities.toArray(new VectorData[0]);
for (VectorData vector : vectorsToProcess) {
final double startX = vector.vectorX;
final double startY = vector.vectorY;
final double startZ = vector.vectorZ;
for (VectorData vector : new HashSet<>(existingVelocities)) {
if (player.couldSkipTick && vector.isZeroPointZeroThree()) {
double extraVelFromVertTickSkipUpwards = GrimMath.clamp(player.actualMovement.getY(), startY, startY + 0.05f);
existingVelocities.add(new VectorData(startX, extraVelFromVertTickSkipUpwards, startZ, vector, VectorData.VectorType.Jump));
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(startX, startY + 0.04f, startZ, 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) {
double[] jumpValues = {startX, startY, startZ};
super.doJump(player, jumpValues);
existingVelocities.add(new VectorData(jumpValues[0], jumpValues[1], jumpValues[2], vector, VectorData.VectorType.Jump));
Vector3dm withJump = vector.vector.clone();
super.doJump(player, withJump);
existingVelocities.add(new VectorData(withJump, vector, VectorData.VectorType.Jump));
}
}
}
@@ -19,24 +19,20 @@ import java.util.Set;
public class PredictionEngineNormal extends PredictionEngine {
public static double staticVectorEndOfTickY(GrimPlayer player, double y) {
double adjustedY = y;
public static void staticVectorEndOfTick(GrimPlayer player, Vector3dm vector) {
double adjustedY = vector.getY();
final OptionalInt levitation = player.compensatedEntities.getPotionLevelForPlayer(PotionTypes.LEVITATION);
if (levitation.isPresent()) {
adjustedY += (0.05 * (levitation.getAsInt() + 1) - y) * 0.2;
adjustedY += (0.05 * (levitation.getAsInt() + 1) - vector.getY()) * 0.2;
// Reset fall distance with levitation
player.fallDistance = 0;
} else if (player.hasGravity) {
adjustedY -= player.gravity;
}
return adjustedY * 0.98F;
}
public static void staticVectorEndOfTick(GrimPlayer player, VectorData vectorData) {
vectorData.vectorX = vectorData.vectorX * player.friction;
vectorData.vectorY = staticVectorEndOfTickY(player, vectorData.vectorY);
vectorData.vectorZ = vectorData.vectorZ * player.friction;
vector.setX(vector.getX() * player.friction);
vector.setY(adjustedY * 0.98F);
vector.setZ(vector.getZ() * player.friction);
}
@Override
@@ -45,9 +41,8 @@ public class PredictionEngineNormal extends PredictionEngine {
return;
}
final VectorData[] vectorsToProcess = existingVelocities.toArray(new VectorData[0]);
for (VectorData vector : vectorsToProcess) {
double[] jumpValues = {vector.vectorX, vector.vectorY, vector.vectorZ};
for (VectorData vector : new HashSet<>(existingVelocities)) {
Vector3dm jump = vector.vector.clone();
if (!player.isFlying) {
// Negative jump boost does not allow the player to leave the ground
@@ -59,17 +54,17 @@ public class PredictionEngineNormal extends PredictionEngine {
if (((jumpBoost.isEmpty() || jumpBoost.getAsInt() >= 0) && player.onGround) || !player.lastOnGround)
return;
JumpPower.jumpFromGround(player, jumpValues);
JumpPower.jumpFromGround(player, jump);
} else {
jumpValues[1] = player.flySpeed * 3;
jump.add(0, player.flySpeed * 3, 0);
if (!player.wasFlying) {
double[] edgeCaseJumpValues = {jumpValues[0], jumpValues[1], jumpValues[2]};
JumpPower.jumpFromGround(player, edgeCaseJumpValues);
existingVelocities.add(vector.returnNewModified(edgeCaseJumpValues[0], edgeCaseJumpValues[1], edgeCaseJumpValues[2], VectorData.VectorType.Jump));
Vector3dm edgeCaseJump = jump.clone();
JumpPower.jumpFromGround(player, edgeCaseJump);
existingVelocities.add(vector.returnNewModified(edgeCaseJump, VectorData.VectorType.Jump));
}
}
existingVelocities.add(vector.returnNewModified(jumpValues[0], jumpValues[1], jumpValues[2], VectorData.VectorType.Jump));
existingVelocities.add(vector.returnNewModified(jump, VectorData.VectorType.Jump));
}
}
@@ -91,30 +86,32 @@ public class PredictionEngineNormal extends PredictionEngine {
if (player.lastWasClimbing == 0 && (player.pointThreeEstimator.isNearClimbable() || player.isClimbing) && (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_14)
|| !Collisions.isEmpty(player, player.boundingBox.copy().expand(
player.clientVelocity.getX(), 0, player.clientVelocity.getZ()).expand(0.5, -SimpleCollisionBox.COLLISION_EPSILON, 0.5))) || walkingOnPowderSnow) {
player.lastWasClimbing = staticVectorEndOfTickY(player, 0.2);
Vector3dm ladderVelocity = player.clientVelocity.clone().setY(0.2);
staticVectorEndOfTick(player, ladderVelocity);
player.lastWasClimbing = ladderVelocity.getY();
}
for (VectorData vector : player.getPossibleVelocitiesMinusKnockback()) {
staticVectorEndOfTick(player, vector);
staticVectorEndOfTick(player, vector.vector);
}
}
@Override
public Vector3dm handleOnClimbable(double x, double y, double z, GrimPlayer player) {
public Vector3dm handleOnClimbable(Vector3dm vector, GrimPlayer player) {
if (player.isClimbing) {
// Reset fall distance when climbing
player.fallDistance = 0;
x = GrimMath.clamp(x, -0.15F, 0.15F);
z = GrimMath.clamp(z, -0.15F, 0.15F);
y = Math.max(y, -0.15F);
vector.setX(GrimMath.clamp(vector.getX(), -0.15F, 0.15F));
vector.setZ(GrimMath.clamp(vector.getZ(), -0.15F, 0.15F));
vector.setY(Math.max(vector.getY(), -0.15F));
// Yes, this uses shifting not crouching
if (y < 0.0 && !(player.compensatedWorld.getBlockType(player.lastX, player.lastY, player.lastZ) == StateTypes.SCAFFOLDING) && player.isSneaking && !player.isFlying) {
y = 0.0;
if (vector.getY() < 0.0 && !(player.compensatedWorld.getBlockType(player.lastX, player.lastY, player.lastZ) == StateTypes.SCAFFOLDING) && player.isSneaking && !player.isFlying) {
vector.setY(0.0);
}
}
return new Vector3dm(x, y, z);
return vector;
}
}
@@ -19,19 +19,12 @@ public class PredictionEngineWater extends PredictionEngine {
private double playerGravity;
private float swimmingFriction;
public static void staticVectorEndOfTick(GrimPlayer player, double[] vector, float swimmingFriction, double playerGravity, boolean isFalling) {
vector[0] *= swimmingFriction;
vector[1] *= 0.8F;
vector[2] *= swimmingFriction;
double y = FluidFallingAdjustedMovement.getAdjustedY(player, playerGravity, isFalling, vector[1]);
vector[1] = y;
}
public static void staticVectorEndOfTick(GrimPlayer player, VectorData vectorData, float swimmingFriction, double playerGravity, boolean isFalling) {
vectorData.vectorX *= swimmingFriction;
vectorData.vectorY *= 0.8F;
vectorData.vectorZ *= swimmingFriction;
vectorData.vectorY = FluidFallingAdjustedMovement.getAdjustedY(player, playerGravity, isFalling, vectorData.vectorY);
public static void staticVectorEndOfTick(GrimPlayer player, Vector3dm vector, float swimmingFriction, double playerGravity, boolean isFalling) {
vector.multiply(swimmingFriction, 0.8F, swimmingFriction);
Vector3dm fluidVector = FluidFallingAdjustedMovement.getFluidFallingAdjustedMovement(player, playerGravity, isFalling, vector);
vector.setX(fluidVector.getX());
vector.setY(fluidVector.getY());
vector.setZ(fluidVector.getZ());
}
public static Set<VectorData> transformSwimmingVectors(GrimPlayer player, Set<VectorData> base) {
@@ -52,7 +45,7 @@ public class PredictionEngineWater extends PredictionEngine {
double scalar = lookYAmount < -0.2 ? 0.085 : 0.06;
// The player can always press jump and activate this
swimmingVelocities.add(vector.returnNewModified(new Vector3dm(vector.vectorX, vector.vectorY + ((lookYAmount - vector.vectorY) * scalar), vector.vectorZ), VectorData.VectorType.SwimmingSpace));
swimmingVelocities.add(vector.returnNewModified(new Vector3dm(vector.vector.getX(), vector.vector.getY() + ((lookYAmount - vector.vector.getY()) * scalar), vector.vector.getZ()), VectorData.VectorType.SwimmingSpace));
// This scenario will occur if the player does not press jump and the other conditions are met
// Theoretically we should check this BEFORE allowing no look, but there isn't a cheat that takes advantage of this yet
@@ -61,7 +54,7 @@ public class PredictionEngineWater extends PredictionEngine {
// If you feel adventurous, re-add the following line to eliminate this unfair disadvantage
//if (d > 0.0 && player.world.getFluidLevelAt(player.lastX, player.lastY + 1.0 - 0.1, player.lastZ) == 0) {
swimmingVelocities.add(vector.returnNewModified(vector.vectorX, vector.vectorY, vector.vectorZ, VectorData.VectorType.SurfaceSwimming));
swimmingVelocities.add(vector.returnNewModified(vector.vector, VectorData.VectorType.SurfaceSwimming));
}
return swimmingVelocities;
@@ -79,21 +72,17 @@ public class PredictionEngineWater extends PredictionEngine {
@Override
public void addJumpsToPossibilities(GrimPlayer player, Set<VectorData> existingVelocities) {
for (VectorData vector : new HashSet<>(existingVelocities)) {
final double startX = vector.vectorX;
final double startY = vector.vectorY;
final double startZ = vector.vectorZ;
if (player.couldSkipTick && vector.isZeroPointZeroThree()) {
double extraVelFromVertTickSkipUpwards = GrimMath.clamp(player.actualMovement.getY(), startY, startY + 0.05f);
existingVelocities.add(new VectorData(startX, extraVelFromVertTickSkipUpwards, startZ, vector, VectorData.VectorType.Jump));
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(startX, startY + 0.04f, startZ, 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) {
double[] jumpValues = {startX, startY, startZ};
super.doJump(player, jumpValues);
existingVelocities.add(new VectorData(jumpValues[0], jumpValues[1], jumpValues[2], vector, VectorData.VectorType.Jump));
Vector3dm withJump = vector.vector.clone();
super.doJump(player, withJump);
existingVelocities.add(new VectorData(withJump, vector, VectorData.VectorType.Jump));
}
}
}
@@ -103,7 +92,7 @@ public class PredictionEngineWater extends PredictionEngine {
super.endOfTick(player, playerGravity);
for (VectorData vector : player.getPossibleVelocitiesMinusKnockback()) {
staticVectorEndOfTick(player, vector, swimmingFriction, playerGravity, isFalling);
staticVectorEndOfTick(player, vector.vector, swimmingFriction, playerGravity, isFalling);
}
}
@@ -112,7 +101,7 @@ public class PredictionEngineWater extends PredictionEngine {
// "hacky" climbing where player enters ladder within 0.03 movement (WHY DOES 0.03 EXIST???)
if (player.lastWasClimbing == 0 && player.pointThreeEstimator.isNearClimbable() && (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_14) || !Collisions.isEmpty(player, player.boundingBox.copy().expand(
player.clientVelocity.getX(), 0, player.clientVelocity.getZ()).expand(0.5, -SimpleCollisionBox.COLLISION_EPSILON, 0.5)))) {
player.lastWasClimbing = FluidFallingAdjustedMovement.getAdjustedY(player, playerGravity, isFalling, 0.2D * 0.8F);
player.lastWasClimbing = FluidFallingAdjustedMovement.getFluidFallingAdjustedMovement(player, playerGravity, isFalling, player.clientVelocity.clone().setY(0.2D * 0.8F)).getY();
}
Set<VectorData> baseVelocities = super.fetchPossibleStartTickVectors(player);
@@ -20,8 +20,8 @@ public class PredictionEngineWaterLegacy extends PredictionEngine {
// This is just the vanilla equation for legacy water movement
@Override
public Vector3dm getMovementResultFromInput(GrimPlayer player, double x, double y, double z, float f, float f2) {
float lengthSquared = (float) GrimMath.lengthSquared(x, y, z);
public Vector3dm getMovementResultFromInput(GrimPlayer player, Vector3dm inputVector, float f, float f2) {
float lengthSquared = (float) inputVector.lengthSquared();
if (lengthSquared >= 1.0E-4F) {
lengthSquared = (float) Math.sqrt(lengthSquared);
@@ -31,15 +31,13 @@ public class PredictionEngineWaterLegacy extends PredictionEngine {
}
lengthSquared = swimmingSpeed / lengthSquared;
x *= lengthSquared;
y *= lengthSquared;
z *= lengthSquared;
inputVector.multiply(lengthSquared);
float yawRadians = GrimMath.radians(player.yaw);
float sinResult = player.trigHandler.sin(yawRadians);
float cosResult = player.trigHandler.cos(yawRadians);
return new Vector3dm(x * cosResult - z * sinResult,
y, z * cosResult + x * sinResult);
return new Vector3dm(inputVector.getX() * cosResult - inputVector.getZ() * sinResult,
inputVector.getY(), inputVector.getZ() * cosResult + inputVector.getX() * sinResult);
}
return new Vector3dm();
@@ -48,12 +46,11 @@ public class PredictionEngineWaterLegacy extends PredictionEngine {
@Override
public void addJumpsToPossibilities(GrimPlayer player, Set<VectorData> existingVelocities) {
final VectorData[] vectorsToProcess = existingVelocities.toArray(new VectorData[0]);
for (VectorData vector : vectorsToProcess) {
existingVelocities.add(new VectorData(vector.vectorX, vector.vectorY + 0.04f, vector.vectorZ, vector, VectorData.VectorType.Jump));
for (VectorData vector : new HashSet<>(existingVelocities)) {
existingVelocities.add(new VectorData(vector.vector.clone().add(0, 0.04f, 0), vector, VectorData.VectorType.Jump));
if (player.skippedTickInActualMovement) {
existingVelocities.add(new VectorData(vector.vectorX,vector.vectorY + 0.02f, vector.vectorZ, vector, VectorData.VectorType.Jump));
existingVelocities.add(new VectorData(vector.vector.clone().add(0, 0.02f, 0), vector, VectorData.VectorType.Jump));
}
}
}
@@ -63,12 +60,10 @@ public class PredictionEngineWaterLegacy extends PredictionEngine {
super.endOfTick(player, playerGravity);
for (VectorData vector : player.getPossibleVelocitiesMinusKnockback()) {
vector.vectorX *= swimmingFriction;
vector.vectorY *= 0.8F;
vector.vectorZ *= swimmingFriction;
vector.vector.multiply(swimmingFriction, 0.8F, swimmingFriction);
// Gravity
vector.vectorY -= 0.02D;
vector.vector.setY(vector.vector.getY() - 0.02D);
}
}
}
@@ -7,7 +7,6 @@ import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
import ac.grim.grimac.utils.data.VectorData;
import ac.grim.grimac.utils.enums.BoatEntityStatus;
import ac.grim.grimac.utils.math.GrimMath;
import ac.grim.grimac.utils.math.Vec2d;
import ac.grim.grimac.utils.math.Vector3dm;
import ac.grim.grimac.utils.nmsutil.BlockProperties;
import ac.grim.grimac.utils.nmsutil.Collisions;
@@ -147,7 +146,7 @@ public class PredictionEngineBoat extends PredictionEngine {
for (VectorData data : possibleVectors) {
// TODO: is this correct?
data.input = new Vec2d(player.vehicleData.vehicleForward, player.vehicleData.vehicleHorizontal);
data.input = new Vector3dm(player.vehicleData.vehicleForward, 0, player.vehicleData.vehicleHorizontal);
for (int applyStuckSpeed = 1; applyStuckSpeed >= 0; applyStuckSpeed--) {
if (applyStuckSpeed == 0 && player.isForceStuckSpeed()) break;
@@ -156,41 +155,14 @@ public class PredictionEngineBoat extends PredictionEngine {
// so if a player tries to move in both directions, a packet will
// show that the player is staying, but the boat will move anyway
if (player.vehicleData.vehicleForward == 0) {
float f = 0.035F;
double x = data.vectorX + player.trigHandler.sin(GrimMath.radians(-player.yaw)) * f;
double y = data.vectorY;
double z = data.vectorZ + (double) (player.trigHandler.cos(GrimMath.radians(player.yaw)) * f);
if (applyStuckSpeed != 0) {
x *= player.stuckSpeedMultiplier.getX();
y *= player.stuckSpeedMultiplier.getY();
z *= player.stuckSpeedMultiplier.getZ();
}
vectors.add(data.returnNewModified(x, y, z, VectorData.VectorType.InputResult));
Vector3dm vector = data.vector.clone();
controlBoat(player, vector, true);
if (applyStuckSpeed != 0) vector.multiply(player.stuckSpeedMultiplier);
vectors.add(data.returnNewModified(vector, VectorData.VectorType.InputResult));
}
float f;
if (player.vehicleData.vehicleHorizontal != 0) {
f = 0.005F;
if (player.vehicleData.vehicleForward > 0.1) {
f += 0.04F;
} else if (player.vehicleData.vehicleForward < -0.01) {
f -= 0.005F;
}
} else {
f = 0.0F;
}
// TODO (Mutation) (Confirm if Correct)
// It seems possible for applyStuckSpeed = 0, and 1 to add the same vector to the set twice
// since we're mutating data.vectorX, Y, Z which is very likely incorrect but I don't know enough to say for certain
data.vectorX = data.vectorX + player.trigHandler.sin(GrimMath.radians(-player.yaw)) * f;
data.vectorZ = data.vectorZ + (double) (player.trigHandler.cos(GrimMath.radians(player.yaw)) * f);
if (applyStuckSpeed != 0) {
data.vectorX *= player.stuckSpeedMultiplier.getX();
data.vectorY *= player.stuckSpeedMultiplier.getY();
data.vectorZ *= player.stuckSpeedMultiplier.getZ();
}
controlBoat(player, data.vector, false);
if (applyStuckSpeed != 0) data.vector.multiply(player.stuckSpeedMultiplier);
vectors.add(data);
}
}
@@ -204,7 +176,7 @@ public class PredictionEngineBoat extends PredictionEngine {
addFluidPushingToStartingVectors(player, vectors);
for (VectorData data : vectors) {
floatBoat(player, data);
floatBoat(player, data.vector);
}
return vectors;
@@ -225,7 +197,7 @@ public class PredictionEngineBoat extends PredictionEngine {
return false;
}
private void floatBoat(GrimPlayer player, VectorData vector) {
private void floatBoat(GrimPlayer player, Vector3dm vector) {
double d1 = player.hasGravity ? -0.04f : 0;
double d2 = 0.0D;
float invFriction = 0.05F;
@@ -236,7 +208,7 @@ public class PredictionEngineBoat extends PredictionEngine {
player.lastY = getWaterLevelAbove(player) - 0.5625F + 0.101D;
player.boundingBox = GetBoundingBox.getCollisionBoxForPlayer(player, player.lastX, player.lastY, player.lastZ);
player.actualMovement = new Vector3dm(player.x - player.lastX, player.y - player.lastY, player.z - player.lastZ);
vector.vectorY = 0;
vector.setY(0);
player.vehicleData.lastYd = 0.0D;
player.vehicleData.status = BoatEntityStatus.IN_WATER;
@@ -257,12 +229,12 @@ public class PredictionEngineBoat extends PredictionEngine {
player.vehicleData.landFriction /= 2.0F;
}
vector.vectorX *= invFriction;
vector.vectorY += d1;
vector.vectorZ *= invFriction;
vector.setX(vector.getX() * invFriction);
vector.setY(vector.getY() + d1);
vector.setZ(vector.getZ() * invFriction);
if (d2 > 0.0D) {
double yVel = vector.vectorY;
double yVel = vector.getY();
vector.setY((yVel + d2 * 0.06153846016296973D) * 0.75D);
}
}
@@ -300,4 +272,22 @@ public class PredictionEngineBoat extends PredictionEngine {
return (float) (l + 1);
}
private void controlBoat(GrimPlayer player, Vector3dm vector, boolean intermediate) {
float f = 0.0F;
if (player.vehicleData.vehicleHorizontal != 0 && (!intermediate && player.vehicleData.vehicleForward == 0)) {
f += 0.005F;
}
//player.boatData.yRot += player.boatData.deltaRotation;
if (intermediate || player.vehicleData.vehicleForward > 0.1) {
f += 0.04F;
}
if (intermediate || player.vehicleData.vehicleForward < -0.01) {
f -= 0.005F;
}
vector.add(new Vector3dm(player.trigHandler.sin(GrimMath.radians(-player.yaw)) * f, 0, (double) (player.trigHandler.cos(GrimMath.radians(player.yaw)) * f)));
}
}
@@ -18,9 +18,9 @@ public class PredictionEngineHappyGhast extends PredictionEngineNormal {
@Override
public void endOfTick(GrimPlayer player, double delta) {
for (VectorData vector : player.getPossibleVelocitiesMinusKnockback()) {
vector.vectorX *= multiplier;
vector.vectorY *= multiplier;
vector.vectorZ *= multiplier;
vector.vector.setX(vector.vector.getX() * multiplier);
vector.vector.setY(vector.vector.getY() * multiplier);
vector.vector.setZ(vector.vector.getZ() * multiplier);
}
}
@@ -30,15 +30,15 @@ public class PredictionEngineHappyGhast extends PredictionEngineNormal {
}
@Override
public Vector3dm getMovementResultFromInput(GrimPlayer player, double x, double y, double z, float flyingSpeed, float yRot) {
public Vector3dm getMovementResultFromInput(GrimPlayer player, Vector3dm inputVector, float flyingSpeed, float yRot) {
float yRotRadians = GrimMath.radians(yRot);
float sin = player.trigHandler.sin(yRotRadians);
float cos = player.trigHandler.cos(yRotRadians);
double xResult = x * cos - z * sin;
double zResult = z * cos + x * sin;
double xResult = inputVector.getX() * cos - inputVector.getZ() * sin;
double zResult = inputVector.getZ() * cos + inputVector.getX() * sin;
return new Vector3dm(xResult * flyingSpeed, y * flyingSpeed, zResult * flyingSpeed);
return new Vector3dm(xResult * flyingSpeed, inputVector.getY() * flyingSpeed, zResult * flyingSpeed);
}
}
@@ -7,7 +7,6 @@ import ac.grim.grimac.utils.data.VectorData;
import ac.grim.grimac.utils.data.packetentity.PacketEntityCamel;
import ac.grim.grimac.utils.data.packetentity.PacketEntityHorse;
import ac.grim.grimac.utils.math.GrimMath;
import ac.grim.grimac.utils.math.Vec2d;
import ac.grim.grimac.utils.math.Vector3dm;
import ac.grim.grimac.utils.nmsutil.BlockProperties;
import ac.grim.grimac.utils.nmsutil.JumpPower;
@@ -63,9 +62,7 @@ public final class PredictionEngineRideableUtils {
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.vectorX += jumpVelocity.getX();
vectorData.vectorY += jumpVelocity.getY();
vectorData.vectorZ += jumpVelocity.getZ();
vectorData.vector.add(jumpVelocity);
}
player.vehicleData.horseJumping = true;
@@ -105,10 +102,9 @@ public final class PredictionEngineRideableUtils {
float f3 = player.trigHandler.cos(yawRadians);
for (VectorData vectorData : possibleVectors) {
vectorData.vectorY = jumpVelocity;
vectorData.vector.setY(jumpVelocity);
if (forwardInput > 0.0F) {
vectorData.vectorX += -0.4F * f2 * player.vehicleData.horseJump;
vectorData.vectorZ += 0.4F * f3 * player.vehicleData.horseJump;
vectorData.vector.add(new Vector3dm(-0.4F * f2 * player.vehicleData.horseJump, 0.0D, 0.4F * f3 * player.vehicleData.horseJump));
}
}
@@ -126,24 +122,22 @@ public final class PredictionEngineRideableUtils {
for (int applyStuckSpeed = 1; applyStuckSpeed >= 0; applyStuckSpeed--) {
if (applyStuckSpeed == 0 && player.isForceStuckSpeed()) break;
Vector3dm vector3dm = predictionEngine.getMovementResultFromInput(player, movementVector.getX(), movementVector.getY(), movementVector.getZ(), speed, player.yaw).add(possibleLastTickOutput.vectorX, possibleLastTickOutput.vectorY, possibleLastTickOutput.vectorZ);
VectorData result = new VectorData(vector3dm, possibleLastTickOutput, VectorData.VectorType.InputResult);
result.input = new Vec2d(player.vehicleData.vehicleForward, player.vehicleData.vehicleHorizontal);
Vector3dm vector = new Vector3dm(result.vectorX, result.vectorY, result.vectorZ);
VectorData result = new VectorData(possibleLastTickOutput.vector.clone().add(predictionEngine.getMovementResultFromInput(player, movementVector, speed, player.yaw)), possibleLastTickOutput, VectorData.VectorType.InputResult);
result.input = new Vector3dm(player.vehicleData.vehicleForward, 0, player.vehicleData.vehicleHorizontal);
Vector3dm vector = result.vector.clone();
if (applyStuckSpeed != 0) vector.multiply(player.stuckSpeedMultiplier);
result = result.returnNewModified(vector, VectorData.VectorType.StuckMultiplier);
result = result.returnNewModified(new PredictionEngineNormal().handleOnClimbable(result.vectorX, result.vectorY, result.vectorZ, player), VectorData.VectorType.Climbable);
result = result.returnNewModified(new PredictionEngineNormal().handleOnClimbable(result.vector.clone(), player), VectorData.VectorType.Climbable);
returnVectors.add(result);
// This is the laziest way to reduce false positives such as horse rearing
// No bypasses can ever be derived from this, so why not?
result = new VectorData(possibleLastTickOutput.vectorX, possibleLastTickOutput.vectorY, possibleLastTickOutput.vectorZ, possibleLastTickOutput, VectorData.VectorType.InputResult);
result.input = new Vec2d(player.vehicleData.vehicleForward, player.vehicleData.vehicleHorizontal);
vector = new Vector3dm(result.vectorX, result.vectorY, result.vectorZ);
result = new VectorData(possibleLastTickOutput.vector.clone(), possibleLastTickOutput, VectorData.VectorType.InputResult);
result.input = new Vector3dm(player.vehicleData.vehicleForward, 0, player.vehicleData.vehicleHorizontal);
vector = result.vector.clone();
if (applyStuckSpeed != 0) vector.multiply(player.stuckSpeedMultiplier);
result = result.returnNewModified(vector, VectorData.VectorType.StuckMultiplier);
result = result.returnNewModified(new PredictionEngineNormal().handleOnClimbable(result.vectorX, result.vectorY, result.vectorZ, player), VectorData.VectorType.Climbable);
result = result.returnNewModified(new PredictionEngineNormal().handleOnClimbable(result.vector.clone(), player), VectorData.VectorType.Climbable);
returnVectors.add(result);
}
}
@@ -30,14 +30,6 @@ public class MessageUtil {
return vec == null ? "null" : vec.x + ", " + vec.y + ", " + vec.z;
}
public @NotNull String toUnlabeledString(int x, int y, int z) {
return x + ", " + y + ", " + z;
}
public @NotNull String toUnlabeledString(double x, double y, double z) {
return x + ", " + y + ", " + z;
}
public @NotNull String toUnlabledString(@Nullable Vector3f vec) {
return vec == null ? "null" : vec.x + ", " + vec.y + ", " + vec.z;
}
@@ -14,9 +14,7 @@ import java.util.ArrayList;
import java.util.List;
public final class BlockBreak {
public final int x;
public final int y;
public final int z;
public final Vector3i position;
public final BlockFace face;
public final int faceId;
public final DiggingAction action;
@@ -26,11 +24,9 @@ public final class BlockBreak {
@Getter
private boolean cancelled;
public BlockBreak(GrimPlayer player, final int x, final int y, final int z, BlockFace face, int faceId, DiggingAction action, int sequence, WrappedBlockState block) {
public BlockBreak(GrimPlayer player, Vector3i position, BlockFace face, int faceId, DiggingAction action, int sequence, WrappedBlockState block) {
this.player = player;
this.x = x;
this.y = y;
this.z = z;
this.position = position;
this.face = face;
this.faceId = faceId;
this.action = action;
@@ -43,12 +39,12 @@ public final class BlockBreak {
}
public SimpleCollisionBox getCombinedBox() {
CollisionBox placedOn = HitboxData.getBlockHitbox(player, player.inventory.getHeldItem().getType().getPlacedType(), player.getClientVersion(), block, true, x, y, z);
CollisionBox placedOn = HitboxData.getBlockHitbox(player, player.inventory.getHeldItem().getType().getPlacedType(), player.getClientVersion(), block, true, position.x, position.y, position.z);
List<SimpleCollisionBox> boxes = new ArrayList<>();
placedOn.downCast(boxes);
SimpleCollisionBox combined = new SimpleCollisionBox(x, y, z);
SimpleCollisionBox combined = new SimpleCollisionBox(position.x, position.y, position.z);
for (SimpleCollisionBox box : boxes) {
double minX = Math.max(box.minX, combined.minX);
double minY = Math.max(box.minY, combined.minY);
@@ -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;
@@ -568,10 +568,8 @@ public class BlockPlace {
// This happens due to the lack of an idle packet on 1.9+ clients
// On 1.8 clients this should practically never happen
if (interpWidth - width > 0.05 || interpHeight - height > 0.05) {
final double x = entity.trackedServerPosition.getX();
final double y = entity.trackedServerPosition.getX();
final double z = entity.trackedServerPosition.getX();
interpBox = GetBoundingBox.getPacketEntityBoundingBox(player, x, y, z, entity);
Vector3d entityPos = entity.trackedServerPosition.getPos();
interpBox = GetBoundingBox.getPacketEntityBoundingBox(player, entityPos.getX(), entityPos.getY(), entityPos.getZ(), entity);
}
if (box.isIntersected(interpBox)) {
@@ -643,13 +641,9 @@ public class BlockPlace {
Vector3dm look = ReachUtils.getLook(player, player.yaw, player.pitch);
final double distance = player.compensatedEntities.self.getAttributeValue(Attributes.BLOCK_INTERACTION_RANGE) + 3;
final double eyeX = player.x;
final double eyeY = player.y + player.getEyeHeight();
final double eyeZ = player.z;
final double endX = eyeX + look.getX() * distance;
final double endY = eyeY + look.getY() * distance;
final double endZ = eyeZ + look.getZ() * distance;
Vector3dm intercept = ReachUtils.calculateIntercept(box, eyeX, eyeY, eyeZ, endX, endY, endZ).first();
Vector3dm eyePos = new Vector3dm(player.x, player.y + player.getEyeHeight(), player.z);
Vector3dm endReachPos = eyePos.clone().add(new Vector3dm(look.getX() * distance, look.getY() * distance, look.getZ() * distance));
Vector3dm intercept = ReachUtils.calculateIntercept(box, eyePos, endReachPos).first();
// Bring this back to relative to the block
// The player didn't even click the block... (we should force resync BEFORE we get here!)
@@ -11,12 +11,7 @@ import lombok.Setter;
@Getter
@Setter
public final class PositionUpdate {
private final double fromX;
private final double fromY;
private final double fromZ;
private final double toX;
private final double toY;
private final double toZ;
private final Vector3d from, to;
private final boolean onGround;
private final SetBackData setback;
private final TeleportData teleportData;
@@ -1,3 +1,5 @@
package ac.grim.grimac.utils.anticheat.update;
public record VehiclePositionUpdate(double fromX, double fromY, double fromZ, double toX, double toY, double toZ, float xRot, float yRot, boolean isTeleport) {}
import com.github.retrooper.packetevents.util.Vector3d;
public record VehiclePositionUpdate(Vector3d from, Vector3d to, float xRot, float yRot, boolean isTeleport) {}
@@ -7,17 +7,15 @@ import org.jetbrains.annotations.NotNull;
public record BlockModification(
WrappedBlockState oldBlockContents,
WrappedBlockState newBlockContents,
int x,
int y,
int z,
Vector3i location,
int tick,
Cause cause
) {
@Override
public @NotNull String toString() {
return String.format(
"BlockModification{x=%d, y=%d, z=%d, old=%s, new=%s, tick=%d, cause=%s}",
x, y, z, oldBlockContents, newBlockContents, tick, cause
"BlockModification{location=%s, old=%s, new=%s, tick=%d, cause=%s}",
location, oldBlockContents, newBlockContents, tick, cause
);
}
@@ -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;
@@ -56,7 +56,7 @@ public class SimpleCollisionBox implements CollisionBox {
}
public SimpleCollisionBox(Vector3i pos) {
this(pos.getX(), pos.getY(), pos.getZ());
this(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1);
}
// If you want to set a full block from a point
@@ -412,16 +412,16 @@ public class SimpleCollisionBox implements CollisionBox {
// Copied from hawk lol
// I would like to point out that this is magic to me and I have not attempted to understand this code
public Vector3dm intersectsRay(Ray ray, float minDist, float maxDist) {
Vector3dm invDir = new Vector3dm(1f / ray.getDirX(), 1f / ray.getDirY(), 1f / ray.getDirZ());
Vector3dm invDir = new Vector3dm(1f / ray.getDirection().getX(), 1f / ray.getDirection().getY(), 1f / ray.getDirection().getZ());
boolean signDirX = invDir.getX() < 0;
boolean signDirY = invDir.getY() < 0;
boolean signDirZ = invDir.getZ() < 0;
double tmin = ((signDirX ? maxX : minX) - ray.getOriginX()) * invDir.getX();
double tmax = ((signDirX ? minX : maxX) - ray.getOriginX()) * invDir.getX();
double tymin = ((signDirY ? maxY : minY) - ray.getOriginY()) * invDir.getY();
double tymax = ((signDirY ? minY : maxY) - ray.getOriginY()) * invDir.getY();
double tmin = ((signDirX ? maxX : minX) - ray.getOrigin().getX()) * invDir.getX();
double tmax = ((signDirX ? minX : maxX) - ray.getOrigin().getX()) * invDir.getX();
double tymin = ((signDirY ? maxY : minY) - ray.getOrigin().getY()) * invDir.getY();
double tymax = ((signDirY ? minY : maxY) - ray.getOrigin().getY()) * invDir.getY();
if (tmin > tymax || tymin > tmax) {
return null;
@@ -430,8 +430,8 @@ public class SimpleCollisionBox implements CollisionBox {
if (tymin > tmin) tmin = tymin;
if (tymax < tmax) tmax = tymax;
double tzmin = ((signDirZ ? maxZ : minZ) - ray.getOriginZ()) * invDir.getZ();
double tzmax = ((signDirZ ? minZ : maxZ) - ray.getOriginZ()) * invDir.getZ();
double tzmin = ((signDirZ ? maxZ : minZ) - ray.getOrigin().getZ()) * invDir.getZ();
double tzmax = ((signDirZ ? minZ : maxZ) - ray.getOrigin().getZ()) * invDir.getZ();
if ((tmin > tzmax) || (tzmin > tmax)) {
return null;
@@ -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;
}
}
@@ -39,11 +39,12 @@ public class ReachInterpolationData {
private boolean expandNonRelative = false;
private int cancelledLerpInterpolationStepsLowBound = Integer.MAX_VALUE;
public ReachInterpolationData(GrimPlayer player, SimpleCollisionBox startingLocation, final double x, final double y, final double z, PacketEntity entity) {
public ReachInterpolationData(GrimPlayer player, SimpleCollisionBox startingLocation, TrackedPosition position, PacketEntity entity) {
final boolean unreliableTicking = !player.inVehicle() && player.canSkipTicks();
this.startingLocation = startingLocation;
this.targetLocation = new SimpleCollisionBox(x, y, z, x, y, z, false);
final Vector3d pos = position.getPos();
this.targetLocation = new SimpleCollisionBox(pos.x, pos.y, pos.z, pos.x, pos.y, pos.z, false);
this.player = player;
this.entity = entity;
@@ -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);
@@ -11,34 +11,22 @@ import lombok.ToString;
public class SetBackData {
private final TeleportData teleportData;
private final float xRot, yRot;
private final double velocityX, velocityY, velocityZ;
private boolean hasVelocity;
private final Vector3dm velocity;
private final boolean vehicle;
private boolean isComplete = false;
// TODO: Rethink when we block movements for teleports, perhaps after 10 ticks or 5 blocks?
private boolean isPlugin;
private int ticksComplete = 0;
public SetBackData(TeleportData teleportData, float xRot, float yRot, double velocityX, double velocityY, double velocityZ, boolean hasVelocity, boolean vehicle, boolean isPlugin) {
public SetBackData(TeleportData teleportData, float xRot, float yRot, Vector3dm velocity, boolean vehicle, boolean isPlugin) {
this.teleportData = teleportData;
this.xRot = xRot;
this.yRot = yRot;
this.velocityX = velocityX;
this.velocityY = velocityY;
this.velocityZ = velocityZ;
this.hasVelocity = hasVelocity;
this.velocity = velocity;
this.vehicle = vehicle;
this.isPlugin = isPlugin;
}
public SetBackData(TeleportData teleportData, float xRot, float yRot, double velocityX, double velocityY, double velocityZ, boolean vehicle, boolean isPlugin) {
this(teleportData, xRot, yRot, velocityX, velocityY, velocityZ, true, vehicle, isPlugin);
}
public SetBackData(TeleportData teleportData, float xRot, float yRot, boolean vehicle, boolean isPlugin) {
this(teleportData, xRot, yRot, 0, 0, 0, false, vehicle, isPlugin);
}
public void tick() {
if (isComplete) ticksComplete++;
}
@@ -12,13 +12,9 @@ import org.jetbrains.annotations.NotNull;
@AllArgsConstructor
@Getter
public class TeleportData {
private final double locationX;
private final double locationY;
private final double locationZ;
private final double velocityX;
private final double velocityY;
private final double velocityZ;
private final @NotNull RelativeFlag flags;
private final Vector3d location;
private final Vector3d velocity;
private final RelativeFlag flags;
@Setter
private int transaction;
@Setter
@@ -40,27 +36,29 @@ public class TeleportData {
if (!isRelativeZ()) {
vector.setZ(0);
}
} else {
}
if (velocity != null && isStupidTeleportSystem) {
// WHAT WAS MOJANG THINKING MAKING TELEPORTS A REPLACEMENT PACKET FOR EXPLOSION VELOCITY /s
if (isRelativeDeltaX()) {
vector.setX(vector.getX() + velocityX);
vector.setX(vector.getX() + velocity.getX());
} else {
vector.setX(velocityX);
vector.setX(velocity.getX());
}
if (isRelativeDeltaY()) {
vector.setY(vector.getY() + velocityY);
vector.setY(vector.getY() + velocity.getY());
} else {
vector.setY(velocityY);
vector.setY(velocity.getY());
// Is this correct? Don't know don't care.
player.lastWasClimbing = 0; // Vertical movement reset
player.canSwimHop = false; // Vertical movement reset
}
if (isRelativeDeltaZ()) {
vector.setZ(vector.getZ() + velocityZ);
vector.setZ(vector.getZ() + velocity.getZ());
} else {
vector.setZ(velocityZ);
vector.setZ(velocity.getZ());
}
}
}
@@ -11,9 +11,8 @@ public final class TrackedPosition {
private static final double LEGACY_COORDINATE_SCALE = 32.0;
private final double scale;
private double x;
private double y;
private double z;
@Setter
private Vector3d pos = new Vector3d();
public TrackedPosition() {
// this.scale = player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9) ? MODERN_COORDINATE_SCALE : LEGACY_COORDINATE_SCALE;
@@ -36,38 +35,23 @@ public final class TrackedPosition {
return value / scale;
}
// Methods since 1.16.
public double withDeltaX(long x) {
if (x == 0L) return this.x;
return unpack(pack(this.x, scale) + x);
// Method since 1.16.
public Vector3d withDelta(long x, long y, long z) {
if (x == 0L && y == 0L && z == 0L) {
return this.pos;
}
double d = x == 0L ? this.pos.x : unpack(pack(this.pos.x, scale) + x);
double e = y == 0L ? this.pos.y : unpack(pack(this.pos.y, scale) + y);
double f = z == 0L ? this.pos.z : unpack(pack(this.pos.z, scale) + z);
return new Vector3d(d, e, f);
}
public double withDeltaY(long y) {
if (y == 0L) return this.y;
return unpack(pack(this.y, scale) + y);
}
public double withDeltaZ(long z) {
if (z == 0L) return this.z;
return unpack(pack(this.z, scale) + z);
}
// In 1.16-, these were different.
public double withDeltaLegacyX(double x) {
return unpackLegacy(packLegacy(this.x, scale) + x);
}
public double withDeltaLegacyY(double y) {
return unpackLegacy(packLegacy(this.y, scale) + y);
}
public double withDeltaLegacyZ(double z) {
return unpackLegacy(packLegacy(this.z, scale) + z);
}
public void setPos(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
// In 1.16-, this was different.
public Vector3d withDeltaLegacy(double x, double y, double z) {
double d = unpackLegacy(packLegacy(this.pos.x, scale) + x);
double e = unpackLegacy(packLegacy(this.pos.y, scale) + y);
double f = unpackLegacy(packLegacy(this.pos.z, scale) + z);
return new Vector3d(d, e, f);
}
}
@@ -1,7 +1,5 @@
package ac.grim.grimac.utils.data;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import ac.grim.grimac.utils.math.Vec2d;
import ac.grim.grimac.utils.math.Vector3dm;
import lombok.Getter;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@@ -9,36 +7,43 @@ 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;
public double vectorX, vectorY, vectorZ;
public @MonotonicNonNull Vec2d input;
public Vector3dm vector;
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) {
this(vector.getX(), vector.getY(), vector.getZ(), lastVector, vectorType);
}
public void setX(double x) {
this.vectorX = x;
}
public void setY(double y) {
this.vectorY = y;
}
public void setZ(double z) {
this.vectorZ = z;
}
public VectorData(final double x, final double y, final double z, VectorData lastVector, VectorType vectorType) {
this.vectorX = x;
this.vectorY = y;
this.vectorZ = z;
this.vector = vector;
this.lastVector = lastVector;
this.vectorType = vectorType;
@@ -56,46 +61,37 @@ public class VectorData {
preUncertainty = lastVector.preUncertainty;
isAttackSlow = lastVector.isAttackSlow;
input = lastVector.input;
isWithInput = lastVector.isWithInput;
}
addVectorType(vectorType);
}
public VectorData(Vector3dm vector, VectorType vectorType) {
this(vector.getX(), vector.getY(), vector.getZ(), vectorType);
}
public VectorData(final double x, final double y, final double z, VectorType vectorType) {
this.vectorX = x;
this.vectorY = y;
this.vectorZ = z;
this.vector = vector;
this.vectorType = vectorType;
addVectorType(vectorType);
}
public VectorData returnNewModified(VectorType type) {
return new VectorData(this.vectorX, this.vectorY, this.vectorZ, this, type);
return new VectorData(vector, this, type);
}
public VectorData returnNewModified(Vector3dm newVec, VectorType type) {
return new VectorData(newVec, this, type);
}
public VectorData returnNewModified(final double x, final double y, final double z, VectorType type) {
return new VectorData(x, y, z, this, type);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
VectorData that = (VectorData) o;
return isKnockback == that.isKnockback && firstBreadKb == that.firstBreadKb && isExplosion == that.isExplosion && firstBreadExplosion == that.firstBreadExplosion && isTrident == that.isTrident && isZeroPointZeroThree == that.isZeroPointZeroThree && isSwimHop == that.isSwimHop && isFlipSneaking == that.isFlipSneaking && isFlipItem == that.isFlipItem && isJump == that.isJump && isAttackSlow == that.isAttackSlow && vectorType == that.vectorType && Objects.equals(lastVector, that.lastVector) && Objects.equals(preUncertainty, that.preUncertainty) && vectorX == that.vectorX && vectorY == that.vectorY && vectorZ == that.vectorZ;
return isKnockback == that.isKnockback && firstBreadKb == that.firstBreadKb && isExplosion == that.isExplosion && firstBreadExplosion == that.firstBreadExplosion && isTrident == that.isTrident && isZeroPointZeroThree == that.isZeroPointZeroThree && isSwimHop == that.isSwimHop && isFlipSneaking == that.isFlipSneaking && isFlipItem == that.isFlipItem && isJump == that.isJump && isAttackSlow == that.isAttackSlow && vectorType == that.vectorType && Objects.equals(lastVector, that.lastVector) && Objects.equals(preUncertainty, that.preUncertainty) && Objects.equals(vector, that.vector);
}
@Override
public int hashCode() {
return Objects.hash(vectorType, lastVector, preUncertainty, vectorX, vectorY, vectorZ, isKnockback, firstBreadKb, isExplosion, firstBreadExplosion, isTrident, isZeroPointZeroThree, isSwimHop, isFlipSneaking, isFlipItem, isJump, isAttackSlow);
return Objects.hash(vectorType, lastVector, preUncertainty, vector, isKnockback, firstBreadKb, isExplosion, firstBreadExplosion, isTrident, isZeroPointZeroThree, isSwimHop, isFlipSneaking, isFlipItem, isJump, isAttackSlow);
}
public void addVectorType(VectorType type) {
@@ -111,6 +107,7 @@ public class VectorData {
case Flip_Use_Item -> isFlipItem = true;
case Jump -> isJump = true;
case AttackSlow -> isAttackSlow = true;
case WithInput -> isWithInput = true;
}
}
@@ -118,7 +115,7 @@ public class VectorData {
public String toString() {
return "VectorData{" +
"pointThree=" + isZeroPointZeroThree +
", vector=" + MessageUtil.toUnlabeledString(vectorX, vectorY, vectorZ) +
", vector=" + vector +
'}';
}
@@ -137,6 +134,7 @@ public class VectorData {
Explosion,
FirstBreadExplosion,
InputResult,
WithInput,
StuckMultiplier,
Spectator,
Dead,
@@ -20,11 +20,9 @@ public class VehicleData {
public float vehicleForward = 0f;
public boolean lastDummy = false;
public boolean wasVehicleSwitch = false;
public final ConcurrentLinkedQueue<VehicleTeleport> vehicleTeleports = new ConcurrentLinkedQueue<>();
public final ConcurrentLinkedQueue<Pair<Integer, Vector3d>> vehicleTeleports = new ConcurrentLinkedQueue<>();
public float nextHorseJump = 0;
public float horseJump = 0;
public boolean horseJumping = false;
public int camelDashCooldown;
public record VehicleTeleport(int teleportId, double x, double y, double z) {}
}
@@ -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;
@@ -73,14 +74,12 @@ public class PacketEntity extends TypedPacketEntity {
this.uuid = uuid;
initAttributes(player);
this.trackedServerPosition = new TrackedPosition();
this.trackedServerPosition.setPos(x, y, z);
this.trackedServerPosition.setPos(new Vector3d(x, y, z));
if (player.getClientVersion().isOlderThan(ClientVersion.V_1_9)) { // Thanks ViaVersion
trackedServerPosition.setPos(((int) (x * 32)) / 32d, ((int) (y * 32)) / 32d, ((int) (z * 32)) / 32d);
trackedServerPosition.setPos(new Vector3d(((int) (x * 32)) / 32d, ((int) (y * 32)) / 32d, ((int) (z * 32)) / 32d));
}
final double xPos = trackedServerPosition.getX();
final double yPos = trackedServerPosition.getY();
final double zPos = trackedServerPosition.getZ();
this.newPacketLocation = new ReachInterpolationData(player, new SimpleCollisionBox(xPos, yPos, zPos, xPos, yPos, zPos, false), xPos, yPos, zPos, this);
final Vector3d pos = trackedServerPosition.getPos();
this.newPacketLocation = new ReachInterpolationData(player, new SimpleCollisionBox(pos.x, pos.y, pos.z, pos.x, pos.y, pos.z, false), trackedServerPosition, this);
}
protected void trackAttribute(ValuedAttribute valuedAttribute) {
@@ -131,31 +130,25 @@ public class PacketEntity extends TypedPacketEntity {
if (relative) {
// This only matters for 1.9+ clients, but it won't hurt 1.8 clients either... align for imprecision
final double scale = trackedServerPosition.getScale();
final double x;
final double y;
final double z;
Vector3d vec3d;
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_16)) {
x = trackedServerPosition.withDeltaX(TrackedPosition.pack(relX, scale));
y = trackedServerPosition.withDeltaY(TrackedPosition.pack(relY, scale));
z = trackedServerPosition.withDeltaZ(TrackedPosition.pack(relZ, scale));
vec3d = trackedServerPosition.withDelta(TrackedPosition.pack(relX, scale), TrackedPosition.pack(relY, scale), TrackedPosition.pack(relZ, scale));
} else {
x = trackedServerPosition.withDeltaLegacyX(TrackedPosition.packLegacy(relX, scale));
y = trackedServerPosition.withDeltaLegacyY(TrackedPosition.packLegacy(relY, scale));
z = trackedServerPosition.withDeltaLegacyZ(TrackedPosition.packLegacy(relZ, scale));
vec3d = trackedServerPosition.withDeltaLegacy(TrackedPosition.packLegacy(relX, scale), TrackedPosition.packLegacy(relY, scale), TrackedPosition.packLegacy(relZ, scale));
}
trackedServerPosition.setPos(x, y, z);
trackedServerPosition.setPos(vec3d);
} else {
trackedServerPosition.setPos(relX, relY, relZ);
trackedServerPosition.setPos(new Vector3d(relX, relY, relZ));
// ViaVersion desync's here for teleports
// It simply teleports the entity with its position divided by 32... ignoring the offset this causes.
// Thanks a lot ViaVersion! Please don't fix this, or it will be a pain to support.
if (player.getClientVersion().isOlderThan(ClientVersion.V_1_9)) {
trackedServerPosition.setPos(((int) (relX * 32)) / 32d, ((int) (relY * 32)) / 32d, ((int) (relZ * 32)) / 32d);
trackedServerPosition.setPos(new Vector3d(((int) (relX * 32)) / 32d, ((int) (relY * 32)) / 32d, ((int) (relZ * 32)) / 32d));
}
}
}
this.oldPacketLocation = newPacketLocation;
this.newPacketLocation = new ReachInterpolationData(player, oldPacketLocation.getPossibleLocationCombined(), trackedServerPosition.getX(), trackedServerPosition.getY(), trackedServerPosition.getZ(), this);
this.newPacketLocation = new ReachInterpolationData(player, oldPacketLocation.getPossibleLocationCombined(), trackedServerPosition, this);
// TODO make config option to rewrite Rots to PosRots instead of expanding to handle this false
// https://bugs.mojang.com/browse/MC-255263
@@ -216,7 +209,7 @@ public class PacketEntity extends TypedPacketEntity {
public void setPositionRaw(GrimPlayer player, SimpleCollisionBox box) {
// I'm disappointed in you mojang. Please don't set the packet position as it desyncs it...
// But let's follow this flawed client-sided logic!
this.trackedServerPosition.setPos((box.maxX - box.minX) / 2 + box.minX, box.minY, (box.maxZ - box.minZ) / 2 + box.minZ);
this.trackedServerPosition.setPos(new Vector3d((box.maxX - box.minX) / 2 + box.minX, box.minY, (box.maxZ - box.minZ) / 2 + box.minZ));
// This disables interpolation
this.newPacketLocation = new ReachInterpolationData(player, box, this);
}
@@ -237,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;
}
}
@@ -1,8 +0,0 @@
package ac.grim.grimac.utils.function;
import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState;
@FunctionalInterface
public interface BlockStateAtPosPredicate {
boolean test(WrappedBlockState state, int x, int y, int z);
}
@@ -1,6 +0,0 @@
package ac.grim.grimac.utils.function;
@FunctionalInterface
public interface Int3Consumer {
void accept(int x, int y, int z);
}
@@ -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,9 +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.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;
@@ -54,7 +52,6 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import lombok.Getter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
@@ -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
@@ -83,16 +80,14 @@ public class CompensatedWorld {
// The owning list is so that this info can be removed when the final list is processed
private final Long2ObjectOpenHashMap<BlockPrediction> originalServerBlocks = new Long2ObjectOpenHashMap<>();
// Blocks the client changed while placing or breaking blocks
private List<Vector3i> currentlyChangedBlocks = new ArrayList<>();
private List<Vector3i> currentlyChangedBlocks = new LinkedList<>();
private final Int2ObjectMap<List<Vector3i>> serverIsCurrentlyProcessingThesePredictions = new Int2ObjectOpenHashMap<>();
private final Object2ObjectLinkedOpenHashMap<UnAckedAction, Vector3d> unackedActions = new Object2ObjectLinkedOpenHashMap<>();
private final Object2ObjectLinkedOpenHashMap<Pair<Vector3i, DiggingAction>, Vector3d> unackedActions = new Object2ObjectLinkedOpenHashMap<>();
private boolean isCurrentlyPredicting = false;
public boolean isRaining = false;
private final boolean noNegativeBlocks;
private record UnAckedAction(int x, int y, int z, DiggingAction action) {}
public CompensatedWorld(GrimPlayer player) {
this.player = player;
chunks = new Long2ObjectOpenHashMap<>(81, 0.5f);
@@ -116,15 +111,15 @@ public class CompensatedWorld {
}
}
public void handleBlockBreakAck(final int x, final int y, final int z, int blockState, DiggingAction action, boolean accepted) {
if (!accepted || action != DiggingAction.START_DIGGING || !unackedActions.containsKey(new UnAckedAction(x, y, z, action))) {
public void handleBlockBreakAck(Vector3i blockPos, int blockState, DiggingAction action, boolean accepted) {
if (!accepted || action != DiggingAction.START_DIGGING || !unackedActions.containsKey(new Pair<>(blockPos, action))) {
player.sendTransaction(); // This packet actually matters
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> {
Vector3d playerPos = unackedActions.remove(new UnAckedAction(x, y, z, action));
handleAck(x, y, z, blockState, playerPos);
Vector3d playerPos = unackedActions.remove(new Pair<>(blockPos, action));
handleAck(blockPos, blockState, playerPos);
});
} else {
unackedActions.remove(new UnAckedAction(x, y, z, action));
unackedActions.remove(new Pair<>(blockPos, action));
}
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> {
@@ -137,38 +132,34 @@ public class CompensatedWorld {
private void applyBlockChanges(List<Vector3i> toApplyBlocks) {
player.sendTransaction();
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> toApplyBlocks.forEach(vector3i -> {
final int x = vector3i.getX();
final int y = vector3i.getY();
final int z = vector3i.getZ();
long serializedPosition = GrimMath.getSerializedPosition(x, y, z);
BlockPrediction predictionData = originalServerBlocks.get(serializedPosition);
BlockPrediction predictionData = originalServerBlocks.get(vector3i.getSerializedPosition());
// We are the last to care about this prediction, remove it to stop memory leak
// Block changes are allowed to execute out of order, because it actually doesn't matter
if (predictionData != null && predictionData.getForBlockUpdate() == toApplyBlocks) {
originalServerBlocks.remove(serializedPosition);
handleAck(x, y, z, predictionData.getOriginalBlockId(), predictionData.getPlayerPosition());
originalServerBlocks.remove(vector3i.getSerializedPosition());
handleAck(vector3i, predictionData.getOriginalBlockId(), predictionData.getPlayerPosition());
}
}));
}
private void handleAck(final int x, final int y, final int z, int originalBlockId, Vector3d playerPosition) {
private void handleAck(Vector3i vector3i, int originalBlockId, Vector3d playerPosition) {
// If we need to change the world block state
if (getBlock(x, y , z).getGlobalId() != originalBlockId) {
if (getBlock(vector3i).getGlobalId() != originalBlockId) {
player.blockHistory.add(
new BlockModification(
getBlock(x, y ,z),
getBlock(vector3i),
WrappedBlockState.getByGlobalId(originalBlockId),
x, y, z,
vector3i,
GrimAPI.INSTANCE.getTickManager().currentTick,
BlockModification.Cause.HANDLE_NETTY_SYNC_TRANSACTION
)
);
updateBlock(x, y, z, originalBlockId);
updateBlock(vector3i.getX(), vector3i.getY(), vector3i.getZ(), originalBlockId);
WrappedBlockState state = WrappedBlockState.getByGlobalId(blockVersion, originalBlockId);
// The player will teleport themselves if they get stuck in the reverted block
if (playerPosition != null && CollisionData.getData(state.getType()).getMovementCollisionBox(player, player.getClientVersion(), state, x, y, z).isIntersected(player.boundingBox)) {
if (playerPosition != null && CollisionData.getData(state.getType()).getMovementCollisionBox(player, player.getClientVersion(), state, vector3i.getX(), vector3i.getY(), vector3i.getZ()).isIntersected(player.boundingBox)) {
player.lastX = player.x;
player.lastY = player.y;
player.lastZ = player.z;
@@ -183,11 +174,7 @@ public class CompensatedWorld {
public void handleBlockBreakPrediction(WrapperPlayClientPlayerDigging digging) {
// 1.14.4 intentional and correct, do not change it to 1.14
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_14_4) && player.getClientVersion().isOlderThanOrEquals(ClientVersion.V_1_18_2)) {
Vector3i blockPosition = digging.getBlockPosition();
final int x = blockPosition.getX();
final int y = blockPosition.getY();
final int z = blockPosition.getZ();
unackedActions.put(new UnAckedAction(x, y, z, digging.getAction()), new Vector3d(player.x, player.y, player.z));
unackedActions.put(new Pair<>(digging.getBlockPosition(), digging.getAction()), new Vector3d(player.x, player.y, player.z));
}
}
@@ -198,7 +185,7 @@ public class CompensatedWorld {
if (this.currentlyChangedBlocks.isEmpty()) return; // Nothing to change
List<Vector3i> toApplyBlocks = this.currentlyChangedBlocks; // We must now track the client applying the server predicted blocks
this.currentlyChangedBlocks = new ArrayList<>(); // Reset variable without changing original
this.currentlyChangedBlocks = new LinkedList<>(); // Reset variable without changing original
// We don't need to simulate any packets, it is native to the version we are on
if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_19)) {
@@ -248,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;
}
@@ -273,13 +260,12 @@ public class CompensatedWorld {
}
public void updateBlock(int x, int y, int z, int combinedID) {
long serializedPosition = GrimMath.getSerializedPosition(x, y, z);
BlockPrediction prediction = originalServerBlocks.get(serializedPosition);
Vector3i asVector = new Vector3i(x, y, z);
BlockPrediction prediction = originalServerBlocks.get(asVector.getSerializedPosition());
if (isCurrentlyPredicting) {
Vector3i asVector = new Vector3i(x , y, z);
if (prediction == null) {
originalServerBlocks.put(serializedPosition, new BlockPrediction(currentlyChangedBlocks, asVector, getBlock(x, y, z).getGlobalId(), new Vector3d(player.x, player.y, player.z))); // Remember server controlled block type
originalServerBlocks.put(asVector.getSerializedPosition(), new BlockPrediction(currentlyChangedBlocks, asVector, getBlock(asVector).getGlobalId(), new Vector3d(player.x, player.y, player.z))); // Remember server controlled block type
} else {
prediction.setForBlockUpdate(currentlyChangedBlocks); // Block existing there was placed by client, mark block to have a new prediction
}
@@ -369,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;
@@ -435,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
@@ -637,7 +623,7 @@ public class CompensatedWorld {
}
public boolean containsLiquid(SimpleCollisionBox var0) {
return Collisions.hasMaterial(player, var0, data -> Materials.isWater(player.getClientVersion(), data) || data.getType() == StateTypes.LAVA);
return Collisions.hasMaterial(player, var0, data -> Materials.isWater(player.getClientVersion(), data.first()) || data.first().getType() == StateTypes.LAVA);
}
public double getLavaFluidLevelAt(int x, int y, int z) {
@@ -659,7 +645,7 @@ public class CompensatedWorld {
}
public boolean containsLava(SimpleCollisionBox var0) {
return Collisions.hasMaterial(player, var0, data -> data.getType() == StateTypes.LAVA);
return Collisions.hasMaterial(player, var0, data -> data.first().getType() == StateTypes.LAVA);
}
public double getWaterFluidLevelAt(double x, double y, double z) {
@@ -708,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())));
}
}
}
@@ -1,7 +1,5 @@
package ac.grim.grimac.utils.math;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.github.retrooper.packetevents.util.Vector3i;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.Contract;
@@ -235,38 +233,4 @@ public class GrimMath {
public static double square(double num) {
return num * num;
}
public static double distanceSquared(double x1, double y1, double z1, double x2, double y2, double z2) {
return GrimMath.square(x1 - x2) + GrimMath.square(y1 - y2) + GrimMath.square(z1 - z2);
}
private static final ServerVersion serverVersion = PacketEvents.getAPI().getServerManager().getVersion();
public static long getSerializedPosition(int x, int y, int z) {
// 1.17 adds support for negative values
if (serverVersion.isNewerThanOrEquals(ServerVersion.V_1_17)) {
long lx = x & 0x3FFFFFF;
long ly = y & 0xFFF;
long lz = z & 0x3FFFFFF;
return lx << 38 | lz << 12 | ly;
// 1.14 method for this is storing X Z Y
} else if (serverVersion.isNewerThanOrEquals(ServerVersion.V_1_14)) {
return ((long) (x & 0x3FFFFFF) << 38) | ((long) (z & 0x3FFFFFF) << 12) | (y & 0xFFF);
}
// 1.13 and below store X Y Z
return ((long) (x & 0x3FFFFFF) << 38) | ((long) (y & 0xFFF) << 26) | (z & 0x3FFFFFF);
}
public static double lengthSquared(double x, double z) {
return GrimMath.square(x) + GrimMath.square(z);
}
public static double lengthSquared(double x, double y, double z) {
return GrimMath.square(x) + GrimMath.square(y) + GrimMath.square(z);
}
public static double length(double x, double y, double z) {
return Math.sqrt(lengthSquared(x, y, z));
}
}
@@ -1,3 +0,0 @@
package ac.grim.grimac.utils.math;
public record Vec2d(double x, double z) {}
Loaded 100 of 189 files, more files were not shown because too many files have changed in this diff. Show more