mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
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:
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();
|
||||
|
||||
@@ -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__':
|
||||
|
||||
Reference in new issue
Block a user