Compare commits

...
23 Commits
Author SHA1 Message Date
yoyosource 7a39ebb058 Fix StateRegistry and update to dev/4.0.0
SteamWarCI Build successful
2026-08-24 17:14:49 +02:00
yoyosource 57e9cd7f24 Merge remote-tracking branch 'upstream/dev/4.0.0'
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java
2026-08-24 17:03:57 +02:00
Wouter Gritter 4772ca3022 Bump lmbda to 3.0.0, defining generated handlers with the proxy's lookup instead of the plugin's
Reapplies the lmbda 3.0.0 bump reverted in f918d0d6.
2026-08-14 12:53:49 +02:00
YoyoNow b9463125d1 Merge pull request 'Update26' (#3) from update26 into master
SteamWarCI Build successful
Reviewed-on: #3
2026-08-13 09:23:04 +02:00
Shane Freeder f918d0d649 Downgrade lmbda back to 2.0.0
lmbda 3.x moved to using hidden classes for generation which does
not work for cross classloader operations which are somewhat typical
for plugins.
2026-08-12 23:39:19 +01:00
Wouter GritterandGitHub e6fbcc9196 Various dependency bumps (#1860)
* Bump fastutil to 8.5.19 and remove exclusions

* Various dependency bumps
2026-08-12 18:32:24 +01:00
Radmir NoirusovandGitHub 14a69904f9 fix: retain reference-counted packets forwarded via handleGeneric (#1856)
During configuration, a ServerboundCustomClickActionPacket arriving
when connectionInFlight is null falls through to handleGeneric, which
writes it to the connected backend without retaining. The encoder
releases the packet, then MinecraftConnection.channelRead's finally
block releases again - double-free.

Two fixes:
- handle() now uses getConnectionInFlightOrConnectedServer() so the
  packet is properly retained before being written
- handleGeneric() retains any ByteBufHolder packet before write, not
  just PluginMessagePacket

Closes #1841
2026-08-12 18:29:30 +01:00
Jason PenillaandGitHub 71c50a75eb Export JSpecify annotations at runtime (#1861)
JSpecify annotations have runtime retention, so expose them through the API variant as recommended by JSpecify.
2026-08-12 18:25:15 +01:00
Shane Freeder 00759e5279 Revert "Fix dimension reading for some mods that add extra dimensions in 1.7.10 (#1734)"
This reverts commit 2676520c6a.
2026-08-03 14:09:05 +01:00
Phillipp W.andGitHub 06ade4775e fix: forward player loaded packet to backend server (#1862)
Return false after handling ServerboundPlayerLoadedPacket so Velocity's normal forwarding path sends the packet to the backend server.
2026-08-03 12:22:58 +01:00
Wouter Gritterandxphorror 2676520c6a Fix dimension reading for some mods that add extra dimensions in 1.7.10 (#1734)
Rewrite the comment and ternary operation to be clearer

Co-authored-by: xphorror <87706197+xphorror@users.noreply.github.com>
2026-08-02 11:13:25 +02:00
e11584ba35 Player Loaded World API (#1541)
Co-authored-by: Emil <12966472+Emilxyz@users.noreply.github.com>
Co-authored-by: Wouter Gritter <wouter@gritter.nl>
2026-07-31 20:03:22 +01:00
Shane Freeder a08972749b Rebuild natives 2026-07-30 16:51:07 +01:00
c6e9ca989e Add provides API (#1853)
Add provides API

Co-authored-by: Shane Freeder <theboyetronic@gmail.com>
2026-07-29 12:51:34 +01:00
Shane Freeder d30f1d9a74 Compressor cleanups 2026-07-29 00:51:41 +01:00
EmilandGitHub b45716deff feat: Make version clickable in velocity info command (#1775) 2026-07-19 16:02:20 +02:00
SpigotRCEandGitHub e653647962 [ci skip] typo fix (#1778) 2026-07-19 16:02:12 +02:00
Shane Freeder 5aab0d1427 [ci skip] primative and functional
oh, my! This stuff is not ideal, but it's the only real protection here unless
we just hack the automation here
2026-07-15 04:28:22 +01:00
Shane Freeder 1cd8d51d02 [ci skip] primative failsafe for mismatched version family 2026-07-15 02:29:29 +01:00
Shane Freeder 9eb338bd1c Fix version family 2026-07-15 02:22:41 +01:00
Andrew Steinborn 60380211f8 Defer ByteBufUtil.getBytes() in config stages until after we check for channel registry 2026-07-14 18:04:07 -04:00
Shane Freeder 3b239daf4c [ci skip] back to snapshots
Hello, Darkness, my old friend
2026-07-14 15:26:19 +01:00
Shane Freeder 90f89053a7 Release 4.0.0 2026-07-14 15:10:04 +01:00
39 changed files with 621 additions and 181 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ java {
} }
dependencies { dependencies {
compileOnlyApi(libs.jspecify) api(libs.jspecify)
api(libs.gson) api(libs.gson)
api(libs.guava) api(libs.guava)
@@ -97,6 +97,16 @@ public class PluginAnnotationProcessor extends AbstractProcessor {
} }
} }
for (String provided : plugin.provides()) {
if (!SerializedPluginDescription.ID_PATTERN.matcher(provided).matches()) {
environment.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Invalid provided ID '" + provided + "' for plugin " + qualifiedName
+ ". IDs must start alphabetically, have lowercase alphanumeric characters, and "
+ "can contain dashes or underscores.");
return false;
}
}
// All good, generate the velocity-plugin.json. // All good, generate the velocity-plugin.json.
SerializedPluginDescription description = SerializedPluginDescription SerializedPluginDescription description = SerializedPluginDescription
.from(plugin, qualifiedName.toString()); .from(plugin, qualifiedName.toString());
@@ -35,11 +35,12 @@ public final class SerializedPluginDescription {
private final @Nullable String url; private final @Nullable String url;
private final @Nullable List<String> authors; private final @Nullable List<String> authors;
private final @Nullable List<Dependency> dependencies; private final @Nullable List<Dependency> dependencies;
private final @Nullable List<String> provides;
private final String main; private final String main;
private SerializedPluginDescription(String id, String name, String version, String description, private SerializedPluginDescription(String id, String name, String version, String description,
String url, String url,
List<String> authors, List<Dependency> dependencies, String main) { List<String> authors, List<Dependency> dependencies, List<String> provides, String main) {
Preconditions.checkNotNull(id, "id"); Preconditions.checkNotNull(id, "id");
Preconditions.checkArgument(ID_PATTERN.matcher(id).matches(), "id is not valid"); Preconditions.checkArgument(ID_PATTERN.matcher(id).matches(), "id is not valid");
this.id = id; this.id = id;
@@ -50,6 +51,7 @@ public final class SerializedPluginDescription {
this.authors = authors == null || authors.isEmpty() ? ImmutableList.of() : authors; this.authors = authors == null || authors.isEmpty() ? ImmutableList.of() : authors;
this.dependencies = this.dependencies =
dependencies == null || dependencies.isEmpty() ? ImmutableList.of() : dependencies; dependencies == null || dependencies.isEmpty() ? ImmutableList.of() : dependencies;
this.provides = provides == null || provides.isEmpty() ? ImmutableList.of() : provides;
this.main = Preconditions.checkNotNull(main, "main"); this.main = Preconditions.checkNotNull(main, "main");
} }
@@ -61,7 +63,9 @@ public final class SerializedPluginDescription {
return new SerializedPluginDescription(plugin.id(), plugin.name(), plugin.version(), return new SerializedPluginDescription(plugin.id(), plugin.name(), plugin.version(),
plugin.description(), plugin.url(), plugin.description(), plugin.url(),
Arrays.stream(plugin.authors()).filter(author -> !author.isEmpty()) Arrays.stream(plugin.authors()).filter(author -> !author.isEmpty())
.collect(Collectors.toList()), dependencies, qualifiedName); .collect(Collectors.toList()), dependencies,
Arrays.stream(plugin.provides()).filter(provided -> !provided.isEmpty())
.collect(Collectors.toList()), qualifiedName);
} }
public String getId() { public String getId() {
@@ -92,6 +96,10 @@ public final class SerializedPluginDescription {
return dependencies == null ? ImmutableList.of() : dependencies; return dependencies == null ? ImmutableList.of() : dependencies;
} }
public List<String> getProvides() {
return provides == null ? ImmutableList.of() : provides;
}
public String getMain() { public String getMain() {
return main; return main;
} }
@@ -112,12 +120,13 @@ public final class SerializedPluginDescription {
&& Objects.equals(url, that.url) && Objects.equals(url, that.url)
&& Objects.equals(authors, that.authors) && Objects.equals(authors, that.authors)
&& Objects.equals(dependencies, that.dependencies) && Objects.equals(dependencies, that.dependencies)
&& Objects.equals(provides, that.provides)
&& Objects.equals(main, that.main); && Objects.equals(main, that.main);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(id, name, version, description, url, authors, dependencies); return Objects.hash(id, name, version, description, url, authors, dependencies, provides);
} }
@Override @Override
@@ -130,6 +139,7 @@ public final class SerializedPluginDescription {
+ ", url='" + url + '\'' + ", url='" + url + '\''
+ ", authors=" + authors + ", authors=" + authors
+ ", dependencies=" + dependencies + ", dependencies=" + dependencies
+ ", provides=" + provides
+ ", main='" + main + '\'' + ", main='" + main + '\''
+ '}'; + '}';
} }
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2018-2026 Velocity Contributors
*
* The Velocity API is licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in the api top-level directory.
*/
package com.velocitypowered.api.event.player;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.Player;
/**
* Called when a player is marked as loaded by the client.
*
* <p>This event is fired once per {@link com.velocitypowered.api.proxy.ServerConnection}
* when the player explicitly notifies the server after loading the world (closing the downloading terrain screen)
*
* @implNote Unlike Paper this event will <u>not</u> fire due to a timeout nor respawning.
* Though plugins can implement a timeout by scheduling a task in {@link ServerPostConnectEvent}
* and checking {@link com.velocitypowered.api.proxy.ServerConnection#isClientLoaded()}.
* @sinceMinecraft 1.21.4
* @since 4.1.0
*/
@Beta
public final class PlayerClientLoadedWorldEvent {
private final Player player;
public PlayerClientLoadedWorldEvent(Player player) {
this.player = Preconditions.checkNotNull(player, "player");
}
public Player getPlayer() {
return player;
}
@Override
public String toString() {
return "PlayerClientLoadedWorldEvent{"
+ "player=" + player
+ '}';
}
}
@@ -72,4 +72,12 @@ public @interface Plugin {
* @return the plugin dependencies * @return the plugin dependencies
*/ */
Dependency[] dependencies() default {}; Dependency[] dependencies() default {};
/**
* The plugin IDs this plugin "provides" for. Each ID must match
* {@link SerializedPluginDescription#ID_PATTERN_STRING}.
*
* @return the provided IDs
*/
String[] provides() default {};
} }
@@ -100,6 +100,16 @@ public interface PluginDescription {
return Optional.empty(); return Optional.empty();
} }
/**
* Gets a {@link Collection} of the provided IDs of the {@link Plugin} within this container.
*
* @return the provided plugins IDs, can be empty
* @see Plugin#provides()
*/
default Collection<String> getProvidedIds() {
return ImmutableSet.of();
}
/** /**
* Returns the source the plugin was loaded from. * Returns the source the plugin was loaded from.
* *
@@ -7,6 +7,7 @@
package com.velocitypowered.api.proxy; package com.velocitypowered.api.proxy;
import com.google.common.annotations.Beta;
import com.velocitypowered.api.proxy.messages.ChannelMessageSink; import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
import com.velocitypowered.api.proxy.messages.ChannelMessageSource; import com.velocitypowered.api.proxy.messages.ChannelMessageSource;
import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.RegisteredServer;
@@ -40,6 +41,17 @@ public interface ServerConnection extends ChannelMessageSource, ChannelMessageSi
*/ */
ServerInfo getServerInfo(); ServerInfo getServerInfo();
/**
* Returns whether the client notified this connection of having loaded the world.
*
* @return true if the client has loaded the world
* @implNote This is purely client-dependent; see {@link com.velocitypowered.api.event.player.PlayerClientLoadedWorldEvent}.
* @sinceMinecraft 1.21.4
* @since 4.1.0
*/
@Beta
boolean isClientLoaded();
/** /**
* Returns the player that this connection is associated with. * Returns the player that this connection is associated with.
* *
+1 -1
View File
@@ -1,2 +1,2 @@
group=com.velocitypowered group=com.velocitypowered
version=4.0.0-SNAPSHOT version=4.1.0-SNAPSHOT
+16 -16
View File
@@ -2,24 +2,24 @@
configurate3 = "3.7.3" configurate3 = "3.7.3"
configurate4 = "4.2.0" configurate4 = "4.2.0"
flare = "2.0.1" flare = "2.0.1"
log4j = "2.26.0" log4j = "2.26.1"
netty = "4.2.16.Final" netty = "4.2.16.Final"
[plugins] [plugins]
fill = "io.papermc.fill.gradle:1.0.12" fill = "io.papermc.fill.gradle:1.0.12"
shadow = "com.gradleup.shadow:9.5.1" shadow = "com.gradleup.shadow:9.6.1"
spotless = "com.diffplug.spotless:8.2.0" spotless = "com.diffplug.spotless:8.9.0"
[libraries] [libraries]
adventure-bom = "net.kyori:adventure-bom:5.2.0" adventure-bom = "net.kyori:adventure-bom:5.2.0"
adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:5.2.0" adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:5.2.0"
asm = "org.ow2.asm:asm:9.9.1" asm = "org.ow2.asm:asm:9.10.1"
auto-service = "com.google.auto.service:auto-service:1.1.1" auto-service = "com.google.auto.service:auto-service:1.1.1"
auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.1.1" auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.1.1"
brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT" brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
bstats = "org.bstats:bstats-base:3.1.0" bstats = "org.bstats:bstats-base:3.2.1"
caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.3" caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.4"
checker-qual = "org.checkerframework:checker-qual:3.53.0" checker-qual = "org.checkerframework:checker-qual:4.2.1"
checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3" checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3"
completablefutures = "com.spotify:completable-futures:0.3.6" completablefutures = "com.spotify:completable-futures:0.3.6"
configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" } configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" }
@@ -29,24 +29,24 @@ configurate4-hocon = { module = "org.spongepowered:configurate-hocon", version.r
configurate4-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate4" } configurate4-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate4" }
configurate4-gson = { module = "org.spongepowered:configurate-gson", version.ref = "configurate4" } configurate4-gson = { module = "org.spongepowered:configurate-gson", version.ref = "configurate4" }
disruptor = "com.lmax:disruptor:4.0.0" disruptor = "com.lmax:disruptor:4.0.0"
fastutil = "it.unimi.dsi:fastutil:8.5.18" fastutil = "it.unimi.dsi:fastutil:8.5.19"
flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" } flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" }
flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" } flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" }
jline = "org.jline:jline-terminal-ffm:4.3.1" jline = "org.jline:jline-terminal-ffm:4.3.1"
jopt = "net.sf.jopt-simple:jopt-simple:5.0.4" jopt = "net.sf.jopt-simple:jopt-simple:5.0.4"
junit = "org.junit.jupiter:junit-jupiter:6.0.3" junit = "org.junit.jupiter:junit-jupiter:6.1.2"
jspecify = "org.jspecify:jspecify:1.0.0" jspecify = "org.jspecify:jspecify:1.0.1"
kyori-ansi = "net.kyori:ansi:1.1.1" kyori-ansi = "net.kyori:ansi:1.1.1"
guava = "com.google.guava:guava:33.6.0-jre" guava = "com.google.guava:guava:33.6.0-jre"
gson = "com.google.code.gson:gson:2.14.0" gson = "com.google.code.gson:gson:2.14.0"
guice = "com.google.inject:guice:7.0.0" guice = "com.google.inject:guice:7.0.0"
lmbda = "org.lanternpowered:lmbda:2.0.0" lmbda = "org.lanternpowered:lmbda:3.0.0"
log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" }
log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" }
log4j-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" } log4j-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" }
log4j-iostreams = { module = "org.apache.logging.log4j:log4j-iostreams", version.ref = "log4j" } log4j-iostreams = { module = "org.apache.logging.log4j:log4j-iostreams", version.ref = "log4j" }
log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" } log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" }
mockito = "org.mockito:mockito-core:5.22.0" mockito = "org.mockito:mockito-core:5.23.0"
netty-codec = { module = "io.netty:netty-codec", version.ref = "netty" } netty-codec = { module = "io.netty:netty-codec", version.ref = "netty" }
netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", version.ref = "netty" } netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", version.ref = "netty" }
netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" } netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" }
@@ -54,10 +54,10 @@ netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" }
netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" } netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" }
netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" } netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" }
netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" } netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" }
nightconfig = "com.electronwill.night-config:toml:3.8.3" nightconfig = "com.electronwill.night-config:toml:3.9.0"
slf4j = "org.slf4j:slf4j-api:2.0.17" slf4j = "org.slf4j:slf4j-api:2.0.18"
snakeyaml = "org.yaml:snakeyaml:2.5" snakeyaml = "org.yaml:snakeyaml:2.6"
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.9.8" spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.10.3"
terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0" terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0"
[bundles] [bundles]
@@ -5,29 +5,44 @@ set -e
# make sure we're in the correct directory - the top-level `native` directory # make sure we're in the correct directory - the top-level `native` directory
cd "$(dirname "$0")/.." || exit 1 cd "$(dirname "$0")/.." || exit 1
# Use docker by default, falling back to podman. Set CONTAINER_ENGINE to pick one explicitly.
if [ -z "$CONTAINER_ENGINE" ]; then
if command -v docker > /dev/null 2>&1; then
CONTAINER_ENGINE=docker
elif command -v podman > /dev/null 2>&1; then
CONTAINER_ENGINE=podman
else
echo "Neither docker nor podman was found on PATH." >&2
exit 1
fi
fi
echo "Using container engine: $CONTAINER_ENGINE"
ARCHS=(x86_64 aarch64) ARCHS=(x86_64 aarch64)
BASE_DOCKERFILE_VARIANTS=(ubuntu-focal ubuntu-jammy alpine) BASE_DOCKERFILE_VARIANTS=(ubuntu-focal ubuntu-jammy alpine)
COMPRESSION_VARIANTS=(ubuntu-focal alpine) COMPRESSION_VARIANTS=(ubuntu-focal alpine)
# Build one image per (variant, arch). Passing several --platform flags to a single tagged build
# does not produce a multi-arch tag - only the last architecture keeps the tag, and the run below
# then sees a platform mismatch, treats the image as missing and tries to pull it from a registry.
for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do
docker_platforms=""
for arch in "${ARCHS[@]}"; do for arch in "${ARCHS[@]}"; do
docker_platforms="$docker_platforms --platform linux/${arch}" echo "Building base build image for $variant on $arch..."
$CONTAINER_ENGINE build -t velocity-native-build:$variant-$arch --platform linux/${arch} \
-f build-support/$variant.Dockerfile .
done done
echo "Building base build image for $variant..."
docker build -t velocity-native-build:$variant $docker_platforms -f build-support/$variant.Dockerfile .
done done
for arch in "${ARCHS[@]}"; do for arch in "${ARCHS[@]}"; do
for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do
echo "Building native crypto for $arch on $variant..." echo "Building native crypto for $arch on $variant..."
docker run --rm -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant /bin/bash -c "cd /app && ./build-support/compile-linux-crypto.sh" $CONTAINER_ENGINE run --rm --pull=never -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant-$arch /bin/bash -c "cd /app && ./build-support/compile-linux-crypto.sh"
done done
for variant in "${COMPRESSION_VARIANTS[@]}"; do for variant in "${COMPRESSION_VARIANTS[@]}"; do
echo "Building native compression for $arch on $variant..." echo "Building native compression for $arch on $variant..."
docker run --rm -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant /bin/bash -c "cd /app && ./build-support/compile-linux-compress.sh" $CONTAINER_ENGINE run --rm --pull=never -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant-$arch /bin/bash -c "cd /app && ./build-support/compile-linux-compress.sh"
done done
done done
+1 -2
View File
@@ -34,8 +34,7 @@ Java_com_velocitypowered_natives_compression_NativeZlibInflate_process(JNIEnv *e
jlong sourceAddress, jlong sourceAddress,
jint sourceLength, jint sourceLength,
jlong destinationAddress, jlong destinationAddress,
jint destinationLength, jint destinationLength)
jlong maximumSize)
{ {
struct libdeflate_decompressor *decompress = (struct libdeflate_decompressor *) ctx; struct libdeflate_decompressor *decompress = (struct libdeflate_decompressor *) ctx;
enum libdeflate_result result = libdeflate_zlib_decompress(decompress, (void *) sourceAddress, enum libdeflate_result result = libdeflate_zlib_decompress(decompress, (void *) sourceAddress,
@@ -56,24 +56,45 @@ public class JavaVelocityCompressor implements VelocityCompressor {
final int origIdx = source.readerIndex(); final int origIdx = source.readerIndex();
inflater.setInput(source.nioBuffer()); inflater.setInput(source.nioBuffer());
int totalProduced = 0;
try { try {
final int readable = source.readableBytes(); final int readable = source.readableBytes();
while (!inflater.finished() && inflater.getBytesRead() < readable) { while (!inflater.finished() && inflater.getBytesRead() < readable) {
if (totalProduced >= uncompressedSize) {
throw new DataFormatException("Decompressed data exceeds the claimed uncompressed size "
+ "of " + uncompressedSize + " bytes");
}
final int remaining = uncompressedSize - totalProduced;
if (!destination.isWritable()) { if (!destination.isWritable()) {
destination.ensureWritable(ZLIB_BUFFER_SIZE); destination.ensureWritable(Math.min(ZLIB_BUFFER_SIZE, remaining));
} }
ByteBuffer destNioBuf = destination.nioBuffer(destination.writerIndex(), ByteBuffer destNioBuf = destination.nioBuffer(destination.writerIndex(),
destination.writableBytes()); destination.writableBytes());
// Never let a single inflate step write past the claimed size
if (destNioBuf.remaining() > remaining) {
destNioBuf.limit(destNioBuf.position() + remaining);
}
int produced = inflater.inflate(destNioBuf); int produced = inflater.inflate(destNioBuf);
if (produced == 0 && !inflater.finished()) {
// Output space was available yet the inflater made no progress: the stream is truncated
// or corrupt (this also covers a peer that over-reported the uncompressed size).
throw new DataFormatException("Received a truncated or malformed deflate stream, "
+ "expected " + uncompressedSize + " bytes");
}
totalProduced += produced;
destination.writerIndex(destination.writerIndex() + produced); destination.writerIndex(destination.writerIndex() + produced);
} }
if (!inflater.finished()) { if (!inflater.finished()) {
throw new DataFormatException("Received a deflate stream that was too large, wanted " throw new DataFormatException("Received a truncated or malformed deflate stream, expected "
+ uncompressedSize); + uncompressedSize + " bytes");
} }
source.readerIndex(origIdx + inflater.getTotalIn());
source.readerIndex(origIdx + (int) inflater.getBytesRead());
} finally { } finally {
inflater.reset(); inflater.reset();
} }
@@ -102,7 +123,7 @@ public class JavaVelocityCompressor implements VelocityCompressor {
destination.writerIndex(destination.writerIndex() + produced); destination.writerIndex(destination.writerIndex() + produced);
} }
source.readerIndex(origIdx + deflater.getTotalIn()); source.readerIndex(origIdx + (int) deflater.getBytesRead());
deflater.reset(); deflater.reset();
} }
@@ -17,6 +17,7 @@
package com.velocitypowered.natives.compression; package com.velocitypowered.natives.compression;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
@@ -77,6 +78,83 @@ class VelocityCompressorTest {
check(compressor, () -> Unpooled.buffer(TEST_DATA.length + 32)); check(compressor, () -> Unpooled.buffer(TEST_DATA.length + 32));
} }
private static final int BOMB_ACTUAL_SIZE = 1 << 20;
private static final int BOMB_LYING_CLAIM = 1024;
@Test
void javaRejectsUnderReportedUncompressedSize() throws DataFormatException {
VelocityCompressor compressor = JavaVelocityCompressor.FACTORY
.create(Deflater.DEFAULT_COMPRESSION);
DataFormatException ex = assertRejectsDecompressionBomb(compressor);
// The Java compressor's size guard names the claimed size in its message, so operators can
// tell an over-size rejection apart from a genuinely corrupt stream.
assertTrue(ex.getMessage().contains(String.valueOf(BOMB_LYING_CLAIM)),
"rejection must originate from the uncompressed-size guard, got: " + ex.getMessage());
}
@Test
@EnabledOnOs({LINUX})
void nativeRejectsUnderReportedUncompressedSize() throws DataFormatException {
VelocityCompressor compressor = Natives.compress.get().create(Deflater.DEFAULT_COMPRESSION);
if (compressor.preferredBufferType() != BufferPreference.DIRECT_REQUIRED) {
compressor.close();
fail("Loaded regular compressor");
}
// libdeflate rejects with its own native-origin message ("uncompressed size is inaccurate"),
// so we only assert the behavioural guarantee here, not the message text.
assertRejectsDecompressionBomb(compressor);
}
/**
* Asserts that a compressor refuses a decompression bomb: a small, valid deflate stream whose
* claimed uncompressed size is far smaller than what it actually inflates to. Verifies the same
* stream round-trips when the claimed size is honest (proving the rejection is caused by the
* under-reported size, not corrupt input) and that no output is written past the claimed size.
* Closes the compressor before returning the exception thrown by the rejected inflate.
*/
private DataFormatException assertRejectsDecompressionBomb(VelocityCompressor compressor)
throws DataFormatException {
// Direct buffers so this works for the native compressor, which requires them.
ByteBuf source = Unpooled.directBuffer(BOMB_ACTUAL_SIZE);
ByteBuf compressed = Unpooled.directBuffer();
try {
source.writeZero(BOMB_ACTUAL_SIZE);
compressor.deflate(source, compressed);
final int compressedSize = compressed.readableBytes();
assertTrue(compressedSize < BOMB_ACTUAL_SIZE / 100,
"sanity: payload really is a decompression bomb (" + compressedSize + " -> "
+ BOMB_ACTUAL_SIZE + ")");
// Positive control: the compressed stream is perfectly valid and round-trips when the peer
// tells the truth about its uncompressed size.
ByteBuf honest = Unpooled.directBuffer();
try {
compressor.inflate(compressed.duplicate(), honest, BOMB_ACTUAL_SIZE);
assertEquals(BOMB_ACTUAL_SIZE, honest.readableBytes(),
"valid stream must fully decompress when the claimed size is honest");
} finally {
honest.release();
}
// Attack: same valid stream, but a tiny claimed size. inflate must refuse rather than grow
// the destination without bound.
ByteBuf decompressed = Unpooled.directBuffer();
try {
DataFormatException ex = assertThrows(DataFormatException.class,
() -> compressor.inflate(compressed.duplicate(), decompressed, BOMB_LYING_CLAIM));
assertTrue(decompressed.writerIndex() <= BOMB_LYING_CLAIM,
"inflate must not write past the claimed uncompressed size");
return ex;
} finally {
decompressed.release();
}
} finally {
source.release();
compressed.release();
compressor.close();
}
}
private void check(VelocityCompressor compressor, Supplier<ByteBuf> bufSupplier) private void check(VelocityCompressor compressor, Supplier<ByteBuf> bufSupplier)
throws DataFormatException { throws DataFormatException {
ByteBuf source = bufSupplier.get(); ByteBuf source = bufSupplier.get();
+5 -60
View File
@@ -33,65 +33,6 @@ tasks {
transform(Log4j2PluginsCacheFileTransformer::class.java) transform(Log4j2PluginsCacheFileTransformer::class.java)
// Exclude all the collection types we don"t intend to use
exclude("it/unimi/dsi/fastutil/booleans/**")
exclude("it/unimi/dsi/fastutil/bytes/**")
exclude("it/unimi/dsi/fastutil/chars/**")
exclude("it/unimi/dsi/fastutil/doubles/**")
exclude("it/unimi/dsi/fastutil/floats/**")
exclude("it/unimi/dsi/fastutil/longs/**")
exclude("it/unimi/dsi/fastutil/shorts/**")
// Exclude the fastutil IO utilities - we don"t use them.
exclude("it/unimi/dsi/fastutil/io/**")
// Exclude most of the int types - Object2IntMap have a values() method that returns an
// IntCollection, and we need Int2ObjectMap
exclude("it/unimi/dsi/fastutil/ints/*Int2Boolean*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Byte*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Char*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Double*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Float*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Int*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Long*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Short*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Reference*")
exclude("it/unimi/dsi/fastutil/ints/IntAVL*")
exclude("it/unimi/dsi/fastutil/ints/IntArrayF*")
exclude("it/unimi/dsi/fastutil/ints/IntArrayI*")
exclude("it/unimi/dsi/fastutil/ints/IntArrayL*")
exclude("it/unimi/dsi/fastutil/ints/IntArrayP*")
exclude("it/unimi/dsi/fastutil/ints/IntArraySet*")
exclude("it/unimi/dsi/fastutil/ints/*IntBi*")
exclude("it/unimi/dsi/fastutil/ints/Int*Pair")
exclude("it/unimi/dsi/fastutil/ints/IntLinked*")
exclude("it/unimi/dsi/fastutil/ints/IntList*")
exclude("it/unimi/dsi/fastutil/ints/IntHeap*")
exclude("it/unimi/dsi/fastutil/ints/IntOpen*")
exclude("it/unimi/dsi/fastutil/ints/IntRB*")
exclude("it/unimi/dsi/fastutil/ints/IntSorted*")
exclude("it/unimi/dsi/fastutil/ints/*Priority*")
exclude("it/unimi/dsi/fastutil/ints/*BigList*")
// Try to exclude everything BUT Object2Int{LinkedOpen,Open,CustomOpen}HashMap
exclude("it/unimi/dsi/fastutil/objects/*ObjectArray*")
exclude("it/unimi/dsi/fastutil/objects/*ObjectAVL*")
exclude("it/unimi/dsi/fastutil/objects/*Object*Big*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Boolean*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Byte*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Char*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Double*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Float*")
exclude("it/unimi/dsi/fastutil/objects/*Object2IntArray*")
exclude("it/unimi/dsi/fastutil/objects/*Object2IntAVL*")
exclude("it/unimi/dsi/fastutil/objects/*Object2IntRB*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Long*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Object*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Reference*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Short*")
exclude("it/unimi/dsi/fastutil/objects/*ObjectRB*")
exclude("it/unimi/dsi/fastutil/objects/*Reference*")
// Exclude Checker Framework annotations // Exclude Checker Framework annotations
exclude("org/checkerframework/checker/**") exclude("org/checkerframework/checker/**")
@@ -129,9 +70,13 @@ fill {
build { build {
channel = BuildChannel.STABLE channel = BuildChannel.STABLE
versionFamily("3.0.0") versionFamily("4.0.0")
version(projectVersion) version(projectVersion)
if (versionFamily.get().split(".")[0] != projectVersion.split(".")[0]) {
throw IllegalArgumentException("Version family does not match project version")
}
downloads { downloads {
register("server:default") { register("server:default") {
file = tasks.shadowJar.flatMap { it.archiveFile } file = tasks.shadowJar.flatMap { it.archiveFile }
@@ -221,7 +221,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
PluginDescription description = new VelocityPluginDescription( PluginDescription description = new VelocityPluginDescription(
"velocity", version.getName(), version.getVersion(), "The Velocity proxy", "velocity", version.getName(), version.getVersion(), "The Velocity proxy",
version.getName().equals("Velocity") ? VELOCITY_URL : null, version.getName().equals("Velocity") ? VELOCITY_URL : null,
ImmutableList.of(version.getVendor()), Collections.emptyList(), null); ImmutableList.of(version.getVendor()), Collections.emptyList(),
Collections.emptyList(), null);
VelocityPluginContainer container = new VelocityPluginContainer(description); VelocityPluginContainer container = new VelocityPluginContainer(description);
container.setInstance(VelocityVirtualPlugin.INSTANCE); container.setInstance(VelocityVirtualPlugin.INSTANCE);
return container; return container;
@@ -163,6 +163,9 @@ public final class VelocityCommand {
.append(Component.text() .append(Component.text()
.content(version.getVersion()) .content(version.getVersion())
.decoration(TextDecoration.BOLD, false)) .decoration(TextDecoration.BOLD, false))
.hoverEvent(Component.translatable("velocity.command.version-offer-copy-version"))
.clickEvent(ClickEvent.copyToClipboard(version.getName() + " "
+ version.getVersion()))
.build(); .build();
final Component copyright = Component final Component copyright = Component
.translatable("velocity.command.version-copyright", .translatable("velocity.command.version-copyright",
@@ -53,6 +53,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket; 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.SetCompressionPacket;
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket; import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
@@ -200,6 +201,10 @@ public interface MinecraftSessionHandler {
return false; return false;
} }
default boolean handle(ServerboundPlayerLoadedPacket packet) {
return false;
}
default boolean handle(ServerLoginPacket packet) { default boolean handle(ServerLoginPacket packet) {
return false; return false;
} }
@@ -296,6 +296,19 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return true; return true;
} }
// Register and unregister packets are simply forwarded to the client as-is.
if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) {
return false;
}
if (PluginMessageUtil.isMcBrand(packet)) {
PluginMessagePacket rewritten = PluginMessageUtil
.rewriteMinecraftBrand(packet,
server.getVersion(), playerConnection.getProtocolVersion());
playerConnection.write(rewritten);
return true;
}
if (serverConn.getPhase().handle(serverConn, serverConn.getPlayer(), packet)) { if (serverConn.getPhase().handle(serverConn, serverConn.getPlayer(), packet)) {
// Handled. // Handled.
return true; return true;
@@ -277,7 +277,6 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(), PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(),
serverConn.getPlayer().getProtocolVersion())); serverConn.getPlayer().getProtocolVersion()));
} else { } else {
byte[] bytes = ByteBufUtil.getBytes(packet.content());
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel()); ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) { if (id == null) {
@@ -287,6 +286,7 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
// Handling this stuff async means that we should probably pause // Handling this stuff async means that we should probably pause
// the connection while we toss this off into another pool // the connection while we toss this off into another pool
byte[] bytes = ByteBufUtil.getBytes(packet.content());
this.serverConn.getConnection().setAutoReading(false); this.serverConn.getConnection().setAutoReading(false);
this.server.getEventManager() this.server.getEventManager()
.fire(new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, bytes)) .fire(new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, bytes))
@@ -68,6 +68,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
private final VelocityServer server; private final VelocityServer server;
private @Nullable MinecraftConnection connection; private @Nullable MinecraftConnection connection;
private boolean hasCompletedJoin = false; private boolean hasCompletedJoin = false;
private boolean clientLoaded = false; // 1.21.4+
private boolean gracefulDisconnect = false; private boolean gracefulDisconnect = false;
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN; private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
private final Map<Long, Long> pendingPings = new HashMap<>(); private final Map<Long, Long> pendingPings = new HashMap<>();
@@ -317,6 +318,15 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
} }
} }
public void setClientLoaded(boolean clientLoaded) {
this.clientLoaded = clientLoaded;
}
@Override
public boolean isClientLoaded() {
return clientLoaded;
}
boolean isGracefulDisconnect() { boolean isGracefulDisconnect() {
return gracefulDisconnect; return gracefulDisconnect;
} }
@@ -47,6 +47,7 @@ import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket; import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.buffer.ByteBufUtil; import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -135,7 +136,6 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
} else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) { } else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) {
return true; return true;
} else if (serverConn != null) { } else if (serverConn != null) {
byte[] bytes = ByteBufUtil.getBytes(packet.content());
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel()); ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) { if (id == null) {
@@ -145,6 +145,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
// Handling this stuff async means that we should probably pause // Handling this stuff async means that we should probably pause
// the connection while we toss this off into another pool // the connection while we toss this off into another pool
byte[] bytes = ByteBufUtil.getBytes(packet.content());
serverConn.getPlayer().getConnection().setAutoReading(false); serverConn.getPlayer().getConnection().setAutoReading(false);
this.server.getEventManager() this.server.getEventManager()
.fire(new PluginMessageEvent(serverConn.getPlayer(), serverConn, id, bytes)) .fire(new PluginMessageEvent(serverConn.getPlayer(), serverConn, id, bytes))
@@ -212,8 +213,9 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(ServerboundCustomClickActionPacket packet) { public boolean handle(ServerboundCustomClickActionPacket packet) {
if (player.getConnectionInFlight() != null) { VelocityServerConnection serverConnection = player.getConnectionInFlightOrConnectedServer();
player.getConnectionInFlight().ensureConnected().write(packet.retain()); if (serverConnection != null) {
serverConnection.ensureConnected().write(packet.retain());
return true; return true;
} }
@@ -240,8 +242,8 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
MinecraftConnection smc = serverConnection.getConnection(); MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && serverConnection.getPhase().consideredComplete()) { if (smc != null && serverConnection.getPhase().consideredComplete()) {
if (packet instanceof PluginMessagePacket) { if (packet instanceof ByteBufHolder bufHolder) {
((PluginMessagePacket) packet).retain(); bufHolder.retain();
} }
smc.write(packet); smc.write(packet);
} }
@@ -26,6 +26,7 @@ import com.velocitypowered.api.event.player.CookieReceiveEvent;
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent; import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
import com.velocitypowered.api.event.player.PlayerChannelUnregisterEvent; import com.velocitypowered.api.event.player.PlayerChannelUnregisterEvent;
import com.velocitypowered.api.event.player.PlayerClientBrandEvent; import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
import com.velocitypowered.api.event.player.PlayerClientLoadedWorldEvent;
import com.velocitypowered.api.event.player.TabCompleteEvent; import com.velocitypowered.api.event.player.TabCompleteEvent;
import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent; import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
@@ -52,6 +53,7 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
import com.velocitypowered.proxy.protocol.packet.RespawnPacket; import com.velocitypowered.proxy.protocol.packet.RespawnPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer;
@@ -243,6 +245,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
return true; // will forward onto the server return true; // will forward onto the server
} }
@Override
public boolean handle(ServerboundPlayerLoadedPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning - shouldn't be possible with a vanilla client
return true;
}
if (!serverConnection.isClientLoaded()) {
serverConnection.setClientLoaded(true);
server.getEventManager().fireAndForget(new PlayerClientLoadedWorldEvent(player));
}
return false;
}
@Override @Override
public boolean handle(SessionPlayerCommandPacket packet) { public boolean handle(SessionPlayerCommandPacket packet) {
if (player.getCurrentServer().isEmpty()) { if (player.getCurrentServer().isEmpty()) {
@@ -60,9 +60,7 @@ final class CustomHandlerAdapter<F> {
UntargetedEventHandler buildUntargetedHandler(final Method method) UntargetedEventHandler buildUntargetedHandler(final Method method)
throws IllegalAccessException { throws IllegalAccessException {
final MethodHandle methodHandle = methodHandlesLookup.unreflect(method); final MethodHandle methodHandle = methodHandlesLookup.unreflect(method);
final MethodHandles.Lookup defineLookup = MethodHandles.privateLookupIn( final LambdaType<F> lambdaType = functionType.defineClassesWith(methodHandlesLookup);
method.getDeclaringClass(), methodHandlesLookup);
final LambdaType<F> lambdaType = functionType.defineClassesWith(defineLookup);
final F invokeFunction = LambdaFactory.create(lambdaType, methodHandle); final F invokeFunction = LambdaFactory.create(lambdaType, methodHandle);
final BiFunction<Object, Object, EventTask> handlerFunction = final BiFunction<Object, Object, EventTask> handlerFunction =
handlerBuilder.apply(invokeFunction); handlerBuilder.apply(invokeFunction);
@@ -243,7 +243,7 @@ public class VelocityEventManager implements EventManager {
} else { } else {
type = untargetedVoidHandlerType; type = untargetedVoidHandlerType;
} }
return LambdaFactory.create(type.defineClassesWith(lookup), methodHandle); return LambdaFactory.create(type.defineClassesWith(methodHandlesLookup), methodHandle);
} }
static final class MethodHandlerInfo { static final class MethodHandlerInfo {
@@ -46,10 +46,12 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -62,6 +64,7 @@ public class VelocityPluginManager implements PluginManager {
private final Map<String, PluginContainer> pluginsById = new LinkedHashMap<>(); private final Map<String, PluginContainer> pluginsById = new LinkedHashMap<>();
private final Map<Object, PluginContainer> pluginInstances = new IdentityHashMap<>(); private final Map<Object, PluginContainer> pluginInstances = new IdentityHashMap<>();
private final Set<PluginContainer> plugins = new LinkedHashSet<>();
private final VelocityServer server; private final VelocityServer server;
public VelocityPluginManager(VelocityServer server) { public VelocityPluginManager(VelocityServer server) {
@@ -74,7 +77,9 @@ public class VelocityPluginManager implements PluginManager {
* @param plugin the plugin to register * @param plugin the plugin to register
*/ */
public void registerPlugin(PluginContainer plugin) { public void registerPlugin(PluginContainer plugin) {
plugins.add(plugin);
pluginsById.put(plugin.getDescription().getId(), plugin); pluginsById.put(plugin.getDescription().getId(), plugin);
plugin.getDescription().getProvidedIds().forEach(id -> pluginsById.put(id, plugin));
Optional<?> instance = plugin.getInstance(); Optional<?> instance = plugin.getInstance();
instance.ifPresent(o -> pluginInstances.put(o, plugin)); instance.ifPresent(o -> pluginInstances.put(o, plugin));
} }
@@ -100,16 +105,34 @@ public class VelocityPluginManager implements PluginManager {
try { try {
PluginDescription candidate = loader.loadCandidate(path); PluginDescription candidate = loader.loadCandidate(path);
// If we found a duplicate candidate (with the same ID), don't load it. // A plugin claims its own ID plus every ID it provides. If any of those are already
PluginDescription maybeExistingCandidate = foundCandidates.putIfAbsent( // claimed by another candidate, don't load this one.
candidate.getId(), candidate); List<String> claimedIds = new ArrayList<>(candidate.getProvidedIds().size() + 1);
claimedIds.add(candidate.getId());
claimedIds.addAll(candidate.getProvidedIds());
if (maybeExistingCandidate != null) { PluginDescription conflict = null;
logger.error("Refusing to load plugin at path {} since we already " String conflictingId = null;
+ "loaded a plugin with the same ID {} from {}", for (String id : claimedIds) {
PluginDescription existing = foundCandidates.get(id);
if (existing != null) {
conflict = existing;
conflictingId = id;
break;
}
}
if (conflict != null) {
logger.error("Refusing to load plugin at path {} since ID {} was already "
+ "claimed by a plugin loaded from {}",
candidate.getSource().map(Objects::toString).orElse("<UNKNOWN>"), candidate.getSource().map(Objects::toString).orElse("<UNKNOWN>"),
candidate.getId(), conflictingId,
maybeExistingCandidate.getSource().map(Objects::toString).orElse("<UNKNOWN>")); conflict.getSource().map(Objects::toString).orElse("<UNKNOWN>"));
continue;
}
for (String id : claimedIds) {
foundCandidates.put(id, candidate);
} }
} catch (Throwable e) { } catch (Throwable e) {
logger.error("Unable to load plugin {}", path, e); logger.error("Unable to load plugin {}", path, e);
@@ -122,8 +145,10 @@ public class VelocityPluginManager implements PluginManager {
return; return;
} }
// foundCandidates indexes each candidate under its ID and any provided IDs, so dedupe before
// sorting to avoid loading a plugin more than once.
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates( List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(
new ArrayList<>(foundCandidates.values())); new ArrayList<>(new LinkedHashSet<>(foundCandidates.values())));
Map<String, PluginDescription> loadedCandidates = new HashMap<>(); Map<String, PluginDescription> loadedCandidates = new HashMap<>();
Map<PluginContainer, Module> pluginContainers = new LinkedHashMap<>(); Map<PluginContainer, Module> pluginContainers = new LinkedHashMap<>();
@@ -144,6 +169,7 @@ public class VelocityPluginManager implements PluginManager {
VelocityPluginContainer container = new VelocityPluginContainer(realPlugin); VelocityPluginContainer container = new VelocityPluginContainer(realPlugin);
pluginContainers.put(container, loader.createModule(container)); pluginContainers.put(container, loader.createModule(container));
loadedCandidates.put(realPlugin.getId(), realPlugin); loadedCandidates.put(realPlugin.getId(), realPlugin);
realPlugin.getProvidedIds().forEach(id -> loadedCandidates.putIfAbsent(id, realPlugin));
} catch (Throwable e) { } catch (Throwable e) {
logger.error("Can't create module for plugin {}", candidate.getId(), e); logger.error("Can't create module for plugin {}", candidate.getId(), e);
} }
@@ -201,7 +227,7 @@ public class VelocityPluginManager implements PluginManager {
@Override @Override
public Collection<PluginContainer> getPlugins() { public Collection<PluginContainer> getPlugins() {
return Collections.unmodifiableCollection(pluginsById.values()); return Collections.unmodifiableCollection(plugins);
} }
@Override @Override
@@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.meta.PluginDependency; import com.velocitypowered.api.plugin.meta.PluginDependency;
@@ -43,6 +44,7 @@ public class VelocityPluginDescription implements PluginDescription {
private final @Nullable String url; private final @Nullable String url;
private final List<String> authors; private final List<String> authors;
private final Map<String, PluginDependency> dependencies; private final Map<String, PluginDependency> dependencies;
private final Collection<String> providedIds;
private final Path source; private final Path source;
/** /**
@@ -55,11 +57,13 @@ public class VelocityPluginDescription implements PluginDescription {
* @param url the website for the plugin * @param url the website for the plugin
* @param authors the authors of this plugin * @param authors the authors of this plugin
* @param dependencies the dependencies for this plugin * @param dependencies the dependencies for this plugin
* @param providedIds the IDs this plugin provides for
* @param source the original source for the plugin * @param source the original source for the plugin
*/ */
public VelocityPluginDescription(String id, @Nullable String name, @Nullable String version, public VelocityPluginDescription(String id, @Nullable String name, @Nullable String version,
@Nullable String description, @Nullable String url, @Nullable String description, @Nullable String url,
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source) { @Nullable List<String> authors, Collection<PluginDependency> dependencies,
@Nullable Collection<String> providedIds, Path source) {
this.id = checkNotNull(id, "id"); this.id = checkNotNull(id, "id");
this.name = Strings.emptyToNull(name); this.name = Strings.emptyToNull(name);
this.version = Strings.emptyToNull(version); this.version = Strings.emptyToNull(version);
@@ -67,6 +71,8 @@ public class VelocityPluginDescription implements PluginDescription {
this.url = Strings.emptyToNull(url); this.url = Strings.emptyToNull(url);
this.authors = authors == null ? ImmutableList.of() : ImmutableList.copyOf(authors); this.authors = authors == null ? ImmutableList.of() : ImmutableList.copyOf(authors);
this.dependencies = Maps.uniqueIndex(dependencies, d -> d == null ? null : d.getId()); this.dependencies = Maps.uniqueIndex(dependencies, d -> d == null ? null : d.getId());
this.providedIds =
providedIds == null ? ImmutableSet.of() : ImmutableSet.copyOf(providedIds);
this.source = source; this.source = source;
} }
@@ -110,6 +116,11 @@ public class VelocityPluginDescription implements PluginDescription {
return Optional.ofNullable(dependencies.get(id)); return Optional.ofNullable(dependencies.get(id));
} }
@Override
public Collection<String> getProvidedIds() {
return providedIds;
}
@Override @Override
public Optional<Path> getSource() { public Optional<Path> getSource() {
return Optional.ofNullable(source); return Optional.ofNullable(source);
@@ -125,6 +136,7 @@ public class VelocityPluginDescription implements PluginDescription {
+ ", url='" + url + '\'' + ", url='" + url + '\''
+ ", authors=" + authors + ", authors=" + authors
+ ", dependencies=" + dependencies + ", dependencies=" + dependencies
+ ", providedIds=" + providedIds
+ ", source=" + source + ", source=" + source
+ '}'; + '}';
} }
@@ -78,6 +78,14 @@ public class JavaPluginLoader implements PluginLoader {
} }
} }
for (String providedId : pd.getProvides()) {
if (!SerializedPluginDescription.ID_PATTERN.matcher(providedId).matches()) {
throw new InvalidPluginException(
"Provided ID '" + providedId + "' for plugin '" + pd.getId() + "' is invalid."
);
}
}
return createCandidateDescription(pd, source); return createCandidateDescription(pd, source);
} }
@@ -181,6 +189,7 @@ public class JavaPluginLoader implements PluginLoader {
description.getUrl(), description.getUrl(),
description.getAuthors(), description.getAuthors(),
dependencies, dependencies,
description.getProvides(),
source, source,
description.getMain() description.getMain()
); );
@@ -197,6 +206,7 @@ public class JavaPluginLoader implements PluginLoader {
description.getUrl().orElse(null), description.getUrl().orElse(null),
description.getAuthors(), description.getAuthors(),
description.getDependencies(), description.getDependencies(),
description.getProvidedIds(),
description.getSource().orElse(null), description.getSource().orElse(null),
mainClass mainClass
); );
@@ -32,9 +32,9 @@ class JavaVelocityPluginDescription extends VelocityPluginDescription {
JavaVelocityPluginDescription(String id, @Nullable String name, @Nullable String version, JavaVelocityPluginDescription(String id, @Nullable String name, @Nullable String version,
@Nullable String description, @Nullable String url, @Nullable String description, @Nullable String url,
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source, @Nullable List<String> authors, Collection<PluginDependency> dependencies,
Class<?> mainClass) { @Nullable Collection<String> providedIds, Path source, Class<?> mainClass) {
super(id, name, version, description, url, authors, dependencies, source); super(id, name, version, description, url, authors, dependencies, providedIds, source);
this.mainClass = checkNotNull(mainClass); this.mainClass = checkNotNull(mainClass);
} }
@@ -32,9 +32,9 @@ class JavaVelocityPluginDescriptionCandidate extends VelocityPluginDescription {
JavaVelocityPluginDescriptionCandidate(String id, @Nullable String name, @Nullable String version, JavaVelocityPluginDescriptionCandidate(String id, @Nullable String name, @Nullable String version,
@Nullable String description, @Nullable String url, @Nullable String description, @Nullable String url,
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source, @Nullable List<String> authors, Collection<PluginDependency> dependencies,
String mainClass) { @Nullable Collection<String> providedIds, Path source, String mainClass) {
super(id, name, version, description, url, authors, dependencies, source); super(id, name, version, description, url, authors, dependencies, providedIds, source);
this.mainClass = checkNotNull(mainClass); this.mainClass = checkNotNull(mainClass);
} }
@@ -17,7 +17,6 @@
package com.velocitypowered.proxy.plugin.util; package com.velocitypowered.proxy.plugin.util;
import com.google.common.collect.Maps;
import com.google.common.graph.Graph; import com.google.common.graph.Graph;
import com.google.common.graph.GraphBuilder; import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph; import com.google.common.graph.MutableGraph;
@@ -60,8 +59,19 @@ public class PluginDependencyUtils {
.allowsSelfLoops(false) .allowsSelfLoops(false)
.expectedNodeCount(sortedCandidates.size()) .expectedNodeCount(sortedCandidates.size())
.build(); .build();
Map<String, PluginDescription> candidateMap = Maps.uniqueIndex(sortedCandidates,
PluginDescription::getId); // Index candidates by their own ID and any IDs they provide, so a dependency can be satisfied
// by a plugin that provides that ID. Real IDs take precedence over provided IDs, and the first
// provider of a given ID wins; upstream loading rejects such conflicts before we get here.
Map<String, PluginDescription> candidateMap = new HashMap<>();
for (PluginDescription description : sortedCandidates) {
candidateMap.putIfAbsent(description.getId(), description);
}
for (PluginDescription description : sortedCandidates) {
for (String provided : description.getProvidedIds()) {
candidateMap.putIfAbsent(provided, description);
}
}
for (PluginDescription description : sortedCandidates) { for (PluginDescription description : sortedCandidates) {
graph.addNode(description); graph.addNode(description);
@@ -69,7 +79,8 @@ public class PluginDependencyUtils {
for (PluginDependency dependency : description.getDependencies()) { for (PluginDependency dependency : description.getDependencies()) {
PluginDescription in = candidateMap.get(dependency.getId()); PluginDescription in = candidateMap.get(dependency.getId());
if (in != null) { // Guard against self-loops: a plugin may name an ID it itself provides.
if (in != null && !in.equals(description)) {
graph.putEdge(description, in); graph.putEdge(description, in);
} }
} }
@@ -18,37 +18,7 @@
package com.velocitypowered.proxy.protocol; package com.velocitypowered.proxy.protocol;
import static com.google.common.collect.Iterables.getLast; import static com.google.common.collect.Iterables.getLast;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12; import static com.velocitypowered.api.network.ProtocolVersion.*;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12_1;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_14;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_15;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_17;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_18;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_18_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_1;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_3;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_3;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_5;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_5;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_6;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_9;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_1;
import static com.velocitypowered.api.network.ProtocolVersion.MINIMUM_VERSION;
import static com.velocitypowered.api.network.ProtocolVersion.SUPPORTED_VERSIONS;
import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction.CLIENTBOUND; import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction.CLIENTBOUND;
import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction.SERVERBOUND; import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction.SERVERBOUND;
@@ -88,6 +58,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket; 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.SetCompressionPacket;
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket; import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
@@ -345,6 +316,12 @@ public enum StateRegistry {
map(0x13, MINECRAFT_1_21_2, false), map(0x13, MINECRAFT_1_21_2, false),
map(0x14, MINECRAFT_1_21_6, false), map(0x14, MINECRAFT_1_21_6, false),
map(0x15, MINECRAFT_26_1, false)); map(0x15, MINECRAFT_26_1, false));
serverbound.register(
ServerboundPlayerLoadedPacket.class,
() -> ServerboundPlayerLoadedPacket.INSTANCE,
map(0x2A, MINECRAFT_1_21_4, false),
map(0x2B, MINECRAFT_1_21_6, false),
map(0x2C, MINECRAFT_26_1, false));
serverbound.register( serverbound.register(
PluginMessagePacket.class, PluginMessagePacket.class,
PluginMessagePacket::new, PluginMessagePacket::new,
@@ -835,24 +812,27 @@ public enum StateRegistry {
ClientboundServerLinksPacket::new, ClientboundServerLinksPacket::new,
map(0x7B, MINECRAFT_1_21, false), map(0x7B, MINECRAFT_1_21, false),
map(0x82, MINECRAFT_1_21_2, false), map(0x82, MINECRAFT_1_21_2, false),
map(0x87, MINECRAFT_1_21_9, false)); map(0x87, MINECRAFT_1_21_9, false),
clientbound.register(UpdateTeamsPacket.class, UpdateTeamsPacket::new,
map(0x41, ProtocolVersion.MINECRAFT_1_9, true),
map(0x43, ProtocolVersion.MINECRAFT_1_12, true),
map(0x44, ProtocolVersion.MINECRAFT_1_12_1, true),
map(0x47, ProtocolVersion.MINECRAFT_1_13, true),
map(0x4B, ProtocolVersion.MINECRAFT_1_14, true),
map(0x4C, ProtocolVersion.MINECRAFT_1_15, true),
map(0x55, ProtocolVersion.MINECRAFT_1_17, true),
map(0x58, ProtocolVersion.MINECRAFT_1_19_1, true),
map(0x56, ProtocolVersion.MINECRAFT_1_19_3, true),
map(0x5A, ProtocolVersion.MINECRAFT_1_19_4, true),
map(0x5C, ProtocolVersion.MINECRAFT_1_20_2, true),
map(0x5E, ProtocolVersion.MINECRAFT_1_20_3, true),
map(0x60, ProtocolVersion.MINECRAFT_1_20_5, true),
map(0x67, ProtocolVersion.MINECRAFT_1_21_2, true),
map(0x6B, MINECRAFT_1_21_9, false),
map(0x89, MINECRAFT_26_1, false)); map(0x89, MINECRAFT_26_1, false));
clientbound.register(UpdateTeamsPacket.class, UpdateTeamsPacket::new,
map(0x41, MINECRAFT_1_9, true),
map(0x43, MINECRAFT_1_12, true),
map(0x44, MINECRAFT_1_12_1, true),
map(0x47, MINECRAFT_1_13, true),
map(0x4B, MINECRAFT_1_14, true),
map(0x4C, MINECRAFT_1_15, true),
map(0x55, MINECRAFT_1_17, true),
map(0x58, MINECRAFT_1_19_1, true),
map(0x56, MINECRAFT_1_19_3, true),
map(0x5A, MINECRAFT_1_19_4, true),
map(0x5C, MINECRAFT_1_20_2, true),
map(0x5E, MINECRAFT_1_20_3, true),
map(0x60, MINECRAFT_1_20_5, true),
map(0x67, MINECRAFT_1_21_2, true),
map(0x6B, MINECRAFT_1_21_9, false),
map(0x63, MINECRAFT_1_21_11, false),
map(0x6D, MINECRAFT_26_1, false),
map(0x6D, MINECRAFT_26_2, false));
} }
}, },
LOGIN { LOGIN {
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2025 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 io.netty.buffer.ByteBuf;
public class ServerboundPlayerLoadedPacket implements MinecraftPacket {
public static final ServerboundPlayerLoadedPacket INSTANCE = new ServerboundPlayerLoadedPacket();
private ServerboundPlayerLoadedPacket() {
}
@Override
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
}
@Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
}
@Override
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
return 0;
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}
@@ -50,6 +50,7 @@ velocity.command.glist-view-all=To view all players on servers, use /glist all.
velocity.command.reload-success=Velocity configuration successfully reloaded. velocity.command.reload-success=Velocity configuration successfully reloaded.
velocity.command.reload-failure=Unable to reload your Velocity configuration. Check the console for more details. velocity.command.reload-failure=Unable to reload your Velocity configuration. Check the console for more details.
velocity.command.version-copyright=Copyright 2018-<arg:2> <arg:0>. <arg:1> is licensed under the terms of the GNU General Public License v3. velocity.command.version-copyright=Copyright 2018-<arg:2> <arg:0>. <arg:1> is licensed under the terms of the GNU General Public License v3.
velocity.command.version-offer-copy-version=Click to copy version to clipboard
velocity.command.no-plugins=There are no plugins currently installed. velocity.command.no-plugins=There are no plugins currently installed.
velocity.command.plugins-list=Plugins: <arg:0> velocity.command.plugins-list=Plugins: <arg:0>
velocity.command.plugin-tooltip-website=Website: <arg:0> velocity.command.plugin-tooltip-website=Website: <arg:0>
@@ -0,0 +1,119 @@
/*
* 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.connection.client;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.backend.BackendConnectionPhase;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.ReferenceCountUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class ClientConfigSessionHandlerTest {
private VelocityServer server;
private ConnectedPlayer player;
private ClientConfigSessionHandler handler;
@BeforeEach
void setUp() {
server = mock(VelocityServer.class);
player = mock(ConnectedPlayer.class);
handler = new ClientConfigSessionHandler(server, player);
}
@AfterEach
void tearDown() {
// nothing to clean up; each test manages its own ByteBufs
}
private ServerboundCustomClickActionPacket makePacket() {
ByteBuf frame = Unpooled.buffer().writeByte(0);
ServerboundCustomClickActionPacket pkt = new ServerboundCustomClickActionPacket();
pkt.replace(frame.readRetainedSlice(frame.readableBytes()));
return pkt;
}
@Test
void handleForwardsToInFlightServer() {
VelocityServerConnection inFlight = mock(VelocityServerConnection.class);
MinecraftConnection backend = mock(MinecraftConnection.class);
when(player.getConnectionInFlightOrConnectedServer()).thenReturn(inFlight);
when(inFlight.ensureConnected()).thenReturn(backend);
ServerboundCustomClickActionPacket pkt = makePacket();
assertTrue(handler.handle(pkt));
verify(backend).write(pkt);
ReferenceCountUtil.release(pkt);
}
@Test
void handleForwardsToConnectedServerWhenInFlightIsNull() {
VelocityServerConnection connected = mock(VelocityServerConnection.class);
MinecraftConnection backend = mock(MinecraftConnection.class);
when(player.getConnectionInFlightOrConnectedServer()).thenReturn(connected);
when(connected.ensureConnected()).thenReturn(backend);
ServerboundCustomClickActionPacket pkt = makePacket();
assertTrue(handler.handle(pkt));
verify(backend).write(pkt);
ReferenceCountUtil.release(pkt);
}
@Test
void handleReturnsFalseWhenNoServer() {
when(player.getConnectionInFlightOrConnectedServer()).thenReturn(null);
ServerboundCustomClickActionPacket pkt = makePacket();
assertFalse(handler.handle(pkt));
ReferenceCountUtil.release(pkt);
}
@Test
void handleGenericRetainsAndForwards() {
VelocityServerConnection connected = mock(VelocityServerConnection.class);
MinecraftConnection backend = mock(MinecraftConnection.class);
BackendConnectionPhase phase = mock(BackendConnectionPhase.class);
when(player.getConnectedServer()).thenReturn(connected);
when(connected.getConnection()).thenReturn(backend);
when(connected.getPhase()).thenReturn(phase);
when(phase.consideredComplete()).thenReturn(true);
ServerboundCustomClickActionPacket pkt = makePacket();
int refBefore = pkt.refCnt();
handler.handleGeneric(pkt);
// retain() was called (+1) before write
assertEquals(refBefore + 1, pkt.refCnt());
verify(backend).write(pkt);
ReferenceCountUtil.release(pkt);
}
}
@@ -44,6 +44,15 @@ class PluginDependencyUtilsTest {
private static final PluginDescription CIRCULAR_DEPENDENCY_2 = testDescription("oval", private static final PluginDescription CIRCULAR_DEPENDENCY_2 = testDescription("oval",
new PluginDependency("circle", "", false)); new PluginDependency("circle", "", false));
// "provider" is loaded from a real ID but provides the virtual ID "some-api"; "consumer" and
// "zdependent" depend on it only through that provided ID / a chain that reaches it.
private static final PluginDescription PROVIDES_API = providingDescription("provider",
ImmutableList.of("some-api"));
private static final PluginDescription DEPENDS_ON_PROVIDED = providingDescription("consumer",
ImmutableList.of(), new PluginDependency("some-api", null, false));
private static final PluginDescription DEPENDS_ON_CONSUMER = testDescription("zdependent",
new PluginDependency("consumer", null, false));
@Test @Test
void sortCandidatesTrivial() throws Exception { void sortCandidatesTrivial() throws Exception {
List<PluginDescription> descriptionList = new ArrayList<>(); List<PluginDescription> descriptionList = new ArrayList<>();
@@ -96,10 +105,31 @@ class PluginDependencyUtilsTest {
assertThrows(IllegalStateException.class, () -> PluginDependencyUtils.sortCandidates(descs)); assertThrows(IllegalStateException.class, () -> PluginDependencyUtils.sortCandidates(descs));
} }
@Test
void sortCandidatesResolvesProvidedDependency() throws Exception {
List<PluginDescription> plugins = ImmutableList.of(DEPENDS_ON_PROVIDED, PROVIDES_API);
List<PluginDescription> expected = ImmutableList.of(PROVIDES_API, DEPENDS_ON_PROVIDED);
assertEquals(expected, PluginDependencyUtils.sortCandidates(plugins));
}
@Test
void sortCandidatesResolvesTransitiveProvidedDependency() throws Exception {
List<PluginDescription> plugins = ImmutableList.of(DEPENDS_ON_CONSUMER, DEPENDS_ON_PROVIDED,
PROVIDES_API);
List<PluginDescription> expected = ImmutableList.of(PROVIDES_API, DEPENDS_ON_PROVIDED,
DEPENDS_ON_CONSUMER);
assertEquals(expected, PluginDependencyUtils.sortCandidates(plugins));
}
private static PluginDescription testDescription(String id, PluginDependency... dependencies) { private static PluginDescription testDescription(String id, PluginDependency... dependencies) {
return providingDescription(id, ImmutableList.of(), dependencies);
}
private static PluginDescription providingDescription(String id, List<String> provides,
PluginDependency... dependencies) {
return new VelocityPluginDescription( return new VelocityPluginDescription(
id, "tuxed", "0.1", null, null, ImmutableList.of(), id, "tuxed", "0.1", null, null, ImmutableList.of(),
ImmutableList.copyOf(dependencies), null ImmutableList.copyOf(dependencies), provides, null
); );
} }
} }