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]],
vecDelta: ['native', minecraft.vecDelta[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]],
vecDelta: ['native', minecraft.vecDelta[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]],
vecDelta: ['native', minecraft.vecDelta[2]]
}
}
3 changes: 2 additions & 1 deletion src/datatypes/lpVec3.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ function readLpVec3 (buffer, offset) {

if ((a & 4) === 4) {
const { value: varIntVal, size: varIntSize } = readVarInt(buffer, offset + 6)
scale = (varIntVal * 4) + scale
// vanilla masks the var-int to unsigned, scale >> 2 goes up to 2^32 - 1
scale = ((varIntVal >>> 0) * 4) + scale
size += varIntSize
}

Expand Down
4 changes: 3 additions & 1 deletion src/datatypes/minecraft.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const UUID = require('uuid-1345')
const zlib = require('zlib')
const [readVarInt, writeVarInt, sizeOfVarInt] = require('protodef').types.varint
const [readLpVec3, writeLpVec3, sizeOfLpVec3] = require('./lpVec3')
const [readVecDelta, writeVecDelta, sizeOfVecDelta] = require('./vecDelta')

module.exports = {
varlong: [readVarLong, writeVarLong, sizeOfVarLong],
Expand All @@ -13,7 +14,8 @@ module.exports = {
restBuffer: [readRestBuffer, writeRestBuffer, sizeOfRestBuffer],
entityMetadataLoop: [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata],
topBitSetTerminatedArray: [readTopBitSetTerminatedArray, writeTopBitSetTerminatedArray, sizeOfTopBitSetTerminatedArray],
lpVec3: [readLpVec3, writeLpVec3, sizeOfLpVec3]
lpVec3: [readLpVec3, writeLpVec3, sizeOfLpVec3],
vecDelta: [readVecDelta, writeVecDelta, sizeOfVecDelta]
}
const PartialReadError = require('protodef').utils.PartialReadError

Expand Down
62 changes: 62 additions & 0 deletions src/datatypes/vecDelta.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const [readVarInt, writeVarInt, sizeOfVarInt] = require('protodef').types.varint

const DELTA_SIZE = 6

function readDelta (buffer, offset) {
return {
dX: buffer.readInt16BE(offset),
dY: buffer.readInt16BE(offset + 2),
dZ: buffer.readInt16BE(offset + 4)
}
}

function writeDelta ({ dX, dY, dZ }, buffer, offset) {
buffer.writeInt16BE(dX, offset)
buffer.writeInt16BE(dY, offset + 2)
return buffer.writeInt16BE(dZ, offset + 4)
}

// The step count that selects the layout shares its var-int with onGround, which a protocol
// definition cannot take apart, so the flag is read and written here along with the delta.
function packProperties ({ onGround, steps = [] }) {
return (onGround ? 1 : 0) | (steps.length << 1)
}

function readVecDelta (buffer, offset) {
const { value: properties, size } = readVarInt(buffer, offset)
const onGround = (properties & 1) === 1
const stepCount = properties >>> 1
let cursor = offset + size

if (stepCount === 0) {
return { value: { onGround, ...readDelta(buffer, cursor) }, size: size + DELTA_SIZE }
}

const steps = []
for (let i = 0; i < stepCount; i++) {
const { value: ticks, size: ticksSize } = readVarInt(buffer, cursor)
steps.push({ ticks, ...readDelta(buffer, cursor + ticksSize) })
cursor += ticksSize + DELTA_SIZE
}
return { value: { onGround, steps }, size: cursor - offset }
}

function writeVecDelta (value, buffer, offset) {
const { steps = [] } = value
offset = writeVarInt(packProperties(value), buffer, offset)
if (steps.length === 0) return writeDelta(value, buffer, offset)

for (const step of steps) {
offset = writeDelta(step, buffer, writeVarInt(step.ticks, buffer, offset))
}
return offset
}

function sizeOfVecDelta (value) {
const { steps = [] } = value
const size = sizeOfVarInt(packProperties(value))
if (steps.length === 0) return size + DELTA_SIZE
return steps.reduce((total, step) => total + sizeOfVarInt(step.ticks) + DELTA_SIZE, size)
}

module.exports = [readVecDelta, writeVecDelta, sizeOfVecDelta]
32 changes: 32 additions & 0 deletions test/lpVec3Test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ const [readLpVec3, writeLpVec3, sizeOfLpVec3] = require('../src/datatypes/lpVec3

const REAL_PAYLOADS = ['f9ff7ffeebed', '59e7800cebed', '51e880011541', '09e98000d8fd']

// { x: scale, y: -scale, z: 0 } as a vanilla 26.3 server writes it: scale >> 2 at each var-int
// length, then either side of 2^31 and at ABS_MAX_VALUE, where it no longer fits a signed int.
const CONTINUED_SCALE_PAYLOADS = [
[511, 'f7ff7ffe00037f'],
[65535, 'f7ff7ffe0003ff7f'],
[65536, 'f4ff7ffe0003808001'],
[8388607, 'f7ff7ffe0003ffff7f'],
[8388608, 'f4ff7ffe000380808001'],
[1073741823, 'f7ff7ffe0003ffffff7f'],
[1073741824, 'f4ff7ffe00038080808001'],
[8589934591, 'f7ff7ffe0003ffffffff07'],
[8589934592, 'f4ff7ffe00038080808008'],
[17179869183, 'f7ff7ffe0003ffffffff0f']
]

describe('lpVec3', () => {
it('decodes the zero vector as a single byte', () => {
const result = readLpVec3(Buffer.from('00', 'hex'), 0)
Expand All @@ -28,6 +43,23 @@ describe('lpVec3', () => {
}
})

it('reads the continued scale as unsigned', () => {
for (const [scale, hex] of CONTINUED_SCALE_PAYLOADS) {
const result = readLpVec3(Buffer.from(hex, 'hex'), 0)
assert.deepStrictEqual(result.value, { x: scale, y: -scale, z: 0 }, 'unexpected value for ' + hex)
assert.strictEqual(result.size, hex.length / 2)
}
})

it('writes the continued scale as vanilla does', () => {
for (const [scale, hex] of CONTINUED_SCALE_PAYLOADS) {
const value = { x: scale, y: -scale, z: 0 }
const buffer = Buffer.alloc(sizeOfLpVec3(value))
assert.strictEqual(writeLpVec3(value, buffer, 0), buffer.length, 'sizeOf and write disagree for ' + scale)
assert.strictEqual(buffer.toString('hex'), hex)
}
})

it('sizes sanitized values consistently', () => {
for (const x of [NaN, Infinity]) {
const value = { x, y: 0, z: 0 }
Expand Down
1 change: 1 addition & 0 deletions test/packetTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ const values = {
Slot: slotValue,
UntrustedSlot: slotValue,
HashedSlot: slotValue,
ItemStackTemplate: slotValue,
SlotComponent: {
type: 'hide_tooltip'
},
Expand Down
81 changes: 81 additions & 0 deletions test/vecDeltaTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-env mocha */
const assert = require('assert')
const { ProtoDef } = require('protodef')
const { ProtoDefCompiler } = require('protodef').Compiler
const [readVecDelta, writeVecDelta, sizeOfVecDelta] = require('../src/datatypes/vecDelta')

// What a vanilla 26.3 server writes after the entity id of rel_entity_move
const VANILLA_PAYLOADS = [
['007fff00000000', { onGround: false, dX: 32767, dY: 0, dZ: 0 }],
['01000080000000', { onGround: true, dX: 0, dY: -32768, dZ: 0 }],
['0100000000ffff', { onGround: true, dX: 0, dY: 0, dZ: -1 }],
['0200000100020003', { onGround: false, steps: [{ ticks: 0, dX: 1, dY: 2, dZ: 3 }] }],
['03ffffffff07000100020003', { onGround: true, steps: [{ ticks: 2147483647, dX: 1, dY: 2, dZ: 3 }] }],
['050100010002000302fffffffefffd', {
onGround: true,
steps: [{ ticks: 1, dX: 1, dY: 2, dZ: 3 }, { ticks: 2, dX: -1, dY: -2, dZ: -3 }]
}],
['06007fff800000007f0000000000008001000100010001', {
onGround: false,
steps: [{ ticks: 0, dX: 32767, dY: -32768, dZ: 0 }, { ticks: 127, dX: 0, dY: 0, dZ: 0 }, { ticks: 128, dX: 1, dY: 1, dZ: 1 }]
}]
]

const protocol = {
types: {
varint: 'native',
vecDelta: 'native',
rel_entity_move: ['container', [{ name: 'entityId', type: 'varint' }, { name: 'delta', type: 'vecDelta' }]]
}
}

function write (value) {
const buffer = Buffer.alloc(sizeOfVecDelta(value))
assert.strictEqual(writeVecDelta(value, buffer, 0), buffer.length, 'sizeOf and write disagree')
return buffer
}

describe('vecDelta', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Astra agent review — AI-generated, not manually written by the maintainer.

Please wire these regressions into normal CI. At this head .github/workflows/ci.yml runs npm run mochaTest -- -g ${{ matrix.mcVersion }}v, but the vecDelta and lpVec3 suites have no matching version-labelled title, so their new assertions are skipped in every matrix job. The focused files pass 11 tests locally; with -g 1.21.4v they report 0 passing. Coordinate the runner repair in #1526 or add an explicit unfiltered unit-suite CI path so these cases protect subsequent changes. There is no need to duplicate version-independent codec cases into separate per-version files.

Skills used: prismarine-review traced exact-head test discovery and the existing version matrix; prismarine-protocol-data-review kept focused codec/schema checks at their appropriate boundary.

it('reads vanilla bytes', () => {
for (const [hex, value] of VANILLA_PAYLOADS) {
const result = readVecDelta(Buffer.from(hex, 'hex'), 0)
assert.deepStrictEqual(result.value, value, 'unexpected value for ' + hex)
assert.strictEqual(result.size, hex.length / 2)
}
})

it('writes the bytes vanilla writes', () => {
for (const [hex, value] of VANILLA_PAYLOADS) {
assert.strictEqual(write(value).toString('hex'), hex)
}
})

it('reads from an offset', () => {
const result = readVecDelta(Buffer.from('ffff0200000100020003', 'hex'), 2)
assert.deepStrictEqual(result, { value: VANILLA_PAYLOADS[3][1], size: 8 })
})

it('spills the step count into a second properties byte at 64 steps', () => {
const steps = Array.from({ length: 64 }, (_, i) => ({ ticks: i, dX: i, dY: i - 64, dZ: 1 }))
const buffer = write({ onGround: true, steps })
assert.strictEqual(buffer.subarray(0, 2).toString('hex'), '8101')
assert.deepStrictEqual(readVecDelta(buffer, 0), { value: { onGround: true, steps }, size: buffer.length })
})

it('is registered for interpreted and compiled protocols', () => {
const interpreted = new ProtoDef(false)
interpreted.addTypes(require('../src/datatypes/minecraft'))
interpreted.addProtocol(protocol, [])

const compiler = new ProtoDefCompiler()
compiler.addTypes(require('../src/datatypes/compiler-minecraft'))
compiler.addProtocol(protocol, [])

const packet = { entityId: 1, delta: VANILLA_PAYLOADS[4][1] }
for (const proto of [interpreted, compiler.compileProtoDefSync()]) {
const buffer = proto.createPacketBuffer('rel_entity_move', packet)
assert.strictEqual(buffer.toString('hex'), '01' + VANILLA_PAYLOADS[4][0])
assert.deepStrictEqual(proto.parsePacketBuffer('rel_entity_move', buffer).data, packet)
}
})
})
Loading