diff --git a/README.md b/README.md index c3bf9a3..4b0fffd 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ A lightweight minecraft client-side protocol library for Kotlin Native and JVM - [x] `0x2C` Keep Alive (`ClientboundKeepAlivePlayPacket`) - [ ] `0x2D` Level Chunk Update With Light (`ClientboundLevelChunkUpdateWithLightPacket`) - [x] `0x2E` Level Event (`ClientboundLevelEventPacket`) -- [ ] `0x2F` Particle (`ClientboundParticlePacket`) +- [x] `0x2F` Particle (`ClientboundLevelParticlePacket`) **NOTE: Slot data are parsed as raw ByteArray** - [ ] `0x30` Light Update (`ClientboundLightUpdatePacket`) - [x] `0x31` Login Play (`ClientboundLoginPlayPacket`) - [x] `0x32` Low Disk Space Warning (`ClientboundLowDiskSpaceWarningPacket`) @@ -195,8 +195,8 @@ A lightweight minecraft client-side protocol library for Kotlin Native and JVM - [x] `0x48` Synchronize Player Position (`ClientboundSynchronizePlayerPositionPacket`) - [x] `0x49` Player Rotation (`ClientboundPlayerRotationPacket`) - [ ] `0x4A` Recipe Book Add (`ClientboundRecipeBookAddPacket`) -- [ ] `0x4B` Recipe Book Remove (`ClientboundRecipeBookRemovePacket`) -- [ ] `0x4C` Recipe Book Settings (`ClientboundRecipeBookSettingsPacket`) +- [x] `0x4B` Recipe Book Remove (`ClientboundRecipeBookRemovePacket`) +- [x] `0x4C` Recipe Book Settings (`ClientboundRecipeBookSettingsPacket`) - [x] `0x4D` Remove Entities (`ClientboundRemoveEntitiesPacket`) - [x] `0x4E` Remove Entity Effect (`ClientboundRemoveEntityEffectPacket`) - [x] `0x4F` Reset Score (`ClientboundResetScorePacket`) 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 dad7b38..f7a96f7 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 @@ -42,7 +42,6 @@ public fun BytesBuffer.writePrefixedStringArray(value: List) { } - public inline fun BytesBuffer.readPrefixed(reader: BytesBuffer.() -> T): List { val count = this.readVarInt() require(count in 0..4096) { @@ -57,4 +56,14 @@ public inline fun BytesBuffer.readPrefixed(reader: BytesBuffer.() -> T): Lis public inline fun BytesBuffer.writePrefixed(list: List, writer: BytesBuffer.(T) -> Unit) { this.writeVarInt(list.size) for (item in list) this.writer(item) +} + +public fun BytesBuffer.readPrefixOptional(reader: BytesBuffer.() -> T): T? { + val hasValue = readBoolean() + return if (hasValue) reader() else null +} + +public fun BytesBuffer.writePrefixedOptional(value: T?, writer: BytesBuffer.(T) -> Unit) { + writeBoolean(value != null) + if (value != null) writer(value) } \ No newline at end of file diff --git a/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/primitives/MinecraftString.kt b/libmc-common/src/commonMain/kotlin/cn/rtast/libmc/primitives/String.kt similarity index 100% rename from libmc-common/src/commonMain/kotlin/cn/rtast/libmc/primitives/MinecraftString.kt rename to libmc-common/src/commonMain/kotlin/cn/rtast/libmc/primitives/String.kt diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundDamageEventPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundDamageEventPacket.kt index 9b5db3e..844498b 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundDamageEventPacket.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundDamageEventPacket.kt @@ -10,6 +10,7 @@ package cn.rtast.libmc.protocol.packet.play.clientbound import cn.rtast.libmc.network.BytesBuffer import cn.rtast.libmc.packet.PacketCodec import cn.rtast.libmc.packet.MinecraftPacket +import cn.rtast.libmc.primitives.readPrefixOptional import cn.rtast.libmc.primitives.readVarInt import cn.rtast.libmc.protocol.protocol.game.math.Vec3d import cn.rtast.libmc.protocol.protocol.game.math.readVec3d @@ -30,7 +31,7 @@ public data class ClientboundDamageEventPacket( val sourceCauseId = if (rawCauseId > 0) rawCauseId - 1 else null val rawDirectId = buffer.readVarInt() val sourceDirectId = if (rawDirectId > 0) rawDirectId - 1 else null - val position = buffer.readVec3d(true) + val position = buffer.readPrefixOptional { readVec3d() } return ClientboundDamageEventPacket(entityId, sourceTypeId, sourceCauseId, sourceDirectId, position) } } diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundLevelParticlePacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundLevelParticlePacket.kt new file mode 100644 index 0000000..2961ade --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundLevelParticlePacket.kt @@ -0,0 +1,52 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/9 + */ + + +package cn.rtast.libmc.protocol.packet.play.clientbound + +import cn.rtast.libmc.network.BytesBuffer +import cn.rtast.libmc.packet.MinecraftPacket +import cn.rtast.libmc.packet.PacketCodec +import cn.rtast.libmc.primitives.readVarInt +import cn.rtast.libmc.protocol.protocol.game.math.Vec3d +import cn.rtast.libmc.protocol.protocol.game.math.Vec3f +import cn.rtast.libmc.protocol.protocol.game.math.readVec3d +import cn.rtast.libmc.protocol.protocol.game.math.readVec3f +import cn.rtast.libmc.protocol.protocol.game.particle.ParticleData +import cn.rtast.libmc.protocol.protocol.game.particle.ParticleType +import cn.rtast.libmc.protocol.protocol.game.particle.readParticleData + +public data class ClientboundLevelParticlePacket( + val longDistance: Boolean, + val alwaysVisible: Boolean, + val position: Vec3d, + val offset: Vec3f, + val maxSpeed: Float, + val particleCount: Int, + val particleId: Int, + val particleType: ParticleType, + val data: ParticleData, +) : MinecraftPacket { + internal companion object Codec : PacketCodec { + override fun encode(buffer: BytesBuffer, value: ClientboundLevelParticlePacket) {} + override fun decode(buffer: BytesBuffer): ClientboundLevelParticlePacket { + val longDistance = buffer.readBoolean() + val alwaysVisible = buffer.readBoolean() + val position = buffer.readVec3d() + val offset = buffer.readVec3f() + val maxSpeed = buffer.readFloat() + val particleCount = buffer.readInt() + val particleId = buffer.readVarInt() + val particleType = ParticleType.fromID(particleId) + val data = buffer.readParticleData(particleType) + return ClientboundLevelParticlePacket( + longDistance, alwaysVisible, position, + offset, maxSpeed, particleCount, + particleId, particleType, data + ) + } + } +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundMoveVehiclePacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundMoveVehiclePacket.kt index 2fd2215..31191b4 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundMoveVehiclePacket.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundMoveVehiclePacket.kt @@ -18,7 +18,7 @@ public data class ClientboundMoveVehiclePacket(val position: Vec3d, val yaw: Flo internal companion object Codec : PacketCodec { override fun encode(buffer: BytesBuffer, value: ClientboundMoveVehiclePacket) {} override fun decode(buffer: BytesBuffer): ClientboundMoveVehiclePacket { - val position = buffer.readVec3d()!! + val position = buffer.readVec3d() val yaw = buffer.readFloat() val pitch = buffer.readFloat() return ClientboundMoveVehiclePacket(position, yaw, pitch) diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundPlayerLookAtPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundPlayerLookAtPacket.kt index 7d67d59..61f7e7c 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundPlayerLookAtPacket.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundPlayerLookAtPacket.kt @@ -29,7 +29,7 @@ public data class ClientboundPlayerLookAtPacket( override fun encode(buffer: BytesBuffer, value: ClientboundPlayerLookAtPacket) {} override fun decode(buffer: BytesBuffer): ClientboundPlayerLookAtPacket { val fromAnchor = AnchorPoint.fromID(buffer.readVarInt()) - val targetPosition = buffer.readVec3d()!! + val targetPosition = buffer.readVec3d() val isEntity = buffer.readBoolean() val entityTarget = if (isEntity) { val entityId = buffer.readVarInt() diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundRecipeBookRemovePacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundRecipeBookRemovePacket.kt new file mode 100644 index 0000000..f923f27 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundRecipeBookRemovePacket.kt @@ -0,0 +1,23 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/9 + */ + + +package cn.rtast.libmc.protocol.packet.play.clientbound + +import cn.rtast.libmc.network.BytesBuffer +import cn.rtast.libmc.packet.MinecraftPacket +import cn.rtast.libmc.packet.PacketCodec +import cn.rtast.libmc.primitives.readPrefixed +import cn.rtast.libmc.primitives.readVarInt + +public data class ClientboundRecipeBookRemovePacket(val recipes: List) : MinecraftPacket { + internal companion object Codec : PacketCodec { + override fun encode(buffer: BytesBuffer, value: ClientboundRecipeBookRemovePacket) {} + override fun decode(buffer: BytesBuffer): ClientboundRecipeBookRemovePacket { + return ClientboundRecipeBookRemovePacket(buffer.readPrefixed { readVarInt() }) + } + } +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundRecipeBookSettingsPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundRecipeBookSettingsPacket.kt new file mode 100644 index 0000000..d073e19 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundRecipeBookSettingsPacket.kt @@ -0,0 +1,43 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/9 + */ + + +package cn.rtast.libmc.protocol.packet.play.clientbound + +import cn.rtast.libmc.network.BytesBuffer +import cn.rtast.libmc.packet.MinecraftPacket +import cn.rtast.libmc.packet.PacketCodec + +public data class ClientboundRecipeBookSettingsPacket( + val craftingBookOpen: Boolean, + val craftingFilterActive: Boolean, + val smeltingBookOpen: Boolean, + val smeltingFilterActive: Boolean, + val blastFurnaceBookOpen: Boolean, + val blastFurnaceFilterActive: Boolean, + val smokerBookOpen: Boolean, + val smokerFilterActive: Boolean, +) : MinecraftPacket { + internal companion object Codec : PacketCodec { + override fun encode(buffer: BytesBuffer, value: ClientboundRecipeBookSettingsPacket) {} + override fun decode(buffer: BytesBuffer): ClientboundRecipeBookSettingsPacket { + val craftingBookOpen = buffer.readBoolean() + val craftingFilterActive = buffer.readBoolean() + val smeltingBookOpen = buffer.readBoolean() + val smeltingFilterActive = buffer.readBoolean() + val blastFurnaceBookOpen = buffer.readBoolean() + val blastFurnaceFilterActive = buffer.readBoolean() + val smokerBookOpen = buffer.readBoolean() + val smokerFilterActive = buffer.readBoolean() + return ClientboundRecipeBookSettingsPacket( + craftingBookOpen, craftingFilterActive, + smeltingBookOpen, smeltingFilterActive, + blastFurnaceBookOpen, blastFurnaceFilterActive, + smokerBookOpen, smokerFilterActive + ) + } + } +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSynchronizePlayerPositionPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSynchronizePlayerPositionPacket.kt index 5c51ecb..08b5493 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSynchronizePlayerPositionPacket.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSynchronizePlayerPositionPacket.kt @@ -27,8 +27,8 @@ public data class ClientboundSynchronizePlayerPositionPacket( override fun encode(buffer: BytesBuffer, value: ClientboundSynchronizePlayerPositionPacket) {} override fun decode(buffer: BytesBuffer): ClientboundSynchronizePlayerPositionPacket { val teleportId = buffer.readVarInt() - val position = buffer.readVec3d()!! - val velocity = buffer.readVec3d()!! + val position = buffer.readVec3d() + val velocity = buffer.readVec3d() val yaw = buffer.readFloat() val pitch = buffer.readFloat() val rawFlags = buffer.readInt() diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSynchronizeVehiclePositionPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSynchronizeVehiclePositionPacket.kt index f0c2b28..15fa30d 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSynchronizeVehiclePositionPacket.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundSynchronizeVehiclePositionPacket.kt @@ -28,8 +28,8 @@ public data class ClientboundSynchronizeVehiclePositionPacket( override fun encode(buffer: BytesBuffer, value: ClientboundSynchronizeVehiclePositionPacket) {} override fun decode(buffer: BytesBuffer): ClientboundSynchronizeVehiclePositionPacket { val entityId = buffer.readVarInt() - val position = buffer.readVec3d()!! - val velocityPosition = buffer.readVec3d()!! + val position = buffer.readVec3d() + val velocityPosition = buffer.readVec3d() val yaw = buffer.readFloat() val pitch = buffer.readFloat() val flags = TeleportFlags.fromInt(buffer.readInt()) diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundTeleportEntityPacket.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundTeleportEntityPacket.kt index fafa5c2..1255bac 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundTeleportEntityPacket.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/packet/play/clientbound/ClientboundTeleportEntityPacket.kt @@ -32,8 +32,8 @@ public data class ClientboundTeleportEntityPacket( override fun encode(buffer: BytesBuffer, value: ClientboundTeleportEntityPacket) {} override fun decode(buffer: BytesBuffer): ClientboundTeleportEntityPacket { val entityId = buffer.readVarInt() - val position = buffer.readVec3d()!! - val velocityPosition = buffer.readVec3d()!! + val position = buffer.readVec3d() + val velocityPosition = buffer.readVec3d() val yaw = buffer.readFloat() val pitch = buffer.readFloat() val onGround = buffer.readBoolean() diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/GamePacketsProtocolCodec.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/GamePacketsProtocolCodec.kt index 7cf8d52..ac9252d 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/GamePacketsProtocolCodec.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/GamePacketsProtocolCodec.kt @@ -107,7 +107,7 @@ internal object GamePacketsProtocolCodec { register(0x2C, ClientboundKeepAlivePlayPacket) // register(0x2D, ClientboundLevelChunkUpdateWithLightPacket) register(0x2E, ClientboundLevelEventPacket) -// register(0x2F, ClientboundParticlePacket) + register(0x2F, ClientboundLevelParticlePacket) // register(0x30, ClientboundLightUpdatePacket) register(0x31, ClientboundLoginPlayPacket) register(0x32, ClientboundLowDiskSpaceWarningPacket) @@ -135,8 +135,8 @@ internal object GamePacketsProtocolCodec { register(0x48, ClientboundSynchronizePlayerPositionPacket) register(0x49, ClientboundPlayerRotationPacket) // register(0x4A, ClientboundRecipeBookAddPacket) -// register(0x4B, ClientboundRecipeBookRemovePacket) -// register(0x4C, ClientboundRecipeBookSettingsPacket) + register(0x4B, ClientboundRecipeBookRemovePacket) + register(0x4C, ClientboundRecipeBookSettingsPacket) register(0x4D, ClientboundRemoveEntitiesPacket) register(0x4E, ClientboundRemoveEntityEffectPacket) register(0x4F, ClientboundResetScorePacket) diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/color/Color24.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/color/Color24.kt new file mode 100644 index 0000000..350df69 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/color/Color24.kt @@ -0,0 +1,47 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/9 + */ + + +package cn.rtast.libmc.protocol.protocol.game.color + +import cn.rtast.libmc.network.BytesBuffer +import kotlin.jvm.JvmInline + +@JvmInline +public value class Color24(public val rawValue: Int) { + public val rgb: Int get() = rawValue and 0xFFFFFF + public val red: Int get() = (rgb shr 16) and 0xFF + public val green: Int get() = (rgb shr 8) and 0xFF + public val blue: Int get() = rgb and 0xFF + + public val redFloat: Float get() = red / 255.0f + public val greenFloat: Float get() = green / 255.0f + public val blueFloat: Float get() = blue / 255.0f + + override fun toString(): String = "Color24(r=$red, g=$green, b=$blue, hex=0x${rgb.toString(16).padStart(6, '0')})" + + public companion object { + public val BLACK: Color24 = Color24(0x000000) + public val WHITE: Color24 = Color24(0xFFFFFF) + public val RED: Color24 = Color24(0xFF0000) + public val GREEN: Color24 = Color24(0x00FF00) + public val BLUE: Color24 = Color24(0x0000FF) + + public fun fromRGB(red: Int, green: Int, blue: Int): Color24 { + val r = red.coerceIn(0, 255) + val g = green.coerceIn(0, 255) + val b = blue.coerceIn(0, 255) + return Color24((r shl 16) or (g shl 8) or b) + } + + public fun fromRGBFloat(red: Float, green: Float, blue: Float): Color24 { + return fromRGB((red * 255.0f).toInt(), (green * 255.0f).toInt(), (blue * 255.0f).toInt()) + } + } +} + +internal fun BytesBuffer.readColor24(): Color24 = Color24(readInt()) +internal fun BytesBuffer.writeColor24(color: Color24) = writeInt(color.rawValue) \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/entity/Minecart.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/entity/Minecart.kt index 5a60623..cd51376 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/entity/Minecart.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/entity/Minecart.kt @@ -22,8 +22,8 @@ public data class MinecartStep( ) internal fun BytesBuffer.readMinecartStep(): MinecartStep { - val position = readVec3d()!! - val velocity = readVec3d()!! + val position = readVec3d() + val velocity = readVec3d() val yaw = readAngle() val pitch = readAngle() val weight = readFloat() diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/math/Vec3d.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/math/Vec3d.kt index 193ae1c..0e8604a 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/math/Vec3d.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/math/Vec3d.kt @@ -15,21 +15,15 @@ public data class Vec3d(val x: Double, val y: Double, val z: Double) { } } -internal fun BytesBuffer.readVec3d(optional: Boolean = false): Vec3d? { - if (optional && !this.readBoolean()) return null +internal fun BytesBuffer.readVec3d(): Vec3d { val x = this.readDouble() val y = this.readDouble() val z = this.readDouble() return Vec3d(x, y, z) } -internal fun BytesBuffer.writeVec3d(vec3d: Vec3d?, optional: Boolean = false) { - if (optional) { - this.writeBoolean(vec3d != null) - if (vec3d == null) return - } - val value = requireNotNull(vec3d) { "Vec3d cannot be null when optional is false" } - this.writeDouble(value.x) - this.writeDouble(value.y) - this.writeDouble(value.z) +internal fun BytesBuffer.writeVec3d(vec3d: Vec3d) { + this.writeDouble(vec3d.x) + this.writeDouble(vec3d.y) + this.writeDouble(vec3d.z) } \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/math/Vec3f.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/math/Vec3f.kt new file mode 100644 index 0000000..31c21d5 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/math/Vec3f.kt @@ -0,0 +1,29 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/9 + */ + + +package cn.rtast.libmc.protocol.protocol.game.math + +import cn.rtast.libmc.network.BytesBuffer + +public data class Vec3f(val x: Float, val y: Float, val z: Float) { + public companion object { + public val ZERO: Vec3f = Vec3f(0f, 0f, 0f) + } +} + +internal fun BytesBuffer.readVec3f(): Vec3f { + val x = readFloat() + val y = readFloat() + val z = readFloat() + return Vec3f(x, y, z) +} + +internal fun BytesBuffer.writeVec3f(value: Vec3f) { + writeFloat(value.x) + writeFloat(value.y) + writeFloat(value.z) +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/particle/ParticleData.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/particle/ParticleData.kt new file mode 100644 index 0000000..bef97cd --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/particle/ParticleData.kt @@ -0,0 +1,207 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/9 + */ + + +package cn.rtast.libmc.protocol.protocol.game.particle + +import cn.rtast.libmc.network.BytesBuffer +import cn.rtast.libmc.primitives.readVarInt +import cn.rtast.libmc.protocol.protocol.game.block.BlockPos +import cn.rtast.libmc.protocol.protocol.game.block.readBlockPos +import cn.rtast.libmc.protocol.protocol.game.color.Color24 +import cn.rtast.libmc.protocol.protocol.game.color.readColor24 +import cn.rtast.libmc.protocol.protocol.game.math.Vec3d +import cn.rtast.libmc.protocol.protocol.game.math.readVec3d + +public sealed interface ParticleData { + public object Empty : ParticleData + public data class Block(val blockStateId: Int) : ParticleData + public data class Geyser(val waterBlocks: Int) : ParticleData + public data class GeyserBase(val waterBlocks: Int, val burstImpulseBase: Float) : ParticleData + public data class GeyserPoof(val waterBlocks: Int, val burstImpulseBase: Float) : ParticleData + public data class GeyserPlume(val waterBlocks: Int) : ParticleData + public data class DragonBreath(val power: Float) : ParticleData + public data class Dust(val color: Color24, val scale: Float) : ParticleData + public data class DustColorTransition(val fromColor: Color24, val toColor: Color24, val scale: Float) : + ParticleData + + public data class Effect(val color: Color24, val power: Float) : ParticleData + public data class ColorARGB(val colorARGB: Int) : ParticleData + public data class SculkCharge(val roll: Float) : ParticleData + public data class Vibration(val source: VibrationSource, val ticks: Int) : ParticleData { + public sealed interface VibrationSource { + public data class Block(val position: BlockPos) : VibrationSource + public data class Entity(val entityId: Int, val eyeHeight: Float) : VibrationSource + } + } + + public data class Trail(val position: Vec3d, val color: Color24, val durationTicks: Int) : ParticleData + public data class Shriek(val delay: Int) : ParticleData + public data class Item(val slot: ByteArray) : ParticleData { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + other as Item + return slot.contentEquals(other.slot) + } + + override fun hashCode(): Int { + return slot.contentHashCode() + } + } +} + +internal fun BytesBuffer.readParticleData(type: ParticleType): ParticleData { + return when (type) { + ParticleType.ANGRY_VILLAGER, + ParticleType.BUBBLE, + ParticleType.SULFUR_BUBBLES, + ParticleType.NOXIOUS_GAS, + ParticleType.NOXIOUS_GAS_CLOUD, + ParticleType.CLOUD, + ParticleType.COPPER_FIRE_FLAME, + ParticleType.CRIT, + ParticleType.DAMAGE_INDICATOR, + ParticleType.DRIPPING_LAVA, + ParticleType.FALLING_LAVA, + ParticleType.LANDING_LAVA, + ParticleType.DRIPPING_WATER, + ParticleType.FALLING_WATER, + ParticleType.ELDER_GUARDIAN, + ParticleType.ENCHANTED_HIT, + ParticleType.ENCHANT, + ParticleType.END_ROD, + ParticleType.EXPLOSION_EMITTER, + ParticleType.EXPLOSION, + ParticleType.GUST, + ParticleType.SMALL_GUST, + ParticleType.GUST_EMITTER_LARGE, + ParticleType.GUST_EMITTER_SMALL, + ParticleType.SONIC_BOOM, + ParticleType.FIREWORK, + ParticleType.FISHING, + ParticleType.FLAME, + ParticleType.INFESTED, + ParticleType.CHERRY_LEAVES, + ParticleType.PALE_OAK_LEAVES, + ParticleType.SCULK_SOUL, + ParticleType.SCULK_CHARGE_POP, + ParticleType.SOUL_FIRE_FLAME, + ParticleType.SOUL, + ParticleType.HAPPY_VILLAGER, + ParticleType.COMPOSTER, + ParticleType.HEART, + ParticleType.PAUSE_MOB_GROWTH, + ParticleType.RESET_MOB_GROWTH, + ParticleType.ITEM_SLIME, + ParticleType.ITEM_COBWEB, + ParticleType.ITEM_SNOWBALL, + ParticleType.LARGE_SMOKE, + ParticleType.LAVA, + ParticleType.MYCELIUM, + ParticleType.NOTE, + ParticleType.POOF, + ParticleType.PORTAL, + ParticleType.RAIN, + ParticleType.SMOKE, + ParticleType.WHITE_SMOKE, + ParticleType.SNEEZE, + ParticleType.SPIT, + ParticleType.SQUID_INK, + ParticleType.SWEEP_ATTACK, + ParticleType.TOTEM_OF_UNDYING, + ParticleType.UNDERWATER, + ParticleType.SPLASH, + ParticleType.WITCH, + ParticleType.BUBBLE_POP, + ParticleType.CURRENT_DOWN, + ParticleType.BUBBLE_COLUMN_UP, + ParticleType.NAUTILUS, + ParticleType.DOLPHIN, + ParticleType.CAMPFIRE_COSY_SMOKE, + ParticleType.CAMPFIRE_SIGNAL_SMOKE, + ParticleType.DRIPPING_HONEY, + ParticleType.FALLING_HONEY, + ParticleType.LANDING_HONEY, + ParticleType.FALLING_NECTAR, + ParticleType.FALLING_SPORE_BLOSSOM, + ParticleType.ASH, + ParticleType.CRIMSON_SPORE, + ParticleType.WARPED_SPORE, + ParticleType.SPORE_BLOSSOM_AIR, + ParticleType.DRIPPING_OBSIDIAN_TEAR, + ParticleType.FALLING_OBSIDIAN_TEAR, + ParticleType.LANDING_OBSIDIAN_TEAR, + ParticleType.REVERSE_PORTAL, + ParticleType.WHITE_ASH, + ParticleType.SMALL_FLAME, + ParticleType.SNOWFLAKE, + ParticleType.DRIPPING_DRIPSTONE_LAVA, + ParticleType.FALLING_DRIPSTONE_LAVA, + ParticleType.DRIPPING_DRIPSTONE_WATER, + ParticleType.FALLING_DRIPSTONE_WATER, + ParticleType.GLOW_SQUID_INK, + ParticleType.GLOW, + ParticleType.WAX_ON, + ParticleType.WAX_OFF, + ParticleType.ELECTRIC_SPARK, + ParticleType.SCRAPE, + ParticleType.EGG_CRACK, + ParticleType.DUST_PLUME, + ParticleType.TRIAL_SPAWNER_DETECTED_PLAYER, + ParticleType.TRIAL_SPAWNER_DETECTED_PLAYER_OMINOUS, + ParticleType.VAULT_CONNECTION, + ParticleType.OMINOUS_SPAWNING, + ParticleType.RAID_OMEN, + ParticleType.TRIAL_OMEN, + ParticleType.FIREFLY, + ParticleType.SULFUR_CUBE_GOO, + -> ParticleData.Empty + + ParticleType.BLOCK, + ParticleType.BLOCK_MARKER, + ParticleType.FALLING_DUST, + ParticleType.DUST_PILLAR, + ParticleType.BLOCK_CRUMBLE, + -> ParticleData.Block(this.readVarInt()) + + ParticleType.GEYSER -> ParticleData.Geyser(this.readInt()) + ParticleType.GEYSER_BASE -> ParticleData.GeyserBase(this.readInt(), this.readFloat()) + ParticleType.GEYSER_POOF -> ParticleData.GeyserPoof(this.readInt(), this.readFloat()) + ParticleType.GEYSER_PLUME -> ParticleData.GeyserPlume(this.readInt()) + ParticleType.DRAGON_BREATH -> ParticleData.DragonBreath(this.readFloat()) + ParticleType.SCULK_CHARGE -> ParticleData.SculkCharge(this.readFloat()) + ParticleType.SHRIEK -> ParticleData.Shriek(this.readVarInt()) + ParticleType.ENTITY_EFFECT, + ParticleType.TINTED_LEAVES, + ParticleType.FLASH, + -> ParticleData.ColorARGB(this.readInt()) + + ParticleType.DUST -> ParticleData.Dust(this.readColor24(), this.readFloat()) + ParticleType.DUST_COLOR_TRANSITION -> ParticleData.DustColorTransition( + this.readColor24(), + this.readColor24(), + this.readFloat() + ) + + ParticleType.EFFECT, + ParticleType.INSTANT_EFFECT, + -> ParticleData.Effect(this.readColor24(), this.readFloat()) + + // slot parsed as raw bytearray + ParticleType.ITEM -> ParticleData.Item(this.toByteArray()) + ParticleType.VIBRATION -> { + val source = when (val sourceTypeId = this.readVarInt()) { + 0 -> ParticleData.Vibration.VibrationSource.Block(this.readBlockPos()) + 1 -> ParticleData.Vibration.VibrationSource.Entity(this.readVarInt(), this.readFloat()) + else -> error("Unknown vibration source type: $sourceTypeId") + } + ParticleData.Vibration(source, this.readVarInt()) + } + + ParticleType.TRAIL -> ParticleData.Trail(readVec3d(), readColor24(), this.readVarInt()) + } +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/particle/ParticleType.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/particle/ParticleType.kt new file mode 100644 index 0000000..25dafb4 --- /dev/null +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/protocol/game/particle/ParticleType.kt @@ -0,0 +1,62 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/9/9 + */ + + +package cn.rtast.libmc.protocol.protocol.game.particle + +import cn.rtast.libmc.protocol.protocol.game.Identifier + +public enum class ParticleType(public val id: Int, public val key: String) { + ANGRY_VILLAGER(0, "angry_villager"), BLOCK(1, "block"), BLOCK_MARKER(2, "block_marker"), + BUBBLE(3, "bubble"), SULFUR_BUBBLES(4, "sulfur_bubbles"), NOXIOUS_GAS(5, "noxious_gas"), + NOXIOUS_GAS_CLOUD(6, "noxious_gas_cloud"), GEYSER(7, "geyser"), GEYSER_BASE(8, "geyser_base"), + GEYSER_POOF(9, "geyser_poof"), GEYSER_PLUME(10, "geyser_plume"), CLOUD(11, "cloud"), + COPPER_FIRE_FLAME(12, "copper_fire_flame"), CRIT(13, "crit"), DAMAGE_INDICATOR(14, "damage_indicator"), + DRAGON_BREATH(15, "dragon_breath"), DRIPPING_LAVA(16, "dripping_lava"), FALLING_LAVA(17, "falling_lava"), + LANDING_LAVA(18, "landing_lava"), DRIPPING_WATER(19, "dripping_water"), FALLING_WATER(20, "falling_water"), + DUST(21, "dust"), DUST_COLOR_TRANSITION(22, "dust_color_transition"), EFFECT(23, "effect"), + ELDER_GUARDIAN(24, "elder_guardian"), ENCHANTED_HIT(25, "enchanted_hit"), ENCHANT(26, "enchant"), + END_ROD(27, "end_rod"), ENTITY_EFFECT(28, "entity_effect"), EXPLOSION_EMITTER(29, "explosion_emitter"), + EXPLOSION(30, "explosion"), GUST(31, "gust"), SMALL_GUST(32, "small_gust"), + GUST_EMITTER_LARGE(33, "gust_emitter_large"), GUST_EMITTER_SMALL(34, "gust_emitter_small"), + SONIC_BOOM(35, "sonic_boom"), FALLING_DUST(36, "falling_dust"), FIREWORK(37, "firework"), FISHING(38, "fishing"), + FLAME(39, "flame"), INFESTED(40, "infested"), CHERRY_LEAVES(41, "cherry_leaves"), + PALE_OAK_LEAVES(42, "pale_oak_leaves"), TINTED_LEAVES(43, "tinted_leaves"), SCULK_SOUL(44, "sculk_soul"), + SCULK_CHARGE(45, "sculk_charge"), SCULK_CHARGE_POP(46, "sculk_charge_pop"), SOUL_FIRE_FLAME(47, "soul_fire_flame"), + SOUL(48, "soul"), FLASH(49, "flash"), HAPPY_VILLAGER(50, "happy_villager"), COMPOSTER(51, "composter"), + HEART(52, "heart"), INSTANT_EFFECT(53, "instant_effect"), ITEM(54, "item"), VIBRATION(55, "vibration"), + TRAIL(56, "trail"), PAUSE_MOB_GROWTH(57, "pause_mob_growth"), RESET_MOB_GROWTH(58, "reset_mob_growth"), + ITEM_SLIME(59, "item_slime"), ITEM_COBWEB(60, "item_cobweb"), ITEM_SNOWBALL(61, "item_snowball"), + LARGE_SMOKE(62, "large_smoke"), LAVA(63, "lava"), MYCELIUM(64, "mycelium"), NOTE(65, "note"), POOF(66, "poof"), + PORTAL(67, "portal"), RAIN(68, "rain"), SMOKE(69, "smoke"), WHITE_SMOKE(70, "white_smoke"), SNEEZE(71, "sneeze"), + SPIT(72, "spit"), SQUID_INK(73, "squid_ink"), SWEEP_ATTACK(74, "sweep_attack"), + TOTEM_OF_UNDYING(75, "totem_of_undying"), UNDERWATER(76, "underwater"), SPLASH(77, "splash"), WITCH(78, "witch"), + BUBBLE_POP(79, "bubble_pop"), CURRENT_DOWN(80, "current_down"), BUBBLE_COLUMN_UP(81, "bubble_column_up"), + NAUTILUS(82, "nautilus"), DOLPHIN(83, "dolphin"), CAMPFIRE_COSY_SMOKE(84, "campfire_cosy_smoke"), + CAMPFIRE_SIGNAL_SMOKE(85, "campfire_signal_smoke"), DRIPPING_HONEY(86, "dripping_honey"), + FALLING_HONEY(87, "falling_honey"), LANDING_HONEY(88, "landing_honey"), FALLING_NECTAR(89, "falling_nectar"), + FALLING_SPORE_BLOSSOM(90, "falling_spore_blossom"), ASH(91, "ash"), CRIMSON_SPORE(92, "crimson_spore"), + WARPED_SPORE(93, "warped_spore"), SPORE_BLOSSOM_AIR(94, "spore_blossom_air"), + DRIPPING_OBSIDIAN_TEAR(95, "dripping_obsidian_tear"), FALLING_OBSIDIAN_TEAR(96, "falling_obsidian_tear"), + LANDING_OBSIDIAN_TEAR(97, "landing_obsidian_tear"), REVERSE_PORTAL(98, "reverse_portal"), + WHITE_ASH(99, "white_ash"), SMALL_FLAME(100, "small_flame"), SNOWFLAKE(101, "snowflake"), + DRIPPING_DRIPSTONE_LAVA(102, "dripping_dripstone_lava"), FALLING_DRIPSTONE_LAVA(103, "falling_dripstone_lava"), + DRIPPING_DRIPSTONE_WATER(104, "dripping_dripstone_water"), FALLING_DRIPSTONE_WATER(105, "falling_dripstone_water"), + GLOW_SQUID_INK(106, "glow_squid_ink"), GLOW(107, "glow"), WAX_ON(108, "wax_on"), WAX_OFF(109, "wax_off"), + ELECTRIC_SPARK(110, "electric_spark"), SCRAPE(111, "scrape"), SHRIEK(112, "shriek"), EGG_CRACK(113, "egg_crack"), + DUST_PLUME(114, "dust_plume"), TRIAL_SPAWNER_DETECTED_PLAYER(115, "trial_spawner_detection"), + TRIAL_SPAWNER_DETECTED_PLAYER_OMINOUS(116, "trial_spawner_detection_ominous"), + VAULT_CONNECTION(117, "vault_connection"), DUST_PILLAR(118, "dust_pillar"), + OMINOUS_SPAWNING(119, "ominous_spawning"), RAID_OMEN(120, "raid_omen"), TRIAL_OMEN(121, "trial_omen"), + BLOCK_CRUMBLE(122, "block_crumble"), FIREFLY(123, "firefly"), SULFUR_CUBE_GOO(124, "sulfur_cube_goo"); + + public val identifier: Identifier = Identifier.of("minecraft", key) + + public companion object { + private val CACHED_ID = entries + public fun fromID(id: Int): ParticleType = CACHED_ID.first { it.id == id } + } +} \ No newline at end of file diff --git a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/util/duration.kt b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/util/duration.kt index 7d8f12e..d36e725 100644 --- a/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/util/duration.kt +++ b/libmc-protocol/src/commonMain/kotlin/cn/rtast/libmc/protocol/util/duration.kt @@ -11,31 +11,33 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds /** - * Convert an int value to minecraft tick + * Converts an [Int] representing a count of ticks into a [Duration] */ public inline val Int.ticks: Duration get() = (this * 50).milliseconds /** - * Convert a long value to minecraft tick + * Converts a [Long] representing a count of ticks into a [Duration] */ public inline val Long.ticks: Duration get() = (this * 50).milliseconds /** - * Convert a double value to minecraft tick + * Converts a [Double] representing a count of + * ticks (including fractional ticks) into a [Duration] */ public inline val Double.ticks: Duration get() = (this * 50.0).milliseconds /** - * Convert [Duration] to ticks([Long]) + * Converts this [Duration] to the total number of whole ticks (floored) */ public inline val Duration.inWholeTicks: Long get() = this.inWholeMilliseconds / 50 /** - * Convert [Duration] to ticks([Double]) + * Converts this [Duration] to the number of ticks as a [Double], + * preserving fractional ticks for high-precision time calculations */ public inline val Duration.inTicksDouble: Double get() = this.inWholeNanoseconds / 50_000_000.0 \ No newline at end of file diff --git a/libmc-protocol/src/commonTest/kotlin/client/TestClient.kt b/libmc-protocol/src/commonTest/kotlin/client/TestClient.kt index f9e85fc..cc10294 100644 --- a/libmc-protocol/src/commonTest/kotlin/client/TestClient.kt +++ b/libmc-protocol/src/commonTest/kotlin/client/TestClient.kt @@ -10,7 +10,11 @@ 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.util.generateOfflineUuid import io.ktor.client.* @@ -67,21 +71,20 @@ class TestClient { socketEngine = KtorNetworkEngine() } ) - 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) +// 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.launch { cli.connect() } while (true) { }