spectra-physics 1.1.0 → 1.3.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
@@ -25,14 +25,18 @@ several places that are visible to the server:
25
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
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
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 |
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 |
29
33
  | Rotation | continuous interpolation | whole mouse steps of 0.15°, accumulated in float |
30
34
  | Sprint | applied the same tick | speed follows the sprint state in the tick it is sent, as the server applies it |
31
35
  | Tick loop | fixed interval | `Minecraft` timer: catches up at most 10 ticks |
32
36
  | 1.21.2+ | no `tick_end` | `tick_end` after every tick, `player_input` on key change |
33
37
  | 1.21.4+ | `player_loaded` on first health update | sent once the level around the player is loaded |
34
38
  | Teleport answer | one order for all versions | per version (1.21.2–1.21.3 send position before the confirmation) |
35
- | 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 |
36
40
  | Brand, 1.20.2+ | sent in play state | sent in the configuration phase, once |
37
41
  | Open container | keeps moving | movement keys are ignored while a window is open, as in the game |
38
42
 
@@ -42,12 +46,23 @@ It also works around mineflayer issues that break movement:
42
46
  - a removed vehicle is never dismounted (`entityGone` is listened for on the wrong emitter);
43
47
  - an out-of-range hotbar slot from the server throws inside the packet handler;
44
48
  - 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.
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.
46
54
 
47
55
  Before 1.18.2 the client only reports moves longer than 0.03. Airborne moves shorter than that are still reported,
48
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
49
57
  cannot reconstruct the skipped vertical movement.
50
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.
65
+
51
66
  ## Installation
52
67
 
53
68
  ```bash
@@ -114,6 +129,35 @@ Added:
114
129
  - `bot.spectra.world` – block and fluid lookups used by the engine
115
130
  - event `clientLoaded` – 1.21.4+, emitted when `player_loaded` is sent
116
131
 
132
+ ### Pathfinder
133
+
134
+ A built-in pathfinder with the same API as mineflayer-pathfinder, driven by the engine itself:
135
+
136
+ ```js
137
+ const { createBot, goals } = require('spectra-physics')
138
+
139
+ const bot = createBot({ host: 'localhost', username: 'Steve', version: '1.21.4' })
140
+
141
+ bot.once('spawn', async () => {
142
+ await bot.pathfinder.goto(new goals.GoalBlock(120, 64, -40))
143
+ bot.pathfinder.setGoal(new goals.GoalFollow(bot.nearestEntity(e => e.name === 'cow'), 2), true)
144
+ })
145
+ ```
146
+
147
+ - A* with costs in ticks, so it picks the fastest route: walking, sprinting, slabs and stairs without jumping,
148
+ jumps, drops, sprint-jumps over gaps, ladders and vines, swimming, and digging through, up and down
149
+ - keys are chosen by simulating the player a few ticks ahead with this engine: when to sprint, when to jump,
150
+ which corners can be cut
151
+ - turning goes through the mouse model; blocks are broken tick by tick like the game client (face in view,
152
+ progress and swing every tick, 5 ticks between blocks), also available as `bot.spectra.dig(block)`
153
+ - the route is planned again when blocks on it change, the goal moves or the bot gets stuck
154
+ - goals: `GoalBlock`, `GoalNear`, `GoalXZ`, `GoalNearXZ`, `GoalY`, `GoalGetToBlock`, `GoalLookAtBlock`,
155
+ `GoalFollow`, `GoalCompositeAny`, `GoalCompositeAll`, `GoalInvert`
156
+ - `bot.pathfinder.settings`: `canDig`, `allowParkour`, `allowLongParkour`, `allowSprinting`, `maxDropDown`,
157
+ `thinkTimeout`, `tickTimeout`; events `path_update`, `path_reset`, `path_stop`, `goal_reached`, `goal_updated`
158
+
159
+ mineflayer-pathfinder works too: `bot.physics.simulatePlayer` runs its look-ahead on this engine.
160
+
117
161
  ### Standalone engine
118
162
 
119
163
  The engine can run without a connection, for prediction or tests:
@@ -148,6 +192,7 @@ See [`test/helpers.js`](test/helpers.js) for a complete in-memory world.
148
192
  - being pushed out of blocks and by overlapping entities
149
193
  - levitation, slow falling, jump boost, dolphin's grace, depth strider and soul speed,
150
194
  attributes: movement speed, gravity, jump strength, step height, sneaking speed, movement and water efficiency
195
+ - using items: eating, drinking, blocking with a shield, drawing a bow
151
196
  - creative flight, including toggling with a double jump and landing
152
197
  - knockback and explosion velocity, 1.21.2+ relative teleports with velocity
153
198
  - riding boats (client-controlled) and minecarts
@@ -162,14 +207,18 @@ npm test
162
207
 
163
208
  Offline tests for 1.16.5 – 26.1 cover free fall, landing on blocks and slabs, walking and sprinting speed,
164
209
  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.
210
+ powder snow, ice, soul sand, pushing, item use, and the position reporting thresholds.
166
211
 
167
212
  ## Layout
168
213
 
169
214
  ```
170
215
  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
216
+ lib/engine/player the player tick: input, fluids, blocks under the player, move, travel, block effects and their
217
+ traversal order, pose, pushing
218
+ lib/pathfinder goals, terrain, moves, A*, block breaking and the path follower
219
+ lib/client mineflayer integration: controls, teleports, server packets, position reporting, riding, item use,
220
+ entity interaction, registry tables, protocol fixes, the shared clock
221
+ bench tick cost and bit-exact output fingerprints
173
222
  ```
174
223
 
175
224
  ## License
package/index.js CHANGED
@@ -25,3 +25,4 @@ module.exports.BoatPhysics = BoatPhysics
25
25
  module.exports.WorldView = WorldView
26
26
  module.exports.AABB = AABB
27
27
  module.exports.versionFlags = versionFlags
28
+ module.exports.goals = require('./lib/pathfinder/goals')
@@ -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 }
@@ -3,6 +3,9 @@
3
3
  // brand + client information in the configuration phase (1.20.2+), settings before brand on older versions,
4
4
  // and player_loaded only when the level has actually loaded.
5
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
+
6
9
  const isBrand = (channel) => channel === 'minecraft:brand' || channel === 'MC|Brand'
7
10
 
8
11
  function brandPayload (brand) {
@@ -25,6 +28,7 @@ function installConnection (ctx) {
25
28
  let ours = false
26
29
 
27
30
  const report = (name, params) => {
31
+ for (const watch of ctx.outgoing) watch(name, params)
28
32
  if (ctx.onSend) ctx.onSend(name, params)
29
33
  if (ctx.debug) ctx.debug('C->S', name, JSON.stringify(params))
30
34
  }
@@ -35,7 +39,13 @@ function installConnection (ctx) {
35
39
  try { rawWrite(name, params) } finally { ours = false }
36
40
  }
37
41
 
42
+ const queued = []
43
+
38
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
+ }
39
49
  if (!ours) {
40
50
  if (name === 'player_loaded' && V.clientLoaded) return
41
51
  if (name === 'custom_payload' && isBrand(params?.channel) && client.state === 'play') {
@@ -72,6 +82,16 @@ function installConnection (ctx) {
72
82
 
73
83
  client.on('finish_configuration', () => { state.suppressPlaySettings = true })
74
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
+
75
95
  ctx.flushHeldBrand = () => {
76
96
  if (!state.heldBrand) return
77
97
  const held = state.heldBrand
@@ -1,46 +1,16 @@
1
1
  'use strict'
2
2
  // What the movement engine needs to know about the player besides the world.
3
3
 
4
- const { PV } = require('../engine/version')
4
+ const { attributeKeyMapper, effectIdMapper, createEnchantmentReader, entityCategory } = require('./registries')
5
+
5
6
  const SPRINT_UUID = '662a6b8d-da3e-4c1c-8813-96ea6097278d'
6
7
  const OPERATIONS = { add_value: 0, add_multiplied_base: 1, add_multiplied_total: 2 }
7
8
  const PUSH_RANGE = 4
8
9
  const PUSHABLE_OBJECTS = /boat|raft|minecart/
9
10
  const LIVING_TYPES = new Set(['player', 'mob', 'animal', 'hostile', 'passive', 'water_creature', 'ambient'])
11
+ const NEVER_PUSHES = new Set(['armor_stand', 'bat', 'parrot'])
10
12
  const BOOTS_SLOT = 8
11
13
 
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
14
  const SPRINT_IDS = new Set([SPRINT_UUID, 'minecraft:sprinting', 'sprinting'])
45
15
  const isSprintModifier = (m) => SPRINT_IDS.has(m.uuid) || SPRINT_IDS.has(m.id)
46
16
  const operationOf = (m) => typeof m.operation === 'number' ? m.operation : (OPERATIONS[m.operation] ?? 0)
@@ -56,9 +26,9 @@ function attributeValue (property, extra = []) {
56
26
  return value
57
27
  }
58
28
 
59
- function createEnvironment (bot) {
60
- const effectIds = {}
61
- for (const [name, effect] of Object.entries(bot.registry.effectsByName ?? {})) effectIds[name.toLowerCase()] = effect.id
29
+ function createEnvironment (bot, state = {}) {
30
+ const effectId = effectIdMapper(bot.registry)
31
+ const enchantmentLevel = createEnchantmentReader(bot)
62
32
  const attributeName = attributeKeyMapper(bot.registry)
63
33
 
64
34
  // kept from the packets directly: mineflayer files 1.16 - 1.20.4 attributes under "undefined"
@@ -80,7 +50,9 @@ function createEnvironment (bot) {
80
50
  function isPushable (e) {
81
51
  if (!e || e === bot.entity || !e.position || e.isValid === false) return false
82
52
  if (e === bot.vehicle || e.vehicle === bot.entity) return false
83
- return LIVING_TYPES.has(e.type) || PUSHABLE_OBJECTS.test(e.name ?? '')
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 ?? '')
84
56
  }
85
57
 
86
58
  return {
@@ -93,18 +65,15 @@ function createEnvironment (bot) {
93
65
  return attributeValue(property, sprinting ? [{ amount: 0.3, operation: 2 }] : [])
94
66
  },
95
67
  effect (name) {
96
- const id = effectIds[name.replace(/_/g, '')]
68
+ const id = effectId(name)
97
69
  if (id == null) return null
98
70
  const effect = bot.entity?.effects?.[id]
99
71
  return effect ? effect.amplifier : null
100
72
  },
101
- bootsEnchant (name) {
102
- const enchants = boots()?.enchants ?? []
103
- return enchants.find(e => e.name === name)?.lvl ?? 0
104
- },
73
+ bootsEnchant: (name) => enchantmentLevel(boots(), name),
105
74
  bootsItem: () => boots()?.name ?? null,
106
75
  food: () => bot.food ?? 20,
107
- usingItem: () => !!bot.usingHeldItem,
76
+ usingItem: () => state.using != null,
108
77
  gameMode: () => bot.game?.gameMode,
109
78
  ultraWarm: () => /nether/.test(String(bot.game?.dimension ?? '')),
110
79
  nearbyEntities () {
@@ -12,6 +12,11 @@ const { installServerHandlers } = require('./server')
12
12
  const { installReporter } = require('./reporter')
13
13
  const { installRiding } = require('./riding')
14
14
  const { installTicker } = require('./ticker')
15
+ const { installItemUse } = require('./items')
16
+ const { installInteract } = require('./interact')
17
+ const { installProtocolPatches } = require('./protocol')
18
+ const { installSimulation } = require('./simulation')
19
+ const { installPathfinder } = require('../pathfinder')
15
20
 
16
21
  function debugLogger (option) {
17
22
  if (typeof option === 'function') return option
@@ -23,7 +28,18 @@ function spectraPhysics (options = {}) {
23
28
  const registry = bot.registry
24
29
  const V = versionFlags(registry)
25
30
  const world = new WorldView(bot)
26
- const player = new PlayerPhysics(world, V, createEnvironment(bot))
31
+ const state = {
32
+ inLevel: false,
33
+ configBrandSent: false,
34
+ suppressPlaySettings: false,
35
+ playSettingsSent: false,
36
+ heldBrand: null,
37
+ selectedSlot: 0,
38
+ carriedSlot: 0,
39
+ lastRotationAt: 0,
40
+ using: null
41
+ }
42
+ const player = new PlayerPhysics(world, V, createEnvironment(bot, state))
27
43
 
28
44
  const ctx = {
29
45
  bot,
@@ -35,16 +51,8 @@ function spectraPhysics (options = {}) {
35
51
  options,
36
52
  debug: debugLogger(options.debug),
37
53
  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
- }
54
+ outgoing: [],
55
+ state
48
56
  }
49
57
 
50
58
  bot.physicsEnabled = true
@@ -59,13 +67,18 @@ function spectraPhysics (options = {}) {
59
67
  set onSend (fn) { ctx.onSend = typeof fn === 'function' ? fn : null }
60
68
  }
61
69
 
70
+ installProtocolPatches(ctx)
62
71
  installConnection(ctx)
72
+ installItemUse(ctx)
63
73
  installControls(ctx)
74
+ installInteract(ctx)
64
75
  installReporter(ctx)
65
76
  installTicker(ctx)
66
77
  installTeleport(ctx)
67
78
  installServerHandlers(ctx)
68
79
  installRiding(ctx)
80
+ installSimulation(ctx)
81
+ installPathfinder(ctx)
69
82
  }
70
83
  }
71
84
 
@@ -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 }
@@ -0,0 +1,85 @@
1
+ 'use strict'
2
+ // LocalPlayer#startedUsingItem: whether a held item is being used, which slows movement to a fifth.
3
+ // mineflayer's usingHeldItem is set for any item and cleared by any entity's status packet, so the state is
4
+ // rebuilt here from what the client sends and from the server's own view of the player.
5
+
6
+ const ALWAYS_EDIBLE = new Set(['golden_apple', 'enchanted_golden_apple', 'chorus_fruit', 'honey_bottle', 'suspicious_stew'])
7
+ const DRINKS = new Set(['potion', 'milk_bucket', 'ominous_bottle'])
8
+ const HELD_USE = new Set(['shield', 'trident', 'spyglass', 'goat_horn'])
9
+ const ARROWS = new Set(['arrow', 'spectral_arrow', 'tipped_arrow'])
10
+ const OFFHAND_SLOT = 45
11
+ const FINISHED_USING = 9
12
+
13
+ function isCharged (item) {
14
+ const projectiles = item.components?.find?.(c => c.type === 'charged_projectiles')
15
+ if (projectiles) return (projectiles.data?.projectiles ?? projectiles.data ?? []).length > 0
16
+ return item.nbt?.value?.Charged?.value === 1
17
+ }
18
+
19
+ function installItemUse (ctx) {
20
+ const { bot, client, V, state } = ctx
21
+ const flagsIndex = V.protocol >= 755 ? 8 : 7
22
+ const cooldowns = new Map()
23
+ state.using = null
24
+
25
+ const itemIn = (hand) => hand === 1 ? bot.inventory.slots[OFFHAND_SLOT] : bot.heldItem
26
+ const creative = () => bot.game?.gameMode === 'creative'
27
+ const hasAmmo = () => creative() || bot.inventory.slots.some(i => i && ARROWS.has(i.name))
28
+
29
+ // Item#use returning CONSUME / startUsingItem for the items that have a use animation
30
+ function canStartUsing (item) {
31
+ if (!item || (cooldowns.get(item.name) ?? 0) > Date.now()) return false
32
+ const name = item.name
33
+ if (DRINKS.has(name) || HELD_USE.has(name)) return true
34
+ if (name === 'bow') return hasAmmo()
35
+ if (name === 'crossbow') return !isCharged(item) && (hasAmmo() || bot.inventory.slots[OFFHAND_SLOT]?.name === 'firework_rocket')
36
+ if (bot.registry.foodsByName?.[name]) return ALWAYS_EDIBLE.has(name) || creative() || (bot.food ?? 20) < 20
37
+ return false
38
+ }
39
+
40
+ const start = (hand) => {
41
+ const item = itemIn(hand)
42
+ state.using = item ? { hand, name: item.name } : null
43
+ }
44
+ const stop = () => { state.using = null }
45
+
46
+ ctx.outgoing.push((name, params) => {
47
+ if (name === 'use_item') {
48
+ const hand = params.hand ?? 0
49
+ if (canStartUsing(itemIn(hand))) start(hand)
50
+ } else if (name === 'block_place' && params.direction === -1) {
51
+ if (canStartUsing(bot.heldItem)) start(0)
52
+ } else if (name === 'block_dig' && params.status === 5) {
53
+ stop()
54
+ }
55
+ })
56
+
57
+ client.on('entity_status', (packet) => {
58
+ if (packet.entityId === bot.entity?.id && packet.entityStatus === FINISHED_USING) stop()
59
+ })
60
+
61
+ // the server's living entity flags override the client state (LocalPlayer#onSyncedDataUpdated)
62
+ client.on('entity_metadata', (packet) => {
63
+ if (packet.entityId !== bot.entity?.id) return
64
+ const entry = packet.metadata?.find(m => m.key === flagsIndex)
65
+ if (!entry || typeof entry.value !== 'number') return
66
+ const active = (entry.value & 1) !== 0
67
+ if (active && !state.using) start((entry.value & 2) !== 0 ? 1 : 0)
68
+ else if (!active && state.using) stop()
69
+ })
70
+
71
+ client.on('set_cooldown', (packet) => {
72
+ const key = packet.cooldownGroup ?? bot.registry.items[packet.itemID]?.name
73
+ if (key) cooldowns.set(String(key).replace(/^minecraft:/, ''), Date.now() + (packet.cooldownTicks ?? 0) * 50)
74
+ })
75
+
76
+ client.on('respawn', stop)
77
+ bot.on('death', stop)
78
+
79
+ // LivingEntity#updatingUsingItem: switching or losing the item stops the use
80
+ ctx.updateItemUse = () => {
81
+ if (state.using && itemIn(state.using.hand)?.name !== state.using.name) stop()
82
+ }
83
+ }
84
+
85
+ module.exports = { installItemUse }
@@ -0,0 +1,42 @@
1
+ 'use strict'
2
+ // Fixes for packet layouts minecraft-data gets wrong. They are merged into the client's custom packets
3
+ // before the play state starts, so the parser is built with them.
4
+
5
+ const { PV } = require('../engine/version')
6
+
7
+ // recipe and advancement packets whose layout in minecraft-data does not match the game; mineflayer reads neither
8
+ function brokenPackets (pv) {
9
+ if (pv >= PV['1.20.5'] && pv < PV['1.21.2']) return ['declare_recipes']
10
+ if (pv === PV['1.21.2']) return ['declare_recipes', 'recipe_book_add', 'advancements']
11
+ return []
12
+ }
13
+
14
+ // minecraft-protocol merges custom packets field by field, so every original field has to be overridden:
15
+ // the first one takes the whole payload, the others read nothing
16
+ function rawLayout (original) {
17
+ const fields = Array.isArray(original?.[1]) ? original[1] : [{}]
18
+ return ['container', fields.map((field, i) => i === 0
19
+ ? { name: 'data', type: 'restBuffer' }
20
+ : { name: field.name ?? `unused${i}`, type: 'void' })]
21
+ }
22
+
23
+ function installProtocolPatches (ctx) {
24
+ const { client, registry } = ctx
25
+ const names = brokenPackets(registry.version.version)
26
+ if (!names.length) return
27
+ const types = registry.protocol?.play?.toClient?.types ?? {}
28
+ const patch = {}
29
+ for (const name of names) patch[`packet_${name}`] = rawLayout(types[`packet_${name}`])
30
+
31
+ const major = registry.version.majorVersion
32
+ const custom = client.customPackets ?? {}
33
+ const own = custom[major] ?? {}
34
+ const userTypes = own.play?.toClient?.types ?? {}
35
+ custom[major] = {
36
+ ...own,
37
+ play: { ...own.play, toClient: { ...own.play?.toClient, types: { ...patch, ...userTypes } } }
38
+ }
39
+ client.customPackets = custom
40
+ }
41
+
42
+ module.exports = { installProtocolPatches }
@@ -0,0 +1,86 @@
1
+ 'use strict'
2
+ // Ids the server uses for attributes, effects and enchantments. minecraft-data is outdated or wrong for
3
+ // several of them, so the tables here follow the game registries instead.
4
+
5
+ const { PV } = require('../engine/version')
6
+
7
+ const normalizeKey = (key) => String(key)
8
+ .replace(/^minecraft:/, '')
9
+ .replace(/^(generic|player|zombie)\./, '')
10
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
11
+ .toLowerCase()
12
+
13
+ // attribute registry order from 1.21 on; the protocol tables in minecraft-data stop at the 1.20.5 list
14
+ function attributeRegistry (pv) {
15
+ if (pv < PV['1.21']) return null
16
+ return [
17
+ 'armor', 'armor_toughness', 'attack_damage', 'attack_knockback', 'attack_speed', 'block_break_speed',
18
+ 'block_interaction_range', 'burning_time', ...(pv >= PV['1.21.6'] ? ['camera_distance'] : []),
19
+ 'explosion_knockback_resistance', 'entity_interaction_range', 'fall_damage_multiplier', 'flying_speed',
20
+ 'follow_range', 'gravity', 'jump_strength', 'knockback_resistance', 'luck', 'max_absorption', 'max_health',
21
+ 'mining_efficiency', 'movement_efficiency', 'movement_speed', 'oxygen_bonus', 'safe_fall_distance', 'scale',
22
+ 'sneaking_speed', 'spawn_reinforcements', 'step_height', 'submerged_mining_speed', 'sweeping_damage_ratio',
23
+ ...(pv >= PV['1.21.2'] ? ['tempt_range'] : []), 'water_movement_efficiency',
24
+ ...(pv >= PV['1.21.6'] ? ['waypoint_receive_range', 'waypoint_transmit_range'] : [])
25
+ ]
26
+ }
27
+
28
+ // mineflayer names attributes through the outdated table; map those names back to ids and then to the real names
29
+ function attributeKeyMapper (registry) {
30
+ const names = attributeRegistry(registry.version.version)
31
+ const type = registry.protocol?.play?.toClient?.types?.packet_entity_update_attributes
32
+ const table = type && JSON.stringify(type).match(/"mappings":(\{[^}]*\})/)
33
+ if (!names || !table) return normalizeKey
34
+ const ids = {}
35
+ for (const [id, name] of Object.entries(JSON.parse(table[1]))) ids[name] = Number(id)
36
+ return (key) => ids[key] != null && names[ids[key]] ? names[ids[key]] : normalizeKey(key)
37
+ }
38
+
39
+ // MobEffects registration order (stable for the effects movement cares about); 1-based before 1.20.2
40
+ const EFFECTS = [
41
+ 'speed', 'slowness', 'haste', 'mining_fatigue', 'strength', 'instant_health', 'instant_damage', 'jump_boost',
42
+ 'nausea', 'regeneration', 'resistance', 'fire_resistance', 'water_breathing', 'invisibility', 'blindness',
43
+ 'night_vision', 'hunger', 'weakness', 'poison', 'wither', 'health_boost', 'absorption', 'saturation', 'glowing',
44
+ 'levitation', 'luck', 'unluck', 'slow_falling', 'conduit_power', 'dolphins_grace', 'bad_omen',
45
+ 'hero_of_the_village', 'darkness'
46
+ ]
47
+
48
+ function effectIdMapper (registry) {
49
+ const zeroBased = registry.version.version >= PV['1.20.2']
50
+ return (name) => {
51
+ const index = EFFECTS.indexOf(name)
52
+ return index < 0 ? null : index + (zeroBased ? 0 : 1)
53
+ }
54
+ }
55
+
56
+ // enchantments: item NBT names before 1.20.5, built-in ids in 1.20.5 - 1.20.6, the server's registry from 1.21
57
+ function createEnchantmentReader (bot) {
58
+ const registry = bot.registry
59
+ let synced = null
60
+ bot._client.on('registry_data', (packet) => {
61
+ if (!/enchantment$/.test(String(packet.id ?? ''))) return
62
+ synced = (packet.entries ?? []).map(e => normalizeKey(e.key ?? e.id ?? ''))
63
+ })
64
+ const nameOf = (id) => synced?.[id] ?? registry.enchantments?.[id]?.name ?? null
65
+
66
+ return (item, name) => {
67
+ if (!item) return 0
68
+ const component = item.components?.find?.(c => c.type === 'enchantments')
69
+ if (component) {
70
+ const list = component.data?.enchantments ?? []
71
+ return list.find(e => nameOf(e.id) === name)?.level ?? 0
72
+ }
73
+ let legacy
74
+ try { legacy = item.enchants } catch { legacy = null }
75
+ return Array.isArray(legacy) ? (legacy.find(e => normalizeKey(e.name) === name)?.lvl ?? 0) : 0
76
+ }
77
+ }
78
+
79
+ // entity categories; the 1.16 tables mark every entity as a mob, so a later table is used by name
80
+ let entityTable = null
81
+ function entityCategory (entity) {
82
+ entityTable ??= require('minecraft-data')('1.20.4').entitiesByName
83
+ return entityTable[entity.name]?.type ?? entity.type
84
+ }
85
+
86
+ module.exports = { normalizeKey, attributeKeyMapper, effectIdMapper, createEnchantmentReader, entityCategory }