Skip to content

Support Minecraft 26.3 / Velocity 4.2.0 - #279

Closed
mjiangmc wants to merge 4 commits into
Elytrium:masterfrom
mjiangmc:velocity-4.2.0-support
Closed

mjiangmc wants to merge 4 commits into
Elytrium:masterfrom
mjiangmc:velocity-4.2.0-support

Conversation

@mjiangmc

@mjiangmc mjiangmc commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds support for Velocity 4.2.0 / Minecraft 26.3 (protocol 777), and fixes two build problems that block it. Verified end-to-end against a vanilla 26.3 client: connects, loads registries, receives chunks, reaches the backend server.

Fixes the startup crash on 4.2.0:

java.lang.NoSuchMethodError: 'void com.velocitypowered.proxy.protocol.packet.JoinGamePacket.setGamemode(short)'
    at net.elytrium.limboapi.server.LimboImpl.createJoinGamePacket(LimboImpl.java:968)

Commits

  1. build: stop the Minecraft data generator from stalling the build — independent bug fix, could be split out if you prefer.
  2. refactor(api)!: widen block state ids from short to int — breaking API change, see below.
  3. feat: support Minecraft 26.3 / Velocity 4.2.0 — the actual 26.3 work.

Build

  • velocity 3.5.0-SNAPSHOT → 4.1.2-SNAPSHOT. Elytrium's repo has no velocity-proxy 4.2.0 yet. I diffed every class LimboAPI touches between 4.1.2-SNAPSHOT and 4.2.0 — the API and proxy internals are byte-for-byte identical, so this can be bumped to 4.2.0 once it is published.
  • Compile target 21 → 25. velocity-api 4.x is Java 25 bytecode and its Gradle module metadata rejects a JVM 21 consumer (Dependency resolution is looking for a library compatible with JVM runtime version 21, but ... is only compatible with JVM runtime version 25). CI already runs JDK 25.

The data generator deadlock (commit 1)

commandLine.execute([], parent).waitFor() gives the child pipes for stdout/stderr that nothing reads, and a stdin pipe that is never closed. The generator logs a lot, so it blocks as soon as the OS pipe buffer fills — at an arbitrary provider, tens of minutes in, or never. On Windows the buffer is only 4 KiB.

Measured over 1.13–26.3: ~30 minutes per version before, 3m40s total after.

Also: the exit code was ignored, so a failed generation surfaced much later as Unable to process file: .../blocks.json. It now fails with the generator's log path — which is how the JDK 25 requirement became obvious (UnsupportedClassVersionError: class file version 69.0 for the 26.x server jars).

Protocol changes in 26.3

Change Fix
Much of the play-state packet id space is renumbered (+1/+2/+3 depending on where packets were inserted) 11 limbo packets get a MINECRAFT_26_3 mapping
JoinGame/Respawn: gamemode and previousGamemode widened byte → VarInt; previousGamemode redefined as "game mode + 1 or 0" (CommonPlayerSpawnInfo now holds an Optional<GameType>) version-dependent encoding
TeleportConfirm now echoes the confirmed position and rotation (3 doubles + 2 floats) decode the extra fields and widen the declared bounds
Chunk light masks switched from writeBitSet (long array: count of longs + longs) to ByteBufCodecs.BIT_SET (byte array: count of bytes + bytes) write byte arrays from 26.3 on

The packet ids come from the vanilla data generator's reports/packets.json, and each was cross-checked by matching the class back to the 26.1 table (every name lines up: level_chunk_with_light, set_default_spawn_position, map_item_data, …). 26.1 → 26.2 changed nothing, which is why the previous version reused the 26.1 ids.

The light mask one is worth calling out: both encodings are the same size for a mask, only the length prefix means something different, so a 2-long mask goes from 0x02 to 0x10 and the client reads 2 bytes instead of 16. That desynchronises the rest of the packet and the client reports found 73815 bytes extra whilst reading packet level_chunk_with_light.

Registries

The config-state registry sync is built once per version range from that range's first version, so a range must not straddle a version that changed a registry element's shape. 26.3 changed minecraft:trim_material, so the 26.1 → MAXIMUM range is split at 26.3 — otherwise a 26.3 client is served 26.1-shaped content.

  • trim_material: asset_name (free-form string) → palette_id (an Identifier, minecraft:trim/<name>).
  • Added minecraft:block_transformer and minecraft:decorated_pot_pattern. The client resolves both while building item data components from the synced item registry and throws Missing element ResourceKey[minecraft:block_transformer / minecraft:shovel] when an entry vanilla items point at is absent — so they have to be present even in a limbo. block_transformer's codec is BlockTransformData.CODEC.listOf(1, 200), i.e. at least one transform, so a vanilla-derived placeholder is sent; a limbo never transforms blocks. block_state_provider, the third new registry, is empty in vanilla and unreferenced, so it is correctly omitted.
  • Registry elements are no longer read with getCompound(), which silently turns a non-compound element into an empty compound — block_transformer's element is a bare list.

I checked all 22 registries LimboAPI fabricates against the 26.3 client; only these differed.

BREAKING CHANGE: block state ids are now int

26.3 has 35723 block states, so the highest state id is 35722 and no longer fits in a short (max 32767). 26.1 and 26.2 peaked at 29872 and 32365, which is why this went unnoticed:

NumberFormatException: Value out of range. Value:"32768" Radix:10
    at java.lang.Short.parseShort
    at net.elytrium.limboapi.server.world.SimpleBlock.lambda$init$3(SimpleBlock.java:85)

VirtualBlock#getModernID, #getID and #getBlockStateID now return int.

Only the state id needed widening — the block registry id (max 1285 in 26.3) and item ids (max 1657) still fit and are untouched, as are the legacy ids in BlockStorage17 and the 1.8 global palette. Downstream plugins (LimboAuth, LimboFilter, …) will need recompiling.

Known gaps

  • 64 of the new 26.3 blocks (concrete and wool slabs/stairs) plus red_shrub and shelf_mushroom have no older counterpart to map to, so they still fall back to stone on older clients. The build prints a note per block. The poplar wood set is mapped.
  • block_transformer's three entries share one vanilla-derived placeholder transform rather than each having its own. Unreachable in a limbo.

Testing

Built with JDK 25, ran on Velocity 4.2.0 with a 26.3 vanilla client through LimboCheck and LimboAuth limbos.

One unrelated warning you may also see, from PacketEvents 2.13.0 rather than LimboAPI: NbtCodecException: Tag asset_name doesn't exist. PacketEvents parses the outgoing registry sync with 26.2-era definitions and cannot read 26.3's palette_id. It is caught and logged, the packet is untouched, but it will appear on any 26.3 server with PacketEvents installed.

  • This PR was written by an AI — apologies in advance for any mistakes.

`commandLine.execute([], parent).waitFor()` gives the child process pipes for
stdout and stderr that nothing ever reads, and an stdin pipe that is never
written to or closed. The data generator logs a lot, so it blocks as soon as
the OS pipe buffer fills - at an arbitrary point inside whichever provider
happens to be running. On Windows the buffer is only 4 KiB, which makes the
build hang for tens of minutes per Minecraft version, or forever.

Measured on 1.13-26.3: ~30 minutes per version (when it finished at all)
before, 3m40s total after.

Redirect the output to a file and close the child's stdin instead, and fail
the build with the generator's log path when it does not produce a report -
previously the exit code was ignored and the failure surfaced much later as
a confusing "Unable to process file: .../blocks.json".
Minecraft 26.3 has 35723 block states, so the highest state id is 35722 and
no longer fits in a short (max 32767). 26.1 and 26.2 peaked at 29872 and
32365, which is why the short typing went unnoticed until now.

`SimpleBlock.init()` fails outright on the new mappings:

  NumberFormatException: Value out of range. Value:"32768" Radix:10
      at java.lang.Short.parseShort
      at SimpleBlock.lambda$init$3(SimpleBlock.java:85)

Only the *state* id needs widening - the block registry id (max 1285 in 26.3)
and the item ids (max 1657) still fit and are left alone, as are the legacy
ids in BlockStorage17 and the 1.8 global palette.

BREAKING CHANGE: VirtualBlock#getModernID, #getID and #getBlockStateID now
return int instead of short. Downstream plugins (LimboAuth, LimboFilter, ...)
need to be recompiled against the new API.
Velocity 4.2.0 (protocol 777) added MINECRAFT_26_3 and raised the maximum
supported version, so LimboAPI refused to start:

  NoSuchMethodError: 'void JoinGamePacket.setGamemode(short)'
      at LimboImpl.createJoinGamePacket(LimboImpl.java:968)

plus, once recompiled, a protocol-support warning and a client disconnect at
every step of the limbo handshake.

Build
  - velocity 3.5.0-SNAPSHOT -> 4.1.2-SNAPSHOT. Elytrium's mirror has no
    velocity-proxy 4.2.0 yet; 4.1.2-SNAPSHOT's API and proxy internals are
    byte-for-byte the same as 4.2.0's (verified by comparing every class
    LimboAPI touches), so this can move to 4.2.0 once it is published.
  - Compile target 21 -> 25: velocity-api 4.x is Java 25 bytecode and its
    Gradle module metadata rejects a JVM 21 consumer outright. CI already
    runs JDK 25.

Protocol
  - SUPPORTED_MAXIMUM_PROTOCOL_VERSION_NUMBER 776 -> 777.
  - 26.3 renumbers much of the play-state packet id space (+1/+2/+3 depending
    on where packets were inserted), so the 11 limbo packets whose ids moved
    get a MINECRAFT_26_3 mapping. The ids come from the vanilla data
    generator's reports/packets.json and were cross-checked by matching every
    class against the 26.1 table. 26.1 -> 26.2 changed nothing, which is why
    the previous version reused the 26.1 ids.
  - JoinGame/Respawn: 26.3 widened gamemode and previousGamemode from a byte
    to a VarInt and redefined previousGamemode as "game mode + 1 or 0"
    (CommonPlayerSpawnInfo holds an Optional<GameType> now).
  - TeleportConfirm gained the position and rotation being confirmed
    (3 doubles + 2 floats). Without this the packet is longer than the
    declared bounds and Velocity drops the connection on the leftover bytes.
  - Chunk light masks: 26.3 replaced writeBitSet (long array: "count of
    longs" + longs) with ByteBufCodecs.BIT_SET (byte array: "count of bytes"
    + bytes). Both are the same size for a mask, but the length prefix means
    something different, so a 2-long mask went from 0x02 to 0x10 and the
    client read 2 bytes instead of 16, desynchronising the rest of the packet.

Registries
  - The config-state registry sync is built once per version range, so a
    range must not straddle a version that changed a registry element. 26.3
    changed minecraft:trim_material, so the range is split at 26.3.
  - trim_material: 26.3 replaced the free-form asset_name with palette_id,
    an Identifier (minecraft:trim/<name>).
  - Added minecraft:block_transformer and minecraft:decorated_pot_pattern.
    The client resolves both while building item data components from the
    synced item registry, and throws when an entry vanilla items point at is
    missing. block_transformer's codec requires at least one transform, so a
    vanilla-derived placeholder is sent; a limbo never transforms blocks.
  - Registry elements are no longer read with getCompound(), which silently
    turns a non-compound element into an empty compound. block_transformer's
    element is a bare list.

Game data
  - MINECRAFT_26_3 in MinecraftVersion, WorldVersion and BlockEntityVersion.
    26.3 renumbers 1173 block ids and 1465 item ids, and adds 90 blocks and
    121 items. The block entity registry is unchanged.
  - fallbackdata.json gains the 26.3 blocks that have an obvious older
    analogue (the poplar wood set). The remaining 26.3 blocks - concrete and
    wool slabs/stairs, red_shrub, shelf_mushroom - have no counterpart to map
    to and still fall back to stone on older clients.

Tested against a 26.3 vanilla client on Velocity 4.2.0: connects, loads
registries, receives the chunks and reaches the backend server.

BREAKING CHANGE: VirtualBlock's id getters now return int, see the previous
commit.

@UserNugget UserNugget 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.

Thanks for PR, but i'd better reimplement it myself to see if mojang broke something.

public interface VirtualBlock {

short getModernID();
int getModernID();

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.

Should be avoidable, there are still ~29k ids left.

Comment thread plugin/mapping/fallbackdata.json Outdated
@@ -1,4 +1,30 @@
{
"MINECRAFT_26_3": {

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.

No new concrete/wool blocks and wrong version, should be MINECRAFT_26_2.

public class TeleportConfirmPacket implements MinecraftPacket {

// 26.3 makes the client echo the position and rotation it is confirming: three doubles and two floats.
private static final int CONFIRMED_POSITION_SIZE = 3 * Double.BYTES + 2 * Float.BYTES;

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.

Is there any reason not to decode this packet properly?

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.

Also changed.

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.

Also changed.

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.

Also changed.

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.

Also changed.

joinGame.setPreviousGamemode((short) -1);
// 26.3 changed previousGamemode from "game mode or -1" to "game mode + 1 or 0"
// and widened both fields from byte to VarInt on the wire.
joinGame.setPreviousGamemode(version.noLessThan(ProtocolVersion.MINECRAFT_26_3) ? 0 : -1);

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.

26.3 uses boolean+varint to encode this field, while Velocity uses only varint. Velocity bug.

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.

This nbt is optional on 26.3.

Comment on lines 1249 to 1255

// Trim material
// Trim material.
// 26.3 replaced the free-form "asset_name" with a reference to a trim palette.
boolean trimPalettes = version.noLessThan(ProtocolVersion.MINECRAFT_26_3);
CompoundBinaryTag trim = CompoundBinaryTag.builder()
.putString("asset_name", "redstone")
.putString(trimPalettes ? "palette_id" : "asset_name", trimPalettes ? "minecraft:trim/redstone" : "redstone")
.put("description", CompoundBinaryTag.builder()

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.

Should be better reimplemented correctly.

- Widen block state ids to char (unsigned 16-bit) rather than int. 26.3 peaks
  at 35722, so ~29k ids are still free in 16 bits, and this keeps the field and
  every map keyed by it the width they were. LimboFactory's two state-id
  parameters are widened too (the legacy/Block overloads stay short).

- fallbackdata: the new entry was keyed MINECRAFT_26_3, which is never read.
  getBlockID() walks down from MAXIMUM_VERSION and decrements *before* the first
  lookup, so the highest key it can apply is MAXIMUM_VERSION - 1. Keyed
  MINECRAFT_26_2 now. Also fills in the 64 concrete/wool slabs and stairs that
  were missing - mapped to the full block of the same colour, since 26.2 has no
  coloured slab to preserve the shape with.

- TeleportConfirm: decode the echoed position and rotation instead of skipping
  them.

- ChunkDataPacket: 26.3 makes the block entity nbt optional, so an empty one is
  written as absent rather than as an empty compound.

- trim_material: each of the 11 entries now carries its own palette/asset name
  and colour instead of all 11 sharing redstone's.

- LimboProtocol: explicit 26.3 mappings for SetSlotPacket and the three Move*
  packets. Per the vanilla data generator's reports/packets.json their ids are
  unchanged between 26.2 and 26.3, so these resolve to the same value the
  inherited mapping already did - they are there for explicitness.
@mjiangmc

Copy link
Copy Markdown
Contributor Author

Thanks for the review — pushed a follow-up commit (6cece22) covering all of it.

VirtualBlock ids → char — done. 35722 leaves ~29k in 16 bits, and it keeps SimpleBlock's field and the maps keyed by it the same width they were. The state-id parameters in LimboFactory had to move too (createSimpleBlock(short id, boolean modern), whose id is passed to SimpleBlock.solid() when modern is true, and createSimpleBlock(boolean, boolean, boolean, short id)); the legacy/Block overloads stay short.

fallbackdata.json — you're right on both counts, and the key one was a real bug rather than style: getBlockID() walks down from MAXIMUM_VERSION and decrements before the first lookup, so the highest key it can ever apply is MAXIMUM_VERSION - 1. Keyed MINECRAFT_26_3 it would never have been read. It is MINECRAFT_26_2 now, with the 64 concrete/wool slabs and stairs added — mapped to the full block of the same colour, since 26.2 has nothing that preserves both shape and colour. Say the word if you would rather they fell back to stone_slab/stone_stairs instead; red_shrub, shelf_mushroom and straw_bed are still unmapped as I do not have a defensible target for them.

TeleportConfirmPacket — decoded properly now, no more skip.

ChunkDataPacket:201 — an empty nbt is written as the absent marker on 26.3 rather than as an empty compound.

Trim materials — each of the 11 now carries its own palette_id/asset_name and colour, taken from data/minecraft/trim_material/*.json.

LimboImpl:977 — thanks, that is a better explanation than mine; the comment now says Velocity writes a bare VarInt for a field 26.3 encodes as boolean + VarInt, and that 0 is what reads back as "no previous gamemode".

The four "Also changed" comments (SetSlotPacket, MovePacket, MovePositionOnlyPacket, MoveRotationOnlyPacket) — I cannot reproduce these, and would like to check I am not missing something. The ids below come from the vanilla data generator's reports/packets.json for 26.2 and 26.3:

packet 26.2 26.3
container_set_slot 0x14 0x14
move_player_pos_rot 0x1F 0x1F
move_player_pos 0x1E 0x1E
move_player_rot 0x20 0x20

and the client classes (ClientboundContainerSetSlotPacket, ServerboundMovePlayerPacket$Pos/$PosRot/$Rot) are byte-identical between the two versions. SetSlotPacket writes a container id, so it is container_set_slot rather than set_carried_item, which did move. I have added explicit 26.3 entries anyway so the handling is uniform — they resolve to the same ids the inherited mappings already did. If you meant the ids do differ, or that something else about those four changed, could you point me at it?

On reimplementing it yourself — no objection at all. Happy to drop this if your version supersedes it.

@mjiangmc mjiangmc closed this Sep 17, 2026
@mjiangmc
mjiangmc deleted the velocity-4.2.0-support branch September 17, 2026 04:45
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.

2 participants