From 673af650784bb41748317547dbbaa359c2ad8df4 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sun, 14 May 2023 03:18:20 -0400 Subject: [PATCH 01/34] fix --- .../proxy/plugin/loader/VelocityPluginContainer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginContainer.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginContainer.java index fb9a4ea3f..40b00df7c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginContainer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginContainer.java @@ -61,6 +61,7 @@ public class VelocityPluginContainer implements PluginContainer { Executors.newCachedThreadPool( new ThreadFactoryBuilder().setDaemon(true) .setNameFormat(name + " - Task Executor #%d") + .setDaemon(true) .build() ) ); From 12a05f6d751ce0f8447a6ae5832c21dc494ed4fd Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Mon, 15 May 2023 01:15:49 -0400 Subject: [PATCH 02/34] When pinging the server with protocol version -1, return the latest version supported by the proxy This matches BungeeCord's behavior and is in line with what clients specifying this meta-version would expect. --- .../proxy/connection/util/ServerListPingHandler.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java index 760ef2fea..d253d3594 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java @@ -47,6 +47,9 @@ public class ServerListPingHandler { } private ServerPing constructLocalPing(ProtocolVersion version) { + if (version == ProtocolVersion.UNKNOWN) { + version = ProtocolVersion.MAXIMUM_VERSION; + } VelocityConfiguration configuration = server.getConfiguration(); return new ServerPing( new ServerPing.Version(version.getProtocol(), From 92ae25b8acac10605480270940bd69cfc4bb50b7 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Mon, 15 May 2023 01:20:45 -0400 Subject: [PATCH 03/34] Also make sure to use the latest ping serializer as well --- .../com/velocitypowered/proxy/VelocityServer.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 74dd5a538..f82fd45d5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -747,9 +747,18 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { return bossBarManager; } + /** + * Returns a Gson instance for use in serializing server ping instances. + * + * @param version the protocol version in use + * @return the Gson instance + */ public static Gson getPingGsonInstance(ProtocolVersion version) { - return version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0 ? POST_1_16_PING_SERIALIZER - : PRE_1_16_PING_SERIALIZER; + if (version == ProtocolVersion.UNKNOWN + || version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { + return POST_1_16_PING_SERIALIZER; + } + return PRE_1_16_PING_SERIALIZER; } @Override From 40b76c633276fcd6aea165baeae74039b2d059c4 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Mon, 15 May 2023 01:21:30 -0400 Subject: [PATCH 04/34] Fix flaky obtainTasksFromPlugin() test --- .../velocitypowered/proxy/scheduler/VelocitySchedulerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/src/test/java/com/velocitypowered/proxy/scheduler/VelocitySchedulerTest.java b/proxy/src/test/java/com/velocitypowered/proxy/scheduler/VelocitySchedulerTest.java index 70cd20b10..e31a8a7e3 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/scheduler/VelocitySchedulerTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/scheduler/VelocitySchedulerTest.java @@ -76,12 +76,12 @@ class VelocitySchedulerTest { scheduler.buildTask(FakePluginManager.PLUGIN_A, task -> { runningLatch.countDown(); - task.cancel(); try { endingLatch.await(); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); } + task.cancel(); }).delay(50, TimeUnit.MILLISECONDS) .repeat(Duration.ofMillis(5)) .schedule(); From e0cf2e211f0ec211a57feccf3a562620ccc1b726 Mon Sep 17 00:00:00 2001 From: Adrian <68704415+4drian3d@users.noreply.github.com> Date: Fri, 2 Jun 2023 21:12:03 -0500 Subject: [PATCH 05/34] Updated Guice to 6.0.0 and manually include updated ASM (#1016) --- gradle/libs.versions.toml | 3 ++- proxy/build.gradle.kts | 1 + .../com/velocitypowered/proxy/event/VelocityEventManager.java | 3 +-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e2e762c01..a3e6182a5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ spotless = "com.diffplug.spotless:6.12.0" [libraries] adventure-bom = "net.kyori:adventure-bom:4.13.1" adventure-facet = "net.kyori:adventure-platform-facet:4.3.0" +asm = "org.ow2.asm:asm:9.5" asynchttpclient = "org.asynchttpclient:async-http-client:2.12.3" brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT" bstats = "org.bstats:bstats-base:3.0.1" @@ -30,7 +31,7 @@ jopt = "net.sf.jopt-simple:jopt-simple:5.0.4" junit = "org.junit.jupiter:junit-jupiter:5.9.0" guava = "com.google.guava:guava:25.1-jre" gson = "com.google.code.gson:gson:2.10.1" -guice = "com.google.inject:guice:5.1.0" +guice = "com.google.inject:guice:6.0.0" lmbda = "org.lanternpowered:lmbda:2.0.0" log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index 997ddd467..8d6ed5945 100644 --- a/proxy/build.gradle.kts +++ b/proxy/build.gradle.kts @@ -114,6 +114,7 @@ dependencies { implementation(libs.nightconfig) implementation(libs.bstats) implementation(libs.lmbda) + implementation(libs.asm) implementation(libs.bundles.flare) compileOnly(libs.spotbugs.annotations) testImplementation(libs.mockito) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java b/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java index 5c8542708..85bb7cceb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java @@ -306,8 +306,7 @@ public class VelocityEventManager implements EventManager { if (returnType != void.class && continuationType == Continuation.class) { errors.add("method return type must be void if a continuation parameter is provided"); } else if (returnType != void.class && returnType != EventTask.class) { - errors.add("method return type must be void, AsyncTask, " - + "AsyncTask.Basic or AsyncTask.WithContinuation"); + errors.add("method return type must be void or EventTask"); } } final short order = (short) subscribe.order().ordinal(); From 5ef90c46e32b9d3fb2b80b9ea6e3a84e7350bfec Mon Sep 17 00:00:00 2001 From: Gero Date: Sat, 13 May 2023 09:59:55 +0200 Subject: [PATCH 06/34] 1.20 --- .../api/network/ProtocolVersion.java | 3 ++- .../proxy/protocol/packet/JoinGame.java | 18 +++++++++++++ .../proxy/protocol/packet/Respawn.java | 26 ++++++++++++++++--- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java index 62d09a32d..8e77a2f8e 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -61,7 +61,8 @@ public enum ProtocolVersion { MINECRAFT_1_19(759, "1.19"), MINECRAFT_1_19_1(760, "1.19.1", "1.19.2"), MINECRAFT_1_19_3(761, "1.19.3"), - MINECRAFT_1_19_4(762, "1.19.4"); + MINECRAFT_1_19_4(762, "1.19.4"), + MINECRAFT_1_20(763, "1.20"); private static final int SNAPSHOT_BIT = 30; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java index 90320f948..cfbecce31 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java @@ -49,6 +49,7 @@ public class JoinGame implements MinecraftPacket { private short previousGamemode; // 1.16+ private int simulationDistance; // 1.18+ private @Nullable Pair lastDeathPosition; // 1.19+ + private int portalCooldown; // 1.20+ public int getEntityId() { return entityId; @@ -162,6 +163,14 @@ public class JoinGame implements MinecraftPacket { this.lastDeathPosition = lastDeathPosition; } + public int getPortalCooldown() { + return portalCooldown; + } + + public void setPortalCooldown(int portalCooldown) { + this.portalCooldown = portalCooldown; + } + public CompoundBinaryTag getRegistry() { return registry; } @@ -187,6 +196,7 @@ public class JoinGame implements MinecraftPacket { + ", previousGamemode=" + previousGamemode + ", simulationDistance=" + simulationDistance + ", lastDeathPosition='" + lastDeathPosition + '\'' + + ", portalCooldown=" + portalCooldown + '}'; } @@ -279,6 +289,10 @@ public class JoinGame implements MinecraftPacket { if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 && buf.readBoolean()) { this.lastDeathPosition = Pair.of(ProtocolUtils.readString(buf), buf.readLong()); } + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) { + this.portalCooldown = ProtocolUtils.readVarInt(buf); + } } @Override @@ -376,6 +390,10 @@ public class JoinGame implements MinecraftPacket { buf.writeBoolean(false); } } + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) { + ProtocolUtils.writeVarInt(buf, portalCooldown); + } } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java index 11d543c89..712024f63 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java @@ -40,6 +40,7 @@ public class Respawn implements MinecraftPacket { private short previousGamemode; // 1.16+ private CompoundBinaryTag currentDimensionData; // 1.16.2+ private @Nullable Pair lastDeathPosition; // 1.19+ + private int portalCooldown; // 1.20+ public Respawn() { } @@ -47,7 +48,7 @@ public class Respawn implements MinecraftPacket { public Respawn(int dimension, long partialHashedSeed, short difficulty, short gamemode, String levelType, byte dataToKeep, DimensionInfo dimensionInfo, short previousGamemode, CompoundBinaryTag currentDimensionData, - @Nullable Pair lastDeathPosition) { + @Nullable Pair lastDeathPosition, int portalCooldown) { this.dimension = dimension; this.partialHashedSeed = partialHashedSeed; this.difficulty = difficulty; @@ -58,13 +59,14 @@ public class Respawn implements MinecraftPacket { this.previousGamemode = previousGamemode; this.currentDimensionData = currentDimensionData; this.lastDeathPosition = lastDeathPosition; + this.portalCooldown = portalCooldown; } public static Respawn fromJoinGame(JoinGame joinGame) { return new Respawn(joinGame.getDimension(), joinGame.getPartialHashedSeed(), joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType(), (byte) 0, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(), - joinGame.getCurrentDimensionData(), joinGame.getLastDeathPosition()); + joinGame.getCurrentDimensionData(), joinGame.getLastDeathPosition(), joinGame.getPortalCooldown()); } public int getDimension() { @@ -123,12 +125,20 @@ public class Respawn implements MinecraftPacket { this.previousGamemode = previousGamemode; } + public Pair getLastDeathPosition() { + return lastDeathPosition; + } + public void setLastDeathPosition(Pair lastDeathPosition) { this.lastDeathPosition = lastDeathPosition; } - public Pair getLastDeathPosition() { - return lastDeathPosition; + public int getPortalCooldown() { + return portalCooldown; + } + + public void setPortalCooldown(int portalCooldown) { + this.portalCooldown = portalCooldown; } @Override @@ -144,6 +154,7 @@ public class Respawn implements MinecraftPacket { + ", dimensionInfo=" + dimensionInfo + ", previousGamemode=" + previousGamemode + ", dimensionData=" + currentDimensionData + + ", portalCooldown=" + portalCooldown + '}'; } @@ -188,6 +199,9 @@ public class Respawn implements MinecraftPacket { if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 && buf.readBoolean()) { this.lastDeathPosition = Pair.of(ProtocolUtils.readString(buf), buf.readLong()); } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) { + this.portalCooldown = ProtocolUtils.readVarInt(buf); + } } @Override @@ -234,6 +248,10 @@ public class Respawn implements MinecraftPacket { buf.writeBoolean(false); } } + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) { + ProtocolUtils.writeVarInt(buf, portalCooldown); + } } @Override From bda1430d5c1a01b0d010cdbc033d79e6602eeedc Mon Sep 17 00:00:00 2001 From: "Pantera (Mad_Daniel)" <89838384+Pantera07@users.noreply.github.com> Date: Tue, 13 Jun 2023 00:11:20 +0900 Subject: [PATCH 07/34] Add version information for 1.20.1 (#1021) --- .../java/com/velocitypowered/api/network/ProtocolVersion.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java index 8e77a2f8e..110b58767 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -62,7 +62,7 @@ public enum ProtocolVersion { MINECRAFT_1_19_1(760, "1.19.1", "1.19.2"), MINECRAFT_1_19_3(761, "1.19.3"), MINECRAFT_1_19_4(762, "1.19.4"), - MINECRAFT_1_20(763, "1.20"); + MINECRAFT_1_20(763, "1.20", "1.20.1"); private static final int SNAPSHOT_BIT = 30; From 37fedf508e10fe03203da9b0ee53dca04e03db45 Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 28 Jun 2023 23:27:32 +0200 Subject: [PATCH 08/34] bump adventure to 4.14.0 (#1034) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a3e6182a5..d204ff616 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ shadow = "com.github.johnrengelman.shadow:8.1.0" spotless = "com.diffplug.spotless:6.12.0" [libraries] -adventure-bom = "net.kyori:adventure-bom:4.13.1" +adventure-bom = "net.kyori:adventure-bom:4.14.0" adventure-facet = "net.kyori:adventure-platform-facet:4.3.0" asm = "org.ow2.asm:asm:9.5" asynchttpclient = "org.asynchttpclient:async-http-client:2.12.3" From be9ecf75b2e1fa014e2ce21ebbcfaea5ecdd458d Mon Sep 17 00:00:00 2001 From: Aaron <71191102+RealBauHD@users.noreply.github.com> Date: Sat, 5 Aug 2023 02:48:01 +0200 Subject: [PATCH 09/34] check if a plugin has a executor service (#1038) * check if a plugin has an executor service * checkstyle --- .../loader/VelocityPluginContainer.java | 4 ++++ .../proxy/scheduler/VelocityScheduler.java | 22 ++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginContainer.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginContainer.java index 40b00df7c..5e757708e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginContainer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginContainer.java @@ -71,4 +71,8 @@ public class VelocityPluginContainer implements PluginContainer { return this.service; } + + public boolean hasExecutorService() { + return this.service != null; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java b/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java index 96d0b94d6..727832bd4 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java @@ -30,9 +30,12 @@ import com.velocitypowered.api.scheduler.ScheduledTask; import com.velocitypowered.api.scheduler.Scheduler; import com.velocitypowered.api.scheduler.TaskStatus; import com.velocitypowered.proxy.plugin.loader.VelocityPluginContainer; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -116,17 +119,24 @@ public class VelocityScheduler implements Scheduler { task.cancel(); } timerExecutionService.shutdown(); - for (final PluginContainer container : this.pluginManager.getPlugins()) { + final List plugins = new ArrayList<>(this.pluginManager.getPlugins()); + final Iterator pluginIterator = plugins.iterator(); + while (pluginIterator.hasNext()) { + final PluginContainer container = pluginIterator.next(); if (container instanceof VelocityPluginContainer) { - (container).getExecutorService().shutdown(); + final VelocityPluginContainer pluginContainer = (VelocityPluginContainer) container; + if (pluginContainer.hasExecutorService()) { + container.getExecutorService().shutdown(); + } else { + pluginIterator.remove(); + } + } else { + pluginIterator.remove(); } } boolean allShutdown = true; - for (final PluginContainer container : this.pluginManager.getPlugins()) { - if (!(container instanceof VelocityPluginContainer)) { - continue; - } + for (final PluginContainer container : plugins) { final String id = container.getDescription().getId(); final ExecutorService service = (container).getExecutorService(); From 81b45d710bf1198e5afac371f790d0c3133f9364 Mon Sep 17 00:00:00 2001 From: powercas_gamer Date: Sat, 12 Aug 2023 19:49:08 +0200 Subject: [PATCH 10/34] feat: add TabList#addEntries (#987) --- .../api/proxy/player/TabList.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java b/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java index bceed260f..4d03d3a87 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java @@ -43,6 +43,28 @@ public interface TabList { */ void addEntry(TabListEntry entry); + /** + * Adds a {@link Iterable} of {@link TabListEntry}'s to the {@link Player}'s tab list. + * + * @param entries to add to the tab list + */ + default void addEntries(Iterable entries) { + for (TabListEntry entry : entries) { + addEntry(entry); + } + } + + /** + * Adds an array of {@link TabListEntry}'s to the {@link Player}'s tab list. + * + * @param entries to add to the tab list + */ + default void addEntries(TabListEntry... entries) { + for (TabListEntry entry : entries) { + addEntry(entry); + } + } + /** * Removes the {@link TabListEntry} from the tab list with the {@link GameProfile} identified with * the specified {@link UUID}. From 2aaf702a2eb53b271b477bebf9ef63665d24e48b Mon Sep 17 00:00:00 2001 From: Groldi <53694440+Plugrol@users.noreply.github.com> Date: Sun, 13 Aug 2023 21:36:53 +0200 Subject: [PATCH 11/34] [ci skip] Replaced weired i with i in javadocs (#1057) In this little patch I replaced an i which caused my build process to crash with an i --- .../velocitypowered/api/event/player/ServerPreConnectEvent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/com/velocitypowered/api/event/player/ServerPreConnectEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/ServerPreConnectEvent.java index 671a7adcd..98bcb60d3 100644 --- a/api/src/main/java/com/velocitypowered/api/event/player/ServerPreConnectEvent.java +++ b/api/src/main/java/com/velocitypowered/api/event/player/ServerPreConnectEvent.java @@ -47,7 +47,7 @@ public final class ServerPreConnectEvent implements * * @param player the player who is connecting to a server * @param originalServer the server the player was trying to connect to - * @param previousServer the server the player ís connected to + * @param previousServer the server the player is connected to */ public ServerPreConnectEvent(Player player, RegisteredServer originalServer, @Nullable RegisteredServer previousServer) { From f62d75989698b9efee51cdd8e59be0a4b587f3ed Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Thu, 17 Aug 2023 23:51:34 -0400 Subject: [PATCH 12/34] Do not track plugin channels registered per-player on the proxy (#591) We don't need to track this information since Velocity uses the JoinGame packet, which is about as good of a server rejoin mechanism we're likely to get in vanilla Minecraft. --- .../backend/BackendPlaySessionHandler.java | 9 +-- .../backend/TransitionSessionHandler.java | 7 -- .../client/ClientPlaySessionHandler.java | 9 --- .../connection/client/ConnectedPlayer.java | 13 ---- .../client/InitialConnectSessionHandler.java | 11 +-- .../proxy/util/collect/CappedSet.java | 70 ------------------ .../proxy/util/collect/CappedSetTest.java | 71 ------------------- 7 files changed, 3 insertions(+), 187 deletions(-) delete mode 100644 proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java delete mode 100644 proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index 3adf6ba25..ff777f5f9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -198,13 +198,8 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { return true; } - // We need to specially handle REGISTER and UNREGISTER packets. Later on, we'll write them to - // the client. - if (PluginMessageUtil.isRegister(packet)) { - serverConn.getPlayer().getKnownChannels().addAll(PluginMessageUtil.getChannels(packet)); - return false; - } else if (PluginMessageUtil.isUnregister(packet)) { - serverConn.getPlayer().getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet)); + // Register and unregister packets are simply forwarded to the server as-is. + if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) { return false; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java index 25cbbef63..5fe6886dc 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java @@ -36,7 +36,6 @@ import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.JoinGame; import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.PluginMessage; -import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import java.io.IOException; import java.util.concurrent.CompletableFuture; import org.apache.logging.log4j.LogManager; @@ -180,12 +179,6 @@ public class TransitionSessionHandler implements MinecraftSessionHandler { return true; } - if (PluginMessageUtil.isRegister(packet)) { - serverConn.getPlayer().getKnownChannels().addAll(PluginMessageUtil.getChannels(packet)); - } else if (PluginMessageUtil.isUnregister(packet)) { - serverConn.getPlayer().getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet)); - } - // We always need to handle plugin messages, for Forge compatibility. if (serverConn.getPhase().handle(serverConn, serverConn.getPlayer(), packet)) { // Handled, but check the server connection phase. diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index bc041d8ab..dec1b85df 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -156,7 +156,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { if (!channels.isEmpty()) { PluginMessage register = constructChannelsPacket(player.getProtocolVersion(), channels); player.getConnection().write(register); - player.getKnownChannels().addAll(channels); } } @@ -295,7 +294,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { packet.getChannel()); } else if (PluginMessageUtil.isRegister(packet)) { List channels = PluginMessageUtil.getChannels(packet); - player.getKnownChannels().addAll(channels); List channelIdentifiers = new ArrayList<>(); for (String channel : channels) { try { @@ -309,7 +307,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channelIdentifiers))); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isUnregister(packet)) { - player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet)); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isMcBrand(packet)) { String brand = PluginMessageUtil.readBrandMessage(packet.content()); @@ -485,12 +482,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } serverBossBars.clear(); - // Tell the server about this client's plugin message channels. - ProtocolVersion serverVersion = serverMc.getProtocolVersion(); - if (!player.getKnownChannels().isEmpty()) { - serverMc.delayedWrite(constructChannelsPacket(serverVersion, player.getKnownChannels())); - } - // If we had plugin messages queued during login/FML handshake, send them now. PluginMessage pm; while ((pm = loginPluginMessages.poll()) != null) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 579cd60ee..90464c9c8 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -77,12 +77,10 @@ import com.velocitypowered.proxy.tablist.VelocityTabList; import com.velocitypowered.proxy.tablist.VelocityTabListLegacy; import com.velocitypowered.proxy.util.ClosestLocaleMatcher; import com.velocitypowered.proxy.util.DurationUtils; -import com.velocitypowered.proxy.util.collect.CappedSet; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import java.net.InetSocketAddress; import java.util.ArrayDeque; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -155,7 +153,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private final InternalTabList tabList; private final VelocityServer server; private ClientConnectionPhase connectionPhase; - private final Collection knownChannels; private final CompletableFuture teardownFuture = new CompletableFuture<>(); private @MonotonicNonNull List serversToTry = null; private @MonotonicNonNull Boolean previousResourceResponse; @@ -185,7 +182,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, this.virtualHost = virtualHost; this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED; this.connectionPhase = connection.getType().getInitialClientPhase(); - this.knownChannels = CappedSet.create(MAX_PLUGIN_CHANNELS); this.onlineMode = onlineMode; if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19_3) >= 0) { @@ -1103,15 +1099,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, this.connectionPhase = connectionPhase; } - /** - * Return all the plugin message channels "known" to the client. - * - * @return the channels - */ - public Collection getKnownChannels() { - return knownChannels; - } - @Override public @Nullable IdentifiedKey getIdentifiedKey() { return playerKey; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java index fcab2d62d..f264d262e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java @@ -24,7 +24,6 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.backend.BungeeCordMessageResponder; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.protocol.packet.PluginMessage; -import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import org.apache.logging.log4j.LogManager; @@ -55,15 +54,7 @@ public class InitialConnectSessionHandler implements MinecraftSessionHandler { return true; } - if (PluginMessageUtil.isRegister(packet)) { - player.getKnownChannels().addAll(PluginMessageUtil.getChannels(packet)); - serverConn.ensureConnected().write(packet.retain()); - return true; - } else if (PluginMessageUtil.isUnregister(packet)) { - player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet)); - serverConn.ensureConnected().write(packet.retain()); - return true; - } else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) { + if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) { return true; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java b/proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java deleted file mode 100644 index 692910d57..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2019-2023 Velocity Contributors - * - * 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 . - */ - -package com.velocitypowered.proxy.util.collect; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ForwardingSet; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -/** - * An unsynchronized collection that puts an upper bound on the size of the collection. - */ -public final class CappedSet extends ForwardingSet { - - private final Set delegate; - private final int upperSize; - - private CappedSet(Set delegate, int upperSize) { - this.delegate = delegate; - this.upperSize = upperSize; - } - - /** - * Creates a capped collection backed by a {@link HashSet}. - * - * @param maxSize the maximum size of the collection - * @param the type of elements in the collection - * @return the new collection - */ - public static Set create(int maxSize) { - return new CappedSet<>(new HashSet<>(), maxSize); - } - - @Override - protected Set delegate() { - return delegate; - } - - @Override - public boolean add(T element) { - if (this.delegate.size() >= upperSize) { - Preconditions.checkState(this.delegate.contains(element), - "collection is too large (%s >= %s)", - this.delegate.size(), this.upperSize); - return false; - } - return this.delegate.add(element); - } - - @Override - public boolean addAll(Collection collection) { - return this.standardAddAll(collection); - } -} diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java deleted file mode 100644 index 2e118b4ac..000000000 --- a/proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2019-2021 Velocity Contributors - * - * 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 . - */ - -package com.velocitypowered.proxy.util.collect; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.google.common.collect.ImmutableSet; -import java.util.Collection; -import java.util.Set; -import org.junit.jupiter.api.Test; - -class CappedSetTest { - - @Test - void basicVerification() { - Collection coll = CappedSet.create(1); - assertTrue(coll.add("coffee"), "did not add single item"); - assertThrows(IllegalStateException.class, () -> coll.add("tea"), - "item was added to collection although it is too full"); - assertEquals(1, coll.size(), "collection grew in size unexpectedly"); - } - - @Test - void testAddAll() { - Set doesFill1 = ImmutableSet.of("coffee", "tea"); - Set doesFill2 = ImmutableSet.of("chocolate"); - Set overfill = ImmutableSet.of("Coke", "Pepsi"); - - Collection coll = CappedSet.create(3); - assertTrue(coll.addAll(doesFill1), "did not add items"); - assertTrue(coll.addAll(doesFill2), "did not add items"); - assertThrows(IllegalStateException.class, () -> coll.addAll(overfill), - "items added to collection although it is too full"); - assertEquals(3, coll.size(), "collection grew in size unexpectedly"); - } - - @Test - void handlesSetBehaviorCorrectly() { - Set doesFill1 = ImmutableSet.of("coffee", "tea"); - Set doesFill2 = ImmutableSet.of("coffee", "chocolate"); - Set overfill = ImmutableSet.of("coffee", "Coke", "Pepsi"); - - Collection coll = CappedSet.create(3); - assertTrue(coll.addAll(doesFill1), "did not add items"); - assertTrue(coll.addAll(doesFill2), "did not add items"); - assertThrows(IllegalStateException.class, () -> coll.addAll(overfill), - "items added to collection although it is too full"); - - assertFalse(coll.addAll(doesFill1), "added items?!?"); - - assertEquals(3, coll.size(), "collection grew in size unexpectedly"); - } -} \ No newline at end of file From 19abb9094e581bc4d21107e839901e5d68090b19 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sun, 20 Aug 2023 02:13:55 -0400 Subject: [PATCH 13/34] fix #1062 --- .../proxy/connection/client/ClientPlaySessionHandler.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index dec1b85df..8993d9b24 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -482,6 +482,14 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } serverBossBars.clear(); + // Tell the server about the proxy's plugin message channels. + ProtocolVersion serverVersion = serverMc.getProtocolVersion(); + final Collection channels = server.getChannelRegistrar() + .getChannelsForProtocol(serverMc.getProtocolVersion()); + if (!channels.isEmpty()) { + serverMc.delayedWrite(constructChannelsPacket(serverVersion, channels)); + } + // If we had plugin messages queued during login/FML handshake, send them now. PluginMessage pm; while ((pm = loginPluginMessages.poll()) != null) { From 768ecdb0c36dd20779b8a9733ade42a60bf7c3c6 Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 10 Oct 2023 13:44:16 +0100 Subject: [PATCH 14/34] 1.20.2 Support (#1088) Co-authored-by: RednedEpic Co-authored-by: Gero --- .gitignore | 2 + .../api/network/ProtocolVersion.java | 3 +- .../proxy/connection/MinecraftConnection.java | 147 ++++++-- .../connection/MinecraftSessionHandler.java | 35 ++ .../backend/BackendPlaySessionHandler.java | 73 ++-- .../backend/ConfigSessionHandler.java | 229 ++++++++++++ .../backend/LoginSessionHandler.java | 40 +- .../backend/TransitionSessionHandler.java | 20 +- .../backend/VelocityServerConnection.java | 43 ++- .../connection/client/AuthSessionHandler.java | 145 +++++--- .../client/ClientConfigSessionHandler.java | 161 ++++++++ .../client/ClientPlaySessionHandler.java | 105 ++++-- .../connection/client/ConnectedPlayer.java | 346 ++++++++++-------- .../client/HandshakeSessionHandler.java | 21 +- .../client/InitialLoginSessionHandler.java | 86 +++-- .../LegacyForgeHandshakeClientPhase.java | 21 +- .../connection/registry/ClientConfigData.java | 128 +++++++ .../proxy/connection/registry/DataTag.java | 95 +++++ .../proxy/network/Connections.java | 1 + .../network/ServerChannelInitializer.java | 4 +- .../proxy/protocol/ProtocolUtils.java | 89 ++++- .../proxy/protocol/StateRegistry.java | 326 ++++++++++++----- .../protocol/netty/MinecraftEncoder.java | 4 + .../netty/PlayPacketQueueHandler.java | 108 ++++++ .../proxy/protocol/packet/ClientSettings.java | 16 +- .../proxy/protocol/packet/JoinGame.java | 121 ++++-- .../protocol/packet/LoginAcknowledged.java | 48 +++ .../proxy/protocol/packet/PingIdentify.java | 49 +++ .../protocol/packet/ResourcePackRequest.java | 29 +- .../protocol/packet/ResourcePackResponse.java | 5 +- .../proxy/protocol/packet/ServerLogin.java | 15 +- .../packet/config/ActiveFeatures.java | 63 ++++ .../packet/config/FinishedUpdate.java | 48 +++ .../protocol/packet/config/RegistrySync.java | 50 +++ .../protocol/packet/config/StartUpdate.java | 48 +++ .../protocol/packet/config/TagsUpdate.java | 82 +++++ .../proxy/server/PingSessionHandler.java | 5 +- .../server/VelocityRegisteredServer.java | 56 ++- 38 files changed, 2272 insertions(+), 595 deletions(-) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/connection/registry/ClientConfigData.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DataTag.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueHandler.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledged.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PingIdentify.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ActiveFeatures.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/FinishedUpdate.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/RegistrySync.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/StartUpdate.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/TagsUpdate.java diff --git a/.gitignore b/.gitignore index f90c93da9..97eb7c9e0 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,8 @@ gradle-app.setting logs/ /velocity.toml /forwarding.secret +forwarding.secret +velocity.toml server-icon.png /bin/ run/ diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java index 110b58767..ef7024efe 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -62,7 +62,8 @@ public enum ProtocolVersion { MINECRAFT_1_19_1(760, "1.19.1", "1.19.2"), MINECRAFT_1_19_3(761, "1.19.3"), MINECRAFT_1_19_4(762, "1.19.4"), - MINECRAFT_1_20(763, "1.20", "1.20.1"); + MINECRAFT_1_20(763, "1.20", "1.20.1"), + MINECRAFT_1_20_2(764, "1.20.2"); private static final int SNAPSHOT_BIT = 30; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java index 9145ff133..bb0c4db72 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -36,6 +36,7 @@ import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler; import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler; import com.velocitypowered.proxy.connection.client.StatusSessionHandler; +import com.velocitypowered.proxy.network.Connections; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.VelocityConnectionEvent; @@ -46,6 +47,7 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftCompressorAndLengthEnco import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; +import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueHandler; import com.velocitypowered.proxy.util.except.QuietDecoderException; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; @@ -60,6 +62,9 @@ import io.netty.util.ReferenceCountUtil; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.security.GeneralSecurityException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -78,7 +83,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { private final Channel channel; private SocketAddress remoteAddress; private StateRegistry state; - private @Nullable MinecraftSessionHandler sessionHandler; + private Map sessionHandlers; + private @Nullable MinecraftSessionHandler activeSessionHandler; private ProtocolVersion protocolVersion; private @Nullable MinecraftConnectionAssociation association; public final VelocityServer server; @@ -96,12 +102,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { this.remoteAddress = channel.remoteAddress(); this.server = server; this.state = StateRegistry.HANDSHAKE; + + this.sessionHandlers = new HashMap<>(); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { - if (sessionHandler != null) { - sessionHandler.connected(); + if (activeSessionHandler != null) { + activeSessionHandler.connected(); } if (association != null && server.getConfiguration().isLogPlayerConnections()) { @@ -111,12 +119,12 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { - if (sessionHandler != null) { - sessionHandler.disconnected(); + if (activeSessionHandler != null) { + activeSessionHandler.disconnected(); } if (association != null && !knownDisconnect - && !(sessionHandler instanceof StatusSessionHandler) + && !(activeSessionHandler instanceof StatusSessionHandler) && server.getConfiguration().isLogPlayerConnections()) { logger.info("{} has disconnected", association); } @@ -125,12 +133,12 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { - if (sessionHandler == null) { + if (activeSessionHandler == null) { // No session handler available, do nothing return; } - if (sessionHandler.beforeHandle()) { + if (activeSessionHandler.beforeHandle()) { return; } @@ -140,15 +148,15 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { if (msg instanceof MinecraftPacket) { MinecraftPacket pkt = (MinecraftPacket) msg; - if (!pkt.handle(sessionHandler)) { - sessionHandler.handleGeneric((MinecraftPacket) msg); + if (!pkt.handle(activeSessionHandler)) { + activeSessionHandler.handleGeneric((MinecraftPacket) msg); } } else if (msg instanceof HAProxyMessage) { HAProxyMessage proxyMessage = (HAProxyMessage) msg; this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(), proxyMessage.sourcePort()); } else if (msg instanceof ByteBuf) { - sessionHandler.handleUnknown((ByteBuf) msg); + activeSessionHandler.handleUnknown((ByteBuf) msg); } } finally { ReferenceCountUtil.release(msg); @@ -157,20 +165,21 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { - if (sessionHandler != null) { - sessionHandler.readCompleted(); + if (activeSessionHandler != null) { + activeSessionHandler.readCompleted(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (ctx.channel().isActive()) { - if (sessionHandler != null) { + if (activeSessionHandler != null) { try { - sessionHandler.exception(cause); + activeSessionHandler.exception(cause); } catch (Exception ex) { logger.error("{}: exception handling exception in {}", - (association != null ? association : channel.remoteAddress()), sessionHandler, cause); + (association != null ? association : channel.remoteAddress()), activeSessionHandler, + cause); } } @@ -178,13 +187,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { if (cause instanceof ReadTimeoutException) { logger.error("{}: read timed out", association); } else { - boolean frontlineHandler = sessionHandler instanceof InitialLoginSessionHandler - || sessionHandler instanceof HandshakeSessionHandler - || sessionHandler instanceof StatusSessionHandler; + boolean frontlineHandler = activeSessionHandler instanceof InitialLoginSessionHandler + || activeSessionHandler instanceof HandshakeSessionHandler + || activeSessionHandler instanceof StatusSessionHandler; boolean isQuietDecoderException = cause instanceof QuietDecoderException; boolean willLog = !isQuietDecoderException && !frontlineHandler; if (willLog) { - logger.error("{}: exception encountered in {}", association, sessionHandler, cause); + logger.error("{}: exception encountered in {}", association, activeSessionHandler, + cause); } else { knownDisconnect = true; } @@ -197,8 +207,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { @Override public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { - if (sessionHandler != null) { - sessionHandler.writabilityChanged(); + if (activeSessionHandler != null) { + activeSessionHandler.writabilityChanged(); } } @@ -323,7 +333,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { } /** - * Determines whether or not the channel should continue reading data automaticaly. + * Determines whether or not the channel should continue reading data automatically. * * @param autoReading whether or not we should read data automatically */ @@ -341,10 +351,12 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { } } + // Ideally only used by the state switch + /** - * Changes the state of the Minecraft connection. + * Sets the new state for the connection. * - * @param state the new state + * @param state the state to use */ public void setState(StateRegistry state) { ensureInEventLoop(); @@ -352,6 +364,25 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { this.state = state; this.channel.pipeline().get(MinecraftEncoder.class).setState(state); this.channel.pipeline().get(MinecraftDecoder.class).setState(state); + + if (state == StateRegistry.CONFIG) { + // Activate the play packet queue + addPlayPacketQueueHandler(); + } else if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE) != null) { + // Remove the queue + this.channel.pipeline().remove(Connections.PLAY_PACKET_QUEUE); + } + } + + /** + * Adds the play packet queue handler. + */ + public void addPlayPacketQueueHandler() { + if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE) == null) { + this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE, + new PlayPacketQueueHandler(this.protocolVersion, + channel.pipeline().get(MinecraftEncoder.class).getDirection())); + } } public ProtocolVersion getProtocolVersion() { @@ -382,32 +413,81 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { } } - public @Nullable MinecraftSessionHandler getSessionHandler() { - return sessionHandler; + public @Nullable MinecraftSessionHandler getActiveSessionHandler() { + return activeSessionHandler; + } + + public @Nullable MinecraftSessionHandler getSessionHandlerForRegistry(StateRegistry registry) { + return this.sessionHandlers.getOrDefault(registry, null); } /** * Sets the session handler for this connection. * + * @param registry the registry of the handler * @param sessionHandler the handler to use */ - public void setSessionHandler(MinecraftSessionHandler sessionHandler) { + public void setActiveSessionHandler(StateRegistry registry, + MinecraftSessionHandler sessionHandler) { + Preconditions.checkNotNull(registry); ensureInEventLoop(); - if (this.sessionHandler != null) { - this.sessionHandler.deactivated(); + if (this.activeSessionHandler != null) { + this.activeSessionHandler.deactivated(); } - this.sessionHandler = sessionHandler; + this.sessionHandlers.put(registry, sessionHandler); + this.activeSessionHandler = sessionHandler; + setState(registry); sessionHandler.activated(); } + /** + * Switches the active session handler to the respective registry one. + * + * @param registry the registry of the handler + * @return true if successful and handler is present + */ + public boolean setActiveSessionHandler(StateRegistry registry) { + Preconditions.checkNotNull(registry); + ensureInEventLoop(); + + MinecraftSessionHandler handler = getSessionHandlerForRegistry(registry); + if (handler != null) { + boolean flag = true; + if (this.activeSessionHandler != null + && (flag = !Objects.equals(handler, this.activeSessionHandler))) { + this.activeSessionHandler.deactivated(); + } + this.activeSessionHandler = handler; + setState(registry); + if (flag) { + handler.activated(); + } + } + return handler != null; + } + + /** + * Adds a secondary session handler for this connection. + * + * @param registry the registry of the handler + * @param sessionHandler the handler to use + */ + public void addSessionHandler(StateRegistry registry, MinecraftSessionHandler sessionHandler) { + Preconditions.checkNotNull(registry); + Preconditions.checkArgument(registry != state, "Handler would overwrite handler"); + ensureInEventLoop(); + + this.sessionHandlers.put(registry, sessionHandler); + } + private void ensureOpen() { Preconditions.checkState(!isClosed(), "Connection is closed."); } /** - * Sets the compression threshold on the connection. You are responsible for sending - * {@link com.velocitypowered.proxy.protocol.packet.SetCompression} beforehand. + * Sets the compression threshold on the connection. You are responsible for sending {@link + * com.velocitypowered.proxy.protocol.packet.SetCompression} beforehand. * * @param threshold the compression threshold to use */ @@ -497,5 +577,4 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { public void setType(ConnectionType connectionType) { this.connectionType = connectionType; } - } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java index ef4c362f4..ea01eb008 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -31,8 +31,10 @@ import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.LegacyHandshake; import com.velocitypowered.proxy.protocol.packet.LegacyPing; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; +import com.velocitypowered.proxy.protocol.packet.PingIdentify; import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; @@ -55,6 +57,11 @@ import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerCommand; import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChat; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerChat; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerCommand; +import com.velocitypowered.proxy.protocol.packet.config.ActiveFeatures; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; +import com.velocitypowered.proxy.protocol.packet.config.RegistrySync; +import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; +import com.velocitypowered.proxy.protocol.packet.config.TagsUpdate; import com.velocitypowered.proxy.protocol.packet.title.LegacyTitlePacket; import com.velocitypowered.proxy.protocol.packet.title.TitleActionbarPacket; import com.velocitypowered.proxy.protocol.packet.title.TitleClearPacket; @@ -279,4 +286,32 @@ public interface MinecraftSessionHandler { default boolean handle(UpsertPlayerInfo packet) { return false; } + + default boolean handle(LoginAcknowledged packet) { + return false; + } + + default boolean handle(ActiveFeatures packet) { + return false; + } + + default boolean handle(FinishedUpdate packet) { + return false; + } + + default boolean handle(RegistrySync packet) { + return false; + } + + default boolean handle(TagsUpdate packet) { + return false; + } + + default boolean handle(StartUpdate packet) { + return false; + } + + default boolean handle(PingIdentify pingIdentify) { + return false; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index ff777f5f9..ffa0c8391 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -39,8 +39,11 @@ import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.packet.AvailableCommands; import com.velocitypowered.proxy.protocol.packet.BossBar; +import com.velocitypowered.proxy.protocol.packet.ClientSettings; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; @@ -51,6 +54,7 @@ import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; import com.velocitypowered.proxy.protocol.packet.ServerData; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; @@ -68,10 +72,10 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { private static final Pattern PLAUSIBLE_SHA1_HASH = Pattern.compile("^[a-z0-9]{40}$"); private static final Logger logger = LogManager.getLogger(BackendPlaySessionHandler.class); - private static final boolean BACKPRESSURE_LOG = Boolean - .getBoolean("velocity.log-server-backpressure"); - private static final int MAXIMUM_PACKETS_TO_FLUSH = Integer - .getInteger("velocity.max-packets-per-flush", 8192); + private static final boolean BACKPRESSURE_LOG = + Boolean.getBoolean("velocity.log-server-backpressure"); + private static final int MAXIMUM_PACKETS_TO_FLUSH = + Integer.getInteger("velocity.max-packets-per-flush", 8192); private final VelocityServer server; private final VelocityServerConnection serverConn; @@ -86,7 +90,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { this.serverConn = serverConn; this.playerConnection = serverConn.getPlayer().getConnection(); - MinecraftSessionHandler psh = playerConnection.getSessionHandler(); + MinecraftSessionHandler psh = playerConnection.getActiveSessionHandler(); if (!(psh instanceof ClientPlaySessionHandler)) { throw new IllegalStateException( "Initializing BackendPlaySessionHandler with no backing client play session handler!"); @@ -119,12 +123,28 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { return false; } + @Override + public boolean handle(StartUpdate packet) { + MinecraftConnection smc = serverConn.ensureConnected(); + smc.setAutoReading(false); + // Even when not auto reading messages are still decoded. Decode them with the correct state + smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.CONFIG); + serverConn.getPlayer().switchToConfigState(); + return true; + } + @Override public boolean handle(KeepAlive packet) { serverConn.getPendingPings().put(packet.getRandomId(), System.currentTimeMillis()); return false; // forwards on } + @Override + public boolean handle(ClientSettings packet) { + serverConn.ensureConnected().write(packet); + return true; + } + @Override public boolean handle(Disconnect packet) { serverConn.disconnect(); @@ -221,20 +241,16 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { } byte[] copy = ByteBufUtil.getBytes(packet.content()); - PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, - copy); - server.getEventManager().fire(event) - .thenAcceptAsync(pme -> { - if (pme.getResult().isAllowed() && !playerConnection.isClosed()) { - PluginMessage copied = new PluginMessage(packet.getChannel(), - Unpooled.wrappedBuffer(copy)); - playerConnection.write(copied); - } - }, playerConnection.eventLoop()) - .exceptionally((ex) -> { - logger.error("Exception while handling plugin message {}", packet, ex); - return null; - }); + PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, copy); + server.getEventManager().fire(event).thenAcceptAsync(pme -> { + if (pme.getResult().isAllowed() && !playerConnection.isClosed()) { + PluginMessage copied = new PluginMessage(packet.getChannel(), Unpooled.wrappedBuffer(copy)); + playerConnection.write(copied); + } + }, playerConnection.eventLoop()).exceptionally((ex) -> { + logger.error("Exception while handling plugin message {}", packet, ex); + return null; + }); return true; } @@ -283,18 +299,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(ServerData packet) { - server.getServerListPingHandler().getInitialPing(this.serverConn.getPlayer()) - .thenComposeAsync( - ping -> server.getEventManager() - .fire(new ProxyPingEvent(this.serverConn.getPlayer(), ping)), - playerConnection.eventLoop() - ) - .thenAcceptAsync(pingEvent -> - this.playerConnection.write( - new ServerData(pingEvent.getPing().getDescriptionComponent(), - pingEvent.getPing().getFavicon().orElse(null), - packet.isSecureChatEnforced()) - ), playerConnection.eventLoop()); + server.getServerListPingHandler().getInitialPing(this.serverConn.getPlayer()).thenComposeAsync( + ping -> server.getEventManager() + .fire(new ProxyPingEvent(this.serverConn.getPlayer(), ping)), + playerConnection.eventLoop()).thenAcceptAsync(pingEvent -> this.playerConnection.write( + new ServerData(pingEvent.getPing().getDescriptionComponent(), + pingEvent.getPing().getFavicon().orElse(null), packet.isSecureChatEnforced())), + playerConnection.eventLoop()); return true; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java new file mode 100644 index 000000000..d84ae49ce --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2019-2023 Velocity Contributors + * + * 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 . + */ + +package com.velocitypowered.proxy.connection.backend; + +import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; +import com.velocitypowered.api.event.player.ServerResourcePackSendEvent; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.client.ClientConfigSessionHandler; +import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.util.ConnectionMessages; +import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; +import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; +import com.velocitypowered.proxy.protocol.packet.Disconnect; +import com.velocitypowered.proxy.protocol.packet.KeepAlive; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; +import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; +import com.velocitypowered.proxy.protocol.packet.config.RegistrySync; +import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; +import com.velocitypowered.proxy.protocol.packet.config.TagsUpdate; +import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; +import net.kyori.adventure.text.Component; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * A special session handler that catches "last minute" disconnects. This version is to accommodate + * 1.20.2+ switching. Yes, some of this is exceptionally stupid. + */ +public class ConfigSessionHandler implements MinecraftSessionHandler { + + private static final Pattern PLAUSIBLE_SHA1_HASH = Pattern.compile("^[a-z0-9]{40}$"); + private static final Logger logger = LogManager.getLogger(ConfigSessionHandler.class); + private final VelocityServer server; + private final VelocityServerConnection serverConn; + private final CompletableFuture resultFuture; + + private ResourcePackInfo resourcePackToApply; + + private State state; + + /** + * Creates the new transition handler. + * + * @param server the Velocity server instance + * @param serverConn the server connection + * @param resultFuture the result future + */ + ConfigSessionHandler(VelocityServer server, VelocityServerConnection serverConn, + CompletableFuture resultFuture) { + this.server = server; + this.serverConn = serverConn; + this.resultFuture = resultFuture; + this.state = State.START; + } + + @Override + public void activated() { + resourcePackToApply = serverConn.getPlayer().getAppliedResourcePack(); + serverConn.getPlayer().clearAppliedResourcePack(); + } + + @Override + public boolean beforeHandle() { + if (!serverConn.isActive()) { + // Obsolete connection + serverConn.disconnect(); + return true; + } + return false; + } + + @Override + public boolean handle(StartUpdate packet) { + serverConn.ensureConnected().write(packet); + return true; + } + + @Override + public boolean handle(TagsUpdate packet) { + serverConn.getPlayer().getConnection().write(packet); + return true; + } + + @Override + public boolean handle(KeepAlive packet) { + serverConn.ensureConnected().write(packet); + return true; + } + + @Override + public boolean handle(ResourcePackRequest packet) { + final MinecraftConnection playerConnection = serverConn.getPlayer().getConnection(); + + ServerResourcePackSendEvent event = + new ServerResourcePackSendEvent(packet.toServerPromptedPack(), this.serverConn); + + server.getEventManager().fire(event).thenAcceptAsync(serverResourcePackSendEvent -> { + if (playerConnection.isClosed()) { + return; + } + if (serverResourcePackSendEvent.getResult().isAllowed()) { + ResourcePackInfo toSend = serverResourcePackSendEvent.getProvidedResourcePack(); + if (toSend != serverResourcePackSendEvent.getReceivedResourcePack()) { + ((VelocityResourcePackInfo) toSend).setOriginalOrigin( + ResourcePackInfo.Origin.DOWNSTREAM_SERVER); + } + + resourcePackToApply = null; + serverConn.getPlayer().queueResourcePack(toSend); + } else if (serverConn.getConnection() != null) { + serverConn.getConnection().write(new ResourcePackResponse(packet.getHash(), + PlayerResourcePackStatusEvent.Status.DECLINED)); + } + }, playerConnection.eventLoop()).exceptionally((ex) -> { + if (serverConn.getConnection() != null) { + serverConn.getConnection().write(new ResourcePackResponse(packet.getHash(), + PlayerResourcePackStatusEvent.Status.DECLINED)); + } + logger.error("Exception while handling resource pack send for {}", playerConnection, ex); + return null; + }); + + return true; + } + + @Override + public boolean handle(FinishedUpdate packet) { + MinecraftConnection smc = serverConn.ensureConnected(); + ClientConfigSessionHandler configHandler = + (ClientConfigSessionHandler) serverConn.getPlayer().getConnection() + .getActiveSessionHandler(); + + smc.setAutoReading(false); + // Even when not auto reading messages are still decoded. Decode them with the correct state + smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.PLAY); + configHandler.handleBackendFinishUpdate(serverConn).thenAcceptAsync((unused) -> { + if (serverConn == serverConn.getPlayer().getConnectedServer()) { + smc.setActiveSessionHandler(StateRegistry.PLAY); + } else { + smc.setActiveSessionHandler(StateRegistry.PLAY, + new TransitionSessionHandler(server, serverConn, resultFuture)); + } + if (serverConn.getPlayer().getAppliedResourcePack() == null && resourcePackToApply != null) { + serverConn.getPlayer().queueResourcePack(resourcePackToApply); + } + smc.setAutoReading(true); + }, smc.eventLoop()); + return true; + } + + @Override + public boolean handle(Disconnect packet) { + serverConn.disconnect(); + resultFuture.complete(ConnectionRequestResults.forDisconnect(packet, serverConn.getServer())); + return true; + } + + @Override + public boolean handle(PluginMessage packet) { + if (PluginMessageUtil.isMcBrand(packet)) { + serverConn.getPlayer().getConnection().write( + PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(), + serverConn.getPlayer().getProtocolVersion())); + } else { + // TODO: Change this so its usable for mod loaders + serverConn.disconnect(); + resultFuture.complete(ConnectionRequestResults.forDisconnect( + Component.translatable("multiplayer.disconnect.missing_tags"), serverConn.getServer())); + } + return true; + } + + @Override + public boolean handle(RegistrySync packet) { + serverConn.getPlayer().getConnection().write(packet.retain()); + return true; + } + + @Override + public void disconnected() { + resultFuture.completeExceptionally( + new IOException("Unexpectedly disconnected from remote server")); + } + + @Override + public void handleGeneric(MinecraftPacket packet) { + serverConn.getPlayer().getConnection().write(packet); + } + + private void switchFailure(Throwable cause) { + logger.error("Unable to switch to new server {} for {}", serverConn.getServerInfo().getName(), + serverConn.getPlayer().getUsername(), cause); + serverConn.getPlayer().disconnect(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); + resultFuture.completeExceptionally(cause); + } + + /** + * Represents the state of the configuration stage. + */ + public static enum State { + START, NEGOTIATING, PLUGIN_MESSAGE_INTERRUPT, RESOURCE_PACK_INTERRUPT, COMPLETE + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java index 90a9a1300..e9801c315 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java @@ -27,6 +27,7 @@ import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.VelocityConstants; +import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; @@ -34,6 +35,7 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.EncryptionRequest; +import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess; @@ -59,8 +61,8 @@ public class LoginSessionHandler implements MinecraftSessionHandler { private static final Logger logger = LogManager.getLogger(LoginSessionHandler.class); - private static final Component MODERN_IP_FORWARDING_FAILURE = Component - .translatable("velocity.error.modern-forwarding-failed"); + private static final Component MODERN_IP_FORWARDING_FAILURE = + Component.translatable("velocity.error.modern-forwarding-failed"); private final VelocityServer server; private final VelocityServerConnection serverConn; @@ -150,10 +152,28 @@ public class LoginSessionHandler implements MinecraftSessionHandler { // Move into the PLAY phase. MinecraftConnection smc = serverConn.ensureConnected(); - smc.setState(StateRegistry.PLAY); + if (smc.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { + smc.setActiveSessionHandler(StateRegistry.PLAY, + new TransitionSessionHandler(server, serverConn, resultFuture)); + } else { + smc.setAutoReading(false); + CompletableFuture switchFuture; + if (serverConn.getPlayer().getConnection() + .getActiveSessionHandler() instanceof ClientPlaySessionHandler) { + switchFuture = ((ClientPlaySessionHandler) serverConn.getPlayer().getConnection() + .getActiveSessionHandler()).doSwitch(); + } else { + switchFuture = CompletableFuture.completedFuture(null); + } + switchFuture.thenAcceptAsync((unused) -> { + smc.write(new LoginAcknowledged()); + // Sync backend + smc.setActiveSessionHandler(StateRegistry.CONFIG, + new ConfigSessionHandler(server, serverConn, resultFuture)); + smc.setAutoReading(true); + }, smc.eventLoop()); + } - // Switch to the transition handler. - smc.setSessionHandler(new TransitionSessionHandler(server, serverConn, resultFuture)); return true; } @@ -165,12 +185,12 @@ public class LoginSessionHandler implements MinecraftSessionHandler { @Override public void disconnected() { if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.LEGACY) { - resultFuture.completeExceptionally( - new QuietRuntimeException("The connection to the remote server was unexpectedly closed.\n" - + "This is usually because the remote server does not have BungeeCord IP forwarding " + resultFuture.completeExceptionally(new QuietRuntimeException( + "The connection to the remote server was unexpectedly closed.\n" + + "This is usually because the remote server " + + "does not have BungeeCord IP forwarding " + "correctly enabled.\nSee https://velocitypowered.com/wiki/users/forwarding/ " - + "for instructions on how to configure player info forwarding correctly.") - ); + + "for instructions on how to configure player info forwarding correctly.")); } else { resultFuture.completeExceptionally( new QuietRuntimeException("The connection to the remote server was unexpectedly closed.") diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java index 5fe6886dc..30d1f4242 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java @@ -32,6 +32,7 @@ import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.JoinGame; import com.velocitypowered.proxy.protocol.packet.KeepAlive; @@ -120,17 +121,21 @@ public class TransitionSessionHandler implements MinecraftSessionHandler { // Change the client to use the ClientPlaySessionHandler if required. ClientPlaySessionHandler playHandler; - if (player.getConnection().getSessionHandler() instanceof ClientPlaySessionHandler) { - playHandler = (ClientPlaySessionHandler) player.getConnection().getSessionHandler(); + if (player.getConnection() + .getActiveSessionHandler() instanceof ClientPlaySessionHandler) { + playHandler = + (ClientPlaySessionHandler) player.getConnection().getActiveSessionHandler(); } else { playHandler = new ClientPlaySessionHandler(server, player); - player.getConnection().setSessionHandler(playHandler); + player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, playHandler); } + assert playHandler != null; playHandler.handleBackendJoinGame(packet, serverConn); // Set the new play session handler for the server. We will have nothing more to do // with this connection once this task finishes up. - smc.setSessionHandler(new BackendPlaySessionHandler(server, serverConn)); + smc.setActiveSessionHandler(StateRegistry.PLAY, + new BackendPlaySessionHandler(server, serverConn)); // Clean up disabling auto-read while the connected event was being processed. smc.setAutoReading(true); @@ -138,12 +143,15 @@ public class TransitionSessionHandler implements MinecraftSessionHandler { // Now set the connected server. serverConn.getPlayer().setConnectedServer(serverConn); + if (player.getClientSettingsPacket() != null) { + serverConn.ensureConnected().write(player.getClientSettingsPacket()); + } + // We're done! :) server.getEventManager().fireAndForget(new ServerPostConnectEvent(player, previousServer)); resultFuture.complete(ConnectionRequestResults.successful(serverConn.getServer())); - }, smc.eventLoop()) - .exceptionally(exc -> { + }, smc.eventLoop()).exceptionally(exc -> { logger.error("Unable to switch to new server {} for {}", serverConn.getServerInfo().getName(), player.getUsername(), exc); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java index a2300b978..9a4b7d72d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java @@ -34,6 +34,7 @@ import com.velocitypowered.proxy.config.PlayerInfoForwarding; import com.velocitypowered.proxy.connection.ConnectionTypes; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.protocol.StateRegistry; @@ -91,8 +92,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, /** * Connects to the server. * - * @return a {@link com.velocitypowered.api.proxy.ConnectionRequestBuilder.Result} representing - * whether or not the connect succeeded + * @return a {@link com.velocitypowered.api.proxy.ConnectionRequestBuilder.Result} + * representing whether the connection succeeded */ public CompletableFuture connect() { CompletableFuture result = new CompletableFuture<>(); @@ -108,15 +109,21 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, future.channel().pipeline().addLast(HANDLER, connection); // Kick off the connection process - connection.setSessionHandler( - new LoginSessionHandler(server, VelocityServerConnection.this, result)); + if (!connection.setActiveSessionHandler(StateRegistry.HANDSHAKE)) { + MinecraftSessionHandler handler = + new LoginSessionHandler(server, VelocityServerConnection.this, result); + connection.setActiveSessionHandler(StateRegistry.HANDSHAKE, handler); + connection.addSessionHandler(StateRegistry.LOGIN, handler); + } - // Set the connection phase, which may, for future forge (or whatever), be determined + // Set the connection phase, which may, for future forge (or whatever), be + // determined // at this point already connectionPhase = connection.getType().getInitialBackendPhase(); startHandshake(); } else { - // Complete the result immediately. ConnectedPlayer will reset the in-flight connection. + // Complete the result immediately. ConnectedPlayer will reset the in-flight + // connection. result.completeExceptionally(future.cause()); } }); @@ -137,10 +144,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, // BungeeCord IP forwarding is simply a special injection after the "address" in the handshake, // separated by \0 (the null byte). In order, you send the original host, the player's IP, their // UUID (undashed), and if you are in online-mode, their login properties (from Mojang). - StringBuilder data = new StringBuilder() - .append(proxyPlayer.getVirtualHost() - .orElseGet(() -> registeredServer.getServerInfo().getAddress()) - .getHostString()) + StringBuilder data = new StringBuilder().append(proxyPlayer.getVirtualHost().orElseGet(() -> + registeredServer.getServerInfo().getAddress()).getHostString()) .append('\0') .append(getPlayerRemoteAddressAsString()) .append('\0') @@ -157,12 +162,10 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, private String createBungeeGuardForwardingAddress(byte[] forwardingSecret) { // Append forwarding secret as a BungeeGuard token. - Property property = new Property("bungeeguard-token", - new String(forwardingSecret, StandardCharsets.UTF_8), ""); - return createLegacyForwardingAddress(properties -> ImmutableList.builder() - .addAll(properties) - .add(property) - .build()); + Property property = + new Property("bungeeguard-token", new String(forwardingSecret, StandardCharsets.UTF_8), ""); + return createLegacyForwardingAddress( + properties -> ImmutableList.builder().addAll(properties).add(property).build()); } private void startHandshake() { @@ -171,9 +174,9 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, // Initiate the handshake. ProtocolVersion protocolVersion = proxyPlayer.getConnection().getProtocolVersion(); - String playerVhost = proxyPlayer.getVirtualHost() - .orElseGet(() -> registeredServer.getServerInfo().getAddress()) - .getHostString(); + String playerVhost = + proxyPlayer.getVirtualHost().orElseGet(() -> registeredServer.getServerInfo().getAddress()) + .getHostString(); Handshake handshake = new Handshake(); handshake.setNextStatus(StateRegistry.LOGIN_ID); @@ -193,7 +196,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, mc.delayedWrite(handshake); mc.setProtocolVersion(protocolVersion); - mc.setState(StateRegistry.LOGIN); + mc.setActiveSessionHandler(StateRegistry.LOGIN); if (proxyPlayer.getIdentifiedKey() == null && proxyPlayer.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19_3) >= 0) { mc.delayedWrite(new ServerLogin(proxyPlayer.getUsername(), proxyPlayer.getUniqueId())); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java index f68d3bb59..603cf686a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java @@ -26,6 +26,7 @@ import com.velocitypowered.api.event.connection.PostLoginEvent; import com.velocitypowered.api.event.permission.PermissionsSetupEvent; import com.velocitypowered.api.event.player.GameProfileRequestEvent; import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.permission.PermissionFunction; import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.proxy.server.RegisteredServer; @@ -38,6 +39,7 @@ import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl; import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess; import com.velocitypowered.proxy.protocol.packet.SetCompression; import io.netty.buffer.ByteBuf; @@ -64,6 +66,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler { private GameProfile profile; private @MonotonicNonNull ConnectedPlayer connectedPlayer; private final boolean onlineMode; + private State loginState = State.START; // 1.20.2+ AuthSessionHandler(VelocityServer server, LoginInboundConnection inbound, GameProfile profile, boolean onlineMode) { @@ -95,8 +98,9 @@ public class AuthSessionHandler implements MinecraftSessionHandler { inbound.getIdentifiedKey()); this.connectedPlayer = player; if (!server.canRegisterConnection(player)) { - player.disconnect0(Component.translatable("velocity.error.already-connected-proxy", - NamedTextColor.RED), true); + player.disconnect0( + Component.translatable("velocity.error.already-connected-proxy", NamedTextColor.RED), + true); return CompletableFuture.completedFuture(null); } @@ -109,16 +113,14 @@ public class AuthSessionHandler implements MinecraftSessionHandler { // wait for permissions to load, then set the players permission function final PermissionFunction function = event.createFunction(player); if (function == null) { - logger.error( - "A plugin permission provider {} provided an invalid permission function" - + " for player {}. This is a bug in the plugin, not in Velocity. Falling" - + " back to the default permission function.", - event.getProvider().getClass().getName(), - player.getUsername()); + logger.error("A plugin permission provider {} provided an invalid permission " + + "function for player {}. This is a bug in the plugin, not in " + + "Velocity. Falling back to the default permission function.", + event.getProvider().getClass().getName(), player.getUsername()); } else { player.setPermissionFunction(function); } - completeLoginProtocolPhaseAndInitialize(player); + startLoginCompletion(player); } }, mcConnection.eventLoop()); }, mcConnection.eventLoop()).exceptionally((ex) -> { @@ -127,7 +129,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler { }); } - private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) { + private void startLoginCompletion(ConnectedPlayer player) { int threshold = server.getConfiguration().getCompressionThreshold(); if (threshold >= 0 && mcConnection.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) { mcConnection.write(new SetCompression(threshold)); @@ -165,64 +167,87 @@ public class AuthSessionHandler implements MinecraftSessionHandler { } } - ServerLoginSuccess success = new ServerLoginSuccess(); - success.setUsername(player.getUsername()); - success.setProperties(player.getGameProfileProperties()); - success.setUuid(playerUniqueId); - mcConnection.write(success); + completeLoginProtocolPhaseAndInitialize(player); + } + @Override + public boolean handle(LoginAcknowledged packet) { + if (loginState != State.SUCCESS_SENT) { + inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_data")); + } else { + loginState = State.ACKNOWLEDGED; + mcConnection.setActiveSessionHandler(StateRegistry.CONFIG, + new ClientConfigSessionHandler(server, connectedPlayer)); + + server.getEventManager().fire(new PostLoginEvent(connectedPlayer)) + .thenCompose((ignored) -> connectToInitialServer(connectedPlayer)).exceptionally((ex) -> { + logger.error("Exception while connecting {} to initial server", connectedPlayer, ex); + return null; + }); + } + return true; + } + + private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) { mcConnection.setAssociation(player); - mcConnection.setState(StateRegistry.PLAY); - server.getEventManager().fire(new LoginEvent(player)) - .thenAcceptAsync(event -> { - if (mcConnection.isClosed()) { - // The player was disconnected - server.getEventManager().fireAndForget(new DisconnectEvent(player, - DisconnectEvent.LoginStatus.CANCELLED_BY_USER_BEFORE_COMPLETE)); - return; - } + server.getEventManager().fire(new LoginEvent(player)).thenAcceptAsync(event -> { + if (mcConnection.isClosed()) { + // The player was disconnected + server.getEventManager().fireAndForget(new DisconnectEvent(player, + DisconnectEvent.LoginStatus.CANCELLED_BY_USER_BEFORE_COMPLETE)); + return; + } - Optional reason = event.getResult().getReasonComponent(); - if (reason.isPresent()) { - player.disconnect0(reason.get(), true); - } else { - if (!server.registerConnection(player)) { - player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"), - true); - return; - } + Optional reason = event.getResult().getReasonComponent(); + if (reason.isPresent()) { + player.disconnect0(reason.get(), true); + } else { + if (!server.registerConnection(player)) { + player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"), + true); + return; + } - mcConnection.setSessionHandler(new InitialConnectSessionHandler(player, server)); - server.getEventManager().fire(new PostLoginEvent(player)) - .thenCompose((ignored) -> connectToInitialServer(player)) - .exceptionally((ex) -> { - logger.error("Exception while connecting {} to initial server", player, ex); - return null; - }); - } - }, mcConnection.eventLoop()) - .exceptionally((ex) -> { - logger.error("Exception while completing login initialisation phase for {}", player, ex); - return null; - }); + ServerLoginSuccess success = new ServerLoginSuccess(); + success.setUsername(player.getUsername()); + success.setProperties(player.getGameProfileProperties()); + success.setUuid(player.getUniqueId()); + mcConnection.write(success); + + loginState = State.SUCCESS_SENT; + if (inbound.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { + loginState = State.ACKNOWLEDGED; + mcConnection.setActiveSessionHandler(StateRegistry.PLAY, + new InitialConnectSessionHandler(player, server)); + server.getEventManager().fire(new PostLoginEvent(player)) + .thenCompose((ignored) -> connectToInitialServer(player)).exceptionally((ex) -> { + logger.error("Exception while connecting {} to initial server", player, ex); + return null; + }); + } + } + }, mcConnection.eventLoop()).exceptionally((ex) -> { + logger.error("Exception while completing login initialisation phase for {}", player, ex); + return null; + }); } private CompletableFuture connectToInitialServer(ConnectedPlayer player) { Optional initialFromConfig = player.getNextServerToTry(); - PlayerChooseInitialServerEvent event = new PlayerChooseInitialServerEvent(player, - initialFromConfig.orElse(null)); + PlayerChooseInitialServerEvent event = + new PlayerChooseInitialServerEvent(player, initialFromConfig.orElse(null)); - return server.getEventManager().fire(event) - .thenRunAsync(() -> { - Optional toTry = event.getInitialServer(); - if (!toTry.isPresent()) { - player.disconnect0(Component.translatable("velocity.error.no-available-servers", - NamedTextColor.RED), true); - return; - } - player.createConnectionRequest(toTry.get()).fireAndForget(); - }, mcConnection.eventLoop()); + return server.getEventManager().fire(event).thenRunAsync(() -> { + Optional toTry = event.getInitialServer(); + if (!toTry.isPresent()) { + player.disconnect0( + Component.translatable("velocity.error.no-available-servers", NamedTextColor.RED), + true); + return; + } + player.createConnectionRequest(toTry.get()).fireAndForget(); + }, mcConnection.eventLoop()); } @Override @@ -237,4 +262,8 @@ public class AuthSessionHandler implements MinecraftSessionHandler { } this.inbound.cleanup(); } + + static enum State { + START, SUCCESS_SENT, ACKNOWLEDGED + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java new file mode 100644 index 000000000..83d9fe91b --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * 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 . + */ + +package com.velocitypowered.proxy.connection.client; + +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.packet.ClientSettings; +import com.velocitypowered.proxy.protocol.packet.KeepAlive; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; +import io.netty.buffer.ByteBuf; +import java.util.concurrent.CompletableFuture; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Handles the client config stage. + */ +public class ClientConfigSessionHandler implements MinecraftSessionHandler { + + private static final Logger logger = LogManager.getLogger(ClientConfigSessionHandler.class); + private final VelocityServer server; + private final ConnectedPlayer player; + + private CompletableFuture configSwitchFuture; + + /** + * Constructs a client config session handler. + * + * @param server the Velocity server instance + * @param player the player + */ + public ClientConfigSessionHandler(VelocityServer server, ConnectedPlayer player) { + this.server = server; + this.player = player; + } + + @Override + public void activated() { + configSwitchFuture = new CompletableFuture<>(); + } + + @Override + public void deactivated() { + } + + @Override + public boolean handle(KeepAlive packet) { + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection != null) { + Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); + if (sentTime != null) { + MinecraftConnection smc = serverConnection.getConnection(); + if (smc != null) { + player.setPing(System.currentTimeMillis() - sentTime); + smc.write(packet); + } + } + } + return true; + } + + @Override + public boolean handle(ClientSettings packet) { + player.setClientSettingsPacket(packet); + return true; + } + + @Override + public boolean handle(ResourcePackResponse packet) { + if (player.getConnectionInFlight() != null) { + player.getConnectionInFlight().ensureConnected().write(packet); + } + return player.onResourcePackResponse(packet.getStatus()); + } + + @Override + public boolean handle(FinishedUpdate packet) { + player.getConnection() + .setActiveSessionHandler(StateRegistry.PLAY, new ClientPlaySessionHandler(server, player)); + + configSwitchFuture.complete(null); + return true; + } + + @Override + public void handleGeneric(MinecraftPacket packet) { + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection == null) { + // No server connection yet, probably transitioning. + return; + } + + MinecraftConnection smc = serverConnection.getConnection(); + if (smc != null && serverConnection.getPhase().consideredComplete()) { + if (packet instanceof PluginMessage) { + ((PluginMessage) packet).retain(); + } + smc.write(packet); + } + } + + @Override + public void handleUnknown(ByteBuf buf) { + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection == null) { + // No server connection yet, probably transitioning. + return; + } + + MinecraftConnection smc = serverConnection.getConnection(); + if (smc != null && !smc.isClosed() && serverConnection.getPhase().consideredComplete()) { + smc.write(buf.retain()); + } + } + + @Override + public void disconnected() { + player.teardown(); + } + + @Override + public void exception(Throwable throwable) { + player.disconnect( + Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED)); + } + + /** + * Handles the backend finishing the config stage. + * + * @param serverConn the server connection + * @return a future that completes when the config stage is finished + */ + public CompletableFuture handleBackendFinishUpdate(VelocityServerConnection serverConn) { + player.getConnection().write(new FinishedUpdate()); + serverConn.ensureConnected().write(new FinishedUpdate()); + return configSwitchFuture; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 8993d9b24..d2da3aa8e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -17,9 +17,6 @@ package com.velocitypowered.proxy.connection.client; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.constructChannelsPacket; import com.google.common.collect.ImmutableList; @@ -67,6 +64,7 @@ import com.velocitypowered.proxy.protocol.packet.chat.session.SessionChatHandler import com.velocitypowered.proxy.protocol.packet.chat.session.SessionCommandHandler; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerChat; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerCommand; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.util.CharacterUtil; @@ -80,6 +78,7 @@ import java.util.Collection; import java.util.List; import java.util.Queue; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -105,6 +104,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { private final CommandHandler commandHandler; private final ChatTimeKeeper timeKeeper = new ChatTimeKeeper(); + private CompletableFuture configSwitchFuture; + /** * Constructs a client play session handler. * @@ -151,8 +152,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public void activated() { - Collection channels = server.getChannelRegistrar() - .getChannelsForProtocol(player.getProtocolVersion()); + configSwitchFuture = new CompletableFuture<>(); + Collection channels = + server.getChannelRegistrar().getChannelsForProtocol(player.getProtocolVersion()); if (!channels.isEmpty()) { PluginMessage register = constructChannelsPacket(player.getProtocolVersion(), channels); player.getConnection().write(register); @@ -185,7 +187,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(ClientSettings packet) { player.setPlayerSettings(packet); - return false; // will forward onto the server + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection == null) { + // No server connection yet, probably transitioning. + return true; + } + player.getConnectedServer().ensureConnected().write(packet); + return true; // will forward onto the server } @Override @@ -288,10 +296,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { MinecraftConnection backendConn = serverConn != null ? serverConn.getConnection() : null; if (serverConn != null && backendConn != null) { if (backendConn.getState() != StateRegistry.PLAY) { - logger.warn( - "A plugin message was received while the backend server was not " - + "ready. Channel: {}. Packet discarded.", - packet.getChannel()); + logger.warn("A plugin message was received while the backend server was not " + + "ready. Channel: {}. Packet discarded.", packet.getChannel()); } else if (PluginMessageUtil.isRegister(packet)) { List channels = PluginMessageUtil.getChannels(packet); List channelIdentifiers = new ArrayList<>(); @@ -377,6 +383,26 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return player.onResourcePackResponse(packet.getStatus()); } + @Override + public boolean handle(FinishedUpdate packet) { + // Complete client switch + player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG); + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection != null) { + MinecraftConnection smc = serverConnection.ensureConnected(); + CompletableFuture.runAsync(() -> { + smc.write(packet); + smc.setActiveSessionHandler(StateRegistry.CONFIG); + smc.setAutoReading(true); + }, smc.eventLoop()).exceptionally((ex) -> { + logger.error("Error forwarding config state acknowledgement to server:", ex); + return null; + }); + } + configSwitchFuture.complete(null); + return true; + } + @Override public void handleGeneric(MinecraftPacket packet) { VelocityServerConnection serverConnection = player.getConnectedServer(); @@ -440,6 +466,33 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } } + /** + * Handles switching stages for swapping between servers. + * + * @return a future that completes when the switch is complete + */ + public CompletableFuture doSwitch() { + VelocityServerConnection existingConnection = player.getConnectedServer(); + + if (existingConnection != null) { + // Shut down the existing server connection. + player.setConnectedServer(null); + existingConnection.disconnect(); + + // Send keep alive to try to avoid timeouts + player.sendKeepAlive(); + + // Reset Tablist header and footer to prevent desync + player.clearHeaderAndFooter(); + } + + spawned = false; + + player.switchToConfigState(); + + return configSwitchFuture; + } + /** * Handles the {@code JoinGame} packet. This function is responsible for handling the client-side * switching servers in Velocity. @@ -482,14 +535,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } serverBossBars.clear(); - // Tell the server about the proxy's plugin message channels. - ProtocolVersion serverVersion = serverMc.getProtocolVersion(); - final Collection channels = server.getChannelRegistrar() - .getChannelsForProtocol(serverMc.getProtocolVersion()); - if (!channels.isEmpty()) { - serverMc.delayedWrite(constructChannelsPacket(serverVersion, channels)); - } - // If we had plugin messages queued during login/FML handshake, send them now. PluginMessage pm; while ((pm = loginPluginMessages.poll()) != null) { @@ -497,7 +542,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } // Clear any title from the previous server. - if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) { + if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { player.getConnection().delayedWrite( GenericTitlePacket.constructTitlePacket(GenericTitlePacket.ActionType.RESET, player.getProtocolVersion())); @@ -520,7 +565,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // improving compatibility with mods. final Respawn respawn = Respawn.fromJoinGame(joinGame); - if (player.getProtocolVersion().compareTo(MINECRAFT_1_16) < 0) { + if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_16) < 0) { // Before Minecraft 1.16, we could not switch to the same dimension without sending an // additional respawn. On older versions of Minecraft this forces the client to perform // garbage collection which adds additional latency. @@ -562,7 +607,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { String commandLabel = command.substring(0, commandEndPosition); if (!server.getCommandManager().hasCommand(commandLabel)) { - if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) { + if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { // Outstanding tab completes are recorded for use with 1.12 clients and below to provide // additional tab completion support. outstandingTabComplete = packet; @@ -604,7 +649,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } private boolean handleRegularTabComplete(TabCompleteRequest packet) { - if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) { + if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { // Outstanding tab completes are recorded for use with 1.12 clients and below to provide // additional tab completion support. outstandingTabComplete = packet; @@ -635,7 +680,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { String command = request.getCommand().substring(1); server.getCommandManager().offerBrigadierSuggestions(player, command) .thenAcceptAsync(offers -> { - boolean legacy = player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0; + boolean legacy = + player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0; try { for (Suggestion suggestion : offers.getList()) { String offer = suggestion.getText(); @@ -659,9 +705,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } }, player.getConnection().eventLoop()).exceptionally((ex) -> { logger.error( - "Exception while finishing command tab completion, with request {} and response {}", - request, - response, ex); + "Exception while finishing command tab completion," + + " with request {} and response {}", + request, response, ex); return null; }); } @@ -680,9 +726,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { player.getConnection().write(response); }, player.getConnection().eventLoop()).exceptionally((ex) -> { logger.error( - "Exception while finishing regular tab completion, with request {} and response{}", - request, - response, ex); + "Exception while finishing regular tab completion," + + " with request {} and response{}", + request, response, ex); return null; }); } @@ -702,5 +748,4 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } } } - } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 90464c9c8..c922e933a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -59,6 +59,7 @@ import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.connection.util.VelocityInboundConnection; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.packet.ClientSettings; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; @@ -69,6 +70,7 @@ import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue; import com.velocitypowered.proxy.protocol.packet.chat.ChatType; import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory; import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChat; +import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.server.VelocityRegisteredServer; import com.velocitypowered.proxy.tablist.InternalTabList; @@ -123,12 +125,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private static final int MAX_PLUGIN_CHANNELS = 1024; private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE = - PlainTextComponentSerializer.builder() - .flattener(ComponentFlattener.basic().toBuilder() - .mapper(KeybindComponent.class, c -> "") - .mapper(TranslatableComponent.class, TranslatableComponent::key) - .build()) - .build(); + PlainTextComponentSerializer.builder().flattener( + ComponentFlattener.basic().toBuilder().mapper(KeybindComponent.class, c -> "") + .mapper(TranslatableComponent.class, TranslatableComponent::key).build()).build(); static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED; private static final Logger logger = LogManager.getLogger(ConnectedPlayer.class); @@ -159,17 +158,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private final Queue outstandingResourcePacks = new ArrayDeque<>(); private @Nullable ResourcePackInfo pendingResourcePack; private @Nullable ResourcePackInfo appliedResourcePack; - private final @NotNull Pointers pointers = Player.super.pointers().toBuilder() - .withDynamic(Identity.UUID, this::getUniqueId) - .withDynamic(Identity.NAME, this::getUsername) - .withDynamic(Identity.DISPLAY_NAME, () -> Component.text(this.getUsername())) - .withDynamic(Identity.LOCALE, this::getEffectiveLocale) - .withStatic(PermissionChecker.POINTER, getPermissionChecker()) - .withStatic(FacetPointers.TYPE, Type.PLAYER) - .build(); + private final @NotNull Pointers pointers = + Player.super.pointers().toBuilder().withDynamic(Identity.UUID, this::getUniqueId) + .withDynamic(Identity.NAME, this::getUsername) + .withDynamic(Identity.DISPLAY_NAME, () -> Component.text(this.getUsername())) + .withDynamic(Identity.LOCALE, this::getEffectiveLocale) + .withStatic(PermissionChecker.POINTER, getPermissionChecker()) + .withStatic(FacetPointers.TYPE, Type.PLAYER).build(); private @Nullable String clientBrand; private @Nullable Locale effectiveLocale; private @Nullable IdentifiedKey playerKey; + private @Nullable ClientSettings clientSettingsPacket; private final ChatQueue chatQueue; private final ChatBuilderFactory chatBuilderFactory; @@ -278,11 +277,19 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, return settings == null ? ClientSettingsWrapper.DEFAULT : this.settings; } + public ClientSettings getClientSettingsPacket() { + return clientSettingsPacket; + } + @Override public boolean hasSentPlayerSettings() { return settings != null; } + public void setClientSettingsPacket(ClientSettings clientSettingsPacket) { + this.clientSettingsPacket = clientSettingsPacket; + } + void setPlayerSettings(ClientSettings settings) { ClientSettingsWrapper cs = new ClientSettingsWrapper(settings); this.settings = cs; @@ -674,8 +681,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, ServerKickResult result; if (kickedFromCurrent) { Optional next = getNextServerToTry(rs); - result = next.map(RedirectPlayer::create) - .orElseGet(() -> DisconnectPlayer.create(friendlyReason)); + result = + next.map(RedirectPlayer::create).orElseGet(() -> DisconnectPlayer.create(friendlyReason)); } else { // If we were kicked by going to another server, the connection should not be in flight if (connectionInFlight != null && connectionInFlight.getServer().equals(rs)) { @@ -689,86 +696,83 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } private void handleKickEvent(KickedFromServerEvent originalEvent, Component friendlyReason, - boolean kickedFromCurrent) { - server.getEventManager().fire(originalEvent) - .thenAcceptAsync(event -> { - // There can't be any connection in flight now. - connectionInFlight = null; + boolean kickedFromCurrent) { + server.getEventManager().fire(originalEvent).thenAcceptAsync(event -> { + // There can't be any connection in flight now. + connectionInFlight = null; - // Make sure we clear the current connected server as the connection is invalid. - VelocityServerConnection previousConnection = connectedServer; - if (kickedFromCurrent) { - connectedServer = null; - } + // Make sure we clear the current connected server as the connection is invalid. + VelocityServerConnection previousConnection = connectedServer; + if (kickedFromCurrent) { + connectedServer = null; + } - if (!isActive()) { - // If the connection is no longer active, it makes no sense to try and recover it. - return; - } + if (!isActive()) { + // If the connection is no longer active, it makes no sense to try and recover it. + return; + } - if (event.getResult() instanceof DisconnectPlayer) { - DisconnectPlayer res = (DisconnectPlayer) event.getResult(); - disconnect(res.getReasonComponent()); - } else if (event.getResult() instanceof RedirectPlayer) { - RedirectPlayer res = (RedirectPlayer) event.getResult(); - createConnectionRequest(res.getServer(), previousConnection) - .connect() - .whenCompleteAsync((status, throwable) -> { - if (throwable != null) { - handleConnectionException(status != null ? status.getAttemptedConnection() - : res.getServer(), throwable, true); - return; + if (event.getResult() instanceof DisconnectPlayer) { + DisconnectPlayer res = (DisconnectPlayer) event.getResult(); + disconnect(res.getReasonComponent()); + } else if (event.getResult() instanceof RedirectPlayer) { + RedirectPlayer res = (RedirectPlayer) event.getResult(); + createConnectionRequest(res.getServer(), previousConnection).connect() + .whenCompleteAsync((status, throwable) -> { + if (throwable != null) { + handleConnectionException( + status != null ? status.getAttemptedConnection() : res.getServer(), throwable, + true); + return; + } + + switch (status.getStatus()) { + // Impossible/nonsensical cases + case ALREADY_CONNECTED: + logger.error("{}: already connected to {}", this, + status.getAttemptedConnection().getServerInfo().getName()); + break; + case CONNECTION_IN_PROGRESS: + // Fatal case + case CONNECTION_CANCELLED: + Component fallbackMsg = res.getMessageComponent(); + if (fallbackMsg == null) { + fallbackMsg = friendlyReason; } - - switch (status.getStatus()) { - // Impossible/nonsensical cases - case ALREADY_CONNECTED: - logger.error("{}: already connected to {}", - this, - status.getAttemptedConnection().getServerInfo().getName() - ); - break; - case CONNECTION_IN_PROGRESS: - // Fatal case - case CONNECTION_CANCELLED: - Component fallbackMsg = res.getMessageComponent(); - if (fallbackMsg == null) { - fallbackMsg = friendlyReason; - } - disconnect(status.getReasonComponent().orElse(fallbackMsg)); - break; - case SERVER_DISCONNECTED: - Component reason = status.getReasonComponent() - .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); - handleConnectionException(res.getServer(), Disconnect.create(reason, - getProtocolVersion()), ((Impl) status).isSafe()); - break; - case SUCCESS: - Component requestedMessage = res.getMessageComponent(); - if (requestedMessage == null) { - requestedMessage = friendlyReason; - } - if (requestedMessage != Component.empty()) { - sendMessage(requestedMessage); - } - break; - default: - // The only remaining value is successful (no need to do anything!) - break; + disconnect(status.getReasonComponent().orElse(fallbackMsg)); + break; + case SERVER_DISCONNECTED: + Component reason = status.getReasonComponent() + .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); + handleConnectionException(res.getServer(), + Disconnect.create(reason, getProtocolVersion()), ((Impl) status).isSafe()); + break; + case SUCCESS: + Component requestedMessage = res.getMessageComponent(); + if (requestedMessage == null) { + requestedMessage = friendlyReason; } - }, connection.eventLoop()); - } else if (event.getResult() instanceof Notify) { - Notify res = (Notify) event.getResult(); - if (event.kickedDuringServerConnect() && previousConnection != null) { - sendMessage(Identity.nil(), res.getMessageComponent()); - } else { - disconnect(res.getMessageComponent()); - } - } else { - // In case someone gets creative, assume we want to disconnect the player. - disconnect(friendlyReason); - } - }, connection.eventLoop()); + if (requestedMessage != Component.empty()) { + sendMessage(requestedMessage); + } + break; + default: + // The only remaining value is successful (no need to do anything!) + break; + } + }, connection.eventLoop()); + } else if (event.getResult() instanceof Notify) { + Notify res = (Notify) event.getResult(); + if (event.kickedDuringServerConnect() && previousConnection != null) { + sendMessage(Identity.nil(), res.getMessageComponent()); + } else { + disconnect(res.getMessageComponent()); + } + } else { + // In case someone gets creative, assume we want to disconnect the player. + disconnect(friendlyReason); + } + }, connection.eventLoop()); } /** @@ -1021,6 +1025,13 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, return pendingResourcePack; } + /** + * Clears the applied resource pack field. + */ + public void clearAppliedResourcePack() { + appliedResourcePack = null; + } + /** * Processes a client response to a sent resource-pack. */ @@ -1068,18 +1079,42 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, && queued.getOriginalOrigin() != ResourcePackInfo.Origin.DOWNSTREAM_SERVER; } + /** + * Gives an indication about the previous resource pack responses. + */ + public @Nullable Boolean getPreviousResourceResponse() { + return previousResourceResponse; + } + /** * Sends a {@link KeepAlive} packet to the player with a random ID. The response will be ignored * by Velocity as it will not match the ID last sent by the server. */ public void sendKeepAlive() { - if (connection.getState() == StateRegistry.PLAY) { + if (connection.getState() == StateRegistry.PLAY + || connection.getState() == StateRegistry.CONFIG) { KeepAlive keepAlive = new KeepAlive(); keepAlive.setRandomId(ThreadLocalRandom.current().nextLong()); connection.write(keepAlive); } } + /** + * Switches the connection to the client into config state. + */ + public void switchToConfigState() { + CompletableFuture.runAsync(() -> { + connection.write(new StartUpdate()); + connection.getChannel().pipeline() + .get(MinecraftEncoder.class).setState(StateRegistry.CONFIG); + // Make sure we don't send any play packets to the player after update start + connection.addPlayPacketQueueHandler(); + }, connection.eventLoop()).exceptionally((ex) -> { + logger.error("Error switching player connection to config state:", ex); + return null; + }); + } + /** * Gets the current "phase" of the connection, mostly used for tracking modded negotiation for * legacy forge servers and provides methods for performing phase specific actions. @@ -1147,37 +1182,34 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } private CompletableFuture internalConnect() { - return this.getInitialStatus() - .thenCompose(initialCheck -> { - if (initialCheck.isPresent()) { - return completedFuture(plainResult(initialCheck.get(), toConnect)); - } + return this.getInitialStatus().thenCompose(initialCheck -> { + if (initialCheck.isPresent()) { + return completedFuture(plainResult(initialCheck.get(), toConnect)); + } - ServerPreConnectEvent event = new ServerPreConnectEvent(ConnectedPlayer.this, - toConnect, previousServer); - return server.getEventManager().fire(event) - .thenComposeAsync(newEvent -> { - Optional newDest = newEvent.getResult().getServer(); - if (!newDest.isPresent()) { - return completedFuture( - plainResult(ConnectionRequestBuilder.Status.CONNECTION_CANCELLED, toConnect) - ); - } + ServerPreConnectEvent event = + new ServerPreConnectEvent(ConnectedPlayer.this, toConnect, previousServer); + return server.getEventManager().fire(event).thenComposeAsync(newEvent -> { + Optional newDest = newEvent.getResult().getServer(); + if (!newDest.isPresent()) { + return completedFuture( + plainResult(ConnectionRequestBuilder.Status.CONNECTION_CANCELLED, toConnect)); + } - RegisteredServer realDestination = newDest.get(); - Optional check = checkServer(realDestination); - if (check.isPresent()) { - return completedFuture(plainResult(check.get(), realDestination)); - } + RegisteredServer realDestination = newDest.get(); + Optional check = checkServer(realDestination); + if (check.isPresent()) { + return completedFuture(plainResult(check.get(), realDestination)); + } - VelocityRegisteredServer vrs = (VelocityRegisteredServer) realDestination; - VelocityServerConnection con = new VelocityServerConnection(vrs, - previousServer, ConnectedPlayer.this, server); - connectionInFlight = con; - return con.connect().whenCompleteAsync( - (result, exception) -> this.resetIfInFlightIs(con), connection.eventLoop()); - }, connection.eventLoop()); - }); + VelocityRegisteredServer vrs = (VelocityRegisteredServer) realDestination; + VelocityServerConnection con = + new VelocityServerConnection(vrs, previousServer, ConnectedPlayer.this, server); + connectionInFlight = con; + return con.connect().whenCompleteAsync((result, exception) -> this.resetIfInFlightIs(con), + connection.eventLoop()); + }, connection.eventLoop()); + }); } private void resetIfInFlightIs(VelocityServerConnection establishedConnection) { @@ -1188,50 +1220,46 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, @Override public CompletableFuture connect() { - return this.internalConnect() - .whenCompleteAsync((status, throwable) -> { - if (status != null && !status.isSuccessful()) { - if (!status.isSafe()) { - handleConnectionException(status.getAttemptedConnection(), throwable, false); - } - } - }, connection.eventLoop()) - .thenApply(x -> x); + return this.internalConnect().whenCompleteAsync((status, throwable) -> { + if (status != null && !status.isSuccessful()) { + if (!status.isSafe()) { + handleConnectionException(status.getAttemptedConnection(), throwable, false); + } + } + }, connection.eventLoop()).thenApply(x -> x); } @Override public CompletableFuture connectWithIndication() { - return internalConnect() - .whenCompleteAsync((status, throwable) -> { - if (throwable != null) { - // TODO: The exception handling from this is not very good. Find a better way. - handleConnectionException(status != null ? status.getAttemptedConnection() - : toConnect, throwable, true); - return; - } + return internalConnect().whenCompleteAsync((status, throwable) -> { + if (throwable != null) { + // TODO: The exception handling from this is not very good. Find a better way. + handleConnectionException(status != null ? status.getAttemptedConnection() : toConnect, + throwable, true); + return; + } - switch (status.getStatus()) { - case ALREADY_CONNECTED: - sendMessage(Identity.nil(), ConnectionMessages.ALREADY_CONNECTED); - break; - case CONNECTION_IN_PROGRESS: - sendMessage(Identity.nil(), ConnectionMessages.IN_PROGRESS); - break; - case CONNECTION_CANCELLED: - // Ignored; the plugin probably already handled this. - break; - case SERVER_DISCONNECTED: - Component reason = status.getReasonComponent() - .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); - handleConnectionException(toConnect, Disconnect.create(reason, - getProtocolVersion()), status.isSafe()); - break; - default: - // The only remaining value is successful (no need to do anything!) - break; - } - }, connection.eventLoop()) - .thenApply(Result::isSuccessful); + switch (status.getStatus()) { + case ALREADY_CONNECTED: + sendMessage(Identity.nil(), ConnectionMessages.ALREADY_CONNECTED); + break; + case CONNECTION_IN_PROGRESS: + sendMessage(Identity.nil(), ConnectionMessages.IN_PROGRESS); + break; + case CONNECTION_CANCELLED: + // Ignored; the plugin probably already handled this. + break; + case SERVER_DISCONNECTED: + Component reason = status.getReasonComponent() + .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); + handleConnectionException(toConnect, Disconnect.create(reason, getProtocolVersion()), + status.isSafe()); + break; + default: + // The only remaining value is successful (no need to do anything!) + break; + } + }, connection.eventLoop()).thenApply(Result::isSuccessful); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java index e814efaa3..079ce035f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java @@ -47,8 +47,8 @@ import org.checkerframework.checker.nullness.qual.Nullable; /** * The initial handler used when a connection is established to the proxy. This will either - * transition to {@link StatusSessionHandler} or {@link InitialLoginSessionHandler} as soon - * as the handshake packet is received. + * transition to {@link StatusSessionHandler} or {@link InitialLoginSessionHandler} as soon as the + * handshake packet is received. */ public class HandshakeSessionHandler implements MinecraftSessionHandler { @@ -65,9 +65,9 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(LegacyPing packet) { connection.setProtocolVersion(ProtocolVersion.LEGACY); - StatusSessionHandler handler = new StatusSessionHandler(server, - new LegacyInboundConnection(connection, packet)); - connection.setSessionHandler(handler); + StatusSessionHandler handler = + new StatusSessionHandler(server, new LegacyInboundConnection(connection, packet)); + connection.setActiveSessionHandler(StateRegistry.STATUS, handler); handler.handle(packet); return true; } @@ -90,13 +90,13 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { LOGGER.error("{} provided invalid protocol {}", ic, handshake.getNextStatus()); connection.close(true); } else { - connection.setState(nextState); connection.setProtocolVersion(handshake.getProtocolVersion()); connection.setAssociation(ic); switch (nextState) { case STATUS: - connection.setSessionHandler(new StatusSessionHandler(server, ic)); + connection.setActiveSessionHandler(StateRegistry.STATUS, + new StatusSessionHandler(server, ic)); break; case LOGIN: this.handleLogin(handshake, ic); @@ -140,14 +140,15 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { // and lower, otherwise IP information will never get forwarded. if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && handshake.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { - ic.disconnectQuietly(Component.translatable( - "velocity.error.modern-forwarding-needs-new-client")); + ic.disconnectQuietly( + Component.translatable("velocity.error.modern-forwarding-needs-new-client")); return; } LoginInboundConnection lic = new LoginInboundConnection(ic); server.getEventManager().fireAndForget(new ConnectionHandshakeEvent(lic)); - connection.setSessionHandler(new InitialLoginSessionHandler(server, connection, lic)); + connection.setActiveSessionHandler(StateRegistry.LOGIN, + new InitialLoginSessionHandler(server, connection, lic)); } private ConnectionType getHandshakeConnectionType(Handshake handshake) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java index 01bef5f92..1409dc4ba 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java @@ -34,6 +34,7 @@ import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.packet.EncryptionRequest; import com.velocitypowered.proxy.protocol.packet.EncryptionResponse; @@ -120,47 +121,45 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler { this.login = packet; PreLoginEvent event = new PreLoginEvent(inbound, login.getUsername()); - server.getEventManager().fire(event) - .thenRunAsync(() -> { - if (mcConnection.isClosed()) { - // The player was disconnected - return; + server.getEventManager().fire(event).thenRunAsync(() -> { + if (mcConnection.isClosed()) { + // The player was disconnected + return; + } + + PreLoginComponentResult result = event.getResult(); + Optional disconnectReason = result.getReasonComponent(); + if (disconnectReason.isPresent()) { + // The component is guaranteed to be provided if the connection was denied. + inbound.disconnect(disconnectReason.get()); + return; + } + + inbound.loginEventFired(() -> { + if (mcConnection.isClosed()) { + // The player was disconnected + return; + } + + mcConnection.eventLoop().execute(() -> { + if (!result.isForceOfflineMode() + && (server.getConfiguration().isOnlineMode() || result.isOnlineModeAllowed())) { + // Request encryption. + EncryptionRequest request = generateEncryptionRequest(); + this.verify = Arrays.copyOf(request.getVerifyToken(), 4); + mcConnection.write(request); + this.currentState = LoginState.ENCRYPTION_REQUEST_SENT; + } else { + mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, + new AuthSessionHandler(server, inbound, + GameProfile.forOfflinePlayer(login.getUsername()), false)); } - - PreLoginComponentResult result = event.getResult(); - Optional disconnectReason = result.getReasonComponent(); - if (disconnectReason.isPresent()) { - // The component is guaranteed to be provided if the connection was denied. - inbound.disconnect(disconnectReason.get()); - return; - } - - inbound.loginEventFired(() -> { - if (mcConnection.isClosed()) { - // The player was disconnected - return; - } - - mcConnection.eventLoop().execute(() -> { - if (!result.isForceOfflineMode() && (server.getConfiguration().isOnlineMode() - || result.isOnlineModeAllowed())) { - // Request encryption. - EncryptionRequest request = generateEncryptionRequest(); - this.verify = Arrays.copyOf(request.getVerifyToken(), 4); - mcConnection.write(request); - this.currentState = LoginState.ENCRYPTION_REQUEST_SENT; - } else { - mcConnection.setSessionHandler(new AuthSessionHandler( - server, inbound, GameProfile.forOfflinePlayer(login.getUsername()), false - )); - } - }); - }); - }, mcConnection.eventLoop()) - .exceptionally((ex) -> { - logger.error("Exception in pre-login stage", ex); - return null; }); + }); + }, mcConnection.eventLoop()).exceptionally((ex) -> { + logger.error("Exception in pre-login stage", ex); + return null; + }); return true; } @@ -246,13 +245,12 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler { } } // All went well, initialize the session. - mcConnection.setSessionHandler(new AuthSessionHandler( - server, inbound, profile, true - )); + mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, + new AuthSessionHandler(server, inbound, profile, true)); } else if (profileResponse.getStatusCode() == 204) { // Apparently an offline-mode user logged onto this online-mode proxy. - inbound.disconnect(Component.translatable("velocity.error.online-mode-only", - NamedTextColor.RED)); + inbound.disconnect( + Component.translatable("velocity.error.online-mode-only", NamedTextColor.RED)); } else { // Something else went wrong logger.error( diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java index 5d7542170..19b09c122 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java @@ -76,9 +76,9 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase { }, /** - * The Mod list is sent to the server, captured by Velocity. Transition to - * {@link #WAITING_SERVER_DATA} when an ACK is sent, which indicates to the server to start - * sending state data. + * The Mod list is sent to the server, captured by Velocity. Transition to {@link + * #WAITING_SERVER_DATA} when an ACK is sent, which indicates to the server to start sending state + * data. */ MOD_LIST(LegacyForgeConstants.ACK_DISCRIMINATOR) { @Override @@ -138,11 +138,10 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase { /** * The handshake is complete. The handshake can be reset. * - *

Note that a successful connection to a server does not mean that - * we will be in this state. After a handshake reset, if the next server is vanilla we will still - * be in the {@link #NOT_STARTED} phase, which means we must NOT send a reset packet. This is - * handled by overriding the {@link #resetConnectionPhase(ConnectedPlayer)} in this element (it is - * usually a no-op).

+ *

Note that a successful connection to a server does not mean that we will be in this state. + * After a handshake reset, if the next server is vanilla we will still be in the {@link + * #NOT_STARTED} phase, which means we must NOT send a reset packet. This is handled by overriding + * the {@link #resetConnectionPhase(ConnectedPlayer)} in this element (it is usually a no-op). */ COMPLETE(null) { @Override @@ -165,7 +164,7 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase { // just in case the timing is awful player.sendKeepAlive(); - MinecraftSessionHandler handler = backendConn.getSessionHandler(); + MinecraftSessionHandler handler = backendConn.getActiveSessionHandler(); if (handler instanceof ClientPlaySessionHandler) { ((ClientPlaySessionHandler) handler).flushQueuedMessages(); } @@ -182,8 +181,8 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase { * * @param packetToAdvanceOn The ID of the packet discriminator that indicates that the client has * moved onto a new phase, and as such, Velocity should do so too - * (inspecting {@link #nextPhase()}. A null indicates there is no further - * phase to transition to. + * (inspecting {@link #nextPhase()}. A null indicates there is + * no further phase to transition to. */ LegacyForgeHandshakeClientPhase(Integer packetToAdvanceOn) { this.packetToAdvanceOn = packetToAdvanceOn; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/ClientConfigData.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/ClientConfigData.java new file mode 100644 index 000000000..1bca74515 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/ClientConfigData.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * 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 . + */ + +package com.velocitypowered.proxy.connection.registry; + +import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.protocol.packet.config.RegistrySync; +import net.kyori.adventure.key.Key; +import org.jetbrains.annotations.Nullable; + +/** + * Holds the registry data that is sent + * to the client during the config stage. + */ +public class ClientConfigData { + + private final @Nullable VelocityResourcePackInfo resourcePackInfo; + private final DataTag tag; + private final RegistrySync registry; + private final Key[] features; + private final String brand; + + private ClientConfigData(@Nullable VelocityResourcePackInfo resourcePackInfo, DataTag tag, + RegistrySync registry, Key[] features, String brand) { + this.resourcePackInfo = resourcePackInfo; + this.tag = tag; + this.registry = registry; + this.features = features; + this.brand = brand; + } + + public RegistrySync getRegistry() { + return registry; + } + + public DataTag getTag() { + return tag; + } + + public Key[] getFeatures() { + return features; + } + + public @Nullable VelocityResourcePackInfo getResourcePackInfo() { + return resourcePackInfo; + } + + public String getBrand() { + return brand; + } + + /** + * Creates a new builder. + * + * @return ClientConfigData.Builder + */ + public static ClientConfigData.Builder builder() { + return new Builder(); + } + + /** + * Builder for ClientConfigData. + */ + public static class Builder { + private VelocityResourcePackInfo resourcePackInfo; + private DataTag tag; + private RegistrySync registry; + private Key[] features; + private String brand; + + private Builder() { + } + + /** + * Clears the builder. + */ + public void clear() { + this.resourcePackInfo = null; + this.tag = null; + this.registry = null; + this.features = null; + this.brand = null; + } + + public Builder resourcePack(@Nullable VelocityResourcePackInfo resourcePackInfo) { + this.resourcePackInfo = resourcePackInfo; + return this; + } + + public Builder dataTag(DataTag tag) { + this.tag = tag; + return this; + } + + public Builder registry(RegistrySync registry) { + this.registry = registry; + return this; + } + + public Builder features(Key[] features) { + this.features = features; + return this; + } + + public Builder brand(String brand) { + this.brand = brand; + return this; + } + + public ClientConfigData build() { + return new ClientConfigData(resourcePackInfo, tag, registry, features, brand); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DataTag.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DataTag.java new file mode 100644 index 000000000..9a7d0de73 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DataTag.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2019-2023 Velocity Contributors + * + * 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 . + */ + +package com.velocitypowered.proxy.connection.registry; + +import com.google.common.collect.ImmutableList; +import java.util.List; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.key.Keyed; +import org.jetbrains.annotations.NotNull; + +/** + * Represents a data tag. + */ +public class DataTag { + private final ImmutableList entrySets; + + public DataTag(ImmutableList entrySets) { + this.entrySets = entrySets; + } + + /** + * Returns the entry sets. + * + * @return List of entry sets + */ + public List getEntrySets() { + return entrySets; + } + + /** + * Represents a data tag set. + */ + public static class Set implements Keyed { + + private final Key key; + private final ImmutableList entries; + + public Set(Key key, ImmutableList entries) { + this.key = key; + this.entries = entries; + } + + /** + * Returns the entries. + * + * @return List of entries + */ + public List getEntries() { + return entries; + } + + @Override + public @NotNull Key key() { + return key; + } + } + + /** + * Represents a data tag entry. + */ + public static class Entry implements Keyed { + + private final Key key; + private final int[] elements; + + public Entry(Key key, int[] elements) { + this.key = key; + this.elements = elements; + } + + public int[] getElements() { + return elements; + } + + @Override + public @NotNull Key key() { + return key; + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java b/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java index dff8d2236..27ec4ba8b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java @@ -35,6 +35,7 @@ public class Connections { public static final String MINECRAFT_DECODER = "minecraft-decoder"; public static final String MINECRAFT_ENCODER = "minecraft-encoder"; public static final String READ_TIMEOUT = "read-timeout"; + public static final String PLAY_PACKET_QUEUE = "play-packet-queue"; private Connections() { throw new AssertionError(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java index 62f38ebd7..ef8e6b1cb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java @@ -29,6 +29,7 @@ import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.LegacyPingDecoder; import com.velocitypowered.proxy.protocol.netty.LegacyPingEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; @@ -67,7 +68,8 @@ public class ServerChannelInitializer extends ChannelInitializer { .addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolUtils.Direction.CLIENTBOUND)); final MinecraftConnection connection = new MinecraftConnection(ch, this.server); - connection.setSessionHandler(new HandshakeSessionHandler(connection, this.server)); + connection.setActiveSessionHandler(StateRegistry.HANDSHAKE, + new HandshakeSessionHandler(connection, this.server)); ch.pipeline().addLast(Connections.HANDLER, connection); if (this.server.getConfiguration().isProxyProtocol()) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java index 97b40def7..e65ee056b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java @@ -41,6 +41,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.UUID; +import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.BinaryTagIO; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; @@ -202,8 +203,7 @@ public enum ProtocolUtils { buf.readableBytes()); String str = buf.toString(buf.readerIndex(), length, StandardCharsets.UTF_8); buf.skipBytes(length); - checkFrame(str.length() <= cap, "Got a too-long string (got %s, max %s)", - str.length(), cap); + checkFrame(str.length() <= cap, "Got a too-long string (got %s, max %s)", str.length(), cap); return str; } @@ -219,6 +219,59 @@ public enum ProtocolUtils { buf.writeCharSequence(str, StandardCharsets.UTF_8); } + /** + * Reads a standard Mojang Text namespaced:key from the buffer. + * + * @param buf the buffer to read from + * @return the decoded key + */ + public static Key readKey(ByteBuf buf) { + return Key.key(readString(buf), Key.DEFAULT_SEPARATOR); + } + + /** + * Writes a standard Mojang Text namespaced:key to the buffer. + * + * @param buf the buffer to write to + * @param key the key to write + */ + public static void writeKey(ByteBuf buf, Key key) { + writeString(buf, key.asString()); + } + + /** + * Reads a standard Mojang Text namespaced:key array from the buffer. + * + * @param buf the buffer to read from + * @return the decoded key array + */ + public static Key[] readKeyArray(ByteBuf buf) { + int length = readVarInt(buf); + checkFrame(length >= 0, "Got a negative-length array (%s)", length); + checkFrame(buf.isReadable(length), + "Trying to read an array that is too long (wanted %s, only have %s)", length, + buf.readableBytes()); + Key[] ret = new Key[length]; + + for (int i = 0; i < ret.length; i++) { + ret[i] = ProtocolUtils.readKey(buf); + } + return ret; + } + + /** + * Writes a standard Mojang Text namespaced:key array to the buffer. + * + * @param buf the buffer to write to + * @param keys the keys to write + */ + public static void writeKeyArray(ByteBuf buf, Key[] keys) { + writeVarInt(buf, keys.length); + for (Key key : keys) { + writeKey(buf, key); + } + } + public static byte[] readByteArray(ByteBuf buf) { return readByteArray(buf, DEFAULT_MAX_STRING_SIZE); } @@ -368,6 +421,38 @@ public enum ProtocolUtils { } } + /** + * Reads an Integer array from the {@code buf}. + * + * @param buf the buffer to read from + * @return the Integer array from the buffer + */ + public static int[] readVarIntArray(ByteBuf buf) { + int length = readVarInt(buf); + checkFrame(length >= 0, "Got a negative-length array (%s)", length); + checkFrame(buf.isReadable(length), + "Trying to read an array that is too long (wanted %s, only have %s)", length, + buf.readableBytes()); + int[] ret = new int[length]; + for (int i = 0; i < length; i++) { + ret[i] = readVarInt(buf); + } + return ret; + } + + /** + * Writes an Integer Array to the {@code buf}. + * + * @param buf the buffer to write to + * @param intArray the array to write + */ + public static void writeVarIntArray(ByteBuf buf, int[] intArray) { + writeVarInt(buf, intArray.length); + for (int i = 0; i < intArray.length; i++) { + writeVarInt(buf, intArray[i]); + } + } + /** * Writes a list of {@link com.velocitypowered.api.util.GameProfile.Property} to the buffer. * diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index 9cb4aacdc..8845dbeae 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -33,6 +33,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_1; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_3; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_4; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; @@ -55,8 +56,10 @@ import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.JoinGame; import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; +import com.velocitypowered.proxy.protocol.packet.PingIdentify; import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; @@ -79,6 +82,11 @@ import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerCommand; import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChat; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerChat; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerCommand; +import com.velocitypowered.proxy.protocol.packet.config.ActiveFeatures; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; +import com.velocitypowered.proxy.protocol.packet.config.RegistrySync; +import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; +import com.velocitypowered.proxy.protocol.packet.config.TagsUpdate; import com.velocitypowered.proxy.protocol.packet.title.LegacyTitlePacket; import com.velocitypowered.proxy.protocol.packet.title.TitleActionbarPacket; import com.velocitypowered.proxy.protocol.packet.title.TitleClearPacket; @@ -98,9 +106,7 @@ import java.util.Objects; import java.util.function.Supplier; import org.checkerframework.checker.nullness.qual.Nullable; -/** - * Registry of all Minecraft protocol states and the packets for each state. - */ +/** Registry of all Minecraft protocol states and the packets for each state. */ public enum StateRegistry { HANDSHAKE { @@ -111,15 +117,46 @@ public enum StateRegistry { }, STATUS { { - serverbound.register(StatusRequest.class, () -> StatusRequest.INSTANCE, - map(0x00, MINECRAFT_1_7_2, false)); - serverbound.register(StatusPing.class, StatusPing::new, - map(0x01, MINECRAFT_1_7_2, false)); + serverbound.register( + StatusRequest.class, () -> StatusRequest.INSTANCE, map(0x00, MINECRAFT_1_7_2, false)); + serverbound.register(StatusPing.class, StatusPing::new, map(0x01, MINECRAFT_1_7_2, false)); - clientbound.register(StatusResponse.class, StatusResponse::new, - map(0x00, MINECRAFT_1_7_2, false)); - clientbound.register(StatusPing.class, StatusPing::new, - map(0x01, MINECRAFT_1_7_2, false)); + clientbound.register( + StatusResponse.class, StatusResponse::new, map(0x00, MINECRAFT_1_7_2, false)); + clientbound.register(StatusPing.class, StatusPing::new, map(0x01, MINECRAFT_1_7_2, false)); + } + }, + CONFIG { + { + serverbound.register( + ClientSettings.class, ClientSettings::new, map(0x00, MINECRAFT_1_20_2, false)); + serverbound.register( + PluginMessage.class, PluginMessage::new, map(0x01, MINECRAFT_1_20_2, false)); + serverbound.register( + FinishedUpdate.class, FinishedUpdate::new, map(0x02, MINECRAFT_1_20_2, false)); + serverbound.register(KeepAlive.class, KeepAlive::new, map(0x03, MINECRAFT_1_20_2, false)); + serverbound.register( + PingIdentify.class, PingIdentify::new, map(0x04, MINECRAFT_1_20_2, false)); + serverbound.register( + ResourcePackResponse.class, + ResourcePackResponse::new, + map(0x05, MINECRAFT_1_20_2, false)); + + clientbound.register( + PluginMessage.class, PluginMessage::new, map(0x00, MINECRAFT_1_20_2, false)); + clientbound.register(Disconnect.class, Disconnect::new, map(0x01, MINECRAFT_1_20_2, false)); + clientbound.register( + FinishedUpdate.class, FinishedUpdate::new, map(0x02, MINECRAFT_1_20_2, false)); + clientbound.register(KeepAlive.class, KeepAlive::new, map(0x03, MINECRAFT_1_20_2, false)); + clientbound.register( + PingIdentify.class, PingIdentify::new, map(0x04, MINECRAFT_1_20_2, false)); + clientbound.register( + RegistrySync.class, RegistrySync::new, map(0x05, MINECRAFT_1_20_2, false)); + clientbound.register( + ResourcePackRequest.class, ResourcePackRequest::new, map(0x06, MINECRAFT_1_20_2, false)); + clientbound.register( + ActiveFeatures.class, ActiveFeatures::new, map(0x07, MINECRAFT_1_20_2, false)); + clientbound.register(TagsUpdate.class, TagsUpdate::new, map(0x08, MINECRAFT_1_20_2, false)); } }, PLAY { @@ -137,8 +174,11 @@ public enum StateRegistry { map(0x08, MINECRAFT_1_19, false), map(0x09, MINECRAFT_1_19_1, false), map(0x08, MINECRAFT_1_19_3, false), - map(0x09, MINECRAFT_1_19_4, false)); - serverbound.register(LegacyChat.class, LegacyChat::new, + map(0x09, MINECRAFT_1_19_4, false), + map(0x0A, MINECRAFT_1_20_2, false)); + serverbound.register( + LegacyChat.class, + LegacyChat::new, map(0x01, MINECRAFT_1_7_2, false), map(0x02, MINECRAFT_1_9, false), map(0x03, MINECRAFT_1_12, false), @@ -152,9 +192,13 @@ public enum StateRegistry { map(0x05, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false)); serverbound.register(SessionPlayerCommand.class, SessionPlayerCommand::new, map(0x04, MINECRAFT_1_19_3, false)); - serverbound.register(SessionPlayerChat.class, SessionPlayerChat::new, - map(0x05, MINECRAFT_1_19_3, false)); - serverbound.register(ClientSettings.class, ClientSettings::new, + serverbound.register( + SessionPlayerChat.class, + SessionPlayerChat::new, + map(0x05, MINECRAFT_1_19_3, MINECRAFT_1_20_2, false)); + serverbound.register( + ClientSettings.class, + ClientSettings::new, map(0x15, MINECRAFT_1_7_2, false), map(0x04, MINECRAFT_1_9, false), map(0x05, MINECRAFT_1_12, false), @@ -163,8 +207,11 @@ public enum StateRegistry { map(0x07, MINECRAFT_1_19, false), map(0x08, MINECRAFT_1_19_1, false), map(0x07, MINECRAFT_1_19_3, false), - map(0x08, MINECRAFT_1_19_4, false)); - serverbound.register(PluginMessage.class, PluginMessage::new, + map(0x08, MINECRAFT_1_19_4, false), + map(0x09, MINECRAFT_1_20_2, false)); + serverbound.register( + PluginMessage.class, + PluginMessage::new, map(0x17, MINECRAFT_1_7_2, false), map(0x09, MINECRAFT_1_9, false), map(0x0A, MINECRAFT_1_12, false), @@ -175,8 +222,11 @@ public enum StateRegistry { map(0x0C, MINECRAFT_1_19, false), map(0x0D, MINECRAFT_1_19_1, false), map(0x0C, MINECRAFT_1_19_3, false), - map(0x0D, MINECRAFT_1_19_4, false)); - serverbound.register(KeepAlive.class, KeepAlive::new, + map(0x0D, MINECRAFT_1_19_4, false), + map(0x0F, MINECRAFT_1_20_2, false)); + serverbound.register( + KeepAlive.class, + KeepAlive::new, map(0x00, MINECRAFT_1_7_2, false), map(0x0B, MINECRAFT_1_9, false), map(0x0C, MINECRAFT_1_12, false), @@ -188,8 +238,11 @@ public enum StateRegistry { map(0x11, MINECRAFT_1_19, false), map(0x12, MINECRAFT_1_19_1, false), map(0x11, MINECRAFT_1_19_3, false), - map(0x12, MINECRAFT_1_19_4, false)); - serverbound.register(ResourcePackResponse.class, ResourcePackResponse::new, + map(0x12, MINECRAFT_1_19_4, false), + map(0x14, MINECRAFT_1_20_2, false)); + serverbound.register( + ResourcePackResponse.class, + ResourcePackResponse::new, map(0x19, MINECRAFT_1_8, false), map(0x16, MINECRAFT_1_9, false), map(0x18, MINECRAFT_1_12, false), @@ -198,16 +251,24 @@ public enum StateRegistry { map(0x20, MINECRAFT_1_16, false), map(0x21, MINECRAFT_1_16_2, false), map(0x23, MINECRAFT_1_19, false), - map(0x24, MINECRAFT_1_19_1, false)); + map(0x24, MINECRAFT_1_19_1, false), + map(0x27, MINECRAFT_1_20_2, false)); + serverbound.register( + FinishedUpdate.class, FinishedUpdate::new, map(0x0B, MINECRAFT_1_20_2, false)); - clientbound.register(BossBar.class, BossBar::new, + clientbound.register( + BossBar.class, + BossBar::new, map(0x0C, MINECRAFT_1_9, false), map(0x0D, MINECRAFT_1_15, false), map(0x0C, MINECRAFT_1_16, false), map(0x0D, MINECRAFT_1_17, false), map(0x0A, MINECRAFT_1_19, false), - map(0x0B, MINECRAFT_1_19_4, false)); - clientbound.register(LegacyChat.class, LegacyChat::new, + map(0x0B, MINECRAFT_1_19_4, false), + map(0x0A, MINECRAFT_1_20_2, false)); + clientbound.register( + LegacyChat.class, + LegacyChat::new, map(0x02, MINECRAFT_1_7_2, true), map(0x0F, MINECRAFT_1_9, true), map(0x0E, MINECRAFT_1_13, true), @@ -224,8 +285,11 @@ public enum StateRegistry { map(0x11, MINECRAFT_1_17, false), map(0x0E, MINECRAFT_1_19, false), map(0x0D, MINECRAFT_1_19_3, false), - map(0x0F, MINECRAFT_1_19_4, false)); - clientbound.register(AvailableCommands.class, AvailableCommands::new, + map(0x0F, MINECRAFT_1_19_4, false), + map(0x10, MINECRAFT_1_20_2, false)); + clientbound.register( + AvailableCommands.class, + AvailableCommands::new, map(0x11, MINECRAFT_1_13, false), map(0x12, MINECRAFT_1_15, false), map(0x11, MINECRAFT_1_16, false), @@ -233,8 +297,11 @@ public enum StateRegistry { map(0x12, MINECRAFT_1_17, false), map(0x0F, MINECRAFT_1_19, false), map(0x0E, MINECRAFT_1_19_3, false), - map(0x10, MINECRAFT_1_19_4, false)); - clientbound.register(PluginMessage.class, PluginMessage::new, + map(0x10, MINECRAFT_1_19_4, false), + map(0x11, MINECRAFT_1_20_2, false)); + clientbound.register( + PluginMessage.class, + PluginMessage::new, map(0x3F, MINECRAFT_1_7_2, false), map(0x18, MINECRAFT_1_9, false), map(0x19, MINECRAFT_1_13, false), @@ -246,8 +313,11 @@ public enum StateRegistry { map(0x15, MINECRAFT_1_19, false), map(0x16, MINECRAFT_1_19_1, false), map(0x15, MINECRAFT_1_19_3, false), - map(0x17, MINECRAFT_1_19_4, false)); - clientbound.register(Disconnect.class, Disconnect::new, + map(0x17, MINECRAFT_1_19_4, false), + map(0x18, MINECRAFT_1_20_2, false)); + clientbound.register( + Disconnect.class, + Disconnect::new, map(0x40, MINECRAFT_1_7_2, false), map(0x1A, MINECRAFT_1_9, false), map(0x1B, MINECRAFT_1_13, false), @@ -259,8 +329,11 @@ public enum StateRegistry { map(0x17, MINECRAFT_1_19, false), map(0x19, MINECRAFT_1_19_1, false), map(0x17, MINECRAFT_1_19_3, false), - map(0x1A, MINECRAFT_1_19_4, false)); - clientbound.register(KeepAlive.class, KeepAlive::new, + map(0x1A, MINECRAFT_1_19_4, false), + map(0x1B, MINECRAFT_1_20_2, false)); + clientbound.register( + KeepAlive.class, + KeepAlive::new, map(0x00, MINECRAFT_1_7_2, false), map(0x1F, MINECRAFT_1_9, false), map(0x21, MINECRAFT_1_13, false), @@ -272,8 +345,11 @@ public enum StateRegistry { map(0x1E, MINECRAFT_1_19, false), map(0x20, MINECRAFT_1_19_1, false), map(0x1F, MINECRAFT_1_19_3, false), - map(0x23, MINECRAFT_1_19_4, false)); - clientbound.register(JoinGame.class, JoinGame::new, + map(0x23, MINECRAFT_1_19_4, false), + map(0x24, MINECRAFT_1_20_2, false)); + clientbound.register( + JoinGame.class, + JoinGame::new, map(0x01, MINECRAFT_1_7_2, false), map(0x23, MINECRAFT_1_9, false), map(0x25, MINECRAFT_1_13, false), @@ -285,8 +361,11 @@ public enum StateRegistry { map(0x23, MINECRAFT_1_19, false), map(0x25, MINECRAFT_1_19_1, false), map(0x24, MINECRAFT_1_19_3, false), - map(0x28, MINECRAFT_1_19_4, false)); - clientbound.register(Respawn.class, Respawn::new, + map(0x28, MINECRAFT_1_19_4, false), + map(0x29, MINECRAFT_1_20_2, false)); + clientbound.register( + Respawn.class, + Respawn::new, map(0x07, MINECRAFT_1_7_2, true), map(0x33, MINECRAFT_1_9, true), map(0x34, MINECRAFT_1_12, true), @@ -300,8 +379,11 @@ public enum StateRegistry { map(0x3B, MINECRAFT_1_19, true), map(0x3E, MINECRAFT_1_19_1, true), map(0x3D, MINECRAFT_1_19_3, true), - map(0x41, MINECRAFT_1_19_4, true)); - clientbound.register(ResourcePackRequest.class, ResourcePackRequest::new, + map(0x41, MINECRAFT_1_19_4, true), + map(0x43, MINECRAFT_1_20_2, true)); + clientbound.register( + ResourcePackRequest.class, + ResourcePackRequest::new, map(0x48, MINECRAFT_1_8, false), map(0x32, MINECRAFT_1_9, false), map(0x33, MINECRAFT_1_12, false), @@ -315,8 +397,11 @@ public enum StateRegistry { map(0x3A, MINECRAFT_1_19, false), map(0x3D, MINECRAFT_1_19_1, false), map(0x3C, MINECRAFT_1_19_3, false), - map(0x40, MINECRAFT_1_19_4, false)); - clientbound.register(HeaderAndFooter.class, HeaderAndFooter::new, + map(0x40, MINECRAFT_1_19_4, false), + map(0x42, MINECRAFT_1_20_2, false)); + clientbound.register( + HeaderAndFooter.class, + HeaderAndFooter::new, map(0x47, MINECRAFT_1_8, true), map(0x48, MINECRAFT_1_9, true), map(0x47, MINECRAFT_1_9_4, true), @@ -331,8 +416,11 @@ public enum StateRegistry { map(0x60, MINECRAFT_1_19, true), map(0x63, MINECRAFT_1_19_1, true), map(0x61, MINECRAFT_1_19_3, true), - map(0x65, MINECRAFT_1_19_4, true)); - clientbound.register(LegacyTitlePacket.class, LegacyTitlePacket::new, + map(0x65, MINECRAFT_1_19_4, true), + map(0x68, MINECRAFT_1_20_2, true)); + clientbound.register( + LegacyTitlePacket.class, + LegacyTitlePacket::new, map(0x45, MINECRAFT_1_8, true), map(0x45, MINECRAFT_1_9, true), map(0x47, MINECRAFT_1_12, true), @@ -346,31 +434,46 @@ public enum StateRegistry { map(0x58, MINECRAFT_1_18, true), map(0x5B, MINECRAFT_1_19_1, true), map(0x59, MINECRAFT_1_19_3, true), - map(0x5D, MINECRAFT_1_19_4, true)); - clientbound.register(TitleTextPacket.class, TitleTextPacket::new, + map(0x5D, MINECRAFT_1_19_4, true), + map(0x5F, MINECRAFT_1_20_2, true)); + clientbound.register( + TitleTextPacket.class, + TitleTextPacket::new, map(0x59, MINECRAFT_1_17, true), map(0x5A, MINECRAFT_1_18, true), map(0x5D, MINECRAFT_1_19_1, true), map(0x5B, MINECRAFT_1_19_3, true), - map(0x5F, MINECRAFT_1_19_4, true)); - clientbound.register(TitleActionbarPacket.class, TitleActionbarPacket::new, + map(0x5F, MINECRAFT_1_19_4, true), + map(0x61, MINECRAFT_1_20_2, true)); + clientbound.register( + TitleActionbarPacket.class, + TitleActionbarPacket::new, map(0x41, MINECRAFT_1_17, true), map(0x40, MINECRAFT_1_19, true), map(0x43, MINECRAFT_1_19_1, true), map(0x42, MINECRAFT_1_19_3, true), - map(0x46, MINECRAFT_1_19_4, true)); - clientbound.register(TitleTimesPacket.class, TitleTimesPacket::new, + map(0x46, MINECRAFT_1_19_4, true), + map(0x48, MINECRAFT_1_20_2, true)); + clientbound.register( + TitleTimesPacket.class, + TitleTimesPacket::new, map(0x5A, MINECRAFT_1_17, true), map(0x5B, MINECRAFT_1_18, true), map(0x5E, MINECRAFT_1_19_1, true), map(0x5C, MINECRAFT_1_19_3, true), - map(0x60, MINECRAFT_1_19_4, true)); - clientbound.register(TitleClearPacket.class, TitleClearPacket::new, + map(0x60, MINECRAFT_1_19_4, true), + map(0x62, MINECRAFT_1_20_2, true)); + clientbound.register( + TitleClearPacket.class, + TitleClearPacket::new, map(0x10, MINECRAFT_1_17, true), map(0x0D, MINECRAFT_1_19, true), map(0x0C, MINECRAFT_1_19_3, true), - map(0x0E, MINECRAFT_1_19_4, true)); - clientbound.register(LegacyPlayerListItem.class, LegacyPlayerListItem::new, + map(0x0E, MINECRAFT_1_19_4, true), + map(0x0F, MINECRAFT_1_20_2, true)); + clientbound.register( + LegacyPlayerListItem.class, + LegacyPlayerListItem::new, map(0x38, MINECRAFT_1_7_2, false), map(0x2D, MINECRAFT_1_9, false), map(0x2E, MINECRAFT_1_12_1, false), @@ -384,44 +487,59 @@ public enum StateRegistry { map(0x37, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false)); clientbound.register(RemovePlayerInfo.class, RemovePlayerInfo::new, map(0x35, MINECRAFT_1_19_3, false), - map(0x39, MINECRAFT_1_19_4, false)); - clientbound.register(UpsertPlayerInfo.class, UpsertPlayerInfo::new, + map(0x39, MINECRAFT_1_19_4, false), + map(0x3B, MINECRAFT_1_20_2, false)); + clientbound.register( + UpsertPlayerInfo.class, + UpsertPlayerInfo::new, map(0x36, MINECRAFT_1_19_3, false), - map(0x3A, MINECRAFT_1_19_4, false)); - clientbound.register(SystemChat.class, SystemChat::new, + map(0x3A, MINECRAFT_1_19_4, false), + map(0x3C, MINECRAFT_1_20_2, false)); + clientbound.register( + SystemChat.class, + SystemChat::new, map(0x5F, MINECRAFT_1_19, true), map(0x62, MINECRAFT_1_19_1, true), map(0x60, MINECRAFT_1_19_3, true), - map(0x64, MINECRAFT_1_19_4, true)); - clientbound.register(PlayerChatCompletion.class, PlayerChatCompletion::new, + map(0x64, MINECRAFT_1_19_4, true), + map(0x67, MINECRAFT_1_20_2, true)); + clientbound.register( + PlayerChatCompletion.class, + PlayerChatCompletion::new, map(0x15, MINECRAFT_1_19_1, true), map(0x14, MINECRAFT_1_19_3, true), - map(0x16, MINECRAFT_1_19_4, true)); - clientbound.register(ServerData.class, ServerData::new, + map(0x16, MINECRAFT_1_19_4, true), + map(0x17, MINECRAFT_1_20_2, true)); + clientbound.register( + ServerData.class, + ServerData::new, map(0x3F, MINECRAFT_1_19, false), map(0x42, MINECRAFT_1_19_1, false), map(0x41, MINECRAFT_1_19_3, false), - map(0x45, MINECRAFT_1_19_4, false)); + map(0x45, MINECRAFT_1_19_4, false), + map(0x47, MINECRAFT_1_20_2, false)); + clientbound.register(StartUpdate.class, StartUpdate::new, map(0x65, MINECRAFT_1_20_2, false)); } }, LOGIN { { - serverbound.register(ServerLogin.class, ServerLogin::new, - map(0x00, MINECRAFT_1_7_2, false)); - serverbound.register(EncryptionResponse.class, EncryptionResponse::new, - map(0x01, MINECRAFT_1_7_2, false)); - serverbound.register(LoginPluginResponse.class, LoginPluginResponse::new, - map(0x02, MINECRAFT_1_13, false)); - clientbound.register(Disconnect.class, Disconnect::new, - map(0x00, MINECRAFT_1_7_2, false)); - clientbound.register(EncryptionRequest.class, EncryptionRequest::new, - map(0x01, MINECRAFT_1_7_2, false)); - clientbound.register(ServerLoginSuccess.class, ServerLoginSuccess::new, - map(0x02, MINECRAFT_1_7_2, false)); - clientbound.register(SetCompression.class, SetCompression::new, - map(0x03, MINECRAFT_1_8, false)); - clientbound.register(LoginPluginMessage.class, LoginPluginMessage::new, - map(0x04, MINECRAFT_1_13, false)); + serverbound.register(ServerLogin.class, ServerLogin::new, map(0x00, MINECRAFT_1_7_2, false)); + serverbound.register( + EncryptionResponse.class, EncryptionResponse::new, map(0x01, MINECRAFT_1_7_2, false)); + serverbound.register( + LoginPluginResponse.class, LoginPluginResponse::new, map(0x02, MINECRAFT_1_13, false)); + serverbound.register( + LoginAcknowledged.class, LoginAcknowledged::new, map(0x03, MINECRAFT_1_20_2, false)); + + clientbound.register(Disconnect.class, Disconnect::new, map(0x00, MINECRAFT_1_7_2, false)); + clientbound.register( + EncryptionRequest.class, EncryptionRequest::new, map(0x01, MINECRAFT_1_7_2, false)); + clientbound.register( + ServerLoginSuccess.class, ServerLoginSuccess::new, map(0x02, MINECRAFT_1_7_2, false)); + clientbound.register( + SetCompression.class, SetCompression::new, map(0x03, MINECRAFT_1_8, false)); + clientbound.register( + LoginPluginMessage.class, LoginPluginMessage::new, map(0x04, MINECRAFT_1_13, false)); } }; @@ -435,9 +553,7 @@ public enum StateRegistry { return (direction == SERVERBOUND ? serverbound : clientbound).getProtocolRegistry(version); } - /** - * Packet registry. - */ + /** Packet registry. */ public static class PacketRegistry { private final Direction direction; @@ -505,19 +621,24 @@ public enum StateRegistry { } ProtocolRegistry registry = this.versions.get(protocol); if (registry == null) { - throw new IllegalArgumentException("Unknown protocol version " - + current.protocolVersion); + throw new IllegalArgumentException( + "Unknown protocol version " + current.protocolVersion); } if (registry.packetIdToSupplier.containsKey(current.id)) { - throw new IllegalArgumentException("Can not register class " + clazz.getSimpleName() - + " with id " + current.id + " for " + registry.version - + " because another packet is already registered"); + throw new IllegalArgumentException( + "Can not register class " + + clazz.getSimpleName() + + " with id " + + current.id + + " for " + + registry.version + + " because another packet is already registered"); } if (registry.packetClassToId.containsKey(clazz)) { - throw new IllegalArgumentException(clazz.getSimpleName() - + " is already registered for version " + registry.version); + throw new IllegalArgumentException( + clazz.getSimpleName() + " is already registered for version " + registry.version); } if (!current.encodeOnly) { @@ -528,9 +649,7 @@ public enum StateRegistry { } } - /** - * Protocol registry. - */ + /** Protocol registry. */ public class ProtocolRegistry { public final ProtocolVersion version; @@ -575,12 +694,20 @@ public enum StateRegistry { } return id; } + + /** + * Checks if the registry contains a packet with the specified {@code id}. + * + * @param packet the packet to check + * @return {@code true} if the packet is registered, {@code false} otherwise + */ + public boolean containsPacket(final MinecraftPacket packet) { + return this.packetClassToId.containsKey(packet.getClass()); + } } } - /** - * Packet mapping. - */ + /** Packet mapping. */ public static final class PacketMapping { private final int id; @@ -599,9 +726,12 @@ public enum StateRegistry { @Override public String toString() { return "PacketMapping{" - + "id=" + id - + ", protocolVersion=" + protocolVersion - + ", encodeOnly=" + encodeOnly + + "id=" + + id + + ", protocolVersion=" + + protocolVersion + + ", encodeOnly=" + + encodeOnly + '}'; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftEncoder.java index 313b7858d..7133f1d26 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftEncoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftEncoder.java @@ -62,4 +62,8 @@ public class MinecraftEncoder extends MessageToByteEncoder { this.state = state; this.setProtocolVersion(registry.version); } + + public ProtocolUtils.Direction getDirection() { + return direction; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueHandler.java new file mode 100644 index 000000000..612de451e --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueHandler.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 Velocity Contributors + * + * 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 . + */ + +package com.velocitypowered.proxy.protocol.netty; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.StateRegistry; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.PlatformDependent; +import java.util.Queue; +import org.jetbrains.annotations.NotNull; + +/** + * Queues up any pending PLAY packets while the client is in the CONFIG state. + * + *

Much of the Velocity API (i.e. chat messages) utilize PLAY packets, however the client is + * incapable of receiving these packets during the CONFIG state. Certain events such as the + * ServerPreConnectEvent may be called during this time, and we need to ensure that any API that + * uses these packets will work as expected. + * + *

This handler will queue up any packets that are sent to the client during this time, and send + * them once the client has (re)entered the PLAY state. + */ +public class PlayPacketQueueHandler extends ChannelDuplexHandler { + + private final StateRegistry.PacketRegistry.ProtocolRegistry registry; + private final Queue queue = PlatformDependent.newMpscQueue(); + + /** + * Provides registries for client & server bound packets. + * + * @param version the protocol version + */ + public PlayPacketQueueHandler(ProtocolVersion version, ProtocolUtils.Direction direction) { + this.registry = + StateRegistry.CONFIG.getProtocolRegistry(direction, version); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) + throws Exception { + if (!(msg instanceof MinecraftPacket)) { + ctx.write(msg, promise); + return; + } + + // If the packet exists in the CONFIG state, we want to always + // ensure that it gets sent out to the client + if (this.registry.containsPacket(((MinecraftPacket) msg))) { + ctx.write(msg, promise); + return; + } + + // Otherwise, queue the packet + this.queue.offer((MinecraftPacket) msg); + } + + @Override + public void channelInactive(@NotNull ChannelHandlerContext ctx) throws Exception { + this.releaseQueue(ctx, false); + + super.channelInactive(ctx); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + this.releaseQueue(ctx, ctx.channel().isActive()); + } + + private void releaseQueue(ChannelHandlerContext ctx, boolean active) { + if (this.queue.isEmpty()) { + return; + } + + // Send out all the queued packets + MinecraftPacket packet; + while ((packet = this.queue.poll()) != null) { + if (active) { + ctx.write(packet, ctx.voidPromise()); + } else { + ReferenceCountUtil.release(packet); + } + } + + if (active) { + ctx.flush(); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettings.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettings.java index 5c267d195..a42d1ee90 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettings.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettings.java @@ -23,10 +23,10 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; import java.util.Objects; + import org.checkerframework.checker.nullness.qual.Nullable; public class ClientSettings implements MinecraftPacket { - private @Nullable String locale; private byte viewDistance; private int chatVisibility; @@ -120,16 +120,10 @@ public class ClientSettings implements MinecraftPacket { @Override public String toString() { - return "ClientSettings{" - + "locale='" + locale + '\'' - + ", viewDistance=" + viewDistance - + ", chatVisibility=" + chatVisibility - + ", chatColors=" + chatColors - + ", skinParts=" + skinParts - + ", mainHand=" + mainHand - + ", chatFilteringEnabled=" + chatFilteringEnabled - + ", clientListingAllowed=" + clientListingAllowed - + '}'; + return "ClientSettings{" + "locale='" + locale + '\'' + ", viewDistance=" + viewDistance + + ", chatVisibility=" + chatVisibility + ", chatColors=" + chatColors + ", skinParts=" + + skinParts + ", mainHand=" + mainHand + ", chatFilteringEnabled=" + chatFilteringEnabled + + ", clientListingAllowed=" + clientListingAllowed + '}'; } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java index cfbecce31..938d1190a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java @@ -42,6 +42,7 @@ public class JoinGame implements MinecraftPacket { private int viewDistance; // 1.14+ private boolean reducedDebugInfo; private boolean showRespawnScreen; + private boolean doLimitedCrafting; // 1.20.2+ private ImmutableSet levelNames; // 1.16+ private CompoundBinaryTag registry; // 1.16+ private DimensionInfo dimensionInfo; // 1.16+ @@ -143,6 +144,14 @@ public class JoinGame implements MinecraftPacket { this.isHardcore = isHardcore; } + public boolean getDoLimitedCrafting() { + return doLimitedCrafting; + } + + public void setDoLimitedCrafting(boolean doLimitedCrafting) { + this.doLimitedCrafting = doLimitedCrafting; + } + public CompoundBinaryTag getCurrentDimensionData() { return currentDimensionData; } @@ -177,32 +186,24 @@ public class JoinGame implements MinecraftPacket { @Override public String toString() { - return "JoinGame{" - + "entityId=" + entityId - + ", gamemode=" + gamemode - + ", dimension=" + dimension - + ", partialHashedSeed=" + partialHashedSeed - + ", difficulty=" + difficulty - + ", isHardcore=" + isHardcore - + ", maxPlayers=" + maxPlayers - + ", levelType='" + levelType + '\'' - + ", viewDistance=" + viewDistance - + ", reducedDebugInfo=" + reducedDebugInfo - + ", showRespawnScreen=" + showRespawnScreen - + ", levelNames=" + levelNames - + ", registry='" + registry + '\'' - + ", dimensionInfo='" + dimensionInfo + '\'' - + ", currentDimensionData='" + currentDimensionData + '\'' - + ", previousGamemode=" + previousGamemode - + ", simulationDistance=" + simulationDistance - + ", lastDeathPosition='" + lastDeathPosition + '\'' - + ", portalCooldown=" + portalCooldown - + '}'; + return "JoinGame{" + "entityId=" + entityId + ", gamemode=" + gamemode + ", dimension=" + + dimension + ", partialHashedSeed=" + partialHashedSeed + ", difficulty=" + difficulty + + ", isHardcore=" + isHardcore + ", maxPlayers=" + maxPlayers + ", levelType='" + levelType + + '\'' + ", viewDistance=" + viewDistance + ", reducedDebugInfo=" + reducedDebugInfo + + ", showRespawnScreen=" + showRespawnScreen + ", doLimitedCrafting=" + doLimitedCrafting + + ", levelNames=" + levelNames + ", registry='" + registry + '\'' + ", dimensionInfo='" + + dimensionInfo + '\'' + ", currentDimensionData='" + currentDimensionData + '\'' + + ", previousGamemode=" + previousGamemode + ", simulationDistance=" + simulationDistance + + ", lastDeathPosition='" + lastDeathPosition + '\'' + ", portalCooldown=" + portalCooldown + + '}'; } @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + // haha funny, they made 1.20.2 more complicated + this.decode1202Up(buf, version); + } else if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { // Minecraft 1.16 and above have significantly more complicated logic for reading this packet, // so separate it out. this.decode116Up(buf, version); @@ -295,9 +296,46 @@ public class JoinGame implements MinecraftPacket { } } + private void decode1202Up(ByteBuf buf, ProtocolVersion version) { + this.entityId = buf.readInt(); + this.isHardcore = buf.readBoolean(); + + this.levelNames = ImmutableSet.copyOf(ProtocolUtils.readStringArray(buf)); + + this.maxPlayers = ProtocolUtils.readVarInt(buf); + + this.viewDistance = ProtocolUtils.readVarInt(buf); + this.simulationDistance = ProtocolUtils.readVarInt(buf); + + this.reducedDebugInfo = buf.readBoolean(); + this.showRespawnScreen = buf.readBoolean(); + this.doLimitedCrafting = buf.readBoolean(); + + String dimensionIdentifier = ProtocolUtils.readString(buf); + String levelName = ProtocolUtils.readString(buf); + this.partialHashedSeed = buf.readLong(); + + this.gamemode = buf.readByte(); + this.previousGamemode = buf.readByte(); + + boolean isDebug = buf.readBoolean(); + boolean isFlat = buf.readBoolean(); + this.dimensionInfo = new DimensionInfo(dimensionIdentifier, levelName, isFlat, isDebug); + + // optional death location + if (buf.readBoolean()) { + this.lastDeathPosition = Pair.of(ProtocolUtils.readString(buf), buf.readLong()); + } + + this.portalCooldown = ProtocolUtils.readVarInt(buf); + } + @Override public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + // haha funny, they made 1.20.2 more complicated + this.encode1202Up(buf, version); + } else if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { // Minecraft 1.16 and above have significantly more complicated logic for reading this packet, // so separate it out. this.encode116Up(buf, version); @@ -396,6 +434,43 @@ public class JoinGame implements MinecraftPacket { } } + private void encode1202Up(ByteBuf buf, ProtocolVersion version) { + buf.writeInt(entityId); + buf.writeBoolean(isHardcore); + + ProtocolUtils.writeStringArray(buf, levelNames.toArray(String[]::new)); + + ProtocolUtils.writeVarInt(buf, maxPlayers); + + ProtocolUtils.writeVarInt(buf, viewDistance); + ProtocolUtils.writeVarInt(buf, simulationDistance); + + buf.writeBoolean(reducedDebugInfo); + buf.writeBoolean(showRespawnScreen); + buf.writeBoolean(doLimitedCrafting); + + ProtocolUtils.writeString(buf, dimensionInfo.getRegistryIdentifier()); + ProtocolUtils.writeString(buf, dimensionInfo.getLevelName()); + buf.writeLong(partialHashedSeed); + + buf.writeByte(gamemode); + buf.writeByte(previousGamemode); + + buf.writeBoolean(dimensionInfo.isDebugType()); + buf.writeBoolean(dimensionInfo.isFlat()); + + // optional death location + if (lastDeathPosition != null) { + buf.writeBoolean(true); + ProtocolUtils.writeString(buf, lastDeathPosition.key()); + buf.writeLong(lastDeathPosition.value()); + } else { + buf.writeBoolean(false); + } + + ProtocolUtils.writeVarInt(buf, portalCooldown); + } + @Override public boolean handle(MinecraftSessionHandler handler) { return handler.handle(this); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledged.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledged.java new file mode 100644 index 000000000..1f7941004 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledged.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 Velocity Contributors + * + * 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 . + */ + +package com.velocitypowered.proxy.protocol.packet; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class LoginAcknowledged implements MinecraftPacket { + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + return 0; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PingIdentify.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PingIdentify.java new file mode 100644 index 000000000..2d3d9b5da --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PingIdentify.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2018-2021 Velocity Contributors + * + * 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 . + */ + +package com.velocitypowered.proxy.protocol.packet; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class PingIdentify implements MinecraftPacket { + + private int id; + + @Override + public String toString() { + return "Ping{" + "id=" + id + '}'; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + id = buf.readInt(); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + buf.writeInt(id); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java index 641407f9f..c80b97c16 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java @@ -17,17 +17,23 @@ package com.velocitypowered.proxy.protocol.packet; +import com.google.common.base.Preconditions; import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.regex.Pattern; + public class ResourcePackRequest implements MinecraftPacket { private @MonotonicNonNull String url; @@ -35,6 +41,8 @@ public class ResourcePackRequest implements MinecraftPacket { private boolean isRequired; // 1.17+ private @Nullable Component prompt; // 1.17+ + private static final Pattern PLAUSIBLE_SHA1_HASH = Pattern.compile("^[a-z0-9]{40}$"); // 1.20.2+ + public @Nullable String getUrl() { return url; } @@ -99,6 +107,19 @@ public class ResourcePackRequest implements MinecraftPacket { } } + public VelocityResourcePackInfo toServerPromptedPack() { + ResourcePackInfo.Builder builder = + new VelocityResourcePackInfo.BuilderImpl(Preconditions.checkNotNull(url)).setPrompt(prompt) + .setShouldForce(isRequired).setOrigin(ResourcePackInfo.Origin.DOWNSTREAM_SERVER); + + if (hash != null && !hash.isEmpty()) { + if (PLAUSIBLE_SHA1_HASH.matcher(hash).matches()) { + builder.setHash(ByteBufUtil.decodeHexDump(hash)); + } + } + return (VelocityResourcePackInfo) builder.build(); + } + @Override public boolean handle(MinecraftSessionHandler handler) { return handler.handle(this); @@ -106,11 +127,7 @@ public class ResourcePackRequest implements MinecraftPacket { @Override public String toString() { - return "ResourcePackRequest{" - + "url='" + url + '\'' - + ", hash='" + hash + '\'' - + ", isRequired=" + isRequired - + ", prompt='" + prompt + '\'' - + '}'; + return "ResourcePackRequest{" + "url='" + url + '\'' + ", hash='" + hash + '\'' + + ", isRequired=" + isRequired + ", prompt='" + prompt + '\'' + '}'; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java index 3b7c93608..4b67cf695 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java @@ -73,9 +73,6 @@ public class ResourcePackResponse implements MinecraftPacket { @Override public String toString() { - return "ResourcePackResponse{" - + "hash=" + hash + ", " - + "status=" + status - + '}'; + return "ResourcePackResponse{" + "hash=" + hash + ", " + "status=" + status + '}'; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java index 0cd589ee9..57c8c61c2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java @@ -74,10 +74,7 @@ public class ServerLogin implements MinecraftPacket { @Override public String toString() { - return "ServerLogin{" - + "username='" + username + '\'' - + "playerKey='" + playerKey + '\'' - + '}'; + return "ServerLogin{" + "username='" + username + '\'' + "playerKey='" + playerKey + '\'' + '}'; } @Override @@ -98,6 +95,11 @@ public class ServerLogin implements MinecraftPacket { } } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + this.holderUuid = ProtocolUtils.readUuid(buf); + return; + } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) { if (buf.readBoolean()) { holderUuid = ProtocolUtils.readUuid(buf); @@ -125,6 +127,11 @@ public class ServerLogin implements MinecraftPacket { } } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + ProtocolUtils.writeUuid(buf, this.holderUuid); + return; + } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) { if (playerKey != null && playerKey.getSignatureHolder() != null) { buf.writeBoolean(true); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ActiveFeatures.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ActiveFeatures.java new file mode 100644 index 000000000..de2a5047c --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ActiveFeatures.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * 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 . + */ + +package com.velocitypowered.proxy.protocol.packet.config; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import net.kyori.adventure.key.Key; + +public class ActiveFeatures implements MinecraftPacket { + + private Key[] activeFeatures; + + public ActiveFeatures(Key[] activeFeatures) { + this.activeFeatures = activeFeatures; + } + + public ActiveFeatures() { + this.activeFeatures = new Key[0]; + } + + public void setActiveFeatures(Key[] activeFeatures) { + this.activeFeatures = activeFeatures; + } + + public Key[] getActiveFeatures() { + return activeFeatures; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + activeFeatures = ProtocolUtils.readKeyArray(buf); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + ProtocolUtils.writeKeyArray(buf, activeFeatures); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/FinishedUpdate.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/FinishedUpdate.java new file mode 100644 index 000000000..866819b48 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/FinishedUpdate.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * 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 . + */ + +package com.velocitypowered.proxy.protocol.packet.config; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class FinishedUpdate implements MinecraftPacket { + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + return 0; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/RegistrySync.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/RegistrySync.java new file mode 100644 index 000000000..74c275401 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/RegistrySync.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * 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 . + */ + +package com.velocitypowered.proxy.protocol.packet.config; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder; +import io.netty.buffer.ByteBuf; + +public class RegistrySync extends DeferredByteBufHolder implements MinecraftPacket { + + public RegistrySync() { + super(null); + } + + // NBT change in 1.20.2 makes it difficult to parse this packet. + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + this.replace(buf.readRetainedSlice(buf.readableBytes())); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + buf.writeBytes(content()); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/StartUpdate.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/StartUpdate.java new file mode 100644 index 000000000..d1c25fa27 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/StartUpdate.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * 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 . + */ + +package com.velocitypowered.proxy.protocol.packet.config; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class StartUpdate implements MinecraftPacket { + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + return 0; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/TagsUpdate.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/TagsUpdate.java new file mode 100644 index 000000000..79b432158 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/TagsUpdate.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * 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 . + */ + +package com.velocitypowered.proxy.protocol.packet.config; + +import com.google.common.collect.ImmutableMap; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +import java.util.Map; + +public class TagsUpdate implements MinecraftPacket { + + private Map> tags; + + public TagsUpdate(Map> tags) { + this.tags = tags; + } + + public TagsUpdate() { + this.tags = Map.of(); + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + ImmutableMap.Builder> builder = ImmutableMap.builder(); + int size = ProtocolUtils.readVarInt(buf); + for (int i = 0; i < size; i++) { + String key = ProtocolUtils.readString(buf); + + int innerSize = ProtocolUtils.readVarInt(buf); + ImmutableMap.Builder innerBuilder = ImmutableMap.builder(); + for (int j = 0; j < innerSize; j++) { + String innerKey = ProtocolUtils.readString(buf); + int[] innerValue = ProtocolUtils.readVarIntArray(buf); + innerBuilder.put(innerKey, innerValue); + } + + builder.put(key, innerBuilder.build()); + } + tags = builder.build(); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + ProtocolUtils.writeVarInt(buf, tags.size()); + for (Map.Entry> entry : tags.entrySet()) { + ProtocolUtils.writeString(buf, entry.getKey()); + // Oh, joy + ProtocolUtils.writeVarInt(buf, entry.getValue().size()); + for (Map.Entry innerEntry : entry.getValue().entrySet()) { + // Yea, object oriented programming be damned + ProtocolUtils.writeString(buf, innerEntry.getKey()); + ProtocolUtils.writeVarIntArray(buf, innerEntry.getValue()); + } + } + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java index 7bafc99b7..4ed43e86b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java @@ -32,8 +32,8 @@ import java.io.IOException; import java.util.concurrent.CompletableFuture; /** - * Session handler used to implement - * {@link VelocityRegisteredServer#ping(EventLoop, ProtocolVersion)}. + * Session handler used to implement {@link VelocityRegisteredServer#ping(EventLoop, + * ProtocolVersion)}. */ public class PingSessionHandler implements MinecraftSessionHandler { @@ -60,6 +60,7 @@ public class PingSessionHandler implements MinecraftSessionHandler { handshake.setProtocolVersion(version); connection.delayedWrite(handshake); + connection.setActiveSessionHandler(StateRegistry.STATUS); connection.setState(StateRegistry.STATUS); connection.delayedWrite(StatusRequest.INSTANCE); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java index 57780110b..928b4ee72 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java @@ -37,6 +37,7 @@ import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; @@ -94,8 +95,8 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud } /** - * Pings the specified server using the specified event {@code loop}, claiming to be - * {@code version}. + * Pings the specified server using the specified event {@code loop}, claiming to be {@code + * version}. * * @param loop the event loop to use * @param pingOptions the options to apply to this ping @@ -106,35 +107,30 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud throw new IllegalStateException("No Velocity proxy instance available"); } CompletableFuture pingFuture = new CompletableFuture<>(); - server.createBootstrap(loop) - .handler(new ChannelInitializer() { - @Override - protected void initChannel(Channel ch) throws Exception { - ch.pipeline() - .addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) - .addLast(READ_TIMEOUT, - new ReadTimeoutHandler(pingOptions.getTimeout() == 0 - ? server.getConfiguration().getReadTimeout() : pingOptions.getTimeout(), - TimeUnit.MILLISECONDS)) - .addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE) - .addLast(MINECRAFT_DECODER, - new MinecraftDecoder(ProtocolUtils.Direction.CLIENTBOUND)) - .addLast(MINECRAFT_ENCODER, - new MinecraftEncoder(ProtocolUtils.Direction.SERVERBOUND)); + server.createBootstrap(loop).handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline().addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) + .addLast(READ_TIMEOUT, new ReadTimeoutHandler( + pingOptions.getTimeout() == 0 + ? server.getConfiguration().getReadTimeout() + : pingOptions.getTimeout(), TimeUnit.MILLISECONDS)) + .addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE) + .addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolUtils.Direction.CLIENTBOUND)) + .addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolUtils.Direction.SERVERBOUND)); - ch.pipeline().addLast(HANDLER, new MinecraftConnection(ch, server)); - } - }) - .connect(serverInfo.getAddress()) - .addListener((ChannelFutureListener) future -> { - if (future.isSuccess()) { - MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class); - conn.setSessionHandler(new PingSessionHandler( - pingFuture, VelocityRegisteredServer.this, conn, pingOptions.getProtocolVersion())); - } else { - pingFuture.completeExceptionally(future.cause()); - } - }); + ch.pipeline().addLast(HANDLER, new MinecraftConnection(ch, server)); + } + }).connect(serverInfo.getAddress()).addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class); + conn.setActiveSessionHandler(StateRegistry.HANDSHAKE, + new PingSessionHandler(pingFuture, VelocityRegisteredServer.this, conn, + pingOptions.getProtocolVersion())); + } else { + pingFuture.completeExceptionally(future.cause()); + } + }); return pingFuture; } From 550ca58a0956255b317dfac68ffbdb13dbb80785 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Tue, 10 Oct 2023 18:05:05 +0100 Subject: [PATCH 15/34] Actually send plugin message registration to backend servers I don't see where this was ever done, and don't see how plugin messaging could of ever worked, at least within the confines of CB and co, given the fact that we never seemed to be sending this to the backend? --- .../backend/BackendPlaySessionHandler.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index ffa0c8391..5a42d2922 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -18,6 +18,7 @@ package com.velocitypowered.proxy.connection.backend; import static com.velocitypowered.proxy.connection.backend.BungeeCordMessageResponder.getBungeeCordChannel; +import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.constructChannelsPacket; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @@ -61,6 +62,8 @@ import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.handler.timeout.ReadTimeoutException; + +import java.util.Collection; import java.util.regex.Pattern; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -105,12 +108,20 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { public void activated() { serverConn.getServer().addPlayer(serverConn.getPlayer()); + MinecraftConnection serverMc = serverConn.ensureConnected(); if (server.getConfiguration().isBungeePluginChannelEnabled()) { - MinecraftConnection serverMc = serverConn.ensureConnected(); serverMc.write(PluginMessageUtil.constructChannelsPacket(serverMc.getProtocolVersion(), ImmutableList.of(getBungeeCordChannel(serverMc.getProtocolVersion())) )); } + + Collection channels = + server.getChannelRegistrar().getChannelsForProtocol(serverConn.getPlayer().getProtocolVersion()); + if (!channels.isEmpty()) { + PluginMessage register = constructChannelsPacket(serverConn.getPlayer().getProtocolVersion(), channels); + serverMc.write(register); + } + } @Override From 05e596366030f04826380221c88fb47b45db9ef7 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Tue, 10 Oct 2023 18:21:02 +0100 Subject: [PATCH 16/34] appease checkstyle, move back to older fix placement --- .../connection/backend/BackendPlaySessionHandler.java | 10 ---------- .../connection/client/ClientPlaySessionHandler.java | 8 ++++++++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index 5a42d2922..c21ae6ecd 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -18,7 +18,6 @@ package com.velocitypowered.proxy.connection.backend; import static com.velocitypowered.proxy.connection.backend.BungeeCordMessageResponder.getBungeeCordChannel; -import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.constructChannelsPacket; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @@ -62,8 +61,6 @@ import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.handler.timeout.ReadTimeoutException; - -import java.util.Collection; import java.util.regex.Pattern; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -115,13 +112,6 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { )); } - Collection channels = - server.getChannelRegistrar().getChannelsForProtocol(serverConn.getPlayer().getProtocolVersion()); - if (!channels.isEmpty()) { - PluginMessage register = constructChannelsPacket(serverConn.getPlayer().getProtocolVersion(), channels); - serverMc.write(register); - } - } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index d2da3aa8e..304fbbaae 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -535,6 +535,14 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } serverBossBars.clear(); + // Tell the server about the proxy's plugin message channels. + ProtocolVersion serverVersion = serverMc.getProtocolVersion(); + final Collection channels = server.getChannelRegistrar() + .getChannelsForProtocol(serverMc.getProtocolVersion()); + if (!channels.isEmpty()) { + serverMc.delayedWrite(constructChannelsPacket(serverVersion, channels)); + } + // If we had plugin messages queued during login/FML handshake, send them now. PluginMessage pm; while ((pm = loginPluginMessages.poll()) != null) { From 2bd2c692ea0294aaa77b8e5653d9d3344c30e487 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Tue, 10 Oct 2023 18:43:59 +0100 Subject: [PATCH 17/34] Change packet decode logging prompt --- .../proxy/protocol/netty/MinecraftDecoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java index b60fbbbc3..f8362cc09 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java @@ -35,8 +35,8 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter { public static final boolean DEBUG = Boolean.getBoolean("velocity.packet-decode-logging"); private static final QuietRuntimeException DECODE_FAILED = - new QuietRuntimeException("A packet did not decode successfully (invalid data). If you are a " - + "developer, launch Velocity with -Dvelocity.packet-decode-logging=true to see more."); + new QuietRuntimeException("A packet did not decode successfully (invalid data). For more " + + "information, launch Velocity with -Dvelocity.packet-decode-logging=true to see more."); private final ProtocolUtils.Direction direction; private StateRegistry state; From 9be0bca3c428bebba972a0a2a42b2a3952055d6e Mon Sep 17 00:00:00 2001 From: Joo200 Date: Wed, 11 Oct 2023 17:24:35 +0200 Subject: [PATCH 18/34] Catch throwables when loading plugins (#1098) --- .../velocitypowered/proxy/plugin/VelocityPluginManager.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java index a862a2b53..2c6f752f7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java @@ -94,7 +94,7 @@ public class VelocityPluginManager implements PluginManager { for (Path path : stream) { try { found.add(loader.loadCandidate(path)); - } catch (Exception e) { + } catch (Throwable e) { logger.error("Unable to load plugin {}", path, e); } } @@ -126,7 +126,7 @@ public class VelocityPluginManager implements PluginManager { VelocityPluginContainer container = new VelocityPluginContainer(realPlugin); pluginContainers.put(container, loader.createModule(container)); loadedPluginsById.add(realPlugin.getId()); - } catch (Exception e) { + } catch (Throwable e) { logger.error("Can't create module for plugin {}", candidate.getId(), e); } } @@ -153,7 +153,7 @@ public class VelocityPluginManager implements PluginManager { try { loader.createPlugin(container, plugin.getValue(), commonModule); - } catch (Exception e) { + } catch (Throwable e) { logger.error("Can't create plugin {}", description.getId(), e); continue; } From 1cc3f120ee3f46e1fc5513c9988ff6370812f1ee Mon Sep 17 00:00:00 2001 From: Gero Date: Thu, 12 Oct 2023 11:11:47 +0200 Subject: [PATCH 19/34] Several improvements and fixes for 1.20.2 (#1097) * Send LoginAcknowledged immediately * Resend player list header/footer after backend server switched to config state * Fix clearHeaderAndFooter not clearing fields in ConnectedPlayer * Clear boss bars, header/footer, tab list when switching client to config state * Send client settings in config state --- .../com/velocitypowered/api/proxy/Player.java | 12 ++++++-- .../backend/ConfigSessionHandler.java | 15 ++++++---- .../backend/LoginSessionHandler.java | 28 +++++++++---------- .../backend/TransitionSessionHandler.java | 7 +++-- .../client/ClientPlaySessionHandler.java | 9 +++--- .../connection/client/ConnectedPlayer.java | 12 ++++++-- .../proxy/tablist/InternalTabList.java | 2 ++ .../proxy/tablist/KeyedVelocityTabList.java | 10 +++++-- .../proxy/tablist/VelocityTabList.java | 8 ++++-- .../proxy/tablist/VelocityTabListLegacy.java | 5 ++++ 10 files changed, 72 insertions(+), 36 deletions(-) diff --git a/api/src/main/java/com/velocitypowered/api/proxy/Player.java b/api/src/main/java/com/velocitypowered/api/proxy/Player.java index 9382fec22..99bf09da0 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -31,7 +31,6 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.event.HoverEventSource; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.NotNull; /** @@ -148,10 +147,17 @@ public interface Player extends /** * Clears the tab list header and footer for the player. * - * @deprecated Use {@link TabList#clearHeaderAndFooter()}. + * @deprecated Use {@link Player#clearPlayerListHeaderAndFooter()}. */ @Deprecated - void clearHeaderAndFooter(); + default void clearHeaderAndFooter() { + clearPlayerListHeaderAndFooter(); + } + + /** + * Clears the player list header and footer. + */ + void clearPlayerListHeaderAndFooter(); /** * Returns the player's player list header. diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java index d84ae49ce..7374f7985 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java @@ -24,6 +24,7 @@ import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.client.ClientConfigSessionHandler; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; @@ -152,22 +153,26 @@ public class ConfigSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(FinishedUpdate packet) { MinecraftConnection smc = serverConn.ensureConnected(); + ConnectedPlayer player = serverConn.getPlayer(); ClientConfigSessionHandler configHandler = - (ClientConfigSessionHandler) serverConn.getPlayer().getConnection() - .getActiveSessionHandler(); + (ClientConfigSessionHandler) player.getConnection().getActiveSessionHandler(); smc.setAutoReading(false); // Even when not auto reading messages are still decoded. Decode them with the correct state smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.PLAY); configHandler.handleBackendFinishUpdate(serverConn).thenAcceptAsync((unused) -> { - if (serverConn == serverConn.getPlayer().getConnectedServer()) { + if (serverConn == player.getConnectedServer()) { smc.setActiveSessionHandler(StateRegistry.PLAY); + player.sendPlayerListHeaderAndFooter( + player.getPlayerListHeader(), player.getPlayerListFooter()); + // The client cleared the tab list. TODO: Restore changes done via TabList API + player.getTabList().clearAllSilent(); } else { smc.setActiveSessionHandler(StateRegistry.PLAY, new TransitionSessionHandler(server, serverConn, resultFuture)); } - if (serverConn.getPlayer().getAppliedResourcePack() == null && resourcePackToApply != null) { - serverConn.getPlayer().queueResourcePack(resourcePackToApply); + if (player.getAppliedResourcePack() == null && resourcePackToApply != null) { + player.queueResourcePack(resourcePackToApply); } smc.setAutoReading(true); }, smc.eventLoop()); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java index e9801c315..10079db72 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java @@ -156,22 +156,20 @@ public class LoginSessionHandler implements MinecraftSessionHandler { smc.setActiveSessionHandler(StateRegistry.PLAY, new TransitionSessionHandler(server, serverConn, resultFuture)); } else { - smc.setAutoReading(false); - CompletableFuture switchFuture; - if (serverConn.getPlayer().getConnection() - .getActiveSessionHandler() instanceof ClientPlaySessionHandler) { - switchFuture = ((ClientPlaySessionHandler) serverConn.getPlayer().getConnection() - .getActiveSessionHandler()).doSwitch(); - } else { - switchFuture = CompletableFuture.completedFuture(null); + smc.write(new LoginAcknowledged()); + smc.setActiveSessionHandler(StateRegistry.CONFIG, + new ConfigSessionHandler(server, serverConn, resultFuture)); + ConnectedPlayer player = serverConn.getPlayer(); + if (player.getClientSettingsPacket() != null) { + smc.write(player.getClientSettingsPacket()); + } + if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler) { + smc.setAutoReading(false); + ((ClientPlaySessionHandler) player.getConnection() + .getActiveSessionHandler()).doSwitch().thenAcceptAsync((unused) -> { + smc.setAutoReading(true); + }, smc.eventLoop()); } - switchFuture.thenAcceptAsync((unused) -> { - smc.write(new LoginAcknowledged()); - // Sync backend - smc.setActiveSessionHandler(StateRegistry.CONFIG, - new ConfigSessionHandler(server, serverConn, resultFuture)); - smc.setAutoReading(true); - }, smc.eventLoop()); } return true; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java index 30d1f4242..0f32fbefb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java @@ -22,6 +22,7 @@ import static com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeHands import com.velocitypowered.api.event.player.ServerConnectedEvent; import com.velocitypowered.api.event.player.ServerPostConnectEvent; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.ConnectionTypes; @@ -104,7 +105,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler { player.sendKeepAlive(); // Reset Tablist header and footer to prevent desync - player.clearHeaderAndFooter(); + player.clearPlayerListHeaderAndFooter(); } // The goods are in hand! We got JoinGame. Let's transition completely to the new state. @@ -143,7 +144,9 @@ public class TransitionSessionHandler implements MinecraftSessionHandler { // Now set the connected server. serverConn.getPlayer().setConnectedServer(serverConn); - if (player.getClientSettingsPacket() != null) { + // Send client settings. In 1.20.2+ this is done in the config state. + if (smc.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0 + && player.getClientSettingsPacket() != null) { serverConn.ensureConnected().write(player.getClientSettingsPacket()); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 304fbbaae..6004cf834 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -482,12 +482,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // Send keep alive to try to avoid timeouts player.sendKeepAlive(); - // Reset Tablist header and footer to prevent desync - player.clearHeaderAndFooter(); + // Config state clears everything in the client. No need to clear later. + spawned = false; + serverBossBars.clear(); + player.clearPlayerListHeaderAndFooterSilent(); + player.getTabList().clearAllSilent(); } - spawned = false; - player.switchToConfigState(); return configSwitchFuture; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index c922e933a..f7e67cc37 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -544,8 +544,16 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } @Override - public void clearHeaderAndFooter() { - tabList.clearHeaderAndFooter(); + public void clearPlayerListHeaderAndFooter() { + clearPlayerListHeaderAndFooterSilent(); + if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { + this.connection.write(HeaderAndFooter.reset()); + } + } + + public void clearPlayerListHeaderAndFooterSilent() { + this.playerListHeader = Component.empty(); + this.playerListFooter = Component.empty(); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/InternalTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/InternalTabList.java index f3add2290..dcf00ecae 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/InternalTabList.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/InternalTabList.java @@ -35,4 +35,6 @@ public interface InternalTabList extends TabList { default void processRemove(RemovePlayerInfo infoPacket) { } + + void clearAllSilent(); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java index 63a9ae56f..e24c337ed 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java @@ -26,7 +26,6 @@ import com.velocitypowered.api.proxy.player.TabListEntry; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; import com.velocitypowered.proxy.protocol.packet.chat.RemoteChatSession; import java.util.ArrayList; @@ -70,7 +69,7 @@ public class KeyedVelocityTabList implements InternalTabList { @Override public void clearHeaderAndFooter() { - connection.write(HeaderAndFooter.reset()); + this.player.clearPlayerListHeaderAndFooter(); } @Override @@ -131,10 +130,15 @@ public class KeyedVelocityTabList implements InternalTabList { for (TabListEntry value : listEntries) { items.add(LegacyPlayerListItem.Item.from(value)); } - entries.clear(); + clearAllSilent(); connection.delayedWrite(new LegacyPlayerListItem(LegacyPlayerListItem.REMOVE_PLAYER, items)); } + @Override + public void clearAllSilent() { + entries.clear(); + } + @Override public Collection getEntries() { return Collections.unmodifiableCollection(this.entries.values()); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java index f707c95ca..4a95b00de 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java @@ -25,7 +25,6 @@ import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.console.VelocityConsole; -import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; import com.velocitypowered.proxy.protocol.packet.chat.RemoteChatSession; @@ -74,7 +73,7 @@ public class VelocityTabList implements InternalTabList { @Override public void clearHeaderAndFooter() { - connection.write(HeaderAndFooter.reset()); + this.player.clearPlayerListHeaderAndFooter(); } @Override @@ -175,6 +174,11 @@ public class VelocityTabList implements InternalTabList { @Override public void clearAll() { this.connection.delayedWrite(new RemovePlayerInfo(new ArrayList<>(this.entries.keySet()))); + clearAllSilent(); + } + + @Override + public void clearAllSilent() { this.entries.clear(); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java index d23ac4cb5..6e1777788 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java @@ -71,6 +71,11 @@ public class VelocityTabListLegacy extends KeyedVelocityTabList { connection.delayedWrite(new LegacyPlayerListItem(LegacyPlayerListItem.REMOVE_PLAYER, Collections.singletonList(LegacyPlayerListItem.Item.from(value)))); } + clearAllSilent(); + } + + @Override + public void clearAllSilent() { entries.clear(); nameMapping.clear(); } From 0d9a097a59a8087d976c5ffac7ff0553bac7f36f Mon Sep 17 00:00:00 2001 From: Joo200 Date: Thu, 12 Oct 2023 16:51:19 +0200 Subject: [PATCH 20/34] Fix unsigned commands detected as signed (#1082) * fix: commands flagged as signed without signed arguments * feat: improve error message for illegal protocol state. --- .../packet/chat/LastSeenMessages.java | 8 ++++++ .../chat/keyed/KeyedCommandHandler.java | 6 ++--- .../chat/session/SessionCommandHandler.java | 6 ++--- .../chat/session/SessionPlayerCommand.java | 27 ++++++++++++++++++- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java index fa3113af2..a6f791232 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java @@ -49,4 +49,12 @@ public class LastSeenMessages { public boolean isEmpty() { return acknowledged.isEmpty(); } + + @Override + public String toString() { + return "LastSeenMessages{" + + "offset=" + offset + + ", acknowledged=" + acknowledged + + '}'; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java index 8aa1051aa..193371aad 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java @@ -52,7 +52,7 @@ public class KeyedCommandHandler implements CommandHandler { && playerKey.getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { logger.fatal("A plugin tried to deny a command with signable component(s). " + "This is not supported. " - + "Disconnecting player " + player.getUsername()); + + "Disconnecting player " + player.getUsername() + ". Command packet: " + packet); player.disconnect(Component.text( "A proxy plugin caused an illegal protocol state. " + "Contact your network administrator.")); @@ -75,7 +75,7 @@ public class KeyedCommandHandler implements CommandHandler { && playerKey.getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { logger.fatal("A plugin tried to change a command with signed component(s). " + "This is not supported. " - + "Disconnecting player " + player.getUsername()); + + "Disconnecting player " + player.getUsername() + ". Command packet: " + packet); player.disconnect(Component.text( "A proxy plugin caused an illegal protocol state. " + "Contact your network administrator.")); @@ -95,7 +95,7 @@ public class KeyedCommandHandler implements CommandHandler { && playerKey.getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { logger.fatal("A plugin tried to change a command with signed component(s). " + "This is not supported. " - + "Disconnecting player " + player.getUsername()); + + "Disconnecting player " + player.getUsername() + ". Command packet: " + packet); player.disconnect(Component.text( "A proxy plugin caused an illegal protocol state. " + "Contact your network administrator.")); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java index 782c78357..b963ac2ec 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java @@ -47,7 +47,7 @@ public class SessionCommandHandler implements CommandHandler Date: Thu, 12 Oct 2023 17:22:42 +0100 Subject: [PATCH 21/34] acknowledge seen messages to server when running proxy commands (#1100) --- .../connection/MinecraftSessionHandler.java | 5 ++ .../proxy/protocol/StateRegistry.java | 5 ++ .../packet/chat/ChatAcknowledgement.java | 57 +++++++++++++++++++ .../packet/chat/LastSeenMessages.java | 4 ++ .../chat/session/SessionCommandHandler.java | 9 +++ 5 files changed, 80 insertions(+) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatAcknowledgement.java diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java index ea01eb008..189820e04 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -50,6 +50,7 @@ import com.velocitypowered.proxy.protocol.packet.StatusResponse; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgement; import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletion; import com.velocitypowered.proxy.protocol.packet.chat.SystemChat; import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerChat; @@ -314,4 +315,8 @@ public interface MinecraftSessionHandler { default boolean handle(PingIdentify pingIdentify) { return false; } + + default boolean handle(ChatAcknowledgement chatAcknowledgement) { + return false; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index 8845dbeae..40d6d015b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -75,6 +75,7 @@ import com.velocitypowered.proxy.protocol.packet.StatusResponse; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgement; import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletion; import com.velocitypowered.proxy.protocol.packet.chat.SystemChat; import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerChat; @@ -184,6 +185,10 @@ public enum StateRegistry { map(0x03, MINECRAFT_1_12, false), map(0x02, MINECRAFT_1_12_1, false), map(0x03, MINECRAFT_1_14, MINECRAFT_1_18_2, false)); + serverbound.register( + ChatAcknowledgement.class, + ChatAcknowledgement::new, + map(0x03, MINECRAFT_1_19_3, false)); serverbound.register(KeyedPlayerCommand.class, KeyedPlayerCommand::new, map(0x03, MINECRAFT_1_19, false), map(0x04, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false)); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatAcknowledgement.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatAcknowledgement.java new file mode 100644 index 000000000..f1f9ad242 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatAcknowledgement.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2023 Velocity Contributors + * + * 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 . + */ + +package com.velocitypowered.proxy.protocol.packet.chat; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class ChatAcknowledgement implements MinecraftPacket { + int offset; + + public ChatAcknowledgement(int offset) { + this.offset = offset; + } + + public ChatAcknowledgement() { + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + offset = ProtocolUtils.readVarInt(buf); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + ProtocolUtils.writeVarInt(buf, offset); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } + + @Override + public String toString() { + return "ChatAcknowledgement{" + + "offset=" + offset + + '}'; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java index a6f791232..a1ed539d6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java @@ -50,6 +50,10 @@ public class LastSeenMessages { return acknowledged.isEmpty(); } + public int getOffset() { + return this.offset; + } + @Override public String toString() { return "LastSeenMessages{" + diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java index b963ac2ec..558531b72 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java @@ -18,8 +18,10 @@ package com.velocitypowered.proxy.protocol.packet.chat.session; import com.velocitypowered.api.event.command.CommandExecuteEvent; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgement; import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; import java.util.concurrent.CompletableFuture; import net.kyori.adventure.text.Component; @@ -52,6 +54,10 @@ public class SessionCommandHandler implements CommandHandler= 0) { + return CompletableFuture.completedFuture(new ChatAcknowledgement(packet.lastSeenMessages.getOffset())); + } return CompletableFuture.completedFuture(null); } @@ -102,6 +108,9 @@ public class SessionCommandHandler implements CommandHandler= 0) { + return new ChatAcknowledgement(packet.lastSeenMessages.getOffset()); + } return null; }); }, packet.command, packet.timeStamp); From eb594fc799281ff418dc2c161c2d8a8eb0c89a19 Mon Sep 17 00:00:00 2001 From: Adrian <68704415+4drian3d@users.noreply.github.com> Date: Thu, 12 Oct 2023 12:44:10 -0500 Subject: [PATCH 22/34] Implement ComponentLogger (#1022) --- api/build.gradle.kts | 2 + gradle/libs.versions.toml | 5 +- proxy/build.gradle.kts | 1 + .../connection/client/ConnectedPlayer.java | 8 +-- .../proxy/console/VelocityConsole.java | 9 ++- .../loader/java/VelocityPluginModule.java | 2 + .../provider/ComponentLoggerProviderImpl.java | 43 ++++++++++++ .../proxy/util/TranslatableMapper.java | 69 +++++++++++++++++++ ....text.logger.slf4j.ComponentLoggerProvider | 1 + 9 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/provider/ComponentLoggerProviderImpl.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/util/TranslatableMapper.java create mode 100644 proxy/src/main/resources/META-INF/services/net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 1e448d0c2..c5a96101e 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -29,6 +29,8 @@ dependencies { api("net.kyori:adventure-text-serializer-legacy") api("net.kyori:adventure-text-serializer-plain") api("net.kyori:adventure-text-minimessage") + api("net.kyori:adventure-text-logger-slf4j") + api("net.kyori:adventure-text-serializer-ansi") api(libs.slf4j) api(libs.guice) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d204ff616..7733baaa7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,13 +29,14 @@ flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = jline = "org.jline:jline-terminal-jansi:3.23.0" jopt = "net.sf.jopt-simple:jopt-simple:5.0.4" junit = "org.junit.jupiter:junit-jupiter:5.9.0" +kyori-ansi = "net.kyori:ansi:1.0.3" guava = "com.google.guava:guava:25.1-jre" gson = "com.google.code.gson:gson:2.10.1" guice = "com.google.inject:guice:6.0.0" lmbda = "org.lanternpowered:lmbda:2.0.0" log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } -log4j-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j-impl", version.ref = "log4j" } +log4j-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" } log4j-iostreams = { module = "org.apache.logging.log4j:log4j-iostreams", version.ref = "log4j" } log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" } mockito = "org.mockito:mockito-core:5.2.0" @@ -45,7 +46,7 @@ netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" } netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" } nightconfig = "com.electronwill.night-config:toml:3.6.6" -slf4j = "org.slf4j:slf4j-api:1.7.30" +slf4j = "org.slf4j:slf4j-api:2.0.7" spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.7.3" terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0" diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index 8d6ed5945..643d5eb78 100644 --- a/proxy/build.gradle.kts +++ b/proxy/build.gradle.kts @@ -93,6 +93,7 @@ dependencies { implementation(project(":velocity-native")) implementation(libs.bundles.log4j) + implementation(libs.kyori.ansi) implementation(libs.netty.codec) implementation(libs.netty.codec.haproxy) implementation(libs.netty.codec.http) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index f7e67cc37..aada98cf5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -79,6 +79,7 @@ import com.velocitypowered.proxy.tablist.VelocityTabList; import com.velocitypowered.proxy.tablist.VelocityTabListLegacy; import com.velocitypowered.proxy.util.ClosestLocaleMatcher; import com.velocitypowered.proxy.util.DurationUtils; +import com.velocitypowered.proxy.util.TranslatableMapper; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import java.net.InetSocketAddress; @@ -100,9 +101,6 @@ import net.kyori.adventure.platform.facet.FacetPointers; import net.kyori.adventure.platform.facet.FacetPointers.Type; import net.kyori.adventure.pointer.Pointers; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.KeybindComponent; -import net.kyori.adventure.text.TranslatableComponent; -import net.kyori.adventure.text.flattener.ComponentFlattener; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; @@ -125,9 +123,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private static final int MAX_PLUGIN_CHANNELS = 1024; private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE = - PlainTextComponentSerializer.builder().flattener( - ComponentFlattener.basic().toBuilder().mapper(KeybindComponent.class, c -> "") - .mapper(TranslatableComponent.class, TranslatableComponent::key).build()).build(); + PlainTextComponentSerializer.builder().flattener(TranslatableMapper.FLATTENER).build(); static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED; private static final Logger logger = LogManager.getLogger(ConnectedPlayer.class); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java index 08bc04c07..fe360d4d6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java @@ -35,8 +35,7 @@ import net.kyori.adventure.platform.facet.FacetPointers.Type; import net.kyori.adventure.pointer.Pointers; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import net.kyori.adventure.translation.GlobalTranslator; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; import net.minecrell.terminalconsole.SimpleTerminalConsole; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; @@ -55,6 +54,8 @@ import org.jline.reader.LineReaderBuilder; public final class VelocityConsole extends SimpleTerminalConsole implements ConsoleCommandSource { private static final Logger logger = LogManager.getLogger(VelocityConsole.class); + private static final ComponentLogger componentLogger = ComponentLogger + .logger(VelocityConsole.class); private final VelocityServer server; private PermissionFunction permissionFunction = ALWAYS_TRUE; @@ -72,9 +73,7 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons @Override public void sendMessage(@NonNull Identity identity, @NonNull Component message, @NonNull MessageType messageType) { - Component translated = GlobalTranslator.render(message, ClosestLocaleMatcher.INSTANCE - .lookupClosest(Locale.getDefault())); - logger.info(LegacyComponentSerializer.legacySection().serialize(translated)); + componentLogger.info(message); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java index 8e59816af..a6331dd48 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java @@ -26,6 +26,7 @@ import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; import java.nio.file.Path; import java.util.concurrent.ExecutorService; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,6 +50,7 @@ class VelocityPluginModule implements Module { binder.bind(description.getMainClass()).in(Scopes.SINGLETON); binder.bind(Logger.class).toInstance(LoggerFactory.getLogger(description.getId())); + binder.bind(ComponentLogger.class).toInstance(ComponentLogger.logger(description.getId())); binder.bind(Path.class).annotatedWith(DataDirectory.class) .toInstance(basePluginPath.resolve(description.getId())); binder.bind(PluginDescription.class).toInstance(description); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/provider/ComponentLoggerProviderImpl.java b/proxy/src/main/java/com/velocitypowered/proxy/provider/ComponentLoggerProviderImpl.java new file mode 100644 index 000000000..df29ca56f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/provider/ComponentLoggerProviderImpl.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 Velocity Contributors + * + * 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 . + */ + +package com.velocitypowered.proxy.provider; + +import com.velocitypowered.proxy.util.TranslatableMapper; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; +import net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider; +import net.kyori.adventure.text.serializer.ansi.ANSIComponentSerializer; +import org.jetbrains.annotations.NotNull; +import org.slf4j.LoggerFactory; + +/** + * Velocity ComponentLogger Provider. + */ +@SuppressWarnings("UnstableApiUsage") +public final class ComponentLoggerProviderImpl implements ComponentLoggerProvider { + private static final ANSIComponentSerializer SERIALIZER = ANSIComponentSerializer.builder() + .flattener(TranslatableMapper.FLATTENER) + .build(); + + @Override + public @NotNull ComponentLogger logger( + final @NotNull LoggerHelper helper, + final @NotNull String name + ) { + return helper.delegating(LoggerFactory.getLogger(name), SERIALIZER::serialize); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/TranslatableMapper.java b/proxy/src/main/java/com/velocitypowered/proxy/util/TranslatableMapper.java new file mode 100644 index 000000000..c2ca2366f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/TranslatableMapper.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 Velocity Contributors + * + * 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 . + */ + +package com.velocitypowered.proxy.util; + +import java.util.Locale; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TranslatableComponent; +import net.kyori.adventure.text.flattener.ComponentFlattener; +import net.kyori.adventure.translation.GlobalTranslator; +import net.kyori.adventure.translation.TranslationRegistry; +import net.kyori.adventure.translation.Translator; +import org.jetbrains.annotations.Nullable; + + +/** + * Velocity Translation Mapper. + */ +public enum TranslatableMapper implements BiConsumer> { + INSTANCE; + + public static final ComponentFlattener FLATTENER = ComponentFlattener.basic().toBuilder() + .complexMapper(TranslatableComponent.class, TranslatableMapper.INSTANCE) + .build(); + + @Override + public void accept( + final TranslatableComponent translatableComponent, + final Consumer componentConsumer + ) { + for (final Translator source : GlobalTranslator.translator().sources()) { + if (source instanceof TranslationRegistry + && ((TranslationRegistry) source).contains(translatableComponent.key())) { + componentConsumer.accept(GlobalTranslator.render(translatableComponent, + ClosestLocaleMatcher.INSTANCE.lookupClosest(Locale.getDefault()))); + return; + } + } + final @Nullable String fallback = translatableComponent.fallback(); + if (fallback == null) { + return; + } + for (final Translator source : GlobalTranslator.translator().sources()) { + if (source instanceof TranslationRegistry + && ((TranslationRegistry) source).contains(fallback)) { + componentConsumer.accept( + GlobalTranslator.render(Component.translatable(fallback), + ClosestLocaleMatcher.INSTANCE.lookupClosest(Locale.getDefault()))); + return; + } + } + } +} diff --git a/proxy/src/main/resources/META-INF/services/net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider b/proxy/src/main/resources/META-INF/services/net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider new file mode 100644 index 000000000..b95209776 --- /dev/null +++ b/proxy/src/main/resources/META-INF/services/net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider @@ -0,0 +1 @@ +com.velocitypowered.proxy.provider.ComponentLoggerProviderImpl \ No newline at end of file From f884e049c006f5fae14abaab9332ccaad9f001e0 Mon Sep 17 00:00:00 2001 From: Adrian <68704415+4drian3d@users.noreply.github.com> Date: Thu, 12 Oct 2023 15:29:04 -0500 Subject: [PATCH 23/34] Log the protocol phase in case of trying to obtain a packet id not existing in the phase (#1107) --- .../proxy/protocol/StateRegistry.java | 13 ++++++++----- .../proxy/protocol/PacketRegistryTest.java | 12 ++++++------ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index 40d6d015b..8ebb7eadc 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -550,8 +550,8 @@ public enum StateRegistry { public static final int STATUS_ID = 1; public static final int LOGIN_ID = 2; - protected final PacketRegistry clientbound = new PacketRegistry(CLIENTBOUND); - protected final PacketRegistry serverbound = new PacketRegistry(SERVERBOUND); + protected final PacketRegistry clientbound = new PacketRegistry(CLIENTBOUND, this); + protected final PacketRegistry serverbound = new PacketRegistry(SERVERBOUND, this); public StateRegistry.PacketRegistry.ProtocolRegistry getProtocolRegistry(Direction direction, ProtocolVersion version) { @@ -562,11 +562,13 @@ public enum StateRegistry { public static class PacketRegistry { private final Direction direction; + private final StateRegistry registry; private final Map versions; private boolean fallback = true; - PacketRegistry(Direction direction) { + PacketRegistry(Direction direction, StateRegistry registry) { this.direction = direction; + this.registry = registry; Map mutableVersions = new EnumMap<>(ProtocolVersion.class); for (ProtocolVersion version : ProtocolVersion.values()) { @@ -693,8 +695,9 @@ public enum StateRegistry { final int id = this.packetClassToId.getInt(packet.getClass()); if (id == Integer.MIN_VALUE) { throw new IllegalArgumentException(String.format( - "Unable to find id for packet of type %s in %s protocol %s", - packet.getClass().getName(), PacketRegistry.this.direction, this.version + "Unable to find id for packet of type %s in %s protocol %s phase %s", + packet.getClass().getName(), PacketRegistry.this.direction, + this.version, PacketRegistry.this.registry )); } return id; diff --git a/proxy/src/test/java/com/velocitypowered/proxy/protocol/PacketRegistryTest.java b/proxy/src/test/java/com/velocitypowered/proxy/protocol/PacketRegistryTest.java index 81a18e72c..05a83eea1 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/protocol/PacketRegistryTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/protocol/PacketRegistryTest.java @@ -44,7 +44,7 @@ class PacketRegistryTest { private StateRegistry.PacketRegistry setupRegistry() { StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry( - ProtocolUtils.Direction.CLIENTBOUND); + ProtocolUtils.Direction.CLIENTBOUND, StateRegistry.PLAY); registry.register(Handshake.class, Handshake::new, new StateRegistry.PacketMapping(0x01, MINECRAFT_1_8, null, false), new StateRegistry.PacketMapping(0x00, MINECRAFT_1_12, null, false), @@ -84,7 +84,7 @@ class PacketRegistryTest { @Test void failOnNoMappings() { StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry( - ProtocolUtils.Direction.CLIENTBOUND); + ProtocolUtils.Direction.CLIENTBOUND, StateRegistry.PLAY); assertThrows(IllegalArgumentException.class, () -> registry.register(Handshake.class, Handshake::new)); assertThrows(IllegalArgumentException.class, @@ -94,7 +94,7 @@ class PacketRegistryTest { @Test void failOnWrongOrder() { StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry( - ProtocolUtils.Direction.CLIENTBOUND); + ProtocolUtils.Direction.CLIENTBOUND, StateRegistry.PLAY); assertThrows(IllegalArgumentException.class, () -> registry.register(Handshake.class, Handshake::new, new StateRegistry.PacketMapping(0x01, MINECRAFT_1_13, null, false), @@ -115,7 +115,7 @@ class PacketRegistryTest { @Test void failOnDuplicate() { StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry( - ProtocolUtils.Direction.CLIENTBOUND); + ProtocolUtils.Direction.CLIENTBOUND, StateRegistry.PLAY); registry.register(Handshake.class, Handshake::new, new StateRegistry.PacketMapping(0x00, MINECRAFT_1_8, null, false)); assertThrows(IllegalArgumentException.class, @@ -129,7 +129,7 @@ class PacketRegistryTest { @Test void shouldNotFailWhenRegisterLatestProtocolVersion() { StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry( - ProtocolUtils.Direction.CLIENTBOUND); + ProtocolUtils.Direction.CLIENTBOUND, StateRegistry.PLAY); assertDoesNotThrow(() -> registry.register(Handshake.class, Handshake::new, new StateRegistry.PacketMapping(0x00, MINECRAFT_1_8, null, false), new StateRegistry.PacketMapping(0x01, getLast(ProtocolVersion.SUPPORTED_VERSIONS), @@ -139,7 +139,7 @@ class PacketRegistryTest { @Test void registrySuppliesCorrectPacketsByProtocol() { StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry( - ProtocolUtils.Direction.CLIENTBOUND); + ProtocolUtils.Direction.CLIENTBOUND, StateRegistry.PLAY); registry.register(Handshake.class, Handshake::new, new StateRegistry.PacketMapping(0x00, MINECRAFT_1_12, null, false), new StateRegistry.PacketMapping(0x01, MINECRAFT_1_12_1, null, false), From 81b183ac391b4034955a00d703c2b6c2e832d0ef Mon Sep 17 00:00:00 2001 From: "Pantera (Mad_Daniel)" <89838384+Pantera07@users.noreply.github.com> Date: Mon, 16 Oct 2023 20:07:17 +0900 Subject: [PATCH 24/34] Bump netty to 4.1.100.Final (#1067) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7733baaa7..1497d4667 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ configurate = "3.7.3" flare = "2.0.1" log4j = "2.20.0" -netty = "4.1.90.Final" +netty = "4.1.100.Final" [plugins] indra-publishing = "net.kyori.indra.publishing:2.0.6" From b30802c1b38ce69adb0dc1843cb664015df61ef5 Mon Sep 17 00:00:00 2001 From: Badbird5907 <50347938+Badbird5907@users.noreply.github.com> Date: Thu, 26 Oct 2023 03:01:24 -0400 Subject: [PATCH 25/34] API: Add Tristate#fromOptionalBoolean (#1125) --- .../api/permission/Tristate.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/com/velocitypowered/api/permission/Tristate.java b/api/src/main/java/com/velocitypowered/api/permission/Tristate.java index 189cc2789..ddbe26a86 100644 --- a/api/src/main/java/com/velocitypowered/api/permission/Tristate.java +++ b/api/src/main/java/com/velocitypowered/api/permission/Tristate.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2022 Velocity Contributors + * Copyright (C) 2018-2023 Velocity Contributors * * The Velocity API is licensed under the terms of the MIT License. For more details, * reference the LICENSE file in the api top-level directory. @@ -7,6 +7,7 @@ package com.velocitypowered.api.permission; +import java.util.Optional; import net.kyori.adventure.util.TriState; import org.checkerframework.checker.nullness.qual.Nullable; @@ -66,6 +67,21 @@ public enum Tristate { return val ? TRUE : FALSE; } + /** + * Returns a {@link Tristate} from an {@link Optional}. + * + *

Unlike {@link #fromBoolean(boolean)}, this method returns {@link #UNDEFINED} + * if the value is empty.

+ * + * @param val the optional boolean value + * @return {@link #UNDEFINED}, {@link #TRUE} or {@link #FALSE}, if the value is empty, + * true or false, respectively. + */ + public static Tristate fromOptionalBoolean(Optional val) { + return val.map(Tristate::fromBoolean).orElse(UNDEFINED); + } + + private final boolean booleanValue; Tristate(boolean booleanValue) { From b33d18af2b5cd54bf8e55fdc11f106298def9311 Mon Sep 17 00:00:00 2001 From: Owen <23108066+Owen1212055@users.noreply.github.com> Date: Fri, 27 Oct 2023 01:39:03 -0400 Subject: [PATCH 26/34] Allow closing active proxy listeners (#1109) --- .../api/proxy/ProxyServer.java | 6 ++++ .../velocitypowered/proxy/VelocityServer.java | 5 ++++ .../proxy/network/ConnectionManager.java | 30 ++++++++++++++----- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java index 8f4ec2f65..f043b0c6d 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java @@ -41,6 +41,12 @@ public interface ProxyServer extends Audience { */ void shutdown(); + /** + * Closes all listening endpoints for this server. + * This includes the main minecraft listener and query channel. + */ + void closeListeners(); + /** * Retrieves the player currently connected to this proxy by their Minecraft username. The search * is case-insensitive. diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index f82fd45d5..e5a4828e8 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -572,6 +572,11 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { shutdown(true); } + @Override + public void closeListeners() { + this.cm.closeEndpoints(false); + } + public AsyncHttpClient getAsyncHttpClient() { return cm.getHttpClient(); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java index c5668db2a..bf596e8e3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java @@ -212,9 +212,11 @@ public final class ConnectionManager { } /** - * Closes all endpoints. + * Closes all the currently registered endpoints. + * + * @param interrupt should closing forward interruptions */ - public void shutdown() { + public void closeEndpoints(boolean interrupt) { for (final Map.Entry entry : this.endpoints.entrySet()) { final InetSocketAddress address = entry.getKey(); final Endpoint endpoint = entry.getValue(); @@ -223,14 +225,26 @@ public final class ConnectionManager { // should have a chance to be notified before the server stops accepting connections. server.getEventManager().fire(new ListenerCloseEvent(address, endpoint.getType())).join(); - try { - LOGGER.info("Closing endpoint {}", address); - endpoint.getChannel().close().sync(); - } catch (final InterruptedException e) { - LOGGER.info("Interrupted whilst closing endpoint", e); - Thread.currentThread().interrupt(); + LOGGER.info("Closing endpoint {}", address); + if (interrupt) { + try { + endpoint.getChannel().close().sync(); + } catch (final InterruptedException e) { + LOGGER.info("Interrupted whilst closing endpoint", e); + Thread.currentThread().interrupt(); + } + } else { + endpoint.getChannel().close().syncUninterruptibly(); } } + this.endpoints.clear(); + } + + /** + * Closes all endpoints. + */ + public void shutdown() { + this.closeEndpoints(true); this.resolver.shutdown(); } From 4b7950658b3d7856df28d140d0067551e6574998 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Fri, 27 Oct 2023 01:50:27 -0400 Subject: [PATCH 27/34] Remove legacy repository cruft This dates from the dark days when I dared to operate my own Jenkins instance. Never again. --- Jenkinsfile | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 7176ec2a6..000000000 --- a/Jenkinsfile +++ /dev/null @@ -1,38 +0,0 @@ -pipeline { - agent none - options { - disableConcurrentBuilds() - } - stages { - stage('Build') { - agent { - docker { - image 'velocitypowered/openjdk8-plus-git:slim' - args '-v gradle-cache:/root/.gradle:rw' - } - } - steps { - sh './gradlew build --no-daemon' - archiveArtifacts 'proxy/build/libs/*-all.jar,api/build/libs/*-all.jar' - } - } - stage('Deploy') { - when { - expression { - GIT_BRANCH = sh(returnStdout: true, script: 'git rev-parse --abbrev-ref HEAD').trim() - return GIT_BRANCH == 'master' - } - } - agent { - docker { - image 'velocitypowered/openjdk8-plus-git:slim' - args '-v gradle-cache:/root/.gradle:rw -v maven-repo:/maven-repo:rw -v javadoc:/javadoc:rw' - } - } - steps { - sh 'export MAVEN_DEPLOYMENT=true; ./gradlew publish --no-daemon' - sh 'rsync -av --delete ./api/build/docs/javadoc/ /javadoc' - } - } - } -} \ No newline at end of file From 0b07456c8900fd762d508ec58a3cbdceb7bb319f Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Fri, 27 Oct 2023 15:17:32 -0400 Subject: [PATCH 28/34] Bump `HARD_MAXIMUM_UNCOMPRESSED_SIZE` to 128MiB --- .../proxy/protocol/netty/MinecraftCompressDecoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java index a19baab70..6e7fb4d4e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java @@ -34,7 +34,7 @@ import java.util.List; public class MinecraftCompressDecoder extends MessageToMessageDecoder { private static final int VANILLA_MAXIMUM_UNCOMPRESSED_SIZE = 8 * 1024 * 1024; // 8MiB - private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 16 * 1024 * 1024; // 16MiB + private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 128 * 1024 * 1024; // 128MiB private static final int UNCOMPRESSED_CAP = Boolean.getBoolean("velocity.increased-compression-cap") From 7e932eaad4ca0d0c21833cb4f65368fbe5f87d4c Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Fri, 27 Oct 2023 15:35:04 -0400 Subject: [PATCH 29/34] Modify `MinecraftCompressorAndLengthEncoder` to handle 8MiB outgoing packets like vanilla supports now --- .../velocitypowered/proxy/protocol/ProtocolUtils.java | 11 ++++++----- .../netty/MinecraftCompressorAndLengthEncoder.java | 10 +++++----- .../proxy/protocol/ProtocolUtilsTest.java | 10 +++++----- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java index e65ee056b..23f9de87c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java @@ -163,16 +163,17 @@ public enum ProtocolUtils { } /** - * Writes the specified {@code value} as a 21-bit Minecraft VarInt to the specified {@code buf}. - * The upper 11 bits will be discarded. + * Writes the specified {@code value} as a 28-bit Minecraft VarInt to the specified {@code buf}. + * The upper 4 bits will be discarded. * * @param buf the buffer to read from * @param value the integer to write */ - public static void write21BitVarInt(ByteBuf buf, int value) { + public static void write28BitVarInt(ByteBuf buf, int value) { // See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/ - int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14); - buf.writeMedium(w); + int w = (value & 0x7F | 0x80) << 24 | (((value >>> 7) & 0x7F | 0x80) << 16) + | ((value >>> 14) & 0x7F | 0x80) << 8 | (value >>> 21); + buf.writeInt(w); } public static String readString(ByteBuf buf) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressorAndLengthEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressorAndLengthEncoder.java index 90952a729..d020d93c6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressorAndLengthEncoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressorAndLengthEncoder.java @@ -57,7 +57,7 @@ public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder= 1 << 21) { - throw new DataFormatException("The server sent a very large (over 2MiB compressed) packet."); + if (compressedLength >= 1 << 23) { + throw new DataFormatException("The server sent a very large (over 8MiB compressed) packet."); } int writerIndex = out.writerIndex(); int packetLength = out.readableBytes() - 3; out.writerIndex(0); - ProtocolUtils.write21BitVarInt(out, packetLength); // Rewrite packet length + ProtocolUtils.write28BitVarInt(out, packetLength); // Rewrite packet length out.writerIndex(writerIndex); } @@ -92,7 +92,7 @@ public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder Date: Mon, 27 Mar 2023 00:00:24 +0200 Subject: [PATCH 30/34] Migrate buildSrc plugins to build-logic --- api/build.gradle.kts | 1 + build-logic/build.gradle.kts | 19 +++++ {buildSrc => build-logic}/settings.gradle.kts | 4 ++ build-logic/src/main/kotlin/LibsAccessor.kt | 6 ++ .../kotlin/velocity-checkstyle.gradle.kts | 10 +++ .../kotlin/velocity-init-manifest.gradle.kts | 29 ++++++++ .../main/kotlin/velocity-publish.gradle.kts | 33 +++++++++ .../main/kotlin/velocity-spotless.gradle.kts | 15 ++++ build.gradle.kts | 17 ++--- buildSrc/build.gradle.kts | 37 ---------- .../script/SetManifestImplVersionPlugin.kt | 54 -------------- .../script/VelocityCheckstylePlugin.kt | 38 ---------- .../script/VelocityPublishPlugin.kt | 72 ------------------- .../script/VelocitySpotlessPlugin.kt | 46 ------------ gradle/libs.versions.toml | 1 + native/build.gradle.kts | 2 +- proxy/build.gradle.kts | 2 +- settings.gradle.kts | 22 +++++- 18 files changed, 145 insertions(+), 263 deletions(-) create mode 100644 build-logic/build.gradle.kts rename {buildSrc => build-logic}/settings.gradle.kts (79%) create mode 100644 build-logic/src/main/kotlin/LibsAccessor.kt create mode 100644 build-logic/src/main/kotlin/velocity-checkstyle.gradle.kts create mode 100644 build-logic/src/main/kotlin/velocity-init-manifest.gradle.kts create mode 100644 build-logic/src/main/kotlin/velocity-publish.gradle.kts create mode 100644 build-logic/src/main/kotlin/velocity-spotless.gradle.kts delete mode 100644 buildSrc/build.gradle.kts delete mode 100644 buildSrc/src/main/kotlin/com/velocitypowered/script/SetManifestImplVersionPlugin.kt delete mode 100644 buildSrc/src/main/kotlin/com/velocitypowered/script/VelocityCheckstylePlugin.kt delete mode 100644 buildSrc/src/main/kotlin/com/velocitypowered/script/VelocityPublishPlugin.kt delete mode 100644 buildSrc/src/main/kotlin/com/velocitypowered/script/VelocitySpotlessPlugin.kt diff --git a/api/build.gradle.kts b/api/build.gradle.kts index c5a96101e..2a1de7af8 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -1,6 +1,7 @@ plugins { `java-library` `maven-publish` + id("velocity-publish") } java { diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 000000000..7bbe0fb3f --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,19 @@ +@Suppress("DSL_SCOPE_VIOLATION") // fixed in Gradle 8.1 +plugins { + `kotlin-dsl` + alias(libs.plugins.spotless) +} + +dependencies { + // this is OK as long as the same version catalog is used in the main build and build-logic + // see https://github.com/gradle/gradle/issues/15383#issuecomment-779893192 + implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) + + implementation("com.diffplug.spotless:spotless-plugin-gradle:${libs.plugins.spotless.get().version}") +} + +spotless { + kotlin { + licenseHeaderFile(rootProject.file("../HEADER.txt")) + } +} diff --git a/buildSrc/settings.gradle.kts b/build-logic/settings.gradle.kts similarity index 79% rename from buildSrc/settings.gradle.kts rename to build-logic/settings.gradle.kts index 0ecd59401..853b9f8c4 100644 --- a/buildSrc/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -1,3 +1,5 @@ +@file:Suppress("UnstableApiUsage") + dependencyResolutionManagement { repositories { mavenCentral() @@ -9,3 +11,5 @@ dependencyResolutionManagement { } } } + +rootProject.name = "build-logic" diff --git a/build-logic/src/main/kotlin/LibsAccessor.kt b/build-logic/src/main/kotlin/LibsAccessor.kt new file mode 100644 index 000000000..1214d4912 --- /dev/null +++ b/build-logic/src/main/kotlin/LibsAccessor.kt @@ -0,0 +1,6 @@ +import org.gradle.accessors.dm.LibrariesForLibs +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType + +val Project.libs: LibrariesForLibs + get() = rootProject.extensions.getByType() diff --git a/build-logic/src/main/kotlin/velocity-checkstyle.gradle.kts b/build-logic/src/main/kotlin/velocity-checkstyle.gradle.kts new file mode 100644 index 000000000..4ac506002 --- /dev/null +++ b/build-logic/src/main/kotlin/velocity-checkstyle.gradle.kts @@ -0,0 +1,10 @@ +plugins { + checkstyle +} + +extensions.configure { + configFile = rootProject.file("config/checkstyle/checkstyle.xml") + maxErrors = 0 + maxWarnings = 0 + toolVersion = libs.checkstyle.get().version.toString() +} diff --git a/build-logic/src/main/kotlin/velocity-init-manifest.gradle.kts b/build-logic/src/main/kotlin/velocity-init-manifest.gradle.kts new file mode 100644 index 000000000..1901430f5 --- /dev/null +++ b/build-logic/src/main/kotlin/velocity-init-manifest.gradle.kts @@ -0,0 +1,29 @@ +import org.gradle.jvm.tasks.Jar +import org.gradle.kotlin.dsl.withType +import java.io.ByteArrayOutputStream + +val currentShortRevision = ByteArrayOutputStream().use { + exec { + executable = "git" + args = listOf("rev-parse", "HEAD") + standardOutput = it + } + it.toString().trim().substring(0, 8) +} + +tasks.withType { + manifest { + val buildNumber = System.getenv("BUILD_NUMBER") + val velocityHumanVersion: String = + if (project.version.toString().endsWith("-SNAPSHOT")) { + if (buildNumber == null) { + "${project.version} (git-$currentShortRevision)" + } else { + "${project.version} (git-$currentShortRevision-b$buildNumber)" + } + } else { + archiveVersion.get() + } + attributes["Implementation-Version"] = velocityHumanVersion + } +} diff --git a/build-logic/src/main/kotlin/velocity-publish.gradle.kts b/build-logic/src/main/kotlin/velocity-publish.gradle.kts new file mode 100644 index 000000000..49419a700 --- /dev/null +++ b/build-logic/src/main/kotlin/velocity-publish.gradle.kts @@ -0,0 +1,33 @@ +plugins { + java + `maven-publish` +} + +extensions.configure { + repositories { + maven { + credentials(PasswordCredentials::class.java) + + name = "paper" + val base = "https://papermc.io/repo/repository/maven" + val releasesRepoUrl = "$base-releases/" + val snapshotsRepoUrl = "$base-snapshots/" + setUrl(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) + } + } + publications { + create("maven") { + from(components["java"]) + pom { + name.set("Velocity") + description.set("The modern, next-generation Minecraft server proxy") + url.set("https://www.velocitypowered.com") + scm { + url.set("https://github.com/PaperMC/Velocity") + connection.set("scm:git:https://github.com/PaperMC/Velocity.git") + developerConnection.set("scm:git:https://github.com/PaperMC/Velocity.git") + } + } + } + } +} diff --git a/build-logic/src/main/kotlin/velocity-spotless.gradle.kts b/build-logic/src/main/kotlin/velocity-spotless.gradle.kts new file mode 100644 index 000000000..cbd3f3d46 --- /dev/null +++ b/build-logic/src/main/kotlin/velocity-spotless.gradle.kts @@ -0,0 +1,15 @@ +import com.diffplug.gradle.spotless.SpotlessExtension +import com.diffplug.gradle.spotless.SpotlessPlugin + +apply() + +extensions.configure { + java { + if (project.name == "velocity-api") { + licenseHeaderFile(file("HEADER.txt")) + } else { + licenseHeaderFile(rootProject.file("HEADER.txt")) + } + removeUnusedImports() + } +} diff --git a/build.gradle.kts b/build.gradle.kts index f1a64c850..3ec6b160c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,17 +1,14 @@ -import com.velocitypowered.script.VelocityCheckstylePlugin -import com.velocitypowered.script.VelocityPublishPlugin -import com.velocitypowered.script.VelocitySpotlessPlugin - plugins { `java-library` + id("velocity-checkstyle") apply false + id("velocity-spotless") apply false } subprojects { apply() - apply() - apply() - apply() + apply(plugin = "velocity-checkstyle") + apply(plugin = "velocity-spotless") java { toolchain { @@ -19,12 +16,6 @@ subprojects { } } - repositories { - mavenCentral() - maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") // adventure - maven("https://repo.papermc.io/repository/maven-public/") - } - dependencies { testImplementation(rootProject.libs.junit) } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts deleted file mode 100644 index e65c26037..000000000 --- a/buildSrc/build.gradle.kts +++ /dev/null @@ -1,37 +0,0 @@ -plugins { - `kotlin-dsl` - checkstyle - alias(libs.plugins.indra.publishing) - alias(libs.plugins.spotless) -} - -dependencies { - implementation("com.diffplug.spotless:spotless-plugin-gradle:${libs.plugins.spotless.get().version}") -} - -gradlePlugin { - plugins { - register("set-manifest-impl-version") { - id = "set-manifest-impl-version" - implementationClass = "com.velocitypowered.script.SetManifestImplVersionPlugin" - } - register("velocity-checkstyle") { - id = "velocity-checkstyle" - implementationClass = "com.velocitypowered.script.VelocityCheckstylePlugin" - } - register("velocity-spotless") { - id = "velocity-spotless" - implementationClass = "com.velocitypowered.script.VelocitySpotlessPlugin" - } - register("velocity-publish") { - id = "velocity-publish" - implementationClass = "com.velocitypowered.script.VelocityPublishPlugin" - } - } -} - -spotless { - kotlin { - licenseHeaderFile(project.rootProject.file("../HEADER.txt")) - } -} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/com/velocitypowered/script/SetManifestImplVersionPlugin.kt b/buildSrc/src/main/kotlin/com/velocitypowered/script/SetManifestImplVersionPlugin.kt deleted file mode 100644 index 5be27e608..000000000 --- a/buildSrc/src/main/kotlin/com/velocitypowered/script/SetManifestImplVersionPlugin.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2023 Velocity Contributors - * - * 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 . - */ - -package com.velocitypowered.script - -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.jvm.tasks.Jar -import org.gradle.kotlin.dsl.withType -import java.io.ByteArrayOutputStream - -class SetManifestImplVersionPlugin : Plugin { - override fun apply(target: Project) = target.afterEvaluate { configure() } - private fun Project.configure() { - val currentShortRevision = ByteArrayOutputStream().use { - exec { - executable = "git" - args = listOf("rev-parse", "HEAD") - standardOutput = it - } - it.toString().trim().substring(0, 8) - } - tasks.withType { - manifest { - val buildNumber = System.getenv("BUILD_NUMBER") - var velocityHumanVersion: String - if (project.version.toString().endsWith("-SNAPSHOT")) { - if (buildNumber != null) { - velocityHumanVersion = "${project.version} (git-$currentShortRevision-b$buildNumber)" - } else { - velocityHumanVersion = "${project.version} (git-$currentShortRevision)" - } - } else { - velocityHumanVersion = archiveVersion.get() - } - attributes["Implementation-Version"] = velocityHumanVersion - } - } - } -} diff --git a/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocityCheckstylePlugin.kt b/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocityCheckstylePlugin.kt deleted file mode 100644 index 73d5e7bcd..000000000 --- a/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocityCheckstylePlugin.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2023 Velocity Contributors - * - * 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 . - */ - -package com.velocitypowered.script - -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.plugins.quality.CheckstyleExtension -import org.gradle.api.plugins.quality.CheckstylePlugin -import org.gradle.kotlin.dsl.apply -import org.gradle.kotlin.dsl.configure - -class VelocityCheckstylePlugin : Plugin { - override fun apply(target: Project) = target.configure() - private fun Project.configure() { - apply() - extensions.configure { - configFile = project.rootProject.file("config/checkstyle/checkstyle.xml") - maxErrors = 0 - maxWarnings = 0 - toolVersion = "10.6.0" - } - } -} diff --git a/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocityPublishPlugin.kt b/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocityPublishPlugin.kt deleted file mode 100644 index 8aa18b048..000000000 --- a/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocityPublishPlugin.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2023 Velocity Contributors - * - * 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 . - */ - -package com.velocitypowered.script - -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.artifacts.repositories.PasswordCredentials -import org.gradle.api.plugins.JavaBasePlugin -import org.gradle.api.plugins.JavaPluginExtension -import org.gradle.api.publish.PublishingExtension -import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.publish.maven.plugins.MavenPublishPlugin -import org.gradle.kotlin.dsl.apply -import org.gradle.kotlin.dsl.configure -import org.gradle.kotlin.dsl.create -import org.gradle.kotlin.dsl.get -import org.gradle.kotlin.dsl.getByType - -class VelocityPublishPlugin : Plugin { - override fun apply(target: Project) = target.afterEvaluate { - if (target.name != "velocity-proxy") { - configure() - } - } - private fun Project.configure() { - apply() - apply() - extensions.configure { - repositories { - maven { - credentials(PasswordCredentials::class.java) - - name = "paper" - val base = "https://papermc.io/repo/repository/maven" - val releasesRepoUrl = "$base-releases/" - val snapshotsRepoUrl = "$base-snapshots/" - setUrl(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) - } - } - publications { - create("maven") { - from(components["java"]) - pom { - name.set("Velocity") - description.set("The modern, next-generation Minecraft server proxy") - url.set("https://www.velocitypowered.com") - scm { - url.set("https://github.com/PaperMC/Velocity") - connection.set("scm:git:https://github.com/PaperMC/Velocity.git") - developerConnection.set("scm:git:https://github.com/PaperMC/Velocity.git") - } - } - } - } - } - } -} diff --git a/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocitySpotlessPlugin.kt b/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocitySpotlessPlugin.kt deleted file mode 100644 index 1641f2c8a..000000000 --- a/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocitySpotlessPlugin.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2023 Velocity Contributors - * - * 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 . - */ - -package com.velocitypowered.script - -import com.diffplug.gradle.spotless.SpotlessExtension -import com.diffplug.gradle.spotless.SpotlessPlugin -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.kotlin.dsl.apply -import org.gradle.kotlin.dsl.configure -import java.io.File - -class VelocitySpotlessPlugin : Plugin { - override fun apply(target: Project) = target.configure() - - private fun Project.configure() { - apply() - - extensions.configure { - java { - if (project.name == "velocity-api") { - licenseHeaderFile(project.file("HEADER.txt")) - } else { - licenseHeaderFile(project.rootProject.file("HEADER.txt")) - } - - removeUnusedImports() - } - } - } -} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1497d4667..8b1f9a817 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT" bstats = "org.bstats:bstats-base:3.0.1" caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.5" checker-qual = "org.checkerframework:checker-qual:3.28.0" +checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3" completablefutures = "com.spotify:completable-futures:0.3.5" configurate-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate" } configurate-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate" } diff --git a/native/build.gradle.kts b/native/build.gradle.kts index 165cb30f9..5ec8673cc 100644 --- a/native/build.gradle.kts +++ b/native/build.gradle.kts @@ -1,6 +1,6 @@ plugins { `java-library` - `maven-publish` + id("velocity-publish") } dependencies { diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index 643d5eb78..322888a27 100644 --- a/proxy/build.gradle.kts +++ b/proxy/build.gradle.kts @@ -2,7 +2,7 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCach plugins { application - `set-manifest-impl-version` + id("velocity-init-manifest") alias(libs.plugins.shadow) } diff --git a/settings.gradle.kts b/settings.gradle.kts index b8d700802..d928550b9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,23 @@ +@file:Suppress("UnstableApiUsage") + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") // adventure + maven("https://repo.papermc.io/repository/maven-public/") + } +} + + +pluginManagement { + includeBuild("build-logic") + repositories { + mavenCentral() + gradlePluginPortal() + } +} + plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.4.0" } @@ -6,8 +26,8 @@ rootProject.name = "velocity" sequenceOf( "api", - "proxy", "native", + "proxy", ).forEach { val project = ":velocity-$it" include(project) From c1420c3ea9ccd10b0e7a522aacd19952feba5580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20St=C3=A4ding?= Date: Mon, 27 Mar 2023 01:47:01 +0200 Subject: [PATCH 31/34] Update build-logic/src/main/kotlin/velocity-publish.gradle.kts Co-authored-by: Riley Park --- build-logic/src/main/kotlin/velocity-publish.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/src/main/kotlin/velocity-publish.gradle.kts b/build-logic/src/main/kotlin/velocity-publish.gradle.kts index 49419a700..a4e2cb876 100644 --- a/build-logic/src/main/kotlin/velocity-publish.gradle.kts +++ b/build-logic/src/main/kotlin/velocity-publish.gradle.kts @@ -9,7 +9,7 @@ extensions.configure { credentials(PasswordCredentials::class.java) name = "paper" - val base = "https://papermc.io/repo/repository/maven" + val base = "https://repo.papermc.io/repository/maven" val releasesRepoUrl = "$base-releases/" val snapshotsRepoUrl = "$base-snapshots/" setUrl(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) From 0aff18f8656cffc0ad67c6a8a91b10560d76df42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20St=C3=A4ding?= Date: Wed, 21 Jun 2023 15:58:44 +0200 Subject: [PATCH 32/34] Fix redundant empty line and url in publish script as per suggestions --- build-logic/src/main/kotlin/velocity-publish.gradle.kts | 2 +- settings.gradle.kts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/build-logic/src/main/kotlin/velocity-publish.gradle.kts b/build-logic/src/main/kotlin/velocity-publish.gradle.kts index a4e2cb876..51de5c672 100644 --- a/build-logic/src/main/kotlin/velocity-publish.gradle.kts +++ b/build-logic/src/main/kotlin/velocity-publish.gradle.kts @@ -21,7 +21,7 @@ extensions.configure { pom { name.set("Velocity") description.set("The modern, next-generation Minecraft server proxy") - url.set("https://www.velocitypowered.com") + url.set("https://papermc.io/software/velocity") scm { url.set("https://github.com/PaperMC/Velocity") connection.set("scm:git:https://github.com/PaperMC/Velocity.git") diff --git a/settings.gradle.kts b/settings.gradle.kts index d928550b9..0c31266f9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,7 +9,6 @@ dependencyResolutionManagement { } } - pluginManagement { includeBuild("build-logic") repositories { From 24ed83dcadcf55e8831d3f6fa84ab89222f6b222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20St=C3=A4ding?= Date: Wed, 19 Jul 2023 16:41:49 +0200 Subject: [PATCH 33/34] Remove redundant suppress in buildscript --- build-logic/build.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 7bbe0fb3f..26af27624 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -1,4 +1,3 @@ -@Suppress("DSL_SCOPE_VIOLATION") // fixed in Gradle 8.1 plugins { `kotlin-dsl` alias(libs.plugins.spotless) @@ -8,7 +7,6 @@ dependencies { // this is OK as long as the same version catalog is used in the main build and build-logic // see https://github.com/gradle/gradle/issues/15383#issuecomment-779893192 implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) - implementation("com.diffplug.spotless:spotless-plugin-gradle:${libs.plugins.spotless.get().version}") } From bbdeb3ec5874d97a58797a3bba5e542bda6a5c5b Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Fri, 27 Oct 2023 16:10:39 -0400 Subject: [PATCH 34/34] Revert "Modify `MinecraftCompressorAndLengthEncoder` to handle 8MiB outgoing packets like vanilla supports now" This reverts commit 7e932eaad4ca0d0c21833cb4f65368fbe5f87d4c. Yikes, I broke something. Need to rethink this approach. --- .../velocitypowered/proxy/protocol/ProtocolUtils.java | 11 +++++------ .../netty/MinecraftCompressorAndLengthEncoder.java | 10 +++++----- .../proxy/protocol/ProtocolUtilsTest.java | 10 +++++----- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java index 23f9de87c..e65ee056b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java @@ -163,17 +163,16 @@ public enum ProtocolUtils { } /** - * Writes the specified {@code value} as a 28-bit Minecraft VarInt to the specified {@code buf}. - * The upper 4 bits will be discarded. + * Writes the specified {@code value} as a 21-bit Minecraft VarInt to the specified {@code buf}. + * The upper 11 bits will be discarded. * * @param buf the buffer to read from * @param value the integer to write */ - public static void write28BitVarInt(ByteBuf buf, int value) { + public static void write21BitVarInt(ByteBuf buf, int value) { // See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/ - int w = (value & 0x7F | 0x80) << 24 | (((value >>> 7) & 0x7F | 0x80) << 16) - | ((value >>> 14) & 0x7F | 0x80) << 8 | (value >>> 21); - buf.writeInt(w); + int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14); + buf.writeMedium(w); } public static String readString(ByteBuf buf) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressorAndLengthEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressorAndLengthEncoder.java index d020d93c6..90952a729 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressorAndLengthEncoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressorAndLengthEncoder.java @@ -57,7 +57,7 @@ public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder= 1 << 23) { - throw new DataFormatException("The server sent a very large (over 8MiB compressed) packet."); + if (compressedLength >= 1 << 21) { + throw new DataFormatException("The server sent a very large (over 2MiB compressed) packet."); } int writerIndex = out.writerIndex(); int packetLength = out.readableBytes() - 3; out.writerIndex(0); - ProtocolUtils.write28BitVarInt(out, packetLength); // Rewrite packet length + ProtocolUtils.write21BitVarInt(out, packetLength); // Rewrite packet length out.writerIndex(writerIndex); } @@ -92,7 +92,7 @@ public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder