From 90f89053a729d83d7529c97898ee0c8ca82f0f27 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Tue, 14 Jul 2026 15:10:04 +0100 Subject: [PATCH 01/20] Release 4.0.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 1ccee529..824c69f1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group=com.velocitypowered -version=4.0.0-SNAPSHOT +version=4.0.0 From 3b239daf4c45a1f6fffd29b5ff39773dd3ea9fc9 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Tue, 14 Jul 2026 15:26:01 +0100 Subject: [PATCH 02/20] [ci skip] back to snapshots Hello, Darkness, my old friend --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 824c69f1..44cdb5d0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group=com.velocitypowered -version=4.0.0 +version=4.1.0-SNAPSHOT From 60380211f80ed9bf57acaa3d7846d1c221f52cf6 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Tue, 14 Jul 2026 18:04:07 -0400 Subject: [PATCH 03/20] Defer `ByteBufUtil.getBytes()` in config stages until after we check for channel registry --- .../proxy/connection/backend/ConfigSessionHandler.java | 2 +- .../proxy/connection/client/ClientConfigSessionHandler.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java index 8cc1bf8c..904e848f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java @@ -277,7 +277,6 @@ public class ConfigSessionHandler implements MinecraftSessionHandler { PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(), serverConn.getPlayer().getProtocolVersion())); } else { - byte[] bytes = ByteBufUtil.getBytes(packet.content()); ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel()); if (id == null) { @@ -287,6 +286,7 @@ public class ConfigSessionHandler implements MinecraftSessionHandler { // Handling this stuff async means that we should probably pause // the connection while we toss this off into another pool + byte[] bytes = ByteBufUtil.getBytes(packet.content()); this.serverConn.getConnection().setAutoReading(false); this.server.getEventManager() .fire(new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, bytes)) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java index e4c754f9..5cb7188e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java @@ -135,7 +135,6 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { } else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) { return true; } else if (serverConn != null) { - byte[] bytes = ByteBufUtil.getBytes(packet.content()); ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel()); if (id == null) { @@ -145,6 +144,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { // Handling this stuff async means that we should probably pause // the connection while we toss this off into another pool + byte[] bytes = ByteBufUtil.getBytes(packet.content()); serverConn.getPlayer().getConnection().setAutoReading(false); this.server.getEventManager() .fire(new PluginMessageEvent(serverConn.getPlayer(), serverConn, id, bytes)) From 9eb338bd1c785fe822cb4aa7d48489627a05973b Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Wed, 15 Jul 2026 02:22:41 +0100 Subject: [PATCH 04/20] Fix version family --- proxy/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index c1864e34..bb0dd8f0 100644 --- a/proxy/build.gradle.kts +++ b/proxy/build.gradle.kts @@ -129,7 +129,7 @@ fill { build { channel = BuildChannel.STABLE - versionFamily("3.0.0") + versionFamily("4.0.0") version(projectVersion) downloads { From 1cd8d51d028045e889ef3263b1a73869e1e76974 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Wed, 15 Jul 2026 02:29:29 +0100 Subject: [PATCH 05/20] [ci skip] primative failsafe for mismatched version family --- proxy/build.gradle.kts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index bb0dd8f0..9922a573 100644 --- a/proxy/build.gradle.kts +++ b/proxy/build.gradle.kts @@ -132,6 +132,10 @@ fill { versionFamily("4.0.0") version(projectVersion) + if (versionFamily.get().split(".") != projectVersion.split(".")) { + throw IllegalArgumentException("Version family does not match project version") + } + downloads { register("server:default") { file = tasks.shadowJar.flatMap { it.archiveFile } From 5aab0d1427f39756db525956ee16ffa0fef8b439 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Wed, 15 Jul 2026 04:28:19 +0100 Subject: [PATCH 06/20] [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 --- proxy/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index 9922a573..c026bda4 100644 --- a/proxy/build.gradle.kts +++ b/proxy/build.gradle.kts @@ -132,7 +132,7 @@ fill { versionFamily("4.0.0") version(projectVersion) - if (versionFamily.get().split(".") != projectVersion.split(".")) { + if (versionFamily.get().split(".")[0] != projectVersion.split(".")[0]) { throw IllegalArgumentException("Version family does not match project version") } From e6536479629ce96838598a7543c6ddaa805041df Mon Sep 17 00:00:00 2001 From: SpigotRCE <128710385+SpigotRCE@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:32:12 +0530 Subject: [PATCH 07/20] [ci skip] typo fix (#1778) --- .../proxy/connection/backend/BackendPlaySessionHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index dfe230e3..5ba2659f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -294,7 +294,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { return true; } - // Register and unregister packets are simply forwarded to the server as-is. + // Register and unregister packets are simply forwarded to the client as-is. if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) { return false; } From b45716deffdfc8476f583d016f481220961fcf77 Mon Sep 17 00:00:00 2001 From: Emil <12966472+Emilxyz@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:02:20 +0200 Subject: [PATCH 08/20] feat: Make version clickable in velocity info command (#1775) --- .../proxy/command/builtin/VelocityCommand.java | 11 +++++++---- .../velocitypowered/proxy/l10n/messages.properties | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java index 562efee4..bebf1c1c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java @@ -161,14 +161,17 @@ public final class VelocityCommand { .decoration(TextDecoration.BOLD, true) .color(VELOCITY_COLOR) .append(Component.text() - .content(version.getVersion()) - .decoration(TextDecoration.BOLD, false)) + .content(version.getVersion()) + .decoration(TextDecoration.BOLD, false)) + .hoverEvent(Component.translatable("velocity.command.version-offer-copy-version")) + .clickEvent(ClickEvent.copyToClipboard(version.getName() + " " + + version.getVersion())) .build(); final Component copyright = Component .translatable("velocity.command.version-copyright", Argument.string("vendor", version.getVendor()), - Argument.string("name", version.getName()), - Argument.component("year", Component.text(LocalDate.now().getYear()))); + Argument.string("name", version.getName()), + Argument.component("year", Component.text(LocalDate.now().getYear()))); source.sendMessage(velocity); source.sendMessage(copyright); diff --git a/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties b/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties index 31f8d1fc..f90a4de6 100644 --- a/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties +++ b/proxy/src/main/resources/com/velocitypowered/proxy/l10n/messages.properties @@ -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-failure=Unable to reload your Velocity configuration. Check the console for more details. velocity.command.version-copyright=Copyright 2018- . 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.plugins-list=Plugins: velocity.command.plugin-tooltip-website=Website: From d30f1d9a747acea74ac5efc3373a464aaf07d612 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Wed, 29 Jul 2026 00:51:41 +0100 Subject: [PATCH 09/20] Compressor cleanups --- native/src/main/c/jni_zlib_inflate.c | 3 +- .../compression/JavaVelocityCompressor.java | 31 ++++++-- .../compression/VelocityCompressorTest.java | 78 +++++++++++++++++++ 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/native/src/main/c/jni_zlib_inflate.c b/native/src/main/c/jni_zlib_inflate.c index d9131908..5eb3ca3d 100644 --- a/native/src/main/c/jni_zlib_inflate.c +++ b/native/src/main/c/jni_zlib_inflate.c @@ -34,8 +34,7 @@ Java_com_velocitypowered_natives_compression_NativeZlibInflate_process(JNIEnv *e jlong sourceAddress, jint sourceLength, jlong destinationAddress, - jint destinationLength, - jlong maximumSize) + jint destinationLength) { struct libdeflate_decompressor *decompress = (struct libdeflate_decompressor *) ctx; enum libdeflate_result result = libdeflate_zlib_decompress(decompress, (void *) sourceAddress, diff --git a/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java b/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java index b6c3aab4..75fe17c5 100644 --- a/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java +++ b/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java @@ -56,24 +56,45 @@ public class JavaVelocityCompressor implements VelocityCompressor { final int origIdx = source.readerIndex(); inflater.setInput(source.nioBuffer()); + int totalProduced = 0; try { final int readable = source.readableBytes(); 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()) { - destination.ensureWritable(ZLIB_BUFFER_SIZE); + destination.ensureWritable(Math.min(ZLIB_BUFFER_SIZE, remaining)); } ByteBuffer destNioBuf = destination.nioBuffer(destination.writerIndex(), 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); + 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); } if (!inflater.finished()) { - throw new DataFormatException("Received a deflate stream that was too large, wanted " - + uncompressedSize); + throw new DataFormatException("Received a truncated or malformed deflate stream, expected " + + uncompressedSize + " bytes"); } - source.readerIndex(origIdx + inflater.getTotalIn()); + + source.readerIndex(origIdx + (int) inflater.getBytesRead()); } finally { inflater.reset(); } @@ -102,7 +123,7 @@ public class JavaVelocityCompressor implements VelocityCompressor { destination.writerIndex(destination.writerIndex() + produced); } - source.readerIndex(origIdx + deflater.getTotalIn()); + source.readerIndex(origIdx + (int) deflater.getBytesRead()); deflater.reset(); } diff --git a/native/src/test/java/com/velocitypowered/natives/compression/VelocityCompressorTest.java b/native/src/test/java/com/velocitypowered/natives/compression/VelocityCompressorTest.java index a370b918..49d4c46a 100644 --- a/native/src/test/java/com/velocitypowered/natives/compression/VelocityCompressorTest.java +++ b/native/src/test/java/com/velocitypowered/natives/compression/VelocityCompressorTest.java @@ -17,6 +17,7 @@ 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.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -77,6 +78,83 @@ class VelocityCompressorTest { 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 bufSupplier) throws DataFormatException { ByteBuf source = bufSupplier.get(); From c6e9ca989eef8f60de854ebeb308077adfdf2d21 Mon Sep 17 00:00:00 2001 From: Auri Date: Wed, 29 Jul 2026 12:51:34 +0100 Subject: [PATCH 10/20] Add provides API (#1853) Add provides API Co-authored-by: Shane Freeder --- .../plugin/ap/PluginAnnotationProcessor.java | 10 ++++ .../ap/SerializedPluginDescription.java | 16 +++++-- .../velocitypowered/api/plugin/Plugin.java | 8 ++++ .../api/plugin/PluginDescription.java | 10 ++++ .../velocitypowered/proxy/VelocityServer.java | 3 +- .../proxy/plugin/VelocityPluginManager.java | 46 +++++++++++++++---- .../loader/VelocityPluginDescription.java | 14 +++++- .../plugin/loader/java/JavaPluginLoader.java | 10 ++++ .../java/JavaVelocityPluginDescription.java | 6 +-- ...avaVelocityPluginDescriptionCandidate.java | 6 +-- .../plugin/util/PluginDependencyUtils.java | 19 ++++++-- .../util/PluginDependencyUtilsTest.java | 32 ++++++++++++- 12 files changed, 154 insertions(+), 26 deletions(-) diff --git a/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java b/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java index b44f2847..572ed03c 100644 --- a/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java +++ b/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java @@ -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. SerializedPluginDescription description = SerializedPluginDescription .from(plugin, qualifiedName.toString()); diff --git a/api/src/ap/java/com/velocitypowered/api/plugin/ap/SerializedPluginDescription.java b/api/src/ap/java/com/velocitypowered/api/plugin/ap/SerializedPluginDescription.java index b712d896..cdcb53b5 100644 --- a/api/src/ap/java/com/velocitypowered/api/plugin/ap/SerializedPluginDescription.java +++ b/api/src/ap/java/com/velocitypowered/api/plugin/ap/SerializedPluginDescription.java @@ -35,11 +35,12 @@ public final class SerializedPluginDescription { private final @Nullable String url; private final @Nullable List authors; private final @Nullable List dependencies; + private final @Nullable List provides; private final String main; private SerializedPluginDescription(String id, String name, String version, String description, String url, - List authors, List dependencies, String main) { + List authors, List dependencies, List provides, String main) { Preconditions.checkNotNull(id, "id"); Preconditions.checkArgument(ID_PATTERN.matcher(id).matches(), "id is not valid"); this.id = id; @@ -50,6 +51,7 @@ public final class SerializedPluginDescription { this.authors = authors == null || authors.isEmpty() ? ImmutableList.of() : authors; this.dependencies = dependencies == null || dependencies.isEmpty() ? ImmutableList.of() : dependencies; + this.provides = provides == null || provides.isEmpty() ? ImmutableList.of() : provides; this.main = Preconditions.checkNotNull(main, "main"); } @@ -61,7 +63,9 @@ public final class SerializedPluginDescription { return new SerializedPluginDescription(plugin.id(), plugin.name(), plugin.version(), plugin.description(), plugin.url(), 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() { @@ -92,6 +96,10 @@ public final class SerializedPluginDescription { return dependencies == null ? ImmutableList.of() : dependencies; } + public List getProvides() { + return provides == null ? ImmutableList.of() : provides; + } + public String getMain() { return main; } @@ -112,12 +120,13 @@ public final class SerializedPluginDescription { && Objects.equals(url, that.url) && Objects.equals(authors, that.authors) && Objects.equals(dependencies, that.dependencies) + && Objects.equals(provides, that.provides) && Objects.equals(main, that.main); } @Override 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 @@ -130,6 +139,7 @@ public final class SerializedPluginDescription { + ", url='" + url + '\'' + ", authors=" + authors + ", dependencies=" + dependencies + + ", provides=" + provides + ", main='" + main + '\'' + '}'; } diff --git a/api/src/main/java/com/velocitypowered/api/plugin/Plugin.java b/api/src/main/java/com/velocitypowered/api/plugin/Plugin.java index d3b458ca..d6c8a4d5 100644 --- a/api/src/main/java/com/velocitypowered/api/plugin/Plugin.java +++ b/api/src/main/java/com/velocitypowered/api/plugin/Plugin.java @@ -72,4 +72,12 @@ public @interface Plugin { * @return the plugin dependencies */ 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 {}; } diff --git a/api/src/main/java/com/velocitypowered/api/plugin/PluginDescription.java b/api/src/main/java/com/velocitypowered/api/plugin/PluginDescription.java index 540b54ea..67e9b9c6 100644 --- a/api/src/main/java/com/velocitypowered/api/plugin/PluginDescription.java +++ b/api/src/main/java/com/velocitypowered/api/plugin/PluginDescription.java @@ -100,6 +100,16 @@ public interface PluginDescription { 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 getProvidedIds() { + return ImmutableSet.of(); + } + /** * Returns the source the plugin was loaded from. * diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 83e443e9..7c027dd6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -221,7 +221,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { PluginDescription description = new VelocityPluginDescription( "velocity", version.getName(), version.getVersion(), "The Velocity proxy", 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); container.setInstance(VelocityVirtualPlugin.INSTANCE); return container; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java index 6bd0e008..aacbdc49 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java @@ -46,10 +46,12 @@ import java.util.Collections; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -62,6 +64,7 @@ public class VelocityPluginManager implements PluginManager { private final Map pluginsById = new LinkedHashMap<>(); private final Map pluginInstances = new IdentityHashMap<>(); + private final Set plugins = new LinkedHashSet<>(); private final VelocityServer server; public VelocityPluginManager(VelocityServer server) { @@ -74,7 +77,9 @@ public class VelocityPluginManager implements PluginManager { * @param plugin the plugin to register */ public void registerPlugin(PluginContainer plugin) { + plugins.add(plugin); pluginsById.put(plugin.getDescription().getId(), plugin); + plugin.getDescription().getProvidedIds().forEach(id -> pluginsById.put(id, plugin)); Optional instance = plugin.getInstance(); instance.ifPresent(o -> pluginInstances.put(o, plugin)); } @@ -100,16 +105,34 @@ public class VelocityPluginManager implements PluginManager { try { PluginDescription candidate = loader.loadCandidate(path); - // If we found a duplicate candidate (with the same ID), don't load it. - PluginDescription maybeExistingCandidate = foundCandidates.putIfAbsent( - candidate.getId(), candidate); + // A plugin claims its own ID plus every ID it provides. If any of those are already + // claimed by another candidate, don't load this one. + List claimedIds = new ArrayList<>(candidate.getProvidedIds().size() + 1); + claimedIds.add(candidate.getId()); + claimedIds.addAll(candidate.getProvidedIds()); - if (maybeExistingCandidate != null) { - logger.error("Refusing to load plugin at path {} since we already " - + "loaded a plugin with the same ID {} from {}", + PluginDescription conflict = null; + String conflictingId = null; + 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(""), - candidate.getId(), - maybeExistingCandidate.getSource().map(Objects::toString).orElse("")); + conflictingId, + conflict.getSource().map(Objects::toString).orElse("")); + continue; + } + + for (String id : claimedIds) { + foundCandidates.put(id, candidate); } } catch (Throwable e) { logger.error("Unable to load plugin {}", path, e); @@ -122,8 +145,10 @@ public class VelocityPluginManager implements PluginManager { 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 sortedPlugins = PluginDependencyUtils.sortCandidates( - new ArrayList<>(foundCandidates.values())); + new ArrayList<>(new LinkedHashSet<>(foundCandidates.values()))); Map loadedCandidates = new HashMap<>(); Map pluginContainers = new LinkedHashMap<>(); @@ -144,6 +169,7 @@ public class VelocityPluginManager implements PluginManager { VelocityPluginContainer container = new VelocityPluginContainer(realPlugin); pluginContainers.put(container, loader.createModule(container)); loadedCandidates.put(realPlugin.getId(), realPlugin); + realPlugin.getProvidedIds().forEach(id -> loadedCandidates.putIfAbsent(id, realPlugin)); } catch (Throwable e) { logger.error("Can't create module for plugin {}", candidate.getId(), e); } @@ -201,7 +227,7 @@ public class VelocityPluginManager implements PluginManager { @Override public Collection getPlugins() { - return Collections.unmodifiableCollection(pluginsById.values()); + return Collections.unmodifiableCollection(plugins); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginDescription.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginDescription.java index cde909ef..1a545ded 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginDescription.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginDescription.java @@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.meta.PluginDependency; @@ -43,6 +44,7 @@ public class VelocityPluginDescription implements PluginDescription { private final @Nullable String url; private final List authors; private final Map dependencies; + private final Collection providedIds; private final Path source; /** @@ -55,11 +57,13 @@ public class VelocityPluginDescription implements PluginDescription { * @param url the website for the plugin * @param authors the authors of 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 */ public VelocityPluginDescription(String id, @Nullable String name, @Nullable String version, @Nullable String description, @Nullable String url, - @Nullable List authors, Collection dependencies, Path source) { + @Nullable List authors, Collection dependencies, + @Nullable Collection providedIds, Path source) { this.id = checkNotNull(id, "id"); this.name = Strings.emptyToNull(name); this.version = Strings.emptyToNull(version); @@ -67,6 +71,8 @@ public class VelocityPluginDescription implements PluginDescription { this.url = Strings.emptyToNull(url); this.authors = authors == null ? ImmutableList.of() : ImmutableList.copyOf(authors); this.dependencies = Maps.uniqueIndex(dependencies, d -> d == null ? null : d.getId()); + this.providedIds = + providedIds == null ? ImmutableSet.of() : ImmutableSet.copyOf(providedIds); this.source = source; } @@ -110,6 +116,11 @@ public class VelocityPluginDescription implements PluginDescription { return Optional.ofNullable(dependencies.get(id)); } + @Override + public Collection getProvidedIds() { + return providedIds; + } + @Override public Optional getSource() { return Optional.ofNullable(source); @@ -125,6 +136,7 @@ public class VelocityPluginDescription implements PluginDescription { + ", url='" + url + '\'' + ", authors=" + authors + ", dependencies=" + dependencies + + ", providedIds=" + providedIds + ", source=" + source + '}'; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/JavaPluginLoader.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/JavaPluginLoader.java index 5b69e49a..cbc4b57f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/JavaPluginLoader.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/JavaPluginLoader.java @@ -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); } @@ -181,6 +189,7 @@ public class JavaPluginLoader implements PluginLoader { description.getUrl(), description.getAuthors(), dependencies, + description.getProvides(), source, description.getMain() ); @@ -197,6 +206,7 @@ public class JavaPluginLoader implements PluginLoader { description.getUrl().orElse(null), description.getAuthors(), description.getDependencies(), + description.getProvidedIds(), description.getSource().orElse(null), mainClass ); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/JavaVelocityPluginDescription.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/JavaVelocityPluginDescription.java index dfc7c89c..d85be85b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/JavaVelocityPluginDescription.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/JavaVelocityPluginDescription.java @@ -32,9 +32,9 @@ class JavaVelocityPluginDescription extends VelocityPluginDescription { JavaVelocityPluginDescription(String id, @Nullable String name, @Nullable String version, @Nullable String description, @Nullable String url, - @Nullable List authors, Collection dependencies, Path source, - Class mainClass) { - super(id, name, version, description, url, authors, dependencies, source); + @Nullable List authors, Collection dependencies, + @Nullable Collection providedIds, Path source, Class mainClass) { + super(id, name, version, description, url, authors, dependencies, providedIds, source); this.mainClass = checkNotNull(mainClass); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/JavaVelocityPluginDescriptionCandidate.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/JavaVelocityPluginDescriptionCandidate.java index fb7d9dea..1de30f4d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/JavaVelocityPluginDescriptionCandidate.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/JavaVelocityPluginDescriptionCandidate.java @@ -32,9 +32,9 @@ class JavaVelocityPluginDescriptionCandidate extends VelocityPluginDescription { JavaVelocityPluginDescriptionCandidate(String id, @Nullable String name, @Nullable String version, @Nullable String description, @Nullable String url, - @Nullable List authors, Collection dependencies, Path source, - String mainClass) { - super(id, name, version, description, url, authors, dependencies, source); + @Nullable List authors, Collection dependencies, + @Nullable Collection providedIds, Path source, String mainClass) { + super(id, name, version, description, url, authors, dependencies, providedIds, source); this.mainClass = checkNotNull(mainClass); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/util/PluginDependencyUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/util/PluginDependencyUtils.java index ed2395fa..cc1026a0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/util/PluginDependencyUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/util/PluginDependencyUtils.java @@ -17,7 +17,6 @@ package com.velocitypowered.proxy.plugin.util; -import com.google.common.collect.Maps; import com.google.common.graph.Graph; import com.google.common.graph.GraphBuilder; import com.google.common.graph.MutableGraph; @@ -60,8 +59,19 @@ public class PluginDependencyUtils { .allowsSelfLoops(false) .expectedNodeCount(sortedCandidates.size()) .build(); - Map 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 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) { graph.addNode(description); @@ -69,7 +79,8 @@ public class PluginDependencyUtils { for (PluginDependency dependency : description.getDependencies()) { 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); } } diff --git a/proxy/src/test/java/com/velocitypowered/proxy/plugin/util/PluginDependencyUtilsTest.java b/proxy/src/test/java/com/velocitypowered/proxy/plugin/util/PluginDependencyUtilsTest.java index 3716f85e..4abc3a2d 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/plugin/util/PluginDependencyUtilsTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/plugin/util/PluginDependencyUtilsTest.java @@ -44,6 +44,15 @@ class PluginDependencyUtilsTest { private static final PluginDescription CIRCULAR_DEPENDENCY_2 = testDescription("oval", 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 void sortCandidatesTrivial() throws Exception { List descriptionList = new ArrayList<>(); @@ -96,10 +105,31 @@ class PluginDependencyUtilsTest { assertThrows(IllegalStateException.class, () -> PluginDependencyUtils.sortCandidates(descs)); } + @Test + void sortCandidatesResolvesProvidedDependency() throws Exception { + List plugins = ImmutableList.of(DEPENDS_ON_PROVIDED, PROVIDES_API); + List expected = ImmutableList.of(PROVIDES_API, DEPENDS_ON_PROVIDED); + assertEquals(expected, PluginDependencyUtils.sortCandidates(plugins)); + } + + @Test + void sortCandidatesResolvesTransitiveProvidedDependency() throws Exception { + List plugins = ImmutableList.of(DEPENDS_ON_CONSUMER, DEPENDS_ON_PROVIDED, + PROVIDES_API); + List expected = ImmutableList.of(PROVIDES_API, DEPENDS_ON_PROVIDED, + DEPENDS_ON_CONSUMER); + assertEquals(expected, PluginDependencyUtils.sortCandidates(plugins)); + } + private static PluginDescription testDescription(String id, PluginDependency... dependencies) { + return providingDescription(id, ImmutableList.of(), dependencies); + } + + private static PluginDescription providingDescription(String id, List provides, + PluginDependency... dependencies) { return new VelocityPluginDescription( id, "tuxed", "0.1", null, null, ImmutableList.of(), - ImmutableList.copyOf(dependencies), null + ImmutableList.copyOf(dependencies), provides, null ); } } From a08972749b83286951737dd906eb960f722568b4 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Thu, 30 Jul 2026 16:51:07 +0100 Subject: [PATCH 11/20] Rebuild natives --- .../build-support/build-all-linux-natives.sh | 31 +++++++++++++----- .../linux_aarch64/velocity-cipher-ossl30x.so | Bin 13048 -> 13048 bytes .../linux_x86_64/velocity-cipher-ossl30x.so | Bin 16232 -> 16232 bytes .../macos_arm64/velocity-cipher.dylib | Bin 34056 -> 34040 bytes .../macos_arm64/velocity-compress.dylib | Bin 103384 -> 104312 bytes 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/native/build-support/build-all-linux-natives.sh b/native/build-support/build-all-linux-natives.sh index 0e182da6..4c2eb3ef 100755 --- a/native/build-support/build-all-linux-natives.sh +++ b/native/build-support/build-all-linux-natives.sh @@ -5,29 +5,44 @@ set -e # make sure we're in the correct directory - the top-level `native` directory 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) BASE_DOCKERFILE_VARIANTS=(ubuntu-focal ubuntu-jammy 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 - docker_platforms="" 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 - - echo "Building base build image for $variant..." - docker build -t velocity-native-build:$variant $docker_platforms -f build-support/$variant.Dockerfile . done for arch in "${ARCHS[@]}"; do for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do 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 for variant in "${COMPRESSION_VARIANTS[@]}"; do 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 \ No newline at end of file +done diff --git a/native/src/main/resources/linux_aarch64/velocity-cipher-ossl30x.so b/native/src/main/resources/linux_aarch64/velocity-cipher-ossl30x.so index 814b22a60f4a95fb039a6f353480ee895b4f9753..24ffa12778815469f04e29f5d139a54b913affd1 100755 GIT binary patch delta 1052 zcmY*YUr3Wt6hHU7nz~J=PIF4N&9#PqaDA@!hQg3^mV<%8ftdyDYn zS{8)utu~C5`Cu>khaiKXOkIK?PKAns6g79r{5sdo!VBmA?m6do{@n9@H{23#>1#|_ zNQC{hptOyrwCf_uKV&83Xe$PmeF*GBm95r8@Z zkP2Gxun}q^oq-wpA+~$3`n7M9f`3a3e2Yqc31sdz%3OxhpiE%2S(14O{VO!hzf29t zllflt)br+InXgmo2gwvXuuag0pxQ%bLYSu)V0d#@@`;=xzsS42NPWF)72G}K=6eR< zJ9g;iw~s+;4?0)%z$th>iNYosI=m9piGmV9A2#0SUM^59vP0=o)d4`>9V}qMWUG3a zXJdRbNZ%!4kAu2BgUmN)-M;O`a^H?!=Gc@Xn%kz3E8>4)Lvk#PHcA#OOw)5icsmTf z8fz}+;XL4BD=4D)8a1+)8e8`*3=&gVqZzrY)QHnC;xvx>+amT{aW0L4jwXP*aGuO( zR5jxo`VL*d>r^b}LgfCIMt);Ci|ap9`5^XT%wYTgy{=jMJRh9J$TIDfM^e3Jp-g*u0Z*czEcl8s zZ?OG?U)hNIF3LpURbpH+n?7DoyaCIJT1iuIHpg68SWr~pWKF~zcw*KO;$@STbeafb zh#91vZJ?&d*bR$IICHEOWGK*KMP-ipPBxHYmSA&`F;;?mD{HekGUuZ25KHc_AnpY8 zFvmua)6PP+3{1prKk@wS)HC8?irs}z?47+jj;ymxhau@EEMYS=ep!WYo c&p2Eu6S_!|12hIQohl)x*+{m5<)3-~7Z!kSPXGV_ delta 1046 zcmY*YT}V_x6h3ojP1n2YzpeJSyRMsQ;awv#q44gCxKW_xK14orZSz4WQYeau)+l=F z;a(=I2Pq$-b{8L%^`RnCD?&lYe%yi})>eopNHNn3=A84*`Oe(Av)H!S zrYhzMD=d!*t$iq0v80?o$ZE*#g3|U=R?C%r$#r*o_PpQu^yr;N#fwBAZZs2;|3t_A{KOFk)$N+ zB`6RC;fsLr)^rFg1bvaE^}NC7VcyJq3yI8As0iWSA1LlfgFyukwCEyQk``7>(cFUQ zG%Z50u7hcgIvs`CP^LR{5;yQuPg7e(67wI?5_E0!L@Hjv*R>|-Yy}9+w|FD34a0UA zbN@B2cr<6iqxmt;8|S<+fp`_s^UE;$5oN?tL=|xn5e&oFjh`|Hv57sfxC%|U!^1N;a?rM+ zIoPZvpX_I8R#)gO=1n}gGI;(x8dq-OF>jWGnXb|LTY^9H&Fc?YP$&A9kteYOv_3Qy z`NvTi`3kdK6YFne+)Vf1S`*-#-oDEMujsvQ?#^t@j8A&Sz00#dK3U@9LEhL5+9N#1 z>MvY`z6lH7kBwN->~oo6gkdW39YX17GbtOn;ptlcdYkFUHy6kZ;2oC##Z zI2%BY!lv9dETp@?;rjFDd!&PDW!3n&#>yJgNrDwil387OhblWDRhz>rUYKd<{e8;jH00xmO?dN3rjpfKnS N0WFg;0~3>+KFsi)7@Gh9 delta 52 zcmV-40L%aAe&~L%*a8#?mZmX?c3FGRb+I0f^KWNPMY}(<>;jH0lYB5Fvz{>O4FM~Y KF#{8moIcEyc^4l5 diff --git a/native/src/main/resources/macos_arm64/velocity-cipher.dylib b/native/src/main/resources/macos_arm64/velocity-cipher.dylib index 552a6fd4772ff09bf874faba2fb99425b958c350..a3877605a2b041a3acddf5cc71ffac0a9c0491bc 100755 GIT binary patch delta 735 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 Q;kMWQ6!JoBo?*%UKj3IgO#lD@ literal 103384 zcmeFa3tUu3mOozi-frlod2}O+f&vX1X+)H05=iXfHV;M6R*}4%%^;u&O$&%-CK#hM zk0fZLu@jTbpP3y5Uw~<2GRX`*yE}p!qY_c#PUf-y{Ruus5MNAQ;K=a*p4;8p8nc<1 zo&9`%|NX$n^4_Xjk5i}4sXFJ>sp?ZVul~iCF^=KS3qg$#Eobay>5Z`o6z3Vs$;nQ8 zDjSh+{7FSUQk`*B!6;Cj^p}(K{K~TDo$t>0J@q~0{C2F>l?QP&d*@HaEO#PQ-`eMY zn78(J(!KBcjaMnO^nY*(4Jack>Sp0&ictwt>_}LPv{(Ip2i?fizH5_Wh zJ^Ooal{q;p3v!-alUuaX`Q={t#*aADdtC)7E_hU;VywU;D$&W0k&u&8mS;l;DT|7bA?nCw=Xj{24{Lrs5fER<14i;hOxFYZ1*aS(}r)rs(0>Gk&yk zRY|_-`E}FtONv$_%i6dV>sFca7@N(QMd|~!i+{7+@d}RBfS`z`sIAjg981A7p@w6> z^JWb6Ai;qL3v-2ucs`1tMEKS}l&3!2`xlA?YA5EYp&vhS^ugZhiH}V*={}$RAkrcb zoH+2uB;zl=OStx(B#Xhf+hc%t;_n0ms*7Of%M6&7I5y*lYuC&`k0~r&xAys!MRCb) z93UHe0DlqZk;)z$gYrF))gO zQ4EY?U=#zR7#PLCCA zMlmpofl&;MVqg>lqZk;)z$gYrF))gOQ4EY?U=#zR7#PLCC=$_18_iPu!SF+M5j%DNU#B7{{xS@FHQCql-mCnbzPX3sU=Qh>h_(PD?x9Uf2QeWS3 z!qM!6BMav)f+CnD%d@ZUz6_@@+vfrYSXj&KiOgo=4*$|D(S5N*M#*4!^^UcFm%r*vfgwJH9 z1)(haOSB!0F%QpW13e)uTcQDRTUNAY#~S5=SF+}FJJwKteFiJDRJSU#%xuS+LT+;% zrRgg0oXkr3icNJ*UgFRuw5j8mO~;`=jtxX`HFccKlJybrKzQyBW*;>9xbkW5PMf$4YTLCc9S%iZL;^O;_P%+l*r=(@X^k8A_E^>ue^Z(KtnHm^ZvUkPS=kzd5YmG-q3u4*gqR*gQw7_dT_( z5M?s12H2)yoN8y)XZhkg&Z7k>Ar1XIdi)VsEJ-VbW&5yrOg>6R=N_;-1u z()J^K`;7Op^$6oy_ykLqTuN8la*?jJ$Zfo@C5us7WFYdOEbwtFmTmuGV<74qi~3Mb zZOQr?X)E{{HpTeLtbAT!Ws@yg?VKg+9NOGI9&^PUXSzaBnYDtxqn;k=``DIRe5W~Y zczrVX(9rzVlZTs#FNv4GL90LXpw+Iwk5-p|J@UVZR_}wOQV$%R{QGbuUi;sJqY**q zx>e%UeDgW)=D{2V=t~JY^TB+tY92n!+y0ET?rUj%=UQ%J9(H`aJK&=i zN*5GX6CcuXd39x04Azw-&ZmFqo0(U3l*?rU%~33y-z8f#jSI>w*NUYa z&$3nEyH9wP)gl+|%s0=dMX<=3-6A)5#5uc}<80*>;y}Y+MyO2I_DIooQ&6@n2p3yx zH%th9bAx7SEGIfnW;|p)z*&YCZrFIHv+!zJN8$7`v!0D9GH9|6a{lI+NS56%_{5yn zv%nJ@#5XqcKGqH&m9<3Pwr;hYt;<9m<(#kCfO&oEHn!yK%PhVCWik!o;7kNHuR5`q zXCGJrLn7uvT2nTMv+T`>W?0>2ImE3fTaL2FBAjJoQMQ&-owx!yj>_U(aZqME>O`PA zS3}0IVvd?j=>`MZGDzt&DL-$?!Z}tce=PD-J+YLI{J=ZXkspETk@5?cESlG;o>&CR zU+d+p=Lw`+F&~y{nI?&snQOcVAF7zeCi7qfLcgGCQ8V=k^~lHv(<0dtHQ)9Jbzs{c z%zlei)Yl9@mR-(GH1l|FL7(3RzD9tr3arUjP|v%Zul0IYPHQZlwRqCE1{KRDJT{GI z+3x@kaR{B9?}=jcxfZxs1eo4Izv2*Xr=xF>5rWYpq@Ac zY9mgDHc%JN?n-zn0-g?{&L+?U;b{SI6er=yujLcu_XVEzY1xumuI&%C8p6{8Ri=zR z^nx1}!CCGKU|9$_P&R?+upRhw!s2@y7Awylf(#T?i}864?T(<`ULV4%Z_Iwvs==8E zSA>8Tf8PIuKia*5c018-C)%y`Gpg!2mDyTh4QYWa*zdz;(|9Z_Sd|dN`E(RBpUlqT zXU;5wjC6fUXWglSl7wq=f2*I$&-$)B)j{&nyK^|W(D}$5B%N9`&rvO z_`41K-R8mHZF0_T2Y(-Q@i+0dSt~A?u4J}O@bf87TnE{1T?fb3?H?<)c7or31AYho zG#PSM+Jiaa1oABg-#-ev%Mn8RqW}}mS_hSDAQNNGQOa3%y`VDxbe1*bf}mk{v`e1$x(_9(tPaB>I0D&)BhI zLM`g_Gp}m3qaHtVY^yKo1PmHq)QP@I{lrxfSv2o`DfqPP!uTp!XKlj?x1tm%!BIqt|t<6F*;Np4;+4!W949y+fwG+*!*hvp%z8*_TkRB`B> z5Q;TH9J+w`MPXvg`Oh0@6e9JrwySBL5_;g<;Trylv-0+0c;3T3CSfLZO7t3!SA& z2pRl2*2bS>Z5->dHX0z4YOyxHfOV19$2>gO#xRI|maI^O&HMyujXdZhE6t3+8u{R+ zklrNbNcT2K>*7D{46=!|PQI3GgDz(Sp)_Y=txU%{+50ZnoQbtE9qVN8yR<&aool7Q zhMYJLYPmd%4fO>>R)6XBtQxG7O^`3^r$M*j#&)Q#ZqKqxYp1H8U0@}aD$f3Nd`+FD z3hS9*(bGDb1Xyzf<`_z1Lpot>$Ivu!C^|}XQ2uT~W$zYz5R?cC1aAZ{1UZ7tp2R5z zA~6q@pr7YnOKZLmn%aCZNpui>dzV&beGj;(0gWZe#Q`5ynWc;0Tt_r`Mc{4IQ6>{K zw#76p0dzXhfcdZ6JJo(JAk8jgkx)4ziz8W7oR&oupAVjEe|DE*M*c2E$zD8nlLhvN)MNS9~#ip8b8~L*5ym<9Uh7t5+5?^_t=-fh*(G zfotND)z1}c)oY87s-KIiRHJC+5aj zjiyJ+3L&rU3lv*dJ;MTPH;f7W3C;C*mTgck?U?(Rbv5SseQ62OGu)q*(mDfq)x6Lj z@&zwC!hKk2FXXOGfOi_^P&4P7QT_~DqvPJJGXo|ww=Lu>@)uzZ<~ZPvmz(n-zfjtA zJa0k6-=X zb*El@<2Byj+Rdr0>~v0RIMbLbuts$QmIi{4ab^zjdXg;_Ec*uJq4$!-!FXPMq7*XG zHo!>o-VMk@?;Y|;hfG9z{0)Nv`3-mSLnb15?*?R;_YQfaLyjRmejDa{20KJJlbO@8 zF5M8+F$1R6gAIkN2X8>#yiPMV4m-jVxSGZa47g^;0W-ChV8Dn{jNT?iRU zibIAf*RoO{jKLNJlFhc53#B!Fi#et>2C$F}5d&Ca080jBtr)R9$z*(Y-otXoVQ%R1*yvz`PGSxw(7i=8C4ZUGP3 z!8`u~9wJ`)F?h%=H!TP5Bs~n{r21R*Fc*KLjKtsF7_;Nx%bDQI&BhnRxGp z{LzE49|C=Rj`v%z2Y(H^n+thm0P+dR07M_O?$JC-b0^KMB)8mxjmIrNTo5L-{P9w1 z^N!z*%+3M6XbvD6BfedDbtIek_9S?AYhh9AS?DXUwP)MG-zUBNto!fb?{?to8T5A( zWLladh=-4Xho^yu_k)K!p^uPWV?`UJ%S{6h?*|XxP6rPYZ%+dc?*|XxP6rPYZ%+dc z?*|W4`ZcP@rOUbbkNT@e`=rB_xOD`KBk3ljmmDZ09nPsE+$a$p*Zwl{K|l0_>t@m6 z))maq70Mx>=Up7RZ$Ecj7?Jq<5M*T1r;=T|59u!N7Mc^zf)^XWi=ZRuQcfNwU5eVE zdK=G;WM3v2frH=T=^o>Yki#xSG0=n1-%E1pdgh3p=9E>Vr`?fN&%a@4ZXDwwr&`74 z3xYWG&q(XWSPsG7(1YhW(9U_#$IW1pS3_DZt`{A@_D7z5Y0U?DpMH{6qamv{y+1O$ zNmw=r-D;Z-@+IW=A;?nwTJ&pp{SP3MJ_FhGpCF@tAF}E)$god0Lw1#9*wa(l_FE;t zLOO@J|;qD8|RW|gJJ7& z%d;+ppv zTPw-3zLG4vyn?eYgDk5!SDi(;d-%PYy5$wjzMON)wVMF*g<0ZIFYH0sZ*bg%d@)~; zbu>k~WZNppwpEaAt03D}LAI@eY+D7{whFRsl~cAQIx9e*dtYUaJ&^tPK=!}%T59uO zppTrJAr5K4-?iY8yX9M1i#zX?ly8x9Uxl3e+I{5QxBm_~xBl;tb3gt&3n2;~0W*#5#MLWZVqMxR){JBWlb(k3x%Ww*hxGLOq|Zls#(mN=k-p$Q=?jr=yifWfq%Xct`VypP z-6wr1(zEZA{y5T~xKH|%NPp@Z=@G!Sh_D>NtuyI1_$<}>$&AgexrB$VEFj1bS}L9e(U(SueCBw9@ZtrDFkx%4r`J^EOZOCM9*qmL;(^f6C5 z?PVby$9WlKA7#Jdy-jt?V2|%*<2sTAg;_VtshjHZI$FpFX(FOrB@&WZiD=HCG9ck z84h_}0bJ$4K6w&4*C&v3X)piSBe0tT-i6SM?9kB;LPyKs-l{9Y8qmbmKu1%VW08k! z+~M2=^Et93Bh8NICvUJNmtp@oDd}bkNjHOhe_J=ZCh2Ckpqr`8lcAf@H})O6Sv$ey zp_@g!bhEB+(apNPMK|mE7Tv7tztqjb%nA?Pto=K5vv3|dJ81MWWW8QVFPmWI7eM!f z+_$2n?Nbx3iC#gU=9`#LNf$f8 z`()DC_R!cW#nNUxkNp+8n5n?2iS9eV${l_4U<(5+NctFLh#~0a$D#Z5 z!`?~b|1e~uL$jcZG8OnjX^zF-R}yR%{|7vgSXCS1i!m;SPIT78W^olZfE^zEumk+C z!-F4o++(xwM}Ns?VS;Q+JRt-^7XY8xH*iY>z!L+7@0P8`vj@BpQE;Q|TH%bcQ{aub zf|+I4=Ehmuz#G@W8*%8zY3N?+jb5SSH)@u4Ztx2I5Aen{@WyHIMqKJ+(zD*h8%cbC z8Fq&3)hJgF-Y9^cw3XLbyAX$6AS3AqY)vO;tt$n5v3zYvI14a;g8cuAajge#4#93g zXpF+MgQ&th(l;gEU*c|bV3i!E9 zX1ui;c8hu@NOp_KOmwUyxR_kBTZF&D;uT7k9Vv=~`Otgv8GoV*x=JNrBN>qN|Hw9^ zL-(O?l$KB5q5JsKH_?D{2>2$YL-!%Sg2*8gP(pNx}XZZ={MuPo5{UUnd21l}^Q1@HVmyW;&QqRK7(U3FG3FBg-3i1Y5 zn(_uoCfLb@69Q}?m9T-F0==IFujC=$SIAcdoUADLQNl{oj}iz6r2BjZxQ zcxW&dmcmllj?|Jq8vm&H`hb@Pc9grvjr7sSPzTWpjXRCaG0?y#7?u*KhNH(<-g?YJ%^Y6I!!?2}`G;PZcAars8nVs;dISYQ8<_%eMASb~GcAw0G!d9|( zWERA<`oJcX0h^EyY(nYyR)~3sY%hf#^H4wLphD0(rPG{LhV}aK?I)w zyTyy>XFV5YuEd_AtA7c(*gK;2qX#}d*tfynk&`~IfId!u7HF^MS9p>fO!FzNO+~*M z$)>gGF8c%Mnd}c&CHuqOYZ#69&!CrQLUuO8MrEGwwo&;=HmZfF%VncF?P;URRH&>) zE*n)5Y*hPve62+;8&wf(RPdoHRbkx7MzycPT31wHmTXi-1=CvhVU8vo?gKuq0zdnJ zAM-Pcn0>%a3-$!x!Jgnj>>L>4&YV9C~6uWQthK$-TfI_T90!%z~~g zODMYvd(nQ=kIJskjkR9GocWIF;W9titM+5gq`C5UkSFYT{tMUE)YIG! z9QZ=kxyt)~=iFWkS*PA5>!8eSd(~#xZ%Bq9`;8))Wv7EzIwkv!;>6Yf`1>Jm9bly~ zr}6*47T*|e1#E+Ed;?GaFnp7J@E?WmUaWg+tb4r}cLnT&^!~6R#!9xKv#<|Fg64Z4 z5r_IfUu0is#C%DzK{)J$Rjk95gKr#R7xxwQs!BCgH{tvF?|Ukr}~*MN@#B>P;LxdwRb6%;Y>R~$TDSTNY@ zvd_^Tey_(Kz5;eRl5-Ay;Izx#%l{2c*fT)g;h1+0fd^c5CY&lPkZf|K&p>v9ZsVMH zdVz;4m~V)dX-y&={ss1~N9|ZU*1H4>mZ-{x*Ya z_O!ow+T8kq%fHs{77e=_*)x{AY;WBu;t=gCHv{I|wl~_37RLQewzscf3ygHz-(F*m zU&H?PYuMi|fF~s$f(>pnXx!8G=Fa1`yLCYJziWSa)b56T>~Gm${vWiv(U@Jrc#(cc zvPLh))5GR=;I_>T>HjdB+iNbH8|wTDa(W+ZY{#K{^?_C|lI=~hxheXg%V*yNy}tum zQjFT(U<3RAukDS-@qgU@26}SY-&}MH`y0~!A@(;9n!ODAtYJPKeV|>??QJ_;2B)xI z1`WfG+))l&oXKU2y97Up^<;}f9sg)s+z9+7$QBptvc>uRO}4nOP={oT3zPPGugpz! z+2Wiwxo@+@^_sui7PkO-oVK{oyKHe+un+pb%NFPT-?YVrnv>Ujmo3iwudu~=yKHgZ z-?YVn?#LE*81t#y4rhQ~Om;XWo^Cr_Kjec#lhZGv(DY5ei1{wRi23k~@U+D>!RAAD zxF!bu7rHvh=#8r($3nJ)zfgZN-re$fR2O7X@J&1R@kvHc!guTGk!<1_w~Veryc_cc z?aO}-AHzRmFMkkwgoBb?PQLJC;nO&-51iS%c(qj=I&A8cK{f88M@I~n>ieUs8%Hr*81bSJ}pD5b-uTLoRY8uq?w z*!%K*Q&l9ZmNS3Jrh5ZA+%@R&K&LFBjw|Aa4TopX|B$GWfDE=X`q} zbI^si=Qa~u1+eG-ACxCNkUiHu4m@C>JY;|Df=yCD-P1XB=FA_3EqxDZYCZtx05vM| zPxSfYNcI-&S(FQY=HgjfLJoq)uUfW%#EuO6JLp^c66pIS+ON2qzK!?LH=U>O!~R3Ai@uw%mwsozf|K?uy0Bm2 zrtflw{Gji>z#GwbBKq^ldq8M5{pGRj>m-%VgH0pzpJ=&l7z=3;KQzu${%Ylf5vhn#HGszE^<0 zrz{Z%cY{9gbAumLV_eA}YXxX~N|#4E=z2V*gRZA6G2F=yx+Y)C6`<=WT^{M6YvO}y z(B&(j%d^bS91VN;1)M8bS+HX8In#>43xZe6MJ7KX2VEWkUG4@S>;@lH`=+UgpXyzF zFaRFFd4!gF@IhV>%f4U#dt82&^=Ky&{iAkX1|IeltVlpx3Doag?CDj5u8~KQBQ9Vr z{v&9Z`b>UVgmc2To0dNT9UqP6K_Di;+4fuEk*>IMe z=$z<~_V!|tFAj7lojIa?738CHQ2ww(YB9%BdHAP&uoZL|kMvli$ALa?=SO-9<%fMz zi+T5Ue$eL>%8&Fo(CzK~NPk4CA9*n!Q+^Kf@mA0+=~uC^XU2h^J z>5}H`AENJDLFsruyr=LMZ z>wfZQBljI2?)XUdKD1Sfx~oukIq?zB^3WbjT;Zz071AD((6SZq6CC8dii{XL+6zDIF>mx>?*?bTg3of^=0ljrIC<;}=#>0nY1|!n(ztfw*$X+rY5z{R%l@r{ zJ(p}W?){*J4d2`kT8=sLzuFHPzBLjI+v+wQ_JefT4^l;tkGoH%89vJkz!$YsI>={v z8_Bqz0B);XWhGf|lTvAo#hwuHa4hzOnsE*?7JEWc+_fj9#5mAcY{I<)3vk|vzAZ58 zTAN{;$G(-O8M0k7_*;klAlREE8F!yc#w8!FD(uB6{V%Q2BDth3=aL3em4 z*tV>k2ytNP%$hn+U*C}tE)i?NHrj6>U*AJ)N7fO*@gYCPnvQd6IS2V6ItRw!Yx@*< znD!ig|KN{97;OE$u+KC2+SVk#Cfx@j!auAVKD`wcI8%T+(sS}`EcX#xH~f7mZ8y@o zp*!P#5$Rl9djC^6|2Ec^o-)?fE3ly;=*_p_BYX*aIX5w;m!^6(cVq9uy)Qv~5(Th* zZQ@lOKJW$J>^c*^31`B4M+}L5(5(yb6TJn1lk`*_r|M8dhz>eyCY^obyzLgw3qg(` zyZ>3XIWGJ3lXvzTynwqA&=T1JHu2*+&hlgX;V*!_uCXo7yDBW7UWU(~^BmIi@Ns+I zDBeDYri(RuQbE20_STeFV2$VeevNL()Sto zPU++u)D9e&FqWGHw(dBu&fMh9)@>TYY}e<`vf4mLH%!o7P4Q)?62q4cR2pnE=FYHc zk-t4Ld{*3Ct(D+tfz16m@am0p4uMA?e7HKo@$5qqI541 zPYIl1?`mBaFJ#68?>Y^$<**6nJghSV=z}MG!lU6!3C2pudpA&~gs}&&16McD@9XIE zCFDO39zKLMJGGi=&Or`Og*;!+#=HE6sIPhGOWq)TlllW0ry4Q{-RqKre0h)|&H*PY z3i1*@D$J97hT7pLk_=fK-zB-?9PU-giQFvnBfzHy9<6^2)X)}VB*++7Hne%*-Pnq07Hh_B9StcuF zfFywQevq)n0&+;O*@;G9RoF^{M)kI)?Q zA&&w;P`2k_k^ zEgyW%&%HYhHtP@hXYWqShc2=jd`)*vEcRWj3Ws0cIn;k%f+HVrklqY737Ryy;@w@C1UZ{ZY^hdNB^Fr%TJ=1GR1%@ZEv2 zP&{oet3jO3hwZ`h6@+DYchjre*QF76dy%h8J$zY`@vZ=mNq#QJP%qY9Pdj@5$FQS= zZryftGvb%L*-$t5w3_6oK=w#CZ1VjiKjHZm)=sxQoaO{t=P!ASL(`#C(cD7o3C#zM zXGgNb0h{N3KIzy0H=YxJH7)gTJ}3TaTH4=yPJCP1+`svp__nlp|6}LGJ;!kq)_>9y zdm$^eLT8L|oe!7J&LEBU@zZ~SF}=_EaIA6C`S5hu-=aOvBbbm*G}8-T0R`@dA=>N1 zdlaY0jONB=UIwkqKbcdOp;UHYo`~N`bPsuve1CGV$8!;9_-X{7j$X)umqs|-Evz*? zQFIp1r^O4fzDP7VhR*8YJSOa0=kSz=K#oV7;^Xc5bZ(+m3Hez`@-wHj8lG;~_XUeX z5hTw!l7}NSY&d;Gnj>0AKA1RHrhyDh@-YW_7;yID>{`wSoZ&@3`T)~0UYXgu7I^_9 z`H)Fx41t@oe30|~G3L+$CW5s`NNtvUnAVDp3xMk)*2X_RCOQ`4doSQ)fRFQlhq>TU zxZwf+xnT*%nK_c#nbM-~R*91Q{m}&I7w9{kuj>wQrnMoBU?H3nU+%!XL_Rx;3New^ z+aewQEB&(OA(?#^iQvYqW82_8w| zy1rN>;2a~)G722-Jrr6LzI1kvm+meZ!5lL(hB*p&nf3FXT=U3ImZ-qpgy*A}jqXIG z`yzXHiiznQd#HB|^fTxJz36A3w>ad3wf8dg5XfbTl*YI*9mDHW;p3IkjCQ4SmEBlt zdN2>u9hT=IM|pndfPeF3m5m$Aq=@>uOx*egO5AG)$D#0%UOyxmrfXA3o-`((j^<*;OG{9%=BgtPa8gvgiF0psy zU{*mOYu$nPmzZZQur<^DolGVUy#V~v{U}T=I_TMpJGY{tKPrb)&Ln-L9H<1ZH=!Kk zI4gZO>8zg~SMUAP@n8M!Pt9BY_$S4N<3CXhXPi+eakrf-&-KTCQFL_{SJXd^FY1dD zif-Yauba=w0hLe}QqJdoRBNe2-y_A61Bs z#MOpoe=qPV=u+I$IcJCP=A7uz%B)Q2go>!jtaLmVoP5GDKlcIK!k(!%2iKvVZ&5g1LBdm+vAymWc#gri+(uCqFUIlebM zVB3rDH&AEb9yRXxP}AKYB?G(EC1a58H>ENw+jMn~pRO|N@uDO8U*JBE-y+{uxv#1- zjcLYXKKb1wnX&AxUmBMp-)AWQA@2S7C7x@4_Dka%Z~emfB=@5Im%?`7;Pp9vQ=RpF zh&q1!)`!N~XyZ-PIp)=$8-IZ`)9#c+-6Y%tkh{cD+>nyk318?%&n$7Q{^yiLwGY!Q zd3uRs?XTW5F6SQz!~Rfcry(Fzm*^Kd25`&*9G|1TH{N{T_{1~o?O(rO(SKEC(Z3H^ zwgaZXpT2K=|IGu&ZHV9a*#YB?H{UaMN%yCyXKY8?|NgTNjQ{@TUgK1}9|aD^;QixW z>KU`~+>3lifs@s5?lJxmY4NBd3*~nRznL>;`yQkCk!bH;YtawnSoHW#_qdb=$vaw;Fj5Y~N)}#kYLCzyJ15;~2!n zEvfeIOpE^fV-|ff(zR0EjmAj4uYMcPy^0xHlzD!8gVBIz#9l?o(zhFosaxjSIafME zUW2|(-xJ_vpSgX9QAGT8Jm+hpZ7GH}{rNF+dj!UULqAdPjJ4Yvji|FEc}u$e+*Gt3 zYSCw*eulP_7g9cT$zmx^?V_Gs)T7U^pSKF%sE5j=OwY9Q5-g3VgYdY+7=h=X@DyJ; zV9b35W^SAzk)7W2={xE@zrIcHe&6}VIy=2r z(|6njv`aWW=g#|YZogm1Y131BGV)wl>wMSJyV?29=zWpLdy+?Psc>I;WD3&aWo*%Wji2ot(jz8`_CE=2`Yg=x3o*wV za|;vgoWCj%aKDfF{Zr(50QX@Lo#dlV!UN3#L{pWZUoHAL9cPg3SVO3<4Jbo>j8Gcv zRF>}KdLS@At{v|TbNF1e{gHZQ@$R2C8s*#ff|p`!3wOuZ#8-Ap_>!XzIqp5C-vL;PO2x$DB2lV;wMmrfUcOS4>dng)r8<*QU+R78qcvFSE9#2` zZbf;H{Ubc3z6>AM^H>Y%J>ueFJyWlc`Z&4ou)YfT%>QYFQ9wVX?_VC)M}Y3=T|0DG z-%V@U$}|agls5VIhxOIK;Z7QVz(Rdk3H+vT?fRFoE)dNV97O9p(xo~F4(sz=utfYP zwVzvTkhklp-_rN1hxG*W4wUgj-$Xq9@!TX#=(r9!GHDVU(7pFs-6CzOW3j=UsE)(^ zf)59Tc4`7ti>4qg?VdCp(&i)WL1(_}$hUZUs$RR^k7*Fe{}i&C{H z9{HCb|HIDwi?P>yEm0l1=pmGO9A%ImkM^!2Z9m}Y6iO{Wr=q|%Q3*%!(P+kjLaCnMX+B>yAqz`TXaMq?e1sCk2 z(Eg!rK2yZ>!pBfKmL=qb!uCk@sF*Fu8@d8)IFhaaTiiINpHA``V~S@M>uqjvG3K@^+NBUw%`wv_|O_ z)IG-c_dxeh0AFOQG@(D61ZBrHAsGGfGm{PT1n$7@0Q?KQF(%0Oj`ou}+;!Gs=J=My z0ZfB^j(#3{8q=`fX%7GDNk=E@ScEzjqmF>cQ*~6f zYyXpucrV$KnUSaKmP9d4+xt&Cm<;DqBf9FuN0_EU1AmK%wmQQ+7~mn>nzik9ID^^J z$uR^qf~KW|Q%SHGmpWl-#My0v|cBu@zuuH)l7WfSH@Jhi&ZrlYen@cwN1XXGV3E= zZv7+lnU5iV|8@e?{P;#rLLKaRKb47zKU0W_tp#NXzbbe!;a>}0O89lb#)OkopbJEG z*3FChxbE$!-`1UsI$5WJJ?uOaTF&u&O9RKXbmF44YBjSRRkHyD#>UK4Etv?@5a18h zzf*X;wGH~=biiR>skZHh9q~G+?ob1sldx}}#F<08X?21f9)YmNU)+L`!Bq7zMvrf_*eENt}DZXV#K zv9XnIc-=gutIj61tC8Au`a}5j_rN}?#5m9%Kt5oe7R(ZAaemD_oPLJD16EfRij$-ZJ@icDscmF2Y_ z;bfU<>+)NxeVOJJ_~}(g91W>v@|cV0=Wbth*Dm-PR3SYQdo0OJP@OZyB{<=f4K!eE z-W&vE<30kJc941$_;~UX}nG+CX z2psJ*_E);_%1Qj!UdKuN$4UIRJrK4OHO_4N;_gP+mVVCB)YMfmLo4yQ9qJh{a{i*+Ul6p4<_{^iCpPE1l~(^05Baft!QOTmdPRx*DYr17Qoh`;#Dm-(5KZHhwp-OJFl^R7|CoKfk(n~ z)wkgJD|}Dqa4(tmbe#%(n$A6ysNfY+{gv>=AwE$`{CT`iDe;NY3jdZsGk(LNz=t{3 z`M`E2_~2aQeAr#mVSkwiJIq|zW72T0F}1lbDg}0$WY}vAMb{rYv5IUwotK}ob!r~6 zo$?E{+VGnf8tj)zJaRzo|3%YaHBj$-OYrTtes4Q*akE^+Dm9Yr}L+ZmoEDt?lu*)(!Et#KHjXWA9b1`m#l)XYe5=ot>F|Z%3FcFBN$_nAC$}z z*y6*M#C*T7Rrh^Us}8$>ozX8~6<>ZG}y-p_%@PjT$Q(x*#Wmp@(HO8&stE4GG^FX?rx*OU$)f|!H&&67&Z z$EB$A8tS=$Jvu7ihqP-#Kn&gcNwDPM-U}t#+yoy}++!rkJbrl6o|Ql9+`6o&we#_D zfn8jH$_`($V(eAFR#@D6eF_V_&S_#^D~zMPWm^WP)+BMNe)0{*{;7uAf{mKsj(Kev z@}rJ&-2ZMt``cbz-Ku_KTp;a-Dk+Y7>02ZEwgv6IhrF+$j&W$8o|a`Ltz7oFz=No> z33+VD6OZ!bZ`z19YB6S83s<#vpg0T4wid>=LTpg^PR3ofh}XV2qxA~*D#<6E z@^3-@1IVx8H7YyG*pX)o%Fv!B-SvJH`L1Dqd)3o%tx3R7AN+o&qwlM*mu|$E7h^Ad z0q$V{4hI;%VXwQ3WI>XrMqJ}`4db*LV?<*T&TGt_kg;f-fd7^fq}Ou+=5tuvOP-$I zdJ1DiZBtn#d|YX76!epI8Fv4ir*+aDs!7~9Gk#AgTZy|KGU!_XbLh~Q&4oB;gg$I8 z)U_(GhkpU|(}!}jPanI^+)De4psOXZ>tb39fzJ$-nMS?_!22}#8eos9G!=8257KK3 z3tO#N`^=ATeBD}TZZ*T+P2;=j@o}?i3u92%INS+4&YXgA1HCVaeY&vqlaZ0xbjJkU zTlAKU%_cg6k6{eKK(u`Y-!6lG(N0$Z$7WB%ZwQ%DX3Mg|)&+p4Ws3NEL19b+cb%K> z0pDW&!TRFdQ@M8!f_NT%|CHdPewU+9z>zIn@abN0iF`jRPBp#t)HXsHQ3jwf!w5+!qyl-^U&$vF34D`|ulA>#=upn0{9ab0g$n`h61xWY-`($1>bgxwaxpIjrk4?GFYG z%LdM)4HND&pfQ>c7!^Z0+{3JB5zr6Cux^Pi0{XxU6RO0$)^%}RNfolvZlv#kUuG`uz|7kqzJ4)~Z7+asQwV(qGAiw3sK{PxIaD=g z)8m!slhSJ8IlF~tb{~vwe}E-x74lt$pDOv`6wl3aK+e<@&kc0=7lbbD)N@PrW8S@n zdufw+g_&UN*-UFlZbp_vD`T46j6jD0`ZQM%I;#QgK)+ITPG=e_8y<~&qCg`gKPE$d zoCx@CfW`-aFZ#Y{cx1LD>w>4cL2u+6PjyxlMzp?5bpb})iME7e{+UUrLp!rJBriG3 zk&HZf$$<_Y?M9;AMyXv+MSB@q)SCp^k=mu-vl8J)1sgyn)ir=N=*}YOi5g2`yt@sF zk2L82FF_wo9QKljlg||VD%ha_J<#tr)y#MPPSXo$Z#hB_@J+wvr2Wgt+s7Ui6QjM^ zL&qSi7l0O@#P^5L&tnfemGxAOW%Ks_!Rmc z=Zyanap)jH#~{2b}Ro5Z6$AoHHJbxF5wM zo$;^H7n19bg*fBCMVxr@SeTfIds0hl5D%8uguL*JU$LtdV-##s+DSo1 zpPBP?$=uYi%1OS)$gm(|-lU1f$z(gk{ffLyw9my}!xV(Qvv(Drp1rg9z3N@XpH}ZI zuCCr)oR2#cQ^F1uPY&B#>^En=E^?z^sK1!1JG?;=n#zRCJopMGTdG5%HU@;sHY!3_ zLRN>LjXB@)a)@jK?nB-v3$1=hxwQHvMd*~5WJ|qRbyhs&>Z7=xDal7HoocjXHO`#( z;>$1X`N{5E3(xGHSs$W($#*H)gvnO+ENof|KQ6SL@ha?>sv4LK_oZ-V9y*0#&P4x6 ze2<#r>pyu;kpC3Ky$}fZT#Z5J)ihTpY)jVp?wY5Qx2EV+AEfKlxQ933$b6jwW8jTF z&oMJ`W{CGzF(wXkLEO?9@Khfit+UanpMEFsi$(3yII`?^z2rk2|Al|0P1CJ$r3!k=s9hr@UALrVAz`Mi|on2&Ww`I6ib z4*xtQ{E~GIkJx-7ELqb^)m^!;JE0&~C^e}L`1Y!l8ks|>#F=9Q8}u2Q>TGwcOxnL5IV|t$ zi&A6HSpyv>5cE0*bQ=Wv9gF#Y9DeIV4&M;(mOnm%yCJaeysK*5Aj!>NLT>KCeZ^m6 zZE*WkH&q1>D$>#&O)2ABgLaU8EU1Os0Y6pT=d+a7Phm_9-Q67ndrAi88!O}ltkoJT zWCE-i0|v~my^rSE#%m&Np1*@~0e7+t;a+4m`Oa^}orlfu0{j%bhezH%hkIJigTF4| zj${Vk#vbtTc`L)c)6nH^jl50%+PiSSO@Wt8(|2Wi)+YEvhhzOzVEqi|#+XyECU0tb zGt1<=W6f2-h_yqL$@z7ZYn;C`NatW_o>=we#@9DySD2 zbK$M~D-eM9{YvJrV{ERYJoO2`N!1Tp$kzG!So^R>DL5bU$+7vMY|P6{>U%r#b>oih zrm80g6-&|`xEJog`t<>}NAV;Wzkt8@Im|5*PA!(KiNGmnKM?*qjuhAzd!d5{K?m*q zUY;$)AOr40VCx*;QmLuP3ZlIDy}e${Kgxv4EP^4N>>F5H`hahp=0V&$1bZXmkdZ!Z z0sdfrInW8a3+yiku4}z*D%|s1jyW%rvxb<9CN5R2m*h!X5+{>vV04#Z&x1$wkw1qcCx0u`KL>Q8w|p z;wjjJpnZzDcqZdX`>EKMm~miN5cX?=O4Pv|_5~FsGZEKP+?&}e@cjtlM-i8W3iedI zC*%EDJY_20o`?7Ic)x(BfPLmDyi?g-cv78SDw$mt>}?mkz3e^7NA&0B9??eu)(aU& z^l5m$Lhu`%@N*M)7V{H#yWn2&t#I#k)$JXuw66rbQ(bL(dn@d@fcG%s1g{XPvcsRE zWG>qM16(RfrntV#y#uAbq##bP3#zdMdx;yrf_IQKHg~=U+aq0NdGB%do;<+(EMP7? zqQ6jlL_Y;^x?$cX!Myu6%tRM=;GAG58o2|U zxGf>8%&QXK;e1uK;F$&M?(huF*~F^~18+N?0RmEvy|Tbu+NQj z#jFS$=f~N3tX17N(}z*7Kk5xYy>TualmG`as0^>6x*Dar__5A?M#U_xjlt(nfwPEi zcrT-08R%Cg@DhvP;iv~LR^godNxau`elA={x|Z~tusy(q^cz<218b~$V(?SU)itEI zY>+KgU@w*CIQlI^`aKDn&u~s=`xo%fbI*mpM|_qyn@w}&U$e%THma5qF6mxsTC-`- zPC$?$^kI&if;*`nz@5}naVPc6f5e^C=)YoYeF)u6ZNfO3AX9{394|-0KM*kMaF4nU zxXIB&ro}zf;J;R$)%vVEb1ejJ;<5j{LLUA_=M(g+&~Myk)NB7 z7I7^%a2av@hExc2pBPhwB`XH&S&o4XG@^a7%k*JQ!&=ue8)Y&zFTHNUx<-4}xnVM6 zqYE#wXsc=hd?m2oN&Cg#*tfsp<7eH5XYDh2Hs}S5YI`1m54WntjX&BGrTud9i3&n^ zyn2uE3H-Lwlhp@|KVDgGuPZwA4to{*`LFIYroOt%xboHAM(*v#;s@XUuz2a)zbJnG z?OzrrSHEY(+A7`gZFr>t{y7cC3IzB5`f}{ES`c<1&|a%AWXBv%C9ScSe*oIceZ-cz z&eXaJd{^|LG8A?dTauq_AQx*a@nv&+eMpkh*L)fKiKIu*CtTq?LASN2naF+If560H-Z8|iQt2v zLhwcKL-0okKu{xS5CRd#AOsEfnW zMrY7@8%{Pr`<1mslN-|L@0_n%|Xb>iENWofv_K=l(1@85wv#s1C;TOH;>E6Q81QrREeuWYij zY~DA^V&1q@_SyTDO>vej{Eo84_bZ#~EW74A%Kqr?vis5B4B$lt{fM3mhv%PZk^5P# z=p*^AZh{^2e>1&cP1we?ZF*WmFpq81PJ(Y9hu;yyd&HDBeY>1r#9>{bH9|_8-lmuG z=_nu4f3CIlg@(TmzuqCm8LjQO*7E6bZQGxu_~(d6As*$7zY7`n9mw7FO8h<__^%OX zW}@Xv6^D046|*V9yS!hyT{c#*N^ zLu-K~2V*UVZ~nH7L~mOz=DWniNO7YTmWV!_`^>qZ_Y z`c-N~zeY}6ct|5IOwOLXC>irol&N=43Vb~B&|ajeX^sNALO6c6AQyZlt+||N7vL8} z_?VpLT#Kdm`840c4^!^Fi2RGBH26Hy8qQFc7Qx*gLCN8#H=X$9P#@)uT;g1prT533 z>$SU_l(!_^q3Ba-6ga=7C{Z{RYdzv?5q|;kW?$#`7Z6|X5nqpZIpQWi=l62NtDNyP zj8E}U`5F8!Q-3q=7Kv8gJvMp3r=Tgf%O{BTsS_KF&%wtt3ePG$C*!#b&nb9*gy$b+ zdW@GIpW67YF{5=VA7fU9G24kTJB%^Q$C%|~%qC;Z zs$oxAfpM!sn^7L)W^j!g!x=N)PqeQ{49@x}zX9R)_{kM2#qY;p41_yl z=$YmoM@pM8)|t;emY!+u@uak;$2s%4$J8V36xIdE0^4Y9q<#^P5Uj-iXF0$A=%7{4f1p+cu-o`z=Joye*@sZ zZt!0Z_;0nKww4MS>vDlva|KS4S2$ku(*}xu91|C6)d(2NL+}MB9wZ-8E!t`~+2@e1 ze$d5#eweQsz%M(&f7O_?NDf|sd5rk(2<9x}yHw0yPM%A`m?eSdobc}i&ozMOd@!dO zP)8)@GV-_MF}IOy%#e@r8;CE7*Mve-G3Et-I)vbNWAsEl3m~}8VBO7`;7z5h2J$6G#Bz1i%5(GkFkivSh(lH z!x)b|@D$Bkhwd{L^qUTiE}nWB<3aqi9OFT}L~97~Q8LEE$wSULkmj_V6u?6b;2|0h z5j;fWLHxro9#o$487R2NgXmfs1CRO7GtE6NlqQV@^0~*xBTb2UP>1zLhw;$ST#E5h zy2ps}N#lg{Duu(T=gDkM!ni?tA7|htJB*jr3VrKw1ZiECx9Pj# zH!r;_@gDAa_rrS>&S{X2HwN!hUFp#0x~Ez6H=z^!yy~gJZaf=Oo@^z3tk{e0BN)?i zM#i?&UXWy?nh)B>envPKVy=nAK8^6ipxfS5=F7G>9e%vE2{u2Pt7-12g`BE07bGM> z{@x^P3eiCyR<3vHfRI16kj;M$nr=edtl^NP-}1W;=~JNR%9;=9U&hnRen|f@p6`?1 z8`e;qI}v{1VR^-k6L%Yv!&VfZpSahU683Dd47xn&u4FeLTY=l2^eXh&YUsz_V>ml} zU`xDRda}2G-`PRForseyLTy1!bQNq5ZDDiT&?FAh}1l)~?6Wn~T>K+@7j~%v;8EzPbF@AQzRhH~Z0(6oH zltbSn8xE%mu**n4X$Ss4I;7{1qc4D&Y)bNzha{WQUc8H8JBtlryNZ*;_PB5k8=ed2 zFQYCAA7eDIT`5ZBuJ+{6cOn2wh3h*vW(?VBN@T9@GU!3nmq^65)EDou1bc}a&ph-a z8lyYkgJD~D+h?FNbphtz0OsEV=Klo#0jH-;Nd&xxF#MJjU?y1azzH1-_1=MFJi+M! zSEvN{owh;<&XPOvNfO+5;uG!N7*`SF$|2t!c)w{=a`Of8h@^MI-nt3)BOP?P7;b_! z1G;Ap_+_)OImBc(CD48W@lD^0cld_vNKI9bw-zw=Fx3<3=Ak|(4}F)7=mLEIX`Q_JQdvLvh` z>nOuX0EGmi1XMuBaFQHI$OcJ37Lg!`K-i)Pip+=$NYEfEIKew3vWP%jK-6*6Q9#f@ zR6s$4qw@XU?mmzN)cMA5?sLEUbmjS{Pjzi?y|s2%)xYb$6FS0K0U7g-aF#&Ez$&;# z{qKSnunsJ3z09^Casux*WL-jJ-|Lrs8siPJOMkO|z$keKK1tRMH1-4iz`05|0s5Y3 ze?x44&6VAO_ZZU8*x(})`)2y3vz){HGkwJo?tP=b3u?4MTT`5Ji(4%psjoFOuCvyV zZLT$B*IsLgGrnJ0_gm|7rcu@wJm*e*dw%7`^#qUp9m{I&Rq1^+-$mv5PWoR5{Wg38 z)oIwJ49e((+4piv-kt@ zeC}W^I)J#p>FwG2E$3{#aHU@Eoqj@0-FNysqA$MF_si4}nf{e``p+|`m-lXu(YMQc zHygS&-qSVK-_unEaNgF`#g0C8b&-Jm-_=E)jkScwcNl94>vkAx2^)48YY7{7{Qt3* z@cfQ`M_$}vtR=j(6? z-7&zwmLPLUS<6e{?0|nA;W%?lSwncdm(eEQp

Rb;cT_wN}S`_kY=1oxgqg$GQL1 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 z zGxG1$1z%ltUp@T3`YPK%-?$uakuOy>AChPO`h9?^*>5M@u3Y}rjq_Sp41t2Jdy&(ED$fGGI|YMGYdt7Uy0*R1h*zKm2g zgFL3Hjoiq{@fBRtU1jH}*NJX^aUjtZ^WR82b=>5O0NQ!ApexNRRT&9y>|b68f`R zA<5Dg_&kMAyV@_R*#9?4@}qZ2(vag3dw{CaZ=U+QA;%{B#rV`?_F8hhXjegA^$(OZ zPQ9b6I#qpb8!r{h)L~YBUTThSiZ4ekvr9dp{^S=-OVz80w+L}o*r(@YrKPGJcDpK` zK%GeRPVq|cR08FvXBADGn183Q0F~!?i?XKp3hP9jtlt|$4C-e7A%~%kkL*R61^IUl znwsvLScDAfDe7PGgrOcs^XnY*wV@%654jgL*wduA{rlZcwFp_G?bjMR2EN~Jv#Ta( zy?wt{lYBJfkbQOYkh$8(urt9&1I}p8=hM7&^AK@0py}AI?3Jedp=85; z#I!e0Gwk1+_KDLCdkgA{#1}H(uy-@<*DWyYgG~GK)rNhnY2WjhVV`5#J!=j7O4DAo z&aiJa?G;ZM_76>ax3>)Yx2AphyM{f2E=S~b z`0d{q_WMoy$ELm9wExqz?=kHy&KUkrnD$PlJpcj~N&iXHex+$|e%A2c&$MTn_7SFi zVvXUx%(Txn?dwhZe$&3qwBK~j@Gs@uko+&|Y1nTt?L$rbBGbOyw7+B8t4w<9hsru{3^KK4Gt-Hm_}|54MuqiK&e%PY~eum05V zpCfjMQVw&xSZLanY2RS_&oSeF&9v8;_3?yhUuMSVq=S+4i~eTB-_f*xWct6s?{3QD zGwr8L_xnuyepCMS{_rP^_;&c+s}1`RfA}v9`%k95%5-l<-4J<{so&1nvC19YTwBjR zte$;rJ^T22_S|~*$@T0r>)Gekv;Ph$t_A&o2gHK};J+{3NPRbMsc4n@Z+3?Mp-~_F zerqK9!24Sx@sHd8Zan@W{x{{wAIWdZkw20PIjS}8c+I&$YVCVD#*D#`{4x&y`BEJx znO*qDP)5nR5!Q8-iOi+$7f0#K2Cwa0KLSQkeR3fZ8xxk=(feI%PiX47>tnEA`Z}`t z2WY>eIp?@s_=^%=HinDN^~Kl4_9sv;^Sax`p!HlYKXIuo+-0XQwLR#Q8Gqt+1h3_s;{$Mq&SzhO>q)J%Uh{1;~Umz3f-Z()(Iut=p|;xe8(l)>}DB@Q{> z=|6Y7`(?c6`Md?G`OM`I^K#Dlc|42w6B(B?xiB*|jb_5rjsIq@H`|w*IeFZ;TyGvt zE2EZwX8TfPlX;l%d&u;8GkmnWjQmtzUWO4D4@kb+G1iueiEY7npFBtv6<$nJ7dUIW zFV#yNmzBz&Zc)KxA5zuU;kYaUT{@31>p&oiQ-TT|-#x6PF6-B#-Rk3(MJL1!03ynl|; zn0w5!?3)x%XNspg+$MriKuo04mW4&zRES~ywf_}+{672-QJwvLQkRTrR2X%vY}r-M zwLChqsN!9<@x}G2TZV_6{UYlc&0}j8^V8yX2g_$Y^Zcm%-N(nh67aVJ2eNH9jtV?~ z*XFyX-}}K|O9n6K^wh7p|E2BCoxZep|KZN9CubiyRvlIL)c)i5y->9KcbxlzlPjDPsQV$ZEU_4mJRE`0gyz!fWQc>9lorgZ9l`@$E- zy!HN`^t3})JsX^!TQKeYJ6jBPK5%SQ-|p9JJvREmQ$PHea&}wP$st?9_qD(3^{Oj+ z|IpMOt7^!It!`u^Cp_rgvY4@Lj<@sJyOyf);Ww{FW#&i^`g zX=uu0kEC@^jO%kGZsyFeFTLv*Z7Gas@!b=B4kWH0@ZpC?8tnY=_O{>2;{pF6|+0wfOJ#yF7 h8><3clV=V1U6WN4W4dkl Date: Fri, 31 Jul 2026 21:03:22 +0200 Subject: [PATCH 12/20] Player Loaded World API (#1541) Co-authored-by: Emil <12966472+Emilxyz@users.noreply.github.com> Co-authored-by: Wouter Gritter --- .../player/PlayerClientLoadedWorldEvent.java | 45 +++++++++++++++++ .../api/proxy/ServerConnection.java | 12 +++++ .../connection/MinecraftSessionHandler.java | 5 ++ .../backend/VelocityServerConnection.java | 10 ++++ .../client/ClientPlaySessionHandler.java | 16 ++++++ .../proxy/protocol/StateRegistry.java | 7 +++ .../packet/ServerboundPlayerLoadedPacket.java | 50 +++++++++++++++++++ 7 files changed, 145 insertions(+) create mode 100644 api/src/main/java/com/velocitypowered/api/event/player/PlayerClientLoadedWorldEvent.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerboundPlayerLoadedPacket.java diff --git a/api/src/main/java/com/velocitypowered/api/event/player/PlayerClientLoadedWorldEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/PlayerClientLoadedWorldEvent.java new file mode 100644 index 00000000..50fb650c --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/PlayerClientLoadedWorldEvent.java @@ -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. + * + *

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 not 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 + + '}'; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/ServerConnection.java b/api/src/main/java/com/velocitypowered/api/proxy/ServerConnection.java index c408e830..d961d4ff 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/ServerConnection.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/ServerConnection.java @@ -7,6 +7,7 @@ package com.velocitypowered.api.proxy; +import com.google.common.annotations.Beta; import com.velocitypowered.api.proxy.messages.ChannelMessageSink; import com.velocitypowered.api.proxy.messages.ChannelMessageSource; import com.velocitypowered.api.proxy.server.RegisteredServer; @@ -40,6 +41,17 @@ public interface ServerConnection extends ChannelMessageSource, ChannelMessageSi */ 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. * diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java index d1101d6a..00783dbf 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -53,6 +53,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket; +import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket; @@ -200,6 +201,10 @@ public interface MinecraftSessionHandler { return false; } + default boolean handle(ServerboundPlayerLoadedPacket packet) { + return false; + } + default boolean handle(ServerLoginPacket packet) { return false; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java index c40a7aaa..c8b3c199 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java @@ -68,6 +68,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, private final VelocityServer server; private @Nullable MinecraftConnection connection; private boolean hasCompletedJoin = false; + private boolean clientLoaded = false; // 1.21.4+ private boolean gracefulDisconnect = false; private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN; private final Map 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() { return gracefulDisconnect; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 8880233b..75258ed1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -26,6 +26,7 @@ import com.velocitypowered.api.event.player.CookieReceiveEvent; import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent; import com.velocitypowered.api.event.player.PlayerChannelUnregisterEvent; 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.configuration.PlayerEnteredConfigurationEvent; import com.velocitypowered.api.network.ProtocolVersion; @@ -50,6 +51,7 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket; import com.velocitypowered.proxy.protocol.packet.RespawnPacket; 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.TabCompleteResponsePacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer; @@ -241,6 +243,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { 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 true; + } + @Override public boolean handle(SessionPlayerCommandPacket packet) { if (player.getCurrentServer().isEmpty()) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index 29710876..b35bf1c7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -88,6 +88,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket; +import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket; @@ -344,6 +345,12 @@ public enum StateRegistry { map(0x13, MINECRAFT_1_21_2, false), map(0x14, MINECRAFT_1_21_6, 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( PluginMessagePacket.class, PluginMessagePacket::new, diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerboundPlayerLoadedPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerboundPlayerLoadedPacket.java new file mode 100644 index 00000000..8d069bbe --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerboundPlayerLoadedPacket.java @@ -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 . + */ + +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); + } +} From 2676520c6a54bac0529544793c982dd701b338d9 Mon Sep 17 00:00:00 2001 From: Wouter Gritter Date: Sun, 2 Aug 2026 11:13:25 +0200 Subject: [PATCH 13/20] 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> --- .../proxy/protocol/packet/JoinGamePacket.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java index 645dc7b6..31b03911 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java @@ -245,7 +245,17 @@ public class JoinGamePacket implements MinecraftPacket { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_9_1)) { this.dimension = buf.readInt(); } else { - this.dimension = buf.readByte(); + // Vanilla 1.7.10 uses a signed byte for dimension (-1 Nether, 0 Overworld, 1 End). + // Modded servers hack this to an unsigned byte to allow dim IDs up to 255. + // We must store the canonical int here because RespawnPacket.fromJoinGame copies this + // value into a packet that encodes dimension as a 4-byte int (RespawnPacket.encode, + // pre-1.16 logic). Sign-extending a modded byte like 0xB4 to int -76 sends an + // illegal dimension ID to the client and crashes it. + // + // The wire byte is ambiguous: 0xFF could be vanilla Nether (-1) or modded dim 255. + // We resolve in Nether's favor since vanilla 1.7.10 only ever uses -1/0/1. + short raw = buf.readUnsignedByte(); + this.dimension = raw == 0xFF ? -1 : raw; } if (version.noGreaterThan(ProtocolVersion.MINECRAFT_1_13_2)) { this.difficulty = buf.readUnsignedByte(); From 06ade4775e9a7bc81c87810f0c0237575cb01db0 Mon Sep 17 00:00:00 2001 From: "Phillipp W." <44843453+wphillipp@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:22:58 +0200 Subject: [PATCH 14/20] 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. --- .../proxy/connection/client/ClientPlaySessionHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 75258ed1..beecf3ac 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -254,7 +254,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { serverConnection.setClientLoaded(true); server.getEventManager().fireAndForget(new PlayerClientLoadedWorldEvent(player)); } - return true; + return false; } @Override From 00759e52790e85e148efc08adb7e7dd29e6c5110 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Mon, 3 Aug 2026 14:09:05 +0100 Subject: [PATCH 15/20] Revert "Fix dimension reading for some mods that add extra dimensions in 1.7.10 (#1734)" This reverts commit 2676520c6a54bac0529544793c982dd701b338d9. --- .../proxy/protocol/packet/JoinGamePacket.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java index 31b03911..645dc7b6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java @@ -245,17 +245,7 @@ public class JoinGamePacket implements MinecraftPacket { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_9_1)) { this.dimension = buf.readInt(); } else { - // Vanilla 1.7.10 uses a signed byte for dimension (-1 Nether, 0 Overworld, 1 End). - // Modded servers hack this to an unsigned byte to allow dim IDs up to 255. - // We must store the canonical int here because RespawnPacket.fromJoinGame copies this - // value into a packet that encodes dimension as a 4-byte int (RespawnPacket.encode, - // pre-1.16 logic). Sign-extending a modded byte like 0xB4 to int -76 sends an - // illegal dimension ID to the client and crashes it. - // - // The wire byte is ambiguous: 0xFF could be vanilla Nether (-1) or modded dim 255. - // We resolve in Nether's favor since vanilla 1.7.10 only ever uses -1/0/1. - short raw = buf.readUnsignedByte(); - this.dimension = raw == 0xFF ? -1 : raw; + this.dimension = buf.readByte(); } if (version.noGreaterThan(ProtocolVersion.MINECRAFT_1_13_2)) { this.difficulty = buf.readUnsignedByte(); From 71c50a75eb50a575466192a263b8fc054d58bed7 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Wed, 12 Aug 2026 10:25:15 -0700 Subject: [PATCH 16/20] Export JSpecify annotations at runtime (#1861) JSpecify annotations have runtime retention, so expose them through the API variant as recommended by JSpecify. --- api/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/build.gradle.kts b/api/build.gradle.kts index c8afb716..65a03a04 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -18,7 +18,7 @@ java { } dependencies { - compileOnlyApi(libs.jspecify) + api(libs.jspecify) api(libs.gson) api(libs.guava) From 14a69904f9d3603af2cb90c2116df1b6966ee94c Mon Sep 17 00:00:00 2001 From: Radmir Noirusov <83647537+bejiihiu@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:29:30 +0500 Subject: [PATCH 17/20] 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 --- .../client/ClientConfigSessionHandler.java | 10 +- .../ClientConfigSessionHandlerTest.java | 119 ++++++++++++++++++ 2 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 proxy/src/test/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandlerTest.java diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java index 5cb7188e..b8604103 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java @@ -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.util.PluginMessageUtil; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import java.util.concurrent.CompletableFuture; @@ -212,8 +213,9 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(ServerboundCustomClickActionPacket packet) { - if (player.getConnectionInFlight() != null) { - player.getConnectionInFlight().ensureConnected().write(packet.retain()); + VelocityServerConnection serverConnection = player.getConnectionInFlightOrConnectedServer(); + if (serverConnection != null) { + serverConnection.ensureConnected().write(packet.retain()); return true; } @@ -240,8 +242,8 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { MinecraftConnection smc = serverConnection.getConnection(); if (smc != null && serverConnection.getPhase().consideredComplete()) { - if (packet instanceof PluginMessagePacket) { - ((PluginMessagePacket) packet).retain(); + if (packet instanceof ByteBufHolder bufHolder) { + bufHolder.retain(); } smc.write(packet); } diff --git a/proxy/src/test/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandlerTest.java b/proxy/src/test/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandlerTest.java new file mode 100644 index 00000000..96e27766 --- /dev/null +++ b/proxy/src/test/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandlerTest.java @@ -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 . + */ + +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); + } +} From e6fbcc9196c110b9fb458d8fc1ac9812e8f36f58 Mon Sep 17 00:00:00 2001 From: Wouter Gritter Date: Wed, 12 Aug 2026 19:32:24 +0200 Subject: [PATCH 18/20] Various dependency bumps (#1860) * Bump fastutil to 8.5.19 and remove exclusions * Various dependency bumps --- gradle/libs.versions.toml | 32 ++++++++++----------- proxy/build.gradle.kts | 59 --------------------------------------- 2 files changed, 16 insertions(+), 75 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f3e5a9ad..9c12ac2d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,24 +2,24 @@ configurate3 = "3.7.3" configurate4 = "4.2.0" flare = "2.0.1" -log4j = "2.26.0" +log4j = "2.26.1" netty = "4.2.16.Final" [plugins] fill = "io.papermc.fill.gradle:1.0.12" -shadow = "com.gradleup.shadow:9.5.1" -spotless = "com.diffplug.spotless:8.2.0" +shadow = "com.gradleup.shadow:9.6.1" +spotless = "com.diffplug.spotless:8.9.0" [libraries] 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" -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-annotations = "com.google.auto.service:auto-service-annotations:1.1.1" brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT" -bstats = "org.bstats:bstats-base:3.1.0" -caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.3" -checker-qual = "org.checkerframework:checker-qual:3.53.0" +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" completablefutures = "com.spotify:completable-futures:0.3.6" 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-gson = { module = "org.spongepowered:configurate-gson", version.ref = "configurate4" } 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-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" } jline = "org.jline:jline-terminal-ffm:4.3.1" jopt = "net.sf.jopt-simple:jopt-simple:5.0.4" -junit = "org.junit.jupiter:junit-jupiter:6.0.3" -jspecify = "org.jspecify:jspecify:1.0.0" +junit = "org.junit.jupiter:junit-jupiter:6.1.2" +jspecify = "org.jspecify:jspecify:1.0.1" kyori-ansi = "net.kyori:ansi:1.1.1" guava = "com.google.guava:guava:33.6.0-jre" gson = "com.google.code.gson:gson:2.14.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-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-iostreams = { module = "org.apache.logging.log4j:log4j-iostreams", 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-haproxy = { module = "io.netty:netty-codec-haproxy", 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-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.8.3" -slf4j = "org.slf4j:slf4j-api:2.0.17" -snakeyaml = "org.yaml:snakeyaml:2.5" -spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.9.8" +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" terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0" [bundles] diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index c026bda4..136f66a7 100644 --- a/proxy/build.gradle.kts +++ b/proxy/build.gradle.kts @@ -33,65 +33,6 @@ tasks { 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("org/checkerframework/checker/**") From f918d0d649e0d07de6e52703bd4a8357c06b658c Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Wed, 12 Aug 2026 23:39:19 +0100 Subject: [PATCH 19/20] 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. --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9c12ac2d..985c9d97 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,7 +40,7 @@ kyori-ansi = "net.kyori:ansi:1.1.1" guava = "com.google.guava:guava:33.6.0-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" +lmbda = "org.lanternpowered:lmbda:2.0.0" 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-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" } From 4772ca3022c49bfab37c703f72cbca7654fb5848 Mon Sep 17 00:00:00 2001 From: Wouter Gritter Date: Fri, 14 Aug 2026 12:53:49 +0200 Subject: [PATCH 20/20] 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. --- gradle/libs.versions.toml | 2 +- .../com/velocitypowered/proxy/event/CustomHandlerAdapter.java | 4 +--- .../com/velocitypowered/proxy/event/VelocityEventManager.java | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 985c9d97..9c12ac2d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,7 +40,7 @@ kyori-ansi = "net.kyori:ansi:1.1.1" guava = "com.google.guava:guava:33.6.0-jre" gson = "com.google.code.gson:gson:2.14.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-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" } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/event/CustomHandlerAdapter.java b/proxy/src/main/java/com/velocitypowered/proxy/event/CustomHandlerAdapter.java index 8b9df6dd..88e731f0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/event/CustomHandlerAdapter.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/event/CustomHandlerAdapter.java @@ -60,9 +60,7 @@ final class CustomHandlerAdapter { UntargetedEventHandler buildUntargetedHandler(final Method method) throws IllegalAccessException { final MethodHandle methodHandle = methodHandlesLookup.unreflect(method); - final MethodHandles.Lookup defineLookup = MethodHandles.privateLookupIn( - method.getDeclaringClass(), methodHandlesLookup); - final LambdaType lambdaType = functionType.defineClassesWith(defineLookup); + final LambdaType lambdaType = functionType.defineClassesWith(methodHandlesLookup); final F invokeFunction = LambdaFactory.create(lambdaType, methodHandle); final BiFunction handlerFunction = handlerBuilder.apply(invokeFunction); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java b/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java index 9d54b2d0..b56b907c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java @@ -243,7 +243,7 @@ public class VelocityEventManager implements EventManager { } else { type = untargetedVoidHandlerType; } - return LambdaFactory.create(type.defineClassesWith(lookup), methodHandle); + return LambdaFactory.create(type.defineClassesWith(methodHandlesLookup), methodHandle); } static final class MethodHandlerInfo {