package com.bergerkiller.bukkit.common.internal;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import org.bukkit.Bukkit;
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.PlayerJoinEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerLoginEvent.Result;
import com.bergerkiller.bukkit.common.collections.EntityMap;
import com.bergerkiller.bukkit.common.collections.FilteredCollectionSelf;
import com.bergerkiller.bukkit.common.collections.UniqueList;
import com.bergerkiller.bukkit.common.conversion.Conversion;
import com.bergerkiller.bukkit.common.events.PacketReceiveEvent;
import com.bergerkiller.bukkit.common.events.PacketSendEvent;
import com.bergerkiller.bukkit.common.protocol.CommonPacket;
import com.bergerkiller.bukkit.common.protocol.PacketListener;
import com.bergerkiller.bukkit.common.protocol.PacketType;
import com.bergerkiller.bukkit.common.reflection.FieldAccessor;
import com.bergerkiller.bukkit.common.reflection.SafeField;
import com.bergerkiller.bukkit.common.tab.TabView;
import com.bergerkiller.bukkit.common.utils.CommonUtil;
import com.bergerkiller.bukkit.common.utils.LogicUtil;
import com.bergerkiller.bukkit.common.utils.MathUtil;
import com.bergerkiller.bukkit.common.utils.PacketUtil;
import com.bergerkiller.bukkit.common.utils.StringUtil;
public class CommonTabController implements PacketListener, Listener {
public static final int MAX_TEXT_LENGTH = 16;
private static final char[] RANDOM_STYLE_CHARS;
private final FieldAccessor<Integer> maxPlayersField;
private int serverMaxPlayers;
private int serverListWidth, serverListHeight, serverListCount;
private int customListWidth, customListHeight, customListCount;
private final EntityMap<Player, PlayerTabInfo> players = new EntityMap<Player, PlayerTabInfo>();
private TabView defaultTab;
static {
LinkedHashSet<Character> chars = new LinkedHashSet<Character>();
// Add all chars available that do not conflict with rendering
// ===========================================================================
LogicUtil.addArray(chars, '0', '1', '2', '3', '4', '5', '6', '7', '8', '9');
LogicUtil.addArray(chars, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j');
LogicUtil.addArray(chars, 'p', 'q', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
LogicUtil.addArray(chars, '+', '-', '=', '|', '(', ')', '{', '}', '[', ']');
// ===========================================================================
// Also add all upper-case versions
for (Character c : chars.toArray(new Character[0])) {
chars.add(Character.toUpperCase(c));
}
// To char array and check length
RANDOM_STYLE_CHARS = Conversion.toCharArr.convert(chars);
if (RANDOM_STYLE_CHARS.length < 60) {
CommonPlugin.LOGGER.log(Level.WARNING, "Not enough unique characters to use: " + RANDOM_STYLE_CHARS.length);
}
}
protected CommonTabController() {
// Read server max players
maxPlayersField = new SafeField<Integer>(CommonNMS.getPlayerList(), "maxPlayers");
serverMaxPlayers = 0;
// Default (startup) dimensions
customListCount = serverListCount = 0;
customListWidth = serverListWidth = 1;
customListHeight = serverListHeight = 1;
// Set the default tab initial
defaultTab = TabView.DEFAULT;
}
private boolean hasChangedMaxPlayers() {
return customListCount > serverListCount;
}
/**
* Gets the width of the (total) Tab view that is being used right now
*
* @return tab width
*/
public int getWidth() {
return customListWidth;
}
/**
* Gets the height of the (total) Tab view that is being used right now
*
* @return tab height
*/
public int getHeight() {
return customListHeight;
}
/**
* Gets the width of the default player list.
* This is the width that would be used if no tabs of greater size are created.
*
* @return default width
*/
public int getDefaultWidth() {
return serverListWidth;
}
/**
* Gets the height of the default player list.
* This is the height that would be used if no tabs of greater size are created.
*
* @return default height
*/
public int getDefaultHeight() {
return serverListHeight;
}
/**
* Sets the default player list dimensions based on the player maximum.
* Bukkit.getMaxPlayers() fails when BKCommonLib enabled, and thus this
* needs to be called the next tick after enabling.
*/
public void setDefaultSize() {
// Calculate the default tab view width and height
serverMaxPlayers = Bukkit.getMaxPlayers();
int slotCount = Math.min(serverMaxPlayers, 3 * TabView.MAX_HEIGHT);
// Calculate the current dimensions of the Player List
serverListWidth = MathUtil.ceil((double) slotCount / (double) TabView.MAX_HEIGHT);
serverListHeight = MathUtil.floor((double) slotCount / (double) serverListWidth);
serverListHeight = Math.max(serverListHeight, getMinHeight(serverListWidth));
requestNewSize(serverListWidth, serverListHeight);
}
/**
* Prepares the internal logic for the addition of a new tab dimension.
*
* @param width of the new tab
* @param height of the new tab
*/
public void requestNewSize(int width, int height) {
if (width > customListWidth || height > customListHeight) {
customListWidth = Math.max(customListWidth, width);
customListHeight = Math.max(customListHeight, height);
customListHeight = Math.max(customListHeight, getMinHeight(customListWidth));
// Calculate the player list player count needed
customListCount = customListWidth * customListHeight;
}
}
/**
* Forces all tabs to resend 'dirty' slot information to all their viewers.
*/
public void refreshAllTabs() {
for (PlayerTabInfo info : players.values()) {
info.refresh();
}
}
/**
* Maps a tab to a player, telling it to start rendering this new Tab.
*
* @param player to map to
* @param tab to map to the player
*/
public void showTab(Player player, TabView tab) {
getInfo(player).setCurrentTab(tab);
}
/**
* Maps a tab to all current and future players, showing it's contents to everyone
*
* @param tab to show to all players
*/
public void showTabToAll(TabView tab) {
defaultTab = tab;
for (PlayerTabInfo info : players.values()) {
info.setCurrentTab(tab);
}
}
/**
* Gets the Tab View that a player is currently viewing
*
* @param player to get the Tab View for
* @return current player Tab View
*/
public TabView getCurrentTab(Player player) {
return getInfo(player).getCurrentTab();
}
/**
* Reloads all text and ping values for a tab.
* This can be used when a large amount of changes occur.
*
* @param tab to reload
*/
public void reloadAll(TabView tab) {
for (PlayerTabInfo info : getViewers(tab)) {
info.reloadAll();
}
}
/**
* Updates the ping value of a slot in a tab
*
* @param tab to update
* @param x - coordinate of the slot
* @param y - coordinate of the slot
* @param ping value to set to
*/
public void setPing(TabView tab, int x, int y, int ping) {
for (PlayerTabInfo info : getViewers(tab)) {
info.setPing(x, y, ping);
}
}
/**
* Updates the text value of a slot in a tab
*
* @param tab to update
* @param x - coordinate of the slot
* @param y - coordinate of the slot
* @param text value to set to
*/
public void setText(TabView tab, int x, int y, String text) {
for (PlayerTabInfo info : getViewers(tab)) {
info.setText(x, y, text);
}
}
/**
* Updates the ping and text values of a slot in a tab
*
* @param tab to update
* @param x - coordinate of the slot
* @param y - coordinate of the slot
* @param text value to set to
* @param ping value to set to
*/
public void setSlot(TabView tab, int x, int y, String text, int ping) {
for (PlayerTabInfo info : getViewers(tab)) {
info.set(x, y, text, ping);
}
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerLoginFirst(PlayerLoginEvent event) {
if (event.getResult() == Result.ALLOWED) {
// Register a PlayerTabInfo instance
getInfo(event.getPlayer());
// Ensure the listeners are registered in the right order
CommonUtil.queueListenerLast(this, PlayerLoginEvent.class);
CommonUtil.queueListenerFirst(this, PlayerJoinEvent.class);
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerLogin(PlayerLoginEvent event) {
if (event.getResult() == Result.ALLOWED) {
// Temporarily set the max player count to the one as specified here
if (hasChangedMaxPlayers()) {
maxPlayersField.set(CommonNMS.getPlayerList(), customListCount);
}
}
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerJoin(PlayerJoinEvent event) {
if (hasChangedMaxPlayers()) {
// Restore server max players (required, otherwise new people can join a full server all of a sudden!)
maxPlayersField.set(CommonNMS.getPlayerList(), serverMaxPlayers);
}
// Send all the elements of the current tab for this player
getInfo(event.getPlayer()).refresh();
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
}
@Override
public void onPacketSend(PacketSendEvent event) {
if (event.getType() == PacketType.OUT_PLAYER_INFO && !event.isCancelled()) {
CommonPacket packet = event.getPacket();
String name = packet.read(PacketType.OUT_PLAYER_INFO.playerName);
int ping = packet.read(PacketType.OUT_PLAYER_INFO.ping);
boolean register = packet.read(PacketType.OUT_PLAYER_INFO.online);
event.setCancelled(!getInfo(event.getPlayer()).handlePlayerInfoPacket(name, ping, register));
}
}
private Collection<PlayerTabInfo> getViewers(final TabView currentTab) {
return new FilteredCollectionSelf<PlayerTabInfo>(players.values()) {
@Override
public boolean isFiltered(PlayerTabInfo element) {
return element.getCurrentTab() != currentTab;
}
};
}
private PlayerTabInfo getInfo(Player player) {
PlayerTabInfo info = players.get(player);
if (info == null) {
info = new PlayerTabInfo(player, customListWidth, customListHeight, defaultTab);
players.put(player, info);
}
return info;
}
private static int getMinHeight(int width) {
if (width == 1) {
return 1;
} else if (width == 2) {
return 11;
} else if (width == 3) {
return 14;
} else {
return 1;
}
}
/**
* Stores the information as known to a single Player
*/
private static class PlayerTabInfo {
private final Player player;
private final Map<String, Integer> defaultView = new LinkedHashMap<String, Integer>();
private final UniqueList<String> names = new UniqueList<String>();
private final String[] text;
private final int[] ping;
private final int width, height, count;
private TabView currentTab;
private int dirtyStartIndex;
public PlayerTabInfo(Player player, int width, int height, TabView currentTab) {
this.player = player;
this.width = width;
this.height = height;
this.count = width * height;
this.text = new String[this.count];
this.ping = new int[this.count];
this.setCurrentTab(currentTab);
// No need to refresh if it is the default tab initially
if (currentTab == TabView.DEFAULT) {
this.dirtyStartIndex = this.count;
}
}
public TabView getCurrentTab() {
return this.currentTab;
}
public void setCurrentTab(TabView currentTab) {
// No need to do anything if it's no valid tab change
if (this.currentTab == currentTab) {
return;
}
// If the current tab was DEFAULT, update the names
if (this.currentTab == TabView.DEFAULT) {
this.names.clear();
this.names.addAll(this.defaultView.keySet());
}
this.currentTab = currentTab;
// Only need to clear (and not send or add names) if EMPTY
if (currentTab == TabView.EMPTY) {
Arrays.fill(this.text, TabView.TEXT_DEFAULT);
Arrays.fill(this.ping, TabView.PING_DEFAULT);
return;
}
// Write all information from the tab to this class
reloadAll();
}
/**
* Forces all values to be re-set and re-sent to the player
*/
public void reloadAll() {
int tabWidth = currentTab.getWidth();
int tabHeight = currentTab.getHeight();
int x, y, index;
String tabText;
int tabPing;
for (x = 0; x < this.width; x++) {
for (y = 0; y < this.height; y++) {
index = getIndex(x, y);
// Read from the tab if in bounds
if (x < tabWidth && y < tabHeight) {
tabText = currentTab.getText(x, y);
tabPing = currentTab.getPing(x, y);
} else {
tabText = TabView.TEXT_DEFAULT;
tabPing = TabView.PING_DEFAULT;
}
this.text[index] = tabText;
this.ping[index] = tabPing;
}
}
// Refresh from the first element
this.dirtyStartIndex = 0;
}
public void setText(int x, int y, String text) {
int index = getIndex(x, y);
set(index, text, ping[index]);
}
public void set(int x, int y, String text, int ping) {
set(getIndex(x, y), text, ping);
}
private void set(int index, String text, int ping) {
if (this.text[index].equals(text)) {
// Only set ping
setPing(index, ping);
return;
}
this.ping[index] = ping;
this.text[index] = text;
// Resend all text past the index
if (index < this.dirtyStartIndex) {
this.dirtyStartIndex = index;
}
}
public void setPing(int x, int y, int ping) {
setPing(getIndex(x, y), ping);
}
private void setPing(int index, int ping) {
if (this.ping[index] == ping) {
return;
}
this.ping[index] = ping;
// Can't update ping if out of bounds (and thus not shown!)
if (index >= names.size()) {
return;
}
// We can (safely) instantly send here
// If text changes too, oh well, too bad!
this.showSlot(names.get(index), ping);
}
public boolean handlePlayerInfoPacket(String name, int ping, boolean register) {
// Update the default view
if (register) {
defaultView.put(name, ping);
} else {
defaultView.remove(name);
}
// If the default tab is shown, allow it to go through
return this.currentTab == TabView.DEFAULT;
}
public void refresh() {
if (this.dirtyStartIndex < this.count) {
this.refresh(this.dirtyStartIndex);
this.dirtyStartIndex = this.count;
}
}
private void refresh(int startIndex) {
// Hide the old contents
if (startIndex == 0) {
// Send removal messages for all names and clear the names
for (String name : names) {
hideSlot(name);
}
names.clear();
} else {
// Send removal messages and remove names past the index
while ((names.size() - 1) >= startIndex) {
hideSlot(names.remove(startIndex));
}
}
// Show the new contents
if (currentTab == TabView.DEFAULT) {
Iterator<Entry<String, Integer>> iter = LogicUtil.skipIterator(this.defaultView.entrySet().iterator(), startIndex);
while (iter.hasNext()) {
Entry<String, Integer> entry = iter.next();
showSlot(entry.getKey(), entry.getValue());
}
} else if (currentTab != TabView.EMPTY) {
// Find out the end-index to stop showing information at
// No-slot is better than an empty slot, it reduces network usage
int endIndex = this.count - 1;
while (endIndex >= 0 && text[endIndex].equals(TabView.TEXT_DEFAULT)) {
endIndex--;
}
// Show the slots from start index to end index
for (int i = startIndex; i <= endIndex; i++) {
showSlot(getName(text[i]), ping[i]);
}
}
}
private String getName(String text) {
if (text.length() > MAX_TEXT_LENGTH) {
text = text.substring(0, MAX_TEXT_LENGTH);
}
if (names.add(text)) {
return text;
}
// Get rid of 2 chars before the maximum length, since we need to append style chars...
int textLength = Math.min(text.length(), MAX_TEXT_LENGTH - 2);
StringBuilder uniqueNameBuilder = new StringBuilder(textLength + 2);
// Initial name + chat style char
// Increment text length since we don't want to trim off the chat style char
uniqueNameBuilder.append(text);
uniqueNameBuilder.setLength(textLength);
uniqueNameBuilder.append(StringUtil.CHAT_STYLE_CHAR);
textLength++;
// Let's start building!
String uniqueName;
for (char styleChar : RANDOM_STYLE_CHARS) {
uniqueNameBuilder.setLength(textLength);
uniqueNameBuilder.append(styleChar);
uniqueName = uniqueNameBuilder.toString();
if (names.add(uniqueName)) {
return uniqueName;
}
}
throw new RuntimeException("Ran out of names to generate... :(");
}
private int getIndex(int x, int y) {
return x + this.width * y;
}
private void hideSlot(String text) {
PacketUtil.sendPacket(player, PacketType.OUT_PLAYER_INFO.newInstance(text, false, 0), false);
}
private void showSlot(String text, int ping) {
PacketUtil.sendPacket(player, PacketType.OUT_PLAYER_INFO.newInstance(text, true, ping), false);
}
}
}