From 25f439499d49e581a84dff722cdf742ef984efdd Mon Sep 17 00:00:00 2001 From: Moritz Zwerger Date: Tue, 16 Jan 2024 10:21:23 +0100 Subject: [PATCH] packet sender, receiver, listen for packets, network fixes and improvements --- .../connection/play/ConnectionTestUtil.kt | 4 +- .../network/connection/play/PacketTestUtil.kt | 31 +++-- .../packet/receiver/PacketReceiverTest.kt | 89 +++++++++++++ .../netty/packet/sender/PacketSenderTest.kt | 73 +++++++++++ .../network/client/test/TestNetwork.kt | 24 +++- .../de/bixilon/minosoft/gui/RenderLoop.kt | 9 +- .../rendering/input/key/DebugKeyBindings.kt | 6 +- .../network/network/client/ClientNetwork.kt | 14 ++- .../network/client/netty/NettyClient.kt | 40 ++---- .../network/client/netty/NetworkPipeline.kt | 4 +- .../packet/receiver/C2SPacketListener.kt | 26 ++++ .../netty/packet/receiver/PacketReceiver.kt | 119 ++++++++++++++++++ .../receiver}/QueuedS2CP.kt | 4 +- .../netty/packet/sender/PacketSender.kt | 67 ++++++++++ .../netty/packet/sender/S2CPacketListener.kt | 27 ++++ .../netty/pipeline/ClientPacketHandler.kt | 73 +---------- .../netty/pipeline/encoding/PacketDecoder.kt | 4 +- .../packets/s2c/configuration/ReadyS2CP.kt | 6 +- .../protocol/packets/s2c/login/SuccessS2CP.kt | 6 +- .../packets/s2c/play/ReconfigureS2CP.kt | 6 +- 20 files changed, 487 insertions(+), 145 deletions(-) create mode 100644 src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/network/client/netty/packet/receiver/PacketReceiverTest.kt create mode 100644 src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/network/client/netty/packet/sender/PacketSenderTest.kt create mode 100644 src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/packet/receiver/C2SPacketListener.kt create mode 100644 src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/packet/receiver/PacketReceiver.kt rename src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/{pipeline => packet/receiver}/QueuedS2CP.kt (94%) create mode 100644 src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/packet/sender/PacketSender.kt create mode 100644 src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/packet/sender/S2CPacketListener.kt diff --git a/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/connection/play/ConnectionTestUtil.kt b/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/connection/play/ConnectionTestUtil.kt index c009167a8..c0233c29a 100644 --- a/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/connection/play/ConnectionTestUtil.kt +++ b/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/connection/play/ConnectionTestUtil.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2020-2023 Moritz Zwerger + * Copyright (C) 2020-2024 Moritz Zwerger * * 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. * @@ -84,7 +84,7 @@ object ConnectionTestUtil { WORLD.forceSet(connection, createWorld(connection, light, (worldSize * 2 + 1).pow(2))) PLAYER.forceSet(connection, LocalPlayerEntity(connection.account, connection, signature)) connection.player.startInit() - NETWORK.forceSet(connection, TestNetwork()) + NETWORK.forceSet(connection, TestNetwork(connection)) EVENTS.forceSet(connection, EventMaster()) PROFILES.forceSet(connection, profiles) ASSETS_MANAGER.forceSet(connection, ConnectionAssetsManager(AssetsManagerProperties(PackProperties(version.packFormat)))) diff --git a/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/connection/play/PacketTestUtil.kt b/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/connection/play/PacketTestUtil.kt index 00d029202..7ce34f5bc 100644 --- a/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/connection/play/PacketTestUtil.kt +++ b/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/connection/play/PacketTestUtil.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2020-2023 Moritz Zwerger + * Copyright (C) 2020-2024 Moritz Zwerger * * 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. * @@ -14,7 +14,9 @@ package de.bixilon.minosoft.protocol.network.connection.play import de.bixilon.kutil.cast.CastUtil.unsafeCast +import de.bixilon.kutil.exception.Broken import de.bixilon.kutil.unsafe.UnsafeUtil.setUnsafeAccessible +import de.bixilon.minosoft.protocol.network.network.client.ClientNetwork import de.bixilon.minosoft.protocol.network.network.client.test.TestNetwork import de.bixilon.minosoft.protocol.packets.c2s.C2SPacket @@ -24,8 +26,9 @@ object PacketTestUtil { return network.unsafeCast() } - fun PlayConnection.assertPacket(expected: C2SPacket) { - val found = test().take() ?: throw AssertionError("Expected packet $expected, but found [null]!") + fun ClientNetwork.assertPacket(expected: C2SPacket) { + if (this !is TestNetwork) Broken("Not testing") + val found = take() ?: throw AssertionError("Expected packet $expected, but found [null]!") if (found::class.java != expected::class.java) { throw AssertionError("Packet class expected: $expected, but found $found") } @@ -39,25 +42,39 @@ object PacketTestUtil { } } - fun PlayConnection.assertNoPacket() { - val packet = test().take() + fun ClientNetwork.assertNoPacket() { + if (this !is TestNetwork) Broken("Not testing") + val packet = take() if (packet != null) { throw AssertionError("Expected no packet, but found $packet") } } + fun PlayConnection.assertPacket(expected: C2SPacket) { + test().assertPacket(expected) + } + + fun PlayConnection.assertNoPacket() { + test().assertNoPacket() + } + @Deprecated("use assertPacket and assertNoPacket") fun PlayConnection.assertOnlyPacket(packet: C2SPacket) { assertPacket(packet) assertNoPacket() } - fun PlayConnection.assertPacket(type: Class): T { - val packet = test().take() ?: throw AssertionError("Expected packet of type $type, but found [null]!") + fun ClientNetwork.assertPacket(type: Class): T { + if (this !is TestNetwork) Broken("Not testing") + val packet = take() ?: throw AssertionError("Expected packet of type $type, but found [null]!") val clazz = packet::class.java if (type.isAssignableFrom(clazz)) { return packet.unsafeCast() } throw AssertionError("Expected packet of type $type, but found found $packet!") } + + fun PlayConnection.assertPacket(type: Class): T { + return test().assertPacket(type) + } } diff --git a/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/network/client/netty/packet/receiver/PacketReceiverTest.kt b/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/network/client/netty/packet/receiver/PacketReceiverTest.kt new file mode 100644 index 000000000..b0fbc3139 --- /dev/null +++ b/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/network/client/netty/packet/receiver/PacketReceiverTest.kt @@ -0,0 +1,89 @@ +/* + * Minosoft + * Copyright (C) 2020-2024 Moritz Zwerger + * + * 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.protocol.network.network.client.netty.packet.receiver + +import de.bixilon.minosoft.protocol.network.connection.Connection +import de.bixilon.minosoft.protocol.network.network.client.netty.exceptions.PacketHandleException +import de.bixilon.minosoft.protocol.network.network.client.test.TestNetwork +import de.bixilon.minosoft.protocol.packets.registry.PacketType +import de.bixilon.minosoft.protocol.packets.s2c.S2CPacket +import org.testng.Assert.assertEquals +import org.testng.Assert.assertTrue +import org.testng.annotations.Test + +@Test(groups = ["network"]) +class PacketReceiverTest { + val type = PacketType("test", false, false, null, null) + + + private fun create(): PacketReceiver { + val network = TestNetwork() + return network.receiver + } + + fun `simple receiving`() { + val receiver = create() + val packet = TestS2CP() + receiver.onReceive(type, packet) + assertEquals(packet.handled, 1) + } + + fun `error handling`() { + val receiver = create() + val packet = TestS2CP(false) + try { + receiver.onReceive(type, packet) + throw AssertionError("Not thrown") + } catch (error: PacketHandleException) { + } + assertEquals(packet.handled, 1) + } + + fun `listen and keep`() { + val receiver = create() + var packets = 0 + receiver.listen { assertTrue(it is TestS2CP); packets++; false } + val packet = TestS2CP(true) + receiver.onReceive(type, packet) + assertEquals(packets, 1) + assertEquals(packet.handled, 1) + } + + fun `listen and discard`() { + val receiver = create() + var packets = 0 + receiver.listen { assertTrue(it is TestS2CP); packets++; true } + val packet = TestS2CP(true) + receiver.onReceive(type, packet) + assertEquals(packets, 1) + assertEquals(packet.handled, 0) + } + + // TODO: async handling + + + private class TestS2CP( + val handle: Boolean = true, + ) : S2CPacket { + var handled = 0 + + override fun log(reducedLog: Boolean) = Unit + + + override fun handle(connection: Connection) { + handled++ + if (!handle) throw Exception("Testing...") + } + } +} diff --git a/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/network/client/netty/packet/sender/PacketSenderTest.kt b/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/network/client/netty/packet/sender/PacketSenderTest.kt new file mode 100644 index 000000000..4b1f27368 --- /dev/null +++ b/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/network/client/netty/packet/sender/PacketSenderTest.kt @@ -0,0 +1,73 @@ +/* + * Minosoft + * Copyright (C) 2020-2024 Moritz Zwerger + * + * 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.protocol.network.network.client.netty.packet.sender + +import de.bixilon.minosoft.protocol.network.connection.play.PacketTestUtil.assertNoPacket +import de.bixilon.minosoft.protocol.network.connection.play.PacketTestUtil.assertPacket +import de.bixilon.minosoft.protocol.network.network.client.test.TestNetwork +import de.bixilon.minosoft.protocol.packets.c2s.C2SPacket +import org.testng.Assert.assertEquals +import org.testng.Assert.assertTrue +import org.testng.annotations.Test + +@Test(groups = ["network"]) +class PacketSenderTest { + + + private fun create(): PacketSender { + val network = TestNetwork() + return network.sender + } + + fun `send normal`() { + val sender = create() + sender.send(DummyC2SPacket()) + sender.network.assertPacket(DummyC2SPacket::class.java) + sender.network.assertNoPacket() + } + + fun `sending paused`() { + val sender = create() + sender.paused = true + sender.send(DummyC2SPacket()) + sender.network.assertNoPacket() + sender.paused = false + sender.network.assertPacket(DummyC2SPacket::class.java) + sender.network.assertNoPacket() + } + + fun `packet listener not discarding`() { + val sender = create() + var packets = 0 + sender.listen { assertTrue(it is DummyC2SPacket); packets++; false } + sender.send(DummyC2SPacket()) + sender.network.assertPacket(DummyC2SPacket::class.java) + sender.network.assertNoPacket() + assertEquals(packets, 1) + } + + fun `packet listener discarding`() { + val sender = create() + var packets = 0 + sender.listen { assertTrue(it is DummyC2SPacket); packets++; true } + sender.send(DummyC2SPacket()) + sender.network.assertNoPacket() + assertEquals(packets, 1) + } + + + private class DummyC2SPacket : C2SPacket { + override fun log(reducedLog: Boolean) = Unit + } +} diff --git a/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/network/client/test/TestNetwork.kt b/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/network/client/test/TestNetwork.kt index 0e8d26d7b..47ea51654 100644 --- a/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/network/client/test/TestNetwork.kt +++ b/src/integration-test/kotlin/de/bixilon/minosoft/protocol/network/network/client/test/TestNetwork.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2020-2023 Moritz Zwerger + * Copyright (C) 2020-2024 Moritz Zwerger * * 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. * @@ -14,18 +14,27 @@ package de.bixilon.minosoft.protocol.network.network.client.test import de.bixilon.minosoft.protocol.address.ServerAddress +import de.bixilon.minosoft.protocol.network.connection.Connection +import de.bixilon.minosoft.protocol.network.connection.play.ConnectionTestUtil.createConnection import de.bixilon.minosoft.protocol.network.network.client.ClientNetwork +import de.bixilon.minosoft.protocol.network.network.client.netty.exceptions.PacketHandleException +import de.bixilon.minosoft.protocol.network.network.client.netty.packet.receiver.PacketReceiver +import de.bixilon.minosoft.protocol.network.network.client.netty.packet.sender.PacketSender import de.bixilon.minosoft.protocol.packets.c2s.C2SPacket import de.bixilon.minosoft.protocol.protocol.ProtocolStates import java.util.concurrent.ConcurrentLinkedQueue import javax.crypto.Cipher -class TestNetwork : ClientNetwork { - override var connected: Boolean = false +class TestNetwork( + connection: Connection = createConnection(), +) : ClientNetwork { + override val sender = PacketSender(this) + override val receiver = PacketReceiver(this, connection) + override var connected: Boolean = true override var state: ProtocolStates = ProtocolStates.PLAY override var compressionThreshold: Int = -1 override var encrypted: Boolean = false - override var receive = true + override val detached = false private var queue = ConcurrentLinkedQueue() @@ -47,9 +56,8 @@ class TestNetwork : ClientNetwork { override fun detach() = Unit - override fun pauseSending(pause: Boolean) = Unit - override fun send(packet: C2SPacket) { + override fun forceSend(packet: C2SPacket) { if (queue.size > 15) { // leaking return @@ -63,4 +71,8 @@ class TestNetwork : ClientNetwork { } return queue.remove() } + + override fun handleError(error: Throwable) { + throw PacketHandleException(error) + } } diff --git a/src/main/java/de/bixilon/minosoft/gui/RenderLoop.kt b/src/main/java/de/bixilon/minosoft/gui/RenderLoop.kt index 7bf3d34ab..66c781572 100644 --- a/src/main/java/de/bixilon/minosoft/gui/RenderLoop.kt +++ b/src/main/java/de/bixilon/minosoft/gui/RenderLoop.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2020-2023 Moritz Zwerger + * Copyright (C) 2020-2024 Moritz Zwerger * * 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. * @@ -40,12 +40,7 @@ class RenderLoop( init { var paused = false context::state.observe(this) { - paused = if (paused) { - // context.queue.clear() // TODO: That might cause bugs and memory leaks - false - } else { - it == RenderingStates.PAUSED - } + paused = if (paused) false else it == RenderingStates.PAUSED } context.profile.performance::slowRendering.observe(this) { this.slowRendering = it } } diff --git a/src/main/java/de/bixilon/minosoft/gui/rendering/input/key/DebugKeyBindings.kt b/src/main/java/de/bixilon/minosoft/gui/rendering/input/key/DebugKeyBindings.kt index c04f9c9c6..6bb1d44d5 100644 --- a/src/main/java/de/bixilon/minosoft/gui/rendering/input/key/DebugKeyBindings.kt +++ b/src/main/java/de/bixilon/minosoft/gui/rendering/input/key/DebugKeyBindings.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2020-2023 Moritz Zwerger + * Copyright (C) 2020-2024 Moritz Zwerger * * 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. * @@ -47,7 +47,7 @@ object DebugKeyBindings { ignoreConsumer = true, )) { connection.util.sendDebugMessage("Pausing incoming packets: ${it.format()}") - connection.network.receive = it + connection.network.receiver.paused = it } register(PAUSE_OUTGOING, KeyBinding( @@ -56,7 +56,7 @@ object DebugKeyBindings { ignoreConsumer = true, )) { connection.util.sendDebugMessage("Pausing outgoing packets: ${it.format()}") - connection.network.pauseSending(it) + connection.network.sender.paused = it } } diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/network/client/ClientNetwork.kt b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/ClientNetwork.kt index 8f7aa2776..9ee6ef5c4 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/network/network/client/ClientNetwork.kt +++ b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/ClientNetwork.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2020-2023 Moritz Zwerger + * Copyright (C) 2020-2024 Moritz Zwerger * * 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. * @@ -14,6 +14,8 @@ package de.bixilon.minosoft.protocol.network.network.client import de.bixilon.minosoft.protocol.address.ServerAddress +import de.bixilon.minosoft.protocol.network.network.client.netty.packet.receiver.PacketReceiver +import de.bixilon.minosoft.protocol.network.network.client.netty.packet.sender.PacketSender import de.bixilon.minosoft.protocol.packets.c2s.C2SPacket import de.bixilon.minosoft.protocol.protocol.ProtocolStates import javax.crypto.Cipher @@ -21,11 +23,14 @@ import javax.crypto.Cipher interface ClientNetwork { val connected: Boolean val encrypted: Boolean + val detached: Boolean var state: ProtocolStates - var receive: Boolean val compressionThreshold: Int + val receiver: PacketReceiver + val sender: PacketSender + fun connect(address: ServerAddress, native: Boolean) fun disconnect() fun detach() @@ -33,7 +38,8 @@ interface ClientNetwork { fun setupEncryption(encrypt: Cipher, decrypt: Cipher) fun setupCompression(threshold: Int) - fun pauseSending(pause: Boolean) + fun send(packet: C2SPacket) = sender.send(packet) + fun forceSend(packet: C2SPacket) - fun send(packet: C2SPacket) + fun handleError(error: Throwable) } diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/NettyClient.kt b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/NettyClient.kt index 543fbc9f0..536c58b6c 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/NettyClient.kt +++ b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/NettyClient.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2020-2023 Moritz Zwerger + * Copyright (C) 2020-2024 Moritz Zwerger * * 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. * @@ -14,7 +14,6 @@ package de.bixilon.minosoft.protocol.network.network.client.netty import de.bixilon.kutil.cast.CastUtil.nullCast -import de.bixilon.kutil.concurrent.pool.DefaultThreadPool import de.bixilon.kutil.observer.DataObserver.Companion.observed import de.bixilon.minosoft.config.profile.profiles.other.OtherProfileManager import de.bixilon.minosoft.protocol.address.ServerAddress @@ -27,6 +26,8 @@ import de.bixilon.minosoft.protocol.network.network.client.netty.exceptions.Pack import de.bixilon.minosoft.protocol.network.network.client.netty.exceptions.ciritical.CriticalNetworkException import de.bixilon.minosoft.protocol.network.network.client.netty.natives.NioNatives import de.bixilon.minosoft.protocol.network.network.client.netty.natives.TransportNatives +import de.bixilon.minosoft.protocol.network.network.client.netty.packet.receiver.PacketReceiver +import de.bixilon.minosoft.protocol.network.network.client.netty.packet.sender.PacketSender import de.bixilon.minosoft.protocol.network.network.client.netty.pipeline.compression.PacketDeflater import de.bixilon.minosoft.protocol.network.network.client.netty.pipeline.compression.PacketInflater import de.bixilon.minosoft.protocol.network.network.client.netty.pipeline.encryption.PacketDecryptor @@ -50,6 +51,8 @@ import javax.crypto.Cipher class NettyClient( val connection: Connection, ) : SimpleChannelInboundHandler(), ClientNetwork { + override val sender = PacketSender(this) + override val receiver = PacketReceiver(this, connection) private var address: ServerAddress? = null override var connected by observed(false) private set @@ -58,14 +61,8 @@ class NettyClient( override var encrypted: Boolean = false private set private var channel: Channel? = null - private val packetQueue: MutableList = mutableListOf() // Used for pause sending - private var sendingPaused = false - private var detached = false - override var receive = true - set(value) { - channel?.config()?.isAutoRead = value - field = value - } + override var detached = false + private set override fun connect(address: ServerAddress, native: Boolean) { this.address = address @@ -133,24 +130,7 @@ class NettyClient( channel?.close() } - override fun pauseSending(pause: Boolean) { - this.sendingPaused = pause - if (!sendingPaused) { - DefaultThreadPool += { - for (packet in packetQueue) { - send(packet) - } - } - } - } - - override fun send(packet: C2SPacket) { - // TODO: packet listener - if (detached) return - if (sendingPaused) { - packetQueue += packet - return - } + override fun forceSend(packet: C2SPacket) { val channel = getChannel() ?: return packet.log((connection.nullCast()?.profiles?.other ?: OtherProfileManager.selected).log.reducedProtocolLog) @@ -166,7 +146,7 @@ class NettyClient( context.channel().config().setOption(ChannelOption.TCP_NODELAY, true) } catch (_: Throwable) { } - context.channel().config().isAutoRead = this.receive + context.channel().config().isAutoRead = true this.channel = context.channel() connected = true } @@ -177,7 +157,7 @@ class NettyClient( connected = false } - fun handleError(error: Throwable) { + override fun handleError(error: Throwable) { var cause = error if (cause is DecoderException) { cause = error.cause ?: cause diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/NetworkPipeline.kt b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/NetworkPipeline.kt index 4781e9c4c..45d781151 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/NetworkPipeline.kt +++ b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/NetworkPipeline.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2020-2023 Moritz Zwerger + * Copyright (C) 2020-2024 Moritz Zwerger * * 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. * @@ -37,7 +37,7 @@ class NetworkPipeline(private val client: NettyClient) : ChannelInitializer. + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.protocol.network.network.client.netty.packet.receiver + +import de.bixilon.minosoft.protocol.packets.s2c.S2CPacket + +fun interface C2SPacketListener { + + /** + * Called for every packet that is received from the server before it is handled. + * Depending on the time it takes to call all onReceive methods, serious network latency can be introduced. + * @return true, if the packet should be instantly discarded and not handled, false if not. + */ + fun onReceive(packet: S2CPacket): Boolean +} diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/packet/receiver/PacketReceiver.kt b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/packet/receiver/PacketReceiver.kt new file mode 100644 index 000000000..29ae75b6c --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/packet/receiver/PacketReceiver.kt @@ -0,0 +1,119 @@ +/* + * Minosoft + * Copyright (C) 2020-2024 Moritz Zwerger + * + * 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.protocol.network.network.client.netty.packet.receiver + +import de.bixilon.kutil.concurrent.pool.DefaultThreadPool +import de.bixilon.kutil.concurrent.pool.ThreadPool +import de.bixilon.kutil.concurrent.pool.runnable.ForcePooledRunnable +import de.bixilon.kutil.exception.ExceptionUtil.ignoreAll +import de.bixilon.minosoft.config.profile.profiles.other.OtherProfileManager +import de.bixilon.minosoft.protocol.network.connection.Connection +import de.bixilon.minosoft.protocol.network.connection.play.PlayConnection +import de.bixilon.minosoft.protocol.network.network.client.ClientNetwork +import de.bixilon.minosoft.protocol.network.network.client.netty.exceptions.NetworkException +import de.bixilon.minosoft.protocol.network.network.client.netty.exceptions.PacketHandleException +import de.bixilon.minosoft.protocol.packets.registry.PacketType +import de.bixilon.minosoft.protocol.packets.s2c.S2CPacket + +class PacketReceiver( + val network: ClientNetwork, + val connection: Connection, +) { + private val listener: MutableList = ArrayList() + private val queue: MutableList> = ArrayList(0) + var paused = false + set(value) { + if (field == value) return + if (!value) process() // try to keep order + field = value + if (!value) process() // flush again, so packets that might got queued won't get lost + } + + + private fun handle(packet: S2CPacket) { + val profile = if (connection is PlayConnection) connection.profiles.other else OtherProfileManager.selected + val reduced = profile.log.reducedProtocolLog + packet.log(reduced) + packet.handle(connection) + } + + private fun handleError(type: PacketType, error: Throwable) { + if (type.extra != null) { + type.extra.onError(error, connection) + } + network.handleError(error) + } + + private fun tryHandle(type: PacketType, packet: S2CPacket) { + if (!network.connected) return + + try { + handle(packet) + } catch (exception: NetworkException) { + handleError(type, exception) + } catch (error: Throwable) { + handleError(type, PacketHandleException(error)) + } + } + + private fun PacketType.handleAsync(): Boolean { + if (!threadSafe) return false + if (lowPriority) return true + if (DefaultThreadPool.queueSize >= (DefaultThreadPool.threadCount - 1)) return false // might backlog a bit + return true + } + + private fun tryHandle2(type: PacketType, packet: S2CPacket) { + if (type.handleAsync()) { + DefaultThreadPool += ForcePooledRunnable(priority = if (type.lowPriority) ThreadPool.NORMAL else ThreadPool.HIGH) { tryHandle(type, packet) } + } else { + tryHandle(type, packet) + } + } + + private fun notify(packet: S2CPacket): Boolean { + if (listener.isEmpty()) return false + for (listener in listener) { + val discard = ignoreAll { listener.onReceive(packet) } ?: continue + if (discard) return true + } + return false + } + + fun onReceive(type: PacketType, packet: S2CPacket) { + if (network.detached) return + if (!network.connected) return + val discard = notify(packet) + if (discard) return + + if (paused) { + queue += QueuedS2CP(type, packet) + } else { + tryHandle2(type, packet) + } + } + + private fun process() { + while (queue.isNotEmpty()) { + val (type, packet) = queue.removeFirst() + tryHandle(type, packet) + } + } + + operator fun plusAssign(listener: C2SPacketListener) = listen(listener) + + fun listen(listener: C2SPacketListener) { + this.listener += listener + } +} diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/pipeline/QueuedS2CP.kt b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/packet/receiver/QueuedS2CP.kt similarity index 94% rename from src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/pipeline/QueuedS2CP.kt rename to src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/packet/receiver/QueuedS2CP.kt index f2b4b127c..e5640dfa9 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/pipeline/QueuedS2CP.kt +++ b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/packet/receiver/QueuedS2CP.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2020-2023 Moritz Zwerger + * Copyright (C) 2020-2024 Moritz Zwerger * * 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. * @@ -11,7 +11,7 @@ * This software is not affiliated with Mojang AB, the original developer of Minecraft. */ -package de.bixilon.minosoft.protocol.network.network.client.netty.pipeline +package de.bixilon.minosoft.protocol.network.network.client.netty.packet.receiver import de.bixilon.minosoft.protocol.packets.registry.PacketType import de.bixilon.minosoft.protocol.packets.s2c.S2CPacket diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/packet/sender/PacketSender.kt b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/packet/sender/PacketSender.kt new file mode 100644 index 000000000..a7f168969 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/packet/sender/PacketSender.kt @@ -0,0 +1,67 @@ +/* + * Minosoft + * Copyright (C) 2020-2024 Moritz Zwerger + * + * 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.protocol.network.network.client.netty.packet.sender + +import de.bixilon.kutil.exception.ExceptionUtil.ignoreAll +import de.bixilon.minosoft.protocol.network.network.client.ClientNetwork +import de.bixilon.minosoft.protocol.packets.c2s.C2SPacket + +class PacketSender( + val network: ClientNetwork, +) { + private val listener: MutableList = ArrayList() + private val queue: MutableList = ArrayList(0) + var paused = false + set(value) { + if (field == value) return + if (!value) flush() // try to keep order + field = value + if (!value) flush() // flush again, so packets that might got queued won't get lost + } + + private fun notify(packet: C2SPacket): Boolean { + if (listener.isEmpty()) return false + for (listener in listener) { + val discard = ignoreAll { listener.onSend(packet) } ?: continue + if (discard) return true + } + return false + } + + fun send(packet: C2SPacket) { + if (network.detached) return + if (!network.connected) return + val discard = notify(packet) + if (discard) return + + if (paused) { + queue += packet + } else { + network.forceSend(packet) + } + } + + private fun flush() { + while (queue.isNotEmpty()) { + val packet = queue.removeFirst() + network.forceSend(packet) + } + } + + operator fun plusAssign(listener: S2CPacketListener) = listen(listener) + + fun listen(listener: S2CPacketListener) { + this.listener += listener + } +} diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/packet/sender/S2CPacketListener.kt b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/packet/sender/S2CPacketListener.kt new file mode 100644 index 000000000..81905ff50 --- /dev/null +++ b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/packet/sender/S2CPacketListener.kt @@ -0,0 +1,27 @@ +/* + * Minosoft + * Copyright (C) 2020-2024 Moritz Zwerger + * + * 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 . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.protocol.network.network.client.netty.packet.sender + +import de.bixilon.minosoft.protocol.packets.c2s.C2SPacket + +fun interface S2CPacketListener { + + /** + * Called for every packet that is sent to the server from the client. + * Might be called from any thread + * Depending on the time it takes to call all onSend methods, serious network latency can be introduced. + * @return true, if the packet should be instantly discarded, false if not. + */ + fun onSend(packet: C2SPacket): Boolean +} diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/pipeline/ClientPacketHandler.kt b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/pipeline/ClientPacketHandler.kt index 7170cc01d..8a4f6969a 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/pipeline/ClientPacketHandler.kt +++ b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/pipeline/ClientPacketHandler.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2020-2023 Moritz Zwerger + * Copyright (C) 2020-2024 Moritz Zwerger * * 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. * @@ -13,80 +13,17 @@ package de.bixilon.minosoft.protocol.network.network.client.netty.pipeline -import de.bixilon.kutil.cast.CastUtil.nullCast -import de.bixilon.kutil.concurrent.pool.DefaultThreadPool -import de.bixilon.kutil.concurrent.pool.ThreadPool -import de.bixilon.kutil.concurrent.pool.runnable.ForcePooledRunnable -import de.bixilon.minosoft.config.profile.profiles.other.OtherProfileManager -import de.bixilon.minosoft.protocol.network.connection.Connection -import de.bixilon.minosoft.protocol.network.connection.play.PlayConnection -import de.bixilon.minosoft.protocol.network.connection.status.StatusConnection -import de.bixilon.minosoft.protocol.network.network.client.netty.NettyClient -import de.bixilon.minosoft.protocol.network.network.client.netty.exceptions.NetworkException -import de.bixilon.minosoft.protocol.network.network.client.netty.exceptions.PacketHandleException -import de.bixilon.minosoft.protocol.network.network.client.netty.exceptions.WrongConnectionException -import de.bixilon.minosoft.protocol.packets.registry.PacketType -import de.bixilon.minosoft.protocol.packets.s2c.PlayS2CPacket -import de.bixilon.minosoft.protocol.packets.s2c.S2CPacket -import de.bixilon.minosoft.protocol.packets.s2c.StatusS2CPacket +import de.bixilon.minosoft.protocol.network.network.client.netty.packet.receiver.PacketReceiver +import de.bixilon.minosoft.protocol.network.network.client.netty.packet.receiver.QueuedS2CP import io.netty.channel.ChannelHandlerContext import io.netty.channel.SimpleChannelInboundHandler class ClientPacketHandler( - private val client: NettyClient, + private val receiver: PacketReceiver, ) : SimpleChannelInboundHandler>() { - private val connection: Connection = client.connection override fun channelRead0(context: ChannelHandlerContext, queued: QueuedS2CP<*>) { - if (queued.type.threadSafe && (queued.type.lowPriority || DefaultThreadPool.queueSize < DefaultThreadPool.threadCount - 1)) { // only handle async when thread pool not busy - val runnable = ForcePooledRunnable(priority = if (queued.type.lowPriority) ThreadPool.Priorities.NORMAL else ThreadPool.Priorities.HIGH) { tryHandle(context, queued.type, queued.packet) } - DefaultThreadPool += runnable - } else { - tryHandle(context, queued.type, queued.packet) - } - } - - private fun handleError(context: ChannelHandlerContext, type: PacketType, error: Throwable) { - if (type.extra != null) { - type.extra.onError(error, connection) - } - client.handleError(error) - } - - private fun tryHandle(context: ChannelHandlerContext, type: PacketType, packet: S2CPacket) { - if (!client.connected) { - return - } - try { - handle(packet) - } catch (exception: NetworkException) { - handleError(context, type, exception) - } catch (error: Throwable) { - handleError(context, type, PacketHandleException(error)) - } - } - - private fun handle(packet: S2CPacket) { - // TODO: packet listener - when (packet) { - is PlayS2CPacket -> handle(packet) - is StatusS2CPacket -> handle(packet) - else -> throw IllegalStateException("Unknown packet type!") - } - } - - private fun handle(packet: PlayS2CPacket) { - val connection = connection.nullCast() ?: throw WrongConnectionException(PlayConnection::class.java, this.connection::class.java) - packet.log(connection.profiles.other.log.reducedProtocolLog) - packet.check(connection) - packet.handle(connection) - } - - private fun handle(packet: StatusS2CPacket) { - val connection = connection.nullCast() ?: throw WrongConnectionException(StatusConnection::class.java, this.connection::class.java) - packet.log(OtherProfileManager.selected.log.reducedProtocolLog) - packet.check(connection) - packet.handle(connection) + receiver.onReceive(queued.type, queued.packet) } companion object { diff --git a/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/pipeline/encoding/PacketDecoder.kt b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/pipeline/encoding/PacketDecoder.kt index 180c5e2eb..2bd17b837 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/pipeline/encoding/PacketDecoder.kt +++ b/src/main/java/de/bixilon/minosoft/protocol/network/network/client/netty/pipeline/encoding/PacketDecoder.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2020-2023 Moritz Zwerger + * Copyright (C) 2020-2024 Moritz Zwerger * * 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. * @@ -19,7 +19,7 @@ import de.bixilon.minosoft.protocol.network.network.client.netty.exceptions.Netw import de.bixilon.minosoft.protocol.network.network.client.netty.exceptions.PacketReadException import de.bixilon.minosoft.protocol.network.network.client.netty.exceptions.ciritical.UnknownPacketIdException import de.bixilon.minosoft.protocol.network.network.client.netty.exceptions.implementation.PacketNotImplementedException -import de.bixilon.minosoft.protocol.network.network.client.netty.pipeline.QueuedS2CP +import de.bixilon.minosoft.protocol.network.network.client.netty.packet.receiver.QueuedS2CP import de.bixilon.minosoft.protocol.packets.s2c.S2CPacket import de.bixilon.minosoft.protocol.protocol.DefaultPacketMapping import de.bixilon.minosoft.protocol.protocol.buffers.InByteBuffer diff --git a/src/main/java/de/bixilon/minosoft/protocol/packets/s2c/configuration/ReadyS2CP.kt b/src/main/java/de/bixilon/minosoft/protocol/packets/s2c/configuration/ReadyS2CP.kt index 1df8561c7..041837c55 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/packets/s2c/configuration/ReadyS2CP.kt +++ b/src/main/java/de/bixilon/minosoft/protocol/packets/s2c/configuration/ReadyS2CP.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2020-2023 Moritz Zwerger + * Copyright (C) 2020-2024 Moritz Zwerger * * 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. * @@ -25,11 +25,9 @@ import de.bixilon.minosoft.util.logging.LogMessageType class ReadyS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket { override fun handle(connection: PlayConnection) { - connection.network.receive = false connection.util.prepareSpawn() - connection.sendPacket(ReadyC2SP()) + connection.network.send(ReadyC2SP()) connection.network.state = ProtocolStates.PLAY - connection.network.receive = true } override fun log(reducedLog: Boolean) { diff --git a/src/main/java/de/bixilon/minosoft/protocol/packets/s2c/login/SuccessS2CP.kt b/src/main/java/de/bixilon/minosoft/protocol/packets/s2c/login/SuccessS2CP.kt index e0260e850..813fbff2f 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/packets/s2c/login/SuccessS2CP.kt +++ b/src/main/java/de/bixilon/minosoft/protocol/packets/s2c/login/SuccessS2CP.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2020-2023 Moritz Zwerger + * Copyright (C) 2020-2024 Moritz Zwerger * * 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. * @@ -34,10 +34,8 @@ class SuccessS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket { override fun handle(connection: PlayConnection) { if (connection.version.hasConfigurationState) { - connection.network.receive = false - connection.sendPacket(ConfigureC2SP()) + connection.network.send(ConfigureC2SP()) connection.network.state = ProtocolStates.CONFIGURATION - connection.network.receive = true connection.sendBrand() connection.settingsManager.sendClientSettings() } else { diff --git a/src/main/java/de/bixilon/minosoft/protocol/packets/s2c/play/ReconfigureS2CP.kt b/src/main/java/de/bixilon/minosoft/protocol/packets/s2c/play/ReconfigureS2CP.kt index c9fe6cd44..3a0ade10b 100644 --- a/src/main/java/de/bixilon/minosoft/protocol/packets/s2c/play/ReconfigureS2CP.kt +++ b/src/main/java/de/bixilon/minosoft/protocol/packets/s2c/play/ReconfigureS2CP.kt @@ -1,6 +1,6 @@ /* * Minosoft - * Copyright (C) 2020-2023 Moritz Zwerger + * Copyright (C) 2020-2024 Moritz Zwerger * * 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. * @@ -26,10 +26,8 @@ class ReconfigureS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket { override fun handle(connection: PlayConnection) { connection.util.resetWorld() - connection.network.receive = false - connection.sendPacket(ReconfigureC2SP()) + connection.network.send(ReconfigureC2SP()) connection.network.state = ProtocolStates.CONFIGURATION - connection.network.receive = true } override fun log(reducedLog: Boolean) {