From 5150ff66bf1ade6f6f31fee5a36d916a353e35e0 Mon Sep 17 00:00:00 2001 From: voidsip Date: Wed, 4 Nov 2026 09:41:51 -0600 Subject: [PATCH] Match shulker teleport collision and attachment rules Co-Authored-By: Codex --- crates/horizon-client/src/mobs/shulker.rs | 168 ++++++++++--- .../src/mobs/shulker_space_tests.rs | 220 ++++++++++++++++++ .../parity/VanillaShulkerSpaceOracle.java | 51 ++++ tools/test_shulker_space_parity.py | 82 +++++++ 4 files changed, 484 insertions(+), 37 deletions(-) create mode 100644 crates/horizon-client/src/mobs/shulker_space_tests.rs create mode 100644 mods/horizon/src/test/java/dev/horizonlab/parity/VanillaShulkerSpaceOracle.java create mode 100644 tools/test_shulker_space_parity.py diff --git a/crates/horizon-client/src/mobs/shulker.rs b/crates/horizon-client/src/mobs/shulker.rs index b859f578f7..ad4f5e35fd 100644 --- a/crates/horizon-client/src/mobs/shulker.rs +++ b/crates/horizon-client/src/mobs/shulker.rs @@ -14,7 +14,7 @@ use super::{ use crate::projectiles::{Projectile, ProjectileWorld, STEPS}; use horizon_core::geometry::trig::Mth; use horizon_core::{ - geometry::{Aabb, Vec3}, + geometry::{Aabb, Direction, ShapeOp, Vec3, VoxelShape}, random::{LegacyRandom, RandomSource}, }; use std::f32::consts::PI; @@ -110,19 +110,121 @@ fn progress_aabb(direction: u8, from: f64, to: f64, pos: Vec3) -> Aabb { )) .move_by(pos) } -/// `Level.noCollision(shulker, aabb)`: no block collision under the empty -/// context, and none of the entities a shulker collides with overlaps -/// (`canBeCollidedWith`: living shulkers and adult happy ghasts; players -/// and other mobs never block it). -fn no_collision(m: &Mob, w: &MobWorld, bounds: Aabb) -> bool { - let mut scene = crate::collisions::SceneCollisions { - world: w.world, - binding: w.collision, - subject: None, - }; - if !crate::collisions::no_collision_empty(&mut scene, bounds) { - return false; +/// Block queries shared by attachment and teleport candidates. Entity overlap +/// is checked separately, using the nearby-entity index from MobWorld. +struct BlockSpace<'w, 'r> { + world: &'w crate::world::World, + registry: &'r horizon_world::registry::BlockRegistry, + facts: &'w crate::blocks::BlockFacts, + binding: &'w horizon_world::block_collision::BlockCollisionRegistry<'r>, + powder_walkable: bool, +} +impl<'w, 'r> BlockSpace<'w, 'r> { + fn context(m: &Mob) -> horizon_world::collision_context::CollisionContext<'_> { + use horizon_world::{ + collision_context::{CollisionContext, CollisionEntity, CollisionItem}, + entity_obstruction::EntityIdentity, + }; + CollisionContext::for_entity( + CollisionEntity { + identity: EntityIdentity(u64::from(m.id)), + // A teleport candidate is queried before setPos changes the entity. + y: m.position.y, + descending: false, + main_hand: Some(CollisionItem { + item: m.extra.main_hand.as_deref().unwrap_or("minecraft:air"), + count: m + .extra + .main_hand_stack() + .map_or(i32::from(m.extra.main_hand.is_some()), |s| { + s.count.min(i32::MAX as u32) as i32 + }), + }), + }, + false, + ) } + fn scene(&self, m: &Mob) -> crate::collisions::SceneCollisions<'_, 'r> { + crate::collisions::SceneCollisions { + world: self.world, + binding: self.binding, + subject: Some(crate::collisions::PowderSnowSubject { + fall_distance: m.fall_distance, + leather_boots: m.extra.feet.as_deref() == Some("minecraft:leather_boots") + && m.extra.armor[3].as_ref().is_none_or(|s| s.count > 0), + walkable_type: self.powder_walkable, + ..Default::default() + }), + } + } + fn no_collision(&self, m: &Mob, bounds: Aabb) -> bool { + use horizon_world::{ + entity_obstruction::EntityIdentity, + world_movement::{CollisionCheckRequest, no_world_collision}, + }; + no_world_collision( + &mut self.scene(m), + CollisionCheckRequest { + entity: EntityIdentity(u64::from(m.id)), + context: Self::context(m), + bounds, + candidate_budget: 1_000_000, + }, + ) + .unwrap_or(false) + } + fn can_stay_at(&self, m: &Mob, p: [i32; 3], face: u8) -> bool { + // isPositionBlocked: anything but air blocks, except a moving piston + // being driven through the shulker's own cell. + if !self.facts.is_air(self.world.block(p[0], p[1], p[2])) + && (p != block_pos(m) + || self + .registry + .state(self.world.block(p[0], p[1], p[2])) + .map_or("", |s| s.name.as_str()) + != "minecraft:moving_piston") + { + return false; + } + // loadedAndEntityCanStandOnFace uses the entity-context collision shape, + // not the block's generic support shape (notably different on soul sand). + let s = face_step(face); + let support_pos = + horizon_core::position::BlockPos::new(p[0] + s[0], p[1] + s[1], p[2] + s[2]); + let support = self + .world + .block(support_pos.x, support_pos.y, support_pos.z); + let open = opposite(face); + let Ok(shape) = self.binding.collision_shape_with_context( + &mut self.scene(m), + support, + support_pos, + &Self::context(m), + ) else { + return false; + }; + let full = shape + .face_shape(Direction::ALL[open as usize]) + .is_ok_and(|face| { + VoxelShape::block() + .join_is_not_empty(&face, ShapeOp::XOR) + .is_ok_and(|differs| !differs) + }); + full && self.no_collision(m, attachment_box(p, face)) + } +} +fn block_space<'w, 'r>(m: &Mob, w: &'w MobWorld<'r>) -> BlockSpace<'w, 'r> { + BlockSpace { + world: w.world, + registry: w.registry, + facts: w.facts, + binding: w.collision, + powder_walkable: w + .data + .entity_has("minecraft:powder_snow_walkable_mobs", &m.kind.id), + } +} +fn entities_clear(m: &Mob, w: &MobWorld, bounds: Aabb) -> bool { let grown = bounds.inflate(Vec3::new(1.0e-7, 1.0e-7, 1.0e-7)); !w.others_near(grown).any(|s| { s.id != m.id @@ -131,27 +233,12 @@ fn no_collision(m: &Mob, w: &MobWorld, bounds: Aabb) -> bool { && s.bounds.intersects(grown) }) } -/// `Shulker.canStayAt`: an empty spot against a sturdy face, with room -/// for the fully opened shell (`canStayAt`). -fn can_stay_at(m: &Mob, w: &MobWorld, p: [i32; 3], face: u8) -> bool { - // isPositionBlocked: anything but air blocks, except a moving piston - // being driven through the shulker's own cell. - if !w.facts.is_air(w.world.block(p[0], p[1], p[2])) - && (p != block_pos(m) || w.block_name(p[0], p[1], p[2]) != "minecraft:moving_piston") - { - return false; - } - // loadedAndEntityCanStandOnFace: the supporting block's face toward - // the shell is sturdy. - let s = face_step(face); - let support = w.world.block(p[0] + s[0], p[1] + s[1], p[2] + s[2]); - let open = opposite(face); - if w.facts.get(support).sturdy_full & (1 << open) == 0 { - return false; - } - // The fully opened shell's box, shrunk by the skin vanilla uses. - let opened = progress_aabb( - open, +fn no_collision(m: &Mob, w: &MobWorld, bounds: Aabb) -> bool { + block_space(m, w).no_collision(m, bounds) && entities_clear(m, w, bounds) +} +fn attachment_box(p: [i32; 3], face: u8) -> Aabb { + progress_aabb( + opposite(face), -1.0, 1.0, Vec3::new( @@ -160,8 +247,11 @@ fn can_stay_at(m: &Mob, w: &MobWorld, p: [i32; 3], face: u8) -> bool { f64::from(p[2]) + 0.5, ), ) - .deflate(Vec3::new(1.0e-6, 1.0e-6, 1.0e-6)); - no_collision(m, w, opened) + .deflate(Vec3::new(1.0e-6, 1.0e-6, 1.0e-6)) +} +/// Shulker.canStayAt, including its separately indexed collidable entities. +fn can_stay_at(m: &Mob, w: &MobWorld, p: [i32; 3], face: u8) -> bool { + block_space(m, w).can_stay_at(m, p, face) && entities_clear(m, w, attachment_box(p, face)) } /// `findAttachableSurface`: the first face (down, up, north, south, west, /// east) to cling to. @@ -615,6 +705,10 @@ pub fn anim(m: &Mob, s: &State, p: &mut Poses) { p.yrot("head", (s.y_rot - 180.0).to_radians()); } +#[cfg(test)] +#[path = "shulker_space_tests.rs"] +mod space_tests; + #[cfg(test)] mod tests { use super::*; @@ -622,7 +716,7 @@ mod tests { use std::sync::Arc; /// A shulker fixed to a block face. - fn shulker() -> Mob { + pub(super) fn shulker() -> Mob { let mut kind = kind(); let k = Arc::make_mut(&mut kind); k.id = "minecraft:shulker".into(); diff --git a/crates/horizon-client/src/mobs/shulker_space_tests.rs b/crates/horizon-client/src/mobs/shulker_space_tests.rs new file mode 100644 index 0000000000..b3bf8b7195 --- /dev/null +++ b/crates/horizon-client/src/mobs/shulker_space_tests.rs @@ -0,0 +1,220 @@ +use super::*; +use horizon_core::geometry::{Axis, VoxelShape}; +use horizon_world::{ + block_collision::BlockCollisionRegistry, + collision::CachedCollisionRegistry, + nbt::Tag, + registry::{BlockRegistry, BlockStateId}, +}; +use std::sync::Arc; + +fn text(value: &str) -> Tag { + Tag::String(value.into()) +} +fn shape_tag(shape: &VoxelShape) -> Tag { + let sizes = shape.sizes(); + let mut bits = Vec::new(); + for z in 0..sizes[2] { + for y in 0..sizes[1] { + for x in 0..sizes[0] { + bits.push(u8::from(shape.is_full(x as i32, y as i32, z as i32))); + } + } + } + let (min, max) = shape.discrete_bounds(); + let mut fields = vec![ + ("occupied", Tag::ByteArray(bits)), + ("cube", Tag::Byte(i8::from(shape.is_cube()))), + ( + "cube_axes", + Tag::Byte( + shape + .cube_coordinate_axes() + .iter() + .enumerate() + .fold(0, |mask, (i, on)| mask | (i8::from(*on) << i)), + ), + ), + ("discrete_min", Tag::IntArray(min.to_vec())), + ("discrete_max", Tag::IntArray(max.to_vec())), + ]; + for (axis, name) in [(Axis::X, "x"), (Axis::Y, "y"), (Axis::Z, "z")] { + fields.push(( + name, + Tag::LongArray( + shape + .coordinates(axis) + .iter() + .map(|v| v.to_bits() as i64) + .collect(), + ), + )); + } + Tag::compound(fields) +} + +fn fixture() -> (BlockRegistry, crate::blocks::BlockFacts, Tag) { + let names = ["air", "soul_sand", "stone", "scaffolding", "oak_slab"]; + let states: Vec<_> = names + .iter() + .map(|name| { + let mut fields = vec![("id", text(&format!("minecraft:{name}")))]; + if *name == "scaffolding" { + fields.push(( + "properties", + Tag::compound([ + ("distance", text("7")), + ("bottom", text("false")), + ("waterlogged", text("false")), + ]), + )); + } + Tag::compound(fields) + }) + .collect(); + let registry_tag = Tag::compound([ + ("DataVersion", Tag::Int(horizon_world::DATA_VERSION)), + ("states", Tag::List(states.clone())), + ( + "defaults", + Tag::Compound( + names + .iter() + .enumerate() + .map(|(i, name)| { + ( + format!("minecraft:{name}").as_str().into(), + Tag::Int(i as i32), + ) + }) + .collect(), + ), + ), + ]); + let registry = BlockRegistry::from_nbt(®istry_tag).unwrap(); + static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + let root = std::env::temp_dir().join(format!( + "horizon-shulker-space-{}-{}", + std::process::id(), + NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + )); + std::fs::create_dir(&root).unwrap(); + // Original minimal facts: air identity and generic sturdy flags, deliberately + // distinct from the soul sand's lower collision shape. + let facts = serde_json::json!({"states":(0..names.len()).map(|id|serde_json::json!({"id":id})).collect::>()}); + std::fs::write(root.join("gameplay-facts.json"), facts.to_string()).unwrap(); + let behaviours = serde_json::json!({"states":[[0,0],[63,0],[63,0],[0,0],[1,0]],"blocks":names.iter().map(|name|serde_json::json!({"name":format!("minecraft:{name}"),"class":["Block"]})).collect::>()}); + let facts = + crate::blocks::BlockFacts::load_with_behaviour_json(&root, ®istry, &behaviours).unwrap(); + std::fs::remove_dir_all(root).unwrap(); + let box_shape = + |height| VoxelShape::from_box(Aabb::new(Vec3::ZERO, Vec3::new(1.0, height, 1.0))).unwrap(); + let shapes = [ + VoxelShape::empty(), + box_shape(0.875), + VoxelShape::block(), + VoxelShape::block(), + box_shape(0.5), + ]; + let table = Tag::compound([ + ("DataVersion", Tag::Int(horizon_world::DATA_VERSION)), + ("states", Tag::List(states)), + ("shapes", Tag::List(shapes.iter().map(shape_tag).collect())), + ("cached_shapes", Tag::IntArray(vec![0, 1, 2, 3, 4])), + ]); + (registry, facts, table) +} + +fn check(current: Vec3, cells: &[([i32; 3], u32)], target: [i32; 3], face: u8) -> bool { + let (registry, facts, table) = fixture(); + let cached = CachedCollisionRegistry::from_nbt(&table, ®istry).unwrap(); + let binding = BlockCollisionRegistry::new(Arc::new(cached)).unwrap(); + let mut world = crate::world::World::new(BlockStateId(0), 0); + for x in -1..=1 { + for z in -1..=1 { + world + .chunks + .insert((x, z), Arc::new(crate::chunk::Chunk::empty(x, z))); + } + } + for &(p, state) in cells { + world.set(p[0], p[1], p[2], BlockStateId(state)).unwrap(); + } + let mut mob = super::tests::shulker(); + mob.position = current; + BlockSpace { + world: &world, + registry: ®istry, + facts: &facts, + binding: &binding, + powder_walkable: false, + } + .can_stay_at(&mob, target, face) +} + +#[test] +fn soul_sand_only_accepts_attachment_to_its_bottom() { + for (face, step) in STEPS.iter().enumerate() { + let support = [step[0], 64 + step[1], step[2]]; + assert_eq!( + check( + Vec3::new(8.5, 64.0, 8.5), + &[(support, 1)], + [0, 64, 0], + face as u8 + ), + face == 1, + "face {face}" + ); + } +} + +#[test] +fn scaffolding_uses_the_shulkers_current_height_before_and_after_teleport() { + let cells = [([0, 63, 0], 3), ([0, 65, 0], 4)]; + assert!(check(Vec3::new(0.5, 62.0, 0.5), &cells, [0, 64, 0], 1)); + assert!(!check(Vec3::new(0.5, 64.0, 0.5), &cells, [0, 64, 0], 1)); +} + +#[test] +#[ignore = "run by tools/test_shulker_space_parity.py"] +fn shulker_space_parity_fixture() { + use std::io::Write; + let input = + std::fs::read_to_string(std::env::var("HORIZON_SHULKER_SPACE_INPUT").unwrap()).unwrap(); + let mut output = + std::fs::File::create(std::env::var("HORIZON_SHULKER_SPACE_OUTPUT").unwrap()).unwrap(); + for line in input.lines() { + let row: serde_json::Value = serde_json::from_str(line).unwrap(); + let current = &row["current"]; + let current = Vec3::new( + current[0].as_f64().unwrap(), + current[1].as_f64().unwrap(), + current[2].as_f64().unwrap(), + ); + let target = std::array::from_fn(|i| row["target"][i].as_i64().unwrap() as i32); + let cells: Vec<_> = row["cells"] + .as_array() + .unwrap() + .iter() + .map(|cell| { + let pos = std::array::from_fn(|i| cell[i].as_i64().unwrap() as i32); + let state = match cell[3].as_str().unwrap() { + "minecraft:air" => 0, + "minecraft:soul_sand" => 1, + "minecraft:stone" => 2, + "minecraft:scaffolding" => 3, + "minecraft:oak_slab" => 4, + _ => panic!("unknown fixture block"), + }; + (pos, state) + }) + .collect(); + writeln!( + output, + "{}", + check(current, &cells, target, row["face"].as_u64().unwrap() as u8) + ) + .unwrap(); + } +} diff --git a/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaShulkerSpaceOracle.java b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaShulkerSpaceOracle.java new file mode 100644 index 0000000000..c3fe5d5590 --- /dev/null +++ b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaShulkerSpaceOracle.java @@ -0,0 +1,51 @@ +package dev.horizonlab.parity; + +import com.google.gson.JsonParser; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.EntityTypes; +import net.minecraft.world.entity.monster.Shulker; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; + +/** Original oracle: the native shulker attachment predicate on a recording level. */ +public final class VanillaShulkerSpaceOracle { + public static void main(String[] args) throws Exception { + ParityData.init(); + var allocator = Class.forName("sun.misc.Unsafe"); + var field = allocator.getDeclaredField("theUnsafe"); + field.setAccessible(true); + VanillaBlockUpdateOracle.unsafe = field.get(null); + VanillaBlockUpdateOracle.allocate = allocator.getMethod("allocateInstance", Class.class); + VanillaBlockUpdateOracle.AIR = Blocks.AIR.defaultBlockState(); + VanillaBlockUpdateOracle.OVERWORLD_TYPE = VanillaBlockUpdateOracle.loadOverworld(); + var stay = Shulker.class.getDeclaredMethod("canStayAt", BlockPos.class, Direction.class); + stay.setAccessible(true); + try (var out = new PrintWriter(Files.newBufferedWriter(Path.of(args[1]), StandardCharsets.UTF_8))) { + for (var line : Files.readAllLines(Path.of(args[0]))) { + var input = JsonParser.parseString(line).getAsJsonObject(); + // Resolve names through actual vanilla states; the shared world takes numeric ids. + var cells = input.getAsJsonArray("cells"); + for (var cell : cells) { + var row = cell.getAsJsonArray(); + var name = row.get(3).getAsString(); + var state = net.minecraft.core.registries.BuiltInRegistries.BLOCK.getValue( + net.minecraft.resources.Identifier.parse(name)).defaultBlockState(); + row.set(3, new com.google.gson.JsonPrimitive(Block.getId(state))); + } + var world = VanillaBlockUpdateOracle.build(input); + world.registries = ParityData.access; + var shulker = new Shulker(EntityTypes.SHULKER, world); + var current = input.getAsJsonArray("current"); + shulker.setPos(current.get(0).getAsDouble(), current.get(1).getAsDouble(), current.get(2).getAsDouble()); + var target = VanillaBlockUpdateOracle.readPos(input.getAsJsonArray("target")); + var direction = Direction.from3DDataValue(input.get("face").getAsInt()); + out.println(stay.invoke(shulker, target, direction)); + } + } + } +} diff --git a/tools/test_shulker_space_parity.py b/tools/test_shulker_space_parity.py new file mode 100644 index 0000000000..f8dba4ab2b --- /dev/null +++ b/tools/test_shulker_space_parity.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Compare native shulker attachment queries with Horizon's production block queries.""" +import os +from pathlib import Path +import json +import tempfile +try: + from . import lab, oracle_harness as harness, test_rust_parity as base, integration_env +except ImportError: + import lab, oracle_harness as harness, test_rust_parity as base, integration_env + + +def cases(): + steps = [(0,-1,0),(0,1,0),(0,0,-1),(0,0,1),(-1,0,0),(1,0,0)] + for x,z in [(0,0),(15,15),(-1,-1)]: + target=[x,64,z] + for name in ['air','soul_sand','stone','scaffolding','oak_slab']: + for face,(dx,dy,dz) in enumerate(steps): + yield json.dumps(dict(seed=42,cells=[[x+dx,64+dy,z+dz,'minecraft:'+name]], + current=[8.5,64,8.5],target=target,face=face)) + for y in range(60,69): + yield json.dumps(dict(seed=42,cells=[[x,63,z,'minecraft:scaffolding'],[x,65,z,'minecraft:oak_slab']], + current=[x+.5,y,z+.5],target=target,face=1)) + for face,(dx,dy,dz) in enumerate(steps): + yield json.dumps(dict(seed=42,cells=[[x+dx,64+dy,z+dz,'minecraft:stone'],[x,64,z,'minecraft:stone']], + current=[8.5,64,8.5],target=target,face=face)) + + +def main(): + lock, directory = lab.config() + manifest, client = lab.import_client(lock, directory) + natives = {v['downloads']['artifact']['path'] for v in manifest['libraries'] + if v['name'].split(':')[-1].startswith('natives-')} + libraries = [lab.import_library(a, directory / 'libraries/client') + for a in lab.client_library_artifacts(manifest) if a['path'] not in natives] + build = lab.LOCAL / 'rust-shulker-space-parity' + build.mkdir(parents=True, exist_ok=True) + report = build / 'report.json' + report.unlink(missing_ok=True) + sources = [Path(__file__), lab.ROOT / 'tools/lab.py', lab.ROOT / 'tools/oracle_harness.py', + lab.ROOT / 'tools/test_rust_parity.py', lab.ROOT / 'tools/integration_env.py', + lab.ROOT / 'Cargo.toml', lab.ROOT / 'Cargo.lock', + lab.ROOT / 'crates/horizon-client/src/mobs/shulker.rs', + lab.ROOT / 'crates/horizon-client/src/mobs/shulker_space_tests.rs', + lab.ROOT / 'crates/horizon-client-render/src/collisions.rs', + lab.ROOT / 'crates/horizon-world/src/block_collision.rs', + lab.ROOT / 'crates/horizon-world/src/collision_context.rs', + lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaShulkerSpaceOracle.java', + lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaBlockUpdateOracle.java', + lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/ParityData.java'] + hashes = base.source_hashes(sources) + rows = list(cases()) + inputs = build / 'inputs.txt' + inputs.write_text('\n'.join(rows) + '\n', encoding='utf-8') + input_hash = lab.digest(inputs, 'sha256') + classes, dependencies, _ = harness.compile_oracle(build, 'VanillaShulkerSpaceOracle', + {'client': client}, libraries, lock['minecraft']['java_major']) + harness.java_run(build, classes, dependencies, 'VanillaShulkerSpaceOracle', + [inputs, build / 'client.txt'], 'client') + with tempfile.TemporaryDirectory(prefix='horizon-shulker-space-parity-') as scratch: + env = integration_env.game_environment(os.environ, scratch) + env.update(HORIZON_SHULKER_SPACE_INPUT=str(inputs), HORIZON_SHULKER_SPACE_OUTPUT=str(build / 'rust.txt')) + harness.run_logged(['cargo', 'test', '--locked', '-p', 'horizon-client-lib', '--lib', + 'mobs::shulker::space_tests::shulker_space_parity_fixture', + '--', '--exact', '--ignored'], build, 'rust', env=env) + matched = harness.compare(build / 'client.txt', build / 'rust.txt', len(rows)) + base.ensure_sources_unchanged(sources, hashes) + if lab.digest(inputs, 'sha256') != input_hash: + raise AssertionError('Input fixtures changed') + lab.write_json(report, dict(schema=1, minecraft=lock['minecraft']['version'], sources_sha256=hashes, + client_jar_sha256=lab.digest(client, 'sha256'), input_sha256=input_hash, + matched_output_lines=matched, output_sha256=lab.digest(build / 'rust.txt', 'sha256'), + scope='Actual native Shulker.canStayAt, loadedAndEntityCanStandOnFace and noCollision on a recording level, ' + 'compared with the production Horizon block query used by attachment and teleport. ' + 'Five support blocks,all six faces,blocked targets,nine current heights and three chunk-boundary positions. ' + 'Rust uses original minimal facts/collision fixtures; normal scale and empty equipment. ' + 'No nearby entities,world border,random teleport destination selection,AI ticks,networking or rendered interpolation comparison.')) + print(f'{matched} shulker attachment cases match the pinned client: {report}', flush=True) + + +if __name__ == '__main__': + main()