test: a floor, a fluid and a whole sample box for the animateTick zoo

The tick cases' stand-in world stopped at y 32..96 and x/z -32..31, one block
short of every sample the loop takes and of the neighbours those samples read,
so a block at the edge compared air against the oracle's unending world: that
is where the beehive, the redstone ore and the pointed dripstone differences
came from. Fill the whole box plus a block either way, let a case name the
floor below y = 64, hand the loop the state's own fluid (a waterlogged block
has a water source, which it ticks and drips) and bind the client jar's block
tags in the oracle the way a datapack load does, which a bare bootstrap leaves
empty. Then add the families that read a neighbour, a shape or their own fluid:
dried ghast over soul sand, soul soil, stone and air; hives and a nest embedded
and hanging; a lit redstone ore and a deepslate one embedded and over air; and
stalactite tips over stone and water. 2,097 cases and 456,747 output lines
become 2,187 and 462,718.
This commit is contained in:
MSedgeMC committed 2026-11-05 14:00:10 +00:00
1 parent 9d11ae8abb
commit 677e00c72f
3 files changed
+158 -17

No files matched your search

@@ -99,30 +99,40 @@ pub fn main() -> Result<()> {
let case: Value = serde_json::from_str(&line?)?;
let kind = case["kind"].as_str().unwrap_or("marker");
// A block's own tick through the whole ClientLevel.animateTick loop,
// over a world that is all of it: every particle and sound it asks the
// engine for, from the level random seeded by the case.
// over a world that is all of it above y = 64 and all of the case's
// floor below: every particle and sound it asks the engine for, from the
// level random seeded by the case.
if kind == "tick" {
let spec = case["state"].as_str().ok_or("state")?.to_owned();
if !tick_worlds.contains_key(&spec) {
let floor_spec = case["floor"].as_str().unwrap_or(&spec).to_owned();
let key = format!("{spec}|{floor_spec}");
if !tick_worlds.contains_key(&key) {
let state = assets.registry.resolve(&parse_state(&spec))?;
let floor = assets.registry.resolve(&parse_state(&floor_spec))?;
let mut world = World::new(facts.air, 0);
for cz in -2..2 {
for cx in -2..2 {
// The loop samples y 32 to 96, reads neighbours one block out
// and a stalactite climbs ten blocks to its root, so the
// stand-in world has to hold all of that: a case that ends
// inside the sample box compares air against the oracle's
// unending world.
for cz in -3..3 {
for cx in -3..3 {
world
.chunks
.insert((cx, cz), Arc::new(Chunk::empty(cx, cz)));
for y in 33..96 {
for y in 31..=107 {
for z in 0..16 {
for x in 0..16 {
world.set(cx * 16 + x, y, cz * 16 + z, state)?;
let at = if y < 64 { floor } else { state };
world.set(cx * 16 + x, y, cz * 16 + z, at)?;
}
}
}
}
}
tick_worlds.insert(spec.clone(), world);
tick_worlds.insert(key.clone(), world);
}
let world = &tick_worlds[&spec];
let world = &tick_worlds[&key];
// A block whose model is jittered per column: the offset the client
// gives its model, so the oracle's shape moves exactly as the game
// moves it.
@@ -97,14 +97,22 @@ public final class VanillaMarkerParticlesOracle {
* particle and local sound it asks for is recorded. */
public static final class TickLevel extends ClientLevel {
BlockState state = Blocks.AIR.defaultBlockState();
/** Every block below y = 64, so a case can put something else under its block. */
BlockState floor = Blocks.AIR.defaultBlockState();
java.util.List<String> particles;
java.util.List<String> sounds;
net.minecraft.world.attribute.EnvironmentAttributeSystem attributes;
TickLevel() { super(null, null, null, null, 0, 0, null, false, 0L, 0); }
@Override public BlockState getBlockState(BlockPos pos) { return state; }
@Override public FluidState getFluidState(BlockPos pos) { return Fluids.EMPTY.defaultFluidState(); }
@Override public BlockState getBlockState(BlockPos pos) { return pos.getY() < 64 ? floor : state; }
/** The state's own fluid, as the probe's world reports it: a waterlogged
* block hands the loop a water source, which it then ticks and drips. */
@Override public FluidState getFluidState(BlockPos pos) { return getBlockState(pos).getFluidState(); }
/** Dry weather, as the probe's stand-in world reports it. */
@Override public boolean isRainingAt(BlockPos pos) { return false; }
/** The overworld's build height, which a stalactite's root search asks for
* and a level built without a dimension type cannot answer. */
@Override public int getMinY() { return -64; }
@Override public int getHeight() { return 384; }
@Override public net.minecraft.world.attribute.EnvironmentAttributeSystem environmentAttributes() {
return attributes;
}
@@ -123,6 +131,70 @@ public final class VanillaMarkerParticlesOracle {
+ " " + Integer.toHexString(Float.floatToIntBits(pitch)));
}
}
/** The pinned client's block tags, bound the way a datapack load binds them:
* a bootstrap-only run leaves every tag empty, and the ticks under test ask
* about `speleothems`, `impermeable` and the two ambient-sound tags. */
@SuppressWarnings("unchecked")
static void bindBlockTags() throws Exception {
var source = Block.class.getProtectionDomain().getCodeSource().getLocation();
var raw = new java.util.HashMap<Identifier, java.util.List<String>>();
try (var file = new java.util.zip.ZipFile(java.nio.file.Path.of(source.toURI()).toFile())) {
var entries = file.entries();
while (entries.hasMoreElements()) {
var entry = entries.nextElement();
var path = entry.getName();
var inner = path.indexOf("/tags/block/");
if (!path.startsWith("data/") || inner < 0 || !path.endsWith(".json")) continue;
var namespace = path.substring("data/".length(), inner);
var name = path.substring(inner + "/tags/block/".length(), path.length() - ".json".length());
var json = JsonParser.parseString(new String(file.getInputStream(entry).readAllBytes(),
java.nio.charset.StandardCharsets.UTF_8)).getAsJsonObject();
var values = new java.util.ArrayList<String>();
for (var value : json.getAsJsonArray("values")) {
if (value.isJsonPrimitive()) values.add(value.getAsString());
else if (value.isJsonObject() && value.getAsJsonObject().has("id"))
values.add(value.getAsJsonObject().get("id").getAsString());
}
raw.put(Identifier.fromNamespaceAndPath(namespace, name), values);
}
}
var registry = (net.minecraft.core.WritableRegistry<Block>) BuiltInRegistries.BLOCK;
var bound = new java.util.HashMap<net.minecraft.tags.TagKey<Block>,
java.util.List<net.minecraft.core.Holder<Block>>>();
for (var entry : raw.entrySet()) {
var holders = new java.util.ArrayList<net.minecraft.core.Holder<Block>>();
collectTaggedBlocks(raw, registry, entry.getValue(), holders, new java.util.HashSet<>());
bound.put(net.minecraft.tags.TagKey.create(
net.minecraft.core.registries.Registries.BLOCK, entry.getKey()), holders);
}
// A frozen registry takes its tags through the reload a datapack load
// uses: prepare the pending set and apply it, which binds the tag
// contents and refreshes every holder's tag set.
var pending = registry.prepareTagReload(
new net.minecraft.tags.TagLoader.LoadResult<Block>(
net.minecraft.core.registries.Registries.BLOCK, bound));
pending.apply();
}
static void collectTaggedBlocks(java.util.Map<Identifier, java.util.List<String>> raw,
net.minecraft.core.WritableRegistry<Block> registry,
java.util.List<String> values,
java.util.List<net.minecraft.core.Holder<Block>> out,
java.util.Set<Identifier> seen) {
for (var value : values) {
if (value.startsWith("#")) {
var tag = Identifier.parse(value.substring(1));
if (seen.add(tag) && raw.containsKey(tag)) {
collectTaggedBlocks(raw, registry, raw.get(tag), out, seen);
}
continue;
}
// A block the pinned jar does not have is left out, as a datapack
// load drops an optional entry it cannot resolve.
registry.get(Identifier.parse(value))
.ifPresent(block -> out.add((net.minecraft.core.Holder<Block>) block));
}
}
/** A `minecraft:name[k=v,...]` state through the real block state definition. */
@SuppressWarnings({"unchecked", "rawtypes"})
static BlockState parseState(String spec) {
@@ -145,6 +217,7 @@ public final class VanillaMarkerParticlesOracle {
for (var item : BuiltInRegistries.ITEM) {
item.builtInRegistryHolder().bindComponents(net.minecraft.core.component.DataComponentMap.EMPTY);
}
bindBlockTags();
var allocator = Class.forName("sun.misc.Unsafe");
var unsafe = field(allocator, "theUnsafe").get(null);
var allocate = allocator.getMethod("allocateInstance", Class.class);
@@ -179,6 +252,8 @@ public final class VanillaMarkerParticlesOracle {
var random = RandomSource.create(c.get("seed").getAsLong());
field(Level.class, "random").set(tickLevel, random);
tickLevel.state = parseState(c.get("state").getAsString());
tickLevel.floor = c.has("floor")
? parseState(c.get("floor").getAsString()) : tickLevel.state;
tickLevel.particles.clear();
tickLevel.sounds.clear();
var pos = new BlockPos.MutableBlockPos();
+62 -6
View File
@@ -165,12 +165,57 @@ def wither_rose_cases(rng):
seed=rng.randrange(-2 ** 63, 2 ** 63), offset='xz')
def neighbour_cases(rng):
"""The families whose own tick reads a neighbour, a shape or the fluid of the
block itself, over a world of that block above y = 64 and a floor below it:
a dried ghast's ambient sound wants a tagged block under it, a hive drips
only past the face below, a lit redstone ore skips its solid-render
neighbours and a stalactite tip drips the fluid above its root."""
dried_ghast = 'minecraft:dried_ghast[facing=north,hydration=0,waterlogged=%s]'
hive = 'minecraft:beehive[facing=north,honey_level=%d]'
nest = 'minecraft:bee_nest[facing=south,honey_level=%d]'
ore = 'minecraft:%sredstone_ore[lit=true]'
tip = 'minecraft:pointed_dripstone[thickness=%s,vertical_direction=down,waterlogged=%s]'
floors = ['minecraft:soul_sand', 'minecraft:soul_soil', 'minecraft:stone', 'minecraft:air']
for waterlogged, floor in itertools.product(('false', 'true'), floors):
for seed in SEEDS:
yield dict(kind='tick', state=dried_ghast % waterlogged, floor=floor, seed=seed)
for block, level in [(hive, 5), (hive, 4), (nest, 5)]:
for floor in [None, 'minecraft:air']:
for seed in SEEDS:
case = dict(kind='tick', state=block % level, seed=seed)
if floor is not None:
case['floor'] = floor
yield case
for name, floor in itertools.product(('', 'deepslate_'), [None, 'minecraft:air']):
for seed in SEEDS:
case = dict(kind='tick', state=ore % name, seed=seed)
if floor is not None:
case['floor'] = floor
yield case
for seed in SEEDS:
yield dict(kind='tick', state='minecraft:redstone_ore[lit=false]', seed=seed)
for thickness, floor in itertools.product(('tip', 'middle'), [None, 'minecraft:stone', 'minecraft:water[level=0]']):
for seed in SEEDS:
case = dict(kind='tick', state=tip % (thickness, 'false'), seed=seed, offset='xz')
if floor is not None:
case['floor'] = floor
yield case
for seed in SEEDS:
yield dict(kind='tick', state=tip % ('tip', 'true'), seed=seed, offset='xz')
for _ in range(12):
yield dict(kind='tick', state=rng.choice([hive % 5, ore % '']),
seed=rng.randrange(-2 ** 63, 2 ** 63),
floor=rng.choice(['minecraft:air', 'minecraft:soul_sand', 'minecraft:stone']))
def make_input(build):
rng = random.Random(0x43414d50)
cases = (marker_cases() + list(campfire_cases(rng)) + list(puff_cases(rng))
+ list(tick_cases(random.Random(0x414e494d)))
+ list(zoo_cases(random.Random(0x5a4f4f)))
+ list(wither_rose_cases(random.Random(0x524f5345))))
+ list(wither_rose_cases(random.Random(0x524f5345)))
+ list(neighbour_cases(random.Random(0x4e454947))))
path = build / 'cases.jsonl'
path.write_text(''.join(json.dumps(case) + '\n' for case in cases))
return path, len(cases)
@@ -193,6 +238,13 @@ def main():
'blinking sculk sensor and a charged respawn anchor, each at two seeds and forty of them at a random one, '
'and the wither rose, whose three smoke puffs sit about the centre of the pick shape the flower\'s '
'model offset moves with the column, at three seeds and twenty-four more at random ones, '
'and the families whose own tick reads a neighbour, a shape or the fluid of the block itself, '
'each over a world of that block above y = 64 and a named floor below it: a dried ghast, '
'waterlogged and not, over soul sand, soul soil, stone and air; a hive full of honey and one '
'short of it, and a bee nest, embedded in themselves and hanging over air; a lit redstone ore '
'and a deepslate one embedded and over air, and an unlit one; and stalactite tips and a middle '
'segment embedded, over stone and over water, a waterlogged tip, and a column of water over '
'hanging tips; each at two seeds and twelve more at random seeds and floors; '
'and the campfire block entity: the real '
'CampfireBlockEntity.particleTick for both campfire blocks, lit and unlit, signal '
'fire on and off, all four facings and every one of the four cooking slots held or empty, ticked six times '
@@ -207,12 +259,16 @@ def main():
'particles past their creation are outside this comparison. Families whose own tick needs something the '
'all-one-state world cannot give it are not here: a falling block wants air below it, a drip wants a face '
'that is not solid, the firefly bush wants a light level, the end gateway counts its visible faces and the '
'lightning rod the level height, the pointed dripstone and the potent sulfur want water above, the bubble '
'column and the fluid drips want a fluid, the dried ghast, the redstone ore and the beehive read a shape or a '
'neighbour that embedding takes away, and the enchanting table needs air between itself and '
'its power providers.',
'lightning rod the level height, the potent sulfur wants water above, the bubble column and the '
'fluid drips want a fluid, and the enchanting table needs air between itself and its power '
'providers. The stand-in world holds every sampled position and one block of neighbour either '
'way, its blocks below y = 64 are the case\'s floor and its fluid state is the state\'s own, so a '
'waterlogged block hands the loop the water source it has in the game; and the block tags these '
'ticks ask about (the dried ghast\'s ambient-sound tag, the hives\' impermeable tag, the speleothem '
'tag the stalactite branch needs) are bound from the pinned client jar the way a datapack load '
'binds them, where a bare bootstrap leaves every tag empty.',
sources=[lab.ROOT / 'tools/test_marker_particles_parity.py',
lab.ROOT / 'crates/horizon-client/src/animate_tick.rs'], expected_lines=456747)
lab.ROOT / 'crates/horizon-client/src/animate_tick.rs'], expected_lines=462718)
if __name__ == '__main__':