mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-07-13 02:15:46 +00:00
Compare commits
1397 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4dea984d77 | |||
| cd05fd1bca | |||
| e42ab6fae6 | |||
| 402bcb04fe | |||
| 6111b5670a | |||
| 0a7a531646 | |||
| d1fec88769 | |||
| 848e056bb8 | |||
| 6b64624233 | |||
| d1cc83a40b | |||
| 927c48f8c7 | |||
| 29ff3f01a7 | |||
| a8e46e3590 | |||
| 75e8c22268 | |||
| f777ea6b18 | |||
| 8fb627f9c1 | |||
| 3cfbcbf8ed | |||
| 79e80bef33 | |||
| 796d81e58b | |||
| b0a5d97e9c | |||
| ad6732812e | |||
| f482b8ef7e | |||
| e2f6061f98 | |||
| b98a341dc7 | |||
| 261a850af4 | |||
| 516537db72 | |||
| 921a98f210 | |||
| 2bac948edb | |||
| 26d9115904 | |||
| 763ff90a61 | |||
| f32094a404 | |||
| 6b24105640 | |||
| a3d7269a99 | |||
| e1196b0364 | |||
| 4c462ba78a | |||
| 65d4abe5c0 | |||
| 5142c048c7 | |||
| 88c87175b3 | |||
| 56a0c75a39 | |||
| 704c94aab4 | |||
| b5d08b6203 | |||
| b584cb4193 | |||
| af3035088a | |||
| df888e5516 | |||
| ca10edef0c | |||
| 986c7f12cd | |||
| 6c5e991051 | |||
| 1a834ed6f6 | |||
| c9929e0880 | |||
| 3063801a3a | |||
| 13002b04df | |||
| 1c9f9d92bb | |||
| 4568f0b4b6 | |||
| 1c08eb23b2 | |||
| 7e224325f3 | |||
| a1ff3b2c55 | |||
| abfc122171 | |||
| 6a68884ce9 | |||
| c5a2a57822 | |||
| 58d24ca55e | |||
| c6c02a9040 | |||
| a530a03459 | |||
| a82b8f4a9c | |||
| cf2e7df298 | |||
| d20b3af9b9 | |||
| 01d98d2141 | |||
| febe930db0 | |||
| 5c832ba128 | |||
| 03f9e233da | |||
| 11efae428f | |||
| 1a6fff35e8 | |||
| 540a416bc9 | |||
| 7eb6e87a81 | |||
| 9ff1c502ef | |||
| 08c6f7ddf9 | |||
| 4bc1913f03 | |||
| 821cc027db | |||
| 1968ef2f2c | |||
| ce29b70618 | |||
| 3d128b70a7 | |||
| 23fad24fb7 | |||
| 6df718e6ca | |||
| 787c728060 | |||
| 88bbce82fe | |||
| e6a8351e57 | |||
| b82472d521 | |||
| 8c05f1bf1d | |||
| 6a5616abae | |||
| 6096c39192 | |||
| b42d0e4710 | |||
| dcb3306197 | |||
| 9a231b2bcf | |||
| aa706d027b | |||
| 568fb07f66 | |||
| bf207b7062 | |||
| 18d4dce1db | |||
| 651dfa247e | |||
| 5b1ff6d2f7 | |||
| 589baafc28 | |||
| 130073989d | |||
| 2ca8cc7ad3 | |||
| 9f35855599 | |||
| 5110a33857 | |||
| a38e87cce3 | |||
| 1d09cd5f0f | |||
| 72c891ce5b | |||
| 3964185a81 | |||
| afc5f67bc2 | |||
| 1c72d6410f | |||
| c2c612eb35 | |||
| 415c2f5837 | |||
| b491d9efd0 | |||
| d643461e2e | |||
| 1f41e195cf | |||
| 7b9280083f | |||
| 73525c170d | |||
| 16c861a781 | |||
| d6bd5ec82f | |||
| 1829fa3eb1 | |||
| 8d215e3911 | |||
| ba819e4899 | |||
| 05d79b6d40 | |||
| cf64fbb730 | |||
| 43a65135b1 | |||
| 440c1bec6c | |||
| 0fd8bcc488 | |||
| bb1c0b8f68 | |||
| fc25acdea2 | |||
| 0aa6f6d523 | |||
| a79c1a7954 | |||
| 7885762cd7 | |||
| 86ed3f0095 | |||
| 99fcc8fd03 | |||
| 41888e33f6 | |||
| 25fa2553e5 | |||
| 86f78baecf | |||
| c31158578f | |||
| 4e86d7d634 | |||
| 62fe29cf34 | |||
| a3bcea4f3e | |||
| 7befce1084 | |||
| 351a1aa495 | |||
| d2cb8a9032 | |||
| 509715087c | |||
| 12527ecdd8 | |||
| 01a2999e03 | |||
| dbafe84fa5 | |||
| 40b020fc5d | |||
| d15f7d62d1 | |||
| f9c062c794 | |||
| 6a89b8bd06 | |||
| 194fcb2ea7 | |||
| 3b68f855b2 | |||
| cfbf68d37a | |||
| 9999f3e429 | |||
| 97fa0562a4 | |||
| 3abe671851 | |||
| e164a3bb5c | |||
| 4dfb4441e4 | |||
| e686f67453 | |||
| e2eed4812a | |||
| 1737833bfe | |||
| a4c5f46c37 | |||
| 025fa833c4 | |||
| 98cc82cc3d | |||
| 298365f588 | |||
| 90e5720e2e | |||
| 7cd43791f4 | |||
| a251d192ad | |||
| abfff28f43 | |||
| fbdae4c928 | |||
| 93ca26e368 | |||
| 8c1db1c223 | |||
| 123708601f | |||
| b5ab4968ba | |||
| 93ae6037bd | |||
| dd8e487a3b | |||
| d3c8377a12 | |||
| 84f3687c0c | |||
| ed1e1f1181 | |||
| 528c97f367 | |||
| f7a459f3bc | |||
| cd179b4321 | |||
| 6373dbb1b8 | |||
| 0b0797f876 | |||
| 446acefc91 | |||
| 7a44e555b2 | |||
| 57d4c2935c | |||
| 234fb1b0c4 | |||
| 0882b5acc4 | |||
| 18da26e1fa | |||
| 65aa95f2a5 | |||
| 5330ddc4ec | |||
| a7fdd37569 | |||
| a226fea9e2 | |||
| 8cea165a29 | |||
| 1d7cba184c | |||
| 4ca7ea3911 | |||
| ea5919def2 | |||
| be35e49302 | |||
| aadd03990a | |||
| 38a579453d | |||
| 0bf5da2ca1 | |||
| 317848692e | |||
| 22118de9e9 | |||
| d7039d120b | |||
| 979ee4e7d8 | |||
| f68d45bd30 | |||
| b86d7f303e | |||
| c573843314 | |||
| 51a7bef18e | |||
| 0e237aa1ad | |||
| b46c413f6b | |||
| f3ef1ca2ae | |||
| 703e61dd54 | |||
| e1ec6b7827 | |||
| f94292fdac | |||
| 7d153bf985 | |||
| f85f15ed02 | |||
| 867686eced | |||
| 526efd3ae1 | |||
| 9d796bd2a0 | |||
| 1a9a5d80ad | |||
| c5c7f9bdc5 | |||
| 01a421b732 | |||
| ae92bcf194 | |||
| 7e7933858b | |||
| 9c073ecbcb | |||
| f4617c1996 | |||
| 21a2e4feef | |||
| 258d0d3aaa | |||
| 27b2fd0823 | |||
| 0524adb0df | |||
| 3981b0976d | |||
| b5811cae08 | |||
| c998fd1fd9 | |||
| a7d874d37f | |||
| 7c41f86fb3 | |||
| e5e0561d5a | |||
| d50cdfec3e | |||
| 00997c1902 | |||
| 3095a92522 | |||
| fca309dec7 | |||
| 2793ed1035 | |||
| e5908285af | |||
| a8ee321eb8 | |||
| aa14242b54 | |||
| f6968269b4 | |||
| 0d0251e2f1 | |||
| 81b8fb02ae | |||
| 77842489e5 | |||
| eda1f59d3a | |||
| 5418868559 | |||
| 1d81daafbb | |||
| 609a3585c1 | |||
| 571dde608c | |||
| 3a13f5a7c1 | |||
| e5654b74d4 | |||
| a75738dd7a | |||
| 003be1f88b | |||
| 1eaafae20d | |||
| 79088c0305 | |||
| 9e147774bd | |||
| e51b632c8f | |||
| 1aa64c9a02 | |||
| 1e148d8fcd | |||
| f63cabd8b8 | |||
| 176d3a5f9f | |||
| eb184983de | |||
| d1c307865d | |||
| 4a26b8b34f | |||
| 33fd01c3ac | |||
| 34874080e7 | |||
| 43131ed8f6 | |||
| 9ea425aee4 | |||
| 709f05c1a5 | |||
| ec74add5de | |||
| d5b706764a | |||
| ca4c205a4a | |||
| 7b9c2ae6ad | |||
| d0e9d44152 | |||
| 2cdffaae33 | |||
| d4a8beac95 | |||
| 0e0e4075d8 | |||
| 9c492a2e66 | |||
| 96bf83684c | |||
| 25ea9ae62d | |||
| 7938c150dd | |||
| a7b4bf3ff2 | |||
| 693a05f2cb | |||
| e48cbe1f69 | |||
| 558f6fa8dd | |||
| 67b29cc363 | |||
| 4702534f9c | |||
| 29390c5e0a | |||
| e8bfce469d | |||
| 770e2f47a2 | |||
| 747e7b3330 | |||
| 8662f3b47a | |||
| 6e738e69e7 | |||
| 4bf14c83e1 | |||
| 837674c295 | |||
| e5f3bbd69e | |||
| 2885a39299 | |||
| bd1b06c761 | |||
| 3e1143112a | |||
| 834b214fbf | |||
| 768e569400 | |||
| a5d04333dd | |||
| ebe8da0fd5 | |||
| 5eb25f3977 | |||
| a5bca0a9bb | |||
| a82882c1c1 | |||
| 05193bd0d9 | |||
| 96efc15c36 | |||
| 67f456cf53 | |||
| 58b1bd115f | |||
| 8ddc8abdb9 | |||
| 12c2c71739 | |||
| 8705ca6e47 | |||
| a610d0a7a9 | |||
| 0648cfd3fa | |||
| fba9c17e3f | |||
| 74128d58cf | |||
| 70130e976d | |||
| 501c426302 | |||
| 8471f15bc8 | |||
| c4539441a0 | |||
| 472a98da16 | |||
| 76a6f1465a | |||
| b0ca0e3617 | |||
| 08b9058c8f | |||
| 556bef6e43 | |||
| 16a4d20c90 | |||
| e5f2fee926 | |||
| 3949468a60 | |||
| fc54fcb7eb | |||
| 9d6e4e87d8 | |||
| f50e964d4f | |||
| 8262e52893 | |||
| 1c41dc69ce | |||
| dee46a4284 | |||
| a8892b04ef | |||
| d49f7d7821 | |||
| 106a3834ab | |||
| 387e8adfe2 | |||
| 9ade90d9ca | |||
| 3d2392843a | |||
| a9891e819a | |||
| 02c13a9391 | |||
| f6590c26e7 | |||
| 64bb81626c | |||
| 343dc429d5 | |||
| 8f5f44bc96 | |||
| 6964b99744 | |||
| 11cfd85f6a | |||
| c5416f54fa | |||
| 94c5782490 | |||
| cc49b0f540 | |||
| 1bc6192c8b | |||
| 4b0766c097 | |||
| ec5cb2d646 | |||
| 0edaeeec99 | |||
| a0543bbbf2 | |||
| 0b1af51227 | |||
| 03c5998c02 | |||
| 6b193f695a | |||
| 0a30881f87 | |||
| c01a7def5d | |||
| 50db1d11a7 | |||
| 2e2ea8f1e4 | |||
| badf108d56 | |||
| f9888d19a5 | |||
| fbc5cee300 | |||
| 12777bc3f0 | |||
| 9324b1b5c0 | |||
| 2ecb555619 | |||
| 20c7891c2f | |||
| c35c858eee | |||
| cb93e78242 | |||
| c9ed4519a8 | |||
| 86d986dfbc | |||
| 54402faea8 | |||
| 77b4253624 | |||
| 44af23ba2e | |||
| 4e8079e431 | |||
| 39f65d02bf | |||
| 8c025a9ba2 | |||
| 7119fa8ef7 | |||
| 6f0b2b6bba | |||
| 0ae1334a57 | |||
| 5d28563b7c | |||
| cf8243a000 | |||
| cca0bed482 | |||
| a802edc375 | |||
| 3677931114 | |||
| e38dae0a32 | |||
| d537459c5a | |||
| 67398174bb | |||
| 0a0f77ff58 | |||
| e5cb4d82a3 | |||
| 9d44ac0b47 | |||
| 7f6d65a13e | |||
| ad720f4aa2 | |||
| 80548f753c | |||
| c9c8a9e412 | |||
| d048c073ac | |||
| 2929a1f0a7 | |||
| b6f9f68b9f | |||
| da2dd42e28 | |||
| 88360ef772 | |||
| 73787e21d2 | |||
| 840608a40f | |||
| fcedc35635 | |||
| 32d9a5e40a | |||
| 8df6253604 | |||
| 01b62c13b6 | |||
| f32f73e65a | |||
| 7b7118fe0d | |||
| 851ac18f0d | |||
| 37be7ca847 | |||
| ed67b4d3c2 | |||
| b62ac875d5 | |||
| 4c3f95b0e1 | |||
| 8712c8874c | |||
| ccc3bab8e0 | |||
| 113f25dab8 | |||
| cd80acdc7d | |||
| 9316ea9e5b | |||
| e2a3f25dcb | |||
| 944cc19ebc | |||
| 172e234514 | |||
| f9dac8a3a1 | |||
| eefbbab7ee | |||
| 9cf567e1ff | |||
| b8ee7561dd | |||
| c0d17742e8 | |||
| a88d389e0f | |||
| b341089996 | |||
| 9cbfd5a10b | |||
| fdaf8ff9d3 | |||
| e63d84c052 | |||
| 0d103a934a | |||
| 8119207254 | |||
| b66e6d8335 | |||
| ce2b62f5ae | |||
| c767b6c8e8 | |||
| 67328d7d10 | |||
| 329e136a66 | |||
| 52f87befa2 | |||
| 2ee22db072 | |||
| cc27e87376 | |||
| abb1d9cd62 | |||
| e0ad029c3d | |||
| 5705caa1ba | |||
| a56cd4c268 | |||
| 48cc6bb49c | |||
| 635ee02459 | |||
| 2c7b7c8c91 | |||
| ad0662be54 | |||
| 835c8422f1 | |||
| 2c60192de3 | |||
| adb7188eb9 | |||
| 2436ebb857 | |||
| ad85a0bbd1 | |||
| 22ac9ebf47 | |||
| 3cb9585dd8 | |||
| f4d1177c51 | |||
| b132862a60 | |||
| 2452a2f633 | |||
| f0476fea9b | |||
| 90c6457d37 | |||
| 61301ffd4d | |||
| e42317139d | |||
| f68600464b | |||
| 97ddfd309b | |||
| 5d42c5cae0 | |||
| 52fecb48d8 | |||
| ce29dc98c3 | |||
| 463e3d9658 | |||
| 9152b25d51 | |||
| bb9c72e414 | |||
| 976648340e | |||
| 5958bcb22e | |||
| 8eb35aa8be | |||
| 49ce84a9e9 | |||
| 6724b0f4c5 | |||
| e72abc8c39 | |||
| 0a2f35dd8d | |||
| c597c55c2c | |||
| f93995e152 | |||
| 7a5a2e5909 | |||
| e15d31a0ed | |||
| 09cdd61a68 | |||
| 0575cd85c8 | |||
| 2008975a8a | |||
| aad1d3815a | |||
| 86c64f99e9 | |||
| 6577f4a5de | |||
| 3415e7c7af | |||
| f1c72974fd | |||
| 26e2e20840 | |||
| c00dcf205b | |||
| a10a784c3b | |||
| f99cc61042 | |||
| d2a1e5cc1e | |||
| 3e2c0fa025 | |||
| 9c151abac7 | |||
| ab4770400e | |||
| 1a810d5d62 | |||
| 536d20bca7 | |||
| 9a691ac5b4 | |||
| 71078a20a9 | |||
| 566fca2b52 | |||
| 74e2576ca2 | |||
| fb0bc112e3 | |||
| 407e51378c | |||
| c468eb1ab1 | |||
| bdb7cc61e5 | |||
| e8f9e841c4 | |||
| 1b1b9d97b7 | |||
| 24355064ff | |||
| 06a45056d9 | |||
| dfe4894be7 | |||
| 8eb2287ec0 | |||
| c4f0722614 | |||
| 7fa1484b21 | |||
| 1c5eb8b910 | |||
| 94bf530d93 | |||
| 686ae57b5b | |||
| a911685aaf | |||
| 6899761ca9 | |||
| a58958fd62 | |||
| 307f3c9158 | |||
| 4f275c2e06 | |||
| 7e4e3f3cd8 | |||
| 84e5add564 | |||
| 4d4adbb76f | |||
| ff2f285784 | |||
| 8b1636e78a | |||
| 3bdad10562 | |||
| ac03a977aa | |||
| d7270f66e1 | |||
| b220b1bffa | |||
| 4796fe98cb | |||
| ece905ec6e | |||
| 53c9e7c04c | |||
| 29f6f52443 | |||
| a778cc51a6 | |||
| c6963d0cd3 | |||
| 489844f61b | |||
| 4d1b0246ca | |||
| 13f3511fa8 | |||
| f6f2766315 | |||
| 56530a1245 | |||
| 210a1f29a7 | |||
| 847bf972ae | |||
| e5d21fdf7e | |||
| 0b2fd3b358 | |||
| 10484d1226 | |||
| ce0092c52a | |||
| 474e033c2b | |||
| 62aad1f497 | |||
| 32b5157682 | |||
| 70717ea282 | |||
| 15975f108c | |||
| 66c66e82c1 | |||
| 4f6da95d8e | |||
| b37ccbdf01 | |||
| 30dbe0881a | |||
| 20ad4657a9 | |||
| d4986f42a6 | |||
| 8df15c0c2d | |||
| 24e1c578c8 | |||
| 1c3bff7559 | |||
| a09657b4d0 | |||
| 910220d3ca | |||
| fc05c24e3a | |||
| e1a7e772cf | |||
| 3c6411c322 | |||
| 628761affa | |||
| fa7b0f68ff | |||
| 487d0ac237 | |||
| e79e3fbe45 | |||
| 23a0ab23aa | |||
| c78ffab948 | |||
| d58f497b71 | |||
| 3284ab84c5 | |||
| 5cf0ac9315 | |||
| 4e4e820826 | |||
| cb6e4570f4 | |||
| fb59c7370d | |||
| c16e65f0a8 | |||
| d9681edf62 | |||
| 520c156d5f | |||
| 5c26ec2521 | |||
| d192e2b6d1 | |||
| 5b60b655ed | |||
| 97c7ac0528 | |||
| 090ff730a7 | |||
| c1f797e7c9 | |||
| 4a1a6b80a6 | |||
| e189b2389c | |||
| 3265447536 | |||
| 8c7c9f89e1 | |||
| d7a991b9b3 | |||
| abf6c93f2e | |||
| 1b76a66760 | |||
| c83ac67b47 | |||
| 1dca502a90 | |||
| cacef8c8fc | |||
| 623fd45ef4 | |||
| 007b4b0b53 | |||
| b6bacee095 | |||
| d5251350a1 | |||
| 1f9c72d093 | |||
| a1495a10e5 | |||
| 11ae48bea1 | |||
| b0f4b29a2d | |||
| c38bb1cd01 | |||
| 7faa727bd2 | |||
| 9e40259ca2 | |||
| 94a7692735 | |||
| 8b803a87f0 | |||
| 3ae6e92eaf | |||
| 0f3c52a5aa | |||
| 747be7aa09 | |||
| d651f204f8 | |||
| 2bd3ac7a9b | |||
| 5e437b34e3 | |||
| a5ef89a128 | |||
| 558b8e4eca | |||
| 0a001b8a63 | |||
| 6c6c9654c1 | |||
| e21fdf46e0 | |||
| a2ff5f58ed | |||
| b0eedee519 | |||
| e101155a4c | |||
| 3c9bcc9bb0 | |||
| 6763844030 | |||
| 08fa436885 | |||
| df8fa79209 | |||
| 8041db4f40 | |||
| 18e57e4097 | |||
| cc584ba377 | |||
| 0c92c20c65 | |||
| ec8af56f0d | |||
| 79341bf562 | |||
| 3f24d5c8e1 | |||
| 66a1739666 | |||
| 29007fdbfa | |||
| be3e8ebd51 | |||
| 38ad345f85 | |||
| 414f46a08d | |||
| 9144606688 | |||
| effb0f5b91 | |||
| bb7bbb6379 | |||
| dafca8e9db | |||
| 41b7aec084 | |||
| dfe1cce6de | |||
| fc793592f7 | |||
| c3ac41f894 | |||
| d0688782b1 | |||
| 25b41fe62c | |||
| b468478fcb | |||
| b6dc934198 | |||
| f58078e8a0 | |||
| b6457e47e6 | |||
| d275466e1e | |||
| f9cb107728 | |||
| 68ad206252 | |||
| 288bead792 | |||
| de670ddfd5 | |||
| 79d6f34879 | |||
| ceb6c15c97 | |||
| 7cf43ad7ab | |||
| ab3397a373 | |||
| 8e9f78e982 | |||
| 61bbfee640 | |||
| cf4796bd12 | |||
| bd89d8a308 | |||
| df2186d70f | |||
| f9638e830f | |||
| cd55a7fed4 | |||
| fc890a5ba1 | |||
| 2ea54b7f2f | |||
| e90e3901f5 | |||
| 36d58f29ab | |||
| 864c7ae27c | |||
| 6d104a2d1c | |||
| 89e754245d | |||
| 4c68d99c6f | |||
| f8f0a65f0a | |||
| 6ff74639b5 | |||
| af80f882da | |||
| 84ce7372c3 | |||
| e31d39c5f7 | |||
| 2296c1368b | |||
| 21d7d96dbc | |||
| 956e511fe0 | |||
| e427887f8c | |||
| 88ea43fbbe | |||
| 29526a80a9 | |||
| 48f901fc8c | |||
| 6c3f3dc889 | |||
| 743410d3ee | |||
| acccbb028f | |||
| 8b31fdc780 | |||
| 55017b9ac2 | |||
| 0288d35c85 | |||
| 8b3577dd0b | |||
| ced77ec70d | |||
| e1e90f5c8a | |||
| dc3487583d | |||
| e1d73ebcd5 | |||
| d7cf3b9500 | |||
| 8ce4e3c6b2 | |||
| 76f7d46b22 | |||
| cfc52ed909 | |||
| cee94d40e1 | |||
| cad7166cbe | |||
| c05fdbd0ec | |||
| c9b26ebaff | |||
| bee1973390 | |||
| 57efcab0b5 | |||
| b1ab2b84f8 | |||
| 02601e5ee4 | |||
| 070479acf3 | |||
| 6b2ba74237 | |||
| 619de29710 | |||
| 12556fa98f | |||
| bef4c0497f | |||
| 9193497d36 | |||
| ca6affbde9 | |||
| 81c40ce228 | |||
| 6ba1d571f2 | |||
| b0663f9b6c | |||
| 3ced6f3e9e | |||
| 3146d61efe | |||
| a967b6af85 | |||
| db3bc74364 | |||
| eb19d9a846 | |||
| 96dd299de7 | |||
| aee0806aa8 | |||
| e39f13bbb4 | |||
| 52ec80d384 | |||
| d9f8909bdc | |||
| 65aa9dc343 | |||
| 9bf7fdf174 | |||
| dafb59e5a8 | |||
| 79f86ca87d | |||
| 1749fcf6b5 | |||
| e1a0481cdf | |||
| 4576b95814 | |||
| e113fa6a61 | |||
| 07228afbff | |||
| afdffc2453 | |||
| 6348442962 | |||
| fd10c005b0 | |||
| 44500d6af9 | |||
| 6ddb0b5304 | |||
| 8ce0976f88 | |||
| 8345a58f2b | |||
| f92234297e | |||
| 6a5bcd5990 | |||
| 1fe6d9a636 | |||
| 97f6ab66c3 | |||
| b78c182d94 | |||
| 09a0f83013 | |||
| f68d7420e3 | |||
| 55c0fa5f32 | |||
| 8b5d1d0298 | |||
| b70e94dc65 | |||
| 49a6552168 | |||
| 0dd6892b28 | |||
| fec587edad | |||
| 712937d661 | |||
| ace434e7b0 | |||
| 1e5aa6e8b0 | |||
| 441b27f8f4 | |||
| 3f3bf70212 | |||
| a12eddbedf | |||
| 12abc1709e | |||
| 9e6035e7b4 | |||
| 20b41d65d3 | |||
| 1593bb2088 | |||
| d4f9a20379 | |||
| 6e247597a4 | |||
| 8a8be4545c | |||
| 05f4955989 | |||
| 93469fb3b4 | |||
| a70258d69f | |||
| c653d852e4 | |||
| 40163d25b8 | |||
| 5b4265783e | |||
| 720417b6c8 | |||
| bb78f412e0 | |||
| a919b91efb | |||
| a3dcf031c9 | |||
| 56eb4b6b84 | |||
| 393cb362db | |||
| 64e422036c | |||
| 468e7ef018 | |||
| e3e4ecbc5c | |||
| 545ffc0e9d | |||
| 8125c8d5f4 | |||
| 304b01d0cf | |||
| a5f687fd76 | |||
| fc1761a55b | |||
| e7c9cad7f6 | |||
| 979af82122 | |||
| 71a62b9c73 | |||
| bb020cab25 | |||
| 1ad35c1310 | |||
| 6470b2f4a9 | |||
| c1d5ba55cd | |||
| 100e450514 | |||
| 0f1d1d9860 | |||
| 178a462a4e | |||
| 8f4ae613f0 | |||
| 9e6963b6ce | |||
| cb9a73c60e | |||
| 0e666a4c35 | |||
| c80138a354 | |||
| 79a4ebcf65 | |||
| a7118aa785 | |||
| 2555cd23a0 | |||
| 484fbeca7b | |||
| 9a45e0df10 | |||
| 9175296fc6 | |||
| 5a4a86aeba | |||
| 768fa7beb5 | |||
| 6645eb9806 | |||
| d7a283c99f | |||
| 8f019cd794 | |||
| c1d9cc62cb | |||
| 010a1e9e91 | |||
| 856c926cde | |||
| 72ed312654 | |||
| c7fe4723f7 | |||
| bfbea83a4a | |||
| 55c58fe896 | |||
| 72949e0950 | |||
| 61c0ddb15b | |||
| 9ac4024e4e | |||
| 7fb2a51a33 | |||
| 5b7c0b2bc3 | |||
| fc15175ff9 | |||
| 1eec3a09c1 | |||
| 60d349dd8b | |||
| 0c8d144ffb | |||
| b898f73a05 | |||
| c2b9b0ba0e | |||
| 206a4e9057 | |||
| 83a0a7dd54 | |||
| 63b8a935ae | |||
| 16affd11cc | |||
| 33b0dec9da | |||
| 121a463788 | |||
| 3625e70948 | |||
| 3d90207172 | |||
| ca7dc19c81 | |||
| d0f16dbbc4 | |||
| fd91c223b7 | |||
| 93690b766c | |||
| cc4af9950b | |||
| a6281483dd | |||
| f587816b30 | |||
| eea3c3ab0b | |||
| ba68ab9e06 | |||
| eb94d97ea4 | |||
| 04ad02719c | |||
| f68f25b92f | |||
| 767cca36c8 | |||
| b30172fe89 | |||
| b2934b0cc2 | |||
| 50686795d0 | |||
| 5561a4dc2f | |||
| 15b8780e17 | |||
| 15c4d312cb | |||
| ccbea89253 | |||
| 7b1e666b3b | |||
| 8af212fca8 | |||
| 352ee7a622 | |||
| f26d0fac0b | |||
| 9612fef2a4 | |||
| a691d49abc | |||
| 289eca35ec | |||
| e9ca30257c | |||
| 6b21997baa | |||
| 6d3dbf84ef | |||
| eb45339c81 | |||
| 65e3fdd26c | |||
| 34cad85942 | |||
| e407679d2b | |||
| 8a08a3e148 | |||
| a01d27d273 | |||
| 43f642ae8b | |||
| 93c1265de9 | |||
| d08600fbaa | |||
| fb9f8d28f4 | |||
| cd3e9f772d | |||
| 7a95441799 | |||
| 9345828c97 | |||
| 58d88dd079 | |||
| 6cbc2374b3 | |||
| 12bd11386d | |||
| 0497045388 | |||
| d3f1640855 | |||
| 960e2fcc61 | |||
| 6a012cf400 | |||
| d658ec2099 | |||
| c9a07bd9d7 | |||
| c0136585e6 | |||
| dc4cfc49ad | |||
| 9fc72868d7 | |||
| 7d4c9d6c1e | |||
| e68b1a52ee | |||
| 955e8622b5 | |||
| 82640de06b | |||
| c93cb19563 | |||
| ccb7855d7d | |||
| bad547356c | |||
| 1740bb0021 | |||
| 20f05bf317 | |||
| 669ede5482 | |||
| ab658f58f9 | |||
| 1b3916749a | |||
| e29ec657d3 | |||
| 04f3a226a1 | |||
| 099d1cc362 | |||
| c11e9bf704 | |||
| ffa0b984a0 | |||
| f0f9261ed3 | |||
| ba433bc244 | |||
| 27f8ad982a | |||
| 9b89ae7846 | |||
| c11b5342e3 | |||
| b9c88f5b12 | |||
| 02c7ab71d4 | |||
| 8ec1ac9875 | |||
| e03027a386 | |||
| 387cf566c9 | |||
| dff7665934 | |||
| 9421d788f3 | |||
| d960574b24 | |||
| e8671cbbf0 | |||
| cbf8039eea | |||
| d7415e3711 | |||
| 26e71ee6b5 | |||
| f8c42b3e48 | |||
| 886bed9634 | |||
| 7b7023fc99 | |||
| 701bfe4a54 | |||
| 290d7a93ab | |||
| 729bcc7ba0 | |||
| e46b68e2f3 | |||
| cdf73c4629 | |||
| 2f0dd2d172 | |||
| 7e5704bf80 | |||
| ef96fb9a0e | |||
| f2370ad814 | |||
| 0783620104 | |||
| 2193b02c0a | |||
| aef05bad20 | |||
| e002ab6378 | |||
| a2c2b4a2d4 | |||
| 3ae896457b | |||
| 683a90ed09 | |||
| 5685e25234 | |||
| 6edf083e7b | |||
| 995d1495eb | |||
| d5f74631cd | |||
| dd5b9b2e81 | |||
| ca78200c1c | |||
| d73c2a65ae | |||
| 0ee2cb8e3c | |||
| 5ba429ec73 | |||
| 709825d073 | |||
| 3fe73af5e8 | |||
| ccc0b0dd7d | |||
| 1114ede267 | |||
| 5546f12499 | |||
| 4f38acb592 | |||
| 39f59e023c | |||
| 5ed5322fce | |||
| 33b926d547 | |||
| af8778c6cd | |||
| bb4ad08cc5 | |||
| ee4eb7b3f0 | |||
| 5bc024a6ee | |||
| 0546caeb05 | |||
| a75348d3ee | |||
| 3f3f947f43 | |||
| 5127b7c959 | |||
| a28df78792 | |||
| 4f888f16bc | |||
| 55ed9d7132 | |||
| 779a56c3a1 | |||
| f6440bde07 | |||
| 930dc2fd1c | |||
| 1e96fd529b | |||
| 167c865bb2 | |||
| 4e6b6c11a3 | |||
| bfdb71ca91 | |||
| f590f9824e | |||
| b3a087702d | |||
| 1a786fb418 | |||
| 650d38e212 | |||
| 130cb7ad93 | |||
| 85e8ffeaa3 | |||
| db40f608fa | |||
| a7b471682a | |||
| 4595fea7d7 | |||
| 4580f8d9a4 | |||
| 37f76fa133 | |||
| 5507c35547 | |||
| 350328f99c | |||
| 91ec6b773d | |||
| c0fdf23133 | |||
| 28523a9593 | |||
| 07e0b52ea3 | |||
| 008e758da8 | |||
| 7ce219a6a8 | |||
| 1809d4a53f | |||
| 54ae026c2b | |||
| 57c647fa0e | |||
| 9745f23fb2 | |||
| 7d2062f298 | |||
| 306a1948cf | |||
| ac4c00d3f2 | |||
| db43bc300e | |||
| 6494cded62 | |||
| b5c1e7e2da | |||
| 83e311870f | |||
| 8fc6e3b335 | |||
| 18a69e97b3 | |||
| 80d8449f02 | |||
| d830aa1d1a | |||
| d473126d4c | |||
| 17f8fe69fb | |||
| 7263d102c7 | |||
| 815b2235a2 | |||
| f61247f8bb | |||
| 205d9d528e | |||
| 1044f901bd | |||
| de261a38fd | |||
| 81776e60fb | |||
| 0c179eae86 | |||
| c30de98449 | |||
| 47990cde46 | |||
| b098c210ce | |||
| 6edd8bf119 | |||
| de0e124ebb | |||
| 5fc012a557 | |||
| 32afcc3478 | |||
| a27f37945d | |||
| 0adfe6911f | |||
| 8bb1964b30 | |||
| cc95e1ae63 | |||
| 9843f18c96 | |||
| 18ea61fcc0 | |||
| 5b3fdb6e62 | |||
| c5220c8d06 | |||
| 74e87a7fae | |||
| c809f50c3b | |||
| e7e46f78c7 | |||
| 2b449a1f3d | |||
| f257f2c551 | |||
| 7f91d73d27 | |||
| 15a90edb03 | |||
| b2f1e16d9a | |||
| f544455708 | |||
| 5118997188 | |||
| b6ae649cd1 | |||
| a6d0d85b84 | |||
| c71c2494af | |||
| 4a3a7bc98d | |||
| 4e4f7f693d | |||
| 7a729c39a6 | |||
| 48b757f693 | |||
| b449938730 | |||
| ba55ff6a4b | |||
| 2b4bc621ac | |||
| acac2d0fc5 | |||
| 6ff5975918 | |||
| 8912cfe37b | |||
| 9e02c318a1 | |||
| f6b51c21d5 | |||
| df3e4b055f | |||
| ffcd2b4179 | |||
| 7afdbc2a53 | |||
| 5004481685 | |||
| 1905741a7c | |||
| 8bf7451107 | |||
| fdfb708ce0 | |||
| 68a4eee8bb | |||
| 746ebfffbd | |||
| 213f8645bc | |||
| 1b2541bd84 | |||
| 522de89792 | |||
| 5feaa45edf | |||
| 9333775e0e | |||
| afd15ad450 | |||
| e7fc9e6fe5 | |||
| 021f9b1d0e | |||
| dc21f05482 | |||
| c22ccf71a7 | |||
| d5b9a074e6 | |||
| 63e6104736 | |||
| de475c9561 | |||
| 3f4fa0c67c | |||
| ca3f228b89 | |||
| 2b159041ac | |||
| 5fa564969e | |||
| 8ba8627281 | |||
| a12cb59a51 | |||
| dd124fab33 | |||
| 9f8ef4c1f3 | |||
| 563c6d0403 | |||
| 1bad0fe048 | |||
| b40dcd3164 | |||
| 8a0b443d28 | |||
| 5f38503ac5 | |||
| 231d1209c9 | |||
| 06bc180127 | |||
| fb2221fa8d | |||
| e1c7f38bb8 | |||
| a82fa578eb | |||
| 0ecefdcc1c | |||
| 8bacc8128b | |||
| 3dff96152c | |||
| 79e198d441 | |||
| e6cf33e262 | |||
| f839ca674b | |||
| f9427d1258 | |||
| bd2cdc9bd0 | |||
| 67352311de | |||
| 555a7682e1 | |||
| 19d94489d7 | |||
| 0175cfa986 | |||
| 1b0ff36d51 | |||
| f5c499ab60 | |||
| 81e3fd346a | |||
| 3bca1f5304 | |||
| 593e96a31a | |||
| 169b783e4f | |||
| f129742a41 | |||
| 01016d1b14 | |||
| a883e43acb | |||
| e0d0657e7b | |||
| e4c27bd49c | |||
| 7a31fd31eb | |||
| 4f0f2c3213 | |||
| d9889b06ad | |||
| 9f5cff4ab8 | |||
| 958543a171 | |||
| b8e03f5e13 | |||
| e76218b39f | |||
| 7d1a375bda | |||
| dfaa80f1e4 | |||
| 8939a903be | |||
| d6f1ea78fa | |||
| f25c4c74be | |||
| 1619df1ba3 | |||
| 566169cdbf | |||
| ee3e631789 | |||
| 8202754988 | |||
| 447d7596dc | |||
| 7c4568066f | |||
| 46ee7a5983 | |||
| 5a8ef99afa | |||
| 571729cfe4 | |||
| c1de316ed6 | |||
| 1e316acd64 | |||
| f89460538d | |||
| 0bf0062c2c | |||
| 540d1b5801 | |||
| 8f0d1b7b7b | |||
| 5061791dcf | |||
| ce98264552 | |||
| 15e6750e11 | |||
| 7a20421580 | |||
| f42f06226c | |||
| cce3d74c52 | |||
| 7b9fb880f4 | |||
| 2a9b0a54d9 | |||
| 373e9e9755 | |||
| 4115dd5797 | |||
| 57bfd251dc | |||
| d324790f66 | |||
| 99d3dba440 | |||
| 5a44e79ad1 | |||
| 86808017db | |||
| f735db9843 | |||
| 5a333c23ac | |||
| 4d2d51edfa | |||
| dcfd5a0cd8 | |||
| a821c0da50 | |||
| 804147e2c4 | |||
| ed346291f9 | |||
| 2190f6eff8 | |||
| ce30e24e42 | |||
| d5e13541c8 | |||
| 2e707adaa0 | |||
| 4d8e36cb05 | |||
| 73b710cdde | |||
| 09182bf3f3 | |||
| 9a945837cd | |||
| b64c821ef4 | |||
| a9ab3bc519 | |||
| 94292cea6e | |||
| b5fdeb2a02 | |||
| fa3ac36e8c | |||
| 6632d1b65f | |||
| 8805693800 | |||
| 93db7ee0ee | |||
| 9ba4dd4099 | |||
| 36efacf43c | |||
| d9bf91afa5 | |||
| b0c96be841 | |||
| 673b08f174 | |||
| 989778af26 | |||
| f5a3a7eb1a | |||
| 7a97928c21 | |||
| 12a6d022cf | |||
| e3d2dfa99e | |||
| 35cc39e9e2 | |||
| 006b0b4a03 | |||
| ec939b9f78 | |||
| 7a94f53735 | |||
| a9efe146ba | |||
| 956e2f6b06 | |||
| 72623e0acf | |||
| e0f673bc3c | |||
| fe40f12d2e | |||
| 0fda7a8506 | |||
| 22887da769 | |||
| a1e0a8ffc1 | |||
| 62010116d7 | |||
| 6ebcd02ae6 | |||
| 051015656a | |||
| ee082762c6 | |||
| e967b5e052 | |||
| 36505e2fa1 | |||
| 502aa054f6 | |||
| cc5a880fd7 | |||
| 6f9ad8b0eb | |||
| ac6ab74d48 | |||
| 807ed2b247 | |||
| 367de5a8fd | |||
| 5bf2da714a | |||
| cc90f42deb | |||
| 20ceaead09 | |||
| 3ec2f8fb7b | |||
| 6c5fac154e | |||
| 540ab8f0d2 | |||
| 17d2ac8d70 | |||
| a6ebdead19 | |||
| aa7631ecd0 | |||
| 4e138cad9f | |||
| 7e55b5fcee | |||
| e19e5278b4 | |||
| dfcd2dc83e | |||
| 8775f842e6 | |||
| 7b07a4ba6c | |||
| d78e5973e9 | |||
| 110d296184 | |||
| 23cd5c117b | |||
| 131c4692bc | |||
| f6571367db | |||
| f5c64c7480 | |||
| 085f63a915 | |||
| 3e022e1931 | |||
| 0dba3725ae | |||
| 8bb409df4e | |||
| 603168a147 | |||
| 2a95edd860 | |||
| e94406fb45 | |||
| e5a7b5d0c6 | |||
| d6f816fe2f | |||
| c15d4a349f | |||
| 3ce832583c | |||
| 5514fd2645 | |||
| 66d07dcaca | |||
| c1cf8e88ee | |||
| 684bd739b9 | |||
| d4c0e07b1d | |||
| a6ea6fcfb2 | |||
| c366ec0c40 | |||
| 8d715e2e4e | |||
| dea3ec80ac | |||
| 4053f05ba9 | |||
| ef07ec2c62 | |||
| 0ffc64231b | |||
| fe112c3ba5 | |||
| 2fa03aac3b | |||
| 6ac5ad880d | |||
| 54f14392d9 | |||
| c7ecdaf8a8 | |||
| 497c24a3b6 | |||
| 00aa26e602 | |||
| 3796381156 | |||
| b0c5700cf5 | |||
| 78a4b1d2ce | |||
| e5e6b9848f | |||
| 3e36f57f14 | |||
| d794ede4d8 | |||
| 7a4d97d76a | |||
| e76ab1d367 | |||
| 9731feff7f | |||
| 4ce790082d | |||
| 065b748036 | |||
| 76891d246f | |||
| 0832007991 | |||
| 87302a046c | |||
| f0f7453b32 | |||
| 2991bea248 | |||
| 673b42c2f5 | |||
| 7d994e27e3 | |||
| 5f2b82aac7 | |||
| af1a03cb67 | |||
| 85fbbeca9d | |||
| a64272620f | |||
| ef9966d02b | |||
| a28e5bb38a | |||
| 34670759e9 | |||
| 7dd1d3881a | |||
| 7b93542014 | |||
| ea1ac0a154 | |||
| 35c7068fa6 | |||
| db4cbb2ea6 | |||
| fa7c073999 | |||
| 72a077662d | |||
| 137abb04b2 | |||
| 211b15332d | |||
| f6fbcade17 | |||
| 1ef115fee8 | |||
| 7973444fc5 | |||
| 2bdb0bfa69 | |||
| bfb6f82ea3 | |||
| 7729ce3753 | |||
| f6ea171669 | |||
| ff081ebc11 | |||
| 0818f971fe | |||
| 27070f44c7 | |||
| 8331cbe375 | |||
| f533ec34b0 | |||
| 23d4fcb827 | |||
| a248962f1b | |||
| afa8fad8e9 | |||
| 4c423bb493 | |||
| e660fe9e1e | |||
| 23c1d12e73 | |||
| 9cf2785626 | |||
| b8b65c7e3e | |||
| dcb467280a | |||
| 6aa802b42d | |||
| d78f6ca589 | |||
| 493f8e0cc0 | |||
| 119563c553 | |||
| 2d61e6af66 | |||
| aa1be934a9 | |||
| f9c98b0a60 | |||
| bd66fa3bf5 | |||
| 380f191f6a | |||
| 7e217b5fba | |||
| c4a516a858 | |||
| bdf181e348 | |||
| f4ea1343b4 | |||
| a37ccddd38 | |||
| 243ef8c0be | |||
| 00d099383b | |||
| dda3f0b8e9 | |||
| 00f4d8b1ee | |||
| 2f5198d533 | |||
| 148261f876 | |||
| 60843b3bb9 | |||
| 2ad9a525bd | |||
| 78e9f21439 | |||
| a66d60eaea | |||
| 3aa3c13477 | |||
| cc850522e6 | |||
| 6d28f57f88 | |||
| 563e0a7cd4 | |||
| 2f2f857e98 | |||
| 251c52a2ee | |||
| 7528bd343b | |||
| 06d9f279ac | |||
| 044403b829 | |||
| 5a24dd3b49 | |||
| 3189b3c7a5 | |||
| 3bf1c6a282 | |||
| 7c7309c3ab | |||
| da777da476 | |||
| 3d0c0a11ed | |||
| e545269b93 |
@@ -40,6 +40,9 @@ body:
|
||||
- 1.17.1
|
||||
- 1.18
|
||||
- 1.19
|
||||
- 1.20
|
||||
- 1.21
|
||||
- 1.22
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Verify (core, plugin, spi, probes)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '25'
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
- name: Run verification gates
|
||||
run: ./gradlew :core:check :adapters:bukkit:plugin:test :spi:build :probe:test :probe:run :probe:deserializationProbe -PuseLocalVolmLib=false --console=plain --stacktrace
|
||||
- name: Run modded shared tests
|
||||
run: ./gradlew -p adapters/fabric test -PuseLocalVolmLib=false --console=plain --stacktrace
|
||||
+31
-1
@@ -1,5 +1,6 @@
|
||||
|
||||
build/
|
||||
libs/
|
||||
|
||||
.gradle/
|
||||
|
||||
@@ -9,4 +10,33 @@ build/
|
||||
|
||||
collection/
|
||||
|
||||
src/main/java/com/volmit/iris/util/uniques/
|
||||
out/
|
||||
tools/simd-bench/out/
|
||||
tools/simd-bench/simd-bench.jar
|
||||
|
||||
DataPackExamples/
|
||||
|
||||
TreeGenStuff/
|
||||
dist/
|
||||
core/plugins/
|
||||
adapters/bukkit/plugin/plugins/
|
||||
adapters/*/run/
|
||||
|
||||
.codegraph/
|
||||
|
||||
plans/
|
||||
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
local.properties
|
||||
*.jks
|
||||
*.p12
|
||||
*.pfx
|
||||
*.pem
|
||||
*.key
|
||||
*.hprof
|
||||
credentials.json
|
||||
service-account*.json
|
||||
|
||||
docs/
|
||||
|
||||
@@ -1,77 +1,136 @@
|
||||
# Iris
|
||||
|
||||
The master branch is for the latest version of minecraft.
|
||||
Iris is a world generation engine for Minecraft servers and mod loaders. It generates terrain,
|
||||
biomes, caves, structures, objects, and entities from editable JSON packs, with a full in-game
|
||||
studio authoring workflow. The same engine runs as a Bukkit-family plugin and as a Fabric, Forge,
|
||||
or NeoForge server mod, and produces bit-identical terrain on every platform for the same pack and
|
||||
seed. The master branch targets the current Minecraft version (26.2).
|
||||
|
||||
# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://docs.volmit.com/iris/) **|** [Git](https://github.com/IrisDimensions)
|
||||
|
||||
# Building
|
||||
Consider supporting development by buying Iris on Spigot.
|
||||
|
||||
Building Iris is fairly simple, though you will need to setup a few things if your system has never been used for java
|
||||
development.
|
||||
## Platforms
|
||||
|
||||
Consider supporting our development by buying Iris on spigot! We work hard to make Iris the best it can be for everyone.
|
||||
| Platform | Artifact | Minecraft | Notes |
|
||||
|---|---|---|---|
|
||||
| Paper / Purpur / Leaf / Canvas | plugin jar | 26.2 | Full feature set |
|
||||
| Folia | plugin jar | 26.2 | Region-safe scheduling throughout |
|
||||
| Spigot / CraftBukkit | plugin jar | 26.2 | Full feature set |
|
||||
| Fabric | mod jar | 26.2 | Server worldgen + client HUD; requires Fabric Loader 0.19.3+ |
|
||||
| Forge | mod jar | 26.2 | Server worldgen + client HUD; requires Forge 65.0.0+ |
|
||||
| NeoForge | mod jar | 26.2 | Server worldgen + client HUD; requires NeoForge 26.2+ |
|
||||
|
||||
### Command Line Builds
|
||||
Java 25 is required on every platform.
|
||||
|
||||
1. Install [Java JDK 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
|
||||
2. Set the JDK installation path to `JAVA_HOME` as an environment variable.
|
||||
* Windows
|
||||
1. Start > Type `env` and press Enter
|
||||
2. Advanced > Environment Variables
|
||||
3. Under System Variables, click `New...`
|
||||
4. Variable Name: `JAVA_HOME`
|
||||
5. Variable Value: `C:\Program Files\Java\jdk-17.0.1` (verify this exists after installing java don't just copy
|
||||
the example text)
|
||||
* MacOS
|
||||
1. Run `/usr/libexec/java_home -V` and look for Java 17
|
||||
2. Run `sudo nano ~/.zshenv`
|
||||
3. Add `export JAVA_HOME=$(/usr/libexec/java_home)` as a new line
|
||||
4. Use `CTRL + X`, then Press `Y`, Then `ENTER`
|
||||
5. Quit & Reopen Terminal and verify with `echo $JAVA_HOME`. It should print a directory
|
||||
3. If this is your first time building Iris for MC 1.18+ run `gradlew setup` inside the root Iris project folder.
|
||||
Otherwise, skip this step. Grab a coffee, this may take up to 5 minutes depending on your cpu & internet connection.
|
||||
4. Once the project has setup, run `gradlew iris`
|
||||
5. The Iris jar will be placed in `Iris/build/Iris-XXX-XXX.jar` Enjoy! Consider supporting us by buying it on spigot!
|
||||
The modded feature set matches the plugin wherever the operation is not Bukkit-bound: worldgen
|
||||
from Iris packs, authoring with mod blocks/items/entities (with validation suggestions), studio
|
||||
workspaces with schema autocomplete over the server's live registries, entity spawning parity
|
||||
including death loot, pregeneration with a boss bar or client HUD, the `/iris` command tree, and
|
||||
the goldenhash determinism gate, which is interchangeable across all four platforms.
|
||||
|
||||
### IDE Builds (for development)
|
||||
## Install
|
||||
|
||||
* Run `gradlew setup` any time you get dependency issues with craftbukkit
|
||||
* Configure ITJ Gradle to use JDK 17 (in settings, search for gradle)
|
||||
* Add a build line in the build.gradle for your own build task to directly compile Iris into your plugins folder if you
|
||||
prefer.
|
||||
* Resync the project & run your newly created task (under the development folder in gradle tasks!)
|
||||
**Plugin (Paper/Purpur/Leaf/Canvas/Folia/Spigot):** drop the plugin jar into `plugins/` and start
|
||||
the server. On first boot Iris downloads the default `overworld` pack automatically.
|
||||
|
||||
# Iris Toolbelt
|
||||
**Mod (Fabric/Forge/NeoForge):** drop the mod jar into `mods/` and start the server. The jar is
|
||||
self-contained (core, SPI, and required Fabric API modules are bundled). On first boot Iris
|
||||
downloads the default `overworld` pack before the worldgen datapack is written, so the default
|
||||
pack is fully active immediately. Packs installed later register their custom dimension types
|
||||
(height ranges) and custom biomes through the forced datapack at server start — restart once after
|
||||
adding a pack so worlds get its full heights and biomes; worlds created before that restart run
|
||||
with fallback heights.
|
||||
|
||||
Everyone needs a tool-belt.
|
||||
**Singleplayer (modded clients):** installed Iris packs appear as selectable World Types on the
|
||||
Create New World screen; the integrated server runs the same engine.
|
||||
|
||||
```java
|
||||
package com.volmit.iris.core.tools;
|
||||
## The client mod
|
||||
|
||||
// Get IrisDataManager from a world
|
||||
IrisToolbelt.access(anyWorld).getCompound().getData();
|
||||
Installing the mod jar on a client adds a native pregeneration HUD: a top-left panel with a
|
||||
progress bar, chunks done/total, percent, chunks per second, and ETA, turning yellow while the
|
||||
pregen is paused. The `H` key (rebindable, under the "Iris" controls category) toggles it.
|
||||
|
||||
// Get Default Engine from world
|
||||
IrisToolbelt.access(anyWorld).getCompound().getDefaultEngine();
|
||||
The HUD works against modded Iris servers and against Bukkit/Paper Iris servers, both over the
|
||||
`irisworldgen:main` channel (custom payloads on modded, plugin messaging on Bukkit). Vanilla
|
||||
clients are unaffected and get the server-side boss bar instead; on non-Iris servers the client
|
||||
mod is inert.
|
||||
|
||||
// Get the engine at the given height
|
||||
IrisToolbelt.access(anyWorld).getCompound().getEngineForHeight(68);
|
||||
## Quickstart
|
||||
|
||||
// IS THIS THING ON?
|
||||
boolean yes=IrisToolbelt.isIrisWorld(world);
|
||||
Create and enter an Iris world.
|
||||
|
||||
// GTFO for worlds (moves players to any other world, just not this one)
|
||||
IrisToolbelt.evacuate(world);
|
||||
Plugin (optional arguments are keyed):
|
||||
|
||||
IrisAccess access=IrisToolbelt.createWorld() // If you like builders...
|
||||
.name("myWorld") // The world name
|
||||
.dimension("terrifyinghands")
|
||||
.seed(69133742) // The world seed
|
||||
.headless(true) // Headless make gen go fast
|
||||
.pregen(PregenTask // Define a pregen job to run
|
||||
.builder()
|
||||
.center(new Position2(0,0)) // REGION coords (1 region = 32x32 chunks)
|
||||
.radius(4) // Radius in REGIONS. Rad of 4 means a 9x9 Region map.
|
||||
.build())
|
||||
.create();
|
||||
```
|
||||
/iris create myworld type=overworld seed=1337
|
||||
/iris load myworld
|
||||
```
|
||||
|
||||
Mod (positional arguments):
|
||||
|
||||
```
|
||||
/iris create myworld overworld 1337
|
||||
```
|
||||
|
||||
Pregeneration requires a radius in blocks. On the plugin, optional arguments are keyed; on modded
|
||||
servers they are positional and composable:
|
||||
|
||||
```
|
||||
/iris pregen start 352 world=myworld center=0,0 gui=false
|
||||
/iris pregen start 352 irisworldgen:myworld at 0 0 sync
|
||||
```
|
||||
|
||||
Players with the client mod see the native HUD; everyone else gets a boss bar (modded) or
|
||||
console/status output. `/iris pregen status` reports progress on the plugin.
|
||||
|
||||
## Studio and VSCode workspace
|
||||
|
||||
The studio is the pack authoring environment, available on all platforms. Studio worlds are
|
||||
transient — they are deleted on close and purged at startup.
|
||||
|
||||
```
|
||||
/iris studio create <name> [template] scaffold a new pack (default template: example)
|
||||
/iris studio open <pack> [seed] open a temporary studio world for live editing
|
||||
/iris studio vscode [pack] write a .code-workspace with JSON schemas
|
||||
/iris studio update [pack] regenerate the workspace schemas
|
||||
/iris studio close close and discard the studio world
|
||||
```
|
||||
|
||||
As in the quickstart, optional arguments are keyed on the plugin (`seed=1234`) and positional on
|
||||
the mod.
|
||||
|
||||
The generated VSCode workspace wires per-type JSON schemas (dimensions, biomes, regions, objects,
|
||||
loot, entities, snippets) for full autocomplete. Schemas are generated from the server's live
|
||||
registries, so on modded servers block, item, entity, enchantment, and potion-effect completion
|
||||
includes installed mod content (for example `create:brass_ingot`). Editing an open studio's pack
|
||||
files hotloads the changes and regenerates the schemas.
|
||||
|
||||
## Building from source
|
||||
|
||||
Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everything else.
|
||||
|
||||
```
|
||||
./gradlew buildAllToOut
|
||||
```
|
||||
|
||||
builds every platform artifact into `dist/`:
|
||||
|
||||
```
|
||||
Iris v<version> [CraftBukkit] <mc>.jar
|
||||
Iris v<version> [Fabric] <mc>+<loader>.jar
|
||||
Iris v<version> [Forge] <mc>+<loader>.jar
|
||||
Iris v<version> [NeoForge] <mc>+<loader>.jar
|
||||
```
|
||||
|
||||
Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `buildNeoforge`. The
|
||||
developer SPI jar (the pure-JVM platform API contract) is built to `spi/build/libs/` by
|
||||
`./gradlew :spi:jar`.
|
||||
|
||||
If you need help compiling as a developer or contributor, ask in the Discord. Do not come to the
|
||||
Discord asking for free copies or a compile tutorial.
|
||||
|
||||
## Maintainer docs
|
||||
|
||||
- [Minecraft version bump checklist](docs/mc-version-bump.md)
|
||||
- [Release checklist](docs/release-checklist.md)
|
||||
|
||||
+438
@@ -0,0 +1,438 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.CraftServer;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class CustomBiomeSource extends BiomeSource {
|
||||
private static final int NOISE_BIOME_CACHE_MAX = 262144;
|
||||
|
||||
private final long seed;
|
||||
private final Engine engine;
|
||||
private final Registry<Biome> biomeCustomRegistry;
|
||||
private final Registry<Biome> biomeRegistry;
|
||||
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
||||
private final KMap<String, Holder<Biome>> customBiomes;
|
||||
private final Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
|
||||
private final Holder<Biome> fallbackBiome;
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
||||
|
||||
public CustomBiomeSource(long seed, Engine engine, World world) {
|
||||
this.engine = engine;
|
||||
this.seed = seed;
|
||||
this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null);
|
||||
this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null);
|
||||
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
|
||||
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
|
||||
this.vanillaSpawnBiomes = fillVanillaSpawnBiomes(this.biomeCustomRegistry, this.biomeRegistry, engine);
|
||||
}
|
||||
|
||||
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine, Holder<Biome> fallback) {
|
||||
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
|
||||
if (fallback != null) {
|
||||
biomes.add(fallback);
|
||||
}
|
||||
|
||||
for (IrisBiome i : engine.getAllBiomes()) {
|
||||
Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, i.getVanillaDerivative());
|
||||
if (vanillaHolder != null) {
|
||||
biomes.add(vanillaHolder);
|
||||
} else if (!i.isCustom() && fallback != null) {
|
||||
biomes.add(fallback);
|
||||
}
|
||||
|
||||
if (i.isCustom()) {
|
||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
|
||||
if (customHolder != null) {
|
||||
biomes.add(customHolder);
|
||||
} else if (fallback != null) {
|
||||
biomes.add(fallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ArrayList<>(biomes);
|
||||
}
|
||||
|
||||
private static Object getFor(Class<?> type, Object source) {
|
||||
Object o = fieldFor(type, source);
|
||||
|
||||
if (o != null) {
|
||||
return o;
|
||||
}
|
||||
|
||||
return invokeFor(type, source);
|
||||
}
|
||||
|
||||
private static Object fieldFor(Class<?> returns, Object in) {
|
||||
return fieldForClass(returns, in.getClass(), in);
|
||||
}
|
||||
|
||||
private static Object invokeFor(Class<?> returns, Object in) {
|
||||
for (Method i : in.getClass().getMethods()) {
|
||||
if (i.getReturnType().equals(returns)) {
|
||||
i.setAccessible(true);
|
||||
try {
|
||||
IrisLogging.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()");
|
||||
return i.invoke(in);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T fieldForClass(Class<T> returnType, Class<?> sourceType, Object in) {
|
||||
for (Field i : sourceType.getDeclaredFields()) {
|
||||
if (i.getType().equals(returnType)) {
|
||||
i.setAccessible(true);
|
||||
try {
|
||||
IrisLogging.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName());
|
||||
return (T) i.get(in);
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return getAllBiomes(
|
||||
((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
|
||||
.lookup(Registries.BIOME).orElse(null),
|
||||
((CraftWorld) engine.getWorld().realWorld()).getHandle().registryAccess().lookup(Registries.BIOME).orElse(null),
|
||||
engine,
|
||||
fallbackBiome).stream();
|
||||
}
|
||||
|
||||
private KMap<String, Holder<Biome>> fillCustomBiomes(Registry<Biome> customRegistry, Engine engine, Holder<Biome> fallback) {
|
||||
KMap<String, Holder<Biome>> m = new KMap<>();
|
||||
if (customRegistry == null) {
|
||||
return m;
|
||||
}
|
||||
|
||||
for (IrisBiome i : engine.getAllBiomes()) {
|
||||
if (i.isCustom()) {
|
||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||
Holder<Biome> holder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
|
||||
if (holder == null) {
|
||||
if (fallback != null) {
|
||||
m.put(j.getId(), fallback);
|
||||
}
|
||||
IrisLogging.error("Cannot find biome for IrisBiomeCustom " + j.getId() + " from engine " + engine.getName());
|
||||
continue;
|
||||
}
|
||||
m.put(j.getId(), holder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
private Map<Biome, Holder<Biome>> fillVanillaSpawnBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine) {
|
||||
IdentityHashMap<Biome, Holder<Biome>> spawnBiomes = new IdentityHashMap<>();
|
||||
if (customRegistry == null || registry == null) {
|
||||
return Collections.unmodifiableMap(spawnBiomes);
|
||||
}
|
||||
|
||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, irisBiome.getVanillaDerivative());
|
||||
if (vanillaHolder == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, customBiome.getId());
|
||||
if (customHolder != null) {
|
||||
spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.unmodifiableMap(spawnBiomes);
|
||||
}
|
||||
|
||||
Holder<Biome> getVanillaSpawnBiome(Holder<Biome> biome) {
|
||||
if (biome == null) {
|
||||
return null;
|
||||
}
|
||||
return vanillaSpawnBiomes.get(biome.value());
|
||||
}
|
||||
|
||||
private RegistryAccess registry() {
|
||||
return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends BiomeSource> codec() {
|
||||
throw new UnsupportedOperationException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
return cachedHolder;
|
||||
}
|
||||
|
||||
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
|
||||
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
}
|
||||
|
||||
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
structureBiomeCache.clear();
|
||||
}
|
||||
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
return cachedHolder;
|
||||
}
|
||||
|
||||
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z);
|
||||
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
}
|
||||
|
||||
if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
noiseBiomeCache.clear();
|
||||
}
|
||||
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
|
||||
if (resolution == null) {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, resolution.irisBiome.getVanillaDerivative());
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
|
||||
if (resolution.irisBiome.isCustom()) {
|
||||
return resolveCustomHolder(resolution);
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveVisibleBiomeHolder(int x, int y, int z) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
|
||||
if (resolution == null) {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
if (resolution.irisBiome.isCustom()) {
|
||||
return resolveCustomHolder(resolution);
|
||||
}
|
||||
|
||||
org.bukkit.block.Biome vanillaBiome = resolution.underground
|
||||
? resolution.irisBiome.getGroundBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ)
|
||||
: resolution.irisBiome.getSkyBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome);
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
|
||||
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||
if (customBiome != null) {
|
||||
Holder<Biome> holder = customBiomes.get(customBiome.getId());
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(int x, int y, int z) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (engine.getComplex() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int blockX = x << 2;
|
||||
int blockZ = z << 2;
|
||||
int blockY = y << 2;
|
||||
int worldMinHeight = engine.getWorld().minHeight();
|
||||
int internalY = blockY - worldMinHeight;
|
||||
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
|
||||
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
|
||||
boolean deepUnderground = internalY <= caveSwitchInternalY;
|
||||
boolean belowSurface = internalY <= surfaceInternalY - 8;
|
||||
boolean underground = deepUnderground && belowSurface;
|
||||
IrisBiome irisBiome = underground
|
||||
? engine.getCaveBiome(blockX, internalY, blockZ)
|
||||
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
if (irisBiome == null && underground) {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
}
|
||||
if (irisBiome == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RNG noiseRng = new RNG(seed
|
||||
^ (((long) blockX) * 341873128712L)
|
||||
^ (((long) blockY) * 132897987541L)
|
||||
^ (((long) blockZ) * 42317861L));
|
||||
|
||||
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, noiseRng);
|
||||
}
|
||||
|
||||
private Holder<Biome> getFallbackBiome() {
|
||||
if (fallbackBiome != null) {
|
||||
return fallbackBiome;
|
||||
}
|
||||
|
||||
Holder<Biome> holder = resolveFallbackBiome(biomeRegistry, biomeCustomRegistry);
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Unable to resolve any biome holder fallback for Iris biome source");
|
||||
}
|
||||
|
||||
private static long packNoiseKey(int x, int y, int z) {
|
||||
return (((long) x & 67108863L) << 38)
|
||||
| (((long) z & 67108863L) << 12)
|
||||
| ((long) y & 4095L);
|
||||
}
|
||||
|
||||
private static Holder<Biome> resolveCustomBiomeHolder(Registry<Biome> customRegistry, Engine engine, String customBiomeId) {
|
||||
if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Identifier resourceLocation = Identifier.fromNamespaceAndPath(
|
||||
engine.getDimension().getLoadKey().toLowerCase(java.util.Locale.ROOT),
|
||||
customBiomeId.toLowerCase(java.util.Locale.ROOT)
|
||||
);
|
||||
Biome biome = customRegistry.getValue(resourceLocation);
|
||||
if (biome == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Optional<ResourceKey<Biome>> optionalBiomeKey = customRegistry.getResourceKey(biome);
|
||||
if (optionalBiomeKey.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Optional<Holder.Reference<Biome>> optionalReferenceHolder = customRegistry.get(optionalBiomeKey.get());
|
||||
if (optionalReferenceHolder.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return optionalReferenceHolder.get();
|
||||
}
|
||||
|
||||
private static Holder<Biome> resolveFallbackBiome(Registry<Biome> registry, Registry<Biome> customRegistry) {
|
||||
Holder<Biome> plains = NMSBinding.biomeToBiomeBase(registry, org.bukkit.block.Biome.PLAINS);
|
||||
if (plains != null) {
|
||||
return plains;
|
||||
}
|
||||
|
||||
Holder<Biome> vanilla = firstHolder(registry);
|
||||
if (vanilla != null) {
|
||||
return vanilla;
|
||||
}
|
||||
|
||||
return firstHolder(customRegistry);
|
||||
}
|
||||
|
||||
private static Holder<Biome> firstHolder(Registry<Biome> registry) {
|
||||
if (registry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Biome biome : registry) {
|
||||
Optional<ResourceKey<Biome>> optionalBiomeKey = registry.getResourceKey(biome);
|
||||
if (optionalBiomeKey.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Optional<Holder.Reference<Biome>> optionalHolder = registry.get(optionalBiomeKey.get());
|
||||
if (optionalHolder.isPresent()) {
|
||||
return optionalHolder.get();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class BiomeResolution {
|
||||
private final IrisBiome irisBiome;
|
||||
private final boolean underground;
|
||||
private final int blockX;
|
||||
private final int blockY;
|
||||
private final int blockZ;
|
||||
private final RNG rng;
|
||||
|
||||
private BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY, int blockZ, RNG rng) {
|
||||
this.irisBiome = irisBiome;
|
||||
this.underground = underground;
|
||||
this.blockX = blockX;
|
||||
this.blockY = blockY;
|
||||
this.blockZ = blockZ;
|
||||
this.rng = rng;
|
||||
}
|
||||
}
|
||||
}
|
||||
+478
@@ -0,0 +1,478 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.RandomSupport;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||
import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.util.random.Weighted;
|
||||
import net.minecraft.util.random.WeightedList;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
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.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import java.util.stream.Collectors;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
|
||||
import org.spigotmc.SpigotWorldConfig;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
|
||||
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
|
||||
private final ChunkGenerator delegate;
|
||||
private final Engine engine;
|
||||
private final CustomBiomeSource customBiomeSource;
|
||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||
private volatile Set<String> reachableStructureKeysCache;
|
||||
|
||||
public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) {
|
||||
this(delegate, engine, world, new CustomBiomeSource(seed, engine, world));
|
||||
}
|
||||
|
||||
private IrisChunkGenerator(ChunkGenerator delegate, Engine engine, World world, CustomBiomeSource customBiomeSource) {
|
||||
super(((CraftWorld) world).getHandle(), edit(delegate, customBiomeSource), world.getGenerator());
|
||||
this.delegate = delegate;
|
||||
this.engine = engine;
|
||||
this.customBiomeSource = customBiomeSource;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
int[] at = IrisStructureLocator.locate(engine, id.toString(), pos.getX(), pos.getZ(), Math.max(1, radius));
|
||||
if (at == null) {
|
||||
continue;
|
||||
}
|
||||
long dx = (long) at[0] - pos.getX();
|
||||
long dz = (long) at[2] - pos.getZ();
|
||||
long d = dx * dx + dz * dz;
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
best = new BlockPos(at[0], at[1], at[2]);
|
||||
bestHolder = holder;
|
||||
}
|
||||
}
|
||||
if (best != null) {
|
||||
return Pair.of(best, bestHolder);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
if (!importedControl().active()) {
|
||||
return null;
|
||||
}
|
||||
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.error("Vanilla structure locate failed near " + pos.getX() + ", " + pos.getZ() + ": " + e);
|
||||
IrisLogging.reportError(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private HolderSet<Structure> filterReachableStructures(ServerLevel level, HolderSet<Structure> holders) {
|
||||
Set<String> reachable = reachableStructureKeysCache;
|
||||
if (reachable == null) {
|
||||
reachable = VanillaStructureBiomes.reachableStructureKeys(level, delegate.getBiomeSource());
|
||||
reachableStructureKeysCache = reachable;
|
||||
}
|
||||
if (reachable.isEmpty()) {
|
||||
return holders;
|
||||
}
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
List<Holder<Structure>> kept = new ArrayList<>();
|
||||
for (Holder<Structure> holder : holders) {
|
||||
Object id = registry.getKey(holder.value());
|
||||
if (id != null && reachable.contains(id.toString())) {
|
||||
kept.add(holder);
|
||||
}
|
||||
}
|
||||
if (kept.size() == holders.size()) {
|
||||
return holders;
|
||||
}
|
||||
return HolderSet.direct(kept);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends ChunkGenerator> codec() {
|
||||
return MapCodec.unit(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkGenerator getDelegate() {
|
||||
if (delegate instanceof CustomChunkGenerator chunkGenerator)
|
||||
return chunkGenerator.getDelegate();
|
||||
return delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinY() {
|
||||
return delegate.getMinY();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSeaLevel() {
|
||||
return delegate.getSeaLevel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
|
||||
if (!importedControl().active()) {
|
||||
return;
|
||||
}
|
||||
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createReferences(WorldGenLevel generatoraccessseed, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
delegate.createReferences(generatoraccessseed, structuremanager, ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
ichunkaccess.fillBiomesFromNoise(customBiomeSource::getVisibleNoiseBiome, randomstate.sampler());
|
||||
return CompletableFuture.completedFuture(ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildSurface(WorldGenRegion regionlimitedworldaccess, StructureManager structuremanager, RandomState randomstate, ChunkAccess ichunkaccess) {
|
||||
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyCarvers(WorldGenRegion regionlimitedworldaccess, long seed, RandomState randomstate, BiomeManager biomemanager, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
return delegate.fillFromNoise(blender, randomstate, structuremanager, ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
|
||||
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
|
||||
if (vanillaSpawnBiome == null) {
|
||||
return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition);
|
||||
}
|
||||
|
||||
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns = vanillaSpawnBiome.value().getMobSettings().getMobs(enumcreaturetype);
|
||||
WeightedList<MobSpawnSettings.SpawnerData> resolvedSpawns = delegate.getMobsAt(
|
||||
vanillaSpawnBiome, structuremanager, enumcreaturetype, blockposition);
|
||||
if (resolvedSpawns != vanillaSpawns) {
|
||||
return resolvedSpawns;
|
||||
}
|
||||
|
||||
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns = holder.value().getMobSettings().getMobs(enumcreaturetype);
|
||||
if (explicitSpawns.isEmpty()) {
|
||||
return vanillaSpawns;
|
||||
}
|
||||
if (vanillaSpawns.isEmpty()) {
|
||||
return explicitSpawns;
|
||||
}
|
||||
|
||||
SpawnTableKey key = new SpawnTableKey(holder.value(), enumcreaturetype);
|
||||
return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager) {
|
||||
applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addDebugScreenInfo(List<String> list, RandomState randomstate, BlockPos blockposition) {
|
||||
delegate.addDebugScreenInfo(list, randomstate, blockposition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
|
||||
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
if (importedControl().active()) {
|
||||
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
}
|
||||
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
|
||||
}
|
||||
|
||||
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
|
||||
if (!structureManager.shouldGenerateStructures()) {
|
||||
return;
|
||||
}
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
|
||||
BlockPos origin = sectionPos.origin();
|
||||
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
Map<Integer, List<Structure>> byStep = registry.stream().collect(Collectors.groupingBy(s -> s.step().ordinal()));
|
||||
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
|
||||
BoundingBox area = writableArea(chunk);
|
||||
int steps = GenerationStep.Decoration.values().length;
|
||||
IrisImportedStructureControl control = importedControl();
|
||||
for (int step = 0; step < steps; step++) {
|
||||
int index = 0;
|
||||
for (Structure structure : byStep.getOrDefault(step, List.of())) {
|
||||
Object id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
if (control.shouldGenerate(structureId) && !IrisStructureLocator.suppressesVanilla(engine, structureId)) {
|
||||
random.setFeatureSeed(decoSeed, index, step);
|
||||
int[] offset = control.resolveOffset(structureId, isUndergroundStep(structure.step()));
|
||||
boolean shifted = offset[0] != 0 || offset[1] != 0 || offset[2] != 0;
|
||||
WorldGenLevel target = shifted ? shiftedLevel(world, offset[0], offset[1], offset[2]) : world;
|
||||
BoundingBox placeArea = shifted
|
||||
? new BoundingBox(area.minX() - offset[0], area.minY(), area.minZ() - offset[2], area.maxX() - offset[0], area.maxY(), area.maxZ() - offset[2])
|
||||
: area;
|
||||
try {
|
||||
structureManager.startsForStructure(sectionPos, structure)
|
||||
.forEach(start -> start.placeInChunk(target, structureManager, this, random, placeArea, chunkPos));
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isUndergroundStep(GenerationStep.Decoration step) {
|
||||
return step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES
|
||||
|| step == GenerationStep.Decoration.STRONGHOLDS;
|
||||
}
|
||||
|
||||
private WorldGenLevel shiftedLevel(WorldGenLevel world, int dx, int dy, int dz) {
|
||||
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||
WorldGenLevel.class.getClassLoader(),
|
||||
new Class<?>[]{WorldGenLevel.class},
|
||||
(proxy, method, args) -> {
|
||||
if (args != null) {
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (args[i] instanceof BlockPos bp) {
|
||||
args[i] = new BlockPos(bp.getX() + dx, bp.getY() + dy, bp.getZ() + dz);
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
return method.invoke(world, args);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw e.getCause();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private BoundingBox writableArea(ChunkAccess chunk) {
|
||||
ChunkPos cp = chunk.getPos();
|
||||
int i = cp.getMinBlockX();
|
||||
int j = cp.getMinBlockZ();
|
||||
int minY = getMinY() + 1;
|
||||
int maxY = getMinY() + engine.getHeight() - 1;
|
||||
return new BoundingBox(i, minY, j, i + 15, maxY, j + 15);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
|
||||
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
|
||||
BlockPos blockPos = sectionPos.origin();
|
||||
|
||||
Heightmap surface = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap ocean = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
|
||||
Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
|
||||
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
int wX = x + blockPos.getX();
|
||||
int wZ = z + blockPos.getZ();
|
||||
|
||||
int terrainTop = engine.getHeight(wX, wZ, false) + engine.getMinHeight() + 1;
|
||||
int terrainNoFluid = engine.getHeight(wX, wZ, true) + engine.getMinHeight() + 1;
|
||||
SET_HEIGHT.invoke(ocean, x, z, terrainNoFluid);
|
||||
SET_HEIGHT.invoke(surface, x, z, terrainTop);
|
||||
SET_HEIGHT.invoke(motion, x, z, terrainTop);
|
||||
SET_HEIGHT.invoke(motionNoLeaves, x, z, terrainTop);
|
||||
}
|
||||
}
|
||||
|
||||
Heightmap.primeHeightmaps(chunkAccess, ChunkStatus.FINAL_HEIGHTMAPS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spawnOriginalMobs(WorldGenRegion regionlimitedworldaccess) {
|
||||
delegate.spawnOriginalMobs(regionlimitedworldaccess);
|
||||
}
|
||||
|
||||
private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables(
|
||||
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
|
||||
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
|
||||
List<Weighted<MobSpawnSettings.SpawnerData>> entries = new ArrayList<>(
|
||||
vanillaSpawns.unwrap().size() + explicitSpawns.unwrap().size());
|
||||
Set<EntityType<?>> explicitTypes = new HashSet<>();
|
||||
for (Weighted<MobSpawnSettings.SpawnerData> entry : explicitSpawns.unwrap()) {
|
||||
explicitTypes.add(entry.value().type());
|
||||
}
|
||||
for (Weighted<MobSpawnSettings.SpawnerData> entry : vanillaSpawns.unwrap()) {
|
||||
if (!explicitTypes.contains(entry.value().type())) {
|
||||
entries.add(entry);
|
||||
}
|
||||
}
|
||||
entries.addAll(explicitSpawns.unwrap());
|
||||
return WeightedList.of(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSpawnHeight(LevelHeightAccessor levelheightaccessor) {
|
||||
return delegate.getSpawnHeight(levelheightaccessor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGenDepth() {
|
||||
return delegate.getGenDepth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||
return levelheightaccessor.getMinY() + engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState())) + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||
int block = engine.getHeight(i, j, true);
|
||||
int water = engine.getHeight(i, j, false);
|
||||
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
|
||||
for (int k = 0; k < column.length; k++) {
|
||||
if (k <= block) column[k] = Blocks.STONE.defaultBlockState();
|
||||
else if (k <= water) column[k] = Blocks.WATER.defaultBlockState();
|
||||
else column[k] = Blocks.AIR.defaultBlockState();
|
||||
}
|
||||
return new NoiseColumn(levelheightaccessor.getMinY(), column);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Identifier> getTypeNameForDataFixer() {
|
||||
return delegate.getTypeNameForDataFixer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() {
|
||||
delegate.validate();
|
||||
}
|
||||
|
||||
static {
|
||||
Field biomeSource = null;
|
||||
for (Field field : ChunkGenerator.class.getDeclaredFields()) {
|
||||
if (!field.getType().equals(BiomeSource.class))
|
||||
continue;
|
||||
biomeSource = field;
|
||||
break;
|
||||
}
|
||||
if (biomeSource == null)
|
||||
throw new RuntimeException("Could not find biomeSource field in ChunkGenerator!");
|
||||
|
||||
Method setHeight = null;
|
||||
for (Method method : Heightmap.class.getDeclaredMethods()) {
|
||||
var types = method.getParameterTypes();
|
||||
if (types.length != 3 || !Arrays.equals(types, new Class<?>[]{int.class, int.class, int.class})
|
||||
|| !method.getReturnType().equals(void.class))
|
||||
continue;
|
||||
setHeight = method;
|
||||
break;
|
||||
}
|
||||
if (setHeight == null)
|
||||
throw new RuntimeException("Could not find setHeight method in Heightmap!");
|
||||
|
||||
BIOME_SOURCE = new WrappedField<>(ChunkGenerator.class, biomeSource.getName());
|
||||
SET_HEIGHT = new WrappedReturningMethod<>(Heightmap.class, setHeight.getName(), setHeight.getParameterTypes());
|
||||
}
|
||||
|
||||
private static ChunkGenerator edit(ChunkGenerator generator, BiomeSource source) {
|
||||
try {
|
||||
BIOME_SOURCE.set(generator, source);
|
||||
if (generator instanceof CustomChunkGenerator custom)
|
||||
BIOME_SOURCE.set(custom.getDelegate(), source);
|
||||
|
||||
return generator;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private record SpawnTableKey(Biome biome, MobCategory category) {
|
||||
}
|
||||
}
|
||||
+1356
File diff suppressed because it is too large
Load Diff
+77
@@ -0,0 +1,77 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
final class VanillaStructureBiomes {
|
||||
private VanillaStructureBiomes() {
|
||||
}
|
||||
|
||||
static Set<String> possibleBiomeKeys(BiomeSource source) {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
if (source == null) {
|
||||
return keys;
|
||||
}
|
||||
for (Holder<Biome> holder : source.possibleBiomes()) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent()) {
|
||||
keys.add(key.get().identifier().toString());
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
static Set<String> structureBiomeKeys(RegistryAccess access, String structureKey) {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
if (access == null || structureKey == null || structureKey.isEmpty()) {
|
||||
return keys;
|
||||
}
|
||||
Registry<Structure> registry = access.lookupOrThrow(Registries.STRUCTURE);
|
||||
Structure structure = registry.getValue(Identifier.parse(structureKey));
|
||||
if (structure == null) {
|
||||
return keys;
|
||||
}
|
||||
for (Holder<Biome> holder : structure.biomes()) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent()) {
|
||||
keys.add(key.get().identifier().toString());
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
static Set<String> reachableStructureKeys(ServerLevel level, BiomeSource source) {
|
||||
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()) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent() && possible.contains(key.get().identifier().toString())) {
|
||||
reachable.add(entry.getKey().identifier().toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return reachable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
String apiVersion = providers.gradleProperty('minecraftVersion').get()
|
||||
def mainClass = 'art.arcane.iris.Iris'
|
||||
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
|
||||
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:cdf7e8bb880199734dacd386a6616bda799fcde1')
|
||||
.get()
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(':core'))
|
||||
compileOnly(volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
compileOnly(libs.paper.api)
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation(libs.paper.api)
|
||||
testImplementation(libs.bstats)
|
||||
testImplementation(libs.sentry)
|
||||
testImplementation(project(':core'))
|
||||
testImplementation(volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
compileOnly(libs.placeholderApi)
|
||||
compileOnly(libs.multiverseCore)
|
||||
}
|
||||
|
||||
tasks.named('processResources').configure {
|
||||
def pluginProperties = [
|
||||
name : rootProject.name,
|
||||
version : rootProject.version,
|
||||
apiVersion: apiVersion,
|
||||
main : mainClass,
|
||||
bootstrapper: bootstrapperClass,
|
||||
]
|
||||
inputs.properties(pluginProperties)
|
||||
filesMatching(['**/paper-plugin.yml', '**/plugin.yml']) {
|
||||
expand(pluginProperties)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('jar', Jar).configure {
|
||||
archiveBaseName.set('iris-bukkit-plugin')
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner;
|
||||
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
|
||||
import io.papermc.paper.datapack.Datapack;
|
||||
import io.papermc.paper.datapack.DatapackRegistrar;
|
||||
import io.papermc.paper.datapack.DiscoveredDatapack;
|
||||
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
|
||||
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
|
||||
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public final class IrisBootstrap implements PluginBootstrap {
|
||||
static final String PACK_ID = "generated";
|
||||
|
||||
@Override
|
||||
public void bootstrap(BootstrapContext context) {
|
||||
ProvisionResult provisioned = provision(context);
|
||||
Path datapackRoot = provisioned.datapackRoot();
|
||||
context.getLogger().info("Iris startup datapack is {} at {}", provisioned.status(), datapackRoot);
|
||||
context.getLifecycleManager().registerEventHandler(LifecycleEvents.DATAPACK_DISCOVERY, event -> {
|
||||
try {
|
||||
discoverPack(event.registrar(), datapackRoot);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Unable to discover the Iris startup datapack at " + datapackRoot, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static DiscoveredDatapack discoverPack(DatapackRegistrar registrar, Path datapackRoot) throws IOException {
|
||||
DiscoveredDatapack datapack = registrar.discoverPack(
|
||||
datapackRoot,
|
||||
PACK_ID,
|
||||
configurer -> configurer
|
||||
.autoEnableOnServerStart(true)
|
||||
.position(true, Datapack.Position.TOP)
|
||||
);
|
||||
if (datapack == null) {
|
||||
throw new IllegalStateException("Paper did not accept the Iris startup datapack at " + datapackRoot);
|
||||
}
|
||||
return datapack;
|
||||
}
|
||||
|
||||
private static ProvisionResult provision(BootstrapContext context) {
|
||||
try {
|
||||
return DefaultPackBootstrapProvisioner.provision(
|
||||
context.getDataDirectory(),
|
||||
message -> context.getLogger().info(message)
|
||||
);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Unable to provision the Iris startup datapack", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.commands;
|
||||
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports (Modrinth)")
|
||||
public class CommandDatapack implements DirectorExecutor {
|
||||
@Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", aliases = {"pull"})
|
||||
public void ingest(
|
||||
@Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", defaultValue = "false")
|
||||
boolean restart
|
||||
) {
|
||||
VolmitSender sender = sender();
|
||||
sender.sendMessage(C.GRAY + "Starting datapack ingest...");
|
||||
J.a(() -> DatapackIngestService.ingestAll(sender, restart));
|
||||
}
|
||||
|
||||
@Director(description = "List configured datapack imports and their installed versions", aliases = {"ls"})
|
||||
public void list() {
|
||||
VolmitSender sender = sender();
|
||||
KList<String> configured = DatapackIngestService.collectConfiguredImports();
|
||||
List<DatapackIngestService.Entry> installed = DatapackIngestService.installed();
|
||||
|
||||
sender.sendMessage(C.GREEN + "Configured datapack imports: " + C.WHITE + configured.size());
|
||||
for (String url : configured) {
|
||||
sender.sendMessage(C.GRAY + " - " + C.WHITE + url);
|
||||
}
|
||||
|
||||
sender.sendMessage(C.GREEN + "Installed datapacks: " + C.WHITE + installed.size());
|
||||
for (DatapackIngestService.Entry entry : installed) {
|
||||
sender.sendMessage(C.GRAY + " - " + C.WHITE + entry.id + C.GRAY + " " + (entry.versionNumber == null ? "?" : entry.versionNumber));
|
||||
}
|
||||
|
||||
if (configured.isEmpty()) {
|
||||
sender.sendMessage(C.YELLOW + "Add Modrinth URLs to a dimension's 'datapackImports' list, then run /iris datapack ingest.");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Remove an installed datapack by id (also delete its URL from datapackImports to keep it gone)", aliases = {"rm"})
|
||||
public void remove(
|
||||
@Param(description = "The datapack id (folder name) shown by /iris datapack list")
|
||||
String id
|
||||
) {
|
||||
DatapackIngestService.remove(sender(), id);
|
||||
}
|
||||
}
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.commands;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.core.runtime.ChunkClearer;
|
||||
import art.arcane.iris.core.runtime.GoldenHashScanner;
|
||||
import art.arcane.iris.core.runtime.InPlaceChunkRegenerator;
|
||||
import art.arcane.iris.core.service.IrisEngineSVC;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.core.tools.IrisPackBenchmarking;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.IrisEngineMantle;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.iris.util.project.matter.IrisMatterContext;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.io.CountingDataInputStream;
|
||||
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.iris.util.nbt.common.mca.MCAFile;
|
||||
import art.arcane.iris.util.nbt.common.mca.MCAUtil;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
@Director(name = "Developer", origin = DirectorOrigin.BOTH, description = "Iris World Manager", aliases = {"dev"})
|
||||
public class CommandDeveloper implements DirectorExecutor {
|
||||
@Director(description = "Get Loaded TectonicPlates Count", origin = DirectorOrigin.BOTH, sync = true)
|
||||
public void EngineStatus() {
|
||||
Iris.service(IrisEngineSVC.class)
|
||||
.engineStatus(sender());
|
||||
}
|
||||
|
||||
@Director(description = "Send a test exception to sentry")
|
||||
public void Sentry() {
|
||||
Engine engine = engine();
|
||||
Exception testException = new Exception("This is a test");
|
||||
if (engine == null) {
|
||||
Iris.reportError(testException);
|
||||
return;
|
||||
}
|
||||
try (IrisContext.Scope scope = IrisContext.open(engine, engine.getGenerationSessionId(), null)) {
|
||||
Iris.reportError(testException);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Hash generated block output of a fixed area for determinism/identity testing", origin = DirectorOrigin.BOTH)
|
||||
public void genhash(
|
||||
@Param(description = "The world to hash", contextual = true)
|
||||
World world,
|
||||
@Param(description = "Radius in chunks around the center", defaultValue = "4")
|
||||
int radius,
|
||||
@Param(description = "Center chunk X", defaultValue = "0")
|
||||
int centerX,
|
||||
@Param(description = "Center chunk Z", defaultValue = "0")
|
||||
int centerZ) {
|
||||
if (world == null) {
|
||||
sender().sendMessage(C.RED + "World is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
VolmitSender sender = sender();
|
||||
sender.sendMessage(C.GREEN + "genhash started: " + ((radius * 2 + 1) * (radius * 2 + 1)) + " chunks...");
|
||||
J.a(() -> runGenhash(sender, world, radius, centerX, centerZ));
|
||||
}
|
||||
|
||||
private void runGenhash(VolmitSender sender, World world, int radius, int centerX, int centerZ) {
|
||||
long startMs = M.ms();
|
||||
int minY = world.getMinHeight();
|
||||
int maxY = world.getMaxHeight();
|
||||
long globalHash = 0L;
|
||||
long solidBlocks = 0L;
|
||||
Map<String, Long> histogram = new TreeMap<>();
|
||||
JsonObject chunkHashes = new JsonObject();
|
||||
|
||||
for (int rx = centerX - radius; rx <= centerX + radius; rx++) {
|
||||
for (int rz = centerZ - radius; rz <= centerZ + radius; rz++) {
|
||||
org.bukkit.ChunkSnapshot snapshot;
|
||||
try {
|
||||
org.bukkit.Chunk loaded = art.arcane.iris.platform.bukkit.BukkitPlatform.chunkAtAsync(world, rx, rz, true).get();
|
||||
snapshot = loaded.getChunkSnapshot(false, false, false);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender.sendMessage(C.RED + "genhash failed at chunk " + rx + "," + rz + ": " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
long chunkHash = 0L;
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
int worldX = (rx << 4) + x;
|
||||
int worldZ = (rz << 4) + z;
|
||||
for (int y = minY; y < maxY; y++) {
|
||||
org.bukkit.Material material = snapshot.getBlockType(x, y, z);
|
||||
long positionSeed = ((long) worldX * 0x9E3779B97F4A7C15L)
|
||||
^ ((long) y * 0xC2B2AE3D27D4EB4FL)
|
||||
^ ((long) worldZ * 0x165667B19E3779F9L);
|
||||
long blockHash = genHashMix(positionSeed ^ ((long) (material.ordinal() + 1) * 0xD6E8FEB86659FD93L));
|
||||
chunkHash ^= blockHash;
|
||||
if (material != org.bukkit.Material.AIR
|
||||
&& material != org.bukkit.Material.CAVE_AIR
|
||||
&& material != org.bukkit.Material.VOID_AIR) {
|
||||
histogram.merge(material.name(), 1L, Long::sum);
|
||||
solidBlocks++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
globalHash ^= chunkHash;
|
||||
chunkHashes.addProperty(rx + "," + rz, Long.toHexString(chunkHash));
|
||||
}
|
||||
}
|
||||
|
||||
int side = radius * 2 + 1;
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("global", Long.toHexString(globalHash));
|
||||
result.addProperty("chunks", side * side);
|
||||
result.addProperty("solidBlocks", solidBlocks);
|
||||
result.addProperty("minY", minY);
|
||||
result.addProperty("maxY", maxY);
|
||||
JsonObject hist = new JsonObject();
|
||||
for (Map.Entry<String, Long> entry : histogram.entrySet()) {
|
||||
hist.addProperty(entry.getKey(), entry.getValue());
|
||||
}
|
||||
result.add("histogram", hist);
|
||||
result.add("chunkHashes", chunkHashes);
|
||||
|
||||
File out = new File(Iris.instance.getDataFolder(), "genhash.json");
|
||||
try {
|
||||
Files.writeString(out.toPath(), result.toString());
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
sender.sendMessage(C.GREEN + "genhash global=" + C.GOLD + Long.toHexString(globalHash)
|
||||
+ C.GREEN + " chunks=" + (side * side) + " solid=" + solidBlocks
|
||||
+ " in " + Form.duration((long) (M.ms() - startMs), 1));
|
||||
Iris.info("genhash world=" + world.getName() + " global=" + Long.toHexString(globalHash)
|
||||
+ " chunks=" + (side * side) + " solidBlocks=" + solidBlocks + " -> " + out.getAbsolutePath());
|
||||
}
|
||||
|
||||
private static long genHashMix(long z) {
|
||||
z = (z ^ (z >>> 30)) * 0xBF58476D1CE4E5B9L;
|
||||
z = (z ^ (z >>> 27)) * 0x94D049BB133111EBL;
|
||||
return z ^ (z >>> 31);
|
||||
}
|
||||
|
||||
@Director(description = "Update the pack of a world (UNSAFE!)", name = "update-world", aliases = "^world")
|
||||
public void updateWorld(
|
||||
@Param(description = "The world to update", contextual = true)
|
||||
World world,
|
||||
@Param(description = "The pack to install into the world", contextual = true, aliases = "dimension")
|
||||
IrisDimension pack,
|
||||
@Param(description = "Make sure to make a backup & read the warnings first!", defaultValue = "false", aliases = "c")
|
||||
boolean confirm,
|
||||
@Param(description = "Should Iris download the pack again for you", defaultValue = "false", name = "fresh-download", aliases = {"fresh", "new"})
|
||||
boolean freshDownload
|
||||
) {
|
||||
if (!confirm) {
|
||||
sender().sendMessage(new String[]{
|
||||
C.RED + "You should always make a backup before using this",
|
||||
C.YELLOW + "Issues caused by this can be, but are not limited to:",
|
||||
C.YELLOW + " - Broken chunks (cut-offs) between old and new chunks (before & after the update)",
|
||||
C.YELLOW + " - Regenerated chunks that do not fit in with the old chunks",
|
||||
C.YELLOW + " - Structures not spawning again when regenerating",
|
||||
C.YELLOW + " - Caves not lining up",
|
||||
C.YELLOW + " - Terrain layers not lining up",
|
||||
C.RED + "Now that you are aware of the risks, and have made a back-up:",
|
||||
C.RED + "/iris developer update-world " + world.getName() + " " + pack.getLoadKey() + " confirm=true"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
File folder = world.getWorldFolder();
|
||||
folder.mkdirs();
|
||||
|
||||
if (freshDownload) {
|
||||
Iris.service(StudioSVC.class).downloadSearch(sender(), pack.getLoadKey(), true);
|
||||
}
|
||||
|
||||
Iris.service(StudioSVC.class).installIntoWorld(sender(), pack, folder);
|
||||
}
|
||||
|
||||
@Director(description = "Test")
|
||||
public void mantle(
|
||||
@Param(name = "plate", description = "Dump the whole tectonic plate instead of a single section", defaultValue = "false")
|
||||
boolean plate,
|
||||
@Param(name = "name", description = "The dump file id under plugins/Iris/dump (pv.<id>.*)", defaultValue = "21474836474")
|
||||
String name
|
||||
) throws Throwable {
|
||||
Engine activeEngine = engine();
|
||||
if (activeEngine == null) {
|
||||
sender().sendMessage(C.RED + "Target an Iris world before reading a mantle dump.");
|
||||
return;
|
||||
}
|
||||
File base = Iris.instance.getDataFile("dump", "pv." + name + ".ttp.lz4b.bin");
|
||||
File section = Iris.instance.getDataFile("dump", "pv." + name + ".section.bin");
|
||||
|
||||
try (IrisMatterContext.Scope scope = IrisMatterContext.open(activeEngine.getData())) {
|
||||
if (plate) {
|
||||
try (CountingDataInputStream in = CountingDataInputStream.wrap(new BufferedInputStream(new FileInputStream(base)))) {
|
||||
TectonicPlate.read(1088, in, true, IrisEngineMantle.createRuntimeDataAdapter(activeEngine.getData()), IrisEngineMantle.createRuntimeHooks());
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
Matter.read(section);
|
||||
}
|
||||
}
|
||||
if (!TectonicPlate.hasError()) {
|
||||
Iris.info("Read " + (plate ? base : section).length() + " bytes from " + (plate ? base : section).getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Test")
|
||||
public void packBenchmark(
|
||||
@Param(description = "The pack to bench", aliases = {"pack"}, defaultValue = "overworld")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "Radius in regions", defaultValue = "2048")
|
||||
int radius,
|
||||
@Param(description = "Open GUI while benchmarking", defaultValue = "false")
|
||||
boolean gui
|
||||
) {
|
||||
new IrisPackBenchmarking(dimension, radius, gui);
|
||||
}
|
||||
|
||||
@Director(description = "Upgrade to another Minecraft version")
|
||||
public void upgrade(
|
||||
@Param(description = "The version to upgrade to", defaultValue = "latest") DataVersion version) {
|
||||
sender().sendMessage(C.GREEN + "Upgrading to " + version.getVersion() + "...");
|
||||
ServerConfigurator.installDataPacks(version.get(), false);
|
||||
sender().sendMessage(C.GREEN + "Done upgrading! You can now update your server version to " + version.getVersion());
|
||||
}
|
||||
|
||||
@Director(description = "test")
|
||||
public void mca (
|
||||
@Param(description = "The world folder to scan for .mca region files") String world) {
|
||||
try {
|
||||
File[] McaFiles = new File(world, "region").listFiles((dir, name) -> name.endsWith(".mca"));
|
||||
for (File mca : McaFiles) {
|
||||
MCAFile MCARegion = MCAUtil.read(mca);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Director(description = "Delete nearby chunk blocks for regen testing", name = "delete-chunk", aliases = {"dc"}, origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void deleteChunk(
|
||||
@Param(description = "Radius in chunks around your current chunk", defaultValue = "0")
|
||||
int radius
|
||||
) {
|
||||
if (radius < 0) {
|
||||
sender().sendMessage(C.RED + "Radius must be 0 or greater.");
|
||||
return;
|
||||
}
|
||||
|
||||
World world = player().getWorld();
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(C.RED + "This is not an Iris world.");
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access == null || access.getEngine() == null) {
|
||||
sender().sendMessage(C.RED + "The engine access for this world is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
int centerX = player().getLocation().getBlockX() >> 4;
|
||||
int centerZ = player().getLocation().getBlockZ() >> 4;
|
||||
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
|
||||
|
||||
sender().sendMessage(C.GREEN + "Delete started: " + C.GOLD + chunks + C.GREEN
|
||||
+ " chunk(s) around " + C.GOLD + centerX + "," + centerZ + C.GREEN
|
||||
+ ". Clearing blocks to air.");
|
||||
|
||||
new ChunkClearer(world, access.getEngine(), sender(), centerX, centerZ, radius).start();
|
||||
}
|
||||
|
||||
@Director(description = "Test", aliases = {"ip"})
|
||||
public void network() {
|
||||
try {
|
||||
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
|
||||
for (NetworkInterface ni : Collections.list(networkInterfaces)) {
|
||||
Iris.info("Display Name: %s", ni.getDisplayName());
|
||||
Enumeration<InetAddress> inetAddresses = ni.getInetAddresses();
|
||||
for (InetAddress ia : Collections.list(inetAddresses)) {
|
||||
Iris.info("IP: %s", ia.getHostAddress());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Regen ---
|
||||
|
||||
@Director(name = "regen", aliases = {"rg"}, description = "Delete and regenerate nearby chunks in place using Iris generation", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void regen(
|
||||
@Param(name = "radius", description = "The radius of nearby chunks", defaultValue = "5")
|
||||
int radius
|
||||
) {
|
||||
if (radius < 0) {
|
||||
sender().sendMessage(C.RED + "Radius must be 0 or greater.");
|
||||
return;
|
||||
}
|
||||
|
||||
World world = player().getWorld();
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(C.RED + "You must be in an Iris world to use regen.");
|
||||
return;
|
||||
}
|
||||
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
if (engine == null) {
|
||||
sender().sendMessage(C.RED + "The engine access for this world is null. Generate nearby chunks first.");
|
||||
return;
|
||||
}
|
||||
|
||||
int centerX = player().getLocation().getBlockX() >> 4;
|
||||
int centerZ = player().getLocation().getBlockZ() >> 4;
|
||||
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
|
||||
|
||||
sender().sendMessage(C.GREEN + "Regen started: " + C.GOLD + chunks + C.GREEN
|
||||
+ " chunk(s) around " + C.GOLD + centerX + "," + centerZ + C.GREEN
|
||||
+ ". Deleting and regenerating in place.");
|
||||
Iris.info("Regen run start: world=" + world.getName()
|
||||
+ " center=" + centerX + "," + centerZ
|
||||
+ " radius=" + radius
|
||||
+ " chunks=" + chunks);
|
||||
|
||||
new InPlaceChunkRegenerator(world, engine, sender(), centerX, centerZ, radius).start();
|
||||
}
|
||||
|
||||
@Director(name = "goldenhash", aliases = {"gold"}, description = "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Resets mantle in the scanned area - use on disposable test worlds.", origin = DirectorOrigin.BOTH)
|
||||
public void goldenhash(
|
||||
@Param(description = "The world to scan", contextual = true)
|
||||
World world,
|
||||
@Param(name = "radius", description = "Radius in chunks around the center", defaultValue = "8")
|
||||
int radius,
|
||||
@Param(name = "center-x", description = "Center chunk X", defaultValue = "0")
|
||||
int centerX,
|
||||
@Param(name = "center-z", description = "Center chunk Z", defaultValue = "0")
|
||||
int centerZ,
|
||||
@Param(name = "reset-mantle", description = "Delete mantle data in the scan area first for full regeneration from scratch", defaultValue = "true")
|
||||
boolean resetMantle,
|
||||
@Param(name = "threads", description = "Concurrent chunk generations; 1 = strictly serial for order-dependence testing", defaultValue = "8")
|
||||
int threads,
|
||||
@Param(name = "deep", description = "Also dump full per-chunk non-air blockstates for offline diffing", defaultValue = "false")
|
||||
boolean deep
|
||||
) {
|
||||
if (radius < 0) {
|
||||
sender().sendMessage(C.RED + "Radius must be 0 or greater.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (world == null || !IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(C.RED + "Target must be an Iris world.");
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access == null || access.getEngine() == null) {
|
||||
sender().sendMessage(C.RED + "The engine access for this world is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
|
||||
sender().sendMessage(C.GREEN + "GoldenHash started: " + C.GOLD + chunks + C.GREEN
|
||||
+ " chunk(s) around " + C.GOLD + centerX + "," + centerZ + C.GREEN
|
||||
+ " in buffers (world untouched).");
|
||||
Iris.info("goldenhash start: world=" + world.getName()
|
||||
+ " center=" + centerX + "," + centerZ
|
||||
+ " radius=" + radius
|
||||
+ " chunks=" + chunks);
|
||||
|
||||
new GoldenHashScanner(world, access.getEngine(), sender(), centerX, centerZ, radius, resetMantle, threads, deep).start();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.commands;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
|
||||
import java.awt.Desktop;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
|
||||
|
||||
@Director(name = "edit", origin = DirectorOrigin.PLAYER, description = "Edit something")
|
||||
public class CommandEdit implements DirectorExecutor {
|
||||
private boolean noStudio() {
|
||||
if (!sender().isPlayer()) {
|
||||
sender().sendMessage(C.RED + "Players only!");
|
||||
return true;
|
||||
}
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
sender().sendMessage(C.RED + "No studio world is open!");
|
||||
return true;
|
||||
}
|
||||
if (!engine().isStudio()) {
|
||||
sender().sendMessage(C.RED + "You must be in a studio world!");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (GraphicsEnvironment.isHeadless()) {
|
||||
sender().sendMessage(C.RED + "Cannot open files in headless environments!");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Desktop.isDesktopSupported()) {
|
||||
sender().sendMessage(C.RED + "Desktop is not supported by this environment!");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Director(description = "Edit the biome you specified", aliases = {"b"}, origin = DirectorOrigin.PLAYER)
|
||||
public void biome(@Param(contextual = false, description = "The biome to edit") IrisBiome biome) {
|
||||
if (noStudio()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (biome == null || biome.getLoadFile() == null) {
|
||||
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
|
||||
return;
|
||||
}
|
||||
Desktop.getDesktop().open(biome.getLoadFile());
|
||||
sender().sendMessage(C.GREEN + "Opening " + biome.getTypeName() + " " + biome.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Edit the region you specified", aliases = {"r"}, origin = DirectorOrigin.PLAYER)
|
||||
public void region(@Param(contextual = false, description = "The region to edit") IrisRegion region) {
|
||||
if (noStudio()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (region == null || region.getLoadFile() == null) {
|
||||
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
|
||||
return;
|
||||
}
|
||||
Desktop.getDesktop().open(region.getLoadFile());
|
||||
sender().sendMessage(C.GREEN + "Opening " + region.getTypeName() + " " + region.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Edit the dimension you specified", aliases = {"d"}, origin = DirectorOrigin.PLAYER)
|
||||
public void dimension(@Param(contextual = false, description = "The dimension to edit") IrisDimension dimension) {
|
||||
if (noStudio()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (dimension == null || dimension.getLoadFile() == null) {
|
||||
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
|
||||
return;
|
||||
}
|
||||
Desktop.getDesktop().open(dimension.getLoadFile());
|
||||
sender().sendMessage(C.GREEN + "Opening " + dimension.getTypeName() + " " + dimension.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.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.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
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.IrisRegion;
|
||||
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;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import io.papermc.paper.registry.RegistryAccess;
|
||||
import io.papermc.paper.registry.RegistryKey;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.generator.structure.Structure;
|
||||
import org.bukkit.util.StructureSearchResult;
|
||||
|
||||
@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", aliases = "goto")
|
||||
public class CommandFind implements DirectorExecutor {
|
||||
@Director(description = "Find a biome")
|
||||
public void biome(
|
||||
@Param(description = "The biome to look for")
|
||||
IrisBiome biome,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
|
||||
if (e == null) {
|
||||
sender().sendMessage(C.GOLD + "Not in an Iris World!");
|
||||
return;
|
||||
}
|
||||
|
||||
EngineBukkitOps.gotoBiome(e, biome, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find a region")
|
||||
public void region(
|
||||
@Param(description = "The region to look for")
|
||||
IrisRegion region,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
|
||||
if (e == null) {
|
||||
sender().sendMessage(C.GOLD + "Not in an Iris World!");
|
||||
return;
|
||||
}
|
||||
|
||||
EngineBukkitOps.gotoRegion(e, region, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find a point of interest.")
|
||||
public void poi(
|
||||
@Param(description = "The type of PoI to look for.")
|
||||
String type,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
if (e == null) {
|
||||
sender().sendMessage(C.GOLD + "Not in an Iris World!");
|
||||
return;
|
||||
}
|
||||
|
||||
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)")
|
||||
public void structure(
|
||||
@Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", customHandler = StructureHandler.class)
|
||||
String structure
|
||||
) {
|
||||
VolmitSender commandSender = sender();
|
||||
if (commandSender == null) {
|
||||
Iris.reportError("Structure lookup started without a command sender context.", new IllegalStateException("Missing command sender context"));
|
||||
return;
|
||||
}
|
||||
|
||||
Engine e = engine();
|
||||
|
||||
if (e == null) {
|
||||
commandSender.sendMessage(C.GOLD + "Not in an Iris World!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IrisStructureLocator.isPlaced(e, structure)) {
|
||||
EngineBukkitOps.gotoStructure(e, structure, player(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
Player target = player();
|
||||
if (target == null) {
|
||||
commandSender.sendMessage(C.GOLD + "Run this in-game to teleport to a structure.");
|
||||
return;
|
||||
}
|
||||
|
||||
commandSender.sendMessage(C.GRAY + "Locating " + structure + "...");
|
||||
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) {
|
||||
commandSender.sendMessage(C.RED + "Unknown structure: " + structure);
|
||||
return;
|
||||
}
|
||||
if (!StructureReachability.isReachable(e, structure)) {
|
||||
KList<String> miss = StructureReachability.missingBiomeKeys(e, structure);
|
||||
commandSender.sendMessage(C.YELLOW + structure + " cannot generate in this world (its required biomes are not produced by this pack"
|
||||
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
|
||||
return;
|
||||
}
|
||||
StructureSearchResult result = target.getWorld().locateNearestStructure(target.getLocation(), match, 100, true);
|
||||
if (result == null || result.getLocation() == null) {
|
||||
commandSender.sendMessage(C.YELLOW + "No " + structure + " found within range of you.");
|
||||
return;
|
||||
}
|
||||
Location at = result.getLocation();
|
||||
int y = target.getWorld().getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2;
|
||||
Location dest = new Location(target.getWorld(), at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5);
|
||||
BukkitPlatform.teleportAsync(target, dest);
|
||||
commandSender.sendMessage(C.GREEN + "Teleported to " + structure + " @ " + at.getBlockX() + ", " + at.getBlockZ());
|
||||
} catch (Throwable t) {
|
||||
commandSender.sendMessage(C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
|
||||
Iris.reportError("Could not locate structure '" + structure + "'.", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Find an object")
|
||||
public void object(
|
||||
@Param(description = "The object to look for", customHandler = ObjectHandler.class)
|
||||
String object,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
|
||||
if (e == null) {
|
||||
sender().sendMessage(C.GOLD + "Not in an Iris World!");
|
||||
return;
|
||||
}
|
||||
|
||||
Player studioPlayer = player();
|
||||
if (studioPlayer != null) {
|
||||
try {
|
||||
if (ObjectStudioSaveService.get().teleportTo(studioPlayer, object)) {
|
||||
sender().sendMessage(C.GREEN + "Object Studio: teleporting to " + object);
|
||||
return;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
Iris.reportError(t);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.hasObjectPlacement(object)) {
|
||||
EngineBukkitOps.gotoObject(e, object, player(), teleport);
|
||||
return;
|
||||
}
|
||||
|
||||
sender().sendMessage(C.RED + object + " is not configured in any region/biome object placements.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,642 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.commands;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.IrisWorldStorage;
|
||||
import art.arcane.iris.core.IrisWorlds;
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.PackDownloader;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import art.arcane.volmlib.util.director.DirectorParameterHandler;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.NullablePlayerHandler;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.iris.util.common.misc.ServerProperties;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static art.arcane.iris.core.service.EditSVC.deletingWorld;
|
||||
import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
|
||||
import static org.bukkit.Bukkit.getServer;
|
||||
|
||||
@Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command")
|
||||
public class CommandIris implements DirectorExecutor {
|
||||
private CommandStudio studio;
|
||||
private CommandPregen pregen;
|
||||
private CommandObject object;
|
||||
private CommandStructure structure;
|
||||
private CommandWhat what;
|
||||
private CommandEdit edit;
|
||||
private CommandDeveloper developer;
|
||||
private CommandPack pack;
|
||||
private CommandFind find;
|
||||
private CommandDatapack datapack;
|
||||
public static boolean worldCreation = false;
|
||||
private static final AtomicReference<Thread> mainWorld = new AtomicReference<>();
|
||||
String WorldEngine;
|
||||
String worldNameToCheck = "YourWorldName";
|
||||
VolmitSender sender = Iris.getSender();
|
||||
|
||||
@Director(description = "Create a new world", aliases = {"c"})
|
||||
public void create(
|
||||
@Param(aliases = "world-name", description = "The name of the world to create")
|
||||
String name,
|
||||
@Param(
|
||||
aliases = {"dimension", "pack"},
|
||||
description = "The dimension/pack to create the world with",
|
||||
defaultValue = "default",
|
||||
customHandler = PackDimensionTypeHandler.class
|
||||
)
|
||||
String type,
|
||||
@Param(description = "The seed to generate the world with", defaultValue = "1337")
|
||||
long seed,
|
||||
@Param(aliases = "main-world", description = "Whether or not to automatically use this world as the main world", defaultValue = "false")
|
||||
boolean main
|
||||
) {
|
||||
if (name.equalsIgnoreCase("iris")) {
|
||||
sender().sendMessage(C.RED + "You cannot use the world name \"iris\" for creating worlds as Iris uses this directory for studio worlds.");
|
||||
sender().sendMessage(C.RED + "May we suggest the name \"IrisWorld\" instead?");
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.equalsIgnoreCase("benchmark")) {
|
||||
sender().sendMessage(C.RED + "You cannot use the world name \"benchmark\" for creating worlds as Iris uses this directory for Benchmarking Packs.");
|
||||
sender().sendMessage(C.RED + "May we suggest the name \"IrisWorld\" instead?");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IrisWorldStorage.dimensionRoot(name).exists()) {
|
||||
sender().sendMessage(C.RED + "That folder already exists!");
|
||||
return;
|
||||
}
|
||||
|
||||
String resolvedType = type.equalsIgnoreCase("default")
|
||||
? IrisSettings.get().getGenerator().getDefaultWorldType()
|
||||
: type;
|
||||
|
||||
IrisDimension dimension = IrisToolbelt.getDimension(resolvedType);
|
||||
if (dimension == null) {
|
||||
sender().sendMessage(C.RED + "Could not find or download dimension \"" + resolvedType + "\".");
|
||||
sender().sendMessage(C.YELLOW + "Try one of: overworld, vanilla, flat, theend");
|
||||
sender().sendMessage(C.YELLOW + "Or download manually: /iris download " + resolvedType);
|
||||
return;
|
||||
}
|
||||
|
||||
if (J.isFolia()) {
|
||||
if (stageFoliaWorldCreation(name, dimension, seed, main)) {
|
||||
sender().sendMessage(C.GREEN + "World staging completed. Restart the server to generate/load \"" + name + "\".");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
worldCreation = true;
|
||||
IrisToolbelt.createWorld()
|
||||
.dimension(resolvedType)
|
||||
.name(name)
|
||||
.seed(seed)
|
||||
.sender(sender())
|
||||
.studio(false)
|
||||
.create();
|
||||
if (main) {
|
||||
Runtime.getRuntime().addShutdownHook(mainWorld.updateAndGet(old -> {
|
||||
if (old != null) Runtime.getRuntime().removeShutdownHook(old);
|
||||
return new Thread(() -> updateMainWorld(name));
|
||||
}));
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
sender().sendMessage(C.RED + "Exception raised during creation. See the console for more details.");
|
||||
Iris.reportError("Exception raised during world creation for \"" + name + "\".", e);
|
||||
worldCreation = false;
|
||||
return;
|
||||
}
|
||||
|
||||
worldCreation = false;
|
||||
sender().sendMessage(C.GREEN + "Successfully created your world!");
|
||||
if (main) sender().sendMessage(C.GREEN + "Your world will automatically be set as the main world when the server restarts.");
|
||||
}
|
||||
|
||||
private boolean updateMainWorld(String newName) {
|
||||
try {
|
||||
File oldLevelRoot = IrisWorldStorage.levelRoot();
|
||||
File worldContainer = oldLevelRoot.getParentFile();
|
||||
Properties data = ServerProperties.DATA;
|
||||
try (FileInputStream in = new FileInputStream(ServerProperties.SERVER_PROPERTIES)) {
|
||||
data.load(in);
|
||||
}
|
||||
|
||||
File sourceDimensionRoot = IrisWorldStorage.dimensionRoot(IrisWorldStorage.keyFromLegacyName(newName));
|
||||
if (!sourceDimensionRoot.isDirectory()) {
|
||||
throw new IllegalStateException("Source dimension folder does not exist: " + sourceDimensionRoot.getAbsolutePath());
|
||||
}
|
||||
|
||||
File newLevelRoot = new File(worldContainer, newName);
|
||||
if (!newLevelRoot.exists() && !newLevelRoot.mkdirs()) {
|
||||
throw new IllegalStateException("Could not create target level folder: " + newLevelRoot.getAbsolutePath());
|
||||
}
|
||||
|
||||
for (String sub : List.of("data", "datapacks", "players")) {
|
||||
File source = new File(oldLevelRoot, sub);
|
||||
if (!source.exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IO.copyDirectory(source.toPath(), new File(newLevelRoot, sub).toPath());
|
||||
}
|
||||
|
||||
File targetDimensionRoot = IrisWorldStorage.dimensionRoot(newLevelRoot, NamespacedKey.minecraft("overworld"));
|
||||
IO.copyDirectory(sourceDimensionRoot.toPath(), targetDimensionRoot.toPath());
|
||||
|
||||
World sourceWorld = WorldIdentity.resolve(IrisWorldStorage.keyFromLegacyName(newName)).orElse(null);
|
||||
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(newName);
|
||||
if (sourceWorld == null && stagedSeed == null) {
|
||||
throw new IllegalStateException("Cannot determine the promoted world's seed.");
|
||||
}
|
||||
long promotedSeed = sourceWorld == null ? stagedSeed : sourceWorld.getSeed();
|
||||
data.setProperty("level-name", newName);
|
||||
data.setProperty("level-seed", Long.toString(promotedSeed));
|
||||
try (FileOutputStream out = new FileOutputStream(ServerProperties.SERVER_PROPERTIES)) {
|
||||
data.store(out, null);
|
||||
}
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
Iris.error("Failed to update server.properties main world to \"" + newName + "\"");
|
||||
Iris.reportError(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean stageFoliaWorldCreation(String name, IrisDimension dimension, long seed, boolean main) {
|
||||
sender().sendMessage(C.YELLOW + "Runtime world creation is disabled on Folia.");
|
||||
sender().sendMessage(C.YELLOW + "Preparing world files and bukkit.yml for next startup...");
|
||||
|
||||
File worldFolder = IrisWorldStorage.dimensionRoot(name);
|
||||
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(sender(), dimension, worldFolder);
|
||||
if (installed == null) {
|
||||
sender().sendMessage(C.RED + "Failed to stage world files for dimension \"" + dimension.getLoadKey() + "\".");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!registerWorldInBukkitYml(name, dimension.getLoadKey(), seed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (main) {
|
||||
if (updateMainWorld(name)) {
|
||||
sender().sendMessage(C.GREEN + "Updated server.properties level-name to \"" + name + "\".");
|
||||
} else {
|
||||
sender().sendMessage(C.RED + "World was staged, but failed to update server.properties main world.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Staged Iris world \"" + name + "\" with generator Iris:" + dimension.getLoadKey() + " and seed " + seed + ".");
|
||||
if (main) {
|
||||
sender().sendMessage(C.GREEN + "This world is now configured as main for next restart.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean registerWorldInBukkitYml(String worldName, String dimension, Long seed) {
|
||||
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
|
||||
ConfigurationSection worlds = yml.getConfigurationSection("worlds");
|
||||
if (worlds == null) {
|
||||
worlds = yml.createSection("worlds");
|
||||
}
|
||||
ConfigurationSection worldSection = worlds.getConfigurationSection(worldName);
|
||||
if (worldSection == null) {
|
||||
worldSection = worlds.createSection(worldName);
|
||||
}
|
||||
|
||||
String generator = "Iris:" + dimension;
|
||||
worldSection.set("generator", generator);
|
||||
if (seed != null) {
|
||||
worldSection.set("seed", seed);
|
||||
}
|
||||
|
||||
try {
|
||||
yml.save(BUKKIT_YML);
|
||||
Iris.info("Registered \"" + worldName + "\" in bukkit.yml");
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
sender().sendMessage(C.RED + "Failed to update bukkit.yml: " + e.getMessage());
|
||||
Iris.error("Failed to update bukkit.yml!");
|
||||
Iris.reportError(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Teleport to another world", aliases = {"tp"}, sync = true)
|
||||
public void teleport(
|
||||
@Param(description = "World to teleport to")
|
||||
World world,
|
||||
@Param(description = "Player to teleport", defaultValue = "---", customHandler = NullablePlayerHandler.class)
|
||||
Player player
|
||||
) {
|
||||
if (player == null && sender().isPlayer())
|
||||
player = sender().player();
|
||||
|
||||
final Player target = player;
|
||||
if (target == null) {
|
||||
sender().sendMessage(C.RED + "The specified player does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
final Location spawn = world.getSpawnLocation();
|
||||
final Runnable teleportTask = () -> {
|
||||
BukkitPlatform.teleportAsync(target, spawn);
|
||||
new VolmitSender(target).sendMessage(C.GREEN + "You have been teleported to " + world.getName() + ".");
|
||||
};
|
||||
if (!J.runEntity(target, teleportTask)) {
|
||||
teleportTask.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Print version information")
|
||||
public void version() {
|
||||
sender().sendMessage(C.GREEN + "Iris v" + Iris.instance.getDescription().getVersion() + " by Volmit Software");
|
||||
}
|
||||
|
||||
/*
|
||||
/todo
|
||||
@Director(description = "Benchmark a pack", origin = DirectorOrigin.CONSOLE)
|
||||
public void packbenchmark(
|
||||
@Param(description = "Dimension to benchmark")
|
||||
IrisDimension type
|
||||
) throws InterruptedException {
|
||||
|
||||
BenchDimension = type.getLoadKey();
|
||||
|
||||
IrisPackBenchmarking.runBenchmark();
|
||||
} */
|
||||
|
||||
@Director(description = "Print world height information", origin = DirectorOrigin.PLAYER)
|
||||
public void height() {
|
||||
if (sender().isPlayer()) {
|
||||
sender().sendMessage(C.GREEN + "" + sender().player().getWorld().getMinHeight() + " to " + sender().player().getWorld().getMaxHeight());
|
||||
sender().sendMessage(C.GREEN + "Total Height: " + (sender().player().getWorld().getMaxHeight() - sender().player().getWorld().getMinHeight()));
|
||||
} else {
|
||||
World mainWorld = getServer().getWorlds().get(0);
|
||||
Iris.info(C.GREEN + "" + mainWorld.getMinHeight() + " to " + mainWorld.getMaxHeight());
|
||||
Iris.info(C.GREEN + "Total Height: " + (mainWorld.getMaxHeight() - mainWorld.getMinHeight()));
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Check access of all worlds.", aliases = {"accesslist"})
|
||||
public void worlds() {
|
||||
KList<World> IrisWorlds = new KList<>();
|
||||
KList<World> BukkitWorlds = new KList<>();
|
||||
|
||||
for (World w : Bukkit.getServer().getWorlds()) {
|
||||
try {
|
||||
Engine engine = IrisToolbelt.access(w).getEngine();
|
||||
if (engine != null) {
|
||||
IrisWorlds.add(w);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
BukkitWorlds.add(w);
|
||||
}
|
||||
}
|
||||
|
||||
if (sender().isPlayer()) {
|
||||
sender().sendMessage(C.BLUE + "Iris Worlds: ");
|
||||
for (World IrisWorld : IrisWorlds.copy()) {
|
||||
sender().sendMessage(C.IRIS + "- " +IrisWorld.getName());
|
||||
}
|
||||
sender().sendMessage(C.GOLD + "Bukkit Worlds: ");
|
||||
for (World BukkitWorld : BukkitWorlds.copy()) {
|
||||
sender().sendMessage(C.GRAY + "- " +BukkitWorld.getName());
|
||||
}
|
||||
} else {
|
||||
Iris.info(C.BLUE + "Iris Worlds: ");
|
||||
for (World IrisWorld : IrisWorlds.copy()) {
|
||||
Iris.info(C.IRIS + "- " +IrisWorld.getName());
|
||||
}
|
||||
Iris.info(C.GOLD + "Bukkit Worlds: ");
|
||||
for (World BukkitWorld : BukkitWorlds.copy()) {
|
||||
Iris.info(C.GRAY + "- " +BukkitWorld.getName());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Remove an Iris world", aliases = {"rm"}, sync = true)
|
||||
public void remove(
|
||||
@Param(description = "The world to remove")
|
||||
World world,
|
||||
@Param(description = "Whether to also remove the folder (if set to false, just does not load the world)", defaultValue = "true")
|
||||
boolean delete
|
||||
) {
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "Removing world: " + world.getName());
|
||||
|
||||
if (!IrisToolbelt.evacuate(world)) {
|
||||
sender().sendMessage(C.RED + "Failed to evacuate world: " + world.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!WorldLifecycleService.get().unload(world, false)) {
|
||||
sender().sendMessage(C.RED + "Failed to unload world: " + world.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (IrisToolbelt.removeWorld(world)) {
|
||||
sender().sendMessage(C.GREEN + "Successfully removed " + world.getName() + " from bukkit.yml");
|
||||
} else {
|
||||
sender().sendMessage(C.YELLOW + "Looks like the world was already removed from bukkit.yml");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
sender().sendMessage(C.RED + "Failed to save bukkit.yml because of " + e.getMessage());
|
||||
Iris.reportError("Failed to remove world \"" + world.getName() + "\" from bukkit.yml.", e);
|
||||
}
|
||||
IrisToolbelt.evacuate(world, "Deleting world");
|
||||
deletingWorld = true;
|
||||
if (!delete) {
|
||||
deletingWorld = false;
|
||||
return;
|
||||
}
|
||||
VolmitSender sender = sender();
|
||||
J.a(() -> {
|
||||
int retries = 12;
|
||||
|
||||
if (deleteDirectory(world.getWorldFolder())) {
|
||||
sender.sendMessage(C.GREEN + "Successfully removed world folder");
|
||||
} else {
|
||||
while(true){
|
||||
if (deleteDirectory(world.getWorldFolder())){
|
||||
sender.sendMessage(C.GREEN + "Successfully removed world folder");
|
||||
break;
|
||||
}
|
||||
retries--;
|
||||
if (retries == 0){
|
||||
sender.sendMessage(C.RED + "Failed to remove world folder");
|
||||
break;
|
||||
}
|
||||
J.sleep(3000);
|
||||
}
|
||||
}
|
||||
deletingWorld = false;
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean deleteDirectory(File dir) {
|
||||
if (dir.isDirectory()) {
|
||||
File[] children = dir.listFiles();
|
||||
for (int i = 0; i < children.length; i++) {
|
||||
boolean success = deleteDirectory(children[i]);
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dir.delete();
|
||||
}
|
||||
|
||||
@Director(description = "Toggle debug")
|
||||
public void debug() {
|
||||
boolean to = !IrisSettings.get().getGeneral().isDebug();
|
||||
IrisSettings.get().getGeneral().setDebug(to);
|
||||
IrisSettings.get().forceSave();
|
||||
sender().sendMessage(C.GREEN + "Set debug to: " + to);
|
||||
}
|
||||
|
||||
@Director(description = "Download a project.", aliases = "dl")
|
||||
public void download(
|
||||
@Param(name = "pack", description = "The pack to download", aliases = "project")
|
||||
String pack,
|
||||
@Param(name = "branch", description = "The branch to download from", defaultValue = "stable")
|
||||
String branch,
|
||||
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false")
|
||||
boolean overwrite
|
||||
) {
|
||||
if (PackDownloader.isDefaultOverworld(pack)) {
|
||||
sender().sendMessage(C.GREEN + "Downloading pack: " + pack + " (beta release)" + (overwrite ? " overwriting" : ""));
|
||||
Iris.service(StudioSVC.class).downloadDefaultOverworld(sender(), overwrite);
|
||||
} else {
|
||||
sender().sendMessage(C.GREEN + "Downloading pack: " + pack + "/" + branch + (overwrite ? " overwriting" : ""));
|
||||
Iris.service(StudioSVC.class).downloadSearch(sender(), "IrisDimensions/" + pack + "/" + branch, overwrite);
|
||||
}
|
||||
ServerConfigurator.installDataPacksIfChanged(true);
|
||||
}
|
||||
|
||||
@Director(description = "Get metrics for your world", aliases = "measure", origin = DirectorOrigin.PLAYER)
|
||||
public void metrics() {
|
||||
if (!IrisToolbelt.isIrisWorld(world())) {
|
||||
sender().sendMessage(C.RED + "You must be in an Iris world");
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "Sending metrics...");
|
||||
engine().printMetrics(sender());
|
||||
}
|
||||
|
||||
@Director(description = "Reload configuration file (this is also done automatically)")
|
||||
public void reload() {
|
||||
IrisSettings.invalidate();
|
||||
IrisSettings.get();
|
||||
sender().sendMessage(C.GREEN + "Hotloaded settings");
|
||||
}
|
||||
|
||||
|
||||
@Director(description = "Unload an Iris World", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void unloadWorld(
|
||||
@Param(description = "The world to unload")
|
||||
World world
|
||||
) {
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "Unloading world: " + world.getName());
|
||||
try {
|
||||
IrisToolbelt.evacuate(world);
|
||||
boolean unloaded = WorldLifecycleService.get().unload(world, false);
|
||||
if (unloaded) {
|
||||
sender().sendMessage(C.GREEN + "World unloaded successfully.");
|
||||
} else {
|
||||
sender().sendMessage(C.RED + "Failed to unload the world.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
sender().sendMessage(C.RED + "Failed to unload the world: " + e.getMessage());
|
||||
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Load an Iris World", origin = DirectorOrigin.PLAYER, sync = true, aliases = {"import"})
|
||||
public void loadWorld(
|
||||
@Param(description = "The name of the world to load")
|
||||
String world
|
||||
) {
|
||||
worldNameToCheck = world;
|
||||
boolean worldExists = doesWorldExist(worldNameToCheck);
|
||||
WorldEngine = world;
|
||||
|
||||
if (!worldExists) {
|
||||
sender().sendMessage(C.YELLOW + world + " Doesnt exist on the server.");
|
||||
return;
|
||||
}
|
||||
|
||||
File directory = new File(IrisWorldStorage.packRoot(IrisWorldStorage.keyFromLegacyName(world)), "dimensions");
|
||||
|
||||
String dimension = null;
|
||||
if (directory.exists() && directory.isDirectory()) {
|
||||
File[] files = directory.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.isFile()) {
|
||||
String fileName = file.getName();
|
||||
if (fileName.endsWith(".json")) {
|
||||
dimension = fileName.substring(0, fileName.length() - 5);
|
||||
sender().sendMessage(C.BLUE + "Generator: " + dimension);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sender().sendMessage(C.GOLD + world + " is not an iris world.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (dimension == null) {
|
||||
sender().sendMessage(C.RED + "Could not determine Iris dimension for " + world + ".");
|
||||
return;
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Loading world: " + world);
|
||||
|
||||
if (!registerWorldInBukkitYml(world, dimension, null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (J.isFolia()) {
|
||||
sender().sendMessage(C.YELLOW + "Folia cannot load new worlds at runtime. Restart the server to load \"" + world + "\".");
|
||||
return;
|
||||
}
|
||||
|
||||
Iris.instance.checkForBukkitWorlds(world::equals);
|
||||
sender().sendMessage(C.GREEN + world + " loaded successfully.");
|
||||
}
|
||||
@Director(description = "Evacuate an iris world", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void evacuate(
|
||||
@Param(description = "Evacuate the world")
|
||||
World world
|
||||
) {
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "Evacuating world" + world.getName());
|
||||
IrisToolbelt.evacuate(world);
|
||||
}
|
||||
|
||||
boolean doesWorldExist(String worldName) {
|
||||
File worldDirectory = IrisWorldStorage.dimensionRoot(worldName);
|
||||
return worldDirectory.exists() && worldDirectory.isDirectory();
|
||||
}
|
||||
|
||||
public static class PackDimensionTypeHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
Set<String> options = new LinkedHashSet<>();
|
||||
options.add("default");
|
||||
|
||||
File packsFolder = Iris.instance.getDataFolder("packs");
|
||||
File[] packs = packsFolder.listFiles();
|
||||
if (packs != null) {
|
||||
for (File pack : packs) {
|
||||
if (pack == null || !pack.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
options.add(pack.getName());
|
||||
|
||||
try {
|
||||
IrisData data = IrisData.get(pack);
|
||||
for (String key : data.getDimensionLoader().getPossibleKeys()) {
|
||||
options.add(key);
|
||||
options.add(pack.getName() + ":" + key);
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Iris.warn("Failed to read dimension keys from pack %s: %s%s",
|
||||
pack.getName(),
|
||||
ex.getClass().getSimpleName(),
|
||||
ex.getMessage() == null ? "" : " - " + ex.getMessage());
|
||||
Iris.reportError(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new KList<>(options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(String in, boolean force) throws DirectorParsingException {
|
||||
if (in == null || in.trim().isEmpty()) {
|
||||
throw new DirectorParsingException("World type cannot be empty");
|
||||
}
|
||||
|
||||
return in.trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> type) {
|
||||
return type == String.class;
|
||||
}
|
||||
}
|
||||
}
|
||||
+902
@@ -0,0 +1,902 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.commands;
|
||||
|
||||
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.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacementScaleInterpolator;
|
||||
import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||
import art.arcane.iris.engine.object.StudioMode;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.data.Cuboid;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.iris.util.common.data.registry.Materials;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ObjectTargetHandler;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.math.Direction;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.HeightMap;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
@Director(name = "object", aliases = "o", origin = DirectorOrigin.PLAYER, description = "Iris object manipulation")
|
||||
public class CommandObject implements DirectorExecutor {
|
||||
@Director(description = "Open an object studio world (grid of every object; dimension optional, defaults to all packs)", sync = true)
|
||||
public void studio(
|
||||
@Param(defaultValue = "null", description = "Optional dimension whose object pack to lay out; omit to aggregate objects from every pack", aliases = "dim", customHandler = NullableDimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(defaultValue = "1337", description = "The seed to generate the studio with", aliases = "s")
|
||||
long seed
|
||||
) {
|
||||
VolmitSender commandSender = sender();
|
||||
Map<String, IrisData> sources = new LinkedHashMap<>();
|
||||
IrisDimension hostDimension = dimension;
|
||||
|
||||
if (dimension != null) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
data = IrisData.get(dimension.getLoadFile().getParentFile().getParentFile());
|
||||
}
|
||||
sources.put(data.getDataFolder().getName(), data);
|
||||
} else {
|
||||
File workspace = Iris.service(StudioSVC.class).getWorkspaceFolder();
|
||||
File[] packs = workspace == null ? null : workspace.listFiles();
|
||||
if (packs != null) {
|
||||
Arrays.sort(packs, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER));
|
||||
for (File pack : packs) {
|
||||
if (!pack.isDirectory()) continue;
|
||||
File dimensionsDir = new File(pack, "dimensions");
|
||||
if (!dimensionsDir.isDirectory()) continue;
|
||||
IrisData data = IrisData.get(pack);
|
||||
String[] keys = data.getObjectLoader().getPossibleKeys();
|
||||
if (keys == null || keys.length == 0) continue;
|
||||
sources.put(pack.getName(), data);
|
||||
if (hostDimension == null) {
|
||||
File[] dimFiles = dimensionsDir.listFiles((f) -> f.isFile() && f.getName().endsWith(".json"));
|
||||
if (dimFiles != null && dimFiles.length > 0) {
|
||||
String loadKey = dimFiles[0].getName().replaceFirst("\\.json$", "");
|
||||
IrisDimension loaded = data.getDimensionLoader().load(loadKey);
|
||||
if (loaded != null) {
|
||||
hostDimension = loaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hostDimension == null || sources.isEmpty()) {
|
||||
commandSender.sendMessage(C.RED + "No packs with objects were found on this server.");
|
||||
return;
|
||||
}
|
||||
|
||||
int totalObjects = 0;
|
||||
for (IrisData d : sources.values()) {
|
||||
String[] k = d.getObjectLoader().getPossibleKeys();
|
||||
if (k != null) totalObjects += k.length;
|
||||
}
|
||||
if (totalObjects == 0) {
|
||||
commandSender.sendMessage(C.RED + "No objects to place across the selected pack(s).");
|
||||
return;
|
||||
}
|
||||
|
||||
hostDimension.setStudioMode(StudioMode.OBJECT_BUFFET);
|
||||
ObjectStudioActivation.activate(hostDimension.getLoadKey());
|
||||
ObjectStudioActivation.setSources(hostDimension.getLoadKey(), sources);
|
||||
|
||||
String scope = dimension == null
|
||||
? ("all packs [" + sources.size() + "]")
|
||||
: ("\"" + hostDimension.getName() + "\"");
|
||||
commandSender.sendMessage(C.GREEN + "Opening Object Studio for " + scope + " ("
|
||||
+ totalObjects + " objects)");
|
||||
|
||||
IrisDimension finalHost = hostDimension;
|
||||
try {
|
||||
Iris.service(StudioSVC.class).open(commandSender, seed, hostDimension.getLoadKey(), world -> {
|
||||
if (world == null) return;
|
||||
try {
|
||||
WorldRuntimeControlService.get().applyObjectStudioWorldRules(world);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to apply object studio world rules for " + world.getName(), e);
|
||||
}
|
||||
|
||||
if (commandSender.isPlayer()) {
|
||||
Player p = commandSender.player();
|
||||
if (p != null) {
|
||||
Location target = new Location(world, 0.5D, 66D, 0.5D);
|
||||
J.runEntity(p, () -> {
|
||||
BukkitPlatform.teleportAsync(p, target).thenRun(() -> p.setGameMode(GameMode.CREATIVE));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to open object studio world \"" + finalHost.getLoadKey() + "\".", e);
|
||||
commandSender.sendMessage(C.RED + "Failed to open object studio: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static final Set<Material> skipBlocks = Set.of(Materials.GRASS, Material.SNOW, Material.VINE, Material.TORCH, Material.DEAD_BUSH,
|
||||
Material.POPPY, Material.DANDELION);
|
||||
|
||||
public static IObjectPlacer createPlacer(World world, Map<Block, BlockData> futureBlockChanges) {
|
||||
|
||||
return new IObjectPlacer() {
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data) {
|
||||
return world.getHighestBlockYAt(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||
return world.getHighestBlockYAt(x, z, ignoreFluid ? HeightMap.OCEAN_FLOOR : HeightMap.MOTION_BLOCKING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, PlatformBlockState s) {
|
||||
BlockData d = (BlockData) s.nativeHandle();
|
||||
Block block = world.getBlockAt(x, y, z);
|
||||
|
||||
//Prevent blocks being set in or bellow bedrock
|
||||
if (y <= world.getMinHeight() || block.getType() == Material.BEDROCK) return;
|
||||
|
||||
futureBlockChanges.put(block, block.getBlockData());
|
||||
|
||||
if (d instanceof IrisCustomData data) {
|
||||
block.setBlockData(data.getBase(), false);
|
||||
Iris.warn("Tried to place custom block at " + x + ", " + y + ", " + z + " which is not supported!");
|
||||
} else block.setBlockData(d, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState get(int x, int y, int z) {
|
||||
return BukkitBlockState.of(world.getBlockAt(x, y, z).getBlockData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPreventingDecay() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolid(int x, int y, int z) {
|
||||
return world.getBlockAt(x, y, z).getType().isSolid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUnderwater(int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFluidHeight() {
|
||||
return 63;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDebugSmartBore() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTile(int xx, int yy, int zz, TileData tile) {
|
||||
tile.toBukkitTry(world.getBlockAt(xx, yy, zz));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void setData(int xx, int yy, int zz, T data) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Engine getEngine() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Director(description = "Check the composition of an object")
|
||||
public void analyze(
|
||||
@Param(description = "The object to analyze", customHandler = ObjectHandler.class)
|
||||
String object
|
||||
) {
|
||||
IrisObject o = IrisData.loadAnyObject(object, data());
|
||||
sender().sendMessage("Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD() + "");
|
||||
sender().sendMessage("Blocks Used: " + NumberFormat.getIntegerInstance().format(o.getBlocks().size()));
|
||||
|
||||
var queue = o.getBlocks().values();
|
||||
Map<Material, Set<BlockData>> unsorted = new HashMap<>();
|
||||
Map<BlockData, Integer> amounts = new HashMap<>();
|
||||
Map<Material, Integer> materials = new HashMap<>();
|
||||
while (queue.hasNext()) {
|
||||
BlockData block = (BlockData) queue.next().nativeHandle();
|
||||
|
||||
//unsorted.put(block.getMaterial(), block);
|
||||
|
||||
if (!amounts.containsKey(block)) {
|
||||
amounts.put(block, 1);
|
||||
|
||||
|
||||
} else
|
||||
amounts.put(block, amounts.get(block) + 1);
|
||||
|
||||
if (!materials.containsKey(block.getMaterial())) {
|
||||
materials.put(block.getMaterial(), 1);
|
||||
unsorted.put(block.getMaterial(), new HashSet<>());
|
||||
unsorted.get(block.getMaterial()).add(block);
|
||||
} else {
|
||||
materials.put(block.getMaterial(), materials.get(block.getMaterial()) + 1);
|
||||
unsorted.get(block.getMaterial()).add(block);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
List<Material> sortedMatsList = amounts.keySet().stream().map(BlockData::getMaterial)
|
||||
.sorted().toList();
|
||||
Set<Material> sortedMats = new TreeSet<>(Comparator.comparingInt(materials::get).reversed());
|
||||
sortedMats.addAll(sortedMatsList);
|
||||
sender().sendMessage("== Blocks in object ==");
|
||||
|
||||
int n = 0;
|
||||
for (Material mat : sortedMats) {
|
||||
int amount = materials.get(mat);
|
||||
List<BlockData> set = new ArrayList<>(unsorted.get(mat));
|
||||
set.sort(Comparator.comparingInt(amounts::get).reversed());
|
||||
BlockData data = set.get(0);
|
||||
int dataAmount = amounts.get(data);
|
||||
|
||||
String string = " - " + mat.toString() + "*" + amount;
|
||||
if (data.getAsString(true).contains("[")) {
|
||||
string = string + " --> [" + data.getAsString(true).split("\\[")[1]
|
||||
.replaceAll("true", ChatColor.GREEN + "true" + ChatColor.GRAY)
|
||||
.replaceAll("false", ChatColor.RED + "false" + ChatColor.GRAY) + "*" + dataAmount;
|
||||
}
|
||||
|
||||
sender().sendMessage(string);
|
||||
|
||||
n++;
|
||||
|
||||
if (n >= 10) {
|
||||
sender().sendMessage(" + " + (sortedMats.size() - n) + " other block types");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Shrink an object to its minimum size")
|
||||
public void shrink(@Param(description = "The object to shrink", customHandler = ObjectHandler.class) String object) {
|
||||
IrisObject o = IrisData.loadAnyObject(object, data());
|
||||
sender().sendMessage("Current Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD());
|
||||
o.shrinkwrap();
|
||||
sender().sendMessage("New Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD());
|
||||
try {
|
||||
o.write(o.getLoadFile());
|
||||
} catch (IOException e) {
|
||||
sender().sendMessage("Failed to save object " + o.getLoadFile() + ": " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Make tree leaves vanilla-decay-plausible (every leaf within 6 blocks of a log)",
|
||||
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")
|
||||
boolean dryRun,
|
||||
@Param(description = "Canopy shell radius (SMOKE only), clamped [0,5]", defaultValue = "2")
|
||||
int radius
|
||||
) {
|
||||
List<Target> targets = resolveTargets(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 : "")
|
||||
+ "] queued " + targets.size() + " object(s)");
|
||||
|
||||
org.bukkit.command.CommandSender s = sender();
|
||||
J.a(() -> runPlausibilize(targets, dryRun, mode, radius, s));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
IrisConverter.convertSchematics(sender());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Director(description = "Get a powder that reveals objects", aliases = "d")
|
||||
public void dust() {
|
||||
player().getInventory().addItem(WandSVC.createDust());
|
||||
sender().playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f);
|
||||
}
|
||||
|
||||
@Director(description = "Contract a selection based on your looking direction", aliases = "-")
|
||||
public void contract(
|
||||
@Param(description = "The amount to inset by", defaultValue = "1")
|
||||
int amount
|
||||
) {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage("Hold your wand.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Location[] b = WandSVC.getCuboid(player());
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
sender().sendMessage("No area selected.");
|
||||
return;
|
||||
}
|
||||
Location a1 = b[0].clone();
|
||||
Location a2 = b[1].clone();
|
||||
Cuboid cursor = new Cuboid(a1, a2);
|
||||
Direction d = Direction.closest(player().getLocation().getDirection()).reverse();
|
||||
assert d != null;
|
||||
cursor = cursor.expand(d.f(), -amount);
|
||||
b[0] = cursor.getLowerNE();
|
||||
b[1] = cursor.getUpperSW();
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
|
||||
player().updateInventory();
|
||||
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
}
|
||||
|
||||
@Director(description = "Set point 1 to look", aliases = "p1")
|
||||
public void position1(
|
||||
@Param(description = "Whether to use your current position, or where you look", defaultValue = "true")
|
||||
boolean here
|
||||
) {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage("Ready your Wand.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (WandSVC.isHoldingWand(player())) {
|
||||
Location[] g = WandSVC.getCuboid(player());
|
||||
|
||||
if (g == null) {
|
||||
return;
|
||||
}
|
||||
if (!here) {
|
||||
// TODO: WARNING HEIGHT
|
||||
g[1] = player().getTargetBlock(null, 256).getLocation().clone();
|
||||
} else {
|
||||
g[1] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0);
|
||||
}
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Set point 2 to look", aliases = "p2")
|
||||
public void position2(
|
||||
@Param(description = "Whether to use your current position, or where you look", defaultValue = "true")
|
||||
boolean here
|
||||
) {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage("Ready your Wand.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (WandSVC.isHoldingIrisWand(player())) {
|
||||
Location[] g = WandSVC.getCuboid(player());
|
||||
|
||||
if (g == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!here) {
|
||||
// TODO: WARNING HEIGHT
|
||||
g[0] = player().getTargetBlock(null, 256).getLocation().clone();
|
||||
} else {
|
||||
g[0] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0);
|
||||
}
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Paste an object", sync = true)
|
||||
public void paste(
|
||||
@Param(description = "The object to paste", customHandler = ObjectHandler.class)
|
||||
String object,
|
||||
@Param(description = "Whether or not to edit the object (need to hold wand)", defaultValue = "false")
|
||||
boolean edit,
|
||||
@Param(description = "The amount of degrees to rotate by", defaultValue = "0")
|
||||
int rotate,
|
||||
@Param(description = "The factor by which to scale the object placement", defaultValue = "1")
|
||||
double scale
|
||||
// ,
|
||||
// @Param(description = "The scale interpolator to use", defaultValue = "none")
|
||||
// IrisObjectPlacementScaleInterpolator interpolator
|
||||
) {
|
||||
IrisObject o = IrisData.loadAnyObject(object, data());
|
||||
double maxScale = Double.max(10 - o.getBlocks().size() / 10000d, 1);
|
||||
if (scale > maxScale) {
|
||||
sender().sendMessage(C.YELLOW + "Indicated scale exceeds maximum. Downscaled to maximum: " + maxScale);
|
||||
scale = maxScale;
|
||||
}
|
||||
|
||||
sender().playSound(Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1f, 1.5f);
|
||||
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
placement.setRotation(IrisObjectRotation.of(0, rotate, 0));
|
||||
|
||||
ItemStack wand = player().getInventory().getItemInMainHand();
|
||||
Location block = player().getTargetBlock(skipBlocks, 256).getLocation().clone().add(0, 1, 0);
|
||||
|
||||
Map<Block, BlockData> futureChanges = new HashMap<>();
|
||||
|
||||
if (scale != 1) {
|
||||
o = o.scaled(scale, IrisObjectPlacementScaleInterpolator.TRICUBIC);
|
||||
}
|
||||
|
||||
o.place(block.getBlockX(), block.getBlockY() + (int) o.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges), placement, new RNG(), null);
|
||||
|
||||
Iris.service(ObjectSVC.class).addChanges(futureChanges);
|
||||
|
||||
if (edit) {
|
||||
Vector center = new Vector(o.getCenter().getX(), o.getCenter().getY(), o.getCenter().getZ());
|
||||
ItemStack newWand = WandSVC.createWand(block.clone().subtract(center).add(o.getW() - 1,
|
||||
o.getH() + center.getY() - 1, o.getD() - 1), block.clone().subtract(center.clone().setY(0)));
|
||||
if (WandSVC.isWand(wand)) {
|
||||
wand = newWand;
|
||||
player().getInventory().setItemInMainHand(wand);
|
||||
sender().sendMessage("Updated wand for " + "objects/" + o.getLoadKey() + ".iob ");
|
||||
} else {
|
||||
int slot = WandSVC.findWand(player().getInventory());
|
||||
if (slot == -1) {
|
||||
player().getInventory().addItem(newWand);
|
||||
sender().sendMessage("Given new wand for " + "objects/" + o.getLoadKey() + ".iob ");
|
||||
} else {
|
||||
player().getInventory().setItem(slot, newWand);
|
||||
sender().sendMessage("Updated wand for " + "objects/" + o.getLoadKey() + ".iob ");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sender().sendMessage(C.IRIS + "Placed " + object);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Save an object")
|
||||
public void save(
|
||||
@Param(description = "The dimension to store the object in", contextual = true)
|
||||
IrisDimension dimension,
|
||||
@Param(description = "The file to store it in, can use / for subfolders")
|
||||
String name,
|
||||
@Param(description = "Overwrite existing object files", defaultValue = "false", aliases = "force")
|
||||
boolean overwrite,
|
||||
@Param(description = "Use legacy TileState serialization if possible", defaultValue = "true")
|
||||
boolean legacy
|
||||
) {
|
||||
IrisObject o = WandSVC.createSchematic(player(), legacy);
|
||||
|
||||
if (o == null) {
|
||||
sender().sendMessage(C.YELLOW + "You need to hold your wand!");
|
||||
return;
|
||||
}
|
||||
|
||||
File file = Iris.service(StudioSVC.class).getWorkspaceFile(dimension.getLoadKey(), "objects", name + ".iob");
|
||||
|
||||
if (file.exists() && !overwrite) {
|
||||
sender().sendMessage(C.RED + "File already exists. Set overwrite=true to overwrite it.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
o.write(file, sender());
|
||||
} catch (IOException e) {
|
||||
sender().sendMessage(C.RED + "Failed to save object because of an IOException: " + e.getMessage());
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
sender().playSound(Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1f, 1.5f);
|
||||
sender().sendMessage(C.GREEN + "Successfully object to saved: " + dimension.getLoadKey() + "/objects/" + name);
|
||||
}
|
||||
|
||||
@Director(description = "Shift a selection in your looking direction")
|
||||
public void shift(
|
||||
@Param(description = "The amount to shift by", defaultValue = "1")
|
||||
int amount
|
||||
) {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage("Hold your wand.");
|
||||
return;
|
||||
}
|
||||
|
||||
Location[] b = WandSVC.getCuboid(player());
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
sender().sendMessage("No area selected.");
|
||||
return;
|
||||
}
|
||||
Location a1 = b[0].clone();
|
||||
Location a2 = b[1].clone();
|
||||
Direction d = Direction.closest(player().getLocation().getDirection()).reverse();
|
||||
if (d == null) {
|
||||
return; // HOW DID THIS HAPPEN
|
||||
}
|
||||
a1.add(d.toVector().multiply(amount));
|
||||
a2.add(d.toVector().multiply(amount));
|
||||
Cuboid cursor = new Cuboid(a1, a2);
|
||||
b[0] = cursor.getLowerNE();
|
||||
b[1] = cursor.getUpperSW();
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
|
||||
player().updateInventory();
|
||||
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
}
|
||||
|
||||
@Director(description = "Undo a number of pastes", aliases = "u")
|
||||
public void undo(
|
||||
@Param(description = "The amount of pastes to undo", defaultValue = "1")
|
||||
int amount
|
||||
) {
|
||||
ObjectSVC service = Iris.service(ObjectSVC.class);
|
||||
int actualReverts = Math.min(service.getUndos().size(), amount);
|
||||
service.revertChanges(actualReverts);
|
||||
sender().sendMessage(C.BLUE + "Reverted " + actualReverts + C.BLUE +" pastes!");
|
||||
}
|
||||
|
||||
@Director(description = "Gets an object wand and grabs the current WorldEdit selection.", aliases = "we", origin = DirectorOrigin.PLAYER)
|
||||
public void we() {
|
||||
if (!Bukkit.getPluginManager().isPluginEnabled("WorldEdit")) {
|
||||
sender().sendMessage(C.RED + "You can't get a WorldEdit selection without WorldEdit, you know.");
|
||||
return;
|
||||
}
|
||||
|
||||
Cuboid locs = WorldEditLink.getSelection(sender().player());
|
||||
|
||||
if (locs == null) {
|
||||
sender().sendMessage(C.RED + "You don't have a WorldEdit selection in this world.");
|
||||
return;
|
||||
}
|
||||
|
||||
sender().player().getInventory().addItem(WandSVC.createWand(locs.getLowerNE(), locs.getUpperSW()));
|
||||
sender().sendMessage(C.GREEN + "A fresh wand with your current WorldEdit selection on it!");
|
||||
}
|
||||
|
||||
@Director(description = "Get an object wand", sync = true)
|
||||
public void wand() {
|
||||
player().getInventory().addItem(WandSVC.createWand());
|
||||
sender().playSound(Sound.ITEM_ARMOR_EQUIP_NETHERITE, 1f, 1.5f);
|
||||
sender().sendMessage(C.GREEN + "Poof! Good luck building!");
|
||||
}
|
||||
|
||||
@Director(name = "x&y", description = "Autoselect up, down & out", sync = true)
|
||||
public void xay() {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage(C.YELLOW + "Hold your wand!");
|
||||
return;
|
||||
}
|
||||
|
||||
Location[] b = WandSVC.getCuboid(player());
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
sender().sendMessage("No area selected.");
|
||||
return;
|
||||
}
|
||||
Location a1 = b[0].clone();
|
||||
Location a2 = b[1].clone();
|
||||
Location a1x = b[0].clone();
|
||||
Location a2x = b[1].clone();
|
||||
Cuboid cursor = new Cuboid(a1, a2);
|
||||
Cuboid cursorx = new Cuboid(a1, a2);
|
||||
|
||||
while (!cursor.containsOnly(Material.AIR)) {
|
||||
a1.add(new org.bukkit.util.Vector(0, 1, 0));
|
||||
a2.add(new org.bukkit.util.Vector(0, 1, 0));
|
||||
cursor = new Cuboid(a1, a2);
|
||||
}
|
||||
|
||||
a1.add(new org.bukkit.util.Vector(0, -1, 0));
|
||||
a2.add(new org.bukkit.util.Vector(0, -1, 0));
|
||||
|
||||
while (!cursorx.containsOnly(Material.AIR)) {
|
||||
a1x.add(new org.bukkit.util.Vector(0, -1, 0));
|
||||
a2x.add(new org.bukkit.util.Vector(0, -1, 0));
|
||||
cursorx = new Cuboid(a1x, a2x);
|
||||
}
|
||||
|
||||
a1x.add(new org.bukkit.util.Vector(0, 1, 0));
|
||||
a2x.add(new Vector(0, 1, 0));
|
||||
b[0] = a1;
|
||||
b[1] = a2x;
|
||||
cursor = new Cuboid(b[0], b[1]);
|
||||
cursor = cursor.contract(Cuboid.CuboidDirection.North);
|
||||
cursor = cursor.contract(Cuboid.CuboidDirection.South);
|
||||
cursor = cursor.contract(Cuboid.CuboidDirection.East);
|
||||
cursor = cursor.contract(Cuboid.CuboidDirection.West);
|
||||
b[0] = cursor.getLowerNE();
|
||||
b[1] = cursor.getUpperSW();
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
|
||||
player().updateInventory();
|
||||
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
sender().sendMessage(C.GREEN + "Auto-select complete!");
|
||||
}
|
||||
|
||||
@Director(name = "x+y", description = "Autoselect up & out", sync = true)
|
||||
public void xpy() {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage(C.YELLOW + "Hold your wand!");
|
||||
return;
|
||||
}
|
||||
|
||||
Location[] b = WandSVC.getCuboid(player());
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
sender().sendMessage("No area selected.");
|
||||
return;
|
||||
}
|
||||
b[0].add(new Vector(0, 1, 0));
|
||||
b[1].add(new Vector(0, 1, 0));
|
||||
Location a1 = b[0].clone();
|
||||
Location a2 = b[1].clone();
|
||||
Cuboid cursor = new Cuboid(a1, a2);
|
||||
|
||||
while (!cursor.containsOnly(Material.AIR)) {
|
||||
a1.add(new Vector(0, 1, 0));
|
||||
a2.add(new Vector(0, 1, 0));
|
||||
cursor = new Cuboid(a1, a2);
|
||||
}
|
||||
|
||||
a1.add(new Vector(0, -1, 0));
|
||||
a2.add(new Vector(0, -1, 0));
|
||||
b[0] = a1;
|
||||
a2 = b[1];
|
||||
cursor = new Cuboid(a1, a2);
|
||||
cursor = cursor.contract(Cuboid.CuboidDirection.North);
|
||||
cursor = cursor.contract(Cuboid.CuboidDirection.South);
|
||||
cursor = cursor.contract(Cuboid.CuboidDirection.East);
|
||||
cursor = cursor.contract(Cuboid.CuboidDirection.West);
|
||||
b[0] = cursor.getLowerNE();
|
||||
b[1] = cursor.getUpperSW();
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
|
||||
player().updateInventory();
|
||||
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
sender().sendMessage(C.GREEN + "Auto-select complete!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.commands;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.core.pack.PackResourceCleanup;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
import art.arcane.iris.core.pack.PackValidator;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance")
|
||||
public class CommandPack implements DirectorExecutor {
|
||||
@Director(description = "Validate a pack (or all packs) and re-publish results", aliases = {"v"})
|
||||
public void validate(
|
||||
@Param(description = "The pack folder name to validate (leave empty for all)", defaultValue = "")
|
||||
String pack
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
File packsRoot = Iris.instance.getDataFolder("packs");
|
||||
if (!packsRoot.isDirectory()) {
|
||||
s.sendMessage(C.RED + "packs/ folder not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (pack == null || pack.isBlank()) {
|
||||
File[] dirs = packsRoot.listFiles(File::isDirectory);
|
||||
if (dirs == null || dirs.length == 0) {
|
||||
s.sendMessage(C.YELLOW + "No packs to validate.");
|
||||
return;
|
||||
}
|
||||
int broken = 0;
|
||||
for (File dir : dirs) {
|
||||
PackValidationResult result = runValidate(s, dir);
|
||||
if (result != null && !result.isLoadable()) {
|
||||
broken++;
|
||||
}
|
||||
}
|
||||
s.sendMessage(C.GREEN + "Validation complete. Broken packs: " + broken + "/" + dirs.length);
|
||||
return;
|
||||
}
|
||||
|
||||
File target = PackDirectoryResolver.resolveExisting(packsRoot, pack);
|
||||
if (target == null) {
|
||||
s.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
|
||||
return;
|
||||
}
|
||||
runValidate(s, target);
|
||||
}
|
||||
|
||||
@Director(description = "Preview or apply unused-resource cleanup", aliases = {"c"})
|
||||
public void cleanup(
|
||||
@Param(description = "The pack folder name to clean")
|
||||
String pack,
|
||||
@Param(description = "preview or apply", defaultValue = "preview")
|
||||
String mode
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
File packFolder = findPack(s, pack);
|
||||
if (packFolder == null) {
|
||||
return;
|
||||
}
|
||||
if ("apply".equalsIgnoreCase(mode)) {
|
||||
PackResourceCleanup.ApplyResult result = PackResourceCleanup.apply(packFolder);
|
||||
if (!result.success()) {
|
||||
s.sendMessage(C.RED + result.error());
|
||||
reportPaths(s, result.quarantinedPaths(), "still quarantined");
|
||||
return;
|
||||
}
|
||||
if (!result.changed()) {
|
||||
s.sendMessage(C.GREEN + "No cleanup candidates found for pack '" + pack + "'.");
|
||||
return;
|
||||
}
|
||||
s.sendMessage(C.GREEN + "Quarantined " + result.quarantinedPaths().size()
|
||||
+ " cleanup candidate(s) under " + result.quarantinePath() + ".");
|
||||
reportPaths(s, result.quarantinedPaths(), "quarantined");
|
||||
return;
|
||||
}
|
||||
if (!"preview".equalsIgnoreCase(mode)) {
|
||||
s.sendMessage(C.RED + "Cleanup mode must be preview or apply.");
|
||||
return;
|
||||
}
|
||||
PackResourceCleanup.Preview preview = PackResourceCleanup.preview(packFolder);
|
||||
if (!preview.success()) {
|
||||
s.sendMessage(C.RED + preview.error());
|
||||
return;
|
||||
}
|
||||
if (!preview.hasCandidates()) {
|
||||
s.sendMessage(C.GREEN + "No cleanup candidates found for pack '" + pack + "'.");
|
||||
return;
|
||||
}
|
||||
s.sendMessage(C.YELLOW + "Cleanup preview for pack '" + pack + "': "
|
||||
+ preview.candidatePaths().size() + " candidate(s). No files were changed.");
|
||||
reportPaths(s, preview.candidatePaths(), "candidate");
|
||||
s.sendMessage(C.GRAY + "Run /iris pack cleanup " + pack + " mode=apply to quarantine these candidates after a fresh scan.");
|
||||
}
|
||||
|
||||
@Director(description = "Preview or apply restoration of the latest quarantine", aliases = {"r"})
|
||||
public void restore(
|
||||
@Param(description = "The pack folder name to restore")
|
||||
String pack,
|
||||
@Param(description = "preview or apply", defaultValue = "preview")
|
||||
String mode
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
File packFolder = findPack(s, pack);
|
||||
if (packFolder == null) {
|
||||
return;
|
||||
}
|
||||
if ("apply".equalsIgnoreCase(mode)) {
|
||||
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(packFolder);
|
||||
if (!result.conflicts().isEmpty()) {
|
||||
s.sendMessage(C.RED + "Restore refused because " + result.conflicts().size() + " destination(s) already exist.");
|
||||
reportPaths(s, result.conflicts(), "conflict");
|
||||
return;
|
||||
}
|
||||
if (!result.success()) {
|
||||
s.sendMessage(C.RED + result.error());
|
||||
return;
|
||||
}
|
||||
if (!result.changed()) {
|
||||
s.sendMessage(C.YELLOW + "Nothing to restore for pack '" + pack + "'.");
|
||||
return;
|
||||
}
|
||||
s.sendMessage(C.GREEN + "Restored " + result.restoredPaths().size()
|
||||
+ " file(s) from " + result.dumpPath() + ".");
|
||||
reportPaths(s, result.restoredPaths(), "restored");
|
||||
return;
|
||||
}
|
||||
if (!"preview".equalsIgnoreCase(mode)) {
|
||||
s.sendMessage(C.RED + "Restore mode must be preview or apply.");
|
||||
return;
|
||||
}
|
||||
PackResourceCleanup.RestorePreview preview = PackResourceCleanup.previewRestore(packFolder);
|
||||
if (!preview.success()) {
|
||||
s.sendMessage(C.RED + preview.error());
|
||||
return;
|
||||
}
|
||||
if (!preview.hasFiles()) {
|
||||
s.sendMessage(C.YELLOW + "Nothing to restore for pack '" + pack + "'.");
|
||||
return;
|
||||
}
|
||||
s.sendMessage(C.YELLOW + "Restore preview for " + preview.dumpPath() + ": "
|
||||
+ preview.filePaths().size() + " file(s). No files were changed.");
|
||||
reportPaths(s, preview.filePaths(), "file");
|
||||
if (!preview.conflicts().isEmpty()) {
|
||||
s.sendMessage(C.RED + "Restore is blocked by " + preview.conflicts().size() + " existing destination(s).");
|
||||
reportPaths(s, preview.conflicts(), "conflict");
|
||||
return;
|
||||
}
|
||||
s.sendMessage(C.GRAY + "Run /iris pack restore " + pack + " mode=apply to restore after a fresh conflict check.");
|
||||
}
|
||||
|
||||
@Director(description = "Show cached validation status for a pack", aliases = {"s"})
|
||||
public void status(
|
||||
@Param(description = "The pack folder name", defaultValue = "")
|
||||
String pack
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
if (pack == null || pack.isBlank()) {
|
||||
if (PackValidationRegistry.snapshot().isEmpty()) {
|
||||
s.sendMessage(C.YELLOW + "No validation results recorded. Run /iris pack validate first.");
|
||||
return;
|
||||
}
|
||||
PackValidationRegistry.snapshot().forEach((name, result) -> {
|
||||
String tag = result.isLoadable() ? (C.GREEN + "OK") : (C.RED + "BROKEN");
|
||||
s.sendMessage(tag + C.RESET + " " + name
|
||||
+ C.GRAY + " (blocking=" + result.getBlockingErrors().size()
|
||||
+ ", warnings=" + result.getWarnings().size() + ")");
|
||||
});
|
||||
return;
|
||||
}
|
||||
PackValidationResult result = PackValidationRegistry.get(pack);
|
||||
if (result == null) {
|
||||
s.sendMessage(C.YELLOW + "No validation result for '" + pack + "'. Run /iris pack validate " + pack + ".");
|
||||
return;
|
||||
}
|
||||
reportResult(s, result);
|
||||
}
|
||||
|
||||
private PackValidationResult runValidate(VolmitSender s, File packFolder) {
|
||||
try {
|
||||
PackValidationResult result = PackValidator.validate(packFolder);
|
||||
PackValidationRegistry.publish(result);
|
||||
reportResult(s, result);
|
||||
return result;
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e);
|
||||
s.sendMessage(C.RED + "Validation of '" + packFolder.getName() + "' failed: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void reportResult(VolmitSender s, PackValidationResult result) {
|
||||
if (result.isLoadable()) {
|
||||
s.sendMessage(C.GREEN + "Pack '" + result.getPackName() + "' is loadable."
|
||||
+ C.GRAY + " (warnings=" + result.getWarnings().size() + ")");
|
||||
} else {
|
||||
s.sendMessage(C.RED + "Pack '" + result.getPackName() + "' is BROKEN:");
|
||||
for (String reason : result.getBlockingErrors()) {
|
||||
s.sendMessage(C.RED + " - " + reason);
|
||||
}
|
||||
}
|
||||
int wMax = Math.min(10, result.getWarnings().size());
|
||||
for (int i = 0; i < wMax; i++) {
|
||||
s.sendMessage(C.YELLOW + " ! " + result.getWarnings().get(i));
|
||||
}
|
||||
if (result.getWarnings().size() > wMax) {
|
||||
s.sendMessage(C.GRAY + " ... and " + (result.getWarnings().size() - wMax) + " more warning(s).");
|
||||
}
|
||||
}
|
||||
|
||||
private File findPack(VolmitSender sender, String pack) {
|
||||
if (pack == null || pack.isBlank()) {
|
||||
sender.sendMessage(C.RED + "You must specify a pack name.");
|
||||
return null;
|
||||
}
|
||||
File packFolder = PackDirectoryResolver.resolveExisting(Iris.instance.getDataFolder("packs"), pack);
|
||||
if (packFolder == null) {
|
||||
sender.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
|
||||
return null;
|
||||
}
|
||||
return packFolder;
|
||||
}
|
||||
|
||||
private void reportPaths(VolmitSender sender, List<String> paths, String label) {
|
||||
int max = Math.min(10, paths.size());
|
||||
for (int i = 0; i < max; i++) {
|
||||
sender.sendMessage(C.GRAY + " - " + label + ": " + paths.get(i));
|
||||
}
|
||||
if (paths.size() > max) {
|
||||
sender.sendMessage(C.GRAY + " ... and " + (paths.size() - max) + " more.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.commands;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.pregenerator.PregenTask;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
@Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!")
|
||||
public class CommandPregen implements DirectorExecutor {
|
||||
@Director(description = "Pregenerate a world")
|
||||
public void start(
|
||||
@Param(description = "The radius of the pregen in blocks", aliases = "size")
|
||||
int radius,
|
||||
@Param(description = "The world to pregen", contextual = true)
|
||||
World world,
|
||||
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0")
|
||||
Vector center,
|
||||
@Param(description = "Open the Iris pregen gui", defaultValue = "true")
|
||||
boolean gui,
|
||||
@Param(name = "serial", description = "Generate only one chunk at a time", defaultValue = "false")
|
||||
boolean serial
|
||||
) {
|
||||
if (radius <= 0) {
|
||||
sender().sendMessage(C.RED + "Pregen radius must be greater than zero blocks.");
|
||||
return;
|
||||
}
|
||||
if (serial && !IrisToolbelt.supportsStrictSerialPregeneration()) {
|
||||
sender().sendMessage(C.RED + "Strict serial pregeneration requires Paper or a Paper-compatible server.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (sender().isPlayer() && access() == null) {
|
||||
sender().sendMessage(C.RED + "The engine access for this world is null!");
|
||||
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
|
||||
}
|
||||
PregenTask task = PregenTask
|
||||
.builder()
|
||||
.center(new Position2(center.getBlockX(), center.getBlockZ()))
|
||||
.gui(gui)
|
||||
.radiusX(radius)
|
||||
.radiusZ(radius)
|
||||
.build();
|
||||
if (serial) {
|
||||
IrisToolbelt.pregenerateSerial(task, world);
|
||||
} else {
|
||||
IrisToolbelt.pregenerate(task, world);
|
||||
}
|
||||
String msg = C.GREEN + "Pregen started in " + C.GOLD + world.getName() + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
|
||||
sender().sendMessage(msg);
|
||||
Iris.info(msg);
|
||||
} catch (Throwable e) {
|
||||
sender().sendMessage(C.RED + "Failed to start pregeneration. See console for details.");
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Stop the active pregeneration task", aliases = "x")
|
||||
public void stop() {
|
||||
if (PregeneratorJob.shutdownInstance()) {
|
||||
String message = C.BLUE + "Pregen stop requested; finishing active work before cancellation.";
|
||||
sender().sendMessage(message);
|
||||
Iris.info(message);
|
||||
} else {
|
||||
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Pause / continue the active pregeneration task", aliases = {"resume"})
|
||||
public void pause() {
|
||||
if (PregeneratorJob.pauseResume()) {
|
||||
sender().sendMessage(C.GREEN + "Paused/unpaused pregeneration task, now: " + (PregeneratorJob.isPaused() ? "Paused" : "Running") + ".");
|
||||
} else {
|
||||
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to pause/unpause.");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Show the active pregeneration status")
|
||||
public void status() {
|
||||
PregeneratorJob.PregenProgress progress = PregeneratorJob.progressSnapshot();
|
||||
if (progress == null) {
|
||||
sender().sendMessage(C.YELLOW + "No active pregeneration task.");
|
||||
return;
|
||||
}
|
||||
|
||||
String world = progress.worldName() == null ? "?" : progress.worldName();
|
||||
sender().sendMessage(C.GREEN + "Pregen " + C.GOLD + world + C.GREEN + ": " + C.GOLD + Form.f(progress.generated()) + "/" + Form.f(progress.totalChunks())
|
||||
+ C.GREEN + " (" + C.GOLD + String.format("%.1f", progress.percent()) + "%" + C.GREEN + ")"
|
||||
+ (progress.paused() ? C.YELLOW + " PAUSED" : ""));
|
||||
sender().sendMessage(C.GREEN + "Speed: " + C.GOLD + Form.f((int) progress.chunksPerSecond()) + "/s" + C.GREEN
|
||||
+ " ETA: " + C.GOLD + Form.duration(progress.eta(), 2) + C.GREEN
|
||||
+ " Elapsed: " + C.GOLD + Form.duration(progress.elapsed(), 2) + C.GREEN
|
||||
+ " Method: " + C.GOLD + progress.method()
|
||||
+ (progress.failed() > 0 ? C.RED + " Failed: " + Form.f(progress.failed()) : ""));
|
||||
}
|
||||
}
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.commands;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.structure.BulkStructureImporter;
|
||||
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.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.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.ObjectPlaceMode;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import io.papermc.paper.registry.RegistryAccess;
|
||||
import io.papermc.paper.registry.RegistryKey;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.generator.structure.Structure;
|
||||
import org.bukkit.util.StructureSearchResult;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Director(name = "structure", aliases = {"struct", "str"}, description = "Iris structure tools (index, import, info)")
|
||||
public class CommandStructure implements DirectorExecutor {
|
||||
@Director(description = "Regenerate structure-index.json listing all vanilla, datapack & iris structures", aliases = {"ls"}, origin = DirectorOrigin.BOTH)
|
||||
public void list(
|
||||
@Param(description = "The dimension whose pack to index", aliases = "dim")
|
||||
IrisDimension dimension
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
File file = StructureIndexService.write(data);
|
||||
sender().sendMessage(C.GREEN + "Wrote structure index: " + C.WHITE + file.getPath());
|
||||
}
|
||||
|
||||
@Director(name = "import", description = "Import EVERY structure - vanilla AND ingested datapacks - into this pack as editable Iris resources, always overwriting. Rebuilds jigsaw structures (villages, outposts, datapack jigsaws) as editable pool/piece graphs, imports every structure template NBT as an object, and assembles the multi-template structures (shipwrecks, ruined portals, ocean ruins, nether fossils). Run after ingesting a datapack and restarting. Regenerate chunks or use a fresh world for the imported copies to place.", aliases = {"import-all", "reimport", "imp", "all"}, origin = DirectorOrigin.BOTH)
|
||||
public void importAll(
|
||||
@Param(description = "The dimension whose pack to import into", aliases = "dim")
|
||||
IrisDimension dimension
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "Importing all vanilla & datapack structures into " + C.WHITE + dimension.getLoadKey() + C.GREEN + " (overwrite)...");
|
||||
BulkStructureImporter.Report jigsaws = BulkStructureImporter.importAllVanilla(data, StructureImporter.Mode.OVERWRITE, true, sender());
|
||||
BulkStructureImporter.Report templates = BulkStructureImporter.importAllTemplates(data, StructureImporter.Mode.OVERWRITE, sender());
|
||||
BulkStructureImporter.Report groups = BulkStructureImporter.importTemplateGroups(data, StructureImporter.Mode.OVERWRITE, sender());
|
||||
StructureCaptureImporter.Report captured = StructureCaptureImporter.importAllStructures(data, StructureImporter.Mode.OVERWRITE, sender());
|
||||
int imported = jigsaws.imported() + templates.imported() + groups.imported() + captured.imported();
|
||||
int failed = jigsaws.failed() + templates.failed() + groups.failed() + captured.failed();
|
||||
sender().sendMessage(C.GREEN + "Import complete: " + C.WHITE + imported + C.GREEN + " structures/objects written, " + C.WHITE + failed + C.GREEN + " failed.");
|
||||
sender().sendMessage(C.GRAY + "Reference them from a biome/region/dimension 'structures' list, or run /iris structure list " + dimension.getLoadKey() + " to refresh the index. Regenerate chunks for changes to take effect.");
|
||||
}
|
||||
|
||||
@Director(name = "capture", description = "Capture code-generated structures that have no NBT template (swamp huts, igloos, etc.) into editable Iris objects by generating each one in a throwaway scratch world and reading back its blocks. Skips structures that already import as a structure, structures wider/taller than the capture cap (strongholds, mansions, monuments stay vanilla), and anything that will not generate in a flat overworld. Each captured structure becomes a single-piece Iris structure you can place from a biome/region/dimension 'structures' list. Runs automatically as the last pass of /iris structure import.", aliases = {"cap"}, origin = DirectorOrigin.BOTH)
|
||||
public void capture(
|
||||
@Param(description = "The dimension whose pack to capture into", aliases = "dim")
|
||||
IrisDimension dimension
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
StructureCaptureImporter.Report report = StructureCaptureImporter.importAllStructures(data, StructureImporter.Mode.OVERWRITE, sender());
|
||||
sender().sendMessage(C.GRAY + "Captured " + report.imported() + " structures. Place them from a 'structures' list and regenerate chunks. Delete a structures/*.json to re-capture it.");
|
||||
}
|
||||
|
||||
@Director(description = "Locate every vanilla/datapack/iris structure to verify which are locatable in this world. Heavy synchronous search per structure - keep the radius modest. 'Not found' can mean rarer than the radius, a different dimension (nether/end structures never appear in the overworld), or a structure that generates but cannot be located; generation itself happens during chunk decoration, independent of locate.", aliases = {"locateall"}, origin = DirectorOrigin.BOTH, sync = true)
|
||||
public void verify(
|
||||
@Param(description = "The dimension to verify", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "Search radius in chunks around the origin (larger is much slower)", defaultValue = "48")
|
||||
int radius
|
||||
) {
|
||||
World world = resolveIrisWorld(dimension);
|
||||
if (world == null) {
|
||||
sender().sendMessage(C.RED + "No loaded Iris world found for " + dimension.getLoadKey() + ". Join or create one first (the search runs against a live world).");
|
||||
return;
|
||||
}
|
||||
boolean senderIsPlayer = sender() != null && sender().isPlayer();
|
||||
Location center = (senderIsPlayer && player().getWorld() == world) ? player().getLocation() : world.getSpawnLocation();
|
||||
int searchRadius = Math.max(1, Math.min(radius, 1000));
|
||||
|
||||
sender().sendMessage(C.GREEN + "Verifying structures in " + C.WHITE + world.getName() + C.GREEN + " from " + center.getBlockX() + "," + center.getBlockZ() + " within " + searchRadius + " chunks...");
|
||||
|
||||
Engine engine = null;
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access != null) {
|
||||
engine = access.getEngine();
|
||||
}
|
||||
Set<String> reachable = engine == null ? Collections.emptySet() : StructureReachability.reachableKeys(engine);
|
||||
|
||||
int found = 0;
|
||||
int missing = 0;
|
||||
int unreachable = 0;
|
||||
int irisPlaced = 0;
|
||||
KList<String> notFound = new KList<>();
|
||||
KList<String> cannotGenerate = new KList<>();
|
||||
Registry<Structure> structureRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.STRUCTURE);
|
||||
for (Structure structure : structureRegistry) {
|
||||
NamespacedKey key = structureRegistry.getKey(structure);
|
||||
String keyName = key == null ? structure.toString() : key.toString();
|
||||
boolean isIrisPlaced = engine != null && IrisStructureLocator.suppressesVanilla(engine, keyName);
|
||||
boolean isReachable = engine != null && reachable.contains(keyName.toLowerCase());
|
||||
if (engine != null && !isIrisPlaced && !isReachable) {
|
||||
unreachable++;
|
||||
KList<String> miss = StructureReachability.missingBiomeKeys(engine, keyName);
|
||||
cannotGenerate.add(keyName + (miss.isEmpty() ? "" : " (needs " + String.join("/", miss) + ")"));
|
||||
continue;
|
||||
}
|
||||
if (isIrisPlaced) {
|
||||
irisPlaced++;
|
||||
}
|
||||
try {
|
||||
StructureSearchResult result = world.locateNearestStructure(center, structure, searchRadius, true);
|
||||
if (result != null && result.getLocation() != null) {
|
||||
found++;
|
||||
Location l = result.getLocation();
|
||||
sender().sendMessage((isIrisPlaced ? C.AQUA + "[iris] " : C.GREEN + "[ok] ") + C.WHITE + keyName + C.GREEN + " @ " + l.getBlockX() + "," + l.getBlockZ());
|
||||
} else {
|
||||
missing++;
|
||||
notFound.add(keyName);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
missing++;
|
||||
notFound.add(keyName + " (error: " + e.getClass().getSimpleName() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Structure verify: " + C.WHITE + found + C.GREEN + " located (" + irisPlaced + " iris-placed), "
|
||||
+ C.WHITE + unreachable + C.GREEN + " cannot generate here, "
|
||||
+ C.WHITE + missing + C.GREEN + " reachable-but-not-found within " + searchRadius + " chunks.");
|
||||
if (!cannotGenerate.isEmpty()) {
|
||||
sender().sendMessage(C.RED + "Cannot generate (required biomes absent from this pack): " + C.GRAY + String.join(", ", cannotGenerate));
|
||||
}
|
||||
if (!notFound.isEmpty()) {
|
||||
sender().sendMessage(C.YELLOW + "Reachable but not found (rarer than radius): " + C.GRAY + String.join(", ", notFound));
|
||||
}
|
||||
}
|
||||
|
||||
private World resolveIrisWorld(IrisDimension dimension) {
|
||||
if (sender() != null && sender().isPlayer() && IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
return player().getWorld();
|
||||
}
|
||||
World fallback = null;
|
||||
for (World w : Bukkit.getWorlds()) {
|
||||
if (!IrisToolbelt.isIrisWorld(w)) {
|
||||
continue;
|
||||
}
|
||||
if (fallback == null) {
|
||||
fallback = w;
|
||||
}
|
||||
PlatformChunkGenerator gen = IrisToolbelt.access(w);
|
||||
if (gen != null && gen.getEngine() != null && gen.getEngine().getDimension() != null
|
||||
&& dimension.getLoadKey().equals(gen.getEngine().getDimension().getLoadKey())) {
|
||||
return w;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
@Director(description = "Resolve an iris structure's jigsaw graph and report piece count & bounds", origin = DirectorOrigin.BOTH)
|
||||
public void info(
|
||||
@Param(description = "The dimension whose pack holds the structure", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "The iris structure key to inspect")
|
||||
String structure
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
IrisStructure s = IrisData.loadAnyStructure(structure, data);
|
||||
if (s == null) {
|
||||
sender().sendMessage(C.RED + "No iris structure '" + structure + "' in this pack");
|
||||
return;
|
||||
}
|
||||
StructureAssembler assembler = new StructureAssembler(data, s, 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() + "')");
|
||||
return;
|
||||
}
|
||||
int minX = Integer.MAX_VALUE;
|
||||
int minZ = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE;
|
||||
int maxZ = Integer.MIN_VALUE;
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
minX = Math.min(minX, p.getMinX());
|
||||
minZ = Math.min(minZ, p.getMinZ());
|
||||
maxX = Math.max(maxX, p.getMaxX());
|
||||
maxZ = Math.max(maxZ, p.getMaxZ());
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "Structure '" + structure + "': " + C.WHITE + pieces.size() + C.GREEN + " pieces, footprint " + C.WHITE + (maxX - minX + 1) + "x" + (maxZ - minZ + 1) + C.GREEN + " blocks (sample seed 1234)");
|
||||
}
|
||||
|
||||
@Director(description = "Assemble and place an iris structure at your location (studio testing)", aliases = {"p"}, origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void place(
|
||||
@Param(description = "The dimension whose pack holds the structure", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "The iris structure key to place")
|
||||
String structure
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
IrisStructure s = IrisData.loadAnyStructure(structure, data);
|
||||
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());
|
||||
RNG rng = new RNG((long) loc.getBlockX() * 341873128712L + loc.getBlockZ());
|
||||
KList<PlacedStructurePiece> pieces = assembler.assemble(rng);
|
||||
if (pieces == null || pieces.isEmpty()) {
|
||||
sender().sendMessage(C.RED + "Structure '" + structure + "' assembled 0 pieces");
|
||||
return;
|
||||
}
|
||||
Map<Block, BlockData> future = new HashMap<>();
|
||||
IObjectPlacer placer = CommandObject.createPlacer(player().getWorld(), future);
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
IrisObjectPlacement config = new IrisObjectPlacement();
|
||||
config.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
|
||||
config.setRotation(p.getRotation());
|
||||
config.getPlace().add(p.getObject().getLoadKey());
|
||||
if (!s.getEdit().isEmpty()) {
|
||||
config.setEdit(s.getEdit());
|
||||
}
|
||||
p.getObject().place(p.getX(), p.getY(), p.getZ(), placer, config, rng, null, null, data);
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "Placed '" + structure + "' (" + pieces.size() + " pieces) at your location.");
|
||||
}
|
||||
|
||||
}
|
||||
+880
@@ -0,0 +1,880 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.commands;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
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.StudioSVC;
|
||||
import art.arcane.iris.core.structure.BulkStructureImporter;
|
||||
import art.arcane.iris.core.structure.FeatureImporter;
|
||||
import art.arcane.iris.core.structure.StructureImporter;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.InventorySlotType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisEntity;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisInterpolator;
|
||||
import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.iris.engine.object.IrisNoiseGenerator;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.handlers.DimensionHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.function.Function2;
|
||||
import art.arcane.volmlib.util.function.NoiseProvider;
|
||||
import art.arcane.iris.util.project.interpolation.InterpolationMethod;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.math.Spiraler;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.scheduling.O;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.FluidCollisionMode;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands")
|
||||
public class CommandStudio implements DirectorExecutor {
|
||||
private CommandEdit edit;
|
||||
//private CommandDeepSearch deepSearch;
|
||||
|
||||
public static String hrf(Duration duration) {
|
||||
return duration.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase();
|
||||
}
|
||||
|
||||
@Director(description = "Open a new studio world", aliases = "o", sync = true)
|
||||
public void open(
|
||||
@Param(description = "The dimension pack to open a studio for", aliases = "dim", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(defaultValue = "1337", description = "The seed to generate the studio with", aliases = "s")
|
||||
long seed) {
|
||||
sender().sendMessage(C.GREEN + "Opening studio for the \"" + dimension.getName() + "\" pack (seed: " + seed + ")");
|
||||
Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey());
|
||||
}
|
||||
|
||||
@Director(description = "Import vanilla trees, mushrooms & objects (and structures/jigsaw) from the server into a pack's objects/vanilla folder", aliases = {"importv", "iv"}, origin = DirectorOrigin.BOTH)
|
||||
public void importvanilla(
|
||||
@Param(description = "The dimension pack to import vanilla content into", aliases = {"pack", "dim"}, customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(description = "How many variants to capture per tree/object feature", defaultValue = "3")
|
||||
int variants,
|
||||
@Param(description = "Also import vanilla & datapack structures/jigsaw into the pack", defaultValue = "true")
|
||||
boolean structures
|
||||
) {
|
||||
if (dimension == null) {
|
||||
sender().sendMessage(C.RED + "Provide a dimension pack: /iris std importvanilla pack=<dimension>");
|
||||
return;
|
||||
}
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
|
||||
VolmitSender sender = sender();
|
||||
sender.sendMessage(C.GREEN + "Importing vanilla content into " + C.WHITE + dimension.getLoadKey() + C.GREEN + "...");
|
||||
|
||||
FeatureImporter.Report features = FeatureImporter.importAllObjectFeatures(data, variants, sender);
|
||||
int imported = features.imported();
|
||||
int failed = features.failed();
|
||||
|
||||
if (structures) {
|
||||
BulkStructureImporter.Report jigsaws = BulkStructureImporter.importAllVanilla(data, StructureImporter.Mode.OVERWRITE, true, sender);
|
||||
BulkStructureImporter.Report templates = BulkStructureImporter.importAllTemplates(data, StructureImporter.Mode.OVERWRITE, sender);
|
||||
BulkStructureImporter.Report groups = BulkStructureImporter.importTemplateGroups(data, StructureImporter.Mode.OVERWRITE, sender);
|
||||
imported += jigsaws.imported() + templates.imported() + groups.imported();
|
||||
failed += jigsaws.failed() + templates.failed() + groups.failed();
|
||||
}
|
||||
|
||||
sender.sendMessage(C.GREEN + "importvanilla complete: " + C.WHITE + imported + C.GREEN + " objects/structures written, " + C.WHITE + failed + C.GREEN + " failed.");
|
||||
sender.sendMessage(C.GRAY + "Trees/objects are under objects/vanilla/...; reference them from biome object placements.");
|
||||
}
|
||||
|
||||
@Director(description = "Open VSCode for a dimension", aliases = {"vsc"})
|
||||
public void vscode(
|
||||
@Param(defaultValue = "default", description = "The dimension to open VSCode for", aliases = "dim", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
sender().sendMessage(C.GREEN + "Opening VSCode for the \"" + dimension.getName() + "\" pack");
|
||||
Iris.service(StudioSVC.class).openVSCode(sender(), dimension.getLoadKey());
|
||||
}
|
||||
|
||||
@Director(description = "Close an open studio project", aliases = {"x"}, sync = true)
|
||||
public void close() {
|
||||
VolmitSender commandSender = sender();
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
commandSender.sendMessage(C.RED + "No open studio projects.");
|
||||
return;
|
||||
}
|
||||
|
||||
commandSender.sendMessage(C.YELLOW + "Closing studio...");
|
||||
Iris.service(StudioSVC.class).close().whenComplete((result, throwable) -> J.s(() -> {
|
||||
if (throwable != null) {
|
||||
commandSender.sendMessage(C.RED + "Studio close failed: " + throwable.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (result != null && result.failureCause() != null) {
|
||||
commandSender.sendMessage(C.RED + "Studio close failed: " + result.failureCause().getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (result != null && result.startupCleanupQueued()) {
|
||||
commandSender.sendMessage(C.YELLOW + "Studio closed. Remaining world-family cleanup was queued for startup fallback.");
|
||||
return;
|
||||
}
|
||||
|
||||
commandSender.sendMessage(C.GREEN + "Studio closed.");
|
||||
}));
|
||||
}
|
||||
|
||||
@Director(description = "Create a new studio project", aliases = "+", sync = true)
|
||||
public void create(
|
||||
@Param(description = "The name of this new Iris Project.", defaultValue = "studio")
|
||||
String name,
|
||||
@Param(
|
||||
description = "Copy the contents of an existing project in your packs folder and use it as a template in this new project.",
|
||||
contextual = true,
|
||||
customHandler = NullableDimensionHandler.class
|
||||
)
|
||||
IrisDimension template) {
|
||||
String projectName = name;
|
||||
if (name.equals("studio")) {
|
||||
File workspace = Iris.service(StudioSVC.class).getWorkspaceFolder();
|
||||
int suffix = 2;
|
||||
while (new File(workspace, projectName).exists()) {
|
||||
projectName = "studio" + suffix++;
|
||||
}
|
||||
}
|
||||
|
||||
if (template != null) {
|
||||
Iris.service(StudioSVC.class).create(sender(), projectName, template.getLoadKey());
|
||||
} else {
|
||||
Iris.service(StudioSVC.class).create(sender(), projectName);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Get the version of a pack")
|
||||
public void version(
|
||||
@Param(defaultValue = "default", description = "The dimension get the version of", aliases = "dim", contextual = true, customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
sender().sendMessage(C.GREEN + "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion());
|
||||
}
|
||||
|
||||
@Director(description = "Open the noise explorer (External GUI)", aliases = {"nmap"})
|
||||
public void noise(
|
||||
@Param(description = "Optional pack generator to preview", defaultValue = "null", contextual = true)
|
||||
IrisGenerator generator,
|
||||
@Param(description = "The seed to preview the generator with", defaultValue = "12345")
|
||||
long seed
|
||||
) {
|
||||
if (noGUI()) return;
|
||||
sender().sendMessage(C.GREEN + "Opening Noise Explorer!");
|
||||
|
||||
if (generator == null) {
|
||||
NoiseExplorerGUI.launch();
|
||||
return;
|
||||
}
|
||||
|
||||
Supplier<Function2<Double, Double, Double>> supplier = () -> (x, z) -> generator.getHeight(x, z, new RNG(seed).nextParallelRNG(3245).lmax());
|
||||
NoiseExplorerGUI.launch(supplier, "Custom Generator");
|
||||
}
|
||||
|
||||
@Director(description = "Show loot if a chest were right here", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void loot(
|
||||
@Param(description = "Fast insertion of items in virtual inventory (may cause performance drop)", defaultValue = "false")
|
||||
boolean fast,
|
||||
@Param(description = "Whether or not to append to the inventory currently open (if false, clears opened inventory)", defaultValue = "true")
|
||||
boolean add
|
||||
) {
|
||||
if (noStudio()) return;
|
||||
|
||||
KList<IrisLootTable> tables = EngineBukkitOps.getLootTables(engine(), RNG.r, player().getLocation().getBlock());
|
||||
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);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cannot add items to virtual inventory because of: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
O<Integer> ta = new O<>();
|
||||
ta.set(-1);
|
||||
|
||||
var sender = sender();
|
||||
var player = player();
|
||||
var engine = engine();
|
||||
|
||||
ta.set(J.sr(() ->
|
||||
{
|
||||
if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) {
|
||||
J.csr(ta.get());
|
||||
sender.sendMessage(C.GREEN + "Opened inventory!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!add) {
|
||||
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);
|
||||
}, fast ? 5 : 35));
|
||||
|
||||
sender().sendMessage(C.GREEN + "Opening inventory now!");
|
||||
player().openInventory(inv);
|
||||
}
|
||||
|
||||
@Director(description = "Calculate the chance for each region to generate", origin = DirectorOrigin.PLAYER)
|
||||
public void regions(@Param(description = "The radius in chunks", defaultValue = "500") int radius) {
|
||||
var engine = engine();
|
||||
if (engine == null) {
|
||||
sender().sendMessage(C.RED + "Only works in an Iris world!");
|
||||
return;
|
||||
}
|
||||
var sender = sender();
|
||||
var player = player();
|
||||
Thread.ofVirtual()
|
||||
.start(() -> {
|
||||
int d = radius * 2;
|
||||
KMap<String, AtomicInteger> data = new KMap<>();
|
||||
engine.getDimension().getRegions().forEach(key -> data.put(key, new AtomicInteger(0)));
|
||||
var multiBurst = new MultiBurst("Region Sampler");
|
||||
var executor = multiBurst.burst(radius * radius);
|
||||
sender.sendMessage(C.GRAY + "Generating data...");
|
||||
var loc = player.getLocation();
|
||||
int totalTasks = d * d;
|
||||
AtomicInteger completedTasks = new AtomicInteger(0);
|
||||
int c = J.ar(() -> sender.sendProgress((double) completedTasks.get() / totalTasks, "Finding regions"), 0);
|
||||
new Spiraler(d, d, (x, z) -> executor.queue(() -> {
|
||||
var region = engine.getRegion((x << 4) + 8, (z << 4) + 8);
|
||||
data.computeIfAbsent(region.getLoadKey(), (k) -> new AtomicInteger(0))
|
||||
.incrementAndGet();
|
||||
completedTasks.incrementAndGet();
|
||||
})).setOffset(loc.getBlockX(), loc.getBlockZ()).drain();
|
||||
executor.complete();
|
||||
multiBurst.close();
|
||||
J.car(c);
|
||||
|
||||
sender.sendMessage(C.GREEN + "Done!");
|
||||
var loader = engine.getData().getRegionLoader();
|
||||
data.forEach((k, v) -> sender.sendMessage(C.GREEN + k + ": " + loader.load(k).getRarity() + " / " + Form.f((double) v.get() / totalTasks * 100, 2) + "%"));
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Render a world map (External GUI)", aliases = "render")
|
||||
public void map(
|
||||
@Param(name = "world", description = "The world to open the generator for", contextual = true)
|
||||
World world
|
||||
) {
|
||||
if (noGUI()) return;
|
||||
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(C.RED + "You need to be in or specify an Iris-generated world!");
|
||||
return;
|
||||
}
|
||||
|
||||
VisionGUI.launch(IrisToolbelt.access(world).getEngine());
|
||||
sender().sendMessage(C.GREEN + "Opening map!");
|
||||
}
|
||||
|
||||
@Director(description = "Package a dimension into a compressed format", aliases = "package")
|
||||
public void pkg(
|
||||
@Param(name = "dimension", description = "The dimension pack to compress", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(name = "obfuscate", description = "Whether or not to obfuscate the pack", defaultValue = "false")
|
||||
boolean obfuscate,
|
||||
@Param(name = "minify", description = "Whether or not to minify the pack", defaultValue = "true")
|
||||
boolean minify
|
||||
) {
|
||||
Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify);
|
||||
}
|
||||
|
||||
@Director(description = "Profiles the performance of a dimension", origin = DirectorOrigin.PLAYER)
|
||||
public void profile(
|
||||
@Param(description = "The dimension to profile", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
// Todo: Make this more accurate
|
||||
File pack = dimension.getLoadFile().getParentFile().getParentFile();
|
||||
File report = Iris.instance.getDataFile("profile.txt");
|
||||
IrisProject project = new IrisProject(pack);
|
||||
IrisData data = IrisData.get(pack);
|
||||
PlatformChunkGenerator activeGenerator = resolveProfileGenerator(dimension);
|
||||
Engine activeEngine = activeGenerator == null ? null : activeGenerator.getEngine();
|
||||
|
||||
if (activeEngine != null) {
|
||||
IrisToolbelt.applyPregenPerformanceProfile(activeEngine);
|
||||
} else {
|
||||
IrisToolbelt.applyPregenPerformanceProfile();
|
||||
}
|
||||
|
||||
KList<String> fileText = new KList<>();
|
||||
|
||||
KMap<NoiseStyle, Double> styleTimings = new KMap<>();
|
||||
KMap<InterpolationMethod, Double> interpolatorTimings = new KMap<>();
|
||||
KMap<String, Double> generatorTimings = new KMap<>();
|
||||
KMap<String, Double> biomeTimings = new KMap<>();
|
||||
KMap<String, Double> regionTimings = new KMap<>();
|
||||
|
||||
sender().sendMessage("Calculating Performance Metrics for Noise generators");
|
||||
|
||||
for (NoiseStyle i : NoiseStyle.values()) {
|
||||
CNG c = i.create(new RNG(i.hashCode()));
|
||||
|
||||
for (int j = 0; j < 3000; j++) {
|
||||
c.noise(j, j + 1000, j * j);
|
||||
c.noise(j, -j);
|
||||
}
|
||||
|
||||
PrecisionStopwatch px = PrecisionStopwatch.start();
|
||||
|
||||
for (int j = 0; j < 100000; j++) {
|
||||
c.noise(j, j + 1000, j * j);
|
||||
c.noise(j, -j);
|
||||
}
|
||||
|
||||
styleTimings.put(i, px.getMilliseconds());
|
||||
}
|
||||
|
||||
fileText.add("Noise Style Performance Impacts: ");
|
||||
|
||||
for (NoiseStyle i : styleTimings.sortKNumber()) {
|
||||
fileText.add(i.name() + ": " + styleTimings.get(i));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
sender().sendMessage("Calculating Interpolator Timings...");
|
||||
|
||||
for (InterpolationMethod i : InterpolationMethod.values()) {
|
||||
IrisInterpolator in = new IrisInterpolator();
|
||||
in.setFunction(i);
|
||||
in.setHorizontalScale(8);
|
||||
|
||||
NoiseProvider np = (x, z) -> Math.random();
|
||||
|
||||
for (int j = 0; j < 3000; j++) {
|
||||
in.interpolate(j, -j, np);
|
||||
}
|
||||
|
||||
PrecisionStopwatch px = PrecisionStopwatch.start();
|
||||
|
||||
for (int j = 0; j < 100000; j++) {
|
||||
in.interpolate(j + 10000, -j - 100000, np);
|
||||
}
|
||||
|
||||
interpolatorTimings.put(i, px.getMilliseconds());
|
||||
}
|
||||
|
||||
fileText.add("Noise Interpolator Performance Impacts: ");
|
||||
|
||||
for (InterpolationMethod i : interpolatorTimings.sortKNumber()) {
|
||||
fileText.add(i.name() + ": " + interpolatorTimings.get(i));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
sender().sendMessage("Processing Generator Scores: ");
|
||||
|
||||
KMap<String, KList<String>> btx = new KMap<>();
|
||||
|
||||
for (String i : data.getGeneratorLoader().getPossibleKeys()) {
|
||||
KList<String> vv = new KList<>();
|
||||
IrisGenerator g = data.getGeneratorLoader().load(i);
|
||||
KList<IrisNoiseGenerator> composites = g.getAllComposites();
|
||||
double score = 0;
|
||||
int m = 0;
|
||||
for (IrisNoiseGenerator j : composites) {
|
||||
m++;
|
||||
score += styleTimings.get(j.getStyle().getStyle());
|
||||
vv.add("Composite Noise Style " + m + " " + j.getStyle().getStyle().name() + ": " + styleTimings.get(j.getStyle().getStyle()));
|
||||
}
|
||||
|
||||
score += interpolatorTimings.get(g.getInterpolator().getFunction());
|
||||
vv.add("Interpolator " + g.getInterpolator().getFunction().name() + ": " + interpolatorTimings.get(g.getInterpolator().getFunction()));
|
||||
generatorTimings.put(i, score);
|
||||
btx.put(i, vv);
|
||||
}
|
||||
|
||||
fileText.add("Project Generator Performance Impacts: ");
|
||||
|
||||
for (String i : generatorTimings.sortKNumber()) {
|
||||
fileText.add(i + ": " + generatorTimings.get(i));
|
||||
|
||||
btx.get(i).forEach((ii) -> fileText.add(" " + ii));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
KMap<String, KList<String>> bt = new KMap<>();
|
||||
|
||||
for (String i : data.getBiomeLoader().getPossibleKeys()) {
|
||||
KList<String> vv = new KList<>();
|
||||
IrisBiome b = data.getBiomeLoader().load(i);
|
||||
double score = 0;
|
||||
|
||||
int m = 0;
|
||||
for (IrisBiomePaletteLayer j : b.getLayers()) {
|
||||
m++;
|
||||
score += styleTimings.get(j.getStyle().getStyle());
|
||||
vv.add("Palette Layer " + m + ": " + styleTimings.get(j.getStyle().getStyle()));
|
||||
}
|
||||
|
||||
score += styleTimings.get(b.getBiomeStyle().getStyle());
|
||||
vv.add("Biome Style: " + styleTimings.get(b.getBiomeStyle().getStyle()));
|
||||
score += styleTimings.get(b.getChildStyle().getStyle());
|
||||
vv.add("Child Style: " + styleTimings.get(b.getChildStyle().getStyle()));
|
||||
biomeTimings.put(i, score);
|
||||
bt.put(i, vv);
|
||||
}
|
||||
|
||||
fileText.add("Project Biome Performance Impacts: ");
|
||||
|
||||
for (String i : biomeTimings.sortKNumber()) {
|
||||
fileText.add(i + ": " + biomeTimings.get(i));
|
||||
|
||||
bt.get(i).forEach((ff) -> fileText.add(" " + ff));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
for (String i : data.getRegionLoader().getPossibleKeys()) {
|
||||
IrisRegion b = data.getRegionLoader().load(i);
|
||||
double score = 0;
|
||||
|
||||
score += styleTimings.get(b.getLakeStyle().getStyle());
|
||||
score += styleTimings.get(b.getRiverStyle().getStyle());
|
||||
regionTimings.put(i, score);
|
||||
}
|
||||
|
||||
fileText.add("Project Region Performance Impacts: ");
|
||||
|
||||
for (String i : regionTimings.sortKNumber()) {
|
||||
fileText.add(i + ": " + regionTimings.get(i));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
double m = 0;
|
||||
for (double i : biomeTimings.v()) {
|
||||
m += i;
|
||||
}
|
||||
m /= biomeTimings.size();
|
||||
double mm = 0;
|
||||
for (double i : generatorTimings.v()) {
|
||||
mm += i;
|
||||
}
|
||||
mm /= generatorTimings.size();
|
||||
m += mm;
|
||||
double mmm = 0;
|
||||
for (double i : regionTimings.v()) {
|
||||
mmm += i;
|
||||
}
|
||||
mmm /= regionTimings.size();
|
||||
m += mmm;
|
||||
|
||||
fileText.add("Average Score: " + m);
|
||||
sender().sendMessage("Score: " + Form.duration(m, 0));
|
||||
|
||||
try {
|
||||
IO.writeAll(report, fileText.toString("\n"));
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Done! " + report.getPath());
|
||||
}
|
||||
|
||||
private PlatformChunkGenerator resolveProfileGenerator(IrisDimension dimension) {
|
||||
StudioSVC studioService = Iris.service(StudioSVC.class);
|
||||
if (studioService != null && studioService.isProjectOpen()) {
|
||||
IrisProject activeProject = studioService.getActiveProject();
|
||||
if (activeProject != null) {
|
||||
PlatformChunkGenerator activeProvider = activeProject.getActiveProvider();
|
||||
if (isGeneratorDimension(activeProvider, dimension)) {
|
||||
return activeProvider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sender().isPlayer()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Player player = sender().player();
|
||||
if (player == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PlatformChunkGenerator worldAccess = IrisToolbelt.access(player.getWorld());
|
||||
if (isGeneratorDimension(worldAccess, dimension)) {
|
||||
return worldAccess;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isGeneratorDimension(PlatformChunkGenerator generator, IrisDimension dimension) {
|
||||
if (generator == null || generator.getEngine() == null || dimension == null || dimension.getLoadKey() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
IrisDimension engineDimension = generator.getEngine().getDimension();
|
||||
if (engineDimension == null || engineDimension.getLoadKey() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return engineDimension.getLoadKey().equalsIgnoreCase(dimension.getLoadKey());
|
||||
}
|
||||
|
||||
@Director(description = "Spawn an Iris entity", aliases = "summon", origin = DirectorOrigin.PLAYER)
|
||||
public void spawn(
|
||||
@Param(description = "The entity to spawn")
|
||||
IrisEntity entity,
|
||||
@Param(description = "The location to spawn the entity at", contextual = true)
|
||||
Vector location
|
||||
) {
|
||||
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
sender().sendMessage(C.RED + "You have to be in an Iris world to spawn entities properly. Trying to spawn the best we can do.");
|
||||
}
|
||||
entity.spawn(engine(), new Location(world(), location.getX(), location.getY(), location.getZ()));
|
||||
}
|
||||
|
||||
@Director(description = "Teleport to the active studio world", aliases = "stp", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void tpstudio() {
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
sender().sendMessage(C.RED + "No studio world is open!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IrisToolbelt.isIrisWorld(world()) && engine().isStudio()) {
|
||||
sender().sendMessage(C.RED + "You are already in a studio world!");
|
||||
return;
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Sending you to the studio world!");
|
||||
var player = player();
|
||||
BukkitPlatform.teleportAsync(player(), Iris.service(StudioSVC.class)
|
||||
.getActiveProject()
|
||||
.getActiveProvider()
|
||||
.getTarget()
|
||||
.getWorld()
|
||||
.spawnLocation()
|
||||
).thenRun(() -> player.setGameMode(GameMode.SPECTATOR));
|
||||
}
|
||||
|
||||
@Director(description = "Update your dimension projects VSCode workspace")
|
||||
public void update(
|
||||
@Param(description = "The dimension to update the workspace of", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
sender().sendMessage(C.GOLD + "Updating Code Workspace for " + dimension.getName() + "...");
|
||||
if (new IrisProject(dimension.getLoader().getDataFolder()).updateWorkspace()) {
|
||||
sender().sendMessage(C.GREEN + "Updated Code Workspace for " + dimension.getName());
|
||||
} else {
|
||||
sender().sendMessage(C.RED + "Invalid project: " + dimension.getName() + ". Try deleting the code-workspace file and try again.");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(aliases = "find-objects", description = "Capture an IGenData chunk report for nearby chunks")
|
||||
public void objects() {
|
||||
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
sender().sendMessage(C.RED + "You must be in an Iris world");
|
||||
return;
|
||||
}
|
||||
|
||||
World world = player().getWorld();
|
||||
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage("You must be in an iris world.");
|
||||
return;
|
||||
}
|
||||
KList<Chunk> chunks = new KList<>();
|
||||
int bx = player().getLocation().getChunk().getX();
|
||||
int bz = player().getLocation().getChunk().getZ();
|
||||
|
||||
try {
|
||||
Location l = player().getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation();
|
||||
|
||||
int cx = l.getChunk().getX();
|
||||
int cz = l.getChunk().getZ();
|
||||
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + cx, z + cz))).drain();
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + bx, z + bz))).drain();
|
||||
sender().sendMessage("Capturing IGenData from " + chunks.size() + " nearby chunks.");
|
||||
try {
|
||||
File ff = Iris.instance.getDataFile("reports/" + M.ms() + ".txt");
|
||||
PrintWriter pw = new PrintWriter(ff);
|
||||
pw.println("=== Iris Chunk Report ===");
|
||||
pw.println("== General Info ==");
|
||||
pw.println("Iris Version: " + Iris.instance.getDescription().getVersion());
|
||||
pw.println("Bukkit Version: " + Bukkit.getBukkitVersion());
|
||||
pw.println("MC Version: " + Bukkit.getVersion());
|
||||
pw.println("PaperSpigot: " + (BukkitPlatform.isPaperServer() ? "Yup!" : "Nope!"));
|
||||
pw.println("Report Captured At: " + new Date());
|
||||
pw.println("Chunks: (" + chunks.size() + "): ");
|
||||
|
||||
for (Chunk i : chunks) {
|
||||
pw.println("- [" + i.getX() + ", " + i.getZ() + "]");
|
||||
}
|
||||
|
||||
int regions = 0;
|
||||
long size = 0;
|
||||
String age = "No idea...";
|
||||
|
||||
try {
|
||||
for (File i : Objects.requireNonNull(new File(world.getWorldFolder(), "region").listFiles())) {
|
||||
if (i.isFile()) {
|
||||
size += i.length();
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
FileTime creationTime = (FileTime) Files.getAttribute(world.getWorldFolder().toPath(), "creationTime");
|
||||
age = hrf(Duration.of(M.ms() - creationTime.toMillis(), ChronoUnit.MILLIS));
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
KList<String> biomes = new KList<>();
|
||||
KList<String> caveBiomes = new KList<>();
|
||||
KMap<String, KMap<String, KList<String>>> objects = new KMap<>();
|
||||
|
||||
for (Chunk i : chunks) {
|
||||
for (int j = 0; j < 16; j += 3) {
|
||||
|
||||
for (int k = 0; k < 16; k += 3) {
|
||||
|
||||
assert engine() != null;
|
||||
IrisBiome bb = engine().getSurfaceBiome((i.getX() * 16) + j, (i.getZ() * 16) + k);
|
||||
IrisBiome bxf = engine().getCaveBiome((i.getX() * 16) + j, (i.getZ() * 16) + k);
|
||||
biomes.addIfMissing(bb.getName() + " [" + Form.capitalize(bb.getInferredType().name().toLowerCase()) + "] " + " (" + bb.getLoadFile().getName() + ")");
|
||||
caveBiomes.addIfMissing(bxf.getName() + " (" + bxf.getLoadFile().getName() + ")");
|
||||
exportObjects(bb, pw, engine(), objects);
|
||||
exportObjects(bxf, pw, engine(), objects);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
regions = Objects.requireNonNull(new File(world.getWorldFolder().getPath() + "/region").list()).length;
|
||||
|
||||
pw.println();
|
||||
pw.println("== World Info ==");
|
||||
pw.println("World Name: " + world.getName());
|
||||
pw.println("Age: " + age);
|
||||
pw.println("Folder: " + world.getWorldFolder().getPath());
|
||||
pw.println("Regions: " + Form.f(regions));
|
||||
pw.println("Chunks: max. " + Form.f(regions * 32 * 32));
|
||||
pw.println("World Size: min. " + Form.fileSize(size));
|
||||
pw.println();
|
||||
pw.println("== Biome Info ==");
|
||||
pw.println("Found " + biomes.size() + " Biome(s): ");
|
||||
|
||||
for (String i : biomes) {
|
||||
pw.println("- " + i);
|
||||
}
|
||||
pw.println();
|
||||
|
||||
pw.println("== Object Info ==");
|
||||
|
||||
for (String i : objects.k()) {
|
||||
pw.println("- " + i);
|
||||
|
||||
for (String j : objects.get(i).k()) {
|
||||
pw.println(" @ " + j);
|
||||
|
||||
for (String k : objects.get(i).get(j)) {
|
||||
pw.println(" * " + k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pw.println();
|
||||
pw.close();
|
||||
|
||||
sender().sendMessage("Reported to: " + ff.getPath());
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportObjects(IrisBiome bb, PrintWriter pw, Engine g, KMap<String, KMap<String, KList<String>>> objects) {
|
||||
String n1 = bb.getName() + " [" + Form.capitalize(bb.getInferredType().name().toLowerCase()) + "] " + " (" + bb.getLoadFile().getName() + ")";
|
||||
int m = 0;
|
||||
KSet<String> stop = new KSet<>();
|
||||
for (IrisObjectPlacement f : bb.getObjects()) {
|
||||
m++;
|
||||
String n2 = "Placement #" + m + " (" + f.getPlace().size() + " possible objects)";
|
||||
|
||||
for (String i : f.getPlace()) {
|
||||
String nn3 = i + ": [ERROR] Failed to find object!";
|
||||
|
||||
try {
|
||||
if (stop.contains(i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
File ff = g.getData().getObjectLoader().findFile(i);
|
||||
art.arcane.iris.util.common.math.IrisBlockVector sz = IrisObject.sampleSize(ff);
|
||||
nn3 = i + ": size=[" + sz.getBlockX() + "," + sz.getBlockY() + "," + sz.getBlockZ() + "] location=[" + ff.getPath() + "]";
|
||||
stop.add(i);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
String n3 = nn3;
|
||||
objects.computeIfAbsent(n1, (k1) -> new KMap<>())
|
||||
.computeIfAbsent(n2, (k) -> new KList<>()).addIfMissing(n3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if server GUIs are not enabled
|
||||
*/
|
||||
private boolean noGUI() {
|
||||
if (!IrisSettings.get().getGui().isUseServerLaunchedGuis()) {
|
||||
sender().sendMessage(C.RED + "You must have server launched GUIs enabled in the settings!");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if no studio is open or the player is not in one
|
||||
*/
|
||||
private boolean noStudio() {
|
||||
if (!sender().isPlayer()) {
|
||||
sender().sendMessage(C.RED + "Players only!");
|
||||
return true;
|
||||
}
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
sender().sendMessage(C.RED + "No studio world is open!");
|
||||
return true;
|
||||
}
|
||||
if (!engine().isStudio()) {
|
||||
sender().sendMessage(C.RED + "You must be in a studio world!");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public void files(File clean, KList<File> files) {
|
||||
if (clean.isDirectory()) {
|
||||
for (File i : clean.listFiles()) {
|
||||
files(i, files);
|
||||
}
|
||||
} else if (clean.getName().endsWith(".json")) {
|
||||
try {
|
||||
files.add(clean);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Iris.error("Failed to beautify " + clean.getAbsolutePath() + " You may have errors in your json!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fixBlocks(JSONObject obj) {
|
||||
for (String i : obj.keySet()) {
|
||||
Object o = obj.get(i);
|
||||
|
||||
if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) {
|
||||
obj.put(i, "minecraft:" + o);
|
||||
}
|
||||
|
||||
if (o instanceof JSONObject) {
|
||||
fixBlocks((JSONObject) o);
|
||||
} else if (o instanceof JSONArray) {
|
||||
fixBlocks((JSONArray) o);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fixBlocks(JSONArray obj) {
|
||||
for (int i = 0; i < obj.length(); i++) {
|
||||
Object o = obj.get(i);
|
||||
|
||||
if (o instanceof JSONObject) {
|
||||
fixBlocks((JSONObject) o);
|
||||
} else if (o instanceof JSONArray) {
|
||||
fixBlocks((JSONArray) o);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.commands;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.edit.BlockSignal;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.FluidCollisionMode;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Director(name = "what", origin = DirectorOrigin.PLAYER, description = "Iris What?")
|
||||
public class CommandWhat implements DirectorExecutor {
|
||||
@Director(description = "What is in my hand?", origin = DirectorOrigin.PLAYER)
|
||||
public void hand() {
|
||||
try {
|
||||
BlockData bd = player().getInventory().getItemInMainHand().getType().createBlockData();
|
||||
if (!bd.getMaterial().equals(Material.AIR)) {
|
||||
sender().sendMessage("Material: " + C.GREEN + bd.getMaterial().name());
|
||||
sender().sendMessage("Full: " + C.WHITE + bd.getAsString(true));
|
||||
} else {
|
||||
sender().sendMessage("Please hold a block/item");
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Material bd = player().getInventory().getItemInMainHand().getType();
|
||||
if (!bd.equals(Material.AIR)) {
|
||||
sender().sendMessage("Material: " + C.GREEN + bd.name());
|
||||
} else {
|
||||
sender().sendMessage("Please hold a block/item");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "What biome am i in?", origin = DirectorOrigin.PLAYER)
|
||||
public void biome() {
|
||||
try {
|
||||
IrisBiome b = engine().getBiome(player().getLocation().getBlockX(), player().getLocation().getBlockY() - player().getWorld().getMinHeight(), player().getLocation().getBlockZ());
|
||||
Biome derivative = b.getDerivative();
|
||||
NamespacedKey derivativeKey = resolveBiomeKey(derivative);
|
||||
sender().sendMessage("IBiome: " + b.getLoadKey() + " (" + (derivativeKey == null ? "unregistered" : derivativeKey.getKey()) + ")");
|
||||
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Biome biome = player().getLocation().getBlock().getBiome();
|
||||
NamespacedKey key = resolveBiomeKey(biome);
|
||||
sender().sendMessage("Non-Iris Biome: " + (key == null ? "unregistered" : key));
|
||||
|
||||
if (key == null || key.getKey().equals("custom")) {
|
||||
try {
|
||||
sender().sendMessage("Data Pack Biome: " + INMS.get().getTrueBiomeBaseKey(player().getLocation()) + " (ID: " + INMS.get().getTrueBiomeBaseId(INMS.get().getTrueBiomeBase(player().getLocation())) + ")");
|
||||
} catch (Throwable ee) {
|
||||
Iris.reportError(ee);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "What region am i in?", origin = DirectorOrigin.PLAYER)
|
||||
public void region() {
|
||||
try {
|
||||
Chunk chunk = world().getChunkAt(player().getLocation().getBlockX() >> 4, player().getLocation().getBlockZ() >> 4);
|
||||
IrisRegion r = EngineBukkitOps.getRegion(engine(), chunk);
|
||||
sender().sendMessage("IRegion: " + r.getLoadKey() + " (" + r.getName() + ")");
|
||||
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.IRIS + "Iris worlds only.");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "What block am i looking at?", origin = DirectorOrigin.PLAYER)
|
||||
public void block() {
|
||||
BlockData bd;
|
||||
try {
|
||||
bd = player().getTargetBlockExact(128, FluidCollisionMode.NEVER).getBlockData();
|
||||
} catch (NullPointerException e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage("Please look at any block, not at the sky");
|
||||
bd = null;
|
||||
}
|
||||
|
||||
if (bd != null) {
|
||||
sender().sendMessage("Material: " + C.GREEN + bd.getMaterial().name());
|
||||
sender().sendMessage("Full: " + C.WHITE + bd.getAsString(true));
|
||||
|
||||
if (BukkitBlockResolution.isStorage(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Storage Block (Loot Capable)");
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isLit(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Lit Block (Light Capable)");
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isFoliage(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Foliage Block");
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isDecorant(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Decorant Block");
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isFluid(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Fluid Block");
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isFoliagePlantable(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Plantable Foliage Block");
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isSolid(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Solid Block");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Show markers in chunk", origin = DirectorOrigin.PLAYER)
|
||||
public void markers(@Param(description = "Marker name such as cave_floor or cave_ceiling") String marker) {
|
||||
Chunk c = player().getLocation().getChunk();
|
||||
|
||||
if (IrisToolbelt.isIrisWorld(c.getWorld())) {
|
||||
int m = 1;
|
||||
AtomicInteger v = new AtomicInteger(0);
|
||||
|
||||
for (int xxx = c.getX() - 4; xxx <= c.getX() + 4; xxx++) {
|
||||
for (int zzz = c.getZ() - 4; zzz <= c.getZ() + 4; zzz++) {
|
||||
IrisToolbelt.access(c.getWorld()).getEngine().getMantle().findMarkers(xxx, zzz, new MatterMarker(marker))
|
||||
.convert((i) -> BukkitPlatform.toLocation(i, c.getWorld())).forEach((i) -> {
|
||||
BlockSignal.of(i.getWorld(), i.getBlockX(), i.getBlockY(), i.getBlockZ(), 100);
|
||||
v.incrementAndGet();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sender().sendMessage("Found " + v.get() + " Nearby Markers (" + marker + ")");
|
||||
} else {
|
||||
sender().sendMessage(C.IRIS + "Iris worlds only.");
|
||||
}
|
||||
}
|
||||
|
||||
private NamespacedKey resolveBiomeKey(Biome biome) {
|
||||
Object keyOrNullValue = invokeNoThrow(biome, "getKeyOrNull");
|
||||
if (keyOrNullValue instanceof NamespacedKey namespacedKey) {
|
||||
return namespacedKey;
|
||||
}
|
||||
|
||||
Object keyOrThrowValue = invokeNoThrow(biome, "getKeyOrThrow");
|
||||
if (keyOrThrowValue instanceof NamespacedKey namespacedKey) {
|
||||
return namespacedKey;
|
||||
}
|
||||
|
||||
Object keyValue = invokeNoThrow(biome, "getKey");
|
||||
if (keyValue instanceof NamespacedKey namespacedKey) {
|
||||
return namespacedKey;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Object invokeNoThrow(Biome biome, String methodName) {
|
||||
if (biome == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Method method = biome.getClass().getMethod(methodName);
|
||||
return method.invoke(biome);
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.edit;
|
||||
package art.arcane.iris.core.edit;
|
||||
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
+8
-6
@@ -16,9 +16,9 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.edit;
|
||||
package art.arcane.iris.core.edit;
|
||||
|
||||
import com.volmit.iris.util.math.M;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
@@ -51,10 +51,13 @@ public class BukkitBlockEditor implements BlockEditor {
|
||||
return M.ms();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void setBiome(int x, int z, Biome b) {
|
||||
world.setBiome(x, z, b);
|
||||
int minHeight = world.getMinHeight();
|
||||
int maxHeight = world.getMaxHeight();
|
||||
for (int y = minHeight; y < maxHeight; y++) {
|
||||
world.setBiome(x, y, z, b);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,9 +70,8 @@ public class BukkitBlockEditor implements BlockEditor {
|
||||
return world.getBiome(x, y, z);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public Biome getBiome(int x, int z) {
|
||||
return world.getBiome(x, z);
|
||||
return world.getBiome(x, world.getMinHeight(), z);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.gui;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class BukkitGuiHost implements GuiHost.Provider {
|
||||
private final Map<Runnable, Listener> hotloadHooks = new ConcurrentHashMap<>();
|
||||
|
||||
public static void install() {
|
||||
GuiHost.set(new BukkitGuiHost());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Engine findActiveEngine() {
|
||||
try {
|
||||
for (World world : new ArrayList<>(Bukkit.getWorlds())) {
|
||||
try {
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
|
||||
return access.getEngine();
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerHotloadHook(Runnable onHotload) {
|
||||
Listener listener = new HotloadListener(onHotload);
|
||||
hotloadHooks.put(onHotload, listener);
|
||||
Iris.instance.registerListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterHotloadHook(Runnable onHotload) {
|
||||
Listener listener = hotloadHooks.remove(onHotload);
|
||||
if (listener != null) {
|
||||
Iris.instance.unregisterListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiOverlay overlayFor(Engine engine) {
|
||||
return engine == null ? null : new BukkitVisionOverlay(engine);
|
||||
}
|
||||
|
||||
private static final class HotloadListener implements Listener {
|
||||
private final Runnable onHotload;
|
||||
|
||||
private HotloadListener(Runnable onHotload) {
|
||||
this.onHotload = onHotload;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(IrisEngineHotloadEvent event) {
|
||||
onHotload.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.gui;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.render.RenderType;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
|
||||
|
||||
public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
private final Engine engine;
|
||||
|
||||
public BukkitVisionOverlay(Engine engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GuiMarker> players() {
|
||||
IrisWorld world = engine.getWorld();
|
||||
List<GuiMarker> markers = new ArrayList<>();
|
||||
for (Player player : world.getPlayers()) {
|
||||
markers.add(GuiMarker.player(player.getName(), player.getLocation().getX(), player.getLocation().getZ()));
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestEntities(Consumer<List<GuiMarker>> sink) {
|
||||
J.s(() -> {
|
||||
IrisWorld world = engine.getWorld();
|
||||
List<GuiMarker> markers = new ArrayList<>();
|
||||
for (LivingEntity entity : world.getEntitiesByClass(LivingEntity.class)) {
|
||||
if (entity instanceof Player) {
|
||||
continue;
|
||||
}
|
||||
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
|
||||
double maxHealth = 0;
|
||||
try {
|
||||
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
markers.add(GuiMarker.entity(label, entity.getLocation().getX(), entity.getLocation().getY(), entity.getLocation().getZ(),
|
||||
entity.getHealth(), maxHealth));
|
||||
}
|
||||
sink.accept(markers);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teleport(double worldX, double worldZ) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (!world.hasRealWorld()) {
|
||||
return;
|
||||
}
|
||||
J.s(() -> {
|
||||
List<Player> players = world.getPlayers();
|
||||
if (players.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Player player = players.get(0);
|
||||
int xx = (int) worldX;
|
||||
int zz = (int) worldZ;
|
||||
int yy = player.getWorld().getHighestBlockYAt(xx, zz) + 1;
|
||||
player.teleport(new Location(player.getWorld(), xx, yy, zz));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String openInEditor(double worldX, double worldZ, RenderType type) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
File file = switch (type) {
|
||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
|
||||
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
|
||||
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
default -> null;
|
||||
};
|
||||
return file == null ? null : file.getName();
|
||||
}
|
||||
}
|
||||
+40
-38
@@ -16,36 +16,38 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.link;
|
||||
package art.arcane.iris.core.link;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||
import com.volmit.iris.engine.object.IrisBiome;
|
||||
import com.volmit.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
// See/update https://app.gitbook.com/@volmitsoftware/s/iris/compatability/papi/
|
||||
public class IrisPapiExpansion extends PlaceholderExpansion {
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
public @NotNull String getIdentifier() {
|
||||
return "iris";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthor() {
|
||||
public @NotNull String getAuthor() {
|
||||
return "Volmit Software";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
public @NotNull String getVersion() {
|
||||
return Iris.instance.getDescription().getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean persist() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -53,55 +55,55 @@ public class IrisPapiExpansion extends PlaceholderExpansion {
|
||||
Location l = null;
|
||||
PlatformChunkGenerator a = null;
|
||||
|
||||
if(player.isOnline()) {
|
||||
l = player.getPlayer().getLocation();
|
||||
if (player.isOnline() && player.getPlayer() != null) {
|
||||
l = player.getPlayer().getLocation().add(0, 2, 0);
|
||||
a = IrisToolbelt.access(l.getWorld());
|
||||
}
|
||||
|
||||
if(p.equalsIgnoreCase("biome_name")) {
|
||||
if(a != null) {
|
||||
if (p.equalsIgnoreCase("biome_name")) {
|
||||
if (a != null) {
|
||||
return getBiome(a, l).getName();
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("biome_id")) {
|
||||
if(a != null) {
|
||||
} else if (p.equalsIgnoreCase("biome_id")) {
|
||||
if (a != null) {
|
||||
return getBiome(a, l).getLoadKey();
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("biome_file")) {
|
||||
if(a != null) {
|
||||
} else if (p.equalsIgnoreCase("biome_file")) {
|
||||
if (a != null) {
|
||||
return getBiome(a, l).getLoadFile().getPath();
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("region_name")) {
|
||||
if(a != null) {
|
||||
return a.getEngine().getRegion(l).getName();
|
||||
} else if (p.equalsIgnoreCase("region_name")) {
|
||||
if (a != null) {
|
||||
return EngineBukkitOps.getRegion(a.getEngine(), l).getName();
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("region_id")) {
|
||||
if(a != null) {
|
||||
return a.getEngine().getRegion(l).getLoadKey();
|
||||
} else if (p.equalsIgnoreCase("region_id")) {
|
||||
if (a != null) {
|
||||
return EngineBukkitOps.getRegion(a.getEngine(), l).getLoadKey();
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("region_file")) {
|
||||
if(a != null) {
|
||||
return a.getEngine().getRegion(l).getLoadFile().getPath();
|
||||
} else if (p.equalsIgnoreCase("region_file")) {
|
||||
if (a != null) {
|
||||
return EngineBukkitOps.getRegion(a.getEngine(), l).getLoadFile().getPath();
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("terrain_slope")) {
|
||||
if(a != null) {
|
||||
} else if (p.equalsIgnoreCase("terrain_slope")) {
|
||||
if (a != null) {
|
||||
return (a.getEngine())
|
||||
.getComplex().getSlopeStream()
|
||||
.get(l.getX(), l.getZ()) + "";
|
||||
.getComplex().getSlopeStream()
|
||||
.get(l.getX(), l.getZ()) + "";
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("terrain_height")) {
|
||||
if(a != null) {
|
||||
} else if (p.equalsIgnoreCase("terrain_height")) {
|
||||
if (a != null) {
|
||||
return Math.round(a.getEngine().getHeight(l.getBlockX(), l.getBlockZ())) + "";
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("world_mode")) {
|
||||
if(a != null) {
|
||||
} else if (p.equalsIgnoreCase("world_mode")) {
|
||||
if (a != null) {
|
||||
return a.isStudio() ? "Studio" : "Production";
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("world_seed")) {
|
||||
if(a != null) {
|
||||
} else if (p.equalsIgnoreCase("world_seed")) {
|
||||
if (a != null) {
|
||||
return a.getEngine().getSeedManager().getSeed() + "";
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("world_speed")) {
|
||||
if(a != null) {
|
||||
} else if (p.equalsIgnoreCase("world_speed")) {
|
||||
if (a != null) {
|
||||
return a.getEngine().getGeneratedPerSecond() + "/s";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.service;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.commands.CommandIris;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.util.common.director.DirectorContext;
|
||||
import art.arcane.iris.util.common.director.DirectorContextHandler;
|
||||
import art.arcane.iris.util.common.director.DirectorSystem;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.director.compat.DirectorEngineFactory;
|
||||
import art.arcane.volmlib.util.director.context.DirectorContextRegistry;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorExecutionMode;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorExecutionResult;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorInvocation;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorInvocationHook;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorRuntimeEngine;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorRuntimeNode;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorSender;
|
||||
import art.arcane.volmlib.util.director.visual.DirectorVisualCommand;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, DirectorInvocationHook {
|
||||
private static final String ROOT_COMMAND = "iris";
|
||||
private static final String ROOT_PERMISSION = "iris.all";
|
||||
|
||||
private final transient AtomicCache<DirectorRuntimeEngine> directorCache = new AtomicCache<>();
|
||||
private final transient AtomicCache<DirectorVisualCommand> helpCache = new AtomicCache<>();
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
PluginCommand command = findBukkitCommand();
|
||||
if (command != null) {
|
||||
command.setExecutor(this);
|
||||
command.setTabCompleter(this);
|
||||
} else {
|
||||
registerPaperCommand();
|
||||
}
|
||||
J.a(this::getDirector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
|
||||
}
|
||||
|
||||
public DirectorRuntimeEngine getDirector() {
|
||||
return directorCache.aquireNastyPrint(() -> DirectorEngineFactory.create(
|
||||
new CommandIris(),
|
||||
null,
|
||||
buildDirectorContexts(),
|
||||
this::dispatchDirector,
|
||||
this,
|
||||
DirectorSystem.handlers
|
||||
));
|
||||
}
|
||||
|
||||
private DirectorContextRegistry buildDirectorContexts() {
|
||||
DirectorContextRegistry contexts = new DirectorContextRegistry();
|
||||
|
||||
for (Map.Entry<Class<?>, DirectorContextHandler<?>> entry : DirectorContextHandler.contextHandlers.entrySet()) {
|
||||
registerContextHandler(contexts, entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
return contexts;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private void registerContextHandler(DirectorContextRegistry contexts, Class<?> type, DirectorContextHandler<?> handler) {
|
||||
contexts.register((Class) type, (invocation, map) -> {
|
||||
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
|
||||
return ((DirectorContextHandler) handler).handle(new VolmitSender(sender.sender()));
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private void dispatchDirector(DirectorExecutionMode mode, Runnable runnable) {
|
||||
if (mode == DirectorExecutionMode.SYNC) {
|
||||
J.s(runnable);
|
||||
} else {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
|
||||
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
|
||||
DirectorContext.touch(new VolmitSender(sender.sender()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
|
||||
DirectorContext.remove();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
|
||||
if (!command.getName().equalsIgnoreCase(ROOT_COMMAND)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return tabCompleteRoot(sender, alias, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
if (!command.getName().equalsIgnoreCase(ROOT_COMMAND)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
executeRoot(sender, label, args);
|
||||
return true;
|
||||
}
|
||||
|
||||
void executeRoot(CommandSender sender, String label, String[] args) {
|
||||
if (!sender.hasPermission(ROOT_PERMISSION)) {
|
||||
sender.sendMessage("You lack the Permission '" + ROOT_PERMISSION + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
J.aBukkit(() -> executeCommand(sender, label, args));
|
||||
}
|
||||
|
||||
List<String> tabCompleteRoot(CommandSender sender, String alias, String[] args) {
|
||||
List<String> suggestions = runDirectorTab(sender, alias, args);
|
||||
if (sender instanceof Player player && IrisSettings.get().getGeneral().isCommandSounds()) {
|
||||
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.25f, RNG.r.f(0.125f, 1.95f));
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
private PluginCommand findBukkitCommand() {
|
||||
try {
|
||||
return Iris.instance.getCommand(ROOT_COMMAND);
|
||||
} catch (UnsupportedOperationException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void registerPaperCommand() {
|
||||
try {
|
||||
Class<?> registrarType = Class.forName(
|
||||
"art.arcane.iris.core.service.PaperCommandRegistrar",
|
||||
true,
|
||||
getClass().getClassLoader()
|
||||
);
|
||||
Method register = registrarType.getDeclaredMethod("register", Iris.class, CommandSVC.class);
|
||||
register.invoke(null, Iris.instance, this);
|
||||
} catch (InvocationTargetException e) {
|
||||
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||
throw new IllegalStateException("Paper command registration failed", cause);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException("Paper command registrar is unavailable", e);
|
||||
} catch (LinkageError e) {
|
||||
throw new IllegalStateException("Paper command APIs are unavailable", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void executeCommand(CommandSender sender, String label, String[] args) {
|
||||
if (sendHelpIfRequested(sender, args)) {
|
||||
playSuccessSound(sender);
|
||||
return;
|
||||
}
|
||||
|
||||
DirectorExecutionResult result = runDirector(sender, label, args);
|
||||
|
||||
if (result.isSuccess()) {
|
||||
playSuccessSound(sender);
|
||||
return;
|
||||
}
|
||||
|
||||
playFailureSound(sender);
|
||||
if (result.getMessage() == null || result.getMessage().trim().isEmpty()) {
|
||||
new VolmitSender(sender).sendMessage(C.RED + "Unknown Iris Command");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean sendHelpIfRequested(CommandSender sender, String[] args) {
|
||||
Optional<DirectorVisualCommand.HelpRequest> request = DirectorVisualCommand.resolveHelp(getHelpRoot(), Arrays.asList(args));
|
||||
if (request.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
VolmitSender volmitSender = new VolmitSender(sender);
|
||||
volmitSender.sendDirectorHelp(request.get().command(), request.get().page());
|
||||
return true;
|
||||
}
|
||||
|
||||
private DirectorVisualCommand getHelpRoot() {
|
||||
return helpCache.aquireNastyPrint(() -> DirectorVisualCommand.createRoot(getDirector()));
|
||||
}
|
||||
|
||||
private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) {
|
||||
try {
|
||||
return getDirector().execute(new DirectorInvocation(new BukkitDirectorSender(sender), label, Arrays.asList(args)));
|
||||
} catch (Throwable e) {
|
||||
Iris.warn("Director command execution failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
|
||||
return DirectorExecutionResult.notHandled();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> runDirectorTab(CommandSender sender, String alias, String[] args) {
|
||||
DirectorContext.touch(new VolmitSender(sender));
|
||||
try {
|
||||
return getDirector().tabComplete(new DirectorInvocation(new BukkitDirectorSender(sender), alias, Arrays.asList(args)));
|
||||
} catch (Throwable e) {
|
||||
Iris.warn("Director tab completion failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
|
||||
return List.of();
|
||||
} finally {
|
||||
DirectorContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private void playFailureSound(CommandSender sender) {
|
||||
if (!IrisSettings.get().getGeneral().isCommandSounds()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender instanceof Player player) {
|
||||
J.s(() -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 0.25f, Sound.BLOCK_BEACON_DEACTIVATE, 0.2f, 0.45f));
|
||||
}
|
||||
}
|
||||
|
||||
private void playSuccessSound(CommandSender sender) {
|
||||
if (!IrisSettings.get().getGeneral().isCommandSounds()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender instanceof Player player) {
|
||||
J.s(() -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 1.65f, Sound.BLOCK_RESPAWN_ANCHOR_CHARGE, 0.125f, 2.99f));
|
||||
}
|
||||
}
|
||||
|
||||
private static final java.util.concurrent.atomic.AtomicBoolean SOUND_LOGGED = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
|
||||
private void playSounds(Player player, Sound a, float av, float ap, Sound b, float bv, float bp) {
|
||||
try {
|
||||
player.playSound(player.getLocation(), a, av, ap);
|
||||
player.playSound(player.getLocation(), b, bv, bp);
|
||||
} catch (Throwable e) {
|
||||
if (SOUND_LOGGED.compareAndSet(false, true)) {
|
||||
try {
|
||||
java.io.File f = Iris.instance.getDataFile("adventure-debug.txt");
|
||||
java.nio.file.Files.writeString(f.toPath(), "SOUND FAIL: " + e.getClass().getName() + ": " + e.getMessage() + "\n", java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record BukkitDirectorSender(CommandSender sender) implements DirectorSender {
|
||||
@Override
|
||||
public String getName() {
|
||||
return sender.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlayer() {
|
||||
return sender instanceof Player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(String message) {
|
||||
if (message != null && !message.trim().isEmpty()) {
|
||||
sender.sendMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
-15
@@ -16,32 +16,42 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.service;
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.edit.BlockEditor;
|
||||
import com.volmit.iris.core.edit.BukkitBlockEditor;
|
||||
import com.volmit.iris.util.collection.KMap;
|
||||
import com.volmit.iris.util.math.M;
|
||||
import com.volmit.iris.util.plugin.IrisService;
|
||||
import org.bukkit.Bukkit;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.edit.BlockEditor;
|
||||
import art.arcane.iris.core.edit.BukkitBlockEditor;
|
||||
import art.arcane.iris.engine.framework.BlockEditAccess;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.world.WorldUnloadEvent;
|
||||
|
||||
public class EditSVC implements IrisService {
|
||||
public class EditSVC implements IrisService, BlockEditAccess<World, BlockData, Biome> {
|
||||
private KMap<World, BlockEditor> editors;
|
||||
private int updateTaskId = -1;
|
||||
public static boolean deletingWorld = false;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
this.editors = new KMap<>();
|
||||
Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, this::update, 1000, 1000);
|
||||
updateTaskId = J.sr(this::update, 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (updateTaskId != -1) {
|
||||
J.csr(updateTaskId);
|
||||
updateTaskId = -1;
|
||||
}
|
||||
if (editors == null) {
|
||||
return;
|
||||
}
|
||||
flushNow();
|
||||
}
|
||||
|
||||
@@ -71,27 +81,40 @@ public class EditSVC implements IrisService {
|
||||
|
||||
@EventHandler
|
||||
public void on(WorldUnloadEvent e) {
|
||||
if(editors.containsKey(e.getWorld())) {
|
||||
if (editors == null) {
|
||||
return;
|
||||
}
|
||||
if (editors.containsKey(e.getWorld()) && !deletingWorld) {
|
||||
editors.remove(e.getWorld()).close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void update() {
|
||||
for(World i : editors.k()) {
|
||||
if(M.ms() - editors.get(i).last() > 1000) {
|
||||
if (editors == null) {
|
||||
return;
|
||||
}
|
||||
for (World i : editors.k()) {
|
||||
if (M.ms() - editors.get(i).last() > 1000) {
|
||||
editors.remove(i).close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void flushNow() {
|
||||
for(World i : editors.k()) {
|
||||
if (editors == null) {
|
||||
return;
|
||||
}
|
||||
for (World i : editors.k()) {
|
||||
editors.remove(i).close();
|
||||
}
|
||||
}
|
||||
|
||||
public BlockEditor open(World world) {
|
||||
if(editors.containsKey(world)) {
|
||||
if (editors == null) {
|
||||
editors = new KMap<>();
|
||||
}
|
||||
if (editors.containsKey(world)) {
|
||||
return editors.get(world);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import com.google.common.util.concurrent.AtomicDouble;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.core.runtime.GoldenHashScanner;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.scheduling.Looper;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
|
||||
import art.arcane.iris.util.project.stream.utility.CachedStream3D;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.pregenerator.MantleHeapPressure;
|
||||
import lombok.Synchronized;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.world.WorldLoadEvent;
|
||||
import org.bukkit.event.world.WorldUnloadEvent;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class IrisEngineSVC implements IrisService {
|
||||
private static final int TRIM_PERIOD = 2_000;
|
||||
private final AtomicInteger tectonicLimit = new AtomicInteger(30);
|
||||
private final AtomicInteger tectonicPlates = new AtomicInteger();
|
||||
private final AtomicInteger queuedTectonicPlates = new AtomicInteger();
|
||||
private final AtomicInteger trimmerAlive = new AtomicInteger();
|
||||
private final AtomicInteger unloaderAlive = new AtomicInteger();
|
||||
private final AtomicInteger totalWorlds = new AtomicInteger();
|
||||
private final AtomicDouble maxIdleDuration = new AtomicDouble();
|
||||
private final AtomicDouble minIdleDuration = new AtomicDouble();
|
||||
private final AtomicLong loadedChunks = new AtomicLong();
|
||||
private final KMap<World, Registered> worlds = new KMap<>();
|
||||
private ScheduledExecutorService service;
|
||||
private Looper updateTicker;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
var settings = IrisSettings.get().getPerformance();
|
||||
var engine = settings.getEngineSVC();
|
||||
service = Executors.newScheduledThreadPool(0,
|
||||
(engine.isUseVirtualThreads()
|
||||
? Thread.ofVirtual()
|
||||
: Thread.ofPlatform().priority(engine.getPriority()))
|
||||
.name("Iris EngineSVC-", 0)
|
||||
.factory());
|
||||
tectonicLimit.set(settings.getTectonicPlateSize());
|
||||
Bukkit.getWorlds().forEach(this::add);
|
||||
setup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
for (World world : worlds.keySet()) {
|
||||
PlatformChunkGenerator gen = IrisToolbelt.access(world);
|
||||
if (gen == null) continue;
|
||||
try {
|
||||
gen.close();
|
||||
} catch (Throwable t) {
|
||||
IrisLogging.reportError(t);
|
||||
}
|
||||
}
|
||||
if (service != null) {
|
||||
service.shutdown();
|
||||
}
|
||||
if (updateTicker != null) {
|
||||
updateTicker.interrupt();
|
||||
}
|
||||
worlds.keySet().forEach(this::remove);
|
||||
worlds.clear();
|
||||
}
|
||||
|
||||
public int getQueuedTectonicPlateCount() {
|
||||
return queuedTectonicPlates.get();
|
||||
}
|
||||
|
||||
public double getAverageIdleDuration() {
|
||||
double min = minIdleDuration.get();
|
||||
double max = maxIdleDuration.get();
|
||||
if (!Double.isFinite(min) || !Double.isFinite(max) || min < 0D || max < 0D) {
|
||||
return 0D;
|
||||
}
|
||||
|
||||
if (max < min) {
|
||||
return max;
|
||||
}
|
||||
|
||||
return (min + max) / 2D;
|
||||
}
|
||||
|
||||
public double getBiomeCacheUsageRatio() {
|
||||
PreservationSVC preservation = IrisServices.get(PreservationSVC.class);
|
||||
if (preservation == null) {
|
||||
return 0D;
|
||||
}
|
||||
|
||||
double total = 0D;
|
||||
int count = 0;
|
||||
for (var cache : preservation.getCaches()) {
|
||||
if (!(cache instanceof CachedStream2D<?>) && !(cache instanceof CachedDoubleStream2D)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double usage = cache.getUsage();
|
||||
if (!Double.isFinite(usage)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
total += Math.max(0D, Math.min(1D, usage));
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count <= 0) {
|
||||
return 0D;
|
||||
}
|
||||
|
||||
return total / count;
|
||||
}
|
||||
|
||||
public void engineStatus(VolmitSender sender) {
|
||||
long[] sizes = new long[4];
|
||||
long[] count = new long[4];
|
||||
|
||||
for (var cache : IrisServices.get(PreservationSVC.class).getCaches()) {
|
||||
var type = switch (cache) {
|
||||
case ResourceLoader<?> ignored -> 0;
|
||||
case CachedStream2D<?> ignored -> 1;
|
||||
case CachedDoubleStream2D ignored -> 1;
|
||||
case CachedStream3D<?> ignored -> 2;
|
||||
default -> 3;
|
||||
};
|
||||
|
||||
sizes[type] += cache.getSize();
|
||||
count[type]++;
|
||||
}
|
||||
|
||||
sender.sendMessage(C.DARK_PURPLE + "-------------------------");
|
||||
sender.sendMessage(C.DARK_PURPLE + "Status:");
|
||||
sender.sendMessage(C.DARK_PURPLE + "- Service: " + C.LIGHT_PURPLE + (service.isShutdown() ? "Shutdown" : "Running"));
|
||||
sender.sendMessage(C.DARK_PURPLE + "- Updater: " + C.LIGHT_PURPLE + (updateTicker.isAlive() ? "Running" : "Stopped"));
|
||||
sender.sendMessage(C.DARK_PURPLE + "- Period: " + C.LIGHT_PURPLE + Form.duration(TRIM_PERIOD));
|
||||
sender.sendMessage(C.DARK_PURPLE + "- Trimmers: " + C.LIGHT_PURPLE + trimmerAlive.get());
|
||||
sender.sendMessage(C.DARK_PURPLE + "- Unloaders: " + C.LIGHT_PURPLE + unloaderAlive.get());
|
||||
sender.sendMessage(C.DARK_PURPLE + "Tectonic Plates:");
|
||||
sender.sendMessage(C.DARK_PURPLE + "- Limit: " + C.LIGHT_PURPLE + tectonicLimit.get());
|
||||
sender.sendMessage(C.DARK_PURPLE + "- Total: " + C.LIGHT_PURPLE + tectonicPlates.get());
|
||||
sender.sendMessage(C.DARK_PURPLE + "- Queued: " + C.LIGHT_PURPLE + queuedTectonicPlates.get());
|
||||
sender.sendMessage(C.DARK_PURPLE + "- Max Idle Duration: " + C.LIGHT_PURPLE + Form.duration(maxIdleDuration.get(), 2));
|
||||
sender.sendMessage(C.DARK_PURPLE + "- Min Idle Duration: " + C.LIGHT_PURPLE + Form.duration(minIdleDuration.get(), 2));
|
||||
sender.sendMessage(C.DARK_PURPLE + "Caches:");
|
||||
sender.sendMessage(C.DARK_PURPLE + "- Resource: " + C.LIGHT_PURPLE + sizes[0] + " (" + count[0] + ")");
|
||||
sender.sendMessage(C.DARK_PURPLE + "- 2D Stream: " + C.LIGHT_PURPLE + sizes[1] + " (" + count[1] + ")");
|
||||
sender.sendMessage(C.DARK_PURPLE + "- 3D Stream: " + C.LIGHT_PURPLE + sizes[2] + " (" + count[2] + ")");
|
||||
sender.sendMessage(C.DARK_PURPLE + "- Other: " + C.LIGHT_PURPLE + sizes[3] + " (" + count[3] + ")");
|
||||
sender.sendMessage(C.DARK_PURPLE + "Other:");
|
||||
sender.sendMessage(C.DARK_PURPLE + "- Iris Worlds: " + C.LIGHT_PURPLE + totalWorlds.get());
|
||||
sender.sendMessage(C.DARK_PURPLE + "- Loaded Chunks: " + C.LIGHT_PURPLE + loadedChunks.get());
|
||||
sender.sendMessage(C.DARK_PURPLE + "-------------------------");
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onWorldUnload(WorldUnloadEvent event) {
|
||||
remove(event.getWorld());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onWorldLoad(WorldLoadEvent event) {
|
||||
add(event.getWorld());
|
||||
}
|
||||
|
||||
private void remove(World world) {
|
||||
var entry = worlds.remove(world);
|
||||
if (entry == null) return;
|
||||
entry.close();
|
||||
}
|
||||
|
||||
private void add(World world) {
|
||||
if (world == null || worlds.containsKey(world)) return;
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access == null) return;
|
||||
worlds.put(world, new Registered(world.getName(), access));
|
||||
}
|
||||
|
||||
private synchronized void setup() {
|
||||
if (updateTicker != null && updateTicker.isAlive())
|
||||
return;
|
||||
|
||||
updateTicker = new Looper() {
|
||||
@Override
|
||||
protected long loop() {
|
||||
try {
|
||||
for (World world : Bukkit.getWorlds()) {
|
||||
add(world);
|
||||
}
|
||||
|
||||
int queuedPlates = 0;
|
||||
int totalPlates = 0;
|
||||
long chunks = 0;
|
||||
int unloaders = 0;
|
||||
int trimmers = 0;
|
||||
int iris = 0;
|
||||
|
||||
double maxDuration = Long.MIN_VALUE;
|
||||
double minDuration = Long.MAX_VALUE;
|
||||
for (var entry : worlds.entrySet()) {
|
||||
var registered = entry.getValue();
|
||||
if (registered.closed) continue;
|
||||
|
||||
iris++;
|
||||
if (registered.unloaderAlive()) unloaders++;
|
||||
if (registered.trimmerAlive()) trimmers++;
|
||||
|
||||
var engine = registered.getEngine();
|
||||
if (engine == null) continue;
|
||||
|
||||
queuedPlates += engine.getMantle().getUnloadRegionCount();
|
||||
totalPlates += engine.getMantle().getLoadedRegionCount();
|
||||
chunks += entry.getKey().getLoadedChunks().length;
|
||||
|
||||
double duration = engine.getMantle().getAdjustedIdleDuration();
|
||||
if (duration > maxDuration) maxDuration = duration;
|
||||
if (duration < minDuration) minDuration = duration;
|
||||
}
|
||||
|
||||
trimmerAlive.set(trimmers);
|
||||
unloaderAlive.set(unloaders);
|
||||
tectonicPlates.set(totalPlates);
|
||||
queuedTectonicPlates.set(queuedPlates);
|
||||
maxIdleDuration.set(maxDuration);
|
||||
minIdleDuration.set(minDuration);
|
||||
loadedChunks.set(chunks);
|
||||
totalWorlds.set(iris);
|
||||
|
||||
worlds.values().forEach(Registered::update);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return 1000;
|
||||
}
|
||||
};
|
||||
updateTicker.start();
|
||||
}
|
||||
|
||||
private static boolean isMantleClosed(Throwable throwable) {
|
||||
Throwable current = throwable;
|
||||
while (current != null) {
|
||||
String message = current.getMessage();
|
||||
if (message != null && message.toLowerCase(Locale.ROOT).contains("mantle is closed")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
current = current.getCause();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean shouldSkipMantleReductionForMaintenance(boolean maintenanceActive, boolean pregeneratorTargetsWorld) {
|
||||
return maintenanceActive && !pregeneratorTargetsWorld;
|
||||
}
|
||||
|
||||
private final class Registered {
|
||||
private final String name;
|
||||
private final PlatformChunkGenerator access;
|
||||
private final int offset = RNG.r.nextInt(TRIM_PERIOD);
|
||||
private transient ScheduledFuture<?> trimmer;
|
||||
private transient ScheduledFuture<?> unloader;
|
||||
private transient boolean closed;
|
||||
|
||||
private Registered(String name, PlatformChunkGenerator access) {
|
||||
this.name = name;
|
||||
this.access = access;
|
||||
update();
|
||||
}
|
||||
|
||||
private boolean unloaderAlive() {
|
||||
return unloader != null && !unloader.isDone() && !unloader.isCancelled();
|
||||
}
|
||||
|
||||
private boolean trimmerAlive() {
|
||||
return trimmer != null && !trimmer.isDone() && !trimmer.isCancelled();
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private void update() {
|
||||
if (closed || service == null || service.isShutdown())
|
||||
return;
|
||||
|
||||
if (trimmer == null || trimmer.isDone() || trimmer.isCancelled()) {
|
||||
trimmer = service.scheduleAtFixedRate(() -> {
|
||||
Engine engine = getEngine();
|
||||
if (engine == null
|
||||
|| engine.isClosed()
|
||||
|| engine.getMantle().getMantle().isClosed()
|
||||
|| !shouldReduce(engine))
|
||||
return;
|
||||
World engineWorld = engine.getWorld().realWorld();
|
||||
if (shouldSkipForMaintenance(engineWorld)) {
|
||||
return;
|
||||
}
|
||||
if (GoldenHashScanner.isScanActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (pregenTargets(engine) && MantleHeapPressure.overHighWater()) {
|
||||
engine.getMantle().trim(0L, 0);
|
||||
} else {
|
||||
engine.getMantle().trim(activeIdleDuration(engine), activeTectonicLimit(engine));
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
if (isMantleClosed(e)) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.error("EngineSVC: Failed to trim for " + name);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}, offset, TRIM_PERIOD, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
if (unloader == null || unloader.isDone() || unloader.isCancelled()) {
|
||||
unloader = service.scheduleAtFixedRate(() -> {
|
||||
Engine engine = getEngine();
|
||||
if (engine == null
|
||||
|| engine.isClosed()
|
||||
|| engine.getMantle().getMantle().isClosed()
|
||||
|| !shouldReduce(engine))
|
||||
return;
|
||||
World engineWorld = engine.getWorld().realWorld();
|
||||
if (shouldSkipForMaintenance(engineWorld)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (GoldenHashScanner.isScanActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
long unloadStart = System.currentTimeMillis();
|
||||
boolean heapPressure = pregenTargets(engine) && MantleHeapPressure.overHighWater();
|
||||
int unloadLimit = (heapPressure || IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite) ? 0 : activeTectonicLimit(engine);
|
||||
int count = engine.getMantle().unloadTectonicPlate(unloadLimit);
|
||||
if (heapPressure && MantleHeapPressure.overPanicWater()) {
|
||||
MantleHeapPressure.requestPanicReclaim();
|
||||
}
|
||||
if (count > 0) {
|
||||
IrisLogging.debug(C.GOLD + "Unloaded " + C.YELLOW + count + " TectonicPlates in " + C.RED + Form.duration(System.currentTimeMillis() - unloadStart, 2));
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
if (isMantleClosed(e)) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.error("EngineSVC: Failed to unload for " + name);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}, offset + TRIM_PERIOD / 2, TRIM_PERIOD, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private int tectonicLimit() {
|
||||
return tectonicLimit.get() / Math.max(worlds.size(), 1);
|
||||
}
|
||||
|
||||
private boolean pregenTargets(Engine engine) {
|
||||
if (engine == null || engine.getWorld() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(engine.getWorld().identity());
|
||||
}
|
||||
|
||||
private int activeTectonicLimit(Engine engine) {
|
||||
int limit = tectonicLimit();
|
||||
if (!pregenTargets(engine)) {
|
||||
return limit;
|
||||
}
|
||||
|
||||
return Math.max(limit, IrisSettings.get().getPregen().getEffectiveResidentTectonicPlates(engine.getHeight()));
|
||||
}
|
||||
|
||||
private long activeIdleDuration(Engine engine) {
|
||||
return TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive());
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private void close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
|
||||
if (trimmer != null) {
|
||||
trimmer.cancel(false);
|
||||
trimmer = null;
|
||||
}
|
||||
|
||||
if (unloader != null) {
|
||||
unloader.cancel(false);
|
||||
unloader = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Engine getEngine() {
|
||||
if (closed) return null;
|
||||
return access.getEngine();
|
||||
}
|
||||
|
||||
private boolean shouldReduce(Engine engine) {
|
||||
if (!engine.isStudio() || IrisSettings.get().getPerformance().isTrimMantleInStudio()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return pregenTargets(engine);
|
||||
}
|
||||
|
||||
private boolean shouldSkipForMaintenance(@Nullable World world) {
|
||||
if (world == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean maintenanceActive = IrisToolbelt.isWorldMaintenanceActive(world);
|
||||
if (!maintenanceActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorld(world);
|
||||
return shouldSkipMantleReductionForMaintenance(maintenanceActive, pregeneratorTargetsWorld);
|
||||
}
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
|
||||
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
|
||||
import art.arcane.volmlib.integration.IntegrationHeartbeat;
|
||||
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
|
||||
import art.arcane.volmlib.integration.IntegrationMetricSample;
|
||||
import art.arcane.volmlib.integration.IntegrationMetricSchema;
|
||||
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
|
||||
import art.arcane.volmlib.integration.IntegrationProtocolVersion;
|
||||
import art.arcane.volmlib.integration.IntegrationServiceContract;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.ServicePriority;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public class IrisIntegrationService implements IrisService, IntegrationServiceContract {
|
||||
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
|
||||
new IntegrationProtocolVersion(1, 0),
|
||||
new IntegrationProtocolVersion(1, 1)
|
||||
);
|
||||
|
||||
private static final Set<String> CAPABILITIES = Set.of(
|
||||
"handshake",
|
||||
"heartbeat",
|
||||
"metrics",
|
||||
"iris-engine-metrics"
|
||||
);
|
||||
|
||||
private volatile IntegrationProtocolVersion negotiatedProtocol = new IntegrationProtocolVersion(1, 1);
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
Bukkit.getServicesManager().register(IntegrationServiceContract.class, this, Iris.instance, ServicePriority.Normal);
|
||||
Iris.verbose("Integration provider registered for Iris");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
Bukkit.getServicesManager().unregister(IntegrationServiceContract.class, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String pluginId() {
|
||||
return "iris";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String pluginVersion() {
|
||||
return Iris.instance.getDescription().getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<IntegrationProtocolVersion> supportedProtocols() {
|
||||
return SUPPORTED_PROTOCOLS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> capabilities() {
|
||||
return CAPABILITIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<IntegrationMetricDescriptor> metricDescriptors() {
|
||||
return IntegrationMetricSchema.descriptors().stream()
|
||||
.filter(descriptor -> descriptor.key().startsWith("iris."))
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntegrationHandshakeResponse handshake(IntegrationHandshakeRequest request) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (request == null) {
|
||||
return new IntegrationHandshakeResponse(
|
||||
pluginId(),
|
||||
pluginVersion(),
|
||||
false,
|
||||
null,
|
||||
SUPPORTED_PROTOCOLS,
|
||||
CAPABILITIES,
|
||||
"missing request",
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
Optional<IntegrationProtocolVersion> negotiated = IntegrationProtocolNegotiator.negotiate(
|
||||
SUPPORTED_PROTOCOLS,
|
||||
request.supportedProtocols()
|
||||
);
|
||||
if (negotiated.isEmpty()) {
|
||||
return new IntegrationHandshakeResponse(
|
||||
pluginId(),
|
||||
pluginVersion(),
|
||||
false,
|
||||
null,
|
||||
SUPPORTED_PROTOCOLS,
|
||||
CAPABILITIES,
|
||||
"no-common-protocol",
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
negotiatedProtocol = negotiated.get();
|
||||
return new IntegrationHandshakeResponse(
|
||||
pluginId(),
|
||||
pluginVersion(),
|
||||
true,
|
||||
negotiatedProtocol,
|
||||
SUPPORTED_PROTOCOLS,
|
||||
CAPABILITIES,
|
||||
"ok",
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntegrationHeartbeat heartbeat() {
|
||||
long now = System.currentTimeMillis();
|
||||
return new IntegrationHeartbeat(negotiatedProtocol, true, now, "ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, IntegrationMetricSample> sampleMetrics(Set<String> metricKeys) {
|
||||
Set<String> requested = metricKeys == null || metricKeys.isEmpty()
|
||||
? IntegrationMetricSchema.irisKeys()
|
||||
: metricKeys;
|
||||
long now = System.currentTimeMillis();
|
||||
Map<String, IntegrationMetricSample> out = new HashMap<>();
|
||||
|
||||
for (String key : requested) {
|
||||
switch (key) {
|
||||
case IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS -> out.put(key, sampleChunkStreamMetric(now));
|
||||
case IntegrationMetricSchema.IRIS_PREGEN_QUEUE -> out.put(key, samplePregenQueueMetric(now));
|
||||
case IntegrationMetricSchema.IRIS_BIOME_CACHE_HIT_RATE -> out.put(key, sampleBiomeCacheHitRateMetric(now));
|
||||
default -> out.put(key, IntegrationMetricSample.unavailable(
|
||||
IntegrationMetricSchema.descriptor(key),
|
||||
"unsupported-key",
|
||||
now
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private IntegrationMetricSample sampleChunkStreamMetric(long now) {
|
||||
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_CHUNK_STREAM_MS);
|
||||
|
||||
double chunksPerSecond = PregeneratorJob.chunksPerSecond();
|
||||
|
||||
if (chunksPerSecond > 0D) {
|
||||
return IntegrationMetricSample.available(descriptor, 1000D / chunksPerSecond, now);
|
||||
}
|
||||
|
||||
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
|
||||
if (engineService != null) {
|
||||
double idle = engineService.getAverageIdleDuration();
|
||||
if (idle > 0D && Double.isFinite(idle)) {
|
||||
return IntegrationMetricSample.available(descriptor, idle, now);
|
||||
}
|
||||
}
|
||||
|
||||
return IntegrationMetricSample.available(descriptor, 0D, now);
|
||||
}
|
||||
|
||||
private IntegrationMetricSample samplePregenQueueMetric(long now) {
|
||||
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_PREGEN_QUEUE);
|
||||
long totalQueue = 0L;
|
||||
boolean hasAnySource = false;
|
||||
|
||||
long pregenRemaining = PregeneratorJob.chunksRemaining();
|
||||
if (pregenRemaining >= 0L) {
|
||||
totalQueue += pregenRemaining;
|
||||
hasAnySource = true;
|
||||
}
|
||||
|
||||
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
|
||||
if (engineService != null) {
|
||||
totalQueue += Math.max(0, engineService.getQueuedTectonicPlateCount());
|
||||
hasAnySource = true;
|
||||
}
|
||||
|
||||
if (!hasAnySource) {
|
||||
return IntegrationMetricSample.unavailable(descriptor, "queue-not-available", now);
|
||||
}
|
||||
|
||||
return IntegrationMetricSample.available(descriptor, totalQueue, now);
|
||||
}
|
||||
|
||||
private IntegrationMetricSample sampleBiomeCacheHitRateMetric(long now) {
|
||||
IntegrationMetricDescriptor descriptor = IntegrationMetricSchema.descriptor(IntegrationMetricSchema.IRIS_BIOME_CACHE_HIT_RATE);
|
||||
IrisEngineSVC engineService = Iris.service(IrisEngineSVC.class);
|
||||
if (engineService == null) {
|
||||
return IntegrationMetricSample.unavailable(descriptor, "engine-service-unavailable", now);
|
||||
}
|
||||
|
||||
double ratio = engineService.getBiomeCacheUsageRatio();
|
||||
if (!Double.isFinite(ratio)) {
|
||||
return IntegrationMetricSample.unavailable(descriptor, "biome-cache-ratio-invalid", now);
|
||||
}
|
||||
|
||||
return IntegrationMetricSample.available(descriptor, Math.max(0D, Math.min(1D, ratio)), now);
|
||||
}
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit 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.core.service;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.protocol.EngineResolver;
|
||||
import art.arcane.iris.core.protocol.IrisProtocolServer;
|
||||
import art.arcane.iris.core.protocol.IrisServerTransport;
|
||||
import art.arcane.iris.core.protocol.IrisSession;
|
||||
import art.arcane.iris.core.protocol.IrisSessionRegistry;
|
||||
import art.arcane.iris.core.protocol.IrisVisionRequestService;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.protocol.IrisProtocol;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.plugin.messaging.Messenger;
|
||||
import org.bukkit.plugin.messaging.PluginMessageListener;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class IrisProtocolService implements IrisService, PluginMessageListener, IrisServerTransport {
|
||||
private static final long SERVER_CAPABILITIES = IrisProtocol.CAPABILITY_PREGEN | IrisProtocol.CAPABILITY_VISION | IrisProtocol.CAPABILITY_CURSOR;
|
||||
private static final int DIMENSION_SYNC_RETRY_TICKS = 20;
|
||||
private static final int DIMENSION_SYNC_MAX_ATTEMPTS = 15;
|
||||
|
||||
private IrisSessionRegistry registry;
|
||||
private IrisProtocolServer protocolServer;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
Messenger messenger = Bukkit.getMessenger();
|
||||
messenger.registerOutgoingPluginChannel(Iris.instance, IrisProtocol.CHANNEL);
|
||||
messenger.registerIncomingPluginChannel(Iris.instance, IrisProtocol.CHANNEL, this);
|
||||
registry = new IrisSessionRegistry();
|
||||
protocolServer = new IrisProtocolServer(registry, SERVER_CAPABILITIES, brand(), true);
|
||||
EngineResolver engineResolver = IrisProtocolService::resolveEngine;
|
||||
protocolServer.setEngineResolver(engineResolver);
|
||||
protocolServer.setVisionTileHandler(IrisVisionRequestService.create(engineResolver, registry));
|
||||
IrisServices.register(IrisProtocolServer.class, protocolServer);
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
registry.register(new IrisSession(player.getUniqueId().toString(), this));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
IrisServices.remove(IrisProtocolServer.class);
|
||||
Messenger messenger = Bukkit.getMessenger();
|
||||
messenger.unregisterIncomingPluginChannel(Iris.instance, IrisProtocol.CHANNEL, this);
|
||||
messenger.unregisterOutgoingPluginChannel(Iris.instance, IrisProtocol.CHANNEL);
|
||||
IrisSessionRegistry current = registry;
|
||||
if (current != null) {
|
||||
for (IrisSession session : current.all()) {
|
||||
current.unregister(session.id());
|
||||
}
|
||||
}
|
||||
registry = null;
|
||||
protocolServer = null;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
IrisSessionRegistry current = registry;
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
current.register(new IrisSession(event.getPlayer().getUniqueId().toString(), this));
|
||||
syncDimension(event.getPlayer(), DIMENSION_SYNC_MAX_ATTEMPTS);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
|
||||
if (registry == null) {
|
||||
return;
|
||||
}
|
||||
syncDimension(event.getPlayer(), 1);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
IrisSessionRegistry current = registry;
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
current.unregister(event.getPlayer().getUniqueId().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPluginMessageReceived(String channel, Player player, byte[] message) {
|
||||
IrisProtocolServer current = protocolServer;
|
||||
if (current == null || !IrisProtocol.CHANNEL.equals(channel)) {
|
||||
return;
|
||||
}
|
||||
current.onClientFrame(player.getUniqueId().toString(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendToClient(String sessionId, byte[] frame) {
|
||||
if (sessionId == null || frame == null) {
|
||||
return;
|
||||
}
|
||||
UUID playerId = parseUuid(sessionId);
|
||||
if (playerId == null) {
|
||||
return;
|
||||
}
|
||||
Player player = Bukkit.getPlayer(playerId);
|
||||
if (player == null || !player.isOnline()) {
|
||||
return;
|
||||
}
|
||||
J.runEntity(player, () -> deliver(player, frame));
|
||||
}
|
||||
|
||||
private void deliver(Player player, byte[] frame) {
|
||||
if (!player.isOnline() || !player.getListeningPluginChannels().contains(IrisProtocol.CHANNEL)) {
|
||||
return;
|
||||
}
|
||||
player.sendPluginMessage(Iris.instance, IrisProtocol.CHANNEL, frame);
|
||||
}
|
||||
|
||||
private void syncDimension(Player player, int attemptsLeft) {
|
||||
IrisProtocolServer protocol = protocolServer;
|
||||
IrisSessionRegistry current = registry;
|
||||
if (protocol == null || current == null || !player.isOnline()) {
|
||||
return;
|
||||
}
|
||||
String sessionId = player.getUniqueId().toString();
|
||||
IrisSession session = current.get(sessionId);
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
if (!session.isReady()) {
|
||||
if (attemptsLeft > 1) {
|
||||
J.runEntity(player, () -> syncDimension(player, attemptsLeft - 1), DIMENSION_SYNC_RETRY_TICKS);
|
||||
}
|
||||
return;
|
||||
}
|
||||
World world = player.getWorld();
|
||||
Engine engine = engineFor(world);
|
||||
if (engine != null) {
|
||||
protocol.sendDimensionStatus(sessionId, engine.getDimension().getLoadKey(), engine.getData().getDataFolder().getName(),
|
||||
world.getSeed(), engine.getMinHeight(), engine.getMaxHeight(), true);
|
||||
return;
|
||||
}
|
||||
protocol.sendDimensionStatus(sessionId, WorldIdentity.serialize(world), "", world.getSeed(), world.getMinHeight(), world.getMaxHeight(), false);
|
||||
}
|
||||
|
||||
private static Engine resolveEngine(String sessionId) {
|
||||
UUID playerId = parseUuid(sessionId);
|
||||
if (playerId == null) {
|
||||
return null;
|
||||
}
|
||||
Player player = Bukkit.getPlayer(playerId);
|
||||
if (player == null || !player.isOnline()) {
|
||||
return null;
|
||||
}
|
||||
return engineFor(player.getWorld());
|
||||
}
|
||||
|
||||
private static Engine engineFor(World world) {
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access == null) {
|
||||
return null;
|
||||
}
|
||||
Engine engine = access.getEngine();
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return null;
|
||||
}
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static UUID parseUuid(String sessionId) {
|
||||
try {
|
||||
return UUID.fromString(sessionId);
|
||||
} catch (IllegalArgumentException invalid) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String brand() {
|
||||
return "Iris " + Iris.instance.getDescription().getVersion() + " (Bukkit)";
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import io.papermc.paper.command.brigadier.BasicCommand;
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
final class PaperCommandRegistrar {
|
||||
private static final String ROOT_COMMAND = "iris";
|
||||
|
||||
private PaperCommandRegistrar() {
|
||||
}
|
||||
|
||||
static void register(Iris plugin, CommandSVC commandService) {
|
||||
plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event ->
|
||||
event.registrar().register(
|
||||
ROOT_COMMAND,
|
||||
"Iris world generation command.",
|
||||
List.of("ir", "irs"),
|
||||
command(commandService)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
static BasicCommand command(CommandSVC commandService) {
|
||||
return new PaperCommand(commandService);
|
||||
}
|
||||
|
||||
private record PaperCommand(CommandSVC commandService) implements BasicCommand {
|
||||
@Override
|
||||
public void execute(CommandSourceStack source, String[] args) {
|
||||
commandService.executeRoot(source.getSender(), ROOT_COMMAND, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> suggest(CommandSourceStack source, String[] args) {
|
||||
return commandService.tabCompleteRoot(source.getSender(), ROOT_COMMAND, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse(CommandSender sender) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+185
-115
@@ -16,29 +16,33 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.service;
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.IrisSettings;
|
||||
import com.volmit.iris.core.edit.DustRevealer;
|
||||
import com.volmit.iris.core.link.WorldEditLink;
|
||||
import com.volmit.iris.core.wand.WandSelection;
|
||||
import com.volmit.iris.engine.object.IrisObject;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.data.Cuboid;
|
||||
import com.volmit.iris.util.format.C;
|
||||
import com.volmit.iris.util.math.M;
|
||||
import com.volmit.iris.util.matter.Matter;
|
||||
import com.volmit.iris.util.matter.WorldMatter;
|
||||
import com.volmit.iris.util.plugin.IrisService;
|
||||
import com.volmit.iris.util.plugin.VolmitSender;
|
||||
import com.volmit.iris.util.scheduling.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.World;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.edit.DustRevealer;
|
||||
import art.arcane.iris.core.link.WorldEditLink;
|
||||
import art.arcane.iris.core.wand.WandSelection;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.util.project.matter.WorldMatter;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import art.arcane.volmlib.util.data.Cuboid;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.iris.util.common.plugin.IrisService;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.scheduling.SR;
|
||||
import art.arcane.iris.util.common.scheduling.jobs.Job;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
@@ -55,9 +59,16 @@ import org.bukkit.util.Vector;
|
||||
import java.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import static art.arcane.iris.util.common.data.registry.Particles.CRIT_MAGIC;
|
||||
import static art.arcane.iris.util.common.data.registry.Particles.REDSTONE;
|
||||
|
||||
public class WandSVC implements IrisService {
|
||||
private static final int MS_PER_TICK = Integer.parseInt(System.getProperty("iris.ms_per_tick", "30"));
|
||||
|
||||
private static ItemStack dust;
|
||||
private static ItemStack wand;
|
||||
|
||||
public static void pasteSchematic(IrisObject s, Location at) {
|
||||
s.place(at);
|
||||
@@ -66,30 +77,100 @@ public class WandSVC implements IrisService {
|
||||
/**
|
||||
* Creates an Iris Object from the 2 coordinates selected with a wand
|
||||
*
|
||||
* @param p
|
||||
* The wand player
|
||||
* @param p The wand player
|
||||
* @return The new object
|
||||
*/
|
||||
public static IrisObject createSchematic(Player p) {
|
||||
if(!isHoldingWand(p)) {
|
||||
public static IrisObject createSchematic(Player p, boolean legacy) {
|
||||
if (!isHoldingWand(p)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Location[] f = getCuboid(p);
|
||||
if (f == null || f[0] == null || f[1] == null)
|
||||
return null;
|
||||
Cuboid c = new Cuboid(f[0], f[1]);
|
||||
IrisObject s = new IrisObject(c.getSizeX(), c.getSizeY(), c.getSizeZ());
|
||||
for(Block b : c) {
|
||||
if(b.getType().equals(Material.AIR)) {
|
||||
continue;
|
||||
|
||||
var it = c.chunkedIterator();
|
||||
|
||||
int total = c.getSizeX() * c.getSizeY() * c.getSizeZ();
|
||||
var latch = new CountDownLatch(1);
|
||||
var holder = Iris.tickets.getHolder(p.getWorld());
|
||||
new Job() {
|
||||
private int i;
|
||||
private Chunk chunk;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Scanning Selection";
|
||||
}
|
||||
|
||||
BlockVector bv = b.getLocation().subtract(c.getLowerNE().toVector()).toVector().toBlockVector();
|
||||
s.setUnsigned(bv.getBlockX(), bv.getBlockY(), bv.getBlockZ(), b);
|
||||
}
|
||||
@Override
|
||||
public void execute() {
|
||||
new SR() {
|
||||
@Override
|
||||
public void run() {
|
||||
var time = M.ms() + MS_PER_TICK;
|
||||
while (time > M.ms()) {
|
||||
if (!it.hasNext()) {
|
||||
if (chunk != null) {
|
||||
holder.removeTicket(chunk);
|
||||
chunk = null;
|
||||
}
|
||||
|
||||
cancel();
|
||||
latch.countDown();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var b = it.next();
|
||||
var bChunk = b.getChunk();
|
||||
if (chunk == null) {
|
||||
chunk = bChunk;
|
||||
holder.addTicket(chunk);
|
||||
} else if (chunk != bChunk) {
|
||||
holder.removeTicket(chunk);
|
||||
holder.addTicket(bChunk);
|
||||
chunk = bChunk;
|
||||
}
|
||||
|
||||
if (b.getType().equals(Material.AIR))
|
||||
continue;
|
||||
|
||||
BlockVector bv = b.getLocation().subtract(c.getLowerNE().toVector()).toVector().toBlockVector();
|
||||
s.setUnsigned(bv.getBlockX(), bv.getBlockY(), bv.getBlockZ(), b, legacy);
|
||||
} finally {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void completeWork() {}
|
||||
|
||||
@Override
|
||||
public int getTotalWork() {
|
||||
return total;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWorkCompleted() {
|
||||
return i;
|
||||
}
|
||||
}.execute(new VolmitSender(p), true, () -> {});
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException ignored) {}
|
||||
|
||||
return s;
|
||||
} catch(Throwable e) {
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
@@ -103,7 +184,7 @@ public class WandSVC implements IrisService {
|
||||
* @return The new object
|
||||
*/
|
||||
public static Matter createMatterSchem(Player p) {
|
||||
if(!isHoldingWand(p)) {
|
||||
if (!isHoldingWand(p)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -111,7 +192,7 @@ public class WandSVC implements IrisService {
|
||||
Location[] f = getCuboid(p);
|
||||
|
||||
return WorldMatter.createMatter(p.getName(), f[0], f[1]);
|
||||
} catch(Throwable e) {
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
@@ -122,16 +203,19 @@ public class WandSVC implements IrisService {
|
||||
/**
|
||||
* Converts a user friendly location string to an actual Location
|
||||
*
|
||||
* @param s
|
||||
* The string
|
||||
* @param s The string
|
||||
* @return The location
|
||||
*/
|
||||
public static Location stringToLocation(String s) {
|
||||
try {
|
||||
String[] f = s.split("\\Q in \\E");
|
||||
if (f.length != 2) return null;
|
||||
String[] g = f[0].split("\\Q,\\E");
|
||||
return new Location(Bukkit.getWorld(f[1]), Integer.parseInt(g[0]), Integer.parseInt(g[1]), Integer.parseInt(g[2]));
|
||||
} catch(Throwable e) {
|
||||
if (g.length != 3) return null;
|
||||
World world = WorldIdentity.resolve(f[1]).orElse(null);
|
||||
if (world == null) return null;
|
||||
return new Location(world, Integer.parseInt(g[0]), Integer.parseInt(g[1]), Integer.parseInt(g[2]));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
return null;
|
||||
}
|
||||
@@ -140,16 +224,15 @@ public class WandSVC implements IrisService {
|
||||
/**
|
||||
* Get a user friendly string of a location
|
||||
*
|
||||
* @param loc
|
||||
* The location
|
||||
* @param loc The location
|
||||
* @return The string
|
||||
*/
|
||||
public static String locationToString(Location loc) {
|
||||
if(loc == null) {
|
||||
if (loc == null) {
|
||||
return "<#>";
|
||||
}
|
||||
|
||||
return loc.getBlockX() + "," + loc.getBlockY() + "," + loc.getBlockZ() + " in " + loc.getWorld().getName();
|
||||
return loc.getBlockX() + "," + loc.getBlockY() + "," + loc.getBlockZ() + " in " + WorldIdentity.serialize(loc.getWorld());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,11 +251,11 @@ public class WandSVC implements IrisService {
|
||||
*/
|
||||
public static ItemStack createDust() {
|
||||
ItemStack is = new ItemStack(Material.GLOWSTONE_DUST);
|
||||
is.addUnsafeEnchantment(Enchantment.ARROW_INFINITE, 1);
|
||||
is.addUnsafeEnchantment(Enchantment.FIRE_ASPECT, 1);
|
||||
ItemMeta im = is.getItemMeta();
|
||||
im.setDisplayName(C.BOLD + "" + C.YELLOW + "Dust of Revealing");
|
||||
im.setUnbreakable(true);
|
||||
im.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_PLACED_ON, ItemFlag.HIDE_POTION_EFFECTS, ItemFlag.HIDE_DESTROYS, ItemFlag.HIDE_ENCHANTS);
|
||||
im.addItemFlags(ItemFlag.values());
|
||||
im.setLore(new KList<String>().qadd("Right click on a block to reveal it's placement structure!"));
|
||||
is.setItemMeta(im);
|
||||
|
||||
@@ -182,8 +265,7 @@ public class WandSVC implements IrisService {
|
||||
/**
|
||||
* Finds an existing wand in a users inventory
|
||||
*
|
||||
* @param inventory
|
||||
* The inventory to search
|
||||
* @param inventory The inventory to search
|
||||
* @return The slot number the wand is in. Or -1 if none are found
|
||||
*/
|
||||
public static int findWand(Inventory inventory) {
|
||||
@@ -192,14 +274,14 @@ public class WandSVC implements IrisService {
|
||||
meta.setLore(new ArrayList<>()); //We are resetting the lore as the lore differs between wands
|
||||
wand.setItemMeta(meta);
|
||||
|
||||
for(int s = 0; s < inventory.getSize(); s++) {
|
||||
for (int s = 0; s < inventory.getSize(); s++) {
|
||||
ItemStack stack = inventory.getItem(s);
|
||||
if(stack == null) continue;
|
||||
if (stack == null) continue;
|
||||
meta = stack.getItemMeta();
|
||||
meta.setLore(new ArrayList<>()); //Reset the lore on this too so we can compare them
|
||||
stack.setItemMeta(meta); //We dont need to clone the item as items from .get are cloned
|
||||
|
||||
if(wand.isSimilar(stack)) return s; //If the name, material and NBT is the same
|
||||
if (wand.isSimilar(stack)) return s; //If the name, material and NBT is the same
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -207,19 +289,17 @@ public class WandSVC implements IrisService {
|
||||
/**
|
||||
* Creates an Iris wand. The locations should be the currently selected locations, or null
|
||||
*
|
||||
* @param a
|
||||
* Location A
|
||||
* @param b
|
||||
* Location B
|
||||
* @param a Location A
|
||||
* @param b Location B
|
||||
* @return A new wand
|
||||
*/
|
||||
public static ItemStack createWand(Location a, Location b) {
|
||||
ItemStack is = new ItemStack(Material.BLAZE_ROD);
|
||||
is.addUnsafeEnchantment(Enchantment.ARROW_INFINITE, 1);
|
||||
is.addUnsafeEnchantment(Enchantment.FIRE_ASPECT, 1);
|
||||
ItemMeta im = is.getItemMeta();
|
||||
im.setDisplayName(C.BOLD + "" + C.GOLD + "Wand of Iris");
|
||||
im.setUnbreakable(true);
|
||||
im.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_PLACED_ON, ItemFlag.HIDE_POTION_EFFECTS, ItemFlag.HIDE_DESTROYS, ItemFlag.HIDE_ENCHANTS);
|
||||
im.addItemFlags(ItemFlag.values());
|
||||
im.setLore(new KList<String>().add(locationToString(a), locationToString(b)));
|
||||
is.setItemMeta(im);
|
||||
|
||||
@@ -228,27 +308,27 @@ public class WandSVC implements IrisService {
|
||||
|
||||
public static Location[] getCuboidFromItem(ItemStack is) {
|
||||
ItemMeta im = is.getItemMeta();
|
||||
return new Location[] {stringToLocation(im.getLore().get(0)), stringToLocation(im.getLore().get(1))};
|
||||
return new Location[]{stringToLocation(im.getLore().get(0)), stringToLocation(im.getLore().get(1))};
|
||||
}
|
||||
|
||||
public static Location[] getCuboid(Player p) {
|
||||
if(isHoldingIrisWand(p))
|
||||
{
|
||||
if (isHoldingIrisWand(p)) {
|
||||
return getCuboidFromItem(p.getInventory().getItemInMainHand());
|
||||
}
|
||||
|
||||
Cuboid c = WorldEditLink.getSelection(p);
|
||||
if (IrisSettings.get().getWorld().worldEditWandCUI) {
|
||||
Cuboid c = WorldEditLink.getSelection(p);
|
||||
|
||||
if(c != null)
|
||||
{
|
||||
return new Location[] {c.getLowerNE(), c.getUpperSW()};
|
||||
if (c != null) {
|
||||
return new Location[]{c.getLowerNE(), c.getUpperSW()};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isHoldingWand(Player p) {
|
||||
return isHoldingIrisWand(p) || WorldEditLink.getSelection(p) != null;
|
||||
return isHoldingIrisWand(p) || (IrisSettings.get().getWorld().worldEditWandCUI && WorldEditLink.getSelection(p) != null);
|
||||
}
|
||||
|
||||
public static boolean isHoldingIrisWand(Player p) {
|
||||
@@ -259,26 +339,24 @@ public class WandSVC implements IrisService {
|
||||
/**
|
||||
* Is the itemstack passed an Iris wand
|
||||
*
|
||||
* @param is
|
||||
* The itemstack
|
||||
* @param is The itemstack
|
||||
* @return True if it is
|
||||
*/
|
||||
public static boolean isWand(ItemStack is) {
|
||||
ItemStack wand = createWand();
|
||||
if(is.getItemMeta() == null) return false;
|
||||
if (is.getItemMeta() == null) return false;
|
||||
return is.getType().equals(wand.getType()) &&
|
||||
is.getItemMeta().getDisplayName().equals(wand.getItemMeta().getDisplayName()) &&
|
||||
is.getItemMeta().getEnchants().equals(wand.getItemMeta().getEnchants()) &&
|
||||
is.getItemMeta().getItemFlags().equals(wand.getItemMeta().getItemFlags());
|
||||
is.getItemMeta().getDisplayName().equals(wand.getItemMeta().getDisplayName()) &&
|
||||
is.getItemMeta().getEnchants().equals(wand.getItemMeta().getEnchants()) &&
|
||||
is.getItemMeta().getItemFlags().equals(wand.getItemMeta().getItemFlags());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
ItemStack wand = createWand();
|
||||
wand = createWand();
|
||||
dust = createDust();
|
||||
|
||||
J.ar(() -> {
|
||||
for(Player i : Bukkit.getOnlinePlayers()) {
|
||||
for (Player i : Bukkit.getOnlinePlayers()) {
|
||||
tick(i);
|
||||
}
|
||||
}, 0);
|
||||
@@ -292,14 +370,15 @@ public class WandSVC implements IrisService {
|
||||
public void tick(Player p) {
|
||||
try {
|
||||
try {
|
||||
if((IrisSettings.get().getWorld().worldEditWandCUI && isHoldingWand(p)) || isWand(p.getInventory().getItemInMainHand())) {
|
||||
if ((IrisSettings.get().getWorld().worldEditWandCUI && isHoldingWand(p)) || isWand(p.getInventory().getItemInMainHand())) {
|
||||
Location[] d = getCuboid(p);
|
||||
if (d == null || d[0] == null || d[1] == null) return;
|
||||
new WandSelection(new Cuboid(d[0], d[1]), p).draw();
|
||||
}
|
||||
} catch(Throwable e) {
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
} catch(Throwable e) {
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
@@ -307,34 +386,30 @@ public class WandSVC implements IrisService {
|
||||
/**
|
||||
* Draw the outline of a selected region
|
||||
*
|
||||
* @param d
|
||||
* The cuboid
|
||||
* @param p
|
||||
* The player to show it to
|
||||
* @param d The cuboid
|
||||
* @param p The player to show it to
|
||||
*/
|
||||
public void draw(Cuboid d, Player p) {
|
||||
draw(new Location[] {d.getLowerNE(), d.getUpperSW()}, p);
|
||||
draw(new Location[]{d.getLowerNE(), d.getUpperSW()}, p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the outline of a selected region
|
||||
*
|
||||
* @param d
|
||||
* A pair of locations
|
||||
* @param p
|
||||
* The player to show them to
|
||||
* @param d A pair of locations
|
||||
* @param p The player to show them to
|
||||
*/
|
||||
public void draw(Location[] d, Player p) {
|
||||
Vector gx = Vector.getRandom().subtract(Vector.getRandom()).normalize().clone().multiply(0.65);
|
||||
d[0].getWorld().spawnParticle(Particle.CRIT_MAGIC, d[0], 1, 0.5 + gx.getX(), 0.5 + gx.getY(), 0.5 + gx.getZ(), 0, null, false);
|
||||
d[0].getWorld().spawnParticle(CRIT_MAGIC, d[0], 1, 0.5 + gx.getX(), 0.5 + gx.getY(), 0.5 + gx.getZ(), 0, null, false);
|
||||
Vector gxx = Vector.getRandom().subtract(Vector.getRandom()).normalize().clone().multiply(0.65);
|
||||
d[1].getWorld().spawnParticle(Particle.CRIT, d[1], 1, 0.5 + gxx.getX(), 0.5 + gxx.getY(), 0.5 + gxx.getZ(), 0, null, false);
|
||||
d[1].getWorld().spawnParticle(CRIT_MAGIC, d[1], 1, 0.5 + gxx.getX(), 0.5 + gxx.getY(), 0.5 + gxx.getZ(), 0, null, false);
|
||||
|
||||
if(!d[0].getWorld().equals(d[1].getWorld())) {
|
||||
if (!d[0].getWorld().equals(d[1].getWorld())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(d[0].distanceSquared(d[1]) > 64 * 64) {
|
||||
if (d[0].distanceSquared(d[1]) > 64 * 64) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -345,38 +420,38 @@ public class WandSVC implements IrisService {
|
||||
int maxy = Math.max(d[0].getBlockY(), d[1].getBlockY());
|
||||
int maxz = Math.max(d[0].getBlockZ(), d[1].getBlockZ());
|
||||
|
||||
for(double j = minx - 1; j < maxx + 1; j += 0.25) {
|
||||
for(double k = miny - 1; k < maxy + 1; k += 0.25) {
|
||||
for(double l = minz - 1; l < maxz + 1; l += 0.25) {
|
||||
if(M.r(0.2)) {
|
||||
for (double j = minx - 1; j < maxx + 1; j += 0.25) {
|
||||
for (double k = miny - 1; k < maxy + 1; k += 0.25) {
|
||||
for (double l = minz - 1; l < maxz + 1; l += 0.25) {
|
||||
if (M.r(0.2)) {
|
||||
boolean jj = j == minx || j == maxx;
|
||||
boolean kk = k == miny || k == maxy;
|
||||
boolean ll = l == minz || l == maxz;
|
||||
|
||||
if((jj && kk) || (jj && ll) || (ll && kk)) {
|
||||
if ((jj && kk) || (jj && ll) || (ll && kk)) {
|
||||
Vector push = new Vector(0, 0, 0);
|
||||
|
||||
if(j == minx) {
|
||||
if (j == minx) {
|
||||
push.add(new Vector(-0.55, 0, 0));
|
||||
}
|
||||
|
||||
if(k == miny) {
|
||||
if (k == miny) {
|
||||
push.add(new Vector(0, -0.55, 0));
|
||||
}
|
||||
|
||||
if(l == minz) {
|
||||
if (l == minz) {
|
||||
push.add(new Vector(0, 0, -0.55));
|
||||
}
|
||||
|
||||
if(j == maxx) {
|
||||
if (j == maxx) {
|
||||
push.add(new Vector(0.55, 0, 0));
|
||||
}
|
||||
|
||||
if(k == maxy) {
|
||||
if (k == maxy) {
|
||||
push.add(new Vector(0, 0.55, 0));
|
||||
}
|
||||
|
||||
if(l == maxz) {
|
||||
if (l == maxz) {
|
||||
push.add(new Vector(0, 0, 0.55));
|
||||
}
|
||||
|
||||
@@ -385,7 +460,7 @@ public class WandSVC implements IrisService {
|
||||
int r = color.getRed();
|
||||
int g = color.getGreen();
|
||||
int b = color.getBlue();
|
||||
p.spawnParticle(Particle.REDSTONE, lv.getX(), lv.getY(), lv.getZ(), 1, 0, 0, 0, 0, new Particle.DustOptions(org.bukkit.Color.fromRGB(r, g, b), 0.75f));
|
||||
p.spawnParticle(REDSTONE, lv.getX(), lv.getY(), lv.getZ(), 1, 0, 0, 0, 0, new Particle.DustOptions(org.bukkit.Color.fromRGB(r, g, b), 0.75f));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -395,16 +470,16 @@ public class WandSVC implements IrisService {
|
||||
|
||||
@EventHandler
|
||||
public void on(PlayerInteractEvent e) {
|
||||
if(e.getHand() != EquipmentSlot.HAND)
|
||||
if (e.getHand() != EquipmentSlot.HAND)
|
||||
return;
|
||||
try {
|
||||
if(isHoldingWand(e.getPlayer())) {
|
||||
if(e.getAction().equals(Action.LEFT_CLICK_BLOCK)) {
|
||||
if (isHoldingIrisWand(e.getPlayer())) {
|
||||
if (e.getAction().equals(Action.LEFT_CLICK_BLOCK)) {
|
||||
e.setCancelled(true);
|
||||
e.getPlayer().getInventory().setItemInMainHand(update(true, Objects.requireNonNull(e.getClickedBlock()).getLocation(), e.getPlayer().getInventory().getItemInMainHand()));
|
||||
e.getPlayer().playSound(e.getClickedBlock().getLocation(), Sound.BLOCK_END_PORTAL_FRAME_FILL, 1f, 0.67f);
|
||||
e.getPlayer().updateInventory();
|
||||
} else if(e.getAction().equals(Action.RIGHT_CLICK_BLOCK)) {
|
||||
} else if (e.getAction().equals(Action.RIGHT_CLICK_BLOCK)) {
|
||||
e.setCancelled(true);
|
||||
e.getPlayer().getInventory().setItemInMainHand(update(false, Objects.requireNonNull(e.getClickedBlock()).getLocation(), e.getPlayer().getInventory().getItemInMainHand()));
|
||||
e.getPlayer().playSound(e.getClickedBlock().getLocation(), Sound.BLOCK_END_PORTAL_FRAME_FILL, 1f, 1.17f);
|
||||
@@ -412,14 +487,14 @@ public class WandSVC implements IrisService {
|
||||
}
|
||||
}
|
||||
|
||||
if(isHoldingDust(e.getPlayer())) {
|
||||
if(e.getAction().equals(Action.RIGHT_CLICK_BLOCK)) {
|
||||
if (isHoldingDust(e.getPlayer())) {
|
||||
if (e.getAction().equals(Action.RIGHT_CLICK_BLOCK)) {
|
||||
e.setCancelled(true);
|
||||
e.getPlayer().playSound(Objects.requireNonNull(e.getClickedBlock()).getLocation(), Sound.ENTITY_ENDER_EYE_DEATH, 2f, 1.97f);
|
||||
DustRevealer.spawn(e.getClickedBlock(), new VolmitSender(e.getPlayer(), Iris.instance.getTag()));
|
||||
}
|
||||
}
|
||||
} catch(Throwable xx) {
|
||||
} catch (Throwable xx) {
|
||||
Iris.reportError(xx);
|
||||
}
|
||||
}
|
||||
@@ -427,8 +502,7 @@ public class WandSVC implements IrisService {
|
||||
/**
|
||||
* Is the player holding Dust?
|
||||
*
|
||||
* @param p
|
||||
* The player
|
||||
* @param p The player
|
||||
* @return True if they are
|
||||
*/
|
||||
public boolean isHoldingDust(Player p) {
|
||||
@@ -439,8 +513,7 @@ public class WandSVC implements IrisService {
|
||||
/**
|
||||
* Is the itemstack passed Iris dust?
|
||||
*
|
||||
* @param is
|
||||
* The itemstack
|
||||
* @param is The itemstack
|
||||
* @return True if it is
|
||||
*/
|
||||
public boolean isDust(ItemStack is) {
|
||||
@@ -450,23 +523,20 @@ public class WandSVC implements IrisService {
|
||||
/**
|
||||
* Update the location on an Iris wand
|
||||
*
|
||||
* @param left
|
||||
* True for first location, false for second
|
||||
* @param a
|
||||
* The location
|
||||
* @param item
|
||||
* The wand
|
||||
* @param left True for first location, false for second
|
||||
* @param a The location
|
||||
* @param item The wand
|
||||
* @return The updated wand
|
||||
*/
|
||||
public ItemStack update(boolean left, Location a, ItemStack item) {
|
||||
if(!isWand(item)) {
|
||||
if (!isWand(item)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
Location[] f = getCuboidFromItem(item);
|
||||
Location other = left ? f[1] : f[0];
|
||||
|
||||
if(other != null && !other.getWorld().getName().equals(a.getWorld().getName())) {
|
||||
if (other != null && !WorldIdentity.key(other.getWorld()).equals(WorldIdentity.key(a.getWorld()))) {
|
||||
other = null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.wand;
|
||||
|
||||
import art.arcane.volmlib.util.data.Cuboid;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
import static art.arcane.iris.util.common.data.registry.Particles.REDSTONE;
|
||||
|
||||
public class WandSelection {
|
||||
private final Cuboid c;
|
||||
private final Player p;
|
||||
private static final double STEP = 0.10;
|
||||
|
||||
public WandSelection(Cuboid c, Player p) {
|
||||
this.c = c;
|
||||
this.p = p;
|
||||
}
|
||||
|
||||
public void draw() {
|
||||
Location playerLoc = p.getLocation();
|
||||
double maxDistanceSquared = 256 * 256;
|
||||
int particleCount = 0;
|
||||
|
||||
// cube!
|
||||
Location[][] edges = {
|
||||
{c.getLowerNE(), new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getLowerZ())},
|
||||
{c.getLowerNE(), new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getLowerZ())},
|
||||
{c.getLowerNE(), new Location(c.getWorld(), c.getLowerX(), c.getLowerY(), c.getUpperZ() + 1)},
|
||||
{new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getLowerZ()), new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getLowerZ())},
|
||||
{new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getLowerZ()), new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getUpperZ() + 1)},
|
||||
{new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getLowerZ()), new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getLowerZ())},
|
||||
{new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getLowerZ()), new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getUpperZ() + 1)},
|
||||
{new Location(c.getWorld(), c.getLowerX(), c.getLowerY(), c.getUpperZ() + 1), new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getUpperZ() + 1)},
|
||||
{new Location(c.getWorld(), c.getLowerX(), c.getLowerY(), c.getUpperZ() + 1), new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getUpperZ() + 1)},
|
||||
{new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getLowerZ()), new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getUpperZ() + 1)},
|
||||
{new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getUpperZ() + 1), new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getUpperZ() + 1)},
|
||||
{new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getUpperZ() + 1), new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getUpperZ() + 1)}
|
||||
};
|
||||
|
||||
for (Location[] edge : edges) {
|
||||
Vector direction = edge[1].toVector().subtract(edge[0].toVector());
|
||||
double length = direction.length();
|
||||
direction.normalize();
|
||||
|
||||
for (double d = 0; d <= length; d += STEP) {
|
||||
Location particleLoc = edge[0].clone().add(direction.clone().multiply(d));
|
||||
|
||||
if (playerLoc.distanceSquared(particleLoc) > maxDistanceSquared) {
|
||||
continue;
|
||||
}
|
||||
|
||||
spawnParticle(particleLoc, playerLoc);
|
||||
particleCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void spawnParticle(Location particleLoc, Location playerLoc) {
|
||||
double accuracy = M.lerpInverse(0, 64 * 64, playerLoc.distanceSquared(particleLoc));
|
||||
double dist = M.lerp(0.125, 3.5, accuracy);
|
||||
|
||||
if (M.r(Math.min(dist * 5, 0.9D) * 0.995)) {
|
||||
return;
|
||||
}
|
||||
|
||||
float hue = (float) (0.5f + (Math.sin((particleLoc.getX() + particleLoc.getY() + particleLoc.getZ() + (p.getTicksLived() / 2f)) / 20f) / 2));
|
||||
Color color = Color.getHSBColor(hue, 1, 1);
|
||||
|
||||
p.spawnParticle(REDSTONE, particleLoc,
|
||||
0, 0, 0, 0, 1,
|
||||
new Particle.DustOptions(org.bukkit.Color.fromRGB(color.getRed(), color.getGreen(), color.getBlue()),
|
||||
(float) dist * 3f));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
name: ${name}
|
||||
version: ${version}
|
||||
main: ${main}
|
||||
bootstrapper: ${bootstrapper}
|
||||
folia-supported: true
|
||||
api-version: '${apiVersion}'
|
||||
load: STARTUP
|
||||
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
||||
website: volmit.com
|
||||
description: More than a Dimension!
|
||||
@@ -0,0 +1,12 @@
|
||||
name: ${name}
|
||||
version: ${version}
|
||||
main: ${main}
|
||||
folia-supported: true
|
||||
load: STARTUP
|
||||
authors: [ cyberpwn, NextdoorPsycho, Vatuu ]
|
||||
website: volmit.com
|
||||
description: More than a Dimension!
|
||||
commands:
|
||||
iris:
|
||||
aliases: [ ir, irs ]
|
||||
api-version: '${apiVersion}'
|
||||
@@ -0,0 +1,199 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import io.papermc.paper.datapack.Datapack;
|
||||
import io.papermc.paper.datapack.DatapackRegistrar;
|
||||
import io.papermc.paper.datapack.DatapackSource;
|
||||
import io.papermc.paper.datapack.DiscoveredDatapack;
|
||||
import io.papermc.paper.plugin.configuration.PluginMeta;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.FeatureFlag;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisBootstrapTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void discoverPackAutoEnablesAtFixedTop() throws Exception {
|
||||
File datapackDirectory = temporaryFolder.newFolder("datapack");
|
||||
RecordingRegistrar registrar = new RecordingRegistrar(DiscoveryBehavior.ACCEPT);
|
||||
|
||||
DiscoveredDatapack discovered = IrisBootstrap.discoverPack(registrar, datapackDirectory.toPath());
|
||||
|
||||
assertSame(registrar.discoveredDatapack, discovered);
|
||||
assertEquals(datapackDirectory.toPath(), registrar.discoveredPath);
|
||||
assertEquals(IrisBootstrap.PACK_ID, registrar.discoveredId);
|
||||
assertTrue(registrar.configurer.autoEnableOnServerStart);
|
||||
assertTrue(registrar.configurer.fixedPosition);
|
||||
assertEquals(Datapack.Position.TOP, registrar.configurer.position);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectedDiscoveryReportsDatapackPath() throws Exception {
|
||||
File datapackDirectory = temporaryFolder.newFolder("datapack");
|
||||
RecordingRegistrar registrar = new RecordingRegistrar(DiscoveryBehavior.REJECT);
|
||||
|
||||
IllegalStateException failure = assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> IrisBootstrap.discoverPack(registrar, datapackDirectory.toPath())
|
||||
);
|
||||
|
||||
assertTrue(failure.getMessage().contains(datapackDirectory.toPath().toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryIoFailurePropagates() throws Exception {
|
||||
File datapackDirectory = temporaryFolder.newFolder("datapack");
|
||||
RecordingRegistrar registrar = new RecordingRegistrar(DiscoveryBehavior.FAIL);
|
||||
|
||||
IOException failure = assertThrows(
|
||||
IOException.class,
|
||||
() -> IrisBootstrap.discoverPack(registrar, datapackDirectory.toPath())
|
||||
);
|
||||
|
||||
assertEquals("discovery failed", failure.getMessage());
|
||||
}
|
||||
|
||||
private enum DiscoveryBehavior {
|
||||
ACCEPT,
|
||||
REJECT,
|
||||
FAIL
|
||||
}
|
||||
|
||||
private static final class RecordingConfigurer implements DatapackRegistrar.Configurer {
|
||||
private boolean autoEnableOnServerStart;
|
||||
private boolean fixedPosition;
|
||||
private Datapack.Position position;
|
||||
|
||||
@Override
|
||||
public DatapackRegistrar.Configurer title(Component title) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatapackRegistrar.Configurer autoEnableOnServerStart(boolean autoEnableOnServerStart) {
|
||||
this.autoEnableOnServerStart = autoEnableOnServerStart;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatapackRegistrar.Configurer position(boolean fixed, Datapack.Position position) {
|
||||
this.fixedPosition = fixed;
|
||||
this.position = position;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingRegistrar implements DatapackRegistrar {
|
||||
private final DiscoveryBehavior behavior;
|
||||
private final RecordingConfigurer configurer;
|
||||
private final DiscoveredDatapack discoveredDatapack;
|
||||
private Path discoveredPath;
|
||||
private String discoveredId;
|
||||
|
||||
private RecordingRegistrar(DiscoveryBehavior behavior) {
|
||||
this.behavior = behavior;
|
||||
this.configurer = new RecordingConfigurer();
|
||||
this.discoveredDatapack = new StubDiscoveredDatapack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPackDiscovered(String name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveredDatapack getDiscoveredPack(String name) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeDiscoveredPack(String name) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, DiscoveredDatapack> getDiscoveredPacks() {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveredDatapack discoverPack(URI uri, String id, Consumer<Configurer> configurer) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveredDatapack discoverPack(Path path, String id, Consumer<Configurer> configurer) throws IOException {
|
||||
this.discoveredPath = path;
|
||||
this.discoveredId = id;
|
||||
if (behavior == DiscoveryBehavior.FAIL) {
|
||||
throw new IOException("discovery failed");
|
||||
}
|
||||
configurer.accept(this.configurer);
|
||||
return behavior == DiscoveryBehavior.ACCEPT ? discoveredDatapack : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveredDatapack discoverPack(PluginMeta pluginMeta, URI uri, String id, Consumer<Configurer> configurer) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveredDatapack discoverPack(PluginMeta pluginMeta, Path path, String id, Consumer<Configurer> configurer) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StubDiscoveredDatapack implements DiscoveredDatapack {
|
||||
@Override
|
||||
public String getName() {
|
||||
return IrisBootstrap.PACK_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getTitle() {
|
||||
return Component.text(IrisBootstrap.PACK_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getDescription() {
|
||||
return Component.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRequired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Datapack.Compatibility getCompatibility() {
|
||||
return Datapack.Compatibility.COMPATIBLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<FeatureFlag> getRequiredFeatures() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatapackSource getSource() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.core.splash.IrisSplashPackScanner;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisDiagnosticsTest {
|
||||
@Test
|
||||
public void reportErrorWithContextPrintsFullStacktrace() {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
PrintStream originalErr = System.err;
|
||||
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
|
||||
try {
|
||||
Iris.reportError("Runtime world creation failed.", new IllegalStateException("outer", new IllegalArgumentException("inner")));
|
||||
} finally {
|
||||
System.setErr(originalErr);
|
||||
}
|
||||
|
||||
String text = output.toString(StandardCharsets.UTF_8);
|
||||
assertTrue(text.contains("Runtime world creation failed."));
|
||||
assertTrue(text.contains("IllegalStateException"));
|
||||
assertTrue(text.contains("IllegalArgumentException"));
|
||||
assertTrue(text.contains("inner"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splashPackScanSkipsInternalAndInvalidFolders() throws Exception {
|
||||
Path root = Files.createTempDirectory("iris-splash");
|
||||
try {
|
||||
Path validPack = root.resolve("overworld");
|
||||
Files.createDirectories(validPack.resolve("dimensions"));
|
||||
Files.writeString(validPack.resolve("dimensions").resolve("overworld.json"), "{\"version\":\"4000\"}");
|
||||
|
||||
Files.createDirectories(root.resolve("datapack-imports"));
|
||||
|
||||
Path brokenPack = root.resolve("broken");
|
||||
Files.createDirectories(brokenPack.resolve("dimensions"));
|
||||
Files.writeString(brokenPack.resolve("dimensions").resolve("broken.json"), "{");
|
||||
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
PrintStream originalErr = System.err;
|
||||
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
|
||||
List<IrisSplashPackScanner.SplashPackMetadata> packs;
|
||||
try {
|
||||
packs = IrisSplashPackScanner.collect(root.toFile(), Iris::reportError);
|
||||
} finally {
|
||||
System.setErr(originalErr);
|
||||
}
|
||||
|
||||
assertEquals(1, packs.size());
|
||||
assertEquals("overworld", packs.get(0).name());
|
||||
assertEquals("4000", packs.get(0).version());
|
||||
|
||||
String text = output.toString(StandardCharsets.UTF_8);
|
||||
assertTrue(text.contains("Failed to read splash metadata for dimension pack \"broken\"."));
|
||||
assertTrue(text.contains("Json"));
|
||||
} finally {
|
||||
Files.walk(root)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.forEach(File::delete);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import org.bukkit.plugin.PluginDescriptionFile;
|
||||
import org.bukkit.plugin.PluginLoadOrder;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
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.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PaperPluginMetadataTest {
|
||||
@Test
|
||||
public void paperMetadataDeclaresBootstrapAndFoliaSupport() throws Exception {
|
||||
String metadata;
|
||||
try (InputStream stream = PaperPluginMetadataTest.class.getResourceAsStream("/paper-plugin.yml")) {
|
||||
assertNotNull(stream);
|
||||
metadata = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
assertTrue(metadata.contains("bootstrapper: " + IrisBootstrap.class.getName()));
|
||||
assertTrue(metadata.contains("folia-supported: true"));
|
||||
assertTrue(metadata.contains("load: STARTUP"));
|
||||
assertFalse(metadata.contains("commands:"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bukkitMetadataRetainsStartupCommandAndAliases() throws Exception {
|
||||
PluginDescriptionFile metadata;
|
||||
try (InputStream stream = PaperPluginMetadataTest.class.getResourceAsStream("/plugin.yml")) {
|
||||
assertNotNull(stream);
|
||||
metadata = new PluginDescriptionFile(stream);
|
||||
}
|
||||
|
||||
assertEquals(Iris.class.getName(), metadata.getMain());
|
||||
assertEquals(PluginLoadOrder.STARTUP, metadata.getLoad());
|
||||
Map<String, Map<String, Object>> commands = metadata.getCommands();
|
||||
assertTrue(commands.containsKey("iris"));
|
||||
assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void processedResourcesContainBothMetadataFormats() throws Exception {
|
||||
URL paperMetadata = PaperPluginMetadataTest.class.getResource("/paper-plugin.yml");
|
||||
assertNotNull(paperMetadata);
|
||||
assertEquals("file", paperMetadata.getProtocol());
|
||||
Path bukkitMetadata = Path.of(paperMetadata.toURI()).resolveSibling("plugin.yml");
|
||||
assertTrue(Files.isRegularFile(bukkitMetadata));
|
||||
assertFalse(Files.readString(bukkitMetadata, StandardCharsets.UTF_8).contains("${"));
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.core.commands.CommandStudio;
|
||||
import art.arcane.iris.core.tools.IrisCreator;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class StudioRuntimeCleanupTest {
|
||||
@Test
|
||||
public void pregenSettingsNoLongerExposeStartupNoisemapPrebake() {
|
||||
boolean found = Arrays.stream(IrisSettings.IrisSettingsPregen.class.getDeclaredFields())
|
||||
.anyMatch(field -> field.getName().equals("startupNoisemapPrebake"));
|
||||
|
||||
assertFalse(found);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void studioCommandNoLongerExposesProfilecache() {
|
||||
boolean found = Arrays.stream(CommandStudio.class.getDeclaredMethods())
|
||||
.anyMatch(method -> method.getName().equals("profilecache"));
|
||||
|
||||
assertFalse(found);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void studioCreatorNoLongerContainsPrewarmOrPrebakeHelpers() {
|
||||
boolean found = Arrays.stream(IrisCreator.class.getDeclaredMethods())
|
||||
.map(method -> method.getName().toLowerCase())
|
||||
.anyMatch(name -> name.contains("prewarm") || name.contains("prebake"));
|
||||
|
||||
assertFalse(found);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noisemapPrebakePipelineClassIsRemoved() {
|
||||
try {
|
||||
Class.forName("art.arcane.iris.engine.IrisNoisemapPrebakePipeline");
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new AssertionError("IrisNoisemapPrebakePipeline should not exist.");
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisEngineSVCTest {
|
||||
@Test
|
||||
public void maintenanceSkipsReductionWhenPregenDoesNotTargetWorld() {
|
||||
assertTrue(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(true, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void maintenanceDoesNotSkipReductionForActivePregenWorld() {
|
||||
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(true, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noMaintenanceNeverSkipsReduction() {
|
||||
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(false, false));
|
||||
assertFalse(IrisEngineSVC.shouldSkipMantleReductionForMaintenance(false, true));
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import io.papermc.paper.command.brigadier.BasicCommand;
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PaperCommandRegistrarTest {
|
||||
@Test
|
||||
public void paperCommandDelegatesExecutionAndSuggestions() {
|
||||
RecordingCommandService service = new RecordingCommandService();
|
||||
BasicCommand command = PaperCommandRegistrar.command(service);
|
||||
CommandSender sender = sender();
|
||||
CommandSourceStack source = new TestCommandSourceStack(sender);
|
||||
String[] arguments = {"create", "world"};
|
||||
|
||||
command.execute(source, arguments);
|
||||
assertSame(sender, service.sender);
|
||||
assertEquals("iris", service.label);
|
||||
assertArrayEquals(arguments, service.arguments);
|
||||
assertEquals(List.of("first", "second"), command.suggest(source, new String[]{"cr"}));
|
||||
assertTrue(command.canUse(sender));
|
||||
}
|
||||
|
||||
private static CommandSender sender() {
|
||||
return (CommandSender) Proxy.newProxyInstance(
|
||||
CommandSender.class.getClassLoader(),
|
||||
new Class<?>[]{CommandSender.class},
|
||||
(proxy, method, arguments) -> primitiveDefault(method.getReturnType())
|
||||
);
|
||||
}
|
||||
|
||||
private static Object primitiveDefault(Class<?> type) {
|
||||
if (type == boolean.class) {
|
||||
return false;
|
||||
}
|
||||
if (type == byte.class) {
|
||||
return (byte) 0;
|
||||
}
|
||||
if (type == short.class) {
|
||||
return (short) 0;
|
||||
}
|
||||
if (type == int.class) {
|
||||
return 0;
|
||||
}
|
||||
if (type == long.class) {
|
||||
return 0L;
|
||||
}
|
||||
if (type == float.class) {
|
||||
return 0F;
|
||||
}
|
||||
if (type == double.class) {
|
||||
return 0D;
|
||||
}
|
||||
if (type == char.class) {
|
||||
return '\0';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class RecordingCommandService extends CommandSVC {
|
||||
private CommandSender sender;
|
||||
private String label;
|
||||
private String[] arguments;
|
||||
|
||||
@Override
|
||||
void executeRoot(CommandSender sender, String label, String[] args) {
|
||||
this.sender = sender;
|
||||
this.label = label;
|
||||
this.arguments = args;
|
||||
}
|
||||
|
||||
@Override
|
||||
List<String> tabCompleteRoot(CommandSender sender, String alias, String[] args) {
|
||||
return List.of("first", "second");
|
||||
}
|
||||
}
|
||||
|
||||
private record TestCommandSourceStack(CommandSender sender) implements CommandSourceStack {
|
||||
@Override
|
||||
public Location getLocation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandSender getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entity getExecutor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandSourceStack withLocation(Location location) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandSourceStack withExecutor(Entity executor) {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ClientPacketSink {
|
||||
void send(byte[] frame);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import art.arcane.iris.spi.protocol.IrisMessageCodec;
|
||||
import art.arcane.iris.spi.protocol.IrisProtocol;
|
||||
import art.arcane.iris.spi.protocol.ProtocolException;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
public final class IrisClient {
|
||||
public static final Identifier HUD_ELEMENT_ID = Identifier.fromNamespaceAndPath("irisworldgen", "pregen_hud");
|
||||
public static final Identifier KEYBIND_CATEGORY_ID = Identifier.fromNamespaceAndPath("irisworldgen", "iris");
|
||||
public static final String KEYBIND_TOGGLE_HUD = "key.irisworldgen.toggle_pregen_hud";
|
||||
public static final String KEYBIND_OPEN_MAP = "key.irisworldgen.open_vision_map";
|
||||
public static final String KEYBIND_TOGGLE_WHAT = "key.irisworldgen.toggle_what_overlay";
|
||||
|
||||
private static final IrisClientSession SESSION = new IrisClientSession();
|
||||
private static final IrisClientPregenState PREGEN = new IrisClientPregenState();
|
||||
private static final IrisClientDimension DIMENSION = new IrisClientDimension();
|
||||
private static final IrisClientTileCache TILES = new IrisClientTileCache(IrisClient::sendFrame, System::currentTimeMillis);
|
||||
private static final IrisClientCursor CURSOR = new IrisClientCursor(IrisClient::sendFrame, System::currentTimeMillis);
|
||||
private static final IrisClientMarkers MARKERS = new IrisClientMarkers();
|
||||
private static final IrisClientRegionMap REGIONS = new IrisClientRegionMap();
|
||||
private static final IrisClientToasts TOASTS = new IrisClientToasts();
|
||||
private static volatile ClientPacketSink sink;
|
||||
private static volatile boolean hudVisible = true;
|
||||
private static volatile boolean whatVisible = false;
|
||||
|
||||
private IrisClient() {
|
||||
}
|
||||
|
||||
public static IrisClientSession session() {
|
||||
return SESSION;
|
||||
}
|
||||
|
||||
public static IrisClientPregenState pregen() {
|
||||
return PREGEN;
|
||||
}
|
||||
|
||||
public static IrisClientDimension dimension() {
|
||||
return DIMENSION;
|
||||
}
|
||||
|
||||
public static IrisClientTileCache tiles() {
|
||||
return TILES;
|
||||
}
|
||||
|
||||
public static IrisClientCursor cursor() {
|
||||
return CURSOR;
|
||||
}
|
||||
|
||||
public static IrisClientMarkers markers() {
|
||||
return MARKERS;
|
||||
}
|
||||
|
||||
public static IrisClientRegionMap regions() {
|
||||
return REGIONS;
|
||||
}
|
||||
|
||||
public static IrisClientToasts toasts() {
|
||||
return TOASTS;
|
||||
}
|
||||
|
||||
public static boolean hudVisible() {
|
||||
return hudVisible;
|
||||
}
|
||||
|
||||
public static void toggleHud() {
|
||||
hudVisible = !hudVisible;
|
||||
}
|
||||
|
||||
public static boolean whatVisible() {
|
||||
return whatVisible;
|
||||
}
|
||||
|
||||
public static void toggleWhat() {
|
||||
whatVisible = !whatVisible;
|
||||
}
|
||||
|
||||
public static boolean visionAvailable() {
|
||||
return SESSION.isReady() && DIMENSION.irisWorld() && (SESSION.serverCapabilities() & IrisProtocol.CAPABILITY_VISION) != 0L;
|
||||
}
|
||||
|
||||
public static boolean cursorAvailable() {
|
||||
return SESSION.isReady() && DIMENSION.irisWorld() && (SESSION.serverCapabilities() & IrisProtocol.CAPABILITY_CURSOR) != 0L;
|
||||
}
|
||||
|
||||
public static void bindSender(ClientPacketSink boundSink) {
|
||||
sink = boundSink;
|
||||
SESSION.bind(boundSink);
|
||||
}
|
||||
|
||||
public static void sendFrame(byte[] frame) {
|
||||
ClientPacketSink activeSink = sink;
|
||||
if (activeSink != null) {
|
||||
activeSink.send(frame);
|
||||
}
|
||||
}
|
||||
|
||||
public static void onWorldJoin() {
|
||||
SESSION.sendHello();
|
||||
}
|
||||
|
||||
public static void onDisconnect() {
|
||||
SESSION.reset();
|
||||
PREGEN.clear();
|
||||
DIMENSION.clear();
|
||||
TILES.clear();
|
||||
CURSOR.clear();
|
||||
MARKERS.clear();
|
||||
REGIONS.clear();
|
||||
TOASTS.clear();
|
||||
}
|
||||
|
||||
public static void onInbound(byte[] frame) {
|
||||
if (frame == null) {
|
||||
return;
|
||||
}
|
||||
IrisMessage message;
|
||||
try {
|
||||
message = IrisMessageCodec.decode(frame);
|
||||
} catch (ProtocolException rejected) {
|
||||
return;
|
||||
}
|
||||
if (message == null) {
|
||||
return;
|
||||
}
|
||||
route(message);
|
||||
}
|
||||
|
||||
private static void route(IrisMessage message) {
|
||||
switch (message) {
|
||||
case IrisMessage.ServerHello serverHello -> SESSION.onServerHello(serverHello);
|
||||
case IrisMessage.PregenProgress progress -> PREGEN.onProgress(progress);
|
||||
case IrisMessage.PregenEnd end -> onPregenEnd(end.jobId());
|
||||
case IrisMessage.DimensionStatus status -> onDimensionStatus(status);
|
||||
case IrisMessage.CursorInfo cursorInfo -> CURSOR.onCursorInfo(cursorInfo);
|
||||
case IrisMessage.VisionTile visionTile -> TILES.onVisionTile(visionTile);
|
||||
case IrisMessage.VisionMarkers visionMarkers -> MARKERS.onMarkers(visionMarkers);
|
||||
case IrisMessage.PregenRegionDelta delta -> REGIONS.onDelta(delta, PREGEN.activeJobId());
|
||||
case IrisMessage.StudioHotload hotload -> TOASTS.enqueueHotload(hotload.packKey(), hotload.changedFiles(), hotload.failed(), hotload.message());
|
||||
case IrisMessage.Toast toast -> TOASTS.enqueue(toast.kind(), toast.title(), toast.body());
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void onPregenEnd(long jobId) {
|
||||
PREGEN.onEnd(jobId);
|
||||
REGIONS.onEnd(jobId);
|
||||
}
|
||||
|
||||
private static void onDimensionStatus(IrisMessage.DimensionStatus status) {
|
||||
boolean changed = DIMENSION.onDimensionStatus(status);
|
||||
if (changed) {
|
||||
TILES.clear();
|
||||
MARKERS.clear();
|
||||
CURSOR.clear();
|
||||
REGIONS.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import art.arcane.iris.spi.protocol.IrisMessageCodec;
|
||||
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
public final class IrisClientCursor {
|
||||
private static final long MIN_REQUEST_INTERVAL_MILLIS = 500L;
|
||||
|
||||
private final ClientPacketSink sink;
|
||||
private final LongSupplier clock;
|
||||
private volatile IrisMessage.CursorInfo latest;
|
||||
private int lastRequestedX;
|
||||
private int lastRequestedZ;
|
||||
private boolean requested;
|
||||
private long lastRequestMillis;
|
||||
|
||||
public IrisClientCursor(ClientPacketSink sink, LongSupplier clock) {
|
||||
this.sink = sink;
|
||||
this.clock = clock;
|
||||
this.latest = null;
|
||||
this.lastRequestedX = 0;
|
||||
this.lastRequestedZ = 0;
|
||||
this.requested = false;
|
||||
this.lastRequestMillis = 0L;
|
||||
}
|
||||
|
||||
public synchronized void requestFor(int blockX, int blockZ) {
|
||||
if (requested && blockX == lastRequestedX && blockZ == lastRequestedZ) {
|
||||
return;
|
||||
}
|
||||
long now = clock.getAsLong();
|
||||
if (now - lastRequestMillis < MIN_REQUEST_INTERVAL_MILLIS) {
|
||||
return;
|
||||
}
|
||||
sink.send(IrisMessageCodec.encode(new IrisMessage.CursorInfoRequest(blockX, blockZ)));
|
||||
lastRequestedX = blockX;
|
||||
lastRequestedZ = blockZ;
|
||||
requested = true;
|
||||
lastRequestMillis = now;
|
||||
}
|
||||
|
||||
public void onCursorInfo(IrisMessage.CursorInfo info) {
|
||||
latest = info;
|
||||
}
|
||||
|
||||
public IrisMessage.CursorInfo latest() {
|
||||
return latest;
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
latest = null;
|
||||
requested = false;
|
||||
lastRequestMillis = 0L;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
|
||||
public final class IrisClientDimension {
|
||||
private volatile IrisMessage.DimensionStatus status;
|
||||
|
||||
public boolean onDimensionStatus(IrisMessage.DimensionStatus incoming) {
|
||||
IrisMessage.DimensionStatus previous = status;
|
||||
status = incoming;
|
||||
if (previous == null) {
|
||||
return true;
|
||||
}
|
||||
return previous.irisWorld() != incoming.irisWorld() || !previous.dimensionKey().equals(incoming.dimensionKey());
|
||||
}
|
||||
|
||||
public IrisMessage.DimensionStatus status() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public boolean irisWorld() {
|
||||
IrisMessage.DimensionStatus current = status;
|
||||
return current != null && current.irisWorld();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
status = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
|
||||
public final class IrisClientHud {
|
||||
private IrisClientHud() {
|
||||
}
|
||||
|
||||
public static void render(GuiGraphicsExtractor graphics) {
|
||||
IrisToastPresenter.pump();
|
||||
IrisPregenHud.render(graphics);
|
||||
IrisWhatOverlay.render(graphics);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
public final class IrisClientKeybinds {
|
||||
private static final KeyMapping.Category CATEGORY = KeyMapping.Category.register(IrisClient.KEYBIND_CATEGORY_ID);
|
||||
public static final KeyMapping TOGGLE_HUD = new KeyMapping(IrisClient.KEYBIND_TOGGLE_HUD, GLFW.GLFW_KEY_H, CATEGORY);
|
||||
public static final KeyMapping OPEN_MAP = new KeyMapping(IrisClient.KEYBIND_OPEN_MAP, GLFW.GLFW_KEY_M, CATEGORY);
|
||||
public static final KeyMapping TOGGLE_WHAT = new KeyMapping(IrisClient.KEYBIND_TOGGLE_WHAT, GLFW.GLFW_KEY_J, CATEGORY);
|
||||
|
||||
private IrisClientKeybinds() {
|
||||
}
|
||||
|
||||
public static KeyMapping.Category category() {
|
||||
return CATEGORY;
|
||||
}
|
||||
|
||||
public static void pollToggle() {
|
||||
while (TOGGLE_HUD.consumeClick()) {
|
||||
IrisClient.toggleHud();
|
||||
}
|
||||
while (TOGGLE_WHAT.consumeClick()) {
|
||||
IrisClient.toggleWhat();
|
||||
}
|
||||
while (OPEN_MAP.consumeClick()) {
|
||||
openVisionScreen();
|
||||
}
|
||||
}
|
||||
|
||||
private static void openVisionScreen() {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
if (minecraft == null || minecraft.level == null || minecraft.player == null) {
|
||||
return;
|
||||
}
|
||||
minecraft.setScreenAndShow(new IrisVisionScreen());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class IrisClientMarkers {
|
||||
private final Map<IrisTileKey, List<IrisMessage.VisionMarkers.Marker>> byTile;
|
||||
|
||||
public IrisClientMarkers() {
|
||||
this.byTile = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
public void onMarkers(IrisMessage.VisionMarkers markers) {
|
||||
byTile.put(new IrisTileKey(markers.tileX(), markers.tileZ(), markers.zoomLevel()), List.copyOf(markers.markers()));
|
||||
}
|
||||
|
||||
public List<IrisMessage.VisionMarkers.Marker> forTile(IrisTileKey key) {
|
||||
return byTile.get(key);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
byTile.clear();
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class IrisClientPregenState {
|
||||
private final ConcurrentHashMap<Long, IrisMessage.PregenProgress> jobs;
|
||||
private volatile Long activeJobId;
|
||||
|
||||
public IrisClientPregenState() {
|
||||
this.jobs = new ConcurrentHashMap<>();
|
||||
this.activeJobId = null;
|
||||
}
|
||||
|
||||
public void onProgress(IrisMessage.PregenProgress progress) {
|
||||
jobs.put(progress.jobId(), progress);
|
||||
activeJobId = progress.jobId();
|
||||
}
|
||||
|
||||
public void onEnd(long jobId) {
|
||||
jobs.remove(jobId);
|
||||
Long current = activeJobId;
|
||||
if (current != null && current == jobId) {
|
||||
activeJobId = jobs.keySet().stream().findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
public IrisMessage.PregenProgress active() {
|
||||
Long current = activeJobId;
|
||||
if (current == null) {
|
||||
return null;
|
||||
}
|
||||
return jobs.get(current);
|
||||
}
|
||||
|
||||
public Long activeJobId() {
|
||||
return activeJobId;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
jobs.clear();
|
||||
activeJobId = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class IrisClientRegionMap {
|
||||
private static final int MAX_REGIONS = 4096;
|
||||
|
||||
private final ConcurrentHashMap<Long, Integer> states;
|
||||
private volatile boolean tracking;
|
||||
private volatile long jobId;
|
||||
private volatile Bounds bounds;
|
||||
|
||||
public IrisClientRegionMap() {
|
||||
this.states = new ConcurrentHashMap<>();
|
||||
this.tracking = false;
|
||||
this.jobId = 0L;
|
||||
this.bounds = null;
|
||||
}
|
||||
|
||||
public void onDelta(IrisMessage.PregenRegionDelta delta, Long activeJobId) {
|
||||
if (activeJobId == null) {
|
||||
return;
|
||||
}
|
||||
long active = activeJobId;
|
||||
if (delta.jobId() != active) {
|
||||
return;
|
||||
}
|
||||
if (!tracking || jobId != delta.jobId()) {
|
||||
resetTo(delta.jobId());
|
||||
}
|
||||
record(delta.regionX(), delta.regionZ(), delta.state());
|
||||
}
|
||||
|
||||
public void onEnd(long endedJobId) {
|
||||
if (tracking && jobId == endedJobId) {
|
||||
clear();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasData() {
|
||||
return tracking && !states.isEmpty();
|
||||
}
|
||||
|
||||
public Bounds bounds() {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
public void forEachCell(CellConsumer consumer) {
|
||||
for (Map.Entry<Long, Integer> entry : states.entrySet()) {
|
||||
long packed = entry.getKey();
|
||||
consumer.accept(regionX(packed), regionZ(packed), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
states.clear();
|
||||
bounds = null;
|
||||
tracking = false;
|
||||
jobId = 0L;
|
||||
}
|
||||
|
||||
private void resetTo(long newJobId) {
|
||||
states.clear();
|
||||
bounds = null;
|
||||
jobId = newJobId;
|
||||
tracking = true;
|
||||
}
|
||||
|
||||
private void record(int regionX, int regionZ, int state) {
|
||||
long packed = pack(regionX, regionZ);
|
||||
if (!states.containsKey(packed)) {
|
||||
if (states.size() >= MAX_REGIONS) {
|
||||
return;
|
||||
}
|
||||
Bounds current = bounds;
|
||||
bounds = current == null ? new Bounds(regionX, regionZ, regionX, regionZ) : current.union(regionX, regionZ);
|
||||
}
|
||||
states.put(packed, state);
|
||||
}
|
||||
|
||||
private static long pack(int regionX, int regionZ) {
|
||||
return ((long) regionX << 32) | (regionZ & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
private static int regionX(long packed) {
|
||||
return (int) (packed >> 32);
|
||||
}
|
||||
|
||||
private static int regionZ(long packed) {
|
||||
return (int) packed;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface CellConsumer {
|
||||
void accept(int regionX, int regionZ, int state);
|
||||
}
|
||||
|
||||
public record Bounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
|
||||
public Bounds union(int regionX, int regionZ) {
|
||||
return new Bounds(
|
||||
Math.min(minRegionX, regionX),
|
||||
Math.min(minRegionZ, regionZ),
|
||||
Math.max(maxRegionX, regionX),
|
||||
Math.max(maxRegionZ, regionZ));
|
||||
}
|
||||
|
||||
public int regionsWide() {
|
||||
return maxRegionX - minRegionX + 1;
|
||||
}
|
||||
|
||||
public int regionsTall() {
|
||||
return maxRegionZ - minRegionZ + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import art.arcane.iris.spi.protocol.IrisMessageCodec;
|
||||
import art.arcane.iris.spi.protocol.IrisProtocol;
|
||||
|
||||
public final class IrisClientSession {
|
||||
private static final long CLIENT_CAPABILITIES = IrisProtocol.CAPABILITY_PREGEN | IrisProtocol.CAPABILITY_VISION | IrisProtocol.CAPABILITY_CURSOR | IrisProtocol.CAPABILITY_STUDIO;
|
||||
|
||||
private volatile State state;
|
||||
private volatile long serverCapabilities;
|
||||
private volatile boolean irisActive;
|
||||
private volatile String serverBrand;
|
||||
private volatile ClientPacketSink sink;
|
||||
|
||||
public IrisClientSession() {
|
||||
this.state = State.IDLE;
|
||||
this.serverCapabilities = 0L;
|
||||
this.irisActive = false;
|
||||
this.serverBrand = "";
|
||||
this.sink = null;
|
||||
}
|
||||
|
||||
public void bind(ClientPacketSink boundSink) {
|
||||
this.sink = boundSink;
|
||||
}
|
||||
|
||||
public State state() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return state == State.READY;
|
||||
}
|
||||
|
||||
public boolean irisActive() {
|
||||
return irisActive;
|
||||
}
|
||||
|
||||
public long serverCapabilities() {
|
||||
return serverCapabilities;
|
||||
}
|
||||
|
||||
public String serverBrand() {
|
||||
return serverBrand;
|
||||
}
|
||||
|
||||
public void sendHello() {
|
||||
ClientPacketSink activeSink = sink;
|
||||
if (activeSink == null) {
|
||||
return;
|
||||
}
|
||||
byte[] frame = IrisMessageCodec.encode(new IrisMessage.ClientHello(IrisProtocol.PROTOCOL_VERSION, CLIENT_CAPABILITIES));
|
||||
state = State.AWAITING_HELLO;
|
||||
activeSink.send(frame);
|
||||
}
|
||||
|
||||
public void onServerHello(IrisMessage.ServerHello hello) {
|
||||
this.serverCapabilities = hello.capabilities();
|
||||
this.irisActive = hello.irisActive();
|
||||
this.serverBrand = hello.serverBrand();
|
||||
this.state = State.READY;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.state = State.IDLE;
|
||||
this.serverCapabilities = 0L;
|
||||
this.irisActive = false;
|
||||
this.serverBrand = "";
|
||||
}
|
||||
|
||||
public enum State {
|
||||
IDLE,
|
||||
AWAITING_HELLO,
|
||||
READY
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import art.arcane.iris.spi.protocol.IrisMessageCodec;
|
||||
import art.arcane.iris.spi.protocol.IrisProtocol;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
public final class IrisClientTileCache {
|
||||
private static final int MAX_CACHED_TILES = 256;
|
||||
private static final long REQUEST_RETRY_MILLIS = 3000L;
|
||||
private static final int REQUESTS_PER_SECOND = IrisProtocol.MAX_VISION_TILE_REQUESTS_PER_SECOND;
|
||||
|
||||
private final ClientPacketSink sink;
|
||||
private final LongSupplier clock;
|
||||
private final IrisTileAssembler assembler;
|
||||
private final LinkedHashMap<IrisTileKey, IrisTileImage> cache;
|
||||
private final Map<IrisTileKey, Long> pending;
|
||||
private final Deque<IrisTileKey> queue;
|
||||
private final Set<IrisTileKey> queued;
|
||||
private long windowStartMillis;
|
||||
private int sentInWindow;
|
||||
|
||||
public IrisClientTileCache(ClientPacketSink sink, LongSupplier clock) {
|
||||
this.sink = sink;
|
||||
this.clock = clock;
|
||||
this.assembler = new IrisTileAssembler();
|
||||
this.cache = new LinkedHashMap<>(64, 0.75f, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<IrisTileKey, IrisTileImage> eldest) {
|
||||
return size() > MAX_CACHED_TILES;
|
||||
}
|
||||
};
|
||||
this.pending = new HashMap<>();
|
||||
this.queue = new ArrayDeque<>();
|
||||
this.queued = new HashSet<>();
|
||||
this.windowStartMillis = 0L;
|
||||
this.sentInWindow = 0;
|
||||
}
|
||||
|
||||
public synchronized void onVisionTile(IrisMessage.VisionTile tile) {
|
||||
IrisTileImage image = assembler.add(tile);
|
||||
if (image == null) {
|
||||
return;
|
||||
}
|
||||
IrisTileKey key = new IrisTileKey(tile.tileX(), tile.tileZ(), tile.zoomLevel());
|
||||
cache.put(key, image);
|
||||
pending.remove(key);
|
||||
queued.remove(key);
|
||||
}
|
||||
|
||||
public synchronized IrisTileImage get(IrisTileKey key) {
|
||||
return cache.get(key);
|
||||
}
|
||||
|
||||
public synchronized void resetRequestQueue() {
|
||||
queue.clear();
|
||||
queued.clear();
|
||||
}
|
||||
|
||||
public synchronized void request(IrisTileKey key) {
|
||||
if (cache.containsKey(key)) {
|
||||
return;
|
||||
}
|
||||
long now = clock.getAsLong();
|
||||
Long lastRequest = pending.get(key);
|
||||
if (lastRequest != null && now - lastRequest < REQUEST_RETRY_MILLIS) {
|
||||
return;
|
||||
}
|
||||
if (queued.add(key)) {
|
||||
queue.addLast(key);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void pump() {
|
||||
long now = clock.getAsLong();
|
||||
if (now - windowStartMillis >= 1000L) {
|
||||
windowStartMillis = now;
|
||||
sentInWindow = 0;
|
||||
}
|
||||
while (sentInWindow < REQUESTS_PER_SECOND && !queue.isEmpty()) {
|
||||
IrisTileKey key = queue.pollFirst();
|
||||
queued.remove(key);
|
||||
if (cache.containsKey(key)) {
|
||||
continue;
|
||||
}
|
||||
Long lastRequest = pending.get(key);
|
||||
if (lastRequest != null && now - lastRequest < REQUEST_RETRY_MILLIS) {
|
||||
continue;
|
||||
}
|
||||
sink.send(IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(key.tileX(), key.tileZ(), key.zoom())));
|
||||
pending.put(key, now);
|
||||
sentInWindow++;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
assembler.clear();
|
||||
cache.clear();
|
||||
pending.clear();
|
||||
queue.clear();
|
||||
queued.clear();
|
||||
sentInWindow = 0;
|
||||
windowStartMillis = 0L;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
|
||||
public final class IrisClientToasts {
|
||||
private static final int MAX_PENDING = 4;
|
||||
|
||||
private final ArrayDeque<Pending> pending;
|
||||
|
||||
public IrisClientToasts() {
|
||||
this.pending = new ArrayDeque<>();
|
||||
}
|
||||
|
||||
public void enqueue(int kind, String title, String body) {
|
||||
Pending entry = new Pending(kind, normalize(title), normalize(body));
|
||||
synchronized (pending) {
|
||||
while (pending.size() >= MAX_PENDING) {
|
||||
pending.pollFirst();
|
||||
}
|
||||
pending.addLast(entry);
|
||||
}
|
||||
}
|
||||
|
||||
public void enqueueHotload(String packKey, int changedFiles, boolean failed, String message) {
|
||||
int kind = failed ? IrisMessage.Toast.KIND_ERROR : IrisMessage.Toast.KIND_SUCCESS;
|
||||
enqueue(kind, "Studio Hotload", hotloadBody(packKey, changedFiles, failed, message));
|
||||
}
|
||||
|
||||
public Pending poll() {
|
||||
synchronized (pending) {
|
||||
return pending.pollFirst();
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
synchronized (pending) {
|
||||
pending.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalize(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private static String hotloadBody(String packKey, int changedFiles, boolean failed, String message) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
String pack = normalize(packKey);
|
||||
if (!pack.isEmpty()) {
|
||||
builder.append(pack);
|
||||
}
|
||||
if (changedFiles != 0) {
|
||||
append(builder, changedFiles + (changedFiles == 1 ? " file" : " files"));
|
||||
}
|
||||
String text = normalize(message);
|
||||
if (!text.isEmpty()) {
|
||||
append(builder, text);
|
||||
}
|
||||
if (builder.isEmpty()) {
|
||||
return failed ? "reload failed" : "reloaded";
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static void append(StringBuilder builder, String part) {
|
||||
if (!builder.isEmpty()) {
|
||||
builder.append(" ");
|
||||
}
|
||||
builder.append(part);
|
||||
}
|
||||
|
||||
public record Pending(int kind, String title, String body) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
|
||||
public final class IrisPregenHud {
|
||||
private static final int PANEL_COLOR = 0xC0101010;
|
||||
private static final int TITLE_COLOR = 0xFF66BB6A;
|
||||
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
||||
private static final int MUTED_COLOR = 0xFFD7D7D7;
|
||||
private static final int BAR_BACK_COLOR = 0xFF2B2B2B;
|
||||
private static final int BAR_RUNNING_COLOR = 0xFF66BB6A;
|
||||
private static final int PAUSED_COLOR = 0xFFFFD54F;
|
||||
private static final int GRID_BACK_COLOR = 0xFF161616;
|
||||
private static final int CELL_PENDING_COLOR = 0xFF3A3A3A;
|
||||
private static final int CELL_GENERATING_COLOR = 0xFFFFD54F;
|
||||
private static final int CELL_DONE_COLOR = 0xFF66BB6A;
|
||||
private static final int ORIGIN_X = 6;
|
||||
private static final int ORIGIN_Y = 6;
|
||||
private static final int PADDING = 4;
|
||||
private static final int ROW_GAP = 2;
|
||||
private static final int BAR_HEIGHT = 6;
|
||||
private static final int MIN_WIDTH = 130;
|
||||
private static final int MINIMAP_MAX_PX = 64;
|
||||
private static final int MINIMAP_MAX_CELL = 6;
|
||||
private static final int MINIMAP_GAP = 4;
|
||||
|
||||
private IrisPregenHud() {
|
||||
}
|
||||
|
||||
public static void render(GuiGraphicsExtractor graphics) {
|
||||
if (!IrisClient.hudVisible()) {
|
||||
return;
|
||||
}
|
||||
IrisMessage.PregenProgress progress = IrisClient.pregen().active();
|
||||
if (progress == null) {
|
||||
return;
|
||||
}
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
if (minecraft == null || minecraft.player == null) {
|
||||
return;
|
||||
}
|
||||
Font font = minecraft.font;
|
||||
boolean paused = progress.state() == IrisMessage.PregenProgress.STATE_PAUSED;
|
||||
double percent = progress.chunksTotal() > 0L
|
||||
? clampPercent((double) progress.chunksDone() / (double) progress.chunksTotal() * 100.0D)
|
||||
: 0.0D;
|
||||
String title = "Iris Pregen";
|
||||
String stats = String.format("%,d / %,d (%.1f%%)", progress.chunksDone(), progress.chunksTotal(), percent);
|
||||
String tail = paused ? "PAUSED" : rateAndEta(progress);
|
||||
int accent = paused ? PAUSED_COLOR : BAR_RUNNING_COLOR;
|
||||
|
||||
int lineHeight = font.lineHeight;
|
||||
int contentWidth = Math.max(MIN_WIDTH, Math.max(font.width(title), Math.max(font.width(stats), font.width(tail))));
|
||||
int contentHeight = lineHeight * 3 + ROW_GAP * 3 + BAR_HEIGHT;
|
||||
|
||||
IrisClientRegionMap regionMap = IrisClient.regions();
|
||||
IrisClientRegionMap.Bounds bounds = regionMap.hasData() ? regionMap.bounds() : null;
|
||||
boolean showMap = bounds != null;
|
||||
int cellPx = 0;
|
||||
int gridWidth = 0;
|
||||
int gridHeight = 0;
|
||||
if (showMap) {
|
||||
int span = Math.max(bounds.regionsWide(), bounds.regionsTall());
|
||||
cellPx = Math.max(1, Math.min(MINIMAP_MAX_CELL, MINIMAP_MAX_PX / span));
|
||||
gridWidth = Math.min(MINIMAP_MAX_PX, bounds.regionsWide() * cellPx);
|
||||
gridHeight = Math.min(MINIMAP_MAX_PX, bounds.regionsTall() * cellPx);
|
||||
}
|
||||
|
||||
int panelWidth = showMap ? Math.max(contentWidth, gridWidth) : contentWidth;
|
||||
int panelHeight = showMap ? contentHeight + MINIMAP_GAP + gridHeight : contentHeight;
|
||||
|
||||
graphics.fill(ORIGIN_X - PADDING, ORIGIN_Y - PADDING, ORIGIN_X + panelWidth + PADDING, ORIGIN_Y + panelHeight + PADDING, PANEL_COLOR);
|
||||
|
||||
int cursorY = ORIGIN_Y;
|
||||
graphics.text(font, title, ORIGIN_X, cursorY, TITLE_COLOR);
|
||||
cursorY += lineHeight + ROW_GAP;
|
||||
graphics.text(font, stats, ORIGIN_X, cursorY, TEXT_COLOR);
|
||||
cursorY += lineHeight + ROW_GAP;
|
||||
|
||||
int fillWidth = (int) Math.round(contentWidth * (percent / 100.0D));
|
||||
graphics.fill(ORIGIN_X, cursorY, ORIGIN_X + contentWidth, cursorY + BAR_HEIGHT, BAR_BACK_COLOR);
|
||||
if (fillWidth > 0) {
|
||||
graphics.fill(ORIGIN_X, cursorY, ORIGIN_X + fillWidth, cursorY + BAR_HEIGHT, accent);
|
||||
}
|
||||
cursorY += BAR_HEIGHT + ROW_GAP;
|
||||
|
||||
graphics.text(font, tail, ORIGIN_X, cursorY, paused ? PAUSED_COLOR : MUTED_COLOR);
|
||||
|
||||
if (showMap) {
|
||||
renderMinimap(graphics, regionMap, bounds, cellPx, gridWidth, gridHeight, ORIGIN_Y + contentHeight + MINIMAP_GAP);
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderMinimap(GuiGraphicsExtractor graphics, IrisClientRegionMap regionMap, IrisClientRegionMap.Bounds bounds, int cellPx, int gridWidth, int gridHeight, int gridTop) {
|
||||
int mapLeft = ORIGIN_X;
|
||||
int mapRight = mapLeft + gridWidth;
|
||||
int mapBottom = gridTop + gridHeight;
|
||||
graphics.fill(mapLeft, gridTop, mapRight, mapBottom, GRID_BACK_COLOR);
|
||||
int minRegionX = bounds.minRegionX();
|
||||
int minRegionZ = bounds.minRegionZ();
|
||||
regionMap.forEachCell((regionX, regionZ, state) -> {
|
||||
int px = mapLeft + (regionX - minRegionX) * cellPx;
|
||||
int py = gridTop + (regionZ - minRegionZ) * cellPx;
|
||||
if (px + cellPx > mapRight || py + cellPx > mapBottom) {
|
||||
return;
|
||||
}
|
||||
graphics.fill(px, py, px + cellPx, py + cellPx, cellColor(state));
|
||||
});
|
||||
}
|
||||
|
||||
private static int cellColor(int state) {
|
||||
return switch (state) {
|
||||
case IrisMessage.PregenRegionDelta.STATE_DONE -> CELL_DONE_COLOR;
|
||||
case IrisMessage.PregenRegionDelta.STATE_GENERATING -> CELL_GENERATING_COLOR;
|
||||
default -> CELL_PENDING_COLOR;
|
||||
};
|
||||
}
|
||||
|
||||
private static String rateAndEta(IrisMessage.PregenProgress progress) {
|
||||
String rate = String.format("%,.0f/s", progress.chunksPerSecond());
|
||||
if (progress.etaMillis() > 0L) {
|
||||
return rate + " ETA " + formatDuration(progress.etaMillis());
|
||||
}
|
||||
return rate;
|
||||
}
|
||||
|
||||
private static String formatDuration(long etaMillis) {
|
||||
long totalSeconds = etaMillis / 1000L;
|
||||
long hours = totalSeconds / 3600L;
|
||||
long minutes = (totalSeconds % 3600L) / 60L;
|
||||
long seconds = totalSeconds % 60L;
|
||||
if (hours > 0L) {
|
||||
return hours + "h " + minutes + "m";
|
||||
}
|
||||
if (minutes > 0L) {
|
||||
return minutes + "m " + seconds + "s";
|
||||
}
|
||||
return seconds + "s";
|
||||
}
|
||||
|
||||
private static double clampPercent(double value) {
|
||||
if (value < 0.0D) {
|
||||
return 0.0D;
|
||||
}
|
||||
if (value > 100.0D) {
|
||||
return 100.0D;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
final class IrisTileAssembler {
|
||||
private final Map<IrisTileKey, Partial> partials;
|
||||
|
||||
IrisTileAssembler() {
|
||||
this.partials = new HashMap<>();
|
||||
}
|
||||
|
||||
IrisTileImage add(IrisMessage.VisionTile tile) {
|
||||
int chunkCount = tile.chunkCount();
|
||||
int chunkIndex = tile.chunkIndex();
|
||||
if (chunkCount <= 0 || chunkIndex < 0 || chunkIndex >= chunkCount || tile.data() == null) {
|
||||
return null;
|
||||
}
|
||||
IrisTileKey key = new IrisTileKey(tile.tileX(), tile.tileZ(), tile.zoomLevel());
|
||||
Partial partial = partials.get(key);
|
||||
if (partial == null || tile.sequence() > partial.sequence() || partial.chunkCount() != chunkCount) {
|
||||
if (partial != null && tile.sequence() < partial.sequence()) {
|
||||
return null;
|
||||
}
|
||||
partial = new Partial(tile.sequence(), chunkCount);
|
||||
partials.put(key, partial);
|
||||
} else if (tile.sequence() < partial.sequence()) {
|
||||
return null;
|
||||
}
|
||||
if (!partial.accept(chunkIndex, tile.data())) {
|
||||
return null;
|
||||
}
|
||||
partials.remove(key);
|
||||
return IrisTileCodec.decode(partial.concat());
|
||||
}
|
||||
|
||||
void clear() {
|
||||
partials.clear();
|
||||
}
|
||||
|
||||
private static final class Partial {
|
||||
private final int sequence;
|
||||
private final int chunkCount;
|
||||
private final byte[][] chunks;
|
||||
private int received;
|
||||
|
||||
Partial(int sequence, int chunkCount) {
|
||||
this.sequence = sequence;
|
||||
this.chunkCount = chunkCount;
|
||||
this.chunks = new byte[chunkCount][];
|
||||
this.received = 0;
|
||||
}
|
||||
|
||||
int sequence() {
|
||||
return sequence;
|
||||
}
|
||||
|
||||
int chunkCount() {
|
||||
return chunkCount;
|
||||
}
|
||||
|
||||
boolean accept(int chunkIndex, byte[] data) {
|
||||
if (chunks[chunkIndex] == null) {
|
||||
chunks[chunkIndex] = data;
|
||||
received++;
|
||||
}
|
||||
return received >= chunkCount;
|
||||
}
|
||||
|
||||
byte[] concat() {
|
||||
int total = 0;
|
||||
for (byte[] chunk : chunks) {
|
||||
total += chunk.length;
|
||||
}
|
||||
byte[] blob = new byte[total];
|
||||
int position = 0;
|
||||
for (byte[] chunk : chunks) {
|
||||
System.arraycopy(chunk, 0, blob, position, chunk.length);
|
||||
position += chunk.length;
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.zip.DataFormatException;
|
||||
import java.util.zip.Inflater;
|
||||
|
||||
public final class IrisTileCodec {
|
||||
public static final int MODE_RAW_RGB = 0;
|
||||
public static final int MODE_PALETTE = 1;
|
||||
private static final int MAX_DIMENSION = 512;
|
||||
private static final int OPAQUE = 0xFF000000;
|
||||
|
||||
private IrisTileCodec() {
|
||||
}
|
||||
|
||||
public static IrisTileImage decode(byte[] deflatedBlob) {
|
||||
if (deflatedBlob == null || deflatedBlob.length == 0) {
|
||||
return null;
|
||||
}
|
||||
byte[] raw = inflate(deflatedBlob);
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(raw))) {
|
||||
int width = in.readInt();
|
||||
int height = in.readInt();
|
||||
if (width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
|
||||
return null;
|
||||
}
|
||||
int mode = in.readUnsignedByte();
|
||||
int[] argb = new int[width * height];
|
||||
return switch (mode) {
|
||||
case MODE_PALETTE -> decodePalette(in, width, height, argb);
|
||||
case MODE_RAW_RGB -> decodeRaw(in, width, height, argb);
|
||||
default -> null;
|
||||
};
|
||||
} catch (IOException failure) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisTileImage decodePalette(DataInputStream in, int width, int height, int[] argb) throws IOException {
|
||||
int paletteSize = in.readInt();
|
||||
if (paletteSize <= 0 || paletteSize > 256) {
|
||||
return null;
|
||||
}
|
||||
int[] palette = new int[paletteSize];
|
||||
for (int index = 0; index < paletteSize; index++) {
|
||||
int red = in.readUnsignedByte();
|
||||
int green = in.readUnsignedByte();
|
||||
int blue = in.readUnsignedByte();
|
||||
palette[index] = OPAQUE | red << 16 | green << 8 | blue;
|
||||
}
|
||||
for (int pixel = 0; pixel < argb.length; pixel++) {
|
||||
int paletteIndex = in.readUnsignedByte();
|
||||
if (paletteIndex >= paletteSize) {
|
||||
return null;
|
||||
}
|
||||
argb[pixel] = palette[paletteIndex];
|
||||
}
|
||||
return new IrisTileImage(width, height, argb);
|
||||
}
|
||||
|
||||
private static IrisTileImage decodeRaw(DataInputStream in, int width, int height, int[] argb) throws IOException {
|
||||
for (int pixel = 0; pixel < argb.length; pixel++) {
|
||||
int red = in.readUnsignedByte();
|
||||
int green = in.readUnsignedByte();
|
||||
int blue = in.readUnsignedByte();
|
||||
argb[pixel] = OPAQUE | red << 16 | green << 8 | blue;
|
||||
}
|
||||
return new IrisTileImage(width, height, argb);
|
||||
}
|
||||
|
||||
private static byte[] inflate(byte[] input) {
|
||||
Inflater inflater = new Inflater();
|
||||
inflater.setInput(input);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(Math.max(64, input.length * 2));
|
||||
byte[] buffer = new byte[8192];
|
||||
try {
|
||||
while (!inflater.finished()) {
|
||||
int produced = inflater.inflate(buffer);
|
||||
if (produced == 0) {
|
||||
if (inflater.finished() || inflater.needsInput() || inflater.needsDictionary()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
out.write(buffer, 0, produced);
|
||||
}
|
||||
} catch (DataFormatException malformed) {
|
||||
return null;
|
||||
} finally {
|
||||
inflater.end();
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
public record IrisTileImage(int width, int height, int[] argb) {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
public record IrisTileKey(int tileX, int tileZ, int zoom) {
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.toasts.SystemToast;
|
||||
import net.minecraft.client.gui.components.toasts.ToastManager;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
public final class IrisToastPresenter {
|
||||
private IrisToastPresenter() {
|
||||
}
|
||||
|
||||
public static void pump() {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
if (minecraft == null || minecraft.gui == null) {
|
||||
return;
|
||||
}
|
||||
ToastManager manager = minecraft.gui.toastManager();
|
||||
IrisClientToasts toasts = IrisClient.toasts();
|
||||
IrisClientToasts.Pending next = toasts.poll();
|
||||
while (next != null) {
|
||||
SystemToast.addOrUpdate(manager, tokenFor(next.kind()), Component.literal(next.title()), Component.literal(next.body()));
|
||||
next = toasts.poll();
|
||||
}
|
||||
}
|
||||
|
||||
private static SystemToast.SystemToastId tokenFor(int kind) {
|
||||
return switch (kind) {
|
||||
case IrisMessage.Toast.KIND_SUCCESS -> SystemToast.SystemToastId.WORLD_BACKUP;
|
||||
case IrisMessage.Toast.KIND_WARNING -> SystemToast.SystemToastId.UNSECURE_SERVER_WARNING;
|
||||
case IrisMessage.Toast.KIND_ERROR -> SystemToast.SystemToastId.PACK_LOAD_FAILURE;
|
||||
default -> SystemToast.SystemToastId.PERIODIC_NOTIFICATION;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.client.renderer.texture.TextureManager;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class IrisVisionScreen extends Screen {
|
||||
private static final int TILE_PIXELS = 128;
|
||||
private static final int MIN_ZOOM = 0;
|
||||
private static final int MAX_ZOOM = 8;
|
||||
private static final int DEFAULT_ZOOM = 2;
|
||||
private static final int MAX_TEXTURES = 220;
|
||||
private static final int BACKGROUND_COLOR = 0xF00B0E14;
|
||||
private static final int PLACEHOLDER_COLOR = 0xFF161A22;
|
||||
private static final int GRID_COLOR = 0x33FFFFFF;
|
||||
private static final int HEADER_COLOR = 0xC0101010;
|
||||
private static final int TITLE_COLOR = 0xFF66BB6A;
|
||||
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
||||
private static final int MUTED_COLOR = 0xFFB9B9B9;
|
||||
private static final int PLAYER_FILL_COLOR = 0xFFFFDD33;
|
||||
private static final int PLAYER_BORDER_COLOR = 0xFF101010;
|
||||
private static final int MARKER_COLOR = 0xFF4FC3F7;
|
||||
private static final int MARKER_LABEL_BG = 0xE0101010;
|
||||
|
||||
private final Map<IrisTileKey, TileTexture> textures;
|
||||
private double centerBlockX;
|
||||
private double centerBlockZ;
|
||||
private int zoom;
|
||||
private boolean initialized;
|
||||
private String renderedDimensionKey;
|
||||
|
||||
public IrisVisionScreen() {
|
||||
super(Component.literal("Iris Vision"));
|
||||
this.textures = new LinkedHashMap<>(64, 0.75f, true);
|
||||
this.centerBlockX = 0.0D;
|
||||
this.centerBlockZ = 0.0D;
|
||||
this.zoom = DEFAULT_ZOOM;
|
||||
this.initialized = false;
|
||||
this.renderedDimensionKey = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
if (!initialized) {
|
||||
centerOnPlayer();
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPauseScreen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
graphics.fill(0, 0, width, height, BACKGROUND_COLOR);
|
||||
IrisClientSession session = IrisClient.session();
|
||||
if (!session.isReady()) {
|
||||
drawCentered(graphics, "Connecting to Iris server...", MUTED_COLOR);
|
||||
drawHeader(graphics, "Iris Vision", "not connected");
|
||||
return;
|
||||
}
|
||||
IrisMessage.DimensionStatus status = IrisClient.dimension().status();
|
||||
if (!IrisClient.visionAvailable() || status == null) {
|
||||
drawCentered(graphics, "Not an Iris world", MUTED_COLOR);
|
||||
drawHeader(graphics, "Iris Vision", status == null ? "no dimension data" : label(status));
|
||||
return;
|
||||
}
|
||||
syncWorld(status);
|
||||
renderTiles(graphics, mouseX, mouseY, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseDragged(MouseButtonEvent event, double dragX, double dragY) {
|
||||
int blocksPerPixel = 1 << zoom;
|
||||
centerBlockX -= dragX * blocksPerPixel;
|
||||
centerBlockZ -= dragY * blocksPerPixel;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
|
||||
if (scrollY == 0.0D) {
|
||||
return false;
|
||||
}
|
||||
int previousBlocksPerPixel = 1 << zoom;
|
||||
double anchorBlockX = centerBlockX + (mouseX - width / 2.0D) * previousBlocksPerPixel;
|
||||
double anchorBlockZ = centerBlockZ + (mouseY - height / 2.0D) * previousBlocksPerPixel;
|
||||
int updated = zoom + (scrollY > 0.0D ? -1 : 1);
|
||||
zoom = Math.max(MIN_ZOOM, Math.min(updated, MAX_ZOOM));
|
||||
int newBlocksPerPixel = 1 << zoom;
|
||||
centerBlockX = anchorBlockX - (mouseX - width / 2.0D) * newBlocksPerPixel;
|
||||
centerBlockZ = anchorBlockZ - (mouseY - height / 2.0D) * newBlocksPerPixel;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed() {
|
||||
releaseTextures();
|
||||
super.removed();
|
||||
}
|
||||
|
||||
private void renderTiles(GuiGraphicsExtractor graphics, int mouseX, int mouseY, IrisMessage.DimensionStatus status) {
|
||||
int blocksPerPixel = 1 << zoom;
|
||||
int tileSpanBlocks = TILE_PIXELS * blocksPerPixel;
|
||||
int originX = (int) Math.floor(width / 2.0D - centerBlockX / blocksPerPixel);
|
||||
int originY = (int) Math.floor(height / 2.0D - centerBlockZ / blocksPerPixel);
|
||||
int minTileX = (int) Math.floor((0.0D - originX) / TILE_PIXELS) - 1;
|
||||
int maxTileX = (int) Math.floor((width - originX) / (double) TILE_PIXELS) + 1;
|
||||
int minTileZ = (int) Math.floor((0.0D - originY) / TILE_PIXELS) - 1;
|
||||
int maxTileZ = (int) Math.floor((height - originY) / (double) TILE_PIXELS) + 1;
|
||||
int centerTileX = Math.floorDiv((int) Math.floor(centerBlockX), tileSpanBlocks);
|
||||
int centerTileZ = Math.floorDiv((int) Math.floor(centerBlockZ), tileSpanBlocks);
|
||||
|
||||
IrisClientTileCache cache = IrisClient.tiles();
|
||||
cache.resetRequestQueue();
|
||||
List<IrisTileKey> missing = new ArrayList<>();
|
||||
|
||||
for (int tileX = minTileX; tileX <= maxTileX; tileX++) {
|
||||
for (int tileZ = minTileZ; tileZ <= maxTileZ; tileZ++) {
|
||||
int screenX = originX + tileX * TILE_PIXELS;
|
||||
int screenY = originY + tileZ * TILE_PIXELS;
|
||||
IrisTileKey key = new IrisTileKey(tileX, tileZ, zoom);
|
||||
IrisTileImage image = cache.get(key);
|
||||
if (image == null) {
|
||||
graphics.fill(screenX, screenY, screenX + TILE_PIXELS, screenY + TILE_PIXELS, PLACEHOLDER_COLOR);
|
||||
missing.add(key);
|
||||
continue;
|
||||
}
|
||||
Identifier texture = ensureTexture(key, image);
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, texture, screenX, screenY, 0.0F, 0.0F, TILE_PIXELS, TILE_PIXELS, TILE_PIXELS, TILE_PIXELS);
|
||||
}
|
||||
}
|
||||
|
||||
requestMissing(cache, missing, centerTileX, centerTileZ);
|
||||
cache.pump();
|
||||
evictTextures();
|
||||
|
||||
drawMarkers(graphics, mouseX, mouseY, minTileX, maxTileX, minTileZ, maxTileZ, originX, originY, blocksPerPixel);
|
||||
drawPlayer(graphics, blocksPerPixel);
|
||||
drawHeader(graphics, "Iris Vision", label(status) + " zoom " + zoom + " x" + (long) centerBlockX + " z" + (long) centerBlockZ);
|
||||
drawFooter(graphics, "Drag to pan Scroll to zoom Esc to close");
|
||||
}
|
||||
|
||||
private void requestMissing(IrisClientTileCache cache, List<IrisTileKey> missing, int centerTileX, int centerTileZ) {
|
||||
missing.sort((left, right) -> Long.compare(distanceSquared(left, centerTileX, centerTileZ), distanceSquared(right, centerTileX, centerTileZ)));
|
||||
for (IrisTileKey key : missing) {
|
||||
cache.request(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawMarkers(GuiGraphicsExtractor graphics, int mouseX, int mouseY, int minTileX, int maxTileX, int minTileZ, int maxTileZ, int originX, int originY, int blocksPerPixel) {
|
||||
IrisClientMarkers markers = IrisClient.markers();
|
||||
String hoverLabel = null;
|
||||
int hoverX = 0;
|
||||
int hoverY = 0;
|
||||
for (int tileX = minTileX; tileX <= maxTileX; tileX++) {
|
||||
for (int tileZ = minTileZ; tileZ <= maxTileZ; tileZ++) {
|
||||
List<IrisMessage.VisionMarkers.Marker> tileMarkers = markers.forTile(new IrisTileKey(tileX, tileZ, zoom));
|
||||
if (tileMarkers == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisMessage.VisionMarkers.Marker marker : tileMarkers) {
|
||||
int screenX = originX + (int) Math.floor((double) marker.blockX() / blocksPerPixel);
|
||||
int screenY = originY + (int) Math.floor((double) marker.blockZ() / blocksPerPixel);
|
||||
graphics.fill(screenX - 2, screenY - 2, screenX + 2, screenY + 2, MARKER_COLOR);
|
||||
if (Math.abs(mouseX - screenX) <= 4 && Math.abs(mouseY - screenY) <= 4 && marker.label() != null && !marker.label().isEmpty()) {
|
||||
hoverLabel = marker.label();
|
||||
hoverX = screenX;
|
||||
hoverY = screenY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hoverLabel != null) {
|
||||
int labelWidth = font.width(hoverLabel);
|
||||
graphics.fill(hoverX + 6, hoverY - 6, hoverX + 12 + labelWidth, hoverY + font.lineHeight, MARKER_LABEL_BG);
|
||||
graphics.text(font, hoverLabel, hoverX + 9, hoverY - 4, TEXT_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawPlayer(GuiGraphicsExtractor graphics, int blocksPerPixel) {
|
||||
LocalPlayer player = Minecraft.getInstance().player;
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
int screenX = (int) Math.round(width / 2.0D + (player.getBlockX() - centerBlockX) / blocksPerPixel);
|
||||
int screenY = (int) Math.round(height / 2.0D + (player.getBlockZ() - centerBlockZ) / blocksPerPixel);
|
||||
graphics.fill(screenX - 4, screenY - 4, screenX + 4, screenY + 4, PLAYER_BORDER_COLOR);
|
||||
graphics.fill(screenX - 3, screenY - 3, screenX + 3, screenY + 3, PLAYER_FILL_COLOR);
|
||||
}
|
||||
|
||||
private void drawHeader(GuiGraphicsExtractor graphics, String title, String detail) {
|
||||
int lineHeight = font.lineHeight;
|
||||
int headerHeight = lineHeight + 8;
|
||||
graphics.fill(0, 0, width, headerHeight, HEADER_COLOR);
|
||||
graphics.text(font, title, 8, 4, TITLE_COLOR);
|
||||
graphics.text(font, detail, 12 + font.width(title), 4, MUTED_COLOR);
|
||||
}
|
||||
|
||||
private void drawFooter(GuiGraphicsExtractor graphics, String hint) {
|
||||
int lineHeight = font.lineHeight;
|
||||
int footerTop = height - lineHeight - 8;
|
||||
graphics.fill(0, footerTop, width, height, HEADER_COLOR);
|
||||
graphics.text(font, hint, 8, footerTop + 4, MUTED_COLOR);
|
||||
}
|
||||
|
||||
private void drawCentered(GuiGraphicsExtractor graphics, String text, int color) {
|
||||
graphics.text(font, text, (width - font.width(text)) / 2, height / 2 - font.lineHeight / 2, color);
|
||||
}
|
||||
|
||||
private Identifier ensureTexture(IrisTileKey key, IrisTileImage image) {
|
||||
TileTexture existing = textures.get(key);
|
||||
if (existing != null) {
|
||||
return existing.id();
|
||||
}
|
||||
int tileWidth = image.width();
|
||||
int tileHeight = image.height();
|
||||
int[] argb = image.argb();
|
||||
NativeImage nativeImage = new NativeImage(NativeImage.Format.RGBA, tileWidth, tileHeight, false);
|
||||
for (int y = 0; y < tileHeight; y++) {
|
||||
int row = y * tileWidth;
|
||||
for (int x = 0; x < tileWidth; x++) {
|
||||
nativeImage.setPixelABGR(x, y, toAbgr(argb[row + x]));
|
||||
}
|
||||
}
|
||||
DynamicTexture texture = new DynamicTexture(() -> "iris_vision_tile", nativeImage);
|
||||
Identifier id = Identifier.fromNamespaceAndPath("irisworldgen", texturePath(key));
|
||||
Minecraft.getInstance().getTextureManager().register(id, texture);
|
||||
textures.put(key, new TileTexture(id, texture));
|
||||
return id;
|
||||
}
|
||||
|
||||
private void evictTextures() {
|
||||
if (textures.size() <= MAX_TEXTURES) {
|
||||
return;
|
||||
}
|
||||
TextureManager manager = Minecraft.getInstance().getTextureManager();
|
||||
Iterator<Map.Entry<IrisTileKey, TileTexture>> iterator = textures.entrySet().iterator();
|
||||
while (textures.size() > MAX_TEXTURES && iterator.hasNext()) {
|
||||
Map.Entry<IrisTileKey, TileTexture> entry = iterator.next();
|
||||
manager.release(entry.getValue().id());
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseTextures() {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
if (minecraft != null) {
|
||||
TextureManager manager = minecraft.getTextureManager();
|
||||
for (TileTexture texture : textures.values()) {
|
||||
manager.release(texture.id());
|
||||
}
|
||||
}
|
||||
textures.clear();
|
||||
}
|
||||
|
||||
private void syncWorld(IrisMessage.DimensionStatus status) {
|
||||
if (renderedDimensionKey == null) {
|
||||
renderedDimensionKey = status.dimensionKey();
|
||||
return;
|
||||
}
|
||||
if (!renderedDimensionKey.equals(status.dimensionKey())) {
|
||||
renderedDimensionKey = status.dimensionKey();
|
||||
releaseTextures();
|
||||
centerOnPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
private void centerOnPlayer() {
|
||||
LocalPlayer player = Minecraft.getInstance().player;
|
||||
if (player != null) {
|
||||
centerBlockX = player.getBlockX();
|
||||
centerBlockZ = player.getBlockZ();
|
||||
}
|
||||
}
|
||||
|
||||
private static long distanceSquared(IrisTileKey key, int centerTileX, int centerTileZ) {
|
||||
long deltaX = (long) key.tileX() - centerTileX;
|
||||
long deltaZ = (long) key.tileZ() - centerTileZ;
|
||||
return deltaX * deltaX + deltaZ * deltaZ;
|
||||
}
|
||||
|
||||
private static int toAbgr(int argb) {
|
||||
int alpha = argb >>> 24 & 0xFF;
|
||||
int red = argb >> 16 & 0xFF;
|
||||
int green = argb >> 8 & 0xFF;
|
||||
int blue = argb & 0xFF;
|
||||
return alpha << 24 | blue << 16 | green << 8 | red;
|
||||
}
|
||||
|
||||
private static String texturePath(IrisTileKey key) {
|
||||
return "vision/t_" + safe(key.tileX()) + "_" + safe(key.tileZ()) + "_" + key.zoom();
|
||||
}
|
||||
|
||||
private static String safe(int value) {
|
||||
return value < 0 ? "m" + (-(long) value) : Integer.toString(value);
|
||||
}
|
||||
|
||||
private static String label(IrisMessage.DimensionStatus status) {
|
||||
String pack = status.packKey() == null || status.packKey().isEmpty() ? "" : " pack " + status.packKey();
|
||||
return status.dimensionKey() + pack;
|
||||
}
|
||||
|
||||
private record TileTexture(Identifier id, DynamicTexture texture) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package art.arcane.iris.client;
|
||||
|
||||
import art.arcane.iris.spi.protocol.IrisMessage;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class IrisWhatOverlay {
|
||||
private static final int PANEL_COLOR = 0xC0101010;
|
||||
private static final int TITLE_COLOR = 0xFF66BB6A;
|
||||
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
||||
private static final int MUTED_COLOR = 0xFFC7C7C7;
|
||||
private static final int PADDING = 4;
|
||||
private static final int ROW_GAP = 2;
|
||||
private static final int CURSOR_OFFSET = 12;
|
||||
|
||||
private IrisWhatOverlay() {
|
||||
}
|
||||
|
||||
public static void render(GuiGraphicsExtractor graphics) {
|
||||
if (!IrisClient.whatVisible() || !IrisClient.cursorAvailable()) {
|
||||
return;
|
||||
}
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
if (minecraft == null || minecraft.player == null) {
|
||||
return;
|
||||
}
|
||||
LocalPlayer player = minecraft.player;
|
||||
int blockX = player.getBlockX();
|
||||
int blockZ = player.getBlockZ();
|
||||
HitResult hit = minecraft.hitResult;
|
||||
if (hit != null && hit.getType() == HitResult.Type.BLOCK && hit instanceof BlockHitResult blockHit) {
|
||||
BlockPos pos = blockHit.getBlockPos();
|
||||
blockX = pos.getX();
|
||||
blockZ = pos.getZ();
|
||||
}
|
||||
IrisClient.cursor().requestFor(blockX, blockZ);
|
||||
|
||||
IrisMessage.CursorInfo info = IrisClient.cursor().latest();
|
||||
List<String> lines = new ArrayList<>();
|
||||
if (info == null) {
|
||||
lines.add("querying " + blockX + ", " + blockZ + "...");
|
||||
} else {
|
||||
lines.add("Biome: " + display(info.biomeKey()));
|
||||
lines.add("Region: " + display(info.regionKey()));
|
||||
if (info.caveBiomeKey() != null && !info.caveBiomeKey().isEmpty()) {
|
||||
lines.add("Cave: " + display(info.caveBiomeKey()));
|
||||
}
|
||||
lines.add("Height: " + info.height() + " (" + info.blockX() + ", " + info.blockZ() + ")");
|
||||
}
|
||||
draw(graphics, minecraft.font, lines);
|
||||
}
|
||||
|
||||
private static void draw(GuiGraphicsExtractor graphics, Font font, List<String> lines) {
|
||||
int lineHeight = font.lineHeight;
|
||||
int contentWidth = font.width("Iris What");
|
||||
for (String line : lines) {
|
||||
contentWidth = Math.max(contentWidth, font.width(line));
|
||||
}
|
||||
int originX = graphics.guiWidth() / 2 + CURSOR_OFFSET;
|
||||
int originY = graphics.guiHeight() / 2 + CURSOR_OFFSET;
|
||||
int rows = lines.size() + 1;
|
||||
int contentHeight = lineHeight * rows + ROW_GAP * rows;
|
||||
graphics.fill(originX - PADDING, originY - PADDING, originX + contentWidth + PADDING, originY + contentHeight + PADDING, PANEL_COLOR);
|
||||
|
||||
int cursorY = originY;
|
||||
graphics.text(font, "Iris What", originX, cursorY, TITLE_COLOR);
|
||||
cursorY += lineHeight + ROW_GAP;
|
||||
for (String line : lines) {
|
||||
graphics.text(font, line, originX, cursorY, line.startsWith("Height") ? MUTED_COLOR : TEXT_COLOR);
|
||||
cursorY += lineHeight + ROW_GAP;
|
||||
}
|
||||
}
|
||||
|
||||
private static String display(String key) {
|
||||
return key == null || key.isEmpty() ? "-" : key;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
import org.gradle.api.tasks.compile.JavaCompile
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'net.fabricmc.fabric-loom' version '1.17.11'
|
||||
alias(libs.plugins.shadow)
|
||||
}
|
||||
|
||||
Properties rootProperties = new Properties()
|
||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
|
||||
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.3'))
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:cdf7e8bb880199734dacd386a6616bda799fcde1'))
|
||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
|
||||
}
|
||||
|
||||
group = 'art.arcane'
|
||||
version = irisVersion
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDir '../modded-common/src/main/java'
|
||||
srcDir '../client-common/src/main/java'
|
||||
}
|
||||
resources {
|
||||
srcDir '../modded-common/src/main/resources'
|
||||
}
|
||||
}
|
||||
test {
|
||||
java {
|
||||
srcDir '../modded-common/src/test/java'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
name = 'FabricMC'
|
||||
url = uri('https://maven.fabricmc.net/')
|
||||
}
|
||||
maven { url = uri('https://repo.codemc.org/repository/maven-public/') }
|
||||
maven { url = uri('https://jitpack.io') }
|
||||
maven { url = uri('https://hub.spigotmc.org/nexus/content/repositories/snapshots/') }
|
||||
}
|
||||
|
||||
configurations {
|
||||
bundle {
|
||||
canBeConsumed = false
|
||||
canBeResolved = true
|
||||
}
|
||||
devBundle {
|
||||
canBeConsumed = false
|
||||
canBeResolved = true
|
||||
}
|
||||
}
|
||||
|
||||
configurations.named('bundle').configure {
|
||||
exclude(group: 'com.google.code.gson')
|
||||
exclude(group: 'com.google.guava')
|
||||
exclude(group: 'commons-io')
|
||||
exclude(group: 'org.apache.commons', module: 'commons-lang3')
|
||||
exclude(group: 'it.unimi.dsi', module: 'fastutil')
|
||||
exclude(group: 'org.slf4j')
|
||||
exclude(group: 'org.apache.logging.log4j')
|
||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||
exclude(group: 'de.crazydev22.slimjar')
|
||||
exclude(group: 'io.github.slimjar')
|
||||
}
|
||||
|
||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||
configurations.runtimeClasspath.extendsFrom(configurations.devBundle)
|
||||
|
||||
configurations.create('jij') {
|
||||
transitive = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
minecraft("com.mojang:minecraft:${minecraftVersion}")
|
||||
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
List<Object> fabricApi = [
|
||||
libs.fabricApi.base,
|
||||
libs.fabricApi.registrySync,
|
||||
libs.fabricApi.resourceLoader,
|
||||
libs.fabricApi.lifecycleEvents,
|
||||
libs.fabricApi.commandApi,
|
||||
libs.fabricApi.eventsInteraction,
|
||||
libs.fabricApi.networking,
|
||||
libs.fabricApi.rendering,
|
||||
libs.fabricApi.keyMapping
|
||||
]
|
||||
fabricApi.each { Object notation ->
|
||||
add('jij', notation)
|
||||
add('implementation', notation)
|
||||
}
|
||||
compileOnly('org.slf4j:slf4j-api:2.0.17')
|
||||
compileOnly(libs.spigot) {
|
||||
transitive = false
|
||||
}
|
||||
|
||||
List<Object> shared = [
|
||||
"art.arcane:core:${irisVersion}".toString(),
|
||||
"art.arcane:spi:${irisVersion}".toString(),
|
||||
volmLibCoordinate,
|
||||
libs.paralithic,
|
||||
libs.lru,
|
||||
libs.kotlin.stdlib,
|
||||
libs.kotlin.coroutines,
|
||||
libs.commons.lang,
|
||||
libs.commons.math3,
|
||||
libs.caffeine,
|
||||
libs.lz4,
|
||||
libs.zip,
|
||||
libs.sentry,
|
||||
libs.oshi,
|
||||
libs.byteBuddy.core,
|
||||
libs.byteBuddy.agent
|
||||
]
|
||||
shared.each { Object notation ->
|
||||
add('bundle', notation)
|
||||
add('devBundle', notation)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('compileJava', JavaCompile).configure {
|
||||
options.encoding = 'UTF-8'
|
||||
options.release.set(25)
|
||||
}
|
||||
|
||||
loom {
|
||||
accessWidenerPath = file('src/main/resources/irisworldgen.accesswidener')
|
||||
runs {
|
||||
server {
|
||||
String parity = providers.gradleProperty('irisParity').getOrNull()
|
||||
if (parity != null) {
|
||||
property('iris.parity', parity)
|
||||
}
|
||||
String parityGolden = providers.gradleProperty('irisParityGolden').getOrNull()
|
||||
if (parityGolden != null) {
|
||||
property('iris.parity.golden', parityGolden)
|
||||
}
|
||||
String parityDeep = providers.gradleProperty('irisParityDeep').getOrNull()
|
||||
if (parityDeep != null) {
|
||||
property('iris.parity.deep', parityDeep)
|
||||
}
|
||||
String worldCheck = providers.gradleProperty('irisWorldCheck').getOrNull()
|
||||
if (worldCheck != null) {
|
||||
property('iris.worldcheck', worldCheck)
|
||||
}
|
||||
vmArg('-Xmx8G')
|
||||
programArgs('nogui')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processResources {
|
||||
inputs.property('version', project.version)
|
||||
inputs.property('minecraftVersion', minecraftVersion)
|
||||
filesMatching('fabric.mod.json') {
|
||||
expand('version': project.version, 'minecraftVersion': minecraftVersion)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('shadowJar', ShadowJar).configure {
|
||||
doFirst {
|
||||
delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-fabric.jar"))
|
||||
}
|
||||
archiveFileName.set(irisArtifactName('Fabric', "${minecraftVersion}+${fabricLoaderVersion}"))
|
||||
configurations = [project.configurations.named('bundle').get()]
|
||||
mergeServiceFiles()
|
||||
exclude('META-INF/maven/**')
|
||||
exclude('META-INF/proguard/**')
|
||||
exclude('META-INF/*.SF')
|
||||
exclude('META-INF/*.DSA')
|
||||
exclude('META-INF/*.RSA')
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
|
||||
relocate('io.sentry', 'art.arcane.iris.shadow.sentry')
|
||||
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
|
||||
relocate('oshi', 'art.arcane.iris.shadow.oshi')
|
||||
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
|
||||
exclude('oshi.properties')
|
||||
from(project.configurations.named('jij')) {
|
||||
into('META-INF/jars')
|
||||
rename { String fileName -> fileName.replaceAll(/-[0-9][^-]*\.jar$/, '.jar') }
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('assemble').configure {
|
||||
dependsOn(tasks.named('shadowJar'))
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
org.gradle.daemon=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.caching=true
|
||||
org.gradle.configuration-cache=false
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import java.io.File
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven {
|
||||
name = 'FabricMC'
|
||||
url = uri('https://maven.fabricmc.net/')
|
||||
}
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||
}
|
||||
|
||||
rootProject.name = 'iris-fabric'
|
||||
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
libs {
|
||||
from(files('../../gradle/libs.versions.toml'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasVolmLibSettings(File directory) {
|
||||
new File(directory, 'settings.gradle.kts').exists() || new File(directory, 'settings.gradle').exists()
|
||||
}
|
||||
|
||||
File resolveLocalVolmLibDirectory() {
|
||||
String configuredPath = providers.gradleProperty('localVolmLibDirectory')
|
||||
.orElse(providers.environmentVariable('VOLMLIB_DIR'))
|
||||
.orNull
|
||||
if (configuredPath != null && !configuredPath.isBlank()) {
|
||||
File configuredDirectory = file(configuredPath)
|
||||
if (hasVolmLibSettings(configuredDirectory)) {
|
||||
return configuredDirectory
|
||||
}
|
||||
}
|
||||
|
||||
File currentDirectory = settingsDir
|
||||
while (currentDirectory != null) {
|
||||
File candidate = new File(currentDirectory, 'VolmLib')
|
||||
if (hasVolmLibSettings(candidate)) {
|
||||
return candidate
|
||||
}
|
||||
|
||||
currentDirectory = currentDirectory.parentFile
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
|
||||
boolean useLocalVolmLib = providers.gradleProperty('useLocalVolmLib')
|
||||
.orElse('true')
|
||||
.map { String value -> value.equalsIgnoreCase('true') }
|
||||
.get()
|
||||
File localVolmLibDirectory = resolveLocalVolmLibDirectory()
|
||||
|
||||
if (useLocalVolmLib && localVolmLibDirectory != null) {
|
||||
includeBuild(localVolmLibDirectory) {
|
||||
dependencySubstitution {
|
||||
substitute(module('com.github.VolmitSoftware:VolmLib')).using(project(':shared'))
|
||||
substitute(module('com.github.VolmitSoftware.VolmLib:shared')).using(project(':shared'))
|
||||
substitute(module('com.github.VolmitSoftware.VolmLib:volmlib-shared')).using(project(':shared'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
includeBuild('../..') {
|
||||
dependencySubstitution {
|
||||
substitute(module('art.arcane:core')).using(project(':core'))
|
||||
substitute(module('art.arcane:spi')).using(project(':spi'))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.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.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.fabric;
|
||||
|
||||
import art.arcane.iris.modded.ModdedLoader;
|
||||
import net.fabricmc.api.EnvType;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.fabricmc.loader.api.ModContainer;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class FabricModdedLoader implements ModdedLoader {
|
||||
@Override
|
||||
public String platformName() {
|
||||
return "fabric";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String minecraftVersion() {
|
||||
return FabricLoader.getInstance().getModContainer("minecraft")
|
||||
.map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString())
|
||||
.orElse("unknown");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String modVersion() {
|
||||
return FabricLoader.getInstance().getModContainer("irisworldgen")
|
||||
.map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString())
|
||||
.orElse("unknown");
|
||||
}
|
||||
|
||||
@Override
|
||||
public MinecraftServer currentServer() {
|
||||
Object instance = FabricLoader.getInstance().getGameInstance();
|
||||
return instance instanceof MinecraftServer server ? server : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean clientEnvironment() {
|
||||
return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path configDir() {
|
||||
return FabricLoader.getInstance().getConfigDir();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File modJar() {
|
||||
return FabricLoader.getInstance().getModContainer("irisworldgen")
|
||||
.flatMap((ModContainer container) -> container.getOrigin().getPaths().stream().findFirst())
|
||||
.map((Path p) -> p.toFile())
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.fabric;
|
||||
|
||||
import art.arcane.iris.modded.ModdedIrisPayload;
|
||||
import art.arcane.iris.modded.ModdedProtocolChannel;
|
||||
import art.arcane.iris.modded.ModdedProtocolHandler;
|
||||
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
public final class FabricProtocolNetworking {
|
||||
private FabricProtocolNetworking() {
|
||||
}
|
||||
|
||||
public static void install() {
|
||||
PayloadTypeRegistry.clientboundPlay().register(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC);
|
||||
ServerPlayNetworking.registerGlobalReceiver(ModdedIrisPayload.TYPE,
|
||||
(payload, context) -> ModdedProtocolHandler.onInbound(context.player(), payload.data()));
|
||||
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> ModdedProtocolHandler.onPlayerJoin(handler.player));
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> ModdedProtocolHandler.onPlayerDisconnect(handler.player));
|
||||
ModdedProtocolHandler.bindChannel(new FabricProtocolChannel());
|
||||
}
|
||||
|
||||
private static final class FabricProtocolChannel implements ModdedProtocolChannel {
|
||||
@Override
|
||||
public boolean canReceive(ServerPlayer player) {
|
||||
return ServerPlayNetworking.canSend(player, ModdedIrisPayload.TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(ServerPlayer player, ModdedIrisPayload payload) {
|
||||
ServerPlayNetworking.send(player, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.fabric;
|
||||
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.command.IrisModdedCommands;
|
||||
import art.arcane.iris.modded.command.ModdedWandService;
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.minecraft.commands.CommandBuildContext;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
public final class IrisFabricBootstrap implements ModInitializer {
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
ModdedEngineBootstrap.bootCommon(new FabricModdedLoader(), "Fabric",
|
||||
() -> Registry.register(BuiltInRegistries.CHUNK_GENERATOR, Identifier.fromNamespaceAndPath("irisworldgen", "iris"), IrisModdedChunkGenerator.CODEC));
|
||||
FabricProtocolNetworking.install();
|
||||
ServerLifecycleEvents.SERVER_STARTING.register((MinecraftServer server) -> ModdedEngineBootstrap.start(server));
|
||||
ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> ModdedEngineBootstrap.stop());
|
||||
CommandRegistrationCallback.EVENT.register((CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext buildContext, Commands.CommandSelection selection) -> IrisModdedCommands.register(dispatcher));
|
||||
AttackBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockPos pos, Direction direction) ->
|
||||
ModdedWandService.attackBlock(player, level, hand, pos) ? InteractionResult.SUCCESS : InteractionResult.PASS);
|
||||
UseBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockHitResult hit) ->
|
||||
ModdedWandService.useBlock(player, level, hand, hit.getBlockPos()) ? InteractionResult.SUCCESS : InteractionResult.PASS);
|
||||
ServerTickEvents.END_SERVER_TICK.register((MinecraftServer server) -> {
|
||||
ModdedEngineBootstrap.tick(server);
|
||||
ModdedWandService.serverTick(server);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.fabric;
|
||||
|
||||
import art.arcane.iris.client.IrisClient;
|
||||
import art.arcane.iris.client.IrisClientHud;
|
||||
import art.arcane.iris.client.IrisClientKeybinds;
|
||||
import art.arcane.iris.modded.ModdedIrisPayload;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry;
|
||||
|
||||
public final class IrisFabricClient implements ClientModInitializer {
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
ClientPlayNetworking.registerGlobalReceiver(ModdedIrisPayload.TYPE,
|
||||
(payload, context) -> IrisClient.onInbound(payload.data()));
|
||||
IrisClient.bindSender(frame -> {
|
||||
if (ClientPlayNetworking.canSend(ModdedIrisPayload.TYPE)) {
|
||||
ClientPlayNetworking.send(new ModdedIrisPayload(frame));
|
||||
}
|
||||
});
|
||||
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> IrisClient.onWorldJoin());
|
||||
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> IrisClient.onDisconnect());
|
||||
KeyMappingHelper.registerKeyMapping(IrisClientKeybinds.TOGGLE_HUD);
|
||||
KeyMappingHelper.registerKeyMapping(IrisClientKeybinds.OPEN_MAP);
|
||||
KeyMappingHelper.registerKeyMapping(IrisClientKeybinds.TOGGLE_WHAT);
|
||||
HudElementRegistry.addLast(IrisClient.HUD_ELEMENT_ID, (graphics, delta) -> IrisClientHud.render(graphics));
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> IrisClientKeybinds.pollToggle());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.fabric.mixin;
|
||||
|
||||
import art.arcane.iris.modded.ModdedDeathLoot;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(LivingEntity.class)
|
||||
public class LivingEntityMixin {
|
||||
@Inject(method = "die", at = @At("TAIL"))
|
||||
private void iris$dropIrisDeathLoot(DamageSource damageSource, CallbackInfo info) {
|
||||
ModdedDeathLoot.handle((LivingEntity) (Object) this);
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.fabric.mixin;
|
||||
|
||||
import art.arcane.iris.fabric.FabricForcedDatapackSources;
|
||||
import net.minecraft.server.packs.repository.PackRepository;
|
||||
import net.minecraft.server.packs.repository.ServerPacksSource;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(ServerPacksSource.class)
|
||||
public class ServerPacksSourceMixin {
|
||||
@Inject(method = "createPackRepository(Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;)Lnet/minecraft/server/packs/repository/PackRepository;", at = @At("RETURN"))
|
||||
private static void iris$addForcedDatapackSource(LevelStorageSource.LevelStorageAccess storage, CallbackInfoReturnable<PackRepository> info) {
|
||||
FabricForcedDatapackSources.attach(info.getReturnValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "irisworldgen",
|
||||
"version": "${version}",
|
||||
"name": "Iris",
|
||||
"description": "Iris World Generation Engine (Fabric adapter - native chunk generator, explicit Iris world datapack workflow, engine lifecycle)",
|
||||
"authors": ["Arcane Arts (Volmit Software)"],
|
||||
"contact": {
|
||||
"sources": "https://github.com/VolmitSoftware/Iris"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"environment": "*",
|
||||
"accessWidener": "irisworldgen.accesswidener",
|
||||
"mixins": ["irisworldgen.mixins.json"],
|
||||
"entrypoints": {
|
||||
"main": ["art.arcane.iris.fabric.IrisFabricBootstrap"],
|
||||
"client": ["art.arcane.iris.fabric.IrisFabricClient"]
|
||||
},
|
||||
"jars": [
|
||||
{ "file": "META-INF/jars/fabric-api-base.jar" },
|
||||
{ "file": "META-INF/jars/fabric-registry-sync-v0.jar" },
|
||||
{ "file": "META-INF/jars/fabric-resource-loader-v1.jar" },
|
||||
{ "file": "META-INF/jars/fabric-lifecycle-events-v1.jar" },
|
||||
{ "file": "META-INF/jars/fabric-networking-api-v1.jar" },
|
||||
{ "file": "META-INF/jars/fabric-command-api-v2.jar" },
|
||||
{ "file": "META-INF/jars/fabric-events-interaction-v0.jar" },
|
||||
{ "file": "META-INF/jars/fabric-rendering-v1.jar" },
|
||||
{ "file": "META-INF/jars/fabric-transitive-access-wideners-v1.jar" },
|
||||
{ "file": "META-INF/jars/fabric-key-mapping-api-v1.jar" }
|
||||
],
|
||||
"depends": {
|
||||
"fabricloader": ">=0.19.3",
|
||||
"minecraft": "~${minecraftVersion}",
|
||||
"java": ">=25"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
accessWidener v2 official
|
||||
accessible field net/minecraft/server/MinecraftServer levels Ljava/util/Map;
|
||||
accessible field net/minecraft/server/MinecraftServer executor Ljava/util/concurrent/Executor;
|
||||
accessible field net/minecraft/server/MinecraftServer storageSource Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;
|
||||
accessible field net/minecraft/server/packs/repository/PackRepository sources Ljava/util/Set;
|
||||
mutable field net/minecraft/server/packs/repository/PackRepository sources Ljava/util/Set;
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"required": true,
|
||||
"minVersion": "0.8",
|
||||
"package": "art.arcane.iris.fabric.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": [
|
||||
"LivingEntityMixin",
|
||||
"ServerPacksSourceMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
import org.gradle.api.tasks.compile.JavaCompile
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'net.minecraftforge.gradle' version '7.0.27'
|
||||
alias(libs.plugins.shadow)
|
||||
}
|
||||
|
||||
Properties rootProperties = new Properties()
|
||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
|
||||
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.2-65.0.0'))
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:cdf7e8bb880199734dacd386a6616bda799fcde1'))
|
||||
Closure<String> loaderDisplayVersion = { String loaderVersion ->
|
||||
String coordinatePrefix = "${minecraftVersion}-"
|
||||
if (loaderVersion.startsWith(coordinatePrefix)) {
|
||||
return loaderVersion.substring(coordinatePrefix.length())
|
||||
}
|
||||
|
||||
return loaderVersion
|
||||
}
|
||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
|
||||
}
|
||||
|
||||
group = 'art.arcane'
|
||||
version = irisVersion
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDir '../modded-common/src/main/java'
|
||||
srcDir '../client-common/src/main/java'
|
||||
}
|
||||
resources {
|
||||
srcDir '../modded-common/src/main/resources'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
minecraft.mavenizer(it)
|
||||
maven fg.forgeMaven
|
||||
maven fg.minecraftLibsMaven
|
||||
mavenCentral()
|
||||
maven { url = uri('https://repo.codemc.org/repository/maven-public/') }
|
||||
maven { url = uri('https://jitpack.io') }
|
||||
maven { url = uri('https://hub.spigotmc.org/nexus/content/repositories/snapshots/') }
|
||||
}
|
||||
|
||||
configurations {
|
||||
bundle {
|
||||
canBeConsumed = false
|
||||
canBeResolved = true
|
||||
}
|
||||
devBundle {
|
||||
canBeConsumed = false
|
||||
canBeResolved = true
|
||||
}
|
||||
runBundle {
|
||||
canBeConsumed = false
|
||||
canBeResolved = true
|
||||
}
|
||||
commonsLangRaw {
|
||||
canBeConsumed = false
|
||||
canBeResolved = true
|
||||
transitive = false
|
||||
}
|
||||
}
|
||||
|
||||
configurations.named('bundle').configure {
|
||||
exclude(group: 'com.google.code.gson')
|
||||
exclude(group: 'com.google.guava')
|
||||
exclude(group: 'commons-io')
|
||||
exclude(group: 'org.apache.commons', module: 'commons-lang3')
|
||||
exclude(group: 'it.unimi.dsi', module: 'fastutil')
|
||||
exclude(group: 'org.slf4j')
|
||||
exclude(group: 'org.apache.logging.log4j')
|
||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||
exclude(group: 'de.crazydev22.slimjar')
|
||||
exclude(group: 'io.github.slimjar')
|
||||
}
|
||||
|
||||
configurations.named('runBundle').configure {
|
||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||
exclude(group: 'de.crazydev22.slimjar')
|
||||
exclude(group: 'io.github.slimjar')
|
||||
}
|
||||
|
||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||
configurations.runtimeClasspath.extendsFrom(configurations.runBundle)
|
||||
|
||||
['compileClasspath', 'runtimeClasspath'].each { String name ->
|
||||
configurations.named(name).configure {
|
||||
resolutionStrategy.capabilitiesResolution.withCapability('org.lz4:lz4-java') {
|
||||
selectHighestVersion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
||||
compileOnly('org.slf4j:slf4j-api:2.0.17')
|
||||
compileOnly(libs.spigot) {
|
||||
transitive = false
|
||||
}
|
||||
|
||||
List<Object> shared = [
|
||||
"art.arcane:core:${irisVersion}".toString(),
|
||||
"art.arcane:spi:${irisVersion}".toString(),
|
||||
volmLibCoordinate,
|
||||
libs.paralithic,
|
||||
libs.lru,
|
||||
libs.kotlin.stdlib,
|
||||
libs.kotlin.coroutines,
|
||||
libs.commons.math3,
|
||||
libs.caffeine,
|
||||
libs.lz4,
|
||||
libs.zip,
|
||||
libs.sentry,
|
||||
libs.oshi,
|
||||
libs.byteBuddy.core,
|
||||
libs.byteBuddy.agent
|
||||
]
|
||||
shared.each { Object notation ->
|
||||
add('bundle', notation)
|
||||
add('devBundle', notation)
|
||||
add('runBundle', notation)
|
||||
}
|
||||
add('bundle', libs.commons.lang)
|
||||
add('devBundle', libs.commons.lang)
|
||||
add('commonsLangRaw', libs.commons.lang)
|
||||
}
|
||||
|
||||
TaskProvider<Jar> sanitizedCommonsLang = tasks.register('sanitizedCommonsLang', Jar) {
|
||||
archiveBaseName.set('commons-lang-sanitized')
|
||||
destinationDirectory.set(layout.buildDirectory.dir('sanitized'))
|
||||
from({ zipTree(configurations.named('commonsLangRaw').get().singleFile) })
|
||||
exclude('org/apache/commons/lang/enum/**')
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
dependencies {
|
||||
add('runBundle', files(sanitizedCommonsLang))
|
||||
}
|
||||
|
||||
minecraft {
|
||||
accessTransformer.from(file('src/main/resources/META-INF/accesstransformer.cfg'))
|
||||
runs {
|
||||
register('server') {
|
||||
workingDir = layout.projectDirectory.dir('run')
|
||||
String parity = providers.gradleProperty('irisParity').getOrNull()
|
||||
if (parity != null) {
|
||||
systemProperty('iris.parity', parity)
|
||||
}
|
||||
String parityGolden = providers.gradleProperty('irisParityGolden').getOrNull()
|
||||
if (parityGolden != null) {
|
||||
systemProperty('iris.parity.golden', parityGolden)
|
||||
}
|
||||
String parityDeep = providers.gradleProperty('irisParityDeep').getOrNull()
|
||||
if (parityDeep != null) {
|
||||
systemProperty('iris.parity.deep', parityDeep)
|
||||
}
|
||||
String worldCheck = providers.gradleProperty('irisWorldCheck').getOrNull()
|
||||
if (worldCheck != null) {
|
||||
systemProperty('iris.worldcheck', worldCheck)
|
||||
}
|
||||
jvmArgs('-Xmx8G')
|
||||
args('--nogui')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('compileJava', JavaCompile).configure {
|
||||
options.encoding = 'UTF-8'
|
||||
options.release.set(25)
|
||||
}
|
||||
|
||||
processResources {
|
||||
inputs.property('version', project.version)
|
||||
inputs.property('minecraftVersion', minecraftVersion)
|
||||
filesMatching('META-INF/mods.toml') {
|
||||
expand('version': project.version, 'minecraftVersion': minecraftVersion)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('shadowJar', ShadowJar).configure {
|
||||
doFirst {
|
||||
delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-forge.jar"))
|
||||
}
|
||||
archiveFileName.set(irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}"))
|
||||
configurations = [project.configurations.named('bundle').get()]
|
||||
from(sourceSets.main.output)
|
||||
mergeServiceFiles()
|
||||
exclude('META-INF/maven/**')
|
||||
exclude('META-INF/proguard/**')
|
||||
exclude('META-INF/*.SF')
|
||||
exclude('META-INF/*.DSA')
|
||||
exclude('META-INF/*.RSA')
|
||||
exclude('org/apache/commons/lang/enum/**')
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
|
||||
relocate('io.sentry', 'art.arcane.iris.shadow.sentry')
|
||||
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
|
||||
relocate('oshi', 'art.arcane.iris.shadow.oshi')
|
||||
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
|
||||
exclude('oshi.properties')
|
||||
exclude('org/jspecify/**')
|
||||
exclude('com/google/errorprone/**')
|
||||
exclude('com/google/j2objc/**')
|
||||
exclude('org/checkerframework/**')
|
||||
exclude('org/jetbrains/annotations/**')
|
||||
exclude('org/intellij/lang/**')
|
||||
}
|
||||
|
||||
tasks.named('assemble').configure {
|
||||
dependsOn(tasks.named('shadowJar'))
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
org.gradle.daemon=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.caching=true
|
||||
org.gradle.configuration-cache=false
|
||||
net.minecraftforge.gradle.merge-source-sets=true
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import java.io.File
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
maven {
|
||||
name = 'MinecraftForge'
|
||||
url = uri('https://maven.minecraftforge.net/')
|
||||
}
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||
}
|
||||
|
||||
rootProject.name = 'iris-forge'
|
||||
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
libs {
|
||||
from(files('../../gradle/libs.versions.toml'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasVolmLibSettings(File directory) {
|
||||
new File(directory, 'settings.gradle.kts').exists() || new File(directory, 'settings.gradle').exists()
|
||||
}
|
||||
|
||||
File resolveLocalVolmLibDirectory() {
|
||||
String configuredPath = providers.gradleProperty('localVolmLibDirectory')
|
||||
.orElse(providers.environmentVariable('VOLMLIB_DIR'))
|
||||
.orNull
|
||||
if (configuredPath != null && !configuredPath.isBlank()) {
|
||||
File configuredDirectory = file(configuredPath)
|
||||
if (hasVolmLibSettings(configuredDirectory)) {
|
||||
return configuredDirectory
|
||||
}
|
||||
}
|
||||
|
||||
File currentDirectory = settingsDir
|
||||
while (currentDirectory != null) {
|
||||
File candidate = new File(currentDirectory, 'VolmLib')
|
||||
if (hasVolmLibSettings(candidate)) {
|
||||
return candidate
|
||||
}
|
||||
|
||||
currentDirectory = currentDirectory.parentFile
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
|
||||
boolean useLocalVolmLib = providers.gradleProperty('useLocalVolmLib')
|
||||
.orElse('true')
|
||||
.map { String value -> value.equalsIgnoreCase('true') }
|
||||
.get()
|
||||
File localVolmLibDirectory = resolveLocalVolmLibDirectory()
|
||||
|
||||
if (useLocalVolmLib && localVolmLibDirectory != null) {
|
||||
includeBuild(localVolmLibDirectory) {
|
||||
dependencySubstitution {
|
||||
substitute(module('com.github.VolmitSoftware:VolmLib')).using(project(':shared'))
|
||||
substitute(module('com.github.VolmitSoftware.VolmLib:shared')).using(project(':shared'))
|
||||
substitute(module('com.github.VolmitSoftware.VolmLib:volmlib-shared')).using(project(':shared'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
includeBuild('../..') {
|
||||
dependencySubstitution {
|
||||
substitute(module('art.arcane:core')).using(project(':core'))
|
||||
substitute(module('art.arcane:spi')).using(project(':spi'))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.forge;
|
||||
|
||||
import art.arcane.iris.client.IrisClient;
|
||||
import net.minecraftforge.event.network.CustomPayloadEvent;
|
||||
|
||||
public final class ForgeClientProtocol {
|
||||
private ForgeClientProtocol() {
|
||||
}
|
||||
|
||||
public static void handleInbound(CustomPayloadEvent.Context context, byte[] data) {
|
||||
context.enqueueWork(() -> IrisClient.onInbound(data));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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.forge;
|
||||
|
||||
import art.arcane.iris.modded.ModdedLoader;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraftforge.fml.ModList;
|
||||
import net.minecraftforge.fml.loading.FMLEnvironment;
|
||||
import net.minecraftforge.fml.loading.FMLLoader;
|
||||
import net.minecraftforge.fml.loading.FMLPaths;
|
||||
import net.minecraftforge.forgespi.language.IModFileInfo;
|
||||
import net.minecraftforge.server.ServerLifecycleHooks;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class ForgeModdedLoader implements ModdedLoader {
|
||||
@Override
|
||||
public String platformName() {
|
||||
return "forge";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String minecraftVersion() {
|
||||
return FMLLoader.versionInfo().mcVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String modVersion() {
|
||||
return ModList.getModContainerById("irisworldgen")
|
||||
.map((net.minecraftforge.fml.ModContainer container) -> container.getModInfo().getVersion().toString())
|
||||
.orElse("unknown");
|
||||
}
|
||||
|
||||
@Override
|
||||
public MinecraftServer currentServer() {
|
||||
return ServerLifecycleHooks.getCurrentServer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean clientEnvironment() {
|
||||
return FMLEnvironment.dist.isClient();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path configDir() {
|
||||
return FMLPaths.CONFIGDIR.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File modJar() {
|
||||
IModFileInfo info = ModList.getModFileById("irisworldgen");
|
||||
return info == null ? null : info.getFile().getFilePath().toFile();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.forge;
|
||||
|
||||
import art.arcane.iris.modded.ModdedIrisPayload;
|
||||
import art.arcane.iris.modded.ModdedProtocolChannel;
|
||||
import art.arcane.iris.modded.ModdedProtocolHandler;
|
||||
import art.arcane.iris.spi.protocol.IrisProtocol;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraftforge.event.network.CustomPayloadEvent;
|
||||
import net.minecraftforge.network.Channel;
|
||||
import net.minecraftforge.network.ChannelBuilder;
|
||||
import net.minecraftforge.network.NetworkProtocol;
|
||||
import net.minecraftforge.network.PacketDistributor;
|
||||
|
||||
public final class ForgeProtocolNetworking {
|
||||
private static volatile Channel<CustomPacketPayload> channel;
|
||||
|
||||
private ForgeProtocolNetworking() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
Channel<CustomPacketPayload> boundChannel = ChannelBuilder.named(Identifier.parse(IrisProtocol.CHANNEL))
|
||||
.optional()
|
||||
.payloadChannel()
|
||||
.protocol(NetworkProtocol.PLAY)
|
||||
.bidirectional()
|
||||
.add(ModdedIrisPayload.TYPE, ModdedIrisPayload.STREAM_CODEC, ForgeProtocolNetworking::onPayload)
|
||||
.build();
|
||||
channel = boundChannel;
|
||||
ModdedProtocolHandler.bindChannel(new ForgeProtocolChannel(boundChannel));
|
||||
}
|
||||
|
||||
public static Channel<CustomPacketPayload> channel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
private static void onPayload(ModdedIrisPayload payload, CustomPayloadEvent.Context context) {
|
||||
context.setPacketHandled(true);
|
||||
if (context.isClientSide()) {
|
||||
ForgeClientProtocol.handleInbound(context, payload.data());
|
||||
return;
|
||||
}
|
||||
ServerPlayer player = context.getSender();
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
ModdedProtocolHandler.onInbound(player, payload.data());
|
||||
}
|
||||
|
||||
private static final class ForgeProtocolChannel implements ModdedProtocolChannel {
|
||||
private final Channel<CustomPacketPayload> channel;
|
||||
|
||||
private ForgeProtocolChannel(Channel<CustomPacketPayload> channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canReceive(ServerPlayer player) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(ServerPlayer player, ModdedIrisPayload payload) {
|
||||
channel.send(payload, PacketDistributor.PLAYER.with(player));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.forge;
|
||||
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedDeathLoot;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.ModdedForcedDatapack;
|
||||
import art.arcane.iris.modded.ModdedProtocolHandler;
|
||||
import art.arcane.iris.modded.command.IrisModdedCommands;
|
||||
import art.arcane.iris.modded.command.ModdedWandService;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.packs.PackType;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraftforge.event.AddPackFindersEvent;
|
||||
import net.minecraftforge.event.RegisterCommandsEvent;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.event.entity.living.LivingDropsEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.event.server.ServerStartingEvent;
|
||||
import net.minecraftforge.event.server.ServerStoppingEvent;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
|
||||
import net.minecraftforge.fml.loading.FMLEnvironment;
|
||||
import net.minecraftforge.fml.loading.FMLLoader;
|
||||
import net.minecraftforge.registries.DeferredRegister;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@Mod("irisworldgen")
|
||||
public final class IrisForgeBootstrap {
|
||||
public IrisForgeBootstrap(FMLJavaModLoadingContext context) {
|
||||
ModdedEngineBootstrap.bootCommon(new ForgeModdedLoader(), "Forge " + FMLLoader.versionInfo().forgeVersion(), () -> {
|
||||
DeferredRegister<MapCodec<? extends ChunkGenerator>> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen");
|
||||
chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC);
|
||||
chunkGenerators.register(context.getModBusGroup());
|
||||
});
|
||||
|
||||
ForgeProtocolNetworking.register();
|
||||
|
||||
if (FMLEnvironment.dist == Dist.CLIENT) {
|
||||
IrisForgeClient.init();
|
||||
}
|
||||
|
||||
ServerStartingEvent.BUS.addListener((ServerStartingEvent event) -> ModdedEngineBootstrap.start(event.getServer()));
|
||||
ServerStoppingEvent.BUS.addListener((ServerStoppingEvent event) -> ModdedEngineBootstrap.stop());
|
||||
PlayerEvent.PlayerLoggedInEvent.BUS.addListener((PlayerEvent.PlayerLoggedInEvent event) -> {
|
||||
if (event.getEntity() instanceof ServerPlayer player) {
|
||||
ModdedProtocolHandler.onPlayerJoin(player);
|
||||
}
|
||||
});
|
||||
PlayerEvent.PlayerLoggedOutEvent.BUS.addListener((PlayerEvent.PlayerLoggedOutEvent event) -> {
|
||||
if (event.getEntity() instanceof ServerPlayer player) {
|
||||
ModdedProtocolHandler.onPlayerDisconnect(player);
|
||||
}
|
||||
});
|
||||
AddPackFindersEvent.BUS.addListener((AddPackFindersEvent event) -> {
|
||||
if (event.getPackType() == PackType.SERVER_DATA) {
|
||||
event.addRepositorySource(ModdedForcedDatapack.repositorySource());
|
||||
}
|
||||
});
|
||||
RegisterCommandsEvent.BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
|
||||
LivingDropsEvent.BUS.addListener((LivingDropsEvent event) -> ModdedDeathLoot.handle(event.getEntity()));
|
||||
PlayerInteractEvent.LeftClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.LeftClickBlock>) (PlayerInteractEvent.LeftClickBlock event) ->
|
||||
ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos()));
|
||||
PlayerInteractEvent.RightClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.RightClickBlock>) (PlayerInteractEvent.RightClickBlock event) ->
|
||||
ModdedWandService.useBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos()));
|
||||
TickEvent.ServerTickEvent.Post.BUS.addListener((TickEvent.ServerTickEvent.Post event) -> {
|
||||
ModdedEngineBootstrap.tick(event.server());
|
||||
ModdedWandService.serverTick(event.server());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.forge;
|
||||
|
||||
import art.arcane.iris.client.IrisClient;
|
||||
import art.arcane.iris.client.IrisClientHud;
|
||||
import art.arcane.iris.client.IrisClientKeybinds;
|
||||
import art.arcane.iris.modded.ModdedIrisPayload;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraftforge.client.event.AddGuiOverlayLayersEvent;
|
||||
import net.minecraftforge.client.event.ClientPlayerNetworkEvent;
|
||||
import net.minecraftforge.client.event.InputEvent;
|
||||
import net.minecraftforge.client.event.RegisterKeyMappingsEvent;
|
||||
import net.minecraftforge.network.Channel;
|
||||
import net.minecraftforge.network.PacketDistributor;
|
||||
|
||||
public final class IrisForgeClient {
|
||||
private IrisForgeClient() {
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
IrisClient.bindSender(IrisForgeClient::sendToServer);
|
||||
AddGuiOverlayLayersEvent.BUS.addListener((AddGuiOverlayLayersEvent event) ->
|
||||
event.getLayeredDraw().add(IrisClient.HUD_ELEMENT_ID, (graphics, delta) -> IrisClientHud.render(graphics)));
|
||||
RegisterKeyMappingsEvent.BUS.addListener((RegisterKeyMappingsEvent event) -> {
|
||||
event.register(IrisClientKeybinds.TOGGLE_HUD);
|
||||
event.register(IrisClientKeybinds.OPEN_MAP);
|
||||
event.register(IrisClientKeybinds.TOGGLE_WHAT);
|
||||
});
|
||||
ClientPlayerNetworkEvent.LoggingIn.BUS.addListener((ClientPlayerNetworkEvent.LoggingIn event) -> IrisClient.onWorldJoin());
|
||||
ClientPlayerNetworkEvent.LoggingOut.BUS.addListener((ClientPlayerNetworkEvent.LoggingOut event) -> IrisClient.onDisconnect());
|
||||
InputEvent.Key.BUS.addListener((InputEvent.Key event) -> IrisClientKeybinds.pollToggle());
|
||||
}
|
||||
|
||||
private static void sendToServer(byte[] frame) {
|
||||
Channel<CustomPacketPayload> channel = ForgeProtocolNetworking.channel();
|
||||
if (channel == null) {
|
||||
return;
|
||||
}
|
||||
channel.send(new ModdedIrisPayload(frame), PacketDistributor.SERVER.noArg());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
public net.minecraft.server.MinecraftServer levels
|
||||
public net.minecraft.server.MinecraftServer executor
|
||||
public net.minecraft.server.MinecraftServer storageSource
|
||||
@@ -0,0 +1,24 @@
|
||||
modLoader = "javafml"
|
||||
loaderVersion = "[65,)"
|
||||
license = "GPL-3.0"
|
||||
|
||||
[[mods]]
|
||||
modId = "irisworldgen"
|
||||
version = "${version}"
|
||||
displayName = "Iris"
|
||||
description = "Iris World Generation Engine (Forge adapter - native chunk generator, explicit Iris world datapack workflow, engine lifecycle)"
|
||||
authors = "Arcane Arts (Volmit Software)"
|
||||
|
||||
[[dependencies.irisworldgen]]
|
||||
modId = "forge"
|
||||
mandatory = true
|
||||
versionRange = "[65,)"
|
||||
ordering = "NONE"
|
||||
side = "BOTH"
|
||||
|
||||
[[dependencies.irisworldgen]]
|
||||
modId = "minecraft"
|
||||
mandatory = true
|
||||
versionRange = "[${minecraftVersion}]"
|
||||
ordering = "NONE"
|
||||
side = "BOTH"
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"pack": {
|
||||
"description": "Iris World Generation Engine resources",
|
||||
"max_format": 101,
|
||||
"min_format": [
|
||||
101,
|
||||
1
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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.modded;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
final class InitialSpawnQueue {
|
||||
private static final long DEFAULT_MAX_AGE_NANOS = TimeUnit.MINUTES.toNanos(5);
|
||||
|
||||
private final int capacity;
|
||||
private final long maxAgeNanos;
|
||||
private final LongSupplier nanoTime;
|
||||
private final ArrayDeque<Long> queue;
|
||||
private final Map<Long, Long> pending;
|
||||
private final Set<Long> queued;
|
||||
private boolean closed;
|
||||
|
||||
InitialSpawnQueue(int capacity) {
|
||||
this(capacity, DEFAULT_MAX_AGE_NANOS, System::nanoTime);
|
||||
}
|
||||
|
||||
InitialSpawnQueue(int capacity, long maxAgeNanos, LongSupplier nanoTime) {
|
||||
if (capacity < 1) {
|
||||
throw new IllegalArgumentException("capacity must be positive");
|
||||
}
|
||||
if (maxAgeNanos < 1L) {
|
||||
throw new IllegalArgumentException("maxAgeNanos must be positive");
|
||||
}
|
||||
this.capacity = capacity;
|
||||
this.maxAgeNanos = maxAgeNanos;
|
||||
this.nanoTime = nanoTime;
|
||||
this.queue = new ArrayDeque<>(Math.min(capacity, 256));
|
||||
this.pending = new HashMap<>();
|
||||
this.queued = new HashSet<>();
|
||||
}
|
||||
|
||||
synchronized boolean offer(long key) {
|
||||
if (closed || pending.containsKey(key)) {
|
||||
return false;
|
||||
}
|
||||
if (pending.size() >= capacity) {
|
||||
expire(nanoTime.getAsLong());
|
||||
}
|
||||
if (pending.size() >= capacity) {
|
||||
return false;
|
||||
}
|
||||
pending.put(key, nanoTime.getAsLong());
|
||||
queued.add(key);
|
||||
queue.addLast(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
synchronized int batchSize(int limit) {
|
||||
if (closed || limit <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(limit, queue.size());
|
||||
}
|
||||
|
||||
synchronized Long poll() {
|
||||
long now = nanoTime.getAsLong();
|
||||
Long key;
|
||||
while ((key = queue.pollFirst()) != null) {
|
||||
queued.remove(key);
|
||||
Long offeredAt = pending.get(key);
|
||||
if (offeredAt == null) {
|
||||
continue;
|
||||
}
|
||||
if (expired(offeredAt, now)) {
|
||||
pending.remove(key);
|
||||
continue;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
synchronized void retry(long key) {
|
||||
Long offeredAt = pending.get(key);
|
||||
if (closed || offeredAt == null || expired(offeredAt, nanoTime.getAsLong())) {
|
||||
pending.remove(key);
|
||||
queued.remove(key);
|
||||
return;
|
||||
}
|
||||
if (!queued.add(key)) {
|
||||
return;
|
||||
}
|
||||
queue.addLast(key);
|
||||
}
|
||||
|
||||
synchronized void complete(long key) {
|
||||
pending.remove(key);
|
||||
if (queued.remove(key)) {
|
||||
queue.removeFirstOccurrence(key);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized boolean isEmpty() {
|
||||
return queue.isEmpty();
|
||||
}
|
||||
|
||||
synchronized int size() {
|
||||
return pending.size();
|
||||
}
|
||||
|
||||
synchronized void clear() {
|
||||
queue.clear();
|
||||
pending.clear();
|
||||
queued.clear();
|
||||
}
|
||||
|
||||
synchronized void close() {
|
||||
closed = true;
|
||||
clear();
|
||||
}
|
||||
|
||||
private void expire(long now) {
|
||||
Set<Long> expired = new HashSet<>();
|
||||
for (Map.Entry<Long, Long> entry : pending.entrySet()) {
|
||||
if (expired(entry.getValue(), now)) {
|
||||
expired.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
if (expired.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (Long key : expired) {
|
||||
pending.remove(key);
|
||||
queued.remove(key);
|
||||
}
|
||||
queue.removeIf(expired::contains);
|
||||
}
|
||||
|
||||
private boolean expired(long offeredAt, long now) {
|
||||
return now - offeredAt >= maxAgeNanos;
|
||||
}
|
||||
}
|
||||
+584
@@ -0,0 +1,584 @@
|
||||
/*
|
||||
* 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.modded;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.QuartPos;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.util.random.WeightedList;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.biome.BiomeResolver;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||
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.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
public static final MapCodec<IrisModdedChunkGenerator> CODEC = RecordCodecBuilder.mapCodec((RecordCodecBuilder.Instance<IrisModdedChunkGenerator> instance) -> instance.group(
|
||||
BiomeSource.CODEC.fieldOf("biome_source").forGetter((IrisModdedChunkGenerator generator) -> generator.biomeSource),
|
||||
Codec.STRING.fieldOf("dimension").forGetter((IrisModdedChunkGenerator generator) -> generator.dimensionKey)
|
||||
).apply(instance, IrisModdedChunkGenerator::new));
|
||||
|
||||
private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger();
|
||||
private static final boolean PARALLEL_CHUNK_SYSTEM = detectParallelChunkSystem();
|
||||
private static volatile ExecutorService genPool = createGenPool();
|
||||
|
||||
public static void startGenPool() {
|
||||
ExecutorService pool = genPool;
|
||||
if (pool == null || pool.isShutdown()) {
|
||||
genPool = createGenPool();
|
||||
}
|
||||
}
|
||||
|
||||
public static void shutdownGenPool() {
|
||||
ExecutorService pool = genPool;
|
||||
if (pool != null) {
|
||||
pool.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean detectParallelChunkSystem() {
|
||||
String[] markers = {
|
||||
"com.ishland.c2me.base.ModProperties",
|
||||
"com.ishland.c2me.base.common.config.C2MEConfig",
|
||||
"com.ishland.c2me.opts.chunkio.ModProperties",
|
||||
"ca.spottedleaf.moonrise.common.util.MoonriseCommon"
|
||||
};
|
||||
for (String marker : markers) {
|
||||
try {
|
||||
Class.forName(marker, false, IrisModdedChunkGenerator.class.getClassLoader());
|
||||
return true;
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ExecutorService createGenPool() {
|
||||
int threads = Math.max(2, Runtime.getRuntime().availableProcessors());
|
||||
ThreadPoolExecutor pool = new ThreadPoolExecutor(
|
||||
threads, threads, 30L, TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(),
|
||||
runnable -> {
|
||||
Thread thread = new Thread(runnable, "Iris ModGen-" + GEN_THREAD_SEQ.incrementAndGet());
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
pool.allowCoreThreadTimeOut(true);
|
||||
return pool;
|
||||
}
|
||||
|
||||
private final String dimensionKey;
|
||||
private final String defaultPack;
|
||||
private final String defaultDimensionKey;
|
||||
private final ConcurrentHashMap<String, Holder<Biome>> biomeHolders = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||
private final Set<String> missingBiomeWarnings = ConcurrentHashMap.newKeySet();
|
||||
private final AtomicBoolean announced = new AtomicBoolean(false);
|
||||
private volatile boolean vanillaSpawnBiomesInitialized;
|
||||
private volatile Engine engine;
|
||||
private volatile String activePack;
|
||||
private volatile String activeDimensionKey;
|
||||
private volatile long seedOverride = Long.MIN_VALUE;
|
||||
private volatile long lastChunkGenAt = 0L;
|
||||
|
||||
public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
|
||||
super(biomeSource);
|
||||
this.dimensionKey = dimensionKey;
|
||||
int colon = dimensionKey.indexOf(':');
|
||||
this.defaultPack = colon >= 0 ? dimensionKey.substring(0, colon) : dimensionKey;
|
||||
this.defaultDimensionKey = colon >= 0 ? dimensionKey.substring(colon + 1) : dimensionKey;
|
||||
this.activePack = defaultPack;
|
||||
this.activeDimensionKey = defaultDimensionKey;
|
||||
}
|
||||
|
||||
public synchronized void repoint(String pack, String packDimensionKey, long seed) {
|
||||
ServerLevel level = boundLevel();
|
||||
if (level != null) {
|
||||
ModdedWorldEngines.evict(level);
|
||||
}
|
||||
this.activePack = pack;
|
||||
this.activeDimensionKey = packDimensionKey;
|
||||
this.seedOverride = seed;
|
||||
this.engine = null;
|
||||
this.announced.set(false);
|
||||
this.biomeHolders.clear();
|
||||
this.missingBiomeWarnings.clear();
|
||||
resetVanillaSpawnBiomes();
|
||||
}
|
||||
|
||||
public synchronized void unbindEngine() {
|
||||
ServerLevel level = boundLevel();
|
||||
if (level != null) {
|
||||
ModdedWorldEngines.evict(level);
|
||||
}
|
||||
this.engine = null;
|
||||
this.announced.set(false);
|
||||
this.biomeHolders.clear();
|
||||
this.missingBiomeWarnings.clear();
|
||||
resetVanillaSpawnBiomes();
|
||||
}
|
||||
|
||||
public synchronized void resetToDefault() {
|
||||
repoint(defaultPack, defaultDimensionKey, Long.MIN_VALUE);
|
||||
}
|
||||
|
||||
public String activePack() {
|
||||
return activePack;
|
||||
}
|
||||
|
||||
public String activeDimensionKey() {
|
||||
return activeDimensionKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends ChunkGenerator> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
private ServerLevel boundLevel() {
|
||||
MinecraftServer server = ModdedEngineBootstrap.currentServer();
|
||||
if (server == null) {
|
||||
return null;
|
||||
}
|
||||
for (ServerLevel level : server.getAllLevels()) {
|
||||
if (level.getChunkSource().getGenerator() == this) {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Engine engine() {
|
||||
Engine cached = engine;
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (engine != null) {
|
||||
return engine;
|
||||
}
|
||||
ServerLevel level = boundLevel();
|
||||
if (level == null) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey + "' has no bound ServerLevel yet");
|
||||
}
|
||||
Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride);
|
||||
engine = created;
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
private Engine engineOrNull() {
|
||||
Engine cached = engine;
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
try {
|
||||
return engine();
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String dimensionKey() {
|
||||
return dimensionKey;
|
||||
}
|
||||
|
||||
public Engine engineIfBound() {
|
||||
return engine;
|
||||
}
|
||||
|
||||
public Engine commandEngine() {
|
||||
return engine();
|
||||
}
|
||||
|
||||
public long lastChunkGenAt() {
|
||||
return lastChunkGenAt;
|
||||
}
|
||||
|
||||
public void onHotload() {
|
||||
biomeHolders.clear();
|
||||
missingBiomeWarnings.clear();
|
||||
resetVanillaSpawnBiomes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(
|
||||
Holder<Biome> biome, StructureManager structureManager, MobCategory category, BlockPos pos) {
|
||||
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns = biome.value().getMobSettings().getMobs(category);
|
||||
WeightedList<MobSpawnSettings.SpawnerData> resolvedSpawns = super.getMobsAt(
|
||||
biome, structureManager, category, pos);
|
||||
if (resolvedSpawns != explicitSpawns) {
|
||||
return resolvedSpawns;
|
||||
}
|
||||
|
||||
Registry<Biome> registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
initializeVanillaSpawnBiomes(registry);
|
||||
Holder<Biome> vanillaSpawnBiome = vanillaSpawnBiomes.get(biome.value());
|
||||
if (vanillaSpawnBiome == null) {
|
||||
return explicitSpawns;
|
||||
}
|
||||
|
||||
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns = vanillaSpawnBiome.value().getMobSettings().getMobs(category);
|
||||
if (explicitSpawns.isEmpty()) {
|
||||
return vanillaSpawns;
|
||||
}
|
||||
if (vanillaSpawns.isEmpty()) {
|
||||
return explicitSpawns;
|
||||
}
|
||||
|
||||
SpawnTableKey key = new SpawnTableKey(biome.value(), category);
|
||||
return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
|
||||
}
|
||||
|
||||
private synchronized void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
|
||||
if (vanillaSpawnBiomesInitialized) {
|
||||
return;
|
||||
}
|
||||
Engine current = engineOrNull();
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : current.getDimension().getAllBiomes(current)) {
|
||||
if (irisBiome == null || !irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
|
||||
if (vanillaHolder == null) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
|
||||
if (customHolder != null) {
|
||||
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
vanillaSpawnBiomesInitialized = true;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
|
||||
if (key == null || key.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(key);
|
||||
if (identifier == null) {
|
||||
return null;
|
||||
}
|
||||
Optional<Holder.Reference<Biome>> reference = registry.get(identifier);
|
||||
return reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElse(null);
|
||||
}
|
||||
|
||||
private synchronized void resetVanillaSpawnBiomes() {
|
||||
vanillaSpawnBiomes.clear();
|
||||
mergedSpawnTables.clear();
|
||||
vanillaSpawnBiomesInitialized = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunk) {
|
||||
Engine generationEngine = engine();
|
||||
ChunkPos pos = chunk.getPos();
|
||||
lastChunkGenAt = System.currentTimeMillis();
|
||||
if (announced.compareAndSet(false, true)) {
|
||||
LOGGER.info("Iris generating {} through IrisModdedChunkGenerator (dim={} first chunk {},{})",
|
||||
dimensionKey, generationEngine.getDimension().getLoadKey(), pos.x(), pos.z());
|
||||
}
|
||||
LOGGER.debug("Iris generating chunk {},{}", pos.x(), pos.z());
|
||||
|
||||
int dimMinY = generationEngine.getMinHeight();
|
||||
int dimMaxY = generationEngine.getMaxHeight();
|
||||
int height = dimMaxY - dimMinY;
|
||||
PlatformBlockState air = IrisPlatforms.get().registries().air();
|
||||
Registry<Biome> biomeRegistry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
|
||||
if (PARALLEL_CHUNK_SYSTEM) {
|
||||
return CompletableFuture.completedFuture(
|
||||
generateTerrain(chunk, generationEngine, pos, dimMinY, height, air, biomeRegistry, randomState));
|
||||
}
|
||||
return CompletableFuture.supplyAsync(
|
||||
() -> generateTerrain(chunk, generationEngine, pos, dimMinY, height, air, biomeRegistry, randomState),
|
||||
genPool);
|
||||
}
|
||||
|
||||
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
|
||||
int dimMinY, int height, PlatformBlockState air,
|
||||
Registry<Biome> biomeRegistry, RandomState randomState) {
|
||||
ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air);
|
||||
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
|
||||
try {
|
||||
generationEngine.generate(pos.getMinBlockX(), pos.getMinBlockZ(), blocks, biomes, false);
|
||||
} catch (GenerationSessionException e) {
|
||||
if (e.isExpectedTeardown()) {
|
||||
LOGGER.debug("Iris chunk {},{} skipped: engine sealed for hotload/teardown", pos.x(), pos.z());
|
||||
return chunk;
|
||||
}
|
||||
LOGGER.error("Iris failed to generate chunk {},{}", pos.x(), pos.z(), e);
|
||||
throw new IllegalStateException("Iris generation failed for chunk " + pos.x() + "," + pos.z(), e);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to generate chunk {},{}", pos.x(), pos.z(), e);
|
||||
throw new IllegalStateException("Iris generation failed for chunk " + pos.x() + "," + pos.z(), e);
|
||||
}
|
||||
|
||||
writeBlocks(chunk, blocks, dimMinY, height);
|
||||
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG, Heightmap.Types.OCEAN_FLOOR_WG));
|
||||
chunk.fillBiomesFromNoise(new HunkBiomeResolver(this, biomes, biomeRegistry, pos, dimMinY, height), randomState.sampler());
|
||||
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
|
||||
return chunk;
|
||||
}
|
||||
|
||||
private Holder<Biome> fallbackBiome(Registry<Biome> registry) {
|
||||
Holder<Biome> existing = biomeHolders.get("minecraft:plains");
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
Holder<Biome> resolved = registry.get(Identifier.fromNamespaceAndPath("minecraft", "plains"))
|
||||
.<Holder<Biome>>map((Holder.Reference<Biome> reference) -> reference)
|
||||
.orElseThrow(() -> new IllegalStateException("minecraft:plains missing from biome registry"));
|
||||
Holder<Biome> raced = biomeHolders.putIfAbsent("minecraft:plains", resolved);
|
||||
return raced != null ? raced : resolved;
|
||||
}
|
||||
|
||||
private Holder<Biome> holderFor(Registry<Biome> registry, PlatformBiome biome) {
|
||||
if (biome == null) {
|
||||
return fallbackBiome(registry);
|
||||
}
|
||||
String key = biome.key();
|
||||
Holder<Biome> existing = biomeHolders.get(key);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(key);
|
||||
Optional<Holder.Reference<Biome>> reference = identifier == null ? Optional.empty() : registry.get(identifier);
|
||||
Holder<Biome> resolved = reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElseGet(() -> {
|
||||
if (missingBiomeWarnings.size() < 256 && missingBiomeWarnings.add(key)) {
|
||||
LOGGER.warn("Iris biome '{}' (pack {}) is not in the biome registry; writing minecraft:plains. Restart so the forced datapack registers the pack biomes.", key, activePack);
|
||||
}
|
||||
return fallbackBiome(registry);
|
||||
});
|
||||
Holder<Biome> raced = biomeHolders.putIfAbsent(key, resolved);
|
||||
return raced != null ? raced : resolved;
|
||||
}
|
||||
|
||||
public BiomeResolver regenBiomeResolver(Registry<Biome> registry, Hunk<PlatformBiome> biomes, ChunkPos pos) {
|
||||
Engine current = engine();
|
||||
int dimMinY = current.getMinHeight();
|
||||
int height = current.getMaxHeight() - dimMinY;
|
||||
return new HunkBiomeResolver(this, biomes, registry, pos, dimMinY, height);
|
||||
}
|
||||
|
||||
private void writeBlocks(ChunkAccess chunk, ModdedBlockBuffer blocks, int dimMinY, int height) {
|
||||
int chunkMinY = chunk.getMinY();
|
||||
int chunkMaxY = chunkMinY + chunk.getHeight();
|
||||
int from = Math.max(dimMinY, chunkMinY);
|
||||
int to = Math.min(dimMinY + height, chunkMaxY);
|
||||
|
||||
for (int y = from; y < to; ) {
|
||||
int sectionIndex = chunk.getSectionIndex(y);
|
||||
LevelChunkSection section = chunk.getSection(sectionIndex);
|
||||
int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4;
|
||||
int sectionEnd = Math.min(sectionMinY + 16, to);
|
||||
section.acquire();
|
||||
try {
|
||||
for (int blockY = y; blockY < sectionEnd; blockY++) {
|
||||
int bufferY = blockY - dimMinY;
|
||||
int localY = blockY & 15;
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
if (blocks.isAir(x, bufferY, z)) {
|
||||
continue;
|
||||
}
|
||||
PlatformBlockState state = blocks.getRaw(x, bufferY, z);
|
||||
section.setBlockState(x, localY, z, (BlockState) state.nativeHandle(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
section.release();
|
||||
}
|
||||
y = sectionEnd;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class HunkBiomeResolver implements BiomeResolver {
|
||||
private final IrisModdedChunkGenerator generator;
|
||||
private final Hunk<PlatformBiome> biomes;
|
||||
private final Registry<Biome> registry;
|
||||
private final ChunkPos pos;
|
||||
private final int dimMinY;
|
||||
private final int height;
|
||||
|
||||
private HunkBiomeResolver(IrisModdedChunkGenerator generator, Hunk<PlatformBiome> biomes, Registry<Biome> registry, ChunkPos pos, int dimMinY, int height) {
|
||||
this.generator = generator;
|
||||
this.biomes = biomes;
|
||||
this.registry = registry;
|
||||
this.pos = pos;
|
||||
this.dimMinY = dimMinY;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ, Climate.Sampler sampler) {
|
||||
int localX = QuartPos.toBlock(quartX) - pos.getMinBlockX();
|
||||
int localZ = QuartPos.toBlock(quartZ) - pos.getMinBlockZ();
|
||||
int bufferY = Math.max(0, Math.min(height - 1, QuartPos.toBlock(quartY) - dimMinY));
|
||||
PlatformBiome biome = biomes.get(localX, bufferY, localZ);
|
||||
return generator.holderFor(registry, biome);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyCarvers(WorldGenRegion region, long seed, RandomState randomState, BiomeManager biomeManager, StructureManager structureManager, ChunkAccess chunk) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildSurface(WorldGenRegion region, StructureManager structureManager, RandomState randomState, ChunkAccess chunk) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spawnOriginalMobs(WorldGenRegion region) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGenDepth() {
|
||||
Engine current = engineOrNull();
|
||||
return current == null ? 384 : current.getMaxHeight() - current.getMinHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSeaLevel() {
|
||||
Engine current = engineOrNull();
|
||||
return current == null ? 63 : current.getMinHeight() + current.getDimension().getFluidHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinY() {
|
||||
Engine current = engineOrNull();
|
||||
return current == null ? -64 : current.getMinHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor, RandomState randomState) {
|
||||
Engine current = engineOrNull();
|
||||
if (current == null) {
|
||||
return heightAccessor.getMinY() + Math.min(heightAccessor.getHeight(), 64);
|
||||
}
|
||||
boolean ignoreFluid = type == Heightmap.Types.OCEAN_FLOOR || type == Heightmap.Types.OCEAN_FLOOR_WG;
|
||||
return current.getMinHeight() + current.getHeight(x, z, ignoreFluid) + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, RandomState randomState) {
|
||||
int minY = heightAccessor.getMinY();
|
||||
BlockState[] states = new BlockState[heightAccessor.getHeight()];
|
||||
Engine current = engineOrNull();
|
||||
BlockState airState = Blocks.AIR.defaultBlockState();
|
||||
if (current == null) {
|
||||
Arrays.fill(states, airState);
|
||||
return new NoiseColumn(minY, states);
|
||||
}
|
||||
int surface = current.getMinHeight() + current.getHeight(x, z, true);
|
||||
int fluid = current.getMinHeight() + current.getDimension().getFluidHeight();
|
||||
BlockState stone = Blocks.STONE.defaultBlockState();
|
||||
BlockState water = Blocks.WATER.defaultBlockState();
|
||||
int solidEnd = Math.max(0, Math.min(states.length, surface - minY + 1));
|
||||
int fluidEnd = Math.max(solidEnd, Math.max(0, Math.min(states.length, fluid - minY + 1)));
|
||||
Arrays.fill(states, 0, solidEnd, stone);
|
||||
Arrays.fill(states, solidEnd, fluidEnd, water);
|
||||
Arrays.fill(states, fluidEnd, states.length, airState);
|
||||
return new NoiseColumn(minY, states);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addDebugScreenInfo(List<String> info, RandomState randomState, BlockPos pos) {
|
||||
info.add("Iris dimension: " + dimensionKey);
|
||||
}
|
||||
|
||||
private record SpawnTableKey(Biome biome, MobCategory category) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* 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.modded;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class MainWorldService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final String PRESET_NAMESPACE = "irisworldgen";
|
||||
private static final String MARKER_NAME = "mainworld.pending";
|
||||
private static final String[] VANILLA_DIMENSION_FOLDERS = {
|
||||
"region",
|
||||
"entities",
|
||||
"poi",
|
||||
"mantle",
|
||||
"dimensions/minecraft/overworld",
|
||||
"dimensions/minecraft/the_nether",
|
||||
"dimensions/minecraft/the_end",
|
||||
"DIM-1",
|
||||
"DIM1"
|
||||
};
|
||||
|
||||
private MainWorldService() {
|
||||
}
|
||||
|
||||
public static String presetIdFor(String packRef) {
|
||||
String value = packRef.trim();
|
||||
int colon = value.indexOf(':');
|
||||
String pack = colon >= 0 ? value.substring(0, colon) : value;
|
||||
String dimension = colon >= 0 ? value.substring(colon + 1) : value;
|
||||
String presetKey = dimension.equals(pack) ? pack : pack + "_" + dimension;
|
||||
return PRESET_NAMESPACE + ":" + presetKey;
|
||||
}
|
||||
|
||||
public static void reconcileEarly() {
|
||||
try {
|
||||
ModdedModConfig config = ModdedModConfig.get();
|
||||
String pack = config.mainWorldPack();
|
||||
if (pack == null || pack.isBlank()) {
|
||||
return;
|
||||
}
|
||||
Path properties = instanceRoot().resolve("server.properties");
|
||||
String target = presetIdFor(pack);
|
||||
String currentType = readProperty(properties, "level-type");
|
||||
if (!target.equals(currentType)) {
|
||||
writeLevelProperties(properties, target, config.mainWorldSeed());
|
||||
markPending();
|
||||
LOGGER.warn("Iris main world '{}' staged: server.properties level-type set to {}. Restart again to generate it (this boot still uses the previous overworld; player data is kept).", pack, target);
|
||||
return;
|
||||
}
|
||||
if (!isPending()) {
|
||||
return;
|
||||
}
|
||||
String levelName = firstNonBlank(readProperty(properties, "level-name"), "world");
|
||||
wipeVanillaDimensions(instanceRoot().resolve(levelName));
|
||||
clearPending();
|
||||
LOGGER.warn("Iris main world '{}' generated fresh: cleared the previous overworld/nether/end so this boot regenerates them as {} (player data kept).", pack, target);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris main world reconciliation failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean stage(String packRef, long seed) {
|
||||
try {
|
||||
Path properties = instanceRoot().resolve("server.properties");
|
||||
writeLevelProperties(properties, presetIdFor(packRef), seed);
|
||||
markPending();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Iris failed to stage the main world in server.properties", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearOverride() {
|
||||
try {
|
||||
clearPending();
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Iris failed to clear the pending main world marker", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Path instanceRoot() {
|
||||
return ModdedEngineBootstrap.loader().configDir().getParent();
|
||||
}
|
||||
|
||||
private static Path markerFile() {
|
||||
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve(MARKER_NAME);
|
||||
}
|
||||
|
||||
private static boolean isPending() {
|
||||
return Files.isRegularFile(markerFile());
|
||||
}
|
||||
|
||||
private static void markPending() throws IOException {
|
||||
Path marker = markerFile();
|
||||
Files.createDirectories(marker.getParent());
|
||||
Files.writeString(marker, "pending", StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static void clearPending() throws IOException {
|
||||
Files.deleteIfExists(markerFile());
|
||||
}
|
||||
|
||||
private static String readProperty(Path properties, String key) throws IOException {
|
||||
if (!Files.isRegularFile(properties)) {
|
||||
return null;
|
||||
}
|
||||
List<String> lines = Files.readAllLines(properties, StandardCharsets.UTF_8);
|
||||
String prefix = key + "=";
|
||||
for (String line : lines) {
|
||||
if (line.startsWith(prefix)) {
|
||||
return unescape(line.substring(prefix.length()).trim());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void writeLevelProperties(Path properties, String target, long seed) throws IOException {
|
||||
List<String> lines = Files.isRegularFile(properties)
|
||||
? new ArrayList<>(Files.readAllLines(properties, StandardCharsets.UTF_8))
|
||||
: new ArrayList<>();
|
||||
setProperty(lines, "level-type", escape(target));
|
||||
if (seed != 0L) {
|
||||
setProperty(lines, "level-seed", Long.toString(seed));
|
||||
}
|
||||
Files.write(properties, lines, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static void setProperty(List<String> lines, String key, String value) {
|
||||
String prefix = key + "=";
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
if (lines.get(i).startsWith(prefix)) {
|
||||
lines.set(i, prefix + value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
lines.add(prefix + value);
|
||||
}
|
||||
|
||||
private static void wipeVanillaDimensions(Path worldRoot) throws IOException {
|
||||
Files.deleteIfExists(worldRoot.resolve("level.dat"));
|
||||
Files.deleteIfExists(worldRoot.resolve("level.dat_old"));
|
||||
for (String folder : VANILLA_DIMENSION_FOLDERS) {
|
||||
deleteRecursively(worldRoot.resolve(folder));
|
||||
}
|
||||
}
|
||||
|
||||
private static void deleteRecursively(Path path) throws IOException {
|
||||
if (!Files.exists(path)) {
|
||||
return;
|
||||
}
|
||||
List<Path> entries = new ArrayList<>();
|
||||
try (Stream<Path> walk = Files.walk(path)) {
|
||||
walk.sorted(Comparator.comparingInt(Path::getNameCount).reversed()).forEach(entries::add);
|
||||
}
|
||||
for (Path entry : entries) {
|
||||
Files.deleteIfExists(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static String escape(String value) {
|
||||
return value.replace(":", "\\:");
|
||||
}
|
||||
|
||||
private static String unescape(String value) {
|
||||
return value.replace("\\:", ":").replace("\\=", "=");
|
||||
}
|
||||
|
||||
private static String firstNonBlank(String value, String fallback) {
|
||||
return value == null || value.isBlank() ? fallback : value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.modded;
|
||||
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ModdedBiome implements PlatformBiome {
|
||||
private static final ConcurrentHashMap<String, ModdedBiome> CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
private final Biome biome;
|
||||
private final String key;
|
||||
private final String namespace;
|
||||
|
||||
private ModdedBiome(Biome biome, String key) {
|
||||
this.biome = biome;
|
||||
this.key = key;
|
||||
int colon = key.indexOf(':');
|
||||
this.namespace = colon >= 0 ? key.substring(0, colon) : "minecraft";
|
||||
}
|
||||
|
||||
public static ModdedBiome of(Biome biome, String key) {
|
||||
return CACHE.computeIfAbsent(key, (String k) -> new ModdedBiome(biome, k));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String namespace() {
|
||||
return namespace;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object nativeHandle() {
|
||||
return biome;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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.modded;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBiomeWriter;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class ModdedBiomeWriter implements PlatformBiomeWriter {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final String VANILLA_FALLBACK_KEY = "minecraft:plains";
|
||||
|
||||
private final Supplier<MinecraftServer> server;
|
||||
|
||||
public ModdedBiomeWriter(Supplier<MinecraftServer> server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int biomeIdFor(String key) {
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
if (registry == null) {
|
||||
return 0;
|
||||
}
|
||||
int direct = idForKey(registry, key);
|
||||
if (direct >= 0) {
|
||||
return direct;
|
||||
}
|
||||
int derivative = idForDerivative(registry, key);
|
||||
if (derivative >= 0) {
|
||||
return derivative;
|
||||
}
|
||||
return fallbackId(registry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PlatformBiome> allBiomes() {
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
List<PlatformBiome> biomes = new ArrayList<>();
|
||||
if (registry == null) {
|
||||
return biomes;
|
||||
}
|
||||
for (Identifier identifier : registry.keySet()) {
|
||||
Biome biome = registry.getValue(identifier);
|
||||
if (biome != null) {
|
||||
biomes.add(ModdedBiome.of(biome, identifier.toString()));
|
||||
}
|
||||
}
|
||||
return biomes;
|
||||
}
|
||||
|
||||
private int idForKey(Registry<Biome> registry, String key) {
|
||||
Identifier identifier = Identifier.tryParse(key);
|
||||
if (identifier == null) {
|
||||
return -1;
|
||||
}
|
||||
Biome biome = registry.getValue(identifier);
|
||||
return biome == null ? -1 : registry.getId(biome);
|
||||
}
|
||||
|
||||
private int idForDerivative(Registry<Biome> registry, String key) {
|
||||
int colon = key.indexOf(':');
|
||||
if (colon <= 0 || colon >= key.length() - 1) {
|
||||
return -1;
|
||||
}
|
||||
String dimensionLoadKey = key.substring(0, colon);
|
||||
String customBiomeId = key.substring(colon + 1);
|
||||
IrisBiome owner = findCustomBiomeOwner(dimensionLoadKey, customBiomeId);
|
||||
if (owner == null) {
|
||||
return -1;
|
||||
}
|
||||
String derivativeKey = owner.getVanillaDerivativeKey();
|
||||
if (derivativeKey == null) {
|
||||
return -1;
|
||||
}
|
||||
return idForKey(registry, derivativeKey);
|
||||
}
|
||||
|
||||
private IrisBiome findCustomBiomeOwner(String dimensionLoadKey, String customBiomeId) {
|
||||
for (Engine engine : ModdedWorldEngines.activeEngines()) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
continue;
|
||||
}
|
||||
if (!dimensionLoadKey.equalsIgnoreCase(engine.getDimension().getLoadKey())) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiome biome : engine.getDimension().getAllBiomes(engine)) {
|
||||
if (!biome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom custom : biome.getCustomDerivitives()) {
|
||||
if (customBiomeId.equals(custom.getId())) {
|
||||
return biome;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private int fallbackId(Registry<Biome> registry) {
|
||||
int plains = idForKey(registry, VANILLA_FALLBACK_KEY);
|
||||
return plains >= 0 ? plains : 0;
|
||||
}
|
||||
|
||||
private Registry<Biome> biomeRegistry() {
|
||||
MinecraftServer instance = server.get();
|
||||
if (instance == null) {
|
||||
return null;
|
||||
}
|
||||
return instance.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.modded;
|
||||
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
|
||||
public final class ModdedBlockBuffer implements Hunk<PlatformBlockState> {
|
||||
private final PlatformBlockState[] data;
|
||||
private final PlatformBlockState air;
|
||||
private final int height;
|
||||
|
||||
public ModdedBlockBuffer(int height, PlatformBlockState air) {
|
||||
this.data = new PlatformBlockState[16 * height * 16];
|
||||
this.air = air;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
private int index(int x, int y, int z) {
|
||||
return (y * 16 + z) * 16 + x;
|
||||
}
|
||||
|
||||
public boolean isAir(int x, int y, int z) {
|
||||
return data[index(x, y, z)] == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() {
|
||||
return 16;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDepth() {
|
||||
return 16;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, PlatformBlockState t) {
|
||||
if (t == null) {
|
||||
return;
|
||||
}
|
||||
if (x < 0 || y < 0 || z < 0 || x >= 16 || y >= height || z >= 16) {
|
||||
return;
|
||||
}
|
||||
data[index(x, y, z)] = t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRaw(int x, int y, int z, PlatformBlockState t) {
|
||||
if (t == null) {
|
||||
return;
|
||||
}
|
||||
data[index(x, y, z)] = t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState getRaw(int x, int y, int z) {
|
||||
PlatformBlockState state = data[index(x, y, z)];
|
||||
return state == null ? air : state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState get(int x, int y, int z) {
|
||||
if (x < 0 || y < 0 || z < 0 || x >= 16 || y >= height || z >= 16) {
|
||||
return air;
|
||||
}
|
||||
return getRaw(x, y, z);
|
||||
}
|
||||
}
|
||||
+504
@@ -0,0 +1,504 @@
|
||||
/*
|
||||
* 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.modded;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.data.UnresolvedKeyLog;
|
||||
import com.mojang.brigadier.StringReader;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import net.minecraft.commands.arguments.blocks.BlockStateParser;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.EmptyBlockGetter;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LeavesBlock;
|
||||
import net.minecraft.world.level.block.PointedDripstoneBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.SpeleothemThickness;
|
||||
import net.minecraft.world.level.block.state.properties.Property;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class ModdedBlockResolution {
|
||||
private static final Set<Block> FOLIAGE = blockSet(
|
||||
"poppy", "dandelion", "cornflower", "sweet_berry_bush", "crimson_roots", "warped_roots",
|
||||
"nether_sprouts", "allium", "azure_bluet", "blue_orchid", "oxeye_daisy", "lily_of_the_valley",
|
||||
"wither_rose", "dark_oak_sapling", "acacia_sapling", "jungle_sapling", "birch_sapling",
|
||||
"spruce_sapling", "oak_sapling", "orange_tulip", "pink_tulip", "red_tulip", "white_tulip",
|
||||
"fern", "large_fern", "short_grass", "tall_grass");
|
||||
private static final Set<Block> DECORANT = decorantSet();
|
||||
private static final Set<Block> LIT = blockSet(
|
||||
"glowstone", "amethyst_cluster", "small_amethyst_bud", "medium_amethyst_bud", "large_amethyst_bud",
|
||||
"end_rod", "soul_sand", "torch", "redstone_torch", "soul_torch", "redstone_wall_torch", "wall_torch",
|
||||
"soul_wall_torch", "lantern", "candle", "jack_o_lantern", "redstone_lamp", "magma_block", "light",
|
||||
"shroomlight", "sea_lantern", "soul_lantern", "fire", "soul_fire", "sea_pickle", "brewing_stand",
|
||||
"redstone_ore");
|
||||
private static final Set<Block> STORAGE = blockSet(
|
||||
"chest", "smoker", "trapped_chest", "shulker_box", "white_shulker_box", "orange_shulker_box",
|
||||
"magenta_shulker_box", "light_blue_shulker_box", "yellow_shulker_box", "lime_shulker_box",
|
||||
"pink_shulker_box", "gray_shulker_box", "light_gray_shulker_box", "cyan_shulker_box",
|
||||
"purple_shulker_box", "blue_shulker_box", "brown_shulker_box", "green_shulker_box",
|
||||
"red_shulker_box", "black_shulker_box", "barrel", "dispenser", "dropper", "hopper", "furnace",
|
||||
"blast_furnace");
|
||||
private static final Set<Block> STORAGE_CHEST = storageChestSet();
|
||||
private static final Set<Block> DEEPSLATE = blockSet(
|
||||
"deepslate", "deepslate_bricks", "deepslate_brick_slab", "deepslate_brick_stairs",
|
||||
"deepslate_brick_wall", "deepslate_tile_slab", "deepslate_tiles", "deepslate_tile_stairs",
|
||||
"deepslate_tile_wall", "cracked_deepslate_tiles", "deepslate_coal_ore", "deepslate_iron_ore",
|
||||
"deepslate_copper_ore", "deepslate_diamond_ore", "deepslate_emerald_ore", "deepslate_gold_ore",
|
||||
"deepslate_lapis_ore", "deepslate_redstone_ore");
|
||||
private static final Map<Block, Block> NORMAL_TO_DEEPSLATE = oreMap(false);
|
||||
private static final Map<Block, Block> DEEPSLATE_TO_NORMAL = oreMap(true);
|
||||
private static final Set<Block> FOLIAGE_PLANTABLE_STATE = blockSet(
|
||||
"grass_block", "moss_block", "rooted_dirt", "dirt", "coarse_dirt", "podzol");
|
||||
private static final Set<Block> FOLIAGE_PLANTABLE_MATERIAL = blockSet(
|
||||
"grass_block", "moss_block", "dirt", "tall_grass", "tall_seagrass", "large_fern", "sunflower",
|
||||
"peony", "lilac", "rose_bush", "rooted_dirt", "coarse_dirt", "podzol");
|
||||
private static final Set<Block> PLACE_ONTO_LEAVES = blockSet(
|
||||
"acacia_leaves", "birch_leaves", "dark_oak_leaves", "jungle_leaves", "oak_leaves", "spruce_leaves");
|
||||
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
|
||||
private static final UnresolvedKeyLog UNRESOLVED = new UnresolvedKeyLog("Iris modded block resolution", 30_000L);
|
||||
|
||||
private ModdedBlockResolution() {
|
||||
}
|
||||
|
||||
record Parsed(BlockState state, Map<Property<?>, Comparable<?>> properties) {
|
||||
}
|
||||
|
||||
private static Set<Block> blockSet(String... ids) {
|
||||
Set<Block> blocks = new HashSet<>();
|
||||
for (String id : ids) {
|
||||
Identifier identifier = Identifier.parse("minecraft:" + id);
|
||||
if (BuiltInRegistries.BLOCK.containsKey(identifier)) {
|
||||
blocks.add(BuiltInRegistries.BLOCK.getValue(identifier));
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private static Set<Block> decorantSet() {
|
||||
Set<Block> blocks = blockSet(
|
||||
"short_grass", "tall_grass", "fern", "large_fern", "cornflower", "sunflower", "chorus_flower",
|
||||
"poppy", "dandelion", "oxeye_daisy", "orange_tulip", "pink_tulip", "red_tulip", "white_tulip",
|
||||
"lilac", "dead_bush", "sweet_berry_bush", "rose_bush", "wither_rose", "allium", "blue_orchid",
|
||||
"lily_of_the_valley", "crimson_fungus", "warped_fungus", "red_mushroom", "brown_mushroom",
|
||||
"crimson_roots", "azure_bluet", "weeping_vines", "weeping_vines_plant", "warped_roots",
|
||||
"nether_sprouts", "twisting_vines", "twisting_vines_plant", "sugar_cane", "wheat", "potatoes",
|
||||
"carrots", "beetroots", "nether_wart", "sea_pickle", "seagrass", "tall_seagrass",
|
||||
"acacia_button", "birch_button", "crimson_button", "dark_oak_button", "jungle_button",
|
||||
"oak_button", "polished_blackstone_button", "spruce_button", "stone_button", "warped_button",
|
||||
"torch", "soul_torch", "glow_lichen", "vine", "sculk_vein");
|
||||
blocks.addAll(FOLIAGE);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private static Set<Block> storageChestSet() {
|
||||
Set<Block> blocks = new HashSet<>(STORAGE);
|
||||
blocks.remove(Blocks.SMOKER);
|
||||
blocks.remove(Blocks.FURNACE);
|
||||
blocks.remove(Blocks.BLAST_FURNACE);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private static Map<Block, Block> oreMap(boolean deepslateToNormal) {
|
||||
String[][] pairs = {
|
||||
{"coal_ore", "deepslate_coal_ore"},
|
||||
{"emerald_ore", "deepslate_emerald_ore"},
|
||||
{"diamond_ore", "deepslate_diamond_ore"},
|
||||
{"copper_ore", "deepslate_copper_ore"},
|
||||
{"gold_ore", "deepslate_gold_ore"},
|
||||
{"iron_ore", "deepslate_iron_ore"},
|
||||
{"lapis_ore", "deepslate_lapis_ore"},
|
||||
{"redstone_ore", "deepslate_redstone_ore"}
|
||||
};
|
||||
Map<Block, Block> map = new HashMap<>();
|
||||
for (String[] pair : pairs) {
|
||||
Block normal = BuiltInRegistries.BLOCK.getValue(Identifier.parse("minecraft:" + pair[0]));
|
||||
Block deep = BuiltInRegistries.BLOCK.getValue(Identifier.parse("minecraft:" + pair[1]));
|
||||
if (deepslateToNormal) {
|
||||
map.put(deep, normal);
|
||||
} else {
|
||||
map.put(normal, deep);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void warnUnresolved(String key, String message) {
|
||||
if (UNRESOLVED.firstOccurrence(key)) {
|
||||
IrisLogging.warn(message);
|
||||
}
|
||||
String summary = UNRESOLVED.pollSummary();
|
||||
if (summary != null) {
|
||||
IrisLogging.warn(summary);
|
||||
}
|
||||
}
|
||||
|
||||
public static BlockState airState() {
|
||||
return AIR;
|
||||
}
|
||||
|
||||
public static ModdedBlockState getAir() {
|
||||
return ModdedBlockState.of(AIR, null);
|
||||
}
|
||||
|
||||
public static ModdedBlockState get(String bdxf) {
|
||||
Parsed parsed = resolveGet(bdxf);
|
||||
return ModdedBlockState.of(parsed.state(), parsed.properties());
|
||||
}
|
||||
|
||||
public static ModdedBlockState getNoCompat(String bdxf) {
|
||||
Parsed parsed = resolveNoCompat(bdxf);
|
||||
return ModdedBlockState.of(parsed.state(), parsed.properties());
|
||||
}
|
||||
|
||||
public static ModdedBlockState getOrNull(String bdxf) {
|
||||
return getOrNull(bdxf, false);
|
||||
}
|
||||
|
||||
public static ModdedBlockState getOrNull(String bdxf, boolean warn) {
|
||||
Parsed parsed = resolveOrNull(bdxf, warn);
|
||||
return parsed == null ? null : ModdedBlockState.of(parsed.state(), parsed.properties());
|
||||
}
|
||||
|
||||
static Parsed resolveGet(String bdxf) {
|
||||
Parsed parsed = resolveOrNull(bdxf, false);
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
}
|
||||
IrisLogging.error("Can't find block data for " + bdxf);
|
||||
return new Parsed(AIR, null);
|
||||
}
|
||||
|
||||
static Parsed resolveNoCompat(String bdxf) {
|
||||
Parsed parsed = resolveOrNull(bdxf, true);
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
}
|
||||
return new Parsed(AIR, null);
|
||||
}
|
||||
|
||||
static Parsed resolveOrNull(String bdxf, boolean warn) {
|
||||
try {
|
||||
String bd = bdxf.trim();
|
||||
|
||||
if (bd.startsWith("minecraft:cauldron[level=")) {
|
||||
bd = bd.replaceAll("\\Q:cauldron[\\E", ":water_cauldron[");
|
||||
}
|
||||
|
||||
if (bd.equals("minecraft:grass_path")) {
|
||||
return new Parsed(Blocks.DIRT_PATH.defaultBlockState(), null);
|
||||
}
|
||||
|
||||
Parsed bdx = parseBlockData(bd, warn);
|
||||
|
||||
if (bdx == null) {
|
||||
BlockState provided = ModdedCustomContentRegistry.resolveBlock(bd);
|
||||
if (provided != null) {
|
||||
return new Parsed(provided, null);
|
||||
}
|
||||
if (warn) {
|
||||
warnUnresolved(bd, "Unknown Block Data '" + bd + "'");
|
||||
}
|
||||
return new Parsed(AIR, null);
|
||||
}
|
||||
|
||||
return bdx;
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
if (warn) {
|
||||
warnUnresolved(bdxf, "Unknown Block Data '" + bdxf + "'");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static ModdedBlockState strictParse(String key) {
|
||||
Parsed parsed = parseStrict(key);
|
||||
return ModdedBlockState.of(parsed.state(), parsed.properties());
|
||||
}
|
||||
|
||||
private static Parsed parseStrict(String key) {
|
||||
StringReader reader = new StringReader(key);
|
||||
BlockStateParser.BlockResult result;
|
||||
try {
|
||||
result = BlockStateParser.parseForBlock(BuiltInRegistries.BLOCK, reader, false);
|
||||
} catch (CommandSyntaxException e) {
|
||||
throw new IllegalArgumentException("Could not parse data: " + key, e);
|
||||
}
|
||||
if (reader.canRead()) {
|
||||
throw new IllegalArgumentException("Could not parse remainder: " + reader.getRemaining());
|
||||
}
|
||||
return new Parsed(result.blockState(), result.properties());
|
||||
}
|
||||
|
||||
private static Parsed createBlockData(String s, boolean warn) {
|
||||
try {
|
||||
return parseStrict(s);
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (s.contains("[")) {
|
||||
return createBlockData(s.split("\\Q[\\E")[0], warn);
|
||||
}
|
||||
}
|
||||
|
||||
if (warn) {
|
||||
IrisLogging.warn("Can't find block data for " + s);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Parsed materialBlockData(String ix) {
|
||||
if (ix.contains("[") || ix.contains(":")) {
|
||||
return null;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse("minecraft:" + ix.toLowerCase(Locale.ROOT));
|
||||
if (identifier == null || !BuiltInRegistries.BLOCK.containsKey(identifier)) {
|
||||
return null;
|
||||
}
|
||||
return new Parsed(BuiltInRegistries.BLOCK.getValue(identifier).defaultBlockState(), null);
|
||||
}
|
||||
|
||||
private static Parsed parseBlockData(String ix, boolean warn) {
|
||||
try {
|
||||
Parsed bx = createBlockData(ix.toLowerCase(), warn);
|
||||
|
||||
if (bx == null) {
|
||||
bx = createBlockData("minecraft:" + ix.toLowerCase(), warn);
|
||||
}
|
||||
|
||||
if (bx == null) {
|
||||
bx = materialBlockData(ix);
|
||||
}
|
||||
|
||||
if (bx == null) {
|
||||
if (warn) {
|
||||
warnUnresolved(ix, "Unknown Block Data: " + ix);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (bx.state().getBlock() instanceof LeavesBlock) {
|
||||
BlockState mutated = bx.state().setValue(LeavesBlock.PERSISTENT, shouldPreventLeafDecay());
|
||||
bx = new Parsed(mutated, bx.properties());
|
||||
}
|
||||
|
||||
return bx;
|
||||
} catch (Throwable e) {
|
||||
String block = ix.contains(":") ? ix.split(":")[1].toLowerCase() : ix.toLowerCase();
|
||||
String state = block.contains("[") ? block.split("\\Q[\\E")[1].split("\\Q]\\E")[0] : "";
|
||||
Map<String, String> stateMap = new HashMap<>();
|
||||
if (!state.equals("")) {
|
||||
Arrays.stream(state.split(",")).forEach((String s) -> stateMap.put(s.split("=")[0], s.split("=")[1]));
|
||||
}
|
||||
block = block.split("\\Q[\\E")[0];
|
||||
|
||||
switch (block) {
|
||||
case "cauldron" -> block = "water_cauldron";
|
||||
case "grass_path" -> block = "dirt_path";
|
||||
case "concrete" -> block = "white_concrete";
|
||||
case "wool" -> block = "white_wool";
|
||||
case "beetroots" -> {
|
||||
if (stateMap.containsKey("age")) {
|
||||
String updated = stateMap.get("age");
|
||||
switch (updated) {
|
||||
case "7" -> updated = "3";
|
||||
case "3", "4", "5" -> updated = "2";
|
||||
case "1", "2" -> updated = "1";
|
||||
}
|
||||
stateMap.put("age", updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> newStates = new HashMap<>();
|
||||
for (String key : stateMap.keySet()) {
|
||||
createBlockData(block + "[" + key + "=" + stateMap.get(key) + "]", false);
|
||||
newStates.put(key, stateMap.get(key));
|
||||
}
|
||||
|
||||
String joined = newStates.entrySet().stream()
|
||||
.map((Map.Entry<String, String> entry) -> entry.getKey() + "=" + entry.getValue())
|
||||
.collect(Collectors.joining(","));
|
||||
if (!joined.equals("")) {
|
||||
joined = "[" + joined + "]";
|
||||
}
|
||||
String newBlock = block + joined;
|
||||
IrisLogging.debug("Converting " + ix + " to " + newBlock);
|
||||
|
||||
try {
|
||||
return createBlockData(newBlock, false);
|
||||
} catch (Throwable e1) {
|
||||
IrisLogging.reportError(e1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldPreventLeafDecay() {
|
||||
return IrisSettings.get().getGenerator().isPreventLeafDecay();
|
||||
}
|
||||
|
||||
public static BlockState toDeepSlateOre(BlockState block, BlockState ore) {
|
||||
Block key = ore.getBlock();
|
||||
|
||||
if (isDeepSlate(block)) {
|
||||
Block mapped = NORMAL_TO_DEEPSLATE.get(key);
|
||||
if (mapped != null) {
|
||||
return mapped.defaultBlockState();
|
||||
}
|
||||
} else {
|
||||
Block mapped = DEEPSLATE_TO_NORMAL.get(key);
|
||||
if (mapped != null) {
|
||||
return mapped.defaultBlockState();
|
||||
}
|
||||
}
|
||||
|
||||
return ore;
|
||||
}
|
||||
|
||||
public static boolean isDeepSlate(BlockState state) {
|
||||
return DEEPSLATE.contains(state.getBlock());
|
||||
}
|
||||
|
||||
public static boolean isOre(BlockState state) {
|
||||
return BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath().endsWith("_ore");
|
||||
}
|
||||
|
||||
public static boolean isAir(BlockState state) {
|
||||
if (state == null) {
|
||||
return true;
|
||||
}
|
||||
Block block = state.getBlock();
|
||||
return block == Blocks.AIR || block == Blocks.CAVE_AIR || block == Blocks.VOID_AIR;
|
||||
}
|
||||
|
||||
public static boolean isSolid(BlockState state) {
|
||||
if (state == null) {
|
||||
return false;
|
||||
}
|
||||
return isSolidMaterial(state.getBlock());
|
||||
}
|
||||
|
||||
private static boolean isSolidMaterial(Block block) {
|
||||
return block.defaultBlockState().blocksMotion();
|
||||
}
|
||||
|
||||
public static boolean isOccluding(BlockState state) {
|
||||
return state.getBlock().defaultBlockState().isRedstoneConductor(EmptyBlockGetter.INSTANCE, BlockPos.ZERO);
|
||||
}
|
||||
|
||||
public static boolean isFluid(BlockState state) {
|
||||
Block block = state.getBlock();
|
||||
return block == Blocks.WATER || block == Blocks.LAVA;
|
||||
}
|
||||
|
||||
public static boolean isWater(BlockState state) {
|
||||
return state.getBlock() == Blocks.WATER;
|
||||
}
|
||||
|
||||
public static boolean isWaterLogged(BlockState state) {
|
||||
return state.hasProperty(BlockStateProperties.WATERLOGGED) && state.getValue(BlockStateProperties.WATERLOGGED);
|
||||
}
|
||||
|
||||
public static boolean isLit(BlockState state) {
|
||||
return LIT.contains(state.getBlock());
|
||||
}
|
||||
|
||||
public static boolean isUpdatable(BlockState state) {
|
||||
return isStorage(state)
|
||||
|| (state.getBlock() instanceof PointedDripstoneBlock
|
||||
&& state.getValue(PointedDripstoneBlock.THICKNESS) == SpeleothemThickness.TIP);
|
||||
}
|
||||
|
||||
public static boolean isFoliage(BlockState state) {
|
||||
return FOLIAGE.contains(state.getBlock());
|
||||
}
|
||||
|
||||
public static boolean isFoliagePlantable(BlockState state) {
|
||||
return FOLIAGE_PLANTABLE_STATE.contains(state.getBlock());
|
||||
}
|
||||
|
||||
public static boolean isDecorant(BlockState state) {
|
||||
return DECORANT.contains(state.getBlock());
|
||||
}
|
||||
|
||||
public static boolean isStorage(BlockState state) {
|
||||
return STORAGE.contains(state.getBlock());
|
||||
}
|
||||
|
||||
public static boolean isStorageChest(BlockState state) {
|
||||
return STORAGE_CHEST.contains(state.getBlock());
|
||||
}
|
||||
|
||||
public static boolean isVineBlock(BlockState state) {
|
||||
Block block = state.getBlock();
|
||||
return block == Blocks.VINE || block == Blocks.SCULK_VEIN || block == Blocks.GLOW_LICHEN;
|
||||
}
|
||||
|
||||
public static boolean hasTileEntity(BlockState state) {
|
||||
return state.getBlock().defaultBlockState().hasBlockEntity();
|
||||
}
|
||||
|
||||
public static boolean canPlaceOnto(Block mat, Block onto) {
|
||||
if ((onto == Blocks.CRIMSON_NYLIUM || onto == Blocks.WARPED_NYLIUM)
|
||||
&& (mat == Blocks.CRIMSON_FUNGUS || mat == Blocks.CRIMSON_ROOTS
|
||||
|| mat == Blocks.WARPED_FUNGUS || mat == Blocks.WARPED_ROOTS)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (FOLIAGE.contains(mat)) {
|
||||
if (!FOLIAGE_PLANTABLE_MATERIAL.contains(onto)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (onto == Blocks.AIR || onto == Blocks.CAVE_AIR || onto == Blocks.VOID_AIR) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (onto == Blocks.GRASS_BLOCK && mat == Blocks.DEAD_BUSH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (onto == Blocks.DIRT_PATH) {
|
||||
if (!isSolidMaterial(mat)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (PLACE_ONTO_LEAVES.contains(onto)) {
|
||||
return isSolidMaterial(mat);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
/*
|
||||
* 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.modded;
|
||||
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.Property;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class ModdedBlockState implements PlatformBlockState {
|
||||
private static final ConcurrentHashMap<String, ModdedBlockState> CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
private final BlockState state;
|
||||
private final Map<Property<?>, Comparable<?>> parsedProperties;
|
||||
private final String key;
|
||||
private final String namespace;
|
||||
private volatile Boolean air;
|
||||
private volatile Boolean solid;
|
||||
private volatile Boolean occluding;
|
||||
private volatile Boolean fluid;
|
||||
private volatile Boolean water;
|
||||
private volatile Boolean foliage;
|
||||
private volatile Boolean decorant;
|
||||
|
||||
private ModdedBlockState(BlockState state, Map<Property<?>, Comparable<?>> parsedProperties, String key) {
|
||||
this.state = state;
|
||||
this.parsedProperties = parsedProperties;
|
||||
this.key = key;
|
||||
this.namespace = parseNamespace(key);
|
||||
}
|
||||
|
||||
public static ModdedBlockState of(BlockState state, Map<Property<?>, Comparable<?>> parsedProperties) {
|
||||
String key = serialize(state);
|
||||
return CACHE.computeIfAbsent(key, (String k) -> new ModdedBlockState(state, parsedProperties, k));
|
||||
}
|
||||
|
||||
public static String serialize(BlockState state) {
|
||||
StringBuilder result = new StringBuilder(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString());
|
||||
if (!state.isSingletonState()) {
|
||||
result.append('[');
|
||||
result.append(state.getValues()
|
||||
.map((Property.Value<?> value) -> value.property().getName() + "=" + value.valueName())
|
||||
.collect(Collectors.joining(",")));
|
||||
result.append(']');
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public BlockState handle() {
|
||||
return state;
|
||||
}
|
||||
|
||||
Map<Property<?>, Comparable<?>> parsedProperties() {
|
||||
return parsedProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof ModdedBlockState blockState)) {
|
||||
return false;
|
||||
}
|
||||
return state.equals(blockState.state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return key.hashCode();
|
||||
}
|
||||
|
||||
private static String parseNamespace(String key) {
|
||||
String base = key;
|
||||
int bracket = base.indexOf('[');
|
||||
if (bracket >= 0) {
|
||||
base = base.substring(0, bracket);
|
||||
}
|
||||
int colon = base.indexOf(':');
|
||||
return colon >= 0 ? base.substring(0, colon) : "minecraft";
|
||||
}
|
||||
|
||||
private static String mergeProperty(String key, String name, String value) {
|
||||
int bracket = key.indexOf('[');
|
||||
if (bracket < 0) {
|
||||
return key + "[" + name + "=" + value + "]";
|
||||
}
|
||||
String base = key.substring(0, bracket);
|
||||
String body = key.substring(bracket + 1, key.lastIndexOf(']'));
|
||||
LinkedHashMap<String, String> properties = new LinkedHashMap<>();
|
||||
for (String entry : body.split(",")) {
|
||||
int equals = entry.indexOf('=');
|
||||
if (equals < 0) {
|
||||
continue;
|
||||
}
|
||||
properties.put(entry.substring(0, equals).trim(), entry.substring(equals + 1).trim());
|
||||
}
|
||||
properties.put(name, value);
|
||||
StringBuilder merged = new StringBuilder(base).append('[');
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, String> property : properties.entrySet()) {
|
||||
if (!first) {
|
||||
merged.append(',');
|
||||
}
|
||||
merged.append(property.getKey()).append('=').append(property.getValue());
|
||||
first = false;
|
||||
}
|
||||
return merged.append(']').toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String namespace() {
|
||||
return namespace;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAir() {
|
||||
Boolean cached = air;
|
||||
if (cached == null) {
|
||||
cached = ModdedBlockResolution.isAir(state);
|
||||
air = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolid() {
|
||||
Boolean cached = solid;
|
||||
if (cached == null) {
|
||||
cached = ModdedBlockResolution.isSolid(state);
|
||||
solid = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOccluding() {
|
||||
Boolean cached = occluding;
|
||||
if (cached == null) {
|
||||
cached = ModdedBlockResolution.isOccluding(state);
|
||||
occluding = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCustom() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFluid() {
|
||||
Boolean cached = fluid;
|
||||
if (cached == null) {
|
||||
cached = ModdedBlockResolution.isFluid(state);
|
||||
fluid = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWater() {
|
||||
Boolean cached = water;
|
||||
if (cached == null) {
|
||||
cached = ModdedBlockResolution.isWater(state);
|
||||
water = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWaterLogged() {
|
||||
return ModdedBlockResolution.isWaterLogged(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLit() {
|
||||
return ModdedBlockResolution.isLit(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUpdatable() {
|
||||
return ModdedBlockResolution.isUpdatable(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoliage() {
|
||||
Boolean cached = foliage;
|
||||
if (cached == null) {
|
||||
cached = ModdedBlockResolution.isFoliage(state);
|
||||
foliage = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoliagePlantable() {
|
||||
return ModdedBlockResolution.isFoliagePlantable(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDecorant() {
|
||||
Boolean cached = decorant;
|
||||
if (cached == null) {
|
||||
cached = ModdedBlockResolution.isDecorant(state);
|
||||
decorant = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStorage() {
|
||||
return ModdedBlockResolution.isStorage(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStorageChest() {
|
||||
return ModdedBlockResolution.isStorageChest(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOre() {
|
||||
return ModdedBlockResolution.isOre(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDeepSlate() {
|
||||
return ModdedBlockResolution.isDeepSlate(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVineBlock() {
|
||||
return ModdedBlockResolution.isVineBlock(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPlaceOnto(PlatformBlockState onto) {
|
||||
return ModdedBlockResolution.canPlaceOnto(state.getBlock(), ((BlockState) onto.nativeHandle()).getBlock());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(PlatformBlockState other) {
|
||||
if (!(other instanceof ModdedBlockState blockState)) {
|
||||
return false;
|
||||
}
|
||||
if (state.getBlock() != blockState.state.getBlock()) {
|
||||
return false;
|
||||
}
|
||||
if (state.equals(blockState.state)) {
|
||||
return true;
|
||||
}
|
||||
if (blockState.parsedProperties == null) {
|
||||
return false;
|
||||
}
|
||||
BlockState merged = state;
|
||||
for (Map.Entry<Property<?>, Comparable<?>> entry : blockState.parsedProperties.entrySet()) {
|
||||
merged = applyProperty(merged, entry.getKey(), entry.getValue());
|
||||
}
|
||||
return merged.equals(state);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T extends Comparable<T>> BlockState applyProperty(BlockState state, Property<?> property, Comparable<?> value) {
|
||||
return state.setValue((Property<T>) property, (T) value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasTileEntity() {
|
||||
return ModdedBlockResolution.hasTileEntity(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState withProperty(String name, String value) {
|
||||
String merged = mergeProperty(key, name, value);
|
||||
return ModdedBlockResolution.strictParse(merged);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object nativeHandle() {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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.modded;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.InventorySlotType;
|
||||
import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
|
||||
import java.util.Set;
|
||||
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 ModdedDeathLoot() {
|
||||
}
|
||||
|
||||
public static void tag(LivingEntity entity, KList<String> tableKeys, int spawnX, int spawnY, int spawnZ, RNG rng) {
|
||||
if (entity == null || tableKeys == null || tableKeys.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
entity.addTag(TAG_PREFIX + rng.getSeed() + "|" + spawnX + "|" + spawnY + "|" + spawnZ + "|" + String.join(",", tableKeys));
|
||||
}
|
||||
|
||||
public static void handle(LivingEntity entity) {
|
||||
if (entity == null || !(entity.level() instanceof ServerLevel level)) {
|
||||
return;
|
||||
}
|
||||
String encoded = findTag(entity);
|
||||
if (encoded == null) {
|
||||
return;
|
||||
}
|
||||
String[] parts = encoded.substring(TAG_PREFIX.length()).split("\\|", 5);
|
||||
if (parts.length < 5) {
|
||||
IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'");
|
||||
return;
|
||||
}
|
||||
long seed;
|
||||
int spawnX;
|
||||
int spawnY;
|
||||
int spawnZ;
|
||||
try {
|
||||
seed = Long.parseLong(parts[0]);
|
||||
spawnX = Integer.parseInt(parts[1]);
|
||||
spawnY = Integer.parseInt(parts[2]);
|
||||
spawnZ = Integer.parseInt(parts[3]);
|
||||
} catch (NumberFormatException error) {
|
||||
IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'");
|
||||
return;
|
||||
}
|
||||
Engine engine = engineFor(level);
|
||||
if (engine == null) {
|
||||
return;
|
||||
}
|
||||
RNG lootRng = new RNG(seed).nextParallelRNG(LOOT_RNG_SALT);
|
||||
KList<ItemStack> drops = new KList<>();
|
||||
for (String key : parts[4].split(",")) {
|
||||
String trimmed = key.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
IrisLootTable table = engine.getData().getLootLoader().load(trimmed);
|
||||
if (table == null) {
|
||||
if (WARNED_TABLES.add(trimmed)) {
|
||||
IrisLogging.warn("Iris death loot: unknown loot table '" + trimmed + "'");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
drops.addAll(ModdedItemTranslator.loot(table, lootRng, InventorySlotType.STORAGE, level, spawnX, spawnY, spawnZ));
|
||||
}
|
||||
emit(level, entity, drops);
|
||||
}
|
||||
|
||||
private static void emit(ServerLevel level, LivingEntity entity, KList<ItemStack> drops) {
|
||||
double x = entity.getX();
|
||||
double y = entity.getY() + 0.5D;
|
||||
double z = entity.getZ();
|
||||
for (ItemStack stack : drops) {
|
||||
if (stack == null || stack.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
ItemEntity itemEntity = new ItemEntity(level, x, y, z, stack);
|
||||
itemEntity.setDefaultPickUpDelay();
|
||||
level.addFreshEntity(itemEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private static String findTag(LivingEntity entity) {
|
||||
for (String tag : entity.entityTags()) {
|
||||
if (tag.startsWith(TAG_PREFIX)) {
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Engine engineFor(ServerLevel level) {
|
||||
ChunkGenerator generator = level.getChunkSource().getGenerator();
|
||||
if (generator instanceof IrisModdedChunkGenerator irisGenerator) {
|
||||
return irisGenerator.engineIfBound();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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.modded;
|
||||
|
||||
import art.arcane.iris.engine.decorator.DecoratorPlatformHooks;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.EmptyBlockGetter;
|
||||
import net.minecraft.world.level.block.SupportType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BooleanProperty;
|
||||
import net.minecraft.world.level.block.state.properties.Property;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class ModdedDecoratorHooks implements DecoratorPlatformHooks.FaceFixer, DecoratorPlatformHooks.SurfaceSturdiness {
|
||||
private static final Direction[] CARTESIAN = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.UP, Direction.DOWN};
|
||||
private static final String[] FACE_NAMES = {"north", "east", "south", "west", "up", "down"};
|
||||
|
||||
@Override
|
||||
public PlatformBlockState fixFaces(PlatformBlockState state, Hunk<PlatformBlockState> hunk, int rX, int rZ, int x, int y, int z, EngineMantle mantle) {
|
||||
ModdedBlockState fabric = (ModdedBlockState) state;
|
||||
BlockState cloned = fabric.handle();
|
||||
Map<String, BooleanProperty> allowed = faceProperties(cloned);
|
||||
|
||||
for (Map.Entry<String, BooleanProperty> entry : allowed.entrySet()) {
|
||||
if (cloned.getValue(entry.getValue())) {
|
||||
cloned = cloned.setValue(entry.getValue(), Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
boolean found = false;
|
||||
for (Direction f : CARTESIAN) {
|
||||
int yy = y + f.getStepY();
|
||||
|
||||
PlatformBlockState rs = null;
|
||||
if (mantle != null) {
|
||||
rs = mantle.getMantle().get(x + f.getStepX(), yy, z + f.getStepZ(), PlatformBlockState.class);
|
||||
}
|
||||
BlockState r = rs == null ? (BlockState) EngineMantle.AIR.nativeHandle() : (BlockState) rs.nativeHandle();
|
||||
if (isFaceSturdy(r, f.getOpposite())) {
|
||||
BooleanProperty property = allowed.get(f.getSerializedName());
|
||||
if (property != null) {
|
||||
found = true;
|
||||
cloned = cloned.setValue(property, Boolean.TRUE);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
int xx = rX + f.getStepX();
|
||||
int zz = rZ + f.getStepZ();
|
||||
if (xx < 0 || xx > 15 || zz < 0 || zz > 15 || yy < 0 || yy > hunk.getHeight()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
r = (BlockState) hunk.get(xx, yy, zz).nativeHandle();
|
||||
if (isFaceSturdy(r, f.getOpposite())) {
|
||||
BooleanProperty property = allowed.get(f.getSerializedName());
|
||||
if (property != null) {
|
||||
found = true;
|
||||
cloned = cloned.setValue(property, Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
String fallback = allowed.containsKey("down") ? "down" : "up";
|
||||
BooleanProperty property = allowed.get(fallback);
|
||||
if (property != null) {
|
||||
cloned = cloned.setValue(property, Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
return ModdedBlockState.of(cloned, fabric.parsedProperties());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canGoOn(PlatformBlockState surface) {
|
||||
return isFaceSturdy((BlockState) surface.nativeHandle(), Direction.UP);
|
||||
}
|
||||
|
||||
private static boolean isFaceSturdy(BlockState state, Direction face) {
|
||||
return state.isFaceSturdy(EmptyBlockGetter.INSTANCE, BlockPos.ZERO, face, SupportType.FULL);
|
||||
}
|
||||
|
||||
private static Map<String, BooleanProperty> faceProperties(BlockState state) {
|
||||
Map<String, BooleanProperty> properties = new LinkedHashMap<>();
|
||||
for (String name : FACE_NAMES) {
|
||||
for (Property<?> property : state.getProperties()) {
|
||||
if (property.getName().equals(name) && property instanceof BooleanProperty bool) {
|
||||
properties.put(name, bool);
|
||||
}
|
||||
}
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* 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.modded;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.level.TicketType;
|
||||
import net.minecraft.world.entity.Relative;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.biome.FixedBiomeSource;
|
||||
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.storage.DerivedLevelData;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import net.minecraft.world.level.storage.ServerLevelData;
|
||||
import net.minecraft.world.level.storage.WorldData;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public final class ModdedDimensionManager {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final Object LOCK = new Object();
|
||||
private static final ConcurrentHashMap<String, Handle> HANDLES = new ConcurrentHashMap<>();
|
||||
private static final Set<String> TYPE_FALLBACK_WARNINGS = ConcurrentHashMap.newKeySet();
|
||||
private static final TicketType TELEPORT_WARM_TICKET = new TicketType(TicketType.NO_TIMEOUT, TicketType.FLAG_LOADING);
|
||||
private static volatile ModdedServerAccess access;
|
||||
|
||||
private ModdedDimensionManager() {
|
||||
}
|
||||
|
||||
public static void bindAccess(ModdedServerAccess serverAccess) {
|
||||
access = serverAccess;
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
HANDLES.clear();
|
||||
}
|
||||
|
||||
public static Handle handle(String dimensionId) {
|
||||
return HANDLES.get(dimensionId);
|
||||
}
|
||||
|
||||
public static List<Handle> handles() {
|
||||
return new ArrayList<>(HANDLES.values());
|
||||
}
|
||||
|
||||
public static ServerLevel level(MinecraftServer server, String dimensionId) {
|
||||
Handle handle = HANDLES.get(dimensionId);
|
||||
if (handle != null && handle.level().getServer() == server) {
|
||||
return handle.level();
|
||||
}
|
||||
ResourceKey<Level> key = levelKey(dimensionId);
|
||||
for (ServerLevel level : server.getAllLevels()) {
|
||||
if (level.dimension().equals(key)) {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Engine engine(MinecraftServer server, String dimensionId) {
|
||||
ServerLevel level = level(server, dimensionId);
|
||||
if (level == null) {
|
||||
return null;
|
||||
}
|
||||
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
|
||||
return null;
|
||||
}
|
||||
return generator.commandEngine();
|
||||
}
|
||||
|
||||
public static Handle create(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
|
||||
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);
|
||||
Handle refreshed = new Handle(dimensionId, pack, packDimensionKey, seed, existing.level(), existing.generator());
|
||||
HANDLES.put(dimensionId, refreshed);
|
||||
return refreshed;
|
||||
}
|
||||
if (serverAccess.hasLevel(server, key)) {
|
||||
ServerLevel present = level(server, dimensionId);
|
||||
if (present == null || !(present.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
|
||||
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);
|
||||
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, present, generator);
|
||||
HANDLES.put(dimensionId, handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
try {
|
||||
Handle handle = inject(server, serverAccess, dimensionId, key, pack, packDimensionKey, seed);
|
||||
HANDLES.put(dimensionId, handle);
|
||||
LOGGER.info("Iris injected runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed);
|
||||
return handle;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to inject runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed, e);
|
||||
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Handle createPersistent(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
|
||||
Handle handle = create(server, dimensionId, pack, packDimensionKey, seed);
|
||||
ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(dimensionId, pack, packDimensionKey, seed));
|
||||
return handle;
|
||||
}
|
||||
|
||||
public static boolean removePersistent(MinecraftServer server, String dimensionId, boolean wipeStorage) {
|
||||
boolean removed = remove(server, dimensionId, wipeStorage);
|
||||
ModdedDimensionRegistryStore.remove(server, dimensionId);
|
||||
return removed;
|
||||
}
|
||||
|
||||
public static boolean remove(MinecraftServer server, String dimensionId, boolean wipeStorage) {
|
||||
ModdedServerAccess serverAccess = requireAccess();
|
||||
synchronized (LOCK) {
|
||||
ResourceKey<Level> key = levelKey(dimensionId);
|
||||
ServerLevel level = level(server, dimensionId);
|
||||
if (level == null) {
|
||||
HANDLES.remove(dimensionId);
|
||||
if (wipeStorage) {
|
||||
ModdedDimensionStorage.wipe(server, key);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
evacuate(server, level);
|
||||
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) {
|
||||
generator.unbindEngine();
|
||||
}
|
||||
ModdedWorldEngines.evict(level);
|
||||
level.save(null, true, false);
|
||||
serverAccess.removeLevel(server, key);
|
||||
level.close();
|
||||
HANDLES.remove(dimensionId);
|
||||
if (wipeStorage) {
|
||||
ModdedDimensionStorage.wipe(server, key);
|
||||
}
|
||||
LOGGER.info("Iris removed runtime dimension '{}'", dimensionId);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to remove runtime dimension '{}'", dimensionId, e);
|
||||
throw new IllegalStateException("Iris runtime dimension removal failed for " + dimensionId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean teleport(ServerPlayer player, MinecraftServer server, String dimensionId, double x, double y, double z) {
|
||||
ServerLevel level = level(server, dimensionId);
|
||||
if (level == null) {
|
||||
return false;
|
||||
}
|
||||
int blockX = (int) Math.floor(x);
|
||||
int blockZ = (int) Math.floor(z);
|
||||
ChunkPos chunkPos = new ChunkPos(blockX >> 4, blockZ >> 4);
|
||||
if (level.getChunkSource().hasChunk(chunkPos.x(), chunkPos.z())) {
|
||||
completeTeleport(player, level, x, y, z, blockX, blockZ);
|
||||
return true;
|
||||
}
|
||||
UUID playerId = player.getUUID();
|
||||
CompletableFuture
|
||||
.supplyAsync(() -> level.getChunkSource().addTicketAndLoadWithRadius(TELEPORT_WARM_TICKET, chunkPos, 1), server)
|
||||
.thenCompose((CompletableFuture<?> inner) -> inner)
|
||||
.whenComplete((Object result, Throwable error) -> server.execute(() -> {
|
||||
level.getChunkSource().removeTicketWithRadius(TELEPORT_WARM_TICKET, chunkPos, 1);
|
||||
if (error != null) {
|
||||
LOGGER.warn("Iris chunk warm for teleport into '{}' at {},{} failed: {}", dimensionId, chunkPos.x(), chunkPos.z(), error.toString());
|
||||
}
|
||||
ServerPlayer target = server.getPlayerList().getPlayer(playerId);
|
||||
if (target == null) {
|
||||
return;
|
||||
}
|
||||
completeTeleport(target, level, x, y, z, blockX, blockZ);
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void completeTeleport(ServerPlayer player, ServerLevel level, double x, double y, double z, int blockX, int blockZ) {
|
||||
double targetY = y;
|
||||
if (y == Double.MIN_VALUE) {
|
||||
targetY = level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ);
|
||||
}
|
||||
player.teleportTo(level, x, targetY, z, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
|
||||
}
|
||||
|
||||
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) {
|
||||
String typeRef = ModdedForcedDatapack.dimensionTypeRef(dimension);
|
||||
ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef));
|
||||
Optional<Holder.Reference<DimensionType>> packType = registry.get(typeKey);
|
||||
if (packType.isPresent()) {
|
||||
return packType.get();
|
||||
}
|
||||
if (TYPE_FALLBACK_WARNINGS.add(typeRef)) {
|
||||
LOGGER.warn("Iris dimension type {} (pack {} dim {}) is not registered yet; injecting with fallback heights. Restart the server so the forced datapack installs it.", typeRef, pack, packDimensionKey);
|
||||
}
|
||||
}
|
||||
ResourceKey<DimensionType> studioPool = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse("irisworldgen:studio_pool"));
|
||||
return registry.get(studioPool)
|
||||
.map(reference -> (Holder<DimensionType>) reference)
|
||||
.orElseGet(() -> registry.getOrThrow(BuiltinDimensionTypes.OVERWORLD));
|
||||
}
|
||||
|
||||
private static IrisDimension loadPackDimension(String pack, String packDimensionKey) {
|
||||
try {
|
||||
File packFolder = ModdedWorldEngines.packFolder(pack);
|
||||
if (!packFolder.isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
return IrisData.get(packFolder).getDimensionLoader().load(packDimensionKey);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.warn("Iris could not load pack '{}' dimension '{}' for dimension type resolution: {}", pack, packDimensionKey, e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Handle inject(MinecraftServer server, ModdedServerAccess serverAccess, String dimensionId, ResourceKey<Level> key, String pack, String packDimensionKey, long seed) {
|
||||
RegistryAccess registryAccess = server.registryAccess();
|
||||
Holder<DimensionType> dimensionType = resolveDimensionType(registryAccess, pack, packDimensionKey);
|
||||
Holder<Biome> plains = registryAccess.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS);
|
||||
FixedBiomeSource biomeSource = new FixedBiomeSource(plains);
|
||||
String generatorRef = pack.equals(packDimensionKey) ? pack : pack + ":" + packDimensionKey;
|
||||
IrisModdedChunkGenerator generator = new IrisModdedChunkGenerator(biomeSource, generatorRef);
|
||||
generator.repoint(pack, packDimensionKey, seed);
|
||||
LevelStem stem = new LevelStem(dimensionType, generator);
|
||||
|
||||
WorldData worldData = server.getWorldData();
|
||||
ServerLevelData overworldData = worldData.overworldData();
|
||||
DerivedLevelData derivedLevelData = new DerivedLevelData(worldData, overworldData);
|
||||
|
||||
Executor executor = serverAccess.levelExecutor(server);
|
||||
LevelStorageSource.LevelStorageAccess storage = serverAccess.levelStorage(server);
|
||||
long obfuscatedSeed = BiomeManager.obfuscateSeed(seed);
|
||||
|
||||
ServerLevel level = new ServerLevel(
|
||||
server,
|
||||
executor,
|
||||
storage,
|
||||
derivedLevelData,
|
||||
key,
|
||||
stem,
|
||||
false,
|
||||
obfuscatedSeed,
|
||||
List.of(),
|
||||
false);
|
||||
|
||||
serverAccess.putLevel(server, key, level);
|
||||
server.getPlayerList().addWorldborderListener(level);
|
||||
return new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
|
||||
}
|
||||
|
||||
public static int evacuate(MinecraftServer server, ServerLevel from) {
|
||||
ServerLevel fallback = server.overworld();
|
||||
if (fallback == from) {
|
||||
return 0;
|
||||
}
|
||||
BlockPos spawn = fallback.getRespawnData().pos();
|
||||
int spawnY = fallback.getHeight(Heightmap.Types.MOTION_BLOCKING, spawn.getX(), spawn.getZ());
|
||||
List<ServerPlayer> players = new ArrayList<>(from.players());
|
||||
for (ServerPlayer player : players) {
|
||||
player.teleportTo(fallback, spawn.getX() + 0.5D, spawnY, spawn.getZ() + 0.5D, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
|
||||
}
|
||||
return players.size();
|
||||
}
|
||||
|
||||
private static ResourceKey<Level> levelKey(String dimensionId) {
|
||||
Identifier identifier = Identifier.parse(dimensionId);
|
||||
return ResourceKey.create(Registries.DIMENSION, identifier);
|
||||
}
|
||||
|
||||
private static ModdedServerAccess requireAccess() {
|
||||
ModdedServerAccess bound = access;
|
||||
if (bound == null) {
|
||||
throw new IllegalStateException("Iris modded server access is not bound; the loader bootstrap must bind ModdedServerAccess before runtime dimension injection");
|
||||
}
|
||||
return bound;
|
||||
}
|
||||
|
||||
public record Handle(String dimensionId, String pack, String packDimensionKey, long seed, ServerLevel level, IrisModdedChunkGenerator generator) {
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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.modded;
|
||||
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class ModdedDimensionRegistryStore {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final String FILE_NAME = "iris-dimensions.json";
|
||||
|
||||
private ModdedDimensionRegistryStore() {
|
||||
}
|
||||
|
||||
public static List<PersistentDimension> load(MinecraftServer server) {
|
||||
Path file = storeFile(server);
|
||||
if (!Files.isRegularFile(file)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
JSONObject root = new JSONObject(Files.readString(file, StandardCharsets.UTF_8));
|
||||
JSONArray entries = root.optJSONArray("dimensions");
|
||||
if (entries == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
Map<String, PersistentDimension> deduplicated = new LinkedHashMap<>();
|
||||
for (int index = 0; index < entries.length(); index++) {
|
||||
JSONObject entry = entries.getJSONObject(index);
|
||||
String id = entry.optString("id", null);
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
String pack = entry.optString("pack", null);
|
||||
String dimension = entry.optString("dimension", null);
|
||||
if (pack == null || dimension == null) {
|
||||
LOGGER.error("Iris registry entry '{}' in {} has no pack/dimension fields; skipping it. Re-create the world with /iris world enable", id, file);
|
||||
continue;
|
||||
}
|
||||
if (!entry.has("seed")) {
|
||||
LOGGER.warn("Iris registry entry '{}' in {} has no seed; skipping it. Re-create the world with /iris world enable", id, file);
|
||||
continue;
|
||||
}
|
||||
deduplicated.put(id, new PersistentDimension(id, pack, dimension, entry.getLong("seed")));
|
||||
}
|
||||
return new ArrayList<>(deduplicated.values());
|
||||
} catch (RuntimeException | IOException e) {
|
||||
LOGGER.error("Iris persistent dimension registry at {} is invalid; ignoring it", file, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void put(MinecraftServer server, PersistentDimension dimension) {
|
||||
Map<String, PersistentDimension> current = index(load(server));
|
||||
current.put(dimension.id(), dimension);
|
||||
write(server, new ArrayList<>(current.values()));
|
||||
}
|
||||
|
||||
public static synchronized void remove(MinecraftServer server, String id) {
|
||||
Map<String, PersistentDimension> current = index(load(server));
|
||||
if (current.remove(id) != null) {
|
||||
write(server, new ArrayList<>(current.values()));
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, PersistentDimension> index(List<PersistentDimension> dimensions) {
|
||||
Map<String, PersistentDimension> map = new LinkedHashMap<>();
|
||||
for (PersistentDimension dimension : dimensions) {
|
||||
map.put(dimension.id(), dimension);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void write(MinecraftServer server, List<PersistentDimension> dimensions) {
|
||||
Path file = storeFile(server);
|
||||
JSONArray entries = new JSONArray();
|
||||
for (PersistentDimension dimension : dimensions) {
|
||||
JSONObject entry = new JSONObject();
|
||||
entry.put("id", dimension.id());
|
||||
entry.put("pack", dimension.pack());
|
||||
entry.put("dimension", dimension.dimension());
|
||||
entry.put("seed", dimension.seed());
|
||||
entries.put(entry);
|
||||
}
|
||||
JSONObject root = new JSONObject();
|
||||
root.put("dimensions", entries);
|
||||
try {
|
||||
Files.createDirectories(file.getParent());
|
||||
Path temp = file.resolveSibling(FILE_NAME + ".tmp");
|
||||
Files.writeString(temp, root.toString(2), StandardCharsets.UTF_8);
|
||||
Files.move(temp, file, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Iris failed to write persistent dimension registry at {}", file, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Path storeFile(MinecraftServer server) {
|
||||
return server.getWorldPath(LevelResource.ROOT).resolve("iris").resolve(FILE_NAME);
|
||||
}
|
||||
|
||||
public record PersistentDimension(String id, String pack, String dimension, long seed) {
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.modded;
|
||||
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class ModdedDimensionStorage {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final List<String> CHUNK_DATA_FOLDERS = List.of("region", "entities", "poi", "mantle");
|
||||
|
||||
private ModdedDimensionStorage() {
|
||||
}
|
||||
|
||||
public static File storageFolder(MinecraftServer server, ResourceKey<Level> dimension) {
|
||||
return DimensionType.getStorageFolder(dimension, server.getWorldPath(LevelResource.ROOT)).toFile();
|
||||
}
|
||||
|
||||
public static void wipe(MinecraftServer server, ResourceKey<Level> dimension) {
|
||||
File storageFolder = storageFolder(server, dimension);
|
||||
for (String folder : CHUNK_DATA_FOLDERS) {
|
||||
deleteRecursively(new File(storageFolder, folder).toPath());
|
||||
}
|
||||
LOGGER.info("Iris wiped dimension storage at {}", storageFolder.getAbsolutePath());
|
||||
}
|
||||
|
||||
private static void deleteRecursively(Path root) {
|
||||
if (!Files.exists(root)) {
|
||||
return;
|
||||
}
|
||||
try (Stream<Path> walk = Files.walk(root)) {
|
||||
for (Path path : walk.sorted(Comparator.reverseOrder()).toList()) {
|
||||
Files.deleteIfExists(path);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Iris failed to wipe dimension storage at {}", root, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user