packet sender, receiver, listen for packets, network fixes and improvements

This commit is contained in:
Moritz Zwerger 2024-01-16 10:21:23 +01:00
parent b388253a49
commit 25f439499d
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
20 changed files with 487 additions and 145 deletions

View File

@ -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))))

View File

@ -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 <T : C2SPacket> PlayConnection.assertPacket(type: Class<T>): T {
val packet = test().take() ?: throw AssertionError("Expected packet of type $type, but found [null]!")
fun <T : C2SPacket> ClientNetwork.assertPacket(type: Class<T>): 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 <T : C2SPacket> PlayConnection.assertPacket(type: Class<T>): T {
return test().assertPacket(type)
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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...")
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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
}
}

View File

@ -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<C2SPacket>()
@ -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)
}
}

View File

@ -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 }
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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<Any>(), 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<C2SPacket> = 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<PlayConnection>()?.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

View File

@ -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<Sock
pipeline.addLast(LengthDecoder.NAME, LengthDecoder(maxLength))
pipeline.addLast(PacketDecoder.NAME, PacketDecoder(client))
pipeline.addLast(ClientPacketHandler.NAME, ClientPacketHandler(client))
pipeline.addLast(ClientPacketHandler.NAME, ClientPacketHandler(client.receiver))
pipeline.addLast(LengthEncoder.NAME, LengthEncoder(maxLength))
pipeline.addLast(PacketEncoder.NAME, PacketEncoder(client))

View File

@ -0,0 +1,26 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*
* 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
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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<C2SPacketListener> = ArrayList()
private val queue: MutableList<QueuedS2CP<*>> = 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
}
}

View File

@ -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

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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<S2CPacketListener> = ArrayList()
private val queue: MutableList<C2SPacket> = 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
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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
}

View File

@ -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<QueuedS2CP<*>>() {
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<PlayConnection>() ?: 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<StatusConnection>() ?: 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 {

View File

@ -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

View File

@ -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) {

View File

@ -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 {

View File

@ -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) {