From 7b2be12d236bb6c49a877e1d7227c393f8c0bbe6 Mon Sep 17 00:00:00 2001 From: RTAkland Date: Mon, 7 Sep 2026 19:52:38 +0800 Subject: [PATCH] Implemented clientbound packet map_item_data 0x33 --- .../cn/rtast/libmc/primitives/BitSet.kt | 10 +-- .../cn/rtast/libmc/primitives/Prefixed.kt | 4 ++ .../libmc/protocol/network/NetworkChannel.kt | 6 +- .../ClientboundMapItemDataPacket.kt | 56 +++++++++++++++++ ...otocols.kt => GamePacketsProtocolCodec.kt} | 4 +- .../libmc/protocol/protocol/game/item/Map.kt | 63 +++++++++++++++++++ .../kotlin/test/TestClientTestInJvm.kt | 4 +- 7 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundMapItemDataPacket.kt rename libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/{GameProtocols.kt => GamePacketsProtocolCodec.kt} (99%) create mode 100644 libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/item/Map.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 22cd3c2..e62c63f 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 @@ -9,23 +9,25 @@ package cn.rtast.libmc.primitives import cn.rtast.libmc.stream.BytesBuffer -public suspend fun BytesBuffer.readBitSet(): LongArray { +public typealias BitSet = LongArray + +public suspend fun BytesBuffer.readBitSet(): BitSet { val count = this.readVarInt() return LongArray(count) { this.readLong() } } -public suspend fun BytesBuffer.writeBitSet(data: LongArray) { +public suspend fun BytesBuffer.writeBitSet(data: BitSet) { this.writeVarInt(data.size) for (i in data.indices) this.writeLong(data[i]) } -public fun LongArray.countSetBits(): Int { +public fun BitSet.countSetBits(): Int { var count = 0 for (i in indices) count += this[i].countOneBits() return count } -public fun LongArray.getBit(bitIndex: Int): Boolean { +public fun BitSet.getBit(bitIndex: Int): Boolean { val longIndex = bitIndex shr 6 if (longIndex !in this.indices) return false val bitOffset = bitIndex and 63 diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/primitives/Prefixed.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/primitives/Prefixed.kt index da0b8b0..fd18040 100644 --- a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/primitives/Prefixed.kt +++ b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/primitives/Prefixed.kt @@ -45,6 +45,10 @@ public suspend fun BytesBuffer.writePrefixedStringArray(value: List) { public suspend inline fun BytesBuffer.readPrefixed(reader: BytesBuffer.() -> T): List { val count = this.readVarInt() + require(count in 0..4096) { + "Prefixed array count $count is invalid (expected 0..4096). " + + "Stream offset is corrupted. Check Heightmaps/NBT encoding." + } val list = ArrayList(count) repeat(count) { _ -> list.add(this.reader()) } return list diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkChannel.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkChannel.kt index a165c36..fd406e6 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkChannel.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/network/NetworkChannel.kt @@ -14,7 +14,7 @@ import cn.rtast.libmc.primitives.readVarInt import cn.rtast.libmc.primitives.writeVarInt import cn.rtast.libmc.protocol.client.ClientStateMachine import cn.rtast.libmc.protocol.event.PacketEventDispatcher -import cn.rtast.libmc.protocol.protocol.GameProtocols +import cn.rtast.libmc.protocol.protocol.GamePacketsProtocolCodec import cn.rtast.libmc.stream.BytesBuffer import cn.rtast.libmc.stream.wrap import cn.rtast.libmc.zlibCompress @@ -53,7 +53,7 @@ public class NetworkChannel internal constructor( } val currentState = stateMachine.currentState val packetId = payloadBuf.readVarInt() - val packet = GameProtocols.clientboundGameProtocols + val packet = GamePacketsProtocolCodec.clientboundGameProtocols .getRegistry(currentState) .decodePacket(packetId, payloadBuf) dispatcher.dispatchReceive(packet) @@ -67,7 +67,7 @@ public class NetworkChannel internal constructor( */ public suspend fun sendPacket(packet: MinecraftPacket) { val uncompressedBodyBuf = BytesBuffer() - GameProtocols.serverboundGameProtocols + GamePacketsProtocolCodec.serverboundGameProtocols .getRegistry(stateMachine.currentState) .encodePacket(uncompressedBodyBuf, packet) val uncompressedData = uncompressedBodyBuf.toByteArray() diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundMapItemDataPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundMapItemDataPacket.kt new file mode 100644 index 0000000..1d3731c --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundMapItemDataPacket.kt @@ -0,0 +1,56 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/7 + */ + + +package cn.rtast.libmc.protocol.packet.play.clientbound + +import cn.rtast.libmc.packet.MinecraftPacket +import cn.rtast.libmc.packet.PacketCodec +import cn.rtast.libmc.primitives.readOptional +import cn.rtast.libmc.primitives.readPrefixed +import cn.rtast.libmc.primitives.readPrefixedByteArray +import cn.rtast.libmc.primitives.readVarInt +import cn.rtast.libmc.protocol.protocol.game.chat.readTextComponent +import cn.rtast.libmc.protocol.protocol.game.item.MapItemColorPatch +import cn.rtast.libmc.protocol.protocol.game.item.MapItemIcon +import cn.rtast.libmc.protocol.protocol.game.item.MapItemIconType +import cn.rtast.libmc.stream.BytesBuffer + +public data class ClientboundMapItemDataPacket( + val mapId: Int, + val scale: Byte, + val locked: Boolean, + val icons: List?, + val colorPatch: MapItemColorPatch?, +) : MinecraftPacket { + internal companion object Codec : PacketCodec { + override suspend fun encode(buffer: BytesBuffer, value: ClientboundMapItemDataPacket) {} + override suspend fun decode(buffer: BytesBuffer): ClientboundMapItemDataPacket { + val mapId = buffer.readVarInt() + val scale = buffer.readByte() + val locked = buffer.readBoolean() + val icons = buffer.readOptional { + readPrefixed { + val type = MapItemIconType.fromID(readVarInt()) + val x = readByte() + val z = readByte() + val direction = readByte() + val displayName = readOptional { readTextComponent() } + MapItemIcon(type, x, z, direction, displayName) + } + } + val columns = buffer.readUByte() + val colorPatch = if (columns > 0u) { + val rows = buffer.readUByte() + val xOffset = buffer.readUByte() + val zOffset = buffer.readUByte() + val data = buffer.readPrefixedByteArray() + MapItemColorPatch(columns, rows, xOffset, zOffset, data) + } else null + return ClientboundMapItemDataPacket(mapId, scale, locked, icons, colorPatch) + } + } +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/GameProtocols.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/GamePacketsProtocolCodec.kt similarity index 99% rename from libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/GameProtocols.kt rename to libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/GamePacketsProtocolCodec.kt index e51bfcb..9706c3c 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/GameProtocols.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/GamePacketsProtocolCodec.kt @@ -23,7 +23,7 @@ import cn.rtast.libmc.protocol.packet.status.serverbound.ServerboundStatusReques import cn.rtast.libmc.protocol.protocol.state.ProtocolState import cn.rtast.libmc.protocol.protocol.state.ProtocolStateRegistry -internal object GameProtocols { +internal object GamePacketsProtocolCodec { val clientboundGameProtocols = ProtocolStateRegistry().apply { register(ProtocolState.STATUS) { register(0x00, ClientboundStatusResponsePacket) @@ -111,7 +111,7 @@ internal object GameProtocols { // register(0x30, ClientboundLightUpdatePacket) register(0x31, ClientboundLoginPlayPacket) register(0x32, ClientboundLowDiskSpaceWarningPacket) -// register(0x33, ClientboundMapItemDataPacket) + register(0x33, ClientboundMapItemDataPacket) // register(0x34, ClientboundMerchantOffersPacket) register(0x35, ClientboundUpdateEntityPositionPacket) register(0x36, ClientboundUpdateEntityPositionAndRotationPacket) diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/item/Map.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/item/Map.kt new file mode 100644 index 0000000..156e587 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/item/Map.kt @@ -0,0 +1,63 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/7 + */ + + +package cn.rtast.libmc.protocol.protocol.game.item + +import cn.rtast.libmc.protocol.protocol.game.chat.TextComponent + +public enum class MapItemIconType(public val id: Int) { + WHITE_ARROW(0), GREEN_ARROW(1), RED_ARROW(2), BLUE_ARROW(3), + WHITE_CROSS(4), RED_POINTER(5), WHITE_CIRCLE(6), SMALL_WHITE_CIRCLE(7), + MANSION(8), MONUMENT(9), WHITE_BANNER(10), ORANGE_BANNER(11), + MAGENTA_BANNER(12), LIGHT_BLUE_BANNER(13), YELLOW_BANNER(14), + LIME_BANNER(15), PINK_BANNER(16), GRAY_BANNER(17), LIGHT_GRAY_BANNER(18), + CYAN_BANNER(19), PURPLE_BANNER(20), BLUE_BANNER(21), BROWN_BANNER(22), + GREEN_BANNER(23), RED_BANNER(24), BLACK_BANNER(25), TREASURE_MARKER(26), + DESERT_VILLAGE(27), PLAINS_VILLAGE(28), SAVANNA_VILLAGE(29), SNOWY_VILLAGE(30), + TAIGA_VILLAGE(31), JUNGLE_TEMPLE(32), SWAMP_HUT(33), TRIAL_CHAMBERS(34); + + public companion object { + public fun fromID(id: Int): MapItemIconType = entries.find { it.id == id } ?: WHITE_ARROW + } +} + +public data class MapItemIcon( + val type: MapItemIconType, + val x: Byte, + val z: Byte, + val direction: Byte, + val displayName: TextComponent? +) + +public data class MapItemColorPatch( + val columns: UByte, + val rows: UByte, + val xOffset: UByte, + val zOffset: UByte, + val data: ByteArray +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + other as MapItemColorPatch + if (columns != other.columns) return false + if (rows != other.rows) return false + if (xOffset != other.xOffset) return false + if (zOffset != other.zOffset) return false + if (!data.contentEquals(other.data)) return false + return true + } + + override fun hashCode(): Int { + var result = columns.hashCode() + result = 31 * result + rows.hashCode() + result = 31 * result + xOffset.hashCode() + result = 31 * result + zOffset.hashCode() + result = 31 * result + data.contentHashCode() + return result + } +} \ 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 3742377..183d150 100644 --- a/libmc-protocol/src/jvmTest/kotlin/test/TestClientTestInJvm.kt +++ b/libmc-protocol/src/jvmTest/kotlin/test/TestClientTestInJvm.kt @@ -81,8 +81,8 @@ class TestClientTestInJvm { null, crypto = DefaultProtocolContext ) -// cli.on { packet, direction -> println("$direction -> $packet") } - cli.onPacket { println(it.content.content) } + cli.on { packet, direction -> println("$direction -> $packet") } +// cli.onPacket { println(it.content.content) } cli.launch { cli.connect() } while (true) { }