codecrafter47.bungeetablistplus.tablisthandler.logic.AbstractTabListLogic.java Source code

Java tutorial

Introduction

Here is the source code for codecrafter47.bungeetablistplus.tablisthandler.logic.AbstractTabListLogic.java

Source

/*
 * BungeeTabListPlus - a BungeeCord plugin to customize the tablist
 *
 * Copyright (C) 2014 - 2015 Florian Stober
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package codecrafter47.bungeetablistplus.tablisthandler.logic;

import codecrafter47.bungeetablistplus.api.bungee.Icon;
import codecrafter47.bungeetablistplus.protocol.PacketListenerResult;
import codecrafter47.bungeetablistplus.tablisthandler.PlayerTablistHandler;
import codecrafter47.bungeetablistplus.util.Object2IntHashMultimap;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import it.unimi.dsi.fastutil.ints.IntIterator;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.md_5.bungee.protocol.DefinedPacket;
import net.md_5.bungee.protocol.packet.PlayerListHeaderFooter;
import net.md_5.bungee.protocol.packet.PlayerListItem;
import net.md_5.bungee.protocol.packet.Team;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static java.lang.Math.min;
import static net.md_5.bungee.protocol.packet.PlayerListItem.Action.*;

public abstract class AbstractTabListLogic extends TabListHandler {

    protected static final String[] fakePlayerUsernames = new String[80];
    protected static final UUID[] fakePlayerUUIDs = new UUID[80];
    protected static final Set<String> fakePlayerUsernameSet;
    protected static final Set<UUID> fakePlayerUUIDSet;
    protected static final String[][] EMPTY_PROPRTIES = new String[0][];

    private static boolean teamCollisionRuleSupported;

    static {
        for (int i = 0; i < 80; i++) {
            fakePlayerUsernames[i] = String.format(" BTLP Slot %02d", i);
            fakePlayerUUIDs[i] = UUID
                    .nameUUIDFromBytes(("OfflinePlayer:" + fakePlayerUsernames[i]).getBytes(Charsets.UTF_8));
        }
        fakePlayerUsernameSet = ImmutableSet.copyOf(fakePlayerUsernames);
        fakePlayerUUIDSet = ImmutableSet.copyOf(fakePlayerUUIDs);

        try {
            Team.class.getDeclaredMethod("setCollisionRule", String.class);
            teamCollisionRuleSupported = true;
        } catch (NoSuchMethodException e) {
            teamCollisionRuleSupported = false;
        }
    }

    protected final Map<UUID, TabListItem> serverTabList = new ConcurrentHashMap<>();
    protected String serverHeader = null;
    protected String serverFooter = null;

    protected final Map<String, TeamData> serverTeams = new Object2ObjectOpenHashMap<>();
    protected final Map<String, String> playerToTeamMap = new Object2ObjectOpenHashMap<>();
    protected final Object2IntMap<String> nameToSlotMap;

    protected Object2IntHashMultimap<UUID> skinUuidToSlotMap = new Object2IntHashMultimap<>();
    protected Object2IntMap<UUID> uuidToSlotMap;

    {
        uuidToSlotMap = new Object2IntOpenHashMap<>();
        uuidToSlotMap.defaultReturnValue(-1);
        nameToSlotMap = new Object2IntOpenHashMap<>();
        nameToSlotMap.defaultReturnValue(-1);
    }

    protected UUID[] clientUuid = new UUID[80];
    protected String[] clientUsername = new String[80];
    protected Icon[] clientSkin = new Icon[80];
    protected String[] clientText = new String[80];
    protected int[] clientPing = new int[80];
    protected String clientHeader = null;
    protected String clientFooter = null;

    protected int size = 0;
    private int requestedSize = 0;

    protected boolean passtrough = true;

    @Setter
    protected PlayerTablistHandler.ResizePolicy resizePolicy = PlayerTablistHandler.ResizePolicy.DEFAULT;

    public AbstractTabListLogic(TabListHandler parent) {
        super(parent);
    }

    abstract protected UUID getUniqueId();

    abstract protected void sendPacket(DefinedPacket packet);

    @Override
    public void onConnected() {
        // add our teams to the client
        for (int i = 0; i < 80; i++) {
            Team team = new Team();
            team.setMode((byte) 0);
            team.setName(fakePlayerUsernames[i]);
            team.setDisplayName(fakePlayerUsernames[i]);
            team.setPrefix("");
            team.setSuffix("");
            team.setFriendlyFire((byte) 1);
            team.setNameTagVisibility("always");
            if (teamCollisionRuleSupported) {
                team.setCollisionRule("always");
            }
            team.setColor((byte) 0);
            team.setPlayers(new String[0]);
            sendPacket(team);
        }
    }

    @Override
    public void onDisconnected() {
        // to nothing
    }

    @Override
    public PacketListenerResult onPlayerListPacket(PlayerListItem packet) {
        return onPlayerListPacketInternal(packet);
    }

    private PacketListenerResult onPlayerListPacketInternal(PlayerListItem packet) {
        // update server tab list
        switch (packet.getAction()) {
        case ADD_PLAYER:
            for (PlayerListItem.Item item : packet.getItems()) {
                if (fakePlayerUUIDSet.contains(item.getUuid())) {
                    throw new AssertionError("UUID collision: " + item);
                }
                if (fakePlayerUsernameSet.contains(item.getUsername())) {
                    throw new AssertionError("Username collision: " + item);
                }
                serverTabList.put(item.getUuid(), new TabListItem(item));
            }
            break;
        case UPDATE_GAMEMODE:
            for (PlayerListItem.Item item : packet.getItems()) {
                TabListItem tabListItem = serverTabList.get(item.getUuid());
                if (tabListItem != null) {
                    tabListItem.setGamemode(item.getGamemode());
                }
            }
            break;
        case UPDATE_LATENCY:
            for (PlayerListItem.Item item : packet.getItems()) {
                TabListItem tabListItem = serverTabList.get(item.getUuid());
                if (tabListItem != null) {
                    tabListItem.setPing(item.getPing());
                }
            }
            break;
        case UPDATE_DISPLAY_NAME:
            for (PlayerListItem.Item item : packet.getItems()) {
                TabListItem tabListItem = serverTabList.get(item.getUuid());
                if (tabListItem != null) {
                    tabListItem.setDisplayName(item.getDisplayName());
                }
            }
            break;
        case REMOVE_PLAYER:
            for (PlayerListItem.Item item : packet.getItems()) {
                serverTabList.remove(item.getUuid());
            }
            break;
        }

        // resize if necessary
        if (serverTabList.size() > size) {
            if (resizePolicy.isMod20()) {
                setSizeInternal(min(((serverTabList.size() + 19) / 20) * 20, 80));
            } else {
                setSizeInternal(min(serverTabList.size(), 80));
            }
        }

        // if passthrough is enabled send the packet to the client
        if (passtrough || size == 80) {
            sendPacket(packet);
            return PacketListenerResult.CANCEL;
        }

        // do magic
        switch (packet.getAction()) {
        case ADD_PLAYER:
            for (PlayerListItem.Item item : packet.getItems()) {
                if (item.getGamemode() == 3 && item.getUuid().equals(getUniqueId())) {

                    int slot = uuidToSlotMap.getInt(item.getUuid());
                    if (slot != -1) {
                        if (slot != size - 1) {
                            // player changed to gm 3
                            useFakePlayerForSlot(slot);
                            slot = size - 1;

                            if (clientUuid[slot] != fakePlayerUUIDs[slot]) {
                                // needs to be moved
                                int targetSlot = findSlotForPlayer(clientUuid[slot]);
                                useRealPlayerForSlot(targetSlot, clientUuid[slot]);
                            }

                            useRealPlayerForSlot(slot, item.getUuid());
                        } else {
                            // player in gm 3 updates username + skin
                            useRealPlayerForSlot(slot, item.getUuid());
                        }
                    } else {
                        // player joined with gm 3
                        slot = size - 1;

                        if (clientUuid[slot] != fakePlayerUUIDs[slot]) {
                            // needs to be moved
                            int targetSlot = findSlotForPlayer(clientUuid[slot]);
                            useRealPlayerForSlot(targetSlot, clientUuid[slot]);
                        }

                        useRealPlayerForSlot(slot, item.getUuid());
                    }
                } else {
                    item.setGamemode(0);

                    int slot;
                    if (-1 != (slot = uuidToSlotMap.getInt(item.getUuid()))
                            && !skinUuidToSlotMap.contains(item.getUuid(), -1)) {
                        // player that was not in correct position updates username + skin
                        // probably changed away from gm 3
                        // move the player slot if he changed await from gm 3
                        useFakePlayerForSlot(slot);
                    }

                    if (slot != -1) {
                        // player is already in the tab list, just update
                        // skin and user name
                        useRealPlayerForSlot(slot, item.getUuid());
                    } else {
                        // player isn't yet in the tab list
                        slot = findSlotForPlayer(item.getUuid());
                        useRealPlayerForSlot(slot, item.getUuid());
                    }
                }
            }
            break;
        case UPDATE_GAMEMODE:
            for (PlayerListItem.Item item : packet.getItems()) {
                if (!serverTabList.containsKey(item.getUuid())) {
                    continue;
                }
                if (item.getUuid().equals(getUniqueId())) {
                    int slot = uuidToSlotMap.getInt(item.getUuid());

                    if (item.getGamemode() == 3 && slot != size - 1) {
                        // player changed to gm 3
                        useFakePlayerForSlot(slot);
                        slot = size - 1;

                        if (clientUuid[slot] != fakePlayerUUIDs[slot]) {
                            // needs to be moved
                            int targetSlot = findSlotForPlayer(clientUuid[slot]);
                            useRealPlayerForSlot(targetSlot, clientUuid[slot]);
                        }

                        useRealPlayerForSlot(slot, item.getUuid());
                    } else if (item.getGamemode() != 3 && slot == size - 1) {
                        useFakePlayerForSlot(size - 1);
                        useRealPlayerForSlot(findSlotForPlayer(getUniqueId()), getUniqueId());
                    } else {
                        PlayerListItem packetOut = new PlayerListItem();
                        packetOut.setAction(UPDATE_GAMEMODE);
                        packetOut.setItems(new PlayerListItem.Item[] { item });
                        sendPacket(packetOut);
                    }
                }
            }
            break;
        case REMOVE_PLAYER:
            sendPacket(packet);
            for (PlayerListItem.Item item : packet.getItems()) {
                // player leaves server
                int slot = uuidToSlotMap.getInt(item.getUuid());
                if (-1 != slot) {
                    useFakePlayerForSlot(slot);
                }
            }
            break;
        case UPDATE_LATENCY:
        case UPDATE_DISPLAY_NAME:
            break;
        }

        if (size != requestedSize && serverTabList.size() < requestedSize) {
            setSizeInternal(requestedSize);
        }

        return PacketListenerResult.CANCEL;
    }

    private int findSlotForPlayer(UUID playerUUID) {
        int targetSlot = -1;
        for (IntIterator iterator = skinUuidToSlotMap.get(playerUUID).iterator(); iterator.hasNext();) {
            int i = iterator.nextInt();
            if (clientUuid[i] == fakePlayerUUIDs[i]) {
                targetSlot = i;
                break;
            }
        }
        if (targetSlot == -1) {
            for (int i = size - 1; i >= 0; i--) {
                if (clientUuid[i] == fakePlayerUUIDs[i]) {
                    targetSlot = i;
                    break;
                }
            }
        }
        if (targetSlot == -1) {
            throw new IllegalStateException("Not enough slots in tab list.");
        }
        return targetSlot;
    }

    private void useFakePlayerForSlot(int slot) {
        boolean change = clientUuid[slot] != fakePlayerUUIDs[slot];

        if (change) {
            removePlayerFromTeam(slot, clientUuid[slot], clientUsername[slot]);
            uuidToSlotMap.remove(clientUuid[slot]);
        }

        PlayerListItem packet = new PlayerListItem();
        packet.setAction(ADD_PLAYER);
        PlayerListItem.Item item = new PlayerListItem.Item();
        item.setUuid(fakePlayerUUIDs[slot]);
        item.setUsername(fakePlayerUsernames[slot]);
        item.setPing(clientPing[slot]);
        item.setDisplayName(clientText[slot]);
        item.setGamemode(0);
        item.setProperties(clientSkin[slot].getProperties());
        packet.setItems(new PlayerListItem.Item[] { item });
        sendPacket(packet);
        packet = new PlayerListItem();
        packet.setAction(UPDATE_DISPLAY_NAME);
        packet.setItems(new PlayerListItem.Item[] { item });
        sendPacket(packet);
        clientUsername[slot] = fakePlayerUsernames[slot];
        clientUuid[slot] = fakePlayerUUIDs[slot];
        uuidToSlotMap.put(clientUuid[slot], slot);

        if (change) {
            addPlayerToTeam(slot, clientUuid[slot], clientUsername[slot]);
        }
    }

    private void useRealPlayerForSlot(int slot, UUID uuid) {
        TabListItem tabListItem = serverTabList.get(uuid);

        boolean change = !clientUuid[slot].equals(uuid) || !tabListItem.getUsername().equals(clientUsername[slot]);

        if (change) {
            removePlayerFromTeam(slot, clientUuid[slot], clientUsername[slot]);
            if (uuidToSlotMap.getInt(clientUuid[slot]) == slot) {
                uuidToSlotMap.remove(clientUuid[slot]);
            }

            // if there was a fake player on that slot previously remove it from
            // the tab list
            if (clientUuid[slot] == fakePlayerUUIDs[slot]) {
                PlayerListItem packet = new PlayerListItem();
                packet.setAction(REMOVE_PLAYER);
                packet.setItems(new PlayerListItem.Item[] { item(clientUuid[slot]) });
                sendPacket(packet);
            }
        }

        PlayerListItem packet = new PlayerListItem();
        packet.setAction(ADD_PLAYER);
        PlayerListItem.Item item = new PlayerListItem.Item();
        item.setUuid(tabListItem.getUuid());
        item.setUsername(tabListItem.getUsername());
        item.setPing(clientPing[slot]);
        item.setDisplayName(clientText[slot]);
        item.setGamemode(uuid.equals(getUniqueId()) ? tabListItem.getGamemode() : 0);
        item.setProperties(tabListItem.getProperties());
        packet.setItems(new PlayerListItem.Item[] { item });
        sendPacket(packet);
        packet = new PlayerListItem();
        packet.setAction(UPDATE_DISPLAY_NAME);
        packet.setItems(new PlayerListItem.Item[] { item });
        sendPacket(packet);

        if (change) {
            clientUsername[slot] = tabListItem.getUsername();
            clientUuid[slot] = tabListItem.getUuid();
            uuidToSlotMap.put(clientUuid[slot], slot);
            addPlayerToTeam(slot, clientUuid[slot], clientUsername[slot]);
        }
    }

    private void addPlayerToTeam(int slot, UUID uuid, String player) {
        // dirty hack for citizens compatibility
        if (uuid.version() == 2)
            return;
        sendPacket(addPlayer(slot, player));
        nameToSlotMap.put(player, slot);
        if (playerToTeamMap.containsKey(player)) {
            TeamData serverTeam = serverTeams.get(playerToTeamMap.get(player));
            Team team = new Team();
            team.setMode((byte) 2);
            team.setName(fakePlayerUsernames[slot]);
            team.setDisplayName(serverTeam.getDisplayName());
            team.setPrefix(serverTeam.getPrefix());
            team.setSuffix(serverTeam.getSuffix());
            team.setFriendlyFire(serverTeam.getFriendlyFire());
            team.setNameTagVisibility(serverTeam.getNameTagVisibility());
            if (teamCollisionRuleSupported) {
                team.setCollisionRule(serverTeam.getCollisionRule());
            }
            team.setColor(serverTeam.getColor());
            sendPacket(team);
        }
    }

    private void removePlayerFromTeam(int slot, UUID uuid, String player) {
        // dirty hack for citizens compatibility
        if (uuid.version() == 2)
            return;
        if (nameToSlotMap.getInt(player) == slot) {
            nameToSlotMap.remove(player);
            sendPacket(removePlayer(slot, player));
            if (playerToTeamMap.containsKey(player)) {
                Team team = new Team();
                team.setName(playerToTeamMap.get(player));
                team.setMode((byte) 3); // add player
                team.setPlayers(new String[] { player });
                sendPacket(team);
                team = new Team();
                team.setMode((byte) 2);
                team.setName(fakePlayerUsernames[slot]);
                team.setDisplayName(fakePlayerUsernames[slot]);
                team.setPrefix("");
                team.setSuffix("");
                team.setFriendlyFire((byte) 1);
                team.setNameTagVisibility("always");
                if (teamCollisionRuleSupported) {
                    team.setCollisionRule("always");
                }
                team.setColor((byte) 0);
                sendPacket(team);
            }
        }
    }

    @Override
    public PacketListenerResult onTeamPacket(Team packet) {
        if (fakePlayerUsernameSet.contains(packet.getName())) {
            throw new AssertionError("Team name collision: " + packet);
        }

        // update server data
        List<String> invalid = null;

        if (packet.getMode() == 1) {
            TeamData team = serverTeams.remove(packet.getName());
            if (team != null) {
                for (String player : team.getPlayers()) {
                    playerToTeamMap.remove(player, packet.getName());
                    int slot;
                    if (!passtrough && size != 80 && -1 != (slot = nameToSlotMap.getInt(player))) {
                        Team packet1 = new Team();
                        packet1.setMode((byte) 2);
                        packet1.setName(fakePlayerUsernames[slot]);
                        packet1.setDisplayName(packet1.getName());
                        packet1.setPrefix("");
                        packet1.setSuffix("");
                        packet1.setFriendlyFire((byte) 1);
                        packet1.setNameTagVisibility("always");
                        if (teamCollisionRuleSupported) {
                            packet1.setCollisionRule("always");
                        }
                        packet1.setColor((byte) 0);
                        sendPacket(packet1);
                    }
                }
            }

        } else {

            // Create or get old team
            TeamData t;
            if (packet.getMode() == 0) {
                t = new TeamData();
                serverTeams.put(packet.getName(), t);
            } else {
                t = serverTeams.get(packet.getName());
            }

            if (t != null) {
                if (packet.getMode() == 0 || packet.getMode() == 2) {
                    t.setDisplayName(packet.getDisplayName());
                    t.setPrefix(packet.getPrefix());
                    t.setSuffix(packet.getSuffix());
                    t.setFriendlyFire(packet.getFriendlyFire());
                    t.setNameTagVisibility(packet.getNameTagVisibility());
                    if (teamCollisionRuleSupported) {
                        t.setCollisionRule(packet.getCollisionRule());
                    }
                    t.setColor(packet.getColor());
                }
                if (packet.getPlayers() != null) {
                    for (String s : packet.getPlayers()) {
                        if (packet.getMode() == 0 || packet.getMode() == 3) {
                            if (playerToTeamMap.containsKey(s)) {
                                serverTeams.get(playerToTeamMap.get(s)).removePlayer(s);
                            }
                            t.addPlayer(s);
                            playerToTeamMap.put(s, packet.getName());
                        } else {
                            t.removePlayer(s);
                            if (!playerToTeamMap.remove(s, packet.getName())) {
                                if (invalid == null) {
                                    invalid = new ArrayList<>();
                                }
                                invalid.add(s);
                            }
                        }
                    }
                }
            }
        }

        if (passtrough || size == 80) {
            return PacketListenerResult.PASS;
        }

        if (packet.getMode() == 2) {
            TeamData serverTeam = serverTeams.get(packet.getName());
            if (serverTeam != null) {
                for (String player : serverTeam.getPlayers()) {
                    int slot;
                    if (-1 != (slot = nameToSlotMap.getInt(player))) {
                        Team team = new Team();
                        team.setMode((byte) 2);
                        team.setName(fakePlayerUsernames[slot]);
                        team.setDisplayName(packet.getDisplayName());
                        team.setPrefix(packet.getPrefix());
                        team.setSuffix(packet.getSuffix());
                        team.setFriendlyFire(packet.getFriendlyFire());
                        team.setNameTagVisibility(packet.getNameTagVisibility());
                        if (teamCollisionRuleSupported) {
                            team.setCollisionRule(packet.getCollisionRule());
                        }
                        team.setColor(packet.getColor());
                        sendPacket(team);
                    }
                }
            }
        }

        boolean modified = false;

        if (packet.getMode() == 0 || packet.getMode() == 3 || packet.getMode() == 4) {
            int length = 0;
            for (String player : packet.getPlayers()) {
                int slot;
                if (-1 == (slot = nameToSlotMap.getInt(player))) {
                    length++;
                } else {
                    if (packet.getMode() == 4) {
                        if (invalid == null || !invalid.contains(player)) {
                            Team team = new Team();
                            team.setMode((byte) 2);
                            team.setName(fakePlayerUsernames[slot]);
                            team.setDisplayName(team.getName());
                            team.setPrefix("");
                            team.setSuffix("");
                            team.setFriendlyFire((byte) 1);
                            team.setNameTagVisibility("always");
                            if (teamCollisionRuleSupported) {
                                team.setCollisionRule("always");
                            }
                            team.setColor((byte) 0);
                            sendPacket(team);
                        }
                    } else {
                        TeamData serverTeam = serverTeams.get(playerToTeamMap.get(player));
                        if (serverTeam != null) {
                            Team team = new Team();
                            team.setMode((byte) 2);
                            team.setName(fakePlayerUsernames[slot]);
                            team.setDisplayName(serverTeam.getDisplayName());
                            team.setPrefix(serverTeam.getPrefix());
                            team.setSuffix(serverTeam.getSuffix());
                            team.setFriendlyFire(serverTeam.getFriendlyFire());
                            team.setNameTagVisibility(serverTeam.getNameTagVisibility());
                            if (teamCollisionRuleSupported) {
                                team.setCollisionRule(serverTeam.getCollisionRule());
                            }
                            team.setColor(serverTeam.getColor());
                            sendPacket(team);
                        }
                    }
                }
            }
            if (length != packet.getPlayers().length) {
                String[] players = new String[length];
                length = 0;
                for (String player : packet.getPlayers()) {
                    if (!nameToSlotMap.containsKey(player)) {
                        players[length++] = player;
                    }
                }
                packet.setPlayers(players);
                modified = true;
            }
        }

        return modified ? PacketListenerResult.MODIFIED : PacketListenerResult.PASS;
    }

    @Override
    public PacketListenerResult onPlayerListHeaderFooterPacket(PlayerListHeaderFooter packet) {
        serverHeader = packet.getHeader();
        serverFooter = packet.getFooter();

        return passtrough || clientHeader == null || clientFooter == null ? PacketListenerResult.PASS
                : PacketListenerResult.CANCEL;
    }

    @Override
    public void onServerSwitch() {
        PlayerListItem packet = new PlayerListItem();
        packet.setAction(REMOVE_PLAYER);

        PlayerListItem.Item[] items = new PlayerListItem.Item[serverTabList.size()];
        Iterator<UUID> iterator = serverTabList.keySet().iterator();
        for (int i = 0; i < items.length; i++) {
            items[i] = item(iterator.next());
        }

        packet.setItems(items);
        if (onPlayerListPacketInternal(packet) != PacketListenerResult.CANCEL) {
            sendPacket(packet);
        }

        serverTeams.clear();
        playerToTeamMap.clear();

        serverTabList.clear();
        serverHeader = null;
        serverFooter = null;
    }

    @Override
    public void setPassThrough(boolean passTrough) {
        if (this.passtrough != passTrough) {
            this.passtrough = passTrough;
            if (passTrough) {
                // remove fake players
                List<PlayerListItem.Item> items = new ArrayList<>();
                for (int i = 0; i < size; i++) {
                    if (clientUuid[i] == fakePlayerUUIDs[i]) {
                        items.add(item(clientUuid[i]));
                    }
                }
                PlayerListItem packet = new PlayerListItem();
                packet.setAction(REMOVE_PLAYER);
                packet.setItems(items.toArray(new PlayerListItem.Item[items.size()]));
                sendPacket(packet);

                if (size < 80) {
                    // remove players from teams
                    for (int i = 0; i < size; i++) {
                        removePlayerFromTeam(i, clientUuid[i], clientUsername[i]);
                    }
                }

                // restore server tab header/ footer
                if (serverHeader != null && serverFooter != null) {
                    sendPacket(new PlayerListHeaderFooter(serverHeader, serverFooter));
                }

                // restore players
                items.clear();
                for (TabListItem tabListItem : serverTabList.values()) {
                    if (tabListItem.getDisplayName() != null) {
                        continue;
                    }
                    PlayerListItem.Item item = new PlayerListItem.Item();
                    item.setUuid(tabListItem.getUuid());
                    item.setUsername(tabListItem.getUsername());
                    item.setPing(tabListItem.getPing());
                    item.setProperties(tabListItem.getProperties());
                    item.setGamemode(tabListItem.getGamemode());
                    items.add(item);
                }
                if (!items.isEmpty()) {
                    packet = new PlayerListItem();
                    packet.setAction(ADD_PLAYER);
                    packet.setItems(items.toArray(new PlayerListItem.Item[items.size()]));
                    sendPacket(packet);
                }

                // restore player ping
                items.clear();
                for (TabListItem tabListItem : serverTabList.values()) {
                    if (tabListItem.getDisplayName() == null) {
                        continue;
                    }
                    PlayerListItem.Item item = new PlayerListItem.Item();
                    item.setUuid(tabListItem.getUuid());
                    item.setPing(tabListItem.getPing());
                    items.add(item);
                }
                if (!items.isEmpty()) {
                    packet = new PlayerListItem();
                    packet.setAction(UPDATE_LATENCY);
                    packet.setItems(items.toArray(new PlayerListItem.Item[items.size()]));
                    sendPacket(packet);
                }

                // restore player gamemode
                items.clear();
                for (TabListItem tabListItem : serverTabList.values()) {
                    if (tabListItem.getDisplayName() == null) {
                        continue;
                    }
                    PlayerListItem.Item item = new PlayerListItem.Item();
                    item.setUuid(tabListItem.getUuid());
                    item.setGamemode(tabListItem.getGamemode());
                    items.add(item);
                }
                if (!items.isEmpty()) {
                    packet = new PlayerListItem();
                    packet.setAction(UPDATE_GAMEMODE);
                    packet.setItems(items.toArray(new PlayerListItem.Item[items.size()]));
                    sendPacket(packet);
                }

                // restore player display name
                items.clear();
                for (TabListItem tabListItem : serverTabList.values()) {
                    if (tabListItem.getDisplayName() == null) {
                        continue;
                    }
                    if (tabListItem.getDisplayName() != null) {
                        PlayerListItem.Item item = new PlayerListItem.Item();
                        item.setUuid(tabListItem.getUuid());
                        item.setDisplayName(tabListItem.getDisplayName());
                        items.add(item);
                    }
                }
                if (!items.isEmpty()) {
                    packet = new PlayerListItem();
                    packet.setAction(UPDATE_DISPLAY_NAME);
                    packet.setItems(items.toArray(new PlayerListItem.Item[items.size()]));
                    sendPacket(packet);
                }
            } else {

                // resize if necessary
                if (serverTabList.size() > size) {
                    setSizeInternal(min(((serverTabList.size() + 19) / 20) * 20, 80));
                }

                if (size == 80) {
                    PlayerListItem.Item[] items = new PlayerListItem.Item[size];
                    for (int slot = 0; slot < size; slot++) {
                        clientUuid[slot] = fakePlayerUUIDs[slot];
                        clientUsername[slot] = fakePlayerUsernames[slot];
                        uuidToSlotMap.put(clientUuid[slot], slot);
                        PlayerListItem.Item item = new PlayerListItem.Item();
                        item.setUuid(clientUuid[slot]);
                        item.setUsername(clientUsername[slot]);
                        item.setPing(clientPing[slot]);
                        item.setDisplayName(clientText[slot]);
                        item.setProperties(clientSkin[slot].getProperties());
                        items[slot] = item;
                    }
                    PlayerListItem packet = new PlayerListItem();
                    packet.setAction(ADD_PLAYER);
                    packet.setItems(items);
                    sendPacket(packet);
                    packet = new PlayerListItem();
                    packet.setAction(UPDATE_DISPLAY_NAME);
                    packet.setItems(items);
                    sendPacket(packet);
                } else {
                    uuidToSlotMap.clear();
                    rebuildTabList();
                }

                if (clientHeader != null && clientFooter != null) {
                    sendPacket(new PlayerListHeaderFooter(clientHeader, clientFooter));
                }
            }
        }
    }

    @Override
    public void setSize(int size) {

        // resize if necessary
        if (serverTabList.size() > size) {
            if (resizePolicy.isMod20()) {
                setSizeInternal(min(((serverTabList.size() + 19) / 20) * 20, 80));
            } else {
                setSizeInternal(min(serverTabList.size(), 80));
            }
        } else {
            setSizeInternal(size);
        }
        requestedSize = size;
    }

    public int getSize() {
        return requestedSize;
    }

    private void setSizeInternal(int size) {
        if (size > 80 || size < 0) {
            throw new IllegalArgumentException();
        }

        if (size < this.size) {
            for (int index = size; index < this.size; index++) {
                if (clientSkin[index].getPlayer() != null) {
                    skinUuidToSlotMap.remove(clientSkin[index].getPlayer(), index);
                }
            }
        }

        if (passtrough) {
            if (size > this.size) {
                for (int slot = this.size; slot < size; slot++) {
                    clientSkin[slot] = Icon.DEFAULT;
                    clientText[slot] = "{\"text\": \"\"}";
                    clientPing[slot] = 0;
                }
            }
        } else {
            if (size > this.size) {
                PlayerListItem.Item[] items = new PlayerListItem.Item[size - this.size];
                for (int slot = this.size; slot < size; slot++) {
                    clientUuid[slot] = fakePlayerUUIDs[slot];
                    clientUsername[slot] = fakePlayerUsernames[slot];
                    clientSkin[slot] = Icon.DEFAULT;
                    clientText[slot] = "{\"text\": \"\"}";
                    clientPing[slot] = 0;
                    uuidToSlotMap.put(clientUuid[slot], slot);
                    PlayerListItem.Item item = new PlayerListItem.Item();
                    item.setUuid(clientUuid[slot]);
                    item.setUsername(clientUsername[slot]);
                    item.setPing(0);
                    item.setDisplayName("{\"text\": \"\"}");
                    item.setProperties(EMPTY_PROPRTIES);
                    items[slot - this.size] = item;
                }
                PlayerListItem packet = new PlayerListItem();
                packet.setAction(ADD_PLAYER);
                packet.setItems(items);
                sendPacket(packet);
                packet = new PlayerListItem();
                packet.setAction(UPDATE_DISPLAY_NAME);
                packet.setItems(items);
                sendPacket(packet);

                if (size == 80) {
                    int realPlayers = 0;

                    for (int slot = 0; slot < this.size; slot++) {
                        if (clientUuid[slot] != fakePlayerUUIDs[slot]) {
                            realPlayers++;
                        }
                    }

                    items = new PlayerListItem.Item[realPlayers];
                    realPlayers = 0;
                    for (int slot = 0; slot < this.size; slot++) {
                        if (clientUuid[slot] != fakePlayerUUIDs[slot]) {
                            PlayerListItem.Item item = new PlayerListItem.Item();
                            TabListItem tabListItem = serverTabList.get(clientUuid[slot]);
                            item.setUuid(tabListItem.getUuid());
                            item.setGamemode(tabListItem.getGamemode());
                            items[realPlayers++] = item;
                            useFakePlayerForSlot(slot);
                        }
                        removePlayerFromTeam(slot, clientUuid[slot], clientUsername[slot]);
                    }

                    if (items.length != 0) {
                        packet = new PlayerListItem();
                        packet.setAction(UPDATE_GAMEMODE);
                        packet.setItems(items);
                        sendPacket(packet);
                    }
                } else {
                    for (int slot = this.size; slot < size; slot++) {
                        addPlayerToTeam(slot, clientUuid[slot], clientUsername[slot]);
                    }
                    if (serverTabList.containsKey(getUniqueId())
                            && serverTabList.get(getUniqueId()).getGamemode() == 3) {
                        if (this.size > 0 && clientUuid[this.size - 1].equals(getUniqueId())) {
                            useFakePlayerForSlot(this.size - 1);
                            useRealPlayerForSlot(size - 1, getUniqueId());
                        }
                    }
                }
            } else if (size < this.size) {
                for (int slot = 0; slot < this.size; slot++) {
                    if (clientUuid[slot] == fakePlayerUUIDs[slot]) {
                        PlayerListItem packet = new PlayerListItem();
                        packet.setAction(REMOVE_PLAYER);
                        packet.setItems(new PlayerListItem.Item[] { item(clientUuid[slot]) });
                        sendPacket(packet);
                    }
                }
                if (this.size != 80) {
                    for (int slot = 0; slot < this.size; slot++) {
                        removePlayerFromTeam(slot, clientUuid[slot], clientUsername[slot]);
                    }
                }
                this.size = size;
                uuidToSlotMap.clear();
                rebuildTabList();
            }
        }
        this.size = size;
    }

    private void rebuildTabList() {
        Preconditions.checkArgument(size < 80 && size >= 0, "Wrong size: " + size);
        Set<UUID> realPlayers = new HashSet<>(serverTabList.keySet());
        boolean isSpectator = serverTabList.containsKey(getUniqueId())
                && serverTabList.get(getUniqueId()).getGamemode() == 3;
        if (isSpectator) {
            realPlayers.remove(getUniqueId());
        }

        PlayerListItem.Item[] items = new PlayerListItem.Item[size];
        for (int i = 0; i < size; i++) {
            PlayerListItem.Item item = new PlayerListItem.Item();

            UUID skinOwner = clientSkin[i].getPlayer();
            if (skinOwner != null && realPlayers.contains(skinOwner)) {
                // use real player
                TabListItem tabListItem = serverTabList.get(skinOwner);
                item.setUuid(tabListItem.getUuid());
                item.setUsername(tabListItem.getUsername());
                item.setProperties(tabListItem.getProperties());
                realPlayers.remove(item.getUuid());
            } else if (size - i - (isSpectator ? 1 : 0) > realPlayers.size()) {
                item.setUuid(fakePlayerUUIDs[i]);
                item.setUsername(fakePlayerUsernames[i]);
                item.setProperties(clientSkin[i].getProperties());
            } else if (!realPlayers.isEmpty()) {
                UUID uuid = realPlayers.iterator().next();
                realPlayers.remove(uuid);
                TabListItem tabListItem = serverTabList.get(uuid);
                item.setUuid(tabListItem.getUuid());
                item.setUsername(tabListItem.getUsername());
                item.setProperties(tabListItem.getProperties());
            } else {
                TabListItem tabListItem = serverTabList.get(getUniqueId());
                item.setUuid(tabListItem.getUuid());
                item.setUsername(tabListItem.getUsername());
                item.setProperties(tabListItem.getProperties());
                item.setGamemode(tabListItem.getGamemode());
            }
            item.setPing(clientPing[i]);
            item.setDisplayName(clientText[i]);

            clientUuid[i] = item.getUuid();
            clientUsername[i] = item.getUsername();
            uuidToSlotMap.put(item.getUuid(), i);

            addPlayerToTeam(i, item.getUuid(), item.getUsername());

            items[i] = item;
        }
        PlayerListItem packet = new PlayerListItem();
        packet.setAction(ADD_PLAYER);
        packet.setItems(items);
        sendPacket(packet);
        packet = new PlayerListItem();
        packet.setAction(UPDATE_DISPLAY_NAME);
        packet.setItems(items);
        sendPacket(packet);
    }

    @Override
    public void setSlot(int index, Icon skin, String text, int ping) {
        Preconditions.checkElementIndex(index, size);

        if (!clientSkin[index].equals(skin)) {
            if (clientSkin[index].getPlayer() != null) {
                skinUuidToSlotMap.remove(clientSkin[index].getPlayer(), index);
            }
            if (skin.getPlayer() != null) {
                skinUuidToSlotMap.put(skin.getPlayer(), index);
            }
        }

        if (!passtrough) {
            if (clientSkin[index].equals(skin)) {
                updatePingInternal(index, ping);
            } else {
                boolean updated = false;
                if (size < 80) {
                    int slot;
                    if (!clientUuid[index].equals(skin.getPlayer())) {
                        boolean moveOld = false;
                        boolean moveNew = false;
                        if ((!clientUuid[index].equals(getUniqueId())
                                || serverTabList.get(getUniqueId()).getGamemode() != 3)) {
                            moveOld = clientUuid[index] != fakePlayerUUIDs[index];
                            moveNew = skin.getPlayer() != null && serverTabList.containsKey(skin.getPlayer())
                                    && (!skin.getPlayer().equals(getUniqueId())
                                            || serverTabList.get(getUniqueId()).getGamemode() != 3)
                                    && (clientSkin[(slot = uuidToSlotMap.getInt(skin.getPlayer()))]
                                            .getPlayer() == null
                                            || !clientSkin[slot].getPlayer().equals(skin.getPlayer()));
                        }

                        UUID oldUuid = clientUuid[index];

                        if (moveOld && !moveNew && serverTabList.size() < size) {
                            clientSkin[index] = skin;
                            clientPing[index] = ping;
                            useFakePlayerForSlot(index);
                            if (skinUuidToSlotMap.containsKey(oldUuid)) {
                                for (IntIterator iterator = skinUuidToSlotMap.get(oldUuid).iterator(); iterator
                                        .hasNext();) {
                                    int i = iterator.nextInt();
                                    if (clientUuid[i] != fakePlayerUUIDs[i] && (!getUniqueId().equals(clientUuid[i])
                                            || serverTabList.get(getUniqueId()).getGamemode() != 3)) {
                                        int target = findSlotForPlayer(clientUuid[i]);
                                        useRealPlayerForSlot(target, clientUuid[i]);
                                        useRealPlayerForSlot(i, oldUuid);
                                        updated = true;
                                        break;
                                    }
                                }
                            }
                            if (!updated) {
                                int target = findSlotForPlayer(oldUuid);
                                useRealPlayerForSlot(target, oldUuid);
                            }
                            updated = true;
                        } else if (moveNew && !moveOld && serverTabList.size() < size) {
                            clientSkin[index] = skin;
                            clientPing[index] = ping;
                            slot = uuidToSlotMap.getInt(skin.getPlayer());
                            useFakePlayerForSlot(slot);
                            useRealPlayerForSlot(index, skin.getPlayer());
                            updated = true;
                        } else if (moveNew && moveOld) {
                            clientSkin[index] = skin;
                            clientPing[index] = ping;
                            slot = uuidToSlotMap.getInt(skin.getPlayer());
                            useFakePlayerForSlot(slot);
                            if (skinUuidToSlotMap.containsKey(oldUuid)) {
                                for (IntIterator iterator = skinUuidToSlotMap.get(oldUuid).iterator(); iterator
                                        .hasNext();) {
                                    int i = iterator.nextInt();
                                    if (clientUuid[i] != fakePlayerUUIDs[i] && (!getUniqueId().equals(clientUuid[i])
                                            || serverTabList.get(getUniqueId()).getGamemode() != 3)) {
                                        int target = findSlotForPlayer(clientUuid[i]);
                                        useRealPlayerForSlot(target, clientUuid[i]);
                                        useRealPlayerForSlot(i, oldUuid);
                                        updated = true;
                                        break;
                                    }
                                }
                            }
                            if (!updated) {
                                int target = findSlotForPlayer(oldUuid);
                                useRealPlayerForSlot(target, oldUuid);
                            }
                            useRealPlayerForSlot(index, skin.getPlayer());
                            updated = true;
                        }
                    }
                }
                if (!updated) {
                    if (clientUuid[index] == fakePlayerUUIDs[index]) {
                        PlayerListItem packet = new PlayerListItem();
                        PlayerListItem.Item item = new PlayerListItem.Item();
                        item.setUuid(clientUuid[index]);
                        item.setUsername(clientUsername[index]);
                        item.setPing(ping);
                        item.setDisplayName(text);
                        item.setProperties(skin.getProperties());
                        packet.setAction(ADD_PLAYER);
                        packet.setItems(new PlayerListItem.Item[] { item });
                        sendPacket(packet);
                        clientText[index] = "";
                    } else {
                        updatePingInternal(index, ping);
                    }
                }
            }
            updateTextInternal(index, text);
        }
        clientSkin[index] = skin;
        clientText[index] = text;
        clientPing[index] = ping;
    }

    @Override
    public void updateText(int index, String text) {
        updateTextInternal(index, text);
    }

    private void updateTextInternal(int index, String text) {
        Preconditions.checkElementIndex(index, size);

        if (!passtrough && !clientText[index].equals(text)) {
            PlayerListItem packet = new PlayerListItem();
            PlayerListItem.Item item = new PlayerListItem.Item();
            item.setUuid(clientUuid[index]);
            item.setDisplayName(text);
            packet.setAction(UPDATE_DISPLAY_NAME);
            packet.setItems(new PlayerListItem.Item[] { item });
            sendPacket(packet);
        }
        clientText[index] = text;
    }

    @Override
    public void updatePing(int index, int ping) {
        updatePingInternal(index, ping);
    }

    private void updatePingInternal(int index, int ping) {
        Preconditions.checkElementIndex(index, size);

        if (!passtrough && clientPing[index] != ping) {
            PlayerListItem packet = new PlayerListItem();
            PlayerListItem.Item item = new PlayerListItem.Item();
            item.setUuid(clientUuid[index]);
            item.setPing(ping);
            packet.setAction(UPDATE_LATENCY);
            packet.setItems(new PlayerListItem.Item[] { item });
            sendPacket(packet);
        }
        clientPing[index] = ping;
    }

    @Override
    public void setHeaderFooter(String header, String footer) {
        if (!Objects.equals(header, clientHeader) || !Objects.equals(footer, clientFooter)) {
            if (header != null && footer != null) {
                sendPacket(new PlayerListHeaderFooter(header, footer));
            }
            clientHeader = header;
            clientFooter = footer;
        }
    }

    private static PlayerListItem.Item item(UUID uuid) {
        PlayerListItem.Item item1 = new PlayerListItem.Item();
        item1.setUuid(uuid);
        return item1;
    }

    private static Team removePlayer(int slot, String player) {
        Team packet = new Team(fakePlayerUsernames[slot]);
        packet.setMode((byte) 4);
        packet.setPlayers(new String[] { player });
        return packet;
    }

    private static Team addPlayer(int slot, String player) {
        Team packet = new Team(fakePlayerUsernames[slot]);
        packet.setMode((byte) 3);
        packet.setPlayers(new String[] { player });
        return packet;
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    static class TabListItem {
        private UUID uuid;
        private String[][] properties;
        private String username;
        private String displayName;
        private int ping;
        private int gamemode;

        private TabListItem(PlayerListItem.Item item) {
            this(item.getUuid(), item.getProperties(), item.getUsername(), item.getDisplayName(), item.getPing(),
                    item.getGamemode());
        }
    }

    @Data
    static class TeamData {
        private String displayName;
        private String prefix;
        private String suffix;
        private byte friendlyFire;
        private String nameTagVisibility;
        private String collisionRule;
        private byte color;
        private Set<String> players = new ObjectOpenHashSet<>();

        public void addPlayer(String name) {
            players.add(name);
        }

        public void removePlayer(String name) {
            players.remove(name);
        }

        public void setNameTagVisibility(String nameTagVisibility) {
            this.nameTagVisibility = nameTagVisibility.intern();
        }

        public void setCollisionRule(String collisionRule) {
            this.collisionRule = collisionRule == null ? null : collisionRule.intern();
        }
    }
}