From da58b0eb48cd05532628632576e1b6d8df59bf1f Mon Sep 17 00:00:00 2001 From: u9g Date: Mon, 7 Sep 2026 01:02:35 -0400 Subject: [PATCH 1/2] fix: send the action ids 26.1 maps to strings 26.1's protocol maps client_command's actionId and use_entity's hand to strings, so writing the numeric id throws "SizeOf error ... is not in the mappings value" and the packet never leaves the client: the bot cannot respawn or right-click an entity there. entity_action has been mapped since 1.21.6, which bed.js's wake() still missed. Needs the two features added in PrismarineJS/minecraft-data#1279. --- lib/plugins/bed.js | 2 +- lib/plugins/creative.js | 2 +- lib/plugins/game.js | 2 +- lib/plugins/health.js | 2 +- lib/plugins/inventory.js | 4 +-- test/internalTest.js | 62 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 68 insertions(+), 6 deletions(-) diff --git a/lib/plugins/bed.js b/lib/plugins/bed.js index 9b526f698d..513d70d2bf 100644 --- a/lib/plugins/bed.js +++ b/lib/plugins/bed.js @@ -70,7 +70,7 @@ function inject (bot) { } else { bot._client.write('entity_action', { entityId: bot.entity.id, - actionId: 2, + actionId: bot.supportFeature('entityActionUsesStringMapper') ? 'leave_bed' : 2, jumpBoost: 0 }) } diff --git a/lib/plugins/creative.js b/lib/plugins/creative.js index 2b894e4c6b..a6e02c4be0 100644 --- a/lib/plugins/creative.js +++ b/lib/plugins/creative.js @@ -46,7 +46,7 @@ function inject (bot) { resolve() }, timeoutMs) pendingStatsRequests.push(request) - bot._client.write('client_command', bot.supportFeature('respawnIsPayload') ? { payload: 1 } : { actionId: 1 }) + bot._client.write('client_command', bot.supportFeature('respawnIsPayload') ? { payload: 1 } : { actionId: bot.supportFeature('clientCommandUsesStringMapper') ? 'request_stats' : 1 }) }) } diff --git a/lib/plugins/game.js b/lib/plugins/game.js index d3291b06ea..265f8c21d3 100644 --- a/lib/plugins/game.js +++ b/lib/plugins/game.js @@ -123,7 +123,7 @@ function inject (bot, options) { bot._client.on('game_state_change', (packet) => { if ((packet.reason === 4 || packet.reason === 'win_game') && packet.gameMode === 1) { - bot._client.write('client_command', { action: 0 }) + bot._client.write('client_command', bot.supportFeature('respawnIsPayload') ? { payload: 0 } : { actionId: bot.supportFeature('clientCommandUsesStringMapper') ? 'perform_respawn' : 0 }) } if ((packet.reason === 3) || (packet.reason === 'change_game_mode')) { bot.game.gameMode = parseGameMode(packet.gameMode) diff --git a/lib/plugins/health.js b/lib/plugins/health.js index 94ff325903..089a5fc281 100644 --- a/lib/plugins/health.js +++ b/lib/plugins/health.js @@ -41,7 +41,7 @@ function inject (bot, options) { const respawn = () => { if (bot.isAlive) return - bot._client.write('client_command', bot.supportFeature('respawnIsPayload') ? { payload: 0 } : { actionId: 0 }) + bot._client.write('client_command', bot.supportFeature('respawnIsPayload') ? { payload: 0 } : { actionId: bot.supportFeature('clientCommandUsesStringMapper') ? 'perform_respawn' : 0 }) } bot.respawn = respawn diff --git a/lib/plugins/inventory.js b/lib/plugins/inventory.js index bab0eef7ba..92bfb39f4b 100644 --- a/lib/plugins/inventory.js +++ b/lib/plugins/inventory.js @@ -252,7 +252,7 @@ function inject (bot, { hideErrors }) { target: entity.id, mouse: 0, // interact with entity sneaking: false, - hand: 0, // interact with the main hand + hand: bot.supportFeature('useEntityUsesStringMapper') ? 'main_hand' : 0, // main hand location: new Vec3(0, 0, 0) }) } @@ -264,7 +264,7 @@ function inject (bot, { hideErrors }) { target: entity.id, mouse: 2, // interact with entity at sneaking: false, - hand: 0, // interact with the main hand + hand: bot.supportFeature('useEntityUsesStringMapper') ? 'main_hand' : 0, // main hand x: position.x - entity.position.x, y: position.y - entity.position.y, z: position.z - entity.position.z, diff --git a/test/internalTest.js b/test/internalTest.js index 068368f4df..9844f4d790 100644 --- a/test/internalTest.js +++ b/test/internalTest.js @@ -1527,6 +1527,68 @@ for (const supportedVersion of mineflayer.testedVersions) { }) }) + describe('string mapped action ids', () => { + // 26.1 maps client_command's actionId and use_entity's hand to strings, and 1.21.6+ maps + // entity_action's actionId; a numeric id fails to serialize there and never reaches the server. + function onJoin (name, act, check) { + server.on('playerJoin', async (client) => { + client.on('packet', (data, meta) => { if (meta.name === name) check(data) }) + await client.write('login', bot.test.generateLoginPacket()) + const chunk = bot.test.buildChunk() + chunk.setBlockType(vec3(0, 64, 0), registry.blocksByName.stone.id) + await client.write('map_chunk', generateChunkPacket(chunk)) + await client.write('position', { + x: 0, + y: 66, + z: 0, + dx: 0, + dy: 0, + dz: 0, + yaw: 0, + pitch: 0, + flags: bot.registry.version['>=']('1.21.3') ? {} : 0, + teleportId: 0 + }) + await sleep(200) + await act() + }) + } + + it('respawns with an action id the protocol accepts', (done) => { + onJoin('client_command', () => { + bot.isAlive = false + bot.respawn() + }, (data) => { + assert.strictEqual(data.actionId ?? data.payload, registry.supportFeature('clientCommandUsesStringMapper') ? 'perform_respawn' : 0) + done() + }) + }) + + it('interacts with an entity using a hand the protocol accepts', (done) => { + onJoin('use_entity', () => { + const Entity = require('prismarine-entity')(registry) + const target = new Entity(42) + target.position = vec3(1, 66, 0) + target.height = 1.8 + bot.entities[target.id] = target + return bot.activateEntity(target) + }, (data) => { + assert.strictEqual(data.hand, registry.supportFeature('useEntityUsesStringMapper') ? 'main_hand' : 0) + done() + }) + }) + + it('leaves a bed with an action id the protocol accepts', (done) => { + onJoin('entity_action', () => { + bot.isSleeping = true + return bot.wake() + }, (data) => { + assert.strictEqual(data.actionId, registry.supportFeature('entityActionUsesStringMapper') ? 'leave_bed' : 2) + done() + }) + }) + }) + describe('onceWithCleanup', () => { it('rejects instead of throwing out of emit when checkCondition throws', async () => { // A condition that throws used to unwind whatever was emitting. For a From 3dfd736b509f35f54356b5982344ba197d276d8e Mon Sep 17 00:00:00 2001 From: u9g Date: Mon, 7 Sep 2026 07:56:27 -0400 Subject: [PATCH 2/2] fix: send the action ids a protocol maps to names, and read the shape from the schema Replaces the supportFeature checks with a lib/protocol_ids.js helper that asks the schema whether the field is a protodef mapper, so the fix is right whether minecraft-data keeps 26.1's mappers or drops them, and needs no unreleased data. The three tests now resolve the packet out of the server's read path before asserting: throwing inside the packet listener wedged the connection and surfaced as an afterEach timeout on versions whose use_entity carries no hand at all (1.8-1.11), and those versions now only assert that the packet arrives. --- lib/plugins/creative.js | 3 +- lib/plugins/game.js | 3 +- lib/plugins/health.js | 3 +- lib/plugins/inventory.js | 7 +++- lib/protocol_ids.js | 12 ++++++ test/internalTest.js | 84 +++++++++++++++++++++++----------------- 6 files changed, 71 insertions(+), 41 deletions(-) create mode 100644 lib/protocol_ids.js diff --git a/lib/plugins/creative.js b/lib/plugins/creative.js index a6e02c4be0..a398690b62 100644 --- a/lib/plugins/creative.js +++ b/lib/plugins/creative.js @@ -2,6 +2,7 @@ const assert = require('assert') const { Vec3 } = require('vec3') const { sleep, onceWithCleanup } = require('../promise_utils') const { once } = require('../promise_utils') +const { mapsIdsToNames } = require('../protocol_ids') module.exports = inject @@ -46,7 +47,7 @@ function inject (bot) { resolve() }, timeoutMs) pendingStatsRequests.push(request) - bot._client.write('client_command', bot.supportFeature('respawnIsPayload') ? { payload: 1 } : { actionId: bot.supportFeature('clientCommandUsesStringMapper') ? 'request_stats' : 1 }) + bot._client.write('client_command', bot.supportFeature('respawnIsPayload') ? { payload: 1 } : { actionId: mapsIdsToNames(bot.registry, 'packet_client_command', 'actionId') ? 'request_stats' : 1 }) }) } diff --git a/lib/plugins/game.js b/lib/plugins/game.js index 265f8c21d3..63197f5d36 100644 --- a/lib/plugins/game.js +++ b/lib/plugins/game.js @@ -1,4 +1,5 @@ const nbt = require('prismarine-nbt') +const { mapsIdsToNames } = require('../protocol_ids') module.exports = inject const difficultyNames = ['peaceful', 'easy', 'normal', 'hard'] @@ -123,7 +124,7 @@ function inject (bot, options) { bot._client.on('game_state_change', (packet) => { if ((packet.reason === 4 || packet.reason === 'win_game') && packet.gameMode === 1) { - bot._client.write('client_command', bot.supportFeature('respawnIsPayload') ? { payload: 0 } : { actionId: bot.supportFeature('clientCommandUsesStringMapper') ? 'perform_respawn' : 0 }) + bot._client.write('client_command', bot.supportFeature('respawnIsPayload') ? { payload: 0 } : { actionId: mapsIdsToNames(bot.registry, 'packet_client_command', 'actionId') ? 'perform_respawn' : 0 }) } if ((packet.reason === 3) || (packet.reason === 'change_game_mode')) { bot.game.gameMode = parseGameMode(packet.gameMode) diff --git a/lib/plugins/health.js b/lib/plugins/health.js index 089a5fc281..a600c26486 100644 --- a/lib/plugins/health.js +++ b/lib/plugins/health.js @@ -1,3 +1,4 @@ +const { mapsIdsToNames } = require('../protocol_ids') module.exports = inject function inject (bot, options) { @@ -41,7 +42,7 @@ function inject (bot, options) { const respawn = () => { if (bot.isAlive) return - bot._client.write('client_command', bot.supportFeature('respawnIsPayload') ? { payload: 0 } : { actionId: bot.supportFeature('clientCommandUsesStringMapper') ? 'perform_respawn' : 0 }) + bot._client.write('client_command', bot.supportFeature('respawnIsPayload') ? { payload: 0 } : { actionId: mapsIdsToNames(bot.registry, 'packet_client_command', 'actionId') ? 'perform_respawn' : 0 }) } bot.respawn = respawn diff --git a/lib/plugins/inventory.js b/lib/plugins/inventory.js index 92bfb39f4b..dabde16a0d 100644 --- a/lib/plugins/inventory.js +++ b/lib/plugins/inventory.js @@ -2,6 +2,7 @@ const assert = require('assert') const { Vec3 } = require('vec3') const { once, sleep, createDoneTask, createTask, withTimeout } = require('../promise_utils') const { toNotchianYaw, toNotchianPitch } = require('../conversions') +const { mapsIdsToNames } = require('../protocol_ids') module.exports = inject @@ -245,6 +246,8 @@ function inject (bot, { hideErrors }) { bot.swingArm() } + const mainHand = mapsIdsToNames(bot.registry, 'packet_use_entity', 'hand') ? 'main_hand' : 0 + async function activateEntity (entity) { // TODO: tell the server that we are not sneaking while doing this await bot.lookAt(entity.position.offset(0, 1, 0), false) @@ -252,7 +255,7 @@ function inject (bot, { hideErrors }) { target: entity.id, mouse: 0, // interact with entity sneaking: false, - hand: bot.supportFeature('useEntityUsesStringMapper') ? 'main_hand' : 0, // main hand + hand: mainHand, // main hand location: new Vec3(0, 0, 0) }) } @@ -264,7 +267,7 @@ function inject (bot, { hideErrors }) { target: entity.id, mouse: 2, // interact with entity at sneaking: false, - hand: bot.supportFeature('useEntityUsesStringMapper') ? 'main_hand' : 0, // main hand + hand: mainHand, // main hand x: position.x - entity.position.x, y: position.y - entity.position.y, z: position.z - entity.position.z, diff --git a/lib/protocol_ids.js b/lib/protocol_ids.js new file mode 100644 index 0000000000..40e4a50159 --- /dev/null +++ b/lib/protocol_ids.js @@ -0,0 +1,12 @@ +/** + * Some serverbound action ids are described as a protodef `mapper` on newer protocols, where the + * field takes the name rather than the number: 26.1 does it for `client_command.actionId` and + * `use_entity.hand`. Writing the number there throws in the serializer and the packet is dropped, so + * every writer has to ask the schema which shape this version wants. + */ +function mapsIdsToNames (registry, packet, field) { + const type = registry.protocol?.play?.toServer?.types?.[packet]?.[1]?.find(f => f.name === field)?.type + return Array.isArray(type) && type[0] === 'mapper' +} + +module.exports = { mapsIdsToNames } diff --git a/test/internalTest.js b/test/internalTest.js index 9844f4d790..b89d9ed76d 100644 --- a/test/internalTest.js +++ b/test/internalTest.js @@ -1530,62 +1530,74 @@ for (const supportedVersion of mineflayer.testedVersions) { describe('string mapped action ids', () => { // 26.1 maps client_command's actionId and use_entity's hand to strings, and 1.21.6+ maps // entity_action's actionId; a numeric id fails to serialize there and never reaches the server. - function onJoin (name, act, check) { - server.on('playerJoin', async (client) => { - client.on('packet', (data, meta) => { if (meta.name === name) check(data) }) - await client.write('login', bot.test.generateLoginPacket()) - const chunk = bot.test.buildChunk() - chunk.setBlockType(vec3(0, 64, 0), registry.blocksByName.stone.id) - await client.write('map_chunk', generateChunkPacket(chunk)) - await client.write('position', { - x: 0, - y: 66, - z: 0, - dx: 0, - dy: 0, - dz: 0, - yaw: 0, - pitch: 0, - flags: bot.registry.version['>=']('1.21.3') ? {} : 0, - teleportId: 0 + const fields = (name) => registry.protocol?.play?.toServer?.types?.[name]?.[1] ?? [] + const has = (name, field) => fields(name).some(f => f.name === field) + + // Resolves with the first matching packet the server sees. Asserting inside the packet listener + // would throw on the read path and wedge the connection instead of failing the test. + function packetFrom (name, act, want = () => true) { + return new Promise((resolve, reject) => { + server.on('playerJoin', async (client) => { + try { + client.on('packet', (data, meta) => { if (meta.name === name && want(data)) resolve(data) }) + await client.write('login', bot.test.generateLoginPacket()) + const chunk = bot.test.buildChunk() + chunk.setBlockType(vec3(0, 64, 0), registry.blocksByName.stone.id) + await client.write('map_chunk', generateChunkPacket(chunk)) + await client.write('position', { + x: 0, + y: 66, + z: 0, + dx: 0, + dy: 0, + dz: 0, + yaw: 0, + pitch: 0, + flags: bot.registry.version['>=']('1.21.3') ? {} : 0, + teleportId: 0 + }) + await sleep(200) + await act() + } catch (err) { + reject(err) + } }) - await sleep(200) - await act() }) } - it('respawns with an action id the protocol accepts', (done) => { - onJoin('client_command', () => { + it('respawns with an action id the protocol accepts', async () => { + const packet = await packetFrom('client_command', () => { bot.isAlive = false bot.respawn() - }, (data) => { - assert.strictEqual(data.actionId ?? data.payload, registry.supportFeature('clientCommandUsesStringMapper') ? 'perform_respawn' : 0) - done() }) + const mapped = fields('packet_client_command').some(f => f.name === 'actionId' && f.type[0] === 'mapper') + assert.strictEqual(packet.actionId ?? packet.payload, mapped ? 'perform_respawn' : 0) }) - it('interacts with an entity using a hand the protocol accepts', (done) => { - onJoin('use_entity', () => { + it('interacts with an entity using a hand the protocol accepts', async () => { + const packet = await packetFrom('use_entity', () => { const Entity = require('prismarine-entity')(registry) const target = new Entity(42) target.position = vec3(1, 66, 0) target.height = 1.8 bot.entities[target.id] = target return bot.activateEntity(target) - }, (data) => { - assert.strictEqual(data.hand, registry.supportFeature('useEntityUsesStringMapper') ? 'main_hand' : 0) - done() }) + // Before 1.9 the packet carries no hand at all; getting it at the server is the whole assertion. + if (has('packet_use_entity', 'hand')) { + const mapped = fields('packet_use_entity').some(f => f.name === 'hand' && f.type[0] === 'mapper') + assert.strictEqual(packet.hand, mapped ? 'main_hand' : 0) + } }) - it('leaves a bed with an action id the protocol accepts', (done) => { - onJoin('entity_action', () => { + it('leaves a bed with an action id the protocol accepts', async () => { + const mapped = registry.supportFeature('entityActionUsesStringMapper') + const sneakOrSprint = new Set(mapped ? ['start_sprinting', 'stop_sprinting'] : [0, 1, 3, 4]) + const packet = await packetFrom('entity_action', () => { bot.isSleeping = true return bot.wake() - }, (data) => { - assert.strictEqual(data.actionId, registry.supportFeature('entityActionUsesStringMapper') ? 'leave_bed' : 2) - done() - }) + }, (data) => !sneakOrSprint.has(data.actionId)) + assert.strictEqual(packet.actionId, mapped ? 'leave_bed' : 2) }) })