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