Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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<Location> history = data.getMovementHistory();
boolean matchFound = false;

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Location> 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<Location> 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<Location> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ public void onPlayerMove(PlayerMoveEvent event) {
history.removeFirst();
}
}

double radius = configManager.getConfinementRadius();
data.processMovement(event.getTo(), radius);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -710,6 +744,10 @@ private Optional<RegionOverride> 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; }
Expand Down
16 changes: 16 additions & 0 deletions AntiAFK-Core/src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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) #
# ================================== #
Expand Down
3 changes: 2 additions & 1 deletion AntiAFK-Core/src/main/resources/messages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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%"
Expand Down
Original file line number Diff line number Diff line change
@@ -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ı.");
}
}
2 changes: 2 additions & 0 deletions messages_yml/english.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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%"
Expand Down
2 changes: 2 additions & 0 deletions messages_yml/french.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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%"
Expand Down
2 changes: 2 additions & 0 deletions messages_yml/german.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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%"
Expand Down
2 changes: 2 additions & 0 deletions messages_yml/polish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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%"
Expand Down
2 changes: 2 additions & 0 deletions messages_yml/russian.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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%"
Expand Down
2 changes: 2 additions & 0 deletions messages_yml/spanish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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%"
Expand Down