From 4a2fcedbd38ef4ad69b652a57b47c25b866a58aa Mon Sep 17 00:00:00 2001 From: Ludovic Marie <136255825+ludovic111@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:51:15 +0200 Subject: [PATCH] Bind registered attribute refresh to native entity update consequences --- .../scoped-parity.json | 319 ++++++++++ .../live_attribute_updates_fixture.rs | 598 ++++++++++++++++++ crates/horizon-world/src/lib.rs | 1 + .../src/live_attribute_updates.rs | 85 +++ docs/rust-parity.md | 24 + .../VanillaLiveAttributeUpdatesOracle.java | 197 ++++++ tools/test_live_attribute_updates_parity.py | 194 ++++++ 7 files changed, 1418 insertions(+) create mode 100644 benchmarks/rust-live-attribute-updates-2026-10-07/scoped-parity.json create mode 100644 crates/horizon-world/examples/live_attribute_updates_fixture.rs create mode 100644 crates/horizon-world/src/live_attribute_updates.rs create mode 100644 mods/horizon/src/test/java/dev/horizonlab/parity/VanillaLiveAttributeUpdatesOracle.java create mode 100644 tools/test_live_attribute_updates_parity.py diff --git a/benchmarks/rust-live-attribute-updates-2026-10-07/scoped-parity.json b/benchmarks/rust-live-attribute-updates-2026-10-07/scoped-parity.json new file mode 100644 index 000000000..673c6f650 --- /dev/null +++ b/benchmarks/rust-live-attribute-updates-2026-10-07/scoped-parity.json @@ -0,0 +1,319 @@ +{ + "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": 9284, + "attributes": 40, + "entity_suppliers": 93, + "fastutil_sha256": { + ".local/minecraft/26.3/libraries/server/it/unimi/dsi/fastutil/8.5.18/fastutil-8.5.18.jar": "9094ae67d01d0ad246f886f11ad557fc2e79c72cbf3feed83e1512a8ae90a74a" + }, + "sources_sha256": { + "mods/horizon/src/test/java/dev/horizonlab/parity/VanillaLiveAttributeUpdatesOracle.java": "23ef1bc52c59018258126197f22ef98cdcd4dd4db852be0bb60eeac3ab9f9b1f", + "mods/horizon/src/test/java/dev/horizonlab/parity/VanillaEntityAttributeOracle.java": "2ad3190a9798ab54bd12e88573028d0ca6a204e2f8a92018aae98b22335213bf", + "mods/horizon/src/test/java/dev/horizonlab/parity/VanillaLiveAttributesOracle.java": "e6a6ab15c6ac2594e41e7830de480a01844d54d171913dd7bfe07881d10226c1", + "tools/test_live_attribute_updates_parity.py": "ea80fc255e14e25fe0e35dfa8037675e3e561e3e72b597b6c548d7ddc6416292", + "tools/test_rust_parity.py": "29d3db08005873528894b45ca10dcd6278753650473b3eb574e53e06ede40ec6", + "tools/lab.py": "d00bf107fa01b6c579734a082c3ca4ade3018f0d6851d46844acf17574f66e64", + "Cargo.toml": "3f93319c1eb70dbd7ba25cd74ef343fbf35c52c7c4752f955605c58699cbff9d", + "Cargo.lock": "6884c7209f007ab9ee742b9c3c73828cfa1c6753d35b5fe25a87cab5955b33f8", + "crates/horizon-core/Cargo.toml": "e21b502c949da93cf67826592459b687a8cbf737ef92fee514e3ed2cc7cd27ad", + "crates/horizon-world/Cargo.toml": "18bbad377a8047e4d4ecd0fc87d83d46d1c0f1ce2b8c47bbe64633e7b4420b57", + "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/active_effect_tick.rs": "88f19cfc3532c47a58c646bb3e9d99ff239a900752302b6a444dda3013e95cbf", + "crates/horizon-world/src/active_effects.rs": "daaf352a69511a7d69fb329997f783b86585e486e0379c84ab0a55641771646c", + "crates/horizon-world/src/attribute_health.rs": "1420a80787d10dd2e2954a3ee6193ff313289d00c7c78ca26ece8bafa21f4380", + "crates/horizon-world/src/attribute_map.rs": "3d871cd530f5c035e0753f3ba065c2b9c5b49cdee75d4119f7dd584b22af67e6", + "crates/horizon-world/src/attribute_updates.rs": "4cb322d75a50d222298994607097ba7f7e6674021128fa02a98a74c057ad05f1", + "crates/horizon-world/src/attributes/entity.rs": "64ac99682a3aa06b1fb94b776b47794234c76b1cf70f1f54933d51e545e69279", + "crates/horizon-world/src/attributes/live.rs": "8198c45134f69135d48ef203e71b6e702529568fd1ed9174d0aa843570bbf9e7", + "crates/horizon-world/src/attributes/mod.rs": "5c2fcbbde58c19261874d227ea98332f00336c9fa53446da2e9adb6aa64a5e5a", + "crates/horizon-world/src/attributes/operation_map.rs": "fc5bc8921d78ea6721a2eaa915e67483e2a33683927467bdba24f0492356ad97", + "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/blocking_item_damage.rs": "afea3cb5761e8b82ee4d9bb4ef3c6fd10182a324dbaec99e6c9ebfac255dda09", + "crates/horizon-world/src/blocking_rules.rs": "83c5e2105cec56a90182ce582e1c67ade2b189be6de1a34d08035000cc07c492", + "crates/horizon-world/src/bundle_weight.rs": "a0e7b0587ab5d46cd55588f25d55a088b7a4725099c13ba6df146957be36014c", + "crates/horizon-world/src/chunk.rs": "795895284d6e615bcb2082fceacdfe7025c152fb2934bb43f598f17a00ab46f1", + "crates/horizon-world/src/chunk_status.rs": "dbb8583cfa775203c544d24d69e03720385428f15a267a561356c21f5025e9c7", + "crates/horizon-world/src/climbing.rs": "6cd2f919872ff0b726effddaa57eccb805b8eae43f9d01144716010853891879", + "crates/horizon-world/src/collision.rs": "a749794d5c27ccd6c0d964ad02f9dc31d49d9f4fe4b7366cd4bd8155e681b00b", + "crates/horizon-world/src/collision_context.rs": "2176c7034e07f547510a8327cdad9d3b63f26e4754032b76b7d5fe13166c333c", + "crates/horizon-world/src/combat_rules.rs": "01131f70d1acdd96a910e7f96deaa12e51a1e3bb9208085f04e3ec7b657d3cd4", + "crates/horizon-world/src/component_durability.rs": "37a46e184f95bc5027e2b9af72ff52f729ad03601d08e1827f181fca383a35af", + "crates/horizon-world/src/component_patch.rs": "f6f8992f241dfb25e012a06a7ba3fecab8d958686dddd096595a4806e56f0ab3", + "crates/horizon-world/src/compression.rs": "4db73e6ca9a0d552d415ce60e7e88b89c9cf2b7d060a669ff8328f518316e101", + "crates/horizon-world/src/consumable.rs": "ba3575cb85866cb940f33a1ac86455d71cf0cc6395b848588a8ee1fe33f5c326", + "crates/horizon-world/src/consume_effect.rs": "03f2aac6879ce133347d42f6f6667d62c06d956ff9faac5dc14182a788b6a236", + "crates/horizon-world/src/damage_immunity.rs": "fd39c41124d694e755cf7faadcf93c636df4042c78ec345092a856b5f952dbcf", + "crates/horizon-world/src/damage_reduction.rs": "f5a599ab11e077f526237018aeda803ea3c420c5b35c78ae9d85e32267e23bf3", + "crates/horizon-world/src/damage_source.rs": "24945e397a926d34695652980923609e4723e02fe6b6e56d4aaf673e45d759f7", + "crates/horizon-world/src/dynamic_support.rs": "5996bc1c436fb6215d8fc3d0bb4de5dd9b966d842d432580d624195771697fb8", + "crates/horizon-world/src/edge_backoff.rs": "5259d7aa44e64f88351d1556c44dd0edcad3b400a099c659755792efba7b5773", + "crates/horizon-world/src/effect_attribute_modifiers.rs": "0bc7ab63d0f3f34a7133fe20364cae0ec1fdbd3773cba90faa1f08e0d63a5646", + "crates/horizon-world/src/effect_notifications.rs": "ea66bfa010d6d44aee97b9672902f41cd99d0d80239f41c6d4eae5c9a9472d3d", + "crates/horizon-world/src/entity_collision.rs": "09f2fd7f6cd56fc282f468ecfa765a94506929e9684bd3a5c90123e5b51cd627", + "crates/horizon-world/src/entity_fluid.rs": "02991040ae879a8914f09913128a3506e9e7a1ca33ffc0a22b5729a168346600", + "crates/horizon-world/src/entity_fluid_tick.rs": "4f9e070b88a73c98a4e2e659360ddd6edee636bca29377f1ea9011fd5a556f64", + "crates/horizon-world/src/entity_move.rs": "a40a50d3e841625a75d4cfbb7c2630ab5a62a574456567e7234f401ae8441b32", + "crates/horizon-world/src/entity_obstruction.rs": "a4b28a19bf83b23611c59d94535c488db37e4aee24b22c973c545910894a809f", + "crates/horizon-world/src/extra_items.rs": "6eba0a0c795d5fd841d31471708e78324b4852adebb2e6ed700dcc0caab668cd", + "crates/horizon-world/src/fall_damage.rs": "8c28e2933493569fe05dea22332024287d28b0746b514dd465681040b003296b", + "crates/horizon-world/src/fluid.rs": "a0a1f34ba94d8b59dc975becee8952960764d67ae08cf2d6a8bcd40a614b15c4", + "crates/horizon-world/src/fluid_box.rs": "b586af72b1c04a9354fe66205c28dc8b619e205ab9ea19a68232722e4b6e96c1", + "crates/horizon-world/src/fluid_flow.rs": "7c7e486b5c565df3602799648b37b879bcca51414337f607962d6c7951b86d05", + "crates/horizon-world/src/fluid_occupancy.rs": "26037579caeef6c8a5a2bde2c3d142d278558b76d77ebff401cee6e35f2ea961", + "crates/horizon-world/src/fluid_shape.rs": "28bb65a72f5bd074a354ba38a1b1707fcb0947537977a11540bba0adb85995c2", + "crates/horizon-world/src/food.rs": "3cf416fa632a5555a8116bb528a6e80bcb5b6b754b0748b4959ed310102418d3", + "crates/horizon-world/src/food_properties.rs": "0926fa3f3bc3d0608c45d02f564355825ca3c3526a82bf9c1375c7a191fa5e12", + "crates/horizon-world/src/food_storage.rs": "58c433801a285efcc973638c11d694a41f85dc96703a3a7ce8d3673f3eb31d52", + "crates/horizon-world/src/free_space.rs": "913536bf4c85ddeeb378d76d1b5110b4175f5688e1a749f32fe6a6d86fbb478e", + "crates/horizon-world/src/ground_support.rs": "3e371d74ecede4d30beef0a7e8a2ee33463584a5b1ed3d852b394efcce1ffbe0", + "crates/horizon-world/src/heightmap.rs": "a5edef082b5af40d814421ea17e129b3b06d70abee6434ed263e4bba58571698", + "crates/horizon-world/src/identifier.rs": "37c921889c4f5d91f7c09ab2ee68d1a5e5a3c4fb1c49b4b189c987b4615fd2d6", + "crates/horizon-world/src/interaction_shapes.rs": "f321284ddab440a394372b6dedc8b5cc29a653c0745251a9e96c650f21c33dea", + "crates/horizon-world/src/inventory_pickup.rs": "ea830c7916e7f2d95c6db7ef48be1e876e6ebafcb438e47fc72f9a5bd7c37d07", + "crates/horizon-world/src/inventory_tick.rs": "293a3d57a1689337cfd58e5a73dcdbed4cb360f94d98ff5458f0df92de387365", + "crates/horizon-world/src/inventory_use.rs": "6a6c2ecc73c83dda4cd798c0304df12ace6096eb110d899f4f0c7ffd852bf001", + "crates/horizon-world/src/item_after_use.rs": "2b6598ce5b7b304dea2c8057b4a1eb6530264ac4d0546c3da0c76deed9609da5", + "crates/horizon-world/src/item_blocking.rs": "f6eee0d6d177cc3148960122327ba1b459a2f0e85241c1d9a3e40f0b8c169296", + "crates/horizon-world/src/item_cooldowns.rs": "7b625868509ab3c51e72db0ac714bb0e7ec5e2610d37271005a1a737f4d14431", + "crates/horizon-world/src/item_durability.rs": "9fdf835e5c7bd0e1dd63ef2ba0076b770cdd4d6d14cdcd11de27690f0702f94e", + "crates/horizon-world/src/item_particles.rs": "861b14f65adb9b14f8158fe9f0b7128beac3df8107b1337c31b8b9b49434ba3f", + "crates/horizon-world/src/item_stack_refs.rs": "18d3ca85cac0daf59200857513761359e5690f719cb3f7f64ae552f0c4169ea3", + "crates/horizon-world/src/item_stack_storage.rs": "b4dcb83dcaa346848c5484161feb3e2cf08e83f7793d7ed83be25b14b9d12884", + "crates/horizon-world/src/item_template.rs": "dc80025215d1867e656924ea2a9d273aacbcaea21293d986ba42d0004ee93b69", + "crates/horizon-world/src/item_use.rs": "b1c79428986efbf9dd5ede703a39eb387fb6dcc7d9950f0e49243a353a5b8070", + "crates/horizon-world/src/item_use_catalog.rs": "a9b53d2e816583239a75c3c61e78dacc5e3b27dee595be34b1c7fa169f17a571", + "crates/horizon-world/src/item_use_lifecycle.rs": "4639b70440cf811e79de09c48a3e035143fcb6be8a64c2f61b10ae55a2f133b7", + "crates/horizon-world/src/item_use_start.rs": "a84092dfb8b8f0a89d0928bb2f8ac252bd9bfd3f8d74fce1a7f7a5b3c3f4f50b", + "crates/horizon-world/src/java_hash_map.rs": "955502e696d7b0b33bae3ec485c8eb1453a8956116aad68226cf1fc67d60babc", + "crates/horizon-world/src/lib.rs": "00243ac1a80934d54546452a98bc5b37b64a2cafa3a3dfadb82cd5997b46d7cb", + "crates/horizon-world/src/liquid_collision.rs": "0f0a83b04e5a5bc9d98ed05fde4d6584ba69642e93522e726e4cfc325d6fcb1d", + "crates/horizon-world/src/live_attribute_updates.rs": "778171faaaf7077deb0b62c4cc1db16351eba41adb8bb07656ba65e827056199", + "crates/horizon-world/src/living_air.rs": "b2de36bb50bed15e2808fe4a874fcef07f9fea0d2c96385b7b5e8ee9560d4442", + "crates/horizon-world/src/living_damage.rs": "2b448209629b9bf2e678d667ad0d6c7fa22146892dd106b169d96b31b0df12bb", + "crates/horizon-world/src/living_fall.rs": "da0f176f4731e1c86e08bfeee8167742fd1b86380ff0e9304c13c1ce3f1c67ab", + "crates/horizon-world/src/living_fluid.rs": "d7cd914cfb2a1f4876ce0ae95e999bf0ff1af732cce3ab4a3aa2d1ab881c7339", + "crates/horizon-world/src/living_health.rs": "221659f85552424e937d503bba6d8003006b106ced042b32d3a6f6cc23e12ed0", + "crates/horizon-world/src/living_hurt.rs": "844239ad7d15c39629bb3cfc06ebae6d1cff20b7627468f26e0ee8dff3abdcb8", + "crates/horizon-world/src/living_landing.rs": "8bca4eb7977abc2b14fb7174be4ea355352106ea1e76ff5f3ad8696969d9eefd", + "crates/horizon-world/src/living_motion.rs": "56f8c84cfbe205a7a69912d5f76b52ef1efca149c1b6801d4684e25fb179aeb9", + "crates/horizon-world/src/living_use_flags.rs": "826f4fdeba7e208368cbbf910fa408db0afbc6442d842d178a70fae53e0d0ea6", + "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/object_open_map.rs": "b4934a9cc38df93ed206ab0bc3bbc4499acd8e40b41b7def7d47067b7170065b", + "crates/horizon-world/src/object_open_set.rs": "d3e0373bf1b652dd054345d5a8e25ec828e06dd7cad65aada35163c95df4e637", + "crates/horizon-world/src/piston_movement.rs": "7a3fd2549a582b5f3a5c21aa05d67d944d62ca1105856679e100ac5b44e9a52f", + "crates/horizon-world/src/piston_shape.rs": "399c2337103b8e7e0eb5667227c5aa14f9549d6a467b7be7da3d0717f0263500", + "crates/horizon-world/src/player_equipment.rs": "1fa8c53e67624c4641246249d79a800a28a4858c62482bd880b4e3b866b936ef", + "crates/horizon-world/src/player_food.rs": "f188dc110745bcc7acc15e6c521db943b06b404f69cd251bf74a345516070304", + "crates/horizon-world/src/player_health.rs": "79ecb6b4d7a044dc98e5552c53a5ac2724545b1c1cec4d797eb52576728e3480", + "crates/horizon-world/src/player_hurt.rs": "99c9bbd8587ec2dc287806049897b8034d2f5349ad47fc497b70d54148844a36", + "crates/horizon-world/src/player_inventory.rs": "f4f67d8312793f25930626c4ac06db688fcf2e2292d6675071d105fb1708d952", + "crates/horizon-world/src/player_travel.rs": "6841a677a900495eb6223031ef3ebe8d859bb98908dda0e2209e5f28839dc593", + "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": "97cc950a9a4ba387b78e368f36d595f8664ab7730d132900d220ce23b92826ea", + "crates/horizon-world/src/registry_fall_ray.rs": "52935436132df505691144840dcbd858b15d00d8305997284e3a542ecf6bc0dc", + "crates/horizon-world/src/registry_fluid.rs": "b8959c05850cfeaaad1e4de8ca7c7d0d6ac56da9b2c1de3ebefa5871a11ed35e", + "crates/horizon-world/src/retrogen.rs": "6efd36a6293756688c1990a7965bd65609d7c5e2bf2a5f4f01d54932d2b9386b", + "crates/horizon-world/src/server_player_combat.rs": "a9743971581af86d926eeba42004d932caf83382c470dd98bf613cb9c417db58", + "crates/horizon-world/src/server_player_damage.rs": "828a4649fec3e9bda2dfe20aced8c95922c697321de2be4127bd4e5712ecbb7c", + "crates/horizon-world/src/simple_container.rs": "0fedbc7e974b1db79b88bd6aff977654189a5d553cf42852fe8bf0885cf90b30", + "crates/horizon-world/src/status_effect.rs": "588bfcf4d93f50be86cb89545bd5c431edbc71771f704ff55cb52906b1964c37", + "crates/horizon-world/src/stored_consume.rs": "621a52c9fba447c393e561f881a16cf1f97d336e3a0ce493725ef51e2db12ffd", + "crates/horizon-world/src/stored_effect_attributes.rs": "c79f6a52d3cfee1a5288225bf8fa91a7bf3ea8cd21faea9f6dc748ecefc6309a", + "crates/horizon-world/src/suffocation.rs": "bed219dd5bbab6263b98fbe3d0eb512cb96c52ae847210efe49514d05f22f11e", + "crates/horizon-world/src/swimming.rs": "49d2dae13c109b37ff832cde98a857944ff45247b864a527b9c4f03ecca7a7b7", + "crates/horizon-world/src/synced_floats.rs": "0152f2752bafaf68769d56dbc88494a976496a680c6b113d6b48a446158ba072", + "crates/horizon-world/src/synced_values.rs": "adffa15026497d6671ffd12e5e17df9329c93f0e022700cdfebb80ab494382ae", + "crates/horizon-world/src/teleport_consume.rs": "d6a53bbb6ac4dbbd4c0591d0646017898a95beb8ef093dc5d81b9833963be6c3", + "crates/horizon-world/src/tick_data.rs": "f8d04a69ea263f4168bce33f92e5b4ee2fe07d7caf50a1621c6b3ef6eb2479f3", + "crates/horizon-world/src/use_remainder.rs": "f555335afeca7970888131a6da2f5a614a73253b631185d1ba804593a349535e", + "crates/horizon-world/src/view_direction.rs": "e18120ae26dc5cf702b29fc4c8487088bab133c8ce5abce01226833fafb0d5a4", + "crates/horizon-world/src/water_splash.rs": "e0461ae1b2e8c851cca6e3251a188334293292201d7f80dc9e7d60fd1815f4d1", + "crates/horizon-world/src/world_movement.rs": "f8fe9e34f2395d2a04c839d1c8ba78e09ce493c26de6f6e4e237b9c19c061da5", + "crates/horizon-world/src/world_ray.rs": "879bdbbe12f56deb705cc183933431489e840b71eade40c33391eed5949c2e61", + "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": "74e4110fb19c4c4047192fd65c38015b8afc7f9fc50404d75380b5063808148c", + "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/live_attribute_updates_fixture.rs": "657f86f79582073ac23d7958cec5464fdc1071b9c50e19b1aa5f5c5033cbe3af", + "crates/horizon-world/examples/live_attributes_fixture.rs": "dc4136118c6b42f700cb5d5474aefc8bb136ca5f1a8ca02f7766c1412ad7e665" + }, + "fixtures_sha256": { + ".local/rust-live-attribute-updates-parity/inputs.txt": "1de9e5ca84a2dbd2c04b42c17548dc3a00ad38d006cae7906cfad8558cf90954", + ".local/rust-live-attribute-updates-parity/catalog.json": "ea8811c786c28dd2a6e3dee78dd36ee9655f7c415b4febd0c2218159e4dbc1eb", + ".local/rust-live-attribute-updates-parity/catalog-client.json": "d1e488e4f432888beb9f643904c8bca242d1e2093f53e23e07da305d70cf7106", + ".local/rust-live-attribute-updates-parity/catalog-server.json": "d1e488e4f432888beb9f643904c8bca242d1e2093f53e23e07da305d70cf7106", + ".local/rust-live-attribute-updates-parity/inputs-client.txt": "5c2d384d141240f79132399e384dcb73c8f25ec2f4d550cd3759f7e9dba2b747", + ".local/rust-live-attribute-updates-parity/inputs-server.txt": "be6a31b6e0b3da73ac012c081ae073d57fc5954bceba59770b9a1e01b22aaa1a" + }, + "references": [ + { + "side": "client", + "jar_sha256": "4508d006323f24fa02876310c192d739af56516eb259000ac50f0909a68c9a2d", + "output_sha256": "6d25f14bb8fe0e8e8bad48dfcea0795c7508515b8ba6892cc5beeeb1aa25d0c2", + "matched_output_lines": 2836190, + "rust_output_sha256": "6d25f14bb8fe0e8e8bad48dfcea0795c7508515b8ba6892cc5beeeb1aa25d0c2" + }, + { + "side": "server", + "jar_sha256": "a362163eec5d1612d520772bc16e5b39c09e3b234fdc045f56bf544284ee8ae6", + "output_sha256": "e124ba567070fd090bc152408f73fd6742e8da8e14280b62f798b4a1e02d5e22", + "matched_output_lines": 2836234, + "rust_output_sha256": "e124ba567070fd090bc152408f73fd6742e8da8e14280b62f798b4a1e02d5e22" + } + ], + "scope": "Native LivingEntity attribute queries, notification and private refresh methods with registered AttributeMaps, real effect modifiers and pending queues, onAttributeUpdated health/absorption consequences and SynchedEntityData Float storage. Exercises all registered attributes/default suppliers, separate captured/selected maps and health stores, callbacks changing limits and routes, null pending members, client guards, failure prefixes and modifier/cache/queue identity. Replays JVM holder/instance hashes per reference. Scale dimension rebuild and waypoint-manager internals remain controlled; passenger lists are empty. Full effect lifecycle/dirty-particle synchronization, player absorption, complete gameplay and playable platform builds remain pending." +} diff --git a/crates/horizon-world/examples/live_attribute_updates_fixture.rs b/crates/horizon-world/examples/live_attribute_updates_fixture.rs new file mode 100644 index 000000000..9f73a07e3 --- /dev/null +++ b/crates/horizon-world/examples/live_attribute_updates_fixture.rs @@ -0,0 +1,598 @@ +#[allow(dead_code)] +mod integration { + include!("live_attributes_fixture.rs"); + use horizon_world::{ + active_effects::{ActiveEffects, EffectHooks}, + attribute_updates::{self, UpdateWorld, UpdatedAttribute}, + attributes::live::Key, + effect_notifications::{self as notify, EffectPacket, NotificationWorld}, + java_hash_map::JavaKey, + live_attribute_updates::{self as live_updates, LiveAttributeWorld}, + living_health::{self, HealthWorld, SyncedFloat}, + object_open_set::{CursorError, ObjectOpenSet}, + status_effect::{Details, EffectInstance}, + }; + type Live = LiveAttributes i32>>; + type Result = std::result::Result; + #[derive(Clone, PartialEq, Eq)] + struct EffectKey; + impl JavaKey for EffectKey { + fn java_hash(&self) -> i32 { + 0 + } + fn tie_break(&self, _: &Self) -> std::cmp::Ordering { + std::cmp::Ordering::Less + } + } + type Storage = ActiveEffects; + type Effect = EffectInstance; + fn single(v: &Value) -> f32 { + f32::from_bits(v.as_u64().unwrap() as u32) + } + fn float_bits(v: f32) -> u32 { + if v.is_nan() { 0x7fc00000 } else { v.to_bits() } + } + fn stop(e: Error) -> &'static str { + match e { + Error::NullAttribute | Error::MissingInstanceModifier => "null", + Error::MissingAttribute | Error::MissingModifier | Error::DuplicateModifier(_) => { + "argument" + } + _ => panic!("invalid fixture: {e:?}"), + } + } + fn cursor_stop(e: CursorError) -> &'static str { + match e { + CursorError::IndexOutOfBounds => "bounds", + CursorError::NoSuchElement => "empty", + _ => "null", + } + } + struct World<'a> { + maps: [Live; 2], + health: [SyncedFloat; 2], + absorption: f32, + selection: usize, + direct: usize, + data: usize, + manager: usize, + server: bool, + client: bool, + calls: i64, + op: Value, + definitions: Definitions, + pools: [Vec; 2], + out: &'a mut dyn Write, + } + impl World<'_> { + fn key(&self, op: &Value) -> Option { + let index = op["attribute"].as_i64().unwrap(); + (index >= 0).then(|| self.maps[0].keys()[index as usize].clone()) + } + fn controls(&mut self, c: &Value) { + if let Some(v) = c["selection"].as_u64() { + self.selection = v as usize; + } + if let Some(v) = c["direct"].as_u64() { + self.direct = v as usize; + } + if let Some(v) = c["data"].as_u64() { + self.data = v as usize; + } + if let Some(v) = c["manager"].as_u64() { + self.manager = v as usize; + } + if let Some(v) = c["server"].as_bool() { + self.server = v; + } + if let Some(v) = c["client"].as_bool() { + self.client = v; + } + if !c["absorption"].is_null() { + self.absorption = single(&c["absorption"]); + } + } + fn event(&mut self, name: impl AsRef) -> Result<()> { + let call = self.calls; + self.calls += 1; + writeln!(self.out, "event {call} {}", name.as_ref()).unwrap(); + if call == self.op["fail"].as_i64().unwrap() { + return Err("callback"); + } + if call == self.op["mutate"].as_i64().unwrap() { + let changes = self.op["changes"].clone(); + self.controls(&changes); + for action in self.op["actions"].as_array().unwrap().clone() { + self.raw(&action)?; + } + } + Ok(()) + } + fn attribute_value(&mut self, key: Option) -> Result { + self.event(format!("value {}", key.as_ref().map_or("null", Key::name)))?; + live_updates::value(self, key, ValueKind::Computed) + } + fn raw(&mut self, op: &Value) -> Result { + let key = self.key(op); + let target = op["target"].as_u64().unwrap() as usize; + let m = Modifier::new( + op["id"].as_str().unwrap(), + number(&op["amount"]), + Operation::ALL[op["operation"].as_u64().unwrap() as usize], + ) + .unwrap(); + match op["mode"].as_str().unwrap() { + "controls" => { + self.controls(&op["changes"]); + Ok("ok".into()) + } + "ensure" => self.maps[target] + .get_instance(key) + .map(|h| handle(h.as_ref())) + .map_err(stop), + "reset" => self.maps[target] + .reset_base(key) + .map(|v| v.to_string()) + .map_err(stop), + "define" => { + effects::add_definition( + &mut self.definitions, + key, + Template::new(m.id(), m.amount(), m.operation()).unwrap(), + ); + Ok("ok".into()) + } + "effect-add" => self.maps[target] + .add_effect_modifiers( + &mut self.definitions, + op["amplifier"].as_i64().unwrap() as i32, + ) + .map(|()| "ok".into()) + .map_err(|e| stop(effect_error(e))), + "effect-remove" => self.maps[target] + .remove_effect_modifiers(&mut self.definitions) + .map(|()| "ok".into()) + .map_err(|e| stop(effect_error(e))), + "clear-updates" => { + self.maps[target].updates_mut().clear(); + Ok("ok".into()) + } + "clear-sync" => { + self.maps[target].sync_mut().clear(); + Ok("ok".into()) + } + "null-update" => { + self.maps[target].updates_mut().add(None); + Ok("ok".into()) + } + mode => { + let Some(h) = self.maps[target].get_instance(key).map_err(stop)? else { + return Ok("absent".into()); + }; + self.maps[target] + .edit(&h, |a, o| match mode { + "set" => a + .set_base_value(number(&op["base"]), o) + .map(|()| "ok".into()), + "update" => a.add_or_update_transient(m, o).map(|()| "ok".into()), + "permanent" => a.add_permanent(m, o).map(|()| "ok".into()), + "remove" => a.remove(m.id(), o).map(|v| v.to_string()), + "clear" => a.remove_all(o).map(|()| "ok".into()), + _ => panic!("raw mode: {mode}"), + }) + .map_err(stop) + } + } + } + fn state(&mut self, storage: &Storage) { + writeln!( + self.out, + "controls {} {} {} {} {} {} {} {}", + self.selection, + self.direct, + self.data, + self.manager, + self.server, + self.client, + self.calls, + storage.dirty + ) + .unwrap(); + for (i, h) in self.health.iter().enumerate() { + writeln!( + self.out, + "health {i} {} {} {} {}", + float_bits(h.value()), + h.is_dirty(), + h.item_dirty(), + h.is_default() + ) + .unwrap(); + } + writeln!(self.out, "absorption {}", float_bits(self.absorption)).unwrap(); + for (i, map) in self.maps.iter().enumerate() { + writeln!(self.out, "attributes {i}").unwrap(); + writeln!( + self.out, + "map {}", + map.entries() + .iter() + .map(|(k, h)| format!( + "{}={}", + k.as_ref().map_or("null", Key::name), + handle(h.as_ref()) + )) + .collect::>() + .join(",") + ) + .unwrap(); + writeln!( + self.out, + "updates {}", + map.updates() + .values() + .map(handle) + .collect::>() + .join(",") + ) + .unwrap(); + writeln!( + self.out, + "sync {}", + map.sync() + .values() + .map(handle) + .collect::>() + .join(",") + ) + .unwrap(); + for (h, key, a) in map.instances() { + writeln!( + self.out, + "instance {} {} {} {} {} | {} | {}", + h.index(), + key.name(), + bits(a.base_value()), + a.is_dirty(), + bits(a.cached_value()), + refs(a.modifiers(), &mut self.pools[i]), + refs(a.permanent_modifiers(), &mut self.pools[i]) + ) + .unwrap(); + } + } + } + } + impl HealthWorld for World<'_> { + type Error = &'static str; + type Data = usize; + fn health(&mut self) -> Result { + self.event("health")?; + Ok(self.health[self.data].value()) + } + fn health_data(&self) -> usize { + self.data + } + fn max_health_attribute(&mut self) -> Result { + self.attribute_value(self.maps[0].key("minecraft:max_health")) + } + fn max_absorption_attribute(&mut self) -> Result { + self.attribute_value(self.maps[0].key("minecraft:max_absorption")) + } + fn store_health(&mut self, data: usize, value: f32) -> Result<()> { + let mut h = self.health[data]; + let result = h.set(value, false, |current| { + self.health[data] = *current; + let result = self.event("health-updated"); + *current = self.health[data]; + result + }); + self.health[data] = h; + result + } + fn store_absorption(&mut self, value: f32) -> Result<()> { + self.event(format!("store-absorption {}", float_bits(value)))?; + self.absorption = value; + Ok(()) + } + fn dead_flag(&self) -> bool { + false + } + fn dispatch_set_health(&mut self, value: f32) -> Result<()> { + self.event(format!("set-health {}", float_bits(value)))?; + living_health::set_health(self, value) + } + } + impl UpdateWorld for World<'_> { + type Attributes = usize; + type Level = bool; + type Manager = usize; + fn attribute_kind(&self, key: &Key) -> UpdatedAttribute { + live_updates::kind(key) + } + fn absorption(&mut self) -> Result { + self.event("absorption")?; + Ok(self.absorption) + } + fn refresh_dimensions(&mut self) -> Result<()> { + self.event("dimensions") + } + fn level(&mut self) -> Result { + self.event("level")?; + Ok(self.server) + } + fn is_server_level(&self, level: bool) -> bool { + level + } + fn waypoint_manager(&mut self, _: bool) -> Result { + self.event("manager")?; + Ok(self.manager) + } + fn attributes_field(&self) -> usize { + self.direct + } + fn attribute_value_in(&mut self, map: usize, key: Key) -> Result { + live_updates::value_in(self, map, Some(key), ValueKind::Computed) + } + fn track_waypoint(&mut self, manager: usize, track: bool) -> Result<()> { + self.event(format!("waypoint {manager} {track}")) + } + } + impl LiveAttributeWorld for World<'_> { + type Identity = Box i32>; + fn get_attributes(&mut self) -> Result { + self.event("attributes")?; + Ok(self.selection) + } + fn live_attributes(&self, map: usize) -> &Live { + &self.maps[map] + } + fn live_attributes_mut(&mut self, map: usize) -> &mut Live { + &mut self.maps[map] + } + fn instance_attribute(&self, instance: Option) -> Result { + let h = instance.ok_or("null")?; + self.maps + .iter() + .find_map(|m| m.attribute(&h).ok().cloned()) + .ok_or("null") + } + fn attribute_error(error: Error) -> &'static str { + stop(error) + } + fn cursor_error(error: CursorError) -> &'static str { + cursor_stop(error) + } + fn dispatch_attribute_updated(&mut self, key: Key) -> Result<()> { + self.event(format!("updated {}", key.name()))?; + attribute_updates::updated(self, key) + } + } + impl EffectHooks for World<'_> { + type Error = &'static str; + type Source = (); + type Level = bool; + fn can_be_affected(&mut self, _: &mut Storage, _: Effect) -> Result { + Ok(true) + } + fn on_added(&mut self, s: &mut Storage, e: Effect, _: Option<()>) -> Result<()> { + notify::on_added(s, &e, self) + } + fn on_updated( + &mut self, + s: &mut Storage, + e: Effect, + reapply: bool, + _: Option<()>, + ) -> Result<()> { + notify::on_updated(s, &e, reapply, self) + } + fn on_removed(&mut self, s: &mut Storage, es: Vec) -> Result<()> { + notify::on_removed(s, &es, self) + } + fn effect_added(&mut self, _: &mut Storage, _: EffectKey, _: i32) -> Result<()> { + Ok(()) + } + fn effect_started(&mut self, _: &mut Storage, _: EffectKey, _: i32) -> Result<()> { + Ok(()) + } + fn level(&mut self, _: &mut Storage) -> Result { + UpdateWorld::level(self) + } + fn is_client(&mut self, _: &mut Storage, level: bool) -> Result { + if !level { + return Err("null"); + } + self.event("client")?; + Ok(self.client) + } + fn actor_removed(&self) -> bool { + false + } + } + impl NotificationWorld for World<'_> { + type Attributes = usize; + type UpdateSet = usize; + type AttributeInstance = Handle; + type Attribute = Key; + type Passenger = (); + type Connection = (); + fn attributes(&mut self, _: &mut Storage) -> Result { + self.get_attributes() + } + fn add_modifiers( + &mut self, + _: &mut Storage, + _: EffectKey, + map: usize, + amp: i32, + ) -> Result<()> { + self.maps[map] + .add_effect_modifiers(&mut self.definitions, amp) + .map_err(|e| stop(effect_error(e))) + } + fn remove_modifiers(&mut self, _: &mut Storage, _: EffectKey, map: usize) -> Result<()> { + self.maps[map] + .remove_effect_modifiers(&mut self.definitions) + .map_err(|e| stop(effect_error(e))) + } + fn pending_updates(&mut self, _: &mut Storage, map: usize) -> Result { + Ok(map) + } + fn update_set(&self, set: usize) -> &ObjectOpenSet { + self.maps[set].updates() + } + fn update_set_mut(&mut self, set: usize) -> &mut ObjectOpenSet { + self.maps[set].updates_mut() + } + fn iterator_error(e: CursorError) -> &'static str { + cursor_stop(e) + } + fn attribute_of(&mut self, _: &mut Storage, instance: Option) -> Result { + self.instance_attribute(instance) + } + fn attribute_updated(&mut self, _: &mut Storage, key: Key) -> Result<()> { + self.dispatch_attribute_updated(key) + } + fn passengers(&self) -> Vec<()> { + vec![] + } + fn is_server_player(&self, _: ()) -> bool { + false + } + fn connection(&self, _: ()) {} + fn entity_id(&mut self, _: &mut Storage) -> Result { + Ok(0) + } + fn send_packet( + &mut self, + _: &mut Storage, + _: (), + _: EffectPacket, + ) -> Result<()> { + Ok(()) + } + } + pub fn run() -> std::result::Result<(), Box> { + let args: Vec<_> = std::env::args().collect(); + let dir = Path::new(&args[1]); + let catalog = Arc::new(Catalog::from_json(&serde_json::from_str( + &fs::read_to_string(dir.join("catalog.json"))?, + )?)?); + let mut out = + io::BufWriter::new(fs::File::create(dir.join(format!("rust-{}.txt", args[2])))?); + for line in fs::read_to_string(dir.join(format!("inputs-{}.txt", args[2])))?.lines() { + let f: Value = serde_json::from_str(line)?; + let hashes: Vec<_> = f["holder_hashes"] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_i64().unwrap() as i32) + .collect(); + let maps = std::array::from_fn(|i| { + let mut ids = f["instance_hashes"][i] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_i64().unwrap() as i32) + .collect::>() + .into_iter(); + Live::new( + catalog.clone(), + f["entity"].as_str().unwrap(), + &hashes, + Box::new(move || ids.next().unwrap()), + ) + .unwrap() + }); + let mut w = World { + maps, + health: std::array::from_fn(|i| SyncedFloat::new(single(&f["health"][i]))), + absorption: single(&f["absorption"]), + selection: 0, + direct: 0, + data: 0, + manager: 0, + server: true, + client: false, + calls: 0, + op: Value::Null, + definitions: Definitions::default(), + pools: [vec![], vec![]], + out: &mut out, + }; + let mut storage = Storage::default(); + for op in f["setup"].as_array().unwrap() { + w.raw(op).unwrap(); + } + for op in f["ops"].as_array().unwrap() { + w.op = op.clone(); + w.calls = 0; + let key = w.key(op); + let effect = EffectInstance::new( + EffectKey, + Details { + duration: 200, + amplifier: op["amplifier"].as_i64().unwrap() as i32, + ambient: false, + visible: true, + show_icon: true, + }, + None, + ); + let result = match op["mode"].as_str().unwrap() { + "value" => w.attribute_value(key).map(bits), + "base" => live_updates::value(&mut w, key, ValueKind::Base).map(bits), + "set-health" => w + .dispatch_set_health(single(&op["value"])) + .map(|()| "ok".into()), + "set-absorption" => living_health::set_absorption(&mut w, single(&op["value"])) + .map(|()| "ok".into()), + "heal" => { + living_health::heal(&mut w, single(&op["value"])).map(|()| "ok".into()) + } + "attribute-update" => match key { + Some(key) => w.dispatch_attribute_updated(key), + None => w.event("updated null").and(Err("null")), + } + .map(|()| "ok".into()), + "refresh" => live_updates::refresh(&mut w).map(|()| "ok".into()), + "notify-add" => { + notify::on_added(&mut storage, &effect, &mut w).map(|()| "ok".into()) + } + "notify-update" => notify::on_updated( + &mut storage, + &effect, + op["reapply"].as_bool().unwrap(), + &mut w, + ) + .map(|()| "ok".into()), + "notify-remove" => notify::on_removed( + &mut storage, + &vec![effect; op["count"].as_u64().unwrap() as usize], + &mut w, + ) + .map(|()| "ok".into()), + "pack" => Ok(w.health[op["target"].as_u64().unwrap() as usize] + .take_dirty() + .map_or("none".into(), |v| float_bits(v).to_string())), + "dirty" => { + storage.dirty = op["flag"].as_bool().unwrap(); + Ok("ok".into()) + } + _ => w.raw(op), + }; + writeln!( + w.out, + "result {}", + result.unwrap_or_else(|e| format!("error {e}")) + )?; + w.state(&storage); + } + } + Ok(()) + } +} +fn main() -> Result<(), Box> { + integration::run() +} diff --git a/crates/horizon-world/src/lib.rs b/crates/horizon-world/src/lib.rs index 1d5d64889..76839c641 100644 --- a/crates/horizon-world/src/lib.rs +++ b/crates/horizon-world/src/lib.rs @@ -12,6 +12,7 @@ pub mod active_effect_tick; pub mod stored_consume; pub mod attribute_health; pub mod attribute_updates; +pub mod live_attribute_updates; pub mod attributes; pub mod block_collision; pub mod block_collision_scan; diff --git a/crates/horizon-world/src/live_attribute_updates.rs b/crates/horizon-world/src/live_attribute_updates.rs new file mode 100644 index 000000000..632a41438 --- /dev/null +++ b/crates/horizon-world/src/live_attribute_updates.rs @@ -0,0 +1,85 @@ +//! LivingEntity query/refresh dispatch over registered live attribute storage. +use crate::{ + attribute_map::ValueKind, + attribute_updates::{self, UpdateWorld, UpdatedAttribute}, + attributes::live::{Error, Handle, Key, LiveAttributes}, + object_open_set::CursorError, +}; + +/// Entity handles keep a captured map alive even if a callback changes the +/// entity's current attributes. Map and instance accessors are direct storage +/// reads; virtual entity methods remain explicit dispatch points. +pub trait LiveAttributeWorld: UpdateWorld { + type Identity: FnMut() -> i32; + fn get_attributes(&mut self) -> Result; + fn live_attributes(&self, map: Self::Attributes) -> &LiveAttributes; + fn live_attributes_mut(&mut self, map: Self::Attributes) + -> &mut LiveAttributes; + /// Resolve the queued instance itself, including an instance from another + /// map if a caller has explicitly placed it in this map's pending set. + fn instance_attribute(&self, instance: Option) -> Result; + fn attribute_error(error: Error) -> Self::Error; + fn cursor_error(error: CursorError) -> Self::Error; + fn dispatch_attribute_updated(&mut self, attribute: Key) -> Result<(), Self::Error> + where + Self: Sized, + { + attribute_updates::updated(self, attribute) + } +} + +/// Registered holder names are unique in a catalog. This classifier is for +/// registered attributes; custom holder dispatch can implement UpdateWorld. +pub fn kind(attribute: &Key) -> UpdatedAttribute { + match attribute.name() { + "minecraft:max_health" => UpdatedAttribute::MaxHealth, + "minecraft:max_absorption" => UpdatedAttribute::MaxAbsorption, + "minecraft:scale" => UpdatedAttribute::Scale, + "minecraft:waypoint_transmit_range" => UpdatedAttribute::WaypointRange, + _ => UpdatedAttribute::Other, + } +} + +/// Base body of getAttributeValue/getAttributeBaseValue, including the virtual +/// getAttributes call. Querying defaults does not create instances. +pub fn value( + world: &mut W, + attribute: Option, + kind: ValueKind, +) -> Result { + let map = world.get_attributes()?; + value_in(world, map, attribute, kind) +} + +/// Query an already captured map, as used by waypoint updates after their +/// direct attributes-field read. This does not call virtual getAttributes. +pub fn value_in( + world: &mut W, + map: W::Attributes, + attribute: Option, + kind: ValueKind, +) -> Result { + world + .live_attributes_mut(map) + .value(attribute, kind) + .map_err(W::attribute_error) +} + +/// Base private refresh body for concrete registered AttributeMaps. Callbacks +/// may change map selection or mutate the captured pending set. Clear that set +/// only after the live cursor and every attribute callback finish successfully. +pub fn refresh(world: &mut W) -> Result<(), W::Error> { + let map = world.get_attributes()?; + let mut cursor = world.live_attributes(map).updates().cursor(); + while cursor.has_next() { + let instance = world + .live_attributes(map) + .updates() + .next(&mut cursor) + .map_err(W::cursor_error)?; + let attribute = world.instance_attribute(instance)?; + world.dispatch_attribute_updated(attribute)?; + } + world.live_attributes_mut(map).updates_mut().clear(); + Ok(()) +} diff --git a/docs/rust-parity.md b/docs/rust-parity.md index 7d764ad43..d208fe3d3 100644 --- a/docs/rust-parity.md +++ b/docs/rust-parity.md @@ -1230,3 +1230,27 @@ targets compile, and library/affected fixture Clippy checks pass. This borrowed binding keeps its selected attribute map fixed; entity virtual dispatch, attribute refresh, player absorption synchronization and starvation damage delivery still require their complete runtime integration. + +## Entity refresh over live registered attributes + +The live attribute bridge now connects virtual entity queries and pending +attribute refresh to registered storage and attribute-update dispatch. Refresh +retains its selected map through callbacks and clears that map's pending set +only on success. Health and absorption consequences read actual computed limits; +waypoint updates retain their manager before reading the direct attribute-map +field. Scale changes dispatch dimension refresh. + +The [combined live update comparison](../benchmarks/rust-live-attribute-updates-2026-10-07/scoped-parity.json) +matches **2,836,190 client-reference lines and 2,836,234 server-reference lines**. +It executes actual native notifications, modifier loops, registered attribute +queries, private refresh, health/absorption update bodies and synchronized Float +storage. Cases cover all registered attributes/default suppliers, selected and +captured map/data handles, callbacks changing limits or queues, null pending +members, client guards, empty removals and failures late in large refreshes. +Each reference uses its own JVM identity hashes, so callback-mutation paths can +produce different trace lengths between runs. Library and fixture Clippy pass. + +Dimension rebuilding and waypoint-manager internals remain controlled, and +passenger lists are empty in this combined fixture. Full active-effect lifecycle +and particle/invisibility synchronization, player absorption storage and complete +gameplay/platform integration remain unfinished. diff --git a/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaLiveAttributeUpdatesOracle.java b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaLiveAttributeUpdatesOracle.java new file mode 100644 index 000000000..ca25e3d2b --- /dev/null +++ b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaLiveAttributeUpdatesOracle.java @@ -0,0 +1,197 @@ +package dev.horizonlab.parity; + +import static dev.horizonlab.parity.VanillaEntityAttributeOracle.*; +import com.google.gson.*; +import com.google.common.collect.ImmutableList; +import java.io.*; +import java.lang.reflect.*; +import java.nio.file.*; +import java.util.*; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.world.effect.*; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.*; +import net.minecraft.world.level.Level; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.waypoints.ServerWaypointManager; +import net.minecraft.world.waypoints.WaypointTransmitter; +import net.minecraft.network.syncher.*; + +/** Native entity updates and notifications with actual registered attribute maps. */ +public final class VanillaLiveAttributeUpdatesOracle { + static Object unsafe; + static Method allocate, refresh; + static Field healthData, absorption, attributesField, effectDirty; + static EntityDataAccessor healthKey; + static Constructor dataCtor; + static T make(Class type) throws Exception { return type.cast(allocate.invoke(unsafe, type)); } + static float single(JsonElement v) { return Float.intBitsToFloat((int)v.getAsLong()); } + static long floatBits(float value) { return Integer.toUnsignedLong(Float.floatToIntBits(value)); } + static final class Stop extends RuntimeException {} + static final class Data { + VanillaLiveAttributesOracle.Data[] maps = new VanillaLiveAttributesOracle.Data[2]; + VanillaLiveAttributesOracle.Effect effect = new VanillaLiveAttributesOracle.Effect(); + SynchedEntityData[] stores = new SynchedEntityData[2]; + List> items = new ArrayList<>(); + Manager[] managers = new Manager[2]; + Actor actor; EffectLevel level; PrintWriter out; JsonObject op; + int selection, direct, data, manager, calls; + boolean server = true, client; + Holder key(JsonObject op) { + int index = op.get("attribute").getAsInt(); + return index < 0 ? null : attributes.get(index); + } + void controls(JsonObject c) throws Exception { + if (c.has("selection")) selection = c.get("selection").getAsInt(); + if (c.has("direct")) direct = c.get("direct").getAsInt(); + if (c.has("data")) data = c.get("data").getAsInt(); + if (c.has("manager")) manager = c.get("manager").getAsInt(); + if (c.has("server")) server = c.get("server").getAsBoolean(); + if (c.has("client")) client = c.get("client").getAsBoolean(); + if (c.has("absorption")) absorption.setFloat(actor, single(c.get("absorption"))); + healthData.set(actor, stores[data]); + attributesField.set(actor, maps[direct].attributes); + } + void event(String name) { + int call = calls++; + out.println("event " + call + " " + name); + if (call == op.get("fail").getAsInt()) throw new Stop(); + if (call == op.get("mutate").getAsInt()) { + try { + controls(op.getAsJsonObject("changes")); + for (var action : op.getAsJsonArray("actions")) raw(action.getAsJsonObject()); + } catch (RuntimeException e) { throw e; } + catch (Exception e) { throw new AssertionError(e); } + } + } + String raw(JsonObject op) throws Exception { + var map = maps[op.get("target").getAsInt()]; + var key = key(op); + String mode = op.get("mode").getAsString(); + var modifier = new AttributeModifier(Identifier.parse(op.get("id").getAsString()), number(op.get("amount")), AttributeModifier.Operation.values()[op.get("operation").getAsInt()]); + switch (mode) { + case "controls": controls(op.getAsJsonObject("changes")); return "ok"; + case "ensure": return map.reference(map.attributes.getInstance(key)); + case "reset": return Boolean.toString(map.attributes.resetBaseValue(key)); + case "define": effect.addAttributeModifier(key, modifier.id(), modifier.amount(), modifier.operation()); return "ok"; + case "effect-add": effect.addAttributeModifiers(map.attributes, op.get("amplifier").getAsInt()); return "ok"; + case "effect-remove": effect.removeAttributeModifiers(map.attributes); return "ok"; + case "clear-updates": map.attributes.getAttributesToUpdate().clear(); return "ok"; + case "clear-sync": map.attributes.getAttributesToSync().clear(); return "ok"; + case "null-update": map.attributes.getAttributesToUpdate().add(null); return "ok"; + case "dirty": effectDirty.setBoolean(actor, op.get("flag").getAsBoolean()); return "ok"; + default: + var instance = map.attributes.getInstance(key); + if (instance == null) return "absent"; + switch (mode) { + case "set": instance.setBaseValue(number(op.get("base"))); break; + case "update": instance.addOrUpdateTransientModifier(modifier); break; + case "permanent": instance.addPermanentModifier(modifier); break; + case "remove": return Boolean.toString(instance.removeModifier(modifier.id())); + case "clear": instance.removeModifiers(); break; + default: throw new AssertionError(mode); + } + return "ok"; + } + } + void state() throws Exception { + out.println("controls " + selection + " " + direct + " " + data + " " + manager + " " + server + " " + client + " " + calls + " " + effectDirty.getBoolean(actor)); + for (int i = 0; i < 2; i++) out.println("health " + i + " " + floatBits(items.get(i).getValue()) + " " + stores[i].isDirty() + " " + items.get(i).isDirty() + " " + items.get(i).isSetToDefault()); + out.println("absorption " + floatBits(absorption.getFloat(actor))); + for (int i = 0; i < 2; i++) { out.println("attributes " + i); maps[i].state(out); } + } + } + static final class Actor extends net.minecraft.world.entity.decoration.ArmorStand { + Data d; Actor() { super(null, null); } + @Override public AttributeMap getAttributes() { d.event("attributes"); return d.maps[d.selection].attributes; } + @Override public double getAttributeValue(Holder h) { d.event("value " + (h == null ? "null" : h.getRegisteredName())); return super.getAttributeValue(h); } + @Override public float getHealth() { d.event("health"); return super.getHealth(); } + @Override public void setHealth(float v) { d.event("set-health " + floatBits(v)); super.setHealth(v); } + @Override public float getAbsorptionAmount() { d.event("absorption"); return super.getAbsorptionAmount(); } + @Override protected void internalSetAbsorptionAmount(float v) { d.event("store-absorption " + floatBits(v)); super.internalSetAbsorptionAmount(v); } + @Override public void onSyncedDataUpdated(EntityDataAccessor key) { if (key != healthKey) throw new AssertionError(); d.event("health-updated"); } + @Override public void refreshDimensions() { d.event("dimensions"); } + @Override public Level level() { d.event("level"); return d.server ? d.level : null; } + @Override protected void onAttributeUpdated(Holder h) { d.event("updated " + (h == null ? "null" : h.getRegisteredName())); super.onAttributeUpdated(h); } + void added(MobEffectInstance e) { super.onEffectAdded(e, null); } + void updated(MobEffectInstance e, boolean reapply) { super.onEffectUpdated(e, reapply, null); } + void removed(List es) { super.onEffectsRemoved(es); } + } + static final class EffectLevel extends ServerLevel { + Data d; EffectLevel() { super(null,null,null,null,null,null,false,0L,List.of(),false); } + @Override public boolean isClientSide() { d.event("client"); return d.client; } + @Override public ServerWaypointManager getWaypointManager() { d.event("manager"); return d.managers[d.manager]; } + } + static final class Manager extends ServerWaypointManager { + Data d; int id; + @Override public void trackWaypoint(WaypointTransmitter actor) { if (actor != d.actor) throw new AssertionError(); d.event("waypoint " + id + " true"); } + @Override public void untrackWaypoint(WaypointTransmitter actor) { if (actor != d.actor) throw new AssertionError(); d.event("waypoint " + id + " false"); } + } + @SuppressWarnings("unchecked") + public static void main(String[] args) throws Exception { + net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); + for (var a : BuiltInRegistries.ATTRIBUTE) attributes.add(BuiltInRegistries.ATTRIBUTE.wrapAsHolder(a)); + Path dir = Path.of(args[1]); + if (args[0].startsWith("export-")) { Files.writeString(dir.resolve("catalog-" + args[0].substring(7) + ".json"), export().toString()); return; } + var u = Class.forName("sun.misc.Unsafe"); unsafe = field(u, "theUnsafe").get(null); allocate = u.getMethod("allocateInstance", Class.class); + healthData = field(Entity.class, "entityData"); absorption = field(LivingEntity.class, "absorptionAmount"); attributesField = field(LivingEntity.class, "attributes"); effectDirty = field(LivingEntity.class, "effectsDirty"); + var passengers = field(Entity.class, "passengers"); + refresh = LivingEntity.class.getDeclaredMethod("refreshDirtyAttributes"); refresh.setAccessible(true); + healthKey = (EntityDataAccessor)field(LivingEntity.class, "DATA_HEALTH_ID").get(null); + dataCtor = SynchedEntityData.class.getDeclaredConstructor(SyncedDataHolder.class, SynchedEntityData.DataItem[].class); dataCtor.setAccessible(true); + VanillaLiveAttributesOracle.storage = field(AttributeMap.class, "attributes"); VanillaLiveAttributesOracle.dirty = field(AttributeInstance.class, "dirty"); VanillaLiveAttributesOracle.cached = field(AttributeInstance.class, "cachedValue"); + try (var out = new PrintWriter(Files.newBufferedWriter(dir.resolve(args[0] + ".txt"))); + var enriched = new PrintWriter(Files.newBufferedWriter(dir.resolve("inputs-" + 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.out = out; + d.actor = make(Actor.class); d.actor.d = d; d.level = make(EffectLevel.class); d.level.d = d; + var entity = BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse(f.get("entity").getAsString())); + for (int i = 0; i < 2; i++) { + var map = new VanillaLiveAttributesOracle.Data(); d.maps[i] = map; + map.attributes = new VanillaLiveAttributesOracle.Attrs(map, supplier(entity)); + map.map = (Map,AttributeInstance>)VanillaLiveAttributesOracle.storage.get(map.attributes); + d.managers[i] = make(Manager.class); d.managers[i].d = d; d.managers[i].id = i; + var items = new SynchedEntityData.DataItem[healthKey.id() + 1]; + for (int j = 0; j < items.length; j++) items[j] = new SynchedEntityData.DataItem(new EntityDataAccessor(j, EntityDataSerializers.FLOAT), 0f); + var health = new SynchedEntityData.DataItem(healthKey, single(f.getAsJsonArray("health").get(i))); + d.items.add(health); items[healthKey.id()] = health; d.stores[i] = dataCtor.newInstance(d.actor, items); + } + effectDirty.setBoolean(d.actor, true); passengers.set(d.actor, ImmutableList.of()); absorption.setFloat(d.actor, single(f.get("absorption"))); d.controls(new JsonObject()); + for (var op : f.getAsJsonArray("setup")) d.raw(op.getAsJsonObject()); + var effect = Holder.direct(d.effect); + for (var ov : f.getAsJsonArray("ops")) { + d.op = ov.getAsJsonObject(); d.calls = 0; String result = "ok"; + var op = d.op; String mode = op.get("mode").getAsString(); + try { + switch (mode) { + case "value": result = bits(d.actor.getAttributeValue(d.key(op))); break; + case "base": result = bits(d.actor.getAttributeBaseValue(d.key(op))); break; + case "set-health": d.actor.setHealth(single(op.get("value"))); break; + case "set-absorption": d.actor.setAbsorptionAmount(single(op.get("value"))); break; + case "heal": d.actor.heal(single(op.get("value"))); break; + case "attribute-update": d.actor.onAttributeUpdated(d.key(op)); break; + case "refresh": try { refresh.invoke(d.actor); } catch (InvocationTargetException e) { if (e.getCause() instanceof RuntimeException x) throw x; throw new AssertionError(e.getCause()); } break; + case "notify-add": d.actor.added(new MobEffectInstance(effect,200,op.get("amplifier").getAsInt())); break; + case "notify-update": d.actor.updated(new MobEffectInstance(effect,200,op.get("amplifier").getAsInt()),op.get("reapply").getAsBoolean()); break; + case "notify-remove": d.actor.removed(Collections.nCopies(op.get("count").getAsInt(),new MobEffectInstance(effect,200,op.get("amplifier").getAsInt()))); break; + case "pack": var packed = d.stores[op.get("target").getAsInt()].packDirty(); result = packed == null ? "none" : Long.toString(floatBits((Float)packed.getFirst().value())); break; + default: result = d.raw(op); + } + } catch (Stop e) { result = "error callback"; } + catch (NullPointerException e) { result = "error null"; } + catch (IndexOutOfBoundsException e) { result = "error bounds"; } + catch (NoSuchElementException e) { result = "error empty"; } + catch (IllegalArgumentException e) { result = "error argument"; } + out.println("result " + result); d.state(); + } + var hashes = new JsonArray(); for (var key : attributes) hashes.add(key.hashCode()); f.add("holder_hashes", hashes); + var instances = new JsonArray(); for (var map : d.maps) { hashes = new JsonArray(); for (var a : map.instances) hashes.add(System.identityHashCode(a)); instances.add(hashes); } f.add("instance_hashes", instances); + enriched.println(f); + } + if (out.checkError() || enriched.checkError()) throw new IOException("cannot write native trace"); + } + } +} diff --git a/tools/test_live_attribute_updates_parity.py b/tools/test_live_attribute_updates_parity.py new file mode 100644 index 000000000..166dbd166 --- /dev/null +++ b/tools/test_live_attribute_updates_parity.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Compare live registered attributes with native entity notifications and update consequences.""" +import argparse +import json +import math +import os +from pathlib import Path +import platform +import random +import struct +import subprocess + +try: + from . import lab, test_rust_parity as base +except ImportError: + import lab + import test_rust_parity as base + + +def corpus(catalog): + def bits(value): + return struct.pack('>d', float(value)).hex() + + def fl(value): + return int.from_bytes(struct.pack('>f', float(value)), 'big') + + def op(mode, attribute=0, **kw): + result = dict(mode=mode, target=0, attribute=attribute, id='horizon:a', + amount=bits(.25), base=bits(7), operation=0, value=fl(100), + amplifier=0, fail=-1, mutate=-1, changes={}, actions=[], + reapply=True, count=1, flag=False) + result.update(kw) + return result + + def row(setup, ops, entity='minecraft:player', **kw): + result = dict(entity=entity, health=[fl(100), fl(200)], absorption=fl(100), setup=setup, ops=ops) + result.update(kw) + return result + + attrs = catalog['attributes'] + count = len(attrs) + ids = [next(i for i, a in enumerate(attrs) if a['id'] == 'minecraft:' + name) + for name in ['max_health', 'max_absorption', 'scale', 'waypoint_transmit_range', 'attack_damage']] + values = [-math.inf, -1e30, -1, -0., 0., 1e-40, .1, 1, 2, 20, 1e30, math.inf, math.nan] + rng = random.Random(0x4c495645555044) + for entity in catalog['entities']: + for reverse in [False, True]: + order = list(range(count)) + if reverse: + order.reverse() + ops = [op(mode, i) for i in [-1, *order] for mode in ['value', 'base']] + for target in range(2): + ops.extend(op('ensure', i, target=target) for i in order) + ops += [op('refresh'), op('pack'), op('pack', target=1), op('controls', changes={'selection': 1}), op('refresh')] + for i in order: + ops.append(op('define', i, operation=i % 3, amount=bits(-2))) + ops += [op('dirty'), op('notify-add', amplifier=1), op('refresh'), op('notify-update', amplifier=2), + op('notify-remove'), op('notify-remove', count=0), op('controls', changes={'client': True}), + op('dirty'), op('notify-add'), op('notify-update'), op('notify-remove'), + op('controls', changes={'client': False, 'server': False}), op('attribute-update', ids[3])] + yield row([], ops, entity) + + setup = [op('set', key, target=target, base=bits(10 if target == 0 else 30)) + for target in range(2) for key in ids] + setup += [op('define', key, amount=bits(-3)) for key in ids] + for key in ids[:4]: + for value in values: + for operation in range(3): + ops = [op('set', key, base=bits(value)), op('attribute-update', key), op('pack'), + op('define', key, amount=bits(value), operation=operation), op('notify-add'), + op('refresh'), op('set-health', value=fl(value)), op('set-absorption', value=fl(value)), + op('notify-update', amplifier=255), op('notify-remove'), op('pack'), op('refresh')] + yield row(setup, ops, health=[fl(value), fl(-value)], absorption=fl(value)) + + changes = {'selection': 1, 'direct': 1, 'data': 1, 'manager': 1, 'absorption': fl(50)} + actions = [[], [op('set', ids[0], base=bits(2)), op('set', ids[0], target=1, base=bits(3))], + [op('set', ids[1], base=bits(2)), op('set', ids[1], target=1, base=bits(3))], + [op('clear-updates')], [op('null-update')], [op('clear-updates', target=1)], + [op('set', ids[3], target=1, base=bits(0))], [op('clear-sync')]] + # Inject failures and routing/limit changes at successive callback positions + # so captures and writes preceding an exception remain visible in state. + for key in ids[:4]: + for mode in ['attribute-update', 'refresh', 'set-health', 'set-absorption', 'notify-add', 'notify-update', 'notify-remove']: + for at in range(-1, 32): + yield row(setup, [op(mode, key, fail=at), op('pack'), op('refresh')]) + for action in actions: + yield row(setup, [op(mode, key, mutate=at, changes=changes, actions=action), op('pack'), op('refresh')]) + large_setup = [op('ensure', key, target=target) for target in range(2) for key in range(count)] + large_setup += [op('define', key, amount=bits(-3)) for key in range(count)] + for mode in ['refresh', 'notify-update', 'notify-remove']: + for at in range(64): + yield row(large_setup, [op(mode, fail=at), op('pack'), op('refresh')]) + yield row(large_setup, [op(mode, mutate=at, changes=changes, actions=actions[at % len(actions)]), op('pack'), op('refresh')]) + for amp in [-2**31, -1, 0, 1, 255, 256, 2**31-1]: + for reapply in [False, True]: + for n in [0, 1, 3]: + yield row(setup, [op('notify-add', amplifier=amp), op('notify-update', amplifier=amp, reapply=reapply), + op('notify-remove', count=n), op('refresh'), op('pack')]) + for _ in range(200): + ops = [] + for _ in range(40): + ops.append(op(rng.choice(['set', 'update', 'permanent', 'remove', 'reset', 'value', 'base', + 'refresh', 'attribute-update', 'define', 'notify-add', 'notify-update', + 'notify-remove', 'effect-add', 'effect-remove', 'pack', 'set-health', + 'set-absorption', 'heal', 'clear-updates', 'clear-sync']), + rng.choice([-1, *ids]), target=rng.randrange(2), operation=rng.randrange(3), + amount=bits(rng.choice(values)), base=bits(rng.choice(values)), value=fl(rng.choice(values)), + amplifier=rng.choice([-2**31, -1, 0, 1, 255, 256, 2**31-1]), count=rng.randrange(3), + reapply=bool(rng.randrange(2)), fail=rng.choice([-1, -1, -1, 0, 1, 2]), + mutate=rng.choice([-1, -1, 0, 1, 2]), changes=changes, actions=rng.choice(actions))) + yield row(setup, ops) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--prepare', action='store_true') + args = parser.parse_args() + build = lab.LOCAL / 'rust-live-attribute-updates-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/VanillaLiveAttributeUpdatesOracle.java' + helper = oracle.with_name('VanillaEntityAttributeOracle.java') + live_helper = oracle.with_name('VanillaLiveAttributesOracle.java') + sources = [oracle, helper, live_helper, 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/live_attribute_updates_fixture.rs', + lab.ROOT / 'crates/horizon-world/examples/live_attributes_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, helper, live_helper], 'compile-java') + + def java(side, mode): + run(['java', '-Xmx512M', '-cp', os.pathsep.join([str(classes), str(jars[side]), dependency_path]), + 'dev.horizonlab.parity.VanillaLiveAttributeUpdatesOracle', mode, build], mode) + + for side in jars: + java(side, 'export-' + side) + catalog = json.loads((build / 'catalog-server.json').read_text()) + if catalog != json.loads((build / 'catalog-client.json').read_text()): + raise AssertionError('Client/server attribute catalogs differ') + lab.write_json(build / 'catalog.json', catalog) + fixtures = list(corpus(catalog)) + inputs = build / 'inputs.txt' + inputs.write_text(''.join(json.dumps(row, separators=(',', ':')) + '\n' for row in fixtures), encoding='utf-8') + fixture_paths = [inputs, build / 'catalog.json', build / 'catalog-client.json', build / 'catalog-server.json'] + tested_fixtures = base.source_hashes(fixture_paths) + run(['cargo', 'build', '--release', '-p', 'horizon-world', '--example', 'live_attribute_updates_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' / ('live_attribute_updates_fixture.exe' if os.name == 'nt' else 'live_attribute_updates_fixture') + references = [] + for side, jar in jars.items(): + java(side, side) + enriched = build / f'inputs-{side}.txt' + fixture_paths.append(enriched) + tested_fixtures.update(base.source_hashes([enriched])) + run([binary, build, side], 'rust-' + side) + reference = build / f'{side}.txt' + candidate = build / f'rust-{side}.txt' + matched = base.compare(reference, candidate) + references.append(dict(side=side, jar_sha256=lab.digest(jar, 'sha256'), + output_sha256=lab.digest(reference, 'sha256'), matched_output_lines=matched, + rust_output_sha256=lab.digest(candidate, 'sha256'))) + print(f'{side}: {matched:,} live attribute update output lines match Rust', flush=True) + base.ensure_sources_unchanged(sources, tested_sources) + base.ensure_sources_unchanged(fixture_paths, tested_fixtures) + report = dict(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), + attributes=len(catalog['attributes']), entity_suppliers=len(catalog['entities']), + fastutil_sha256={str(p.relative_to(lab.ROOT)): lab.digest(p, 'sha256') for p in libraries if 'fastutil' in str(p)}, + sources_sha256=tested_sources, fixtures_sha256=tested_fixtures, references=references, + scope='Native LivingEntity attribute queries, notification and private refresh methods with registered AttributeMaps, real effect modifiers and pending queues, onAttributeUpdated health/absorption consequences and SynchedEntityData Float storage. Exercises all registered attributes/default suppliers, separate captured/selected maps and health stores, callbacks changing limits and routes, null pending members, client guards, failure prefixes and modifier/cache/queue identity. Replays JVM holder/instance hashes per reference. Scale dimension rebuild and waypoint-manager internals remain controlled; passenger lists are empty. Full effect lifecycle/dirty-particle synchronization, player absorption, complete gameplay and playable platform builds remain pending.') + lab.write_json(report_path, report) + print(f'Scoped live attribute update parity passed: {report_path}') + + +if __name__ == '__main__': + main()