Merge latest version from upstream

This commit was merged in pull request #2.
This commit is contained in:
NotAlexNoyle committed 2026-03-25 05:45:28 -04:00
commit 252deb7fd7
182 files changed
+3116 -1674

No files matched your search

+3
View File
@@ -17,3 +17,6 @@ ij_formatter_on_tag = @formatter:on
ij_formatter_tags_enabled = false
ij_smart_tabs = false
ij_wrap_on_typing = false
[.github/ISSUE_TEMPLATE/*.yml]
indent_size = 2
+10 -10
View File
@@ -5,7 +5,7 @@ body:
- type: textarea
attributes:
label: Describe the bypass and how to replicate it
description: Provide a video, or detailed instructions to help replicate it.
description: Provide a video or detailed instructions to help replicate it.
placeholder: |
Step 1. Do this
Step 2. Then do this
@@ -17,8 +17,8 @@ body:
attributes:
label: Grim version
description: |
Provide the git commit or the version in the jar's name.
placeholder: A example would be b85c2d0 or grim version 2.3.5, etc
Provide the version of Grim running on the server. You can run `/grim version` to check.
placeholder: Examples include 2.3.73-8ddd7f1, 2.3.72, etc.
validations:
required: true
@@ -26,23 +26,23 @@ body:
attributes:
label: Server version
description: |
Run `/version` on your server to check. Provide both the server type (paper, airplane, etc) & the minecraft version.
Run `/version` on your server to check. Provide both the server type (ie Paper, Airplane, etc) & the Minecraft version.
validations:
required: true
- type: input
attributes:
label: Client version
description: |
Provide the version of your Minecraft client.
label: Client version
description: |
Provide the version of your Minecraft client. Also provide the protocol version if changed.
validations:
required: true
required: true
- type: textarea
attributes:
label: Plugins
label: Plugins/Mods
description: |
List all plugins or mods running on your server. To list plugins on bukkit, run `/plugins`.
List all plugins or mods installed on your server. To list plugins on Bukkit-based servers, run `/plugins`.
If your server is behind a proxy such as Bungeecord, you should list the proxy's plugins as well.
validations:
required: true
+14 -14
View File
@@ -6,7 +6,7 @@ body:
attributes:
label: Stack trace
description: |
Please include the entire stack trace with no attachments. If you would like, you can use a text pasting site such as https://paste.gg.
Provide the entire stack trace with no attachments. If you would like, you can use a text pasting site such as https://mclo.gs.
placeholder: Paste the stack trace or link to the stack trace here
validations:
required: true
@@ -14,7 +14,7 @@ body:
- type: textarea
attributes:
label: How to replicate it if known
description: A video, or detailed instructions to help replicate the issue would help.
description: A video or detailed instructions to help replicate the issue would help.
placeholder: |
Step 1. Do this
Step 2. Then do this
@@ -26,8 +26,8 @@ body:
attributes:
label: Grim version
description: |
Provide the git commit or the version in the jar's name.
placeholder: A example would be b85c2d0 or grim version 2.3.5, etc
Provide the version of Grim running on the server. You can run `/grim version` to check.
placeholder: Examples include 2.3.73-8ddd7f1, 2.3.72, etc.
validations:
required: true
@@ -35,27 +35,27 @@ body:
attributes:
label: Server version
description: |
Run `/version` on your server to check. Provide both the server type (paper, airplane, etc) & the minecraft version.
Run `/version` on your server to check. Provide both the server type (ie Paper, Airplane, etc) & the Minecraft version.
validations:
required: true
- type: dropdown
id: type
attributes:
label: Affected Platform(s)
description: Which platforms does this issue affect? If you aren't sure, select both.
options:
- "Bukkit"
- "Fabric"
multiple: true
label: Affected Platform(s)
description: Which platforms does this issue affect? If you aren't sure, select both.
options:
- "Bukkit"
- "Fabric"
multiple: true
validations:
required: true
required: true
- type: textarea
attributes:
label: Plugins/mods
label: Plugins/Mods
description: |
List all plugins or mods running on your server. To list plugins on bukkit, run `/plugins`.
List all plugins or mods installed on your server. To list plugins on Bukkit-based servers, run `/plugins`.
If your server is behind a proxy such as Bungeecord, you should list the proxy's plugins as well.
validations:
required: true
+7 -7
View File
@@ -20,8 +20,8 @@ body:
attributes:
label: Grim version
description: |
Provide the git commit or the version in the jar's name.
placeholder: A example would be b85c2d0 or grim version 2.3.5, etc
Provide the version of Grim running on the server. You can run `/grim version` to check.
placeholder: Examples include 2.3.73-8ddd7f1, 2.3.72, etc.
validations:
required: true
@@ -29,7 +29,7 @@ body:
attributes:
label: Server version
description: |
Run `/version` on your server to check. Provide both the server type (paper, airplane, etc) & the minecraft version.
Run `/version` on your server to check. Provide both the server type (ie Paper, Airplane, etc) & the Minecraft version.
validations:
required: true
@@ -37,16 +37,16 @@ body:
attributes:
label: Client version
description: |
Provide the version of your Minecraft client. You must be able to reproduce the false positive using the vanilla client.
Lunar Client, TLauncher etc. will result in your issue being closed.
Provide the version of your Minecraft client. You must be able to reproduce the false positive using a vanilla client.
Using Lunar Client, TLauncher, Fabric, etc, will result in your issue being closed.
validations:
required: true
- type: textarea
attributes:
label: Plugins
label: Plugins/Mods
description: |
List all plugins or mods running on your server. To list plugins on bukkit, run `/plugins`.
List all plugins or mods installed on your server. To list plugins on Bukkit-based servers, run `/plugins`.
If your server is behind a proxy such as Bungeecord, you should list the proxy's plugins as well.
validations:
required: true
+2 -2
View File
@@ -5,6 +5,6 @@ body:
- type: textarea
attributes:
label: Feature description
description: Describe the feature and why it should be added
description: Describe the feature and why it should be added.
validations:
required: true
required: true
+13 -14
View File
@@ -5,7 +5,7 @@ body:
- type: textarea
attributes:
label: Describe the bug and how to replicate it
description: A video, or detailed instructions to help replicate the issue would help.
description: A video or detailed instructions to help replicate the issue would help.
placeholder: |
Step 1. Do this
Step 2. Then do this
@@ -16,9 +16,8 @@ body:
- type: textarea
attributes:
label: Grim version
description: |
Provide the git commit or the version in the jar's name.
placeholder: A example would be b85c2d0 or grim version 2.3.5, etc
description: Provide the version of Grim running on the server. You can run `/grim version` to check.
placeholder: Examples include 2.3.73-8ddd7f1, 2.3.72, etc.
validations:
required: true
@@ -26,27 +25,27 @@ body:
attributes:
label: Server version
description: |
Run `/version` on your server to check. Provide both the server type (paper, airplane, etc) & the minecraft version.
Run `/version` on your server to check. Provide both the server type (ie Paper, Airplane, etc) & the Minecraft version.
validations:
required: true
- type: dropdown
id: type
attributes:
label: Affected Platform(s)
description: Which platforms does this issue affect? If you aren't sure, select both.
options:
- "Bukkit"
- "Fabric"
multiple: true
label: Affected Platform(s)
description: Which platforms does this issue affect? If you aren't sure, select both.
options:
- "Bukkit"
- "Fabric"
multiple: true
validations:
required: true
required: true
- type: textarea
attributes:
label: Plugins
label: Plugins/Mods
description: |
List all plugins or mods running on your server. To list plugins on bukkit, run `/plugins`.
List all plugins or mods installed on your server. To list plugins on Bukkit-based servers, run `/plugins`.
If your server is behind a proxy such as Bungeecord, you should list the proxy's plugins as well.
validations:
required: true
@@ -19,8 +19,15 @@ body:
- type: input
attributes:
label: Grim Log
description: |
Provide the generated /grim log link.
description: Provide the generated /grim log link.
validations:
required: true
- type: textarea
attributes:
label: Grim version
description: Provide the version of Grim running on the server. You can run `/grim version` to check.
placeholder: Examples include 2.3.73-8ddd7f1, 2.3.72, etc.
validations:
required: true
@@ -28,7 +35,7 @@ body:
attributes:
label: Server version
description: |
Run `/version` on your server to check. Provide both the server type (paper, airplane, etc) & the minecraft version.
Run `/version` on your server to check. Provide both the server type (ie Paper, Airplane, etc) & the Minecraft version.
validations:
required: true
@@ -36,16 +43,16 @@ body:
attributes:
label: Client version
description: |
Provide the version of your Minecraft client. You must be able to reproduce the false positive using the vanilla client.
Lunar Client, TLauncher etc. will result in your issue being closed.
Provide the version of your Minecraft client. You must be able to reproduce the false positive using a vanilla client.
Using Lunar Client, TLauncher, Fabric, etc, will result in your issue being closed.
validations:
required: true
- type: textarea
attributes:
label: Plugins
label: Plugins/Mods
description: |
List all plugins or mods running on your server. To list plugins on bukkit, run `/plugins`.
List all plugins or mods installed on your server. To list plugins on Bukkit-based servers, run `/plugins`.
If your server is behind a proxy such as Bungeecord, you should list the proxy's plugins as well.
validations:
required: true
required: true
+14 -9
View File
@@ -3,6 +3,11 @@ name: Reusable Build and Publish
on:
workflow_call:
inputs:
runner:
description: 'The runner to use for the build job.'
required: false
type: string
default: 'ubuntu-latest'
is-release-build:
description: 'True if this is a release build, which affects versioning and publishing.'
required: true
@@ -36,7 +41,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ${{ inputs.runner }}
permissions:
# Grant permission to write contents (for creating tags/refs)
contents: write
@@ -106,7 +111,7 @@ jobs:
echo "Build version: $VERSION"
- name: Publish Build Scan URL
# Only run this step if a scan was requested
# Only run this step if a scan was requested
if: ${{ inputs.run-build-scan == true }}
run: |
# Search the log file for the build scan URL and extract it
@@ -124,14 +129,14 @@ jobs:
fi
- name: Upload Bukkit Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: grimac-bukkit
path: ${{ github.workspace }}/bukkit/build/libs/grimac-bukkit-${{ env.BUILD_VERSION }}.jar
if-no-files-found: error
- name: Upload Fabric Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: grimac-fabric
path: ${{ github.workspace }}/fabric/build/libs/grimac-fabric-${{ env.BUILD_VERSION }}.jar
@@ -152,7 +157,7 @@ jobs:
- name: Prepare Final Changelog
id: changelog
# Add this 'env' block to safely load the changelogs as environment variables
# Add this 'env' block to safely load the changelogs as environment variables
env:
RELEASE_NOTES: ${{ inputs.release-changelog }}
DEV_NOTES: ${{ steps.git-cliff.outputs.content }}
@@ -192,13 +197,13 @@ jobs:
if: ${{ inputs.publish-artifacts == true && env.MODRINTH_ID != '' && env.MODRINTH_TOKEN != '' }}
uses: Kir-Antipov/mc-publish@v3.3
with:
modrinth-id: ${{ secrets.MODRINTH_ID }}
modrinth-id: ${{ secrets.MODRINTH_ID }}
modrinth-token: ${{ secrets.MODRINTH_TOKEN }}
modrinth-featured: ${{ inputs.is-release-build }} # Only feature release builds
files: bukkit/build/libs/grimac-bukkit-${{ env.BUILD_VERSION }}.jar
name: LightningGrim Anticheat (Bukkit) ${{ env.BUILD_VERSION }}-b${{ steps.buildnumber.outputs.build_number }}
version: ${{ env.BUILD_VERSION }}
version: ${{ env.BUILD_VERSION }}-b${{ steps.buildnumber.outputs.build_number }}
version-type: ${{ inputs.release-version-type }}
changelog: ${{ steps.changelog.outputs.notes }}
loaders: |
@@ -207,7 +212,7 @@ jobs:
paper
folia
purpur
game-versions: '>=1.7 <=1.21.10'
game-versions: '>=1.7 <=1.21.11'
- name: Publish Fabric to Modrinth
if: ${{ inputs.publish-artifacts == true && env.MODRINTH_ID != '' && env.MODRINTH_TOKEN != '' }}
@@ -219,7 +224,7 @@ jobs:
files: fabric/build/libs/grimac-fabric-${{ env.BUILD_VERSION }}.jar
name: LightningGrim Anticheat (Fabric) ${{ env.BUILD_VERSION }}-b${{ steps.buildnumber.outputs.build_number }}
version: ${{ env.BUILD_VERSION }}
version: ${{ env.BUILD_VERSION }}-b${{ steps.buildnumber.outputs.build_number }}
version-type: ${{ inputs.release-version-type }}
changelog: ${{ steps.changelog.outputs.notes }}
loaders: |
+3 -3
View File
@@ -14,12 +14,12 @@ name: "CodeQL"
on:
workflow_dispatch:
push:
branches: [ dev ]
branches: [dev]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
runs-on: tenki-standard-autoscale
permissions:
actions: read
contents: read
@@ -28,7 +28,7 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'java-kotlin' ]
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
+6 -2
View File
@@ -12,6 +12,11 @@ on:
description: 'Upload to Modrinth & API? (Uncheck for a dry run)'
type: boolean
default: true
runner:
description: 'The runner to use for the build job.'
required: false
type: string
default: 'tenki-standard-autoscale'
push:
branches:
- 'main'
@@ -21,8 +26,6 @@ on:
- 'release/**'
- 'lightning'
paths:
- '.github/workflows/**'
- 'buildSrc/**'
- 'bukkit/**'
- 'common/**'
- 'fabric/**'
@@ -34,6 +37,7 @@ jobs:
call-build-workflow:
uses: ./.github/workflows/build-and-publish.yml
with:
runner: ${{ github.event.inputs.runner || 'tenki-standard-autoscale' }}
is-release-build: false
release-version-type: alpha # Dev builds are always 'alpha'
run-build-scan: ${{ github.event.inputs.run-build-scan == 'true' }}
+1
View File
@@ -8,6 +8,7 @@ jobs:
call-release-workflow:
uses: ./.github/workflows/build-and-publish.yml
with:
runner: 'tenki-standard-autoscale'
is-release-build: true
release-version-type: release # Official releases are 'release'
# Use the body of the GitHub Release as the changelog
+2
View File
@@ -73,3 +73,5 @@ nb-configuration.xml
##############################
run/
fabric/run/
.ignored/
+13 -10
View File
@@ -33,23 +33,26 @@ A premium version is planned, which will offer additional subscription-based pai
- Checks for PacketOrder (patches lots of noslow, autoblocks, etc...)
- Checks for Inventory interactions (prevents using crafting slots as extra storage, moving while having inventory open, etc...)
## Requirements & Installation
- Java 17 or higher. *For more details, see [Updating-to-Java-17](https://github.com/GrimAnticheat/Grim/wiki/Updating-to-Java-17).*
- A Spigot, Paper, Folia, or Fabric server environment. *For more details, see [Supported-environments](https://github.com/GrimAnticheat/Grim/wiki/Supported-environments).*
If you use a proxy such as Velocity or Bungeecord:
- If you use Geyser, Floodgate must be installed on the backend server (where Grim is) so Grim can access the Floodgate API.
- If you use ViaVersion, it must be installed on the backend server (where Grim is) ONLY.
Grim does not support having ViaVersion installed on the proxy, even if it is also installed on the backend.
## Resources
- For documentation and examples visit the [Wiki](https://github.com/GrimAnticheat/Grim/wiki).
- For answers to commonly asked questions visit the [FAQ](https://github.com/GrimAnticheat/Grim/wiki/FAQ).
- For community support and project discussion join our [Discord](https://discord.grim.ac).
## Pull Requests
## Requirements & Installation
- Java 17 or higher. *For more details
see [Updating-to-Java-17](https://github.com/GrimAnticheat/Grim/wiki/Updating-to-Java-17).*
- A Spigot, Paper, or Folia, server environment. *For more details
see [Supported-environments](https://github.com/GrimAnticheat/Grim/wiki/Supported-environments).*
- If you use Geyser, place Floodgate on the backend server so LightningGrim can exempt bedrock players. LightningGrim
cannot access the Floodgate API if it is on the proxy.
- If you use ViaVersion, it should be on the backend server as movement is highly dependent on
client version.
See [Contributing](CONTRIBUTING.md) for more information about contributing and what our guidelines
are.
## Developer Plugin API
+1 -1
View File
@@ -1,6 +1,6 @@
plugins {
`kotlin-dsl`
kotlin("plugin.serialization") version "1.9.0" // Use the same Kotlin version as your main project
kotlin("plugin.serialization") version "2.2.20"
}
repositories {
+42
View File
@@ -0,0 +1,42 @@
import org.gradle.api.artifacts.dsl.RepositoryHandler
import org.gradle.api.artifacts.repositories.MavenArtifactRepository
import org.gradle.api.artifacts.repositories.InclusiveRepositoryContentDescriptor
import org.gradle.kotlin.dsl.maven
import versioning.BuildConfig
/**
* Registers a repository with strict content exclusivity.
*
* Logic:
* 1. If BuildConfig.mavenLocalOverride is true, checks Maven Local first.
* 2. Checks the provided remote URL second.
* 3. Enforces strict filtering so Gradle never checks this repo for other deps,
* and never checks other repos (like Central) for these deps.
*/
fun RepositoryHandler.exclusive(
url: String,
repoConfig: MavenArtifactRepository.() -> Unit = {},
filterConfig: InclusiveRepositoryContentDescriptor.() -> Unit
) {
// We define the repositories inside the function scope so they are added to the handler
val remote = maven(url, repoConfig)
exclusive(remote, filterConfig)
}
fun RepositoryHandler.exclusive(
remote: MavenArtifactRepository,
filterConfig: InclusiveRepositoryContentDescriptor.() -> Unit
) {
// We access BuildConfig here again to check for local override
val local = if (BuildConfig.mavenLocalOverride) mavenLocal() else null
exclusiveContent {
if (local != null) {
forRepositories(local, remote)
} else {
forRepositories(remote)
}
filter(filterConfig)
}
}
@@ -56,7 +56,7 @@ object BuildConfig {
_shadePE = resolveBool(project, "shadePE", altKey = "SHADE_PE", default = true)
_relocate = resolveBool(project, "relocate", altKey = "RELOCATE_JAR", default = true)
_release = resolveBool(project, "release", default = false)
_mavenLocalOverride = resolveBool(project, "mavenLocalOverride", default = false)
_mavenLocalOverride = resolveBool(project, "mavenLocalOverride", altKey = "MAVEN_LOCAL_OVERRIDE", default = false)
}
// Unified resolution logic (System > Gradle > Env)
+19 -54
View File
@@ -10,69 +10,32 @@ plugins {
}
repositories {
if (BuildConfig.mavenLocalOverride) {
mavenLocal()
// 1. Fallback for non-exclusive deps (e.g. Maven Central deps)
if (BuildConfig.mavenLocalOverride) mavenLocal()
// 2. Exclusive Repositories (One HTTP request per dep)
exclusive("https://repo.papermc.io/repository/maven-public/", { name = "papermc" }) {
includeGroup("io.papermc.paper")
includeGroup("net.md-5")
}
// For paper-api
exclusiveContent {
forRepository {
maven {
name = "papermc"
url = uri("https://repo.papermc.io/repository/maven-public/")
}
}
filter {
includeGroup("io.papermc.paper")
includeGroup("net.md-5")
}
exclusive("https://libraries.minecraft.net", { mavenContent { releasesOnly() } }) {
includeModule("com.mojang", "brigadier")
}
exclusiveContent {
forRepository {
maven("https://libraries.minecraft.net") { // Brigadier
mavenContent { releasesOnly() }
}
}
filter {
includeModule("com.mojang", "brigadier")
}
exclusive("https://repo.extendedclip.com/content/repositories/placeholderapi/") {
includeGroup("me.clip")
}
// For placeholderapi
exclusiveContent {
forRepository {
maven("https://repo.extendedclip.com/content/repositories/placeholderapi/")
}
filter {
includeGroup("me.clip")
}
exclusive("https://repo.grim.ac/snapshots") {
includeGroup("ac.grim.grimac")
includeGroup("com.github.retrooper")
}
// For GrimAPI and PacketEvents (transitive from :common, direct in :bukkit)
exclusiveContent {
forRepository {
maven("https://repo.grim.ac/snapshots")
}
filter {
includeGroup("ac.grim.grimac")
includeGroup("com.github.retrooper")
}
exclusive("https://nexus.scarsz.me/content/repositories/releases", { mavenContent { releasesOnly() } }) {
includeGroup("github.scarsz")
}
// For Configuralize (transitive from :common)
exclusiveContent {
forRepository {
maven("https://nexus.scarsz.me/content/repositories/releases") {
mavenContent { releasesOnly() }
}
}
filter {
includeGroup("github.scarsz")
}
}
// Maven Central Fallback
mavenCentral()
}
@@ -87,6 +50,7 @@ dependencies {
}
implementation(libs.cloud.paper)
implementation(libs.adventure.platform.bukkit)
implementation(libs.grim.bukkit.internal)
implementation(project(":common"))
shadow(project(":common"))
@@ -115,6 +79,7 @@ bukkit {
"floodgate",
"FastLogin",
"PlaceholderAPI",
"ZNPCsPlus",
)
permissions {
@@ -193,7 +158,7 @@ publishing.publications.create<MavenPublication>("maven") {
tasks {
runServer {
minecraftVersion("1.21.10")
minecraftVersion("1.21.11")
}
shadowJar {
@@ -1,5 +1,7 @@
package ac.grim.grimac.platform.bukkit;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.platform.api.Platform;
import ac.grim.grimac.platform.api.PlatformServer;
import ac.grim.grimac.platform.api.sender.Sender;
import io.github.retrooper.packetevents.util.SpigotReflectionUtil;
@@ -7,6 +9,7 @@ import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
public class BukkitPlatformServer implements PlatformServer {
@Override
@@ -32,6 +35,10 @@ public class BukkitPlatformServer implements PlatformServer {
@Override
public double getTPS() {
// Folia throws UnsupportedOperationException on calling getTPS(), there is no API for getting TPS on Folia
if (GrimAPI.INSTANCE.getPlatform() == Platform.FOLIA) {
return Double.NaN;
}
return SpigotReflectionUtil.getTPS();
}
}
@@ -1,20 +1,24 @@
package ac.grim.grimac.platform.bukkit;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.GrimExternalAPI;
import ac.grim.grimac.api.GrimAPIProvider;
import ac.grim.grimac.api.GrimAbstractAPI;
import ac.grim.grimac.api.event.EventBus;
import ac.grim.grimac.api.plugin.GrimPlugin;
import ac.grim.grimac.events.GrimExtensionManager;
import ac.grim.grimac.command.CloudCommandService;
import ac.grim.grimac.internal.platform.bukkit.resolver.BukkitResolverRegistrar;
import ac.grim.grimac.manager.init.Initable;
import ac.grim.grimac.manager.init.start.ExemptOnlinePlayersOnReload;
import ac.grim.grimac.manager.init.start.StartableInitable;
import ac.grim.grimac.platform.api.Platform;
import ac.grim.grimac.platform.api.PlatformLoader;
import ac.grim.grimac.platform.api.PlatformServer;
import ac.grim.grimac.platform.api.command.CommandService;
import ac.grim.grimac.platform.api.manager.ItemResetHandler;
import ac.grim.grimac.platform.api.manager.MessagePlaceHolderManager;
import ac.grim.grimac.platform.api.manager.CommandAdapter;
import ac.grim.grimac.platform.api.manager.PlatformPluginManager;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.player.PlatformPlayerFactory;
import ac.grim.grimac.platform.api.scheduler.PlatformScheduler;
import ac.grim.grimac.platform.api.sender.Sender;
@@ -28,11 +32,11 @@ import ac.grim.grimac.platform.bukkit.manager.BukkitParserDescriptorFactory;
import ac.grim.grimac.platform.bukkit.manager.BukkitPermissionRegistrationManager;
import ac.grim.grimac.platform.bukkit.manager.BukkitPlatformPluginManager;
import ac.grim.grimac.platform.bukkit.player.BukkitPlatformPlayerFactory;
import ac.grim.grimac.platform.bukkit.resolver.BukkitResolverRegistrar;
import ac.grim.grimac.platform.bukkit.scheduler.bukkit.BukkitPlatformScheduler;
import ac.grim.grimac.platform.bukkit.scheduler.folia.FoliaPlatformScheduler;
import ac.grim.grimac.platform.bukkit.sender.BukkitSenderFactory;
import ac.grim.grimac.platform.bukkit.utils.placeholder.PlaceholderAPIExpansion;
import ac.grim.grimac.utils.anticheat.LogUtil;
import ac.grim.grimac.utils.lazy.LazyHolder;
import com.github.retrooper.packetevents.PacketEventsAPI;
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
@@ -47,7 +51,6 @@ import org.incendo.cloud.brigadier.CloudBrigadierManager;
import org.incendo.cloud.bukkit.CloudBukkitCapabilities;
import org.incendo.cloud.execution.ExecutionCoordinator;
import org.incendo.cloud.paper.LegacyPaperCommandManager;
import org.incendo.cloud.setting.Configurable;
public final class GrimACBukkitLoaderPlugin extends JavaPlugin implements PlatformLoader {
@@ -57,11 +60,11 @@ public final class GrimACBukkitLoaderPlugin extends JavaPlugin implements Platfo
private final LazyHolder<PlatformScheduler> scheduler = LazyHolder.simple(this::createScheduler);
private final LazyHolder<PacketEventsAPI<?>> packetEvents = LazyHolder.simple(() -> SpigotPacketEventsBuilder.build(this));
private final LazyHolder<BukkitSenderFactory> senderFactory = LazyHolder.simple(BukkitSenderFactory::new);
private final LazyHolder<CommandManager<Sender>> commandManager = LazyHolder.simple(this::createCommandManager);
private final LazyHolder<ItemResetHandler> itemResetHandler = LazyHolder.simple(BukkitItemResetHandler::new);
private final LazyHolder<CommandService> commandService = LazyHolder.simple(this::createCommandService);
private final CloudCommandAdapter commandAdapter = new BukkitParserDescriptorFactory();
@Getter private final PlatformPlayerFactory platformPlayerFactory = new BukkitPlatformPlayerFactory();
@Getter private final CommandAdapter commandAdapter = new BukkitParserDescriptorFactory();
@Getter private final PlatformPluginManager pluginManager = new BukkitPlatformPluginManager();
@Getter private final GrimPlugin plugin;
@Getter private final PlatformServer platformServer = new BukkitPlatformServer();
@@ -69,9 +72,8 @@ public final class GrimACBukkitLoaderPlugin extends JavaPlugin implements Platfo
@Getter private final BukkitPermissionRegistrationManager permissionManager = new BukkitPermissionRegistrationManager();
public GrimACBukkitLoaderPlugin() {
GrimExtensionManager extensionManager = GrimAPI.INSTANCE.getExtensionManager();
BukkitResolverRegistrar registrar = new BukkitResolverRegistrar(extensionManager);
registrar.registerAll();
BukkitResolverRegistrar registrar = new BukkitResolverRegistrar();
registrar.registerAll(GrimAPI.INSTANCE.getExtensionManager());
this.plugin = registrar.resolvePlugin(this);
}
@@ -116,13 +118,13 @@ public final class GrimACBukkitLoaderPlugin extends JavaPlugin implements Platfo
}
@Override
public CommandManager<Sender> getCommandManager() {
return commandManager.get();
public ItemResetHandler getItemResetHandler() {
return itemResetHandler.get();
}
@Override
public ItemResetHandler getItemResetHandler() {
return itemResetHandler.get();
public CommandService getCommandService() {
return commandService.get();
}
@Override
@@ -132,25 +134,111 @@ public final class GrimACBukkitLoaderPlugin extends JavaPlugin implements Platfo
@Override
public void registerAPIService() {
GrimAPIProvider.init(GrimAPI.INSTANCE.getExternalAPI());
Bukkit.getServicesManager().register(GrimAbstractAPI.class, GrimAPI.INSTANCE.getExternalAPI(), GrimACBukkitLoaderPlugin.LOADER, ServicePriority.Normal);
final GrimExternalAPI externalAPI = GrimAPI.INSTANCE.getExternalAPI();
final EventBus eventBus = externalAPI.getEventBus();
final ac.grim.grimac.api.plugin.GrimPlugin context = GrimAPI.INSTANCE.getGrimPlugin();
eventBus.subscribe(context, ac.grim.grimac.api.event.events.GrimJoinEvent.class, (event) -> {
ac.grim.grimac.api.events.GrimJoinEvent bukkitEvent =
new ac.grim.grimac.api.events.GrimJoinEvent(event.getUser());
Bukkit.getPluginManager().callEvent(bukkitEvent);
});
eventBus.subscribe(context, ac.grim.grimac.api.event.events.GrimQuitEvent.class, (event) -> {
ac.grim.grimac.api.events.GrimQuitEvent bukkitEvent =
new ac.grim.grimac.api.events.GrimQuitEvent(event.getUser());
Bukkit.getPluginManager().callEvent(bukkitEvent);
});
eventBus.subscribe(context, ac.grim.grimac.api.event.events.GrimReloadEvent.class, (event) -> {
ac.grim.grimac.api.events.GrimReloadEvent bukkitEvent =
new ac.grim.grimac.api.events.GrimReloadEvent(event.isSuccess());
Bukkit.getPluginManager().callEvent(bukkitEvent);
});
eventBus.subscribe(context, ac.grim.grimac.api.event.events.FlagEvent.class, (event) -> {
ac.grim.grimac.api.events.FlagEvent bukkitEvent =
new ac.grim.grimac.api.events.FlagEvent(
event.getUser(),
event.getCheck(),
event.getVerbose()
);
Bukkit.getPluginManager().callEvent(bukkitEvent);
if (bukkitEvent.isCancelled()) {
event.setCancelled(true);
}
});
eventBus.subscribe(context, ac.grim.grimac.api.event.events.CommandExecuteEvent.class, (event) -> {
ac.grim.grimac.api.events.CommandExecuteEvent bukkitEvent =
new ac.grim.grimac.api.events.CommandExecuteEvent(
event.getUser(),
event.getCheck(),
event.getVerbose(),
event.getCommand()
);
Bukkit.getPluginManager().callEvent(bukkitEvent);
if (bukkitEvent.isCancelled()) {
event.setCancelled(true);
}
});
eventBus.subscribe(context, ac.grim.grimac.api.event.events.CompletePredictionEvent.class, (event) -> {
// Note: New event doesn't have verbose, passing null or check name is standard fallback
ac.grim.grimac.api.events.CompletePredictionEvent bukkitEvent =
new ac.grim.grimac.api.events.CompletePredictionEvent(
event.getUser(),
event.getCheck(),
"",
event.getOffset()
);
Bukkit.getPluginManager().callEvent(bukkitEvent);
if (bukkitEvent.isCancelled()) {
event.setCancelled(true);
}
});
GrimAPIProvider.init(externalAPI);
Bukkit.getServicesManager().register(GrimAbstractAPI.class, externalAPI, GrimACBukkitLoaderPlugin.LOADER, ServicePriority.Normal);
}
private PlatformScheduler createScheduler() {
return GrimAPI.INSTANCE.getPlatform() == Platform.FOLIA ? new FoliaPlatformScheduler() : new BukkitPlatformScheduler();
}
private CommandManager<Sender> createCommandManager() {
private CommandService createCommandService() {
try {
return new CloudCommandService(this::createCloudCommandManager, commandAdapter);
} catch (Throwable t) {
LogUtil.warn("CRITICAL: Failed to initialize Command Framework. " +
"Grim will continue to run with no commands.", t);
return () -> {};
}
}
private CommandManager<Sender> createCloudCommandManager() {
LegacyPaperCommandManager<Sender> manager = new LegacyPaperCommandManager<>(
this,
ExecutionCoordinator.simpleCoordinator(),
senderFactory.get()
);
if (manager.hasCapability(CloudBukkitCapabilities.NATIVE_BRIGADIER)) {
manager.registerBrigadier();
CloudBrigadierManager<Sender, ?> cbm = manager.brigadierManager();
Configurable<BrigadierSetting> settings = cbm.settings();
settings.set(BrigadierSetting.FORCE_EXECUTABLE, true);
try {
manager.registerBrigadier();
CloudBrigadierManager<Sender, ?> cbm = manager.brigadierManager();
cbm.settings().set(BrigadierSetting.FORCE_EXECUTABLE, true);
} catch (Throwable t) {
LogUtil.error("Failed to register Brigadier native completions. Falling back to standard completions.", t);
}
} else if (manager.hasCapability(CloudBukkitCapabilities.ASYNCHRONOUS_COMPLETION)) {
manager.registerAsynchronousCompletions();
}
@@ -8,6 +8,7 @@ import ac.grim.grimac.platform.bukkit.utils.reflection.PaperUtils;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.LogUtil;
import ac.grim.grimac.utils.lists.HookedListWrapper;
import ac.grim.grimac.utils.reflection.ReflectionUtils;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.github.retrooper.packetevents.util.reflection.Reflection;
@@ -25,6 +26,20 @@ import java.util.List;
@SuppressWarnings(value = {"unchecked", "deprecated"})
public class BukkitTickEndEvent extends AbstractTickEndEvent implements Listener {
private Boolean getLateBindState() {
Class<?> spigotConfig = ReflectionUtils.getClass("org.spigotmc.SpigotConfig");
// ReflectionUtils.getField(class, name) handles the loop and setAccessible
Field field = ReflectionUtils.getField(spigotConfig, "lateBind");
if (field == null) return null;
try {
return (boolean) field.get(null);
} catch (Exception ignored) {
return null;
}
}
@Override
public void start() {
if (!super.shouldInjectEndTick()) {
@@ -38,9 +53,18 @@ public class BukkitTickEndEvent extends AbstractTickEndEvent implements Listener
PaperUtils.registerTickEndEvent(this, this::tickAllFoliaPlayers);
return;
}
// if it fails to inject, try to use paper's event
if (!injectWithReflection() && !PaperUtils.registerTickEndEvent(this, this::tickAllPlayers)) {
// if it fails to register Paper event, try to inject via reflection
if (!PaperUtils.registerTickEndEvent(this, this::tickAllPlayers) && !injectWithReflection()) {
LogUtil.error("Failed to inject into the end of tick event!");
if (PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_14_4)) {
Boolean lateBind = getLateBindState();
if (lateBind == null) {
LogUtil.error("Failed to determine the late-bind state. Perhaps you are using a custom server fork? Check the fork configuration for a late-bind option and disable it.");
} else if (lateBind) {
LogUtil.error("Injection failed because the late-bind option is enabled. Disable it in spigot.yml.");
}
}
}
}
@@ -65,6 +89,7 @@ public class BukkitTickEndEvent extends AbstractTickEndEvent implements Listener
// Inject so we can add the final transaction pre-flush event
try {
Object connection = SpigotReflectionUtil.getMinecraftServerConnectionInstance();
if (connection == null) return false;
Field connectionsList = Reflection.getField(connection.getClass(), List.class, 1);
List<Object> endOfTickObject = (List<Object>) connectionsList.get(connection);
@@ -1,7 +1,7 @@
package ac.grim.grimac.platform.bukkit.manager;
import ac.grim.grimac.platform.api.command.PlayerSelector;
import ac.grim.grimac.platform.api.manager.CommandAdapter;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.platform.bukkit.command.BukkitPlayerSelectorParser;
import org.bukkit.Bukkit;
@@ -16,7 +16,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class BukkitParserDescriptorFactory implements CommandAdapter {
public class BukkitParserDescriptorFactory implements CloudCommandAdapter {
// Parser is kept as a field because its stateless
private final BukkitPlayerSelectorParser<Sender> bukkitPlayerSelectorParser = new BukkitPlayerSelectorParser<>();
@@ -8,6 +8,7 @@ import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Collection;
import java.util.UUID;
@@ -54,4 +55,14 @@ public class BukkitPlatformPlayerFactory extends AbstractPlatformPlayerFactory<P
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(name);
return new BukkitOfflinePlatformPlayer(offlinePlayer);
}
@Override
public Collection<OfflinePlatformPlayer> getOfflinePlayers() {
OfflinePlayer[] offlinePlayers = Bukkit.getOfflinePlayers();
BukkitOfflinePlatformPlayer[] offlinePlatformPlayers = new BukkitOfflinePlatformPlayer[offlinePlayers.length];
for (int i = 0; i < offlinePlayers.length; i++) {
offlinePlatformPlayers[i] = new BukkitOfflinePlatformPlayer(offlinePlayers[i]);
}
return Arrays.asList(offlinePlatformPlayers);
}
}
@@ -1,111 +0,0 @@
package ac.grim.grimac.platform.bukkit.resolver;
import ac.grim.grimac.api.plugin.BasicGrimPlugin;
import ac.grim.grimac.api.plugin.GrimPlugin;
import ac.grim.grimac.events.GrimExtensionManager;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Manages the registration of Bukkit-specific resolvers with the GrimExtensionManager.
* This class is designed to be instantiated once during plugin startup.
*/
public final class BukkitResolverRegistrar {
private final GrimExtensionManager extensionManager;
// Cache to ensure we only create one GrimPlugin wrapper per Bukkit Plugin instance.
private final Map<Plugin, GrimPlugin> pluginCache = new ConcurrentHashMap<>();
public BukkitResolverRegistrar(GrimExtensionManager extensionManager) {
this.extensionManager = extensionManager;
}
/**
* Registers all the Bukkit-specific resolvers in order of performance (fastest to slowest).
*/
public void registerAll() {
extensionManager.setFailureHandler(this::createFailureException);
extensionManager.registerResolver(this::resolvePluginInstance);
extensionManager.registerResolver(this::resolveStringName);
extensionManager.registerResolver(this::resolveClass);
}
/**
* The core logic for converting a Bukkit Plugin into a GrimPlugin, with caching.
*/
public GrimPlugin resolvePlugin(Plugin bukkitPlugin) {
return pluginCache.computeIfAbsent(bukkitPlugin, plugin -> {
PluginDescriptionFile desc = plugin.getDescription();
return new BasicGrimPlugin(
plugin.getLogger(),
plugin.getDataFolder(),
desc.getVersion(),
desc.getDescription(),
desc.getAuthors()
);
});
}
/**
* 1. Resolver for raw Plugin instances (the most common case).
*/
private GrimPlugin resolvePluginInstance(Object context) {
if (context instanceof Plugin bukkitPlugin) {
return resolvePlugin(bukkitPlugin);
}
return null;
}
/**
* 2. Resolver for String plugin names.
*/
private GrimPlugin resolveStringName(Object context) {
if (context instanceof String pluginName) {
// Use the Bukkit API to look up the plugin by name. This returns null if not found.
Plugin bukkitPlugin = Bukkit.getPluginManager().getPlugin(pluginName);
// We must check for null before applying the logic to avoid a NullPointerException.
return (bukkitPlugin != null) ? resolvePlugin(bukkitPlugin) : null;
}
return null;
}
/**
* 3. Resolver for Class objects.
*/
private GrimPlugin resolveClass(Object context) {
if (context instanceof Class) {
try {
// First, resolve the Class to a Plugin instance.
JavaPlugin providingPlugin = JavaPlugin.getProvidingPlugin((Class<?>) context);
// Then, use our shared logic directly to get the GrimPlugin wrapper.
return resolvePlugin(providingPlugin);
} catch (IllegalArgumentException | IllegalStateException e) {
// This is an expected failure if the class is not from a plugin
// or is called from a static initializer. We simply indicate that
// this resolver cannot handle this context.
return null;
}
}
// This resolver only handles Classes.
return null;
}
private RuntimeException createFailureException(Object failedContext) {
String message = """
Failed to resolve GrimPlugin context from the provided object of type '%s'.
Please ensure you are passing one of the following:
- The main instance of your plugin (e.g., 'this' from your class extending JavaPlugin).
- The plugin name as a String (e.g., "MyPluginName").
- Any Class from your plugin's JAR file (e.g., MyListener.class).
- A pre-existing GrimPlugin instance.
""".formatted(failedContext.getClass().getName());
return new IllegalArgumentException(message);
}
}
+25 -51
View File
@@ -7,70 +7,42 @@ plugins {
}
repositories {
if (BuildConfig.mavenLocalOverride) {
mavenLocal()
// We still call mavenLocal() conditionally at the top for non-exclusive deps (general fallback)
if (BuildConfig.mavenLocalOverride) mavenLocal()
// Grim API & PacketEvents
exclusive("https://repo.grim.ac/snapshots") {
includeGroup("ac.grim.grimac")
includeGroup("com.github.retrooper")
}
exclusiveContent {
forRepository {
maven("https://repo.grim.ac/snapshots") // Grim API
}
filter {
includeGroup("ac.grim.grimac")
includeGroup("com.github.retrooper")
}
// ViaVersion
exclusive("https://repo.viaversion.com", { mavenContent { releasesOnly() } }) {
includeGroup("com.viaversion")
}
exclusiveContent {
forRepository {
maven("https://repo.viaversion.com") { // ViaVersion
mavenContent { releasesOnly() }
}
}
filter {
includeGroup("com.viaversion")
}
// Configuralize
exclusive("https://nexus.scarsz.me/content/repositories/releases", { mavenContent { releasesOnly() } }) {
includeGroup("github.scarsz")
}
exclusiveContent {
forRepository {
maven("https://nexus.scarsz.me/content/repositories/releases") { // Configuralize
mavenContent { releasesOnly() }
}
}
filter {
includeGroup("github.scarsz")
}
// Cumulus
exclusive("https://repo.opencollab.dev/maven-releases/", { mavenContent { releasesOnly() } }) {
includeGroup("org.geysermc.api")
}
exclusiveContent {
forRepository {
maven("https://repo.opencollab.dev/maven-releases/") { // Cumulus (for Floodgate)
mavenContent { releasesOnly() }
}
}
filter {
includeGroup("org.geysermc.api")
}
}
exclusiveContent {
forRepository {
maven("https://repo.opencollab.dev/maven-snapshots/") { // Floodgate
mavenContent { snapshotsOnly() }
}
}
filter {
includeGroup("org.geysermc.floodgate")
includeGroup("org.geysermc.cumulus")
includeModule("org.geysermc", "common")
includeModule("org.geysermc", "geyser-parent")
}
// Floodgate
exclusive("https://repo.opencollab.dev/maven-snapshots/", { mavenContent { snapshotsOnly() } }) {
includeGroup("org.geysermc.floodgate")
includeGroup("org.geysermc.cumulus")
includeModule("org.geysermc", "common")
includeModule("org.geysermc", "geyser-parent")
}
mavenCentral()
}
dependencies {
if (BuildConfig.shadePE) {
api(libs.packetevents.api)
@@ -93,6 +65,8 @@ dependencies {
api(libs.hikaricp)
api(libs.grim.api)
api(libs.grim.internal)
compileOnly(libs.grim.internal.shims)
compileOnly(libs.geyser.base.api) {
isTransitive = false // messes with guava otherwise
@@ -2,8 +2,8 @@ package ac.grim.grimac;
import ac.grim.grimac.api.event.EventBus;
import ac.grim.grimac.api.plugin.GrimPlugin;
import ac.grim.grimac.events.GrimExtensionManager;
import ac.grim.grimac.events.OptimizedEventBus;
import ac.grim.grimac.internal.plugin.resolver.GrimExtensionManager;
import ac.grim.grimac.internal.event.OptimizedEventBus;
import ac.grim.grimac.manager.AlertManagerImpl;
import ac.grim.grimac.manager.DiscordManager;
import ac.grim.grimac.manager.InitManager;
@@ -15,20 +15,18 @@ import ac.grim.grimac.manager.violationdatabase.ViolationDatabaseManager;
import ac.grim.grimac.platform.api.Platform;
import ac.grim.grimac.platform.api.PlatformLoader;
import ac.grim.grimac.platform.api.PlatformServer;
import ac.grim.grimac.platform.api.command.CommandService;
import ac.grim.grimac.platform.api.manager.ItemResetHandler;
import ac.grim.grimac.platform.api.manager.MessagePlaceHolderManager;
import ac.grim.grimac.platform.api.manager.CommandAdapter;
import ac.grim.grimac.platform.api.manager.PermissionRegistrationManager;
import ac.grim.grimac.platform.api.manager.PlatformPluginManager;
import ac.grim.grimac.platform.api.player.PlatformPlayerFactory;
import ac.grim.grimac.platform.api.scheduler.PlatformScheduler;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.platform.api.sender.SenderFactory;
import ac.grim.grimac.utils.anticheat.PlayerDataManager;
import ac.grim.grimac.utils.common.arguments.CommonGrimArguments;
import ac.grim.grimac.utils.reflection.ReflectionUtils;
import lombok.Getter;
import org.incendo.cloud.CommandManager;
import org.jetbrains.annotations.NotNull;
@Getter
@@ -77,7 +75,7 @@ public final class GrimAPI {
public void load(PlatformLoader platformLoader, Initable... platformSpecificInitables) {
this.loader = platformLoader;
this.violationDatabaseManager = new ViolationDatabaseManager(getGrimPlugin());
this.initManager = new InitManager(loader.getPacketEvents(), loader::getCommandManager, platformSpecificInitables);
this.initManager = new InitManager(loader.getPacketEvents(), platformSpecificInitables);
this.initManager.load();
this.initialized = true;
}
@@ -100,10 +98,6 @@ public final class GrimAPI {
return loader.getPlatformPlayerFactory();
}
public CommandAdapter getCommandAdapter() {
return loader.getCommandAdapter();
}
public GrimPlugin getGrimPlugin() {
return loader.getPlugin();
}
@@ -128,8 +122,8 @@ public final class GrimAPI {
return loader.getMessagePlaceHolderManager();
}
public CommandManager<Sender> getCommandManager() {
return loader.getCommandManager();
public CommandService getCommandService() {
return loader.getCommandService();
}
private void checkInitialized() {
@@ -6,6 +6,7 @@ import ac.grim.grimac.api.alerts.AlertManager;
import ac.grim.grimac.api.config.ConfigManager;
import ac.grim.grimac.api.event.EventBus;
import ac.grim.grimac.api.event.events.GrimReloadEvent;
import ac.grim.grimac.api.plugin.GrimPlugin;
import ac.grim.grimac.manager.config.ConfigManagerFileImpl;
import ac.grim.grimac.manager.init.start.StartableInitable;
import ac.grim.grimac.player.GrimPlayer;
@@ -13,6 +14,7 @@ import ac.grim.grimac.utils.anticheat.LogUtil;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import ac.grim.grimac.utils.common.ConfigReloadObserver;
import lombok.Getter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -45,6 +47,11 @@ public class GrimExternalAPI implements GrimAbstractAPI, ConfigReloadObserver, S
return api.getEventBus();
}
@Override
public @Nullable GrimUser getGrimUser(Player player) {
return getGrimUser(player.getUniqueId());
}
@Override
public @Nullable GrimUser getGrimUser(UUID uuid) {
return api.getPlayerDataManager().getPlayer(uuid);
@@ -107,6 +114,11 @@ public class GrimExternalAPI implements GrimAbstractAPI, ConfigReloadObserver, S
return GrimAPI.INSTANCE.getTickManager().currentTick;
}
@Override
public @NotNull GrimPlugin getGrimPlugin(@NotNull Object o) {
return this.api.getExtensionManager().getPlugin(o);
}
// on load, load the config & register the service
public void load() {
reload(configManagerFile);
@@ -60,7 +60,11 @@ public class Check extends GrimProcessor implements AbstractCheck {
}
public boolean shouldModifyPackets() {
return isEnabled && !player.disableGrim && !player.noModifyPacketPermission && !exemptPermission;
return isEnabled
&& !player.disableGrim
&& !player.noModifyPacketPermission
&& !noModifyPacketPermission
&& !exemptPermission;
}
public final void updatePermissions() {
@@ -6,7 +6,6 @@ import ac.grim.grimac.checks.type.PacketCheck;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
@CheckData(name = "BadPacketsC", description = "Interacted with self")
@@ -17,15 +16,13 @@ public class BadPacketsC extends Check implements PacketCheck {
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) {
if (player.gamemode == GameMode.SPECTATOR) return;
if (new WrapperPlayClientInteractEntity(event).getEntityId() == player.entityID) {
// Instant ban
if (flagAndAlert() && shouldModifyPackets()) {
event.setCancelled(true);
player.onPacketCancel();
}
}
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY
&& player.cameraEntity.isSelf()
&& new WrapperPlayClientInteractEntity(event).getEntityId() == player.entityID
&& flagAndAlert() && shouldModifyPackets() // Instant ban
) {
event.setCancelled(true);
player.onPacketCancel();
}
}
}
@@ -10,7 +10,6 @@ import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientUseItem;
import java.util.ArrayList;
@@ -26,7 +25,7 @@ public class BadPacketsJ extends Check implements PacketCheck {
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (player.gamemode == GameMode.SPECTATOR) {
if (!player.cameraEntity.isSelf()) {
rotations.clear();
return;
}
@@ -6,7 +6,6 @@ import ac.grim.grimac.checks.type.PacketCheck;
import ac.grim.grimac.player.GrimPlayer;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.GameMode;
@CheckData(name = "BadPacketsR", decay = 0.25, experimental = true)
public class BadPacketsR extends Check implements PacketCheck {
@@ -25,7 +24,7 @@ public class BadPacketsR extends Check implements PacketCheck {
long ms = (player.getPlayerClockAtLeast() - clock) / 1000000L;
long diff = (System.currentTimeMillis() - lastTransTime);
if (diff > 2000 && ms > 2000) {
if (positions == 0 && clock != 0 && player.gamemode != GameMode.SPECTATOR && !player.compensatedEntities.self.isDead) {
if (positions == 0 && clock != 0 && player.cameraEntity.isSelf() && !player.compensatedEntities.self.isDead) {
flag("time=" + ms + "ms, " + "lst=" + diff + "ms, positions=" + positions);
} else {
reward();
@@ -7,7 +7,6 @@ import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.update.PredictionComplete;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientEntityAction;
@CheckData(name = "BadPacketsX", experimental = true)
@@ -42,7 +41,7 @@ public class BadPacketsX extends Check implements PostPredictionCheck {
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (player.gamemode == GameMode.SPECTATOR || isTickPacket(event.getPacketType())) {
if (!player.cameraEntity.isSelf() || isTickPacket(event.getPacketType())) {
sprint = sneak = false;
return;
}
@@ -10,7 +10,6 @@ import ac.grim.grimac.utils.math.Vector3dm;
import ac.grim.grimac.utils.math.VectorUtils;
import com.github.retrooper.packetevents.protocol.attribute.Attributes;
import com.github.retrooper.packetevents.protocol.player.DiggingAction;
import com.github.retrooper.packetevents.protocol.player.GameMode;
@CheckData(name = "FarBreak", description = "Breaking blocks too far away", experimental = true)
public class FarBreak extends Check implements BlockBreakCheck {
@@ -20,7 +19,7 @@ public class FarBreak extends Check implements BlockBreakCheck {
@Override
public void onBlockBreak(BlockBreak blockBreak) {
if (player.gamemode == GameMode.SPECTATOR || player.inVehicle() || blockBreak.action == DiggingAction.CANCELLED_DIGGING)
if (!player.cameraEntity.isSelf() || player.inVehicle() || blockBreak.action == DiggingAction.CANCELLED_DIGGING)
return; // falses
double min = Double.MAX_VALUE;
@@ -9,7 +9,6 @@ import ac.grim.grimac.utils.anticheat.update.BlockBreak;
import ac.grim.grimac.utils.anticheat.update.PredictionComplete;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.player.DiggingAction;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.protocol.world.BlockFace;
import com.github.retrooper.packetevents.util.Vector3i;
@@ -53,7 +52,7 @@ public class MultiBreak extends Check implements BlockBreakCheck {
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (player.gamemode == GameMode.SPECTATOR || isTickPacket(event.getPacketType())) {
if (!player.cameraEntity.isSelf() || isTickPacket(event.getPacketType())) {
hasBroken = false;
}
}
@@ -13,7 +13,6 @@ import ac.grim.grimac.utils.nmsutil.ReachUtils;
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.world.BlockFace;
import com.github.retrooper.packetevents.util.Vector3d;
import com.github.retrooper.packetevents.util.Vector3f;
@@ -34,7 +33,7 @@ public class RotationBreak extends Check implements BlockBreakCheck {
@Override
public void onBlockBreak(BlockBreak blockBreak) {
if (player.gamemode == GameMode.SPECTATOR)
if (!player.cameraEntity.isSelf())
return; // you don't send flying packets when spectating entities
if (player.inVehicle()) return; // falses
if (blockBreak.action == DiggingAction.CANCELLED_DIGGING) return; // falses
@@ -50,7 +49,7 @@ public class RotationBreak extends Check implements BlockBreakCheck {
@Override
public void onPostFlyingBlockBreak(BlockBreak blockBreak) {
if (player.gamemode == GameMode.SPECTATOR)
if (!player.cameraEntity.isSelf())
return; // you don't send flying packets when spectating entities
if (player.inVehicle()) return; // falses
if (blockBreak.action == DiggingAction.CANCELLED_DIGGING) return; // falses
@@ -7,7 +7,6 @@ import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.update.PredictionComplete;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
import java.util.ArrayList;
@@ -48,7 +47,7 @@ public class MultiInteractA extends Check implements PostPredictionCheck {
hasInteracted = true;
}
if (player.gamemode == GameMode.SPECTATOR || isTickPacket(event.getPacketType())) {
if (!player.cameraEntity.isSelf() || isTickPacket(event.getPacketType())) {
hasInteracted = false;
}
}
@@ -8,7 +8,6 @@ import ac.grim.grimac.utils.anticheat.MessageUtil;
import ac.grim.grimac.utils.anticheat.update.PredictionComplete;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.util.Vector3f;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
@@ -49,7 +48,7 @@ public class MultiInteractB extends Check implements PostPredictionCheck {
hasInteracted = true;
}
if (player.gamemode == GameMode.SPECTATOR || isTickPacket(event.getPacketType())) {
if (!player.cameraEntity.isSelf() || isTickPacket(event.getPacketType())) {
hasInteracted = false;
}
}
@@ -34,14 +34,20 @@ 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.PacketEvents;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.github.retrooper.packetevents.protocol.attribute.Attributes;
import com.github.retrooper.packetevents.protocol.component.ComponentTypes;
import com.github.retrooper.packetevents.protocol.component.builtin.item.ItemAttackRange;
import com.github.retrooper.packetevents.protocol.entity.type.EntityType;
import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes;
import com.github.retrooper.packetevents.protocol.item.ItemStack;
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.protocol.player.InteractionHand;
import com.github.retrooper.packetevents.util.Vector3d;
import com.github.retrooper.packetevents.util.Vector3i;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
@@ -64,7 +70,7 @@ public class Reach extends Check implements PacketCheck {
private static final CheckResult NONE = new CheckResult(ResultType.NONE, "");
// 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<>();
private final Int2ObjectMap<InteractionData> 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
@@ -114,18 +120,25 @@ public class Reach extends Check implements PacketCheck {
// TODO: Remove when in front of via
if (entity.type == EntityTypes.ARMOR_STAND && player.getClientVersion().isOlderThan(ClientVersion.V_1_8))
return;
//Prevents Happy Ghast Reach false on 1.21.6+ servers with ViaBackwards set up
if (entity.type == EntityTypes.HAPPY_GHAST && player.getClientVersion().isOlderThan(ClientVersion.V_1_21_6)) {
return;
}
if (player.gamemode == GameMode.CREATIVE || player.gamemode == GameMode.SPECTATOR)
return;
if (player.inVehicle()) return;
if (entity.riding != null) return;
InteractionHand hand = action.getAction() == WrapperPlayClientInteractEntity.InteractAction.ATTACK ?
InteractionHand.MAIN_HAND : action.getHand(); // attacks can be only performed with the main hand
ItemStack itemInHand = player.inventory.getItemInHand(hand);
boolean tooManyAttacks = playerAttackQueue.size() > 10;
if (!tooManyAttacks) {
playerAttackQueue.put(action.getEntityId(), new Vector3d(player.x, player.y, player.z)); // Queue for next tick for very precise check
playerAttackQueue.put(action.getEntityId(), new InteractionData(new Vector3d(player.x, player.y, player.z), itemInHand)); // Queue for next tick for very precise check
}
boolean knownInvalid = isKnownInvalid(entity);
boolean knownInvalid = isKnownInvalid(entity, itemInHand);
if ((shouldModifyPackets() && cancelImpossibleHits && knownInvalid) || tooManyAttacks) {
event.setCancelled(true);
@@ -148,7 +161,7 @@ public class Reach extends Check implements PacketCheck {
// than this method. If this method flags, the other method WILL flag.
//
// Meaning that the other check should be the only one that flags.
private boolean isKnownInvalid(PacketEntity reachEntity) {
private boolean isKnownInvalid(PacketEntity reachEntity, ItemStack itemInHand) {
// If the entity doesn't exist, or if it is exempt, or if it is dead
if ((blacklisted.contains(reachEntity.type) || !reachEntity.isLivingEntity) && reachEntity.type != EntityTypes.END_CRYSTAL)
return false; // exempt
@@ -159,23 +172,24 @@ public class Reach extends Check implements PacketCheck {
// Filter out what we assume to be cheats
if (cancelBuffer != 0) {
CheckResult result = checkReach(reachEntity, new Vector3d(player.x, player.y, player.z), true);
CheckResult result = checkReach(reachEntity, new Vector3d(player.x, player.y, player.z), itemInHand, true);
return result.isFlag(); // If they flagged
} else {
SimpleCollisionBox targetBox = reachEntity.getPossibleCollisionBoxes();
if (reachEntity.type == EntityTypes.END_CRYSTAL) {
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);
SimpleCollisionBox targetBox = getTargetBox(reachEntity);
double maxReach = applyReachModifiers(targetBox, itemInHand, !player.packetStateData.didLastMovementIncludePosition);
return ReachUtils.getMinReachToBox(player, targetBox) > maxReach;
}
}
private void tickBetterReachCheckWithAngle(boolean isFlying) {
for (Int2ObjectMap.Entry<Vector3d> attack : playerAttackQueue.int2ObjectEntrySet()) {
for (Int2ObjectMap.Entry<InteractionData> attack : playerAttackQueue.int2ObjectEntrySet()) {
PacketEntity reachEntity = player.compensatedEntities.entityMap.get(attack.getIntKey());
if (reachEntity == null) continue;
CheckResult result = checkReach(reachEntity, attack.getValue(), false);
InteractionData interactionData = attack.getValue();
CheckResult result = checkReach(reachEntity, interactionData.vector(), interactionData.itemInHand(), false);
switch (result.type()) {
case REACH -> {
String added = ", type=" + reachEntity.type.getName().getKey();
@@ -209,38 +223,20 @@ public class Reach extends Check implements PacketCheck {
}
@NotNull
private CheckResult checkReach(PacketEntity reachEntity, Vector3d from, boolean isPrediction) {
SimpleCollisionBox targetBox = reachEntity.getPossibleCollisionBoxes();
if (reachEntity.type == EntityTypes.END_CRYSTAL) { // Hardcode end crystal box
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)
// Yes, this is vanilla and not uncertainty. All reach checks have this or they are wrong.
if (player.getClientVersion().isOlderThan(ClientVersion.V_1_9)) {
targetBox.expand(0.1f);
}
targetBox.expand(threshold);
// This is better than adding to the reach, as 0.03 can cause a player to miss their target
// Adds some more than 0.03 uncertainty in some cases, but a good trade off for simplicity
//
// Just give the uncertainty on 1.9+ clients as we have no way of knowing whether they had 0.03 movement
// However, on 1.21.2+ we do know if they had 0.03 movement
if (!player.packetStateData.didLastLastMovementIncludePosition || player.canSkipTicks())
targetBox.expand(player.getMovementThreshold());
private CheckResult checkReach(PacketEntity reachEntity, Vector3d from, ItemStack itemInHand, boolean isPrediction) {
SimpleCollisionBox targetBox = getTargetBox(reachEntity);
double maxReach = applyReachModifiers(targetBox, itemInHand, !player.packetStateData.didLastLastMovementIncludePosition);
double minDistance = Double.MAX_VALUE;
// will store all lookVecsAndEyeHeight pairs that landed a hit on the target entity
// We only need to check for blocking intersections for these
List<Pair<Vector3dm, Double>> lookVecsAndEyeHeights = new ArrayList<>();
final double maxReach = player.compensatedEntities.self.getAttributeValue(Attributes.ENTITY_INTERACTION_RANGE);
// +3 would be 3 + 3 = 6, which is the pre-1.20.5 behaviour, preventing "Missed Hitbox"
final double distance = maxReach + 3;
final double[] possibleEyeHeights = player.getPossibleEyeHeights();
final Vector3dm[] possibleLookDirs = player.getPossibleLookVectors(isPrediction);
final Vector3dm eyePos = new Vector3dm(from.getX(), 0, from.getZ());
@@ -309,6 +305,52 @@ public class Reach extends Check implements PacketCheck {
return NONE;
}
private SimpleCollisionBox getTargetBox(PacketEntity reachEntity) {
if (reachEntity.type == EntityTypes.END_CRYSTAL) { // Hardcode end crystal box
return new SimpleCollisionBox(reachEntity.trackedServerPosition.getPos().subtract(1, 0, 1), reachEntity.trackedServerPosition.getPos().add(1, 2, 1));
}
return reachEntity.getPossibleCollisionBoxes();
}
private double applyReachModifiers(SimpleCollisionBox targetBox, ItemStack itemInHand, boolean giveMovementThreshold) {
double maxReach;
double hitboxMargin = threshold;
ItemAttackRange attackRange = null;
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_21_11) && ATTACK_RANGE_COMPONENT_EXISTS) {
// TODO: ViaVersion support https://github.com/ViaVersion/ViaVersion/pull/4733
attackRange = itemInHand.getComponentOr(ComponentTypes.ATTACK_RANGE, null);
}
if (attackRange != null) {
maxReach = attackRange.getMaxRange();
hitboxMargin += attackRange.getHitboxMargin();
} else {
maxReach = player.compensatedEntities.self.getAttributeValue(Attributes.ENTITY_INTERACTION_RANGE);
// 1.7 and 1.8 players get a bit of extra hitbox (this is why you should use 1.8 on cross version servers)
// Yes, this is vanilla and not uncertainty. All reach checks have this or they are wrong.
if (player.getClientVersion().isOlderThan(ClientVersion.V_1_9)) {
hitboxMargin += 0.1f;
}
}
// This is better than adding to the reach, as 0.03 can cause a player to miss their target
// Adds some more than 0.03 uncertainty in some cases, but a good trade off for simplicity
//
// Just give the uncertainty on 1.9+ clients as we have no way of knowing whether they had 0.03 movement
// However, on 1.21.2+ we do know if they had 0.03 movement
if (giveMovementThreshold || player.canSkipTicks()) {
hitboxMargin += player.getMovementThreshold();
}
targetBox.expand(hitboxMargin);
return maxReach;
}
private static final boolean ATTACK_RANGE_COMPONENT_EXISTS = PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_21_11);
@Override
public void onReload(ConfigManager config) {
this.ignoreNonPlayerTargets = config.getBooleanElse("Reach.ignore-non-player-targets", false);
@@ -389,4 +431,8 @@ public class Reach extends Check implements PacketCheck {
new Vector3dm(from.getX(), from.getY(), from.getZ()),
isPrediction, player.compensatedEntities.self.getAttributeValue(Attributes.ENTITY_INTERACTION_RANGE));
}
private record InteractionData(Vector3d vector, ItemStack itemInHand) {
}
}
@@ -20,11 +20,10 @@ public class CrashC extends Check implements PacketCheck {
WrapperPlayClientPlayerFlying flying = new WrapperPlayClientPlayerFlying(event);
if (flying.hasPositionChanged()) {
Location pos = flying.getLocation();
if (Double.isNaN(pos.getX()) || Double.isNaN(pos.getY()) || Double.isNaN(pos.getZ())
|| Double.isInfinite(pos.getX()) || Double.isInfinite(pos.getY()) || Double.isInfinite(pos.getZ()) ||
Float.isNaN(pos.getYaw()) || Float.isNaN(pos.getPitch()) ||
Float.isInfinite(pos.getYaw()) || Float.isInfinite(pos.getPitch())) {
flagAndAlert("xyzYP: " + pos.getX() + ", " + pos.getY() + ", " + pos.getZ() + ", " + pos.getYaw() + ", " + pos.getPitch());
if (!Double.isFinite(pos.getX()) || !Double.isFinite(pos.getY()) || !Double.isFinite(pos.getZ())
|| !Float.isFinite(pos.getYaw()) || !Float.isFinite(pos.getPitch())
) {
flagAndAlert("xyzYP=" + pos.getX() + ", " + pos.getY() + ", " + pos.getZ() + ", " + pos.getYaw() + ", " + pos.getPitch());
player.getSetbackTeleportUtil().executeViolationSetback();
event.setCancelled(true);
player.onPacketCancel();
@@ -8,7 +8,6 @@ import ac.grim.grimac.utils.anticheat.update.PredictionComplete;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientEntityAction;
@CheckData(name = "ElytraC", description = "Started gliding too frequently")
@@ -27,7 +26,7 @@ public class ElytraC extends Check implements PostPredictionCheck {
return;
}
if (player.gamemode == GameMode.SPECTATOR) {
if (!player.cameraEntity.isSelf()) {
glideThisTick = glideLastTick = false;
}
@@ -37,7 +37,7 @@ public class InventoryD extends InventoryCheck {
VehicleData vehicle = player.vehicleData;
// Will flag once if player open anything with pressed space bar
isJumping = vehicle.nextHorseJump > 0 && horseJumpVerbose++ >= 1;
isJumping = !vehicle.pendingJumps.isEmpty() && horseJumpVerbose++ >= 1;
isMoving = vehicle.nextVehicleForward != 0 || vehicle.nextVehicleHorizontal != 0;
} else {
MoveVectorData move = findMovement(player.predictedVelocity);
@@ -106,7 +106,7 @@ public class PacketOrderI extends Check implements PostPredictionCheck {
}
}
if (player.gamemode == GameMode.SPECTATOR || isTickPacket(event.getPacketType())) {
if (!player.cameraEntity.isSelf() || isTickPacket(event.getPacketType())) {
digging = false;
}
}
@@ -7,7 +7,6 @@ import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.update.PredictionComplete;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.protocol.world.BlockFace;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerBlockPlacement;
@@ -49,7 +48,7 @@ public class PacketOrderM extends Check implements PostPredictionCheck {
interacting = false;
}
if (player.gamemode == GameMode.SPECTATOR || isTickPacket(event.getPacketType())) {
if (!player.cameraEntity.isSelf() || isTickPacket(event.getPacketType())) {
usingWithoutInteract = interacting = false;
}
}
@@ -7,7 +7,6 @@ import ac.grim.grimac.utils.anticheat.update.BlockPlace;
import ac.grim.grimac.utils.anticheat.update.PredictionComplete;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.protocol.world.BlockFace;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerBlockPlacement;
@@ -46,7 +45,7 @@ public class PacketOrderN extends BlockPlaceCheck {
placing = false;
}
if (player.gamemode == GameMode.SPECTATOR || isTickPacket(event.getPacketType())) {
if (!player.cameraEntity.isSelf() || isTickPacket(event.getPacketType())) {
usingWithoutPlacing = placing = false;
}
}
@@ -8,7 +8,6 @@ import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.protocol.world.BlockFace;
import com.github.retrooper.packetevents.wrapper.play.client.*;
import lombok.Getter;
@@ -111,7 +110,7 @@ public final class PacketOrderProcessor extends Check implements PacketCheck {
closingInventory = true;
}
if (player.gamemode == GameMode.SPECTATOR || isTickPacket(packetType)
if (!player.cameraEntity.isSelf() || isTickPacket(packetType)
|| player.getClientVersion().isOlderThan(ClientVersion.V_1_21_2)
&& !player.compensatedWorld.isChunkLoaded(GrimMath.floor(player.x) >> 4, GrimMath.floor(player.z) >> 4)) {
openingInventory = false;
@@ -10,6 +10,29 @@ import com.github.retrooper.packetevents.util.Vector3f;
@CheckData(name = "FabricatedPlace", description = "Sent out of bounds cursor position")
public class FabricatedPlace extends BlockPlaceCheck {
/**
* MAX_DOUBLE_ERROR:
* Represents the maximum possible floating-point arithmetic error that can occur
* when calculating vectors at the Minecraft World Border (30,000,000 blocks).
*
* Math.ulp(30,000,000.0) is approx 3.72E-9.
* We multiply by 2.0 to account for subtraction compounding logic in raytracing.
*
* This constant is safe to use everywhere; 7.450580596923828E-9 is physically indistinguishable from zero.
*/
private static final double MAX_DOUBLE_ERROR = Math.ulp(30_000_000.0) * 2.0;
/**
* FLOAT_STEP_AT_ONE:
* Represents the resolution of a Float at the value 1.0 (and 1.5).
* Math.ulp(1.0f) is approx 1.1920929E-7.
*
* When checking the upper bound (1.0), we must allow this variance because a Double
* slightly larger than 1.0 might get cast/snapped to the next available Float step.
*/
private static final double FLOAT_STEP_AT_ONE = Math.ulp(1.0f);
public FabricatedPlace(GrimPlayer player) {
super(player);
}
@@ -19,11 +42,50 @@ public class FabricatedPlace extends BlockPlaceCheck {
Vector3f cursor = place.cursor;
if (cursor == null) return;
double allowed = Materials.isShapeExceedsCube(place.getPlacedAgainstMaterial()) || place.getPlacedAgainstMaterial() == StateTypes.LECTERN ? 1.5 : 1;
double minAllowed = 1 - allowed;
// Determine if we allow up to 1.5 (Lecterns, Scaffolding, etc)
boolean isExtended = Materials.isShapeExceedsCube(place.getPlacedAgainstMaterial())
|| place.getPlacedAgainstMaterial() == StateTypes.LECTERN;
if (cursor.getX() < minAllowed || cursor.getY() < minAllowed || cursor.getZ() < minAllowed || cursor.getX() > allowed || cursor.getY() > allowed || cursor.getZ() > allowed) {
if (flagAndAlert() && shouldModifyPackets() && shouldCancel()) {
double maxBound = isExtended ? 1.5 : 1.0;
double minBound = 1.0 - maxBound; // Usually 0.0
// ====================================================================================
// LOWER BOUND CHECK (< 0.0)
// ====================================================================================
// Why only MAX_DOUBLE_ERROR?
// Near 0.0, 'float' has extremely high resolution (down to E-45).
// It acts like a double. When the client has a tiny calculation error (e.g. -4.44E-16),
// the float cast preserves it exactly. We only need to account for the arithmetic noise.
if (cursor.getX() < minBound - MAX_DOUBLE_ERROR ||
cursor.getY() < minBound - MAX_DOUBLE_ERROR ||
cursor.getZ() < minBound - MAX_DOUBLE_ERROR) {
// Alert logic
String debug = String.format("cursor=%s limit=%.16f", cursor, minBound - MAX_DOUBLE_ERROR);
if (flagAndAlert(debug) && shouldModifyPackets() && shouldCancel()) {
place.resync();
}
return;
}
// ====================================================================================
// UPPER BOUND CHECK (> 1.0 or > 1.5)
// ====================================================================================
// Near 1.0, the "Float Step" is E-7. This is 16x larger than the Double error.
// If the calculation is slightly off, it might snap to the NEXT float.
// We tolerate exactly one "Float Step" of overflow.
double upperTolerance = FLOAT_STEP_AT_ONE;
// Note: Effectively Math.max(MAX_DOUBLE_ERROR, FLOAT_STEP_AT_ONE)
// but since FLOAT_STEP is always larger in MC coordinates, we just use it.
if (cursor.getX() > maxBound + upperTolerance ||
cursor.getY() > maxBound + upperTolerance ||
cursor.getZ() > maxBound + upperTolerance) {
// Alert logic
String debug = String.format("cursor=%s limit=%.16f", cursor, maxBound + upperTolerance);
if (flagAndAlert(debug) && shouldModifyPackets() && shouldCancel()) {
place.resync();
}
}
@@ -8,7 +8,6 @@ import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
import ac.grim.grimac.utils.math.Vector3dm;
import ac.grim.grimac.utils.math.VectorUtils;
import com.github.retrooper.packetevents.protocol.attribute.Attributes;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.protocol.world.states.type.StateTypes;
import com.github.retrooper.packetevents.util.Vector3i;
@@ -20,7 +19,7 @@ public class FarPlace extends BlockPlaceCheck {
@Override
public void onBlockPlace(final BlockPlace place) {
if (player.gamemode == GameMode.SPECTATOR || player.inVehicle()) return;
if (!player.cameraEntity.isSelf() || player.inVehicle()) return;
Vector3i blockPos = place.position;
@@ -7,7 +7,6 @@ import ac.grim.grimac.utils.anticheat.MessageUtil;
import ac.grim.grimac.utils.anticheat.update.BlockPlace;
import ac.grim.grimac.utils.anticheat.update.PredictionComplete;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.protocol.world.BlockFace;
import com.github.retrooper.packetevents.util.Vector3f;
import com.github.retrooper.packetevents.util.Vector3i;
@@ -54,7 +53,7 @@ public class MultiPlace extends BlockPlaceCheck {
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (player.gamemode == GameMode.SPECTATOR || isTickPacket(event.getPacketType())) {
if (!player.cameraEntity.isSelf() || isTickPacket(event.getPacketType())) {
hasPlaced = false;
}
}
@@ -11,7 +11,6 @@ import ac.grim.grimac.utils.nmsutil.Ray;
import ac.grim.grimac.utils.nmsutil.ReachUtils;
import com.github.retrooper.packetevents.protocol.attribute.Attributes;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.protocol.player.GameMode;
import com.github.retrooper.packetevents.protocol.world.BlockFace;
import com.github.retrooper.packetevents.protocol.world.states.type.StateTypes;
import com.github.retrooper.packetevents.util.Vector3d;
@@ -34,7 +33,7 @@ public class RotationPlace extends BlockPlaceCheck {
@Override
public void onBlockPlace(final BlockPlace place) {
if (place.material == StateTypes.SCAFFOLDING) return;
if (player.gamemode == GameMode.SPECTATOR)
if (!player.cameraEntity.isSelf())
return; // you don't send flying packets when spectating entities
if (player.inVehicle()) return;
if (flagBuffer > 0 && !didRayTraceHit(place)) {
@@ -50,7 +49,7 @@ public class RotationPlace extends BlockPlaceCheck {
@Override
public void onPostFlyingBlockPlace(BlockPlace place) {
if (place.material == StateTypes.SCAFFOLDING) return;
if (player.gamemode == GameMode.SPECTATOR)
if (!player.cameraEntity.isSelf())
return; // you don't send flying packets when spectating entities
if (player.inVehicle()) return;
@@ -21,7 +21,7 @@ public class VehicleD extends Check implements PacketCheck {
if (event.getPacketType() == PacketType.Play.Client.ENTITY_ACTION && new WrapperPlayClientEntityAction(event).getAction() == WrapperPlayClientEntityAction.Action.START_JUMPING_WITH_HORSE) {
final EntityType vehicle = player.getVehicleType();
if (!EntityTypes.isTypeInstanceOf(vehicle, EntityTypes.ABSTRACT_HORSE)) {
if (!EntityTypes.isTypeInstanceOf(vehicle, EntityTypes.ABSTRACT_HORSE) && !EntityTypes.isTypeInstanceOf(vehicle, EntityTypes.ABSTRACT_NAUTILUS)) {
if (flagAndAlert("vehicle=" + (vehicle == null ? "null" : vehicle.getName().getKey().toLowerCase())) && shouldModifyPackets()) {
event.setCancelled(true);
player.onPacketCancel();
@@ -1,8 +1,9 @@
package ac.grim.grimac.command;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import org.incendo.cloud.CommandManager;
public interface BuildableCommand {
void register(CommandManager<Sender> manager);
void register(CommandManager<Sender> manager, CloudCommandAdapter adapter);
}
@@ -0,0 +1,77 @@
package ac.grim.grimac.command;
import ac.grim.grimac.command.commands.*;
import ac.grim.grimac.command.handler.GrimCommandFailureHandler;
import ac.grim.grimac.platform.api.command.CommandService;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import io.leangen.geantyref.TypeToken;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.format.NamedTextColor;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.exception.InvalidSyntaxException;
import org.incendo.cloud.key.CloudKey;
import org.incendo.cloud.processors.requirements.RequirementApplicable;
import org.incendo.cloud.processors.requirements.RequirementApplicable.RequirementApplicableFactory;
import org.incendo.cloud.processors.requirements.RequirementPostprocessor;
import org.incendo.cloud.processors.requirements.Requirements;
import java.util.function.Function;
import java.util.function.Supplier;
public class CloudCommandService implements CommandService {
public static final CloudKey<Requirements<Sender, SenderRequirement>> REQUIREMENT_KEY
= CloudKey.of("requirements", new TypeToken<>() {});
public static final RequirementApplicableFactory<Sender, SenderRequirement> REQUIREMENT_FACTORY
= RequirementApplicable.factory(REQUIREMENT_KEY);
private boolean commandsRegistered = false;
private final Supplier<CommandManager<Sender>> commandManagerSupplier;
private final CloudCommandAdapter commandAdapter;
public CloudCommandService(Supplier<CommandManager<Sender>> commandManagerSupplier, CloudCommandAdapter commandAdapter) {
this.commandManagerSupplier = commandManagerSupplier;
this.commandAdapter = commandAdapter;
}
public void registerCommands() {
if (commandsRegistered) return;
CommandManager<Sender> commandManager = commandManagerSupplier.get();
new GrimPerf().register(commandManager, commandAdapter);
new GrimDebug().register(commandManager, commandAdapter);
new GrimAlerts().register(commandManager, commandAdapter);
new GrimProfile().register(commandManager, commandAdapter);
new GrimSendAlert().register(commandManager, commandAdapter);
new GrimHelp().register(commandManager, commandAdapter);
new GrimHistory().register(commandManager, commandAdapter);
new GrimReload().register(commandManager, commandAdapter);
new GrimSpectate().register(commandManager, commandAdapter);
new GrimStopSpectating().register(commandManager, commandAdapter);
new GrimLog().register(commandManager, commandAdapter);
new GrimVerbose().register(commandManager, commandAdapter);
new GrimVersion().register(commandManager, commandAdapter);
new GrimDump().register(commandManager, commandAdapter);
new GrimBrands().register(commandManager, commandAdapter);
new GrimList().register(commandManager, commandAdapter);
new GrimTestWebhook().register(commandManager, commandAdapter);
final RequirementPostprocessor<Sender, SenderRequirement>
senderRequirementPostprocessor = RequirementPostprocessor.of(
REQUIREMENT_KEY,
new GrimCommandFailureHandler()
);
commandManager.registerCommandPostProcessor(senderRequirementPostprocessor);
registerExceptionHandler(commandManager, InvalidSyntaxException.class, e -> MessageUtil.miniMessage(e.correctSyntax()));
commandsRegistered = true;
}
protected <E extends Exception> void registerExceptionHandler(CommandManager<Sender> commandManager, Class<E> ex, Function<E, ComponentLike> toComponent) {
commandManager.exceptionController().registerHandler(ex,
(c) -> c.context().sender().sendMessage(toComponent.apply(c.exception()).asComponent().colorIfAbsent(NamedTextColor.RED))
);
}
}
@@ -2,6 +2,7 @@ package ac.grim.grimac.command.commands;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.command.BuildableCommand;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.context.CommandContext;
@@ -12,7 +13,7 @@ import java.util.Objects;
public class GrimAlerts implements BuildableCommand {
@Override
public void register(CommandManager<Sender> commandManager) {
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
commandManager.command(
commandManager.commandBuilder("grim", "grimac")
.literal("alerts", Description.of("Toggle alerts for the sender"))
@@ -2,6 +2,7 @@ package ac.grim.grimac.command.commands;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.command.BuildableCommand;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.context.CommandContext;
@@ -12,7 +13,7 @@ import java.util.Objects;
public class GrimBrands implements BuildableCommand {
@Override
public void register(CommandManager<Sender> commandManager) {
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
commandManager.command(
commandManager.commandBuilder("grim", "grimac")
.literal("brands", Description.of("Toggle brands for the sender"))
@@ -4,6 +4,7 @@ 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.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.MessageUtil;
@@ -20,27 +21,27 @@ import org.jetbrains.annotations.Nullable;
public class GrimDebug implements BuildableCommand {
public void register(CommandManager<Sender> commandManager) {
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
Command.Builder<Sender> grimCommand = commandManager.commandBuilder("grim", "grimac");
// Register "debug" subcommand
Command.Builder<Sender> debugCommand = grimCommand
.literal("debug", Description.of("Toggle debug output for a player"))
.permission("grim.debug")
.optional("target", GrimAPI.INSTANCE.getCommandAdapter().singlePlayerSelectorParser())
.optional("target", adapter.singlePlayerSelectorParser())
.handler(this::handleDebug);
// Register "consoledebug" subcommand
Command.Builder<Sender> consoleDebugCommand = grimCommand
.literal("consoledebug", Description.of("Toggle console debug output for a player"))
.permission("grim.consoledebug")
.required("target", GrimAPI.INSTANCE.getCommandAdapter().singlePlayerSelectorParser())
.required("target", adapter.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)"))
.optional("target", adapter.singlePlayerSelectorParser(), Description.of("Player to debug (defaults to self if sender is player)"))
.handler(this::handleHitboxDebug);
// Register command
@@ -3,6 +3,7 @@ package ac.grim.grimac.command.commands;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.command.BuildableCommand;
import ac.grim.grimac.platform.api.PlatformPlugin;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import ac.grim.grimac.utils.common.PropertiesUtil;
@@ -29,7 +30,7 @@ public class GrimDump implements BuildableCommand {
private String link = null; // these links should not expire for a while
@Override
public void register(CommandManager<Sender> commandManager) {
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
commandManager.command(
commandManager.commandBuilder("grim", "grimac")
.literal("dump", Description.of("Generate a debug dump"))
@@ -51,9 +52,9 @@ public class GrimDump implements BuildableCommand {
GrimLog.sendLogAsync(sender, generateDump(), string -> link = string, "text/yaml");
}
public static JsonObject getBasicInfo(String type) {
public static JsonObject getDumpInfo() {
JsonObject base = new JsonObject();
base.addProperty("type", type);
base.addProperty("type", "dump");
base.addProperty("timestamp", System.currentTimeMillis());
// versions
JsonObject versions = new JsonObject();
@@ -68,6 +69,11 @@ public class GrimDump implements BuildableCommand {
if (GrimAPI.INSTANCE.isInitialized()) states.addProperty("platform", GrimAPI.INSTANCE.getPlatform().toString());
if (ViaVersionUtil.isAvailable) states.addProperty("has_viaversion", true);
if (PAPER) states.addProperty("has_paper", true);
// include some relevant settings if not default
JsonObject settings = new JsonObject();
if (GrimAPI.INSTANCE.getAlertManager().hasConsoleVerboseEnabled()) settings.addProperty("console_verbose", true);
if (!GrimAPI.INSTANCE.getAlertManager().hasConsoleAlertsEnabled()) settings.addProperty("console_alerts", false);
if (settings.size() > 0) states.add("settings", settings);
// system
JsonObject system = new JsonObject();
base.add("system", system);
@@ -75,8 +81,17 @@ public class GrimDump implements BuildableCommand {
system.addProperty("java_version", System.getProperty("java.version"));
system.addProperty("user_language", System.getProperty("user.language"));
// build
JsonObject build = new JsonObject();
base.add("build", getBuildInfo());
// plugins
JsonArray plugins = new JsonArray();
base.add("plugins", plugins);
for (PlatformPlugin plugin : GrimAPI.INSTANCE.getPluginManager().getPlugins()) {
JsonObject pluginJson = new JsonObject();
pluginJson.addProperty("enabled", plugin.isEnabled());
pluginJson.addProperty("name", plugin.getName());
pluginJson.addProperty("version", plugin.getVersion());
plugins.add(pluginJson);
}
return base;
}
@@ -98,17 +113,7 @@ public class GrimDump implements BuildableCommand {
* @return A JSON-formatted string containing the diagnostic dump.
*/
private String generateDump() {
JsonObject base = getBasicInfo("dump");
// plugins
JsonArray plugins = new JsonArray();
base.add("plugins", plugins);
for (PlatformPlugin plugin : GrimAPI.INSTANCE.getPluginManager().getPlugins()) {
JsonObject pluginJson = new JsonObject();
pluginJson.addProperty("enabled", plugin.isEnabled());
pluginJson.addProperty("name", plugin.getName());
pluginJson.addProperty("version", plugin.getVersion());
plugins.add(pluginJson);
}
JsonObject base = getDumpInfo();
return gson.toJson(base);
}
}
@@ -2,6 +2,7 @@ package ac.grim.grimac.command.commands;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.command.BuildableCommand;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import org.incendo.cloud.CommandManager;
@@ -11,7 +12,7 @@ import org.jetbrains.annotations.NotNull;
public class GrimHelp implements BuildableCommand {
@Override
public void register(CommandManager<Sender> commandManager) {
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
commandManager.command(
commandManager.commandBuilder("grim", "grimac")
.literal("help", Description.of("Display help information"))
@@ -4,6 +4,7 @@ import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.command.BuildableCommand;
import ac.grim.grimac.manager.violationdatabase.Violation;
import ac.grim.grimac.manager.violationdatabase.ViolationDatabaseManager;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.player.OfflinePlatformPlayer;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.utils.anticheat.MessageUtil;
@@ -18,12 +19,12 @@ import java.util.concurrent.TimeUnit;
public class GrimHistory implements BuildableCommand {
@Override
public void register(CommandManager<Sender> commandManager) {
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
commandManager.command(
commandManager.commandBuilder("grim", "grimac")
.literal("history", "hist")
.permission("grim.help")
.required("target", StringParser.stringParser(), GrimAPI.INSTANCE.getCommandAdapter().onlinePlayerSuggestions())
.required("target", StringParser.stringParser(), adapter.onlinePlayerSuggestions())
.optional("page", IntegerParser.integerParser())
.permission("grim.history")
.handler(this::handleHistory)
@@ -4,6 +4,7 @@ import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.api.GrimIdentity;
import ac.grim.grimac.command.BuildableCommand;
import ac.grim.grimac.command.CommandUtils;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.player.PlatformPlayer;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.player.GrimPlayer;
@@ -28,7 +29,7 @@ public class GrimList implements BuildableCommand {
// Mainly for debugging purposes. Useful for seeing which players are exempt or not.
@Override
public void register(CommandManager<Sender> commandManager) {
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
commandManager.command(commandManager.commandBuilder("grim", "grimac")
.literal("list")
.permission("grim.list")
@@ -3,6 +3,7 @@ package ac.grim.grimac.command.commands;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.command.BuildableCommand;
import ac.grim.grimac.manager.init.start.SuperDebug;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.utils.anticheat.LogUtil;
import ac.grim.grimac.utils.anticheat.MessageUtil;
@@ -68,7 +69,7 @@ public class GrimLog implements BuildableCommand {
}
@Override
public void register(CommandManager<Sender> commandManager) {
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
Command<Sender> command = commandManager.commandBuilder("grim", "grimac")
.literal("log", "logs")
.permission("grim.log")
@@ -1,5 +1,6 @@
package ac.grim.grimac.command.commands;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.predictionengine.MovementCheckRunner;
import net.kyori.adventure.text.Component;
@@ -11,7 +12,7 @@ import org.jetbrains.annotations.NotNull;
public class GrimPerf {
public void register(CommandManager<Sender> commandManager) {
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
Command.Builder<Sender> grimCommand = commandManager.commandBuilder("grim", "grimac");
Command.Builder<Sender> configuredBuilder = grimCommand
@@ -3,6 +3,7 @@ package ac.grim.grimac.command.commands;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.command.BuildableCommand;
import ac.grim.grimac.platform.api.command.PlayerSelector;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.player.PlatformPlayer;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.player.GrimPlayer;
@@ -16,12 +17,12 @@ import java.util.Objects;
public class GrimProfile implements BuildableCommand {
@Override
public void register(CommandManager<Sender> commandManager) {
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
commandManager.command(
commandManager.commandBuilder("grim", "grimac")
.literal("profile")
.permission("grim.profile")
.required("target", GrimAPI.INSTANCE.getCommandAdapter().singlePlayerSelectorParser())
.required("target", adapter.singlePlayerSelectorParser())
.handler(this::handleProfile)
);
}
@@ -2,6 +2,7 @@ package ac.grim.grimac.command.commands;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.command.BuildableCommand;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import net.kyori.adventure.text.Component;
@@ -11,7 +12,7 @@ import org.jetbrains.annotations.NotNull;
public class GrimReload implements BuildableCommand {
@Override
public void register(CommandManager<Sender> commandManager) {
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
commandManager.command(
commandManager.commandBuilder("grim", "grimac")
.literal("reload")
@@ -2,6 +2,7 @@ package ac.grim.grimac.command.commands;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.command.BuildableCommand;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import net.kyori.adventure.text.Component;
@@ -12,7 +13,7 @@ import org.jetbrains.annotations.NotNull;
public class GrimSendAlert implements BuildableCommand {
@Override
public void register(CommandManager<Sender> commandManager) {
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
commandManager.command(
commandManager.commandBuilder("grim", "grimac")
.literal("sendalert")
@@ -2,9 +2,10 @@ package ac.grim.grimac.command.commands;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.command.BuildableCommand;
import ac.grim.grimac.command.CloudCommandService;
import ac.grim.grimac.command.requirements.PlayerSenderRequirement;
import ac.grim.grimac.manager.init.start.CommandRegister;
import ac.grim.grimac.platform.api.command.PlayerSelector;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.player.PlatformPlayer;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.utils.anticheat.MessageUtil;
@@ -17,14 +18,14 @@ import java.util.Objects;
public class GrimSpectate implements BuildableCommand {
@Override
public void register(CommandManager<Sender> commandManager) {
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
commandManager.command(
commandManager.commandBuilder("grim", "grimac")
.literal("spectate")
.permission("grim.spectate")
.required("target", GrimAPI.INSTANCE.getCommandAdapter().singlePlayerSelectorParser())
.required("target", adapter.singlePlayerSelectorParser())
.handler(this::handleSpectate)
.apply(CommandRegister.REQUIREMENT_FACTORY.create(PlayerSenderRequirement.PLAYER_SENDER_REQUIREMENT))
.apply(CloudCommandService.REQUIREMENT_FACTORY.create(PlayerSenderRequirement.PLAYER_SENDER_REQUIREMENT))
);
}
@@ -2,8 +2,9 @@ package ac.grim.grimac.command.commands;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.command.BuildableCommand;
import ac.grim.grimac.command.CloudCommandService;
import ac.grim.grimac.command.requirements.PlayerSenderRequirement;
import ac.grim.grimac.manager.init.start.CommandRegister;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import org.incendo.cloud.CommandManager;
@@ -18,7 +19,7 @@ import java.util.Objects;
public class GrimStopSpectating implements BuildableCommand {
@Override
public void register(CommandManager<Sender> commandManager) {
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
commandManager.command(
commandManager.commandBuilder("grim", "grimac")
.literal("stopspectating")
@@ -30,7 +31,7 @@ public class GrimStopSpectating implements BuildableCommand {
return List.of(); // No suggestions if no permission
}))
.handler(this::onStopSpectate)
.apply(CommandRegister.REQUIREMENT_FACTORY.create(PlayerSenderRequirement.PLAYER_SENDER_REQUIREMENT))
.apply(CloudCommandService.REQUIREMENT_FACTORY.create(PlayerSenderRequirement.PLAYER_SENDER_REQUIREMENT))
);
}
@@ -0,0 +1,45 @@
package ac.grim.grimac.command.commands;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.command.BuildableCommand;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.utils.anticheat.LogUtil;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import ac.grim.grimac.utils.webhook.WebhookMessage;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.context.CommandContext;
import org.jetbrains.annotations.NotNull;
public class GrimTestWebhook implements BuildableCommand {
@Override
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
commandManager.command(
commandManager.commandBuilder("grim", "grimac")
.literal("testwebhook")
.permission("grim.testwebhook")
.handler(this::handleTestWebhook)
);
}
private void handleTestWebhook(@NotNull CommandContext<Sender> context) {
if (GrimAPI.INSTANCE.getDiscordManager().isDisabled()) {
context.sender().sendMessage(MessageUtil.miniMessage(GrimAPI.INSTANCE.getConfigManager().getWebhookNotEnabled()));
return;
}
WebhookMessage webhookMessage = new WebhookMessage().content(GrimAPI.INSTANCE.getConfigManager().getWebhookTestMessage());
GrimAPI.INSTANCE.getDiscordManager().sendWebhookMessage(webhookMessage).whenCompleteAsync(((successful, throwable) -> {
if (successful == true) {
context.sender().sendMessage(MessageUtil.miniMessage(GrimAPI.INSTANCE.getConfigManager().getWebhookTestSucceeded()));
return;
}
context.sender().sendMessage(MessageUtil.miniMessage(GrimAPI.INSTANCE.getConfigManager().getWebhookTestFailed()));
if (throwable != null) {
LogUtil.error("Exception caught while sending a Discord webhook test alert", throwable);
}
}));
}
}
@@ -2,6 +2,7 @@ package ac.grim.grimac.command.commands;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.command.BuildableCommand;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.context.CommandContext;
@@ -9,7 +10,7 @@ import org.jetbrains.annotations.NotNull;
public class GrimVerbose implements BuildableCommand {
@Override
public void register(CommandManager<Sender> commandManager) {
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
commandManager.command(
commandManager.commandBuilder("grim", "grimac")
.literal("verbose")
@@ -2,6 +2,7 @@ package ac.grim.grimac.command.commands;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.command.BuildableCommand;
import ac.grim.grimac.platform.api.manager.cloud.CloudCommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.utils.anticheat.LogUtil;
import ac.grim.grimac.utils.anticheat.MessageUtil;
@@ -136,7 +137,7 @@ public class GrimVersion implements BuildableCommand {
}
@Override
public void register(CommandManager<Sender> commandManager) {
public void register(CommandManager<Sender> commandManager, CloudCommandAdapter adapter) {
commandManager.command(
commandManager.commandBuilder("grim", "grimac")
.literal("version")
@@ -1,73 +0,0 @@
package ac.grim.grimac.events;
import ac.grim.grimac.api.plugin.GrimPlugin;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Manages the resolution of platform-specific plugin contexts into the universal {@link GrimPlugin} interface.
* <p>
* This class is the bridge that allows the API to remain platform-agnostic while still supporting
* platform-specific objects for lifecycle management.
*/
@ApiStatus.Internal
public final class GrimExtensionManager {
private final List<GrimExtensionResolver> resolvers = new CopyOnWriteArrayList<>();
private ResolutionFailureHandler failureHandler = failedContext ->
new IllegalArgumentException("Unable to resolve plugin context for type: " + failedContext.getClass().getName() +
". Ensure you are passing a valid platform plugin instance or a pre-existing GrimPlugin.");
/**
* Sets a custom handler for creating exceptions when plugin context resolution fails.
* This allows platform-specific implementations to provide more descriptive error messages.
*
* @param failureHandler The handler to use.
*/
public void setFailureHandler(@NotNull ResolutionFailureHandler failureHandler) {
this.failureHandler = Objects.requireNonNull(failureHandler, "failureHandler cannot be null");
}
/**
* Registers a new resolver. This is intended to be called by the core GrimAC implementation
* during its startup phase.
*
* @param resolver The resolver to add.
*/
public void registerResolver(@NotNull GrimExtensionResolver resolver) {
Objects.requireNonNull(resolver, "resolver cannot be null");
this.resolvers.add(resolver);
}
/**
* Resolves a context object into a GrimPlugin by trying all registered resolvers in order.
* <p>
* It also has built-in logic to handle being passed a {@link GrimPlugin} instance directly.
*
* @param context The context object to resolve.
* @return The resolved GrimPlugin.
* @throws IllegalArgumentException if no registered resolver can handle the provided context type.
*/
@NotNull
public GrimPlugin getPlugin(@NotNull Object context) {
Objects.requireNonNull(context, "context cannot be null");
// First, check for the universal case: was a GrimPlugin already passed?
if (context instanceof GrimPlugin) {
return (GrimPlugin) context;
}
// Next, iterate through platform-specific resolvers.
for (GrimExtensionResolver resolver : resolvers) {
GrimPlugin resolved = resolver.resolve(context);
if (resolved != null) {
return resolved;
}
}
// If no resolver succeeded, throw a descriptive error.
throw this.failureHandler.createExceptionFor(context);
}
}
@@ -1,27 +0,0 @@
package ac.grim.grimac.events;
import ac.grim.grimac.api.plugin.GrimPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A functional interface responsible for attempting to resolve a generic context object
* into a {@link GrimPlugin}.
* <p>
* Implementations of this are provided by the core GrimAC platform module (e.g., for Bukkit, Fabric)
* and registered with the central {@link GrimPluginManager}.
*/
@FunctionalInterface
public interface GrimExtensionResolver {
/**
* Attempts to resolve the given context object into a GrimPlugin.
*
* @param context The context object to resolve (e.g., a Bukkit Plugin, a Plugin Class, a Fabric Mod).
* @return An Optional containing the resolved GrimPlugin if this resolver supports the context type,
* otherwise an empty Optional.
*/
@Nullable GrimPlugin resolve(@NotNull Object context);
}
@@ -1,337 +0,0 @@
package ac.grim.grimac.events;
import ac.grim.grimac.api.event.EventBus;
import ac.grim.grimac.api.event.GrimEvent;
import ac.grim.grimac.api.event.GrimEventHandler;
import ac.grim.grimac.api.event.GrimEventListener;
import ac.grim.grimac.api.event.events.*;
import ac.grim.grimac.api.plugin.GrimPlugin;
import ac.grim.grimac.utils.anticheat.LogUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
public class OptimizedEventBus implements EventBus {
private final MethodHandles.Lookup lookup = MethodHandles.lookup();
private final GrimExtensionManager extensionManager;
// Store all listeners together by event type
private final Map<Class<? extends GrimEvent>, AtomicReference<OptimizedListener[]>> listenerMap = new ConcurrentHashMap<>();
public OptimizedEventBus(GrimExtensionManager extensionManager) {
this.extensionManager = extensionManager;
prefillKnownEventTypes(listenerMap);
}
/**
* Minor optimization to prefill the listenerMap for known existing events so that they don't have to be computed later
* When registering an event of the type for the first time
* @param map
*/
private void prefillKnownEventTypes(Map<Class<? extends GrimEvent>, AtomicReference<OptimizedListener[]>> map) {
// Add all your known event types here
map.put(GrimReloadEvent.class, new AtomicReference<>(new OptimizedListener[0]));
map.put(GrimQuitEvent.class, new AtomicReference<>(new OptimizedListener[0]));
map.put(GrimJoinEvent.class, new AtomicReference<>(new OptimizedListener[0]));
map.put(FlagEvent.class, new AtomicReference<>(new OptimizedListener[0]));
map.put(CommandExecuteEvent.class, new AtomicReference<>(new OptimizedListener[0]));
map.put(CompletePredictionEvent.class, new AtomicReference<>(new OptimizedListener[0]));
// Base event type
map.put(GrimEvent.class, new AtomicReference<>(new OptimizedListener[0]));
}
@Override
public void registerAnnotatedListeners(@NotNull Object pluginContext, @NotNull Object listener) {
GrimPlugin plugin = extensionManager.getPlugin(pluginContext);
registerAnnotatedListeners(plugin, listener);
}
@Override
public void registerAnnotatedListeners(GrimPlugin plugin, @NotNull Object listener) {
registerMethods(plugin, listener, listener.getClass());
}
@Override
public void registerStaticAnnotatedListeners(@NotNull Object pluginContext, @NotNull Class<?> clazz) {
GrimPlugin plugin = extensionManager.getPlugin(pluginContext);
registerStaticAnnotatedListeners(plugin, clazz);
}
@Override
public void registerStaticAnnotatedListeners(GrimPlugin plugin, @NotNull Class<?> clazz) {
registerMethods(plugin, null, clazz);
}
@Override
public void unregisterListeners(@NotNull Object pluginContext, @NotNull Object listener) {
GrimPlugin plugin = extensionManager.getPlugin(pluginContext);
unregisterListeners(plugin, listener);
}
private void registerMethods(GrimPlugin plugin, @Nullable Object instance, @NotNull Class<?> clazz) {
for (Method method : clazz.getDeclaredMethods()) {
GrimEventHandler annotation = method.getAnnotation(GrimEventHandler.class);
if (annotation != null && method.getParameterCount() == 1) {
Class<?> eventType = method.getParameterTypes()[0];
if (GrimEvent.class.isAssignableFrom(eventType)) {
try {
if (instance == null && !Modifier.isStatic(method.getModifiers())) {
continue;
}
method.setAccessible(true);
MethodHandle handle = lookup.unreflect(method);
final GrimEventListener<GrimEvent> listener = getGrimEventListener(instance, handle, eventType);
OptimizedListener optimizedListener = new OptimizedListener(
plugin, listener, annotation.priority(),
annotation.ignoreCancelled(), method.getDeclaringClass(), instance
);
addListener((Class<? extends GrimEvent>) eventType, optimizedListener);
} catch (IllegalAccessException e) {
LogUtil.error("Failed to register listener for " + eventType.getName(), e);
}
}
}
}
}
// Decide which listener to create at registration time to avoid the runtime 'if' check.
private @NotNull GrimEventListener<GrimEvent> getGrimEventListener(@Nullable Object instance, MethodHandle handle, Class<?> eventType) {
final GrimEventListener<GrimEvent> listener;
if (instance != null) {
// For instance methods, create a listener that captures the instance.
listener = event -> {
try {
handle.invoke(instance, event);
} catch (Throwable throwable) {
throw new RuntimeException("Failed to invoke listener for " + eventType.getName(), throwable);
}
};
} else {
// For static methods, create a listener that does NOT capture an instance.
// This is slightly more memory efficient and avoids the null check.
listener = event -> {
try {
handle.invoke(event);
} catch (Throwable throwable) {
throw new RuntimeException("Failed to invoke listener for " + eventType.getName(), throwable);
}
};
}
return listener;
}
private void addListener(Class<? extends GrimEvent> eventType, OptimizedListener newListener) {
AtomicReference<OptimizedListener[]> ref = listenerMap.computeIfAbsent(
eventType, k -> new AtomicReference<>(new OptimizedListener[0])
);
while (true) {
OptimizedListener[] oldArray = ref.get();
// Find insertion point using Arrays.binarySearch
// We use a comparator for descending order (higher priorities first)
int insertionPoint = Arrays.binarySearch(oldArray, newListener,
(a, b) -> Integer.compare(b.priority, a.priority));
// If negative, convert to insertion point
if (insertionPoint < 0) {
insertionPoint = -(insertionPoint + 1);
} else {
// If we found an exact match, insert after the last equal priority element
// to maintain stable ordering
while (insertionPoint < oldArray.length - 1 &&
oldArray[insertionPoint + 1].priority == newListener.priority) {
insertionPoint++;
}
insertionPoint++; // Insert after the last equal priority element
}
OptimizedListener[] newArray = new OptimizedListener[oldArray.length + 1];
// Copy elements before insertion point
System.arraycopy(oldArray, 0, newArray, 0, insertionPoint);
// Insert the new listener
newArray[insertionPoint] = newListener;
// Copy remaining elements, shifted by one
System.arraycopy(oldArray, insertionPoint, newArray, insertionPoint + 1,
oldArray.length - insertionPoint);
if (ref.compareAndSet(oldArray, newArray)) {
break;
}
// If CAS failed, loop and try again
}
}
@Override
public void unregisterListeners(GrimPlugin plugin, Object instance) {
for (Map.Entry<Class<? extends GrimEvent>, AtomicReference<OptimizedListener[]>> entry : listenerMap.entrySet()) {
removeListeners(entry.getKey(), entry.getValue(),
listener -> listener.plugin.equals(plugin) &&
listener.instance != null &&
listener.instance.equals(instance));
}
}
@Override
public void unregisterStaticListeners(@NotNull Object pluginContext, @NotNull Class<?> clazz) {
GrimPlugin plugin = extensionManager.getPlugin(pluginContext);
unregisterStaticListeners(plugin, clazz);
}
@Override
public void unregisterStaticListeners(GrimPlugin plugin, Class<?> clazz) {
for (Map.Entry<Class<? extends GrimEvent>, AtomicReference<OptimizedListener[]>> entry : listenerMap.entrySet()) {
removeListeners(entry.getKey(), entry.getValue(),
listener -> listener.plugin.equals(plugin) &&
listener.instance == null &&
listener.declaringClass.equals(clazz));
}
}
@Override public void unregisterAllListeners(@NotNull Object pluginContext) {
GrimPlugin plugin = extensionManager.getPlugin(pluginContext);
unregisterAllListeners(plugin);
}
@Override
public void unregisterAllListeners(GrimPlugin plugin) {
for (Map.Entry<Class<? extends GrimEvent>, AtomicReference<OptimizedListener[]>> entry : listenerMap.entrySet()) {
removeListeners(entry.getKey(), entry.getValue(),
listener -> listener.plugin.equals(plugin));
}
}
@Override
public void unregisterListener(@NotNull Object pluginContext, @NotNull GrimEventListener<?> listener) {
GrimPlugin plugin = extensionManager.getPlugin(pluginContext);
unregisterListener(plugin, listener);
}
@Override
public void unregisterListener(GrimPlugin plugin, GrimEventListener<?> eventListener) {
for (Map.Entry<Class<? extends GrimEvent>, AtomicReference<OptimizedListener[]>> entry : listenerMap.entrySet()) {
removeListeners(entry.getKey(), entry.getValue(),
listener -> listener.plugin.equals(plugin) &&
listener.listener.equals(eventListener));
}
}
private void removeListeners(Class<? extends GrimEvent> eventType,
AtomicReference<OptimizedListener[]> ref,
Predicate<OptimizedListener> filter) {
while (true) {
OptimizedListener[] oldArray = ref.get();
// Count how many listeners will remain after filtering
int remaining = 0;
for (OptimizedListener listener : oldArray) {
if (!filter.test(listener)) {
remaining++;
}
}
// If no change, return early
if (remaining == oldArray.length) {
return;
}
// Create new array with filtered listeners
OptimizedListener[] newArray = new OptimizedListener[remaining];
int index = 0;
for (OptimizedListener listener : oldArray) {
if (!filter.test(listener)) {
newArray[index++] = listener;
}
}
// Try to update the array
if (ref.compareAndSet(oldArray, newArray)) {
// If array is empty, remove the event type entry
if (newArray.length == 0) {
listenerMap.remove(eventType);
}
break;
}
// If CAS failed, retry
}
}
@Override
public void post(@NotNull GrimEvent event) {
Class<?> currentEventType = event.getClass();
while (GrimEvent.class.isAssignableFrom(currentEventType)) {
AtomicReference<OptimizedListener[]> ref = listenerMap.get(currentEventType);
if (ref != null) {
// Get current array - no locking needed for reads
OptimizedListener[] listeners = ref.get();
for (OptimizedListener listener : listeners) {
try {
if (event.isCancelled() && !listener.ignoreCancelled) {
continue;
}
listener.listener.handle(event);
} catch (Throwable throwable) {
LogUtil.error("Error handling event " + event.getEventName(), throwable);
}
}
}
currentEventType = currentEventType.getSuperclass();
}
}
@Override
public <T extends GrimEvent> void subscribe(@NotNull Object pluginContext, @NotNull Class<T> eventType, @NotNull GrimEventListener<T> listener, int priority, boolean ignoreCancelled, @NotNull Class<?> declaringClass) {
}
@Override
public <T extends GrimEvent> void subscribe(GrimPlugin plugin, @NotNull Class<T> eventType,
@NotNull GrimEventListener<T> listener, int priority,
boolean ignoreCancelled, @NotNull Class<?> declaringClass) {
@SuppressWarnings("unchecked")
OptimizedListener optimizedListener = new OptimizedListener(
plugin, (GrimEventListener<GrimEvent>)listener, priority,
ignoreCancelled, declaringClass, null
);
addListener(eventType, optimizedListener);
}
private static class OptimizedListener {
final GrimPlugin plugin;
final GrimEventListener<GrimEvent> listener;
final int priority;
final boolean ignoreCancelled;
final Class<?> declaringClass;
final Object instance;
OptimizedListener(GrimPlugin plugin, GrimEventListener<GrimEvent> listener,
int priority, boolean ignoreCancelled,
Class<?> declaringClass, Object instance) {
this.plugin = plugin;
this.listener = listener;
this.priority = priority;
this.ignoreCancelled = ignoreCancelled;
this.declaringClass = declaringClass;
this.instance = instance;
}
}
}
@@ -1,14 +0,0 @@
package ac.grim.grimac.events;
import org.jetbrains.annotations.NotNull;
@FunctionalInterface
public interface ResolutionFailureHandler {
/**
* Creates a runtime exception to be thrown when resolution fails.
*
* @param failedContext The object that could not be resolved.
* @return The exception to be thrown.
*/
RuntimeException createExceptionFor(@NotNull Object failedContext);
}
@@ -485,7 +485,7 @@ public class CheckManagerListener extends PacketListenerAbstract {
player.yaw = move.getYaw();
player.pitch = move.getPitch();
final VehiclePositionUpdate update = new VehiclePositionUpdate(clamp, position, move.getYaw(), move.getPitch(), player.packetStateData.lastPacketWasTeleport);
final VehiclePositionUpdate update = new VehiclePositionUpdate(clamp, position, move.getYaw(), move.getPitch(), move.isOnGround(), player.packetStateData.lastPacketWasTeleport);
player.checkManager.onVehiclePositionUpdate(update);
player.packetStateData.receivedSteerVehicle = false;
@@ -582,6 +582,11 @@ public class CheckManagerListener extends PacketListenerAbstract {
// The player didn't send a movement packet, so we can predict this like we had idle tick on 1.8
player.packetStateData.didLastLastMovementIncludePosition = player.packetStateData.didLastMovementIncludePosition;
player.packetStateData.didLastMovementIncludePosition = false;
// Track dash cooldown
if (!player.inVehicle()) {
player.dashableEntities.tick();
}
}
player.packetStateData.didSendMovementBeforeTickEnd = false;
}
@@ -717,6 +722,8 @@ public class CheckManagerListener extends PacketListenerAbstract {
if (hasLook) {
player.yaw = yaw;
player.pitch = pitch;
player.vehicleData.playerPitch = pitch;
player.vehicleData.playerYaw = yaw;
float deltaXRot = player.yaw - player.lastYaw;
float deltaYRot = player.pitch - player.lastPitch;
@@ -3,9 +3,14 @@ package ac.grim.grimac.events.packets;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.checks.impl.elytra.ElytraA;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.data.Pair;
import ac.grim.grimac.utils.data.packetentity.JumpableEntity;
import ac.grim.grimac.utils.data.packetentity.PacketEntity;
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.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.WrapperPlayClientEntityAction;
@@ -43,8 +48,10 @@ public class PacketEntityAction extends PacketListenerAbstract {
player.isSneaking = false;
break;
case START_FLYING_WITH_ELYTRA:
if (PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_9)) return;
if (player.onGround || player.lastOnGround) {
player.getSetbackTeleportUtil().executeForceResync();
player.getSetbackTeleportUtil().executeNonSimulatingForceResync();
if (player.platformPlayer != null) {
// Client ignores sneaking, use it to resync
@@ -66,7 +73,7 @@ public class PacketEntityAction extends PacketListenerAbstract {
player.pointThreeEstimator.updatePlayerGliding();
} else {
// A client is flying with a ghost elytra, resync
player.getSetbackTeleportUtil().executeForceResync();
player.getSetbackTeleportUtil().executeNonSimulatingForceResync();
if (player.platformPlayer != null) {
// Client ignores sneaking, use it to resync
player.platformPlayer.setSneaking(!player.platformPlayer.isSneaking());
@@ -76,12 +83,10 @@ public class PacketEntityAction extends PacketListenerAbstract {
}
break;
case START_JUMPING_WITH_HORSE:
int jumpBoost = action.getJumpBoost();
if (jumpBoost < 0) jumpBoost = 0;
if (jumpBoost >= 90) {
player.vehicleData.nextHorseJump = 1;
} else {
player.vehicleData.nextHorseJump = 0.4F + 0.4F * jumpBoost / 90.0F;
PacketEntity riding = player.compensatedEntities.self.getRiding();
if (riding instanceof JumpableEntity jumpable) {
if (player.vehicleData.pendingJumps.size() >= 20) return; // discard
player.vehicleData.pendingJumps.add(new Pair<>(action.getJumpBoost(), jumpable));
}
break;
}
@@ -7,6 +7,7 @@ import ac.grim.grimac.checks.type.PacketCheck;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.LogUtil;
import ac.grim.grimac.utils.data.TrackerData;
import ac.grim.grimac.utils.data.packetentity.DashableEntity;
import ac.grim.grimac.utils.data.packetentity.PacketEntity;
import ac.grim.grimac.utils.data.packetentity.PacketEntityHook;
import ac.grim.grimac.utils.data.packetentity.PacketEntityTrackXRot;
@@ -152,13 +153,15 @@ public class PacketEntityReplication extends Check implements PacketCheck {
// 1.19.3+
else if (event.getPacketType() == PacketType.Play.Server.PLAYER_INFO_UPDATE) {
WrapperPlayServerPlayerInfoUpdate info = new WrapperPlayServerPlayerInfoUpdate(event);
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> {
for (WrapperPlayServerPlayerInfoUpdate.PlayerInfo entry : info.getEntries()) {
final UserProfile gameProfile = entry.getGameProfile();
final UUID uuid = gameProfile.getUUID();
player.compensatedEntities.profiles.put(uuid, gameProfile);
}
});
if (info.getActions().contains(WrapperPlayServerPlayerInfoUpdate.Action.ADD_PLAYER)) {
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> {
for (WrapperPlayServerPlayerInfoUpdate.PlayerInfo entry : info.getEntries()) {
final UserProfile gameProfile = entry.getGameProfile();
final UUID uuid = gameProfile.getUUID();
player.compensatedEntities.profiles.put(uuid, gameProfile);
}
});
}
} else if (event.getPacketType() == PacketType.Play.Server.PLAYER_INFO_REMOVE) {
WrapperPlayServerPlayerInfoRemove remove = new WrapperPlayServerPlayerInfoRemove(event);
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> remove.getProfileIds().forEach(player.compensatedEntities.profiles::remove));
@@ -377,10 +380,11 @@ public class PacketEntityReplication extends Check implements PacketCheck {
final int destroyTransaction = player.lastTransactionSent.get() + 1;
player.latencyUtils.addRealTimeTask(destroyTransaction, () -> {
for (int integer : destroyEntityIds) {
player.compensatedEntities.removeEntity(integer);
player.fireworks.removeFirework(integer);
player.compensatedEntities.entitiesRemovedThisTick.add(integer);
for (int entityId : destroyEntityIds) {
player.compensatedEntities.removeEntity(entityId);
player.dashableEntities.removeEntity(entityId);
player.fireworks.removeFirework(entityId);
player.compensatedEntities.entitiesRemovedThisTick.add(entityId);
}
});
@@ -522,7 +526,11 @@ public class PacketEntityReplication extends Check implements PacketCheck {
player.compensatedEntities.serverPositionsMap.put(entityID, new TrackerData(position.getX(), position.getY(), position.getZ(), xRot, yRot, type, player.lastTransactionSent.get()));
player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> {
player.compensatedEntities.addEntity(entityID, uuid, type, position, xRot, extraData);
PacketEntity entity = player.compensatedEntities.addEntity(entityID, uuid, type, position, xRot, extraData);
if (entity instanceof DashableEntity dashable) {
player.dashableEntities.addEntity(entityID, dashable);
}
if (entityMetadata != null) {
player.compensatedEntities.updateEntityMetadata(entityID, entityMetadata);
}
@@ -210,7 +210,9 @@ public class PacketPlayerRespawn extends PacketListenerAbstract {
}
player.pose = Pose.STANDING;
player.clientVelocity = new Vector3dm();
player.gamemode = respawn.getGameMode();
if (!GrimAPI.INSTANCE.getSpectateManager().isSpectating(player.uuid)) {
player.gamemode = respawn.getGameMode();
}
if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_17)) {
player.compensatedWorld.setDimension(respawn.getDimensionType(), event.getUser());
}
@@ -3,11 +3,11 @@ package ac.grim.grimac.events.packets;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.predictionengine.predictions.PredictionEngine;
import ac.grim.grimac.utils.math.Vec2;
import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox;
import ac.grim.grimac.utils.data.KnownInput;
import ac.grim.grimac.utils.data.packetentity.JumpableEntity;
import ac.grim.grimac.utils.data.packetentity.PacketEntity;
import ac.grim.grimac.utils.data.packetentity.PacketEntityHorse;
import ac.grim.grimac.utils.math.Vec2;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketListenerPriority;
@@ -98,7 +98,7 @@ public class PacketPlayerSteer extends PacketListenerAbstract {
if (player.packetStateData.receivedSteerVehicle && riding != null) {
// Horse and boat have first passenger in control
// If the player is the first passenger, disregard this attempt to have the server control the entity
if ((riding.isBoat || riding.isHappyGhast || (riding instanceof PacketEntityHorse horse && horse.hasSaddle())) &&
if ((riding.isBoat || riding.isHappyGhast || (riding instanceof JumpableEntity jumpable && jumpable.hasSaddle())) &&
riding.passengers.get(0) == player.compensatedEntities.self &&
// Although if the player has server controlled entities
player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9) &&
@@ -110,6 +110,7 @@ public class PacketPlayerSteer extends PacketListenerAbstract {
// Tick update
player.compensatedWorld.tickPlayerInPistonPushingArea();
player.compensatedEntities.tick();
player.dashableEntities.tick();
// Note for the movement check
player.vehicleData.lastDummy = true;
@@ -0,0 +1,42 @@
package ac.grim.grimac.events.packets;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.player.GrimPlayer;
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.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerFlying;
public class PacketPlayerTick extends PacketListenerAbstract {
public PacketPlayerTick() {
super(PacketListenerPriority.LOW);
}
@Override
public boolean isPreVia() {
return true;
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (event.getPacketType() == PacketType.Play.Client.CLIENT_TICK_END) {
GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser());
if (player == null || player.getClientVersion().isOlderThan(ClientVersion.V_1_21_2))
return;
PacketWorldBorder border = player.checkManager.getPacketCheck(PacketWorldBorder.class);
border.tickBorder();
} else if (WrapperPlayClientPlayerFlying.isFlying(event.getPacketType())) {
GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser());
if (player == null || player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_21_2))
return;
PacketWorldBorder border = player.checkManager.getPacketCheck(PacketWorldBorder.class);
border.tickBorder();
}
}
}
@@ -3,9 +3,15 @@ package ac.grim.grimac.events.packets;
import ac.grim.grimac.checks.Check;
import ac.grim.grimac.checks.type.PacketCheck;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.math.GrimMath;
import ac.grim.grimac.utils.worldborder.BorderExtent;
import ac.grim.grimac.utils.worldborder.RealTimeMovingBorderExtent;
import ac.grim.grimac.utils.worldborder.StaticBorderExtent;
import ac.grim.grimac.utils.worldborder.TickBasedMovingBorderExtent;
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.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerInitializeWorldBorder;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerWorldBorder;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerWorldBorderCenter;
@@ -19,20 +25,38 @@ public class PacketWorldBorder extends Check implements PacketCheck {
private double centerX;
@Getter
private double centerZ;
private double oldDiameter;
private double newDiameter;
@Getter
private double absoluteMaxSize;
private long startTime = 1;
private long endTime = 1;
@Getter
private BorderExtent extent;
public PacketWorldBorder(GrimPlayer playerData) {
super(playerData);
this.extent = new StaticBorderExtent(5.999997E7);
}
public double getCurrentDiameter() {
double d0 = (double) (System.currentTimeMillis() - startTime) / ((double) endTime - startTime);
return d0 < 1.0D ? GrimMath.lerp(d0, oldDiameter, newDiameter) : newDiameter;
return extent.size();
}
public double getMinX() {
return extent.getMinX(centerX, absoluteMaxSize);
}
public double getMaxX() {
return extent.getMaxX(centerX, absoluteMaxSize);
}
public double getMinZ() {
return extent.getMinZ(centerZ, absoluteMaxSize);
}
public double getMaxZ() {
return extent.getMaxZ(centerZ, absoluteMaxSize);
}
public void tickBorder() {
extent = extent.tick();
}
@Override
@@ -117,15 +141,28 @@ public class PacketWorldBorder extends Check implements PacketCheck {
@Contract(mutates = "this")
private void setSize(double size) {
oldDiameter = size;
newDiameter = size;
this.extent = new StaticBorderExtent(size);
}
@Contract(mutates = "this")
private void setLerp(double oldDiameter, double newDiameter, long length) {
this.oldDiameter = oldDiameter;
this.newDiameter = newDiameter;
this.startTime = System.currentTimeMillis();
this.endTime = this.startTime + length;
private void setLerp(double oldDiameter, double newDiameter, long speed) {
if (speed <= 0 || oldDiameter == newDiameter) {
this.extent = new StaticBorderExtent(newDiameter);
} else {
this.extent = createMovingExtent(oldDiameter, newDiameter, speed);
}
}
private static final boolean SERVER_TICK_BASED = PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_21_11);
private BorderExtent createMovingExtent(double from, double to, long speed) {
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_21_11)) { // tick-based
long durationTicks = SERVER_TICK_BASED ? speed : (speed / 50);
return new TickBasedMovingBorderExtent(from, to, durationTicks);
} else { // real-time based
long durationMs = SERVER_TICK_BASED ? (speed * 50) : speed;
return new RealTimeMovingBorderExtent(from, to, durationMs);
}
}
}
@@ -11,6 +11,7 @@ import ac.grim.grimac.platform.api.player.PlatformPlayer;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -155,6 +156,30 @@ public final class AlertManagerImpl implements AlertManager, ConfigReloadable, S
setPlayerStateAndNotify(requirePlatformPlayerFromUser(player), enabled, silent, AlertType.BRAND);
}
@Override
public boolean hasAlertsEnabled(Player player) {
if (player == null) return false;
return hasAlertsEnabled(GrimAPI.INSTANCE.getPlatformPlayerFactory().getFromNativePlayerType(player));
}
@Override
public void toggleAlerts(Player player) {
if (player == null) return;
toggleAlerts(GrimAPI.INSTANCE.getPlatformPlayerFactory().getFromNativePlayerType(player));
}
@Override
public boolean hasVerboseEnabled(Player player) {
if (player == null) return false;
return hasVerboseEnabled(GrimAPI.INSTANCE.getPlatformPlayerFactory().getFromNativePlayerType(player));
}
@Override
public void toggleVerbose(Player player) {
if (player == null) return;
toggleVerbose(GrimAPI.INSTANCE.getPlatformPlayerFactory().getFromNativePlayerType(player));
}
public void handlePlayerQuit(@Nullable PlatformPlayer platformPlayer) {
if (platformPlayer == null) return;
@@ -50,6 +50,7 @@ import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.predictionengine.GhostBlockDetector;
import ac.grim.grimac.predictionengine.SneakingEstimator;
import ac.grim.grimac.utils.anticheat.update.*;
import ac.grim.grimac.utils.latency.CompensatedCameraEntity;
import ac.grim.grimac.utils.latency.CompensatedCooldown;
import ac.grim.grimac.utils.latency.CompensatedFireworks;
import ac.grim.grimac.utils.latency.CompensatedInventory;
@@ -59,6 +60,8 @@ import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.google.common.collect.ClassToInstanceMap;
import com.google.common.collect.ImmutableClassToInstanceMap;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class CheckManager {
@@ -75,8 +78,18 @@ public class CheckManager {
private final ClassToInstanceMap<PostPredictionCheck> postPredictionChecks;
private PacketEntityReplication packetEntityReplication = null;
private final List<PacketCheck> packetChecksValues;
private final List<PositionCheck> positionChecksValues;
private final List<RotationCheck> rotationChecksValues;
private final List<VehicleCheck> vehicleChecksValues;
private final List<PacketCheck> prePredictionChecksValues;
private final List<BlockBreakCheck> blockBreakChecksValues;
private final List<BlockPlaceCheck> blockPlaceChecksValues;
private final List<PostPredictionCheck> postPredictionChecksValues;
public CheckManager(GrimPlayer player) {
packetChecks = new ImmutableClassToInstanceMap.Builder<PacketCheck>()
.put(CompensatedCameraEntity.class, player.cameraEntity)
.put(PacketOrderProcessor.class, player.packetOrderProcessor)
.put(Reach.class, new Reach(player))
.put(PacketEntityReplication.class, new PacketEntityReplication(player))
@@ -163,6 +176,7 @@ public class CheckManager {
.put(ExplosionHandler.class, new ExplosionHandler(player))
.put(KnockbackHandler.class, new KnockbackHandler(player))
.put(GhostBlockDetector.class, new GhostBlockDetector(player))
.put(InventoryD.class, new InventoryD(player))
.put(Phase.class, new Phase(player))
.put(Post.class, new Post(player))
.put(PacketOrderA.class, new PacketOrderA(player))
@@ -272,6 +286,15 @@ public class CheckManager {
.putAll(noneModules)
.build();
packetChecksValues = new ArrayList<>(packetChecks.values());
positionChecksValues = new ArrayList<>(positionChecks.values());
rotationChecksValues = new ArrayList<>(rotationChecks.values());
vehicleChecksValues = new ArrayList<>(vehicleChecks.values());
prePredictionChecksValues = new ArrayList<>(prePredictionChecks.values());
blockBreakChecksValues = new ArrayList<>(blockBreakChecks.values());
blockPlaceChecksValues = new ArrayList<>(blockPlaceChecks.values());
postPredictionChecksValues = new ArrayList<>(postPredictionChecks.values());
init();
}
@@ -296,103 +319,103 @@ public class CheckManager {
}
public void onPrePredictionReceivePacket(final PacketReceiveEvent packet) {
for (PacketCheck check : prePredictionChecks.values()) {
for (PacketCheck check : prePredictionChecksValues) {
check.onPacketReceive(packet);
}
}
public void onPacketReceive(final PacketReceiveEvent packet) {
for (PacketCheck check : packetChecks.values()) {
for (PacketCheck check : packetChecksValues) {
check.onPacketReceive(packet);
}
for (PostPredictionCheck check : postPredictionChecks.values()) {
for (PostPredictionCheck check : postPredictionChecksValues) {
check.onPacketReceive(packet);
}
for (BlockPlaceCheck check : blockPlaceChecks.values()) {
for (BlockPlaceCheck check : blockPlaceChecksValues) {
check.onPacketReceive(packet);
}
for (BlockBreakCheck check : blockBreakChecks.values()) {
for (BlockBreakCheck check : blockBreakChecksValues) {
check.onPacketReceive(packet);
}
}
public void onPacketSend(final PacketSendEvent packet) {
for (PacketCheck check : prePredictionChecks.values()) {
for (PacketCheck check : prePredictionChecksValues) {
check.onPacketSend(packet);
}
for (PacketCheck check : packetChecks.values()) {
for (PacketCheck check : packetChecksValues) {
check.onPacketSend(packet);
}
for (PostPredictionCheck check : postPredictionChecks.values()) {
for (PostPredictionCheck check : postPredictionChecksValues) {
check.onPacketSend(packet);
}
for (BlockPlaceCheck check : blockPlaceChecks.values()) {
for (BlockPlaceCheck check : blockPlaceChecksValues) {
check.onPacketSend(packet);
}
for (BlockBreakCheck check : blockBreakChecks.values()) {
for (BlockBreakCheck check : blockBreakChecksValues) {
check.onPacketSend(packet);
}
}
public void onPositionUpdate(final PositionUpdate position) {
for (PositionCheck check : positionChecks.values()) {
for (PositionCheck check : positionChecksValues) {
check.onPositionUpdate(position);
}
}
public void onRotationUpdate(final RotationUpdate rotation) {
for (RotationCheck check : rotationChecks.values()) {
for (RotationCheck check : rotationChecksValues) {
check.process(rotation);
}
for (BlockPlaceCheck check : blockPlaceChecks.values()) {
for (BlockPlaceCheck check : blockPlaceChecksValues) {
check.process(rotation);
}
}
public void onVehiclePositionUpdate(final VehiclePositionUpdate update) {
for (VehicleCheck check : vehicleChecks.values()) {
for (VehicleCheck check : vehicleChecksValues) {
check.process(update);
}
}
public void onPredictionFinish(final PredictionComplete complete) {
for (PostPredictionCheck check : postPredictionChecks.values()) {
for (PostPredictionCheck check : postPredictionChecksValues) {
check.onPredictionComplete(complete);
}
for (BlockPlaceCheck check : blockPlaceChecks.values()) {
for (BlockPlaceCheck check : blockPlaceChecksValues) {
check.onPredictionComplete(complete);
}
for (BlockBreakCheck check : blockBreakChecks.values()) {
for (BlockBreakCheck check : blockBreakChecksValues) {
check.onPredictionComplete(complete);
}
}
public void onBlockPlace(final BlockPlace place) {
for (BlockPlaceCheck check : blockPlaceChecks.values()) {
for (BlockPlaceCheck check : blockPlaceChecksValues) {
check.onBlockPlace(place);
}
}
public void onPostFlyingBlockPlace(final BlockPlace place) {
for (BlockPlaceCheck check : blockPlaceChecks.values()) {
for (BlockPlaceCheck check : blockPlaceChecksValues) {
check.onPostFlyingBlockPlace(place);
}
}
public void onBlockBreak(final BlockBreak blockBreak) {
for (BlockBreakCheck check : blockBreakChecks.values()) {
for (BlockBreakCheck check : blockBreakChecksValues) {
check.onBlockBreak(blockBreak);
}
for (BlockPlaceCheck check : blockPlaceChecks.values()) {
for (BlockPlaceCheck check : blockPlaceChecksValues) {
check.onBlockBreak(blockBreak);
}
}
public void onPostFlyingBlockBreak(final BlockBreak blockBreak) {
for (BlockBreakCheck check : blockBreakChecks.values()) {
for (BlockBreakCheck check : blockBreakChecksValues) {
check.onPostFlyingBlockBreak(blockBreak);
}
for (BlockPlaceCheck check : blockPlaceChecks.values()) {
for (BlockPlaceCheck check : blockPlaceChecksValues) {
check.onPostFlyingBlockBreak(blockBreak);
}
}
@@ -7,6 +7,7 @@ import ac.grim.grimac.manager.init.start.StartableInitable;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.LogUtil;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import ac.grim.grimac.utils.data.Pair;
import ac.grim.grimac.utils.webhook.*;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
@@ -21,16 +22,17 @@ import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import java.util.regex.Pattern;
public class DiscordManager implements StartableInitable, ReloadableInitable {
private static final Predicate<String> WEBHOOK_REGEX = Pattern.compile("https://discord.com/api/webhooks/\\d+/[\\w-]+").asMatchPredicate();
private static final Predicate<String> WEBHOOK_REGEX = Pattern.compile("^https://discord\\.com/api(?:/v\\d+)?/webhooks/\\d+/[\\w-]+(\\?thread_id=\\d+)?$").asMatchPredicate();
private static final Duration timeout = Duration.ofSeconds(15);
private static final HttpClient client = HttpClient.newBuilder().connectTimeout(timeout).build();
private static final ConcurrentLinkedDeque<HttpRequest> requests = new ConcurrentLinkedDeque<>();
private static final ConcurrentLinkedDeque<Pair<HttpRequest, CompletableFuture<Boolean>>> requests = new ConcurrentLinkedDeque<>();
private static final AtomicBoolean taskStarted = new AtomicBoolean();
private static final AtomicBoolean sending = new AtomicBoolean();
private static long rateLimitedUntil;
@@ -120,7 +122,7 @@ public class DiscordManager implements StartableInitable, ReloadableInitable {
}
public void sendAlert(GrimPlayer player, String verbose, String checkName, int violations) {
if (url == null) {
if (isDisabled()) {
return;
}
@@ -148,23 +150,36 @@ public class DiscordManager implements StartableInitable, ReloadableInitable {
sendWebhookMessage(new WebhookMessage().addEmbeds(embed));
}
public void sendWebhookMessage(WebhookMessage message) {
requests.add(HttpRequest.newBuilder()
public CompletableFuture<Boolean> sendWebhookMessage(WebhookMessage message) {
if (isDisabled()) return CompletableFuture.completedFuture(false);
HttpRequest request = HttpRequest.newBuilder()
.uri(url)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(message.toJson().toString()))
.timeout(timeout)
.build());
.build();
CompletableFuture<Boolean> future = new CompletableFuture<>();
requests.add(new Pair<>(request, future));
if (!taskStarted.getAndSet(true)) {
// there's probably a better way to handle rate limits, but this works, so whatever.
GrimAPI.INSTANCE.getScheduler().getAsyncScheduler().runAtFixedRate(GrimAPI.INSTANCE.getGrimPlugin(), DiscordManager::tick, 0, 1);
}
return future;
}
public boolean isDisabled() {
return url == null;
}
private static void tick() {
HttpRequest request = requests.peek();
if (request != null && rateLimitedUntil < System.currentTimeMillis() && !sending.getAndSet(true)) {
Pair<HttpRequest, CompletableFuture<Boolean>> pair = requests.peek();
if (pair != null && rateLimitedUntil < System.currentTimeMillis() && !sending.getAndSet(true)) {
HttpRequest request = pair.first();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).whenComplete((response, throwable) -> {
if (throwable != null) {
sending.set(false);
@@ -178,12 +193,15 @@ public class DiscordManager implements StartableInitable, ReloadableInitable {
return;
}
requests.remove(request);
requests.remove(pair);
sending.set(false);
// TODO: handle 503 (Service Unavailable)?
if (response != null && response.statusCode() >= 400) {
LogUtil.error("Encountered status code " + response.statusCode() + " with body " + response.body() + " and headers " + response.headers().map() + " while sending a Discord webhook alert.");
pair.second().complete(false);
} else {
pair.second().complete(true);
}
});
}
@@ -7,15 +7,12 @@ import ac.grim.grimac.manager.init.load.PacketEventsInit;
import ac.grim.grimac.manager.init.start.*;
import ac.grim.grimac.manager.init.stop.StoppableInitable;
import ac.grim.grimac.manager.init.stop.TerminatePacketEvents;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.utils.anticheat.LogUtil;
import com.github.retrooper.packetevents.PacketEventsAPI;
import com.google.common.collect.ImmutableList;
import lombok.Getter;
import org.incendo.cloud.CommandManager;
import java.util.ArrayList;
import java.util.function.Supplier;
public class InitManager {
@@ -30,7 +27,7 @@ public class InitManager {
@Getter
private boolean stopped = false;
public InitManager(PacketEventsAPI<?> packetEventsAPI, Supplier<CommandManager<Sender>> commandManager, Initable... platformSpecificInitables) {
public InitManager(PacketEventsAPI<?> packetEventsAPI, Initable... platformSpecificInitables) {
ArrayList<LoadableInitable> extraLoadableInitables = new ArrayList<>();
ArrayList<StartableInitable> extraStartableInitables = new ArrayList<>();
ArrayList<StoppableInitable> extraStoppableInitables = new ArrayList<>();
@@ -51,7 +48,8 @@ public class InitManager {
.add(new PacketManager())
.add(new ViaBackwardsManager())
.add(new TickRunner())
.add(new CommandRegister(commandManager))
.add(new CommandRegister(GrimAPI.INSTANCE.getCommandService()))
.add(new UpdateChecker())
.add(new PacketLimiter())
.add(GrimAPI.INSTANCE.getAlertManager())
.add(GrimAPI.INSTANCE.getDiscordManager())
@@ -40,6 +40,7 @@ import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSe
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.HashSet;
@@ -100,7 +101,7 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
blockMovementsUntilResync(true, true);
}
public void executeForceResyncNoSimulation() {
public void executeNonSimulatingForceResync() {
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
@@ -144,9 +145,11 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
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
}
// Prevent abusing setbacks to move out of blocks like webs
vector.multiply(player.stuckSpeedMultiplier);
// 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))));
}
@@ -170,12 +173,14 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
VelocityData futureExplosion = player.checkManager.getExplosionHandler().getFutureExplosion();
// Velocity sets
if (futureKb.first() != null) {
// Don't let player reuse setback velocity
if (futureKb.first() != null && !futureKb.first().isSetback) {
clientVel = futureKb.second();
}
// Explosion adds
if (futureExplosion != null && (futureKb.first() == null || futureKb.first().transaction < futureExplosion.transaction)) {
if (futureExplosion != null && (futureKb.first() == null
|| (futureKb.first().transaction < futureExplosion.transaction && !futureKb.first().isSetback))) {
clientVel.add(futureExplosion.vector);
}
@@ -215,7 +220,7 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
blockOffsets = true;
}
SetBackData data = new SetBackData(new TeleportData(position, new Vector3d(), new RelativeFlag(0b11000), player.lastTransactionSent.get(), 0), player.yaw, player.pitch, clientVel, player.inVehicle(), false);
SetBackData data = new SetBackData(new TeleportData(position, null, RelativeFlag.YAW.or(RelativeFlag.PITCH), player.lastTransactionSent.get(), 0), player.yaw, player.pitch, clientVel, player.inVehicle(), false);
sendSetback(data);
}
@@ -267,7 +272,8 @@ 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(new Location(null, position.getX(), y, position.getZ(), player.yaw % 360, player.pitch % 360), new Vector3d(), data.getTeleportData().getTransaction(), new RelativeFlag(0b11000), false, teleportId);
addSentTeleport(new Location(null, position.getX(), y, position.getZ(), player.yaw % 360, player.pitch % 360),
null, data.getTeleportData().getTransaction(), RelativeFlag.YAW.or(RelativeFlag.PITCH), 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
@@ -382,7 +388,7 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
private boolean isPendingSetback() {
// Relative setbacks shouldn't count
if (requiredSetBack.getTeleportData().isRelativeX() || requiredSetBack.getTeleportData().isRelativeY() || requiredSetBack.getTeleportData().isRelativeZ()) {
if (requiredSetBack != null && (requiredSetBack.getTeleportData().isRelativeX() || requiredSetBack.getTeleportData().isRelativeY() || requiredSetBack.getTeleportData().isRelativeZ())) {
return false;
}
// The setback is not complete
@@ -403,7 +409,12 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
!player.getSetbackTeleportUtil().hasAcceptedSpawnTeleport);
}
public void addSentTeleport(Location position, Vector3d velocity, int transaction, RelativeFlag flags, boolean plugin, int teleportId) {
public void addSentTeleport(Location position, @Nullable Vector3d velocity, int transaction, RelativeFlag flags, boolean plugin, int teleportId) {
// Clients below 1.21.2 do not have this.
if (player.getClientVersion().isOlderThan(ClientVersion.V_1_21_2)) {
velocity = null;
}
TeleportData data = new TeleportData(new Vector3d(position.getX(), position.getY(), position.getZ()), velocity, flags, transaction, teleportId);
pendingTeleports.add(data);
@@ -422,7 +433,7 @@ public class SetbackTeleportUtil extends Check implements PostPredictionCheck {
safePosition = safePosition.withZ(safePosition.getZ() + lastKnownGoodPosition.pos.getZ());
}
data = new TeleportData(safePosition, velocity, new RelativeFlag(0b11000), transaction, teleportId);
data = new TeleportData(safePosition, velocity, RelativeFlag.YAW.or(RelativeFlag.PITCH), transaction, teleportId);
requiredSetBack = new SetBackData(data, player.yaw, player.pitch, null, false, plugin);
this.lastKnownGoodPosition = new SetbackPosWithVector(safePosition, new Vector3dm());
@@ -23,6 +23,14 @@ public class BaseConfigManager {
@Getter
private String prefix = "&bGrim &8»";
@Getter
private String webhookNotEnabled;
@Getter
private String webhookTestMessage;
@Getter
private String webhookTestSucceeded;
@Getter
private String webhookTestFailed;
@Getter
private String disconnectTimeout;
@Getter
private String disconnectClosed;
@@ -57,6 +65,10 @@ public class BaseConfigManager {
printAlertsToConsole = config.getBooleanElse("alerts.print-to-console", true);
prefix = config.getStringElse("prefix", "&bGrim &8»");
webhookNotEnabled = config.getStringElse("webhook-not-enabled", "Discord webhooks are not enabled!");
webhookTestMessage = config.getStringElse("webhook-test-message", "test message");
webhookTestSucceeded = config.getStringElse("webhook-test-succeeded", "Discord webhook test succeeded!");
webhookTestFailed = config.getStringElse("webhook-test-failed", "Discord webhook test failed!");
disconnectTimeout = config.getStringElse("disconnect.timeout", "<lang:disconnect.timeout>");
disconnectClosed = config.getStringElse("disconnect.closed", "<lang:disconnect.timeout>");
disconnectPacketError = config.getStringElse("disconnect.error", "<red>An error occurred whilst processing packets. Please contact the administrators.");
@@ -1,82 +1,19 @@
package ac.grim.grimac.manager.init.start;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.command.SenderRequirement;
import ac.grim.grimac.command.commands.*;
import ac.grim.grimac.command.handler.GrimCommandFailureHandler;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.utils.anticheat.MessageUtil;
import io.leangen.geantyref.TypeToken;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.format.NamedTextColor;
import org.incendo.cloud.CommandManager;
import org.incendo.cloud.exception.InvalidSyntaxException;
import org.incendo.cloud.key.CloudKey;
import org.incendo.cloud.processors.requirements.RequirementApplicable;
import org.incendo.cloud.processors.requirements.RequirementApplicable.RequirementApplicableFactory;
import org.incendo.cloud.processors.requirements.RequirementPostprocessor;
import org.incendo.cloud.processors.requirements.Requirements;
import java.util.function.Function;
import java.util.function.Supplier;
@RequiredArgsConstructor
public class CommandRegister implements StartableInitable {
public static final CloudKey<Requirements<Sender, SenderRequirement>> REQUIREMENT_KEY
= CloudKey.of("requirements", new TypeToken<>() {});
public static final RequirementApplicableFactory<Sender, SenderRequirement> REQUIREMENT_FACTORY
= RequirementApplicable.factory(REQUIREMENT_KEY);
private static boolean commandsRegistered = false;
private final Supplier<CommandManager<Sender>> commandManagerSupplier;
// Public static method that can be called on platforms where command must be registered earlier than InitManager.load()
public static void registerCommands(CommandManager<Sender> commandManager) {
if (commandsRegistered) return;
new GrimPerf().register(commandManager);
new GrimDebug().register(commandManager);
new GrimAlerts().register(commandManager);
new GrimProfile().register(commandManager);
new GrimSendAlert().register(commandManager);
new GrimHelp().register(commandManager);
new GrimHistory().register(commandManager);
new GrimReload().register(commandManager);
new GrimSpectate().register(commandManager);
new GrimStopSpectating().register(commandManager);
new GrimLog().register(commandManager);
new GrimVerbose().register(commandManager);
new GrimVersion().register(commandManager);
new GrimDump().register(commandManager);
new GrimBrands().register(commandManager);
new GrimList().register(commandManager);
final RequirementPostprocessor<Sender, SenderRequirement>
senderRequirementPostprocessor = RequirementPostprocessor.of(
REQUIREMENT_KEY,
new GrimCommandFailureHandler()
);
commandManager.registerCommandPostProcessor(senderRequirementPostprocessor);
registerExceptionHandler(commandManager, InvalidSyntaxException.class, e -> MessageUtil.miniMessage(e.correctSyntax()));
commandsRegistered = true;
}
protected static <E extends Exception> void registerExceptionHandler(CommandManager<Sender> commandManager, Class<E> ex, Function<E, ComponentLike> toComponent) {
commandManager.exceptionController().registerHandler(ex,
(c) -> c.context().sender().sendMessage(toComponent.apply(c.exception()).asComponent().colorIfAbsent(NamedTextColor.RED))
);
}
import ac.grim.grimac.platform.api.command.CommandService;
import ac.grim.grimac.utils.anticheat.LogUtil;
public record CommandRegister(CommandService service) implements StartableInitable {
@Override
public void start() {
CommandManager<Sender> commandManager = commandManagerSupplier.get();
registerCommands(commandManager);
if (GrimAPI.INSTANCE.getConfigManager().getConfig().getBooleanElse("check-for-updates", true)) {
GrimVersion.checkForUpdatesAsync(GrimAPI.INSTANCE.getPlatformServer().getConsoleSender());
try {
if (service != null) {
service.registerCommands();
}
} catch (Throwable t) {
// This is the ultimate safety net. If command registration fails, Grim keeps running.
LogUtil.error("Failed to register commands! Grim will run without command support.", t);
}
}
}
@@ -24,6 +24,7 @@ public class PacketManager implements StartableInitable {
PacketEvents.getAPI().getEventManager().registerListener(new PacketServerTeleport());
PacketEvents.getAPI().getEventManager().registerListener(new PacketPlayerCooldown());
PacketEvents.getAPI().getEventManager().registerListener(new PacketPlayerRespawn());
PacketEvents.getAPI().getEventManager().registerListener(new PacketPlayerTick());
PacketEvents.getAPI().getEventManager().registerListener(new CheckManagerListener());
PacketEvents.getAPI().getEventManager().registerListener(new PacketPlayerSteer());
@@ -0,0 +1,13 @@
package ac.grim.grimac.manager.init.start;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.command.commands.GrimVersion;
public class UpdateChecker implements StartableInitable {
@Override
public void start() {
if (GrimAPI.INSTANCE.getConfigManager().getConfig().getBooleanElse("check-for-updates", true)) {
GrimVersion.checkForUpdatesAsync(GrimAPI.INSTANCE.getPlatformServer().getConsoleSender());
}
}
}
@@ -121,10 +121,13 @@ public class DefaultResyncHandler implements ResyncHandler {
final int blockId = world.getChunkAt(chunkX, chunkZ).getBlockID(x & 15, y, z & 15);
player.user.sendPacket(new WrapperPlayServerBlockChange(new Vector3i(x, y, z), blockId));
if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_19)) { // Via will handle this for us pre-1.19
player.user.sendPacket(new WrapperPlayServerAcknowledgeBlockChanges(sequence)); // Make 1.19 clients apply the changes
}
player.runSafely(() -> {
player.user.sendPacket(new WrapperPlayServerBlockChange(new Vector3i(x, y, z), blockId));
if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_19)) { // Via will handle this for us pre-1.19
player.user.sendPacket(new WrapperPlayServerAcknowledgeBlockChanges(sequence)); // Make 1.19 clients apply the changes
}
});
});
}
@@ -0,0 +1,13 @@
package ac.grim.grimac.manager.player.handlers;
import ac.grim.grimac.api.handler.ResyncHandler;
public class NoOpResyncHandler implements ResyncHandler {
public static final NoOpResyncHandler INSTANCE = new NoOpResyncHandler();
@Override
public void resync(int minBlockX, int minBlockY, int minBlockZ, int maxBlockX, int maxBlockY, int maxBlockZ) {}
@Override public void resyncPosition(int x, int y, int z, int sequence) {}
}
@@ -6,6 +6,7 @@ import ac.grim.grimac.api.plugin.GrimPlugin;
import ac.grim.grimac.manager.init.ReloadableInitable;
import ac.grim.grimac.manager.init.start.StartableInitable;
import ac.grim.grimac.manager.violationdatabase.mysql.MySQLViolationDatabase;
import ac.grim.grimac.manager.violationdatabase.postgresql.PostgresqlViolationDatabase;
import ac.grim.grimac.manager.violationdatabase.sqlite.SQLiteViolationDatabase;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.LogUtil;
@@ -57,7 +58,7 @@ public class ViolationDatabaseManager implements StartableInitable, ReloadableIn
} catch (ClassNotFoundException e) {
LogUtil.error(
"""
Could not load SQLite driver for /grim history database.
IMPORTANT: Could not load SQLite driver for /grim history database.
Download the minecraft-sqlite-jdbc mod/plugin for SQLite support, or change history.database.type
Alternatively set history.enabled=false to remove this message if /grim history support is not desired"""
);
@@ -72,10 +73,11 @@ public class ViolationDatabaseManager implements StartableInitable, ReloadableIn
}
case "MYSQL" -> {
String host = cfg.getStringElse("history.database.host", "localhost:3306");
String db = cfg.getStringElse("history.database.database", "grimac");
int port = cfg.getIntElse("history.database.port", 3306);
String host = cfg.getStringElse("history.database.host", "localhost") + ":" + port;
String db = cfg.getStringElse("history.database.database", "grimac");
String user = cfg.getStringElse("history.database.username", "root");
String pwd = cfg.getStringElse("history.database.password", "password");
String pwd = cfg.getStringElse("history.database.password", "password");
if (database instanceof MySQLViolationDatabase mysql
&& mysql.sameConfig(host, db, user, pwd)) {
@@ -93,6 +95,29 @@ public class ViolationDatabaseManager implements StartableInitable, ReloadableIn
}
}
case "POSTGRESQL" -> {
int port = cfg.getIntElse("history.database.port", 3306);
String host = cfg.getStringElse("history.database.host", "localhost") + ":" + port;
String db = cfg.getStringElse("history.database.database", "grimac");
String user = cfg.getStringElse("history.database.username", "root");
String pwd = cfg.getStringElse("history.database.password", "password");
if (database instanceof PostgresqlViolationDatabase postgresql
&& postgresql.sameConfig(host, db, user, pwd)) {
break; // nothing changed → keep pool
}
database.disconnect();
database = new PostgresqlViolationDatabase(host, db, user, pwd);
try {
database.connect();
loaded = true;
} catch (SQLException e) {
LogUtil.error(e);
this.database = NoOpViolationDatabase.INSTANCE;
loaded = false;
}
}
default -> { // NOOP or invalid
if (!(database instanceof NoOpViolationDatabase)) {
database.disconnect();
@@ -115,5 +140,4 @@ public class ViolationDatabaseManager implements StartableInitable, ReloadableIn
public List<Violation> getViolations(UUID player, int page, int limit) {
return database.getViolations(player, page, limit);
}
}
@@ -0,0 +1,31 @@
package ac.grim.grimac.manager.violationdatabase.postgresql;
import ac.grim.grimac.manager.violationdatabase.DatabaseDialect;
public class PostgresqlDialect implements DatabaseDialect {
@Override
public String getUuidColumnType() {
return "UUID";
}
@Override
public String getAutoIncrementPrimaryKeySyntax() {
return "BIGSERIAL PRIMARY KEY";
}
@Override
public String getInsertOrIgnoreSyntax(String tableName, String columnNames) {
return "INSERT INTO " + tableName + " (" + columnNames + ") VALUES (?) ON CONFLICT DO NOTHING";
}
@Override
public String getUniqueConstraintViolationSQLState() {
return "23505"; // Postgresql duplicate key error
}
@Override
public int getUniqueConstraintViolationErrorCode() {
return 0; // Postgresql is not using numbers
}
}
@@ -0,0 +1,286 @@
package ac.grim.grimac.manager.violationdatabase.postgresql;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.manager.violationdatabase.*;
import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.utils.anticheat.LogUtil;
import com.github.retrooper.packetevents.PacketEvents;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
public class PostgresqlViolationDatabase implements ViolationDatabase {
private static String quoteVerboseColumn() {
return "\"" + DatabaseConstants.VIOLATIONS_VERBOSE_COLUMN + "\"";
}
private HikariDataSource dataSource;
private final DatabaseDialect dialect;
public PostgresqlViolationDatabase(String url, String database, String username, String password) {
this.dialect = new PostgresqlDialect();
setupDataSource(url, database, username, password);
}
private void setupDataSource(String url, String database, String username, String password) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://" + url + "/" + database);
config.setUsername(username);
config.setPassword(password);
config.setMaximumPoolSize(10);
config.setAutoCommit(true);
dataSource = new HikariDataSource(config);
}
@Override
public void connect() throws SQLException {
try (Connection connection = dataSource.getConnection()) {
String pkSyntax = dialect.getAutoIncrementPrimaryKeySyntax();
String uuidType = dialect.getUuidColumnType();
// 1. Create Lookup Table for Server Names
connection.prepareStatement(
"CREATE TABLE IF NOT EXISTS " + DatabaseConstants.SERVERS_TABLE + "(" +
"id " + pkSyntax + ", " +
DatabaseConstants.SERVERS_STRING_COLUMN + " VARCHAR(255) NOT NULL UNIQUE" +
")"
).execute();
connection.prepareStatement(
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.SERVERS_TABLE + "_name ON " + DatabaseConstants.SERVERS_TABLE + "(" + DatabaseConstants.SERVERS_STRING_COLUMN + ");"
).execute();
// 2. Create Lookup Table for Check Names
connection.prepareStatement(
"CREATE TABLE IF NOT EXISTS " + DatabaseConstants.CHECK_NAMES_TABLE + "(" +
"id " + pkSyntax + ", " +
DatabaseConstants.CHECK_NAMES_STRING_COLUMN + " VARCHAR(255) NOT NULL UNIQUE" +
")"
).execute();
connection.prepareStatement(
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.CHECK_NAMES_TABLE + "_string ON " + DatabaseConstants.CHECK_NAMES_TABLE + "(" + DatabaseConstants.CHECK_NAMES_STRING_COLUMN + ");"
).execute();
// --- NEW LOOKUP TABLES ---
// 3. Create Lookup Table for Grim Versions
connection.prepareStatement(
"CREATE TABLE IF NOT EXISTS " + DatabaseConstants.GRIM_VERSIONS_TABLE + "(" +
"id " + pkSyntax + ", " +
DatabaseConstants.GRIM_VERSIONS_STRING_COLUMN + " VARCHAR(255) NOT NULL UNIQUE" +
")"
).execute();
connection.prepareStatement(
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.GRIM_VERSIONS_TABLE + "_string ON " + DatabaseConstants.GRIM_VERSIONS_TABLE + "(" + DatabaseConstants.GRIM_VERSIONS_STRING_COLUMN + ");"
).execute();
// 4. Create Lookup Table for Client Brands
connection.prepareStatement(
"CREATE TABLE IF NOT EXISTS " + DatabaseConstants.CLIENT_BRANDS_TABLE + "(" +
"id " + pkSyntax + ", " +
DatabaseConstants.CLIENT_BRANDS_STRING_COLUMN + " VARCHAR(255) NOT NULL UNIQUE" +
")"
).execute();
connection.prepareStatement(
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.CLIENT_BRANDS_TABLE + "_string ON " + DatabaseConstants.CLIENT_BRANDS_TABLE + "(" + DatabaseConstants.CLIENT_BRANDS_STRING_COLUMN + ");"
).execute();
// 5. Create Lookup Table for Client Versions
connection.prepareStatement(
"CREATE TABLE IF NOT EXISTS " + DatabaseConstants.CLIENT_VERSIONS_TABLE + "(" +
"id " + pkSyntax + ", " +
DatabaseConstants.CLIENT_VERSIONS_STRING_COLUMN + " VARCHAR(255) NOT NULL UNIQUE" +
")"
).execute();
connection.prepareStatement(
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.CLIENT_VERSIONS_TABLE + "_string ON " + DatabaseConstants.CLIENT_VERSIONS_TABLE + "(" + DatabaseConstants.CLIENT_VERSIONS_STRING_COLUMN + ");"
).execute();
// 6. Create Lookup Table for Server Versions
connection.prepareStatement(
"CREATE TABLE IF NOT EXISTS " + DatabaseConstants.SERVER_VERSIONS_TABLE + "(" +
"id " + pkSyntax + ", " +
DatabaseConstants.SERVER_VERSIONS_STRING_COLUMN + " VARCHAR(255) NOT NULL UNIQUE" +
")"
).execute();
connection.prepareStatement(
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.SERVER_VERSIONS_TABLE + "_string ON " + DatabaseConstants.SERVER_VERSIONS_TABLE + "(" + DatabaseConstants.SERVER_VERSIONS_STRING_COLUMN + ");"
).execute();
// --- END NEW LOOKUP TABLES ---
// 7. Create Main Violations Table with ALL Foreign Keys and optimized UUID
connection.prepareStatement(
"CREATE TABLE IF NOT EXISTS " + DatabaseConstants.VIOLATIONS_TABLE + "(" +
"id " + pkSyntax + ", " +
DatabaseConstants.VIOLATIONS_SERVER_ID_COLUMN + " BIGINT NOT NULL, " +
DatabaseConstants.VIOLATIONS_UUID_COLUMN + " " + uuidType + " NOT NULL, " +
DatabaseConstants.VIOLATIONS_CHECK_NAME_ID_COLUMN + " BIGINT NOT NULL, " +
quoteVerboseColumn() + " TEXT NOT NULL, " +
DatabaseConstants.VIOLATIONS_VL_COLUMN + " INT NOT NULL, " +
DatabaseConstants.VIOLATIONS_CREATED_AT_COLUMN + " BIGINT NOT NULL, " +
DatabaseConstants.VIOLATIONS_GRIM_VERSION_ID_COLUMN + " BIGINT NOT NULL, " + // NEW
DatabaseConstants.VIOLATIONS_CLIENT_BRAND_ID_COLUMN + " BIGINT NOT NULL, " + // NEW
DatabaseConstants.VIOLATIONS_CLIENT_VERSION_ID_COLUMN + " BIGINT NOT NULL, " + // NEW
DatabaseConstants.VIOLATIONS_SERVER_VERSION_ID_COLUMN + " BIGINT NOT NULL, " + // NEW
"FOREIGN KEY (" + DatabaseConstants.VIOLATIONS_SERVER_ID_COLUMN + ") REFERENCES " + DatabaseConstants.SERVERS_TABLE + "(id), " +
"FOREIGN KEY (" + DatabaseConstants.VIOLATIONS_CHECK_NAME_ID_COLUMN + ") REFERENCES " + DatabaseConstants.CHECK_NAMES_TABLE + "(id), " +
"FOREIGN KEY (" + DatabaseConstants.VIOLATIONS_GRIM_VERSION_ID_COLUMN + ") REFERENCES " + DatabaseConstants.GRIM_VERSIONS_TABLE + "(id), " + // NEW
"FOREIGN KEY (" + DatabaseConstants.VIOLATIONS_CLIENT_BRAND_ID_COLUMN + ") REFERENCES " + DatabaseConstants.CLIENT_BRANDS_TABLE + "(id), " + // NEW
"FOREIGN KEY (" + DatabaseConstants.VIOLATIONS_CLIENT_VERSION_ID_COLUMN + ") REFERENCES " + DatabaseConstants.CLIENT_VERSIONS_TABLE + "(id), " + // NEW
"FOREIGN KEY (" + DatabaseConstants.VIOLATIONS_SERVER_VERSION_ID_COLUMN + ") REFERENCES " + DatabaseConstants.SERVER_VERSIONS_TABLE + "(id)" + // NEW
")"
).execute();
// 8. Create Indexes for efficient querying on main table (includes new FKs)
connection.prepareStatement(
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_uuid ON " + DatabaseConstants.VIOLATIONS_TABLE + "(" + DatabaseConstants.VIOLATIONS_UUID_COLUMN + ");"
).execute();
connection.prepareStatement(
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_created_at ON " + DatabaseConstants.VIOLATIONS_TABLE + "(" + DatabaseConstants.VIOLATIONS_CREATED_AT_COLUMN + ");"
).execute();
connection.prepareStatement(
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_server_id ON " + DatabaseConstants.VIOLATIONS_TABLE + "(" + DatabaseConstants.VIOLATIONS_SERVER_ID_COLUMN + ");"
).execute();
connection.prepareStatement(
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_check_name_id ON " + DatabaseConstants.VIOLATIONS_TABLE + "(" + DatabaseConstants.VIOLATIONS_CHECK_NAME_ID_COLUMN + ");"
).execute();
connection.prepareStatement(
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_grim_version_id ON " + DatabaseConstants.VIOLATIONS_TABLE + "(" + DatabaseConstants.VIOLATIONS_GRIM_VERSION_ID_COLUMN + ");" // NEW
).execute();
connection.prepareStatement(
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_client_brand_id ON " + DatabaseConstants.VIOLATIONS_TABLE + "(" + DatabaseConstants.VIOLATIONS_CLIENT_BRAND_ID_COLUMN + ");" // NEW
).execute();
connection.prepareStatement(
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_client_version_id ON " + DatabaseConstants.VIOLATIONS_TABLE + "(" + DatabaseConstants.VIOLATIONS_CLIENT_VERSION_ID_COLUMN + ");" // NEW
).execute();
connection.prepareStatement(
"CREATE INDEX IF NOT EXISTS idx_" + DatabaseConstants.VIOLATIONS_TABLE + "_server_version_id ON " + DatabaseConstants.VIOLATIONS_TABLE + "(" + DatabaseConstants.VIOLATIONS_SERVER_VERSION_ID_COLUMN + ");" // NEW
).execute();
} catch (SQLException ex) {
LogUtil.error("Failed to generate violations database:", ex);
throw ex;
}
}
@Override
// Updated method signature to accept all new parameters
public synchronized void logAlert(GrimPlayer player, String grimVersion, String verbose, String checkName, int vls) {
try (Connection connection = dataSource.getConnection();
// Updated INSERT statement with all new columns
PreparedStatement insertAlert = connection.prepareStatement(
"INSERT INTO " + DatabaseConstants.VIOLATIONS_TABLE + " (" +
DatabaseConstants.VIOLATIONS_SERVER_ID_COLUMN + ", " +
DatabaseConstants.VIOLATIONS_UUID_COLUMN + ", " +
DatabaseConstants.VIOLATIONS_CHECK_NAME_ID_COLUMN + ", " +
quoteVerboseColumn() + ", " +
DatabaseConstants.VIOLATIONS_VL_COLUMN + ", " +
DatabaseConstants.VIOLATIONS_CREATED_AT_COLUMN + ", " +
DatabaseConstants.VIOLATIONS_GRIM_VERSION_ID_COLUMN + ", " + // NEW
DatabaseConstants.VIOLATIONS_CLIENT_BRAND_ID_COLUMN + ", " + // NEW
DatabaseConstants.VIOLATIONS_CLIENT_VERSION_ID_COLUMN + ", " + // NEW
DatabaseConstants.VIOLATIONS_SERVER_VERSION_ID_COLUMN + // NEW
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" // Total 10 parameters now
)
) {
// Get or create IDs for all deduplicated strings
String serverName = GrimAPI.INSTANCE.getConfigManager().getConfig().getStringElse("history.server-name", "Prison");
long serverId = DatabaseUtils.getOrCreateId(connection, dialect, DatabaseConstants.SERVERS_TABLE, DatabaseConstants.SERVERS_STRING_COLUMN, serverName);
long checkNameId = DatabaseUtils.getOrCreateId(connection, dialect, DatabaseConstants.CHECK_NAMES_TABLE, DatabaseConstants.CHECK_NAMES_STRING_COLUMN, checkName);
long grimVersionId = DatabaseUtils.getOrCreateId(connection, dialect, DatabaseConstants.GRIM_VERSIONS_TABLE, DatabaseConstants.GRIM_VERSIONS_STRING_COLUMN, grimVersion);
long clientBrandId = DatabaseUtils.getOrCreateId(connection, dialect, DatabaseConstants.CLIENT_BRANDS_TABLE, DatabaseConstants.CLIENT_BRANDS_STRING_COLUMN, player.getBrand());
long clientVersionId = DatabaseUtils.getOrCreateId(connection, dialect, DatabaseConstants.CLIENT_VERSIONS_TABLE, DatabaseConstants.CLIENT_VERSIONS_STRING_COLUMN, player.getClientVersion().getReleaseName());
long serverVersionId = DatabaseUtils.getOrCreateId(connection, dialect, DatabaseConstants.SERVER_VERSIONS_TABLE, DatabaseConstants.SERVER_VERSIONS_STRING_COLUMN, PacketEvents.getAPI().getServerManager().getVersion().toString());
// Set parameters for the PreparedStatement
insertAlert.setLong(1, serverId);
insertAlert.setObject(2, player.getUniqueId());
insertAlert.setLong(3, checkNameId);
insertAlert.setString(4, verbose);
insertAlert.setInt(5, vls);
insertAlert.setLong(6, System.currentTimeMillis());
insertAlert.setLong(7, grimVersionId); // NEW
insertAlert.setLong(8, clientBrandId); // NEW
insertAlert.setLong(9, clientVersionId); // NEW
insertAlert.setLong(10, serverVersionId); // NEW
insertAlert.execute();
} catch (SQLException ex) {
LogUtil.error("Failed to log alert", ex);
}
}
@Override
public synchronized int getLogCount(UUID uuid) {
try (Connection connection = dataSource.getConnection();
PreparedStatement countLogs = connection.prepareStatement(
"SELECT COUNT(*) FROM " + DatabaseConstants.VIOLATIONS_TABLE + " WHERE " + DatabaseConstants.VIOLATIONS_UUID_COLUMN + " = ?"
)
) {
countLogs.setObject(1, uuid);
ResultSet result = countLogs.executeQuery();
if (result.next()) {
return result.getInt(1);
}
} catch (SQLException ex) {
LogUtil.error("Failed to count logs", ex);
}
return 0;
}
@Override
public synchronized List<Violation> getViolations(UUID uuid, int page, int limit) {
try (Connection connection = dataSource.getConnection();
// Updated SELECT statement with all new joins and column selections
PreparedStatement fetchLogs = connection.prepareStatement(
"SELECT " +
"v." + DatabaseConstants.VIOLATIONS_ID_COLUMN + ", " +
"s." + DatabaseConstants.SERVERS_STRING_COLUMN + ", " +
"v." + DatabaseConstants.VIOLATIONS_UUID_COLUMN + ", " +
"cn." + DatabaseConstants.CHECK_NAMES_STRING_COLUMN + ", " +
"v." + quoteVerboseColumn() + ", " +
"v." + DatabaseConstants.VIOLATIONS_VL_COLUMN + ", " +
"v." + DatabaseConstants.VIOLATIONS_CREATED_AT_COLUMN + ", " +
"gv." + DatabaseConstants.GRIM_VERSIONS_STRING_COLUMN + ", " + // NEW
"cb." + DatabaseConstants.CLIENT_BRANDS_STRING_COLUMN + ", " + // NEW
"clv." + DatabaseConstants.CLIENT_VERSIONS_STRING_COLUMN + ", " + // NEW
"srv." + DatabaseConstants.SERVER_VERSIONS_STRING_COLUMN + " " + // NEW
"FROM " + DatabaseConstants.VIOLATIONS_TABLE + " v " +
"JOIN " + DatabaseConstants.SERVERS_TABLE + " s ON v." + DatabaseConstants.VIOLATIONS_SERVER_ID_COLUMN + " = s.id " +
"JOIN " + DatabaseConstants.CHECK_NAMES_TABLE + " cn ON v." + DatabaseConstants.VIOLATIONS_CHECK_NAME_ID_COLUMN + " = cn.id " +
"JOIN " + DatabaseConstants.GRIM_VERSIONS_TABLE + " gv ON v." + DatabaseConstants.VIOLATIONS_GRIM_VERSION_ID_COLUMN + " = gv.id " + // NEW
"JOIN " + DatabaseConstants.CLIENT_BRANDS_TABLE + " cb ON v." + DatabaseConstants.VIOLATIONS_CLIENT_BRAND_ID_COLUMN + " = cb.id " + // NEW
"JOIN " + DatabaseConstants.CLIENT_VERSIONS_TABLE + " clv ON v." + DatabaseConstants.VIOLATIONS_CLIENT_VERSION_ID_COLUMN + " = clv.id " + // NEW
"JOIN " + DatabaseConstants.SERVER_VERSIONS_TABLE + " srv ON v." + DatabaseConstants.VIOLATIONS_SERVER_VERSION_ID_COLUMN + " = srv.id " + // NEW
"WHERE v." + DatabaseConstants.VIOLATIONS_UUID_COLUMN + " = ? ORDER BY v." + DatabaseConstants.VIOLATIONS_CREATED_AT_COLUMN + " DESC LIMIT ? OFFSET ?"
)
) {
fetchLogs.setObject(1, uuid);
fetchLogs.setInt(2, limit);
fetchLogs.setInt(3, Math.max(0, (page - 1)) * limit); // postgresql is not allowing negative numbers
return Violation.fromResultSet(fetchLogs.executeQuery());
} catch (SQLException ex) {
LogUtil.error("Failed to fetch logs", ex);
return null;
}
}
@Override
public void disconnect() {
if (dataSource != null && !dataSource.isClosed()) {
dataSource.close();
}
}
public boolean sameConfig(String host, String db, String user, String pwd) {
String wantUrl = "jdbc:postgresql://" + host + "/" + db;
return wantUrl.equalsIgnoreCase(dataSource.getJdbcUrl())
&& user.equals(dataSource.getUsername())
&& pwd.equals(dataSource.getPassword());
}
}
@@ -1,17 +1,15 @@
package ac.grim.grimac.platform.api;
import ac.grim.grimac.api.plugin.GrimPlugin;
import ac.grim.grimac.platform.api.command.CommandService;
import ac.grim.grimac.platform.api.manager.ItemResetHandler;
import ac.grim.grimac.platform.api.manager.MessagePlaceHolderManager;
import ac.grim.grimac.platform.api.manager.CommandAdapter;
import ac.grim.grimac.platform.api.manager.PermissionRegistrationManager;
import ac.grim.grimac.platform.api.manager.PlatformPluginManager;
import ac.grim.grimac.platform.api.player.PlatformPlayerFactory;
import ac.grim.grimac.platform.api.scheduler.PlatformScheduler;
import ac.grim.grimac.platform.api.sender.Sender;
import ac.grim.grimac.platform.api.sender.SenderFactory;
import com.github.retrooper.packetevents.PacketEventsAPI;
import org.incendo.cloud.CommandManager;
import org.jetbrains.annotations.NotNull;
public interface PlatformLoader {
@@ -19,14 +17,12 @@ public interface PlatformLoader {
PlatformPlayerFactory getPlatformPlayerFactory();
CommandAdapter getCommandAdapter();
PacketEventsAPI<?> getPacketEvents();
CommandManager<Sender> getCommandManager();
ItemResetHandler getItemResetHandler();
CommandService getCommandService();
SenderFactory<?> getSenderFactory();
GrimPlugin getPlugin();
@@ -0,0 +1,5 @@
package ac.grim.grimac.platform.api.command;
public interface CommandService {
void registerCommands();
}
@@ -1,30 +1,8 @@
package ac.grim.grimac.platform.api.manager;
import ac.grim.grimac.platform.api.command.PlayerSelector;
import ac.grim.grimac.platform.api.sender.Sender;
import org.incendo.cloud.parser.ParserDescriptor;
import org.incendo.cloud.suggestion.SuggestionProvider;
/**
* Acts as a bridge between platform-agnostic command definitions
* and platform-specific parsers, suggestion providers, etc.
*/
public interface CommandAdapter {
/**
* Provides a parser descriptor for a single player selector (@p, player name).
*
* @return The parser descriptor.
*/
ParserDescriptor<Sender, PlayerSelector> singlePlayerSelectorParser();
/**
* Provides a suggestion provider that lists all online players.
* <p>
* Platform-specific implementations should handle things like
* vanished players.
*
* @return The suggestion provider.
*/
SuggestionProvider<Sender> onlinePlayerSuggestions();
}
@@ -0,0 +1,30 @@
package ac.grim.grimac.platform.api.manager.cloud;
import ac.grim.grimac.platform.api.command.PlayerSelector;
import ac.grim.grimac.platform.api.manager.CommandAdapter;
import ac.grim.grimac.platform.api.sender.Sender;
import org.incendo.cloud.parser.ParserDescriptor;
import org.incendo.cloud.suggestion.SuggestionProvider;
/**
* Acts as a bridge between platform-agnostic command definitions
* and platform-specific parsers, suggestion providers, etc.
*/
public interface CloudCommandAdapter extends CommandAdapter {
/**
* Provides a parser descriptor for a single player selector (@p, player name).
*
* @return The parser descriptor.
*/
ParserDescriptor<Sender, PlayerSelector> singlePlayerSelectorParser();
/**
* Provides a suggestion provider that lists all online players.
* <p>
* Platform-specific implementations should handle things like
* vanished players.
*
* @return The suggestion provider.
*/
SuggestionProvider<Sender> onlinePlayerSuggestions();
}
@@ -8,6 +8,8 @@ public interface PlatformPlayerFactory {
OfflinePlatformPlayer getOfflineFromName(String name);
Collection<OfflinePlatformPlayer> getOfflinePlayers();
PlatformPlayer getFromName(String name);
PlatformPlayer getFromUUID(UUID uuid);
@@ -3,6 +3,7 @@ package ac.grim.grimac.player;
import ac.grim.grimac.GrimAPI;
import ac.grim.grimac.api.AbstractCheck;
import ac.grim.grimac.api.GrimUser;
import ac.grim.grimac.api.PacketWorld;
import ac.grim.grimac.api.config.ConfigManager;
import ac.grim.grimac.api.handler.ResyncHandler;
import ac.grim.grimac.checks.Check;
@@ -14,6 +15,7 @@ import ac.grim.grimac.events.packets.CheckManagerListener;
import ac.grim.grimac.manager.*;
import ac.grim.grimac.manager.player.features.FeatureManagerImpl;
import ac.grim.grimac.manager.player.handlers.DefaultResyncHandler;
import ac.grim.grimac.manager.player.handlers.NoOpResyncHandler;
import ac.grim.grimac.platform.api.player.PlatformPlayer;
import ac.grim.grimac.predictionengine.MovementCheckRunner;
import ac.grim.grimac.predictionengine.PointThreeEstimator;
@@ -74,6 +76,8 @@ import io.github.retrooper.packetevents.adventure.serializer.legacy.LegacyCompon
import io.netty.channel.Channel;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import it.unimi.dsi.fastutil.objects.Object2DoubleArrayMap;
import it.unimi.dsi.fastutil.objects.Object2DoubleMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import lombok.Getter;
import lombok.Setter;
@@ -88,6 +92,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
// Everything in this class should be sync'd to the anticheat thread.
@@ -210,6 +215,7 @@ public class GrimPlayer implements GrimUser {
public final CompensatedFireworks fireworks;
public final CompensatedWorld compensatedWorld;
public final CompensatedEntities compensatedEntities;
public final CompensatedDashableEntities dashableEntities;
public final CompensatedInventory inventory;
public final ILatencyUtils latencyUtils = new LatencyUtils(this);
public final PointThreeEstimator pointThreeEstimator;
@@ -235,6 +241,7 @@ public class GrimPlayer implements GrimUser {
public long lastBlockBreak = 0;
public final AtomicInteger cancelledPackets = new AtomicInteger(0);
public MainSupportingBlockData mainSupportingBlockData = new MainSupportingBlockData(null, false);
public final Object2DoubleMap<FluidTag> fluidHeight = new Object2DoubleArrayMap<>(2);
// possibleEyeHeights[0] = Standing eye heights, [1] = Sneaking. [2] = Elytra, Swimming, and Riptide Trident which only exists in 1.9+
public final double[][] possibleEyeHeights = new double[3][];
public int totalFlyingPacketsSent;
@@ -245,7 +252,8 @@ public class GrimPlayer implements GrimUser {
public final Queue<BlockBreak> queuedBreaks = new LinkedBlockingQueue<>();
public final PlayerBlockHistory blockHistory = new PlayerBlockHistory();
public final ArrayDeque<RotationData> pendingRotations = new ArrayDeque<>();
@Getter @Setter private ResyncHandler resyncHandler = new DefaultResyncHandler(this);
public final CompensatedCameraEntity cameraEntity;
@Getter @Setter private ResyncHandler resyncHandler = GrimAPI.INSTANCE.getConfigManager().getConfig().getBooleanElse("disable-default-resync-handler", false) ? NoOpResyncHandler.INSTANCE : new DefaultResyncHandler(this);
@Getter private final FeatureManagerImpl featureManager = new FeatureManagerImpl(this);
public boolean serverOpenedInventoryThisTick;
// start config
@@ -282,6 +290,11 @@ public class GrimPlayer implements GrimUser {
fireworks = new CompensatedFireworks(this); // Must be before checkmanager
inventory = new CompensatedInventory(this);
compensatedWorld = new CompensatedWorld(this);
compensatedEntities = new CompensatedEntities(this);
dashableEntities = new CompensatedDashableEntities();
cameraEntity = new CompensatedCameraEntity(this);
lastInstanceManager = new LastInstanceManager(this);
actionManager = new ActionManager(this);
checkManager = new CheckManager(this);
@@ -289,8 +302,6 @@ public class GrimPlayer implements GrimUser {
this.tagManager = new SyncedTags(this); // must be after this.user = user
movementCheckRunner = new MovementCheckRunner(this);
compensatedWorld = new CompensatedWorld(this);
compensatedEntities = new CompensatedEntities(this);
uncertaintyHandler = new UncertaintyHandler(this); // must be after checkmanager
pointThreeEstimator = new PointThreeEstimator(this);
@@ -504,11 +515,17 @@ public class GrimPlayer implements GrimUser {
: isSneaking ? 1.54f : 1.62f;
}
private final AtomicBoolean hasDisconnected = new AtomicBoolean(false);
public void timedOut() {
disconnect(MessageUtil.miniMessage(MessageUtil.replacePlaceholders(this, GrimAPI.INSTANCE.getConfigManager().getDisconnectTimeout())));
}
public void disconnect(Component reason) {
if (!hasDisconnected.compareAndSet(false, true)) {
return;
}
String textReason;
if (reason instanceof TranslatableComponent translatableComponent) {
textReason = translatableComponent.key();
@@ -690,6 +707,11 @@ public class GrimPlayer implements GrimUser {
return possibleLookDirs.toArray(new Vector3dm[0]);
}
@Override
public PacketWorld getPacketWorld() {
return compensatedWorld;
}
@Override
public int getTransactionPing() {
return GrimMath.floor(transactionPing / 1e6);
@@ -739,8 +761,9 @@ public class GrimPlayer implements GrimUser {
EntityTypes.isTypeInstanceOf(data.getEntityType(), EntityTypes.ABSTRACT_HORSE) ||
data.getEntityType() == EntityTypes.PIG ||
data.getEntityType() == EntityTypes.STRIDER ||
data.getEntityType() == EntityTypes.CAMEL ||
data.getEntityType() == EntityTypes.HAPPY_GHAST) {
EntityTypes.isTypeInstanceOf(data.getEntityType(), EntityTypes.CAMEL) ||
data.getEntityType() == EntityTypes.HAPPY_GHAST ||
EntityTypes.isTypeInstanceOf(data.getEntityType(), EntityTypes.ABSTRACT_NAUTILUS)) {
// We need to set its velocity otherwise it will jump a bit on us, flagging the anticheat
// The server does override this with some vehicles. This is intentional.
user.writePacket(new WrapperPlayServerEntityVelocity(vehicleID, new Vector3d()));
@@ -787,23 +810,25 @@ public class GrimPlayer implements GrimUser {
}
public boolean canGlide() {
// Servers older than 1.21.2 don't have this component
// don't check the client/server version, this is relevant for all
final ItemStack chestPlate = inventory.getChestplate();
if (chestPlate.getType() == ItemTypes.ELYTRA && chestPlate.getDamageValue() < chestPlate.getMaxDamage() - 1)
return true;
// if the server or client doesn't support glider components return false
if (getClientVersion().isOlderThan(ClientVersion.V_1_21_2)
|| PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_21_2)) {
final ItemStack chestPlate = inventory.getChestplate();
return chestPlate.getType() == ItemTypes.ELYTRA && chestPlate.getDamageValue() < chestPlate.getMaxDamage() - 1;
}
|| PacketEvents.getAPI().getServerManager().getVersion().isOlderThan(ServerVersion.V_1_21_2)) return false;
// PacketEvents mappings are wrong
// TODO https://github.com/retrooper/packetevents/pull/1125
return isGlider(inventory.getHelmet(), EquipmentSlot.CHEST_PLATE)
|| isGlider(inventory.getChestplate(), EquipmentSlot.LEGGINGS)
|| isGlider(inventory.getLeggings(), EquipmentSlot.BOOTS)
|| isGlider(inventory.getBoots(), EquipmentSlot.OFF_HAND);
|| isGlider(inventory.getBoots(), EquipmentSlot.OFF_HAND)
|| isGlider(inventory.getOffHand(), EquipmentSlot.HELMET);
}
private static boolean isGlider(ItemStack stack, EquipmentSlot slot) {
if (!stack.hasComponent(ComponentTypes.GLIDER) || stack.getDamageValue() >= (stack.getMaxDamage() - 1)) {
if (!stack.hasComponent(ComponentTypes.GLIDER) || (stack.canBeDepleted() && stack.getDamageValue() >= (stack.getMaxDamage() - 1))) {
return false;
}
@@ -924,6 +949,30 @@ public class GrimPlayer implements GrimUser {
maxTransactionTime = GrimMath.clamp(config.getIntElse("max-transaction-time", 60), 1, 180);
ignoreDuplicatePacketRotation = config.getBooleanElse("ignore-duplicate-packet-rotation", false);
cancelDuplicatePacket = config.getBooleanElse("cancel-duplicate-packet", true);
boolean shouldDisableResync = config.getBooleanElse("disable-default-resync-handler", false);
Class<?> currentHandlerClass = this.resyncHandler.getClass();
// Check if the current handler is EXACTLY one of our internal types.
// If someone extended DefaultResyncHandler, .getClass() will not match,
// so we will skip this block and preserve their custom handler.
boolean isInternalHandler = currentHandlerClass == DefaultResyncHandler.class
|| currentHandlerClass == NoOpResyncHandler.class;
if (isInternalHandler) {
if (shouldDisableResync) {
// Config says disable, but we aren't using NoOp yet? Switch to NoOp.
if (currentHandlerClass != NoOpResyncHandler.class) {
this.resyncHandler = NoOpResyncHandler.INSTANCE;
}
} else {
// Config says enable, but we are using NoOp? Switch to Default.
if (currentHandlerClass != DefaultResyncHandler.class) {
this.resyncHandler = new DefaultResyncHandler(this);
}
}
}
resetItemUsageOnAttack = config.getBooleanElse("reset-item-usage-on-attack", true);
resetItemUsageOnItemUpdate = config.getBooleanElse("reset-item-usage-on-item-update", true);
resetItemUsageOnSlotChange = config.getBooleanElse("reset-item-usage-on-slot-change", true);
@@ -10,19 +10,23 @@ import ac.grim.grimac.player.GrimPlayer;
import ac.grim.grimac.predictionengine.movementtick.MovementTickerCamel;
import ac.grim.grimac.predictionengine.movementtick.MovementTickerHappyGhast;
import ac.grim.grimac.predictionengine.movementtick.MovementTickerHorse;
import ac.grim.grimac.predictionengine.movementtick.MovementTickerNautilus;
import ac.grim.grimac.predictionengine.movementtick.MovementTickerPig;
import ac.grim.grimac.predictionengine.movementtick.MovementTickerPlayer;
import ac.grim.grimac.predictionengine.movementtick.MovementTickerStrider;
import ac.grim.grimac.predictionengine.predictions.PredictionEngineNormal;
import ac.grim.grimac.predictionengine.predictions.rideable.PredictionEngineBoat;
import ac.grim.grimac.predictionengine.predictions.rideable.PredictionEngineRideableUtils;
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;
import ac.grim.grimac.utils.data.packetentity.PacketEntityHappyGhast;
import ac.grim.grimac.utils.data.packetentity.PacketEntityHorse;
import ac.grim.grimac.utils.data.packetentity.PacketEntityNautilus;
import ac.grim.grimac.utils.data.packetentity.PacketEntityRideable;
import ac.grim.grimac.utils.data.packetentity.PacketEntityTrackXRot;
import ac.grim.grimac.utils.enums.Pose;
@@ -71,7 +75,7 @@ public class MovementCheckRunner extends Check implements PositionCheck {
if (!invalidVehicle && !data.isTeleport()) {
// Teleport the player back to avoid players being able to simply ignore transactions
// We shouldn't simulate movement in unloaded chunks
player.getSetbackTeleportUtil().executeForceResyncNoSimulation();
player.getSetbackTeleportUtil().executeNonSimulatingForceResync();
}
}
@@ -107,7 +111,15 @@ public class MovementCheckRunner extends Check implements PositionCheck {
player.lastWasClimbing = 0; // Vertical movement reset
player.canSwimHop = false; // Vertical movement reset
} else {
update.getTeleportData().modifyVector(player, player.clientVelocity);
final SetBackData setback = update.getSetback();
if (setback == null || setback.getVelocity() == null) {
update.getTeleportData().modifyVector(player, player.clientVelocity);
} else {
// Enforce setback velocity?
player.clientVelocity.setX(setback.getVelocity().getX());
player.clientVelocity.setY(setback.getVelocity().getY());
player.clientVelocity.setZ(setback.getVelocity().getZ());
}
}
}
@@ -499,6 +511,9 @@ public class MovementCheckRunner extends Check implements PositionCheck {
PlayerBaseTick.doBaseTick(player);
// Speed doesn't affect anything with boat movement
new PredictionEngineBoat(player).guessBestMovement(0.1f, player);
} else if (riding instanceof PacketEntityNautilus) {
PlayerBaseTick.doBaseTick(player);
new MovementTickerNautilus(player).livingEntityAIStep();
} else if (riding instanceof PacketEntityCamel) {
PlayerBaseTick.doBaseTick(player);
new MovementTickerCamel(player).livingEntityAIStep();
@@ -599,7 +614,10 @@ public class MovementCheckRunner extends Check implements PositionCheck {
player.riptideSpinAttackTicks = 20;
player.uncertaintyHandler.lastMovementWasZeroPointZeroThree = !player.inVehicle() && player.skippedTickInActualMovement;
player.uncertaintyHandler.lastMovementWasUnknown003VectorReset = !player.inVehicle() && player.couldSkipTick && player.predictedVelocity.isKnockback();
player.uncertaintyHandler.lastMovementWasUnknown003VectorReset = !player.inVehicle() && player.couldSkipTick
&& player.predictedVelocity.isKnockback()
// Don't let setbacks count
&& !player.predictedVelocity.isSetbackKb(player);
player.couldSkipTick = false;
// Logic is if the player was directly 0.03 and the player could control vertical movement in 0.03
@@ -613,14 +631,12 @@ public class MovementCheckRunner extends Check implements PositionCheck {
player.uncertaintyHandler.lastPacketWasGroundPacket = player.uncertaintyHandler.onGroundUncertain;
player.uncertaintyHandler.onGroundUncertain = false;
PredictionEngineRideableUtils.applyPendingJumps(player);
player.vehicleData.vehicleForward = (float) Math.min(0.98, Math.max(-0.98, player.vehicleData.nextVehicleForward));
player.vehicleData.vehicleHorizontal = (float) Math.min(0.98, Math.max(-0.98, player.vehicleData.nextVehicleHorizontal));
if (player.onGround) { // if vehicle is on ground, this gets set
player.vehicleData.horseJump = player.vehicleData.nextHorseJump;
player.vehicleData.nextHorseJump = 0;
}
player.vehicleData.camelDashCooldown = Math.max(0, player.vehicleData.camelDashCooldown - 1);
player.dashableEntities.tick();
player.minAttackSlow = 0;
player.maxAttackSlow = 0;
Loaded 100 of 182 files, more files were not shown because too many files have changed in this diff. Show more