From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Minecrell <minecrell@minecrell.net>
Date: Wed, 11 Oct 2017 15:55:38 +0200
Subject: [PATCH] Add extended PaperServerListPingEvent

Add a new event that extends the original ServerListPingEvent
and allows full control of the response sent to the client.

diff --git a/src/main/java/com/destroystokyo/paper/event/server/PaperServerListPingEvent.java b/src/main/java/com/destroystokyo/paper/event/server/PaperServerListPingEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..baac2e4f090a490372ef4aed92c8a5771955e921
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/event/server/PaperServerListPingEvent.java
@@ -0,0 +1,334 @@
+package com.destroystokyo.paper.event.server;
+
+import static java.util.Objects.requireNonNull;
+
+import com.destroystokyo.paper.network.StatusClient;
+import com.destroystokyo.paper.profile.PlayerProfile;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.server.ServerListPingEvent;
+import org.bukkit.util.CachedServerIcon;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.UUID;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Extended version of {@link ServerListPingEvent} that allows full control
+ * of the response sent to the client.
+ */
+public class PaperServerListPingEvent extends ServerListPingEvent implements Cancellable {
+
+    @NotNull private final StatusClient client;
+
+    private int numPlayers;
+    private boolean hidePlayers;
+    @NotNull private final List<PlayerProfile> playerSample = new ArrayList<>();
+
+    @NotNull private String version;
+    private int protocolVersion;
+
+    @Nullable private CachedServerIcon favicon;
+
+    private boolean cancelled;
+
+    private boolean originalPlayerCount = true;
+    private Object[] players;
+
+    @Deprecated
+    public PaperServerListPingEvent(@NotNull StatusClient client, @NotNull String motd, int numPlayers, int maxPlayers,
+            @NotNull String version, int protocolVersion, @Nullable CachedServerIcon favicon) {
+        super(client.getAddress().getAddress(), motd, numPlayers, maxPlayers);
+        this.client = client;
+        this.numPlayers = numPlayers;
+        this.version = version;
+        this.protocolVersion = protocolVersion;
+        setServerIcon(favicon);
+    }
+
+    public PaperServerListPingEvent(@NotNull StatusClient client, @NotNull net.kyori.adventure.text.Component motd, int numPlayers, int maxPlayers,
+                                    @NotNull String version, int protocolVersion, @Nullable CachedServerIcon favicon) {
+        super(client.getAddress().getAddress(), motd, numPlayers, maxPlayers);
+        this.client = client;
+        this.numPlayers = numPlayers;
+        this.version = version;
+        this.protocolVersion = protocolVersion;
+        setServerIcon(favicon);
+    }
+
+    /**
+     * Returns the {@link StatusClient} pinging the server.
+     *
+     * @return The client
+     */
+    @NotNull
+    public StatusClient getClient() {
+        return this.client;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Returns {@code -1} if players are hidden using
+     * {@link #shouldHidePlayers()}.</p>
+     */
+    @Override
+    public int getNumPlayers() {
+        if (this.hidePlayers) {
+            return -1;
+        }
+
+        return this.numPlayers;
+    }
+
+    /**
+     * Sets the number of players displayed in the server list.
+     *
+     * <p>Note that this won't have any effect if {@link #shouldHidePlayers()}
+     * is enabled.</p>
+     *
+     * @param numPlayers The number of online players
+     */
+    public void setNumPlayers(int numPlayers) {
+        if (this.numPlayers != numPlayers) {
+            this.numPlayers = numPlayers;
+            this.originalPlayerCount = false;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Returns {@code -1} if players are hidden using
+     * {@link #shouldHidePlayers()}.</p>
+     */
+    @Override
+    public int getMaxPlayers() {
+        if (this.hidePlayers) {
+            return -1;
+        }
+
+        return super.getMaxPlayers();
+    }
+
+    /**
+     * Returns whether all player related information is hidden in the server
+     * list. This will cause {@link #getNumPlayers()}, {@link #getMaxPlayers()}
+     * and {@link #getPlayerSample()} to be skipped in the response.
+     *
+     * <p>The Vanilla Minecraft client will display the player count as {@code ???}
+     * when this option is enabled.</p>
+     *
+     * @return {@code true} if the player count is hidden
+     */
+    public boolean shouldHidePlayers() {
+        return hidePlayers;
+    }
+
+    /**
+     * Sets whether all player related information is hidden in the server
+     * list. This will cause {@link #getNumPlayers()}, {@link #getMaxPlayers()}
+     * and {@link #getPlayerSample()} to be skipped in the response.
+     *
+     * <p>The Vanilla Minecraft client will display the player count as {@code ???}
+     * when this option is enabled.</p>
+     *
+     * @param hidePlayers {@code true} if the player count should be hidden
+     */
+    public void setHidePlayers(boolean hidePlayers) {
+        this.hidePlayers = hidePlayers;
+    }
+
+    /**
+     * Returns a mutable list of {@link PlayerProfile} that will be displayed
+     * as online players on the client.
+     *
+     * <p>The Vanilla Minecraft client will display them when hovering the
+     * player count with the mouse.</p>
+     *
+     * @return The mutable player sample list
+     */
+    @NotNull
+    public List<PlayerProfile> getPlayerSample() {
+        return this.playerSample;
+    }
+
+    /**
+     * Returns the version that will be sent as server version on the client.
+     *
+     * @return The server version
+     */
+    @NotNull
+    public String getVersion() {
+        return version;
+    }
+
+    /**
+     * Sets the version that will be sent as server version to the client.
+     *
+     * @param version The server version
+     */
+    public void setVersion(@NotNull String version) {
+        this.version = requireNonNull(version, "version");
+    }
+
+    /**
+     * Returns the protocol version that will be sent as the protocol version
+     * of the server to the client.
+     *
+     * @return The protocol version of the server, or {@code -1} if the server
+     * has not finished initialization yet
+     */
+    public int getProtocolVersion() {
+        return protocolVersion;
+    }
+
+    /**
+     * Sets the protocol version that will be sent as the protocol version
+     * of the server to the client.
+     *
+     * @param protocolVersion The protocol version of the server
+     */
+    public void setProtocolVersion(int protocolVersion) {
+        this.protocolVersion = protocolVersion;
+    }
+
+    /**
+     * Gets the server icon sent to the client.
+     *
+     * @return The icon to send to the client, or {@code null} for none
+     */
+    @Nullable
+    public CachedServerIcon getServerIcon() {
+        return this.favicon;
+    }
+
+    /**
+     * Sets the server icon sent to the client.
+     *
+     * @param icon The icon to send to the client, or {@code null} for none
+     */
+    @Override
+    public void setServerIcon(@Nullable CachedServerIcon icon) {
+        if (icon != null && icon.isEmpty()) {
+            // Represent empty icons as null
+            icon = null;
+        }
+
+        this.favicon = icon;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Cancelling this event will cause the connection to be closed immediately,
+     * without sending a response to the client.</p>
+     */
+    @Override
+    public boolean isCancelled() {
+        return this.cancelled;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Cancelling this event will cause the connection to be closed immediately,
+     * without sending a response to the client.</p>
+     */
+    @Override
+    public void setCancelled(boolean cancel) {
+        this.cancelled = cancel;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p><b>Note:</b> For compatibility reasons, this method will return all
+     * online players, not just the ones referenced in {@link #getPlayerSample()}.
+     * Removing a player will:</p>
+     *
+     * <ul>
+     *     <li>Decrement the online player count (if and only if) the player
+     *     count wasn't changed by another plugin before.</li>
+     *     <li>Remove all entries from {@link #getPlayerSample()} that refer to
+     *     the removed player (based on their {@link UUID}).</li>
+     * </ul>
+     */
+    @NotNull
+    @Override
+    public Iterator<Player> iterator() {
+        if (this.players == null) {
+            this.players = getOnlinePlayers();
+        }
+
+        return new PlayerIterator();
+    }
+
+    @NotNull
+    protected Object[] getOnlinePlayers() {
+        return Bukkit.getOnlinePlayers().toArray();
+    }
+
+    @NotNull
+    protected Player getBukkitPlayer(@NotNull Object player) {
+        return (Player) player;
+    }
+
+    private final class PlayerIterator implements Iterator<Player> {
+
+        private int next;
+        private int current;
+        @Nullable private Player player;
+
+        @Override
+        public boolean hasNext() {
+            for (; this.next < players.length; this.next++) {
+                if (players[this.next] != null) {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        @NotNull
+        @Override
+        public Player next() {
+            if (!hasNext()) {
+                this.player = null;
+                throw new NoSuchElementException();
+            }
+
+            this.current = this.next++;
+            return this.player = getBukkitPlayer(players[this.current]);
+        }
+
+        @Override
+        public void remove() {
+            if (this.player == null) {
+                throw new IllegalStateException();
+            }
+
+            UUID uniqueId = this.player.getUniqueId();
+            this.player = null;
+
+            // Remove player from iterator
+            players[this.current] = null;
+
+            // Remove player from sample
+            getPlayerSample().removeIf(p -> uniqueId.equals(p.getId()));
+
+            // Decrement player count
+            if (originalPlayerCount) {
+                numPlayers--;
+            }
+        }
+    }
+
+}
diff --git a/src/main/java/com/destroystokyo/paper/network/StatusClient.java b/src/main/java/com/destroystokyo/paper/network/StatusClient.java
new file mode 100644
index 0000000000000000000000000000000000000000..517d15238ed117f38bbd39f570874014cecf7bb5
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/network/StatusClient.java
@@ -0,0 +1,13 @@
+package com.destroystokyo.paper.network;
+
+import com.destroystokyo.paper.event.server.PaperServerListPingEvent;
+
+/**
+ * Represents a client requesting the current status from the server (e.g. from
+ * the server list).
+ *
+ * @see PaperServerListPingEvent
+ */
+public interface StatusClient extends NetworkClient {
+
+}
diff --git a/src/main/java/org/bukkit/util/CachedServerIcon.java b/src/main/java/org/bukkit/util/CachedServerIcon.java
index 612958a331575d1da2715531ebdf6b1168f2e860..bb4f7702ced0baf0670a7a21d48ad528b7249361 100644
--- a/src/main/java/org/bukkit/util/CachedServerIcon.java
+++ b/src/main/java/org/bukkit/util/CachedServerIcon.java
@@ -18,4 +18,9 @@ public interface CachedServerIcon {
     @Nullable
     public String getData(); // Paper
 
+    // Paper start
+    default boolean isEmpty() {
+        return getData() == null;
+    }
+    // Paper end
 }
