This commit is contained in:
Brian Neumann-Fopiano
2026-07-16 12:26:01 -04:00
parent 55b7460b9a
commit 699247b1de
327 changed files with 30138 additions and 3934 deletions
+2
View File
@@ -40,3 +40,5 @@ credentials.json
service-account*.json
docs/
CROSSPLATFORM_PLAN.md
+298
View File
@@ -0,0 +1,298 @@
#iris-goldenhash v1
#world=goldentest
#dim=overworld
#seed=1337
#mc=26.1.2.build.2591-stable
#minY=-256 maxY=512
#center=0,0
#radius=8
-8 0 e8c6f44099c05ed375fc14866389bc8c7c14b51b69434c9c516caf53d031c415 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-8 1 c555764a99e9bb25c1ad95890f24b38447d4104637eb8d1c84629b929dbef7ff 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-8 2 0d7bfb60c8913b6ee0225faa06c5759c2c1c05646ed2bac3fc924b3e53c0a125 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-8 3 f682f9939349330bfe2160d9b8d9ce0d43b3c710860107abce8d98a38ee85b7e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-8 4 e96d9e6b17b59f251023cb70f78a2e4a155d613ddd9208c0bb66c01589f3649c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-8 5 f3dc77a9e64d2d9639d7f23222bc4c61272e8dab1c93e97420d9667640e6d212 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-8 6 ad62cd0be871a2bb2d547e64df2ea40b40e38e729b19cbb21d7343d8146dfc51 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-8 7 4f01547457c8cb8da9b4de50780c8ceffbe8da3781cb171c682e367a46a4006a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-8 8 0fc9c9d510b070d6b8bc7bd867134f1367a3f5ad0514ffd8925ce089de734cb3 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-8 -8 01347120983abadd10251a68a3ea131414986aadc5861b9ed2684930998b187f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-8 -7 65d0f851d4c2467f0f541967b692cd44d95ddb59cd2250af08bfa788af8a3803 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-8 -6 d1bc22d2912b8555226c19f38f5f8e574331be5901215122a5ffbeb73b419765 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-8 -5 ba6f58e5f76cbee8058c839ff7e42f4e7c4a07158cf97f959e4f676d4a477b72 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-8 -4 3e7d2602fff6f0ace932134462745586d7baaed6fbdc5d4285f71219c19d281d 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-8 -3 88c0654c7380d9b616079d7991b41677e7f4c46acc6dcad93024e2c2b0f72288 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-8 -2 94d591ae106d14204ed910cc1de28de4040f853d6a9bd47fc7fdebec0324f6a9 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-8 -1 26218d2073cbe0cc87e91e789f973444510a49c0e3c3c5f94f32febb1496288a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 0 4549aad82cdfa6356d7de621924f62ca950640e9b8f5e771964fb7a66d648009 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 1 89e7a6a250b09565b2d00050fac709bc6612e16c4a7e0d189595f460ef28f1eb 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 2 2da2f65fa337c51d140f339af28faa2987dc09f793b7dd3dcbdb2f801cec76d7 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 3 5d5ec222384ce07398e352f405f23dd3454277c8d5251c32d870659a8c52f6fc 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 4 1839f53ff781c3d133d741d297527a6c864f185be77c6a79ec294de34bb86e45 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 5 9f6f17c5c3e12f1d94e6a7b8a81ebe082e55b548213c605c904554baa864c83d 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 6 85482bebafc31f647b56d400e41df1c78555446df9d24c7c9baeaa0760f0ea1e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 7 bb9382d28c0841a6aa6ebfc35ab8df808bea54cad20c858fb58a2652a8ca3e9e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 8 68bba9b6d00765c098cb3d29907977198303c69a39410e5d46ca109d791d22de 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 -8 2c622b6c55a20271a48874ae9e489ce3c938c634eb4cb2c317b66a21ecf36c76 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 -7 1f29f9d700900e966d96f6425c28e6ac5c30f77d6d5bc8efca22937678f6fbfa 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 -6 3b9e3e48cc43167ac8e659409c62ae9bd7cdddca74cf6efc23dfead2913dff2b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 -5 1ed142541d6bb7b11909a69257678277546b72cfdf8a64f44a471f827bbb671a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 -4 d7f808e9d725823ba2b68e54b1985f458ce180fc1c5dda3dac4add38d831a1ce 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 -3 0971e32206dcb07b64ef050c23a06a354f3d776095e10c7e65ca44db999b868e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 -2 25e93be610632dbb353b4673aaf02b17a8153332f5557617ddafbce5332b8bb2 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-7 -1 92bb2c7632a54d837db26d79d114fa6626b16289ab9879eca89aa3030633813f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 0 7ecbcbb6be3e4e6d8449f90468d9969204b18fb9746ecad6319741f9f12c6fbc 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 1 3898687b4624ebeeb6e61fc40f39d6b5842f25495bfe63dcf9a4733596bc3c82 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 2 0e3017098a07c8b596f4d3b221b9ebd0d564bb0e2776391031a716c2bf1d01b7 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 3 126fba2b943704a1791648a76e100f5ea263920837e334ecb7d9188c3d76412d 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 4 68d6139155e498e561efb77c9f57568684429ada7918c47f44f6a85041478220 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 5 b50c5089e48a84d456120884e90e6a17d8bb02478743a97a0beb1b4eb4fb9890 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 6 0317ed303e7ae37b236bfa0b86d6c00bc2c6b6423f2d1b0228a0f0e4b0f5b6d2 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 7 330599a6b02758c52f13792e83173fb8beca89e94f26f7c12d82415d8a0c49e1 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 8 e74f8d776ca319f992298dfb1d0545f9e3796d8c74ff59d0a57e8155eb5d955b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 -8 f65ef781c13b78f1ec46d98b1396c8c716b3752da6315cc805359354917480ab 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 -7 efdef3ed4747552d65982c605365dc6e576ab9030643efe8b1bb2456be5a09a8 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 -6 06f8d72fc8281b0e0fbbbf8f8b91575a1db4af551dc87f459877e3eaeebf363c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 -5 54211524408e92d4518e7409ebaa45647ce7a6645a34de338167d2ff213839f0 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 -4 5540bded756e63b331df11f89f57566ba86b711a4b88fb5480e5dda9f2540586 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 -3 601b734e87ee9a74173b780d566f85322c45bdaf03125bbb070e136c9dfe5007 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 -2 1ca8f873f3369f53537cb7b5506f19e1c7ea4fd06c817616508b0c96103b26c4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-6 -1 ea159282ee7c77e10e8506feccc6f8d6798ac414737521565b0f604911939c1f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 0 c1c95112697de045a0add257e6e793f03f1592d451e774eac0757ac7155b48ef 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 1 a5ea2ca87fa5e0dceb6df886d926cd4559c667a83abd86415f1c27dc9ed81ee5 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 2 dc7f16b61d9edc02d5249b8d2ee6354272b03db78af103b149d393517b84bda8 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 3 cee16aa555acbdf22437047b4256f6c768a9a030118f5638a642917657c1ef55 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 4 67681fafbaea4803cc440ec1e8f65d743c3b8a46697be8976d413d32f7a3c72f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 5 fba2f12c44e498b0507d319a43c0f2c967ce63bb77793027deb6d1ca3e6e234a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 6 b72a83a562f7d9e2f684f89de7f2893f8c213e74aadb58cd186ba61c5dc7e45d 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 7 7089ebbd8ee10a7d7fc335055a350072414b0ffde6c0b3033df6d1adff1d2169 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 8 185ff9a6c30cfb4b0536b484bced135792f00509751cd006df2c714f53fec7b2 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 -8 d1d70baf419895f95f0b4eeb26a7b7510015153c72d47a9a48105ac170688ce6 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 -7 e328bdfec9d335c3dc8079a54b28c776752870004058db0844b2b4ebfc025071 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 -6 70d531e3e5930e2fe203a446f97720f94ac7ce8a7aa5b1f9be733d081b503593 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 -5 bd2b0e65f1c27f14e168f12add29691b2375583e9967f6b85b7f0a4bf5dad848 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 -4 327d84fbaf300d46540d858bf08df637404d80d4de3be3dd0baac2b4939df441 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 -3 c50372e6900776f53862e713ba873fcaec8102d77120460599a75f5aec4ef4ea 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 -2 81ce5424ec2ad445bd0158cbe57137d5a566726a705fe35c7538088bb986526a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-5 -1 ad141fd05004823307416d1d3b4261d39387f2dc6e52b38475e1d9fd526dfe81 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 0 86e9e5ce6e00100fbfb8ea5112a0d86a450fdc297c9e710dc5a26f727c865ecd 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 1 27dc14d872694dc1502d2f9b84a5d1a5dafca624a3972ddad3f25941feaaaa73 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 2 bed812029e6fed53cb5abb9ee76f970c322a9028c6421c6334ade091f2d467f0 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 3 b8a6b54438b8980d0ecbe3c2d7d0aebeb2bca228c530651c76213b2f6be90af4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 4 dda62b17f47229a7e1d88f29aaea0c06a98456b3d11a6fcab848f61be6011bc9 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 5 b4b40ef42ec4dc99fbc16149387750bf43a4c0111909693de2ea14b98eb631a0 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 6 e946df48cd22fdef70b500519b5f83a7369a014e6e92521df3ef94305ba12541 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 7 9c19d9ed486cf5fd9a8786a0ae4bea850dec20e987b3f34683e09d829b6bff14 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 8 e1851ad3d1e45e2447e581670864f5b31b804727d36ee6ceed956701379b5931 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 -8 5f604232c902d63a68d07da0d381d6c211a0fc5cc5132b57c63eee182b9bb570 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 -7 039905dec3c6e4f37ee946e4b593411f8ccc0b359d2fb0a26ee111b6c79cf242 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 -6 bbbc3fa281dabfc4301c21237b1a3b7fae9bec0ce10545fd566e0e00dcc80043 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 -5 ba1603746f8a4016fe9d1e6f9e58fd49b57c08445e49434624dd3f005b120f18 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 -4 4bcf6ab1960c59eac15286e5adf8efb5140626bd22225bb78bddfe43adb7c1e0 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 -3 1ca1fae7c121e55be9ca1e369fe58f09a61f13b981f189598eb46aac03ebadc5 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 -2 a3154b14c6c2472b9e3596bcee51372ddaaf2b2b4225090c77797612b73571cd 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-4 -1 ede17ad65f03e78875b5a96aa33004e0900c0304d56c228cea550f414203c469 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 0 bd87aa58f3fb8d3b974fc46449c776e610f2aff7667419089563dac7c938ca0f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 1 4dc30c498d41c84f8f6deed9accb857c29d759b533bce9b48768092690ac0424 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 2 d46285266abb3d5ca853d70c5549c4fd0fc8f32905feb66a55aec573d045afab 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 3 01d5a13a92f8221047bf8755efbb4a42a6a68582f0bae4e3b081b7c306632bbd 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 4 597746d886a37cdc7d875c6a0df97c3e7d8980d0ea33915fe307828f23686f1a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 5 3510c1c85c72591b00b7f08efa70ec438142f9fd22a89daa1f8981ed2dc2b10e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 6 9d531df8effbb51c64f08e99f1d5e2841eb4ef6c49d1397381a20a6576e16850 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 7 755e79e527c74cdce0d0b1b4dbea74d639ce939c306129e0ebea84cf7d72bd13 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 8 0d1b0b8b1729aa56df6c1bdcc08c9057c4fd2741b9a3d369242774f53944e02f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 -8 4367ce7e43ce3dce053a5aa52b7e7b700a7e266239e91a70a90fc06b63d18ece 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 -7 c59e78e77238284e754406e9e5edc9ab95836de31ea02b3f9d892a97b8cd78a3 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 -6 c63548a3b36db9293dd8bca785b008a3e555cfc28ebd29c3f3f2db6b48ad7d90 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 -5 4680afd2a8a2070863108cf1b6bf2d20526870039194856dbc01a247decf4433 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 -4 697e060186a06cae5ceea0c2022ef734e8515834f154c339c4f858933af34bbb 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 -3 2819fced8d0234d52889df26a34816f77e5dcdcbd0ad6a84f14eb76805460ffb 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 -2 e07b5b1c0c9fa889833d12b7d4e0d77a6e13562115f7df0821d33d944ffb951c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-3 -1 fe3f09f13704393d2b7b1de01ad1e6019c2026732c7f37fb606e98b8caa8fc57 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 0 51f8a1a38a41616de5065389e8c2d8ab2ef140b84cbfdba5450455b5595b9d3d 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 1 e581ea05cc318caeda834761bfd658a9a5c6a7538319a8054828ee9e130bf263 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 2 cd300f15b2ac4e29c99f9259f789f70693e187b47fb4327366d0a2f7e1b43767 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 3 9ca913943094b455fd9ddb2baf4a2d5d9804dd9d6306ed8fe9343f08c28f3699 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 4 22090840e16f339a5f2785d6499a431f00c39fce5d54f87b4cd338b03993729a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 5 62278637c9203e9dc118d6b572fa305988d3b8835e3c6f51317418e02807c569 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 6 fc79a0b5b1c3c5807de4a44f5859d4a9b0a71850ff423f2007f2690e15635eab 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 7 a8369fe6fd76191aac527edcf568b15f6f222a96ded1fea44bf855f0ae9b48ae 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 8 55d37c3ee6dffcf24c0b831d0bbd8bf48391002bae36cb179d89d95ae7a21605 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 -8 0a70e75164d5358712a0e87b6bf1a66c9bf42a43332eb2d1de9bd6661f0501e6 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 -7 60e5e216ff3a4147a4a206861051a773924bf7d59910dc690bf78486ccf0a826 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 -6 00fd0a1144b5a098953d57a1f2b43258c7796b5f023f072c15267e993258de85 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 -5 cafd29e010f6af2ebe580b9b0bb710a54fd23592dade31b7e669b35c3f76f3fb 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 -4 622462ae5157344ddfd31919d4f829bd7de499d859e8ea9e7a78aece2948185c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 -3 0298439e5b148523b1a4a39ef0ceb4d46cf05afa0409b0531b44e096ef3cc371 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 -2 ba01546214f5c228ac0d49fd29af7e537e690e6c23ada25ff5ebb8b7cf8900a1 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-2 -1 9ea6b0d37b3b9e81fe0d0384de53793b23075433b7c77bb7dd3b10dcea2ba0c1 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 0 5b945f1828d88ac141f97bbe29587a2841d85a631c19e9575ee33f92d3340813 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 1 df696e4e814a2ede0ef1377cf1a1ab783577d4f328397ddebd8ad87cee12e535 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 2 dc6b7b03f4731400d9e04a20e0a3f6175ea7b5cf48a1dc9ec17d93b3b55d79f0 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 3 d9929169dba8ea107712fe27d739d662155b37ee61a54363f029365a89b110ae 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 4 8c6a4a674b635ba2cfd81b5eeb001c9feb27be24c4c63519f1b3bd5e5eedb27a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 5 bf7eda22dfb6b3b4200ffbdb769cd431556a60a6f475482775d2df4a36e6f348 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 6 0fb399a09635cb5b63def5091260c412e2471259f99b4cfa62fd9c636ecbf096 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 7 0b607c01a69c2265f51a6f8064fe74f1a79d92b8cfc4d1079f8d853de407b307 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 8 9cc17e392bb780aa2dbd777f2e639e23acaef1f27c5c60ad2d3d992b78d31d48 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 -8 802a05904797cf3791f398056dbdcf9fa04cf8f30412f330e74a4c4344075558 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 -7 85246d2e6d27b72a997a36b467b6938e0f8d0e9d1ba865bcc02f3391afb07e26 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 -6 3b7485773bdcc52ad4dc09e2aa66aff5d2d339d481c7e8f4041f175486cf112f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 -5 3de97a621cd4c0871e0bcad0bf260151ec13436496912e48d994b938a0b0e4d7 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 -4 6084ca4bd6718289312e4a510635608bdb9a53111f8f855f3f62436eb0eb5c19 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 -3 7d2c092c140c2105a6e1be99c2535a97c240bcdac31a31ae724c0fd1af3d4415 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 -2 040127151932013ebe1894f4204923cd18ad5f80b6b756d32e915d1261476e05 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
-1 -1 d534d548ba184029ad57ed1619cb66af64c8559d2e1916dbdc35352944a92002 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 0 9d634e5914634328e642e3ff4ee25b1d855f485f93af99e8ab3abfe396ad0711 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 1 642706de1286e15f8d63bd5b71c24dbe00906a4f0375e4c74fc65365aae72991 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 2 88a50a0c23489c5b7bc49181b2839ff5d9c0af0699101c778b2c6fcb4a0a8922 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 3 a6e306902412137b51c1f9e49b22874765a9fc474535e2b12fa6e98fcbfeb5df 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 4 b3200e2fcb52fd1e925d043eea822b06bf9628fadd1f58ad58c2656d33d1ae85 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 5 2b8861ca99726fa6cb1b85ccca44426c79b36fe61b9384e4bed568c5104feef2 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 6 952eb848f973913ab7c037c4dead78872690720732d751fef6f4777315f6c44d 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 7 b93001115c62d7ab2733a86b482b340fa5eb789ddb99039e7e299e14f1c106e4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 8 dad0064ff5ad856d2140e923b771ea92d0bdcf157e3bb813b79565ff9f473b36 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 -8 e5b7f9b4885bbb15d3da42b3343610521e1b03a73bbf208b66b53833b180166b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 -7 b6a2ff710e665cff92200cd77b21e575a3d7db02986aca3054a86d5f10b7f05c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 -6 3ba1c1c28f3b875a40eacc39a74ef819bb52df10b3313c535edf5b973551e8f6 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 -5 905d0c485c49a9f01aec61d859b5548c4ec72eade81ca85c3864294d3677b151 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 -4 33dc036e9420b269dcb5247f2df35e55a42310c6d8a6784ed121d56c7487ee00 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 -3 2c8d81467deb96167f74cd6e1fc51ce97adeaf56596f0bf948f37edb6707bf0b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 -2 64d93e292f86d8701d0ec2e7d099196b94d5fd179ceed471c28ae59e55ee28e2 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
0 -1 708b7e4b5c912b10ed5147d2d87bd77b913a37fbde4155aac7ce6260b5471bcf 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 0 8f2cba2e6652796cbab43457666667d8ec5beef356095467fb75ba53e7de38ce 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 1 0b69855d8785d992be6743aba315c2b1014a58dab0b364af3394823652ca62ed 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 2 0db7977d44cbcbee49be1a963b42b60756f21077f04fabf78f3a71c617fc8db7 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 3 c0c5bb35ddcbf2b25c360f100e949629c2533a326feae8d114fe465740eb08db 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 4 54822a01ccf10a646ed9459b82a12d177813bf4a23c55ea4f7aa92bf1afb9974 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 5 f01e402910c0d3760368d2a6486a2e6ecc9f63ca8e58765074fc920d25b0faed 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 6 2cbb7b541d8e8012c64fdc872270fc1400cc510182f4333d6a0b5dd21215d08e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 7 b711c83e157c349c2d3cfac9b6add13e097d507284f6fa62f18c24ae78afa814 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 8 76e802d6e2425307ca4644014b7b03e53fab0f4adc03dfa4baa7623d7d8b7413 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 -8 56b4596f35a47200714e74589f9b89a8eb5a01df53f2e5035a294f4c80d88641 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 -7 ae559e04a1367fe775479c35168382ee74bd450774c6bb785335d2ea468a741e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 -6 e3e4f5ad931e99baecf24895bc7c3d7f8cfae5c3dc485c21ac47a3f5030e3617 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 -5 047a5d16348cbac2e1add22d67268d5f1e43b7d03de0fe85edb6abea470c6cc9 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 -4 53021a6c0ca99c136c1dc7c7365c6a3b7b84dcae506fbf1ea54eb1cf6456dcc4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 -3 83d897f90ec0fc2357c93bf07d43952df23c9d1d0d6d5b22ff73cac0ab338b7b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 -2 afbb2abecfa34904954f2e16ffb56b49dd292170f90dd9552a2f101d101c01ef 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
1 -1 a22df312d77de965860d2bd1ff489d1c38b343c2beb11707949b444725c25a4c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 0 9abdce5ab4c5df4479bf095f5d200174650f4beb3504a99c26f988b3b7675898 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 1 a2cd93c410be4911fe0304da5d2993058ad5957f670df5e5d781e3a7fde37fbb 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 2 19606f317ca9aae79e2d7f66c9ab14d9138a7b7d9bcc5d61a2040297fb1580a3 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 3 ae9e0cef6124a2fb1283cb349d698030f90b6f1509b46a88b79d4ad81c5075c3 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 4 ceb1afc0561b22131484f37f704c66fb0be6e62262ddc778de791b3780b5b535 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 5 703003021b68774b61e040bb552f4883793828cdbe75dbcad424afe2220e18ab 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 6 b7e2a7f81ffce5851a996898cacc38b3597426dd473ea6310dca7ffe126f99e3 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 7 e13be39d90d9bd7e395e0763bebdf29e8c08d676ec78d5a16b7511710d302909 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 8 907fc6c166e0a65d37f5276072ccaa3fcab9064660836691e3f459c8a0cf42e6 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 -8 86c8905772609245555d5860066b167773b4cb919a46a7ea7ec2fb8075399753 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 -7 c132100840d3ac286482223957a18729768cff3f4cd0721c794771e4c50ec315 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 -6 9a6becb1d8aa3c329623f53f1a1944e1e2a0f0988acb1d2cdb6c231afa211e90 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 -5 cb8c69a2143e96e0c49c3131f209fee789513246f1afaf95cb01e3088ab299f4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 -4 d256f0a962427bc0624eaf826516d981596fc252ad0d10d7204b1fc06477b07a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 -3 11ddf89eb5ed8e7d5c6c0c98c209f1237260fdaa5f5ba1e9608f412a9b1d1a08 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 -2 9bcc19afed624473a53a7d6492714d7fd36203c731c37fb2a0c483afcac0dd8f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
2 -1 44daf4251ccad570516d5c2490cfa8095da33dbd0f66465db700791db9642919 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 0 31a12ef94005ed541ecc4e83a2fa493f0d4d28a3ba08e9a5d9bfafb30aefe28d 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 1 a5cb0ee02b85b4235cc64e5ae388ed0c0749712efe1ee79479b72555f05e4f30 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 2 9047f6e870831d21b794349be2c312a0df67022627e6288dcee60aa4159b5d22 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 3 f8888a1491bf3e2df54d40c3814ef2ea912b88ad40cbecdeb44f1fa5663a5f71 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 4 287401b60ebd6b4e4f7a73dbcd35c919b182fea0e62c41ddb1c1288537b62ca9 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 5 dd885bb7003dd62933dab34145561b99f8035f8041c05ee6ef8346cd31b89933 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 6 8f8ede04cf803d45a24932177ba707688e09cc90d1b2cbabf6298a6b29d19eeb 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 7 d825796e3a32cbbb3604aa65fdc417defb5effdc32e05696344d9a3c016ed474 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 8 260a4e2748902a3a2fb8d7af62929100ba6fe9975142286032f33fcfccf51f6c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 -8 d66d74a3e4b2f247cdefeb554299b45cbabc8d879e22b95d4b27cfc8e5141cbe 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 -7 0bfea12ea04c7720f5648c89a01fd175be32e8a77cc4a7a603ed2876c62161dc 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 -6 626380d70931e53f62e83ae8118840eda96107e5f0965daeb199a2740e1c2e5b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 -5 56b49e323ffc4ada99a906dfce0d1e2c11b31dc3e9052c0c468ed6420d31b858 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 -4 f2350cb0d8dde09f298c613f5ad1d97fc382b0c1d5b12826eddb2f2195bc7831 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 -3 8c57558a9a5644c05c5daa58d17e2be1b9de1a65d6f7deb4855297a97523f482 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 -2 336127103c8f6aae0fd993713bd880199eeefb589442fa5ebc2386f0c022dce0 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
3 -1 523a07c0a804ede754f78283488eddef94bfe25b72cacaf534263282dfd46be5 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 0 cf509fb497c5d7bda76448d12a1a4fe8ce0fd38036a271fcd7b0c48449f82ab8 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 1 59ab0838a0833b55c37406f68c447c2b084e88f4e17d59cd6dac4bfa30263b8a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 2 3588d7ae2ee246864fad05fd0f03dad8741ffcc0cb7704c11c1bd431b049b235 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 3 f70ae200940e87bc0df540e7921ccf5600b1c323b9c07368e07587f34c55d1c1 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 4 6e98b393eaad8c62f1a2425559556100ca3dff5a13f82887e88093d9ef7d620a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 5 7d6899d1c1a8de24aae6e47ff09cf5f876e247b66f7925943b6bcc5c2fa18b2f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 6 4fd67412489af76fe1c5955685380e31a34475d401b3c7fcbb7c5d088faa42ad 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 7 774c770ffb3179e5b2c6d1fe97ff6ac8d3645aa725b52c4ccdd46260e8505ff5 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 8 3097971d8936ed6d53a74ffb45f4eb98118bb7d364a34494de72a86f0b3cf2d5 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 -8 20ab184d0549d5312eb3c4c4c93c27d3dfaf3cf5dc599f35c291b928218938be 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 -7 b22075a0dd7dd86db2e98414131576d3f28fec55d7e8ad623fdc92d2e5c77f17 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 -6 d4e34b1219c0d25371cedb943dd3ff72fedf96e2d86d4d62e36c29195efbf1d4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 -5 b13ceb0561b22bb5dc346608a63635892771509538f26a214d5cac8213207b99 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 -4 40c1c9e7104508edae785a3534a98e9e74dd2a4f66c5853138fa441fb64f003e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 -3 e4b49f322b501551da69e06e3ce41572ce6af154dffd1e4b27691c8153cd4fc4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 -2 0fdb0c8ac97ac20a56c15058ae46a6e4c734ff6974dd85068005779f5685ea9e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
4 -1 35edd44c1c2f3001316a226dafdcd90aa67de5ae613ae56cad6d3ee985eac4e4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 0 b08c5b7e8d207372eb4be8e01289449e574f1d9e0f882d1af190529d23f3e001 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 1 282aa33dfda7d77a51448969f29f5414d452ab88e4ee7c531465746db049f394 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 2 2a6739e7840576314fbcf347982829160ca4a5fb910746cc7a7f93c457de360a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 3 ad2a08c773242a0f61c0d90a00c166b3de65466d09f02ce5daba95fa8af6ecfb 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 4 8ed02630b5641e2efcce55ef70d0387898996d9fdc4979c41ca894a39776b365 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 5 d66c6279267ce407bbe5c0007a50bb48359d08cdf95cce6659be45963148e402 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 6 50bc4674208dde2e0f0124d95fbdeda90cd9d9aebfb584a0ca24680e9275bf46 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 7 e1080da9f31c8cf90e12335cacd75a3439f693f963e4e7da28643a4aeea62a56 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 8 fe11aca2e168ecdca408d4dcbc45b0083f834fee5de4ddbb35227f475925978a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 -8 8f51ef501c2b4b16b808dc7cf3845ada51ea8edaa6f8681a234b592f27937983 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 -7 69937c9e1b63da726e051f0725db5355e2bcb18376df2f3b3028cf804bb24db6 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 -6 c20e2b424f96d572cace671c9063268b764a08b9927bf4ca785dfbf300d4ec63 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 -5 a14bedb28bcb68f69e39257ada66536a1eebd15af6aaf8dc9c868cc4808a079f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 -4 044bf8274f32bad7bdc4a720611faac7ecf961d201790af3b14646cc6ef65280 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 -3 f9e4bf03c9e9a0bb68e22aabffdcd10da64b91f736d1e09a7b43f4bc94a98077 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 -2 fd6394bbe1b01be2f5fb8847f543e69bbbf6ed272c29650d721eb7a4de32b4e2 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
5 -1 620d5b20d3cc817044d8e1087cf18cc42fce70ef04a26ff7b3ea6b824b685122 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 0 9f1d84e4b370154ed36eb76bd8feb1a92d4e0d04aed7e1fe55f30ba19248ae8a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 1 65501bdc9af1fa61b6ac7f3016d7db4529b9bf20f39866a569b6e58c73865297 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 2 c4258153706bd0863490040c8af3e32c6d39ce0e664e99069cf7b10f1a1ebebc 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 3 43f539e2d1729422f8c7724f0bc2a0ad1bb750bbded45ee10dfb6f9cf8de9bfe 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 4 329e0c6370d49ef08a3515a6491389a1dd1501a31ab98f9a7b64e87e2e43ca30 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 5 a980add3cfa56e95901b5d693cd741c97cf35e3fcdbde7272c48a39d3785575e 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 6 3626dc0c6fb1e8f1c05573e6a309d97fa83cff6af9020948f5471fee24f11969 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 7 1ccdf04b99d41c2fdf5f62fa148f6c9a45285650a7fc0a8438d3bd80ec9a5753 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 8 12aaa847543f538ba7a85aa90edecaede9fa313e881b1dc560ee6e0e6640c8ff 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 -8 683ce017cbc497d1b823090a0ec984c0478ddc4e3ec2077096b6569a54d0186b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 -7 94ea7dea313d1ddb52b086acf681185b45de615ca19b07f9b31863ad50d63df5 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 -6 942e705c68ed7f1ef3c625843a06924dcb1299c10421bdaf3914c3cf091a2ddb 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 -5 7a8982f6e99e72e735d32f7685b486447642b80463b9ac09afa4c8d392d1cd8c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 -4 22b2dac5870b720f01d68d38475287a34f0ed21f39dd9717b7dda9f96818ef1c 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 -3 20fa5ff56a7e160284fee9efe2731d505b59feac67ccacaf7c33f8cc92d2bece 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 -2 7427be500743e898067c3879844931a0c1894cb62524717547267ed1e69eb4a0 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
6 -1 98446f1c88d2efea8314f61f35f8fb01f0478c77fc5204095ba83fb53ac8c7ef 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 0 1196a06655cf67cc0d56ac2281315211a13769b9198bed293aa3813d088e9d44 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 1 2b50aedb8471f269114bb7e8be545c0066869bb7812d96c5019006d8bbfbc250 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 2 d71eea940fa42cf164940ccf32137f76bfbf78087e3bf55827b32c0539e3651a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 3 3b10ae5f226f8bfef16e84cafc24c3d06fcf7e9fe894024ce1bffd9548c2200b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 4 c627e0fdede69245d1d864e93f7d1fdb09bc28a8d1573de2d9bfcc2b3ef865a3 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 5 88ba2b1f11c70e7c4e132ef95d20e5fcddaad3aab614814b303a9af7e29e453a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 6 6bbc34b26da81a0820eb03b848f372f32fd9c09c41c5f4c094f4598b9b495a7d 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 7 a7c48618de4cd7bf9cfbfc794514556f028755bc6dcce0a34cfae009d28ad4f1 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 8 32885533d96c8ed9d11a23080df65d08c8360852ee79ea3923767808b06c599b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 -8 3e7c6219d03bcb3d3f5e845735e648a9dc0ddeb4047f3004469e89058e954094 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 -7 7202ef092e139a4f3119e32d827ce1fa0640d02329230a92c7e43f6893e8cfb7 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 -6 f57cac950053f8f2aa978e01473a91655bc32427befbf2f2eaaca5642304abbd 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 -5 e07049ac9a576241138f0c931bce0f737328b9f5cf5e2f076a33d0c4dc96843a 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 -4 48e72991ba4d7f30546a4db4e7d6e0eb3908ac2f45150d57aca666ace4e0ecbe 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 -3 7b2c25f93d898720159a89cdcf6fd64d108d3325a4b4b7136ad4054232f7a416 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 -2 fda7e40529cf6ff9c3447d9fff9906c549f2998baba2c7edba22c0641156c3b7 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
7 -1 88ae6027aca5f412c97a28bbb7c7d31dda5616a3f2600764f4ddf608f27ad1f4 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 0 62b82563ed14f8182e3e4b83ff54cc41657cf55eec2a5112123cd7cf095f9995 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 1 02797a2b0765485eeea460b65f03a7a94937eb3300b5eb9d09fd1e09cee38894 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 2 c52de795f6ca08db1be71a2a4f518c8b6b70bdc53800d708503118ae95893f86 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 3 378b7f06bb7dace3bac1566f3148256a0d8365d2918926dc73186b40c25e1518 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 4 ddef83c202a59cb14b5b75bef83d678c11d1d0a33fa803f662c9f9cb435d3110 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 5 ab3866ec28e7eac2b18b12b539337cf3343816885e18a1498b7f7106288cc670 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 6 45e9ea668e339e9bccdb103eb6a055f10bb8dc49e7177a37b9a19a862f13e861 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 7 1871c550876139b2a533c43a3b6b6afbbdc4c3aa3aa903cca98e3b67f23cc4d2 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 8 5a9a452a44f385b53ac52f5fc6921f2f9e865bb63b1f0958b2314f5526156ef8 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 -8 819d7f6becf5f5acf6aef8b750353ad867289aa8a8a6b3ced77e999c7958a47b 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 -7 57ac4798c2301dc72028f4704fbe897a2e871cc2d88957840a7f9264261cb6f1 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 -6 6da3519f7893f32772681a6e12f06edabb774e9e8d5a9293002608a8cc40e9fe 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 -5 2e2e6e327e0e27d6d6de0ea6618f219fd080f6f9de05b6d6602ebb90bad6a077 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 -4 01989f7b6de2ec03426e2b68190d6fc75e2d57b24225305377338294ca2d14e6 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 -3 e57520d1b8c17e9de905510adda0d94fe4d8309245db26ecfc6dedbdd7702719 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 -2 35ba48632ddfcbe171d457105142dee5f1123f528f1a02c127377b80e83ca58f 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
8 -1 ab693d879e0a8e294d19057eb9540fcf4d2daae5c4bd1b5df649b74dd86697a9 27f1c6a7c3a67d38cd1c3cb8cded37b2d7866b89839b0e9f0624bee0644d5b23
#combined=28b777380f2a5e467cb387f9c3fb08c8a3e6bdc6bc226c38ee6e14cc7e380da4
@@ -66,34 +66,33 @@ public class CustomBiomeSource extends BiomeSource {
this.cacheDimension = engine.getDimension();
}
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine, Holder<Biome> fallback) {
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine) {
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
boolean resolutionFailed = false;
for (IrisBiome i : engine.getAllBiomes()) {
Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, i.getVanillaDerivative());
if (vanillaHolder != null) {
biomes.add(vanillaHolder);
} else if (!i.isCustom()) {
resolutionFailed = true;
if (vanillaHolder == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ i.getVanillaDerivativeKey() + "' is not registered for biome '" + i.getLoadKey() + "'");
}
biomes.add(vanillaHolder);
if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
if (customHolder != null) {
biomes.add(customHolder);
} else {
resolutionFailed = true;
if (customHolder == null) {
throw new IllegalStateException("Iris custom structure biome '"
+ engine.getDimension().getLoadKey() + ":" + j.getId() + "' is not registered");
}
biomes.add(customHolder);
}
}
}
if ((resolutionFailed || biomes.isEmpty()) && fallback != null) {
biomes.add(fallback);
if (biomes.isEmpty()) {
throw new IllegalStateException("Iris pack '" + engine.getName()
+ "' has no registered structure biomes");
}
return new ArrayList<>(biomes);
}
@@ -159,7 +158,7 @@ public class CustomBiomeSource extends BiomeSource {
.lookup(Registries.BIOME).orElse(null);
Registry<Biome> worldRegistry = ((CraftWorld) world).getHandle().registryAccess()
.lookup(Registries.BIOME).orElse(null);
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine, fallbackBiome));
return Set.copyOf(getAllBiomes(customRegistry, worldRegistry, engine));
}
private KMap<String, Holder<Biome>> fillCustomBiomes(Registry<Biome> customRegistry, Engine engine, Holder<Biome> fallback) {
@@ -310,10 +309,16 @@ public class CustomBiomeSource extends BiomeSource {
int blockZ = z << 2;
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
if (irisBiome == null) {
return getFallbackBiome();
throw new IllegalStateException("Iris returned no surface structure biome at block "
+ blockX + "," + blockZ);
}
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, irisBiome.getVanillaDerivative());
return holder == null ? getFallbackBiome() : holder;
if (holder == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ irisBiome.getVanillaDerivativeKey() + "' is not registered at block "
+ blockX + "," + blockZ);
}
return holder;
}
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
@@ -362,19 +367,17 @@ public class CustomBiomeSource extends BiomeSource {
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
if (resolution == null) {
return getFallbackBiome();
throw new IllegalStateException("Iris returned no structure biome at quart "
+ x + "," + y + "," + z);
}
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, resolution.irisBiome.getVanillaDerivative());
if (holder != null) {
return holder;
if (holder == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ resolution.irisBiome.getVanillaDerivativeKey() + "' is not registered at block "
+ resolution.blockX + "," + resolution.blockY + "," + resolution.blockZ);
}
if (resolution.irisBiome.isCustom()) {
return resolveCustomHolder(resolution);
}
return getFallbackBiome();
return holder;
}
private Holder<Biome> resolveVisibleBiomeHolder(int x, int y, int z) {
@@ -2,12 +2,15 @@ package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.core.structure.NativeStructureLocateCapability;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisVanillaStructureStiltSettings;
import art.arcane.iris.engine.object.IrisStructureStiltSettings;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
import art.arcane.iris.nativegen.NativeStructureLocateResults;
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.reflect.WrappedField;
@@ -81,7 +84,6 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public class IrisChunkGenerator extends CustomChunkGenerator {
private static final String NATIVE_MONUMENT_KEY = "minecraft:monument";
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
private final ChunkGenerator delegate;
@@ -109,74 +111,83 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders, BlockPos pos, int radius, boolean findUnexplored) {
try {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
BlockPos best = null;
Holder<Structure> bestHolder = null;
long bestDist = Long.MAX_VALUE;
for (Holder<Structure> holder : holders) {
Object id = registry.getKey(holder.value());
if (id == null) {
continue;
}
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
engine, id.toString(), pos.getX(), pos.getZ(), Math.max(1, radius));
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
continue;
}
if (!result.found()) {
continue;
}
long dx = (long) result.originX() - pos.getX();
long dz = (long) result.originZ() - pos.getZ();
long d = dx * dx + dz * dz;
if (d < bestDist) {
bestDist = d;
best = new BlockPos(result.originX(), result.baseY(), result.originZ());
bestHolder = holder;
}
}
if (best != null) {
return Pair.of(best, bestHolder);
}
} catch (Throwable e) {
IrisLogging.reportError("Iris-placed structure lookup failed near "
+ pos.getX() + ", " + pos.getZ() + ".", e);
}
if (!importedControl().active()) {
return null;
}
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
level, holders, pos, Math.max(1, radius), findUnexplored);
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
if (reachable == null || reachable.size() == 0) {
return null;
}
try {
return delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
} catch (Throwable e) {
IrisLogging.reportError("Vanilla structure locate failed near "
+ pos.getX() + ", " + pos.getZ() + ".", e);
return null;
}
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable == null || reachable.size() == 0
? null
: delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
return NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated);
}
private HolderSet<Structure> filterReachableStructures(ServerLevel level, HolderSet<Structure> holders) {
Set<String> reachable = reachableStructureKeys(level);
IrisImportedStructureControl control = importedControl();
private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
HolderSet<Structure> holders,
BlockPos pos, int radius,
boolean findUnexplored) {
if (findUnexplored) {
return null;
}
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<Holder<Structure>> kept = new ArrayList<>(holders.size());
BlockPos best = null;
Holder<Structure> bestHolder = null;
long bestDist = Long.MAX_VALUE;
for (Holder<Structure> holder : holders) {
Object id = registry.getKey(holder.value());
if (id == null) {
throw new IllegalStateException("Native structure locate received an unregistered structure holder");
}
String structureId = id.toString();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine,
structureId, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
if (decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
continue;
}
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
engine, structureId, pos.getX(), pos.getZ(), radius);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
throw new IllegalStateException("Iris structure locate reached its safety limit for "
+ structureId + " within " + radius + " chunks");
}
if (!result.found()) {
continue;
}
long dx = (long) result.originX() - pos.getX();
long dz = (long) result.originZ() - pos.getZ();
long d = dx * dx + dz * dz;
if (d < bestDist) {
bestDist = d;
best = new BlockPos(result.originX(), result.baseY(), result.originZ());
bestHolder = holder;
}
}
return best == null ? null : Pair.of(best, bestHolder);
}
private HolderSet<Structure> filterReachableStructures(ServerLevel level, HolderSet<Structure> holders) {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<NativeLocateCandidate> candidates = new ArrayList<>(holders.size());
for (Holder<Structure> holder : holders) {
Object id = registry.getKey(holder.value());
if (id == null) {
throw new IllegalStateException("Native structure filtering received an unregistered structure holder");
}
String key = id.toString();
IrisNativeStructureDecision decision = control.resolve(
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine,
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
if (NATIVE_MONUMENT_KEY.equals(key) || !decision.generate()
|| IrisStructureLocator.suppressesVanilla(engine, key) || !reachable.contains(key)) {
if (NativeStructureLocateCapability.isPaperUnavailable(key) || !decision.generate()) {
continue;
}
kept.add(holder);
candidates.add(new NativeLocateCandidate(holder, key));
}
if (candidates.isEmpty()) {
return HolderSet.direct(List.of());
}
Set<String> reachable = reachableStructureKeys(level);
List<Holder<Structure>> kept = new ArrayList<>(candidates.size());
for (NativeLocateCandidate candidate : candidates) {
if (reachable.contains(candidate.key())) {
kept.add(candidate.holder());
}
}
if (kept.size() == holders.size()) {
return holders;
@@ -195,14 +206,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (cached != null && cached.dimension() == dimension) {
return cached.keys();
}
Set<String> reachable;
try {
reachable = Set.copyOf(VanillaStructureBiomes.reachableStructureKeys(level, customBiomeSource));
} catch (Throwable error) {
IrisLogging.reportError("Iris could not resolve native structure biome reachability; "
+ "native locate is disabled until the next hotload.", error);
reachable = Set.of();
}
Set<String> reachable = Set.copyOf(
VanillaStructureBiomes.reachableStructureKeys(level, customBiomeSource));
reachableStructureCache = new ReachableStructureCache(dimension, reachable);
return reachable;
}
@@ -232,9 +237,6 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
if (!importedControl().active()) {
return;
}
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
adjustGeneratedStructures(registryAccess, access, previousStarts);
@@ -242,7 +244,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess access, Map<Structure, StructureStart> previousStarts) {
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
IrisImportedStructureControl control = importedControl();
ChunkPos chunkPos = access.getPos();
for (Map.Entry<Structure, StructureStart> entry : access.getAllStarts().entrySet()) {
Structure structure = entry.getKey();
StructureStart start = entry.getValue();
@@ -251,32 +253,40 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
IrisNativeStructureDecision decision = control.resolve(
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
if (!decision.generate() || IrisStructureLocator.suppressesVanilla(engine, structureId)) {
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
boolean undergroundStep = NativeStructurePostProcessor.isUndergroundStep(structure.step());
IrisNativeStructureDecision decision;
try {
decision = NativeStructureGenerationPolicy.resolve(engine,
structureId, undergroundStep);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"policy resolution", structureId, chunkPos.x(), chunkPos.z(), error);
}
if (!decision.generate()) {
access.setStartForStructure(structure, StructureStart.INVALID_START);
continue;
}
try {
NativeStructurePostProcessor.applyVerticalShift(
NativeStructurePostProcessor.applyVerticalPlacement(
start,
structureId,
decision.yShift(),
getSeaLevel(),
access.getMinY(),
access.getMinY() + access.getHeight());
} catch (RuntimeException error) {
access.setStartForStructure(structure, StructureStart.INVALID_START);
IrisLogging.reportError("Iris rejected native structure " + structureId + " in chunk "
+ access.getPos().x() + "," + access.getPos().z()
+ " because its vertical bounds are invalid.", error);
continue;
access.getMinY() + access.getHeight(),
undergroundStep,
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error);
}
}
}
private IrisImportedStructureControl importedControl() {
return engine.getDimension().getImportedStructures();
}
@Override
public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> holderlookup, RandomState randomstate, long i, SpigotWorldConfig conf) {
return delegate.createState(holderlookup, randomstate, i, conf);
@@ -347,15 +357,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
if (importedControl().active()) {
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
}
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
}
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
if (!structureManager.shouldGenerateStructures()) {
return;
ChunkPos disabledChunk = chunk.getPos();
throw new IllegalStateException("Iris cannot generate native structures in chunk "
+ disabledChunk.x() + "," + disabledChunk.z()
+ " because structure generation is disabled outside the pack; enable native structure generation "
+ "and deny individual structures through importedStructures.disabled");
}
ChunkPos chunkPos = chunk.getPos();
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
@@ -366,42 +378,60 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
BoundingBox area = writableArea(chunk);
int steps = GenerationStep.Decoration.values().length;
IrisImportedStructureControl control = importedControl();
List<NativePlacementGroup> placementGroups = new ArrayList<>();
List<StructureStart> nativeStarts = new ArrayList<>();
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
for (int step = 0; step < steps; step++) {
int index = 0;
for (Structure structure : byStep.get(step)) {
Object id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
IrisNativeStructureDecision decision = control.resolve(
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
if (decision.generate() && !IrisStructureLocator.suppressesVanilla(engine, structureId)) {
try {
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
try {
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine,
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
if (decision.generate()) {
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
if (!starts.isEmpty()) {
List<StructureStart> resolvedStarts = List.copyOf(starts);
placementGroups.add(new NativePlacementGroup(
structureId, decision, index, step, resolvedStarts));
nativeStarts.addAll(resolvedStarts);
boolean clearEntireFootprint = NativeStructurePostProcessor
.shouldClearEntireVegetationFootprint(
structure.step(), decision.clearVegetation());
for (StructureStart start : resolvedStarts) {
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
start, decision.clearVegetation()));
start, clearEntireFootprint));
}
}
} catch (Throwable e) {
IrisLogging.reportError("Iris failed to resolve native structure " + structureId
+ " in chunk " + chunkPos.x() + "," + chunkPos.z() + ".", e);
}
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"resolution", structureId, chunkPos.x(), chunkPos.z(), error);
}
index++;
}
}
try {
NativeStructurePostProcessor.prepareSurfaceStructures(
world, area, nativeStarts,
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain integration", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructurePostProcessor.clearIntersectingVegetation(
world, chunk, area, vegetationTargets);
} catch (Throwable e) {
IrisLogging.reportError("Iris failed to clear vegetation from native structures in chunk "
+ chunkPos.x() + "," + chunkPos.z() + ".", e);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
for (NativePlacementGroup group : placementGroups) {
random.setFeatureSeed(decoSeed, group.featureIndex(), group.step());
@@ -410,13 +440,27 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
placeVanillaStructure(world, structureManager, random, area, chunkPos,
group.structureId(), start, group.decision());
}
} catch (Throwable e) {
IrisLogging.reportError("Iris failed to place native structure " + group.structureId()
+ " in chunk " + chunkPos.x() + "," + chunkPos.z() + ".", e);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"placement", group.structureId(), chunkPos.x(), chunkPos.z(), error);
}
}
}
private static String nativeStructureBatchContext(List<NativePlacementGroup> placementGroups) {
if (placementGroups.isEmpty()) {
return "<no resolved native structures>";
}
StringBuilder context = new StringBuilder("[");
for (int i = 0; i < placementGroups.size(); i++) {
if (i > 0) {
context.append(", ");
}
context.append(placementGroups.get(i).structureId());
}
return context.append(']').toString();
}
private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager, WorldgenRandom random,
BoundingBox area, ChunkPos chunkPos, String structureId, StructureStart start,
IrisNativeStructureDecision decision) {
@@ -451,13 +495,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
}
private BlockState resolveStiltBlock(IrisVanillaStructureStiltSettings settings, RNG rng, int x, int y, int z) {
private BlockState resolveStiltBlock(IrisStructureStiltSettings settings, RNG rng, int x, int y, int z) {
if (settings.getPalette() == null) {
return Blocks.COBBLESTONE.defaultBlockState();
}
PlatformBlockState platformState = settings.getPalette().get(rng, x, y, z, engine.getData());
if (platformState == null || !(platformState.nativeHandle() instanceof BlockData blockData)) {
return Blocks.COBBLESTONE.defaultBlockState();
throw new IllegalStateException("Configured native structure stilt palette did not resolve a Bukkit block at "
+ x + "," + y + "," + z);
}
if (blockData instanceof IrisCustomData customData) {
blockData = customData.getBase();
@@ -465,7 +510,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (blockData instanceof CraftBlockData craftBlockData) {
return craftBlockData.getState();
}
return Blocks.COBBLESTONE.defaultBlockState();
throw new IllegalStateException("Configured native structure stilt palette resolved unsupported Bukkit block data "
+ blockData.getClass().getName() + " at " + x + "," + y + "," + z);
}
private BoundingBox writableArea(ChunkAccess chunk) {
@@ -616,4 +662,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
private record NativePlacementGroup(String structureId, IrisNativeStructureDecision decision,
int featureIndex, int step, List<StructureStart> starts) {
}
private record NativeLocateCandidate(Holder<Structure> holder, String key) {
}
}
@@ -15,6 +15,7 @@ import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
import art.arcane.iris.util.project.agent.Agent;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -396,6 +397,11 @@ public class NMSBinding implements INMSBinding {
return true;
}
@Override
public boolean supportsIrisWorldGeneration() {
return true;
}
@Override
public int getTrueBiomeBaseId(Object biomeBase) {
return getCustomBiomeRegistry().getId(((Holder<net.minecraft.world.level.biome.Biome>) biomeBase).value());
@@ -470,8 +476,8 @@ public class NMSBinding implements INMSBinding {
KList<String> keys = new KList<>();
try {
registry().lookupOrThrow(Registries.STRUCTURE).keySet().forEach(k -> keys.add(k.toString()));
} catch (Throwable e) {
IrisLogging.reportError(e);
} catch (RuntimeException e) {
throw new IllegalStateException("Iris failed to read registered structure keys from the Minecraft registry", e);
}
return keys;
}
@@ -481,8 +487,8 @@ public class NMSBinding implements INMSBinding {
KList<String> keys = new KList<>();
try {
registry().lookupOrThrow(Registries.STRUCTURE_SET).keySet().forEach(k -> keys.add(k.toString()));
} catch (Throwable e) {
IrisLogging.reportError(e);
} catch (RuntimeException e) {
throw new IllegalStateException("Iris failed to read registered structure-set keys from the Minecraft registry", e);
}
return keys;
}
@@ -494,8 +500,9 @@ public class NMSBinding implements INMSBinding {
ServerLevel level = ((CraftWorld) world).getHandle();
BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource();
keys.addAll(VanillaStructureBiomes.reachableStructureKeys(level, source));
} catch (Throwable e) {
IrisLogging.reportError(e);
} catch (RuntimeException e) {
throw new IllegalStateException("Iris failed to resolve reachable structures for Bukkit world '"
+ (world == null ? "<null>" : world.getName()) + "'", e);
}
return keys;
}
@@ -504,9 +511,14 @@ public class NMSBinding implements INMSBinding {
public KList<String> getStructureBiomeKeys(String structureKey) {
KList<String> keys = new KList<>();
try {
keys.addAll(VanillaStructureBiomes.structureBiomeKeys(registry(), structureKey));
} catch (Throwable e) {
IrisLogging.reportError(e);
RegistryAccess access = registry();
if (access == null) {
throw new IllegalStateException("Minecraft registry access is unavailable");
}
keys.addAll(VanillaStructureBiomes.structureBiomeKeys(access, structureKey));
} catch (RuntimeException e) {
throw new IllegalStateException("Iris failed to resolve biome keys for registered structure '"
+ structureKey + "'", e);
}
return keys;
}
@@ -518,8 +530,9 @@ public class NMSBinding implements INMSBinding {
ServerLevel level = ((CraftWorld) world).getHandle();
BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource();
keys.addAll(VanillaStructureBiomes.possibleBiomeKeys(source));
} catch (Throwable e) {
IrisLogging.reportError(e);
} catch (RuntimeException e) {
throw new IllegalStateException("Iris failed to resolve possible structure biome keys for Bukkit world '"
+ (world == null ? "<null>" : world.getName()) + "'", e);
}
return keys;
}
@@ -636,9 +649,9 @@ public class NMSBinding implements INMSBinding {
}
}
return new int[]{box.minX(), box.minY(), box.minZ(), box.maxX(), box.maxY(), box.maxZ()};
} catch (Throwable e) {
IrisLogging.reportError(e);
return null;
} catch (RuntimeException e) {
throw NativeStructureGenerationException.failure(
"capture placement", structureKey, chunkX, chunkZ, e);
}
}
@@ -21,10 +21,10 @@ final class VanillaStructureBiomes {
}
static Set<String> possibleBiomeKeys(BiomeSource source) {
Set<String> keys = new LinkedHashSet<>();
if (source == null) {
return keys;
throw new IllegalStateException("Minecraft chunk generator has no biome source");
}
Set<String> keys = new LinkedHashSet<>();
Set<Holder<Biome>> possibleBiomes = source instanceof CustomBiomeSource customBiomeSource
? customBiomeSource.possibleStructureBiomes()
: source.possibleBiomes();
@@ -34,18 +34,24 @@ final class VanillaStructureBiomes {
keys.add(key.get().identifier().toString());
}
}
if (keys.isEmpty()) {
throw new IllegalStateException("Minecraft biome source exposes no registered possible biomes");
}
return keys;
}
static Set<String> structureBiomeKeys(RegistryAccess access, String structureKey) {
Set<String> keys = new LinkedHashSet<>();
if (access == null || structureKey == null || structureKey.isEmpty()) {
return keys;
if (access == null) {
throw new IllegalStateException("Minecraft registry access is unavailable");
}
if (structureKey == null || structureKey.isBlank()) {
throw new IllegalArgumentException("Registered structure key must not be blank");
}
Set<String> keys = new LinkedHashSet<>();
Registry<Structure> registry = access.lookupOrThrow(Registries.STRUCTURE);
Structure structure = registry.getValue(Identifier.parse(structureKey));
if (structure == null) {
return keys;
throw new IllegalArgumentException("Registered structure does not exist: " + structureKey);
}
for (Holder<Biome> holder : structure.biomes()) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
@@ -53,18 +59,19 @@ final class VanillaStructureBiomes {
keys.add(key.get().identifier().toString());
}
}
if (keys.isEmpty()) {
throw new IllegalStateException("Registered structure '" + structureKey
+ "' exposes no registered biome keys");
}
return keys;
}
static Set<String> reachableStructureKeys(ServerLevel level, BiomeSource source) {
if (level == null) {
throw new IllegalStateException("Minecraft server level is unavailable");
}
Set<String> reachable = new LinkedHashSet<>();
if (level == null || source == null) {
return reachable;
}
Set<String> possible = possibleBiomeKeys(source);
if (possible.isEmpty()) {
return reachable;
}
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
for (Map.Entry<ResourceKey<Structure>, Structure> entry : registry.entrySet()) {
for (Holder<Biome> holder : entry.getValue().biomes()) {
@@ -0,0 +1,57 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisChunkGeneratorFailureContractTest {
@Test
public void structureLocateDoesNotCatchAndFallThroughToAnotherImplementation() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
int locateStart = source.indexOf("public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure");
int locateEnd = source.indexOf("private HolderSet<Structure> filterReachableStructures", locateStart);
String locate = source.substring(locateStart, locateEnd);
int reachabilityStart = source.indexOf("private Set<String> reachableStructureKeys");
int reachabilityEnd = source.indexOf("protected MapCodec", reachabilityStart);
String reachability = source.substring(reachabilityStart, reachabilityEnd);
assertTrue(locate.contains("reached its safety limit"));
assertTrue(locate.contains("unregistered structure holder"));
assertFalse(locate.contains("catch (Throwable"));
assertFalse(locate.contains("IrisLogging.reportError"));
assertFalse(reachability.contains("catch (Throwable"));
assertFalse(reachability.contains("reachable = Set.of()"));
}
@Test
public void structureGenerationAbortsInsteadOfLoggingAndContinuing() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
int adjustmentStart = source.indexOf("private void adjustGeneratedStructures");
int adjustmentEnd = source.indexOf("public ChunkGeneratorStructureState createState", adjustmentStart);
String adjustment = source.substring(adjustmentStart, adjustmentEnd);
int placementStart = source.indexOf("private void placeVanillaStructures");
int placementEnd = source.indexOf("private void placeVanillaStructure(", placementStart + 1);
String placement = source.substring(placementStart, placementEnd);
assertTrue(adjustment.contains("NativeStructureGenerationException.failure("));
assertTrue(adjustment.contains("\"vertical adjustment\""));
assertFalse(adjustment.contains("IrisLogging.reportError"));
assertTrue(placement.contains("\"resolution\""));
assertTrue(placement.contains("\"terrain integration\""));
assertTrue(placement.contains("\"vegetation cleanup\""));
assertTrue(placement.contains("\"placement\""));
assertTrue(placement.contains("because structure generation is disabled outside the pack"));
assertTrue(placement.contains("prepareSurfaceStructures"));
assertTrue(placement.contains("clearIntersectingVegetation"));
assertTrue(placement.indexOf("prepareSurfaceStructures")
< placement.indexOf("clearIntersectingVegetation"));
assertTrue(placement.indexOf("clearIntersectingVegetation")
< placement.indexOf("for (NativePlacementGroup group"));
assertFalse(placement.contains("IrisLogging.reportError"));
}
}
@@ -1,5 +1,10 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.nativegen.NativeStructureLocateResults;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.world.level.levelgen.structure.Structure;
import org.junit.Test;
import java.io.IOException;
@@ -8,35 +13,70 @@ import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
public class IrisChunkGeneratorMonumentLocateContractTest {
@Test
public void irisPlacementRunsBeforeExactNativeMonumentIsRemovedFromDelegateLookup() throws IOException {
public void nativeLocateAllowsExplicitReplacementBeforeNativeCapabilityGate() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
int findStart = source.indexOf("findNearestMapStructure(ServerLevel level");
assertTrue(findStart >= 0);
int filterStart = source.indexOf("private HolderSet<Structure> filterReachableStructures", findStart);
assertTrue(filterStart > findStart);
String findMethod = source.substring(findStart, filterStart);
int irisLocate = findMethod.indexOf("IrisStructureLocator.locate(");
int searchLimit = findMethod.indexOf("LocateStatus.SEARCH_LIMIT_REACHED", irisLocate);
int limitSkip = findMethod.indexOf("continue;", searchLimit);
int nativeFilter = findMethod.indexOf("filterReachableStructures(level, holders)");
int delegateLocate = findMethod.indexOf("delegate.findNearestMapStructure(level, reachable");
int irisHelperStart = source.indexOf("private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(", findStart);
int filterStart = source.indexOf("private HolderSet<Structure> filterReachableStructures", irisHelperStart);
assertTrue(irisHelperStart > findStart);
assertTrue(filterStart > irisHelperStart);
String outerMethod = source.substring(findStart, irisHelperStart);
String irisHelper = source.substring(irisHelperStart, filterStart);
int policyResolution = irisHelper.indexOf("NativeStructureGenerationPolicy.resolve(engine,");
int unexploredGuard = irisHelper.indexOf("if (findUnexplored)");
int registryLookup = irisHelper.indexOf("level.registryAccess().lookupOrThrow(Registries.STRUCTURE)");
int replacementCheck = irisHelper.indexOf(
"decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS");
int irisLocate = irisHelper.indexOf("IrisStructureLocator.locate(", replacementCheck);
int searchLimit = irisHelper.indexOf("LocateStatus.SEARCH_LIMIT_REACHED", irisLocate);
int limitSkip = irisHelper.indexOf("continue;", searchLimit);
int nativeFilter = outerMethod.indexOf("filterReachableStructures(level, holders)");
int delegateLocate = outerMethod.indexOf("delegate.findNearestMapStructure(level, reachable");
int nearestSelection = outerMethod.indexOf(
"NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated)");
int reachabilityStart = source.indexOf("private Set<String> reachableStructureKeys", filterStart);
assertTrue(reachabilityStart > filterStart);
String filterMethod = source.substring(filterStart, reachabilityStart);
int monumentReject = filterMethod.indexOf("if (NATIVE_MONUMENT_KEY.equals(key)");
int monumentReject = filterMethod.indexOf("if (NativeStructureLocateCapability.isPaperUnavailable(key)");
int rejectContinue = filterMethod.indexOf("continue;", monumentReject);
int emptyNativePartition = filterMethod.indexOf("if (candidates.isEmpty())", rejectContinue);
int reachabilityLookup = filterMethod.indexOf("reachableStructureKeys(level)", emptyNativePartition);
assertTrue(irisLocate >= 0);
assertTrue(policyResolution >= 0);
assertTrue(unexploredGuard >= 0);
assertTrue(registryLookup > unexploredGuard);
assertTrue(policyResolution > registryLookup);
assertTrue(replacementCheck > policyResolution);
assertTrue(irisLocate > replacementCheck);
assertTrue(searchLimit > irisLocate);
assertTrue(limitSkip > searchLimit);
assertTrue(nativeFilter > limitSkip);
assertTrue(nativeFilter >= 0);
assertTrue(delegateLocate > nativeFilter);
assertTrue(nearestSelection > delegateLocate);
assertTrue(monumentReject >= 0);
assertTrue(rejectContinue > monumentReject);
assertTrue(findMethod.contains("new BlockPos(result.originX(), result.baseY(), result.originZ())"));
assertTrue(emptyNativePartition > rejectContinue);
assertTrue(reachabilityLookup > emptyNativePartition);
assertFalse(irisHelper.contains("NativeStructureLocateCapability.isPaperUnavailable(structureId)"));
assertTrue(irisHelper.contains("new BlockPos(result.originX(), result.baseY(), result.originZ())"));
}
@Test
public void mixedLocateSelectsNearestProviderAndPrefersNativeOnTie() {
BlockPos origin = BlockPos.ZERO;
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
Pair<BlockPos, Holder<Structure>> nativeTie = Pair.of(new BlockPos(0, 70, 4), null);
assertSame(irisNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeFar));
assertSame(nativeNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeNear));
assertSame(nativeTie, NativeStructureLocateResults.nearest(origin, irisNear, nativeTie));
}
@Test
@@ -54,18 +94,34 @@ public class IrisChunkGeneratorMonumentLocateContractTest {
}
@Test
public void verticalShiftMovesPiecesJigsawJunctionsAndCachedBoundsTogether() throws IOException {
public void verticalPlacementMovesPiecesMonumentChildrenJigsawJunctionsAndCachedBoundsTogether() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")));
int method = source.indexOf("public static int applyVerticalShift");
int clamp = source.indexOf("StructureVerticalBounds.clampOffset", method);
int pieceMove = source.indexOf("piece.move(0, offsetY, 0)", clamp);
int junctionMove = source.indexOf("junction.getSourceGroundY() + offsetY", pieceMove);
int boundsMove = source.indexOf("bounds.move(0, offsetY, 0)", junctionMove);
int placementStart = source.indexOf("public static int applyVerticalPlacement");
int shiftStart = source.indexOf("public static int applyVerticalShift", placementStart);
int alignmentStart = source.indexOf("static int alignOceanMonumentToSeaLevel", shiftStart);
int moveStart = source.indexOf("private static void moveStructureStart", alignmentStart);
int movePieceStart = source.indexOf("private static void moveStructurePiece", moveStart);
int monumentChildrenStart = source.indexOf("static List<StructurePiece> monumentChildPieces", movePieceStart);
assertTrue(method >= 0);
assertTrue(clamp > method);
assertTrue(pieceMove > clamp);
assertTrue(junctionMove > pieceMove);
assertTrue(boundsMove > junctionMove);
assertTrue(placementStart >= 0);
assertTrue(shiftStart > placementStart);
assertTrue(alignmentStart > shiftStart);
assertTrue(moveStart > alignmentStart);
assertTrue(movePieceStart > moveStart);
assertTrue(monumentChildrenStart > movePieceStart);
String placementMethod = source.substring(placementStart, shiftStart);
String shiftMethod = source.substring(shiftStart, alignmentStart);
String moveMethod = source.substring(moveStart, movePieceStart);
String movePieceMethod = source.substring(movePieceStart, monumentChildrenStart);
assertTrue(placementMethod.contains("return alignOceanMonumentToSeaLevel("));
assertTrue(placementMethod.contains("return applyVerticalShift("));
assertTrue(shiftMethod.contains("StructureVerticalBounds.clampOffset"));
assertTrue(shiftMethod.contains("moveStructureStart(start, bounds, offsetY)"));
assertTrue(moveMethod.contains("moveStructurePiece(piece, offsetY)"));
assertTrue(moveMethod.contains("cachedBounds.move(0, offsetY, 0)"));
assertTrue(movePieceMethod.contains("piece.move(0, offsetY, 0)"));
assertTrue(movePieceMethod.contains("child.move(0, offsetY, 0)"));
assertTrue(movePieceMethod.contains("junction.getSourceGroundY() + offsetY"));
}
}
@@ -0,0 +1,29 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NMSBindingPlacementFailureContractTest {
@Test
public void structurePlacementPropagatesRuntimeFailuresWithContext() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java"));
int methodStart = source.indexOf("public int[] placeStructure(");
int methodEnd = source.indexOf("\n @Override", methodStart + 1);
String method = source.substring(methodStart, methodEnd);
int catchStart = method.indexOf("catch (RuntimeException e)");
assertTrue(catchStart >= 0);
String failurePath = method.substring(catchStart);
assertTrue(failurePath.contains("throw NativeStructureGenerationException.failure("));
assertTrue(failurePath.contains("\"capture placement\", structureKey, chunkX, chunkZ, e"));
assertFalse(failurePath.contains("return null;"));
assertFalse(failurePath.contains("catch (Throwable"));
}
}
@@ -0,0 +1,43 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NMSBindingStructureFailureContractTest {
@Test
public void structureRegistryHooksThrowInsteadOfReturningEmptyOnFailure() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java"));
List<String> methodNames = List.of(
"getStructureKeys",
"getStructureSetKeys",
"getReachableStructureKeys",
"getStructureBiomeKeys",
"getPossibleBiomeKeys");
for (String methodName : methodNames) {
String method = methodSource(source, methodName);
assertTrue(methodName + " must preserve failure context", method.contains("throw new IllegalStateException("));
assertTrue(methodName + " must retain its root cause", method.contains(", e);"));
assertFalse(methodName + " must not log and return an empty registry result",
method.contains("IrisLogging.reportError"));
assertFalse(methodName + " must not catch fatal JVM errors", method.contains("catch (Throwable"));
}
}
private static String methodSource(String source, String methodName) {
int start = source.indexOf("public KList<String> " + methodName + "(");
int end = source.indexOf("\n @Override", start + 1);
if (start < 0 || end < 0) {
throw new IllegalArgumentException("Unable to find NMS binding method " + methodName);
}
return source.substring(start, end);
}
}
@@ -0,0 +1,140 @@
package art.arcane.iris.nativegen;
import net.minecraft.SharedConstants;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderSet;
import net.minecraft.server.Bootstrap;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorMonumentTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void vanillaSeaLevelKeepsTheVanillaMonumentHeight() {
StructureStart start = monumentStart(1337L);
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
start, "minecraft:monument", 0, 63, -64, 320, false, (x, z) -> 0);
assertEquals(0, offset);
assertEquals(39, start.getBoundingBox().minY());
assertEquals(61, start.getBoundingBox().maxY());
}
@Test
public void shiftedSeaLevelMovesTheShellAndEveryRoomTogether() {
StructureStart start = monumentStart(1337L);
OceanMonumentPieces.MonumentBuilding building = monumentBuilding(start);
List<StructurePiece> children = NativeStructurePostProcessor.monumentChildPieces(building);
int[] childMinY = minimumYs(children);
BoundingBox cachedBounds = start.getBoundingBox();
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
start, "minecraft:monument", 0, 50, -256, 512, false, (x, z) -> 0);
assertEquals(-13, offset);
assertSame(cachedBounds, start.getBoundingBox());
assertEquals(26, building.getBoundingBox().minY());
assertEquals(48, building.getBoundingBox().maxY());
assertEquals(26, start.getBoundingBox().minY());
assertEquals(48, start.getBoundingBox().maxY());
assertTrue(children.size() > 20);
for (int i = 0; i < children.size(); i++) {
assertEquals(childMinY[i] - 13, children.get(i).getBoundingBox().minY());
}
int repeatedOffset = NativeStructurePostProcessor.applyVerticalPlacement(
start, "minecraft:monument", 0, 50, -256, 512, false, (x, z) -> 0);
assertEquals(0, repeatedOffset);
}
@Test
public void configuredOffsetIsRelativeToTheActualSeaLevel() {
StructureStart start = monumentStart(1337L);
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
start, "minecraft:monument", 3, 50, -256, 512, false, (x, z) -> 0);
assertEquals(-10, offset);
assertEquals(29, start.getBoundingBox().minY());
assertEquals(51, start.getBoundingBox().maxY());
}
@Test
public void vanillaReloadRegenerationIsRealignedBeforePlacement() {
long seed = 1337L;
ChunkPos chunkPos = new ChunkPos(0, 0);
StructureStart initial = monumentStart(seed);
NativeStructurePostProcessor.applyVerticalPlacement(
initial, "minecraft:monument", 0, 50, -256, 512, false, (x, z) -> 0);
PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad(
chunkPos, seed, new PiecesContainer(initial.getPieces()));
StructureStart reloaded = new StructureStart(
monumentStructure(), chunkPos, 0, regenerated);
assertEquals(39, reloaded.getBoundingBox().minY());
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
reloaded, "minecraft:monument", 0, 50, -256, 512, false, (x, z) -> 0);
assertEquals(-13, offset);
assertEquals(26, reloaded.getBoundingBox().minY());
assertEquals(48, reloaded.getBoundingBox().maxY());
}
@Test
public void impossibleSeaLevelAlignmentFailsInsteadOfClippingTheMonument() {
StructureStart start = monumentStart(1337L);
try {
NativeStructurePostProcessor.applyVerticalPlacement(
start, "minecraft:monument", 0, -50, -64, 320, false, (x, z) -> 0);
} catch (IllegalStateException error) {
assertTrue(error.getMessage().contains("cannot align"));
return;
}
throw new AssertionError("Expected an out-of-bounds monument alignment to fail");
}
private static StructureStart monumentStart(long seed) {
ChunkPos chunkPos = new ChunkPos(0, 0);
OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding(
RandomSource.create(seed), -29, -29, Direction.NORTH);
return new StructureStart(
monumentStructure(), chunkPos, 0, new PiecesContainer(List.of(building)));
}
private static OceanMonumentStructure monumentStructure() {
return new OceanMonumentStructure(new Structure.StructureSettings(HolderSet.empty()));
}
private static OceanMonumentPieces.MonumentBuilding monumentBuilding(StructureStart start) {
return (OceanMonumentPieces.MonumentBuilding) start.getPieces().get(0);
}
private static int[] minimumYs(List<StructurePiece> pieces) {
int[] ys = new int[pieces.size()];
for (int i = 0; i < pieces.size(); i++) {
ys[i] = pieces.get(i).getBoundingBox().minY();
}
return ys;
}
}
@@ -0,0 +1,327 @@
package art.arcane.iris.nativegen;
import com.mojang.datafixers.util.Either;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorSurfaceTerrainTest {
@BeforeClass
public static void bootstrapMinecraft() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void onlySurfaceBeardThinStructuresPrepareTerrain() {
assertTrue(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
TerrainAdjustment.BEARD_THIN, GenerationStep.Decoration.SURFACE_STRUCTURES));
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
TerrainAdjustment.BURY, GenerationStep.Decoration.SURFACE_STRUCTURES));
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
TerrainAdjustment.BEARD_BOX, GenerationStep.Decoration.SURFACE_STRUCTURES));
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
TerrainAdjustment.ENCAPSULATE, GenerationStep.Decoration.SURFACE_STRUCTURES));
}
@Test
public void undergroundAdjustmentsNeverPrepareSurfaceTerrain() {
for (TerrainAdjustment adjustment : List.of(
TerrainAdjustment.BEARD_THIN,
TerrainAdjustment.BURY,
TerrainAdjustment.BEARD_BOX,
TerrainAdjustment.ENCAPSULATE)) {
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
adjustment, GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
}
}
@Test
public void surfaceAnchorIsFlushInsideAndUnchangedAtRadius() {
NativeStructurePostProcessor.SurfaceAnchor anchor = anchor(80, 2);
assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(anchor), 2, 2, 64));
assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(anchor), 16, 2, 64));
assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(anchor), 17, 2, 64));
}
@Test
public void surfaceAnchorRaisesAndLowersThroughTheTaper() {
NativeStructurePostProcessor.SurfaceAnchor raised = anchor(80, 2);
NativeStructurePostProcessor.SurfaceAnchor lowered = anchor(64, 2);
assertEquals(68, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(raised), 10, 2, 64));
assertEquals(76, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(lowered), 10, 2, 80));
}
@Test
public void containingRigidFloorsHaveDeterministicPriority() {
NativeStructurePostProcessor.SurfaceAnchor rigid = anchor(70, 2);
NativeStructurePostProcessor.SurfaceAnchor junction = anchor(90, 1);
assertEquals(70, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(rigid, junction), 2, 2, 64));
assertEquals(70, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(junction, rigid), 2, 2, 64));
NativeStructurePostProcessor.SurfaceAnchor weakTie = anchor(48, 1);
NativeStructurePostProcessor.SurfaceAnchor strongTie = anchor(80, 2);
assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(weakTie, strongTie), 2, 2, 64));
assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(strongTie, weakTie), 2, 2, 64));
}
@Test
public void containingFootprintOverridesAnAdjacentPiecesFalloff() {
NativeStructurePostProcessor.SurfaceAnchor local =
new NativeStructurePostProcessor.SurfaceAnchor(0, 4, 0, 4, 65, 2);
NativeStructurePostProcessor.SurfaceAnchor adjacent =
new NativeStructurePostProcessor.SurfaceAnchor(5, 9, 0, 4, 80, 2);
assertEquals(77, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(adjacent), 4, 2, 64));
assertEquals(65, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(local, adjacent), 4, 2, 64));
assertEquals(65, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(adjacent, local), 4, 2, 64));
}
@Test
public void opposingFalloffsBlendWithoutAnAbruptMidpointSeam() {
NativeStructurePostProcessor.SurfaceAnchor high =
new NativeStructurePostProcessor.SurfaceAnchor(0, 0, 0, 0, 80, 2);
NativeStructurePostProcessor.SurfaceAnchor low =
new NativeStructurePostProcessor.SurfaceAnchor(12, 12, 0, 0, 48, 2);
int previous = NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(high, low), 0, 0, 64);
for (int x = 1; x <= 12; x++) {
int forward = NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(high, low), x, 0, 64);
int reversed = NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(low, high), x, 0, 64);
assertEquals(forward, reversed);
assertTrue(Math.abs(forward - previous) <= 4);
previous = forward;
}
assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget(
List.of(high, low), 6, 0, 64));
}
@Test
public void surfaceColumnsMutateTerrainAndRemoveUnsupportedDecoration() {
Map<BlockPos, BlockState> lowered = new HashMap<>();
put(lowered, 0, 61, 0, Blocks.STONE.defaultBlockState());
put(lowered, 0, 62, 0, Blocks.DIRT.defaultBlockState());
put(lowered, 0, 63, 0, Blocks.DIRT.defaultBlockState());
put(lowered, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState());
put(lowered, 0, 65, 0, Blocks.DANDELION.defaultBlockState());
NativeStructurePostProcessor.applySurfaceColumn(
world(lowered), new BlockPos.MutableBlockPos(),
0, 0, 64, 62, -64, 319);
assertEquals(Blocks.STONE.defaultBlockState(), state(lowered, 0, 61, 0));
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(lowered, 0, 62, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(lowered, 0, 63, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(lowered, 0, 64, 0));
assertEquals(Blocks.AIR.defaultBlockState(), state(lowered, 0, 65, 0));
Map<BlockPos, BlockState> raised = new HashMap<>();
put(raised, 0, 63, 0, Blocks.DIRT.defaultBlockState());
put(raised, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState());
put(raised, 0, 65, 0, Blocks.DANDELION.defaultBlockState());
NativeStructurePostProcessor.applySurfaceColumn(
world(raised), new BlockPos.MutableBlockPos(),
0, 0, 64, 68, -64, 319);
assertEquals(Blocks.DIRT.defaultBlockState(), state(raised, 0, 65, 0));
assertEquals(Blocks.DIRT.defaultBlockState(), state(raised, 0, 66, 0));
assertEquals(Blocks.DIRT.defaultBlockState(), state(raised, 0, 67, 0));
assertEquals(Blocks.GRASS_BLOCK.defaultBlockState(), state(raised, 0, 68, 0));
}
@Test
public void loweredFluidColumnsRemainFluidFilled() {
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, 0, 62, 0, Blocks.DIRT.defaultBlockState());
put(blocks, 0, 63, 0, Blocks.DIRT.defaultBlockState());
put(blocks, 0, 64, 0, Blocks.GRAVEL.defaultBlockState());
put(blocks, 0, 65, 0, Blocks.WATER.defaultBlockState());
NativeStructurePostProcessor.applySurfaceColumn(
world(blocks), new BlockPos.MutableBlockPos(),
0, 0, 64, 62, -64, 319);
assertEquals(Blocks.WATER.defaultBlockState(), state(blocks, 0, 63, 0));
assertEquals(Blocks.WATER.defaultBlockState(), state(blocks, 0, 64, 0));
assertEquals(Blocks.WATER.defaultBlockState(), state(blocks, 0, 65, 0));
assertEquals(Blocks.GRAVEL.defaultBlockState(), state(blocks, 0, 62, 0));
}
@Test
public void singlePoolTemplateFieldMatchesTheRuntimeContract() {
Field field = NativeStructurePostProcessor.resolveSinglePoolTemplateField();
assertEquals(SinglePoolElement.class, field.getDeclaringClass());
assertEquals(Either.class, field.getType());
assertTrue(Modifier.isProtected(field.getModifiers()));
assertTrue(Modifier.isFinal(field.getModifiers()));
assertFalse(Modifier.isStatic(field.getModifiers()));
assertTrue(field.trySetAccessible());
}
@Test
public void runtimeTemplatesAndLegacyAirUseTheExactContract() {
StructureTemplate runtimeTemplate = new StructureTemplate();
assertEquals(runtimeTemplate, NativeStructurePostProcessor.resolveTemplateReference(
Either.right(runtimeTemplate), null));
assertFalse(NativeStructurePostProcessor.shouldClearLegacyAir(79, 80, false));
assertTrue(NativeStructurePostProcessor.shouldClearLegacyAir(80, 80, false));
assertTrue(NativeStructurePostProcessor.shouldClearLegacyAir(96, 80, false));
assertFalse(NativeStructurePostProcessor.shouldClearLegacyAir(96, 80, true));
}
@Test
public void rotatedTemplateAirClearsOnlyInsideTheChunkAndAboveTheFloor() throws Exception {
StructureTemplate.StructureBlockInfo clear = new StructureTemplate.StructureBlockInfo(
new BlockPos(1, 0, 0), Blocks.AIR.defaultBlockState(), null);
StructureTemplate.StructureBlockInfo belowFloor = new StructureTemplate.StructureBlockInfo(
new BlockPos(0, -1, 0), Blocks.AIR.defaultBlockState(), null);
StructureTemplate.StructureBlockInfo outsideChunk = new StructureTemplate.StructureBlockInfo(
new BlockPos(3, 0, 0), Blocks.AIR.defaultBlockState(), null);
StructureTemplate template = template(List.of(clear, belowFloor, outsideChunk));
BlockPos origin = new BlockPos(10, 80, 10);
StructurePlaceSettings settings = new StructurePlaceSettings().setRotation(Rotation.CLOCKWISE_90);
BlockPos clearPosition = origin.offset(StructureTemplate.calculateRelativePosition(settings, clear.pos()));
BlockPos belowFloorPosition = origin.offset(
StructureTemplate.calculateRelativePosition(settings, belowFloor.pos()));
BlockPos outsidePosition = origin.offset(
StructureTemplate.calculateRelativePosition(settings, outsideChunk.pos()));
BoundingBox area = new BoundingBox(
Math.min(clearPosition.getX(), belowFloorPosition.getX()),
Math.min(clearPosition.getY(), belowFloorPosition.getY()),
Math.min(clearPosition.getZ(), belowFloorPosition.getZ()),
Math.max(clearPosition.getX(), belowFloorPosition.getX()),
Math.max(clearPosition.getY(), belowFloorPosition.getY()),
Math.max(clearPosition.getZ(), belowFloorPosition.getZ()));
settings.setBoundingBox(area);
assertFalse(area.isInside(outsidePosition));
Map<BlockPos, BlockState> blocks = new HashMap<>();
blocks.put(clearPosition, Blocks.DIRT.defaultBlockState());
blocks.put(belowFloorPosition, Blocks.DIRT.defaultBlockState());
blocks.put(outsidePosition, Blocks.DIRT.defaultBlockState());
NativeStructurePostProcessor.clearTemplateAir(
world(blocks), template, origin, 80, settings);
assertEquals(Blocks.AIR.defaultBlockState(), blocks.get(clearPosition));
assertEquals(Blocks.DIRT.defaultBlockState(), blocks.get(belowFloorPosition));
assertEquals(Blocks.DIRT.defaultBlockState(), blocks.get(outsidePosition));
}
@Test
public void unrelatedPieceBoundsAreRejectedBeforeTemplateScanning() {
BoundingBox area = new BoundingBox(0, -64, 0, 15, 319, 15);
assertTrue(NativeStructurePostProcessor.intersects(
new BoundingBox(15, 60, 15, 30, 90, 30), area));
assertFalse(NativeStructurePostProcessor.intersects(
new BoundingBox(16, 60, 16, 30, 90, 30), area));
assertFalse(NativeStructurePostProcessor.intersects(
new BoundingBox(0, 320, 0, 15, 350, 15), area));
}
@Test
public void vegetationCleanupUsesTheSameCircularTaperAsTerrain() {
BoundingBox piece = new BoundingBox(0, 60, 0, 4, 80, 4);
assertTrue(NativeStructurePostProcessor.withinSurfaceTerrainRadius(16, 2, piece, 12));
assertFalse(NativeStructurePostProcessor.withinSurfaceTerrainRadius(16, 16, piece, 12));
}
private static NativeStructurePostProcessor.SurfaceAnchor anchor(int meetY, int strength) {
return new NativeStructurePostProcessor.SurfaceAnchor(0, 4, 0, 4, meetY, strength);
}
private static StructureTemplate template(
List<StructureTemplate.StructureBlockInfo> blocks) throws Exception {
Constructor<StructureTemplate.Palette> constructor =
StructureTemplate.Palette.class.getDeclaredConstructor(List.class);
assertTrue(constructor.trySetAccessible());
StructureTemplate.Palette palette = constructor.newInstance(blocks);
StructureTemplate template = new StructureTemplate();
template.palettes.add(palette);
return template;
}
private static WorldGenLevel world(Map<BlockPos, BlockState> blocks) {
InvocationHandler handler = (proxy, method, arguments) -> {
String methodName = method.getName();
if (methodName.equals("getBlockState")) {
BlockPos position = (BlockPos) arguments[0];
return state(blocks, position.getX(), position.getY(), position.getZ());
}
if (methodName.equals("setBlock")) {
BlockPos position = (BlockPos) arguments[0];
BlockState blockState = (BlockState) arguments[1];
put(blocks, position.getX(), position.getY(), position.getZ(), blockState);
return true;
}
if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (methodName.equals("equals")) {
return proxy == arguments[0];
}
if (methodName.equals("toString")) {
return "surface-test-world";
}
throw new UnsupportedOperationException(method.toString());
};
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
}
private static void put(Map<BlockPos, BlockState> blocks,
int x, int y, int z, BlockState state) {
blocks.put(new BlockPos(x, y, z), state);
}
private static BlockState state(Map<BlockPos, BlockState> blocks, int x, int y, int z) {
return blocks.getOrDefault(new BlockPos(x, y, z), Blocks.AIR.defaultBlockState());
}
}
@@ -1,8 +1,10 @@
package art.arcane.iris.nativegen;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -24,6 +26,22 @@ public class NativeStructurePostProcessorVegetationTest {
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, true));
}
@Test
public void surfaceStructuresClearTheirEntireFootprintByDefault() {
assertTrue(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.SURFACE_STRUCTURES, false));
assertTrue(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.SURFACE_STRUCTURES, true));
}
@Test
public void undergroundStructuresPreserveSurfaceVegetationUnlessConfigured() {
assertFalse(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES, false));
assertTrue(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES, true));
}
@Test
public void allUndergroundGenerationStepsShareOneClassification() {
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
@@ -35,4 +53,25 @@ public class NativeStructurePostProcessorVegetationTest {
assertFalse(NativeStructurePostProcessor.isUndergroundStep(
GenerationStep.Decoration.SURFACE_STRUCTURES));
}
@Test
public void undergroundStructuresUseTheLowestTerrainColumn() {
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
int offset = NativeStructurePostProcessor.resolveBuriedOffset(
bounds, 0, -64, 320, (x, z) -> x == 1 ? 76 : 100);
assertEquals(-5, offset);
}
@Test
public void undergroundStructuresFailWhenBurialWouldCrossWorldFloor() {
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
try {
NativeStructurePostProcessor.resolveBuriedOffset(
bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100);
} catch (IllegalStateException e) {
assertTrue(e.getMessage().contains("world minimum"));
return;
}
throw new AssertionError("Expected underground burial to fail below the world floor");
}
}
@@ -21,12 +21,16 @@ package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.service.ObjectStudioSaveService;
import art.arcane.iris.core.structure.NativeStructureLocateCapability;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
@@ -117,13 +121,34 @@ public class CommandFind implements DirectorExecutor {
return;
}
if (IrisStructureLocator.isPlaced(e, structure)) {
locateIrisStructure(e, structure, commandSender);
String structureKey = structure == null ? "" : structure.trim();
Structure nativeStructure = resolveNativeStructure(structureKey);
if (nativeStructure != null) {
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
e, structureKey, false);
if (!decision.generate()
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
commandSender.sendMessage(C.RED + NativeStructureGenerationPolicy.generationStatusMessage(
structureKey, decision.status()));
return;
}
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
locateIrisStructure(e, structureKey, commandSender);
return;
}
if (NativeStructureLocateCapability.isPaperUnavailable(structureKey)) {
commandSender.sendMessage(C.RED + NativeStructureLocateCapability.unavailableMessage());
return;
}
}
if (nativeStructure == null && IrisStructureLocator.isPlaced(e, structureKey)) {
locateIrisStructure(e, structureKey, commandSender);
return;
}
if (BukkitNativeStructureLocatePolicy.isUnavailable(structure)) {
commandSender.sendMessage(C.RED + BukkitNativeStructureLocatePolicy.unavailableMessage());
if (nativeStructure == null) {
commandSender.sendMessage(C.RED + "Unknown structure: " + structureKey);
return;
}
@@ -135,42 +160,44 @@ public class CommandFind implements DirectorExecutor {
World targetWorld = target.getWorld();
Location origin = target.getLocation();
commandSender.sendMessage(C.GRAY + "Locating " + structure + "...");
commandSender.sendMessage(C.GRAY + "Locating " + structureKey + "...");
J.s(() -> {
try {
Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
Structure match = null;
for (Structure candidate : structureRegistry) {
NamespacedKey key = structureRegistry.getKey(candidate);
if (key != null && key.toString().equalsIgnoreCase(structure)) {
match = candidate;
break;
}
}
if (match == null) {
sendStructureMessage(target, commandSender, C.RED + "Unknown structure: " + structure);
return;
}
if (!StructureReachability.isReachable(e, structure)) {
KList<String> miss = StructureReachability.missingBiomeKeys(e, structure);
if (!StructureReachability.isReachable(e, structureKey)) {
KList<String> miss = StructureReachability.missingBiomeKeys(e, structureKey);
sendStructureMessage(target, commandSender,
C.YELLOW + structure + " cannot generate in this world (its required biomes are not produced by this pack"
C.YELLOW + structureKey + " cannot generate in this world (its required biomes are not produced by this pack"
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
return;
}
StructureSearchResult result = targetWorld.locateNearestStructure(origin, match, 100, false);
StructureSearchResult result = targetWorld.locateNearestStructure(
origin, nativeStructure, 100, false);
if (result == null || result.getLocation() == null) {
sendStructureMessage(target, commandSender, C.YELLOW + "No " + structure + " found within range of you.");
sendStructureMessage(target, commandSender,
C.YELLOW + "No " + structureKey + " found within range of you.");
return;
}
prepareStructureTeleport(target, targetWorld, commandSender, structure, result.getLocation(), false);
prepareStructureTeleport(
target, targetWorld, commandSender, structureKey, result.getLocation(), false);
} catch (Throwable t) {
sendStructureMessage(target, commandSender, C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
Iris.reportError("Could not locate structure '" + structure + "'.", t);
sendStructureMessage(target, commandSender,
C.RED + "Could not locate " + structureKey + ": " + t.getClass().getSimpleName());
Iris.reportError("Could not locate structure '" + structureKey + "'.", t);
}
});
}
private static Structure resolveNativeStructure(String structureKey) {
Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
for (Structure candidate : structureRegistry) {
NamespacedKey key = structureRegistry.getKey(candidate);
if (key != null && key.toString().equalsIgnoreCase(structureKey)) {
return candidate;
}
}
return null;
}
private void locateIrisStructure(Engine engine, String structure, VolmitSender commandSender) {
Player target = player();
if (target == null) {
@@ -22,15 +22,13 @@ import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.link.WorldEditLink;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.runtime.ObjectStudioActivation;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.service.ObjectSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.service.WandSVC;
import art.arcane.iris.core.tools.IrisConverter;
import art.arcane.iris.core.tools.PlausibilizeMode;
import art.arcane.iris.core.tools.TreePlausibilizer;
import art.arcane.iris.core.tools.TreePlausibilizeBatch;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisDimension;
@@ -353,199 +351,63 @@ public class CommandObject implements DirectorExecutor {
}
}
@Director(description = "Make tree leaves vanilla-decay-plausible (every leaf within 6 blocks of a log)",
@Director(description = "Grow organic branches through the canopy so every leaf survives vanilla decay",
origin = DirectorOrigin.BOTH)
public void plausibilize(
@Param(description = "Object key, prefix (trees/), or filesystem path",
customHandler = ObjectTargetHandler.class)
String target,
@Param(description = "DEFAULT: tentacle logs, delete orphans. NORMALIZE: + flip persistent=false. FOLIAGE_OVERATURE: add leaves to bridge orphans, no deletions. SMOKE: wipe & repaint canopy shell.",
defaultValue = "DEFAULT")
PlausibilizeMode mode,
@Param(description = "Analyze only, do not write", defaultValue = "false")
@Param(name = "dryrun", description = "dryrun=true analyzes only, writes nothing", defaultValue = "false")
boolean dryRun,
@Param(description = "Canopy shell radius (SMOKE only), clamped [0,5]", defaultValue = "2")
int radius
@Param(name = "reach", description = "reach=N max branch length in blocks from existing wood; farther leaf clusters are pinned persistent instead. reach=0 grows unlimited", defaultValue = "12")
int reach
) {
List<Target> targets = resolveTargets(target);
IrisData nearest = data();
List<TreePlausibilizeBatch.Target> targets = TreePlausibilizeBatch.resolve(target, nearest);
if (targets.isEmpty() && nearest == null) {
targets = resolveFromPacks(target);
}
if (targets.isEmpty()) {
sender().sendMessage(C.RED + "No objects matched: " + target);
return;
}
sender().sendMessage(C.IRIS + "Plausibilize [" + mode.name()
+ (dryRun ? " DRY" : "")
+ (mode == PlausibilizeMode.SMOKE ? " r=" + radius : "")
sender().sendMessage(C.IRIS + "Plausibilize [reach=" + reach + (dryRun ? ", DRY" : "")
+ "] queued " + targets.size() + " object(s)");
org.bukkit.command.CommandSender s = sender();
J.a(() -> runPlausibilize(targets, dryRun, mode, radius, s));
List<TreePlausibilizeBatch.Target> queued = targets;
J.a(() -> TreePlausibilizeBatch.run(queued, dryRun, reach, nearest, (String line) ->
s.sendMessage(line.startsWith("Done:") || line.startsWith("Totals:") || line.startsWith("[")
? C.IRIS + line
: C.GRAY + " " + line)));
}
private List<Target> resolveTargets(String target) {
List<Target> out = new ArrayList<>();
if (target == null || target.isEmpty()) {
return out;
}
File direct = new File(target);
if (direct.isFile() && target.toLowerCase().endsWith(".iob")) {
out.add(new Target(direct.getName().replaceAll("\\.iob$", ""), direct));
return out;
}
if (direct.isDirectory()) {
walkIob(direct, direct, out);
return out;
}
IrisData irisData = data();
if (irisData != null) {
ResourceLoader<IrisObject> loader = irisData.getObjectLoader();
if (!target.endsWith("/") && loader.findFile(target) != null) {
out.add(new Target(target, null));
return out;
}
String prefix = target.endsWith("/") ? target : target + "/";
for (String k : loader.getPossibleKeys()) {
if (k.startsWith(prefix)) {
out.add(new Target(k, null));
}
}
return out;
}
private static List<TreePlausibilizeBatch.Target> resolveFromPacks(String target) {
List<TreePlausibilizeBatch.Target> out = new ArrayList<>();
File packsFolder = Iris.instance.getDataFolder("packs");
File[] packs = packsFolder.listFiles(File::isDirectory);
if (packs != null) {
for (File pack : packs) {
File objectsRoot = new File(pack, "objects");
if (!objectsRoot.isDirectory()) continue;
File candidate = new File(objectsRoot, target + ".iob");
if (candidate.isFile()) {
out.add(new Target(pack.getName() + "/" + target, candidate));
continue;
}
File candidateDir = new File(objectsRoot, target);
if (candidateDir.isDirectory()) {
walkIob(candidateDir, objectsRoot, out);
}
if (packs == null) {
return out;
}
for (File pack : packs) {
File objectsRoot = new File(pack, "objects");
if (!objectsRoot.isDirectory()) {
continue;
}
File candidate = new File(objectsRoot, target + ".iob");
if (candidate.isFile()) {
out.add(new TreePlausibilizeBatch.Target(pack.getName() + "/" + target, candidate));
continue;
}
File candidateDir = new File(objectsRoot, target);
if (candidateDir.isDirectory()) {
TreePlausibilizeBatch.walkIob(candidateDir, objectsRoot, out);
}
}
return out;
}
private static void walkIob(File root, File keyRoot, List<Target> out) {
File[] kids = root.listFiles();
if (kids == null) return;
for (File f : kids) {
if (f.isDirectory()) {
walkIob(f, keyRoot, out);
} else if (f.getName().toLowerCase().endsWith(".iob")) {
String rel = keyRoot.toPath().relativize(f.toPath()).toString()
.replace(File.separatorChar, '/')
.replaceAll("\\.iob$", "");
out.add(new Target(rel, f));
}
}
}
private record Target(String key, File file) {
}
private static void runPlausibilize(
List<Target> targets,
boolean dryRun,
PlausibilizeMode mode,
int radius,
org.bukkit.command.CommandSender s
) {
int processed = 0;
int skipped = 0;
int failed = 0;
int changed = 0;
long totalLogsAdded = 0L;
long totalLeavesAdded = 0L;
long totalLeavesRemoved = 0L;
long totalNormalized = 0L;
long totalUnreachableAfter = 0L;
int progressStep = Math.max(1, targets.size() / 20);
int index = 0;
for (Target t : targets) {
index++;
try {
IrisObject o = loadTarget(t);
if (o == null) {
s.sendMessage(C.YELLOW + " skip " + t.key() + ": failed to load");
skipped++;
continue;
}
TreePlausibilizer.Result r = dryRun
? TreePlausibilizer.analyze(o, mode, radius)
: TreePlausibilizer.apply(o, mode, radius);
if (r.skipReason() != null) {
s.sendMessage(C.YELLOW + " skip " + t.key() + ": " + r.skipReason());
skipped++;
continue;
}
boolean touched = r.logsAdded() > 0 || r.leavesAdded() > 0
|| r.leavesRemoved() > 0 || r.leavesNormalized() > 0;
if (!dryRun && touched) {
File dest = o.getLoadFile() != null ? o.getLoadFile() : t.file();
if (dest != null) {
o.write(dest);
changed++;
}
}
processed++;
totalLogsAdded += r.logsAdded();
totalLeavesAdded += r.leavesAdded();
totalLeavesRemoved += r.leavesRemoved();
totalNormalized += r.leavesNormalized();
totalUnreachableAfter += r.unreachableAfter();
if (touched || targets.size() == 1) {
s.sendMessage(C.GRAY + " " + t.key()
+ C.WHITE + " +" + r.logsAdded() + " logs"
+ C.WHITE + " +" + r.leavesAdded() + " leaves"
+ C.WHITE + " -" + r.leavesRemoved() + " removed"
+ (r.leavesNormalized() > 0 ? C.WHITE + " ~" + r.leavesNormalized() + " normalized" : "")
+ (r.unreachableAfter() > 0 ? C.YELLOW + " " + r.unreachableAfter() + " unreachable" : ""));
}
if (targets.size() > 1 && index % progressStep == 0) {
s.sendMessage(C.IRIS + " [" + index + "/" + targets.size() + "]");
}
} catch (Throwable e) {
s.sendMessage(C.RED + " fail " + t.key() + ": " + e.getClass().getSimpleName()
+ ": " + e.getMessage());
e.printStackTrace();
failed++;
}
}
s.sendMessage(C.IRIS + "Done: " + processed + " processed, " + changed + " changed, "
+ skipped + " skipped, " + failed + " failed");
s.sendMessage(C.IRIS + "Totals: +" + totalLogsAdded + " logs, +" + totalLeavesAdded + " leaves, -"
+ totalLeavesRemoved + " removed, ~" + totalNormalized + " normalized, "
+ totalUnreachableAfter + " unreachable");
}
private static IrisObject loadTarget(Target t) throws IOException {
if (t.file() != null) {
IrisObject o = new IrisObject();
o.read(t.file());
o.setLoadFile(t.file());
return o;
}
return IrisData.loadAnyObject(t.key(), null);
}
@Director(description = "Convert .schem files in the 'convert' folder to .iob files.")
public void convert () {
try {
@@ -21,18 +21,23 @@ package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.structure.BulkStructureImporter;
import art.arcane.iris.core.structure.NativeStructureLocateCapability;
import art.arcane.iris.core.structure.StructureCaptureImporter;
import art.arcane.iris.core.structure.StructureImporter;
import art.arcane.iris.core.structure.StructureIndexService;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.PlacedStructurePiece;
import art.arcane.iris.engine.framework.StructureAssembler;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.director.DirectorExecutor;
@@ -158,14 +163,23 @@ public class CommandStructure implements DirectorExecutor {
private void runVerification(Engine engine, KList<String> structureKeys, int centerX, int centerZ,
int searchRadius, VolmitSender commandSender, Player target) {
KList<String> messages = new KList<>();
Set<String> reachable;
try {
reachable = StructureReachability.reachableKeys(engine);
} catch (Throwable error) {
messages.add(C.RED + "Structure verification could not resolve native biome reachability.");
sendVerificationMessages(commandSender, target, messages);
Iris.reportError("Could not resolve native structure biome reachability for verification.", error);
return;
Map<String, IrisNativeStructureDecision> decisions = new HashMap<>(structureKeys.size());
boolean requiresNativeReachability = false;
for (String keyName : structureKeys) {
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, keyName, false);
decisions.put(keyName, decision);
requiresNativeReachability |= decision.generate();
}
Set<String> reachable = Set.of();
if (requiresNativeReachability) {
try {
reachable = StructureReachability.reachableKeys(engine);
} catch (Throwable error) {
messages.add(C.RED + "Structure verification could not resolve native biome reachability.");
sendVerificationMessages(commandSender, target, messages);
Iris.reportError("Could not resolve native structure biome reachability for verification.", error);
return;
}
}
int located = 0;
int nativeEligible = 0;
@@ -175,8 +189,8 @@ public class CommandStructure implements DirectorExecutor {
int searchLimited = 0;
int errors = 0;
for (String keyName : structureKeys) {
boolean irisPlaced = IrisStructureLocator.isPlaced(engine, keyName);
if (irisPlaced) {
IrisNativeStructureDecision decision = decisions.get(keyName);
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
try {
IrisStructureLocator.LocateResult result =
IrisStructureLocator.locate(engine, keyName, centerX, centerZ, searchRadius);
@@ -201,7 +215,7 @@ public class CommandStructure implements DirectorExecutor {
}
continue;
}
if (!engine.getDimension().getImportedStructures().shouldGenerate(keyName)) {
if (!decision.generate()) {
disabled++;
messages.add(C.GRAY + "[disabled] " + C.WHITE + keyName);
continue;
@@ -213,20 +227,20 @@ public class CommandStructure implements DirectorExecutor {
+ (missing.isEmpty() ? "" : C.YELLOW + " needs " + String.join("/", missing)));
continue;
}
if (BukkitNativeStructureLocatePolicy.isUnavailable(keyName)) {
nativeEligible++;
if (NativeStructureLocateCapability.isPaperUnavailable(keyName)) {
unavailable++;
messages.add(C.RED + "[unavailable] " + C.WHITE + keyName + C.RED + ": "
+ BukkitNativeStructureLocatePolicy.unavailableMessage());
messages.add(C.YELLOW + "[native-eligible/locate-unavailable] " + C.WHITE + keyName + C.YELLOW + ": "
+ NativeStructureLocateCapability.unavailableMessage());
continue;
}
nativeEligible++;
messages.add(C.GREEN + "[native-eligible] " + C.WHITE + keyName);
}
messages.add(C.GREEN + "Structure verify: " + C.WHITE + located + C.GREEN + " Iris placements located, "
+ C.WHITE + nativeEligible + C.GREEN + " native structures eligible, "
+ C.WHITE + disabled + C.GREEN + " disabled by policy, "
+ C.WHITE + unreachable + C.GREEN + " biome-unreachable, "
+ C.WHITE + unavailable + C.GREEN + " unavailable on this platform, "
+ C.WHITE + unavailable + C.GREEN + " generation-eligible but unavailable to synchronous locate, "
+ C.WHITE + searchLimited + C.GREEN + " density searches safety-limited, "
+ C.WHITE + errors + C.GREEN + " errors. Native eligibility is checked without running blocking live locates.");
sendVerificationMessages(commandSender, target, messages);
@@ -285,12 +299,13 @@ public class CommandStructure implements DirectorExecutor {
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
return;
}
IrisStructure s = IrisData.loadAnyStructure(structure, data);
IrisStructure s = data.load(IrisStructure.class, structure, false);
if (s == null) {
sender().sendMessage(C.RED + "No iris structure '" + structure + "' in this pack");
return;
}
StructureAssembler assembler = new StructureAssembler(data, s, 0, 64, 0);
StructureAssembler assembler = StructureAssembler.forData(
data, s, new IrisPosition(0, 64, 0));
KList<PlacedStructurePiece> pieces = assembler.assemble(new RNG(1234));
if (pieces == null || pieces.isEmpty()) {
sender().sendMessage(C.RED + "Structure '" + structure + "' assembled 0 pieces (check startPool '" + s.getStartPool() + "')");
@@ -321,13 +336,14 @@ public class CommandStructure implements DirectorExecutor {
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
return;
}
IrisStructure s = IrisData.loadAnyStructure(structure, data);
IrisStructure s = data.load(IrisStructure.class, structure, false);
if (s == null) {
sender().sendMessage(C.RED + "No iris structure '" + structure + "' in this pack");
return;
}
Location loc = player().getLocation();
StructureAssembler assembler = new StructureAssembler(data, s, loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
StructureAssembler assembler = StructureAssembler.forData(
data, s, new IrisPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()));
RNG rng = new RNG((long) loc.getBlockX() * 341873128712L + loc.getBlockZ());
KList<PlacedStructurePiece> pieces = assembler.assemble(rng);
if (pieces == null || pieces.isEmpty()) {
@@ -26,6 +26,7 @@ import art.arcane.iris.core.gui.NoiseExplorerGUI;
import art.arcane.iris.core.gui.VisionGUI;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.service.BoardSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.structure.BulkStructureImporter;
import art.arcane.iris.core.structure.FeatureImporter;
@@ -193,6 +194,25 @@ public class CommandStudio implements DirectorExecutor {
}));
}
@Director(description = "Toggle your Studio debug scoreboard", aliases = {"board", "sidebar", "sb"}, origin = DirectorOrigin.PLAYER)
public void scoreboard() {
Player target = player();
VolmitSender commandSender = sender();
if (!J.runEntity(target, () -> {
PlatformChunkGenerator generator = IrisToolbelt.access(target.getWorld());
if (generator == null || !generator.isStudio()) {
commandSender.sendMessage(C.RED + "You must be in a Studio world to toggle the debug scoreboard.");
return;
}
boolean visible = Iris.service(BoardSVC.class).toggle(target);
commandSender.sendMessage((visible ? C.GREEN : C.YELLOW)
+ "Studio debug scoreboard " + (visible ? "enabled." : "disabled."));
})) {
commandSender.sendMessage(C.RED + "Could not update the Studio debug scoreboard right now.");
}
}
@Director(description = "Create a new studio project", aliases = "+", sync = true)
public void create(
@Param(description = "The name of this new Iris Project.", defaultValue = "studio")
@@ -259,7 +279,7 @@ public class CommandStudio implements DirectorExecutor {
Inventory inv = Bukkit.createInventory(null, 27 * 2);
try {
EngineBukkitOps.addItems(engine(), true, inv, RNG.r, tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ(), 1);
EngineBukkitOps.addItems(engine(), true, inv, tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ());
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(C.RED + "Cannot add items to virtual inventory because of: " + e.getMessage());
@@ -286,7 +306,7 @@ public class CommandStudio implements DirectorExecutor {
inv.clear();
}
EngineBukkitOps.addItems(engine, true, inv, new RNG(RNG.r.imax()), tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ(), 1);
EngineBukkitOps.addItems(engine, true, inv, tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ());
}, fast ? 5 : 35));
sender().sendMessage(C.GREEN + "Opening inventory now!");
@@ -829,7 +849,7 @@ public class CommandStudio implements DirectorExecutor {
sender().sendMessage(C.RED + "No studio world is open!");
return true;
}
if (!engine().isStudio()) {
if (!IrisToolbelt.isStudio(world())) {
sender().sendMessage(C.RED + "You must be in a studio world!");
return true;
}
@@ -1,32 +0,0 @@
package art.arcane.iris.core.commands;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class BukkitNativeStructureLocatePolicyTest {
@Test
public void nativeMonumentLocateIsUnavailable() {
assertTrue(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:monument"));
assertTrue(BukkitNativeStructureLocatePolicy.isUnavailable(" MINECRAFT:MONUMENT "));
}
@Test
public void unrelatedAndNonCanonicalKeysRemainAvailable() {
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable(null));
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable(""));
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:stronghold"));
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:ocean_monument"));
assertFalse(BukkitNativeStructureLocatePolicy.isUnavailable("minecraft:ocean_monuments"));
}
@Test
public void rejectionExplainsSafetyAndGenerationBehavior() {
String message = BukkitNativeStructureLocatePolicy.unavailableMessage().toLowerCase();
assertTrue(message.contains("unavailable"));
assertTrue(message.contains("stall the server thread"));
assertTrue(message.contains("generation is unaffected"));
}
}
@@ -1,11 +1,14 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -21,6 +24,47 @@ public class IrisStructureLocateCommandContractTest {
assertFalse(source.contains("at[2] + 8"));
}
@Test
public void findRoutesOnlyExplicitNativeReplacementThroughIrisLocate() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource")));
int methodStart = source.indexOf("public void structure(");
int methodEnd = source.indexOf("private static Structure resolveNativeStructure", methodStart);
String method = source.substring(methodStart, methodEnd);
int nativeResolution = method.indexOf("resolveNativeStructure(structureKey)");
int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(", nativeResolution);
int replacementCheck = method.indexOf(
"decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS", policyResolution);
int replacementLocate = method.indexOf("locateIrisStructure(e, structureKey, commandSender)", replacementCheck);
int capabilityCheck = method.indexOf("NativeStructureLocateCapability.isPaperUnavailable(structureKey)", replacementLocate);
int genericIrisLookup = method.indexOf(
"nativeStructure == null && IrisStructureLocator.isPlaced(e, structureKey)", capabilityCheck);
int nativeLocate = method.indexOf("targetWorld.locateNearestStructure(", genericIrisLookup);
assertTrue(nativeResolution >= 0);
assertTrue(policyResolution > nativeResolution);
assertTrue(replacementCheck > policyResolution);
assertTrue(replacementLocate > replacementCheck);
assertTrue(capabilityCheck > replacementLocate);
assertTrue(genericIrisLookup > capabilityCheck);
assertTrue(nativeLocate > policyResolution);
assertTrue(method.contains("decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS"));
}
@Test
public void findNativePolicyMessagesMatchModdedDiagnostics() {
assertEquals(
"Native structure minecraft:village_plains is disabled by this dimension's importedStructures settings.",
NativeStructureGenerationPolicy.generationStatusMessage(
"minecraft:village_plains", NativeStructureGenerationStatus.DISABLED_BY_PACK));
assertEquals(
"Native structure minecraft:ancient_city is replaced by an Iris placement in this pack and locates through that explicit replacement.",
NativeStructureGenerationPolicy.generationStatusMessage(
"minecraft:ancient_city", NativeStructureGenerationStatus.REPLACED_BY_IRIS));
assertEquals(
"Native structure minecraft:stronghold generates natively.",
NativeStructureGenerationPolicy.generationStatusMessage(
"minecraft:stronghold", NativeStructureGenerationStatus.GENERATE_NATIVE));
}
@Test
public void structureVerifyReportsDensityLimitAndUsesExactLocatedOrigin() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource")));
@@ -31,4 +75,21 @@ public class IrisStructureLocateCommandContractTest {
assertFalse(source.contains("at[0] + 8"));
assertFalse(source.contains("at[2] + 8"));
}
@Test
public void structureVerifyPartitionsPolicyBeforeNativeReachability() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandStructureSource")));
int methodStart = source.indexOf("private void runVerification(");
int methodEnd = source.indexOf("private void sendVerificationMessages(", methodStart);
String method = source.substring(methodStart, methodEnd);
int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(engine, keyName, false)");
int nativeRequirement = method.indexOf("requiresNativeReachability |= decision.generate()", policyResolution);
int reachabilityGuard = method.indexOf("if (requiresNativeReachability)", nativeRequirement);
int reachabilityLookup = method.indexOf("StructureReachability.reachableKeys(engine)", reachabilityGuard);
assertTrue(policyResolution >= 0);
assertTrue(nativeRequirement > policyResolution);
assertTrue(reachabilityGuard > nativeRequirement);
assertTrue(reachabilityLookup > reachabilityGuard);
}
}
@@ -0,0 +1,25 @@
package art.arcane.iris.core.commands;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import org.junit.Test;
import java.lang.reflect.Method;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class StudioScoreboardCommandTest {
@Test
public void scoreboardCommandIsPlayerScopedAndEntityScheduled() throws NoSuchMethodException {
Method command = CommandStudio.class.getDeclaredMethod("scoreboard");
Director director = command.getAnnotation(Director.class);
assertEquals(DirectorOrigin.PLAYER, director.origin());
assertFalse(director.sync());
assertTrue(List.of(director.aliases()).contains("board"));
assertTrue(List.of(director.aliases()).contains("sidebar"));
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+152 -13
View File
@@ -1,9 +1,102 @@
[21:25:27] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test'
[21:25:27] [Test worker/ERROR]: [worldcheck] server stop request failed
[05:44:33] [Test worker/INFO]: Iris registered custom content provider 'iris_deferred_test'
[05:44:33] [Test worker/ERROR]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[05:44:33] [Test worker/ERROR]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: enable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Suppressed: java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32)
... 42 more
[05:44:33] [Test worker/ERROR]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:218)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171)
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:215)
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:176)
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:232)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
@@ -46,11 +139,11 @@ java.lang.IllegalStateException: stop request failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[21:25:27] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed
[05:44:33] [Test worker/ERROR]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:244)
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:186)
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:242)
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261)
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:191)
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:259)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
@@ -93,11 +186,57 @@ java.lang.IllegalStateException: shutdown wait failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[21:25:27] [Test worker/ERROR]: [worldcheck] check failed
[05:44:33] [Test worker/ERROR]: [worldcheck] check failed
java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:204)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:166)
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:202)
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221)
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171)
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:219)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47)
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65)
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88)
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
[05:44:33] [Test worker/INFO]: Iris registered custom content provider 'iris_discovery_success'
[05:44:33] [Test worker/ERROR]: Iris custom content provider discovery failed
java.lang.RuntimeException: provider init failed
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
@@ -21,28 +21,20 @@ package art.arcane.iris.fabric;
import art.arcane.iris.modded.ModdedForcedDatapack;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.server.packs.repository.RepositorySource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
public final class FabricForcedDatapackSources {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private FabricForcedDatapackSources() {
}
public static void attach(PackRepository repository) {
if (repository == null) {
return;
}
try {
Set<RepositorySource> merged = new LinkedHashSet<>(repository.sources);
merged.add(ModdedForcedDatapack.repositorySource());
repository.sources = merged;
} catch (Throwable e) {
LOGGER.error("Iris failed to attach the forced startup datapack source to the server data pack repository", e);
}
PackRepository activeRepository = Objects.requireNonNull(repository,
"Iris cannot attach the forced startup datapack source to a null repository");
Set<RepositorySource> merged = new LinkedHashSet<>(activeRepository.sources);
merged.add(ModdedForcedDatapack.repositorySource());
activeRepository.sources = merged;
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+155 -16
View File
@@ -1,12 +1,105 @@
[13Jul2026 21:25:26.811] [Test worker/DEBUG] [io.netty.util.internal.logging.InternalLoggerFactory/]: Using SLF4J as the default logging framework
[13Jul2026 21:25:26.813] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.level: simple
[13Jul2026 21:25:26.813] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.targetRecords: 4
[13Jul2026 21:25:28.681] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[13Jul2026 21:25:28.758] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
[16Jul2026 05:44:24.794] [Test worker/DEBUG] [io.netty.util.internal.logging.InternalLoggerFactory/]: Using SLF4J as the default logging framework
[16Jul2026 05:44:24.795] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.level: simple
[16Jul2026 05:44:24.795] [Test worker/DEBUG] [io.netty.util.ResourceLeakDetector/]: -Dio.netty.leakDetection.targetRecords: 4
[16Jul2026 05:44:26.716] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[16Jul2026 05:44:26.771] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[16Jul2026 05:44:26.779] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: enable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
Suppressed: java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
... 42 more
[16Jul2026 05:44:26.814] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:218) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:215) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:176) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:232) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
@@ -49,11 +142,11 @@ java.lang.IllegalStateException: stop request failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[13Jul2026 21:25:28.765] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
[16Jul2026 05:44:26.821] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:244) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:186) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:242) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:191) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:259) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
@@ -96,11 +189,57 @@ java.lang.IllegalStateException: shutdown wait failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[13Jul2026 21:25:28.769] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
[16Jul2026 05:44:26.826] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:204) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:166) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:202) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:219) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[16Jul2026 05:44:26.832] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success'
[16Jul2026 05:44:26.833] [Test worker/ERROR] [Iris/]: Iris custom content provider discovery failed
java.lang.RuntimeException: provider init failed
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
+152 -13
View File
@@ -1,9 +1,102 @@
[13Jul2026 21:25:28.681] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[13Jul2026 21:25:28.758] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
[16Jul2026 05:44:26.716] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_deferred_test'
[16Jul2026 05:44:26.771] [Test worker/ERROR] [Iris/]: Iris service rollback failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[16Jul2026 05:44:26.779] [Test worker/ERROR] [Iris/]: Iris service onEnable failed for art.arcane.iris.modded.ModdedServiceManagerTest$SecondService
java.lang.RuntimeException: enable failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:31) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
Suppressed: java.lang.RuntimeException: cleanup failed
at art.arcane.iris.modded.ModdedServiceManagerTest.rollsBackEnabledServicesAndPreservesCleanupFailures(ModdedServiceManagerTest.java:32) ~[test/:?]
... 42 more
[16Jul2026 05:44:26.814] [Test worker/ERROR] [Iris/]: [worldcheck] server stop request failed
java.lang.IllegalStateException: stop request failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:218) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:215) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$stopRequestFailureForcesNonzeroResult$1(ModdedWorldCheckTest.java:235) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:176) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.stopRequestFailureForcesNonzeroResult(ModdedWorldCheckTest.java:232) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
@@ -46,11 +139,11 @@ java.lang.IllegalStateException: stop request failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[13Jul2026 21:25:28.765] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
[16Jul2026 05:44:26.821] [Test worker/ERROR] [Iris/]: [worldcheck] waiting for server shutdown failed
java.lang.IllegalStateException: shutdown wait failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:244) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:186) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:242) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$shutdownWaitFailureForcesNonzeroExit$0(ModdedWorldCheckTest.java:261) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.awaitStopAndExit(ModdedWorldCheck.java:191) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.shutdownWaitFailureForcesNonzeroExit(ModdedWorldCheckTest.java:259) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
@@ -93,11 +186,57 @@ java.lang.IllegalStateException: shutdown wait failed
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[13Jul2026 21:25:28.769] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
[16Jul2026 05:44:26.826] [Test worker/ERROR] [Iris/]: [worldcheck] check failed
java.lang.IllegalStateException: check failed
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:204) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:166) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:202) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.lambda$thrownCheckStillRequestsStop$0(ModdedWorldCheckTest.java:221) ~[test/:?]
at art.arcane.iris.modded.ModdedWorldCheck.runAndRequestStop(ModdedWorldCheck.java:171) ~[main/:?]
at art.arcane.iris.modded.ModdedWorldCheckTest.thrownCheckStillRequestsStop(ModdedWorldCheckTest.java:219) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) ~[junit-4.13.2.jar:4.13.2]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runners.ParentRunner.run(ParentRunner.java:413) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) ~[junit-4.13.2.jar:4.13.2]
at org.junit.runner.JUnitCore.run(JUnitCore.java:115) ~[junit-4.13.2.jar:4.13.2]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.runRequest(JUnitTestExecutor.java:175) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:84) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.JUnitTestExecutor.accept(JUnitTestExecutor.java:47) ~[?:?]
at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestDefinitionProcessor.processTestDefinition(AbstractJUnitTestDefinitionProcessor.java:65) ~[?:?]
at org.gradle.api.internal.tasks.testing.SuiteTestDefinitionProcessor.processTestDefinition(SuiteTestDefinitionProcessor.java:53) ~[?:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.gradle.internal.dispatch.MethodInvocation.invokeOn(MethodInvocation.java:77) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:28) ~[?:?]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:19) ~[?:?]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[?:?]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:88) ~[?:?]
at jdk.proxy1/jdk.proxy1.$Proxy4.processTestDefinition(Unknown Source) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:178) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:126) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:103) ~[?:?]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:63) ~[?:?]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:122) ~[?:?]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:72) ~[?:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) [gradle-worker.jar:?]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) [gradle-worker.jar:?]
[16Jul2026 05:44:26.832] [Test worker/INFO] [Iris/]: Iris registered custom content provider 'iris_discovery_success'
[16Jul2026 05:44:26.833] [Test worker/ERROR] [Iris/]: Iris custom content provider discovery failed
java.lang.RuntimeException: provider init failed
at art.arcane.iris.modded.api.ModdedCustomContentRegistryTest.failedDiscoveryPublishesNothingAndPreservesTheCause(ModdedCustomContentRegistryTest.java:39) ~[test/:?]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?]
at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[?:?]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) ~[junit-4.13.2.jar:4.13.2]
@@ -0,0 +1,35 @@
package art.arcane.iris.nativegen;
public final class NativeStructureGenerationException extends IllegalStateException {
private NativeStructureGenerationException(FailureContext context, Throwable cause) {
super(message(context), cause);
}
private NativeStructureGenerationException(FailureContext context) {
super(message(context));
}
public static NativeStructureGenerationException failure(String operation, String structureContext,
int chunkX, int chunkZ, Throwable cause) {
return new NativeStructureGenerationException(
new FailureContext(operation, structureContext, chunkX, chunkZ), cause);
}
public static NativeStructureGenerationException failure(String operation, String structureContext,
int chunkX, int chunkZ) {
return new NativeStructureGenerationException(
new FailureContext(operation, structureContext, chunkX, chunkZ));
}
private static String message(FailureContext context) {
String resolvedContext = context.structureContext() == null || context.structureContext().isBlank()
? "<unregistered structure>"
: context.structureContext();
return "Iris native structure " + context.operation() + " failed for " + resolvedContext
+ " in chunk " + context.chunkX() + "," + context.chunkZ()
+ "; chunk generation was aborted to prevent partial structure state";
}
private record FailureContext(String operation, String structureContext, int chunkX, int chunkZ) {
}
}
@@ -0,0 +1,45 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.nativegen;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
public final class NativeStructureLocateResults {
private NativeStructureLocateResults() {
}
public static <T> Pair<BlockPos, T> nearest(BlockPos origin, Pair<BlockPos, T> replacement,
Pair<BlockPos, T> nativeResult) {
if (replacement == null) {
return nativeResult;
}
if (nativeResult == null) {
return replacement;
}
return horizontalDistanceSquared(origin, nativeResult.getFirst())
<= horizontalDistanceSquared(origin, replacement.getFirst()) ? nativeResult : replacement;
}
private static long horizontalDistanceSquared(BlockPos origin, BlockPos target) {
long dx = (long) target.getX() - origin.getX();
long dz = (long) target.getZ() - origin.getZ();
return dx * dx + dz * dz;
}
}
@@ -2,9 +2,12 @@ package art.arcane.iris.nativegen;
import art.arcane.iris.engine.framework.StructureVerticalBounds;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisVanillaStructureStiltSettings;
import art.arcane.iris.engine.object.IrisObjectVacuum;
import art.arcane.iris.engine.object.IrisStructureStiltSettings;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.datafixers.util.Either;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.Identifier;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
@@ -20,15 +23,36 @@ import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction;
import net.minecraft.world.level.levelgen.structure.pools.LegacySinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
import java.util.Objects;
import java.util.function.IntBinaryOperator;
public final class NativeStructurePostProcessor {
private static final int FOUNDATION_VERTICAL_TOLERANCE = 1;
private static final int MAX_BURIAL_COLUMNS = 2_000_000;
private static final int MONUMENT_BASE_BELOW_SEA_LEVEL = 24;
private static final String OCEAN_MONUMENT_ID = "minecraft:monument";
private static final double SURFACE_TERRAIN_FALLOFF = 2.0;
private static final long SURFACE_TERRAIN_INFLUENCE_SCALE = 1_000_000L;
private static final int SURFACE_TERRAIN_RADIUS = 12;
private static final int UNDERGROUND_SURFACE_CLEARANCE = 1;
private NativeStructurePostProcessor() {
}
@@ -37,38 +61,185 @@ public final class NativeStructurePostProcessor {
WorldgenRandom random, BoundingBox area, ChunkPos chunkPos, String structureId,
StructureStart start, IrisNativeStructureDecision decision,
StiltBlockResolver stiltBlockResolver) {
IrisVanillaStructureStiltSettings stilt = decision.stilt();
IrisStructureStiltSettings stilt = decision.stilt();
ensureMonumentSeaLevelAlignment(start, structureId, decision.yShift(), generator.getSeaLevel(),
area.minY(), area.maxY() + 1);
start.placeInChunk(world, structureManager, generator, random, area, chunkPos);
if (stilt != null) {
placeStilts(world, area, structureId, start, stilt, stiltBlockResolver);
}
}
public static int applyVerticalPlacement(StructureStart start, String structureId, int requestedOffset,
int seaLevel, int worldMinY, int worldMaxYExclusive,
boolean underground, IntBinaryOperator surfaceHeight) {
if (isOceanMonument(structureId)) {
return alignOceanMonumentToSeaLevel(
start, requestedOffset, seaLevel, worldMinY, worldMaxYExclusive);
}
return applyVerticalShift(
start, requestedOffset, worldMinY, worldMaxYExclusive, underground, surfaceHeight);
}
public static int applyVerticalShift(StructureStart start, int requestedOffset, int worldMinY,
int worldMaxYExclusive) {
int worldMaxYExclusive, boolean underground,
IntBinaryOperator surfaceHeight) {
BoundingBox bounds = start.getBoundingBox();
int resolvedOffset = underground
? resolveBuriedOffset(bounds, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight)
: requestedOffset;
int offsetY = StructureVerticalBounds.clampOffset(
bounds.minY(), bounds.maxY(), requestedOffset, worldMinY, worldMaxYExclusive);
bounds.minY(), bounds.maxY(), resolvedOffset, worldMinY, worldMaxYExclusive);
if (underground && offsetY > resolvedOffset) {
throw new IllegalStateException("Underground native structure cannot fit below terrain and inside world bounds");
}
if (offsetY == 0) {
return 0;
}
moveStructureStart(start, bounds, offsetY);
return offsetY;
}
static int alignOceanMonumentToSeaLevel(StructureStart start, int configuredOffset, int seaLevel,
int worldMinY, int worldMaxYExclusive) {
OceanMonumentPieces.MonumentBuilding building = requireOceanMonumentBuilding(start);
BoundingBox bounds = start.getBoundingBox();
int targetMinY = Math.addExact(
Math.subtractExact(seaLevel, MONUMENT_BASE_BELOW_SEA_LEVEL), configuredOffset);
int requestedOffset = Math.subtractExact(targetMinY, building.getBoundingBox().minY());
int offsetY = StructureVerticalBounds.clampOffset(
bounds.minY(), bounds.maxY(), requestedOffset, worldMinY, worldMaxYExclusive);
if (offsetY != requestedOffset) {
throw new IllegalStateException("Ocean monument cannot align to sea level " + seaLevel
+ " with configured offset " + configuredOffset + " inside world bounds ["
+ worldMinY + "," + worldMaxYExclusive + ")");
}
if (offsetY == 0) {
return 0;
}
moveStructureStart(start, bounds, offsetY);
return offsetY;
}
private static void ensureMonumentSeaLevelAlignment(StructureStart start, String structureId,
int configuredOffset, int seaLevel,
int worldMinY, int worldMaxYExclusive) {
if (isOceanMonument(structureId)) {
alignOceanMonumentToSeaLevel(
start, configuredOffset, seaLevel, worldMinY, worldMaxYExclusive);
}
}
private static boolean isOceanMonument(String structureId) {
return OCEAN_MONUMENT_ID.equals(structureId);
}
private static OceanMonumentPieces.MonumentBuilding requireOceanMonumentBuilding(StructureStart start) {
Objects.requireNonNull(start, "Ocean monument start must not be null");
List<StructurePiece> pieces = start.getPieces();
if (pieces.size() != 1 || !(pieces.get(0) instanceof OceanMonumentPieces.MonumentBuilding building)) {
throw new IllegalStateException("minecraft:monument must contain exactly one MonumentBuilding, found "
+ pieces.size() + " top-level pieces");
}
return building;
}
private static void moveStructureStart(StructureStart start, BoundingBox cachedBounds, int offsetY) {
for (StructurePiece piece : start.getPieces()) {
piece.move(0, offsetY, 0);
if (piece instanceof PoolElementStructurePiece poolPiece) {
List<JigsawJunction> junctions = poolPiece.getJunctions();
for (int i = 0; i < junctions.size(); i++) {
JigsawJunction junction = junctions.get(i);
junctions.set(i, new JigsawJunction(
junction.getSourceX(),
junction.getSourceGroundY() + offsetY,
junction.getSourceZ(),
junction.getDeltaY(),
junction.getDestProjection()));
}
moveStructurePiece(piece, offsetY);
}
cachedBounds.move(0, offsetY, 0);
}
private static void moveStructurePiece(StructurePiece piece, int offsetY) {
piece.move(0, offsetY, 0);
if (piece instanceof OceanMonumentPieces.MonumentBuilding building) {
for (StructurePiece child : monumentChildPieces(building)) {
child.move(0, offsetY, 0);
}
}
bounds.move(0, offsetY, 0);
return offsetY;
if (piece instanceof PoolElementStructurePiece poolPiece) {
List<JigsawJunction> junctions = poolPiece.getJunctions();
for (int i = 0; i < junctions.size(); i++) {
JigsawJunction junction = junctions.get(i);
junctions.set(i, new JigsawJunction(
junction.getSourceX(),
junction.getSourceGroundY() + offsetY,
junction.getSourceZ(),
junction.getDeltaY(),
junction.getDestProjection()));
}
}
}
static List<StructurePiece> monumentChildPieces(OceanMonumentPieces.MonumentBuilding building) {
Object value;
try {
value = MonumentChildPiecesAccess.FIELD.get(building);
} catch (IllegalAccessException error) {
throw new IllegalStateException("Cannot read Ocean Monument child pieces", error);
}
if (!(value instanceof List<?> children)) {
throw new IllegalStateException("Ocean Monument child-pieces field is not a list");
}
List<StructurePiece> pieces = new ArrayList<>(children.size());
for (Object child : children) {
if (!(child instanceof StructurePiece monumentPiece)
|| child.getClass().getEnclosingClass() != OceanMonumentPieces.class) {
throw new IllegalStateException("Ocean Monument child-pieces list contains "
+ (child == null ? "null" : child.getClass().getName()));
}
pieces.add(monumentPiece);
}
return List.copyOf(pieces);
}
private static Field resolveMonumentChildPiecesField() {
Field resolved = null;
for (Field field : OceanMonumentPieces.MonumentBuilding.class.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || field.getType() != List.class) {
continue;
}
if (resolved != null) {
throw new IllegalStateException("Ocean Monument has multiple instance List fields");
}
resolved = field;
}
if (resolved == null) {
throw new IllegalStateException("Ocean Monument child-pieces List field is missing");
}
if (!Modifier.isPrivate(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) {
throw new IllegalStateException("Ocean Monument child-pieces field has an unexpected access contract");
}
if (!resolved.trySetAccessible()) {
throw new IllegalStateException("Ocean Monument child-pieces field is inaccessible");
}
return resolved;
}
static int resolveBuriedOffset(BoundingBox bounds, int requestedOffset, int worldMinY,
int worldMaxYExclusive, IntBinaryOperator surfaceHeight) {
Objects.requireNonNull(bounds, "Native structure bounds must not be null");
Objects.requireNonNull(surfaceHeight, "Underground native structure requires a terrain height resolver");
long width = (long) bounds.maxX() - bounds.minX() + 1L;
long depth = (long) bounds.maxZ() - bounds.minZ() + 1L;
if (width <= 0L || depth <= 0L || width > MAX_BURIAL_COLUMNS / depth) {
throw new IllegalStateException("Underground native structure burial footprint is invalid or exceeds "
+ MAX_BURIAL_COLUMNS + " columns");
}
int maximumOffset = requestedOffset;
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
int allowedTopY = surfaceHeight.applyAsInt(x, z) - UNDERGROUND_SURFACE_CLEARANCE;
maximumOffset = Math.min(maximumOffset, allowedTopY - bounds.maxY());
}
}
int clampedOffset = StructureVerticalBounds.clampOffset(
bounds.minY(), bounds.maxY(), maximumOffset, worldMinY, worldMaxYExclusive);
if (clampedOffset > maximumOffset) {
throw new IllegalStateException("Underground native structure cannot be buried without crossing world minimum Y");
}
return clampedOffset;
}
public static boolean isUndergroundStep(GenerationStep.Decoration step) {
@@ -77,6 +248,365 @@ public final class NativeStructurePostProcessor {
|| step == GenerationStep.Decoration.STRONGHOLDS;
}
public static boolean shouldClearEntireVegetationFootprint(GenerationStep.Decoration step,
boolean configured) {
return configured || !isUndergroundStep(step);
}
public static void prepareSurfaceStructures(WorldGenLevel world, BoundingBox area,
List<StructureStart> starts,
IntBinaryOperator surfaceHeight) {
if (starts == null || starts.isEmpty()) {
return;
}
Objects.requireNonNull(surfaceHeight, "Surface structure terrain fitting requires an Iris height resolver");
List<SurfaceAnchor> anchors = collectSurfaceAnchors(starts);
if (!anchors.isEmpty()) {
fitSurfaceTerrain(world, area, anchors, surfaceHeight);
}
StructureTemplateManager templateManager = world.getLevel().getStructureManager();
for (StructureStart start : starts) {
if (requiresSurfaceTerrain(start)) {
clearLegacyTemplateAir(world, area, start, templateManager);
}
}
}
static boolean shouldPrepareSurfaceTerrain(TerrainAdjustment adjustment,
GenerationStep.Decoration step) {
return adjustment == TerrainAdjustment.BEARD_THIN
&& step == GenerationStep.Decoration.SURFACE_STRUCTURES;
}
static int resolveSurfaceTarget(List<SurfaceAnchor> anchors, int worldX, int worldZ,
int originalY) {
int localTargetY = originalY;
SurfaceAnchor selectedLocal = null;
long totalInfluence = 0L;
long weightedMeetY = 0L;
long maximumInfluence = 0L;
for (SurfaceAnchor anchor : anchors) {
int outX = IrisObjectVacuum.outset(worldX, anchor.minX(), anchor.maxX());
int outZ = IrisObjectVacuum.outset(worldZ, anchor.minZ(), anchor.maxZ());
long distanceSquared = (long) outX * outX + (long) outZ * outZ;
if (distanceSquared > (long) SURFACE_TERRAIN_RADIUS * SURFACE_TERRAIN_RADIUS) {
continue;
}
boolean containsColumn = outX == 0 && outZ == 0;
if (containsColumn) {
if (precedes(anchor, selectedLocal)) {
localTargetY = anchor.meetY();
selectedLocal = anchor;
}
continue;
}
double factor = IrisObjectVacuum.columnInfluence(
worldX, worldZ,
anchor.minX(), anchor.maxX(), anchor.minZ(), anchor.maxZ(),
SURFACE_TERRAIN_RADIUS, SURFACE_TERRAIN_FALLOFF);
long influence = Math.round(factor * SURFACE_TERRAIN_INFLUENCE_SCALE);
if (influence <= 0L) {
continue;
}
long weightedInfluence = influence * Math.max(1, anchor.strength());
totalInfluence += weightedInfluence;
weightedMeetY += weightedInfluence * anchor.meetY();
maximumInfluence = Math.max(maximumInfluence, influence);
}
if (selectedLocal != null) {
return localTargetY;
}
if (totalInfluence == 0L) {
return originalY;
}
double blendedMeetY = weightedMeetY / (double) totalInfluence;
double factor = maximumInfluence / (double) SURFACE_TERRAIN_INFLUENCE_SCALE;
return (int) Math.round(originalY + ((blendedMeetY - originalY) * factor));
}
private static boolean precedes(SurfaceAnchor candidate, SurfaceAnchor selected) {
if (selected == null) {
return true;
}
if (candidate.strength() != selected.strength()) {
return candidate.strength() > selected.strength();
}
if (candidate.meetY() != selected.meetY()) {
return candidate.meetY() < selected.meetY();
}
if (candidate.minX() != selected.minX()) {
return candidate.minX() < selected.minX();
}
if (candidate.minZ() != selected.minZ()) {
return candidate.minZ() < selected.minZ();
}
if (candidate.maxX() != selected.maxX()) {
return candidate.maxX() < selected.maxX();
}
if (candidate.maxZ() != selected.maxZ()) {
return candidate.maxZ() < selected.maxZ();
}
return false;
}
private static List<SurfaceAnchor> collectSurfaceAnchors(List<StructureStart> starts) {
List<SurfaceAnchor> anchors = new ArrayList<>();
for (StructureStart start : starts) {
if (!requiresSurfaceTerrain(start)) {
continue;
}
for (StructurePiece piece : start.getPieces()) {
if (piece instanceof PoolElementStructurePiece poolPiece) {
if (poolPiece.getElement().getProjection() == StructureTemplatePool.Projection.RIGID) {
BoundingBox bounds = poolPiece.getBoundingBox();
anchors.add(new SurfaceAnchor(
bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(),
bounds.minY() + poolPiece.getGroundLevelDelta() - 1, 2));
}
for (JigsawJunction junction : poolPiece.getJunctions()) {
anchors.add(new SurfaceAnchor(
junction.getSourceX(), junction.getSourceX(),
junction.getSourceZ(), junction.getSourceZ(),
junction.getSourceGroundY() - 1, 1));
}
continue;
}
BoundingBox bounds = piece.getBoundingBox();
anchors.add(new SurfaceAnchor(
bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(),
bounds.minY() - 1, 2));
}
}
return List.copyOf(anchors);
}
private static boolean requiresSurfaceTerrain(StructureStart start) {
return start != null
&& start.isValid()
&& shouldPrepareSurfaceTerrain(
start.getStructure().terrainAdaptation(), start.getStructure().step());
}
private static void fitSurfaceTerrain(WorldGenLevel world, BoundingBox area,
List<SurfaceAnchor> anchors,
IntBinaryOperator surfaceHeight) {
int width = area.getXSpan();
int depth = area.getZSpan();
int[] originalHeights = new int[width * depth];
int[] targetHeights = new int[width * depth];
for (int z = area.minZ(); z <= area.maxZ(); z++) {
for (int x = area.minX(); x <= area.maxX(); x++) {
int column = (z - area.minZ()) * width + x - area.minX();
int originalY = Math.max(area.minY(), Math.min(
area.maxY(), surfaceHeight.applyAsInt(x, z)));
originalHeights[column] = originalY;
targetHeights[column] = Math.max(area.minY(), Math.min(
area.maxY(), resolveSurfaceTarget(anchors, x, z, originalY)));
}
}
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (int z = area.minZ(); z <= area.maxZ(); z++) {
for (int x = area.minX(); x <= area.maxX(); x++) {
int column = (z - area.minZ()) * width + x - area.minX();
applySurfaceColumn(world, position, x, z,
originalHeights[column], targetHeights[column], area.minY(), area.maxY());
}
}
}
static void applySurfaceColumn(WorldGenLevel world, BlockPos.MutableBlockPos position,
int x, int z, int originalY, int targetY,
int worldMinY, int worldMaxY) {
if (targetY == originalY) {
return;
}
SurfaceMaterials materials = resolveSurfaceMaterials(world, position, x, z, originalY, worldMinY);
if (targetY < originalY) {
BlockState clearedState = clearSurfaceDecorationAndResolveFill(
world, position, x, z, originalY, worldMaxY);
for (int y = originalY; y > targetY; y--) {
world.setBlock(position.set(x, y, z), clearedState, 2);
}
world.setBlock(position.set(x, targetY, z), materials.surface(), 2);
return;
}
for (int y = originalY + 1; y < targetY; y++) {
world.setBlock(position.set(x, y, z), materials.subsurface(), 2);
}
world.setBlock(position.set(x, targetY, z), materials.surface(), 2);
}
private static BlockState clearSurfaceDecorationAndResolveFill(
WorldGenLevel world, BlockPos.MutableBlockPos position,
int x, int z, int originalY, int worldMaxY) {
BlockState air = Blocks.AIR.defaultBlockState();
for (int y = originalY + 1; y <= worldMaxY; y++) {
BlockState state = world.getBlockState(position.set(x, y, z));
if (state.isAir()) {
return air;
}
if (!state.getFluidState().isEmpty()) {
BlockState fluid = state.getFluidState().createLegacyBlock();
if (state != fluid) {
world.setBlock(position, fluid, 2);
}
return fluid;
}
if (state.isSolid() || isTreeBlock(state)) {
return air;
}
world.setBlock(position, air, 2);
}
return air;
}
private static SurfaceMaterials resolveSurfaceMaterials(WorldGenLevel world,
BlockPos.MutableBlockPos position,
int x, int z, int originalY,
int worldMinY) {
BlockState surface = world.getBlockState(position.set(x, originalY, z));
BlockState subsurface = null;
for (int y = originalY - 1; y >= worldMinY; y--) {
BlockState candidate = world.getBlockState(position.set(x, y, z));
if (isTerrainBlock(candidate)) {
subsurface = candidate;
break;
}
}
if (subsurface == null) {
subsurface = isTerrainBlock(surface) ? surface : Blocks.STONE.defaultBlockState();
}
if (!isTerrainBlock(surface)) {
surface = subsurface;
}
return new SurfaceMaterials(surface, subsurface);
}
private static boolean isTerrainBlock(BlockState state) {
return state.isSolid() && !isTreeBlock(state);
}
private static void clearLegacyTemplateAir(WorldGenLevel world, BoundingBox area,
StructureStart start,
StructureTemplateManager templateManager) {
for (StructurePiece piece : start.getPieces()) {
if (!(piece instanceof PoolElementStructurePiece poolPiece)
|| poolPiece.getElement().getProjection() != StructureTemplatePool.Projection.RIGID
|| !intersects(poolPiece.getBoundingBox(), area)) {
continue;
}
int groundY = poolPiece.getBoundingBox().minY() + poolPiece.getGroundLevelDelta();
StructurePlaceSettings settings = new StructurePlaceSettings()
.setRotation(poolPiece.getRotation())
.setBoundingBox(area);
clearLegacyTemplateAir(world, poolPiece.getElement(), poolPiece.getPosition(),
groundY, settings, templateManager);
}
}
private static void clearLegacyTemplateAir(WorldGenLevel world, StructurePoolElement element,
BlockPos position, int groundY,
StructurePlaceSettings settings,
StructureTemplateManager templateManager) {
if (element instanceof ListPoolElement listElement) {
for (StructurePoolElement child : listElement.getElements()) {
clearLegacyTemplateAir(world, child, position, groundY, settings, templateManager);
}
return;
}
if (!(element instanceof LegacySinglePoolElement legacyElement)) {
return;
}
StructureTemplate template = resolveTemplate(legacyElement, templateManager);
clearTemplateAir(world, template, position, groundY, settings);
}
static void clearTemplateAir(WorldGenLevel world, StructureTemplate template,
BlockPos position, int groundY,
StructurePlaceSettings settings) {
List<StructureTemplate.StructureBlockInfo> airBlocks = template.filterBlocks(
position, settings, Blocks.AIR);
BlockState air = Blocks.AIR.defaultBlockState();
for (StructureTemplate.StructureBlockInfo airBlock : airBlocks) {
BlockPos airPosition = airBlock.pos();
if (shouldClearLegacyAir(
airPosition.getY(), groundY, world.getBlockState(airPosition).isAir())) {
world.setBlock(airPosition, air, 2);
}
}
}
static boolean shouldClearLegacyAir(int airY, int groundY, boolean existingAir) {
return airY >= groundY && !existingAir;
}
static boolean intersects(BoundingBox first, BoundingBox second) {
return first.maxX() >= second.minX() && first.minX() <= second.maxX()
&& first.maxY() >= second.minY() && first.minY() <= second.maxY()
&& first.maxZ() >= second.minZ() && first.minZ() <= second.maxZ();
}
private static StructureTemplate resolveTemplate(SinglePoolElement element,
StructureTemplateManager templateManager) {
Object value;
try {
value = SinglePoolTemplateAccess.FIELD.get(element);
} catch (IllegalAccessException error) {
throw new IllegalStateException("Cannot read native structure pool template", error);
}
if (!(value instanceof Either<?, ?> reference)) {
throw new IllegalStateException("Native structure pool template field is not an Either");
}
return resolveTemplateReference(reference, templateManager);
}
static StructureTemplate resolveTemplateReference(Either<?, ?> reference,
StructureTemplateManager templateManager) {
return reference.map(
location -> resolveNamedTemplate(location, templateManager),
NativeStructurePostProcessor::requireRuntimeTemplate);
}
private static StructureTemplate resolveNamedTemplate(Object value,
StructureTemplateManager templateManager) {
if (!(value instanceof Identifier identifier)) {
throw new IllegalStateException("Native structure pool template identifier is "
+ (value == null ? "null" : value.getClass().getName()));
}
return templateManager.getOrCreate(identifier);
}
private static StructureTemplate requireRuntimeTemplate(Object value) {
if (!(value instanceof StructureTemplate template)) {
throw new IllegalStateException("Native runtime structure pool template is "
+ (value == null ? "null" : value.getClass().getName()));
}
return template;
}
static Field resolveSinglePoolTemplateField() {
Field resolved = null;
for (Field field : SinglePoolElement.class.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || field.getType() != Either.class) {
continue;
}
if (resolved != null) {
throw new IllegalStateException("SinglePoolElement has multiple instance Either fields");
}
resolved = field;
}
if (resolved == null) {
throw new IllegalStateException("SinglePoolElement template Either field is missing");
}
if (!Modifier.isProtected(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) {
throw new IllegalStateException("SinglePoolElement template field has an unexpected access contract");
}
if (!resolved.trySetAccessible()) {
throw new IllegalStateException("SinglePoolElement template field is inaccessible");
}
return resolved;
}
public static void clearIntersectingVegetation(WorldGenLevel world, ChunkAccess chunk, BoundingBox area,
List<VegetationTarget> targets) {
if (targets == null || targets.isEmpty()) {
@@ -141,19 +671,25 @@ public final class NativeStructurePostProcessor {
private static void markVegetationColumns(BoundingBox area, VegetationSnapshot snapshot,
VegetationTarget target, boolean[] clearColumns) {
int width = area.getXSpan();
int padding = requiresSurfaceTerrain(target.start()) ? SURFACE_TERRAIN_RADIUS : 0;
int[] pieceTops = new int[clearColumns.length];
Arrays.fill(pieceTops, Integer.MIN_VALUE);
for (StructurePiece piece : target.start().getPieces()) {
BoundingBox bounds = piece.getBoundingBox();
int minX = Math.max(area.minX(), bounds.minX());
int maxX = Math.min(area.maxX(), bounds.maxX());
int minZ = Math.max(area.minZ(), bounds.minZ());
int maxZ = Math.min(area.maxZ(), bounds.maxZ());
int minX = Math.max(area.minX(), bounds.minX() - padding);
int maxX = Math.min(area.maxX(), bounds.maxX() + padding);
int minZ = Math.max(area.minZ(), bounds.minZ() - padding);
int maxZ = Math.min(area.maxZ(), bounds.maxZ() + padding);
if (minX > maxX || minZ > maxZ) {
continue;
}
for (int z = minZ; z <= maxZ; z++) {
for (int x = minX; x <= maxX; x++) {
if (padding > 0) {
if (!withinSurfaceTerrainRadius(x, z, bounds, padding)) {
continue;
}
}
int column = (z - area.minZ()) * width + x - area.minX();
pieceTops[column] = Math.max(pieceTops[column], bounds.maxY());
}
@@ -169,6 +705,12 @@ public final class NativeStructurePostProcessor {
}
}
static boolean withinSurfaceTerrainRadius(int x, int z, BoundingBox bounds, int radius) {
int outX = IrisObjectVacuum.outset(x, bounds.minX(), bounds.maxX());
int outZ = IrisObjectVacuum.outset(z, bounds.minZ(), bounds.maxZ());
return (long) outX * outX + (long) outZ * outZ <= (long) radius * radius;
}
static boolean shouldClearVegetationColumn(int pieceTopY, int lowestTreeY, boolean force) {
return force || pieceTopY >= lowestTreeY;
}
@@ -255,7 +797,7 @@ public final class NativeStructurePostProcessor {
}
private static void placeStilts(WorldGenLevel world, BoundingBox area, String structureId,
StructureStart start, IrisVanillaStructureStiltSettings settings,
StructureStart start, IrisStructureStiltSettings settings,
StiltBlockResolver stiltBlockResolver) {
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
int structureHash = structureId == null ? 0 : structureId.hashCode();
@@ -273,12 +815,55 @@ public final class NativeStructurePostProcessor {
if (existingState.isSolid() && !vegetation) {
break;
}
BlockState stilt = stiltBlockResolver.resolve(settings, rng, column.x(), y, column.z());
world.setBlock(position, stilt == null ? Blocks.COBBLESTONE.defaultBlockState() : stilt, 2);
BlockState stilt = Objects.requireNonNull(
stiltBlockResolver.resolve(settings, rng, column.x(), y, column.z()),
"Stilt palette returned no block for " + structureId + " at "
+ column.x() + "," + y + "," + column.z());
world.setBlock(position, stilt, 2);
}
}
}
public static StiltSupportAudit auditStiltSupport(WorldGenLevel world, BoundingBox area,
StructureStart start, BlockState expectedStilt) {
Objects.requireNonNull(expectedStilt, "Expected stilt state must not be null");
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
int baseColumns = 0;
int stiltBlocks = 0;
int stiltColumns = 0;
int unsupportedColumns = 0;
for (FoundationColumn column : foundationEnvelope(area, start)) {
int foundationY = findFoundationY(world, column, position);
if (foundationY == Integer.MIN_VALUE) {
continue;
}
baseColumns++;
boolean grounded = foundationY == area.minY();
boolean stiltColumn = false;
for (int y = foundationY - 1; y >= area.minY(); y--) {
BlockState state = world.getBlockState(position.set(column.x(), y, column.z()));
if (state.is(expectedStilt.getBlock())) {
stiltBlocks++;
stiltColumn = true;
if (y == area.minY()) {
grounded = true;
}
continue;
}
boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES);
grounded = state.isSolid() && !vegetation;
break;
}
if (stiltColumn) {
stiltColumns++;
}
if (!grounded) {
unsupportedColumns++;
}
}
return new StiltSupportAudit(baseColumns, stiltBlocks, stiltColumns, unsupportedColumns);
}
private static int findFoundationY(WorldGenLevel world, FoundationColumn column,
BlockPos.MutableBlockPos position) {
for (int cell = 0; cell < column.ys().length; cell++) {
@@ -293,7 +878,7 @@ public final class NativeStructurePostProcessor {
@FunctionalInterface
public interface StiltBlockResolver {
BlockState resolve(IrisVanillaStructureStiltSettings settings, RNG rng, int x, int y, int z);
BlockState resolve(IrisStructureStiltSettings settings, RNG rng, int x, int y, int z);
}
private record FoundationColumn(int x, int z, int[] ys) {
@@ -302,6 +887,30 @@ public final class NativeStructurePostProcessor {
public record VegetationTarget(StructureStart start, boolean force) {
}
public record StiltSupportAudit(int baseColumns, int stiltBlocks, int stiltColumns,
int unsupportedColumns) {
}
private record VegetationSnapshot(BitSet[] columns, int[] lowestY, int treeBlockCount) {
}
record SurfaceAnchor(int minX, int maxX, int minZ, int maxZ, int meetY, int strength) {
}
private record SurfaceMaterials(BlockState surface, BlockState subsurface) {
}
private static final class MonumentChildPiecesAccess {
private static final Field FIELD = resolveMonumentChildPiecesField();
private MonumentChildPiecesAccess() {
}
}
private static final class SinglePoolTemplateAccess {
private static final Field FIELD = resolveSinglePoolTemplateField();
private SinglePoolTemplateAccess() {
}
}
}
@@ -75,7 +75,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (registry == null) {
throw new IllegalStateException("Iris cannot create structure state without the biome registry");
}
Set<String> generatedBiomeKeys = exactStructureBiomeKeys();
Set<String> generatedBiomeKeys = requireConfiguredStructureBiomeKeys(exactStructureBiomeKeys());
Set<String> missingBiomeKeys = new LinkedHashSet<>(generatedBiomeKeys);
missingBiomeKeys.removeAll(registeredBiomeKeys(registry));
if (!missingBiomeKeys.isEmpty()) {
@@ -101,21 +101,32 @@ final class IrisModdedBiomeSource extends BiomeSource {
@Override
protected Stream<Holder<Biome>> collectPossibleBiomes() {
Set<String> generatedBiomeKeys = requireConfiguredStructureBiomeKeys(exactStructureBiomeKeys());
Registry<Biome> registry = biomeRegistry();
if (registry == null) {
return serializedSource.possibleBiomes().stream();
}
Set<String> generatedBiomeKeys = possibleStructureBiomeKeys();
LinkedHashSet<Holder<Biome>> possible = new LinkedHashSet<>();
Holder<Biome> fallback = fallbackHolder(registry);
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
String key = holderKey(reference);
if (isGeneratedBiomeKey(key, generatedBiomeKeys)) {
possible.add(reference);
if (registry == null) {
for (Holder<Biome> biome : serializedSource.possibleBiomes()) {
if (isGeneratedBiomeKey(holderKey(biome), generatedBiomeKeys)) {
possible.add(biome);
}
}
});
if (possible.isEmpty() && fallback != null) {
possible.add(fallback);
} else {
Set<String> missingBiomeKeys = new LinkedHashSet<>(generatedBiomeKeys);
missingBiomeKeys.removeAll(registeredBiomeKeys(registry));
if (!missingBiomeKeys.isEmpty()) {
throw new IllegalStateException("Iris structure biomes are not registered: "
+ missingBiomeKeys);
}
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
if (isGeneratedBiomeKey(holderKey(reference), generatedBiomeKeys)) {
possible.add(reference);
}
});
}
if (possible.isEmpty()) {
String phase = registry == null ? "serialized biome bootstrap" : "biome registry";
throw new IllegalStateException("Iris configured structure biomes are absent from the "
+ phase + ": " + generatedBiomeKeys);
}
return possible.stream();
}
@@ -220,31 +231,44 @@ final class IrisModdedBiomeSource extends BiomeSource {
private Holder<Biome> resolveSurfaceStructureBiome(Engine engine, int quartX, int quartZ,
Climate.Sampler sampler) {
Registry<Biome> registry = biomeRegistry();
if (!isReady(engine) || registry == null) {
return serializedSource.getNoiseBiome(quartX, 0, quartZ, sampler);
if (!isReady(engine)) {
throw new IllegalStateException("Iris structure biome lookup ran before engine binding");
}
if (registry == null) {
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
}
IrisBiome irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
Holder<Biome> resolved = irisBiome == null ? null : resolveHolder(registry, irisBiome.getVanillaDerivativeKey());
return resolved == null ? serializedSource.getNoiseBiome(quartX, 0, quartZ, sampler) : resolved;
if (irisBiome == null) {
throw new IllegalStateException("Iris returned no surface structure biome at quart "
+ quartX + "," + quartZ);
}
Holder<Biome> resolved = resolveHolder(registry, irisBiome.getVanillaDerivativeKey());
if (resolved == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ irisBiome.getVanillaDerivativeKey() + "' is not registered at quart "
+ quartX + "," + quartZ);
}
return resolved;
}
private Holder<Biome> resolveStructureBiome(Engine engine, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
Registry<Biome> registry = biomeRegistry();
BiomeResolution resolution = resolveBiomeResolution(engine, quartX, quartY, quartZ);
if (resolution == null || registry == null) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
if (resolution == null) {
throw new IllegalStateException("Iris returned no structure biome at quart "
+ quartX + "," + quartY + "," + quartZ);
}
if (registry == null) {
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
}
Holder<Biome> resolved = resolveHolder(registry, resolution.irisBiome().getVanillaDerivativeKey());
if (resolved == null && resolution.irisBiome().isCustom()) {
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
if (customBiome != null) {
resolved = resolveHolder(registry, engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT)
+ ":" + customBiome.getId().toLowerCase(Locale.ROOT));
}
if (resolved == null) {
throw new IllegalStateException("Iris structure biome derivative '"
+ resolution.irisBiome().getVanillaDerivativeKey() + "' is not registered at block "
+ resolution.blockX() + "," + resolution.blockY() + "," + resolution.blockZ());
}
return resolved == null ? fallbackBiome(registry, quartX, quartY, quartZ, sampler) : resolved;
return resolved;
}
private Holder<Biome> resolveVisibleBiome(Engine engine, int quartX, int quartY, int quartZ,
@@ -349,6 +373,13 @@ final class IrisModdedBiomeSource extends BiomeSource {
return key != null && generatedBiomeKeys.contains(key.toLowerCase(Locale.ROOT));
}
static Set<String> requireConfiguredStructureBiomeKeys(Set<String> configuredBiomeKeys) {
if (configuredBiomeKeys == null || configuredBiomeKeys.isEmpty()) {
throw new IllegalStateException("Iris has no configured structure biomes");
}
return configuredBiomeKeys;
}
static boolean isGuaranteedSurfaceBiome(int quartY, int minHeight) {
int internalY = (quartY << 2) - minHeight;
int caveSwitchY = Math.max(-8 - minHeight, 40);
@@ -377,15 +408,15 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (cached != null) {
return cached;
}
LinkedHashSet<String> possible = new LinkedHashSet<>(exactStructureBiomeKeys());
Set<String> possible = requireConfiguredStructureBiomeKeys(exactStructureBiomeKeys());
Registry<Biome> registry = biomeRegistry();
if (registry != null) {
Set<String> registered = registeredBiomeKeys(registry);
Holder<Biome> fallback = fallbackHolder(registry);
String fallbackKey = fallback == null ? null : holderKey(fallback);
if (fallbackKey != null && requiresPossibleBiomeFallback(possible, registered)) {
possible.add(fallbackKey);
}
if (registry == null) {
throw new IllegalStateException("Iris cannot resolve structure biomes without the biome registry");
}
Set<String> missing = new LinkedHashSet<>(possible);
missing.removeAll(registeredBiomeKeys(registry));
if (!missing.isEmpty()) {
throw new IllegalStateException("Iris structure biomes are not registered: " + missing);
}
Set<String> resolved = Set.copyOf(possible);
possibleStructureBiomeKeys = resolved;
@@ -393,34 +424,31 @@ final class IrisModdedBiomeSource extends BiomeSource {
}
private Set<String> exactStructureBiomeKeys() {
IrisModdedChunkGenerator current = generator;
if (current == null) {
return Set.of();
}
Engine engine = current.structureEngineOrNull();
if (!isReady(engine)) {
return current.configuredStructureBiomeKeys();
}
LinkedHashSet<String> possible = new LinkedHashSet<>();
Engine engine = engineOrNull();
if (isReady(engine)) {
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : engine.getAllBiomes()) {
String derivative = normalizeKey(irisBiome.getVanillaDerivativeKey());
if (derivative != null) {
possible.add(derivative);
}
if (!irisBiome.isCustom()) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
possible.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
}
String namespace = engine.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : engine.getAllBiomes()) {
String derivative = normalizeKey(irisBiome.getVanillaDerivativeKey());
if (derivative != null) {
possible.add(derivative);
}
if (!irisBiome.isCustom()) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
possible.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
}
}
IrisModdedChunkGenerator current = generator;
if (current != null) {
possible.addAll(current.configuredStructureBiomeKeys());
}
return Set.copyOf(possible);
}
static boolean requiresPossibleBiomeFallback(Set<String> configured, Set<String> registered) {
return configured.isEmpty() || !registered.containsAll(configured);
}
private static Set<String> registeredBiomeKeys(Registry<Biome> registry) {
Set<String> registered = new HashSet<>();
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
@@ -459,18 +487,6 @@ final class IrisModdedBiomeSource extends BiomeSource {
return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT);
}
private static Holder<Biome> fallbackHolder(Registry<Biome> registry) {
if (registry == null) {
return null;
}
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
if (plains != null) {
return plains;
}
return registry.listElements().findFirst().<Holder<Biome>>map(
(Holder.Reference<Biome> reference) -> reference).orElse(null);
}
private Holder<Biome> fallbackBiome(Registry<Biome> registry, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
@@ -19,15 +19,19 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisVanillaStructureStiltSettings;
import art.arcane.iris.engine.object.IrisStructureStiltSettings;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
import art.arcane.iris.nativegen.NativeStructureLocateResults;
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
@@ -99,6 +103,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@@ -208,11 +213,62 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
public synchronized void repoint(String pack, String packDimensionKey, long seed) {
ServerLevel level = boundLevel();
if (level != null) {
ModdedWorldEngines.evict(level);
repointAndBind(level, pack, packDimensionKey, seed);
return;
}
applyUnboundConfiguration(pack, packDimensionKey, seed);
}
synchronized void repointAndBind(ServerLevel level, String pack, String packDimensionKey, long seed) {
if (level.getChunkSource().getGenerator() != this) {
throw new IllegalArgumentException("ServerLevel does not use Iris generator '" + dimensionKey + "'");
}
requireGlobalStructureGeneration(
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
Engine replacement = ModdedWorldEngines.prepareReplacement(level, pack, packDimensionKey, seed);
try {
ModdedWorldEngines.installReplacement(level, replacement);
} catch (Throwable error) {
try {
ModdedWorldEngines.closeUnregistered(replacement);
} catch (Throwable cleanupError) {
error.addSuppressed(cleanupError);
}
if (error instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (error instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to replace its engine", error);
}
this.activePack = pack;
this.activeDimensionKey = packDimensionKey;
this.seedOverride = seed;
this.engine = replacement;
this.configuredStructureBiomeKeys = null;
this.configuredPack = null;
this.engineBinding.reset();
this.engineBinding.complete(replacement);
this.announced.set(false);
this.structureBiomeSource.clearCaches();
this.worldCheckStructureShifts.clear();
resetVanillaSpawnBiomes();
}
public synchronized void unbindEngine() {
ServerLevel level = boundLevel();
unbindEngine(level);
}
synchronized void unbindEngine(ServerLevel level) {
if (level != null) {
ModdedWorldEngines.evictOrThrow(level);
}
clearEngineBinding();
}
private void clearEngineBinding() {
this.engine = null;
this.configuredStructureBiomeKeys = null;
this.configuredPack = null;
@@ -223,11 +279,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
resetVanillaSpawnBiomes();
}
public synchronized void unbindEngine() {
ServerLevel level = boundLevel();
if (level != null) {
ModdedWorldEngines.evict(level);
}
private void applyUnboundConfiguration(String pack, String packDimensionKey, long seed) {
this.activePack = pack;
this.activeDimensionKey = packDimensionKey;
this.seedOverride = seed;
this.engine = null;
this.configuredStructureBiomeKeys = null;
this.configuredPack = null;
@@ -265,33 +320,14 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders,
BlockPos pos, int radius,
boolean findUnexplored) {
Engine current = engineOrNull();
if (current == null) {
return super.findNearestMapStructure(level, holders, pos, radius, findUnexplored);
}
try {
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
level, holders, pos, Math.max(1, radius), current);
if (irisPlaced != null) {
return irisPlaced;
}
} catch (Throwable e) {
LOGGER.error("Iris-placed structure locate failed near {},{}", pos.getX(), pos.getZ(), e);
}
IrisImportedStructureControl control = current.getDimension().getImportedStructures();
if (control == null || !control.active()) {
return null;
}
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current, control);
if (reachable.size() == 0) {
return null;
}
try {
return super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
} catch (Throwable e) {
LOGGER.error("Native structure locate failed near {},{}", pos.getX(), pos.getZ(), e);
return null;
}
Engine current = engine();
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
level, holders, pos, Math.max(1, radius), findUnexplored, current);
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current);
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable.size() == 0
? null
: super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
return NativeStructureLocateResults.nearest(pos, irisPlaced, nativeLocated);
}
public boolean isNativeStructureReachable(Holder<Structure> structure) {
@@ -300,8 +336,11 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
HolderSet<Structure> holders,
BlockPos pos, int radius,
BlockPos pos, int radius, boolean findUnexplored,
Engine current) {
if (findUnexplored) {
return null;
}
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
BlockPos best = null;
Holder<Structure> bestHolder = null;
@@ -309,12 +348,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
for (Holder<Structure> holder : holders) {
Identifier id = registry.getKey(holder.value());
if (id == null) {
throw new IllegalStateException("Native structure locate received an unregistered structure holder");
}
String structureId = id.toString();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(current,
structureId, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
if (decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
continue;
}
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
current, id.toString(), pos.getX(), pos.getZ(), radius);
current, structureId, pos.getX(), pos.getZ(), radius);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
continue;
throw new IllegalStateException("Iris structure locate reached its safety limit for "
+ structureId + " within " + radius + " chunks");
}
if (!result.found()) {
continue;
@@ -332,28 +378,23 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
private HolderSet<Structure> filterReachableNativeStructures(ServerLevel level, HolderSet<Structure> holders,
Engine current,
IrisImportedStructureControl control) {
try {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<Holder<Structure>> kept = new ArrayList<>(holders.size());
for (Holder<Structure> holder : holders) {
Identifier id = registry.getKey(holder.value());
String key = id == null ? null : id.toString();
IrisNativeStructureDecision decision = control.resolve(
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
if (key == null || !decision.generate()
|| IrisStructureLocator.suppressesVanilla(current, key)
|| !structureBiomeSource.isStructureReachable(holder)) {
continue;
}
kept.add(holder);
Engine current) {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<Holder<Structure>> kept = new ArrayList<>(holders.size());
for (Holder<Structure> holder : holders) {
Identifier id = registry.getKey(holder.value());
if (id == null) {
throw new IllegalStateException("Native structure filtering received an unregistered structure holder");
}
return kept.size() == holders.size() ? holders : HolderSet.direct(kept);
} catch (Throwable error) {
LOGGER.error("Iris could not resolve native structure biome reachability; native locate failed closed", error);
return HolderSet.direct(List.of());
String key = id.toString();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(current,
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
if (!decision.generate() || !structureBiomeSource.isStructureReachable(holder)) {
continue;
}
kept.add(holder);
}
return kept.size() == holders.size() ? holders : HolderSet.direct(kept);
}
private ServerLevel boundLevel() {
@@ -389,6 +430,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
private Engine bindEngine(ServerLevel level) {
try {
requireGlobalStructureGeneration(
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
} catch (RuntimeException error) {
engineBinding.fail(error);
throw error;
}
Engine cached = engine;
if (cached != null && !cached.isClosed() && cached.getComplex() != null) {
engineBinding.complete(cached);
@@ -424,6 +472,15 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
}
static void requireGlobalStructureGeneration(boolean enabled, String dimensionKey) {
if (enabled) {
return;
}
throw new IllegalStateException("Iris generator '" + dimensionKey
+ "' cannot bind while generate-structures=false; set generate-structures=true, restart the server, "
+ "and deny individual structures through importedStructures.disabled");
}
private Engine engineOrNull() {
Engine cached = engine;
if (cached != null) {
@@ -437,7 +494,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
Engine structureEngineOrNull() {
return engineOrNull();
engineBinding.throwIfFailed(dimensionKey);
Engine cached = engine;
if (cached != null && !cached.isClosed() && cached.getComplex() != null) {
return cached;
}
ServerLevel level = boundLevel();
return level == null ? null : bindEngine(level);
}
Engine awaitStructureEngine() {
@@ -523,6 +586,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
if (configuredPack != null) {
return configuredPack;
}
PackValidationRegistry.requireLoadable(activePack);
File packDirectory = ModdedWorldEngines.resolvePack(activePack, activeDimensionKey);
IrisData data = IrisData.get(packDirectory);
IrisDimension dimension = data.getDimensionLoader().load(activeDimensionKey);
@@ -807,19 +871,15 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
if (importedControl().active()) {
placeVanillaStructures(level, chunk, structureManager);
}
placeVanillaStructures(level, chunk, structureManager);
}
@Override
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
if (!importedControl().active()) {
return;
}
Engine current = engine();
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey);
adjustGeneratedStructures(registryAccess, chunk, previousStarts);
adjustGeneratedStructures(registryAccess, chunk, previousStarts, current);
}
@Override
@@ -828,9 +888,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess chunk,
Map<Structure, StructureStart> previousStarts) {
Map<Structure, StructureStart> previousStarts, Engine current) {
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
IrisImportedStructureControl control = importedControl();
ChunkPos chunkPos = chunk.getPos();
for (Map.Entry<Structure, StructureStart> entry : chunk.getAllStarts().entrySet()) {
Structure structure = entry.getKey();
StructureStart start = entry.getValue();
@@ -839,24 +899,37 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
IrisNativeStructureDecision decision = control.resolve(
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
if (!decision.generate() || IrisStructureLocator.suppressesVanilla(engine(), structureId)) {
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
boolean undergroundStep = NativeStructurePostProcessor.isUndergroundStep(structure.step());
IrisNativeStructureDecision decision;
try {
decision = NativeStructureGenerationPolicy.resolve(current,
structureId, undergroundStep);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"policy resolution", structureId, chunkPos.x(), chunkPos.z(), error);
}
if (!decision.generate()) {
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
continue;
}
int offsetY;
try {
offsetY = NativeStructurePostProcessor.applyVerticalShift(
offsetY = NativeStructurePostProcessor.applyVerticalPlacement(
start,
structureId,
decision.yShift(),
getSeaLevel(),
chunk.getMinY(),
chunk.getMinY() + chunk.getHeight());
} catch (RuntimeException error) {
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
LOGGER.error("Iris rejected native structure {} in chunk {},{} because its vertical bounds are invalid",
structureId, chunk.getPos().x(), chunk.getPos().z(), error);
continue;
chunk.getMinY() + chunk.getHeight(),
undergroundStep,
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error);
}
recordWorldCheckStructureShift(structureId, start.getChunkPos(), offsetY);
}
@@ -864,7 +937,11 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
if (!structureManager.shouldGenerateStructures()) {
return;
ChunkPos disabledChunk = chunk.getPos();
throw new IllegalStateException("Iris cannot generate native structures in chunk "
+ disabledChunk.x() + "," + disabledChunk.z()
+ " because generate-structures=false disables them outside the pack; set generate-structures=true, "
+ "restart the server, and deny individual structures through importedStructures.disabled");
}
ChunkPos chunkPos = chunk.getPos();
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
@@ -875,42 +952,61 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
long decorationSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
BoundingBox area = writableArea(chunk);
int steps = GenerationStep.Decoration.values().length;
IrisImportedStructureControl control = importedControl();
Engine current = engine();
List<NativePlacementGroup> placementGroups = new ArrayList<>();
List<StructureStart> nativeStarts = new ArrayList<>();
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
for (int step = 0; step < steps; step++) {
int index = 0;
for (Structure structure : byStep.get(step)) {
Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
IrisNativeStructureDecision decision = control.resolve(
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
if (decision.generate() && !IrisStructureLocator.suppressesVanilla(engine(), structureId)) {
try {
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
try {
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(current,
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
if (decision.generate()) {
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
if (!starts.isEmpty()) {
List<StructureStart> resolvedStarts = List.copyOf(starts);
placementGroups.add(new NativePlacementGroup(
structureId, decision, index, step, resolvedStarts));
nativeStarts.addAll(resolvedStarts);
boolean clearEntireFootprint = NativeStructurePostProcessor
.shouldClearEntireVegetationFootprint(
structure.step(), decision.clearVegetation());
for (StructureStart start : resolvedStarts) {
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
start, decision.clearVegetation()));
start, clearEntireFootprint));
}
}
} catch (Throwable e) {
LOGGER.error("Iris failed to resolve native structure {} in chunk {},{}",
structureId, chunkPos.x(), chunkPos.z(), e);
}
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"resolution", structureId, chunkPos.x(), chunkPos.z(), error);
}
index++;
}
}
try {
NativeStructurePostProcessor.prepareSurfaceStructures(
world, area, nativeStarts,
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain integration", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructurePostProcessor.clearIntersectingVegetation(
world, chunk, area, vegetationTargets);
} catch (Throwable e) {
LOGGER.error("Iris failed to clear vegetation from native structures in chunk {},{}",
chunkPos.x(), chunkPos.z(), e);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
for (NativePlacementGroup group : placementGroups) {
random.setFeatureSeed(decorationSeed, group.featureIndex(), group.step());
@@ -919,13 +1015,27 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
placeVanillaStructure(world, structureManager, random, area, chunkPos,
group.structureId(), start, group.decision());
}
} catch (Throwable e) {
LOGGER.error("Iris failed to place native structure {} in chunk {},{}",
group.structureId(), chunkPos.x(), chunkPos.z(), e);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"placement", group.structureId(), chunkPos.x(), chunkPos.z(), error);
}
}
}
private static String nativeStructureBatchContext(List<NativePlacementGroup> placementGroups) {
if (placementGroups.isEmpty()) {
return "<no resolved native structures>";
}
StringBuilder context = new StringBuilder("[");
for (int i = 0; i < placementGroups.size(); i++) {
if (i > 0) {
context.append(", ");
}
context.append(placementGroups.get(i).structureId());
}
return context.append(']').toString();
}
private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager,
WorldgenRandom random, BoundingBox area, ChunkPos chunkPos,
String structureId, StructureStart start,
@@ -978,22 +1088,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return worldCheckStructureShifts.get(new NativeStructureStartKey(structureId, startChunk.pack()));
}
private BlockState resolveStiltBlock(IrisVanillaStructureStiltSettings settings, RNG rng,
private BlockState resolveStiltBlock(IrisStructureStiltSettings settings, RNG rng,
int x, int y, int z) {
if (settings.getPalette() == null) {
return Blocks.COBBLESTONE.defaultBlockState();
}
PlatformBlockState platformState = settings.getPalette().get(rng, x, y, z, engine().getData());
if (platformState == null || !(platformState.nativeHandle() instanceof BlockState blockState)) {
return Blocks.COBBLESTONE.defaultBlockState();
throw new IllegalStateException("Configured native structure stilt palette did not resolve a Minecraft block at "
+ x + "," + y + "," + z);
}
return blockState;
}
private IrisImportedStructureControl importedControl() {
return engine().getDimension().getImportedStructures();
}
private BoundingBox writableArea(ChunkAccess chunk) {
ChunkPos chunkPos = chunk.getPos();
int minX = chunkPos.getMinBlockX();
@@ -1128,6 +1235,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
future.completeExceptionally(error);
}
void throwIfFailed(String dimensionKey) {
CompletableFuture<T> current = future;
if (!current.isCompletedExceptionally()) {
return;
}
try {
current.join();
} catch (CompletionException error) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind",
error.getCause());
}
}
void reset() {
future = new CompletableFuture<>();
}
@@ -20,6 +20,7 @@ package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisBlockDrops;
@@ -145,7 +146,7 @@ public final class ModdedBlockBreakHandler {
for (IrisBlockDrops provider : providers) {
replaceVanillaDrops |= provider.isReplaceVanillaDrops();
for (IrisLoot loot : provider.getDrops()) {
if (RNG.r.i(1, loot.getRarity()) != loot.getRarity()) {
if (!LootResolver.oneIn(RNG.r, loot.getRarity())) {
continue;
}
ItemStack stack = ModdedItemTranslator.stack(loot, RNG.r, level);
@@ -38,7 +38,6 @@ import java.util.concurrent.ConcurrentHashMap;
public final class ModdedDeathLoot {
private static final String TAG_PREFIX = "iris_loot|";
private static final int LOOT_RNG_SALT = 345911;
private static final Set<String> WARNED_TABLES = ConcurrentHashMap.newKeySet();
private static final Set<String> WARNED_ENTITY_TYPES = ConcurrentHashMap.newKeySet();
@@ -112,8 +111,7 @@ public final class ModdedDeathLoot {
}
continue;
}
RNG lootRng = new RNG(binding.seed()).nextParallelRNG(LOOT_RNG_SALT);
drops.addAll(ModdedItemTranslator.loot(table, lootRng, InventorySlotType.STORAGE, level,
drops.addAll(ModdedItemTranslator.loot(table, engine.getSeedManager().getLoot(), InventorySlotType.STORAGE, level,
binding.spawnX(), binding.spawnY(), binding.spawnZ()));
}
return drops;
@@ -68,7 +68,13 @@ public final class ModdedDimensionManager {
private ModdedDimensionManager() {
}
public static void bindAccess(ModdedServerAccess serverAccess) {
public static synchronized ModdedServerAccess bindAccess(ModdedServerAccess serverAccess) {
ModdedServerAccess previous = access;
access = serverAccess;
return previous;
}
public static synchronized void restoreAccess(ModdedServerAccess serverAccess) {
access = serverAccess;
}
@@ -110,12 +116,13 @@ public final class ModdedDimensionManager {
}
public static Handle create(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
ModdedStartup.requirePackForWorldCreation(pack);
ModdedServerAccess serverAccess = requireAccess();
synchronized (LOCK) {
ResourceKey<Level> key = levelKey(dimensionId);
Handle existing = HANDLES.get(dimensionId);
if (existing != null && serverAccess.hasLevel(server, key)) {
existing.generator().repoint(pack, packDimensionKey, seed);
existing.generator().repointAndBind(existing.level(), pack, packDimensionKey, seed);
Handle refreshed = new Handle(dimensionId, pack, packDimensionKey, seed, existing.level(), existing.generator());
HANDLES.put(dimensionId, refreshed);
return refreshed;
@@ -126,7 +133,7 @@ public final class ModdedDimensionManager {
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + "': a non-Iris level with that id is already loaded");
}
LOGGER.warn("Iris dimension '{}' is already present in the running server; reusing it", dimensionId);
generator.repoint(pack, packDimensionKey, seed);
generator.repointAndBind(present, pack, packDimensionKey, seed);
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, present, generator);
HANDLES.put(dimensionId, handle);
return handle;
@@ -231,10 +238,6 @@ public final class ModdedDimensionManager {
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) {
Registry<DimensionType> registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE);
IrisDimension dimension = loadPackDimension(pack, packDimensionKey);
if (dimension == null) {
throw new IllegalStateException("Iris cannot resolve dimension type for missing pack dimension '"
+ packDimensionKey + "' in pack '" + pack + "'");
}
String typeRef = ModdedForcedDatapack.dimensionTypeRef(dimension);
ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef));
Holder.Reference<DimensionType> packType = ModdedForcedDatapack.requireRegisteredDimensionType(
@@ -246,12 +249,22 @@ public final class ModdedDimensionManager {
try {
File packFolder = ModdedWorldEngines.packFolder(pack);
if (!packFolder.isDirectory()) {
return null;
throw new IllegalStateException("Iris pack directory is missing: " + packFolder.getAbsolutePath());
}
return IrisData.get(packFolder).getDimensionLoader().load(packDimensionKey);
IrisDimension dimension = IrisData.get(packFolder).getDimensionLoader().load(packDimensionKey);
if (dimension == null) {
throw new IllegalStateException("Iris pack '" + pack
+ "' does not contain dimension '" + packDimensionKey + "'");
}
return dimension;
} catch (Throwable e) {
LOGGER.warn("Iris could not load pack '{}' dimension '{}' for dimension type resolution: {}", pack, packDimensionKey, e.toString());
return null;
LOGGER.error("Iris could not load pack '{}' dimension '{}' for dimension type resolution",
pack, packDimensionKey, e);
if (e instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris could not load pack '" + pack + "' dimension '"
+ packDimensionKey + "' for dimension type resolution", e);
}
}
@@ -285,10 +298,56 @@ public final class ModdedDimensionManager {
List.of(),
false);
serverAccess.putLevel(server, key, level);
generator.bindLevel(level);
server.getPlayerList().addWorldborderListener(level);
return new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
try {
generator.bindLevel(level);
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
ServerLevel previous = serverAccess.putLevelIfAbsent(server, key, level);
if (previous != null) {
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId
+ "': the level was registered concurrently");
}
server.getPlayerList().addWorldborderListener(level);
return handle;
} catch (Throwable error) {
rollbackInjection(server, serverAccess, key, level, generator, error);
if (error instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (error instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, error);
}
}
private static void rollbackInjection(MinecraftServer server, ModdedServerAccess serverAccess,
ResourceKey<Level> key, ServerLevel level,
IrisModdedChunkGenerator generator, Throwable failure) {
try {
if (serverAccess.hasLevel(server, key)) {
ServerLevel removed = serverAccess.removeLevel(server, key);
if (removed != null && removed != level) {
serverAccess.putLevel(server, key, removed);
throw new IllegalStateException("Iris injection rollback encountered another registered level for "
+ key.identifier());
}
}
} catch (Throwable cleanupError) {
failure.addSuppressed(cleanupError);
LOGGER.error("Iris failed to remove a partially injected level for {}", key.identifier(), cleanupError);
}
try {
generator.unbindEngine(level);
} catch (Throwable cleanupError) {
failure.addSuppressed(cleanupError);
LOGGER.error("Iris failed to close a partially bound engine for {}", key.identifier(), cleanupError);
}
try {
level.close();
} catch (Throwable cleanupError) {
failure.addSuppressed(cleanupError);
LOGGER.error("Iris failed to close a partially injected level for {}", key.identifier(), cleanupError);
}
}
public static int evacuate(MinecraftServer server, ServerLevel from) {
@@ -42,6 +42,7 @@ import art.arcane.iris.modded.service.ModdedLogFilterService;
import art.arcane.iris.modded.service.ModdedPreservationService;
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import net.minecraft.core.BlockPos;
@@ -53,6 +54,8 @@ import net.minecraft.world.level.storage.LevelData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayDeque;
public final class ModdedEngineBootstrap {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final String[] CORE_SELF_TEST_CLASSES = {
@@ -61,9 +64,9 @@ public final class ModdedEngineBootstrap {
"art.arcane.iris.core.loader.IrisData"
};
private static final Object LOCK = new Object();
private static final ModdedServiceManager SERVICE_MANAGER = new ModdedServiceManager();
private static final ModdedServiceManager UNBOUND_SERVICE_MANAGER = new ModdedServiceManager();
private static volatile ModdedLoader loader;
private static volatile ModdedPlatform platform;
private static volatile BoundRuntime runtime;
private static volatile MinecraftServer currentServer;
private static volatile MinecraftServer spawnCaptureServer;
private static volatile boolean initialSpawnWasDefault;
@@ -72,19 +75,20 @@ public final class ModdedEngineBootstrap {
}
public static ModdedServiceManager services() {
return SERVICE_MANAGER;
BoundRuntime bound = runtime;
return bound == null ? UNBOUND_SERVICE_MANAGER : bound.services();
}
public static ModdedScheduler schedulerOrNull() {
ModdedPlatform bound = platform;
return bound == null ? null : bound.moddedScheduler();
BoundRuntime bound = runtime;
return bound == null ? null : bound.platform().moddedScheduler();
}
public static void tick(MinecraftServer server) {
ModdedScheduler.tick(server);
ModdedStartup.runOnce(server);
ModdedPrimaryWorldRouter.tick(server);
SERVICE_MANAGER.tick(server);
services().tick(server);
ModdedPregenBossBar.tick(server);
ModdedProtocolHandler.tickDimensionSync(server);
}
@@ -93,14 +97,15 @@ public final class ModdedEngineBootstrap {
captureInitialSpawn(server);
currentServer = server;
bind();
bindWorldGenerators(server);
ModdedStartup.reset();
ModdedScheduler scheduler = schedulerOrNull();
if (scheduler != null) {
scheduler.reset();
}
ModdedStartup.prepareForStartup();
IrisModdedChunkGenerator.startGenPool();
SERVICE_MANAGER.enableAll();
bindWorldGenerators(server);
ModdedStartup.runOnce(server);
services().enableAll();
ModdedProtocolHandler.start(server);
ModdedSentry.start(loader());
}
@@ -115,6 +120,7 @@ public final class ModdedEngineBootstrap {
public static void serverAboutToStart(MinecraftServer server) {
captureInitialSpawn(server);
ModdedStartup.prepareForStartup();
}
public static void serverStarted(MinecraftServer server) {
@@ -140,7 +146,7 @@ public final class ModdedEngineBootstrap {
ModdedStudioCommands.clear();
ModdedWorldEngines.shutdown();
ModdedPrimaryWorldRouter.clear();
SERVICE_MANAGER.disableAll();
services().disableAll();
ModdedDimensionManager.clear();
ModdedScheduler scheduler = schedulerOrNull();
if (scheduler != null) {
@@ -256,45 +262,141 @@ public final class ModdedEngineBootstrap {
}
public static ModdedPlatform bind() {
ModdedPlatform bound = platform;
BoundRuntime bound = runtime;
if (bound != null) {
return bound;
return bound.platform();
}
synchronized (LOCK) {
if (platform != null) {
return platform;
BoundRuntime synchronizedBound = runtime;
if (synchronizedBound != null) {
return synchronizedBound.platform();
}
ModdedLoader boundLoader = loader();
GuiHost.suppressDesktop(boundLoader.clientEnvironment());
ModdedPlatform created = new ModdedPlatform(boundLoader);
IrisPlatforms.bind(created);
ModdedDimensionManager.bindAccess(new ModdedServerLevels());
IrisObjectRotation.bindFallbackRotator(new ModdedStateRotator());
BlockDataMergeSupport.bindFallbackMerger(new ModdedStateMerger());
TileData.bindFallbackReader(new ModdedTileReader(boundLoader::currentServer));
TileData.bindFallbackFactory(ModdedTileData::fromProperties);
ModdedGuiHost.install();
ModdedDecoratorHooks decoratorHooks = new ModdedDecoratorHooks();
DecoratorPlatformHooks.bind(decoratorHooks, decoratorHooks);
ModdedPreservationService preservation = SERVICE_MANAGER.register(ModdedPreservationService.class, new ModdedPreservationService());
SERVICE_MANAGER.register(ModdedLogFilterService.class, new ModdedLogFilterService());
SERVICE_MANAGER.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService());
SERVICE_MANAGER.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
ModdedStudioHotloadService studioHotloadService = SERVICE_MANAGER.register(ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
SERVICE_MANAGER.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
SERVICE_MANAGER.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService());
IrisServices.register(PreservationRegistry.class, preservation);
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) ModdedEngineEffects::new);
IrisServices.register(EnginePlatformHooks.class, studioHotloadService);
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new ModdedWorldManager(engine));
ModdedCustomContentRegistry.discover();
platform = created;
SERVICE_MANAGER.enableAll();
if (boundLoader.clientEnvironment()) {
ModdedStartup.prefetchDefaultPack();
ModdedServiceManager createdServices = new ModdedServiceManager();
BindRollback rollback = new BindRollback();
try {
boolean previousDesktopSuppression = GuiHost.isDesktopSuppressed();
GuiHost.suppressDesktop(boundLoader.clientEnvironment());
rollback.add(() -> GuiHost.suppressDesktop(previousDesktopSuppression));
IrisPlatform previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
IrisPlatforms.bind(created);
rollback.add(() -> restorePlatform(previousPlatform));
ModdedServerAccess previousServerAccess = ModdedDimensionManager.bindAccess(new ModdedServerLevels());
rollback.add(() -> ModdedDimensionManager.restoreAccess(previousServerAccess));
IrisObjectRotation.StateRotator previousRotator = IrisObjectRotation.bindPlatformRotator(new ModdedStateRotator());
rollback.add(() -> IrisObjectRotation.restorePlatformRotator(previousRotator));
BlockDataMergeSupport.StateMerger previousMerger = BlockDataMergeSupport.bindPlatformMerger(new ModdedStateMerger());
rollback.add(() -> BlockDataMergeSupport.restorePlatformMerger(previousMerger));
TileData.TileReader previousTileReader = TileData.bindPlatformReader(new ModdedTileReader(boundLoader::currentServer));
rollback.add(() -> TileData.restorePlatformReader(previousTileReader));
TileData.TileFactory previousTileFactory = TileData.bindPlatformFactory(ModdedTileData::fromProperties);
rollback.add(() -> TileData.restorePlatformFactory(previousTileFactory));
GuiHost.Provider previousGuiHost = GuiHost.get();
ModdedGuiHost.install();
rollback.add(() -> GuiHost.set(previousGuiHost));
ModdedDecoratorHooks decoratorHooks = new ModdedDecoratorHooks();
DecoratorPlatformHooks.Bindings previousDecoratorBindings = DecoratorPlatformHooks.bind(decoratorHooks, decoratorHooks);
rollback.add(() -> DecoratorPlatformHooks.restore(previousDecoratorBindings));
ModdedPreservationService preservation = createdServices.register(
ModdedPreservationService.class, new ModdedPreservationService());
createdServices.register(ModdedLogFilterService.class, new ModdedLogFilterService());
createdServices.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService());
createdServices.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
ModdedStudioHotloadService studioHotloadService = createdServices.register(
ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
createdServices.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
createdServices.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService());
bindService(PreservationRegistry.class, preservation, rollback);
bindService(EngineEffectsProvider.class,
(EngineEffectsProvider) ModdedEngineEffects::new, rollback);
bindService(EnginePlatformHooks.class, studioHotloadService, rollback);
bindService(EngineWorldManagerProvider.class,
(EngineWorldManagerProvider) (Engine engine) -> new ModdedWorldManager(engine), rollback);
ModdedCustomContentRegistry.Discovery customContentDiscovery =
ModdedCustomContentRegistry.discover();
rollback.add(customContentDiscovery::rollback);
ModdedIrisSplash.print(boundLoader);
createdServices.enableAll();
runtime = new BoundRuntime(created, createdServices);
rollback.clear();
return created;
} catch (Throwable failure) {
createdServices.rollback(failure);
rollback.restore(failure);
LOGGER.error("Iris modded platform binding failed", failure);
if (failure instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (failure instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris modded platform binding failed", failure);
}
ModdedIrisSplash.print(boundLoader);
return created;
}
}
private static <T> void bindService(Class<T> type, T service, BindRollback rollback) {
T previous = IrisServices.getOrNull(type);
IrisServices.register(type, service);
rollback.add(() -> restoreService(type, previous));
}
private static <T> void restoreService(Class<T> type, T service) {
if (service == null) {
IrisServices.remove(type);
return;
}
IrisServices.register(type, service);
}
private static void restorePlatform(IrisPlatform platform) {
IrisPlatforms.unbind();
if (platform != null) {
IrisPlatforms.bind(platform);
}
}
private record BoundRuntime(ModdedPlatform platform, ModdedServiceManager services) {
}
@FunctionalInterface
private interface RollbackAction {
void restore() throws Throwable;
}
private static final class BindRollback {
private final ArrayDeque<RollbackAction> actions = new ArrayDeque<>();
void add(RollbackAction action) {
actions.push(action);
}
void restore(Throwable failure) {
while (!actions.isEmpty()) {
try {
actions.pop().restore();
} catch (Throwable rollbackFailure) {
if (rollbackFailure != failure) {
failure.addSuppressed(rollbackFailure);
}
}
}
}
void clear() {
actions.clear();
}
}
}
@@ -20,6 +20,7 @@ package art.arcane.iris.modded;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.object.IrisAttributeModifier;
import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.iris.engine.object.IrisEffect;
@@ -241,7 +242,7 @@ public final class ModdedEntitySpawner {
}
private static void setSlot(LivingEntity living, EquipmentSlot slot, IrisLoot loot, ServerLevel level, RNG rng) {
if (loot == null || rng.i(1, loot.getRarity()) != 1) {
if (loot == null || !LootResolver.oneIn(rng, loot.getRarity())) {
return;
}
ItemStack stack = ModdedItemTranslator.stack(loot, rng, level);
@@ -41,12 +41,15 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Stream;
@@ -62,9 +65,7 @@ public final class ModdedForcedDatapack {
public static RepositorySource repositorySource() {
return (Consumer<Pack> consumer) -> {
Pack pack = buildPack();
if (pack != null) {
consumer.accept(pack);
}
consumer.accept(pack);
};
}
@@ -78,9 +79,10 @@ public final class ModdedForcedDatapack {
private static Pack buildPack() {
Path directory = regenerate();
if (directory == null) {
return null;
}
return requireReadablePack(directory);
}
private static Pack requireReadablePack(Path directory) {
PackLocationInfo location = new PackLocationInfo(
PACK_ID,
Component.literal("Iris World Generation"),
@@ -90,7 +92,8 @@ public final class ModdedForcedDatapack {
PathPackResources.PathResourcesSupplier supplier = new PathPackResources.PathResourcesSupplier(directory);
Pack pack = Pack.readMetaAndCreate(location, supplier, PackType.SERVER_DATA, selection);
if (pack == null) {
LOGGER.error("Iris forced datapack at {} produced no readable pack metadata", directory);
throw new IllegalStateException("Iris forced datapack at " + directory
+ " produced no readable pack metadata");
}
return pack;
}
@@ -99,24 +102,41 @@ public final class ModdedForcedDatapack {
synchronized (LOCK) {
try {
return write();
} catch (IOException e) {
} catch (Throwable e) {
LOGGER.error("Iris failed to generate the forced startup datapack", e);
return null;
if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (e instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris failed to generate the forced startup datapack", e);
}
}
}
private static Path write() throws IOException {
ModdedStartup.ensureDefaultPack();
Path datapackRoot = datapackRoot();
Files.createDirectories(datapackRoot);
Path stagingDirectory = Files.createTempDirectory(datapackRoot, PACK_FOLDER + ".staging-");
try {
ModdedStartup.ensureDefaultPack();
} catch (Throwable e) {
LOGGER.warn("Iris could not ensure the default pack before building the forced datapack", e);
writeStagedPack(stagingDirectory);
requireReadablePack(stagingDirectory);
publishDirectory(stagingDirectory, packDirectory());
return packDirectory();
} catch (IOException | RuntimeException | Error failure) {
try {
clean(stagingDirectory);
} catch (Throwable cleanupError) {
failure.addSuppressed(cleanupError);
}
throw failure;
}
Path packDirectory = packDirectory();
clean(packDirectory);
Files.createDirectories(packDirectory);
}
File packFolder = packDirectory.toFile();
private static void writeStagedPack(Path stagingDirectory) throws IOException {
File packFolder = stagingDirectory.toFile();
KList<File> folders = new KList<>();
folders.add(packFolder);
Map<String, KSet<String>> seenBiomes = new LinkedHashMap<>();
@@ -124,8 +144,13 @@ public final class ModdedForcedDatapack {
int packCount = 0;
KList<String> presetIds = new KList<>();
File[] packs = packsRoot().toFile().listFiles(File::isDirectory);
File root = packsRoot().toFile();
File[] packs = root.listFiles(File::isDirectory);
if (packs == null && root.exists()) {
throw new IOException("Iris could not read installed pack directory " + root.getAbsolutePath());
}
if (packs != null) {
Arrays.sort(packs, Comparator.comparing(File::getName));
for (File pack : packs) {
if (installPack(pack, fixer, folders, seenBiomes, presetIds)) {
packCount++;
@@ -133,46 +158,52 @@ public final class ModdedForcedDatapack {
}
}
writePackMeta(packDirectory);
writePackMeta(stagingDirectory);
if ("forge".equalsIgnoreCase(ModdedEngineBootstrap.loader().platformName())) {
writeForgeBlockLootModifier(packDirectory);
writeForgeBlockLootModifier(stagingDirectory);
}
if (!presetIds.isEmpty()) {
writeWorldPresetTag(packDirectory, presetIds);
writeWorldPresetTag(stagingDirectory, presetIds);
}
LOGGER.info("Iris forced startup datapack regenerated: {} pack(s), {} world preset(s), {} custom biome(s) at {}", packCount, presetIds.size(), countBiomes(seenBiomes), packDirectory);
LOGGER.info("Iris forced startup datapack staged: {} pack(s), {} world preset(s), {} custom biome(s) at {}", packCount, presetIds.size(), countBiomes(seenBiomes), stagingDirectory);
if (packCount == 0) {
LOGGER.warn("Iris installed NO worldgen packs into the forced datapack - custom biomes and their colors will NOT generate. Install a pack (e.g. /iris download overworld) and restart the server before creating an Iris world.");
}
return packDirectory;
}
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders, Map<String, KSet<String>> seenBiomes, KList<String> presetIds) {
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders,
Map<String, KSet<String>> seenBiomes,
KList<String> presetIds) throws IOException {
String packName = packFolder.getName();
File[] dimensionFiles = new File(packFolder, "dimensions").listFiles((File file) -> file.isFile() && file.getName().endsWith(".json"));
if (dimensionFiles == null || dimensionFiles.length == 0) {
File dimensionsDirectory = new File(packFolder, "dimensions");
if (!dimensionsDirectory.isDirectory()) {
return false;
}
boolean installed = false;
File[] dimensionFiles = dimensionsDirectory.listFiles(
(File file) -> file.isFile() && file.getName().endsWith(".json"));
if (dimensionFiles == null) {
throw new IOException("Iris could not read dimensions for pack '" + packName + "'");
}
if (dimensionFiles.length == 0) {
return false;
}
Arrays.sort(dimensionFiles, Comparator.comparing(File::getName));
IrisData data = IrisData.get(packFolder);
for (File dimensionFile : dimensionFiles) {
String dimensionKey = dimensionFile.getName().substring(0, dimensionFile.getName().length() - ".json".length());
try {
IrisData data = IrisData.get(packFolder);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
continue;
}
dimension.installBiomes(fixer, () -> data, folders, biomesForNamespace(seenBiomes, dimension.getLoadKey()));
writeDimensionType(folders, fixer, dimension);
String presetKey = dimensionKey.equals(packName) ? packName : packName + "_" + dimensionKey;
writeWorldPreset(folders, dimension, packName, dimensionKey, presetKey);
presetIds.add("irisworldgen:" + presetKey);
installed = true;
} catch (Throwable e) {
LOGGER.error("Iris failed to install forced datapack content for pack '{}' dimension '{}'", packName, dimensionKey, e);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
throw new IllegalStateException("Iris pack '" + packName + "' dimension '"
+ dimensionKey + "' did not load while building the forced datapack");
}
dimension.installBiomes(fixer, () -> data, folders,
biomesForNamespace(seenBiomes, dimension.getLoadKey()));
writeDimensionType(folders, fixer, dimension);
String presetKey = dimensionKey.equals(packName) ? packName : packName + "_" + dimensionKey;
writeWorldPreset(folders, dimension, packName, dimensionKey, presetKey);
presetIds.add("irisworldgen:" + presetKey);
}
return installed;
return true;
}
static KSet<String> biomesForNamespace(Map<String, KSet<String>> biomes, String namespace) {
@@ -319,6 +350,35 @@ public final class ModdedForcedDatapack {
}
}
static void publishDirectory(Path stagingDirectory, Path publishedDirectory) throws IOException {
Path backupDirectory = publishedDirectory.resolveSibling(
publishedDirectory.getFileName() + ".backup-" + UUID.randomUUID());
boolean hadPublishedDirectory = Files.exists(publishedDirectory);
if (hadPublishedDirectory) {
Files.move(publishedDirectory, backupDirectory, StandardCopyOption.ATOMIC_MOVE);
}
try {
Files.move(stagingDirectory, publishedDirectory, StandardCopyOption.ATOMIC_MOVE);
} catch (IOException | RuntimeException | Error failure) {
if (hadPublishedDirectory) {
try {
Files.move(backupDirectory, publishedDirectory, StandardCopyOption.ATOMIC_MOVE);
} catch (Throwable rollbackError) {
failure.addSuppressed(rollbackError);
}
}
throw failure;
}
if (hadPublishedDirectory) {
try {
clean(backupDirectory);
} catch (Throwable cleanupError) {
LOGGER.warn("Iris published the forced datapack but could not remove backup {}",
backupDirectory, cleanupError);
}
}
}
private static Path packsRoot() {
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs");
}
@@ -18,12 +18,12 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.object.InventorySlotType;
import art.arcane.iris.engine.object.IrisAttributeModifier;
import art.arcane.iris.engine.object.IrisEnchantment;
import art.arcane.iris.engine.object.IrisLoot;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -76,23 +76,25 @@ public final class ModdedItemTranslator {
private ModdedItemTranslator() {
}
public static KList<ItemStack> loot(IrisLootTable table, RNG rng, InventorySlotType slot, ServerLevel level, int x, int y, int z) {
public static KList<ItemStack> loot(IrisLootTable table, long lootSeed, InventorySlotType slot, ServerLevel level, int x, int y, int z) {
KList<ItemStack> out = new KList<>();
KList<IrisLoot> entries = table.getLoot();
if (entries.isEmpty()) {
if (entries.isEmpty() || table.getMaxTries() <= 0) {
return out;
}
RNG rng = LootResolver.tableRng(lootSeed, table, x, y, z);
int m = 0;
int c = 0;
int mx = rng.i(table.getMinPicked(), table.getMaxPicked());
int mx = Math.max(0, LootResolver.inclusive(rng, table.getMinPicked(), table.getMaxPicked()));
while (m < mx && c++ < table.getMaxTries()) {
IrisLoot entry = entries.get(rng.i(entries.size()));
if (entry.getSlotTypes() != slot) {
int entryIndex = rng.nextInt(entries.size());
IrisLoot entry = entries.get(entryIndex);
if (entry == null || entry.getSlotTypes() != slot) {
continue;
}
ItemStack item = item(entry, table, rng, level, x, y, z);
ItemStack item = item(entry, table, rng, lootSeed, entryIndex, level, x, y, z);
if (item != null && !item.isEmpty()) {
out.add(item);
m++;
@@ -117,8 +119,9 @@ public final class ModdedItemTranslator {
}
}
public static ItemStack item(IrisLoot loot, IrisLootTable table, RNG rng, ServerLevel level, int x, int y, int z) {
if (loot.getChance().aquire(() -> NoiseStyle.STATIC.create(rng)).fit(1, loot.getRarity() * table.getRarity(), x, y, z) != 1) {
public static ItemStack item(IrisLoot loot, IrisLootTable table, RNG rng, long lootSeed, int entryIndex, ServerLevel level, int x, int y, int z) {
long combinedRarity = LootResolver.combinedRarity(loot.getRarity(), table.getRarity());
if (!LootResolver.spatialOneIn(lootSeed, table, entryIndex, x, y, z, combinedRarity)) {
return null;
}
@@ -150,7 +153,7 @@ public final class ModdedItemTranslator {
}
Item item = BuiltInRegistries.ITEM.getValue(id);
return new ItemStack(item, Math.max(1, rng.i(loot.getMinAmount(), loot.getMaxAmount())));
return new ItemStack(item, Math.max(1, LootResolver.inclusive(rng, loot.getMinAmount(), loot.getMaxAmount())));
}
private static void applyComponents(IrisLoot loot, ItemStack stack, RNG rng, ServerLevel level) {
@@ -19,17 +19,14 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.framework.PlacedObject;
import art.arcane.iris.engine.object.IObjectLoot;
import art.arcane.iris.engine.object.InventorySlotType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisLootMode;
import art.arcane.iris.engine.object.IrisLootReference;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.IrisObjectLoot;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectVanillaLoot;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
@@ -42,10 +39,12 @@ import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Container;
import net.minecraft.world.RandomizableContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.ChestBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.ChestType;
import net.minecraft.world.level.storage.loot.LootParams;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
@@ -60,8 +59,13 @@ public final class ModdedLootApplier {
private ModdedLootApplier() {
}
public static void apply(Engine engine, ServerLevel level, BlockPos pos, BlockState state, MantleChunk<Matter> mc, RNG rng, int localX, int localZ) {
List<LootSource> sources = resolveSources(engine, level, rng, pos, state, mc);
public static void apply(Engine engine, ServerLevel level, BlockPos pos, BlockState state, MantleChunk<Matter> mc) {
if (!isCanonicalContainer(pos, state)) {
return;
}
RNG rng = LootResolver.containerRng(engine.getSeedManager().getLoot(), pos.getX(), pos.getY(), pos.getZ());
boolean nativeLootDefined = hasNativeLootTable(level, pos, state);
List<LootSource> sources = resolveSources(engine, level, rng, pos, state, mc, nativeLootDefined);
if (sources.isEmpty()) {
return;
}
@@ -80,7 +84,7 @@ public final class ModdedLootApplier {
if (table == null) {
continue;
}
items.addAll(ModdedItemTranslator.loot(table, rng, InventorySlotType.STORAGE, level, localX, pos.getY(), localZ));
items.addAll(ModdedItemTranslator.loot(table, engine.getSeedManager().getLoot(), InventorySlotType.STORAGE, level, pos.getX(), pos.getY(), pos.getZ()));
continue;
}
if (source instanceof NativeLootSource nativeSource) {
@@ -96,15 +100,18 @@ public final class ModdedLootApplier {
fillContainer(container, items, rng);
}
private static List<LootSource> resolveSources(Engine engine, ServerLevel level, RNG rng, BlockPos pos, BlockState state, MantleChunk<Matter> mc) {
private static List<LootSource> resolveSources(Engine engine, ServerLevel level, RNG rng, BlockPos pos, BlockState state, MantleChunk<Matter> mc, boolean nativeLootDefined) {
int rx = pos.getX();
int rz = pos.getZ();
int ry = pos.getY() - engine.getWorld().minHeight();
double he = engine.getComplex().getHeightStream().get(rx, rz);
List<LootSource> sources = new ArrayList<>();
boolean objectLootDefined = nativeLootDefined;
PlacedObject po = engine.getObjectPlacement(rx, ry, rz, mc);
if (po != null && po.getPlacement() != null && ModdedBlockResolution.isStorageChest(state)) {
objectLootDefined = objectLootDefined
|| po.getPlacement().getLoot().isNotEmpty()
|| po.getPlacement().getVanillaLoot().isNotEmpty();
Candidate picked = pickPlacementTable(engine, level, po.getPlacement(), state, rng);
if (picked != null) {
sources.add(picked.source());
@@ -114,17 +121,16 @@ public final class ModdedLootApplier {
}
}
IrisRegion region = engine.getComplex().getRegionStream().get(rx, rz);
IrisBiome biomeSurface = engine.getComplex().getTrueBiomeStream().get(rx, rz);
IrisBiome biomeUnder = ry < he ? engine.getCaveBiome(rx, ry, rz) : biomeSurface;
double multiplier = 1D * engine.getDimension().getLoot().getMultiplier() * region.getLoot().getMultiplier() * biomeSurface.getLoot().getMultiplier() * biomeUnder.getLoot().getMultiplier();
boolean fallback = sources.isEmpty();
injectReferenceSources(sources, engine.getDimension().getLoot(), engine, fallback);
injectReferenceSources(sources, region.getLoot(), engine, fallback);
injectReferenceSources(sources, biomeSurface.getLoot(), engine, fallback);
injectReferenceSources(sources, biomeUnder.getLoot(), engine, fallback);
scaleSources(sources, multiplier, rng);
LootResolver.resolveEnvironmentSources(
sources,
engine,
rng,
rx,
ry,
rz,
objectLootDefined,
IrisLootSource::new
);
return sources;
}
@@ -134,7 +140,7 @@ public final class ModdedLootApplier {
List<Candidate> global = new ArrayList<>();
for (IrisObjectLoot loot : placement.getLoot()) {
if (loot == null) {
if (loot == null || loot.getWeight() <= 0) {
continue;
}
IrisLootTable table = engine.getData().getLootLoader().load(loot.getName());
@@ -146,7 +152,7 @@ public final class ModdedLootApplier {
}
for (IrisObjectVanillaLoot loot : placement.getVanillaLoot()) {
if (loot == null) {
if (loot == null || loot.getWeight() <= 0) {
continue;
}
ResourceKey<LootTable> key = resolveNativeKey(loot.getName(), candidate -> hasNativeTable(level, candidate));
@@ -162,30 +168,7 @@ public final class ModdedLootApplier {
}
List<Candidate> pool = !exact.isEmpty() ? exact : !basic.isEmpty() ? basic : global;
return pickWeighted(pool, rng);
}
static <T> void injectSources(List<T> sources, List<? extends T> additions, IrisLootMode mode, boolean fallback) {
if (mode == IrisLootMode.FALLBACK && !fallback) {
return;
}
if (mode == IrisLootMode.CLEAR || mode == IrisLootMode.REPLACE) {
sources.clear();
}
sources.addAll(additions);
}
static <T> void scaleSources(List<T> sources, double multiplier, RNG rng) {
if (sources.isEmpty()) {
return;
}
int target = (int) Math.round(sources.size() * multiplier);
while (sources.size() < target && !sources.isEmpty()) {
sources.add(sources.get(rng.i(sources.size() - 1)));
}
while (sources.size() > target && !sources.isEmpty()) {
sources.remove(rng.i(sources.size() - 1));
}
return LootResolver.pickWeighted(pool, Candidate::weight, rng);
}
static ResourceKey<LootTable> resolveNativeKey(String name, Predicate<ResourceKey<LootTable>> registryContains) {
@@ -205,23 +188,6 @@ public final class ModdedLootApplier {
container.setChanged();
}
private static void injectReferenceSources(List<LootSource> sources, IrisLootReference reference, Engine engine, boolean fallback) {
IrisLootMode mode = reference.getMode();
if (mode == IrisLootMode.FALLBACK && !fallback) {
return;
}
injectSources(sources, irisSources(reference, engine), mode, fallback);
}
private static List<LootSource> irisSources(IrisLootReference reference, Engine engine) {
KList<IrisLootTable> tables = reference.getLootTables(engine.getComplex());
List<LootSource> sources = new ArrayList<>(tables.size());
for (IrisLootTable table : tables) {
sources.add(new IrisLootSource(table));
}
return sources;
}
private static boolean hasNativeTable(ServerLevel level, ResourceKey<LootTable> key) {
return level.getServer().reloadableRegistries().lookup()
.lookup(Registries.LOOT_TABLE)
@@ -244,25 +210,43 @@ public final class ModdedLootApplier {
}
}
private static Candidate pickWeighted(List<Candidate> pool, RNG rng) {
if (pool.isEmpty()) {
return null;
static boolean isCanonicalContainer(BlockPos pos, BlockState state) {
if (!(state.getBlock() instanceof ChestBlock) || state.getValue(ChestBlock.TYPE) == ChestType.SINGLE) {
return true;
}
int total = 0;
for (Candidate candidate : pool) {
total += Math.max(0, candidate.weight());
BlockPos connected = ChestBlock.getConnectedBlockPos(pos, state);
if (connected.equals(pos)) {
return true;
}
if (total <= 0) {
return pool.get(rng.nextInt(pool.size()));
return isCanonicalPair(pos, connected);
}
static boolean isCanonicalPair(BlockPos pos, BlockPos connected) {
return pos.getX() < connected.getX()
|| pos.getX() == connected.getX() && pos.getZ() <= connected.getZ();
}
static boolean hasNativeLootTable(ServerLevel level, BlockPos pos, BlockState state) {
if (hasNativeLootTableAt(level, pos)) {
return true;
}
int pull = rng.nextInt(total);
for (Candidate candidate : pool) {
pull -= Math.max(0, candidate.weight());
if (pull < 0) {
return candidate;
}
if (!(state.getBlock() instanceof ChestBlock) || state.getValue(ChestBlock.TYPE) == ChestType.SINGLE) {
return false;
}
return pool.get(pool.size() - 1);
BlockPos connected = ChestBlock.getConnectedBlockPos(pos, state);
return level.hasChunkAt(connected) && hasNativeLootTableAt(level, connected);
}
private static boolean hasNativeLootTableAt(ServerLevel level, BlockPos pos) {
return hasNativeLootTable(level.getBlockEntity(pos));
}
static boolean hasNativeLootTable(BlockEntity blockEntity) {
return blockEntity instanceof RandomizableContainer container && hasNativeLootTable(container);
}
static boolean hasNativeLootTable(RandomizableContainer container) {
return container != null && container.getLootTable() != null;
}
private static Container resolveContainer(ServerLevel level, BlockPos pos, BlockState state) {
@@ -33,6 +33,8 @@ public interface ModdedServerAccess {
ServerLevel putLevel(MinecraftServer server, ResourceKey<Level> key, ServerLevel level);
ServerLevel putLevelIfAbsent(MinecraftServer server, ResourceKey<Level> key, ServerLevel level);
ServerLevel removeLevel(MinecraftServer server, ResourceKey<Level> key);
boolean hasLevel(MinecraftServer server, ResourceKey<Level> key);
@@ -42,6 +42,11 @@ public final class ModdedServerLevels implements ModdedServerAccess {
return server.levels.put(key, level);
}
@Override
public ServerLevel putLevelIfAbsent(MinecraftServer server, ResourceKey<Level> key, ServerLevel level) {
return server.levels.putIfAbsent(key, level);
}
@Override
public ServerLevel removeLevel(MinecraftServer server, ResourceKey<Level> key) {
return server.levels.remove(key);
@@ -40,7 +40,13 @@ public final class ModdedServiceManager {
}
services.put(type, service);
if (enabled) {
enableService(service);
try {
service.onEnable();
} catch (Throwable failure) {
services.remove(type);
disableAfterFailure(service, failure);
throw serviceFailure(service, failure);
}
}
return service;
}
@@ -55,8 +61,18 @@ public final class ModdedServiceManager {
return;
}
enabled = true;
for (ModdedService service : services.values()) {
enableService(service);
ModdedService[] ordered = services.values().toArray(new ModdedService[0]);
for (int i = 0; i < ordered.length; i++) {
ModdedService service = ordered[i];
try {
service.onEnable();
} catch (Throwable failure) {
enabled = false;
for (int rollbackIndex = i; rollbackIndex >= 0; rollbackIndex--) {
disableAfterFailure(ordered[rollbackIndex], failure);
}
throw serviceFailure(service, failure);
}
}
}
@@ -79,12 +95,15 @@ public final class ModdedServiceManager {
forEachReversed(this::disableService);
}
private void enableService(ModdedService service) {
try {
service.onEnable();
} catch (Throwable error) {
LOGGER.error("Iris service onEnable failed for {}", service.getClass().getName(), error);
synchronized void rollback(Throwable failure) {
if (enabled) {
enabled = false;
ModdedService[] ordered = services.values().toArray(new ModdedService[0]);
for (int i = ordered.length - 1; i >= 0; i--) {
disableAfterFailure(ordered[i], failure);
}
}
services.clear();
}
private void disableService(ModdedService service) {
@@ -103,6 +122,28 @@ public final class ModdedServiceManager {
}
}
private void disableAfterFailure(ModdedService service, Throwable failure) {
try {
service.onDisable();
} catch (Throwable cleanupError) {
if (cleanupError != failure) {
failure.addSuppressed(cleanupError);
}
LOGGER.error("Iris service rollback failed for {}", service.getClass().getName(), cleanupError);
}
}
private RuntimeException serviceFailure(ModdedService service, Throwable failure) {
LOGGER.error("Iris service onEnable failed for {}", service.getClass().getName(), failure);
if (failure instanceof RuntimeException runtimeException) {
return runtimeException;
}
if (failure instanceof Error fatalError) {
throw fatalError;
}
return new IllegalStateException("Iris service failed to enable: " + service.getClass().getName(), failure);
}
private void forEachReversed(Consumer<ModdedService> action) {
ModdedService[] ordered = services.values().toArray(new ModdedService[0]);
for (int i = ordered.length - 1; i >= 0; i--) {
@@ -18,6 +18,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
@@ -34,6 +35,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
public final class ModdedStartup {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final AtomicBoolean PREPARED = new AtomicBoolean(false);
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
private static final Object PACK_LOCK = new Object();
@@ -41,9 +43,17 @@ public final class ModdedStartup {
}
public static void reset() {
PREPARED.set(false);
STARTED.set(false);
}
public static void prepareForStartup() {
if (!PREPARED.compareAndSet(false, true)) {
return;
}
validateAllPacks();
}
public static void prefetchDefaultPack() {
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler != null) {
@@ -56,30 +66,30 @@ public final class ModdedStartup {
}
public static void runOnce(MinecraftServer server) {
if (server == null || !STARTED.compareAndSet(false, true)) {
if (server == null) {
return;
}
prepareForStartup();
if (!STARTED.compareAndSet(false, true)) {
return;
}
reinjectPersistentDimensions(server);
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
validateAllPacks();
ensureDefaultPack();
return;
}
scheduler.async(() -> {
validateAllPacks();
ensureDefaultPack();
});
scheduler.async(ModdedStartup::ensureDefaultPack);
}
public static void validateAllPacks() {
File packsRoot = ModdedPackCommands.packsRoot();
File[] packDirs = packsRoot.listFiles(File::isDirectory);
PackValidationRegistry.clear();
if (packDirs == null || packDirs.length == 0) {
return;
}
PackValidationRegistry.clear();
for (File packDir : packDirs) {
try {
PackValidationResult result = PackValidator.validate(packDir);
@@ -99,10 +109,50 @@ public final class ModdedStartup {
}
} catch (Throwable e) {
LOGGER.error("Iris pack validation failed for '{}'", packDir.getName(), e);
String detail = e.getMessage();
if (detail == null || detail.isBlank()) {
detail = e.getClass().getSimpleName();
}
PackValidationRegistry.publish(new PackValidationResult(
packDir.getName(),
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
List.of(),
System.currentTimeMillis()));
}
}
}
public static PackValidationResult requirePackForWorldCreation(String pack) {
if (pack == null || pack.isBlank()) {
throw new IllegalArgumentException("Pack name is required for world creation");
}
File packDir = new File(ModdedPackCommands.packsRoot(), pack);
if (!packDir.isDirectory()) {
throw new BrokenPackException(pack, List.of(
"Pack folder does not exist under " + ModdedPackCommands.packsRoot().getAbsolutePath() + "."));
}
try {
PackValidationResult result = PackValidator.validate(packDir);
PackValidationRegistry.publish(result);
return PackValidationRegistry.requireLoadable(pack);
} catch (BrokenPackException e) {
throw e;
} catch (Throwable e) {
LOGGER.error("Iris required world-creation validation failed for '{}'", pack, e);
String detail = e.getMessage();
if (detail == null || detail.isBlank()) {
detail = e.getClass().getSimpleName();
}
PackValidationResult failure = new PackValidationResult(
pack,
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
List.of(),
System.currentTimeMillis());
PackValidationRegistry.publish(failure);
throw new BrokenPackException(pack, failure.getBlockingErrors());
}
}
private static void reinjectPersistentDimensions(MinecraftServer server) {
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions = ModdedDimensionRegistryStore.load(server);
if (dimensions.isEmpty()) {
@@ -115,6 +165,11 @@ public final class ModdedStartup {
injected++;
} catch (Throwable e) {
LOGGER.error("Iris failed to re-inject persistent dimension '{}' (pack={} dim={} seed={})", dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed(), e);
if (e instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris failed to re-inject persistent dimension '"
+ dimension.id() + "' from pack '" + dimension.pack() + "'", e);
}
}
LOGGER.info("Iris re-injected {} persistent dimension(s) at startup", injected);
@@ -18,6 +18,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.spi.PlatformWorld;
@@ -51,6 +52,7 @@ import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
@@ -59,7 +61,7 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
private final Supplier<MinecraftServer> server;
public ModdedStructureHooks(Supplier<MinecraftServer> server) {
this.server = server;
this.server = Objects.requireNonNull(server);
}
@Override
@@ -75,19 +77,16 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
@Override
public List<String> structureBiomeKeys(String structureKey) {
List<String> keys = new ArrayList<>();
MinecraftServer instance = requireServer("resolve biome keys for registered structure '" + structureKey + "'");
try {
MinecraftServer instance = server.get();
if (instance == null) {
return keys;
}
Identifier identifier = Identifier.tryParse(structureKey);
if (identifier == null) {
return keys;
throw new IllegalArgumentException("Registered structure key is invalid: " + structureKey);
}
Registry<Structure> registry = instance.registryAccess().lookupOrThrow(Registries.STRUCTURE);
Structure structure = registry.getValue(identifier);
if (structure == null) {
return keys;
throw new IllegalArgumentException("Registered structure does not exist: " + structureKey);
}
for (Holder<Biome> holder : structure.biomes()) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
@@ -95,8 +94,13 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
keys.add(key.get().identifier().toString());
}
}
} catch (Throwable error) {
IrisLogging.reportError(error);
if (keys.isEmpty()) {
throw new IllegalStateException("Registered structure '" + structureKey
+ "' exposes no registered biome keys");
}
} catch (RuntimeException error) {
throw new IllegalStateException("Iris failed to resolve biome keys for registered structure '"
+ structureKey + "' from the modded structure registry", error);
}
return keys;
}
@@ -129,24 +133,19 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
@Override
public List<String> reachableStructureKeys(PlatformWorld world) {
List<String> keys = new ArrayList<>();
ServerLevel level = requireLevel(world, "resolve reachable structures");
try {
ServerLevel level = level(world);
if (level == null) {
return keys;
}
BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource();
Set<String> possibleBiomes = possibleBiomeKeys(source);
if (possibleBiomes.isEmpty()) {
return keys;
}
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
for (Map.Entry<ResourceKey<Structure>, Structure> entry : registry.entrySet()) {
if (hasPossibleBiome(entry.getValue(), possibleBiomes)) {
keys.add(entry.getKey().identifier().toString());
}
}
} catch (Throwable error) {
IrisLogging.reportError(error);
} catch (RuntimeException error) {
throw new IllegalStateException("Iris failed to resolve reachable structures for modded level '"
+ level.dimension().identifier() + "'", error);
}
return keys;
}
@@ -154,14 +153,12 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
@Override
public List<String> possibleBiomeKeys(PlatformWorld world) {
List<String> keys = new ArrayList<>();
ServerLevel level = requireLevel(world, "resolve possible structure biome keys");
try {
ServerLevel level = level(world);
if (level == null) {
return keys;
}
keys.addAll(possibleBiomeKeys(level.getChunkSource().getGenerator().getBiomeSource()));
} catch (Throwable error) {
IrisLogging.reportError(error);
} catch (RuntimeException error) {
throw new IllegalStateException("Iris failed to resolve possible structure biome keys for modded level '"
+ level.dimension().identifier() + "'", error);
}
return keys;
}
@@ -230,9 +227,9 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
}
placeChunks(level, structureManager, generator, start, box, seed);
return bounds(box);
} catch (Throwable error) {
IrisLogging.reportError(error);
return null;
} catch (RuntimeException error) {
throw NativeStructureGenerationException.failure(
"capture placement", structureKey, chunkX, chunkZ, error);
}
}
@@ -264,16 +261,19 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
}
private static Set<String> possibleBiomeKeys(BiomeSource source) {
Set<String> keys = new LinkedHashSet<>();
if (source == null) {
return keys;
throw new IllegalStateException("Minecraft chunk generator has no biome source");
}
Set<String> keys = new LinkedHashSet<>();
for (Holder<Biome> holder : source.possibleBiomes()) {
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
if (key.isPresent()) {
keys.add(key.get().identifier().toString());
}
}
if (keys.isEmpty()) {
throw new IllegalStateException("Minecraft biome source exposes no registered possible biomes");
}
return keys;
}
@@ -312,18 +312,38 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
private <T> List<String> registryKeys(ResourceKey<Registry<T>> registryKey) {
List<String> keys = new ArrayList<>();
MinecraftServer instance = requireServer("read registry '" + registryKey.identifier() + "'");
try {
MinecraftServer instance = server.get();
if (instance == null) {
return keys;
}
Registry<T> registry = instance.registryAccess().lookupOrThrow(registryKey);
for (Identifier identifier : registry.keySet()) {
keys.add(identifier.toString());
}
} catch (Throwable error) {
IrisLogging.reportError(error);
} catch (RuntimeException error) {
throw new IllegalStateException("Iris failed to read modded registry '"
+ registryKey.identifier() + "'", error);
}
return keys;
}
private MinecraftServer requireServer(String operation) {
MinecraftServer instance;
try {
instance = server.get();
} catch (RuntimeException error) {
throw new IllegalStateException("Iris failed to access the Minecraft server to " + operation, error);
}
if (instance == null) {
throw new IllegalStateException("Iris cannot " + operation + " before the Minecraft server is available");
}
return instance;
}
private static ServerLevel requireLevel(PlatformWorld world, String operation) {
ServerLevel level = level(world);
if (level == null) {
throw new IllegalStateException("Iris cannot " + operation
+ " without a bound modded ServerLevel");
}
return level;
}
}
@@ -19,6 +19,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.StructureVerticalBounds;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
@@ -27,7 +28,6 @@ import art.arcane.volmlib.util.json.JSONObject;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
@@ -43,14 +43,17 @@ import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -74,6 +77,7 @@ public final class ModdedWorldCheck {
private static final int EXIT_FAILURE = 1;
private static final int MAX_FOOTPRINT_CHUNKS = 96;
private static final int MAX_START_REFERENCE_CHUNKS = 16;
private static final int MAX_STRUCTURE_CANDIDATES = 1024;
private static final long SERVER_WAIT_TIMEOUT_MILLIS = 600000L;
private static final long SERVER_WAIT_INTERVAL_MILLIS = 250L;
private static final List<StructureCheck> STRUCTURE_CHECKS = List.of(
@@ -136,8 +140,9 @@ public final class ModdedWorldCheck {
}
MinecraftServer serverRef = server;
WorldCheckPreparation preparation = serverRef.submit(() -> run(serverRef)).join();
exitCode = serverRef.submit(() -> runAndRequestStop(
() -> run(serverRef),
() -> completeWorldCheck(preparation),
() -> {
stopRequested.set(true);
serverRef.halt(false);
@@ -195,11 +200,12 @@ public final class ModdedWorldCheck {
processExit.exit(exitCode);
}
private static boolean run(MinecraftServer server) {
private static WorldCheckPreparation run(MinecraftServer server) {
ServerLevel level = targetLevel(server);
if (level == null) {
LOGGER.error("[worldcheck] no Iris dimension is loaded");
return false;
return new WorldCheckPreparation(false, false, false, false,
new NativeStructureGate(false, 0, false, null));
}
String levelId = level.dimension().identifier().toString();
@@ -268,16 +274,43 @@ public final class ModdedWorldCheck {
}
boolean entityMixinsOk = checkEntityMixins(level);
boolean structureOk = false;
if (generator != null) {
structureOk = checkNativeStructures(level, generator, spawn);
}
NativeStructureGate structureGate = generator == null
? new NativeStructureGate(false, 0, false, null)
: checkNativeStructures(level, generator, spawn);
boolean terrainOk = sectionsOk && varietyOk;
boolean nonStructurePass = irisGenerator && dimensionTypeOk && terrainOk && entityMixinsOk;
return new WorldCheckPreparation(nonStructurePass, terrainOk, dimensionTypeOk,
entityMixinsOk, structureGate);
}
boolean pass = irisGenerator && dimensionTypeOk && sectionsOk && varietyOk && entityMixinsOk && structureOk;
private static boolean completeWorldCheck(WorldCheckPreparation preparation) {
NativeStructureGate structureGate = preparation.structureGate();
boolean poiOk = false;
PendingVillagePoi pendingPoi = structureGate.pendingPoi();
if (pendingPoi != null) {
PoiAudit poi = auditStructurePois(pendingPoi.level(), pendingPoi.start());
poiOk = villagePoiPass(poi.inBounds(), poi.outOfBounds());
qaEvent("village_poi_metric", "village", poiOk,
"inBounds=" + poi.inBounds() + ",outOfBounds=" + poi.outOfBounds());
if (!poiOk) {
LOGGER.error("[worldcheck] village POI audit failed: inBounds={} outOfBounds={}",
poi.inBounds(), poi.outOfBounds());
}
} else {
qaEvent("village_poi_metric", "village", false, "skipped=structure");
}
int passed = structureGate.nonVillagePassed()
+ (structureGate.villagePassBeforePoi() && poiOk ? 1 : 0);
boolean structurePass = structureGate.passBeforePoi() && poiOk;
LOGGER.info("[worldcheck] native structure gate: {}/{} passed", passed, STRUCTURE_CHECKS.size());
qaEvent("structure_aggregate", "all", structurePass,
"passed=" + passed + ",total=" + STRUCTURE_CHECKS.size());
boolean pass = preparation.nonStructurePass() && structurePass;
LOGGER.info("[worldcheck] {}", pass ? "PASS" : "FAIL");
qaEvent("worldcheck_final", "all", pass,
"structures=" + STRUCTURE_CHECKS.size() + ",terrain=" + (sectionsOk && varietyOk)
+ ",dimensionType=" + dimensionTypeOk + ",entityMixins=" + entityMixinsOk);
"structures=" + STRUCTURE_CHECKS.size() + ",terrain=" + preparation.terrainOk()
+ ",dimensionType=" + preparation.dimensionTypeOk()
+ ",entityMixins=" + preparation.entityMixinsOk());
return pass;
}
@@ -354,25 +387,32 @@ public final class ModdedWorldCheck {
return pass;
}
private static boolean checkNativeStructures(ServerLevel level, IrisModdedChunkGenerator generator, BlockPos origin) {
private static NativeStructureGate checkNativeStructures(ServerLevel level,
IrisModdedChunkGenerator generator,
BlockPos origin) {
boolean pass = true;
int passed = 0;
int nonVillagePassed = 0;
boolean villagePass = false;
PendingVillagePoi pendingPoi = null;
for (StructureCheck check : STRUCTURE_CHECKS) {
boolean structurePass = checkNativeStructure(level, generator, origin, check);
if (structurePass) {
passed++;
} else {
StructureCheckResult result = checkNativeStructure(level, generator, origin, check);
if (!result.pass()) {
pass = false;
}
if (check.label().equals("village")) {
villagePass = result.pass();
pendingPoi = result.pendingPoi();
} else if (result.pass()) {
nonVillagePassed++;
}
}
LOGGER.info("[worldcheck] native structure gate: {}/{} passed", passed, STRUCTURE_CHECKS.size());
qaEvent("structure_aggregate", "all", pass,
"passed=" + passed + ",total=" + STRUCTURE_CHECKS.size());
return pass;
return new NativeStructureGate(pass, nonVillagePassed, villagePass, pendingPoi);
}
private static boolean checkNativeStructure(ServerLevel level, IrisModdedChunkGenerator generator,
BlockPos origin, StructureCheck check) {
private static StructureCheckResult checkNativeStructure(ServerLevel level,
IrisModdedChunkGenerator generator,
BlockPos origin,
StructureCheck check) {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<Holder<Structure>> registered = new ArrayList<>(check.registryKeys().size());
LinkedHashSet<String> registeredKeys = new LinkedHashSet<>();
@@ -398,7 +438,7 @@ public final class ModdedWorldCheck {
emitSkipped(check, "registry", "structure_reachability", "structure_locate",
"structure_start_reference", "structure_footprint", "structure_material",
"structure_block_entity");
return false;
return new StructureCheckResult(false, null);
}
List<Holder<Structure>> reachable = new ArrayList<>(registered.size());
@@ -422,35 +462,35 @@ public final class ModdedWorldCheck {
LOGGER.error("[worldcheck] {} cannot generate in any biome produced by this Iris pack", check.label());
emitSkipped(check, "reachability", "structure_locate", "structure_start_reference",
"structure_footprint", "structure_material", "structure_block_entity");
return false;
return new StructureCheckResult(false, null);
}
long locateStart = System.nanoTime();
Pair<BlockPos, Holder<Structure>> found = generator.findNearestMapStructure(
level, HolderSet.direct(reachable), origin, check.locateRadius(), false);
Pair<BlockPos, Holder<Structure>> found = findGeneratedStructureCandidate(
level, reachable, origin, check.locateRadius());
long locateMillis = (System.nanoTime() - locateStart) / 1_000_000L;
Identifier foundKey = found == null ? null : registry.getKey(found.getSecond().value());
boolean locateOk = found != null && foundKey != null && reachableKeys.contains(foundKey.toString());
qaEvent("structure_locate", check.label(), locateOk,
"millis=" + locateMillis + ",radius=" + check.locateRadius()
"method=placement_candidates,millis=" + locateMillis + ",radius=" + check.locateRadius()
+ ",result=" + (foundKey == null ? "none" : foundKey));
if (found == null) {
LOGGER.error("[worldcheck] {} locate found no result within {} chunks after {}ms",
LOGGER.error("[worldcheck] {} native placement candidates produced no valid start within {} rings after {}ms",
check.label(), check.locateRadius(), locateMillis);
emitSkipped(check, "locate", "structure_start_reference", "structure_footprint",
"structure_material", "structure_block_entity");
return false;
return new StructureCheckResult(false, null);
}
BlockPos position = found.getFirst();
LOGGER.info("[worldcheck] {} locate: {} {} {} in {}ms (radius={}, unexplored=false, result={})",
LOGGER.info("[worldcheck] {} generated candidate: {} {} {} in {}ms (radius={}, result={})",
check.label(), position.getX(), position.getY(), position.getZ(), locateMillis,
check.locateRadius(), foundKey);
if (!locateOk) {
LOGGER.error("[worldcheck] {} locate returned unexpected structure {}", check.label(), foundKey);
LOGGER.error("[worldcheck] {} candidate scan returned unexpected structure {}", check.label(), foundKey);
emitSkipped(check, "locate", "structure_start_reference", "structure_footprint",
"structure_material", "structure_block_entity");
return false;
return new StructureCheckResult(false, null);
}
int chunkX = position.getX() >> 4;
@@ -471,11 +511,12 @@ public final class ModdedWorldCheck {
check.label(), chunkX, chunkZ);
emitSkipped(check, "start_reference", "structure_footprint", "structure_material",
"structure_block_entity");
return false;
return new StructureCheckResult(false, null);
}
IrisNativeStructureDecision decision = generator.commandEngine().getDimension().getImportedStructures()
.resolve(foundKey.toString(), NativeStructurePostProcessor.isUndergroundStep(structure.step()));
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
generator.commandEngine(), foundKey.toString(),
NativeStructurePostProcessor.isUndergroundStep(structure.step()));
Integer appliedShift = generator.worldCheckStructureShift(foundKey.toString(), start.getChunkPos());
BoundingBox shiftedBounds = start.getBoundingBox();
boolean verticalShiftOk = verticalShiftMatches(
@@ -522,22 +563,17 @@ public final class ModdedWorldCheck {
}
boolean foundationOk = true;
boolean poiOk = true;
PendingVillagePoi pendingPoi = null;
if (check.label().equals("village")) {
foundationOk = villageFoundationPass(footprint.foundationGapColumns());
LOGGER.info("[worldcheck] village foundation metric: cobblestone below piece bases={} columns={} unsupported={}",
footprint.foundationBlocks(), footprint.foundationColumns(), footprint.foundationGapColumns());
LOGGER.info("[worldcheck] village foundation metric: bases={} cobblestone={} columns={} unsupported={}",
footprint.foundationBaseColumns(), footprint.foundationBlocks(),
footprint.foundationColumns(), footprint.foundationGapColumns());
qaEvent("village_foundation_metric", check.label(), foundationOk,
"cobblestoneBelowBase=" + footprint.foundationBlocks() + ",columns="
"bases=" + footprint.foundationBaseColumns() + ",cobblestoneBelowBase="
+ footprint.foundationBlocks() + ",columns="
+ footprint.foundationColumns() + ",unsupported=" + footprint.foundationGapColumns());
PoiAudit poi = auditStructurePois(level, start);
poiOk = villagePoiPass(poi.inBounds(), poi.outOfBounds());
qaEvent("village_poi_metric", check.label(), poiOk,
"inBounds=" + poi.inBounds() + ",outOfBounds=" + poi.outOfBounds());
if (!poiOk) {
LOGGER.error("[worldcheck] village POI audit failed: inBounds={} outOfBounds={}",
poi.inBounds(), poi.outOfBounds());
}
pendingPoi = new PendingVillagePoi(level, start);
}
boolean blockEntityOk = footprint.blockEntityStates() == footprint.blockEntitiesPresent();
@@ -562,8 +598,9 @@ public final class ModdedWorldCheck {
if (!foundationOk) {
LOGGER.error("[worldcheck] village has unsupported foundation columns after stilt placement");
}
return verticalShiftOk && footprintOk && materialOk && blockEntityOk
&& vegetationOk && foundationOk && poiOk;
boolean pass = verticalShiftOk && footprintOk && materialOk && blockEntityOk
&& vegetationOk && foundationOk;
return new StructureCheckResult(pass, pass ? pendingPoi : null);
}
private static StructureStart resolveStructureStart(ServerLevel level, ChunkAccess targetChunk,
@@ -586,6 +623,96 @@ public final class ModdedWorldCheck {
return direct;
}
private static Pair<BlockPos, Holder<Structure>> findGeneratedStructureCandidate(
ServerLevel level, List<Holder<Structure>> structures, BlockPos origin, int maxRadius) {
ChunkGeneratorStructureState state = level.getChunkSource().getGeneratorState();
Set<Long> attempted = new LinkedHashSet<>();
for (Holder<Structure> structure : structures) {
for (StructurePlacement placement : state.getPlacementsForStructure(structure)) {
if (!(placement instanceof ConcentricRingsStructurePlacement rings)) {
continue;
}
List<ChunkPos> positions = state.getRingPositionsFor(rings);
if (positions == null) {
continue;
}
List<ChunkPos> sorted = new ArrayList<>(positions);
sorted.sort(Comparator.comparingLong(position -> distanceSquared(origin, position)));
for (ChunkPos position : sorted) {
Pair<BlockPos, Holder<Structure>> found = inspectStructureCandidate(
level, structures, placement, position, attempted);
if (found != null) {
return found;
}
if (attempted.size() >= MAX_STRUCTURE_CANDIDATES) {
return null;
}
}
}
}
int originChunkX = origin.getX() >> 4;
int originChunkZ = origin.getZ() >> 4;
for (int radius = 0; radius <= maxRadius; radius++) {
for (Holder<Structure> structure : structures) {
for (StructurePlacement placement : state.getPlacementsForStructure(structure)) {
if (!(placement instanceof RandomSpreadStructurePlacement randomSpread)) {
continue;
}
for (int x = -radius; x <= radius; x++) {
boolean xEdge = x == -radius || x == radius;
for (int z = -radius; z <= radius; z++) {
if (!xEdge && z != -radius && z != radius) {
continue;
}
int sectorX = originChunkX + randomSpread.spacing() * x;
int sectorZ = originChunkZ + randomSpread.spacing() * z;
ChunkPos candidate = randomSpread.getPotentialStructureChunk(
state.getLevelSeed(), sectorX, sectorZ);
if (!placement.isStructureChunk(state, candidate.x(), candidate.z())) {
continue;
}
Pair<BlockPos, Holder<Structure>> found = inspectStructureCandidate(
level, structures, placement, candidate, attempted);
if (found != null) {
return found;
}
if (attempted.size() >= MAX_STRUCTURE_CANDIDATES) {
return null;
}
}
}
}
}
}
return null;
}
private static Pair<BlockPos, Holder<Structure>> inspectStructureCandidate(
ServerLevel level, List<Holder<Structure>> structures, StructurePlacement placement,
ChunkPos candidate, Set<Long> attempted) {
if (!attempted.add(candidate.pack())) {
return null;
}
ChunkAccess chunk = level.getChunk(candidate.x(), candidate.z());
for (Holder<Structure> structure : structures) {
StructureStart start = resolveStructureStart(level, chunk, structure.value());
if (start == null || !start.isValid()) {
continue;
}
BlockPos locate = placement.getLocatePos(start.getChunkPos());
BlockPos resolved = new BlockPos(locate.getX(), start.getBoundingBox().minY(), locate.getZ());
return Pair.of(resolved, structure);
}
return null;
}
private static long distanceSquared(BlockPos origin, ChunkPos position) {
long x = (long) position.getMinBlockX() - origin.getX();
long z = (long) position.getMinBlockZ() - origin.getZ();
return x * x + z * z;
}
private static FootprintAudit auditFootprint(ServerLevel level, Structure structure, StructureStart start,
StructureCheck check, Identifier structureKey) {
List<StructurePiece> pieces = start.getPieces();
@@ -600,11 +727,11 @@ public final class ModdedWorldCheck {
int characteristicChunks = 0;
int vegetationBlocks = 0;
int vegetationColumns = 0;
int foundationBaseColumns = 0;
int foundationBlocks = 0;
int foundationColumns = 0;
int foundationGapColumns = 0;
BitSet visited = new BitSet(level.getHeight() << 8);
int[] minimumPieceY = new int[256];
int[] maximumPieceY = new int[256];
for (ChunkPos chunkPos : selected) {
ChunkAccess chunk = level.getChunk(chunkPos.x(), chunkPos.z());
@@ -617,8 +744,8 @@ public final class ModdedWorldCheck {
BlockEntityAudit blockEntities = auditBlockEntities(level, chunk);
blockEntityStates += blockEntities.states();
blockEntitiesPresent += blockEntities.present();
StructureMaterialAudit material = auditStructureMaterial(level, chunk, pieces, check,
structureKey, visited, minimumPieceY, maximumPieceY);
StructureMaterialAudit material = auditStructureMaterial(level, chunk, start, check,
structureKey, visited, maximumPieceY);
if (material.scanned()) {
materialScannedChunks++;
}
@@ -628,6 +755,7 @@ public final class ModdedWorldCheck {
}
vegetationBlocks += material.vegetationBlocks();
vegetationColumns += material.vegetationColumns();
foundationBaseColumns += material.foundationBaseColumns();
foundationBlocks += material.foundationBlocks();
foundationColumns += material.foundationColumns();
foundationGapColumns += material.foundationGapColumns();
@@ -648,19 +776,18 @@ public final class ModdedWorldCheck {
return new FootprintAudit(selected.size(), availableChunks, evidenceChunks,
coveredPieces, pieces.size(), blockEntityStates, blockEntitiesPresent,
materialScannedChunks, characteristicBlocks, characteristicChunks,
vegetationBlocks, vegetationColumns, foundationBlocks, foundationColumns,
foundationGapColumns);
vegetationBlocks, vegetationColumns, foundationBaseColumns, foundationBlocks,
foundationColumns, foundationGapColumns);
}
private static StructureMaterialAudit auditStructureMaterial(ServerLevel level, ChunkAccess chunk,
List<StructurePiece> pieces,
StructureStart start,
StructureCheck check,
Identifier structureKey,
BitSet visited,
int[] minimumPieceY,
int[] maximumPieceY) {
List<StructurePiece> pieces = start.getPieces();
visited.clear();
Arrays.fill(minimumPieceY, Integer.MAX_VALUE);
Arrays.fill(maximumPieceY, Integer.MIN_VALUE);
int characteristicBlocks = 0;
int minimumWorldY = level.getMinY();
@@ -683,20 +810,10 @@ public final class ModdedWorldCheck {
continue;
}
scanned = true;
int metricMinimumY = minimumY;
boolean metricBaseValid = true;
if (check.label().equals("village") && piece instanceof PoolElementStructurePiece poolPiece) {
int groundY = bounds.minY() + poolPiece.getGroundLevelDelta();
metricBaseValid = groundY >= bounds.minY();
metricMinimumY = Math.max(minimumWorldY, Math.min(maximumWorldY, groundY));
}
for (int z = minimumZ; z <= maximumZ; z++) {
int localZ = z - minimumChunkZ;
for (int x = minimumX; x <= maximumX; x++) {
int column = (localZ << 4) | (x - minimumChunkX);
if (metricBaseValid) {
minimumPieceY[column] = Math.min(minimumPieceY[column], metricMinimumY);
}
maximumPieceY[column] = Math.max(maximumPieceY[column], maximumY);
}
}
@@ -747,55 +864,26 @@ public final class ModdedWorldCheck {
}
}
int foundationBaseColumns = 0;
int foundationBlocks = 0;
int foundationColumns = 0;
int foundationGapColumns = 0;
if (check.label().equals("village")) {
for (int column = 0; column < minimumPieceY.length; column++) {
int minimumY = minimumPieceY[column];
if (minimumY == Integer.MAX_VALUE) {
continue;
}
boolean foundationColumn = false;
int x = minimumChunkX + (column & 15);
int z = minimumChunkZ + (column >> 4);
int foundationY = Integer.MIN_VALUE;
int foundationMaximumY = Math.min(maximumWorldY, minimumY + 1);
for (int y = minimumY; y <= foundationMaximumY; y++) {
if (chunk.getBlockState(position.set(x, y, z)).isSolid()) {
foundationY = y;
break;
}
}
boolean basePresent = foundationY != Integer.MIN_VALUE;
boolean supportSolid = false;
boolean vegetationSupport = false;
if (basePresent && foundationY > minimumWorldY) {
BlockState support = chunk.getBlockState(position.set(x, foundationY - 1, z));
supportSolid = support.isSolid();
vegetationSupport = support.is(BlockTags.LOGS) || support.is(BlockTags.LEAVES);
}
boolean worldFloorBase = basePresent && foundationY == minimumWorldY;
if (!villageFoundationSupported(basePresent, worldFloorBase, supportSolid, vegetationSupport)) {
foundationGapColumns++;
continue;
}
for (int y = foundationY - 1; y >= minimumWorldY; y--) {
BlockState state = chunk.getBlockState(position.set(x, y, z));
if (!state.is(Blocks.COBBLESTONE)) {
break;
}
foundationBlocks++;
foundationColumn = true;
}
if (foundationColumn) {
foundationColumns++;
}
}
BoundingBox area = new BoundingBox(
minimumChunkX, minimumWorldY, minimumChunkZ,
maximumChunkX, maximumWorldY, maximumChunkZ);
NativeStructurePostProcessor.StiltSupportAudit foundation =
NativeStructurePostProcessor.auditStiltSupport(
level, area, start, Blocks.COBBLESTONE.defaultBlockState());
foundationBaseColumns = foundation.baseColumns();
foundationBlocks = foundation.stiltBlocks();
foundationColumns = foundation.stiltColumns();
foundationGapColumns = foundation.unsupportedColumns();
}
return new StructureMaterialAudit(scanned, characteristicBlocks, vegetationBlocks,
vegetationColumns, foundationBlocks, foundationColumns, foundationGapColumns);
vegetationColumns, foundationBaseColumns, foundationBlocks, foundationColumns,
foundationGapColumns);
}
private static List<ChunkPos> selectFootprintChunks(StructureStart start, int limit) {
@@ -1036,11 +1124,6 @@ public final class ModdedWorldCheck {
return vegetation && blockY > highestPieceY;
}
static boolean villageFoundationSupported(boolean basePresent, boolean worldFloorBase,
boolean supportSolid, boolean vegetationSupport) {
return basePresent && (worldFloorBase || (supportSolid && !vegetationSupport));
}
static boolean villageFoundationPass(int unsupportedColumns) {
return unsupportedColumns == 0;
}
@@ -1194,12 +1277,27 @@ public final class ModdedWorldCheck {
private record StructureCheck(String label, List<String> registryKeys, int locateRadius) {
}
private record WorldCheckPreparation(boolean nonStructurePass, boolean terrainOk,
boolean dimensionTypeOk, boolean entityMixinsOk,
NativeStructureGate structureGate) {
}
private record NativeStructureGate(boolean passBeforePoi, int nonVillagePassed,
boolean villagePassBeforePoi, PendingVillagePoi pendingPoi) {
}
private record StructureCheckResult(boolean pass, PendingVillagePoi pendingPoi) {
}
private record PendingVillagePoi(ServerLevel level, StructureStart start) {
}
private record FootprintAudit(int inspectedChunks, int availableChunks, int evidenceChunks,
int coveredPieces, int totalPieces, int blockEntityStates,
int blockEntitiesPresent, int materialScannedChunks,
int characteristicBlocks, int characteristicChunks,
int vegetationBlocks, int vegetationColumns,
int foundationBlocks, int foundationColumns,
int foundationBaseColumns, int foundationBlocks, int foundationColumns,
int foundationGapColumns) {
}
@@ -1208,7 +1306,8 @@ public final class ModdedWorldCheck {
private record StructureMaterialAudit(boolean scanned, int characteristicBlocks,
int vegetationBlocks, int vegetationColumns,
int foundationBlocks, int foundationColumns,
int foundationBaseColumns, int foundationBlocks,
int foundationColumns,
int foundationGapColumns) {
}
@@ -19,6 +19,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget;
@@ -35,6 +36,7 @@ import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
public final class ModdedWorldEngines {
@@ -57,22 +59,48 @@ public final class ModdedWorldEngines {
}
public static void evict(ServerLevel level) {
Engine removed = ENGINES.remove(level);
if (removed == null) {
return;
}
try {
if (!removed.isClosed()) {
removed.close();
}
LOGGER.info("Iris engine evicted for {}", level.dimension().identifier());
evictOrThrow(level);
} catch (Throwable e) {
LOGGER.error("Iris engine evict close failed for {}", level.dimension().identifier(), e);
}
}
static void evictOrThrow(ServerLevel level) {
Engine[] removed = new Engine[1];
ENGINES.computeIfPresent(level, (ServerLevel ignored, Engine current) -> {
close(current);
removed[0] = current;
return null;
});
if (removed[0] == null) {
return;
}
LOGGER.info("Iris engine evicted for {}", level.dimension().identifier());
}
static Engine prepareReplacement(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
return create(level, pack, dimensionKey, seedOverride);
}
static void installReplacement(ServerLevel level, Engine replacement) {
ServerLevel activeLevel = Objects.requireNonNull(level);
Engine activeReplacement = Objects.requireNonNull(replacement);
ENGINES.compute(activeLevel, (ServerLevel ignored, Engine current) -> {
if (current != null && current != activeReplacement) {
close(current);
}
return activeReplacement;
});
}
static void closeUnregistered(Engine engine) {
close(engine);
}
private static Engine create(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
ModdedEngineBootstrap.bind();
PackValidationRegistry.requireLoadable(pack);
File packDir = resolvePack(pack, dimensionKey);
IrisData data = IrisData.get(packDir);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
@@ -95,6 +123,16 @@ public final class ModdedWorldEngines {
.platformWorld(new ModdedPlatformWorld(level))
.build();
Engine engine = new IrisEngine(new EngineTarget(world, dimension, data), false);
if (engine.isClosed() || engine.getComplex() == null) {
IllegalStateException failure = new IllegalStateException("Iris engine for "
+ level.dimension().identifier() + " did not initialize a ready biome complex");
try {
close(engine);
} catch (Throwable cleanupError) {
failure.addSuppressed(cleanupError);
}
throw failure;
}
LOGGER.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{}",
level.dimension().identifier(), packDir.getAbsolutePath(), dimension.getLoadKey(), seed, dimension.getMinHeight(), dimension.getMaxHeight());
@@ -131,24 +169,6 @@ public final class ModdedWorldEngines {
return packDir;
}
String parity = System.getProperty("iris.parity");
if (parity != null) {
String parityPath = parity;
int lastColon = parity.lastIndexOf(':');
if (lastColon > 0) {
try {
Integer.parseInt(parity.substring(lastColon + 1));
parityPath = parity.substring(0, lastColon);
} catch (NumberFormatException ignored) {
}
}
File parityPack = new File(parityPath);
if (parityPack.isDirectory()) {
LOGGER.warn("Iris pack missing at {}; falling back to parity pack {}", packDir.getAbsolutePath(), parityPack.getAbsolutePath());
return parityPack;
}
}
LOGGER.error("===============================================================");
LOGGER.error("Iris pack '{}' is not installed.", pack);
LOGGER.error("Expected a pack folder at: {}", packDir.getAbsolutePath());
@@ -157,6 +177,12 @@ public final class ModdedWorldEngines {
throw new IllegalStateException("Iris pack not installed: " + packDir.getAbsolutePath());
}
private static void close(Engine engine) {
if (engine != null && !engine.isClosed()) {
engine.close();
}
}
public static void shutdown() {
for (Map.Entry<ServerLevel, Engine> entry : ENGINES.entrySet()) {
Engine engine = entry.getValue();
@@ -23,6 +23,7 @@ import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.object.IRare;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisEntity;
@@ -492,7 +493,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
int min = entry.getMinSpawns();
int max = entry.getMaxSpawns();
int count = min == max ? min : RNG.r.i(Math.min(min, max), Math.max(min, max));
int count = LootResolver.inclusive(RNG.r, min, max);
if (count <= 0) {
return 0;
}
@@ -516,8 +517,8 @@ public final class ModdedWorldManager implements EngineWorldManager {
worldY = caveFloor.getY() + 1;
worldZ = caveFloor.getZ();
} else {
worldX = (chunkX << 4) + RNG.r.i(15);
worldZ = (chunkZ << 4) + RNG.r.i(15);
worldX = (chunkX << 4) + RNG.r.i(16);
worldZ = (chunkZ << 4) + RNG.r.i(16);
int surfaceY = level.getMinY() + engine.getHeight(worldX, worldZ, false);
int solidY = level.getMinY() + engine.getHeight(worldX, worldZ, true);
worldY = group == IrisSpawnGroup.NORMAL
@@ -527,6 +528,9 @@ public final class ModdedWorldManager implements EngineWorldManager {
if (worldY <= level.getMinY() || worldY >= level.getMaxY()) {
continue;
}
if (!LootResolver.oneIn(entityRng, entry.getRarity())) {
continue;
}
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
continue;
}
@@ -551,7 +555,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
int min = entry.getMinSpawns();
int max = entry.getMaxSpawns();
int count = min == max ? min : RNG.r.i(Math.min(min, max), Math.max(min, max));
int count = LootResolver.inclusive(RNG.r, min, max);
if (count <= 0) {
return 0;
}
@@ -564,6 +568,9 @@ public final class ModdedWorldManager implements EngineWorldManager {
int worldZ = position.getZ();
int spawned = 0;
for (int i = 0; i < count; i++) {
if (!LootResolver.oneIn(entityRng, entry.getRarity())) {
continue;
}
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
continue;
}
@@ -28,9 +28,11 @@ import net.minecraft.world.level.block.state.BlockState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -40,11 +42,12 @@ public final class ModdedCustomContentRegistry {
private static final List<ModdedDataProvider> PROVIDERS = new CopyOnWriteArrayList<>();
private static final Map<String, BlockState> CUSTOM_BLOCKS = new ConcurrentHashMap<>();
private static volatile boolean scanned = false;
private static DiscoveryBatch discoveryBatch;
private ModdedCustomContentRegistry() {
}
public static void registerCustomBlockData(String namespace, String key, String state) {
public static synchronized void registerCustomBlockData(String namespace, String key, String state) {
if (namespace == null || key == null || state == null) {
return;
}
@@ -60,14 +63,24 @@ public final class ModdedCustomContentRegistry {
LOGGER.error("Iris custom block data '{}:{}' has unparseable state '{}'", namespace, key, state, error);
return;
}
CUSTOM_BLOCKS.put(identifier.toString(), parsed);
DiscoveryBatch activeBatch = discoveryBatch;
if (activeBatch == null) {
CUSTOM_BLOCKS.put(identifier.toString(), parsed);
} else {
activeBatch.customBlocks.put(identifier.toString(), parsed);
}
LOGGER.info("Iris registered custom block data {}:{} -> {}", namespace, key, state);
}
public static void register(ModdedDataProvider provider) {
public static synchronized void register(ModdedDataProvider provider) {
if (provider == null) {
return;
}
DiscoveryBatch activeBatch = discoveryBatch;
if (activeBatch != null) {
activeBatch.add(provider);
return;
}
for (ModdedDataProvider existing : PROVIDERS) {
if (existing.modId().equals(provider.modId())) {
LOGGER.warn("Iris custom content provider for '{}' already registered; ignoring duplicate", provider.modId());
@@ -83,17 +96,66 @@ public final class ModdedCustomContentRegistry {
LOGGER.info("Iris registered custom content provider '{}'", provider.modId());
}
public static void discover() {
public static synchronized Discovery discover() {
if (scanned) {
return;
return Discovery.unchanged();
}
scanned = true;
ServiceLoader<ModdedDataProvider> loader = ServiceLoader.load(ModdedDataProvider.class, ModdedCustomContentRegistry.class.getClassLoader());
for (ModdedDataProvider provider : loader) {
register(provider);
return discover(ServiceLoader.load(
ModdedDataProvider.class, ModdedCustomContentRegistry.class.getClassLoader()));
}
static synchronized Discovery discover(Iterable<ModdedDataProvider> discoveredProviders) {
List<ModdedDataProvider> previousProviders = List.copyOf(PROVIDERS);
Map<String, BlockState> previousCustomBlocks = new LinkedHashMap<>(CUSTOM_BLOCKS);
boolean previousDiscoveryComplete = scanned;
DiscoveryBatch batch = new DiscoveryBatch(previousProviders, previousCustomBlocks);
discoveryBatch = batch;
try {
for (ModdedDataProvider provider : discoveredProviders) {
batch.add(provider);
}
PROVIDERS.addAll(batch.additions);
CUSTOM_BLOCKS.putAll(batch.customBlocks);
scanned = true;
for (ModdedDataProvider provider : batch.additions) {
LOGGER.info("Iris registered custom content provider '{}'", provider.modId());
}
return new Discovery(previousProviders, previousCustomBlocks,
previousDiscoveryComplete, true);
} catch (Throwable failure) {
try {
restore(previousProviders, previousCustomBlocks, previousDiscoveryComplete);
} catch (Throwable rollbackFailure) {
if (rollbackFailure != failure) {
failure.addSuppressed(rollbackFailure);
}
}
LOGGER.error("Iris custom content provider discovery failed", failure);
if (failure instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (failure instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris custom content provider discovery failed", failure);
} finally {
discoveryBatch = null;
}
}
static synchronized boolean hasProvider(String modId) {
for (ModdedDataProvider provider : PROVIDERS) {
if (Objects.equals(provider.modId(), modId)) {
return true;
}
}
return false;
}
static boolean discoveryComplete() {
return scanned;
}
public static boolean hasProviders() {
return !PROVIDERS.isEmpty() || !CUSTOM_BLOCKS.isEmpty();
}
@@ -203,4 +265,68 @@ public final class ModdedCustomContentRegistry {
}
return state;
}
private static synchronized void restore(List<ModdedDataProvider> providers,
Map<String, BlockState> customBlocks,
boolean discoveryComplete) {
PROVIDERS.clear();
PROVIDERS.addAll(providers);
CUSTOM_BLOCKS.clear();
CUSTOM_BLOCKS.putAll(customBlocks);
scanned = discoveryComplete;
}
public static final class Discovery {
private final List<ModdedDataProvider> providers;
private final Map<String, BlockState> customBlocks;
private final boolean discoveryComplete;
private boolean active;
private Discovery(List<ModdedDataProvider> providers, Map<String, BlockState> customBlocks,
boolean discoveryComplete, boolean active) {
this.providers = providers;
this.customBlocks = customBlocks;
this.discoveryComplete = discoveryComplete;
this.active = active;
}
private static Discovery unchanged() {
return new Discovery(List.of(), Map.of(), true, false);
}
public synchronized void rollback() {
if (!active) {
return;
}
restore(providers, customBlocks, discoveryComplete);
active = false;
}
}
private static final class DiscoveryBatch {
private final List<ModdedDataProvider> providers;
private final List<ModdedDataProvider> additions = new ArrayList<>();
private final Map<String, BlockState> customBlocks;
private DiscoveryBatch(List<ModdedDataProvider> providers, Map<String, BlockState> customBlocks) {
this.providers = new ArrayList<>(providers);
this.customBlocks = new LinkedHashMap<>(customBlocks);
}
private void add(ModdedDataProvider provider) {
ModdedDataProvider candidate = Objects.requireNonNull(provider,
"Iris discovered a null custom content provider");
String modId = Objects.requireNonNull(candidate.modId(),
"Iris custom content provider returned a null mod id");
for (ModdedDataProvider existing : providers) {
if (modId.equals(existing.modId())) {
LOGGER.warn("Iris custom content provider for '{}' already registered; ignoring duplicate", modId);
return;
}
}
providers.add(candidate);
additions.add(candidate);
candidate.init();
}
}
}
@@ -25,9 +25,11 @@ import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.Locator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.WrongEngineBroException;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
@@ -889,16 +891,27 @@ public final class IrisModdedCommands {
fail(source, "This command can only be used by players.");
return 0;
}
if (IrisStructureLocator.isPlaced(engine, key)) {
locateIrisStructure(source, level, engine, player, key);
return 1;
}
Optional<NativeStructureTarget> resolved = resolveNativeStructure(source, level, engine, key);
if (resolved.isEmpty()) {
if (IrisStructureLocator.isPlaced(engine, key)) {
locateIrisStructure(source, level, engine, player, key);
return 1;
}
fail(source, "Unknown structure '" + key + "'. Use tab completion to choose an Iris placement or a registered native/datapack structure.");
return 0;
}
NativeStructureTarget target = resolved.get();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, target.key(), false);
if (!decision.generate()
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
fail(source, NativeStructureGenerationPolicy.generationStatusMessage(
target.key(), decision.status()));
return 0;
}
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
locateIrisStructure(source, level, engine, player, target.key());
return 1;
}
if (target.availability() != NativeStructureAvailability.AVAILABLE) {
fail(source, nativeUnavailableMessage(target.key(), target.availability()));
return 0;
@@ -1035,7 +1048,7 @@ public final class IrisModdedCommands {
}
}
int irisPlaced = IrisStructureLocator.placedKeys(engine).size();
ok(source, "Structure reachability: " + available + " native locatable, " + irisPlaced
ok(source, "Structure reachability: " + available + " native generation-eligible, " + irisPlaced
+ " Iris-placed, " + disabled + " native disabled, " + suppressed
+ " native replaced by Iris placements, " + unreachableBiomes
+ " native excluded by this pack's biomes, and " + unsupported
@@ -1044,16 +1057,22 @@ public final class IrisModdedCommands {
}
private static int verifyStructure(CommandSourceStack source, ServerLevel level, Engine engine, String key) {
if (IrisStructureLocator.isPlaced(engine, key)) {
ok(source, "Structure " + key + " is Iris-placed and locatable with /iris goto structure " + key + ".");
return 1;
}
Optional<NativeStructureTarget> target = resolveNativeStructure(source, level, engine, key);
if (target.isEmpty()) {
if (IrisStructureLocator.isPlaced(engine, key)) {
ok(source, "Structure " + key + " is Iris-placed and locatable with /iris goto structure " + key + ".");
return 1;
}
fail(source, "Unknown structure '" + key + "'. It is neither Iris-placed nor registered by vanilla or a datapack.");
return 0;
}
NativeStructureTarget resolved = target.get();
if (resolved.availability() == NativeStructureAvailability.IRIS_SUPPRESSED) {
ok(source, "Structure " + resolved.key()
+ " is explicitly replaced by an Iris placement and locatable with /iris goto structure "
+ resolved.key() + ".");
return 1;
}
if (resolved.availability() != NativeStructureAvailability.AVAILABLE) {
fail(source, nativeUnavailableMessage(resolved.key(), resolved.availability()));
return 0;
@@ -1084,9 +1103,9 @@ public final class IrisModdedCommands {
Engine engine, String key,
Holder.Reference<Structure> holder) {
boolean worldEnabled = source.getServer().getWorldGenSettings().options().generateStructures();
IrisImportedStructureControl control = engine.getDimension().getImportedStructures();
boolean selected = control != null && control.active() && control.shouldGenerate(key);
boolean suppressed = IrisStructureLocator.suppressesVanilla(engine, key);
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false);
boolean selected = decision.status() != NativeStructureGenerationStatus.DISABLED_BY_PACK;
boolean suppressed = decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS;
ChunkGenerator chunkGenerator = level.getChunkSource().getGenerator();
boolean biomeReachable = chunkGenerator instanceof IrisModdedChunkGenerator irisGenerator
&& irisGenerator.isNativeStructureReachable(holder);
@@ -1121,8 +1140,10 @@ public final class IrisModdedCommands {
private static String nativeUnavailableMessage(String key, NativeStructureAvailability availability) {
return switch (availability) {
case WORLD_DISABLED -> "Native structure generation is disabled for this world, so " + key + " cannot generate or be located.";
case FILTERED -> "Native structure " + key + " is disabled by this dimension's importedStructures settings.";
case IRIS_SUPPRESSED -> "Native structure " + key + " is replaced by an Iris placement in this pack; locate the Iris key instead.";
case FILTERED -> NativeStructureGenerationPolicy.generationStatusMessage(
key, NativeStructureGenerationStatus.DISABLED_BY_PACK);
case IRIS_SUPPRESSED -> NativeStructureGenerationPolicy.generationStatusMessage(
key, NativeStructureGenerationStatus.REPLACED_BY_IRIS);
case BIOME_UNREACHABLE -> "Native structure " + key + " cannot generate because none of its required biomes are produced by this Iris pack.";
case NO_PLACEMENT -> "Native structure " + key + " is registered, but its structure set has no placement supported by this dimension's generator state.";
case AVAILABLE -> "Native structure " + key + " is available.";
@@ -109,6 +109,7 @@ final class ModdedCommandHelp {
Entry.command("x&y", "", "Autoselect up, down and out", "xay"),
Entry.command("analyze", "<key>", "Show object composition"),
Entry.command("shrink", "<key>", "Shrink an object to its minimum size"),
Entry.command("plausibilize", "<key|prefix/> [dryrun=true] [reach=N]", "Grow branches so tree leaves survive vanilla decay"),
Entry.command("undo", "[amount]", "Undo pasted objects", "u")
));
SECTIONS.put("o", SECTIONS.get("object"));
@@ -19,6 +19,8 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.TreePlausibilizeBatch;
import art.arcane.iris.core.tools.TreePlausibilizer;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
@@ -27,6 +29,7 @@ import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.modded.ModdedBlockState;
import art.arcane.iris.modded.ModdedTileData;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
@@ -43,6 +46,7 @@ import net.minecraft.core.HolderLookup;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.Blocks;
@@ -157,7 +161,9 @@ public final class ModdedObjectCommands {
root.then(bukkitOnly("we", "WorldEdit selection import requires the Bukkit plugin with WorldEdit installed."));
root.then(bukkitOnly("studio", "The object studio world requires the Bukkit studio toolchain; it is not available on modded servers."));
root.then(bukkitOnly("convert", "Schematic conversion (.schem -> .iob) requires the Bukkit plugin."));
root.then(bukkitOnly("plausibilize", "Tree plausibilization requires the Bukkit plugin (its block-data tooling is not ported to modded yet)."));
root.then(Commands.literal("plausibilize")
.then(Commands.argument("args", StringArgumentType.greedyString()).suggests(OBJECT_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> plausibilize(context.getSource(), StringArgumentType.getString(context, "args")))));
return root;
}
@@ -645,6 +651,48 @@ public final class ModdedObjectCommands {
return 1;
}
private static int plausibilize(CommandSourceStack source, String raw) {
Engine engine = IrisModdedCommands.engineFor(source.getLevel());
IrisData data = engine == null ? null : engine.getData();
boolean dryRun = false;
int reach = TreePlausibilizer.DEFAULT_REACH;
StringBuilder targetBuilder = new StringBuilder();
for (String token : raw.trim().split("\\s+")) {
String lower = token.toLowerCase(Locale.ROOT);
if (lower.startsWith("dryrun=")) {
dryRun = Boolean.parseBoolean(lower.substring("dryrun=".length()));
continue;
}
if (lower.startsWith("reach=")) {
try {
reach = Integer.parseInt(lower.substring("reach=".length()));
} catch (NumberFormatException e) {
IrisModdedCommands.fail(source, "Invalid reach: " + token);
return 0;
}
continue;
}
if (!targetBuilder.isEmpty()) {
targetBuilder.append(' ');
}
targetBuilder.append(token);
}
String target = targetBuilder.toString();
List<TreePlausibilizeBatch.Target> targets = TreePlausibilizeBatch.resolve(target, data);
if (targets.isEmpty()) {
IrisModdedCommands.fail(source, "No objects matched: " + target);
return 0;
}
IrisModdedCommands.ok(source, "Plausibilize [reach=" + reach + (dryRun ? ", DRY" : "")
+ "] queued " + targets.size() + " object(s)");
boolean dry = dryRun;
int reachFinal = reach;
MinecraftServer server = source.getServer();
J.a(() -> TreePlausibilizeBatch.run(targets, dry, reachFinal, data, (String line) ->
server.execute(() -> IrisModdedCommands.ok(source, line))));
return 1;
}
private static int undo(CommandSourceStack source, int amount) {
ServerPlayer player = source.getPlayer();
UUID owner = player == null ? ModdedObjectUndo.CONSOLE : player.getUUID();

Some files were not shown because too many files have changed in this diff Show More