Add Item.toHashedNotch for 1.21.5+ window_click slot claims - #184
Conversation
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.
| @@ -0,0 +1,302 @@ | |||
| // Hashes item component values the way vanilla's HashOps does: each value is | |||
There was a problem hiding this comment.
This should be in prismarine item
Additionally it should have more tests
And how do we make sure it stays up to date ?
… 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.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
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"] | |||
There was a problem hiding this comment.
this is when they introduced it
|
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. |
|
For what it's worth, geyser also implements this in code: https://github.com/GeyserMC/Geyser/blob/master/core/src/main/java/org/geysermc/geyser/item/hashing/DataComponentHashers.java |
|
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 |
|
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 |
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.
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.
|
Reworked along the lines of @extremeheat's comment, in layers:
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 ( Found while running the e2e on protodef master: minecraft-protocol's client sends the configuration-phase |
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.
|
Simplified the shape format in the last commit: types use HashOps' own names, a record is an object of field name to type, and "food": ["map", {
"nutrition": "int",
"saturation": "float",
"can_always_eat": { "type": "boolean", "default": false }
}],
"lore": ["list", "text"],
"enchantments": ["dict", "int"] |
Since 1.21.5,
window_clickno longer carries the items the client thinks are in the changed slots and on the cursor; it carries aHashedSlot: 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 sendsItem.toNotchoutput there, so thehashfield is never populated and every click that touches an item with components triggers aset_slot/set_cursor_itemresync (see PrismarineJS/mineflayer#4026 for the kind of races that causes).This adds
Item.toHashedNotch(item)producing thatHashedSlot.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 (GuavaHasher), 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'sMinecraftHashEncoder/DataComponentHasherswere 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 withwindow_click, and confirming from a packet trace that the server sent noset_slot/set_cursor_itemafterwards — i.e. it accepted the hash.test/hashedSlot.vectors.jsonfreezes 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_dataon 1.21.11+ needs a block-entity-type registry that minecraft-data doesn't expose; falls back to 0.potion_contents,trim,profile(26.1ResolvableProfile) and the other data-driven-registry components fall back to 0.bundle_contentsetc.) hash wrong because minecraft-data 26.1 decodes the nestedSlotwithitemId/itemCountswapped — a minecraft-data issue, not a hashing one; 1.21.5–1.21.11 are correct.Consumer: PrismarineJS/mineflayer
clickWindow(PR to follow).