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 }