From ebb4ff848def2f7283c8866999f5cf20bbe07443 Mon Sep 17 00:00:00 2001 From: RTAkland Date: Mon, 7 Sep 2026 21:40:33 +0800 Subject: [PATCH] Test: complete send chat message without signature --- .../cn/rtast/libmc/primitives/BitSet.kt | 29 +++++++++ .../ClientboundEntityEffectPacket.kt | 2 +- .../ClientboundPlayerEndCombatPacket.kt | 2 +- .../libmc/protocol/{tick => util}/duration.kt | 2 +- .../jvmTest/kotlin/test/ClientChatTracker.kt | 62 +++++++++++++++++++ .../kotlin/test/TestClientTestInJvm.kt | 28 +++++++-- 6 files changed, 118 insertions(+), 7 deletions(-) rename libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/{tick => util}/duration.kt (95%) create mode 100644 libmc-protocol/src/jvmTest/kotlin/test/ClientChatTracker.kt diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/primitives/BitSet.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/primitives/BitSet.kt index e62c63f..584b89c 100644 --- a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/primitives/BitSet.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/primitives/BitSet.kt @@ -33,3 +33,32 @@ public fun BitSet.getBit(bitIndex: Int): Boolean { val bitOffset = bitIndex and 63 return (this[longIndex] and (1L shl bitOffset)) != 0L } + +public class FixedBitSet20(initialBits: Int = 0) { + private var bits: Int = initialBits and 0xFFFFF + + public operator fun get(index: Int): Boolean { + require(index in 0..19) { "Index out of range [0, 19]" } + return (bits and (1 shl index)) != 0 + } + + public operator fun set(index: Int, value: Boolean) { + require(index in 0..19) { "Index out of range [0, 19]" } + bits = if (value) { + bits or (1 shl index) + } else { + bits and (1 shl index).inv() + } + } + + public fun toByteArray(): ByteArray { + val result = ByteArray(3) + result[0] = (bits and 0xFF).toByte() + result[1] = ((bits shr 8) and 0xFF).toByte() + result[2] = ((bits shr 16) and 0x0F).toByte() + return result + } +} + +public fun createFixedBitSet20(initialBits: Int = 0): FixedBitSet20 = + FixedBitSet20(initialBits) \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundEntityEffectPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundEntityEffectPacket.kt index f18e9ec..aa4f19d 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundEntityEffectPacket.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundEntityEffectPacket.kt @@ -13,7 +13,7 @@ import cn.rtast.libmc.packet.MinecraftPacket import cn.rtast.libmc.primitives.readVarInt import cn.rtast.libmc.protocol.protocol.game.effect.EntityEffectFlags import cn.rtast.libmc.protocol.protocol.game.effect.readEntityEffectFlags -import cn.rtast.libmc.protocol.tick.ticks +import cn.rtast.libmc.protocol.util.ticks import kotlin.time.Duration public data class ClientboundEntityEffectPacket( diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundPlayerEndCombatPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundPlayerEndCombatPacket.kt index e7917d9..2ed426b 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundPlayerEndCombatPacket.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundPlayerEndCombatPacket.kt @@ -11,7 +11,7 @@ import cn.rtast.libmc.stream.BytesBuffer import cn.rtast.libmc.packet.PacketCodec import cn.rtast.libmc.packet.MinecraftPacket import cn.rtast.libmc.primitives.readVarInt -import cn.rtast.libmc.protocol.tick.ticks +import cn.rtast.libmc.protocol.util.ticks import kotlin.time.Duration public data class ClientboundPlayerEndCombatPacket(val duration: Duration) : MinecraftPacket { diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/tick/duration.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/util/duration.kt similarity index 95% rename from libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/tick/duration.kt rename to libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/util/duration.kt index c19410d..7d8f12e 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/tick/duration.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/util/duration.kt @@ -5,7 +5,7 @@ */ -package cn.rtast.libmc.protocol.tick +package cn.rtast.libmc.protocol.util import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds diff --git a/libmc-protocol/src/jvmTest/kotlin/test/ClientChatTracker.kt b/libmc-protocol/src/jvmTest/kotlin/test/ClientChatTracker.kt new file mode 100644 index 0000000..db4bef9 --- /dev/null +++ b/libmc-protocol/src/jvmTest/kotlin/test/ClientChatTracker.kt @@ -0,0 +1,62 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/7 + */ + + +package test + +import cn.rtast.libmc.primitives.FixedBitSet20 +import cn.rtast.libmc.primitives.createFixedBitSet20 +import cn.rtast.libmc.stream.BytesBuffer +import kotlin.uuid.Uuid + +class ClientChatTracker { + private val lastSeenQueue = ArrayDeque(20) + var pendingMessageCount: Int = 0 + private set + + fun onReceivePlayerChat(signature: ByteArray?) { + if (signature == null || signature.size != 256) return + if (lastSeenQueue.size >= 20) lastSeenQueue.removeFirst() + lastSeenQueue.addLast(signature) + pendingMessageCount++ + } + + fun prepareForOutgoingMessage(): ChatStateSnapshot { + val signatures = lastSeenQueue.toList() + val count = pendingMessageCount + pendingMessageCount = 0 + return ChatStateSnapshot(signatures, count) + } +} + +data class ChatStateSnapshot(val lastSeenSignatures: List, val messageCount: Int) + +fun createAcknowledgedBitSet(lastSeenSignatures: List): FixedBitSet20 { + val bitSet = createFixedBitSet20() + for (i in lastSeenSignatures.indices) { + if (i >= 20) break + bitSet[i] = true + } + return bitSet +} + +object ChatPacketUtils { + fun computeByteArrayHashCode(bytes: ByteArray): Int { + var result = 1 + for (element in bytes) result = 31 * result + element.toInt() + return result + } + + fun computePacketChecksum(lastSeenSignatures: List): Byte { + var combinedHash = 1 + for (sig in lastSeenSignatures) { + val sigHash = computeByteArrayHashCode(sig) + combinedHash = 31 * combinedHash + sigHash + } + val resultByte = combinedHash.toByte() + return if (resultByte == 0.toByte()) 1.toByte() else resultByte + } +} \ No newline at end of file diff --git a/libmc-protocol/src/jvmTest/kotlin/test/TestClientTestInJvm.kt b/libmc-protocol/src/jvmTest/kotlin/test/TestClientTestInJvm.kt index 9dd9e1b..ed938e1 100644 --- a/libmc-protocol/src/jvmTest/kotlin/test/TestClientTestInJvm.kt +++ b/libmc-protocol/src/jvmTest/kotlin/test/TestClientTestInJvm.kt @@ -10,8 +10,8 @@ package test import cn.rtast.libmc.packet.ClientboundUnknownPacket import cn.rtast.libmc.protocol.client.createMinecraftClient import cn.rtast.libmc.protocol.crypto.DefaultProtocolContext -import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundPlayerInfoUpdatePacket -import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundWaypointPacket +import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundPlayerChatMessagePacket +import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundChatMessagePacket import cn.rtast.libmc.protocol.util.generateOfflineUuid import kotlinx.coroutines.launch import org.junit.Test @@ -21,11 +21,14 @@ import java.security.KeyFactory import java.security.MessageDigest import java.security.spec.X509EncodedKeySpec import javax.crypto.Cipher +import kotlin.random.Random +import kotlin.time.Clock import kotlin.uuid.Uuid class TestClientTestInJvm { val accessToken = File("src/jvmTest/resources/accessToken.txt").readText() + private val chatTracker = ClientChatTracker() fun encrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray { val keySpec = X509EncodedKeySpec(publicKeyBytes) @@ -84,8 +87,25 @@ class TestClientTestInJvm { // if (packet !is ClientboundWaypointPacket) // println("$direction -> $packet") // } -// cli.onPacket { println(it) } - cli.onPacket { println(it) } + cli.onPacket { + println(it) + val snapshot = chatTracker.prepareForOutgoingMessage() + cli.networkChannel.sendPacket( + ServerboundChatMessagePacket( + "114514", Clock.System.now().toEpochMilliseconds(), + Random.nextLong(), + null, snapshot.messageCount, createAcknowledgedBitSet(snapshot.lastSeenSignatures).toByteArray(), + ChatPacketUtils.computePacketChecksum(snapshot.lastSeenSignatures) + ) + ) + } + cli.onPacket { + chatTracker.onReceivePlayerChat(it.messageSignature) + } +// cli.onPacket { println(it) } +// cli.onPacket { println(it) } +// cli.onPacket { println(it) } +// cli.onPacket { println(it) } cli.launch { cli.connect() } while (true) { }