Skip to content

Commit

Permalink
Add elytra flying support and rocket support (#3163)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
lkwilson and rom1504 authored Aug 31, 2023
1 parent 0639837 commit 010460e
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 12 deletions.
25 changes: 24 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
- [bot.foodSaturation](#botfoodsaturation)
- [bot.oxygenLevel](#botoxygenlevel)
- [bot.physics](#botphysics)
- [bot.fireworkRocketDuration](#botfireworkrocketduration)
- [bot.simpleClick.leftMouse (slot)](#botsimpleclickleftmouse-slot)
- [bot.simpleClick.rightMouse (slot)](#botsimpleclickrightmouse-slot)
- [bot.time.doDaylightCycle](#bottimedodaylightcycle)
Expand Down Expand Up @@ -197,6 +198,7 @@
- ["entityEquip" (entity)](#entityequip-entity)
- ["entitySleep" (entity)](#entitysleep-entity)
- ["entitySpawn" (entity)](#entityspawn-entity)
- ["entityElytraFlew" (entity)](#entityelytraflew-entity)
- ["itemDrop" (entity)](#itemdrop-entity)
- ["playerCollect" (collector, collected)](#playercollect-collector-collected)
- ["entityGone" (entity)](#entitygone-entity)
Expand All @@ -223,6 +225,7 @@
- ["blockBreakProgressEnd" (block, entity)](#blockbreakprogressend-block-entity)
- ["diggingCompleted" (block)](#diggingcompleted-block)
- ["diggingAborted" (block)](#diggingaborted-block)
- ["usedFirework"](#usedfirework)
- ["move"](#move)
- ["forcedMove"](#forcedmove)
- ["mount"](#mount)
Expand Down Expand Up @@ -293,6 +296,7 @@
- [bot.unequip(destination)](#botunequipdestination)
- [bot.tossStack(item)](#bottossstackitem)
- [bot.toss(itemType, metadata, count)](#bottossitemtype-metadata-count)
- [bot.elytraFly()](#botelytrafly)
- [bot.dig(block, [forceLook = true], [digFace])](#botdigblock-forcelook--true-digface)
- [bot.stopDigging()](#botstopdigging)
- [bot.digTime(block)](#botdigtimeblock)
Expand Down Expand Up @@ -1037,6 +1041,10 @@ Number in the range [0, 20] respresenting the number of water-icons known as oxy
Edit these numbers to tweak gravity, jump speed, terminal velocity, etc.
Do this at your own risk.

#### bot.fireworkRocketDuration

How many physics ticks worth of firework rocket boost are left.

#### bot.simpleClick.leftMouse (slot)

abstraction over `bot.clickWindow(slot, 0, 0)`
Expand Down Expand Up @@ -1301,6 +1309,10 @@ Fires when an attribute of an entity changes.
#### "entityEquip" (entity)
#### "entitySleep" (entity)
#### "entitySpawn" (entity)
#### "entityElytraFlew" (entity)

An entity started elytra flying.

#### "itemDrop" (entity)
#### "playerCollect" (collector, collected)

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

* `block` - the block that still exists

#### "usedfirework"

Fires when the bot uses a firework while elytra flying.

#### "move"

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

#### bot.elytraFly()

This function returns a `Promise`, with `void` as its argument once activating
elytra flight is complete. It will throw an Error if it fails.

#### bot.dig(block, [forceLook = true], [digFace])

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

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

Activates the currently held item. This is how you eat, shoot bows, throw an egg, etc.
Activates the currently held item. This is how you eat, shoot bows, throw an
egg, activate firework rockets, etc.

Optional parameter is `false` for main hand and `true` for off hand.

#### bot.deactivateItem()
Expand Down
66 changes: 66 additions & 0 deletions examples/elytra.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// This example will shoot the player that said "fire" in chat, when it is said in chat.
const mineflayer = require('mineflayer')

if (process.argv.length < 4 || process.argv.length > 6) {
console.log('Usage : node elytra.js <host> <port> [<name>] [<password>]')
process.exit(1)
}

const bot = mineflayer.createBot({
host: process.argv[2],
port: parseInt(process.argv[3]),
username: process.argv[4] ? process.argv[4] : 'elytraer',
password: process.argv[5]
})

bot.on('error', err => {
console.log(err)
})

bot.on('kicked', err => {
console.log(err)
})

bot.on('spawn', async function () {
bot.chat(`/give ${bot.username} minecraft:elytra`)
bot.chat(`/give ${bot.username} minecraft:firework_rocket 64`)

await sleep(1000)
const elytraItem = bot.inventory.slots.find(item => item?.name === 'elytra')
if (elytraItem == null) {
console.log('no elytra')
return
}
await bot.equip(elytraItem, 'torso')
const fireworkItem = bot.inventory.slots.find(item => item?.name === 'firework_rocket')
if (fireworkItem == null) {
console.log('no fireworks')
return
}
await bot.equip(fireworkItem, 'hand')
})

bot.on('chat', async (username, message) => {
if (message === 'fly') {
await bot.look(bot.entity.yaw, 50 * Math.PI / 180)
bot.setControlState('jump', true)
bot.setControlState('jump', false)
await sleep(50)

// try to fly
try {
await bot.elytraFly()
} catch (err) {
bot.chat(`Failed to fly: ${err}`)
return
}
await sleep(50)

// use rocket
bot.activateItem()
}
})

function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
5 changes: 5 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ export interface BotEvents {
entityEquip: (entity: Entity) => Promise<void> | void
entitySleep: (entity: Entity) => Promise<void> | void
entitySpawn: (entity: Entity) => Promise<void> | void
entityElytraFlew: (entity: Entity) => Promise<void> | void
usedFirework: () => Promise<void> | void
itemDrop: (entity: Entity) => Promise<void> | void
playerCollect: (collector: Entity, collected: Entity) => Promise<void> | void
entityAttributes: (entity: Entity) => Promise<void> | void
Expand Down Expand Up @@ -164,6 +166,7 @@ export interface Bot extends TypedEmitter<BotEvents> {
version: string
entity: Entity
entities: { [id: string]: Entity }
fireworkRocketDuration: number
spawnPoint: Vec3
game: GameState
player: Player
Expand Down Expand Up @@ -260,6 +263,8 @@ export interface Bot extends TypedEmitter<BotEvents> {

wake: () => Promise<void>

elytraFly: () => Promise<void>

setControlState: (control: ControlState, state: boolean) => void

getControlState: (control: ControlState) => boolean
Expand Down
122 changes: 113 additions & 9 deletions lib/plugins/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,70 @@ function inject (bot) {
}
})

bot.fireworkRocketDuration = 0
function setElytraFlyingState (entity, elytraFlying) {
let startedFlying = false
if (elytraFlying) {
startedFlying = !entity.elytraFlying
entity.elytraFlying = true
} else if (entity.elytraFlying) {
entity.elytraFlying = false
}
if (bot.fireworkRocketDuration !== 0 && entity.id === bot.entity?.id && !elytraFlying) {
bot.fireworkRocketDuration = 0
knownFireworks.splice(0, knownFireworks.length)
}

if (startedFlying) {
bot.emit('entityElytraFlew', entity)
}
}

const knownFireworks = []
function handleBotUsedFireworkRocket (fireworkEntityId, fireworkInfo) {
if (knownFireworks.includes(fireworkEntityId)) return
knownFireworks.push(fireworkEntityId)
let flightDur = 1
if (fireworkInfo?.nbtData != null) {
let nbt = fireworkInfo.nbtData
if (nbt.type === 'compound' && nbt.value.Fireworks != null) {
nbt = nbt.value.Fireworks
if (nbt.type === 'compound' && nbt.value.Flight != null) {
nbt = nbt.value.Flight
if (nbt.type === 'int') {
flightDur += nbt.value
}
}
}
}
const baseDuration = 10 * flightDur
const randomDuration = Math.floor(Math.random() * 6) + Math.floor(Math.random() * 7)
bot.fireworkRocketDuration = baseDuration + randomDuration

bot.emit('usedFirework')
}

let fireworkEntityName
if (bot.supportFeature('fireworkNamePlural')) {
fireworkEntityName = 'fireworks_rocket'
} else if (bot.supportFeature('fireworkNameSingular')) {
fireworkEntityName = 'firework_rocket'
}

let fireworkMetadataIdx
let fireworkMetadataIsOpt
if (bot.supportFeature('fireworkMetadataVarInt7')) {
fireworkMetadataIdx = 7
fireworkMetadataIsOpt = false
} else if (bot.supportFeature('fireworkMetadataOptVarInt8')) {
fireworkMetadataIdx = 8
fireworkMetadataIsOpt = true
} else if (bot.supportFeature('fireworkMetadataOptVarInt9')) {
fireworkMetadataIdx = 9
fireworkMetadataIsOpt = true
}
const hasFireworkSupport = fireworkEntityName !== undefined && fireworkMetadataIdx !== undefined && fireworkMetadataIsOpt !== undefined

bot._client.on('entity_metadata', (packet) => {
// entity metadata
const entity = fetchEntity(packet.entityId)
Expand All @@ -374,7 +438,25 @@ function inject (bot) {
if (metas.sleeping_pos || metas.pose === 2) {
bot.emit('entitySleep', entity)
}

if (hasFireworkSupport && fireworkEntityName === entity.name && metas.attached_to_target !== undefined) {
// fireworkMetadataOptVarInt9 and later is implied by
// mcDataHasEntityMetadata, so no need to check metadata index and type
// (eg fireworkMetadataOptVarInt8)
if (metas.attached_to_target !== 0) {
const entityId = metas.attached_to_target - 1
if (entityId === bot.entity?.id) {
handleBotUsedFireworkRocket(entity.id, metas.fireworks_item)
}
}
}

if (metas.shared_flags != null) {
if (bot.supportFeature('hasElytraFlying')) {
const elytraFlying = metas.shared_flags & 0x80
setElytraFlyingState(entity, Boolean(elytraFlying))
}

if (metas.shared_flags & 2) {
entity.crouching = true
bot.emit('entityCrouch', entity)
Expand All @@ -396,16 +478,38 @@ function inject (bot) {
bot.emit('entitySleep', entity)
}

const bitField = packet.metadata.find(p => p.key === 0)
if (bitField === undefined) {
return
if (hasFireworkSupport && fireworkEntityName === entity.name) {
const attachedToTarget = packet.metadata.find(e => e.key === fireworkMetadataIdx)
if (attachedToTarget !== undefined) {
let entityId
if (fireworkMetadataIsOpt) {
if (attachedToTarget.value !== 0) {
entityId = attachedToTarget.value - 1
} // else, not attached to an entity
} else {
entityId = attachedToTarget.value
}
if (entityId !== undefined && entityId === bot.entity?.id) {
const fireworksItem = packet.metadata.find(e => e.key === (fireworkMetadataIdx - 1))
handleBotUsedFireworkRocket(entity.id, fireworksItem?.value)
}
}
}
if ((bitField.value & 2) !== 0) {
entity.crouching = true
bot.emit('entityCrouch', entity)
} else if (entity.crouching) { // prevent the initial entity_metadata packet from firing off an uncrouch event
entity.crouching = false
bot.emit('entityUncrouch', entity)

const bitField = packet.metadata.find(p => p.key === 0)
if (bitField !== undefined) {
if (bot.supportFeature('hasElytraFlying')) {
const elytraFlying = bitField.value & 0x80
setElytraFlyingState(entity, Boolean(elytraFlying))
}

if ((bitField.value & 2) !== 0) {
entity.crouching = true
bot.emit('entityCrouch', entity)
} else if (entity.crouching) { // prevent the initial entity_metadata packet from firing off an uncrouch event
entity.crouching = false
bot.emit('entityUncrouch', entity)
}
}
}
})
Expand Down
38 changes: 38 additions & 0 deletions lib/plugins/physics.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,44 @@ function inject (bot, { physicsEnabled, maxCatchupTicks }) {

bot.physics = physics

function getEffectLevel (mcData, effectName, effects) {
const effectDescriptor = mcData.effectsByName[effectName]
if (!effectDescriptor) {
return 0
}
const effectInfo = effects[effectDescriptor.id]
if (!effectInfo) {
return 0
}
return effectInfo.amplifier + 1
}

bot.elytraFly = async () => {
if (bot.entity.elytraFlying) {
throw new Error('Already elytra flying')
} else if (bot.entity.onGround) {
throw new Error('Unable to fly from ground')
} else if (bot.entity.isInWater) {
throw new Error('Unable to elytra fly while in water')
}

const mcData = require('minecraft-data')(bot.version)
if (getEffectLevel(mcData, 'Levitation', bot.entity.effects) > 0) {
throw new Error('Unable to elytra fly with levitation effect')
}

const torsoSlot = bot.getEquipmentDestSlot('torso')
const item = bot.inventory.slots[torsoSlot]
if (item == null || item.name !== 'elytra') {
throw new Error('Elytra must be equip to start flying')
}
bot._client.write('entity_action', {
entityId: bot.entity.id,
actionId: 8,
jumpBoost: 0
})
}

bot.setControlState = (control, state) => {
assert.ok(control in controlState, `invalid control: ${control}`)
assert.ok(typeof state === 'boolean', `invalid state: ${state}`)
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
},
"license": "MIT",
"dependencies": {
"minecraft-data": "^3.37.0",
"minecraft-data": "^3.44.0",
"minecraft-protocol": "^1.44.0",
"prismarine-biome": "^1.1.1",
"prismarine-block": "^1.17.0",
Expand All @@ -30,7 +30,7 @@
"prismarine-entity": "^2.3.0",
"prismarine-item": "^1.14.0",
"prismarine-nbt": "^2.0.0",
"prismarine-physics": "^1.7.0",
"prismarine-physics": "^1.8.0",
"prismarine-recipe": "^1.3.0",
"prismarine-registry": "^1.5.0",
"prismarine-windows": "^2.8.0",
Expand Down
Loading

0 comments on commit 010460e

Please sign in to comment.