Fix serverbound client command;

Implemented 0x85-ClientboundUpdateRecipesPacket
0x82-ClientboundUpdateAdvancementsPacket
0x6d-ClientboundSetPlayerTeamPacket
0x6c-ClientboundSetPlayerInventorySlotPacket
0x66-ClientboundSetEquipmentPacket
0x60-ClientboundSetCursorItemPacket
0x4a-ClientboundRecipeBookAddPacket
0x3f-ClientboundPlaceGhostRecipePacket
0x34-ClientboundMerchantOffersPacket
0x30-ClientboundLightUpdatePacket
This commit is contained in:
2026-09-11 15:52:22 +08:00
parent 3cca64d9c1
commit e47a0bff04
35 files changed
+895 -87

No files matched your search

@@ -0,0 +1,93 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
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.readPrefixOptional
import cn.rtast.libmc.primitives.readPrefixed
import cn.rtast.libmc.primitives.readVarInt
import cn.rtast.libmc.protocol.protocol.game.data.component.DataComponent
import cn.rtast.libmc.protocol.protocol.game.entity.VillagerLevel
import cn.rtast.libmc.protocol.protocol.game.item.slot.Slot
import cn.rtast.libmc.protocol.protocol.game.item.slot.readSlot
import cn.rtast.libmc.protocol.registry.readDataComponent
public data class ClientboundMerchantOffersPacket(
val windowId: Int,
val trades: List<MerchantTrade>,
val villagerLevel: VillagerLevel,
val experience: Int,
val isRegularVillager: Boolean,
val canRestock: Boolean,
) : MinecraftPacket {
public data class MerchantTrade(
val inputItem1: TradeItem,
val outputItem: Slot,
val inputItem2: TradeItem?,
val tradable: Boolean,
val tradeUsed: Int,
val maxTradeUsed: Int,
val xp: Int,
val specialPrice: Int,
val priceMultiplier: Float,
val demand: Int,
)
public data class TradeItem(
val itemId: Int,
val itemCount: Int,
val components: Map<Int, DataComponent>,
)
internal companion object Codec : PacketCodec<ClientboundMerchantOffersPacket> {
private fun BytesBuffer.readTradeItem(): TradeItem {
val itemId = readVarInt()
val itemCount = readVarInt()
val componentCount = readVarInt()
val components = HashMap<Int, DataComponent>(componentCount)
repeat(componentCount) {
val typeId = readVarInt()
val componentData = readDataComponent(typeId)
components[typeId] = componentData
}
return TradeItem(itemId, itemCount, components)
}
override fun encode(buffer: BytesBuffer, value: ClientboundMerchantOffersPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundMerchantOffersPacket {
val windowId = buffer.readVarInt()
val trades = buffer.readPrefixed {
val inputItem1 = readTradeItem()
val outputItem = readSlot()
val inputItem2 = readPrefixOptional { readTradeItem() }
val tradable = !readBoolean() // reverse
val tradeUsed = readInt()
val maxTradeUsed = readInt()
val xp = readInt()
val specialPrice = readInt()
val priceMultiplier = readFloat()
val demand = readInt()
MerchantTrade(
inputItem1, outputItem, inputItem2, tradable,
tradeUsed, maxTradeUsed, xp, specialPrice,
priceMultiplier, demand,
)
}
val villagerLevel = VillagerLevel.fromLevel(buffer.readVarInt())
val experience = buffer.readVarInt()
val isRegularVillager = buffer.readBoolean()
val canRestock = buffer.readBoolean()
return ClientboundMerchantOffersPacket(
windowId, trades, villagerLevel,
experience, isRegularVillager, canRestock
)
}
}
}
@@ -0,0 +1,27 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
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.recipe.RecipeDisplay
import cn.rtast.libmc.protocol.protocol.game.recipe.readRecipeDisplay
public data class ClientboundPlaceGhostRecipePacket(val windowId: Int, val recipeDisplay: RecipeDisplay) :
MinecraftPacket {
internal companion object Codec : PacketCodec<ClientboundPlaceGhostRecipePacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundPlaceGhostRecipePacket) {}
override fun decode(buffer: BytesBuffer): ClientboundPlaceGhostRecipePacket {
val windowId = buffer.readVarInt()
val recipeDisplay = buffer.readRecipeDisplay()
return ClientboundPlaceGhostRecipePacket(windowId, recipeDisplay)
}
}
}
@@ -0,0 +1,38 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
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.readIdSet
import cn.rtast.libmc.primitives.readPrefixOptional
import cn.rtast.libmc.primitives.readPrefixed
import cn.rtast.libmc.primitives.readVarInt
import cn.rtast.libmc.protocol.protocol.game.recipe.RecipeEntry
import cn.rtast.libmc.protocol.protocol.game.recipe.readRecipeDisplay
public data class ClientboundRecipeBookAddPacket(val recipes: List<RecipeEntry>, val replace: Boolean) :
MinecraftPacket {
internal companion object Codec : PacketCodec<ClientboundRecipeBookAddPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundRecipeBookAddPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundRecipeBookAddPacket {
val recipes = buffer.readPrefixed {
val recipeId = readVarInt()
val display = readRecipeDisplay()
val groupId = readVarInt()
val categoryId = readVarInt()
val ingredients = readPrefixOptional { readPrefixed { readIdSet() } }
val flags = readByte()
RecipeEntry(recipeId, display, groupId, categoryId, ingredients, flags)
}
val replace = buffer.readBoolean()
return ClientboundRecipeBookAddPacket(recipes, replace)
}
}
}
@@ -0,0 +1,22 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
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.protocol.protocol.game.item.slot.Slot
import cn.rtast.libmc.protocol.protocol.game.item.slot.readSlot
public data class ClientboundSetCursorItemPacket(val carriedItem: Slot) : MinecraftPacket {
internal companion object Codec : PacketCodec<ClientboundSetCursorItemPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundSetCursorItemPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundSetCursorItemPacket =
ClientboundSetCursorItemPacket(buffer.readSlot())
}
}
@@ -0,0 +1,37 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
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.inventory.equipment.EntityEquipment
import cn.rtast.libmc.protocol.protocol.game.inventory.equipment.EntityEquipmentSlot
import cn.rtast.libmc.protocol.protocol.game.item.slot.readSlot
public data class ClientboundSetEquipmentPacket(val entityId: Int, val equipments: List<EntityEquipment>) :
MinecraftPacket {
internal companion object Codec : PacketCodec<ClientboundSetEquipmentPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundSetEquipmentPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundSetEquipmentPacket {
val entityId = buffer.readVarInt()
val equipments = ArrayList<EntityEquipment>()
while (true) {
val rawByte = buffer.readByte().toInt()
val hasNext = (rawByte and 0x80) != 0
val slotId = rawByte and 0x7F
val slot = EntityEquipmentSlot.fromID(slotId)
val item = buffer.readSlot()
equipments.add(EntityEquipment(slot, item))
if (!hasNext) break
}
return ClientboundSetEquipmentPacket(entityId, equipments)
}
}
}
@@ -0,0 +1,26 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
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.item.slot.Slot
import cn.rtast.libmc.protocol.protocol.game.item.slot.readSlot
public data class ClientboundSetPlayerInventorySlotPacket(val slot: Int, val slotData: Slot) : MinecraftPacket {
internal companion object Codec : PacketCodec<ClientboundSetPlayerInventorySlotPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundSetPlayerInventorySlotPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundSetPlayerInventorySlotPacket {
val slot = buffer.readVarInt()
val slotData = buffer.readSlot()
return ClientboundSetPlayerInventorySlotPacket(slot, slotData)
}
}
}
@@ -0,0 +1,57 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
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.readMcString
import cn.rtast.libmc.primitives.readPrefixed
import cn.rtast.libmc.primitives.readVarInt
import cn.rtast.libmc.protocol.protocol.game.chat.readTextComponent
import cn.rtast.libmc.protocol.protocol.game.team.CollisionRule
import cn.rtast.libmc.protocol.protocol.game.team.NameTagVisibility
import cn.rtast.libmc.protocol.protocol.game.team.TeamAction
public data class ClientboundSetPlayerTeamPacket(val teamName: String, val action: TeamAction) : MinecraftPacket {
internal companion object Codec : PacketCodec<ClientboundSetPlayerTeamPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundSetPlayerTeamPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundSetPlayerTeamPacket {
val teamName = buffer.readMcString()
val action = when (val teamAction = buffer.readByte().toInt()) {
0 -> {
val info = buffer.decodeTeamInfo()
val entities = buffer.readPrefixed { readMcString() }
TeamAction.CreateTeam(info, entities)
}
1 -> TeamAction.RemoveTeam
2 -> TeamAction.UpdateTeamInfo(buffer.decodeTeamInfo())
3 -> TeamAction.AddEntities(buffer.readPrefixed { readMcString() })
4 -> TeamAction.RemoveEntities(buffer.readPrefixed { readMcString() })
else -> error("Unknown team action method id $teamAction")
}
return ClientboundSetPlayerTeamPacket(teamName, action)
}
private fun BytesBuffer.decodeTeamInfo(): TeamAction.TeamInfo {
val displayName = readTextComponent()
val prefix = readTextComponent()
val suffix = readTextComponent()
val nameTagVisibility = NameTagVisibility.fromID(readVarInt())
val collisionRule = CollisionRule.fromID(readVarInt())
val teamColor = readVarInt()
val friendlyFlags = readByte()
return TeamAction.TeamInfo(
displayName, prefix, suffix,
nameTagVisibility, collisionRule,
teamColor, friendlyFlags
)
}
}
}
@@ -0,0 +1,64 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
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.readPrefixOptional
import cn.rtast.libmc.primitives.readPrefixed
import cn.rtast.libmc.primitives.readVarInt
import cn.rtast.libmc.protocol.protocol.game.Identifier
import cn.rtast.libmc.protocol.protocol.game.advancement.Advancement
import cn.rtast.libmc.protocol.protocol.game.advancement.AdvancementProgress
import cn.rtast.libmc.protocol.protocol.game.advancement.readAdvancement
import cn.rtast.libmc.protocol.protocol.game.readIdentifier
public data class ClientboundUpdateAdvancementsPacket(
val reset: Boolean,
val advancementMapping: Map<Identifier, Advancement>,
/**
* The identifiers of the advancements that should be removed
*/
val identifiers: List<Identifier>,
val progressMapping: Map<Identifier, AdvancementProgress>,
val showAdvancements: Boolean,
) : MinecraftPacket {
internal companion object Codec : PacketCodec<ClientboundUpdateAdvancementsPacket> {
override fun encode(buffer: BytesBuffer, value: ClientboundUpdateAdvancementsPacket) {}
override fun decode(buffer: BytesBuffer): ClientboundUpdateAdvancementsPacket {
val reset = buffer.readBoolean()
val mappingCount = buffer.readVarInt()
val advancementMapping = HashMap<Identifier, Advancement>(mappingCount)
repeat(mappingCount) {
val key = buffer.readIdentifier()
val advancement = buffer.readAdvancement()
advancementMapping[key] = advancement
}
val identifiers = buffer.readPrefixed { readIdentifier() }
val progressMappingCount = buffer.readVarInt()
val progressMapping = HashMap<Identifier, AdvancementProgress>(progressMappingCount)
repeat(progressMappingCount) {
val key = buffer.readIdentifier()
val criteriaCount = buffer.readVarInt()
val criteria = HashMap<Identifier, Long?>(criteriaCount)
repeat(criteriaCount) {
val k = buffer.readIdentifier()
val date = buffer.readPrefixOptional { readLong() }
criteria[k] = date
}
progressMapping[key] = AdvancementProgress(criteria)
}
val showAdvancements = buffer.readBoolean()
return ClientboundUpdateAdvancementsPacket(
reset, advancementMapping, identifiers,
progressMapping, showAdvancements
)
}
}
}
@@ -1,25 +0,0 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/5
*/
package cn.rtast.libmc.protocol.packet.play.serverbound
import cn.rtast.libmc.network.BytesBuffer
import cn.rtast.libmc.packet.PacketCodec
import cn.rtast.libmc.packet.MinecraftPacket
import cn.rtast.libmc.primitives.writeVarInt
import cn.rtast.libmc.protocol.protocol.game.registry.ClientAction
public data class ServerboundClientActionPacket(val action: ClientAction) : MinecraftPacket {
internal companion object Codec : PacketCodec<ServerboundClientActionPacket> {
override fun encode(buffer: BytesBuffer, value: ServerboundClientActionPacket) {
buffer.writeVarInt(value.action.actionID)
}
override fun decode(buffer: BytesBuffer): ServerboundClientActionPacket =
throw UnsupportedOperationException()
}
}
@@ -8,12 +8,12 @@
package cn.rtast.libmc.protocol.packet.play.serverbound
import cn.rtast.libmc.network.BytesBuffer
import cn.rtast.libmc.packet.PacketCodec
import cn.rtast.libmc.packet.MinecraftPacket
import cn.rtast.libmc.packet.PacketCodec
import cn.rtast.libmc.primitives.writeVarInt
import cn.rtast.libmc.protocol.protocol.game.player.action.PlayerActionStatus
import cn.rtast.libmc.protocol.protocol.game.registry.ClientAction
public data class ServerboundClientCommandPacket(val action: PlayerActionStatus) : MinecraftPacket {
public data class ServerboundClientCommandPacket(val action: ClientAction) : MinecraftPacket {
internal companion object Codec : PacketCodec<ServerboundClientCommandPacket> {
override fun encode(buffer: BytesBuffer, value: ServerboundClientCommandPacket) {
buffer.writeVarInt(value.action.id)
@@ -23,9 +23,9 @@ public value class Identifier internal constructor(public val raw: String) {
override fun toString(): String = "$namespace:$path"
internal companion object Codec : PacketCodec<Identifier> {
fun of(namespace: String, path: String): Identifier = Identifier("$namespace:$path")
fun of(full: String): Identifier = Identifier(full)
public companion object Codec : PacketCodec<Identifier> {
public fun of(namespace: String, path: String): Identifier = Identifier("$namespace:$path")
public fun of(full: String): Identifier = Identifier(full)
override fun encode(buffer: BytesBuffer, value: Identifier) {
buffer.writeMcString(value.toString())
@@ -0,0 +1,44 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.protocol.game.advancement
import cn.rtast.libmc.network.BytesBuffer
import cn.rtast.libmc.primitives.readMcString
import cn.rtast.libmc.primitives.readPrefixOptional
import cn.rtast.libmc.primitives.readPrefixed
import cn.rtast.libmc.primitives.readVarInt
import cn.rtast.libmc.protocol.protocol.game.Identifier
import cn.rtast.libmc.protocol.protocol.game.chat.readTextComponent
import cn.rtast.libmc.protocol.protocol.game.item.slot.readSlot
import cn.rtast.libmc.protocol.protocol.game.readIdentifier
public data class Advancement(
val parentId: Identifier?,
val displayData: AdvancementDisplay?,
val requirements: List<List<String>>,
val sendsTelemetryData: Boolean,
)
internal fun BytesBuffer.readAdvancement(): Advancement {
val parentId = readPrefixOptional { readIdentifier() }
val displayData = readPrefixOptional {
val title = readTextComponent()
val description = readTextComponent()
val icon = readSlot()
val frameType = AdvancementFrameType.fromID(readVarInt())
val flags = readInt()
val hasBackground = (flags and 0x01) != 0
val backgroundTexture = if (hasBackground) readIdentifier() else null
val x = readFloat()
val y = readFloat()
AdvancementDisplay(title, description, icon, frameType, flags, backgroundTexture, x, y)
}
val requirements = readPrefixed { readPrefixed { readMcString() } }
val sendsTelemetryData = readBoolean()
return Advancement(parentId, displayData, requirements, sendsTelemetryData)
}
@@ -0,0 +1,27 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.protocol.game.advancement
import cn.rtast.libmc.protocol.protocol.game.Identifier
import cn.rtast.libmc.protocol.protocol.game.chat.TextComponent
import cn.rtast.libmc.protocol.protocol.game.item.slot.Slot
public data class AdvancementDisplay(
val title: TextComponent,
val description: TextComponent,
val icon: Slot,
val frameType: AdvancementFrameType,
val flags: Int,
val backgroundTexture: Identifier?,
val x: Float,
val y: Float
) {
public val hasBackgroundTexture: Boolean get() = (flags and 0x01) != 0
public val showToast: Boolean get() = (flags and 0x02) != 0
public val isHidden: Boolean get() = (flags and 0x04) != 0
}
@@ -0,0 +1,16 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.protocol.game.advancement
public enum class AdvancementFrameType(public val id: Int) {
TASK(0), CHALLENGE(1), GOAL(2);
public companion object {
public fun fromID(id: Int): AdvancementFrameType = entries.first { it.id == id }
}
}
@@ -0,0 +1,12 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.protocol.game.advancement
import cn.rtast.libmc.protocol.protocol.game.Identifier
public data class AdvancementProgress(val criteria: Map<Identifier, Long?>)
@@ -0,0 +1,16 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.protocol.game.entity
public enum class VillagerLevel(public val level: Int) {
Novice(1), Apprentice(2), Journeyman(3), Expert(4), Master(5);
public companion object {
public fun fromLevel(level: Int): VillagerLevel = entries.first { it.level == level }
}
}
@@ -0,0 +1,12 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.protocol.game.inventory.equipment
import cn.rtast.libmc.protocol.protocol.game.item.slot.Slot
public data class EntityEquipment(val slot: EntityEquipmentSlot, val item: Slot)
@@ -0,0 +1,23 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.protocol.game.inventory.equipment
public enum class EntityEquipmentSlot(public val id: Int) {
MAIN_HAND(0),
OFF_HAND(1),
BOOTS(2),
LEGGINGS(3),
CHESTPLATE(4),
HELMET(5),
BODY(6),
SADDLE(7);
public companion object {
public fun fromID(id: Int): EntityEquipmentSlot = entries.firstOrNull { it.id == id } ?: MAIN_HAND
}
}
@@ -8,7 +8,6 @@
package cn.rtast.libmc.protocol.protocol.game.item.slot
import cn.rtast.libmc.network.BytesBuffer
import cn.rtast.libmc.packet.EmptyPacketCodec
import cn.rtast.libmc.packet.PacketCodec
import cn.rtast.libmc.primitives.readVarInt
import cn.rtast.libmc.primitives.writePrefixed
@@ -18,8 +17,16 @@ import cn.rtast.libmc.protocol.protocol.game.readIdentifier
import cn.rtast.libmc.protocol.protocol.game.writeIdentifier
public sealed interface SlotDisplayData {
public data object Empty : SlotDisplayData, EmptyPacketCodec<Empty>(Empty)
public data object AnyFuel : SlotDisplayData, EmptyPacketCodec<Empty>(Empty)
public data object Empty : SlotDisplayData, PacketCodec<Empty> {
override fun encode(buffer: BytesBuffer, value: Empty) {}
override fun decode(buffer: BytesBuffer): Empty = Empty
}
public data object AnyFuel : SlotDisplayData, PacketCodec<AnyFuel> {
override fun encode(buffer: BytesBuffer, value: AnyFuel) {}
override fun decode(buffer: BytesBuffer): AnyFuel = AnyFuel
}
public data class WithAnyPotion(val base: SlotDisplay) : SlotDisplayData {
internal companion object Codec : PacketCodec<WithAnyPotion> {
override fun encode(buffer: BytesBuffer, value: WithAnyPotion) {
@@ -65,13 +72,13 @@ public sealed interface SlotDisplayData {
}
}
public data class Tags(val tag: Identifier) : SlotDisplayData {
internal companion object Codec : PacketCodec<Tags> {
override fun encode(buffer: BytesBuffer, value: Tags) {
public data class Tag(val tag: Identifier) : SlotDisplayData {
internal companion object Codec : PacketCodec<Tag> {
override fun encode(buffer: BytesBuffer, value: Tag) {
buffer.writeIdentifier(value.tag)
}
override fun decode(buffer: BytesBuffer): Tags = Tags(buffer.readIdentifier())
override fun decode(buffer: BytesBuffer): Tag = Tag(buffer.readIdentifier())
}
}
@@ -79,6 +86,7 @@ public sealed interface SlotDisplayData {
internal companion object Codec : PacketCodec<Dyed> {
override fun encode(buffer: BytesBuffer, value: Dyed) {
buffer.writeSlotDisplay(value.dye)
buffer.writeSlotDisplay(value.target)
}
override fun decode(buffer: BytesBuffer): Dyed {
@@ -0,0 +1,27 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.protocol.game.recipe
import cn.rtast.libmc.network.BytesBuffer
import cn.rtast.libmc.primitives.readVarInt
import cn.rtast.libmc.primitives.writeVarInt
import cn.rtast.libmc.protocol.registry.readRecipeDisplayData
import cn.rtast.libmc.protocol.registry.writeRecipeDisplayData
public data class RecipeDisplay(val displayType: Int, val data: RecipeDisplayData)
internal fun BytesBuffer.readRecipeDisplay(): RecipeDisplay {
val displayType = readVarInt()
val data = readRecipeDisplayData(displayType)
return RecipeDisplay(displayType, data)
}
internal fun BytesBuffer.writeRecipeDisplay(display: RecipeDisplay) {
writeVarInt(display.displayType)
writeRecipeDisplayData(display.data)
}
@@ -0,0 +1,154 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.protocol.game.recipe
import cn.rtast.libmc.network.BytesBuffer
import cn.rtast.libmc.packet.PacketCodec
import cn.rtast.libmc.primitives.readVarInt
import cn.rtast.libmc.primitives.writePrefixed
import cn.rtast.libmc.primitives.writeVarInt
import cn.rtast.libmc.protocol.protocol.game.item.slot.SlotDisplay
import cn.rtast.libmc.protocol.protocol.game.item.slot.readSlotDisplay
import cn.rtast.libmc.protocol.protocol.game.item.slot.writeSlotDisplay
public sealed interface RecipeDisplayData {
public val result: SlotDisplay
public val craftingStation: SlotDisplay
public data class CraftingShapeless(
val ingredientCount: Int,
val ingredients: List<SlotDisplay>,
override val result: SlotDisplay,
override val craftingStation: SlotDisplay,
) : RecipeDisplayData {
internal companion object Codec : PacketCodec<CraftingShapeless> {
override fun encode(buffer: BytesBuffer, value: CraftingShapeless) {
buffer.writePrefixed(value.ingredients) { writeSlotDisplay(it) }
buffer.writeSlotDisplay(value.result)
buffer.writeSlotDisplay(value.craftingStation)
}
override fun decode(buffer: BytesBuffer): CraftingShapeless {
val ingredientCount = buffer.readVarInt()
val ingredients = ArrayList<SlotDisplay>(ingredientCount)
repeat(ingredientCount) { ingredients.add(buffer.readSlotDisplay()) }
val result = buffer.readSlotDisplay()
val craftingStation = buffer.readSlotDisplay()
return CraftingShapeless(ingredientCount, ingredients, result, craftingStation)
}
}
}
public data class CraftingShaped(
val width: Int,
val height: Int,
val ingredientCount: Int,
val ingredients: List<SlotDisplay>,
override val result: SlotDisplay,
override val craftingStation: SlotDisplay,
) : RecipeDisplayData {
internal companion object Codec : PacketCodec<CraftingShaped> {
override fun encode(buffer: BytesBuffer, value: CraftingShaped) {
buffer.writeVarInt(value.width)
buffer.writeVarInt(value.height)
buffer.writePrefixed(value.ingredients) { writeSlotDisplay(it) }
buffer.writeSlotDisplay(value.result)
buffer.writeSlotDisplay(value.craftingStation)
}
override fun decode(buffer: BytesBuffer): CraftingShaped {
val width = buffer.readVarInt()
val height = buffer.readVarInt()
val ingredientCount = buffer.readVarInt()
val ingredients = ArrayList<SlotDisplay>(ingredientCount)
repeat(ingredientCount) { ingredients.add(buffer.readSlotDisplay()) }
val result = buffer.readSlotDisplay()
val craftingStation = buffer.readSlotDisplay()
return CraftingShaped(width, height, ingredientCount, ingredients, result, craftingStation)
}
}
}
public data class Furnace(
val ingredient: SlotDisplay,
val fuel: SlotDisplay,
override val result: SlotDisplay,
override val craftingStation: SlotDisplay,
val cookingTime: Int, // ticks
val experience: Float,
) : RecipeDisplayData {
internal companion object Codec : PacketCodec<Furnace> {
override fun encode(buffer: BytesBuffer, value: Furnace) {
buffer.writeSlotDisplay(value.ingredient)
buffer.writeSlotDisplay(value.fuel)
buffer.writeSlotDisplay(value.result)
buffer.writeSlotDisplay(value.craftingStation)
buffer.writeVarInt(value.cookingTime)
buffer.writeFloat(value.experience)
}
override fun decode(buffer: BytesBuffer): Furnace {
val ingredient = buffer.readSlotDisplay()
val fuel = buffer.readSlotDisplay()
val result = buffer.readSlotDisplay()
val craftingStation = buffer.readSlotDisplay()
val cookingTime = buffer.readVarInt()
val experience = buffer.readFloat()
return Furnace(ingredient, fuel, result, craftingStation, cookingTime, experience)
}
}
}
public data class Stonecutter(
val ingredient: SlotDisplay,
override val result: SlotDisplay,
override val craftingStation: SlotDisplay,
) : RecipeDisplayData {
internal companion object Codec : PacketCodec<Stonecutter> {
override fun encode(buffer: BytesBuffer, value: Stonecutter) {
buffer.writeSlotDisplay(value.ingredient)
buffer.writeSlotDisplay(value.result)
buffer.writeSlotDisplay(value.craftingStation)
}
override fun decode(buffer: BytesBuffer): Stonecutter {
val ingredient = buffer.readSlotDisplay()
val result = buffer.readSlotDisplay()
val craftingStation = buffer.readSlotDisplay()
return Stonecutter(ingredient, result, craftingStation)
}
}
}
public data class Smithing(
val template: SlotDisplay,
val base: SlotDisplay,
val addition: SlotDisplay,
override val result: SlotDisplay,
override val craftingStation: SlotDisplay,
) : RecipeDisplayData {
internal companion object Codec : PacketCodec<Smithing> {
override fun encode(buffer: BytesBuffer, value: Smithing) {
buffer.writeSlotDisplay(value.template)
buffer.writeSlotDisplay(value.base)
buffer.writeSlotDisplay(value.addition)
buffer.writeSlotDisplay(value.result)
buffer.writeSlotDisplay(value.craftingStation)
}
override fun decode(buffer: BytesBuffer): Smithing {
val template = buffer.readSlotDisplay()
val base = buffer.readSlotDisplay()
val addition = buffer.readSlotDisplay()
val result = buffer.readSlotDisplay()
val craftingStation = buffer.readSlotDisplay()
return Smithing(template, base, addition, result, craftingStation)
}
}
}
}
@@ -0,0 +1,22 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.protocol.game.recipe
import cn.rtast.libmc.primitives.IdSet
public data class RecipeEntry(
val recipeId: Int,
val display: RecipeDisplay,
val groupId: Int,
val categoryId: Int,
val ingredients: List<IdSet>?,
val flags: Byte
) {
val showNotification: Boolean get() = (flags.toInt() and 0x01) != 0
val highlightAsNew: Boolean get() = (flags.toInt() and 0x02) != 0
}
@@ -7,7 +7,7 @@
package cn.rtast.libmc.protocol.protocol.game.registry
public enum class ClientAction(public val actionID: Int) {
public enum class ClientAction(public val id: Int) {
/**
* Sent when the client is ready to respawn after death.
*/
@@ -24,6 +24,6 @@ public enum class ClientAction(public val actionID: Int) {
RequestGameRuleValues(2);
public companion object {
public fun fromID(id: Int): ClientAction = entries.first { it.actionID == id }
public fun fromID(id: Int): ClientAction = entries.first { it.id == id }
}
}
@@ -0,0 +1,19 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.protocol.game.team
public enum class CollisionRule(public val id: Int) {
ALWAYS(0),
NEVER(1),
PUSH_OTHER_TEAMS(2),
PUSH_OWN_TEAM(3);
public companion object {
public fun fromID(id: Int): CollisionRule = entries.firstOrNull { it.id == id } ?: ALWAYS
}
}
@@ -0,0 +1,19 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.protocol.game.team
public enum class NameTagVisibility(public val id: Int) {
ALWAYS(0),
NEVER(1),
HIDE_FOR_OTHER_TEAMS(2),
HIDE_FOR_OWN_TEAMS(3);
public companion object {
public fun fromID(id: Int): NameTagVisibility = entries.first { it.id == id }
}
}
@@ -0,0 +1,50 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.protocol.game.team
import cn.rtast.libmc.protocol.protocol.game.chat.TextComponent
public sealed interface TeamAction {
public val methodId: Int
public data class CreateTeam(
val teamInfo: TeamInfo,
val entities: List<String>,
) : TeamAction {
override val methodId: Int = 0
}
public data object RemoveTeam : TeamAction {
override val methodId: Int = 1
}
public data class UpdateTeamInfo(val teamInfo: TeamInfo) : TeamAction {
override val methodId: Int = 2
}
public data class AddEntities(val entities: List<String>) : TeamAction {
override val methodId: Int = 3
}
public data class RemoveEntities(val entities: List<String>) : TeamAction {
override val methodId: Int = 4
}
public data class TeamInfo(
val displayName: TextComponent,
val prefix: TextComponent,
val suffix: TextComponent,
val nameTagVisibility: NameTagVisibility,
val collisionRule: CollisionRule,
val teamColor: Int,
val friendlyFlags: Byte,
) {
public val allowFriendlyFire: Boolean get() = (friendlyFlags.toInt() and 0x01) != 0
public val canSeeFriendlyInvisible: Boolean get() = (friendlyFlags.toInt() and 0x02) != 0
}
}
@@ -7,6 +7,7 @@
package cn.rtast.libmc.protocol.protocol.session
import cn.rtast.libmc.packet.MinecraftPacket
import cn.rtast.libmc.protocol.client.CURRENT_MINECRAFT_PROTOCOL_VERSION
import cn.rtast.libmc.protocol.protocol.state.HandshakeIntent
import kotlin.reflect.KClass
@@ -24,6 +25,7 @@ public interface Session {
public suspend fun status(protocolVersion: Int = CURRENT_MINECRAFT_PROTOCOL_VERSION): String
public suspend fun disconnect()
public suspend fun init()
public suspend fun sendPacket(packet: MinecraftPacket)
}
public inline fun <reified T : SessionEvent> Session.onEvent(noinline block: suspend Session.(T) -> Unit) {
@@ -1,5 +1,6 @@
package cn.rtast.libmc.protocol.protocol.session
import cn.rtast.libmc.packet.MinecraftPacket
import cn.rtast.libmc.protocol.client.MinecraftClient
import cn.rtast.libmc.protocol.crypto.minecraftServerIdHash
import cn.rtast.libmc.protocol.crypto.rsaEncrypt
@@ -76,6 +77,8 @@ public class SessionImpl internal constructor() : Session {
}
}
public override suspend fun sendPacket(packet: MinecraftPacket): Unit = client.networkChannel.sendPacket(packet)
override fun <T : SessionEvent> _onEvent(clazz: KClass<T>, block: suspend Session.(T) -> Unit) {
@Suppress("UNCHECKED_CAST")
this.eventListener.getOrPut(clazz) { mutableListOf() }.add { block(it as T) }
@@ -112,7 +112,7 @@ internal object GamePacketsProtocolRegistry {
register(0x31, ClientboundLoginPlayPacket)
register(0x32, ClientboundLowDiskSpaceWarningPacket)
register(0x33, ClientboundMapItemDataPacket)
// register(0x34, ClientboundMerchantOffersPacket)
register(0x34, ClientboundMerchantOffersPacket)
register(0x35, ClientboundUpdateEntityPositionPacket)
register(0x36, ClientboundUpdateEntityPositionAndRotationPacket)
register(0x37, ClientboundMoveMinecartAlongTrackPacket)
@@ -123,7 +123,7 @@ internal object GamePacketsProtocolRegistry {
register(0x3C, ClientboundOpenSignEditorPacket)
register(0x3D, ClientboundPingPacket)
register(0x3E, ClientboundPongResponsePacket)
// register(0x3F, ClientboundPlaceGhostRecipePacket)
register(0x3F, ClientboundPlaceGhostRecipePacket)
register(0x40, ClientboundPlayerAbilitiesPacket)
register(0x41, ClientboundPlayerChatMessagePacket)
register(0x42, ClientboundPlayerEndCombatPacket)
@@ -134,7 +134,7 @@ internal object GamePacketsProtocolRegistry {
register(0x47, ClientboundPlayerLookAtPacket)
register(0x48, ClientboundSynchronizePlayerPositionPacket)
register(0x49, ClientboundPlayerRotationPacket)
// register(0x4A, ClientboundRecipeBookAddPacket)
register(0x4A, ClientboundRecipeBookAddPacket)
register(0x4B, ClientboundRecipeBookRemovePacket)
register(0x4C, ClientboundRecipeBookSettingsPacket)
register(0x4D, ClientboundRemoveEntitiesPacket)
@@ -156,20 +156,20 @@ internal object GamePacketsProtocolRegistry {
register(0x5D, ClientboundSetCameraPacket)
register(0x5E, ClientboundSetCenterChunkPacket)
register(0x5F, ClientboundSetRenderDistancePacket)
// register(0x60, ClientboundSetCursorItemPacket)
register(0x60, ClientboundSetCursorItemPacket)
register(0x61, ClientboundSetDefaultSpawnPositionPacket)
register(0x62, ClientboundSetDisplayObjectivePacket)
// register(0x63, ClientboundSetEntityMetadataPacket)
register(0x64, ClientboundLinkEntitiesPacket)
register(0x65, ClientboundSetEntityVelocityPacket)
// register(0x66, ClientboundSetEquipmentPacket)
register(0x66, ClientboundSetEquipmentPacket)
register(0x67, ClientboundSetExperiencePacket)
register(0x68, ClientboundSetHealthPacket)
register(0x69, ClientboundSetCarriedItemPacket)
register(0x6A, ClientboundUpdateObjectivePacket)
register(0x6B, ClientboundSetPassengersPacket)
// register(0x6C, ClientboundSetPlayerInventorySlotPacket)
// register(0x6D, ClientboundSetPlayerTeamPacket)
register(0x6C, ClientboundSetPlayerInventorySlotPacket)
register(0x6D, ClientboundSetPlayerTeamPacket)
register(0x6E, ClientboundUpdateScorePacket)
register(0x6F, ClientboundSetSimulationDistancePacket)
register(0x70, ClientboundSetSubtitleTextPacket)
@@ -190,7 +190,7 @@ internal object GamePacketsProtocolRegistry {
register(0x7F, ClientboundSetTickingStatePacket)
register(0x80, ClientboundStepTickPacket)
register(0x81, ClientboundTransferPacket)
// register(0x82, ClientboundUpdateAdvancementsPacket)
register(0x82, ClientboundUpdateAdvancementsPacket)
register(0x83, ClientboundUpdateAttributesPacket)
register(0x84, ClientboundEntityEffectPacket)
register(0x85, ClientboundUpdateRecipesPacket)
@@ -0,0 +1,26 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/11
*/
package cn.rtast.libmc.protocol.registry
import cn.rtast.libmc.network.BytesBuffer
import cn.rtast.libmc.protocol.protocol.game.recipe.RecipeDisplayData
internal object RecipeDisplayDataRegistry : ArrayIndexedRegistry<RecipeDisplayData>("RecipeDisplayDataRegistry") {
init {
register(RecipeDisplayData.CraftingShapeless)
register(RecipeDisplayData.CraftingShaped)
register(RecipeDisplayData.Furnace)
register(RecipeDisplayData.Stonecutter)
register(RecipeDisplayData.Smithing)
freeze()
}
}
internal fun BytesBuffer.readRecipeDisplayData(typeId: Int) = RecipeDisplayDataRegistry.read(typeId, this)
internal fun BytesBuffer.writeRecipeDisplayData(data: RecipeDisplayData) = RecipeDisplayDataRegistry.write(this, data)
@@ -10,12 +10,10 @@ package cn.rtast.libmc.protocol.registry
import cn.rtast.libmc.network.BytesBuffer
import cn.rtast.libmc.packet.PacketCodec
import cn.rtast.libmc.primitives.writeVarInt
import kotlin.concurrent.Volatile
import kotlin.reflect.KClass
internal interface Registry<T : Any> {
fun freeze()
fun isFrozen(): Boolean
fun getCodec(id: Int): PacketCodec<out T>?
fun getId(kClass: KClass<out T>): Int?
}
@@ -23,13 +21,9 @@ internal interface Registry<T : Any> {
internal open class ArrayIndexedRegistry<T : Any>(private val registryName: String = "Registry") : Registry<T> {
private val codecMap = mutableMapOf<Int, PacketCodec<out T>>()
private val classToIdMap = mutableMapOf<KClass<out T>, Int>()
@Volatile
private var frozen = false
private lateinit var codecArray: Array<PacketCodec<T>>
fun <U : T> register(kClass: KClass<U>, codec: PacketCodec<U>) {
check(!frozen) { "$registryName is frozen and cannot accept new registrations." }
val id = codecMap.size
codecMap[id] = codec
classToIdMap[kClass] = id
@@ -38,20 +32,15 @@ internal open class ArrayIndexedRegistry<T : Any>(private val registryName: Stri
inline fun <reified U : T> register(codec: PacketCodec<U>) = register(U::class, codec)
override fun freeze() {
if (frozen) return
val maxId = codecMap.keys.maxOrNull() ?: -1
codecArray = Array(maxId + 1) { index ->
@Suppress("UNCHECKED_CAST")
codecMap[index] as? PacketCodec<T> ?: error("[$registryName] Missing codec for ID: $index")
}
codecMap.clear()
frozen = true
}
override fun isFrozen(): Boolean = frozen
override fun getCodec(id: Int): PacketCodec<T>? {
check(frozen) { "$registryName is not frozen yet!" }
return codecArray.getOrNull(id)
}
@@ -18,7 +18,7 @@ internal object SlotDisplayDataRegistry : ArrayIndexedRegistry<SlotDisplayData>(
register(SlotDisplayData.OnlyWithComponent)
register(SlotDisplayData.Item)
register(SlotDisplayData.ItemStack)
register(SlotDisplayData.Tags)
register(SlotDisplayData.Tag)
register(SlotDisplayData.Dyed)
register(SlotDisplayData.SmithingTrim)
register(SlotDisplayData.WithRemainder)
@@ -9,13 +9,14 @@ package client
import cn.rtast.libmc.crypto.AuthenticationProvider
import cn.rtast.libmc.protocol.client.createMinecraftClient
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundContainerSetSlotPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundExplodePacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundLevelParticlePacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundLightUpdatePacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundAwardStatisticsPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundPlayerChatMessagePacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundSetCursorItemPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundSetHealthPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundStepTickPacket
import cn.rtast.libmc.protocol.packet.play.clientbound.ClientboundUpdateRecipesPacket
import cn.rtast.libmc.protocol.packet.play.serverbound.ServerboundClientCommandPacket
import cn.rtast.libmc.protocol.packet.status.clientbound.ClientboundStatusResponsePacket
import cn.rtast.libmc.protocol.protocol.game.registry.ClientAction
import cn.rtast.libmc.protocol.protocol.session.SessionEvent
import cn.rtast.libmc.protocol.protocol.session.onEvent
import cn.rtast.libmc.protocol.util.generateOfflineUuid
@@ -104,7 +105,10 @@ class TestClient {
println(it)
}
cli.onPacket<ClientboundPlayerChatMessagePacket> { chatTracker.onReceivePlayerChat(it.messageSignature) }
cli.onPacket<ClientboundUpdateRecipesPacket> { println(it) }
cli.onPacket<ClientboundSetHealthPacket> {
if (it.health <= 0) sendPacket(ServerboundClientCommandPacket(ClientAction.PerformRespawn))
}
cli.onPacket<ClientboundSetCursorItemPacket> { println(it) }
// cli.on { packet, direction -> println("$direction -> $packet") }
cli.connect()