diff --git a/.gitignore b/.gitignore index 3721ad4cf..c76d8b46f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ versions/ server_jars/ test/server_* .vscode -.DS_Store \ No newline at end of file +.DS_Store +launcher_accounts.json diff --git a/lib/plugins/physics.js b/lib/plugins/physics.js index a6dd8c8eb..520c3028c 100644 --- a/lib/plugins/physics.js +++ b/lib/plugins/physics.js @@ -12,9 +12,10 @@ 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 +const PHYSICS_TIMESTEP = PHYSICS_INTERVAL_MS / 1000 // 0.05 -function inject (bot, { physicsEnabled }) { +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) @@ -38,6 +39,7 @@ function inject (bot, { physicsEnabled }) { let lastPhysicsFrameTime = null let shouldUsePhysics = false bot.physicsEnabled = physicsEnabled ?? true + let deadTicks = 21 const lastSent = { x: 0, @@ -51,25 +53,44 @@ function inject (bot, { physicsEnabled }) { // 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) { - 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 - } - updatePosition(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 @@ -117,17 +138,25 @@ function inject (bot, { physicsEnabled }) { return dYaw } - function updatePosition (dt) { - // If you're dead, you're probably on the ground though ... - if (!bot.isAlive) bot.entity.onGround = true + // returns false if packet should be sent, true if not + function sendPositionPacketInDeath () { + 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 (sendPositionPacketInDeath()) 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 = dt * physics.yawSpeed - const maxDeltaPitch = dt * physics.pitchSpeed + const maxDeltaYaw = PHYSICS_TIMESTEP * physics.yawSpeed + const maxDeltaPitch = PHYSICS_TIMESTEP * physics.pitchSpeed lastSentYaw += math.clamp(-maxDeltaYaw, dYaw, maxDeltaYaw) lastSentPitch += math.clamp(-maxDeltaPitch, dPitch, maxDeltaPitch) @@ -137,24 +166,28 @@ function inject (bot, { physicsEnabled }) { 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 + 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 && bot.isAlive) { + if (positionUpdated && lookUpdated) { sendPacketPositionAndLook(position, yaw, pitch, onGround) - } else if (positionUpdated && bot.isAlive) { + lastSent.time = now // only reset if positionUpdated is true + } else if (positionUpdated) { sendPacketPosition(position, onGround) - } else if (lookUpdated && bot.isAlive) { + lastSent.time = now // only reset if positionUpdated is true + } else if (lookUpdated) { sendPacketLook(yaw, pitch, onGround) - } else if (performance.now() - lastSent.time >= 1000) { - // Send a position packet every second, even if no update was made - sendPacketPosition(position, onGround) - lastSent.time = performance.now() - } else if (positionUpdateSentEveryTick && bot.isAlive) { + } 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 @@ -262,7 +295,14 @@ function inject (bot, { physicsEnabled }) { // player position and look (clientbound) bot._client.on('position', (packet) => { bot.entity.height = 1.62 - bot.entity.velocity.set(0, 0, 0) + + // 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 @@ -280,19 +320,14 @@ function inject (bot, { physicsEnabled }) { if (bot.supportFeature('teleportUsesOwnPacket')) { bot._client.write('teleport_confirm', { teleportId: packet.teleportId }) - // Force send an extra packet to be like vanilla client - sendPacketPositionAndLook(pos, newYaw, newPitch, bot.entity.onGround) } sendPacketPositionAndLook(pos, newYaw, newPitch, bot.entity.onGround) shouldUsePhysics = true - bot.entity.timeSinceOnGround = 0 + bot.jumpTicks = 0 lastSentYaw = bot.entity.yaw + lastSentPitch = bot.entity.pitch - if (doPhysicsTimer === null) { - lastPhysicsFrameTime = performance.now() - doPhysicsTimer = setInterval(doPhysics, PHYSICS_INTERVAL_MS) - } bot.emit('forcedMove') }) @@ -313,5 +348,12 @@ function inject (bot, { physicsEnabled }) { 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) } diff --git a/test/internalTest.js b/test/internalTest.js index 584e0f4fe..cfe70ffb3 100644 --- a/test/internalTest.js +++ b/test/internalTest.js @@ -4,6 +4,7 @@ const mineflayer = require('../') const vec3 = require('vec3') const mc = require('minecraft-protocol') const assert = require('assert') +const { sleep } = require('../lib/promise_utils') for (const supportedVersion of mineflayer.testedVersions) { const registry = require('prismarine-registry')(supportedVersion) @@ -224,7 +225,147 @@ for (const supportedVersion of mineflayer.testedVersions) { describe('physics', () => { const pos = vec3(1, 65, 1) + const pos2 = vec3(2, 65, 1) const goldId = 41 + it('no physics if there is no chunk', (done) => { + let fail = 0 + const basePosition = { + x: 1.5, + y: 66, + z: 1.5, + pitch: 0, + yaw: 0, + flags: 0, + teleportId: 0 + } + server.on('login', async (client) => { + await client.write('login', bot.test.generateLoginPacket()) + await client.write('position', basePosition) + client.on('packet', (data, meta) => { + const packetName = meta.name + switch (packetName) { + case 'position': + fail++ + break + case 'position_look': + fail++ + break + case 'look': + fail++ + break + } + if (fail > 1) assert.fail('position packet sent') + }) + await sleep(2000) + done() + }) + }) + it('absolute position & relative position (velocity)', (done) => { + server.on('login', async (client) => { + await client.write('login', bot.test.generateLoginPacket()) + const chunk = await bot.test.buildChunk() + + await chunk.setBlockType(pos, goldId) + await chunk.setBlockType(pos2, goldId) + await client.write('map_chunk', generateChunkPacket(chunk)) + let check = true + let absolute = true + const basePosition = { + x: 1.5, + y: 66, + z: 1.5, + pitch: 0, + yaw: 0, + flags: 0, + teleportId: 0 + } + client.on('packet', (data, meta) => { + const packetName = meta.name + switch (packetName) { + case 'teleport_confirm': { + assert.ok(basePosition.teleportId === data.teleportId) + break + } + case 'position_look': { + if (!check) return + if (absolute) { + assert.ok(bot.entity.velocity.y === 0) + } else { + assert.ok(bot.entity.velocity.y !== 0) + } + assert.ok(basePosition.x === data.x) + assert.ok(basePosition.y === data.y) + assert.ok(basePosition.z === data.z) + assert.ok(basePosition.yaw === data.yaw) + assert.ok(basePosition.pitch === data.pitch) + check = false + break + } + default: + break + } + }) + // Absolute Position Tests + // absolute position test + check = true + await client.write('position', basePosition) + await bot.waitForTicks(5) + // absolute position test 2 + basePosition.x = 2.5 + basePosition.teleportId = 1 + await bot.waitForTicks(1) + check = true + await client.write('position', basePosition) + await bot.waitForTicks(2) + // absolute position test 3 + basePosition.x = 1.5 + basePosition.teleportId = 2 + await bot.waitForTicks(1) + check = true + await client.write('position', basePosition) + await bot.waitForTicks(2) + + // Relative Position Tests + const relativePosition = { + x: 1, + y: 0, + z: 0, + pitch: 0, + yaw: 0, + flags: 31, + teleportId: 3 + } + absolute = false + // relative position test 1 + basePosition.x = 2.5 + basePosition.teleportId = 3 + relativePosition.x = 1 + relativePosition.teleportId = 3 + await bot.waitForTicks(1) + check = true + await client.write('position', relativePosition) + await bot.waitForTicks(2) + // relative position test 2 + basePosition.x = 1.5 + basePosition.teleportId = 4 + relativePosition.x = -1 + relativePosition.teleportId = 4 + await bot.waitForTicks(1) + check = true + await client.write('position', relativePosition) + await bot.waitForTicks(2) + // relative position test 3 + basePosition.x = 2.5 + basePosition.teleportId = 5 + relativePosition.x = 1 + relativePosition.teleportId = 5 + await bot.waitForTicks(1) + check = true + await client.write('position', relativePosition) + await bot.waitForTicks(2) + done() + }) + }) it('gravity + land on solid block + jump', (done) => { let y = 80 let landed = false