From 7e693725179870ea79e006ed55365525bde80d9c Mon Sep 17 00:00:00 2001 From: Ludovic Marie <136255825+ludovic111@users.noreply.github.com> Date: Wed, 7 Oct 2026 01:52:30 +0200 Subject: [PATCH] Match entity move lifecycle ordering and retained movement records --- .../scoped-parity.json | 220 +++++++++ .../examples/entity_move_fixture.rs | 418 ++++++++++++++++++ crates/horizon-world/src/entity_move.rs | 217 +++++++++ crates/horizon-world/src/lib.rs | 1 + docs/collision-geometry.md | 18 + .../parity/VanillaEntityMoveOracle.java | 92 ++++ tools/test_entity_move_parity.py | 72 +++ 7 files changed, 1038 insertions(+) create mode 100644 benchmarks/rust-entity-move-2026-10-07/scoped-parity.json create mode 100644 crates/horizon-world/examples/entity_move_fixture.rs create mode 100644 crates/horizon-world/src/entity_move.rs create mode 100644 mods/horizon/src/test/java/dev/horizonlab/parity/VanillaEntityMoveOracle.java create mode 100644 tools/test_entity_move_parity.py diff --git a/benchmarks/rust-entity-move-2026-10-07/scoped-parity.json b/benchmarks/rust-entity-move-2026-10-07/scoped-parity.json new file mode 100644 index 0000000000..fcef52f247 --- /dev/null +++ b/benchmarks/rust-entity-move-2026-10-07/scoped-parity.json @@ -0,0 +1,220 @@ +{ + "schema": 1, + "minecraft": "26.3", + "manifest_sha1": "4fe1aa1ef8da1cb95c5bad1fb98890ca56dd8ca3", + "platform": "Linux-7.0.0-31-generic-x86_64-with-glibc2.43", + "java": "openjdk 25.0.4.1 2026-08-18", + "rustc": "rustc 1.99.0 (b940084d7 2026-09-28)", + "fixtures": 21656, + "sources_sha256": { + "mods/horizon/src/test/java/dev/horizonlab/parity/VanillaEntityMoveOracle.java": "8e7abd89ab8fe5c64d5608f1e9218127c546a1cfcbe08d784492c9b021cd8e70", + "tools/test_entity_move_parity.py": "3fc4497b90559a59ec8d74f0ebb413585499516dd698d534c2e7db0ad6109cb8", + "tools/test_rust_parity.py": "29d3db08005873528894b45ca10dcd6278753650473b3eb574e53e06ede40ec6", + "tools/lab.py": "d00bf107fa01b6c579734a082c3ca4ade3018f0d6851d46844acf17574f66e64", + "Cargo.toml": "3f93319c1eb70dbd7ba25cd74ef343fbf35c52c7c4752f955605c58699cbff9d", + "Cargo.lock": "cb1e87d449114a63edd904c0559d631e8fa9feeb27efd6539aacdcfeae178876", + "crates/horizon-core/Cargo.toml": "e21b502c949da93cf67826592459b687a8cbf737ef92fee514e3ed2cc7cd27ad", + "crates/horizon-world/Cargo.toml": "977338bf3e654193685b015d383493b9f59ff6f5d6210bec212cef8ac4a50bed", + "crates/horizon-core/src/bin/density_parity.rs": "e5b1dd33b7d01306c73cf072629b4fb186e9ab87b672a00108999d1a72263fd0", + "crates/horizon-core/src/bin/float_log_parity.rs": "4a655fcf8822ee7668c4bd126bc84d0b118864a6fc7f5a6aeee667d8bf2590b1", + "crates/horizon-core/src/bin/geometry_parity.rs": "27f6c09fe5018ac3de8e70e0f0bbf78f9abb14f5e4783bcaf46e8ca87b9c6f8a", + "crates/horizon-core/src/bin/noise_parity.rs": "c135bfc708d853e7061e81487d2cfe10de64b2f65677d3b4837cbfe0b8341010", + "crates/horizon-core/src/bin/parity.rs": "f35d928c66f958de39fac5e7e836a657eed95f83599e84899f7ea7a5802ba481", + "crates/horizon-core/src/bin/tick_parity.rs": "a54c164fe4814034d369947a887f5273966f5a878ba849204b80d1c729526a8f", + "crates/horizon-core/src/biome/seed_hash.rs": "5afbb4921c750826b395a08bf06f382fc7f01b5759a93de02a2749c523e2a317", + "crates/horizon-core/src/biome.rs": "908efa8cd4aa0a7adf1cb5e1a222e3dfb640eed5e9172e0fe7f0d1287afae7e8", + "crates/horizon-core/src/carving/ellipsoid.rs": "09232a7588610e8a9176fd9c62d40afb1fb89f2ecab6d555536a1d847b142d86", + "crates/horizon-core/src/carving.rs": "0aa3093caa0437868a8c0ed52a6c6e1993bd51c03583c670a2837e749eae5426", + "crates/horizon-core/src/density/arithmetic.rs": "372faec96c923e2cf4aa6c66c1ac398e676853274557823850dbc8933dd15ab1", + "crates/horizon-core/src/density/beardifier.rs": "bb1052e27dae4a01954ca78d7d5babafef85f3b2c704ccaf1920cb85a098cb50", + "crates/horizon-core/src/density/cache.rs": "38d0b6c9dd04ffef979fdad3f29cfebf98d331c8c9d430be5f39aa34f6c30975", + "crates/horizon-core/src/density/end_islands.rs": "168b9384237bd2e41d0ae5a74e944da4dc74e493bdec31fb73184c9a49e3de55", + "crates/horizon-core/src/density/graph/compiler.rs": "ac67deeb16f16d0859aa20e5a165811a29aca0b910037ee6bf5098211f3a6ede", + "crates/horizon-core/src/density/graph/mod.rs": "b776e7da934cb03051a3053d56d461c71e9978e084922fe4194147aa580c07bf", + "crates/horizon-core/src/density/graph/seeded.rs": "6bcdcaa3dffcfa9df673bbbea35120057a16ba3d2b506c52709c36cf8976b74a", + "crates/horizon-core/src/density/graph/tests.rs": "6bfb8cbff462587dd147b0775ea01d370abbd97544b9bd596c792df1b5f7e634", + "crates/horizon-core/src/density/interpolation.rs": "8c890f52b6467f38a94c654986a6a484b8e0b296ab98c398e60a6ce465b5b010", + "crates/horizon-core/src/density/interval.rs": "b42629163baa6b599a233afe8596d1c370cd81d015cc6fc36a9663aa47b98424", + "crates/horizon-core/src/density/mod.rs": "ca0d77d3282e3746ddcbc92bee97dbf640b9a56214e6ac1537f51f9a5b63d6e7", + "crates/horizon-core/src/density/power.rs": "d5b7270a128d4264268cd772109ad16d71a8ec3ff2d3b63b84cb985296d54a91", + "crates/horizon-core/src/density/rounding.rs": "71840c5206ff8d218ac29bf6f66915d5212764ba270c675711a866ba5b55a7a6", + "crates/horizon-core/src/density/selection.rs": "0dd26af2e85004ec5e053039ad11c384c17c56ace9ab45eb3f5cb3503c0b0330", + "crates/horizon-core/src/density/slice.rs": "5d77605573c0deda47778f1f78f7fa3fbf1944d8f5efbcc65cf589181cadf80b", + "crates/horizon-core/src/density/spline.rs": "6bb2e40f940fc6544d6fb09cb0b8cac722be03bc663dbdfb0f61faae207400bc", + "crates/horizon-core/src/density/surface.rs": "159e0e69cda74f870e80ea64fe523c4397e64af4ccb861bb9fd07b2c3cb91b00", + "crates/horizon-core/src/density/terrain_noise.rs": "8d0059d25eb5d248cb97a4c5a2e670d61528928ea151495c004f25adb723fe3e", + "crates/horizon-core/src/density/tests.rs": "7817796ee40d0ae241bfbebec6bca190e0a8e7d5a1caa8ddf858c3eaa58b8858", + "crates/horizon-core/src/density/world_fields.rs": "c1ce305cb8046b384d90c564cafb24af7365d580389637910f3d8b0cc18eb583", + "crates/horizon-core/src/geometry/border.rs": "1386f8080521231d05ba3e84d94a3e1d5218d35ccb3d5e9fe2bbb05a8739e573", + "crates/horizon-core/src/geometry/mod.rs": "c650386f9860e44a96c55f18c8f1428ca715fcb0bc5c2434db5b1caa43df27fe", + "crates/horizon-core/src/geometry/movement.rs": "bcfb452752940ce53ae6dc267a3c4eb29459046a063c1f884684ab4f4bd0068f", + "crates/horizon-core/src/geometry/shape/create.rs": "189812b91d5e0e3ad963afcd1a6e2831b31b8188df91a904ef27061e644806ad", + "crates/horizon-core/src/geometry/shape/face.rs": "f243bce81384040a95ce7984b02b5699206c596c537b2713097e86005b4d3a5c", + "crates/horizon-core/src/geometry/shape/join.rs": "f1b51887b7bf9482557e3ffa99e136f2e5db07ceeb05fdb116adf45e87129075", + "crates/horizon-core/src/geometry/shape.rs": "e8d88a763de1525f77a30b99591641e8a8b80946263acbde035a9bcc356df960", + "crates/horizon-core/src/geometry/sweep.rs": "abd6009c61555161ef2f18b431b2aa384b126c0871cae8b96a62358498a56a1d", + "crates/horizon-core/src/geometry/traversal.rs": "77a3da570a0b85582b669cf4d841cba4b06327006dda26d4a26df4e8ce05bae4", + "crates/horizon-core/src/geometry/trig.rs": "05cef9341a298a474221a0556a11ddf47650393ea7a97b4b2022a4da6628fe37", + "crates/horizon-core/src/lib.rs": "4255e16837fde42a52ba118f60581d197c571bb530b60d635e506184ebf4db0b", + "crates/horizon-core/src/noise/mod.rs": "1dab5a713abf702402e69ace1bf9e476bba544b0ede5a61c3c3afc60af0a03f3", + "crates/horizon-core/src/noise/normal.rs": "e9e7a46472a54a02c98c46da819842090567153b32fe0c84249009ef19af7aad", + "crates/horizon-core/src/noise/perlin.rs": "e73815447432eab7542f67366cb3f1b368d164e33f6c9cdbd045ac8e4db0932a", + "crates/horizon-core/src/noise/simplex.rs": "6c0dbc15b2b7510195dd28fceca01dcaac1847ccc4a84d299c351589c642fe7a", + "crates/horizon-core/src/noise/stack.rs": "7f6ef0e7d920139ef1a6ad3ec5640f92765715f86dcc5ec29f1a149984f45fa3", + "crates/horizon-core/src/noise/volume.rs": "3f05d2b1780acfdfee60037677d309937a5033b45ef428fc2d540942d4b56b43", + "crates/horizon-core/src/position/iter.rs": "32d2d8e74821cc4e49e3354202d35d3120e4415bfdda9375a73f2137ef651129", + "crates/horizon-core/src/position.rs": "e10101088e6902386b9b6d27773d44312f1ff72bebb19e57967a54945dde9dcd", + "crates/horizon-core/src/random/hash.rs": "0f88a127a1ba2705d3d6c0ebd009324d6ba86ffa2d27edf2d2a84765e690e4a3", + "crates/horizon-core/src/random/positional.rs": "35494745e5d4cf89e282c3ae0ee245e9f2640ddbbc03bc0055df742006a9c862", + "crates/horizon-core/src/random/worldgen.rs": "9f07ff33bcc038d34e27aa5465753bb21df18f2975e43cae201f709bc186945a", + "crates/horizon-core/src/random.rs": "ddb95d3a83e28ad605ff1f49f5df270cc543ca5ef1130e37d989dc74c8d861e6", + "crates/horizon-core/src/storage.rs": "f6c32201c64aefde06e57a2c767be46918df613548a322801546b133799cc33d", + "crates/horizon-core/src/text.rs": "e6553ddb5db5039e8dc4614b6a6dc9f35a26dc3d1673b68a04e72e7983b67abb", + "crates/horizon-core/src/ticks/generation.rs": "be1237e790693f633a61714ddae68c6506a777588a8a3ab5efbe53a35774dbd2", + "crates/horizon-core/src/ticks/heap.rs": "bf28afab8af77c3d243aa96cd8a1fb8d934140a4dbd07dbfdbf4e7908bb4b845", + "crates/horizon-core/src/ticks/index.rs": "1f1a6a568c2a30eafecb020ca4d5f5fc414860de85b8b381fc419a2e3e65c24a", + "crates/horizon-core/src/ticks/mod.rs": "01634630668172516b8b79d0907bad47f6e2c543a6d499c2fa4b201823b375cb", + "crates/horizon-world/src/bin/worldgen_parity.rs": "3143ceb9b56eecfd7667e5feb5f9f0cd13cfb8754b38d4ea338dbba97f2819da", + "crates/horizon-world/src/block_collision.rs": "39207247f2f6dd3ddc77640229044246f91b32054563f88a435f1bd92ad2092c", + "crates/horizon-world/src/block_collision_scan.rs": "20802862364c110ff85eeaf48606596a8468b83768dda46b412d337958790b41", + "crates/horizon-world/src/block_entities.rs": "a9deae92e6e5a1fcf4f0476c33ea4d170ddad6fd4b3303e1bb66d933973d5e06", + "crates/horizon-world/src/block_entity_push.rs": "12ee91bf604946234b24a0da4333f07d1830ab9743a422d41c8d0afac7d49cfd", + "crates/horizon-world/src/block_landing.rs": "33083fe39266fbefec1b3ea1a19f2afc609791d189da247c895e116ad6eb266a", + "crates/horizon-world/src/block_placement.rs": "524f4892d50bab77b7d89d19f7b048ffe746996f2ed7f376db2c6ab00ed75c24", + "crates/horizon-world/src/block_properties.rs": "b0bd8a983336c48e488a20caa77dd5c39b1a74bbd14ebc03b3b64556a1c864fc", + "crates/horizon-world/src/block_render.rs": "8df52ee11aef406a1571198a64048062a0ef6864e0058d9293a57f64b73c916a", + "crates/horizon-world/src/block_support.rs": "ade4fbd47284b450dabdd682dc86138b2ac05a0fd379525d7110648908245476", + "crates/horizon-world/src/block_survival.rs": "28ce64375e09819a4f13da9cfbe37c08b5ad0daf01b289c1c360b4567e50e811", + "crates/horizon-world/src/chunk.rs": "8843e6f48dbbc4245ce4e95564dfba3a4d7a051ba0b1577fb038c07adb0ff0f9", + "crates/horizon-world/src/chunk_status.rs": "dbb8583cfa775203c544d24d69e03720385428f15a267a561356c21f5025e9c7", + "crates/horizon-world/src/collision.rs": "a749794d5c27ccd6c0d964ad02f9dc31d49d9f4fe4b7366cd4bd8155e681b00b", + "crates/horizon-world/src/collision_context.rs": "2176c7034e07f547510a8327cdad9d3b63f26e4754032b76b7d5fe13166c333c", + "crates/horizon-world/src/compression.rs": "4db73e6ca9a0d552d415ce60e7e88b89c9cf2b7d060a669ff8328f518316e101", + "crates/horizon-world/src/dynamic_support.rs": "5996bc1c436fb6215d8fc3d0bb4de5dd9b966d842d432580d624195771697fb8", + "crates/horizon-world/src/edge_backoff.rs": "5259d7aa44e64f88351d1556c44dd0edcad3b400a099c659755792efba7b5773", + "crates/horizon-world/src/entity_collision.rs": "09f2fd7f6cd56fc282f468ecfa765a94506929e9684bd3a5c90123e5b51cd627", + "crates/horizon-world/src/entity_move.rs": "a40a50d3e841625a75d4cfbb7c2630ab5a62a574456567e7234f401ae8441b32", + "crates/horizon-world/src/entity_obstruction.rs": "a4b28a19bf83b23611c59d94535c488db37e4aee24b22c973c545910894a809f", + "crates/horizon-world/src/fall_damage.rs": "8c28e2933493569fe05dea22332024287d28b0746b514dd465681040b003296b", + "crates/horizon-world/src/fluid.rs": "a0a1f34ba94d8b59dc975becee8952960764d67ae08cf2d6a8bcd40a614b15c4", + "crates/horizon-world/src/ground_support.rs": "3e371d74ecede4d30beef0a7e8a2ee33463584a5b1ed3d852b394efcce1ffbe0", + "crates/horizon-world/src/heightmap.rs": "a5edef082b5af40d814421ea17e129b3b06d70abee6434ed263e4bba58571698", + "crates/horizon-world/src/lib.rs": "c27984bbc003c31f60ff60f7bf01d928f3eaf8ba977c3bf0b3102b2cb0faeced", + "crates/horizon-world/src/liquid_collision.rs": "0f0a83b04e5a5bc9d98ed05fde4d6584ba69642e93522e726e4cfc325d6fcb1d", + "crates/horizon-world/src/living_fall.rs": "da0f176f4731e1c86e08bfeee8167742fd1b86380ff0e9304c13c1ce3f1c67ab", + "crates/horizon-world/src/main.rs": "fce57418b8c22a6a0a9044baa89ed3d9c243bc2e8413fea3ff0f32985daba021", + "crates/horizon-world/src/movement_emission.rs": "76e7d6f5b19c94676190f54389f9c4ce12cf89945ccfcb9c750de473223e7753", + "crates/horizon-world/src/movement_properties.rs": "6f01ab920b376a13f5ae12c3718f4dd067e1dff2b829585af00a8b2100f3775d", + "crates/horizon-world/src/movement_restitution.rs": "04b4fe0947e7e52576145bd8ed15920278e8a538afcc74d125eaca513dffa0ef", + "crates/horizon-world/src/movement_support.rs": "02d85cf572847f2e60b5a7f9d60b856888a67b3c1f4df03e81998c66fdf874be", + "crates/horizon-world/src/nbt.rs": "60262c3de8c8f68bc2da0370ccb17393d32b85f5f5022c6358599a7d32119ea6", + "crates/horizon-world/src/piston_movement.rs": "7a3fd2549a582b5f3a5c21aa05d67d944d62ca1105856679e100ac5b44e9a52f", + "crates/horizon-world/src/piston_shape.rs": "399c2337103b8e7e0eb5667227c5aa14f9549d6a467b7be7da3d0717f0263500", + "crates/horizon-world/src/powder_snow_collision.rs": "7289946a21478d11abfd2bdcdddff3d7f98e25bf6a68564fed45c3cc01ac2a36", + "crates/horizon-world/src/region.rs": "72b642e87fed37ed17f54d4c30f96c3b64d26c14bc0589c31ef40ff1616188fc", + "crates/horizon-world/src/registry/json.rs": "7a683afcd1742c5307756927d7f0aa779c7d7992af87d8d371dd98108728e235", + "crates/horizon-world/src/registry.rs": "f2e58b2b4d944ac8fc6226bd3352cefc3d2052d8473e02ccf7dc402b176df435", + "crates/horizon-world/src/registry_collision_scan.rs": "a5021a168adcedc37d77f533a8921c02f09c59f46a01737586aaa33c5908b8d0", + "crates/horizon-world/src/retrogen.rs": "6efd36a6293756688c1990a7965bd65609d7c5e2bf2a5f4f01d54932d2b9386b", + "crates/horizon-world/src/suffocation.rs": "bed219dd5bbab6263b98fbe3d0eb512cb96c52ae847210efe49514d05f22f11e", + "crates/horizon-world/src/tick_data.rs": "f8d04a69ea263f4168bce33f92e5b4ee2fe07d7caf50a1621c6b3ef6eb2479f3", + "crates/horizon-world/src/world_movement.rs": "f8fe9e34f2395d2a04c839d1c8ba78e09ce493c26de6f6e4e237b9c19c061da5", + "crates/horizon-world/src/worldgen/anchor.rs": "d156b8a0b147a274cdd143409b441f4f43edecde6e8ee14ef9cf0309f743143a", + "crates/horizon-world/src/worldgen/aquifer/config.rs": "fcd2cc3a520c1527c88f095a3b5d38487e7ebb1ccecc5f757f93d04a069add4f", + "crates/horizon-world/src/worldgen/aquifer/noise_based.rs": "74daaa389b5c3dfdc99214dfff256a9e172f83548d2326cbc941d9637cc53fb7", + "crates/horizon-world/src/worldgen/aquifer.rs": "cb4b66d87de3f9f79ae6ffe4badd6f0f44886cd42ad9de2771b8b56c4f1e28c7", + "crates/horizon-world/src/worldgen/bamboo.rs": "d9b47113c99be34256e58308bc6f40072ff25c579d6bf995182aa862c0006102", + "crates/horizon-world/src/worldgen/biome.rs": "3fe316b875aa32683759c337dc71f624910faf0812a2d6d83b8098d6d0bac7a0", + "crates/horizon-world/src/worldgen/biome_chunk.rs": "8fdfe6ebe3ffeca97611fd5991a5bc6b2783ea1c62f4d8ef3b2dd53c04a5dc1d", + "crates/horizon-world/src/worldgen/biome_predicate.rs": "bfc581bc40077cc74ffda43e990a55fe997adc04c7130f98e039c3305ca79aac", + "crates/horizon-world/src/worldgen/biome_source/codec.rs": "2b9e4f5d679eb444cb73174f511e3b9f32e9eb138248ab12cffc6cfd76adf3a8", + "crates/horizon-world/src/worldgen/biome_source/runtime.rs": "f0b02c1521178e4d0377c6ca62694604b6a773f1f234f070ff0cde4685911d45", + "crates/horizon-world/src/worldgen/biome_source.rs": "e181e1736f3e0b75f7091a24c88de3fd116e01b98424d02382aea7a503d1eb54", + "crates/horizon-world/src/worldgen/biome_temperature.rs": "cde4bbab5e260c4065b113376c423b8511ade93c5aa5fd976e245ee8a58b5ad1", + "crates/horizon-world/src/worldgen/biome_weather.rs": "96b1f9494559ed31d06257b83d604d00cc069ccc8c7b68094117e034b4a71bd7", + "crates/horizon-world/src/worldgen/block_blob.rs": "c608c57b3d77279646ecb89e50677dd8e452b0422ffd357ba755536c5589eb89", + "crates/horizon-world/src/worldgen/block_column.rs": "f72c21a9066ee0aa68c2fb79fba436d05afb14c6ef5a2cb012f1ed35fc17cd22", + "crates/horizon-world/src/worldgen/block_feature.rs": "36b4e46ee7f7f73bcb94d8bce8d99013e4b16234c9a03542eeb3186e88dd1c21", + "crates/horizon-world/src/worldgen/block_pile.rs": "623e369b77e0526ed2756485a6c0bb09683cd28ad8876dea907360ca6665b39d", + "crates/horizon-world/src/worldgen/block_predicate.rs": "074c692367e3f2191a1bb28d87b23402430a843c246bd30a4ea838ee3500280e", + "crates/horizon-world/src/worldgen/blue_ice.rs": "eedd77bdaff808330b864166d303378096daa5c7d235e925770b171c05109247", + "crates/horizon-world/src/worldgen/bonus_chest.rs": "05d0f63fbc7412f13ba1fa37a7238114b8f70bfff318beb2dece590482d49cc0", + "crates/horizon-world/src/worldgen/bulk_section.rs": "eecf61532d7793f853884c5a18a7f436a59835c7b13d3ec9075804511553c9ba", + "crates/horizon-world/src/worldgen/carver/canyon.rs": "509c4d5a9d689849e98e894ab14a9fbf82d10707d38e9cc688ded3c4778df3e2", + "crates/horizon-world/src/worldgen/carver/generation.rs": "a4498d0d1457536de3922a08e9c40ccce8d2ef9cc263801c15a412327513c098", + "crates/horizon-world/src/worldgen/carver.rs": "46b251e28225fe1250de38f49dec5dacda4e2c4ba1e556e21e6d1e14e2651fc3", + "crates/horizon-world/src/worldgen/carving.rs": "4e77d4e5c4a0ca55ca3563b9bdf6f379342e0bdf9d019d60f7bc57ce4afb5fbd", + "crates/horizon-world/src/worldgen/chorus_plant.rs": "29224c164ede919e280a65a68f155c873e2c8fd0aa7d7e244d8fa9e468760250", + "crates/horizon-world/src/worldgen/climate/codec.rs": "d567adeb5d837816fca69102db51d790f71aec1b51c09298230d24612e400a14", + "crates/horizon-world/src/worldgen/climate/index.rs": "c5ce61679a5f0e0f2f0af2a373424effa124d0cce8250609eff38b7034650756", + "crates/horizon-world/src/worldgen/climate.rs": "1b9ad44407f40aee4c1cd5357a5c9c443c1b105728f01cf5c644072f99f8357e", + "crates/horizon-world/src/worldgen/decoration.rs": "75a79b20b21e133816e729f16e758be6165d0db18a64bfbd5fd3618325a2adf3", + "crates/horizon-world/src/worldgen/delta.rs": "c2bf49ddf3231d28c109e68871429bc13d76a7bebe14eb33e84aa856bdd66145", + "crates/horizon-world/src/worldgen/density.rs": "65a98c156a290a6b374ef670d28cd5dfbe0aa93171824d1bbd96422536273f84", + "crates/horizon-world/src/worldgen/disk.rs": "6f8f9e059755af44c9bfa1b91eba54a7d13fad811dce31717c5502d7f6df33d7", + "crates/horizon-world/src/worldgen/end_gateway.rs": "f53fde77d6a36a525cd15d890003b2c129d0689a4cbfa2cf443a0c2e3acae040", + "crates/horizon-world/src/worldgen/end_podium.rs": "8f6d0d6d60ff0fde63de5781e867239db321f82cf12639f0537d410a3731ee1d", + "crates/horizon-world/src/worldgen/feature.rs": "40500af71ee6628dfb27518262d99f08d0383ae6c7b9397452feef0c213eadd9", + "crates/horizon-world/src/worldgen/feature_catalog.rs": "efc7b17b227e71622ef34108be9abd010f5675b68ea0409fcab82581d9aab88a", + "crates/horizon-world/src/worldgen/feature_graph.rs": "da132fca9fdafb0d89a917d5f4b68c60b70fb33cadf948a107da0e35d83dcd91", + "crates/horizon-world/src/worldgen/feature_region.rs": "df0ff142a461cafa9a3b42b949ad3376887f2be8d4ea5cc794a3ed2e977bb499", + "crates/horizon-world/src/worldgen/feature_sort.rs": "3488a093dfe1321aaa29d8a30cfb115d8e666a81bf195514e87b170de626835c", + "crates/horizon-world/src/worldgen/fluid_predicate.rs": "64197c9e63bebf1ce04bc280bfe980bb6c1b9aa887ca5cded904aeb0d9ae6c46", + "crates/horizon-world/src/worldgen/height_provider.rs": "368d4dc119854742a045c4a717679c921456da681fc5f71aa9b37102d6f33b47", + "crates/horizon-world/src/worldgen/material_condition/codec.rs": "09b909bc384472f76d163c8069ec6b507c7d5c6a028a36d5e0e3df20deff7b3b", + "crates/horizon-world/src/worldgen/material_condition/density.rs": "df9d0761e978e5010663ae8b44f2d6c1ecb4907331e333bdb6a2b487c1e107de", + "crates/horizon-world/src/worldgen/material_condition/library.rs": "a55534c5da97575a819af5a7dc0dbec9069993fa8b2619c6c9d5576946e683fa", + "crates/horizon-world/src/worldgen/material_condition/noise.rs": "ffb59cc6be2275d7a5621bf7d768527e74a8326ed7ad78618746711540921675", + "crates/horizon-world/src/worldgen/material_condition/rule.rs": "fed643352efbcb1f68f29b6241896617db889fc7c577a6247564e01922fec1fe", + "crates/horizon-world/src/worldgen/material_condition.rs": "d638dcb8cbcb5da20b3cb29602b0ae6a86c4fdf9d8c0fb725de8ec5fc108717e", + "crates/horizon-world/src/worldgen/material_extensions.rs": "ab806eb679d5116e1aebb64fca70e1499ae71f83c9507a8cb4c3201c2b1bf037", + "crates/horizon-world/src/worldgen/material_stage.rs": "698cbc786eff13c246e2bc2ea062401918cd4265899fbc3d53abc87e45a8e7a1", + "crates/horizon-world/src/worldgen/material_surface.rs": "5d37b90099df3d6da0845dab164e71469fb287019c2ee74bc6714f3b9d6d6d5b", + "crates/horizon-world/src/worldgen/ore_feature.rs": "83b89fd3be691c5412c0215b6abefdcaa44119a05aa41217e061970eb7e12ebd", + "crates/horizon-world/src/worldgen/placement/chain.rs": "9218399b4d3757f7bbc550d7729a7201f32a1c83a7030865e0c12aab8a80d116", + "crates/horizon-world/src/worldgen/placement/context.rs": "a278304795136677ad4b4933ea50c972b9729a1c671c03fa309c193e224b1fd6", + "crates/horizon-world/src/worldgen/placement/layer.rs": "a0f5ca776f33f9d4f72b8fdcd163426152e5e43cb4a7572fc1d4fc57bf965de1", + "crates/horizon-world/src/worldgen/placement.rs": "b0397e5818fae84358546193a95f84bcb0b5bffafce909e6fcb3386179ee8023", + "crates/horizon-world/src/worldgen/replace_blobs.rs": "bf3377d86d2570cd15288ba69dde64a2cf67c5bcbb9a31d0c23421964af74afa", + "crates/horizon-world/src/worldgen/router.rs": "2cee7cf15a0d2ada12be6b39d83e78177fd3033922e4f4cce87a64073cec2847", + "crates/horizon-world/src/worldgen/rule_test.rs": "d1662ec492489ebaa4b87e20f9a8c809dcf3a223a151211bd65ec241a63692fc", + "crates/horizon-world/src/worldgen/settings.rs": "a8fe87752e1982ef11cb67f8d80d8e3428b2c27db178044cb425ba803e689e59", + "crates/horizon-world/src/worldgen/simple_block.rs": "9ba1a88d5db4925f8c37366557887f776c805f9a6444781615b5dbaff52e92a4", + "crates/horizon-world/src/worldgen/snow_freeze.rs": "9040be3d5c7f8f0fd812d2df45e154bf8a9eeef038530b83fa4159d246eac35f", + "crates/horizon-world/src/worldgen/speleothem.rs": "c71651c1c97818a8a85863e14414d6139ea3bdeb897fa422043bcc1499d99aae", + "crates/horizon-world/src/worldgen/spike.rs": "eacf0d9d3d2bc3d4bcf62bc87555f90031138236a5bfef0eb5270204f7c2a4fc", + "crates/horizon-world/src/worldgen/spring.rs": "830849e9b89fcdf09e9b06fbefdcdebe3c8c3f82804f60b8bd5de04ba303cb62", + "crates/horizon-world/src/worldgen/state_provider/catalog.rs": "c8687e15a4887a9200c8d365857d801efaa8f90e669eee3e600a1db4905119eb", + "crates/horizon-world/src/worldgen/state_provider/noise.rs": "7ee369c2a032e5620cb9bdf9ee7adad1f70870fb0a27efb7b7c514d92f83491f", + "crates/horizon-world/src/worldgen/state_provider/properties.rs": "9909139aaca1dc4f743957257fadaa9d423d21fa401fa9abc050505f79365139", + "crates/horizon-world/src/worldgen/state_provider.rs": "be29ada818826a90afe01ab60916360fdfc4cbde5af15c9359cd56b1f1f7b156", + "crates/horizon-world/src/worldgen/tags.rs": "d35ded93fe6e95ea045cadcf4925e656dc34ef832b742179b9ad0cc49f902f31", + "crates/horizon-world/src/worldgen/terrain.rs": "fd1f2e0afc15daf66f484ea40a1f2b7013650bda0941b23c64c81ee9f79cb673", + "crates/horizon-world/src/worldgen/underwater_magma.rs": "7317e40af5ee5a9780f90ce358fa5d3e7f5b3d513b731df6ff97754d9c2a0154", + "crates/horizon-world/src/worldgen/value_provider.rs": "3358b563db8cb0bc8a9cfbbe2b8b3e70941cd2d37a0b519fbb6545f8d0dcbb69", + "crates/horizon-world/src/worldgen/vines.rs": "ee9b79b65012bd86a838241d2ef708680f7fe8eafc8aa656dae9cf2abf3c6418", + "crates/horizon-world/src/worldgen/weighted.rs": "839840aae25302933bbd97a81a2336330e2d03a0ad1684e1e16ac769ed408e6b", + "crates/horizon-world/src/worldgen/write_zone.rs": "8e17a179764d3f68c05882aef35f6810c28a0000f9a83e58bd6030660b76e64e", + "crates/horizon-world/src/worldgen.rs": "e8e9f099ee1fcec8eedce31805d676d3f89c18e84f494a147ffcbfee23d3d54f", + "crates/horizon-world/examples/entity_move_fixture.rs": "dc26dffa594a9c6d7fcc39ae03b254a2a45398db513b34f6b30867818539ff53" + }, + "fixtures_sha256": { + ".local/rust-entity-move-parity/inputs.txt": "b7dd8870db34cf8a5c178ee0fa709e8a6eff7196b25cc95f931072283bdc69a7" + }, + "rust_output_sha256": "23db36cb04444bef7e7bcab15c976a698b247a6de7a0192acf43a592b095b70c", + "references": [ + { + "side": "client", + "jar_sha256": "4508d006323f24fa02876310c192d739af56516eb259000ac50f0909a68c9a2d", + "output_sha256": "23db36cb04444bef7e7bcab15c976a698b247a6de7a0192acf43a592b095b70c", + "matched_output_lines": 543653 + }, + { + "side": "server", + "jar_sha256": "a362163eec5d1612d520772bc16e5b39c09e3b234fdc045f56bf544284ee8ae6", + "output_sha256": "23db36cb04444bef7e7bcab15c976a698b247a6de7a0192acf43a592b095b70c", + "matched_output_lines": 543653 + } + ], + "scope": "Actual Entity.move with native shape collision resolution (zero step height), native zero-restitution handling and final authority checks. Compares no-physics and piston early exits including signed zero, stuck multiplier reset, edge callback, fall-reset clip endpoints, movement recording cap, collision flag writes, ground/fall callback ordering, removal, passenger emission guard, speed multiplication and failures after partial state changes. Providers control piston/edge decisions, ground support, ray hits, landing effects and sound policy; live world integration and non-passenger movement emission remain separate work." +} diff --git a/crates/horizon-world/examples/entity_move_fixture.rs b/crates/horizon-world/examples/entity_move_fixture.rs new file mode 100644 index 0000000000..fbcbb8ed14 --- /dev/null +++ b/crates/horizon-world/examples/entity_move_fixture.rs @@ -0,0 +1,418 @@ +use horizon_core::{ + geometry::{Aabb, Axis, Vec3, VoxelShape, resolve_shape_movement}, + position::BlockPos, +}; +use horizon_world::{ + entity_move::*, + movement_emission::MovementEmission, + movement_restitution::{RestitutionInput, RestitutionWorld, restitute_movement}, + registry::BlockStateId, +}; +use serde_json::Value; +use std::{ + fs, + io::{self, Write}, + path::Path, +}; +fn number(v: &Value) -> f64 { + f64::from_bits(u64::from_str_radix(v.as_str().unwrap(), 16).unwrap()) +} +fn single(v: &Value) -> f32 { + f32::from_bits(u32::from_str_radix(v.as_str().unwrap(), 16).unwrap()) +} +fn vector(v: &Value) -> Vec3 { + Vec3::new(number(&v[0]), number(&v[1]), number(&v[2])) +} +fn bits(v: f64) -> u64 { + if v.is_nan() { + 0x7ff8000000000000 + } else { + v.to_bits() + } +} +fn vec(label: &str, v: Vec3) -> String { + format!( + "{label} {:016x} {:016x} {:016x}", + bits(v.x), + bits(v.y), + bits(v.z) + ) +} +fn bounds(label: &str, b: Aabb) -> String { + format!( + "{label} {:016x} {:016x} {:016x} {:016x} {:016x} {:016x}", + bits(b.min(Axis::X)), + bits(b.min(Axis::Y)), + bits(b.min(Axis::Z)), + bits(b.max(Axis::X)), + bits(b.max(Axis::Y)), + bits(b.max(Axis::Z)) + ) +} +fn pos(p: BlockPos) -> String { + format!("{} {} {}", p.x, p.y, p.z) +} +struct World<'a> { + f: &'a Value, + out: &'a mut dyn Write, + calls: i64, + position: Vec3, + velocity: Vec3, + stuck: Vec3, + bounds: Aabb, + flags: CollisionFlags, + grounded: bool, + removed: bool, + fall: f64, + history: MovementHistory, + shapes: Vec, +} +impl World<'_> { + fn flag(&self, n: &str) -> bool { + self.f[n].as_bool().unwrap() + } + fn query(&mut self, s: String) -> io::Result<()> { + writeln!(self.out, "{s}")?; + let call = self.calls; + self.calls += 1; + if call == self.f["fail"].as_i64().unwrap() { + Err(io::Error::other("injected move failure")) + } else { + Ok(()) + } + } +} +impl EntityMoveWorld for World<'_> { + type Error = io::Error; + fn no_physics(&self) -> bool { + self.flag("no_physics") + } + fn position(&self) -> Vec3 { + self.position + } + fn set_position(&mut self, p: Vec3) -> io::Result<()> { + self.query(vec("set-position", p))?; + self.bounds = self.bounds.move_by(p.subtract(self.position)); + self.position = p; + Ok(()) + } + fn record_movement(&mut self, t: MoverType, v: Vec3) -> io::Result<()> { + self.query(vec(&format!("record {}", t as u32), v)) + } + fn collision_flags(&self) -> CollisionFlags { + self.flags + } + fn set_collision_flags(&mut self, f: CollisionFlags) { + self.flags = f; + } + fn limit_piston_movement(&mut self, v: Vec3) -> io::Result { + self.query(vec("piston", v))?; + Ok(vector(&self.f["piston_result"])) + } + fn stuck_multiplier(&self) -> Vec3 { + self.stuck + } + fn set_stuck_multiplier(&mut self, v: Vec3) { + self.stuck = v; + } + fn velocity(&mut self) -> io::Result { + self.query("velocity".into())?; + Ok(self.velocity) + } + fn set_velocity(&mut self, v: Vec3) -> io::Result<()> { + self.query(vec("set-velocity", v))?; + self.velocity = v; + Ok(()) + } + fn back_off_edge(&mut self, v: Vec3, t: MoverType) -> io::Result { + self.query(vec(&format!("edge {}", t as u32), v))?; + Ok(if self.flag("edge_override") { + vector(&self.f["edge_result"]) + } else { + v + }) + } + fn collide(&mut self, v: Vec3) -> io::Result { + let b = self.bounds.expand_towards(v); + self.query(bounds("entities", b))?; + if v.length_squared() == 0.0 { + return Ok(v); + } + self.query("border".into())?; + self.query(bounds("border-test", b))?; + self.query(bounds("blocks", b))?; + Ok(resolve_shape_movement( + v, + self.bounds, + &self.shapes.iter().collect::>(), + )) + } + fn fall_distance(&self) -> f64 { + self.fall + } + fn fall_reset_clip(&mut self, from: Vec3, to: Vec3) -> io::Result { + self.query(vec("clip-from", from))?; + self.query(vec("clip-to", to))?; + Ok(self.flag("clip_hit")) + } + fn reset_fall_distance(&mut self) -> io::Result<()> { + self.query("reset-fall".into())?; + self.fall = 0.0; + Ok(()) + } + fn movement_history(&mut self) -> &mut MovementHistory { + &mut self.history + } + fn locally_authoritative(&mut self) -> io::Result { + let client = self.client_side()?; + self.query( + if client { + "local-client-authority" + } else { + "client-authority" + } + .into(), + )?; + Ok(if client { + self.flag("local_client") + } else { + !self.flag("client_authority") + }) + } + fn set_on_ground_with_movement( + &mut self, + below: bool, + horizontal: bool, + v: Vec3, + ) -> io::Result<()> { + self.query(vec(&format!("ground-set {below} {horizontal}"), v))?; + self.grounded = below; + self.flags.horizontal = horizontal; + Ok(()) + } + fn minor_horizontal_collision(&mut self, v: Vec3) -> io::Result { + self.query(vec("minor", v))?; + Ok(self.flag("minor_result")) + } + fn legacy_on_position(&mut self) -> io::Result { + self.query("legacy".into())?; + Ok(BlockPos::new(17, -64, -33)) + } + fn block_state(&mut self, p: BlockPos) -> io::Result { + self.query(format!("block {}", pos(p)))?; + Ok(BlockStateId(0)) + } + fn on_ground(&mut self) -> io::Result { + self.query("ground".into())?; + Ok(self.grounded) + } + fn check_fall_damage( + &mut self, + dy: f64, + ground: bool, + _: BlockStateId, + p: BlockPos, + ) -> io::Result<()> { + self.query(format!("fall {:016x} {ground} {}", bits(dy), pos(p)))?; + if self.flag("fall_mutates") { + self.fall = 3.0; + self.velocity = Vec3::new(17.0, 18.0, 19.0); + self.flags.horizontal = self.flag("mutated_horizontal"); + self.flags.vertical = self.flag("mutated_vertical"); + self.flags.below = self.flag("mutated_below"); + } + if self.flag("fall_removes") { + self.removed = true; + } + Ok(()) + } + fn removed(&mut self) -> io::Result { + Ok(self.removed) + } + fn can_simulate(&mut self) -> io::Result { + self.query("simulation".into())?; + Ok(true) + } + fn restitute( + &mut self, + state: BlockStateId, + x: bool, + z: bool, + resolved: Vec3, + ) -> io::Result<()> { + let input = RestitutionInput { + block: state, + collided_x: x, + collided_z: z, + vertical_collision: self.flags.vertical, + collision_below: self.flags.below, + living: false, + resolved_movement: resolved, + fall_distance: self.fall, + }; + restitute_movement(&mut Bounce(self), input) + } + fn client_side(&mut self) -> io::Result { + self.query("client".into())?; + Ok(self.flag("client")) + } + fn movement_emission(&mut self) -> io::Result { + self.query("emission".into())?; + Ok(if self.flag("emits") { + MovementEmission::All + } else { + MovementEmission::None + }) + } + fn passenger(&mut self) -> io::Result { + self.query("passenger".into())?; + Ok(true) + } + fn apply_movement_emission( + &mut self, + _: MovementEmission, + _: Vec3, + _: BlockPos, + _: BlockStateId, + ) -> io::Result<()> { + panic!("passenger guard must skip emission in this fixture") + } + fn block_speed_factor(&mut self) -> io::Result { + self.query("speed".into())?; + Ok(single(&self.f["speed"])) + } +} +struct Bounce<'a, 'b>(&'a mut World<'b>); +impl RestitutionWorld for Bounce<'_, '_> { + type Error = io::Error; + fn suppressing_bounce(&mut self) -> io::Result { + self.0.query("suppress".into())?; + Ok(false) + } + fn entity_bounciness(&mut self) -> io::Result { + self.0.query("entity-bounce".into())?; + Ok(0.0) + } + fn velocity(&mut self) -> io::Result { + self.0.velocity() + } + fn effective_gravity(&mut self) -> io::Result { + self.0.query("gravity".into())?; + Ok(0.08) + } + fn block_suppresses_bounce(&mut self, _: BlockStateId) -> io::Result { + self.0.query("block-suppresses".into())?; + Ok(false) + } + fn block_bounce_restitution(&mut self, _: BlockStateId) -> io::Result { + self.0.query("block-bounce".into())?; + Ok(0.0) + } + fn air_drag(&mut self) -> io::Result { + panic!("no positive restitution") + } + fn movement_support_position(&mut self) -> io::Result { + panic!("no bounce event") + } + fn bounce_on(&mut self, _: BlockStateId, _: BlockPos, _: f64) -> io::Result<()> { + panic!("no bounce event") + } + fn bounce_event(&mut self) -> io::Result<()> { + panic!("no bounce event") + } + fn mark_position_for_sync(&mut self) { + panic!("no bounce event") + } + fn set_velocity(&mut self, v: Vec3) -> io::Result<()> { + self.0.set_velocity(v) + } +} +fn main() -> Result<(), Box> { + let arg = std::env::args().nth(1).expect("private fixture directory"); + let dir = Path::new(&arg); + let mut out = io::BufWriter::new(fs::File::create(dir.join("rust.txt"))?); + for line in fs::read_to_string(dir.join("inputs.txt"))?.lines() { + let f: Value = serde_json::from_str(line)?; + let p = vector(&f["position"]); + let flag = |n: &str| f[n].as_bool().unwrap(); + let shapes = f["shapes"] + .as_array() + .unwrap() + .iter() + .map(|v| { + VoxelShape::from_box(Aabb::new( + Vec3::new(number(&v[0]), number(&v[1]), number(&v[2])), + Vec3::new(number(&v[3]), number(&v[4]), number(&v[5])), + )) + }) + .collect::, _>>()?; + let mut history = MovementHistory::default(); + for i in 0..f["history"].as_u64().unwrap() { + history.add(MovementRecord { + from: Vec3::new(i as f64, 0.0, 0.0), + to: Vec3::new((i + 1) as f64, 0.0, 0.0), + original: Some(Vec3::new(1.0, 0.0, 0.0)), + }); + } + let mut w = World { + f: &f, + out: &mut out, + calls: 0, + position: p, + velocity: vector(&f["velocity"]), + stuck: vector(&f["stuck"]), + bounds: Aabb::new( + Vec3::new(p.x - 0.3, p.y, p.z - 0.3), + Vec3::new(p.x + 0.3, p.y + 1.8, p.z + 0.3), + ), + flags: CollisionFlags { + horizontal: flag("horizontal"), + vertical: flag("vertical"), + below: flag("below"), + minor: flag("minor"), + }, + grounded: flag("ground"), + removed: flag("removed"), + fall: number(&f["distance"]), + history, + shapes, + }; + let mover = [ + MoverType::SelfMovement, + MoverType::Player, + MoverType::Piston, + MoverType::ShulkerBox, + MoverType::Shulker, + ][f["mover"].as_u64().unwrap() as usize]; + let result = move_entity(&mut w, mover, vector(&f["movement"])); + writeln!(w.out, "{}", if result.is_ok() { "ok" } else { "error" })?; + writeln!(w.out, "{}", vec("position", w.position))?; + writeln!(w.out, "{}", vec("velocity-final", w.velocity))?; + writeln!(w.out, "{}", vec("stuck", w.stuck))?; + writeln!(w.out, "{}", bounds("bounds", w.bounds))?; + writeln!( + w.out, + "flags {} {} {} {} {} {} {:016x}", + w.flags.horizontal, + w.flags.vertical, + w.flags.below, + w.flags.minor, + w.grounded, + w.removed, + bits(w.fall) + )?; + writeln!(w.out, "history {}", w.history.records().len())?; + for r in w.history.records() { + writeln!(w.out, "{}", vec("from", r.from))?; + writeln!(w.out, "{}", vec("to", r.to))?; + writeln!( + w.out, + "{}", + r.original + .map_or_else(|| "original none".to_string(), |v| vec("original", v)) + )?; + } + writeln!(w.out, "queries {}", w.calls)?; + } + Ok(()) +} diff --git a/crates/horizon-world/src/entity_move.rs b/crates/horizon-world/src/entity_move.rs new file mode 100644 index 0000000000..b3b189df0d --- /dev/null +++ b/crates/horizon-world/src/entity_move.rs @@ -0,0 +1,217 @@ +//! Entity.move ordering around collision, support, landing and movement effects. +use crate::{movement_emission::MovementEmission, registry::BlockStateId}; +use horizon_core::{geometry::Vec3, position::BlockPos}; +use std::collections::VecDeque; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MoverType { + SelfMovement, + Player, + Piston, + ShulkerBox, + Shulker, +} +#[derive(Clone, Copy, Debug, Default)] +pub struct CollisionFlags { + pub horizontal: bool, + pub vertical: bool, + pub below: bool, + pub minor: bool, +} +#[derive(Clone, Copy, Debug)] +pub struct MovementRecord { + pub from: Vec3, + pub to: Vec3, + pub original: Option, +} +#[derive(Clone, Debug, Default)] +pub struct MovementHistory { + records: VecDeque, +} +impl MovementHistory { + pub fn records(&self) -> &VecDeque { + &self.records + } + pub fn add(&mut self, record: MovementRecord) { + if self.records.len() >= 100 { + let first = self.records.pop_front().unwrap(); + let second = self.records.pop_front().unwrap(); + self.records.push_front(MovementRecord { + from: first.from, + to: second.to, + original: None, + }); + } + self.records.push_back(record); + } + pub fn remove_latest(&mut self) { + self.records.pop_back(); + } + pub fn clear(&mut self) { + self.records.clear(); + } +} +/// The provider retains entity fields so callbacks can mutate them. Hooks below +/// are native phase boundaries; bind collision, support, landing, restitution +/// and emission hooks to their corresponding world modules. +pub trait EntityMoveWorld { + type Error; + fn no_physics(&self) -> bool; + fn position(&self) -> Vec3; + fn set_position(&mut self, position: Vec3) -> Result<(), Self::Error>; + fn record_movement(&mut self, mover: MoverType, movement: Vec3) -> Result<(), Self::Error>; + fn collision_flags(&self) -> CollisionFlags; + fn set_collision_flags(&mut self, flags: CollisionFlags); + fn limit_piston_movement(&mut self, movement: Vec3) -> Result; + fn stuck_multiplier(&self) -> Vec3; + fn set_stuck_multiplier(&mut self, value: Vec3); + fn velocity(&mut self) -> Result; + fn set_velocity(&mut self, value: Vec3) -> Result<(), Self::Error>; + fn back_off_edge(&mut self, movement: Vec3, mover: MoverType) -> Result; + fn collide(&mut self, movement: Vec3) -> Result; + fn fall_distance(&self) -> f64; + /// FALLDAMAGE_RESETTING blocks and WATER fluids, entity collision context. + /// Return true for a non-MISS hit. + fn fall_reset_clip(&mut self, from: Vec3, to: Vec3) -> Result; + fn reset_fall_distance(&mut self) -> Result<(), Self::Error>; + fn movement_history(&mut self) -> &mut MovementHistory; + fn locally_authoritative(&mut self) -> Result; + fn set_on_ground_with_movement( + &mut self, + below: bool, + horizontal: bool, + movement: Vec3, + ) -> Result<(), Self::Error>; + fn minor_horizontal_collision(&mut self, movement: Vec3) -> Result; + fn legacy_on_position(&mut self) -> Result; + fn block_state(&mut self, p: BlockPos) -> Result; + fn on_ground(&mut self) -> Result; + fn check_fall_damage( + &mut self, + dy: f64, + on_ground: bool, + state: BlockStateId, + p: BlockPos, + ) -> Result<(), Self::Error>; + fn removed(&mut self) -> Result; + fn can_simulate(&mut self) -> Result; + fn restitute( + &mut self, + state: BlockStateId, + collided_x: bool, + collided_z: bool, + resolved: Vec3, + ) -> Result<(), Self::Error>; + fn client_side(&mut self) -> Result; + fn movement_emission(&mut self) -> Result; + fn passenger(&mut self) -> Result; + fn apply_movement_emission( + &mut self, + emission: MovementEmission, + resolved: Vec3, + p: BlockPos, + state: BlockStateId, + ) -> Result<(), Self::Error>; + fn block_speed_factor(&mut self) -> Result; +} +fn java_zero(v: Vec3) -> bool { + v.x.to_bits() == 0 && v.y.to_bits() == 0 && v.z.to_bits() == 0 +} +fn different(a: f64, b: f64) -> bool { + let delta = (b - a).abs(); + delta >= f64::from(1.0e-5f32) || delta.is_nan() +} +/// Release behavior; native IDE-only invalid-simulation assertions and profiler +/// callbacks do not form part of this gameplay transition. +pub fn move_entity( + world: &mut W, + mover: MoverType, + mut movement: Vec3, +) -> Result<(), W::Error> { + if world.no_physics() { + world.set_position(world.position().add_vector(movement))?; + world.record_movement(mover, movement)?; + world.set_collision_flags(CollisionFlags::default()); + return Ok(()); + } + if mover == MoverType::Piston { + movement = world.limit_piston_movement(movement)?; + if java_zero(movement) { + return Ok(()); + } + } + let stuck = world.stuck_multiplier(); + if stuck.length_squared() > 1.0e-7 { + if mover != MoverType::Piston { + movement = movement.multiply(stuck); + } + world.set_stuck_multiplier(Vec3::ZERO); + world.set_velocity(Vec3::ZERO)?; + } + movement = world.back_off_edge(movement, mover)?; + let resolved = world.collide(movement)?; + let length = resolved.length_squared(); + if length > 1.0e-7 || movement.length_squared() - length < 1.0e-7 { + if world.fall_distance() != 0.0 && length >= 1.0 { + let distance = resolved.length().min(8.0); + let from = world.position(); + let to = from.add_vector(resolved.normalize().scale(distance)); + if world.fall_reset_clip(from, to)? { + world.reset_fall_distance()?; + } + } + let from = world.position(); + let to = from.add_vector(resolved); + world.movement_history().add(MovementRecord { + from, + to, + original: Some(movement), + }); + world.set_position(to)?; + world.record_movement(mover, resolved)?; + } + let x = different(movement.x, resolved.x); + let z = different(movement.z, resolved.z); + let mut flags = world.collision_flags(); + flags.horizontal = x || z; + world.set_collision_flags(flags); + let nonzero_y = movement.y.abs() > 0.0; + if nonzero_y || world.locally_authoritative()? { + flags = world.collision_flags(); + flags.vertical = movement.y != resolved.y; + flags.below = flags.vertical && movement.y < 0.0; + world.set_collision_flags(flags); + world.set_on_ground_with_movement(flags.below, flags.horizontal, resolved)?; + } + let minor = if world.collision_flags().horizontal { + world.minor_horizontal_collision(resolved)? + } else { + false + }; + flags = world.collision_flags(); + flags.minor = minor; + world.set_collision_flags(flags); + let p = world.legacy_on_position()?; + let state = world.block_state(p)?; + if world.locally_authoritative()? { + let grounded = world.on_ground()?; + world.check_fall_damage(resolved.y, grounded, state, p)?; + } + if world.removed()? { + return Ok(()); + } + if world.can_simulate()? + && (nonzero_y && world.collision_flags().vertical || world.collision_flags().horizontal) + { + world.restitute(state, x, z, resolved)?; + } + if !world.client_side()? || world.locally_authoritative()? { + let emission = world.movement_emission()?; + if emission.anything() && !world.passenger()? { + world.apply_movement_emission(emission, resolved, p, state)?; + } + } + let factor = f64::from(world.block_speed_factor()?); + let velocity = world.velocity()?.multiply(Vec3::new(factor, 1.0, factor)); + world.set_velocity(velocity) +} diff --git a/crates/horizon-world/src/lib.rs b/crates/horizon-world/src/lib.rs index e6ac16b88f..e4d6daf09e 100644 --- a/crates/horizon-world/src/lib.rs +++ b/crates/horizon-world/src/lib.rs @@ -19,6 +19,7 @@ pub mod compression; pub mod dynamic_support; pub mod edge_backoff; pub mod entity_collision; +pub mod entity_move; pub mod entity_obstruction; pub mod fall_damage; pub mod fluid; diff --git a/docs/collision-geometry.md b/docs/collision-geometry.md index 7be42378c8..bc8fc6050e 100644 --- a/docs/collision-geometry.md +++ b/docs/collision-geometry.md @@ -737,3 +737,21 @@ predicate combinations, sound/event modes, differing support blocks, nonfinite numbers, mutations and provider failures. Targeted Clippy passes. Sound callbacks are controlled and the crystal-sound predicate is false in this gate; registered sound selection, audio playback and full movement integration remain pending. + + +`entity_move` sequences native movement phases: no-physics/piston early exits, +stuck multiplier reset, edge backoff, collision resolution, fall-reset clipping, +position/recording writes, collision/ground flags, landing, removal, restitution, +movement emission and block speed multiplication. `MovementHistory` retains at +most 100 records by merging the oldest pair and dropping their original-axis +movement metadata. Callbacks can change fields read by later phases. + +`python3 tools/test_entity_move_parity.py` invokes actual `Entity.move` with native +shape resolution and zero restitution. The +[movement lifecycle report](../benchmarks/rust-entity-move-2026-10-07/scoped-parity.json) +records **543,653 matching lines per JAR**, covering signed zero, tiny/nonfinite +movement, collisions, early exits, recording cap, authority guards, landing +mutations/removal and failures after partial writes. Targeted Clippy passes. +This gate controls piston/edge decisions, ground support, ray hits and landing +callbacks; step height is zero and non-passenger emission is tested separately. +Binding all phases to a live entity/world and player travel remains pending. diff --git a/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaEntityMoveOracle.java b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaEntityMoveOracle.java new file mode 100644 index 0000000000..8c40f26c46 --- /dev/null +++ b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaEntityMoveOracle.java @@ -0,0 +1,92 @@ +package dev.horizonlab.parity; +import com.google.gson.*; +import java.io.*; +import java.lang.reflect.*; +import java.nio.file.*; +import java.util.*; +import net.minecraft.core.*; +import net.minecraft.tags.*; +import net.minecraft.world.entity.*; +import net.minecraft.world.level.*; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.state.*; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.phys.*; +import net.minecraft.world.phys.shapes.*; +/** Actual Entity.move with controlled lifecycle providers and native collision resolution. */ +public final class VanillaEntityMoveOracle { + static final class Stop extends RuntimeException {} + static Field position,stuck,history,removal; + static double number(JsonElement v){return Double.longBitsToDouble(Long.parseUnsignedLong(v.getAsString(),16));} + static float single(JsonElement v){return Float.intBitsToFloat(Integer.parseUnsignedInt(v.getAsString(),16));} + static Vec3 vector(JsonElement v){var a=v.getAsJsonArray();return new Vec3(number(a.get(0)),number(a.get(1)),number(a.get(2)));} + static String vec(String label,Vec3 v){return String.format("%s %016x %016x %016x",label,Double.doubleToLongBits(v.x),Double.doubleToLongBits(v.y),Double.doubleToLongBits(v.z));} + static String box(String label,AABB b){return String.format("%s %016x %016x %016x %016x %016x %016x",label,Double.doubleToLongBits(b.minX),Double.doubleToLongBits(b.minY),Double.doubleToLongBits(b.minZ),Double.doubleToLongBits(b.maxX),Double.doubleToLongBits(b.maxY),Double.doubleToLongBits(b.maxZ));} + static String pos(BlockPos p){return p.getX()+" "+p.getY()+" "+p.getZ();} + static void write(Field f,Object o,Object v){try{f.set(o,v);}catch(Exception e){throw new AssertionError(e);}} + static final class Data { + JsonObject f;PrintWriter out;int calls;Vec3 velocity;boolean grounded;Actor actor;State state;List shapes; + boolean flag(String n){return f.get(n).getAsBoolean();} + void query(String s){out.println(s);if(calls++==f.get("fail").getAsInt())throw new Stop();} + } + static final class BounceBlock extends Block { + Data d;BounceBlock(){super(BlockBehaviour.Properties.of());} + @Override public float getBounceRestitution(){d.query("block-bounce");return 0;} + } + static final class State extends BlockState { + Data d;BounceBlock block;State(){super(null,null,null);} + @Override public Block getBlock(){return block;} + @Override public boolean is(TagKey tag){if(tag!=BlockTags.SUPPRESSES_BOUNCE)throw new AssertionError();d.query("block-suppresses");return false;} + } + static final class Border extends WorldBorder { + Data d;@Override public boolean isInsideCloseToBorder(Entity e,AABB b){d.query(box("border-test",b));return false;} + } + static final class Level extends net.minecraft.server.level.ServerLevel { + Data d;Border border;Level(){super(null,null,null,null,null,null,false,0L,List.of(),false);} + @Override public boolean isClientSide(){d.query("client");return d.flag("client");} + @Override public BlockState getBlockState(BlockPos p){d.query("block "+pos(p));return d.state;} + @Override public WorldBorder getWorldBorder(){d.query("border");return border;} + @Override public List getEntityCollisions(Entity e,AABB b){d.query(box("entities",b));return List.of();} + @Override public Iterable getBlockCollisions(Entity e,AABB b){d.query(box("blocks",b));return d.shapes;} + @Override public BlockHitResult clip(ClipContext c){d.query(vec("clip-from",c.getFrom()));d.query(vec("clip-to",c.getTo()));return d.flag("clip_hit")?new BlockHitResult(c.getTo(),Direction.UP,BlockPos.ZERO,false):BlockHitResult.miss(c.getTo(),Direction.UP,BlockPos.ZERO);} + } + static final class Actor extends Marker { + Data d;Actor(){super(null,null);}void configure(Level level){setLevel(level);} + @Override public void setPos(double x,double y,double z){var next=new Vec3(x,y,z);d.query(vec("set-position",next));setBoundingBox(getBoundingBox().move(next.subtract(position())));write(position,this,next);} + @Override public void recordMovement(MoverType mover,Vec3 v){d.query(vec("record "+mover.ordinal(),v));} + @Override protected Vec3 limitPistonMovement(Vec3 v){d.query(vec("piston",v));return vector(d.f.get("piston_result"));} + @Override public Vec3 getDeltaMovement(){d.query("velocity");return d.velocity;} + @Override public void setDeltaMovement(Vec3 v){d.query(vec("set-velocity",v));d.velocity=v;} + @Override protected Vec3 maybeBackOffFromEdge(Vec3 v,MoverType mover){d.query(vec("edge "+mover.ordinal(),v));return d.flag("edge_override")?vector(d.f.get("edge_result")):v;} + @Override public float maxUpStep(){return 0;} + @Override public boolean isDescending(){return false;} + @Override protected boolean isLocalClientAuthoritative(){d.query("local-client-authority");return d.flag("local_client");} + @Override public boolean isClientAuthoritative(){d.query("client-authority");return d.flag("client_authority");} + @Override public MoveSimulationType getMoveSimulationType(){d.query("simulation");return MoveSimulationType.SERVER_AND_CLIENT;} + @Override public boolean onGround(){d.query("ground");return d.grounded;} + @Override public void setOnGroundWithMovement(boolean below,boolean horizontal,Vec3 v){d.query(vec("ground-set "+below+" "+horizontal,v));d.grounded=below;horizontalCollision=horizontal;} + @Override protected boolean isHorizontalCollisionMinor(Vec3 v){d.query(vec("minor",v));return d.flag("minor_result");} + @Override public BlockPos getOnPosLegacy(){d.query("legacy");return new BlockPos(17,-64,-33);} + @Override protected void checkFallDamage(double dy,boolean ground,BlockState s,BlockPos p){d.query(String.format("fall %016x %s %s",Double.doubleToLongBits(dy),ground,pos(p)));if(d.flag("fall_mutates")){fallDistance=3;d.velocity=new Vec3(17,18,19);horizontalCollision=d.flag("mutated_horizontal");verticalCollision=d.flag("mutated_vertical");verticalCollisionBelow=d.flag("mutated_below");}if(d.flag("fall_removes"))write(removal,this,Entity.RemovalReason.DISCARDED);} + @Override public void resetFallDistance(){d.query("reset-fall");fallDistance=0;} + @Override public boolean isSuppressingBounce(){d.query("suppress");return false;} + @Override protected double getEntityBounciness(){d.query("entity-bounce");return 0;} + @Override protected double getEffectiveGravity(){d.query("gravity");return .08;} + @Override protected MovementEmission getMovementEmission(){d.query("emission");return d.flag("emits")?MovementEmission.ALL:MovementEmission.NONE;} + @Override public boolean isPassenger(){d.query("passenger");return true;} + @Override protected float getBlockSpeedFactor(){d.query("speed");return single(d.f.get("speed"));} + } + public static void main(String[] args)throws Exception { + net.minecraft.SharedConstants.tryDetectVersion();net.minecraft.server.Bootstrap.bootStrap();var allocator=Class.forName("sun.misc.Unsafe");var field=allocator.getDeclaredField("theUnsafe");field.setAccessible(true);var unsafe=field.get(null);var allocate=allocator.getMethod("allocateInstance",Class.class); + position=Entity.class.getDeclaredField("position");position.setAccessible(true);stuck=Entity.class.getDeclaredField("stuckSpeedMultiplier");stuck.setAccessible(true);history=Entity.class.getDeclaredField("movementThisTick");history.setAccessible(true);removal=Entity.class.getDeclaredField("removalReason");removal.setAccessible(true); + var record=Class.forName("net.minecraft.world.entity.Entity$Movement");var constructor=record.getDeclaredConstructor(Vec3.class,Vec3.class,Vec3.class);constructor.setAccessible(true);var from=record.getDeclaredMethod("from");from.setAccessible(true);var to=record.getDeclaredMethod("to");to.setAccessible(true);var original=record.getDeclaredMethod("axisDependentOriginalMovement");original.setAccessible(true);Path dir=Path.of(args[1]); + try(var out=new PrintWriter(Files.newBufferedWriter(dir.resolve(args[0]+".txt")));var lines=Files.lines(dir.resolve("inputs.txt"))){for(var line:(Iterable)lines::iterator){ + var f=JsonParser.parseString(line).getAsJsonObject();var d=new Data();d.f=f;d.out=out;d.velocity=vector(f.get("velocity"));d.grounded=d.flag("ground");var actor=(Actor)allocate.invoke(unsafe,Actor.class);actor.d=d;d.actor=actor;var level=(Level)allocate.invoke(unsafe,Level.class);level.d=d;actor.configure(level);var border=(Border)allocate.invoke(unsafe,Border.class);border.d=d;level.border=border;var state=(State)allocate.invoke(unsafe,State.class);state.d=d;var block=(BounceBlock)allocate.invoke(unsafe,BounceBlock.class);block.d=d;state.block=block;d.state=state; + var p=vector(f.get("position"));position.set(actor,p);actor.setBoundingBox(new AABB(p.x-.3,p.y,p.z-.3,p.x+.3,p.y+1.8,p.z+.3));stuck.set(actor,vector(f.get("stuck")));actor.noPhysics=d.flag("no_physics");actor.fallDistance=number(f.get("distance"));actor.horizontalCollision=d.flag("horizontal");actor.verticalCollision=d.flag("vertical");actor.verticalCollisionBelow=d.flag("below");actor.minorHorizontalCollision=d.flag("minor");removal.set(actor,d.flag("removed")?Entity.RemovalReason.DISCARDED:null); + var movements=new ArrayDeque();for(int i=0;i();for(var v:f.getAsJsonArray("shapes")){var a=v.getAsJsonArray();d.shapes.add(Shapes.box(number(a.get(0)),number(a.get(1)),number(a.get(2)),number(a.get(3)),number(a.get(4)),number(a.get(5))));} + try{actor.move(MoverType.values()[f.get("mover").getAsInt()],vector(f.get("movement")));out.println("ok");}catch(Stop e){out.println("error");} + out.println(vec("position",actor.position()));out.println(vec("velocity-final",d.velocity));out.println(vec("stuck",(Vec3)stuck.get(actor)));out.println(box("bounds",actor.getBoundingBox()));out.printf("flags %s %s %s %s %s %s %016x%n",actor.horizontalCollision,actor.verticalCollision,actor.verticalCollisionBelow,actor.minorHorizontalCollision,d.grounded,actor.isRemoved(),Double.doubleToLongBits(actor.fallDistance));out.println("history "+movements.size());for(var r:movements){out.println(vec("from",(Vec3)from.invoke(r)));out.println(vec("to",(Vec3)to.invoke(r)));var v=(Optional)original.invoke(r);out.println(v.isEmpty()?"original none":vec("original",(Vec3)v.get()));}out.println("queries "+d.calls); + }} + } +} diff --git a/tools/test_entity_move_parity.py b/tools/test_entity_move_parity.py new file mode 100644 index 0000000000..662ca393f9 --- /dev/null +++ b/tools/test_entity_move_parity.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Compare Entity.move lifecycle ordering, state writes and recording with vanilla.""" +import argparse +import json +import os +from pathlib import Path +import platform +import subprocess +import random +import struct +import math +try: + from . import lab, test_rust_parity as base +except ImportError: + import lab + import test_rust_parity as base + + +def corpus(): + import itertools + def bits(v):return struct.pack('>d',float(v)).hex() + def single(v):return struct.pack('>f',float(v)).hex() + def row(**kw): + f=dict(movement=list(map(bits,[.4,-.2,.3])),position=list(map(bits,[.5,1.,.5])),velocity=list(map(bits,[.1,-.8,.2])),stuck=list(map(bits,[0,0,0])),piston_result=list(map(bits,[.25,0,0])),edge_result=list(map(bits,[.1,0,.2])),edge_override=False,no_physics=False,mover=0,client=False,client_authority=False,local_client=False,ground=True,horizontal=False,vertical=False,below=False,minor=False,minor_result=True,removed=False,distance=bits(5),clip_hit=False,fall_mutates=False,fall_removes=False,mutated_horizontal=False,mutated_vertical=False,mutated_below=False,emits=False,speed=single(.4),history=0,shapes=[],fail=-1) + f.update(kw);return f + scenes=[[],[[0,0,0,1,1,1]],[[1,0,0,2,3,1]],[[0,3,0,1,4,1]],[[0,0,1,1,3,2]],[[0,0,0,1,.9375,1]],[[0,0,0,1,1,1],[1,0,0,2,3,1],[0,0,1,1,3,2]]] + motions=[[0,0,0],[-0.,0,0],[1e-200,-1e-200,1e-200],[1e-4,-1e-4,1e-4],[.4,-.2,.3],[2,-1,2],[-2,-5,-2],[0,9,0],[1e-5,0,-1e-5],[0,0,math.nan],[0,math.nan,0],[math.inf,0,0]] + for shapes,motion,mover,client,authority,local in itertools.product(scenes,motions,range(5),[False,True],[False,True],[False,True]): + yield row(shapes=[[bits(v) for v in b] for b in shapes],movement=list(map(bits,motion)),mover=mover,client=client,client_authority=authority,local_client=local,emits=True) + for mover,no_physics,stuck,piston,fail in itertools.product(range(5),[False,True],[[0,0,0],[1e-200,0,0],[.1,.2,.3],[math.nan,1,1],[math.inf,0,0]],[[0,0,0],[-0.,0,0],[0,-0.,0],[0,0,-0.],[.25,0,0]],range(-1,12)): + yield row(mover=mover,no_physics=no_physics,stuck=list(map(bits,stuck)),piston_result=list(map(bits,piston)),fail=fail,horizontal=True,vertical=True,below=True,minor=True) + for shapes,flags,fail in itertools.product(scenes,range(32),range(-1,38)): + yield row(shapes=[[bits(v) for v in b] for b in shapes],fall_mutates=True,fall_removes=bool(flags&1),mutated_horizontal=bool(flags&2),mutated_vertical=bool(flags&4),mutated_below=bool(flags&8),clip_hit=bool(flags&16),movement=list(map(bits,[2,-2,2])),fail=fail) + for count,fail,no_physics in itertools.product([0,1,98,99,100],range(-1,30),[False,True]):yield row(history=count,fail=fail,no_physics=no_physics) + rng=random.Random(0x4d4f56454d454e54) + values=[-math.inf,-1e30,-2.,-0.,0.,1e-200,1e-7,1.,1e30,math.inf,math.nan] + for _ in range(6000): + yield row(movement=list(map(bits,rng.choice(motions))),shapes=[[bits(v) for v in b] for b in rng.choice(scenes)],mover=rng.randrange(5),stuck=[bits(rng.choice([0,.5,1,math.nan])) for _ in range(3)],piston_result=[bits(rng.choice(values)) for _ in range(3)],edge_override=bool(rng.randrange(2)),edge_result=[bits(rng.choice(values)) for _ in range(3)],velocity=[bits(rng.choice(values)) for _ in range(3)],no_physics=bool(rng.randrange(2)),client=bool(rng.randrange(2)),client_authority=bool(rng.randrange(2)),local_client=bool(rng.randrange(2)),ground=bool(rng.randrange(2)),horizontal=bool(rng.randrange(2)),vertical=bool(rng.randrange(2)),below=bool(rng.randrange(2)),minor=bool(rng.randrange(2)),minor_result=bool(rng.randrange(2)),removed=bool(rng.randrange(2)),distance=bits(rng.choice(values)),clip_hit=bool(rng.randrange(2)),fall_mutates=bool(rng.randrange(2)),fall_removes=bool(rng.randrange(2)),mutated_horizontal=bool(rng.randrange(2)),mutated_vertical=bool(rng.randrange(2)),mutated_below=bool(rng.randrange(2)),emits=bool(rng.randrange(2)),speed=single(rng.choice(values)),fail=rng.randrange(-1,34)) + + +def main(): + parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--prepare',action='store_true');args=parser.parse_args() + build=lab.LOCAL/'rust-entity-move-parity';build.mkdir(parents=True,exist_ok=True) + report_path=build/'report.json';report_path.unlink(missing_ok=True) + if args.prepare:lab.prepare() + lock,jars,libraries=base.verify_artifacts() + oracle=lab.ROOT/'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaEntityMoveOracle.java' + sources=[oracle,Path(__file__),lab.ROOT/'tools/test_rust_parity.py',lab.ROOT/'tools/lab.py',lab.ROOT/'Cargo.toml',lab.ROOT/'Cargo.lock',lab.ROOT/'crates/horizon-core/Cargo.toml',lab.ROOT/'crates/horizon-world/Cargo.toml',*sorted((lab.ROOT/'crates/horizon-core/src').rglob('*.rs')),*sorted((lab.ROOT/'crates/horizon-world/src').rglob('*.rs')),lab.ROOT/'crates/horizon-world/examples/entity_move_fixture.rs'] + tested_sources=base.source_hashes(sources) + classes=build/'classes';classes.mkdir(exist_ok=True) + def run(command,name): + with (build/(name+'.log')).open('w',encoding='utf-8') as log:base.run(list(map(str,command)),stdout=log,stderr=subprocess.STDOUT) + dependency_path=os.pathsep.join(map(str,libraries)) + run(['javac','--release',lock['minecraft']['java_major'],'-proc:none','-cp',str(jars['server'])+os.pathsep+dependency_path,'-d',classes,oracle],'compile-java') + def java(side,mode,path): + run(['java','-Xmx512M','-cp',os.pathsep.join([str(classes),str(jars[side]),dependency_path]),'dev.horizonlab.parity.VanillaEntityMoveOracle',mode,path],side+'-'+mode) + fixtures=list(corpus());inputs=build/'inputs.txt';inputs.write_text(''.join(json.dumps(value,separators=(',',':'))+'\n' for value in fixtures),encoding='utf-8') + fixture_paths=[inputs];tested_fixtures=base.source_hashes(fixture_paths) + run(['cargo','build','--release','-p','horizon-world','--example','entity_move_fixture'],'compile-rust') + metadata=json.loads(subprocess.check_output(['cargo','metadata','--no-deps','--format-version','1'],cwd=lab.ROOT,text=True)) + binary=Path(metadata['target_directory'])/'release/examples'/('entity_move_fixture.exe' if os.name=='nt' else 'entity_move_fixture') + run([binary,build],'rust');references=[] + for side,jar in jars.items(): + java(side,side,build);reference=build/f'{side}.txt';matched=base.compare(reference,build/'rust.txt') + references.append({'side':side,'jar_sha256':lab.digest(jar,'sha256'),'output_sha256':lab.digest(reference,'sha256'),'matched_output_lines':matched}) + print(f'{side}: {matched:,} entity-move output lines match Rust',flush=True) + base.ensure_sources_unchanged(sources,tested_sources);base.ensure_sources_unchanged(fixture_paths,tested_fixtures) + report={'schema':1,'minecraft':lock['minecraft']['version'],'manifest_sha1':lock['minecraft']['manifest_sha1'],'platform':platform.platform(),'java':subprocess.check_output(['java','--version'],text=True).splitlines()[0],'rustc':subprocess.check_output(['rustc','--version'],text=True).strip(),'fixtures':len(fixtures),'sources_sha256':tested_sources,'fixtures_sha256':tested_fixtures,'rust_output_sha256':lab.digest(build/'rust.txt','sha256'),'references':references,'scope':'Actual Entity.move with native shape collision resolution (zero step height), native zero-restitution handling and final authority checks. Compares no-physics and piston early exits including signed zero, stuck multiplier reset, edge callback, fall-reset clip endpoints, movement recording cap, collision flag writes, ground/fall callback ordering, removal, passenger emission guard, speed multiplication and failures after partial state changes. Providers control piston/edge decisions, ground support, ray hits, landing effects and sound policy; live world integration and non-passenger movement emission remain separate work.'} + lab.write_json(report_path,report);print(f'Scoped entity move parity passed: {report_path}') + + +if __name__=='__main__':main()