diff --git a/Spigot-API-Patches/0176-Add-GS4-Query-event.patch b/Spigot-API-Patches/0176-Add-GS4-Query-event.patch new file mode 100644 index 000000000..2a4d5975f --- /dev/null +++ b/Spigot-API-Patches/0176-Add-GS4-Query-event.patch @@ -0,0 +1,426 @@ +From 784e07fa6c5fa8c82d5da5f08f79563f467231ca Mon Sep 17 00:00:00 2001 +From: Mark Vainomaa +Date: Sun, 17 Mar 2019 21:46:27 +0200 +Subject: [PATCH] Add GS4 Query event + + +diff --git a/src/main/java/com/destroystokyo/paper/event/server/GS4QueryEvent.java b/src/main/java/com/destroystokyo/paper/event/server/GS4QueryEvent.java +new file mode 100644 +index 000000000..2ead0466e +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/event/server/GS4QueryEvent.java +@@ -0,0 +1,411 @@ ++package com.destroystokyo.paper.event.server; ++ ++import com.google.common.base.Preconditions; ++import com.google.common.collect.ImmutableList; ++import org.bukkit.event.Event; ++import org.bukkit.event.HandlerList; ++import org.jetbrains.annotations.NotNull; ++ ++import java.net.InetAddress; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Collection; ++import java.util.List; ++ ++/** ++ * This event is fired if server is getting queried over GS4 Query protocol ++ * ++ * Adapted from Velocity's ProxyQueryEvent ++ * ++ * @author Mark Vainomaa ++ */ ++public final class GS4QueryEvent extends Event { ++ private static final HandlerList handlers = new HandlerList(); ++ ++ private final QueryType queryType; ++ private final InetAddress querierAddress; ++ private QueryResponse response; ++ ++ public GS4QueryEvent(@NotNull QueryType queryType, @NotNull InetAddress querierAddress, @NotNull QueryResponse response) { ++ this.queryType = Preconditions.checkNotNull(queryType, "queryType"); ++ this.querierAddress = Preconditions.checkNotNull(querierAddress, "querierAddress"); ++ this.response = Preconditions.checkNotNull(response, "response"); ++ } ++ ++ /** ++ * Get query type ++ * @return query type ++ */ ++ @NotNull ++ public QueryType getQueryType() { ++ return queryType; ++ } ++ ++ /** ++ * Get querier address ++ * @return querier address ++ */ ++ @NotNull ++ public InetAddress getQuerierAddress() { ++ return querierAddress; ++ } ++ ++ /** ++ * Get query response ++ * @return query response ++ */ ++ @NotNull ++ public QueryResponse getResponse() { ++ return response; ++ } ++ ++ /** ++ * Set query response ++ * @param response query response ++ */ ++ public void setResponse(@NotNull QueryResponse response) { ++ this.response = Preconditions.checkNotNull(response, "response"); ++ } ++ ++ @Override ++ public String toString() { ++ return "GS4QueryEvent{" + ++ "queryType=" + queryType + ++ ", querierAddress=" + querierAddress + ++ ", response=" + response + ++ '}'; ++ } ++ ++ @NotNull ++ @Override ++ public HandlerList getHandlers() { ++ return handlers; ++ } ++ ++ @NotNull ++ public static HandlerList getHandlerList() { ++ return handlers; ++ } ++ ++ /** ++ * The type of query ++ */ ++ public enum QueryType { ++ /** ++ * Basic query asks only a subset of information, such as motd, game type (hardcoded to
MINECRAFT
), map, ++ * current players, max players, server port and server motd ++ */ ++ BASIC, ++ ++ /** ++ * Full query asks pretty much everything present on this event (only hardcoded values cannot be modified here). ++ */ ++ FULL ++ ; ++ } ++ ++ public final static class QueryResponse { ++ private final String motd; ++ private final String gameVersion; ++ private final String map; ++ private final int currentPlayers; ++ private final int maxPlayers; ++ private final String hostname; ++ private final int port; ++ private final Collection players; ++ private final String serverVersion; ++ private final Collection plugins; ++ ++ private QueryResponse(String motd, String gameVersion, String map, int currentPlayers, int maxPlayers, String hostname, int port, Collection players, String serverVersion, Collection plugins) { ++ this.motd = motd; ++ this.gameVersion = gameVersion; ++ this.map = map; ++ this.currentPlayers = currentPlayers; ++ this.maxPlayers = maxPlayers; ++ this.hostname = hostname; ++ this.port = port; ++ this.players = players; ++ this.serverVersion = serverVersion; ++ this.plugins = plugins; ++ } ++ ++ /** ++ * Get motd which will be used to reply to the query. By default it is {@link org.bukkit.Server#getMotd()}. ++ * @return motd ++ */ ++ @NotNull ++ public String getMotd() { ++ return motd; ++ } ++ ++ /** ++ * Get game version which will be used to reply to the query. By default supported Minecraft versions range is sent. ++ * @return game version ++ */ ++ @NotNull ++ public String getGameVersion() { ++ return gameVersion; ++ } ++ ++ /** ++ * Get map name which will be used to reply to the query. By default {@code world} is sent. ++ * @return map name ++ */ ++ @NotNull ++ public String getMap() { ++ return map; ++ } ++ ++ /** ++ * Get current online player count which will be used to reply to the query. ++ * @return online player count ++ */ ++ public int getCurrentPlayers() { ++ return currentPlayers; ++ } ++ ++ /** ++ * Get max player count which will be used to reply to the query. ++ * @return max player count ++ */ ++ public int getMaxPlayers() { ++ return maxPlayers; ++ } ++ ++ /** ++ * Get server (public facing) hostname ++ * @return server hostname ++ */ ++ @NotNull ++ public String getHostname() { ++ return hostname; ++ } ++ ++ /** ++ * Get server (public facing) port ++ * @return server port ++ */ ++ public int getPort() { ++ return port; ++ } ++ ++ /** ++ * Get collection of players which will be used to reply to the query. ++ * @return collection of players ++ */ ++ @NotNull ++ public Collection getPlayers() { ++ return players; ++ } ++ ++ /** ++ * Get server software (name and version) which will be used to reply to the query. ++ * @return server software ++ */ ++ @NotNull ++ public String getServerVersion() { ++ return serverVersion; ++ } ++ ++ /** ++ * Get list of plugins which will be used to reply to the query. ++ * @return collection of plugins ++ */ ++ @NotNull ++ public Collection getPlugins() { ++ return plugins; ++ } ++ ++ ++ /** ++ * Creates a new {@link Builder} instance from data represented by this response ++ * @return {@link QueryResponse} builder ++ */ ++ @NotNull ++ public Builder toBuilder() { ++ return QueryResponse.builder() ++ .motd(getMotd()) ++ .gameVersion(getGameVersion()) ++ .map(getMap()) ++ .currentPlayers(getCurrentPlayers()) ++ .maxPlayers(getMaxPlayers()) ++ .hostname(getHostname()) ++ .port(getPort()) ++ .players(getPlayers()) ++ .serverVersion(getServerVersion()) ++ .plugins(getPlugins()); ++ } ++ ++ /** ++ * Creates a new {@link Builder} instance ++ * @return {@link QueryResponse} builder ++ */ ++ @NotNull ++ public static Builder builder() { ++ return new Builder(); ++ } ++ ++ /** ++ * A builder for {@link QueryResponse} objects. ++ */ ++ public static final class Builder { ++ private String motd; ++ private String gameVersion; ++ private String map; ++ private String hostname; ++ private String serverVersion; ++ ++ private int currentPlayers; ++ private int maxPlayers; ++ private int port; ++ ++ private List players = new ArrayList<>(); ++ private List plugins = new ArrayList<>(); ++ ++ private Builder() {} ++ ++ @NotNull ++ public Builder motd(@NotNull String motd) { ++ this.motd = Preconditions.checkNotNull(motd, "motd"); ++ return this; ++ } ++ ++ @NotNull ++ public Builder gameVersion(@NotNull String gameVersion) { ++ this.gameVersion = Preconditions.checkNotNull(gameVersion, "gameVersion"); ++ return this; ++ } ++ ++ @NotNull ++ public Builder map(@NotNull String map) { ++ this.map = Preconditions.checkNotNull(map, "map"); ++ return this; ++ } ++ ++ @NotNull ++ public Builder currentPlayers(int currentPlayers) { ++ Preconditions.checkArgument(currentPlayers >= 0, "currentPlayers cannot be negative"); ++ this.currentPlayers = currentPlayers; ++ return this; ++ } ++ ++ @NotNull ++ public Builder maxPlayers(int maxPlayers) { ++ Preconditions.checkArgument(maxPlayers >= 0, "maxPlayers cannot be negative"); ++ this.maxPlayers = maxPlayers; ++ return this; ++ } ++ ++ @NotNull ++ public Builder hostname(@NotNull String hostname) { ++ this.hostname = Preconditions.checkNotNull(hostname, "hostname"); ++ return this; ++ } ++ ++ @NotNull ++ public Builder port(int port) { ++ Preconditions.checkArgument(port >= 1 && port <= 65535, "port must be between 1-65535"); ++ this.port = port; ++ return this; ++ } ++ ++ @NotNull ++ public Builder players(@NotNull Collection players) { ++ this.players.addAll(Preconditions.checkNotNull(players, "players")); ++ return this; ++ } ++ ++ @NotNull ++ public Builder players(@NotNull String... players) { ++ this.players.addAll(Arrays.asList(Preconditions.checkNotNull(players, "players"))); ++ return this; ++ } ++ ++ @NotNull ++ public Builder clearPlayers() { ++ this.players.clear(); ++ return this; ++ } ++ ++ @NotNull ++ public Builder serverVersion(@NotNull String serverVersion) { ++ this.serverVersion = Preconditions.checkNotNull(serverVersion, "serverVersion"); ++ return this; ++ } ++ ++ @NotNull ++ public Builder plugins(@NotNull Collection plugins) { ++ this.plugins.addAll(Preconditions.checkNotNull(plugins, "plugins")); ++ return this; ++ } ++ ++ @NotNull ++ public Builder plugins(@NotNull PluginInformation... plugins) { ++ this.plugins.addAll(Arrays.asList(Preconditions.checkNotNull(plugins, "plugins"))); ++ return this; ++ } ++ ++ @NotNull ++ public Builder clearPlugins() { ++ this.plugins.clear(); ++ return this; ++ } ++ ++ /** ++ * Builds new {@link QueryResponse} with supplied data ++ * @return response ++ */ ++ @NotNull ++ public QueryResponse build() { ++ return new QueryResponse( ++ Preconditions.checkNotNull(motd, "motd"), ++ Preconditions.checkNotNull(gameVersion, "gameVersion"), ++ Preconditions.checkNotNull(map, "map"), ++ currentPlayers, ++ maxPlayers, ++ Preconditions.checkNotNull(hostname, "hostname"), ++ port, ++ ImmutableList.copyOf(players), ++ Preconditions.checkNotNull(serverVersion, "serverVersion"), ++ ImmutableList.copyOf(plugins) ++ ); ++ } ++ } ++ ++ /** ++ * Plugin information ++ */ ++ public static class PluginInformation { ++ private String name; ++ private String version; ++ ++ public PluginInformation(@NotNull String name, @NotNull String version) { ++ this.name = Preconditions.checkNotNull(name, "name"); ++ this.version = Preconditions.checkNotNull(version, "version"); ++ } ++ ++ @NotNull ++ public String getName() { ++ return name; ++ } ++ ++ public void setName(@NotNull String name) { ++ this.name = name; ++ } ++ ++ public void setVersion(@NotNull String version) { ++ this.version = version; ++ } ++ ++ @NotNull ++ public String getVersion() { ++ return version; ++ } ++ ++ @NotNull ++ public static PluginInformation of(@NotNull String name, @NotNull String version) { ++ return new PluginInformation(name, version); ++ } ++ } ++ } ++} +-- +2.21.0 + diff --git a/Spigot-Server-Patches/0426-Fire-event-on-GS4-query.patch b/Spigot-Server-Patches/0426-Fire-event-on-GS4-query.patch new file mode 100644 index 000000000..f203d2b48 --- /dev/null +++ b/Spigot-Server-Patches/0426-Fire-event-on-GS4-query.patch @@ -0,0 +1,220 @@ +From 7ec017a7334afb1f9abf3e69d755fdc797ce68ed Mon Sep 17 00:00:00 2001 +From: Mark Vainomaa +Date: Sun, 17 Mar 2019 21:46:56 +0200 +Subject: [PATCH] Fire event on GS4 query + + +diff --git a/src/main/java/net/minecraft/server/RemoteConnectionThread.java b/src/main/java/net/minecraft/server/RemoteConnectionThread.java +index d875b799ac..bcc36bbbfa 100644 +--- a/src/main/java/net/minecraft/server/RemoteConnectionThread.java ++++ b/src/main/java/net/minecraft/server/RemoteConnectionThread.java +@@ -15,7 +15,7 @@ public abstract class RemoteConnectionThread implements Runnable { + private static final Logger h = LogManager.getLogger(); + private static final AtomicInteger i = new AtomicInteger(0); + protected boolean a; +- protected IMinecraftServer b; ++ protected IMinecraftServer b; protected IMinecraftServer getServer() { return b; } // Paper - OBFHELPER + protected final String c; + protected Thread d; + protected int e = 5; +@@ -58,6 +58,7 @@ public abstract class RemoteConnectionThread implements Runnable { + this.b.f(s); + } + ++ protected int getPlayerCount() { return d(); } // Paper - OBFHELPER + protected int d() { + return this.b.getPlayerCount(); + } +diff --git a/src/main/java/net/minecraft/server/RemoteStatusListener.java b/src/main/java/net/minecraft/server/RemoteStatusListener.java +index 7dd81564a2..fd981931b0 100644 +--- a/src/main/java/net/minecraft/server/RemoteStatusListener.java ++++ b/src/main/java/net/minecraft/server/RemoteStatusListener.java +@@ -21,19 +21,19 @@ public class RemoteStatusListener extends RemoteConnectionThread { + + private long h; + private int i; +- private final int j; +- private final int k; +- private final String l; +- private final String m; ++ private final int j; private int getServerPort() { return j; } // Paper - OBFHELPER ++ private final int k; private int getMaxPlayers() { return k; } // Paper - OBFHELPER ++ private final String l; private String getMotd() { return l; } // Paper - OBFHELPER ++ private final String m; private String getWorldName() { return m; } // Paper - OBFHELPER + private DatagramSocket n; + private final byte[] o = new byte[1460]; + private DatagramPacket p; + private final Map q; +- private String r; ++ private String r; private String getServerHost() { return r; } // Paper - OBFHELPER + private String s; + private final Map t; + private final long u; +- private final RemoteStatusReply v; ++ private final RemoteStatusReply v; private RemoteStatusReply getCachedFullResponse() { return v; } // Paper - OBFHELPER + private long w; + + public RemoteStatusListener(IMinecraftServer iminecraftserver) { +@@ -99,6 +99,7 @@ public class RemoteStatusListener extends RemoteConnectionThread { + + remotestatusreply.a((int) 0); + remotestatusreply.a(this.a(datagrampacket.getSocketAddress())); ++ /* Paper start - GS4 Query event + remotestatusreply.a(this.l); + remotestatusreply.a("SMP"); + remotestatusreply.a(this.m); +@@ -106,6 +107,31 @@ public class RemoteStatusListener extends RemoteConnectionThread { + remotestatusreply.a(Integer.toString(this.k)); + remotestatusreply.a((short) this.j); + remotestatusreply.a(this.r); ++ */ ++ com.destroystokyo.paper.event.server.GS4QueryEvent.QueryType queryType = ++ com.destroystokyo.paper.event.server.GS4QueryEvent.QueryType.BASIC; ++ com.destroystokyo.paper.event.server.GS4QueryEvent.QueryResponse queryResponse = com.destroystokyo.paper.event.server.GS4QueryEvent.QueryResponse.builder() ++ .motd(this.getMotd()) ++ .map(this.getWorldName()) ++ .currentPlayers(this.getPlayerCount()) ++ .maxPlayers(this.getMaxPlayers()) ++ .port(this.getServerPort()) ++ .hostname(this.getServerHost()) ++ .gameVersion(this.getServer().getVersion()) ++ .serverVersion(org.bukkit.Bukkit.getServer().getName() + " on " + org.bukkit.Bukkit.getServer().getBukkitVersion()) ++ .build(); ++ com.destroystokyo.paper.event.server.GS4QueryEvent queryEvent = ++ new com.destroystokyo.paper.event.server.GS4QueryEvent(queryType, datagrampacket.getAddress(), queryResponse); ++ queryEvent.callEvent(); ++ queryResponse = queryEvent.getResponse(); ++ remotestatusreply.writeString(queryResponse.getMotd()); ++ remotestatusreply.writeString("SMP"); ++ remotestatusreply.writeString(queryResponse.getMap()); ++ remotestatusreply.writeString(Integer.toString(queryResponse.getCurrentPlayers())); ++ remotestatusreply.writeString(Integer.toString(queryResponse.getMaxPlayers())); ++ remotestatusreply.writeShort((short) queryResponse.getPort()); ++ remotestatusreply.writeString(queryResponse.getHostname()); ++ // Paper end + this.a(remotestatusreply.a(), datagrampacket); + this.a("Status [" + socketaddress + "]"); + } +@@ -142,6 +168,7 @@ public class RemoteStatusListener extends RemoteConnectionThread { + this.v.a("splitnum"); + this.v.a((int) 128); + this.v.a((int) 0); ++ /* Paper start - GS4 Query event + this.v.a("hostname"); + this.v.a(this.l); + this.v.a("gametype"); +@@ -177,6 +204,79 @@ public class RemoteStatusListener extends RemoteConnectionThread { + } + + this.v.a((int) 0); ++ */ ++ // Pack plugins ++ java.util.List plugins = java.util.Collections.emptyList(); ++ org.bukkit.plugin.Plugin[] bukkitPlugins; ++ if(((DedicatedServer) this.getServer()).server.getQueryPlugins() && (bukkitPlugins = org.bukkit.Bukkit.getPluginManager().getPlugins()).length > 0) { ++ plugins = java.util.stream.Stream.of(bukkitPlugins) ++ .map(plugin -> com.destroystokyo.paper.event.server.GS4QueryEvent.QueryResponse.PluginInformation.of(plugin.getName(), plugin.getDescription().getVersion())) ++ .collect(java.util.stream.Collectors.toList()); ++ } ++ ++ com.destroystokyo.paper.event.server.GS4QueryEvent.QueryResponse queryResponse = com.destroystokyo.paper.event.server.GS4QueryEvent.QueryResponse.builder() ++ .motd(this.getMotd()) ++ .map(this.getWorldName()) ++ .currentPlayers(this.getPlayerCount()) ++ .maxPlayers(this.getMaxPlayers()) ++ .port(this.getServerPort()) ++ .hostname(this.getServerHost()) ++ .plugins(plugins) ++ .players(this.getServer().getPlayers()) ++ .gameVersion(this.getServer().getVersion()) ++ .serverVersion(org.bukkit.Bukkit.getServer().getName() + " on " + org.bukkit.Bukkit.getServer().getBukkitVersion()) ++ .build(); ++ com.destroystokyo.paper.event.server.GS4QueryEvent.QueryType queryType = ++ com.destroystokyo.paper.event.server.GS4QueryEvent.QueryType.FULL; ++ com.destroystokyo.paper.event.server.GS4QueryEvent queryEvent = ++ new com.destroystokyo.paper.event.server.GS4QueryEvent(queryType, datagrampacket.getAddress(), queryResponse); ++ queryEvent.callEvent(); ++ queryResponse = queryEvent.getResponse(); ++ this.getCachedFullResponse().writeString("hostname"); ++ this.getCachedFullResponse().writeString(queryResponse.getMotd()); ++ this.getCachedFullResponse().writeString("gametype"); ++ this.getCachedFullResponse().writeString("SMP"); ++ this.getCachedFullResponse().writeString("game_id"); ++ this.getCachedFullResponse().writeString("MINECRAFT"); ++ this.getCachedFullResponse().writeString("version"); ++ this.getCachedFullResponse().writeString(queryResponse.getGameVersion()); ++ this.getCachedFullResponse().writeString("plugins"); ++ java.lang.StringBuilder pluginsString = new java.lang.StringBuilder(); ++ pluginsString.append(queryResponse.getServerVersion()); ++ if(!queryResponse.getPlugins().isEmpty()) { ++ pluginsString.append(": "); ++ Iterator iter = queryResponse.getPlugins().iterator(); ++ while(iter.hasNext()) { ++ com.destroystokyo.paper.event.server.GS4QueryEvent.QueryResponse.PluginInformation info = iter.next(); ++ pluginsString.append(info.getName()); ++ if (info.getVersion() != null) { ++ pluginsString.append(' ').append(info.getVersion().replaceAll(";", ",")); ++ } ++ if (iter.hasNext()) { ++ pluginsString.append(';').append(' '); ++ } ++ } ++ } ++ this.getCachedFullResponse().writeString(pluginsString.toString()); ++ this.getCachedFullResponse().writeString("map"); ++ this.getCachedFullResponse().writeString(queryResponse.getMap()); ++ this.getCachedFullResponse().writeString("numplayers"); ++ this.getCachedFullResponse().writeString(Integer.toString(queryResponse.getCurrentPlayers())); ++ this.getCachedFullResponse().writeString("maxplayers"); ++ this.getCachedFullResponse().writeString(Integer.toString(queryResponse.getMaxPlayers())); ++ this.getCachedFullResponse().writeString("hostport"); ++ this.getCachedFullResponse().writeString(Integer.toString(queryResponse.getPort())); ++ this.getCachedFullResponse().writeString("hostip"); ++ this.getCachedFullResponse().writeString(queryResponse.getHostname()); ++ // The "meaningless data" start, copied from above ++ this.getCachedFullResponse().writeInt(0); ++ this.getCachedFullResponse().writeInt(1); ++ this.getCachedFullResponse().writeString("player_"); ++ this.getCachedFullResponse().writeInt(0); ++ // "Meaningless data" end ++ queryResponse.getPlayers().forEach(this.getCachedFullResponse()::writeStringUnchecked); ++ this.getCachedFullResponse().writeInt(0); ++ // Paper end + return this.v.a(); + } + } +diff --git a/src/main/java/net/minecraft/server/RemoteStatusReply.java b/src/main/java/net/minecraft/server/RemoteStatusReply.java +index 848b5c3f0e..9e8c8b3df2 100644 +--- a/src/main/java/net/minecraft/server/RemoteStatusReply.java ++++ b/src/main/java/net/minecraft/server/RemoteStatusReply.java +@@ -18,15 +18,27 @@ public class RemoteStatusReply { + this.b.write(abyte, 0, abyte.length); + } + ++ public void writeString(String string) throws IOException { a(string); } // Paper - OBFHELPER + public void a(String s) throws IOException { + this.b.writeBytes(s); + this.b.write(0); + } ++ // Paper start - unchecked exception variant to use in Stream API ++ public void writeStringUnchecked(String string) { ++ try { ++ writeString(string); ++ } catch (IOException e) { ++ com.destroystokyo.paper.util.SneakyThrow.sneaky(e); ++ } ++ } ++ // Paper end + ++ public void writeInt(int i) throws IOException { a(i); } // Paper - OBFHELPER + public void a(int i) throws IOException { + this.b.write(i); + } + ++ public void writeShort(short i) throws IOException { a(i); } // Paper - OBFHELPER + public void a(short short0) throws IOException { + this.b.writeShort(Short.reverseBytes(short0)); + } +-- +2.21.0 +