From a078053e26eff94b106afcaf594f9fde2897c592 Mon Sep 17 00:00:00 2001
From: Shane Freeder
Date: Mon, 24 Aug 2026 17:26:12 +0100
Subject: [PATCH 01/18] velocity 4.1.0 release
---
gradle.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index 44cdb5d0..90fbaf0b 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,2 +1,2 @@
group=com.velocitypowered
-version=4.1.0-SNAPSHOT
+version=4.1.0
From b2ba2e66bff993e04208a3eb4093f6385c137aa7 Mon Sep 17 00:00:00 2001
From: Wouter Gritter
Date: Wed, 26 Aug 2026 12:09:07 +0200
Subject: [PATCH 02/18] Snapshot 4.1.1-SNAPSHOT
---
gradle.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index 90fbaf0b..b68ca355 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,2 +1,2 @@
group=com.velocitypowered
-version=4.1.0
+version=4.1.1-SNAPSHOT
From 133f0e36b1247faed11502b13de1de80667ad370 Mon Sep 17 00:00:00 2001
From: Wouter Gritter
Date: Wed, 26 Aug 2026 12:09:46 +0200
Subject: [PATCH 03/18] Switch to JLine's jni backend
FFM is the canonical choice for Java 22+, but it has some issues (mainly crashing the JVM on window resize), see jline/jline3#2139 and jline/jline3#2141
Downgrading to 4.0.x isn't an option as it re-introduces CVE-2026-77420..77423
Once the referenced PR gets merged and released, jline should be bumped to jline-terminal-ffm's latest version again
---
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 9c12ac2d..0a3b79af 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -32,7 +32,7 @@ disruptor = "com.lmax:disruptor:4.0.0"
fastutil = "it.unimi.dsi:fastutil:8.5.19"
flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" }
flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" }
-jline = "org.jline:jline-terminal-ffm:4.3.1"
+jline = "org.jline:jline-terminal-jni:4.3.1"
jopt = "net.sf.jopt-simple:jopt-simple:5.0.4"
junit = "org.junit.jupiter:junit-jupiter:6.1.2"
jspecify = "org.jspecify:jspecify:1.0.1"
From db0a17eea61f18994963da0ae936d3560aade74e Mon Sep 17 00:00:00 2001
From: Shane Freeder
Date: Wed, 26 Aug 2026 20:29:34 +0100
Subject: [PATCH 04/18] Velocity 4.1.1 release
---
gradle.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index b68ca355..48599a5d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,2 +1,2 @@
group=com.velocitypowered
-version=4.1.1-SNAPSHOT
+version=4.1.1
From ddea763c8ba064975905e4b2ee638152b87d7ef2 Mon Sep 17 00:00:00 2001
From: Wouter Gritter
Date: Tue, 1 Sep 2026 13:10:58 +0200
Subject: [PATCH 05/18] Snapshot 4.1.2-SNAPSHOT
---
gradle.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index 48599a5d..ed875a01 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,2 +1,2 @@
group=com.velocitypowered
-version=4.1.1
+version=4.1.2-SNAPSHOT
From 940f395e3e2b4df1f16d3457c9930f1c87bf1e18 Mon Sep 17 00:00:00 2001
From: Wouter Gritter
Date: Tue, 1 Sep 2026 13:19:46 +0200
Subject: [PATCH 06/18] Use correct minimum ClientSettings packet length for
older protocol versions
For correctness's sake: in practice this bug never got triggered because `locale` is almost always a string of length 5+ ("en_us").
---
.../protocol/packet/ClientSettingsPacket.java | 29 ++++++++++++++++++-
1 file changed, 28 insertions(+), 1 deletion(-)
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettingsPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettingsPacket.java
index b8b60a9f..7e6600cc 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettingsPacket.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettingsPacket.java
@@ -213,7 +213,34 @@ public class ClientSettingsPacket implements MinecraftPacket {
@Override
public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
- return 1 + 0 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1;
+ int minLength = 1 // locale
+ + 1 // viewDistance
+ + 1 // chatVisibility
+ + 1; // chatColors
+
+ if (version.noGreaterThan(ProtocolVersion.MINECRAFT_1_7_6)) {
+ minLength += 1; // difficulty
+ }
+
+ minLength += 1; // skinParts
+
+ if (version.noLessThan(ProtocolVersion.MINECRAFT_1_9)) {
+ minLength += 1; // mainHand
+
+ if (version.noLessThan(ProtocolVersion.MINECRAFT_1_17)) {
+ minLength += 1; // textFilteringEnabled
+
+ if (version.noLessThan(ProtocolVersion.MINECRAFT_1_18)) {
+ minLength += 1; // clientListingAllowed
+
+ if (version.noLessThan(ProtocolVersion.MINECRAFT_1_21_2)) {
+ minLength += 1; // particleStatus
+ }
+ }
+ }
+ }
+
+ return minLength;
}
@Override
From e84b48a71654b0195674e9d1ca5de9a3ddd49993 Mon Sep 17 00:00:00 2001
From: Wouter Gritter
Date: Tue, 1 Sep 2026 13:55:10 +0200
Subject: [PATCH 07/18] Clean up Title packets and enforce action types (#1803)
---
.../connection/client/ConnectedPlayer.java | 5 +--
.../packet/title/GenericTitlePacket.java | 29 +++++------------
.../packet/title/LegacyTitlePacket.java | 31 +++++++++++++------
.../packet/title/TitleActionbarPacket.java | 6 ++--
.../packet/title/TitleClearPacket.java | 23 +++++++++-----
.../packet/title/TitleSubtitlePacket.java | 6 ++--
.../packet/title/TitleTextPacket.java | 6 ++--
.../packet/title/TitleTimesPacket.java | 6 ++--
8 files changed, 63 insertions(+), 49 deletions(-)
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 e28b7c94..f5898ace 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
@@ -135,6 +135,7 @@ import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
import net.kyori.adventure.text.minimessage.translation.Argument;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import net.kyori.adventure.title.Title;
import net.kyori.adventure.title.Title.Times;
import net.kyori.adventure.title.TitlePart;
import net.kyori.adventure.translation.GlobalTranslator;
@@ -486,11 +487,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
}
@Override
- public void showTitle(net.kyori.adventure.title.@NonNull Title title) {
+ public void showTitle(@NonNull Title title) {
if (this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
GenericTitlePacket timesPkt = GenericTitlePacket.constructTitlePacket(
GenericTitlePacket.ActionType.SET_TIMES, this.getProtocolVersion());
- net.kyori.adventure.title.Title.Times times = title.times();
+ Title.Times times = title.times();
if (times != null) {
timesPkt.setFadeIn((int) DurationUtils.toTicks(times.fadeIn()));
timesPkt.setStay((int) DurationUtils.toTicks(times.stay()));
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/GenericTitlePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/GenericTitlePacket.java
index 8641173e..411dcc91 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/GenericTitlePacket.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/GenericTitlePacket.java
@@ -22,10 +22,12 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf;
+import org.jetbrains.annotations.NotNull;
public abstract class GenericTitlePacket implements MinecraftPacket {
public enum ActionType {
+
SET_TITLE(0),
SET_SUBTITLE(1),
SET_ACTION_BAR(2),
@@ -45,16 +47,7 @@ public abstract class GenericTitlePacket implements MinecraftPacket {
}
}
-
- private ActionType action;
-
- protected void setAction(ActionType action) {
- this.action = action;
- }
-
- public final ActionType getAction() {
- return action;
- }
+ public abstract @NotNull ActionType getAction();
public ComponentHolder getComponent() {
throw new UnsupportedOperationException("Invalid function for this TitlePacket ActionType");
@@ -88,11 +81,9 @@ public abstract class GenericTitlePacket implements MinecraftPacket {
throw new UnsupportedOperationException("Invalid function for this TitlePacket ActionType");
}
-
@Override
- public final void decode(ByteBuf buf, ProtocolUtils.Direction direction,
- ProtocolVersion version) {
- throw new UnsupportedOperationException(); // encode only
+ public final void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
+ throw new UnsupportedOperationException("Decode is not implemented");
}
/**
@@ -103,21 +94,17 @@ public abstract class GenericTitlePacket implements MinecraftPacket {
* @return GenericTitlePacket instance that follows the invoker type/version
*/
public static GenericTitlePacket constructTitlePacket(ActionType type, ProtocolVersion version) {
- GenericTitlePacket packet = null;
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_17)) {
- packet = switch (type) {
+ return switch (type) {
case SET_ACTION_BAR -> new TitleActionbarPacket();
case SET_SUBTITLE -> new TitleSubtitlePacket();
case SET_TIMES -> new TitleTimesPacket();
case SET_TITLE -> new TitleTextPacket();
- case HIDE, RESET -> new TitleClearPacket();
+ case HIDE, RESET -> new TitleClearPacket(type);
default -> throw new IllegalArgumentException("Invalid ActionType");
};
} else {
- packet = new LegacyTitlePacket();
+ return new LegacyTitlePacket(type);
}
- packet.setAction(type);
- return packet;
}
-
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/LegacyTitlePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/LegacyTitlePacket.java
index ebae8a9e..8033a90f 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/LegacyTitlePacket.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/LegacyTitlePacket.java
@@ -17,32 +17,44 @@
package com.velocitypowered.proxy.protocol.packet.title;
+import com.google.common.base.Preconditions;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf;
-import org.checkerframework.checker.nullness.qual.Nullable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
public class LegacyTitlePacket extends GenericTitlePacket {
+ private final ActionType action;
+
private @Nullable ComponentHolder component;
private int fadeIn;
private int stay;
private int fadeOut;
+ public LegacyTitlePacket() {
+ throw new UnsupportedOperationException("Decode is not implemented");
+ }
+
+ public LegacyTitlePacket(ActionType action) {
+ this.action = Preconditions.checkNotNull(action, "action");
+ }
+
@Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
if (version.lessThan(ProtocolVersion.MINECRAFT_1_11)
- && getAction() == ActionType.SET_ACTION_BAR) {
+ && this.action == ActionType.SET_ACTION_BAR) {
throw new IllegalStateException("Action bars are only supported on 1.11 and newer");
}
- ProtocolUtils.writeVarInt(buf, getAction().getAction(version));
+ ProtocolUtils.writeVarInt(buf, this.action.getAction(version));
- switch (getAction()) {
+ switch (this.action) {
case SET_TITLE, SET_SUBTITLE, SET_ACTION_BAR -> {
if (component == null) {
- throw new IllegalStateException("No component found for " + getAction());
+ throw new IllegalStateException("No component found for " + this.action);
}
component.write(buf);
}
@@ -52,14 +64,13 @@ public class LegacyTitlePacket extends GenericTitlePacket {
buf.writeInt(fadeOut);
}
case HIDE, RESET -> {}
- default -> throw new UnsupportedOperationException("Unknown action " + getAction());
+ default -> throw new UnsupportedOperationException("Unknown action " + this.action);
}
-
}
@Override
- public void setAction(ActionType action) {
- super.setAction(action);
+ public @NotNull ActionType getAction() {
+ return action;
}
@Override
@@ -105,7 +116,7 @@ public class LegacyTitlePacket extends GenericTitlePacket {
@Override
public String toString() {
return "GenericTitlePacket{"
- + "action=" + getAction()
+ + "action=" + action
+ ", component='" + component + '\''
+ ", fadeIn=" + fadeIn
+ ", stay=" + stay
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleActionbarPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleActionbarPacket.java
index 6bab8dd6..b52c6364 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleActionbarPacket.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleActionbarPacket.java
@@ -22,13 +22,15 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf;
+import org.jetbrains.annotations.NotNull;
public class TitleActionbarPacket extends GenericTitlePacket {
private ComponentHolder component;
- public TitleActionbarPacket() {
- setAction(ActionType.SET_ACTION_BAR);
+ @Override
+ public @NotNull ActionType getAction() {
+ return ActionType.SET_ACTION_BAR;
}
@Override
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleClearPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleClearPacket.java
index 1b348969..0019b4a9 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleClearPacket.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleClearPacket.java
@@ -21,30 +21,37 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
+import org.jetbrains.annotations.NotNull;
public class TitleClearPacket extends GenericTitlePacket {
+ private final ActionType action;
+
public TitleClearPacket() {
- setAction(ActionType.HIDE);
+ this(ActionType.HIDE);
+ }
+
+ public TitleClearPacket(ActionType action) {
+ if (action != ActionType.HIDE && action != ActionType.RESET) {
+ throw new IllegalArgumentException("TitleClearPacket only accepts the HIDE and RESET actions.");
+ }
+ this.action = action;
}
@Override
- public void setAction(ActionType action) {
- if (action != ActionType.HIDE && action != ActionType.RESET) {
- throw new IllegalArgumentException("TitleClearPacket only accepts CLEAR and RESET actions");
- }
- super.setAction(action);
+ public @NotNull ActionType getAction() {
+ return action;
}
@Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
- buf.writeBoolean(getAction() == ActionType.RESET);
+ buf.writeBoolean(this.action == ActionType.RESET);
}
@Override
public String toString() {
return "TitleClearPacket{"
- + ", resetTimes=" + (getAction() == ActionType.RESET)
+ + ", resetTimes=" + (this.action == ActionType.RESET)
+ '}';
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleSubtitlePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleSubtitlePacket.java
index 0f375ae2..a17d5399 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleSubtitlePacket.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleSubtitlePacket.java
@@ -22,13 +22,15 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf;
+import org.jetbrains.annotations.NotNull;
public class TitleSubtitlePacket extends GenericTitlePacket {
private ComponentHolder component;
- public TitleSubtitlePacket() {
- setAction(ActionType.SET_SUBTITLE);
+ @Override
+ public @NotNull ActionType getAction() {
+ return ActionType.SET_SUBTITLE;
}
@Override
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleTextPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleTextPacket.java
index ae75f5d6..ceab8290 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleTextPacket.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleTextPacket.java
@@ -22,13 +22,15 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf;
+import org.jetbrains.annotations.NotNull;
public class TitleTextPacket extends GenericTitlePacket {
private ComponentHolder component;
- public TitleTextPacket() {
- setAction(ActionType.SET_TITLE);
+ @Override
+ public @NotNull ActionType getAction() {
+ return ActionType.SET_TITLE;
}
@Override
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleTimesPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleTimesPacket.java
index 8764a12f..21972991 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleTimesPacket.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/title/TitleTimesPacket.java
@@ -21,6 +21,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
+import org.jetbrains.annotations.NotNull;
public class TitleTimesPacket extends GenericTitlePacket {
@@ -28,8 +29,9 @@ public class TitleTimesPacket extends GenericTitlePacket {
private int stay;
private int fadeOut;
- public TitleTimesPacket() {
- setAction(ActionType.SET_TIMES);
+ @Override
+ public @NotNull ActionType getAction() {
+ return ActionType.SET_TIMES;
}
@Override
From 21bbf35d1da1d8a74aad897ecb4e6915f121898d Mon Sep 17 00:00:00 2001
From: Loganius <31364192+TheMiningTeamYT@users.noreply.github.com>
Date: Tue, 1 Sep 2026 07:49:24 -0500
Subject: [PATCH 08/18] Adding more options for configuring ping passthrough.
(#1870)
* Added 'ALLBUTVERSION' option for ping passthrough.
* Added more configuration options for ping passthrough.
* Updated default velocity.toml
* Add support for the legacy ping passthrough.
* Removed legacy ping passthrough & added config migration
- Bumped config-version to 2.8
- Updated comments and grammar
- Removed legacy ping passthrough in the config
* Cleaned up code to match code style
* Update to latest
* Move ping passthrough into its own section
* Reorder migrations so they occur in the right order.
* Clean up ping passthrough related code.
* Update file dates and cleanup/consolidate ping-passthrough code.
* Update proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java
* Update proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java
* Remaining nits
- Read a string instead of LegacyPingPassthroughMode
- Update `announce-forge`'s comment in `PingPassthroughMigration`
- Consume `Config` instead of `CommentedConfig` in `PingPassthroughMode#fromConfig`
---------
Co-authored-by: TheMiningTeamYT
Co-authored-by: ButterDebugger <34288129+ButterDebugger@users.noreply.github.com>
Co-authored-by: Wouter Gritter
---
.../proxy/config/PingPassthroughMode.java | 48 +++++++-
.../proxy/config/VelocityConfiguration.java | 12 +-
.../migration/ConfigurationMigration.java | 3 +-
.../migration/PingPassthroughMigration.java | 105 +++++++++++++++++
.../util/ServerListPingHandler.java | 109 ++++++++++--------
.../src/main/resources/default-velocity.toml | 31 +++--
6 files changed, 228 insertions(+), 80 deletions(-)
create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/config/migration/PingPassthroughMigration.java
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java b/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java
index 718e57f1..aa73cefd 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java
@@ -16,13 +16,49 @@
*/
package com.velocitypowered.proxy.config;
+import com.electronwill.nightconfig.core.Config;
/**
- * Supported passthrough modes for ping passthrough.
+ * Object to contain all the things that can be toggled for ping passthrough.
+ *
+ * @param version Whether the version should be passed through.
+ * @param players Whether the player count should be passed through.
+ * @param description Whether the description should be passed through.
+ * @param favicon Whether the favicon should be passed through.
+ * @param modinfo Whether the modinfo should be passed through.
*/
-public enum PingPassthroughMode {
- DISABLED,
- MODS,
- DESCRIPTION,
- ALL
+public record PingPassthroughMode(boolean version, boolean players,
+ boolean description, boolean favicon, boolean modinfo) {
+ public static final PingPassthroughMode DEFAULT = new PingPassthroughMode();
+
+ /**
+ * Creates a default PingPassthroughMode.
+ */
+ private PingPassthroughMode() {
+ this(false, false, false, false, false);
+ }
+
+ /**
+ * Returns a PingPassthroughMode from a config section, or the default if the section is null.
+ * Based on the code for PacketLimiterConfig.
+ *
+ * @param config The configuration object to parse.
+ * @return The PingPassthroughMode, or the default if {@code config} is null.
+ */
+ public static PingPassthroughMode fromConfig(Config config) {
+ if (config == null) {
+ return DEFAULT;
+ }
+ return new PingPassthroughMode(
+ config.getOrElse("version", DEFAULT.version()),
+ config.getOrElse("players", DEFAULT.players()),
+ config.getOrElse("description", DEFAULT.description()),
+ config.getOrElse("favicon", DEFAULT.favicon()),
+ config.getOrElse("modinfo", DEFAULT.modinfo()));
+ }
+
+ public boolean enabled() {
+ return this.version || this.players || this.description || this.favicon
+ || this.modinfo;
+ }
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java
index 2c7826e0..fc9ba583 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java
@@ -32,6 +32,7 @@ import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration;
import com.velocitypowered.proxy.config.migration.MiniMessageTranslationsMigration;
import com.velocitypowered.proxy.config.migration.MotdMigration;
import com.velocitypowered.proxy.config.migration.PacketLimiterMigration;
+import com.velocitypowered.proxy.config.migration.PingPassthroughMigration;
import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration;
import com.velocitypowered.proxy.util.AddressUtil;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -79,7 +80,7 @@ public class VelocityConfiguration implements ProxyConfig {
@Expose
private boolean onlineModeKickExistingPlayers = false;
@Expose
- private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED;
+ private PingPassthroughMode pingPassthrough = PingPassthroughMode.DEFAULT;
@Expose
private boolean samplePlayersInPing = false;
private final Servers servers;
@@ -513,7 +514,8 @@ public class VelocityConfiguration implements ProxyConfig {
new MotdMigration(),
new MiniMessageTranslationsMigration(),
new TransferIntegrationMigration(),
- new PacketLimiterMigration()
+ new PacketLimiterMigration(),
+ new PingPassthroughMigration(),
};
for (final ConfigurationMigration migration : migrations) {
@@ -555,9 +557,7 @@ public class VelocityConfiguration implements ProxyConfig {
final CommentedConfig metricsConfig = config.get("metrics");
final PlayerInfoForwarding forwardingMode = config.getEnumOrElse(
"player-info-forwarding-mode", PlayerInfoForwarding.NONE);
- final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough",
- PingPassthroughMode.DISABLED);
-
+ final PingPassthroughMode pingPassthrough = PingPassthroughMode.fromConfig(config.get("ping-passthrough"));
final boolean samplePlayersInPing = config.getOrElse("sample-players-in-ping", false);
final String bind = config.getOrElse("bind", "0.0.0.0:25565");
@@ -590,7 +590,7 @@ public class VelocityConfiguration implements ProxyConfig {
forwardingMode,
forwardingSecret,
kickExisting,
- pingPassthroughMode,
+ pingPassthrough,
samplePlayersInPing,
enablePlayerAddressLogging,
new Servers(serversConfig),
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/migration/ConfigurationMigration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/migration/ConfigurationMigration.java
index 7c00b7bb..27891369 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/config/migration/ConfigurationMigration.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/config/migration/ConfigurationMigration.java
@@ -30,7 +30,8 @@ public sealed interface ConfigurationMigration
MotdMigration,
MiniMessageTranslationsMigration,
TransferIntegrationMigration,
- PacketLimiterMigration {
+ PacketLimiterMigration,
+ PingPassthroughMigration {
boolean shouldMigrate(CommentedFileConfig config);
void migrate(CommentedFileConfig config, Logger logger) throws IOException;
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/migration/PingPassthroughMigration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/migration/PingPassthroughMigration.java
new file mode 100644
index 00000000..702a06cf
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/config/migration/PingPassthroughMigration.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2024-2026 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.config.migration;
+
+import com.electronwill.nightconfig.core.file.CommentedFileConfig;
+import java.util.Locale;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * Migrate the old ping passthrough entry to separate config entries.
+ */
+public final class PingPassthroughMigration implements ConfigurationMigration {
+ @Override
+ public boolean shouldMigrate(final CommentedFileConfig config) {
+ return configVersion(config) < 2.9;
+ }
+
+ @Override
+ public void migrate(final CommentedFileConfig config, final Logger logger) {
+ // Get legacy ping passthrough value
+ final String legacyMode = config.getOrElse("ping-passthrough", "DISABLED")
+ .toUpperCase(Locale.ROOT);
+ boolean version = false;
+ boolean players = false;
+ boolean description = false;
+ boolean favicon = false;
+ boolean modinfo = false;
+
+ switch (legacyMode) {
+ case "ALL":
+ version = true;
+ players = true;
+ description = true;
+ favicon = true;
+ modinfo = true;
+ break;
+ case "DESCRIPTION":
+ description = true;
+ modinfo = true;
+ break;
+ case "MODS":
+ modinfo = true;
+ break;
+ default: // DISABLED
+ break;
+ }
+
+ config.removeComment("ping-passthrough");
+ config.remove("ping-passthrough");
+
+ config.setComment("announce-forge",
+ " Announce whether or not your server supports Forge. If you run a modded server, we\n"
+ + " suggest turning this on.\n"
+ + "\n"
+ + " If your network runs one modpack consistently, consider using ping-passthrough.modinfo = true\n"
+ + " instead for a nicer display in the server list.");
+
+ config.set("ping-passthrough.version", version);
+ config.setComment(
+ "ping-passthrough.version",
+ " Should Velocity pass the version number from the backend server when responding to server list ping requests?"
+ );
+
+ config.set("ping-passthrough.players", players);
+ config.setComment(
+ "ping-passthrough.players",
+ " Should Velocity pass the player count from the backend server when responding to server list ping requests?"
+ );
+
+ config.set("ping-passthrough.description", description);
+ config.setComment(
+ "ping-passthrough.description",
+ " Should Velocity pass the description from the backend server when responding to server list ping requests?"
+ );
+
+ config.set("ping-passthrough.favicon", favicon);
+ config.setComment(
+ "ping-passthrough.favicon",
+ " Should Velocity pass the favicon (also known as the server icon) from the backend server when responding to server list ping requests?"
+ );
+
+ config.set("ping-passthrough.modinfo", modinfo);
+ config.setComment(
+ "ping-passthrough.modinfo",
+ " Should Velocity pass the mod list from the backend server when responding to server list ping requests?"
+ );
+
+ config.set("config-version", "2.9");
+ }
+}
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 5ccb2b11..97582ca2 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
@@ -23,6 +23,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.server.PingOptions;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerPing;
+import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.api.util.ModInfo;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.PingPassthroughMode;
@@ -100,60 +101,68 @@ public class ServerListPingHandler {
CompletableFuture> pingResponses = CompletableFutures.successfulAsList(pings,
(ex) -> fallback);
- return switch (mode) {
- case ALL -> pingResponses.thenApply(responses -> {
- // Find the first non-fallback
- for (ServerPing response : responses) {
- if (response == fallback) {
- continue;
- }
- if (response.getDescriptionComponent() == null) {
- return response.asBuilder()
- .description(Component.empty())
- .build();
- }
+ // Return early if ping passthrough is not enabled
+ if (!mode.enabled()) {
+ return CompletableFuture.completedFuture(fallback);
+ }
- return response;
+ return pingResponses.thenApply(responses -> {
+ // Find the first non-fallback
+ for (ServerPing response : responses) {
+ if (response == fallback) {
+ continue;
}
- return fallback;
- });
- case MODS -> pingResponses.thenApply(responses -> {
- // Find the first non-fallback that contains a mod list
- for (ServerPing response : responses) {
- if (response == fallback) {
- continue;
- }
- Optional modInfo = response.getModinfo();
- if (modInfo.isPresent()) {
- return fallback.asBuilder().mods(modInfo.get()).build();
- }
- }
- return fallback;
- });
- case DESCRIPTION -> pingResponses.thenApply(responses -> {
- // Find the first non-fallback. If it includes a modlist, add it too.
- for (ServerPing response : responses) {
- if (response == fallback) {
- continue;
- }
- if (response.getDescriptionComponent() == null) {
- continue;
- }
- return new ServerPing(
- fallback.getVersion(),
- fallback.getPlayers().orElse(null),
- response.getDescriptionComponent(),
- fallback.getFavicon().orElse(null),
- response.getModinfo().orElse(null)
- );
+ ServerPing.Version version;
+ if (mode.version()) {
+ version = response.getVersion();
+ } else {
+ version = fallback.getVersion();
}
- return fallback;
- });
- // Not possible, but covered for completeness.
- default -> CompletableFuture.completedFuture(fallback);
- };
+
+ ServerPing.Players players;
+ if (mode.players()) {
+ players = response.getPlayers().orElse(null);
+ } else {
+ players = fallback.getPlayers().orElse(null);
+ }
+
+ Component description;
+ if (mode.description()) {
+ if (response.getDescriptionComponent() != null) {
+ description = response.getDescriptionComponent();
+ } else {
+ description = Component.empty();
+ }
+ } else {
+ description = fallback.getDescriptionComponent();
+ }
+
+ Favicon favicon;
+ if (mode.favicon()) {
+ favicon = response.getFavicon().orElse(null);
+ } else {
+ favicon = fallback.getFavicon().orElse(null);
+ }
+
+ ModInfo modinfo;
+ if (mode.modinfo()) {
+ modinfo = response.getModinfo().orElse(null);
+ } else {
+ modinfo = fallback.getModinfo().orElse(null);
+ }
+
+ return new ServerPing(
+ version,
+ players,
+ description,
+ favicon,
+ modinfo
+ );
+ }
+ return fallback;
+ });
}
/**
@@ -168,7 +177,7 @@ public class ServerListPingHandler {
? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION;
PingPassthroughMode passthroughMode = configuration.getPingPassthrough();
- if (passthroughMode == PingPassthroughMode.DISABLED) {
+ if (!passthroughMode.enabled()) {
return CompletableFuture.completedFuture(constructLocalPing(shownVersion));
} else {
String virtualHostStr = connection.getVirtualHost().map(InetSocketAddress::getHostString)
diff --git a/proxy/src/main/resources/default-velocity.toml b/proxy/src/main/resources/default-velocity.toml
index 0eae2734..34a17f6c 100644
--- a/proxy/src/main/resources/default-velocity.toml
+++ b/proxy/src/main/resources/default-velocity.toml
@@ -1,5 +1,5 @@
# Config version. Do not change this
-config-version = "2.8"
+config-version = "2.9"
# What port should the proxy be bound to? By default, we'll bind to all addresses on port 25565.
bind = "0.0.0.0:25565"
@@ -43,7 +43,7 @@ forwarding-secret-file = "forwarding.secret"
# Announce whether or not your server supports Forge. If you run a modded server, we
# suggest turning this on.
#
-# If your network runs one modpack consistently, consider using ping-passthrough = "mods"
+# If your network runs one modpack consistently, consider using ping-passthrough.modinfo = true
# instead for a nicer display in the server list.
announce-forge = false
@@ -51,21 +51,6 @@ announce-forge = false
# any existing player who is online if a duplicate connection attempt is made.
kick-existing-players = false
-# Should Velocity pass server list ping requests to a backend server?
-# Available options:
-# - "disabled": No pass-through will be done. The velocity.toml and server-icon.png
-# will determine the initial server list ping response.
-# - "mods": Passes only the mod list from your backend server into the response.
-# The first server in your try list (or forced host) with a mod list will be
-# used. If no backend servers can be contacted, Velocity won't display any
-# mod information.
-# - "description": Uses the description and mod list from the backend server. The first
-# server in the try (or forced host) list that responds is used for the
-# description and mod list.
-# - "all": Uses the backend server's response as the proxy response. The Velocity
-# configuration is used if no servers could be contacted.
-ping-passthrough = "DISABLED"
-
# If enabled (default is false), then a sample of the online players on the proxy will be visible
# when hovering over the player count in the server list.
# This doesn't have any effect when ping passthrough is set to either "description" or "all".
@@ -74,6 +59,18 @@ sample-players-in-ping = false
# If not enabled (default is true) player IP addresses will be replaced by in logs
enable-player-address-logging = true
+[ping-passthrough]
+# Should Velocity pass the version number from the backend server when responding to server list ping requests?
+version = false
+# Should Velocity pass the player count from the backend server when responding to server list ping requests?
+players = false
+# Should Velocity pass the description from the backend server when responding to server list ping requests?
+description = false
+# Should Velocity pass the favicon (also known as the server icon) from the backend server when responding to server list ping requests?
+favicon = false
+# Should Velocity pass the mod list from the backend server when responding to server list ping requests?
+modinfo = false
+
[packet-limiter]
# Size of the moving time window in seconds used to calculate average rates.
# A larger window tolerates short bursts while still enforcing the configured limits over time.
From 15b6ddd1bf2cd0ac115a810fce88b9edf5dee58b Mon Sep 17 00:00:00 2001
From: Wouter Gritter
Date: Sun, 13 Sep 2026 12:41:56 +0200
Subject: [PATCH 09/18] Various dependency bumps
---
gradle/libs.versions.toml | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0a3b79af..f00c0aca 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,12 +3,12 @@ configurate3 = "3.7.3"
configurate4 = "4.2.0"
flare = "2.0.1"
log4j = "2.26.1"
-netty = "4.2.16.Final"
+netty = "4.2.18.Final"
[plugins]
fill = "io.papermc.fill.gradle:1.0.12"
shadow = "com.gradleup.shadow:9.6.1"
-spotless = "com.diffplug.spotless:8.9.0"
+spotless = "com.diffplug.spotless:8.10.2"
[libraries]
adventure-bom = "net.kyori:adventure-bom:5.2.0"
@@ -19,7 +19,7 @@ auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.1
brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
bstats = "org.bstats:bstats-base:3.2.1"
caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.4"
-checker-qual = "org.checkerframework:checker-qual:4.2.1"
+checker-qual = "org.checkerframework:checker-qual:4.2.3"
checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3"
completablefutures = "com.spotify:completable-futures:0.3.6"
configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" }
@@ -34,10 +34,10 @@ flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" }
flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" }
jline = "org.jline:jline-terminal-jni:4.3.1"
jopt = "net.sf.jopt-simple:jopt-simple:5.0.4"
-junit = "org.junit.jupiter:junit-jupiter:6.1.2"
+junit = "org.junit.jupiter:junit-jupiter:6.1.3"
jspecify = "org.jspecify:jspecify:1.0.1"
kyori-ansi = "net.kyori:ansi:1.1.1"
-guava = "com.google.guava:guava:33.6.0-jre"
+guava = "com.google.guava:guava:33.7.1-jre"
gson = "com.google.code.gson:gson:2.14.0"
guice = "com.google.inject:guice:7.0.0"
lmbda = "org.lanternpowered:lmbda:3.0.0"
@@ -55,9 +55,9 @@ netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll
netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" }
netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" }
nightconfig = "com.electronwill.night-config:toml:3.9.0"
-slf4j = "org.slf4j:slf4j-api:2.0.18"
-snakeyaml = "org.yaml:snakeyaml:2.6"
-spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.10.3"
+slf4j = "org.slf4j:slf4j-api:2.0.19"
+snakeyaml = "org.yaml:snakeyaml:2.7"
+spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.10.4"
terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0"
[bundles]
From bc553001230ba01fc41a9a8e0aad1b21b87fabfc Mon Sep 17 00:00:00 2001
From: Wouter Gritter
Date: Sun, 13 Sep 2026 14:01:39 +0200
Subject: [PATCH 10/18] Bump `com.puppycrawl.tools:checkstyle` to `14.1.0` and
fix warnings that were false-negatives before [ci skip]
Checkstyle fixed a bunch of false-negatives; linting errors in the source that should've been caught according to our settings. Rather than updating `checkstyle.xml` to work around these, this commit also properly fixes these checkstyle errors.
---
.../api/event/connection/PreLoginEvent.java | 1 +
.../event/player/PlayerClientBrandEvent.java | 6 +-
.../PlayerConfigurationEvent.java | 1 +
.../PlayerEnterConfigurationEvent.java | 1 +
.../PlayerEnteredConfigurationEvent.java | 1 +
.../PlayerFinishConfigurationEvent.java | 1 +
.../PlayerFinishedConfigurationEvent.java | 1 +
.../api/network/ProtocolState.java | 5 ++
.../api/permission/Tristate.java | 1 -
.../com/velocitypowered/api/proxy/Player.java | 15 ++---
.../api/proxy/crypto/KeySigned.java | 1 +
.../api/proxy/player/TabListEntry.java | 3 +
gradle/libs.versions.toml | 2 +-
.../natives/util/MoreByteBufUtils.java | 4 +-
.../velocitypowered/proxy/VelocityServer.java | 2 +-
.../proxy/config/PingPassthroughMode.java | 2 +-
.../handler/ModernResourcePackHandler.java | 2 +-
.../handler/ResourcePackHandler.java | 1 +
.../proxy/event/VelocityEventManager.java | 60 +++++++++----------
.../proxy/protocol/ProtocolUtils.java | 6 +-
20 files changed, 66 insertions(+), 50 deletions(-)
diff --git a/api/src/main/java/com/velocitypowered/api/event/connection/PreLoginEvent.java b/api/src/main/java/com/velocitypowered/api/event/connection/PreLoginEvent.java
index 952cb091..3407bcdb 100644
--- a/api/src/main/java/com/velocitypowered/api/event/connection/PreLoginEvent.java
+++ b/api/src/main/java/com/velocitypowered/api/event/connection/PreLoginEvent.java
@@ -73,6 +73,7 @@ public final class PreLoginEvent implements ResultedEventThis value is {@code null} on 1.19.2 and lower,
* up to 1.20.1 it is optional and from 1.20.2 it will always be available.
*
diff --git a/api/src/main/java/com/velocitypowered/api/event/player/PlayerClientBrandEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/PlayerClientBrandEvent.java
index 268d5b6a..2a671ac6 100644
--- a/api/src/main/java/com/velocitypowered/api/event/player/PlayerClientBrandEvent.java
+++ b/api/src/main/java/com/velocitypowered/api/event/player/PlayerClientBrandEvent.java
@@ -40,9 +40,9 @@ public final class PlayerClientBrandEvent {
@Override
public String toString() {
return "PlayerClientBrandEvent{"
- + "player=" + player
- + ", brand='" + brand + '\''
- + '}';
+ + "player=" + player
+ + ", brand='" + brand + '\''
+ + '}';
}
}
diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerConfigurationEvent.java
index 6e042af1..60b172f6 100644
--- a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerConfigurationEvent.java
+++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerConfigurationEvent.java
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
/**
* This event is executed when a player entered the configuration state and can be configured by Velocity.
+ *
* Velocity will wait for this event before continuing/ending the configuration state.
*
* @param player The player who can be configured.
diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnterConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnterConfigurationEvent.java
index 05d6c2af..01782d63 100644
--- a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnterConfigurationEvent.java
+++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnterConfigurationEvent.java
@@ -15,6 +15,7 @@ import org.jetbrains.annotations.NotNull;
/**
* This event is executed when a player is about to enter the configuration state.
* It is not called for the initial configuration of a player after login.
+ *
* Velocity will wait for this event before asking the client to enter configuration state.
* However due to backend server being unable to keep the connection alive during state changes,
* Velocity will only wait for a maximum of 5 seconds.
diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnteredConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnteredConfigurationEvent.java
index c1677706..6560f7bc 100644
--- a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnteredConfigurationEvent.java
+++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnteredConfigurationEvent.java
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
/**
* This event is executed when a player has entered the configuration state.
+ *
* From this moment on, until the {@link PlayerFinishedConfigurationEvent} is executed,
* the {@linkplain Player#getProtocolState()} method is guaranteed
* to return {@link ProtocolState#CONFIGURATION}.
diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishConfigurationEvent.java
index 50df5a8a..ab87974a 100644
--- a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishConfigurationEvent.java
+++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishConfigurationEvent.java
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
/**
* This event is executed when a player is about to finish the configuration state.
+ *
* Velocity will wait for this event before asking the client to finish the configuration state.
* However due to backend server being unable to keep the connection alive during state changes,
* Velocity will only wait for a maximum of 5 seconds. If you need to hold a player in configuration
diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishedConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishedConfigurationEvent.java
index 517f119c..994a1d41 100644
--- a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishedConfigurationEvent.java
+++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishedConfigurationEvent.java
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
/**
* This event is executed when a player has finished the configuration state.
+ *
*
From this moment on, the {@link Player#getProtocolState()} method
* will return {@link ProtocolState#PLAY}.
*
diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolState.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolState.java
index 0ecc8287..cd9b04d5 100644
--- a/api/src/main/java/com/velocitypowered/api/network/ProtocolState.java
+++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolState.java
@@ -16,6 +16,7 @@ package com.velocitypowered.api.network;
public enum ProtocolState {
/**
* Initial connection State.
+ *
* This status can be caused by a {@link HandshakeIntent#STATUS},
* {@link HandshakeIntent#LOGIN} or {@link HandshakeIntent#TRANSFER} intent.
* If the intent is LOGIN or TRANSFER, the next state will be {@link #LOGIN},
@@ -24,6 +25,7 @@ public enum ProtocolState {
HANDSHAKE,
/**
* Ping State of a connection.
+ *
* Connections with the {@link HandshakeIntent#STATUS} intent will pass through this state
* and be disconnected after it requests the ping from the server
* and the server responds with the respective ping.
@@ -31,11 +33,13 @@ public enum ProtocolState {
STATUS,
/**
* Authentication State of a connection.
+ *
* At this moment the player is authenticating with the authentication servers.
*/
LOGIN,
/**
* Configuration State of a connection.
+ *
* At this point the player allows the server to send information
* such as resource packs and plugin messages, at the same time the player
* will send his client brand and the respective plugin messages
@@ -46,6 +50,7 @@ public enum ProtocolState {
CONFIGURATION,
/**
* Game State of a connection.
+ *
*
In this state is where the whole game runs, the server is able to change
* the player's state to {@link #CONFIGURATION} as needed in versions 1.20.2 and higher.
*/
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 ddbe26a8..0d81b1dc 100644
--- a/api/src/main/java/com/velocitypowered/api/permission/Tristate.java
+++ b/api/src/main/java/com/velocitypowered/api/permission/Tristate.java
@@ -15,7 +15,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* Represents three different states of a setting.
*
* Possible values:
- *
*
* - {@link #TRUE} - a positive setting
* - {@link #FALSE} - a negative (negated) setting
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 c4b8703f..b209954d 100644
--- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java
+++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java
@@ -244,7 +244,7 @@ public interface Player extends
* Gets the {@link ResourcePackInfo} of the currently applied
* resource-pack or null if none.
*
- * Note that since 1.20.3 it is no longer recommended to use
+ *
Note that since 1.20.3 it is no longer recommended to use
* this method as it will only return the last applied
* resource pack. To get all applied resource packs, use
* {@link #getAppliedResourcePacks()} instead.
@@ -260,7 +260,7 @@ public interface Player extends
* the user is currently downloading or is currently
* prompted to install or null if none.
*
- * Note that since 1.20.3 it is no longer recommended to use
+ *
Note that since 1.20.3 it is no longer recommended to use
* this method as it will only return the last pending
* resource pack. To get all pending resource packs, use
* {@link #getPendingResourcePacks()} instead.
@@ -315,6 +315,7 @@ public interface Player extends
/**
* {@inheritDoc}
+ *
* Note that this method does not send a plugin message to the server the player
* is connected to. You should only use this method if you are trying to communicate
* with a mod that is installed on the player's client.
@@ -400,7 +401,7 @@ public interface Player extends
* and will not perform any actions.
* @see #playSound(Sound, Sound.Emitter)
* @see
- * Unsupported Adventure Operations
+ * Unsupported Adventure Operations
*/
@Override
default void playSound(@NotNull Sound sound) {
@@ -413,7 +414,7 @@ public interface Player extends
* and will not perform any actions.
* @see #playSound(Sound, Sound.Emitter)
* @see
- * Unsupported Adventure Operations
+ * Unsupported Adventure Operations
*/
@Override
default void playSound(@NotNull Sound sound, double x, double y, double z) {
@@ -456,7 +457,7 @@ public interface Player extends
* and will not perform any actions.
*
* @see
- * Unsupported Adventure Operations
+ * Unsupported Adventure Operations
*/
@Override
default void openBook(@NotNull Book book) {
@@ -469,7 +470,7 @@ public interface Player extends
* and will not perform any actions.
*
* @see
- * Unsupported Adventure Operations
+ * Unsupported Adventure Operations
*/
@Override
default void showDialog(@NotNull DialogLike dialog) {
@@ -482,7 +483,7 @@ public interface Player extends
* and will not perform any actions.
*
* @see
- * Unsupported Adventure Operations
+ * Unsupported Adventure Operations
*/
@Override
default void closeDialog() {
diff --git a/api/src/main/java/com/velocitypowered/api/proxy/crypto/KeySigned.java b/api/src/main/java/com/velocitypowered/api/proxy/crypto/KeySigned.java
index 8ad64c18..c653218e 100644
--- a/api/src/main/java/com/velocitypowered/api/proxy/crypto/KeySigned.java
+++ b/api/src/main/java/com/velocitypowered/api/proxy/crypto/KeySigned.java
@@ -56,6 +56,7 @@ public interface KeySigned {
* Validates the signature, expiry temporal and key against the
* signer public key. Note: This will **not** check for
* expiry. You can check for expiry with {@link KeySigned#hasExpired()}.
+ *
* DOES NOT WORK YET FOR MESSAGES AND COMMANDS!
* Addendum: Does not work for 1.19.1 until the user has authenticated.
*
diff --git a/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java b/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java
index aea45287..57a42392 100644
--- a/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java
+++ b/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java
@@ -237,8 +237,11 @@ public interface TabListEntry extends KeyIdentifiable {
/**
* Sets the {@link IdentifiedKey} of the {@link TabListEntry}.
+ *
* This only works for players currently not connected to this proxy.
+ *
* For any player currently connected to this proxy this will be filled automatically.
+ *
* Will ignore mismatching key revisions data.
*
* @param chatSession session to set
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f00c0aca..30325a81 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -20,7 +20,7 @@ brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
bstats = "org.bstats:bstats-base:3.2.1"
caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.4"
checker-qual = "org.checkerframework:checker-qual:4.2.3"
-checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3"
+checkstyle = "com.puppycrawl.tools:checkstyle:14.1.0"
completablefutures = "com.spotify:completable-futures:0.3.6"
configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" }
configurate3-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate3" }
diff --git a/native/src/main/java/com/velocitypowered/natives/util/MoreByteBufUtils.java b/native/src/main/java/com/velocitypowered/natives/util/MoreByteBufUtils.java
index 13f4f0c5..886f2081 100644
--- a/native/src/main/java/com/velocitypowered/natives/util/MoreByteBufUtils.java
+++ b/native/src/main/java/com/velocitypowered/natives/util/MoreByteBufUtils.java
@@ -54,8 +54,8 @@ public class MoreByteBufUtils {
BufferPreference preferred = nativeStuff.preferredBufferType();
return switch (preferred) {
case DIRECT_PREFERRED, HEAP_PREFERRED ->
- // The native prefers this type, but doesn't strictly require we provide it.
- true;
+ // The native prefers this type, but doesn't strictly require we provide it.
+ true;
case DIRECT_REQUIRED -> buf.hasMemoryAddress();
case HEAP_REQUIRED -> buf.hasArray();
};
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
index 7c027dd6..4fbeabb9 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
@@ -653,7 +653,7 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
}
/**
- * Calls {@link #shutdown(boolean, Component)} with the default reason "Proxy shutting down."
+ * Calls {@link #shutdown(boolean, Component)} with the default reason "Proxy shutting down".
*
* @param explicitExit whether the user explicitly shut down the proxy
*/
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java b/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java
index aa73cefd..623e3d07 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java
@@ -59,6 +59,6 @@ public record PingPassthroughMode(boolean version, boolean players,
public boolean enabled() {
return this.version || this.players || this.description || this.favicon
- || this.modinfo;
+ || this.modinfo;
}
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java
index f0fd5e08..3bef8a4b 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java
@@ -37,7 +37,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
- * Modern (Minecraft 1.20.3+) ResourcePackHandler
+ * Modern (Minecraft 1.20.3+) ResourcePackHandler.
*/
public final class ModernResourcePackHandler extends ResourcePackHandler {
private final ListMultimap outstandingResourcePacks =
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java
index b3843885..dc04b912 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java
@@ -118,6 +118,7 @@ public abstract sealed class ResourcePackHandler
/**
* Processes a client response to a sent resource-pack.
+ *
* Cases in which no action will be taken:
*
*
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 b56b907c..8f12dca0 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java
@@ -545,6 +545,36 @@ public class VelocityEventManager implements EventManager {
}
}
+ private void fire(final @Nullable CompletableFuture future, final E event,
+ final int offset, final boolean currentlyAsync, final HandlerRegistration[] registrations) {
+ for (int i = offset; i < registrations.length; i++) {
+ final HandlerRegistration registration = registrations[i];
+ try {
+ final EventTask eventTask = registration.handler.executeAsync(event);
+ if (eventTask == null) {
+ continue;
+ }
+ final ContinuationTask continuationTask = new ContinuationTask<>(eventTask,
+ registrations, future, event, i, currentlyAsync);
+ if (currentlyAsync || !eventTask.requiresAsync()) {
+ if (continuationTask.execute()) {
+ continue;
+ }
+ } else {
+ registration.plugin.getExecutorService().execute(continuationTask);
+ }
+ // fire will continue in another thread once the async task is
+ // executed and the continuation is resumed
+ return;
+ } catch (final Throwable t) {
+ logHandlerException(registration, t);
+ }
+ }
+ if (future != null) {
+ future.complete(event);
+ }
+ }
+
private static final int TASK_STATE_DEFAULT = 0;
private static final int TASK_STATE_EXECUTING = 1;
private static final int TASK_STATE_CONTINUE_IMMEDIATELY = 2;
@@ -669,36 +699,6 @@ public class VelocityEventManager implements EventManager {
}
}
- private void fire(final @Nullable CompletableFuture future, final E event,
- final int offset, final boolean currentlyAsync, final HandlerRegistration[] registrations) {
- for (int i = offset; i < registrations.length; i++) {
- final HandlerRegistration registration = registrations[i];
- try {
- final EventTask eventTask = registration.handler.executeAsync(event);
- if (eventTask == null) {
- continue;
- }
- final ContinuationTask continuationTask = new ContinuationTask<>(eventTask,
- registrations, future, event, i, currentlyAsync);
- if (currentlyAsync || !eventTask.requiresAsync()) {
- if (continuationTask.execute()) {
- continue;
- }
- } else {
- registration.plugin.getExecutorService().execute(continuationTask);
- }
- // fire will continue in another thread once the async task is
- // executed and the continuation is resumed
- return;
- } catch (final Throwable t) {
- logHandlerException(registration, t);
- }
- }
- if (future != null) {
- future.complete(event);
- }
- }
-
private static void logHandlerException(
final HandlerRegistration registration, final Throwable t) {
final PluginDescription pluginDescription = registration.plugin.getDescription();
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 3cb64382..328252ea 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
@@ -644,7 +644,7 @@ public enum ProtocolUtils {
private static final int FORGE_MAX_ARRAY_LENGTH = Integer.MAX_VALUE & 0x1FFF9A;
/**
- * Reads an byte array for legacy version 1.7 from the specified {@code buf}
+ * Reads an byte array for legacy version 1.7 from the specified {@code buf}.
*
* @param buf the buffer to read from
* @return the read byte array
@@ -685,7 +685,7 @@ public enum ProtocolUtils {
}
/**
- * Writes an byte array for legacy version 1.7 to the specified {@code buf}
+ * Writes an byte array for legacy version 1.7 to the specified {@code buf}.
*
* @param b array
* @param buf buf
@@ -709,7 +709,7 @@ public enum ProtocolUtils {
}
/**
- * Writes an {@link ByteBuf} for legacy version 1.7 to the specified {@code buf}
+ * Writes an {@link ByteBuf} for legacy version 1.7 to the specified {@code buf}.
*
* @param b array
* @param buf buf
From 1b48f4053422e39b556e323a6525e2db28022cfc Mon Sep 17 00:00:00 2001
From: Braydon
Date: Mon, 14 Sep 2026 12:34:50 -0400
Subject: [PATCH 11/18] Fix config reload not removing deleted servers (#1882)
* Fix config reload not removing deleted servers
When reloading velocity.toml, unregister servers that were removed from
the config and evacuate any connected players to fallback servers.
* Requested PR fixes
* Make Expected ConnectedPlayer exceptions more detailed
---
.../velocitypowered/proxy/VelocityServer.java | 41 +++++++++++++++----
1 file changed, 32 insertions(+), 9 deletions(-)
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
index 4fbeabb9..b3059f9e 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
@@ -488,23 +488,46 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
return false;
}
- // Re-register servers. If a server is being replaced, make sure to note what players need to
- // move back to a fallback server.
+ // Re-register servers. If a server is being replaced or removed, make sure to note what
+ // players need to move back to a fallback server.
Collection evacuate = new ArrayList<>();
+ for (Map.Entry entry : configuration.getServers().entrySet()) {
+ if (newConfiguration.getServers().keySet().stream()
+ .anyMatch(name -> name.equalsIgnoreCase(entry.getKey()))) {
+ continue;
+ }
+
+ RegisteredServer rs = servers.getServer(entry.getKey()).orElse(null);
+ if (rs == null) {
+ continue;
+ }
+
+ for (Player player : rs.getPlayersConnected()) {
+ if (!(player instanceof ConnectedPlayer)) {
+ throw new IllegalStateException("Expected ConnectedPlayer for player " + player
+ + " on server " + rs.getServerInfo().getName() + ", got "
+ + player.getClass().getName());
+ }
+ evacuate.add((ConnectedPlayer) player);
+ }
+ servers.unregister(rs.getServerInfo());
+ }
+
for (Map.Entry entry : newConfiguration.getServers().entrySet()) {
ServerInfo newInfo = new ServerInfo(entry.getKey(), AddressUtil.parseAddress(entry.getValue()));
- Optional rs = servers.getServer(entry.getKey());
- if (rs.isEmpty()) {
+ RegisteredServer rs = servers.getServer(entry.getKey()).orElse(null);
+ if (rs == null) {
servers.register(newInfo);
- } else if (!rs.get().getServerInfo().equals(newInfo)) {
- for (Player player : rs.get().getPlayersConnected()) {
+ } else if (!rs.getServerInfo().equals(newInfo)) {
+ for (Player player : rs.getPlayersConnected()) {
if (!(player instanceof ConnectedPlayer)) {
- throw new IllegalStateException("ConnectedPlayer not found for player " + player
- + " in server " + rs.get().getServerInfo().getName());
+ throw new IllegalStateException("Expected ConnectedPlayer for player " + player
+ + " on server " + rs.getServerInfo().getName() + ", got "
+ + player.getClass().getName());
}
evacuate.add((ConnectedPlayer) player);
}
- servers.unregister(rs.get().getServerInfo());
+ servers.unregister(rs.getServerInfo());
servers.register(newInfo);
}
}
From dff94da2b3d79bff1874ecdf7320e178a8480625 Mon Sep 17 00:00:00 2001
From: Aaron <71191102+RealBauHD@users.noreply.github.com>
Date: Mon, 14 Sep 2026 18:34:59 +0200
Subject: [PATCH 12/18] Minecraft 26.3 (#1867)
* 26.3-snapshot-7
* 26.3-snapshot-8
* 26.3-snapshot-9
* 26.3-snapshot-10
* 26.3-pre-1
* 26.3-pre-2
* feat: add post effects packet
* chore: write game modes as var int
* 26.3-pre-3
* 26.3-rc-1
* 26.3
* chore: add key array cap
* fix: add a zero
---
.../api/network/ProtocolVersion.java | 3 +-
.../connection/MinecraftSessionHandler.java | 5 +
.../proxy/protocol/ProtocolUtils.java | 8 +-
.../proxy/protocol/StateRegistry.java | 104 ++++++++++++------
.../packet/ClientboundPostEffectsPacket.java | 53 +++++++++
.../proxy/protocol/packet/JoinGamePacket.java | 30 +++--
.../proxy/protocol/packet/RespawnPacket.java | 40 +++++--
.../brigadier/ArgumentPropertyRegistry.java | 12 +-
8 files changed, 196 insertions(+), 59 deletions(-)
create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundPostEffectsPacket.java
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 7f6912a6..6653ff9c 100644
--- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java
+++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java
@@ -96,7 +96,8 @@ public enum ProtocolVersion implements Ordered {
MINECRAFT_1_21_9(773, "1.21.9", "1.21.10"),
MINECRAFT_1_21_11(774, "1.21.11"),
MINECRAFT_26_1(775, "26.1", "26.1.1", "26.1.2"),
- MINECRAFT_26_2(776, "26.2");
+ MINECRAFT_26_2(776, "26.2"),
+ MINECRAFT_26_3(777, "26.3");
private static final int SNAPSHOT_BIT = 30;
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 00783dbf..9df71c3f 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java
@@ -23,6 +23,7 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
+import com.velocitypowered.proxy.protocol.packet.ClientboundPostEffectsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
@@ -404,4 +405,8 @@ public interface MinecraftSessionHandler {
default boolean handle(ClientboundStopSoundPacket packet) {
return false;
}
+
+ default boolean handle(ClientboundPostEffectsPacket packet) {
+ return false;
+ }
}
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 328252ea..f5ed5d3e 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
@@ -143,6 +143,7 @@ public enum ProtocolUtils {
.build();
public static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB
+ private static final int DEFAULT_MAX_KEY_ARRAY_SIZE = 1024;
private static final int MAXIMUM_VARINT_SIZE = 5;
private static final BinaryTagType extends BinaryTag>[] BINARY_TAG_TYPES = new BinaryTagType[] {
BinaryTagTypes.END, BinaryTagTypes.BYTE, BinaryTagTypes.SHORT, BinaryTagTypes.INT,
@@ -349,15 +350,20 @@ public enum ProtocolUtils {
writeString(buf, key.asMinimalString());
}
+ public static Key[] readKeyArray(ByteBuf buf) {
+ return readKeyArray(buf, DEFAULT_MAX_KEY_ARRAY_SIZE);
+ }
+
/**
* 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) {
+ public static Key[] readKeyArray(ByteBuf buf, int cap) {
int length = readVarInt(buf);
checkFrame(length >= 0, "Got a negative-length array (%s)", length);
+ checkFrame(length <= cap, "Bad key array size (got %s, maximum is %s)", length, cap);
checkFrame(buf.isReadable(length),
"Trying to read an array that is too long (wanted %s, only have %s)", length,
buf.readableBytes());
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 b35bf1c7..1da9c424 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
@@ -47,6 +47,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_1;
+import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_3;
import static com.velocitypowered.api.network.ProtocolVersion.MINIMUM_VERSION;
import static com.velocitypowered.api.network.ProtocolVersion.SUPPORTED_VERSIONS;
import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
@@ -60,6 +61,7 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
+import com.velocitypowered.proxy.protocol.packet.ClientboundPostEffectsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
@@ -234,31 +236,44 @@ public enum StateRegistry {
map(0x06, MINECRAFT_1_20_2, false),
map(0x07, MINECRAFT_1_20_3, false),
map(0x09, MINECRAFT_1_20_5, false));
+ clientbound.register(
+ ClientboundPostEffectsPacket.class, ClientboundPostEffectsPacket::new,
+ map(0x0A, MINECRAFT_26_3, false));
clientbound.register(
ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new,
- map(0x0A, MINECRAFT_1_20_5, false));
+ map(0x0A, MINECRAFT_1_20_5, false),
+ map(0x0B, MINECRAFT_26_3, false));
clientbound.register(TransferPacket.class, TransferPacket::new,
- map(0x0B, MINECRAFT_1_20_5, false));
+ map(0x0B, MINECRAFT_1_20_5, false),
+ map(0x0C, MINECRAFT_26_3, false));
clientbound.register(ActiveFeaturesPacket.class, ActiveFeaturesPacket::new,
map(0x07, MINECRAFT_1_20_2, false),
map(0x08, MINECRAFT_1_20_3, false),
- map(0x0C, MINECRAFT_1_20_5, false));
+ map(0x0C, MINECRAFT_1_20_5, false),
+ map(0x0D, MINECRAFT_26_3, false));
clientbound.register(TagsUpdatePacket.class, TagsUpdatePacket::new,
map(0x08, MINECRAFT_1_20_2, false),
map(0x09, MINECRAFT_1_20_3, false),
- map(0x0D, MINECRAFT_1_20_5, false));
+ map(0x0D, MINECRAFT_1_20_5, false),
+ map(0x0E, MINECRAFT_26_3, false));
clientbound.register(KnownPacksPacket.class, KnownPacksPacket::new,
- map(0x0E, MINECRAFT_1_20_5, false));
+ map(0x0E, MINECRAFT_1_20_5, false),
+ map(0x0F, MINECRAFT_26_3, false));
clientbound.register(ClientboundCustomReportDetailsPacket.class, ClientboundCustomReportDetailsPacket::new,
- map(0x0F, MINECRAFT_1_21, false));
+ map(0x0F, MINECRAFT_1_21, false),
+ map(0x10, MINECRAFT_26_3, false));
clientbound.register(ClientboundServerLinksPacket.class, ClientboundServerLinksPacket::new,
- map(0x10, MINECRAFT_1_21, false));
+ map(0x10, MINECRAFT_1_21, false),
+ map(0x11, MINECRAFT_26_3, false));
clientbound.register(DialogClearPacket.class, () -> DialogClearPacket.INSTANCE,
- map(0x11, MINECRAFT_1_21_6, false));
+ map(0x11, MINECRAFT_1_21_6, false),
+ map(0x12, MINECRAFT_26_3, false));
clientbound.register(DialogShowPacket.class, () -> new DialogShowPacket(this),
- map(0x12, MINECRAFT_1_21_6, false));
+ map(0x12, MINECRAFT_1_21_6, false),
+ map(0x13, MINECRAFT_26_3, false));
clientbound.register(CodeOfConductPacket.class, CodeOfConductPacket::new,
- map(0x13, MINECRAFT_1_21_9, false));
+ map(0x13, MINECRAFT_1_21_9, false),
+ map(0x14, MINECRAFT_26_3, false));
}
},
PLAY {
@@ -410,7 +425,8 @@ public enum StateRegistry {
map(0x2D, MINECRAFT_1_21_2, false),
map(0x2F, MINECRAFT_1_21_4, false),
map(0x30, MINECRAFT_1_21_6, false),
- map(0x31, MINECRAFT_26_1, false));
+ map(0x31, MINECRAFT_26_1, false),
+ map(0x32, MINECRAFT_26_3, false));
serverbound.register(
FinishedUpdatePacket.class, () -> FinishedUpdatePacket.INSTANCE,
map(0x0B, MINECRAFT_1_20_2, false),
@@ -469,6 +485,9 @@ public enum StateRegistry {
ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new,
map(0x16, MINECRAFT_1_20_5, false),
map(0x15, MINECRAFT_1_21_5, false));
+ clientbound.register(
+ ClientboundPostEffectsPacket.class, ClientboundPostEffectsPacket::new,
+ map(0x53, MINECRAFT_26_3, false));
clientbound.register(
ClientboundSoundEntityPacket.class, ClientboundSoundEntityPacket::new,
map(0x5D, MINECRAFT_1_19_3, true),
@@ -479,7 +498,8 @@ public enum StateRegistry {
map(0x6E, MINECRAFT_1_21_2, true),
map(0x6D, MINECRAFT_1_21_5, true),
map(0x72, MINECRAFT_1_21_9, true),
- map(0x74, MINECRAFT_26_1, true));
+ map(0x74, MINECRAFT_26_1, true),
+ map(0x76, MINECRAFT_26_3, true));
clientbound.register(
ClientboundStopSoundPacket.class, ClientboundStopSoundPacket::new,
map(0x5F, MINECRAFT_1_19_3, true),
@@ -490,7 +510,8 @@ public enum StateRegistry {
map(0x71, MINECRAFT_1_21_2, true),
map(0x70, MINECRAFT_1_21_5, true),
map(0x75, MINECRAFT_1_21_9, true),
- map(0x77, MINECRAFT_26_1, true));
+ map(0x77, MINECRAFT_26_1, true),
+ map(0x79, MINECRAFT_26_3, true));
clientbound.register(
PluginMessagePacket.class,
PluginMessagePacket::new,
@@ -548,7 +569,8 @@ public enum StateRegistry {
map(0x27, MINECRAFT_1_21_2, false),
map(0x26, MINECRAFT_1_21_5, false),
map(0x2B, MINECRAFT_1_21_9, false),
- map(0x2C, MINECRAFT_26_1, false));
+ map(0x2C, MINECRAFT_26_1, false),
+ map(0x2D, MINECRAFT_26_3, false));
clientbound.register(
JoinGamePacket.class,
JoinGamePacket::new,
@@ -569,7 +591,8 @@ public enum StateRegistry {
map(0x2C, MINECRAFT_1_21_2, false),
map(0x2B, MINECRAFT_1_21_5, false),
map(0x30, MINECRAFT_1_21_9, false),
- map(0x31, MINECRAFT_26_1, false));
+ map(0x31, MINECRAFT_26_1, false),
+ map(0x32, MINECRAFT_26_3, false));
clientbound.register(
RespawnPacket.class,
RespawnPacket::new,
@@ -593,7 +616,8 @@ public enum StateRegistry {
map(0x4C, MINECRAFT_1_21_2, true),
map(0x4B, MINECRAFT_1_21_5, true),
map(0x50, MINECRAFT_1_21_9, true),
- map(0x52, MINECRAFT_26_1, true));
+ map(0x52, MINECRAFT_26_1, true),
+ map(0x54, MINECRAFT_26_3, true));
clientbound.register(
RemoveResourcePackPacket.class,
RemoveResourcePackPacket::new,
@@ -602,7 +626,8 @@ public enum StateRegistry {
map(0x4A, MINECRAFT_1_21_2, false),
map(0x49, MINECRAFT_1_21_5, false),
map(0x4E, MINECRAFT_1_21_9, false),
- map(0x50, MINECRAFT_26_1, false));
+ map(0x50, MINECRAFT_26_1, false),
+ map(0x51, MINECRAFT_26_3, false));
clientbound.register(
ResourcePackRequestPacket.class,
ResourcePackRequestPacket::new,
@@ -626,7 +651,8 @@ public enum StateRegistry {
map(0x4B, MINECRAFT_1_21_2, false),
map(0x4A, MINECRAFT_1_21_5, false),
map(0x4F, MINECRAFT_1_21_9, false),
- map(0x51, MINECRAFT_26_1, false));
+ map(0x51, MINECRAFT_26_1, false),
+ map(0x52, MINECRAFT_26_3, false));
clientbound.register(
HeaderAndFooterPacket.class,
HeaderAndFooterPacket::new,
@@ -651,7 +677,8 @@ public enum StateRegistry {
map(0x74, MINECRAFT_1_21_2, true),
map(0x73, MINECRAFT_1_21_5, true),
map(0x78, MINECRAFT_1_21_9, true),
- map(0x7A, MINECRAFT_26_1, true));
+ map(0x7A, MINECRAFT_26_1, true),
+ map(0x7D, MINECRAFT_26_3, true));
clientbound.register(
LegacyTitlePacket.class,
LegacyTitlePacket::new,
@@ -675,7 +702,8 @@ public enum StateRegistry {
map(0x6A, MINECRAFT_1_21_2, true),
map(0x69, MINECRAFT_1_21_5, true),
map(0x6E, MINECRAFT_1_21_9, true),
- map(0x70, MINECRAFT_26_1, true));
+ map(0x70, MINECRAFT_26_1, true),
+ map(0x72, MINECRAFT_26_3, true));
clientbound.register(
TitleTextPacket.class,
TitleTextPacket::new,
@@ -690,7 +718,8 @@ public enum StateRegistry {
map(0x6C, MINECRAFT_1_21_2, true),
map(0x6B, MINECRAFT_1_21_5, true),
map(0x70, MINECRAFT_1_21_9, true),
- map(0x72, MINECRAFT_26_1, true));
+ map(0x72, MINECRAFT_26_1, true),
+ map(0x74, MINECRAFT_26_3, true));
clientbound.register(
TitleActionbarPacket.class,
TitleActionbarPacket::new,
@@ -705,7 +734,8 @@ public enum StateRegistry {
map(0x51, MINECRAFT_1_21_2, true),
map(0x50, MINECRAFT_1_21_5, true),
map(0x55, MINECRAFT_1_21_9, true),
- map(0x57, MINECRAFT_26_1, true));
+ map(0x57, MINECRAFT_26_1, true),
+ map(0x59, MINECRAFT_26_3, true));
clientbound.register(
TitleTimesPacket.class,
TitleTimesPacket::new,
@@ -720,7 +750,8 @@ public enum StateRegistry {
map(0x6D, MINECRAFT_1_21_2, true),
map(0x6C, MINECRAFT_1_21_5, true),
map(0x71, MINECRAFT_1_21_9, true),
- map(0x73, MINECRAFT_26_1, true));
+ map(0x73, MINECRAFT_26_1, true),
+ map(0x75, MINECRAFT_26_3, true));
clientbound.register(
TitleClearPacket.class,
TitleClearPacket::new,
@@ -752,7 +783,8 @@ public enum StateRegistry {
map(0x3F, MINECRAFT_1_21_2, false),
map(0x3E, MINECRAFT_1_21_5, false),
map(0x43, MINECRAFT_1_21_9, false),
- map(0x45, MINECRAFT_26_1, false));
+ map(0x45, MINECRAFT_26_1, false),
+ map(0x46, MINECRAFT_26_3, false));
clientbound.register(
UpsertPlayerInfoPacket.class,
UpsertPlayerInfoPacket::new,
@@ -763,14 +795,16 @@ public enum StateRegistry {
map(0x40, MINECRAFT_1_21_2, false),
map(0x3F, MINECRAFT_1_21_5, false),
map(0x44, MINECRAFT_1_21_9, false),
- map(0x46, MINECRAFT_26_1, false));
+ map(0x46, MINECRAFT_26_1, false),
+ map(0x47, MINECRAFT_26_3, false));
clientbound.register(
ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new,
map(0x6B, MINECRAFT_1_20_5, false),
map(0x72, MINECRAFT_1_21_2, false),
map(0x71, MINECRAFT_1_21_5, false),
map(0x76, MINECRAFT_1_21_9, false),
- map(0x78, MINECRAFT_26_1, false));
+ map(0x78, MINECRAFT_26_1, false),
+ map(0x7A, MINECRAFT_26_3, false));
clientbound.register(
SystemChatPacket.class,
SystemChatPacket::new,
@@ -784,7 +818,8 @@ public enum StateRegistry {
map(0x73, MINECRAFT_1_21_2, true),
map(0x72, MINECRAFT_1_21_5, true),
map(0x77, MINECRAFT_1_21_9, true),
- map(0x79, MINECRAFT_26_1, true));
+ map(0x79, MINECRAFT_26_1, true),
+ map(0x7C, MINECRAFT_26_3, true));
clientbound.register(
PlayerChatCompletionPacket.class,
PlayerChatCompletionPacket::new,
@@ -807,7 +842,8 @@ public enum StateRegistry {
map(0x50, MINECRAFT_1_21_2, false),
map(0x4F, MINECRAFT_1_21_5, false),
map(0x54, MINECRAFT_1_21_9, false),
- map(0x56, MINECRAFT_26_1, false));
+ map(0x56, MINECRAFT_26_1, false),
+ map(0x58, MINECRAFT_26_3, false));
clientbound.register(
StartUpdatePacket.class,
() -> StartUpdatePacket.INSTANCE,
@@ -817,7 +853,8 @@ public enum StateRegistry {
map(0x70, MINECRAFT_1_21_2, false),
map(0x6F, MINECRAFT_1_21_5, false),
map(0x74, MINECRAFT_1_21_9, false),
- map(0x76, MINECRAFT_26_1, false));
+ map(0x76, MINECRAFT_26_1, false),
+ map(0x78, MINECRAFT_26_3, false));
clientbound.register(
BundleDelimiterPacket.class,
() -> BundleDelimiterPacket.INSTANCE,
@@ -828,21 +865,24 @@ public enum StateRegistry {
map(0x73, MINECRAFT_1_20_5, false),
map(0x7A, MINECRAFT_1_21_2, false),
map(0x7F, MINECRAFT_1_21_9, false),
- map(0x81, MINECRAFT_26_1, false));
+ map(0x81, MINECRAFT_26_1, false),
+ map(0x84, MINECRAFT_26_3, false));
clientbound.register(
ClientboundCustomReportDetailsPacket.class,
ClientboundCustomReportDetailsPacket::new,
map(0x7A, MINECRAFT_1_21, false),
map(0x81, MINECRAFT_1_21_2, false),
map(0x86, MINECRAFT_1_21_9, false),
- map(0x88, MINECRAFT_26_1, false));
+ map(0x88, MINECRAFT_26_1, false),
+ map(0x8B, MINECRAFT_26_3, false));
clientbound.register(
ClientboundServerLinksPacket.class,
ClientboundServerLinksPacket::new,
map(0x7B, MINECRAFT_1_21, false),
map(0x82, MINECRAFT_1_21_2, false),
map(0x87, MINECRAFT_1_21_9, false),
- map(0x89, MINECRAFT_26_1, false));
+ map(0x89, MINECRAFT_26_1, false),
+ map(0x8C, MINECRAFT_26_3, false));
}
},
LOGIN {
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundPostEffectsPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundPostEffectsPacket.java
new file mode 100644
index 00000000..19d4f403
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundPostEffectsPacket.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2018-2026 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 com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
+import io.netty.buffer.ByteBuf;
+import net.kyori.adventure.key.Key;
+
+public final class ClientboundPostEffectsPacket implements MinecraftPacket {
+
+ private Key[] postEffects;
+
+ public ClientboundPostEffectsPacket() {
+ }
+
+ public ClientboundPostEffectsPacket(Key[] postEffects) {
+ this.postEffects = postEffects;
+ }
+
+ @Override
+ public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) {
+ this.postEffects = ProtocolUtils.readKeyArray(buf);
+ }
+
+ @Override
+ public void encode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) {
+ ProtocolUtils.writeKeyArray(buf, this.postEffects);
+ }
+
+ @Override
+ public boolean handle(MinecraftSessionHandler handler) {
+ return handler.handle(this);
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java
index 645dc7b6..ca777a27 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java
@@ -32,7 +32,7 @@ public class JoinGamePacket implements MinecraftPacket {
private static final BinaryTagIO.Reader JOINGAME_READER = BinaryTagIO.reader(4 * 1024 * 1024);
private int entityId;
- private short gamemode;
+ private int gamemode;
private int dimension;
private long partialHashedSeed; // 1.15+
private short difficulty;
@@ -47,7 +47,7 @@ public class JoinGamePacket implements MinecraftPacket {
private CompoundBinaryTag registry; // 1.16+
private DimensionInfo dimensionInfo; // 1.16+
private CompoundBinaryTag currentDimensionData; // 1.16.2+
- private short previousGamemode; // 1.16+
+ private int previousGamemode; // 1.16+
private int simulationDistance; // 1.18+
private @Nullable Pair lastDeathPosition; // 1.19+
private int portalCooldown; // 1.20+
@@ -63,11 +63,11 @@ public class JoinGamePacket implements MinecraftPacket {
this.entityId = entityId;
}
- public short getGamemode() {
+ public int getGamemode() {
return gamemode;
}
- public void setGamemode(short gamemode) {
+ public void setGamemode(int gamemode) {
this.gamemode = gamemode;
}
@@ -131,11 +131,11 @@ public class JoinGamePacket implements MinecraftPacket {
this.dimensionInfo = dimensionInfo;
}
- public short getPreviousGamemode() {
+ public int getPreviousGamemode() {
return previousGamemode;
}
- public void setPreviousGamemode(short previousGamemode) {
+ public void setPreviousGamemode(int previousGamemode) {
this.previousGamemode = previousGamemode;
}
@@ -345,8 +345,13 @@ public class JoinGamePacket implements MinecraftPacket {
String levelName = ProtocolUtils.readString(buf);
this.partialHashedSeed = buf.readLong();
- this.gamemode = buf.readByte();
- this.previousGamemode = buf.readByte();
+ if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) {
+ this.gamemode = ProtocolUtils.readVarInt(buf);
+ this.previousGamemode = ProtocolUtils.readVarInt(buf); // game mode + 1 or 0
+ } else {
+ this.gamemode = buf.readByte();
+ this.previousGamemode = buf.readByte(); // game mode or -1
+ }
boolean isDebug = buf.readBoolean();
boolean isFlat = buf.readBoolean();
@@ -498,8 +503,13 @@ public class JoinGamePacket implements MinecraftPacket {
ProtocolUtils.writeString(buf, dimensionInfo.getLevelName());
buf.writeLong(partialHashedSeed);
- buf.writeByte(gamemode);
- buf.writeByte(previousGamemode);
+ if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) {
+ ProtocolUtils.writeVarInt(buf, this.gamemode);
+ ProtocolUtils.writeVarInt(buf, this.previousGamemode);
+ } else {
+ buf.writeByte(this.gamemode);
+ buf.writeByte(this.previousGamemode);
+ }
buf.writeBoolean(dimensionInfo.isDebugType());
buf.writeBoolean(dimensionInfo.isFlat());
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java
index fd9c8ca7..d20c253f 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java
@@ -33,11 +33,11 @@ public class RespawnPacket implements MinecraftPacket {
private int dimension;
private long partialHashedSeed;
private short difficulty;
- private short gamemode;
+ private int gamemode;
private String levelType = "";
private byte dataToKeep; // 1.16+
private DimensionInfo dimensionInfo; // 1.16-1.16.1
- private short previousGamemode; // 1.16+
+ private int previousGamemode; // 1.16+
private CompoundBinaryTag currentDimensionData; // 1.16.2+
private @Nullable Pair lastDeathPosition; // 1.19+
private int portalCooldown; // 1.20+
@@ -46,9 +46,9 @@ public class RespawnPacket implements MinecraftPacket {
public RespawnPacket() {
}
- public RespawnPacket(int dimension, long partialHashedSeed, short difficulty, short gamemode,
+ public RespawnPacket(int dimension, long partialHashedSeed, short difficulty, int gamemode,
String levelType, byte dataToKeep, DimensionInfo dimensionInfo,
- short previousGamemode, CompoundBinaryTag currentDimensionData,
+ int previousGamemode, CompoundBinaryTag currentDimensionData,
@Nullable Pair lastDeathPosition, int portalCooldown,
int seaLevel) {
this.dimension = dimension;
@@ -97,11 +97,11 @@ public class RespawnPacket implements MinecraftPacket {
this.difficulty = difficulty;
}
- public short getGamemode() {
+ public int getGamemode() {
return gamemode;
}
- public void setGamemode(short gamemode) {
+ public void setGamemode(int gamemode) {
this.gamemode = gamemode;
}
@@ -121,11 +121,11 @@ public class RespawnPacket implements MinecraftPacket {
this.dataToKeep = dataToKeep;
}
- public short getPreviousGamemode() {
+ public int getPreviousGamemode() {
return previousGamemode;
}
- public void setPreviousGamemode(short previousGamemode) {
+ public void setPreviousGamemode(int previousGamemode) {
this.previousGamemode = previousGamemode;
}
@@ -197,9 +197,17 @@ public class RespawnPacket implements MinecraftPacket {
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15)) {
this.partialHashedSeed = buf.readLong();
}
- this.gamemode = buf.readByte();
+ if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) {
+ this.gamemode = ProtocolUtils.readVarInt(buf);
+ } else {
+ this.gamemode = buf.readByte();
+ }
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_16)) {
- this.previousGamemode = buf.readByte();
+ if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) {
+ this.previousGamemode = ProtocolUtils.readVarInt(buf);
+ } else {
+ this.previousGamemode = buf.readByte();
+ }
boolean isDebug = buf.readBoolean();
boolean isFlat = buf.readBoolean();
this.dimensionInfo = new DimensionInfo(dimensionKey, levelName, isFlat, isDebug, version);
@@ -249,9 +257,17 @@ public class RespawnPacket implements MinecraftPacket {
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15)) {
buf.writeLong(partialHashedSeed);
}
- buf.writeByte(gamemode);
+ if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) {
+ ProtocolUtils.writeVarInt(buf, this.gamemode);
+ } else {
+ buf.writeByte(this.gamemode);
+ }
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_16)) {
- buf.writeByte(previousGamemode);
+ if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) {
+ ProtocolUtils.writeVarInt(buf, this.previousGamemode);
+ } else {
+ buf.writeByte(this.previousGamemode);
+ }
buf.writeBoolean(dimensionInfo.isDebugType());
buf.writeBoolean(dimensionInfo.isFlat());
if (version.lessThan(ProtocolVersion.MINECRAFT_1_19_3)) {
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java
index 3c57cd75..6d735fa8 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java
@@ -25,6 +25,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_5;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_5;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_6;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_2;
+import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_3;
import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.id;
import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.mapSet;
import static com.velocitypowered.proxy.protocol.packet.brigadier.DoubleArgumentPropertySerializer.DOUBLE;
@@ -273,16 +274,21 @@ public class ArgumentPropertyRegistry {
empty(id("minecraft:heightmap", mapSet(MINECRAFT_1_21_6, 51), mapSet(MINECRAFT_1_21_5, 50), mapSet(MINECRAFT_1_20_3, 49),
mapSet(MINECRAFT_1_19_4, 47))); // 1.19.4
- empty(id("minecraft:uuid", mapSet(MINECRAFT_1_21_6, 56), mapSet(MINECRAFT_1_21_5, 54),mapSet(MINECRAFT_1_20_5, 53), mapSet(MINECRAFT_1_20_3, 48),
- mapSet(MINECRAFT_1_19_4, 48), mapSet(MINECRAFT_1_19, 47))); // added in 1.16
+ empty(id("minecraft:uuid", mapSet(MINECRAFT_26_3, 61), mapSet(MINECRAFT_1_21_6, 56), mapSet(MINECRAFT_1_21_5, 54), mapSet(MINECRAFT_1_20_5, 53),
+ mapSet(MINECRAFT_1_20_3, 48), mapSet(MINECRAFT_1_19_4, 48), mapSet(MINECRAFT_1_19, 47))); // added in 1.16
empty(id("minecraft:loot_table", mapSet(MINECRAFT_1_21_6, 52), mapSet(MINECRAFT_1_21_5, 51), mapSet(MINECRAFT_1_20_5, 50)));
empty(id("minecraft:loot_predicate", mapSet(MINECRAFT_1_21_6, 53), mapSet(MINECRAFT_1_21_5, 52), mapSet(MINECRAFT_1_20_5, 51)));
empty(id("minecraft:loot_modifier", mapSet(MINECRAFT_1_21_6, 54), mapSet(MINECRAFT_1_21_5, 53), mapSet(MINECRAFT_1_20_5, 52)));
empty(id("minecraft:hex_color", mapSet(MINECRAFT_1_21_6, 17))); // added in 1.21.6
- empty(id("minecraft:dialog", mapSet(MINECRAFT_1_21_6, 55))); // added in 1.21.6
+ empty(id("minecraft:dialog", mapSet(MINECRAFT_26_3, 58), mapSet(MINECRAFT_1_21_6, 55))); // added in 1.21.6
empty(id("minecraft:team_color", mapSet(MINECRAFT_26_2, 16))); // renamed from color in 26.2
+ empty(id("minecraft:context_float_provider", mapSet(MINECRAFT_26_3, 55))); // added in 26.3
+ empty(id("minecraft:context_int_provider", mapSet(MINECRAFT_26_3, 56))); // added in 26.3
+ empty(id("minecraft:slot_source", mapSet(MINECRAFT_26_3, 57))); // added in 26.3
+ empty(id("minecraft:feature", mapSet(MINECRAFT_26_3, 59))); // added in 26.3
+ empty(id("minecraft:swing_animation", mapSet(MINECRAFT_26_3, 60))); // added in 26.3
// Crossstitch support
register(id("crossstitch:mod_argument", mapSet(MINECRAFT_1_19, -256)), ModArgumentProperty.class, MOD);
From c10b49255447694ad42c3c189d7a3f4ee3e73e5e Mon Sep 17 00:00:00 2001
From: Wouter Gritter
Date: Mon, 14 Sep 2026 18:36:05 +0200
Subject: [PATCH 13/18] Release 4.2.0
---
gradle.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index ed875a01..3313abc0 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,2 +1,2 @@
group=com.velocitypowered
-version=4.1.2-SNAPSHOT
+version=4.2.0
From 5000227d11583d121409d01a420ed5d3fe855ebd Mon Sep 17 00:00:00 2001
From: Wouter Gritter
Date: Wed, 16 Sep 2026 13:41:50 +0200
Subject: [PATCH 14/18] Snapshot 4.2.1-SNAPSHOT
---
gradle.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index 3313abc0..b6abe9af 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,2 +1,2 @@
group=com.velocitypowered
-version=4.2.0
+version=4.2.1-SNAPSHOT
From a6f9de9581e0cae5d826c8f71ada64cb2a4ce812 Mon Sep 17 00:00:00 2001
From: Wouter Gritter
Date: Wed, 16 Sep 2026 13:41:57 +0200
Subject: [PATCH 15/18] Bound IntervalledCounter size under packet floods
The counter stored one entry per addTime call, so a client sending tiny
(even empty) packets could grow it without limit while staying under the
bytes-per-second limit, eventually OOMing the proxy. Merge data points
within 1ms of the newest one so the entry count is bounded by the window
size (~7000 for the default 7s window) rather than by packet rate.
---
.../proxy/util/IntervalledCounter.java | 40 ++++--
.../proxy/util/IntervalledCounterTest.java | 115 ++++++++++++++++++
2 files changed, 146 insertions(+), 9 deletions(-)
create mode 100644 proxy/src/test/java/com/velocitypowered/proxy/util/IntervalledCounterTest.java
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/IntervalledCounter.java b/proxy/src/main/java/com/velocitypowered/proxy/util/IntervalledCounter.java
index 9b986dca..7e0bf4c8 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/util/IntervalledCounter.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/util/IntervalledCounter.java
@@ -30,40 +30,51 @@ package com.velocitypowered.proxy.util;
* This class is not thread-safe. If multiple threads access an instance concurrently,
* external synchronization is required.
*/
-@SuppressWarnings("checkstyle:WhitespaceAfter") // Not our class
public final class IntervalledCounter {
private static final int INITIAL_SIZE = 8;
+ /**
+ * Data points within this many nanoseconds of the newest one are merged into it, bounding the
+ * number of stored data points to roughly {@code interval / COALESCE_INTERVAL}.
+ */
+ private static final long COALESCE_INTERVAL = 1_000_000L; // 1ms
+
/**
* Ring buffer holding the timestamp (in nanoseconds) for each data point.
*/
- protected long[] times;
+ private long[] times;
+
/**
* Ring buffer holding the count associated with each timestamp.
*/
- protected long[] counts;
+ private long[] counts;
+
/**
* The sliding window size in nanoseconds. Only entries with time >= (currentTime - interval)
* are considered part of the window.
*/
- protected final long interval;
+ private final long interval;
+
/**
* Cached lower bound of the window (in nanoseconds) after the last update.
*/
- protected long minTime;
+ private long minTime;
+
/**
* Running sum of all counts currently within the window.
*/
- protected long sum;
+ private long sum;
+
/**
* Head index (inclusive) of the ring buffer.
*/
- protected int head; // inclusive
+ private int head; // inclusive
+
/**
* Tail index (exclusive) of the ring buffer.
*/
- protected int tail; // exclusive
+ private int tail; // exclusive
/**
* Creates a new counter with the specified interval.
@@ -131,6 +142,8 @@ public final class IntervalledCounter {
/**
* Adds {@code count} units at the specified timestamp, assuming the timestamp is within the
* current window. If the timestamp is older than {@code minTime}, the value is ignored.
+ * If the timestamp is within {@link #COALESCE_INTERVAL} of the newest stored data point, the
+ * count is merged into that data point instead of creating a new one.
* This method does not automatically advance the window; callers should invoke
* {@link #updateCurrentTime()} or {@link #updateCurrentTime(long)} beforehand.
*
@@ -142,6 +155,15 @@ public final class IntervalledCounter {
if (currTime - this.minTime < 0) {
return;
}
+ if (this.head != this.tail) {
+ final int last = this.tail == 0 ? this.times.length - 1 : this.tail - 1;
+ // guard against overflow by using subtraction
+ if (currTime - this.times[last] < COALESCE_INTERVAL) {
+ this.counts[last] += count;
+ this.sum += count;
+ return;
+ }
+ }
int nextTail = (this.tail + 1) % this.times.length;
if (nextTail == this.head) {
this.resize();
@@ -219,7 +241,7 @@ public final class IntervalledCounter {
* @return the rate in units per second for the current window
*/
public double getRate() {
- return (double)this.sum / ((double)this.interval * 1.0E-9);
+ return (double) this.sum / ((double) this.interval * 1.0E-9);
}
/**
diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/IntervalledCounterTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/IntervalledCounterTest.java
new file mode 100644
index 00000000..f187ff2c
--- /dev/null
+++ b/proxy/src/test/java/com/velocitypowered/proxy/util/IntervalledCounterTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2026 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.Test;
+
+class IntervalledCounterTest {
+
+ private static final long INTERVAL = TimeUnit.SECONDS.toNanos(7);
+
+ @Test
+ void sumAndRateTrackAddedCounts() {
+ IntervalledCounter counter = new IntervalledCounter(INTERVAL);
+ long now = 0;
+
+ counter.updateAndAdd(700, now);
+ assertEquals(700, counter.getSum());
+ assertEquals(100.0, counter.getRate(), 1e-9);
+
+ now += TimeUnit.SECONDS.toNanos(1);
+ counter.updateAndAdd(1400, now);
+ assertEquals(2100, counter.getSum());
+ assertEquals(300.0, counter.getRate(), 1e-9);
+ }
+
+ @Test
+ void dataPointsOutsideTheWindowAreEvicted() {
+ IntervalledCounter counter = new IntervalledCounter(INTERVAL);
+ long now = 0;
+
+ counter.updateAndAdd(10, now);
+ now += TimeUnit.SECONDS.toNanos(3);
+ counter.updateAndAdd(20, now);
+ assertEquals(30, counter.getSum());
+ assertEquals(2, counter.totalDataPoints());
+
+ // 7.5s after the first point: only the second point remains
+ now = TimeUnit.MILLISECONDS.toNanos(7500);
+ counter.updateCurrentTime(now);
+ assertEquals(20, counter.getSum());
+ assertEquals(1, counter.totalDataPoints());
+
+ // 10.5s: everything has expired
+ now = TimeUnit.MILLISECONDS.toNanos(10500);
+ counter.updateCurrentTime(now);
+ assertEquals(0, counter.getSum());
+ assertEquals(0, counter.totalDataPoints());
+ }
+
+ @Test
+ void storedDataPointsAreBoundedByTimeNotByCallCount() {
+ IntervalledCounter counter = new IntervalledCounter(INTERVAL);
+
+ // Simulate a flood of tiny (even zero-sized) packets arriving far faster than one per
+ // millisecond for the whole window. Before coalescing was introduced, each call stored a
+ // separate data point, so an attacker could grow the ring buffer without bound while staying
+ // under a bytes-per-second limit.
+ final long stepNanos = 100;
+ final long calls = INTERVAL / stepNanos;
+ long now = 0;
+ for (long i = 0; i < calls; i++) {
+ counter.updateAndAdd(i % 2, now);
+ now += stepNanos;
+ }
+
+ long expectedSum = calls / 2;
+ assertEquals(expectedSum, counter.getSum());
+ // 7s window at 1ms coalescing granularity is ~7000 points; leave some slack.
+ assertTrue(counter.totalDataPoints() <= 7100,
+ "expected at most ~7000 data points, got " + counter.totalDataPoints());
+ assertTrue(counter.totalDataPoints() >= 7000,
+ "expected at least 7000 data points, got " + counter.totalDataPoints());
+ }
+
+ @Test
+ void coalescedDataPointsExpireTogether() {
+ IntervalledCounter counter = new IntervalledCounter(INTERVAL);
+
+ long now = 0;
+ counter.updateAndAdd(5, now);
+ // Within the same 1ms bucket: merged into the previous point
+ counter.updateAndAdd(7, now + 500_000L);
+ assertEquals(12, counter.getSum());
+ assertEquals(1, counter.totalDataPoints());
+
+ // A new bucket starts a new point
+ counter.updateAndAdd(1, now + 1_000_000L);
+ assertEquals(13, counter.getSum());
+ assertEquals(2, counter.totalDataPoints());
+
+ // The merged bucket carries the timestamp of its first point and expires with it
+ counter.updateCurrentTime(now + INTERVAL + 1);
+ assertEquals(1, counter.getSum());
+ assertEquals(1, counter.totalDataPoints());
+ }
+}
From f975230f14f102d9adef65a858a82cc800751dca Mon Sep 17 00:00:00 2001
From: MiniDigger | Martin
Date: Sun, 20 Sep 2026 10:23:18 +0200
Subject: [PATCH 16/18] =?UTF-8?q?[ci=20skip]=20migrate=20CI=20to=20namespa?=
=?UTF-8?q?ce=20=F0=9F=9A=80=20(#1888)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/gradle.yml | 20 +++++++++++++++-----
README.md | 2 ++
2 files changed, 17 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index 865c9390..f3d18540 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -6,18 +6,28 @@ on: [push, pull_request]
jobs:
build:
- runs-on: ubuntu-24.04
+ runs-on: namespace-profile-linux-default
steps:
- name: Checkout Repository
- uses: actions/checkout@v6
+ uses: namespacelabs/nscloud-checkout-action@v9
with:
persist-credentials: false
+ - name: Set up cache
+ uses: namespacelabs/nscloud-cache-action@v1
+ with:
+ cache: gradle
+ - name: Setup gradle build cache
+ run: |
+ nsc cache gradle setup --init-gradle /tmp/init.gradle
- name: Set up Gradle
- uses: gradle/actions/setup-gradle@v5
+ uses: gradle/actions/setup-gradle@v6
+ with:
+ cache-disabled: true # namespace handles this
- name: Set up JDK 21
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with:
java-version: 21
distribution: 'zulu'
+ cache: "" # namespace handles this
- name: Build with Gradle
- run: ./gradlew build
+ run: ./gradlew --init-script=/tmp/init.gradle build
diff --git a/README.md b/README.md
index aa6527f0..fccffeb7 100644
--- a/README.md
+++ b/README.md
@@ -40,3 +40,5 @@ page.
Translations are handled using [Crowdin](https://papermc-io.crowdin.com/velocity).
If you want to translate a language not available on Crowdin,
you might want to ask in the [Discord](https://discord.gg/papermc) about it.
+
+
From 5bb1992014e951b8ce757cf27d135eff25a26eb6 Mon Sep 17 00:00:00 2001
From: MiniDigger | Martin
Date: Tue, 22 Sep 2026 16:09:28 +0200
Subject: [PATCH 17/18] [ci skip] update namespace badge
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index fccffeb7..7441044f 100644
--- a/README.md
+++ b/README.md
@@ -41,4 +41,4 @@ Translations are handled using [Crowdin](https://papermc-io.crowdin.com/velocity
If you want to translate a language not available on Crowdin,
you might want to ask in the [Discord](https://discord.gg/papermc) about it.
-
+[](https://namespace.so/github-actions/?utm_source=oss&utm_campaign=papermc)
From fb0f21f425a36a2ebe4a7c35d233b0e6771b81f6 Mon Sep 17 00:00:00 2001
From: Rocco
Date: Wed, 23 Sep 2026 13:09:07 +0000
Subject: [PATCH 18/18] Update online mode in reconfigure join packet (#1889)
---
.../connection/backend/BackendPlaySessionHandler.java | 8 ++++++++
1 file changed, 8 insertions(+)
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 5ba2659f..37a61d35 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
@@ -55,6 +55,7 @@ import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
+import com.velocitypowered.proxy.protocol.packet.JoinGamePacket;
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket;
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
@@ -164,6 +165,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return false; // forwards on
}
+ @Override
+ public boolean handle(JoinGamePacket packet) {
+ // We receive this if the connection is reconfigured
+ packet.setOnlineMode(serverConn.getPlayer().isOnlineMode());
+ return false; // forward
+ }
+
@Override
public boolean handle(ClientSettingsPacket packet) {
serverConn.ensureConnected().write(packet);