Skip to content

pc 26.3: add entityDelta type for the new stepped entity-move encoding - #1538

Open
DallasCarraher wants to merge 1 commit into
PrismarineJS:pc26_3from
DallasCarraher:fix-entity-move-stepped-delta
Open

DallasCarraher wants to merge 1 commit into
PrismarineJS:pc26_3from
DallasCarraher:fix-entity-move-stepped-delta

Conversation

@DallasCarraher

Copy link
Copy Markdown
Contributor

Summary

26.3 replaced ClientboundMoveEntityPacket's (rel_entity_move / entity_move_look) flat 3x-i16
delta with a packed properties varint (bit 0 = onGround, remaining bits = stepCount) followed
by either the old flat format (stepCount <= 0) or stepCount chained
{ticks: varint, dX, dY, dZ: i16} DeltaStep entries used for smoother client-side interpolation
when the server batches more than one tick's movement into a single packet.

Without this, minecraft-data's 26.3 schema (see paired PR
minecraft-data#1301) still assumed
the old flat format, so almost every entity-movement packet was silently misparsed as soon as
there was more than one nearby entity -- flooding the console with
Chunk size is N but only M was read ; partial packet : rel_entity_move /
PartialReadError.

The step count depends on a value derived from properties at parse time (properties >>> 1),
which plain protodef container/switch/countType can't express (those only reference a
sibling field's raw value, not a transform of it). This adds a native entityDelta type
(read/write/sizeOf, registered in both the interpreted and compiled datatype tables) following
the existing UUID/restBuffer/entityMetadataLoop custom-type pattern, exposing a uniform
{ onGround, steps: [{ dX, dY, dZ, ticks }] } shape regardless of which wire variant was used.

Root cause found by decompiling VecDelta, VecDelta$Stepped$DeltaStep,
ClientboundMoveEntityPacket (.Pos/.PosRot) and their packProperties/unpackStepCount
static helpers in the real 26.3 server jar (javap -c -p), continuing the precedent from
mineflayer#3888.

Depends on / paired with:

  • minecraft-data#1301 (schema
    referencing this type via packet_rel_entity_move.move / packet_entity_move_look.move)
  • mineflayer fix updating lib/plugins/entities.js to consume packet.move.steps (PR to follow)

Test plan

  • Unit roundtrip test of entityDelta read/write/sizeOf against synthetic linear and
    multi-step buffers, including the exact dX:769, dY:-145 values from the original crash
    report.
  • Verified end-to-end against a local offline-mode 26.3 server with several bots + existing
    mobs constantly moving: thousands of rel_entity_move/entity_move_look packets and
    hundreds of stepped sync_entity_position packets parsed with zero
    Chunk size is ... was read warnings, entity positions tracked sanely (no NaN/drift).
  • Verified against a real Mojang Realm running 26.3: bot stayed connected, tens of thousands
    of rel_entity_move/entity_move_look packets (including real multi-step packets with
    ticks > 1) and hundreds of sync_entity_position packets, zero warnings for these packet
    types.

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 <noreply@anthropic.com>

@rom1504 rom1504 left a comment

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.

The native type belongs at this layer, but please coordinate one public movement representation with #1539 before releasing either API. This PR returns a uniform steps array, while #1539 uses flat deltas or a stepped value and already has captured-byte fixtures. minecraft-data #1301 and Mineflayer #4144 must consume the same chosen contract.

A single step with ticks: 0 is also rewritten as the flat encoding here; preserve or explicitly define that normalization if exact packet rewriting is supported. Please commit the stated codec tests, including flat, one-step, multi-step, truncation, and read/write/size coverage, ideally incorporating #1539’s independent captured-byte cases. The author’s server/Realm results are useful evidence, but I did not rerun them. This remains an integration/API choice, rather than a recommendation to ship two overlapping native types.

Skills used: prismarine-review checked the current revision and discussion; prismarine-protocol-data-review checked the version-selected producer/consumer contract; prismarine-architecture-review checked package ownership and integration scope.

@mc-zuri

mc-zuri commented Oct 3, 2026 •

Copy link
Copy Markdown

The current solution fails one step with ticks 0 case. I used (test/entityDeltaTest.js), one vanilla payload per layout:

const assert = require('assert')
const [readEntityDelta, writeEntityDelta, sizeOfEntityDelta] = require('../src/datatypes/minecraft').entityDelta

// Bytes a vanilla 26.3 server writes after the entity id of rel_entity_move, one per layout
const VANILLA_PAYLOADS = [
  ['linear', '007fff00000000', { onGround: false, steps: [{ ticks: 0, dX: 32767, dY: 0, dZ: 0 }] }],
  ['stepped, one step', '03ffffffff07000100020003', { onGround: true, steps: [{ ticks: 2147483647, dX: 1, dY: 2, dZ: 3 }] }],
  ['stepped, one step with ticks 0', '0200000100020003', { onGround: false, steps: [{ ticks: 0, dX: 1, dY: 2, dZ: 3 }] }],
  ['stepped, two steps', '050100010002000302fffffffefffd', { onGround: true, steps: [{ ticks: 1, dX: 1, dY: 2, dZ: 3 }, { ticks: 2, dX: -1, dY: -2, dZ: -3 }] }]
]

describe('entityDelta', () => {
  for (const [layout, hex, value] of VANILLA_PAYLOADS) {
    it('round-trips ' + layout, () => {
      assert.deepStrictEqual(readEntityDelta(Buffer.from(hex, 'hex'), 0), { value, size: hex.length / 2 })
      const buffer = Buffer.alloc(sizeOfEntityDelta(value))
      writeEntityDelta(value, buffer, 0)
      assert.strictEqual(buffer.toString('hex'), hex)
    })
  }
})

@rim5432

rim5432 commented Oct 7, 2026

Copy link
Copy Markdown

Validation: this branch (a1a3380, on top of #1532 pc26_3) together with the 26.3 data from minecraft-data #1301 (924e26d) successfully joins a modded NeoForge 26.3.0.52-beta dedicated server directly — handshake, configuration, registry sync, PLAY, and 10 s of full entity traffic where rel_entity_move/entity_move_look stream through the new entityDelta encoding without parse errors. For reference, the same join with #1532 alone fails on the first entity-move packet with ctx.entityDelta is not a function, which matches the PR description.

@Taiizor

Taiizor commented Oct 20, 2026

Copy link
Copy Markdown

Confirmed by an independent decompilation of the 26.3 server jar - the layout in this PR matches the wire format exactly.

From net.minecraft.network.protocol.game.VecDelta.read/write and ClientboundMoveEntityPacket.packProperties/unpackStepCount:

  • The leading properties varint is onGround | (stepCount << 1) - bit 0 is the on-ground flag, the remaining bits are the step count.
  • stepCount <= 0 (Linear): three i16 (dX, dY, dZ), where a zero means "no delta on that axis" - the write side only writes the axes that are present, so 0 is a valid value.
  • stepCount > 0 (Stepped): stepCount entries, each read in wire order as {ticks: varint, dX: i16, dY: i16, dZ: i16} (VecDelta$Stepped$DeltaStep holds exactly xa, ya, za, ticks). Each step is relative to the position produced by the previous step, not to the base.

Two details that are easy to miss and that this implementation gets right:

  • The deltas are fixed point with 1/4096 block precision: VecDeltaCodec.encode is Math.round(value * 4096.0) and decode is value / 4096.0 (not 32 or 128*32 as in older versions).
  • The per-axis range is limited to [-32768, 32767], i.e. +/- 8 blocks, which VecDeltaCodec.isDeltaTooBig checks before encoding.

Verified end to end against a live Spigot 26.3 server (protocol 777): with this type, plus the mineflayer-side handling of the stepped shape, entity move packets parse without desyncing the stream and the bot's own position tracking matches the server's.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants