# Conflicts: # proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
[](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 + '\''
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -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.
|
||||
|
||||
+1
@@ -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>
|
||||
|
||||
+1
@@ -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>
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
+1
@@ -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
@@ -1,2 +1,2 @@
|
||||
group=com.velocitypowered
|
||||
version=4.1.0-SNAPSHOT
|
||||
version=4.2.1-SNAPSHOT
|
||||
|
||||
+10
-10
@@ -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),
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+105
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -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);
|
||||
|
||||
+3
-2
@@ -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()));
|
||||
|
||||
+1
-1
@@ -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 =
|
||||
|
||||
+1
@@ -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>
|
||||
*
|
||||
|
||||
+59
-50
@@ -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
|
||||
|
||||
@@ -66,7 +66,6 @@ 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.chat.ChatAcknowledgementPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket;
|
||||
@@ -205,31 +204,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 +393,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 +453,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 +466,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 +478,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 +537,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 +559,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 +584,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 +594,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 +619,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 +645,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 +670,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 +686,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 +702,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 +718,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 +751,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 +763,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 +786,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 +810,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 +821,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,20 +833,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(0x8C, MINECRAFT_26_3, false));
|
||||
map(0x89, MINECRAFT_26_1, false));
|
||||
clientbound.register(
|
||||
UpdateTeamsPacket.class,
|
||||
|
||||
+28
-1
@@ -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
|
||||
|
||||
+53
@@ -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)) {
|
||||
|
||||
+9
-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);
|
||||
|
||||
+8
-21
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+21
-10
@@ -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
|
||||
|
||||
+4
-2
@@ -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
|
||||
|
||||
+15
-8
@@ -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)
|
||||
+ '}';
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -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
|
||||
|
||||
+4
-2
@@ -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
|
||||
|
||||
+4
-2
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user