Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/plugins/bed.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
}
Expand Down
3 changes: 2 additions & 1 deletion lib/plugins/creative.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -46,7 +47,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: mapsIdsToNames(bot.registry, 'packet_client_command', 'actionId') ? 'request_stats' : 1 })
})
}

Expand Down
3 changes: 2 additions & 1 deletion lib/plugins/game.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const nbt = require('prismarine-nbt')
const { mapsIdsToNames } = require('../protocol_ids')
module.exports = inject

const difficultyNames = ['peaceful', 'easy', 'normal', 'hard']
Expand Down Expand Up @@ -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', { action: 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)
Expand Down
3 changes: 2 additions & 1 deletion lib/plugins/health.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const { mapsIdsToNames } = require('../protocol_ids')
module.exports = inject

function inject (bot, options) {
Expand Down Expand Up @@ -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: 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
Expand Down
7 changes: 5 additions & 2 deletions lib/plugins/inventory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -245,14 +246,16 @@ 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)
bot._client.write('use_entity', {
target: entity.id,
mouse: 0, // interact with entity
sneaking: false,
hand: 0, // interact with the main hand
hand: mainHand, // main hand
location: new Vec3(0, 0, 0)
})
}
Expand All @@ -264,7 +267,7 @@ function inject (bot, { hideErrors }) {
target: entity.id,
mouse: 2, // interact with entity at
sneaking: false,
hand: 0, // interact with the main hand
hand: mainHand, // main hand
x: position.x - entity.position.x,
y: position.y - entity.position.y,
z: position.z - entity.position.z,
Expand Down
12 changes: 12 additions & 0 deletions lib/protocol_ids.js
Original file line number Diff line number Diff line change
@@ -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 }
74 changes: 74 additions & 0 deletions test/internalTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,80 @@ 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.
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)
}
})
})
}

it('respawns with an action id the protocol accepts', async () => {
const packet = await packetFrom('client_command', () => {
bot.isAlive = false
bot.respawn()
})
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', 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)
})
// 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', 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) => !sneakOrSprint.has(data.actionId))
assert.strictEqual(packet.actionId, mapped ? 'leave_bed' : 2)
})
})

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
Expand Down
Loading