Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
PrismarineJS
GitHub Repository: PrismarineJS/mineflayer
Path: blob/master/lib/plugins/blocks.js
9427 views
1
const { Vec3 } = require('vec3')
2
const assert = require('assert')
3
const Painting = require('../painting')
4
const { onceWithCleanup } = require('../promise_utils')
5
6
const { OctahedronIterator } = require('prismarine-world').iterators
7
8
module.exports = inject
9
10
const paintingFaceToVec = [
11
new Vec3(0, 0, -1),
12
new Vec3(-1, 0, 0),
13
new Vec3(0, 0, 1),
14
new Vec3(1, 0, 0)
15
]
16
17
const dimensionNames = {
18
'-1': 'minecraft:nether',
19
0: 'minecraft:overworld',
20
1: 'minecraft:end'
21
}
22
23
function inject (bot, { version, storageBuilder, hideErrors }) {
24
const Block = require('prismarine-block')(bot.registry)
25
const Chunk = require('prismarine-chunk')(bot.registry)
26
const World = require('prismarine-world')(bot.registry)
27
const paintingsByPos = {}
28
const paintingsById = {}
29
30
function addPainting (painting) {
31
paintingsById[painting.id] = painting
32
paintingsByPos[painting.position] = painting
33
}
34
35
function deletePainting (painting) {
36
delete paintingsById[painting.id]
37
delete paintingsByPos[painting.position]
38
}
39
40
function delColumn (chunkX, chunkZ) {
41
bot.world.unloadColumn(chunkX, chunkZ)
42
}
43
44
function addColumn (args) {
45
if (!args.bitMap && args.groundUp) {
46
// stop storing the chunk column
47
delColumn(args.x, args.z)
48
return
49
}
50
let column = bot.world.getColumn(args.x, args.z)
51
if (!column) {
52
// Allocates new chunk object while taking world's custom min/max height into account
53
column = new Chunk({ minY: bot.game.minY, worldHeight: bot.game.height })
54
}
55
56
try {
57
column.load(args.data, args.bitMap, args.skyLightSent, args.groundUp)
58
if (args.biomes !== undefined) {
59
column.loadBiomes(args.biomes)
60
}
61
if (args.skyLight !== undefined) {
62
column.loadParsedLight(args.skyLight, args.blockLight, args.skyLightMask, args.blockLightMask, args.emptySkyLightMask, args.emptyBlockLightMask)
63
}
64
bot.world.setColumn(args.x, args.z, column)
65
} catch (e) {
66
bot.emit('error', e)
67
}
68
}
69
70
async function waitForChunksToLoad () {
71
const dist = 2
72
// This makes sure that the bot's real position has been already sent
73
if (!bot.entity.height) await onceWithCleanup(bot, 'chunkColumnLoad', { timeout: 10000 })
74
const pos = bot.entity.position
75
const center = new Vec3(pos.x >> 4 << 4, 0, pos.z >> 4 << 4)
76
// get corner coords of 5x5 chunks around us
77
const chunkPosToCheck = new Set()
78
for (let x = -dist; x <= dist; x++) {
79
for (let y = -dist; y <= dist; y++) {
80
// ignore any chunks which are already loaded
81
const pos = center.plus(new Vec3(x, 0, y).scaled(16))
82
if (!bot.world.getColumnAt(pos)) chunkPosToCheck.add(pos.toString())
83
}
84
}
85
86
if (chunkPosToCheck.size) {
87
return new Promise((resolve, reject) => {
88
const timeout = setTimeout(() => {
89
bot.world.off('chunkColumnLoad', waitForLoadEvents)
90
reject(new Error(`Timeout waiting for ${chunkPosToCheck.size} chunks to load after 10000ms`))
91
}, 10000)
92
93
function waitForLoadEvents (columnCorner) {
94
chunkPosToCheck.delete(columnCorner.toString())
95
if (chunkPosToCheck.size === 0) { // no chunks left to find
96
clearTimeout(timeout)
97
bot.world.off('chunkColumnLoad', waitForLoadEvents) // remove this listener instance
98
resolve()
99
}
100
}
101
102
// begin listening for remaining chunks to load
103
bot.world.on('chunkColumnLoad', waitForLoadEvents)
104
})
105
}
106
}
107
108
function getMatchingFunction (matching) {
109
if (typeof (matching) !== 'function') {
110
if (!Array.isArray(matching)) {
111
matching = [matching]
112
}
113
return isMatchingType
114
}
115
return matching
116
117
function isMatchingType (block) {
118
return block === null ? false : matching.indexOf(block.type) >= 0
119
}
120
}
121
122
function isBlockInSection (section, matcher) {
123
if (!section) return false // section is empty, skip it (yay!)
124
// If the chunk use a palette we can speed up the search by first
125
// checking the palette which usually contains less than 20 ids
126
// vs checking the 4096 block of the section. If we don't have a
127
// match in the palette, we can skip this section.
128
if (section.palette) {
129
for (const stateId of section.palette) {
130
if (matcher(Block.fromStateId(stateId, 0))) {
131
return true // the block is in the palette
132
}
133
}
134
return false // skip
135
}
136
return true // global palette, the block might be in there
137
}
138
139
function getFullMatchingFunction (matcher, useExtraInfo) {
140
if (typeof (useExtraInfo) === 'boolean') {
141
return fullSearchMatcher
142
}
143
144
return nonFullSearchMatcher
145
146
function nonFullSearchMatcher (point) {
147
const block = blockAt(point, true)
148
return matcher(block) && useExtraInfo(block)
149
}
150
151
function fullSearchMatcher (point) {
152
return matcher(bot.blockAt(point, useExtraInfo))
153
}
154
}
155
156
bot.findBlocks = (options) => {
157
const matcher = getMatchingFunction(options.matching)
158
const point = (options.point || bot.entity.position).floored()
159
const maxDistance = options.maxDistance || 16
160
const count = options.count || 1
161
const useExtraInfo = options.useExtraInfo || false
162
const fullMatcher = getFullMatchingFunction(matcher, useExtraInfo)
163
const start = new Vec3(Math.floor(point.x / 16), Math.floor(point.y / 16), Math.floor(point.z / 16))
164
const it = new OctahedronIterator(start, Math.ceil((maxDistance + 8) / 16))
165
// the octahedron iterator can sometime go through the same section again
166
// we use a set to keep track of visited sections
167
const visitedSections = new Set()
168
169
let blocks = []
170
let startedLayer = 0
171
let next = start
172
while (next) {
173
const column = bot.world.getColumn(next.x, next.z)
174
const sectionY = next.y + Math.abs(bot.game.minY >> 4)
175
const totalSections = bot.game.height >> 4
176
if (sectionY >= 0 && sectionY < totalSections && column && !visitedSections.has(next.toString())) {
177
const section = column.sections[sectionY]
178
if (useExtraInfo === true || isBlockInSection(section, matcher)) {
179
const begin = new Vec3(next.x * 16, sectionY * 16 + bot.game.minY, next.z * 16)
180
const cursor = begin.clone()
181
const end = cursor.offset(16, 16, 16)
182
for (cursor.x = begin.x; cursor.x < end.x; cursor.x++) {
183
for (cursor.y = begin.y; cursor.y < end.y; cursor.y++) {
184
for (cursor.z = begin.z; cursor.z < end.z; cursor.z++) {
185
if (fullMatcher(cursor) && cursor.distanceTo(point) <= maxDistance) blocks.push(cursor.clone())
186
}
187
}
188
}
189
}
190
visitedSections.add(next.toString())
191
}
192
// If we started a layer, we have to finish it otherwise we might miss closer blocks
193
if (startedLayer !== it.apothem && blocks.length >= count) {
194
break
195
}
196
startedLayer = it.apothem
197
next = it.next()
198
}
199
blocks.sort((a, b) => {
200
return a.distanceTo(point) - b.distanceTo(point)
201
})
202
// We found more blocks than needed, shorten the array to not confuse people
203
if (blocks.length > count) {
204
blocks = blocks.slice(0, count)
205
}
206
return blocks
207
}
208
209
function findBlock (options) {
210
const blocks = bot.findBlocks(options)
211
if (blocks.length === 0) return null
212
return bot.blockAt(blocks[0])
213
}
214
215
function blockAt (absolutePoint, extraInfos = true) {
216
const block = bot.world.getBlock(absolutePoint)
217
// null block means chunk not loaded
218
if (!block) return null
219
220
if (extraInfos) {
221
block.painting = paintingsByPos[block.position]
222
}
223
224
return block
225
}
226
227
// if passed in block is within line of sight to the bot, returns true
228
// also works on anything with a position value
229
function canSeeBlock (block) {
230
const headPos = bot.entity.position.offset(0, bot.entity.eyeHeight, 0)
231
const range = headPos.distanceTo(block.position)
232
const dir = block.position.offset(0.5, 0.5, 0.5).minus(headPos)
233
const match = (inputBlock, iter) => {
234
const intersect = iter.intersect(inputBlock.shapes, inputBlock.position)
235
if (intersect) { return true }
236
return block.position.equals(inputBlock.position)
237
}
238
const blockAtCursor = bot.world.raycast(headPos, dir.normalize(), range, match)
239
return blockAtCursor && blockAtCursor.position.equals(block.position)
240
}
241
242
bot._client.on('unload_chunk', (packet) => {
243
delColumn(packet.chunkX, packet.chunkZ)
244
})
245
246
function updateBlockState (point, stateId) {
247
const oldBlock = blockAt(point)
248
bot.world.setBlockStateId(point, stateId)
249
250
const newBlock = blockAt(point)
251
// sometimes minecraft server sends us block updates before it sends
252
// us the column that the block is in. ignore this.
253
if (newBlock === null) {
254
return
255
}
256
if (oldBlock.type !== newBlock.type) {
257
const pos = point.floored()
258
const painting = paintingsByPos[pos]
259
if (painting) deletePainting(painting)
260
}
261
}
262
263
bot._client.on('update_light', (packet) => {
264
let column = bot.world.getColumn(packet.chunkX, packet.chunkZ)
265
if (!column) {
266
column = new Chunk({ minY: bot.game.minY, worldHeight: bot.game.height })
267
bot.world.setColumn(packet.chunkX, packet.chunkZ, column)
268
}
269
270
if (bot.supportFeature('newLightingDataFormat')) {
271
column.loadParsedLight(packet.skyLight, packet.blockLight, packet.skyLightMask, packet.blockLightMask, packet.emptySkyLightMask, packet.emptyBlockLightMask)
272
} else {
273
column.loadLight(packet.data, packet.skyLightMask, packet.blockLightMask, packet.emptySkyLightMask, packet.emptyBlockLightMask)
274
}
275
})
276
277
// Chunk batches are used by the server to throttle the chunks per tick for players based on their connection speed.
278
let chunkBatchStartTime = 0
279
// The Vanilla client uses nano seconds with its weighted average starting at 2000000 converted to milliseconds that is 2
280
let weightedAverage = 2
281
// This is used for keeping track of the weight of the old average when updating it.
282
let oldSampleWeight = 1
283
284
bot._client.on('chunk_batch_start', (packet) => {
285
// Get the time the chunk batch is starting.
286
chunkBatchStartTime = Date.now()
287
})
288
289
bot._client.on('chunk_batch_finished', (packet) => {
290
const milliPerChunk = (Date.now() - chunkBatchStartTime) / packet.batchSize
291
// Prevents the MilliPerChunk from being hugely different then the average, Vanilla uses 3 as a constant here.
292
const clampedMilliPerChunk = Math.min(Math.max(milliPerChunk, weightedAverage / 3.0), weightedAverage * 3.0)
293
weightedAverage = ((weightedAverage * oldSampleWeight) + clampedMilliPerChunk) / (oldSampleWeight + 1)
294
// 49 is used in Vanilla client to limit it to 50 samples
295
oldSampleWeight = Math.min(49, oldSampleWeight + 1)
296
bot._client.write('chunk_batch_received', {
297
// Vanilla uses 7000000 as a constant here, since we are using milliseconds that is now 7. Not sure why they pick this constant to convert from nano seconds per chunk to chunks per tick.
298
chunksPerTick: 7 / weightedAverage
299
})
300
})
301
bot._client.on('map_chunk', (packet) => {
302
addColumn({
303
x: packet.x,
304
z: packet.z,
305
bitMap: packet.bitMap,
306
heightmaps: packet.heightmaps,
307
biomes: packet.biomes,
308
skyLightSent: bot.game.dimension === 'overworld',
309
groundUp: packet.groundUp,
310
data: packet.chunkData,
311
trustEdges: packet.trustEdges,
312
skyLightMask: packet.skyLightMask,
313
blockLightMask: packet.blockLightMask,
314
emptySkyLightMask: packet.emptySkyLightMask,
315
emptyBlockLightMask: packet.emptyBlockLightMask,
316
skyLight: packet.skyLight,
317
blockLight: packet.blockLight
318
})
319
320
if (typeof packet.blockEntities !== 'undefined') {
321
const column = bot.world.getColumn(packet.x, packet.z)
322
if (!column) {
323
if (!hideErrors) console.warn('Ignoring block entities as chunk failed to load at', packet.x, packet.z)
324
return
325
}
326
for (const blockEntity of packet.blockEntities) {
327
if (blockEntity.x !== undefined) { // 1.17+
328
column.setBlockEntity(blockEntity, blockEntity.nbtData)
329
} else {
330
const pos = new Vec3(blockEntity.value.x.value & 0xf, blockEntity.value.y.value, blockEntity.value.z.value & 0xf)
331
column.setBlockEntity(pos, blockEntity)
332
}
333
}
334
}
335
})
336
337
bot._client.on('map_chunk_bulk', (packet) => {
338
let offset = 0
339
let meta
340
let i
341
let size
342
for (i = 0; i < packet.meta.length; ++i) {
343
meta = packet.meta[i]
344
size = (8192 + (packet.skyLightSent ? 2048 : 0)) *
345
onesInShort(meta.bitMap) + // block ids
346
2048 * onesInShort(meta.bitMap) + // (two bytes per block id)
347
256 // biomes
348
addColumn({
349
x: meta.x,
350
z: meta.z,
351
bitMap: meta.bitMap,
352
heightmaps: packet.heightmaps,
353
skyLightSent: packet.skyLightSent,
354
groundUp: true,
355
data: packet.data.slice(offset, offset + size)
356
})
357
offset += size
358
}
359
360
assert.strictEqual(offset, packet.data.length)
361
})
362
363
bot._client.on('multi_block_change', (packet) => {
364
// multi block change
365
for (let i = 0; i < packet.records.length; ++i) {
366
const record = packet.records[i]
367
368
let blockX, blockY, blockZ
369
if (bot.supportFeature('usesMultiblockSingleLong')) {
370
blockZ = (record >> 4) & 0x0f
371
blockX = (record >> 8) & 0x0f
372
blockY = record & 0x0f
373
} else {
374
blockZ = record.horizontalPos & 0x0f
375
blockX = (record.horizontalPos >> 4) & 0x0f
376
blockY = record.y
377
}
378
379
let pt
380
if (bot.supportFeature('usesMultiblock3DChunkCoords')) {
381
pt = new Vec3(packet.chunkCoordinates.x, packet.chunkCoordinates.y, packet.chunkCoordinates.z)
382
} else {
383
pt = new Vec3(packet.chunkX, 0, packet.chunkZ)
384
}
385
386
pt = pt.scale(16).offset(blockX, blockY, blockZ)
387
388
if (bot.supportFeature('usesMultiblockSingleLong')) {
389
updateBlockState(pt, record >> 12)
390
} else {
391
updateBlockState(pt, record.blockId)
392
}
393
}
394
})
395
396
bot._client.on('block_change', (packet) => {
397
const pt = new Vec3(packet.location.x, packet.location.y, packet.location.z)
398
updateBlockState(pt, packet.type)
399
})
400
401
bot._client.on('explosion', (packet) => {
402
// explosion
403
const p = new Vec3(packet.x, packet.y, packet.z)
404
if (packet.affectedBlockOffsets) {
405
// TODO: server no longer sends in 1.21.3. Is client supposed to compute this or is it sent via normal block updates?
406
packet.affectedBlockOffsets.forEach((offset) => {
407
const pt = p.offset(offset.x, offset.y, offset.z)
408
updateBlockState(pt, 0)
409
})
410
}
411
})
412
413
bot._client.on('spawn_entity_painting', (packet) => {
414
const pos = new Vec3(packet.location.x, packet.location.y, packet.location.z)
415
const painting = new Painting(packet.entityId,
416
pos, packet.title, paintingFaceToVec[packet.direction])
417
addPainting(painting)
418
})
419
420
bot._client.on('entity_destroy', (packet) => {
421
// destroy entity
422
packet.entityIds.forEach((id) => {
423
const painting = paintingsById[id]
424
if (painting) deletePainting(painting)
425
})
426
})
427
428
bot._client.on('update_sign', (packet) => {
429
const pos = new Vec3(packet.location.x & 0xf, packet.location.y, packet.location.z & 0xf)
430
431
// TODO: warn if out of loaded world?
432
const column = bot.world.getColumn(packet.location.x >> 4, packet.location.z >> 4)
433
if (!column) {
434
return
435
}
436
437
const blockAt = column.getBlock(pos)
438
439
blockAt.signText = [packet.text1, packet.text2, packet.text3, packet.text4].map(text => {
440
if (text === 'null' || text === '') return ''
441
return JSON.parse(text)
442
})
443
column.setBlock(pos, blockAt)
444
})
445
446
bot._client.on('tile_entity_data', (packet) => {
447
if (packet.location !== undefined) {
448
const column = bot.world.getColumn(packet.location.x >> 4, packet.location.z >> 4)
449
if (!column) return
450
const pos = new Vec3(packet.location.x & 0xf, packet.location.y, packet.location.z & 0xf)
451
column.setBlockEntity(pos, packet.nbtData)
452
} else {
453
const tag = packet.nbtData
454
const column = bot.world.getColumn(tag.value.x.value >> 4, tag.value.z.value >> 4)
455
if (!column) return
456
const pos = new Vec3(tag.value.x.value & 0xf, tag.value.y.value, tag.value.z.value & 0xf)
457
column.setBlockEntity(pos, tag)
458
}
459
})
460
461
bot.updateSign = (block, text, back = false) => {
462
const lines = text.split('\n')
463
if (lines.length > 4) {
464
bot.emit('error', new Error('too many lines for sign text'))
465
return
466
}
467
468
for (let i = 0; i < lines.length; ++i) {
469
if (lines[i].length > 45) {
470
bot.emit('error', new Error('Signs have a maximum of 45 characters per line'))
471
return
472
}
473
}
474
475
let signData
476
if (bot.supportFeature('sendStringifiedSignText')) {
477
signData = {
478
text1: lines[0] ? JSON.stringify(lines[0]) : '""',
479
text2: lines[1] ? JSON.stringify(lines[1]) : '""',
480
text3: lines[2] ? JSON.stringify(lines[2]) : '""',
481
text4: lines[3] ? JSON.stringify(lines[3]) : '""'
482
}
483
} else {
484
signData = {
485
text1: lines[0] ?? '',
486
text2: lines[1] ?? '',
487
text3: lines[2] ?? '',
488
text4: lines[3] ?? ''
489
}
490
}
491
492
bot._client.write('update_sign', {
493
location: block.position,
494
isFrontText: !back,
495
...signData
496
})
497
}
498
499
// if we get a respawn packet and the dimension is changed,
500
// unload all chunks from memory.
501
let dimension
502
let worldName
503
function dimensionToFolderName (dimension) {
504
if (bot.supportFeature('dimensionIsAnInt')) {
505
return dimensionNames[dimension]
506
} else if (bot.supportFeature('dimensionIsAString') || bot.supportFeature('dimensionIsAWorld')) {
507
return worldName
508
}
509
}
510
// only exposed for testing
511
bot._getDimensionName = () => worldName
512
513
async function switchWorld () {
514
if (bot.world) {
515
if (storageBuilder) {
516
await bot.world.async.waitSaving()
517
}
518
519
for (const [name, listener] of Object.entries(bot._events)) {
520
if (name.startsWith('blockUpdate:') && typeof listener === 'function') {
521
bot.emit(name, null, null)
522
bot.off(name, listener)
523
}
524
}
525
526
for (const [x, z] of Object.keys(bot.world.async.columns).map(key => key.split(',').map(x => parseInt(x, 10)))) {
527
bot.world.unloadColumn(x, z)
528
}
529
530
if (storageBuilder) {
531
bot.world.async.storageProvider = storageBuilder({ version: bot.version, worldName: dimensionToFolderName(dimension) })
532
}
533
} else {
534
bot.world = new World(null, storageBuilder ? storageBuilder({ version: bot.version, worldName: dimensionToFolderName(dimension) }) : null).sync
535
startListenerProxy()
536
}
537
}
538
539
bot._client.on('login', (packet) => {
540
if (bot.supportFeature('dimensionIsAnInt')) {
541
dimension = packet.dimension
542
worldName = dimensionToFolderName(dimension)
543
} else if (bot.supportFeature('spawnRespawnWorldDataField')) { // 1.20.5+
544
dimension = packet.worldState.dimension
545
worldName = packet.worldState.name
546
} else {
547
dimension = packet.dimension
548
worldName = /^minecraft:.+/.test(packet.worldName) ? packet.worldName : `minecraft:${packet.worldName}`
549
}
550
switchWorld()
551
})
552
553
bot._client.on('respawn', (packet) => {
554
if (bot.supportFeature('dimensionIsAnInt')) { // <=1.15.2
555
if (dimension === packet.dimension) return
556
dimension = packet.dimension
557
} else if (bot.supportFeature('spawnRespawnWorldDataField')) { // 1.20.5+
558
if (dimension === packet.worldState.dimension) return
559
if (worldName === packet.worldState.name && packet.copyMetadata === true) return // don't unload chunks if in same world and metaData is true
560
dimension = packet.worldState.dimension
561
worldName = packet.worldState.name
562
} else { // >= 1.15.2
563
if (dimension === packet.dimension) return
564
if (worldName === packet.worldName && packet.copyMetadata === true) return // don't unload chunks if in same world and metaData is true
565
// Metadata is true when switching dimensions however, then the world name is different
566
dimension = packet.dimension
567
worldName = packet.worldName
568
}
569
switchWorld()
570
})
571
572
let listener
573
let listenerRemove
574
function startListenerProxy () {
575
if (listener) {
576
// custom forwarder for custom events
577
bot.off('newListener', listener)
578
bot.off('removeListener', listenerRemove)
579
}
580
// standardized forwarding
581
const forwardedEvents = ['blockUpdate', 'chunkColumnLoad', 'chunkColumnUnload']
582
for (const event of forwardedEvents) {
583
bot.world.on(event, (...args) => bot.emit(event, ...args))
584
}
585
const blockUpdateRegex = /blockUpdate:\(-?\d+, -?\d+, -?\d+\)/
586
listener = (event, listener) => {
587
if (blockUpdateRegex.test(event)) {
588
bot.world.on(event, listener)
589
}
590
}
591
listenerRemove = (event, listener) => {
592
if (blockUpdateRegex.test(event)) {
593
bot.world.off(event, listener)
594
}
595
}
596
bot.on('newListener', listener)
597
bot.on('removeListener', listenerRemove)
598
}
599
600
bot.findBlock = findBlock
601
bot.canSeeBlock = canSeeBlock
602
bot.blockAt = blockAt
603
bot._updateBlockState = updateBlockState
604
bot.waitForChunksToLoad = waitForChunksToLoad
605
}
606
607
function onesInShort (n) {
608
n = n & 0xffff
609
let count = 0
610
for (let i = 0; i < 16; ++i) {
611
count = ((1 << i) & n) ? count + 1 : count
612
}
613
return count
614
}
615
616