diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 3a056ec5b..f41203fc5 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -17,7 +17,7 @@ java { } dependencies { - compileOnlyApi(libs.jspecify) + api(libs.jspecify) api(libs.gson) api(libs.guava) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c5fa29c9..39f2f049b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT" bstats = "org.bstats:bstats-base:3.2.1" caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.4" checker-qual = "org.checkerframework:checker-qual:4.2.2" -checkstyle = "com.puppycrawl.tools:checkstyle:13.9.0" +checkstyle = "com.puppycrawl.tools:checkstyle:13.10.0" completablefutures = "com.spotify:completable-futures:0.3.6" component = "gg.gemstone:component:1.0.2" configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" } diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index ecb16a177..c1f812703 100644 --- a/proxy/build.gradle.kts +++ b/proxy/build.gradle.kts @@ -107,64 +107,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/*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/**") 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 132087eb7..eeea1c7c0 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 @@ -48,6 +48,7 @@ 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; @@ -251,8 +252,9 @@ public boolean handle(ServerboundCookieResponsePacket packet) { @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; } @@ -279,8 +281,8 @@ public void handleGeneric(MinecraftPacket packet) { 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 000000000..96e27766e --- /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); + } +}