LookAtMySuitBot/js/node_modules/mineflayer/lib/plugins/blocks.js

573 lines
19 KiB
JavaScript

const { Vec3 } = require('vec3')
const assert = require('assert')
const Painting = require('../painting')
const { onceWithCleanup } = require('../promise_utils')
const { OctahedronIterator } = require('prismarine-world').iterators
module.exports = inject
const paintingFaceToVec = [
new Vec3(0, 0, -1),
new Vec3(-1, 0, 0),
new Vec3(0, 0, 1),
new Vec3(1, 0, 0)
]
const dimensionNames = {
'-1': 'minecraft:nether',
0: 'minecraft:overworld',
1: 'minecraft:end'
}
function inject (bot, { version, storageBuilder, hideErrors }) {
const Block = require('prismarine-block')(bot.registry)
const Chunk = require('prismarine-chunk')(bot.registry)
const World = require('prismarine-world')(bot.registry)
const paintingsByPos = {}
const paintingsById = {}
function addPainting (painting) {
paintingsById[painting.id] = painting
paintingsByPos[painting.position] = painting
}
function deletePainting (painting) {
delete paintingsById[painting.id]
delete paintingsByPos[painting.position]
}
function delColumn (chunkX, chunkZ) {
bot.world.unloadColumn(chunkX, chunkZ)
}
function addColumn (args) {
if (!args.bitMap && args.groundUp) {
// stop storing the chunk column
delColumn(args.x, args.z)
return
}
let column = bot.world.getColumn(args.x, args.z)
if (!column) {
// Allocates new chunk object while taking world's custom min/max height into account
column = new Chunk({ minY: bot.game.minY, worldHeight: bot.game.height })
}
try {
column.load(args.data, args.bitMap, args.skyLightSent, args.groundUp)
if (args.biomes !== undefined) {
column.loadBiomes(args.biomes)
}
if (args.skyLight !== undefined) {
column.loadParsedLight(args.skyLight, args.blockLight, args.skyLightMask, args.blockLightMask, args.emptySkyLightMask, args.emptyBlockLightMask)
}
bot.world.setColumn(args.x, args.z, column)
} catch (e) {
bot.emit('error', e)
}
}
async function waitForChunksToLoad () {
const dist = 2
// This makes sure that the bot's real position has been already sent
if (!bot.entity.height) await onceWithCleanup(bot, 'chunkColumnLoad')
const pos = bot.entity.position
const center = new Vec3(pos.x >> 4 << 4, 0, pos.z >> 4 << 4)
// get corner coords of 5x5 chunks around us
const chunkPosToCheck = new Set()
for (let x = -dist; x <= dist; x++) {
for (let y = -dist; y <= dist; y++) {
// ignore any chunks which are already loaded
const pos = center.plus(new Vec3(x, 0, y).scaled(16))
if (!bot.world.getColumnAt(pos)) chunkPosToCheck.add(pos.toString())
}
}
if (chunkPosToCheck.size) {
return new Promise((resolve) => {
function waitForLoadEvents (columnCorner) {
chunkPosToCheck.delete(columnCorner.toString())
if (chunkPosToCheck.size === 0) { // no chunks left to find
bot.world.off('chunkColumnLoad', waitForLoadEvents) // remove this listener instance
resolve()
}
}
// begin listening for remaining chunks to load
bot.world.on('chunkColumnLoad', waitForLoadEvents)
})
}
}
function getMatchingFunction (matching) {
if (typeof (matching) !== 'function') {
if (!Array.isArray(matching)) {
matching = [matching]
}
return isMatchingType
}
return matching
function isMatchingType (block) {
return block === null ? false : matching.indexOf(block.type) >= 0
}
}
function isBlockInSection (section, matcher) {
if (!section) return false // section is empty, skip it (yay!)
// If the chunk use a palette we can speed up the search by first
// checking the palette which usually contains less than 20 ids
// vs checking the 4096 block of the section. If we don't have a
// match in the palette, we can skip this section.
if (section.palette) {
for (const stateId of section.palette) {
if (matcher(Block.fromStateId(stateId, 0))) {
return true // the block is in the palette
}
}
return false // skip
}
return true // global palette, the block might be in there
}
function getFullMatchingFunction (matcher, useExtraInfo) {
if (typeof (useExtraInfo) === 'boolean') {
return fullSearchMatcher
}
return nonFullSearchMatcher
function nonFullSearchMatcher (point) {
const block = blockAt(point, true)
return matcher(block) && useExtraInfo(block)
}
function fullSearchMatcher (point) {
return matcher(bot.blockAt(point, useExtraInfo))
}
}
bot.findBlocks = (options) => {
const matcher = getMatchingFunction(options.matching)
const point = (options.point || bot.entity.position).floored()
const maxDistance = options.maxDistance || 16
const count = options.count || 1
const useExtraInfo = options.useExtraInfo || false
const fullMatcher = getFullMatchingFunction(matcher, useExtraInfo)
const start = new Vec3(Math.floor(point.x / 16), Math.floor(point.y / 16), Math.floor(point.z / 16))
const it = new OctahedronIterator(start, Math.ceil((maxDistance + 8) / 16))
// the octahedron iterator can sometime go through the same section again
// we use a set to keep track of visited sections
const visitedSections = new Set()
let blocks = []
let startedLayer = 0
let next = start
while (next) {
const column = bot.world.getColumn(next.x, next.z)
const sectionY = next.y + Math.abs(bot.game.minY >> 4)
const totalSections = bot.game.height >> 4
if (sectionY >= 0 && sectionY < totalSections && column && !visitedSections.has(next.toString())) {
const section = column.sections[sectionY]
if (useExtraInfo === true || isBlockInSection(section, matcher)) {
const begin = new Vec3(next.x * 16, sectionY * 16 + bot.game.minY, next.z * 16)
const cursor = begin.clone()
const end = cursor.offset(16, 16, 16)
for (cursor.x = begin.x; cursor.x < end.x; cursor.x++) {
for (cursor.y = begin.y; cursor.y < end.y; cursor.y++) {
for (cursor.z = begin.z; cursor.z < end.z; cursor.z++) {
if (fullMatcher(cursor) && cursor.distanceTo(point) <= maxDistance) blocks.push(cursor.clone())
}
}
}
}
visitedSections.add(next.toString())
}
// If we started a layer, we have to finish it otherwise we might miss closer blocks
if (startedLayer !== it.apothem && blocks.length >= count) {
break
}
startedLayer = it.apothem
next = it.next()
}
blocks.sort((a, b) => {
return a.distanceTo(point) - b.distanceTo(point)
})
// We found more blocks than needed, shorten the array to not confuse people
if (blocks.length > count) {
blocks = blocks.slice(0, count)
}
return blocks
}
function findBlock (options) {
const blocks = bot.findBlocks(options)
if (blocks.length === 0) return null
return bot.blockAt(blocks[0])
}
function blockAt (absolutePoint, extraInfos = true) {
const block = bot.world.getBlock(absolutePoint)
// null block means chunk not loaded
if (!block) return null
if (extraInfos) {
block.painting = paintingsByPos[block.position]
}
return block
}
// if passed in block is within line of sight to the bot, returns true
// also works on anything with a position value
function canSeeBlock (block) {
const headPos = bot.entity.position.offset(0, bot.entity.height, 0)
const range = headPos.distanceTo(block.position)
const dir = block.position.offset(0.5, 0.5, 0.5).minus(headPos)
const match = (inputBlock, iter) => {
const intersect = iter.intersect(inputBlock.shapes, inputBlock.position)
if (intersect) { return true }
return block.position.equals(inputBlock.position)
}
const blockAtCursor = bot.world.raycast(headPos, dir.normalize(), range, match)
return blockAtCursor && blockAtCursor.position.equals(block.position)
}
bot._client.on('unload_chunk', (packet) => {
delColumn(packet.chunkX, packet.chunkZ)
})
function updateBlockState (point, stateId) {
const oldBlock = blockAt(point)
bot.world.setBlockStateId(point, stateId)
const newBlock = blockAt(point)
// sometimes minecraft server sends us block updates before it sends
// us the column that the block is in. ignore this.
if (newBlock === null) {
return
}
if (oldBlock.type !== newBlock.type) {
const pos = point.floored()
const painting = paintingsByPos[pos]
if (painting) deletePainting(painting)
}
}
bot._client.on('update_light', (packet) => {
let column = bot.world.getColumn(packet.chunkX, packet.chunkZ)
if (!column) {
column = new Chunk({ minY: bot.game.minY, worldHeight: bot.game.height })
bot.world.setColumn(packet.chunkX, packet.chunkZ, column)
}
if (bot.supportFeature('dimensionDataIsAvailable')) {
column.loadParsedLight(packet.skyLight, packet.blockLight, packet.skyLightMask, packet.blockLightMask, packet.emptySkyLightMask, packet.emptyBlockLightMask)
} else {
column.loadLight(packet.data, packet.skyLightMask, packet.blockLightMask, packet.emptySkyLightMask, packet.emptyBlockLightMask)
}
})
bot._client.on('map_chunk', (packet) => {
addColumn({
x: packet.x,
z: packet.z,
bitMap: packet.bitMap,
heightmaps: packet.heightmaps,
biomes: packet.biomes,
skyLightSent: bot.game.dimension === 'overworld',
groundUp: packet.groundUp,
data: packet.chunkData,
trustEdges: packet.trustEdges,
skyLightMask: packet.skyLightMask,
blockLightMask: packet.blockLightMask,
emptySkyLightMask: packet.emptySkyLightMask,
emptyBlockLightMask: packet.emptyBlockLightMask,
skyLight: packet.skyLight,
blockLight: packet.blockLight
})
if (typeof packet.blockEntities !== 'undefined') {
const column = bot.world.getColumn(packet.x, packet.z)
if (!column) {
if (!hideErrors) console.warn('Ignoring block entities as chunk failed to load at', packet.x, packet.z)
return
}
for (const blockEntity of packet.blockEntities) {
if (blockEntity.x !== undefined) { // 1.17+
column.setBlockEntity(blockEntity, blockEntity.nbtData)
} else {
const pos = new Vec3(blockEntity.value.x.value & 0xf, blockEntity.value.y.value, blockEntity.value.z.value & 0xf)
column.setBlockEntity(pos, blockEntity)
}
}
}
})
bot._client.on('map_chunk_bulk', (packet) => {
let offset = 0
let meta
let i
let size
for (i = 0; i < packet.meta.length; ++i) {
meta = packet.meta[i]
size = (8192 + (packet.skyLightSent ? 2048 : 0)) *
onesInShort(meta.bitMap) + // block ids
2048 * onesInShort(meta.bitMap) + // (two bytes per block id)
256 // biomes
addColumn({
x: meta.x,
z: meta.z,
bitMap: meta.bitMap,
heightmaps: packet.heightmaps,
skyLightSent: packet.skyLightSent,
groundUp: true,
data: packet.data.slice(offset, offset + size)
})
offset += size
}
assert.strictEqual(offset, packet.data.length)
})
bot._client.on('multi_block_change', (packet) => {
// multi block change
for (let i = 0; i < packet.records.length; ++i) {
const record = packet.records[i]
let blockX, blockY, blockZ
if (bot.supportFeature('usesMultiblockSingleLong')) {
blockZ = (record >> 4) & 0x0f
blockX = (record >> 8) & 0x0f
blockY = record & 0x0f
} else {
blockZ = record.horizontalPos & 0x0f
blockX = (record.horizontalPos >> 4) & 0x0f
blockY = record.y
}
let pt
if (bot.supportFeature('usesMultiblock3DChunkCoords')) {
pt = new Vec3(packet.chunkCoordinates.x, packet.chunkCoordinates.y, packet.chunkCoordinates.z)
} else {
pt = new Vec3(packet.chunkX, 0, packet.chunkZ)
}
pt = pt.scale(16).offset(blockX, blockY, blockZ)
if (bot.supportFeature('usesMultiblockSingleLong')) {
updateBlockState(pt, record >> 12)
} else {
updateBlockState(pt, record.blockId)
}
}
})
bot._client.on('block_change', (packet) => {
const pt = new Vec3(packet.location.x, packet.location.y, packet.location.z)
updateBlockState(pt, packet.type)
})
bot._client.on('explosion', (packet) => {
// explosion
const p = new Vec3(packet.x, packet.y, packet.z)
packet.affectedBlockOffsets.forEach((offset) => {
const pt = p.offset(offset.x, offset.y, offset.z)
updateBlockState(pt, 0)
})
})
bot._client.on('spawn_entity_painting', (packet) => {
const pos = new Vec3(packet.location.x, packet.location.y, packet.location.z)
const painting = new Painting(packet.entityId,
pos, packet.title, paintingFaceToVec[packet.direction])
addPainting(painting)
})
bot._client.on('entity_destroy', (packet) => {
// destroy entity
packet.entityIds.forEach((id) => {
const painting = paintingsById[id]
if (painting) deletePainting(painting)
})
})
bot._client.on('update_sign', (packet) => {
const pos = new Vec3(packet.location.x & 0xf, packet.location.y, packet.location.z & 0xf)
// TODO: warn if out of loaded world?
const column = bot.world.getColumn(packet.location.x >> 4, packet.location.z >> 4)
if (!column) {
return
}
const blockAt = column.getBlock(pos)
blockAt.signText = [packet.text1, packet.text2, packet.text3, packet.text4].map(text => {
if (text === 'null' || text === '') return ''
return JSON.parse(text)
})
column.setBlock(pos, blockAt)
})
bot._client.on('tile_entity_data', (packet) => {
if (packet.location !== undefined) {
const column = bot.world.getColumn(packet.location.x >> 4, packet.location.z >> 4)
if (!column) return
const pos = new Vec3(packet.location.x & 0xf, packet.location.y, packet.location.z & 0xf)
column.setBlockEntity(pos, packet.nbtData)
} else {
const tag = packet.nbtData
const column = bot.world.getColumn(tag.value.x.value >> 4, tag.value.z.value >> 4)
if (!column) return
const pos = new Vec3(tag.value.x.value & 0xf, tag.value.y.value, tag.value.z.value & 0xf)
column.setBlockEntity(pos, tag)
}
})
bot.updateSign = (block, text, back = false) => {
const lines = text.split('\n')
if (lines.length > 4) {
bot.emit('error', new Error('too many lines for sign text'))
return
}
for (let i = 0; i < lines.length; ++i) {
if (lines[i].length > 15) {
bot.emit('error', new Error('signs have max line length 15'))
return
}
}
let signData
if (bot.supportFeature('sendStringifiedSignText')) {
signData = {
text1: lines[0] ? JSON.stringify(lines[0]) : '""',
text2: lines[1] ? JSON.stringify(lines[1]) : '""',
text3: lines[2] ? JSON.stringify(lines[2]) : '""',
text4: lines[3] ? JSON.stringify(lines[3]) : '""'
}
} else {
signData = {
text1: lines[0] ?? '',
text2: lines[1] ?? '',
text3: lines[2] ?? '',
text4: lines[3] ?? ''
}
}
bot._client.write('update_sign', {
location: block.position,
isFrontText: !back,
...signData
})
}
// if we get a respawn packet and the dimension is changed,
// unload all chunks from memory.
let dimension
let worldName
function dimensionToFolderName (dimension) {
if (bot.supportFeature('dimensionIsAnInt')) {
return dimensionNames[dimension]
} else if (bot.supportFeature('dimensionIsAString') || bot.supportFeature('dimensionIsAWorld')) {
return worldName
}
}
// only exposed for testing
bot._getDimensionName = () => worldName
async function switchWorld () {
if (bot.world) {
if (storageBuilder) {
await bot.world.async.waitSaving()
}
for (const [name, listener] of Object.entries(bot._events)) {
if (name.startsWith('blockUpdate:')) {
bot.emit(name, null, null)
bot.off(name, listener)
}
}
for (const [x, z] of Object.keys(bot.world.async.columns).map(key => key.split(',').map(x => parseInt(x, 10)))) {
bot.world.unloadColumn(x, z)
}
if (storageBuilder) {
bot.world.async.storageProvider = storageBuilder({ version: bot.version, worldName: dimensionToFolderName(dimension) })
}
} else {
bot.world = new World(null, storageBuilder ? storageBuilder({ version: bot.version, worldName: dimensionToFolderName(dimension) }) : null).sync
startListenerProxy()
}
}
bot._client.on('login', (packet) => {
if (bot.supportFeature('dimensionIsAnInt')) {
dimension = packet.dimension
worldName = dimensionToFolderName(dimension)
} else {
dimension = packet.dimension
worldName = /^minecraft:.+/.test(packet.worldName) ? packet.worldName : `minecraft:${packet.worldName}`
}
switchWorld()
})
bot._client.on('respawn', (packet) => {
if (bot.supportFeature('dimensionIsAnInt')) { // <=1.15.2
if (dimension === packet.dimension) return
dimension = packet.dimension
} else { // >= 1.15.2
if (dimension === packet.dimension) return
if (worldName === packet.worldName && packet.copyMetadata === true) return // don't unload chunks if in same world and metaData is true
// Metadata is true when switching dimensions however, then the world name is different
dimension = packet.dimension
worldName = packet.worldName
}
switchWorld()
})
let listener
let listenerRemove
function startListenerProxy () {
if (listener) {
// custom forwarder for custom events
bot.off('newListener', listener)
bot.off('removeListener', listenerRemove)
}
// standardized forwarding
const forwardedEvents = ['blockUpdate', 'chunkColumnLoad', 'chunkColumnUnload']
for (const event of forwardedEvents) {
bot.world.on(event, (...args) => bot.emit(event, ...args))
}
const blockUpdateRegex = /blockUpdate:\(-?\d+, -?\d+, -?\d+\)/
listener = (event, listener) => {
if (blockUpdateRegex.test(event)) {
bot.world.on(event, listener)
}
}
listenerRemove = (event, listener) => {
if (blockUpdateRegex.test(event)) {
bot.world.off(event, listener)
}
}
bot.on('newListener', listener)
bot.on('removeListener', listenerRemove)
}
bot.findBlock = findBlock
bot.canSeeBlock = canSeeBlock
bot.blockAt = blockAt
bot._updateBlockState = updateBlockState
bot.waitForChunksToLoad = waitForChunksToLoad
}
function onesInShort (n) {
n = n & 0xffff
let count = 0
for (let i = 0; i < 16; ++i) {
count = ((1 << i) & n) ? count + 1 : count
}
return count
}