spectra-physics 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -23
- package/index.js +2 -2
- package/lib/client/clock.js +38 -0
- package/lib/client/connection.js +103 -0
- package/lib/client/controls.js +78 -0
- package/lib/client/environment.js +94 -0
- package/lib/client/index.js +81 -0
- package/lib/client/interact.js +79 -0
- package/lib/client/items.js +85 -0
- package/lib/client/protocol.js +42 -0
- package/lib/client/registries.js +86 -0
- package/lib/client/reporter.js +91 -0
- package/lib/client/riding.js +69 -0
- package/lib/client/server.js +103 -0
- package/lib/client/teleport.js +108 -0
- package/lib/client/ticker.js +129 -0
- package/lib/engine/aabb.js +5 -3
- package/lib/engine/boat.js +57 -7
- package/lib/engine/player/blocks.js +134 -0
- package/lib/engine/player/effects.js +115 -0
- package/lib/engine/player/fluids.js +150 -0
- package/lib/engine/player/index.js +220 -0
- package/lib/engine/player/input.js +89 -0
- package/lib/engine/player/move.js +141 -0
- package/lib/engine/player/pose.js +95 -0
- package/lib/engine/player/push.js +27 -0
- package/lib/engine/player/travel.js +203 -0
- package/lib/engine/player/traversal.js +290 -0
- package/lib/engine/version.js +52 -27
- package/lib/engine/world.js +85 -20
- package/package.json +2 -1
- package/lib/engine/physics.js +0 -780
- package/lib/plugin.js +0 -574
package/README.md
CHANGED
|
@@ -22,21 +22,46 @@ several places that are visible to the server:
|
|
|
22
22
|
| Collision | legacy 1.8 algorithm | `Shapes.collide` with VoxelShape epsilons, Y → larger horizontal axis order |
|
|
23
23
|
| Step-up | single step | pre-1.21 heuristic and 1.21+ candidate step heights |
|
|
24
24
|
| First tick after a teleport | moves immediately | no movement, gravity applied after `move`, like vanilla |
|
|
25
|
-
| Position packets | time based, last-sent values reset on teleport | `sendPosition` rules: 2·10⁻⁴
|
|
25
|
+
| Position packets | time based, last-sent values reset on teleport | `sendPosition` rules: 0.03 threshold before 1.18.2 and 2·10⁻⁴ after, 20-tick reminder, last-sent values kept across teleports |
|
|
26
|
+
| Input | raw ±1 | per version: sneak/item scaling then 0.98 up to 1.21.4, normalized square movement from 1.21.5 |
|
|
27
|
+
| Wall collisions | always zeroes the blocked axis | 1.14–1.18.1 keep X speed in corners, 1.18.2+ compare with an epsilon |
|
|
28
|
+
| Block effects | during the move | during the move before 1.21.2; from 1.21.2 after travel, walking the path in each version's own block order |
|
|
29
|
+
| Powder snow | never solid | solid from above in leather boots (not while sneaking), 0.9 high when falling onto it |
|
|
30
|
+
| Item use | any `activateItem`, cleared by any entity's status | only items with a use animation (food when hungry, potions, shield, bow with arrows, …), ended by release, slot change or the server; movement slowed to a fifth |
|
|
31
|
+
| Actions | sent whenever called | use, dig, interact, attack and swing go out at the start of the next tick, before movement; `use_item` carries that tick's rotation |
|
|
32
|
+
| Entity clicks, mounting | `interact` only | aim, `interact_at` with the hit point, `interact`, swing; leaving a vehicle by holding sneak |
|
|
26
33
|
| Rotation | continuous interpolation | whole mouse steps of 0.15°, accumulated in float |
|
|
27
|
-
| Sprint | applied the same tick | speed
|
|
34
|
+
| Sprint | applied the same tick | speed follows the sprint state in the tick it is sent, as the server applies it |
|
|
28
35
|
| Tick loop | fixed interval | `Minecraft` timer: catches up at most 10 ticks |
|
|
29
36
|
| 1.21.2+ | no `tick_end` | `tick_end` after every tick, `player_input` on key change |
|
|
30
37
|
| 1.21.4+ | `player_loaded` on first health update | sent once the level around the player is loaded |
|
|
31
38
|
| Teleport answer | one order for all versions | per version (1.21.2–1.21.3 send position before the confirmation) |
|
|
32
|
-
| Vehicles | rotation packets only | boat simulation with paddle / input / vehicle move packets, minecart riding |
|
|
39
|
+
| Vehicles | rotation packets only | boat simulation with paddle / input / vehicle move packets (input applies a tick later, as in the game), minecart riding |
|
|
33
40
|
| Brand, 1.20.2+ | sent in play state | sent in the configuration phase, once |
|
|
41
|
+
| Open container | keeps moving | movement keys are ignored while a window is open, as in the game |
|
|
34
42
|
|
|
35
|
-
It also
|
|
43
|
+
It also works around mineflayer issues that break movement:
|
|
36
44
|
|
|
37
45
|
- an empty full chunk (no sections) sent to a 1.16 client is treated as an unload, so the player never ticks in it;
|
|
38
46
|
- a removed vehicle is never dismounted (`entityGone` is listened for on the wrong emitter);
|
|
39
|
-
- an out-of-range hotbar slot from the server throws inside the packet handler
|
|
47
|
+
- an out-of-range hotbar slot from the server throws inside the packet handler;
|
|
48
|
+
- player attributes are misread: stored under `undefined` on 1.16.5 – 1.20.4 and named from an outdated id table
|
|
49
|
+
on 1.21+ (on 1.21.2 – 1.21.5 movement speed arrives as step height), so speed effects and step height go wrong;
|
|
50
|
+
- `set_passengers` without the bot never clears `bot.vehicle`, so after every dismount the bot keeps acting as a passenger;
|
|
51
|
+
- enchantments on 1.20.5+ items are not a list (reading boots throws), and from 1.21 their ids come from the server's registry;
|
|
52
|
+
- the 1.16 data numbers dolphin's grace wrong and marks every entity, arrows and items included, as a mob;
|
|
53
|
+
- recipe packets on 1.20.5 – 1.21.3 and advancements on 1.21.2 – 1.21.3 fail to parse; they arrive as raw buffers there.
|
|
54
|
+
|
|
55
|
+
Before 1.18.2 the client only reports moves longer than 0.03. Airborne moves shorter than that are still reported,
|
|
56
|
+
and on 1.17 – 1.18.1 the mouse holds a turn for up to two seconds while such moves last, because anti-cheats
|
|
57
|
+
cannot reconstruct the skipped vertical movement.
|
|
58
|
+
|
|
59
|
+
## Performance
|
|
60
|
+
|
|
61
|
+
All bots in a process share one 20 TPS clock. Inside a tick the engine memoises chunk columns and block data and
|
|
62
|
+
reuses its collision boxes, so a player tick costs about 3 µs (`node bench/tick.js`, 50 players on a course of
|
|
63
|
+
slabs, fences, water, cobwebs and honey). `node bench/trajectory.js --check` compares a fingerprint of every
|
|
64
|
+
simulated position with `bench/baseline.json`, which is how optimisations are checked to change nothing.
|
|
40
65
|
|
|
41
66
|
## Installation
|
|
42
67
|
|
|
@@ -131,16 +156,19 @@ See [`test/helpers.js`](test/helpers.js) for a complete in-memory world.
|
|
|
131
156
|
## What is simulated
|
|
132
157
|
|
|
133
158
|
- gravity, air drag and ground friction (slipperiness per block, supporting block on 1.20+)
|
|
134
|
-
- collisions against block shapes, step-up, sneaking edge back-off
|
|
135
|
-
- walking, sprinting (double-tap and key), sprint-jumping,
|
|
136
|
-
- water and lava with flow pushing, climbing (ladders, vines, scaffolding, trapdoors over ladders)
|
|
137
|
-
- cobweb, sweet berry bush, powder snow, honey
|
|
138
|
-
-
|
|
159
|
+
- collisions against block shapes, step-up, sneaking edge back-off
|
|
160
|
+
- walking, sprinting (double-tap and key), sprint-jumping, crouching, crawling and swimming poses
|
|
161
|
+
- water and lava with flow pushing, swimming, climbing (ladders, vines, scaffolding, trapdoors over ladders, powder snow in leather boots)
|
|
162
|
+
- cobweb (and weaving), sweet berry bush, powder snow, honey sliding, bubble columns, soul sand, slime and bed bouncing
|
|
163
|
+
- being pushed out of blocks and by overlapping entities
|
|
164
|
+
- levitation, slow falling, jump boost, dolphin's grace, depth strider and soul speed,
|
|
165
|
+
attributes: movement speed, gravity, jump strength, step height, sneaking speed, movement and water efficiency
|
|
166
|
+
- using items: eating, drinking, blocking with a shield, drawing a bow
|
|
139
167
|
- creative flight, including toggling with a double jump and landing
|
|
140
168
|
- knockback and explosion velocity, 1.21.2+ relative teleports with velocity
|
|
141
169
|
- riding boats (client-controlled) and minecarts
|
|
142
170
|
|
|
143
|
-
Not simulated yet: elytra flight,
|
|
171
|
+
Not simulated yet: elytra flight, riptide, world border.
|
|
144
172
|
|
|
145
173
|
## Tests
|
|
146
174
|
|
|
@@ -148,20 +176,20 @@ Not simulated yet: elytra flight, swimming pose, bubble columns, entity collisio
|
|
|
148
176
|
npm test
|
|
149
177
|
```
|
|
150
178
|
|
|
151
|
-
Offline tests for 1.16.5
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
## Releasing
|
|
179
|
+
Offline tests for 1.16.5 – 26.1 cover free fall, landing on blocks and slabs, walking and sprinting speed,
|
|
180
|
+
jump height, step-up, walls and corners, sneaking at an edge, input per version, swimming, cobwebs, bubble columns,
|
|
181
|
+
powder snow, ice, soul sand, pushing, item use, and the position reporting thresholds.
|
|
155
182
|
|
|
156
|
-
|
|
183
|
+
## Layout
|
|
157
184
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
185
|
+
```
|
|
186
|
+
lib/engine world access, collisions, version switches, boat
|
|
187
|
+
lib/engine/player the player tick: input, fluids, blocks under the player, move, travel, block effects and their
|
|
188
|
+
traversal order, pose, pushing
|
|
189
|
+
lib/client mineflayer integration: controls, teleports, server packets, position reporting, riding, item use,
|
|
190
|
+
entity interaction, registry tables, protocol fixes, the shared clock
|
|
191
|
+
bench tick cost and bit-exact output fingerprints
|
|
192
|
+
```
|
|
165
193
|
|
|
166
194
|
## License
|
|
167
195
|
|
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const spectraPhysics = require('./lib/
|
|
4
|
-
const { PlayerPhysics } = require('./lib/engine/
|
|
3
|
+
const spectraPhysics = require('./lib/client')
|
|
4
|
+
const { PlayerPhysics } = require('./lib/engine/player')
|
|
5
5
|
const BoatPhysics = require('./lib/engine/boat')
|
|
6
6
|
const WorldView = require('./lib/engine/world')
|
|
7
7
|
const AABB = require('./lib/engine/aabb')
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// One 20 TPS timer for every bot in the process (Minecraft's Timer: catches up at most 10 ticks,
|
|
3
|
+
// gives up after falling a second behind).
|
|
4
|
+
|
|
5
|
+
const { performance } = require('perf_hooks')
|
|
6
|
+
|
|
7
|
+
const TICK_MS = 50
|
|
8
|
+
const MAX_CATCH_UP = 10
|
|
9
|
+
|
|
10
|
+
const tickers = new Set()
|
|
11
|
+
let timer = null
|
|
12
|
+
let nextAt = 0
|
|
13
|
+
|
|
14
|
+
function loop () {
|
|
15
|
+
const now = performance.now()
|
|
16
|
+
for (let n = 0; now >= nextAt && n < MAX_CATCH_UP; n++) {
|
|
17
|
+
for (const tick of tickers) tick()
|
|
18
|
+
nextAt += TICK_MS
|
|
19
|
+
}
|
|
20
|
+
if (now - nextAt > 1000) nextAt = now + TICK_MS
|
|
21
|
+
timer = tickers.size ? setTimeout(loop, Math.max(0, nextAt - performance.now())) : null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function startTicking (tick) {
|
|
25
|
+
tickers.add(tick)
|
|
26
|
+
if (timer) return
|
|
27
|
+
nextAt = performance.now() + TICK_MS
|
|
28
|
+
timer = setTimeout(loop, TICK_MS)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function stopTicking (tick) {
|
|
32
|
+
tickers.delete(tick)
|
|
33
|
+
if (tickers.size || !timer) return
|
|
34
|
+
clearTimeout(timer)
|
|
35
|
+
timer = null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { startTicking, stopTicking, TICK_MS }
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// Outgoing packets. Also reorders what mineflayer sends on login so it matches the game client:
|
|
3
|
+
// brand + client information in the configuration phase (1.20.2+), settings before brand on older versions,
|
|
4
|
+
// and player_loaded only when the level has actually loaded.
|
|
5
|
+
|
|
6
|
+
// actions the game sends from Minecraft#handleKeybinds, before the movement packet of the same tick
|
|
7
|
+
const TICK_ACTIONS = new Set(['use_item', 'block_dig', 'block_place', 'use_entity', 'attack', 'arm_animation', 'entity_action', 'spectate'])
|
|
8
|
+
|
|
9
|
+
const isBrand = (channel) => channel === 'minecraft:brand' || channel === 'MC|Brand'
|
|
10
|
+
|
|
11
|
+
function brandPayload (brand) {
|
|
12
|
+
const text = Buffer.from(brand, 'utf8')
|
|
13
|
+
const length = []
|
|
14
|
+
let n = text.length
|
|
15
|
+
do {
|
|
16
|
+
let byte = n & 0x7f
|
|
17
|
+
n >>>= 7
|
|
18
|
+
if (n) byte |= 0x80
|
|
19
|
+
length.push(byte)
|
|
20
|
+
} while (n)
|
|
21
|
+
return { channel: 'minecraft:brand', data: Buffer.concat([Buffer.from(length), text]) }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function installConnection (ctx) {
|
|
25
|
+
const { client, V, state } = ctx
|
|
26
|
+
const rawWrite = client.write.bind(client)
|
|
27
|
+
const brand = ctx.options.brand ?? client.options?.brand ?? 'vanilla'
|
|
28
|
+
let ours = false
|
|
29
|
+
|
|
30
|
+
const report = (name, params) => {
|
|
31
|
+
for (const watch of ctx.outgoing) watch(name, params)
|
|
32
|
+
if (ctx.onSend) ctx.onSend(name, params)
|
|
33
|
+
if (ctx.debug) ctx.debug('C->S', name, JSON.stringify(params))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ctx.send = (name, params) => {
|
|
37
|
+
report(name, params)
|
|
38
|
+
ours = true
|
|
39
|
+
try { rawWrite(name, params) } finally { ours = false }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const queued = []
|
|
43
|
+
|
|
44
|
+
client.write = function (name, params) {
|
|
45
|
+
if (!ours && TICK_ACTIONS.has(name) && client.state === 'play' && state.inLevel) {
|
|
46
|
+
queued.push([name, params])
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
if (!ours) {
|
|
50
|
+
if (name === 'player_loaded' && V.clientLoaded) return
|
|
51
|
+
if (name === 'custom_payload' && isBrand(params?.channel) && client.state === 'play') {
|
|
52
|
+
if (V.configPhase && state.configBrandSent) return
|
|
53
|
+
if (!V.configPhase && !state.playSettingsSent) {
|
|
54
|
+
state.heldBrand = params
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (name === 'settings') {
|
|
59
|
+
if (client.state === 'configuration' && V.configPhase && !state.configBrandSent) {
|
|
60
|
+
ctx.send('custom_payload', brandPayload(brand))
|
|
61
|
+
state.configBrandSent = true
|
|
62
|
+
} else if (client.state === 'play') {
|
|
63
|
+
if (state.suppressPlaySettings) {
|
|
64
|
+
state.suppressPlaySettings = false
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
report(name, params)
|
|
68
|
+
rawWrite(name, params)
|
|
69
|
+
state.playSettingsSent = true
|
|
70
|
+
if (state.heldBrand) {
|
|
71
|
+
const held = state.heldBrand
|
|
72
|
+
state.heldBrand = null
|
|
73
|
+
rawWrite('custom_payload', held)
|
|
74
|
+
}
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
report(name, params)
|
|
79
|
+
}
|
|
80
|
+
return rawWrite(name, params)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
client.on('finish_configuration', () => { state.suppressPlaySettings = true })
|
|
84
|
+
|
|
85
|
+
// use_item carries the rotation of the tick it is sent in (checked by servers since 1.21)
|
|
86
|
+
ctx.flushActions = () => {
|
|
87
|
+
while (queued.length) {
|
|
88
|
+
const [name, params] = queued.shift()
|
|
89
|
+
if (name === 'use_item' && params.rotation) params.rotation = { x: ctx.player.yRot, y: ctx.player.xRot }
|
|
90
|
+
report(name, params)
|
|
91
|
+
rawWrite(name, params)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
ctx.flushHeldBrand = () => {
|
|
96
|
+
if (!state.heldBrand) return
|
|
97
|
+
const held = state.heldBrand
|
|
98
|
+
state.heldBrand = null
|
|
99
|
+
ctx.send('custom_payload', held)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { installConnection }
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// mineflayer movement API on top of the simulated player: control states and mouse-like turning.
|
|
3
|
+
|
|
4
|
+
const conv = require('mineflayer/lib/conversions')
|
|
5
|
+
const { f32, clamp, wrapDegrees } = require('../engine/mth')
|
|
6
|
+
|
|
7
|
+
const MOUSE_STEP = f32(0.15) // degrees per mouse pixel at default sensitivity
|
|
8
|
+
const RELEASED = Object.freeze({ forward: false, back: false, left: false, right: false, jump: false, sprint: false, sneak: false })
|
|
9
|
+
|
|
10
|
+
function installControls (ctx) {
|
|
11
|
+
const { bot, player } = ctx
|
|
12
|
+
const keys = { ...RELEASED }
|
|
13
|
+
const turnSpeed = ctx.options.turnSpeed ?? 35
|
|
14
|
+
let lookWaiters = []
|
|
15
|
+
|
|
16
|
+
bot.setControlState = (control, value) => {
|
|
17
|
+
if (!(control in keys)) throw new Error(`invalid control: ${control}`)
|
|
18
|
+
keys[control] = !!value
|
|
19
|
+
}
|
|
20
|
+
bot.getControlState = (control) => keys[control]
|
|
21
|
+
bot.clearControlStates = () => { for (const k in keys) keys[k] = false }
|
|
22
|
+
bot.controlState = {}
|
|
23
|
+
for (const k of Object.keys(keys)) {
|
|
24
|
+
Object.defineProperty(bot.controlState, k, { get: () => keys[k], set: (v) => { keys[k] = !!v } })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// with a container open the game client stops reading movement keys
|
|
28
|
+
ctx.keys = () => bot.currentWindow ? RELEASED : keys
|
|
29
|
+
|
|
30
|
+
ctx.turn = (maxDegrees = turnSpeed) => {
|
|
31
|
+
const targetYaw = conv.toNotchianYaw(bot.entity.yaw)
|
|
32
|
+
const targetPitch = conv.toNotchianPitch(bot.entity.pitch)
|
|
33
|
+
const limit = maxDegrees / MOUSE_STEP
|
|
34
|
+
const dx = clamp(Math.round(wrapDegrees(targetYaw - player.yRot) / MOUSE_STEP), -limit, limit)
|
|
35
|
+
const dy = clamp(Math.round((targetPitch - player.xRot) / MOUSE_STEP), -limit, limit)
|
|
36
|
+
if (dx !== 0) player.yRot = f32(player.yRot + f32(f32(dx) * MOUSE_STEP))
|
|
37
|
+
if (dy !== 0) player.xRot = f32(clamp(f32(player.xRot + f32(f32(dy) * MOUSE_STEP)), -90, 90))
|
|
38
|
+
const reached = Math.abs(wrapDegrees(targetYaw - player.yRot)) < MOUSE_STEP && Math.abs(targetPitch - player.xRot) < MOUSE_STEP
|
|
39
|
+
if (reached && lookWaiters.length) {
|
|
40
|
+
const waiters = lookWaiters
|
|
41
|
+
lookWaiters = []
|
|
42
|
+
for (const resolve of waiters) resolve()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
bot.look = async (yaw, pitch, force) => {
|
|
47
|
+
bot.entity.yaw = yaw
|
|
48
|
+
bot.entity.pitch = pitch
|
|
49
|
+
if (force) {
|
|
50
|
+
ctx.turn(Infinity)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
await new Promise(resolve => lookWaiters.push(resolve))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
bot.lookAt = async (point, force) => {
|
|
57
|
+
const delta = point.minus(bot.entity.position.offset(0, player.eyeHeight(), 0))
|
|
58
|
+
const yaw = Math.atan2(-delta.x, -delta.z)
|
|
59
|
+
const pitch = Math.atan2(delta.y, Math.sqrt(delta.x * delta.x + delta.z * delta.z))
|
|
60
|
+
await bot.look(yaw, pitch, force)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
bot.waitForTicks = async (ticks) => {
|
|
64
|
+
if (ticks <= 0) return
|
|
65
|
+
await new Promise(resolve => {
|
|
66
|
+
const onTick = () => {
|
|
67
|
+
if (--ticks > 0) return
|
|
68
|
+
bot.removeListener('physicsTick', onTick)
|
|
69
|
+
resolve()
|
|
70
|
+
}
|
|
71
|
+
bot.on('physicsTick', onTick)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
bot.elytraFly = async () => { throw new Error('elytra flight is not simulated') }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { installControls }
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// What the movement engine needs to know about the player besides the world.
|
|
3
|
+
|
|
4
|
+
const { attributeKeyMapper, effectIdMapper, createEnchantmentReader, entityCategory } = require('./registries')
|
|
5
|
+
|
|
6
|
+
const SPRINT_UUID = '662a6b8d-da3e-4c1c-8813-96ea6097278d'
|
|
7
|
+
const OPERATIONS = { add_value: 0, add_multiplied_base: 1, add_multiplied_total: 2 }
|
|
8
|
+
const PUSH_RANGE = 4
|
|
9
|
+
const PUSHABLE_OBJECTS = /boat|raft|minecart/
|
|
10
|
+
const LIVING_TYPES = new Set(['player', 'mob', 'animal', 'hostile', 'passive', 'water_creature', 'ambient'])
|
|
11
|
+
const NEVER_PUSHES = new Set(['armor_stand', 'bat', 'parrot'])
|
|
12
|
+
const BOOTS_SLOT = 8
|
|
13
|
+
|
|
14
|
+
const SPRINT_IDS = new Set([SPRINT_UUID, 'minecraft:sprinting', 'sprinting'])
|
|
15
|
+
const isSprintModifier = (m) => SPRINT_IDS.has(m.uuid) || SPRINT_IDS.has(m.id)
|
|
16
|
+
const operationOf = (m) => typeof m.operation === 'number' ? m.operation : (OPERATIONS[m.operation] ?? 0)
|
|
17
|
+
|
|
18
|
+
// AttributeInstance#calculateValue, with the sprint modifier owned by the client
|
|
19
|
+
function attributeValue (property, extra = []) {
|
|
20
|
+
const modifiers = property.modifiers.filter(m => !isSprintModifier(m)).concat(extra)
|
|
21
|
+
let base = property.value
|
|
22
|
+
for (const m of modifiers) if (operationOf(m) === 0) base += m.amount
|
|
23
|
+
let value = base
|
|
24
|
+
for (const m of modifiers) if (operationOf(m) === 1) value += base * m.amount
|
|
25
|
+
for (const m of modifiers) if (operationOf(m) === 2) value *= 1 + m.amount
|
|
26
|
+
return value
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createEnvironment (bot, state = {}) {
|
|
30
|
+
const effectId = effectIdMapper(bot.registry)
|
|
31
|
+
const enchantmentLevel = createEnchantmentReader(bot)
|
|
32
|
+
const attributeName = attributeKeyMapper(bot.registry)
|
|
33
|
+
|
|
34
|
+
// kept from the packets directly: mineflayer files 1.16 - 1.20.4 attributes under "undefined"
|
|
35
|
+
let attributes = {}
|
|
36
|
+
const onAttributes = (packet) => {
|
|
37
|
+
if (packet.entityId !== bot.entity?.id) return
|
|
38
|
+
for (const p of packet.properties) attributes[attributeName(p.key ?? p.name)] = { value: p.value, modifiers: p.modifiers ?? [] }
|
|
39
|
+
}
|
|
40
|
+
bot._client.on('entity_update_attributes', onAttributes)
|
|
41
|
+
bot._client.on('update_attributes', onAttributes)
|
|
42
|
+
bot._client.on('login', () => { attributes = {} })
|
|
43
|
+
|
|
44
|
+
const findAttribute = (name) => attributes[name] ?? null
|
|
45
|
+
|
|
46
|
+
function boots () {
|
|
47
|
+
return bot.inventory?.slots?.[BOOTS_SLOT] ?? null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isPushable (e) {
|
|
51
|
+
if (!e || e === bot.entity || !e.position || e.isValid === false) return false
|
|
52
|
+
if (e === bot.vehicle || e.vehicle === bot.entity) return false
|
|
53
|
+
if (NEVER_PUSHES.has(e.name)) return false
|
|
54
|
+
// living entities, boats and minecarts push the player; items, projectiles and frames do not
|
|
55
|
+
return LIVING_TYPES.has(entityCategory(e)) || PUSHABLE_OBJECTS.test(e.name ?? '')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
attribute (name, fallback) {
|
|
60
|
+
const property = findAttribute(name)
|
|
61
|
+
return property ? attributeValue(property) : fallback
|
|
62
|
+
},
|
|
63
|
+
movementSpeed (sprinting) {
|
|
64
|
+
const property = findAttribute('movement_speed') ?? { value: 0.1, modifiers: [] }
|
|
65
|
+
return attributeValue(property, sprinting ? [{ amount: 0.3, operation: 2 }] : [])
|
|
66
|
+
},
|
|
67
|
+
effect (name) {
|
|
68
|
+
const id = effectId(name)
|
|
69
|
+
if (id == null) return null
|
|
70
|
+
const effect = bot.entity?.effects?.[id]
|
|
71
|
+
return effect ? effect.amplifier : null
|
|
72
|
+
},
|
|
73
|
+
bootsEnchant: (name) => enchantmentLevel(boots(), name),
|
|
74
|
+
bootsItem: () => boots()?.name ?? null,
|
|
75
|
+
food: () => bot.food ?? 20,
|
|
76
|
+
usingItem: () => state.using != null,
|
|
77
|
+
gameMode: () => bot.game?.gameMode,
|
|
78
|
+
ultraWarm: () => /nether/.test(String(bot.game?.dimension ?? '')),
|
|
79
|
+
nearbyEntities () {
|
|
80
|
+
const me = bot.entity?.position
|
|
81
|
+
if (!me) return []
|
|
82
|
+
const out = []
|
|
83
|
+
for (const e of Object.values(bot.entities)) {
|
|
84
|
+
if (!isPushable(e)) continue
|
|
85
|
+
const p = e.position
|
|
86
|
+
if (Math.abs(p.x - me.x) > PUSH_RANGE || Math.abs(p.y - me.y) > PUSH_RANGE || Math.abs(p.z - me.z) > PUSH_RANGE) continue
|
|
87
|
+
out.push({ x: p.x, y: p.y, z: p.z, width: e.width ?? 0.6, height: e.height ?? 1.8 })
|
|
88
|
+
}
|
|
89
|
+
return out
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { createEnvironment }
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// SpectraPhysics: replacement for mineflayer's `physics` plugin that ticks and reports movement like the game client.
|
|
3
|
+
|
|
4
|
+
const { versionFlags } = require('../engine/version')
|
|
5
|
+
const WorldView = require('../engine/world')
|
|
6
|
+
const { PlayerPhysics } = require('../engine/player')
|
|
7
|
+
const { createEnvironment } = require('./environment')
|
|
8
|
+
const { installConnection } = require('./connection')
|
|
9
|
+
const { installControls } = require('./controls')
|
|
10
|
+
const { installTeleport } = require('./teleport')
|
|
11
|
+
const { installServerHandlers } = require('./server')
|
|
12
|
+
const { installReporter } = require('./reporter')
|
|
13
|
+
const { installRiding } = require('./riding')
|
|
14
|
+
const { installTicker } = require('./ticker')
|
|
15
|
+
const { installItemUse } = require('./items')
|
|
16
|
+
const { installInteract } = require('./interact')
|
|
17
|
+
const { installProtocolPatches } = require('./protocol')
|
|
18
|
+
|
|
19
|
+
function debugLogger (option) {
|
|
20
|
+
if (typeof option === 'function') return option
|
|
21
|
+
return option ? (...args) => console.log('[spectra]', ...args) : null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function spectraPhysics (options = {}) {
|
|
25
|
+
return function inject (bot) {
|
|
26
|
+
const registry = bot.registry
|
|
27
|
+
const V = versionFlags(registry)
|
|
28
|
+
const world = new WorldView(bot)
|
|
29
|
+
const state = {
|
|
30
|
+
inLevel: false,
|
|
31
|
+
configBrandSent: false,
|
|
32
|
+
suppressPlaySettings: false,
|
|
33
|
+
playSettingsSent: false,
|
|
34
|
+
heldBrand: null,
|
|
35
|
+
selectedSlot: 0,
|
|
36
|
+
carriedSlot: 0,
|
|
37
|
+
lastRotationAt: 0,
|
|
38
|
+
using: null
|
|
39
|
+
}
|
|
40
|
+
const player = new PlayerPhysics(world, V, createEnvironment(bot, state))
|
|
41
|
+
|
|
42
|
+
const ctx = {
|
|
43
|
+
bot,
|
|
44
|
+
client: bot._client,
|
|
45
|
+
registry,
|
|
46
|
+
V,
|
|
47
|
+
world,
|
|
48
|
+
player,
|
|
49
|
+
options,
|
|
50
|
+
debug: debugLogger(options.debug),
|
|
51
|
+
onSend: options.onSend ?? null,
|
|
52
|
+
outgoing: [],
|
|
53
|
+
state
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
bot.physicsEnabled = true
|
|
57
|
+
bot.physics = { gravity: 0.08, yawSpeed: 3, pitchSpeed: 3, stepHeight: 0.6, engine: 'spectra' }
|
|
58
|
+
bot.spectra = {
|
|
59
|
+
player,
|
|
60
|
+
flags: V,
|
|
61
|
+
world,
|
|
62
|
+
state: ctx.state,
|
|
63
|
+
get lastRotationAt () { return ctx.state.lastRotationAt },
|
|
64
|
+
get onSend () { return ctx.onSend },
|
|
65
|
+
set onSend (fn) { ctx.onSend = typeof fn === 'function' ? fn : null }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
installProtocolPatches(ctx)
|
|
69
|
+
installConnection(ctx)
|
|
70
|
+
installItemUse(ctx)
|
|
71
|
+
installControls(ctx)
|
|
72
|
+
installInteract(ctx)
|
|
73
|
+
installReporter(ctx)
|
|
74
|
+
installTicker(ctx)
|
|
75
|
+
installTeleport(ctx)
|
|
76
|
+
installServerHandlers(ctx)
|
|
77
|
+
installRiding(ctx)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = spectraPhysics
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// Right click on an entity the way Minecraft#startUseItem does it: aim, interact_at with the hit point,
|
|
3
|
+
// then interact (a single packet with the location from 26.1), and leaving a vehicle by holding sneak.
|
|
4
|
+
|
|
5
|
+
const { Vec3 } = require('vec3')
|
|
6
|
+
|
|
7
|
+
const DISMOUNT_TICKS = 3
|
|
8
|
+
|
|
9
|
+
// where the look ray enters the entity's box, relative to the entity position
|
|
10
|
+
function hitPoint (eye, look, entity) {
|
|
11
|
+
const w = (entity.width ?? 0.6) / 2
|
|
12
|
+
const h = entity.height ?? 1.8
|
|
13
|
+
const p = entity.position
|
|
14
|
+
const min = [p.x - w, p.y, p.z - w]
|
|
15
|
+
const max = [p.x + w, p.y + h, p.z + w]
|
|
16
|
+
const origin = [eye.x, eye.y, eye.z]
|
|
17
|
+
const dir = [look.x, look.y, look.z]
|
|
18
|
+
let near = -Infinity
|
|
19
|
+
let far = Infinity
|
|
20
|
+
for (let i = 0; i < 3; i++) {
|
|
21
|
+
if (Math.abs(dir[i]) < 1e-9) {
|
|
22
|
+
if (origin[i] < min[i] || origin[i] > max[i]) return new Vec3(0, h / 2, 0)
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
let t1 = (min[i] - origin[i]) / dir[i]
|
|
26
|
+
let t2 = (max[i] - origin[i]) / dir[i]
|
|
27
|
+
if (t1 > t2) [t1, t2] = [t2, t1]
|
|
28
|
+
near = Math.max(near, t1)
|
|
29
|
+
far = Math.min(far, t2)
|
|
30
|
+
}
|
|
31
|
+
if (near > far || far < 0) return new Vec3(0, h / 2, 0)
|
|
32
|
+
const t = Math.max(near, 0)
|
|
33
|
+
return new Vec3(eye.x + dir[0] * t - p.x, eye.y + dir[1] * t - p.y, eye.z + dir[2] * t - p.z)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function installInteract (ctx) {
|
|
37
|
+
const { bot, client, registry, player, state } = ctx
|
|
38
|
+
const useEntityType = JSON.stringify(registry.protocol?.play?.toServer?.types?.packet_use_entity ?? '')
|
|
39
|
+
const singleInteract = !useEntityType.includes('"mouse"')
|
|
40
|
+
state.dismountTicks = 0
|
|
41
|
+
|
|
42
|
+
function lookVector () {
|
|
43
|
+
const yaw = player.yRot * Math.PI / 180
|
|
44
|
+
const pitch = player.xRot * Math.PI / 180
|
|
45
|
+
return { x: -Math.sin(yaw) * Math.cos(pitch), y: -Math.sin(pitch), z: Math.cos(yaw) * Math.cos(pitch) }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function rightClick (entity, swing) {
|
|
49
|
+
if (!entity?.position) throw new Error('entity is gone')
|
|
50
|
+
await bot.lookAt(entity.position.offset(0, (entity.height ?? 1) / 2, 0))
|
|
51
|
+
const eye = { x: player.pos.x, y: player.pos.y + player.eyeHeight(), z: player.pos.z }
|
|
52
|
+
const hit = hitPoint(eye, lookVector(), entity)
|
|
53
|
+
const sneaking = !!ctx.keys().sneak
|
|
54
|
+
if (singleInteract) {
|
|
55
|
+
client.write('use_entity', { target: entity.id, hand: 'main_hand', location: { x: hit.x, y: hit.y, z: hit.z }, sneaking })
|
|
56
|
+
} else {
|
|
57
|
+
client.write('use_entity', { target: entity.id, mouse: 2, x: hit.x, y: hit.y, z: hit.z, hand: 0, sneaking })
|
|
58
|
+
client.write('use_entity', { target: entity.id, mouse: 0, hand: 0, sneaking })
|
|
59
|
+
}
|
|
60
|
+
if (swing) client.write('arm_animation', { hand: 0 })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
bot.useOn = (entity) => rightClick(entity, false)
|
|
64
|
+
bot.mount = (entity) => rightClick(entity, true)
|
|
65
|
+
|
|
66
|
+
bot.dismount = () => {
|
|
67
|
+
if (!bot.vehicle) {
|
|
68
|
+
bot.emit('error', new Error('dismount: not mounted'))
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
state.dismountTicks = DISMOUNT_TICKS
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const readKeys = ctx.keys
|
|
75
|
+
ctx.keys = () => state.dismountTicks > 0 ? { ...readKeys(), sneak: true } : readKeys()
|
|
76
|
+
bot.on('physicsTick', () => { if (state.dismountTicks > 0) state.dismountTicks-- })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { installInteract, hitPoint }
|