-
-
Notifications
You must be signed in to change notification settings - Fork 298
Description
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!