398 lines
13 KiB
JavaScript
398 lines
13 KiB
JavaScript
|
const { Vec3 } = require('vec3')
|
||
|
const assert = require('assert')
|
||
|
const math = require('../math')
|
||
|
const conv = require('../conversions')
|
||
|
const { performance } = require('perf_hooks')
|
||
|
const { createDoneTask, createTask } = require('../promise_utils')
|
||
|
|
||
|
const { Physics, PlayerState } = require('prismarine-physics')
|
||
|
|
||
|
module.exports = inject
|
||
|
|
||
|
const PI = Math.PI
|
||
|
const PI_2 = Math.PI * 2
|
||
|
const PHYSICS_INTERVAL_MS = 50
|
||
|
const PHYSICS_TIMESTEP = PHYSICS_INTERVAL_MS / 1000 // 0.05
|
||
|
|
||
|
function inject (bot, { physicsEnabled, maxCatchupTicks }) {
|
||
|
const PHYSICS_CATCHUP_TICKS = maxCatchupTicks ?? 4
|
||
|
const world = { getBlock: (pos) => { return bot.blockAt(pos, false) } }
|
||
|
const physics = Physics(bot.registry, world)
|
||
|
|
||
|
const positionUpdateSentEveryTick = bot.supportFeature('positionUpdateSentEveryTick')
|
||
|
|
||
|
bot.jumpQueued = false
|
||
|
bot.jumpTicks = 0 // autojump cooldown
|
||
|
|
||
|
const controlState = {
|
||
|
forward: false,
|
||
|
back: false,
|
||
|
left: false,
|
||
|
right: false,
|
||
|
jump: false,
|
||
|
sprint: false,
|
||
|
sneak: false
|
||
|
}
|
||
|
let lastSentYaw = null
|
||
|
let lastSentPitch = null
|
||
|
let doPhysicsTimer = null
|
||
|
let lastPhysicsFrameTime = null
|
||
|
let shouldUsePhysics = false
|
||
|
bot.physicsEnabled = physicsEnabled ?? true
|
||
|
let deadTicks = 21
|
||
|
|
||
|
const lastSent = {
|
||
|
x: 0,
|
||
|
y: 0,
|
||
|
z: 0,
|
||
|
yaw: 0,
|
||
|
pitch: 0,
|
||
|
onGround: false,
|
||
|
time: 0
|
||
|
}
|
||
|
|
||
|
// This function should be executed each tick (every 0.05 seconds)
|
||
|
// How it works: https://gafferongames.com/post/fix_your_timestep/
|
||
|
|
||
|
// WARNING: THIS IS NOT ACCURATE ON WINDOWS (15.6 Timer Resolution)
|
||
|
// use WSL or switch to Linux
|
||
|
// see: https://discord.com/channels/413438066984747026/519952494768685086/901948718255833158
|
||
|
let timeAccumulator = 0
|
||
|
let catchupTicks = 0
|
||
|
function doPhysics () {
|
||
|
const now = performance.now()
|
||
|
const deltaSeconds = (now - lastPhysicsFrameTime) / 1000
|
||
|
lastPhysicsFrameTime = now
|
||
|
|
||
|
timeAccumulator += deltaSeconds
|
||
|
catchupTicks = 0
|
||
|
while (timeAccumulator >= PHYSICS_TIMESTEP) {
|
||
|
tickPhysics(now)
|
||
|
timeAccumulator -= PHYSICS_TIMESTEP
|
||
|
catchupTicks++
|
||
|
if (catchupTicks >= PHYSICS_CATCHUP_TICKS) break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function tickPhysics (now) {
|
||
|
if (bot.blockAt(bot.entity.position) == null) return // check if chunk is unloaded
|
||
|
if (bot.physicsEnabled && shouldUsePhysics) {
|
||
|
physics.simulatePlayer(new PlayerState(bot, controlState), world).apply(bot)
|
||
|
bot.emit('physicsTick')
|
||
|
bot.emit('physicTick') // Deprecated, only exists to support old plugins. May be removed in the future
|
||
|
}
|
||
|
if (shouldUsePhysics) {
|
||
|
updatePosition(now)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// remove this when 'physicTick' is removed
|
||
|
bot.on('newListener', (name) => {
|
||
|
if (name === 'physicTick') console.warn('Mineflayer detected that you are using a deprecated event (physicTick)! Please use this event (physicsTick) instead.')
|
||
|
})
|
||
|
|
||
|
function cleanup () {
|
||
|
clearInterval(doPhysicsTimer)
|
||
|
doPhysicsTimer = null
|
||
|
}
|
||
|
|
||
|
function sendPacketPosition (position, onGround) {
|
||
|
// sends data, no logic
|
||
|
const oldPos = new Vec3(lastSent.x, lastSent.y, lastSent.z)
|
||
|
lastSent.x = position.x
|
||
|
lastSent.y = position.y
|
||
|
lastSent.z = position.z
|
||
|
lastSent.onGround = onGround
|
||
|
bot._client.write('position', lastSent)
|
||
|
bot.emit('move', oldPos)
|
||
|
}
|
||
|
|
||
|
function sendPacketLook (yaw, pitch, onGround) {
|
||
|
// sends data, no logic
|
||
|
const oldPos = new Vec3(lastSent.x, lastSent.y, lastSent.z)
|
||
|
lastSent.yaw = yaw
|
||
|
lastSent.pitch = pitch
|
||
|
lastSent.onGround = onGround
|
||
|
bot._client.write('look', lastSent)
|
||
|
bot.emit('move', oldPos)
|
||
|
}
|
||
|
|
||
|
function sendPacketPositionAndLook (position, yaw, pitch, onGround) {
|
||
|
// sends data, no logic
|
||
|
const oldPos = new Vec3(lastSent.x, lastSent.y, lastSent.z)
|
||
|
lastSent.x = position.x
|
||
|
lastSent.y = position.y
|
||
|
lastSent.z = position.z
|
||
|
lastSent.yaw = yaw
|
||
|
lastSent.pitch = pitch
|
||
|
lastSent.onGround = onGround
|
||
|
bot._client.write('position_look', lastSent)
|
||
|
bot.emit('move', oldPos)
|
||
|
}
|
||
|
|
||
|
function deltaYaw (yaw1, yaw2) {
|
||
|
let dYaw = (yaw1 - yaw2) % PI_2
|
||
|
if (dYaw < -PI) dYaw += PI_2
|
||
|
else if (dYaw > PI) dYaw -= PI_2
|
||
|
|
||
|
return dYaw
|
||
|
}
|
||
|
|
||
|
// returns false if bot should send position packets
|
||
|
function isEntityRemoved () {
|
||
|
if (bot.isAlive === true) deadTicks = 0
|
||
|
if (bot.isAlive === false && deadTicks <= 20) deadTicks++
|
||
|
if (deadTicks >= 20) return true
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
function updatePosition (now) {
|
||
|
// Only send updates for 20 ticks after death
|
||
|
if (isEntityRemoved()) return
|
||
|
|
||
|
// Increment the yaw in baby steps so that notchian clients (not the server) can keep up.
|
||
|
const dYaw = deltaYaw(bot.entity.yaw, lastSentYaw)
|
||
|
const dPitch = bot.entity.pitch - (lastSentPitch || 0)
|
||
|
|
||
|
// Vanilla doesn't clamp yaw, so we don't want to do it either
|
||
|
const maxDeltaYaw = PHYSICS_TIMESTEP * physics.yawSpeed
|
||
|
const maxDeltaPitch = PHYSICS_TIMESTEP * physics.pitchSpeed
|
||
|
lastSentYaw += math.clamp(-maxDeltaYaw, dYaw, maxDeltaYaw)
|
||
|
lastSentPitch += math.clamp(-maxDeltaPitch, dPitch, maxDeltaPitch)
|
||
|
|
||
|
const yaw = Math.fround(conv.toNotchianYaw(lastSentYaw))
|
||
|
const pitch = Math.fround(conv.toNotchianPitch(lastSentPitch))
|
||
|
const position = bot.entity.position
|
||
|
const onGround = bot.entity.onGround
|
||
|
|
||
|
// Only send a position update if necessary, select the appropriate packet
|
||
|
const positionUpdated = lastSent.x !== position.x || lastSent.y !== position.y || lastSent.z !== position.z ||
|
||
|
// Send a position update every second, even if no other update was made
|
||
|
// This function rounds to the nearest 50ms (or PHYSICS_INTERVAL_MS) and checks if a second has passed.
|
||
|
(Math.round((now - lastSent.time) / PHYSICS_INTERVAL_MS) * PHYSICS_INTERVAL_MS) >= 1000
|
||
|
const lookUpdated = lastSent.yaw !== yaw || lastSent.pitch !== pitch
|
||
|
|
||
|
if (positionUpdated && lookUpdated) {
|
||
|
sendPacketPositionAndLook(position, yaw, pitch, onGround)
|
||
|
lastSent.time = now // only reset if positionUpdated is true
|
||
|
} else if (positionUpdated) {
|
||
|
sendPacketPosition(position, onGround)
|
||
|
lastSent.time = now // only reset if positionUpdated is true
|
||
|
} else if (lookUpdated) {
|
||
|
sendPacketLook(yaw, pitch, onGround)
|
||
|
} else if (positionUpdateSentEveryTick || onGround !== lastSent.onGround) {
|
||
|
// For versions < 1.12, one player packet should be sent every tick
|
||
|
// for the server to update health correctly
|
||
|
// For versions >= 1.12, onGround !== lastSent.onGround should be used, but it doesn't ever trigger outside of login
|
||
|
bot._client.write('flying', { onGround: bot.entity.onGround })
|
||
|
}
|
||
|
|
||
|
lastSent.onGround = bot.entity.onGround // onGround is always set
|
||
|
}
|
||
|
|
||
|
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}`)
|
||
|
if (controlState[control] === state) return
|
||
|
controlState[control] = state
|
||
|
if (control === 'jump' && state) {
|
||
|
bot.jumpQueued = true
|
||
|
} else if (control === 'sprint') {
|
||
|
bot._client.write('entity_action', {
|
||
|
entityId: bot.entity.id,
|
||
|
actionId: state ? 3 : 4,
|
||
|
jumpBoost: 0
|
||
|
})
|
||
|
} else if (control === 'sneak') {
|
||
|
bot._client.write('entity_action', {
|
||
|
entityId: bot.entity.id,
|
||
|
actionId: state ? 0 : 1,
|
||
|
jumpBoost: 0
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bot.getControlState = (control) => {
|
||
|
assert.ok(control in controlState, `invalid control: ${control}`)
|
||
|
return controlState[control]
|
||
|
}
|
||
|
|
||
|
bot.clearControlStates = () => {
|
||
|
for (const control in controlState) {
|
||
|
bot.setControlState(control, false)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bot.controlState = {}
|
||
|
|
||
|
for (const control of Object.keys(controlState)) {
|
||
|
Object.defineProperty(bot.controlState, control, {
|
||
|
get () {
|
||
|
return controlState[control]
|
||
|
},
|
||
|
set (state) {
|
||
|
bot.setControlState(control, state)
|
||
|
return state
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
let lookingTask = createDoneTask()
|
||
|
|
||
|
bot.on('move', () => {
|
||
|
if (!lookingTask.done && Math.abs(deltaYaw(bot.entity.yaw, lastSentYaw)) < 0.001) {
|
||
|
lookingTask.finish()
|
||
|
}
|
||
|
})
|
||
|
|
||
|
bot._client.on('explosion', explosion => {
|
||
|
// TODO: emit an explosion event with more info
|
||
|
if (bot.physicsEnabled && bot.game.gameMode !== 'creative') {
|
||
|
bot.entity.velocity.x += explosion.playerMotionX
|
||
|
bot.entity.velocity.y += explosion.playerMotionY
|
||
|
bot.entity.velocity.z += explosion.playerMotionZ
|
||
|
}
|
||
|
})
|
||
|
|
||
|
bot.look = async (yaw, pitch, force) => {
|
||
|
if (!lookingTask.done) {
|
||
|
lookingTask.finish() // finish the previous one
|
||
|
}
|
||
|
lookingTask = createTask()
|
||
|
|
||
|
// this is done to bypass certain anticheat checks that detect the player's sensitivity
|
||
|
// by calculating the gcd of how much they move the mouse each tick
|
||
|
const sensitivity = conv.fromNotchianPitch(0.15) // this is equal to 100% sensitivity in vanilla
|
||
|
const yawChange = Math.round((yaw - bot.entity.yaw) / sensitivity) * sensitivity
|
||
|
const pitchChange = Math.round((pitch - bot.entity.pitch) / sensitivity) * sensitivity
|
||
|
|
||
|
if (yawChange === 0 && pitchChange === 0) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
bot.entity.yaw += yawChange
|
||
|
bot.entity.pitch += pitchChange
|
||
|
|
||
|
if (force) {
|
||
|
lastSentYaw = yaw
|
||
|
lastSentPitch = pitch
|
||
|
return
|
||
|
}
|
||
|
|
||
|
await lookingTask.promise
|
||
|
}
|
||
|
|
||
|
bot.lookAt = async (point, force) => {
|
||
|
const delta = point.minus(bot.entity.position.offset(0, bot.entity.height, 0))
|
||
|
const yaw = Math.atan2(-delta.x, -delta.z)
|
||
|
const groundDistance = Math.sqrt(delta.x * delta.x + delta.z * delta.z)
|
||
|
const pitch = Math.atan2(delta.y, groundDistance)
|
||
|
await bot.look(yaw, pitch, force)
|
||
|
}
|
||
|
|
||
|
// player position and look (clientbound)
|
||
|
bot._client.on('position', (packet) => {
|
||
|
bot.entity.height = 1.62
|
||
|
|
||
|
// Velocity is only set to 0 if the flag is not set, otherwise keep current velocity
|
||
|
const vel = bot.entity.velocity
|
||
|
vel.set(
|
||
|
packet.flags & 1 ? vel.x : 0,
|
||
|
packet.flags & 2 ? vel.y : 0,
|
||
|
packet.flags & 4 ? vel.z : 0
|
||
|
)
|
||
|
|
||
|
// If flag is set, then the corresponding value is relative, else it is absolute
|
||
|
const pos = bot.entity.position
|
||
|
pos.set(
|
||
|
packet.flags & 1 ? (pos.x + packet.x) : packet.x,
|
||
|
packet.flags & 2 ? (pos.y + packet.y) : packet.y,
|
||
|
packet.flags & 4 ? (pos.z + packet.z) : packet.z
|
||
|
)
|
||
|
|
||
|
const newYaw = (packet.flags & 8 ? conv.toNotchianYaw(bot.entity.yaw) : 0) + packet.yaw
|
||
|
const newPitch = (packet.flags & 16 ? conv.toNotchianPitch(bot.entity.pitch) : 0) + packet.pitch
|
||
|
bot.entity.yaw = conv.fromNotchianYaw(newYaw)
|
||
|
bot.entity.pitch = conv.fromNotchianPitch(newPitch)
|
||
|
bot.entity.onGround = false
|
||
|
|
||
|
if (bot.supportFeature('teleportUsesOwnPacket')) {
|
||
|
bot._client.write('teleport_confirm', { teleportId: packet.teleportId })
|
||
|
}
|
||
|
sendPacketPositionAndLook(pos, newYaw, newPitch, bot.entity.onGround)
|
||
|
|
||
|
shouldUsePhysics = true
|
||
|
bot.jumpTicks = 0
|
||
|
lastSentYaw = bot.entity.yaw
|
||
|
lastSentPitch = bot.entity.pitch
|
||
|
|
||
|
bot.emit('forcedMove')
|
||
|
})
|
||
|
|
||
|
bot.waitForTicks = async function (ticks) {
|
||
|
if (ticks <= 0) return
|
||
|
await new Promise(resolve => {
|
||
|
const tickListener = () => {
|
||
|
ticks--
|
||
|
if (ticks === 0) {
|
||
|
bot.removeListener('physicsTick', tickListener)
|
||
|
resolve()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bot.on('physicsTick', tickListener)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
bot.on('mount', () => { shouldUsePhysics = false })
|
||
|
bot.on('respawn', () => { shouldUsePhysics = false })
|
||
|
bot.on('login', () => {
|
||
|
shouldUsePhysics = false
|
||
|
if (doPhysicsTimer === null) {
|
||
|
lastPhysicsFrameTime = performance.now()
|
||
|
doPhysicsTimer = setInterval(doPhysics, PHYSICS_INTERVAL_MS)
|
||
|
}
|
||
|
})
|
||
|
bot.on('end', cleanup)
|
||
|
}
|