Skip to content

Commit

Permalink
Add anti-AFK system for preventing AFK XP gain (#305)
Browse files Browse the repository at this point in the history
  • Loading branch information
Archy-X authored Jul 20, 2024
1 parent 74fc404 commit 5f98216
Show file tree
Hide file tree
Showing 36 changed files with 1,120 additions and 77 deletions.
6 changes: 4 additions & 2 deletions bukkit/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@ tasks.withType<ShadowJar> {
relocate("co.aikar.locales", "dev.aurelium.auraskills.locales")
relocate("de.tr7zw.changeme.nbtapi", "dev.aurelium.auraskills.nbtapi")
relocate("org.bstats", "dev.aurelium.auraskills.bstats")
relocate("com.udojava.evalex", "dev.aurelium.auraskills.evalex")
relocate("com.ezylang.evalex", "dev.aurelium.auraskills.evalex")
relocate("net.kyori", "dev.aurelium.auraskills.kyori")
relocate("com.zaxxer.hikari", "dev.aurelium.auraskills.hikari")
relocate("dev.aurelium.slate", "dev.aurelium.auraskills.slate")

relocate("net.querz", "dev.aurelium.auraskills.querz")
relocate("com.archyx.polyglot", "dev.aurelium.auraskills.polyglot")
relocate("org.atteo.evo.inflector", "dev.aurelium.auraskills.inflector")
exclude("acf-*.properties")

finalizedBy("copyJar")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import dev.aurelium.auraskills.api.skill.Skill;
import dev.aurelium.auraskills.api.skill.Skills;
import dev.aurelium.auraskills.bukkit.ability.BukkitAbilityManager;
import dev.aurelium.auraskills.bukkit.antiafk.AntiAfkManager;
import dev.aurelium.auraskills.bukkit.api.ApiAuraSkillsBukkit;
import dev.aurelium.auraskills.bukkit.api.ApiBukkitRegistrationUtil;
import dev.aurelium.auraskills.bukkit.api.implementation.BukkitApiProvider;
Expand Down Expand Up @@ -158,6 +159,7 @@ public class AuraSkills extends JavaPlugin implements AuraSkillsPlugin {
private ConfirmManager confirmManager;
private PresetManager presetManager;
private PlatformUtil platformUtil;
private AntiAfkManager antiAfkManager;
private boolean nbtApiEnabled;

@Override
Expand Down Expand Up @@ -230,6 +232,7 @@ public void onEnable() {
commandManager = commandRegistrar.registerCommands();
messageProvider.setACFMessages(commandManager);
levelManager = new BukkitLevelManager(this);
antiAfkManager = new AntiAfkManager(this); // Requires config loaded
registerPriorityEvents();
// Enabled bStats
Metrics metrics = new Metrics(this, 21318);
Expand Down Expand Up @@ -441,6 +444,10 @@ public ConfirmManager getConfirmManager() {
return confirmManager;
}

public AntiAfkManager getAntiAfkManager() {
return antiAfkManager;
}

public int getResourceId() {
return 81069;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package dev.aurelium.auraskills.bukkit.antiafk;

import com.ezylang.evalex.Expression;
import com.ezylang.evalex.parser.ParseException;
import dev.aurelium.auraskills.bukkit.AuraSkills;
import dev.aurelium.auraskills.bukkit.user.BukkitUser;
import dev.aurelium.auraskills.common.config.Option;
import dev.aurelium.auraskills.common.message.type.CommandMessage;
import dev.aurelium.auraskills.common.region.BlockPosition;
import dev.aurelium.auraskills.common.user.AntiAfkLog;
import dev.aurelium.auraskills.common.user.User;
import dev.aurelium.auraskills.common.util.text.TextUtil;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.Nullable;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public class AntiAfkManager {

private final AuraSkills plugin;
private final Map<CheckType, Check> checkMap = new HashMap<>();
private Expression logThresholdExpression;

public AntiAfkManager(AuraSkills plugin) {
this.plugin = plugin;

if (!plugin.configBoolean(Option.ANTI_AFK_ENABLED)) return;

loadLogThresholdExpression();
registerChecks();
}

public void reload() {
loadLogThresholdExpression();
// Unregister checks
for (Check existing : checkMap.values()) {
HandlerList.unregisterAll(existing);
}
checkMap.clear();
// Register checks again to account for changed config values
registerChecks();
}

private void loadLogThresholdExpression() {
this.logThresholdExpression = new Expression(plugin.configString(Option.ANTI_AFK_LOG_THRESHOLD));
try {
this.logThresholdExpression.validate();
} catch (ParseException e) {
plugin.logger().warn("Failed to parse anti_afk.log_threshold expression: " + e.getMessage());
e.printStackTrace();
}
}

private void registerChecks() {
for (CheckType type : CheckType.values()) {
registerCheck(type);
}
}

public AuraSkills getPlugin() {
return plugin;
}

@Nullable
public Check getCheck(CheckType type) {
return checkMap.get(type);
}

public CheckData getCheckData(Player player, CheckType type) {
return ((BukkitUser) plugin.getUser(player)).getCheckData(type);
}

public Expression getLogThresholdExpression() {
return logThresholdExpression;
}

public void logAndNotifyFail(Player player, CheckType type, CheckData checkData) {
String message = TextUtil.replace(plugin.getMsg(CommandMessage.ANTIAFK_FAILED, plugin.getDefaultLanguage()),
"{player}", player.getName(),
"{check}", type.name(),
"{count}", String.valueOf(checkData.getCount()));

Location loc = player.getLocation();
BlockPosition coords = new BlockPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
@Nullable World world = loc.getWorld();
String worldName = world != null ? world.getName() : "";

// Log message
User user = plugin.getUser(player);
var log = new AntiAfkLog(System.currentTimeMillis(), message, coords, worldName);
user.getSessionAntiAfkLogs().add(log);

// Send to online players with notify permission
for (Player notified : Bukkit.getOnlinePlayers()) {
if (!notified.hasPermission("auraskills.antiafk.notify")) {
continue;
}

notified.sendMessage(message);
}
}

private void registerCheck(CheckType type) {
Class<? extends Check> checkClass = type.getCheckClass();
try {
Constructor<?> constructor = checkClass.getDeclaredConstructor(CheckType.class, AntiAfkManager.class);
Object checkObj = constructor.newInstance(type, this);
if (checkObj instanceof Check check) {
plugin.getServer().getPluginManager().registerEvents(check, plugin);
checkMap.put(type, check);
}
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
plugin.logger().warn("Failed to register check of type " + type);
e.printStackTrace();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package dev.aurelium.auraskills.bukkit.antiafk;

import com.ezylang.evalex.EvaluationException;
import com.ezylang.evalex.parser.ParseException;
import dev.aurelium.auraskills.bukkit.AuraSkills;
import dev.aurelium.auraskills.common.config.Option;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;

import java.util.Locale;

public class Check implements Listener {

private final CheckType type;
private final AntiAfkManager manager;
private final AuraSkills plugin;
private final String CONFIG_PREFIX;
private final Option ENABLED_OPTION;
private final int LOG_THRESHOLD;

public Check(CheckType type, AntiAfkManager manager) {
this.type = type;
this.manager = manager;
this.plugin = manager.getPlugin();
this.CONFIG_PREFIX = "ANTI_AFK_CHECKS_" + type.toString() + "_";
this.ENABLED_OPTION = Option.valueOf(CONFIG_PREFIX + "ENABLED");
int minCount = optionInt("min_count");
int logThresholdParsed;
try {
logThresholdParsed = manager.getLogThresholdExpression()
.with("min_count", minCount)
.evaluate()
.getNumberValue()
.intValue();
} catch (EvaluationException | ParseException e) {
plugin.logger().warn("Failed to evaluate anti_afk.log_threshold expression: " + e.getMessage());
e.printStackTrace();
logThresholdParsed = minCount; // Fallback value
}
this.LOG_THRESHOLD = logThresholdParsed;
}

public int getLogThreshold() {
return LOG_THRESHOLD;
}

protected CheckData getCheckData(Player player) {
return manager.getCheckData(player, type);
}

protected void logFail(Player player) {
if (!plugin.configBoolean(Option.ANTI_AFK_LOGGING_ENABLED)) return;

CheckData checkData = getCheckData(player);
if (checkData.getLogCount() >= LOG_THRESHOLD) {
manager.logAndNotifyFail(player, type, checkData);
checkData.resetLogCount();
}
}

protected boolean isDisabled() {
return !plugin.configBoolean(ENABLED_OPTION);
}

protected int optionInt(String option) {
return plugin.configInt(Option.valueOf(CONFIG_PREFIX + option.toUpperCase(Locale.ROOT)));
}

protected double optionDouble(String option) {
return plugin.configDouble(Option.valueOf(CONFIG_PREFIX + option.toUpperCase(Locale.ROOT)));
}

protected String optionString(String option) {
return plugin.configString(Option.valueOf(CONFIG_PREFIX + option.toUpperCase(Locale.ROOT)));
}

protected boolean optionBoolean(String option) {
return plugin.configBoolean(Option.valueOf(CONFIG_PREFIX + option.toUpperCase(Locale.ROOT)));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package dev.aurelium.auraskills.bukkit.antiafk;

import java.util.HashMap;
import java.util.Map;

public class CheckData {

private final Map<String, Object> cache = new HashMap<>();

public <T> T getCache(String key, Class<T> type, T def) {
Object val = cache.get(key);
if (val != null) {
return type.cast(val);
} else {
return def;
}
}

public void setCache(String key, Object value) {
cache.put(key, value);
}

public int getCount() {
return getCache("count", Integer.class, 0);
}

public void incrementCount() {
setCache("count", getCache("count", Integer.class, 0) + 1);
setCache("log_count", getCache("log_count", Integer.class, 0) + 1);
}

public void resetCount() {
setCache("count", 0);
setCache("log_count", 0);
}

public int getLogCount() {
return getCache("log_count", Integer.class, 0);
}

public void resetLogCount() {
setCache("log_count", 0);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package dev.aurelium.auraskills.bukkit.antiafk;

import dev.aurelium.auraskills.bukkit.antiafk.checks.*;

public enum CheckType {

BLOCK_A(BlockA.class),
DAMAGE_A(DamageA.class),
DAMAGE_B(DamageB.class),
DAMAGE_C(DamageC.class),
ENTITY_A(EntityA.class),
ENTITY_B(EntityB.class),
ENTITY_C(EntityC.class),
FISHING_A(FishingA.class);

private final Class<? extends Check> checkClass;

CheckType(Class<? extends Check> checkClass) {
this.checkClass = checkClass;
}

public Class<? extends Check> getCheckClass() {
return checkClass;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package dev.aurelium.auraskills.bukkit.antiafk.checks;

import dev.aurelium.auraskills.api.event.skill.XpGainEvent;
import dev.aurelium.auraskills.api.source.type.BlockXpSource;
import dev.aurelium.auraskills.bukkit.antiafk.AntiAfkManager;
import dev.aurelium.auraskills.bukkit.antiafk.Check;
import dev.aurelium.auraskills.bukkit.antiafk.CheckType;
import dev.aurelium.auraskills.bukkit.antiafk.handler.FacingHandler;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;

public class BlockA extends Check {

private final FacingHandler handler;

public BlockA(CheckType type, AntiAfkManager manager) {
super(type, manager);
this.handler = new FacingHandler(optionInt("min_count"));
}

@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
public void onXpGain(XpGainEvent event) {
if (isDisabled() || !(event.getSource() instanceof BlockXpSource)) return;

Player player = event.getPlayer();
if (handler.failsCheck(getCheckData(player), player)) {
event.setCancelled(true);
logFail(player);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package dev.aurelium.auraskills.bukkit.antiafk.checks;

import dev.aurelium.auraskills.api.event.skill.XpGainEvent;
import dev.aurelium.auraskills.api.source.type.DamageXpSource;
import dev.aurelium.auraskills.bukkit.antiafk.AntiAfkManager;
import dev.aurelium.auraskills.bukkit.antiafk.Check;
import dev.aurelium.auraskills.bukkit.antiafk.CheckType;
import dev.aurelium.auraskills.bukkit.antiafk.handler.PositionHandler;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;

public class DamageA extends Check {

private final PositionHandler handler;

public DamageA(CheckType type, AntiAfkManager manager) {
super(type, manager);
this.handler = new PositionHandler(optionDouble("max_distance"), optionInt("min_count"));
}

@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
public void onXpGain(XpGainEvent event) {
if (isDisabled() || !(event.getSource() instanceof DamageXpSource)) return;

Player player = event.getPlayer();
if (handler.failsCheck(getCheckData(player), player)) {
event.setCancelled(true);
logFail(player);
}
}

}
Loading

0 comments on commit 5f98216

Please sign in to comment.