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) { - - } -}