Skip to content
Open
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ jobs:
with:
node-version: 24
- name: Setup Java JDK
uses: actions/setup-java@v1.4.3
uses: actions/setup-java@v4
with:
java-version: 21
java-package: jre
distribution: 'temurin'
java-version: ${{ contains(matrix.versions, '26.1.2') && '26' || '21' }}
- name: Install Dependencies
run: npm install

Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ First time using Node.js? You may want to start with the [tutorial](tutorial.md)

## Features

* Supports Minecraft 1.8 to 1.21.11 (1.8, 1.9, 1.10, 1.11, 1.12, 1.13, 1.14, 1.15, 1.16, 1.17, 1.18, 1.19, 1.20, 1.21, 1.21.9, 1.21.11) <!--version-->
* Supports Minecraft 1.8 to 26.1.2 (1.8, 1.9, 1.10, 1.11, 1.12, 1.13, 1.14, 1.15, 1.16, 1.17, 1.18, 1.19, 1.20, 1.21, 1.21.9, 1.21.11, 26.1.2) <!--version-->
* Entity knowledge and tracking.
* Block knowledge. You can query the world around you. Milliseconds to find any block.
* Physics and movement - handle all bounding boxes
Expand Down
22 changes: 22 additions & 0 deletions lib/plugins/bed.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,28 @@ function inject (bot) {
headOffset: new Vec3(0, 0, 1)
}

const properties = bedBlock.getProperties?.()
if (properties?.part) {
metadata.part = properties.part === 'head'
metadata.occupied = properties.occupied === true || properties.occupied === 'true'

switch (properties.facing) {
case 'west':
metadata.facing = 1
metadata.headOffset.set(-1, 0, 0)
break
case 'north':
metadata.facing = 2
metadata.headOffset.set(0, 0, -1)
break
case 'east':
metadata.facing = 3
metadata.headOffset.set(1, 0, 0)
break
}
return metadata
}

if (bot.supportFeature('blockStateId')) {
const state = bedBlock.stateId - bot.registry.blocksByStateId[bedBlock.stateId].minStateId
const bitMetadata = state.toString(2).padStart(4, '0') // FACING (first 2 bits), PART (3rd bit), OCCUPIED (4th bit)
Expand Down
25 changes: 24 additions & 1 deletion lib/plugins/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,19 +422,23 @@ function inject (bot) {
bot.emit('entityElytraFlew', entity)
}
}
bot._setElytraFlyingState = setElytraFlyingState

const knownFireworks = new Set()
function handleBotUsedFireworkRocket (fireworkEntityId, fireworkInfo) {
if (knownFireworks.has(fireworkEntityId)) return
knownFireworks.add(fireworkEntityId)
let flightDur = fireworkInfo?.nbtData?.value?.Fireworks?.value?.Flight.value ?? 1
let flightDur = fireworkInfo?.nbtData?.value?.Fireworks?.value?.Flight.value ??
fireworkInfo?.nbt?.value?.Fireworks?.value?.Flight.value ??
1
if (typeof flightDur !== 'number') { flightDur = 1 }
const baseDuration = 10 * (flightDur + 1)
const randomDuration = Math.floor(Math.random() * 6) + Math.floor(Math.random() * 7)
bot.fireworkRocketDuration = baseDuration + randomDuration

bot.emit('usedFirework', fireworkEntityId)
}
bot._handleFireworkRocketUse = handleBotUsedFireworkRocket

let fireworkEntityName
if (bot.supportFeature('fireworkNamePlural')) {
Expand Down Expand Up @@ -911,6 +915,25 @@ function inject (bot) {

function useEntity (target, leftClick, x, y, z) {
const sneaking = bot.getControlState('sneak')
if (bot.supportFeature('useEntityUsesEntityId')) {
if (leftClick === 1 && bot.supportFeature('attackUsesOwnPacket')) {
bot._client.write('attack', { entityId: target.id })
return
}

bot._client.write('use_entity', {
entityId: target.id,
hand: 0,
location: {
x: x ?? 0,
y: y ?? 0,
z: z ?? 0
},
usingSecondaryAction: sneaking
})
return
}

if (x && y && z) {
bot._client.write('use_entity', {
target: target.id,
Expand Down
28 changes: 28 additions & 0 deletions lib/plugins/inventory.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ function inject (bot, { hideErrors }) {
function activateItem (offHand = false) {
bot.usingHeldItem = true
sequence++
const heldItem = offHand ? bot.inventory.slots[45] : bot.heldItem

if (bot.supportFeature('useItemWithBlockPlace')) {
bot._client.write('block_place', {
Expand All @@ -135,6 +136,9 @@ function inject (bot, { hideErrors }) {
}
})
}
if (heldItem?.name === 'firework_rocket' && bot.entity?.elytraFlying) {
bot._handleFireworkRocketUse?.(`local:${sequence}`, heldItem)
}
}

function deactivateItem () {
Expand Down Expand Up @@ -243,6 +247,16 @@ function inject (bot, { hideErrors }) {
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)
if (bot.supportFeature('useEntityUsesEntityId')) {
bot._client.write('use_entity', {
entityId: entity.id,
hand: 0,
location: { x: 0, y: 0, z: 0 },
usingSecondaryAction: false
})
return
}

bot._client.write('use_entity', {
target: entity.id,
mouse: 0, // interact with entity
Expand All @@ -254,6 +268,20 @@ function inject (bot, { hideErrors }) {
async function activateEntityAt (entity, position) {
// TODO: tell the server that we are not sneaking while doing this
await bot.lookAt(position, false)
if (bot.supportFeature('useEntityUsesEntityId')) {
bot._client.write('use_entity', {
entityId: entity.id,
hand: 0,
location: {
x: position.x - entity.position.x,
y: position.y - entity.position.y,
z: position.z - entity.position.z
},
usingSecondaryAction: false
})
return
}

bot._client.write('use_entity', {
target: entity.id,
mouse: 2, // interact with entity at
Expand Down
7 changes: 7 additions & 0 deletions lib/plugins/physics.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ function inject (bot, { physicsEnabled, maxCatchupTicks }) {
bot.emit('physicsTick')
bot.emit('physicTick') // Deprecated, only exists to support old plugins. May be removed in the future
}
if (bot.entity.elytraFlying && bot.entity.onGround) {
bot._setElytraFlyingState?.(bot.entity, false)
}
if (bot.fireworkRocketDuration > 0) {
bot.fireworkRocketDuration--
}
if (shouldUsePhysics) {
updatePosition(now)
}
Expand Down Expand Up @@ -240,6 +246,7 @@ function inject (bot, { physicsEnabled, maxCatchupTicks }) {
actionId: bot.supportFeature('entityActionUsesStringMapper') ? 'start_elytra_flying' : 8,
jumpBoost: 0
})
bot._setElytraFlyingState?.(bot.entity, true)
}

bot.setControlState = (control, state) => {
Expand Down
80 changes: 77 additions & 3 deletions lib/plugins/time.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,36 @@ function inject (bot) {
age: null
}
bot._client.on('update_time', (packet) => {
const time = longToBigInt(packet.time)
const age = longToBigInt(packet.age)
const doDaylightCycle = packet.tickDayTime !== undefined ? !!packet.tickDayTime : time >= 0n
const clockUpdate = readClockUpdates(packet.clockUpdates)
const packetAge = packet.age ?? packet.gameTime ?? packet.time
if (packetAge == null) return

const age = longToBigInt(packetAge)
const previousAge = bot.time.bigAge
const previousTime = bot.time.bigTime

let packetTime = packet.time ?? clockUpdate?.time
const daylightBasis = packetTime ?? packet.gameTime ?? packet.age
const packetDayTime = packetTime == null ? null : longToBigInt(packetTime)
let doDaylightCycle
if (packet.tickDayTime !== undefined) {
doDaylightCycle = !!packet.tickDayTime
} else if (clockUpdate?.tickRate !== undefined) {
doDaylightCycle = clockUpdate.tickRate > 0
} else if (packetDayTime != null) {
doDaylightCycle = packetDayTime >= 0n
} else {
doDaylightCycle = bot.time.doDaylightCycle ?? (longToBigInt(daylightBasis) >= 0n)
}

if (packetTime == null && previousTime != null && previousAge != null) {
const ageDelta = age - previousAge
packetTime = doDaylightCycle ? previousTime + ageDelta : previousTime
}
packetTime ??= packet.gameTime ?? packet.age
if (packetTime == null) return

const time = packetDayTime ?? longToBigInt(packetTime)
// When doDaylightCycle is false, we need to take the absolute value of time
const finalTime = doDaylightCycle ? time : (time < 0n ? -time : time)

Expand All @@ -33,6 +60,53 @@ function inject (bot) {
})
}

function readClockUpdates (buffer) {
if (!buffer || buffer.length === 0) return null
let offset = 0
const count = readVarInt(buffer, offset)
offset = count.offset

for (let i = 0; i < count.value && offset < buffer.length; i++) {
const type = readVarInt(buffer, offset)
offset = type.offset
const time = readVarLong(buffer, offset)
offset = time.offset
const tickRate = offset + 8 <= buffer.length ? buffer.readFloatBE(offset + 4) : undefined
offset += 8

if (type.value === 0) return { time: time.value, tickRate }
}
return null
}

function readVarInt (buffer, offset) {
let value = 0
let shift = 0
let cursor = offset
while (cursor < buffer.length) {
const byte = buffer[cursor++]
value |= (byte & 0x7f) << shift
if ((byte & 0x80) === 0) return { value, offset: cursor }
shift += 7
}
throw new Error('Invalid varint in update_time clockUpdates')
}

function readVarLong (buffer, offset) {
let value = 0n
let shift = 0n
let cursor = offset
while (cursor < buffer.length) {
const byte = buffer[cursor++]
value |= BigInt(byte & 0x7f) << shift
if ((byte & 0x80) === 0) return { value: BigInt.asIntN(64, value), offset: cursor }
shift += 7n
}
throw new Error('Invalid varlong in update_time clockUpdates')
}

function longToBigInt (arr) {
if (typeof arr === 'bigint') return BigInt.asIntN(64, arr)
if (typeof arr === 'number') return BigInt.asIntN(64, BigInt(arr))
return BigInt.asIntN(64, (BigInt(arr[0]) << 32n)) | BigInt(arr[1])
}
2 changes: 1 addition & 1 deletion lib/version.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const testedVersions = ['1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20.1', '1.20.2', '1.20.4', '1.20.6', '1.21.1', '1.21.3', '1.21.4', '1.21.5', '1.21.6', '1.21.8', '1.21.9', '1.21.11']
const testedVersions = ['1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20.1', '1.20.2', '1.20.4', '1.20.6', '1.21.1', '1.21.3', '1.21.4', '1.21.5', '1.21.6', '1.21.8', '1.21.9', '1.21.11', '26.1.2']
module.exports = {

testedVersions,
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@
},
"license": "MIT",
"dependencies": {
"minecraft-data": "^3.108.0",
"minecraft-protocol": "^1.66.0",
"minecraft-data": "github:mneuhaus/node-minecraft-data#add-26.1.2-data-wrapper",
"minecraft-protocol": "github:mneuhaus/node-minecraft-protocol#pc26_1_2-clean",
"mojangson": "^2.0.4",
"prismarine-biome": "^1.1.1",
"prismarine-block": "^1.22.0",
"prismarine-chat": "^1.7.1",
"prismarine-chunk": "^1.39.0",
"prismarine-chunk": "github:mneuhaus/prismarine-chunk#pc26_1_2-fluid-count",
"prismarine-entity": "^2.5.0",
"prismarine-item": "^1.17.0",
"prismarine-nbt": "^2.0.0",
"prismarine-physics": "^1.9.0",
"prismarine-physics": "github:mneuhaus/prismarine-physics#add-26.1-physics-features",
"prismarine-recipe": "^1.5.0",
"prismarine-registry": "^1.10.0",
"prismarine-windows": "^2.9.0",
Expand Down
43 changes: 31 additions & 12 deletions test/externalTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,31 @@ const download = require('minecraft-wrap').download

const MC_SERVER_PATH = path.join(__dirname, 'server')

async function pingServerWithRetry (port, version, attempts = 3) {
let lastError
for (let attempt = 1; attempt <= attempts; attempt++) {
try {
console.log(`pinging ${version} port : ${port} attempt ${attempt}/${attempts}`)
const results = await mc.ping({
port,
host: '127.0.0.1',
version,
closeTimeout: 10000,
noPongTimeout: 2000
})
console.log('pong')
assert.ok(results.latency >= 0)
assert.ok(results.latency <= 1000)
return
} catch (err) {
lastError = err
console.log(`ping failed attempt ${attempt}/${attempts}: ${err.message}`)
if (attempt < attempts) await new Promise(resolve => setTimeout(resolve, 1000))
}
}
throw lastError
}

for (const supportedVersion of mineflayer.testedVersions) {
let PORT = 25565
const registry = require('prismarine-registry')(supportedVersion)
Expand Down Expand Up @@ -91,20 +116,14 @@ for (const supportedVersion of mineflayer.testedVersions) {
return
}
propOverrides['server-port'] = PORT
wrap.startServer(propOverrides, (err) => {
wrap.startServer(propOverrides, async (err) => {
if (err) return done(err)
console.log(`pinging ${version.minecraftVersion} port : ${PORT}`)
mc.ping({
port: PORT,
host: '127.0.0.1',
version: supportedVersion
}, (err, results) => {
if (err) return done(err)
console.log('pong')
assert.ok(results.latency >= 0)
assert.ok(results.latency <= 1000)
try {
await pingServerWithRetry(PORT, supportedVersion)
begin()
})
} catch (err) {
done(err)
}
})
})
} else begin()
Expand Down
6 changes: 6 additions & 0 deletions test/externalTests/elytra.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@ module.exports = () => async (bot) => {
const Item = require('prismarine-item')(bot.registry)

await bot.test.setInventorySlot(6, new Item(bot.registry.itemsByName.elytra.id, 1))
// 1.21.6+ does not acknowledge creative armor-slot writes, so ensure the
// server-side armor slot is populated before testing elytra flight.
bot.chat(`/item replace entity ${bot.username} armor.chest with minecraft:elytra`)
await bot.test.wait(500)
if (supportsFireworkRockets) {
const fireworkItem = bot.registry.itemsArray.find(item => item.displayName === 'Firework Rocket')
assert.ok(fireworkItem !== undefined)
await bot.test.setInventorySlot(36, new Item(fireworkItem.id, 64))
bot.chat(`/item replace entity ${bot.username} hotbar.0 with minecraft:firework_rocket 64`)
await bot.test.wait(500)
}
await bot.test.teleport(bot.entity.position.offset(0, 100, 0))
await bot.test.becomeSurvival()
Expand Down
Loading
Loading