diff --git a/lib/plugins/physics.js b/lib/plugins/physics.js index cee1bb8a5..0422cf147 100644 --- a/lib/plugins/physics.js +++ b/lib/plugins/physics.js @@ -1,11 +1,10 @@ -const { Vec3 } = require('vec3') +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 {performance} = require('perf_hooks') +const {createDoneTask, createTask} = require('../promise_utils') -const { Physics, PlayerState } = require('prismarine-physics') +const {Physics, PlayerState} = require('prismarine-physics') module.exports = inject @@ -14,388 +13,418 @@ 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 inject(bot, {physicsEnabled, maxCatchupTicks}) { + const PHYSICS_CATCHUP_TICKS = maxCatchupTicks ?? 4 + const world = { + getBlock: (pos) => { + return bot.blockAt(pos, false) + } } - } - - 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 + 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 } - if (shouldUsePhysics) { - updatePosition(now) + + let lastSentYaw = null + let lastSentPitch = null + let doPhysicsTimer = null + let lastPhysicsFrameTime = null + let shouldUsePhysics = false + bot.physicsEnabled = physicsEnabled ?? true + let deadTicks = 21 + + let ticksSinceLastPosition = 0; + + const lastSent = { + x: 0, + y: 0, + z: 0, + yaw: 0, + pitch: 0, + onGround: false, + sprintState: false, + sneakState: false, + }; + + // 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 + } } - } - - // 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 }) + + 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) + } } - lastSent.onGround = bot.entity.onGround // onGround is always set - } + // 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.') + }) - bot.physics = physics + function cleanup() { + clearInterval(doPhysicsTimer) + doPhysicsTimer = null + } - function getEffectLevel (mcData, effectName, effects) { - const effectDescriptor = mcData.effectsByName[effectName] - if (!effectDescriptor) { - return 0 + function sendPacketPosition(position, onGround) { + 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', { + x: position.x, + y: position.y, + z: position.z, + onGround, + }); + bot.emit('move', oldPos); } - const effectInfo = effects[effectDescriptor.id] - if (!effectInfo) { - return 0 + + function sendPacketLook(yaw, pitch, onGround) { + const oldPos = new Vec3(lastSent.x, lastSent.y, lastSent.z); + lastSent.yaw = yaw; + lastSent.pitch = pitch; + lastSent.onGround = onGround; + bot._client.write('look', { + yaw, + pitch, + onGround, + }); + bot.emit('move', oldPos); } - 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') + + function sendPacketPositionAndLook(position, yaw, pitch, onGround) { + 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', { + x: position.x, + y: position.y, + z: position.z, + yaw, + pitch, + onGround, + }); + bot.emit('move', oldPos); } - 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') + 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 } - 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') + // 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 } - 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 - }) + + 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 + lastSentYaw += dYaw + lastSentPitch += dPitch + + const yaw = Math.fround(conv.toNotchianYaw(lastSentYaw)) + const pitch = Math.fround(conv.toNotchianPitch(lastSentPitch)) + const position = bot.entity.position + const onGround = bot.entity.onGround + + if (lastSent.sprintState !== controlState.sprint) { + bot._client.write('entity_action', { + entityId: bot.entity.id, + actionId: bot.entity.sprintState ? 3 : 4, + jumpBoost: 0, + }); + lastSent.sprintState = controlState.sprint; + } + + if (lastSent.sneakState !== controlState.sneak) { + bot._client.write('entity_action', { + entityId: bot.entity.id, + actionId: bot.entity.sneakState ? 0 : 1, + jumpBoost: 0, + }); + lastSent.sneakState = controlState.sneak; + } + + // 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 + || ticksSinceLastPosition >= 20 + const lookUpdated = lastSent.yaw !== yaw || lastSent.pitch !== pitch + + if (positionUpdated && lookUpdated) { + sendPacketPositionAndLook(position, yaw, pitch, onGround) + } else if (positionUpdated) { + sendPacketPosition(position, onGround) + } else if (lookUpdated) { + sendPacketLook(yaw, pitch, onGround) + } else if (positionUpdateSentEveryTick || onGround !== lastSent.onGround) { + bot._client.write('flying', {onGround: bot.entity.onGround}) + } + + ticksSinceLastPosition++ + + if (positionUpdated) { + ticksSinceLastPosition = 0 + } + + lastSent.onGround = bot.entity.onGround // onGround is always set } - } - bot.getControlState = (control) => { - assert.ok(control in controlState, `invalid control: ${control}`) - return controlState[control] - } + bot.physics = physics - bot.clearControlStates = () => { - for (const control in controlState) { - bot.setControlState(control, false) + 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.spoofControlState = (control, state) => { - controlState[control] = state - } - - bot.controlState = {} + 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') + } - for (const control of Object.keys(controlState)) { - Object.defineProperty(bot.controlState, control, { - get () { - return controlState[control] - }, - set (state) { - bot.setControlState(control, state) - return state - } - }) - } + 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') + } - let lookingTask = createDoneTask() + 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.on('move', () => { - if (!lookingTask.done && Math.abs(deltaYaw(bot.entity.yaw, lastSentYaw)) < 0.001) { - lookingTask.finish() + 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 + } } - }) - - 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.getControlState = (control) => { + assert.ok(control in controlState, `invalid control: ${control}`) + return controlState[control] } - }) - bot.look = async (yaw, pitch, force) => { - if (!lookingTask.done) { - lookingTask.finish() // finish the previous one + bot.clearControlStates = () => { + for (const control in controlState) { + bot.setControlState(control, false) + } } - 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 + bot.spoofControlState = (control, state) => { + controlState[control] = state + } - if (yawChange === 0 && pitchChange === 0) { - return + 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 + } + }) } - bot.entity.yaw += yawChange - bot.entity.pitch += pitchChange + 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 + } - if (force) { - lastSentYaw = yaw - lastSentPitch = pitch - return + bot.entity.yaw += yawChange + bot.entity.pitch += pitchChange + + if (force) { + lastSentYaw = yaw + lastSentPitch = pitch + return + } + + await lookingTask.promise } - 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 }) + 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) } - 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() + + // 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) - bot.on('physicsTick', tickListener) + shouldUsePhysics = true + bot.jumpTicks = 0 + lastSentYaw = bot.entity.yaw + lastSentPitch = bot.entity.pitch + + bot.emit('forcedMove') }) - } - - 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.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('end', cleanup) + + bot.on('mount', () => { + shouldUsePhysics = false + }) + bot.on('respawn', () => { + shouldUsePhysics = false + }) + bot.on('login', () => { + shouldUsePhysics = false + bot.clearControlStates() + if (doPhysicsTimer === null) { + lastPhysicsFrameTime = performance.now() + doPhysicsTimer = setInterval(doPhysics, PHYSICS_INTERVAL_MS) + } + }) + bot.on('end', cleanup) }