spectra-physics 1.0.0 → 1.1.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 +31 -22
- package/index.js +2 -2
- package/lib/client/connection.js +83 -0
- package/lib/client/controls.js +78 -0
- package/lib/client/environment.js +125 -0
- package/lib/client/index.js +72 -0
- package/lib/client/reporter.js +91 -0
- package/lib/client/riding.js +65 -0
- package/lib/client/server.js +90 -0
- package/lib/client/teleport.js +108 -0
- package/lib/client/ticker.js +144 -0
- package/lib/engine/player/blocks.js +134 -0
- package/lib/engine/player/effects.js +152 -0
- package/lib/engine/player/fluids.js +141 -0
- package/lib/engine/player/index.js +202 -0
- package/lib/engine/player/input.js +86 -0
- package/lib/engine/player/move.js +135 -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/version.js +49 -26
- package/lib/engine/world.js +3 -0
- package/package.json +1 -1
- package/lib/engine/physics.js +0 -780
- package/lib/plugin.js +0 -574
package/README.md
CHANGED
|
@@ -22,21 +22,31 @@ 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, after travel along the path from 1.21.2 |
|
|
26
29
|
| Rotation | continuous interpolation | whole mouse steps of 0.15°, accumulated in float |
|
|
27
|
-
| Sprint | applied the same tick | speed
|
|
30
|
+
| Sprint | applied the same tick | speed follows the sprint state in the tick it is sent, as the server applies it |
|
|
28
31
|
| Tick loop | fixed interval | `Minecraft` timer: catches up at most 10 ticks |
|
|
29
32
|
| 1.21.2+ | no `tick_end` | `tick_end` after every tick, `player_input` on key change |
|
|
30
33
|
| 1.21.4+ | `player_loaded` on first health update | sent once the level around the player is loaded |
|
|
31
34
|
| Teleport answer | one order for all versions | per version (1.21.2–1.21.3 send position before the confirmation) |
|
|
32
35
|
| Vehicles | rotation packets only | boat simulation with paddle / input / vehicle move packets, minecart riding |
|
|
33
36
|
| Brand, 1.20.2+ | sent in play state | sent in the configuration phase, once |
|
|
37
|
+
| Open container | keeps moving | movement keys are ignored while a window is open, as in the game |
|
|
34
38
|
|
|
35
|
-
It also
|
|
39
|
+
It also works around mineflayer issues that break movement:
|
|
36
40
|
|
|
37
41
|
- 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
42
|
- 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
|
|
43
|
+
- an out-of-range hotbar slot from the server throws inside the packet handler;
|
|
44
|
+
- player attributes are misread: stored under `undefined` on 1.16.5 – 1.20.4 and named from an outdated id table
|
|
45
|
+
on 1.21+ (on 1.21.2 – 1.21.5 movement speed arrives as step height), so speed effects and step height go wrong.
|
|
46
|
+
|
|
47
|
+
Before 1.18.2 the client only reports moves longer than 0.03. Airborne moves shorter than that are still reported,
|
|
48
|
+
and on 1.17 – 1.18.1 the mouse holds a turn for up to two seconds while such moves last, because anti-cheats
|
|
49
|
+
cannot reconstruct the skipped vertical movement.
|
|
40
50
|
|
|
41
51
|
## Installation
|
|
42
52
|
|
|
@@ -131,16 +141,18 @@ See [`test/helpers.js`](test/helpers.js) for a complete in-memory world.
|
|
|
131
141
|
## What is simulated
|
|
132
142
|
|
|
133
143
|
- 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
|
-
-
|
|
144
|
+
- collisions against block shapes, step-up, sneaking edge back-off
|
|
145
|
+
- walking, sprinting (double-tap and key), sprint-jumping, crouching, crawling and swimming poses
|
|
146
|
+
- water and lava with flow pushing, swimming, climbing (ladders, vines, scaffolding, trapdoors over ladders, powder snow in leather boots)
|
|
147
|
+
- cobweb (and weaving), sweet berry bush, powder snow, honey sliding, bubble columns, soul sand, slime and bed bouncing
|
|
148
|
+
- being pushed out of blocks and by overlapping entities
|
|
149
|
+
- levitation, slow falling, jump boost, dolphin's grace, depth strider and soul speed,
|
|
150
|
+
attributes: movement speed, gravity, jump strength, step height, sneaking speed, movement and water efficiency
|
|
139
151
|
- creative flight, including toggling with a double jump and landing
|
|
140
152
|
- knockback and explosion velocity, 1.21.2+ relative teleports with velocity
|
|
141
153
|
- riding boats (client-controlled) and minecarts
|
|
142
154
|
|
|
143
|
-
Not simulated yet: elytra flight,
|
|
155
|
+
Not simulated yet: elytra flight, riptide, world border.
|
|
144
156
|
|
|
145
157
|
## Tests
|
|
146
158
|
|
|
@@ -148,20 +160,17 @@ Not simulated yet: elytra flight, swimming pose, bubble columns, entity collisio
|
|
|
148
160
|
npm test
|
|
149
161
|
```
|
|
150
162
|
|
|
151
|
-
Offline tests for 1.16.5
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
## Releasing
|
|
155
|
-
|
|
156
|
-
Releases are published to [npm](https://www.npmjs.com/package/spectra-physics) by GitHub Actions.
|
|
163
|
+
Offline tests for 1.16.5 – 26.1 cover free fall, landing on blocks and slabs, walking and sprinting speed,
|
|
164
|
+
jump height, step-up, walls and corners, sneaking at an edge, input per version, swimming, cobwebs, bubble columns,
|
|
165
|
+
ice, soul sand, pushing, and the position reporting thresholds.
|
|
157
166
|
|
|
158
|
-
|
|
159
|
-
2. Create a GitHub release with the tag `v<version>` (for example `v1.0.1`).
|
|
167
|
+
## Layout
|
|
160
168
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
169
|
+
```
|
|
170
|
+
lib/engine world access, collisions, version switches, boat
|
|
171
|
+
lib/engine/player the player tick: input, fluids, blocks under the player, move, travel, block effects, pose, pushing
|
|
172
|
+
lib/client mineflayer integration: controls, teleports, server packets, position reporting, riding, tick loop
|
|
173
|
+
```
|
|
165
174
|
|
|
166
175
|
## License
|
|
167
176
|
|
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,83 @@
|
|
|
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
|
+
const isBrand = (channel) => channel === 'minecraft:brand' || channel === 'MC|Brand'
|
|
7
|
+
|
|
8
|
+
function brandPayload (brand) {
|
|
9
|
+
const text = Buffer.from(brand, 'utf8')
|
|
10
|
+
const length = []
|
|
11
|
+
let n = text.length
|
|
12
|
+
do {
|
|
13
|
+
let byte = n & 0x7f
|
|
14
|
+
n >>>= 7
|
|
15
|
+
if (n) byte |= 0x80
|
|
16
|
+
length.push(byte)
|
|
17
|
+
} while (n)
|
|
18
|
+
return { channel: 'minecraft:brand', data: Buffer.concat([Buffer.from(length), text]) }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function installConnection (ctx) {
|
|
22
|
+
const { client, V, state } = ctx
|
|
23
|
+
const rawWrite = client.write.bind(client)
|
|
24
|
+
const brand = ctx.options.brand ?? client.options?.brand ?? 'vanilla'
|
|
25
|
+
let ours = false
|
|
26
|
+
|
|
27
|
+
const report = (name, params) => {
|
|
28
|
+
if (ctx.onSend) ctx.onSend(name, params)
|
|
29
|
+
if (ctx.debug) ctx.debug('C->S', name, JSON.stringify(params))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
ctx.send = (name, params) => {
|
|
33
|
+
report(name, params)
|
|
34
|
+
ours = true
|
|
35
|
+
try { rawWrite(name, params) } finally { ours = false }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
client.write = function (name, params) {
|
|
39
|
+
if (!ours) {
|
|
40
|
+
if (name === 'player_loaded' && V.clientLoaded) return
|
|
41
|
+
if (name === 'custom_payload' && isBrand(params?.channel) && client.state === 'play') {
|
|
42
|
+
if (V.configPhase && state.configBrandSent) return
|
|
43
|
+
if (!V.configPhase && !state.playSettingsSent) {
|
|
44
|
+
state.heldBrand = params
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (name === 'settings') {
|
|
49
|
+
if (client.state === 'configuration' && V.configPhase && !state.configBrandSent) {
|
|
50
|
+
ctx.send('custom_payload', brandPayload(brand))
|
|
51
|
+
state.configBrandSent = true
|
|
52
|
+
} else if (client.state === 'play') {
|
|
53
|
+
if (state.suppressPlaySettings) {
|
|
54
|
+
state.suppressPlaySettings = false
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
report(name, params)
|
|
58
|
+
rawWrite(name, params)
|
|
59
|
+
state.playSettingsSent = true
|
|
60
|
+
if (state.heldBrand) {
|
|
61
|
+
const held = state.heldBrand
|
|
62
|
+
state.heldBrand = null
|
|
63
|
+
rawWrite('custom_payload', held)
|
|
64
|
+
}
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
report(name, params)
|
|
69
|
+
}
|
|
70
|
+
return rawWrite(name, params)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
client.on('finish_configuration', () => { state.suppressPlaySettings = true })
|
|
74
|
+
|
|
75
|
+
ctx.flushHeldBrand = () => {
|
|
76
|
+
if (!state.heldBrand) return
|
|
77
|
+
const held = state.heldBrand
|
|
78
|
+
state.heldBrand = null
|
|
79
|
+
ctx.send('custom_payload', held)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
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,125 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// What the movement engine needs to know about the player besides the world.
|
|
3
|
+
|
|
4
|
+
const { PV } = require('../engine/version')
|
|
5
|
+
const SPRINT_UUID = '662a6b8d-da3e-4c1c-8813-96ea6097278d'
|
|
6
|
+
const OPERATIONS = { add_value: 0, add_multiplied_base: 1, add_multiplied_total: 2 }
|
|
7
|
+
const PUSH_RANGE = 4
|
|
8
|
+
const PUSHABLE_OBJECTS = /boat|raft|minecart/
|
|
9
|
+
const LIVING_TYPES = new Set(['player', 'mob', 'animal', 'hostile', 'passive', 'water_creature', 'ambient'])
|
|
10
|
+
const BOOTS_SLOT = 8
|
|
11
|
+
|
|
12
|
+
const normalizeKey = (key) => String(key)
|
|
13
|
+
.replace(/^minecraft:/, '')
|
|
14
|
+
.replace(/^(generic|player)\./, '')
|
|
15
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
|
|
18
|
+
// attribute registry order from 1.21 on; the protocol tables in minecraft-data stop at the 1.20.5 list
|
|
19
|
+
function attributeRegistry (pv) {
|
|
20
|
+
if (pv < PV['1.21']) return null
|
|
21
|
+
return [
|
|
22
|
+
'armor', 'armor_toughness', 'attack_damage', 'attack_knockback', 'attack_speed', 'block_break_speed',
|
|
23
|
+
'block_interaction_range', 'burning_time', ...(pv >= PV['1.21.6'] ? ['camera_distance'] : []),
|
|
24
|
+
'explosion_knockback_resistance', 'entity_interaction_range', 'fall_damage_multiplier', 'flying_speed',
|
|
25
|
+
'follow_range', 'gravity', 'jump_strength', 'knockback_resistance', 'luck', 'max_absorption', 'max_health',
|
|
26
|
+
'mining_efficiency', 'movement_efficiency', 'movement_speed', 'oxygen_bonus', 'safe_fall_distance', 'scale',
|
|
27
|
+
'sneaking_speed', 'spawn_reinforcements', 'step_height', 'submerged_mining_speed', 'sweeping_damage_ratio',
|
|
28
|
+
...(pv >= PV['1.21.2'] ? ['tempt_range'] : []), 'water_movement_efficiency',
|
|
29
|
+
...(pv >= PV['1.21.6'] ? ['waypoint_receive_range', 'waypoint_transmit_range'] : [])
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// mineflayer names attributes through the outdated table; map those names back to ids and then to the real names
|
|
34
|
+
function attributeKeyMapper (registry) {
|
|
35
|
+
const names = attributeRegistry(registry.version.version)
|
|
36
|
+
const type = registry.protocol?.play?.toClient?.types?.packet_entity_update_attributes
|
|
37
|
+
const table = type && JSON.stringify(type).match(/"mappings":(\{[^}]*\})/)
|
|
38
|
+
if (!names || !table) return normalizeKey
|
|
39
|
+
const ids = {}
|
|
40
|
+
for (const [id, name] of Object.entries(JSON.parse(table[1]))) ids[name] = Number(id)
|
|
41
|
+
return (key) => ids[key] != null && names[ids[key]] ? names[ids[key]] : normalizeKey(key)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const SPRINT_IDS = new Set([SPRINT_UUID, 'minecraft:sprinting', 'sprinting'])
|
|
45
|
+
const isSprintModifier = (m) => SPRINT_IDS.has(m.uuid) || SPRINT_IDS.has(m.id)
|
|
46
|
+
const operationOf = (m) => typeof m.operation === 'number' ? m.operation : (OPERATIONS[m.operation] ?? 0)
|
|
47
|
+
|
|
48
|
+
// AttributeInstance#calculateValue, with the sprint modifier owned by the client
|
|
49
|
+
function attributeValue (property, extra = []) {
|
|
50
|
+
const modifiers = property.modifiers.filter(m => !isSprintModifier(m)).concat(extra)
|
|
51
|
+
let base = property.value
|
|
52
|
+
for (const m of modifiers) if (operationOf(m) === 0) base += m.amount
|
|
53
|
+
let value = base
|
|
54
|
+
for (const m of modifiers) if (operationOf(m) === 1) value += base * m.amount
|
|
55
|
+
for (const m of modifiers) if (operationOf(m) === 2) value *= 1 + m.amount
|
|
56
|
+
return value
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createEnvironment (bot) {
|
|
60
|
+
const effectIds = {}
|
|
61
|
+
for (const [name, effect] of Object.entries(bot.registry.effectsByName ?? {})) effectIds[name.toLowerCase()] = effect.id
|
|
62
|
+
const attributeName = attributeKeyMapper(bot.registry)
|
|
63
|
+
|
|
64
|
+
// kept from the packets directly: mineflayer files 1.16 - 1.20.4 attributes under "undefined"
|
|
65
|
+
let attributes = {}
|
|
66
|
+
const onAttributes = (packet) => {
|
|
67
|
+
if (packet.entityId !== bot.entity?.id) return
|
|
68
|
+
for (const p of packet.properties) attributes[attributeName(p.key ?? p.name)] = { value: p.value, modifiers: p.modifiers ?? [] }
|
|
69
|
+
}
|
|
70
|
+
bot._client.on('entity_update_attributes', onAttributes)
|
|
71
|
+
bot._client.on('update_attributes', onAttributes)
|
|
72
|
+
bot._client.on('login', () => { attributes = {} })
|
|
73
|
+
|
|
74
|
+
const findAttribute = (name) => attributes[name] ?? null
|
|
75
|
+
|
|
76
|
+
function boots () {
|
|
77
|
+
return bot.inventory?.slots?.[BOOTS_SLOT] ?? null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isPushable (e) {
|
|
81
|
+
if (!e || e === bot.entity || !e.position || e.isValid === false) return false
|
|
82
|
+
if (e === bot.vehicle || e.vehicle === bot.entity) return false
|
|
83
|
+
return LIVING_TYPES.has(e.type) || PUSHABLE_OBJECTS.test(e.name ?? '')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
attribute (name, fallback) {
|
|
88
|
+
const property = findAttribute(name)
|
|
89
|
+
return property ? attributeValue(property) : fallback
|
|
90
|
+
},
|
|
91
|
+
movementSpeed (sprinting) {
|
|
92
|
+
const property = findAttribute('movement_speed') ?? { value: 0.1, modifiers: [] }
|
|
93
|
+
return attributeValue(property, sprinting ? [{ amount: 0.3, operation: 2 }] : [])
|
|
94
|
+
},
|
|
95
|
+
effect (name) {
|
|
96
|
+
const id = effectIds[name.replace(/_/g, '')]
|
|
97
|
+
if (id == null) return null
|
|
98
|
+
const effect = bot.entity?.effects?.[id]
|
|
99
|
+
return effect ? effect.amplifier : null
|
|
100
|
+
},
|
|
101
|
+
bootsEnchant (name) {
|
|
102
|
+
const enchants = boots()?.enchants ?? []
|
|
103
|
+
return enchants.find(e => e.name === name)?.lvl ?? 0
|
|
104
|
+
},
|
|
105
|
+
bootsItem: () => boots()?.name ?? null,
|
|
106
|
+
food: () => bot.food ?? 20,
|
|
107
|
+
usingItem: () => !!bot.usingHeldItem,
|
|
108
|
+
gameMode: () => bot.game?.gameMode,
|
|
109
|
+
ultraWarm: () => /nether/.test(String(bot.game?.dimension ?? '')),
|
|
110
|
+
nearbyEntities () {
|
|
111
|
+
const me = bot.entity?.position
|
|
112
|
+
if (!me) return []
|
|
113
|
+
const out = []
|
|
114
|
+
for (const e of Object.values(bot.entities)) {
|
|
115
|
+
if (!isPushable(e)) continue
|
|
116
|
+
const p = e.position
|
|
117
|
+
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
|
|
118
|
+
out.push({ x: p.x, y: p.y, z: p.z, width: e.width ?? 0.6, height: e.height ?? 1.8 })
|
|
119
|
+
}
|
|
120
|
+
return out
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { createEnvironment }
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
|
|
16
|
+
function debugLogger (option) {
|
|
17
|
+
if (typeof option === 'function') return option
|
|
18
|
+
return option ? (...args) => console.log('[spectra]', ...args) : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function spectraPhysics (options = {}) {
|
|
22
|
+
return function inject (bot) {
|
|
23
|
+
const registry = bot.registry
|
|
24
|
+
const V = versionFlags(registry)
|
|
25
|
+
const world = new WorldView(bot)
|
|
26
|
+
const player = new PlayerPhysics(world, V, createEnvironment(bot))
|
|
27
|
+
|
|
28
|
+
const ctx = {
|
|
29
|
+
bot,
|
|
30
|
+
client: bot._client,
|
|
31
|
+
registry,
|
|
32
|
+
V,
|
|
33
|
+
world,
|
|
34
|
+
player,
|
|
35
|
+
options,
|
|
36
|
+
debug: debugLogger(options.debug),
|
|
37
|
+
onSend: options.onSend ?? null,
|
|
38
|
+
state: {
|
|
39
|
+
inLevel: false,
|
|
40
|
+
configBrandSent: false,
|
|
41
|
+
suppressPlaySettings: false,
|
|
42
|
+
playSettingsSent: false,
|
|
43
|
+
heldBrand: null,
|
|
44
|
+
selectedSlot: 0,
|
|
45
|
+
carriedSlot: 0,
|
|
46
|
+
lastRotationAt: 0
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
bot.physicsEnabled = true
|
|
51
|
+
bot.physics = { gravity: 0.08, yawSpeed: 3, pitchSpeed: 3, stepHeight: 0.6, engine: 'spectra' }
|
|
52
|
+
bot.spectra = {
|
|
53
|
+
player,
|
|
54
|
+
flags: V,
|
|
55
|
+
world,
|
|
56
|
+
state: ctx.state,
|
|
57
|
+
get lastRotationAt () { return ctx.state.lastRotationAt },
|
|
58
|
+
get onSend () { return ctx.onSend },
|
|
59
|
+
set onSend (fn) { ctx.onSend = typeof fn === 'function' ? fn : null }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
installConnection(ctx)
|
|
63
|
+
installControls(ctx)
|
|
64
|
+
installReporter(ctx)
|
|
65
|
+
installTicker(ctx)
|
|
66
|
+
installTeleport(ctx)
|
|
67
|
+
installServerHandlers(ctx)
|
|
68
|
+
installRiding(ctx)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = spectraPhysics
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// LocalPlayer#sendPosition and the state packets around it.
|
|
3
|
+
|
|
4
|
+
const { Vec3 } = require('vec3')
|
|
5
|
+
const { PV } = require('../engine/version')
|
|
6
|
+
|
|
7
|
+
const POINT_THREE_SQ = 9.0e-4 // 0.03, before 1.18.2
|
|
8
|
+
const MODERN_SQ = 4.0000000000000003e-8 // 2.0E-4 squared
|
|
9
|
+
const REMINDER_TICKS = 20
|
|
10
|
+
|
|
11
|
+
function installReporter (ctx) {
|
|
12
|
+
const { bot, V, player, state } = ctx
|
|
13
|
+
const moveThresholdSq = V.pointThree ? POINT_THREE_SQ : MODERN_SQ
|
|
14
|
+
const groundFlags = () => ({ onGround: player.onGround, hasHorizontalCollision: player.horizontalCollision })
|
|
15
|
+
|
|
16
|
+
ctx.sendSprinting = () => {
|
|
17
|
+
if (player.sprinting === state.wasSprinting) return
|
|
18
|
+
ctx.send('entity_action', { entityId: bot.entity.id, actionId: player.sprinting ? 'start_sprinting' : 'stop_sprinting', jumpBoost: 0 })
|
|
19
|
+
state.wasSprinting = player.sprinting
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
ctx.sendShift = () => {
|
|
23
|
+
if (V.shiftInInput) return
|
|
24
|
+
const shift = player.input.shift
|
|
25
|
+
if (shift === state.wasShift) return
|
|
26
|
+
ctx.send('entity_action', { entityId: bot.entity.id, actionId: shift ? 'start_sneaking' : 'stop_sneaking', jumpBoost: 0 })
|
|
27
|
+
state.wasShift = shift
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ctx.sendInput = () => {
|
|
31
|
+
const k = ctx.keys()
|
|
32
|
+
const current = { forward: k.forward, backward: k.back, left: k.left, right: k.right, jump: k.jump, shift: k.sneak, sprint: k.sprint }
|
|
33
|
+
const last = state.lastInput
|
|
34
|
+
if (!Object.keys(current).some(key => current[key] !== last[key])) return
|
|
35
|
+
ctx.send('player_input', { inputs: current })
|
|
36
|
+
state.lastInput = current
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
ctx.sendLook = () => {
|
|
40
|
+
ctx.send('look', { yaw: player.yRot, pitch: player.xRot, onGround: player.onGround, flags: groundFlags() })
|
|
41
|
+
state.lastRotationAt = Date.now()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ctx.sendVehicleMove = () => {
|
|
45
|
+
const boat = state.boat
|
|
46
|
+
ctx.send('vehicle_move', { x: boat.pos.x, y: boat.pos.y, z: boat.pos.z, yaw: boat.yRot, pitch: boat.xRot, onGround: boat.onGround })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ctx.sendPosition = () => {
|
|
50
|
+
ctx.sendSprinting()
|
|
51
|
+
if (!V.playerInputPacket) ctx.sendShift()
|
|
52
|
+
const last = state.last
|
|
53
|
+
const { pos } = player
|
|
54
|
+
const dx = pos.x - last.x
|
|
55
|
+
const dy = pos.y - last.y
|
|
56
|
+
const dz = pos.z - last.z
|
|
57
|
+
state.positionReminder++
|
|
58
|
+
const rotated = player.yRot !== last.yRot || player.xRot !== last.xRot
|
|
59
|
+
// Grim drops the vertical 0.03 slack for skipped airborne and landing ticks, so those are always reported;
|
|
60
|
+
// 1.17+ position_look packets under 0.03 would be taken for the item-use duplicate and ignored
|
|
61
|
+
const airborne = !player.onGround || !last.onGround
|
|
62
|
+
const reportSmall = V.pointThree && airborne && dy !== 0 && !(rotated && V.protocol >= PV['1.17'])
|
|
63
|
+
const distanceSq = dx * dx + dy * dy + dz * dz
|
|
64
|
+
const moved = distanceSq > (reportSmall ? MODERN_SQ : moveThresholdSq) || state.positionReminder >= REMINDER_TICKS
|
|
65
|
+
state.creeping = V.pointThree && V.protocol >= PV['1.17'] && airborne && dy !== 0 && distanceSq <= POINT_THREE_SQ
|
|
66
|
+
const flags = { onGround: player.onGround, flags: groundFlags() }
|
|
67
|
+
const previous = new Vec3(last.x, last.y, last.z)
|
|
68
|
+
|
|
69
|
+
if (moved && rotated) ctx.send('position_look', { x: pos.x, y: pos.y, z: pos.z, yaw: player.yRot, pitch: player.xRot, ...flags })
|
|
70
|
+
else if (moved) ctx.send('position', { x: pos.x, y: pos.y, z: pos.z, ...flags })
|
|
71
|
+
else if (rotated) ctx.send('look', { yaw: player.yRot, pitch: player.xRot, ...flags })
|
|
72
|
+
else if (last.onGround !== player.onGround || (V.movementFlags && last.horizontalCollision !== player.horizontalCollision)) ctx.send('flying', flags)
|
|
73
|
+
|
|
74
|
+
if (moved) {
|
|
75
|
+
last.x = pos.x
|
|
76
|
+
last.y = pos.y
|
|
77
|
+
last.z = pos.z
|
|
78
|
+
state.positionReminder = 0
|
|
79
|
+
}
|
|
80
|
+
if (rotated) {
|
|
81
|
+
last.yRot = player.yRot
|
|
82
|
+
last.xRot = player.xRot
|
|
83
|
+
state.lastRotationAt = Date.now()
|
|
84
|
+
}
|
|
85
|
+
last.onGround = player.onGround
|
|
86
|
+
last.horizontalCollision = player.horizontalCollision
|
|
87
|
+
if (moved || rotated) bot.emit('move', previous)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { installReporter }
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// Passenger tick: the boat moves first (client controlled), then the rider sends rotation, input and vehicle move.
|
|
3
|
+
|
|
4
|
+
const conv = require('mineflayer/lib/conversions')
|
|
5
|
+
const BoatPhysics = require('../engine/boat')
|
|
6
|
+
const { f32, clamp, wrapDegrees } = require('../engine/mth')
|
|
7
|
+
const { readKeys } = require('../engine/player/input')
|
|
8
|
+
|
|
9
|
+
const isBoat = (entity) => /boat|raft/.test(entity?.name ?? '')
|
|
10
|
+
|
|
11
|
+
function riderOffset (vehicle) {
|
|
12
|
+
if (isBoat(vehicle)) return f32(-0.1 + -0.35)
|
|
13
|
+
if (/minecart/.test(vehicle?.name ?? '')) return f32(-0.35)
|
|
14
|
+
return (vehicle?.height ?? 1) * 0.75 - 0.35
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function installRiding (ctx) {
|
|
18
|
+
const { bot, V, player, state, world } = ctx
|
|
19
|
+
|
|
20
|
+
function tickBoat (vehicle, keys) {
|
|
21
|
+
if (!state.boat || state.boat.entityId !== vehicle.id) {
|
|
22
|
+
state.boat = new BoatPhysics(world, V, { position: vehicle.position, yawDegrees: f32(conv.toNotchianYaw(vehicle.yaw)) })
|
|
23
|
+
state.boat.entityId = vehicle.id
|
|
24
|
+
}
|
|
25
|
+
state.boat.tick({ forward: keys.forward, back: keys.back, left: keys.left, right: keys.right })
|
|
26
|
+
ctx.send('steer_boat', { leftPaddle: state.boat.paddle[0], rightPaddle: state.boat.paddle[1] })
|
|
27
|
+
vehicle.position.set(state.boat.pos.x, state.boat.pos.y, state.boat.pos.z)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ctx.ridingTick = (vehicle) => {
|
|
31
|
+
const keys = ctx.keys()
|
|
32
|
+
const controlling = vehicle.passengers?.[0] === bot.entity
|
|
33
|
+
if (isBoat(vehicle) && controlling) tickBoat(vehicle, keys)
|
|
34
|
+
else state.boat = null
|
|
35
|
+
|
|
36
|
+
player.riding = true
|
|
37
|
+
player.vel = { x: 0, y: 0, z: 0 }
|
|
38
|
+
readKeys(player, keys, false)
|
|
39
|
+
player.pos = { x: vehicle.position.x, y: vehicle.position.y + riderOffset(vehicle), z: vehicle.position.z }
|
|
40
|
+
ctx.turn()
|
|
41
|
+
if (isBoat(vehicle)) {
|
|
42
|
+
const boatYaw = state.boat ? state.boat.yRot : f32(conv.toNotchianYaw(vehicle.yaw))
|
|
43
|
+
const offset = wrapDegrees(player.yRot - boatYaw)
|
|
44
|
+
player.yRot = f32(player.yRot + clamp(offset, -105, 105) - offset)
|
|
45
|
+
}
|
|
46
|
+
ctx.syncEntity()
|
|
47
|
+
|
|
48
|
+
if (!V.playerInputPacket) {
|
|
49
|
+
ctx.sendLook()
|
|
50
|
+
const flags = (player.input.jumping ? 1 : 0) | (player.input.shift ? 2 : 0)
|
|
51
|
+
ctx.send('steer_vehicle', { sideways: player.input.left, forward: player.input.forward, jump: flags })
|
|
52
|
+
if (state.boat) ctx.sendVehicleMove()
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
ctx.sendShift()
|
|
56
|
+
ctx.sendInput()
|
|
57
|
+
ctx.sendLook()
|
|
58
|
+
if (state.boat) {
|
|
59
|
+
ctx.sendVehicleMove()
|
|
60
|
+
ctx.sendSprinting()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { installRiding }
|