Merge branch 'master-antiGrief'

This commit is contained in:
2025-10-01 19:12:01 +02:00
21 changed files with 1086 additions and 8 deletions

View File

@@ -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'

View File

@@ -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<Player, OpenSettingsInventory> openSettingsInventories = new WeakHashMap<>();
private final WeakHashMap<Player, List<Setting<?>>> settingsCache = new WeakHashMap<>();
protected final Map<Class<? extends Setting<?>>, Consumer<Player>> changeListeners = new WeakHashMap<>();
public <TDataType extends Setting<?>> void addChangeListener(Class<TDataType> setting, Consumer<Player> listener) {
this.changeListeners.merge(setting, listener, Consumer::andThen);
}
public <TDataType extends Setting<?>> void invokeChangeListener(Player player, Class<TDataType> setting) {
Optional.ofNullable(this.changeListeners.get(setting))
.ifPresent(listener -> listener.accept(player));
}
private List<Setting<?>> getSettings(Player player) {
if(this.settingsCache.containsKey(player)) return this.settingsCache.get(player);

View File

@@ -34,7 +34,24 @@ public abstract class Setting<TDataType> {
}
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<TDataType> {
this.change(p, clickType);
InteractSounds.of(p).click();
this.toStorage(p.getPersistentDataContainer(), this.state());
Settings.instance().invokeChangeListener(p, this.getClass());
}
public ItemStack buildItem() {

View File

@@ -19,6 +19,7 @@ class KickCommand extends ApplianceCommand<Kick> {
@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(" "))

View File

@@ -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'

View File

@@ -8,4 +8,10 @@ public class NumberUtil {
return out;
}
public static <T extends Comparable<T>> T clamp(T value, T min, T max) {
if (value.compareTo(min) < 0) return min;
if (value.compareTo(max) > 0) return max;
return value;
}
}

View File

@@ -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'

View File

@@ -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<Listener> listeners() {
return List.of(
new LocatorBarUpdateListener()
);
}
}

View File

@@ -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;
}
}

View File

@@ -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<LocatorBar> {
@EventHandler
public void onJoin(PlayerJoinEvent event) {
this.getAppliance().updateLocatorBar(event.getPlayer());
}
}

View File

@@ -17,9 +17,9 @@ public class VaroRank extends Appliance implements DisplayName.Prefixed {
private List<UUID> winners = new ArrayList<>();
private List<UUID> 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() {

View File

@@ -0,0 +1,350 @@
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;
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(
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 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 static final class AreaState {
public final UUID worldId;
public final int chunkX, chunkZ;
/** Rolling bucket scores for Spike Detection. */
public final NavigableMap<Long, Double> scores = new ConcurrentSkipListMap<>();
/** Incidents per Bucket */
public final Map<Long, List<GriefIncident>> incidentsByBucket = new ConcurrentHashMap<>();
public volatile double ema = 0.0;
public volatile long lastAlertAt = 0L;
AreaState(UUID worldId, int chunkX, int chunkZ) {
this.worldId = worldId;
this.chunkX = chunkX;
this.chunkZ = chunkZ;
}
public 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);
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 = 5.0;
/** Absolute threshold for spike detection. Triggers if current score exceeds this value. */
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.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<UUID, Set<GriefIncident>> directGriefRegistry = new ConcurrentHashMap<>();
/** Stores passive incidents mapped by chunk key. */
private final Map<Long, Set<GriefIncident>> passiveGriefRegistry = new ConcurrentHashMap<>();
/** Stores scores by area */
private final Map<Long, AreaState> 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);
}
@Override
public void onEnable() {
Bukkit.getScheduler().runTaskTimerAsynchronously(Main.instance(), () -> {
final long now = System.currentTimeMillis();
final long bucketIdx = this.bucketIdx(now);
this.areas.forEach((areaKey, state) -> {
final double currentScore = state.currentScore(bucketIdx);
if (currentScore <= 0.0) return;
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);
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, adjustedScore, 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 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;
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<Player> 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<GriefIncident> 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<Block> 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 @Nullable AreaState getInfoAtChunk(Chunk chunk) {
long areaKey = this.packArea(chunk.getWorld().getUID(), chunk.getX(), chunk.getZ());
return this.areas.get(areaKey);
}
public List<AreaState> 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
protected @NotNull List<Listener> listeners() {
return List.of(
new BlockRelatedGriefListener(),
new ExplosionRelatedGriefListener(),
new FireRelatedGriefListener(),
new LiquidRelatedGriefListener(),
new EntityRelatedGriefListener()
);
}
@Override
protected @NotNull List<ApplianceCommand<?>> commands() {
return List.of(
new AntiGriefCommand()
);
}
}

View File

@@ -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<AntiGrief> {
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<AntiGrief.AreaState> 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<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if(args.length == 1) return List.of("currentChunk", "topChunks");
return null;
}
}

View File

@@ -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<AntiGrief> {
@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
)
);
}
}

View File

@@ -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<AntiGrief> {
@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<EntityDamageEvent.DamageCause> 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<EntityType> 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<EntityDamageEvent.DamageCause> 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);
}
}

View File

@@ -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<AntiGrief> {
@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
)
);
}
}

View File

@@ -0,0 +1,82 @@
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.block.PistonMoveReaction;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.*;
import java.util.List;
public class FireRelatedGriefListener extends ApplianceListener<AntiGrief> {
@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) {
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<Material> 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(
event.getBlock().getLocation(),
event,
event.getBlock().getType(),
AntiGrief.GriefIncident.Severity.MODERATE
)
);
}
}

View File

@@ -0,0 +1,61 @@
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;
import java.util.concurrent.ThreadLocalRandom;
public class LiquidRelatedGriefListener extends ApplianceListener<AntiGrief> {
@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(),
new AntiGrief.GriefIncident(
event.getToBlock().getLocation(),
event,
event.getToBlock().getType(),
AntiGrief.GriefIncident.Severity.INFO
)
);
}
@EventHandler
public void lavaCast(BlockFormEvent event) {
if(!event.getNewState().getType().equals(Material.COBBLESTONE)) 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
)
);
}
}

View File

@@ -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<Block, UUID> 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<Listener> listeners() {
return List.of(new PlayerGriefListener());
}
}

View File

@@ -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<AntiGrief> {
@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);
}
}

View File

@@ -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'
}