Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/datatypes/compiler-minecraft.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ if (n !== 0) {
}
`.trim())
}],
lpVec3: ['native', minecraft.lpVec3[0]]
lpVec3: ['native', minecraft.lpVec3[0]],
nbtOptionalLengthPrefixed: ['native', minecraft.nbtOptionalLengthPrefixed[0]]
},
Write: {
varlong: ['native', minecraft.varlong[1]],
Expand Down Expand Up @@ -137,7 +138,8 @@ if (${baseName} != null) {
return offset
`.trim())
}],
lpVec3: ['native', minecraft.lpVec3[1]]
lpVec3: ['native', minecraft.lpVec3[1]],
nbtOptionalLengthPrefixed: ['native', minecraft.nbtOptionalLengthPrefixed[1]]
},
SizeOf: {
varlong: ['native', minecraft.varlong[2]],
Expand Down Expand Up @@ -197,6 +199,7 @@ if (${baseName} != null) {
return size
`.trim())
}],
lpVec3: ['native', minecraft.lpVec3[2]]
lpVec3: ['native', minecraft.lpVec3[2]],
nbtOptionalLengthPrefixed: ['native', minecraft.nbtOptionalLengthPrefixed[2]]
}
}
25 changes: 24 additions & 1 deletion src/datatypes/minecraft.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ module.exports = {
restBuffer: [readRestBuffer, writeRestBuffer, sizeOfRestBuffer],
entityMetadataLoop: [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata],
topBitSetTerminatedArray: [readTopBitSetTerminatedArray, writeTopBitSetTerminatedArray, sizeOfTopBitSetTerminatedArray],
lpVec3: [readLpVec3, writeLpVec3, sizeOfLpVec3]
lpVec3: [readLpVec3, writeLpVec3, sizeOfLpVec3],
nbtOptionalLengthPrefixed: [readNbtOptionalLengthPrefixed, writeNbtOptionalLengthPrefixed, sizeOfNbtOptionalLengthPrefixed]
}
const PartialReadError = require('protodef').utils.PartialReadError

Expand Down Expand Up @@ -187,3 +188,25 @@ function sizeOfTopBitSetTerminatedArray (value, { type }) {
}
return size
}

// A network (anonymous) optional NBT tag preceded by its byte length as a VarInt.
// This is Mojang's `ByteBufCodecs.optionalTagCodec(...).apply(ByteBufCodecs.lengthPrefixed(N))`
// (e.g. the payload of ServerboundCustomClickActionPacket / the dialog system). An empty tag
// is a single TAG_END (0) byte, so an absent value encodes as `01 00`.
function readNbtOptionalLengthPrefixed (buffer, offset) {
const { value: length, size: lengthSize } = readVarInt(buffer, offset)
if (offset + lengthSize + length > buffer.length) { throw new PartialReadError() }
const tag = nbt.proto.read(buffer, offset + lengthSize, 'anonOptionalNbt')
return { value: tag.value, size: lengthSize + tag.size }
}

function writeNbtOptionalLengthPrefixed (value, buffer, offset) {
const innerSize = nbt.proto.sizeOf(value, 'anonOptionalNbt')
offset = writeVarInt(innerSize, buffer, offset)
return nbt.proto.write(value, buffer, offset, 'anonOptionalNbt')
}

function sizeOfNbtOptionalLengthPrefixed (value) {
const innerSize = nbt.proto.sizeOf(value, 'anonOptionalNbt')
return sizeOfVarInt(innerSize) + innerSize
}
38 changes: 38 additions & 0 deletions test/nbtOptionalLengthPrefixedTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* eslint-env mocha */
const assert = require('assert')
const nbt = require('prismarine-nbt')
const { nbtOptionalLengthPrefixed } = require('../src/datatypes/minecraft')
const [read, write, sizeOf] = nbtOptionalLengthPrefixed

describe('nbtOptionalLengthPrefixed', () => {
// The NBT payload of a real ServerboundCustomClickActionPacket (a dialog form submit),
// captured from a 1.21.11 client: a single VarInt byte-length (0x1a = 26) followed by the
// anonymous compound { password: "hello world" }.
const compound = nbt.comp({ password: nbt.string('hello world') })
// 1a | 0a(compound) 08(string) 0008 "password" 000b "hello world" 00(end)
const expectedHex = '1a' + '0a' + '08' + '0008' + Buffer.from('password').toString('hex') +
'000b' + Buffer.from('hello world').toString('hex') + '00'

it('writes a present tag as VarInt(byteLength) + anonymous NBT', () => {
const buf = Buffer.alloc(sizeOf(compound))
const end = write(compound, buf, 0)
assert.strictEqual(buf.subarray(0, end).toString('hex'), expectedHex)
assert.strictEqual(sizeOf(compound), end)
})

it('round-trips a present tag', () => {
const buf = Buffer.alloc(sizeOf(compound))
write(compound, buf, 0)
const { value, size } = read(buf, 0)
assert.strictEqual(size, buf.length)
assert.strictEqual(value.value.password.value, 'hello world')
})

it('encodes an absent tag as 01 00 (length 1, TAG_END)', () => {
const buf = Buffer.alloc(8)
const end = write(undefined, buf, 0)
assert.strictEqual(buf.subarray(0, end).toString('hex'), '0100')
assert.strictEqual(sizeOf(undefined), 2)
assert.strictEqual(read(buf, 0).value, undefined)
})
})
Loading