Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 54 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,52 @@ function provider (registry, { Biome, version }) {
return 0
}

// Block tags (registry.tags, minecraft-data 1.13+) describe which tools mine a block since 1.17. When present they
// replace the single-material speed lookup: a block can be in several mineable/* tags, and the sword and shears
// rules below are the vanilla ToolMaterial/ShearsItem constants, which are not in any data file.
const blockTags = registry.tags?.['minecraft:block']
const tagSets = {}
function inTag (tag, blockName) {
if (!tagSets[tag]) tagSets[tag] = new Set(blockTags?.[`minecraft:${tag}`] ?? [])
return tagSets[tag].has(`minecraft:${blockName}`)
}
const hasToolTags = Boolean(blockTags?.['minecraft:mineable/pickaxe'])
const TOOL_TYPES = ['pickaxe', 'axe', 'shovel', 'hoe']

function tagBreakingSpeed (block, heldItemType) {
const item = registry.items[heldItemType]
if (!item) return 1
const name = block.name
if (item.name === 'shears') {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This kind of hard coding is not future proof how can we do better?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed these constants are not future proof. The good news is the speeds are not actually unique to this code: they already exist in registry.materials, keyed by the block's (possibly composite) material and the item id. For example on released 1.17 through 1.21 data:

  • materials["leaves;mineable/hoe"] has { <shears>: 15, <sword>: 1.5, <hoe>: ... }
  • materials.wool has { <shears>: 5 }, materials.vine_or_glow_lichen has { <shears>: 2, <sword>: 1.5 }, materials.coweb has { <shears>: 15, <sword>: 15 }

So instead of the shears/sword constants we can read the speed straight from the material table: registry.materials[block.material]?.[heldItemType] ?? 1. I prototyped that and compared it against the current hard-coded version across every block, 11 tools (hand, shears, each sword/pickaxe/axe/shovel/hoe tier) and 9 versions (1.16.5 to 1.21.11): 98,835 (block, tool, version) cells, 0 differences. The only speed not in the material table is a sword mining bamboo instantly, which is a getDestroyProgress override rather than a tool speed; that stays as the sword_instantly_mines tag (1.21.5+) or the bamboo name before it.

The one caveat is the one that led me to hard-code them originally: the generator can drop these composite speeds when the data is regenerated (the leaves-sword-1.5-to-1 regression), which is being fixed separately in minecraft-data-generator#88. Released data carries them correctly, so a consumer reading registry.materials is safe today.

The tradeoff is that this sources the speed from registry.materials rather than the tag set, so the dispatch becomes material-driven and the current tag-fixture tests would move to material-based assertions. I did not want to pivot the PR's approach without your call. Want me to take the material-table direction (I will convert the tests to match), or keep the tag dispatch and just document these as the vanilla ToolMaterial/ShearsItem constants?

if (name === 'cobweb' || inTag('leaves', name)) return 15
if (inTag('wool', name)) return 5
if (name === 'vine' || name === 'glow_lichen') return 2
return 1
}
if (item.name.endsWith('_sword')) {
if (name === 'cobweb') return 15
// bamboo breaks instantly with a sword: BambooStalkBlock/BambooSaplingBlock.getDestroyProgress up to 1.21.4, the
// sword_instantly_mines tag from 1.21.5
const instantly = blockTags['minecraft:sword_instantly_mines'] ? inTag('sword_instantly_mines', name) : (name === 'bamboo' || name === 'bamboo_sapling')
if (instantly) return Infinity
// 1.20+: SwordItem.getDestroySpeed is 1.5 for the sword_efficient tag.
if (blockTags['minecraft:sword_efficient']) return inTag('sword_efficient', name) ? 1.5 : 1
// 1.17 to 1.19 have no sword_efficient tag: getDestroySpeed returns 1.5 for the leaves tag and the PLANT,
// REPLACEABLE_PLANT, VEGETABLE (gourd) and VINE materials, 1 otherwise. These are ToolMaterial constants, so
// encode them here rather than trusting the material table, whose composite sword speeds do not survive
// regeneration (e.g. leaves' sword speed drops from 1.5 to 1 when the tags are rebuilt).
if (inTag('leaves', name)) return 1.5
const materials = (block.material ?? '').split(';')
if (materials.some(m => m === 'plant' || m === 'gourd' || m === 'vine_or_glow_lichen')) return 1.5
return 1
}
for (const type of TOOL_TYPES) {
const speed = registry.materials[`mineable/${type}`]?.[heldItemType]
if (speed && inTag(`mineable/${type}`, name)) return speed
}
return 1
}

function getMiningFatigueMultiplier (effectLevel) {
switch (effectLevel) {
case 0: return 1.0
Expand Down Expand Up @@ -306,14 +352,17 @@ function provider (registry, { Biome, version }) {
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]
if (heldItemType && hasToolTags) {
blockBreakingSpeed = tagBreakingSpeed(this, heldItemType)
if (blockBreakingSpeed === Infinity) return 0
} else {
const materialToolMultipliers = registry.materials[this.material]
if (heldItemType && materialToolMultipliers && materialToolMultipliers[heldItemType]) {
blockBreakingSpeed = materialToolMultipliers[heldItemType]
}
}

// Efficiency is applied if tools speed multiplier is more than 1.0
Expand Down
86 changes: 86 additions & 0 deletions test/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,92 @@

const expect = require('expect').default

// Block tags decide which tools mine a block from 1.17 on (minecraft-data tags.json, exposed as registry.tags).
// The fixture is the subset of vanilla 26.1 tags these cases need, in the tags.json shape.
describe('Dig time from block tags', () => {
const registry = require('prismarine-registry')('26.1')
registry.tags = {
'minecraft:block': {
'minecraft:leaves': ['minecraft:oak_leaves'],
'minecraft:mineable/axe': ['minecraft:melon', 'minecraft:oak_log'],
'minecraft:mineable/hoe': ['minecraft:oak_leaves'],
'minecraft:mineable/pickaxe': ['minecraft:iron_ore', 'minecraft:obsidian', 'minecraft:stone'],
'minecraft:sword_efficient': ['minecraft:melon'],
'minecraft:sword_instantly_mines': ['minecraft:bamboo'],
'minecraft:wool': ['minecraft:white_wool']
}
}
const Block = require('prismarine-block')(registry)
const block = name => Block.fromStateId(registry.blocksByName[name].defaultState, 0)
const item = name => registry.itemsByName[name].id
const dig = (blockName, itemName, enchantments = []) => block(blockName).digTime(itemName ? item(itemName) : null, false, false, false, enchantments)

it('uses the pickaxe tier speed for a tier-gated ore', () => {
expect(dig('iron_ore', 'diamond_pickaxe')).toBe(600)
expect(dig('obsidian', 'diamond_pickaxe')).toBe(9400)
})
it('applies efficiency on top of the tag speed', () => {
expect(dig('iron_ore', 'diamond_pickaxe', [{ name: 'efficiency', lvl: 5 }])).toBe(150)
})
it('keeps the harvest penalty for a tool below the required tier', () => {
expect(dig('iron_ore', 'wooden_pickaxe')).toBe(7500)
expect(dig('stone', null)).toBe(7500)
})
it('uses the vanilla sword rules', () => {
expect(dig('melon', 'diamond_sword')).toBe(1000)
expect(dig('melon', 'diamond_axe')).toBe(200)
expect(dig('bamboo', 'diamond_sword')).toBe(0)
})
it('uses the vanilla shears rules', () => {
expect(dig('oak_leaves', 'shears')).toBe(0)
expect(dig('white_wool', 'shears')).toBe(250)
})
it('ignores tools that do not apply to the block', () => {
expect(dig('oak_log', 'diamond_pickaxe')).toBe(dig('oak_log', null))
})
})

describe('Dig time from block tags before the sword tags (1.17 to 1.19)', () => {
const registry = require('prismarine-registry')('1.19.4')
registry.tags = {
'minecraft:block': {
'minecraft:leaves': ['minecraft:oak_leaves'],
'minecraft:mineable/axe': ['minecraft:bamboo', 'minecraft:melon'],
'minecraft:mineable/hoe': ['minecraft:oak_leaves'],
'minecraft:mineable/pickaxe': ['minecraft:stone']
}
}
const Block = require('prismarine-block')(registry)
const block = name => Block.fromStateId(registry.blocksByName[name].defaultState, 0)
const dig = (blockName, itemName) => block(blockName).digTime(registry.itemsByName[itemName].id, false, false, false)

it('applies the pre-1.20 sword rule from the leaves tag and plant materials', () => {
// SwordItem.getDestroySpeed is 1.5 for the leaves tag and the PLANT/VEGETABLE/VINE materials. This comes from the
// tag and material names, not the material table's composite sword speeds, which drop to 1 when tags are regenerated.
expect(dig('oak_leaves', 'diamond_sword')).toBe(200) // leaves tag -> 1.5
expect(dig('melon', 'diamond_sword')).toBe(1000) // gourd (VEGETABLE) material -> 1.5
expect(dig('vine', 'diamond_sword')).toBe(200) // vine material -> 1.5
expect(dig('stone', 'diamond_sword')).toBe(7500) // no sword rule -> 1
})
it('mines bamboo instantly with a sword', () => {
expect(dig('bamboo', 'diamond_sword')).toBe(0)
expect(dig('bamboo', 'diamond_axe')).toBe(200)
})
it('still takes the tool tiers from the mineable tags', () => {
expect(dig('oak_leaves', 'diamond_hoe')).toBe(0)
expect(dig('stone', 'diamond_pickaxe')).toBe(300)
})
})

describe('Dig time without block tags (before 1.17)', () => {
const registry = require('prismarine-registry')('1.16.4')
const Block = require('prismarine-block')(registry)
it('mines stone with an iron pickaxe from materials', () => {
const stone = Block.fromStateId(registry.blocksByName.stone.defaultState, 0)
expect(stone.digTime(registry.itemsByName.iron_pickaxe.id, false, false, false)).toBe(400)
})
})

// https://minecraft.gamepedia.com/Breaking#Blocks_by_hardness
describe('Dig time', () => {
describe('1.20.5', () => {
Expand Down
Loading