From f95d94a0aae2bdf7ba855b6149ee8c24bf86ffe1 Mon Sep 17 00:00:00 2001 From: Minecrell 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 00000000..dd1deafd --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/event/server/PaperServerListPingEvent.java @@ -0,0 +1,319 @@ +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 javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Extended version of {@link ServerListPingEvent} that allows full control + * of the response sent to the client. + */ +public class PaperServerListPingEvent extends ServerListPingEvent implements Cancellable { + + @Nonnull private final StatusClient client; + + private int numPlayers; + private boolean hidePlayers; + @Nonnull private final List playerSample = new ArrayList<>(); + + @Nonnull private String version; + private int protocolVersion; + + @Nullable private CachedServerIcon favicon; + + private boolean cancelled; + + private boolean originalPlayerCount = true; + private Object[] players; + + public PaperServerListPingEvent(@Nonnull StatusClient client, String motd, int numPlayers, int maxPlayers, + @Nonnull 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 + */ + @Nonnull + public StatusClient getClient() { + return this.client; + } + + /** + * {@inheritDoc} + * + *

Returns {@code -1} if players are hidden using + * {@link #shouldHidePlayers()}.

+ */ + @Override + public int getNumPlayers() { + if (this.hidePlayers) { + return -1; + } + + return this.numPlayers; + } + + /** + * Sets the number of players displayed in the server list. + * + *

Note that this won't have any effect if {@link #shouldHidePlayers()} + * is enabled.

+ * + * @param numPlayers The number of online players + */ + public void setNumPlayers(int numPlayers) { + if (this.numPlayers != numPlayers) { + this.numPlayers = numPlayers; + this.originalPlayerCount = false; + } + } + + /** + * {@inheritDoc} + * + *

Returns {@code -1} if players are hidden using + * {@link #shouldHidePlayers()}.

+ */ + @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. + * + *

The Vanilla Minecraft client will display the player count as {@code ???} + * when this option is enabled.

+ * + * @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. + * + *

The Vanilla Minecraft client will display the player count as {@code ???} + * when this option is enabled.

+ * + * @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. + * + *

The Vanilla Minecraft client will display them when hovering the + * player count with the mouse.

+ * + * @return The mutable player sample list + */ + @Nonnull + public List getPlayerSample() { + return this.playerSample; + } + + /** + * Returns the version that will be sent as server version on the client. + * + * @return The server version + */ + @Nonnull + 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(@Nonnull 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 + */ + 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} + * + *

Cancelling this event will cause the connection to be closed immediately, + * without sending a response to the client.

+ */ + @Override + public boolean isCancelled() { + return this.cancelled; + } + + /** + * {@inheritDoc} + * + *

Cancelling this event will cause the connection to be closed immediately, + * without sending a response to the client.

+ */ + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + /** + * {@inheritDoc} + * + *

Note: For compatibility reasons, this method will return all + * online players, not just the ones referenced in {@link #getPlayerSample()}. + * Removing a player will:

+ * + *
    + *
  • Decrement the online player count (if and only if) the player + * count wasn't changed by another plugin before.
  • + *
  • Remove all entries from {@link #getPlayerSample()} that refer to + * the removed player (based on their {@link UUID}).
  • + *
+ */ + @Nonnull + @Override + public Iterator iterator() { + if (this.players == null) { + this.players = getOnlinePlayers(); + } + + return new PlayerIterator(); + } + + protected Object[] getOnlinePlayers() { + return Bukkit.getOnlinePlayers().toArray(); + } + + protected Player getBukkitPlayer(Object player) { + return (Player) player; + } + + private final class PlayerIterator implements Iterator { + + 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; + } + + @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 00000000..517d1523 --- /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 04804706..44563482 100644 --- a/src/main/java/org/bukkit/util/CachedServerIcon.java +++ b/src/main/java/org/bukkit/util/CachedServerIcon.java @@ -14,4 +14,10 @@ import org.bukkit.event.server.ServerListPingEvent; */ public interface CachedServerIcon { public String getData(); // Spigot + + // Paper start + default boolean isEmpty() { + return getData() == null; + } + // Paper end } -- 2.16.2