mirror of
https://github.com/PaperMC/Paper.git
synced 2024-12-30 16:19:03 +01:00
Add extended PaperServerListPingEvent
Add a new event that extends the original ServerListPingEvent and allows full control of the response sent to the client.
This commit is contained in:
parent
bb5e4dd0eb
commit
2eaa723e96
4 changed files with 522 additions and 0 deletions
|
@ -0,0 +1,502 @@
|
||||||
|
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 com.destroystokyo.paper.profile.ProfileProperty;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import io.papermc.paper.util.TransformingRandomAccessList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.Cancellable;
|
||||||
|
import org.bukkit.event.server.ServerListPingEvent;
|
||||||
|
import org.bukkit.profile.PlayerTextures;
|
||||||
|
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.ApiStatus;
|
||||||
|
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<ListedPlayerInfo> listedPlayers = new ArrayList<>();
|
||||||
|
@NotNull private final TransformingRandomAccessList<ListedPlayerInfo, PlayerProfile> playerSample = new TransformingRandomAccessList<>(
|
||||||
|
listedPlayers,
|
||||||
|
info -> new UncheckedPlayerProfile(info.name(), info.id()),
|
||||||
|
profile -> new ListedPlayerInfo(profile.getName(), profile.getId())
|
||||||
|
);
|
||||||
|
|
||||||
|
@NotNull private String version;
|
||||||
|
private int protocolVersion;
|
||||||
|
|
||||||
|
@Nullable private CachedServerIcon favicon;
|
||||||
|
|
||||||
|
private boolean cancelled;
|
||||||
|
|
||||||
|
private boolean originalPlayerCount = true;
|
||||||
|
private Object[] players;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
@ApiStatus.Internal
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiStatus.Internal
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @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()}.
|
||||||
|
*/
|
||||||
|
@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.
|
||||||
|
*
|
||||||
|
* @return {@code true} if the player count is hidden
|
||||||
|
*/
|
||||||
|
public boolean shouldHidePlayers() {
|
||||||
|
return this.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.
|
||||||
|
*
|
||||||
|
* @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 ListedPlayerInfo} 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.
|
||||||
|
*
|
||||||
|
* @return The mutable player sample list
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public List<ListedPlayerInfo> getListedPlayers() {
|
||||||
|
return this.listedPlayers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @return The mutable player sample list
|
||||||
|
* @deprecated Use {@link #getListedPlayers()}, as this does not contain real player profiles
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
@Deprecated(forRemoval = true, since = "1.20.6")
|
||||||
|
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 this.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 this.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.
|
||||||
|
*/
|
||||||
|
@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.
|
||||||
|
*/
|
||||||
|
@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:
|
||||||
|
*
|
||||||
|
* <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>
|
||||||
|
* @deprecated the Iterable interface will be removed at some point
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
@Deprecated(forRemoval = true, since = "1.20.6")
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiStatus.Internal
|
||||||
|
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--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a player that will be displayed in the player sample of the server list.
|
||||||
|
*
|
||||||
|
* @param name name of the listed player
|
||||||
|
* @param id UUID of the listed player
|
||||||
|
*/
|
||||||
|
public record ListedPlayerInfo(@NotNull String name, @NotNull UUID id) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiStatus.Internal
|
||||||
|
private static final class UncheckedPlayerProfile implements PlayerProfile {
|
||||||
|
private String name;
|
||||||
|
private UUID uuid;
|
||||||
|
|
||||||
|
public UncheckedPlayerProfile(final @NotNull String name, final @NotNull UUID uuid) {
|
||||||
|
Preconditions.checkNotNull(name, "name cannot be null");
|
||||||
|
Preconditions.checkNotNull(uuid, "uuid cannot be null");
|
||||||
|
this.name = name;
|
||||||
|
this.uuid = uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable UUID getUniqueId() {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String setName(@Nullable final String name) {
|
||||||
|
Preconditions.checkNotNull(name, "name cannot be null");
|
||||||
|
return this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable UUID getId() {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable UUID setId(@Nullable final UUID uuid) {
|
||||||
|
Preconditions.checkNotNull(uuid, "uuid cannot be null");
|
||||||
|
return this.uuid = uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull PlayerTextures getTextures() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTextures(@Nullable final PlayerTextures textures) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Set<ProfileProperty> getProperties() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasProperty(@Nullable final String property) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setProperty(@NotNull final ProfileProperty property) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setProperties(@NotNull final Collection<ProfileProperty> properties) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean removeProperty(@Nullable final String property) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearProperties() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isComplete() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean completeFromCache() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean completeFromCache(final boolean onlineMode) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean completeFromCache(final boolean lookupUUID, final boolean onlineMode) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean complete(final boolean textures) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean complete(final boolean textures, final boolean onlineMode) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull CompletableFuture<PlayerProfile> update() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public org.bukkit.profile.@NotNull PlayerProfile clone() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Map<String, Object> serialize() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
|
@ -248,9 +248,11 @@ public class ServerListPingEvent extends ServerEvent implements Iterable<Player>
|
||||||
*
|
*
|
||||||
* @throws UnsupportedOperationException if the caller of this event does
|
* @throws UnsupportedOperationException if the caller of this event does
|
||||||
* not support removing players
|
* not support removing players
|
||||||
|
* @deprecated the Iterable interface will be removed at some point
|
||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
|
@Deprecated(forRemoval = true, since = "1.20.6")
|
||||||
public Iterator<Player> iterator() throws UnsupportedOperationException {
|
public Iterator<Player> iterator() throws UnsupportedOperationException {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,4 +18,9 @@ public interface CachedServerIcon {
|
||||||
@Nullable
|
@Nullable
|
||||||
public String getData(); // Paper
|
public String getData(); // Paper
|
||||||
|
|
||||||
|
// Paper start
|
||||||
|
default boolean isEmpty() {
|
||||||
|
return getData() == null;
|
||||||
|
}
|
||||||
|
// Paper end
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue