mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-07-13 02:15:46 +00:00
Compare commits
3503 Commits
1.3.3
...
unification
| 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 | |||
| 141ca76647 | |||
| 94557830f5 | |||
| 49acb7faba | |||
| 1904f67662 | |||
| 5b2bf38344 | |||
| 494c38a153 | |||
| dd4b85cbfe | |||
| 8f7b54a5a4 | |||
| cec502340e | |||
| 64e27c7fb0 | |||
| b2bbd31548 | |||
| 17df92a07c | |||
| 2b749b5ab7 | |||
| 28e3402d88 | |||
| d96bb061e0 | |||
| e6def804f8 | |||
| a5be48c07c | |||
| fe3909f594 | |||
| f37d91a530 | |||
| 614ef78771 | |||
| fb294fc03c | |||
| 8c7e7c3d48 | |||
| 98b6280652 | |||
| 7c4c2d6382 | |||
| 1a29ea302e | |||
| be39fce741 | |||
| 4fed2e6e5f | |||
| 14ae5809b0 | |||
| 0f66aa47c9 | |||
| cf85d300f4 | |||
| 91d60e56df | |||
| 1d3681beb4 | |||
| 0b403cf329 | |||
| 8ba75899e3 | |||
| 4d4ed72159 | |||
| b72bf072b3 | |||
| 4edcb54b31 | |||
| eae7fc0e53 | |||
| fddacd4410 | |||
| de6c4b9a30 | |||
| 6990e34138 | |||
| d9883697ef | |||
| 05a26ff8b2 | |||
| 84bb082d44 | |||
| a0db94d84f | |||
| 0367b4ecd7 | |||
| dbc425dce6 | |||
| 116c017c6d | |||
| 9c387475f0 | |||
| 0765c21cef | |||
| 2871038584 | |||
| 340885f939 | |||
| 1e32e47f54 | |||
| 3c759f3b01 | |||
| 6615f34d20 | |||
| 3a4aac1ee4 | |||
| 4d6c092615 | |||
| 93421a1dc9 | |||
| 209458a856 | |||
| 82bd94620e | |||
| af9f017871 | |||
| 2f8922ea87 | |||
| 0ea936f3fe | |||
| d1870ee0a8 | |||
| d2151690ad | |||
| dff373e7d5 | |||
| 0aec2a664f | |||
| 723b696393 | |||
| f5f89eb4e4 | |||
| df15332c7f | |||
| 61d9e5a869 | |||
| 9628b66a97 | |||
| 5e041366d4 | |||
| 3ecae8d72c | |||
| 5709ce9d82 | |||
| ce3c334ac5 | |||
| 2ed5aa8730 | |||
| 8480a63dda | |||
| 132a1695f3 | |||
| 21775630c3 | |||
| eac2ef7c6d | |||
| 386997f646 | |||
| 637b90ad62 | |||
| 95d755c2ec | |||
| 781aeebef6 | |||
| 5e133fd51a | |||
| 85f62a3b9d | |||
| a32de58c8b | |||
| 1cb2ea6c17 | |||
| 6a37ab7af2 | |||
| e0f0aaf767 | |||
| 3d7c5b050a | |||
| 6bc57c255f | |||
| b82edfe688 | |||
| 6b32eb3441 | |||
| 23a07fa8a5 | |||
| b019faedd2 | |||
| 4430433a10 | |||
| f52cd29e7b | |||
| 6a44e593a6 | |||
| f7065fe034 | |||
| 0a247956f7 | |||
| 03836acded | |||
| b5fb277982 | |||
| e523d3c166 | |||
| 41477e4aa6 | |||
| fbaf42a8c4 | |||
| 1c3668047b | |||
| 7d78c69b6e | |||
| 43bd96a679 | |||
| a87c83dae2 | |||
| 490d2b69e3 | |||
| 7eac936431 | |||
| 588b261d52 | |||
| daf2cf02b3 | |||
| b9d97f2434 | |||
| 1467dc963d | |||
| 89bf687fc4 | |||
| 2e7c80562b | |||
| 5d54b82be0 | |||
| 2ae1df4d42 | |||
| 26782e6f0c | |||
| cc84d5b562 | |||
| 8bb1efdca3 | |||
| 7a5794c8e2 | |||
| 9dd20661f5 | |||
| ad0ab66711 | |||
| 82396b5145 | |||
| 246e974dc1 | |||
| 203b6a4b4f | |||
| f204f3a209 | |||
| ba8e060370 | |||
| c51172acbc | |||
| 39100a5011 | |||
| fcee61a703 | |||
| ce8cc2cf37 | |||
| 3b5bdcfb51 | |||
| 4d67e97973 | |||
| da09f4d6d8 | |||
| dbc7e2e0bf | |||
| dd804b6665 | |||
| 33eb878834 | |||
| 2a2c456011 | |||
| 05a12f3899 | |||
| eb04710f71 | |||
| 257760453e | |||
| d11baeb08a | |||
| 47d322c993 | |||
| d316207a61 | |||
| e6daabbb47 | |||
| 53156449f1 | |||
| e3d4c32d03 | |||
| 9cd5c39bbe | |||
| a1fbf25465 | |||
| ec74037f05 | |||
| fff650b766 | |||
| bae2a18e24 | |||
| fd8a7ebb4c | |||
| 4a3a1491d4 | |||
| 9aa2c5ed8a | |||
| d5699a7472 | |||
| df98f8f626 | |||
| d37b298f07 | |||
| 62f7fc17d1 | |||
| 6e4929246d | |||
| 909461975a | |||
| dec191e5e7 | |||
| 07b0cd6f5a | |||
| b7a892ecfe | |||
| 79d16102fa | |||
| 67efdb47aa | |||
| 751c233b49 | |||
| f51734fea0 | |||
| 516a7111a7 | |||
| 1e3ff59b26 | |||
| 4c48f812d5 | |||
| a63a6f751f | |||
| 923860a90b | |||
| 5a4ca2f907 | |||
| ccc2ed0806 | |||
| 44c707e059 | |||
| e7c458de3f | |||
| 11b05b10a4 | |||
| d133ac0088 | |||
| 8951fcdebd | |||
| ecbd5677ce | |||
| e31b9c1a28 | |||
| faa7b7c742 | |||
| d08f326477 | |||
| 6500ed2075 | |||
| 56eb775a0f | |||
| e225bcfb96 | |||
| 4be6abe416 | |||
| c3de8b33de | |||
| 5658d6709c | |||
| 192538a741 | |||
| b8b9d7bf8c | |||
| 1db8603910 | |||
| 1c5774fca5 | |||
| 0b332b06b6 | |||
| 0be9b049a3 | |||
| 6993639cc0 | |||
| 74172e3123 | |||
| 6e1c4f682e | |||
| af0d282a1f | |||
| 09bf15fa05 | |||
| 43f7d95b4c | |||
| 8b536b1775 | |||
| 60a7f4bc35 | |||
| 0ed0e0c983 | |||
| f4b1ca9f21 | |||
| 609104cfa8 | |||
| 935d11b433 | |||
| e9eeb8335d | |||
| 4a2f42437f | |||
| 51b0693c99 | |||
| 185b366d8d | |||
| a40e533068 | |||
| 5b3918fcb1 | |||
| d315e99b63 | |||
| 8090ba0259 | |||
| 584400d011 | |||
| 124ad23a95 | |||
| 6b59aa38ae | |||
| 03554eb372 | |||
| dabfe41f29 | |||
| 6e2c6fc51e | |||
| d81986bcc5 | |||
| ea17e916b8 | |||
| ce56224038 | |||
| 9978932391 | |||
| 2ebc9e0deb | |||
| 7ddc621009 | |||
| aa4e574eef | |||
| 15bc964fcc | |||
| 99713ac964 | |||
| f9a6bd1343 | |||
| f4bee872e1 | |||
| be99b286b3 | |||
| c7d5212f82 | |||
| 75726b3d34 | |||
| 8d7e76ed81 | |||
| 01b08b2297 | |||
| cf51581ac1 | |||
| d99fd56995 | |||
| d149051c27 | |||
| 660aaa87ca | |||
| 4010f03b05 | |||
| e2d2dcf337 | |||
| b11aaf3f0c | |||
| 30ae065cb4 | |||
| 7f5b0559aa | |||
| 851954efb2 | |||
| b8c6a459a6 | |||
| 4523089440 | |||
| 217fe47878 | |||
| 525e3db24a | |||
| c3cac7d8c7 | |||
| a92717bb1d | |||
| 9dc32eada2 | |||
| d1644c0454 | |||
| 81ce84b3f1 | |||
| 18a8a405e0 | |||
| 07840b3b55 | |||
| f639c5b14e | |||
| a6851554e8 | |||
| 6859b83fe0 | |||
| 699da3cd26 | |||
| 6d429ef57b | |||
| 0bc7c20106 | |||
| d8f83d09ee | |||
| 43874d35ec | |||
| 5a186e4b02 | |||
| 70b3ac21ef | |||
| 7f24782aee | |||
| 009835c17f | |||
| 18db39d480 | |||
| 5956c358d0 | |||
| f18232eb6c | |||
| ac69d57de0 | |||
| 9297d4a7d2 | |||
| e6fd4e2255 | |||
| c0fa6db602 | |||
| d61d732a75 | |||
| e9c0ac3c6b | |||
| e071ecc1d7 | |||
| 14f6ef3da0 | |||
| edac6d1f22 | |||
| 525dfab3e3 | |||
| 0b77986034 | |||
| f0ab72db92 | |||
| 6243555db1 | |||
| abefb652c1 | |||
| 483e3ff800 | |||
| 0642e5a67e | |||
| 9553aa6e15 | |||
| 090fb1a33e | |||
| c4896a1c07 | |||
| 5f165d0a89 | |||
| df2c1ca6f2 | |||
| 5e41b6c471 | |||
| 1d3fff08ed | |||
| 73ffcbcb41 | |||
| 510535fdd4 | |||
| 4ade79a307 | |||
| 954abb974e | |||
| 8f3093ea2b | |||
| 2ead5b3c40 | |||
| 47cab96d10 | |||
| 7cfcebf0f5 | |||
| 12e310f1fe | |||
| f28b855189 | |||
| 43ed3aba41 | |||
| 1da4ee04d3 | |||
| 17b5e4015d | |||
| 1a8b8b7232 | |||
| fd2e8cfa8a | |||
| ccd05c04a5 | |||
| e70edffaa3 | |||
| 3ecaaf33b6 | |||
| c424ea52fa | |||
| ebb1aaeefd | |||
| f2d1f7bf52 | |||
| deb42da38e | |||
| 43df2a754d | |||
| d468d0ca28 | |||
| e586d0aecf | |||
| 8e36fed7bf | |||
| 2e3bf585ea | |||
| f57ad5a888 | |||
| 47fa31515d | |||
| 7e45c17078 | |||
| 32a0fc9b17 | |||
| 62c2757afc | |||
| 7a7596fb7b | |||
| 708b0d6365 | |||
| daf1a1baa2 | |||
| 59150a655a | |||
| 5594ad1ce3 | |||
| f12bd1368f | |||
| 6921ad49db | |||
| 4430fd21f5 | |||
| 64d8f22757 | |||
| 2a73524a19 | |||
| bfd7a02c9d | |||
| b108a0164a | |||
| 9f1aa916e4 | |||
| 5144a2a2df | |||
| c9c2deae1a | |||
| b220db9d85 | |||
| f9e3d96673 | |||
| 6c2b65055b | |||
| 5fa1efb1f2 | |||
| 6b5d4fc557 | |||
| 369b125c52 | |||
| 732e049252 | |||
| cdb419c3f1 | |||
| 2dde426df6 | |||
| 8c00499e76 | |||
| 32dac5fbe2 | |||
| 0d3f8de811 | |||
| 90b8747775 | |||
| 5333d23ad8 | |||
| e38e11b890 | |||
| 79303c90d7 | |||
| 4eac0978ed | |||
| aef5b9a057 | |||
| c65b951493 | |||
| 75385e5c01 | |||
| ad19baf1f6 | |||
| fb7acbac0a | |||
| 7cd752ceb7 | |||
| 9df288e7e3 | |||
| 26396bbe1a | |||
| a21850e85f | |||
| b19358914a | |||
| 5a296f2d06 | |||
| 755c84039a | |||
| 136ea44509 | |||
| ba758470fc | |||
| 878c39413a | |||
| a63a3a8fa5 | |||
| 6a2e1f0271 | |||
| 21f1ecfa15 | |||
| 9c5966c49c | |||
| ffc1d01a0a | |||
| e66e6e0b51 | |||
| b40a86e114 | |||
| ef23d6d51a | |||
| 4fea28b313 | |||
| 5502fa4584 | |||
| 0e2ae6840b | |||
| 3a4493c914 | |||
| 66deda7c76 | |||
| 44beeec1d7 | |||
| 824f74a4ce | |||
| 8d72d2b3a4 | |||
| 876f1231ee | |||
| ffbf395074 | |||
| 550b513f6b | |||
| 3f4ee39ca9 | |||
| a4887ec3c3 | |||
| f1185d8f34 | |||
| 030b2998eb | |||
| 3f8ae692ff | |||
| 07df92d75c | |||
| 8bbf98979e | |||
| 24408e53a3 | |||
| f8160e254b | |||
| ff9b585b29 | |||
| 095f413147 | |||
| 3dd1162a59 | |||
| 436225e341 | |||
| ca89073537 | |||
| 7663145821 | |||
| eed75eea58 | |||
| 194955770e | |||
| 3c3820cb38 | |||
| fc25a328bf | |||
| c7e857506d | |||
| d343cd4f08 | |||
| b4aa1b74ab | |||
| b043c5eade | |||
| c1a6ca80f5 | |||
| a010ef0f60 | |||
| 1240632dae | |||
| 87df6e08ca | |||
| d7ad947cbb | |||
| 434e7f75fa | |||
| 6f6e74e015 | |||
| 24f3176b1b | |||
| 2d0df240f4 | |||
| def658ccc6 | |||
| 676b35fb1c | |||
| 18a1a693b5 | |||
| 617a797743 | |||
| e1711b5d03 | |||
| 28ef4a0b2b | |||
| 8ee17150b8 | |||
| b52017625d | |||
| 60ac9dfca2 | |||
| c671735e84 | |||
| 29e5247131 | |||
| f5c533094e | |||
| 06a8e08244 | |||
| 30eda70dda | |||
| 38ec4cc3ce | |||
| 435271becf | |||
| 991bb12c80 | |||
| b8b643ebdf | |||
| 42ad8ba1ee | |||
| 9f425cce4c | |||
| 8be08acfa2 | |||
| 6422772530 | |||
| a008a425ec | |||
| 0936015d84 | |||
| 488a59df2e | |||
| 5bf1533067 | |||
| 77024cd651 | |||
| d0e209b2eb | |||
| d934fc181c | |||
| 2ca6235b5d | |||
| a271ed000e | |||
| 4d64ad8bb8 | |||
| 66a717b927 | |||
| 79114a90f5 | |||
| 4427748b8b | |||
| ae9b8326a4 | |||
| 619c240e62 | |||
| 19889e7d6f | |||
| 107d1fc8ff | |||
| 636fddd642 | |||
| 13532a1f0a | |||
| e1ace2ba08 | |||
| 96f0c1d148 | |||
| 03ecf8fe82 | |||
| 1c03a7fc5c | |||
| 85fe65612e | |||
| 88469f6141 | |||
| d81ba4a8c8 | |||
| 734c0a37f7 | |||
| 7becca450a | |||
| f2a5489363 | |||
| c7a8cb566b | |||
| a80031c3c5 | |||
| b6d9eb3dcc | |||
| 4a594a8e4f | |||
| 384a28c517 | |||
| 57ef3842e9 | |||
| 5b509466e6 | |||
| 7019947083 | |||
| 5ddc9243dd | |||
| 1dd89597fd | |||
| 2ac1de4ae9 | |||
| 1f783ddf44 | |||
| bee920f96a | |||
| 333e158ca5 | |||
| 67baf07efc | |||
| 4ba8ecd3fd | |||
| aee42cdecc | |||
| adb2a32fda | |||
| d2c373b27d | |||
| b0060bcb82 | |||
| 4f63db3f70 | |||
| 0d356514f2 | |||
| 6ce37944a7 | |||
| 8ef75f7c34 | |||
| bf2da499d6 | |||
| ef2fe6a775 | |||
| 2ac84f14be | |||
| 5078becbb3 | |||
| e19784a429 | |||
| 1628652264 | |||
| 668c7d301b | |||
| d56a63e712 | |||
| 8070b211ab | |||
| 5ed59d0282 | |||
| 70223cfcd6 | |||
| 7d0ff90b01 | |||
| ecc09a710a | |||
| 846b4faefa | |||
| 66da382789 | |||
| 3378723bdd | |||
| 56723330b3 | |||
| 08e2244975 | |||
| f9e934fa1a | |||
| 30eacc5019 | |||
| 3ba7c18288 | |||
| 958a09b280 | |||
| cc66e7e7c8 | |||
| 7f58e0413c | |||
| 3c256ddc5d | |||
| d2f8e62ecc | |||
| 5ce479b1af | |||
| be2b893a3f | |||
| 32994ce4af | |||
| 0c6a43fe0a | |||
| 81e0376102 | |||
| f21306a19d | |||
| 5f3dcac8e1 | |||
| 600bf94044 | |||
| 3b38b69034 | |||
| faa8f0f9b6 | |||
| 125624b09e | |||
| 7186afda93 | |||
| ee542abb5d | |||
| c7638a6485 | |||
| 16795871c3 | |||
| 54eff0e27f | |||
| 0a85521028 | |||
| 587af255a6 | |||
| 06326f4d55 | |||
| b73f9798e4 | |||
| 7ef5031717 | |||
| ba30258ee6 | |||
| 940a65cac0 | |||
| 7878b05030 | |||
| 16eb63d2ea | |||
| 8539bc5ade | |||
| 02f357a88d | |||
| 92f72b45d5 | |||
| 7755b19abc | |||
| 6b62ba1c5b | |||
| 2514aa2e3c | |||
| 82982c54f9 | |||
| 726718a6f5 | |||
| 1609e232a2 | |||
| fda2e265af | |||
| 5c19ae0e94 | |||
| f01b3caeb1 | |||
| 9e7262660f | |||
| 4489197d01 | |||
| 3d411fc6f3 | |||
| d4f26577a5 | |||
| c744f761a1 | |||
| 7224ca43f3 | |||
| b95cea35a2 | |||
| d891791929 | |||
| 826b0ad4ce | |||
| c99c61cc7a | |||
| 5eb57de264 | |||
| 976cfdbbc2 | |||
| a7aea8d1ed | |||
| 9caa89b733 | |||
| e42515c1ea | |||
| 901169775d | |||
| 3d86540d1d | |||
| 6cb97d067e | |||
| ad323ebf2b | |||
| 18efedf7c5 | |||
| 4eaf4ff26b | |||
| b1d2ac7822 | |||
| 87e6e583a6 | |||
| 2897e1a3ad | |||
| dfd5a7c21d | |||
| 093f6411c7 | |||
| 845bea522b | |||
| d0175f9c39 | |||
| 0b1d59e398 | |||
| 93bcb8994f | |||
| ecee464b34 | |||
| 42789fcc24 | |||
| f26b36f3e8 | |||
| 9f09052900 | |||
| 95a7ba0785 | |||
| ef4a3b405d | |||
| 7096fcd43e | |||
| eeab12ed86 | |||
| ca961e8498 | |||
| 40b0a37110 | |||
| 6daf0861c9 | |||
| 03bc1e722e | |||
| ce94c5e237 | |||
| 88aad1e107 | |||
| c4fe14bd45 | |||
| 2191b539e3 | |||
| dcd3495564 | |||
| 30559841d5 | |||
| 0dc0d5c981 | |||
| 3b981e2818 | |||
| 47bfc74ba9 | |||
| 8858e1bce8 | |||
| 04be430b8c | |||
| d46351b866 | |||
| d3ec29aa42 | |||
| 357abee462 | |||
| c86a040768 | |||
| d9e7dc158d | |||
| 46591968c0 | |||
| bb7c256ecf | |||
| d239ed1c9f | |||
| 604dece366 | |||
| 8b55f20684 | |||
| 8bf12ff32c | |||
| 0596482a8d | |||
| b5a77aa62a | |||
| 5db27d9d93 | |||
| a630e4937b | |||
| 96d27d17ce | |||
| 742b848f42 | |||
| 57884e3400 | |||
| 40459f4e01 | |||
| a461072113 | |||
| 0105931414 | |||
| c0b7b871ba | |||
| 1250c94600 | |||
| ccbd5b17d3 | |||
| 2fd3abbb14 | |||
| 5bc019d3d3 | |||
| 91c24d5501 | |||
| cd7f192fba | |||
| 7d0555fb67 | |||
| 3864708cf1 | |||
| ef22a58631 | |||
| acf4fa106f | |||
| 5a5704ee0b | |||
| 7e65416f10 | |||
| 2d7bc59db4 | |||
| 930469a006 | |||
| c657ba52ad | |||
| 46499512eb | |||
| c2779570fa | |||
| 750dc609bb | |||
| 1037183f4c | |||
| 979f959291 | |||
| 86cfe0465c | |||
| 61d461eea5 | |||
| ff1733225a | |||
| 784fca98c8 | |||
| 1f0ebd8ba1 | |||
| 2e645f04e7 | |||
| debe8eb81c | |||
| 2e8f8f258d | |||
| 697f40595e | |||
| 37404b6619 | |||
| 4b3a016d70 | |||
| ed2af5f9b4 | |||
| 5418ab52b7 | |||
| 61037af138 | |||
| f86cd11d74 | |||
| e46b335dc0 | |||
| 79299112eb | |||
| 3d5bee3a2c | |||
| 466cc84c16 | |||
| 27f9e2ea9f | |||
| 593e10eec2 | |||
| 5d5b8b78d4 | |||
| 620c4087bc | |||
| c5bf0e0cb7 | |||
| 7f7d53e00a | |||
| 073466bd6a | |||
| 151e88046d | |||
| c3b1d6735e | |||
| e3a5da64a5 | |||
| 3c4599423d | |||
| 2bb1f27e07 | |||
| e3adbbbd0b | |||
| 5e6838bdc9 | |||
| fc6720e090 | |||
| e29f671363 | |||
| 6e501c7b8a | |||
| 7dd3cc6f9f | |||
| f346f080ee | |||
| 9f19e585db | |||
| bc74487301 | |||
| 693baa8cef | |||
| 9ae60d4859 | |||
| 281efef207 | |||
| d893537ae6 | |||
| 986de265fe | |||
| d25633e213 | |||
| 0c8c7157f6 | |||
| e393442df9 | |||
| fc94fe30d0 | |||
| 1065079be6 | |||
| e426172b18 | |||
| effe93286a | |||
| 2a669618a2 | |||
| 44cd0e1e10 | |||
| 621155f06f | |||
| 6c116e261d | |||
| bcc89d91f7 | |||
| 8e874afbd5 | |||
| 9421cd0b2d | |||
| 37c46042c3 | |||
| c5429f0515 | |||
| f0375ca446 | |||
| b34bac9b5e | |||
| f42e418346 | |||
| a060c01f58 | |||
| f41686fab4 | |||
| be88d584cf | |||
| 80aa87935d | |||
| 405073cca9 | |||
| 22cb831138 | |||
| 95c8c75789 | |||
| e8e3cadeff | |||
| 8123ba9a01 | |||
| 5aa6c98dc1 | |||
| 8e16b8e4a8 | |||
| 90f0a5eac6 | |||
| 892d3be144 | |||
| 164242ae4b | |||
| 07b8c5087b | |||
| abba2e969b | |||
| 0a4b57ea01 | |||
| 422ecb758f | |||
| 20f953b9cb | |||
| d2f1488ef1 | |||
| a3aedd5fad | |||
| e2d07f7e78 | |||
| 3aa0fac852 | |||
| 238243a7d4 | |||
| 9533b62ebf | |||
| d06445e04b | |||
| 69255a938b | |||
| 773f24238f | |||
| 548ed4a1f0 | |||
| 76bf764262 | |||
| 075882b7b0 | |||
| d25299e1ba | |||
| 0160d42302 | |||
| 6ca7a013d6 | |||
| 60ed93e13b | |||
| 747e2e2e27 | |||
| 16d1694a78 | |||
| 17e74d9ed2 | |||
| 923a42e9a3 | |||
| bac66dfbf9 | |||
| 6895c0b392 | |||
| ef08319670 | |||
| 1aee758f87 | |||
| 3d0eca9432 | |||
| 93936a7498 | |||
| 8e873ccd00 | |||
| 7a188af002 | |||
| aaffeb9fac | |||
| 67711bcb0d | |||
| f93c158caa | |||
| c74d05a47b | |||
| f867c0f8a1 | |||
| 98d024295e | |||
| 963d3de709 | |||
| e20fa7f3a2 | |||
| 719d262a00 | |||
| 9ad9f28f33 | |||
| 6ac4a405b5 | |||
| 64fbdff13d | |||
| 65011f42da | |||
| d763967d4b | |||
| 44c807aedd | |||
| 6e6bb72245 | |||
| ca96b45637 | |||
| d8ff7786df | |||
| 35ed07e0ba | |||
| 7b002606bf | |||
| ac0d0a6680 | |||
| e5033cceec | |||
| b5d7e2969f | |||
| 6b4bd90bfa | |||
| b2aff55160 | |||
| 569c34bca0 | |||
| e63c966756 | |||
| 43f9efb9e4 | |||
| 82666d62ef | |||
| 72b62481a7 | |||
| 9cf13e320a | |||
| c8c4a0f61b | |||
| 801c44389e | |||
| fbdb060e10 | |||
| 09edc78bdc | |||
| 6951817088 | |||
| 2a22d25975 | |||
| 875adc8bdf | |||
| b8b914a99c | |||
| 59401d27ca | |||
| 26cb21f541 | |||
| 49154759c0 | |||
| abc07968e5 | |||
| d28899ae84 | |||
| 04c44de145 | |||
| 7997d52df5 | |||
| f3d899cbf5 | |||
| e540596c20 | |||
| b1aa7a8e88 | |||
| 3eef5b2c02 | |||
| 443aa0040c | |||
| 48c1db6a8a | |||
| b9f2530e2f | |||
| 3cb7c0206a | |||
| a7360f1145 | |||
| c4e3c4b34f | |||
| ae763ae25d | |||
| dd0b4f27d4 | |||
| faf5bcb32b | |||
| 93f2c83c0e | |||
| 63b5545502 | |||
| 31c99517d7 | |||
| 2e3a2ff69a | |||
| 4aa2c83e03 | |||
| 376d7a8878 | |||
| dd5f55f4fc | |||
| d2806c7ec0 | |||
| 2db34f4d49 | |||
| a451189d83 | |||
| f3a2be4a06 | |||
| dddd97bc22 | |||
| 8e53be6810 | |||
| d24b9ec0ae | |||
| d8900341ac | |||
| 0465d047e7 | |||
| cf1f3677ae | |||
| 354b02612d | |||
| f06d5b806a | |||
| 49385a08ad | |||
| d07c8c8851 | |||
| 803c3678e3 | |||
| 3118f743a0 | |||
| 2fa5c7eca4 | |||
| a09829ed4b | |||
| cf3d92d6e1 | |||
| 524b63e88c | |||
| 0a930db7d6 | |||
| e6338bbde1 | |||
| e4f7dd21b6 | |||
| b0bae93d06 | |||
| 9b93a5cd04 | |||
| 3142717154 | |||
| 3dcfacfeed | |||
| a51f5fefc4 | |||
| fcc3bbf9b6 | |||
| 109390d21e | |||
| c7dd91ef90 | |||
| 3d15abfdae | |||
| a9caaefd16 | |||
| 261c786a10 | |||
| a5270ccea8 | |||
| 08865dc218 | |||
| 3f50767e48 | |||
| 8ff5887955 | |||
| 2aa240337c | |||
| 44d75d9955 | |||
| 3b61b87501 | |||
| 04de362bc9 | |||
| e06175f600 | |||
| fe78d4f1e7 | |||
| 4a1de4c0da | |||
| 22b77411ec | |||
| 3f730ead84 | |||
| d52d70a014 | |||
| 0a7b485514 | |||
| 151c384313 | |||
| 8586d44d7e | |||
| 5c8b600cb1 | |||
| afd67d2f00 | |||
| 125a3fdc5e | |||
| 9963ff9c8e | |||
| 8547ea4c6a | |||
| fd434e8b21 | |||
| 1524866432 | |||
| 19fa9390ff | |||
| a335050332 | |||
| 8aa9ecffc5 | |||
| ef02e5169d | |||
| ba31a56949 | |||
| 6b7bd75b18 | |||
| 013d6625d5 | |||
| ca228ca23e | |||
| 1dce68212f | |||
| 2683c2433b | |||
| 2929800a9f | |||
| c1b04ace78 | |||
| 04a54dbc2a | |||
| e7c1f31315 | |||
| 62f6917d81 | |||
| babefc7bd3 | |||
| a09c9f4a2e | |||
| 5876598ca0 | |||
| f45d643739 | |||
| c1ba176e27 | |||
| 9b3013c51a | |||
| a746720a6e | |||
| a95c61c1d8 | |||
| d5da8e4e2b | |||
| 861e11a713 | |||
| 23ac209c05 | |||
| 8df789ae34 | |||
| 432e95e4ec | |||
| 401ed0a7a5 | |||
| dae6957093 | |||
| 7b6405fba7 | |||
| 6dc3e74607 | |||
| 617066340b | |||
| 2dd6db352f | |||
| 56e13641df | |||
| 9d62113388 | |||
| 47b4a6c498 | |||
| 7dfe3a1c59 | |||
| 44acfc7ec8 | |||
| 7933f5e357 | |||
| 858c5a27ad | |||
| 28e41a0ae9 | |||
| ae79f9be66 | |||
| 4a6d58c36e | |||
| 5bf6687f1f | |||
| beb80f0422 | |||
| 0e86d6fbdb | |||
| 2a8e4f19af | |||
| 6ed8b6d058 | |||
| 33812b3f4a | |||
| b62e542801 | |||
| 0080110fad | |||
| 19ef661457 | |||
| 7946221f0b | |||
| 203a42c043 | |||
| d3998dd03b | |||
| e752a48343 | |||
| 71955b777b | |||
| d686d07d53 | |||
| 72b4c9c6ab | |||
| 4b991ab67a | |||
| 59399124fa | |||
| 3eb4962d7d | |||
| 42d3b4c326 | |||
| f3931567f9 | |||
| 154adafbcb | |||
| 4a1e511262 | |||
| 3fc907e50f | |||
| bbf441c3b4 | |||
| bbf660d80e | |||
| a1c3bc26c9 | |||
| fc73e0e482 | |||
| e7727afbe8 | |||
| de5ce2660d | |||
| 2e661cb2f8 | |||
| ebeaad0b4e | |||
| ce60c048f8 | |||
| 6795ff7db9 | |||
| 931f1fb27d | |||
| 74625a8d65 | |||
| 03e8ccee7d | |||
| 01b9e4cb55 | |||
| b5d5014ba2 | |||
| 4f7bfd14c0 | |||
| 7347300f05 | |||
| aa6bdd2786 | |||
| db0aeeabbd | |||
| 9aa426ed56 | |||
| 30f9348c26 | |||
| baf76c51f9 | |||
| 6abef569e8 | |||
| ade88a6055 | |||
| 11d0274a1b | |||
| 957de5d6f7 | |||
| f5f4c5c672 | |||
| 5924495035 | |||
| c486f65e74 | |||
| b19a7d45d8 | |||
| 7f7397f684 | |||
| 39142909f6 | |||
| 23f5b84c5e | |||
| 630b6e6313 | |||
| 4396ca9420 | |||
| 5286a69c34 | |||
| 059d5542c9 | |||
| a09e77a17d | |||
| 5a236e3bea | |||
| aba5add7f9 | |||
| a4190672d3 | |||
| 10da9182d1 | |||
| 2de5bc2855 | |||
| b32db945be | |||
| 44d02e6dfd | |||
| fc096dc6fa | |||
| 1d52bbde58 | |||
| 1a049e854b | |||
| 5b15ef81f5 | |||
| 05088c1063 | |||
| 04c09efd75 | |||
| 2c0576099a | |||
| 27b49f9d7e | |||
| 6a247c425a | |||
| 7910034916 | |||
| c56e5d17be | |||
| 522eabce5e | |||
| 0e4b874994 | |||
| 319cc8a5f9 | |||
| b5c26392e1 | |||
| 7d53c55cc1 | |||
| 3e30994541 | |||
| a669fcba7a | |||
| 851053fd9a | |||
| b71a87e803 | |||
| c72658d21a | |||
| d0b8c59769 | |||
| 583b74fb7e | |||
| cf6f28aa3e | |||
| f3ece64635 | |||
| 6343b7087d | |||
| e279dc4cea | |||
| fc5763c8e8 | |||
| 9e32fcda88 | |||
| 7fe8c7b547 | |||
| 38329514f9 | |||
| 37705dfc71 | |||
| e8e7e3144f | |||
| 0da229e8f4 | |||
| 1029b2523d | |||
| 477b520cf0 | |||
| d54fdf5452 | |||
| bfb47aba87 | |||
| d241a57d38 | |||
| 01479a82de | |||
| 198820d95d | |||
| 86741a4e95 | |||
| 7d859661ba | |||
| d04c95514e | |||
| e6e3953a85 | |||
| 104f1232f3 | |||
| b55bf6e6c1 | |||
| d79af8fcc2 | |||
| ff05429f58 | |||
| 0dbabe629a | |||
| 3348d46b5a | |||
| aff1a245c2 | |||
| 51802f71a5 | |||
| 32f34f1444 | |||
| b7309ccdf1 | |||
| 8abaac56ff | |||
| a462ab98e9 | |||
| af602a414a | |||
| 67e426354c | |||
| e2f8e31fa0 | |||
| d67cce4184 | |||
| ca63b412cb | |||
| 89e372c229 | |||
| 4ae5b49764 | |||
| 1057f32b34 | |||
| e3948eb4ba | |||
| 6d363235a5 | |||
| fb50b4fa78 | |||
| 85fbaa2b97 | |||
| 1440453640 | |||
| e364959d25 | |||
| 89642e71b5 | |||
| d8413b3b7d | |||
| 92ba038af7 | |||
| 2ac2cc2ebf | |||
| 990ed86283 | |||
| 953d702ed6 | |||
| 1fa828ad1a | |||
| 9f16fa189f | |||
| a7e78cc3fb | |||
| 353c40cc46 | |||
| 3a7c8d660c | |||
| b2c9108ffc | |||
| ef447e2ba1 | |||
| 85f0b37b03 | |||
| 9f6cdfcd11 | |||
| 376477767b | |||
| f1376be2e0 | |||
| 5e57ec0e67 | |||
| 15721f1279 | |||
| e8fe58438d | |||
| 8ef04b1ad0 | |||
| a3d6776c02 | |||
| e7a529257a | |||
| 714afabf30 | |||
| f1df8d07bb | |||
| a18c4b6449 | |||
| 2099342352 | |||
| 50ee2727a2 | |||
| 571e918ee0 | |||
| 7c5d8721c0 | |||
| b5984a965e | |||
| 0e0bd94251 | |||
| a9d2d52c1e | |||
| b7c67a16d9 | |||
| 35615526a1 | |||
| c43ce52e78 | |||
| 61a313f24b | |||
| d71393cead | |||
| 64bb6714a3 | |||
| f8336d6adb | |||
| 30245f4aed | |||
| a861652d6f | |||
| 5fe5ba78da | |||
| 8fa9977617 | |||
| 0df2dd3ce2 | |||
| 80893f2592 | |||
| 91cf10541b | |||
| aaccfdee44 | |||
| a8449de731 | |||
| 9d98641a94 | |||
| 4f13227d9e | |||
| 70e9ee0dd1 | |||
| d43932a9d2 | |||
| 7118261fee | |||
| 89b171db36 | |||
| 1c4778790a | |||
| d20d1488de | |||
| c969f277ee | |||
| cf4a77af75 | |||
| 4042592420 | |||
| a750241ec3 | |||
| 8ab79ec1ae | |||
| a9289df93f | |||
| f6c42c4b53 | |||
| bfb121938a | |||
| 20d26d4bcd | |||
| 474b851275 | |||
| 5c541275c5 | |||
| 486e92dfc9 | |||
| 959ce3bc92 | |||
| 3866c740e9 | |||
| a65a85fb67 | |||
| c921b7bb1c | |||
| 7acf2d80d2 | |||
| 01b2d6cb39 | |||
| e2665e3cbe | |||
| 9b95094207 | |||
| 59b637c317 | |||
| 52963e8e80 | |||
| 43f42ac917 | |||
| 0352951d8c | |||
| fe334d0aed | |||
| f2371d8735 | |||
| 3c9608efbd | |||
| 8dff3b55c9 | |||
| b42b6303ae | |||
| 263e6d5cca | |||
| 5b5622b4a7 | |||
| e328bf616f | |||
| 52ba831b1c | |||
| 12b1016d66 | |||
| a471bfec6a | |||
| 3b6d3116bd | |||
| 5c0c680f56 | |||
| 3af8f4772f | |||
| 10b2593389 | |||
| cf67d58236 | |||
| 748227319c | |||
| 94c18e835a | |||
| cfb5693ffe | |||
| 3e1f3d5a51 | |||
| c0bf529a6b | |||
| f951ee1f91 | |||
| e8ac971f9e | |||
| b1278aff2e | |||
| c26c14227c | |||
| ce36bcc951 | |||
| 1134cd7788 | |||
| 7920d2225c | |||
| d0faaf2d53 | |||
| fc60878c25 | |||
| 1b941536d0 | |||
| 4215bfc6fe | |||
| 22d215b2c7 | |||
| 69eb7979c1 | |||
| 0f0c24d31b | |||
| 30c5a0d9cd | |||
| 93e0773758 | |||
| c92e64afbd | |||
| 6ba9dc74d8 | |||
| f69c244080 | |||
| f1b4a973a8 | |||
| efe49ee4c1 | |||
| 51573cb9f5 | |||
| f6ab374718 | |||
| 7ace88ae0d | |||
| 5f5e04506c | |||
| 9a58e121e8 | |||
| 7153347d67 | |||
| 611ccd9605 | |||
| 0befa2eb39 | |||
| b0677bb66d | |||
| f476f1447c | |||
| c8907232da | |||
| 864ca81d1d | |||
| 636427ac6b | |||
| 3d68caed6b | |||
| 4f7e25038c | |||
| ba0a95ed26 | |||
| 0cd25be408 | |||
| c6fa0ddde3 | |||
| b64a9b7a44 | |||
| 326bf39bed | |||
| ac4c37ba48 | |||
| 305017d523 | |||
| 19d1c552fe | |||
| 7d6908d146 | |||
| 9cf399f956 | |||
| a0b4450e5b | |||
| 9d983a7c96 | |||
| 89be08bc58 | |||
| 6dcf3e15da | |||
| e66cb9479f | |||
| bd8d9cc3aa | |||
| 9453b86907 | |||
| ecf9441fae | |||
| 70b659acbe | |||
| 2ebb9f2763 | |||
| 1231b13dfd | |||
| bebad4d361 | |||
| ed5a1887f4 | |||
| 342e689825 | |||
| c5c1a9b25b | |||
| 7637905de2 | |||
| ec780b7b93 | |||
| 3728c59f1d | |||
| 4442d9a706 | |||
| 8928b35f30 | |||
| fca189ee4c | |||
| d0fb3dde66 | |||
| fc4377abaf | |||
| 97a62752c6 | |||
| 87e1e55da7 | |||
| 5410cda182 | |||
| ee240ca201 | |||
| 98ffa0a256 | |||
| 2f450b7ea0 | |||
| 099b50dbfa | |||
| ece7a443fe | |||
| 984f2669b8 | |||
| 9b94b9e3fe | |||
| a781bcb7f4 | |||
| b596f38ab5 | |||
| f2b4d6f829 | |||
| d2d2dc64db | |||
| 5b8171a719 | |||
| 838ad5ec1a | |||
| 3d16323e33 | |||
| ba25eaa788 | |||
| 747412e522 | |||
| 059ff25d8e | |||
| 831143427d | |||
| de51d4860e | |||
| 6569038777 | |||
| 075aec3ac0 | |||
| 734847db68 | |||
| a99f2b78a1 | |||
| 037473f2e1 | |||
| 378f735e9f | |||
| 4cdac76ca5 | |||
| 9ed3c8a6bb | |||
| 252d289a8d | |||
| 832bad1fee | |||
| d5b99316e5 | |||
| 3723c5dae5 | |||
| 7c3fa7aef5 | |||
| 4ee31db7a0 | |||
| 0d8cb7393c | |||
| 0d3d0a8da1 | |||
| e45eb7f2be | |||
| 128e5c414c | |||
| 5a70d98c71 | |||
| 69d4215397 | |||
| 722190305b | |||
| 428d4859da | |||
| 9455860554 | |||
| eb4b42c089 | |||
| 79b3d7359a | |||
| c95ea19da1 | |||
| a8b3f2af14 | |||
| 2b8ccc38bb | |||
| baebadb7a6 | |||
| f73db056f2 | |||
| 429ac1e71c | |||
| 07cd91dda4 | |||
| 956f61e775 | |||
| 14fc2d2c70 | |||
| 9c07240948 | |||
| 7e2260578e | |||
| 7f2d781241 | |||
| df05ade4a7 | |||
| e4b45e512f | |||
| 623bb52fd8 | |||
| ab809dabf8 | |||
| 2ebf26c156 | |||
| 1ffc598fd4 | |||
| 0495d954bc | |||
| cc6a7a6a72 | |||
| b081bbb444 | |||
| 69cbb262ba | |||
| ba2a398da3 | |||
| a8a87e7f79 | |||
| 2ffd1e6e47 | |||
| efaebb43aa | |||
| d201350a08 | |||
| 81184d77fe | |||
| b9d60e0018 | |||
| f50ac49ede | |||
| 70d8b8a88a | |||
| 535f1b5988 | |||
| 366c903f84 | |||
| 1781ff36c6 | |||
| 4e131d9a54 | |||
| 7f6fec5530 | |||
| 2d43014029 | |||
| 33ea66ea88 | |||
| b74e449efd | |||
| afd4820713 | |||
| 8e032fa26a | |||
| 897669a069 | |||
| 03ea78b40b | |||
| 18465cf6cb | |||
| ac6ccd2dc7 | |||
| fb32d11d64 | |||
| ec75e911bd | |||
| 603f032e28 | |||
| cf76e43898 | |||
| a0df548dd3 | |||
| dff6048a07 | |||
| 478d465cb6 | |||
| 271f308c36 | |||
| 956958fa0e | |||
| 6a68da0559 | |||
| 6683eee49a | |||
| f258d5f932 | |||
| 600995a220 | |||
| 803dfed9f7 | |||
| 771cb45b49 | |||
| a2a7689881 | |||
| e9f4c3d0c7 | |||
| a5ef5769b9 | |||
| 9d251f94e5 | |||
| d1e8d52f5c | |||
| 14d2ae0c0e | |||
| dafa3bde8b | |||
| cb923cef47 | |||
| 93888aeed3 | |||
| c41e34e9d8 | |||
| 154aea7a61 | |||
| bc53cd1aa4 | |||
| ef51aa7e98 | |||
| 705c5ce790 | |||
| 26dee50e68 | |||
| be3d7ef11d | |||
| ef5897969f | |||
| d9cd0257ba | |||
| 95bb8cb1e5 | |||
| e1aa2f651a | |||
| 03c535e80d | |||
| 6f9c4f220e | |||
| 6f3813b685 | |||
| 510dde6923 | |||
| a7761c1867 | |||
| a2af965bad | |||
| 28b6ed500b | |||
| 6991fa39cc | |||
| 274469d299 | |||
| a0b8581ec5 | |||
| b844a0e98d | |||
| e5f14ac176 | |||
| a1110a847d | |||
| a3889fe369 | |||
| bd93918d9b | |||
| 421a6ba143 | |||
| f091256edb | |||
| c0e4733eb8 | |||
| e636a6ad58 | |||
| 071617bfc5 | |||
| 8a079c98cc | |||
| fa95f96a6d | |||
| c198abe5ac | |||
| ef5ba0f1d1 | |||
| fff7cd53cc | |||
| 64ce5138e1 | |||
| 3c07e7a0d8 | |||
| a1510ffcb5 | |||
| 1b96bce44f | |||
| 294a5e39a6 | |||
| 65fdd6e25f | |||
| 6d2ef1f985 | |||
| 14af19873c | |||
| 2f5dd470d2 | |||
| dc4592ae4d | |||
| baa3cfa636 | |||
| ad324df937 | |||
| f2b301cb17 | |||
| 6e369ea787 | |||
| f640abda91 | |||
| 77f8f11d99 | |||
| 12b4c27468 | |||
| 75f3073cbf | |||
| 5eddfaed98 | |||
| 89b4fdef56 | |||
| 6f08c1f758 | |||
| 1155789a75 | |||
| 8c4a74179d | |||
| bdbc94e54a | |||
| 30f880361e | |||
| c5ac347538 | |||
| 008da9467f | |||
| ee78249c2f | |||
| 412c67b47a | |||
| f4191ad984 | |||
| ea8d1100ac | |||
| 20e8c033cc | |||
| ff2d10e0e1 | |||
| 672c39e58d | |||
| 36d3849118 | |||
| 43d7dfde4c | |||
| 51a056b3d7 | |||
| 6e22d6687c | |||
| 369fdf07e2 | |||
| 8c6c1b3ccc | |||
| 160bdf5472 | |||
| 18c70002cb | |||
| 4b8cfd9fdd | |||
| ffa5fd5b2a | |||
| 9fb4798c77 | |||
| a6039278c3 | |||
| c50702c83b | |||
| d4c1e62c72 | |||
| 488c609109 | |||
| 0a6e24104e | |||
| d6acac0208 | |||
| b02f61ef21 | |||
| eb82b44726 | |||
| ca035f9447 | |||
| 19aa3c0854 | |||
| 93675d90bb | |||
| 0d90c1d960 | |||
| d826f968b5 | |||
| f682d7489f | |||
| 649b1adfdd | |||
| 3d67ea736f | |||
| ba88f837fe | |||
| 6728703473 | |||
| 55521afe35 | |||
| 8820b02406 | |||
| e79ba6117b | |||
| 668d29a9d7 | |||
| 6b97acdb50 | |||
| 9a11021560 | |||
| ee18217520 | |||
| 16de60f82d | |||
| 8cd94f976c | |||
| 9ab0662cbf | |||
| 5cceed1fd7 | |||
| 03a1b51bd7 | |||
| 1edf567ea7 | |||
| fa73eddf50 | |||
| 1923ae6f4b | |||
| 229f3dc061 | |||
| cc95042a94 | |||
| 302f54971d | |||
| dff88403b5 | |||
| cd3f9af232 | |||
| b217162cad | |||
| 7d24a61ae8 | |||
| ca1b37dbcf | |||
| 0e887fb2ea | |||
| a122adf05d | |||
| 04fc1e25e4 | |||
| cff402affb | |||
| e1e0368f8c | |||
| 8ee39d3822 | |||
| e2b533db64 | |||
| fc5db74f16 | |||
| 42e62a3698 | |||
| 62f03356ca | |||
| 25bb0912fb | |||
| beff1d8d42 | |||
| 7dc56cc075 | |||
| 394d653978 | |||
| 742fa27029 | |||
| 18b962dc57 | |||
| 233a179dad | |||
| 9cc76c5ee6 | |||
| 401ba40373 | |||
| 6dad50aab1 | |||
| 9a43f868aa | |||
| 294e9bb590 | |||
| d101311109 | |||
| e0e920bfe0 | |||
| 3a090d7c1d | |||
| dc851d856d | |||
| 2f8fe43ea3 | |||
| 959f96dd74 | |||
| 6620d2d023 | |||
| 408742494a | |||
| f7ae9c06d8 | |||
| b4c0d8e829 | |||
| b2a410d6cf | |||
| 16fa453163 | |||
| fc678684f5 | |||
| 8f7db7e52f | |||
| e9d6cf278a | |||
| d1c6f5f98e | |||
| 48f581525e | |||
| 8d967856b5 | |||
| ee83cf2989 | |||
| cb304d35e2 | |||
| 65185c5854 | |||
| 91526674be | |||
| 3e11ff65db | |||
| 4a3ac17a2f | |||
| 2283f1ddc9 | |||
| ae78887746 | |||
| 9de4bf839f | |||
| 295e51181b | |||
| 287e3a0090 | |||
| 4d150e71a3 | |||
| 0debbc263e | |||
| a9e5e40a54 | |||
| cf61080980 | |||
| 3e33731cf0 | |||
| d7cda90acd | |||
| 9a7dcc8c1a | |||
| d477121790 | |||
| 60e7a21e39 | |||
| b915ca2add | |||
| e90ef7aaee | |||
| e918daaa9b | |||
| 83b4f645db | |||
| 1797df7017 | |||
| 3beda9c201 | |||
| 68e6bd668c | |||
| 696cf61fe9 | |||
| 7fe56766fc | |||
| 1e62f8c951 | |||
| 82b995f96f | |||
| 05aa0c2482 | |||
| bca5f4bec4 | |||
| 8b599f78c0 | |||
| 38776e74bc | |||
| 21ce09a5b5 | |||
| 5fabec6a42 | |||
| 64f361d4b1 | |||
| f8ae842971 | |||
| ca01d6efb8 | |||
| 498ab6d165 | |||
| 8e8524ccc8 | |||
| 714936f710 | |||
| 11c0477a34 | |||
| 4f65c6bbcc | |||
| c7ee183fe4 | |||
| 80545ce1f2 | |||
| c80edcfe9f | |||
| 74318fb3c0 | |||
| 83a8942724 | |||
| 48f57068d8 | |||
| 3f85916764 | |||
| ab0f544267 | |||
| 14e16c874e | |||
| 7fb87ad67f | |||
| 1b85417ae6 | |||
| 8d41e6fece | |||
| ab2906991d | |||
| ac85c75cc4 | |||
| 7f1fae307c | |||
| cce41082a1 | |||
| 902a489b04 | |||
| 30006fe520 | |||
| 2ce1bb9c3f | |||
| c468fa2a95 | |||
| 06ef342953 | |||
| 02121105de | |||
| e24788157a | |||
| 63905c608e | |||
| 69127e6952 | |||
| 87a84f02a3 | |||
| 0b5bea9965 | |||
| 0d5e3a080c | |||
| a4ba07de99 | |||
| 970a5ec763 | |||
| 6f338c44c4 | |||
| f360fadd3f | |||
| 846fa4722b | |||
| 9752f1d492 | |||
| ce2d386b7e | |||
| 993d433d03 | |||
| ede344db3a | |||
| 6c397da1f8 | |||
| 1d9862fe11 | |||
| 2d3db00639 | |||
| 7269233134 | |||
| 27be0ad808 | |||
| 51e875694c | |||
| 92d64d77c5 | |||
| 579e9e0b8c | |||
| 25b9c491c3 | |||
| 8f1b48b0ab | |||
| 611d3c3b63 | |||
| 3a658e10f1 | |||
| 92a182e5a8 | |||
| 954f0cc26f | |||
| 5e0b4e1be6 | |||
| c72511da6a | |||
| 15c65ad4e1 | |||
| d06657e2ff | |||
| 5dae1ee34f | |||
| 88a5d33bad | |||
| 42e02a6129 | |||
| 7d56164882 | |||
| 3e60ad21bf | |||
| 55c4e9635a | |||
| 101d0212a7 | |||
| f0338b5247 | |||
| 80136b4aff | |||
| f0a89e99f5 | |||
| 09f830c315 | |||
| ace92f8be0 | |||
| 7e08f495a7 | |||
| a8299b4f54 | |||
| bdb00b8d58 | |||
| 4e18127371 | |||
| e5165e6fba | |||
| 77dc237549 | |||
| e3cc04f538 | |||
| 322d1ad75c | |||
| 4767903a07 | |||
| 0064ce97f0 | |||
| 71f067f678 | |||
| 8f37dc8570 | |||
| b15b640652 | |||
| 3e892f30ca | |||
| 20f3efced9 | |||
| 25bcd0f2a9 | |||
| d990bf794a | |||
| aa25feb57e | |||
| b203fc7c68 | |||
| 9298e045d3 | |||
| e3255588d3 | |||
| 514f23cf4d | |||
| ab206b2abf | |||
| 2f8a65d364 | |||
| a30e47d094 | |||
| 0a78a87a17 | |||
| 886ec9c574 | |||
| 434fe894bc | |||
| cde9dbe406 | |||
| c5262f188e | |||
| 8f57364547 | |||
| eb193f230b | |||
| 6a868f975e | |||
| 66a8db598f | |||
| bfa13583e3 | |||
| d865e646b9 | |||
| f630537897 | |||
| aaf7ae8fc3 | |||
| 52cba23190 | |||
| c48c74a0cf | |||
| 9c6c27909d | |||
| 3ba3e5cf76 | |||
| df4040a96a | |||
| 6932e468cf | |||
| a82279b068 | |||
| 0ae7b9c120 | |||
| 72f86069a2 | |||
| 509c32db70 | |||
| 7fa45c0658 | |||
| 4e81a6af83 | |||
| 66d3eb3400 | |||
| 7e36cb76ba | |||
| fc47651c33 | |||
| 6948d70d94 | |||
| eee2db8ad9 | |||
| 318a36a831 | |||
| 4d90bd9e31 | |||
| 4e81151e11 | |||
| 18195b778a | |||
| 326bddad2a | |||
| f4b41a43cf | |||
| 38c762f775 | |||
| 9833d466c4 | |||
| 4d965333ef | |||
| a6ec18690d | |||
| d3fe0b48d8 | |||
| 35220e1c4d | |||
| de7bc9870d | |||
| 680a3a5d13 | |||
| cf8612c99f | |||
| e817d3355c | |||
| 53b7f33ec4 | |||
| 5ed9a29f81 | |||
| 89406c4754 | |||
| b27b26837f | |||
| 0595008fce | |||
| d6b2e019c7 | |||
| 25cfb1f5c0 | |||
| 10a159b315 | |||
| 60c113b0e8 | |||
| ec4330aea7 | |||
| f263fe30a4 | |||
| 0ec2f68a96 | |||
| 1156b909eb | |||
| 9ed246e482 | |||
| c342c5c4f6 | |||
| 3b7ef5b262 | |||
| 79bf3d8d4f | |||
| cfe91a8722 | |||
| c3e2d34372 | |||
| 56b6ee5d2a | |||
| 3f34ac1c75 | |||
| be5015d16b | |||
| 2c1d4cfc40 | |||
| 5bf75e122c | |||
| aa51048990 | |||
| 6d117f824d | |||
| 0a591e748f | |||
| be2cb5d2f2 | |||
| 280ec22348 | |||
| f9a815bba6 | |||
| 1534dcc932 | |||
| e19ae7af59 | |||
| 1726d6f59e | |||
| 756f395158 | |||
| 90d2cf4970 | |||
| 37e70fd5bb | |||
| 126481833b | |||
| b2630cfb49 | |||
| 579e2e95e3 | |||
| 6580436900 | |||
| a0ecf72a1c | |||
| bc3aaa94de | |||
| 482ca5ed15 | |||
| 66894c2e8a | |||
| 5d5a1fadd9 | |||
| 2d531c3a15 | |||
| e4da11727a | |||
| 309f97b443 | |||
| 98240e910e | |||
| 6d79d3d1f8 | |||
| def4e19359 | |||
| 6cfd055bec | |||
| 1208b25b66 | |||
| b25bf70224 | |||
| 7ecb139dba | |||
| 2a7720852a | |||
| 627eb556cb | |||
| e251155f4f | |||
| 4e9c44e574 | |||
| 5b662df7cd | |||
| 802574fb44 | |||
| 51f3334711 | |||
| 254b5c963b | |||
| d5f64a79b2 | |||
| dd54a714cb | |||
| d40e9e3d73 | |||
| d5066adb4b | |||
| f933aae562 | |||
| db65a80a7f | |||
| 2f9658fa22 | |||
| ab06365a81 | |||
| 2bc27bd6f5 | |||
| 078ac1abe4 | |||
| 05259eb0d2 | |||
| 8c272ea032 | |||
| f58f29389c | |||
| 854b137820 | |||
| fb8b5ac6b6 | |||
| 35106f5e67 | |||
| a5ea58dbf7 | |||
| b4ddc8e5fc | |||
| 2223405e79 | |||
| 7260e0d0f5 | |||
| 44cf7f7330 | |||
| 465ceab271 | |||
| fa818d50d1 | |||
| deb75df77a | |||
| aae47e103b | |||
| 957dbecb3d | |||
| 75fe59ef79 | |||
| 63ba5cf39d | |||
| 8a0b7fbc47 | |||
| ea9fb5bcb0 | |||
| 21d8ceacf0 | |||
| 986cf025a0 | |||
| 85b936ead2 | |||
| f241679087 | |||
| 6618cdc0ad | |||
| fe2de5d8ae | |||
| bd954e7292 | |||
| 86367298cc | |||
| 08346a2326 | |||
| 1bb9090434 | |||
| 879bcdf8d8 | |||
| 1b7658d8f9 | |||
| d635705449 | |||
| af1b17cb98 | |||
| 944ef83805 | |||
| 20b3e95b4f | |||
| c2de0dd613 | |||
| 2a7f5f55b0 | |||
| a03539ad8b | |||
| c420a8a686 | |||
| 8d6a2e8882 | |||
| 8da9156bb5 | |||
| ffc8a5188d | |||
| 4eb1117659 | |||
| b4602c1e48 | |||
| 80ede42191 | |||
| ea6937c0ac | |||
| 2f99ba8526 | |||
| 16324e16c7 | |||
| c2e1ecf5b1 | |||
| cd353217d6 | |||
| 6903f67f0f | |||
| 487dab793a | |||
| 4cd3f9bd38 | |||
| f581e76057 | |||
| 312aa856ad | |||
| 6f4363e95b | |||
| 595ba92f06 | |||
| df9114d7b0 | |||
| 1496d0e482 | |||
| 92f5783f35 | |||
| da1895125a | |||
| bcdd470567 | |||
| 7cd0070d90 | |||
| 8217949fa2 | |||
| 4bdf1553c9 | |||
| 6fd5d38cd8 | |||
| 8a737104ef | |||
| a608da759b | |||
| c828625cc1 | |||
| 7ead247fcb | |||
| 7a62af78d0 | |||
| 9896d6e6b2 | |||
| 9c376183e0 | |||
| e5cc9a3cbf | |||
| 985979ab14 | |||
| 377a4c2284 | |||
| 24242db981 | |||
| 73a4152b5d | |||
| b5eb094566 | |||
| 71140381e0 | |||
| 22387b2610 | |||
| da53a7d469 | |||
| b9b30f9f53 | |||
| c984eb9b8c | |||
| eef548f6a1 | |||
| 2da8ffb8cd | |||
| 8188c21641 | |||
| 19dca8c5bb | |||
| 5de5b6047a | |||
| 695c4159dc | |||
| 2624315bd5 | |||
| fff6e50702 | |||
| 753b69726a | |||
| df273aca7e | |||
| 9e9feb5980 | |||
| e71eb465fb | |||
| e9f2550a26 | |||
| 12bfd98011 | |||
| 2b45056842 | |||
| c86fdf07db | |||
| 385ffd5b27 | |||
| ad6b746124 | |||
| 57e8b59db6 | |||
| ecb869695e | |||
| c99c7ee6c9 | |||
| d3c47fe1a4 | |||
| 1c21b23e8e | |||
| bb55cd3c42 | |||
| 4a74190241 | |||
| 586f9594fb | |||
| e68facfd51 | |||
| 7d423fa49d | |||
| 886553a923 | |||
| ff4a00d103 | |||
| dbdf8a97ae | |||
| 2433e31db0 | |||
| 5538aac732 | |||
| c05f169d54 | |||
| 24aaefe342 | |||
| c167e881fb | |||
| 9a8c22f487 | |||
| fd8077a5cd | |||
| 6dd196a7a6 | |||
| bdfe14e278 | |||
| 1c7b318c3f | |||
| a6acc30eea | |||
| c54a849243 | |||
| 1f2871f649 | |||
| 147d5902ed | |||
| ec47ca1983 | |||
| 6ca6fc6989 | |||
| f29015426f | |||
| 57f09fd64d | |||
| 2477ee4952 | |||
| 2bd7fd2aa7 | |||
| bc5529ded8 | |||
| b05736e4fb | |||
| 6a187881c0 | |||
| d1accc4e09 | |||
| e7e130460e | |||
| d33a02f6cd | |||
| 8f6c511598 | |||
| 7fb04d51ae | |||
| 64d5275554 | |||
| 76041a9f55 | |||
| 502f7de040 | |||
| a86512e807 | |||
| 2e19dbe05f | |||
| 18d8f07242 | |||
| fb0ff734d7 | |||
| 13a5482f91 | |||
| 4a5794ad5f | |||
| 7652aca406 | |||
| f1dfb1c952 | |||
| b4f866be03 | |||
| 4fed35d189 | |||
| c3cff6a911 | |||
| 91f718f39d | |||
| e4ea01cfd2 | |||
| dd7d5a46e8 | |||
| 3be4b08bb0 | |||
| 2fde7b7879 | |||
| 60777e5714 | |||
| 9d7fcc4987 | |||
| 95ed1c3ac3 | |||
| 7e285969a7 | |||
| 083f1be766 | |||
| d39ed3c03f | |||
| a068bda34f | |||
| 559f37e5d2 | |||
| d50b7de668 | |||
| 35c5191a4d | |||
| 6df6296cd1 | |||
| b88a565344 | |||
| d98bc1ec63 | |||
| 74886db10d | |||
| 48e4fd8088 | |||
| 54c7f896ad | |||
| ccd14e6505 | |||
| dc33534478 | |||
| 83d79e23d5 | |||
| 151f99e343 | |||
| ccea63f1e2 | |||
| 1019419d85 | |||
| a234823d0a | |||
| d2015845e8 | |||
| 1857cca081 | |||
| 195bbf4652 | |||
| af76d937e1 | |||
| 885780a92b | |||
| 547bfe26d7 | |||
| bc87fdde1f | |||
| 6fc8324337 | |||
| 5103d91db0 | |||
| b67ab5ecd3 | |||
| 3aa085f17e | |||
| 1b9a78827a | |||
| 3299e0fa6a | |||
| 79e12c8dd8 | |||
| ced33114cc | |||
| 294324e31b | |||
| 1b27d652af | |||
| e98209249f | |||
| 9ddf09e187 | |||
| 73e975cfa1 | |||
| a942ea6ee2 | |||
| 38ca7c030f | |||
| 7d59edc8a5 | |||
| 7d05450244 | |||
| 8e5ba70706 | |||
| d43c40fc59 | |||
| 02ce707546 | |||
| 4a11ed6dc4 | |||
| 72d07fc3fb | |||
| 9b910b26a9 | |||
| 4a9be7b4a5 | |||
| 34958c654f | |||
| bf91156182 | |||
| b774b6920d | |||
| 56f6143873 | |||
| 7ba051c7ff | |||
| 63f5c240a4 | |||
| ba91e433c3 | |||
| 2818b207db | |||
| 0527066a4c | |||
| a48400535f | |||
| fa201d21e2 | |||
| c9222ff6f6 | |||
| a45d311339 | |||
| b59278601c | |||
| b48a913fbf | |||
| a0ef779aee | |||
| 861ff636b1 | |||
| bbce4bdeb0 | |||
| df0ceb4850 | |||
| b1e45756dd | |||
| 4b52878dc1 | |||
| 62a4f18866 | |||
| b00faf3930 | |||
| d6ddb2b247 | |||
| 900510e7f2 | |||
| 7370af0084 | |||
| 9644693de3 | |||
| b6bc269505 | |||
| 82161c10f8 | |||
| c939033031 | |||
| 8c8c040fb3 | |||
| 48a07e2b34 | |||
| 7bdfbb17b5 | |||
| 492646edc3 | |||
| 975096b221 | |||
| 0c4c790a65 | |||
| 012539b5d3 | |||
| f4cd9f27e4 | |||
| 449c2322b8 | |||
| 7efdcdc592 | |||
| 356aa27f70 | |||
| a579c7882f | |||
| 3077cf6bb5 | |||
| 7c46ee50a4 | |||
| 1d37910bd4 | |||
| 5555ff2ac1 | |||
| 2ad93d79f5 | |||
| 6d32fe743b | |||
| f2dd96c3bf | |||
| 9b4384d3e6 | |||
| c1831e58f5 | |||
| 991f241209 | |||
| b565cf4be1 | |||
| 8a5e1cd4f1 | |||
| 4445121023 | |||
| 1421869094 | |||
| 24480ef13a | |||
| 73bd7991bb | |||
| 6e74761977 | |||
| a7f7a226d6 | |||
| 491cdc5fef | |||
| 98fe47d085 | |||
| 2290627c5f | |||
| 656d436028 | |||
| cff8e822f2 | |||
| 5dfd46f809 | |||
| 97a6395ddd | |||
| 5226106ff0 | |||
| 5e838e3f10 | |||
| 574860f117 | |||
| c19857d463 | |||
| 6741d58bec | |||
| eb806dbca5 | |||
| 7d8aa683ff | |||
| 3bceaa7a2f | |||
| bfa45b66ac | |||
| e39b89859f | |||
| 1f14a01994 | |||
| 91310ce98c | |||
| 94dcfbf197 | |||
| d2b4b8b279 | |||
| aaa9495ceb | |||
| a8b75c9b13 | |||
| f21685e81d | |||
| e24a7f887a | |||
| 54b5ab6d79 | |||
| 361b2ed7c7 | |||
| cee6c7c999 | |||
| 3e5efd4090 | |||
| 10b8ba1ec8 | |||
| be90380775 | |||
| 3b2e775698 | |||
| 01ba90689f | |||
| e53fa82c46 | |||
| 48ccbfd860 | |||
| 601d4e1f48 | |||
| 7fce3bdd25 | |||
| 4b1bccd4b5 | |||
| 28e809f92f | |||
| 866e0e3fa9 |
@@ -0,0 +1,67 @@
|
||||
name: Iris Bug Report
|
||||
description: File a bug report for Iris
|
||||
labels: [ bug ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to fill this out!
|
||||
If this does not work for you, feel free to use the [blank](https://github.com/VolmitSoftware/Iris/issues/new) format.
|
||||
- type: textarea
|
||||
id: how
|
||||
attributes:
|
||||
label: Problem
|
||||
description: Please give a text description of how you reached the problem
|
||||
value: |
|
||||
1. Install Iris...
|
||||
2. Do this...
|
||||
3. Do that...
|
||||
4. Observe the error...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
label: Solution
|
||||
description: Explain where you think the problem comes from (optional)
|
||||
placeholder: The code to place a is missing b and c...
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: mcversion
|
||||
attributes:
|
||||
label: Minecraft Version
|
||||
description: What version of Minecraft is the server on?
|
||||
options:
|
||||
- 1.14.X
|
||||
- 1.15.X
|
||||
- 1.16.X
|
||||
- 1.17
|
||||
- 1.17.1
|
||||
- 1.18
|
||||
- 1.19
|
||||
- 1.20
|
||||
- 1.21
|
||||
- 1.22
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: irisversion
|
||||
attributes:
|
||||
label: Iris Version
|
||||
description: What version of Iris are you running? (see console)
|
||||
placeholder: DO NOT SAY "LATEST"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: logs
|
||||
attributes:
|
||||
label: Log
|
||||
description: Paste a full log. Always use [mclogs](https://mclo.gs) Or [Pastebin](https://pastebin.com/). Must not be a crash report. Must be a full log. Must not be a screenshot of a log.
|
||||
placeholder: https://mslog.gs/...
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
id: thanks
|
||||
attributes:
|
||||
value: "Thank you for filling out the form! We will be with you soon. Please do not ask support to review your report."
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: Bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots or Video Recordings**
|
||||
If applicable, add screenshots or video recordings to help explain your problem.
|
||||
|
||||
**Server and Plugin Informations**
|
||||
- Installed plugins:
|
||||
- Iris Version:
|
||||
- Server Platform and Version [eg: PaperSpigot 1.16.3 #240]:
|
||||
- Operating System (if applicable):
|
||||
- Server Logs:
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here, server timings reports, Iris dump information, complete console log etc. Please do not make Pastebin dumps or screenshot expire.
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Iris Feature Request
|
||||
description: File a feature request for Iris. If you want to report a bug this is not the place.
|
||||
labels: [ feature ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to fill this out!
|
||||
If this does not work for you, feel free to use the [blank](https://github.com/VolmitSoftware/Iris/issues/new) format.
|
||||
- type: dropdown
|
||||
id: arc
|
||||
attributes:
|
||||
label: Adding, Removing, or Changing
|
||||
description: What are you doing
|
||||
options:
|
||||
- Adding
|
||||
- Removing
|
||||
- Changing
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: atype
|
||||
attributes:
|
||||
label: Type of Modification
|
||||
description: What is it for?
|
||||
value: I want to ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: desc
|
||||
attributes:
|
||||
label: What are you trying to modify
|
||||
description: Give as detailed of a description as you can for the modification that you want done (include pictures if applicable)
|
||||
value: The way I would implement this is ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternative
|
||||
attributes:
|
||||
label: Alternatives
|
||||
description: What alternatives have you considered?
|
||||
value: If this could not be implemented I would ...
|
||||
- type: markdown
|
||||
id: thanks
|
||||
attributes:
|
||||
value: "Thank you for filling out the form! We will be with you soon. Please do not ask support to review your report."
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: Enhancement, Addition
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -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
-114
@@ -1,125 +1,42 @@
|
||||
# User-specific stuff
|
||||
|
||||
build/
|
||||
libs/
|
||||
|
||||
.gradle/
|
||||
|
||||
.idea/
|
||||
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Package Files #
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
collection/
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
out/
|
||||
tools/simd-bench/out/
|
||||
tools/simd-bench/simd-bench.jar
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
DataPackExamples/
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
TreeGenStuff/
|
||||
dist/
|
||||
core/plugins/
|
||||
adapters/bukkit/plugin/plugins/
|
||||
adapters/*/run/
|
||||
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
.codegraph/
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
plans/
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
local.properties
|
||||
*.jks
|
||||
*.p12
|
||||
*.pfx
|
||||
*.pem
|
||||
*.key
|
||||
*.hprof
|
||||
credentials.json
|
||||
service-account*.json
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
target/
|
||||
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
.flattened-pom.xml
|
||||
|
||||
# Common working directory
|
||||
run/
|
||||
|
||||
.gradle
|
||||
**/build/
|
||||
!src/**/build/
|
||||
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
|
||||
# Cache of project
|
||||
.gradletasknamecache
|
||||
|
||||
# Custom folders
|
||||
release/
|
||||
docs/
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="minecraft" name="Minecraft">
|
||||
<configuration>
|
||||
<autoDetectTypes>
|
||||
<platformType>SPIGOT</platformType>
|
||||
<platformType>BUKKIT</platformType>
|
||||
</autoDetectTypes>
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
|
||||
<output url="file://$MODULE_DIR$/target/classes" />
|
||||
<output-test url="file://$MODULE_DIR$/target/test-classes" />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Maven: io.timeandspace:smoothie-map:2.0.2" level="project" />
|
||||
<orderEntry type="library" name="Maven: io.timeandspace:jpsg-core:1.4" level="project" />
|
||||
<orderEntry type="library" name="Maven: org.checkerframework:checker-qual:2.11.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.spigotmc:spigot-api:1.16.1-R0.1-SNAPSHOT" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: commons-lang:commons-lang:2.6" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: com.google.guava:guava:21.0" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.md-5:bungeecord-chat:1.16-R0.3" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.yaml:snakeyaml:1.26" level="project" />
|
||||
<orderEntry type="library" name="Maven: io.papermc:paperlib:1.0.5" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.bukkit.craftbukkit:1.16.3:1.16.3" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.bukkit.craftbukkit:1.16.4:1.16.4" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: com.bergerkiller.bukkit:BKCommonLib:1.16.4-v2" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: com.sk89q.worldedit:worldedit-bukkit:7.2.0-SNAPSHOT" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: com.sk89q.worldedit:worldedit-core:7.2.0-SNAPSHOT" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: com.sk89q.worldedit.worldedit-libs:core:7.2.0-SNAPSHOT" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: de.schlichtherle:truezip:6.8.3" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-profile-default_2.13:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.scala-lang:scala-library:2.13.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-driver-http:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-kernel-spec:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truecommons:truecommons-annotations:2.5.0" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: com.google.code.findbugs:annotations:3.0.0" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truecommons:truecommons-cio:2.5.0" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truecommons:truecommons-io:2.5.0" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truecommons:truecommons-services:2.5.0" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truecommons:truecommons-logging:2.5.0" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: javax.inject:javax.inject:1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.httpcomponents:httpclient:4.5.10" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.httpcomponents:httpcore:4.4.12" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: commons-codec:commons-codec:1.11" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.slf4j:jcl-over-slf4j:1.7.28" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-driver-odf:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-comp-zipdriver:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-comp-zip:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.bouncycastle:bcprov-jdk15on:1.63" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-driver-tar:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-comp-tardriver:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.commons:commons-compress:1.19" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-driver-tar-bzip2:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-driver-tar-gzip:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-driver-tar-xz:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.tukaani:xz:1.8" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-driver-zip-raes:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-profile-base_2.13:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-access-swing:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-access:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-driver-file:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-driver-jar:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-driver-zip:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-comp-ibm437:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truevfs:truevfs-kernel-impl_2.13:0.12.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truecommons:truecommons-key-console:2.5.0" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truecommons:truecommons-key-default:2.5.0" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truecommons:truecommons-key-swing:2.5.0" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truecommons:truecommons-key-macosx:2.5.0" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truecommons:truecommons-key-spec:2.5.0" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.truecommons:truecommons-shed:2.5.0" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: net.java.dev.jna:jna:4.1.0" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.mozilla:rhino-runtime:1.7.12" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: com.google.code.findbugs:jsr305:1.3.9" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.antlr:antlr4-runtime:4.7.2" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: com.sk89q.worldedit.worldedit-libs:bukkit:7.2.0-SNAPSHOT" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.logging.log4j:log4j-slf4j-impl:2.8.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.logging.log4j:log4j-api:2.8.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.bstats:bstats-bukkit:1.7" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: it.unimi.dsi:fastutil:8.2.1" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: io.lumine.xikage:MythicMobs:4.9.1" level="project" />
|
||||
<orderEntry type="library" name="Maven: com.github.ben-manes.caffeine:caffeine:2.8.5" level="project" />
|
||||
<orderEntry type="library" name="Maven: com.google.errorprone:error_prone_annotations:2.4.0" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.projectlombok:lombok:1.18.10" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: com.google.code.gson:gson:2.8.5" level="project" />
|
||||
<orderEntry type="library" name="Maven: org.zeroturnaround:zt-zip:1.14" level="project" />
|
||||
<orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.6.6" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
+501
-18
@@ -1,35 +1,518 @@
|
||||
VOLMIT SOFTWARE PUBLIC LICENSE
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Version 1.2, 13 March 2021
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||
|
||||
Copyright :copyright: 2021 Andrew Baker
|
||||
Preamble
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
The GNU General Public License is a free, copyleft license for software and other kinds of works.
|
||||
|
||||
0. Definitions
|
||||
The licenses for most software and other practical works are designed to take away your freedom to share and change the
|
||||
works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all
|
||||
versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use
|
||||
the GNU General Public License for most of our software; it applies also to any other work released this way by its
|
||||
authors. You can apply it to your programs, too.
|
||||
|
||||
"License" refers to this file. An updated version is available at <https://github.com/VolmitSoftware/Licenses>. The version at <https://github.com/VolmitSoftware/Licenses> is always correct and up-to-date.
|
||||
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make
|
||||
sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive
|
||||
source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and
|
||||
that you know you can do these things.
|
||||
|
||||
"Author" refers to the person Andrew Baker <andrew@andrewcbaker.net>.
|
||||
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights.
|
||||
Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it:
|
||||
responsibilities to respect the freedom of others.
|
||||
|
||||
"Repository" refers to the location at which the software protected under this License is stored.
|
||||
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients
|
||||
the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must
|
||||
show them these terms so they know their rights.
|
||||
|
||||
"Content" refers to the files and folders inside of the Repository.
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute
|
||||
and/or modify it.
|
||||
|
||||
"Copyright" refers to the laws on intellectual property and the legal rights automatically granted to the Author during the creation of the Content.
|
||||
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software.
|
||||
For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems
|
||||
will not be attributed erroneously to authors of previous versions.
|
||||
|
||||
"Modify" refers to editing the Content as well as creating programs or code which depends on the Content to run.
|
||||
Some devices are designed to deny users access to install or run modified versions of the software inside them, although
|
||||
the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the
|
||||
software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely
|
||||
where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we stand ready to extend this provision to those
|
||||
domains in future versions of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
"Compile" refers to the act of building the Content into an executable file such as a JAR file.
|
||||
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict
|
||||
development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger
|
||||
that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
1. You may freely copy, Modify, distribute, create derivative works, or distribute derivative works of the Content as long as you obey the unmodified License that is supplied with the Content.
|
||||
The precise terms and conditions for copying, distribution and modification follow.
|
||||
|
||||
2. You may not copy, Modify, or distribute the Content in such a way that it will, directly or indirectly, generate revenue without explicit, written permission from the Author.
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
3. You may not host publicly available download links to Compiled versions of the Content unless you are using less than 5% of the original Content in a Repository of your own.
|
||||
0. Definitions.
|
||||
|
||||
4. For any conditions not outlined in the License, refer to your country or state laws for Copyright.
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
5. If you do not agree to any of the above conditions you must delete the Content in its entirety as well as all copies of the Content and derivative works of the Content that you have made.
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
|
||||
|
||||
END TERMS AND CONDITIONS
|
||||
"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "
|
||||
Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission,
|
||||
other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a
|
||||
work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily
|
||||
liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy.
|
||||
Propagation includes copying, distribution (with or without modification), making available to the public, and in some
|
||||
countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction
|
||||
with a user through a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright
|
||||
notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the extent that warranties are provided), that
|
||||
licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a
|
||||
list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means
|
||||
any non-source form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body,
|
||||
or, in the case of interfaces specified for a particular programming language, one that is widely used among developers
|
||||
working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in
|
||||
the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to
|
||||
enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is
|
||||
available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used
|
||||
to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (
|
||||
for an executable work) run the object code and to modify the work, including scripts to control those activities.
|
||||
However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs
|
||||
which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding
|
||||
Source includes interface definition files associated with source files for the work, and the source code for shared
|
||||
libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data
|
||||
communication or control flow between those subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the
|
||||
Corresponding Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided
|
||||
the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program.
|
||||
The output from running a covered work is covered by this License only if the output, given its content, constitutes a
|
||||
covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not convey, without conditions so long as your license
|
||||
otherwise remains in force. You may convey covered works to others for the sole purpose of having them make
|
||||
modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do not control copyright. Those thus making or running
|
||||
the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that
|
||||
prohibit them from making any copies of your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not
|
||||
allowed; section 10 makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling
|
||||
obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or
|
||||
restricting circumvention of such measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the
|
||||
extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you
|
||||
disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users,
|
||||
your or third parties' legal rights to forbid circumvention of technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating
|
||||
that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices
|
||||
of the absence of any warranty; and give all recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for
|
||||
a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source
|
||||
code under the terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of
|
||||
the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or
|
||||
distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the
|
||||
compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause
|
||||
this License to apply to the other parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License, in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library,
|
||||
need not be included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used
|
||||
for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In
|
||||
determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a
|
||||
particular product received by a particular user, "normally used" refers to a typical or common use of that class of
|
||||
product, regardless of the status of the particular user or of the way in which the particular user actually uses, or
|
||||
expects or is expected to use, the product. A product is a consumer product regardless of whether the product has
|
||||
substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of
|
||||
the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information
|
||||
required to install and execute modified versions of a covered work in that User Product from a modified version of its
|
||||
Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code
|
||||
is in no case prevented or interfered with solely because modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the
|
||||
conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to
|
||||
the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding
|
||||
Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not
|
||||
apply if neither you nor any third party retains the ability to install modified object code on the User Product (for
|
||||
example, the work has been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a requirement to continue to provide support
|
||||
service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product
|
||||
in which it has been modified or installed. Access to a network may be denied when the modification itself materially
|
||||
and adversely affects the operation of the network or violates the rules and protocols for communication across the
|
||||
network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format
|
||||
that is publicly documented (and with an implementation available to the public in source code form), and must require
|
||||
no special password or key for unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of
|
||||
its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were
|
||||
included in this License, to the extent that they are valid under applicable law. If additional permissions apply only
|
||||
to part of the Program, that part may be used separately under those permissions, but the entire Program remains
|
||||
governed by this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or
|
||||
from any part of it. (Additional permissions may be written to require their own removal in certain cases when you
|
||||
modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have
|
||||
or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by
|
||||
the copyright holders of that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the
|
||||
Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with
|
||||
a term that is a further restriction, you may remove that term. If a license document contains a further restriction but
|
||||
permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of
|
||||
that license document, provided that the further restriction does not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a
|
||||
statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as
|
||||
exceptions; the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to
|
||||
propagate or modify it is void, and will automatically terminate your rights under this License (including any patent
|
||||
licenses granted under the third paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your license from a particular copyright holder is
|
||||
reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b)
|
||||
permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days
|
||||
after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you
|
||||
of the violation by some reasonable means, this is the first time you have received notice of violation of this
|
||||
License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the
|
||||
notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the licenses of parties who have received copies or
|
||||
rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not
|
||||
qualify to receive new licenses for the same material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a
|
||||
covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not
|
||||
require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered
|
||||
work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run,
|
||||
modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third
|
||||
parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or
|
||||
subdividing an organization, or merging organizations. If propagation of a covered work results from an entity
|
||||
transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work
|
||||
the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with
|
||||
reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For
|
||||
example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License,
|
||||
and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using,
|
||||
selling, offering for sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the
|
||||
Program is based. The work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already
|
||||
acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or
|
||||
selling its contributor version, but do not include claims that would be infringed only as a consequence of further
|
||||
modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent
|
||||
sublicenses in a manner consistent with the requirements of this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential
|
||||
patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its
|
||||
contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not
|
||||
to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a
|
||||
patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not
|
||||
available for anyone to copy, free of charge and under the terms of this License, through a publicly available network
|
||||
server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available,
|
||||
or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a
|
||||
manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "
|
||||
Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in
|
||||
a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in
|
||||
that country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring
|
||||
conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing
|
||||
them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is
|
||||
automatically extended to all recipients of the covered work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of,
|
||||
or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You
|
||||
may not convey a covered work if you are a party to an arrangement with a third party that is in the business of
|
||||
distributing software, under which you make payment to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a
|
||||
discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from
|
||||
those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered
|
||||
work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to
|
||||
infringement that may otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this
|
||||
License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to
|
||||
satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence
|
||||
you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further
|
||||
conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License
|
||||
would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work
|
||||
licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the
|
||||
resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special
|
||||
requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply
|
||||
to the combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to
|
||||
time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new
|
||||
problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the
|
||||
GNU General Public License "or any later version" applies to it, you have the option of following the terms and
|
||||
conditions either of that numbered version or of any later version published by the Free Software Foundation. If the
|
||||
Program does not specify a version number of the GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used,
|
||||
that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the
|
||||
Program.
|
||||
|
||||
Later license versions may give you additional or different permissions. However, no additional obligations are imposed
|
||||
on any author or copyright holder as a result of your choosing to follow a later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING
|
||||
THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR
|
||||
IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU
|
||||
ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO
|
||||
MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
|
||||
INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO
|
||||
LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
||||
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to
|
||||
their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil
|
||||
liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program
|
||||
in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve
|
||||
this is to make it free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to
|
||||
most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer
|
||||
to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of
|
||||
course, your program's commands might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for
|
||||
the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is
|
||||
a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If
|
||||
this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
||||
@@ -1,3 +1,136 @@
|
||||
# Iris
|
||||
|
||||
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)
|
||||

|
||||
|
||||
Consider supporting development by buying Iris on Spigot.
|
||||
|
||||
## Platforms
|
||||
|
||||
| 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+ |
|
||||
|
||||
Java 25 is required on every platform.
|
||||
|
||||
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.
|
||||
|
||||
## Install
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**Singleplayer (modded clients):** installed Iris packs appear as selectable World Types on the
|
||||
Create New World screen; the integrated server runs the same engine.
|
||||
|
||||
## The client mod
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Quickstart
|
||||
|
||||
Create and enter an Iris world.
|
||||
|
||||
Plugin (optional arguments are keyed):
|
||||
|
||||
```
|
||||
/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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.edit;
|
||||
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
public interface BlockEditor extends Closeable {
|
||||
long last();
|
||||
|
||||
void set(int x, int y, int z, BlockData d);
|
||||
|
||||
BlockData get(int x, int y, int z);
|
||||
|
||||
void setBiome(int x, int z, Biome b);
|
||||
|
||||
void setBiome(int x, int y, int z, Biome b);
|
||||
|
||||
@Override
|
||||
void close();
|
||||
|
||||
Biome getBiome(int x, int y, int z);
|
||||
|
||||
Biome getBiome(int x, int z);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.edit;
|
||||
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
@SuppressWarnings("ClassCanBeRecord")
|
||||
public class BukkitBlockEditor implements BlockEditor {
|
||||
private final World world;
|
||||
|
||||
public BukkitBlockEditor(World world) {
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, BlockData d) {
|
||||
world.getBlockAt(x, y, z).setBlockData(d, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockData get(int x, int y, int z) {
|
||||
return world.getBlockAt(x, y, z).getBlockData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public long last() {
|
||||
return M.ms();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBiome(int x, int z, Biome b) {
|
||||
int minHeight = world.getMinHeight();
|
||||
int maxHeight = world.getMaxHeight();
|
||||
for (int y = minHeight; y < maxHeight; y++) {
|
||||
world.setBiome(x, y, z, b);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBiome(int x, int y, int z, Biome b) {
|
||||
world.setBiome(x, y, z, b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Biome getBiome(int x, int y, int z) {
|
||||
return world.getBiome(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Biome getBiome(int x, int 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();
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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.link;
|
||||
|
||||
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 @NotNull String getIdentifier() {
|
||||
return "iris";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getAuthor() {
|
||||
return "Volmit Software";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getVersion() {
|
||||
return Iris.instance.getDescription().getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean persist() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String onRequest(OfflinePlayer player, String p) {
|
||||
Location l = null;
|
||||
PlatformChunkGenerator a = null;
|
||||
|
||||
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) {
|
||||
return getBiome(a, l).getName();
|
||||
}
|
||||
} else if (p.equalsIgnoreCase("biome_id")) {
|
||||
if (a != null) {
|
||||
return getBiome(a, l).getLoadKey();
|
||||
}
|
||||
} 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 EngineBukkitOps.getRegion(a.getEngine(), l).getName();
|
||||
}
|
||||
} 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 EngineBukkitOps.getRegion(a.getEngine(), l).getLoadFile().getPath();
|
||||
}
|
||||
} else if (p.equalsIgnoreCase("terrain_slope")) {
|
||||
if (a != null) {
|
||||
return (a.getEngine())
|
||||
.getComplex().getSlopeStream()
|
||||
.get(l.getX(), l.getZ()) + "";
|
||||
}
|
||||
} 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) {
|
||||
return a.isStudio() ? "Studio" : "Production";
|
||||
}
|
||||
} else if (p.equalsIgnoreCase("world_seed")) {
|
||||
if (a != null) {
|
||||
return a.getEngine().getSeedManager().getSeed() + "";
|
||||
}
|
||||
} else if (p.equalsIgnoreCase("world_speed")) {
|
||||
if (a != null) {
|
||||
return a.getEngine().getGeneratedPerSecond() + "/s";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private IrisBiome getBiome(PlatformChunkGenerator a, Location l) {
|
||||
return a.getEngine().getBiome(l.getBlockX(), l.getBlockY() - l.getWorld().getMinHeight(), l.getBlockZ());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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.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, 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<>();
|
||||
updateTaskId = J.sr(this::update, 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (updateTaskId != -1) {
|
||||
J.csr(updateTaskId);
|
||||
updateTaskId = -1;
|
||||
}
|
||||
if (editors == null) {
|
||||
return;
|
||||
}
|
||||
flushNow();
|
||||
}
|
||||
|
||||
public BlockData get(World world, int x, int y, int z) {
|
||||
return open(world).get(x, y, z);
|
||||
}
|
||||
|
||||
public void set(World world, int x, int y, int z, BlockData d) {
|
||||
open(world).set(x, y, z, d);
|
||||
}
|
||||
|
||||
public void setBiome(World world, int x, int y, int z, Biome d) {
|
||||
open(world).setBiome(x, y, z, d);
|
||||
}
|
||||
|
||||
public void setBiome(World world, int x, int z, Biome d) {
|
||||
open(world).setBiome(x, z, d);
|
||||
}
|
||||
|
||||
public Biome getBiome(World world, int x, int y, int z) {
|
||||
return open(world).getBiome(x, y, z);
|
||||
}
|
||||
|
||||
public Biome getBiome(World world, int x, int z) {
|
||||
return open(world).getBiome(x, z);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(WorldUnloadEvent e) {
|
||||
if (editors == null) {
|
||||
return;
|
||||
}
|
||||
if (editors.containsKey(e.getWorld()) && !deletingWorld) {
|
||||
editors.remove(e.getWorld()).close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void update() {
|
||||
if (editors == null) {
|
||||
return;
|
||||
}
|
||||
for (World i : editors.k()) {
|
||||
if (M.ms() - editors.get(i).last() > 1000) {
|
||||
editors.remove(i).close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void flushNow() {
|
||||
if (editors == null) {
|
||||
return;
|
||||
}
|
||||
for (World i : editors.k()) {
|
||||
editors.remove(i).close();
|
||||
}
|
||||
}
|
||||
|
||||
public BlockEditor open(World world) {
|
||||
if (editors == null) {
|
||||
editors = new KMap<>();
|
||||
}
|
||||
if (editors.containsKey(world)) {
|
||||
return editors.get(world);
|
||||
}
|
||||
|
||||
BlockEditor e = new BukkitBlockEditor(world);
|
||||
editors.put(world, e);
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,545 @@
|
||||
/*
|
||||
* 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 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.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;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.util.BlockVector;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Iris Object from the 2 coordinates selected with a wand
|
||||
*
|
||||
* @param p The wand player
|
||||
* @return The new object
|
||||
*/
|
||||
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());
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
@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) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Iris Object from the 2 coordinates selected with a wand
|
||||
*
|
||||
* @return The new object
|
||||
*/
|
||||
public static Matter createMatterSchem(Player p) {
|
||||
if (!isHoldingWand(p)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Location[] f = getCuboid(p);
|
||||
|
||||
return WorldMatter.createMatter(p.getName(), f[0], f[1]);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a user friendly location string to an actual Location
|
||||
*
|
||||
* @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");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user friendly string of a location
|
||||
*
|
||||
* @param loc The location
|
||||
* @return The string
|
||||
*/
|
||||
public static String locationToString(Location loc) {
|
||||
if (loc == null) {
|
||||
return "<#>";
|
||||
}
|
||||
|
||||
return loc.getBlockX() + "," + loc.getBlockY() + "," + loc.getBlockZ() + " in " + WorldIdentity.serialize(loc.getWorld());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new blank Iris wand
|
||||
*
|
||||
* @return The wand itemstack
|
||||
*/
|
||||
public static ItemStack createWand() {
|
||||
return createWand(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new dust itemstack
|
||||
*
|
||||
* @return The stack
|
||||
*/
|
||||
public static ItemStack createDust() {
|
||||
ItemStack is = new ItemStack(Material.GLOWSTONE_DUST);
|
||||
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.values());
|
||||
im.setLore(new KList<String>().qadd("Right click on a block to reveal it's placement structure!"));
|
||||
is.setItemMeta(im);
|
||||
|
||||
return is;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an existing wand in a users inventory
|
||||
*
|
||||
* @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) {
|
||||
ItemStack wand = createWand(); //Create blank wand
|
||||
ItemMeta meta = wand.getItemMeta();
|
||||
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++) {
|
||||
ItemStack stack = inventory.getItem(s);
|
||||
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
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Iris wand. The locations should be the currently selected locations, or null
|
||||
*
|
||||
* @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.FIRE_ASPECT, 1);
|
||||
ItemMeta im = is.getItemMeta();
|
||||
im.setDisplayName(C.BOLD + "" + C.GOLD + "Wand of Iris");
|
||||
im.setUnbreakable(true);
|
||||
im.addItemFlags(ItemFlag.values());
|
||||
im.setLore(new KList<String>().add(locationToString(a), locationToString(b)));
|
||||
is.setItemMeta(im);
|
||||
|
||||
return is;
|
||||
}
|
||||
|
||||
public static Location[] getCuboidFromItem(ItemStack is) {
|
||||
ItemMeta im = is.getItemMeta();
|
||||
return new Location[]{stringToLocation(im.getLore().get(0)), stringToLocation(im.getLore().get(1))};
|
||||
}
|
||||
|
||||
public static Location[] getCuboid(Player p) {
|
||||
if (isHoldingIrisWand(p)) {
|
||||
return getCuboidFromItem(p.getInventory().getItemInMainHand());
|
||||
}
|
||||
|
||||
if (IrisSettings.get().getWorld().worldEditWandCUI) {
|
||||
Cuboid c = WorldEditLink.getSelection(p);
|
||||
|
||||
if (c != null) {
|
||||
return new Location[]{c.getLowerNE(), c.getUpperSW()};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isHoldingWand(Player p) {
|
||||
return isHoldingIrisWand(p) || (IrisSettings.get().getWorld().worldEditWandCUI && WorldEditLink.getSelection(p) != null);
|
||||
}
|
||||
|
||||
public static boolean isHoldingIrisWand(Player p) {
|
||||
ItemStack is = p.getInventory().getItemInMainHand();
|
||||
return is != null && isWand(is);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the itemstack passed an Iris wand
|
||||
*
|
||||
* @param is The itemstack
|
||||
* @return True if it is
|
||||
*/
|
||||
public static boolean isWand(ItemStack is) {
|
||||
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());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
wand = createWand();
|
||||
dust = createDust();
|
||||
|
||||
J.ar(() -> {
|
||||
for (Player i : Bukkit.getOnlinePlayers()) {
|
||||
tick(i);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
|
||||
}
|
||||
|
||||
public void tick(Player p) {
|
||||
try {
|
||||
try {
|
||||
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) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the outline of a selected region
|
||||
*
|
||||
* @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 the outline of a selected region
|
||||
*
|
||||
* @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(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(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())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (d[0].distanceSquared(d[1]) > 64 * 64) {
|
||||
return;
|
||||
}
|
||||
|
||||
int minx = Math.min(d[0].getBlockX(), d[1].getBlockX());
|
||||
int miny = Math.min(d[0].getBlockY(), d[1].getBlockY());
|
||||
int minz = Math.min(d[0].getBlockZ(), d[1].getBlockZ());
|
||||
int maxx = Math.max(d[0].getBlockX(), d[1].getBlockX());
|
||||
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)) {
|
||||
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)) {
|
||||
Vector push = new Vector(0, 0, 0);
|
||||
|
||||
if (j == minx) {
|
||||
push.add(new Vector(-0.55, 0, 0));
|
||||
}
|
||||
|
||||
if (k == miny) {
|
||||
push.add(new Vector(0, -0.55, 0));
|
||||
}
|
||||
|
||||
if (l == minz) {
|
||||
push.add(new Vector(0, 0, -0.55));
|
||||
}
|
||||
|
||||
if (j == maxx) {
|
||||
push.add(new Vector(0.55, 0, 0));
|
||||
}
|
||||
|
||||
if (k == maxy) {
|
||||
push.add(new Vector(0, 0.55, 0));
|
||||
}
|
||||
|
||||
if (l == maxz) {
|
||||
push.add(new Vector(0, 0, 0.55));
|
||||
}
|
||||
|
||||
Location lv = new Location(d[0].getWorld(), j, k, l).clone().add(0.5, 0.5, 0.5).clone().add(push);
|
||||
Color color = Color.getHSBColor((float) (0.5f + (Math.sin((j + k + l + (p.getTicksLived() / 2f)) / 20f) / 2)), 1, 1);
|
||||
int r = color.getRed();
|
||||
int g = color.getGreen();
|
||||
int b = color.getBlue();
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(PlayerInteractEvent e) {
|
||||
if (e.getHand() != EquipmentSlot.HAND)
|
||||
return;
|
||||
try {
|
||||
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)) {
|
||||
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);
|
||||
e.getPlayer().updateInventory();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
Iris.reportError(xx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the player holding Dust?
|
||||
*
|
||||
* @param p The player
|
||||
* @return True if they are
|
||||
*/
|
||||
public boolean isHoldingDust(Player p) {
|
||||
ItemStack is = p.getInventory().getItemInMainHand();
|
||||
return is != null && isDust(is);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the itemstack passed Iris dust?
|
||||
*
|
||||
* @param is The itemstack
|
||||
* @return True if it is
|
||||
*/
|
||||
public boolean isDust(ItemStack is) {
|
||||
return is.isSimilar(dust);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the location on an Iris 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)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
Location[] f = getCuboidFromItem(item);
|
||||
Location other = left ? f[1] : f[0];
|
||||
|
||||
if (other != null && !WorldIdentity.key(other.getWorld()).equals(WorldIdentity.key(a.getWorld()))) {
|
||||
other = null;
|
||||
}
|
||||
|
||||
return createWand(left ? a : other, left ? other : a);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user