spectra-physics 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -23
- package/index.js +2 -2
- package/lib/client/clock.js +38 -0
- package/lib/client/connection.js +103 -0
- package/lib/client/controls.js +78 -0
- package/lib/client/environment.js +94 -0
- package/lib/client/index.js +81 -0
- package/lib/client/interact.js +79 -0
- package/lib/client/items.js +85 -0
- package/lib/client/protocol.js +42 -0
- package/lib/client/registries.js +86 -0
- package/lib/client/reporter.js +91 -0
- package/lib/client/riding.js +69 -0
- package/lib/client/server.js +103 -0
- package/lib/client/teleport.js +108 -0
- package/lib/client/ticker.js +129 -0
- package/lib/engine/aabb.js +5 -3
- package/lib/engine/boat.js +57 -7
- package/lib/engine/player/blocks.js +134 -0
- package/lib/engine/player/effects.js +115 -0
- package/lib/engine/player/fluids.js +150 -0
- package/lib/engine/player/index.js +220 -0
- package/lib/engine/player/input.js +89 -0
- package/lib/engine/player/move.js +141 -0
- package/lib/engine/player/pose.js +95 -0
- package/lib/engine/player/push.js +27 -0
- package/lib/engine/player/travel.js +203 -0
- package/lib/engine/player/traversal.js +290 -0
- package/lib/engine/version.js +52 -27
- package/lib/engine/world.js +85 -20
- package/package.json +2 -1
- package/lib/engine/physics.js +0 -780
- package/lib/plugin.js +0 -574
|
@@ -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 }
|