/******************************************************************************* * This file is part of ASkyBlock. * * ASkyBlock is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ASkyBlock is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ASkyBlock. If not, see <http://www.gnu.org/licenses/>. *******************************************************************************/ package com.wasteofplastic.askyblock.listeners; import java.math.BigInteger; import java.text.DecimalFormat; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.AsyncPlayerChatEvent; import com.wasteofplastic.askyblock.ASkyBlock; import com.wasteofplastic.askyblock.Settings; import com.wasteofplastic.askyblock.util.Util; /** * This class is to catch chats and do two things: (1) substitute in the island level to the chat string * and (2) implement team chat. As it can be called asynchronously (and usually it is when a player chats) * it cannot access any HashMaps or Bukkit APIs without running the risk of a clash with another thread * or the main thread. As such two things are done: * 1. To handle the level substitution, a thread-safe hashmap of players and levels is stored and updated * in this class. * 2. To handle team chat, a thread-safe hashmap is used to store whether team chat is on for a player or not * and if it is, the team chat itself is queued to run on the next server tick, i.e., in the main thread * This all ensures it's thread-safe. * @author tastybento * */ public class ChatListener implements Listener { private final ASkyBlock plugin; private final Map<UUID,Boolean> teamChatUsers; private final Map<UUID,String> playerLevels; private final Map<UUID, String> playerChallengeLevels; // List of which admins are spying or not on team chat private final Set<UUID> spies; private static final boolean DEBUG = false; /** * @param plugin - ASkyBlock plugin object */ public ChatListener(ASkyBlock plugin) { this.teamChatUsers = new ConcurrentHashMap<>(); this.playerLevels = new ConcurrentHashMap<>(); this.playerChallengeLevels = new ConcurrentHashMap<>(); this.plugin = plugin; // Add all online player Levels for (Player player : plugin.getServer().getOnlinePlayers()) { playerLevels.put(player.getUniqueId(), String.valueOf(plugin.getPlayers().getIslandLevel(player.getUniqueId()))); playerChallengeLevels.put(player.getUniqueId(), plugin.getChallenges().getChallengeLevel(player)); } // Initialize spies spies = new HashSet<>(); } private static final BigInteger THOUSAND = BigInteger.valueOf(1000); /** * Provides an easy way to "fancy" the island level in chat * @since 3.0.8.3 */ private static final TreeMap<BigInteger, String> LEVELS; static { LEVELS = new TreeMap<>(); LEVELS.put(THOUSAND, "k"); LEVELS.put(THOUSAND.pow(2), "M"); LEVELS.put(THOUSAND.pow(3), "G"); LEVELS.put(THOUSAND.pow(4), "T"); } @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) public void onChat(final AsyncPlayerChatEvent event) { if (DEBUG) plugin.getLogger().info("DEBUG: " + event.getEventName()); // Substitute variable - thread safe String level = ""; if (playerLevels.containsKey(event.getPlayer().getUniqueId())) { level = playerLevels.get(event.getPlayer().getUniqueId()); if(Settings.fancyIslandLevelDisplay) { BigInteger levelValue = BigInteger.valueOf(Long.valueOf(level)); Map.Entry<BigInteger, String> stage = LEVELS.floorEntry(levelValue); if (stage != null) { // level > 1000 // 1 052 -> 1.0k // 1 527 314 -> 1.5M // 3 874 130 021 -> 3.8G // 4 002 317 889 -> 4.0T level = new DecimalFormat("#.#").format(levelValue.divide(stage.getKey().divide(THOUSAND)).doubleValue()/1000.0) + stage.getValue(); } } } if (DEBUG) { plugin.getLogger().info("DEBUG: player level = " + level); plugin.getLogger().info("DEBUG: getFormat = " + event.getFormat()); plugin.getLogger().info("DEBUG: getMessage = " + event.getMessage()); } String format = event.getFormat(); if (!Settings.chatLevelPrefix.isEmpty()) { format = format.replace(Settings.chatLevelPrefix, level); if (DEBUG) plugin.getLogger().info("DEBUG: format (island level substitute) = " + format); } if (!Settings.chatChallengeLevelPrefix.isEmpty()) { level = ""; if (playerChallengeLevels.containsKey(event.getPlayer().getUniqueId())) { level = playerChallengeLevels.get(event.getPlayer().getUniqueId()); } format = format.replace(Settings.chatChallengeLevelPrefix, level); if (DEBUG) plugin.getLogger().info("DEBUG: format (challenge level sub) = " + format); } event.setFormat(format); if (DEBUG) plugin.getLogger().info("DEBUG: format set"); // Team chat if (Settings.teamChat && teamChatUsers.containsKey(event.getPlayer().getUniqueId())) { if (DEBUG) plugin.getLogger().info("DEBUG: team chat"); // Cancel the event event.setCancelled(true); // Queue the sync task because you cannot use HashMaps asynchronously. Delaying to the next tick // won't be a major issue for synch events either. Bukkit.getScheduler().runTask(plugin, new Runnable() { @Override public void run() { teamChat(event,event.getMessage()); }}); } } private void teamChat(final AsyncPlayerChatEvent event, String message) { Player player = event.getPlayer(); UUID playerUUID = player.getUniqueId(); //Bukkit.getLogger().info("DEBUG: post: " + message); // Is team chat on for this player // Find out if this player is in a team (should be if team chat is on) // TODO: remove when player resets or leaves team if (plugin.getPlayers().inTeam(playerUUID)) { List<UUID> teamMembers = plugin.getPlayers().getMembers(player.getUniqueId()); // Tell only the team members if they are online boolean onLine = false; if (Settings.chatIslandPlayer.isEmpty()) { message = plugin.myLocale(playerUUID).teamChatPrefix + message; } else { message = plugin.myLocale(playerUUID).teamChatPrefix.replace(Settings.chatIslandPlayer,player.getDisplayName()) + message; } for (UUID teamMember : teamMembers) { Player teamPlayer = plugin.getServer().getPlayer(teamMember); if (teamPlayer != null) { Util.sendMessage(teamPlayer, message); if (!teamMember.equals(playerUUID)) { onLine = true; } } } // Spy function if (onLine) { for (Player onlinePlayer: plugin.getServer().getOnlinePlayers()) { if (spies.contains(onlinePlayer.getUniqueId()) && onlinePlayer.hasPermission(Settings.PERMPREFIX + "mod.spy")) { Util.sendMessage(onlinePlayer, ChatColor.RED + "[TCSpy] " + ChatColor.WHITE + message); } } //Log teamchat if(Settings.logTeamChat) plugin.getLogger().info(ChatColor.stripColor(message)); } if (!onLine) { Util.sendMessage(player, ChatColor.RED + plugin.myLocale(playerUUID).teamChatNoTeamAround); Util.sendMessage(player, ChatColor.RED + plugin.myLocale(playerUUID).teamChatStatusOff); teamChatUsers.remove(playerUUID); } } else { Util.sendMessage(player, ChatColor.RED + plugin.myLocale(playerUUID).teamChatNoTeamAround); Util.sendMessage(player, ChatColor.RED + plugin.myLocale(playerUUID).teamChatStatusOff); // Not in a team any more so delete teamChatUsers.remove(playerUUID); } } /** * Adds player to team chat * @param playerUUID - the player's UUID */ public void setPlayer(UUID playerUUID) { this.teamChatUsers.put(playerUUID,true); } /** * Removes player from team chat * @param playerUUID - the player's UUID */ public void unSetPlayer(UUID playerUUID) { this.teamChatUsers.remove(playerUUID); } /** * Whether the player has team chat on or not * @param playerUUID - the player's UUID * @return true if team chat is on */ public boolean isTeamChat(UUID playerUUID) { return this.teamChatUsers.containsKey(playerUUID); } /** * Store the player's level for use in their chat tag * @param playerUUID - the player's UUID * @param l */ public void setPlayerLevel(UUID playerUUID, long l) { //plugin.getLogger().info("DEBUG: putting " + playerUUID.toString() + " Level " + level); playerLevels.put(playerUUID, String.valueOf(l)); } /** * Store the player's challenge level for use in their chat tag * @param player */ public void setPlayerChallengeLevel(Player player) { //plugin.getLogger().info("DEBUG: setting player's challenge level to " + plugin.getChallenges().getChallengeLevel(player)); playerChallengeLevels.put(player.getUniqueId(), plugin.getChallenges().getChallengeLevel(player)); } /** * Return the player's level for use in chat - async safe * @param playerUUID - the player's UUID * @return Player's level as string */ public String getPlayerLevel(UUID playerUUID) { return playerLevels.get(playerUUID); } /** * Return the player's challenge level for use in chat - async safe * @param playerUUID - the player's UUID * @return challenge level as string or empty string none */ public String getPlayerChallengeLevel(UUID playerUUID) { if (playerChallengeLevels.containsKey(playerUUID)) return playerChallengeLevels.get(playerUUID); return ""; } /** * Toggles team chat spy. Spy must also have the spy permission to see chats * @param playerUUID - the player's UUID * @return true if toggled on, false if toggled off */ public boolean toggleSpy(UUID playerUUID) { if (spies.contains(playerUUID)) { spies.remove(playerUUID); return false; } else { spies.add(playerUUID); return true; } } }