Files
horizon/tools/test_use_item_on_parity.py
2026-11-07 12:41:14 +00:00

1042 lines
56 KiB
Python

#!/usr/bin/env python3
"""Compare a player's right click on a block with vanilla's, for items that are
both food and blocks (sweet and glow berries, carrots, potatoes), for sweet
berry bushes, for spawn eggs used on a monster spawner, for shears carving
pumpkins or cropping a growing plant's head (kelp, weeping and twisting vines,
cave vines) to its top age, for copper golem statues turned to their next pose,
for blocks placed where mobs and armor stands stand, for respawn anchors, for
beds in the Nether and the End, and for doors, trapdoors and fence gates, which a
hand opens unless their material cannot be (`BlockSetType.canOpenByHand`).
Axe, hoe and shovel clicks use the data-driven `minecraft:block_transformer`
registry (`ItemStack.useOn` -> `Item.useOn` -> `BlockTransformer.transformBlock`):
logs stripped and copper scraped, waxed or unwaxed, a double copper chest's other
half told of the change, copper doors and trapdoors, farmland and dirt paths, and
the hanging roots a hoe leaves on rooted dirt's clicked face. The states those
transforms produce are compared by `test_block_transformer_parity.py`; here the
click that uses one is.
Dragon eggs, which a click moves somewhere else nearby
(`DragonEggBlock.useWithoutItem`).
Every case has a block at the origin, something above it, and a player three
blocks south, survival or creative, hungry or full, sneaking or not, who
right-clicks the top face of the origin (for pumpkins, any face, turned any
way) holding an item, sometimes with another (or nothing) in the main hand
and the item in the off hand, the main hand trying first. Vanilla runs the
real `ServerPlayerGameMode.useItemOn` over the headless level of the
dispenser harness (`VanillaDispenserOracle.useItem`): the block's `useItemOn`
and `useWithoutItem` (picking a bush's berries, `SweetBerryBushBlock.useItemOn`
passing bone meal on, `PumpkinBlock.useItemOn` carving with shears and
dropping the seeds from the carved side), then `ItemStack.useOn`
(`BlockItem.place` with its `mayPlaceOn` rules, `BoneMealItem.useOn`,
`ShearsItem.useOn` cropping a growing plant's head, eating a consumable that
could not be placed, and `SpawnEggItem.useOn` setting a spawner's mob). Respawn
anchors are charged with glowstone, set the respawn point in the Nether and
explode elsewhere (`RespawnAnchorBlock`, through the stand-in level's real
`ServerExplosion`, with water at the anchor). Respawn cases call
`ServerPlayer.findRespawnAndUseSpawnBlock` on an anchor among blocks
(`RespawnAnchorBlock.findStandUpPosition` through `DismountHelper`): the
position, the yaw that faces the anchor and the charge it uses; others on a
bed's head set from some yaw (`AbstractBedBlock.findStandUpPosition`: intact,
bunk, only the foot left or gone, crowded around and on top). Villagers lying
in such beds wake up from some yaw (`Villager.stopSleeping`: the same search for
a villager's box, with shelves among the blocks around, #9502). Beds and straw
beds clicked (head or foot, whole or broken) in the Nether and the End blow up
or fall apart (`AbstractBedBlock.useWithoutItem` under the dimension's
`bed_rule`: `BedBlock.destroyOnUse`, `StrawBedBlock.destroyBed`). Horizon runs
`use_item_as_player` on that hit in a headless game
(`HORIZON_DISPENSE_FIXTURE`). Both print block changes, dropped items, level
events, sounds, game events, the used statistic, the held stack, the item the
player started using, spawners' next spawn data, potentials and delay, the
off hand's stack,
player's respawn point, system messages and the level random afterwards.
"""
import json
import os
import random
try:
from . import block_update_harness as h, lab, test_dispenser_parity as d, test_rust_parity as base
except ImportError:
import block_update_harness as h
import lab
import test_dispenser_parity as d
import test_rust_parity as base
# Blocks the item is used on: the supports of the planted blocks and others.
GROUNDS = ['grass_block', 'dirt', 'coarse_dirt', 'podzol', 'rooted_dirt', 'moss_block', 'pale_moss_block', 'mud',
'muddy_mangrove_roots', 'farmland', 'stone', 'sand', 'gravel', 'clay', 'snow_block', 'oak_planks']
ITEMS = ['sweet_berries', 'sweet_berries', 'sweet_berries', 'glow_berries', 'carrot', 'potato', 'bone_meal',
'wheat_seeds', 'beetroot_seeds', 'apple']
# What is in the way above the clicked block.
ABOVE = ['air'] * 8 + ['short_grass', 'snow', 'stone', 'water', 'dead_bush', 'fern']
EGGS = ['zombie', 'skeleton', 'spider', 'cave_spider', 'pig', 'creeper', 'blaze', 'ghast', 'villager', 'bee', 'cod']
# What stands around an anchor that explodes.
AROUND = ['stone', 'dirt', 'oak_planks', 'cobblestone', 'glass', 'obsidian', 'netherrack', 'water', 'tnt', 'oak_log',
'grass_block', 'soul_sand', 'gravel', 'crying_obsidian']
STAND = ['air'] * 6 + ['stone', 'stone', 'oak_slab', 'snow', 'lava', 'magma_block', 'fire', 'cactus', 'water',
'ladder', 'oak_trapdoor', 'short_grass', 'white_carpet', 'campfire', 'sweet_berry_bush',
'oak_fence', 'powder_snow', 'farmland', 'soul_sand', 'cobweb', 'end_portal']
def respawn_case(rng, states, case):
"""Respawning at an anchor with blocks around it."""
charges = rng.choice([0, 1, 1, 2, 4])
cells = [[0, 0, 0, states.get('respawn_anchor', charges=str(charges))]]
for dy in (-2, -1, 0, 1, 2):
for dx in range(-1, 2):
for dz in range(-1, 2):
if (dx, dy, dz) == (0, 0, 0):
continue
if dy == -1 or dy == -2:
name = rng.choice(['stone'] * 6 + STAND)
else:
name = rng.choice(['air'] * 3 + STAND)
if name == 'air':
continue
state = states.any(rng, name) if name in ('oak_trapdoor', 'oak_slab', 'snow', 'campfire', 'water', 'ladder') else states.get(name)
cells.append([dx, dy, dz, state])
if rng.random() < 0.6:
case['dim'] = 'nether'
case['respawn_at'] = {'forced': rng.random() < 0.2, 'consume': rng.random() < 0.8}
case['cells'] = cells
BED_COLORS = ['red', 'white', 'black', 'cyan', 'lime']
FACINGS = {'north': (0, -1), 'south': (0, 1), 'west': (-1, 0), 'east': (1, 0)}
OPPOSITE = {'north': 'south', 'south': 'north', 'west': 'east', 'east': 'west'}
def bed_respawn_case(rng, states, case):
"""Respawning at a bed's head set from some yaw: intact, a bunk bed, only the foot left or
gone, with blocks around and on it, in the Overworld (and a few in the Nether or the End)."""
name = 'straw_bed' if rng.random() < 0.15 else rng.choice(BED_COLORS) + '_bed'
cells = bed_cells(rng, states, name, STAND)
if rng.random() < 0.1:
case['dim'] = rng.choice(['nether', 'end'])
yaw = rng.choice([0.0, 90.0, -90.0, 180.0, round(rng.uniform(-720, 720), rng.choice([0, 2]))])
case['respawn_at'] = {'forced': rng.random() < 0.15, 'consume': rng.random() < 0.8, 'yaw': yaw}
case['cells'] = cells
def bed_cells(rng, states, name, stand):
"""A bed `name` at the origin: intact, a bunk bed, only the foot left or gone, with blocks from
`stand` around and on it."""
facing = rng.choice(list(FACINGS))
fx, fz = FACINGS[facing]
bed = {}
kept = rng.random()
if kept < 0.75:
bed[(0, 0, 0)] = states.get(name, facing=facing, part='head', occupied='false')
elif kept < 0.85:
bed[(0, 0, 0)] = states.get(rng.choice(['stone', 'white_carpet', 'oak_slab']))
if kept < 0.9 or rng.random() < 0.5:
bed[(-fx, 0, -fz)] = states.get(name, facing=facing, part='foot', occupied='false')
if rng.random() < 0.2:
# A bunk bed: another bed below.
bed[(0, -1, 0)] = states.get(name, facing=facing, part='head', occupied='false')
bed[(-fx, -1, -fz)] = states.get(name, facing=facing, part='foot', occupied='false')
crowd = rng.choice([0.0, 0.1, 0.3, 0.6, 0.95])
cells = [[*k, v] for k, v in bed.items()]
if (0, 0, 0) not in bed:
cells.insert(0, [0, 0, 0, states.get('air')])
for dy in (-2, -1, 0, 1, 2):
for dx in range(-3, 4):
for dz in range(-3, 4):
if (dx, dy, dz) in bed or (dx, dy, dz) == (0, 0, 0):
continue
if dy == -1 and (dx, 0, dz) in bed:
name2 = 'stone'
elif dy < 0:
name2 = rng.choice(['stone'] * 8 + stand)
elif rng.random() < crowd:
name2 = rng.choice(['stone'] * 4 + stand)
else:
continue
if name2 == 'air':
continue
varied = name2 in ('oak_trapdoor', 'oak_slab', 'snow', 'campfire', 'water', 'ladder') or name2.endswith('_shelf')
state = states.any(rng, name2) if varied else states.get(name2)
cells.append([dx, dy, dz, state])
return cells
# What a villager waking up may stand on or beside: shelves (#9502) beside the blocks a player
# stands up on.
WAKE_STAND = STAND + ['oak_shelf', 'spruce_shelf', 'bamboo_shelf', 'oak_shelf']
def wake_case(rng, states, case):
"""A villager lying in a bed (`Villager.stopSleeping`) wakes up from some yaw: the bed intact, a
bunk bed, only the foot left or gone, with blocks and shelves around and on it."""
case['cells'] = bed_cells(rng, states, rng.choice(BED_COLORS) + '_bed', WAKE_STAND)
yaw = rng.choice([0.0, 90.0, -90.0, 180.0, round(rng.uniform(-720, 720), rng.choice([0, 2]))])
case['wake'] = {'yaw': yaw}
def bed_case(rng, states, case):
"""A bed (or straw bed) clicked in the Nether or the End, among blocks."""
name = 'straw_bed' if rng.random() < 0.25 else rng.choice(BED_COLORS) + '_bed'
facing = rng.choice(list(FACINGS))
dx, dz = FACINGS[facing]
# The clicked block is the origin; the other half lies along the facing.
part = rng.choice(['head', 'foot'])
other = (-dx, 0, -dz) if part == 'head' else (dx, 0, dz)
cells = [[0, 0, 0, states.get(name, facing=facing, part=part, occupied='false')],
[0, -1, 0, states.get(rng.choice(['netherrack', 'end_stone', 'obsidian', 'stone']))],
[other[0], -1, other[2], states.get('stone')]]
if rng.random() < 0.9:
cells.append([*other, states.get(name, facing=facing, part='foot' if part == 'head' else 'head', occupied='false')])
taken = {tuple(c[:3]) for c in cells}
for _ in range(rng.choice([0, 3, 10, 30])):
p = (rng.randrange(-3, 4), rng.randrange(-2, 3), rng.randrange(-3, 4))
if p in taken:
continue
taken.add(p)
cells.append([*p, states.get(rng.choice(AROUND))])
case['dim'] = rng.choice(['nether', 'end'])
item = rng.choice(['-', '-', 'apple', 'stone', 'glowstone'])
case['use_item'] = {'item': '-' if item == '-' else d_plain(item, 1), 'creative': rng.random() < 0.2,
'sneaking': rng.random() < 0.1, 'food': 20, 'distance': 12}
if rng.random() < 0.2:
case['rules'] = {'block_explosion_drop_decay': False}
case['cells'] = cells
def anchor_case(rng, states, case):
"""A respawn anchor clicked in the Nether or the Overworld, with blocks
and water around it."""
charges = rng.choice([0, 0, 1, 2, 3, 4, 4])
cells = [[0, 0, 0, states.get('respawn_anchor', charges=str(charges))],
[0, -1, 0, states.get(rng.choice(['netherrack', 'stone', 'soul_sand', 'obsidian', 'soul_soil']))]]
taken = {(0, 0, 0), (0, -1, 0)}
for _ in range(rng.choice([0, 3, 8, 20, 40])):
p = (rng.randrange(-3, 4), rng.randrange(-2, 3), rng.randrange(-3, 4))
if p in taken:
continue
taken.add(p)
name = rng.choice(AROUND)
state = states.any(rng, 'water') if name == 'water' and rng.random() < 0.5 else states.get(name)
cells.append([*p, state])
if rng.random() < 0.2:
# Water that would flow into the anchor's place.
d = rng.choice([(1, 0, 0), (-1, 0, 0), (0, 0, 1), (0, 0, -1), (0, 1, 0)])
cells = [c for c in cells if tuple(c[:3]) != d]
cells.append([*d, states.any(rng, 'water') if rng.random() < 0.4 else states.get('water')])
nether = rng.random() < 0.5
if nether:
case['dim'] = 'nether'
item = rng.choice(['glowstone'] * 5 + ['apple', '-', 'stone', 'glowstone_dust'])
count = rng.choice([1, 1, 16, 64])
use = {'item': '-' if item == '-' else d_plain(item, count), 'creative': rng.random() < 0.2,
'sneaking': rng.random() < 0.1, 'food': 20,
# Away from the blast: the stand-in player is not an entity of the level.
'distance': 3 if nether else 12}
if rng.random() < 0.3:
use['respawn'] = rng.choice([[0, 0, 0], [5, 0, 5]])
if rng.random() < 0.4:
# Glowstone in the off hand: the main hand passes it on (PASS).
use['offhand'] = d_plain(rng.choice(['glowstone', 'glowstone', 'apple']), rng.choice([1, 16]))
if rng.random() < 0.2:
case['rules'] = {'block_explosion_drop_decay': False}
case['use_item'] = use
case['cells'] = cells
def pumpkin_case(rng, states, case):
"""Shears (or something else) used on a pumpkin (`PumpkinBlock.useItemOn`):
any face, the player turned any way, worn shears (the stand-in player
has no entity id for the break event, so they keep one use)."""
cells = [[0, 0, 0, states.get(rng.choice(['pumpkin'] * 6 + ['carved_pumpkin', 'melon']))]]
if rng.random() < 0.3:
cells.append([0, 1, 0, states.get(rng.choice(['stone', 'snow', 'short_grass']))])
damage = rng.choice([0, 0, 0, 100, 236])
shears = d_plain('shears', 1) if damage == 0 else \
'{id:"minecraft:shears",count:1,components:{"minecraft:damage":%d}}' % damage
item = rng.choice([shears] * 6 + [d_plain('stick', 1), d_plain('pumpkin', 3), '-'])
use = {'item': item, 'creative': rng.random() < 0.2, 'sneaking': rng.random() < 0.15, 'food': 20,
'yaw': rng.choice([0, 30, 90, 135, 180, 200, 270, 315, -45, -100]),
'face': rng.choice(['up', 'up', 'down', 'north', 'south', 'west', 'east'])}
if rng.random() < 0.3:
# Shears in the off hand, something else or nothing in the main hand.
use.update(offhand=item, item=rng.choice(['-', '-', d_plain('stick', 1), d_plain('stone', 16)]))
case['use_item'] = use
case['cells'] = cells
# Growing plant heads (kelp, the two nether vines, cave vines) and the plants' bodies.
HEADS = ['kelp', 'weeping_vines', 'twisting_vines', 'cave_vines']
BODIES = ['kelp_plant', 'weeping_vines_plant', 'twisting_vines_plant', 'cave_vines_plant']
def crop_case(rng, states, case):
"""Shears (or something else) on a growing plant's head (`ShearsItem.useOn`): a head below its
top age is cropped to `GrowingPlantHeadBlock.getMaxAgeState` (age 25) with a
`block.growing_plant.crop` sound, a `block_change` game event and one point of wear; a head
already at 25, or a plant's body (`GrowingPlantBodyBlock`), takes nothing (`Item.useOn`)."""
head = rng.random() < 0.85
name = rng.choice(HEADS) if head else rng.choice(BODIES)
props = {}
if name in ('cave_vines', 'cave_vines_plant'):
# Berry-less: a berry-bearing vine picks its berries first (`useWithoutItem`), which is
# another arm's.
props['berries'] = 'false'
if head:
# Heads carry `AGE_25`; the bodies below them carry no age at all.
props['age'] = str(rng.choice([0, 3, 11, 24, 25, 25]))
state = states.get(name, **props) if props else states.any(rng, name)
cells = [[0, 0, 0, state]]
if rng.random() < 0.3:
cells.append([0, -1, 0, states.get(rng.choice(['stone', 'dirt', 'sand', 'water']))])
damage = rng.choice([0, 0, 0, 100, 236])
shears = d_plain('shears', 1) if damage == 0 else \
'{id:"minecraft:shears",count:1,components:{"minecraft:damage":%d}}' % damage
item = rng.choice([shears] * 6 + [d_plain('stick', 1), d_plain('stick', 1), '-'])
use = {'item': item, 'creative': rng.random() < 0.2, 'sneaking': rng.random() < 0.15, 'food': 20}
if rng.random() < 0.2:
# Shears in the off hand, something else or nothing in the main hand.
use.update(offhand=item, item=rng.choice(['-', d_plain('stick', 1)]))
case['use_item'] = use
case['cells'] = cells
WIRE_SIDES = ['none', 'side', 'up']
def wire_case(rng, states, case):
"""A redstone wire clicked with an empty hand (`RedstoneWireBlock.useWithoutItem`): a wire that
is neither a `cross` (connected to all four sides) nor a `dot` (connected to none) passes the
click on, and one that is either takes the opposite shape, keeping its `POWER` and its
connections, which `getConnectionState` recomputes while `updatesOnShapeChange` notifies the
neighbours whose connection changed. Nothing is played and no game event is sent."""
props = dict(zip(('north', 'east', 'south', 'west'), (rng.choice(WIRE_SIDES) for _ in range(4))))
props['power'] = str(rng.choice([0, 0, 1, 7, 15]))
cells = [[0, 0, 0, states.get('redstone_wire', **props)],
[0, -1, 0, states.get(rng.choice(['stone', 'dirt', 'oak_planks', 'grass_block']))]]
for direction, at in (('north', (0, 0, -1)), ('east', (1, 0, 0)), ('south', (0, 0, 1)), ('west', (-1, 0, 0))):
if rng.random() < 0.6:
name = rng.choice(['redstone_wire', 'redstone_wire', 'lever', 'stone', 'repeater', 'redstone_block'])
# Wire, levers and repeaters have too many states to name one; stone and
# a redstone block have a single default.
varied = ('redstone_wire', 'lever', 'repeater')
cells.append([*at, states.any(rng, name) if name in varied else states.get(name)])
case['use_item'] = {'item': '-', 'creative': rng.random() < 0.2, 'sneaking': rng.random() < 0.15,
'food': 20, 'face': rng.choice(['up', 'north', 'south', 'west', 'east'])}
case['cells'] = cells
STATUES = [f'{wax}{age}copper_golem_statue' for wax in ('', 'waxed_')
for age in ('', 'exposed_', 'weathered_', 'oxidized_')]
def statue_case(rng, states, case):
"""A copper golem statue right-clicked (`CopperGolemStatueBlock.useItemOn`, the weathering
one's): anything but an axe, an empty hand too, turns it to its next pose; honeycomb waxes a
statue that still weathers instead (`HoneycombItem.useOn`), and an axe scrapes or unwaxes it."""
statue = rng.choice(STATUES)
case['cells'] = [[0, 0, 0, states.any(rng, statue)]]
items = ['-', '-', 'stick', 'honeycomb', 'honeycomb', 'stone', 'copper_ingot', 'diamond_pickaxe',
'copper_golem_statue']
if statue != 'copper_golem_statue':
# An axe scrapes or unwaxes the others; on the unaffected one it brings the golem back
# (`CopperGolemStatueBlockEntity.removeStatue`), which Horizon does not do yet.
items.append('iron_axe')
item = rng.choice(items)
use = {'item': '-' if item == '-' else d_plain(item, rng.choice([1, 16])), 'creative': rng.random() < 0.2,
'sneaking': rng.random() < 0.15, 'food': 20,
'face': rng.choice(['up', 'up', 'north', 'south', 'west', 'east'])}
if rng.random() < 0.2:
# The item in the off hand, nothing in the main hand.
use.update(offhand=use['item'], item='-')
case['use_item'] = use
def timed_case(rng, states, case):
"""A repeater, a comparator or a daylight detector clicked with an empty hand, none of which may
be turned where `Player.getAbilities().mayBuild` is false (`RepeaterBlock.useWithoutItem`,
`ComparatorBlock.useWithoutItem`, `DaylightDetectorBlock.useWithoutItem` all answer PASS there).
A repeater cycles its `delay` through 1 to 4 with `setBlockAndUpdate` and nothing else; a
comparator cycles `mode` between `compare` and `subtract` with a `block.comparator.click` at
volume 0.3 and pitch 0.55 for `subtract` / 0.5 for `compare`, `Level.setBlock` writes the state
and the comparator refreshes its output; a daylight detector cycles `inverted`, writes the state,
sends `block_change` and recomputes the `power` it emits."""
name = rng.choice(['repeater'] * 2 + ['comparator'] * 2 + ['daylight_detector'])
props = {'facing': rng.choice(['north', 'south', 'west', 'east'])}
if name == 'repeater':
props.update(delay=str(rng.randrange(1, 5)), locked=rng.choice(['true', 'false']))
elif name == 'comparator':
props.update(mode=rng.choice(['compare', 'subtract']))
else:
props.pop('facing')
props.update(inverted=rng.choice(['true', 'false']), power=str(rng.randrange(16)))
cells = [[0, 0, 0, states.get(name, **props)],
[0, -1, 0, states.get(rng.choice(['stone', 'dirt', 'oak_planks', 'grass_block']))]]
if name != 'daylight_detector' and rng.random() < 0.5:
# Something for the diode to read, on the side it faces.
face = {'north': (0, 0, -1), 'south': (0, 0, 1), 'west': (-1, 0, 0), 'east': (1, 0, 0)}[props['facing']]
cells.append([*face, states.get(rng.choice(['redstone_wire', 'lever', 'stone', 'redstone_block']))])
case['use_item'] = {'item': '-', 'creative': rng.random() < 0.2, 'sneaking': rng.random() < 0.15,
'food': 20, 'face': rng.choice(['up', 'up', 'north', 'south', 'west', 'east'])}
case['cells'] = cells
# Entities a block may be placed into (`Entity.blocksBuilding`), and blocks to place.
BLOCKERS = ['skeleton', 'zombie', 'pig', 'chicken', 'cow', 'armor_stand', 'slime', 'bat']
PLACED = ['obsidian', 'stone', 'oak_slab', 'oak_fence', 'white_carpet', 'torch', 'glass', 'oak_trapdoor', 'snow']
def blocked_case(rng, states, case):
"""A block placed on top of the origin where a mob or an armor stand stands, in its box or beside it
(`BlockItem.canPlace`'s `Level.isUnobstructed`)."""
case['cells'] = [[0, 0, 0, states.get(rng.choice(['stone', 'grass_block', 'dirt']))]]
entities = []
for _ in range(rng.choice([1, 1, 2])):
kind = rng.choice(BLOCKERS)
offset = [rng.choice([0, 0, 0.3, -0.45, 0.7, -1.2]), rng.choice([0, 0, 0.5, 0.95, 1.02, -0.5]),
rng.choice([0, 0, 0.3, -0.6, 0.9])]
e = {'type': f'minecraft:{kind}', 'pos': [0.5 + offset[0], 1 + offset[1], 0.5 + offset[2]]}
if kind == 'armor_stand' and rng.random() < 0.3:
e['marker'] = True
if kind in ('pig', 'cow', 'chicken') and rng.random() < 0.3:
e['baby'] = True
entities.append(e)
case['entities'] = entities
case['use_item'] = {'item': d_plain(rng.choice(PLACED), rng.choice([1, 16])), 'creative': rng.random() < 0.3,
'sneaking': False, 'food': 20}
BUTTONS = ['stone_button', 'oak_button', 'spruce_button', 'birch_button', 'jungle_button', 'acacia_button',
'cherry_button', 'dark_oak_button', 'mangrove_button', 'pale_oak_button', 'bamboo_button',
'crimson_button', 'warped_button', 'polished_blackstone_button']
def switch_case(rng, states, case):
"""A lever or a button clicked with an empty hand. `LeverBlock.useWithoutItem` always answers
SUCCESS and calls `pull`, which cycles `powered` both ways, writes it with `setBlockAndUpdate`,
updates the neighbours at the lever and at the block it hangs on, plays `block.lever.click` at
volume 0.3 with pitch 0.6 when the lever is now on and 0.5 when it is off, and sends
`block_activate` or `block_deactivate` (`LeverBlock.pull`, `LeverBlock.playSound`).
`ButtonBlock.useWithoutItem` answers CONSUME and touches nothing while the button is `powered`,
and otherwise `ButtonBlock.press`es it: `powered` on, the same neighbour updates, a release
scheduled `ticksToStayPressed` away (the block set type's own), the button type's own sound and
`block_activate`. Neither reads the held item, the hit or the game mode, so the case varies the
`face` and `facing` the block is attached by, every button type and `powered` either way."""
name = rng.choice(['lever'] * 2 + BUTTONS)
props = {'facing': rng.choice(['north', 'south', 'west', 'east']),
'face': rng.choice(['floor', 'wall', 'ceiling']),
'powered': rng.choice(['true', 'false'])}
cells = [[0, 0, 0, states.get(name, **props)],
[0, -1, 0, states.get(rng.choice(['stone', 'dirt', 'oak_planks', 'grass_block']))]]
case['use_item'] = {'item': '-', 'creative': rng.random() < 0.2, 'sneaking': rng.random() < 0.15,
'food': 20, 'face': rng.choice(['up', 'up', 'north', 'south', 'west', 'east', 'down'])}
case['cells'] = cells
def jukebox_case(rng, states, case):
"""A jukebox placed from a Creative pick that carries a record: the block entity takes the disc
and `JukeboxBlock.setPlacedBy` sets `has_record`, so the model shows it and a right click pops it
out. Without the state the box is empty while it holds one."""
case['cells'] = [[0, 0, 0, states.get(rng.choice(['stone', 'dirt', 'grass_block']))]]
data = ''
if rng.random() < 0.75:
record = rng.choice(['music_disc_cat', 'music_disc_13', 'music_disc_pigstep'])
data = ',components:{"minecraft:block_entity_data":{id:"minecraft:jukebox",' \
'RecordItem:{id:"minecraft:%s",count:1}}}' % record
case['use_item'] = {'item': '{id:"minecraft:jukebox",count:1%s}' % data,
'creative': True, 'sneaking': False, 'food': 20}
# Doors, trapdoors and fence gates of every material, and the ones whose `BlockSetType` cannot be
# opened by hand (`DoorBlock.useWithoutItem`'s `type.canOpenByHand`).
DOORS = ['oak', 'spruce', 'birch', 'jungle', 'acacia', 'dark_oak', 'mangrove', 'cherry', 'pale_oak', 'poplar',
'bamboo', 'crimson', 'warped', 'copper', 'exposed_copper', 'weathered_copper', 'oxidized_copper',
'waxed_copper', 'iron']
GATES = ['oak', 'spruce', 'birch', 'jungle', 'acacia', 'dark_oak', 'mangrove', 'cherry', 'pale_oak', 'poplar',
'bamboo', 'crimson', 'warped']
def openable_case(rng, states, case):
"""A door, trapdoor or fence gate right-clicked by hand (`DoorBlock.useWithoutItem`,
`TrapDoorBlock.useWithoutItem`, `FenceGateBlock.useWithoutItem`): the ones whose
`BlockSetType.canOpenByHand` turn to their other `open` with their open or close sound and
`block_open`/`block_close`, a waterlogged trapdoor also schedules the water again, a fence gate
closed across the player turns to face them first (`player.getDirection`) and draws its sound's
pitch, and an iron one passes, so what the hand holds is placed against it instead."""
kind = rng.random()
facing = rng.choice(list(FACINGS))
if kind < 0.4:
material = rng.choice(DOORS)
props = {'facing': facing, 'half': 'lower', 'hinge': rng.choice(['left', 'right']),
'open': rng.choice(['false', 'false', 'true']), 'powered': rng.choice(['false', 'true'])}
cells = [[0, 0, 0, states.get(material + '_door', **props)],
[0, 1, 0, states.get(material + '_door', **{**props, 'half': 'upper'})]]
if rng.random() < 0.5:
cells.append([0, -1, 0, states.get(rng.choice(['stone', 'dirt', 'oak_planks']))])
face = rng.choice([facing, OPPOSITE[facing], 'up'])
elif kind < 0.75:
material = rng.choice(DOORS)
cells = [[0, 0, 0, states.get(material + '_trapdoor', facing=facing, half=rng.choice(['bottom', 'top']),
open=rng.choice(['false', 'false', 'true']),
powered=rng.choice(['false', 'true']),
waterlogged=rng.choice(['false', 'false', 'false', 'true']))]]
if rng.random() < 0.5:
cells.append([0, -1, 0, states.get(rng.choice(['stone', 'dirt', 'oak_planks']))])
face = rng.choice([facing, OPPOSITE[facing], 'up', 'down'])
else:
material = rng.choice(GATES)
cells = [[0, 0, 0, states.get(material + '_fence_gate', facing=facing,
open=rng.choice(['false', 'false', 'true']),
powered=rng.choice(['false', 'true']),
in_wall=rng.choice(['false', 'false', 'true']))]]
if rng.random() < 0.5:
cells.append([0, -1, 0, states.get(rng.choice(['stone', 'dirt', 'oak_planks']))])
face = rng.choice([facing, OPPOSITE[facing], 'up', 'down'])
held = rng.choice(['-', '-', 'stone', 'oak_planks', 'stick', 'iron_ingot', 'oak_door', 'oak_fence_gate'])
case['use_item'] = {'item': '-' if held == '-' else d_plain(held, rng.choice([1, 16])),
'creative': rng.random() < 0.2, 'sneaking': rng.random() < 0.25, 'food': 20,
'yaw': rng.choice([0, 45, 90, 135, 180, -90, -135, -180, 270, 315, 30, 200]),
'face': face}
case['cells'] = cells
def dragon_egg_case(rng, states, case):
"""A dragon egg clicked with anything, or an empty hand (`DragonEggBlock.useWithoutItem`, which
takes the click before the item is placed): `teleport` draws 16/8/16-wide offsets up to 1000
times and moves the egg to the first air block inside the build height that has a block under it,
sending level event 2015 with `BlockUtil.packDifferenceInPosition` of the move. A floor gives the
search somewhere to land; in a bare sky only the cell above the egg has a block under it, and a
block in that cell leaves the 1000 draws with nowhere to go and the egg where it was."""
cells = [[0, 0, 0, states.get('dragon_egg')]]
if rng.random() < 0.8:
# A floor for the egg to land on: it only moves to air with a block under it.
cells.append([0, -1, 0, states.get(rng.choice(['stone', 'dirt', 'grass_block', 'netherrack', 'sand']))])
for dx in range(-2, 3):
for dz in range(-2, 3):
if (dx, dz) == (0, 0) or rng.random() < 0.15:
continue
cells.append([dx, -1, dz, states.get(rng.choice(['stone', 'dirt', 'oak_planks', 'glass',
'snow_block', 'gravel']))])
if rng.random() < 0.3:
# Something above it, whose own block below is air, so the search never takes it.
cells.append([0, 1, 0, states.get(rng.choice(['stone', 'oak_slab', 'glass']))])
item = rng.choice(['-', '-', 'stick', 'stone', 'torch', 'dragon_egg'])
case['use_item'] = {'item': '-' if item == '-' else d_plain(item, rng.choice([1, 16])),
'creative': rng.random() < 0.2, 'sneaking': rng.random() < 0.15, 'food': 20}
case['cells'] = cells
# A block placed from an item carrying `minecraft:block_state`: the block and the properties the
# component sets, none of them the ones the click would have chosen. The last two name a property the
# block has no such thing as, and no property at all, which `BlockItemStateProperties.apply` skips.
STATED = [('oak_stairs', '{half:"top",facing:"east",shape:"outer_right"}'),
('oak_slab', '{type:"top"}'),
('oak_log', '{axis:"x"}'),
('oak_trapdoor', '{half:"top",open:"true"}'),
('hopper', '{enabled:"false"}'),
('repeater', '{delay:"4",facing:"west",locked:"true"}'),
('comparator', '{mode:"subtract",powered:"true"}'),
('beehive', '{honey_level:"5"}'),
('chest', '{facing:"east"}'),
('composter', '{level:"6"}'),
('oak_fence_gate', '{in_wall:"true",open:"true"}'),
('oak_stairs', '{half:"sideways"}'),
('stone', '{facing:"east"}'),
('oak_stairs', '{}')]
def block_state_case(rng, states, case):
"""A block placed from an item that carries a `minecraft:block_state` component (Ctrl + Pick Block
of a turned stair, a half slab, a disabled hopper): `BlockItem.updateBlockStateFromTag` sets those
properties on the state `getStateForPlacement` chose, so the block ends up in the state the item
names. Ignoring the component leaves the state the click alone would have placed."""
case['cells'] = [[0, 0, 0, states.get(rng.choice(['stone', 'dirt', 'grass_block']))]]
block, props = rng.choice(STATED)
case['use_item'] = {'item': '{id:"minecraft:%s",count:1,components:{"minecraft:block_state":%s}}'
% (block, props),
'creative': rng.random() < 0.7, 'sneaking': rng.random() < 0.2, 'food': 20}
# Axes, hoes and shovels transform blocks through the data-driven
# `minecraft:block_transformer` registry. `test_block_transformer_parity.py`
# compares the states that registry produces; this compares the click that uses
# one: the state change, the sound, the scrub particle, the wear, the statistic,
# the `block_change` game event, the loot rooted dirt leaves on the clicked face,
# and the second game event and particle a double copper chest's other half gets.
AXES = ['wooden_axe', 'stone_axe', 'iron_axe', 'golden_axe', 'diamond_axe', 'netherite_axe']
HOES = ['wooden_hoe', 'stone_hoe', 'iron_hoe', 'golden_hoe', 'diamond_hoe', 'netherite_hoe']
SHOVELS = ['wooden_shovel', 'stone_shovel', 'iron_shovel', 'golden_shovel', 'diamond_shovel', 'netherite_shovel']
LOGS = ['oak_log', 'spruce_log', 'birch_log', 'jungle_log', 'acacia_log', 'dark_oak_log', 'mangrove_log',
'cherry_log', 'pale_oak_log', 'poplar_log', 'bamboo_block', 'crimson_stem', 'warped_stem',
'oak_wood', 'spruce_wood', 'birch_wood', 'crimson_hyphae', 'warped_hyphae']
COPPERS = ['exposed_copper', 'weathered_copper', 'oxidized_copper', 'exposed_cut_copper', 'weathered_cut_copper',
'oxidized_cut_copper', 'exposed_copper_bulb', 'weathered_copper_bulb', 'oxidized_copper_bulb',
'exposed_copper_grate', 'weathered_copper_grate', 'oxidized_copper_grate', 'exposed_copper_bars',
'weathered_copper_bars', 'oxidized_copper_bars', 'exposed_copper_chain', 'weathered_copper_chain',
'oxidized_copper_chain', 'exposed_lightning_rod', 'weathered_lightning_rod', 'oxidized_lightning_rod',
'waxed_copper_block', 'waxed_exposed_copper', 'waxed_weathered_copper', 'waxed_oxidized_copper',
'waxed_cut_copper', 'waxed_exposed_cut_copper', 'waxed_weathered_cut_copper', 'waxed_oxidized_cut_copper',
'waxed_copper_bulb', 'waxed_exposed_copper_bulb', 'waxed_weathered_copper_bulb',
'waxed_oxidized_copper_bulb', 'waxed_copper_grate', 'waxed_exposed_copper_grate',
'waxed_weathered_copper_grate', 'waxed_oxidized_copper_grate', 'waxed_copper_bars',
'waxed_exposed_copper_bars', 'waxed_weathered_copper_bars', 'waxed_oxidized_copper_bars',
'waxed_copper_chain', 'waxed_exposed_copper_chain', 'waxed_weathered_copper_chain',
'waxed_oxidized_copper_chain', 'waxed_lightning_rod', 'waxed_exposed_lightning_rod']
COPPER_CHESTS = ['exposed_copper_chest', 'weathered_copper_chest', 'oxidized_copper_chest',
'waxed_copper_chest', 'waxed_exposed_copper_chest', 'waxed_weathered_copper_chest',
'waxed_oxidized_copper_chest']
COPPER_DOORS = ['exposed_copper_door', 'weathered_copper_door', 'oxidized_copper_door',
'waxed_copper_door', 'waxed_exposed_copper_door', 'waxed_weathered_copper_door',
'waxed_oxidized_copper_door']
COPPER_TRAPDOORS = ['exposed_copper_trapdoor', 'weathered_copper_trapdoor', 'oxidized_copper_trapdoor',
'waxed_copper_trapdoor', 'waxed_exposed_copper_trapdoor',
'waxed_weathered_copper_trapdoor', 'waxed_oxidized_copper_trapdoor']
SOIL = {'grass_block': {'snowy': 'false'}, 'podzol': {'snowy': 'false'}, 'mycelium': {'snowy': 'false'},
'dirt': {}, 'coarse_dirt': {}, 'dirt_path': {}, 'rooted_dirt': {}}
FACINGS = {'north': (0, -1), 'south': (0, 1), 'west': (-1, 0), 'east': (1, 0)}
def damage_stack(item, damage):
if damage:
return '{id:"minecraft:%s",count:1,components:{"minecraft:damage":%d}}' % (item, damage)
return d_plain(item, 1)
def transformer_case(rng, states, case):
"""One click that uses a block transformer: an axe on a log, a copper block,
its door, trapdoor or chest, or a hoe or shovel on soil. The chest and door
clicks sneak, because their own use (opening) would otherwise take the click
(`suppressUsingBlock`)."""
kind = rng.choices(('log', 'copper', 'chest', 'door', 'trapdoor', 'soil'), (3, 4, 2, 1, 1, 3))[0]
creative = rng.random() < 0.15
damage = rng.choice([0, 0, 0, 1, 10])
face = rng.choice(['up', 'up', 'down', 'north', 'south', 'west', 'east'])
sneaking = rng.random() < 0.3
cells = [[0, -1, 0, states.get(rng.choice(['stone', 'dirt', 'oak_planks']))]]
items = []
if kind == 'log':
name = rng.choice(LOGS)
cells.insert(0, [0, 0, 0, states.get(name, axis=rng.choice(['x', 'y', 'z']))])
item = rng.choice(AXES) if rng.random() < 0.9 else rng.choice(['iron_sword', 'stick'])
elif kind == 'copper':
name = rng.choice(COPPERS)
cells.insert(0, [0, 0, 0, states.get(name)])
item = rng.choice(AXES) if rng.random() < 0.9 else 'iron_sword'
elif kind == 'chest':
name = rng.choice(COPPER_CHESTS)
facing = rng.choice(list(FACINGS))
double = rng.random() < 0.5
if double:
own = rng.choice(['left', 'right'])
other = 'right' if own == 'left' else 'left'
dx, dz = FACINGS[facing]
# ChestBlock.getConnectedDirection: LEFT is facing.getClockWise().
dx, dz = (-dz, dx) if own == 'left' else (dz, -dx)
cells.insert(0, [0, 0, 0, states.get(name, type=own, facing=facing, waterlogged='false')])
cells.insert(1, [dx, 0, dz, states.get(name, type=other, facing=facing, waterlogged='false')])
else:
cells.insert(0, [0, 0, 0, states.get(name, type='single', facing=facing, waterlogged='false')])
for slot in rng.sample(range(27), rng.choice([0, 1, 2])):
items.append([0, 0, 0, slot, d.plain(rng.choice(['stone', 'diamond', 'bread']), rng.choice([1, 3, 16]))])
item = rng.choice(['iron_axe', 'diamond_axe', 'netherite_axe'])
sneaking = True
elif kind == 'door':
name = rng.choice(COPPER_DOORS)
facing = rng.choice(list(FACINGS))
props = {'facing': facing, 'hinge': rng.choice(['left', 'right']),
'open': rng.choice(['false', 'true']), 'powered': rng.choice(['false', 'true'])}
cells.insert(0, [0, 0, 0, states.get(name, half='lower', **props)])
cells.insert(1, [0, 1, 0, states.get(name, half='upper', **props)])
item = rng.choice(['iron_axe', 'diamond_axe'])
sneaking = True
elif kind == 'trapdoor':
name = rng.choice(COPPER_TRAPDOORS)
cells.insert(0, [0, 0, 0, states.get(name, facing=rng.choice(list(FACINGS)),
half=rng.choice(['bottom', 'top']),
open=rng.choice(['false', 'true']),
powered=rng.choice(['false', 'true']),
waterlogged=rng.choice(['false', 'true']))])
item = rng.choice(['iron_axe', 'diamond_axe'])
sneaking = True
else:
hoe = rng.random() < 0.5
# Rooted dirt is the one transform with a loot table, so give it a
# third of the hoe cases.
names = list(SOIL) + (['rooted_dirt'] * 3 if hoe else [])
name = rng.choice(names)
cells.insert(0, [0, 0, 0, states.get(name, **SOIL[name])])
above = rng.choice(['air', 'air', 'air', 'stone', 'water'])
if above != 'air':
cells.append([0, 1, 0, states.get(above)])
item = rng.choice(HOES if hoe else SHOVELS) if rng.random() < 0.9 else 'iron_sword'
case['cells'] = cells
case['items'] = items
case['use_item'] = {'item': damage_stack(item, damage), 'creative': creative, 'sneaking': sneaking,
'food': 20, 'face': face}
def cases(facts, count, seed, transformers=True):
rng = random.Random(seed)
states = d.States(facts)
for n in range(count):
case = {**h.environment(rng), 'pos': [0, 0, 0]}
case.pop('raining', None)
case.pop('thundering', None)
if transformers and rng.random() < 0.08:
transformer_case(rng, states, case)
case['mode'] = 'dispense'
yield case
continue
if rng.random() < 0.08:
wire_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
continue
if rng.random() < 0.1:
bed_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
continue
if rng.random() < 0.1:
respawn_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
continue
if rng.random() < 0.1:
bed_respawn_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
continue
if rng.random() < 0.15:
anchor_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
continue
if rng.random() < 0.08:
timed_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
continue
if rng.random() < 0.1:
blocked_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
continue
if rng.random() < 0.08:
statue_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
continue
if rng.random() < 0.12:
pumpkin_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
continue
if rng.random() < 0.08:
crop_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
continue
if rng.random() < 0.08:
switch_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
continue
if rng.random() < 0.08:
jukebox_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
continue
if rng.random() < 0.1:
openable_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
continue
if rng.random() < 0.1:
dragon_egg_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
continue
if rng.random() < 0.06:
block_state_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
continue
cells = []
item = rng.choice(ITEMS)
if rng.random() < 0.15:
# A spawn egg on a fresh monster spawner (SpawnEggItem.useOn).
cells.append([0, 0, 0, states.get('spawner')])
if rng.random() < 0.3:
cells.append([0, 1, 0, states.get(rng.choice(['stone', 'water', 'short_grass']))])
item = rng.choice([f'{e}_spawn_egg' for e in EGGS] + ['apple', '-'])
if rng.random() < 0.15:
case['rules'] = {'spawner_blocks_work': False}
elif rng.random() < 0.3:
# A sweet berry bush on grass: pick it, bone meal it, or plant against it.
age = rng.randrange(4)
cells.append([0, 0, 0, states.get('sweet_berry_bush', age=str(age))])
cells.append([0, -1, 0, states.get(rng.choice(['grass_block', 'dirt', 'podzol', 'farmland']))])
item = rng.choice(['bone_meal', 'bone_meal', 'sweet_berries', 'apple', '-', 'stick'])
else:
ground = rng.choice(GROUNDS)
cells.append([0, 0, 0, states.any(rng, ground) if ground == 'farmland' else states.get(ground)])
above = rng.choice(ABOVE)
if above != 'air':
cells.append([0, 1, 0, states.get(above)])
if item == 'glow_berries' and rng.random() < 0.5:
# A ceiling two blocks up for the cave vines to hang from.
cells.append([0, 2, 0, states.get(rng.choice(['stone', 'dirt', 'oak_leaves']))])
if item == 'bone_meal' and ground in ('grass_block', 'moss_block', 'pale_moss_block'):
# Bone meal on spreading blocks is the bone meal harness's.
item = 'sweet_berries'
held = '-' if item == '-' else d.plain(item, rng.choice([1, 1, 2, 16, 64]))
case['use_item'] = {'item': held, 'creative': rng.random() < 0.2, 'sneaking': rng.random() < 0.2,
'food': rng.choice([20, 20, 19, 10, 0])}
if rng.random() < 0.3:
# The item in the off hand, something else or nothing in the main hand.
main = rng.choice(['-', '-', 'stick', 'apple', 'stone', 'bone_meal', 'shield'])
case['use_item'].update(offhand=held, item='-' if main == '-' else d.plain(main, rng.choice([1, 16])))
case['cells'] = cells
case['items'] = []
case['mode'] = 'dispense'
yield case
def wake_cases(facts, count, seed):
"""Villagers waking up, from a stream of their own so the other cases stay as they were."""
rng = random.Random(f'wake {seed}')
states = d.States(facts)
for _ in range(count):
case = {**h.environment(rng), 'pos': [0, 0, 0]}
case.pop('raining', None)
case.pop('thundering', None)
wake_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
# Items whose `Item.use` counts the used statistic (#9775): books, the spyglass, boats, a carrot or
# warped fungus on a stick off its mount, armor and other equippables swapped from the hand, and
# flint and steel or a fire charge on TNT (or lighting a fire). An empty map needs the server's
# saved data and a fishing rod a hook, which the stand-in level and the fixture leave out.
USED = ['writable_book', 'written_book', 'spyglass', 'oak_boat', 'birch_chest_boat', 'bamboo_raft',
'carrot_on_a_stick', 'warped_fungus_on_a_stick', 'iron_helmet', 'diamond_chestplate',
'leather_leggings', 'golden_boots', 'turtle_helmet', 'carved_pumpkin', 'flint_and_steel', 'fire_charge']
# What the player looks down on (three blocks south of the clicked block), for the boats: two
# blocks below its feet, so the new boat clears the player (vanilla's `noCollision` counts the
# player, which is not in the oracle's level).
FLOORS = ['stone', 'water', 'water', 'air', 'oak_planks']
def used_stat_case(rng, states, case):
"""An item whose `use` counts the used statistic, in either hand, over the clicked block (TNT for
flint and steel and fire charges, most of the time), with a floor under the player for boats."""
item = rng.choice(USED)
lights = item in ('flint_and_steel', 'fire_charge')
origin = 'tnt' if lights and rng.random() < 0.8 else rng.choice(['stone', 'dirt', 'oak_planks'])
cells = [[0, 0, 0, states.get(origin)]]
floor = rng.choice(FLOORS)
if floor != 'air':
cells.append([0, -2, 3, states.get(floor)])
case['cells'] = cells
# One of each: a second would go to the inventory, which the oracle's player lacks.
held = d.plain(item, 1)
case['use_item'] = {'item': held, 'creative': rng.random() < 0.2, 'sneaking': rng.random() < 0.15,
'food': 20, 'yaw': rng.choice([0.0, 90.0, 180.0, 270.0, 33.0])}
if rng.random() < 0.3:
main = rng.choice(['-', '-', 'stick', 'stone'])
case['use_item'].update(offhand=held, item='-' if main == '-' else d.plain(main, 1))
def used_stat_cases(facts, count, seed):
"""`used_stat_case`s, from a stream of their own so the other cases stay as they were."""
rng = random.Random(f'used {seed}')
states = d.States(facts)
for _ in range(count):
case = {**h.environment(rng), 'pos': [0, 0, 0]}
case.pop('raining', None)
case.pop('thundering', None)
used_stat_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
# Entities an item places, each with its ENTITY_PLACE game event and used statistic (#9776):
# paintings and item frames on a wall, armor stands, end crystals on obsidian or bedrock, minecarts
# on rails.
HANGING = ['painting', 'painting', 'item_frame', 'glow_item_frame']
RAILS = ['rail', 'powered_rail', 'detector_rail', 'activator_rail']
CARTS = ['minecart', 'chest_minecart', 'furnace_minecart', 'tnt_minecart', 'hopper_minecart']
def placement_case(rng, states, case):
"""An item that places an entity, clicked where it fits or does not."""
roll = rng.random()
if roll < 0.35:
# A wall facing the player (south), up to 4x4 so the larger paintings fit; the click on its
# south face, or another face.
width, height = rng.choice([(1, 1), (2, 2), (4, 3), (4, 4)])
wall = rng.choice(['stone', 'oak_planks', 'glass', 'stone'])
cells = [[x, y, 0, states.get(wall)] for x in range(-(width // 2), width - width // 2)
for y in range(height)]
item = rng.choice(HANGING)
# Something in front of the wall, for the frames only: a painting picks among the largest
# variants that fit with its own (unseeded) random, so each wall here fits one size alone.
if item != 'painting' and rng.random() < 0.3:
cells.append([0, 1 if height > 1 else 0, 1, states.get(rng.choice(['torch', 'stone', 'oak_slab']))])
case['use_item'] = {'item': d.plain(item, 1), 'face': rng.choice(['south', 'south', 'south', 'up', 'east'])}
elif roll < 0.6:
cells = [[0, 0, 0, states.get(rng.choice(['stone', 'grass_block', 'short_grass', 'oak_slab']))]]
if rng.random() < 0.3:
cells.append([0, rng.choice([1, 2]), 0, states.get(rng.choice(['stone', 'glass']))])
case['use_item'] = {'item': d.plain('armor_stand', 1)}
elif roll < 0.8:
cells = [[0, 0, 0, states.get(rng.choice(['obsidian', 'obsidian', 'bedrock', 'stone']))]]
if rng.random() < 0.25:
cells.append([0, 1, 0, states.get(rng.choice(['fire', 'stone']))])
case['use_item'] = {'item': d.plain('end_crystal', 1)}
else:
rail = rng.choice(RAILS)
cells = [[0, -1, 0, states.get('stone')], [0, 0, 0, states.any(rng, rail)]]
case['use_item'] = {'item': d.plain(rng.choice(CARTS), 1)}
case['cells'] = cells
case['use_item'].update(creative=rng.random() < 0.2, sneaking=rng.random() < 0.15, food=20,
yaw=rng.choice([0.0, 90.0, 180.0, 270.0, 33.0]))
if rng.random() < 0.25:
held = case['use_item']['item']
case['use_item'].update(offhand=held, item=rng.choice(['-', '-', d.plain('stick', 1)]))
def placement_cases(facts, count, seed):
"""`placement_case`s, from a stream of their own."""
rng = random.Random(f'placed {seed}')
states = d.States(facts)
for _ in range(count):
case = {**h.environment(rng), 'pos': [0, 0, 0]}
case.pop('raining', None)
case.pop('thundering', None)
placement_case(rng, states, case)
case['items'] = []
case['mode'] = 'dispense'
yield case
def forced_respawn_cases(facts):
"""Non-bed forced points: Block.isPossibleToRespawnInThis at feet and head.
Plants containing water are not liquid blocks; bubble columns are. Keep both
cases even in a small run so fluid-containing is not substituted for liquid.
"""
states = d.States(facts)
for name in ('air', 'water', 'lava', 'bubble_column', 'kelp', 'seagrass', 'stone'):
for height in (0, 1):
cells = [[0, 0, 0, states.get('air')], [0, 1, 0, states.get('air')]]
cells[height][3] = states.get(name)
yield {**h.environment(random.Random(10044)), 'pos': [0, 0, 0],
'cells': cells, 'items': [], 'mode': 'dispense',
'respawn_at': {'forced': True, 'consume': False, 'yaw': 37.0}}
def d_plain(item, count):
return d.plain(item, count)
def kind(case, facts):
first = facts.states[case['cells'][0][3]][0].split(':')[1]
if 'respawn_at' in case:
return f'respawn at {first}'
if 'wake' in case:
return f'villager waking at {first}'
item = case['use_item']['item']
item = item.split('"')[1].split(':')[1] if item != '-' else 'empty hand'
if 'entities' in case:
return f'{item} placed by ' + ', '.join(e['type'].split(':')[1] for e in case['entities'])
return f'{item} on {first}'
def main():
parser = h.argument_parser(__doc__)
parser.add_argument('--no-transformers', action='store_true',
help='leave out the axe, hoe and shovel transforms, as the corpus did before them')
args = parser.parse_args()
if args.count == 40000:
args.count = 4000
lock, jars, libraries = base.verify_artifacts()
facts = h.load_facts()
resources = lab.LOCAL / 'horizon-client' / lock['minecraft']['version']
work = lab.LOCAL / 'use-item-on-parity'
work.mkdir(parents=True, exist_ok=True)
all_cases = [*cases(facts, args.count, args.seed, not args.no_transformers),
*wake_cases(facts, max(args.count // 10, 1), args.seed),
*used_stat_cases(facts, max(args.count // 10, 1), args.seed),
*placement_cases(facts, max(args.count // 10, 1), args.seed), *forced_respawn_cases(facts)]
(work / 'cases.jsonl').write_text(''.join(json.dumps(case) + '\n' for case in all_cases))
classes = work / 'classes'
classes.mkdir(exist_ok=True)
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
java_root = lab.ROOT / 'mods/horizon/src/test/java'
d.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none', '-sourcepath', str(java_root), '-nowarn',
'-cp', dependencies, '-d', str(classes), str(java_root / 'dev/horizonlab/parity/VanillaDispenserOracle.java')])
d.run(['java', '-Xmx3G', '-cp', str(classes) + os.pathsep + dependencies, 'dev.horizonlab.parity.VanillaDispenserOracle',
str(resources), str(work / 'cases.jsonl'), str(work / 'native.txt')])
d.run(['cargo', 'build', '-q', '--profile', 'fast', '-p', 'horizon-client', '--bin', 'horizon-client'], cwd=lab.ROOT,
env={**os.environ, 'CARGO_BUILD_JOBS': os.environ.get('CARGO_BUILD_JOBS', '3')})
d.run([str(lab.cargo_target() / 'fast/horizon-client'), '--resources', str(resources)], cwd=work,
env=lab.game_env(HORIZON_DISPENSE_FIXTURE=f'{work / "cases.jsonl"},{work / "rust.txt"}'))
ignore = tuple(prefix + ' ' for prefix in args.ignore.split(',') if prefix) + ('D ',)
native = h.split_cases((work / 'native.txt').read_text(), ignore)
rust = h.split_cases((work / 'rust.txt').read_text(), ignore)
assert len(native) == len(rust) == len(all_cases), (len(native), len(rust), len(all_cases))
groups, errors = {}, {}
planted = eaten = respawned = exploded = moved = 0
for case, a, b in zip(all_cases, native, rust):
if a and a[0].startswith('error'):
errors.setdefault(a[0], []).append(case)
continue
planted += any(line.startswith('set 0,1,0') for line in a)
eaten += any(line.startswith('held') and not line.endswith('using -') for line in a)
respawned += any(line.startswith('respawn ') and line != 'respawn none' and 'respawn_at' in case for line in a)
exploded += any(line.endswith(' minecraft:explode') for line in a)
# Off the bed: not where the villager lay (y 0.6875).
moved += any(line.startswith('wake ') and line.split()[2] != '0.687500' for line in a)
if not d.same(a, b):
groups.setdefault(kind(case, facts), []).append((case, a, b))
for name, entries in sorted(errors.items(), key=lambda e: -len(e[1])):
print(f'oracle error x{len(entries)}: {name}')
for name, entries in sorted(groups.items(), key=lambda e: -len(e[1])):
print(f'{name}: {len(entries)} differing')
for case, a, b in entries[:args.show]:
print(' case ' + d.describe(case, facts) + ' use='
+ json.dumps(case.get('use_item', case.get('respawn_at', case.get('wake')))))
print(' native ' + ' | '.join(sorted(a)))
print(' rust ' + ' | '.join(sorted(b)))
total = sum(len(v) for v in groups.values())
skipped = sum(len(v) for v in errors.values())
print(f'{len(all_cases) - total - skipped}/{len(all_cases) - skipped} cases match ({skipped} oracle errors); '
f'vanilla planted in {planted}, started eating in {eaten}, exploded {exploded} times and found a respawn '
f'spot in {respawned}; villagers woke off the bed in {moved}')
h.finish(total + skipped)
if __name__ == '__main__':
main()