Support SNBT

This commit is contained in:
2026-09-08 12:23:55 +08:00
parent 08392f92b8
commit 74f9e6f529
22 files changed
+772 -41

No files matched your search

+10 -2
View File
@@ -1,5 +1,7 @@
# libmc # libmc
[中文](zh/README-zh.md)
A lightweight, modern Minecraft client protocol library designed for Kotlin Native & JVM. The core protocol library A lightweight, modern Minecraft client protocol library designed for Kotlin Native & JVM. The core protocol library
module relies on the following dependencies: module relies on the following dependencies:
@@ -19,11 +21,17 @@ module relies on the following dependencies:
# Get started # Get started
# Protocol
> `libmc-protocol` is current under development. It only supports the latest Minecraft version > `libmc-protocol` is current under development. It only supports the latest Minecraft version
> (Current supported Minecraft version: `26.2`, Protocol Version: `776`) > (Current supported Minecraft version: `26.2`, Protocol Version: `776`)
[Start using libmc-protocol](Get-started.md) [Start using libmc-protocol](en/Get-started.md)
# Assemble all context APIs # Assemble all context APIs
[Assemble context APIs](Assemble-context.md) [Assemble context APIs](en/Assemble-context.md)
## NBT & SNBT
[NBT & SNBT](en/NBT-SNBT.md)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+75
View File
@@ -0,0 +1,75 @@
# NBT
```kotlin
fun main() {
// build
val nbt = buildNBT {
"t_b" byte 0x01
"n" compound {
"n_s" string "STR"
"n_d" double 0.0
"n_f" float 0.1f
"n_ia" intArray intArrayOf(1, 1, 1, 1)
}
}
val buf = BytesBuffer().toNBTOutput(
NBTTag.CompoundTag(
mapOf("" to nbt)
)
).writeNBTRootCompound()
println(buf.toHexString())
// read
val levelDat: BytesBuffer = File("src/commonTest/resources/level.dat").readBytes().wrap()
val readRoot = levelDat.toNBTInput().readNBTRootCompound()
println(readRoot)
}
```
# SNBT
```kotlin
fun main() {
// parse snbt from snbt string
val raw = """{key1: 123,'key2': 'somevalue1',"key3": {subkey1: 0x1C8,"subkey2": "somevalue2"}}"""
println(snbt(raw))
// serialize snbt from NBTCompound
println(snbt(raw).toSNBT())
// build snbt
val snbt = buildSNBT {
"key1" string "TEST"
"intValue" int 1
"Count" byte 1
"Damage" int 0
}
val prettySnbt = buildSNBT(prettyPrint = true) {
"Name" string "Steve"
"Health" float 20.0f
"IsCreative" boolean true
"Pos" intArray intArrayOf(100, 64, -200)
"Custom Name" string "Alex\nWith Newline"
"Attributes" compound {
"AttackDamage" double 5.5
"MovementSpeed" float 0.1f
}
"Inventory" list {
compound {
"id" string "minecraft:diamond_sword"
"Count" byte 1
}
compound {
"id" string "minecraft:apple"
"Count" byte 16
}
}
}
println(snbt) // SNBT String
println(prettySnbt) // SNBT String
println(snbt(snbt)) // parse snbt string to NBTCompound
println(snbt(prettySnbt)) // parse snbt string to NBTCompound
}
```
+5 -1
View File
@@ -8,7 +8,7 @@
## 需要实现的API ## 需要实现的API
| 名称 | 是否必须实现 | 描述 | | 名称 | 是否必须实现 | 额外说明 |
|:-------------|:-------------|:-----------------------------------------------------------------------------------------------------------------------------------------------| |:-------------|:-------------|:-----------------------------------------------------------------------------------------------------------------------------------------------|
| TCP Socket | 是 | `protocol` 模块没有内置TCP Socket实现. [实现 TCP Socket](Impl-TCP-Socket-zh.md) | | TCP Socket | 是 | `protocol` 模块没有内置TCP Socket实现. [实现 TCP Socket](Impl-TCP-Socket-zh.md) |
| AES-128-CFB8 | 视情况而定 | 仅在登录开启了正版验证(`online-mode`)的服务器时需要, 用于加密/解密流量. [实现AES-128-CFB8](Impl-Crypto-zh.md#AES-128-CFB8) | | AES-128-CFB8 | 视情况而定 | 仅在登录开启了正版验证(`online-mode`)的服务器时需要, 用于加密/解密流量. [实现AES-128-CFB8](Impl-Crypto-zh.md#AES-128-CFB8) |
@@ -26,3 +26,7 @@
# 将实现的API组合起来 # 将实现的API组合起来
[将所有上下文API组合](Assemble-context-zh.md) [将所有上下文API组合](Assemble-context-zh.md)
# NBT & SNBT
[NBT & SNBT](../en/NBT-SNBT.md)
@@ -56,7 +56,7 @@ public class CompoundBuilder {
} }
public infix fun String.compound(block: CompoundBuilder.() -> Unit) { public infix fun String.compound(block: CompoundBuilder.() -> Unit) {
tags[this] = nbtCompound(block) tags[this] = buildNBT(block)
} }
@Suppress("FunctionName") @Suppress("FunctionName")
@@ -76,7 +76,7 @@ public class CompoundBuilder {
public fun build(): NBTTag.CompoundTag = NBTTag.CompoundTag(tags) public fun build(): NBTTag.CompoundTag = NBTTag.CompoundTag(tags)
} }
public fun nbtCompound(block: CompoundBuilder.() -> Unit): NBTTag.CompoundTag { public fun buildNBT(block: CompoundBuilder.() -> Unit): NBTTag.CompoundTag {
val builder = CompoundBuilder() val builder = CompoundBuilder()
builder.block() builder.block()
return builder.build() return builder.build()
@@ -53,7 +53,7 @@ public class NBTListBuilder(
public fun compound(block: CompoundBuilder.() -> Unit) { public fun compound(block: CompoundBuilder.() -> Unit) {
checkType(NBTType.Compound) checkType(NBTType.Compound)
elements += nbtCompound(block) elements += buildNBT(block)
} }
public fun list(elementType: NBTType, block: NBTListBuilder.() -> Unit) { public fun list(elementType: NBTType, block: NBTListBuilder.() -> Unit) {
@@ -67,7 +67,7 @@ public fun NBTInput.readCompound(): NBTTag {
return NBTTag.ListTag(NBTType.Compound, map.values.toMutableList()) return NBTTag.ListTag(NBTType.Compound, map.values.toMutableList())
} }
public fun NBTInput.readRootCompound(): NBTCompound { public fun NBTInput.readNBTRootCompound(): NBTCompound {
val rootType = NBTType.fromID(readByte().toInt()) val rootType = NBTType.fromID(readByte().toInt())
require(rootType == NBTType.Compound) { "Root tag must be TAG_Compound" } require(rootType == NBTType.Compound) { "Root tag must be TAG_Compound" }
val nameLen = readShort().toInt() and 0xFFFF val nameLen = readShort().toInt() and 0xFFFF
@@ -15,7 +15,7 @@ public sealed class NBTTag(public val type: NBTType) {
public data class FloatTag(val value: Float) : NBTTag(NBTType.Float) public data class FloatTag(val value: Float) : NBTTag(NBTType.Float)
public data class DoubleTag(val value: Double) : NBTTag(NBTType.Double) public data class DoubleTag(val value: Double) : NBTTag(NBTType.Double)
public data class StringTag(val value: String) : NBTTag(NBTType.String) public data class StringTag(val value: String) : NBTTag(NBTType.String)
public data class CompoundTag(val value: MutableMap<String, NBTTag>) : NBTTag(NBTType.Compound) public data class CompoundTag(val value: Map<String, NBTTag>) : NBTTag(NBTType.Compound)
public data class ListTag(val elementType: NBTType, val value: MutableList<NBTTag>) : NBTTag(NBTType.List) { public data class ListTag(val elementType: NBTType, val value: MutableList<NBTTag>) : NBTTag(NBTType.List) {
public val length: Int get() = value.size public val length: Int get() = value.size
} }
@@ -56,7 +56,7 @@ public fun NBTOutput.writeTagPayload(tag: NBTTag) {
} }
} }
public fun NBTOutput.writeRootNBTCompound(name: String = ""): ByteArray { public fun NBTOutput.writeNBTRootCompound(name: String = ""): ByteArray {
writeByte(NBTType.Compound.id) writeByte(NBTType.Compound.id)
writeStringTag(name) writeStringTag(name)
writeTagPayload(root) writeTagPayload(root)
@@ -0,0 +1,171 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/8
*/
package cn.rtast.libmc.snbt
@DslMarker
public annotation class SnbtDsl
@SnbtDsl
public abstract class BaseSnbtBuilder(
protected val sb: StringBuilder,
protected val prettyPrint: Boolean,
protected val indentLevel: Int,
) {
public var isFirst: Boolean = true
protected fun appendSeparator() {
if (!isFirst) {
sb.append(if (prettyPrint) ",\n" else ",")
} else {
if (prettyPrint) sb.append('\n')
isFirst = false
}
if (prettyPrint) {
sb.append(" ".repeat(indentLevel + 1))
}
}
protected inline fun buildBlock(openChar: Char, closeChar: Char, action: () -> Unit) {
sb.append(openChar)
action()
if (prettyPrint && !isFirst) {
sb.append('\n').append(" ".repeat(indentLevel))
}
sb.append(closeChar)
}
}
@SnbtDsl
public class SnbtCompoundBuilder(
sb: StringBuilder = StringBuilder(),
prettyPrint: Boolean = false,
indentLevel: Int = 0,
) : BaseSnbtBuilder(sb, prettyPrint, indentLevel) {
private fun appendKey(key: String) {
appendSeparator()
sb.append(escapeSNBTKey(key)).append(if (prettyPrint) ": " else ":")
}
public infix fun String.byte(value: Byte) {
appendKey(this); sb.append(value).append('b')
}
public infix fun String.boolean(value: Boolean) {
appendKey(this); sb.append(if (value) "1b" else "0b")
}
public infix fun String.short(value: Short) {
appendKey(this); sb.append(value).append('s')
}
public infix fun String.int(value: Int) {
appendKey(this); sb.append(value)
}
public infix fun String.long(value: Long) {
appendKey(this); sb.append(value).append('L')
}
public infix fun String.float(value: Float) {
appendKey(this); sb.append(value).append('f')
}
public infix fun String.double(value: Double) {
appendKey(this); sb.append(value).append('d')
}
public infix fun String.string(value: String) {
appendKey(this); sb.append(escapeSNBTString(value))
}
public infix fun String.byteArray(value: ByteArray) {
appendKey(this)
sb.append(value.joinToString(prefix = "[B; ", postfix = "]", separator = ", ") { "${it}b" })
}
public infix fun String.intArray(value: IntArray) {
appendKey(this)
sb.append(value.joinToString(prefix = "[I; ", postfix = "]", separator = ", ") { "$it" })
}
public infix fun String.longArray(value: LongArray) {
appendKey(this)
sb.append(value.joinToString(prefix = "[L; ", postfix = "]", separator = ", ") { "${it}L" })
}
public infix fun String.compound(block: SnbtCompoundBuilder.() -> Unit) {
appendKey(this)
SnbtCompoundBuilder(sb, prettyPrint, indentLevel + 1).buildInternal(block)
}
public infix fun String.list(block: SnbtListBuilder.() -> Unit) {
appendKey(this)
SnbtListBuilder(sb, prettyPrint, indentLevel + 1).buildInternal(block)
}
internal fun buildInternal(block: SnbtCompoundBuilder.() -> Unit): String {
buildBlock('{', '}') { this.block() }
return sb.toString()
}
}
@SnbtDsl
public class SnbtListBuilder(
sb: StringBuilder,
prettyPrint: Boolean,
indentLevel: Int,
) : BaseSnbtBuilder(sb, prettyPrint, indentLevel) {
public fun byte(value: Byte) {
appendSeparator()
sb.append(value).append('b')
}
public fun short(value: Short) {
appendSeparator()
sb.append(value).append('s')
}
public fun int(value: Int) {
appendSeparator()
sb.append(value)
}
public fun long(value: Long) {
appendSeparator()
sb.append(value).append('L')
}
public fun float(value: Float) {
appendSeparator()
sb.append(value).append('f')
}
public fun double(value: Double) {
appendSeparator()
sb.append(value).append('d')
}
public fun string(value: String) {
appendSeparator()
sb.append(escapeSNBTString(value))
}
public fun compound(block: SnbtCompoundBuilder.() -> Unit) {
appendSeparator()
SnbtCompoundBuilder(sb, prettyPrint, indentLevel + 1).buildInternal(block)
}
internal fun buildInternal(block: SnbtListBuilder.() -> Unit) = buildBlock('[', ']') { this.block() }
}
public fun buildSNBT(
prettyPrint: Boolean = false,
block: SnbtCompoundBuilder.() -> Unit,
): String {
return SnbtCompoundBuilder(prettyPrint = prettyPrint).buildInternal(block)
}
@@ -0,0 +1,324 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/8
*/
package cn.rtast.libmc.snbt
import cn.rtast.libmc.nbt.NBTTag
import cn.rtast.libmc.nbt.NBTType
public class SNBTDecoder internal constructor(private val src: String) {
private var ptr = 0
public fun parse(): NBTTag {
skipWhitespace()
val result = parseTag()
skipWhitespace()
require(ptr >= src.length) { "Unexpected trailing characters at index $ptr: '${src.substring(ptr)}'" }
return result
}
private fun parseTag(): NBTTag {
skipWhitespace()
check(ptr < src.length) { "Unexpected end of SNBT string" }
if (lookingAtOperation()) return parseOperation()
return when (src[ptr]) {
'{' -> parseCompoundTag()
'[' -> parseListOrArrayTag()
'"', '\'' -> NBTTag.StringTag(parseQuotedString())
else -> parseNumberOrStringTag()
}
}
private fun parseCompoundTag(): NBTTag.CompoundTag {
expect('{')
val map = LinkedHashMap<String, NBTTag>()
skipWhitespace()
if (peek() == '}') {
ptr++
return NBTTag.CompoundTag(map)
}
while (true) {
skipWhitespace()
val key = parseKey()
skipWhitespace()
expect(':')
val value = parseTag()
map[key] = value
skipWhitespace()
val ch = peek()
if (ch == '}') {
ptr++
break
}
if (ch == ',') {
ptr++
skipWhitespace()
if (peek() == '}') {
ptr++
break
}
} else error("Expected ',' or '}' at index $ptr, found '$ch'")
}
return NBTTag.CompoundTag(map)
}
private fun parseListOrArrayTag(): NBTTag {
expect('[')
skipWhitespace()
if (ptr + 1 < src.length && src[ptr + 1] == ';') {
val typeChar = src[ptr].uppercaseChar()
ptr += 2
return parseNativeArray(typeChar)
}
val list = ArrayList<NBTTag>()
if (peek() == ']') {
ptr++
return NBTTag.ListTag(NBTType.End, list)
}
var elementType = NBTType.End
while (true) {
val tag = parseTag()
if (elementType == NBTType.End) elementType = tag.type
list.add(tag)
skipWhitespace()
val ch = peek()
if (ch == ']') {
ptr++
break
}
if (ch == ',') {
ptr++
skipWhitespace()
if (peek() == ']') {
ptr++
break
}
} else error("Expected ',' or ']' at index $ptr, found '$ch'")
}
return NBTTag.ListTag(elementType, list)
}
private fun parseNativeArray(typeChar: Char): NBTTag {
skipWhitespace()
val rawTags = mutableListOf<NBTTag>()
if (peek() == ']') {
ptr++
return castNativeArray(typeChar, rawTags)
}
while (true) {
rawTags.add(parseTag())
skipWhitespace()
val ch = peek()
if (ch == ']') {
ptr++
break
}
if (ch == ',') {
ptr++
skipWhitespace()
if (peek() == ']') {
ptr++
break
}
} else error("Expected ',' or ']' at index $ptr, found '$ch'")
}
return castNativeArray(typeChar, rawTags)
}
private fun castNativeArray(typeChar: Char, elements: List<NBTTag>): NBTTag = when (typeChar) {
'B' -> NBTTag.ByteArrayTag(ByteArray(elements.size) { extractNumber(elements[it]).toByte() })
'I' -> NBTTag.IntArrayTag(IntArray(elements.size) { extractNumber(elements[it]).toInt() })
'L' -> NBTTag.LongArrayTag(LongArray(elements.size) { extractNumber(elements[it]).toLong() })
else -> error("Unknown array prefix: $typeChar")
}
private fun extractNumber(tag: NBTTag): Number = when (tag) {
is NBTTag.ByteTag -> tag.value
is NBTTag.ShortTag -> tag.value
is NBTTag.IntTag -> tag.value
is NBTTag.LongTag -> tag.value
is NBTTag.FloatTag -> tag.value.toInt()
is NBTTag.DoubleTag -> tag.value.toLong()
else -> error("Cannot convert $tag to array element")
}
private fun parseKey(): String {
return when (peek()) {
'"', '\'' -> parseQuotedString()
else -> parseUnquotedKey()
}
}
private fun parseUnquotedKey(): String {
val start = ptr
while (ptr < src.length && isAllowedUnquotedChar(src[ptr])) ptr++
val key = src.substring(start, ptr)
require(key.isNotEmpty()) { "Expected key at index $start" }
return key
}
private fun parseNumberOrStringTag(): NBTTag {
val start = ptr
while (ptr < src.length && isAllowedUnquotedChar(src[ptr])) ptr++
val raw = src.substring(start, ptr)
if (raw.equals("true", ignoreCase = true)) return NBTTag.ByteTag(1)
if (raw.equals("false", ignoreCase = true)) return NBTTag.ByteTag(0)
val parsedNum = tryParseNumber(raw)
if (parsedNum != null) return parsedNum
val first = raw.firstOrNull()
if (first in '0'..'9' || first == '.' || first == '+' || first == '-') error("Unquoted string cannot start with '$first': $raw")
return NBTTag.StringTag(raw)
}
private fun tryParseNumber(raw: String): NBTTag? {
val clean = raw.replace("_", "")
val lower = clean.lowercase()
if (lower.endsWith("f")) return NBTTag.FloatTag(lower.dropLast(1).toFloatOrNull() ?: return null)
if (lower.endsWith("d")) return NBTTag.DoubleTag(lower.dropLast(1).toDoubleOrNull() ?: return null)
if (clean.contains('.') || lower.contains('e')) return NBTTag.DoubleTag(clean.toDoubleOrNull() ?: return null)
var rad = 10
var digits = lower
if (digits.startsWith("0x")) {
rad = 16
digits = digits.substring(2)
} else if (digits.startsWith("0b")) {
rad = 2
digits = digits.substring(2)
}
val (numStr, suffix) = splitSuffix(digits)
return try {
val unsigned = suffix.startsWith("u") || if (suffix.startsWith("s")) false else rad != 10
val typeChar = suffix.lastOrNull() ?: if (rad == 10) 'i' else 'i'
when (typeChar) {
'b' -> NBTTag.ByteTag(if (unsigned) numStr.toUByte(rad).toByte() else numStr.toByte(rad))
's' -> NBTTag.ShortTag(if (unsigned) numStr.toUShort(rad).toShort() else numStr.toShort(rad))
'l' -> NBTTag.LongTag(if (unsigned) numStr.toULong(rad).toLong() else numStr.toLong(rad))
else -> NBTTag.IntTag(if (unsigned) numStr.toUInt(rad).toInt() else numStr.toInt(rad))
}
} catch (_: Exception) {
null
}
}
private fun splitSuffix(str: String): Pair<String, String> {
val suffixes = listOf("sb", "ub", "ss", "us", "si", "ui", "sl", "ul", "b", "s", "i", "l")
for (s in suffixes) if (str.endsWith(s)) return Pair(str.dropLast(s.length), s)
return Pair(str, "")
}
private fun parseQuotedString(): String {
val quote = src[ptr++]
val sb = StringBuilder()
while (ptr < src.length) {
val ch = src[ptr++]
if (ch == quote) return sb.toString()
if (ch == '\\') {
if (ptr >= src.length) break
when (val esc = src[ptr++]) {
'b' -> sb.append('\b')
'f' -> sb.append('\u000C')
'n' -> sb.append('\n')
'r' -> sb.append('\r')
's' -> sb.append(' ')
't' -> sb.append('\t')
'\\' -> sb.append('\\')
'\'', '"' -> sb.append(esc)
'x' -> {
val hex = src.substring(ptr, ptr + 2)
ptr += 2
sb.append(hex.toInt(16).toChar())
}
'u' -> {
val hex = src.substring(ptr, ptr + 4)
ptr += 4
sb.append(hex.toInt(16).toChar())
}
'U' -> {
val hex = src.substring(ptr, ptr + 8)
ptr += 8
val codePoint = hex.toInt(16)
sb.appendCodePoint(codePoint)
}
else -> sb.append(esc)
}
} else sb.append(ch)
}
error("Unterminated quoted string")
}
private fun lookingAtOperation(): Boolean {
val rest = src.substring(ptr)
return rest.startsWith("bool(") || rest.startsWith("uuid(")
}
private fun parseOperation(): NBTTag {
val isBool = src.substring(ptr).startsWith("bool(")
val opName = if (isBool) "bool" else "uuid"
ptr += opName.length
expect('(')
skipWhitespace()
val argTag = parseTag()
skipWhitespace()
expect(')')
return if (isBool) {
val isTrue = when (argTag) {
is NBTTag.ByteTag -> argTag.value != 0.toByte()
is NBTTag.ShortTag -> argTag.value != 0.toShort()
is NBTTag.IntTag -> argTag.value != 0
is NBTTag.LongTag -> argTag.value != 0L
is NBTTag.FloatTag -> argTag.value != 0.0f
is NBTTag.DoubleTag -> argTag.value != 0.0
else -> error("bool() requires a number or boolean argument")
}
NBTTag.ByteTag(if (isTrue) 1 else 0)
} else {
require(argTag is NBTTag.StringTag) { "uuid() argument must be a string" }
val uuidStr = argTag.value.replace("-", "")
require(uuidStr.length == 32) { "Invalid UUID string: ${argTag.value}" }
val i1 = uuidStr.substring(0, 8).toLong(16).toInt()
val i2 = uuidStr.substring(8, 16).toLong(16).toInt()
val i3 = uuidStr.substring(16, 24).toLong(16).toInt()
val i4 = uuidStr.substring(24, 32).toLong(16).toInt()
NBTTag.IntArrayTag(intArrayOf(i1, i2, i3, i4))
}
}
private fun isAllowedUnquotedChar(ch: Char): Boolean =
ch in 'a'..'z' || ch in 'A'..'Z' || ch in '0'..'9' || ch == '_' || ch == '-' || ch == '+' || ch == '.'
private fun skipWhitespace() {
while (ptr < src.length && src[ptr].isWhitespace()) ptr++
}
private fun peek(): Char = src.getOrNull(ptr) ?: '\u0000'
private fun expect(expected: Char) {
require(peek() == expected) { "Expected '$expected' at index $ptr, found '${peek()}'" }
ptr++
}
public fun StringBuilder.appendCodePoint(codePoint: Int) {
when (codePoint) {
in 0x0000..0xFFFF -> append(codePoint.toChar())
in 0x10000..0x10FFFF -> {
val cpPrime = codePoint - 0x10000
val high = ((cpPrime shr 10) + 0xD800).toChar()
val low = ((cpPrime and 0x3FF) + 0xDC00).toChar()
append(high)
append(low)
}
else -> error("Invalid Unicode code point: 0x${codePoint.toString(16)}")
}
}
}
public fun snbt(src: String): NBTTag = SNBTDecoder(src).parse()
@@ -0,0 +1,75 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/8
*/
package cn.rtast.libmc.snbt
import cn.rtast.libmc.nbt.NBTTag
public fun NBTTag.toSNBT(prettyPrint: Boolean = false, indentLevel: Int = 0): String = when (this) {
is NBTTag.ByteTag -> "${value}b"
is NBTTag.ShortTag -> "${value}s"
is NBTTag.IntTag -> "$value"
is NBTTag.LongTag -> "${value}L"
is NBTTag.FloatTag -> "${value}f"
is NBTTag.DoubleTag -> "${value}d"
is NBTTag.StringTag -> escapeSNBTString(value)
is NBTTag.ByteArrayTag -> value.joinToString(prefix = "[B; ", postfix = "]", separator = ", ") { "${it}b" }
is NBTTag.IntArrayTag -> value.joinToString(prefix = "[I; ", postfix = "]", separator = ", ") { "$it" }
is NBTTag.LongArrayTag -> value.joinToString(prefix = "[L; ", postfix = "]", separator = ", ") { "${it}L" }
is NBTTag.ListTag -> {
if (value.isEmpty()) "[]" else if (!prettyPrint) {
value.joinToString(prefix = "[", postfix = "]", separator = ",") { it.toSNBT(false) }
} else {
val indent = " ".repeat(indentLevel + 1)
val closingIndent = " ".repeat(indentLevel)
val body = value.joinToString(separator = ",\n") { "$indent${it.toSNBT(true, indentLevel + 1)}" }
"[\n$body\n$closingIndent]"
}
}
is NBTTag.CompoundTag -> {
if (value.isEmpty()) "{}" else if (!prettyPrint) {
value.entries.joinToString(prefix = "{", postfix = "}", separator = ",") { (k, v) ->
"${escapeSNBTKey(k)}:${v.toSNBT(false)}"
}
} else {
val indent = " ".repeat(indentLevel + 1)
val closingIndent = " ".repeat(indentLevel)
val body = value.entries.joinToString(separator = ",\n") { (k, v) ->
"$indent${escapeSNBTKey(k)}: ${v.toSNBT(true, indentLevel + 1)}"
}
"{\n$body\n$closingIndent}"
}
}
}
internal fun escapeSNBTKey(key: String): String {
if (key.isEmpty()) return "\"\""
val first = key.first()
val isValidFirst = first in 'a'..'z' || first in 'A'..'Z' || first == '_'
val isValidBody =
key.all { it in 'a'..'z' || it in 'A'..'Z' || it in '0'..'9' || it == '_' || it == '-' || it == '+' || it == '.' }
return if (isValidFirst && isValidBody) key else escapeSNBTString(key)
}
internal fun escapeSNBTString(str: String): String {
val sb = StringBuilder("\"")
for (ch in str) {
when (ch) {
'\\' -> sb.append("\\\\")
'"' -> sb.append("\\\"")
'\b' -> sb.append("\\b")
'\u000C' -> sb.append("\\f")
'\n' -> sb.append("\\n")
'\r' -> sb.append("\\r")
'\t' -> sb.append("\\t")
else -> if (ch.code < 0x20) sb.append("\\x${ch.code.toString(16).padStart(2, '0')}") else sb.append(ch)
}
}
sb.append("\"")
return sb.toString()
}
@@ -0,0 +1,36 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/8
*/
package test
import cn.rtast.libmc.nbt.NBTTag
import cn.rtast.libmc.nbt.buildNBT
import cn.rtast.libmc.nbt.toNBTOutput
import cn.rtast.libmc.nbt.writeNBTRootCompound
import cn.rtast.libmc.network.BytesBuffer
import kotlin.test.Test
class TestNBT {
@Test
fun `test nbt`() {
val nbt = buildNBT {
"t_b" byte 0x01
"n" compound {
"n_s" string "STR"
"n_d" double 0.0
"n_f" float 0.1f
"n_ia" intArray intArrayOf(1, 1, 1, 1)
}
}
val buf = BytesBuffer().toNBTOutput(
NBTTag.CompoundTag(
mapOf("" to nbt)
)
).writeNBTRootCompound()
println(buf.toHexString())
}
}
@@ -0,0 +1,64 @@
/*
* Copyright © 2026 RTAkland
* Author: RTAkland
* Date: 2026/9/8
*/
package test
import cn.rtast.libmc.snbt.SNBTDecoder
import cn.rtast.libmc.snbt.buildSNBT
import cn.rtast.libmc.snbt.snbt
import cn.rtast.libmc.snbt.toSNBT
import kotlin.test.Test
class TestSNBT {
val raw = """{key1: 123,'key2': 'somevalue1',"key3": {subkey1: 0x1C8,"subkey2": "somevalue2"}}"""
@Test
fun `test snbt decode`() {
println(snbt(raw))
}
@Test
fun `test snbt encode`() {
println(snbt(raw).toSNBT())
}
@Test
fun `test snbt builder`() {
val snbt = buildSNBT {
"key1" string "TEST"
"intValue" int 1
"Count" byte 1
"Damage" int 0
}
val prettySnbt = buildSNBT(prettyPrint = true) {
"Name" string "Steve"
"Health" float 20.0f
"IsCreative" boolean true
"Pos" intArray intArrayOf(100, 64, -200)
"Custom Name" string "Alex\nWith Newline"
"Attributes" compound {
"AttackDamage" double 5.5
"MovementSpeed" float 0.1f
}
"Inventory" list {
compound {
"id" string "minecraft:diamond_sword"
"Count" byte 1
}
compound {
"id" string "minecraft:apple"
"Count" byte 16
}
}
}
println(snbt)
println(prettySnbt)
println(snbt(snbt))
println(snbt(prettySnbt))
}
}
@@ -7,8 +7,9 @@
package test package test
import cn.rtast.libmc.nbt.readNBTRootCompound
import cn.rtast.libmc.nbt.toNBTInput
import cn.rtast.libmc.network.wrap import cn.rtast.libmc.network.wrap
import cn.rtast.libmc.nbt.NbtReader
import org.junit.Test import org.junit.Test
import java.io.File import java.io.File
@@ -18,7 +19,7 @@ class TestNBTReader {
@Test @Test
fun `test read java nbt`() { fun `test read java nbt`() {
val readRoot = NbtReader(javaNBTBuffer).readRoot() val readRoot = javaNBTBuffer.toNBTInput().readNBTRootCompound()
println(readRoot) println(readRoot)
} }
} }
@@ -11,7 +11,7 @@ import cn.rtast.libmc.network.BytesBuffer
import cn.rtast.libmc.nbt.* import cn.rtast.libmc.nbt.*
internal fun BytesBuffer.readNBTCompound(): NBTCompound = internal fun BytesBuffer.readNBTCompound(): NBTCompound =
this.toNBTInput().readRootCompound() this.toNBTInput().readNBTRootCompound()
internal fun BytesBuffer.readNetworkNBTCompound(): NBTCompound = internal fun BytesBuffer.readNetworkNBTCompound(): NBTCompound =
this.toNBTInput().readNetworkCompound() this.toNBTInput().readNetworkCompound()
-27
View File
@@ -1,27 +0,0 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
kotlin {
explicitApi()
withSourcesJar()
linuxX64()
linuxArm64()
macosArm64()
mingwX64()
jvm { compilerOptions.jvmTarget = JvmTarget.JVM_1_8 }
sourceSets {
commonMain.dependencies {
api(project(":common"))
}
jvmMain.dependencies {
}
commonTest.dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.coroutines.test)
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ includeSubModule("common")
includeSubModule("protocol") includeSubModule("protocol")
includeSubModule("protocol-context") includeSubModule("protocol-context")
includeSubModule("nbt") includeSubModule("nbt")
includeSubModule("snbt") //includeSubModule("snbt")
//includeSubModule("protocol-engine-netty", path = "libmc-network-engines/netty") //includeSubModule("protocol-engine-netty", path = "libmc-network-engines/netty")
//includeSubModule("protocol-engine-ktor-network", path = "libmc-network-engines/ktor-network") //includeSubModule("protocol-engine-ktor-network", path = "libmc-network-engines/ktor-network")