Merge branch 'master-antiGrief'
This commit is contained in:
@@ -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'
|
||||
|
@@ -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);
|
||||
|
@@ -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() {
|
||||
|
@@ -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(" "))
|
||||
|
@@ -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'
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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'
|
||||
|
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -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() {
|
||||
|
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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'
|
||||
}
|
Reference in New Issue
Block a user