From 3e4dea271bf4b3cba920b3bc96dc2730bea26399 Mon Sep 17 00:00:00 2001 From: u9g Date: Sun, 6 Sep 2026 19:49:53 -0400 Subject: [PATCH] fix: write the 0 holder id for inline registryEntryHolder entries The compiled writer for registryEntryHolder advanced the offset by one byte for an inline (non-registry) entry without writing anything there, so the holder id came out as whatever the unsafe buffer already held. Any serverbound packet carrying such an entry was corrupt on the wire: a 1.21.2+ client echoing an item ViaVersion gave a consumable component with an inline sound (every sword on a ViaVersion proxy) in a window_click sends a garbage holder id, and the backend drops the connection ("Your connection to encountered a problem"). sizeOf already counted the byte and the reader already expects 0. Adds a test that writes into a 0xff-filled buffer, where a skipped byte cannot pass by chance. --- src/datatypes/compiler-minecraft.js | 2 +- test/registryEntryHolderTest.js | 57 +++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 test/registryEntryHolderTest.js diff --git a/src/datatypes/compiler-minecraft.js b/src/datatypes/compiler-minecraft.js index 8a252a7c..e74e8d7f 100644 --- a/src/datatypes/compiler-minecraft.js +++ b/src/datatypes/compiler-minecraft.js @@ -111,7 +111,7 @@ if (n !== 0) { if (${baseName} != null) { offset = ${compiler.callType(`${baseName} + 1`, 'varint')} } else if (${otherwiseName}) { - offset += 1 + offset = ${compiler.callType(0, 'varint')} offset = ${compiler.callType(`${otherwiseName}`, opts.otherwise.type)} } else { throw new Error('registryEntryHolder type requires "${baseName}" or "${otherwiseName}" fields to be set') diff --git a/test/registryEntryHolderTest.js b/test/registryEntryHolderTest.js new file mode 100644 index 00000000..ac367242 --- /dev/null +++ b/test/registryEntryHolderTest.js @@ -0,0 +1,57 @@ +/* eslint-env mocha */ +const assert = require('assert') +const { createSerializer, createDeserializer } = require('../src/transforms/serializer') + +// A consumable component with an inline sound event: holder id 0 followed by the event. +const version = '1.21.4' +const sword = { + itemCount: 1, + itemId: 854, + addedComponentCount: 1, + removedComponentCount: 0, + components: [{ + type: 'consumable', + data: { + consume_seconds: 3600, + animation: 'block', + sound: { data: { soundName: 'minecraft:intentionally_empty', fixedRange: undefined } }, + makes_particles: false, + effects: [] + } + }], + removeComponents: [] +} + +describe('registryEntryHolder', () => { + const serializer = createSerializer({ state: 'play', isServer: false, version }) + const deserializer = createDeserializer({ state: 'play', isServer: false, version }) + + // The buffer is 0xff-filled: a byte the writer skips stays 0xff. + function write (value) { + const buffer = Buffer.alloc(serializer.proto.sizeOf(value, 'Slot'), 0xff) + const end = serializer.proto.write(value, buffer, 0, 'Slot') + assert.strictEqual(end, buffer.length, 'sizeOf and write disagree') + return buffer + } + + it('writes the 0 holder id in front of an inline entry', () => { + const buffer = write(sword) + // count, item id, added, removed, component type, consume_seconds (f32), animation, then the holder id + const holderIdOffset = 1 + 2 + 1 + 1 + 1 + 4 + 1 + assert.strictEqual(buffer[holderIdOffset], 0, 'holder id byte: ' + buffer.toString('hex')) + }) + + it('round-trips an inline entry', () => { + const buffer = write(sword) + const parsed = deserializer.proto.parsePacketBuffer('Slot', buffer) + assert.strictEqual(parsed.metadata.size, buffer.length) + assert.deepStrictEqual(parsed.data.components[0].data.sound, { data: { soundName: 'minecraft:intentionally_empty', fixedRange: undefined } }) + }) + + it('writes id + 1 for a registry entry', () => { + const registry = { ...sword, components: [{ type: 'consumable', data: { ...sword.components[0].data, sound: { soundId: 5 } } }] } + const buffer = write(registry) + assert.strictEqual(buffer[1 + 2 + 1 + 1 + 1 + 4 + 1], 6) + assert.deepStrictEqual(deserializer.proto.parsePacketBuffer('Slot', buffer).data.components[0].data.sound, { soundId: 5 }) + }) +})