spectra-physics 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 }
@@ -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,69 @@
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
+ // the boat ticks before its rider, so it steers with the keys the rider set on the previous tick (Boat#setInput)
21
+ function tickBoat (vehicle) {
22
+ if (!state.boat || state.boat.entityId !== vehicle.id) {
23
+ state.boat = new BoatPhysics(world, V, { position: vehicle.position, yawDegrees: f32(conv.toNotchianYaw(vehicle.yaw)) })
24
+ state.boat.entityId = vehicle.id
25
+ state.boat.input = { forward: false, back: false, left: false, right: false }
26
+ }
27
+ state.boat.tick(state.boat.input)
28
+ ctx.send('steer_boat', { leftPaddle: state.boat.paddle[0], rightPaddle: state.boat.paddle[1] })
29
+ vehicle.position.set(state.boat.pos.x, state.boat.pos.y, state.boat.pos.z)
30
+ }
31
+
32
+ ctx.ridingTick = (vehicle) => {
33
+ ctx.flushActions()
34
+ const keys = ctx.keys()
35
+ const controlling = vehicle.passengers?.[0] === bot.entity
36
+ if (isBoat(vehicle) && controlling) tickBoat(vehicle)
37
+ else state.boat = null
38
+
39
+ player.riding = true
40
+ player.vel = { x: 0, y: 0, z: 0 }
41
+ readKeys(player, keys, false)
42
+ player.pos = { x: vehicle.position.x, y: vehicle.position.y + riderOffset(vehicle), z: vehicle.position.z }
43
+ ctx.turn()
44
+ if (isBoat(vehicle)) {
45
+ const boatYaw = state.boat ? state.boat.yRot : f32(conv.toNotchianYaw(vehicle.yaw))
46
+ const offset = wrapDegrees(player.yRot - boatYaw)
47
+ player.yRot = f32(player.yRot + clamp(offset, -105, 105) - offset)
48
+ }
49
+ ctx.syncEntity()
50
+ if (state.boat) state.boat.input = { forward: keys.forward, back: keys.back, left: keys.left, right: keys.right }
51
+
52
+ if (!V.playerInputPacket) {
53
+ ctx.sendLook()
54
+ const flags = (player.input.jumping ? 1 : 0) | (player.input.shift ? 2 : 0)
55
+ ctx.send('steer_vehicle', { sideways: player.input.left, forward: player.input.forward, jump: flags })
56
+ if (state.boat) ctx.sendVehicleMove()
57
+ return
58
+ }
59
+ ctx.sendShift()
60
+ ctx.sendInput()
61
+ ctx.sendLook()
62
+ if (state.boat) {
63
+ ctx.sendVehicleMove()
64
+ ctx.sendSprinting()
65
+ }
66
+ }
67
+ }
68
+
69
+ module.exports = { installRiding }
@@ -0,0 +1,103 @@
1
+ 'use strict'
2
+ // Clientbound packets that change the local player, answered the way the game client does.
3
+
4
+ const { f32 } = require('../engine/mth')
5
+
6
+ function installServerHandlers (ctx) {
7
+ const { bot, client, V, player, state, registry } = ctx
8
+
9
+ client.on('abilities', (packet) => {
10
+ const flags = packet.flags
11
+ player.abilities.invulnerable = !!(flags & 1)
12
+ player.abilities.flying = !!(flags & 2)
13
+ player.abilities.mayfly = !!(flags & 4)
14
+ player.abilities.instabuild = !!(flags & 8)
15
+ player.abilities.flyingSpeed = f32(packet.flyingSpeed)
16
+ player.abilities.walkingSpeed = f32(packet.walkingSpeed)
17
+ })
18
+
19
+ client.on('entity_velocity', (packet) => {
20
+ if (!bot.entity || packet.entityId !== bot.entity.id) return
21
+ const scale = V.lpVelocity ? 1 : 1 / 8000
22
+ player.vel = { x: packet.velocity.x * scale, y: packet.velocity.y * scale, z: packet.velocity.z * scale }
23
+ })
24
+
25
+ client.on('explosion', (packet) => {
26
+ const push = packet.playerKnockback ??
27
+ (('playerMotionX' in packet) ? { x: packet.playerMotionX, y: packet.playerMotionY, z: packet.playerMotionZ } : null)
28
+ if (!push) return
29
+ player.vel.x += push.x
30
+ player.vel.y += push.y
31
+ player.vel.z += push.z
32
+ })
33
+
34
+ // an invalid slot is ignored, a valid one is confirmed on the next tick by MultiPlayerGameMode#tick
35
+ client.removeAllListeners('held_item_slot')
36
+ const select = (slot) => {
37
+ state.selectedSlot = slot
38
+ bot.quickBarSlot = slot
39
+ try { bot.updateHeldItem?.() } catch (e) {}
40
+ }
41
+ client.on('held_item_slot', (packet) => {
42
+ const slot = packet.slot ?? packet.slotId
43
+ if (slot >= 0 && slot <= 8) select(slot)
44
+ })
45
+ bot.setQuickBarSlot = (slot) => {
46
+ if (!(slot >= 0 && slot < 9)) throw new Error('invalid hotbar slot ' + slot)
47
+ select(slot)
48
+ }
49
+
50
+ // LocalPlayer#swing is triggered by the animate packet for our own entity and sends a swing back
51
+ client.on('animation', (packet) => {
52
+ if (!bot.entity || packet.entityId !== bot.entity.id) return
53
+ if (packet.animation === 0) ctx.send('arm_animation', { hand: 0 })
54
+ else if (packet.animation === 3) ctx.send('arm_animation', { hand: 1 })
55
+ })
56
+
57
+ client.on('vehicle_move', (packet) => {
58
+ const boat = state.boat
59
+ if (!boat) return
60
+ boat.pos = { x: packet.x, y: packet.y, z: packet.z }
61
+ boat.yRot = f32(packet.yaw)
62
+ boat.xRot = f32(packet.pitch)
63
+ ctx.sendVehicleMove()
64
+ })
65
+
66
+ // mineflayer treats a full chunk without sections as an unload before 1.17; the game client keeps it loaded
67
+ const Chunk = require('prismarine-chunk')(registry)
68
+ client.on('map_chunk', (packet) => {
69
+ const empty = packet.bitMap === 0 ||
70
+ (Array.isArray(packet.bitMap) && packet.bitMap.every(n => !n || (Array.isArray(n) && n.every(m => !m))))
71
+ if (!empty || !packet.groundUp || bot.world.getColumn(packet.x, packet.z)) return
72
+ const column = new Chunk({ minY: bot.game.minY, worldHeight: bot.game.height })
73
+ try {
74
+ if (packet.biomes !== undefined) column.loadBiomes(packet.biomes)
75
+ } catch (e) {}
76
+ bot.world.setColumn(packet.x, packet.z, column)
77
+ })
78
+
79
+ // mineflayer listens for 'entityGone' on the client instead of the bot, so a removed vehicle is never left
80
+ client.on('entity_destroy', (packet) => {
81
+ const vehicle = bot.vehicle
82
+ if (!vehicle || !packet.entityIds.includes(vehicle.id)) return
83
+ bot.vehicle = null
84
+ if (bot.entity) bot.entity.vehicle = null
85
+ state.boat = null
86
+ bot.emit('dismount', vehicle)
87
+ })
88
+
89
+ // the passenger list replaces the old one; mineflayer only appends and only leaves on vehicle id -1,
90
+ // which the server never sends, so a bot stays "mounted" after every dismount
91
+ client.on('set_passengers', ({ entityId, passengers }) => {
92
+ const vehicle = bot.entities[entityId]
93
+ if (vehicle) vehicle.passengers = passengers.map(id => bot.entities[id]).filter(Boolean)
94
+ const current = bot.vehicle
95
+ if (!current || current.id !== entityId || passengers.includes(bot.entity?.id)) return
96
+ bot.vehicle = null
97
+ if (bot.entity) bot.entity.vehicle = null
98
+ state.boat = null
99
+ bot.emit('dismount', current)
100
+ })
101
+ }
102
+
103
+ module.exports = { installServerHandlers }
@@ -0,0 +1,108 @@
1
+ 'use strict'
2
+ // ClientboundPlayerPositionPacket handling and the answer each client version sends back.
3
+
4
+ const conv = require('mineflayer/lib/conversions')
5
+ const { f32, clamp } = require('../engine/mth')
6
+
7
+ function relativeFlags (flags) {
8
+ if (flags && typeof flags === 'object') {
9
+ return {
10
+ x: !!flags.x, y: !!flags.y, z: !!flags.z, yaw: !!flags.yaw, pitch: !!flags.pitch,
11
+ dx: !!flags.dx, dy: !!flags.dy, dz: !!flags.dz, rotateDelta: !!flags.yawDelta
12
+ }
13
+ }
14
+ const n = flags | 0
15
+ return { x: !!(n & 1), y: !!(n & 2), z: !!(n & 4), yaw: !!(n & 8), pitch: !!(n & 16) }
16
+ }
17
+
18
+ // 1.16 - 1.21.1: relative axes keep their velocity, absolute ones reset it
19
+ function legacyTarget (player, packet, r) {
20
+ const v = { ...player.vel }
21
+ const pos = {
22
+ x: r.x ? player.pos.x + packet.x : packet.x,
23
+ y: r.y ? player.pos.y + packet.y : packet.y,
24
+ z: r.z ? player.pos.z + packet.z : packet.z
25
+ }
26
+ if (!r.x) v.x = 0
27
+ if (!r.y) v.y = 0
28
+ if (!r.z) v.z = 0
29
+ let yaw = f32(packet.yaw)
30
+ let pitch = f32(packet.pitch)
31
+ if (r.pitch) pitch = f32(pitch + player.xRot)
32
+ if (r.yaw) yaw = f32(yaw + player.yRot)
33
+ return { pos, vel: v, yaw: f32(yaw % 360), pitch: f32(clamp(pitch, -90, 90) % 360) }
34
+ }
35
+
36
+ // 1.21.2+: PositionMoveRotation#calculateAbsolute
37
+ function modernTarget (player, packet, r) {
38
+ const pos = {
39
+ x: (r.x ? player.pos.x : 0) + packet.x,
40
+ y: (r.y ? player.pos.y : 0) + packet.y,
41
+ z: (r.z ? player.pos.z : 0) + packet.z
42
+ }
43
+ const yaw = f32((r.yaw ? player.yRot : 0) + packet.yaw)
44
+ const pitch = f32(clamp(f32((r.pitch ? player.xRot : 0) + packet.pitch), -90, 90))
45
+ let v = { ...player.vel }
46
+ if (r.rotateDelta) {
47
+ const ax = (player.xRot - pitch) * Math.PI / 180
48
+ const ay = (player.yRot - yaw) * Math.PI / 180
49
+ const y1 = v.y * Math.cos(ax) + v.z * Math.sin(ax)
50
+ const z1 = v.z * Math.cos(ax) - v.y * Math.sin(ax)
51
+ v = { x: v.x * Math.cos(ay) + z1 * Math.sin(ay), y: y1, z: z1 * Math.cos(ay) - v.x * Math.sin(ay) }
52
+ }
53
+ const delta = (current, change, relative) => relative ? current + (change ?? 0) : (change ?? 0)
54
+ return {
55
+ pos,
56
+ vel: { x: delta(v.x, packet.dx, r.dx), y: delta(v.y, packet.dy, r.dy), z: delta(v.z, packet.dz, r.dz) },
57
+ yaw,
58
+ pitch
59
+ }
60
+ }
61
+
62
+ function installTeleport (ctx) {
63
+ const { bot, client, V, player, state } = ctx
64
+
65
+ const answerPosition = () => ctx.send('position_look', {
66
+ x: player.pos.x,
67
+ y: player.pos.y,
68
+ z: player.pos.z,
69
+ yaw: player.yRot,
70
+ pitch: player.xRot,
71
+ onGround: false,
72
+ flags: { onGround: false, hasHorizontalCollision: false }
73
+ })
74
+
75
+ client.on('position', (packet) => {
76
+ const r = relativeFlags(packet.flags)
77
+ const target = V.relativeTeleportVelocity ? modernTarget(player, packet, r) : legacyTarget(player, packet, r)
78
+ if (!bot.vehicle) {
79
+ player.pos = target.pos
80
+ player.vel = target.vel
81
+ player.onGround = false
82
+ }
83
+ player.yRot = target.yaw
84
+ player.xRot = target.pitch
85
+ bot.entity.yaw = conv.fromNotchianYaw(target.yaw)
86
+ bot.entity.pitch = conv.fromNotchianPitch(target.pitch)
87
+ ctx.syncEntity()
88
+
89
+ if (V.posRotBeforeConfirm) {
90
+ answerPosition()
91
+ ctx.send('teleport_confirm', { teleportId: packet.teleportId })
92
+ } else {
93
+ if (bot.supportFeature('teleportUsesOwnPacket')) ctx.send('teleport_confirm', { teleportId: packet.teleportId })
94
+ answerPosition()
95
+ }
96
+ state.positionReceived = true
97
+ bot.emit('forcedMove')
98
+ })
99
+
100
+ client.on('player_rotation', (packet) => {
101
+ player.yRot = f32(packet.yaw)
102
+ player.xRot = f32(clamp(packet.pitch, -90, 90))
103
+ bot.entity.yaw = conv.fromNotchianYaw(player.yRot)
104
+ bot.entity.pitch = conv.fromNotchianPitch(player.xRot)
105
+ })
106
+ }
107
+
108
+ module.exports = { installTeleport }