From a1a33809c49553e544a7efd3d8bc03eb0867f4ab Mon Sep 17 00:00:00 2001 From: DallasCarraher Date: Fri, 18 Sep 2026 16:00:11 -0700 Subject: [PATCH] pc 26.3: add entityDelta type for the new stepped entity-move encoding 26.3's ClientboundMoveEntityPacket (rel_entity_move / entity_move_look) replaced the flat 3x-i16 delta with a packed `properties` varint (onGround bit + stepCount) followed by either the old flat format (stepCount <= 0) or `stepCount` chained {ticks, dX, dY, dZ} DeltaStep entries. The sub-count depends on a value derived from properties at parse time (properties >>> 1), which plain protodef container/switch/countType can't express since those only reference a sibling field's raw value, not a transform of it. Adds a native `entityDelta` type (read/write/sizeOf, registered in both the interpreted and compiled datatype tables, following the existing UUID/restBuffer/entityMetadataLoop pattern) that reads/writes the whole properties+delta combo and exposes a uniform `{ onGround, steps: [{ dX, dY, dZ, ticks }] }` shape regardless of which wire variant was used, so minecraft-data's schema and downstream consumers (mineflayer) never have to branch on stepCount themselves. Verified with a roundtrip unit test and against the manual byte layout from the original crash report's dX:769/dY:-145 values. Depends on the matching minecraft-data schema change (DallasCarraher/minecraft-data, pc-26.3-support branch) that references this type by name from packet_rel_entity_move / packet_entity_move_look. Co-Authored-By: Claude Sonnet 5 --- src/datatypes/compiler-minecraft.js | 9 ++- src/datatypes/minecraft.js | 97 ++++++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/src/datatypes/compiler-minecraft.js b/src/datatypes/compiler-minecraft.js index e74e8d7fa..108146156 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]], + entityDelta: ['native', minecraft.entityDelta[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]], + entityDelta: ['native', minecraft.entityDelta[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]], + entityDelta: ['native', minecraft.entityDelta[2]] } } diff --git a/src/datatypes/minecraft.js b/src/datatypes/minecraft.js index 1b6161862..be114c7a9 100644 --- a/src/datatypes/minecraft.js +++ b/src/datatypes/minecraft.js @@ -13,10 +13,105 @@ module.exports = { restBuffer: [readRestBuffer, writeRestBuffer, sizeOfRestBuffer], entityMetadataLoop: [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata], topBitSetTerminatedArray: [readTopBitSetTerminatedArray, writeTopBitSetTerminatedArray, sizeOfTopBitSetTerminatedArray], - lpVec3: [readLpVec3, writeLpVec3, sizeOfLpVec3] + lpVec3: [readLpVec3, writeLpVec3, sizeOfLpVec3], + entityDelta: [readEntityDelta, writeEntityDelta, sizeOfEntityDelta] } const PartialReadError = require('protodef').utils.PartialReadError +// 26.3+ ClientboundMoveEntityPacket ("rel_entity_move" / "entity_move_look") delta encoding. +// +// Mojang replaced the old flat 3x-i16 delta with VecDelta, a packed `properties` varint +// (bit 0 = onGround, remaining bits = stepCount) followed by either: +// - stepCount <= 0: the old flat format, 3x i16 (dX, dY, dZ) -- the common case when the +// entity moved every tick, which is why this bug only shows up with several entities/ticks +// skipped at once. +// - stepCount > 0: `stepCount` DeltaStep entries, each read in wire order as +// {ticks: varint, dX: i16, dY: i16, dZ: i16} -- these are chained deltas for smoother +// client-side interpolation (each step's delta is relative to the position produced by +// applying the previous step, not to the original base). +// +// Confirmed by decompiling VecDelta.read()/write() and ClientboundMoveEntityPacket's +// packProperties/unpackProperties/unpackStepCount in the real 26.3 server jar. +// +// Parsed shape (uniform regardless of wire variant so downstream code never has to branch): +// { onGround: boolean, steps: [{ dX, dY, dZ, ticks }, ...] } +// For the flat/Linear wire format this is always a single-element array with ticks: 0. +function readEntityDelta (buffer, offset) { + let cursor = offset + const props = readVarInt(buffer, cursor) + cursor += props.size + const onGround = (props.value & 1) !== 0 + const stepCount = props.value >>> 1 + + const steps = [] + if (stepCount <= 0) { + if (cursor + 6 > buffer.length) throw new PartialReadError('Unexpected buffer end while reading entityDelta (linear)') + steps.push({ + dX: buffer.readInt16BE(cursor), + dY: buffer.readInt16BE(cursor + 2), + dZ: buffer.readInt16BE(cursor + 4), + ticks: 0 + }) + cursor += 6 + } else { + for (let i = 0; i < stepCount; i++) { + const ticksResult = readVarInt(buffer, cursor) + cursor += ticksResult.size + if (cursor + 6 > buffer.length) throw new PartialReadError('Unexpected buffer end while reading entityDelta (stepped)') + steps.push({ + dX: buffer.readInt16BE(cursor), + dY: buffer.readInt16BE(cursor + 2), + dZ: buffer.readInt16BE(cursor + 4), + ticks: ticksResult.value + }) + cursor += 6 + } + } + + return { value: { onGround, steps }, size: cursor - offset } +} + +function writeEntityDelta (value, buffer, offset) { + const { onGround, steps } = value + const useLinear = steps.length === 1 && steps[0].ticks === 0 + const stepCount = useLinear ? 0 : steps.length + const properties = (onGround ? 1 : 0) | (stepCount << 1) + + offset = writeVarInt(properties, buffer, offset) + if (useLinear) { + buffer.writeInt16BE(steps[0].dX, offset) + buffer.writeInt16BE(steps[0].dY, offset + 2) + buffer.writeInt16BE(steps[0].dZ, offset + 4) + offset += 6 + } else { + for (const step of steps) { + offset = writeVarInt(step.ticks, buffer, offset) + buffer.writeInt16BE(step.dX, offset) + buffer.writeInt16BE(step.dY, offset + 2) + buffer.writeInt16BE(step.dZ, offset + 4) + offset += 6 + } + } + return offset +} + +function sizeOfEntityDelta (value) { + const { onGround, steps } = value + const useLinear = steps.length === 1 && steps[0].ticks === 0 + const stepCount = useLinear ? 0 : steps.length + const properties = (onGround ? 1 : 0) | (stepCount << 1) + + let size = sizeOfVarInt(properties) + if (useLinear) { + size += 6 + } else { + for (const step of steps) { + size += sizeOfVarInt(step.ticks) + 6 + } + } + return size +} + function readVarLong (buffer, offset) { return readVarInt(buffer, offset) }