Skip to content

feat(proto): eleven vanilla enums stop being typed out by hand - #1049

Merged
andrewgazelka merged 1 commit into
mainfrom
gen-proto
Jul 28, 2026
Merged

feat(proto): eleven vanilla enums stop being typed out by hand#1049
andrewgazelka merged 1 commit into
mainfrom
gen-proto

Conversation

@andrewgazelka

@andrewgazelka andrewgazelka commented Jul 28, 2026

Copy link
Copy Markdown
Member

A Java enum's constant order is a wire table. FriendlyByteBuf.writeEnum sends ordinal(); ByteBufCodecs.idMapper sends a field each constant declares. Either way the declaration order in the jar is as much a definition with a source of truth as a registry is, and hand-transcribing one is the same defect — with the extra hazard that an ordinal carries no name on the wire, so a transcription one constant out is a wrong value rather than a failed lookup.

Eleven of them were typed out in this crate. EquipmentSlot twice, once per module, with different accessor names on the two copies.

Why exactly these eleven, which is the interesting part

A hand-written ordinal enum here is almost exactly the set whose enclosing packet the extractor refused.

Where a packet's layout is recovered in full, build.rs::lower_enum already turns the enums it mentions into closed #[repr] Rust enums from protocol.json — twenty of them exist in OUT_DIR right now, and nobody typed any of them. protocol.json carries 42 extracted enum definitions with full constant lists; the other 22 never reach Rust because they sit inside a refused packet or data component. AttributeModifier$Operation is the clean proof: it is extracted, with its three constants, and it is also hand-written in item/payload.rs, purely because dataComponent/minecraft:attribute_modifiers is a refusal.

So these eleven are the ones no recovered layout reaches — carried by set_player_team, set_objective, set_equipment, commands, level_chunk_with_light, or by the boss bar packet that until #1038 was extracted as carrying no bytes at all. JAVA_ENUMS in nix/extract-protocol.py reads them out of the decompiled class directly rather than waiting for those layouts to be recovered.

generated Java class numbered by n
BossBarColor world.BossEvent$BossBarColor ordinal 7
BossBarOverlay world.BossEvent$BossBarOverlay ordinal 5
ChatTypeParameter network.chat.ChatTypeDecoration$Parameter ordinal 3
Direction core.Direction ordinal 6
DisplaySlot world.scores.DisplaySlot id 19
EquipmentSlot world.entity.EquipmentSlot ordinal 8
HeightmapKind world.level.levelgen.Heightmap$Types id 6
ObjectiveRenderType world.scores.criteria.ObjectiveCriteria$RenderType ordinal 2
TeamCollisionRule world.scores.Team$CollisionRule id 4
TeamColor world.scores.TeamColor id 16
TeamVisibility world.scores.Team$Visibility id 4

80 constants. Same conventions as #1021: #[repr(uN)] with the discriminant being the number, id() an as cast, name() off a static table, from_name a binary search over a table the generator sorted, closed with no Unknown, and a const assertion per file proving the discriminant folds.

The two numberings are declared, not guessed

EquipmentSlot sends ordinal() from ClientboundSetEquipmentPacket (output.writeByte(slotType.ordinal())) and s.id from its own STREAM_CODEC (idMapper(BY_ID, s -> s.id)), and the two disagree: OFFHAND is 1 by ordinal and 5 by id. Which numbering a class uses is the second element of its JAVA_ENUMS entry, resolved against the source and named in the generated module's own docs. Nothing infers it, because a value read with the wrong table is a wrong value rather than a failed lookup, and the two tables are the same shape.

The extractor refuses rather than approximates: a class that has moved, an id field that has gone, a constant the id table does not mention, or a set that is not dense 0..n-1 all fail the extraction with the class named.

Checked against the server, not against the parse

These constants are read out of decompiled Java by a regex. No test written from that same reading could catch a misreading of it, so the numbers are taken from the other side: nix/java/VanillaEncoder.java now emits java_enum.<Type>.<CONSTANT> for all 80, and for the six classes that publish a StreamCodec it gets the number by running Mojang's encoder and reading the VarInt back out (asserting the encoding is exactly one VarInt and no more). For the five with no codec it reads DisplaySlot.id() or ordinal() off the class. EquipmentSlot deliberately uses the ordinal and not its STREAM_CODEC, with the reason at the call site.

tests/java_enum.rs then walks ALL for every type and compares both directions: every variant against the server's number for the name it claims, and the count of java_enum.<Type>. fixtures against the number of variants. The second half is what catches a dropped constant, which the first half cannot see.

Guards broken, and what they said

Each mutation was checked to have applied (assert t.count(old) == 1, then grep for the replacement in the file) and to parse, before being run.

G — a discriminant one out (TeamColor::DarkBlue = 1= 2). Caught, but not by the test I was aiming at:

error[E0081]: discriminant value `2` assigned more than once
  --> crates/hyperion-minecraft-proto/src/generated/java_enum/team_color.rs:22:1

Reporting it as it happened: this mutation never reached the test, so it proves the compiler and not the fixture check. Hence H.

H — two entries swapped in the generated name table, which compiles cleanly and is what a sorting bug in the generator would actually look like. Both tests fire:

panicked at tests/java_enum.rs:34:
assertion `left == right` failed: TeamColor::Black disagrees with the server about its number
  left: 0
 right: 1

panicked at tests/java_enum.rs:88:
assertion `left == right` failed
  left: None
 right: Some(Black)

I — the extractor drops a constant, patched into scan_java_enums so the whole pipeline runs on a realistic misparse (BossBarColor loses WHITE). Regenerated, rebuilt, and the count half fires:

panicked at tests/java_enum.rs:42:
assertion `left == right` failed: BossBarColor has 6 variants but the server declares 7:
["java_enum.BossBarColor.PINK", ..., "java_enum.BossBarColor.WHITE"]
  left: 6
 right: 7

All three restored; nix run .#sync-minecraft-proto reproduces the committed tables and minecraft-proto-generated passes.

Call sites that changed, and one naming decision

DisplaySlot::to_id()id() (three tests and one line in events/smash/src/adapter.rs). HeightmapKind::from_id returned Result, the generated one returns Option, so world/chunk.rs does the ok_or itself with the reason for not clamping kept at the site. The two EquipmentSlot copies collapse into one, so from_raw/from_ordinal and to_raw/ordinal become from_id/id, plus one private slot_byte in packets/play/entity.rs because that packet packs the number into a byte with a continuation bit and the narrowing should happen once.

EquipmentSlot::MainHand becomes Mainhand, and OffHand becomes Offhand. Mojang's constants are MAINHAND and OFFHAND, one word each; MainHand needs a dictionary the generator does not have, and hand-maintaining an exception list is the thing this PR removes. Two call sites in crates/hyperion/src/simulation/inventory.rs.

Decisions on the hand-written enums this PR does not generate

Listed because a hand-written type nobody has decided about is the thing to avoid, not the existence of hand-written types.

  • ChatVisiblity, ParticleStatus — generatable, blocked. Neither class is in the decompiled tree: nix/minecraft-data.nix selects classes referenced one hop from net/minecraft/network, and both are reached only via net.minecraft.server.level.ClientInformation, which is itself one hop out. Filed as ENG-10940. The extractor sees the gap today — both degrade to a bare varint noted enum ordinal, with the class name dropped.
  • StringArgumentKindcom.mojang.brigadier.arguments.StringArgumentType$StringType. Brigadier is a separate artifact and the decompile covers net/minecraft only. Staying hand-written; the reason is now at the type.
  • NamedColor, DecorationChatFormatting, but travelling as JSON/NBT names rather than numbers, and Decoration is a reordered subset. Their truth lives in DFU MapCodecs, which this extractor cannot read at all. Staying hand-written; see the note on the text module in the enumeration.
  • EntityDataSerializer — not a Java enum. EntityDataSerializers is a static registry of fields, numbered by registerSerializer call order, so it needs a different reader. Not attempted.
  • ComponentType — ENG-10872, three enums over one registry, blocked on a hand-transcribed shape() whose variant order is load-bearing.
  • HumanoidArm — the pure duplicate. build.rs already emits it into OUT_DIR/types.rs; the copy in packets/configuration.rs exists because that module was written in parallel with the generator and landed first. Deliberately left for its own change, because packets/mod.rs documents eleven packets defined twice for exactly the same reason and HumanoidArm is one thread of that knot rather than a separate cleanup. Filed as ENG-10941 with the eleven named.

Checks

cargo test --workspace, cargo clippy --workspace --all-targets --all-features -- -D warnings (CI's exact command) and cargo fmt --all --check all green. nix build .#checks.aarch64-darwin.{minecraft-proto-coverage,minecraft-proto-json,minecraft-proto-generated,minecraft-encoder-fixtures,tool-paths} all green.

minecraft-literals is red on main and not from this branch: events/smash/tests/visuals.rs gained minecraft:entity.player.hurt_on_fire in #1035, one over the baseline of 322. Untouched here, reported rather than fixed.

A Java enum's constant order is a wire table. `FriendlyByteBuf.writeEnum`
sends `ordinal()` and `ByteBufCodecs.idMapper` sends a field each constant
declares, so the declaration order in the jar is as much a definition with
a source of truth as a registry is -- with the extra hazard that an ordinal
carries no name on the wire, so a transcription one constant out is a wrong
value rather than a failed lookup.

Eleven of them were typed out here: the boss bar pair, the four scoreboard
and team enums, `Direction`, `EquipmentSlot`, `HeightmapKind`,
`ChatTypeParameter` and `ObjectiveRenderType`. `EquipmentSlot` twice, once
per module, with different accessor names on the two copies.

They were hand-written for one reason, and it is structural: a hand-written
ordinal enum in this crate is almost exactly the set whose enclosing packet
the extractor refused. Where a layout is recovered, `build.rs` already emits
the enums it mentions from `protocol.json` -- twenty of them exist today.
These eleven sit inside `set_player_team`, `set_objective`, `set_equipment`,
`commands`, `level_chunk_with_light`, or the boss bar packet that until
tonight extracted as carrying no bytes at all. `JAVA_ENUMS` reads them out
of the decompiled class directly rather than waiting for those layouts.

The two numberings are declared per class rather than guessed, because the
difference is invisible in the result. `EquipmentSlot` sends `ordinal()`
from `ClientboundSetEquipmentPacket` and `s.id` from its own `STREAM_CODEC`,
and those disagree: `OFFHAND` is 1 by ordinal and 5 by id.

Because the constants are read out of decompiled Java by a regex, and no
test written from that reading can catch a misreading of it,
`nix/java/VanillaEncoder.java` now emits the number the *server* produces
for all eighty constants -- through the class's own `StreamCodec` for the
six that publish one -- and `tests/java_enum.rs` walks `ALL` and compares
every one, both directions, so an invented constant and a dropped constant
both fail.
@andrewgazelka
andrewgazelka merged commit 1b0ba3e into main Jul 28, 2026
9 of 12 checks passed
@andrewgazelka
andrewgazelka deleted the gen-proto branch July 28, 2026 12:31
@github-actions github-actions Bot added the feat label Jul 28, 2026
@github-actions

Copy link
Copy Markdown

Benchmark Results for general

ray_intersection/aabb_size_0.1                     [  22.2 ns ...  22.2 ns ]      +0.09%
ray_intersection/aabb_size_1                       [  22.2 ns ...  22.2 ns ]      +0.01%
ray_intersection/aabb_size_10                      [  22.8 ns ...  22.8 ns ]      -0.13%
ray_intersection/ray_distance_1                    [   0.9 ns ...   0.9 ns ]      -0.06%
ray_intersection/ray_distance_5                    [   0.9 ns ...   0.9 ns ]      +0.32%
ray_intersection/ray_distance_20                   [   0.9 ns ...   0.9 ns ]      -0.15%
overlap/no_overlap                                 [  14.3 ns ...  14.4 ns ]      +0.17%
overlap/partial_overlap                            [  14.3 ns ...  14.3 ns ]      +0.15%
overlap/full_containment                           [  14.1 ns ...  14.1 ns ]      +0.18%
point_containment/inside                           [   5.9 ns ...   5.9 ns ]      +0.14%
point_containment/outside                          [   5.9 ns ...   6.0 ns ]      +0.22%
point_containment/boundary                         [   5.9 ns ...   5.9 ns ]      -0.34%

Comparing to 04e85fb

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant