Merge remote-tracking branch 'upstream/dev/4.0.0' into update
SteamWarCI Build failed

# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
This commit is contained in:
yoyosource
2026-09-24 17:05:06 +02:00
47 changed files with 791 additions and 271 deletions
+15 -5
View File
@@ -6,18 +6,28 @@ on: [push, pull_request]
jobs: jobs:
build: build:
runs-on: ubuntu-24.04 runs-on: namespace-profile-linux-default
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v6 uses: namespacelabs/nscloud-checkout-action@v9
with: with:
persist-credentials: false 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 - 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 - name: Set up JDK 21
uses: actions/setup-java@v5 uses: actions/setup-java@v6
with: with:
java-version: 21 java-version: 21
distribution: 'zulu' distribution: 'zulu'
cache: "" # namespace handles this
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew build run: ./gradlew --init-script=/tmp/init.gradle build
+2
View File
@@ -40,3 +40,5 @@ page.
Translations are handled using [Crowdin](https://papermc-io.crowdin.com/velocity). Translations are handled using [Crowdin](https://papermc-io.crowdin.com/velocity).
If you want to translate a language not available on Crowdin, 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. you might want to ask in the [Discord](https://discord.gg/papermc) about it.
[![CI powered by namespace badge](https://papermc.io/assets/misc/namespace-oss-badge.svg?project=velocity)](https://namespace.so/github-actions/?utm_source=oss&utm_campaign=papermc)
@@ -73,6 +73,7 @@ public final class PreLoginEvent implements ResultedEvent<PreLoginEvent.PreLogin
/** /**
* Returns the UUID of the connecting player. * Returns the UUID of the connecting player.
*
* <p>This value is {@code null} on 1.19.2 and lower, * <p>This 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.</p> * up to 1.20.1 it is optional and from 1.20.2 it will always be available.</p>
* *
@@ -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. * This event is executed when a player entered the configuration state and can be configured by Velocity.
*
* <p>Velocity will wait for this event before continuing/ending the configuration state.</p> * <p>Velocity will wait for this event before continuing/ending the configuration state.</p>
* *
* @param player The player who can be configured. * @param player The player who can be configured.
@@ -15,6 +15,7 @@ import org.jetbrains.annotations.NotNull;
/** /**
* This event is executed when a player is about to enter the configuration state. * This event is executed when a player is about to enter the configuration state.
* It is <b>not</b> called for the initial configuration of a player after login. * It is <b>not</b> called for the initial configuration of a player after login.
*
* <p>Velocity will wait for this event before asking the client to enter configuration state. * <p>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, * 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.</p> * Velocity will only wait for a maximum of 5 seconds.</p>
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
/** /**
* This event is executed when a player has entered the configuration state. * This event is executed when a player has entered the configuration state.
*
* <p>From this moment on, until the {@link PlayerFinishedConfigurationEvent} is executed, * <p>From this moment on, until the {@link PlayerFinishedConfigurationEvent} is executed,
* the {@linkplain Player#getProtocolState()} method is guaranteed * the {@linkplain Player#getProtocolState()} method is guaranteed
* to return {@link ProtocolState#CONFIGURATION}.</p> * to return {@link ProtocolState#CONFIGURATION}.</p>
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
/** /**
* This event is executed when a player is about to finish the configuration state. * This event is executed when a player is about to finish the configuration state.
*
* <p>Velocity will wait for this event before asking the client to finish the configuration state. * <p>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, * 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 * Velocity will only wait for a maximum of 5 seconds. If you need to hold a player in configuration
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
/** /**
* This event is executed when a player has finished the configuration state. * This event is executed when a player has finished the configuration state.
*
* <p>From this moment on, the {@link Player#getProtocolState()} method * <p>From this moment on, the {@link Player#getProtocolState()} method
* will return {@link ProtocolState#PLAY}.</p> * will return {@link ProtocolState#PLAY}.</p>
* *
@@ -16,6 +16,7 @@ package com.velocitypowered.api.network;
public enum ProtocolState { public enum ProtocolState {
/** /**
* Initial connection State. * Initial connection State.
*
* <p>This status can be caused by a {@link HandshakeIntent#STATUS}, * <p>This status can be caused by a {@link HandshakeIntent#STATUS},
* {@link HandshakeIntent#LOGIN} or {@link HandshakeIntent#TRANSFER} intent.</p> * {@link HandshakeIntent#LOGIN} or {@link HandshakeIntent#TRANSFER} intent.</p>
* If the intent is LOGIN or TRANSFER, the next state will be {@link #LOGIN}, * If the intent is LOGIN or TRANSFER, the next state will be {@link #LOGIN},
@@ -24,6 +25,7 @@ public enum ProtocolState {
HANDSHAKE, HANDSHAKE,
/** /**
* Ping State of a connection. * Ping State of a connection.
*
* <p>Connections with the {@link HandshakeIntent#STATUS} intent will pass through this state * <p>Connections with the {@link HandshakeIntent#STATUS} intent will pass through this state
* and be disconnected after it requests the ping from the server * and be disconnected after it requests the ping from the server
* and the server responds with the respective ping.</p> * and the server responds with the respective ping.</p>
@@ -31,11 +33,13 @@ public enum ProtocolState {
STATUS, STATUS,
/** /**
* Authentication State of a connection. * Authentication State of a connection.
*
* <p>At this moment the player is authenticating with the authentication servers.</p> * <p>At this moment the player is authenticating with the authentication servers.</p>
*/ */
LOGIN, LOGIN,
/** /**
* Configuration State of a connection. * Configuration State of a connection.
*
* <p>At this point the player allows the server to send information * <p>At this point the player allows the server to send information
* such as resource packs and plugin messages, at the same time the player * such as resource packs and plugin messages, at the same time the player
* will send his client brand and the respective plugin messages * will send his client brand and the respective plugin messages
@@ -46,6 +50,7 @@ public enum ProtocolState {
CONFIGURATION, CONFIGURATION,
/** /**
* Game State of a connection. * Game State of a connection.
*
* <p>In this state is where the whole game runs, the server is able to change * <p>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.</p> * the player's state to {@link #CONFIGURATION} as needed in versions 1.20.2 and higher.</p>
*/ */
@@ -96,7 +96,8 @@ public enum ProtocolVersion implements Ordered<ProtocolVersion> {
MINECRAFT_1_21_9(773, "1.21.9", "1.21.10"), MINECRAFT_1_21_9(773, "1.21.9", "1.21.10"),
MINECRAFT_1_21_11(774, "1.21.11"), MINECRAFT_1_21_11(774, "1.21.11"),
MINECRAFT_26_1(775, "26.1", "26.1.1", "26.1.2"), 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; private static final int SNAPSHOT_BIT = 30;
@@ -15,7 +15,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* Represents three different states of a setting. * Represents three different states of a setting.
* *
* <p>Possible values:</p> * <p>Possible values:</p>
* <p></p>
* <ul> * <ul>
* <li>{@link #TRUE} - a positive setting</li> * <li>{@link #TRUE} - a positive setting</li>
* <li>{@link #FALSE} - a negative (negated) setting</li> * <li>{@link #FALSE} - a negative (negated) setting</li>
@@ -315,6 +315,7 @@ public interface Player extends
/** /**
* {@inheritDoc} * {@inheritDoc}
*
* <p><strong>Note that this method does not send a plugin message to the server the player * <p><strong>Note that this method does not send a plugin message to the server the player
* is connected to.</strong> You should only use this method if you are trying to communicate * is connected to.</strong> You should only use this method if you are trying to communicate
* with a mod that is installed on the player's client.</p> * with a mod that is installed on the player's client.</p>
@@ -56,6 +56,7 @@ public interface KeySigned {
* Validates the signature, expiry temporal and key against the * Validates the signature, expiry temporal and key against the
* signer public key. Note: This will **not** check for * signer public key. Note: This will **not** check for
* expiry. You can check for expiry with {@link KeySigned#hasExpired()}. * expiry. You can check for expiry with {@link KeySigned#hasExpired()}.
*
* <p>DOES NOT WORK YET FOR MESSAGES AND COMMANDS!</p> * <p>DOES NOT WORK YET FOR MESSAGES AND COMMANDS!</p>
* Addendum: Does not work for 1.19.1 until the user has authenticated. * Addendum: Does not work for 1.19.1 until the user has authenticated.
* *
@@ -237,8 +237,11 @@ public interface TabListEntry extends KeyIdentifiable {
/** /**
* Sets the {@link IdentifiedKey} of the {@link TabListEntry}. * Sets the {@link IdentifiedKey} of the {@link TabListEntry}.
*
* <p>This only works for players currently <b>not</b> connected to this proxy.</p> * <p>This only works for players currently <b>not</b> connected to this proxy.</p>
*
* <p>For any player currently connected to this proxy this will be filled automatically.</p> * <p>For any player currently connected to this proxy this will be filled automatically.</p>
*
* <p>Will ignore mismatching key revisions data.</p> * <p>Will ignore mismatching key revisions data.</p>
* *
* @param chatSession session to set * @param chatSession session to set
+1 -1
View File
@@ -1,2 +1,2 @@
group=com.velocitypowered group=com.velocitypowered
version=4.1.0-SNAPSHOT version=4.2.1-SNAPSHOT
+10 -10
View File
@@ -3,12 +3,12 @@ configurate3 = "3.7.3"
configurate4 = "4.2.0" configurate4 = "4.2.0"
flare = "2.0.1" flare = "2.0.1"
log4j = "2.26.1" log4j = "2.26.1"
netty = "4.2.16.Final" netty = "4.2.18.Final"
[plugins] [plugins]
fill = "io.papermc.fill.gradle:1.0.12" fill = "io.papermc.fill.gradle:1.0.12"
shadow = "com.gradleup.shadow:9.6.1" shadow = "com.gradleup.shadow:9.6.1"
spotless = "com.diffplug.spotless:8.9.0" spotless = "com.diffplug.spotless:8.10.2"
[libraries] [libraries]
adventure-bom = "net.kyori:adventure-bom:5.2.0" adventure-bom = "net.kyori:adventure-bom:5.2.0"
@@ -19,8 +19,8 @@ auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.1
brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT" brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
bstats = "org.bstats:bstats-base:3.2.1" bstats = "org.bstats:bstats-base:3.2.1"
caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.4" 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" checkstyle = "com.puppycrawl.tools:checkstyle:14.1.0"
completablefutures = "com.spotify:completable-futures:0.3.6" completablefutures = "com.spotify:completable-futures:0.3.6"
configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" } configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" }
configurate3-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate3" } configurate3-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate3" }
@@ -32,12 +32,12 @@ disruptor = "com.lmax:disruptor:4.0.0"
fastutil = "it.unimi.dsi:fastutil:8.5.19" fastutil = "it.unimi.dsi:fastutil:8.5.19"
flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" } flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" }
flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", 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" 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" jspecify = "org.jspecify:jspecify:1.0.1"
kyori-ansi = "net.kyori:ansi:1.1.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" gson = "com.google.code.gson:gson:2.14.0"
guice = "com.google.inject:guice:7.0.0" guice = "com.google.inject:guice:7.0.0"
lmbda = "org.lanternpowered:lmbda:3.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-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" } netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" }
nightconfig = "com.electronwill.night-config:toml:3.9.0" nightconfig = "com.electronwill.night-config:toml:3.9.0"
slf4j = "org.slf4j:slf4j-api:2.0.18" slf4j = "org.slf4j:slf4j-api:2.0.19"
snakeyaml = "org.yaml:snakeyaml:2.6" snakeyaml = "org.yaml:snakeyaml:2.7"
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.10.3" spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.10.4"
terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0" terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0"
[bundles] [bundles]
@@ -488,23 +488,46 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
return false; return false;
} }
// Re-register servers. If a server is being replaced, make sure to note what players need to // Re-register servers. If a server is being replaced or removed, make sure to note what
// move back to a fallback server. // players need to move back to a fallback server.
Collection<ConnectedPlayer> evacuate = new ArrayList<>(); Collection<ConnectedPlayer> evacuate = new ArrayList<>();
for (Map.Entry<String, String> entry : newConfiguration.getServers().entrySet()) { for (Map.Entry<String, String> entry : configuration.getServers().entrySet()) {
ServerInfo newInfo = new ServerInfo(entry.getKey(), AddressUtil.parseAddress(entry.getValue())); if (newConfiguration.getServers().keySet().stream()
Optional<RegisteredServer> rs = servers.getServer(entry.getKey()); .anyMatch(name -> name.equalsIgnoreCase(entry.getKey()))) {
if (rs.isEmpty()) { continue;
servers.register(newInfo); }
} else if (!rs.get().getServerInfo().equals(newInfo)) {
for (Player player : rs.get().getPlayersConnected()) { RegisteredServer rs = servers.getServer(entry.getKey()).orElse(null);
if (rs == null) {
continue;
}
for (Player player : rs.getPlayersConnected()) {
if (!(player instanceof ConnectedPlayer)) { if (!(player instanceof ConnectedPlayer)) {
throw new IllegalStateException("ConnectedPlayer not found for player " + player throw new IllegalStateException("Expected ConnectedPlayer for player " + player
+ " in server " + rs.get().getServerInfo().getName()); + " on server " + rs.getServerInfo().getName() + ", got "
+ player.getClass().getName());
} }
evacuate.add((ConnectedPlayer) player); evacuate.add((ConnectedPlayer) player);
} }
servers.unregister(rs.get().getServerInfo()); servers.unregister(rs.getServerInfo());
}
for (Map.Entry<String, String> entry : newConfiguration.getServers().entrySet()) {
ServerInfo newInfo = new ServerInfo(entry.getKey(), AddressUtil.parseAddress(entry.getValue()));
RegisteredServer rs = servers.getServer(entry.getKey()).orElse(null);
if (rs == null) {
servers.register(newInfo);
} else if (!rs.getServerInfo().equals(newInfo)) {
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());
servers.register(newInfo); servers.register(newInfo);
} }
} }
@@ -653,7 +676,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 * @param explicitExit whether the user explicitly shut down the proxy
*/ */
@@ -16,13 +16,49 @@
*/ */
package com.velocitypowered.proxy.config; 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 { public record PingPassthroughMode(boolean version, boolean players,
DISABLED, boolean description, boolean favicon, boolean modinfo) {
MODS, public static final PingPassthroughMode DEFAULT = new PingPassthroughMode();
DESCRIPTION,
ALL /**
* 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;
}
} }
@@ -32,6 +32,7 @@ import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration;
import com.velocitypowered.proxy.config.migration.MiniMessageTranslationsMigration; import com.velocitypowered.proxy.config.migration.MiniMessageTranslationsMigration;
import com.velocitypowered.proxy.config.migration.MotdMigration; import com.velocitypowered.proxy.config.migration.MotdMigration;
import com.velocitypowered.proxy.config.migration.PacketLimiterMigration; 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.config.migration.TransferIntegrationMigration;
import com.velocitypowered.proxy.util.AddressUtil; import com.velocitypowered.proxy.util.AddressUtil;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -79,7 +80,7 @@ public class VelocityConfiguration implements ProxyConfig {
@Expose @Expose
private boolean onlineModeKickExistingPlayers = false; private boolean onlineModeKickExistingPlayers = false;
@Expose @Expose
private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED; private PingPassthroughMode pingPassthrough = PingPassthroughMode.DEFAULT;
@Expose @Expose
private boolean samplePlayersInPing = false; private boolean samplePlayersInPing = false;
private final Servers servers; private final Servers servers;
@@ -513,7 +514,8 @@ public class VelocityConfiguration implements ProxyConfig {
new MotdMigration(), new MotdMigration(),
new MiniMessageTranslationsMigration(), new MiniMessageTranslationsMigration(),
new TransferIntegrationMigration(), new TransferIntegrationMigration(),
new PacketLimiterMigration() new PacketLimiterMigration(),
new PingPassthroughMigration(),
}; };
for (final ConfigurationMigration migration : migrations) { for (final ConfigurationMigration migration : migrations) {
@@ -555,9 +557,7 @@ public class VelocityConfiguration implements ProxyConfig {
final CommentedConfig metricsConfig = config.get("metrics"); final CommentedConfig metricsConfig = config.get("metrics");
final PlayerInfoForwarding forwardingMode = config.getEnumOrElse( final PlayerInfoForwarding forwardingMode = config.getEnumOrElse(
"player-info-forwarding-mode", PlayerInfoForwarding.NONE); "player-info-forwarding-mode", PlayerInfoForwarding.NONE);
final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough", final PingPassthroughMode pingPassthrough = PingPassthroughMode.fromConfig(config.get("ping-passthrough"));
PingPassthroughMode.DISABLED);
final boolean samplePlayersInPing = config.getOrElse("sample-players-in-ping", false); final boolean samplePlayersInPing = config.getOrElse("sample-players-in-ping", false);
final String bind = config.getOrElse("bind", "0.0.0.0:25565"); final String bind = config.getOrElse("bind", "0.0.0.0:25565");
@@ -590,7 +590,7 @@ public class VelocityConfiguration implements ProxyConfig {
forwardingMode, forwardingMode,
forwardingSecret, forwardingSecret,
kickExisting, kickExisting,
pingPassthroughMode, pingPassthrough,
samplePlayersInPing, samplePlayersInPing,
enablePlayerAddressLogging, enablePlayerAddressLogging,
new Servers(serversConfig), new Servers(serversConfig),
@@ -30,7 +30,8 @@ public sealed interface ConfigurationMigration
MotdMigration, MotdMigration,
MiniMessageTranslationsMigration, MiniMessageTranslationsMigration,
TransferIntegrationMigration, TransferIntegrationMigration,
PacketLimiterMigration { PacketLimiterMigration,
PingPassthroughMigration {
boolean shouldMigrate(CommentedFileConfig config); boolean shouldMigrate(CommentedFileConfig config);
void migrate(CommentedFileConfig config, Logger logger) throws IOException; void migrate(CommentedFileConfig config, Logger logger) throws IOException;
@@ -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 <https://www.gnu.org/licenses/>.
*/
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");
}
}
@@ -23,6 +23,7 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; 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.ClientboundSoundEntityPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
@@ -404,4 +405,8 @@ public interface MinecraftSessionHandler {
default boolean handle(ClientboundStopSoundPacket packet) { default boolean handle(ClientboundStopSoundPacket packet) {
return false; return false;
} }
default boolean handle(ClientboundPostEffectsPacket packet) {
return false;
}
} }
@@ -57,6 +57,7 @@ import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; 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.KeepAlivePacket;
import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket;
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
@@ -166,6 +167,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return false; // forwards on 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 @Override
public boolean handle(ClientSettingsPacket packet) { public boolean handle(ClientSettingsPacket packet) {
serverConn.ensureConnected().write(packet); serverConn.ensureConnected().write(packet);
@@ -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.minimessage.translation.Argument;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; 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.Title.Times;
import net.kyori.adventure.title.TitlePart; import net.kyori.adventure.title.TitlePart;
import net.kyori.adventure.translation.GlobalTranslator; import net.kyori.adventure.translation.GlobalTranslator;
@@ -486,11 +487,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
} }
@Override @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)) { if (this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
GenericTitlePacket timesPkt = GenericTitlePacket.constructTitlePacket( GenericTitlePacket timesPkt = GenericTitlePacket.constructTitlePacket(
GenericTitlePacket.ActionType.SET_TIMES, this.getProtocolVersion()); GenericTitlePacket.ActionType.SET_TIMES, this.getProtocolVersion());
net.kyori.adventure.title.Title.Times times = title.times(); Title.Times times = title.times();
if (times != null) { if (times != null) {
timesPkt.setFadeIn((int) DurationUtils.toTicks(times.fadeIn())); timesPkt.setFadeIn((int) DurationUtils.toTicks(times.fadeIn()));
timesPkt.setStay((int) DurationUtils.toTicks(times.stay())); timesPkt.setStay((int) DurationUtils.toTicks(times.stay()));
@@ -37,7 +37,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
/** /**
* Modern (Minecraft 1.20.3+) ResourcePackHandler * Modern (Minecraft 1.20.3+) ResourcePackHandler.
*/ */
public final class ModernResourcePackHandler extends ResourcePackHandler { public final class ModernResourcePackHandler extends ResourcePackHandler {
private final ListMultimap<UUID, ResourcePackInfo> outstandingResourcePacks = private final ListMultimap<UUID, ResourcePackInfo> outstandingResourcePacks =
@@ -118,6 +118,7 @@ public abstract sealed class ResourcePackHandler
/** /**
* Processes a client response to a sent resource-pack. * Processes a client response to a sent resource-pack.
*
* <p>Cases in which no action will be taken:</p> * <p>Cases in which no action will be taken:</p>
* <ul> * <ul>
* *
@@ -23,6 +23,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.server.PingOptions; import com.velocitypowered.api.proxy.server.PingOptions;
import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerPing; import com.velocitypowered.api.proxy.server.ServerPing;
import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.api.util.ModInfo; import com.velocitypowered.api.util.ModInfo;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.PingPassthroughMode; import com.velocitypowered.proxy.config.PingPassthroughMode;
@@ -100,60 +101,68 @@ public class ServerListPingHandler {
CompletableFuture<List<ServerPing>> pingResponses = CompletableFutures.successfulAsList(pings, CompletableFuture<List<ServerPing>> pingResponses = CompletableFutures.successfulAsList(pings,
(ex) -> fallback); (ex) -> fallback);
return switch (mode) {
case ALL -> pingResponses.thenApply(responses -> { // Return early if ping passthrough is not enabled
if (!mode.enabled()) {
return CompletableFuture.completedFuture(fallback);
}
return pingResponses.thenApply(responses -> {
// Find the first non-fallback // Find the first non-fallback
for (ServerPing response : responses) { for (ServerPing response : responses) {
if (response == fallback) { if (response == fallback) {
continue; continue;
} }
if (response.getDescriptionComponent() == null) { ServerPing.Version version;
return response.asBuilder() if (mode.version()) {
.description(Component.empty()) version = response.getVersion();
.build(); } else {
version = fallback.getVersion();
} }
return response; ServerPing.Players players;
if (mode.players()) {
players = response.getPlayers().orElse(null);
} else {
players = fallback.getPlayers().orElse(null);
} }
return fallback;
}); Component description;
case MODS -> pingResponses.thenApply(responses -> { if (mode.description()) {
// Find the first non-fallback that contains a mod list if (response.getDescriptionComponent() != null) {
for (ServerPing response : responses) { description = response.getDescriptionComponent();
if (response == fallback) { } else {
continue; description = Component.empty();
} }
Optional<ModInfo> modInfo = response.getModinfo(); } else {
if (modInfo.isPresent()) { description = fallback.getDescriptionComponent();
return fallback.asBuilder().mods(modInfo.get()).build();
} }
Favicon favicon;
if (mode.favicon()) {
favicon = response.getFavicon().orElse(null);
} else {
favicon = fallback.getFavicon().orElse(null);
} }
return fallback;
}); ModInfo modinfo;
case DESCRIPTION -> pingResponses.thenApply(responses -> { if (mode.modinfo()) {
// Find the first non-fallback. If it includes a modlist, add it too. modinfo = response.getModinfo().orElse(null);
for (ServerPing response : responses) { } else {
if (response == fallback) { modinfo = fallback.getModinfo().orElse(null);
continue;
}
if (response.getDescriptionComponent() == null) {
continue;
} }
return new ServerPing( return new ServerPing(
fallback.getVersion(), version,
fallback.getPlayers().orElse(null), players,
response.getDescriptionComponent(), description,
fallback.getFavicon().orElse(null), favicon,
response.getModinfo().orElse(null) modinfo
); );
} }
return fallback; return fallback;
}); });
// Not possible, but covered for completeness.
default -> CompletableFuture.completedFuture(fallback);
};
} }
/** /**
@@ -168,7 +177,7 @@ public class ServerListPingHandler {
? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION; ? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION;
PingPassthroughMode passthroughMode = configuration.getPingPassthrough(); PingPassthroughMode passthroughMode = configuration.getPingPassthrough();
if (passthroughMode == PingPassthroughMode.DISABLED) { if (!passthroughMode.enabled()) {
return CompletableFuture.completedFuture(constructLocalPing(shownVersion)); return CompletableFuture.completedFuture(constructLocalPing(shownVersion));
} else { } else {
String virtualHostStr = connection.getVirtualHost().map(InetSocketAddress::getHostString) String virtualHostStr = connection.getVirtualHost().map(InetSocketAddress::getHostString)
@@ -545,6 +545,36 @@ public class VelocityEventManager implements EventManager {
} }
} }
private <E> void fire(final @Nullable CompletableFuture<E> 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<E> 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_DEFAULT = 0;
private static final int TASK_STATE_EXECUTING = 1; private static final int TASK_STATE_EXECUTING = 1;
private static final int TASK_STATE_CONTINUE_IMMEDIATELY = 2; private static final int TASK_STATE_CONTINUE_IMMEDIATELY = 2;
@@ -669,36 +699,6 @@ public class VelocityEventManager implements EventManager {
} }
} }
private <E> void fire(final @Nullable CompletableFuture<E> 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<E> 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( private static void logHandlerException(
final HandlerRegistration registration, final Throwable t) { final HandlerRegistration registration, final Throwable t) {
final PluginDescription pluginDescription = registration.plugin.getDescription(); final PluginDescription pluginDescription = registration.plugin.getDescription();
@@ -143,6 +143,7 @@ public enum ProtocolUtils {
.build(); .build();
public static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB 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 int MAXIMUM_VARINT_SIZE = 5;
private static final BinaryTagType<? extends BinaryTag>[] BINARY_TAG_TYPES = new BinaryTagType[] { private static final BinaryTagType<? extends BinaryTag>[] BINARY_TAG_TYPES = new BinaryTagType[] {
BinaryTagTypes.END, BinaryTagTypes.BYTE, BinaryTagTypes.SHORT, BinaryTagTypes.INT, BinaryTagTypes.END, BinaryTagTypes.BYTE, BinaryTagTypes.SHORT, BinaryTagTypes.INT,
@@ -349,15 +350,20 @@ public enum ProtocolUtils {
writeString(buf, key.asMinimalString()); 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. * Reads a standard Mojang Text namespaced:key array from the buffer.
* *
* @param buf the buffer to read from * @param buf the buffer to read from
* @return the decoded key array * @return the decoded key array
*/ */
public static Key[] readKeyArray(ByteBuf buf) { public static Key[] readKeyArray(ByteBuf buf, int cap) {
int length = readVarInt(buf); int length = readVarInt(buf);
checkFrame(length >= 0, "Got a negative-length array (%s)", length); 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), checkFrame(buf.isReadable(length),
"Trying to read an array that is too long (wanted %s, only have %s)", length, "Trying to read an array that is too long (wanted %s, only have %s)", length,
buf.readableBytes()); buf.readableBytes());
@@ -644,7 +650,7 @@ public enum ProtocolUtils {
private static final int FORGE_MAX_ARRAY_LENGTH = Integer.MAX_VALUE & 0x1FFF9A; 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 * @param buf the buffer to read from
* @return the read byte array * @return the read byte array
@@ -685,7 +691,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 b array
* @param buf buf * @param buf buf
@@ -709,7 +715,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 b array
* @param buf buf * @param buf buf
@@ -66,7 +66,6 @@ import com.velocitypowered.proxy.protocol.packet.StatusResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TransferPacket; import com.velocitypowered.proxy.protocol.packet.TransferPacket;
import com.velocitypowered.proxy.protocol.packet.UpdateTeamsPacket;
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket; import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket;
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket; import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket;
@@ -205,31 +204,44 @@ public enum StateRegistry {
map(0x06, MINECRAFT_1_20_2, false), map(0x06, MINECRAFT_1_20_2, false),
map(0x07, MINECRAFT_1_20_3, false), map(0x07, MINECRAFT_1_20_3, false),
map(0x09, MINECRAFT_1_20_5, false)); map(0x09, MINECRAFT_1_20_5, false));
clientbound.register(
ClientboundPostEffectsPacket.class, ClientboundPostEffectsPacket::new,
map(0x0A, MINECRAFT_26_3, false));
clientbound.register( clientbound.register(
ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new, 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, 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, clientbound.register(ActiveFeaturesPacket.class, ActiveFeaturesPacket::new,
map(0x07, MINECRAFT_1_20_2, false), map(0x07, MINECRAFT_1_20_2, false),
map(0x08, MINECRAFT_1_20_3, 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, clientbound.register(TagsUpdatePacket.class, TagsUpdatePacket::new,
map(0x08, MINECRAFT_1_20_2, false), map(0x08, MINECRAFT_1_20_2, false),
map(0x09, MINECRAFT_1_20_3, 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, 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, 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, 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, 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), 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, 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 { PLAY {
@@ -381,7 +393,8 @@ public enum StateRegistry {
map(0x2D, MINECRAFT_1_21_2, false), map(0x2D, MINECRAFT_1_21_2, false),
map(0x2F, MINECRAFT_1_21_4, false), map(0x2F, MINECRAFT_1_21_4, false),
map(0x30, MINECRAFT_1_21_6, 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( serverbound.register(
FinishedUpdatePacket.class, () -> FinishedUpdatePacket.INSTANCE, FinishedUpdatePacket.class, () -> FinishedUpdatePacket.INSTANCE,
map(0x0B, MINECRAFT_1_20_2, false), map(0x0B, MINECRAFT_1_20_2, false),
@@ -440,6 +453,9 @@ public enum StateRegistry {
ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new, ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new,
map(0x16, MINECRAFT_1_20_5, false), map(0x16, MINECRAFT_1_20_5, false),
map(0x15, MINECRAFT_1_21_5, false)); map(0x15, MINECRAFT_1_21_5, false));
clientbound.register(
ClientboundPostEffectsPacket.class, ClientboundPostEffectsPacket::new,
map(0x53, MINECRAFT_26_3, false));
clientbound.register( clientbound.register(
ClientboundSoundEntityPacket.class, ClientboundSoundEntityPacket::new, ClientboundSoundEntityPacket.class, ClientboundSoundEntityPacket::new,
map(0x5D, MINECRAFT_1_19_3, true), map(0x5D, MINECRAFT_1_19_3, true),
@@ -450,7 +466,8 @@ public enum StateRegistry {
map(0x6E, MINECRAFT_1_21_2, true), map(0x6E, MINECRAFT_1_21_2, true),
map(0x6D, MINECRAFT_1_21_5, true), map(0x6D, MINECRAFT_1_21_5, true),
map(0x72, MINECRAFT_1_21_9, 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( clientbound.register(
ClientboundStopSoundPacket.class, ClientboundStopSoundPacket::new, ClientboundStopSoundPacket.class, ClientboundStopSoundPacket::new,
map(0x5F, MINECRAFT_1_19_3, true), map(0x5F, MINECRAFT_1_19_3, true),
@@ -461,7 +478,8 @@ public enum StateRegistry {
map(0x71, MINECRAFT_1_21_2, true), map(0x71, MINECRAFT_1_21_2, true),
map(0x70, MINECRAFT_1_21_5, true), map(0x70, MINECRAFT_1_21_5, true),
map(0x75, MINECRAFT_1_21_9, 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( clientbound.register(
PluginMessagePacket.class, PluginMessagePacket.class,
PluginMessagePacket::new, PluginMessagePacket::new,
@@ -519,7 +537,8 @@ public enum StateRegistry {
map(0x27, MINECRAFT_1_21_2, false), map(0x27, MINECRAFT_1_21_2, false),
map(0x26, MINECRAFT_1_21_5, false), map(0x26, MINECRAFT_1_21_5, false),
map(0x2B, MINECRAFT_1_21_9, 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( clientbound.register(
JoinGamePacket.class, JoinGamePacket.class,
JoinGamePacket::new, JoinGamePacket::new,
@@ -540,7 +559,8 @@ public enum StateRegistry {
map(0x2C, MINECRAFT_1_21_2, false), map(0x2C, MINECRAFT_1_21_2, false),
map(0x2B, MINECRAFT_1_21_5, false), map(0x2B, MINECRAFT_1_21_5, false),
map(0x30, MINECRAFT_1_21_9, 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( clientbound.register(
RespawnPacket.class, RespawnPacket.class,
RespawnPacket::new, RespawnPacket::new,
@@ -564,7 +584,8 @@ public enum StateRegistry {
map(0x4C, MINECRAFT_1_21_2, true), map(0x4C, MINECRAFT_1_21_2, true),
map(0x4B, MINECRAFT_1_21_5, true), map(0x4B, MINECRAFT_1_21_5, true),
map(0x50, MINECRAFT_1_21_9, 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( clientbound.register(
RemoveResourcePackPacket.class, RemoveResourcePackPacket.class,
RemoveResourcePackPacket::new, RemoveResourcePackPacket::new,
@@ -573,7 +594,8 @@ public enum StateRegistry {
map(0x4A, MINECRAFT_1_21_2, false), map(0x4A, MINECRAFT_1_21_2, false),
map(0x49, MINECRAFT_1_21_5, false), map(0x49, MINECRAFT_1_21_5, false),
map(0x4E, MINECRAFT_1_21_9, 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( clientbound.register(
ResourcePackRequestPacket.class, ResourcePackRequestPacket.class,
ResourcePackRequestPacket::new, ResourcePackRequestPacket::new,
@@ -597,7 +619,8 @@ public enum StateRegistry {
map(0x4B, MINECRAFT_1_21_2, false), map(0x4B, MINECRAFT_1_21_2, false),
map(0x4A, MINECRAFT_1_21_5, false), map(0x4A, MINECRAFT_1_21_5, false),
map(0x4F, MINECRAFT_1_21_9, 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( clientbound.register(
HeaderAndFooterPacket.class, HeaderAndFooterPacket.class,
HeaderAndFooterPacket::new, HeaderAndFooterPacket::new,
@@ -622,7 +645,8 @@ public enum StateRegistry {
map(0x74, MINECRAFT_1_21_2, true), map(0x74, MINECRAFT_1_21_2, true),
map(0x73, MINECRAFT_1_21_5, true), map(0x73, MINECRAFT_1_21_5, true),
map(0x78, MINECRAFT_1_21_9, 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( clientbound.register(
LegacyTitlePacket.class, LegacyTitlePacket.class,
LegacyTitlePacket::new, LegacyTitlePacket::new,
@@ -646,7 +670,8 @@ public enum StateRegistry {
map(0x6A, MINECRAFT_1_21_2, true), map(0x6A, MINECRAFT_1_21_2, true),
map(0x69, MINECRAFT_1_21_5, true), map(0x69, MINECRAFT_1_21_5, true),
map(0x6E, MINECRAFT_1_21_9, 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( clientbound.register(
TitleTextPacket.class, TitleTextPacket.class,
TitleTextPacket::new, TitleTextPacket::new,
@@ -661,7 +686,8 @@ public enum StateRegistry {
map(0x6C, MINECRAFT_1_21_2, true), map(0x6C, MINECRAFT_1_21_2, true),
map(0x6B, MINECRAFT_1_21_5, true), map(0x6B, MINECRAFT_1_21_5, true),
map(0x70, MINECRAFT_1_21_9, 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( clientbound.register(
TitleActionbarPacket.class, TitleActionbarPacket.class,
TitleActionbarPacket::new, TitleActionbarPacket::new,
@@ -676,7 +702,8 @@ public enum StateRegistry {
map(0x51, MINECRAFT_1_21_2, true), map(0x51, MINECRAFT_1_21_2, true),
map(0x50, MINECRAFT_1_21_5, true), map(0x50, MINECRAFT_1_21_5, true),
map(0x55, MINECRAFT_1_21_9, 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( clientbound.register(
TitleTimesPacket.class, TitleTimesPacket.class,
TitleTimesPacket::new, TitleTimesPacket::new,
@@ -691,7 +718,8 @@ public enum StateRegistry {
map(0x6D, MINECRAFT_1_21_2, true), map(0x6D, MINECRAFT_1_21_2, true),
map(0x6C, MINECRAFT_1_21_5, true), map(0x6C, MINECRAFT_1_21_5, true),
map(0x71, MINECRAFT_1_21_9, 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( clientbound.register(
TitleClearPacket.class, TitleClearPacket.class,
TitleClearPacket::new, TitleClearPacket::new,
@@ -723,7 +751,8 @@ public enum StateRegistry {
map(0x3F, MINECRAFT_1_21_2, false), map(0x3F, MINECRAFT_1_21_2, false),
map(0x3E, MINECRAFT_1_21_5, false), map(0x3E, MINECRAFT_1_21_5, false),
map(0x43, MINECRAFT_1_21_9, 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( clientbound.register(
UpsertPlayerInfoPacket.class, UpsertPlayerInfoPacket.class,
UpsertPlayerInfoPacket::new, UpsertPlayerInfoPacket::new,
@@ -734,14 +763,16 @@ public enum StateRegistry {
map(0x40, MINECRAFT_1_21_2, false), map(0x40, MINECRAFT_1_21_2, false),
map(0x3F, MINECRAFT_1_21_5, false), map(0x3F, MINECRAFT_1_21_5, false),
map(0x44, MINECRAFT_1_21_9, 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( clientbound.register(
ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new, ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new,
map(0x6B, MINECRAFT_1_20_5, false), map(0x6B, MINECRAFT_1_20_5, false),
map(0x72, MINECRAFT_1_21_2, false), map(0x72, MINECRAFT_1_21_2, false),
map(0x71, MINECRAFT_1_21_5, false), map(0x71, MINECRAFT_1_21_5, false),
map(0x76, MINECRAFT_1_21_9, 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( clientbound.register(
SystemChatPacket.class, SystemChatPacket.class,
SystemChatPacket::new, SystemChatPacket::new,
@@ -755,7 +786,8 @@ public enum StateRegistry {
map(0x73, MINECRAFT_1_21_2, true), map(0x73, MINECRAFT_1_21_2, true),
map(0x72, MINECRAFT_1_21_5, true), map(0x72, MINECRAFT_1_21_5, true),
map(0x77, MINECRAFT_1_21_9, 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( clientbound.register(
PlayerChatCompletionPacket.class, PlayerChatCompletionPacket.class,
PlayerChatCompletionPacket::new, PlayerChatCompletionPacket::new,
@@ -778,7 +810,8 @@ public enum StateRegistry {
map(0x50, MINECRAFT_1_21_2, false), map(0x50, MINECRAFT_1_21_2, false),
map(0x4F, MINECRAFT_1_21_5, false), map(0x4F, MINECRAFT_1_21_5, false),
map(0x54, MINECRAFT_1_21_9, 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( clientbound.register(
StartUpdatePacket.class, StartUpdatePacket.class,
() -> StartUpdatePacket.INSTANCE, () -> StartUpdatePacket.INSTANCE,
@@ -788,7 +821,8 @@ public enum StateRegistry {
map(0x70, MINECRAFT_1_21_2, false), map(0x70, MINECRAFT_1_21_2, false),
map(0x6F, MINECRAFT_1_21_5, false), map(0x6F, MINECRAFT_1_21_5, false),
map(0x74, MINECRAFT_1_21_9, 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( clientbound.register(
BundleDelimiterPacket.class, BundleDelimiterPacket.class,
() -> BundleDelimiterPacket.INSTANCE, () -> BundleDelimiterPacket.INSTANCE,
@@ -799,20 +833,24 @@ public enum StateRegistry {
map(0x73, MINECRAFT_1_20_5, false), map(0x73, MINECRAFT_1_20_5, false),
map(0x7A, MINECRAFT_1_21_2, false), map(0x7A, MINECRAFT_1_21_2, false),
map(0x7F, MINECRAFT_1_21_9, 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( clientbound.register(
ClientboundCustomReportDetailsPacket.class, ClientboundCustomReportDetailsPacket.class,
ClientboundCustomReportDetailsPacket::new, ClientboundCustomReportDetailsPacket::new,
map(0x7A, MINECRAFT_1_21, false), map(0x7A, MINECRAFT_1_21, false),
map(0x81, MINECRAFT_1_21_2, false), map(0x81, MINECRAFT_1_21_2, false),
map(0x86, MINECRAFT_1_21_9, 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( clientbound.register(
ClientboundServerLinksPacket.class, ClientboundServerLinksPacket.class,
ClientboundServerLinksPacket::new, ClientboundServerLinksPacket::new,
map(0x7B, MINECRAFT_1_21, false), map(0x7B, MINECRAFT_1_21, false),
map(0x82, MINECRAFT_1_21_2, false), map(0x82, MINECRAFT_1_21_2, false),
map(0x87, MINECRAFT_1_21_9, false), map(0x87, MINECRAFT_1_21_9, false),
map(0x89, MINECRAFT_26_1, false),
map(0x8C, MINECRAFT_26_3, false));
map(0x89, MINECRAFT_26_1, false)); map(0x89, MINECRAFT_26_1, false));
clientbound.register( clientbound.register(
UpdateTeamsPacket.class, UpdateTeamsPacket.class,
@@ -213,7 +213,34 @@ public class ClientSettingsPacket implements MinecraftPacket {
@Override @Override
public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { 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 @Override
@@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}
@@ -32,7 +32,7 @@ public class JoinGamePacket implements MinecraftPacket {
private static final BinaryTagIO.Reader JOINGAME_READER = BinaryTagIO.reader(4 * 1024 * 1024); private static final BinaryTagIO.Reader JOINGAME_READER = BinaryTagIO.reader(4 * 1024 * 1024);
private int entityId; private int entityId;
private short gamemode; private int gamemode;
private int dimension; private int dimension;
private long partialHashedSeed; // 1.15+ private long partialHashedSeed; // 1.15+
private short difficulty; private short difficulty;
@@ -47,7 +47,7 @@ public class JoinGamePacket implements MinecraftPacket {
private CompoundBinaryTag registry; // 1.16+ private CompoundBinaryTag registry; // 1.16+
private DimensionInfo dimensionInfo; // 1.16+ private DimensionInfo dimensionInfo; // 1.16+
private CompoundBinaryTag currentDimensionData; // 1.16.2+ private CompoundBinaryTag currentDimensionData; // 1.16.2+
private short previousGamemode; // 1.16+ private int previousGamemode; // 1.16+
private int simulationDistance; // 1.18+ private int simulationDistance; // 1.18+
private @Nullable Pair<String, Long> lastDeathPosition; // 1.19+ private @Nullable Pair<String, Long> lastDeathPosition; // 1.19+
private int portalCooldown; // 1.20+ private int portalCooldown; // 1.20+
@@ -63,11 +63,11 @@ public class JoinGamePacket implements MinecraftPacket {
this.entityId = entityId; this.entityId = entityId;
} }
public short getGamemode() { public int getGamemode() {
return gamemode; return gamemode;
} }
public void setGamemode(short gamemode) { public void setGamemode(int gamemode) {
this.gamemode = gamemode; this.gamemode = gamemode;
} }
@@ -131,11 +131,11 @@ public class JoinGamePacket implements MinecraftPacket {
this.dimensionInfo = dimensionInfo; this.dimensionInfo = dimensionInfo;
} }
public short getPreviousGamemode() { public int getPreviousGamemode() {
return previousGamemode; return previousGamemode;
} }
public void setPreviousGamemode(short previousGamemode) { public void setPreviousGamemode(int previousGamemode) {
this.previousGamemode = previousGamemode; this.previousGamemode = previousGamemode;
} }
@@ -345,8 +345,13 @@ public class JoinGamePacket implements MinecraftPacket {
String levelName = ProtocolUtils.readString(buf); String levelName = ProtocolUtils.readString(buf);
this.partialHashedSeed = buf.readLong(); this.partialHashedSeed = buf.readLong();
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.gamemode = buf.readByte();
this.previousGamemode = buf.readByte(); this.previousGamemode = buf.readByte(); // game mode or -1
}
boolean isDebug = buf.readBoolean(); boolean isDebug = buf.readBoolean();
boolean isFlat = buf.readBoolean(); boolean isFlat = buf.readBoolean();
@@ -498,8 +503,13 @@ public class JoinGamePacket implements MinecraftPacket {
ProtocolUtils.writeString(buf, dimensionInfo.getLevelName()); ProtocolUtils.writeString(buf, dimensionInfo.getLevelName());
buf.writeLong(partialHashedSeed); buf.writeLong(partialHashedSeed);
buf.writeByte(gamemode); if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) {
buf.writeByte(previousGamemode); 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.isDebugType());
buf.writeBoolean(dimensionInfo.isFlat()); buf.writeBoolean(dimensionInfo.isFlat());
@@ -33,11 +33,11 @@ public class RespawnPacket implements MinecraftPacket {
private int dimension; private int dimension;
private long partialHashedSeed; private long partialHashedSeed;
private short difficulty; private short difficulty;
private short gamemode; private int gamemode;
private String levelType = ""; private String levelType = "";
private byte dataToKeep; // 1.16+ private byte dataToKeep; // 1.16+
private DimensionInfo dimensionInfo; // 1.16-1.16.1 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 CompoundBinaryTag currentDimensionData; // 1.16.2+
private @Nullable Pair<String, Long> lastDeathPosition; // 1.19+ private @Nullable Pair<String, Long> lastDeathPosition; // 1.19+
private int portalCooldown; // 1.20+ private int portalCooldown; // 1.20+
@@ -46,9 +46,9 @@ public class RespawnPacket implements MinecraftPacket {
public RespawnPacket() { 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, String levelType, byte dataToKeep, DimensionInfo dimensionInfo,
short previousGamemode, CompoundBinaryTag currentDimensionData, int previousGamemode, CompoundBinaryTag currentDimensionData,
@Nullable Pair<String, Long> lastDeathPosition, int portalCooldown, @Nullable Pair<String, Long> lastDeathPosition, int portalCooldown,
int seaLevel) { int seaLevel) {
this.dimension = dimension; this.dimension = dimension;
@@ -97,11 +97,11 @@ public class RespawnPacket implements MinecraftPacket {
this.difficulty = difficulty; this.difficulty = difficulty;
} }
public short getGamemode() { public int getGamemode() {
return gamemode; return gamemode;
} }
public void setGamemode(short gamemode) { public void setGamemode(int gamemode) {
this.gamemode = gamemode; this.gamemode = gamemode;
} }
@@ -121,11 +121,11 @@ public class RespawnPacket implements MinecraftPacket {
this.dataToKeep = dataToKeep; this.dataToKeep = dataToKeep;
} }
public short getPreviousGamemode() { public int getPreviousGamemode() {
return previousGamemode; return previousGamemode;
} }
public void setPreviousGamemode(short previousGamemode) { public void setPreviousGamemode(int previousGamemode) {
this.previousGamemode = previousGamemode; this.previousGamemode = previousGamemode;
} }
@@ -197,9 +197,17 @@ public class RespawnPacket implements MinecraftPacket {
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15)) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15)) {
this.partialHashedSeed = buf.readLong(); this.partialHashedSeed = buf.readLong();
} }
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) {
this.gamemode = ProtocolUtils.readVarInt(buf);
} else {
this.gamemode = buf.readByte(); this.gamemode = buf.readByte();
}
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_16)) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_16)) {
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) {
this.previousGamemode = ProtocolUtils.readVarInt(buf);
} else {
this.previousGamemode = buf.readByte(); this.previousGamemode = buf.readByte();
}
boolean isDebug = buf.readBoolean(); boolean isDebug = buf.readBoolean();
boolean isFlat = buf.readBoolean(); boolean isFlat = buf.readBoolean();
this.dimensionInfo = new DimensionInfo(dimensionKey, levelName, isFlat, isDebug, version); 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)) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15)) {
buf.writeLong(partialHashedSeed); 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)) { 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.isDebugType());
buf.writeBoolean(dimensionInfo.isFlat()); buf.writeBoolean(dimensionInfo.isFlat());
if (version.lessThan(ProtocolVersion.MINECRAFT_1_19_3)) { if (version.lessThan(ProtocolVersion.MINECRAFT_1_19_3)) {
@@ -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_5;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_6; 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_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.id;
import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.mapSet; import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.mapSet;
import static com.velocitypowered.proxy.protocol.packet.brigadier.DoubleArgumentPropertySerializer.DOUBLE; 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), 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 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), 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_19_4, 48), mapSet(MINECRAFT_1_19, 47))); // added in 1.16 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_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_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: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: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: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 // Crossstitch support
register(id("crossstitch:mod_argument", mapSet(MINECRAFT_1_19, -256)), ModArgumentProperty.class, MOD); register(id("crossstitch:mod_argument", mapSet(MINECRAFT_1_19, -256)), ModArgumentProperty.class, MOD);
@@ -22,10 +22,12 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
public abstract class GenericTitlePacket implements MinecraftPacket { public abstract class GenericTitlePacket implements MinecraftPacket {
public enum ActionType { public enum ActionType {
SET_TITLE(0), SET_TITLE(0),
SET_SUBTITLE(1), SET_SUBTITLE(1),
SET_ACTION_BAR(2), SET_ACTION_BAR(2),
@@ -45,16 +47,7 @@ public abstract class GenericTitlePacket implements MinecraftPacket {
} }
} }
public abstract @NotNull ActionType getAction();
private ActionType action;
protected void setAction(ActionType action) {
this.action = action;
}
public final ActionType getAction() {
return action;
}
public ComponentHolder getComponent() { public ComponentHolder getComponent() {
throw new UnsupportedOperationException("Invalid function for this TitlePacket ActionType"); 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"); throw new UnsupportedOperationException("Invalid function for this TitlePacket ActionType");
} }
@Override @Override
public final void decode(ByteBuf buf, ProtocolUtils.Direction direction, public final void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
ProtocolVersion version) { throw new UnsupportedOperationException("Decode is not implemented");
throw new UnsupportedOperationException(); // encode only
} }
/** /**
@@ -103,21 +94,17 @@ public abstract class GenericTitlePacket implements MinecraftPacket {
* @return GenericTitlePacket instance that follows the invoker type/version * @return GenericTitlePacket instance that follows the invoker type/version
*/ */
public static GenericTitlePacket constructTitlePacket(ActionType type, ProtocolVersion version) { public static GenericTitlePacket constructTitlePacket(ActionType type, ProtocolVersion version) {
GenericTitlePacket packet = null;
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_17)) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_17)) {
packet = switch (type) { return switch (type) {
case SET_ACTION_BAR -> new TitleActionbarPacket(); case SET_ACTION_BAR -> new TitleActionbarPacket();
case SET_SUBTITLE -> new TitleSubtitlePacket(); case SET_SUBTITLE -> new TitleSubtitlePacket();
case SET_TIMES -> new TitleTimesPacket(); case SET_TIMES -> new TitleTimesPacket();
case SET_TITLE -> new TitleTextPacket(); case SET_TITLE -> new TitleTextPacket();
case HIDE, RESET -> new TitleClearPacket(); case HIDE, RESET -> new TitleClearPacket(type);
default -> throw new IllegalArgumentException("Invalid ActionType"); default -> throw new IllegalArgumentException("Invalid ActionType");
}; };
} else { } else {
packet = new LegacyTitlePacket(); return new LegacyTitlePacket(type);
} }
packet.setAction(type);
return packet;
} }
} }
@@ -17,32 +17,44 @@
package com.velocitypowered.proxy.protocol.packet.title; package com.velocitypowered.proxy.protocol.packet.title;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf; 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 { public class LegacyTitlePacket extends GenericTitlePacket {
private final ActionType action;
private @Nullable ComponentHolder component; private @Nullable ComponentHolder component;
private int fadeIn; private int fadeIn;
private int stay; private int stay;
private int fadeOut; private int fadeOut;
public LegacyTitlePacket() {
throw new UnsupportedOperationException("Decode is not implemented");
}
public LegacyTitlePacket(ActionType action) {
this.action = Preconditions.checkNotNull(action, "action");
}
@Override @Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
if (version.lessThan(ProtocolVersion.MINECRAFT_1_11) 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"); 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 -> { case SET_TITLE, SET_SUBTITLE, SET_ACTION_BAR -> {
if (component == null) { if (component == null) {
throw new IllegalStateException("No component found for " + getAction()); throw new IllegalStateException("No component found for " + this.action);
} }
component.write(buf); component.write(buf);
} }
@@ -52,14 +64,13 @@ public class LegacyTitlePacket extends GenericTitlePacket {
buf.writeInt(fadeOut); buf.writeInt(fadeOut);
} }
case HIDE, RESET -> {} case HIDE, RESET -> {}
default -> throw new UnsupportedOperationException("Unknown action " + getAction()); default -> throw new UnsupportedOperationException("Unknown action " + this.action);
} }
} }
@Override @Override
public void setAction(ActionType action) { public @NotNull ActionType getAction() {
super.setAction(action); return action;
} }
@Override @Override
@@ -105,7 +116,7 @@ public class LegacyTitlePacket extends GenericTitlePacket {
@Override @Override
public String toString() { public String toString() {
return "GenericTitlePacket{" return "GenericTitlePacket{"
+ "action=" + getAction() + "action=" + action
+ ", component='" + component + '\'' + ", component='" + component + '\''
+ ", fadeIn=" + fadeIn + ", fadeIn=" + fadeIn
+ ", stay=" + stay + ", stay=" + stay
@@ -22,13 +22,15 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
public class TitleActionbarPacket extends GenericTitlePacket { public class TitleActionbarPacket extends GenericTitlePacket {
private ComponentHolder component; private ComponentHolder component;
public TitleActionbarPacket() { @Override
setAction(ActionType.SET_ACTION_BAR); public @NotNull ActionType getAction() {
return ActionType.SET_ACTION_BAR;
} }
@Override @Override
@@ -21,30 +21,37 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
public class TitleClearPacket extends GenericTitlePacket { public class TitleClearPacket extends GenericTitlePacket {
private final ActionType action;
public TitleClearPacket() { 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 @Override
public void setAction(ActionType action) { public @NotNull ActionType getAction() {
if (action != ActionType.HIDE && action != ActionType.RESET) { return action;
throw new IllegalArgumentException("TitleClearPacket only accepts CLEAR and RESET actions");
}
super.setAction(action);
} }
@Override @Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
buf.writeBoolean(getAction() == ActionType.RESET); buf.writeBoolean(this.action == ActionType.RESET);
} }
@Override @Override
public String toString() { public String toString() {
return "TitleClearPacket{" return "TitleClearPacket{"
+ ", resetTimes=" + (getAction() == ActionType.RESET) + ", resetTimes=" + (this.action == ActionType.RESET)
+ '}'; + '}';
} }
@@ -22,13 +22,15 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
public class TitleSubtitlePacket extends GenericTitlePacket { public class TitleSubtitlePacket extends GenericTitlePacket {
private ComponentHolder component; private ComponentHolder component;
public TitleSubtitlePacket() { @Override
setAction(ActionType.SET_SUBTITLE); public @NotNull ActionType getAction() {
return ActionType.SET_SUBTITLE;
} }
@Override @Override
@@ -22,13 +22,15 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
public class TitleTextPacket extends GenericTitlePacket { public class TitleTextPacket extends GenericTitlePacket {
private ComponentHolder component; private ComponentHolder component;
public TitleTextPacket() { @Override
setAction(ActionType.SET_TITLE); public @NotNull ActionType getAction() {
return ActionType.SET_TITLE;
} }
@Override @Override
@@ -21,6 +21,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
public class TitleTimesPacket extends GenericTitlePacket { public class TitleTimesPacket extends GenericTitlePacket {
@@ -28,8 +29,9 @@ public class TitleTimesPacket extends GenericTitlePacket {
private int stay; private int stay;
private int fadeOut; private int fadeOut;
public TitleTimesPacket() { @Override
setAction(ActionType.SET_TIMES); public @NotNull ActionType getAction() {
return ActionType.SET_TIMES;
} }
@Override @Override
@@ -30,40 +30,51 @@ package com.velocitypowered.proxy.util;
* <p>This class is not thread-safe. If multiple threads access an instance concurrently, * <p>This class is not thread-safe. If multiple threads access an instance concurrently,
* external synchronization is required.</p> * external synchronization is required.</p>
*/ */
@SuppressWarnings("checkstyle:WhitespaceAfter") // Not our class
public final class IntervalledCounter { public final class IntervalledCounter {
private static final int INITIAL_SIZE = 8; 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. * 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. * 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) * The sliding window size in nanoseconds. Only entries with time >= (currentTime - interval)
* are considered part of the window. * 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. * 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. * Running sum of all counts currently within the window.
*/ */
protected long sum; private long sum;
/** /**
* Head index (inclusive) of the ring buffer. * Head index (inclusive) of the ring buffer.
*/ */
protected int head; // inclusive private int head; // inclusive
/** /**
* Tail index (exclusive) of the ring buffer. * Tail index (exclusive) of the ring buffer.
*/ */
protected int tail; // exclusive private int tail; // exclusive
/** /**
* Creates a new counter with the specified interval. * 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 * 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. * 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 * This method does not automatically advance the window; callers should invoke
* {@link #updateCurrentTime()} or {@link #updateCurrentTime(long)} beforehand. * {@link #updateCurrentTime()} or {@link #updateCurrentTime(long)} beforehand.
* *
@@ -142,6 +155,15 @@ public final class IntervalledCounter {
if (currTime - this.minTime < 0) { if (currTime - this.minTime < 0) {
return; 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; int nextTail = (this.tail + 1) % this.times.length;
if (nextTail == this.head) { if (nextTail == this.head) {
this.resize(); this.resize();
+14 -17
View File
@@ -1,5 +1,5 @@
# Config version. Do not change this # 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. # 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" 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 # Announce whether or not your server supports Forge. If you run a modded server, we
# suggest turning this on. # 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. # instead for a nicer display in the server list.
announce-forge = false announce-forge = false
@@ -51,21 +51,6 @@ announce-forge = false
# any existing player who is online if a duplicate connection attempt is made. # any existing player who is online if a duplicate connection attempt is made.
kick-existing-players = false 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 # 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. # 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". # 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 <ip address withheld> in logs # If not enabled (default is true) player IP addresses will be replaced by <ip address withheld> in logs
enable-player-address-logging = true 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] [packet-limiter]
# Size of the moving time window in seconds used to calculate average rates. # 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. # A larger window tolerates short bursts while still enforcing the configured limits over time.
@@ -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 <https://www.gnu.org/licenses/>.
*/
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());
}
}