mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Implement vanilla simple-block placement and feature composition
Add registry-bound double-plant and moss placement, independent world RNG, survival ordering and post-write tick scheduling. Integrate named providers and feature graphs. Both pinned JARs matched 9,613,872 trace lines across 492,554 fixtures; workspace tests and all-target Clippy passed.
This commit is contained in:
1 parent
097638b4f1
commit
f41cd70060
16 files changed
+1568
No files matched your search
@@ -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
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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<BlockStateId>,
|
||||
fluids: &'a FluidStateTable,
|
||||
water_mask: u8,
|
||||
survival: &'a BlockSurvivalRegistry<'r>,
|
||||
support: &'a BlockSupportRegistry<'r>,
|
||||
calls: i32,
|
||||
fail: i32,
|
||||
reject: i32,
|
||||
brightness: i32,
|
||||
}
|
||||
impl<W: Write, R> World<'_, '_, W, R> {
|
||||
fn event(&mut self, text: std::fmt::Arguments<'_>) -> io::Result<i32> {
|
||||
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<BlockStateId> {
|
||||
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<W: Write, R> DynamicSupportWorld for World<'_, '_, W, R> {
|
||||
type Error = io::Error;
|
||||
fn shulker_is_closed(&mut self, p: [i32; 3]) -> io::Result<Option<bool>> {
|
||||
self.entity(p)?;
|
||||
Ok(None)
|
||||
}
|
||||
fn moving_piston_shape(&mut self, p: [i32; 3]) -> io::Result<Option<VoxelShape>> {
|
||||
self.entity(p)?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
impl<W: Write, R> DynamicCollisionWorld for World<'_, '_, W, R> {
|
||||
fn shulker_collision_state(
|
||||
&mut self,
|
||||
p: [i32; 3],
|
||||
) -> io::Result<Option<ShulkerCollisionState>> {
|
||||
self.entity(p)?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
impl<W: Write, R> SurvivalWorld for World<'_, '_, W, R> {
|
||||
fn block_state(&mut self, p: [i32; 3]) -> io::Result<BlockStateId> {
|
||||
self.get(p)
|
||||
}
|
||||
fn fluid_state(&mut self, p: [i32; 3]) -> io::Result<FluidState> {
|
||||
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<i32> {
|
||||
self.event(format_args!(
|
||||
"light {} {} {} {sky_darken}",
|
||||
p[0], p[1], p[2]
|
||||
))?;
|
||||
Ok(self.brightness)
|
||||
}
|
||||
}
|
||||
impl<W: Write, R> FeatureWorld for World<'_, '_, W, R> {
|
||||
type Error = io::Error;
|
||||
fn block(&mut self, p: [i32; 3]) -> io::Result<BlockStateId> {
|
||||
self.get(p)
|
||||
}
|
||||
fn set_block(&mut self, p: [i32; 3], state: BlockStateId, flags: i32) -> io::Result<bool> {
|
||||
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<W: Write, R: RandomSource> PlacementContext for World<'_, '_, W, R> {
|
||||
fn water_at(&mut self, p: [i32; 3]) -> Result<bool, PlacementQueryError> {
|
||||
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<bool, PlacementQueryError> {
|
||||
let survival = self.survival;
|
||||
survival.can_attach_to(self, p, direction).map_err(query)
|
||||
}
|
||||
fn next_world_boolean(&mut self) -> Result<bool, PlacementQueryError> {
|
||||
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<i32, PlacementQueryError> {
|
||||
Ok(-64)
|
||||
}
|
||||
fn max_y(&mut self) -> Result<i32, PlacementQueryError> {
|
||||
Ok(319)
|
||||
}
|
||||
fn height(&mut self, _: HeightmapType, _: i32, _: i32) -> Result<i32, PlacementQueryError> {
|
||||
Err(PlacementQueryError::Unavailable)
|
||||
}
|
||||
fn block_state(&mut self, p: [i32; 3]) -> Result<BlockStateId, PlacementQueryError> {
|
||||
self.get(p).map_err(query)
|
||||
}
|
||||
fn block_survives(
|
||||
&mut self,
|
||||
state: BlockStateId,
|
||||
p: [i32; 3],
|
||||
) -> Result<bool, PlacementQueryError> {
|
||||
let survival = self.survival;
|
||||
survival.can_survive(self, state, p).map_err(query)
|
||||
}
|
||||
fn face_sturdy(
|
||||
&mut self,
|
||||
p: [i32; 3],
|
||||
direction: Direction,
|
||||
) -> Result<bool, PlacementQueryError> {
|
||||
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<FeatureDefinition>),
|
||||
Placed(Box<PlacedFeatureDefinition>),
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn fixture<R: RandomSource>(
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
let arg = std::env::args().nth(1).expect("private fixture directory");
|
||||
let dir = Path::new(&arg);
|
||||
let read = |name: &str| -> Result<Tag, Box<dyn std::error::Error>> {
|
||||
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<String, String> =
|
||||
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<String, String> =
|
||||
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<String, String> =
|
||||
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<String, String> =
|
||||
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(())
|
||||
}
|
||||
@@ -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<E> {
|
||||
World(E),
|
||||
Registry(io::Error),
|
||||
Query(PlacementQueryError),
|
||||
}
|
||||
impl<E: std::fmt::Display> std::fmt::Display for BlockPlacementError<E> {
|
||||
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<E: std::error::Error + 'static> std::error::Error for BlockPlacementError<E> {}
|
||||
|
||||
pub struct BlockPlacementBindings<'r> {
|
||||
registry: &'r BlockRegistry,
|
||||
operations: Arc<BlockPlacement>,
|
||||
}
|
||||
/// Precompiled transitions shared by decoded features in one registry snapshot.
|
||||
#[derive(Debug)]
|
||||
pub struct BlockPlacement {
|
||||
kinds: Vec<Placement>,
|
||||
doubles: Vec<DoublePlant>,
|
||||
replaceable: BoundBlockProperty,
|
||||
fluids: FluidStateTable,
|
||||
air: [BlockStateId; 3],
|
||||
defaults: Vec<BlockStateId>,
|
||||
moss: Vec<Option<Moss>>,
|
||||
moss_ids: [BlockStateId; 162],
|
||||
moss_default: BlockStateId,
|
||||
}
|
||||
impl<'r> BlockPlacementBindings<'r> {
|
||||
pub fn new(
|
||||
registry: &'r BlockRegistry,
|
||||
properties: &BlockPropertiesRegistry<'r>,
|
||||
double_plants: &BTreeSet<String>,
|
||||
mossy_carpets: &BTreeSet<String>,
|
||||
) -> io::Result<Self> {
|
||||
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::<Option<Vec<_>>>()
|
||||
.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<BlockPlacement> {
|
||||
self.operations.clone()
|
||||
}
|
||||
}
|
||||
impl BlockPlacement {
|
||||
fn read<W: FeatureWorld + PlacementContext>(
|
||||
&self,
|
||||
world: &mut W,
|
||||
position: [i32; 3],
|
||||
) -> Result<BlockStateId, BlockPlacementError<W::Error>> {
|
||||
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<Moss> {
|
||||
self.moss[state.0 as usize]
|
||||
}
|
||||
fn updated_moss<W: FeatureWorld + PlacementContext>(
|
||||
&self,
|
||||
world: &mut W,
|
||||
state: Moss,
|
||||
position: [i32; 3],
|
||||
force: bool,
|
||||
) -> Result<Moss, BlockPlacementError<W::Error>> {
|
||||
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<W: FeatureWorld + PlacementContext>(
|
||||
&self,
|
||||
world: &mut W,
|
||||
position: [i32; 3],
|
||||
) -> Result<Option<BlockStateId>, BlockPlacementError<W::Error>> {
|
||||
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<W: FeatureWorld + PlacementContext>(
|
||||
&self,
|
||||
world: &mut W,
|
||||
state: BlockStateId,
|
||||
position: [i32; 3],
|
||||
flags: i32,
|
||||
) -> Result<bool, BlockPlacementError<W::Error>> {
|
||||
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<W: FeatureWorld + PlacementContext>(
|
||||
&self,
|
||||
world: &mut W,
|
||||
position: [i32; 3],
|
||||
delay: i32,
|
||||
) -> Result<(), BlockPlacementError<W::Error>> {
|
||||
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]))
|
||||
}
|
||||
@@ -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<W: SurvivalWorld>(
|
||||
&self,
|
||||
world: &mut W,
|
||||
position: [i32; 3],
|
||||
direction: Direction,
|
||||
) -> Result<bool, SurvivalError<W::Error>> {
|
||||
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<W: SurvivalWorld>(
|
||||
&self,
|
||||
world: &mut W,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Arc<super::biome_predicate::BiomePredicateBindings>>,
|
||||
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<Self, DecodeError> {
|
||||
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,
|
||||
|
||||
@@ -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<E> {
|
||||
Basic(FeatureError<E>),
|
||||
SimpleBlock(super::simple_block::SimpleBlockError<E>),
|
||||
Random(InvalidBound),
|
||||
Query(PlacementQueryError),
|
||||
WorkLimit,
|
||||
@@ -53,6 +55,7 @@ impl<E: std::fmt::Display> std::fmt::Display for FeatureExecutionError<E> {
|
||||
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),
|
||||
|
||||
@@ -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<bool, PlacementQueryError> {
|
||||
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<bool, PlacementQueryError> {
|
||||
Err(PlacementQueryError::Unavailable)
|
||||
}
|
||||
/// Draw from the world's random stream, independently of the feature RNG.
|
||||
fn next_world_boolean(&mut self) -> Result<bool, PlacementQueryError> {
|
||||
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,
|
||||
|
||||
@@ -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<BlockPlacement>,
|
||||
schedule_tick: bool,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub enum SimpleBlockError<E> {
|
||||
Provider(StateProviderError<E>),
|
||||
Placement(BlockPlacementError<E>),
|
||||
Query(PlacementQueryError),
|
||||
}
|
||||
impl<E: std::fmt::Display> std::fmt::Display for SimpleBlockError<E> {
|
||||
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<E: std::error::Error + 'static> std::error::Error for SimpleBlockError<E> {}
|
||||
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, DecodeError> {
|
||||
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<Self, DecodeError> {
|
||||
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::<bool>(v.get()))
|
||||
.transpose()?
|
||||
.unwrap_or(false);
|
||||
Ok(Self {
|
||||
provider,
|
||||
placement: placement.operations(),
|
||||
schedule_tick,
|
||||
})
|
||||
}
|
||||
pub fn place<W: FeatureWorld + PlacementContext>(
|
||||
&self,
|
||||
world: &mut W,
|
||||
random: &mut impl RandomSource,
|
||||
position: [i32; 3],
|
||||
) -> Result<bool, SimpleBlockError<W::Error>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,9 @@ pub struct StateProviderBindings<'r> {
|
||||
predicate_states: OnceLock<BTreeMap<BlockStateId, Arc<BTreeSet<BlockStateId>>>>,
|
||||
}
|
||||
impl<'r> StateProviderBindings<'r> {
|
||||
pub fn registry(&self) -> &'r BlockRegistry {
|
||||
self.registry
|
||||
}
|
||||
pub fn with_block_properties(
|
||||
mut self,
|
||||
properties: &BlockPropertiesRegistry<'r>,
|
||||
|
||||
@@ -36,6 +36,15 @@ impl StateProviderCatalog {
|
||||
) -> Result<BlockStateProvider, DecodeError> {
|
||||
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, DecodeError> {
|
||||
BlockStateProvider::decode_holder(json, bindings, depth, remaining, Some(self))
|
||||
}
|
||||
/// Decode DIRECT_CODEC while allowing nested named holder references.
|
||||
pub fn direct_provider(
|
||||
&self,
|
||||
|
||||
@@ -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.
|
||||
@@ -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<Feature> decode(RegistryOps<JsonElement> 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<? extends Feature> 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<BlockPos,BlockState> states=new TreeMap<>(Comparator.<BlockPos>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<BlockStateProvider>(Registries.BLOCK_STATE_PROVIDER,com.mojang.serialization.Lifecycle.stable());
|
||||
var features=new MappedRegistry<Feature>(Registries.FEATURE,com.mojang.serialization.Lifecycle.stable());
|
||||
var placed=new MappedRegistry<PlacedFeature>(Registries.PLACED_FEATURE,com.mojang.serialization.Lifecycle.stable());
|
||||
var ops=RegistryOps.create(JsonOps.INSTANCE,new RegistryOps.RegistryInfoLookup(){public <T> Optional<HolderGetter<T>> lookup(ResourceKey<? extends Registry<? extends T>> key){
|
||||
if(key.equals(Registries.FEATURE))return (Optional<HolderGetter<T>>)(Optional<?>)Optional.of(features);
|
||||
if(key.equals(Registries.PLACED_FEATURE))return (Optional<HolderGetter<T>>)(Optional<?>)Optional.of(placed);
|
||||
if(key.equals(Registries.BLOCK_STATE_PROVIDER))return (Optional<HolderGetter<T>>)(Optional<?>)Optional.of(providers);
|
||||
if(key.equals(Registries.FLUID))return (Optional<HolderGetter<T>>)(Optional<?>)Optional.of(BuiltInRegistries.FLUID);
|
||||
return key.equals(Registries.BLOCK)?(Optional<HolderGetter<T>>)(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<String>)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<Feature> 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<f.get("repeats").getAsInt();repeat++){
|
||||
handler.calls=0;try{out.println("place "+decoded.get().place(world,generator,random,origin));}catch(Stop|IllegalArgumentException error){out.println("place error");}
|
||||
out.println("next "+random.nextLong()+" "+worldRandom.nextLong());out.println("calls "+handler.calls);
|
||||
for(var entry:handler.states.entrySet())out.println("stored "+xyz(entry.getKey())+" "+Block.getId(entry.getValue()));out.println("end");
|
||||
}
|
||||
}out.println("tail "+random.nextLong()+" "+worldRandom.nextLong());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare simple-block feature effects and separate random streams with vanilla."""
|
||||
import argparse
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import random
|
||||
import subprocess
|
||||
try:
|
||||
from . import lab, test_rust_parity as base, test_worldgen_parity as worldgen, test_block_survival_parity as survival
|
||||
except ImportError:
|
||||
import lab
|
||||
import test_rust_parity as base
|
||||
import test_worldgen_parity as worldgen
|
||||
import test_block_survival_parity as survival
|
||||
|
||||
|
||||
def corpus(facts, placement):
|
||||
states=facts['states'];get=lambda name:facts['defaults']['minecraft:'+name]
|
||||
encode=lambda obj:json.dumps(obj,separators=(',',':'))
|
||||
def case(candidate=None,**extra):
|
||||
origin=extra.pop('origin',[0,64,0])
|
||||
def at(dy):return [origin[0],((origin[1]+dy+2**31)%2**32)-2**31,origin[2]]
|
||||
f={'definition':encode({'to_place':states[candidate if candidate is not None else get('stone')],'schedule_tick':True}),'origin':origin,'legacy':False,'seed':0,'world_seed':1,'fail':-1,'reject':0,'brightness':15,'fallback':get('stone'),'states':[[*at(0),get('air')],[*at(1),get('air')],[*at(-1),get('dirt')]],'repeats':2}
|
||||
f.update(extra);return f
|
||||
origins=[[0,64,0],[2147483647,-2147483648,2147483647],[-17,2147483647,-33]]
|
||||
for candidate in range(len(states)):yield case(candidate,origin=origins[candidate%3])
|
||||
double_names=set(placement['double_plants']);representatives={}
|
||||
for candidate,state in enumerate(states):
|
||||
if state['id'] not in double_names:continue
|
||||
key=(state['id'],state.get('properties',{}).get('waterlogged'))
|
||||
if state.get('properties',{}).get('half')=='lower':representatives.setdefault(key,candidate)
|
||||
below=next(i for i,s in enumerate(states) if s['id']==state['id'] and s.get('properties',{}).get('half')=='lower') if state.get('properties',{}).get('half')=='upper' else get('clay') if state['id']=='minecraft:small_dripleaf' else get('dirt')
|
||||
for above in [get(name) for name in ['air','cave_air','void_air','stone','water','lava','short_grass','snow','small_dripleaf','tall_seagrass','pale_moss_carpet']]:
|
||||
for fluid in [get('air'),get('water'),get('lava')]:
|
||||
for fail in [-1,*range(9)]:yield case(candidate,states=[[0,63,0,below],[0,64,0,get('air')],[0,65,0,above]],fluid=fluid,fail=fail,reject=3)
|
||||
for candidate in representatives.values():
|
||||
name=states[candidate]['id'];below=get('clay') if name=='minecraft:small_dripleaf' else get('farmland') if name=='minecraft:pitcher_crop' else get('dirt')
|
||||
for above in range(len(states)):yield case(candidate,states=[[0,63,0,below],[0,64,0,get('air')],[0,65,0,above]],fluid=get('water'),repeats=1)
|
||||
moss_ids=[i for i,s in enumerate(states) if s['id']=='minecraft:pale_moss_carpet'];sides=[(0,-1),(1,0),(0,1),(-1,0)]
|
||||
for lower_mask,upper_mask in itertools.product(range(16),repeat=2):
|
||||
for legacy in [False,True]:
|
||||
for seed in [0,-1,0x123456789abcdef]:
|
||||
for fail in [-1,0,3,7,12,19,25]:
|
||||
entries=[[0,63,0,get('stone')],[0,64,0,get('air')],[0,65,0,get('air')]]
|
||||
for y,mask in [(64,lower_mask),(65,upper_mask)]:entries.extend([x,y,z,get('stone') if mask&(1<<index) else get('air')] for index,(x,z) in enumerate(sides))
|
||||
yield case(get('pale_moss_carpet'),states=entries,legacy=legacy,seed=seed,world_seed=seed,fail=fail,fallback=get('air'),reject=0 if seed==0 else 3)
|
||||
for lower,upper in itertools.product(moss_ids,moss_ids):yield case(lower,states=[[0,63,0,get('stone')],[0,64,0,get('air')],[0,65,0,upper],[0,66,0,lower]],world_seed=upper,reject=0 if lower%2 else 3)
|
||||
rng=random.Random(0x53494d504c45);palette=[get(name) for name in ['shulker_box','scaffolding','snow','piston_head','moving_piston','powder_snow','bamboo','stone','air','oak_slab','oak_stairs']]
|
||||
for _ in range(4096):
|
||||
entries=[[0,63,0,get('stone')],[0,64,0,get('air')],[0,65,0,rng.choice(moss_ids)]]
|
||||
entries.extend([x,y,z,rng.choice(palette)] for y in [64,65] for x,z in sides)
|
||||
yield case(get('pale_moss_carpet'),states=entries,seed=rng.randrange(2**63),world_seed=rng.randrange(2**63),fail=rng.randrange(-1,25),fallback=get('air'))
|
||||
providers=[{'type':'random_block','blocks':[]},{'type':'random_block','blocks':['stone','small_dripleaf','pale_moss_carpet']},{'type':'weighted','entries':[{'data':'stone','weight':1},{'data':'pale_moss_carpet','weight':3}]},{'type':'rotated','state':{'id':'oak_log'}},{'type':'rule_based','rules':[]},{'type':'rule_based','rules':[{'if_true':{'type':'true'},'then':{'id':'pale_moss_carpet'}}]},'horizon:stone']
|
||||
for provider in providers:
|
||||
for legacy in [False,True]:
|
||||
for seed in [0,1,-1,0x123456789abcdef]:
|
||||
for fail in [-1,*range(12)]:
|
||||
for reject in [0,1,3]:yield case(definition=encode({'to_place':provider,'schedule_tick':True}),legacy=legacy,seed=seed,world_seed=seed,fail=fail,reject=reject,repeats=4)
|
||||
for schedule in [None,False,True,0,1,'true',{},[]]:yield case(definition=encode({'to_place':{'id':'stone'},'schedule_tick':schedule}))
|
||||
for definition in [None,False,0,[],{}, {'to_place':None},{'to_place':'missing'},{'to_place':{'id':'stone'}},{'to_place':{'id':'missing'}},{'to_place':{'type':'random_block','blocks':[]},'ignored':1}]:yield case(definition=encode(definition))
|
||||
|
||||
simple=lambda provider:{'type':'simple_block','to_place':provider,'schedule_tick':True}
|
||||
placed=lambda feature,modifiers=[]:{'feature':feature,'placement':modifiers}
|
||||
moss=placed('horizon:moss',[{'type':'count','count':2}]);empty=placed('horizon:empty');stone=placed('horizon:stone')
|
||||
graphs=['horizon:moss',simple('horizon:stone'),
|
||||
{'type':'sequence','features':[moss,stone]},
|
||||
{'type':'sequence','features':[moss,empty,stone]},
|
||||
{'type':'simple_random_selector','features':[moss,empty,stone]},
|
||||
{'type':'random_boolean_selector','feature_true':moss,'feature_false':empty},
|
||||
{'type':'weighted_random_selector','features':[{'data':moss,'weight':1},{'data':empty,'weight':3},{'data':stone,'weight':2}]},
|
||||
{'type':'random_selector','features':[{'feature':moss,'chance':0.4},{'feature':empty,'chance':0.7}],'default':stone}]
|
||||
chains=[[],[{'type':'count','count':2}],[{'type':'count','count':2},{'type':'in_square'}],
|
||||
[{'type':'random_chance','chance':0.5},{'type':'count','count':2}],
|
||||
[{'type':'offset','x':1,'y':1,'z':-1}],
|
||||
[{'type':'block_predicate_filter','predicate':{'type':'would_survive','state':'pale_moss_carpet'}}]]
|
||||
for graph in graphs:
|
||||
for mode,definition in [('feature',graph),*[('placed',placed(graph,chain)) for chain in chains]]:
|
||||
for legacy in [False,True]:
|
||||
for seed in [0,1,-1,0x123456789abcdef]:
|
||||
for fail in [-1,0,3,7,19]:
|
||||
for reject in [0,1,3]:yield case(definition=encode(definition),mode=mode,legacy=legacy,seed=seed,world_seed=seed,fail=fail,reject=reject,repeats=3)
|
||||
for name in ['horizon:moss','horizon:stone','horizon:empty','horizon:missing']:
|
||||
yield case(definition=encode(name),mode='placed')
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--prepare',action='store_true');args=parser.parse_args()
|
||||
build=lab.LOCAL/'rust-simple-block-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/VanillaSimpleBlockOracle.java'
|
||||
helpers=[oracle.with_name(name+'.java') for name in ['VanillaPlacementOracle','VanillaBlockSurvivalOracle','VanillaBlockPropertiesOracle','VanillaBlockSupportOracle','VanillaBlockStateCodecOracle','VanillaCollisionOracle','VanillaHeightmapOracle']]
|
||||
sources=[oracle,*helpers,Path(__file__),*[lab.ROOT/'tools'/name for name in ['test_block_survival_parity.py','test_worldgen_parity.py','test_rust_parity.py','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/simple_block_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:subprocess.run(list(map(str,command)),cwd=lab.ROOT,check=True,timeout=3600,stdout=log,stderr=subprocess.STDOUT)
|
||||
dependency_path=os.pathsep.join(map(str,libraries))
|
||||
def java(side,mode,path):run(['java','-Xmx512M','-cp',os.pathsep.join([str(classes),str(jars[side]),dependency_path]),'dev.horizonlab.parity.VanillaSimpleBlockOracle',mode,path],side+'-'+mode)
|
||||
tags=worldgen.tag_resources(jars['server']);fluid_tags=survival.fluid_tag_resources(jars['server'])
|
||||
if tags!=worldgen.tag_resources(jars['client']) or fluid_tags!=survival.fluid_tag_resources(jars['client']):raise ValueError('reference tags differ')
|
||||
for name,value in [('tags.json',tags),('fluid-tags.json',fluid_tags),('catalog.json',{'horizon:stone':json.dumps({'id':'stone'})})]:(build/name).write_text(json.dumps(value,separators=(',',':')))
|
||||
feature_bundle={'features':{name:json.dumps({'type':'simple_block','to_place':provider,'schedule_tick':True}) for name,provider in [('horizon:moss',{'id':'pale_moss_carpet'}),('horizon:stone','horizon:stone'),('horizon:empty',{'type':'random_block','blocks':[]})]},'placed':{name:json.dumps({'feature':name,'placement':[{'type':'count','count':2}]}) for name in ['horizon:moss','horizon:stone','horizon:empty']}}
|
||||
(build/'feature-catalog.json').write_text(json.dumps(feature_bundle,separators=(',',':')))
|
||||
run(['javac','--release' ,lock['minecraft']['java_major'],'-proc:none','-cp',str(jars['server'])+os.pathsep+dependency_path,'-d',classes,oracle,*helpers],'compile-java')
|
||||
for side in jars:
|
||||
export=build/(side+'-definitions');export.mkdir(exist_ok=True);java(side,'export',export)
|
||||
for path in (build/'server-definitions').iterdir():
|
||||
if path.read_bytes()!=(build/'client-definitions'/path.name).read_bytes():raise ValueError('reference definitions differ: '+path.name)
|
||||
(build/path.name).write_bytes(path.read_bytes())
|
||||
facts=json.loads((build/'facts.json').read_text());placement=json.loads((build/'placement-definitions.json').read_text())
|
||||
rules=survival.bind_definitions(*[json.loads((build/name).read_text()) for name in ['survival-classes.json','survival-placement-classes.json','survival-support-tags.json']]);(build/'survival-rules.json').write_text(json.dumps(rules,separators=(',',':')))
|
||||
inputs=build/'inputs.txt';fixture_count=0
|
||||
with inputs.open('w',encoding='utf-8') as output:
|
||||
for fixture in corpus(facts,placement):output.write(json.dumps(fixture,separators=(',',':'))+'\n');fixture_count+=1
|
||||
fixtures=[inputs,*sorted(build.glob('*.nbt')),*[p for p in sorted(build.glob('*.json')) if p.name!='report.json']];tested_fixtures=base.source_hashes(fixtures);print(f'Prepared {fixture_count:,} placement fixtures',flush=True)
|
||||
run(['cargo','build','--release','-p','horizon-world','--example','simple_block_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'/('simple_block_fixture.exe' if os.name=='nt' else 'simple_block_fixture')
|
||||
run([binary,build],'rust');references=[]
|
||||
for side,jar in jars.items():
|
||||
java(side,side,build);matched=base.compare(build/(side+'.txt'),build/'rust.txt');print(f'{side}: {matched:,} simple-block output lines match Rust',flush=True)
|
||||
references.append({'side':side,'jar_sha256':lab.digest(jar,'sha256'),'output_sha256':lab.digest(build/(side+'.txt'),'sha256'),'matched_output_lines':matched})
|
||||
base.ensure_sources_unchanged(sources,tested_sources);base.ensure_sources_unchanged(fixtures,tested_fixtures)
|
||||
report={'schema':1,'minecraft':lock['minecraft']['version'],'manifest_sha1':lock['minecraft']['manifest_sha1'],'platform':platform.platform(),'java':subprocess.check_output(['java','--version'],text=True).splitlines()[0],'rustc':subprocess.check_output(['rustc','--version'],text=True).strip(),'fixtures':fixture_count,'candidate_states':len(facts['states']),'double_plant_blocks':len(placement['double_plants']),'mossy_carpet_blocks':len(placement['mossy_carpets']),'sources_sha256':tested_sources,'fixtures_sha256':tested_fixtures,'rust_output_sha256':lab.digest(build/'rust.txt','sha256'),'references':references,'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.'}
|
||||
lab.write_json(report_path,report);print(f'Scoped simple-block parity passed: {report_path}',flush=True)
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
Reference in new issue
Block a user