Implemented consumer interface API, support TextComponent serialize to json string, fix some bugs
This commit is contained in:
16 files changed
+650
-413
No files matched your search
@@ -19,7 +19,7 @@ A lightweight minecraft client-side protocol library for Kotlin Native and JVM
|
||||
- [x] **Online Mode Authentication & Encryption/Decryption**: See [Embedded cryptography](docs/Embedded-cryptography.md)
|
||||
- [x] **Structured `TextComponent` Parser**: TextComponent AST decoder
|
||||
- [x] **Command Tree Parser**: Full binary graph decoder for brigadier nodes, argument types, and suggestions
|
||||
- [ ] **Recipe Book & Recipe Data**: Recipe layout declarations and client-side recipe settings
|
||||
- [x] **Recipe Book & Recipe Data**
|
||||
- [ ] **Chunk & World Data**: Level Chunk Data with Light decoder (Paletted Containers, Direct/Indirect Palettes)
|
||||
- [ ] **Light Engine Update**: Sky & Block light nibble array parser
|
||||
- [ ] **Explosion Event Decoder**: Knockback vectors and destroyed block offsets array
|
||||
|
||||
+24
-1
@@ -6,7 +6,7 @@ public fun main() = runBlocking {
|
||||
"127.0.0.1", 25566, "MyBot",
|
||||
generateOfflineUuid("MyBot"),
|
||||
accessToken = null,
|
||||
context = DefaultProtocolContext.withCustom {
|
||||
context = {
|
||||
socketEngine = KtorNetworkEngine()
|
||||
}
|
||||
)
|
||||
@@ -81,6 +81,29 @@ client.networkChannel.sendPacket(
|
||||
)
|
||||
```
|
||||
|
||||
# Get server MOTD
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
val cli = createMinecraftClient(
|
||||
"127.0.0.1", 25566, "11",
|
||||
generateOfflineUuid("11"), null,
|
||||
context = {
|
||||
socketEngine = KtorNetworkEngine()
|
||||
}
|
||||
)
|
||||
cli.session.onEvent<SessionEvent.ConnectedEvent> {
|
||||
println(status())
|
||||
disconnect()
|
||||
}
|
||||
cli.session.onEvent<SessionEvent.DisconnectedEvent> {
|
||||
println(it.reason.toJsonString())
|
||||
}
|
||||
cli.connect()
|
||||
awaitCancellation()
|
||||
}
|
||||
```
|
||||
|
||||
# Respond velocity and update client motion
|
||||
|
||||
> This part uses math calculations
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/10
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.libmc.nbt
|
||||
|
||||
public fun NBTTag.toJsonString(): String = when (this) {
|
||||
is NBTTag.StringTag -> buildString {
|
||||
append('"')
|
||||
for (ch in value) {
|
||||
when (ch) {
|
||||
'\\' -> append("\\\\")
|
||||
'"' -> append("\\\"")
|
||||
'\b' -> append("\\b")
|
||||
'\u000C' -> append("\\f")
|
||||
'\n' -> append("\\n")
|
||||
'\r' -> append("\\r")
|
||||
'\t' -> append("\\t")
|
||||
else -> {
|
||||
if (ch < ' ') {
|
||||
val hex = ch.code.toString(16).padStart(4, '0')
|
||||
append("\\u").append(hex)
|
||||
} else {
|
||||
append(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
append('"')
|
||||
}
|
||||
|
||||
is NBTTag.ByteTag -> if (value == 1.toByte() || value == 0.toByte()) {
|
||||
if (value == 1.toByte()) "true" else "false"
|
||||
} else value.toString()
|
||||
|
||||
is NBTTag.ShortTag -> value.toString()
|
||||
is NBTTag.IntTag -> value.toString()
|
||||
is NBTTag.LongTag -> value.toString()
|
||||
is NBTTag.FloatTag -> value.toString()
|
||||
is NBTTag.DoubleTag -> value.toString()
|
||||
|
||||
is NBTTag.ByteArrayTag -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
|
||||
is NBTTag.IntArrayTag -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
|
||||
is NBTTag.LongArrayTag -> value.joinToString(prefix = "[", postfix = "]") { it.toString() }
|
||||
|
||||
is NBTTag.ListTag -> value.joinToString(prefix = "[", postfix = "]") { it.toJsonString() }
|
||||
|
||||
is NBTTag.CompoundTag -> value.entries.joinToString(prefix = "{", postfix = "}") { (k, v) ->
|
||||
val escapedKey = NBTTag.StringTag(k).toJsonString()
|
||||
"$escapedKey:${v.toJsonString()}"
|
||||
}
|
||||
}
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/9
|
||||
*/
|
||||
|
||||
package cn.rtast.libmc.protocol.client
|
||||
|
||||
import cn.rtast.libmc.protocol.crypto.minecraftServerIdHash
|
||||
import cn.rtast.libmc.protocol.crypto.rsaEncrypt
|
||||
import cn.rtast.libmc.protocol.packet.configuration.clientbound.*
|
||||
import cn.rtast.libmc.protocol.packet.configuration.serverbound.*
|
||||
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundDisconnectLoginPacket
|
||||
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundHelloPacket
|
||||
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundLoginSuccessPacket
|
||||
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundSetCompressionPacket
|
||||
import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundKeyPacket
|
||||
import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundLoginAcknowledgedPacket
|
||||
import cn.rtast.libmc.protocol.packet.play.clientbound.*
|
||||
import cn.rtast.libmc.protocol.packet.play.serverbound.*
|
||||
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
|
||||
import cn.rtast.libmc.protocol.util.generateRandom16Bytes
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.concurrent.Volatile
|
||||
import kotlin.math.abs
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
public class ClientTickingLoop internal constructor(private val client: MinecraftClient) {
|
||||
private var tickJob: Job? = null
|
||||
private val tickIntervalMs = 50L
|
||||
public var currentTick: Long = 0L
|
||||
private set
|
||||
|
||||
@Volatile
|
||||
public var isRunning: Boolean = false
|
||||
private set
|
||||
|
||||
private val listeners = mutableListOf<suspend (Long) -> Unit>()
|
||||
internal fun registerListener(action: suspend (Long) -> Unit) = listeners.add(action)
|
||||
|
||||
init {
|
||||
client.onPacket<ClientboundLoginSuccessPacket> { handleLoginSuccess() }
|
||||
client.onPacket<ClientboundSetCompressionPacket> { client.networkChannel.setCompression(it.threshold) }
|
||||
client.onPacket<ClientboundHelloPacket> { handleEncryptRequest(it) }
|
||||
client.onPacket<ClientboundDisconnectLoginPacket> { client.close() }
|
||||
client.onPacket<ClientboundDisconnectPlayPacket> { client.close() }
|
||||
client.onPacket<ClientboundDisconnectConfigurationPacket> { client.close() }
|
||||
client.onPacket<ClientboundSetTimePacket> { syncServerTick(it.worldAge) }
|
||||
client.onPacket<ClientboundPingPacket> { client.networkChannel.sendPacket(ServerboundPongPlayPacket(it.id)) }
|
||||
client.onPacket<ClientboundSelectKnownPacksPacket> {
|
||||
client.networkChannel.sendPacket(ServerboundSelectKnownPacksPacket(emptyList()))
|
||||
}
|
||||
client.onPacket<ClientboundCodeOfConductPacket> {
|
||||
client.networkChannel.sendPacket(ServerboundAcceptCodeOfConductPacket)
|
||||
}
|
||||
client.onPacket<ClientboundKeepAlivePlayPacket> {
|
||||
client.networkChannel.sendPacket(ServerboundKeepAlivePlayPacket(it.id))
|
||||
}
|
||||
client.onPacket<ClientboundPingConfigurationPacket> {
|
||||
client.networkChannel.sendPacket(ServerboundPongConfigurationPacket(it.id))
|
||||
}
|
||||
client.onPacket<ClientboundStartConfigurationPacket> {
|
||||
client.networkChannel.sendPacket(ServerboundConfigurationAcknowledgedPacket)
|
||||
client.stateMachine.transitionTo(ProtocolState.CONFIGURATION)
|
||||
}
|
||||
client.onPacket<ClientboundKeepAliveConfigurationPacket> {
|
||||
client.networkChannel.sendPacket(ServerboundKeepAliveConfigurationPacket(it.id))
|
||||
}
|
||||
client.onPacket<ClientboundFinishConfigurationPacket> {
|
||||
client.networkChannel.sendPacket(ServerboundAckFinishConfigurationPacket)
|
||||
client.stateMachine.transitionTo(ProtocolState.PLAY)
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun handleLoginSuccess() {
|
||||
client.networkChannel.sendPacket(ServerboundLoginAcknowledgedPacket)
|
||||
client.stateMachine.transitionTo(ProtocolState.CONFIGURATION)
|
||||
}
|
||||
|
||||
internal suspend fun handleEncryptRequest(packet: ClientboundHelloPacket) {
|
||||
val sharedSecret = generateRandom16Bytes()
|
||||
if (client.isOnlineMode) {
|
||||
val serverHash = minecraftServerIdHash(packet.serverId, sharedSecret, packet.publicKey)
|
||||
client.protocolContext.authProvider!!.joinServer(
|
||||
"https://sessionserver.mojang.com/session/minecraft/join",
|
||||
client.accessToken!!, client.uuid.toString().replace("-", ""), serverHash
|
||||
)
|
||||
}
|
||||
val encryptedSecret = rsaEncrypt(packet.publicKey, sharedSecret)
|
||||
val encryptedVerifyToken = rsaEncrypt(packet.publicKey, packet.verifyToken)
|
||||
client.networkChannel.sendPacket(ServerboundKeyPacket(encryptedSecret, encryptedVerifyToken))
|
||||
client.networkChannel.session.enableEncryption(sharedSecret)
|
||||
}
|
||||
|
||||
internal fun syncServerTick(serverWorldAge: Long) {
|
||||
if (abs(this.currentTick - serverWorldAge) > 2) this.currentTick = serverWorldAge
|
||||
}
|
||||
|
||||
internal fun start() {
|
||||
if (isRunning) return
|
||||
isRunning = true
|
||||
tickJob = client.launch(CoroutineName("LibMC-ClientTickingLoop")) {
|
||||
var nextTickTime = Clock.System.now().toEpochMilliseconds()
|
||||
while (isActive && isRunning) {
|
||||
val now = Clock.System.now().toEpochMilliseconds()
|
||||
if (now >= nextTickTime) {
|
||||
try {
|
||||
listeners.forEach { it.invoke(currentTick) }
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
e.printStackTrace()
|
||||
}
|
||||
currentTick++
|
||||
nextTickTime += tickIntervalMs
|
||||
if (now - nextTickTime > tickIntervalMs * 5) nextTickTime = now + tickIntervalMs
|
||||
} else delay((nextTickTime - now).milliseconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun stop() {
|
||||
isRunning = false
|
||||
tickJob?.cancel()
|
||||
tickJob = null
|
||||
}
|
||||
}
|
||||
+24
-41
@@ -9,10 +9,10 @@ package cn.rtast.libmc.protocol.client
|
||||
import cn.rtast.libmc.crypto.ProtocolContext
|
||||
import cn.rtast.libmc.crypto.ProtocolContextBuilder
|
||||
import cn.rtast.libmc.protocol.network.NetworkChannel
|
||||
import cn.rtast.libmc.protocol.packet.handshake.ServerboundHandshakePacket
|
||||
import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundLoginStartPacket
|
||||
import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent
|
||||
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
|
||||
import cn.rtast.libmc.protocol.protocol.event.PacketEventDispatcher
|
||||
import cn.rtast.libmc.protocol.protocol.session.Session
|
||||
import cn.rtast.libmc.protocol.protocol.session.SessionEvent
|
||||
import cn.rtast.libmc.protocol.protocol.session.SessionImpl
|
||||
import cn.rtast.libmc.protocol.util.TransactionIdManager
|
||||
import cn.rtast.libmc.protocol.util.generateOfflineUuid
|
||||
import kotlinx.coroutines.*
|
||||
@@ -20,68 +20,51 @@ import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
public class MinecraftClient internal constructor(
|
||||
private val host: String,
|
||||
private val port: Int,
|
||||
private val username: String,
|
||||
internal val host: String,
|
||||
internal val port: Int,
|
||||
internal val username: String,
|
||||
internal val uuid: Uuid,
|
||||
internal val accessToken: String?,
|
||||
parentJob: Job?,
|
||||
private val ioDispatcher: CoroutineDispatcher,
|
||||
internal val protocolContext: ProtocolContext,
|
||||
) : PacketEventDispatcher(), CoroutineScope {
|
||||
public val session: SessionImpl = SessionImpl(),
|
||||
) : PacketEventDispatcher(), CoroutineScope, Session by session {
|
||||
internal val stateMachine = ClientStateMachine()
|
||||
public val networkChannel: NetworkChannel = NetworkChannel(host, port, stateMachine, this, protocolContext)
|
||||
public val networkChannel: NetworkChannel = NetworkChannel(this)
|
||||
private val clientJob = SupervisorJob(parentJob)
|
||||
private var listenJob: Job? = null
|
||||
|
||||
public val isOnlineMode: Boolean = accessToken != null
|
||||
|
||||
public val transactionManager: TransactionIdManager = TransactionIdManager()
|
||||
public val clientTickingLoop: ClientTickingLoop = ClientTickingLoop(this)
|
||||
|
||||
/**
|
||||
* Register a client ticking event callback.
|
||||
* NOTE: Blocking operations will **block** the bot thread.
|
||||
* Using #launch to avoid blocking.
|
||||
*/
|
||||
public fun onTick(action: suspend (Long) -> Unit): Unit = run { clientTickingLoop.registerListener(action) }
|
||||
|
||||
public suspend fun connect(protocolVersion: Int = CURRENT_MINECRAFT_PROTOCOL_VERSION) {
|
||||
networkChannel.connect()
|
||||
startListening()
|
||||
clientTickingLoop.start()
|
||||
networkChannel.sendPacket(
|
||||
ServerboundHandshakePacket(
|
||||
protocolVersion,
|
||||
host, port.toUShort(),
|
||||
HandshakeIntent.LOGIN
|
||||
)
|
||||
)
|
||||
stateMachine.transitionTo(ProtocolState.LOGIN)
|
||||
networkChannel.sendPacket(ServerboundLoginStartPacket(username, uuid))
|
||||
init {
|
||||
session.attachClient(this)
|
||||
}
|
||||
|
||||
public fun setCompression(threshold: Int): Unit = networkChannel.setCompression(threshold)
|
||||
public suspend fun connect() {
|
||||
networkChannel.connect()
|
||||
startListening()
|
||||
session.init()
|
||||
session.emitEvent(SessionEvent.ConnectedEvent)
|
||||
}
|
||||
|
||||
private fun startListening() {
|
||||
listenJob = launch {
|
||||
try {
|
||||
while (isActive) networkChannel.readNextPacket()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
if (isActive) {
|
||||
e.printStackTrace()
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) return@launch
|
||||
println("Network read loop exception: ${e.message}")
|
||||
close()
|
||||
}
|
||||
} finally {
|
||||
networkChannel.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun close() {
|
||||
networkChannel.close()
|
||||
clientTickingLoop.stop()
|
||||
listenJob?.cancel()
|
||||
clientJob.cancel()
|
||||
cancel()
|
||||
}
|
||||
|
||||
public override val coroutineContext: CoroutineContext
|
||||
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/5
|
||||
*/
|
||||
|
||||
package cn.rtast.libmc.protocol.client
|
||||
|
||||
import cn.rtast.libmc.packet.MinecraftPacket
|
||||
import cn.rtast.libmc.protocol.protocol.PacketDirection
|
||||
import kotlin.concurrent.Volatile
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
private typealias Handler = suspend (MinecraftPacket) -> Unit
|
||||
private typealias DirectionalHandler = suspend (MinecraftPacket, PacketDirection) -> Unit
|
||||
|
||||
public abstract class PacketEventDispatcher {
|
||||
@Volatile
|
||||
@PublishedApi
|
||||
internal var receiveHandlers: Map<KClass<out MinecraftPacket>, List<Handler>> = emptyMap()
|
||||
|
||||
@Volatile
|
||||
@PublishedApi
|
||||
internal var sentHandlers: Map<KClass<out MinecraftPacket>, List<Handler>> = emptyMap()
|
||||
|
||||
@Volatile
|
||||
@PublishedApi
|
||||
internal var globalHandlers: List<DirectionalHandler> = emptyList()
|
||||
|
||||
@PublishedApi
|
||||
internal fun <T : MinecraftPacket> addTypedHandler(
|
||||
isReceive: Boolean,
|
||||
key: KClass<T>,
|
||||
handler: Handler,
|
||||
) {
|
||||
if (isReceive) {
|
||||
val current = receiveHandlers[key] ?: emptyList()
|
||||
receiveHandlers = receiveHandlers + (key to (current + handler))
|
||||
} else {
|
||||
val current = sentHandlers[key] ?: emptyList()
|
||||
sentHandlers = sentHandlers + (key to (current + handler))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun dispatch(
|
||||
handlersMap: Map<KClass<out MinecraftPacket>, List<Handler>>,
|
||||
packet: MinecraftPacket,
|
||||
direction: PacketDirection,
|
||||
) {
|
||||
handlersMap[packet::class]?.forEach { handler -> handler(packet) }
|
||||
globalHandlers.forEach { handler -> handler(packet, direction) }
|
||||
}
|
||||
|
||||
internal suspend fun dispatchReceive(packet: MinecraftPacket) =
|
||||
dispatch(receiveHandlers, packet, PacketDirection.CLIENTBOUND)
|
||||
|
||||
internal suspend fun dispatchSent(packet: MinecraftPacket) =
|
||||
dispatch(sentHandlers, packet, PacketDirection.SERVERBOUND)
|
||||
|
||||
/**
|
||||
* Lambda will be invoked when received a packet
|
||||
*/
|
||||
public inline fun <reified T : MinecraftPacket> onPacket(crossinline block: suspend (T) -> Unit) {
|
||||
addTypedHandler(true, T::class) { block(it as T) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Lambda will be invoked after a packet sent
|
||||
*/
|
||||
public inline fun <reified T : MinecraftPacket> onSent(crossinline block: suspend (T) -> Unit) {
|
||||
addTypedHandler(false, T::class) { block(it as T) }
|
||||
}
|
||||
|
||||
/**
|
||||
* All packets will be appeared here, including `Outbound(Serverbound)` and `Inbound(Clientbound)` packet
|
||||
*/
|
||||
public fun on(block: suspend (packet: MinecraftPacket, direction: PacketDirection) -> Unit) {
|
||||
globalHandlers = globalHandlers + block
|
||||
}
|
||||
}
|
||||
+13
-20
@@ -6,34 +6,27 @@
|
||||
|
||||
package cn.rtast.libmc.protocol.network
|
||||
|
||||
import cn.rtast.libmc.crypto.ProtocolContext
|
||||
import cn.rtast.libmc.network.BytesBuffer
|
||||
import cn.rtast.libmc.network.wrap
|
||||
import cn.rtast.libmc.packet.MinecraftPacket
|
||||
import cn.rtast.libmc.packet.writeBuffer
|
||||
import cn.rtast.libmc.primitives.readVarInt
|
||||
import cn.rtast.libmc.primitives.writeVarInt
|
||||
import cn.rtast.libmc.protocol.client.ClientStateMachine
|
||||
import cn.rtast.libmc.protocol.client.PacketEventDispatcher
|
||||
import cn.rtast.libmc.protocol.client.MinecraftClient
|
||||
import cn.rtast.libmc.protocol.protocol.event.PacketEventDispatcher
|
||||
import cn.rtast.libmc.protocol.protocol.GamePacketsProtocolCodec.clientboundGameProtocols
|
||||
import cn.rtast.libmc.protocol.protocol.GamePacketsProtocolCodec.serverboundGameProtocols
|
||||
import cn.rtast.libmc.zlibCompress
|
||||
import cn.rtast.libmc.zlibDecompress
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
public class NetworkChannel internal constructor(
|
||||
host: String,
|
||||
port: Int,
|
||||
private val stateMachine: ClientStateMachine,
|
||||
private val dispatcher: PacketEventDispatcher,
|
||||
protocolContext: ProtocolContext,
|
||||
) {
|
||||
internal val session: NetworkSession = NetworkSession(host, port, protocolContext)
|
||||
public class NetworkChannel internal constructor(private val client: MinecraftClient) {
|
||||
internal val networkSession: NetworkSession = NetworkSession(client)
|
||||
|
||||
@Volatile
|
||||
private var threshold = -1
|
||||
|
||||
public suspend fun connect(): Unit = session.connect()
|
||||
public suspend fun connect(): Unit = networkSession.connect()
|
||||
public fun setCompression(threshold: Int): Unit = run { this.threshold = threshold }
|
||||
|
||||
/**
|
||||
@@ -42,8 +35,8 @@ public class NetworkChannel internal constructor(
|
||||
* Use [PacketEventDispatcher.onPacket] to get packet event
|
||||
*/
|
||||
public suspend fun readNextPacket(): MinecraftPacket {
|
||||
val packetLength = session.readVarInt()
|
||||
val frameBuf = session.readBytes(packetLength).wrap()
|
||||
val packetLength = networkSession.readVarInt()
|
||||
val frameBuf = networkSession.readBytes(packetLength).wrap()
|
||||
val payloadBuf = if (threshold < 0) frameBuf else {
|
||||
val dataLength = frameBuf.readVarInt()
|
||||
if (dataLength == 0) frameBuf else {
|
||||
@@ -51,10 +44,10 @@ public class NetworkChannel internal constructor(
|
||||
compressedBytes.zlibDecompress(dataLength).wrap()
|
||||
}
|
||||
}
|
||||
val currentState = stateMachine.currentState
|
||||
val currentState = client.stateMachine.currentState
|
||||
val packetId = payloadBuf.readVarInt()
|
||||
val packet = clientboundGameProtocols.getRegistry(currentState).decodePacket(packetId, payloadBuf)
|
||||
dispatcher.dispatchReceive(packet)
|
||||
client.dispatchReceive(packet, client.session)
|
||||
return packet
|
||||
}
|
||||
|
||||
@@ -65,7 +58,7 @@ public class NetworkChannel internal constructor(
|
||||
*/
|
||||
public suspend fun sendPacket(packet: MinecraftPacket) {
|
||||
val uncompressedBodyBuf = BytesBuffer()
|
||||
serverboundGameProtocols.getRegistry(stateMachine.currentState).encodePacket(uncompressedBodyBuf, packet)
|
||||
serverboundGameProtocols.getRegistry(client.stateMachine.currentState).encodePacket(uncompressedBodyBuf, packet)
|
||||
val uncompressedData = uncompressedBodyBuf.toByteArray()
|
||||
val frameBuffer = BytesBuffer()
|
||||
if (threshold < 0) {
|
||||
@@ -84,9 +77,9 @@ public class NetworkChannel internal constructor(
|
||||
frameBuffer.writeVarInt(contentBuf.size)
|
||||
frameBuffer.writeBuffer(contentBuf)
|
||||
}
|
||||
session.writeFully(frameBuffer.toByteArray())
|
||||
dispatcher.dispatchSent(packet)
|
||||
networkSession.writeFully(frameBuffer.toByteArray())
|
||||
client.dispatchSent(packet, client.session)
|
||||
}
|
||||
|
||||
public fun close(): Unit = session.close()
|
||||
public fun close(): Unit = networkSession.close()
|
||||
}
|
||||
+3
-7
@@ -6,18 +6,14 @@
|
||||
|
||||
package cn.rtast.libmc.protocol.network
|
||||
|
||||
import cn.rtast.libmc.crypto.ProtocolContext
|
||||
import cn.rtast.libmc.network.RawSocket
|
||||
import cn.rtast.libmc.network.ReadChannel
|
||||
import cn.rtast.libmc.network.WriteChannel
|
||||
import cn.rtast.libmc.primitives.readVarInt
|
||||
import cn.rtast.libmc.protocol.client.MinecraftClient
|
||||
import cn.rtast.libmc.protocol.crypto.Aes128Cfb8ChannelCipher
|
||||
|
||||
public class NetworkSession internal constructor(
|
||||
private val host: String,
|
||||
private val port: Int,
|
||||
private val context: ProtocolContext,
|
||||
) {
|
||||
public class NetworkSession internal constructor(private val client: MinecraftClient) {
|
||||
private var socket: RawSocket? = null
|
||||
public var readChannel: ReadChannel? = null
|
||||
private set
|
||||
@@ -26,7 +22,7 @@ public class NetworkSession internal constructor(
|
||||
private set
|
||||
|
||||
public suspend fun connect() {
|
||||
val sk = context.createSocket(host, port)
|
||||
val sk = client.protocolContext.createSocket(client.host, client.port)
|
||||
sk.connect()
|
||||
this.socket = sk
|
||||
this.readChannel = sk.openReadChannel()
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/10
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.libmc.protocol.protocol.event
|
||||
|
||||
public fun interface ListenerRegistration {
|
||||
public suspend fun unregister()
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/5
|
||||
*/
|
||||
|
||||
package cn.rtast.libmc.protocol.protocol.event
|
||||
|
||||
import cn.rtast.libmc.packet.MinecraftPacket
|
||||
import cn.rtast.libmc.protocol.protocol.PacketDirection
|
||||
import cn.rtast.libmc.protocol.protocol.session.Session
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
private typealias Handler = suspend Session.(MinecraftPacket) -> Unit
|
||||
private typealias DirectionalHandler = suspend Session.(MinecraftPacket, PacketDirection) -> Unit
|
||||
|
||||
public abstract class PacketEventDispatcher {
|
||||
private val mutex = Mutex()
|
||||
|
||||
@PublishedApi
|
||||
internal var receiveHandlers: Map<KClass<out MinecraftPacket>, List<Handler>> = emptyMap()
|
||||
|
||||
@PublishedApi
|
||||
internal var sentHandlers: Map<KClass<out MinecraftPacket>, List<Handler>> = emptyMap()
|
||||
|
||||
@PublishedApi
|
||||
internal var globalHandlers: List<DirectionalHandler> = emptyList()
|
||||
|
||||
@PublishedApi
|
||||
internal suspend fun <T : MinecraftPacket> addTypedHandler(
|
||||
isReceive: Boolean,
|
||||
key: KClass<T>,
|
||||
handler: Handler,
|
||||
): ListenerRegistration {
|
||||
mutex.withLock {
|
||||
if (isReceive) {
|
||||
val current = receiveHandlers[key] ?: emptyList()
|
||||
receiveHandlers = receiveHandlers + (key to (current + handler))
|
||||
} else {
|
||||
val current = sentHandlers[key] ?: emptyList()
|
||||
sentHandlers = sentHandlers + (key to (current + handler))
|
||||
}
|
||||
}
|
||||
return ListenerRegistration { removeTypedHandler(isReceive, key, handler) }
|
||||
}
|
||||
|
||||
@PublishedApi
|
||||
internal suspend fun <T : MinecraftPacket> removeTypedHandler(
|
||||
isReceive: Boolean,
|
||||
key: KClass<T>,
|
||||
handler: Handler,
|
||||
) {
|
||||
mutex.withLock {
|
||||
if (isReceive) {
|
||||
val current = receiveHandlers[key] ?: return@withLock
|
||||
val updated = current - handler
|
||||
receiveHandlers = if (updated.isEmpty()) receiveHandlers - key else receiveHandlers + (key to updated)
|
||||
} else {
|
||||
val current = sentHandlers[key] ?: return@withLock
|
||||
val updated = current - handler
|
||||
sentHandlers = if (updated.isEmpty()) sentHandlers - key else sentHandlers + (key to updated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun dispatch(
|
||||
session: Session,
|
||||
isReceive: Boolean,
|
||||
packet: MinecraftPacket,
|
||||
direction: PacketDirection,
|
||||
) {
|
||||
val (typeHandlers, globals) = mutex.withLock {
|
||||
val map = if (isReceive) receiveHandlers else sentHandlers
|
||||
Pair(map[packet::class], globalHandlers)
|
||||
}
|
||||
typeHandlers?.forEach { handler -> handler(session, packet) }
|
||||
globals.forEach { handler -> handler(session, packet, direction) }
|
||||
}
|
||||
|
||||
internal suspend fun dispatchReceive(packet: MinecraftPacket, session: Session) =
|
||||
dispatch(session, true, packet, PacketDirection.CLIENTBOUND)
|
||||
|
||||
internal suspend fun dispatchSent(packet: MinecraftPacket, session: Session) =
|
||||
dispatch(session, false, packet, PacketDirection.SERVERBOUND)
|
||||
|
||||
public suspend inline fun <reified T : MinecraftPacket> onPacket(noinline block: suspend Session.(T) -> Unit): ListenerRegistration {
|
||||
val handler: Handler = { block(it as T) }
|
||||
return addTypedHandler(true, T::class, handler)
|
||||
}
|
||||
|
||||
public suspend inline fun <reified T : MinecraftPacket> onSent(noinline block: suspend Session.(T) -> Unit): ListenerRegistration {
|
||||
val handler: Handler = { block(it as T) }
|
||||
return addTypedHandler(false, T::class, handler)
|
||||
}
|
||||
|
||||
public suspend fun on(block: suspend Session.(packet: MinecraftPacket, direction: PacketDirection) -> Unit): ListenerRegistration {
|
||||
mutex.withLock { globalHandlers = globalHandlers + block }
|
||||
return ListenerRegistration { mutex.withLock { globalHandlers = globalHandlers - block } }
|
||||
}
|
||||
}
|
||||
+118
-115
@@ -9,10 +9,11 @@ package cn.rtast.libmc.protocol.protocol.game.chat
|
||||
import cn.rtast.libmc.nbt.NBTCompound
|
||||
import cn.rtast.libmc.nbt.NBTTag
|
||||
import cn.rtast.libmc.nbt.NBTType
|
||||
import cn.rtast.libmc.nbt.toJsonString
|
||||
import cn.rtast.libmc.network.BytesBuffer
|
||||
import cn.rtast.libmc.protocol.protocol.game.Identifier
|
||||
import cn.rtast.libmc.protocol.protocol.util.readNetworkNBTCompound
|
||||
import cn.rtast.libmc.protocol.protocol.util.writeNetworkNBTCompound
|
||||
import cn.rtast.libmc.network.BytesBuffer
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
public data class TextComponent(
|
||||
@@ -223,6 +224,8 @@ public data class TextComponent(
|
||||
}
|
||||
}
|
||||
|
||||
public fun toJsonString(): String = this.toNBTCompound().toJsonString()
|
||||
|
||||
public companion object {
|
||||
public fun of(text: String): TextComponent = TextComponent(content = Content.PlainText(text))
|
||||
}
|
||||
@@ -261,6 +264,117 @@ public fun NBTTag.toTextComponent(): TextComponent {
|
||||
}
|
||||
}
|
||||
|
||||
public fun TextComponent.toNBTCompound(): NBTTag.CompoundTag {
|
||||
val map = mutableMapOf<String, NBTTag>()
|
||||
when (val c = this.content) {
|
||||
is TextComponent.Content.PlainText -> {
|
||||
map["type"] = NBTTag.StringTag("text")
|
||||
map["text"] = NBTTag.StringTag(c.text)
|
||||
}
|
||||
|
||||
is TextComponent.Content.Translatable -> {
|
||||
map["type"] = NBTTag.StringTag("translatable")
|
||||
map["translate"] = NBTTag.StringTag(c.key)
|
||||
c.fallback?.let { map["fallback"] = NBTTag.StringTag(it) }
|
||||
if (c.args.isNotEmpty()) {
|
||||
map["with"] = NBTTag.ListTag(NBTType.List, c.args.map { it.toNBTCompound() as NBTTag }.toMutableList())
|
||||
}
|
||||
}
|
||||
|
||||
is TextComponent.Content.Score -> {
|
||||
map["type"] = NBTTag.StringTag("score")
|
||||
map["score"] = NBTTag.CompoundTag(
|
||||
mutableMapOf(
|
||||
"name" to NBTTag.StringTag(c.name),
|
||||
"objective" to NBTTag.StringTag(c.objective)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is TextComponent.Content.Selector -> {
|
||||
map["type"] = NBTTag.StringTag("selector")
|
||||
map["selector"] = NBTTag.StringTag(c.selector)
|
||||
c.separator?.let { map["separator"] = it.toNBTCompound() }
|
||||
}
|
||||
|
||||
is TextComponent.Content.Keybind -> {
|
||||
map["type"] = NBTTag.StringTag("keybind")
|
||||
map["keybind"] = NBTTag.StringTag(c.keybind)
|
||||
}
|
||||
|
||||
is TextComponent.Content.Nbt -> {
|
||||
map["type"] = NBTTag.StringTag("nbt")
|
||||
map["nbt"] = NBTTag.StringTag(c.path)
|
||||
map["interpret"] = NBTTag.ByteTag(if (c.interpret) 1 else 0)
|
||||
map["plain"] = NBTTag.ByteTag(if (c.plain) 1 else 0)
|
||||
c.separator?.let { map["separator"] = it.toNBTCompound() }
|
||||
when (val s = c.source) {
|
||||
is TextComponent.Content.Nbt.NbtSource.Entity -> map["entity"] = NBTTag.StringTag(s.selector)
|
||||
is TextComponent.Content.Nbt.NbtSource.Block -> map["block"] = NBTTag.StringTag(s.coordinates)
|
||||
is TextComponent.Content.Nbt.NbtSource.Storage -> map["storage"] = NBTTag.StringTag(s.id.toString())
|
||||
}
|
||||
}
|
||||
|
||||
is TextComponent.Content.ObjectContent.Atlas -> {
|
||||
map["type"] = NBTTag.StringTag("object")
|
||||
map["object"] = NBTTag.StringTag("atlas")
|
||||
map["sprite"] = NBTTag.StringTag(c.sprite.toString())
|
||||
map["atlas"] = NBTTag.StringTag(c.atlas.toString())
|
||||
}
|
||||
|
||||
is TextComponent.Content.ObjectContent.Player -> {
|
||||
map["type"] = NBTTag.StringTag("object")
|
||||
map["object"] = NBTTag.StringTag("player")
|
||||
map["hat"] = NBTTag.ByteTag(if (c.hat) 1 else 0)
|
||||
when (val p = c.profile) {
|
||||
is TextComponent.PlayerProfile.Name -> map["player"] = NBTTag.StringTag(p.name)
|
||||
is TextComponent.PlayerProfile.FullProfile -> {
|
||||
val pMap = mutableMapOf<String, NBTTag>()
|
||||
p.name?.let { pMap["name"] = NBTTag.StringTag(it) }
|
||||
p.id?.let { pMap["id"] = NBTTag.StringTag(it.toString()) }
|
||||
map["player"] = NBTTag.CompoundTag(pMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.style.color?.let {
|
||||
map["color"] = NBTTag.StringTag(
|
||||
when (it) {
|
||||
is TextComponent.TextColor.Named -> it.name
|
||||
is TextComponent.TextColor.Hex -> it.hex
|
||||
}
|
||||
)
|
||||
}
|
||||
this.style.font?.let { map["font"] = NBTTag.StringTag(it.toString()) }
|
||||
this.style.bold?.let { map["bold"] = NBTTag.ByteTag(if (it) 1 else 0) }
|
||||
this.style.italic?.let { map["italic"] = NBTTag.ByteTag(if (it) 1 else 0) }
|
||||
this.style.underlined?.let { map["underlined"] = NBTTag.ByteTag(if (it) 1 else 0) }
|
||||
this.style.strikethrough?.let { map["strikethrough"] = NBTTag.ByteTag(if (it) 1 else 0) }
|
||||
this.style.obfuscated?.let { map["obfuscated"] = NBTTag.ByteTag(if (it) 1 else 0) }
|
||||
this.style.shadowColor?.let {
|
||||
when (it) {
|
||||
is TextComponent.ShadowColor.ArgbInt -> map["shadow_color"] = NBTTag.IntTag(it.argb.toInt())
|
||||
is TextComponent.ShadowColor.RgbaFloat -> map["shadow_color"] = NBTTag.ListTag(
|
||||
NBTType.List, mutableListOf(
|
||||
NBTTag.FloatTag(it.red),
|
||||
NBTTag.FloatTag(it.green),
|
||||
NBTTag.FloatTag(it.blue),
|
||||
NBTTag.FloatTag(it.alpha)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.extra.isNotEmpty()) map["extra"] =
|
||||
NBTTag.ListTag(NBTType.List, this.extra.map { it.toNBTCompound() as NBTTag }.toMutableList())
|
||||
this.insertion?.let { map["insertion"] = NBTTag.StringTag(it) }
|
||||
this.clickEvent?.let { map["click_event"] = encodeClickEventToNbt(it) }
|
||||
this.hoverEvent?.let { map["hover_event"] = encodeHoverEventToNbt(it) }
|
||||
|
||||
return NBTTag.CompoundTag(map)
|
||||
}
|
||||
|
||||
private fun parseContentFromNbt(tag: NBTTag.CompoundTag): TextComponent.Content {
|
||||
val type = tag.getStringOrNull("type")
|
||||
return when {
|
||||
@@ -437,117 +551,6 @@ private fun parseHoverEventFromNbt(tag: NBTTag.CompoundTag): TextComponent.Hover
|
||||
}
|
||||
}
|
||||
|
||||
public fun TextComponent.toNbt(): NBTTag.CompoundTag {
|
||||
val map = mutableMapOf<String, NBTTag>()
|
||||
when (val c = this.content) {
|
||||
is TextComponent.Content.PlainText -> {
|
||||
map["type"] = NBTTag.StringTag("text")
|
||||
map["text"] = NBTTag.StringTag(c.text)
|
||||
}
|
||||
|
||||
is TextComponent.Content.Translatable -> {
|
||||
map["type"] = NBTTag.StringTag("translatable")
|
||||
map["translate"] = NBTTag.StringTag(c.key)
|
||||
c.fallback?.let { map["fallback"] = NBTTag.StringTag(it) }
|
||||
if (c.args.isNotEmpty()) {
|
||||
map["with"] = NBTTag.ListTag(NBTType.List, c.args.map { it.toNbt() as NBTTag }.toMutableList())
|
||||
}
|
||||
}
|
||||
|
||||
is TextComponent.Content.Score -> {
|
||||
map["type"] = NBTTag.StringTag("score")
|
||||
map["score"] = NBTTag.CompoundTag(
|
||||
mutableMapOf(
|
||||
"name" to NBTTag.StringTag(c.name),
|
||||
"objective" to NBTTag.StringTag(c.objective)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is TextComponent.Content.Selector -> {
|
||||
map["type"] = NBTTag.StringTag("selector")
|
||||
map["selector"] = NBTTag.StringTag(c.selector)
|
||||
c.separator?.let { map["separator"] = it.toNbt() }
|
||||
}
|
||||
|
||||
is TextComponent.Content.Keybind -> {
|
||||
map["type"] = NBTTag.StringTag("keybind")
|
||||
map["keybind"] = NBTTag.StringTag(c.keybind)
|
||||
}
|
||||
|
||||
is TextComponent.Content.Nbt -> {
|
||||
map["type"] = NBTTag.StringTag("nbt")
|
||||
map["nbt"] = NBTTag.StringTag(c.path)
|
||||
map["interpret"] = NBTTag.ByteTag(if (c.interpret) 1 else 0)
|
||||
map["plain"] = NBTTag.ByteTag(if (c.plain) 1 else 0)
|
||||
c.separator?.let { map["separator"] = it.toNbt() }
|
||||
when (val s = c.source) {
|
||||
is TextComponent.Content.Nbt.NbtSource.Entity -> map["entity"] = NBTTag.StringTag(s.selector)
|
||||
is TextComponent.Content.Nbt.NbtSource.Block -> map["block"] = NBTTag.StringTag(s.coordinates)
|
||||
is TextComponent.Content.Nbt.NbtSource.Storage -> map["storage"] = NBTTag.StringTag(s.id.toString())
|
||||
}
|
||||
}
|
||||
|
||||
is TextComponent.Content.ObjectContent.Atlas -> {
|
||||
map["type"] = NBTTag.StringTag("object")
|
||||
map["object"] = NBTTag.StringTag("atlas")
|
||||
map["sprite"] = NBTTag.StringTag(c.sprite.toString())
|
||||
map["atlas"] = NBTTag.StringTag(c.atlas.toString())
|
||||
}
|
||||
|
||||
is TextComponent.Content.ObjectContent.Player -> {
|
||||
map["type"] = NBTTag.StringTag("object")
|
||||
map["object"] = NBTTag.StringTag("player")
|
||||
map["hat"] = NBTTag.ByteTag(if (c.hat) 1 else 0)
|
||||
when (val p = c.profile) {
|
||||
is TextComponent.PlayerProfile.Name -> map["player"] = NBTTag.StringTag(p.name)
|
||||
is TextComponent.PlayerProfile.FullProfile -> {
|
||||
val pMap = mutableMapOf<String, NBTTag>()
|
||||
p.name?.let { pMap["name"] = NBTTag.StringTag(it) }
|
||||
p.id?.let { pMap["id"] = NBTTag.StringTag(it.toString()) }
|
||||
map["player"] = NBTTag.CompoundTag(pMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.style.color?.let {
|
||||
map["color"] = NBTTag.StringTag(
|
||||
when (it) {
|
||||
is TextComponent.TextColor.Named -> it.name
|
||||
is TextComponent.TextColor.Hex -> it.hex
|
||||
}
|
||||
)
|
||||
}
|
||||
this.style.font?.let { map["font"] = NBTTag.StringTag(it.toString()) }
|
||||
this.style.bold?.let { map["bold"] = NBTTag.ByteTag(if (it) 1 else 0) }
|
||||
this.style.italic?.let { map["italic"] = NBTTag.ByteTag(if (it) 1 else 0) }
|
||||
this.style.underlined?.let { map["underlined"] = NBTTag.ByteTag(if (it) 1 else 0) }
|
||||
this.style.strikethrough?.let { map["strikethrough"] = NBTTag.ByteTag(if (it) 1 else 0) }
|
||||
this.style.obfuscated?.let { map["obfuscated"] = NBTTag.ByteTag(if (it) 1 else 0) }
|
||||
this.style.shadowColor?.let {
|
||||
when (it) {
|
||||
is TextComponent.ShadowColor.ArgbInt -> map["shadow_color"] = NBTTag.IntTag(it.argb.toInt())
|
||||
is TextComponent.ShadowColor.RgbaFloat -> map["shadow_color"] = NBTTag.ListTag(
|
||||
NBTType.List, mutableListOf(
|
||||
NBTTag.FloatTag(it.red),
|
||||
NBTTag.FloatTag(it.green),
|
||||
NBTTag.FloatTag(it.blue),
|
||||
NBTTag.FloatTag(it.alpha)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.extra.isNotEmpty()) map["extra"] =
|
||||
NBTTag.ListTag(NBTType.List, this.extra.map { it.toNbt() as NBTTag }.toMutableList())
|
||||
this.insertion?.let { map["insertion"] = NBTTag.StringTag(it) }
|
||||
this.clickEvent?.let { map["click_event"] = encodeClickEventToNbt(it) }
|
||||
this.hoverEvent?.let { map["hover_event"] = encodeHoverEventToNbt(it) }
|
||||
|
||||
return NBTTag.CompoundTag(map)
|
||||
}
|
||||
|
||||
private fun encodeClickEventToNbt(event: TextComponent.ClickEvent): NBTTag.CompoundTag {
|
||||
val map = mutableMapOf<String, NBTTag>("action" to NBTTag.StringTag(event.action.text))
|
||||
when (event) {
|
||||
@@ -575,7 +578,7 @@ private fun encodeClickEventToNbt(event: TextComponent.ClickEvent): NBTTag.Compo
|
||||
private fun encodeHoverEventToNbt(event: TextComponent.HoverEvent): NBTTag.CompoundTag {
|
||||
val map = mutableMapOf<String, NBTTag>("action" to NBTTag.StringTag(event.action.text))
|
||||
when (event) {
|
||||
is TextComponent.HoverEvent.ShowText -> map["value"] = event.component.toNbt()
|
||||
is TextComponent.HoverEvent.ShowText -> map["value"] = event.component.toNBTCompound()
|
||||
is TextComponent.HoverEvent.ShowItem -> {
|
||||
map["id"] = NBTTag.StringTag(event.id.toString())
|
||||
map["count"] = NBTTag.IntTag(event.count)
|
||||
@@ -590,7 +593,7 @@ private fun encodeHoverEventToNbt(event: TextComponent.HoverEvent): NBTTag.Compo
|
||||
intArrayOf(u.i1, u.i2, u.i3, u.i4)
|
||||
)
|
||||
}
|
||||
event.name?.let { map["name"] = it.toNbt() }
|
||||
event.name?.let { map["name"] = it.toNBTCompound() }
|
||||
}
|
||||
}
|
||||
return NBTTag.CompoundTag(map)
|
||||
@@ -617,6 +620,6 @@ internal fun BytesBuffer.readTextComponent(): TextComponent =
|
||||
this.readNetworkNBTCompound().element.toTextComponent()
|
||||
|
||||
internal fun BytesBuffer.writeTextComponent(component: TextComponent) {
|
||||
val nbt = component.toNbt()
|
||||
val nbt = component.toNBTCompound()
|
||||
this.writeNetworkNBTCompound(NBTCompound("", nbt))
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/10
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.libmc.protocol.protocol.session
|
||||
|
||||
import cn.rtast.libmc.protocol.client.CURRENT_MINECRAFT_PROTOCOL_VERSION
|
||||
import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
public interface Session {
|
||||
@Suppress("FunctionName")
|
||||
public fun <T : SessionEvent> _onEvent(clazz: KClass<T>, block: suspend Session.(T) -> Unit)
|
||||
public suspend fun emitEvent(event: SessionEvent): Unit?
|
||||
public suspend fun login(protocolVersion: Int = CURRENT_MINECRAFT_PROTOCOL_VERSION)
|
||||
public suspend fun handshake(
|
||||
protocolVersion: Int = CURRENT_MINECRAFT_PROTOCOL_VERSION,
|
||||
intent: HandshakeIntent = HandshakeIntent.LOGIN,
|
||||
)
|
||||
|
||||
public suspend fun status(protocolVersion: Int = CURRENT_MINECRAFT_PROTOCOL_VERSION): String
|
||||
public suspend fun disconnect()
|
||||
public suspend fun init()
|
||||
}
|
||||
|
||||
public inline fun <reified T : SessionEvent> Session.onEvent(noinline block: suspend Session.(T) -> Unit) {
|
||||
_onEvent(T::class, block)
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright © 2026 RTAkland
|
||||
* Author: RTAkland
|
||||
* Date: 2026/9/10
|
||||
*/
|
||||
|
||||
|
||||
package cn.rtast.libmc.protocol.protocol.session
|
||||
|
||||
import cn.rtast.libmc.protocol.protocol.game.chat.TextComponent
|
||||
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
|
||||
|
||||
public sealed interface SessionEvent {
|
||||
public data class DisconnectedEvent(val reason: TextComponent, val state: ProtocolState) : SessionEvent
|
||||
public object ConnectedEvent : SessionEvent
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
package cn.rtast.libmc.protocol.protocol.session
|
||||
|
||||
import cn.rtast.libmc.protocol.client.MinecraftClient
|
||||
import cn.rtast.libmc.protocol.crypto.minecraftServerIdHash
|
||||
import cn.rtast.libmc.protocol.crypto.rsaEncrypt
|
||||
import cn.rtast.libmc.protocol.packet.configuration.clientbound.*
|
||||
import cn.rtast.libmc.protocol.packet.configuration.serverbound.ServerboundAckFinishConfigurationPacket
|
||||
import cn.rtast.libmc.protocol.packet.configuration.serverbound.ServerboundKeepAliveConfigurationPacket
|
||||
import cn.rtast.libmc.protocol.packet.configuration.serverbound.ServerboundPongConfigurationPacket
|
||||
import cn.rtast.libmc.protocol.packet.configuration.serverbound.ServerboundSelectKnownPacksPacket
|
||||
import cn.rtast.libmc.protocol.packet.handshake.ServerboundHandshakePacket
|
||||
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundDisconnectLoginPacket
|
||||
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundHelloPacket
|
||||
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundLoginSuccessPacket
|
||||
import cn.rtast.libmc.protocol.packet.login.clientbound.ClientboundSetCompressionPacket
|
||||
import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundKeyPacket
|
||||
import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundLoginAcknowledgedPacket
|
||||
import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundLoginStartPacket
|
||||
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundDisconnectPlayPacket
|
||||
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundKeepAlivePlayPacket
|
||||
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundPingPacket
|
||||
import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundKeepAlivePlayPacket
|
||||
import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundPongPlayPacket
|
||||
import cn.rtast.libmc.protocol.packet.status.clientbound.ClientboundStatusResponsePacket
|
||||
import cn.rtast.libmc.protocol.packet.status.serverbound.ServerboundStatusRequestPacket
|
||||
import cn.rtast.libmc.protocol.protocol.event.ListenerRegistration
|
||||
import cn.rtast.libmc.protocol.protocol.game.chat.TextComponent
|
||||
import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent
|
||||
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
|
||||
import cn.rtast.libmc.protocol.util.generateRandom16Bytes
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
private typealias EventHandler = suspend Session.(SessionEvent) -> Unit
|
||||
|
||||
public class SessionImpl internal constructor() : Session {
|
||||
private lateinit var client: MinecraftClient
|
||||
internal fun attachClient(client: MinecraftClient) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
private val stateMachine get() = client.stateMachine
|
||||
private val networkChannel get() = client.networkChannel
|
||||
|
||||
@PublishedApi
|
||||
internal val eventListener: HashMap<KClass<out SessionEvent>, MutableList<EventHandler>> = hashMapOf()
|
||||
|
||||
override suspend fun init() {
|
||||
client.onPacket<ClientboundDisconnectLoginPacket> {
|
||||
emitEvent(SessionEvent.DisconnectedEvent(it.reason, stateMachine.currentState))
|
||||
}
|
||||
client.onPacket<ClientboundDisconnectPlayPacket> {
|
||||
emitEvent(SessionEvent.DisconnectedEvent(it.reason, stateMachine.currentState))
|
||||
}
|
||||
client.onPacket<ClientboundDisconnectConfigurationPacket> {
|
||||
emitEvent(SessionEvent.DisconnectedEvent(it.reason, stateMachine.currentState))
|
||||
}
|
||||
|
||||
client.onPacket<ClientboundHelloPacket> { acceptEncryption(it) }
|
||||
client.onPacket<ClientboundSetCompressionPacket> { setCompression(it.threshold) }
|
||||
client.onPacket<ClientboundLoginSuccessPacket> { acknowledgeLogin() }
|
||||
client.onPacket<ClientboundFinishConfigurationPacket> { finishConfiguration() }
|
||||
client.onPacket<ClientboundPingPacket> { networkChannel.sendPacket(ServerboundPongPlayPacket(it.id)) }
|
||||
client.onPacket<ClientboundKeepAlivePlayPacket> { networkChannel.sendPacket(ServerboundKeepAlivePlayPacket(it.id)) }
|
||||
client.onPacket<ClientboundKeepAliveConfigurationPacket> {
|
||||
networkChannel.sendPacket(ServerboundKeepAliveConfigurationPacket(it.id))
|
||||
}
|
||||
client.onPacket<ClientboundPingConfigurationPacket> {
|
||||
networkChannel.sendPacket(ServerboundPongConfigurationPacket(it.id))
|
||||
}
|
||||
client.onPacket<ClientboundSelectKnownPacksPacket> {
|
||||
networkChannel.sendPacket(ServerboundSelectKnownPacksPacket(emptyList()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : SessionEvent> _onEvent(clazz: KClass<T>, block: suspend Session.(T) -> Unit) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
this.eventListener.getOrPut(clazz) { mutableListOf() }.add { block(it as T) }
|
||||
}
|
||||
|
||||
override suspend fun emitEvent(event: SessionEvent): Unit? =
|
||||
eventListener[event::class]?.forEach { handler -> handler(event) }
|
||||
|
||||
override suspend fun login(protocolVersion: Int) {
|
||||
handshake(protocolVersion, HandshakeIntent.LOGIN)
|
||||
loginStart()
|
||||
}
|
||||
|
||||
override suspend fun handshake(protocolVersion: Int, intent: HandshakeIntent) {
|
||||
ensureState(ProtocolState.HANDSHAKE)
|
||||
client.networkChannel.sendPacket(
|
||||
ServerboundHandshakePacket(protocolVersion, client.host, client.port.toUShort(), intent)
|
||||
)
|
||||
val targetState = if (intent == HandshakeIntent.LOGIN) ProtocolState.LOGIN else {
|
||||
if (intent == HandshakeIntent.TRANSFER) ProtocolState.HANDSHAKE else ProtocolState.STATUS
|
||||
}
|
||||
stateMachine.transitionTo(targetState)
|
||||
}
|
||||
|
||||
override suspend fun status(protocolVersion: Int): String =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
client.launch {
|
||||
try {
|
||||
var reg: ListenerRegistration? = null
|
||||
reg = client.onPacket<ClientboundStatusResponsePacket> { packet ->
|
||||
reg?.unregister()
|
||||
if (continuation.isActive) continuation.resume(packet.jsonResponse)
|
||||
}
|
||||
handshake(protocolVersion, HandshakeIntent.STATUS)
|
||||
networkChannel.sendPacket(ServerboundStatusRequestPacket)
|
||||
} catch (e: Exception) {
|
||||
if (continuation.isActive) continuation.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun loginStart() {
|
||||
ensureState(ProtocolState.LOGIN)
|
||||
networkChannel.sendPacket(ServerboundLoginStartPacket(client.username, client.uuid))
|
||||
}
|
||||
|
||||
internal suspend fun acceptEncryption(context: ClientboundHelloPacket) {
|
||||
ensureState(ProtocolState.LOGIN)
|
||||
val sharedSecret = generateRandom16Bytes()
|
||||
val serverHash = minecraftServerIdHash(context.serverId, sharedSecret, context.publicKey)
|
||||
client.protocolContext.authProvider!!.joinServer(
|
||||
"https://sessionserver.mojang.com/session/minecraft/join",
|
||||
client.accessToken!!, client.uuid.toString().replace("-", ""), serverHash
|
||||
)
|
||||
val encryptedSecret = rsaEncrypt(context.publicKey, sharedSecret)
|
||||
val encryptedVerifyToken = rsaEncrypt(context.publicKey, context.verifyToken)
|
||||
networkChannel.sendPacket(ServerboundKeyPacket(encryptedSecret, encryptedVerifyToken))
|
||||
networkChannel.networkSession.enableEncryption(sharedSecret)
|
||||
}
|
||||
|
||||
internal suspend fun acknowledgeLogin() {
|
||||
ensureState(ProtocolState.LOGIN)
|
||||
networkChannel.sendPacket(ServerboundLoginAcknowledgedPacket)
|
||||
stateMachine.transitionTo(ProtocolState.CONFIGURATION)
|
||||
}
|
||||
|
||||
internal suspend fun finishConfiguration() {
|
||||
ensureState(ProtocolState.CONFIGURATION)
|
||||
networkChannel.sendPacket(ServerboundAckFinishConfigurationPacket)
|
||||
stateMachine.transitionTo(ProtocolState.PLAY)
|
||||
}
|
||||
|
||||
internal fun setCompression(threshold: Int) {
|
||||
ensureState(ProtocolState.LOGIN, ProtocolState.CONFIGURATION)
|
||||
networkChannel.setCompression(threshold)
|
||||
}
|
||||
|
||||
override suspend fun disconnect(): Unit = client.close().apply {
|
||||
emitEvent(
|
||||
SessionEvent.DisconnectedEvent(
|
||||
TextComponent(TextComponent.Content.PlainText("LibMC-Disconnected by calling disconnect()")),
|
||||
stateMachine.currentState
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun ensureState(vararg allowedStates: ProtocolState) {
|
||||
if (stateMachine.currentState !in allowedStates) throw IllegalStateException(
|
||||
"Cannot perform this action in state ${stateMachine.currentState}. Expected state: ${
|
||||
allowedStates.joinToString(",").removeSuffix(",")
|
||||
}."
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -8,28 +8,23 @@
|
||||
package client
|
||||
|
||||
import cn.rtast.libmc.crypto.AuthenticationProvider
|
||||
import cn.rtast.libmc.packet.ClientboundUnknownPacket
|
||||
import cn.rtast.libmc.protocol.client.createMinecraftClient
|
||||
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundLevelParticlePacket
|
||||
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundPlayerChatMessagePacket
|
||||
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundRecipeBookRemovePacket
|
||||
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundRecipeBookSettingsPacket
|
||||
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundStepTickPacket
|
||||
import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundChatMessagePacket
|
||||
import cn.rtast.libmc.protocol.protocol.session.SessionEvent
|
||||
import cn.rtast.libmc.protocol.protocol.session.onEvent
|
||||
import cn.rtast.libmc.protocol.util.generateOfflineUuid
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.utils.io.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.io.buffered
|
||||
import kotlinx.io.files.Path
|
||||
import kotlinx.io.files.SystemFileSystem
|
||||
import test.KtorNetworkEngine
|
||||
import kotlin.random.Random
|
||||
import kotlin.test.Test
|
||||
import kotlin.time.Clock
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
|
||||
@@ -40,7 +35,7 @@ class TestClient {
|
||||
private val httpClient = HttpClient()
|
||||
|
||||
@Test
|
||||
fun `test client`() {
|
||||
fun `test client`() = runTest {
|
||||
val cli = createMinecraftClient(
|
||||
"127.0.0.1", 25565, "RTAkland",
|
||||
Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"),
|
||||
@@ -56,14 +51,25 @@ class TestClient {
|
||||
}
|
||||
}
|
||||
)
|
||||
cli.onEvent<SessionEvent.ConnectedEvent> {
|
||||
// println(status())
|
||||
login()
|
||||
// disconnect()
|
||||
}
|
||||
cli.onEvent<SessionEvent.DisconnectedEvent> {
|
||||
println(it.reason.toJsonString())
|
||||
}
|
||||
cli.onPacket<ClientboundPlayerChatMessagePacket> { chatTracker.onReceivePlayerChat(it.messageSignature) }
|
||||
cli.onPacket<ClientboundStepTickPacket> { println(it) }
|
||||
cli.on { packet, direction -> println("$direction -> $packet") }
|
||||
cli.launch { cli.connect() }
|
||||
cli.connect()
|
||||
// awaitCancellation()
|
||||
while (true) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test client offline mode`() {
|
||||
fun `test client offline mode`() = runTest {
|
||||
val cli = createMinecraftClient(
|
||||
"127.0.0.1", 25566, "11",
|
||||
generateOfflineUuid("11"), null,
|
||||
@@ -83,9 +89,20 @@ class TestClient {
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
|
||||
cli.onEvent<SessionEvent.ConnectedEvent> {
|
||||
// println(status())
|
||||
login()
|
||||
// disconnect()
|
||||
}
|
||||
cli.onEvent<SessionEvent.DisconnectedEvent> {
|
||||
println(it.reason.toJsonString())
|
||||
}
|
||||
cli.onPacket<ClientboundPlayerChatMessagePacket> { chatTracker.onReceivePlayerChat(it.messageSignature) }
|
||||
cli.onPacket<ClientboundStepTickPacket> { println(it) }
|
||||
cli.launch { cli.connect() }
|
||||
cli.on { packet, direction -> println("$direction -> $packet") }
|
||||
cli.connect()
|
||||
// awaitCancellation()
|
||||
while (true) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import io.ktor.network.sockets.*
|
||||
import io.ktor.utils.io.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class KtorNetworkEngine : SocketEngine {
|
||||
override fun create(host: String, port: Int): RawSocket = KtorNetworkSocket(host, port)
|
||||
@@ -24,22 +25,62 @@ class KtorNetworkEngine : SocketEngine {
|
||||
class KtorNetworkSocket(private val host: String, private val port: Int) : RawSocket {
|
||||
private val sm = SelectorManager(Dispatchers.IO)
|
||||
private lateinit var socket: Socket
|
||||
private var ktorReadChannel: ByteReadChannel? = null
|
||||
private var ktorWriteChannel: ByteWriteChannel? = null
|
||||
|
||||
override suspend fun connect(): Unit = run { socket = aSocket(sm).tcp().connect(host, port) }
|
||||
override fun openReadChannel(): ReadChannel = KtorReadChannel(socket.openReadChannel())
|
||||
override fun openWriteChannel(): WriteChannel = KtorWriteChannel(socket.openWriteChannel())
|
||||
|
||||
override fun openReadChannel(): ReadChannel {
|
||||
val channel = socket.openReadChannel()
|
||||
this.ktorReadChannel = channel
|
||||
return KtorReadChannel(channel)
|
||||
}
|
||||
|
||||
override fun openWriteChannel(): WriteChannel {
|
||||
val channel = socket.openWriteChannel(autoFlush = false)
|
||||
this.ktorWriteChannel = channel
|
||||
return KtorWriteChannel(channel)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
socket.close()
|
||||
try {
|
||||
ktorReadChannel?.cancel(CancellationException("Socket closed"))
|
||||
ktorWriteChannel?.close(null)
|
||||
if (::socket.isInitialized) socket.dispose()
|
||||
} catch (_: Exception) {
|
||||
} finally {
|
||||
sm.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class KtorReadChannel(private val readChannel: ByteReadChannel) : ReadChannel {
|
||||
override suspend fun readByte(): Byte = readChannel.readByte()
|
||||
override suspend fun readBytes(length: Int): ByteArray = readChannel.readByteArray(length)
|
||||
override suspend fun readFully(out: ByteArray, start: Int, end: Int): Unit =
|
||||
private inline fun <T> wrapChannelException(block: () -> T): T {
|
||||
return try {
|
||||
block()
|
||||
} catch (e: Throwable) {
|
||||
if (e is ClosedByteChannelException || e is CancellationException || readChannel.isClosedForRead) {
|
||||
throw CancellationException("ReadChannel was closed", e)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun readByte(): Byte = wrapChannelException {
|
||||
if (readChannel.isClosedForRead) throw CancellationException("ReadChannel is closed for read")
|
||||
readChannel.readByte()
|
||||
}
|
||||
|
||||
override suspend fun readBytes(length: Int): ByteArray = wrapChannelException {
|
||||
if (readChannel.isClosedForRead) throw CancellationException("ReadChannel is closed for read")
|
||||
readChannel.readByteArray(length)
|
||||
}
|
||||
|
||||
override suspend fun readFully(out: ByteArray, start: Int, end: Int): Unit = wrapChannelException {
|
||||
if (readChannel.isClosedForRead) throw CancellationException("ReadChannel is closed for read")
|
||||
readChannel.readFully(out, start, end)
|
||||
}
|
||||
}
|
||||
|
||||
class KtorWriteChannel(private val writeChannel: ByteWriteChannel) : WriteChannel {
|
||||
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
|
||||
|
||||
Reference in New Issue
Block a user