From 092d33beb35f868c3b229621298f627580181700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 29 Mar 2025 22:03:12 +0100 Subject: [PATCH 01/10] prototype for grief detection --- .../tooling/antiGrief/AntiGrief.java | 49 ++++++++++++++++++ .../antiGrief/player/PlayerGriefListener.java | 51 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/main/java/eu/mhsl/craftattack/spawn/appliances/tooling/antiGrief/AntiGrief.java create mode 100644 src/main/java/eu/mhsl/craftattack/spawn/appliances/tooling/antiGrief/player/PlayerGriefListener.java diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/tooling/antiGrief/AntiGrief.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/tooling/antiGrief/AntiGrief.java new file mode 100644 index 0000000..5c4c911 --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/tooling/antiGrief/AntiGrief.java @@ -0,0 +1,49 @@ +package eu.mhsl.craftattack.spawn.appliances.tooling.antiGrief; + +import eu.mhsl.craftattack.spawn.appliance.Appliance; +import eu.mhsl.craftattack.spawn.appliances.tooling.antiGrief.player.PlayerGriefListener; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.BlockDisplay; +import org.bukkit.entity.ItemDisplay; +import org.bukkit.event.Listener; +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.Transformation; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class AntiGrief extends Appliance { + private final Map blockRegistry = new HashMap<>(); + + public void addTracking(Block block, UUID player) { + this.blockRegistry.put(block, player); + block.getLocation().getWorld().spawn(block.getLocation(), ItemDisplay.class, itemDisplay -> { + itemDisplay.setItemStack(ItemStack.of(Material.FIRE_CHARGE)); + itemDisplay.teleport(block.getLocation().add(0.5, 0.5, 0.5)); + }); + } + + public @Nullable UUID getTracked(Block block) { + return this.blockRegistry.get(block); + } + + public void addDestroyed(Block block, UUID player) { + block.getLocation().getWorld().spawn(block.getLocation().add(0.5, 0.5, 0.5), BlockDisplay.class, blockDisplay -> { + blockDisplay.setBlock(Material.GOLD_BLOCK.createBlockData()); + + Transformation transformation = blockDisplay.getTransformation(); + transformation.getScale().set(0.3); + blockDisplay.setTransformation(transformation); + }); + } + + @Override + protected @NotNull List listeners() { + return List.of(new PlayerGriefListener()); + } +} diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/tooling/antiGrief/player/PlayerGriefListener.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/tooling/antiGrief/player/PlayerGriefListener.java new file mode 100644 index 0000000..23052ca --- /dev/null +++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/tooling/antiGrief/player/PlayerGriefListener.java @@ -0,0 +1,51 @@ +package eu.mhsl.craftattack.spawn.appliances.tooling.antiGrief.player; + +import eu.mhsl.craftattack.spawn.appliance.ApplianceListener; +import eu.mhsl.craftattack.spawn.appliances.tooling.antiGrief.AntiGrief; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.event.EventHandler; +import org.bukkit.event.block.BlockBurnEvent; +import org.bukkit.event.block.BlockIgniteEvent; +import org.bukkit.event.block.BlockSpreadEvent; + +import java.util.UUID; + +public class PlayerGriefListener extends ApplianceListener { + @EventHandler + public void onIgnite(BlockIgniteEvent event) { + Bukkit.broadcast(Component.text(event.getCause().toString())); + switch(event.getCause()) { + case LAVA: + case SPREAD: + case EXPLOSION: + if(event.getIgnitingBlock() == null) return; + UUID ignitedBy = this.getAppliance().getTracked(event.getIgnitingBlock()); + this.getAppliance().addTracking(event.getBlock(), ignitedBy); + break; + + case FLINT_AND_STEEL: + case ENDER_CRYSTAL: + case ARROW: + case FIREBALL: + if(event.getPlayer() == null) return; + this.getAppliance().addTracking(event.getBlock(), event.getPlayer().getUniqueId()); + break; + } + } + + @EventHandler + public void onSpread(BlockSpreadEvent event) { + if(!event.getBlock().getType().equals(Material.FIRE)) return; + UUID ignitedBy = this.getAppliance().getTracked(event.getBlock()); + this.getAppliance().addTracking(event.getBlock(), ignitedBy); + } + + @EventHandler + public void onDestroy(BlockBurnEvent event) { + UUID ignitedBy = this.getAppliance().getTracked(event.getIgnitingBlock()); + if(ignitedBy == null) return; + this.getAppliance().addDestroyed(event.getBlock(), ignitedBy); + } +} From dc1b5957f66a70fd628cfa117638b5a7cdd41d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 20 Sep 2025 11:35:19 +0200 Subject: [PATCH 02/10] WIP: different method for grief detection --- .../security/antiGrief/AntiGrief.java | 33 ++++++++++++++++ .../commands/GriefOverviewCommand.java | 39 +++++++++++++++++++ .../antiGrief/player/PlayerGriefListener.java | 20 ++++++++++ 3 files changed, 92 insertions(+) create mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java create mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/commands/GriefOverviewCommand.java create mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/player/PlayerGriefListener.java diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java new file mode 100644 index 0000000..5995eb6 --- /dev/null +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java @@ -0,0 +1,33 @@ +package eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief; + +import eu.mhsl.craftattack.spawn.core.appliance.Appliance; +import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.player.PlayerGriefListener; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; +import java.util.*; + +public class AntiGrief extends Appliance { + public record GriefIncident(Location location, @Nullable Block block) { + public static final Long timestamp = System.currentTimeMillis(); + } + private final Map> griefRegistry = new HashMap<>(); + + public void addTracking(Player player, GriefIncident incident) { + this.griefRegistry.computeIfAbsent(player.getUniqueId(), uuid -> new ArrayList<>()); + this.griefRegistry.get(player.getUniqueId()).add(incident); + } + + public Map> getGriefRegistry() { + return this.griefRegistry; + } + + @Override + protected @NotNull List listeners() { + return List.of(new PlayerGriefListener()); + } +} diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/commands/GriefOverviewCommand.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/commands/GriefOverviewCommand.java new file mode 100644 index 0000000..d803910 --- /dev/null +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/commands/GriefOverviewCommand.java @@ -0,0 +1,39 @@ +package eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.commands; + +import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand; +import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.AntiGrief; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentBuilder; +import net.kyori.adventure.text.ScopedComponent; +import net.kyori.adventure.text.TextComponent; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class GriefOverviewCommand extends ApplianceCommand { + public GriefOverviewCommand() { + super("griefOverview"); + } + + @Override + protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception { + sender.sendMessage( + this.getAppliance().getGriefRegistry().entrySet().stream() + .map(griefEntry -> { + List entries = griefEntry.getValue().stream() + .map(incident -> Component.text(incident.location().toString())) + .toList(); + ComponentBuilder builder = Component.text() + .append(Component.text(griefEntry.getKey().toString())) + .append(Component.text(": ")); + entries.forEach(builder::append); + + return builder.build(); + }) + .reduce(ScopedComponent::append) + .orElseThrow() + ); + } +} diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/player/PlayerGriefListener.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/player/PlayerGriefListener.java new file mode 100644 index 0000000..8382637 --- /dev/null +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/player/PlayerGriefListener.java @@ -0,0 +1,20 @@ +package eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.player; + +import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener; +import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.AntiGrief; +import org.bukkit.event.EventHandler; +import org.bukkit.event.block.BlockIgniteEvent; +import org.bukkit.event.entity.ExplosionPrimeEvent; + +public class PlayerGriefListener extends ApplianceListener { + @EventHandler + public void onIgnite(BlockIgniteEvent event) { + if(event.getPlayer() == null) return; + this.getAppliance().addTracking(event.getPlayer(), new AntiGrief.GriefIncident(event.getBlock().getLocation(), event.getBlock())); + } + + @EventHandler + public void onTnt(ExplosionPrimeEvent event) { + + } +} From 0d18b81399a4f751e821d036acb6cb095ef7dae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 21 Sep 2025 01:06:29 +0200 Subject: [PATCH 03/10] changed coloring of VaroRank --- .../appliances/metaGameplay/varoRank/VaroRank.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/metaGameplay/varoRank/VaroRank.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/metaGameplay/varoRank/VaroRank.java index b7b2abd..3a3e708 100644 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/metaGameplay/varoRank/VaroRank.java +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/metaGameplay/varoRank/VaroRank.java @@ -17,9 +17,9 @@ public class VaroRank extends Appliance implements DisplayName.Prefixed { private List winners = new ArrayList<>(); private List mostKills = new ArrayList<>(); - private final Component winnerBadge = Component.text("\uD83D\uDC51", NamedTextColor.YELLOW) + private final Component winnerBadge = Component.text("\uD83D\uDC51", NamedTextColor.GOLD) .hoverEvent(HoverEvent.showText(Component.text("Hat zusammen mit seinem Team Varo gewonnen"))); - private final Component killBadge = Component.text("\uD83D\uDDE1", NamedTextColor.RED) + private final Component killBadge = Component.text("\uD83D\uDDE1", NamedTextColor.GOLD) .hoverEvent(HoverEvent.showText(Component.text("Hat zusammen mit seinem Team die meisten Kills in Varo"))); public VaroRank() { From 74f17e1b6daf7c5fdbfe676730a8fe98c119b140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 21 Sep 2025 01:54:20 +0200 Subject: [PATCH 04/10] WIP: AntiGrief --- .../security/antiGrief/AntiGrief.java | 300 +++++++++++++++++- .../commands/GriefOverviewCommand.java | 4 +- .../listener/BlockRelatedGriefListener.java | 38 +++ .../listener/EntityRelatedGriefListener.java | 142 +++++++++ .../ExplosionRelatedGriefListener.java | 105 ++++++ .../listener/FireRelatedGriefListener.java | 70 ++++ .../listener/LiquidRelatedGriefListener.java | 59 ++++ .../antiGrief/player/PlayerGriefListener.java | 20 -- 8 files changed, 705 insertions(+), 33 deletions(-) create mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/BlockRelatedGriefListener.java create mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/EntityRelatedGriefListener.java create mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/ExplosionRelatedGriefListener.java create mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/FireRelatedGriefListener.java create mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/LiquidRelatedGriefListener.java delete mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/player/PlayerGriefListener.java diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java index 5995eb6..a333d45 100644 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java @@ -1,33 +1,311 @@ package eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief; +import eu.mhsl.craftattack.spawn.core.Main; import eu.mhsl.craftattack.spawn.core.appliance.Appliance; -import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.player.PlayerGriefListener; +import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.listener.*; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.util.Ticks; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; import org.bukkit.Location; +import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.entity.Player; +import org.bukkit.event.Event; import org.bukkit.event.Listener; import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static net.kyori.adventure.text.Component.text; public class AntiGrief extends Appliance { - public record GriefIncident(Location location, @Nullable Block block) { - public static final Long timestamp = System.currentTimeMillis(); - } - private final Map> griefRegistry = new HashMap<>(); + public record GriefIncident( + long timestamp, + UUID worldId, + int x, + int y, + int z, + String event, + String data, + Severity severity + ) { + public GriefIncident(Location loc, Event event, @Nullable Object data, Severity severity) { + this( + System.currentTimeMillis(), + loc.getWorld().getUID(), + loc.getBlockX(), + loc.getBlockY(), + loc.getBlockZ(), + event.getEventName(), + String.valueOf(data), + severity + ); + } - public void addTracking(Player player, GriefIncident incident) { - this.griefRegistry.computeIfAbsent(player.getUniqueId(), uuid -> new ArrayList<>()); - this.griefRegistry.get(player.getUniqueId()).add(incident); + public enum Severity { + /** + * No direct severity, but possible beginning of an incident + */ + INFO(0.5f), + + /** + * Direct interaction which can lead to damage + */ + LIGHT(1), + + /** + * Direkt interaction which can spread to severe incidents + */ + MODERATE(3), + + /** + * Direct and most likely harmful interaction + */ + SEVERE(5); + + public final float weight; + Severity(float weight) { + this.weight = weight; + } + } } - public Map> getGriefRegistry() { - return this.griefRegistry; + private static final class AreaState { + final UUID worldId; + final int chunkX, chunkZ; + + /** Rolling bucket scores for Spike Detection. */ + final NavigableMap scores = new ConcurrentSkipListMap<>(); + /** Incidents per Bucket */ + final Map> incidentsByBucket = new ConcurrentHashMap<>(); + + volatile double ema = 0.0; + volatile long lastAlertAt = 0L; + + AreaState(UUID worldId, int chunkX, int chunkZ) { + this.worldId = worldId; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + } + + void addIncident(GriefIncident incident) { + long b = incident.timestamp / BUCKET_DURATION_MS; + this.scores.merge(b, (double) incident.severity.weight, Double::sum); + this.incidentsByBucket + .computeIfAbsent(b, k -> Collections.synchronizedList(new ArrayList<>())) + .add(incident); + } + + double currentScore(long bucketIdx) { + return this.scores.getOrDefault(bucketIdx, 0.0); + } + + void prune(long bucket) { + long oldest = bucket - BUCKETS_PER_CHUNK; + this.scores.headMap(oldest, true).clear(); + this.incidentsByBucket.keySet().removeIf(b -> b < oldest); + } + + boolean isEmpty() { + return this.scores.isEmpty() && this.incidentsByBucket.isEmpty(); + } + } + + /** Duration of a time bucket in milliseconds. */ + private static final long BUCKET_DURATION_MS = 60 * 1000; + /** Number of buckets kept in memory per area. Defines analysis window length. */ + private static final int BUCKETS_PER_CHUNK = 30; + /** Maximum retention time for individual incidents in milliseconds. */ + private static final long INCIDENT_RETAIN_MS = 60 * 60 * 1000; + /** Spike factor against EMA baseline. Triggers if current score >= baseline * FACTOR_SPIKE. */ + private static final double FACTOR_SPIKE = 3.0; + /** Absolute threshold for spike detection. Triggers if current score exceeds this value. */ + private static final double HARD_THRESHOLD = 20.0; + /** Cooldown time in ms to suppress repeated alerts for the same area. */ + private static final long ALERT_COOLDOWN_MS = 2 * 60 * 1000; + /** Smoothing factor for EMA baseline. Lower = smoother, higher = more reactive. 0.0 < EMA_ALPHA <= 1.0 */ + private static final double EMA_ALPHA = 0.3; + + /** Stores direct incidents mapped by player UUID. */ + private final Map> directGriefRegistry = new ConcurrentHashMap<>(); + /** Stores passive incidents mapped by chunk key. */ + private final Map> passiveGriefRegistry = new ConcurrentHashMap<>(); + + /** Stores scores by area */ + private final Map areas = new ConcurrentHashMap<>(); + + public void trackDirect(Player player, GriefIncident incident) { + this.directGriefRegistry + .computeIfAbsent(player.getUniqueId(), uuid -> ConcurrentHashMap.newKeySet()) + .add(incident); + + this.trackPassive(player.getLocation().getChunk(), incident); + } + + public void trackPassive(Chunk chunk, GriefIncident incident) { + this.passiveGriefRegistry + .computeIfAbsent(chunk.getChunkKey(), aLong -> ConcurrentHashMap.newKeySet()) + .add(incident); + + final long areaKey = this.packArea(incident.worldId, chunk.getX(), chunk.getZ()); + this.areas + .computeIfAbsent(areaKey, key -> new AreaState(incident.worldId, chunk.getX(), chunk.getZ())) + .addIncident(incident); + + Bukkit.broadcast(text(String.format("%d, %d: %s - %s: {%s}", chunk.getX(), chunk.getZ(), incident.severity, incident.event, incident.data))); + } + + @Override + public void onEnable() { + Bukkit.getScheduler().runTaskTimerAsynchronously(Main.instance(), () -> { + final long now = System.currentTimeMillis(); + final long bucketIdx = this.bucketIdx(now); + + this.areas.forEach((areaKey, st) -> { + final double currentScore = st.currentScore(bucketIdx); + if (currentScore <= 0.0) return; + + final double base = (st.ema == 0.0) ? currentScore : st.ema; + final double newBase = EMA_ALPHA * currentScore + (1 - EMA_ALPHA) * base; + st.ema = newBase; + + boolean spike = currentScore >= HARD_THRESHOLD || currentScore >= base * FACTOR_SPIKE; + if (spike && (now - st.lastAlertAt) >= ALERT_COOLDOWN_MS) { + st.lastAlertAt = now; + Bukkit.getScheduler().runTask(Main.instance(), () -> + this.alertAdmins(areaKey, bucketIdx, currentScore, newBase) + ); + } + }); + }, Ticks.TICKS_PER_SECOND, Ticks.TICKS_PER_SECOND); + + Bukkit.getScheduler().runTaskTimerAsynchronously(Main.instance(), () -> { + final long cutoff = System.currentTimeMillis() - INCIDENT_RETAIN_MS; + final long nowBucket = this.bucketIdx(System.currentTimeMillis()); + + this.directGriefRegistry.entrySet().removeIf(e -> { + e.getValue().removeIf(inc -> inc.timestamp < cutoff); + return e.getValue().isEmpty(); + }); + + this.passiveGriefRegistry.entrySet().removeIf(e -> { + e.getValue().removeIf(inc -> inc.timestamp < cutoff); + return e.getValue().isEmpty(); + }); + + this.areas.entrySet().removeIf(en -> { + AreaState state = en.getValue(); + state.prune(nowBucket); + return state.isEmpty(); + }); + }, Ticks.TICKS_PER_SECOND * 30, Ticks.TICKS_PER_SECOND * 30); + } + + private void alertAdmins(long areaKey, long bucketIdx, double curr, double baseline) { + AreaState meta = this.areas.get(areaKey); + if (meta == null) return; + + int cx = meta.chunkX, cz = meta.chunkZ; + UUID worldId = meta.worldId; + + World world = Bukkit.getWorld(worldId); + if (world == null) return; + + int bx = (cx << 4) + 8; + int bz = (cz << 4) + 8; + int by = world.getHighestBlockYAt(bx, bz); + + Location center = new Location(world, bx + 0.5, by + 1.0, bz + 0.5); + List nearest = Bukkit.getOnlinePlayers().stream() + .filter(p -> p.getWorld().equals(world)) + .sorted(Comparator.comparingDouble(p -> p.getLocation().distanceSquared(center))) + .limit(3) + .collect(Collectors.toList()); + + String playersHover = nearest.isEmpty() + ? "Keine Spieler in der Nähe" + : String.join("\n", nearest.stream() + .map(p -> String.format("- %s (%.1fm)", p.getName(), Math.sqrt(p.getLocation().distanceSquared(center)))) + .toList()); + + List incidents = meta.incidentsByBucket.getOrDefault(bucketIdx, List.of()); + String incidentsHover = incidents.isEmpty() + ? "Keine Details" + : String.join("\n", incidents.stream().limit(20) + .map(i -> String.format("• %s · %s · %s", i.event, i.severity, i.data)) + .toList()); + + Component coords = text("[" + cx + ", " + cz + "]") + .hoverEvent(HoverEvent.showText(text( + "Chunk: [" + cx + ", " + cz + "]\n" + + "Block: [" + bx + ", " + by + ", " + bz + "]\n\n" + + "Nächste Spieler:\n" + playersHover + ))) + .clickEvent(ClickEvent.suggestCommand(String.format("/tp %d %d %d", bx, by, bz))); + + Component incCount = text(incidents.size() + " Incidents") + .hoverEvent(HoverEvent.showText(text(incidentsHover))); + + Component msg = text("Möglicher Grief in ").append(coords) + .append(text(" - score=" + String.format("%.1f", curr))) + .append(text(" base=" + String.format("%.1f", baseline))) + .append(text(" - ")).append(incCount); + + Bukkit.getOnlinePlayers().stream() + .filter(p -> p.hasPermission("antigrief.alert")) + .forEach(p -> p.sendMessage(msg)); + + Bukkit.getConsoleSender().sendMessage( + String.format("[AntiGrief] Alert %s score=%.1f base=%.1f incidents=%d @ [%d,%d]", + world.getName(), curr, baseline, incidents.size(), cx, cz)); + } + + private long bucketIdx(long timestamp) { + return timestamp / AntiGrief.BUCKET_DURATION_MS; + } + + private long packArea(UUID worldId, int chunkX, int chunkZ) { + long chunkKey = (((long)chunkX) << 32) ^ (chunkZ & 0xffffffffL); + return worldId.getMostSignificantBits() ^ worldId.getLeastSignificantBits() ^ chunkKey; + } + + public Stream getSurroundingBlocks(Location location) { + Block center = location.getBlock(); + World world = center.getWorld(); + int x = center.getX(); + int y = center.getY(); + int z = center.getZ(); + + return Stream.of( + world.getBlockAt(x + 1, y, z), + world.getBlockAt(x - 1, y, z), + world.getBlockAt(x, y + 1, z), + world.getBlockAt(x, y - 1, z), + world.getBlockAt(x, y, z + 1), + world.getBlockAt(x, y, z - 1) + ); + } + + public Map> getDirectGriefRegistry() { + return this.directGriefRegistry; } @Override protected @NotNull List listeners() { - return List.of(new PlayerGriefListener()); + return List.of( + new BlockRelatedGriefListener(), + new ExplosionRelatedGriefListener(), + new FireRelatedGriefListener(), + new LiquidRelatedGriefListener(), + new EntityRelatedGriefListener() + ); } } diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/commands/GriefOverviewCommand.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/commands/GriefOverviewCommand.java index d803910..700d8ba 100644 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/commands/GriefOverviewCommand.java +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/commands/GriefOverviewCommand.java @@ -20,10 +20,10 @@ public class GriefOverviewCommand extends ApplianceCommand { @Override protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception { sender.sendMessage( - this.getAppliance().getGriefRegistry().entrySet().stream() + this.getAppliance().getDirectGriefRegistry().entrySet().stream() .map(griefEntry -> { List entries = griefEntry.getValue().stream() - .map(incident -> Component.text(incident.location().toString())) + .map(incident -> Component.text(incident.x())) .toList(); ComponentBuilder builder = Component.text() .append(Component.text(griefEntry.getKey().toString())) diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/BlockRelatedGriefListener.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/BlockRelatedGriefListener.java new file mode 100644 index 0000000..510a19d --- /dev/null +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/BlockRelatedGriefListener.java @@ -0,0 +1,38 @@ +package eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.listener; + +import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener; +import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.AntiGrief; +import org.bukkit.block.Container; +import org.bukkit.event.EventHandler; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.inventory.Inventory; + +import java.util.Arrays; +import java.util.Objects; + + +public class BlockRelatedGriefListener extends ApplianceListener { + @EventHandler + public void containerBlockBreak(BlockBreakEvent event) { + if(!(event.getBlock().getState() instanceof Container container)) return; + Inventory containerInv = container.getInventory(); + if(containerInv.isEmpty()) return; + + long itemCount = Arrays.stream(containerInv.getStorageContents()) + .filter(Objects::nonNull) + .filter(itemStack -> !itemStack.isEmpty()) + .count(); + + this.getAppliance().trackDirect( + event.getPlayer(), + new AntiGrief.GriefIncident( + event.getBlock().getLocation(), + event, + event.getBlock().getType(), + itemCount > containerInv.getSize() / 2 + ? AntiGrief.GriefIncident.Severity.SEVERE + : AntiGrief.GriefIncident.Severity.MODERATE + ) + ); + } +} diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/EntityRelatedGriefListener.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/EntityRelatedGriefListener.java new file mode 100644 index 0000000..de1afab --- /dev/null +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/EntityRelatedGriefListener.java @@ -0,0 +1,142 @@ +package eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.listener; + +import com.destroystokyo.paper.event.entity.CreeperIgniteEvent; +import eu.mhsl.craftattack.spawn.core.Main; +import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener; +import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.AntiGrief; +import org.bukkit.Bukkit; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Item; +import org.bukkit.entity.Tameable; +import org.bukkit.entity.Villager; +import org.bukkit.event.EventHandler; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityDeathEvent; + +import java.util.List; +import java.util.Set; + +public class EntityRelatedGriefListener extends ApplianceListener { + @EventHandler + public void buildWither(CreatureSpawnEvent event) { + if(!event.getSpawnReason().equals(CreatureSpawnEvent.SpawnReason.BUILD_WITHER)) return; + this.getAppliance().trackPassive( + event.getEntity().getLocation().getChunk(), + new AntiGrief.GriefIncident( + event.getEntity().getLocation(), + event, + event.getEntity().getType(), + AntiGrief.GriefIncident.Severity.MODERATE + ) + ); + } + + @EventHandler + public void creeperPurposelyIgnite(CreeperIgniteEvent event) { + this.getAppliance().trackPassive( + event.getEntity().getChunk(), + new AntiGrief.GriefIncident( + event.getEntity().getLocation(), + event, + event.getEntity().getType(), + AntiGrief.GriefIncident.Severity.SEVERE + ) + ); + } + + @EventHandler + public void villagerDeath(EntityDeathEvent event) { + if(!(event.getEntity() instanceof Villager villager)) return; + + if(event.getEntity().getLastDamageCause() != null) { + EntityDamageEvent lastDamage = event.getEntity().getLastDamageCause(); + + List suspiciousCauses = List.of( + EntityDamageEvent.DamageCause.FIRE, + EntityDamageEvent.DamageCause.LAVA, + EntityDamageEvent.DamageCause.PROJECTILE, + EntityDamageEvent.DamageCause.ENTITY_ATTACK, + EntityDamageEvent.DamageCause.FIRE_TICK + ); + if(!suspiciousCauses.contains(lastDamage.getCause())) return; + } + + this.getAppliance().trackPassive( + villager.getChunk(), + new AntiGrief.GriefIncident( + villager.getLocation(), + event, + List.of(villager.getVillagerType(), String.valueOf(villager.getLastDamageCause())), + AntiGrief.GriefIncident.Severity.LIGHT + ) + ); + } + + @EventHandler + public void petKilled(EntityDeathEvent event) { + Set petEntities = Set.of( + EntityType.SNIFFER, + EntityType.WOLF, + EntityType.AXOLOTL, + EntityType.ALLAY, + EntityType.CAMEL, + EntityType.PARROT, + EntityType.CAT, + EntityType.OCELOT, + EntityType.HORSE, + EntityType.DONKEY, + EntityType.MULE, + EntityType.LLAMA, + EntityType.FOX, + EntityType.TURTLE, + EntityType.PANDA, + EntityType.GOAT, + EntityType.BEE + ); + + if(!petEntities.contains(event.getEntity().getType())) return; + this.getAppliance().trackPassive( + event.getEntity().getChunk(), + new AntiGrief.GriefIncident( + event.getEntity().getLocation(), + event, + event.getEntity().getType(), + event.getEntity() instanceof Tameable tameable + ? tameable.isTamed() ? AntiGrief.GriefIncident.Severity.SEVERE : AntiGrief.GriefIncident.Severity.MODERATE + : AntiGrief.GriefIncident.Severity.LIGHT + ) + ); + } + + @EventHandler + public void itemBurned(EntityDamageEvent event) { + if(!(event.getEntity() instanceof Item item)) return; + int amount = item.getItemStack().getAmount(); + int half = item.getItemStack().getMaxStackSize() / 2; + if (amount < half / 2) return; + List forbiddenCauses = List.of( + EntityDamageEvent.DamageCause.FIRE, + EntityDamageEvent.DamageCause.FIRE_TICK, + EntityDamageEvent.DamageCause.LAVA, + EntityDamageEvent.DamageCause.HOT_FLOOR, + EntityDamageEvent.DamageCause.CAMPFIRE + ); + if(forbiddenCauses.contains(event.getCause())) return; + + Bukkit.getScheduler().runTaskLater(Main.instance(), () -> { + if (item.isValid() || !item.isDead()) return; + this.getAppliance().trackPassive( + event.getEntity().getChunk(), + new AntiGrief.GriefIncident( + event.getEntity().getLocation(), + event, + event.getEntity().getType(), + amount > half + ? AntiGrief.GriefIncident.Severity.MODERATE + : AntiGrief.GriefIncident.Severity.LIGHT + ) + ); + }, 1L); + } +} diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/ExplosionRelatedGriefListener.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/ExplosionRelatedGriefListener.java new file mode 100644 index 0000000..89d3b67 --- /dev/null +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/ExplosionRelatedGriefListener.java @@ -0,0 +1,105 @@ +package eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.listener; + +import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener; +import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.AntiGrief; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.entity.minecart.ExplosiveMinecart; +import org.bukkit.event.EventHandler; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.block.TNTPrimeEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.entity.EntityPlaceEvent; +import org.bukkit.event.vehicle.VehicleCreateEvent; + +import java.util.List; + +public class ExplosionRelatedGriefListener extends ApplianceListener { + @EventHandler + public void tntPlacement(BlockPlaceEvent event) { + if(!event.getBlockPlaced().getType().equals(Material.TNT)) return; + this.getAppliance().trackDirect( + event.getPlayer(), + new AntiGrief.GriefIncident( + event.getBlock().getLocation(), + event, + event.getBlock().getType(), + AntiGrief.GriefIncident.Severity.MODERATE + ) + ); + } + + @EventHandler + public void crystalPlacement(EntityPlaceEvent event) { + if(!event.getEntityType().equals(EntityType.END_CRYSTAL)) return; + AntiGrief.GriefIncident incident = new AntiGrief.GriefIncident( + event.getEntity().getLocation(), + event, + event.getEntityType(), + AntiGrief.GriefIncident.Severity.LIGHT + ); + if(event.getPlayer() != null) + this.getAppliance().trackDirect(event.getPlayer(), incident); + else + this.getAppliance().trackPassive(event.getBlock().getChunk(), incident); + } + + @EventHandler + public void tntPrime(TNTPrimeEvent event) { + AntiGrief.GriefIncident incident = new AntiGrief.GriefIncident( + event.getBlock().getLocation(), + event, + List.of(event.getCause(), event.getBlock().getType()), + AntiGrief.GriefIncident.Severity.MODERATE + ); + + if(event.getCause().equals(TNTPrimeEvent.PrimeCause.PLAYER) && event.getPrimingEntity() instanceof Player player) { + this.getAppliance().trackDirect(player, incident); + } + + this.getAppliance().trackPassive(event.getBlock().getChunk(), incident); + } + + @EventHandler + public void tntMinecartPlace(VehicleCreateEvent event) { + if(!(event.getVehicle() instanceof ExplosiveMinecart minecart)) return; + this.getAppliance().trackPassive( + event.getVehicle().getChunk(), + new AntiGrief.GriefIncident( + minecart.getLocation(), + event, + minecart.getType(), + AntiGrief.GriefIncident.Severity.SEVERE + ) + ); + } + + @EventHandler + public void entityExplosion(EntityExplodeEvent event) { + this.getAppliance().trackPassive( + event.getEntity().getChunk(), + new AntiGrief.GriefIncident( + event.getLocation(), + event, + List.of(event.getEntityType(), event.blockList().stream().map(Block::getType).distinct().toList()), + AntiGrief.GriefIncident.Severity.MODERATE + ) + ); + } + + @EventHandler + public void blockExplosion(BlockExplodeEvent event) { + this.getAppliance().trackPassive( + event.getBlock().getChunk(), + new AntiGrief.GriefIncident( + event.getBlock().getLocation(), + event, + List.of(event.getBlock().getType(), event.getExplodedBlockState().getType()), + AntiGrief.GriefIncident.Severity.MODERATE + ) + ); + } +} diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/FireRelatedGriefListener.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/FireRelatedGriefListener.java new file mode 100644 index 0000000..06d05de --- /dev/null +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/FireRelatedGriefListener.java @@ -0,0 +1,70 @@ +package eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.listener; + +import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener; +import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.AntiGrief; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.event.EventHandler; +import org.bukkit.event.block.*; + +import java.util.List; + +public class FireRelatedGriefListener extends ApplianceListener { + @EventHandler + public void activeBlockIgnite(BlockPlaceEvent event) { + if(!event.getBlock().getType().equals(Material.FIRE)) return; + if(this.getAppliance().getSurroundingBlocks(event.getBlock().getLocation()).noneMatch(Block::isBurnable)) return; + this.getAppliance().trackDirect( + event.getPlayer(), + new AntiGrief.GriefIncident( + event.getBlock().getLocation(), + event, + event.getBlock().getType(), + AntiGrief.GriefIncident.Severity.MODERATE + ) + ); + } + + @EventHandler + public void blockIgnite(BlockIgniteEvent event) { + if(!event.getBlock().isBurnable()) return; + this.getAppliance().trackPassive( + event.getBlock().getChunk(), + new AntiGrief.GriefIncident( + event.getBlock().getLocation(), + event, + List.of(event.getBlock().getType(), event.getCause()), + event.getCause().equals(BlockIgniteEvent.IgniteCause.FLINT_AND_STEEL) + ? AntiGrief.GriefIncident.Severity.MODERATE + : AntiGrief.GriefIncident.Severity.LIGHT + ) + ); + } + + @EventHandler + public void fireSpread(BlockSpreadEvent event) { + if(!event.getBlock().getType().equals(Material.FIRE)) return; + this.getAppliance().trackPassive( + event.getBlock().getChunk(), + new AntiGrief.GriefIncident( + event.getBlock().getLocation(), + event, + event.getBlock().getType(), + AntiGrief.GriefIncident.Severity.LIGHT + ) + ); + } + + @EventHandler + public void blockBurned(BlockBurnEvent event) { + this.getAppliance().trackPassive( + event.getBlock().getChunk(), + new AntiGrief.GriefIncident( + event.getBlock().getLocation(), + event, + event.getBlock().getType(), + AntiGrief.GriefIncident.Severity.MODERATE + ) + ); + } +} diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/LiquidRelatedGriefListener.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/LiquidRelatedGriefListener.java new file mode 100644 index 0000000..0084642 --- /dev/null +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/LiquidRelatedGriefListener.java @@ -0,0 +1,59 @@ +package eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.listener; + +import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener; +import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.AntiGrief; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.event.EventHandler; +import org.bukkit.event.block.BlockFormEvent; +import org.bukkit.event.block.BlockFromToEvent; +import org.bukkit.event.player.PlayerBucketEmptyEvent; + +import java.util.List; + +public class LiquidRelatedGriefListener extends ApplianceListener { + @EventHandler + public void liquidFlow(BlockFromToEvent event) { + if(event.getToBlock().isEmpty()) return; + if(event.getToBlock().isSolid()) return; + + this.getAppliance().trackPassive( + event.getToBlock().getChunk(), + new AntiGrief.GriefIncident( + event.getToBlock().getLocation(), + event, + event.getToBlock().getType(), + AntiGrief.GriefIncident.Severity.INFO + ) + ); + } + + @EventHandler + public void lavaCast(BlockFormEvent event) { + if(!List.of(Material.COBBLESTONE, Material.STONE, Material.OBSIDIAN).contains(event.getNewState().getType())) return; + this.getAppliance().trackPassive( + event.getBlock().getChunk(), + new AntiGrief.GriefIncident( + event.getBlock().getLocation(), + event, + List.of(event.getBlock().getType(), event.getNewState().getType()), + AntiGrief.GriefIncident.Severity.LIGHT + ) + ); + } + + @EventHandler + public void lavaPlace(PlayerBucketEmptyEvent event) { + if(!event.getBucket().equals(Material.LAVA_BUCKET)) return; + if(this.getAppliance().getSurroundingBlocks(event.getBlockClicked().getLocation()).noneMatch(Block::isBurnable)) return; + this.getAppliance().trackDirect( + event.getPlayer(), + new AntiGrief.GriefIncident( + event.getBlock().getLocation(), + event, + List.of(event.getBlock().getType(), event.getBucket()), + AntiGrief.GriefIncident.Severity.MODERATE + ) + ); + } +} diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/player/PlayerGriefListener.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/player/PlayerGriefListener.java deleted file mode 100644 index 8382637..0000000 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/player/PlayerGriefListener.java +++ /dev/null @@ -1,20 +0,0 @@ -package eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.player; - -import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener; -import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.AntiGrief; -import org.bukkit.event.EventHandler; -import org.bukkit.event.block.BlockIgniteEvent; -import org.bukkit.event.entity.ExplosionPrimeEvent; - -public class PlayerGriefListener extends ApplianceListener { - @EventHandler - public void onIgnite(BlockIgniteEvent event) { - if(event.getPlayer() == null) return; - this.getAppliance().addTracking(event.getPlayer(), new AntiGrief.GriefIncident(event.getBlock().getLocation(), event.getBlock())); - } - - @EventHandler - public void onTnt(ExplosionPrimeEvent event) { - - } -} From fdf3b5c73f52d902382450755fc7b87afd36c8ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 21 Sep 2025 13:31:21 +0200 Subject: [PATCH 05/10] smaller lavaCast detector to prevent natural flow detection --- .../security/antiGrief/listener/LiquidRelatedGriefListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/LiquidRelatedGriefListener.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/LiquidRelatedGriefListener.java index 0084642..78de348 100644 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/LiquidRelatedGriefListener.java +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/LiquidRelatedGriefListener.java @@ -30,7 +30,7 @@ public class LiquidRelatedGriefListener extends ApplianceListener { @EventHandler public void lavaCast(BlockFormEvent event) { - if(!List.of(Material.COBBLESTONE, Material.STONE, Material.OBSIDIAN).contains(event.getNewState().getType())) return; + if(!event.getNewState().getType().equals(Material.COBBLESTONE)) return; this.getAppliance().trackPassive( event.getBlock().getChunk(), new AntiGrief.GriefIncident( From 16d7347fd058dffdc12c976b29713d82d76e2056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 27 Sep 2025 07:30:23 +0200 Subject: [PATCH 06/10] antigrief false positives tweaks --- .../appliances/tooling/kick/KickCommand.java | 1 + .../security/antiGrief/AntiGrief.java | 25 +++++++++++-------- .../listener/FireRelatedGriefListener.java | 12 +++++++++ .../listener/LiquidRelatedGriefListener.java | 2 ++ 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/kick/KickCommand.java b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/kick/KickCommand.java index c9edaac..1122db6 100644 --- a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/kick/KickCommand.java +++ b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/kick/KickCommand.java @@ -19,6 +19,7 @@ class KickCommand extends ApplianceCommand { @Override protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception { + if(args.length < 1) throw new Error("Es muss ein Spielername angegeben werden!"); this.getAppliance().kick( args[0], Arrays.stream(args).skip(1).collect(Collectors.joining(" ")) diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java index a333d45..b9aacf8 100644 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java @@ -96,6 +96,11 @@ public class AntiGrief extends Appliance { this.chunkZ = chunkZ; } + long getInhabitedTime() { + return Objects.requireNonNull(Bukkit.getWorld(this.worldId)) + .getChunkAt(this.chunkX, this.chunkX).getInhabitedTime(); + } + void addIncident(GriefIncident incident) { long b = incident.timestamp / BUCKET_DURATION_MS; this.scores.merge(b, (double) incident.severity.weight, Double::sum); @@ -126,13 +131,13 @@ public class AntiGrief extends Appliance { /** Maximum retention time for individual incidents in milliseconds. */ private static final long INCIDENT_RETAIN_MS = 60 * 60 * 1000; /** Spike factor against EMA baseline. Triggers if current score >= baseline * FACTOR_SPIKE. */ - private static final double FACTOR_SPIKE = 3.0; + private static final double FACTOR_SPIKE = 5.0; /** Absolute threshold for spike detection. Triggers if current score exceeds this value. */ - private static final double HARD_THRESHOLD = 20.0; + private static final double HARD_THRESHOLD = 50.0; /** Cooldown time in ms to suppress repeated alerts for the same area. */ private static final long ALERT_COOLDOWN_MS = 2 * 60 * 1000; /** Smoothing factor for EMA baseline. Lower = smoother, higher = more reactive. 0.0 < EMA_ALPHA <= 1.0 */ - private static final double EMA_ALPHA = 0.3; + private static final double EMA_ALPHA = 0.2; /** Stores direct incidents mapped by player UUID. */ private final Map> directGriefRegistry = new ConcurrentHashMap<>(); @@ -159,8 +164,6 @@ public class AntiGrief extends Appliance { this.areas .computeIfAbsent(areaKey, key -> new AreaState(incident.worldId, chunk.getX(), chunk.getZ())) .addIncident(incident); - - Bukkit.broadcast(text(String.format("%d, %d: %s - %s: {%s}", chunk.getX(), chunk.getZ(), incident.severity, incident.event, incident.data))); } @Override @@ -169,17 +172,17 @@ public class AntiGrief extends Appliance { final long now = System.currentTimeMillis(); final long bucketIdx = this.bucketIdx(now); - this.areas.forEach((areaKey, st) -> { - final double currentScore = st.currentScore(bucketIdx); + this.areas.forEach((areaKey, state) -> { + final double currentScore = state.currentScore(bucketIdx); if (currentScore <= 0.0) return; - final double base = (st.ema == 0.0) ? currentScore : st.ema; + final double base = (state.ema == 0.0) ? currentScore : state.ema; final double newBase = EMA_ALPHA * currentScore + (1 - EMA_ALPHA) * base; - st.ema = newBase; + state.ema = Math.max(3, newBase); boolean spike = currentScore >= HARD_THRESHOLD || currentScore >= base * FACTOR_SPIKE; - if (spike && (now - st.lastAlertAt) >= ALERT_COOLDOWN_MS) { - st.lastAlertAt = now; + if (spike && (now - state.lastAlertAt) >= ALERT_COOLDOWN_MS) { + state.lastAlertAt = now; Bukkit.getScheduler().runTask(Main.instance(), () -> this.alertAdmins(areaKey, bucketIdx, currentScore, newBase) ); diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/FireRelatedGriefListener.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/FireRelatedGriefListener.java index 06d05de..f084428 100644 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/FireRelatedGriefListener.java +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/FireRelatedGriefListener.java @@ -4,6 +4,7 @@ import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener; import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.AntiGrief; import org.bukkit.Material; import org.bukkit.block.Block; +import org.bukkit.block.PistonMoveReaction; import org.bukkit.event.EventHandler; import org.bukkit.event.block.*; @@ -57,6 +58,17 @@ public class FireRelatedGriefListener extends ApplianceListener { @EventHandler public void blockBurned(BlockBurnEvent event) { + if(event.getBlock().isReplaceable()) return; + if(event.getBlock().isPassable()) return; + if(event.getBlock().getPistonMoveReaction().equals(PistonMoveReaction.BREAK)) return; + if(event.getBlock().getType().name().endsWith("_LEAVES")) return; + if(event.getBlock().getType().name().endsWith("_LOG")) return; + List allowed = List.of( + Material.MOSS_BLOCK, + Material.MOSS_CARPET + ); + if(allowed.contains(event.getBlock().getType())) return; + this.getAppliance().trackPassive( event.getBlock().getChunk(), new AntiGrief.GriefIncident( diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/LiquidRelatedGriefListener.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/LiquidRelatedGriefListener.java index 78de348..fafa54e 100644 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/LiquidRelatedGriefListener.java +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/listener/LiquidRelatedGriefListener.java @@ -10,12 +10,14 @@ import org.bukkit.event.block.BlockFromToEvent; import org.bukkit.event.player.PlayerBucketEmptyEvent; import java.util.List; +import java.util.concurrent.ThreadLocalRandom; public class LiquidRelatedGriefListener extends ApplianceListener { @EventHandler public void liquidFlow(BlockFromToEvent event) { if(event.getToBlock().isEmpty()) return; if(event.getToBlock().isSolid()) return; + if(ThreadLocalRandom.current().nextDouble() < 0.95) return; this.getAppliance().trackPassive( event.getToBlock().getChunk(), From d7cc141b94902045c24a12b701085ee2fa167195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 27 Sep 2025 07:42:13 +0200 Subject: [PATCH 07/10] added antiGrief inhabited chunk time calculation --- .../spawn/core/util/NumberUtil.java | 6 +++++ .../security/antiGrief/AntiGrief.java | 25 ++++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/eu/mhsl/craftattack/spawn/core/util/NumberUtil.java b/core/src/main/java/eu/mhsl/craftattack/spawn/core/util/NumberUtil.java index 9e8cd8d..2df53c1 100644 --- a/core/src/main/java/eu/mhsl/craftattack/spawn/core/util/NumberUtil.java +++ b/core/src/main/java/eu/mhsl/craftattack/spawn/core/util/NumberUtil.java @@ -8,4 +8,10 @@ public class NumberUtil { return out; } + + public static > T clamp(T value, T min, T max) { + if (value.compareTo(min) < 0) return min; + if (value.compareTo(max) > 0) return max; + return value; + } } diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java index b9aacf8..5865e06 100644 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java @@ -2,6 +2,7 @@ package eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief; import eu.mhsl.craftattack.spawn.core.Main; import eu.mhsl.craftattack.spawn.core.appliance.Appliance; +import eu.mhsl.craftattack.spawn.core.util.NumberUtil; import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.listener.*; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; @@ -139,6 +140,11 @@ public class AntiGrief extends Appliance { /** Smoothing factor for EMA baseline. Lower = smoother, higher = more reactive. 0.0 < EMA_ALPHA <= 1.0 */ private static final double EMA_ALPHA = 0.2; + /** Minimal chunk inhabited time to start registering scores linearly to INHABITED_FULL_MS */ + private static final int INHABITED_MIN_MS = 60 * 60 * 1000; + /** Max time to reach 100% effect on the score */ + private static final int INHABITED_FULL_MS = 24 * 60 * 60 * 1000; + /** Stores direct incidents mapped by player UUID. */ private final Map> directGriefRegistry = new ConcurrentHashMap<>(); /** Stores passive incidents mapped by chunk key. */ @@ -176,15 +182,18 @@ public class AntiGrief extends Appliance { final double currentScore = state.currentScore(bucketIdx); if (currentScore <= 0.0) return; - final double base = (state.ema == 0.0) ? currentScore : state.ema; - final double newBase = EMA_ALPHA * currentScore + (1 - EMA_ALPHA) * base; + final double adjustedScore = adjustScoreToInhabitantTime(state, currentScore); + if (adjustedScore <= 0.0) return; + + final double base = (state.ema == 0.0) ? adjustedScore : state.ema; + final double newBase = EMA_ALPHA * adjustedScore + (1 - EMA_ALPHA) * base; state.ema = Math.max(3, newBase); - boolean spike = currentScore >= HARD_THRESHOLD || currentScore >= base * FACTOR_SPIKE; + final boolean spike = adjustedScore >= HARD_THRESHOLD || adjustedScore >= base * FACTOR_SPIKE; if (spike && (now - state.lastAlertAt) >= ALERT_COOLDOWN_MS) { state.lastAlertAt = now; Bukkit.getScheduler().runTask(Main.instance(), () -> - this.alertAdmins(areaKey, bucketIdx, currentScore, newBase) + this.alertAdmins(areaKey, bucketIdx, adjustedScore, newBase) ); } }); @@ -212,6 +221,14 @@ public class AntiGrief extends Appliance { }, Ticks.TICKS_PER_SECOND * 30, Ticks.TICKS_PER_SECOND * 30); } + private static double adjustScoreToInhabitantTime(AreaState state, double currentScore) { + final long inhabitedMs = state.getInhabitedTime() * Ticks.SINGLE_TICK_DURATION_MS / Ticks.TICKS_PER_SECOND; + + double factor = (double) (inhabitedMs - INHABITED_MIN_MS) / (double) (INHABITED_FULL_MS - INHABITED_MIN_MS); + factor = NumberUtil.clamp(factor, 0.0, 1.0); + return currentScore * factor; + } + private void alertAdmins(long areaKey, long bucketIdx, double curr, double baseline) { AreaState meta = this.areas.get(areaKey); if (meta == null) return; From 32a20cd4c57124c0a3dae9038016fc63637d007b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 28 Sep 2025 12:59:18 +0200 Subject: [PATCH 08/10] added antiGrief command --- .../security/antiGrief/AntiGrief.java | 39 +++++++++--- .../antiGrief/commands/AntiGriefCommand.java | 63 +++++++++++++++++++ .../commands/GriefOverviewCommand.java | 39 ------------ 3 files changed, 92 insertions(+), 49 deletions(-) create mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/commands/AntiGriefCommand.java delete mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/commands/GriefOverviewCommand.java diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java index 5865e06..e638b1e 100644 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/AntiGrief.java @@ -2,7 +2,9 @@ package eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief; import eu.mhsl.craftattack.spawn.core.Main; import eu.mhsl.craftattack.spawn.core.appliance.Appliance; +import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand; import eu.mhsl.craftattack.spawn.core.util.NumberUtil; +import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.commands.AntiGriefCommand; import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.listener.*; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; @@ -79,17 +81,17 @@ public class AntiGrief extends Appliance { } } - private static final class AreaState { - final UUID worldId; - final int chunkX, chunkZ; + public static final class AreaState { + public final UUID worldId; + public final int chunkX, chunkZ; /** Rolling bucket scores for Spike Detection. */ - final NavigableMap scores = new ConcurrentSkipListMap<>(); + public final NavigableMap scores = new ConcurrentSkipListMap<>(); /** Incidents per Bucket */ - final Map> incidentsByBucket = new ConcurrentHashMap<>(); + public final Map> incidentsByBucket = new ConcurrentHashMap<>(); - volatile double ema = 0.0; - volatile long lastAlertAt = 0L; + public volatile double ema = 0.0; + public volatile long lastAlertAt = 0L; AreaState(UUID worldId, int chunkX, int chunkZ) { this.worldId = worldId; @@ -97,7 +99,7 @@ public class AntiGrief extends Appliance { this.chunkZ = chunkZ; } - long getInhabitedTime() { + public long getInhabitedTime() { return Objects.requireNonNull(Bukkit.getWorld(this.worldId)) .getChunkAt(this.chunkX, this.chunkX).getInhabitedTime(); } @@ -314,8 +316,18 @@ public class AntiGrief extends Appliance { ); } - public Map> getDirectGriefRegistry() { - return this.directGriefRegistry; + public @Nullable AreaState getInfoAtChunk(Chunk chunk) { + long areaKey = this.packArea(chunk.getWorld().getUID(), chunk.getX(), chunk.getZ()); + return this.areas.get(areaKey); + } + + public List getHighesScoredChunks(int limit) { + long nowBucket = this.bucketIdx(System.currentTimeMillis()); + + return this.areas.values().stream() + .sorted(Comparator.comparingDouble((AreaState st) -> st.currentScore(nowBucket)).reversed()) + .limit(limit) + .toList(); } @Override @@ -328,4 +340,11 @@ public class AntiGrief extends Appliance { new EntityRelatedGriefListener() ); } + + @Override + protected @NotNull List> commands() { + return List.of( + new AntiGriefCommand() + ); + } } diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/commands/AntiGriefCommand.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/commands/AntiGriefCommand.java new file mode 100644 index 0000000..2f38e29 --- /dev/null +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/commands/AntiGriefCommand.java @@ -0,0 +1,63 @@ +package eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.commands; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand; +import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.AntiGrief; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class AntiGriefCommand extends ApplianceCommand.PlayerChecked { + private final Gson prettyGson = new GsonBuilder().setPrettyPrinting().create(); + + public AntiGriefCommand() { + super("antiGrief"); + } + + @Override + protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception { + if(args.length != 1) throw new Error("One argument expected"); + switch(args[0]) { + case "currentChunk": { + AntiGrief.AreaState state = this.getAppliance().getInfoAtChunk(this.getPlayer().getChunk()); + if(state == null) throw new Error("The current chunk does not have a Score!"); + sender.sendMessage(this.areaStateDisplay(state)); + sender.sendMessage(String.format("ChunkLoaded: %ds", state.getInhabitedTime() / 1000)); + break; + } + case "topChunks": { + List states = this.getAppliance().getHighesScoredChunks(10); + sender.sendMessage(Component.empty().append( + states.stream().map(state -> this.areaStateDisplay(state).appendNewline()).toList() + )); + break; + } + default: throw new Error("No such option!"); + } + } + + private Component areaStateDisplay(AntiGrief.AreaState state) { + var object = Component.text("[\uD83D\uDCC2]", NamedTextColor.GRAY) + .append(Component.text(" - ", NamedTextColor.GOLD)) + .hoverEvent(HoverEvent.showText(Component.text(this.prettyGson.toJson(state)))); + var location = Component.text(String.format("[%d,%d]", state.chunkX, state.chunkZ), NamedTextColor.YELLOW) + .append(Component.text(" > ", NamedTextColor.DARK_GRAY)); + int incidentCount = state.incidentsByBucket.values().stream().map(List::size).mapToInt(Integer::intValue).sum(); + var total = Component.text(String.format("ema:%.2f, totalIncidents:%d", state.ema, incidentCount), NamedTextColor.GRAY); + + return Component.empty().append(object, location, total); + } + + @Override + public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if(args.length == 1) return List.of("currentChunk", "topChunks"); + return null; + } +} diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/commands/GriefOverviewCommand.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/commands/GriefOverviewCommand.java deleted file mode 100644 index 700d8ba..0000000 --- a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/security/antiGrief/commands/GriefOverviewCommand.java +++ /dev/null @@ -1,39 +0,0 @@ -package eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.commands; - -import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand; -import eu.mhsl.craftattack.spawn.craftattack.appliances.security.antiGrief.AntiGrief; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.ComponentBuilder; -import net.kyori.adventure.text.ScopedComponent; -import net.kyori.adventure.text.TextComponent; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -public class GriefOverviewCommand extends ApplianceCommand { - public GriefOverviewCommand() { - super("griefOverview"); - } - - @Override - protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception { - sender.sendMessage( - this.getAppliance().getDirectGriefRegistry().entrySet().stream() - .map(griefEntry -> { - List entries = griefEntry.getValue().stream() - .map(incident -> Component.text(incident.x())) - .toList(); - ComponentBuilder builder = Component.text() - .append(Component.text(griefEntry.getKey().toString())) - .append(Component.text(": ")); - entries.forEach(builder::append); - - return builder.build(); - }) - .reduce(ScopedComponent::append) - .orElseThrow() - ); - } -} From c88c2ab6aa87782b87698e8f92b20b1b977ec79b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 28 Sep 2025 13:22:42 +0200 Subject: [PATCH 09/10] updated dependencies --- common/build.gradle | 2 +- core/build.gradle | 2 +- craftattack/build.gradle | 2 +- varo/build.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/build.gradle b/common/build.gradle index 3129896..ec69de6 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -1,7 +1,7 @@ dependencies { implementation project(':core') - compileOnly 'io.papermc.paper:paper-api:1.21.7-R0.1-SNAPSHOT' + compileOnly 'io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT' compileOnly 'org.geysermc.floodgate:api:2.2.4-SNAPSHOT' implementation 'org.apache.httpcomponents:httpclient:4.5.14' implementation 'com.sparkjava:spark-core:2.9.4' diff --git a/core/build.gradle b/core/build.gradle index 34337b5..d3f249c 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,5 +1,5 @@ dependencies { - compileOnly 'io.papermc.paper:paper-api:1.21.7-R0.1-SNAPSHOT' + compileOnly 'io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT' compileOnly 'org.geysermc.floodgate:api:2.2.4-SNAPSHOT' implementation 'org.apache.httpcomponents:httpclient:4.5.14' implementation 'com.sparkjava:spark-core:2.9.4' diff --git a/craftattack/build.gradle b/craftattack/build.gradle index be30e01..90a3c35 100644 --- a/craftattack/build.gradle +++ b/craftattack/build.gradle @@ -2,7 +2,7 @@ dependencies { implementation project(':core') implementation project(':common') - compileOnly 'io.papermc.paper:paper-api:1.21.7-R0.1-SNAPSHOT' + compileOnly 'io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT' compileOnly 'org.geysermc.floodgate:api:2.2.4-SNAPSHOT' implementation 'org.apache.httpcomponents:httpclient:4.5.14' implementation 'com.sparkjava:spark-core:2.9.4' diff --git a/varo/build.gradle b/varo/build.gradle index ec631a1..8557e5c 100644 --- a/varo/build.gradle +++ b/varo/build.gradle @@ -2,7 +2,7 @@ dependencies { implementation project(':core') implementation project(':common') - compileOnly 'io.papermc.paper:paper-api:1.21.7-R0.1-SNAPSHOT' + compileOnly 'io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT' implementation 'org.apache.httpcomponents:httpclient:4.5.14' implementation 'com.sparkjava:spark-core:2.9.4' } \ No newline at end of file From d4a3c798f8b2d3d28e3f746d9798cc4866785106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Wed, 1 Oct 2025 19:11:16 +0200 Subject: [PATCH 10/10] added LocatorBar preferences --- .../metaGameplay/settings/Settings.java | 14 +++++- .../settings/datatypes/Setting.java | 20 +++++++- .../gameplay/locatorBar/LocatorBar.java | 50 +++++++++++++++++++ .../locatorBar/LocatorBarSettings.java | 38 ++++++++++++++ .../locatorBar/LocatorBarUpdateListener.java | 12 +++++ 5 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/locatorBar/LocatorBar.java create mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/locatorBar/LocatorBarSettings.java create mode 100644 craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/locatorBar/LocatorBarUpdateListener.java diff --git a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/metaGameplay/settings/Settings.java b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/metaGameplay/settings/Settings.java index 6bc857f..653416d 100644 --- a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/metaGameplay/settings/Settings.java +++ b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/metaGameplay/settings/Settings.java @@ -17,6 +17,7 @@ import org.jetbrains.annotations.NotNull; import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.stream.Collectors; public class Settings extends Appliance { @@ -34,7 +35,8 @@ public class Settings extends Appliance { ChatMentions, DoubleDoors, KnockDoors, - BorderWarning + BorderWarning, + LocatorBarConfig } public static Settings instance() { @@ -58,6 +60,16 @@ public class Settings extends Appliance { private final WeakHashMap openSettingsInventories = new WeakHashMap<>(); private final WeakHashMap>> settingsCache = new WeakHashMap<>(); + protected final Map>, Consumer> changeListeners = new WeakHashMap<>(); + + public > void addChangeListener(Class setting, Consumer listener) { + this.changeListeners.merge(setting, listener, Consumer::andThen); + } + + public > void invokeChangeListener(Player player, Class setting) { + Optional.ofNullable(this.changeListeners.get(setting)) + .ifPresent(listener -> listener.accept(player)); + } private List> getSettings(Player player) { if(this.settingsCache.containsKey(player)) return this.settingsCache.get(player); diff --git a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/metaGameplay/settings/datatypes/Setting.java b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/metaGameplay/settings/datatypes/Setting.java index 296a46b..31cd596 100644 --- a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/metaGameplay/settings/datatypes/Setting.java +++ b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/metaGameplay/settings/datatypes/Setting.java @@ -34,7 +34,24 @@ public abstract class Setting { } public void initializeFromPlayer(Player p) { - this.fromStorage(p.getPersistentDataContainer()); + PersistentDataContainer dataContainer = p.getPersistentDataContainer(); + try { + this.fromStorage(dataContainer); + } catch(IllegalArgumentException e) { + Main.logger().warning(String.format( + "Could not load state of setting %s from player %s: '%s'\n Did the datatype of the setting change?", + this.getNamespacedKey(), + e.getMessage(), + p.getName() + )); + dataContainer.remove(this.getNamespacedKey()); + this.fromStorage(dataContainer); + Main.logger().info(String.format( + "Restoring defaults of setting %s of player %s", + this.getNamespacedKey(), + p.getName() + )); + } } public void triggerChange(Player p, ClickType clickType) { @@ -42,6 +59,7 @@ public abstract class Setting { this.change(p, clickType); InteractSounds.of(p).click(); this.toStorage(p.getPersistentDataContainer(), this.state()); + Settings.instance().invokeChangeListener(p, this.getClass()); } public ItemStack buildItem() { diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/locatorBar/LocatorBar.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/locatorBar/LocatorBar.java new file mode 100644 index 0000000..bc0d1f8 --- /dev/null +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/locatorBar/LocatorBar.java @@ -0,0 +1,50 @@ +package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.locatorBar; + +import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings; +import eu.mhsl.craftattack.spawn.core.appliance.Appliance; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; + +public class LocatorBar extends Appliance { + private enum Distance { + MAX(6.0e7), + ZERO(0.0); + + final double distance; + Distance(double distance) { + this.distance = distance; + } + } + + @Override + public void onEnable() { + Settings.instance().declareSetting(LocatorBarSettings.class); + Settings.instance().addChangeListener(LocatorBarSettings.class, this::updateLocatorBar); + } + + public void updateLocatorBar(Player player) { + boolean enabled = Settings.instance().getSetting(player, Settings.Key.LocatorBarConfig, Boolean.class); + + AttributeInstance receive = player.getAttribute(Attribute.WAYPOINT_RECEIVE_RANGE); + AttributeInstance transmit = player.getAttribute(Attribute.WAYPOINT_TRANSMIT_RANGE); + + Objects.requireNonNull(receive); + Objects.requireNonNull(transmit); + + receive.setBaseValue(enabled ? Distance.MAX.distance : Distance.ZERO.distance); + transmit.setBaseValue(enabled ? Distance.MAX.distance : Distance.ZERO.distance); + } + + @Override + protected @NotNull List listeners() { + return List.of( + new LocatorBarUpdateListener() + ); + } +} diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/locatorBar/LocatorBarSettings.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/locatorBar/LocatorBarSettings.java new file mode 100644 index 0000000..bf54025 --- /dev/null +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/locatorBar/LocatorBarSettings.java @@ -0,0 +1,38 @@ +package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.locatorBar; + +import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.CategorizedSetting; +import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.SettingCategory; +import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.Settings; +import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.settings.datatypes.BoolSetting; +import org.bukkit.Material; + +public class LocatorBarSettings extends BoolSetting implements CategorizedSetting { + @Override + public SettingCategory category() { + return SettingCategory.Gameplay; + } + + public LocatorBarSettings() { + super(Settings.Key.LocatorBarConfig); + } + + @Override + protected String title() { + return "Ortungsleiste / Locator Bar"; + } + + @Override + protected String description() { + return "Konfiguriere, ob andere Spieler deine Position und du die Position anderer sehen möchtest"; + } + + @Override + protected Material icon() { + return Material.COMPASS; + } + + @Override + protected Boolean defaultValue() { + return true; + } +} diff --git a/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/locatorBar/LocatorBarUpdateListener.java b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/locatorBar/LocatorBarUpdateListener.java new file mode 100644 index 0000000..6feb17b --- /dev/null +++ b/craftattack/src/main/java/eu/mhsl/craftattack/spawn/craftattack/appliances/gameplay/locatorBar/LocatorBarUpdateListener.java @@ -0,0 +1,12 @@ +package eu.mhsl.craftattack.spawn.craftattack.appliances.gameplay.locatorBar; + +import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerJoinEvent; + +class LocatorBarUpdateListener extends ApplianceListener { + @EventHandler + public void onJoin(PlayerJoinEvent event) { + this.getAppliance().updateLocatorBar(event.getPlayer()); + } +}