Skip to content

Revert the 1.21.6 enum mappers whose ordinals never moved - #1292

Open
u9g wants to merge 1 commit into
PrismarineJS:masterfrom
u9g:fix/revert-1216-enum-mappers
Open

Revert the 1.21.6 enum mappers whose ordinals never moved#1292
u9g wants to merge 1 commit into
PrismarineJS:masterfrom
u9g:fix/revert-1216-enum-mappers

Conversation

@u9g

@u9g u9g commented Sep 12, 2026

Copy link
Copy Markdown
Member

Reverts four fields that became mappers in 1.21.6 while staying plain numbers in every earlier protocol, back to their exact pre-1.21.6 types:

field pre-1.21.6 1.21.6+ this PR
difficulty.difficulty u8 mapper(varint) u8
set_difficulty.newDifficulty u8 mapper(varint) u8
game_state_change.reason u8 mapper(u8) u8
map_chunk.heightmaps[].type varint mapper(varint) varint

The wire values behind all four have never moved, so the mapper buys none of the insertion-stability a mapper is for, and it is currently breaking consumers in both directions:

  • Read. difficulty now yields a string, so mineflayer's difficultyNames[packet.difficulty] is undefined and bot.game.difficulty is unset on 1.21.6+.
  • Write. A numeric value throws since Compiled mapper: throw on a value not in the mappings instead of writing it ProtoDef-io/node-protodef#176 made the compiled mapper reject values that aren't in the mappings. Before that it fell through to the raw value, which is why these went unnoticed for four releases. It breaks flying-squid's game_state_change { reason: 2 } and difficulty { difficulty: 0..3 }, and any client writing set_difficulty { newDifficulty: 0..3 }.

difficulty/newDifficulty go back to u8 rather than the varint they were given here: vanilla writes the enum ordinal, and for a 4-value enum that is byte-identical to a varint, so u8 matches the other 52 pc versions exactly.

Touches data/pc/{1.21.6,1.21.8,1.21.9,1.21.11}/proto.yml and data/pc/latest/proto.yml, with protocol.json regenerated via npm run build. npm test passes (1860 passing).

Deliberately not reverted

  • entity_action.actionId keeps its mapper. The two sneak actions moved to player_input in 1.21.6, so every ordinal after them shifted by 2 (start_elytra_flying went 8 → 6). Here a bare number really does mean different things per version, which is exactly the argument in Fix pc/26.1 client_command actionId: plain varint, not a mapper #1284 for naming these — reverting it would turn mineflayer's actionId: 2 in bed.js from a loud throw into a silent stop_sprinting. Its entityActionUsesStringMapper feature flag stays valid.
  • teams.mode is entangled with a real 1.21.6 packet restructure (flat mode ? if 0: switches → an anonymous _ switch, friendlyFire: i8 → a flags bitflags). Not a revertable line.
  • update_structure_block.flags landed in 1.21.5, not 1.21.6, and is mismodelled independently of this: a bitfield expressed as a 4-entry enum mapper.
  • client_command.actionId (Fix pc/26.1 client_command actionId: plain varint, not a mapper #1284) and use_entity.hand (Fix pc/26.1 use_entity hand: plain varint, not a mapper #1278) are already in flight.

After this, those four are the only remaining fields in the pc data that are a mapper in some versions and a raw number in others.

On the wider direction

@extremeheat — this cuts against your suggestion on #1284 of mapper-ising everything, so to be explicit about where I think the line is: I agree names are the right long-run representation, and the insertion-stability argument is real (entity_action above is a live example of it). What makes the 1.21.6 batch a problem isn't the direction, it's that it landed per-version on fields whose numbers were never unstable, which means consumers pay a migration for no stability gain and end up with version-gated branches like mineflayer's entityActionUsesStringMapper.

If we do want to go wide, the sequencing that would make it painless is worth doing first:

  1. Teach ProtoDef's mapper to accept the underlying wire value on write as well as the name — i.e. after the name lookup fails, accept the value if it is itself a key of mappings. An unmapped name still throws, so add PDS file for 1.11 #176's guarantee is untouched, but mapper-ising a field stops being a breaking change on the write side.
  2. Source the names from the game rather than by hand, so new versions arrive correct — the decompiled writeEnum/readEnum call sites give ordinal order mechanically, and minecraft-data-generator could emit an enums.json.
  3. Add a CI audit: a field that is a mapper in any version must be a mapper in every version where it exists. That both prevents another per-version drift and gives the exact field list downstream needs to grep for.

Happy to do (1) and (3) as follow-ups if that sounds right.

`difficulty.difficulty`, `set_difficulty.newDifficulty`,
`game_state_change.reason` and `map_chunk.heightmaps[].type` became mappers
in 1.21.6 while staying plain numbers in every earlier protocol. The wire
values behind them have not changed in any version, so the mapper buys no
stability and only costs compatibility:

- Reading, `difficulty` now yields a string, so mineflayer's
  `difficultyNames[packet.difficulty]` is `undefined` on 1.21.6+ and
  `bot.game.difficulty` is unset.
- Writing, a numeric value throws since ProtoDef-io/node-protodef#176 made
  the compiled mapper reject values not in the mappings (before that it fell
  through to the raw value, which is why these went unnoticed). That breaks
  flying-squid's `game_state_change {reason: 2}` and any client writing
  `set_difficulty {newDifficulty: 0..3}`.

Reverted to the exact pre-1.21.6 types, so existing code needs no change.
`difficulty`/`newDifficulty` go back to `u8`: vanilla writes the enum
ordinal, and for a 4-value enum that is byte-identical to the varint they
were given here.

`entity_action.actionId` deliberately keeps its mapper: the two sneak
actions moved to `player_input` in 1.21.6, so every ordinal after them
shifted by 2 and a numeric value really does mean different things per
version. `teams.mode`, `update_structure_block.flags` and
`client_command.actionId` (PrismarineJS#1284) are left for separate changes.
@extremeheat

Copy link
Copy Markdown
Member

This is actually a pretty simple thing to fix, might do it in a bit

@VasilisDragon VasilisDragon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both difficulty fields should stay varint. From 1.21.6, vanilla uses Difficulty.STREAM_CODEC through ByteBufCodecs.idMapper; u8 only happens to match the bytes for 0–3.

This also changes existing named writes: newDifficulty: 'hard' now writes 00 instead of 03. Numeric writers recover, but named callers would need migrating.

@extremeheat

extremeheat commented Sep 13, 2026

Copy link
Copy Markdown
Member

Nominally, varint vs u8 should only matters for values >128 as the 4 MSBs are unused on both

However if clients/servers write invalid values that intentionally are out of an enum's bounds, this can cause serializer exceptions if using u8 in place of something that's supposed to be varint32 (upto 5 bytes)

Copy link
Copy Markdown

Yeah, agreed. Numeric 0–3 encode the same. I should've been clearer that this was about matching vanilla's varint field encoding.

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.

3 participants