Merge pull request 'Update to 26.3' (#4) from update into master
SteamWarCI Build successful

Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
2026-09-24 17:09:44 +02:00
47 changed files with 794 additions and 315 deletions
+15 -5
View File
@@ -6,18 +6,28 @@ on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-24.04
runs-on: namespace-profile-linux-default
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: namespacelabs/nscloud-checkout-action@v9
with:
persist-credentials: false
- name: Set up cache
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: gradle
- name: Setup gradle build cache
run: |
nsc cache gradle setup --init-gradle /tmp/init.gradle
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v5
uses: gradle/actions/setup-gradle@v6
with:
cache-disabled: true # namespace handles this
- name: Set up JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@v6
with:
java-version: 21
distribution: 'zulu'
cache: "" # namespace handles this
- name: Build with Gradle
run: ./gradlew build
run: ./gradlew --init-script=/tmp/init.gradle build
+2
View File
@@ -40,3 +40,5 @@ page.
Translations are handled using [Crowdin](https://papermc-io.crowdin.com/velocity).
If you want to translate a language not available on Crowdin,
you might want to ask in the [Discord](https://discord.gg/papermc) about it.
[![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.
*
* <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>
*
@@ -40,9 +40,9 @@ public final class PlayerClientBrandEvent {
@Override
public String toString() {
return "PlayerClientBrandEvent{"
+ "player=" + player
+ ", brand='" + brand + '\''
+ '}';
+ "player=" + player
+ ", brand='" + brand + '\''
+ '}';
}
}
@@ -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.
*
* <p>Velocity will wait for this event before continuing/ending the configuration state.</p>
*
* @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.
* 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.
* 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>
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
/**
* This event is executed when a player has entered the configuration state.
*
* <p>From this moment on, until the {@link PlayerFinishedConfigurationEvent} is executed,
* the {@linkplain Player#getProtocolState()} method is guaranteed
* 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.
*
* <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,
* 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.
*
* <p>From this moment on, the {@link Player#getProtocolState()} method
* will return {@link ProtocolState#PLAY}.</p>
*
@@ -16,6 +16,7 @@ package com.velocitypowered.api.network;
public enum ProtocolState {
/**
* Initial connection State.
*
* <p>This status can be caused by a {@link HandshakeIntent#STATUS},
* {@link HandshakeIntent#LOGIN} or {@link HandshakeIntent#TRANSFER} intent.</p>
* If the intent is LOGIN or TRANSFER, the next state will be {@link #LOGIN},
@@ -24,6 +25,7 @@ public enum ProtocolState {
HANDSHAKE,
/**
* Ping State of a connection.
*
* <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 the server responds with the respective ping.</p>
@@ -31,11 +33,13 @@ public enum ProtocolState {
STATUS,
/**
* Authentication State of a connection.
*
* <p>At this moment the player is authenticating with the authentication servers.</p>
*/
LOGIN,
/**
* Configuration State of a connection.
*
* <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
* will send his client brand and the respective plugin messages
@@ -46,6 +50,7 @@ public enum ProtocolState {
CONFIGURATION,
/**
* Game State of a connection.
*
* <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>
*/
@@ -96,7 +96,8 @@ public enum ProtocolVersion implements Ordered<ProtocolVersion> {
MINECRAFT_1_21_9(773, "1.21.9", "1.21.10"),
MINECRAFT_1_21_11(774, "1.21.11"),
MINECRAFT_26_1(775, "26.1", "26.1.1", "26.1.2"),
MINECRAFT_26_2(776, "26.2");
MINECRAFT_26_2(776, "26.2"),
MINECRAFT_26_3(777, "26.3");
private static final int SNAPSHOT_BIT = 30;
@@ -15,7 +15,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* Represents three different states of a setting.
*
* <p>Possible values:</p>
* <p></p>
* <ul>
* <li>{@link #TRUE} - a positive setting</li>
* <li>{@link #FALSE} - a negative (negated) setting</li>
@@ -244,7 +244,7 @@ public interface Player extends
* Gets the {@link ResourcePackInfo} of the currently applied
* resource-pack or null if none.
*
* <p> Note that since 1.20.3 it is no longer recommended to use
* <p>Note that since 1.20.3 it is no longer recommended to use
* this method as it will only return the last applied
* resource pack. To get all applied resource packs, use
* {@link #getAppliedResourcePacks()} instead. </p>
@@ -260,7 +260,7 @@ public interface Player extends
* the user is currently downloading or is currently
* prompted to install or null if none.
*
* <p> Note that since 1.20.3 it is no longer recommended to use
* <p>Note that since 1.20.3 it is no longer recommended to use
* this method as it will only return the last pending
* resource pack. To get all pending resource packs, use
* {@link #getPendingResourcePacks()} instead. </p>
@@ -315,6 +315,7 @@ public interface Player extends
/**
* {@inheritDoc}
*
* <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
* with a mod that is installed on the player's client.</p>
@@ -400,7 +401,7 @@ public interface Player extends
* and will not perform any actions.</b>
* @see #playSound(Sound, Sound.Emitter)
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
* Unsupported Adventure Operations</a>
* Unsupported Adventure Operations</a>
*/
@Override
default void playSound(@NotNull Sound sound) {
@@ -413,7 +414,7 @@ public interface Player extends
* and will not perform any actions.</b>
* @see #playSound(Sound, Sound.Emitter)
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
* Unsupported Adventure Operations</a>
* Unsupported Adventure Operations</a>
*/
@Override
default void playSound(@NotNull Sound sound, double x, double y, double z) {
@@ -456,7 +457,7 @@ public interface Player extends
* and will not perform any actions.</b>
*
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
* Unsupported Adventure Operations</a>
* Unsupported Adventure Operations</a>
*/
@Override
default void openBook(@NotNull Book book) {
@@ -469,7 +470,7 @@ public interface Player extends
* and will not perform any actions.</b>
*
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
* Unsupported Adventure Operations</a>
* Unsupported Adventure Operations</a>
*/
@Override
default void showDialog(@NotNull DialogLike dialog) {
@@ -482,7 +483,7 @@ public interface Player extends
* and will not perform any actions.</b>
*
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
* Unsupported Adventure Operations</a>
* Unsupported Adventure Operations</a>
*/
@Override
default void closeDialog() {
@@ -56,6 +56,7 @@ public interface KeySigned {
* Validates the signature, expiry temporal and key against the
* signer public key. Note: This will **not** check for
* expiry. You can check for expiry with {@link KeySigned#hasExpired()}.
*
* <p>DOES NOT WORK YET FOR MESSAGES AND COMMANDS!</p>
* 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}.
*
* <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>Will ignore mismatching key revisions data.</p>
*
* @param chatSession session to set
+1 -1
View File
@@ -1,2 +1,2 @@
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"
flare = "2.0.1"
log4j = "2.26.1"
netty = "4.2.16.Final"
netty = "4.2.18.Final"
[plugins]
fill = "io.papermc.fill.gradle:1.0.12"
shadow = "com.gradleup.shadow:9.6.1"
spotless = "com.diffplug.spotless:8.9.0"
spotless = "com.diffplug.spotless:8.10.2"
[libraries]
adventure-bom = "net.kyori:adventure-bom:5.2.0"
@@ -19,8 +19,8 @@ auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.1
brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
bstats = "org.bstats:bstats-base:3.2.1"
caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.4"
checker-qual = "org.checkerframework:checker-qual:4.2.1"
checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3"
checker-qual = "org.checkerframework:checker-qual:4.2.3"
checkstyle = "com.puppycrawl.tools:checkstyle:14.1.0"
completablefutures = "com.spotify:completable-futures:0.3.6"
configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" }
configurate3-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate3" }
@@ -32,12 +32,12 @@ disruptor = "com.lmax:disruptor:4.0.0"
fastutil = "it.unimi.dsi:fastutil:8.5.19"
flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" }
flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" }
jline = "org.jline:jline-terminal-ffm:4.3.1"
jline = "org.jline:jline-terminal-jni:4.3.1"
jopt = "net.sf.jopt-simple:jopt-simple:5.0.4"
junit = "org.junit.jupiter:junit-jupiter:6.1.2"
junit = "org.junit.jupiter:junit-jupiter:6.1.3"
jspecify = "org.jspecify:jspecify:1.0.1"
kyori-ansi = "net.kyori:ansi:1.1.1"
guava = "com.google.guava:guava:33.6.0-jre"
guava = "com.google.guava:guava:33.7.1-jre"
gson = "com.google.code.gson:gson:2.14.0"
guice = "com.google.inject:guice:7.0.0"
lmbda = "org.lanternpowered:lmbda:3.0.0"
@@ -55,9 +55,9 @@ netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll
netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" }
netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" }
nightconfig = "com.electronwill.night-config:toml:3.9.0"
slf4j = "org.slf4j:slf4j-api:2.0.18"
snakeyaml = "org.yaml:snakeyaml:2.6"
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.10.3"
slf4j = "org.slf4j:slf4j-api:2.0.19"
snakeyaml = "org.yaml:snakeyaml:2.7"
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.10.4"
terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0"
[bundles]
@@ -54,8 +54,8 @@ public class MoreByteBufUtils {
BufferPreference preferred = nativeStuff.preferredBufferType();
return switch (preferred) {
case DIRECT_PREFERRED, HEAP_PREFERRED ->
// The native prefers this type, but doesn't strictly require we provide it.
true;
// The native prefers this type, but doesn't strictly require we provide it.
true;
case DIRECT_REQUIRED -> buf.hasMemoryAddress();
case HEAP_REQUIRED -> buf.hasArray();
};
@@ -488,23 +488,46 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
return false;
}
// Re-register servers. If a server is being replaced, make sure to note what players need to
// move back to a fallback server.
// Re-register servers. If a server is being replaced or removed, make sure to note what
// players need to move back to a fallback server.
Collection<ConnectedPlayer> evacuate = new ArrayList<>();
for (Map.Entry<String, String> entry : configuration.getServers().entrySet()) {
if (newConfiguration.getServers().keySet().stream()
.anyMatch(name -> name.equalsIgnoreCase(entry.getKey()))) {
continue;
}
RegisteredServer rs = servers.getServer(entry.getKey()).orElse(null);
if (rs == null) {
continue;
}
for (Player player : rs.getPlayersConnected()) {
if (!(player instanceof ConnectedPlayer)) {
throw new IllegalStateException("Expected ConnectedPlayer for player " + player
+ " on server " + rs.getServerInfo().getName() + ", got "
+ player.getClass().getName());
}
evacuate.add((ConnectedPlayer) player);
}
servers.unregister(rs.getServerInfo());
}
for (Map.Entry<String, String> entry : newConfiguration.getServers().entrySet()) {
ServerInfo newInfo = new ServerInfo(entry.getKey(), AddressUtil.parseAddress(entry.getValue()));
Optional<RegisteredServer> rs = servers.getServer(entry.getKey());
if (rs.isEmpty()) {
RegisteredServer rs = servers.getServer(entry.getKey()).orElse(null);
if (rs == null) {
servers.register(newInfo);
} else if (!rs.get().getServerInfo().equals(newInfo)) {
for (Player player : rs.get().getPlayersConnected()) {
} else if (!rs.getServerInfo().equals(newInfo)) {
for (Player player : rs.getPlayersConnected()) {
if (!(player instanceof ConnectedPlayer)) {
throw new IllegalStateException("ConnectedPlayer not found for player " + player
+ " in server " + rs.get().getServerInfo().getName());
throw new IllegalStateException("Expected ConnectedPlayer for player " + player
+ " on server " + rs.getServerInfo().getName() + ", got "
+ player.getClass().getName());
}
evacuate.add((ConnectedPlayer) player);
}
servers.unregister(rs.get().getServerInfo());
servers.unregister(rs.getServerInfo());
servers.register(newInfo);
}
}
@@ -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
*/
@@ -16,13 +16,49 @@
*/
package com.velocitypowered.proxy.config;
import com.electronwill.nightconfig.core.Config;
/**
* Supported passthrough modes for ping passthrough.
* Object to contain all the things that can be toggled for ping passthrough.
*
* @param version Whether the version should be passed through.
* @param players Whether the player count should be passed through.
* @param description Whether the description should be passed through.
* @param favicon Whether the favicon should be passed through.
* @param modinfo Whether the modinfo should be passed through.
*/
public enum PingPassthroughMode {
DISABLED,
MODS,
DESCRIPTION,
ALL
public record PingPassthroughMode(boolean version, boolean players,
boolean description, boolean favicon, boolean modinfo) {
public static final PingPassthroughMode DEFAULT = new PingPassthroughMode();
/**
* Creates a default PingPassthroughMode.
*/
private PingPassthroughMode() {
this(false, false, false, false, false);
}
/**
* Returns a PingPassthroughMode from a config section, or the default if the section is null.
* Based on the code for PacketLimiterConfig.
*
* @param config The configuration object to parse.
* @return The PingPassthroughMode, or the default if {@code config} is null.
*/
public static PingPassthroughMode fromConfig(Config config) {
if (config == null) {
return DEFAULT;
}
return new PingPassthroughMode(
config.getOrElse("version", DEFAULT.version()),
config.getOrElse("players", DEFAULT.players()),
config.getOrElse("description", DEFAULT.description()),
config.getOrElse("favicon", DEFAULT.favicon()),
config.getOrElse("modinfo", DEFAULT.modinfo()));
}
public boolean enabled() {
return this.version || this.players || this.description || this.favicon
|| this.modinfo;
}
}
@@ -32,6 +32,7 @@ import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration;
import com.velocitypowered.proxy.config.migration.MiniMessageTranslationsMigration;
import com.velocitypowered.proxy.config.migration.MotdMigration;
import com.velocitypowered.proxy.config.migration.PacketLimiterMigration;
import com.velocitypowered.proxy.config.migration.PingPassthroughMigration;
import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration;
import com.velocitypowered.proxy.util.AddressUtil;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -79,7 +80,7 @@ public class VelocityConfiguration implements ProxyConfig {
@Expose
private boolean onlineModeKickExistingPlayers = false;
@Expose
private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED;
private PingPassthroughMode pingPassthrough = PingPassthroughMode.DEFAULT;
@Expose
private boolean samplePlayersInPing = false;
private final Servers servers;
@@ -513,7 +514,8 @@ public class VelocityConfiguration implements ProxyConfig {
new MotdMigration(),
new MiniMessageTranslationsMigration(),
new TransferIntegrationMigration(),
new PacketLimiterMigration()
new PacketLimiterMigration(),
new PingPassthroughMigration(),
};
for (final ConfigurationMigration migration : migrations) {
@@ -555,9 +557,7 @@ public class VelocityConfiguration implements ProxyConfig {
final CommentedConfig metricsConfig = config.get("metrics");
final PlayerInfoForwarding forwardingMode = config.getEnumOrElse(
"player-info-forwarding-mode", PlayerInfoForwarding.NONE);
final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough",
PingPassthroughMode.DISABLED);
final PingPassthroughMode pingPassthrough = PingPassthroughMode.fromConfig(config.get("ping-passthrough"));
final boolean samplePlayersInPing = config.getOrElse("sample-players-in-ping", false);
final String bind = config.getOrElse("bind", "0.0.0.0:25565");
@@ -590,7 +590,7 @@ public class VelocityConfiguration implements ProxyConfig {
forwardingMode,
forwardingSecret,
kickExisting,
pingPassthroughMode,
pingPassthrough,
samplePlayersInPing,
enablePlayerAddressLogging,
new Servers(serversConfig),
@@ -30,7 +30,8 @@ public sealed interface ConfigurationMigration
MotdMigration,
MiniMessageTranslationsMigration,
TransferIntegrationMigration,
PacketLimiterMigration {
PacketLimiterMigration,
PingPassthroughMigration {
boolean shouldMigrate(CommentedFileConfig config);
void migrate(CommentedFileConfig config, Logger logger) throws IOException;
@@ -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.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundPostEffectsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
@@ -404,4 +405,8 @@ public interface MinecraftSessionHandler {
default boolean handle(ClientboundStopSoundPacket packet) {
return false;
}
default boolean handle(ClientboundPostEffectsPacket packet) {
return false;
}
}
@@ -57,6 +57,7 @@ import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.JoinGamePacket;
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket;
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
@@ -166,6 +167,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return false; // forwards on
}
@Override
public boolean handle(JoinGamePacket packet) {
// We receive this if the connection is reconfigured
packet.setOnlineMode(serverConn.getPlayer().isOnlineMode());
return false; // forward
}
@Override
public boolean handle(ClientSettingsPacket packet) {
serverConn.ensureConnected().write(packet);
@@ -135,6 +135,7 @@ import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
import net.kyori.adventure.text.minimessage.translation.Argument;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.kyori.adventure.title.Title;
import net.kyori.adventure.title.Title.Times;
import net.kyori.adventure.title.TitlePart;
import net.kyori.adventure.translation.GlobalTranslator;
@@ -486,11 +487,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
}
@Override
public void showTitle(net.kyori.adventure.title.@NonNull Title title) {
public void showTitle(@NonNull Title title) {
if (this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
GenericTitlePacket timesPkt = GenericTitlePacket.constructTitlePacket(
GenericTitlePacket.ActionType.SET_TIMES, this.getProtocolVersion());
net.kyori.adventure.title.Title.Times times = title.times();
Title.Times times = title.times();
if (times != null) {
timesPkt.setFadeIn((int) DurationUtils.toTicks(times.fadeIn()));
timesPkt.setStay((int) DurationUtils.toTicks(times.stay()));
@@ -37,7 +37,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Modern (Minecraft 1.20.3+) ResourcePackHandler
* Modern (Minecraft 1.20.3+) ResourcePackHandler.
*/
public final class ModernResourcePackHandler extends ResourcePackHandler {
private final ListMultimap<UUID, ResourcePackInfo> outstandingResourcePacks =
@@ -118,6 +118,7 @@ public abstract sealed class ResourcePackHandler
/**
* Processes a client response to a sent resource-pack.
*
* <p>Cases in which no action will be taken:</p>
* <ul>
*
@@ -23,6 +23,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.server.PingOptions;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerPing;
import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.api.util.ModInfo;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.PingPassthroughMode;
@@ -100,60 +101,68 @@ public class ServerListPingHandler {
CompletableFuture<List<ServerPing>> pingResponses = CompletableFutures.successfulAsList(pings,
(ex) -> fallback);
return switch (mode) {
case ALL -> pingResponses.thenApply(responses -> {
// Find the first non-fallback
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
if (response.getDescriptionComponent() == null) {
return response.asBuilder()
.description(Component.empty())
.build();
}
// Return early if ping passthrough is not enabled
if (!mode.enabled()) {
return CompletableFuture.completedFuture(fallback);
}
return response;
return pingResponses.thenApply(responses -> {
// Find the first non-fallback
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
return fallback;
});
case MODS -> pingResponses.thenApply(responses -> {
// Find the first non-fallback that contains a mod list
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
Optional<ModInfo> modInfo = response.getModinfo();
if (modInfo.isPresent()) {
return fallback.asBuilder().mods(modInfo.get()).build();
}
}
return fallback;
});
case DESCRIPTION -> pingResponses.thenApply(responses -> {
// Find the first non-fallback. If it includes a modlist, add it too.
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
if (response.getDescriptionComponent() == null) {
continue;
}
return new ServerPing(
fallback.getVersion(),
fallback.getPlayers().orElse(null),
response.getDescriptionComponent(),
fallback.getFavicon().orElse(null),
response.getModinfo().orElse(null)
);
ServerPing.Version version;
if (mode.version()) {
version = response.getVersion();
} else {
version = fallback.getVersion();
}
return fallback;
});
// Not possible, but covered for completeness.
default -> CompletableFuture.completedFuture(fallback);
};
ServerPing.Players players;
if (mode.players()) {
players = response.getPlayers().orElse(null);
} else {
players = fallback.getPlayers().orElse(null);
}
Component description;
if (mode.description()) {
if (response.getDescriptionComponent() != null) {
description = response.getDescriptionComponent();
} else {
description = Component.empty();
}
} else {
description = fallback.getDescriptionComponent();
}
Favicon favicon;
if (mode.favicon()) {
favicon = response.getFavicon().orElse(null);
} else {
favicon = fallback.getFavicon().orElse(null);
}
ModInfo modinfo;
if (mode.modinfo()) {
modinfo = response.getModinfo().orElse(null);
} else {
modinfo = fallback.getModinfo().orElse(null);
}
return new ServerPing(
version,
players,
description,
favicon,
modinfo
);
}
return fallback;
});
}
/**
@@ -168,7 +177,7 @@ public class ServerListPingHandler {
? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION;
PingPassthroughMode passthroughMode = configuration.getPingPassthrough();
if (passthroughMode == PingPassthroughMode.DISABLED) {
if (!passthroughMode.enabled()) {
return CompletableFuture.completedFuture(constructLocalPing(shownVersion));
} else {
String virtualHostStr = connection.getVirtualHost().map(InetSocketAddress::getHostString)
@@ -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_EXECUTING = 1;
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(
final HandlerRegistration registration, final Throwable t) {
final PluginDescription pluginDescription = registration.plugin.getDescription();
@@ -143,6 +143,7 @@ public enum ProtocolUtils {
.build();
public static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB
private static final int DEFAULT_MAX_KEY_ARRAY_SIZE = 1024;
private static final int MAXIMUM_VARINT_SIZE = 5;
private static final BinaryTagType<? extends BinaryTag>[] BINARY_TAG_TYPES = new BinaryTagType[] {
BinaryTagTypes.END, BinaryTagTypes.BYTE, BinaryTagTypes.SHORT, BinaryTagTypes.INT,
@@ -349,15 +350,20 @@ public enum ProtocolUtils {
writeString(buf, key.asMinimalString());
}
public static Key[] readKeyArray(ByteBuf buf) {
return readKeyArray(buf, DEFAULT_MAX_KEY_ARRAY_SIZE);
}
/**
* Reads a standard Mojang Text namespaced:key array from the buffer.
*
* @param buf the buffer to read from
* @return the decoded key array
*/
public static Key[] readKeyArray(ByteBuf buf) {
public static Key[] readKeyArray(ByteBuf buf, int cap) {
int length = readVarInt(buf);
checkFrame(length >= 0, "Got a negative-length array (%s)", length);
checkFrame(length <= cap, "Bad key array size (got %s, maximum is %s)", length, cap);
checkFrame(buf.isReadable(length),
"Trying to read an array that is too long (wanted %s, only have %s)", length,
buf.readableBytes());
@@ -644,7 +650,7 @@ public enum ProtocolUtils {
private static final int FORGE_MAX_ARRAY_LENGTH = Integer.MAX_VALUE & 0x1FFF9A;
/**
* Reads an byte array for legacy version 1.7 from the specified {@code buf}
* Reads an byte array for legacy version 1.7 from the specified {@code buf}.
*
* @param buf the buffer to read from
* @return the read byte array
@@ -685,7 +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 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 buf buf
@@ -25,49 +25,7 @@ import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction.SERVERB
import com.velocitypowered.api.network.ProtocolState;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.protocol.packet.AvailableCommandsPacket;
import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DialogClearPacket;
import com.velocitypowered.proxy.protocol.packet.DialogShowPacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket;
import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket;
import com.velocitypowered.proxy.protocol.packet.HandshakePacket;
import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket;
import com.velocitypowered.proxy.protocol.packet.JoinGamePacket;
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket;
import com.velocitypowered.proxy.protocol.packet.LoginAcknowledgedPacket;
import com.velocitypowered.proxy.protocol.packet.LoginPluginMessagePacket;
import com.velocitypowered.proxy.protocol.packet.LoginPluginResponsePacket;
import com.velocitypowered.proxy.protocol.packet.PingIdentifyPacket;
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket;
import com.velocitypowered.proxy.protocol.packet.RemoveResourcePackPacket;
import com.velocitypowered.proxy.protocol.packet.ResourcePackRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
import com.velocitypowered.proxy.protocol.packet.RespawnPacket;
import com.velocitypowered.proxy.protocol.packet.ServerDataPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
import com.velocitypowered.proxy.protocol.packet.StatusResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
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.*;
import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket;
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket;
import com.velocitypowered.proxy.protocol.packet.chat.SystemChatPacket;
@@ -205,31 +163,44 @@ public enum StateRegistry {
map(0x06, MINECRAFT_1_20_2, false),
map(0x07, MINECRAFT_1_20_3, false),
map(0x09, MINECRAFT_1_20_5, false));
clientbound.register(
ClientboundPostEffectsPacket.class, ClientboundPostEffectsPacket::new,
map(0x0A, MINECRAFT_26_3, false));
clientbound.register(
ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new,
map(0x0A, MINECRAFT_1_20_5, false));
map(0x0A, MINECRAFT_1_20_5, false),
map(0x0B, MINECRAFT_26_3, false));
clientbound.register(TransferPacket.class, TransferPacket::new,
map(0x0B, MINECRAFT_1_20_5, false));
map(0x0B, MINECRAFT_1_20_5, false),
map(0x0C, MINECRAFT_26_3, false));
clientbound.register(ActiveFeaturesPacket.class, ActiveFeaturesPacket::new,
map(0x07, MINECRAFT_1_20_2, false),
map(0x08, MINECRAFT_1_20_3, false),
map(0x0C, MINECRAFT_1_20_5, false));
map(0x0C, MINECRAFT_1_20_5, false),
map(0x0D, MINECRAFT_26_3, false));
clientbound.register(TagsUpdatePacket.class, TagsUpdatePacket::new,
map(0x08, MINECRAFT_1_20_2, false),
map(0x09, MINECRAFT_1_20_3, false),
map(0x0D, MINECRAFT_1_20_5, false));
map(0x0D, MINECRAFT_1_20_5, false),
map(0x0E, MINECRAFT_26_3, false));
clientbound.register(KnownPacksPacket.class, KnownPacksPacket::new,
map(0x0E, MINECRAFT_1_20_5, false));
map(0x0E, MINECRAFT_1_20_5, false),
map(0x0F, MINECRAFT_26_3, false));
clientbound.register(ClientboundCustomReportDetailsPacket.class, ClientboundCustomReportDetailsPacket::new,
map(0x0F, MINECRAFT_1_21, false));
map(0x0F, MINECRAFT_1_21, false),
map(0x10, MINECRAFT_26_3, false));
clientbound.register(ClientboundServerLinksPacket.class, ClientboundServerLinksPacket::new,
map(0x10, MINECRAFT_1_21, false));
map(0x10, MINECRAFT_1_21, false),
map(0x11, MINECRAFT_26_3, false));
clientbound.register(DialogClearPacket.class, () -> DialogClearPacket.INSTANCE,
map(0x11, MINECRAFT_1_21_6, false));
map(0x11, MINECRAFT_1_21_6, false),
map(0x12, MINECRAFT_26_3, false));
clientbound.register(DialogShowPacket.class, () -> new DialogShowPacket(this),
map(0x12, MINECRAFT_1_21_6, false));
map(0x12, MINECRAFT_1_21_6, false),
map(0x13, MINECRAFT_26_3, false));
clientbound.register(CodeOfConductPacket.class, CodeOfConductPacket::new,
map(0x13, MINECRAFT_1_21_9, false));
map(0x13, MINECRAFT_1_21_9, false),
map(0x14, MINECRAFT_26_3, false));
}
},
PLAY {
@@ -381,7 +352,8 @@ public enum StateRegistry {
map(0x2D, MINECRAFT_1_21_2, false),
map(0x2F, MINECRAFT_1_21_4, false),
map(0x30, MINECRAFT_1_21_6, false),
map(0x31, MINECRAFT_26_1, false));
map(0x31, MINECRAFT_26_1, false),
map(0x32, MINECRAFT_26_3, false));
serverbound.register(
FinishedUpdatePacket.class, () -> FinishedUpdatePacket.INSTANCE,
map(0x0B, MINECRAFT_1_20_2, false),
@@ -440,6 +412,9 @@ public enum StateRegistry {
ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new,
map(0x16, MINECRAFT_1_20_5, false),
map(0x15, MINECRAFT_1_21_5, false));
clientbound.register(
ClientboundPostEffectsPacket.class, ClientboundPostEffectsPacket::new,
map(0x53, MINECRAFT_26_3, false));
clientbound.register(
ClientboundSoundEntityPacket.class, ClientboundSoundEntityPacket::new,
map(0x5D, MINECRAFT_1_19_3, true),
@@ -450,7 +425,8 @@ public enum StateRegistry {
map(0x6E, MINECRAFT_1_21_2, true),
map(0x6D, MINECRAFT_1_21_5, true),
map(0x72, MINECRAFT_1_21_9, true),
map(0x74, MINECRAFT_26_1, true));
map(0x74, MINECRAFT_26_1, true),
map(0x76, MINECRAFT_26_3, true));
clientbound.register(
ClientboundStopSoundPacket.class, ClientboundStopSoundPacket::new,
map(0x5F, MINECRAFT_1_19_3, true),
@@ -461,7 +437,8 @@ public enum StateRegistry {
map(0x71, MINECRAFT_1_21_2, true),
map(0x70, MINECRAFT_1_21_5, true),
map(0x75, MINECRAFT_1_21_9, true),
map(0x77, MINECRAFT_26_1, true));
map(0x77, MINECRAFT_26_1, true),
map(0x79, MINECRAFT_26_3, true));
clientbound.register(
PluginMessagePacket.class,
PluginMessagePacket::new,
@@ -519,7 +496,8 @@ public enum StateRegistry {
map(0x27, MINECRAFT_1_21_2, false),
map(0x26, MINECRAFT_1_21_5, false),
map(0x2B, MINECRAFT_1_21_9, false),
map(0x2C, MINECRAFT_26_1, false));
map(0x2C, MINECRAFT_26_1, false),
map(0x2D, MINECRAFT_26_3, false));
clientbound.register(
JoinGamePacket.class,
JoinGamePacket::new,
@@ -540,7 +518,8 @@ public enum StateRegistry {
map(0x2C, MINECRAFT_1_21_2, false),
map(0x2B, MINECRAFT_1_21_5, false),
map(0x30, MINECRAFT_1_21_9, false),
map(0x31, MINECRAFT_26_1, false));
map(0x31, MINECRAFT_26_1, false),
map(0x32, MINECRAFT_26_3, false));
clientbound.register(
RespawnPacket.class,
RespawnPacket::new,
@@ -564,7 +543,8 @@ public enum StateRegistry {
map(0x4C, MINECRAFT_1_21_2, true),
map(0x4B, MINECRAFT_1_21_5, true),
map(0x50, MINECRAFT_1_21_9, true),
map(0x52, MINECRAFT_26_1, true));
map(0x52, MINECRAFT_26_1, true),
map(0x54, MINECRAFT_26_3, true));
clientbound.register(
RemoveResourcePackPacket.class,
RemoveResourcePackPacket::new,
@@ -573,7 +553,8 @@ public enum StateRegistry {
map(0x4A, MINECRAFT_1_21_2, false),
map(0x49, MINECRAFT_1_21_5, false),
map(0x4E, MINECRAFT_1_21_9, false),
map(0x50, MINECRAFT_26_1, false));
map(0x50, MINECRAFT_26_1, false),
map(0x51, MINECRAFT_26_3, false));
clientbound.register(
ResourcePackRequestPacket.class,
ResourcePackRequestPacket::new,
@@ -597,7 +578,8 @@ public enum StateRegistry {
map(0x4B, MINECRAFT_1_21_2, false),
map(0x4A, MINECRAFT_1_21_5, false),
map(0x4F, MINECRAFT_1_21_9, false),
map(0x51, MINECRAFT_26_1, false));
map(0x51, MINECRAFT_26_1, false),
map(0x52, MINECRAFT_26_3, false));
clientbound.register(
HeaderAndFooterPacket.class,
HeaderAndFooterPacket::new,
@@ -622,7 +604,8 @@ public enum StateRegistry {
map(0x74, MINECRAFT_1_21_2, true),
map(0x73, MINECRAFT_1_21_5, true),
map(0x78, MINECRAFT_1_21_9, true),
map(0x7A, MINECRAFT_26_1, true));
map(0x7A, MINECRAFT_26_1, true),
map(0x7D, MINECRAFT_26_3, true));
clientbound.register(
LegacyTitlePacket.class,
LegacyTitlePacket::new,
@@ -646,7 +629,8 @@ public enum StateRegistry {
map(0x6A, MINECRAFT_1_21_2, true),
map(0x69, MINECRAFT_1_21_5, true),
map(0x6E, MINECRAFT_1_21_9, true),
map(0x70, MINECRAFT_26_1, true));
map(0x70, MINECRAFT_26_1, true),
map(0x72, MINECRAFT_26_3, true));
clientbound.register(
TitleTextPacket.class,
TitleTextPacket::new,
@@ -661,7 +645,8 @@ public enum StateRegistry {
map(0x6C, MINECRAFT_1_21_2, true),
map(0x6B, MINECRAFT_1_21_5, true),
map(0x70, MINECRAFT_1_21_9, true),
map(0x72, MINECRAFT_26_1, true));
map(0x72, MINECRAFT_26_1, true),
map(0x74, MINECRAFT_26_3, true));
clientbound.register(
TitleActionbarPacket.class,
TitleActionbarPacket::new,
@@ -676,7 +661,8 @@ public enum StateRegistry {
map(0x51, MINECRAFT_1_21_2, true),
map(0x50, MINECRAFT_1_21_5, true),
map(0x55, MINECRAFT_1_21_9, true),
map(0x57, MINECRAFT_26_1, true));
map(0x57, MINECRAFT_26_1, true),
map(0x59, MINECRAFT_26_3, true));
clientbound.register(
TitleTimesPacket.class,
TitleTimesPacket::new,
@@ -691,7 +677,8 @@ public enum StateRegistry {
map(0x6D, MINECRAFT_1_21_2, true),
map(0x6C, MINECRAFT_1_21_5, true),
map(0x71, MINECRAFT_1_21_9, true),
map(0x73, MINECRAFT_26_1, true));
map(0x73, MINECRAFT_26_1, true),
map(0x75, MINECRAFT_26_3, true));
clientbound.register(
TitleClearPacket.class,
TitleClearPacket::new,
@@ -723,7 +710,8 @@ public enum StateRegistry {
map(0x3F, MINECRAFT_1_21_2, false),
map(0x3E, MINECRAFT_1_21_5, false),
map(0x43, MINECRAFT_1_21_9, false),
map(0x45, MINECRAFT_26_1, false));
map(0x45, MINECRAFT_26_1, false),
map(0x46, MINECRAFT_26_3, false));
clientbound.register(
UpsertPlayerInfoPacket.class,
UpsertPlayerInfoPacket::new,
@@ -734,14 +722,16 @@ public enum StateRegistry {
map(0x40, MINECRAFT_1_21_2, false),
map(0x3F, MINECRAFT_1_21_5, false),
map(0x44, MINECRAFT_1_21_9, false),
map(0x46, MINECRAFT_26_1, false));
map(0x46, MINECRAFT_26_1, false),
map(0x47, MINECRAFT_26_3, false));
clientbound.register(
ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new,
map(0x6B, MINECRAFT_1_20_5, false),
map(0x72, MINECRAFT_1_21_2, false),
map(0x71, MINECRAFT_1_21_5, false),
map(0x76, MINECRAFT_1_21_9, false),
map(0x78, MINECRAFT_26_1, false));
map(0x78, MINECRAFT_26_1, false),
map(0x7A, MINECRAFT_26_3, false));
clientbound.register(
SystemChatPacket.class,
SystemChatPacket::new,
@@ -755,7 +745,8 @@ public enum StateRegistry {
map(0x73, MINECRAFT_1_21_2, true),
map(0x72, MINECRAFT_1_21_5, true),
map(0x77, MINECRAFT_1_21_9, true),
map(0x79, MINECRAFT_26_1, true));
map(0x79, MINECRAFT_26_1, true),
map(0x7C, MINECRAFT_26_3, true));
clientbound.register(
PlayerChatCompletionPacket.class,
PlayerChatCompletionPacket::new,
@@ -778,7 +769,8 @@ public enum StateRegistry {
map(0x50, MINECRAFT_1_21_2, false),
map(0x4F, MINECRAFT_1_21_5, false),
map(0x54, MINECRAFT_1_21_9, false),
map(0x56, MINECRAFT_26_1, false));
map(0x56, MINECRAFT_26_1, false),
map(0x58, MINECRAFT_26_3, false));
clientbound.register(
StartUpdatePacket.class,
() -> StartUpdatePacket.INSTANCE,
@@ -788,7 +780,8 @@ public enum StateRegistry {
map(0x70, MINECRAFT_1_21_2, false),
map(0x6F, MINECRAFT_1_21_5, false),
map(0x74, MINECRAFT_1_21_9, false),
map(0x76, MINECRAFT_26_1, false));
map(0x76, MINECRAFT_26_1, false),
map(0x78, MINECRAFT_26_3, false));
clientbound.register(
BundleDelimiterPacket.class,
() -> BundleDelimiterPacket.INSTANCE,
@@ -799,21 +792,24 @@ public enum StateRegistry {
map(0x73, MINECRAFT_1_20_5, false),
map(0x7A, MINECRAFT_1_21_2, false),
map(0x7F, MINECRAFT_1_21_9, false),
map(0x81, MINECRAFT_26_1, false));
map(0x81, MINECRAFT_26_1, false),
map(0x84, MINECRAFT_26_3, false));
clientbound.register(
ClientboundCustomReportDetailsPacket.class,
ClientboundCustomReportDetailsPacket::new,
map(0x7A, MINECRAFT_1_21, false),
map(0x81, MINECRAFT_1_21_2, false),
map(0x86, MINECRAFT_1_21_9, false),
map(0x88, MINECRAFT_26_1, false));
map(0x88, MINECRAFT_26_1, false),
map(0x8B, MINECRAFT_26_3, false));
clientbound.register(
ClientboundServerLinksPacket.class,
ClientboundServerLinksPacket::new,
map(0x7B, MINECRAFT_1_21, false),
map(0x82, MINECRAFT_1_21_2, false),
map(0x87, MINECRAFT_1_21_9, false),
map(0x89, MINECRAFT_26_1, false));
map(0x89, MINECRAFT_26_1, false),
map(0x8C, MINECRAFT_26_3, false));
clientbound.register(
UpdateTeamsPacket.class,
UpdateTeamsPacket::new,
@@ -834,7 +830,8 @@ public enum StateRegistry {
map(0x6B, MINECRAFT_1_21_9, true),
map(0x63, MINECRAFT_1_21_11, true),
map(0x6D, MINECRAFT_26_1, true),
map(0x6D, MINECRAFT_26_2, true));
map(0x6D, MINECRAFT_26_2, true),
map(0x6F, MINECRAFT_26_3, true));
}
},
LOGIN {
@@ -213,7 +213,34 @@ public class ClientSettingsPacket implements MinecraftPacket {
@Override
public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
return 1 + 0 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1;
int minLength = 1 // locale
+ 1 // viewDistance
+ 1 // chatVisibility
+ 1; // chatColors
if (version.noGreaterThan(ProtocolVersion.MINECRAFT_1_7_6)) {
minLength += 1; // difficulty
}
minLength += 1; // skinParts
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_9)) {
minLength += 1; // mainHand
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_17)) {
minLength += 1; // textFilteringEnabled
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_18)) {
minLength += 1; // clientListingAllowed
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_21_2)) {
minLength += 1; // particleStatus
}
}
}
}
return minLength;
}
@Override
@@ -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 int entityId;
private short gamemode;
private int gamemode;
private int dimension;
private long partialHashedSeed; // 1.15+
private short difficulty;
@@ -47,7 +47,7 @@ public class JoinGamePacket implements MinecraftPacket {
private CompoundBinaryTag registry; // 1.16+
private DimensionInfo dimensionInfo; // 1.16+
private CompoundBinaryTag currentDimensionData; // 1.16.2+
private short previousGamemode; // 1.16+
private int previousGamemode; // 1.16+
private int simulationDistance; // 1.18+
private @Nullable Pair<String, Long> lastDeathPosition; // 1.19+
private int portalCooldown; // 1.20+
@@ -63,11 +63,11 @@ public class JoinGamePacket implements MinecraftPacket {
this.entityId = entityId;
}
public short getGamemode() {
public int getGamemode() {
return gamemode;
}
public void setGamemode(short gamemode) {
public void setGamemode(int gamemode) {
this.gamemode = gamemode;
}
@@ -131,11 +131,11 @@ public class JoinGamePacket implements MinecraftPacket {
this.dimensionInfo = dimensionInfo;
}
public short getPreviousGamemode() {
public int getPreviousGamemode() {
return previousGamemode;
}
public void setPreviousGamemode(short previousGamemode) {
public void setPreviousGamemode(int previousGamemode) {
this.previousGamemode = previousGamemode;
}
@@ -345,8 +345,13 @@ public class JoinGamePacket implements MinecraftPacket {
String levelName = ProtocolUtils.readString(buf);
this.partialHashedSeed = buf.readLong();
this.gamemode = buf.readByte();
this.previousGamemode = buf.readByte();
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) {
this.gamemode = ProtocolUtils.readVarInt(buf);
this.previousGamemode = ProtocolUtils.readVarInt(buf); // game mode + 1 or 0
} else {
this.gamemode = buf.readByte();
this.previousGamemode = buf.readByte(); // game mode or -1
}
boolean isDebug = buf.readBoolean();
boolean isFlat = buf.readBoolean();
@@ -498,8 +503,13 @@ public class JoinGamePacket implements MinecraftPacket {
ProtocolUtils.writeString(buf, dimensionInfo.getLevelName());
buf.writeLong(partialHashedSeed);
buf.writeByte(gamemode);
buf.writeByte(previousGamemode);
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) {
ProtocolUtils.writeVarInt(buf, this.gamemode);
ProtocolUtils.writeVarInt(buf, this.previousGamemode);
} else {
buf.writeByte(this.gamemode);
buf.writeByte(this.previousGamemode);
}
buf.writeBoolean(dimensionInfo.isDebugType());
buf.writeBoolean(dimensionInfo.isFlat());
@@ -33,11 +33,11 @@ public class RespawnPacket implements MinecraftPacket {
private int dimension;
private long partialHashedSeed;
private short difficulty;
private short gamemode;
private int gamemode;
private String levelType = "";
private byte dataToKeep; // 1.16+
private DimensionInfo dimensionInfo; // 1.16-1.16.1
private short previousGamemode; // 1.16+
private int previousGamemode; // 1.16+
private CompoundBinaryTag currentDimensionData; // 1.16.2+
private @Nullable Pair<String, Long> lastDeathPosition; // 1.19+
private int portalCooldown; // 1.20+
@@ -46,9 +46,9 @@ public class RespawnPacket implements MinecraftPacket {
public RespawnPacket() {
}
public RespawnPacket(int dimension, long partialHashedSeed, short difficulty, short gamemode,
public RespawnPacket(int dimension, long partialHashedSeed, short difficulty, int gamemode,
String levelType, byte dataToKeep, DimensionInfo dimensionInfo,
short previousGamemode, CompoundBinaryTag currentDimensionData,
int previousGamemode, CompoundBinaryTag currentDimensionData,
@Nullable Pair<String, Long> lastDeathPosition, int portalCooldown,
int seaLevel) {
this.dimension = dimension;
@@ -97,11 +97,11 @@ public class RespawnPacket implements MinecraftPacket {
this.difficulty = difficulty;
}
public short getGamemode() {
public int getGamemode() {
return gamemode;
}
public void setGamemode(short gamemode) {
public void setGamemode(int gamemode) {
this.gamemode = gamemode;
}
@@ -121,11 +121,11 @@ public class RespawnPacket implements MinecraftPacket {
this.dataToKeep = dataToKeep;
}
public short getPreviousGamemode() {
public int getPreviousGamemode() {
return previousGamemode;
}
public void setPreviousGamemode(short previousGamemode) {
public void setPreviousGamemode(int previousGamemode) {
this.previousGamemode = previousGamemode;
}
@@ -197,9 +197,17 @@ public class RespawnPacket implements MinecraftPacket {
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15)) {
this.partialHashedSeed = buf.readLong();
}
this.gamemode = buf.readByte();
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) {
this.gamemode = ProtocolUtils.readVarInt(buf);
} else {
this.gamemode = buf.readByte();
}
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_16)) {
this.previousGamemode = buf.readByte();
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) {
this.previousGamemode = ProtocolUtils.readVarInt(buf);
} else {
this.previousGamemode = buf.readByte();
}
boolean isDebug = buf.readBoolean();
boolean isFlat = buf.readBoolean();
this.dimensionInfo = new DimensionInfo(dimensionKey, levelName, isFlat, isDebug, version);
@@ -249,9 +257,17 @@ public class RespawnPacket implements MinecraftPacket {
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_15)) {
buf.writeLong(partialHashedSeed);
}
buf.writeByte(gamemode);
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) {
ProtocolUtils.writeVarInt(buf, this.gamemode);
} else {
buf.writeByte(this.gamemode);
}
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_16)) {
buf.writeByte(previousGamemode);
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_3)) {
ProtocolUtils.writeVarInt(buf, this.previousGamemode);
} else {
buf.writeByte(this.previousGamemode);
}
buf.writeBoolean(dimensionInfo.isDebugType());
buf.writeBoolean(dimensionInfo.isFlat());
if (version.lessThan(ProtocolVersion.MINECRAFT_1_19_3)) {
@@ -25,6 +25,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_5;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_5;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_6;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_3;
import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.id;
import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.mapSet;
import static com.velocitypowered.proxy.protocol.packet.brigadier.DoubleArgumentPropertySerializer.DOUBLE;
@@ -273,16 +274,21 @@ public class ArgumentPropertyRegistry {
empty(id("minecraft:heightmap", mapSet(MINECRAFT_1_21_6, 51), mapSet(MINECRAFT_1_21_5, 50), mapSet(MINECRAFT_1_20_3, 49),
mapSet(MINECRAFT_1_19_4, 47))); // 1.19.4
empty(id("minecraft:uuid", mapSet(MINECRAFT_1_21_6, 56), mapSet(MINECRAFT_1_21_5, 54),mapSet(MINECRAFT_1_20_5, 53), mapSet(MINECRAFT_1_20_3, 48),
mapSet(MINECRAFT_1_19_4, 48), mapSet(MINECRAFT_1_19, 47))); // added in 1.16
empty(id("minecraft:uuid", mapSet(MINECRAFT_26_3, 61), mapSet(MINECRAFT_1_21_6, 56), mapSet(MINECRAFT_1_21_5, 54), mapSet(MINECRAFT_1_20_5, 53),
mapSet(MINECRAFT_1_20_3, 48), mapSet(MINECRAFT_1_19_4, 48), mapSet(MINECRAFT_1_19, 47))); // added in 1.16
empty(id("minecraft:loot_table", mapSet(MINECRAFT_1_21_6, 52), mapSet(MINECRAFT_1_21_5, 51), mapSet(MINECRAFT_1_20_5, 50)));
empty(id("minecraft:loot_predicate", mapSet(MINECRAFT_1_21_6, 53), mapSet(MINECRAFT_1_21_5, 52), mapSet(MINECRAFT_1_20_5, 51)));
empty(id("minecraft:loot_modifier", mapSet(MINECRAFT_1_21_6, 54), mapSet(MINECRAFT_1_21_5, 53), mapSet(MINECRAFT_1_20_5, 52)));
empty(id("minecraft:hex_color", mapSet(MINECRAFT_1_21_6, 17))); // added in 1.21.6
empty(id("minecraft:dialog", mapSet(MINECRAFT_1_21_6, 55))); // added in 1.21.6
empty(id("minecraft:dialog", mapSet(MINECRAFT_26_3, 58), mapSet(MINECRAFT_1_21_6, 55))); // added in 1.21.6
empty(id("minecraft:team_color", mapSet(MINECRAFT_26_2, 16))); // renamed from color in 26.2
empty(id("minecraft:context_float_provider", mapSet(MINECRAFT_26_3, 55))); // added in 26.3
empty(id("minecraft:context_int_provider", mapSet(MINECRAFT_26_3, 56))); // added in 26.3
empty(id("minecraft:slot_source", mapSet(MINECRAFT_26_3, 57))); // added in 26.3
empty(id("minecraft:feature", mapSet(MINECRAFT_26_3, 59))); // added in 26.3
empty(id("minecraft:swing_animation", mapSet(MINECRAFT_26_3, 60))); // added in 26.3
// Crossstitch support
register(id("crossstitch:mod_argument", mapSet(MINECRAFT_1_19, -256)), ModArgumentProperty.class, MOD);
@@ -22,10 +22,12 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
public abstract class GenericTitlePacket implements MinecraftPacket {
public enum ActionType {
SET_TITLE(0),
SET_SUBTITLE(1),
SET_ACTION_BAR(2),
@@ -45,16 +47,7 @@ public abstract class GenericTitlePacket implements MinecraftPacket {
}
}
private ActionType action;
protected void setAction(ActionType action) {
this.action = action;
}
public final ActionType getAction() {
return action;
}
public abstract @NotNull ActionType getAction();
public ComponentHolder getComponent() {
throw new UnsupportedOperationException("Invalid function for this TitlePacket ActionType");
@@ -88,11 +81,9 @@ public abstract class GenericTitlePacket implements MinecraftPacket {
throw new UnsupportedOperationException("Invalid function for this TitlePacket ActionType");
}
@Override
public final void decode(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) {
throw new UnsupportedOperationException(); // encode only
public final void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
throw new UnsupportedOperationException("Decode is not implemented");
}
/**
@@ -103,21 +94,17 @@ public abstract class GenericTitlePacket implements MinecraftPacket {
* @return GenericTitlePacket instance that follows the invoker type/version
*/
public static GenericTitlePacket constructTitlePacket(ActionType type, ProtocolVersion version) {
GenericTitlePacket packet = null;
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_17)) {
packet = switch (type) {
return switch (type) {
case SET_ACTION_BAR -> new TitleActionbarPacket();
case SET_SUBTITLE -> new TitleSubtitlePacket();
case SET_TIMES -> new TitleTimesPacket();
case SET_TITLE -> new TitleTextPacket();
case HIDE, RESET -> new TitleClearPacket();
case HIDE, RESET -> new TitleClearPacket(type);
default -> throw new IllegalArgumentException("Invalid ActionType");
};
} else {
packet = new LegacyTitlePacket();
return new LegacyTitlePacket(type);
}
packet.setAction(type);
return packet;
}
}
@@ -17,32 +17,44 @@
package com.velocitypowered.proxy.protocol.packet.title;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class LegacyTitlePacket extends GenericTitlePacket {
private final ActionType action;
private @Nullable ComponentHolder component;
private int fadeIn;
private int stay;
private int fadeOut;
public LegacyTitlePacket() {
throw new UnsupportedOperationException("Decode is not implemented");
}
public LegacyTitlePacket(ActionType action) {
this.action = Preconditions.checkNotNull(action, "action");
}
@Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
if (version.lessThan(ProtocolVersion.MINECRAFT_1_11)
&& getAction() == ActionType.SET_ACTION_BAR) {
&& this.action == ActionType.SET_ACTION_BAR) {
throw new IllegalStateException("Action bars are only supported on 1.11 and newer");
}
ProtocolUtils.writeVarInt(buf, getAction().getAction(version));
ProtocolUtils.writeVarInt(buf, this.action.getAction(version));
switch (getAction()) {
switch (this.action) {
case SET_TITLE, SET_SUBTITLE, SET_ACTION_BAR -> {
if (component == null) {
throw new IllegalStateException("No component found for " + getAction());
throw new IllegalStateException("No component found for " + this.action);
}
component.write(buf);
}
@@ -52,14 +64,13 @@ public class LegacyTitlePacket extends GenericTitlePacket {
buf.writeInt(fadeOut);
}
case HIDE, RESET -> {}
default -> throw new UnsupportedOperationException("Unknown action " + getAction());
default -> throw new UnsupportedOperationException("Unknown action " + this.action);
}
}
@Override
public void setAction(ActionType action) {
super.setAction(action);
public @NotNull ActionType getAction() {
return action;
}
@Override
@@ -105,7 +116,7 @@ public class LegacyTitlePacket extends GenericTitlePacket {
@Override
public String toString() {
return "GenericTitlePacket{"
+ "action=" + getAction()
+ "action=" + action
+ ", component='" + component + '\''
+ ", fadeIn=" + fadeIn
+ ", stay=" + stay
@@ -22,13 +22,15 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
public class TitleActionbarPacket extends GenericTitlePacket {
private ComponentHolder component;
public TitleActionbarPacket() {
setAction(ActionType.SET_ACTION_BAR);
@Override
public @NotNull ActionType getAction() {
return ActionType.SET_ACTION_BAR;
}
@Override
@@ -21,30 +21,37 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
public class TitleClearPacket extends GenericTitlePacket {
private final ActionType action;
public TitleClearPacket() {
setAction(ActionType.HIDE);
this(ActionType.HIDE);
}
public TitleClearPacket(ActionType action) {
if (action != ActionType.HIDE && action != ActionType.RESET) {
throw new IllegalArgumentException("TitleClearPacket only accepts the HIDE and RESET actions.");
}
this.action = action;
}
@Override
public void setAction(ActionType action) {
if (action != ActionType.HIDE && action != ActionType.RESET) {
throw new IllegalArgumentException("TitleClearPacket only accepts CLEAR and RESET actions");
}
super.setAction(action);
public @NotNull ActionType getAction() {
return action;
}
@Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
buf.writeBoolean(getAction() == ActionType.RESET);
buf.writeBoolean(this.action == ActionType.RESET);
}
@Override
public String toString() {
return "TitleClearPacket{"
+ ", resetTimes=" + (getAction() == ActionType.RESET)
+ ", resetTimes=" + (this.action == ActionType.RESET)
+ '}';
}
@@ -22,13 +22,15 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
public class TitleSubtitlePacket extends GenericTitlePacket {
private ComponentHolder component;
public TitleSubtitlePacket() {
setAction(ActionType.SET_SUBTITLE);
@Override
public @NotNull ActionType getAction() {
return ActionType.SET_SUBTITLE;
}
@Override
@@ -22,13 +22,15 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
public class TitleTextPacket extends GenericTitlePacket {
private ComponentHolder component;
public TitleTextPacket() {
setAction(ActionType.SET_TITLE);
@Override
public @NotNull ActionType getAction() {
return ActionType.SET_TITLE;
}
@Override
@@ -21,6 +21,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
public class TitleTimesPacket extends GenericTitlePacket {
@@ -28,8 +29,9 @@ public class TitleTimesPacket extends GenericTitlePacket {
private int stay;
private int fadeOut;
public TitleTimesPacket() {
setAction(ActionType.SET_TIMES);
@Override
public @NotNull ActionType getAction() {
return ActionType.SET_TIMES;
}
@Override
@@ -30,40 +30,51 @@ package com.velocitypowered.proxy.util;
* <p>This class is not thread-safe. If multiple threads access an instance concurrently,
* external synchronization is required.</p>
*/
@SuppressWarnings("checkstyle:WhitespaceAfter") // Not our class
public final class IntervalledCounter {
private static final int INITIAL_SIZE = 8;
/**
* Data points within this many nanoseconds of the newest one are merged into it, bounding the
* number of stored data points to roughly {@code interval / COALESCE_INTERVAL}.
*/
private static final long COALESCE_INTERVAL = 1_000_000L; // 1ms
/**
* Ring buffer holding the timestamp (in nanoseconds) for each data point.
*/
protected long[] times;
private long[] times;
/**
* Ring buffer holding the count associated with each timestamp.
*/
protected long[] counts;
private long[] counts;
/**
* The sliding window size in nanoseconds. Only entries with time >= (currentTime - interval)
* are considered part of the window.
*/
protected final long interval;
private final long interval;
/**
* Cached lower bound of the window (in nanoseconds) after the last update.
*/
protected long minTime;
private long minTime;
/**
* Running sum of all counts currently within the window.
*/
protected long sum;
private long sum;
/**
* Head index (inclusive) of the ring buffer.
*/
protected int head; // inclusive
private int head; // inclusive
/**
* Tail index (exclusive) of the ring buffer.
*/
protected int tail; // exclusive
private int tail; // exclusive
/**
* Creates a new counter with the specified interval.
@@ -131,6 +142,8 @@ public final class IntervalledCounter {
/**
* Adds {@code count} units at the specified timestamp, assuming the timestamp is within the
* current window. If the timestamp is older than {@code minTime}, the value is ignored.
* If the timestamp is within {@link #COALESCE_INTERVAL} of the newest stored data point, the
* count is merged into that data point instead of creating a new one.
* This method does not automatically advance the window; callers should invoke
* {@link #updateCurrentTime()} or {@link #updateCurrentTime(long)} beforehand.
*
@@ -142,6 +155,15 @@ public final class IntervalledCounter {
if (currTime - this.minTime < 0) {
return;
}
if (this.head != this.tail) {
final int last = this.tail == 0 ? this.times.length - 1 : this.tail - 1;
// guard against overflow by using subtraction
if (currTime - this.times[last] < COALESCE_INTERVAL) {
this.counts[last] += count;
this.sum += count;
return;
}
}
int nextTail = (this.tail + 1) % this.times.length;
if (nextTail == this.head) {
this.resize();
@@ -219,7 +241,7 @@ public final class IntervalledCounter {
* @return the rate in units per second for the current window
*/
public double getRate() {
return (double)this.sum / ((double)this.interval * 1.0E-9);
return (double) this.sum / ((double) this.interval * 1.0E-9);
}
/**
+14 -17
View File
@@ -1,5 +1,5 @@
# Config version. Do not change this
config-version = "2.8"
config-version = "2.9"
# What port should the proxy be bound to? By default, we'll bind to all addresses on port 25565.
bind = "0.0.0.0:25565"
@@ -43,7 +43,7 @@ forwarding-secret-file = "forwarding.secret"
# Announce whether or not your server supports Forge. If you run a modded server, we
# suggest turning this on.
#
# If your network runs one modpack consistently, consider using ping-passthrough = "mods"
# If your network runs one modpack consistently, consider using ping-passthrough.modinfo = true
# instead for a nicer display in the server list.
announce-forge = false
@@ -51,21 +51,6 @@ announce-forge = false
# any existing player who is online if a duplicate connection attempt is made.
kick-existing-players = false
# Should Velocity pass server list ping requests to a backend server?
# Available options:
# - "disabled": No pass-through will be done. The velocity.toml and server-icon.png
# will determine the initial server list ping response.
# - "mods": Passes only the mod list from your backend server into the response.
# The first server in your try list (or forced host) with a mod list will be
# used. If no backend servers can be contacted, Velocity won't display any
# mod information.
# - "description": Uses the description and mod list from the backend server. The first
# server in the try (or forced host) list that responds is used for the
# description and mod list.
# - "all": Uses the backend server's response as the proxy response. The Velocity
# configuration is used if no servers could be contacted.
ping-passthrough = "DISABLED"
# If enabled (default is false), then a sample of the online players on the proxy will be visible
# when hovering over the player count in the server list.
# This doesn't have any effect when ping passthrough is set to either "description" or "all".
@@ -74,6 +59,18 @@ sample-players-in-ping = false
# If not enabled (default is true) player IP addresses will be replaced by <ip address withheld> in logs
enable-player-address-logging = true
[ping-passthrough]
# Should Velocity pass the version number from the backend server when responding to server list ping requests?
version = false
# Should Velocity pass the player count from the backend server when responding to server list ping requests?
players = false
# Should Velocity pass the description from the backend server when responding to server list ping requests?
description = false
# Should Velocity pass the favicon (also known as the server icon) from the backend server when responding to server list ping requests?
favicon = false
# Should Velocity pass the mod list from the backend server when responding to server list ping requests?
modinfo = false
[packet-limiter]
# Size of the moving time window in seconds used to calculate average rates.
# A larger window tolerates short bursts while still enforcing the configured limits over time.
@@ -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());
}
}