From bc5c9a2a131194917da9df2c57c07858c6669bdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Mon, 27 Oct 2025 16:02:13 +0100 Subject: [PATCH 1/3] added AntiIllegalBundlePicker to track and notify admins on illegal bundle interactions --- .../AntiIllegalBundlePicker.java | 78 +++++++++++++++++++ .../OnBundlePickListener.java | 18 +++++ 2 files changed, 96 insertions(+) create mode 100644 common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiIllegalBundlePicker/AntiIllegalBundlePicker.java create mode 100644 common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiIllegalBundlePicker/OnBundlePickListener.java diff --git a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiIllegalBundlePicker/AntiIllegalBundlePicker.java b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiIllegalBundlePicker/AntiIllegalBundlePicker.java new file mode 100644 index 0000000..d75313b --- /dev/null +++ b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiIllegalBundlePicker/AntiIllegalBundlePicker.java @@ -0,0 +1,78 @@ +package eu.mhsl.craftattack.spawn.common.appliances.security.antiIllegalBundlePicker; + +import eu.mhsl.craftattack.spawn.common.appliances.tooling.acInform.AcInform; +import eu.mhsl.craftattack.spawn.core.Main; +import eu.mhsl.craftattack.spawn.core.appliance.Appliance; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BundleMeta; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.stream.Collectors; + +public class AntiIllegalBundlePicker extends Appliance { + private static final int visibleSlotsInBundle = 9; + + public void trackBundle(InventoryClickEvent event) { + ItemStack bundle = Objects.requireNonNull(event.getCurrentItem()); + final int rawSlot = event.getRawSlot(); + final Player player = (Player) event.getWhoClicked(); + final InventoryView view = event.getView(); + final List before = this.getBundleContents(bundle); + + Bukkit.getScheduler().runTask(Main.instance(), () -> { + ItemStack afterStack = view.getItem(rawSlot); + if(afterStack == null || afterStack.getType() != Material.BUNDLE) return; + + List after = this.getBundleContents(afterStack); + int removedSlotIndex = this.findRemoved(before, after); + + if(removedSlotIndex >= visibleSlotsInBundle) { + Main.instance().getAppliance(AcInform.class).notifyAdmins( + "internal", + player.getName(), + "illegalBundlePick", + (float) removedSlotIndex + ); + } + }); + } + + private int findRemoved(@NotNull List before, @NotNull List after) { + for (int i = 0; i < Math.max(before.size(), after.size()); i++) { + ItemStack a = i < after.size() ? after.get(i) : null; + ItemStack b = i < before.size() ? before.get(i) : null; + + if (b == null && a == null) continue; + if (b == null) throw new IllegalStateException("Size of bundle was smaller before pickup Action"); + + if (a == null) return i; + if (!a.isSimilar(b)) return i; + if (a.getAmount() != b.getAmount()) return i; + } + throw new IllegalStateException("Failed to find picked Item in bundle"); + } + + private List getBundleContents(@NotNull ItemStack bundle) { + if (bundle.getType() != Material.BUNDLE) + throw new IllegalStateException("ItemStack is not a bundle"); + + BundleMeta meta = (BundleMeta) bundle.getItemMeta(); + return meta.getItems().stream() + .map(ItemStack::clone) + .collect(Collectors.toCollection(ArrayList::new)); + } + + @Override + protected @NotNull List listeners() { + return List.of( + new OnBundlePickListener() + ); + } +} diff --git a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiIllegalBundlePicker/OnBundlePickListener.java b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiIllegalBundlePicker/OnBundlePickListener.java new file mode 100644 index 0000000..b25ae78 --- /dev/null +++ b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiIllegalBundlePicker/OnBundlePickListener.java @@ -0,0 +1,18 @@ +package eu.mhsl.craftattack.spawn.common.appliances.security.antiIllegalBundlePicker; + +import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener; +import org.bukkit.Material; +import org.bukkit.event.EventHandler; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; + +class OnBundlePickListener extends ApplianceListener { + @EventHandler + public void onBundlePick(InventoryClickEvent event) { + if(!event.getAction().equals(InventoryAction.PICKUP_FROM_BUNDLE)) return; + final ItemStack bundle = event.getCurrentItem(); + if (bundle == null || bundle.getType() != Material.BUNDLE) return; + this.getAppliance().trackBundle(event); + } +} From e745ff4721601dbe8075e05aec4c3ddc07ddfdf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Mon, 27 Oct 2025 17:10:44 +0100 Subject: [PATCH 2/3] added AntiFormattedBook to detect and sanitize illegal book formatting --- .../antiFormattedBook/AntiFormattedBook.java | 66 +++++++++++++++++++ .../antiFormattedBook/BookEditListener.java | 36 ++++++++++ 2 files changed, 102 insertions(+) create mode 100644 common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiFormattedBook/AntiFormattedBook.java create mode 100644 common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiFormattedBook/BookEditListener.java diff --git a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiFormattedBook/AntiFormattedBook.java b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiFormattedBook/AntiFormattedBook.java new file mode 100644 index 0000000..6af6d80 --- /dev/null +++ b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiFormattedBook/AntiFormattedBook.java @@ -0,0 +1,66 @@ +package eu.mhsl.craftattack.spawn.common.appliances.security.antiFormattedBook; + +import eu.mhsl.craftattack.spawn.core.appliance.Appliance; +import net.kyori.adventure.text.*; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.event.Listener; +import org.bukkit.inventory.meta.BookMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Objects; + +public class AntiFormattedBook extends Appliance { + private static final char SECTION = '\u00A7'; + + public boolean containsFormatting(BookMeta meta) { + if (this.hasFormattingDeep(meta.title())) return true; + if (this.hasFormattingDeep(meta.author())) return true; + + for (Component c : meta.pages()) { + if (this.hasFormattingDeep(c)) return true; + } + return false; + } + + private boolean hasFormattingDeep(@Nullable Component component) { + if(component == null) return false; + if (this.hastFormatting(component)) return true; + + if (component instanceof TextComponent tc && tc.content().indexOf(SECTION) >= 0) return true; + + if (component instanceof NBTComponent nbt) { + if (nbt.separator() != null && this.hasFormattingDeep(nbt.separator())) return true; + } + + for (Component child : component.children()) { + if (this.hasFormattingDeep(child)) return true; + } + return false; + } + + private boolean hastFormatting(Component component) { + Style style = component.style(); + + TextColor color = style.color(); + if (color != null) return true; + if (style.font() != null) return true; + if (style.insertion() != null && !Objects.requireNonNull(style.insertion()).isEmpty()) return true; + + for (var decoration : style.decorations().entrySet()) { + if (decoration.getValue() == TextDecoration.State.TRUE) return true; + } + + return style.hoverEvent() != null || style.clickEvent() != null; + } + + @Override + protected @NotNull List listeners() { + return List.of( + new BookEditListener() + ); + } +} diff --git a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiFormattedBook/BookEditListener.java b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiFormattedBook/BookEditListener.java new file mode 100644 index 0000000..1fcf3c9 --- /dev/null +++ b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiFormattedBook/BookEditListener.java @@ -0,0 +1,36 @@ +package eu.mhsl.craftattack.spawn.common.appliances.security.antiFormattedBook; + +import eu.mhsl.craftattack.spawn.common.appliances.tooling.acInform.AcInform; +import eu.mhsl.craftattack.spawn.core.Main; +import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener; +import net.kyori.adventure.text.Component; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerEditBookEvent; +import org.bukkit.inventory.meta.BookMeta; + +import java.util.List; + +class BookEditListener extends ApplianceListener { + @EventHandler + public void onBookEdit(PlayerEditBookEvent event) { + Player player = event.getPlayer(); + BookMeta meta = event.getNewBookMeta(); + + if (this.getAppliance().containsFormatting(meta)) { + Main.instance().getAppliance(AcInform.class).notifyAdmins( + "internal", + player.getName(), + "illegalBookFormatting", + 1f + ); + + BookMeta sanitized = meta.clone(); + sanitized.title(null); + sanitized.author(null); + //noinspection ResultOfMethodCallIgnored + sanitized.pages(List.of(Component.empty())); + event.setNewBookMeta(sanitized); + } + } +} From 469cd19b55ffd54785768a7d91321c073ce75939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Mon, 27 Oct 2025 17:54:19 +0100 Subject: [PATCH 3/3] added AntiBoatFreecam to detect and notify admins of illegal boat yaw behavior --- .../antiBoatFreecam/AntiBoatFreecam.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiBoatFreecam/AntiBoatFreecam.java diff --git a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiBoatFreecam/AntiBoatFreecam.java b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiBoatFreecam/AntiBoatFreecam.java new file mode 100644 index 0000000..8313799 --- /dev/null +++ b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/security/antiBoatFreecam/AntiBoatFreecam.java @@ -0,0 +1,51 @@ +package eu.mhsl.craftattack.spawn.common.appliances.security.antiBoatFreecam; + +import eu.mhsl.craftattack.spawn.common.appliances.tooling.acInform.AcInform; +import eu.mhsl.craftattack.spawn.core.Main; +import eu.mhsl.craftattack.spawn.core.appliance.Appliance; +import org.bukkit.Bukkit; +import org.bukkit.entity.Boat; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("unused") +public class AntiBoatFreecam extends Appliance { + private static final float MAX_YAW_OFFSET = 106.0f; + private final Map violatedPlayers = new HashMap<>(); + + public AntiBoatFreecam() { + Bukkit.getScheduler().runTaskTimerAsynchronously( + Main.instance(), + () -> Bukkit.getOnlinePlayers().forEach(player -> { + if(!(player.getVehicle() instanceof Boat boat)) return; + if(!boat.getPassengers().getFirst().equals(player)) return; + float playerYaw = player.getYaw(); + float boatYaw = boat.getYaw(); + + float yawDelta = wrapDegrees(playerYaw - boatYaw); + if(Math.abs(yawDelta) <= MAX_YAW_OFFSET) return; + + this.violatedPlayers.merge(player, 1f, Float::sum); + float violationCount = this.violatedPlayers.get(player); + if(violationCount != 1 && violationCount % 100 != 0) return; + Main.instance().getAppliance(AcInform.class).notifyAdmins( + "internal", + player.getName(), + "illegalBoatLookYaw", + violationCount + ); + }), + 1L, + 1L + ); + } + + private static float wrapDegrees(float deg) { + deg = deg % 360f; + if (deg >= 180f) deg -= 360f; + if (deg < -180f) deg += 360f; + return deg; + } +}