Skip to content

feat(net): serve Minecraft 26.2 (protocol 776) and drop the valence 763 pre-play path - #972

Merged
andrewgazelka merged 5 commits into
mainfrom
feat/serve-26.2
Jul 27, 2026
Merged

andrewgazelka merged 5 commits into
mainfrom
feat/serve-26.2

Conversation

@andrewgazelka

@andrewgazelka andrewgazelka commented Jul 27, 2026 •

Copy link
Copy Markdown
Member

Serves Minecraft 26.2 (protocol 776) through crates/hyperion-minecraft-proto.
A scripted client reaches play state; a vanilla client does not yet stay there,
and the reason is spelled out below.

What changed

The four states before play -- handshake, status, login, and the configuration
state that did not exist in 1.20.1 -- are now crates/hyperion/src/net/protocol/.
The valence 763 path was deleted rather than feature-gated: a branch that
only one of two protocols ever takes is a branch nobody exercises.

The transport did not change with the protocol. hyperion frames packets itself
and the proxy forwards bytes without reading them, so net::encoder,
net::decoder and packet_channel are untouched; one adapter turns a proto-crate
Encode plus a packet id into a PacketBundle and the existing compression and
length framing carry it.

Registry contents did not have to be written.
RegistrySynchronization.packRegistry writes an element with an empty payload
when the client reported the pack that defines it. Offering
minecraft:core:26.2 and getting it back means all 397 elements of all 29
synchronised registries go out as names alone, so registries.rs is a name
table. The order is RegistryDataLoader.SYNCHRONIZED_REGISTRIES and the names
are what the vanilla data generator writes; both read off the jar. A client that
does not report the core pack is refused with a message saying exactly that.

ClientboundLoginFinishedPacket was the one packet the proto crate was missing.
It was added here by hand and then dropped again when #971 landed mid-flight and
generated it; this branch is rebased onto that and uses the generated login,
handshake and status structs. Configuration follows the generated types where
they exist rather than the hand-written duplicates, which shortens the
reconciliation that crate's packets/mod.rs describes rather than lengthening
it. client_information, custom_payload and update_tags stay hand-written,
because the generator cannot express them.

Verified

nix run .#lint and nix run .#test pass (315 tests). Full transcripts are in
docs/minecraft-26.2-migration.md. The join, against a real running stack on
dedicated ports:

[Prober] <- LoginCompression threshold=256
[Prober] <- LoginFinished profile=c07403a5245bf4e124ad10d435771f55 name=Prober (authenticated, NOT yet in the world)
[Prober] -> LoginAcknowledged
[Prober] <- SelectKnownPacks [('minecraft', 'core', '26.2')]
[Prober] -> SelectKnownPacks (accepting all)
[Prober] <- RegistryData x29 (397 elements): ...
[Prober] <- FinishConfiguration
[Prober] -> FinishConfiguration (ack)
[Prober] <- Login entity_id=67109860  ** REACHED PLAY STATE **
[Prober] RESULT: reached play state (Login received)

tools/client-26.2.py is the 776 counterpart to smash-client.py. It exists
because a player count is not evidence of a join, and on 776 there are two more
gates before the world than there were on 763.

Two bugs this was watched for

  • Authenticated but never in the world. The join now runs off the client's
    own finish_configuration rather than off a skin channel, so a skin that
    never arrives can no longer keep anyone out. tests/join_world.rs still
    guards the channel that applies skins.
  • Skin layers. 1.20.2 moved ClientInformation into configuration, so it is
    handled there and DisplayedSkinParts is set before the player enters the
    world.

The test client caught a third. hyperion-permission unicast the command tree
the moment a permission group was set, during login. On 763 that landed after
LoginSuccess, where the client is already reading play packets, so it worked
by accident; on 776 the same bytes arrive in the configuration state and are
read as whatever configuration packet shares that id. Now gated on the player
being in play.

What a reviewer should not assume works

A vanilla client disconnects immediately after the join packet. Clientbound
play is still valence at 1.20.1 ids -- the same transcript continues
0x1E x4096, 0x24 x2434, 0x4E x1, 0x65 x154, which are UnloadChunkS2c,
ChunkDataS2c, ChunkRenderDistanceCenterS2c and PlayerListHeaderS2c. The
scripted client counts them and carries on, which is what lets it prove the
join; a real client will not. Serverbound play is likewise still decoded on
valence ids.

So the next piece is the whole of clientbound play, not just terrain.
registries.rs should also be deleted in favour of generated contents once the
proto crate produces them.

@github-actions github-actions Bot added the feat label Jul 27, 2026
Replaces the four pre-play states with a hand-written 26.2 state machine
and deletes the valence 763 path rather than gating it, so there is one
protocol in the tree and no dead branch to keep compiling.

- crates/hyperion/src/net/protocol: handshake, status, login and the
  configuration state, plus the play Login packet that puts a client in
  the world. Encoding reuses the existing framing: hyperion's own length
  prefix, zlib and packet id are protocol-independent, and the proxy
  forwards bytes without reading them.
- registries.rs carries names only. A client that reports the vanilla
  core data pack already has the contents, so RegistrySynchronization
  sends each element with an empty payload; the names came off the
  server jar's data generator, and the registry order out of
  RegistryDataLoader.SYNCHRONIZED_REGISTRIES.
- ClientInformation is handled in configuration, where 1.20.2 moved it,
  so DisplayedSkinParts is set before the player enters the world.
- proto crate gains ClientboundLoginFinishedPacket, which the config
  work left out.
tools/client-26.2.py is the 776 counterpart to smash-client.py. It exists
because on 776 the proxy's player count is even further from proof than
it was on 763: there are now two gates before the world, the
configuration state and the known-pack negotiation, and either can
swallow a client silently.

The command tree exposed one. hyperion-permission unicast it the moment a
permission group was set, which is during login. On 763 that landed after
LoginSuccess, where the client is already reading play packets, so it
worked by accident. On 776 the handover is explicit and the same bytes
arrive in the configuration state, where the client reads them as
whatever configuration packet shares that id. Gated on the player
actually being in play.
#971 landed the build-time generator while this was in flight. Login,
handshake and status are now generated from protocol.json, so the
hand-written ClientboundLoginFinishedPacket added here is gone in favour
of the generated one.

Configuration follows the generated types where they exist --
select_known_packs, registry_data, update_enabled_features and
finish_configuration -- rather than the hand-written duplicates, which
shortens the reconciliation that crate's packets/mod.rs describes instead
of lengthening it. client_information, custom_payload and update_tags
stay hand-written; the generator cannot express them.
@github-actions

Copy link
Copy Markdown

Benchmark Results for general

ray_intersection/aabb_size_0.1                     [  18.8 ns ...  18.7 ns ]      -0.11%
ray_intersection/aabb_size_1                       [  18.8 ns ...  18.8 ns ]      +0.25%
ray_intersection/aabb_size_10                      [  18.8 ns ...  18.8 ns ]      -0.09%
ray_intersection/ray_distance_1                    [   1.3 ns ...   1.3 ns ]      +0.02%
ray_intersection/ray_distance_5                    [   1.3 ns ...   1.3 ns ]      +0.22%
ray_intersection/ray_distance_20                   [   1.3 ns ...   1.3 ns ]      +0.18%
overlap/no_overlap                                 [  15.6 ns ...  15.6 ns ]      +0.26%
overlap/partial_overlap                            [  15.6 ns ...  15.6 ns ]      +0.02%
overlap/full_containment                           [  14.4 ns ...  14.4 ns ]      -0.47%
point_containment/inside                           [   5.7 ns ...   5.7 ns ]      -0.22%
point_containment/outside                          [   5.4 ns ...   5.4 ns ]      +0.07%
point_containment/boundary                         [   5.4 ns ...   5.4 ns ]      -0.24%

Comparing to 6f7c0d4

@andrewgazelka
andrewgazelka merged commit 3fbdffc into main Jul 27, 2026
9 of 11 checks passed
@andrewgazelka
andrewgazelka deleted the feat/serve-26.2 branch July 27, 2026 12:10
@github-actions

Copy link
Copy Markdown

Benchmark Results for general

ray_intersection/aabb_size_0.1                     [  19.2 ns ...  19.2 ns ]      -0.07%
ray_intersection/aabb_size_1                       [  19.2 ns ...  19.2 ns ]      -0.11%
ray_intersection/aabb_size_10                      [  19.1 ns ...  19.1 ns ]      +0.12%
ray_intersection/ray_distance_1                    [   0.8 ns ...   0.8 ns ]      +1.53%
ray_intersection/ray_distance_5                    [   0.8 ns ...   0.8 ns ]      +0.24%
ray_intersection/ray_distance_20                   [   0.8 ns ...   0.8 ns ]      +0.45%
overlap/no_overlap                                 [  13.0 ns ...  13.0 ns ]      +0.21%
overlap/partial_overlap                            [  12.8 ns ...  12.8 ns ]      -0.11%
overlap/full_containment                           [  12.2 ns ...  12.2 ns ]      +0.11%
point_containment/inside                           [   5.1 ns ...   5.2 ns ]      +0.21%
point_containment/outside                          [   5.1 ns ...   5.1 ns ]      +0.00%
point_containment/boundary                         [   5.1 ns ...   5.1 ns ]      +0.02%

Comparing to 6f7c0d4

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 17.52212% with 466 lines in your changes missing coverage. Please review.
✅ Project coverage is 36.08%. Comparing base (7f7bf16) to head (804f0f9).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
crates/hyperion/src/net/protocol/pre_play.rs 18.18% 297 Missing ⚠️
crates/hyperion/src/net/protocol/join.rs 8.94% 111 Missing and 1 partial ⚠️
crates/hyperion/src/net/protocol/mod.rs 26.92% 38 Missing ⚠️
crates/hyperion-permission/src/lib.rs 0.00% 17 Missing ⚠️
crates/hyperion/src/ingress/decode.rs 75.00% 1 Missing ⚠️
crates/hyperion/src/ingress/mod.rs 50.00% 1 Missing ⚠️
@@            Coverage Diff             @@
##             main     #972      +/-   ##
==========================================
+ Coverage   34.95%   36.08%   +1.13%     
==========================================
  Files         219      230      +11     
  Lines       22476    23243     +767     
  Branches      835      878      +43     
==========================================
+ Hits         7856     8387     +531     
- Misses      14438    14650     +212     
- Partials      182      206      +24     
Files with missing lines Coverage Δ
crates/hyperion-packet-macros/src/lib.rs 100.00% <ø> (ø)
crates/hyperion-packet-macros/src/packet.rs 78.57% <ø> (-14.29%) ⬇️
crates/hyperion/src/egress/player_join/mod.rs 100.00% <ø> (+88.88%) ⬆️
crates/hyperion/src/net/mod.rs 8.51% <ø> (ø)
crates/hyperion/src/net/protocol/registries.rs 100.00% <100.00%> (ø)
crates/hyperion/src/simulation/mod.rs 34.16% <ø> (ø)
crates/hyperion/src/simulation/packet.rs 28.73% <ø> (ø)
crates/hyperion/src/ingress/decode.rs 40.96% <75.00%> (+9.60%) ⬆️
crates/hyperion/src/ingress/mod.rs 55.84% <50.00%> (+17.12%) ⬆️
crates/hyperion-permission/src/lib.rs 0.00% <0.00%> (ø)
... and 3 more

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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