diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml index be2a751cb0..dd10fc65eb 100644 --- a/.github/workflows/gradle-publish.yml +++ b/.github/workflows/gradle-publish.yml @@ -1,10 +1,3 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. -# This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created -# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle - name: Gradle Package on: [push, workflow_dispatch] @@ -13,47 +6,226 @@ jobs: build: runs-on: ubuntu-latest permissions: - contents: read + contents: write # needed by build-tag-number and mc-publish packages: write + actions: write steps: - - uses: actions/checkout@v4 - - - name: Set up JDK 21 - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' - server-id: github # Value of the distributionManagement/repository/id field of the pom.xml - settings-path: ${{ github.workspace }} # location for the settings.xml file - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 # Handles Gradle wrapper validation and basic caching - - - name: Cache Gradle Loom Cache - uses: actions/cache@v4 - with: - path: .gradle/loom-cache # Path to Loom cache relative to workspace root - key: loom-cache-${{ runner.os }}-${{ hashFiles('**/libs.versions.toml', 'fabric/**/build.gradle.kts') }} - restore-keys: | - loom-cache-${{ runner.os }} - - - name: Build with Gradle (Actual Build for Artifacts) - run: ./gradlew build - - - name: Upload Bukkit Artifact - uses: actions/upload-artifact@v4 - with: - name: grimac-bukkit - # Adding if-no-files-found for robustness - path: ${{ github.workspace }}/bukkit/build/libs/grimac-bukkit-*.jar - if-no-files-found: error - - - name: Upload Fabric Artifact - uses: actions/upload-artifact@v4 - with: - name: grimac-fabric - # Adding if-no-files-found for robustness - path: ${{ github.workspace }}/fabric/build/libs/grimac-fabric-*.jar - if-no-files-found: error + #------------------------------------------------------------------ + # 0) Checkout + JDK + #------------------------------------------------------------------ + - name: Checkout repository + uses: actions/checkout@v4 + with: + # fetch the entire history and all tags + fetch-depth: 50 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + server-id: github + settings-path: ${{ github.workspace }} + + #------------------------------------------------------------------ + # 1) Gradle wrapper validation + caches + #------------------------------------------------------------------ + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Cache Loom + uses: actions/cache@v4 + with: + path: .gradle/loom-cache + key: loom-cache-${{ runner.os }}-${{ hashFiles('**/libs.versions.toml', + 'fabric/**/build.gradle.kts', + 'bukkit/**/build.gradle.kts') }} + restore-keys: | + loom-cache-${{ runner.os }} + + #------------------------------------------------------------------ + # 2) Resolve VERSIONs **before** building + #------------------------------------------------------------------ + - name: Compute MAIN version (default build) + id: ver_main + run: | + VERSION=$(./gradlew -q printVersion | grep '^VERSION=' | cut -d'=' -f2) + echo "main_version=$VERSION" >> $GITHUB_OUTPUT + echo "MAIN_VERSION=$VERSION" >> $GITHUB_ENV + echo "Main version: $VERSION" + + - name: Compute LITE version (-PshadePE=false) + id: ver_lite + run: | + VERSION=$(./gradlew -q -PshadePE=false printVersion | grep '^VERSION=' | cut -d'=' -f2) + echo "lite_version=$VERSION" >> $GITHUB_OUTPUT + echo "LITE_VERSION=$VERSION" >> $GITHUB_ENV + echo "Lite version: $VERSION" + + #------------------------------------------------------------------ + # 3) Build shaded / “main” jars (all modules) + #------------------------------------------------------------------ + - name: Build (all platforms, shaded) + run: ./gradlew build + + #------------------------------------------------------------------ + # 4) Build **lite** Bukkit jar + #------------------------------------------------------------------ + - name: Build Bukkit-Lite (no shaded PacketEvents) + run: ./gradlew :bukkit:build -PshadePE=false + + #------------------------------------------------------------------ + # 4.5) Rename jars to LightningGrim-* before we upload/publish + #------------------------------------------------------------------ + - name: Rename jars to LightningGrim + shell: bash + run: | + set -e + mv "bukkit/build/libs/grimac-bukkit-${MAIN_VERSION}.jar" \ + "bukkit/build/libs/LightningGrim-bukkit-${MAIN_VERSION}.jar" + + mv "bukkit/build/libs/grimac-bukkit-${LITE_VERSION}.jar" \ + "bukkit/build/libs/LightningGrim-bukkit-${LITE_VERSION}.jar" + + mv "fabric/build/libs/grimac-fabric-${MAIN_VERSION}.jar" \ + "fabric/build/libs/LightningGrim-fabric-${MAIN_VERSION}.jar" + + #------------------------------------------------------------------ + # 5) Upload MAIN Bukkit + Fabric artefacts + #------------------------------------------------------------------ + - name: Upload Bukkit – MAIN + uses: actions/upload-artifact@v4 + with: + name: grimac-bukkit + path: bukkit/build/libs/LightningGrim-bukkit-${{ env.MAIN_VERSION }}.jar + if-no-files-found: error + + - name: Upload Fabric + uses: actions/upload-artifact@v4 + with: + name: grimac-fabric + path: fabric/build/libs/LightningGrim-fabric-${{ env.MAIN_VERSION }}.jar + if-no-files-found: error + + #------------------------------------------------------------------ + # 6) Upload LITE artefact + #------------------------------------------------------------------ + - name: Upload Bukkit – LITE + uses: actions/upload-artifact@v4 + with: + name: grimac-bukkit-lite + path: bukkit/build/libs/LightningGrim-bukkit-${{ env.LITE_VERSION }}.jar + if-no-files-found: error + + #------------------------------------------------------------------ + # 7) Auto-generate CHANGELOG (since previous build tag) + #------------------------------------------------------------------ + - name: Generate changelog + id: changelog + if: contains(fromJSON('["merge","release","main","lightning"]'), github.ref_name) + run: | + set -e + git fetch --tags --quiet + + # Most recent tag that looks like “build-*”; empty if none exist + LAST_TAG=$(git describe --tags --match "build-*" --abbrev=0 2>/dev/null || echo "") + echo "Last tag: ${LAST_TAG:-}" + + if [ -z "$LAST_TAG" ]; then + # First build (or no previous tag) → grab latest 50 commits + NOTES=$(git log -n 50 --pretty=format:'* %s (%h)' --no-merges) + else + # Normal case → commits since the last build tag + NOTES=$(git log "$LAST_TAG"..HEAD --pretty=format:'* %s (%h)' --no-merges) + fi + + # Fallback if there were no code changes + if [ -z "$NOTES" ]; then + NOTES="* No code changes since last build" + fi + + # Export multiline output for downstream steps + { + echo "notes<> "$GITHUB_OUTPUT" + + echo "Changelog generated:" + echo "$NOTES" + + #------------------------------------------------------------------ + # 8) Generate incremental build number (after changelog step!) + #------------------------------------------------------------------ + - name: Generate build number + if: contains(fromJSON('["merge","release","main","lightning"]'), github.ref_name) + id: buildnumber + uses: onyxmueller/build-tag-number@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + #------------------------------------------------------------------ + # 9-A) Publish **Bukkit** jars to Modrinth + #------------------------------------------------------------------ + - name: Publish to Modrinth (Bukkit) + if: contains(fromJSON('["merge","release","main","lightning"]'), github.ref_name) + uses: Kir-Antipov/mc-publish@v3.3 + with: + modrinth-id: ${{ vars.MODRINTH_ID }} # Bukkit & Fabric can share or differ + modrinth-token: ${{ secrets.MODRINTH_TOKEN }} + modrinth-featured: true + modrinth-unfeature-mode: subset + + files: | + bukkit/build/libs/LightningGrim-bukkit-${{ env.MAIN_VERSION }}.jar + bukkit/build/libs/LightningGrim-bukkit-${{ env.LITE_VERSION }}.jar + + name: Lightning Grim Anticheat (Bukkit) ${{ env.MAIN_VERSION }}-b${{ steps.buildnumber.outputs.build_number }} + version: ${{ env.MAIN_VERSION }}-b${{ steps.buildnumber.outputs.build_number }} + version-type: alpha + changelog: ${{ steps.changelog.outputs.notes }} + + loaders: | + bukkit + spigot + paper + folia + purpur + + game-versions: | + >=1.7 + + retry-attempts: 2 + retry-delay: 10000 + fail-mode: fail + + #------------------------------------------------------------------ + # 9-B) Publish **Fabric** jar to Modrinth + #------------------------------------------------------------------ + - name: Publish to Modrinth (Fabric) + if: contains(fromJSON('["merge","release","main","lightning"]'), github.ref_name) + uses: Kir-Antipov/mc-publish@v3.3 + with: + modrinth-id: ${{ vars.MODRINTH_ID_FABRIC || vars.MODRINTH_ID }} + modrinth-token: ${{ secrets.MODRINTH_TOKEN }} + modrinth-featured: true + modrinth-unfeature-mode: subset + + files: | + fabric/build/libs/LightningGrim-fabric-${{ env.MAIN_VERSION }}.jar + + name: Lightning Grim Anticheat (Fabric) ${{ env.MAIN_VERSION }}-b${{ steps.buildnumber.outputs.build_number }} + version: ${{ env.MAIN_VERSION }}-b${{ steps.buildnumber.outputs.build_number }} + version-type: alpha + changelog: ${{ steps.changelog.outputs.notes }} + + loaders: | + fabric + + game-versions: | + >=1.16.1 + retry-attempts: 2 + retry-delay: 10000 + fail-mode: fail \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/grim.shadow-conventions.gradle.kts b/buildSrc/src/main/kotlin/grim.shadow-conventions.gradle.kts index 56bc68db5c..b4bf0fbb05 100644 --- a/buildSrc/src/main/kotlin/grim.shadow-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/grim.shadow-conventions.gradle.kts @@ -38,6 +38,7 @@ tasks.named("shadowJar") { relocate("org.jetbrains", "ac.grim.grimac.shaded.jetbrains") relocate("org.incendo", "ac.grim.grimac.shaded.incendo") relocate("io.leangen.geantyref", "ac.grim.grimac.shaded.geantyref") // Required by cloud + relocate("com.zaxxer", "ac.grim.grimac.shaded.zaxxer") // Database history } mergeServiceFiles() } diff --git a/buildSrc/src/main/kotlin/versioning/VersionUtil.kt b/buildSrc/src/main/kotlin/versioning/VersionUtil.kt index 058bdb39a7..39a3d34f2b 100644 --- a/buildSrc/src/main/kotlin/versioning/VersionUtil.kt +++ b/buildSrc/src/main/kotlin/versioning/VersionUtil.kt @@ -81,6 +81,7 @@ object VersionUtil { val branch = stdout.toString().trim() return when (branch) { + "lightning" -> null "main", "2.0" -> null // ← ignore these branches else -> branch.replace("/", "_") } diff --git a/bukkit/src/main/java/ac/grim/grimac/platform/bukkit/entity/BukkitGrimEntity.java b/bukkit/src/main/java/ac/grim/grimac/platform/bukkit/entity/BukkitGrimEntity.java index c81ad5c280..524e06bf7a 100644 --- a/bukkit/src/main/java/ac/grim/grimac/platform/bukkit/entity/BukkitGrimEntity.java +++ b/bukkit/src/main/java/ac/grim/grimac/platform/bukkit/entity/BukkitGrimEntity.java @@ -60,7 +60,6 @@ public PlatformWorld getWorld() { if (bukkitPlatformWorld == null || !bukkitPlatformWorld.getBukkitWorld().equals(entity.getWorld())) { bukkitPlatformWorld = new BukkitPlatformWorld(entity.getWorld()); } - return bukkitPlatformWorld; } diff --git a/bukkit/src/main/java/ac/grim/grimac/platform/bukkit/events/PistonEvent.java b/bukkit/src/main/java/ac/grim/grimac/platform/bukkit/events/PistonEvent.java index 3bb0c15a0a..c120587ef9 100644 --- a/bukkit/src/main/java/ac/grim/grimac/platform/bukkit/events/PistonEvent.java +++ b/bukkit/src/main/java/ac/grim/grimac/platform/bukkit/events/PistonEvent.java @@ -4,7 +4,7 @@ import ac.grim.grimac.platform.bukkit.utils.convert.BukkitConversionUtils; import ac.grim.grimac.player.GrimPlayer; import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox; -import ac.grim.grimac.utils.data.PistonData; +import ac.grim.grimac.utils.data.PistonTemplate; import com.github.retrooper.packetevents.protocol.world.BlockFace; import org.bukkit.Material; import org.bukkit.block.Block; @@ -55,17 +55,8 @@ public void onPistonPushEvent(BlockPistonExtendEvent event) { piston.getY() + event.getDirection().getModY(), piston.getZ() + event.getDirection().getModZ())); - boolean finalHasSlimeBlock = hasSlimeBlock; - boolean finalHasHoneyBlock = hasHoneyBlock; - for (GrimPlayer player : GrimAPI.INSTANCE.getPlayerDataManager().getEntries()) { - int lastTrans = player.lastTransactionSent.get(); - player.runSafely(() -> { - if (player.compensatedWorld.isChunkLoaded(event.getBlock().getX() >> 4, event.getBlock().getZ() >> 4)) { - PistonData data = new PistonData(BukkitConversionUtils.fromBukkitFace(event.getDirection()), boxes, lastTrans, true, finalHasSlimeBlock, finalHasHoneyBlock); - player.latencyUtils.addRealTimeTaskAsync(lastTrans, () -> player.compensatedWorld.activePistons.add(data)); - } - }); - } + PistonTemplate data = new PistonTemplate(BukkitConversionUtils.fromBukkitFace(event.getDirection()), boxes, true, hasSlimeBlock, hasHoneyBlock); + addPistonData(data, event.getBlock().getX() >> 4, event.getBlock().getZ() >> 4); } // For some unknown reason, bukkit handles this stupidly @@ -113,15 +104,18 @@ public void onPistonRetractEvent(BlockPistonRetractEvent event) { } } - boolean finalHasSlimeBlock = hasSlimeBlock; - boolean finalHasHoneyBlock = hasHoneyBlock; + PistonTemplate data = new PistonTemplate(face, boxes, false, hasSlimeBlock, hasHoneyBlock); + addPistonData(data, event.getBlock().getX() >> 4, event.getBlock().getZ() >> 4); + } + + private void addPistonData(PistonTemplate pistonTemplate, int chunkX, int chunkZ) { for (GrimPlayer player : GrimAPI.INSTANCE.getPlayerDataManager().getEntries()) { + if (player.compensatedWorld.isChunkLoaded(chunkX, chunkZ)) continue; + int lastTrans = player.lastTransactionSent.get(); - player.runSafely(() -> { - if (player.compensatedWorld.isChunkLoaded(event.getBlock().getX() >> 4, event.getBlock().getZ() >> 4)) { - PistonData data = new PistonData(BukkitConversionUtils.fromBukkitFace(event.getDirection()), boxes, lastTrans, false, finalHasSlimeBlock, finalHasHoneyBlock); - player.latencyUtils.addRealTimeTaskAsync(lastTrans, () -> player.compensatedWorld.activePistons.add(data)); - } + + player.latencyUtils.addRealTimeTaskAsync(lastTrans, () -> { + player.compensatedWorld.addPiston(pistonTemplate, lastTrans); }); } } diff --git a/bukkit/src/main/java/ac/grim/grimac/platform/bukkit/player/BukkitOfflinePlatformPlayer.java b/bukkit/src/main/java/ac/grim/grimac/platform/bukkit/player/BukkitOfflinePlatformPlayer.java new file mode 100644 index 0000000000..ec85a7440f --- /dev/null +++ b/bukkit/src/main/java/ac/grim/grimac/platform/bukkit/player/BukkitOfflinePlatformPlayer.java @@ -0,0 +1,38 @@ +package ac.grim.grimac.platform.bukkit.player; + +import ac.grim.grimac.platform.api.player.OfflinePlatformPlayer; +import org.bukkit.OfflinePlayer; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class BukkitOfflinePlatformPlayer implements OfflinePlatformPlayer { + private final OfflinePlayer offlinePlayer; + + public BukkitOfflinePlatformPlayer(OfflinePlayer offlinePlayer) { + this.offlinePlayer = offlinePlayer; + } + + @Override + public boolean isOnline() { + return offlinePlayer.isOnline(); + } + + @Override + public @NotNull String getName() { + return offlinePlayer.getName(); + } + + @Override + public @NotNull UUID getUniqueId() { + return offlinePlayer.getUniqueId(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof OfflinePlatformPlayer offlinePlatformPlayer) { + return this.getUniqueId().equals(offlinePlatformPlayer.getUniqueId()); + } + return false; + } +} diff --git a/bukkit/src/main/java/ac/grim/grimac/platform/bukkit/player/BukkitPlatformPlayerFactory.java b/bukkit/src/main/java/ac/grim/grimac/platform/bukkit/player/BukkitPlatformPlayerFactory.java index ecd49b324d..705840af13 100644 --- a/bukkit/src/main/java/ac/grim/grimac/platform/bukkit/player/BukkitPlatformPlayerFactory.java +++ b/bukkit/src/main/java/ac/grim/grimac/platform/bukkit/player/BukkitPlatformPlayerFactory.java @@ -1,9 +1,12 @@ package ac.grim.grimac.platform.bukkit.player; import ac.grim.grimac.platform.api.player.AbstractPlatformPlayerFactory; +import ac.grim.grimac.platform.api.player.OfflinePlatformPlayer; import ac.grim.grimac.platform.api.player.PlatformPlayer; import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; +import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.NotNull; import java.util.Collection; @@ -11,11 +14,17 @@ public class BukkitPlatformPlayerFactory extends AbstractPlatformPlayerFactory { + @Override protected Player getNativePlayer(@NotNull UUID uuid) { return Bukkit.getPlayer(uuid); } + @Override + protected Player getNativePlayer(@NonNull String name) { + return Bukkit.getPlayer(name); + } + @Override protected PlatformPlayer createPlatformPlayer(@NotNull Player nativePlayer) { return new BukkitPlatformPlayer(nativePlayer); @@ -44,4 +53,16 @@ protected Collection getNativeOnlinePlayers() { // Cast Collection to Collection return (Collection) Bukkit.getOnlinePlayers(); } + + @Override + public OfflinePlatformPlayer getOfflineFromUUID(@NotNull UUID uuid) { + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid); + return new BukkitOfflinePlatformPlayer(offlinePlayer); + } + + @Override + public OfflinePlatformPlayer getOfflineFromName(@NotNull String name) { + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(name); + return new BukkitOfflinePlatformPlayer(offlinePlayer); + } } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index d1ab1daabd..9f805b592c 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { api(libs.fastutil) api(libs.adventure.text.minimessage) api(libs.jetbrains.annotations) + api("com.zaxxer:HikariCP:4.0.3") api("ac.grim.grimac:GrimAPI:1.1.0.0") diff --git a/common/src/main/java/ac/grim/grimac/GrimAPI.java b/common/src/main/java/ac/grim/grimac/GrimAPI.java index a36088ce0a..a974009da3 100644 --- a/common/src/main/java/ac/grim/grimac/GrimAPI.java +++ b/common/src/main/java/ac/grim/grimac/GrimAPI.java @@ -10,6 +10,7 @@ import ac.grim.grimac.manager.TickManager; import ac.grim.grimac.manager.config.BaseConfigManager; import ac.grim.grimac.manager.init.Initable; +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; @@ -28,7 +29,6 @@ import org.incendo.cloud.CommandManager; import org.jetbrains.annotations.NotNull; - @Getter public final class GrimAPI { public static final GrimAPI INSTANCE = new GrimAPI(); @@ -43,6 +43,7 @@ public final class GrimAPI { private final TickManager tickManager; private final EventBus eventBus; private final GrimExternalAPI externalAPI; + private ViolationDatabaseManager violationDatabaseManager; private PlatformLoader loader; @Getter private InitManager initManager; @@ -69,6 +70,7 @@ private static Platform detectPlatform() { 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.load(); this.initialized = true; diff --git a/common/src/main/java/ac/grim/grimac/GrimExternalAPI.java b/common/src/main/java/ac/grim/grimac/GrimExternalAPI.java index 7ca9a57f3c..fd105ceebb 100644 --- a/common/src/main/java/ac/grim/grimac/GrimExternalAPI.java +++ b/common/src/main/java/ac/grim/grimac/GrimExternalAPI.java @@ -181,6 +181,7 @@ public void onReload(ConfigManager newConfig) { GrimAPI.INSTANCE.getAlertManager().reload(configManager); GrimAPI.INSTANCE.getDiscordManager().reload(); GrimAPI.INSTANCE.getSpectateManager().reload(); + GrimAPI.INSTANCE.getViolationDatabaseManager().reload(); // Don't reload players if the plugin hasn't started yet if (!started) return; // Reload checks for all players diff --git a/common/src/main/java/ac/grim/grimac/checks/debug/AbstractDebugHandler.java b/common/src/main/java/ac/grim/grimac/checks/debug/AbstractDebugHandler.java index eb4a1ee92f..7f344f01c1 100644 --- a/common/src/main/java/ac/grim/grimac/checks/debug/AbstractDebugHandler.java +++ b/common/src/main/java/ac/grim/grimac/checks/debug/AbstractDebugHandler.java @@ -16,7 +16,7 @@ public GrimPlayer getPlayer() { return grimPlayer; } - public abstract void toggleListener(GrimPlayer player); + public abstract boolean toggleListener(GrimPlayer player); public abstract boolean toggleConsoleOutput(); } diff --git a/common/src/main/java/ac/grim/grimac/checks/debug/HitboxDebugHandler.java b/common/src/main/java/ac/grim/grimac/checks/debug/HitboxDebugHandler.java new file mode 100644 index 0000000000..60f580f651 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/checks/debug/HitboxDebugHandler.java @@ -0,0 +1,205 @@ +package ac.grim.grimac.checks.debug; + +import ac.grim.grimac.player.GrimPlayer; +import ac.grim.grimac.utils.collisions.datatypes.CollisionBox; +import ac.grim.grimac.utils.collisions.datatypes.NoCollisionBox; +import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox; +import ac.grim.grimac.utils.data.Pair; +import ac.grim.grimac.utils.math.Vector3dm; +import com.github.retrooper.packetevents.netty.buffer.ByteBufHelper; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPluginMessage; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Handles debug visualization of hitboxes and reach calculations for GrimAC. + * Sends debug data to clients through plugin messages that can be visualized by compatible clients. + */ +public class HitboxDebugHandler extends AbstractDebugHandler { + + /** + * Set of players currently listening to hitbox debug data + */ + private final Set listeners = new CopyOnWriteArraySet<>(new HashSet<>()); + + /** + * Creates a new HitboxDebugHandler for the specified player + * + * @param grimPlayer The GrimAC player instance to debug + */ + public HitboxDebugHandler(GrimPlayer grimPlayer) { + super(grimPlayer); + } + + /** + * Toggles whether a player receives hitbox debug visualization data. + * If the player is already listening, they will be removed. If not, they will be added. + * + * @param player The player to toggle debug visualization for + * @return {@code true} if the player is now listening (was added), + * {@code false} if the player is no longer listening (was removed). + */ + @Override + public boolean toggleListener(GrimPlayer player) { + boolean wasPresent = listeners.remove(player); + + if (wasPresent) { + return false; + } else { + listeners.add(player); + return true; + } + } + + @Override + public boolean toggleConsoleOutput() { + throw new UnsupportedOperationException(); + } + + /** + * Sends debug visualization data for reach calculations to all listening players. + * The data includes hitboxes, target entities, look vectors, and eye heights used in reach calculations. + * + * @param hitboxes Map of entity IDs to their collision boxes + * @param targetEntities Set of entity IDs that are considered targets + * @param lookVecsAndEyeHeights List of pairs containing look vectors and their corresponding eye heights + * @param basePos Base position before eye height adjustments + * @param isPrediction Whether these hitboxes are from a prediction calculation + * + * Packet Format (Version 1): + * - Byte: Version (0) + * - Byte: Global flags + * - Bit 0: isPrediction + * - Bits 1-7: Reserved + * - Double: Player reach/interact distance + * - Vector3d: Base position (3 doubles) + * - VarInt: Number of ray traces + * - For each ray trace: + * - Double: Eye height delta + * - Vector3d: Look vector (3 doubles) + * - VarInt: Number of hitboxes + * - For each hitbox: + * - VarInt: Entity ID + * - Byte: Flags + * - Bit 0: Is target entity + * - Bit 1: Is no collision + * - Bits 2-7: Reserved + * - If not NoCollisionBox: + * - Double: minX + * - Double: minY + * - Double: minZ + * - Double: maxX + * - Double: maxY + * - Double: maxZ + */ + public void sendHitboxData(Map hitboxes, Set targetEntities, + List> lookVecsAndEyeHeights, Vector3dm basePos, + boolean isPrediction, double reachDistance) { + if (!isEnabled()) return; + + ByteBuf buffer = Unpooled.buffer(); + try { + // Version Header + buffer.writeByte(1); + + // Global Flags Header + // Pack boolean flags into a single byte + byte global_flags = 0; + global_flags |= (isPrediction ? 1 : 0); // Bit 0: are the hitboxes from a prediction? + // Bits 2-7 reserved for future use + buffer.writeByte(global_flags); + + // Write reach distance + buffer.writeDouble(reachDistance); + + // Write base position + writeVector(buffer, basePos); + + // Write number of ray traces + ByteBufHelper.writeVarInt(buffer, lookVecsAndEyeHeights.size()); + + // Write all possible ray traces + for (Pair pair : lookVecsAndEyeHeights) { + Vector3dm lookVec = pair.first(); + double eyeHeight = pair.second(); + + // Write eye height delta and look vector + // we make them 3 meters shorter because all of the vectors passed into here are made 3 meters longer + // then they need to be so grim can report correct reach distance for too far away hits + buffer.writeDouble(eyeHeight); + writeVector(buffer, lookVec); + } + + // Write number of hitboxes + ByteBufHelper.writeVarInt(buffer, hitboxes.size()); + + // Write hitbox data + for (Map.Entry entry : hitboxes.entrySet()) { + int entityId = entry.getKey(); + CollisionBox box = entry.getValue(); + + // Write entity ID + ByteBufHelper.writeVarInt(buffer, entityId); + + // Pack boolean flags into a single byte + byte flags = 0; + flags |= (targetEntities.contains(entityId) ? 1 : 0); // Bit 0: Is target + flags |= (box == NoCollisionBox.INSTANCE ? 2 : 0); // Bit 1: Is no collision + // Bits 2-7 reserved for future use + buffer.writeByte(flags); + + // Write box coordinates if it's not a NoCollisionBox + if ((flags & 2) == 0) { + SimpleCollisionBox simpleCollisionBox = (SimpleCollisionBox) box; + buffer.writeDouble(simpleCollisionBox.minX); + buffer.writeDouble(simpleCollisionBox.minY); + buffer.writeDouble(simpleCollisionBox.minZ); + buffer.writeDouble(simpleCollisionBox.maxX); + buffer.writeDouble(simpleCollisionBox.maxY); + buffer.writeDouble(simpleCollisionBox.maxZ); + } + } + + // Convert buffer to byte array + byte[] data = new byte[buffer.readableBytes()]; + buffer.readBytes(data); + + // Create and send packet + WrapperPlayServerPluginMessage packet = new WrapperPlayServerPluginMessage( + "grim:debug_hitbox", + data + ); + + // Send to all listeners + for (GrimPlayer listener : listeners) { + if (listener != null) { + listener.user.sendPacket(packet); + } + } + } finally { + // Release buffer to prevent memory leaks + buffer.release(); + } + } + + public boolean isEnabled() { + return !listeners.isEmpty(); + } + + /** + * Helper method to write a Vector to the ByteBuf + * @param buffer The buffer to write to + * @param vector The vector to write + */ + private void writeVector(ByteBuf buffer, Vector3dm vector) { + buffer.writeDouble(vector.getX()); + buffer.writeDouble(vector.getY()); + buffer.writeDouble(vector.getZ()); + } +} diff --git a/common/src/main/java/ac/grim/grimac/checks/impl/combat/EntityPierce.java b/common/src/main/java/ac/grim/grimac/checks/impl/combat/EntityPierce.java new file mode 100644 index 0000000000..114306219c --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/checks/impl/combat/EntityPierce.java @@ -0,0 +1,13 @@ +package ac.grim.grimac.checks.impl.combat; + +import ac.grim.grimac.checks.Check; +import ac.grim.grimac.checks.CheckData; +import ac.grim.grimac.checks.type.PacketCheck; +import ac.grim.grimac.player.GrimPlayer; + +@CheckData(name = "EntityPierce", configName = "EntityPierce", setback = 30) +public class EntityPierce extends Check implements PacketCheck { + public EntityPierce(GrimPlayer player) { + super(player); + } +} diff --git a/common/src/main/java/ac/grim/grimac/checks/impl/combat/Reach.java b/common/src/main/java/ac/grim/grimac/checks/impl/combat/Reach.java index 43cb2cc49e..cc61863ec3 100644 --- a/common/src/main/java/ac/grim/grimac/checks/impl/combat/Reach.java +++ b/common/src/main/java/ac/grim/grimac/checks/impl/combat/Reach.java @@ -18,13 +18,21 @@ import ac.grim.grimac.api.config.ConfigManager; import ac.grim.grimac.checks.Check; import ac.grim.grimac.checks.CheckData; +import ac.grim.grimac.checks.debug.HitboxDebugHandler; import ac.grim.grimac.checks.type.PacketCheck; import ac.grim.grimac.player.GrimPlayer; +import ac.grim.grimac.utils.collisions.datatypes.CollisionBox; +import ac.grim.grimac.utils.collisions.datatypes.NoCollisionBox; import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox; +import ac.grim.grimac.utils.data.BlockHitData; +import ac.grim.grimac.utils.data.EntityHitData; +import ac.grim.grimac.utils.data.HitData; +import ac.grim.grimac.utils.data.Pair; import ac.grim.grimac.utils.data.packetentity.PacketEntity; import ac.grim.grimac.utils.data.packetentity.dragon.PacketEntityEnderDragonPart; import ac.grim.grimac.utils.math.Vector3dm; import ac.grim.grimac.utils.nmsutil.ReachUtils; +import ac.grim.grimac.utils.nmsutil.WorldRayTrace; import com.github.retrooper.packetevents.event.PacketReceiveEvent; import com.github.retrooper.packetevents.protocol.attribute.Attributes; import com.github.retrooper.packetevents.protocol.entity.type.EntityType; @@ -32,16 +40,17 @@ import com.github.retrooper.packetevents.protocol.packettype.PacketType; import com.github.retrooper.packetevents.protocol.player.ClientVersion; import com.github.retrooper.packetevents.protocol.player.GameMode; +import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState; import com.github.retrooper.packetevents.util.Vector3d; +import com.github.retrooper.packetevents.util.Vector3i; import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerFlying; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; // You may not copy the check unless you are licensed under GPL @CheckData(name = "Reach", setback = 10) @@ -55,8 +64,14 @@ public class Reach extends Check implements PacketCheck { // Only one flag per reach attack, per entity, per tick. // We store position because lastX isn't reliable on teleports. private final Int2ObjectMap playerAttackQueue = new Int2ObjectOpenHashMap<>(); + // temporarily used to prevent falses in the wall hit check + private final Set blocksChangedThisTick = new HashSet<>(); + // extra distance to raytrace beyond player reach distance so we know how far beyond the legit distance a cheater hit + public static final double extraSearchDistance = 3; + + private boolean ignoreNonPlayerTargets; private boolean cancelImpossibleHits; - private double threshold; + public double threshold; private double cancelBuffer; // For the next 4 hits after using reach, we aggressively cancel reach public Reach(GrimPlayer player) { @@ -88,6 +103,10 @@ public void onPacketReceive(final PacketReceiveEvent event) { return; } + if (ignoreNonPlayerTargets && !entity.getType().equals(EntityTypes.PLAYER)) { + return; + } + // Dead entities cause false flags (https://github.com/GrimAnticheat/Grim/issues/546) if (entity.isDead) return; @@ -114,8 +133,9 @@ public void onPacketReceive(final PacketReceiveEvent event) { } // If the player set their look, or we know they have a new tick + final boolean isFlying = WrapperPlayClientPlayerFlying.isFlying(event.getPacketType()); if (isUpdate(event.getPacketType())) { - tickBetterReachCheckWithAngle(); + tickBetterReachCheckWithAngle(isFlying); } } @@ -149,7 +169,7 @@ private boolean isKnownInvalid(PacketEntity reachEntity) { } } - private void tickBetterReachCheckWithAngle() { + private void tickBetterReachCheckWithAngle(boolean isFlying) { for (Int2ObjectMap.Entry attack : playerAttackQueue.int2ObjectEntrySet()) { PacketEntity reachEntity = player.compensatedEntities.entityMap.get(attack.getIntKey()); if (reachEntity == null) continue; @@ -164,10 +184,21 @@ private void tickBetterReachCheckWithAngle() { String added = "type=" + reachEntity.getType().getName().getKey(); player.checkManager.getCheck(Hitboxes.class).flagAndAlert(result.verbose() + added); } + case WALL_HIT -> { + String added = reachEntity.getType() == EntityTypes.PLAYER ? "" : "type=" + reachEntity.getType().getName().getKey(); + player.checkManager.getCheck(WallHit.class).flagAndAlert(result.verbose() + added); + } + case ENTITY_PIERCE -> { + String added = reachEntity.getType() == EntityTypes.PLAYER ? "" : "type=" + reachEntity.getType().getName().getKey(); + player.checkManager.getCheck(EntityPierce.class).flagAndAlert(result.verbose() + added); + } } } playerAttackQueue.clear(); + // We can't use transactions for this because of this problem: + // transaction -> block changed applied -> 2nd transaction -> list cleared -> attack packet -> flying -> reach block hit checked, falses + if (isFlying) blocksChangedThisTick.clear(); } @NotNull @@ -196,28 +227,15 @@ private CheckResult checkReach(PacketEntity reachEntity, Vector3d from, boolean double minDistance = Double.MAX_VALUE; - // https://bugs.mojang.com/browse/MC-67665 - List possibleLookDirs = new ArrayList<>(Collections.singletonList(ReachUtils.getLook(player, player.xRot, player.yRot))); - - // If we are a tick behind, we don't know their next look so don't bother doing this - if (!isPrediction) { - possibleLookDirs.add(ReachUtils.getLook(player, player.lastXRot, player.yRot)); - - // 1.9+ players could be a tick behind because we don't get skipped ticks - if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9)) { - possibleLookDirs.add(ReachUtils.getLook(player, player.lastXRot, player.lastYRot)); - } - - // 1.7 players do not have any of these issues! They are always on the latest look vector - if (player.getClientVersion().isOlderThan(ClientVersion.V_1_8)) { - possibleLookDirs = Collections.singletonList(ReachUtils.getLook(player, player.xRot, player.yRot)); - } - } + // will store all lookVecsAndEyeHeight pairs that landed a hit on the target entity + // We only need to check for blocking intersections for these + List> lookVecsAndEyeHeights = new ArrayList<>(); final double maxReach = player.compensatedEntities.self.getAttributeValue(Attributes.ENTITY_INTERACTION_RANGE); // +3 would be 3 + 3 = 6, which is the pre-1.20.5 behaviour, preventing "Missed Hitbox" final double distance = maxReach + 3; final double[] possibleEyeHeights = player.getPossibleEyeHeights(); + final Vector3dm[] possibleLookDirs = player.getPossibleLookVectors(isPrediction); final Vector3dm eyePos = new Vector3dm(from.getX(), 0, from.getZ()); for (Vector3dm lookVec : possibleLookDirs) { for (double eye : possibleEyeHeights) { @@ -233,13 +251,44 @@ private CheckResult checkReach(PacketEntity reachEntity, Vector3d from, boolean if (intercept != null) { minDistance = Math.min(eyePos.distance(intercept), minDistance); + lookVecsAndEyeHeights.add(new Pair<>(lookVec, eye)); } } } + if (hitboxDebuggingEnabled()) + sendHitboxDebugData(reachEntity, from, lookVecsAndEyeHeights, isPrediction); + + HitData foundHitData = null; + // If the entity is within range of the player (we'll flag anyway if not, so no point checking blocks in this case) + // Ignore when could be hitting through a moving shulker, piston blocks. They are just too glitchy/uncertain to check. + if (minDistance <= distance - extraSearchDistance && !player.compensatedWorld.isNearHardEntity(player.boundingBox.copy().expand(4))) { + // we can optimize didRayTraceHit more to only rayTrace up to the maximize distance of all rays that hit to the target... + // I'm too lazy to do that and we don't need to optimize that much yet so... + final @Nullable Pair hitResult = WorldRayTrace.didRayTraceHit(player, reachEntity, lookVecsAndEyeHeights, from); + HitData hitData = hitResult.second(); + // If the returned hit result was NOT the target entity we flag the check + if (hitData instanceof EntityHitData && + player.compensatedEntities.getPacketEntityID(((EntityHitData) hitData).getEntity()) != player.compensatedEntities.getPacketEntityID(reachEntity)) { + minDistance = Double.MIN_VALUE; + foundHitData = hitData; + // until I fix block modeling exempt any blocks changed this tick + } else if (hitData instanceof BlockHitData && !blocksChangedThisTick.contains(((BlockHitData) hitData).getPosition())) { + minDistance = Double.MIN_VALUE; + foundHitData = hitData; + } + } + // if the entity is not exempt and the entity is alive if ((!blacklisted.contains(reachEntity.getType()) && reachEntity.isLivingEntity()) || reachEntity.getType() == EntityTypes.END_CRYSTAL) { - if (minDistance == Double.MAX_VALUE) { + if (minDistance == Double.MIN_VALUE && foundHitData != null) { + cancelBuffer = 1; + if (foundHitData instanceof BlockHitData) { + return new CheckResult(ResultType.WALL_HIT, "Hit block=" + ((BlockHitData) foundHitData).getState().getType().getName() + " "); + } else { // entity hit data + return new CheckResult(ResultType.ENTITY_PIERCE, "Hit entity=" + ((EntityHitData) foundHitData).getEntity().getType().getName() + " "); + } + } else if (minDistance == Double.MAX_VALUE) { cancelBuffer = 1; return new CheckResult(ResultType.HITBOX, ""); } else if (minDistance > maxReach) { @@ -255,12 +304,13 @@ private CheckResult checkReach(PacketEntity reachEntity, Vector3d from, boolean @Override public void onReload(ConfigManager config) { + this.ignoreNonPlayerTargets = config.getBooleanElse("Reach.ignore-non-player-targets", false); this.cancelImpossibleHits = config.getBooleanElse("Reach.block-impossible-hits", true); this.threshold = config.getDoubleElse("Reach.threshold", 0.0005); } private enum ResultType { - REACH, HITBOX, NONE + REACH, HITBOX, WALL_HIT, ENTITY_PIERCE, NONE } private record CheckResult(ResultType type, String verbose) { @@ -268,4 +318,69 @@ public boolean isFlag() { return type != ResultType.NONE; } } + + public void handleBlockChange(Vector3i vector3i, WrappedBlockState state) { + if (blocksChangedThisTick.size() >= 40) return; // Don't let players freeze movement packets to grow this + // Only do this for nearby blocks + if (new Vector3dm(vector3i.x, vector3i.y, vector3i.z).distanceSquared(new Vector3dm(player.x, player.y, player.z)) > 6) return; + // Only do this if the state really had any world impact + if (state.equals(player.compensatedWorld.getBlock(vector3i))) return; + blocksChangedThisTick.add(vector3i); + } + + private boolean hitboxDebuggingEnabled() { + return player.checkManager.getCheck(HitboxDebugHandler.class).isEnabled(); + } + + private void sendHitboxDebugData(PacketEntity reachEntity, Vector3d from, List> lookVecsAndEyeHeights, boolean isPrediction) { + Map hitboxes = new HashMap<>(); + for (Int2ObjectMap.Entry entry : player.compensatedEntities.entityMap.int2ObjectEntrySet()) { + PacketEntity entity = entry.getValue(); + if (!entity.canHit()) continue; + + CollisionBox box; + + if (entity.equals(reachEntity)) { + // Target entity gets expanded hitbox + box = entity.getPossibleCollisionBoxes(); + SimpleCollisionBox sBox = (SimpleCollisionBox) box; + sBox.expand(threshold); + + // Add movement threshold uncertainty for 1.9+ or non-position updates + if (!player.packetStateData.didLastLastMovementIncludePosition + || player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9)) { + sBox.expand(player.getMovementThreshold()); + } + } else { + // Non-target entities + box = entity.getMinimumPossibleCollisionBoxes(); + if (box instanceof NoCollisionBox) { + hitboxes.put(entry.getIntKey(), NoCollisionBox.INSTANCE); + continue; + } else if (box instanceof SimpleCollisionBox) { + SimpleCollisionBox sBox = (SimpleCollisionBox) box; + sBox.expand(-threshold); + // Shrink non-target entities by movement threshold when applicable + if (!player.packetStateData.didLastLastMovementIncludePosition + || player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9)) { + sBox.expand(-player.getMovementThreshold()); + } + } + } + + // Add 1.8 and below extra hitbox size + if (player.getClientVersion().isOlderThan(ClientVersion.V_1_9) + && box instanceof SimpleCollisionBox) { + ((SimpleCollisionBox) box).expand(0.1f); + } + + hitboxes.put(entry.getIntKey(), box); + } + + player.checkManager.getCheck(HitboxDebugHandler.class).sendHitboxData(hitboxes, + Collections.singleton(player.compensatedEntities.getPacketEntityID(reachEntity)), + lookVecsAndEyeHeights, + new Vector3dm(from.getX(), from.getY(), from.getZ()), + isPrediction, player.compensatedEntities.self.getAttributeValue(Attributes.ENTITY_INTERACTION_RANGE)); + } } diff --git a/common/src/main/java/ac/grim/grimac/checks/impl/combat/WallHit.java b/common/src/main/java/ac/grim/grimac/checks/impl/combat/WallHit.java new file mode 100644 index 0000000000..2033e2476f --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/checks/impl/combat/WallHit.java @@ -0,0 +1,13 @@ +package ac.grim.grimac.checks.impl.combat; + +import ac.grim.grimac.checks.Check; +import ac.grim.grimac.checks.CheckData; +import ac.grim.grimac.checks.type.PacketCheck; +import ac.grim.grimac.player.GrimPlayer; + +@CheckData(name = "WallHit", configName = "WallHit", setback = 20) +public class WallHit extends Check implements PacketCheck { + public WallHit(GrimPlayer player) { + super(player); + } +} diff --git a/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryA.java b/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryA.java new file mode 100644 index 0000000000..1e15d49bba --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryA.java @@ -0,0 +1,42 @@ +package ac.grim.grimac.checks.impl.inventory; + +import ac.grim.grimac.checks.CheckData; +import ac.grim.grimac.checks.type.InventoryCheck; +import ac.grim.grimac.player.GrimPlayer; +import com.github.retrooper.packetevents.event.PacketReceiveEvent; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity.InteractAction; + +@CheckData(name = "InventoryA", setback = 3, description = "Attacked an entity while inventory is open") +public class InventoryA extends InventoryCheck { + public InventoryA(GrimPlayer player) { + super(player); + } + + @Override + public void onPacketReceive(PacketReceiveEvent event) { + super.onPacketReceive(event); + + if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) { + WrapperPlayClientInteractEntity wrapper = new WrapperPlayClientInteractEntity(event); + + if (wrapper.getAction() != InteractAction.ATTACK) return; + + // Is not possible to attack while the inventory is open. + if (player.hasInventoryOpen) { + if (flagAndAlert()) { + // Cancel the packet + if (shouldModifyPackets()) { + event.setCancelled(true); + player.onPacketCancel(); + } + if (!isNoSetbackPermission()) + closeInventory(); + } + } else { + reward(); + } + } + } +} diff --git a/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryB.java b/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryB.java new file mode 100644 index 0000000000..ee42ad0a2b --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryB.java @@ -0,0 +1,35 @@ +package ac.grim.grimac.checks.impl.inventory; + +import ac.grim.grimac.checks.CheckData; +import ac.grim.grimac.checks.type.InventoryCheck; +import ac.grim.grimac.player.GrimPlayer; +import com.github.retrooper.packetevents.event.PacketReceiveEvent; +import com.github.retrooper.packetevents.protocol.player.DiggingAction; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerDigging; + +@CheckData(name = "InventoryB", setback = 3, description = "Started digging blocks while inventory is open") +public class InventoryB extends InventoryCheck { + public InventoryB(GrimPlayer player) { + super(player); + } + + public void handle(PacketReceiveEvent event, WrapperPlayClientPlayerDigging wrapper) { + if (wrapper.getAction() != DiggingAction.START_DIGGING) return; + + // Is not possible to start digging a block while the inventory is open. + if (player.hasInventoryOpen) { + if (flagAndAlert()) { + // Cancel the packet + if (shouldModifyPackets()) { + event.setCancelled(true); + player.onPacketCancel(); + } + if (!isNoSetbackPermission()) { + closeInventory(); + } + } + } else { + reward(); + } + } +} diff --git a/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryC.java b/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryC.java new file mode 100644 index 0000000000..20d3852671 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryC.java @@ -0,0 +1,30 @@ +package ac.grim.grimac.checks.impl.inventory; + +import ac.grim.grimac.checks.CheckData; +import ac.grim.grimac.checks.type.InventoryCheck; +import ac.grim.grimac.player.GrimPlayer; +import ac.grim.grimac.utils.anticheat.update.BlockPlace; + +@CheckData(name = "InventoryC", setback = 3, description = "Placed a block while inventory is open") +public class InventoryC extends InventoryCheck { + + public InventoryC(GrimPlayer player) { + super(player); + } + + public void onBlockPlace(final BlockPlace place) { + // It is not possible to place a block while the inventory is open + if (player.hasInventoryOpen) { + if (flagAndAlert()) { + if (shouldModifyPackets()) { + place.resync(); + } + if (!isNoSetbackPermission()) { + closeInventory(); + } + } + } else { + reward(); + } + } +} diff --git a/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryD.java b/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryD.java new file mode 100644 index 0000000000..b643e4b9c0 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryD.java @@ -0,0 +1,85 @@ +package ac.grim.grimac.checks.impl.inventory; + +import ac.grim.grimac.checks.CheckData; +import ac.grim.grimac.checks.type.InventoryCheck; +import ac.grim.grimac.player.GrimPlayer; +import ac.grim.grimac.utils.anticheat.update.PredictionComplete; +import ac.grim.grimac.utils.data.VectorData; +import ac.grim.grimac.utils.data.VectorData.MoveVectorData; +import ac.grim.grimac.utils.data.VehicleData; + +import java.util.StringJoiner; + +@CheckData(name = "InventoryD", setback = 1, decay = 0.25) +public class InventoryD extends InventoryCheck { + private int horseJumpVerbose; + + public InventoryD(GrimPlayer player) { + super(player); + } + + @Override + public void onPredictionComplete(final PredictionComplete predictionComplete) { + if (!predictionComplete.isChecked() || + predictionComplete.getData().isTeleport() || + player.getSetbackTeleportUtil().blockOffsets || + player.packetStateData.lastPacketWasTeleport || + player.packetStateData.isSlowedByUsingItem() || + System.currentTimeMillis() - player.lastBlockPlaceUseItem < 50L) { + return; + } + + if (player.hasInventoryOpen) { + boolean inVehicle = player.inVehicle(); + boolean isJumping, isMoving; + + if (inVehicle) { + VehicleData vehicle = player.vehicleData; + + // Will flag once if player open anything with pressed space bar + isJumping = vehicle.nextHorseJump > 0 && horseJumpVerbose++ >= 1; + isMoving = vehicle.nextVehicleForward != 0 || vehicle.nextVehicleHorizontal != 0; + } else { + MoveVectorData move = findMovement(player.predictedVelocity); + + isJumping = player.predictedVelocity.isJump(); + isMoving = move != null && (move.x != 0 || move.z != 0); + } + + if (!isMoving && !isJumping) { + reward(); + return; + } + + if (flag()) { + if (!isNoSetbackPermission()) + closeInventory(); + + StringJoiner joiner = new StringJoiner(" "); + + if (isMoving) joiner.add("moving"); + if (isJumping) joiner.add("jumping"); + if (inVehicle) joiner.add("inVehicle"); + + alert(joiner.toString()); + } + } else { + horseJumpVerbose = 0; + } + } + + private MoveVectorData findMovement(VectorData vectorData) { + if (vectorData instanceof MoveVectorData) { + return (MoveVectorData) vectorData; + } + + while (vectorData != null) { + vectorData = vectorData.lastVector; + if (vectorData instanceof MoveVectorData) { + return (MoveVectorData) vectorData; + } + } + + return null; + } +} diff --git a/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryE.java b/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryE.java new file mode 100644 index 0000000000..caf6270001 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryE.java @@ -0,0 +1,50 @@ +package ac.grim.grimac.checks.impl.inventory; + +import ac.grim.grimac.checks.CheckData; +import ac.grim.grimac.checks.type.InventoryCheck; +import ac.grim.grimac.player.GrimPlayer; +import com.github.retrooper.packetevents.event.PacketReceiveEvent; +import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; + +@CheckData(name = "InventoryE", setback = 3, description = "Sent a held item change packet while inventory is open") +public class InventoryE extends InventoryCheck { + private long lastTransaction = Long.MAX_VALUE; // Impossible transaction ID + + public InventoryE(GrimPlayer player) { + super(player); + } + + @Override + public void onPacketReceive(PacketReceiveEvent event) { + super.onPacketReceive(event); + + if (event.getPacketType() == PacketType.Play.Client.HELD_ITEM_CHANGE) { + // It is not possible to change hotbar slots with held item change while the inventory is open + // A container click packet would be sent instead + if (player.hasInventoryOpen) { + if (this.lastTransaction < player.lastTransactionReceived.get() + && flagAndAlert()) { + // Cancel the packet + if (shouldModifyPackets()) { + event.setCancelled(true); + player.onPacketCancel(); + player.getInventory().needResend = true; + } + if (!isNoSetbackPermission()) { + closeInventory(); + } + } + } else { + reward(); + } + } + } + + @Override + public void onPacketSend(PacketSendEvent event) { + if (event.getPacketType() == PacketType.Play.Server.HELD_ITEM_CHANGE) { + this.lastTransaction = player.lastTransactionSent.get(); + } + } +} diff --git a/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryF.java b/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryF.java new file mode 100644 index 0000000000..28171b20e7 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryF.java @@ -0,0 +1,46 @@ +package ac.grim.grimac.checks.impl.inventory; + +import ac.grim.grimac.checks.CheckData; +import ac.grim.grimac.checks.type.InventoryCheck; +import ac.grim.grimac.player.GrimPlayer; +import ac.grim.grimac.utils.inventory.InventoryDesyncStatus; +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.event.PacketReceiveEvent; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.player.ClientVersion; + +@CheckData(name = "InventoryF", setback = 3, description = "Sent a click window packet without a open inventory", experimental = true) +public class InventoryF extends InventoryCheck { + + public InventoryF(GrimPlayer player) { + super(player); + } + + @Override + public void onPacketReceive(PacketReceiveEvent event) { + // Exempt on 1.9+ server version due to the Via hack done in PacketPlayerWindow, the exemption can be deleted + // once we are ahead of ViaVersion + if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_9) + || player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9)) return; + + super.onPacketReceive(event); + + if (event.getPacketType() == PacketType.Play.Client.CLICK_WINDOW) { + if (!player.hasInventoryOpen && player.inventoryDesyncStatus == InventoryDesyncStatus.NOT_DESYNCED) { + if (flagAndAlert()) { + // Cancel the packet + if (shouldModifyPackets()) { + event.setCancelled(true); + player.onPacketCancel(); + } + if (!isNoSetbackPermission()) { + closeInventory(); + } + } + } else { + reward(); + } + } + } +} diff --git a/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryG.java b/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryG.java new file mode 100644 index 0000000000..a189df88cd --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/checks/impl/inventory/InventoryG.java @@ -0,0 +1,40 @@ +package ac.grim.grimac.checks.impl.inventory; + +import ac.grim.grimac.checks.CheckData; +import ac.grim.grimac.checks.type.InventoryCheck; +import ac.grim.grimac.player.GrimPlayer; +import com.github.retrooper.packetevents.event.PacketReceiveEvent; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientEntityAction; + +@CheckData(name = "InventoryG", setback = 3, description = "Sent a entity action packet while inventory is open", experimental = true) +public class InventoryG extends InventoryCheck { + + public InventoryG(GrimPlayer player) { + super(player); + } + + @Override + public void onPacketReceive(PacketReceiveEvent event) { + if (player.packetStateData.lastPacketWasTeleport) return; + super.onPacketReceive(event); + + if (event.getPacketType() == PacketType.Play.Client.ENTITY_ACTION) { + WrapperPlayClientEntityAction wrapper = new WrapperPlayClientEntityAction(event); + WrapperPlayClientEntityAction.Action action = wrapper.getAction(); + + if (action == WrapperPlayClientEntityAction.Action.STOP_SNEAKING + || action == WrapperPlayClientEntityAction.Action.STOP_SPRINTING) { + return; + } + + if (player.hasInventoryOpen) { + if (flagAndAlert() && !isNoSetbackPermission()) { + closeInventory(); + } + } else { + reward(); + } + } + } +} diff --git a/common/src/main/java/ac/grim/grimac/checks/impl/prediction/DebugHandler.java b/common/src/main/java/ac/grim/grimac/checks/impl/prediction/DebugHandler.java index ea2650dd4d..5f4d07981e 100644 --- a/common/src/main/java/ac/grim/grimac/checks/impl/prediction/DebugHandler.java +++ b/common/src/main/java/ac/grim/grimac/checks/impl/prediction/DebugHandler.java @@ -133,8 +133,14 @@ private String pickColor(double offset, double totalOffset) { } @Override - public void toggleListener(GrimPlayer player) { - if (!listeners.remove(player)) listeners.add(player); + public boolean toggleListener(GrimPlayer player) { + boolean wasPresent = listeners.remove(player); + if (wasPresent) { + return false; + } else { + listeners.add(player); + return true; + } } @Override diff --git a/common/src/main/java/ac/grim/grimac/checks/impl/prediction/OffsetHandler.java b/common/src/main/java/ac/grim/grimac/checks/impl/prediction/OffsetHandler.java index 3f5197dd33..3b278b3207 100644 --- a/common/src/main/java/ac/grim/grimac/checks/impl/prediction/OffsetHandler.java +++ b/common/src/main/java/ac/grim/grimac/checks/impl/prediction/OffsetHandler.java @@ -8,7 +8,9 @@ import ac.grim.grimac.checks.type.PostPredictionCheck; import ac.grim.grimac.player.GrimPlayer; import ac.grim.grimac.utils.anticheat.update.PredictionComplete; +import ac.grim.grimac.platform.api.scheduler.PlatformScheduler; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @CheckData(name = "Simulation", decay = 0.02) @@ -20,6 +22,9 @@ public class OffsetHandler extends Check implements PostPredictionCheck { double immediateSetbackThreshold; double maxAdvantage; double maxCeiling; + double vlScale; + long vlScaleDelay; + double maxVlsPerFlag; double setbackViolationThreshold; // Current advantage gained double advantageGained = 0; @@ -64,6 +69,30 @@ public void onPredictionComplete(final PredictionComplete predictionComplete) { predictionComplete.setIdentifier(flagId); } + double extra = Math.ceil(offset * vlScale) - 1.0; + if (extra > 0) { + int amount = (int) Math.min(maxVlsPerFlag, extra); + violations += amount; + + if (vlScaleDelay > 0 && player.platformPlayer != null) { + for (int i = 0; i < amount; i++) { + long ticks = PlatformScheduler.convertTimeToTicks(vlScaleDelay * (i + 1), TimeUnit.MILLISECONDS); + GrimAPI.INSTANCE.getScheduler().getEntityScheduler().runDelayed( + player.platformPlayer, + GrimAPI.INSTANCE.getGrimPlugin(), + () -> { + player.punishmentManager.addExtraViolation(this, 1); + alert(verbose); + }, + null, + ticks + ); + } + } else { + player.punishmentManager.addExtraViolation(this, amount); + } + } + if ((advantageGained >= maxAdvantage || offset >= immediateSetbackThreshold) && !isNoSetbackPermission() && violations >= setbackViolationThreshold) { @@ -104,9 +133,13 @@ public void onReload(ConfigManager config) { immediateSetbackThreshold = config.getDoubleElse("Simulation.immediate-setback-threshold", 0.1); maxAdvantage = config.getDoubleElse("Simulation.max-advantage", 1); maxCeiling = config.getDoubleElse("Simulation.max-ceiling", 4); + vlScale = Math.max(1.0, config.getDoubleElse("Simulation.vl-scale", 1)); + vlScaleDelay = config.getLongElse("Simulation.vl-scale-delay", 0L); + maxVlsPerFlag = config.getDoubleElse("Simulation.max-vls-per-flag", -1); setbackViolationThreshold = config.getDoubleElse("Simulation.setback-violation-threshold", 1); if (maxAdvantage == -1) maxAdvantage = Double.MAX_VALUE; if (immediateSetbackThreshold == -1) immediateSetbackThreshold = Double.MAX_VALUE; + if (maxVlsPerFlag == -1) maxVlsPerFlag = Double.MAX_VALUE; } public boolean doesOffsetFlag(double offset) { diff --git a/common/src/main/java/ac/grim/grimac/checks/type/InventoryCheck.java b/common/src/main/java/ac/grim/grimac/checks/type/InventoryCheck.java new file mode 100644 index 0000000000..5c3f1ed94c --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/checks/type/InventoryCheck.java @@ -0,0 +1,67 @@ +package ac.grim.grimac.checks.type; + +import ac.grim.grimac.player.GrimPlayer; +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.event.PacketReceiveEvent; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientCloseWindow; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerCloseWindow; +import org.jetbrains.annotations.MustBeInvokedByOverriders; + +public class InventoryCheck extends BlockPlaceCheck implements PacketCheck { + // Impossible transaction ID + protected static final long NONE = Long.MAX_VALUE; + + protected long closeTransaction = NONE; + protected int closePacketsToSkip; + + public InventoryCheck(GrimPlayer player) { + super(player); + } + + @Override + @MustBeInvokedByOverriders + public void onPacketReceive(final PacketReceiveEvent event) { + if (event.getPacketType() == PacketType.Play.Client.CLICK_WINDOW) { + // Disallow any clicks if inventory is closing + if (closeTransaction != NONE && shouldModifyPackets()) { + event.setCancelled(true); + player.onPacketCancel(); + player.getInventory().needResend = true; + } + } else if (event.getPacketType() == PacketType.Play.Client.CLOSE_WINDOW) { + // Players with high ping can close inventory faster than send transaction back + if (closeTransaction != NONE && closePacketsToSkip-- <= 0) { + closeTransaction = NONE; + } + } + } + + public void closeInventory() { + if (closeTransaction != NONE) { + return; + } + + int windowId = player.getInventory().openWindowID; + + player.user.writePacket(new WrapperPlayServerCloseWindow(windowId)); + + // Force close inventory on server side + closePacketsToSkip = 1; // Sending close packet to itself, so skip it + PacketEvents.getAPI().getProtocolManager().receivePacket( + player.user.getChannel(), new WrapperPlayClientCloseWindow(windowId) + ); + + player.sendTransaction(); + + int transaction = player.lastTransactionSent.get(); + closeTransaction = transaction; + player.latencyUtils.addRealTimeTask(transaction, () -> { + if (closeTransaction == transaction) { + closeTransaction = NONE; + } + }); + + player.user.flushPackets(); + } +} diff --git a/common/src/main/java/ac/grim/grimac/command/commands/GrimDebug.java b/common/src/main/java/ac/grim/grimac/command/commands/GrimDebug.java index 71bb6f33aa..756a4f6a3f 100644 --- a/common/src/main/java/ac/grim/grimac/command/commands/GrimDebug.java +++ b/common/src/main/java/ac/grim/grimac/command/commands/GrimDebug.java @@ -1,6 +1,7 @@ package ac.grim.grimac.command.commands; import ac.grim.grimac.GrimAPI; +import ac.grim.grimac.checks.debug.HitboxDebugHandler; import ac.grim.grimac.command.BuildableCommand; import ac.grim.grimac.platform.api.command.PlayerSelector; import ac.grim.grimac.platform.api.sender.Sender; @@ -36,9 +37,16 @@ public void register(CommandManager commandManager) { .required("target", GrimAPI.INSTANCE.getParserDescriptors().getSinglePlayer()) .handler(this::handleConsoleDebug); + Command.Builder hitboxDebugCommand = grimCommand + .literal("hitboxdebug", Description.of("Toggle hitbox debug visualization")) + .permission("grim.hitboxdebug") + .optional("target", GrimAPI.INSTANCE.getParserDescriptors().getSinglePlayer(), Description.of("Player to debug (defaults to self if sender is player)")) + .handler(this::handleHitboxDebug); + // Register command commandManager.command(debugCommand); commandManager.command(consoleDebugCommand); + commandManager.command(hitboxDebugCommand); } private void handleDebug(@NonNull CommandContext context) { @@ -81,6 +89,48 @@ private void handleConsoleDebug(@NonNull CommandContext context) { sender.sendMessage(message); } + private void handleHitboxDebug(@NonNull CommandContext context) { + Sender sender = context.sender(); + PlayerSelector playerSelector = context.getOrDefault("target", null); + + // Hitbox debug requires a *player* to be the listener (the one seeing the boxes) + if (!sender.isPlayer()) { + sender.sendMessage(MessageUtil.getParsedComponent(sender, + "hitboxdebug-player-only", + "%prefix% &cHitbox debug can only be toggled by players.") + ); + return; + } + + // Determine the target player whose hitboxes are being debugged + GrimPlayer targetGrimPlayer = parseTarget(sender, playerSelector == null ? sender : playerSelector.getSinglePlayer()); + if (targetGrimPlayer == null) return; + + // Get the sender's GrimPlayer data, as they are the listener + GrimPlayer senderGrimPlayer = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(sender.getUniqueId()); + if (senderGrimPlayer == null) { + sender.sendMessage(Component.text("Could not find your player data to register as a listener.", NamedTextColor.RED)); + return; + } + + // Get the HitboxDebugHandler check instance and toggle the listener + HitboxDebugHandler hitboxHandler = targetGrimPlayer.checkManager.getCheck(HitboxDebugHandler.class); + if (hitboxHandler == null) { + sender.sendMessage(Component.text("HitboxDebugHandler check not found for target player.", NamedTextColor.RED)); + return; + } + + boolean enabled = hitboxHandler.toggleListener(senderGrimPlayer); // Pass the sender/listener + + // Send feedback message + Component message = Component.text() + .append(Component.text("Hitbox debug listener for ", NamedTextColor.GRAY)) + .append(Component.text(targetGrimPlayer.getName(), NamedTextColor.WHITE)) + .append(Component.text(enabled ? " enabled." : " disabled.", NamedTextColor.GRAY)) + .build(); + sender.sendMessage(message); + } + private @Nullable GrimPlayer parseTarget(@NonNull Sender sender, @Nullable Sender t) { if (sender.isConsole() && t == null) { sender.sendMessage(MessageUtil.getParsedComponent(sender, "console-specify-target", "%prefix% &cYou must specify a target as the console!")); diff --git a/common/src/main/java/ac/grim/grimac/command/commands/GrimHistory.java b/common/src/main/java/ac/grim/grimac/command/commands/GrimHistory.java new file mode 100644 index 0000000000..42bdc37dd5 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/command/commands/GrimHistory.java @@ -0,0 +1,103 @@ +package ac.grim.grimac.command.commands; + +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.player.OfflinePlatformPlayer; +import ac.grim.grimac.platform.api.sender.Sender; +import ac.grim.grimac.utils.anticheat.MessageUtil; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.parser.standard.IntegerParser; +import org.incendo.cloud.parser.standard.StringParser; + +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class GrimHistory implements BuildableCommand { + + @Override + public void register(CommandManager commandManager) { + commandManager.command( + commandManager.commandBuilder("grim", "grimac") + .literal("history", "hist") + .permission("grim.help") + .required("target", StringParser.stringParser()) + .optional("page", IntegerParser.integerParser()) + .permission("grim.history") + .handler(this::handleHistory) + ); + } + + private void handleHistory(CommandContext context) { + Sender sender = context.sender(); + String target = context.get("target"); + Integer page = context.getOrDefault("page", 1); + + if (!GrimAPI.INSTANCE.getViolationDatabaseManager().isEnabled()) { + String msg = GrimAPI.INSTANCE.getConfigManager().getConfig() + .getStringElse("grim-history-disabled", + "%prefix% &cHistory subsystem is disabled!"); + sender.sendMessage(MessageUtil.miniMessage(msg)); + return; + } + + GrimAPI.INSTANCE.getScheduler().getAsyncScheduler().runNow(GrimAPI.INSTANCE.getGrimPlugin(), () -> { + int entriesPerPage = GrimAPI.INSTANCE.getConfigManager().getConfig().getIntElse("history.entries-per-page", 15); + String header = GrimAPI.INSTANCE.getConfigManager().getConfig().getStringElse("grim-history-header", + "%prefix% &bShowing logs for &f%player% (&f%page%&b/&f%maxPages%&f)"); + String logFormat = GrimAPI.INSTANCE.getConfigManager().getConfig().getStringElse("grim-history-entry", + "%prefix% &8[&f%server%&8] &bFailed &f%check% (x&c%vl%&f) &7%verbose% (&b%timeago% ago&7)"); + + OfflinePlatformPlayer targetPlayer = GrimAPI.INSTANCE.getPlatformPlayerFactory().getOfflineFromName(target); + + ViolationDatabaseManager violations = GrimAPI.INSTANCE.getViolationDatabaseManager(); + int logCount = violations.getLogCount(targetPlayer.getUniqueId()); + List logs = violations.getViolations(targetPlayer.getUniqueId(), page, entriesPerPage); + int maxPages = (int) Math.ceil((float) logCount / entriesPerPage); + + sender.sendMessage(MessageUtil.miniMessage(MessageUtil.replacePlaceholders(sender, header + .replace("%player%", targetPlayer.getName()) + .replace("%page%", String.valueOf(page)) + .replace("%maxPages%", String.valueOf(maxPages)) + ))); + + for (int i = logs.size() - 1; i >= 0; i--) { + Violation log = logs.get(i); + sender.sendMessage(MessageUtil.miniMessage(MessageUtil.replacePlaceholders(sender, logFormat + .replace("%player%", targetPlayer.getName()) + .replace("%check%", log.getCheckName()) + .replace("%verbose%", log.getVerbose()) + .replace("%vl%", String.valueOf(log.getVl())) + .replace("%timeago%", getTimeAgo(log.getCreatedAt())) + .replace("%server%", log.getServerName()) + ))); + } + }); + } + + private String getTimeAgo(Date date) { + long durationMillis = new Date().getTime() - date.getTime(); + + long days = TimeUnit.MILLISECONDS.toDays(durationMillis); + durationMillis -= TimeUnit.DAYS.toMillis(days); + + long hours = TimeUnit.MILLISECONDS.toHours(durationMillis); + durationMillis -= TimeUnit.HOURS.toMillis(hours); + + long minutes = TimeUnit.MILLISECONDS.toMinutes(durationMillis); + durationMillis -= TimeUnit.MINUTES.toMillis(minutes); + + long seconds = TimeUnit.MILLISECONDS.toSeconds(durationMillis); + + StringBuilder result = new StringBuilder(); + if (days > 0) result.append(days).append("d "); + if (hours > 0) result.append(hours).append("h "); + if (minutes > 0) result.append(minutes).append("m "); + if (seconds > 0) result.append(seconds).append("s"); + + return result.toString().trim(); + } +} diff --git a/common/src/main/java/ac/grim/grimac/events/packets/CheckManagerListener.java b/common/src/main/java/ac/grim/grimac/events/packets/CheckManagerListener.java index 12dd35c221..33f227d298 100644 --- a/common/src/main/java/ac/grim/grimac/events/packets/CheckManagerListener.java +++ b/common/src/main/java/ac/grim/grimac/events/packets/CheckManagerListener.java @@ -1,6 +1,7 @@ package ac.grim.grimac.events.packets; import ac.grim.grimac.GrimAPI; +import ac.grim.grimac.checks.impl.inventory.InventoryB; import ac.grim.grimac.player.GrimPlayer; import ac.grim.grimac.utils.anticheat.update.BlockBreak; import ac.grim.grimac.utils.anticheat.update.BlockPlace; @@ -65,7 +66,7 @@ public CheckManagerListener() { } private static void placeWaterLavaSnowBucket(GrimPlayer player, ItemStack held, StateType toPlace, InteractionHand hand, int sequence) { - HitData data = WorldRayTrace.getNearestBlockHitResult(player, StateTypes.AIR, false, true, true); + BlockHitData data = WorldRayTrace.getNearestBlockHitResult(player, StateTypes.AIR, false, true, true); if (data != null) { BlockPlace blockPlace = new BlockPlace(player, hand, data.getPosition(), data.getClosestDirection().getFaceValue(), data.getClosestDirection(), held, data, sequence); @@ -281,7 +282,7 @@ private static void handleBlockPlaceOrUseItem(PacketWrapper packet, GrimPlaye } private static void placeBucket(GrimPlayer player, InteractionHand hand, int sequence) { - HitData data = WorldRayTrace.getNearestBlockHitResult(player, null, true, false, true); + BlockHitData data = WorldRayTrace.getNearestBlockHitResult(player, null, true, false, true); if (data != null) { BlockPlace blockPlace = new BlockPlace(player, hand, data.getPosition(), data.getClosestDirection().getFaceValue(), data.getClosestDirection(), ItemStack.EMPTY, data, sequence); @@ -360,7 +361,7 @@ public static void setPlayerItem(GrimPlayer player, InteractionHand hand, ItemTy } private static void placeLilypad(GrimPlayer player, InteractionHand hand, int sequence) { - HitData data = WorldRayTrace.getNearestBlockHitResult(player, null, true, false, true); + BlockHitData data = WorldRayTrace.getNearestBlockHitResult(player, null, true, false, true); if (data != null) { // A lilypad cannot replace a fluid @@ -564,6 +565,8 @@ public void onPacketReceive(PacketReceiveEvent event) { final WrapperPlayClientPlayerDigging packet = new WrapperPlayClientPlayerDigging(event); final DiggingAction action = packet.getAction(); + player.checkManager.getPacketCheck(InventoryB.class).handle(event, packet); + if (action == DiggingAction.START_DIGGING || action == DiggingAction.FINISHED_DIGGING || action == DiggingAction.CANCELLED_DIGGING) { final BlockBreak blockBreak = new BlockBreak(player, packet.getBlockPosition(), packet.getBlockFace(), packet.getBlockFaceId(), action, packet.getSequence(), player.compensatedWorld.getBlock(packet.getBlockPosition())); diff --git a/common/src/main/java/ac/grim/grimac/events/packets/PacketPlayerWindow.java b/common/src/main/java/ac/grim/grimac/events/packets/PacketPlayerWindow.java new file mode 100644 index 0000000000..404bf1b738 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/events/packets/PacketPlayerWindow.java @@ -0,0 +1,163 @@ +package ac.grim.grimac.events.packets; + +import ac.grim.grimac.GrimAPI; +import ac.grim.grimac.player.GrimPlayer; +import ac.grim.grimac.utils.data.packetentity.PacketEntitySelf; +import ac.grim.grimac.utils.inventory.InventoryDesyncStatus; +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.event.PacketListenerAbstract; +import com.github.retrooper.packetevents.event.PacketListenerPriority; +import com.github.retrooper.packetevents.event.PacketReceiveEvent; +import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.protocol.packettype.PacketType; +import com.github.retrooper.packetevents.protocol.player.ClientVersion; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientClientStatus; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientClientStatus.Action; +import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerFlying; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerOpenWindow; + +public class PacketPlayerWindow extends PacketListenerAbstract { + + public PacketPlayerWindow() { + super(PacketListenerPriority.LOWEST); + } + + @Override + public void onPacketReceive(PacketReceiveEvent event) { + if (WrapperPlayClientPlayerFlying.isFlying(event.getPacketType()) && !event.isCancelled()) { + GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser()); + if (player == null) return; + + if (player.hasInventoryOpen && isNearNetherPortal(player)) { + handleInventoryClose(player, InventoryDesyncStatus.NETHER_PORTAL); + } + } + + // Client Status is sent in 1.7-1.8 + if (event.getPacketType() == PacketType.Play.Client.CLIENT_STATUS) { + WrapperPlayClientClientStatus wrapper = new WrapperPlayClientClientStatus(event); + + if (wrapper.getAction() == Action.OPEN_INVENTORY_ACHIEVEMENT) { + GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser()); + if (player == null) return; + + handleInventoryOpen(player); + } + } + + // We need to do this due to 1.9 not sending anymore the inventory action in the Client Status + if (event.getPacketType() == PacketType.Play.Client.CLICK_WINDOW) { + GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser()); + if (player == null) return; + + // TODO: Remove this check after we finish the before ViaVersion injection + // Explanation: On 1.7 and 1.8 we have OPEN_INVENTORY_ACHIEVEMENT on CLIENT_STATUS packet + // but after Via translation this information gets lost + // This is a workaround to atleast make our inventory checks work "decently" in 1.8 clients for 1.9+ servers + if (PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_9) + && player.getClientVersion().isOlderThanOrEquals(ClientVersion.V_1_8)) { + handleInventoryOpen(player); + } + + if (player.getClientVersion().isNewerThan(ClientVersion.V_1_8)) { + handleInventoryOpen(player); + } + } + + if (event.getPacketType() == PacketType.Play.Client.CLOSE_WINDOW) { + GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser()); + if (player == null) return; + + handleInventoryClose(player, InventoryDesyncStatus.NOT_DESYNCED); + } + } + + @Override + public void onPacketSend(PacketSendEvent event) { + if (event.getPacketType() == PacketType.Play.Server.RESPAWN) { + GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser()); + if (player == null) return; + + player.sendTransaction(); + + player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), + () -> handleInventoryClose(player, InventoryDesyncStatus.NOT_DESYNCED)); + } else if (event.getPacketType() == PacketType.Play.Server.OPEN_WINDOW) { + WrapperPlayServerOpenWindow wrapper = new WrapperPlayServerOpenWindow(event); + + GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser()); + if (player == null) return; + + player.sendTransaction(); + + String legacyType = wrapper.getLegacyType(); + int modernType = wrapper.getType(); + InventoryDesyncStatus inventoryDesyncStatus = getContainerDesyncStatus(player, legacyType, modernType); + + player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), + () -> { + if (inventoryDesyncStatus == InventoryDesyncStatus.NOT_DESYNCED) { + handleInventoryOpen(player); + } else { + handleInventoryClose(player, inventoryDesyncStatus); + } + }); + } else if (event.getPacketType() == PacketType.Play.Server.OPEN_HORSE_WINDOW) { + GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser()); + if (player == null) return; + + player.sendTransaction(); + + player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), + () -> handleInventoryOpen(player)); + } else if (event.getPacketType() == PacketType.Play.Server.CLOSE_WINDOW) { + GrimPlayer player = GrimAPI.INSTANCE.getPlayerDataManager().getPlayer(event.getUser()); + if (player == null) return; + + player.sendTransaction(); + + player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), + () -> handleInventoryClose(player, InventoryDesyncStatus.NOT_DESYNCED)); + } + } + + private void handleInventoryOpen(GrimPlayer player) { + if (!player.hasInventoryOpen) { + player.lastInventoryOpen = System.currentTimeMillis(); + } + + player.hasInventoryOpen = true; + } + + private void handleInventoryClose(GrimPlayer player, InventoryDesyncStatus desyncStatus) { + player.hasInventoryOpen = false; + player.inventoryDesyncStatus = desyncStatus; + } + + public InventoryDesyncStatus getContainerDesyncStatus(GrimPlayer player, String legacyType, int modernType) { + // Closing beacon with the cross button cause desync in 1.7-1.8 + if (player.getClientVersion().isOlderThanOrEquals(ClientVersion.V_1_8) && + ("minecraft:beacon".equals(legacyType) || modernType == 8)) { + return player.inventoryDesyncStatus = InventoryDesyncStatus.BEACON; + } + + if (isNearNetherPortal(player)) { + return player.inventoryDesyncStatus = InventoryDesyncStatus.NETHER_PORTAL; + } + + return player.inventoryDesyncStatus = InventoryDesyncStatus.NOT_DESYNCED; + } + + public boolean isNearNetherPortal(GrimPlayer player) { + // Going inside nether portal with opened inventory cause desync, fixed in 1.12.2 + if (player.getClientVersion().isOlderThanOrEquals(ClientVersion.V_1_12_1) && + player.pointThreeEstimator.isNearNetherPortal) { + PacketEntitySelf playerEntity = player.compensatedEntities.self; + // Client ignore nether portal if player has passengers or riding an entity + return !playerEntity.inVehicle() && playerEntity.passengers.isEmpty(); + } + + return false; + } +} diff --git a/common/src/main/java/ac/grim/grimac/events/packets/worldreader/BasePacketWorldReader.java b/common/src/main/java/ac/grim/grimac/events/packets/worldreader/BasePacketWorldReader.java index 88b5514993..ca806a03fc 100644 --- a/common/src/main/java/ac/grim/grimac/events/packets/worldreader/BasePacketWorldReader.java +++ b/common/src/main/java/ac/grim/grimac/events/packets/worldreader/BasePacketWorldReader.java @@ -1,12 +1,17 @@ package ac.grim.grimac.events.packets.worldreader; import ac.grim.grimac.GrimAPI; +import ac.grim.grimac.events.packets.worldreader.multiblockchange.LegacyMultiBlockChangeHandler; +import ac.grim.grimac.events.packets.worldreader.multiblockchange.V1160MultiBlockChangeBitRepackHandler; +import ac.grim.grimac.events.packets.worldreader.multiblockchange.VersionedMultiBlockChangeHandler; import ac.grim.grimac.player.GrimPlayer; import ac.grim.grimac.utils.chunks.Column; import ac.grim.grimac.utils.data.TeleportData; +import com.github.retrooper.packetevents.PacketEvents; import com.github.retrooper.packetevents.event.PacketListenerAbstract; import com.github.retrooper.packetevents.event.PacketListenerPriority; import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.manager.server.ServerVersion; import com.github.retrooper.packetevents.protocol.packettype.PacketType; import com.github.retrooper.packetevents.protocol.world.chunk.BaseChunk; import com.github.retrooper.packetevents.util.Vector3i; @@ -16,11 +21,15 @@ import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChangeGameState; import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChunkData; import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChunkDataBulk; -import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerMultiBlockChange; import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerUnloadChunk; public class BasePacketWorldReader extends PacketListenerAbstract { + private final static VersionedMultiBlockChangeHandler versionedMultiBlockChangeHandler + = PacketEvents.getAPI().getServerManager().getVersion().isNewerThanOrEquals(ServerVersion.V_1_16) + ? new V1160MultiBlockChangeBitRepackHandler() + : new LegacyMultiBlockChangeHandler(); + public BasePacketWorldReader() { super(PacketListenerPriority.HIGH); } @@ -161,28 +170,15 @@ public void handleBlockChange(GrimPlayer player, PacketSendEvent event) { player.lastTransSent + 2 < System.currentTimeMillis()) player.sendTransaction(); - player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> player.compensatedWorld.updateBlock(blockPosition.getX(), blockPosition.getY(), blockPosition.getZ(), blockChange.getBlockId())); + int x = blockPosition.getX(); + int y = blockPosition.getY(); + int z = blockPosition.getZ(); + int blockId = blockChange.getBlockId(); + + player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> player.compensatedWorld.updateBlock(x, y, z, blockId)); } public void handleMultiBlockChange(GrimPlayer player, PacketSendEvent event) { - WrapperPlayServerMultiBlockChange multiBlockChange = new WrapperPlayServerMultiBlockChange(event); - - int range = 16; - - final var blocks = multiBlockChange.getBlocks(); - for (WrapperPlayServerMultiBlockChange.EncodedBlock blockChange : blocks) { - // Don't send a transaction unless it's within 16 blocks of the player - if (Math.abs(blockChange.getX() - player.x) < range && Math.abs(blockChange.getY() - player.y) < range && Math.abs(blockChange.getZ() - player.z) < range && player.lastTransSent + 2 < System.currentTimeMillis()) { - player.sendTransaction(); - break; - } - } - - // Add a single runnable to prevent excessive memory use when there are lots of block changes - player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> { - for (WrapperPlayServerMultiBlockChange.EncodedBlock blockChange : blocks) { - player.compensatedWorld.updateBlock(blockChange.getX(), blockChange.getY(), blockChange.getZ(), blockChange.getBlockId()); - } - }); + versionedMultiBlockChangeHandler.handleMultiBlockChange(player, event); } } diff --git a/common/src/main/java/ac/grim/grimac/events/packets/worldreader/multiblockchange/LegacyMultiBlockChangeHandler.java b/common/src/main/java/ac/grim/grimac/events/packets/worldreader/multiblockchange/LegacyMultiBlockChangeHandler.java new file mode 100644 index 0000000000..7f91ad4e0f --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/events/packets/worldreader/multiblockchange/LegacyMultiBlockChangeHandler.java @@ -0,0 +1,30 @@ +package ac.grim.grimac.events.packets.worldreader.multiblockchange; + +import ac.grim.grimac.player.GrimPlayer; +import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerMultiBlockChange; + +public class LegacyMultiBlockChangeHandler implements VersionedMultiBlockChangeHandler { + + // TODO hande optimize Pre 1.16 code to also reduce memory usage and not use wrapper + @Override + public void handleMultiBlockChange(GrimPlayer player, PacketSendEvent event) { + WrapperPlayServerMultiBlockChange multiBlockChange = new WrapperPlayServerMultiBlockChange(event); + + final var blocks = multiBlockChange.getBlocks(); + for (WrapperPlayServerMultiBlockChange.EncodedBlock blockChange : blocks) { + // Don't send a transaction unless it's within 16 blocks of the player + if (Math.abs(blockChange.getX() - player.x) < RANGE && Math.abs(blockChange.getY() - player.y) < RANGE && Math.abs(blockChange.getZ() - player.z) < RANGE && player.lastTransSent + TRANSACTION_COOLDOWN_MS < System.currentTimeMillis()) { + player.sendTransaction(); + break; + } + } + + // Add a single runnable to prevent excessive memory use when there are lots of block changes + player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> { + for (WrapperPlayServerMultiBlockChange.EncodedBlock blockChange : blocks) { + player.compensatedWorld.updateBlock(blockChange.getX(), blockChange.getY(), blockChange.getZ(), blockChange.getBlockId()); + } + }); + } +} diff --git a/common/src/main/java/ac/grim/grimac/events/packets/worldreader/multiblockchange/V1160MultiBlockChangeBitRepackHandler.java b/common/src/main/java/ac/grim/grimac/events/packets/worldreader/multiblockchange/V1160MultiBlockChangeBitRepackHandler.java new file mode 100644 index 0000000000..e2419b828e --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/events/packets/worldreader/multiblockchange/V1160MultiBlockChangeBitRepackHandler.java @@ -0,0 +1,175 @@ +package ac.grim.grimac.events.packets.worldreader.multiblockchange; + +import ac.grim.grimac.player.GrimPlayer; +import com.github.retrooper.packetevents.PacketEvents; +import com.github.retrooper.packetevents.event.PacketSendEvent; +import com.github.retrooper.packetevents.manager.server.ServerVersion; +import com.github.retrooper.packetevents.netty.buffer.ByteBufHelper; +import io.netty.buffer.ByteBuf; + +/** + *

+ * Minecraft’s MultiBlockChange packet batches many block updates in one shot: + * the server sends a 64-bit section coordinate, an optional trustEdges flag, + * a VarInt count, then that many VarLong-encoded block changes + * (52 bits of global blockStateId + 12 bits of local X/Z/Y + * within the 16×16×16 section). + *

+ * + *

+ * To shrink our on-heap footprint for deferred tasks, we immediately repack + * each VarLong into one 32-bit int, while still using the + * vanilla 64-bit header for coords. + *

+ * + *

Vanilla “on-the-wire” format

+ *
+ * 1) sectionEncodedPosition : Long
+ * 2) [trustEdges?           : Boolean]   // only on protocol ≤1.19.4
+ * 3) recordCount            : VarInt
+ * 4) records[]              : recordCount × VarLong
+ *
+ *    Each VarLong is bits:
+ *     [63……12] blockStateId (52 bits)
+ *     [11……8]  localX       (4 bits, 0–15)
+ *     [7……4]   localZ       (4 bits, 0–15)
+ *     [3……0]   localY       (4 bits, 0–15)
+ * 
+ * + *

Vanilla 64-bit section header “encodedPosition”

+ *
+ * bits   63……42   41……20    19……0
+ *       ┌────────┬─────────┬────────┐
+ *       │  secX  │  secZ   │  secY  │
+ *       │ (22b)  │ (22b)   │ (20b)  │
+ *       └────────┴─────────┴────────┘
+ *
+ *   secX = encoded >> 42
+ *   secZ = encoded << 22 >> 42
+ *   secY = encoded << 44 >> 44
+ * 
+ * + *

Our 32-bit repacked block record (MSB → LSB)

+ *
+ * bits  31…17   16…12   11…8   7…4   3…0
+ *      ┌───────┬───────┬───────┬──────┬─────┐
+ *      │ state │ spare │  lX   │  lZ  │ lY  │
+ *      │ (15b) │ (5b)  │ (4b)  │ (4b) │(4b) │
+ *      └───────┴───────┴───────┴──────┴─────┘
+ *
+ *  • state = (data >>> 12) & 0x7FFF
+ *  • spare = bits 12–16 (unused, reserved for flags)
+ *  • lX    = (data >>>  8) & 0xF
+ *  • lZ    = (data >>>  4) & 0xF
+ *  • lY    =  data          & 0xF
+ *
+ *  Then pack:  packed = (state << 17) | ((lX<<8)|(lZ<<4)|lY)
+ * 
+ */ +public final class V1160MultiBlockChangeBitRepackHandler + implements ac.grim.grimac.events.packets.worldreader.multiblockchange.VersionedMultiBlockChangeHandler { + + /* ---------- bit masks / shifts for the packed int ---------- */ + private static final int SHIFT_STATE = 17; // 32-15 = 17 + private static final int MASK_STATE = 0x7FFF; // 15 bits + + static final int MASK_LOCAL = 0xFFF; // 12 bits + + /* ---------- does this protocol still have trustEdges ? ----- */ + private static final boolean HAS_TRUST_EDGES = + PacketEvents.getAPI().getServerManager().getVersion().isOlderThanOrEquals(ServerVersion.V_1_19_4); + + @Override + public void handleMultiBlockChange(GrimPlayer player, PacketSendEvent event) { + // PE resets writer index for us, we don't have to call buffer.writerIndex(originalWriterIndex) + ByteBuf buf = (ByteBuf) event.getByteBuf(); + + /* 1. Section-position header (64 bits) ------------------ */ + long sectionEncodedPosition = ByteBufHelper.readLong(buf); + + if (HAS_TRUST_EDGES) { // skip only when it really exists + buf.skipBytes(1); + } + + /* 2. Record count + packed-int array ------------------- */ + int recordCount = ByteBufHelper.readVarInt(buf); + int[] packed = new int[recordCount]; + + /* 3. Decode packet (still comes as varLong per record) */ + // Unpack section coords once for the “near player” test + int secX = (int) (sectionEncodedPosition >> 42); + int secZ = (int) (sectionEncodedPosition << 22 >> 42); + int secY = (int) (sectionEncodedPosition << 44 >> 44); + + int baseX = secX << 4; + int baseY = secY << 4; + int baseZ = secZ << 4; + + boolean sendTx = false; + + // Use this to not spam the player with transactions if one has been sent within COOLDOWN + long now = System.currentTimeMillis(); + for (int i = 0; i < recordCount; i++) { + + long data = readVarLong(buf); // 52+12 bits + + int local = (int) (data & 0xFFFL); // 12-bit pos + + packed[i] = repackFromLong(data); + + /* -------- near-player distance test -------------- */ + if (!sendTx) { + int lx = (local >>> 8) & 0xF; + int lz = (local >>> 4) & 0xF; + int ly = local & 0xF; + + int wx = baseX + lx, wy = baseY + ly, wz = baseZ + lz; + + if (Math.abs(wx - player.x) < RANGE && + Math.abs(wy - player.y) < RANGE && + Math.abs(wz - player.z) < RANGE && + player.lastTransSent + TRANSACTION_COOLDOWN_MS < now) { + sendTx = true; + } + } + } + + if (sendTx) + player.sendTransaction(); + + /* 4. Queue runnable – captures only int[] + sectionPos */ + player.latencyUtils.addRealTimeTask(player.lastTransactionSent.get(), () -> { + + // unpack section once per execution + int sX = (int) (sectionEncodedPosition >> 42); + int sY = (int) (sectionEncodedPosition << 44 >> 44); + int sZ = (int) (sectionEncodedPosition << 22 >> 42); + + int bx = sX << 4, by = sY << 4, bz = sZ << 4; + + for (int rec : packed) { + int stateId = (rec >>> SHIFT_STATE) & MASK_STATE; + int lx = (rec >>> 8) & 0xF; + int lz = (rec >>> 4) & 0xF; + int ly = rec & 0xF; + + int wx = bx + lx; + int wy = by + ly; + int wz = bz + lz; + + player.compensatedWorld.updateBlock(wx, wy, wz, stateId); + } + }); + } + + public int repackFromLong(long data) { + // 1) extract the 15-bit state from bits 12.. (original >> 12) + int blockState = (int)((data >>> 12) & MASK_STATE); + + // 2) extract the 12-bit local from bits 0..11 + int local = (int)( data & MASK_LOCAL); + + // 3) glue them together + return (blockState << SHIFT_STATE) | local; + } +} diff --git a/common/src/main/java/ac/grim/grimac/events/packets/worldreader/multiblockchange/VersionedMultiBlockChangeHandler.java b/common/src/main/java/ac/grim/grimac/events/packets/worldreader/multiblockchange/VersionedMultiBlockChangeHandler.java new file mode 100644 index 0000000000..909a53c1a1 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/events/packets/worldreader/multiblockchange/VersionedMultiBlockChangeHandler.java @@ -0,0 +1,22 @@ +package ac.grim.grimac.events.packets.worldreader.multiblockchange; + +import ac.grim.grimac.player.GrimPlayer; +import com.github.retrooper.packetevents.event.PacketSendEvent; +import io.netty.buffer.ByteBuf; + +public interface VersionedMultiBlockChangeHandler { + + int RANGE = 16; + long TRANSACTION_COOLDOWN_MS = 2; // In milliseconds + + void handleMultiBlockChange(GrimPlayer player, PacketSendEvent event); + default long readVarLong(ByteBuf buf) { + long value = 0; + int size = 0; + int b; + while (((b = buf.readByte()) & 0x80) == 0x80) { + value |= (long) (b & 0x7F) << (size++ * 7); + } + return value | ((long) (b & 0x7F) << (size * 7)); + } +} diff --git a/common/src/main/java/ac/grim/grimac/manager/CheckManager.java b/common/src/main/java/ac/grim/grimac/manager/CheckManager.java index f132705602..5669de47cd 100644 --- a/common/src/main/java/ac/grim/grimac/manager/CheckManager.java +++ b/common/src/main/java/ac/grim/grimac/manager/CheckManager.java @@ -2,6 +2,7 @@ import ac.grim.grimac.GrimAPI; import ac.grim.grimac.api.AbstractCheck; +import ac.grim.grimac.checks.debug.HitboxDebugHandler; import ac.grim.grimac.checks.impl.aim.AimDuplicateLook; import ac.grim.grimac.checks.impl.aim.AimModulo360; import ac.grim.grimac.checks.impl.aim.processor.AimProcessor; @@ -16,10 +17,7 @@ import ac.grim.grimac.checks.impl.breaking.PositionBreakB; import ac.grim.grimac.checks.impl.breaking.RotationBreak; import ac.grim.grimac.checks.impl.breaking.WrongBreak; -import ac.grim.grimac.checks.impl.combat.Hitboxes; -import ac.grim.grimac.checks.impl.combat.MultiInteractA; -import ac.grim.grimac.checks.impl.combat.MultiInteractB; -import ac.grim.grimac.checks.impl.combat.Reach; +import ac.grim.grimac.checks.impl.combat.*; import ac.grim.grimac.checks.impl.crash.*; import ac.grim.grimac.checks.impl.elytra.ElytraA; import ac.grim.grimac.checks.impl.elytra.ElytraB; @@ -34,6 +32,7 @@ import ac.grim.grimac.checks.impl.exploit.ExploitB; import ac.grim.grimac.checks.impl.exploit.ExploitC; import ac.grim.grimac.checks.impl.groundspoof.NoFall; +import ac.grim.grimac.checks.impl.inventory.*; import ac.grim.grimac.checks.impl.misc.ClientBrand; import ac.grim.grimac.checks.impl.misc.GhostBlockMitigation; import ac.grim.grimac.checks.impl.misc.TransactionOrder; @@ -154,6 +153,11 @@ public CheckManager(GrimPlayer player) { .put(BadPacketsU.class, new BadPacketsU(player)) .put(BadPacketsV.class, new BadPacketsV(player)) .put(BadPacketsY.class, new BadPacketsY(player)) + .put(InventoryA.class, new InventoryA(player)) + .put(InventoryB.class, new InventoryB(player)) + .put(InventoryE.class, new InventoryE(player)) + .put(InventoryF.class, new InventoryF(player)) + .put(InventoryG.class, new InventoryG(player)) .put(MultiActionsA.class, new MultiActionsA(player)) .put(MultiActionsB.class, new MultiActionsB(player)) .put(MultiActionsC.class, new MultiActionsC(player)) @@ -189,6 +193,7 @@ public CheckManager(GrimPlayer player) { .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)) @@ -231,6 +236,7 @@ public CheckManager(GrimPlayer player) { .build(); blockPlaceCheck = new ImmutableClassToInstanceMap.Builder() + .put(InventoryC.class, new InventoryC(player)) .put(InvalidPlaceA.class, new InvalidPlaceA(player)) .put(InvalidPlaceB.class, new InvalidPlaceB(player)) .put(AirLiquidPlace.class, new AirLiquidPlace(player)) @@ -287,6 +293,9 @@ public CheckManager(GrimPlayer player) { .put(TransactionOrder.class, new TransactionOrder(player)) .put(VehicleC.class, new VehicleC(player)) .put(Hitboxes.class, new Hitboxes(player)) // Hitboxes is invoked by Reach + .put(WallHit.class, new WallHit(player)) + .put(EntityPierce.class, new EntityPierce(player)) + .put(HitboxDebugHandler.class, new HitboxDebugHandler(player)) .build(); allChecks = new ImmutableClassToInstanceMap.Builder() diff --git a/common/src/main/java/ac/grim/grimac/manager/InitManager.java b/common/src/main/java/ac/grim/grimac/manager/InitManager.java index 649d862159..b91c94a51c 100644 --- a/common/src/main/java/ac/grim/grimac/manager/InitManager.java +++ b/common/src/main/java/ac/grim/grimac/manager/InitManager.java @@ -56,6 +56,7 @@ public InitManager(PacketEventsAPI packetEventsAPI, Supplier verboseListeners = null; @@ -126,17 +130,21 @@ public boolean handleAlert(GrimPlayer player, String verbose, Check check) { verboseListeners = GrimAPI.INSTANCE.getAlertManager().sendVerbose(component, null); } - if (violationCount >= command.threshold) { - // 0 means execute once - // Any other number means execute every X interval - boolean inInterval = command.interval == 0 ? (command.executeCount == 0) : (violationCount % command.interval == 0); - if (inInterval) { + // 0 means execute once + // Any other number means execute every X interval + for (; vl >= (command.threshold + (command.interval * command.executeCount)); command.executeCount++) { + if (command.interval == 0 && command.executeCount > 0) break; + CommandExecuteEvent executeEvent = new CommandExecuteEvent(player, check, verbose, cmd); GrimAPI.INSTANCE.getEventBus().post(executeEvent); if (executeEvent.isCancelled()) continue; switch (command.command) { case "[webhook]" -> GrimAPI.INSTANCE.getDiscordManager().sendAlert(player, verbose, check.getDisplayName(), vl); + case "[log]" -> { + String verboseWithoutGl = verbose.replaceAll(" /gl .*", ""); + GrimAPI.INSTANCE.getViolationDatabaseManager().logAlert(player, verboseWithoutGl, check.getDisplayName(), vl); + } case "[proxy]" -> ProxyAlertMessenger.sendPluginMessage(cmd); case "[alert]" -> { sentDebug = true; @@ -156,8 +164,6 @@ public boolean handleAlert(GrimPlayer player, String verbose, Check check) { ) ); } - } - command.executeCount++; } } @@ -168,21 +174,29 @@ public boolean handleAlert(GrimPlayer player, String verbose, Check check) { } public void handleViolation(Check check) { + addViolation(check, 1); + } + + public void addExtraViolation(Check check, int amount) { + if (amount <= 0) return; + addViolation(check, amount); + } + + private void addViolation(Check check, int amount) { for (PunishGroup group : groups) { if (group.checks.contains(check)) { long currentTime = System.currentTimeMillis(); - - group.violations.put(currentTime, check); + group.violations.put(currentTime, new ViolationRecord(check, amount)); // Remove violations older than the defined time in the config - group.violations.entrySet().removeIf(time -> currentTime - time.getKey() > group.removeViolationsAfter); + group.violations.entrySet().removeIf(entry -> currentTime - entry.getKey() > group.removeViolationsAfter); } } } private int getViolations(PunishGroup group, Check check) { int vl = 0; - for (Check value : group.violations.values()) { - if (value == check) vl++; + for (ViolationRecord value : group.violations.values()) { + if (value.check == check) vl += value.amount; } return vl; } @@ -192,7 +206,7 @@ private int getViolations(PunishGroup group, Check check) { class PunishGroup { public final List checks; public final List commands; - public final Map violations = new HashMap<>(); + public final Map violations = new HashMap<>(); public final int removeViolationsAfter; // time to remove violations after in milliseconds } @@ -203,3 +217,9 @@ class ParsedCommand { public final String command; public int executeCount; } + +@RequiredArgsConstructor +class ViolationRecord { + public final Check check; + public final int amount; +} diff --git a/common/src/main/java/ac/grim/grimac/manager/init/load/PacketEventsInit.java b/common/src/main/java/ac/grim/grimac/manager/init/load/PacketEventsInit.java index 1c586f9511..7ab0690e57 100644 --- a/common/src/main/java/ac/grim/grimac/manager/init/load/PacketEventsInit.java +++ b/common/src/main/java/ac/grim/grimac/manager/init/load/PacketEventsInit.java @@ -10,12 +10,14 @@ import com.github.retrooper.packetevents.protocol.item.type.ItemTypes; import com.github.retrooper.packetevents.protocol.particle.type.ParticleTypes; import com.github.retrooper.packetevents.protocol.world.states.type.StateTypes; +import com.github.retrooper.packetevents.util.PEVersion; import java.util.concurrent.Executors; public class PacketEventsInit implements LoadableInitable { PacketEventsAPI packetEventsAPI; + PEVersion MINIMUM_REQUIRED_PE_VERSION = new PEVersion(2, 8, 0, true); public PacketEventsInit(PacketEventsAPI packetEventsAPI) { this.packetEventsAPI = packetEventsAPI; @@ -25,6 +27,17 @@ public PacketEventsInit(PacketEventsAPI packetEventsAPI) { public void load() { LogUtil.info("Loading PacketEvents..."); PacketEvents.setAPI(packetEventsAPI); + + if (!checkPacketEventsVersion()) { + LogUtil.error("\n" + + "******************************************************\n" + + "GrimAC requires PacketEvents >= " + MINIMUM_REQUIRED_PE_VERSION + + (MINIMUM_REQUIRED_PE_VERSION.snapshot() ? "-SNAPSHOT" : "") + "\n" + + "Current version: " + PacketEvents.getAPI().getVersion() + "\n" + + "Please update PacketEvents to a compatible version.\n" + + "*****************************************************"); + } + PacketEvents.getAPI().getSettings() .fullStackTrace(true) .kickOnPacketException(true) @@ -43,4 +56,33 @@ public void load() { ParticleTypes.DUST.getName(); }).start(); } + + private boolean checkPacketEventsVersion() { + PEVersion current = PacketEvents.getAPI().getVersion(); + PEVersion required = MINIMUM_REQUIRED_PE_VERSION; + + // If current version is newer, always accept + if (current.isNewerThan(required)) { + return true; + } + + // If current version is exactly equal to required (including snapshot status), accept + if (current.major() == required.major() + && current.minor() == required.minor() + && current.patch() == required.patch() + && current.snapshot() == required.snapshot()) { + return true; + } + + // If required is a snapshot, accept matching release or snapshot + if (required.snapshot() + && current.major() == required.major() + && current.minor() == required.minor() + && current.patch() == required.patch()) { + return true; + } + + // Otherwise, reject + return false; + } } diff --git a/common/src/main/java/ac/grim/grimac/manager/init/start/CommandRegister.java b/common/src/main/java/ac/grim/grimac/manager/init/start/CommandRegister.java index 260dc465cc..bddc957aaa 100644 --- a/common/src/main/java/ac/grim/grimac/manager/init/start/CommandRegister.java +++ b/common/src/main/java/ac/grim/grimac/manager/init/start/CommandRegister.java @@ -2,20 +2,7 @@ import ac.grim.grimac.GrimAPI; import ac.grim.grimac.command.SenderRequirement; -import ac.grim.grimac.command.commands.GrimAlerts; -import ac.grim.grimac.command.commands.GrimBrands; -import ac.grim.grimac.command.commands.GrimDebug; -import ac.grim.grimac.command.commands.GrimDump; -import ac.grim.grimac.command.commands.GrimHelp; -import ac.grim.grimac.command.commands.GrimLog; -import ac.grim.grimac.command.commands.GrimPerf; -import ac.grim.grimac.command.commands.GrimProfile; -import ac.grim.grimac.command.commands.GrimReload; -import ac.grim.grimac.command.commands.GrimSendAlert; -import ac.grim.grimac.command.commands.GrimSpectate; -import ac.grim.grimac.command.commands.GrimStopSpectating; -import ac.grim.grimac.command.commands.GrimVerbose; -import ac.grim.grimac.command.commands.GrimVersion; +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; @@ -60,6 +47,7 @@ public static void registerCommands(CommandManager 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); diff --git a/common/src/main/java/ac/grim/grimac/manager/init/start/PacketManager.java b/common/src/main/java/ac/grim/grimac/manager/init/start/PacketManager.java index 4c78a402a8..f271465e2f 100644 --- a/common/src/main/java/ac/grim/grimac/manager/init/start/PacketManager.java +++ b/common/src/main/java/ac/grim/grimac/manager/init/start/PacketManager.java @@ -1,20 +1,6 @@ package ac.grim.grimac.manager.init.start; -import ac.grim.grimac.events.packets.CheckManagerListener; -import ac.grim.grimac.events.packets.PacketBlockAction; -import ac.grim.grimac.events.packets.PacketEntityAction; -import ac.grim.grimac.events.packets.PacketHidePlayerInfo; -import ac.grim.grimac.events.packets.PacketPingListener; -import ac.grim.grimac.events.packets.PacketPlayerAttack; -import ac.grim.grimac.events.packets.PacketPlayerCooldown; -import ac.grim.grimac.events.packets.PacketPlayerDigging; -import ac.grim.grimac.events.packets.PacketPlayerJoinQuit; -import ac.grim.grimac.events.packets.PacketPlayerRespawn; -import ac.grim.grimac.events.packets.PacketPlayerSteer; -import ac.grim.grimac.events.packets.PacketSelfMetadataListener; -import ac.grim.grimac.events.packets.PacketServerTags; -import ac.grim.grimac.events.packets.PacketServerTeleport; -import ac.grim.grimac.events.packets.ProxyAlertMessenger; +import ac.grim.grimac.events.packets.*; import ac.grim.grimac.events.packets.worldreader.BasePacketWorldReader; import ac.grim.grimac.events.packets.worldreader.PacketWorldReaderEight; import ac.grim.grimac.events.packets.worldreader.PacketWorldReaderEighteen; @@ -30,6 +16,7 @@ public void start() { PacketEvents.getAPI().getEventManager().registerListener(new PacketPlayerJoinQuit()); PacketEvents.getAPI().getEventManager().registerListener(new PacketPingListener()); + PacketEvents.getAPI().getEventManager().registerListener(new PacketPlayerWindow()); PacketEvents.getAPI().getEventManager().registerListener(new PacketPlayerDigging()); PacketEvents.getAPI().getEventManager().registerListener(new PacketPlayerAttack()); PacketEvents.getAPI().getEventManager().registerListener(new PacketEntityAction()); diff --git a/common/src/main/java/ac/grim/grimac/manager/violationdatabase/MySQLViolationDatabase.java b/common/src/main/java/ac/grim/grimac/manager/violationdatabase/MySQLViolationDatabase.java new file mode 100644 index 0000000000..8434748ce6 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/manager/violationdatabase/MySQLViolationDatabase.java @@ -0,0 +1,131 @@ +package ac.grim.grimac.manager.violationdatabase; + +import ac.grim.grimac.GrimAPI; +import ac.grim.grimac.api.plugin.GrimPlugin; +import ac.grim.grimac.player.GrimPlayer; +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; +import java.util.logging.Level; + +public class MySQLViolationDatabase implements ViolationDatabase { + + private final GrimPlugin plugin; + private HikariDataSource dataSource; + + public MySQLViolationDatabase(GrimPlugin plugin, String url, String database, String username, String password) { + this.plugin = plugin; + setupDataSource(url, database, username, password); + } + + private void setupDataSource(String url, String database, String username, String password) { + HikariConfig config = new HikariConfig(); + config.setJdbcUrl("jdbc:mysql://" + url + "/" + database); + config.setUsername(username); + config.setPassword(password); + config.addDataSourceProperty("cachePrepStmts", "true"); + config.addDataSourceProperty("prepStmtCacheSize", "250"); + config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + config.setMaximumPoolSize(10); + config.setAutoCommit(true); + dataSource = new HikariDataSource(config); + } + + @Override + public void connect() { + try (Connection connection = dataSource.getConnection()) { + connection.prepareStatement( + "CREATE TABLE IF NOT EXISTS violations(" + + "id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, " + + "server VARCHAR(255) NOT NULL, " + + "uuid CHAR(36) NOT NULL, " + + "check_name TEXT NOT NULL, " + + "verbose TEXT NOT NULL, " + + "vl INTEGER NOT NULL, " + + "created_at BIGINT NOT NULL" + + ")" + ).execute(); + + connection.prepareStatement( + "CREATE INDEX IF NOT EXISTS idx_violations_uuid ON violations(uuid);" + ).execute(); + } catch (SQLException ex) { + plugin.getLogger().log(Level.SEVERE, "Failed to generate violations database:", ex); + } + } + + @Override + public synchronized void logAlert(GrimPlayer player, String verbose, String checkName, int vls) { + try (Connection connection = dataSource.getConnection(); + PreparedStatement insertAlert = connection.prepareStatement( + "INSERT INTO violations (server, uuid, check_name, verbose, vl, created_at) VALUES (?, ?, ?, ?, ?, ?)" + ) + ) { + insertAlert.setString(1, GrimAPI.INSTANCE.getConfigManager().getConfig().getStringElse("history.server-name", "Prison")); + insertAlert.setString(2, player.getUniqueId().toString()); + insertAlert.setString(3, checkName); + insertAlert.setString(4, verbose); + insertAlert.setInt(5, vls); + insertAlert.setLong(6, System.currentTimeMillis()); + insertAlert.execute(); + } catch (SQLException ex) { + plugin.getLogger().log(Level.SEVERE, "Failed to log alert", ex); + } + } + + @Override + public synchronized int getLogCount(UUID player) { + try (Connection connection = dataSource.getConnection(); + PreparedStatement countLogs = connection.prepareStatement( + "SELECT COUNT(*) FROM violations WHERE uuid = ?" + ) + ) { + countLogs.setString(1, player.toString()); + ResultSet result = countLogs.executeQuery(); + if (result.next()) { + return result.getInt(1); + } + } catch (SQLException ex) { + plugin.getLogger().log(Level.SEVERE, "Failed to count logs", ex); + } + return 0; + } + + @Override + public synchronized List getViolations(UUID player, int page, int limit) { + try (Connection connection = dataSource.getConnection(); + PreparedStatement fetchLogs = connection.prepareStatement( + "SELECT server, uuid, check_name, verbose, vl, created_at FROM violations" + + " WHERE uuid = ? ORDER BY created_at DESC LIMIT ? OFFSET ?" + ) + ) { + fetchLogs.setString(1, player.toString()); + fetchLogs.setInt(2, limit); + fetchLogs.setInt(3, (page - 1) * limit); + return Violation.fromResultSet(fetchLogs.executeQuery()); + } catch (SQLException ex) { + plugin.getLogger().log(Level.SEVERE, "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:mysql://" + host + "/" + db; + return wantUrl.equalsIgnoreCase(dataSource.getJdbcUrl()) + && user.equals(dataSource.getUsername()) + && pwd .equals(dataSource.getPassword()); // Hikari stores clear text + } +} diff --git a/common/src/main/java/ac/grim/grimac/manager/violationdatabase/NoOpViolationDatabase.java b/common/src/main/java/ac/grim/grimac/manager/violationdatabase/NoOpViolationDatabase.java new file mode 100644 index 0000000000..5d008d5144 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/manager/violationdatabase/NoOpViolationDatabase.java @@ -0,0 +1,17 @@ +package ac.grim.grimac.manager.violationdatabase; + +import ac.grim.grimac.player.GrimPlayer; + +import java.util.List; +import java.util.UUID; + +public final class NoOpViolationDatabase implements ViolationDatabase { + public static final NoOpViolationDatabase INSTANCE = new NoOpViolationDatabase(); + private NoOpViolationDatabase() {} + + @Override public void connect() {} + @Override public void disconnect() {} + @Override public void logAlert(GrimPlayer p, String v, String c, int vl) {} + @Override public int getLogCount(UUID player) { return 0; } + @Override public List getViolations(UUID p, int page, int lim) { return List.of(); } +} diff --git a/common/src/main/java/ac/grim/grimac/manager/violationdatabase/SQLiteViolationDatabase.java b/common/src/main/java/ac/grim/grimac/manager/violationdatabase/SQLiteViolationDatabase.java new file mode 100644 index 0000000000..889a14b143 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/manager/violationdatabase/SQLiteViolationDatabase.java @@ -0,0 +1,130 @@ +package ac.grim.grimac.manager.violationdatabase; + +import ac.grim.grimac.GrimAPI; +import ac.grim.grimac.api.plugin.GrimPlugin; +import ac.grim.grimac.player.GrimPlayer; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.logging.Level; + +public class SQLiteViolationDatabase implements ViolationDatabase { + + private final GrimPlugin plugin; + + private Connection openConnection; + + public SQLiteViolationDatabase(@NotNull GrimPlugin plugin) { + this.plugin = plugin; + } + + @Override + public void connect() { + try (Connection connection = getConnection()) { + connection.prepareStatement( + "CREATE TABLE IF NOT EXISTS violations(" + + "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + + "server VARCHAR(255) NOT NULL, " + + "uuid CHARACTER(36) NOT NULL, " + + "check_name TEXT NOT NULL, " + + "verbose TEXT NOT NULL, " + + "vl INTEGER NOT NULL, " + + "created_at BIGINT NOT NULL" + + ")" + ).execute(); + + connection.prepareStatement( + "CREATE INDEX IF NOT EXISTS idx_violations_uuid ON violations(uuid)" + ).execute(); + } catch (SQLException ex) { + plugin.getLogger().log(Level.SEVERE, "Failed to generate violations database:", ex); + } + } + + @Override + public synchronized void logAlert(GrimPlayer player, String verbose, String checkName, int vls) { + try ( + Connection connection = getConnection(); + PreparedStatement insertLog = connection.prepareStatement( + "INSERT INTO violations (server, uuid, check_name, verbose, vl, created_at) VALUES (?, ?, ?, ?, ?, ?)" + ) + ) { + insertLog.setString(1, GrimAPI.INSTANCE.getConfigManager().getConfig().getStringElse("history.server-name", "Prison")); + insertLog.setString(2, player.getUniqueId().toString()); + insertLog.setString(3, verbose); + insertLog.setString(4, checkName); + insertLog.setInt(5, vls); + insertLog.setLong(6, System.currentTimeMillis()); + + insertLog.executeUpdate(); + } catch (SQLException ex) { + plugin.getLogger().log(Level.SEVERE, "Failed to insert violation:", ex); + } + } + + public synchronized int getLogCount(UUID player) { + try ( + Connection connection = getConnection(); + PreparedStatement fetchLogs = connection.prepareStatement( + "SELECT COUNT(*) FROM violations WHERE uuid = ?" + ) + ) { + fetchLogs.setString(1, player.toString()); + ResultSet resultSet = fetchLogs.executeQuery(); + if (resultSet.next()) { + return resultSet.getInt(1); + } + } catch (SQLException ex) { + plugin.getLogger().log(Level.SEVERE, "Failed to fetch number of violations:", ex); + } + return 0; + } + + @Override + public synchronized List getViolations(UUID player, int page, int limit) { + List violations = new ArrayList<>(); + try ( + Connection connection = getConnection(); + PreparedStatement fetchLogs = connection.prepareStatement( + "SELECT server, uuid, check_name, verbose, vl, created_at FROM violations" + + " WHERE uuid = ? ORDER BY created_at DESC LIMIT ? OFFSET ?" + ) + ) { + fetchLogs.setString(1, player.toString()); + fetchLogs.setInt(2, limit); + fetchLogs.setInt(3, (page - 1) * limit); + + return Violation.fromResultSet(fetchLogs.executeQuery()); + } catch (SQLException ex) { + plugin.getLogger().log(Level.SEVERE, "Failed to fetch violations:", ex); + } + + return violations; + } + + @Override + public void disconnect() { + try { + if (openConnection != null && !openConnection.isClosed()) { + openConnection.close(); + } + } catch (SQLException ex) { + plugin.getLogger().log(Level.SEVERE, "Failed to close connection", ex); + } + } + + protected synchronized Connection getConnection() throws SQLException { + if (openConnection == null || openConnection.isClosed()) { + openConnection = openConnection(); + } + return openConnection; + } + + protected Connection openConnection() throws SQLException { + return DriverManager.getConnection("jdbc:sqlite:" + plugin.getDataFolder().getAbsolutePath() + File.separator + "violations.sqlite"); + } +} diff --git a/common/src/main/java/ac/grim/grimac/manager/violationdatabase/Violation.java b/common/src/main/java/ac/grim/grimac/manager/violationdatabase/Violation.java new file mode 100644 index 0000000000..7aae419def --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/manager/violationdatabase/Violation.java @@ -0,0 +1,38 @@ +package ac.grim.grimac.manager.violationdatabase; + +import lombok.Data; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +@Data +public class Violation { + + private final String serverName; + private final UUID playerUUID; + private final String checkName; + private final String verbose; + private final int vl; + private final Date createdAt; + + public static List fromResultSet(ResultSet resultSet) throws SQLException { + List violations = new ArrayList<>(); + while(resultSet.next()) { + String server = resultSet.getString("server"); + UUID player = UUID.fromString(resultSet.getString("uuid")); + String checkName = resultSet.getString("check_name"); + String verbose = resultSet.getString("verbose"); + int vl = resultSet.getInt("vl"); + Date createdAt = new Date(resultSet.getLong("created_at")); + + violations.add(new Violation(server, player, checkName, verbose, vl, createdAt)); + } + + return violations; + } + +} diff --git a/common/src/main/java/ac/grim/grimac/manager/violationdatabase/ViolationDatabase.java b/common/src/main/java/ac/grim/grimac/manager/violationdatabase/ViolationDatabase.java new file mode 100644 index 0000000000..d749758c84 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/manager/violationdatabase/ViolationDatabase.java @@ -0,0 +1,20 @@ +package ac.grim.grimac.manager.violationdatabase; + +import ac.grim.grimac.player.GrimPlayer; + +import java.util.List; +import java.util.UUID; + +public interface ViolationDatabase { + + void connect(); + + void logAlert(GrimPlayer player, String verbose, String checkName, int vls); + + int getLogCount(UUID player); + + List getViolations(UUID player, int page, int limit); + + void disconnect(); + +} diff --git a/common/src/main/java/ac/grim/grimac/manager/violationdatabase/ViolationDatabaseManager.java b/common/src/main/java/ac/grim/grimac/manager/violationdatabase/ViolationDatabaseManager.java new file mode 100644 index 0000000000..c70b573116 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/manager/violationdatabase/ViolationDatabaseManager.java @@ -0,0 +1,101 @@ +package ac.grim.grimac.manager.violationdatabase; + +import ac.grim.grimac.GrimAPI; +import ac.grim.grimac.api.config.ConfigManager; +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.player.GrimPlayer; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.List; +import java.util.UUID; +import java.util.logging.Level; + +public class ViolationDatabaseManager implements StartableInitable, ReloadableInitable { + + private final GrimPlugin plugin; + + private @NonNull ViolationDatabase database; + + public ViolationDatabaseManager(GrimPlugin plugin) { + this.plugin = plugin; + this.database = NoOpViolationDatabase.INSTANCE; + } + + @Override + public void start() { + load(); + } + + @Override + public void reload() { + load(); + } + + public void load() { + ConfigManager cfg = GrimAPI.INSTANCE.getConfigManager().getConfig(); + boolean enabled = cfg.getBooleanElse("history.enabled", false); + String rawType = enabled ? cfg.getStringElse("history.database.type", "SQLITE").toUpperCase() : "NOOP"; + + switch (rawType) { + case "SQLITE" -> { + if (!(database instanceof SQLiteViolationDatabase)) { + database.disconnect(); + try { + // Init sqlite + Class.forName("org.sqlite.JDBC"); + this.database = new SQLiteViolationDatabase(plugin); + } catch (ClassNotFoundException e) { + plugin.getLogger().log(Level.SEVERE, + """ + 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 if /grim history support is not desired""" + ); + this.database = NoOpViolationDatabase.INSTANCE; + } + database.connect(); + } + } + + case "MYSQL" -> { + String host = cfg.getStringElse("history.database.host", "localhost:3306"); + 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 MySQLViolationDatabase mysql + && mysql.sameConfig(host, db, user, pwd)) { + break; // nothing changed → keep pool + } + database.disconnect(); + database = new MySQLViolationDatabase(plugin, host, db, user, pwd); + database.connect(); + } + + default -> { // NOOP or invalid + if (!(database instanceof NoOpViolationDatabase)) { + database.disconnect(); + database = NoOpViolationDatabase.INSTANCE; + } + } + } + } + + public void logAlert(GrimPlayer player, String verbose, String checkName, int vls) { + GrimAPI.INSTANCE.getScheduler().getAsyncScheduler().runNow(plugin, () -> database.logAlert(player, verbose, checkName, vls)); + } + + public int getLogCount(UUID player) { + return database.getLogCount(player); + } + + public List getViolations(UUID player, int page, int limit) { + return database.getViolations(player, page, limit); + } + + public boolean isEnabled() { + return !(database instanceof NoOpViolationDatabase); + } +} diff --git a/common/src/main/java/ac/grim/grimac/platform/api/entity/GrimEntity.java b/common/src/main/java/ac/grim/grimac/platform/api/entity/GrimEntity.java index 27a49ee292..82236e60c9 100644 --- a/common/src/main/java/ac/grim/grimac/platform/api/entity/GrimEntity.java +++ b/common/src/main/java/ac/grim/grimac/platform/api/entity/GrimEntity.java @@ -1,15 +1,13 @@ package ac.grim.grimac.platform.api.entity; +import ac.grim.grimac.api.GrimIdentity; import ac.grim.grimac.platform.api.world.PlatformWorld; import ac.grim.grimac.utils.math.Location; import org.checkerframework.checker.nullness.qual.NonNull; -import java.util.UUID; import java.util.concurrent.CompletableFuture; -public interface GrimEntity { - UUID getUniqueId(); - +public interface GrimEntity extends GrimIdentity { /** * Eject any passenger. * diff --git a/common/src/main/java/ac/grim/grimac/platform/api/player/AbstractPlatformPlayerFactory.java b/common/src/main/java/ac/grim/grimac/platform/api/player/AbstractPlatformPlayerFactory.java index ad17f0e56f..61de0c863a 100644 --- a/common/src/main/java/ac/grim/grimac/platform/api/player/AbstractPlatformPlayerFactory.java +++ b/common/src/main/java/ac/grim/grimac/platform/api/player/AbstractPlatformPlayerFactory.java @@ -2,6 +2,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Collection; @@ -11,9 +12,8 @@ public abstract class AbstractPlatformPlayerFactory implements PlatformPlayerFactory { protected final PlatformPlayerCache cache = PlatformPlayerCache.getInstance(); - @Override - public @Nullable - final PlatformPlayer getFromUUID(@NonNull UUID uuid) { + @Override @Nullable + public final PlatformPlayer getFromUUID(@NonNull UUID uuid) { // Check cache first PlatformPlayer cachedPlayer = cache.getPlayer(uuid); if (cachedPlayer != null) { @@ -31,6 +31,18 @@ final PlatformPlayer getFromUUID(@NonNull UUID uuid) { return cache.addOrGetPlayer(uuid, platformPlayer); } + @Override @Nullable + public PlatformPlayer getFromName(@NonNull String name) { + T nativePlayer = getNativePlayer(name); + if (nativePlayer == null) { + return null; + } + + // Create new PlatformPlayer and cache it + PlatformPlayer platformPlayer = createPlatformPlayer(nativePlayer); + return cache.addOrGetPlayer(platformPlayer.getUniqueId(), platformPlayer); + } + @Override public final PlatformPlayer getFromNativePlayerType(@NonNull Object playerObject) { if (!isNativePlayerType(playerObject)) { @@ -81,6 +93,8 @@ public void replaceNativePlayer(@NonNull UUID uuid, @NonNull T player) {} */ protected abstract T getNativePlayer(@NonNull UUID uuid); + protected abstract T getNativePlayer(@NonNull String name); + /** * Creates a PlatformPlayer instance from the native player object. * @@ -118,4 +132,11 @@ public void replaceNativePlayer(@NonNull UUID uuid, @NonNull T player) {} * @return a collection of native player objects */ protected abstract Collection getNativeOnlinePlayers(); + + + @Override + public abstract OfflinePlatformPlayer getOfflineFromUUID(@NotNull UUID uuid); + + @Override + public abstract OfflinePlatformPlayer getOfflineFromName(@NotNull String name); } diff --git a/common/src/main/java/ac/grim/grimac/platform/api/player/OfflinePlatformPlayer.java b/common/src/main/java/ac/grim/grimac/platform/api/player/OfflinePlatformPlayer.java new file mode 100644 index 0000000000..cc3f6ad596 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/platform/api/player/OfflinePlatformPlayer.java @@ -0,0 +1,10 @@ +package ac.grim.grimac.platform.api.player; + +import ac.grim.grimac.api.GrimIdentity; + +public interface OfflinePlatformPlayer extends GrimIdentity { + + boolean isOnline(); + + String getName(); +} diff --git a/common/src/main/java/ac/grim/grimac/platform/api/player/PlatformPlayer.java b/common/src/main/java/ac/grim/grimac/platform/api/player/PlatformPlayer.java index fea5591c7c..2a1f7d6952 100644 --- a/common/src/main/java/ac/grim/grimac/platform/api/player/PlatformPlayer.java +++ b/common/src/main/java/ac/grim/grimac/platform/api/player/PlatformPlayer.java @@ -7,24 +7,20 @@ import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; -public interface PlatformPlayer extends GrimEntity { +public interface PlatformPlayer extends GrimEntity, OfflinePlatformPlayer { void kickPlayer(String textReason); - boolean hasPermission(String s); - - boolean hasPermission(String s, boolean defaultIfUnset); - boolean isSneaking(); void setSneaking(boolean b); - void sendMessage(String message); + boolean hasPermission(String s); - void sendMessage(Component message); + boolean hasPermission(String s, boolean defaultIfUnset); - boolean isOnline(); + void sendMessage(String message); - String getName(); + void sendMessage(Component message); void updateInventory(); diff --git a/common/src/main/java/ac/grim/grimac/platform/api/player/PlatformPlayerFactory.java b/common/src/main/java/ac/grim/grimac/platform/api/player/PlatformPlayerFactory.java index 0326c3a234..742325b433 100644 --- a/common/src/main/java/ac/grim/grimac/platform/api/player/PlatformPlayerFactory.java +++ b/common/src/main/java/ac/grim/grimac/platform/api/player/PlatformPlayerFactory.java @@ -4,6 +4,12 @@ import java.util.UUID; public interface PlatformPlayerFactory { + OfflinePlatformPlayer getOfflineFromUUID(UUID uuid); + + OfflinePlatformPlayer getOfflineFromName(String name); + + PlatformPlayer getFromName(String name); + PlatformPlayer getFromUUID(UUID uuid); PlatformPlayer getFromNativePlayerType(Object playerObject); diff --git a/common/src/main/java/ac/grim/grimac/player/GrimPlayer.java b/common/src/main/java/ac/grim/grimac/player/GrimPlayer.java index 2ca21a01fd..8ff0a9ab85 100644 --- a/common/src/main/java/ac/grim/grimac/player/GrimPlayer.java +++ b/common/src/main/java/ac/grim/grimac/player/GrimPlayer.java @@ -34,17 +34,15 @@ import ac.grim.grimac.utils.data.tags.SyncedTags; import ac.grim.grimac.utils.enums.FluidTag; import ac.grim.grimac.utils.enums.Pose; -import ac.grim.grimac.utils.latency.CompensatedEntities; -import ac.grim.grimac.utils.latency.CompensatedFireworks; -import ac.grim.grimac.utils.latency.CompensatedInventory; -import ac.grim.grimac.utils.latency.CompensatedWorld; -import ac.grim.grimac.utils.latency.LatencyUtils; +import ac.grim.grimac.utils.inventory.InventoryDesyncStatus; +import ac.grim.grimac.utils.latency.*; import ac.grim.grimac.utils.math.GrimMath; import ac.grim.grimac.utils.math.Location; import ac.grim.grimac.utils.math.TrigHandler; import ac.grim.grimac.utils.math.Vector3dm; import ac.grim.grimac.utils.nmsutil.BlockProperties; import ac.grim.grimac.utils.nmsutil.GetBoundingBox; +import ac.grim.grimac.utils.nmsutil.ReachUtils; import ac.grim.grimac.utils.reflection.ViaVersionUtil; import com.github.retrooper.packetevents.PacketEvents; import com.github.retrooper.packetevents.event.PacketSendEvent; @@ -214,7 +212,7 @@ public class GrimPlayer implements GrimUser { public final CompensatedFireworks fireworks; public final CompensatedWorld compensatedWorld; public final CompensatedEntities compensatedEntities; - public final LatencyUtils latencyUtils = new LatencyUtils(this); + public final ILatencyUtils latencyUtils = new LatencyUtils(this); public final PointThreeEstimator pointThreeEstimator; public final TrigHandler trigHandler = new TrigHandler(this); public final PacketStateData packetStateData = new PacketStateData(); @@ -240,6 +238,9 @@ public class GrimPlayer implements GrimUser { // possibleEyeHeights[0] = Standing eye heights, [1] = Sneaking. [2] = Elytra, Swimming, and Riptide Trident which only exists in 1.9+ public final double[][] possibleEyeHeights = new double[3][]; public int totalFlyingPacketsSent; + public boolean hasInventoryOpen; + public long lastInventoryOpen; + public InventoryDesyncStatus inventoryDesyncStatus; public final Queue placeUseItemPackets = new LinkedBlockingQueue<>(); public final Queue queuedBreaks = new LinkedBlockingQueue<>(); public final PlayerBlockHistory blockHistory = new PlayerBlockHistory(); @@ -647,6 +648,48 @@ public double[] getPossibleEyeHeights() { // We don't return sleeping eye height } } + // 1.8-1.10.2 specific mouse delay fix (MC-67665) + // https://bugs.mojang.com/browse/MC-67665 + // 1.9-1.21.1 specific desync due to skipped ticks + // Players can be a tick behind on both pitch and yaw together + // 1.21.2+ added end tick input packet, fixing skipped tick issues + public Vector3dm[] getPossibleLookVectors(boolean isPrediction) { + // If we are a tick behind, we don't know their next look so only use current + if (isPrediction) { + return new Vector3dm[]{ReachUtils.getLook(this, this.xRot, this.yRot)}; + } + + // 1.9-1.10.2: All three vectors (normal, mouse delay, tick desync) + if (this.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9) && + this.getClientVersion().isOlderThan(ClientVersion.V_1_11)) { + return new Vector3dm[]{ + ReachUtils.getLook(this, this.xRot, this.yRot), + ReachUtils.getLook(this, this.lastXRot, this.yRot), + ReachUtils.getLook(this, this.lastXRot, this.lastYRot) + }; + } + // 1.11-1.21.1: Two vectors (normal, tick desync) + else if (this.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_11) && + this.getClientVersion().isOlderThan(ClientVersion.V_1_21_2)) { + return new Vector3dm[]{ + ReachUtils.getLook(this, this.xRot, this.yRot), + ReachUtils.getLook(this, this.lastXRot, this.lastYRot) + }; + } + // 1.8: Two vectors (normal, mouse delay) + else if (this.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_8) && + this.getClientVersion().isOlderThan(ClientVersion.V_1_9)) { + return new Vector3dm[]{ + ReachUtils.getLook(this, this.xRot, this.yRot), + ReachUtils.getLook(this, this.lastXRot, this.yRot) + }; + } + // 1.7 and 1.21.2+: Just normal vector + else { + return new Vector3dm[]{ReachUtils.getLook(this, this.xRot, this.yRot)}; + } + } + @Override public int getTransactionPing() { return GrimMath.floor(transactionPing / 1e6); diff --git a/common/src/main/java/ac/grim/grimac/predictionengine/PointThreeEstimator.java b/common/src/main/java/ac/grim/grimac/predictionengine/PointThreeEstimator.java index 6a5d28e595..7db473be8f 100644 --- a/common/src/main/java/ac/grim/grimac/predictionengine/PointThreeEstimator.java +++ b/common/src/main/java/ac/grim/grimac/predictionengine/PointThreeEstimator.java @@ -1,5 +1,6 @@ package ac.grim.grimac.predictionengine; +import ac.grim.grimac.checks.impl.combat.Reach; import ac.grim.grimac.player.GrimPlayer; import ac.grim.grimac.predictionengine.predictions.PredictionEngine; import ac.grim.grimac.utils.collisions.CollisionData; @@ -20,6 +21,7 @@ import com.github.retrooper.packetevents.protocol.world.states.defaulttags.BlockTags; import com.github.retrooper.packetevents.protocol.world.states.type.StateType; import com.github.retrooper.packetevents.protocol.world.states.type.StateTypes; +import com.github.retrooper.packetevents.util.Vector3i; import lombok.Getter; import lombok.Setter; @@ -106,6 +108,7 @@ public class PointThreeEstimator { private boolean isNearHorizontalFlowingLiquid = false; // We can't calculate the direction, only a toggle private boolean isNearVerticalFlowingLiquid = false; // We can't calculate exact values, once again a toggle private boolean isNearBubbleColumn = false; // We can't calculate exact values once again + public boolean isNearNetherPortal = false; // We can't calculate exact values once again private int maxPositiveLevitation = Integer.MIN_VALUE; // Positive potion effects [0, 128] private int minNegativeLevitation = Integer.MAX_VALUE; // Negative potion effects [-127, -1]r @@ -154,6 +157,8 @@ public void handleChangeBlock(int x, int y, int z, WrappedBlockState state) { isNearFluid = true; } + player.checkManager.getPacketCheck(Reach.class).handleBlockChange(new Vector3i(x, y, z), state); + if (pointThreeBox.isIntersected(new SimpleCollisionBox(x, y, z))) { // https://github.com/MWHunter/Grim/issues/613 int controllingEntityId = player.inVehicle() ? player.getRidingVehicleId() : player.entityID; @@ -249,6 +254,7 @@ private void checkNearbyBlocks(SimpleCollisionBox pointThreeBox) { isNearClimbable = false; isNearBubbleColumn = false; isNearFluid = false; + isNearNetherPortal = false; // Check for flowing water Collisions.hasMaterial(player, pointThreeBox, (pair) -> { @@ -270,6 +276,10 @@ private void checkNearbyBlocks(SimpleCollisionBox pointThreeBox) { isNearFluid = true; } + if (state.getType() == StateTypes.NETHER_PORTAL) { + isNearNetherPortal = true; + } + return false; }); } diff --git a/common/src/main/java/ac/grim/grimac/predictionengine/predictions/PredictionEngine.java b/common/src/main/java/ac/grim/grimac/predictionengine/predictions/PredictionEngine.java index e94fef94cc..914fa6b27a 100644 --- a/common/src/main/java/ac/grim/grimac/predictionengine/predictions/PredictionEngine.java +++ b/common/src/main/java/ac/grim/grimac/predictionengine/predictions/PredictionEngine.java @@ -479,52 +479,63 @@ public int sortVectorData(VectorData a, VectorData b, GrimPlayer player) { // Order priority (to avoid false positives and false flagging future predictions): // Knockback and explosions // 0.03 ticks + // Movement without input // Normal movement // First bread knockback and explosions // Flagging groundspoof // Flagging flip items if (a.isExplosion()) - aScore -= 5; + aScore -= 10; if (a.isKnockback()) - aScore -= 5; + aScore -= 10; if (b.isExplosion()) - bScore -= 5; + bScore -= 10; if (b.isKnockback()) - bScore -= 5; + bScore -= 10; if (a.isFirstBreadExplosion()) - aScore += 1; + aScore += 2; if (b.isFirstBreadExplosion()) - bScore += 1; + bScore += 2; if (a.isFirstBreadKb()) - aScore += 1; + aScore += 2; if (b.isFirstBreadKb()) - bScore += 1; + bScore += 2; if (a.isFlipItem()) - aScore += 3; + aScore += 6; if (b.isFlipItem()) - bScore += 3; + bScore += 6; if (a.isZeroPointZeroThree()) - aScore -= 1; + aScore -= 2; if (b.isZeroPointZeroThree()) + bScore -= 2; + + if (a.isWithInput() || a.isJump()) + aScore += 1; + else + aScore -= 1; + + if (b.isWithInput() || b.isJump()) + bScore += 1; + else bScore -= 1; // If the player is on the ground but the vector leads the player off the ground if ((player.inVehicle() ? player.clientControlledVerticalCollision : player.onGround) && a.vector.getY() >= 0) - aScore += 2; + aScore += 4; if ((player.inVehicle() ? player.clientControlledVerticalCollision : player.onGround) && b.vector.getY() >= 0) - bScore += 2; + bScore += 4; if (aScore != bScore) return Integer.compare(aScore, bScore); @@ -812,9 +823,9 @@ private void loopVectors(GrimPlayer player, Set possibleVectors, flo continue; for (int strafe = strafeMin; strafe <= strafeMax; strafe++) { for (int forward = forwardMin; forward <= forwardMax; forward++) { - VectorData result = new VectorData(possibleLastTickOutput.vector.clone() + VectorData result = new VectorData.MoveVectorData(possibleLastTickOutput.vector.clone() .add(getMovementResultFromInput(player, transformInputsToVector(player, new Vector3dm(strafe, 0, forward)), speed, player.xRot)), - possibleLastTickOutput, VectorData.VectorType.InputResult); + possibleLastTickOutput, VectorData.VectorType.InputResult, forward, strafe); result = result.returnNewModified(result.vector.clone().multiply(player.stuckSpeedMultiplier), VectorData.VectorType.StuckMultiplier); result = result.returnNewModified(handleOnClimbable(result.vector.clone(), player), VectorData.VectorType.Climbable); // Signal that we need to flip sneaking bounding box diff --git a/common/src/main/java/ac/grim/grimac/utils/anticheat/update/BlockPlace.java b/common/src/main/java/ac/grim/grimac/utils/anticheat/update/BlockPlace.java index 021656ee7c..f4bfc26c25 100644 --- a/common/src/main/java/ac/grim/grimac/utils/anticheat/update/BlockPlace.java +++ b/common/src/main/java/ac/grim/grimac/utils/anticheat/update/BlockPlace.java @@ -9,7 +9,7 @@ import ac.grim.grimac.utils.collisions.datatypes.CollisionBox; import ac.grim.grimac.utils.collisions.datatypes.ComplexCollisionBox; import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox; -import ac.grim.grimac.utils.data.HitData; +import ac.grim.grimac.utils.data.BlockHitData; import ac.grim.grimac.utils.data.packetentity.PacketEntity; import ac.grim.grimac.utils.latency.CompensatedWorld; import ac.grim.grimac.utils.math.GrimMath; @@ -69,7 +69,7 @@ public class BlockPlace { @Getter StateType material; @Getter - @Nullable HitData hitData; + @Nullable BlockHitData hitData; @Getter int faceId; BlockFace face; @@ -81,7 +81,7 @@ public class BlockPlace { Vector3f cursor; public final int sequence; - public BlockPlace(GrimPlayer player, InteractionHand hand, Vector3i blockPosition, int faceId, BlockFace face, ItemStack itemStack, HitData hitData, int sequence) { + public BlockPlace(GrimPlayer player, InteractionHand hand, Vector3i blockPosition, int faceId, BlockFace face, ItemStack itemStack, BlockHitData hitData, int sequence) { this.player = player; this.hand = hand; this.blockPosition = blockPosition; diff --git a/common/src/main/java/ac/grim/grimac/utils/data/BlockHitData.java b/common/src/main/java/ac/grim/grimac/utils/data/BlockHitData.java new file mode 100644 index 0000000000..78fc4c128f --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/utils/data/BlockHitData.java @@ -0,0 +1,32 @@ +package ac.grim.grimac.utils.data; + +import ac.grim.grimac.utils.math.Vector3dm; +import com.github.retrooper.packetevents.protocol.world.BlockFace; +import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState; +import com.github.retrooper.packetevents.util.Vector3d; +import com.github.retrooper.packetevents.util.Vector3i; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class BlockHitData extends HitData { + Vector3i position; + WrappedBlockState state; + BlockFace closestDirection; +// public Boolean success; + + public BlockHitData(Vector3i position, Vector3dm blockHitLocation, BlockFace closestDirection, WrappedBlockState state +// , Boolean success + ) { + super(blockHitLocation); + this.position = position; + this.closestDirection = closestDirection; + this.state = state; +// this.success = success; + } + + public Vector3d getRelativeBlockHitLocation() { + return new Vector3d(blockHitLocation.getX() - position.getX(), blockHitLocation.getY() - position.getY(), blockHitLocation.getZ() - position.getZ()); + } +} diff --git a/common/src/main/java/ac/grim/grimac/utils/data/EntityHitData.java b/common/src/main/java/ac/grim/grimac/utils/data/EntityHitData.java new file mode 100644 index 0000000000..addb7e95f0 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/utils/data/EntityHitData.java @@ -0,0 +1,23 @@ +package ac.grim.grimac.utils.data; + +import ac.grim.grimac.utils.data.packetentity.PacketEntity; +import ac.grim.grimac.utils.math.Vector3dm; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class EntityHitData extends HitData { + private final PacketEntity entity; + + public EntityHitData(PacketEntity packetEntity) { + this(packetEntity, new Vector3dm(packetEntity.trackedServerPosition.getPos().x, + packetEntity.trackedServerPosition.getPos().y, + packetEntity.trackedServerPosition.getPos().z)); + } + + public EntityHitData(PacketEntity packetEntity, Vector3dm intersectionPoint) { + super(intersectionPoint); // Use actual intersection point + this.entity = packetEntity; + } +} diff --git a/common/src/main/java/ac/grim/grimac/utils/data/HitData.java b/common/src/main/java/ac/grim/grimac/utils/data/HitData.java index 8f8effd875..449f8fb134 100644 --- a/common/src/main/java/ac/grim/grimac/utils/data/HitData.java +++ b/common/src/main/java/ac/grim/grimac/utils/data/HitData.java @@ -1,29 +1,15 @@ package ac.grim.grimac.utils.data; import ac.grim.grimac.utils.math.Vector3dm; -import com.github.retrooper.packetevents.protocol.world.BlockFace; -import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState; -import com.github.retrooper.packetevents.util.Vector3d; -import com.github.retrooper.packetevents.util.Vector3i; -import lombok.Getter; -import lombok.ToString; -@Getter -@ToString public class HitData { - Vector3i position; Vector3dm blockHitLocation; - WrappedBlockState state; - BlockFace closestDirection; - public HitData(Vector3i position, Vector3dm blockHitLocation, BlockFace closestDirection, WrappedBlockState state) { - this.position = position; + public HitData(Vector3dm blockHitLocation) { this.blockHitLocation = blockHitLocation; - this.closestDirection = closestDirection; - this.state = state; } - public Vector3d getRelativeBlockHitLocation() { - return new Vector3d(blockHitLocation.getX() - position.getX(), blockHitLocation.getY() - position.getY(), blockHitLocation.getZ() - position.getZ()); + public ac.grim.grimac.utils.math.Vector3dm getBlockHitLocation() { + return this.blockHitLocation; } } diff --git a/common/src/main/java/ac/grim/grimac/utils/data/PistonData.java b/common/src/main/java/ac/grim/grimac/utils/data/PistonData.java deleted file mode 100644 index 0d8657b70d..0000000000 --- a/common/src/main/java/ac/grim/grimac/utils/data/PistonData.java +++ /dev/null @@ -1,36 +0,0 @@ -package ac.grim.grimac.utils.data; - -import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox; -import com.github.retrooper.packetevents.protocol.world.BlockFace; - -import java.util.List; - -public class PistonData { - public final boolean isPush; - public final boolean hasSlimeBlock; - public final boolean hasHoneyBlock; - public final BlockFace direction; - public final int lastTransactionSent; - - // Calculate if the player has no-push, and when to end the possibility of applying piston - public int ticksOfPistonBeingAlive = 0; - - // The actual blocks pushed by the piston, plus the piston head itself - public List boxes; - - public PistonData(BlockFace direction, List pushedBlocks, int lastTransactionSent, boolean isPush, boolean hasSlimeBlock, boolean hasHoneyBlock) { - this.direction = direction; - this.boxes = pushedBlocks; - this.lastTransactionSent = lastTransactionSent; - this.isPush = isPush; - this.hasSlimeBlock = hasSlimeBlock; - this.hasHoneyBlock = hasHoneyBlock; - } - - // We don't know when the piston has applied, or what stage of pushing it is on - // Therefore, we need to use what we have - the number of movement packets. - // 10 is a very cautious number - public boolean tickIfGuaranteedFinished() { - return ++ticksOfPistonBeingAlive >= 10; - } -} diff --git a/common/src/main/java/ac/grim/grimac/utils/data/PistonTemplate.java b/common/src/main/java/ac/grim/grimac/utils/data/PistonTemplate.java new file mode 100644 index 0000000000..f21030bb5d --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/utils/data/PistonTemplate.java @@ -0,0 +1,11 @@ +package ac.grim.grimac.utils.data; + +import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox; +import com.github.retrooper.packetevents.protocol.world.BlockFace; + +import java.util.List; + +public record PistonTemplate(BlockFace dir, + List boxes, + boolean push, boolean slime, boolean honey) { +} diff --git a/common/src/main/java/ac/grim/grimac/utils/data/PlayerPistonData.java b/common/src/main/java/ac/grim/grimac/utils/data/PlayerPistonData.java new file mode 100644 index 0000000000..4f4f356e27 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/utils/data/PlayerPistonData.java @@ -0,0 +1,23 @@ +package ac.grim.grimac.utils.data; + + + +public class PlayerPistonData { + public final PistonTemplate pistonTemplate; + public final int lastTransactionSent; + + // Calculate if the player has no-push, and when to end the possibility of applying piston + int ticksOfPistonBeingAlive = 0; + + public PlayerPistonData(PistonTemplate playerPistonData, int lastTransactionSent) { + this.pistonTemplate = playerPistonData; + this.lastTransactionSent = lastTransactionSent; + } + + // We don't know when the piston has applied, or what stage of pushing it is on + // Therefore, we need to use what we have - the number of movement packets. + // 10 is a very cautious number + public boolean tickIfGuaranteedFinished() { + return ++ticksOfPistonBeingAlive >= 10; + } +} diff --git a/common/src/main/java/ac/grim/grimac/utils/data/ReachInterpolationData.java b/common/src/main/java/ac/grim/grimac/utils/data/ReachInterpolationData.java index 8110030328..08798ef932 100644 --- a/common/src/main/java/ac/grim/grimac/utils/data/ReachInterpolationData.java +++ b/common/src/main/java/ac/grim/grimac/utils/data/ReachInterpolationData.java @@ -196,6 +196,133 @@ public SimpleCollisionBox getPossibleHitboxCombined() { return minimumInterpLocation; } + public CollisionBox getOverlapHitboxCombined() { + int interpSteps = getInterpolationSteps(); + + // Calculate step increments for each axis + double stepMinX = (targetLocation.minX - startingLocation.minX) / (double) interpSteps; + double stepMaxX = (targetLocation.maxX - startingLocation.maxX) / (double) interpSteps; + double stepMinY = (targetLocation.minY - startingLocation.minY) / (double) interpSteps; + double stepMaxY = (targetLocation.maxY - startingLocation.maxY) / (double) interpSteps; + double stepMinZ = (targetLocation.minZ - startingLocation.minZ) / (double) interpSteps; + double stepMaxZ = (targetLocation.maxZ - startingLocation.maxZ) / (double) interpSteps; + + // Track the intersection of all expanded hitboxes + double overallMinX = Double.NEGATIVE_INFINITY; + double overallMaxX = Double.POSITIVE_INFINITY; + double overallMinY = Double.NEGATIVE_INFINITY; + double overallMaxY = Double.POSITIVE_INFINITY; + double overallMinZ = Double.NEGATIVE_INFINITY; + double overallMaxZ = Double.POSITIVE_INFINITY; + + boolean isFirstStep = true; + + for (int step = interpolationStepsLowBound; step <= interpolationStepsHighBound; step++) { + // Compute interpolated position for this step + double currentMinX = startingLocation.minX + (step * stepMinX); + double currentMaxX = startingLocation.maxX + (step * stepMaxX); + double currentMinY = startingLocation.minY + (step * stepMinY); + double currentMaxY = startingLocation.maxY + (step * stepMaxY); + double currentMinZ = startingLocation.minZ + (step * stepMinZ); + double currentMaxZ = startingLocation.maxZ + (step * stepMaxZ); + + // Create the collision box for this step's position + // Create boxes for each bottom corner + SimpleCollisionBox[] cornerBoxes = new SimpleCollisionBox[4]; + + // Bottom corners: (minX,minY,minZ), (maxX,minY,minZ), (minX,minY,maxZ), (maxX,minY,maxZ) + cornerBoxes[0] = new SimpleCollisionBox(currentMinX, currentMinY, currentMinZ, + currentMinX, currentMinY, currentMinZ); + cornerBoxes[1] = new SimpleCollisionBox(currentMaxX, currentMinY, currentMinZ, + currentMaxX, currentMinY, currentMinZ); + cornerBoxes[2] = new SimpleCollisionBox(currentMinX, currentMinY, currentMaxZ, + currentMinX, currentMinY, currentMaxZ); + cornerBoxes[3] = new SimpleCollisionBox(currentMaxX, currentMinY, currentMaxZ, + currentMaxX, currentMinY, currentMaxZ); + + // Expand each corner box by entity dimensions + for (SimpleCollisionBox cornerBox : cornerBoxes) { + GetBoundingBox.expandBoundingBoxByEntityDimensions(cornerBox, player, entity); + } + + // Get the overlap of the 4 corner boxes + CollisionBox stepOverlap = getOverlapOfBoxes(cornerBoxes); + if (stepOverlap == NoCollisionBox.INSTANCE) + return NoCollisionBox.INSTANCE; + SimpleCollisionBox stepBox = (SimpleCollisionBox) stepOverlap; + + // Initialize overall bounds with the first expanded box + if (isFirstStep) { + overallMinX = stepBox.minX; + overallMaxX = stepBox.maxX; + overallMinY = stepBox.minY; + overallMaxY = stepBox.maxY; + overallMinZ = stepBox.minZ; + overallMaxZ = stepBox.maxZ; + isFirstStep = false; + } else { + // Update bounds to the intersection of all expanded boxes + overallMinX = Math.max(overallMinX, stepBox.minX); + overallMaxX = Math.min(overallMaxX, stepBox.maxX); + overallMinY = Math.max(overallMinY, stepBox.minY); + overallMaxY = Math.min(overallMaxY, stepBox.maxY); + overallMinZ = Math.max(overallMinZ, stepBox.minZ); + overallMaxZ = Math.min(overallMaxZ, stepBox.maxZ); + } + + // Early exit if the intersection becomes empty + if (overallMinX > overallMaxX || overallMinY > overallMaxY || overallMinZ > overallMaxZ) { + return NoCollisionBox.INSTANCE; + } + } + + // Check if the final intersection is valid + if (overallMinX > overallMaxX || overallMinY > overallMaxY || overallMinZ > overallMaxZ) { + return NoCollisionBox.INSTANCE; + } + + return new SimpleCollisionBox( + overallMinX, overallMinY, overallMinZ, + overallMaxX, overallMaxY, overallMaxZ + ); + } + + private CollisionBox getOverlapOfBoxes(SimpleCollisionBox[] boxes) { + double minX = Double.NEGATIVE_INFINITY; + double maxX = Double.POSITIVE_INFINITY; + double minY = Double.NEGATIVE_INFINITY; + double maxY = Double.POSITIVE_INFINITY; + double minZ = Double.NEGATIVE_INFINITY; + double maxZ = Double.POSITIVE_INFINITY; + + boolean first = true; + + for (SimpleCollisionBox box : boxes) { + if (first) { + minX = box.minX; + maxX = box.maxX; + minY = box.minY; + maxY = box.maxY; + minZ = box.minZ; + maxZ = box.maxZ; + first = false; + } else { + minX = Math.max(minX, box.minX); + maxX = Math.min(maxX, box.maxX); + minY = Math.max(minY, box.minY); + maxY = Math.min(maxY, box.maxY); + minZ = Math.max(minZ, box.minZ); + maxZ = Math.min(maxZ, box.maxZ); + } + + if (minX > maxX || minY > maxY || minZ > maxZ) { + return NoCollisionBox.INSTANCE; + } + } + + return new SimpleCollisionBox(minX, minY, minZ, maxX, maxY, maxZ); + } + public void updatePossibleStartingLocation(SimpleCollisionBox possibleLocationCombined) { //GrimACBukkitLoaderPlugin.staticGetLogger().info(ChatColor.BLUE + "Updated new starting location as second trans hasn't arrived " + startingLocation); this.startingLocation = combineCollisionBox(startingLocation, possibleLocationCombined); diff --git a/common/src/main/java/ac/grim/grimac/utils/data/VectorData.java b/common/src/main/java/ac/grim/grimac/utils/data/VectorData.java index aa77492115..3e6541ff69 100644 --- a/common/src/main/java/ac/grim/grimac/utils/data/VectorData.java +++ b/common/src/main/java/ac/grim/grimac/utils/data/VectorData.java @@ -6,13 +6,38 @@ import java.util.Objects; public class VectorData { + public static final class MoveVectorData extends VectorData { + public int x; + public int z; + + public MoveVectorData(Vector3dm vector, VectorData lastVector, VectorType vectorType, int x, int z) { + super(vector, lastVector, vectorType); + this.x = x; + this.z = z; + + if (x != 0 || z != 0) { + addVectorType(VectorType.WithInput); + } + } + + public MoveVectorData(Vector3dm vector, VectorType vectorType, int x, int z) { + super(vector, vectorType); + this.x = x; + this.z = z; + + if (x != 0 || z != 0) { + addVectorType(VectorType.WithInput); + } + } + } + public VectorType vectorType; public VectorData lastVector; public VectorData preUncertainty; public Vector3dm vector; @Getter - private boolean isKnockback, firstBreadKb, isExplosion, firstBreadExplosion, isTrident, isZeroPointZeroThree, isSwimHop, isFlipSneaking, isFlipItem, isJump, isAttackSlow = false; + private boolean isKnockback, firstBreadKb, isExplosion, firstBreadExplosion, isTrident, isZeroPointZeroThree, isSwimHop, isFlipSneaking, isFlipItem, isJump, isAttackSlow = false, isWithInput = false; // For handling replacing the type of vector it is while keeping data public VectorData(Vector3dm vector, VectorData lastVector, VectorType vectorType) { @@ -33,6 +58,7 @@ public VectorData(Vector3dm vector, VectorData lastVector, VectorType vectorType isJump = lastVector.isJump; preUncertainty = lastVector.preUncertainty; isAttackSlow = lastVector.isAttackSlow; + isWithInput = lastVector.isWithInput; } addVectorType(vectorType); @@ -78,6 +104,7 @@ public void addVectorType(VectorType type) { case Flip_Use_Item -> isFlipItem = true; case Jump -> isJump = true; case AttackSlow -> isAttackSlow = true; + case WithInput -> isWithInput = true; } } @@ -104,6 +131,7 @@ public enum VectorType { Explosion, FirstBreadExplosion, InputResult, + WithInput, StuckMultiplier, Spectator, Dead, diff --git a/common/src/main/java/ac/grim/grimac/utils/data/packetentity/PacketEntity.java b/common/src/main/java/ac/grim/grimac/utils/data/packetentity/PacketEntity.java index 1a0c69aeea..4e8a0ed06c 100644 --- a/common/src/main/java/ac/grim/grimac/utils/data/packetentity/PacketEntity.java +++ b/common/src/main/java/ac/grim/grimac/utils/data/packetentity/PacketEntity.java @@ -16,6 +16,7 @@ package ac.grim.grimac.utils.data.packetentity; import ac.grim.grimac.player.GrimPlayer; +import ac.grim.grimac.utils.collisions.datatypes.CollisionBox; import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox; import ac.grim.grimac.utils.data.ReachInterpolationData; import ac.grim.grimac.utils.data.TrackedPosition; @@ -214,6 +215,18 @@ public SimpleCollisionBox getPossibleCollisionBoxes() { return ReachInterpolationData.combineCollisionBox(oldPacketLocation.getPossibleHitboxCombined(), newPacketLocation.getPossibleHitboxCombined()); } + public CollisionBox getMinimumPossibleCollisionBoxes() { + if (oldPacketLocation == null) { + return newPacketLocation.getOverlapHitboxCombined(); + } + + return ReachInterpolationData.getOverlapHitbox(oldPacketLocation.getOverlapHitboxCombined(), newPacketLocation.getOverlapHitboxCombined()); + } + + public PacketEntity getRiding() { + return riding; + } + public OptionalInt getPotionEffectLevel(PotionType effect) { final int amplifier = potionsMap == null ? -1 : potionsMap.getInt(effect); return amplifier == -1 ? OptionalInt.empty() : OptionalInt.of(amplifier); diff --git a/common/src/main/java/ac/grim/grimac/utils/data/packetentity/PacketEntityPainting.java b/common/src/main/java/ac/grim/grimac/utils/data/packetentity/PacketEntityPainting.java index 7c371b0f0d..22e5c55f15 100644 --- a/common/src/main/java/ac/grim/grimac/utils/data/packetentity/PacketEntityPainting.java +++ b/common/src/main/java/ac/grim/grimac/utils/data/packetentity/PacketEntityPainting.java @@ -16,4 +16,11 @@ public PacketEntityPainting(GrimPlayer player, UUID uuid, double x, double y, do super(player, uuid, EntityTypes.PAINTING, x, y, z); this.direction = direction; } + + // This is incorrect, temporary measure to exempt paintings from HitboxEntity + // Will properly model later + @Override + public boolean canHit() { + return false; + } } diff --git a/common/src/main/java/ac/grim/grimac/utils/inventory/InventoryDesyncStatus.java b/common/src/main/java/ac/grim/grimac/utils/inventory/InventoryDesyncStatus.java new file mode 100644 index 0000000000..6c6a7761b4 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/utils/inventory/InventoryDesyncStatus.java @@ -0,0 +1,7 @@ +package ac.grim.grimac.utils.inventory; + +public enum InventoryDesyncStatus { + BEACON, + NETHER_PORTAL, + NOT_DESYNCED +} diff --git a/common/src/main/java/ac/grim/grimac/utils/latency/CompensatedInventory.java b/common/src/main/java/ac/grim/grimac/utils/latency/CompensatedInventory.java index 889dc0c621..487b855ff2 100644 --- a/common/src/main/java/ac/grim/grimac/utils/latency/CompensatedInventory.java +++ b/common/src/main/java/ac/grim/grimac/utils/latency/CompensatedInventory.java @@ -48,7 +48,7 @@ public class CompensatedInventory extends Check implements PacketCheck { public boolean isPacketInventoryActive = true; public boolean needResend = false; public int stateID = 0; // Don't mess up the last sent state ID by changing it - int openWindowID = 0; + public int openWindowID = 0; // Special values: // Player inventory is -1 // Unsupported inventory is -2 diff --git a/common/src/main/java/ac/grim/grimac/utils/latency/CompensatedWorld.java b/common/src/main/java/ac/grim/grimac/utils/latency/CompensatedWorld.java index 3f13118a30..f4d37a273b 100644 --- a/common/src/main/java/ac/grim/grimac/utils/latency/CompensatedWorld.java +++ b/common/src/main/java/ac/grim/grimac/utils/latency/CompensatedWorld.java @@ -6,10 +6,7 @@ import ac.grim.grimac.utils.chunks.Column; import ac.grim.grimac.utils.collisions.CollisionData; import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox; -import ac.grim.grimac.utils.data.BlockPrediction; -import ac.grim.grimac.utils.data.Pair; -import ac.grim.grimac.utils.data.PistonData; -import ac.grim.grimac.utils.data.ShulkerData; +import ac.grim.grimac.utils.data.*; import ac.grim.grimac.utils.data.packetentity.PacketEntity; import ac.grim.grimac.utils.data.packetentity.PacketEntityShulker; import ac.grim.grimac.utils.math.GrimMath; @@ -70,7 +67,7 @@ public class CompensatedWorld { public final GrimPlayer player; public final Long2ObjectMap chunks; // Packet locations for blocks - public Set activePistons = new HashSet<>(); + public Set activePistons = new HashSet<>(); public Set openShulkerBoxes = new HashSet<>(); // 1.17 with datapacks, and 1.18, have negative world offset values @Getter @@ -239,8 +236,8 @@ public boolean isNearHardEntity(SimpleCollisionBox playerBox) { } // Pistons are a block entity. - for (PistonData data : activePistons) { - for (SimpleCollisionBox box : data.boxes) { + for (PlayerPistonData data : activePistons) { + for (SimpleCollisionBox box : data.pistonTemplate.boxes()) { if (playerBox.isCollided(box)) { return true; } @@ -359,18 +356,18 @@ public void tickPlayerInPistonPushingArea() { double modY = 0; double modZ = 0; - for (PistonData data : activePistons) { - for (SimpleCollisionBox box : data.boxes) { + for (PlayerPistonData data : activePistons) { + for (SimpleCollisionBox box : data.pistonTemplate.boxes()) { if (playerBox.isCollided(box)) { - modX = Math.max(modX, Math.abs(data.direction.getModX() * 0.51D)); - modY = Math.max(modY, Math.abs(data.direction.getModY() * 0.51D)); - modZ = Math.max(modZ, Math.abs(data.direction.getModZ() * 0.51D)); + modX = Math.max(modX, Math.abs(data.pistonTemplate.dir().getModX() * 0.51D)); + modY = Math.max(modY, Math.abs(data.pistonTemplate.dir().getModY() * 0.51D)); + modZ = Math.max(modZ, Math.abs(data.pistonTemplate.dir().getModZ() * 0.51D)); playerBox.expandMax(modX, modY, modZ); playerBox.expandMin(modX * -1, modY * -1, modZ * -1); - if (data.hasSlimeBlock || (data.hasHoneyBlock && player.getClientVersion().isOlderThan(ClientVersion.V_1_15_2))) { - player.uncertaintyHandler.slimePistonBounces.add(data.direction); + if (data.pistonTemplate.slime() || (data.pistonTemplate.honey() && player.getClientVersion().isOlderThan(ClientVersion.V_1_15_2))) { + player.uncertaintyHandler.slimePistonBounces.add(data.pistonTemplate.dir()); } break; @@ -425,7 +422,7 @@ public void removeInvalidPistonLikeStuff(int transactionId) { activePistons.removeIf(data -> data.lastTransactionSent < transactionId); openShulkerBoxes.removeIf(data -> data.isClosing && data.lastTransactionSent < transactionId); } else { - activePistons.removeIf(PistonData::tickIfGuaranteedFinished); + activePistons.removeIf(PlayerPistonData::tickIfGuaranteedFinished); openShulkerBoxes.removeIf(ShulkerData::tickIfGuaranteedFinished); } // Remove if a shulker is not in this block position anymore @@ -698,4 +695,8 @@ public void setDimension(DimensionType dimension, User user) { public WrappedBlockState getBlock(Vector3dm aboveCCWPos) { return getBlock(aboveCCWPos.getX(), aboveCCWPos.getY(), aboveCCWPos.getZ()); } + + public void addPiston(PistonTemplate pistonTemplate, int transactionID) { + activePistons.add(new PlayerPistonData(pistonTemplate, transactionID)); + } } diff --git a/common/src/main/java/ac/grim/grimac/utils/latency/ILatencyUtils.java b/common/src/main/java/ac/grim/grimac/utils/latency/ILatencyUtils.java new file mode 100644 index 0000000000..329c7683a0 --- /dev/null +++ b/common/src/main/java/ac/grim/grimac/utils/latency/ILatencyUtils.java @@ -0,0 +1,28 @@ +package ac.grim.grimac.utils.latency; + +public interface ILatencyUtils { + /** + * Adds a task to be executed when the corresponding transaction ACK is received. + * + * @param transaction The transaction ID this task is associated with. + * @param runnable The task to execute. + */ + void addRealTimeTask(int transaction, Runnable runnable); + + /** + * Adds a task to be executed asynchronously via the player's event loop + * when the corresponding transaction ACK is received. + * (Note: Benchmark might simplify/ignore the async part unless specifically testing event loop contention) + * + * @param transaction The transaction ID this task is associated with. + * @param runnable The task to execute. + */ + void addRealTimeTaskAsync(int transaction, Runnable runnable); + + /** + * Processes received transaction ACKs and runs associated tasks. + * + * @param receivedTransactionId The ID of the transaction ACK received from the client. + */ + void handleNettySyncTransaction(int receivedTransactionId); +} diff --git a/common/src/main/java/ac/grim/grimac/utils/latency/LatencyUtils.java b/common/src/main/java/ac/grim/grimac/utils/latency/LatencyUtils.java index 20c7010d74..0e87aa751c 100644 --- a/common/src/main/java/ac/grim/grimac/utils/latency/LatencyUtils.java +++ b/common/src/main/java/ac/grim/grimac/utils/latency/LatencyUtils.java @@ -4,19 +4,18 @@ 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 com.github.retrooper.packetevents.netty.channel.ChannelHelper; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.ListIterator; +import java.util.*; -public class LatencyUtils { - private final LinkedList> transactionMap = new LinkedList<>(); - private final GrimPlayer player; +public class LatencyUtils implements ILatencyUtils { + + // Record to replace Pair with primitive int + private record TransactionTask(int transactionId, Runnable task) {} + + private final ArrayDeque transactionMap = new ArrayDeque<>(); - // Built from transactionMap and cleared at start of every handleNettySyncTransaction() call - // The actual usage scope of this variable's use is limited to within the synchronized block of handleNettySyncTransaction + private final GrimPlayer player; private final ArrayList tasksToRun = new ArrayList<>(); public LatencyUtils(GrimPlayer player) { @@ -24,78 +23,70 @@ public LatencyUtils(GrimPlayer player) { } public void addRealTimeTask(int transaction, Runnable runnable) { - addRealTimeTask(transaction, false, runnable); + addRealTimeTaskInternal(transaction, false, runnable); } public void addRealTimeTaskAsync(int transaction, Runnable runnable) { - addRealTimeTask(transaction, true, runnable); + addRealTimeTaskInternal(transaction, true, runnable); } - public void addRealTimeTask(int transaction, boolean async, Runnable runnable) { - if (player.lastTransactionReceived.get() >= transaction) { // If the player already responded to this transaction + private void addRealTimeTaskInternal(int transactionId, boolean async, Runnable runnable) { + if (player.lastTransactionReceived.get() >= transactionId) { if (async) { - ChannelHelper.runInEventLoop(player.user.getChannel(), runnable); // Run it sync to player channel + ChannelHelper.runInEventLoop(player.user.getChannel(), runnable); } else { runnable.run(); } return; } - synchronized (this) { - transactionMap.add(new Pair<>(transaction, runnable)); + synchronized (transactionMap) { + transactionMap.add(new TransactionTask(transactionId, runnable)); } } - public void handleNettySyncTransaction(int transaction) { - /* - * This code uses a two-pass approach within the synchronized block to prevent CMEs. - * First we collect and remove tasks using the iterator, then execute all collected tasks. - * - * The issue: - * We cannot execute tasks during iteration because if a runnable modifies transactionMap - * or calls addRealTimeTask, it will cause a ConcurrentModificationException. - * While only seen on Folia servers, this is theoretically possible everywhere. - * - * Why this solution: - * Rather than documenting "don't modify transactionMap in runnables" and risking subtle - * bugs from future contributions or Check API usage, we prevent the issue entirely - * at a small performance cost. - * - * Future considerations: - * If this becomes a performance bottleneck, we may revisit using a single-pass approach - * on non-Folia servers. We could also explore concurrent data structures or parallel - * execution, but this would lose the guarantee that transactions are processed in order. - */ - synchronized (this) { + @Override + public void handleNettySyncTransaction(int receivedTransactionId) { + synchronized (transactionMap) { tasksToRun.clear(); - // First pass: collect tasks and mark them for removal - ListIterator> iterator = transactionMap.listIterator(); + Iterator iterator = transactionMap.iterator(); while (iterator.hasNext()) { - Pair pair = iterator.next(); + TransactionTask taskEntry = iterator.next(); + int taskTransactionId = taskEntry.transactionId(); - // We are at most a tick ahead when running tasks based on transactions, meaning this is too far - if (transaction + 1 < pair.first()) + // If tasks are added with monotonically increasing IDs, + // once we find one that's too far ahead, all subsequent ones + // will also be too far ahead. + if (receivedTransactionId + 1 < taskTransactionId) { break; + } // This is at most tick ahead of what we want - if (transaction == pair.first() - 1) - continue; + if (receivedTransactionId == taskTransactionId - 1) { + continue; // Skip this specific task + } - tasksToRun.add(pair.second()); - iterator.remove(); + // If we didn't break or continue, the task is eligible + tasksToRun.add(taskEntry.task()); + iterator.remove(); // Remove using the iterator } + // Task execution loop for (Runnable runnable : tasksToRun) { try { runnable.run(); } catch (Exception e) { - LogUtil.error("An error has occurred when running transactions for player: " + player.user.getName(), e); - // Kick the player SO PEOPLE ACTUALLY REPORT PROBLEMS AND KNOW WHEN THEY HAPPEN - if (!Boolean.getBoolean("grim.disable-transaction-kick")) { - player.disconnect(MessageUtil.miniMessage(MessageUtil.replacePlaceholders(player, GrimAPI.INSTANCE.getConfigManager().getDisconnectPacketError()))); - } + handleRunnableError(e); } } } } + + private void handleRunnableError(Exception e) { + LogUtil.error("An error has occurred when running transactions for player: " + player.user.getName(), e); + // Kick the player SO PEOPLE ACTUALLY REPORT PROBLEMS AND KNOW WHEN THEY HAPPEN + if (!Boolean.getBoolean("grim.disable-transaction-kick")) { + player.disconnect(MessageUtil.miniMessage(MessageUtil.replacePlaceholders(player, GrimAPI.INSTANCE.getConfigManager().getDisconnectPacketError()))); + } + } } diff --git a/common/src/main/java/ac/grim/grimac/utils/nmsutil/WorldRayTrace.java b/common/src/main/java/ac/grim/grimac/utils/nmsutil/WorldRayTrace.java index ad8e6b1012..00270a6265 100644 --- a/common/src/main/java/ac/grim/grimac/utils/nmsutil/WorldRayTrace.java +++ b/common/src/main/java/ac/grim/grimac/utils/nmsutil/WorldRayTrace.java @@ -1,12 +1,18 @@ package ac.grim.grimac.utils.nmsutil; +import ac.grim.grimac.checks.impl.combat.Reach; import ac.grim.grimac.player.GrimPlayer; import ac.grim.grimac.utils.collisions.CollisionData; import ac.grim.grimac.utils.collisions.HitboxData; import ac.grim.grimac.utils.collisions.datatypes.CollisionBox; +import ac.grim.grimac.utils.collisions.datatypes.ComplexCollisionBox; +import ac.grim.grimac.utils.collisions.datatypes.NoCollisionBox; import ac.grim.grimac.utils.collisions.datatypes.SimpleCollisionBox; +import ac.grim.grimac.utils.data.BlockHitData; +import ac.grim.grimac.utils.data.EntityHitData; import ac.grim.grimac.utils.data.HitData; import ac.grim.grimac.utils.data.Pair; +import ac.grim.grimac.utils.data.packetentity.PacketEntity; import ac.grim.grimac.utils.math.GrimMath; import ac.grim.grimac.utils.math.Vector3dm; import com.github.retrooper.packetevents.protocol.attribute.Attributes; @@ -16,13 +22,15 @@ import com.github.retrooper.packetevents.protocol.world.states.type.StateType; import com.github.retrooper.packetevents.util.Vector3d; import com.github.retrooper.packetevents.util.Vector3i; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; import java.util.function.BiFunction; public class WorldRayTrace { - public static HitData getNearestBlockHitResult(GrimPlayer player, StateType heldItem, boolean sourcesHaveHitbox, boolean fluidPlacement, boolean itemUsePlacement) { + public static BlockHitData getNearestBlockHitResult(GrimPlayer player, StateType heldItem, boolean sourcesHaveHitbox, boolean fluidPlacement, boolean itemUsePlacement) { Vector3d startingPos = new Vector3d(player.x, player.y + player.getEyeHeight(), player.z); Vector3dm startingVec = new Vector3dm(startingPos.getX(), startingPos.getY(), startingPos.getZ()); Ray trace = new Ray(player, startingPos.getX(), startingPos.getY(), startingPos.getZ(), player.xRot, player.yRot); @@ -57,7 +65,7 @@ public static HitData getNearestBlockHitResult(GrimPlayer player, StateType held } } if (bestHitLoc != null) { - return new HitData(vector3i, bestHitLoc, bestFace, block); + return new BlockHitData(vector3i, bestHitLoc, bestFace, block); } if (sourcesHaveHitbox && @@ -70,7 +78,7 @@ public static HitData getNearestBlockHitResult(GrimPlayer player, StateType held Pair intercept = ReachUtils.calculateIntercept(box, trace.getOrigin(), trace.getPointAtDistance(distance)); if (intercept.first() != null) { - return new HitData(vector3i, intercept.first(), intercept.second(), block); + return new BlockHitData(vector3i, intercept.first(), intercept.second(), block); } } @@ -83,7 +91,7 @@ public static HitData getNearestBlockHitResult(GrimPlayer player, StateType held // // I do have to admit that I'm starting to like bifunctions/new java 8 things more than I originally did. // although I still don't understand Mojang's obsession with streams in some of the hottest methods... that kills performance - public static HitData traverseBlocks(GrimPlayer player, Vector3d start, Vector3d end, BiFunction predicate) { + public static BlockHitData traverseBlocks(GrimPlayer player, Vector3d start, Vector3d end, BiFunction predicate) { // I guess go back by the collision epsilon? double endX = GrimMath.lerp(-1.0E-7D, end.x, start.x); double endY = GrimMath.lerp(-1.0E-7D, end.y, start.y); @@ -99,7 +107,7 @@ public static HitData traverseBlocks(GrimPlayer player, Vector3d start, Vector3d if (start.equals(end)) return null; WrappedBlockState state = player.compensatedWorld.getBlock(floorStartX, floorStartY, floorStartZ); - HitData apply = predicate.apply(state, new Vector3i(floorStartX, floorStartY, floorStartZ)); + BlockHitData apply = predicate.apply(state, new Vector3i(floorStartX, floorStartY, floorStartZ)); if (apply != null) { return apply; @@ -148,4 +156,162 @@ public static HitData traverseBlocks(GrimPlayer player, Vector3d start, Vector3d return null; } + + @Nullable + public static HitData getNearestHitResult(GrimPlayer player, PacketEntity targetEntity, Vector3dm eyePos, Vector3dm lookVec) { + + double maxAttackDistance = player.compensatedEntities.self.getAttributeValue(Attributes.ENTITY_INTERACTION_RANGE); + double maxBlockDistance = player.compensatedEntities.self.getAttributeValue(Attributes.BLOCK_INTERACTION_RANGE); + + Vector3d startingPos = new Vector3d(eyePos.getX(), eyePos.getY(), eyePos.getZ()); + Vector3dm startingVec = new Vector3dm(startingPos.getX(), startingPos.getY(), startingPos.getZ()); + Ray trace = new Ray(eyePos, lookVec); + Vector3dm endVec = trace.getPointAtDistance(maxBlockDistance); + Vector3d endPos = new Vector3d(endVec.getX(), endVec.getY(), endVec.getZ()); + + // Get block hit + BlockHitData blockHitData = getTraverseResult(player, null, startingPos, startingVec, trace, endPos, false, true, maxBlockDistance, true); + Vector3dm closestHitVec = null; + PacketEntity closestEntity = null; + double closestDistanceSquared = blockHitData != null ? blockHitData.getBlockHitLocation().distanceSquared(startingVec) : maxAttackDistance * maxAttackDistance; + + for (PacketEntity entity : player.compensatedEntities.entityMap.values().stream().filter(PacketEntity::canHit).toList()) { + SimpleCollisionBox box = null; + // 1.7 and 1.8 players get a bit of extra hitbox (this is why you should use 1.8 on cross version servers) + // Yes, this is vanilla and not uncertainty. All reach checks have this or they are wrong. + + if (entity.equals(targetEntity)) { + box = entity.getPossibleCollisionBoxes(); + box.expand(player.checkManager.getPacketCheck(Reach.class).threshold); + // This is better than adding to the reach, as 0.03 can cause a player to miss their target + // Adds some more than 0.03 uncertainty in some cases, but a good trade off for simplicity + // + // Just give the uncertainty on 1.9+ clients as we have no way of knowing whether they had 0.03 movement + if (!player.packetStateData.didLastLastMovementIncludePosition || player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9)) + box.expand(player.getMovementThreshold()); + if (ReachUtils.isVecInside(box, eyePos)) { + return new EntityHitData(entity, eyePos); + } + } else { + CollisionBox b = entity.getMinimumPossibleCollisionBoxes(); + if (b instanceof NoCollisionBox) { + continue; + } + box = (SimpleCollisionBox) b; + box.expand(-player.checkManager.getPacketCheck(Reach.class).threshold); + // todo, shrink by reachThreshold as well for non-target entities? + if (!player.packetStateData.didLastLastMovementIncludePosition || player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_9)) + box.expand(-player.getMovementThreshold()); + } + if (player.getClientVersion().isOlderThan(ClientVersion.V_1_9)) { + box.expand(0.1f); + } + + + Pair intercept = ReachUtils.calculateIntercept(box, trace.getOrigin(), trace.getPointAtDistance(Math.sqrt(closestDistanceSquared))); + + if (intercept.first() != null) { + double distSquared = intercept.first().distanceSquared(startingVec); + if (distSquared < closestDistanceSquared) { + closestDistanceSquared = distSquared; + closestHitVec = intercept.first(); + closestEntity = entity; + } + } + } + + return closestEntity == null ? blockHitData : new EntityHitData(closestEntity, closestHitVec); + } + + // Checks if it was possible to hit a target entity + // TODO refactor to return list of rays and why each of them didn't hit instead of closest obstruction + // NOTE: It should be impossible for the returned Pair to be null + // because all of the possibleLookVecsAndEyeHeights passed in should be ones that hit the target entity + // in previous parts of this check when we didn't check for any obstructions like blocks/entities + public static @NotNull Pair<@NotNull Double, @NotNull HitData> didRayTraceHit(GrimPlayer player, PacketEntity targetEntity, + List> possibleLookVecsAndEyeHeights, + Vector3d from) { + HitData firstObstruction = null; + double firstObstructionDistanceSq = 0; + + // Check every possible look direction and every possible eye height + for (Pair vectorDoublePair : possibleLookVecsAndEyeHeights) { + Vector3dm lookVec = vectorDoublePair.first(); + double eye = vectorDoublePair.second(); + + Vector3dm eyes = new Vector3dm(from.getX(), from.getY() + eye, from.getZ()); + // this function is completely 0.03 aware + final HitData hitResult = WorldRayTrace.getNearestHitResult(player, targetEntity, eyes, lookVec); + + // If we hit the target entity, it's a valid hit + if (hitResult instanceof EntityHitData && ((EntityHitData) hitResult).getEntity().equals(targetEntity)) { + double distanceSquared = eyes.distanceSquared(hitResult.getBlockHitLocation()); + return new Pair<>(distanceSquared, hitResult); // Legitimate hit + } else if (hitResult != null && firstObstruction == null) { + // Store the first obstruction only + firstObstruction = hitResult; + firstObstructionDistanceSq = eyes.distanceSquared(hitResult.getBlockHitLocation()); + } + } + + // Return the first obstruction if no valid hit found + // Since we sort eye heights by likeniness, we should in effect return the most likely (first) obstruction + assert firstObstruction != null; + return new Pair<>(firstObstructionDistanceSq, firstObstruction); + } + + // TODO replace shrinkBlocks boolean with a data structure/better way to represent + // 1. We have a target block. Shrink everything by movementThreshold except expand target block (we are checking to see if it matches the target block) + // 2. We do not have a target block. Shrink everything by movementThreshold() + // 3. Do not expand or shrink everything, we do not expect 0.03/0.002 or we legacy example where we want to keep old behaviour + private static BlockHitData getTraverseResult(GrimPlayer player, @Nullable StateType heldItem, Vector3d startingPos, Vector3dm startingVec, Ray trace, Vector3d endPos, boolean sourcesHaveHitbox, boolean checkInside, double knownDistance, boolean shrinkBlocks) { + return traverseBlocks(player, startingPos, endPos, (block, vector3i) -> { + // even though sometimes we are raytracing against a block that is the target block, we pass false to this function because it only applies a change for brewing stands in 1.8 + CollisionBox data = HitboxData.getBlockHitbox(player, heldItem, player.getClientVersion(), block, false, vector3i.getX(), vector3i.getY(), vector3i.getZ()); + SimpleCollisionBox[] boxes = new SimpleCollisionBox[ComplexCollisionBox.DEFAULT_MAX_COLLISION_BOX_SIZE]; + int size = data.downCast(boxes); + + double bestHitResult = Double.MAX_VALUE; + Vector3dm bestHitLoc = null; + BlockFace bestFace = null; + + for (int i = 0; i < size; i++) { + if (shrinkBlocks) boxes[i].expand(-player.getMovementThreshold()); + Pair intercept = ReachUtils.calculateIntercept(boxes[i], trace.getOrigin(), trace.getPointAtDistance(knownDistance)); + if (intercept.first() == null) continue; // No intercept + + Vector3dm hitLoc = intercept.first(); + + // If inside a block, return empty result for reach check (don't bother checking this?) + if (checkInside && ReachUtils.isVecInside(boxes[i], trace.getOrigin())) { + return null; + } + + if (hitLoc.distanceSquared(startingVec) < bestHitResult) { + bestHitResult = hitLoc.distanceSquared(startingVec); + bestHitLoc = hitLoc; + bestFace = intercept.second(); + } + } + + if (bestHitLoc != null) { + return new BlockHitData(vector3i, bestHitLoc, bestFace, block); + } + + if (sourcesHaveHitbox && + (player.compensatedWorld.isWaterSourceBlock(vector3i.getX(), vector3i.getY(), vector3i.getZ()) + || player.compensatedWorld.getLavaFluidLevelAt(vector3i.getX(), vector3i.getY(), vector3i.getZ()) == (8 / 9f))) { + double waterHeight = player.compensatedWorld.getFluidLevelAt(vector3i.getX(), vector3i.getY(), vector3i.getZ()); + SimpleCollisionBox box = new SimpleCollisionBox(vector3i.getX(), vector3i.getY(), vector3i.getZ(), vector3i.getX() + 1, vector3i.getY() + waterHeight, vector3i.getZ() + 1); + + Pair intercept = ReachUtils.calculateIntercept(box, trace.getOrigin(), trace.getPointAtDistance(knownDistance)); + + if (intercept.first() != null) { + return new BlockHitData(vector3i, intercept.first(), intercept.second(), block); + } + } + + return null; + }); + } } diff --git a/common/src/main/resources/config/de.yml b/common/src/main/resources/config/de.yml index b0e27ce116..34f1f9d6c8 100644 --- a/common/src/main/resources/config/de.yml +++ b/common/src/main/resources/config/de.yml @@ -66,6 +66,14 @@ Simulation: # Dies soll verhindern, dass der Spieler zu viele Verstöße sammelt und nie in der Lage ist, sie alle zu beseitigen. # Standard-Vorteilsgrenze (x-Achse = Sekunden, y-Achse = 1/1000 Block): https://www.desmos.com/calculator/4lovswdarj max-ceiling: 4 + # Wie stark sollen wir den VL-Gewinn anhand des Vorteils des Spielers skaliieren? + # ≤1 für keine Skalierung (altes Verhalten) + vl-scale: 1 + # Verzögerung in Millisekunden zwischen Flags für die VL-Skalierung + vl-scale-delay: 0 + # Beschränkt die Menge an VL, die pro Flag hinzugefügt werden kann + # -1 deaktiviert die Begrenzung + max-vls-per-flag: -1 # Schwellenwert für die Verletzungsstufe für den Rückschlag # 1 für das alte Verhalten setback-violation-threshold: 1 @@ -182,6 +190,8 @@ debug-pipeline-on-join: false # Aktiviert experimentelle Prüfungen experimental-checks: false +# Setzt die Ausführungsanzahl einer Bestrafung zurück, wenn der VL unter den Schwellenwert fällt +reset-punishment-execute-count: true reset-item-usage-on-item-update: true reset-item-usage-on-attack: true @@ -202,4 +212,20 @@ max-ping-out-of-flying: 1000 # Dies verhindert, dass Spieler mit hoher Latenz einen einzigen Feuerwerks‑Boost mit Elytra unbegrenzt nutzen können. max-ping-firework-boost: 1000 +history: + enabled: true + # Wie viele Einträge sollen pro Seite mit /grim history angezeigt werden + entries-per-page: 15 + # Welcher Servername soll für den History‑Befehl eingetragen werden? Nützlich, wenn dieselbe Datenbank für mehrere Server verwendet wird + server-name: Prison + database: + # SQLITE für lokale Speicherung, MYSQL für eine externe MySQL‑Datenbank. Wird nur nach Serverneustart aktualisiert + type: SQLITE + # MySQL‑Verbindungsdetails + host: localhost + port: 3306 + database: grim + username: root + password: "" + config-version: 9 diff --git a/common/src/main/resources/config/en.yml b/common/src/main/resources/config/en.yml index 6daa065ab7..fb16a5dfbe 100644 --- a/common/src/main/resources/config/en.yml +++ b/common/src/main/resources/config/en.yml @@ -66,6 +66,15 @@ Simulation: # This is to stop the player from gathering too many violations and never being able to clear them all # Default advantage ceiling (x axis = seconds, y axis = 1/1000 block): https://www.desmos.com/calculator/4lovswdarj max-ceiling: 4 + # How much should we scale VL gain from a flag by the player's advantage? + # ≤1 for no scaling (old behavior) + vl-scale: 1 + # Delay in milliseconds between flags for VL scaling + vl-scale-delay: 0 + # Limits the amount of VLs the player could gain from each flag + # This is to prevent high latency players from getting large amount of VLs because of lag spikes + # -1 to disable + max-vls-per-flag: -1 # Violation level threshold for setback # 1 for old behavior setback-violation-threshold: 1 @@ -182,6 +191,8 @@ debug-pipeline-on-join: false # Enables experimental checks experimental-checks: false +# Resets how many times a punishment command has executed when the player's VL drops below its threshold +reset-punishment-execute-count: true reset-item-usage-on-item-update: true reset-item-usage-on-attack: true @@ -202,4 +213,20 @@ max-ping-out-of-flying: 1000 # This prevents high latency players from being able to use 1 firework boost with an elytra forever. max-ping-firework-boost: 1000 +history: + enabled: true + # How many entries should be shown for each page with /grim history + entries-per-page: 15 + # What should the inserted server name be for the history command? This is useful if you use the same database for multiple servers + server-name: Prison + database: + # Use SQLITE for local storage, use MYSQL if you have an external MySQL database. This is only updated on server restart + type: SQLITE + # MySQL connection details + host: localhost + port: 3306 + database: grim + username: root + password: "" + config-version: 9 diff --git a/common/src/main/resources/config/es.yml b/common/src/main/resources/config/es.yml index c33facb88f..00cd9c1b2c 100644 --- a/common/src/main/resources/config/es.yml +++ b/common/src/main/resources/config/es.yml @@ -67,6 +67,14 @@ Simulation: # Esto es para prevenir que el jugador obtenga muchas violaciones y no pueda ser capaz de borrarlas # Tope de ventaja por defecto (eje x = segundos, eje y = bloque 1/1000): https://www.desmos.com/calculator/4lovswdarj max-ceiling: 4 + # ¿Cuánto deberíamos escalar la ganancia de VL por una bandera según la ventaja del jugador? + # ≤1 para no escalar (comportamiento antiguo) + vl-scale: 1 + # Retraso en milisegundos entre flags para el escalado de VL + vl-scale-delay: 0 + # Limita la cantidad de VL que el jugador puede ganar por cada bandera + # -1 para desactivar + max-vls-per-flag: -1 # Umbral del nivel de violación para el retroceso # 1 para el comportamiento antiguo setback-violation-threshold: 1 @@ -183,6 +191,8 @@ debug-pipeline-on-join: false # Habilitar comprobaciones experimentales experimental-checks: false +# Restablece el conteo de ejecuciones del castigo cuando el VL del jugador baja del umbral +reset-punishment-execute-count: true reset-item-usage-on-item-update: true reset-item-usage-on-attack: true @@ -204,4 +214,20 @@ max-ping-out-of-flying: 1000 # Esto evita que los jugadores con alta latencia puedan usar un solo impulso con fuegos artificiales y una elytra para siempre. max-ping-firework-boost: 1000 +history: + enabled: true + # Cuántas entradas se mostrarán por página con /grim history + entries-per-page: 15 + # ¿Qué nombre de servidor debe insertarse para el comando history? Útil si compartes la misma base de datos entre varios servidores + server-name: Prison + database: + # Usa SQLITE para almacenamiento local o MYSQL si tienes una base de datos MySQL externa. Solo se actualiza al reiniciar el servidor + type: SQLITE + # Detalles de conexión MySQL + host: localhost + port: 3306 + database: grim + username: root + password: "" + config-version: 9 diff --git a/common/src/main/resources/config/fr.yml b/common/src/main/resources/config/fr.yml index e9a8aa1156..0a7d499d5d 100644 --- a/common/src/main/resources/config/fr.yml +++ b/common/src/main/resources/config/fr.yml @@ -66,6 +66,14 @@ Simulation: # Cela vise à empêcher le joueur d'accumuler trop de violations et de ne jamais pouvoir toutes les réinitialiser. # Plafond d'avantage par défaut (l"axe x = secondes, l'axe y = 1/1000 de bloc)) : https://www.desmos.com/calculator/4lovswdarj max-ceiling: 4 + # Dans quelle mesure doit-on augmenter les VL en fonction de l'avantage du joueur ? + # ≤1 pour conserver l'ancien comportement sans échelle + vl-scale: 1 + # Délai en millisecondes entre les flags pour la mise à l'échelle du VL + vl-scale-delay: 0 + # Limite la quantité de VL que le joueur peut gagner par alerte + # -1 pour désactiver + max-vls-per-flag: -1 # Seuil du niveau de violation pour le setback # 1 pour le comportement ancien setback-violation-threshold: 1 @@ -178,6 +186,8 @@ debug-pipeline-on-join: false # Active les vérifications expérimentales experimental-checks: false +# Réinitialise le nombre d'exécutions de la sanction lorsque le VL du joueur repasse sous le seuil +reset-punishment-execute-count: true reset-item-usage-on-item-update: true reset-item-usage-on-attack: true @@ -199,4 +209,20 @@ max-ping-out-of-flying: 1000 # Cela empêche les joueurs à forte latence d’utiliser indéfiniment un seul boost de feu d’artifice avec une élytra. max-ping-firework-boost: 1000 +history: + enabled: true + # Combien d’entrées doivent être affichées par page avec /grim history + entries-per-page: 15 + # Quel nom de serveur doit être inséré pour la commande history ? Utile si vous utilisez la même base de données pour plusieurs serveurs + server-name: Prison + database: + # Utilisez SQLITE pour un stockage local, MYSQL si vous disposez d’une base MySQL externe. Mis à jour seulement au redémarrage du serveur + type: SQLITE + # Détails de connexion MySQL + host: localhost + port: 3306 + database: grim + username: root + password: "" + config-version: 9 diff --git a/common/src/main/resources/config/it.yml b/common/src/main/resources/config/it.yml index 6c9f474f7d..e3ab3a92f4 100644 --- a/common/src/main/resources/config/it.yml +++ b/common/src/main/resources/config/it.yml @@ -58,6 +58,14 @@ Simulation: max-advantage: 1 # Limite massimo di vantaggio accumulabile prima di arretrare il giocatore max-ceiling: 4 + # Di quanto dobbiamo scalare l'aumento di VL di un flag in base al vantaggio del giocatore? + # ≤1 per mantenere il vecchio comportamento + vl-scale: 1 + # Ritardo in millisecondi tra le flag per la scalatura VL + vl-scale-delay: 0 + # Limita quante VL si possono ottenere per ogni flag + # -1 per disattivare + max-vls-per-flag: -1 # Soglia del livello di violazione per il setback # 1 per il comportamento precedente setback-violation-threshold: 1 @@ -160,6 +168,8 @@ debug-pipeline-on-join: false # Enables experimental checks experimental-checks: false +# Reimposta il conteggio di esecuzione della punizione quando il VL del giocatore scende sotto la soglia +reset-punishment-execute-count: true reset-item-usage-on-item-update: true reset-item-usage-on-attack: true @@ -180,4 +190,20 @@ max-ping-out-of-flying: 1000 # Impedisce ai giocatori con alta latenza di usare all’infinito un solo boost con fuochi d’artificio ed elytra. max-ping-firework-boost: 1000 +history: + enabled: true + # Quante voci devono essere mostrate per pagina con /grim history + entries-per-page: 15 + # Quale nome server deve essere inserito per il comando history? Utile se usi lo stesso database per più server + server-name: Prison + database: + # Usa SQLITE per l’archiviazione locale, MYSQL se hai un database MySQL esterno. Aggiornato solo al riavvio del server + type: SQLITE + # Dettagli di connessione MySQL + host: localhost + port: 3306 + database: grim + username: root + password: "" + config-version: 9 diff --git a/common/src/main/resources/config/ja.yml b/common/src/main/resources/config/ja.yml index 5a9cd74ff9..85458d04bc 100644 --- a/common/src/main/resources/config/ja.yml +++ b/common/src/main/resources/config/ja.yml @@ -68,6 +68,14 @@ Simulation: # これは、プレイヤーが違反を蓄積しすぎて、違反を解消できなくなるのを防ぐためです。 # デフォルトのアドバンテージの上限 (x軸 = 秒, y軸 = 1/1000ブロック): https://www.desmos.com/calculator/4lovswdarj max-ceiling: 4 + # プレイヤーのアドバンテージに応じてVL増加量をどれだけスケールさせるか + # 1以下で従来の挙動になります + vl-scale: 1 + # VLスケーリングのフラグ間の遅延(ミリ秒) + vl-scale-delay: 0 + # 1回のフラグで増加できるVLの上限 + # -1で無制限 + max-vls-per-flag: -1 # セットバックの違反レベル閾値 # 旧仕様の挙動は 1 です setback-violation-threshold: 1 @@ -191,6 +199,8 @@ debug-pipeline-on-join: false # 実験的なチェックを有効にします experimental-checks: false +# プレイヤーのVLがしきい値を下回ったときに制裁実行回数をリセット +reset-punishment-execute-count: true reset-item-usage-on-item-update: true reset-item-usage-on-attack: true @@ -211,4 +221,20 @@ max-ping-out-of-flying: 1000 # これにより、レイテンシの高いプレイヤーが1つのロケット花火による加速でエリトラを永久に使用するのを防ぎます。 max-ping-firework-boost: 1000 +history: + enabled: true + # /grim history で 1 ページに表示するエントリー数 + entries-per-page: 15 + # history コマンドで挿入されるサーバー名は? 複数サーバーで同じデータベースを使用する場合に便利 + server-name: Prison + database: + # ローカル保存には SQLITE、外部 MySQL データベースには MYSQL を使用します。サーバー再起動時にのみ反映 + type: SQLITE + # MySQL 接続情報 + host: localhost + port: 3306 + database: grim + username: root + password: "" + config-version: 9 diff --git a/common/src/main/resources/config/nl.yml b/common/src/main/resources/config/nl.yml index 8aab2b96ba..a0a2c6cf6b 100644 --- a/common/src/main/resources/config/nl.yml +++ b/common/src/main/resources/config/nl.yml @@ -66,6 +66,14 @@ Simulation: # Dit is om te voorkomen dat de speler te veel schendingen verzamelt en ze nooit allemaal kan opruimen # Standaard voordelenplatform (x-as = seconden, y-as = 1/1000 blok): https://www.desmos.com/calculator/4lovswdarj max-ceiling: 4 + # Hoeveel moeten we de VL-verhoging van een flag schalen op basis van het voordeel van de speler? + # ≤1 voor geen schaal (oud gedrag) + vl-scale: 1 + # Vertraging in milliseconden tussen vlaggen voor VL-schaal + vl-scale-delay: 0 + # Beperkt hoeveel VL een speler per flag kan krijgen + # -1 om uit te schakelen + max-vls-per-flag: -1 # Drempelwaarde voor het schendingenniveau voor de terugslag # 1 voor het oude gedrag setback-violation-threshold: 1 @@ -182,6 +190,8 @@ debug-pipeline-on-join: false # Experimentele controles inschakelen experimental-checks: false +# Reset het aantal keren dat een straf is uitgevoerd wanneer de VL onder de drempel zakt +reset-punishment-execute-count: true reset-item-usage-on-item-update: true reset-item-usage-on-attack: true @@ -202,4 +212,20 @@ max-ping-out-of-flying: 1000 # Dit voorkomt dat spelers met hoge latency oneindig één vuurwerkboost met een elytra kunnen gebruiken. max-ping-firework-boost: 1000 +history: + enabled: true + # Hoeveel items worden per pagina weergegeven met /grim history + entries-per-page: 15 + # Welke servernaam moet voor het history‑commando worden ingevoerd? Handig als je dezelfde database voor meerdere servers gebruikt + server-name: Prison + database: + # Gebruik SQLITE voor lokale opslag, MYSQL voor een externe MySQL‑database. Alleen bijgewerkt na een serverherstart + type: SQLITE + # MySQL‑verbindingsgegevens + host: localhost + port: 3306 + database: grim + username: root + password: "" + config-version: 9 diff --git a/common/src/main/resources/config/pt.yml b/common/src/main/resources/config/pt.yml index 1241ece763..423390ef26 100644 --- a/common/src/main/resources/config/pt.yml +++ b/common/src/main/resources/config/pt.yml @@ -67,6 +67,14 @@ Simulation: # Isso serve para previnir o jogador de alcançar violações demais e nunca conseguir se livrar delas. # Cela de vantagens padrão (eixo X = segundos, eixo Y = 1/1000 blocos): https://www.desmos.com/calculator/4lovswdarj max-ceiling: 4 + # Quanto devemos escalar o ganho de VL de um flag com base na vantagem do jogador? + # ≤1 para manter o comportamento antigo + vl-scale: 1 + # Atraso em milissegundos entre flags para o escalonamento de VL + vl-scale-delay: 0 + # Limita a quantidade de VL que o jogador pode ganhar por flag + # -1 para desativar + max-vls-per-flag: -1 # Quantas violações para começar a recuar o jogador? # 1 para comportamento antigo setback-violation-threshold: 1 @@ -187,6 +195,8 @@ debug-pipeline-on-join: false # Habilita verificações experimentais. experimental-checks: false +# Reinicia a contagem de execuções da punição quando o VL do jogador fica abaixo do limite +reset-punishment-execute-count: true reset-item-usage-on-item-update: true reset-item-usage-on-attack: true @@ -207,4 +217,20 @@ max-ping-out-of-flying: 1000 # Previne jogadores com ping alto de usarem um foguete ara voar indefinidamente. max-ping-firework-boost: 1000 +history: + enabled: true + # Quantas entradas devem ser mostradas por página com /grim history + entries-per-page: 15 + # Qual nome de servidor deve ser inserido no comando history? Útil se você usa o mesmo banco de dados para vários servidores + server-name: Prison + database: + # Use SQLITE para armazenamento local ou MYSQL para um banco MySQL externo. Atualizado somente ao reiniciar o servidor + type: SQLITE + # Detalhes de conexão MySQL + host: localhost + port: 3306 + database: grim + username: root + password: "" + config-version: 9 diff --git a/common/src/main/resources/config/ru.yml b/common/src/main/resources/config/ru.yml index 651a212134..4eb44941eb 100644 --- a/common/src/main/resources/config/ru.yml +++ b/common/src/main/resources/config/ru.yml @@ -66,6 +66,14 @@ Simulation: # Это сделано для того, чтобы игрок не собирал слишком много нарушений и никогда не смог очистить их все. # Потолок преимущества по умолчанию (ось x = секунды, ось y = 1/1000 блока): https://www.desmos.com/calculator/4lovswdarj max-ceiling: 4 + # Насколько увеличивать получение VL в зависимости от преимущества игрока? + # ≤1 — оставить старое поведение + vl-scale: 1 + # Задержка в миллисекундах между флагами для масштабирования VL + vl-scale-delay: 0 + # Ограничивает количество VL, которое можно получить за один флаг + # -1 для отключения + max-vls-per-flag: -1 # Порог уровня нарушения для отката # 1 для старого поведения setback-violation-threshold: 1 @@ -178,6 +186,8 @@ debug-pipeline-on-join: false # Включает экспериментальные проверки experimental-checks: false +# Сбрасывать количество выполнения наказания, когда VL игрока опускается ниже порога +reset-punishment-execute-count: true reset-item-usage-on-item-update: true reset-item-usage-on-attack: true @@ -198,4 +208,20 @@ max-ping-out-of-flying: 1000 # Это предотвращает возможность для игроков с большим пингом бесконечно использовать один фейерверк‑буст с элитрой. max-ping-firework-boost: 1000 +history: + enabled: true + # Сколько записей показывать на странице команды /grim history + entries-per-page: 15 + # Какое имя сервера вставлять для команды history? Полезно при использовании одной базы для нескольких серверов + server-name: Prison + database: + # Используйте SQLITE для локального хранения или MYSQL для внешней MySQL‑БД. Изменения применяются после перезапуска сервера + type: SQLITE + # Параметры подключения MySQL + host: localhost + port: 3306 + database: grim + username: root + password: "" + config-version: 9 diff --git a/common/src/main/resources/config/tr.yml b/common/src/main/resources/config/tr.yml index 851630142e..8901fab08c 100644 --- a/common/src/main/resources/config/tr.yml +++ b/common/src/main/resources/config/tr.yml @@ -66,6 +66,14 @@ Simulation: # Bu, oyuncunun çok fazla ihlal biriktirmesini ve bunların hepsini temizleyememesini önlemek içindir # Varsayılan avantaj tavanı (x ekseni = saniye, y ekseni = 1/1000 blok): https://www.desmos.com/calculator/4lovswdarj max-ceiling: 4 + # Oyuncunun avantajına bağlı olarak VL artışını ne kadar ölçekleyelim? + # ≤1 eski davranış için + vl-scale: 1 + # VL ölçeklendirme için bayraklar arasındaki gecikme (ms) + vl-scale-delay: 0 + # Her bir flagden alınabilecek VL miktarını sınırlar + # -1 devre dışı bırakır + max-vls-per-flag: -1 # Geri alma için ihlal seviyesi eşiği # Eski davranış için 1 setback-violation-threshold: 1 @@ -182,6 +190,8 @@ debug-pipeline-on-join: false # Deneysel kontrolleri etkinleştir experimental-checks: false +# Oyuncunun VL degeri esik altina dustugunde cezanin yurutulme sayisini sifirlar +reset-punishment-execute-count: true reset-item-usage-on-item-update: true reset-item-usage-on-attack: true @@ -202,4 +212,20 @@ max-ping-out-of-flying: 1000 # Bu, yüksek gecikmeli oyuncuların bir havai fişek ivmesini elytra ile sonsuza kadar kullanmalarını engeller. max-ping-firework-boost: 1000 +history: + enabled: true + # /grim history ile her sayfada kaç kayıt gösterilsin + entries-per-page: 15 + # History komutu için eklenecek sunucu adı nedir? Aynı veritabanını birden fazla sunucuda kullanıyorsanız faydalıdır + server-name: Prison + database: + # Yerel saklama için SQLITE, harici MySQL veritabanı için MYSQL kullanın. Sadece sunucu yeniden başlatıldığında güncellenir + type: SQLITE + # MySQL bağlantı bilgileri + host: localhost + port: 3306 + database: grim + username: root + password: "" + config-version: 9 diff --git a/common/src/main/resources/config/zh.yml b/common/src/main/resources/config/zh.yml index 0a056dd186..7cdd42be7d 100644 --- a/common/src/main/resources/config/zh.yml +++ b/common/src/main/resources/config/zh.yml @@ -71,6 +71,14 @@ Simulation: # 这是为了防止玩家收集过多的违规行为,并且永远无法清除所有的违规行为 # 这是默认配置的样子(x 轴 = seconds ,y 轴 = 1/1000 方块): https://www.desmos.com/calculator/4lovswdarj max-ceiling: 4 + # 根据玩家的优势对VL增量进行缩放 + # ≤1 表示与以前相同 + vl-scale: 1 + # VL缩放标记之间的延迟(毫秒) + vl-scale-delay: 0 + # 限制每次触发可增加的VL数量 + # -1 表示不限制 + max-vls-per-flag: -1 # 设置回退的违规等级阈值 # 1 为旧行为 setback-violation-threshold: 1 @@ -184,6 +192,8 @@ debug-pipeline-on-join: false # 启用实验性检查 experimental-checks: false +# 当玩家的VL低于阈值时重置头期执行次数 +reset-punishment-execute-count: true # 取消有问题的格挡 reset-item-usage-on-item-update: true @@ -205,5 +215,21 @@ max-ping-out-of-flying: 1000 # 填写-1则关闭 max-ping-firework-boost: 1000 +history: + enabled: true + # 使用 /grim history 时每页显示多少条记录 + entries-per-page: 15 + # history 指令中插入的服务器名称?如果多个服务器共用同一数据库则很有用 + server-name: Prison + database: + # 本地存储使用 SQLITE,外部数据库使用 MYSQL。仅在重启服务器后生效 + type: SQLITE + # MySQL 连接详情 + host: localhost + port: 3306 + database: grim + username: root + password: "" + # 不要调整这个 config-version: 9 diff --git a/common/src/main/resources/messages/de.yml b/common/src/main/resources/messages/de.yml index 1a1e23f6f2..745d640f5c 100644 --- a/common/src/main/resources/messages/de.yml +++ b/common/src/main/resources/messages/de.yml @@ -60,4 +60,11 @@ help: - "/grim spectate &f- &7Beobachte einen Spieler" - "/grim verbose &f- &7Zeigt dir jeden Verstoß ohne Puffer an" - "/grim log [0-255] &f- &7Lädt ein Debug-Log für Vorhersagefehler hoch" + - "/grim history [Seite] &f- &7Zeigt frühere Warnungen für den Spieler" - "&7======================" + +grim-history-disabled: "%prefix% &cDas History‑Subsystem ist deaktiviert!" +# Valid placeholders: %prefix% %player% %page% %maxPages% +grim-history-header: "%prefix% &bZeige Logs für &f%player% (&f%page%&b/&f%maxPages%&f)" +# Valid placeholders: %prefix% %check% %vl% %verbose% %timeago% %server% +grim-history-entry: "%prefix% &8[&f%server%&8] &bFehlgeschlagen &f%check% (x&c%vl%&f) &7%verbose% (&bvor %timeago%&7)" diff --git a/common/src/main/resources/messages/en.yml b/common/src/main/resources/messages/en.yml index e2223f58f2..3af1ad2ed7 100644 --- a/common/src/main/resources/messages/en.yml +++ b/common/src/main/resources/messages/en.yml @@ -61,4 +61,11 @@ help: - "/grim spectate &f- &7Spectate a player" - "/grim verbose &f- &7Shows every flag to you, without buffers" - "/grim log [0-255] &f- &7Uploads a debug log for prediction flags" + - "/grim history [page] &f- &7Shows previous alerts for the player" - "&7======================" + +grim-history-disabled: "%prefix% &cHistory subsystem is disabled!" +# Valid placeholders: %prefix% %player% %page% %maxPages% +grim-history-header: "%prefix% &bShowing logs for &f%player% (&f%page%&b/&f%maxPages%&f)" +# Valid placeholders: %prefix% %check% %vl% %verbose% %timeago% %server% +grim-history-entry: "%prefix% &8[&f%server%&8] &bFailed &f%check% (x&c%vl%&f) &7%verbose% (&b%timeago% ago&7)" diff --git a/common/src/main/resources/messages/es.yml b/common/src/main/resources/messages/es.yml index b0799c9416..9863765dd1 100644 --- a/common/src/main/resources/messages/es.yml +++ b/common/src/main/resources/messages/es.yml @@ -61,4 +61,11 @@ help: - "/grim spectate &f- &7Espectar a un jugador" - "/grim verbose &f- &7Te muestra todo aviso, sin buffers" - "/grim log [0-255] &f- &7Sube un registro de depuración para avisos de predicciones" + - "/grim history [página] &f- &7Muestra alertas previas del jugador" - "&7======================" + +grim-history-disabled: "%prefix% &c¡El subsistema de historial está deshabilitado!" +# Valid placeholders: %prefix% %player% %page% %maxPages% +grim-history-header: "%prefix% &bMostrando registros de &f%player% (&f%page%&b/&f%maxPages%&f)" +# Valid placeholders: %prefix% %check% %vl% %verbose% %timeago% %server% +grim-history-entry: "%prefix% &8[&f%server%&8] &bFallido &f%check% (x&c%vl%&f) &7%verbose% (&bhace %timeago%&7)" diff --git a/common/src/main/resources/messages/fr.yml b/common/src/main/resources/messages/fr.yml index 6947e7578f..c67eac53cf 100644 --- a/common/src/main/resources/messages/fr.yml +++ b/common/src/main/resources/messages/fr.yml @@ -61,4 +61,11 @@ help: - "/grim spectate &f- &7Regarder un joueur" - "/grim verbose &f- &7Affiche chaqu'une de vos violations, sans tampons" - "/grim log [0-255] &f- &7Téléverse un journal de débogage pour les indicateurs de prédiction" + - "/grim history [page] &f- &7Affiche les alertes précédentes du joueur" - "&7======================" + +grim-history-disabled: "%prefix% &cLe sous‑système d’historique est désactivé !" +# Valid placeholders: %prefix% %player% %page% %maxPages% +grim-history-header: "%prefix% &bAffichage des journaux pour &f%player% (&f%page%&b/&f%maxPages%&f)" +# Valid placeholders: %prefix% %check% %vl% %verbose% %timeago% %server% +grim-history-entry: "%prefix% &8[&f%server%&8] &bÉchec &f%check% (x&c%vl%&f) &7%verbose% (&bil y a %timeago%&7)" diff --git a/common/src/main/resources/messages/it.yml b/common/src/main/resources/messages/it.yml index f2bbb0cfad..208b662cf1 100644 --- a/common/src/main/resources/messages/it.yml +++ b/common/src/main/resources/messages/it.yml @@ -53,4 +53,11 @@ help: - "/grim spectate &f- &7Osserva un giocatore" - "/grim verbose &f- &7Mostra ogni segnalazione a te, senza buffer" - "/grim log [0-255] &f- &7Carica un registro di debug per le segnalazioni di previsione" + - "/grim history [pagina] &f- &7Mostra gli avvisi precedenti del giocatore" - "&7======================" + +grim-history-disabled: "%prefix% &cIl sottosistema cronologia è disabilitato!" +# Valid placeholders: %prefix% %player% %page% %maxPages% +grim-history-header: "%prefix% &bMostrando log per &f%player% (&f%page%&b/&f%maxPages%&f)" +# Valid placeholders: %prefix% %check% %vl% %verbose% %timeago% %server% +grim-history-entry: "%prefix% &8[&f%server%&8] &bFallito &f%check% (x&c%vl%&f) &7%verbose% (&b%timeago% fa&7)" diff --git a/common/src/main/resources/messages/ja.yml b/common/src/main/resources/messages/ja.yml index a68779a147..463b4f4517 100644 --- a/common/src/main/resources/messages/ja.yml +++ b/common/src/main/resources/messages/ja.yml @@ -58,4 +58,11 @@ help: - "/grim spectate &f- &7プレイヤーを観戦します" - "/grim verbose &f- &7全てのフラグをバッファなしで表示します" - "/grim log [0-255] &f- &7予測フラグのデバッグログをアップロードします" + - "/grim history [ページ] &f- &7プレイヤーの過去のアラートを表示します" - "&7======================" + +grim-history-disabled: "%prefix% &c履歴サブシステムは無効です!" +# Valid placeholders: %prefix% %player% %page% %maxPages% +grim-history-header: "%prefix% &b%player% のログを表示中 (&f%page%&b/&f%maxPages%&f)" +# Valid placeholders: %prefix% %check% %vl% %verbose% %timeago% %server% +grim-history-entry: "%prefix% &8[&f%server%&8] &b失敗 &f%check% (x&c%vl%&f) &7%verbose% (&b%timeago% 前&7)" diff --git a/common/src/main/resources/messages/nl.yml b/common/src/main/resources/messages/nl.yml index 901f950409..ecfb50a410 100644 --- a/common/src/main/resources/messages/nl.yml +++ b/common/src/main/resources/messages/nl.yml @@ -61,4 +61,11 @@ help: - "/grim spectate &f- &7Een speler bekijken" - "/grim verbose &f- &7Toont elke flag, zonder buffers" - "/grim log [0-255] &f- &7Uploadt een debug-log voor voorspellings-flaggen" + - "/grim history [pagina] &f- &7Toont eerdere waarschuwingen voor de speler" - "&7======================" + +grim-history-disabled: "%prefix% &cHet geschiedenissubsysteem is uitgeschakeld!" +# Valid placeholders: %prefix% %player% %page% %maxPages% +grim-history-header: "%prefix% &bLogbestanden tonen voor &f%player% (&f%page%&b/&f%maxPages%&f)" +# Valid placeholders: %prefix% %check% %vl% %verbose% %timeago% %server% +grim-history-entry: "%prefix% &8[&f%server%&8] &bMislukt &f%check% (x&c%vl%&f) &7%verbose% (&b%timeago% geleden&7)" diff --git a/common/src/main/resources/messages/pt.yml b/common/src/main/resources/messages/pt.yml index ebec29b72a..5c94d28aa5 100644 --- a/common/src/main/resources/messages/pt.yml +++ b/common/src/main/resources/messages/pt.yml @@ -59,4 +59,11 @@ help: - "/grim spectate &f- &7Especta um jogador." - "/grim verbose &f- &7Esconde as informações extra." - "/grim log [0-255] &f- &7Envia o registro da simulação." + - "/grim history [página] &f- &7Mostra alertas anteriores do jogador" - "&7======================" + +grim-history-disabled: "%prefix% &cO subsistema de histórico está desativado!" +# Valid placeholders: %prefix% %player% %page% %maxPages% +grim-history-header: "%prefix% &bMostrando logs de &f%player% (&f%page%&b/&f%maxPages%&f)" +# Valid placeholders: %prefix% %check% %vl% %verbose% %timeago% %server% +grim-history-entry: "%prefix% &8[&f%server%&8] &bFalhou &f%check% (x&c%vl%&f) &7%verbose% (&bhá %timeago%&7)" diff --git a/common/src/main/resources/messages/ru.yml b/common/src/main/resources/messages/ru.yml index 9cec83dcf6..dcede5bc2d 100644 --- a/common/src/main/resources/messages/ru.yml +++ b/common/src/main/resources/messages/ru.yml @@ -61,4 +61,11 @@ help: - "/grim spectate <игрок> &f- &7Наблюдать за игроком" - "/grim verbose &f- &7Показывает все флаги без буферов" - "/grim log [0-255] &f- &7Загружает журнал отладки для флагов предсказания" + - "/grim history [страница] &f- &7Показывает предыдущие предупреждения игрока" - "&7======================" + +grim-history-disabled: "%prefix% &cПодсистема истории отключена!" +# Valid placeholders: %prefix% %player% %page% %maxPages% +grim-history-header: "%prefix% &bПоказ журналов для &f%player% (&f%page%&b/&f%maxPages%&f)" +# Valid placeholders: %prefix% %check% %vl% %verbose% %timeago% %server% +grim-history-entry: "%prefix% &8[&f%server%&8] &bПровалено &f%check% (x&c%vl%&f) &7%verbose% (&b%timeago% назад&7)" diff --git a/common/src/main/resources/messages/tr.yml b/common/src/main/resources/messages/tr.yml index 8b35924c70..5a53e53942 100644 --- a/common/src/main/resources/messages/tr.yml +++ b/common/src/main/resources/messages/tr.yml @@ -61,7 +61,11 @@ help: - "/grim spectate (oyuncu) &f- &7Bir oyuncuyu izlemenizi sağlar" - "/grim verbose &f- &7Arabelleğe almadan tüm uyarıları görmenizi sağlar" - "/grim log (0-255) &f- &7Uyarılar için bir debug logu çıktısı verir" + - "/grim history [sayfa] &f- &7Oyuncunun önceki uyarılarını gösterir" - "&7======================" -# Translated by Kayera -# Have a nice day ;) +grim-history-disabled: "%prefix% &cGeçmiş alt sistemi devre dışı!" +# Valid placeholders: %prefix% %player% %page% %maxPages% +grim-history-header: "%prefix% &bKayıtlar gösteriliyor: &f%player% (&f%page%&b/&f%maxPages%&f)" +# Valid placeholders: %prefix% %check% %vl% %verbose% %timeago% %server% +grim-history-entry: "%prefix% &8[&f%server%&8] &bBaşarısız &f%check% (x&c%vl%&f) &7%verbose% (&b%timeago% önce&7)" \ No newline at end of file diff --git a/common/src/main/resources/messages/zh.yml b/common/src/main/resources/messages/zh.yml index cf53908b88..8d68ed058d 100644 --- a/common/src/main/resources/messages/zh.yml +++ b/common/src/main/resources/messages/zh.yml @@ -61,4 +61,11 @@ help: - "/grim spectate &f- &7观看玩家" - "/grim verbose &f- &7显示无缓冲区的每个拉回" - "/grim log [1-999] &f- &7预测标志的调试日志" + - "/grim history [页码] &f- &7显示玩家之前的警报" - "&7======================" + +grim-history-disabled: "%prefix% &c历史子系统已禁用!" +# Valid placeholders: %prefix% %player% %page% %maxPages% +grim-history-header: "%prefix% &b显示 &f%player% 的日志 (&f%page%&b/&f%maxPages%&f)" +# Valid placeholders: %prefix% %check% %vl% %verbose% %timeago% %server% +grim-history-entry: "%prefix% &8[&f%server%&8] &b失败 &f%check% (x&c%vl%&f) &7%verbose% (&b%timeago% 前&7)" diff --git a/common/src/main/resources/punishments/de.yml b/common/src/main/resources/punishments/de.yml index ae191ec37d..75677f54c4 100644 --- a/common/src/main/resources/punishments/de.yml +++ b/common/src/main/resources/punishments/de.yml @@ -30,6 +30,7 @@ Punishments: # commands: - "100:40 [alert]" + - "100:40 [log]" - "100:100 [webhook]" - "100:100 [proxy]" Knockback: @@ -39,6 +40,7 @@ Punishments: - "Explosion" commands: - "5:5 [alert]" + - "5:5 [log]" - "20:20 [webhook]" - "20:20 [proxy]" Post: @@ -47,6 +49,7 @@ Punishments: - "Post" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" BadPackets: @@ -57,14 +60,25 @@ Punishments: - "Crash" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" + Inventory: + remove-violations-after: 300 + checks: + - "Inventory" + commands: + - "10:10 [alert]" + - "10:10 [log]" + - "20:20 [webhook]" + - "20:20 [proxy]" Reach: remove-violations-after: 300 checks: - "Reach" commands: - "1:1 [alert]" + - "1:1 [log]" - "1:1 [webhook]" - "1:1 [proxy]" Hitboxes: @@ -73,8 +87,27 @@ Punishments: - "Hitboxes" commands: - "5:3 [alert]" + - "5:3 [log]" - "5:3 [webhook]" - "5:3 [proxy]" + WallHit: + remove-violations-after: 300 + checks: + - "WallHit" + commands: + - "10:5 [alert]" + - "10:5 [webhook]" + - "10:5 [proxy]" + - "10:5 [log]" + EntityPierce: + remove-violations-after: 300 + checks: + - "EntityPierce" + commands: + - "15:10 [alert]" + - "15:10 [webhook]" + - "15:10 [proxy]" + - "15:10 [log]" Misc: remove-violations-after: 300 checks: @@ -89,6 +122,7 @@ Punishments: - "Elytra" commands: - "10:5 [alert]" + - "10:5 [log]" - "20:10 [webhook]" - "20:10 [proxy]" Combat: @@ -99,6 +133,7 @@ Punishments: - "Aim" commands: - "20:40 [alert]" + - "20:40 [log]" # Ab 2.2.10 gibt es keine AutoClicker-Prüfungen mehr und dies ist ein Platzhalter. Grim wird in Zukunft AutoClicker-Prüfungen einbauen. Autoclicker: remove-violations-after: 300 @@ -106,3 +141,4 @@ Punishments: - "Autoclicker" commands: - "20:40 [alert]" + - "20:40 [log]" diff --git a/common/src/main/resources/punishments/en.yml b/common/src/main/resources/punishments/en.yml index a2201f628f..7b56d7be4a 100644 --- a/common/src/main/resources/punishments/en.yml +++ b/common/src/main/resources/punishments/en.yml @@ -30,6 +30,7 @@ Punishments: # commands: - "100:40 [alert]" + - "100:40 [log]" - "100:100 [webhook]" - "100:100 [proxy]" Knockback: @@ -39,6 +40,7 @@ Punishments: - "Explosion" commands: - "5:5 [alert]" + - "5:5 [log]" - "20:20 [webhook]" - "20:20 [proxy]" Post: @@ -47,6 +49,7 @@ Punishments: - "Post" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" BadPackets: @@ -57,14 +60,25 @@ Punishments: - "Crash" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" + Inventory: + remove-violations-after: 300 + checks: + - "Inventory" + commands: + - "10:10 [alert]" + - "10:10 [log]" + - "20:20 [webhook]" + - "20:20 [proxy]" Reach: remove-violations-after: 300 checks: - "Reach" commands: - "1:1 [alert]" + - "1:1 [log]" - "1:1 [webhook]" - "1:1 [proxy]" Hitboxes: @@ -73,8 +87,27 @@ Punishments: - "Hitboxes" commands: - "5:3 [alert]" + - "5:3 [log]" - "5:3 [webhook]" - "5:3 [proxy]" + WallHit: + remove-violations-after: 300 + checks: + - "WallHit" + commands: + - "10:5 [alert]" + - "10:5 [webhook]" + - "10:5 [proxy]" + - "10:5 [log]" + EntityPierce: + remove-violations-after: 300 + checks: + - "EntityPierce" + commands: + - "15:10 [alert]" + - "15:10 [webhook]" + - "15:10 [proxy]" + - "15:10 [log]" Misc: remove-violations-after: 300 checks: @@ -89,6 +122,7 @@ Punishments: - "Elytra" commands: - "10:5 [alert]" + - "10:5 [log]" - "20:10 [webhook]" - "20:10 [proxy]" Combat: @@ -99,6 +133,7 @@ Punishments: - "Aim" commands: - "20:40 [alert]" + - "20:40 [log]" # As of 2.2.10, there are no AutoClicker checks and this is a placeholder. Grim will include AutoClicker checks in the future. Autoclicker: remove-violations-after: 300 @@ -106,3 +141,4 @@ Punishments: - "Autoclicker" commands: - "20:40 [alert]" + - "20:40 [log]" diff --git a/common/src/main/resources/punishments/es.yml b/common/src/main/resources/punishments/es.yml index 65a9379d0a..a12f8c14ad 100644 --- a/common/src/main/resources/punishments/es.yml +++ b/common/src/main/resources/punishments/es.yml @@ -30,6 +30,7 @@ Punishments: # commands: - "100:40 [alert]" + - "100:40 [log]" - "100:100 [webhook]" - "100:100 [proxy]" Knockback: @@ -39,6 +40,7 @@ Punishments: - "Explosion" commands: - "5:5 [alert]" + - "5:5 [log]" - "20:20 [webhook]" - "20:20 [proxy]" Post: @@ -47,6 +49,7 @@ Punishments: - "Post" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" BadPackets: @@ -57,14 +60,25 @@ Punishments: - "Crash" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" + Inventory: + remove-violations-after: 300 + checks: + - "Inventory" + commands: + - "10:10 [alert]" + - "10:10 [log]" + - "20:20 [webhook]" + - "20:20 [proxy]" Reach: remove-violations-after: 300 checks: - "Reach" commands: - "1:1 [alert]" + - "1:1 [log]" - "1:1 [webhook]" - "1:1 [proxy]" Hitboxes: @@ -73,8 +87,27 @@ Punishments: - "Hitboxes" commands: - "5:3 [alert]" + - "5:3 [log]" - "5:3 [webhook]" - "5:3 [proxy]" + WallHit: + remove-violations-after: 300 + checks: + - "WallHit" + commands: + - "10:5 [alert]" + - "10:5 [webhook]" + - "10:5 [proxy]" + - "10:5 [log]" + EntityPierce: + remove-violations-after: 300 + checks: + - "EntityPierce" + commands: + - "15:10 [alert]" + - "15:10 [webhook]" + - "15:10 [proxy]" + - "15:10 [log]" Misc: remove-violations-after: 300 checks: @@ -89,6 +122,7 @@ Punishments: - "Elytra" commands: - "10:5 [alert]" + - "10:5 [log]" - "20:10 [webhook]" - "20:10 [proxy]" Combat: @@ -99,6 +133,7 @@ Punishments: - "Aim" commands: - "20:40 [alert]" + - "20:40 [log]" # A partir de 2.2.10, no hay ninguna comprobación de AutoClicker y esto es un placeholder. # Grim incluirá revisiones de AutoClicker en el futuro. Autoclicker: @@ -107,3 +142,4 @@ Punishments: - "Autoclicker" commands: - "20:40 [alert]" + - "20:40 [log]" diff --git a/common/src/main/resources/punishments/fr.yml b/common/src/main/resources/punishments/fr.yml index f7bba0223a..c9f4030cf7 100644 --- a/common/src/main/resources/punishments/fr.yml +++ b/common/src/main/resources/punishments/fr.yml @@ -30,6 +30,7 @@ Punishments: # commands: - "100:40 [alert]" + - "100:40 [log]" - "100:100 [webhook]" - "100:100 [proxy]" Knockback: @@ -39,6 +40,7 @@ Punishments: - "Explosion" commands: - "5:5 [alert]" + - "5:5 [log]" - "20:20 [webhook]" - "20:20 [proxy]" Post: @@ -47,6 +49,7 @@ Punishments: - "Post" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" BadPackets: @@ -57,14 +60,25 @@ Punishments: - "Crash" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" + Inventory: + remove-violations-after: 300 + checks: + - "Inventory" + commands: + - "10:10 [alert]" + - "10:10 [log]" + - "20:20 [webhook]" + - "20:20 [proxy]" Reach: remove-violations-after: 300 checks: - "Reach" commands: - "1:1 [alert]" + - "1:1 [log]" - "1:1 [webhook]" - "1:1 [proxy]" Hitboxes: @@ -73,8 +87,27 @@ Punishments: - "Hitboxes" commands: - "5:3 [alert]" + - "5:3 [log]" - "5:3 [webhook]" - "5:3 [proxy]" + WallHit: + remove-violations-after: 300 + checks: + - "WallHit" + commands: + - "10:5 [alert]" + - "10:5 [webhook]" + - "10:5 [proxy]" + - "10:5 [log]" + EntityPierce: + remove-violations-after: 300 + checks: + - "EntityPierce" + commands: + - "15:10 [alert]" + - "15:10 [webhook]" + - "15:10 [proxy]" + - "15:10 [log]" Misc: remove-violations-after: 300 checks: @@ -89,6 +122,7 @@ Punishments: - "Elytra" commands: - "10:5 [alert]" + - "10:5 [log]" - "20:10 [webhook]" - "20:10 [proxy]" Combat: @@ -99,6 +133,7 @@ Punishments: - "Aim" commands: - "20:40 [alert]" + - "20:40 [log]" # A partir de la version 2.2.10, il n'y a plus de vérifications pour AutoClicker et c'est un placeholder. Grim inclura des vérifications AutoClicker dans le futur. Autoclicker: remove-violations-after: 300 @@ -106,3 +141,4 @@ Punishments: - "Autoclicker" commands: - "20:40 [alert]" + - "20:40 [log]" diff --git a/common/src/main/resources/punishments/it.yml b/common/src/main/resources/punishments/it.yml index 3294ae26ca..2bdeb57d39 100644 --- a/common/src/main/resources/punishments/it.yml +++ b/common/src/main/resources/punishments/it.yml @@ -17,6 +17,7 @@ Punishments: - "NoFall" commands: - "100:40 [alert]" + - "100:40 [log]" - "100:100 [webhook]" - "100:100 [proxy]" Knockback: @@ -26,6 +27,7 @@ Punishments: - "Explosion" commands: - "5:5 [alert]" + - "5:5 [log]" - "20:20 [webhook]" - "20:20 [proxy]" Post: @@ -34,6 +36,7 @@ Punishments: - "Post" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" BadPackets: @@ -44,14 +47,25 @@ Punishments: - "Crash" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" + Inventory: + remove-violations-after: 300 + checks: + - "Inventory" + commands: + - "10:10 [alert]" + - "10:10 [log]" + - "20:20 [webhook]" + - "20:20 [proxy]" Reach: remove-violations-after: 300 checks: - "Reach" commands: - "1:1 [alert]" + - "1:1 [log]" - "1:1 [webhook]" - "1:1 [proxy]" Hitboxes: @@ -60,8 +74,27 @@ Punishments: - "Hitboxes" commands: - "5:3 [alert]" + - "5:3 [log]" - "5:3 [webhook]" - "5:3 [proxy]" + WallHit: + remove-violations-after: 300 + checks: + - "WallHit" + commands: + - "10:5 [alert]" + - "10:5 [webhook]" + - "10:5 [proxy]" + - "10:5 [log]" + EntityPierce: + remove-violations-after: 300 + checks: + - "EntityPierce" + commands: + - "15:10 [alert]" + - "15:10 [webhook]" + - "15:10 [proxy]" + - "15:10 [log]" Misc: remove-violations-after: 300 checks: @@ -76,6 +109,7 @@ Punishments: - "Elytra" commands: - "10:5 [alert]" + - "10:5 [log]" - "20:10 [webhook]" - "20:10 [proxy]" Combat: @@ -86,9 +120,11 @@ Punishments: - "Aim" commands: - "20:40 [alert]" + - "20:40 [log]" Autoclicker: remove-violations-after: 300 checks: - "Autoclicker" commands: - "20:40 [alert]" + - "20:40 [log]" diff --git a/common/src/main/resources/punishments/ja.yml b/common/src/main/resources/punishments/ja.yml index e698a5832d..aa0a280128 100644 --- a/common/src/main/resources/punishments/ja.yml +++ b/common/src/main/resources/punishments/ja.yml @@ -30,6 +30,7 @@ Punishments: # commands: - "100:40 [alert]" + - "100:40 [log]" - "100:100 [webhook]" - "100:100 [proxy]" Knockback: @@ -39,6 +40,7 @@ Punishments: - "Explosion" commands: - "5:5 [alert]" + - "5:5 [log]" - "20:20 [webhook]" - "20:20 [proxy]" Post: @@ -47,6 +49,7 @@ Punishments: - "Post" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" BadPackets: @@ -57,14 +60,25 @@ Punishments: - "Crash" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" + Inventory: + remove-violations-after: 300 + checks: + - "Inventory" + commands: + - "10:10 [alert]" + - "10:10 [log]" + - "20:20 [webhook]" + - "20:20 [proxy]" Reach: remove-violations-after: 300 checks: - "Reach" commands: - "1:1 [alert]" + - "1:1 [log]" - "1:1 [webhook]" - "1:1 [proxy]" Hitboxes: @@ -73,8 +87,27 @@ Punishments: - "Hitboxes" commands: - "5:3 [alert]" + - "5:3 [log]" - "5:3 [webhook]" - "5:3 [proxy]" + WallHit: + remove-violations-after: 300 + checks: + - "WallHit" + commands: + - "10:5 [alert]" + - "10:5 [webhook]" + - "10:5 [proxy]" + - "10:5 [log]" + EntityPierce: + remove-violations-after: 300 + checks: + - "EntityPierce" + commands: + - "15:10 [alert]" + - "15:10 [webhook]" + - "15:10 [proxy]" + - "15:10 [log]" Misc: remove-violations-after: 300 checks: @@ -89,6 +122,7 @@ Punishments: - "Elytra" commands: - "10:5 [alert]" + - "10:5 [log]" - "20:10 [webhook]" - "20:10 [proxy]" Combat: @@ -98,6 +132,7 @@ Punishments: - "Aim" commands: - "20:40 [alert]" + - "20:40 [log]" # バージョン2.2.10時点では、AutoClickerのチェックはまだなく、これはプレースホルダーです。Grimは将来的にAutoClickerのチェックを追加予定です。 Autoclicker: remove-violations-after: 300 @@ -105,3 +140,4 @@ Punishments: - "Autoclicker" commands: - "20:40 [alert]" + - "20:40 [log]" diff --git a/common/src/main/resources/punishments/nl.yml b/common/src/main/resources/punishments/nl.yml index 3791d73be4..dc03cc70c4 100644 --- a/common/src/main/resources/punishments/nl.yml +++ b/common/src/main/resources/punishments/nl.yml @@ -30,6 +30,7 @@ Punishments: # commands: - "100:40 [alert]" + - "100:40 [log]" - "100:100 [webhook]" - "100:100 [proxy]" Knockback: @@ -39,6 +40,7 @@ Punishments: - "Explosion" commands: - "5:5 [alert]" + - "5:5 [log]" - "20:20 [webhook]" - "20:20 [proxy]" Post: @@ -47,6 +49,7 @@ Punishments: - "Post" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" BadPackets: @@ -57,14 +60,25 @@ Punishments: - "Crash" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" + Inventory: + remove-violations-after: 300 + checks: + - "Inventory" + commands: + - "10:10 [alert]" + - "10:10 [log]" + - "20:20 [webhook]" + - "20:20 [proxy]" Reach: remove-violations-after: 300 checks: - "Reach" commands: - "1:1 [alert]" + - "1:1 [log]" - "1:1 [webhook]" - "1:1 [proxy]" Hitboxes: @@ -73,8 +87,27 @@ Punishments: - "Hitboxes" commands: - "5:3 [alert]" + - "5:3 [log]" - "5:3 [webhook]" - "5:3 [proxy]" + WallHit: + remove-violations-after: 300 + checks: + - "WallHit" + commands: + - "10:5 [alert]" + - "10:5 [webhook]" + - "10:5 [proxy]" + - "10:5 [log]" + EntityPierce: + remove-violations-after: 300 + checks: + - "EntityPierce" + commands: + - "15:10 [alert]" + - "15:10 [webhook]" + - "15:10 [proxy]" + - "15:10 [log]" Misc: remove-violations-after: 300 checks: @@ -89,6 +122,7 @@ Punishments: - "Elytra" commands: - "10:5 [alert]" + - "10:5 [log]" - "20:10 [webhook]" - "20:10 [proxy]" Combat: @@ -99,6 +133,7 @@ Punishments: - "Aim" commands: - "20:40 [alert]" + - "20:40 [log]" # Vanaf 2.2.10 zijn er geen AutoClicker-controles en is dit een placeholder. Grim zal in de toekomst AutoClicker-controles toevoegen. Autoclicker: remove-violations-after: 300 @@ -106,3 +141,4 @@ Punishments: - "Autoclicker" commands: - "20:40 [alert]" + - "20:40 [log]" diff --git a/common/src/main/resources/punishments/pt.yml b/common/src/main/resources/punishments/pt.yml index 2e882616be..5c13da85a0 100644 --- a/common/src/main/resources/punishments/pt.yml +++ b/common/src/main/resources/punishments/pt.yml @@ -30,6 +30,7 @@ Punishments: # commands: - "100:40 [alert]" + - "100:40 [log]" - "100:100 [webhook]" - "100:100 [proxy]" Knockback: @@ -39,6 +40,7 @@ Punishments: - "Explosion" commands: - "5:5 [alert]" + - "5:5 [log]" - "20:20 [webhook]" - "20:20 [proxy]" Post: @@ -47,6 +49,7 @@ Punishments: - "Post" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" BadPackets: @@ -57,14 +60,25 @@ Punishments: - "Crash" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" + Inventory: + remove-violations-after: 300 + checks: + - "Inventory" + commands: + - "10:10 [alert]" + - "10:10 [log]" + - "20:20 [webhook]" + - "20:20 [proxy]" Reach: remove-violations-after: 300 checks: - "Reach" commands: - "1:1 [alert]" + - "1:1 [log]" - "1:1 [webhook]" - "1:1 [proxy]" Hitboxes: @@ -73,8 +87,27 @@ Punishments: - "Hitboxes" commands: - "5:3 [alert]" + - "5:3 [log]" - "5:3 [webhook]" - "5:3 [proxy]" + WallHit: + remove-violations-after: 300 + checks: + - "WallHit" + commands: + - "10:5 [alert]" + - "10:5 [webhook]" + - "10:5 [proxy]" + - "10:5 [log]" + EntityPierce: + remove-violations-after: 300 + checks: + - "EntityPierce" + commands: + - "15:10 [alert]" + - "15:10 [webhook]" + - "15:10 [proxy]" + - "15:10 [log]" Misc: remove-violations-after: 300 checks: @@ -89,6 +122,7 @@ Punishments: - "Elytra" commands: - "10:5 [alert]" + - "10:5 [log]" - "20:10 [webhook]" - "20:10 [proxy]" Combat: @@ -99,6 +133,7 @@ Punishments: - "Aim" commands: - "20:40 [alert]" + - "20:40 [log]" # Dês da 2.2.10, não temos verificações de AutoClicker, isso é somente um placeholder. Grim vai verificar por AutoClicker no futuro. Autoclicker: remove-violations-after: 300 @@ -106,3 +141,4 @@ Punishments: - "Autoclicker" commands: - "20:40 [alert]" + - "20:40 [log]" diff --git a/common/src/main/resources/punishments/ru.yml b/common/src/main/resources/punishments/ru.yml index d88859a704..de30538e3a 100644 --- a/common/src/main/resources/punishments/ru.yml +++ b/common/src/main/resources/punishments/ru.yml @@ -30,6 +30,7 @@ Punishments: # commands: - "100:40 [alert]" + - "100:40 [log]" - "100:100 [webhook]" - "100:100 [proxy]" Knockback: @@ -39,6 +40,7 @@ Punishments: - "Explosion" commands: - "5:5 [alert]" + - "5:5 [log]" - "20:20 [webhook]" - "20:20 [proxy]" Post: @@ -47,6 +49,7 @@ Punishments: - "Post" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" BadPackets: @@ -57,14 +60,25 @@ Punishments: - "Crash" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" + Inventory: + remove-violations-after: 300 + checks: + - "Inventory" + commands: + - "10:10 [alert]" + - "10:10 [log]" + - "20:20 [webhook]" + - "20:20 [proxy]" Reach: remove-violations-after: 300 checks: - "Reach" commands: - "1:1 [alert]" + - "1:1 [log]" - "1:1 [webhook]" - "1:1 [proxy]" Hitboxes: @@ -73,8 +87,27 @@ Punishments: - "Hitboxes" commands: - "5:3 [alert]" + - "5:3 [log]" - "5:3 [webhook]" - "5:3 [proxy]" + WallHit: + remove-violations-after: 300 + checks: + - "WallHit" + commands: + - "10:5 [alert]" + - "10:5 [webhook]" + - "10:5 [proxy]" + - "10:5 [log]" + EntityPierce: + remove-violations-after: 300 + checks: + - "EntityPierce" + commands: + - "15:10 [alert]" + - "15:10 [webhook]" + - "15:10 [proxy]" + - "15:10 [log]" Misc: remove-violations-after: 300 checks: @@ -89,6 +122,7 @@ Punishments: - "Elytra" commands: - "10:5 [alert]" + - "10:5 [log]" - "20:10 [webhook]" - "20:10 [proxy]" Combat: @@ -99,6 +133,7 @@ Punishments: - "Aim" commands: - "20:40 [alert]" + - "20:40 [log]" # Начиная с версии 2.2.10, проверки на AutoClicker отсутствуют, на их месте пока используется placeholder. Grim будет включать проверки AutoClicker в будущем. Autoclicker: remove-violations-after: 300 @@ -106,3 +141,4 @@ Punishments: - "Autoclicker" commands: - "20:40 [alert]" + - "20:40 [log]" diff --git a/common/src/main/resources/punishments/tr.yml b/common/src/main/resources/punishments/tr.yml index afb34e4bea..65d86c78d8 100644 --- a/common/src/main/resources/punishments/tr.yml +++ b/common/src/main/resources/punishments/tr.yml @@ -28,6 +28,7 @@ Punishments: # Kullanıcı 100 işaretine ulaştığında çalışır ve bundan sonra, 100'ün üzerindeki her 50. işarette çalışır commands: - "100:40 [alert]" + - "100:40 [log]" - "100:100 [webhook]" - "100:100 [proxy]" Knockback: @@ -37,6 +38,7 @@ Punishments: - "Explosion" commands: - "5:5 [alert]" + - "5:5 [log]" - "20:20 [webhook]" - "20:20 [proxy]" Post: @@ -45,6 +47,7 @@ Punishments: - "Post" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" BadPackets: @@ -55,14 +58,25 @@ Punishments: - "Crash" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" + Inventory: + remove-violations-after: 300 + checks: + - "Inventory" + commands: + - "10:10 [alert]" + - "10:10 [log]" + - "20:20 [webhook]" + - "20:20 [proxy]" Reach: remove-violations-after: 300 checks: - "Reach" commands: - "1:1 [alert]" + - "1:1 [log]" - "1:1 [webhook]" - "1:1 [proxy]" Hitboxes: @@ -71,8 +85,27 @@ Punishments: - "Hitboxes" commands: - "5:3 [alert]" + - "5:3 [log]" - "5:3 [webhook]" - "5:3 [proxy]" + WallHit: + remove-violations-after: 300 + checks: + - "WallHit" + commands: + - "10:5 [alert]" + - "10:5 [webhook]" + - "10:5 [proxy]" + - "10:5 [log]" + EntityPierce: + remove-violations-after: 300 + checks: + - "EntityPierce" + commands: + - "15:10 [alert]" + - "15:10 [webhook]" + - "15:10 [proxy]" + - "15:10 [log]" Misc: remove-violations-after: 300 checks: @@ -87,6 +120,7 @@ Punishments: - "Elytra" commands: - "10:5 [alert]" + - "10:5 [log]" - "20:10 [webhook]" - "20:10 [proxy]" Combat: @@ -97,6 +131,7 @@ Punishments: - "Aim" commands: - "20:40 [alert]" + - "20:40 [log]" # 2.2.10 itibarıyla, AutoClicker denetimleri yoktur ve bu bir yer tutucudur. Grim, gelecekte AutoClicker denetimlerini dahil edecektir. Autoclicker: remove-violations-after: 300 @@ -104,3 +139,4 @@ Punishments: - "Autoclicker" commands: - "20:40 [alert]" + - "20:40 [log]" diff --git a/common/src/main/resources/punishments/zh.yml b/common/src/main/resources/punishments/zh.yml index 78101b050c..2e4e6c9f8f 100644 --- a/common/src/main/resources/punishments/zh.yml +++ b/common/src/main/resources/punishments/zh.yml @@ -30,6 +30,7 @@ Punishments: # commands: - "100:40 [alert]" + - "100:40 [log]" - "100:100 [webhook]" - "100:100 [proxy]" Knockback: @@ -39,6 +40,7 @@ Punishments: - "Explosion" commands: - "5:5 [alert]" + - "5:5 [log]" - "20:20 [webhook]" - "20:20 [proxy]" Post: @@ -47,6 +49,7 @@ Punishments: - "Post" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" BadPackets: @@ -57,14 +60,25 @@ Punishments: - "Crash" commands: - "20:20 [alert]" + - "20:20 [log]" - "40:40 [webhook]" - "40:40 [proxy]" + Inventory: + remove-violations-after: 300 + checks: + - "Inventory" + commands: + - "10:10 [alert]" + - "10:10 [log]" + - "20:20 [webhook]" + - "20:20 [proxy]" Reach: remove-violations-after: 300 checks: - "Reach" commands: - "1:1 [alert]" + - "1:1 [log]" - "1:1 [webhook]" - "1:1 [proxy]" Hitboxes: @@ -73,8 +87,27 @@ Punishments: - "Hitboxes" commands: - "5:3 [alert]" + - "5:3 [log]" - "5:3 [webhook]" - "5:3 [proxy]" + WallHit: + remove-violations-after: 300 + checks: + - "WallHit" + commands: + - "10:5 [alert]" + - "10:5 [webhook]" + - "10:5 [proxy]" + - "10:5 [log]" + EntityPierce: + remove-violations-after: 300 + checks: + - "EntityPierce" + commands: + - "15:10 [alert]" + - "15:10 [webhook]" + - "15:10 [proxy]" + - "15:10 [log]" Misc: remove-violations-after: 300 checks: @@ -89,6 +122,7 @@ Punishments: - "Elytra" commands: - "10:5 [alert]" + - "10:5 [log]" - "20:10 [webhook]" - "20:10 [proxy]" Combat: @@ -99,6 +133,7 @@ Punishments: - "Aim" commands: - "20:40 [alert]" + - "20:40 [log]" # Grim2.0.10版本 没有连点器检查,Grim将在未来添加 Autoclicker: remove-violations-after: 300 @@ -106,3 +141,4 @@ Punishments: - "Autoclicker" commands: - "20:40 [alert]" + - "20:40 [log]" diff --git a/fabric/mc1161/src/main/java/ac/grim/grimac/platform/fabric/mc1161/GrimACFabric1161LoaderPlugin.java b/fabric/mc1161/src/main/java/ac/grim/grimac/platform/fabric/mc1161/GrimACFabric1161LoaderPlugin.java index 07b72c1859..7896097c5d 100644 --- a/fabric/mc1161/src/main/java/ac/grim/grimac/platform/fabric/mc1161/GrimACFabric1161LoaderPlugin.java +++ b/fabric/mc1161/src/main/java/ac/grim/grimac/platform/fabric/mc1161/GrimACFabric1161LoaderPlugin.java @@ -1,6 +1,6 @@ package ac.grim.grimac.platform.fabric.mc1161; -import ac.grim.grimac.platform.api.PlatformServer; +import ac.grim.grimac.platform.fabric.AbstractFabricPlatformServer; import ac.grim.grimac.platform.fabric.command.FabricPlayerSelectorParser; import ac.grim.grimac.platform.fabric.manager.FabricParserDescriptorFactory; import ac.grim.grimac.platform.fabric.mc1161.command.Fabric1161PlayerSelectorAdapter; @@ -31,7 +31,7 @@ public GrimACFabric1161LoaderPlugin() { protected GrimACFabric1161LoaderPlugin( FabricPlatformPlayerFactory playerFactory, - PlatformServer platformServer, + AbstractFabricPlatformServer platformServer, IFabricMessageUtil fabricMessageUtil, IFabricConversionUtil fabricConversionUtil ) { diff --git a/fabric/mc1171/src/main/java/ac/grim/grimac/platform/fabric/mc1171/Fabric1171PlatformServer.java b/fabric/mc1171/src/main/java/ac/grim/grimac/platform/fabric/mc1171/Fabric1171PlatformServer.java new file mode 100644 index 0000000000..4fe9c5673a --- /dev/null +++ b/fabric/mc1171/src/main/java/ac/grim/grimac/platform/fabric/mc1171/Fabric1171PlatformServer.java @@ -0,0 +1,18 @@ +package ac.grim.grimac.platform.fabric.mc1171; + +import ac.grim.grimac.platform.fabric.GrimACFabricLoaderPlugin; +import ac.grim.grimac.platform.fabric.mc1161.Fabric1140PlatformServer; +import com.mojang.authlib.GameProfile; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + + +public class Fabric1171PlatformServer extends Fabric1140PlatformServer { + + @Override @Nullable + public GameProfile getProfileByName(String name) { + Optional gameProfile = GrimACFabricLoaderPlugin.FABRIC_SERVER.getUserCache().findByName(name); + return gameProfile.orElse(null); + } +} diff --git a/fabric/mc1171/src/main/java/ac/grim/grimac/platform/fabric/mc1171/GrimACFabric1170LoaderPlugin.java b/fabric/mc1171/src/main/java/ac/grim/grimac/platform/fabric/mc1171/GrimACFabric1170LoaderPlugin.java index 7c0efe8194..28080d0702 100644 --- a/fabric/mc1171/src/main/java/ac/grim/grimac/platform/fabric/mc1171/GrimACFabric1170LoaderPlugin.java +++ b/fabric/mc1171/src/main/java/ac/grim/grimac/platform/fabric/mc1171/GrimACFabric1170LoaderPlugin.java @@ -1,6 +1,6 @@ package ac.grim.grimac.platform.fabric.mc1171; -import ac.grim.grimac.platform.api.PlatformServer; +import ac.grim.grimac.platform.fabric.AbstractFabricPlatformServer; import ac.grim.grimac.platform.api.manager.ParserDescriptorFactory; import ac.grim.grimac.platform.fabric.GrimACFabricLoaderPlugin; import ac.grim.grimac.platform.fabric.command.FabricPlayerSelectorParser; @@ -15,6 +15,7 @@ import ac.grim.grimac.platform.fabric.player.FabricPlatformPlayerFactory; import ac.grim.grimac.platform.fabric.utils.convert.IFabricConversionUtil; import ac.grim.grimac.platform.fabric.utils.message.IFabricMessageUtil; +import com.github.retrooper.packetevents.PacketEvents; import com.github.retrooper.packetevents.manager.server.ServerVersion; public class GrimACFabric1170LoaderPlugin extends GrimACFabricLoaderPlugin { @@ -28,7 +29,8 @@ public GrimACFabric1170LoaderPlugin() { Fabric1170GrimEntity::new, Fabric1161PlatformInventory::new ), - new Fabric1140PlatformServer(), + PacketEvents.getAPI().getServerManager().getVersion().isNewerThan(ServerVersion.V_1_17) + ? new Fabric1171PlatformServer() : new Fabric1140PlatformServer(), new Fabric1161MessageUtil(), new Fabric1140ConversionUtil() ); @@ -36,7 +38,7 @@ public GrimACFabric1170LoaderPlugin() { protected GrimACFabric1170LoaderPlugin(ParserDescriptorFactory parserDescriptorFactory, FabricPlatformPlayerFactory playerFactory, - PlatformServer platformServer, + AbstractFabricPlatformServer platformServer, IFabricMessageUtil fabricMessageUtil, IFabricConversionUtil fabricConversionUtil) { super( diff --git a/fabric/mc1194/src/main/java/ac/grim/grimac/platform/fabric/mc1194/Fabric1190PlatformServer.java b/fabric/mc1194/src/main/java/ac/grim/grimac/platform/fabric/mc1194/Fabric1190PlatformServer.java index b5f9b0e1a4..4f4551e034 100644 --- a/fabric/mc1194/src/main/java/ac/grim/grimac/platform/fabric/mc1194/Fabric1190PlatformServer.java +++ b/fabric/mc1194/src/main/java/ac/grim/grimac/platform/fabric/mc1194/Fabric1190PlatformServer.java @@ -2,10 +2,10 @@ import ac.grim.grimac.platform.api.sender.Sender; import ac.grim.grimac.platform.fabric.GrimACFabricLoaderPlugin; -import ac.grim.grimac.platform.fabric.mc1161.Fabric1140PlatformServer; +import ac.grim.grimac.platform.fabric.mc1171.Fabric1171PlatformServer; import net.minecraft.server.command.ServerCommandSource; -public class Fabric1190PlatformServer extends Fabric1140PlatformServer { +public class Fabric1190PlatformServer extends Fabric1171PlatformServer { @Override public void dispatchCommand(Sender sender, String command) { ServerCommandSource commandSource = GrimACFabricLoaderPlugin.LOADER.getFabricSenderFactory().reverse(sender); diff --git a/fabric/mc1194/src/main/java/ac/grim/grimac/platform/fabric/mc1194/GrimACFabric1190LoaderPlugin.java b/fabric/mc1194/src/main/java/ac/grim/grimac/platform/fabric/mc1194/GrimACFabric1190LoaderPlugin.java index e3367d5e5c..87c9a18d43 100644 --- a/fabric/mc1194/src/main/java/ac/grim/grimac/platform/fabric/mc1194/GrimACFabric1190LoaderPlugin.java +++ b/fabric/mc1194/src/main/java/ac/grim/grimac/platform/fabric/mc1194/GrimACFabric1190LoaderPlugin.java @@ -1,6 +1,6 @@ package ac.grim.grimac.platform.fabric.mc1194; -import ac.grim.grimac.platform.api.PlatformServer; +import ac.grim.grimac.platform.fabric.AbstractFabricPlatformServer; import ac.grim.grimac.platform.api.manager.ParserDescriptorFactory; import ac.grim.grimac.platform.fabric.mc1161.command.Fabric1161PlayerSelectorAdapter; import ac.grim.grimac.platform.fabric.command.FabricPlayerSelectorParser; @@ -40,7 +40,7 @@ public GrimACFabric1190LoaderPlugin() { protected GrimACFabric1190LoaderPlugin( ParserDescriptorFactory parserDescriptorFactory, FabricPlatformPlayerFactory platformPlayerFactory, - PlatformServer platformServer, + AbstractFabricPlatformServer platformServer, IFabricMessageUtil fabricMessageUtil, IFabricConversionUtil fabricConversionUtil) { super(parserDescriptorFactory, platformPlayerFactory, platformServer, fabricMessageUtil, fabricConversionUtil); diff --git a/fabric/src/main/java/ac/grim/grimac/platform/fabric/AbstractFabricPlatformServer.java b/fabric/src/main/java/ac/grim/grimac/platform/fabric/AbstractFabricPlatformServer.java index 8848206f7c..ce7464811a 100644 --- a/fabric/src/main/java/ac/grim/grimac/platform/fabric/AbstractFabricPlatformServer.java +++ b/fabric/src/main/java/ac/grim/grimac/platform/fabric/AbstractFabricPlatformServer.java @@ -2,8 +2,10 @@ import ac.grim.grimac.platform.api.PlatformServer; import ac.grim.grimac.platform.api.sender.Sender; +import com.mojang.authlib.GameProfile; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.server.command.ServerCommandSource; +import org.jetbrains.annotations.Nullable; public abstract class AbstractFabricPlatformServer implements PlatformServer { @@ -24,4 +26,9 @@ public Sender getConsoleSender() { public void registerOutgoingPluginChannel(String name) { throw new UnsupportedOperationException(); } + + @Nullable + public GameProfile getProfileByName(String name) { + return GrimACFabricLoaderPlugin.FABRIC_SERVER.getUserCache().findByName(name); + } } diff --git a/fabric/src/main/java/ac/grim/grimac/platform/fabric/GrimACFabricLoaderPlugin.java b/fabric/src/main/java/ac/grim/grimac/platform/fabric/GrimACFabricLoaderPlugin.java index a9e21e04cb..0ae028717d 100644 --- a/fabric/src/main/java/ac/grim/grimac/platform/fabric/GrimACFabricLoaderPlugin.java +++ b/fabric/src/main/java/ac/grim/grimac/platform/fabric/GrimACFabricLoaderPlugin.java @@ -5,7 +5,6 @@ import ac.grim.grimac.api.GrimAPIProvider; import ac.grim.grimac.api.plugin.GrimPlugin; import ac.grim.grimac.platform.api.PlatformLoader; -import ac.grim.grimac.platform.api.PlatformServer; import ac.grim.grimac.platform.api.manager.*; import ac.grim.grimac.platform.api.sender.Sender; import ac.grim.grimac.platform.api.sender.SenderFactory; @@ -58,7 +57,7 @@ public abstract class GrimACFabricLoaderPlugin implements PlatformLoader { protected final ParserDescriptorFactory parserFactory; protected final FabricPlatformPlayerFactory playerFactory; - protected final PlatformServer platformServer; + protected final AbstractFabricPlatformServer platformServer; @Getter protected final IFabricConversionUtil fabricConversionUtil; protected final IFabricMessageUtil fabricMessageUtil; @@ -66,7 +65,7 @@ public abstract class GrimACFabricLoaderPlugin implements PlatformLoader { public GrimACFabricLoaderPlugin( ParserDescriptorFactory parserDescriptorFactory, FabricPlatformPlayerFactory playerFactory, - PlatformServer platformServer, + AbstractFabricPlatformServer platformServer, IFabricMessageUtil fabricMessageUtil, IFabricConversionUtil fabricConversionUtil ) { @@ -149,7 +148,7 @@ public FabricPlatformPlayerFactory getPlatformPlayerFactory() { } @Override - public PlatformServer getPlatformServer() { + public AbstractFabricPlatformServer getPlatformServer() { return platformServer; } diff --git a/fabric/src/main/java/ac/grim/grimac/platform/fabric/mixins/ServerPlayerEntityMixin.java b/fabric/src/main/java/ac/grim/grimac/platform/fabric/mixins/ServerPlayerEntityMixin.java index 95c7e43b63..7a9699ea7f 100644 --- a/fabric/src/main/java/ac/grim/grimac/platform/fabric/mixins/ServerPlayerEntityMixin.java +++ b/fabric/src/main/java/ac/grim/grimac/platform/fabric/mixins/ServerPlayerEntityMixin.java @@ -10,7 +10,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import net.minecraft.server.network.ServerPlayerEntity; - +// Works from 1.14 - latest (1.21.5) @Mixin(ServerPlayerEntity.class) abstract class ServerPlayerEntityMixin { diff --git a/fabric/src/main/java/ac/grim/grimac/platform/fabric/player/FabricOfflinePlatformPlayer.java b/fabric/src/main/java/ac/grim/grimac/platform/fabric/player/FabricOfflinePlatformPlayer.java new file mode 100644 index 0000000000..98773c9265 --- /dev/null +++ b/fabric/src/main/java/ac/grim/grimac/platform/fabric/player/FabricOfflinePlatformPlayer.java @@ -0,0 +1,40 @@ +package ac.grim.grimac.platform.fabric.player; + +import ac.grim.grimac.platform.api.player.OfflinePlatformPlayer; +import ac.grim.grimac.platform.fabric.GrimACFabricLoaderPlugin; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class FabricOfflinePlatformPlayer implements OfflinePlatformPlayer { + private final UUID uuid; + private final String name; + + public FabricOfflinePlatformPlayer(@NotNull UUID uuid, @NotNull String name) { + this.uuid = uuid; + this.name = name; + } + + @Override + public boolean isOnline() { + return GrimACFabricLoaderPlugin.FABRIC_SERVER.getPlayerManager().getPlayer(uuid) != null; + } + + @Override + public @NotNull String getName() { + return name; + } + + @Override + public @NotNull UUID getUniqueId() { + return uuid; + } + + @Override + public boolean equals(Object o) { + if (o instanceof OfflinePlatformPlayer offlinePlatformPlayer) { + return this.getUniqueId().equals(offlinePlatformPlayer.getUniqueId()); + } + return false; + } +} diff --git a/fabric/src/main/java/ac/grim/grimac/platform/fabric/player/FabricPlatformPlayerFactory.java b/fabric/src/main/java/ac/grim/grimac/platform/fabric/player/FabricPlatformPlayerFactory.java index fadad12b84..cfba9bd15d 100644 --- a/fabric/src/main/java/ac/grim/grimac/platform/fabric/player/FabricPlatformPlayerFactory.java +++ b/fabric/src/main/java/ac/grim/grimac/platform/fabric/player/FabricPlatformPlayerFactory.java @@ -2,18 +2,24 @@ import ac.grim.grimac.platform.api.entity.GrimEntity; import ac.grim.grimac.platform.api.player.AbstractPlatformPlayerFactory; +import ac.grim.grimac.platform.api.player.OfflinePlatformPlayer; import ac.grim.grimac.platform.fabric.GrimACFabricLoaderPlugin; +import com.mojang.authlib.GameProfile; import net.minecraft.entity.Entity; import net.minecraft.server.network.ServerPlayerEntity; import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.NotNull; +import java.nio.charset.StandardCharsets; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; import java.util.function.Function; public class FabricPlatformPlayerFactory extends AbstractPlatformPlayerFactory { + private final Map offlinePlatformPlayerCache = new HashMap<>(); private final Function getPlayerFunction; private final Function getEntityFunction; private final Function getPlayerInventoryFunction; @@ -32,6 +38,11 @@ protected ServerPlayerEntity getNativePlayer(@NotNull UUID uuid) { return GrimACFabricLoaderPlugin.FABRIC_SERVER.getPlayerManager().getPlayer(uuid); } + @Override + protected ServerPlayerEntity getNativePlayer(@NonNull String name) { + return GrimACFabricLoaderPlugin.FABRIC_SERVER.getPlayerManager().getPlayer(name); + } + @Override protected AbstractFabricPlatformPlayer createPlatformPlayer(@NotNull ServerPlayerEntity nativePlayer) { return getPlayerFunction.apply(nativePlayer); @@ -58,6 +69,43 @@ protected Collection getNativeOnlinePlayers() { return GrimACFabricLoaderPlugin.FABRIC_SERVER.getPlayerManager().getPlayerList(); } + @Override + public OfflinePlatformPlayer getOfflineFromUUID(@NotNull UUID uuid) { + return null; + } + + @Override + public OfflinePlatformPlayer getOfflineFromName(@NotNull String name) { + OfflinePlatformPlayer result = this.getFromName(name); + if (result == null) { + GameProfile profile = null; + // Only fetch an online UUID in online mode + // TODO (cross-platform) add a config option for "offline-mode" servers with online-mode behind a proxy + if (GrimACFabricLoaderPlugin.FABRIC_SERVER.isOnlineMode()) { + // THIS CAN BLOCK THE CALLING THREAD! + profile = GrimACFabricLoaderPlugin.LOADER.getPlatformServer().getProfileByName(name); + } + + if (profile == null) { + // Make an OfflinePlayer using an offline mode UUID since the name has no profile + result = this.getOfflinePlayer(new GameProfile(UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(StandardCharsets.UTF_8)), name)); + } else { + // Use the GameProfile even when we get a UUID so we ensure we still have a name + result = this.getOfflinePlayer(profile); + } + } else { + this.offlinePlatformPlayerCache.remove(result.getUniqueId()); + } + + return result; + } + + public OfflinePlatformPlayer getOfflinePlayer(GameProfile profile) { + OfflinePlatformPlayer player = new FabricOfflinePlatformPlayer(profile.getId(), profile.getName()); + this.offlinePlatformPlayerCache.put(profile.getId(), player); + return player; + } + @Override public void replaceNativePlayer(@NonNull UUID uuid, @NonNull ServerPlayerEntity serverPlayerEntity) { super.cache.getPlayer(uuid).replaceNativePlayer(serverPlayerEntity); diff --git a/fabric/src/main/java/ac/grim/grimac/platform/fabric/scheduler/FabricAsyncScheduler.java b/fabric/src/main/java/ac/grim/grimac/platform/fabric/scheduler/FabricAsyncScheduler.java index fcddfc9ea7..6a23c81e92 100644 --- a/fabric/src/main/java/ac/grim/grimac/platform/fabric/scheduler/FabricAsyncScheduler.java +++ b/fabric/src/main/java/ac/grim/grimac/platform/fabric/scheduler/FabricAsyncScheduler.java @@ -4,7 +4,6 @@ import ac.grim.grimac.platform.api.scheduler.AsyncScheduler; import ac.grim.grimac.platform.api.scheduler.PlatformScheduler; import ac.grim.grimac.platform.api.scheduler.TaskHandle; -import ac.grim.grimac.utils.data.Pair; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -15,9 +14,11 @@ import java.util.concurrent.TimeUnit; public class FabricAsyncScheduler implements AsyncScheduler { - private final Map> asyncTasks = new HashMap<>(); + private final Map asyncTasks = new HashMap<>(); private final GrimPlugin plugin; + record InternalAsyncTask(GrimPlugin plugin, Runnable runnable) {} + public FabricAsyncScheduler(GrimPlugin plugin) { this.plugin = plugin; } @@ -29,7 +30,7 @@ public TaskHandle runNow(@NotNull GrimPlugin plugin, @NotNull Runnable task) { thread.interrupt(); asyncTasks.remove(thread); }; - asyncTasks.put(thread, new Pair<>(plugin, cancellationTask)); + asyncTasks.put(thread, new InternalAsyncTask(plugin, cancellationTask)); thread.start(); return new FabricTaskHandle(cancellationTask, false); } @@ -49,7 +50,7 @@ public TaskHandle runDelayed(@NotNull GrimPlugin plugin, @NotNull Runnable task, thread.interrupt(); asyncTasks.remove(thread); }; - asyncTasks.put(thread, new Pair<>(plugin, cancellationTask)); + asyncTasks.put(thread, new InternalAsyncTask(plugin, cancellationTask)); thread.start(); return new FabricTaskHandle(cancellationTask, false); // false for async } @@ -73,7 +74,7 @@ public TaskHandle runAtFixedRate(@NotNull GrimPlugin plugin, @NotNull Runnable t thread.interrupt(); asyncTasks.remove(thread); }; - asyncTasks.put(thread, new Pair<>(plugin, cancellationTask)); + asyncTasks.put(thread, new InternalAsyncTask(plugin, cancellationTask)); thread.start(); return new FabricTaskHandle(cancellationTask, false); // false for async } @@ -89,13 +90,13 @@ public TaskHandle runAtFixedRate(@NotNull GrimPlugin plugin, @NotNull Runnable t @Override public void cancel(@NotNull GrimPlugin plugin) { // Cancel tasks only for the specified plugin - Iterator>> iterator = asyncTasks.entrySet().iterator(); + Iterator> iterator = asyncTasks.entrySet().iterator(); List cancellationTasks = new ArrayList<>(); while (iterator.hasNext()) { - Map.Entry> entry = iterator.next(); - if (entry.getValue().first().equals(plugin)) { - cancellationTasks.add(entry.getValue().second()); + Map.Entry entry = iterator.next(); + if (entry.getValue().plugin().equals(plugin)) { + cancellationTasks.add(entry.getValue().runnable()); iterator.remove(); } } @@ -107,7 +108,7 @@ public void cancel(@NotNull GrimPlugin plugin) { public void cancelAll() { List cancellationTasks = asyncTasks.values().stream() - .map(Pair::second) + .map(InternalAsyncTask::runnable) .toList(); asyncTasks.clear();