From 5d581dc482b8ba2f2ce8e2666b3c49e86613a897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Thu, 5 Nov 2026 10:44:35 +0100 Subject: [PATCH] item guard: photos cannot be cloned, locked or scaled in the cartography table, the anvil only renames camera, film and photo, the crafter does not craft with them (it handed out the static load/copy placeholders and ate the ingredients), camera and film cannot be placed as heads (the broken skull lost the markers) or put into mount saddle slots by horse inventory or dispenser --- .../eu/mhsl/minecraft/pixelpics/Main.java | 9 +- .../pixelpics/survival/ItemGuardListener.java | 120 ++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 src/main/java/eu/mhsl/minecraft/pixelpics/survival/ItemGuardListener.java diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/Main.java b/src/main/java/eu/mhsl/minecraft/pixelpics/Main.java index f46f0fb..c1b94dc 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/Main.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/Main.java @@ -19,6 +19,7 @@ import eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer; import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider; import eu.mhsl.minecraft.pixelpics.survival.CameraListener; import eu.mhsl.minecraft.pixelpics.survival.CraftingListener; +import eu.mhsl.minecraft.pixelpics.survival.ItemGuardListener; import eu.mhsl.minecraft.pixelpics.survival.JoinListener; import eu.mhsl.minecraft.pixelpics.survival.PendingPhotos; import eu.mhsl.minecraft.pixelpics.survival.SurvivalRecipes; @@ -62,8 +63,12 @@ public final class Main extends JavaPlugin { initRenderManager(); Objects.requireNonNull(getCommand("pixelPic")).setExecutor(new PixelPicsCommand()); - for (Listener listener : - List.of(new OnMapInitialize(), new CameraListener(), new CraftingListener(), new JoinListener())) { + for (Listener listener : List.of( + new OnMapInitialize(), + new CameraListener(), + new CraftingListener(), + new ItemGuardListener(), + new JoinListener())) { Bukkit.getPluginManager().registerEvents(listener, this); } SurvivalRecipes.register(); diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/survival/ItemGuardListener.java b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/ItemGuardListener.java new file mode 100644 index 0000000..f3e865c --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/survival/ItemGuardListener.java @@ -0,0 +1,120 @@ +package eu.mhsl.minecraft.pixelpics.survival; + +import com.destroystokyo.paper.event.inventory.PrepareResultEvent; +import io.papermc.paper.event.player.CartographyItemEvent; +import org.bukkit.block.Crafter; +import org.bukkit.entity.LivingEntity; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockDispenseArmorEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.block.CrafterCraftEvent; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.AnvilInventory; +import org.bukkit.inventory.CartographyInventory; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +/** + * Keeps camera, film and photo out of the vanilla workstations that would copy or rewrite them for free. A photo may + * only be copied by the copy recipe (with film): the cartography table would clone it, and locking or scaling moves it + * to a fresh map id without the picture, so a photo in the table gets no result. The anvil only renames them (no + * books or other items combined in). The crafter cannot compute the load/copy results (it would hand out the static + * placeholders) and would feed our items into vanilla recipes such as map cloning, so it does not craft with them; + * the ingredients stay in the crafter. Camera and film cannot be placed as head blocks either: breaking the placed skull + * drops a plain head without our markers (from the main hand a camera takes a photo instead, but an off-hand camera + * or film would be placed). Their unwearable equippable component uses the saddle slot, which every saddleable mount + * accepts, so they are kept out of mount equipment slots (horse inventory, dispenser onto a pig or horse). + */ +public class ItemGuardListener implements Listener { + + @EventHandler + public void onPrepareResult(PrepareResultEvent event) { + Inventory inv = event.getInventory(); + if (inv instanceof CartographyInventory) { + if (CameraItems.isPhoto(inv.getItem(0))) event.setResult(null); + } else if (inv instanceof AnvilInventory) { + ItemStack first = inv.getItem(0), second = inv.getItem(1); + boolean combining = second != null && !second.isEmpty(); + if (combining && (isOurs(first) || isOurs(second))) event.setResult(null); + } + } + + @EventHandler + public void onCartographyTake(CartographyItemEvent event) { + if (event.getSlotType() == InventoryType.SlotType.RESULT + && CameraItems.isPhoto(event.getInventory().getItem(0))) { + event.setCancelled(true); + } + } + + @EventHandler + public void onCrafterCraft(CrafterCraftEvent event) { + if (!(event.getBlock().getState(false) instanceof Crafter crafter)) return; + for (ItemStack it : crafter.getInventory().getContents()) { + if (isOurs(it)) { + // An empty result makes the crafter fail like a missing recipe: nothing is consumed. + event.setResult(ItemStack.empty()); + return; + } + } + } + + @EventHandler + public void onPlace(BlockPlaceEvent event) { + ItemStack item = event.getItemInHand(); + if (isWearable(item)) event.setCancelled(true); + } + + @EventHandler + public void onDispenseEquip(BlockDispenseArmorEvent event) { + if (isWearable(event.getItem())) event.setCancelled(true); + } + + @EventHandler + public void onMountClick(InventoryClickEvent event) { + if (!isMountInventory(event.getView().getTopInventory())) return; + boolean intoEquipment = event.getRawSlot() < MOUNT_EQUIPMENT_SLOTS; + ItemStack moved; + if (event.isShiftClick()) { + moved = event.getRawSlot() >= event.getView().getTopInventory().getSize() ? event.getCurrentItem() : null; + intoEquipment = true; + } else if (event.getClick() == ClickType.NUMBER_KEY) { + moved = event.getView().getBottomInventory().getItem(event.getHotbarButton()); + } else if (event.getClick() == ClickType.SWAP_OFFHAND) { + moved = event.getWhoClicked().getInventory().getItemInOffHand(); + } else { + moved = event.getCursor(); + } + if (intoEquipment && isWearable(moved)) event.setCancelled(true); + } + + @EventHandler + public void onMountDrag(InventoryDragEvent event) { + if (!isMountInventory(event.getView().getTopInventory()) || !isWearable(event.getOldCursor())) return; + for (int raw : event.getRawSlots()) { + if (raw < MOUNT_EQUIPMENT_SLOTS) { + event.setCancelled(true); + return; + } + } + } + + /** Saddle and body armour are the first two slots of a mount's inventory menu, chest slots follow. */ + private static final int MOUNT_EQUIPMENT_SLOTS = 2; + + private static boolean isMountInventory(Inventory inv) { + return inv.getHolder(false) instanceof LivingEntity; + } + + private static boolean isWearable(ItemStack item) { + return CameraItems.isCamera(item) || CameraItems.isFilm(item); + } + + private static boolean isOurs(ItemStack item) { + return isWearable(item) || CameraItems.isPhoto(item); + } +}