module.exports = loader module.exports.testedVersions = ['1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.4', '1.17.1', '1.18.1', 'bedrock_1.17.10', 'bedrock_1.18.0', '1.20'] const nbt = require('prismarine-nbt') const mcData = require('minecraft-data') const legacyPcBlocksByName = Object.entries(mcData.legacy.pc.blocks).reduce((obj, [idmeta, name]) => { const n = name.replace('minecraft:', '').split('[')[0] const s = name.split('[')[1]?.replace(']', '') ?? '' ;(obj[n] = obj[n] || {})[s] = idmeta return obj // array of { [name]: { [states string]: string(id:meta) } } }, {}) const legacyPcBlocksByIdmeta = Object.entries(mcData.legacy.pc.blocks).reduce((obj, [idmeta, name]) => { const s = name.split('[')[1]?.replace(']', '') obj[idmeta] = s ? Object.fromEntries(s.split(',').map(s => { let [k, v] = s.split('=') if (!isNaN(parseInt(v))) v = parseInt(v) return [k, v] })) : {} return obj // array of { '255:0': { mode: 'save' }, } }, {}) function loader (registryOrVersion) { const registry = typeof registryOrVersion === 'string' ? require('prismarine-registry')(registryOrVersion) : registryOrVersion const version = registry.version return provider(registry, { Biome: require('prismarine-biome')(version), version }) } function provider (registry, { Biome, version }) { const blockMethods = require('./blockEntity')(registry) const usesBlockStates = (version.type === 'pc' && registry.supportFeature('blockStateId')) || (version.type === 'bedrock') const shapes = registry.blockCollisionShapes if (shapes) { // Prepare block shapes for (const id in registry.blocks) { const block = registry.blocks[id] const shapesId = shapes.blocks[block.name] block.shapes = (shapesId instanceof Array) ? shapes.shapes[shapesId[0]] : shapes.shapes[shapesId] if (block.states || version.type === 'bedrock') { // post 1.13 if (shapesId instanceof Array) { block.stateShapes = [] for (const i in shapesId) { block.stateShapes.push(shapes.shapes[shapesId[i]]) } } } else { // pre 1.13 if ('variations' in block) { for (const i in block.variations) { const metadata = block.variations[i].metadata if (shapesId instanceof Array) { block.variations[i].shapes = shapes.shapes[shapesId[metadata]] } else { block.variations[i].shapes = shapes.shapes[shapesId] } } } } if (!block.shapes && version.type === 'bedrock') { // if no shapes are present for this block (for example, some chemistry stuff we don't have BBs for), assume it's stone block.shapes = shapes.shapes[shapes.blocks.stone[0]] block.stateShapes = block.shapes } } } function getEffectLevel (effectName, effects) { const effectDescriptor = registry.effectsByName[effectName] if (!effectDescriptor) { return 0 } const effectInfo = effects[effectDescriptor.id] if (!effectInfo) { return 0 } return effectInfo.amplifier + 1 } function getEnchantmentLevel (enchantmentName, enchantments) { const enchantmentDescriptor = registry.enchantmentsByName[enchantmentName] if (!enchantmentDescriptor) { return 0 } for (const enchInfo of enchantments) { if (typeof enchInfo.name === 'string') { if (enchInfo.name.includes(enchantmentName)) { return enchInfo.lvl } } else if (enchInfo.name === enchantmentDescriptor.name) { return enchInfo.lvl } } return 0 } function getMiningFatigueMultiplier (effectLevel) { switch (effectLevel) { case 0: return 1.0 case 1: return 0.3 case 2: return 0.09 case 3: return 0.0027 default: return 8.1E-4 } } return class Block { constructor (type, biomeId, metadata, stateId) { this.type = type this.metadata = metadata ?? 0 this.light = 0 this.skyLight = 0 this.biome = new Biome(biomeId) this.position = null this.stateId = stateId this.computedStates = {} if (stateId === undefined && type !== undefined) { const b = registry.blocks[type] // Make sure the block is actually valid and metadata is within valid bounds this.stateId = b === undefined ? null : Math.min(b.minStateId + metadata, b.maxStateId) } const blockEnum = registry.blocksByStateId[this.stateId] if (blockEnum) { this.metadata = this.stateId - blockEnum.minStateId this.type = blockEnum.id this.name = blockEnum.name this.hardness = blockEnum.hardness this.displayName = blockEnum.displayName this.shapes = blockEnum.shapes if (blockEnum.stateShapes) { if (blockEnum.stateShapes[this.metadata] !== undefined) { this.shapes = blockEnum.stateShapes[this.metadata] } else { // Default to shape 0 this.shapes = blockEnum.stateShapes[0] this.missingStateShape = true } } else if (blockEnum.variations) { const variations = blockEnum.variations for (const i in variations) { if (variations[i].metadata === metadata) { this.displayName = variations[i].displayName this.shapes = variations[i].shapes } } } this.boundingBox = blockEnum.boundingBox this.transparent = blockEnum.transparent this.diggable = blockEnum.diggable this.material = blockEnum.material this.harvestTools = blockEnum.harvestTools this.drops = blockEnum.drops } else { this.name = '' this.displayName = '' this.shapes = [] this.hardness = 0 this.boundingBox = 'empty' this.transparent = true this.diggable = false } this._properties = {} if (version.type === 'pc') { if (usesBlockStates) { const blockEnum = registry.blocksByStateId[this.stateId] if (blockEnum && blockEnum.states) { let data = this.metadata for (let i = blockEnum.states.length - 1; i >= 0; i--) { const prop = blockEnum.states[i] this._properties[prop.name] = propValue(prop, data % prop.num_values) data = Math.floor(data / prop.num_values) } } } else { this._properties = legacyPcBlocksByIdmeta[this.type + ':' + this.metadata] || legacyPcBlocksByIdmeta[this.type + ':0'] if (!this._properties) { // If no props, try different metadata for type match only for (let i = 0; i < 15; i++) { this._properties = legacyPcBlocksByIdmeta[this.type + ':' + i] if (this._properties) break } } } } else if (version.type === 'bedrock') { const states = registry.blockStates?.[this.stateId]?.states || {} for (const state in states) { this._properties[state] = states[state].value } } // This can be expanded to other non-sign related things if (this.name.includes('sign')) { mergeObject(this, blockMethods.sign) } } static fromStateId (stateId, biomeId) { // 1.13+: metadata is completely removed and only block state IDs are used if (usesBlockStates) { return new Block(undefined, biomeId, 0, stateId) } else { return new Block(stateId >> 4, biomeId, stateId & 15, stateId) } } static fromProperties (typeId, properties, biomeId) { const block = typeof typeId === 'string' ? registry.blocksByName[typeId] : registry.blocks[typeId] if (version.type === 'pc') { if (block.states) { let data = 0 for (const [key, value] of Object.entries(properties)) { data += getStateValue(block.states, key, value) } return new Block(undefined, biomeId, 0, block.minStateId + data) } else { const states = legacyPcBlocksByName[block.name] for (const state in states) { let broke for (const [key, value] of Object.entries(properties)) { const s = key + '=' + value if (!state.includes(s)) { broke = true break } } if (!broke) { const [id, meta] = states[state].split(':').map(Number) return new Block(id, biomeId, meta) } } throw new Error('No matching block state found for ' + block.name + ' with properties ' + JSON.stringify(properties)) // This should not happen } } else if (version.type === 'bedrock') { for (let stateId = block.minStateId; stateId <= block.maxStateId; stateId++) { const state = registry.blockStates[stateId].states if (Object.entries(properties).find(([prop, val]) => state[prop]?.value !== val)) continue return new Block(undefined, biomeId, 0, stateId) } return block } } static fromString (str, biomeId) { if (str.startsWith('minecraft:')) str = str.substring(10) const name = str.split('[', 1)[0] const propertiesStr = str.slice(name.length + 1, -1).split(',') if (version.type === 'pc') { return Block.fromProperties(name, Object.fromEntries(propertiesStr.map(property => property.split('='))), biomeId) } else if (version.type === 'bedrock') { return Block.fromProperties(name, Object.fromEntries(propertiesStr.map(property => { const [key, value] = property.split(':') return [key.slice(1, -1), value.startsWith('"') ? value.slice(1, -1) : { true: 1, false: 0 }[value] ?? parseInt(value)] })), biomeId) } } get blockEntity () { return this.entity ? nbt.simplify(this.entity) : undefined } getProperties () { return Object.assign(this._properties, this.computedStates) } canHarvest (heldItemType) { if (!this.harvestTools) { return true }; // for blocks harvestable by hand return heldItemType && this.harvestTools && this.harvestTools[heldItemType] } // http://minecraft.gamepedia.com/Breaking#Calculation // for more concrete information, look up following Minecraft methods (assuming yarn mappings): // AbstractBlock#calcBlockBreakingDelta, PlayerEntity#getBlockBreakingSpeed, PlayerEntity#canHarvest digTime (heldItemType, creative, inWater, notOnGround, enchantments = [], effects = {}) { if (creative) return 0 const materialToolMultipliers = registry.materials[this.material] const isBestTool = heldItemType && materialToolMultipliers && materialToolMultipliers[heldItemType] // Compute breaking speed multiplier let blockBreakingSpeed = 1 if (isBestTool) { blockBreakingSpeed = materialToolMultipliers[heldItemType] } // Efficiency is applied if tools speed multiplier is more than 1.0 const efficiencyLevel = getEnchantmentLevel('efficiency', enchantments) if (efficiencyLevel > 0 && blockBreakingSpeed > 1.0) { blockBreakingSpeed += efficiencyLevel * efficiencyLevel + 1 } // Haste is always considered when effect is present, and when both // Conduit Power and Haste are present, highest level is considered const hasteLevel = Math.max( getEffectLevel('Haste', effects), getEffectLevel('ConduitPower', effects)) if (hasteLevel > 0) { blockBreakingSpeed *= 1 + (0.2 * hasteLevel) } // Mining fatigue is applied afterwards, but multiplier only decreases up to level 4 const miningFatigueLevel = getEffectLevel('MiningFatigue', effects) if (miningFatigueLevel > 0) { blockBreakingSpeed *= getMiningFatigueMultiplier(miningFatigueLevel) } // Apply 5x breaking speed de-buff if we are submerged in water and do not have aqua affinity const aquaAffinityLevel = getEnchantmentLevel('aqua_affinity', enchantments) if (inWater && aquaAffinityLevel === 0) { blockBreakingSpeed /= 5.0 } // We always get 5x breaking speed de-buff if we are not on the ground if (notOnGround) { blockBreakingSpeed /= 5.0 } // Compute block breaking delta (breaking progress applied in a single tick) const blockHardness = this.hardness const matchingToolMultiplier = this.canHarvest(heldItemType) ? 30.0 : 100.0 let blockBreakingDelta = blockBreakingSpeed / blockHardness / matchingToolMultiplier // Delta will always be zero if block has -1.0 durability if (blockHardness === -1.0) { blockBreakingDelta = 0.0 } // We will never be capable of breaking block if delta is zero, so abort now and return infinity if (blockBreakingDelta === 0.0) { return Infinity } // If breaking delta is more than 1.0 per tick, the block is broken instantly, so return 0 if (blockBreakingDelta >= 1.0) { return 0 } // Determine how many ticks breaking will take, then convert to millis and return result // We round ticks up because if progress is below 1.0, it will be finished next tick const ticksToBreakBlock = Math.ceil(1.0 / blockBreakingDelta) return ticksToBreakBlock * 50 } } function parseValue (value, state) { if (state.type === 'enum') { return state.values.indexOf(value) } if (state.type === 'bool') { if (value === true) return 0 if (value === false) return 1 } if (state.type === 'int') { return value } // Assume by-name mapping for unknown properties return state.values?.indexOf(value.toString()) ?? 0 } function getStateValue (states, name, value) { let offset = 1 for (let i = states.length - 1; i >= 0; i--) { const state = states[i] if (state.name === name) { return offset * parseValue(value, state) } offset *= state.num_values } return 0 } function propValue (state, value) { if (state.type === 'enum') return state.values[value] if (state.type === 'bool') return !value return value } } function mergeObject (to, from) { Object.defineProperties(to, Object.getOwnPropertyDescriptors(from)) }