Implemented 0x2F-ClientboundLevelParticle, 0x4B-ClientboundRecipeBookRemovePacket, 0x4C-ClientboundRecipeBookSettingsPacket packets

This commit is contained in:
2026-09-09 19:31:08 +08:00
parent 54409d9e8a
commit 7bbe99ba01
21 files changed
+521 -49

No files matched your search

+3 -3
View File
@@ -167,7 +167,7 @@ A lightweight minecraft client-side protocol library for Kotlin Native and JVM
- [x] `0x2C` Keep Alive (`ClientboundKeepAlivePlayPacket`) - [x] `0x2C` Keep Alive (`ClientboundKeepAlivePlayPacket`)
- [ ] `0x2D` Level Chunk Update With Light (`ClientboundLevelChunkUpdateWithLightPacket`) - [ ] `0x2D` Level Chunk Update With Light (`ClientboundLevelChunkUpdateWithLightPacket`)
- [x] `0x2E` Level Event (`ClientboundLevelEventPacket`) - [x] `0x2E` Level Event (`ClientboundLevelEventPacket`)
- [ ] `0x2F` Particle (`ClientboundParticlePacket`) - [x] `0x2F` Particle (`ClientboundLevelParticlePacket`) **NOTE: Slot data are parsed as raw ByteArray**
- [ ] `0x30` Light Update (`ClientboundLightUpdatePacket`) - [ ] `0x30` Light Update (`ClientboundLightUpdatePacket`)
- [x] `0x31` Login Play (`ClientboundLoginPlayPacket`) - [x] `0x31` Login Play (`ClientboundLoginPlayPacket`)
- [x] `0x32` Low Disk Space Warning (`ClientboundLowDiskSpaceWarningPacket`) - [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] `0x48` Synchronize Player Position (`ClientboundSynchronizePlayerPositionPacket`)
- [x] `0x49` Player Rotation (`ClientboundPlayerRotationPacket`) - [x] `0x49` Player Rotation (`ClientboundPlayerRotationPacket`)
- [ ] `0x4A` Recipe Book Add (`ClientboundRecipeBookAddPacket`) - [ ] `0x4A` Recipe Book Add (`ClientboundRecipeBookAddPacket`)
- [ ] `0x4B` Recipe Book Remove (`ClientboundRecipeBookRemovePacket`) - [x] `0x4B` Recipe Book Remove (`ClientboundRecipeBookRemovePacket`)
- [ ] `0x4C` Recipe Book Settings (`ClientboundRecipeBookSettingsPacket`) - [x] `0x4C` Recipe Book Settings (`ClientboundRecipeBookSettingsPacket`)
- [x] `0x4D` Remove Entities (`ClientboundRemoveEntitiesPacket`) - [x] `0x4D` Remove Entities (`ClientboundRemoveEntitiesPacket`)
- [x] `0x4E` Remove Entity Effect (`ClientboundRemoveEntityEffectPacket`) - [x] `0x4E` Remove Entity Effect (`ClientboundRemoveEntityEffectPacket`)
- [x] `0x4F` Reset Score (`ClientboundResetScorePacket`) - [x] `0x4F` Reset Score (`ClientboundResetScorePacket`)
@@ -42,7 +42,6 @@ public fun BytesBuffer.writePrefixedStringArray(value: List<String>) {
} }
public inline fun <T> BytesBuffer.readPrefixed(reader: BytesBuffer.() -> T): List<T> { public inline fun <T> BytesBuffer.readPrefixed(reader: BytesBuffer.() -> T): List<T> {
val count = this.readVarInt() val count = this.readVarInt()
require(count in 0..4096) { require(count in 0..4096) {
@@ -57,4 +56,14 @@ public inline fun <T> BytesBuffer.readPrefixed(reader: BytesBuffer.() -> T): Lis
public inline fun <T> BytesBuffer.writePrefixed(list: List<T>, writer: BytesBuffer.(T) -> Unit) { public inline fun <T> BytesBuffer.writePrefixed(list: List<T>, writer: BytesBuffer.(T) -> Unit) {
this.writeVarInt(list.size) this.writeVarInt(list.size)
for (item in list) this.writer(item) for (item in list) this.writer(item)
}
public fun <T> BytesBuffer.readPrefixOptional(reader: BytesBuffer.() -> T): T? {
val hasValue = readBoolean()
return if (hasValue) reader() else null
}
public fun <T> BytesBuffer.writePrefixedOptional(value: T?, writer: BytesBuffer.(T) -> Unit) {
writeBoolean(value != null)
if (value != null) writer(value)
} }
@@ -10,6 +10,7 @@ package cn.rtast.libmc.protocol.packet.play.clientbound
import cn.rtast.libmc.network.BytesBuffer import cn.rtast.libmc.network.BytesBuffer
import cn.rtast.libmc.packet.PacketCodec import cn.rtast.libmc.packet.PacketCodec
import cn.rtast.libmc.packet.MinecraftPacket import cn.rtast.libmc.packet.MinecraftPacket
import cn.rtast.libmc.primitives.readPrefixOptional
import cn.rtast.libmc.primitives.readVarInt import cn.rtast.libmc.primitives.readVarInt
import cn.rtast.libmc.protocol.protocol.game.math.Vec3d import cn.rtast.libmc.protocol.protocol.game.math.Vec3d
import cn.rtast.libmc.protocol.protocol.game.math.readVec3d 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 sourceCauseId = if (rawCauseId > 0) rawCauseId - 1 else null
val rawDirectId = buffer.readVarInt() val rawDirectId = buffer.readVarInt()
val sourceDirectId = if (rawDirectId > 0) rawDirectId - 1 else null 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) return ClientboundDamageEventPacket(entityId, sourceTypeId, sourceCauseId, sourceDirectId, position)
} }
} }
@@ -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<ClientboundLevelParticlePacket> {
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
)
}
}
}
@@ -18,7 +18,7 @@ public data class ClientboundMoveVehiclePacket(val position: Vec3d, val yaw: Flo
internal companion object Codec : PacketCodec<ClientboundMoveVehiclePacket> { internal companion object Codec : PacketCodec<ClientboundMoveVehiclePacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundMoveVehiclePacket) {} override fun encode(buffer: BytesBuffer, value: ClientboundMoveVehiclePacket) {}
override fun decode(buffer: BytesBuffer): ClientboundMoveVehiclePacket { override fun decode(buffer: BytesBuffer): ClientboundMoveVehiclePacket {
val position = buffer.readVec3d()!! val position = buffer.readVec3d()
val yaw = buffer.readFloat() val yaw = buffer.readFloat()
val pitch = buffer.readFloat() val pitch = buffer.readFloat()
return ClientboundMoveVehiclePacket(position, yaw, pitch) return ClientboundMoveVehiclePacket(position, yaw, pitch)
@@ -29,7 +29,7 @@ public data class ClientboundPlayerLookAtPacket(
override fun encode(buffer: BytesBuffer, value: ClientboundPlayerLookAtPacket) {} override fun encode(buffer: BytesBuffer, value: ClientboundPlayerLookAtPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundPlayerLookAtPacket { override fun decode(buffer: BytesBuffer): ClientboundPlayerLookAtPacket {
val fromAnchor = AnchorPoint.fromID(buffer.readVarInt()) val fromAnchor = AnchorPoint.fromID(buffer.readVarInt())
val targetPosition = buffer.readVec3d()!! val targetPosition = buffer.readVec3d()
val isEntity = buffer.readBoolean() val isEntity = buffer.readBoolean()
val entityTarget = if (isEntity) { val entityTarget = if (isEntity) {
val entityId = buffer.readVarInt() val entityId = buffer.readVarInt()
@@ -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<Int>) : MinecraftPacket {
internal companion object Codec : PacketCodec<ClientboundRecipeBookRemovePacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundRecipeBookRemovePacket) {}
override fun decode(buffer: BytesBuffer): ClientboundRecipeBookRemovePacket {
return ClientboundRecipeBookRemovePacket(buffer.readPrefixed { readVarInt() })
}
}
}
@@ -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<ClientboundRecipeBookSettingsPacket> {
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
)
}
}
}
@@ -27,8 +27,8 @@ public data class ClientboundSynchronizePlayerPositionPacket(
override fun encode(buffer: BytesBuffer, value: ClientboundSynchronizePlayerPositionPacket) {} override fun encode(buffer: BytesBuffer, value: ClientboundSynchronizePlayerPositionPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundSynchronizePlayerPositionPacket { override fun decode(buffer: BytesBuffer): ClientboundSynchronizePlayerPositionPacket {
val teleportId = buffer.readVarInt() val teleportId = buffer.readVarInt()
val position = buffer.readVec3d()!! val position = buffer.readVec3d()
val velocity = buffer.readVec3d()!! val velocity = buffer.readVec3d()
val yaw = buffer.readFloat() val yaw = buffer.readFloat()
val pitch = buffer.readFloat() val pitch = buffer.readFloat()
val rawFlags = buffer.readInt() val rawFlags = buffer.readInt()
@@ -28,8 +28,8 @@ public data class ClientboundSynchronizeVehiclePositionPacket(
override fun encode(buffer: BytesBuffer, value: ClientboundSynchronizeVehiclePositionPacket) {} override fun encode(buffer: BytesBuffer, value: ClientboundSynchronizeVehiclePositionPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundSynchronizeVehiclePositionPacket { override fun decode(buffer: BytesBuffer): ClientboundSynchronizeVehiclePositionPacket {
val entityId = buffer.readVarInt() val entityId = buffer.readVarInt()
val position = buffer.readVec3d()!! val position = buffer.readVec3d()
val velocityPosition = buffer.readVec3d()!! val velocityPosition = buffer.readVec3d()
val yaw = buffer.readFloat() val yaw = buffer.readFloat()
val pitch = buffer.readFloat() val pitch = buffer.readFloat()
val flags = TeleportFlags.fromInt(buffer.readInt()) val flags = TeleportFlags.fromInt(buffer.readInt())
@@ -32,8 +32,8 @@ public data class ClientboundTeleportEntityPacket(
override fun encode(buffer: BytesBuffer, value: ClientboundTeleportEntityPacket) {} override fun encode(buffer: BytesBuffer, value: ClientboundTeleportEntityPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundTeleportEntityPacket { override fun decode(buffer: BytesBuffer): ClientboundTeleportEntityPacket {
val entityId = buffer.readVarInt() val entityId = buffer.readVarInt()
val position = buffer.readVec3d()!! val position = buffer.readVec3d()
val velocityPosition = buffer.readVec3d()!! val velocityPosition = buffer.readVec3d()
val yaw = buffer.readFloat() val yaw = buffer.readFloat()
val pitch = buffer.readFloat() val pitch = buffer.readFloat()
val onGround = buffer.readBoolean() val onGround = buffer.readBoolean()
@@ -107,7 +107,7 @@ internal object GamePacketsProtocolCodec {
register(0x2C, ClientboundKeepAlivePlayPacket) register(0x2C, ClientboundKeepAlivePlayPacket)
// register(0x2D, ClientboundLevelChunkUpdateWithLightPacket) // register(0x2D, ClientboundLevelChunkUpdateWithLightPacket)
register(0x2E, ClientboundLevelEventPacket) register(0x2E, ClientboundLevelEventPacket)
// register(0x2F, ClientboundParticlePacket) register(0x2F, ClientboundLevelParticlePacket)
// register(0x30, ClientboundLightUpdatePacket) // register(0x30, ClientboundLightUpdatePacket)
register(0x31, ClientboundLoginPlayPacket) register(0x31, ClientboundLoginPlayPacket)
register(0x32, ClientboundLowDiskSpaceWarningPacket) register(0x32, ClientboundLowDiskSpaceWarningPacket)
@@ -135,8 +135,8 @@ internal object GamePacketsProtocolCodec {
register(0x48, ClientboundSynchronizePlayerPositionPacket) register(0x48, ClientboundSynchronizePlayerPositionPacket)
register(0x49, ClientboundPlayerRotationPacket) register(0x49, ClientboundPlayerRotationPacket)
// register(0x4A, ClientboundRecipeBookAddPacket) // register(0x4A, ClientboundRecipeBookAddPacket)
// register(0x4B, ClientboundRecipeBookRemovePacket) register(0x4B, ClientboundRecipeBookRemovePacket)
// register(0x4C, ClientboundRecipeBookSettingsPacket) register(0x4C, ClientboundRecipeBookSettingsPacket)
register(0x4D, ClientboundRemoveEntitiesPacket) register(0x4D, ClientboundRemoveEntitiesPacket)
register(0x4E, ClientboundRemoveEntityEffectPacket) register(0x4E, ClientboundRemoveEntityEffectPacket)
register(0x4F, ClientboundResetScorePacket) register(0x4F, ClientboundResetScorePacket)
@@ -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)
@@ -22,8 +22,8 @@ public data class MinecartStep(
) )
internal fun BytesBuffer.readMinecartStep(): MinecartStep { internal fun BytesBuffer.readMinecartStep(): MinecartStep {
val position = readVec3d()!! val position = readVec3d()
val velocity = readVec3d()!! val velocity = readVec3d()
val yaw = readAngle() val yaw = readAngle()
val pitch = readAngle() val pitch = readAngle()
val weight = readFloat() val weight = readFloat()
@@ -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? { internal fun BytesBuffer.readVec3d(): Vec3d {
if (optional && !this.readBoolean()) return null
val x = this.readDouble() val x = this.readDouble()
val y = this.readDouble() val y = this.readDouble()
val z = this.readDouble() val z = this.readDouble()
return Vec3d(x, y, z) return Vec3d(x, y, z)
} }
internal fun BytesBuffer.writeVec3d(vec3d: Vec3d?, optional: Boolean = false) { internal fun BytesBuffer.writeVec3d(vec3d: Vec3d) {
if (optional) { this.writeDouble(vec3d.x)
this.writeBoolean(vec3d != null) this.writeDouble(vec3d.y)
if (vec3d == null) return this.writeDouble(vec3d.z)
}
val value = requireNotNull(vec3d) { "Vec3d cannot be null when optional is false" }
this.writeDouble(value.x)
this.writeDouble(value.y)
this.writeDouble(value.z)
} }
@@ -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)
}
@@ -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())
}
}
@@ -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 }
}
}
@@ -11,31 +11,33 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds 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 public inline val Int.ticks: Duration
get() = (this * 50).milliseconds 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 public inline val Long.ticks: Duration
get() = (this * 50).milliseconds 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 public inline val Double.ticks: Duration
get() = (this * 50.0).milliseconds 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 public inline val Duration.inWholeTicks: Long
get() = this.inWholeMilliseconds / 50 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 public inline val Duration.inTicksDouble: Double
get() = this.inWholeNanoseconds / 50_000_000.0 get() = this.inWholeNanoseconds / 50_000_000.0
@@ -10,7 +10,11 @@ package client
import cn.rtast.libmc.crypto.AuthenticationProvider import cn.rtast.libmc.crypto.AuthenticationProvider
import cn.rtast.libmc.packet.ClientboundUnknownPacket 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.serverbound.ServerboundChatMessagePacket import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundChatMessagePacket
import cn.rtast.libmc.protocol.util.generateOfflineUuid import cn.rtast.libmc.protocol.util.generateOfflineUuid
import io.ktor.client.* import io.ktor.client.*
@@ -67,21 +71,20 @@ class TestClient {
socketEngine = KtorNetworkEngine() socketEngine = KtorNetworkEngine()
} }
) )
cli.onPacket<ClientboundUnknownPacket> { // cli.onPacket<ClientboundUnknownPacket> {
println(it) // println(it)
val snapshot = chatTracker.prepareForOutgoingMessage() // val snapshot = chatTracker.prepareForOutgoingMessage()
cli.networkChannel.sendPacket( // cli.networkChannel.sendPacket(
ServerboundChatMessagePacket( // ServerboundChatMessagePacket(
"114514", Clock.System.now().toEpochMilliseconds(), // "114514", Clock.System.now().toEpochMilliseconds(),
Random.nextLong(), // Random.nextLong(),
null, snapshot.messageCount, createAcknowledgedBitSet(snapshot.lastSeenSignatures).toByteArray(), // null, snapshot.messageCount, createAcknowledgedBitSet(snapshot.lastSeenSignatures).toByteArray(),
ChatPacketUtils.computePacketChecksum(snapshot.lastSeenSignatures) // ChatPacketUtils.computePacketChecksum(snapshot.lastSeenSignatures)
) // )
) // )
} // }
cli.onPacket<ClientboundPlayerChatMessagePacket> { cli.onPacket<ClientboundPlayerChatMessagePacket> { chatTracker.onReceivePlayerChat(it.messageSignature) }
chatTracker.onReceivePlayerChat(it.messageSignature) cli.onPacket<ClientboundStepTickPacket> { println(it) }
}
cli.launch { cli.connect() } cli.launch { cli.connect() }
while (true) { while (true) {
} }