Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
PrismarineJS
GitHub Repository: PrismarineJS/mineflayer
Path: blob/master/lib/plugins/digging.js
1467 views
1
const { performance } = require('perf_hooks')
2
const { createDoneTask, createTask } = require('../promise_utils')
3
const BlockFaces = require('prismarine-world').iterators.BlockFace
4
const { Vec3 } = require('vec3')
5
6
module.exports = inject
7
8
function inject (bot) {
9
let swingInterval = null
10
let waitTimeout = null
11
12
let diggingTask = createDoneTask()
13
14
bot.targetDigBlock = null
15
bot.targetDigFace = null
16
bot.lastDigTime = null
17
18
async function dig (block, forceLook, digFace) {
19
if (block === null || block === undefined) {
20
throw new Error('dig was called with an undefined or null block')
21
}
22
23
if (!digFace || typeof digFace === 'function') {
24
digFace = 'auto'
25
}
26
27
const waitTime = bot.digTime(block)
28
if (waitTime === Infinity) {
29
throw new Error(`dig time for ${block?.name ?? block} is Infinity`)
30
}
31
32
bot.targetDigFace = 1 // Default (top)
33
34
if (forceLook !== 'ignore') {
35
if (digFace?.x || digFace?.y || digFace?.z) {
36
// Determine the block face the bot should mine
37
if (digFace.x) {
38
bot.targetDigFace = digFace.x > 0 ? BlockFaces.EAST : BlockFaces.WEST
39
} else if (digFace.y) {
40
bot.targetDigFace = digFace.y > 0 ? BlockFaces.TOP : BlockFaces.BOTTOM
41
} else if (digFace.z) {
42
bot.targetDigFace = digFace.z > 0 ? BlockFaces.SOUTH : BlockFaces.NORTH
43
}
44
await bot.lookAt(
45
block.position.offset(0.5, 0.5, 0.5).offset(digFace.x * 0.5, digFace.y * 0.5, digFace.z * 0.5),
46
forceLook
47
)
48
} else if (digFace === 'raycast') {
49
// Check faces that could be seen from the current position. If the delta is smaller then 0.5 that means the
50
// bot can most likely not see the face as the block is 1 block thick
51
// this could be false for blocks that have a smaller bounding box than 1x1x1
52
const dx = bot.entity.position.x - (block.position.x + 0.5)
53
const dy = bot.entity.position.y + bot.entity.eyeHeight - (block.position.y + 0.5)
54
const dz = bot.entity.position.z - (block.position.z + 0.5)
55
// Check y first then x and z
56
const visibleFaces = {
57
y: Math.sign(Math.abs(dy) > 0.5 ? dy : 0),
58
x: Math.sign(Math.abs(dx) > 0.5 ? dx : 0),
59
z: Math.sign(Math.abs(dz) > 0.5 ? dz : 0)
60
}
61
const validFaces = []
62
const closerBlocks = []
63
for (const i in visibleFaces) {
64
if (!visibleFaces[i]) continue // skip as this face is not visible
65
// target position on the target block face. -> 0.5 + (current face) * 0.5
66
const targetPos = block.position.offset(
67
0.5 + (i === 'x' ? visibleFaces[i] * 0.5 : 0),
68
0.5 + (i === 'y' ? visibleFaces[i] * 0.5 : 0),
69
0.5 + (i === 'z' ? visibleFaces[i] * 0.5 : 0)
70
)
71
const startPos = bot.entity.position.offset(0, bot.entity.eyeHeight, 0)
72
const rayBlock = bot.world.raycast(startPos, targetPos.clone().subtract(startPos).normalize(), 5)
73
if (rayBlock) {
74
if (startPos.distanceTo(rayBlock.intersect) < startPos.distanceTo(targetPos)) {
75
// Block is closer then the raycasted block
76
closerBlocks.push(rayBlock)
77
// continue since if distance is ever less, then we did not intersect the block we wanted,
78
// meaning that the position of the intersected block is not what we want.
79
continue
80
}
81
const rayPos = rayBlock.position
82
if (
83
rayPos.x === block.position.x &&
84
rayPos.y === block.position.y &&
85
rayPos.z === block.position.z
86
) {
87
validFaces.push({
88
face: rayBlock.face,
89
targetPos: rayBlock.intersect
90
})
91
}
92
}
93
}
94
95
if (validFaces.length > 0) {
96
// Chose closest valid face
97
let closest
98
let distSqrt = 999
99
for (const i in validFaces) {
100
const tPos = validFaces[i].targetPos
101
const cDist = new Vec3(tPos.x, tPos.y, tPos.z).distanceSquared(
102
bot.entity.position.offset(0, bot.entity.eyeHeight, 0)
103
)
104
if (distSqrt > cDist) {
105
closest = validFaces[i]
106
distSqrt = cDist
107
}
108
}
109
await bot.lookAt(closest.targetPos, forceLook)
110
bot.targetDigFace = closest.face
111
} else if (closerBlocks.length === 0 && block.shapes.length === 0) {
112
// no other blocks were detected and the block has no shapes.
113
// The block in question is replaceable (like tall grass) so we can just dig it
114
// TODO: do AABB + ray intercept check to this position for digFace.
115
await bot.lookAt(block.position.offset(0.5, 0.5, 0.5), forceLook)
116
} else {
117
// Block is obstructed return error?
118
throw new Error('Block not in view')
119
}
120
} else {
121
await bot.lookAt(block.position.offset(0.5, 0.5, 0.5), forceLook)
122
}
123
}
124
125
// In vanilla the client will cancel digging the current block once the other block is at the crosshair.
126
// Todo: don't wait until lookAt is at middle of the block, but at the edge of it.
127
if (bot.targetDigBlock) bot.stopDigging()
128
129
diggingTask = createTask()
130
bot._client.write('block_dig', {
131
status: 0, // start digging
132
location: block.position,
133
face: bot.targetDigFace // default face is 1 (top)
134
})
135
waitTimeout = setTimeout(finishDigging, waitTime)
136
bot.targetDigBlock = block
137
bot.swingArm()
138
139
swingInterval = setInterval(() => {
140
bot.swingArm()
141
}, 350)
142
143
function finishDigging () {
144
clearInterval(swingInterval)
145
clearTimeout(waitTimeout)
146
swingInterval = null
147
waitTimeout = null
148
if (bot.targetDigBlock) {
149
bot._client.write('block_dig', {
150
status: 2, // finish digging
151
location: bot.targetDigBlock.position,
152
face: bot.targetDigFace // always the same as the start face
153
})
154
}
155
bot.targetDigBlock = null
156
bot.targetDigFace = null
157
bot.lastDigTime = performance.now()
158
bot._updateBlockState(block.position, 0)
159
}
160
161
const eventName = `blockUpdate:${block.position}`
162
bot.on(eventName, onBlockUpdate)
163
164
const currentBlock = block
165
bot.stopDigging = () => {
166
if (!bot.targetDigBlock) return
167
168
// Replicate the odd vanilla cancellation face value.
169
// When the cancellation is because of a new dig request on another block it's the same as the new dig start face. In all other cases it's 0.
170
const stoppedBecauseOfNewDigRequest = !currentBlock.position.equals(bot.targetDigBlock.position)
171
const cancellationDiggingFace = !stoppedBecauseOfNewDigRequest ? bot.targetDigFace : 0
172
173
bot.removeListener(eventName, onBlockUpdate)
174
clearInterval(swingInterval)
175
clearTimeout(waitTimeout)
176
swingInterval = null
177
waitTimeout = null
178
bot._client.write('block_dig', {
179
status: 1, // cancel digging
180
location: bot.targetDigBlock.position,
181
face: cancellationDiggingFace
182
})
183
const block = bot.targetDigBlock
184
bot.targetDigBlock = null
185
bot.targetDigFace = null
186
bot.lastDigTime = performance.now()
187
bot.emit('diggingAborted', block)
188
bot.stopDigging = noop
189
diggingTask.cancel(new Error('Digging aborted'))
190
}
191
192
function onBlockUpdate (oldBlock, newBlock) {
193
// vanilla server never actually interrupt digging, but some server send block update when you start digging
194
// so ignore block update if not air
195
// All block update listeners receive (null, null) when the world is unloaded. So newBlock can be null.
196
if (newBlock?.type !== 0) return
197
bot.removeListener(eventName, onBlockUpdate)
198
clearInterval(swingInterval)
199
clearTimeout(waitTimeout)
200
swingInterval = null
201
waitTimeout = null
202
bot.targetDigBlock = null
203
bot.targetDigFace = null
204
bot.lastDigTime = performance.now()
205
bot.emit('diggingCompleted', newBlock)
206
diggingTask.finish()
207
}
208
209
await diggingTask.promise
210
}
211
212
bot.on('death', () => {
213
bot.removeAllListeners('diggingAborted')
214
bot.removeAllListeners('diggingCompleted')
215
bot.stopDigging()
216
})
217
218
function canDigBlock (block) {
219
return (
220
block &&
221
block.diggable &&
222
block.position.offset(0.5, 0.5, 0.5).distanceTo(bot.entity.position.offset(0, 1.65, 0)) <= 5.1
223
)
224
}
225
226
function digTime (block) {
227
let type = null
228
let enchantments = []
229
230
// Retrieve currently held item ID and active enchantments from heldItem
231
const currentlyHeldItem = bot.heldItem
232
if (currentlyHeldItem) {
233
type = currentlyHeldItem.type
234
enchantments = currentlyHeldItem.enchants
235
}
236
237
// Append helmet enchantments (because Aqua Affinity actually affects dig speed)
238
const headEquipmentSlot = bot.getEquipmentDestSlot('head')
239
const headEquippedItem = bot.inventory.slots[headEquipmentSlot]
240
if (headEquippedItem) {
241
const helmetEnchantments = headEquippedItem.enchants
242
enchantments = enchantments.concat(helmetEnchantments)
243
}
244
245
const creative = bot.game.gameMode === 'creative'
246
return block.digTime(
247
type,
248
creative,
249
bot.entity.isInWater,
250
!bot.entity.onGround,
251
enchantments,
252
bot.entity.effects
253
)
254
}
255
256
bot.dig = dig
257
bot.stopDigging = noop
258
bot.canDigBlock = canDigBlock
259
bot.digTime = digTime
260
}
261
262
function noop (err) {
263
if (err) throw err
264
}
265
266