ProjectAres/PGM/src/main/java/tc/oc/pgm/inventory/ViewInventoryMatchModule.java

423 lines
18 KiB
Java

package tc.oc.pgm.inventory;
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 com.google.common.collect.Lists;
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.commons.bukkit.util.BukkitUtils;
import tc.oc.commons.core.commands.CommandBinder;
import tc.oc.pgm.PGMTranslations;
import tc.oc.pgm.blitz.BlitzMatchModuleImpl;
import tc.oc.pgm.doublejump.DoubleJumpMatchModule;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.ObserverInteractEvent;
import tc.oc.pgm.events.PlayerBlockTransformEvent;
import tc.oc.pgm.events.PlayerPartyChangeEvent;
import tc.oc.pgm.kits.WalkSpeedKit;
import tc.oc.pgm.blitz.BlitzMatchModule;
import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.match.Repeatable;
import tc.oc.pgm.match.inject.MatchModuleFixtureManifest;
import tc.oc.pgm.spawns.events.ParticipantSpawnEvent;
import tc.oc.time.Time;
@ListenerScope(MatchScope.LOADED)
public class ViewInventoryMatchModule extends MatchModule implements Listener {
public static class Manifest extends MatchModuleFixtureManifest<ViewInventoryMatchModule> {
@Override protected void configure() {
super.configure();
new CommandBinder(binder())
.register(InventoryCommands.class);
}
}
public static final Duration TICK = Duration.ofMillis(50);
protected final Map<Player, View> views = new HashMap<>();
protected final Map<Player, Instant> updateQueue = new HashMap<>();
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
}
@Repeatable(scope = MatchScope.LOADED, interval = @Time(ticks = 4))
public void queuedChecks() {
for(Iterator<Map.Entry<Player, Instant>> iterator = updateQueue.entrySet().iterator(); iterator.hasNext();) {
final Map.Entry<Player, Instant> entry = iterator.next();
if(entry.getValue().isAfter(Instant.now())) continue;
checkMonitoredInventories(entry.getKey());
iterator.remove();
}
}
@EventHandler
public void closeMonitoredInventory(final InventoryCloseEvent event) {
views.remove(event.getActor());
}
@EventHandler
public void playerQuit(final PlayerPartyChangeEvent event) {
views.remove(event.getPlayer().getBukkit());
}
@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.LOW, ignoreCancelled = true)
public void cancelClicks(final InventoryClickEvent event) {
final View view = views.get(event.getActor());
if(view != null && event.getInventory().equals(view.preview)) {
event.setCancelled(true);
}
}
@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<Player, View> entry : new HashSet<>(this.views.entrySet())) { // avoid ConcurrentModificationException
final Player viewer = entry.getKey();
View view = 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() ||
view.watched.getViewers().isEmpty() ||
inventory.getViewers().size() > view.watched.getViewers().size()) continue invLoop;
for(int i = 0; i < inventory.getViewers().size(); i++) {
if(!inventory.getViewers().get(i).equals(view.watched.getViewers().get(i))) {
continue invLoop;
}
}
// a watched user is in a chest
if(view.isPlayerInventory() && !playerInventory) {
inventory = view.getPlayerInventory().getHolder().getInventory();
playerInventory = true;
}
if(playerInventory) {
this.previewPlayerInventory(viewer, (PlayerInventory) inventory);
} else {
this.previewInventory(viewer, 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());
}
public boolean canPreviewInventory(Player viewer, Player holder) {
MatchPlayer matchViewer = getMatch().getPlayer(viewer);
MatchPlayer matchHolder = getMatch().getPlayer(holder);
return matchViewer != null && matchHolder != null && canPreviewInventory(matchViewer, matchHolder);
}
public boolean canPreviewInventory(MatchPlayer viewer, MatchPlayer holder) {
return viewer.isObserving() && holder.isSpawned();
}
protected void scheduleCheck(Player updater) {
updateQueue.computeIfAbsent(updater, player -> Instant.now().plus(TICK));
}
protected void checkMonitoredInventories(Player updater) {
views.forEach((viewer, view) -> {
if(view.isPlayerInventory() && updater.equals(view.getPlayerInventory().getHolder())) {
previewPlayerInventory(viewer, view.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().getMatchModule(BlitzMatchModuleImpl.class);
if(module != null) {
int livesLeft = module.livesCount(matchHolder);
ItemStack lives = new ItemStack(Material.EGG, livesLeft);
ItemMeta lifeMeta = lives.getItemMeta();
lifeMeta.addItemFlags(ItemFlag.values());
String key = livesLeft == 1 ? "match.blitz.livesRemaining.singularLives" : "match.blitz.livesRemaining.pluralLives";
lifeMeta.setDisplayName(ChatColor.GREEN + PGMTranslations.get().t(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 + PGMTranslations.get().t("specialAbility.flying", viewer));
}
DoubleJumpMatchModule djmm = matchHolder.getMatch().getMatchModule(DoubleJumpMatchModule.class);
if(djmm != null && djmm.hasKit(matchHolder)) {
specialLore.add(ChatColor.LIGHT_PURPLE + PGMTranslations.get().t("specialAbility.doubleJump", viewer));
}
double knockbackResistance = holder.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE).getValue();
if(knockbackResistance > 0) {
specialLore.add(ChatColor.LIGHT_PURPLE + PGMTranslations.get().t("specialAbility.knockbackResistance", viewer, (int) Math.ceil(knockbackResistance * 100)));
}
double knockbackReduction = holder.getKnockbackReduction();
if(knockbackReduction > 0) {
specialLore.add(ChatColor.LIGHT_PURPLE + PGMTranslations.get().t("specialAbility.knockbackReduction", viewer, (int) Math.ceil(knockbackReduction * 100)));
}
double walkSpeed = holder.getWalkSpeed();
if(walkSpeed != WalkSpeedKit.BUKKIT_DEFAULT) {
specialLore.add(ChatColor.LIGHT_PURPLE + PGMTranslations.get().t("specialAbility.walkSpeed", viewer, String.format("%.1f", walkSpeed / WalkSpeedKit.BUKKIT_DEFAULT)));
}
if(!specialLore.isEmpty()) {
ItemStack special = new ItemStack(Material.NETHER_STAR);
ItemMeta specialMeta = special.getItemMeta();
specialMeta.addItemFlags(ItemFlag.values());
specialMeta.setDisplayName(ChatColor.AQUA.toString() + ChatColor.ITALIC + PGMTranslations.get().t("player.inventoryPreview.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.addItemFlags(ItemFlag.values());
potionMeta.setDisplayName(ChatColor.AQUA.toString() + ChatColor.ITALIC + PGMTranslations.get().t("player.inventoryPreview.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 + PGMTranslations.get().t("player.inventoryPreview.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.addItemFlags(ItemFlag.values());
hungerMeta.setDisplayName(ChatColor.AQUA.toString() + ChatColor.ITALIC + PGMTranslations.get().t("player.inventoryPreview.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.addItemFlags(ItemFlag.values());
healthMeta.setDisplayName(ChatColor.AQUA.toString() + ChatColor.ITALIC + PGMTranslations.get().t("player.inventoryPreview.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.contents());
this.showInventoryPreview(viewer, realInventory, fakeInventory);
}
}
protected void showInventoryPreview(Player viewer, Inventory realInventory, Inventory fakeInventory) {
if(viewer == null) return;
View view = views.get(viewer);
if(view != null && view.watched.equals(realInventory) && view.preview.getSize() == fakeInventory.getSize()) {
view.preview.setContents(fakeInventory.contents());
} else {
view = new View(realInventory, fakeInventory);
views.put(viewer, view);
viewer.openInventory(fakeInventory);
}
}
private static class View {
final Inventory watched;
final Inventory preview;
View(Inventory watched, Inventory preview) {
this.watched = watched;
this.preview = preview;
}
boolean isPlayerInventory() {
return this.watched instanceof PlayerInventory;
}
PlayerInventory getPlayerInventory() {
return (PlayerInventory) this.watched;
}
}
}