Skip to content

Commit d0eb3a1

Browse files
authored
Mineflayer physics refactor (#2492)
* add tests that fail * fixed a test * fix: set velocity to 0 only if it is absolute * refactor: precise physics timer & limiter * refactor: position update logic * fix: minor issues * add: death ticking chunk check was not working properly * change packet object * unhardcode deltaSeconds limiter * add deprecated notice for physicTick * add physics chunk check * Karang changes added back lastSent optimization moved depercated event to bot.on('newListener' removed tick limiter for now * Karang changes part 2 * Karang changes part 3 * remove speedy setInterval * remove 50ms test * cache supportFeature check (thanks Ic3Tank) * remove tests that rely on precise timing * lint * change console.log to warn remove debug comment * add configurable "maxCatchupTicks" rename isDead to sendPositionPacketInDeath
1 parent a16d270 commit d0eb3a1

File tree

3 files changed

+216
-32
lines changed

3 files changed

+216
-32
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ versions/
88
server_jars/
99
test/server_*
1010
.vscode
11-
.DS_Store
11+
.DS_Store
12+
launcher_accounts.json

lib/plugins/physics.js

+73-31
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ module.exports = inject
1212
const PI = Math.PI
1313
const PI_2 = Math.PI * 2
1414
const PHYSICS_INTERVAL_MS = 50
15-
const PHYSICS_TIMESTEP = PHYSICS_INTERVAL_MS / 1000
15+
const PHYSICS_TIMESTEP = PHYSICS_INTERVAL_MS / 1000 // 0.05
1616

17-
function inject (bot, { physicsEnabled }) {
17+
function inject (bot, { physicsEnabled, maxCatchupTicks }) {
18+
const PHYSICS_CATCHUP_TICKS = maxCatchupTicks ?? 4
1819
const world = { getBlock: (pos) => { return bot.blockAt(pos, false) } }
1920
const physics = Physics(bot.registry, world)
2021

@@ -38,6 +39,7 @@ function inject (bot, { physicsEnabled }) {
3839
let lastPhysicsFrameTime = null
3940
let shouldUsePhysics = false
4041
bot.physicsEnabled = physicsEnabled ?? true
42+
let deadTicks = 21
4143

4244
const lastSent = {
4345
x: 0,
@@ -51,25 +53,44 @@ function inject (bot, { physicsEnabled }) {
5153

5254
// This function should be executed each tick (every 0.05 seconds)
5355
// How it works: https://gafferongames.com/post/fix_your_timestep/
56+
57+
// WARNING: THIS IS NOT ACCURATE ON WINDOWS (15.6 Timer Resolution)
58+
// use WSL or switch to Linux
59+
// see: https://discord.com/channels/413438066984747026/519952494768685086/901948718255833158
5460
let timeAccumulator = 0
61+
let catchupTicks = 0
5562
function doPhysics () {
5663
const now = performance.now()
5764
const deltaSeconds = (now - lastPhysicsFrameTime) / 1000
5865
lastPhysicsFrameTime = now
5966

6067
timeAccumulator += deltaSeconds
61-
68+
catchupTicks = 0
6269
while (timeAccumulator >= PHYSICS_TIMESTEP) {
63-
if (bot.physicsEnabled && shouldUsePhysics) {
64-
physics.simulatePlayer(new PlayerState(bot, controlState), world).apply(bot)
65-
bot.emit('physicsTick')
66-
bot.emit('physicTick') // Deprecated, only exists to support old plugins. May be removed in the future
67-
}
68-
updatePosition(PHYSICS_TIMESTEP)
70+
tickPhysics(now)
6971
timeAccumulator -= PHYSICS_TIMESTEP
72+
catchupTicks++
73+
if (catchupTicks >= PHYSICS_CATCHUP_TICKS) break
74+
}
75+
}
76+
77+
function tickPhysics (now) {
78+
if (bot.blockAt(bot.entity.position) == null) return // check if chunk is unloaded
79+
if (bot.physicsEnabled && shouldUsePhysics) {
80+
physics.simulatePlayer(new PlayerState(bot, controlState), world).apply(bot)
81+
bot.emit('physicsTick')
82+
bot.emit('physicTick') // Deprecated, only exists to support old plugins. May be removed in the future
83+
}
84+
if (shouldUsePhysics) {
85+
updatePosition(now)
7086
}
7187
}
7288

89+
// remove this when 'physicTick' is removed
90+
bot.on('newListener', (name) => {
91+
if (name === 'physicTick') console.warn('Mineflayer detected that you are using a deprecated event (physicTick)! Please use this event (physicsTick) instead.')
92+
})
93+
7394
function cleanup () {
7495
clearInterval(doPhysicsTimer)
7596
doPhysicsTimer = null
@@ -117,17 +138,25 @@ function inject (bot, { physicsEnabled }) {
117138
return dYaw
118139
}
119140

120-
function updatePosition (dt) {
121-
// If you're dead, you're probably on the ground though ...
122-
if (!bot.isAlive) bot.entity.onGround = true
141+
// returns false if packet should be sent, true if not
142+
function sendPositionPacketInDeath () {
143+
if (bot.isAlive === true) deadTicks = 0
144+
if (bot.isAlive === false && deadTicks <= 20) deadTicks++
145+
if (deadTicks >= 20) return true
146+
return false
147+
}
148+
149+
function updatePosition (now) {
150+
// Only send updates for 20 ticks after death
151+
if (sendPositionPacketInDeath()) return
123152

124153
// Increment the yaw in baby steps so that notchian clients (not the server) can keep up.
125154
const dYaw = deltaYaw(bot.entity.yaw, lastSentYaw)
126155
const dPitch = bot.entity.pitch - (lastSentPitch || 0)
127156

128157
// Vanilla doesn't clamp yaw, so we don't want to do it either
129-
const maxDeltaYaw = dt * physics.yawSpeed
130-
const maxDeltaPitch = dt * physics.pitchSpeed
158+
const maxDeltaYaw = PHYSICS_TIMESTEP * physics.yawSpeed
159+
const maxDeltaPitch = PHYSICS_TIMESTEP * physics.pitchSpeed
131160
lastSentYaw += math.clamp(-maxDeltaYaw, dYaw, maxDeltaYaw)
132161
lastSentPitch += math.clamp(-maxDeltaPitch, dPitch, maxDeltaPitch)
133162

@@ -137,24 +166,28 @@ function inject (bot, { physicsEnabled }) {
137166
const onGround = bot.entity.onGround
138167

139168
// Only send a position update if necessary, select the appropriate packet
140-
const positionUpdated = lastSent.x !== position.x || lastSent.y !== position.y || lastSent.z !== position.z
169+
const positionUpdated = lastSent.x !== position.x || lastSent.y !== position.y || lastSent.z !== position.z ||
170+
// Send a position update every second, even if no other update was made
171+
// This function rounds to the nearest 50ms (or PHYSICS_INTERVAL_MS) and checks if a second has passed.
172+
(Math.round((now - lastSent.time) / PHYSICS_INTERVAL_MS) * PHYSICS_INTERVAL_MS) >= 1000
141173
const lookUpdated = lastSent.yaw !== yaw || lastSent.pitch !== pitch
142174

143-
if (positionUpdated && lookUpdated && bot.isAlive) {
175+
if (positionUpdated && lookUpdated) {
144176
sendPacketPositionAndLook(position, yaw, pitch, onGround)
145-
} else if (positionUpdated && bot.isAlive) {
177+
lastSent.time = now // only reset if positionUpdated is true
178+
} else if (positionUpdated) {
146179
sendPacketPosition(position, onGround)
147-
} else if (lookUpdated && bot.isAlive) {
180+
lastSent.time = now // only reset if positionUpdated is true
181+
} else if (lookUpdated) {
148182
sendPacketLook(yaw, pitch, onGround)
149-
} else if (performance.now() - lastSent.time >= 1000) {
150-
// Send a position packet every second, even if no update was made
151-
sendPacketPosition(position, onGround)
152-
lastSent.time = performance.now()
153-
} else if (positionUpdateSentEveryTick && bot.isAlive) {
183+
} else if (positionUpdateSentEveryTick || onGround !== lastSent.onGround) {
154184
// For versions < 1.12, one player packet should be sent every tick
155185
// for the server to update health correctly
186+
// For versions >= 1.12, onGround !== lastSent.onGround should be used, but it doesn't ever trigger outside of login
156187
bot._client.write('flying', { onGround: bot.entity.onGround })
157188
}
189+
190+
lastSent.onGround = bot.entity.onGround // onGround is always set
158191
}
159192

160193
bot.physics = physics
@@ -262,7 +295,14 @@ function inject (bot, { physicsEnabled }) {
262295
// player position and look (clientbound)
263296
bot._client.on('position', (packet) => {
264297
bot.entity.height = 1.62
265-
bot.entity.velocity.set(0, 0, 0)
298+
299+
// Velocity is only set to 0 if the flag is not set, otherwise keep current velocity
300+
const vel = bot.entity.velocity
301+
vel.set(
302+
packet.flags & 1 ? vel.x : 0,
303+
packet.flags & 2 ? vel.y : 0,
304+
packet.flags & 4 ? vel.z : 0
305+
)
266306

267307
// If flag is set, then the corresponding value is relative, else it is absolute
268308
const pos = bot.entity.position
@@ -280,19 +320,14 @@ function inject (bot, { physicsEnabled }) {
280320

281321
if (bot.supportFeature('teleportUsesOwnPacket')) {
282322
bot._client.write('teleport_confirm', { teleportId: packet.teleportId })
283-
// Force send an extra packet to be like vanilla client
284-
sendPacketPositionAndLook(pos, newYaw, newPitch, bot.entity.onGround)
285323
}
286324
sendPacketPositionAndLook(pos, newYaw, newPitch, bot.entity.onGround)
287325

288326
shouldUsePhysics = true
289-
bot.entity.timeSinceOnGround = 0
327+
bot.jumpTicks = 0
290328
lastSentYaw = bot.entity.yaw
329+
lastSentPitch = bot.entity.pitch
291330

292-
if (doPhysicsTimer === null) {
293-
lastPhysicsFrameTime = performance.now()
294-
doPhysicsTimer = setInterval(doPhysics, PHYSICS_INTERVAL_MS)
295-
}
296331
bot.emit('forcedMove')
297332
})
298333

@@ -313,5 +348,12 @@ function inject (bot, { physicsEnabled }) {
313348

314349
bot.on('mount', () => { shouldUsePhysics = false })
315350
bot.on('respawn', () => { shouldUsePhysics = false })
351+
bot.on('login', () => {
352+
shouldUsePhysics = false
353+
if (doPhysicsTimer === null) {
354+
lastPhysicsFrameTime = performance.now()
355+
doPhysicsTimer = setInterval(doPhysics, PHYSICS_INTERVAL_MS)
356+
}
357+
})
316358
bot.on('end', cleanup)
317359
}

test/internalTest.js

+141
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const mineflayer = require('../')
44
const vec3 = require('vec3')
55
const mc = require('minecraft-protocol')
66
const assert = require('assert')
7+
const { sleep } = require('../lib/promise_utils')
78

89
for (const supportedVersion of mineflayer.testedVersions) {
910
const registry = require('prismarine-registry')(supportedVersion)
@@ -224,7 +225,147 @@ for (const supportedVersion of mineflayer.testedVersions) {
224225

225226
describe('physics', () => {
226227
const pos = vec3(1, 65, 1)
228+
const pos2 = vec3(2, 65, 1)
227229
const goldId = 41
230+
it('no physics if there is no chunk', (done) => {
231+
let fail = 0
232+
const basePosition = {
233+
x: 1.5,
234+
y: 66,
235+
z: 1.5,
236+
pitch: 0,
237+
yaw: 0,
238+
flags: 0,
239+
teleportId: 0
240+
}
241+
server.on('login', async (client) => {
242+
await client.write('login', bot.test.generateLoginPacket())
243+
await client.write('position', basePosition)
244+
client.on('packet', (data, meta) => {
245+
const packetName = meta.name
246+
switch (packetName) {
247+
case 'position':
248+
fail++
249+
break
250+
case 'position_look':
251+
fail++
252+
break
253+
case 'look':
254+
fail++
255+
break
256+
}
257+
if (fail > 1) assert.fail('position packet sent')
258+
})
259+
await sleep(2000)
260+
done()
261+
})
262+
})
263+
it('absolute position & relative position (velocity)', (done) => {
264+
server.on('login', async (client) => {
265+
await client.write('login', bot.test.generateLoginPacket())
266+
const chunk = await bot.test.buildChunk()
267+
268+
await chunk.setBlockType(pos, goldId)
269+
await chunk.setBlockType(pos2, goldId)
270+
await client.write('map_chunk', generateChunkPacket(chunk))
271+
let check = true
272+
let absolute = true
273+
const basePosition = {
274+
x: 1.5,
275+
y: 66,
276+
z: 1.5,
277+
pitch: 0,
278+
yaw: 0,
279+
flags: 0,
280+
teleportId: 0
281+
}
282+
client.on('packet', (data, meta) => {
283+
const packetName = meta.name
284+
switch (packetName) {
285+
case 'teleport_confirm': {
286+
assert.ok(basePosition.teleportId === data.teleportId)
287+
break
288+
}
289+
case 'position_look': {
290+
if (!check) return
291+
if (absolute) {
292+
assert.ok(bot.entity.velocity.y === 0)
293+
} else {
294+
assert.ok(bot.entity.velocity.y !== 0)
295+
}
296+
assert.ok(basePosition.x === data.x)
297+
assert.ok(basePosition.y === data.y)
298+
assert.ok(basePosition.z === data.z)
299+
assert.ok(basePosition.yaw === data.yaw)
300+
assert.ok(basePosition.pitch === data.pitch)
301+
check = false
302+
break
303+
}
304+
default:
305+
break
306+
}
307+
})
308+
// Absolute Position Tests
309+
// absolute position test
310+
check = true
311+
await client.write('position', basePosition)
312+
await bot.waitForTicks(5)
313+
// absolute position test 2
314+
basePosition.x = 2.5
315+
basePosition.teleportId = 1
316+
await bot.waitForTicks(1)
317+
check = true
318+
await client.write('position', basePosition)
319+
await bot.waitForTicks(2)
320+
// absolute position test 3
321+
basePosition.x = 1.5
322+
basePosition.teleportId = 2
323+
await bot.waitForTicks(1)
324+
check = true
325+
await client.write('position', basePosition)
326+
await bot.waitForTicks(2)
327+
328+
// Relative Position Tests
329+
const relativePosition = {
330+
x: 1,
331+
y: 0,
332+
z: 0,
333+
pitch: 0,
334+
yaw: 0,
335+
flags: 31,
336+
teleportId: 3
337+
}
338+
absolute = false
339+
// relative position test 1
340+
basePosition.x = 2.5
341+
basePosition.teleportId = 3
342+
relativePosition.x = 1
343+
relativePosition.teleportId = 3
344+
await bot.waitForTicks(1)
345+
check = true
346+
await client.write('position', relativePosition)
347+
await bot.waitForTicks(2)
348+
// relative position test 2
349+
basePosition.x = 1.5
350+
basePosition.teleportId = 4
351+
relativePosition.x = -1
352+
relativePosition.teleportId = 4
353+
await bot.waitForTicks(1)
354+
check = true
355+
await client.write('position', relativePosition)
356+
await bot.waitForTicks(2)
357+
// relative position test 3
358+
basePosition.x = 2.5
359+
basePosition.teleportId = 5
360+
relativePosition.x = 1
361+
relativePosition.teleportId = 5
362+
await bot.waitForTicks(1)
363+
check = true
364+
await client.write('position', relativePosition)
365+
await bot.waitForTicks(2)
366+
done()
367+
})
368+
})
228369
it('gravity + land on solid block + jump', (done) => {
229370
let y = 80
230371
let landed = false

0 commit comments

Comments
 (0)