diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9759367532..98cdd9e14f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,6 +95,10 @@ jobs: env: RUSTUP_TOOLCHAIN: stable run: python tools/test_rule_test_parity.py + - name: Compare simple-block placement and feature composition + env: + RUSTUP_TOOLCHAIN: stable + run: python tools/test_simple_block_parity.py - name: Compare ordered single-block replacement features env: RUSTUP_TOOLCHAIN: stable diff --git a/benchmarks/rust-simple-block-2026-10-06/scoped-parity.json b/benchmarks/rust-simple-block-2026-10-06/scoped-parity.json new file mode 100644 index 0000000000..416d5a0577 --- /dev/null +++ b/benchmarks/rust-simple-block-2026-10-06/scoped-parity.json @@ -0,0 +1,201 @@ +{ + "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": 492554, + "candidate_states": 35723, + "double_plant_blocks": 10, + "mossy_carpet_blocks": 1, + "sources_sha256": { + "mods/horizon/src/test/java/dev/horizonlab/parity/VanillaSimpleBlockOracle.java": "82204d06e279275c5967f6d5fd80afe7416ba6c5f5ec19c682a30942f2fb6d7a", + "mods/horizon/src/test/java/dev/horizonlab/parity/VanillaPlacementOracle.java": "12b1c46cc397755ef6827da777faef3e71c008cafa48403669c03ca2bdc3c7cb", + "mods/horizon/src/test/java/dev/horizonlab/parity/VanillaBlockSurvivalOracle.java": "4e7cc151617c1b02e872f9ca3e9fe4632379f225f4f07718ff6492df4ee551e7", + "mods/horizon/src/test/java/dev/horizonlab/parity/VanillaBlockPropertiesOracle.java": "99a396b9b983f6ca9149133251f3ece9aaf76d3d16dafa1f322378212a19490c", + "mods/horizon/src/test/java/dev/horizonlab/parity/VanillaBlockSupportOracle.java": "04350901a2d708fb8fa672de61d058f73f332d2e90df1b93a67781862f89c1ad", + "mods/horizon/src/test/java/dev/horizonlab/parity/VanillaBlockStateCodecOracle.java": "5c217a8cb72094020082bd4846ed675a6399b9fa371be95a221bd92d227b2e09", + "mods/horizon/src/test/java/dev/horizonlab/parity/VanillaCollisionOracle.java": "60ff9ef7c1d5e23c080f9aec95ab2e7d3b7c5b5bcd67b9efa9518776c400629a", + "mods/horizon/src/test/java/dev/horizonlab/parity/VanillaHeightmapOracle.java": "145c943637ed177cc4ef8a85f2484da8afd164868e84a8463882feec1653a860", + "tools/test_simple_block_parity.py": "710cc537a5abfb410c28c8121dea9f216a00f96ea60fafbf2c3f82e7621b6e4e", + "tools/test_block_survival_parity.py": "9e8dff3bd4b95dbfd29f8adc6a2f6d66abf19a6b1cd91423b794a345d4ec244a", + "tools/test_worldgen_parity.py": "b40972ab2ad222aa6d77537cc36b434a217baa1f1b56bbaad945d6e12fe776af", + "tools/test_rust_parity.py": "7efa699a38c5820424f5fc8992ee601dbfe26d6482da9d2dab2c17c41fe8f12a", + "tools/lab.py": "d00bf107fa01b6c579734a082c3ca4ade3018f0d6851d46844acf17574f66e64", + "Cargo.toml": "dc370d5fdde37238c3e4820046f11c3201020406a4edfa1308257c13edbdcda4", + "Cargo.lock": "7307d0c887364db36d327685ecf0671b75305e31e5ba5fbdcdbb15eaf156d20e", + "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/mod.rs": "dc92f73885dd0a7760743bf74f1ab8afa839d61cef2dfef890f8a23b67b4742e", + "crates/horizon-core/src/geometry/movement.rs": "930e34abef6f6eeec1ee7a6c920b5ba01bfe37b90678884490f4be53241f6b96", + "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": "47350edab5791f5074a54563ba98108818c5c6b1c5818a591388eab2ac9f5490", + "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/ticks/heap.rs": "bf28afab8af77c3d243aa96cd8a1fb8d934140a4dbd07dbfdbf4e7908bb4b845", + "crates/horizon-core/src/ticks/index.rs": "1f1a6a568c2a30eafecb020ca4d5f5fc414860de85b8b381fc419a2e3e65c24a", + "crates/horizon-core/src/ticks/mod.rs": "84852599dde78687d782f115a5b4b3ef6856c1f9cc92917255167e89c72ab01e", + "crates/horizon-world/src/bin/worldgen_parity.rs": "3143ceb9b56eecfd7667e5feb5f9f0cd13cfb8754b38d4ea338dbba97f2819da", + "crates/horizon-world/src/block_collision.rs": "cec2af81d212d5eed7a2196e4cf0be2daf4cecf53248f78cc68f2190e4d83f9a", + "crates/horizon-world/src/block_placement.rs": "524f4892d50bab77b7d89d19f7b048ffe746996f2ed7f376db2c6ab00ed75c24", + "crates/horizon-world/src/block_properties.rs": "b3caf26bd3afc9d10cdcca3f7e794c30abf5ba09afa1fe80ba8f44713acdb274", + "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/collision.rs": "05313e0c1f3089524fe029f9aac3e439cd47f1c9584c8c7e154c9adcfa88c335", + "crates/horizon-world/src/compression.rs": "4db73e6ca9a0d552d415ce60e7e88b89c9cf2b7d060a669ff8328f518316e101", + "crates/horizon-world/src/dynamic_support.rs": "e799b31e5120cfdb04e0b342f2097d610ed98ef8c4be031620ee00368d82df59", + "crates/horizon-world/src/entity_obstruction.rs": "a4b28a19bf83b23611c59d94535c488db37e4aee24b22c973c545910894a809f", + "crates/horizon-world/src/fluid.rs": "8a2834a9347ee2c2db7cf1c86a064907ec06e9ac16ba9cc3936e315fc4ad49ca", + "crates/horizon-world/src/heightmap.rs": "a5edef082b5af40d814421ea17e129b3b06d70abee6434ed263e4bba58571698", + "crates/horizon-world/src/lib.rs": "a8cd85f6223668c1446ff103276d80937b9707d9a7dc619a176b1b1ceed36a4a", + "crates/horizon-world/src/main.rs": "fce57418b8c22a6a0a9044baa89ed3d9c243bc2e8413fea3ff0f32985daba021", + "crates/horizon-world/src/nbt.rs": "60262c3de8c8f68bc2da0370ccb17393d32b85f5f5022c6358599a7d32119ea6", + "crates/horizon-world/src/piston_shape.rs": "399c2337103b8e7e0eb5667227c5aa14f9549d6a467b7be7da3d0717f0263500", + "crates/horizon-world/src/region.rs": "72b642e87fed37ed17f54d4c30f96c3b64d26c14bc0589c31ef40ff1616188fc", + "crates/horizon-world/src/registry/json.rs": "40974e8ddbca79f4a4f866f91f1241a7ac7289874527d4729483995dcf9b4f7e", + "crates/horizon-world/src/registry.rs": "566d7ef6a0d0d7e492d73b7f963ff0158544560a86ba33e179ef4d17e1274706", + "crates/horizon-world/src/tick_data.rs": "f8d04a69ea263f4168bce33f92e5b4ee2fe07d7caf50a1621c6b3ef6eb2479f3", + "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/biome.rs": "3fe316b875aa32683759c337dc71f624910faf0812a2d6d83b8098d6d0bac7a0", + "crates/horizon-world/src/worldgen/biome_chunk.rs": "d6389c8d1adf900eb8f0e6c8833feafe43dc92d74e5c7ffa6f3950975529d624", + "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/block_feature.rs": "2ea6d26fe88c633ad3efe7bdb6fc49f54961cbdf93b82919d79cf7d37a5c3c7d", + "crates/horizon-world/src/worldgen/block_predicate.rs": "074c692367e3f2191a1bb28d87b23402430a843c246bd30a4ea838ee3500280e", + "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/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/density.rs": "65a98c156a290a6b374ef670d28cd5dfbe0aa93171824d1bbd96422536273f84", + "crates/horizon-world/src/worldgen/feature.rs": "40500af71ee6628dfb27518262d99f08d0383ae6c7b9397452feef0c213eadd9", + "crates/horizon-world/src/worldgen/feature_catalog.rs": "486fe9d8588eb09e0ee5707a0541b0df167b63b0b56c90a2709bfa09a0a3ad19", + "crates/horizon-world/src/worldgen/feature_graph.rs": "1db1c7bffcb045e4365c3434a22ab9b2589a78753f897d17a0ef09ae735619c1", + "crates/horizon-world/src/worldgen/feature_region.rs": "2c78e5f1bdb08307f8d982f231f1abbc3306cd54348d9023b86ee6ed3516fd33", + "crates/horizon-world/src/worldgen/feature_sort.rs": "3488a093dfe1321aaa29d8a30cfb115d8e666a81bf195514e87b170de626835c", + "crates/horizon-world/src/worldgen/fluid_predicate.rs": "e682d13a21f90605215b79f5d254bf2e524d35e89eab2e3b46384709af80b847", + "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": "2fc100f8861fe0e0519a53dc4ae4a1eaa8a96aff2f6bf546ce613bdf665df237", + "crates/horizon-world/src/worldgen/placement/layer.rs": "a0f5ca776f33f9d4f72b8fdcd163426152e5e43cb4a7572fc1d4fc57bf965de1", + "crates/horizon-world/src/worldgen/placement.rs": "b0397e5818fae84358546193a95f84bcb0b5bffafce909e6fcb3386179ee8023", + "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/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": "7fb7130111654563f7217f7b84563adde0b11c3ebd6a499a514dc98ebef7c733", + "crates/horizon-world/src/worldgen/value_provider.rs": "3358b563db8cb0bc8a9cfbbe2b8b3e70941cd2d37a0b519fbb6545f8d0dcbb69", + "crates/horizon-world/src/worldgen/weighted.rs": "839840aae25302933bbd97a81a2336330e2d03a0ad1684e1e16ac769ed408e6b", + "crates/horizon-world/src/worldgen.rs": "c0190c771ec164902db6988a511b7b409c4fa44456548cb7bc40a520ccdf7a92", + "crates/horizon-world/examples/simple_block_fixture.rs": "d839de7fa2827189f8625903cedad84ab47c3e7384bed42e250f660f41d961dc" + }, + "fixtures_sha256": { + ".local/rust-simple-block-parity/inputs.txt": "c60ac2104d268f70971d19ad2d9d94b064b6161e2ca3e258b3eaee5c0e217adc", + ".local/rust-simple-block-parity/block-properties.nbt": "1aba22092f70d16b8092014c058ddc210e4aaa77bac32f6ec62e4d0e91d92a99", + ".local/rust-simple-block-parity/block-registry.nbt": "5042073c80753f9548cb8fdb5b1000f0e5a1220127a0cc3c6a75737e180bf9bb", + ".local/rust-simple-block-parity/cached-collision.nbt": "f3a5c6a5b2d51dad6c52e486e2b9989e79fd55db1a9bda6196db399550b04078", + ".local/rust-simple-block-parity/cached-support.nbt": "b6fdc9471b0b61e904206d3f099f4dc235aad6d723536c210be0cb7c3560d82c", + ".local/rust-simple-block-parity/catalog.json": "93581107e3564e60261044fdca61e3b7f237b9e6724c14b86e335ccc01198ba9", + ".local/rust-simple-block-parity/facts.json": "5e89a805514cabfe46e082d651db406b9479781e1d97b65faa2ab9f6f871309b", + ".local/rust-simple-block-parity/feature-catalog.json": "c048c761c5084a40560731955cc999a364d14055bdd86fa50989a92e4a771286", + ".local/rust-simple-block-parity/fluid-tags.json": "f1e5eb6d22ea7effbb118a2282709cfdeef1bfae4516b775a55ad7a4df29d7d0", + ".local/rust-simple-block-parity/placement-definitions.json": "f8a972ec87c2352500af6481158a3192bdbf699951025639bea744e4127b97c4", + ".local/rust-simple-block-parity/support-cached-states.json": "353945a58fe69b352925473f1693f9b14a9ad19f13c15159c4184f2c29258622", + ".local/rust-simple-block-parity/survival-classes.json": "361832008eea0aa9151d367a125a19c6353d99a0b88b27dd0682f55b7954553e", + ".local/rust-simple-block-parity/survival-extra-definitions.json": "170f2db5bc97a49fa25cc249540aa7f04beea455cdbaf43742ed9415f5090a02", + ".local/rust-simple-block-parity/survival-placement-classes.json": "7fdbf48ab18c87098bbe1b6176cb81f4842b6c5d9a4d6bbaf7037ff61cb1848c", + ".local/rust-simple-block-parity/survival-rules.json": "8805c18b3195bb1eac1f1e5281333df56e2892505f64023e5403c58ce2ec4368", + ".local/rust-simple-block-parity/survival-support-tags.json": "15d88d5bb5ad10d65124d5abb2cc055d251b06d129bca7be06f55b43dc13f6fd", + ".local/rust-simple-block-parity/tags.json": "36975358262568f32bf84778d63d9989a36e7394ed4796d70a34809a8f4a64f0" + }, + "rust_output_sha256": "844b94c4148519899e030393f1def423da6ab96353b73e8797e53bbb6ba958a6", + "references": [ + { + "side": "client", + "jar_sha256": "4508d006323f24fa02876310c192d739af56516eb259000ac50f0909a68c9a2d", + "output_sha256": "844b94c4148519899e030393f1def423da6ab96353b73e8797e53bbb6ba958a6", + "matched_output_lines": 9613872 + }, + { + "side": "server", + "jar_sha256": "a362163eec5d1612d520772bc16e5b39c09e3b234fdc045f56bf544284ee8ae6", + "output_sha256": "844b94c4148519899e030393f1def423da6ab96353b73e8797e53bbb6ba958a6", + "matched_output_lines": 9613872 + } + ], + "scope": "Native SimpleBlockFeature map codec, provider selection, actual state survival and single/double-plant/moss placement. Named feature/placed-feature/provider holders, selectors, sequences and placement modifier chains compose the same operations. Mutable world logs ordered reads, fluid/light/entity queries, writes and flags, rejected writes, thrown failures, scheduled block identities, repeated placements and both feature/world random tails. All candidate states and every above-state obstruction for representative double-plant definitions, waterlogging, moss attachment combinations and topper identities are exercised. Registry definition classes and immutable geometry are independently checked in both authenticated JARs. Actual chunks, neighbor updates, scheduled tick execution, entity lifecycle, complete feature registries and playable worlds remain pending." +} diff --git a/crates/horizon-world/examples/simple_block_fixture.rs b/crates/horizon-world/examples/simple_block_fixture.rs new file mode 100644 index 0000000000..a90e864b87 --- /dev/null +++ b/crates/horizon-world/examples/simple_block_fixture.rs @@ -0,0 +1,519 @@ +//! Ordered feature effects over a mutable registry-bound world. +use horizon_core::{ + geometry::{Direction, VoxelShape}, + random::{LegacyRandom, RandomSource, XoroshiroRandom}, +}; +use horizon_world::{ + block_collision::{BlockCollisionRegistry, DynamicCollisionWorld, ShulkerCollisionState}, + block_placement::BlockPlacementBindings, + block_properties::BlockPropertiesRegistry, + block_support::{BlockSupportRegistry, SupportType}, + block_survival::{BlockSurvivalRegistry, SurvivalDefinitions, SurvivalRule, SurvivalWorld}, + collision::CachedCollisionRegistry, + dynamic_support::DynamicSupportWorld, + fluid::{FluidKind, FluidState, FluidStateTable}, + heightmap::HeightmapType, + nbt::{self, Limits, Tag}, + registry::{BlockRegistry, BlockStateId}, + worldgen::{ + anchor::WorldGenerationContext, + block_feature::FeatureBlockBindings, + feature::{FeatureBlocks, FeatureWorld}, + feature_catalog::FeatureCatalog, + feature_graph::{FeatureDefinition, PlacedFeatureDefinition}, + placement::{PlacementContext, PlacementQueryError}, + simple_block::SimpleBlockFeature, + state_provider::{StateProviderBindings, StateProviderCatalog}, + tags::TagDefinitions, + }, +}; +use serde_json::Value; +use std::{ + collections::BTreeMap, + fs, + io::{self, BufRead, Write}, + path::Path, + sync::Arc, +}; +struct World<'a, 'r, W, R> { + out: &'a mut W, + random: R, + states: BTreeMap<[i32; 3], BlockStateId>, + fallback: BlockStateId, + fluid: Option, + fluids: &'a FluidStateTable, + water_mask: u8, + survival: &'a BlockSurvivalRegistry<'r>, + support: &'a BlockSupportRegistry<'r>, + calls: i32, + fail: i32, + reject: i32, + brightness: i32, +} +impl World<'_, '_, W, R> { + fn event(&mut self, text: std::fmt::Arguments<'_>) -> io::Result { + writeln!(self.out, "{text}")?; + let call = self.calls; + self.calls += 1; + if call == self.fail { + return Err(io::Error::other("injected world failure")); + } + Ok(call) + } + fn at(&self, p: [i32; 3]) -> BlockStateId { + self.states.get(&p).copied().unwrap_or(self.fallback) + } + fn get(&mut self, p: [i32; 3]) -> io::Result { + self.event(format_args!("get {} {} {}", p[0], p[1], p[2]))?; + let state = self.at(p); + writeln!(self.out, "state {}", state.0)?; + Ok(state) + } + fn entity(&mut self, p: [i32; 3]) -> io::Result<()> { + self.event(format_args!("entity {} {} {}", p[0], p[1], p[2]))?; + Ok(()) + } +} +impl DynamicSupportWorld for World<'_, '_, W, R> { + type Error = io::Error; + fn shulker_is_closed(&mut self, p: [i32; 3]) -> io::Result> { + self.entity(p)?; + Ok(None) + } + fn moving_piston_shape(&mut self, p: [i32; 3]) -> io::Result> { + self.entity(p)?; + Ok(None) + } +} +impl DynamicCollisionWorld for World<'_, '_, W, R> { + fn shulker_collision_state( + &mut self, + p: [i32; 3], + ) -> io::Result> { + self.entity(p)?; + Ok(None) + } +} +impl SurvivalWorld for World<'_, '_, W, R> { + fn block_state(&mut self, p: [i32; 3]) -> io::Result { + self.get(p) + } + fn fluid_state(&mut self, p: [i32; 3]) -> io::Result { + self.event(format_args!("fluid {} {} {}", p[0], p[1], p[2]))?; + self.fluids + .get(self.fluid.unwrap_or_else(|| self.at(p))) + .ok_or_else(|| io::Error::other("unknown fluid")) + } + fn raw_brightness(&mut self, p: [i32; 3], sky_darken: i32) -> io::Result { + self.event(format_args!( + "light {} {} {} {sky_darken}", + p[0], p[1], p[2] + ))?; + Ok(self.brightness) + } +} +impl FeatureWorld for World<'_, '_, W, R> { + type Error = io::Error; + fn block(&mut self, p: [i32; 3]) -> io::Result { + self.get(p) + } + fn set_block(&mut self, p: [i32; 3], state: BlockStateId, flags: i32) -> io::Result { + let call = self.event(format_args!( + "set {} {} {} {} {flags}", + p[0], p[1], p[2], state.0 + ))?; + let result = self.reject == 0 || call % self.reject != 0; + if result { + self.states.insert(p, state); + } + writeln!(self.out, "written {result}")?; + Ok(result) + } +} +fn query(error: impl std::fmt::Display) -> PlacementQueryError { + PlacementQueryError::Failed(error.to_string()) +} +impl PlacementContext for World<'_, '_, W, R> { + fn water_at(&mut self, p: [i32; 3]) -> Result { + Ok(self.fluid_state(p).map_err(query)?.kind().mask() & self.water_mask != 0) + } + fn attachment_at( + &mut self, + p: [i32; 3], + direction: Direction, + ) -> Result { + let survival = self.survival; + survival.can_attach_to(self, p, direction).map_err(query) + } + fn next_world_boolean(&mut self) -> Result { + Ok(self.random.next_boolean()) + } + fn schedule_block_tick( + &mut self, + p: [i32; 3], + block: BlockStateId, + delay: i32, + ) -> Result<(), PlacementQueryError> { + self.event(format_args!( + "tick {} {} {} {} {delay}", + p[0], p[1], p[2], block.0 + )) + .map_err(query)?; + Ok(()) + } + fn generation(&self) -> WorldGenerationContext { + WorldGenerationContext::new(-64, 384, 63) + } + fn min_y(&mut self) -> Result { + Ok(-64) + } + fn max_y(&mut self) -> Result { + Ok(319) + } + fn height(&mut self, _: HeightmapType, _: i32, _: i32) -> Result { + Err(PlacementQueryError::Unavailable) + } + fn block_state(&mut self, p: [i32; 3]) -> Result { + self.get(p).map_err(query) + } + fn block_survives( + &mut self, + state: BlockStateId, + p: [i32; 3], + ) -> Result { + let survival = self.survival; + survival.can_survive(self, state, p).map_err(query) + } + fn face_sturdy( + &mut self, + p: [i32; 3], + direction: Direction, + ) -> Result { + let state = self.get(p).map_err(query)?; + let support = self.support; + support + .is_face_sturdy(self, state, p, direction, SupportType::Full) + .map_err(query) + } +} +enum Feature { + Map(SimpleBlockFeature), + Graph(Box), + Placed(Box), +} +#[allow(clippy::too_many_arguments)] +fn fixture( + f: &Value, + mut random: R, + world_random: R, + providers: &StateProviderBindings<'_>, + catalog: &StateProviderCatalog, + placement: &BlockPlacementBindings<'_>, + feature_bindings: &FeatureBlockBindings<'_>, + feature_catalog: &FeatureCatalog, + blocks: FeatureBlocks, + survival: &BlockSurvivalRegistry<'_>, + support: &BlockSupportRegistry<'_>, + fluids: &FluidStateTable, + water_mask: u8, + out: &mut impl Write, +) -> Result<(), Box> { + let json = f["definition"].as_str().unwrap(); + let feature = match f["mode"].as_str().unwrap_or("map") { + "map" => { + SimpleBlockFeature::from_json(json, providers, catalog, placement).map(Feature::Map) + } + "feature" => feature_catalog + .feature_with_blocks(json, feature_bindings) + .map(|v| Feature::Graph(Box::new(v))), + _ => feature_catalog + .placed_feature_with_blocks(json, feature_bindings) + .map(|v| Feature::Placed(Box::new(v))), + }; + let mut world = World { + out, + random: world_random, + states: f["states"] + .as_array() + .unwrap() + .iter() + .map(|v| { + ( + std::array::from_fn(|i| v[i].as_i64().unwrap() as i32), + BlockStateId(v[3].as_u64().unwrap() as u32), + ) + }) + .collect(), + fallback: BlockStateId(f["fallback"].as_u64().unwrap() as u32), + fluid: f + .get("fluid") + .map(|v| BlockStateId(v.as_u64().unwrap() as u32)), + fluids, + water_mask, + survival, + support, + calls: 0, + fail: f["fail"].as_i64().unwrap() as i32, + reject: f["reject"].as_i64().unwrap() as i32, + brightness: f["brightness"].as_i64().unwrap() as i32, + }; + match feature { + Err(_) => writeln!(world.out, "decode error")?, + Ok(feature) => { + writeln!(world.out, "decode ok")?; + let origin = std::array::from_fn(|i| f["origin"][i].as_i64().unwrap() as i32); + for _ in 0..f["repeats"].as_u64().unwrap() { + world.calls = 0; + let result = match &feature { + Feature::Map(feature) => feature + .place(&mut world, &mut random, origin) + .map_err(|_| ()), + Feature::Graph(feature) => feature + .place(&mut world, &mut random, origin, blocks) + .map_err(|_| ()), + Feature::Placed(feature) => feature + .place(&mut world, &mut random, origin, blocks) + .map_err(|_| ()), + }; + match result { + Ok(result) => writeln!(world.out, "place {result}")?, + Err(_) => writeln!(world.out, "place error")?, + } + writeln!( + world.out, + "next {} {}", + random.next_long(), + world.random.next_long() + )?; + writeln!(world.out, "calls {}", world.calls)?; + for (p, state) in &world.states { + writeln!(world.out, "stored {} {} {} {}", p[0], p[1], p[2], state.0)?; + } + writeln!(world.out, "end")?; + } + } + } + writeln!( + world.out, + "tail {} {}", + random.next_long(), + world.random.next_long() + )?; + Ok(()) +} +fn main() -> Result<(), Box> { + let arg = std::env::args().nth(1).expect("private fixture directory"); + let dir = Path::new(&arg); + let read = |name: &str| -> Result> { + Ok( + nbt::read_named(&fs::read(dir.join(name))?, Limits::default())? + .0 + .tag, + ) + }; + let registry = BlockRegistry::from_nbt(&read("block-registry.nbt")?)?; + let collisions = CachedCollisionRegistry::from_nbt(&read("cached-collision.nbt")?, ®istry)?; + let properties = + BlockPropertiesRegistry::from_nbt(&read("block-properties.nbt")?, &collisions)?; + let support = Arc::new(BlockSupportRegistry::from_nbt( + &read("cached-support.nbt")?, + ®istry, + )?); + let resources: BTreeMap = + serde_json::from_str(&fs::read_to_string(dir.join("tags.json"))?)?; + let mut tags = TagDefinitions::default(); + for (name, text) in resources { + tags.apply_json(&name, "vanilla", &text)?; + } + let tags = tags.build(|name, _| registry.default_state(name).map(|_| name.to_owned()))?; + assert!(tags.missing.is_empty()); + let definitions: BTreeMap = + serde_json::from_str(&fs::read_to_string(dir.join("survival-rules.json"))?)?; + let definitions = definitions + .into_iter() + .map(|(name, rule)| { + use SurvivalRule::*; + let rule = match rule.as_str() { + value if value.starts_with("TagBelow:") => TagBelow(value[9..].to_owned()), + "CactusFlower" => CactusFlower, + "Seagrass" => Seagrass, + "BubbleColumn" => BubbleColumn, + "RedstoneWire" => RedstoneWire, + "DoublePlant" => DoublePlant, + "Door" => Door, + "BigDripleaf" => BigDripleaf, + "BigDripleafStem" => BigDripleafStem, + "Farmland" => Farmland, + "Cocoa" => Cocoa, + "Crop" => Crop, + "GrowingPlant" => GrowingPlant, + "MangrovePropagule" => MangrovePropagule, + "Speleothem" => Speleothem, + "Scaffolding" => Scaffolding, + "PistonHead" => PistonHead, + "ChorusPlant" => ChorusPlant, + "ChorusFlower" => ChorusFlower, + "PitcherCrop" => PitcherCrop, + "Multiface" => Multiface, + "Vine" => Vine, + "HangingMoss" => HangingMoss, + "SnowLayer" => SnowLayer, + "SeaPickle" => SeaPickle, + "Fire" => Fire, + "Cactus" => Cactus, + "Path" => Path, + "Mushroom" => Mushroom, + "Bell" => Bell, + "SugarCane" => SugarCane, + "LilyPad" => LilyPad, + "Frogspawn" => Frogspawn, + "SporeBlossom" => SporeBlossom, + "SmallDripleaf" => SmallDripleaf, + "TallSeagrass" => TallSeagrass, + "Always" => Always, + "SolidBelow" => SolidBelow, + "SolidBehind" => SolidBehind, + "NonAirBelow" => NonAirBelow, + "MossCarpet" => MossCarpet, + "CenterBelow" => CenterBelow, + "Lantern" => Lantern, + "FullAbove" => FullAbove, + "FullBehind" => FullBehind, + "RigidOrCenterBelow" => RigidOrCenterBelow, + "RigidBelow" => RigidBelow, + "FullBelow" => FullBelow, + "CenterAbove" => CenterAbove, + "FaceAttached" => FaceAttached, + "SupportsBamboo" => SupportsBamboo, + _ => panic!("unknown rule {rule}"), + }; + (name, rule) + }) + .collect(); + let fluid_resources: BTreeMap = + serde_json::from_str(&fs::read_to_string(dir.join("fluid-tags.json"))?)?; + let mut fluid_tags = TagDefinitions::default(); + for (name, text) in fluid_resources { + fluid_tags.apply_json(&name, "vanilla", &text)?; + } + let fluid_tags = + fluid_tags.build(|name, _| FluidKind::from_name(name).map(|_| name.to_owned()))?; + assert!(fluid_tags.missing.is_empty()); + let fluids = FluidStateTable::new(®istry)?; + let collision = Arc::new(BlockCollisionRegistry::new(Arc::new(collisions))?); + let extra: Value = serde_json::from_str(&fs::read_to_string( + dir.join("survival-extra-definitions.json"), + )?)?; + let definitions = SurvivalDefinitions { + rules: definitions, + ignition_odds: serde_json::from_value(extra["ignition_odds"].clone())?, + fence_gates: serde_json::from_value(extra["fence_gates"].clone())?, + }; + let survival = BlockSurvivalRegistry::new( + ®istry, + &properties, + support.clone(), + collision, + &definitions, + &tags.tags, + &fluid_tags.tags, + )?; + let facts: Value = serde_json::from_str(&fs::read_to_string(dir.join("facts.json"))?)?; + let integers: std::collections::BTreeSet<(String, String)> = facts["integers"] + .as_array() + .unwrap() + .iter() + .map(|v| { + ( + v[0].as_str().unwrap().to_owned(), + v[1].as_str().unwrap().to_owned(), + ) + }) + .collect(); + let providers = StateProviderBindings::new(®istry, integers.clone(), &tags.tags)? + .with_block_properties(&properties)? + .with_fluid_tags(&fluid_tags.tags)?; + let resources: BTreeMap = + serde_json::from_str(&fs::read_to_string(dir.join("catalog.json"))?)?; + let catalog = StateProviderCatalog::from_resources(&resources)?; + let definitions: Value = + serde_json::from_str(&fs::read_to_string(dir.join("placement-definitions.json"))?)?; + let placement = BlockPlacementBindings::new( + ®istry, + &properties, + &serde_json::from_value(definitions["double_plants"].clone())?, + &serde_json::from_value(definitions["mossy_carpets"].clone())?, + )?; + let feature_bindings = + FeatureBlockBindings::with_tags(®istry, integers.clone(), &tags.tags)? + .with_block_properties(&properties)? + .with_fluid_tags(&fluid_tags.tags)? + .with_state_providers( + StateProviderBindings::new(®istry, integers, &tags.tags)? + .with_block_properties(&properties)? + .with_fluid_tags(&fluid_tags.tags)?, + StateProviderCatalog::from_resources(&resources)?, + BlockPlacementBindings::new( + ®istry, + &properties, + &serde_json::from_value(definitions["double_plants"].clone())?, + &serde_json::from_value(definitions["mossy_carpets"].clone())?, + )?, + )?; + let feature_resources: Value = + serde_json::from_str(&fs::read_to_string(dir.join("feature-catalog.json"))?)?; + let feature_catalog = FeatureCatalog::from_resources( + &serde_json::from_value(feature_resources["features"].clone())?, + &serde_json::from_value(feature_resources["placed"].clone())?, + &TagDefinitions::default(), + )?; + let blocks = FeatureBlocks::from_registry(®istry)?; + let water_mask = fluid_tags.tags["minecraft:water"] + .iter() + .fold(0, |mask, name| { + mask | FluidKind::from_name(name).unwrap().mask() + }); + let mut out = io::BufWriter::new(fs::File::create(dir.join("rust.txt"))?); + for line in io::BufReader::new(fs::File::open(dir.join("inputs.txt"))?).lines() { + let f: Value = serde_json::from_str(&line?)?; + let seed = f["seed"].as_i64().unwrap(); + let world_seed = f["world_seed"].as_i64().unwrap(); + if f["legacy"].as_bool().unwrap() { + fixture( + &f, + LegacyRandom::new(seed), + LegacyRandom::new(world_seed), + &providers, + &catalog, + &placement, + &feature_bindings, + &feature_catalog, + blocks, + &survival, + &support, + &fluids, + water_mask, + &mut out, + )?; + } else { + fixture( + &f, + XoroshiroRandom::new(seed), + XoroshiroRandom::new(world_seed), + &providers, + &catalog, + &placement, + &feature_bindings, + &feature_catalog, + blocks, + &survival, + &support, + &fluids, + water_mask, + &mut out, + )?; + } + } + out.flush()?; + Ok(()) +} diff --git a/crates/horizon-world/src/block_placement.rs b/crates/horizon-world/src/block_placement.rs new file mode 100644 index 0000000000..cb18e55f5f --- /dev/null +++ b/crates/horizon-world/src/block_placement.rs @@ -0,0 +1,376 @@ +//! Block placement operations used by the simple-block world-generation feature. +use crate::{ + block_properties::{BlockPropertiesRegistry, BlockProperty, BoundBlockProperty}, + compression::invalid, + fluid::FluidStateTable, + registry::{BlockRegistry, BlockStateId}, + worldgen::{ + feature::FeatureWorld, + placement::{PlacementContext, PlacementQueryError}, + }, +}; +use horizon_core::geometry::Direction; +use std::{collections::BTreeSet, io, sync::Arc}; + +const HORIZONTAL: [Direction; 4] = [ + Direction::North, + Direction::East, + Direction::South, + Direction::West, +]; +const SIDE_NAMES: [&str; 4] = ["north", "east", "south", "west"]; + +#[derive(Clone, Copy, Debug)] +enum Placement { + Single, + Double(usize), + Moss, +} +#[derive(Clone, Copy, Debug)] +struct DoublePlant { + halves: [[BlockStateId; 2]; 2], + waterlogged: bool, +} +#[derive(Clone, Copy, Debug)] +struct Moss { + bottom: bool, + sides: [u8; 4], +} +impl Moss { + fn code(self) -> usize { + self.sides + .into_iter() + .fold(usize::from(self.bottom), |code, side| { + code * 3 + usize::from(side) + }) + } + fn has_faces(self) -> bool { + self.bottom || self.sides.into_iter().any(|side| side != 0) + } +} +#[derive(Debug)] +pub enum BlockPlacementError { + World(E), + Registry(io::Error), + Query(PlacementQueryError), +} +impl std::fmt::Display for BlockPlacementError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::World(error) => error.fmt(f), + Self::Registry(error) => error.fmt(f), + Self::Query(error) => error.fmt(f), + } + } +} +impl std::error::Error for BlockPlacementError {} + +pub struct BlockPlacementBindings<'r> { + registry: &'r BlockRegistry, + operations: Arc, +} +/// Precompiled transitions shared by decoded features in one registry snapshot. +#[derive(Debug)] +pub struct BlockPlacement { + kinds: Vec, + doubles: Vec, + replaceable: BoundBlockProperty, + fluids: FluidStateTable, + air: [BlockStateId; 3], + defaults: Vec, + moss: Vec>, + moss_ids: [BlockStateId; 162], + moss_default: BlockStateId, +} +impl<'r> BlockPlacementBindings<'r> { + pub fn new( + registry: &'r BlockRegistry, + properties: &BlockPropertiesRegistry<'r>, + double_plants: &BTreeSet, + mossy_carpets: &BTreeSet, + ) -> io::Result { + if !std::ptr::eq(registry, properties.registry()) { + return Err(invalid("placement properties use another registry")); + } + for name in double_plants.iter().chain(mossy_carpets) { + if registry.default_state(name).is_none() { + return Err(invalid("unknown placement definition")); + } + } + let default = |name: &str| { + registry + .default_state(name) + .ok_or_else(|| invalid("missing placement block")) + }; + let mut kinds = Vec::with_capacity(registry.len()); + let mut doubles = Vec::new(); + let mut defaults = Vec::new(); + let mut moss = Vec::new(); + let mut moss_ids = [None; 162]; + for index in 0..registry.len() { + let id = BlockStateId(index as u32); + let state = registry.state(id).unwrap(); + defaults.push(default(&state.name)?); + let kind = if double_plants.contains(&state.name) { + let waterlogged = state.properties.contains_key("waterlogged"); + let mut halves = [[id; 2]; 2]; + for (half, values) in halves.iter_mut().enumerate() { + for (water, target) in values.iter_mut().enumerate() { + let mut descriptor = state.clone(); + descriptor.properties.insert( + "half".into(), + if half == 0 { "lower" } else { "upper" }.into(), + ); + if waterlogged { + descriptor.properties.insert( + "waterlogged".into(), + if water == 0 { "false" } else { "true" }.into(), + ); + } + *target = registry + .id(&descriptor) + .ok_or_else(|| invalid("missing double-plant transition"))?; + } + } + let selected = doubles.len(); + doubles.push(DoublePlant { + halves, + waterlogged, + }); + Placement::Double(selected) + } else if mossy_carpets.contains(&state.name) { + Placement::Moss + } else { + Placement::Single + }; + kinds.push(kind); + let value = if state.name == "minecraft:pale_moss_carpet" { + let bottom = match state.properties.get("bottom").map(String::as_str) { + Some("true") => true, + Some("false") => false, + _ => return Err(invalid("invalid moss bottom")), + }; + let mut sides = [0; 4]; + for (slot, name) in sides.iter_mut().zip(SIDE_NAMES) { + *slot = match state.properties.get(name).map(String::as_str) { + Some("none") => 0, + Some("low") => 1, + Some("tall") => 2, + _ => return Err(invalid("invalid moss side")), + }; + } + let value = Moss { bottom, sides }; + if moss_ids[value.code()].replace(id).is_some() { + return Err(invalid("ambiguous moss state")); + } + Some(value) + } else { + None + }; + moss.push(value); + } + let moss_ids = moss_ids + .into_iter() + .collect::>>() + .ok_or_else(|| invalid("incomplete moss states"))? + .try_into() + .map_err(|_| invalid("invalid moss state count"))?; + Ok(Self { + registry, + operations: Arc::new(BlockPlacement { + kinds, + doubles, + replaceable: properties.bind(BlockProperty::Replaceable), + fluids: FluidStateTable::new(registry)?, + air: [ + default("minecraft:air")?, + default("minecraft:cave_air")?, + default("minecraft:void_air")?, + ], + defaults, + moss, + moss_ids, + moss_default: default("minecraft:pale_moss_carpet")?, + }), + }) + } + pub fn registry(&self) -> &'r BlockRegistry { + self.registry + } + pub fn operations(&self) -> Arc { + self.operations.clone() + } +} +impl BlockPlacement { + fn read( + &self, + world: &mut W, + position: [i32; 3], + ) -> Result> { + let state = world.block(position).map_err(BlockPlacementError::World)?; + if state.0 as usize >= self.kinds.len() { + return Err(BlockPlacementError::Registry(invalid( + "unregistered placement world state", + ))); + } + Ok(state) + } + fn moss_state(&self, state: BlockStateId) -> Option { + self.moss[state.0 as usize] + } + fn updated_moss( + &self, + world: &mut W, + state: Moss, + position: [i32; 3], + force: bool, + ) -> Result> { + let force = force | state.bottom; + let mut result = state; + let mut above = None; + let mut below = None; + for (index, direction) in HORIZONTAL.into_iter().enumerate() { + let mut side = if world + .attachment_at(position, direction) + .map_err(BlockPlacementError::Query)? + { + if force { 1 } else { state.sides[index] } + } else { + 0 + }; + if side == 1 { + let upper = match above { + Some(id) => id, + None => { + let id = self.read(world, relative(position, Direction::Up))?; + above = Some(id); + id + } + }; + if self + .moss_state(upper) + .is_some_and(|value| !value.bottom && value.sides[index] != 0) + { + side = 2; + } + if !state.bottom { + let lower = match below { + Some(id) => id, + None => { + let id = self.read(world, relative(position, Direction::Down))?; + below = Some(id); + id + } + }; + if self + .moss_state(lower) + .is_some_and(|value| value.sides[index] == 0) + { + side = 0; + } + } + } + result.sides[index] = side; + } + Ok(result) + } + fn topper( + &self, + world: &mut W, + position: [i32; 3], + ) -> Result, BlockPlacementError> { + let above = relative(position, Direction::Up); + let current = self.read(world, above)?; + if self.moss_state(current).is_some_and(|value| value.bottom) + || (self.moss_state(current).is_none() && !self.replaceable.contains(current)) + { + return Ok(None); + } + let mut state = self.moss_state(self.moss_default).unwrap(); + state.bottom = false; + state = self.updated_moss(world, state, above, true)?; + for side in &mut state.sides { + if *side != 0 + && !world + .next_world_boolean() + .map_err(BlockPlacementError::Query)? + { + *side = 0; + } + } + let id = self.moss_ids[state.code()]; + Ok((state.has_faces() && id != current).then_some(id)) + } + pub fn place( + &self, + world: &mut W, + state: BlockStateId, + position: [i32; 3], + flags: i32, + ) -> Result> { + let kind = *self.kinds.get(state.0 as usize).ok_or_else(|| { + BlockPlacementError::Registry(invalid("unregistered placement candidate")) + })?; + match kind { + Placement::Single => { + world + .set_block(position, state, flags) + .map_err(BlockPlacementError::World)?; + } + Placement::Double(index) => { + let above = relative(position, Direction::Up); + let current = self.read(world, above)?; + if !self.air.contains(¤t) + && (self.fluids.get(state) != self.fluids.get(current) + || !self.replaceable.contains(current)) + { + return Ok(false); + } + let plant = self.doubles[index]; + for (half, at) in [position, above].into_iter().enumerate() { + let water = plant.waterlogged + && world.water_at(at).map_err(BlockPlacementError::Query)?; + world + .set_block(at, plant.halves[half][usize::from(water)], flags) + .map_err(BlockPlacementError::World)?; + } + } + Placement::Moss => { + let lower = self.updated_moss( + world, + self.moss_state(self.moss_default).unwrap(), + position, + true, + )?; + world + .set_block(position, self.moss_ids[lower.code()], flags) + .map_err(BlockPlacementError::World)?; + if let Some(top) = self.topper(world, position)? { + world + .set_block(relative(position, Direction::Up), top, flags) + .map_err(BlockPlacementError::World)?; + let lower = self.updated_moss(world, lower, position, true)?; + world + .set_block(position, self.moss_ids[lower.code()], flags) + .map_err(BlockPlacementError::World)?; + } + } + } + Ok(true) + } + pub fn schedule_tick( + &self, + world: &mut W, + position: [i32; 3], + delay: i32, + ) -> Result<(), BlockPlacementError> { + let state = self.read(world, position)?; + world + .schedule_block_tick(position, self.defaults[state.0 as usize], delay) + .map_err(BlockPlacementError::Query) + } +} +fn relative(position: [i32; 3], direction: Direction) -> [i32; 3] { + let step = direction.normal(); + std::array::from_fn(|i| position[i].wrapping_add(step[i])) +} diff --git a/crates/horizon-world/src/block_survival.rs b/crates/horizon-world/src/block_survival.rs index 0140729a2c..bb21f46df9 100644 --- a/crates/horizon-world/src/block_survival.rs +++ b/crates/horizon-world/src/block_survival.rs @@ -578,6 +578,19 @@ impl<'r> BlockSurvivalRegistry<'r> { .get(direction_name(direction)) .is_some_and(|value| value == "true") } + /// Native multiface attachment at a candidate position, testing support + /// first and then empty-context collision on the neighbor's opposite face. + pub fn can_attach_to( + &self, + world: &mut W, + position: [i32; 3], + direction: Direction, + ) -> Result> { + let step = direction.normal(); + let at = std::array::from_fn(|i| position[i].wrapping_add(step[i])); + let neighbor = self.read(world, at)?; + self.can_attach(world, neighbor, at, direction) + } fn can_attach( &self, world: &mut W, diff --git a/crates/horizon-world/src/lib.rs b/crates/horizon-world/src/lib.rs index f3cea70283..80c53eae86 100644 --- a/crates/horizon-world/src/lib.rs +++ b/crates/horizon-world/src/lib.rs @@ -2,6 +2,7 @@ #![forbid(unsafe_code)] pub mod block_collision; +pub mod block_placement; pub mod block_properties; pub mod block_support; pub mod block_survival; diff --git a/crates/horizon-world/src/worldgen.rs b/crates/horizon-world/src/worldgen.rs index 8642bdc087..45e7ad6715 100644 --- a/crates/horizon-world/src/worldgen.rs +++ b/crates/horizon-world/src/worldgen.rs @@ -31,6 +31,7 @@ pub mod placement; pub mod router; pub mod rule_test; pub mod settings; +pub mod simple_block; pub mod state_provider; pub mod tags; pub mod terrain; diff --git a/crates/horizon-world/src/worldgen/block_feature.rs b/crates/horizon-world/src/worldgen/block_feature.rs index 4684a4eb47..9b93d94d0d 100644 --- a/crates/horizon-world/src/worldgen/block_feature.rs +++ b/crates/horizon-world/src/worldgen/block_feature.rs @@ -16,6 +16,11 @@ use std::{ pub struct FeatureBlockBindings<'r> { registry: &'r BlockRegistry, + pub(crate) simple_blocks: Option<( + super::state_provider::StateProviderBindings<'r>, + super::state_provider::StateProviderCatalog, + crate::block_placement::BlockPlacementBindings<'r>, + )>, physical_properties: Option<[BoundBlockProperty; 4]>, biomes: Option>, fluids: super::fluid_predicate::FluidPredicateBindings, @@ -25,6 +30,24 @@ pub struct FeatureBlockBindings<'r> { pub(crate) air: [BlockStateId; 3], } impl<'r> FeatureBlockBindings<'r> { + /// Bind provider resources and precompiled placement operations to this registry. + pub fn with_state_providers( + mut self, + providers: super::state_provider::StateProviderBindings<'r>, + catalog: super::state_provider::StateProviderCatalog, + placement: crate::block_placement::BlockPlacementBindings<'r>, + ) -> Result { + if !std::ptr::eq(self.registry, providers.registry()) + || !std::ptr::eq(self.registry, placement.registry()) + { + return Err(DecodeError::Field( + "feature provider/placement registry identity", + )); + } + self.simple_blocks = Some((providers, catalog, placement)); + Ok(self) + } + pub fn with_block_properties( mut self, properties: &BlockPropertiesRegistry<'r>, @@ -103,6 +126,7 @@ impl<'r> FeatureBlockBindings<'r> { }; Ok(Self { registry, + simple_blocks: None, codec, biomes: None, physical_properties: None, diff --git a/crates/horizon-world/src/worldgen/feature_graph.rs b/crates/horizon-world/src/worldgen/feature_graph.rs index 5b2d48c940..8e11f8b458 100644 --- a/crates/horizon-world/src/worldgen/feature_graph.rs +++ b/crates/horizon-world/src/worldgen/feature_graph.rs @@ -28,6 +28,7 @@ pub struct PlacedFeatureDefinition { #[derive(Clone, Debug)] pub enum FeatureDefinition { Basic(BasicFeature), + SimpleBlock(super::simple_block::SimpleBlockFeature), FillLayer(FillLayerFeature), ReplaceBlock(ReplaceBlockFeature), ScatteredOre(ScatteredOreFeature), @@ -44,6 +45,7 @@ pub enum FeatureDefinition { #[derive(Debug)] pub enum FeatureExecutionError { Basic(FeatureError), + SimpleBlock(super::simple_block::SimpleBlockError), Random(InvalidBound), Query(PlacementQueryError), WorkLimit, @@ -53,6 +55,7 @@ impl std::fmt::Display for FeatureExecutionError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Basic(e) => e.fmt(f), + Self::SimpleBlock(e) => e.fmt(f), Self::Random(e) => e.fmt(f), Self::Query(e) => e.fmt(f), Self::WorkLimit => f.write_str("feature graph work limit exceeded"), @@ -218,6 +221,15 @@ impl FeatureDefinition { )?)?; match kind.as_str() { "minecraft:no_op" => Ok(Self::NoOp), + "minecraft:simple_block" => { + let (providers, catalog, placement) = blocks + .and_then(|blocks| blocks.simple_blocks.as_ref()) + .ok_or(DecodeError::Field("simple block bindings unavailable"))?; + super::simple_block::SimpleBlockFeature::decode( + json, providers, catalog, placement, depth, remaining, + ) + .map(Self::SimpleBlock) + } "minecraft:fill_layer" => FillLayerFeature::from_json( json, blocks.ok_or_else(|| { @@ -393,6 +405,9 @@ impl FeatureDefinition { .place(world, random, origin, run.blocks) .map_err(FeatureExecutionError::Basic), Self::NoOp => Ok(true), + Self::SimpleBlock(feature) => feature + .place(world, random, origin) + .map_err(FeatureExecutionError::SimpleBlock), Self::ScatteredOre(feature) => feature .place(world, random, origin) .map_err(FeatureExecutionError::Basic), diff --git a/crates/horizon-world/src/worldgen/placement/context.rs b/crates/horizon-world/src/worldgen/placement/context.rs index 695f0bde60..7e8a5a871d 100644 --- a/crates/horizon-world/src/worldgen/placement/context.rs +++ b/crates/horizon-world/src/worldgen/placement/context.rs @@ -26,6 +26,32 @@ pub enum LayerBlock { Ground, } pub trait PlacementContext { + /// Test the current fluid at this position against the world's water tag. + fn water_at(&mut self, _position: [i32; 3]) -> Result { + Err(PlacementQueryError::Unavailable) + } + /// Multiface attachment: neighbor support, then empty-context collision. + fn attachment_at( + &mut self, + _position: [i32; 3], + _direction: horizon_core::geometry::Direction, + ) -> Result { + Err(PlacementQueryError::Unavailable) + } + /// Draw from the world's random stream, independently of the feature RNG. + fn next_world_boolean(&mut self) -> Result { + Err(PlacementQueryError::Unavailable) + } + /// Schedule the block type identified by its registry default state. + fn schedule_block_tick( + &mut self, + _position: [i32; 3], + _block: crate::registry::BlockStateId, + _delay: i32, + ) -> Result<(), PlacementQueryError> { + Err(PlacementQueryError::Unavailable) + } + /// Evaluate this candidate state's survival without placing it. fn block_survives( &mut self, diff --git a/crates/horizon-world/src/worldgen/simple_block.rs b/crates/horizon-world/src/worldgen/simple_block.rs new file mode 100644 index 0000000000..a732765c5a --- /dev/null +++ b/crates/horizon-world/src/worldgen/simple_block.rs @@ -0,0 +1,109 @@ +//! Native simple-block feature selection, survival and ordered placement. +use super::feature::FeatureWorld; +use super::{ + DecodeError, Object, optional, + placement::{PlacementContext, PlacementQueryError}, + required, + state_provider::{ + BlockStateProvider, StateProviderBindings, StateProviderCatalog, StateProviderError, + }, +}; +use crate::block_placement::{BlockPlacement, BlockPlacementBindings, BlockPlacementError}; +use horizon_core::random::RandomSource; +use std::sync::Arc; + +/// Immutable state providers and placement transitions share one registry. +#[derive(Clone, Debug)] +pub struct SimpleBlockFeature { + provider: BlockStateProvider, + placement: Arc, + schedule_tick: bool, +} +#[derive(Debug)] +pub enum SimpleBlockError { + Provider(StateProviderError), + Placement(BlockPlacementError), + Query(PlacementQueryError), +} +impl std::fmt::Display for SimpleBlockError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Provider(e) => e.fmt(f), + Self::Placement(e) => e.fmt(f), + Self::Query(e) => e.fmt(f), + } + } +} +impl std::error::Error for SimpleBlockError {} +impl SimpleBlockFeature { + /// Decode the feature's map codec, including optional named providers. + pub fn from_json( + json: &str, + providers: &StateProviderBindings<'_>, + catalog: &StateProviderCatalog, + placement: &BlockPlacementBindings<'_>, + ) -> Result { + Self::decode(json, providers, catalog, placement, 0, &mut 65536) + } + pub(crate) fn decode( + json: &str, + providers: &StateProviderBindings<'_>, + catalog: &StateProviderCatalog, + placement: &BlockPlacementBindings<'_>, + depth: usize, + remaining: &mut usize, + ) -> Result { + if !std::ptr::eq(providers.registry(), placement.registry()) { + return Err(DecodeError::Field("simple block registry identity")); + } + let object: Object = serde_json::from_str(json)?; + let provider = catalog.provider_with_budget( + required(&object, "to_place")?.get(), + providers, + depth + 1, + remaining, + )?; + let schedule_tick = optional(&object, "schedule_tick") + .map(|v| serde_json::from_str::(v.get())) + .transpose()? + .unwrap_or(false); + Ok(Self { + provider, + placement: placement.operations(), + schedule_tick, + }) + } + pub fn place( + &self, + world: &mut W, + random: &mut impl RandomSource, + position: [i32; 3], + ) -> Result> { + let Some(state) = self + .provider + .get_optional_state(world, random, position) + .map_err(SimpleBlockError::Provider)? + else { + return Ok(false); + }; + if !world + .block_survives(state, position) + .map_err(SimpleBlockError::Query)? + { + return Ok(false); + } + if !self + .placement + .place(world, state, position, 2) + .map_err(SimpleBlockError::Placement)? + { + return Ok(false); + } + if self.schedule_tick { + self.placement + .schedule_tick(world, position, 1) + .map_err(SimpleBlockError::Placement)?; + } + Ok(true) + } +} diff --git a/crates/horizon-world/src/worldgen/state_provider.rs b/crates/horizon-world/src/worldgen/state_provider.rs index be1901a440..6f69cfafa1 100644 --- a/crates/horizon-world/src/worldgen/state_provider.rs +++ b/crates/horizon-world/src/worldgen/state_provider.rs @@ -41,6 +41,9 @@ pub struct StateProviderBindings<'r> { predicate_states: OnceLock>>>, } impl<'r> StateProviderBindings<'r> { + pub fn registry(&self) -> &'r BlockRegistry { + self.registry + } pub fn with_block_properties( mut self, properties: &BlockPropertiesRegistry<'r>, diff --git a/crates/horizon-world/src/worldgen/state_provider/catalog.rs b/crates/horizon-world/src/worldgen/state_provider/catalog.rs index 0cda3d1e82..e8316713ae 100644 --- a/crates/horizon-world/src/worldgen/state_provider/catalog.rs +++ b/crates/horizon-world/src/worldgen/state_provider/catalog.rs @@ -36,6 +36,15 @@ impl StateProviderCatalog { ) -> Result { BlockStateProvider::decode_holder(json, bindings, 0, &mut 65536, Some(self)) } + pub(crate) fn provider_with_budget( + &self, + json: &str, + bindings: &StateProviderBindings<'_>, + depth: usize, + remaining: &mut usize, + ) -> Result { + BlockStateProvider::decode_holder(json, bindings, depth, remaining, Some(self)) + } /// Decode DIRECT_CODEC while allowing nested named holder references. pub fn direct_provider( &self, diff --git a/docs/feature-placement.md b/docs/feature-placement.md index 4be7613979..94995d0e6a 100644 --- a/docs/feature-placement.md +++ b/docs/feature-placement.md @@ -544,3 +544,30 @@ and active-piston snapshots have separate geometry comparisons. This establishes the recorded provider/predicate integration, while full block placement, updates and gameplay remain pending. Workspace tests, warning-free Clippy and the **147-test Python suite** passed. + + +## Simple-block placement + +`SimpleBlockFeature` combines holder-aware state providers, candidate survival, +precompiled placement transitions and optional scheduled ticks. Ordinary writes +retain native flags and ignore a rejected write's boolean result. Double plants +first check the block above for air or matching replaceable fluid, then copy +waterlogging independently into each half. Pale moss carpet derives side states +from support/collision queries, consumes the world's separate random stream for +its topper and recomputes lower sides after the upper write. Scheduled ticks use +the block actually stored after placement, including rejected writes. + +`FeatureBlockBindings::with_state_providers` binds these operations and provider +resources to the same registry. `FeatureDefinition` and `FeatureCatalog` compose +simple blocks with named holders, selectors, sequences and placement modifiers. +Provider decoding shares the enclosing graph's bounded work/depth budget; compiled +placement tables are shared by immutable reference-counted handles. + +Run `python3 tools/test_simple_block_parity.py` against the pinned JARs. The +[simple-block report](../benchmarks/rust-simple-block-2026-10-06/scoped-parity.json) +records **9,613,872 matching output lines per JAR** across **492,554 fixtures**. +It covers every candidate state, double-plant obstructions, waterlogging, moss +attachment/topper combinations, ordered reads/writes, both random streams, +rejected writes, partial failures, wrapped coordinates and nested composition. +Workspace tests and all-target Clippy passed. Native chunk updates, scheduled tick +execution and complete playable integration remain pending. diff --git a/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaSimpleBlockOracle.java b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaSimpleBlockOracle.java new file mode 100644 index 0000000000..9c954a3638 --- /dev/null +++ b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaSimpleBlockOracle.java @@ -0,0 +1,111 @@ +package dev.horizonlab.parity; +import com.google.gson.*; +import com.mojang.serialization.JsonOps; +import java.io.*; +import java.lang.reflect.*; +import java.nio.file.*; +import java.util.*; +import net.minecraft.core.*; +import net.minecraft.core.registries.*; +import net.minecraft.resources.*; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.*; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.*; +import net.minecraft.world.level.levelgen.feature.SimpleBlockFeature; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.placement.*; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider; + +/** Actual native simple-block map codec and mutable world placement. */ +public final class VanillaSimpleBlockOracle { + private static final class Stop extends RuntimeException {} + private static Optional decode(RegistryOps ops,JsonObject f) { + var json=JsonParser.parseString(f.get("definition").getAsString()); + String mode=f.has("mode")?f.get("mode").getAsString():"map"; + if(mode.equals("map"))return SimpleBlockFeature.CODEC.codec().parse(ops,json).result().map(value->value); + if(mode.equals("feature"))return Feature.CODEC.parse(ops,json).result().map(Holder::value); + return PlacedFeature.CODEC.parse(ops,json).result().map(holder->new Feature(){ + public com.mojang.serialization.MapCodec codec(){return com.mojang.serialization.MapCodec.unit(this);} + public boolean place(WorldGenLevel world,ChunkGenerator generator,RandomSource random,BlockPos p){return holder.value().place(world,generator,random,p);} + }); + } + private static BlockPos pos(JsonArray a){return new BlockPos(a.get(0).getAsInt(),a.get(1).getAsInt(),a.get(2).getAsInt());} + private static String xyz(BlockPos p){return p.getX()+" "+p.getY()+" "+p.getZ();} + private static final class World implements InvocationHandler { + final JsonObject f;final PrintWriter out;final RandomSource random;int calls; + final TreeMap states=new TreeMap<>(Comparator.comparingInt(BlockPos::getX).thenComparingInt(BlockPos::getY).thenComparingInt(BlockPos::getZ)); + World(JsonObject f,PrintWriter out,RandomSource random){this.f=f;this.out=out;this.random=random;for(var raw:f.getAsJsonArray("states")){var v=raw.getAsJsonArray();states.put(pos(v),Block.stateById(v.get(3).getAsInt()));}} + void event(String text){out.println(text);if(calls++==f.get("fail").getAsInt())throw new Stop();} + BlockState at(BlockPos p){return states.getOrDefault(p,Block.stateById(f.get("fallback").getAsInt()));} + public Object invoke(Object proxy,Method method,Object[] args)throws Throwable { + String name=method.getName(); + if(name.equals("getRandom"))return random; + if(name.equals("getMinY"))return -64; + if(name.equals("getHeight")&&(args==null||args.length==0))return 384; + if(name.equals("getSeaLevel"))return 63; + if(name.equals("getBlockState")){var p=(BlockPos)args[0];event("get "+xyz(p));var state=at(p);out.println("state "+Block.getId(state));return state;} + if(name.equals("getFluidState")){var p=(BlockPos)args[0];event("fluid "+xyz(p));return (f.has("fluid")?Block.stateById(f.get("fluid").getAsInt()):at(p)).getFluidState();} + if(name.equals("getRawBrightness")){var p=(BlockPos)args[0];event("light "+xyz(p)+" "+args[1]);return f.get("brightness").getAsInt();} + if(name.equals("getBlockEntity")){event("entity "+xyz((BlockPos)args[0]));return null;} + if(name.equals("setBlock")){ + var p=((BlockPos)args[0]).immutable();var state=(BlockState)args[1];int call=calls;event("set "+xyz(p)+" "+Block.getId(state)+" "+args[2]); + int reject=f.get("reject").getAsInt();boolean result=reject==0||call%reject!=0;if(result)states.put(p,state);out.println("written "+result);return result; + } + if(name.equals("scheduleTick")){event("tick "+xyz((BlockPos)args[0])+" "+Block.getId(((Block)args[1]).defaultBlockState())+" "+args[2]);return null;} + if(method.isDefault())return InvocationHandler.invokeDefault(proxy,method,args); + throw new AssertionError(method); + } + } + @SuppressWarnings("unchecked") + public static void main(String[] args)throws Exception { + net.minecraft.SharedConstants.tryDetectVersion();net.minecraft.server.Bootstrap.bootStrap();Path dir=Path.of(args[1]); + if(args[0].equals("export")) { + VanillaBlockSurvivalOracle.main(args); + var data=new JsonObject();var doubles=new JsonArray();var moss=new JsonArray(); + for(var block:BuiltInRegistries.BLOCK){String name=BuiltInRegistries.BLOCK.getKey(block).toString();if(block instanceof DoublePlantBlock)doubles.add(name);if(block instanceof MossyCarpetBlock)moss.add(name);} + data.add("double_plants",doubles);data.add("mossy_carpets",moss);Files.writeString(dir.resolve("placement-definitions.json"),data.toString());return; + } + VanillaHeightmapOracle.bindTags(JsonParser.parseString(Files.readString(dir.resolve("tags.json"))).getAsJsonObject()); + var bind=VanillaBlockSurvivalOracle.class.getDeclaredMethod("bindFluidTags",JsonObject.class);bind.setAccessible(true);bind.invoke(null,JsonParser.parseString(Files.readString(dir.resolve("fluid-tags.json"))).getAsJsonObject()); + var providers=new MappedRegistry(Registries.BLOCK_STATE_PROVIDER,com.mojang.serialization.Lifecycle.stable()); + var features=new MappedRegistry(Registries.FEATURE,com.mojang.serialization.Lifecycle.stable()); + var placed=new MappedRegistry(Registries.PLACED_FEATURE,com.mojang.serialization.Lifecycle.stable()); + var ops=RegistryOps.create(JsonOps.INSTANCE,new RegistryOps.RegistryInfoLookup(){public Optional> lookup(ResourceKey> key){ + if(key.equals(Registries.FEATURE))return (Optional>)(Optional)Optional.of(features); + if(key.equals(Registries.PLACED_FEATURE))return (Optional>)(Optional)Optional.of(placed); + if(key.equals(Registries.BLOCK_STATE_PROVIDER))return (Optional>)(Optional)Optional.of(providers); + if(key.equals(Registries.FLUID))return (Optional>)(Optional)Optional.of(BuiltInRegistries.FLUID); + return key.equals(Registries.BLOCK)?(Optional>)(Optional)Optional.of(BuiltInRegistries.BLOCK):Optional.empty(); + }}); + for(var entry:JsonParser.parseString(Files.readString(dir.resolve("catalog.json"))).getAsJsonObject().entrySet()) { + var provider=BlockStateProvider.DIRECT_CODEC.parse(ops,JsonParser.parseString(entry.getValue().getAsString())).getOrThrow(); + providers.register(ResourceKey.create(Registries.BLOCK_STATE_PROVIDER,Identifier.parse(entry.getKey())),provider,RegistrationInfo.BUILT_IN); + }providers.freeze(); + var bundle=JsonParser.parseString(Files.readString(dir.resolve("feature-catalog.json"))).getAsJsonObject(); + for(var entry:bundle.getAsJsonObject("features").entrySet())features.register(ResourceKey.create(Registries.FEATURE,Identifier.parse(entry.getKey())),Feature.DIRECT_CODEC.parse(ops,JsonParser.parseString(entry.getValue().getAsString())).getOrThrow(),RegistrationInfo.BUILT_IN); + features.freeze(); + for(var entry:bundle.getAsJsonObject("placed").entrySet())placed.register(ResourceKey.create(Registries.PLACED_FEATURE,Identifier.parse(entry.getKey())),PlacedFeature.DIRECT_CODEC.parse(ops,JsonParser.parseString(entry.getValue().getAsString())).getOrThrow(),RegistrationInfo.BUILT_IN); + placed.freeze(); + var contextMethod=VanillaPlacementOracle.class.getDeclaredMethod("context",JsonObject.class);contextMethod.setAccessible(true); + var bounds=new JsonObject();bounds.add("context",JsonParser.parseString("[-64,384,63]")); + var generator=((PlacementContext)contextMethod.invoke(null,bounds)).generator(); + try(var out=new PrintWriter(Files.newBufferedWriter(dir.resolve(args[0]+".txt")));var inputs=Files.lines(dir.resolve("inputs.txt"))) { + for(var line:(Iterable)inputs::iterator){var f=JsonParser.parseString(line).getAsJsonObject();long seed=f.get("seed").getAsLong(),worldSeed=f.get("world_seed").getAsLong();boolean legacy=f.get("legacy").getAsBoolean(); + RandomSource random=legacy?new LegacyRandomSource(seed):new XoroshiroRandomSource(seed),worldRandom=legacy?new LegacyRandomSource(worldSeed):new XoroshiroRandomSource(worldSeed); + Optional decoded; + try{decoded=decode(ops,f);}catch(IllegalArgumentException|IllegalStateException error){decoded=Optional.empty();} + if(decoded.isEmpty())out.println("decode error");else { + out.println("decode ok");var handler=new World(f,out,worldRandom);var world=(WorldGenLevel)Proxy.newProxyInstance(WorldGenLevel.class.getClassLoader(),new Class[]{WorldGenLevel.class},handler);var origin=pos(f.getAsJsonArray("origin")); + for(int repeat=0;repeat