Skip to content

Commit 010460e

Browse files
lkwilsonrom1504
andauthored
Add elytra flying support and rocket support (#3163)
* Add elytra flying support and rocket support * Add example and docs * Elytra and firework handling moved to entities * Add support for older versions and add test * Documentation * Bump deps * Bump mc data * Update package.json --------- Co-authored-by: Romain Beaumont <[email protected]>
1 parent 0639837 commit 010460e

File tree

8 files changed

+298
-12
lines changed

8 files changed

+298
-12
lines changed

docs/api.md

+24-1
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@
136136
- [bot.foodSaturation](#botfoodsaturation)
137137
- [bot.oxygenLevel](#botoxygenlevel)
138138
- [bot.physics](#botphysics)
139+
- [bot.fireworkRocketDuration](#botfireworkrocketduration)
139140
- [bot.simpleClick.leftMouse (slot)](#botsimpleclickleftmouse-slot)
140141
- [bot.simpleClick.rightMouse (slot)](#botsimpleclickrightmouse-slot)
141142
- [bot.time.doDaylightCycle](#bottimedodaylightcycle)
@@ -197,6 +198,7 @@
197198
- ["entityEquip" (entity)](#entityequip-entity)
198199
- ["entitySleep" (entity)](#entitysleep-entity)
199200
- ["entitySpawn" (entity)](#entityspawn-entity)
201+
- ["entityElytraFlew" (entity)](#entityelytraflew-entity)
200202
- ["itemDrop" (entity)](#itemdrop-entity)
201203
- ["playerCollect" (collector, collected)](#playercollect-collector-collected)
202204
- ["entityGone" (entity)](#entitygone-entity)
@@ -223,6 +225,7 @@
223225
- ["blockBreakProgressEnd" (block, entity)](#blockbreakprogressend-block-entity)
224226
- ["diggingCompleted" (block)](#diggingcompleted-block)
225227
- ["diggingAborted" (block)](#diggingaborted-block)
228+
- ["usedFirework"](#usedfirework)
226229
- ["move"](#move)
227230
- ["forcedMove"](#forcedmove)
228231
- ["mount"](#mount)
@@ -293,6 +296,7 @@
293296
- [bot.unequip(destination)](#botunequipdestination)
294297
- [bot.tossStack(item)](#bottossstackitem)
295298
- [bot.toss(itemType, metadata, count)](#bottossitemtype-metadata-count)
299+
- [bot.elytraFly()](#botelytrafly)
296300
- [bot.dig(block, [forceLook = true], [digFace])](#botdigblock-forcelook--true-digface)
297301
- [bot.stopDigging()](#botstopdigging)
298302
- [bot.digTime(block)](#botdigtimeblock)
@@ -1037,6 +1041,10 @@ Number in the range [0, 20] respresenting the number of water-icons known as oxy
10371041
Edit these numbers to tweak gravity, jump speed, terminal velocity, etc.
10381042
Do this at your own risk.
10391043

1044+
#### bot.fireworkRocketDuration
1045+
1046+
How many physics ticks worth of firework rocket boost are left.
1047+
10401048
#### bot.simpleClick.leftMouse (slot)
10411049

10421050
abstraction over `bot.clickWindow(slot, 0, 0)`
@@ -1301,6 +1309,10 @@ Fires when an attribute of an entity changes.
13011309
#### "entityEquip" (entity)
13021310
#### "entitySleep" (entity)
13031311
#### "entitySpawn" (entity)
1312+
#### "entityElytraFlew" (entity)
1313+
1314+
An entity started elytra flying.
1315+
13041316
#### "itemDrop" (entity)
13051317
#### "playerCollect" (collector, collected)
13061318

@@ -1417,6 +1429,10 @@ This occurs whether the process was completed or aborted.
14171429

14181430
* `block` - the block that still exists
14191431

1432+
#### "usedfirework"
1433+
1434+
Fires when the bot uses a firework while elytra flying.
1435+
14201436
#### "move"
14211437

14221438
Fires when the bot moves. If you want the current position, use
@@ -1851,6 +1867,11 @@ This function returns a `Promise`, with `void` as its argument once tossing is c
18511867
to match any metadata
18521868
* `count` - how many you want to toss. `null` is an alias for `1`.
18531869

1870+
#### bot.elytraFly()
1871+
1872+
This function returns a `Promise`, with `void` as its argument once activating
1873+
elytra flight is complete. It will throw an Error if it fails.
1874+
18541875
#### bot.dig(block, [forceLook = true], [digFace])
18551876

18561877
This function returns a `Promise`, with `void` as its argument when the block is broken or you are interrupted.
@@ -1946,7 +1967,9 @@ Use fishing rod
19461967

19471968
#### bot.activateItem(offHand=false)
19481969

1949-
Activates the currently held item. This is how you eat, shoot bows, throw an egg, etc.
1970+
Activates the currently held item. This is how you eat, shoot bows, throw an
1971+
egg, activate firework rockets, etc.
1972+
19501973
Optional parameter is `false` for main hand and `true` for off hand.
19511974

19521975
#### bot.deactivateItem()

examples/elytra.js

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// This example will shoot the player that said "fire" in chat, when it is said in chat.
2+
const mineflayer = require('mineflayer')
3+
4+
if (process.argv.length < 4 || process.argv.length > 6) {
5+
console.log('Usage : node elytra.js <host> <port> [<name>] [<password>]')
6+
process.exit(1)
7+
}
8+
9+
const bot = mineflayer.createBot({
10+
host: process.argv[2],
11+
port: parseInt(process.argv[3]),
12+
username: process.argv[4] ? process.argv[4] : 'elytraer',
13+
password: process.argv[5]
14+
})
15+
16+
bot.on('error', err => {
17+
console.log(err)
18+
})
19+
20+
bot.on('kicked', err => {
21+
console.log(err)
22+
})
23+
24+
bot.on('spawn', async function () {
25+
bot.chat(`/give ${bot.username} minecraft:elytra`)
26+
bot.chat(`/give ${bot.username} minecraft:firework_rocket 64`)
27+
28+
await sleep(1000)
29+
const elytraItem = bot.inventory.slots.find(item => item?.name === 'elytra')
30+
if (elytraItem == null) {
31+
console.log('no elytra')
32+
return
33+
}
34+
await bot.equip(elytraItem, 'torso')
35+
const fireworkItem = bot.inventory.slots.find(item => item?.name === 'firework_rocket')
36+
if (fireworkItem == null) {
37+
console.log('no fireworks')
38+
return
39+
}
40+
await bot.equip(fireworkItem, 'hand')
41+
})
42+
43+
bot.on('chat', async (username, message) => {
44+
if (message === 'fly') {
45+
await bot.look(bot.entity.yaw, 50 * Math.PI / 180)
46+
bot.setControlState('jump', true)
47+
bot.setControlState('jump', false)
48+
await sleep(50)
49+
50+
// try to fly
51+
try {
52+
await bot.elytraFly()
53+
} catch (err) {
54+
bot.chat(`Failed to fly: ${err}`)
55+
return
56+
}
57+
await sleep(50)
58+
59+
// use rocket
60+
bot.activateItem()
61+
}
62+
})
63+
64+
function sleep (ms) {
65+
return new Promise(resolve => setTimeout(resolve, ms))
66+
}

index.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ export interface BotEvents {
9292
entityEquip: (entity: Entity) => Promise<void> | void
9393
entitySleep: (entity: Entity) => Promise<void> | void
9494
entitySpawn: (entity: Entity) => Promise<void> | void
95+
entityElytraFlew: (entity: Entity) => Promise<void> | void
96+
usedFirework: () => Promise<void> | void
9597
itemDrop: (entity: Entity) => Promise<void> | void
9698
playerCollect: (collector: Entity, collected: Entity) => Promise<void> | void
9799
entityAttributes: (entity: Entity) => Promise<void> | void
@@ -164,6 +166,7 @@ export interface Bot extends TypedEmitter<BotEvents> {
164166
version: string
165167
entity: Entity
166168
entities: { [id: string]: Entity }
169+
fireworkRocketDuration: number
167170
spawnPoint: Vec3
168171
game: GameState
169172
player: Player
@@ -260,6 +263,8 @@ export interface Bot extends TypedEmitter<BotEvents> {
260263

261264
wake: () => Promise<void>
262265

266+
elytraFly: () => Promise<void>
267+
263268
setControlState: (control: ControlState, state: boolean) => void
264269

265270
getControlState: (control: ControlState) => boolean

lib/plugins/entities.js

+113-9
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,70 @@ function inject (bot) {
358358
}
359359
})
360360

361+
bot.fireworkRocketDuration = 0
362+
function setElytraFlyingState (entity, elytraFlying) {
363+
let startedFlying = false
364+
if (elytraFlying) {
365+
startedFlying = !entity.elytraFlying
366+
entity.elytraFlying = true
367+
} else if (entity.elytraFlying) {
368+
entity.elytraFlying = false
369+
}
370+
if (bot.fireworkRocketDuration !== 0 && entity.id === bot.entity?.id && !elytraFlying) {
371+
bot.fireworkRocketDuration = 0
372+
knownFireworks.splice(0, knownFireworks.length)
373+
}
374+
375+
if (startedFlying) {
376+
bot.emit('entityElytraFlew', entity)
377+
}
378+
}
379+
380+
const knownFireworks = []
381+
function handleBotUsedFireworkRocket (fireworkEntityId, fireworkInfo) {
382+
if (knownFireworks.includes(fireworkEntityId)) return
383+
knownFireworks.push(fireworkEntityId)
384+
let flightDur = 1
385+
if (fireworkInfo?.nbtData != null) {
386+
let nbt = fireworkInfo.nbtData
387+
if (nbt.type === 'compound' && nbt.value.Fireworks != null) {
388+
nbt = nbt.value.Fireworks
389+
if (nbt.type === 'compound' && nbt.value.Flight != null) {
390+
nbt = nbt.value.Flight
391+
if (nbt.type === 'int') {
392+
flightDur += nbt.value
393+
}
394+
}
395+
}
396+
}
397+
const baseDuration = 10 * flightDur
398+
const randomDuration = Math.floor(Math.random() * 6) + Math.floor(Math.random() * 7)
399+
bot.fireworkRocketDuration = baseDuration + randomDuration
400+
401+
bot.emit('usedFirework')
402+
}
403+
404+
let fireworkEntityName
405+
if (bot.supportFeature('fireworkNamePlural')) {
406+
fireworkEntityName = 'fireworks_rocket'
407+
} else if (bot.supportFeature('fireworkNameSingular')) {
408+
fireworkEntityName = 'firework_rocket'
409+
}
410+
411+
let fireworkMetadataIdx
412+
let fireworkMetadataIsOpt
413+
if (bot.supportFeature('fireworkMetadataVarInt7')) {
414+
fireworkMetadataIdx = 7
415+
fireworkMetadataIsOpt = false
416+
} else if (bot.supportFeature('fireworkMetadataOptVarInt8')) {
417+
fireworkMetadataIdx = 8
418+
fireworkMetadataIsOpt = true
419+
} else if (bot.supportFeature('fireworkMetadataOptVarInt9')) {
420+
fireworkMetadataIdx = 9
421+
fireworkMetadataIsOpt = true
422+
}
423+
const hasFireworkSupport = fireworkEntityName !== undefined && fireworkMetadataIdx !== undefined && fireworkMetadataIsOpt !== undefined
424+
361425
bot._client.on('entity_metadata', (packet) => {
362426
// entity metadata
363427
const entity = fetchEntity(packet.entityId)
@@ -374,7 +438,25 @@ function inject (bot) {
374438
if (metas.sleeping_pos || metas.pose === 2) {
375439
bot.emit('entitySleep', entity)
376440
}
441+
442+
if (hasFireworkSupport && fireworkEntityName === entity.name && metas.attached_to_target !== undefined) {
443+
// fireworkMetadataOptVarInt9 and later is implied by
444+
// mcDataHasEntityMetadata, so no need to check metadata index and type
445+
// (eg fireworkMetadataOptVarInt8)
446+
if (metas.attached_to_target !== 0) {
447+
const entityId = metas.attached_to_target - 1
448+
if (entityId === bot.entity?.id) {
449+
handleBotUsedFireworkRocket(entity.id, metas.fireworks_item)
450+
}
451+
}
452+
}
453+
377454
if (metas.shared_flags != null) {
455+
if (bot.supportFeature('hasElytraFlying')) {
456+
const elytraFlying = metas.shared_flags & 0x80
457+
setElytraFlyingState(entity, Boolean(elytraFlying))
458+
}
459+
378460
if (metas.shared_flags & 2) {
379461
entity.crouching = true
380462
bot.emit('entityCrouch', entity)
@@ -396,16 +478,38 @@ function inject (bot) {
396478
bot.emit('entitySleep', entity)
397479
}
398480

399-
const bitField = packet.metadata.find(p => p.key === 0)
400-
if (bitField === undefined) {
401-
return
481+
if (hasFireworkSupport && fireworkEntityName === entity.name) {
482+
const attachedToTarget = packet.metadata.find(e => e.key === fireworkMetadataIdx)
483+
if (attachedToTarget !== undefined) {
484+
let entityId
485+
if (fireworkMetadataIsOpt) {
486+
if (attachedToTarget.value !== 0) {
487+
entityId = attachedToTarget.value - 1
488+
} // else, not attached to an entity
489+
} else {
490+
entityId = attachedToTarget.value
491+
}
492+
if (entityId !== undefined && entityId === bot.entity?.id) {
493+
const fireworksItem = packet.metadata.find(e => e.key === (fireworkMetadataIdx - 1))
494+
handleBotUsedFireworkRocket(entity.id, fireworksItem?.value)
495+
}
496+
}
402497
}
403-
if ((bitField.value & 2) !== 0) {
404-
entity.crouching = true
405-
bot.emit('entityCrouch', entity)
406-
} else if (entity.crouching) { // prevent the initial entity_metadata packet from firing off an uncrouch event
407-
entity.crouching = false
408-
bot.emit('entityUncrouch', entity)
498+
499+
const bitField = packet.metadata.find(p => p.key === 0)
500+
if (bitField !== undefined) {
501+
if (bot.supportFeature('hasElytraFlying')) {
502+
const elytraFlying = bitField.value & 0x80
503+
setElytraFlyingState(entity, Boolean(elytraFlying))
504+
}
505+
506+
if ((bitField.value & 2) !== 0) {
507+
entity.crouching = true
508+
bot.emit('entityCrouch', entity)
509+
} else if (entity.crouching) { // prevent the initial entity_metadata packet from firing off an uncrouch event
510+
entity.crouching = false
511+
bot.emit('entityUncrouch', entity)
512+
}
409513
}
410514
}
411515
})

lib/plugins/physics.js

+38
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,44 @@ function inject (bot, { physicsEnabled, maxCatchupTicks }) {
192192

193193
bot.physics = physics
194194

195+
function getEffectLevel (mcData, effectName, effects) {
196+
const effectDescriptor = mcData.effectsByName[effectName]
197+
if (!effectDescriptor) {
198+
return 0
199+
}
200+
const effectInfo = effects[effectDescriptor.id]
201+
if (!effectInfo) {
202+
return 0
203+
}
204+
return effectInfo.amplifier + 1
205+
}
206+
207+
bot.elytraFly = async () => {
208+
if (bot.entity.elytraFlying) {
209+
throw new Error('Already elytra flying')
210+
} else if (bot.entity.onGround) {
211+
throw new Error('Unable to fly from ground')
212+
} else if (bot.entity.isInWater) {
213+
throw new Error('Unable to elytra fly while in water')
214+
}
215+
216+
const mcData = require('minecraft-data')(bot.version)
217+
if (getEffectLevel(mcData, 'Levitation', bot.entity.effects) > 0) {
218+
throw new Error('Unable to elytra fly with levitation effect')
219+
}
220+
221+
const torsoSlot = bot.getEquipmentDestSlot('torso')
222+
const item = bot.inventory.slots[torsoSlot]
223+
if (item == null || item.name !== 'elytra') {
224+
throw new Error('Elytra must be equip to start flying')
225+
}
226+
bot._client.write('entity_action', {
227+
entityId: bot.entity.id,
228+
actionId: 8,
229+
jumpBoost: 0
230+
})
231+
}
232+
195233
bot.setControlState = (control, state) => {
196234
assert.ok(control in controlState, `invalid control: ${control}`)
197235
assert.ok(typeof state === 'boolean', `invalid state: ${state}`)

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
},
2222
"license": "MIT",
2323
"dependencies": {
24-
"minecraft-data": "^3.37.0",
24+
"minecraft-data": "^3.44.0",
2525
"minecraft-protocol": "^1.44.0",
2626
"prismarine-biome": "^1.1.1",
2727
"prismarine-block": "^1.17.0",
@@ -30,7 +30,7 @@
3030
"prismarine-entity": "^2.3.0",
3131
"prismarine-item": "^1.14.0",
3232
"prismarine-nbt": "^2.0.0",
33-
"prismarine-physics": "^1.7.0",
33+
"prismarine-physics": "^1.8.0",
3434
"prismarine-recipe": "^1.3.0",
3535
"prismarine-registry": "^1.5.0",
3636
"prismarine-windows": "^2.8.0",

0 commit comments

Comments
 (0)