Skip to content

Add Item.toHashedNotch for 1.21.5+ window_click slot claims - #184

Open
u9g wants to merge 6 commits into
masterfrom
hashed-slot
Open

Add Item.toHashedNotch for 1.21.5+ window_click slot claims#184
u9g wants to merge 6 commits into
masterfrom
hashed-slot

Conversation

@u9g

@u9g u9g commented Aug 29, 2026

Copy link
Copy Markdown
Member

Since 1.21.5, window_click no longer carries the items the client thinks are in the changed slots and on the cursor; it carries a HashedSlot: item id, count, a CRC32C per added component, and the removed component types. The server compares each hash against its own copy of the slot and resends any slot whose claim doesn't match. mineflayer currently sends Item.toNotch output there, so the hash field is never populated and every click that touches an item with components triggers a set_slot/set_cursor_item resync (see PrismarineJS/mineflayer#4026 for the kind of races that causes).

This adds Item.toHashedNotch(item) producing that HashedSlot.

How the hash works

Vanilla runs each component's codec (the data-pack shape) through HashOps: values are encoded as a tagged byte stream and CRC32C'd; numbers and UTF-16 chars are little-endian (Guava Hasher), map entries are ordered by (key hash, value hash) as unsigned ints, and optional fields equal to their codec default are omitted. Reference: net.minecraft.util.HashOps, HashedStack, HashedPatchMap; Geyser's MinecraftHashEncoder/DataComponentHashers were a useful cross-check for the per-component shapes.

Because the hash is over the codec form and the wire carries the network form, each component needs its own mapping. Covered: custom_data, max_stack_size, max_damage, damage, repair_cost, dyed_color, map_color, map_id, ominous_bottle_amplifier, unbreakable, glider, intangible_projectile, enchantment_glint_override, potion_duration_scale, minimum_attack_charge, item_model, tooltip_style, note_block_sound, rarity, custom_name, item_name, lore (text components incl. style, extra, translate/with, hover/click events), enchantments, stored_enchantments, the dye-colour components, block_state, food, tooltip_display, custom_model_data, writable_book_content, written_book_content, charged_projectiles, bundle_contents, container, profile (≤1.21.9 shape), attribute_modifiers (incl. the 1.21.11 wrapper), entity_data, block_entity_data (≤1.21.9 shape), and the NBT-valued ones (map_decorations, lock, container_loot, debug_stick_state, bucket_entity_data, recipes). Anything else hashes to 0, which just keeps today's behaviour (a resync) for that slot.

Verification

Every covered component was checked against real vanilla servers (1.21.5, 1.21.11, 26.1) by /give-ing an item carrying it, moving it between slots with window_click, and confirming from a packet trace that the server sent no set_slot/set_cursor_item afterwards — i.e. it accepted the hash. test/hashedSlot.vectors.json freezes those accepted (component data → hash) pairs as unit tests for 1.21.5 and 1.21.11 (63 vectors), alongside the CRC-32C check value and structural tests.

Known gaps:

  • block_entity_data on 1.21.11+ needs a block-entity-type registry that minecraft-data doesn't expose; falls back to 0.
  • potion_contents, trim, profile (26.1 ResolvableProfile) and the other data-driven-registry components fall back to 0.
  • On 26.1, nested stacks (bundle_contents etc.) hash wrong because minecraft-data 26.1 decodes the nested Slot with itemId/itemCount swapped — a minecraft-data issue, not a hashing one; 1.21.5–1.21.11 are correct.

Consumer: PrismarineJS/mineflayer clickWindow (PR to follow).

Since 1.21.5 window_click carries a HashedSlot per changed slot: item id,
count, and a CRC32C per added component, computed the way vanilla's
HashOps does (tagged byte encoding, little-endian numbers and UTF-16
chars, map entries ordered by hash). A claim whose hashes don't match
makes the server resend the slot, which is what every click with a
component-bearing item has been getting until now.

The hash covers the component's codec form, not its network form, so
each component has its own mapping; the ones covered are those that
show up in item patches in practice. Unsupported components hash to 0.

The vectors in test/hashedSlot.vectors.json are values a vanilla server
accepted without resyncing the slot.
Comment thread lib/hashedSlot.js Outdated
@@ -0,0 +1,302 @@
// Hashes item component values the way vanilla's HashOps does: each value is

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 should be in prismarine item

Additionally it should have more tests

And how do we make sure it stays up to date ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is prismarine-item.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added e2e live tests

… claims

For each vector in hashedSlot.vectors.json, join a real vanilla server,
/give the item, and move it with window_click claims built by
Item.toHashedNotch, rebuilt via fromNotch from the live set_slot. The
server is the oracle: it resends any slot whose claimed hash mismatches
its own copy, so the test asserts nothing but the sentinel echo follows
each click.

The sentinel is a drag-end click with no drag in progress falsely
claiming an always-empty slot holds stone: a no-op in every cursor
state whose claims are still checked, so its forced resync both orders
the round trip and proves the server is actually verifying hashes. A
/me round-trip trips the chat spam throttle after a few tests, and a
drag-start sentinel leaves drag state that swallows the next click.

Run with npm run e2e; MC_VERSION limits to one version. CI runs a
version matrix; covering a new version is a vectors.json key plus a
matrix entry.
@socket-security

socket-security Bot commented Aug 30, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​minecraft-wrap@​1.10.07810010091100
Addednpm/​minecraft-protocol@​1.68.09810010096100

View full report

u9g added 2 commits August 30, 2026 16:54
A unit test fails when a hasher has no vector covering it, unless no
vectored version has the component (adding a vectors key for the
version that introduces a component brings it under the requirement).
Fill the 16 gaps it found with new server-accepted vectors: the nbt
passthrough components, glider, note_block_sound, item_name, the
entity dye colours, profile, block_entity_data (1.21.5, where it still
hashes) and minimum_attack_charge (1.21.11, where it exists).
The version grid is mineflayer's testedVersions from 1.21.5, when
hashed slots arrived, minus 26.1 which waits on minecraft-data's
nested-Slot decode fix; CI builds its matrix from the same file. A
version between vector sets uses the give strings of the latest set at
or below it, since hashes are recomputed from what the live server
sends. A component the hasher can't reproduce on a version is sent
with hash 0 and rejected by design, so those tests skip as pending
rather than fail.

Running 1.21.9 revealed both gap boundaries sit earlier than assumed:
it already sends block_entity_data as a block entity type id and
profile in resolvable form, so those hashers fall back on 1.21.9 too
(1.21.11 uses the older shapes again). Comments updated.
@@ -0,0 +1 @@
["1.21.5", "1.21.6", "1.21.8", "1.21.9", "1.21.11"]

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 other versions ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this is when they introduced it

@rom1504

rom1504 commented Aug 30, 2026

Copy link
Copy Markdown
Member

This code feels extremely brittle and more data than code, eg I wonder if this should be something like protocol.json

@u9g

u9g commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

This code feels extremely brittle and more data than code, eg I wonder if this should be something like protocol.json

We can do it in a protocol.json-like format, but there's no version based config so far so it would just be strictly more code to make a new format and codegen/interpret it compared to now.

@u9g

u9g commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

@extremeheat

Copy link
Copy Markdown
Member

Well, part of issue is that we never correctly implemented item components in the first place.

There is no schema or registry for this data inside minecraft-data.

But aside from that, as for the hashing code itself, this code is trying to do binary structure serialization+hashing next to normal prismarine-item lib code on top of an adhoc item component implementation which makes things quite messy/unreadable

I think a proper implementation of item components could be prereq, then once that's implemented the protocol can be extended to include a HashableItemComponent type that returns the hashable binary representation before writing that on the wire. In fact may be possible to just do this completely within a node-minecraft-protocol wrapper datatype since the hash itself is quite standard

@extremeheat

Copy link
Copy Markdown
Member

To elaborate on the protodef type;

It could even be generic protodef type in node-protodef. Since CRC32 isn't cryptographic the Node.js crypto stdlib doesn't include it, but importing https://www.npmjs.com/package/crc (simple pure JS) can add support for that family:

["hash", {
    "alg": "crc32",
    "type": "u32", // data type to write serialized buffer hash (most hashes are 32/64 bit)
    "body": ["container", []] // what to serialize first
}]

For read/sizeof, read type, for write call the writer and then hash on top. Similar to how we handle NBT currently

u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Sep 4, 2026
window_click has carried HashedSlot claims since 1.21.5: item id, count
and a CRC32C per component rather than the components themselves.
clickWindow kept sending Item.toNotch output, so the hash field was
never populated and the server resynced every slot holding an item with
components after each click. Those resyncs are ordinary set_slot
packets, indistinguishable from an acknowledgement the plugin is
waiting for (see the book plugin in #4026).

Item.toHashedNotch (PrismarineJS/prismarine-item#184) computes the
hashes the server expects; use it for changedSlots and the cursor when
the protocol has a HashedSlot type.
u9g added a commit to PrismarineJS/mineflayer that referenced this pull request Sep 4, 2026
window_click has carried HashedSlot claims since 1.21.5: item id, count
and a CRC32C per component rather than the components themselves.
clickWindow kept sending Item.toNotch output, so the hash field was
never populated and the server resynced every slot holding an item with
components after each click. Those resyncs are ordinary set_slot
packets, indistinguishable from an acknowledgement the plugin is
waiting for (see the book plugin in #4026).

Item.toHashedNotch (PrismarineJS/prismarine-item#184) computes the
hashes the server expects; use it for changedSlots and the cursor when
the protocol has a HashedSlot type.
…as data

The hashing is now the generic protodef `hash` datatype over vanilla's
HashOps encoding, expressed as protodef types in lib/hashOps.js; each
component's codec shape is data in lib/hashedSlot.json. What is left in
lib/hashedSlot.js turns the network form into the codec form where the
two differ, mostly registry ids into names.
@u9g

u9g commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

Reworked along the lines of @extremeheat's comment, in layers:

  • Add the hash datatype ProtoDef-io/node-protodef#177 adds a generic hash datatype (["hash", { alg, type, body }]: serialize the value as body, write its digest as type), with the schema and docs in Add the hash datatype ProtoDef-io/ProtoDef#65. CRC32C is computed at write time by protodef, nothing hand-rolled here.
  • lib/hashOps.js is vanilla's HashOps encoding as protodef types: tagged leaves, hashops_list, hashops_dict, hashops_map (a record with from/optional/default, the way a codec omits fields), text components. NBT is pure protodef JSON, a switch on the tag type.
  • lib/hashedSlot.json is every component's codec shape as data, which is the "more data than code" part @rom1504 asked for.
  • lib/hashedSlot.js is down to the network → codec conversion that needs the registry (ids to names, dye indices, UUIDs, item stacks).

All 63 frozen vectors still pass unchanged, and the 1.21.5 e2e passes against a live vanilla server. This now depends on the unreleased protodef (^1.20.0 in package.json), so CI stays red until #177 is released.

Found while running the e2e on protodef master: minecraft-protocol's client sends the configuration-phase settings packet without particleStatus (a mapper since 1.21.2); ProtoDef-io/node-protodef#176 makes that throw where 1.19.0 silently wrote it. Worth a one-line default in src/client/play.js before protodef 1.20 ships.

Types take HashOps' own names; a record is an object of field name to
type (or { type, optional, default }); list and dict take their element
type as the bare argument. The JSON now describes only the codec shape:
the two network-form renames moved into converters.
@u9g

u9g commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

Simplified the shape format in the last commit: types use HashOps' own names, a record is an object of field name to type, and list/dict take their element type directly. lib/hashedSlot.json now describes only the data-pack shape, e.g.

"food": ["map", {
  "nutrition": "int",
  "saturation": "float",
  "can_always_eat": { "type": "boolean", "default": false }
}],
"lore": ["list", "text"],
"enchantments": ["dict", "int"]

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