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] **Online Mode Authentication & Encryption/Decryption**: See [Embedded cryptography](docs/Embedded-cryptography.md)
|
||||||
- [x] **Structured `TextComponent` Parser**: TextComponent AST decoder
|
- [x] **Structured `TextComponent` Parser**: TextComponent AST decoder
|
||||||
- [x] **Command Tree Parser**: Full binary graph decoder for brigadier nodes, argument types, and suggestions
|
- [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)
|
- [ ] **Chunk & World Data**: Level Chunk Data with Light decoder (Paletted Containers, Direct/Indirect Palettes)
|
||||||
- [ ] **Light Engine Update**: Sky & Block light nibble array parser
|
- [ ] **Light Engine Update**: Sky & Block light nibble array parser
|
||||||
- [ ] **Explosion Event Decoder**: Knockback vectors and destroyed block offsets array
|
- [ ] **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",
|
"127.0.0.1", 25566, "MyBot",
|
||||||
generateOfflineUuid("MyBot"),
|
generateOfflineUuid("MyBot"),
|
||||||
accessToken = null,
|
accessToken = null,
|
||||||
context = DefaultProtocolContext.withCustom {
|
context = {
|
||||||
socketEngine = KtorNetworkEngine()
|
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
|
# Respond velocity and update client motion
|
||||||
|
|
||||||
> This part uses math calculations
|
> 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.ProtocolContext
|
||||||
import cn.rtast.libmc.crypto.ProtocolContextBuilder
|
import cn.rtast.libmc.crypto.ProtocolContextBuilder
|
||||||
import cn.rtast.libmc.protocol.network.NetworkChannel
|
import cn.rtast.libmc.protocol.network.NetworkChannel
|
||||||
import cn.rtast.libmc.protocol.packet.handshake.ServerboundHandshakePacket
|
import cn.rtast.libmc.protocol.protocol.event.PacketEventDispatcher
|
||||||
import cn.rtast.libmc.protocol.packet.login.serverbound.ServerboundLoginStartPacket
|
import cn.rtast.libmc.protocol.protocol.session.Session
|
||||||
import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent
|
import cn.rtast.libmc.protocol.protocol.session.SessionEvent
|
||||||
import cn.rtast.libmc.protocol.protocol.state.ProtocolState
|
import cn.rtast.libmc.protocol.protocol.session.SessionImpl
|
||||||
import cn.rtast.libmc.protocol.util.TransactionIdManager
|
import cn.rtast.libmc.protocol.util.TransactionIdManager
|
||||||
import cn.rtast.libmc.protocol.util.generateOfflineUuid
|
import cn.rtast.libmc.protocol.util.generateOfflineUuid
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
@@ -20,68 +20,51 @@ import kotlin.coroutines.CoroutineContext
|
|||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
public class MinecraftClient internal constructor(
|
public class MinecraftClient internal constructor(
|
||||||
private val host: String,
|
internal val host: String,
|
||||||
private val port: Int,
|
internal val port: Int,
|
||||||
private val username: String,
|
internal val username: String,
|
||||||
internal val uuid: Uuid,
|
internal val uuid: Uuid,
|
||||||
internal val accessToken: String?,
|
internal val accessToken: String?,
|
||||||
parentJob: Job?,
|
parentJob: Job?,
|
||||||
private val ioDispatcher: CoroutineDispatcher,
|
private val ioDispatcher: CoroutineDispatcher,
|
||||||
internal val protocolContext: ProtocolContext,
|
internal val protocolContext: ProtocolContext,
|
||||||
) : PacketEventDispatcher(), CoroutineScope {
|
public val session: SessionImpl = SessionImpl(),
|
||||||
|
) : PacketEventDispatcher(), CoroutineScope, Session by session {
|
||||||
internal val stateMachine = ClientStateMachine()
|
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 val clientJob = SupervisorJob(parentJob)
|
||||||
private var listenJob: Job? = null
|
private var listenJob: Job? = null
|
||||||
|
|
||||||
public val isOnlineMode: Boolean = accessToken != null
|
|
||||||
|
|
||||||
public val transactionManager: TransactionIdManager = TransactionIdManager()
|
public val transactionManager: TransactionIdManager = TransactionIdManager()
|
||||||
public val clientTickingLoop: ClientTickingLoop = ClientTickingLoop(this)
|
|
||||||
|
|
||||||
/**
|
init {
|
||||||
* Register a client ticking event callback.
|
session.attachClient(this)
|
||||||
* 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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
private fun startListening() {
|
||||||
listenJob = launch {
|
listenJob = launch {
|
||||||
try {
|
try {
|
||||||
while (isActive) networkChannel.readNextPacket()
|
while (isActive) networkChannel.readNextPacket()
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) return@launch
|
||||||
if (isActive) {
|
|
||||||
e.printStackTrace()
|
|
||||||
println("Network read loop exception: ${e.message}")
|
println("Network read loop exception: ${e.message}")
|
||||||
close()
|
} finally {
|
||||||
}
|
networkChannel.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun close() {
|
public fun close() {
|
||||||
networkChannel.close()
|
networkChannel.close()
|
||||||
clientTickingLoop.stop()
|
listenJob?.cancel()
|
||||||
clientJob.cancel()
|
clientJob.cancel()
|
||||||
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
public override val coroutineContext: CoroutineContext
|
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
|
package cn.rtast.libmc.protocol.network
|
||||||
|
|
||||||
import cn.rtast.libmc.crypto.ProtocolContext
|
|
||||||
import cn.rtast.libmc.network.BytesBuffer
|
import cn.rtast.libmc.network.BytesBuffer
|
||||||
import cn.rtast.libmc.network.wrap
|
import cn.rtast.libmc.network.wrap
|
||||||
import cn.rtast.libmc.packet.MinecraftPacket
|
import cn.rtast.libmc.packet.MinecraftPacket
|
||||||
import cn.rtast.libmc.packet.writeBuffer
|
import cn.rtast.libmc.packet.writeBuffer
|
||||||
import cn.rtast.libmc.primitives.readVarInt
|
import cn.rtast.libmc.primitives.readVarInt
|
||||||
import cn.rtast.libmc.primitives.writeVarInt
|
import cn.rtast.libmc.primitives.writeVarInt
|
||||||
import cn.rtast.libmc.protocol.client.ClientStateMachine
|
import cn.rtast.libmc.protocol.client.MinecraftClient
|
||||||
import cn.rtast.libmc.protocol.client.PacketEventDispatcher
|
import cn.rtast.libmc.protocol.protocol.event.PacketEventDispatcher
|
||||||
import cn.rtast.libmc.protocol.protocol.GamePacketsProtocolCodec.clientboundGameProtocols
|
import cn.rtast.libmc.protocol.protocol.GamePacketsProtocolCodec.clientboundGameProtocols
|
||||||
import cn.rtast.libmc.protocol.protocol.GamePacketsProtocolCodec.serverboundGameProtocols
|
import cn.rtast.libmc.protocol.protocol.GamePacketsProtocolCodec.serverboundGameProtocols
|
||||||
import cn.rtast.libmc.zlibCompress
|
import cn.rtast.libmc.zlibCompress
|
||||||
import cn.rtast.libmc.zlibDecompress
|
import cn.rtast.libmc.zlibDecompress
|
||||||
import kotlin.concurrent.Volatile
|
import kotlin.concurrent.Volatile
|
||||||
|
|
||||||
public class NetworkChannel internal constructor(
|
public class NetworkChannel internal constructor(private val client: MinecraftClient) {
|
||||||
host: String,
|
internal val networkSession: NetworkSession = NetworkSession(client)
|
||||||
port: Int,
|
|
||||||
private val stateMachine: ClientStateMachine,
|
|
||||||
private val dispatcher: PacketEventDispatcher,
|
|
||||||
protocolContext: ProtocolContext,
|
|
||||||
) {
|
|
||||||
internal val session: NetworkSession = NetworkSession(host, port, protocolContext)
|
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var threshold = -1
|
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 }
|
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
|
* Use [PacketEventDispatcher.onPacket] to get packet event
|
||||||
*/
|
*/
|
||||||
public suspend fun readNextPacket(): MinecraftPacket {
|
public suspend fun readNextPacket(): MinecraftPacket {
|
||||||
val packetLength = session.readVarInt()
|
val packetLength = networkSession.readVarInt()
|
||||||
val frameBuf = session.readBytes(packetLength).wrap()
|
val frameBuf = networkSession.readBytes(packetLength).wrap()
|
||||||
val payloadBuf = if (threshold < 0) frameBuf else {
|
val payloadBuf = if (threshold < 0) frameBuf else {
|
||||||
val dataLength = frameBuf.readVarInt()
|
val dataLength = frameBuf.readVarInt()
|
||||||
if (dataLength == 0) frameBuf else {
|
if (dataLength == 0) frameBuf else {
|
||||||
@@ -51,10 +44,10 @@ public class NetworkChannel internal constructor(
|
|||||||
compressedBytes.zlibDecompress(dataLength).wrap()
|
compressedBytes.zlibDecompress(dataLength).wrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val currentState = stateMachine.currentState
|
val currentState = client.stateMachine.currentState
|
||||||
val packetId = payloadBuf.readVarInt()
|
val packetId = payloadBuf.readVarInt()
|
||||||
val packet = clientboundGameProtocols.getRegistry(currentState).decodePacket(packetId, payloadBuf)
|
val packet = clientboundGameProtocols.getRegistry(currentState).decodePacket(packetId, payloadBuf)
|
||||||
dispatcher.dispatchReceive(packet)
|
client.dispatchReceive(packet, client.session)
|
||||||
return packet
|
return packet
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +58,7 @@ public class NetworkChannel internal constructor(
|
|||||||
*/
|
*/
|
||||||
public suspend fun sendPacket(packet: MinecraftPacket) {
|
public suspend fun sendPacket(packet: MinecraftPacket) {
|
||||||
val uncompressedBodyBuf = BytesBuffer()
|
val uncompressedBodyBuf = BytesBuffer()
|
||||||
serverboundGameProtocols.getRegistry(stateMachine.currentState).encodePacket(uncompressedBodyBuf, packet)
|
serverboundGameProtocols.getRegistry(client.stateMachine.currentState).encodePacket(uncompressedBodyBuf, packet)
|
||||||
val uncompressedData = uncompressedBodyBuf.toByteArray()
|
val uncompressedData = uncompressedBodyBuf.toByteArray()
|
||||||
val frameBuffer = BytesBuffer()
|
val frameBuffer = BytesBuffer()
|
||||||
if (threshold < 0) {
|
if (threshold < 0) {
|
||||||
@@ -84,9 +77,9 @@ public class NetworkChannel internal constructor(
|
|||||||
frameBuffer.writeVarInt(contentBuf.size)
|
frameBuffer.writeVarInt(contentBuf.size)
|
||||||
frameBuffer.writeBuffer(contentBuf)
|
frameBuffer.writeBuffer(contentBuf)
|
||||||
}
|
}
|
||||||
session.writeFully(frameBuffer.toByteArray())
|
networkSession.writeFully(frameBuffer.toByteArray())
|
||||||
dispatcher.dispatchSent(packet)
|
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
|
package cn.rtast.libmc.protocol.network
|
||||||
|
|
||||||
import cn.rtast.libmc.crypto.ProtocolContext
|
|
||||||
import cn.rtast.libmc.network.RawSocket
|
import cn.rtast.libmc.network.RawSocket
|
||||||
import cn.rtast.libmc.network.ReadChannel
|
import cn.rtast.libmc.network.ReadChannel
|
||||||
import cn.rtast.libmc.network.WriteChannel
|
import cn.rtast.libmc.network.WriteChannel
|
||||||
import cn.rtast.libmc.primitives.readVarInt
|
import cn.rtast.libmc.primitives.readVarInt
|
||||||
|
import cn.rtast.libmc.protocol.client.MinecraftClient
|
||||||
import cn.rtast.libmc.protocol.crypto.Aes128Cfb8ChannelCipher
|
import cn.rtast.libmc.protocol.crypto.Aes128Cfb8ChannelCipher
|
||||||
|
|
||||||
public class NetworkSession internal constructor(
|
public class NetworkSession internal constructor(private val client: MinecraftClient) {
|
||||||
private val host: String,
|
|
||||||
private val port: Int,
|
|
||||||
private val context: ProtocolContext,
|
|
||||||
) {
|
|
||||||
private var socket: RawSocket? = null
|
private var socket: RawSocket? = null
|
||||||
public var readChannel: ReadChannel? = null
|
public var readChannel: ReadChannel? = null
|
||||||
private set
|
private set
|
||||||
@@ -26,7 +22,7 @@ public class NetworkSession internal constructor(
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
public suspend fun connect() {
|
public suspend fun connect() {
|
||||||
val sk = context.createSocket(host, port)
|
val sk = client.protocolContext.createSocket(client.host, client.port)
|
||||||
sk.connect()
|
sk.connect()
|
||||||
this.socket = sk
|
this.socket = sk
|
||||||
this.readChannel = sk.openReadChannel()
|
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.NBTCompound
|
||||||
import cn.rtast.libmc.nbt.NBTTag
|
import cn.rtast.libmc.nbt.NBTTag
|
||||||
import cn.rtast.libmc.nbt.NBTType
|
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.game.Identifier
|
||||||
import cn.rtast.libmc.protocol.protocol.util.readNetworkNBTCompound
|
import cn.rtast.libmc.protocol.protocol.util.readNetworkNBTCompound
|
||||||
import cn.rtast.libmc.protocol.protocol.util.writeNetworkNBTCompound
|
import cn.rtast.libmc.protocol.protocol.util.writeNetworkNBTCompound
|
||||||
import cn.rtast.libmc.network.BytesBuffer
|
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
public data class TextComponent(
|
public data class TextComponent(
|
||||||
@@ -223,6 +224,8 @@ public data class TextComponent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fun toJsonString(): String = this.toNBTCompound().toJsonString()
|
||||||
|
|
||||||
public companion object {
|
public companion object {
|
||||||
public fun of(text: String): TextComponent = TextComponent(content = Content.PlainText(text))
|
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 {
|
private fun parseContentFromNbt(tag: NBTTag.CompoundTag): TextComponent.Content {
|
||||||
val type = tag.getStringOrNull("type")
|
val type = tag.getStringOrNull("type")
|
||||||
return when {
|
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 {
|
private fun encodeClickEventToNbt(event: TextComponent.ClickEvent): NBTTag.CompoundTag {
|
||||||
val map = mutableMapOf<String, NBTTag>("action" to NBTTag.StringTag(event.action.text))
|
val map = mutableMapOf<String, NBTTag>("action" to NBTTag.StringTag(event.action.text))
|
||||||
when (event) {
|
when (event) {
|
||||||
@@ -575,7 +578,7 @@ private fun encodeClickEventToNbt(event: TextComponent.ClickEvent): NBTTag.Compo
|
|||||||
private fun encodeHoverEventToNbt(event: TextComponent.HoverEvent): NBTTag.CompoundTag {
|
private fun encodeHoverEventToNbt(event: TextComponent.HoverEvent): NBTTag.CompoundTag {
|
||||||
val map = mutableMapOf<String, NBTTag>("action" to NBTTag.StringTag(event.action.text))
|
val map = mutableMapOf<String, NBTTag>("action" to NBTTag.StringTag(event.action.text))
|
||||||
when (event) {
|
when (event) {
|
||||||
is TextComponent.HoverEvent.ShowText -> map["value"] = event.component.toNbt()
|
is TextComponent.HoverEvent.ShowText -> map["value"] = event.component.toNBTCompound()
|
||||||
is TextComponent.HoverEvent.ShowItem -> {
|
is TextComponent.HoverEvent.ShowItem -> {
|
||||||
map["id"] = NBTTag.StringTag(event.id.toString())
|
map["id"] = NBTTag.StringTag(event.id.toString())
|
||||||
map["count"] = NBTTag.IntTag(event.count)
|
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)
|
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)
|
return NBTTag.CompoundTag(map)
|
||||||
@@ -617,6 +620,6 @@ internal fun BytesBuffer.readTextComponent(): TextComponent =
|
|||||||
this.readNetworkNBTCompound().element.toTextComponent()
|
this.readNetworkNBTCompound().element.toTextComponent()
|
||||||
|
|
||||||
internal fun BytesBuffer.writeTextComponent(component: TextComponent) {
|
internal fun BytesBuffer.writeTextComponent(component: TextComponent) {
|
||||||
val nbt = component.toNbt()
|
val nbt = component.toNBTCompound()
|
||||||
this.writeNetworkNBTCompound(NBTCompound("", nbt))
|
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
|
package client
|
||||||
|
|
||||||
import cn.rtast.libmc.crypto.AuthenticationProvider
|
import cn.rtast.libmc.crypto.AuthenticationProvider
|
||||||
import cn.rtast.libmc.packet.ClientboundUnknownPacket
|
|
||||||
import cn.rtast.libmc.protocol.client.createMinecraftClient
|
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.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.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 cn.rtast.libmc.protocol.util.generateOfflineUuid
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.statement.*
|
import io.ktor.client.statement.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.utils.io.*
|
import io.ktor.utils.io.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlinx.io.buffered
|
import kotlinx.io.buffered
|
||||||
import kotlinx.io.files.Path
|
import kotlinx.io.files.Path
|
||||||
import kotlinx.io.files.SystemFileSystem
|
import kotlinx.io.files.SystemFileSystem
|
||||||
import test.KtorNetworkEngine
|
import test.KtorNetworkEngine
|
||||||
import kotlin.random.Random
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.time.Clock
|
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
|
||||||
@@ -40,7 +35,7 @@ class TestClient {
|
|||||||
private val httpClient = HttpClient()
|
private val httpClient = HttpClient()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test client`() {
|
fun `test client`() = runTest {
|
||||||
val cli = createMinecraftClient(
|
val cli = createMinecraftClient(
|
||||||
"127.0.0.1", 25565, "RTAkland",
|
"127.0.0.1", 25565, "RTAkland",
|
||||||
Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"),
|
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.on { packet, direction -> println("$direction -> $packet") }
|
||||||
cli.launch { cli.connect() }
|
cli.connect()
|
||||||
|
// awaitCancellation()
|
||||||
while (true) {
|
while (true) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test client offline mode`() {
|
fun `test client offline mode`() = runTest {
|
||||||
val cli = createMinecraftClient(
|
val cli = createMinecraftClient(
|
||||||
"127.0.0.1", 25566, "11",
|
"127.0.0.1", 25566, "11",
|
||||||
generateOfflineUuid("11"), null,
|
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<ClientboundPlayerChatMessagePacket> { chatTracker.onReceivePlayerChat(it.messageSignature) }
|
||||||
cli.onPacket<ClientboundStepTickPacket> { println(it) }
|
cli.onPacket<ClientboundStepTickPacket> { println(it) }
|
||||||
cli.launch { cli.connect() }
|
cli.on { packet, direction -> println("$direction -> $packet") }
|
||||||
|
cli.connect()
|
||||||
|
// awaitCancellation()
|
||||||
while (true) {
|
while (true) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import io.ktor.network.sockets.*
|
|||||||
import io.ktor.utils.io.*
|
import io.ktor.utils.io.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.IO
|
import kotlinx.coroutines.IO
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
class KtorNetworkEngine : SocketEngine {
|
class KtorNetworkEngine : SocketEngine {
|
||||||
override fun create(host: String, port: Int): RawSocket = KtorNetworkSocket(host, port)
|
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 {
|
class KtorNetworkSocket(private val host: String, private val port: Int) : RawSocket {
|
||||||
private val sm = SelectorManager(Dispatchers.IO)
|
private val sm = SelectorManager(Dispatchers.IO)
|
||||||
private lateinit var socket: Socket
|
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 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() {
|
override fun close() {
|
||||||
socket.close()
|
try {
|
||||||
|
ktorReadChannel?.cancel(CancellationException("Socket closed"))
|
||||||
|
ktorWriteChannel?.close(null)
|
||||||
|
if (::socket.isInitialized) socket.dispose()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
} finally {
|
||||||
sm.close()
|
sm.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class KtorReadChannel(private val readChannel: ByteReadChannel) : ReadChannel {
|
class KtorReadChannel(private val readChannel: ByteReadChannel) : ReadChannel {
|
||||||
override suspend fun readByte(): Byte = readChannel.readByte()
|
private inline fun <T> wrapChannelException(block: () -> T): T {
|
||||||
override suspend fun readBytes(length: Int): ByteArray = readChannel.readByteArray(length)
|
return try {
|
||||||
override suspend fun readFully(out: ByteArray, start: Int, end: Int): Unit =
|
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)
|
readChannel.readFully(out, start, end)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class KtorWriteChannel(private val writeChannel: ByteWriteChannel) : WriteChannel {
|
class KtorWriteChannel(private val writeChannel: ByteWriteChannel) : WriteChannel {
|
||||||
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
|
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
|
||||||
|
|||||||
Reference in New Issue
Block a user