From 6e622ad2f3c0e6bdfc56ffa1e74a59e506673341 Mon Sep 17 00:00:00 2001 From: CoolJWB Date: Tue, 14 Jul 2020 21:00:22 +0200 Subject: [PATCH] Potionspy remake Remade the entire potionspy and monitor class to avoid spam in the chat. Furthermore, there is now a way to look at a history of potions thrown (individually and globally) however this history is limited to avoid too much useless data in the memory. --- .../totalfreedommod/Monitors.java | 162 ++++++++++---- .../blocking/EventBlocker.java | 3 +- .../command/Command_potionspy.java | 209 +++++++++++++++++- .../totalfreedommod/util/FUtil.java | 31 +++ 4 files changed, 357 insertions(+), 48 deletions(-) diff --git a/src/main/java/me/totalfreedom/totalfreedommod/Monitors.java b/src/main/java/me/totalfreedom/totalfreedommod/Monitors.java index 7147f9a0..ae05de95 100644 --- a/src/main/java/me/totalfreedom/totalfreedommod/Monitors.java +++ b/src/main/java/me/totalfreedom/totalfreedommod/Monitors.java @@ -1,26 +1,61 @@ package me.totalfreedom.totalfreedommod; -import java.text.DecimalFormat; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.Getter; import me.totalfreedom.totalfreedommod.util.FUtil; +import org.bukkit.Bukkit; import org.bukkit.ChatColor; -import org.bukkit.Location; -import org.bukkit.Material; import org.bukkit.entity.Player; +import org.bukkit.entity.ThrownPotion; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.entity.LingeringPotionSplashEvent; import org.bukkit.event.entity.PotionSplashEvent; -import org.bukkit.projectiles.ProjectileSource; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; public class Monitors extends FreedomService { - - private final DecimalFormat decimalFormat = new DecimalFormat("#"); - private String potionSpyPrefix = ChatColor.DARK_GRAY + "[" + ChatColor.YELLOW + "PotionSpy" + ChatColor.DARK_GRAY + "] "; + @Getter + private List> allThrownPotions = new ArrayList<>(); + private Map> recentlyThrownPotions = new HashMap<>(); + private final List badPotionEffects = new ArrayList<>(Arrays.asList(PotionEffectType.BLINDNESS, + PotionEffectType.LEVITATION, PotionEffectType.CONFUSION, PotionEffectType.SLOW, PotionEffectType.SLOW_DIGGING, PotionEffectType.HUNGER)); // A list of all effects that count as "troll". @Override public void onStart() { + Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, () -> + { + for (Player player : recentlyThrownPotions.keySet()) + { + if (plugin.al.isAdmin(player) && plugin.al.getAdmin(player).getPotionSpy()) + { + List playerThrownPotions = recentlyThrownPotions.get(player); + ThrownPotion latestThrownPotion = playerThrownPotions.get(playerThrownPotions.size() - 1); // Get most recently thrown potion for the position. + int potionsThrown = playerThrownPotions.size(); + boolean trollPotions = false; + + for(ThrownPotion potion : playerThrownPotions) + { + if(isTrollPotion(potion)) + { + trollPotions = true; + } + } + + FUtil.playerMsg(player, ChatColor.translateAlternateColorCodes('&', String.format("&8[&ePotionSpy&8] &r%s splashed %s %s at X: %s Y: %s Z: %s in the world '%s'%s.", + player.getName(), potionsThrown, potionsThrown == 1 ? "potion" : "potions", latestThrownPotion.getLocation().getBlockX(), latestThrownPotion.getLocation().getBlockY(), latestThrownPotion.getLocation().getBlockZ(), + latestThrownPotion.getWorld().getName(), trollPotions ? " &c(most likely troll potion/potions)" : ""))); + } + } + recentlyThrownPotions.clear(); + }, 0L, 40L); } @Override @@ -31,26 +66,25 @@ public class Monitors extends FreedomService @EventHandler(priority = EventPriority.MONITOR) public void onLingeringPotionSplash(LingeringPotionSplashEvent event) { - ProjectileSource source = event.getEntity().getShooter(); - - if (!(source instanceof Player)) + if (event.getEntity().getShooter() instanceof Player) { - return; - } - Player player = (Player)source; - - if (plugin.al.isAdmin((Player)event.getEntity().getShooter())) - { - return; - } - final Material droppedItem = event.getEntity().getItem().getType(); - final Location location = player.getLocation(); - - for (Player p : server.getOnlinePlayers()) - { - if (plugin.al.isAdmin(p) && plugin.al.getAdmin(p).getPotionSpy()) + ThrownPotion potion = event.getEntity(); + if(potion.getShooter() instanceof Player) { - FUtil.playerMsg(p, potionSpyPrefix + ChatColor.WHITE + player.getName() + " splashed " + event.getEntity().getItem().getAmount() + " " + droppedItem + " at X: " + decimalFormat.format(location.getX()) + ", Y: " + decimalFormat.format(location.getY()) + ", Z: " + decimalFormat.format(location.getZ()) + ", in the world '" + location.getWorld().getName() + "'."); + Player player = (Player)potion.getShooter(); + + recentlyThrownPotions.putIfAbsent(player, new ArrayList<>()); + recentlyThrownPotions.get(player).add(potion); + allThrownPotions.add(new AbstractMap.SimpleEntry<>(potion, System.currentTimeMillis())); + + if(recentlyThrownPotions.get(player).size() > 128) + { + recentlyThrownPotions.get(player).remove(0); + } + if(allThrownPotions.size() > 1024) + { + allThrownPotions.remove(0); // Remove the first element in the set. + } } } } @@ -58,27 +92,67 @@ public class Monitors extends FreedomService @EventHandler(priority = EventPriority.MONITOR) public void onPotionSplash(PotionSplashEvent event) { - ProjectileSource source = event.getEntity().getShooter(); - - if (!(source instanceof Player)) + if (event.getEntity().getShooter() instanceof Player) { - return; - } - Player player = (Player)source; - - if (plugin.al.isAdmin((Player)event.getEntity().getShooter())) - { - return; - } - final Material droppedItem = event.getPotion().getItem().getType(); - final Location location = player.getLocation(); - - for (Player p : server.getOnlinePlayers()) - { - if (plugin.al.isAdmin(p) && plugin.al.getAdmin(p).getPotionSpy()) + ThrownPotion potion = event.getEntity(); + if(potion.getShooter() instanceof Player) { - FUtil.playerMsg(p, potionSpyPrefix + ChatColor.WHITE + player.getName() + " splashed " + event.getEntity().getItem().getAmount() + " " + droppedItem + " at X: " + decimalFormat.format(location.getX()) + ", Y: " + decimalFormat.format(location.getY()) + ", Z: " + decimalFormat.format(location.getZ()) + ", in the world '" + location.getWorld().getName() + "'."); + Player player = (Player)potion.getShooter(); + + recentlyThrownPotions.putIfAbsent(player, new ArrayList<>()); + recentlyThrownPotions.get(player).add(potion); + allThrownPotions.add(new AbstractMap.SimpleEntry<>(potion, System.currentTimeMillis())); + + if(recentlyThrownPotions.get(player).size() > 128) + { + recentlyThrownPotions.get(player).remove(0); + } + if(allThrownPotions.size() > 1024) + { + allThrownPotions.remove(0); // Remove the first element in the set. + } } } } -} \ No newline at end of file + + /** + * Get a list of potions the player has thrown with unix time stamps. + * @param player The player that has thrown potions. + * @return A list of map entries with the key as the thrown potion and the value as a long (unix time stamp when the throw happened). + */ + public List> getPlayerThrownPotions(Player player) + { + List> thrownPotions = new ArrayList<>(); + + for(Map.Entry potionEntry : allThrownPotions) + { + ThrownPotion potion = potionEntry.getKey(); + if(potion.getShooter() != null && potion.getShooter().equals(player)) + { + thrownPotions.add(potionEntry); + } + } + + return thrownPotions; + } + + /** + * Detects if a thrown potion is most likely a troll potion. + * @param potion Any thrown potion that should be checked. + * @return A boolean that indicates if the potion param is most likely a troll potion. + */ + public boolean isTrollPotion(ThrownPotion potion) + { + int badEffectsDetected = 0; + + for(PotionEffect effect : potion.getEffects()) + { + if(badPotionEffects.contains(effect.getType()) && effect.getAmplifier() > 2 && effect.getDuration() > 200) + { + badEffectsDetected++; + } + } + + return badEffectsDetected > 0; + } +} diff --git a/src/main/java/me/totalfreedom/totalfreedommod/blocking/EventBlocker.java b/src/main/java/me/totalfreedom/totalfreedommod/blocking/EventBlocker.java index 4b54e31e..9917d90e 100644 --- a/src/main/java/me/totalfreedom/totalfreedommod/blocking/EventBlocker.java +++ b/src/main/java/me/totalfreedom/totalfreedommod/blocking/EventBlocker.java @@ -208,7 +208,8 @@ public class EventBlocker extends FreedomService // TODO: Revert back to old redstone block system when (or if) it is fixed in Bukkit, Spigot or Paper. private ArrayList redstoneBlocks = new ArrayList<>(Arrays.asList(Material.REDSTONE, Material.DISPENSER, Material.DROPPER, Material.REDSTONE_LAMP)); @EventHandler - public void onBlockPhysics(BlockPhysicsEvent event) { + public void onBlockPhysics(BlockPhysicsEvent event) + { if (!ConfigEntry.ALLOW_REDSTONE.getBoolean()) { // Check if the block is involved with redstone. diff --git a/src/main/java/me/totalfreedom/totalfreedommod/command/Command_potionspy.java b/src/main/java/me/totalfreedom/totalfreedommod/command/Command_potionspy.java index cd94100a..f0d72a81 100644 --- a/src/main/java/me/totalfreedom/totalfreedommod/command/Command_potionspy.java +++ b/src/main/java/me/totalfreedom/totalfreedommod/command/Command_potionspy.java @@ -1,24 +1,227 @@ package me.totalfreedom.totalfreedommod.command; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; import me.totalfreedom.totalfreedommod.admin.Admin; import me.totalfreedom.totalfreedommod.rank.Rank; +import me.totalfreedom.totalfreedommod.util.FUtil; +import org.apache.commons.lang.math.NumberUtils; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import org.bukkit.entity.ThrownPotion; @CommandPermissions(level = Rank.SUPER_ADMIN, source = SourceType.ONLY_IN_GAME) -@CommandParameters(description = "Allows admins to see every time a potion is splashed.", usage = "/", aliases = "potspy") +@CommandParameters(description = "Allows admins to see potions that are thrown.", usage = "/ | history [player] ", aliases = "potspy") public class Command_potionspy extends FreedomCommand { + private String titleText = "&8&m------------------&r &ePotionSpy &8&m------------------&r"; + private String validPageText = "Please specify a valid page number between 1 and %s."; + private String noPlayerRecord = "That player has not thrown any potions yet."; + private String splashedText = "&r%s splashed a potion at &eX: %s Y: %s Z: %s&r\nin the world '&e%s&r' about &e%s &rago%s."; + private String bottomText = "&8&m--------------------&r &e%s / %s &8&m--------------------&r"; @Override public boolean run(CommandSender sender, Player playerSender, Command cmd, String commandLabel, String[] args, boolean senderIsConsole) { Admin admin = plugin.al.getAdmin(playerSender); - admin.setPotionSpy(!admin.getPotionSpy()); + + if(args.length <= 0) + { + setPotionSpyState(admin, !admin.getPotionSpy()); + return true; + } + else + { + switch (args[0].toLowerCase()) + { + case "enable": + case "on": + setPotionSpyState(admin, true); + break; + + case "disable": + case "off": + setPotionSpyState(admin, false); + break; + + case "history": + if(args.length == 3) + { + Player player = Bukkit.getPlayer(args[1]); + if(player == null) + { + msg(sender, "Please specify a valid player name."); + return true; + } + + List> thrownPotions = new ArrayList<>(); + thrownPotions.addAll(plugin.mo.getPlayerThrownPotions(player)); // Make a copy of the list to avoid modifying the original. + + List potionThrowNotifications = new ArrayList<>(); + int lastPage = (int)Math.ceil(thrownPotions.size() / 5.0); + + if(thrownPotions.isEmpty()) + { + msg(sender, noPlayerRecord); + return true; + } + if(!NumberUtils.isNumber(args[2])) + { + msg(sender, String.format(validPageText, lastPage)); + return true; + } + + Collections.reverse(thrownPotions); + int pageIndex = Integer.parseInt(args[2]); + + for(Map.Entry potionEntry : thrownPotions) + { + ThrownPotion potion = potionEntry.getKey(); + boolean trollPotions = plugin.mo.isTrollPotion(potion); + + potionThrowNotifications.add(ChatColor.translateAlternateColorCodes('&', String.format(splashedText, player.getName(), potion.getLocation().getBlockX(), + potion.getLocation().getBlockY(), potion.getLocation().getBlockZ(), potion.getWorld().getName(), getUnixTimeDifference(potionEntry.getValue(), System.currentTimeMillis()), trollPotions ? " &c(most likely troll potion/potions)" : ""))); + } + + List page = FUtil.getPageFromList(potionThrowNotifications, 5, pageIndex - 1); + if(!page.isEmpty()) + { + msg(sender, ChatColor.translateAlternateColorCodes('&', titleText)); + for (String potionThrowNotification : page) + { + msg(sender, potionThrowNotification); + } + } + else + { + msg(sender, String.format(validPageText, lastPage)); + return true; + } + + msg(sender, ChatColor.translateAlternateColorCodes('&', String.format(bottomText, pageIndex, lastPage))); + } + else if(args.length == 2) + { + List> thrownPotions = new ArrayList<>(); + thrownPotions.addAll(plugin.mo.getAllThrownPotions()); // Make a copy of the list to avoid modifying the original. + + List potionThrowNotifications = new ArrayList<>(); + int lastPage = (int)Math.ceil(thrownPotions.size() / 5.0); + + if(thrownPotions.isEmpty()) + { + if(Bukkit.getPlayer(args[1]) != null) + { + msg(sender, noPlayerRecord); + } + else + { + msg(sender, "No potions have been thrown yet."); + } + return true; + } + if(!NumberUtils.isNumber(args[1])) + { + msg(sender, String.format(validPageText, lastPage)); + return true; + } + + Collections.reverse(thrownPotions); + int pageIndex = Integer.parseInt(args[1]); + + for(Map.Entry potionEntry : thrownPotions) + { + ThrownPotion potion = potionEntry.getKey(); + Player player = (Player)potion.getShooter(); + boolean trollPotions = plugin.mo.isTrollPotion(potion); + + if (player != null) + { + potionThrowNotifications.add(ChatColor.translateAlternateColorCodes('&', String.format(splashedText, player.getName(), potion.getLocation().getBlockX(), + potion.getLocation().getBlockY(), potion.getLocation().getBlockZ(), potion.getWorld().getName(), getUnixTimeDifference(potionEntry.getValue(), System.currentTimeMillis()), trollPotions ? " &c(most likely troll potion/potions)" : ""))); + } + } + + List page = FUtil.getPageFromList(potionThrowNotifications, 5, pageIndex - 1); + if(!page.isEmpty()) + { + msg(sender, ChatColor.translateAlternateColorCodes('&', titleText)); + for (String potionThrowNotification : page) + { + msg(sender, potionThrowNotification); + } + } + else + { + msg(sender, String.format(validPageText, lastPage)); + return true; + } + + msg(sender, ChatColor.translateAlternateColorCodes('&', String.format(bottomText, pageIndex, lastPage))); + } + else + { + return false; + } + break; + default: + return false; + } + } + return true; + } + + /** + * Sets and updates the potion spy state for an admin. + * @param admin The admin that the state should be changed for. + * @param state A boolean that will set the state of potion spy for the admin (enabled or disabled). + */ + private void setPotionSpyState(Admin admin, boolean state) + { + admin.setPotionSpy(state); plugin.al.save(admin); plugin.al.updateTables(); msg("PotionSpy is now " + (admin.getPotionSpy() ? "enabled." : "disabled.")); - return true; + } + + /** + * Get the unix time difference in string format (1h, 30m, 15s). + * @param past The unix time at the start. + * @param now The current unix time. + * @return A string that displays the time difference between the two unix time values. + */ + private String getUnixTimeDifference(long past, long now) + { + long unix = now - past; + long seconds = Math.round(unix / 1000.0); + if(seconds < 60) + { + return seconds + "s"; + } + else + { + long minutes = Math.round(seconds / 60.0); + if(minutes < 60) + { + return minutes + "m"; + } + else + { + long hours = Math.round(minutes / 60.0); + if(hours < 24) + { + return hours + "h"; + } + else + { + return Math.round(hours / 24.0) + "d"; + } + } + } } } diff --git a/src/main/java/me/totalfreedom/totalfreedommod/util/FUtil.java b/src/main/java/me/totalfreedom/totalfreedommod/util/FUtil.java index fec53507..cc9e9b1b 100644 --- a/src/main/java/me/totalfreedom/totalfreedommod/util/FUtil.java +++ b/src/main/java/me/totalfreedom/totalfreedommod/util/FUtil.java @@ -172,6 +172,37 @@ public class FUtil return Arrays.asList(string.split(", ")); } + + /** + * A way to get a sublist with a page index and a page size. + * @param list A list of objects that should be split into pages. + * @param size The size of the pages. + * @param index The page index, if outside of bounds error will be thrown. The page index starts at 0 as with all lists. + * @return A list of objects that is the page that has been selected from the previous last parameter. + */ + public static List getPageFromList(List list, int size, int index) + { + try + { + if (size >= list.size()) + { + return list; + } + else if (size * (index + 1) <= list.size()) + { + return list.subList(size * index, size * (index + 1)); + } + else + { + return list.subList(size * index, (size * index) + (list.size() % size)); + } + } + catch (IndexOutOfBoundsException e) + { + return new ArrayList<>(); + } + } + public static List getAllMaterialNames() { List names = new ArrayList<>();