diff --git a/AntiAFK-Core/src/main/java/com/bentahsin/antiafk/behavior/BehaviorAnalysisTask.java b/AntiAFK-Core/src/main/java/com/bentahsin/antiafk/behavior/BehaviorAnalysisTask.java index 197e453..ef74c4a 100644 --- a/AntiAFK-Core/src/main/java/com/bentahsin/antiafk/behavior/BehaviorAnalysisTask.java +++ b/AntiAFK-Core/src/main/java/com/bentahsin/antiafk/behavior/BehaviorAnalysisTask.java @@ -68,6 +68,29 @@ public void run() { } PlayerBehaviorData data = analysisManagerProvider.get().getPlayerData(player); + + if (configManager.isConfinementCheckEnabled()) { + long durationMillis = data.getConfinementDuration(); + long requiredMillis = configManager.getConfinementCheckDurationMillis(); + + if (durationMillis > requiredMillis) { + if (data.getTotalDistanceTraveled() > configManager.getConfinementMinDistance()) { + Bukkit.getScheduler().runTask(plugin, () -> + afkManagerProvider.get().getBotDetectionManager().triggerSuspicionAndChallenge( + player, + "behavior.afk_pool_detected", + DetectionType.POINTLESS_ACTIVITY + ) + ); + data.reset(); + } else { + debugManager.log(DebugManager.DebugModule.BEHAVIORAL_ANALYSIS, + "Confinement period ended for %s without violation. Resetting window.", player.getName()); + data.reset(); + } + } + } + LinkedList history = data.getMovementHistory(); boolean matchFound = false; diff --git a/AntiAFK-Core/src/main/java/com/bentahsin/antiafk/behavior/PlayerBehaviorData.java b/AntiAFK-Core/src/main/java/com/bentahsin/antiafk/behavior/PlayerBehaviorData.java index 12bac3a..72dbd79 100644 --- a/AntiAFK-Core/src/main/java/com/bentahsin/antiafk/behavior/PlayerBehaviorData.java +++ b/AntiAFK-Core/src/main/java/com/bentahsin/antiafk/behavior/PlayerBehaviorData.java @@ -1,63 +1,77 @@ package com.bentahsin.antiafk.behavior; import org.bukkit.Location; - import java.util.LinkedList; +import java.util.Objects; -/** - * Her oyuncu için davranış analizi verilerini tutan veri sınıfı. - * Bu sınıf, oyuncunun hareket geçmişini ve tespit edilen otonom hareket - * tekrarlarını depolamak için kullanılır. - */ public class PlayerBehaviorData { - /** - * Oyuncunun son 'X' saniyelik tüm anlamlı hareketlerini tutan ana veri havuzu. - * LinkedList, listenin başından (en eski veri) eleman silme işlemlerinde - * (FIFO - First-In, First-Out) ArrayList'e göre daha performanslıdır. - */ private final LinkedList movementHistory = new LinkedList<>(); - /** - * Tespit edilen son tekrarın ne zaman gerçekleştiğini milisaniye cinsinden tutar. - * Bu, tekrarların ardışık olup olmadığını anlamak için kullanılır. - */ - private long lastRepeatTimestamp; + private volatile Location confinementStartLocation; + private volatile long confinementStartTime; + private volatile double totalDistanceTraveled = 0.0; + private Location lastMoveLocation; - /** - * Birbirini takip eden (ardışık) tekrar sayısını tutar. - * Oyuncu kalıbı bozduğunda bu sayaç sıfırlanır. - */ - private int consecutiveRepeatCount = 0; + private volatile long lastRepeatTimestamp; + private volatile int consecutiveRepeatCount = 0; - public LinkedList getMovementHistory() { - return movementHistory; + public synchronized void processMovement(Location current, double maxRadius) { + long now = System.currentTimeMillis(); + + if (confinementStartLocation == null) { + resetConfinement(current, now); + return; + } + + if (!isInsideRadius(current, confinementStartLocation, maxRadius)) { + resetConfinement(current, now); + return; + } + + if (lastMoveLocation != null && Objects.equals(lastMoveLocation.getWorld(), current.getWorld())) { + totalDistanceTraveled += current.distance(lastMoveLocation); + } + + lastMoveLocation = current; } - public long getLastRepeatTimestamp() { - return lastRepeatTimestamp; + private void resetConfinement(Location current, long timestamp) { + this.confinementStartLocation = current; + this.lastMoveLocation = current; + this.confinementStartTime = timestamp; + this.totalDistanceTraveled = 0.0; } - public void setLastRepeatTimestamp(long lastRepeatTimestamp) { - this.lastRepeatTimestamp = lastRepeatTimestamp; + private boolean isInsideRadius(Location loc1, Location loc2, double radius) { + if (!Objects.equals(loc1.getWorld(), loc2.getWorld())) return false; + return loc1.distanceSquared(loc2) <= (radius * radius); } - public int getConsecutiveRepeatCount() { - return consecutiveRepeatCount; + public synchronized long getConfinementDuration() { + return (confinementStartLocation == null) ? 0 : System.currentTimeMillis() - confinementStartTime; } - public void setConsecutiveRepeatCount(int consecutiveRepeatCount) { - this.consecutiveRepeatCount = consecutiveRepeatCount; + public synchronized double getTotalDistanceTraveled() { + return totalDistanceTraveled; } - /** - * Oyuncunun tüm analiz verilerini sıfırlar. - * Bu metot, oyuncu AFK olarak işaretlendiğinde veya bilinçli bir aktivite - * (sohbet, envanter açma vb.) göstererek kalıbı bozduğunda çağrılır. - */ - public void reset() { + public LinkedList getMovementHistory() { + return movementHistory; + } + + public long getLastRepeatTimestamp() { return lastRepeatTimestamp; } + public void setLastRepeatTimestamp(long lastRepeatTimestamp) { this.lastRepeatTimestamp = lastRepeatTimestamp; } + + public int getConsecutiveRepeatCount() { return consecutiveRepeatCount; } + public void setConsecutiveRepeatCount(int consecutiveRepeatCount) { this.consecutiveRepeatCount = consecutiveRepeatCount; } + + public synchronized void reset() { this.movementHistory.clear(); this.lastRepeatTimestamp = 0; this.consecutiveRepeatCount = 0; + this.confinementStartLocation = null; + this.totalDistanceTraveled = 0; + this.lastMoveLocation = null; } } \ No newline at end of file diff --git a/AntiAFK-Core/src/main/java/com/bentahsin/antiafk/listeners/handlers/PlayerMovementListener.java b/AntiAFK-Core/src/main/java/com/bentahsin/antiafk/listeners/handlers/PlayerMovementListener.java index e5c7b20..faedcce 100644 --- a/AntiAFK-Core/src/main/java/com/bentahsin/antiafk/listeners/handlers/PlayerMovementListener.java +++ b/AntiAFK-Core/src/main/java/com/bentahsin/antiafk/listeners/handlers/PlayerMovementListener.java @@ -67,6 +67,9 @@ public void onPlayerMove(PlayerMoveEvent event) { history.removeFirst(); } } + + double radius = configManager.getConfinementRadius(); + data.processMovement(event.getTo(), radius); } } diff --git a/AntiAFK-Core/src/main/java/com/bentahsin/antiafk/managers/ConfigManager.java b/AntiAFK-Core/src/main/java/com/bentahsin/antiafk/managers/ConfigManager.java index 839c291..b7dd292 100644 --- a/AntiAFK-Core/src/main/java/com/bentahsin/antiafk/managers/ConfigManager.java +++ b/AntiAFK-Core/src/main/java/com/bentahsin/antiafk/managers/ConfigManager.java @@ -226,6 +226,40 @@ public class ConfigManager { private boolean behaviorAnalysisEnabled = false; @ConfigPath("behavioral-analysis.history-size-ticks") @Validate(min = 20) private int behaviorHistorySizeTicks = 600; + // --- CONFINEMENT CHECK (AFK POOLS) --- + @ConfigPath("behavioral-analysis.confinement-check.enabled") + @Comment({ + "==================================", + " CONFINEMENT ANALYSIS", + "==================================", + "Detects players who move significantly but never leave a small area.", + "Useful for catching AFK Pools, Piston pushers, and Circle macros." + }) + private boolean confinementCheckEnabled = true; + + @ConfigPath("behavioral-analysis.confinement-check.check-duration") + @Comment({ + "The duration a player must remain within the restricted area to be flagged.", + "Recommended: Max AFK time + a small buffer (e.g., 15m + 5m = 20m)." + }) + @Transform(TimeConverter.class) + private long confinementCheckDurationSeconds = 1200; + + @ConfigPath("behavioral-analysis.confinement-check.radius") + @Comment({ + "The radius in blocks for the confinement area.", + "A value of 3.0 means checking if the player stayed within a 6x6 area." + }) + private double confinementRadius = 3.0; + + @ConfigPath("behavioral-analysis.confinement-check.min-distance-traveled") + @Comment({ + "The minimum total distance (sum of all steps) the player must have traveled.", + "This ensures that someone simply standing still is handled by standard AFK timers.", + "AFK pool users will have a high total distance but low displacement." + }) + private double confinementMinDistance = 100.0; + // --- DISCORD --- @ConfigPath("discord_webhook.enabled") @Comment({ @@ -710,6 +744,10 @@ private Optional checkWorldGuardRegion(Player player) { public double getPreFilterSizeRatio() { return preFilterSizeRatio; } public boolean isBehaviorAnalysisEnabled() { return behaviorAnalysisEnabled; } public int getBehaviorHistorySizeTicks() { return behaviorHistorySizeTicks; } + public boolean isConfinementCheckEnabled() { return confinementCheckEnabled; } + public long getConfinementCheckDurationMillis() { return confinementCheckDurationSeconds * 1000L; } + public double getConfinementRadius() { return confinementRadius; } + public double getConfinementMinDistance() { return confinementMinDistance; } public boolean isDiscordWebhookEnabled() { return discordWebhookEnabled; } public String getDiscordWebhookUrl() { return discordWebhookUrl; } public String getDiscordBotName() { return discordBotName; } diff --git a/AntiAFK-Core/src/main/resources/config.yml b/AntiAFK-Core/src/main/resources/config.yml index 4daefe7..6e34d56 100644 --- a/AntiAFK-Core/src/main/resources/config.yml +++ b/AntiAFK-Core/src/main/resources/config.yml @@ -223,6 +223,22 @@ behavioral-analysis: direction-tolerance: 25.0 similarity-threshold: 0.85 + confinement-check: + enabled: true + + # The duration a player must remain within the restricted area to be flagged. + # Recommended: Max AFK time + a small buffer (e.g., 15m + 5m = 20m). + check-duration: "20m" + + # The radius in blocks for the confinement area. + # A value of 3.0 means checking if the player stayed within a 6x6 area. + radius: 3.0 + + # The minimum total distance (sum of all steps) the player must have traveled. + # This ensures that someone simply standing still is handled by standard AFK timers. + # AFK pool users will have a high total distance but low displacement. + min-distance-traveled: 100.0 + # ================================== # # LEARNING MODE (PATTERN AI) # # ================================== # diff --git a/AntiAFK-Core/src/main/resources/messages.yml b/AntiAFK-Core/src/main/resources/messages.yml index 26a7bc5..5a853ae 100644 --- a/AntiAFK-Core/src/main/resources/messages.yml +++ b/AntiAFK-Core/src/main/resources/messages.yml @@ -159,7 +159,8 @@ behavior: autoclicker_detected: "Auto-clicker tespiti." rapid_world_change: "Sürekli dünya değiştirme tespiti." turing_test_failed: "Bot testi başarısız." - learned_pattern_detected: "Öğrenilmiş bot deseni (%pattern%)" + learned_pattern_detected: "Öğrenilmiş bot deseni tespit edildi. (%pattern%)" + afk_pool_detected: "AFK havuzu veya dairesel hareket tespiti." debug: suspicion: "!§8[AntiAFK-Debug] Yörünge şüphesi. Analiz başlıyor." repetition: "!§8[AntiAFK-Debug] %time%s'lik yörünge tekrarı! Sayaç: %count%/%max_repeats%" diff --git a/AntiAFK-Core/src/test/java/com/bentahsin/antiafk/behavior/ConfinementDetectionTest.java b/AntiAFK-Core/src/test/java/com/bentahsin/antiafk/behavior/ConfinementDetectionTest.java new file mode 100644 index 0000000..987fcec --- /dev/null +++ b/AntiAFK-Core/src/test/java/com/bentahsin/antiafk/behavior/ConfinementDetectionTest.java @@ -0,0 +1,70 @@ +package com.bentahsin.antiafk.behavior; + +import org.bukkit.Location; +import org.bukkit.World; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class ConfinementDetectionTest { + + private PlayerBehaviorData behaviorData; + @Mock private World mockWorld; + @Mock private Location loc1; + @Mock private Location loc2; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + behaviorData = new PlayerBehaviorData(); + + when(loc1.getWorld()).thenReturn(mockWorld); + when(loc2.getWorld()).thenReturn(mockWorld); + } + + @Test + @DisplayName("Pozitif Tespit: Oyuncu dar alanda çok hareket ederse yakalanmalı") + void testConfinementPositive() { + behaviorData.processMovement(loc1, 5.0); + + when(loc1.distance(loc2)).thenReturn(2.0); + when(loc1.distanceSquared(loc2)).thenReturn(4.0); + + for(int i = 0; i < 50; i++) { + behaviorData.processMovement(loc2, 5.0); + behaviorData.processMovement(loc1, 5.0); + } + + assertTrue(behaviorData.getTotalDistanceTraveled() >= 100.0, "Toplam mesafe doğru hesaplanmadı."); + assertTrue(behaviorData.getConfinementDuration() >= 0); + } + + @Test + @DisplayName("Negatif Tespit: Oyuncu alanın dışına çıkarsa sayaç sıfırlanmalı") + void testConfinementResetOnExit() { + behaviorData.processMovement(loc1, 3.0); + + when(loc1.distanceSquared(loc2)).thenReturn(100.0); + + behaviorData.processMovement(loc2, 3.0); + assertEquals(0.0, behaviorData.getTotalDistanceTraveled(), 0.001); + } + + @Test + @DisplayName("Dünya Değişimi: Farklı dünyaya geçerse sistem sıfırlanmalı") + void testWorldChangeReset() { + World otherWorld = mock(World.class); + Location otherWorldLoc = mock(Location.class); + when(otherWorldLoc.getWorld()).thenReturn(otherWorld); + + behaviorData.processMovement(loc1, 5.0); + behaviorData.processMovement(otherWorldLoc, 5.0); + + assertEquals(0.0, behaviorData.getTotalDistanceTraveled(), "Dünya değişince veriler sıfırlanmadı."); + } +} \ No newline at end of file diff --git a/messages_yml/english.yml b/messages_yml/english.yml index a18c601..fae41ef 100644 --- a/messages_yml/english.yml +++ b/messages_yml/english.yml @@ -160,6 +160,8 @@ behavior: autoclicker_detected: "Auto-clicker detected." rapid_world_change: "Rapid world changing detected." turing_test_failed: "Bot test failed." + learned_pattern_detected: "Learned bot pattern detected (%pattern%)" + afk_pool_detected: "AFK pool or circular movement detected." debug: suspicion: "!§8[AntiAFK-Debug] Trajectory suspicion. Analysis started." repetition: "!§8[AntiAFK-Debug] %time%s trajectory repetition! Count: %count%/%max_repeats%" diff --git a/messages_yml/french.yml b/messages_yml/french.yml index 9ef4bb8..7de22ec 100644 --- a/messages_yml/french.yml +++ b/messages_yml/french.yml @@ -159,6 +159,8 @@ behavior: autoclicker_detected: "Auto-clicker detectado." rapid_world_change: "Mudança rápida de mundo detectada." turing_test_failed: "Falhou no teste de bot." + learned_pattern_detected: "Motif de bot appris détecté (%pattern%)" + afk_pool_detected: "Zone AFK ou mouvement circulaire détecté." debug: suspicion: "!§8[AntiAFK-Debug] Suspeita de trajetória. Análise iniciada." repetition: "!§8[AntiAFK-Debug] Repetição de trajetória de %time%s! Contagem: %count%/%max_repeats%" diff --git a/messages_yml/german.yml b/messages_yml/german.yml index 84395f3..00771b1 100644 --- a/messages_yml/german.yml +++ b/messages_yml/german.yml @@ -159,6 +159,8 @@ behavior: autoclicker_detected: "Auto-Klicker erkannt." rapid_world_change: "Schneller Weltwechsel erkannt." turing_test_failed: "Bot-Test fehlgeschlagen." + learned_pattern_detected: "Gelerntes Bot-Muster erkannt (%pattern%)" + afk_pool_detected: "AFK-Pool oder Kreisbewegung erkannt." debug: suspicion: "!§8[AntiAFK-Debug] Verdächtige Trajektorie. Analyse gestartet." repetition: "!§8[AntiAFK-Debug] %time%s Trajektorienwiederholung! Anzahl: %count%/%max_repeats%" diff --git a/messages_yml/polish.yml b/messages_yml/polish.yml index 51fc114..6e95ed8 100644 --- a/messages_yml/polish.yml +++ b/messages_yml/polish.yml @@ -159,6 +159,8 @@ behavior: autoclicker_detected: "Wykryto auto-klicker." rapid_world_change: "Wykryto szybką zmianę świata." turing_test_failed: "Test na bota nie powiódł się." + learned_pattern_detected: "Wykryto wyuczony wzorzec bota (%pattern%)" + afk_pool_detected: "Wykryto AFK pool lub ruch okrężny." debug: suspicion: "!§8[AntiAFK-Debug] Podejrzenie trajektorii. Rozpoczęto analizę." repetition: "!§8[AntiAFK-Debug] Powtórzenie trajektorii %time%s! Licznik: %count%/%max_repeats%" diff --git a/messages_yml/russian.yml b/messages_yml/russian.yml index 7c42a6d..9fce9b5 100644 --- a/messages_yml/russian.yml +++ b/messages_yml/russian.yml @@ -159,6 +159,8 @@ behavior: autoclicker_detected: "Обнаружен авто-кликер." rapid_world_change: "Обнаружена быстрая смена мира." turing_test_failed: "Тест на бота провален." + learned_pattern_detected: "Обнаружен изученный паттерн бота (%pattern%)" + afk_pool_detected: "Обнаружен AFK-pool или круговое движение." debug: suspicion: "!§8[AntiAFK-Debug] Подозрение на траекторию. Анализ начат." repetition: "!§8[AntiAFK-Debug] Повторение траектории %time%с! Счетчик: %count%/%max_repeats%" diff --git a/messages_yml/spanish.yml b/messages_yml/spanish.yml index d97466b..9a7cb20 100644 --- a/messages_yml/spanish.yml +++ b/messages_yml/spanish.yml @@ -159,6 +159,8 @@ behavior: autoclicker_detected: "Auto-clicker detectado." rapid_world_change: "Cambio rápido de mundo detectado." turing_test_failed: "Prueba de bot fallada." + learned_pattern_detected: "Patrón de bot aprendido detectado (%pattern%)" + afk_pool_detected: "Detección de AFK pool o movimiento circular." debug: suspicion: "!§8[AntiAFK-Debug] Sospecha de trayectoria. Análisis iniciado." repetition: "!§8[AntiAFK-Debug] ¡Repetición de trayectoria de %time%s! Conteo: %count%/%max_repeats%"