From d39cbf0a4b141ce347f57523487e52ced9aef290 Mon Sep 17 00:00:00 2001 From: mc-zuri <174940476+mc-zuri@users.noreply.github.com> Date: Sun, 20 Sep 2026 19:17:05 +0300 Subject: [PATCH 1/3] Add the vecDelta datatype for 26.3 entity moves 26.3 replaced the three i16 deltas and the trailing onGround boolean of rel_entity_move and entity_move_look with a VecDelta behind a packed var-int: writeVarInt(entityId) writeVarInt((onGround ? 1 : 0) | stepCount << 1) VecDelta.write(...) A step count of 0 is VecDelta.Linear, the same three i16 as before. Anything higher is VecDelta.Stepped, that many { varint ticks, i16, i16, i16 } records. The count comes out of the var-int by a shift, which a protocol definition cannot express, so minecraft-data declares the field as a vecDelta native and the type reads and writes the var-int together with the delta it governs. The value is { onGround, dX, dY, dZ } or { onGround, steps: [{ ticks, dX, dY, dZ }] }, keeping the field names those packets had. Tested against the bytes a vanilla 26.3 server writes for both forms, and through an interpreted and a compiled protocol since the type is registered for each separately. --- src/datatypes/compiler-minecraft.js | 9 ++-- src/datatypes/minecraft.js | 4 +- src/datatypes/vecDelta.js | 62 ++++++++++++++++++++++ test/vecDeltaTest.js | 81 +++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 src/datatypes/vecDelta.js create mode 100644 test/vecDeltaTest.js diff --git a/src/datatypes/compiler-minecraft.js b/src/datatypes/compiler-minecraft.js index e74e8d7fa..05e9751ec 100644 --- a/src/datatypes/compiler-minecraft.js +++ b/src/datatypes/compiler-minecraft.js @@ -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]], @@ -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]], @@ -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]] } } diff --git a/src/datatypes/minecraft.js b/src/datatypes/minecraft.js index 1b6161862..c1bc380a4 100644 --- a/src/datatypes/minecraft.js +++ b/src/datatypes/minecraft.js @@ -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], @@ -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 diff --git a/src/datatypes/vecDelta.js b/src/datatypes/vecDelta.js new file mode 100644 index 000000000..36b0290bf --- /dev/null +++ b/src/datatypes/vecDelta.js @@ -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] diff --git a/test/vecDeltaTest.js b/test/vecDeltaTest.js new file mode 100644 index 000000000..be7a2a774 --- /dev/null +++ b/test/vecDeltaTest.js @@ -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', () => { + 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) + } + }) +}) From 8c5e75f2c10f74bd0bd755afb8fb9d5f762d1ed6 Mon Sep 17 00:00:00 2001 From: mc-zuri <174940476+mc-zuri@users.noreply.github.com> Date: Sun, 20 Sep 2026 19:18:48 +0300 Subject: [PATCH 2/3] test: give packetTest a value for ItemStackTemplate minecraft-data#1267 moved the 26.1 item fields that cannot be empty (advancement icons, use_remainder, bundle and container contents) onto a new ItemStackTemplate type. packetTest builds every packet from a table of values per type and had none for it, so 26.1 packet_advancements threw "No value for type ItemStackTemplate" before writing anything. It takes the same fields as a filled Slot, so reuse that value. --- test/packetTest.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/packetTest.js b/test/packetTest.js index 9851fce24..73464327a 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -243,6 +243,7 @@ const values = { Slot: slotValue, UntrustedSlot: slotValue, HashedSlot: slotValue, + ItemStackTemplate: slotValue, SlotComponent: { type: 'hide_tooltip' }, From f8f8a20a300e9bf3914580b8cea6573691c55036 Mon Sep 17 00:00:00 2001 From: mc-zuri <174940476+mc-zuri@users.noreply.github.com> Date: Sat, 3 Oct 2026 23:49:12 +0300 Subject: [PATCH 3/3] ci: run the version-independent codec suites The test job runs mocha with -g v, so only suites whose title carries a Minecraft version are selected. lpVec3 and vecDelta are version-independent codec suites and never ran in any job. A separate UnitTests job runs them without the filter. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 684924d02..1fc7db02b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,15 @@ jobs: with: node-version: 24.x - run: npm i && npm run lint + UnitTests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 24.x + uses: actions/setup-node@v1.4.4 + with: + node-version: 24.x + - run: npm i && npm run mochaTest -- test/lpVec3Test.js test/vecDeltaTest.js PrepareSupportedVersions: runs-on: ubuntu-latest outputs: