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); + } +}