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 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⁻⁴ threshold, 20-tick reminder, last-sent values kept across teleports |
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 attribute updated at the end of the tick, as in `Player#aiStep` |
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 fixes three mineflayer issues that leave a bot stuck:
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 (both the old and the 1.21.2+ rules)
135
- - walking, sprinting (double-tap and key), sprint-jumping, crouch pose and its bounding box
136
- - water and lava with flow pushing, climbing (ladders, vines, scaffolding, trapdoors over ladders)
137
- - cobweb, sweet berry bush, powder snow, honey (sliding), soul sand, slime and bed bouncing
138
- - levitation, slow falling, jump boost, speed / slowness via attributes, dolphin's grace
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, swimming pose, bubble columns, entity collisions, world border.
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, 1.18.2, 1.20.4, 1.21.4 and 26.1 cover free fall, landing on full blocks and slabs,
152
- walking and sprinting speed, jump height, step-up, wall collision, sneaking at an edge, ice and cobwebs.
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
- 1. Bump `version` in `package.json` and push to `main`.
159
- 2. Create a GitHub release with the tag `v<version>` (for example `v1.0.1`).
167
+ ## Layout
160
168
 
161
- The *Publish to npm* workflow then runs the tests, checks that the tag matches `package.json`, and runs
162
- `npm publish --provenance --access public`. Publishing needs the repository secret `NPM_TOKEN`
163
- (an npm granular access token with read and write access to packages and two-factor bypass enabled;
164
- set it with `gh secret set NPM_TOKEN`). npm limits the lifetime of such tokens, so rotate it before it expires.
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/plugin')
4
- const { PlayerPhysics } = require('./lib/engine/physics')
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 }