package tc.oc.pgm.inventory; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import org.apache.commons.lang.StringUtils; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.attribute.Attribute; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityRegainHealthEvent; import org.bukkit.event.entity.FoodLevelChangeEvent; import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryClickedEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.inventory.InventoryDragEvent; import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.player.PlayerPickupItemEvent; import org.bukkit.inventory.DoubleChestInventory; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.potion.PotionEffect; import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.match.MatchModule; import tc.oc.pgm.api.match.MatchScope; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.api.player.event.ObserverInteractEvent; import tc.oc.pgm.blitz.BlitzMatchModule; import tc.oc.pgm.doublejump.DoubleJumpMatchModule; import tc.oc.pgm.events.ListenerScope; import tc.oc.pgm.events.PlayerBlockTransformEvent; import tc.oc.pgm.events.PlayerPartyChangeEvent; import tc.oc.pgm.kits.WalkSpeedKit; import tc.oc.pgm.spawns.events.ParticipantSpawnEvent; import tc.oc.pgm.util.TimeUtils; import tc.oc.pgm.util.bukkit.BukkitUtils; import tc.oc.pgm.util.text.TextTranslations; @ListenerScope(MatchScope.LOADED) public class ViewInventoryMatchModule implements MatchModule, Listener { /** * Amount of milliseconds after the match begins where players may not add / remove items from * chests. */ public static final Duration CHEST_PROTECT_TIME = Duration.ofSeconds(2); public static final Duration TICK = Duration.ofMillis(50); protected final HashMap<String, InventoryTrackerEntry> monitoredInventories = new HashMap<>(); protected final HashMap<String, Instant> updateQueue = Maps.newHashMap(); public static int getInventoryPreviewSlot(int inventorySlot) { if (inventorySlot < 9) { return inventorySlot + 36; // put hotbar on bottom } if (inventorySlot < 36) { return inventorySlot; // rest of inventory } // TODO: investigate why this method doesn't work with CraftBukkit's armor slots return inventorySlot; // default } private final Match match; public ViewInventoryMatchModule(Match match) { this.match = match; match .getExecutor(MatchScope.RUNNING) .scheduleWithFixedDelay(this::checkAllMonitoredInventories, 0, 1, TimeUnit.SECONDS); } private void checkAllMonitoredInventories() { if (updateQueue.isEmpty()) return; for (Iterator<Map.Entry<String, Instant>> iterator = updateQueue.entrySet().iterator(); iterator.hasNext(); ) { Map.Entry<String, Instant> entry = iterator.next(); if (entry.getValue().isAfter(Instant.now())) continue; Player player = Bukkit.getPlayerExact(entry.getKey()); if (player != null) { checkMonitoredInventories(player); } iterator.remove(); } } @EventHandler(ignoreCancelled = true) public void checkInventoryClick(final InventoryClickEvent event) { if (event.getWhoClicked() instanceof Player) { MatchPlayer player = this.match.getPlayer((Player) event.getWhoClicked()); if (player == null) { return; } // we only cancel when the view is a chest because the other views tend to crash if (!allowedInventoryType(event.getInventory().getType())) { // cancel the click if the player cannot interact with the world or if the match has just // started if (!player.canInteract() || (player.getMatch().isRunning() && TimeUtils.isShorterThan(player.getMatch().getDuration(), CHEST_PROTECT_TIME))) { event.setCancelled(true); } } } } @EventHandler public void closeMonitoredInventory(final InventoryCloseEvent event) { this.monitoredInventories.remove(event.getPlayer().getName()); } @EventHandler public void playerQuit(final PlayerPartyChangeEvent event) { this.monitoredInventories.remove(event.getPlayer().getBukkit().getName()); } @EventHandler(ignoreCancelled = true) public void showInventories(final ObserverInteractEvent event) { if (event.getClickType() != ClickType.RIGHT) return; if (event.getPlayer().isDead()) return; if (event.getClickedParticipant() != null) { event.setCancelled(true); if (canPreviewInventory(event.getPlayer(), event.getClickedParticipant())) { this.previewPlayerInventory( event.getPlayer().getBukkit(), event.getClickedParticipant().getInventory()); } } else if (event.getClickedEntity() instanceof InventoryHolder && !(event.getClickedEntity() instanceof Player)) { event.setCancelled(true); this.previewInventory( event.getPlayer().getBukkit(), ((InventoryHolder) event.getClickedEntity()).getInventory()); } else if (event.getClickedBlockState() instanceof InventoryHolder) { event.setCancelled(true); this.previewInventory( event.getPlayer().getBukkit(), ((InventoryHolder) event.getClickedBlockState()).getInventory()); } } @EventHandler(priority = EventPriority.MONITOR) public void updateMonitoredClick(final InventoryClickedEvent event) { if (event.getWhoClicked() instanceof Player) { Player player = (Player) event.getWhoClicked(); boolean playerInventory = event.getInventory().getType() == InventoryType.CRAFTING; // cb bug fix Inventory inventory; if (playerInventory) { inventory = player.getInventory(); } else { inventory = event.getInventory(); } invLoop: for (Map.Entry<String, InventoryTrackerEntry> entry : new HashSet<>( this.monitoredInventories.entrySet())) { // avoid ConcurrentModificationException String pl = entry.getKey(); InventoryTrackerEntry tracker = entry.getValue(); // because a player can only be viewing one inventory at a time, // this is how we determine if we have a match if (inventory.getViewers().isEmpty() || tracker.getWatched().getViewers().isEmpty() || inventory.getViewers().size() > tracker.getWatched().getViewers().size()) continue invLoop; for (int i = 0; i < inventory.getViewers().size(); i++) { if (!inventory.getViewers().get(i).equals(tracker.getWatched().getViewers().get(i))) { continue invLoop; } } // a watched user is in a chest if (tracker.isPlayerInventory() && !playerInventory) { inventory = tracker.getPlayerInventory().getHolder().getInventory(); playerInventory = true; } if (playerInventory) { this.previewPlayerInventory( Bukkit.getServer().getPlayerExact(pl), (PlayerInventory) inventory); } else { this.previewInventory(Bukkit.getServer().getPlayerExact(pl), inventory); } } } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void updateMonitoredInventory(final InventoryClickEvent event) { this.scheduleCheck((Player) event.getWhoClicked()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void updateMonitoredInventory(final InventoryDragEvent event) { this.scheduleCheck((Player) event.getWhoClicked()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void updateMonitoredTransform(final PlayerBlockTransformEvent event) { MatchPlayer player = event.getPlayer(); if (player != null) this.scheduleCheck(player.getBukkit()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void updateMonitoredPickup(final PlayerPickupItemEvent event) { this.scheduleCheck(event.getPlayer()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void updateMonitoredDrop(final PlayerDropItemEvent event) { this.scheduleCheck(event.getPlayer()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void updateMonitoredDamage(final EntityDamageEvent event) { if (event.getEntity() instanceof Player) { this.scheduleCheck((Player) event.getEntity()); } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void updateMonitoredHealth(final EntityRegainHealthEvent event) { if (event.getEntity() instanceof Player) { Player player = (Player) event.getEntity(); if (player.getHealth() == player.getMaxHealth()) return; this.scheduleCheck((Player) event.getEntity()); } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void updateMonitoredHunger(final FoodLevelChangeEvent event) { this.scheduleCheck((Player) event.getEntity()); } @EventHandler(priority = EventPriority.MONITOR) public void updateMonitoredSpawn(final ParticipantSpawnEvent event) { // must have this hack so we update player's inventories when they respawn and recieve a kit ViewInventoryMatchModule.this.scheduleCheck(event.getPlayer().getBukkit()); } @Override public void unload() { monitoredInventories.clear(); updateQueue.clear(); } public boolean canPreviewInventory(Player viewer, Player holder) { MatchPlayer matchViewer = match.getPlayer(viewer); MatchPlayer matchHolder = match.getPlayer(holder); return matchViewer != null && matchHolder != null && canPreviewInventory(matchViewer, matchHolder); } public boolean canPreviewInventory(MatchPlayer viewer, MatchPlayer holder) { return viewer.isObserving() && holder.isAlive(); } protected static boolean allowedInventoryType(InventoryType type) { switch (type) { case CREATIVE: case PLAYER: return true; default: return false; } } protected void scheduleCheck(Player updater) { if (this.updateQueue.containsKey(updater.getName())) return; this.updateQueue.put(updater.getName(), Instant.now().plus(TICK)); } protected void checkMonitoredInventories(Player updater) { for (Map.Entry<String, InventoryTrackerEntry> entry : this.monitoredInventories.entrySet()) { String pl = entry.getKey(); InventoryTrackerEntry tracker = entry.getValue(); if (tracker.isPlayerInventory()) { Player holder = (Player) tracker.getPlayerInventory().getHolder(); if (updater.getName().equals(holder.getName())) { this.previewPlayerInventory( Bukkit.getServer().getPlayerExact(pl), tracker.getPlayerInventory()); } } } } protected void previewPlayerInventory(Player viewer, PlayerInventory inventory) { if (viewer == null) { return; } Player holder = (Player) inventory.getHolder(); // Ensure that the title of the inventory is <= 32 characters long to appease Minecraft's // restrictions on inventory titles String title = StringUtils.substring(holder.getDisplayName(viewer), 0, 32); Inventory preview = Bukkit.getServer().createInventory(viewer, 45, title); // handle inventory mapping for (int i = 0; i <= 35; i++) { preview.setItem(getInventoryPreviewSlot(i), inventory.getItem(i)); } MatchPlayer matchHolder = this.match.getPlayer(holder); if (matchHolder != null && matchHolder.isParticipating()) { BlitzMatchModule module = matchHolder.getMatch().getModule(BlitzMatchModule.class); if (module != null) { int livesLeft = module.getNumOfLives(holder.getUniqueId()); ItemStack lives = new ItemStack(Material.EGG, livesLeft); ItemMeta lifeMeta = lives.getItemMeta(); String key = livesLeft == 1 ? "misc.life" : "misc.lives"; lifeMeta.setDisplayName( ChatColor.GREEN + TextTranslations.translate( key, viewer, ChatColor.AQUA + String.valueOf(livesLeft) + ChatColor.GREEN)); lives.setItemMeta(lifeMeta); preview.setItem(4, lives); } List<String> specialLore = new ArrayList<>(); if (holder.getAllowFlight()) { specialLore.add( ChatColor.LIGHT_PURPLE + TextTranslations.translate("preview.flying", viewer)); } DoubleJumpMatchModule djmm = matchHolder.getMatch().getModule(DoubleJumpMatchModule.class); if (djmm != null && djmm.hasKit(matchHolder)) { specialLore.add( ChatColor.LIGHT_PURPLE + TextTranslations.translate("preview.doubleJump", viewer)); } double knockbackResistance = holder.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE).getValue(); if (knockbackResistance > 0) { specialLore.add( ChatColor.LIGHT_PURPLE + TextTranslations.translate( "preview.knockbackResistance", viewer, (int) Math.ceil(knockbackResistance * 100))); } double knockbackReduction = holder.getKnockbackReduction(); if (knockbackReduction > 0) { specialLore.add( ChatColor.LIGHT_PURPLE + TextTranslations.translate( "preview.knockbackReduction", viewer, (int) Math.ceil(knockbackReduction * 100))); } double walkSpeed = holder.getWalkSpeed(); if (walkSpeed != WalkSpeedKit.BUKKIT_DEFAULT) { specialLore.add( ChatColor.LIGHT_PURPLE + TextTranslations.translate( "preview.walkSpeed", viewer, String.format("%.1f", walkSpeed / WalkSpeedKit.BUKKIT_DEFAULT))); } if (!specialLore.isEmpty()) { ItemStack special = new ItemStack(Material.NETHER_STAR); ItemMeta specialMeta = special.getItemMeta(); specialMeta.setDisplayName( ChatColor.AQUA.toString() + ChatColor.ITALIC + TextTranslations.translate("preview.specialAbilities", viewer)); specialMeta.setLore(specialLore); special.setItemMeta(specialMeta); preview.setItem(5, special); } } // potions boolean hasPotions = holder.getActivePotionEffects().size() > 0; ItemStack potions = new ItemStack(hasPotions ? Material.POTION : Material.GLASS_BOTTLE); ItemMeta potionMeta = potions.getItemMeta(); potionMeta.setDisplayName( ChatColor.AQUA.toString() + ChatColor.ITALIC + TextTranslations.translate("preview.potionEffects", viewer)); List<String> lore = Lists.newArrayList(); if (hasPotions) { for (PotionEffect effect : holder.getActivePotionEffects()) { lore.add( ChatColor.YELLOW + BukkitUtils.potionEffectTypeName(effect.getType()) + " " + (effect.getAmplifier() + 1)); } } else { lore.add(ChatColor.YELLOW + TextTranslations.translate("preview.noPotionEffects", viewer)); } potionMeta.setLore(lore); potions.setItemMeta(potionMeta); preview.setItem(6, potions); // hunger and health ItemStack hunger = new ItemStack(Material.COOKED_BEEF, holder.getFoodLevel()); ItemMeta hungerMeta = hunger.getItemMeta(); hungerMeta.setDisplayName( ChatColor.AQUA.toString() + ChatColor.ITALIC + TextTranslations.translate("preview.hungerLevel", viewer)); hungerMeta.addItemFlags(ItemFlag.HIDE_POTION_EFFECTS); hunger.setItemMeta(hungerMeta); preview.setItem(7, hunger); ItemStack health = new ItemStack(Material.REDSTONE, (int) holder.getHealth()); ItemMeta healthMeta = health.getItemMeta(); healthMeta.setDisplayName( ChatColor.AQUA.toString() + ChatColor.ITALIC + TextTranslations.translate("preview.healthLevel", viewer)); healthMeta.addItemFlags(ItemFlag.HIDE_POTION_EFFECTS); health.setItemMeta(healthMeta); preview.setItem(8, health); // set armor manually because craftbukkit is a derp preview.setItem(0, inventory.getHelmet()); preview.setItem(1, inventory.getChestplate()); preview.setItem(2, inventory.getLeggings()); preview.setItem(3, inventory.getBoots()); this.showInventoryPreview(viewer, inventory, preview); } public void previewInventory(Player viewer, Inventory realInventory) { if (viewer == null) { return; } if (realInventory instanceof PlayerInventory) { previewPlayerInventory(viewer, (PlayerInventory) realInventory); } else { Inventory fakeInventory; if (realInventory instanceof DoubleChestInventory) { if (realInventory.hasCustomName()) { fakeInventory = Bukkit.createInventory(viewer, realInventory.getSize(), realInventory.getName()); } else { fakeInventory = Bukkit.createInventory(viewer, realInventory.getSize()); } } else { if (realInventory.hasCustomName()) { fakeInventory = Bukkit.createInventory(viewer, realInventory.getType(), realInventory.getName()); } else { fakeInventory = Bukkit.createInventory(viewer, realInventory.getType()); } } fakeInventory.setContents(realInventory.getContents()); this.showInventoryPreview(viewer, realInventory, fakeInventory); } } protected void showInventoryPreview( Player viewer, Inventory realInventory, Inventory fakeInventory) { if (viewer == null) { return; } InventoryTrackerEntry entry = this.monitoredInventories.get(viewer.getName()); if (entry != null && entry.getWatched().equals(realInventory) && entry.getPreview().getSize() == fakeInventory.getSize()) { entry.getPreview().setContents(fakeInventory.getContents()); } else { entry = new InventoryTrackerEntry(realInventory, fakeInventory); this.monitoredInventories.put(viewer.getName(), entry); viewer.openInventory(fakeInventory); } } }