Support SNBT
This commit is contained in:
22 files changed
+772
-41
No files matched your search
@@ -0,0 +1,31 @@
|
||||
# Assemble context API
|
||||
|
||||
```kotlin
|
||||
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
|
||||
rsaEncryptor = RSA1024Encryptor { key, data -> ... }
|
||||
sha1Hasher = Sha1Hasher { serverId, secretKey, publicKey -> ... }
|
||||
cipherFactory = { key -> ... }
|
||||
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> ... }
|
||||
socketEngine = MyCustomTCPSocketImpl()
|
||||
}
|
||||
```
|
||||
|
||||
> If using `DefaultProtocolContext` from `libmc-protocol-context` as the default context implementation,
|
||||
> please refer to [Implementing TCPSocket](Impl-TCP-Socket.md)
|
||||
|
||||
# Modify exists context API implementation
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
val myNewProtocolContext = DefaultProtocolContext.withCustom {
|
||||
rsaEncryptor = RSA1024Encryptor { key, data -> ... }
|
||||
sha1Hasher = Sha1Hasher { serverId, secretKey, publicKey -> ... }
|
||||
cipherFactory = { key -> ... }
|
||||
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash -> ... }
|
||||
socketEngine = MyCustomTCPSocketImpl()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Call `.withCustom` on a `ProtocolContextBuilder` to modify exists context implementation
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Creating a Client
|
||||
|
||||
```kotlin
|
||||
public fun main() = runBlocking {
|
||||
val client = createMinecraftClient(
|
||||
"127.0.0.1", 25566, "MyBot",
|
||||
generateOfflineUuid("MyBot"),
|
||||
accessToken = null,
|
||||
context = DefaultProtocolContext.withCustom {
|
||||
socketEngine = KtorNetworkEngine()
|
||||
}
|
||||
)
|
||||
client.launch { client.connect() }
|
||||
while (true) {
|
||||
delay(5.seconds)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> In the example code above, a `MinecraftClient` is created. This client will connect to an offline server at
|
||||
> `127.0.0.1:25565` using `MyBot` as the player name, and replaces the underlying TCP Socket
|
||||
> implementation with a `ktor-network` based TCP Socket. (For details on how to create a SocketEngine, please refer
|
||||
> to [Implementing TCP Socket](Impl-TCP-Socket.md). For details on how to
|
||||
> create a Context, please refer to [Required APIs](README.md#required-apis))
|
||||
> MinecraftClient implements CoroutineScope, and calling `client.connect()` will execute the connection on a background
|
||||
> thread. Blocking thread to prevent the application from exiting
|
||||
|
||||
## Connecting to an Online-Mode Server
|
||||
|
||||
```kotlin
|
||||
private val accessToken = "eyJraWQiOiIw..."
|
||||
|
||||
public fun main() = runBlocking {
|
||||
val client = createMinecraftClient(
|
||||
// Other parameters
|
||||
username = "RTAkland",
|
||||
uuid = Uuid.parse("bb033844-e68e-4909-a636-1a5d1821ddc4"),
|
||||
accessToken = accessToken,
|
||||
// Other parameters
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
> In the example code above, the client will connect to the server using `RTAkland` as the player name.
|
||||
|
||||
# Get an AccessToken
|
||||
|
||||
Open [minecraft.net](https://minecraft.net), log in, press `F12`, and run the following code in the Console:
|
||||
|
||||
```javascript
|
||||
console.log(`; ${document.cookie}`.split('; bearer_token=').pop().split(';').shift())
|
||||
```
|
||||
|
||||
> Note: AccessTokens are valid for 24 hours
|
||||
|
||||
# Listening for Packets
|
||||
|
||||
```kotlin
|
||||
// Listen for specific received packets (must start with Clientbound)
|
||||
client.onPacket<ClientboundLoginSuccessPacket> {
|
||||
println(it)
|
||||
}
|
||||
|
||||
// Listen for all received packets
|
||||
client.on { packet, direction ->
|
||||
println("$direction ->$packet")
|
||||
}
|
||||
|
||||
// Listen for outgoing packets (must start with Serverbound)
|
||||
cli.onSent<ServerboundPongConfigurationPacket> {
|
||||
println(it)
|
||||
}
|
||||
```
|
||||
|
||||
# Sending Packets
|
||||
|
||||
```kotlin
|
||||
// Only packets starting with Serverbound can be sent, otherwise an UnsupportedOperationException will be thrown
|
||||
client.networkChannel.sendPacket(
|
||||
ServerboundChatCommandPacket(command = "say Hello from libmc")
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,67 @@
|
||||
# Before start
|
||||
|
||||
`AES-128-CFB8`, `RSA1024` and `SHA1` has been implemented in `libmc-protocol-context` module, it uses
|
||||
`cryptography-kotlin`(and its platform based provider)
|
||||
|
||||
> All impl example below are based on Java's built-in security & cryptography API
|
||||
|
||||
# AES-128-CFB8
|
||||
|
||||
```kotlin
|
||||
class JavaAesCipher(sharedKey: ByteArray) : NetworkCipher {
|
||||
private val encryptCipher = Cipher.getInstance("AES/CFB8/NoPadding").apply {
|
||||
init(Cipher.ENCRYPT_MODE, SecretKeySpec(sharedKey, "AES"), IvParameterSpec(sharedKey))
|
||||
}
|
||||
|
||||
private val decryptCipher = Cipher.getInstance("AES/CFB8/NoPadding").apply {
|
||||
init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedKey, "AES"), IvParameterSpec(sharedKey))
|
||||
}
|
||||
|
||||
override fun encrypt(buffer: ByteArray, offset: Int, length: Int) {
|
||||
encryptCipher.update(buffer, offset, length, buffer, offset)
|
||||
}
|
||||
|
||||
override fun decrypt(buffer: ByteArray, offset: Int, length: Int) {
|
||||
decryptCipher.update(buffer, offset, length, buffer, offset)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# RSA1024
|
||||
|
||||
```kotlin
|
||||
private fun encrypt(publicKeyBytes: ByteArray, data: ByteArray): ByteArray {
|
||||
val keySpec = X509EncodedKeySpec(publicKeyBytes)
|
||||
val keyFactory = KeyFactory.getInstance("RSA")
|
||||
val publicKey = keyFactory.generatePublic(keySpec)
|
||||
val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
|
||||
return cipher.doFinal(data)
|
||||
}
|
||||
|
||||
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
|
||||
rsaEncryptor = RSA1024Encryptor { key: ByteArray, data: ByteArray -> encrypt(key, data) }
|
||||
}
|
||||
```
|
||||
|
||||
# SHA1
|
||||
|
||||
```kotlin
|
||||
private fun minecraftServerIdHash(serverId: String, secretKey: ByteArray, publicKey: ByteArray): String {
|
||||
val serverIdBytes: ByteArray = serverId.encodeToByteArray()
|
||||
for (b in serverIdBytes) require((b.toInt() and 0xFF) <= 0x7F) { "serverId contains non-US-ASCII character" }
|
||||
val data = ByteArray(serverIdBytes.size + secretKey.size + publicKey.size)
|
||||
System.arraycopy(serverIdBytes, 0, data, 0, serverIdBytes.size)
|
||||
System.arraycopy(secretKey, 0, data, serverIdBytes.size, secretKey.size)
|
||||
System.arraycopy(publicKey, 0, data, serverIdBytes.size + secretKey.size, publicKey.size)
|
||||
val digest = MessageDigest.getInstance("SHA-1")
|
||||
val hash = digest.digest(data)
|
||||
return BigInteger(hash).toString(16)
|
||||
}
|
||||
|
||||
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
|
||||
sha1Hasher = Sha1Hasher { serverId: String, secretKey: ByteArray, publicKey: ByteArray ->
|
||||
minecraftServerIdHash(serverId, secretKey, publicKey)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
# Ktor based http client
|
||||
|
||||
```kotlin
|
||||
private val httpClient = HttpClient()
|
||||
|
||||
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
|
||||
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash ->
|
||||
val status = httpClient.post(url) {
|
||||
headers { header("Content-Type", "application/json") }
|
||||
setBody("{\"accessToken\":\"$accessToken\", \"selectedProfile\":\"$uuid\", \"serverId\":\"$serverIdHash\"}")
|
||||
}.status
|
||||
require(status == HttpStatusCode.NoContent)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# HttpURLConnection based http client
|
||||
|
||||
```kotlin
|
||||
private fun sendJoinRequest(url: String, accessToken: String, uuid: String, serverIdHash: String): Boolean {
|
||||
val connection = (URL(url).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "POST"
|
||||
setRequestProperty("Content-Type", "application/json")
|
||||
doOutput = true
|
||||
connectTimeout = 5000
|
||||
readTimeout = 5000
|
||||
}
|
||||
val jsonPayload = """{"accessToken":"$accessToken","selectedProfile":"$uuid","serverId":"$serverIdHash"}"""
|
||||
connection.outputStream.use { os -> os.write(jsonPayload.toByteArray(Charsets.UTF_8)) }
|
||||
val responseCode = connection.responseCode
|
||||
connection.disconnect()
|
||||
return responseCode == HttpURLConnection.HTTP_NO_CONTENT
|
||||
}
|
||||
|
||||
public val CustomProtocolContext: ProtocolContextBuilder.() -> Unit = {
|
||||
authProvider = AuthenticationProvider { url, accessToken, uuid, serverIdHash ->
|
||||
val success = sendJoinRequest(url, accessToken, uuid, serverIdHash)
|
||||
require(success) { "Failed to authenticate with Mojang session server" }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,329 @@
|
||||
# Before started
|
||||
|
||||
If the TCP Socket library is non-suspending, **DO NOT** wrap I/O read and write operations with `withContext`.
|
||||
It will cause frequent thread context switching during network I/O, leading to a degradation in application
|
||||
performance
|
||||
|
||||
# Netty based TCP Socket implementation
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("io.netty:netty-transport:4.2.17.Final")
|
||||
implementation("io.netty:netty-buffer:4.2.17.Final")
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Click to expand code</summary>
|
||||
|
||||
```kotlin
|
||||
import cn.rtast.libmc.network.RawSocket
|
||||
import cn.rtast.libmc.network.ReadChannel
|
||||
import cn.rtast.libmc.network.SocketEngine
|
||||
import cn.rtast.libmc.network.WriteChannel
|
||||
import io.netty.bootstrap.Bootstrap
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.Unpooled
|
||||
import io.netty.channel.*
|
||||
import io.netty.channel.nio.NioIoHandler
|
||||
import io.netty.channel.socket.SocketChannel
|
||||
import io.netty.channel.socket.nio.NioSocketChannel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import io.netty.channel.Channel as NettyChannel
|
||||
import kotlinx.coroutines.channels.Channel as KotlinChannel
|
||||
|
||||
public class NettyNetworkSocketEngine : SocketEngine {
|
||||
override fun create(host: String, port: Int): RawSocket = NettyNetworkSocket(host, port)
|
||||
|
||||
private class NettyNetworkSocket(private val host: String, private val port: Int) : RawSocket {
|
||||
private val workerGroup: EventLoopGroup = MultiThreadIoEventLoopGroup(0, NioIoHandler.newFactory())
|
||||
private lateinit var channel: NettyChannel
|
||||
private val inboundQueue = KotlinChannel<ByteArray>(KotlinChannel.UNLIMITED)
|
||||
private lateinit var readChannel: NettyReadChannel
|
||||
private lateinit var writeChannel: NettyWriteChannel
|
||||
|
||||
override suspend fun connect() {
|
||||
val bootstrap = Bootstrap()
|
||||
.group(workerGroup)
|
||||
.channel(NioSocketChannel::class.java)
|
||||
.option(ChannelOption.TCP_NODELAY, true)
|
||||
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
|
||||
.handler(object : ChannelInitializer<SocketChannel>() {
|
||||
override fun initChannel(ch: SocketChannel) {
|
||||
ch.pipeline().addLast(object : ChannelInboundHandlerAdapter() {
|
||||
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||
if (msg is ByteBuf) {
|
||||
try {
|
||||
val bytes = ByteArray(msg.readableBytes())
|
||||
msg.readBytes(bytes)
|
||||
inboundQueue.trySend(bytes)
|
||||
} finally {
|
||||
msg.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
|
||||
inboundQueue.close(cause)
|
||||
ctx.close()
|
||||
}
|
||||
|
||||
override fun channelInactive(ctx: ChannelHandlerContext) {
|
||||
inboundQueue.close()
|
||||
super.channelInactive(ctx)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
channel = withContext(Dispatchers.IO) {
|
||||
bootstrap.connect(host, port).suspendAwait()
|
||||
}
|
||||
|
||||
readChannel = NettyReadChannel(inboundQueue)
|
||||
writeChannel = NettyWriteChannel(channel)
|
||||
}
|
||||
|
||||
override fun openReadChannel(): ReadChannel = readChannel
|
||||
override fun openWriteChannel(): WriteChannel = writeChannel
|
||||
|
||||
override fun close() {
|
||||
if (::channel.isInitialized && channel.isOpen) {
|
||||
channel.close()
|
||||
}
|
||||
inboundQueue.close()
|
||||
workerGroup.shutdownGracefully()
|
||||
}
|
||||
}
|
||||
|
||||
private class NettyReadChannel(private val inboundQueue: KotlinChannel<ByteArray>) : ReadChannel {
|
||||
private var currentChunk: ByteArray? = null
|
||||
private var chunkOffset = 0
|
||||
|
||||
private suspend fun fetchNextChunk() {
|
||||
val next = inboundQueue.receiveCatching().getOrNull()
|
||||
?: throw IllegalStateException("Socket/Channel closed while reading")
|
||||
currentChunk = next
|
||||
chunkOffset = 0
|
||||
}
|
||||
|
||||
override suspend fun readByte(): Byte {
|
||||
while (currentChunk == null || chunkOffset >= currentChunk!!.size) {
|
||||
fetchNextChunk()
|
||||
}
|
||||
val chunk = currentChunk!!
|
||||
return chunk[chunkOffset++]
|
||||
}
|
||||
|
||||
override suspend fun readBytes(length: Int): ByteArray {
|
||||
val result = ByteArray(length)
|
||||
readFully(result, 0, length)
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun readFully(out: ByteArray, start: Int, end: Int) {
|
||||
var written = start
|
||||
|
||||
while (written < end) {
|
||||
while (currentChunk == null || chunkOffset >= currentChunk!!.size) {
|
||||
fetchNextChunk()
|
||||
}
|
||||
val chunk = currentChunk!!
|
||||
val available = chunk.size - chunkOffset
|
||||
val toCopy = minOf(available, end - written)
|
||||
|
||||
chunk.copyInto(
|
||||
out,
|
||||
destinationOffset = written,
|
||||
startIndex = chunkOffset,
|
||||
endIndex = chunkOffset + toCopy
|
||||
)
|
||||
chunkOffset += toCopy
|
||||
written += toCopy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NettyWriteChannel(private val channel: NettyChannel) : WriteChannel {
|
||||
private val writeMutex = Mutex()
|
||||
|
||||
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
|
||||
val length = endIndex - startIndex
|
||||
if (length <= 0) return
|
||||
|
||||
val nettyBuf = Unpooled.copiedBuffer(value, startIndex, length)
|
||||
|
||||
writeMutex.withLock {
|
||||
withContext(Dispatchers.IO) {
|
||||
channel.writeAndFlush(nettyBuf).suspendAwait()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun flush() {
|
||||
writeMutex.withLock {
|
||||
withContext(Dispatchers.IO) {
|
||||
channel.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private suspend inline fun ChannelFuture.suspendAwait(): NettyChannel = suspendCancellableCoroutine { cont ->
|
||||
if (isDone) {
|
||||
if (isSuccess) cont.resume(channel())
|
||||
else cont.resumeWithException(cause() ?: RuntimeException("Netty operation failed"))
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
|
||||
addListener { future ->
|
||||
if (future.isSuccess) {
|
||||
cont.resume(channel())
|
||||
} else {
|
||||
cont.resumeWithException(future.cause() ?: RuntimeException("Netty operation failed"))
|
||||
}
|
||||
}
|
||||
cont.invokeOnCancellation { cancel(false) }
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
> Also copy the imports
|
||||
|
||||
</details>
|
||||
|
||||
# ktor-network based TCP Socket implementation
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("io.ktor:ktor-network:3.5.2")
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
|
||||
```kotlin
|
||||
public class KtorNetworkEngine : SocketEngine {
|
||||
override fun create(host: String, port: Int): RawSocket = KtorNetworkSocket(host, port)
|
||||
}
|
||||
|
||||
public class KtorNetworkSocket(private val host: String, private val port: Int) : RawSocket {
|
||||
private val sm = SelectorManager(Dispatchers.IO)
|
||||
private lateinit var socket: Socket
|
||||
|
||||
override suspend fun connect(): Unit = run { socket = aSocket(sm).tcp().connect(host, port) }
|
||||
override fun openReadChannel(): ReadChannel = KtorReadChannel(socket.openReadChannel())
|
||||
override fun openWriteChannel(): WriteChannel = KtorWriteChannel(socket.openWriteChannel())
|
||||
override fun close() {
|
||||
socket.close()
|
||||
sm.close()
|
||||
}
|
||||
}
|
||||
|
||||
public class KtorReadChannel(private val readChannel: ByteReadChannel) : ReadChannel {
|
||||
override suspend fun readByte(): Byte = readChannel.readByte()
|
||||
override suspend fun readBytes(length: Int): ByteArray = readChannel.readByteArray(length)
|
||||
override suspend fun readFully(out: ByteArray, start: Int, end: Int): Unit =
|
||||
readChannel.readFully(out, start, end)
|
||||
}
|
||||
|
||||
public class KtorWriteChannel(private val writeChannel: ByteWriteChannel) : WriteChannel {
|
||||
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
|
||||
writeChannel.writeFully(value, startIndex, endIndex)
|
||||
}
|
||||
|
||||
override suspend fun flush(): Unit = writeChannel.flush()
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
# Java built-in Socket based implementation
|
||||
|
||||
<details>
|
||||
<summary>Click to expand code</summary>
|
||||
|
||||
```kotlin
|
||||
class JavaSocketEngine : SocketEngine {
|
||||
override fun create(host: String, port: Int): RawSocket = JavaNetworkSocket(host, port)
|
||||
}
|
||||
|
||||
class JavaNetworkSocket(
|
||||
private val host: String,
|
||||
private val port: Int,
|
||||
private val connectTimeoutMs: Int = 10000,
|
||||
) : RawSocket {
|
||||
private lateinit var socket: Socket
|
||||
private lateinit var readChannel: JavaReadChannel
|
||||
private lateinit var writeChannel: JavaWriteChannel
|
||||
|
||||
override suspend fun connect() {
|
||||
val s = Socket()
|
||||
s.tcpNoDelay = true
|
||||
s.connect(InetSocketAddress(host, port), connectTimeoutMs)
|
||||
|
||||
socket = s
|
||||
readChannel = JavaReadChannel(s.getInputStream())
|
||||
writeChannel = JavaWriteChannel(s.getOutputStream())
|
||||
}
|
||||
|
||||
override fun openReadChannel(): ReadChannel = readChannel
|
||||
override fun openWriteChannel(): WriteChannel = writeChannel
|
||||
|
||||
override fun close() {
|
||||
if (::socket.isInitialized && !socket.isClosed) {
|
||||
runCatching { socket.close() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class JavaReadChannel(private val inputStream: InputStream) : ReadChannel {
|
||||
override suspend fun readByte(): Byte {
|
||||
val b = inputStream.read()
|
||||
if (b == -1) throw IllegalStateException("Socket stream reached EOF while reading byte")
|
||||
return b.toByte()
|
||||
}
|
||||
|
||||
override suspend fun readBytes(length: Int): ByteArray {
|
||||
val buffer = ByteArray(length)
|
||||
readFullyInternal(buffer, 0, length)
|
||||
return buffer
|
||||
}
|
||||
|
||||
override suspend fun readFully(out: ByteArray, start: Int, end: Int) {
|
||||
readFullyInternal(out, start, end - start)
|
||||
}
|
||||
|
||||
private fun readFullyInternal(out: ByteArray, offset: Int, length: Int) {
|
||||
var bytesRead = 0
|
||||
while (bytesRead < length) {
|
||||
val count = inputStream.read(out, offset + bytesRead, length - bytesRead)
|
||||
if (count == -1) {
|
||||
throw IllegalStateException("Socket stream closed unexpectedly (read $bytesRead of $length bytes)")
|
||||
}
|
||||
bytesRead += count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class JavaWriteChannel(private val outputStream: OutputStream) : WriteChannel {
|
||||
override suspend fun writeFully(value: ByteArray, startIndex: Int, endIndex: Int) {
|
||||
outputStream.write(value, startIndex, endIndex - startIndex)
|
||||
}
|
||||
|
||||
override suspend fun flush() {
|
||||
outputStream.flush()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -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
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user