Skip to content

1.21.8 + ProtocolLib 5.4.0: Correct way to build MULTI_BLOCK_CHANGE (section long + short[] + WrappedBlockData[])? #3559

@SalomonPicos

Description

@SalomonPicos

I’m implementing per-player visual remapping of crop blocks by sending ClientboundSectionBlocksUpdate (aka MULTI_BLOCK_CHANGE) on Paper 1.21.8 using ProtocolLib 5.4.0-SNAPSHOT.
I precompute, per section (16×16×16), a short[] indices (0..4095) of all positions to rewrite, and build a WrappedBlockData[] of the same length for the target crop.

The feature works, but on this server build many sections still fail the MBC write path at runtime, so I fall back to Player#sendMultiBlockChange(Map<Location,BlockData>) per section. The visual is correct, but I would like to send pure MBC for performance.

Question: What is the canonical set of typed accessors to populate MULTI_BLOCK_CHANGE on 1.21.x, and is there anything else I need to set on the packet so that getBlockDataArrays().write(0, data) is always writable and passes a read-back check?

API method(s) used

  • PacketType.Play.Server.MULTI_BLOCK_CHANGE

  • ProtocolManager#sendServerPacket(Player, PacketContainer, boolean)

  • On PacketContainer:

    • getLongs().write(0, packedSectionLong) for the section position (see code)
    • getShortArrays().write(0, indices) (0..4095 local indices; idx = (y<<8)|(z<<4)|x)
    • getBlockDataArrays().write(0, WrappedBlockData[]) (followed by a read-back validation)
  • Fallback (Bukkit): Player#sendMultiBlockChange(Map<Location, BlockData>)

  • Wrapped types:

    • WrappedBlockData.createData(Material) and createData(BlockData)

Expected behavior

  • MULTI_BLOCK_CHANGE should accept:

    • the section position (packed long),
    • the short[] indices,
    • and the WrappedBlockData[] of the same length,
      and then render client-side without needing Bukkit fallback.
  • A read-back (getBlockDataArrays().read(0)) should return the same length that was written.

Code

Minimal construction (streamlined from our engine):

java
PacketContainer p = new PacketContainer(PacketType.Play.Server.MULTI_BLOCK_CHANGE);

// 1) Section position (section coords, not block coords)
long packed = packSectionLong(sectionX /*chunkX*/, sectionY /*y>>4*/, sectionZ /*chunkZ*/);
p.getLongs().write(0, packed);

// 2) Indices (0..4095) — length N
p.getShortArrays().write(0, indices);

// 3) Data array — WrappedBlockData[N] (no nulls)
p.getBlockDataArrays().write(0, data);

// Read-back validation
WrappedBlockData[] back = p.getBlockDataArrays().read(0);
if (back == null || back.length != data.length) {
    // fallback to Bukkit sendMultiBlockChange(map) for this section
}

protocolManager.sendServerPacket(player, p);

// Helpers
private static long packSectionLong(int sx, int sy, int sz) {
    long x = (long) sx & 0x3FFFFFL; // 22 bits
    long y = (long) sy & 0xFFFFFL;  // 20 bits
    long z = (long) sz & 0x3FFFFFL; // 22 bits
    return (x << 42) | (z << 20) | y; // Mojang X|Z|Y packing
}

Notes:

  • indices and data are 1:1 aligned (indices.length == data.length), values in range [0..4095], no nulls in data.
  • Section coords are (chunkX, y>>4, chunkZ). We don’t multiply by 16 anywhere.
  • If any write/validation step fails, we now fall back to player.sendMultiBlockChange(map) for that section (works visually).

Additional context

  • Environment

    • Paper 1.21.8, Java 21, Windows 11 (dev), ProtocolLib 5.4.0-SNAPSHOT from repo.dmulloy2.net.
  • What we observed

    • Earlier we sometimes saw:

      com.comphenix.protocol.reflect.FieldAccessException: Field index 0 is out of bounds for length 0
      at StructureModifier.write(...)

      when attempting to write a field whose accessor had size 0 (e.g., before we switched to long packing).

    • After switching the section position to the packed long and hardening the path, the feature renders correctly, but on join / crop switch we still see many sections go through our fallback with logs like:

      [RETINT-FALLBACK] bukkit-mbc key=(cx,sy,cz) count=

      meaning our step-based write failed somewhere (pos/shorts/data) and we fell back to Bukkit for that section.

  • Ask

    • On 1.21.x, is getBlockDataArrays() the canonical accessor for the IBlockData[] field on MULTI_BLOCK_CHANGE?
    • Is using getLongs().write(0, packedSectionLong) the recommended way to set SectionPosition, or should we prefer getSectionPositions().write(...) in this version?
    • Are there known conditions where getBlockDataArrays() exists but writing/reading index 0 intermittently fails? (We do a read-back and sometimes it doesn’t match, hence fallback.)
    • Any additional field(s) we are supposed to set on this packet in 1.21.x?

If helpful, I can provide a tiny test plugin with:

  • precomputed sections (short[]) and synthetic WrappedBlockData[],
  • a toggle between ProtocolLib MBC vs Bukkit sendMultiBlockChange,
  • and step logs for the three write phases.

Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions