Support SNBT
This commit is contained in:
22 files changed
+772
-41
No files matched your search
+10
-2
@@ -1,5 +1,7 @@
|
||||
# libmc
|
||||
|
||||
[中文](zh/README-zh.md)
|
||||
|
||||
A lightweight, modern Minecraft client protocol library designed for Kotlin Native & JVM. The core protocol library
|
||||
module relies on the following dependencies:
|
||||
|
||||
@@ -19,11 +21,17 @@ module relies on the following dependencies:
|
||||
|
||||
# Get started
|
||||
|
||||
# Protocol
|
||||
|
||||
> `libmc-protocol` is current under development. It only supports the latest Minecraft version
|
||||
> (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 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.
@@ -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
|
||||
}
|
||||
```
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## 需要实现的API
|
||||
|
||||
| 名称 | 是否必须实现 | 描述 |
|
||||
| 名称 | 是否必须实现 | 额外说明 |
|
||||
|:-------------|:-------------|:-----------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 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) |
|
||||
@@ -26,3 +26,7 @@
|
||||
# 将实现的API组合起来
|
||||
|
||||
[将所有上下文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) {
|
||||
tags[this] = nbtCompound(block)
|
||||
tags[this] = buildNBT(block)
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
@@ -76,7 +76,7 @@ public class CompoundBuilder {
|
||||
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()
|
||||
builder.block()
|
||||
return builder.build()
|
||||
|
||||
@@ -53,7 +53,7 @@ public class NBTListBuilder(
|
||||
|
||||
public fun compound(block: CompoundBuilder.() -> Unit) {
|
||||
checkType(NBTType.Compound)
|
||||
elements += nbtCompound(block)
|
||||
elements += buildNBT(block)
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
public fun NBTInput.readRootCompound(): NBTCompound {
|
||||
public fun NBTInput.readNBTRootCompound(): NBTCompound {
|
||||
val rootType = NBTType.fromID(readByte().toInt())
|
||||
require(rootType == NBTType.Compound) { "Root tag must be TAG_Compound" }
|
||||
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 DoubleTag(val value: Double) : NBTTag(NBTType.Double)
|
||||
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 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)
|
||||
writeStringTag(name)
|
||||
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
|
||||
|
||||
import cn.rtast.libmc.nbt.readNBTRootCompound
|
||||
import cn.rtast.libmc.nbt.toNBTInput
|
||||
import cn.rtast.libmc.network.wrap
|
||||
import cn.rtast.libmc.nbt.NbtReader
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
|
||||
@@ -18,7 +19,7 @@ class TestNBTReader {
|
||||
|
||||
@Test
|
||||
fun `test read java nbt`() {
|
||||
val readRoot = NbtReader(javaNBTBuffer).readRoot()
|
||||
val readRoot = javaNBTBuffer.toNBTInput().readNBTRootCompound()
|
||||
println(readRoot)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import cn.rtast.libmc.network.BytesBuffer
|
||||
import cn.rtast.libmc.nbt.*
|
||||
|
||||
internal fun BytesBuffer.readNBTCompound(): NBTCompound =
|
||||
this.toNBTInput().readRootCompound()
|
||||
this.toNBTInput().readNBTRootCompound()
|
||||
|
||||
internal fun BytesBuffer.readNetworkNBTCompound(): NBTCompound =
|
||||
this.toNBTInput().readNetworkCompound()
|
||||
|
||||
@@ -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
@@ -8,7 +8,7 @@ includeSubModule("common")
|
||||
includeSubModule("protocol")
|
||||
includeSubModule("protocol-context")
|
||||
includeSubModule("nbt")
|
||||
includeSubModule("snbt")
|
||||
//includeSubModule("snbt")
|
||||
|
||||
//includeSubModule("protocol-engine-netty", path = "libmc-network-engines/netty")
|
||||
//includeSubModule("protocol-engine-ktor-network", path = "libmc-network-engines/ktor-network")
|
||||
|
||||
Reference in New Issue
Block a user