mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-05-20 16:40:22 +00:00
Compare commits
1545 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -39,6 +39,10 @@ body:
|
||||
- 1.17
|
||||
- 1.17.1
|
||||
- 1.18
|
||||
- 1.19
|
||||
- 1.20
|
||||
- 1.21
|
||||
- 1.22
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
|
||||
build/
|
||||
libs/
|
||||
|
||||
.gradle/
|
||||
|
||||
.idea/
|
||||
|
||||
.DS_Store
|
||||
|
||||
collection/
|
||||
|
||||
/core/src/main/java/art/arcane/iris/util/uniques/
|
||||
|
||||
DataPackExamples/
|
||||
|
||||
@@ -7,36 +7,35 @@ The master branch is for the latest version of minecraft.
|
||||
# Building
|
||||
|
||||
Building Iris is fairly simple, though you will need to setup a few things if your system has never been used for java
|
||||
development.
|
||||
development.[README.md](README.md)
|
||||
|
||||
Consider supporting our development by buying Iris on spigot! We work hard to make Iris the best it can be for everyone.
|
||||
|
||||
## Preface: if you need help compiling and you are a developer / intend to help out in the community or with development we would love to help you regardless in the discord! however do not come to the discord asking for free copies, or a tutorial on how to compile.
|
||||
|
||||
### Command Line Builds
|
||||
|
||||
1. Install [Java JDK 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
|
||||
1. Install [Java JDK 25](https://adoptium.net/temurin/releases/?version=25)
|
||||
2. Set the JDK installation path to `JAVA_HOME` as an environment variable.
|
||||
* Windows
|
||||
1. Start > Type `env` and press Enter
|
||||
2. Advanced > Environment Variables
|
||||
3. Under System Variables, click `New...`
|
||||
4. Variable Name: `JAVA_HOME`
|
||||
5. Variable Value: `C:\Program Files\Java\jdk-17.0.1` (verify this exists after installing java don't just copy
|
||||
5. Variable Value: `C:\Program Files\Java\jdk-25` (verify this exists after installing java don't just copy
|
||||
the example text)
|
||||
* MacOS
|
||||
1. Run `/usr/libexec/java_home -V` and look for Java 17
|
||||
1. Run `/usr/libexec/java_home -V` and look for Java 25
|
||||
2. Run `sudo nano ~/.zshenv`
|
||||
3. Add `export JAVA_HOME=$(/usr/libexec/java_home)` as a new line
|
||||
4. Use `CTRL + X`, then Press `Y`, Then `ENTER`
|
||||
5. Quit & Reopen Terminal and verify with `echo $JAVA_HOME`. It should print a directory
|
||||
3. If this is your first time building Iris for MC 1.18+ run `gradlew setup` inside the root Iris project folder.
|
||||
Otherwise, skip this step. Grab a coffee, this may take up to 5 minutes depending on your cpu & internet connection.
|
||||
4. Once the project has setup, run `gradlew iris`
|
||||
5. The Iris jar will be placed in `Iris/build/Iris-XXX-XXX.jar` Enjoy! Consider supporting us by buying it on spigot!
|
||||
3. Once the project has setup, run `gradlew iris`
|
||||
4. The Iris jar will be placed in `Iris/build/Iris-XXX-XXX.jar` Enjoy! Consider supporting us by buying it on spigot!
|
||||
|
||||
### IDE Builds (for development)
|
||||
|
||||
* Run `gradlew setup` any time you get dependency issues with craftbukkit
|
||||
* Configure ITJ Gradle to use JDK 17 (in settings, search for gradle)
|
||||
* Configure ITJ Gradle to use JDK 21 (in settings, search for gradle)
|
||||
* Add a build line in the build.gradle for your own build task to directly compile Iris into your plugins folder if you
|
||||
prefer.
|
||||
* Resync the project & run your newly created task (under the development folder in gradle tasks!)
|
||||
@@ -46,7 +45,7 @@ Consider supporting our development by buying Iris on spigot! We work hard to ma
|
||||
Everyone needs a tool-belt.
|
||||
|
||||
```java
|
||||
package com.volmit.iris.core.tools;
|
||||
package art.arcane.iris.core.tools;
|
||||
|
||||
// Get IrisDataManager from a world
|
||||
IrisToolbelt.access(anyWorld).getCompound().getData();
|
||||
@@ -67,7 +66,6 @@ IrisAccess access=IrisToolbelt.createWorld() // If you like builders...
|
||||
.name("myWorld") // The world name
|
||||
.dimension("terrifyinghands")
|
||||
.seed(69133742) // The world seed
|
||||
.headless(true) // Headless make gen go fast
|
||||
.pregen(PregenTask // Define a pregen job to run
|
||||
.builder()
|
||||
.center(new Position2(0,0)) // REGION coords (1 region = 32x32 chunks)
|
||||
|
||||
+206
-252
@@ -1,3 +1,10 @@
|
||||
import de.undercouch.gradle.tasks.download.Download
|
||||
import org.gradle.api.plugins.JavaPlugin
|
||||
import org.gradle.api.tasks.Copy
|
||||
import org.gradle.api.tasks.compile.JavaCompile
|
||||
import org.gradle.jvm.tasks.Jar
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2021 Arcane Arts (Volmit Software)
|
||||
@@ -16,301 +23,248 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
maven {
|
||||
url = uri('https://jitpack.io')
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath('com.github.VolmitSoftware:NMSTools:c88961416f')
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'java-library'
|
||||
id "io.freefair.lombok" version "6.3.0"
|
||||
id "com.github.johnrengelman.shadow" version "7.1.2"
|
||||
id "de.undercouch.download" version "5.0.1"
|
||||
alias(libs.plugins.download)
|
||||
}
|
||||
|
||||
version '2.0.4-1.18.2' // Needs to be version specific
|
||||
def nmsVersion = "1.18.2"
|
||||
def apiVersion = '1.18'
|
||||
def spigotJarVersion = '1.18.2-R0.1-SNAPSHOT'
|
||||
def name = getRootProject().getName() // Defined in settings.gradle
|
||||
def main = 'com.volmit.iris.Iris'
|
||||
group = 'art.arcane'
|
||||
version = '4.0.0-26.1'
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:master-SNAPSHOT')
|
||||
.get()
|
||||
|
||||
apply plugin: ApiGenerator
|
||||
|
||||
// ADD YOURSELF AS A NEW LINE IF YOU WANT YOUR OWN BUILD TASK GENERATED
|
||||
// ======================== WINDOWS =============================
|
||||
registerCustomOutputTask('Cyberpwn', 'C://Users/cyberpwn/Documents/development/server/plugins')
|
||||
registerCustomOutputTask('Psycho', 'D://Dan/MinecraftDevelopment/server/plugins')
|
||||
registerCustomOutputTask('Psycho', 'C://Dan/MinecraftDevelopment/Server/plugins')
|
||||
registerCustomOutputTask('ArcaneArts', 'C://Users/arcane/Documents/development/server/plugins')
|
||||
registerCustomOutputTask('Coco', 'D://Documents/MC/plugins')
|
||||
registerCustomOutputTask('Coco', 'D://mcsm/plugins')
|
||||
registerCustomOutputTask('Strange', 'D://Servers/1.17 Test Server/plugins')
|
||||
registerCustomOutputTask('Vatuu', 'D://Minecraft/Servers/1.18.2/plugins')
|
||||
registerCustomOutputTask('Vatuu', 'D://Minecraft/Servers/1.19.4/plugins')
|
||||
registerCustomOutputTask('CrazyDev22', 'C://Users/Julian/Desktop/server/plugins')
|
||||
registerCustomOutputTask('PixelFury', 'C://Users/repix/workplace/Iris/1.21.3 - Development-Public-v3/plugins')
|
||||
registerCustomOutputTask('PixelFuryDev', 'C://Users/repix/workplace/Iris/1.21 - Development-v3/plugins')
|
||||
// ========================== UNIX ==============================
|
||||
registerCustomOutputTaskUnix('CyberpwnLT', '/Users/danielmills/Documents/development/server/plugins')
|
||||
registerCustomOutputTaskUnix('PsychoLT', '/Users/brianfopiano/Desktop/REMOTES/RemoteMinecraft/plugins')
|
||||
registerCustomOutputTaskUnix('CyberpwnLT', '/Users/danielmills/development/server/plugins')
|
||||
registerCustomOutputTaskUnix('PsychoLT', '/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/plugin-consumers/dropins/plugins')
|
||||
registerCustomOutputTaskUnix('PixelMac', '/Users/test/Desktop/mcserver/plugins')
|
||||
registerCustomOutputTaskUnix('CrazyDev22LT', '/home/julian/Desktop/server/plugins')
|
||||
// ==============================================================
|
||||
|
||||
/**
|
||||
* Gradle is weird sometimes, we need to delete the plugin yml from the build folder to actually filter properly.
|
||||
*/
|
||||
file(jar.archiveFile.get().getAsFile().getParentFile().getParentFile().getParentFile().getAbsolutePath() + '/build/resources/main/plugin.yml').delete()
|
||||
def nmsBindings = [
|
||||
v1_21_R7: '1.21.11-R0.1-SNAPSHOT',
|
||||
]
|
||||
Class nmsTypeClass = Class.forName('NMSBinding$Type')
|
||||
nmsBindings.each { key, value ->
|
||||
project(":nms:${key}") {
|
||||
apply plugin: JavaPlugin
|
||||
|
||||
/**
|
||||
* Expand properties into plugin yml
|
||||
*/
|
||||
processResources {
|
||||
filesMatching('**/plugin.yml') {
|
||||
expand(
|
||||
'name': name.toString(),
|
||||
'version': version.toString(),
|
||||
'main': main.toString(),
|
||||
'apiversion': apiVersion.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
def nmsConfig = new Config()
|
||||
nmsConfig.jvm = 25
|
||||
nmsConfig.version = value
|
||||
nmsConfig.type = Enum.valueOf(nmsTypeClass, 'USER_DEV')
|
||||
extensions.extraProperties.set('nms', nmsConfig)
|
||||
plugins.apply(NMSBinding)
|
||||
|
||||
/**
|
||||
* Unified repo
|
||||
*/
|
||||
repositories {
|
||||
mavenLocal {
|
||||
content {
|
||||
includeGroup("org.bukkit")
|
||||
includeGroup("org.spigotmc")
|
||||
dependencies {
|
||||
compileOnly(project(':core'))
|
||||
compileOnly(volmLibCoordinate) {
|
||||
changing = true
|
||||
transitive = false
|
||||
}
|
||||
compileOnly(rootProject.libs.annotations)
|
||||
compileOnly(rootProject.libs.byteBuddy.core)
|
||||
}
|
||||
}
|
||||
maven { url "https://dl.cloudsmith.io/public/arcane/archive/maven/" }
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
/**
|
||||
* We need parameter meta for the decree command system
|
||||
*/
|
||||
compileJava {
|
||||
options.compilerArgs << '-parameters'
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Iris for shading
|
||||
*/
|
||||
shadowJar {
|
||||
//minimize()
|
||||
append("plugin.yml")
|
||||
relocate 'com.dfsek.paralithic', 'com.volmit.iris.util.paralithic'
|
||||
relocate 'io.papermc.lib', 'com.volmit.iris.util.paper'
|
||||
relocate 'net.kyori', 'com.volmit.iris.util.kyori'
|
||||
dependencies {
|
||||
include(dependency('io.papermc:paperlib'))
|
||||
include(dependency('com.dfsek:Paralithic'))
|
||||
include(dependency('net.kyori:'))
|
||||
}
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy.cacheChangingModulesFor 60, 'minutes'
|
||||
resolutionStrategy.cacheDynamicVersionsFor 60, 'minutes'
|
||||
}
|
||||
|
||||
/**
|
||||
* Dependencies.
|
||||
*
|
||||
* Provided or classpath dependencies are not shaded and are available on the runtime classpath
|
||||
*
|
||||
* Shaded dependencies are not available at runtime, nor are they available on mvn central so they
|
||||
* need to be shaded into the jar (increasing binary size)
|
||||
*
|
||||
* Dynamically loaded dependencies are defined in the plugin.yml (updating these must be updated in the
|
||||
* plugin.yml also, otherwise they wont be available). These do not increase binary size). Only declare
|
||||
* these dependencies if they are available on mvn central.
|
||||
*/
|
||||
def included = configurations.create('included')
|
||||
def jarJar = configurations.create('jarJar')
|
||||
dependencies {
|
||||
// Provided or Classpath
|
||||
compileOnly 'org.projectlombok:lombok:1.18.22'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.22'
|
||||
implementation 'org.spigotmc:spigot-api:1.18.2-R0.1-SNAPSHOT'
|
||||
implementation 'me.clip:placeholderapi:2.11.1'
|
||||
implementation 'io.th0rgal:oraxen:1.94.0'
|
||||
implementation 'org.bukkit:craftbukkit:1.18.2-R0.1-SNAPSHOT:remapped-mojang'
|
||||
|
||||
// Shaded
|
||||
implementation 'com.dfsek:Paralithic:0.4.0'
|
||||
implementation 'io.papermc:paperlib:1.0.5'
|
||||
implementation "net.kyori:adventure-text-minimessage:4.2.0-SNAPSHOT"
|
||||
implementation 'net.kyori:adventure-platform-bukkit:4.0.1' // Dont upgrade yet
|
||||
implementation 'net.kyori:adventure-api:4.9.3' // Dont upgrade yet
|
||||
|
||||
// Dynamically Loaded
|
||||
implementation 'io.timeandspace:smoothie-map:2.0.2'
|
||||
implementation 'it.unimi.dsi:fastutil:8.5.8'
|
||||
implementation 'com.googlecode.concurrentlinkedhashmap:concurrentlinkedhashmap-lru:1.4.2'
|
||||
implementation 'org.zeroturnaround:zt-zip:1.14'
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation 'org.ow2.asm:asm:9.2'
|
||||
implementation 'com.google.guava:guava:31.1-jre'
|
||||
implementation 'bsf:bsf:2.4.0'
|
||||
implementation 'rhino:js:1.7R2'
|
||||
implementation 'com.github.ben-manes.caffeine:caffeine:3.0.6'
|
||||
implementation 'org.apache.commons:commons-lang3:3.12.0'
|
||||
nmsBindings.keySet().each { key ->
|
||||
add('included', project(path: ":nms:${key}", configuration: 'runtimeElements'))
|
||||
}
|
||||
add('included', project(path: ':core', configuration: 'shadow'))
|
||||
add('jarJar', project(':core:agent'))
|
||||
}
|
||||
|
||||
if (JavaVersion.current().toString() != "17") {
|
||||
System.err.println()
|
||||
System.err.println("=========================================================================================================")
|
||||
System.err.println("You must run gradle on Java 17. You are using " + JavaVersion.current())
|
||||
System.err.println()
|
||||
System.err.println("=== For IDEs ===")
|
||||
System.err.println("1. Configure the project for Java 17")
|
||||
System.err.println("2. Configure the bundled gradle to use Java 17 in settings")
|
||||
System.err.println()
|
||||
System.err.println("=== For Command Line (gradlew) ===")
|
||||
System.err.println("1. Install JDK 17 from https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html")
|
||||
System.err.println("2. Set JAVA_HOME environment variable to the new jdk installation folder such as C:\\Program Files\\Java\\jdk-17.0.1")
|
||||
System.err.println("3. Open a new command prompt window to get the new environment variables if need be.")
|
||||
System.err.println("=========================================================================================================")
|
||||
System.err.println()
|
||||
System.exit(69);
|
||||
tasks.named('jar', Jar).configure {
|
||||
inputs.files(included)
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
from(jarJar, provider { included.resolve().collect { zipTree(it) } })
|
||||
archiveFileName.set("Iris-${project.version}.jar")
|
||||
}
|
||||
|
||||
def buildToolsJar = new File(buildDir, "buildtools/BuildTools.jar");
|
||||
def specialSourceJar = new File(buildDir, "specialsource/SpecialSource.jar");
|
||||
def buildToolsFolder = new File(buildDir, "buildtools");
|
||||
def specialSourceFolder = new File(buildDir, "specialsource");
|
||||
def buildToolsHint = new File(buildDir, "buildtools/craftbukkit-" + nmsVersion + ".jar");
|
||||
def outputShadeJar = new File(buildDir, "libs/Iris-" + version + "-all.jar");
|
||||
def ssiJar = new File(buildDir, "specialsource/Iris-" + version + "-all.jar");
|
||||
def ssobfJar = new File(buildDir, "specialsource/Iris-" + version + "-rmo.jar");
|
||||
def ssJar = new File(buildDir, "specialsource/Iris-" + version + "-rma.jar");
|
||||
def homePath = System.properties['user.home']
|
||||
def m2 = new File(homePath + "/.m2/repository")
|
||||
def m2s = m2.getAbsolutePath();
|
||||
tasks.register('iris', Copy) {
|
||||
group = 'iris'
|
||||
dependsOn('jar')
|
||||
from(layout.buildDirectory.file("libs/Iris-${project.version}.jar"))
|
||||
into(layout.buildDirectory)
|
||||
}
|
||||
|
||||
// ======================== Building Mapped Jars =============================
|
||||
task downloadBuildtools(type: Download) {
|
||||
group "remapping"
|
||||
src 'https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar'
|
||||
dest buildToolsJar
|
||||
onlyIf {
|
||||
!buildToolsJar.exists()
|
||||
tasks.register('irisDev', Copy) {
|
||||
group = 'iris'
|
||||
from(project(':core').layout.buildDirectory.files('libs/core-javadoc.jar', 'libs/core-sources.jar'))
|
||||
rename { String fileName -> fileName.replace('core', "Iris-${project.version}") }
|
||||
into(layout.buildDirectory)
|
||||
dependsOn(':core:sourcesJar')
|
||||
dependsOn(':core:javadocJar')
|
||||
}
|
||||
|
||||
def cli = file('sentry-cli.exe')
|
||||
tasks.register('downloadCli', Download) {
|
||||
group = 'io.sentry'
|
||||
src("https://release-registry.services.sentry.io/apps/sentry-cli/latest?response=download&arch=x86_64&platform=${System.getProperty('os.name')}&package=sentry-cli")
|
||||
dest(cli)
|
||||
|
||||
doLast {
|
||||
cli.setExecutable(true)
|
||||
}
|
||||
}
|
||||
|
||||
task downloadSpecialSource(type: Download) {
|
||||
group "remapping"
|
||||
src 'https://repo.maven.apache.org/maven2/net/md-5/SpecialSource/1.10.0/SpecialSource-1.10.0-shaded.jar'
|
||||
dest specialSourceJar
|
||||
onlyIf {
|
||||
!specialSourceJar.exists()
|
||||
tasks.register('release') {
|
||||
group = 'io.sentry'
|
||||
dependsOn('downloadCli')
|
||||
doLast {
|
||||
String url = 'http://sentry.volmit.com:8080'
|
||||
def authToken = project.findProperty('sentry.auth.token') ?: System.getenv('SENTRY_AUTH_TOKEN')
|
||||
String org = 'sentry'
|
||||
String projectName = 'iris'
|
||||
runCommand(cli, '--url', url, '--auth-token', authToken, 'releases', 'new', '-o', org, '-p', projectName, version)
|
||||
runCommand(cli, '--url', url, '--auth-token', authToken, 'releases', 'set-commits', '-o', org, '-p', projectName, version, '--auto', '--ignore-missing')
|
||||
//exec(cli, "--url", url, "--auth-token", authToken, "releases", "finalize", "-o", org, "-p", projectName, version)
|
||||
cli.delete()
|
||||
}
|
||||
}
|
||||
|
||||
task executeBuildTools(dependsOn: downloadBuildtools, type: JavaExec)
|
||||
{
|
||||
group "remapping"
|
||||
classpath = files(buildToolsJar)
|
||||
workingDir = buildToolsFolder
|
||||
args = [
|
||||
"--rev",
|
||||
nmsVersion,
|
||||
"--compile",
|
||||
"craftbukkit",
|
||||
"--remap"
|
||||
]
|
||||
onlyIf {
|
||||
!buildToolsHint.exists()
|
||||
}
|
||||
void runCommand(Object... command) {
|
||||
Process process = new ProcessBuilder(command.collect { it.toString() }).start()
|
||||
process.inputStream.readLines().each { println(it) }
|
||||
process.errorStream.readLines().each { println(it) }
|
||||
process.waitFor()
|
||||
}
|
||||
|
||||
task copyBuildToSpecialSource(type: Copy)
|
||||
{
|
||||
group "remapping"
|
||||
from outputShadeJar
|
||||
into specialSourceFolder
|
||||
dependsOn(downloadSpecialSource, shadowJar)
|
||||
configurations.configureEach {
|
||||
resolutionStrategy.cacheChangingModulesFor(0, 'seconds')
|
||||
resolutionStrategy.cacheDynamicVersionsFor(0, 'seconds')
|
||||
}
|
||||
|
||||
task specialSourceRemapObfuscate(type: JavaExec)
|
||||
{
|
||||
group "remapping"
|
||||
dependsOn(copyBuildToSpecialSource, downloadSpecialSource, shadowJar)
|
||||
workingDir = specialSourceFolder
|
||||
classpath = files(specialSourceJar,
|
||||
new File(m2s + "/org/spigotmc/spigot/" + spigotJarVersion + "/spigot-" + spigotJarVersion + "-remapped-mojang.jar"))
|
||||
mainClass = "net.md_5.specialsource.SpecialSource"
|
||||
args = [
|
||||
"--live",
|
||||
"-i",
|
||||
ssiJar.getName(),
|
||||
"-o",
|
||||
ssobfJar.getName(),
|
||||
"-m",
|
||||
m2s + "/org/spigotmc/minecraft-server/" + spigotJarVersion + "/minecraft-server-" + spigotJarVersion + "-maps-mojang.txt",
|
||||
"--reverse",
|
||||
]
|
||||
}
|
||||
allprojects {
|
||||
apply plugin: 'java'
|
||||
|
||||
task specialSourceRemap(type: JavaExec)
|
||||
{
|
||||
group "remapping"
|
||||
dependsOn(specialSourceRemapObfuscate)
|
||||
workingDir = specialSourceFolder
|
||||
classpath = files(specialSourceJar,
|
||||
new File(m2s + "/org/spigotmc/spigot/" + spigotJarVersion + "/spigot-" + spigotJarVersion + "-remapped-obf.jar"))
|
||||
mainClass = "net.md_5.specialsource.SpecialSource"
|
||||
args = [
|
||||
"--live",
|
||||
"-i",
|
||||
ssobfJar.getName(),
|
||||
"-o",
|
||||
ssJar.getName(),
|
||||
"-m",
|
||||
m2s + "/org/spigotmc/minecraft-server/" + spigotJarVersion + "/minecraft-server-" + spigotJarVersion + "-maps-spigot.csrg"
|
||||
]
|
||||
}
|
||||
|
||||
tasks.compileJava.dependsOn(executeBuildTools)
|
||||
|
||||
task setup()
|
||||
{
|
||||
group("iris")
|
||||
dependsOn(clean, executeBuildTools)
|
||||
}
|
||||
|
||||
task iris(type: Copy)
|
||||
{
|
||||
group "iris"
|
||||
from ssJar
|
||||
into buildDir
|
||||
rename { String fileName ->
|
||||
fileName.replace('Iris-' + version + '-rma.jar', "Iris-" + version + ".jar")
|
||||
}
|
||||
dependsOn(specialSourceRemap)
|
||||
}
|
||||
|
||||
def registerCustomOutputTask(name, path) {
|
||||
if (!System.properties['os.name'].toLowerCase().contains('windows')) {
|
||||
return;
|
||||
}
|
||||
|
||||
tasks.register('build' + name, Copy) {
|
||||
group('development')
|
||||
outputs.upToDateWhen { false }
|
||||
dependsOn(iris)
|
||||
from(new File(buildDir, "Iris-" + version + ".jar"))
|
||||
into(file(path))
|
||||
rename { String fileName ->
|
||||
fileName.replace("Iris-" + version + ".jar", "Iris.jar")
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def registerCustomOutputTaskUnix(name, path) {
|
||||
if (System.properties['os.name'].toLowerCase().contains('windows')) {
|
||||
return;
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = uri('https://repo.papermc.io/repository/maven-public/') }
|
||||
maven { url = uri('https://repo.codemc.org/repository/maven-public/') }
|
||||
|
||||
maven { url = uri('https://jitpack.io') } // EcoItems, score
|
||||
maven { url = uri('https://repo.nexomc.com/releases/') } // nexo
|
||||
maven { url = uri('https://maven.devs.beer/') } // itemsadder
|
||||
maven { url = uri('https://repo.extendedclip.com/releases/') } // placeholderapi
|
||||
maven { url = uri('https://mvn.lumine.io/repository/maven-public/') } // mythic
|
||||
maven { url = uri('https://nexus.phoenixdevt.fr/repository/maven-public/') } //MMOItems
|
||||
maven { url = uri('https://repo.onarandombox.com/content/groups/public/') } //Multiverse Core
|
||||
maven { url = uri('https://repo.momirealms.net/releases/') } // CraftEngine
|
||||
}
|
||||
|
||||
tasks.register('build' + name, Copy) {
|
||||
group('development')
|
||||
dependencies {
|
||||
// Provided or Classpath
|
||||
compileOnly(rootProject.libs.lombok)
|
||||
annotationProcessor(rootProject.libs.lombok)
|
||||
}
|
||||
|
||||
/**
|
||||
* We need parameter meta for the decree command system
|
||||
*/
|
||||
tasks.named('compileJava', JavaCompile).configure {
|
||||
options.compilerArgs.add('-parameters')
|
||||
options.encoding = 'UTF-8'
|
||||
options.debugOptions.debugLevel = 'none'
|
||||
options.release.set(25)
|
||||
}
|
||||
|
||||
tasks.named('javadoc').configure {
|
||||
options.encoding = 'UTF-8'
|
||||
options.quiet()
|
||||
//options.addStringOption("Xdoclint:none") // TODO: Re-enable this
|
||||
}
|
||||
|
||||
tasks.register('sourcesJar', Jar) {
|
||||
archiveClassifier.set('sources')
|
||||
from(sourceSets.main.allSource)
|
||||
}
|
||||
|
||||
tasks.register('javadocJar', Jar) {
|
||||
archiveClassifier.set('javadoc')
|
||||
from(tasks.named('javadoc').map { it.destinationDir })
|
||||
}
|
||||
}
|
||||
|
||||
if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_25)) {
|
||||
System.err.println()
|
||||
System.err.println('=========================================================================================================')
|
||||
System.err.println('You must run gradle on Java 25 or newer. You are using ' + JavaVersion.current())
|
||||
System.err.println()
|
||||
System.err.println('=== For IDEs ===')
|
||||
System.err.println('1. Configure the project for Java 25 toolchain')
|
||||
System.err.println('2. Configure the bundled gradle to use Java 25+ in settings')
|
||||
System.err.println()
|
||||
System.err.println('=== For Command Line (gradlew) ===')
|
||||
System.err.println('1. Install JDK 25 from https://adoptium.net/temurin/releases/?version=25')
|
||||
System.err.println('2. Set JAVA_HOME environment variable to the new jdk installation folder such as C:\\Program Files\\Java\\jdk-25')
|
||||
System.err.println('3. Open a new command prompt window to get the new environment variables if need be.')
|
||||
System.err.println('=========================================================================================================')
|
||||
System.err.println()
|
||||
System.exit(69)
|
||||
}
|
||||
|
||||
void registerCustomOutputTask(String name, String path) {
|
||||
if (!System.getProperty('os.name').toLowerCase().contains('windows')) {
|
||||
return
|
||||
}
|
||||
|
||||
tasks.register("build${name}", Copy) {
|
||||
group = 'development'
|
||||
outputs.upToDateWhen { false }
|
||||
dependsOn(iris)
|
||||
from(new File(buildDir, "Iris-" + version + ".jar"))
|
||||
dependsOn('iris')
|
||||
from(layout.buildDirectory.file("Iris-${project.version}.jar"))
|
||||
into(file(path))
|
||||
rename { String fileName ->
|
||||
fileName.replace("Iris-" + version + ".jar", "Iris.jar")
|
||||
}
|
||||
rename { String ignored -> 'Iris.jar' }
|
||||
}
|
||||
}
|
||||
|
||||
void registerCustomOutputTaskUnix(String name, String path) {
|
||||
if (System.getProperty('os.name').toLowerCase().contains('windows')) {
|
||||
return
|
||||
}
|
||||
|
||||
tasks.register("build${name}", Copy) {
|
||||
group = 'development'
|
||||
outputs.upToDateWhen { false }
|
||||
dependsOn('iris')
|
||||
from(layout.buildDirectory.file("Iris-${project.version}.jar"))
|
||||
into(file(path))
|
||||
rename { String ignored -> 'Iris.jar' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven {
|
||||
url = uri('https://jitpack.io')
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation('org.ow2.asm:asm:9.8')
|
||||
implementation('com.github.VolmitSoftware:NMSTools:c88961416f')
|
||||
implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.18')
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.publish.PublishingExtension;
|
||||
import org.gradle.api.publish.maven.MavenPublication;
|
||||
import org.gradle.api.tasks.InputFile;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
import org.gradle.jvm.tasks.Jar;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.JarOutputStream;
|
||||
|
||||
public class ApiGenerator implements Plugin<Project> {
|
||||
@Override
|
||||
public void apply(Project target) {
|
||||
target.getPlugins().apply("maven-publish");
|
||||
TaskProvider<GenerateApiTask> task = target.getTasks().register("irisApi", GenerateApiTask.class);
|
||||
|
||||
PublishingExtension publishing = target.getExtensions().findByType(PublishingExtension.class);
|
||||
if (publishing == null) {
|
||||
throw new GradleException("Publishing extension not found");
|
||||
}
|
||||
|
||||
publishing.getRepositories().maven(repository -> {
|
||||
repository.setName("deployDir");
|
||||
repository.setUrl(targetDirectory(target).toURI());
|
||||
});
|
||||
|
||||
publishing.getPublications().create("maven", MavenPublication.class, publication -> {
|
||||
publication.setGroupId(target.getName());
|
||||
publication.setVersion(target.getVersion().toString());
|
||||
publication.artifact(task);
|
||||
});
|
||||
}
|
||||
|
||||
public static File targetDirectory(Project project) {
|
||||
String dir = System.getenv("DEPLOY_DIR");
|
||||
if (dir == null) {
|
||||
return project.getLayout().getBuildDirectory().dir("api").get().getAsFile();
|
||||
}
|
||||
return new File(dir);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class GenerateApiTask extends DefaultTask {
|
||||
private final File inputFile;
|
||||
private final File outputFile;
|
||||
|
||||
public GenerateApiTask() {
|
||||
setGroup("iris");
|
||||
dependsOn("jar");
|
||||
finalizedBy("publishMavenPublicationToDeployDirRepository");
|
||||
doLast(task -> getLogger().lifecycle("The API is located at " + getOutputFile().getAbsolutePath()));
|
||||
|
||||
TaskProvider<Jar> jarTask = getProject().getTasks().named("jar", Jar.class);
|
||||
this.inputFile = jarTask.get().getArchiveFile().get().getAsFile();
|
||||
this.outputFile = ApiGenerator.targetDirectory(getProject()).toPath().resolve(this.inputFile.getName()).toFile();
|
||||
}
|
||||
|
||||
@InputFile
|
||||
public File getInputFile() {
|
||||
return inputFile;
|
||||
}
|
||||
|
||||
@OutputFile
|
||||
public File getOutputFile() {
|
||||
return outputFile;
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
public void generate() throws IOException {
|
||||
File parent = outputFile.getParentFile();
|
||||
if (parent != null) {
|
||||
parent.mkdirs();
|
||||
}
|
||||
|
||||
try (JarFile jar = new JarFile(inputFile);
|
||||
JarOutputStream out = new JarOutputStream(new FileOutputStream(outputFile))) {
|
||||
jar.stream()
|
||||
.parallel()
|
||||
.filter(entry -> !entry.isDirectory())
|
||||
.filter(entry -> entry.getName().endsWith(".class"))
|
||||
.forEach(entry -> writeStrippedClass(jar, out, entry));
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeStrippedClass(JarFile jar, JarOutputStream out, JarEntry entry) {
|
||||
byte[] bytes;
|
||||
try (InputStream input = jar.getInputStream(entry)) {
|
||||
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
ClassVisitor visitor = new MethodClearingVisitor(writer);
|
||||
ClassReader reader = new ClassReader(input);
|
||||
reader.accept(visitor, 0);
|
||||
bytes = writer.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
synchronized (out) {
|
||||
try {
|
||||
JarEntry outputEntry = new JarEntry(entry.getName());
|
||||
out.putNextEntry(outputEntry);
|
||||
out.write(bytes);
|
||||
out.closeEntry();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MethodClearingVisitor extends ClassVisitor {
|
||||
public MethodClearingVisitor(ClassVisitor cv) {
|
||||
super(Opcodes.ASM9, cv);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
|
||||
return new ExceptionThrowingMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions));
|
||||
}
|
||||
}
|
||||
|
||||
class ExceptionThrowingMethodVisitor extends MethodVisitor {
|
||||
public ExceptionThrowingMethodVisitor(MethodVisitor mv) {
|
||||
super(Opcodes.ASM9, mv);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitCode() {
|
||||
if (mv == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mv.visitCode();
|
||||
mv.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalStateException");
|
||||
mv.visitInsn(Opcodes.DUP);
|
||||
mv.visitLdcInsn("Only API");
|
||||
mv.visitMethodInsn(
|
||||
Opcodes.INVOKESPECIAL,
|
||||
"java/lang/IllegalStateException",
|
||||
"<init>",
|
||||
"(Ljava/lang/String;)V",
|
||||
false
|
||||
);
|
||||
mv.visitInsn(Opcodes.ATHROW);
|
||||
mv.visitMaxs(0, 0);
|
||||
mv.visitEnd();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
public class Config {
|
||||
public int jvm = 25;
|
||||
public NMSBinding.Type type = NMSBinding.Type.DIRECT;
|
||||
public String version;
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import com.volmit.nmstools.NMSToolsExtension;
|
||||
import com.volmit.nmstools.NMSToolsPlugin;
|
||||
import io.papermc.paperweight.userdev.PaperweightUser;
|
||||
import io.papermc.paperweight.userdev.PaperweightUserDependenciesExtension;
|
||||
import io.papermc.paperweight.userdev.PaperweightUserExtension;
|
||||
import io.papermc.paperweight.userdev.attribute.Obfuscation;
|
||||
import org.gradle.api.Action;
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Named;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.attributes.Bundling;
|
||||
import org.gradle.api.attributes.Category;
|
||||
import org.gradle.api.attributes.LibraryElements;
|
||||
import org.gradle.api.attributes.Usage;
|
||||
import org.gradle.api.file.FileTree;
|
||||
import org.gradle.api.model.ObjectFactory;
|
||||
import org.gradle.api.plugins.JavaPluginExtension;
|
||||
import org.gradle.api.plugins.ExtraPropertiesExtension;
|
||||
import org.gradle.api.provider.Provider;
|
||||
import org.gradle.api.tasks.SourceSet;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion;
|
||||
import org.gradle.jvm.toolchain.JavaToolchainService;
|
||||
import org.gradle.work.DisableCachingByDefault;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static io.papermc.paperweight.util.constants.ConstantsKt.REOBF_CONFIG;
|
||||
|
||||
public class NMSBinding implements Plugin<Project> {
|
||||
private static final String NEW_LINE = System.lineSeparator();
|
||||
private static final byte[] NEW_LINE_BYTES = NEW_LINE.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
@Override
|
||||
public void apply(Project target) {
|
||||
ExtraPropertiesExtension extra = target.getExtensions().getExtraProperties();
|
||||
Object configValue = extra.has("nms") ? extra.get("nms") : null;
|
||||
if (!(configValue instanceof Config)) {
|
||||
throw new GradleException("No NMS binding configuration found");
|
||||
}
|
||||
|
||||
Config config = (Config) configValue;
|
||||
int jvm = config.jvm;
|
||||
Type type = config.type;
|
||||
|
||||
if (type == Type.USER_DEV) {
|
||||
target.getPlugins().apply(PaperweightUser.class);
|
||||
|
||||
PaperweightUserDependenciesExtension dependenciesExtension =
|
||||
target.getDependencies().getExtensions().findByType(PaperweightUserDependenciesExtension.class);
|
||||
if (dependenciesExtension != null) {
|
||||
dependenciesExtension.paperDevBundle(config.version);
|
||||
}
|
||||
|
||||
JavaPluginExtension java = target.getExtensions().findByType(JavaPluginExtension.class);
|
||||
if (java == null) {
|
||||
throw new GradleException("Java plugin not found");
|
||||
}
|
||||
|
||||
java.getToolchain().getLanguageVersion().set(JavaLanguageVersion.of(jvm));
|
||||
JavaToolchainService javaToolchains = target.getExtensions().getByType(JavaToolchainService.class);
|
||||
target.getExtensions().configure(PaperweightUserExtension.class,
|
||||
extension -> extension.getJavaLauncher().set(javaToolchains.launcherFor(java.getToolchain())));
|
||||
} else {
|
||||
extra.set("nmsTools.useBuildTools", type == Type.BUILD_TOOLS);
|
||||
target.getPlugins().apply(NMSToolsPlugin.class);
|
||||
target.getExtensions().configure(NMSToolsExtension.class, extension -> {
|
||||
extension.getJvm().set(jvm);
|
||||
extension.getVersion().set(config.version);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
String outgoingArtifactTask = type == Type.USER_DEV ? "jar" : "remap";
|
||||
ObjectFactory objects = target.getObjects();
|
||||
Configuration reobfConfiguration = target.getConfigurations().findByName(REOBF_CONFIG);
|
||||
if (reobfConfiguration == null) {
|
||||
reobfConfiguration = target.getConfigurations().create(REOBF_CONFIG);
|
||||
}
|
||||
|
||||
target.getConfigurations().named(REOBF_CONFIG).configure(configuration -> {
|
||||
configuration.setCanBeConsumed(true);
|
||||
configuration.setCanBeResolved(false);
|
||||
configuration.getAttributes().attribute(Usage.USAGE_ATTRIBUTE, named(objects, Usage.class, Usage.JAVA_RUNTIME));
|
||||
configuration.getAttributes().attribute(Category.CATEGORY_ATTRIBUTE, named(objects, Category.class, Category.LIBRARY));
|
||||
configuration.getAttributes().attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, named(objects, LibraryElements.class, LibraryElements.JAR));
|
||||
configuration.getAttributes().attribute(Bundling.BUNDLING_ATTRIBUTE, named(objects, Bundling.class, Bundling.EXTERNAL));
|
||||
configuration.getAttributes().attribute(Obfuscation.Companion.getOBFUSCATION_ATTRIBUTE(), named(objects, Obfuscation.class, Obfuscation.OBFUSCATED));
|
||||
configuration.getOutgoing().artifact(target.getTasks().named(outgoingArtifactTask));
|
||||
});
|
||||
|
||||
int[] version = parseVersion(config.version);
|
||||
int major = version[0];
|
||||
int minor = version[1];
|
||||
if (major <= 20 && minor <= 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.getTasks().register("convert", ConversionTask.class, type);
|
||||
target.getTasks().named("compileJava").configure(task -> task.dependsOn("convert"));
|
||||
target.getRootProject().getTasks()
|
||||
.matching(task -> task.getName().equals("prepareKotlinBuildScriptModel"))
|
||||
.configureEach(task -> task.dependsOn(target.getPath() + ":convert"));
|
||||
}
|
||||
|
||||
public static void nmsBinding(Project project, Action<Config> action) {
|
||||
Config config = new Config();
|
||||
action.execute(config);
|
||||
project.getExtensions().getExtraProperties().set("nms", config);
|
||||
project.getPlugins().apply(NMSBinding.class);
|
||||
}
|
||||
|
||||
private static int[] parseVersion(String version) {
|
||||
String trimmed = version;
|
||||
int suffix = trimmed.indexOf('-');
|
||||
if (suffix >= 0) {
|
||||
trimmed = trimmed.substring(0, suffix);
|
||||
}
|
||||
|
||||
String[] parts = trimmed.split("\\.");
|
||||
return new int[]{Integer.parseInt(parts[1]), Integer.parseInt(parts[2])};
|
||||
}
|
||||
|
||||
private static <T extends Named> T named(ObjectFactory objects, Class<T> type, String name) {
|
||||
return objects.named(type, name);
|
||||
}
|
||||
|
||||
@DisableCachingByDefault
|
||||
public abstract static class ConversionTask extends DefaultTask {
|
||||
private final Pattern pattern;
|
||||
private final String replacement;
|
||||
|
||||
@Inject
|
||||
public ConversionTask(Type type) {
|
||||
setGroup("nms");
|
||||
getInputs().property("type", type);
|
||||
|
||||
JavaPluginExtension java = getProject().getExtensions().findByType(JavaPluginExtension.class);
|
||||
if (java == null) {
|
||||
throw new GradleException("Java plugin not found");
|
||||
}
|
||||
|
||||
Provider<FileTree> source = java.getSourceSets().named(SourceSet.MAIN_SOURCE_SET_NAME).map(SourceSet::getAllJava);
|
||||
getInputs().files(source);
|
||||
getOutputs().files(source);
|
||||
|
||||
if (type == Type.USER_DEV) {
|
||||
this.pattern = Pattern.compile("org\\.bukkit\\.craftbukkit\\." + getProject().getName());
|
||||
this.replacement = "org.bukkit.craftbukkit";
|
||||
} else {
|
||||
this.pattern = Pattern.compile("org\\.bukkit\\.craftbukkit\\.(?!" + getProject().getName() + ")");
|
||||
this.replacement = "org.bukkit.craftbukkit." + getProject().getName() + ".";
|
||||
}
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
public void process() {
|
||||
ExecutorService executor = Executors.newFixedThreadPool(16);
|
||||
try {
|
||||
Set<File> files = getInputs().getFiles().getFiles();
|
||||
List<Future<?>> futures = new ArrayList<>(files.size());
|
||||
for (File file : files) {
|
||||
if (!file.getName().endsWith(".java")) {
|
||||
continue;
|
||||
}
|
||||
futures.add(executor.submit(() -> processFile(file)));
|
||||
}
|
||||
|
||||
for (Future<?> future : futures) {
|
||||
future.get();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException(e);
|
||||
} catch (ExecutionException e) {
|
||||
throw new RuntimeException(e.getCause());
|
||||
} finally {
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private void processFile(File file) {
|
||||
List<String> output = new ArrayList<>();
|
||||
boolean changed = false;
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("package") || line.isBlank()) {
|
||||
output.add(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.startsWith("import")) {
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
output.add(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
Matcher matcher = pattern.matcher(line);
|
||||
if (!matcher.find()) {
|
||||
output.add(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
output.add(matcher.replaceAll(replacement));
|
||||
changed = true;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (hasTrailingNewLine(file)) {
|
||||
output.add("");
|
||||
}
|
||||
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
|
||||
for (int i = 0; i < output.size(); i++) {
|
||||
writer.append(output.get(i));
|
||||
if (i + 1 < output.size()) {
|
||||
writer.append(NEW_LINE);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasTrailingNewLine(File file) throws IOException {
|
||||
if (NEW_LINE_BYTES.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
|
||||
if (raf.length() < NEW_LINE_BYTES.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] bytes = new byte[NEW_LINE_BYTES.length];
|
||||
raf.seek(raf.length() - bytes.length);
|
||||
raf.readFully(bytes);
|
||||
return Arrays.equals(bytes, NEW_LINE_BYTES);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
USER_DEV,
|
||||
BUILD_TOOLS,
|
||||
DIRECT
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import org.gradle.jvm.tasks.Jar
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
}
|
||||
|
||||
tasks.named('jar', Jar).configure {
|
||||
manifest.attributes(
|
||||
'Agent-Class': 'art.arcane.iris.util.project.agent.Installer',
|
||||
'Premain-Class': 'art.arcane.iris.util.project.agent.Installer',
|
||||
'Can-Redefine-Classes': true,
|
||||
'Can-Retransform-Classes': true
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package art.arcane.iris.util.project.agent;
|
||||
|
||||
import java.lang.instrument.Instrumentation;
|
||||
|
||||
public class Installer {
|
||||
private static volatile Instrumentation instrumentation;
|
||||
|
||||
public static Instrumentation getInstrumentation() {
|
||||
Instrumentation instrumentation = Installer.instrumentation;
|
||||
if (instrumentation == null) {
|
||||
throw new IllegalStateException("The agent is not loaded or this method is not called via the system class loader");
|
||||
}
|
||||
return instrumentation;
|
||||
}
|
||||
|
||||
public static void premain(String arguments, Instrumentation instrumentation) {
|
||||
doMain(instrumentation);
|
||||
}
|
||||
|
||||
public static void agentmain(String arguments, Instrumentation instrumentation) {
|
||||
doMain(instrumentation);
|
||||
}
|
||||
|
||||
private static synchronized void doMain(Instrumentation instrumentation) {
|
||||
if (Installer.instrumentation != null)
|
||||
return;
|
||||
Installer.instrumentation = instrumentation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
import io.github.slimjar.resolver.data.Mirror
|
||||
import org.ajoberstar.grgit.Grgit
|
||||
import org.gradle.api.Task
|
||||
import org.gradle.api.tasks.Copy
|
||||
import org.gradle.api.tasks.TaskProvider
|
||||
import org.gradle.api.tasks.compile.JavaCompile
|
||||
import org.gradle.jvm.tasks.Jar
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
import java.net.URI
|
||||
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2021 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/>.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'java-library'
|
||||
alias(libs.plugins.shadow)
|
||||
alias(libs.plugins.sentry)
|
||||
alias(libs.plugins.slimjar)
|
||||
alias(libs.plugins.grgit)
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlin.lombok)
|
||||
}
|
||||
|
||||
def apiVersion = '1.21'
|
||||
def mainClass = 'art.arcane.iris.Iris'
|
||||
def lib = 'art.arcane.iris.util'
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:master-SNAPSHOT')
|
||||
.get()
|
||||
String sentryAuthToken = findProperty('sentry.auth.token') as String ?: System.getenv('SENTRY_AUTH_TOKEN')
|
||||
boolean hasSentryAuthToken = sentryAuthToken != null && !sentryAuthToken.isBlank()
|
||||
|
||||
/**
|
||||
* Dependencies.
|
||||
*
|
||||
* Provided or classpath dependencies are not shaded and are available on the runtime classpath
|
||||
*
|
||||
* Shaded dependencies are not available at runtime, nor are they available on mvn central so they
|
||||
* need to be shaded into the jar (increasing binary size)
|
||||
*
|
||||
* Dynamically loaded dependencies are defined in the plugin.yml (updating these must be updated in the
|
||||
* plugin.yml also, otherwise they wont be available). These do not increase binary size). Only declare
|
||||
* these dependencies if they are available on mvn central.
|
||||
*/
|
||||
dependencies {
|
||||
// Provided or Classpath
|
||||
compileOnly(libs.spigot)
|
||||
compileOnly(libs.log4j.api)
|
||||
compileOnly(libs.log4j.core)
|
||||
|
||||
// Third Party Integrations
|
||||
compileOnly(libs.nexo)
|
||||
compileOnly(libs.itemsadder)
|
||||
compileOnly(libs.placeholderApi)
|
||||
compileOnly(libs.score)
|
||||
compileOnly(libs.mmoitems)
|
||||
compileOnly(libs.ecoitems)
|
||||
compileOnly(libs.mythic)
|
||||
compileOnly(libs.mythicChrucible)
|
||||
compileOnly(libs.kgenerators) {
|
||||
transitive = false
|
||||
}
|
||||
compileOnly(libs.multiverseCore)
|
||||
compileOnly(libs.craftengine.core)
|
||||
compileOnly(libs.craftengine.bukkit)
|
||||
|
||||
// Shaded
|
||||
implementation('de.crazydev22.slimjar.helper:spigot:2.1.9')
|
||||
implementation(volmLibCoordinate) {
|
||||
changing = true
|
||||
transitive = false
|
||||
}
|
||||
|
||||
// Dynamically Loaded
|
||||
slim(libs.paralithic)
|
||||
slim(libs.paperlib)
|
||||
slim(libs.adventure.api)
|
||||
slim(libs.adventure.minimessage)
|
||||
slim(libs.adventure.platform)
|
||||
slim(libs.bstats)
|
||||
slim(libs.sentry)
|
||||
|
||||
slim(libs.commons.io)
|
||||
slim(libs.commons.lang)
|
||||
slim(libs.commons.lang3)
|
||||
slim(libs.commons.math3)
|
||||
slim(libs.oshi)
|
||||
slim(libs.lz4)
|
||||
slim(libs.fastutil)
|
||||
slim(libs.lru)
|
||||
slim(libs.zip)
|
||||
slim(libs.gson)
|
||||
slim(libs.asm)
|
||||
slim(libs.caffeine)
|
||||
slim(libs.byteBuddy.core)
|
||||
slim(libs.byteBuddy.agent)
|
||||
slim(libs.dom4j)
|
||||
slim(libs.jaxen)
|
||||
|
||||
// Script Engine
|
||||
slim(libs.kotlin.stdlib)
|
||||
slim(libs.kotlin.coroutines)
|
||||
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.16.1')
|
||||
testImplementation(libs.spigot)
|
||||
testRuntimeOnly(libs.spigot)
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(25)
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.fromTarget('25'))
|
||||
}
|
||||
}
|
||||
|
||||
sentry {
|
||||
url = 'http://sentry.volmit.com:8080'
|
||||
autoInstallation.enabled = false
|
||||
includeSourceContext = true
|
||||
|
||||
org = 'sentry'
|
||||
projectName = 'iris'
|
||||
authToken = sentryAuthToken
|
||||
}
|
||||
|
||||
slimJar {
|
||||
mirrors = [
|
||||
new Mirror(
|
||||
URI.create('https://maven-central.storage-download.googleapis.com/maven2').toURL(),
|
||||
URI.create('https://repo.maven.apache.org/maven2/').toURL()
|
||||
)
|
||||
]
|
||||
|
||||
relocate('com.dfsek.paralithic', "${lib}.paralithic")
|
||||
relocate('io.papermc.lib', "${lib}.paper")
|
||||
relocate('net.kyori', "${lib}.kyori")
|
||||
relocate('org.bstats', "${lib}.metrics")
|
||||
relocate('io.sentry', "${lib}.sentry")
|
||||
relocate('org.apache.maven', "${lib}.maven")
|
||||
relocate('org.codehaus.plexus', "${lib}.plexus")
|
||||
relocate('org.eclipse.sisu', "${lib}.sisu")
|
||||
relocate('org.eclipse.aether', "${lib}.aether")
|
||||
relocate('com.google.inject', "${lib}.guice")
|
||||
relocate('org.dom4j', "${lib}.dom4j")
|
||||
relocate('org.jaxen', "${lib}.jaxen")
|
||||
relocate('com.github.benmanes.caffeine', "${lib}.caffeine")
|
||||
}
|
||||
|
||||
def embeddedAgentJar = project(':core:agent').tasks.named('jar', Jar)
|
||||
def templateSource = file('src/main/templates')
|
||||
def templateDest = layout.buildDirectory.dir('generated/sources/templates')
|
||||
def generateTemplates = tasks.register('generateTemplates', Copy) {
|
||||
inputs.properties([
|
||||
environment: providers.provider {
|
||||
if (project.hasProperty('release')) {
|
||||
return 'production'
|
||||
}
|
||||
if (project.hasProperty('argghh')) {
|
||||
return 'Argghh!'
|
||||
}
|
||||
return 'development'
|
||||
},
|
||||
commit: providers.provider {
|
||||
String commitId = null
|
||||
Exception failure = null
|
||||
try {
|
||||
commitId = project.extensions.getByType(Grgit).head().id
|
||||
} catch (Exception ex) {
|
||||
failure = ex
|
||||
}
|
||||
|
||||
if (commitId != null && commitId.length() == 40) {
|
||||
return commitId
|
||||
}
|
||||
|
||||
logger.error('Git commit hash not found', failure)
|
||||
return 'unknown'
|
||||
},
|
||||
])
|
||||
|
||||
from(templateSource)
|
||||
into(templateDest)
|
||||
rename { String fileName -> "art/arcane/iris/${fileName}" }
|
||||
expand(inputs.properties)
|
||||
}
|
||||
|
||||
tasks.named('compileJava', JavaCompile).configure {
|
||||
/**
|
||||
* We need parameter meta for the decree command system
|
||||
*/
|
||||
options.compilerArgs.add('-parameters')
|
||||
options.encoding = 'UTF-8'
|
||||
options.debugOptions.debugLevel = 'none'
|
||||
}
|
||||
|
||||
tasks.named('processResources').configure {
|
||||
/**
|
||||
* Expand properties into plugin yml
|
||||
*/
|
||||
def pluginProperties = [
|
||||
name : rootProject.name,
|
||||
version : rootProject.version,
|
||||
apiVersion: apiVersion,
|
||||
main : mainClass,
|
||||
]
|
||||
inputs.properties(pluginProperties)
|
||||
filesMatching('**/plugin.yml') {
|
||||
expand(pluginProperties)
|
||||
}
|
||||
}
|
||||
|
||||
def runningTestTasks = gradle.startParameter.taskNames.any { String taskName -> taskName.toLowerCase().contains('test') }
|
||||
if (runningTestTasks) {
|
||||
TaskProvider<Task> processResourcesTask = tasks.named('processResources')
|
||||
tasks.named('classes').configure { Task classesTask ->
|
||||
Set<Object> dependencies = new LinkedHashSet<Object>(classesTask.getDependsOn())
|
||||
dependencies.removeIf { Object dependency ->
|
||||
if (dependency instanceof TaskProvider) {
|
||||
return ((TaskProvider<?>) dependency).name == processResourcesTask.name
|
||||
}
|
||||
if (dependency instanceof Task) {
|
||||
return ((Task) dependency).name == processResourcesTask.name
|
||||
}
|
||||
String dependencyName = String.valueOf(dependency)
|
||||
return dependencyName == 'processResources' || dependencyName.endsWith(':processResources')
|
||||
}
|
||||
classesTask.setDependsOn(dependencies)
|
||||
}
|
||||
processResourcesTask.configure { Task task ->
|
||||
task.enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).configure {
|
||||
dependsOn(embeddedAgentJar)
|
||||
mergeServiceFiles()
|
||||
//minimize()
|
||||
relocate('io.github.slimjar', "${lib}.slimjar")
|
||||
exclude('modules/loader-agent.isolated-jar')
|
||||
from(embeddedAgentJar.map { it.archiveFile }) {
|
||||
rename { String ignored -> 'agent.jar' }
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('sentryCollectSourcesJava').configure {
|
||||
dependsOn(generateTemplates)
|
||||
}
|
||||
|
||||
tasks.named('generateSentryBundleIdJava').configure {
|
||||
dependsOn(generateTemplates)
|
||||
}
|
||||
|
||||
tasks.matching { Task task ->
|
||||
task.name.startsWith('sentry') || task.name.startsWith('generateSentry')
|
||||
}.configureEach {
|
||||
onlyIf {
|
||||
hasSentryAuthToken
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.tasks.matching {
|
||||
it.name == 'prepareKotlinBuildScriptModel'
|
||||
}.configureEach {
|
||||
dependsOn(generateTemplates)
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDir(generateTemplates.map { it.outputs })
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
1435163759
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
public enum IrisPaperLikeBackendMode {
|
||||
AUTO,
|
||||
TICKET,
|
||||
SERVICE
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Server;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum IrisRuntimeSchedulerMode {
|
||||
AUTO,
|
||||
PAPER_LIKE,
|
||||
FOLIA;
|
||||
|
||||
public static IrisRuntimeSchedulerMode resolve(IrisSettings.IrisSettingsPregen pregen) {
|
||||
Server server = Bukkit.getServer();
|
||||
boolean regionizedRuntime = FoliaScheduler.isRegionizedRuntime(server);
|
||||
if (regionizedRuntime) {
|
||||
return FOLIA;
|
||||
}
|
||||
|
||||
IrisRuntimeSchedulerMode configuredMode = pregen == null ? null : pregen.getRuntimeSchedulerMode();
|
||||
if (configuredMode != null && configuredMode != AUTO) {
|
||||
if (configuredMode == FOLIA) {
|
||||
return PAPER_LIKE;
|
||||
}
|
||||
return configuredMode;
|
||||
}
|
||||
|
||||
String bukkitName = Bukkit.getName();
|
||||
String bukkitVersion = Bukkit.getVersion();
|
||||
String serverClassName = server == null ? "" : server.getClass().getName();
|
||||
if (containsIgnoreCase(bukkitName, "folia")
|
||||
|| containsIgnoreCase(bukkitVersion, "folia")
|
||||
|| containsIgnoreCase(serverClassName, "folia")) {
|
||||
return FOLIA;
|
||||
}
|
||||
|
||||
if (containsIgnoreCase(bukkitName, "purpur")
|
||||
|| containsIgnoreCase(bukkitVersion, "purpur")
|
||||
|| containsIgnoreCase(serverClassName, "purpur")
|
||||
|| containsIgnoreCase(bukkitName, "canvas")
|
||||
|| containsIgnoreCase(bukkitVersion, "canvas")
|
||||
|| containsIgnoreCase(serverClassName, "canvas")
|
||||
|| containsIgnoreCase(bukkitName, "paper")
|
||||
|| containsIgnoreCase(bukkitVersion, "paper")
|
||||
|| containsIgnoreCase(serverClassName, "paper")
|
||||
|| containsIgnoreCase(bukkitName, "pufferfish")
|
||||
|| containsIgnoreCase(bukkitVersion, "pufferfish")
|
||||
|| containsIgnoreCase(serverClassName, "pufferfish")
|
||||
|| containsIgnoreCase(bukkitName, "spigot")
|
||||
|| containsIgnoreCase(bukkitVersion, "spigot")
|
||||
|| containsIgnoreCase(serverClassName, "spigot")
|
||||
|| containsIgnoreCase(bukkitName, "craftbukkit")
|
||||
|| containsIgnoreCase(bukkitVersion, "craftbukkit")
|
||||
|| containsIgnoreCase(serverClassName, "craftbukkit")) {
|
||||
return PAPER_LIKE;
|
||||
}
|
||||
|
||||
if (regionizedRuntime) {
|
||||
return FOLIA;
|
||||
}
|
||||
|
||||
return PAPER_LIKE;
|
||||
}
|
||||
|
||||
private static boolean containsIgnoreCase(String value, String contains) {
|
||||
if (value == null || contains == null || contains.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value.toLowerCase(Locale.ROOT).contains(contains.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
+154
-60
@@ -16,14 +16,15 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core;
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.util.io.IO;
|
||||
import com.volmit.iris.util.json.JSONException;
|
||||
import com.volmit.iris.util.json.JSONObject;
|
||||
import com.volmit.iris.util.plugin.VolmitSender;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONException;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.iris.util.common.misc.getHardware;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.File;
|
||||
@@ -32,7 +33,7 @@ import java.io.IOException;
|
||||
@SuppressWarnings("SynchronizeOnNonFinalField")
|
||||
@Data
|
||||
public class IrisSettings {
|
||||
public static transient IrisSettings settings;
|
||||
public static IrisSettings settings;
|
||||
private IrisSettingsGeneral general = new IrisSettingsGeneral();
|
||||
private IrisSettingsWorld world = new IrisSettingsWorld();
|
||||
private IrisSettingsGUI gui = new IrisSettingsGUI();
|
||||
@@ -41,13 +42,65 @@ public class IrisSettings {
|
||||
private IrisSettingsConcurrency concurrency = new IrisSettingsConcurrency();
|
||||
private IrisSettingsStudio studio = new IrisSettingsStudio();
|
||||
private IrisSettingsPerformance performance = new IrisSettingsPerformance();
|
||||
private IrisSettingsPregen pregen = new IrisSettingsPregen();
|
||||
private IrisSettingsSentry sentry = new IrisSettingsSentry();
|
||||
|
||||
public static int getThreadCount(int c) {
|
||||
return switch(c) {
|
||||
return Math.max(switch (c) {
|
||||
case -1, -2, -4 -> Runtime.getRuntime().availableProcessors() / -c;
|
||||
case 0, 1, 2 -> 1;
|
||||
default -> Math.max(c, 2);
|
||||
};
|
||||
}, 1);
|
||||
}
|
||||
|
||||
public static IrisSettings get() {
|
||||
if (settings != null) {
|
||||
return settings;
|
||||
}
|
||||
|
||||
settings = new IrisSettings();
|
||||
|
||||
File s = Iris.instance.getDataFile("settings.json");
|
||||
|
||||
if (!s.exists()) {
|
||||
try {
|
||||
IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4));
|
||||
} catch (JSONException | IOException e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
String ss = IO.readAll(s);
|
||||
settings = new Gson().fromJson(ss, IrisSettings.class);
|
||||
try {
|
||||
IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} catch (Throwable ee) {
|
||||
// Iris.reportError(ee); causes a self-reference & stackoverflow
|
||||
Iris.error("Configuration Error in settings.json! " + ee.getClass().getSimpleName() + ": " + ee.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
public static void invalidate() {
|
||||
synchronized (settings) {
|
||||
settings = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void forceSave() {
|
||||
File s = Iris.instance.getDataFile("settings.json");
|
||||
|
||||
try {
|
||||
IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4));
|
||||
} catch (JSONException | IOException e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -74,36 +127,105 @@ public class IrisSettings {
|
||||
public double targetSpawnEntitiesPerChunk = 0.95;
|
||||
public boolean markerEntitySpawningSystem = true;
|
||||
public boolean effectSystem = true;
|
||||
public boolean worldEditWandCUI = true;
|
||||
public boolean globalPregenCache = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisSettingsConcurrency {
|
||||
public int parallelism = -1;
|
||||
public int ioParallelism = -2;
|
||||
public int worldGenParallelism = -1;
|
||||
|
||||
public int getWorldGenThreads() {
|
||||
return getThreadCount(worldGenParallelism);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisSettingsPregen {
|
||||
public boolean useCacheByDefault = true;
|
||||
public boolean useHighPriority = false;
|
||||
public boolean useVirtualThreads = false;
|
||||
public boolean useTicketQueue = true;
|
||||
public IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.AUTO;
|
||||
public IrisPaperLikeBackendMode paperLikeBackendMode = IrisPaperLikeBackendMode.AUTO;
|
||||
public int maxConcurrency = 256;
|
||||
public int paperLikeMaxConcurrency = 96;
|
||||
public int foliaMaxConcurrency = 32;
|
||||
public int chunkLoadTimeoutSeconds = 15;
|
||||
public int timeoutWarnIntervalMs = 500;
|
||||
public int saveIntervalMs = 30_000;
|
||||
public boolean enablePregenPerformanceProfile = true;
|
||||
public int pregenProfileNoiseCacheSize = 4_096;
|
||||
public boolean pregenProfileEnableFastCache = true;
|
||||
public boolean pregenProfileLogJvmHints = true;
|
||||
|
||||
public int getChunkLoadTimeoutSeconds() {
|
||||
return Math.max(5, Math.min(chunkLoadTimeoutSeconds, 120));
|
||||
}
|
||||
|
||||
public int getTimeoutWarnIntervalMs() {
|
||||
return Math.max(timeoutWarnIntervalMs, 250);
|
||||
}
|
||||
|
||||
public int getPaperLikeMaxConcurrency() {
|
||||
return Math.max(1, paperLikeMaxConcurrency);
|
||||
}
|
||||
|
||||
public int getFoliaMaxConcurrency() {
|
||||
return Math.max(1, foliaMaxConcurrency);
|
||||
}
|
||||
|
||||
public IrisPaperLikeBackendMode getPaperLikeBackendMode() {
|
||||
if (paperLikeBackendMode == null) {
|
||||
return IrisPaperLikeBackendMode.AUTO;
|
||||
}
|
||||
|
||||
return paperLikeBackendMode;
|
||||
}
|
||||
|
||||
public int getSaveIntervalMs() {
|
||||
return Math.max(5_000, Math.min(saveIntervalMs, 900_000));
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisSettingsPerformance {
|
||||
public boolean trimMantleInStudio = false;
|
||||
private IrisSettingsEngineSVC engineSVC = new IrisSettingsEngineSVC();
|
||||
public boolean trimMantleInStudio = false;
|
||||
public int mantleKeepAlive = 30;
|
||||
public int cacheSize = 4_096;
|
||||
public int noiseCacheSize = 1_024;
|
||||
public int resourceLoaderCacheSize = 1_024;
|
||||
public int objectLoaderCacheSize = 4_096;
|
||||
public int scriptLoaderCacheSize = 512;
|
||||
public int tectonicPlateSize = -1;
|
||||
public int mantleCleanupDelay = 200;
|
||||
|
||||
public int getTectonicPlateSize() {
|
||||
if (tectonicPlateSize > 0)
|
||||
return tectonicPlateSize;
|
||||
|
||||
return (int) (getHardware.getProcessMemory() / 512L);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisSettingsGeneral {
|
||||
public boolean commandSounds = true;
|
||||
public boolean debug = false;
|
||||
public boolean dumpMantleOnError = false;
|
||||
public boolean disableNMS = false;
|
||||
public boolean pluginMetrics = true;
|
||||
public boolean splashLogoStartup = true;
|
||||
public boolean useConsoleCustomColors = true;
|
||||
public boolean useCustomColorsIngame = true;
|
||||
public boolean adjustVanillaHeight = false;
|
||||
public String forceMainWorld = "";
|
||||
public int spinh = -20;
|
||||
public int spins = 7;
|
||||
public int spinb = 8;
|
||||
public String cartographerMessage = "Iris does not allow cartographers in its world due to crashes.";
|
||||
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean canUseCustomColors(VolmitSender volmitSender) {
|
||||
@@ -111,10 +233,18 @@ public class IrisSettings {
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisSettingsSentry {
|
||||
public boolean includeServerId = true;
|
||||
public boolean disableAutoReporting = false;
|
||||
public boolean debug = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisSettingsGUI {
|
||||
public boolean useServerLaunchedGuis = true;
|
||||
public boolean maximumPregenGuiFPS = false;
|
||||
public boolean colorMode = true;
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -122,6 +252,9 @@ public class IrisSettings {
|
||||
public String defaultWorldType = "overworld";
|
||||
public int maxBiomeChildDepth = 4;
|
||||
public boolean preventLeafDecay = true;
|
||||
public boolean useMulticore = false;
|
||||
public boolean useMulticoreMantle = false;
|
||||
public boolean earlyCustomBlocks = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -129,57 +262,18 @@ public class IrisSettings {
|
||||
public boolean studio = true;
|
||||
public boolean openVSCode = true;
|
||||
public boolean disableTimeAndWeather = true;
|
||||
public boolean enableEntitySpawning = false;
|
||||
public boolean autoStartDefaultStudio = false;
|
||||
}
|
||||
|
||||
public static IrisSettings get() {
|
||||
if(settings != null) {
|
||||
return settings;
|
||||
}
|
||||
@Data
|
||||
public static class IrisSettingsEngineSVC {
|
||||
public boolean useVirtualThreads = true;
|
||||
public boolean forceMulticoreWrite = false;
|
||||
public int priority = Thread.NORM_PRIORITY;
|
||||
|
||||
settings = new IrisSettings();
|
||||
|
||||
File s = Iris.instance.getDataFile("settings.json");
|
||||
|
||||
if(!s.exists()) {
|
||||
try {
|
||||
IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4));
|
||||
} catch(JSONException | IOException e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
String ss = IO.readAll(s);
|
||||
settings = new Gson().fromJson(ss, IrisSettings.class);
|
||||
try {
|
||||
IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4));
|
||||
} catch(IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} catch(Throwable ee) {
|
||||
// Iris.reportError(ee); causes a self-reference & stackoverflow
|
||||
Iris.error("Configuration Error in settings.json! " + ee.getClass().getSimpleName() + ": " + ee.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
public static void invalidate() {
|
||||
synchronized(settings) {
|
||||
settings = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void forceSave() {
|
||||
File s = Iris.instance.getDataFile("settings.json");
|
||||
|
||||
try {
|
||||
IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4));
|
||||
} catch(JSONException | IOException e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
public int getPriority() {
|
||||
return Math.max(Math.min(priority, Thread.MAX_PRIORITY), Thread.MIN_PRIORITY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package art.arcane.iris.core;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.iris.util.common.misc.ServerProperties;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class IrisWorlds {
|
||||
private static final AtomicCache<IrisWorlds> cache = new AtomicCache<>();
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final Type TYPE = TypeToken.getParameterized(KMap.class, String.class, String.class).getType();
|
||||
private final KMap<String, String> worlds;
|
||||
private volatile boolean dirty = false;
|
||||
|
||||
private IrisWorlds(KMap<String, String> worlds) {
|
||||
this.worlds = worlds;
|
||||
readBukkitWorlds().forEach(this::put0);
|
||||
save();
|
||||
}
|
||||
|
||||
public static IrisWorlds get() {
|
||||
return cache.aquire(() -> {
|
||||
File file = Iris.instance.getDataFile("worlds.json");
|
||||
if (!file.exists()) {
|
||||
return new IrisWorlds(new KMap<>());
|
||||
}
|
||||
|
||||
try {
|
||||
String json = IO.readAll(file);
|
||||
KMap<String, String> worlds = GSON.fromJson(json, TYPE);
|
||||
return new IrisWorlds(Objects.requireNonNullElseGet(worlds, KMap::new));
|
||||
} catch (Throwable e) {
|
||||
Iris.error("Failed to load worlds.json!");
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
return new IrisWorlds(new KMap<>());
|
||||
});
|
||||
}
|
||||
|
||||
public void put(String name, String type) {
|
||||
put0(name, type);
|
||||
save();
|
||||
}
|
||||
|
||||
private void put0(String name, String type) {
|
||||
String old = worlds.put(name, type);
|
||||
if (!type.equals(old))
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
public KMap<String, String> getWorlds() {
|
||||
clean();
|
||||
return readBukkitWorlds().put(worlds);
|
||||
}
|
||||
|
||||
public Stream<IrisData> getPacks() {
|
||||
return getDimensions()
|
||||
.map(IrisDimension::getLoader)
|
||||
.filter(Objects::nonNull);
|
||||
}
|
||||
|
||||
public Stream<IrisDimension> getDimensions() {
|
||||
return getWorlds()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(entry -> Iris.loadDimension(entry.getKey(), entry.getValue()))
|
||||
.filter(Objects::nonNull);
|
||||
}
|
||||
|
||||
public void clean() {
|
||||
dirty = worlds.entrySet().removeIf(entry -> !new File(Bukkit.getWorldContainer(), entry.getKey() + "/iris/pack/dimensions/" + entry.getValue() + ".json").exists());
|
||||
}
|
||||
|
||||
public synchronized void save() {
|
||||
clean();
|
||||
if (!dirty) return;
|
||||
try {
|
||||
IO.write(Iris.instance.getDataFile("worlds.json"), OutputStreamWriter::new, writer -> GSON.toJson(worlds, TYPE, writer));
|
||||
dirty = false;
|
||||
} catch (IOException e) {
|
||||
Iris.error("Failed to save worlds.json!");
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static KMap<String, String> readBukkitWorlds() {
|
||||
var bukkit = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
|
||||
var worlds = bukkit.getConfigurationSection("worlds");
|
||||
if (worlds == null) return new KMap<>();
|
||||
|
||||
var result = new KMap<String, String>();
|
||||
for (String world : worlds.getKeys(false)) {
|
||||
var gen = worlds.getString(world + ".generator");
|
||||
if (gen == null) continue;
|
||||
|
||||
String loadKey;
|
||||
if (gen.equalsIgnoreCase("iris")) {
|
||||
loadKey = IrisSettings.get().getGenerator().getDefaultWorldType();
|
||||
} else if (gen.startsWith("Iris:")) {
|
||||
loadKey = gen.substring(5);
|
||||
} else continue;
|
||||
|
||||
result.put(world, loadKey);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.core.nms.datapack.IDataFixer;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
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 lombok.NonNull;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicIntegerArray;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ServerConfigurator {
|
||||
private static volatile boolean deferredInstallPending = false;
|
||||
|
||||
public static void configure() {
|
||||
IrisSettings.IrisSettingsAutoconfiguration s = IrisSettings.get().getAutoConfiguration();
|
||||
if (s.isConfigureSpigotTimeoutTime()) {
|
||||
J.attempt(ServerConfigurator::increaseKeepAliveSpigot);
|
||||
}
|
||||
|
||||
if (s.isConfigurePaperWatchdogDelay()) {
|
||||
J.attempt(ServerConfigurator::increasePaperWatchdog);
|
||||
}
|
||||
|
||||
if (shouldDeferInstallUntilWorldsReady()) {
|
||||
deferredInstallPending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
deferredInstallPending = false;
|
||||
installDataPacks(true);
|
||||
}
|
||||
|
||||
public static void configureIfDeferred() {
|
||||
if (!deferredInstallPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
configure();
|
||||
if (deferredInstallPending) {
|
||||
J.a(ServerConfigurator::configureIfDeferred, 20);
|
||||
}
|
||||
}
|
||||
|
||||
private static void increaseKeepAliveSpigot() throws IOException, InvalidConfigurationException {
|
||||
File spigotConfig = new File("spigot.yml");
|
||||
FileConfiguration f = new YamlConfiguration();
|
||||
f.load(spigotConfig);
|
||||
long tt = f.getLong("settings.timeout-time");
|
||||
|
||||
long spigotTimeout = TimeUnit.MINUTES.toSeconds(5);
|
||||
|
||||
if (tt < spigotTimeout) {
|
||||
Iris.warn("Updating spigot.yml timeout-time: " + tt + " -> " + spigotTimeout + " (5 minutes)");
|
||||
Iris.warn("You can disable this change (autoconfigureServer) in Iris settings, then change back the value.");
|
||||
f.set("settings.timeout-time", spigotTimeout);
|
||||
f.save(spigotConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private static void increasePaperWatchdog() throws IOException, InvalidConfigurationException {
|
||||
File spigotConfig = new File("config/paper-global.yml");
|
||||
FileConfiguration f = new YamlConfiguration();
|
||||
f.load(spigotConfig);
|
||||
long tt = f.getLong("watchdog.early-warning-delay");
|
||||
|
||||
long watchdog = TimeUnit.MINUTES.toMillis(3);
|
||||
if (tt < watchdog) {
|
||||
Iris.warn("Updating paper.yml watchdog early-warning-delay: " + tt + " -> " + watchdog + " (3 minutes)");
|
||||
Iris.warn("You can disable this change (autoconfigureServer) in Iris settings, then change back the value.");
|
||||
f.set("watchdog.early-warning-delay", watchdog);
|
||||
f.save(spigotConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private static KList<File> getDatapacksFolder() {
|
||||
if (!IrisSettings.get().getGeneral().forceMainWorld.isEmpty()) {
|
||||
return new KList<File>().qadd(new File(Bukkit.getWorldContainer(), IrisSettings.get().getGeneral().forceMainWorld + "/datapacks"));
|
||||
}
|
||||
KList<File> worlds = new KList<>();
|
||||
Bukkit.getServer().getWorlds().forEach(w -> {
|
||||
File folder = resolveDatapacksFolder(w.getWorldFolder());
|
||||
if (!worlds.contains(folder)) {
|
||||
worlds.add(folder);
|
||||
}
|
||||
});
|
||||
if (worlds.isEmpty()) {
|
||||
worlds.add(new File(Bukkit.getWorldContainer(), ServerProperties.LEVEL_NAME + "/datapacks"));
|
||||
}
|
||||
return worlds;
|
||||
}
|
||||
|
||||
public static boolean installDataPacks(boolean fullInstall) {
|
||||
IDataFixer fixer = DataVersion.getDefault();
|
||||
if (fixer == null) {
|
||||
DataVersion fallback = DataVersion.getLatest();
|
||||
Iris.warn("Primary datapack fixer was null, forcing latest fixer: " + fallback.getVersion());
|
||||
fixer = fallback.get();
|
||||
}
|
||||
return installDataPacks(fixer, fullInstall);
|
||||
}
|
||||
|
||||
public static boolean installDataPacks(IDataFixer fixer, boolean fullInstall) {
|
||||
if (fixer == null) {
|
||||
Iris.error("Unable to install datapacks, fixer is null!");
|
||||
return false;
|
||||
}
|
||||
if (fullInstall) {
|
||||
Iris.info("Checking Data Packs...");
|
||||
} else {
|
||||
Iris.verbose("Checking Data Packs...");
|
||||
}
|
||||
DimensionHeight height = new DimensionHeight(fixer);
|
||||
KList<File> folders = getDatapacksFolder();
|
||||
java.util.concurrent.ConcurrentMap<String, KSet<String>> biomes = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
try (Stream<IrisData> stream = allPacks()) {
|
||||
stream.flatMap(height::merge)
|
||||
.parallel()
|
||||
.forEach(dim -> {
|
||||
Iris.verbose(" Checking Dimension " + dim.getLoadFile().getPath());
|
||||
dim.installBiomes(fixer, dim::getLoader, folders, biomes.computeIfAbsent(dim.getLoadKey(), k -> new KSet<>()));
|
||||
dim.installDimensionType(fixer, folders);
|
||||
});
|
||||
}
|
||||
IrisDimension.writeShared(folders, height);
|
||||
if (fullInstall) {
|
||||
Iris.info("Data Packs Setup!");
|
||||
} else {
|
||||
Iris.verbose("Data Packs Setup!");
|
||||
}
|
||||
|
||||
return fullInstall && verifyDataPacksPost(IrisSettings.get().getAutoConfiguration().isAutoRestartOnCustomBiomeInstall());
|
||||
}
|
||||
|
||||
public static boolean installDataPacksIfChanged(boolean fullInstall) {
|
||||
File packsDir = Iris.instance.getDataFolder("packs");
|
||||
String current = computePackFingerprint(packsDir);
|
||||
File cacheFile = new File(Iris.instance.getDataFolder("cache"), "datapack-fingerprint");
|
||||
String cached = "";
|
||||
if (cacheFile.exists()) {
|
||||
try {
|
||||
cached = Files.readString(cacheFile.toPath(), StandardCharsets.UTF_8).trim();
|
||||
} catch (IOException e) {
|
||||
cached = "";
|
||||
}
|
||||
}
|
||||
if (!current.isEmpty() && current.equals(cached)) {
|
||||
Iris.verbose("Data packs unchanged, skipping install.");
|
||||
return false;
|
||||
}
|
||||
boolean result = installDataPacks(fullInstall);
|
||||
try {
|
||||
cacheFile.getParentFile().mkdirs();
|
||||
Files.writeString(cacheFile.toPath(), current, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
Iris.warn("Failed to write datapack fingerprint cache: " + e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String computePackFingerprint(File packsDir) {
|
||||
if (packsDir == null || !packsDir.isDirectory()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
List<String> entries = new ArrayList<>();
|
||||
collectFingerprintEntries(packsDir, packsDir.getAbsolutePath(), entries);
|
||||
Collections.sort(entries);
|
||||
for (String entry : entries) {
|
||||
digest.update(entry.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
byte[] hash = digest.digest();
|
||||
StringBuilder sb = new StringBuilder(hash.length * 2);
|
||||
for (byte b : hash) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectFingerprintEntries(File dir, String rootPath, List<String> entries) {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
collectFingerprintEntries(file, rootPath, entries);
|
||||
} else {
|
||||
String relative = file.getAbsolutePath().substring(rootPath.length());
|
||||
entries.add(relative + "|" + file.length() + "|" + file.lastModified());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldDeferInstallUntilWorldsReady() {
|
||||
String forcedMainWorld = IrisSettings.get().getGeneral().forceMainWorld;
|
||||
if (forcedMainWorld != null && !forcedMainWorld.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Bukkit.getServer().getWorlds().isEmpty();
|
||||
}
|
||||
|
||||
public static File resolveDatapacksFolder(File worldFolder) {
|
||||
File rootFolder = resolveWorldRootFolder(worldFolder);
|
||||
return new File(rootFolder, "datapacks");
|
||||
}
|
||||
|
||||
static File resolveWorldRootFolder(File worldFolder) {
|
||||
if (worldFolder == null) {
|
||||
return new File(Bukkit.getWorldContainer(), ServerProperties.LEVEL_NAME);
|
||||
}
|
||||
|
||||
File current = worldFolder.getAbsoluteFile();
|
||||
while (current != null) {
|
||||
if ("dimensions".equals(current.getName())) {
|
||||
File parent = current.getParentFile();
|
||||
if (parent != null) {
|
||||
return parent;
|
||||
}
|
||||
break;
|
||||
}
|
||||
current = current.getParentFile();
|
||||
}
|
||||
|
||||
return worldFolder.getAbsoluteFile();
|
||||
}
|
||||
|
||||
private static boolean verifyDataPacksPost(boolean allowRestarting) {
|
||||
try (Stream<IrisData> stream = allPacks()) {
|
||||
boolean bad = stream
|
||||
.map(data -> {
|
||||
Iris.verbose("Checking Pack: " + data.getDataFolder().getPath());
|
||||
var loader = data.getDimensionLoader();
|
||||
return loader.loadAll(loader.getPossibleKeys())
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(ServerConfigurator::verifyDataPackInstalled)
|
||||
.toList()
|
||||
.contains(false);
|
||||
})
|
||||
.toList()
|
||||
.contains(true);
|
||||
if (!bad) return false;
|
||||
}
|
||||
|
||||
|
||||
if (allowRestarting) {
|
||||
restart();
|
||||
} else if (INMS.get().supportsDataPacks()) {
|
||||
Iris.error("============================================================================");
|
||||
Iris.error(C.ITALIC + "You need to restart your server to properly generate custom biomes.");
|
||||
Iris.error(C.ITALIC + "By continuing, Iris will use backup biomes in place of the custom biomes.");
|
||||
Iris.error("----------------------------------------------------------------------------");
|
||||
Iris.error(C.UNDERLINE + "IT IS HIGHLY RECOMMENDED YOU RESTART THE SERVER BEFORE GENERATING!");
|
||||
Iris.error("============================================================================");
|
||||
|
||||
for (Player i : Bukkit.getOnlinePlayers()) {
|
||||
if (i.isOp() || i.hasPermission("iris.all")) {
|
||||
VolmitSender sender = new VolmitSender(i, Iris.instance.getTag("WARNING"));
|
||||
sender.sendMessage("There are some Iris Packs that have custom biomes in them");
|
||||
sender.sendMessage("You need to restart your server to use these packs.");
|
||||
}
|
||||
}
|
||||
|
||||
J.sleep(3000);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void restart() {
|
||||
J.s(() -> {
|
||||
Iris.warn("New data pack entries have been installed in Iris! Restarting server!");
|
||||
Iris.warn("This will only happen when your pack changes (updates/first time setup)");
|
||||
Iris.warn("(You can disable this auto restart in iris settings)");
|
||||
J.s(() -> {
|
||||
Iris.warn("Looks like the restart command didn't work. Stopping the server instead!");
|
||||
Bukkit.shutdown();
|
||||
}, 100);
|
||||
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "restart");
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean verifyDataPackInstalled(IrisDimension dimension) {
|
||||
KSet<String> keys = new KSet<>();
|
||||
boolean warn = false;
|
||||
|
||||
for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) {
|
||||
if (i.isCustom()) {
|
||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||
keys.add(dimension.getLoadKey() + ":" + j.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
String key = getWorld(dimension.getLoader());
|
||||
if (key == null) key = dimension.getLoadKey();
|
||||
else key += "/" + dimension.getLoadKey();
|
||||
|
||||
if (!INMS.get().supportsDataPacks()) {
|
||||
if (!keys.isEmpty()) {
|
||||
Iris.warn("===================================================================================");
|
||||
Iris.warn("Pack " + key + " has " + keys.size() + " custom biome(s). ");
|
||||
Iris.warn("Your server version does not yet support datapacks for iris.");
|
||||
Iris.warn("The world will generate these biomes as backup biomes.");
|
||||
Iris.warn("====================================================================================");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
for (String i : keys) {
|
||||
Object o = INMS.get().getCustomBiomeBaseFor(i);
|
||||
|
||||
if (o == null) {
|
||||
Iris.warn("The Biome " + i + " is not registered on the server.");
|
||||
warn = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (INMS.get().missingDimensionTypes(dimension.getDimensionTypeKey())) {
|
||||
Iris.warn("The Dimension Type for " + dimension.getLoadFile() + " is not registered on the server.");
|
||||
warn = true;
|
||||
}
|
||||
|
||||
if (warn) {
|
||||
Iris.error("The Pack " + key + " is INCAPABLE of generating custom biomes");
|
||||
Iris.error("If not done automatically, restart your server before generating with this pack!");
|
||||
}
|
||||
|
||||
return !warn;
|
||||
}
|
||||
|
||||
public static Stream<IrisData> allPacks() {
|
||||
File[] packs = Iris.instance.getDataFolder("packs").listFiles(File::isDirectory);
|
||||
Stream<File> locals = packs == null ? Stream.empty() : Arrays.stream(packs);
|
||||
return Stream.concat(locals
|
||||
.filter( base -> {
|
||||
var content = new File(base, "dimensions").listFiles();
|
||||
return content != null && content.length > 0;
|
||||
})
|
||||
.map(IrisData::get), IrisWorlds.get().getPacks());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getWorld(@NonNull IrisData data) {
|
||||
String worldContainer = Bukkit.getWorldContainer().getAbsolutePath();
|
||||
if (!worldContainer.endsWith(File.separator)) worldContainer += File.separator;
|
||||
|
||||
String path = data.getDataFolder().getAbsolutePath();
|
||||
if (!path.startsWith(worldContainer)) return null;
|
||||
int l = path.endsWith(File.separator) ? 11 : 10;
|
||||
return path.substring(worldContainer.length(), path.length() - l);
|
||||
}
|
||||
|
||||
public static class DimensionHeight {
|
||||
private final IDataFixer fixer;
|
||||
private final AtomicIntegerArray[] dimensions = new AtomicIntegerArray[3];
|
||||
|
||||
public DimensionHeight(IDataFixer fixer) {
|
||||
this.fixer = fixer;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
dimensions[i] = new AtomicIntegerArray(new int[]{
|
||||
Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public Stream<IrisDimension> merge(IrisData data) {
|
||||
Iris.verbose("Checking Pack: " + data.getDataFolder().getPath());
|
||||
var loader = data.getDimensionLoader();
|
||||
return loader.loadAll(loader.getPossibleKeys())
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.peek(this::merge);
|
||||
}
|
||||
|
||||
public void merge(IrisDimension dimension) {
|
||||
AtomicIntegerArray array = dimensions[dimension.getBaseDimension().ordinal()];
|
||||
array.updateAndGet(0, min -> Math.min(min, dimension.getMinHeight()));
|
||||
array.updateAndGet(1, max -> Math.max(max, dimension.getMaxHeight()));
|
||||
array.updateAndGet(2, logical -> Math.max(logical, dimension.getLogicalHeight()));
|
||||
}
|
||||
|
||||
public String[] jsonStrings() {
|
||||
var dims = IDataFixer.Dimension.values();
|
||||
var arr = new String[3];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
arr[i] = jsonString(dims[i]);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
public String jsonString(IDataFixer.Dimension dimension) {
|
||||
var data = dimensions[dimension.ordinal()];
|
||||
int minY = data.get(0);
|
||||
int maxY = data.get(1);
|
||||
int logicalHeight = data.get(2);
|
||||
if (minY == Integer.MAX_VALUE || maxY == Integer.MIN_VALUE || Integer.MIN_VALUE == logicalHeight)
|
||||
return null;
|
||||
return fixer.createDimension(dimension, minY, maxY - minY, logicalHeight, null).toString(4);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.*;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.DirectorHelp;
|
||||
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.*;
|
||||
|
||||
|
||||
@Director(name = "edit", origin = DirectorOrigin.PLAYER, studio = true, description = "Edit something")
|
||||
public class CommandEdit implements DirectorExecutor {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
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,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.ObjectStudioSaveService;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
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.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 org.bukkit.entity.Player;
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
e.gotoBiome(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;
|
||||
}
|
||||
|
||||
e.gotoRegion(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;
|
||||
}
|
||||
|
||||
e.gotoPOI(type, player(), teleport);
|
||||
}
|
||||
|
||||
@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)) {
|
||||
e.gotoObject(object, player(), teleport);
|
||||
return;
|
||||
}
|
||||
|
||||
sender().sendMessage(C.RED + object + " is not configured in any region/biome object placements.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,793 @@
|
||||
/*
|
||||
* 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.IrisSettings;
|
||||
import art.arcane.iris.core.IrisWorlds;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
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.iris.engine.platform.ChunkReplacementListener;
|
||||
import art.arcane.iris.engine.platform.ChunkReplacementOptions;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.director.DirectorContext;
|
||||
import art.arcane.volmlib.util.director.DirectorParameterHandler;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.DirectorHelp;
|
||||
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.volmlib.util.math.Position2;
|
||||
import art.arcane.iris.util.common.parallel.SyncExecutor;
|
||||
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.World;
|
||||
import org.bukkit.boss.BarColor;
|
||||
import org.bukkit.boss.BarStyle;
|
||||
import org.bukkit.boss.BossBar;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorCompletionService;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
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 {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
private CommandStudio studio;
|
||||
private CommandPregen pregen;
|
||||
private CommandSettings settings;
|
||||
private CommandObject object;
|
||||
private CommandWhat what;
|
||||
private CommandEdit edit;
|
||||
private CommandDeveloper developer;
|
||||
private CommandPack pack;
|
||||
private CommandFind find;
|
||||
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,
|
||||
@Param(aliases = {"remove-others", "removeothers"}, description = "When main-world is true, remove other Iris worlds from bukkit.yml and queue deletion on startup", defaultValue = "false")
|
||||
boolean removeOthers,
|
||||
@Param(aliases = {"remove-worlds", "removeworlds"}, description = "Comma-separated world names to remove from Iris control and delete on next startup (main-world only)", defaultValue = "none")
|
||||
String removeWorlds
|
||||
) {
|
||||
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 (new File(Bukkit.getWorldContainer(), 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 IrisDimensions/" + resolvedType);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!main && (removeOthers || hasExplicitCleanupWorlds(removeWorlds))) {
|
||||
sender().sendMessage(C.YELLOW + "remove-others/remove-worlds only apply when main-world=true. Ignoring cleanup options.");
|
||||
removeOthers = false;
|
||||
removeWorlds = "none";
|
||||
}
|
||||
|
||||
if (J.isFolia()) {
|
||||
if (stageFoliaWorldCreation(name, dimension, seed, main, removeOthers, removeWorlds)) {
|
||||
sender().sendMessage(C.GREEN + "World staging completed. Restart the server to generate/load \"" + name + "\".");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
worldCreation = true;
|
||||
IrisToolbelt.createWorld()
|
||||
.dimension(dimension.getLoadKey())
|
||||
.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;
|
||||
}
|
||||
|
||||
if (main && !applyMainWorldCleanup(name, removeOthers, removeWorlds)) {
|
||||
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 worlds = Bukkit.getWorldContainer();
|
||||
var data = ServerProperties.DATA;
|
||||
try (var in = new FileInputStream(ServerProperties.SERVER_PROPERTIES)) {
|
||||
data.load(in);
|
||||
}
|
||||
|
||||
File oldWorldFolder = new File(worlds, ServerProperties.LEVEL_NAME);
|
||||
File newWorldFolder = new File(worlds, newName);
|
||||
if (!newWorldFolder.exists() && !newWorldFolder.mkdirs()) {
|
||||
Iris.warn("Could not create target main world folder: " + newWorldFolder.getAbsolutePath());
|
||||
}
|
||||
|
||||
for (String sub : List.of("datapacks", "playerdata", "advancements", "stats")) {
|
||||
File source = new File(oldWorldFolder, sub);
|
||||
if (!source.exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IO.copyDirectory(source.toPath(), new File(newWorldFolder, sub).toPath());
|
||||
}
|
||||
|
||||
data.setProperty("level-name", newName);
|
||||
try (var 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, boolean removeOthers, String removeWorlds) {
|
||||
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 = new File(Bukkit.getWorldContainer(), name);
|
||||
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(sender(), dimension.getLoadKey(), 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;
|
||||
}
|
||||
|
||||
if (!applyMainWorldCleanup(name, removeOthers, removeWorlds)) {
|
||||
sender().sendMessage(C.RED + "World was staged, but failed to apply main-world cleanup options.");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean applyMainWorldCleanup(String mainWorld, boolean removeOthers, String removeWorlds) {
|
||||
Set<String> targets = resolveCleanupTargets(mainWorld, removeOthers, removeWorlds);
|
||||
if (targets.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
sender().sendMessage(C.YELLOW + "Applying main-world cleanup for " + targets.size() + " world(s).");
|
||||
|
||||
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
|
||||
ConfigurationSection worlds = yml.getConfigurationSection("worlds");
|
||||
|
||||
Set<String> removedFromBukkit = new LinkedHashSet<>();
|
||||
Set<String> notRemoved = new LinkedHashSet<>();
|
||||
for (String target : targets) {
|
||||
String key = findWorldKeyIgnoreCase(worlds, target);
|
||||
if (key == null) {
|
||||
notRemoved.add(target);
|
||||
continue;
|
||||
}
|
||||
|
||||
String generator = worlds.getString(key + ".generator");
|
||||
if (generator == null || !(generator.equalsIgnoreCase("iris") || generator.startsWith("Iris:"))) {
|
||||
notRemoved.add(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
worlds.set(key, null);
|
||||
removedFromBukkit.add(key);
|
||||
}
|
||||
|
||||
try {
|
||||
if (worlds != null && worlds.getKeys(false).isEmpty()) {
|
||||
yml.set("worlds", null);
|
||||
}
|
||||
|
||||
if (!removedFromBukkit.isEmpty()) {
|
||||
yml.save(BUKKIT_YML);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
sender().sendMessage(C.RED + "Failed to update bukkit.yml while applying cleanup: " + e.getMessage());
|
||||
Iris.reportError(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
int queued = Iris.queueWorldDeletionOnStartup(targets);
|
||||
if (queued > 0) {
|
||||
sender().sendMessage(C.GREEN + "Queued " + queued + " world folder(s) for deletion on next startup.");
|
||||
} else {
|
||||
sender().sendMessage(C.YELLOW + "Cleanup queue already contained the requested world folder(s).");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
sender().sendMessage(C.RED + "Failed to queue startup world deletions: " + e.getMessage());
|
||||
Iris.reportError(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!removedFromBukkit.isEmpty()) {
|
||||
sender().sendMessage(C.GREEN + "Removed from Iris control in bukkit.yml: " + String.join(", ", removedFromBukkit));
|
||||
}
|
||||
|
||||
if (!notRemoved.isEmpty()) {
|
||||
sender().sendMessage(C.YELLOW + "Skipped from bukkit.yml removal (not found or non-Iris generator): " + String.join(", ", notRemoved));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Set<String> resolveCleanupTargets(String mainWorld, boolean removeOthers, String removeWorlds) {
|
||||
Set<String> targets = new LinkedHashSet<>();
|
||||
if (removeOthers) {
|
||||
IrisWorlds.readBukkitWorlds().keySet().stream()
|
||||
.filter(world -> !world.equalsIgnoreCase(mainWorld))
|
||||
.forEach(targets::add);
|
||||
}
|
||||
|
||||
if (hasExplicitCleanupWorlds(removeWorlds)) {
|
||||
for (String raw : removeWorlds.split("[,;\\s]+")) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (raw.equalsIgnoreCase(mainWorld)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
targets.add(raw.trim());
|
||||
}
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
private static boolean hasExplicitCleanupWorlds(String removeWorlds) {
|
||||
if (removeWorlds == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String trimmed = removeWorlds.trim();
|
||||
return !trimmed.isEmpty() && !trimmed.equalsIgnoreCase("none");
|
||||
}
|
||||
|
||||
private static String findWorldKeyIgnoreCase(ConfigurationSection worlds, String requested) {
|
||||
if (worlds == null || requested == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (worlds.contains(requested)) {
|
||||
return requested;
|
||||
}
|
||||
|
||||
for (String key : worlds.getKeys(false)) {
|
||||
if (key.equalsIgnoreCase(requested)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
target.teleport(world.getSpawnLocation());
|
||||
new VolmitSender(target).sendMessage(C.GREEN + "You have been teleported to " + world.getName() + ".");
|
||||
}
|
||||
}.runTask(Iris.instance);
|
||||
}
|
||||
|
||||
@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 = {"del", "rm", "delete"}, 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(
|
||||
@Param(name = "on", description = "Whether or not debug should be on", defaultValue = "other")
|
||||
Boolean on
|
||||
) {
|
||||
boolean to = on == null ? !IrisSettings.get().getGeneral().isDebug() : on;
|
||||
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", defaultValue = "overworld", 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
|
||||
) {
|
||||
sender().sendMessage(C.GREEN + "Downloading pack: " + pack + "/" + branch + (overwrite ? " overwriting" : ""));
|
||||
if (pack.equals("overworld")) {
|
||||
String url = "https://github.com/IrisDimensions/overworld/releases/download/" + INMS.OVERWORLD_TAG + "/overworld.zip";
|
||||
Iris.service(StudioSVC.class).downloadRelease(sender(), url, overwrite);
|
||||
} else {
|
||||
Iris.service(StudioSVC.class).downloadSearch(sender(), "IrisDimensions/" + pack + "/" + branch, overwrite);
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
) {
|
||||
World worldloaded = Bukkit.getWorld(world);
|
||||
worldNameToCheck = world;
|
||||
boolean worldExists = doesWorldExist(worldNameToCheck);
|
||||
WorldEngine = world;
|
||||
|
||||
if (!worldExists) {
|
||||
sender().sendMessage(C.YELLOW + world + " Doesnt exist on the server.");
|
||||
return;
|
||||
}
|
||||
|
||||
String pathtodim = world + File.separator +"iris"+File.separator +"pack"+File.separator +"dimensions"+File.separator;
|
||||
File directory = new File(Bukkit.getWorldContainer(), pathtodim);
|
||||
|
||||
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 worldContainer = Bukkit.getWorldContainer();
|
||||
File worldDirectory = new File(worldContainer, 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);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,881 @@
|
||||
/*
|
||||
* 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.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.*;
|
||||
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.DirectorHelp;
|
||||
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 io.papermc.lib.PaperLib;
|
||||
import org.bukkit.*;
|
||||
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.*;
|
||||
|
||||
@Director(name = "object", aliases = "o", origin = DirectorOrigin.PLAYER, studio = true, description = "Iris object manipulation")
|
||||
public class CommandObject implements DirectorExecutor {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
@Director(description = "Open an object studio world (grid of every object; dimension optional, defaults to all packs)", aliases = {"std", "s"}, 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, () -> {
|
||||
PaperLib.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, BlockData d) {
|
||||
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 BlockData get(int x, int y, int z) {
|
||||
return 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 = queue.next();
|
||||
|
||||
//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, studio = false)
|
||||
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", studio = true, 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) {
|
||||
ItemStack newWand = WandSVC.createWand(block.clone().subtract(o.getCenter()).add(o.getW() - 1,
|
||||
o.getH() + o.getCenter().clone().getY() - 1, o.getD() - 1), block.clone().subtract(o.getCenter().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", aliases = "-")
|
||||
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 = "-")
|
||||
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, studio = true)
|
||||
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,169 @@
|
||||
/*
|
||||
* 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.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.director.DirectorHelp;
|
||||
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;
|
||||
|
||||
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance")
|
||||
public class CommandPack implements DirectorExecutor {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
@Director(description = "Validate a pack (or all packs) and re-publish results", aliases = {"v", "check"})
|
||||
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 = new File(packsRoot, pack);
|
||||
if (!target.isDirectory()) {
|
||||
s.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
|
||||
return;
|
||||
}
|
||||
runValidate(s, target);
|
||||
}
|
||||
|
||||
@Director(description = "Restore most recent trashed files for a pack", aliases = {"r", "undelete"})
|
||||
public void restore(
|
||||
@Param(description = "The pack folder name to restore")
|
||||
String pack
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
if (pack == null || pack.isBlank()) {
|
||||
s.sendMessage(C.RED + "You must specify a pack name.");
|
||||
return;
|
||||
}
|
||||
File packFolder = new File(Iris.instance.getDataFolder("packs"), pack);
|
||||
if (!packFolder.isDirectory()) {
|
||||
s.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
|
||||
return;
|
||||
}
|
||||
int restored = PackValidator.restoreTrash(packFolder);
|
||||
if (restored == 0) {
|
||||
s.sendMessage(C.YELLOW + "Nothing to restore for pack '" + pack + "'.");
|
||||
return;
|
||||
}
|
||||
s.sendMessage(C.GREEN + "Restored " + restored + " file(s) from the most recent trash dump for pack '" + pack + "'.");
|
||||
s.sendMessage(C.GRAY + "Re-run /iris pack validate " + pack + " to re-check.");
|
||||
}
|
||||
|
||||
@Director(description = "Show cached validation status for a pack", aliases = {"s", "info"})
|
||||
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()
|
||||
+ ", trashed=" + result.getRemovedUnusedFiles().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()
|
||||
+ ", trashed=" + result.getRemovedUnusedFiles().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).");
|
||||
}
|
||||
int tMax = Math.min(10, result.getRemovedUnusedFiles().size());
|
||||
for (int i = 0; i < tMax; i++) {
|
||||
s.sendMessage(C.GRAY + " ~ trashed " + result.getRemovedUnusedFiles().get(i));
|
||||
}
|
||||
if (result.getRemovedUnusedFiles().size() > tMax) {
|
||||
s.sendMessage(C.GRAY + " ... and " + (result.getRemovedUnusedFiles().size() - tMax) + " more trashed file(s).");
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
-31
@@ -16,66 +16,74 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.commands;
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.gui.PregeneratorJob;
|
||||
import com.volmit.iris.core.pregenerator.PregenTask;
|
||||
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||
import com.volmit.iris.util.decree.DecreeExecutor;
|
||||
import com.volmit.iris.util.decree.annotations.Decree;
|
||||
import com.volmit.iris.util.decree.annotations.Param;
|
||||
import com.volmit.iris.util.format.C;
|
||||
import com.volmit.iris.util.math.Position2;
|
||||
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.iris.util.common.director.DirectorHelp;
|
||||
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.math.Position2;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
@Decree(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!")
|
||||
public class CommandPregen implements DecreeExecutor {
|
||||
@Decree(description = "Pregenerate a world")
|
||||
@Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!")
|
||||
public class CommandPregen implements DirectorExecutor {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
@Director(description = "Pregenerate a world")
|
||||
public void start(
|
||||
@Param(description = "The radius of the pregen in blocks", aliases = "size")
|
||||
@Param(description = "The radius of the pregen in blocks", aliases = "size")
|
||||
int radius,
|
||||
@Param(description = "The world to pregen", contextual = true)
|
||||
@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(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
|
||||
) {
|
||||
try {
|
||||
if(sender().isPlayer() && access() == null) {
|
||||
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.");
|
||||
}
|
||||
radius = Math.max(radius, 1024);
|
||||
int w = (radius >> 9 + 1) * 2;
|
||||
IrisToolbelt.pregenerate(PregenTask
|
||||
.builder()
|
||||
.center(new Position2(center))
|
||||
.width(w)
|
||||
.height(w)
|
||||
.build(), world);
|
||||
.builder()
|
||||
.center(new Position2(center.getBlockX(), center.getBlockZ()))
|
||||
.gui(gui)
|
||||
.radiusX(radius)
|
||||
.radiusZ(radius)
|
||||
.build(), 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) {
|
||||
} catch (Throwable e) {
|
||||
sender().sendMessage(C.RED + "Epic fail. See console.");
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Stop the active pregeneration task", aliases = "x")
|
||||
@Director(description = "Stop the active pregeneration task", aliases = "x")
|
||||
public void stop() {
|
||||
if(PregeneratorJob.shutdownInstance()) {
|
||||
sender().sendMessage(C.GREEN + "Stopped pregeneration task");
|
||||
if (PregeneratorJob.shutdownInstance()) {
|
||||
Iris.info( C.BLUE + "Finishing up mca region...");
|
||||
} else {
|
||||
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"})
|
||||
@Director(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"})
|
||||
public void pause() {
|
||||
if(PregeneratorJob.pauseResume()) {
|
||||
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.");
|
||||
+3
-3
@@ -16,10 +16,10 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.commands;
|
||||
package art.arcane.iris.core.commands;
|
||||
|
||||
import com.volmit.iris.util.decree.DecreeExecutor;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
|
||||
public class CommandSettings implements DecreeExecutor {
|
||||
public class CommandSettings implements DirectorExecutor {
|
||||
|
||||
}
|
||||
+320
-368
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* 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.edit.BlockSignal;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.DirectorHelp;
|
||||
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.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Director(name = "what", origin = DirectorOrigin.PLAYER, studio = true, description = "Iris What?")
|
||||
public class CommandWhat implements DirectorExecutor {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
@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().getBlockZ() / 16, player().getLocation().getBlockZ() / 16);
|
||||
IrisRegion r = engine().getRegion(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 (B.isStorage(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Storage Block (Loot Capable)");
|
||||
}
|
||||
|
||||
if (B.isLit(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Lit Block (Light Capable)");
|
||||
}
|
||||
|
||||
if (B.isFoliage(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Foliage Block");
|
||||
}
|
||||
|
||||
if (B.isDecorant(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Decorant Block");
|
||||
}
|
||||
|
||||
if (B.isFluid(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Fluid Block");
|
||||
}
|
||||
|
||||
if (B.isFoliagePlantable(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Plantable Foliage Block");
|
||||
}
|
||||
|
||||
if (B.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) -> i.toLocation(c.getWorld())).forEach((i) -> {
|
||||
BlockSignal.of(i.getWorld(), i.getBlockX(), i.getBlockY(), i.getBlockZ(), 100);
|
||||
v.incrementAndGet();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sender().sendMessage("Found " + v.get() + " Nearby Markers (" + marker + ")");
|
||||
} else {
|
||||
sender().sendMessage(C.IRIS + "Iris worlds only.");
|
||||
}
|
||||
}
|
||||
|
||||
private NamespacedKey resolveBiomeKey(Biome biome) {
|
||||
Object keyOrNullValue = invokeNoThrow(biome, "getKeyOrNull");
|
||||
if (keyOrNullValue instanceof NamespacedKey namespacedKey) {
|
||||
return namespacedKey;
|
||||
}
|
||||
|
||||
Object keyOrThrowValue = invokeNoThrow(biome, "getKeyOrThrow");
|
||||
if (keyOrThrowValue instanceof NamespacedKey namespacedKey) {
|
||||
return namespacedKey;
|
||||
}
|
||||
|
||||
Object keyValue = invokeNoThrow(biome, "getKey");
|
||||
if (keyValue instanceof NamespacedKey namespacedKey) {
|
||||
return namespacedKey;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Object invokeNoThrow(Biome biome, String methodName) {
|
||||
if (biome == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Method method = biome.getClass().getMethod(methodName);
|
||||
return method.invoke(biome);
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.edit;
|
||||
package art.arcane.iris.core.edit;
|
||||
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.scheduling.SR;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.FallingBlock;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@SuppressWarnings("InstantiationOfUtilityClass")
|
||||
public class BlockSignal {
|
||||
public static final AtomicInteger active = new AtomicInteger(0);
|
||||
|
||||
public BlockSignal(Block block, int ticks) {
|
||||
active.incrementAndGet();
|
||||
Location tg = block.getLocation().clone().add(0.5, 0, 0.5);
|
||||
FallingBlock e = block.getWorld().spawnFallingBlock(tg, block.getBlockData());
|
||||
e.setGravity(false);
|
||||
e.setInvulnerable(true);
|
||||
e.setGlowing(true);
|
||||
e.setDropItem(false);
|
||||
e.setHurtEntities(false);
|
||||
e.setSilent(true);
|
||||
e.setTicksLived(1);
|
||||
e.setVelocity(new Vector(0, 0, 0));
|
||||
Location blockLocation = block.getLocation();
|
||||
Runnable removeTask = () -> {
|
||||
if (!J.runEntity(e, e::remove) && !e.isDead()) {
|
||||
e.remove();
|
||||
}
|
||||
active.decrementAndGet();
|
||||
sendBlockRefresh(block);
|
||||
};
|
||||
if (!J.runAt(blockLocation, removeTask, ticks)) {
|
||||
if (!J.isFolia()) {
|
||||
J.s(removeTask, ticks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void of(Block block, int ticks) {
|
||||
if (block == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
of(block.getWorld(), block.getX(), block.getY(), block.getZ(), ticks);
|
||||
}
|
||||
|
||||
public static void of(Block block) {
|
||||
of(block, 100);
|
||||
}
|
||||
|
||||
public static void of(World world, int x, int y, int z, int ticks) {
|
||||
if (world == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Location location = new Location(world, x, y, z);
|
||||
Runnable createTask = () -> new BlockSignal(world.getBlockAt(x, y, z), ticks);
|
||||
if (!J.runAt(location, createTask)) {
|
||||
if (!J.isFolia()) {
|
||||
J.s(createTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void of(World world, int x, int y, int z) {
|
||||
of(world, x, y, z, 100);
|
||||
}
|
||||
|
||||
public static Runnable forever(Block block) {
|
||||
Location tg = block.getLocation().clone().add(0.5, 0, 0.5).clone();
|
||||
FallingBlock e = block.getWorld().spawnFallingBlock(tg.clone(), block.getBlockData());
|
||||
e.setGravity(false);
|
||||
e.setInvulnerable(true);
|
||||
e.setGlowing(true);
|
||||
e.teleport(tg.clone());
|
||||
e.setDropItem(false);
|
||||
e.setHurtEntities(false);
|
||||
e.setSilent(true);
|
||||
e.setTicksLived(1);
|
||||
e.setVelocity(new Vector(0, 0, 0));
|
||||
|
||||
new SR(20) {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!J.runEntity(e, () -> {
|
||||
if (e.isDead()) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
e.setTicksLived(1);
|
||||
e.teleport(tg.clone());
|
||||
e.setVelocity(new Vector(0, 0, 0));
|
||||
})) {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return () -> {
|
||||
if (!J.runEntity(e, e::remove) && !e.isDead()) {
|
||||
e.remove();
|
||||
}
|
||||
Location blockLocation = block.getLocation();
|
||||
Runnable refreshTask = () -> sendBlockRefresh(block);
|
||||
if (!J.runAt(blockLocation, refreshTask)) {
|
||||
refreshTask.run();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void sendBlockRefresh(Block block) {
|
||||
if (block == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Location location = block.getLocation();
|
||||
BlockData blockData = block.getBlockData();
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (!player.getWorld().equals(location.getWorld())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
J.runEntity(player, () -> player.sendBlockChange(location, blockData));
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-6
@@ -16,9 +16,9 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.edit;
|
||||
package art.arcane.iris.core.edit;
|
||||
|
||||
import com.volmit.iris.util.math.M;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
@@ -51,10 +51,13 @@ public class BukkitBlockEditor implements BlockEditor {
|
||||
return M.ms();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void setBiome(int x, int z, Biome b) {
|
||||
world.setBiome(x, z, b);
|
||||
int minHeight = world.getMinHeight();
|
||||
int maxHeight = world.getMaxHeight();
|
||||
for (int y = minHeight; y < maxHeight; y++) {
|
||||
world.setBiome(x, y, z, b);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,9 +70,8 @@ public class BukkitBlockEditor implements BlockEditor {
|
||||
return world.getBiome(x, y, z);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public Biome getBiome(int x, int z) {
|
||||
return world.getBiome(x, z);
|
||||
return world.getBiome(x, world.getMinHeight(), z);
|
||||
}
|
||||
}
|
||||
+38
-22
@@ -16,18 +16,20 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.edit;
|
||||
package art.arcane.iris.core.edit;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.math.BlockPosition;
|
||||
import com.volmit.iris.util.math.M;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
import com.volmit.iris.util.plugin.VolmitSender;
|
||||
import com.volmit.iris.util.scheduling.J;
|
||||
import art.arcane.iris.Iris;
|
||||
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.KList;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import lombok.Data;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
@@ -48,13 +50,14 @@ public class DustRevealer {
|
||||
this.key = key;
|
||||
this.hits = hits;
|
||||
|
||||
J.s(() -> {
|
||||
new BlockSignal(world.getBlockAt(block.getX(), block.getY(), block.getZ()), 7);
|
||||
if(M.r(0.25)) {
|
||||
Location blockLocation = block.toBlock(world).getLocation();
|
||||
Runnable revealTask = () -> {
|
||||
BlockSignal.of(world, block.getX(), block.getY(), block.getZ(), 10);
|
||||
if (M.r(0.25)) {
|
||||
world.playSound(block.toBlock(world).getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 1f, RNG.r.f(0.2f, 2f));
|
||||
}
|
||||
J.a(() -> {
|
||||
while(BlockSignal.active.get() > 128) {
|
||||
while (BlockSignal.active.get() > 128) {
|
||||
J.sleep(5);
|
||||
}
|
||||
|
||||
@@ -85,22 +88,31 @@ public class DustRevealer {
|
||||
is(new BlockPosition(block.getX() + 1, block.getY() + 1, block.getZ() + 1));
|
||||
is(new BlockPosition(block.getX() + 1, block.getY() - 1, block.getZ() - 1));
|
||||
is(new BlockPosition(block.getX() + 1, block.getY() - 1, block.getZ() + 1));
|
||||
} catch(Throwable e) {
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}, RNG.r.i(2, 8));
|
||||
};
|
||||
int delay = RNG.r.i(2, 8);
|
||||
if (!J.runAt(blockLocation, revealTask, delay)) {
|
||||
if (!J.isFolia()) {
|
||||
J.s(revealTask, delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void spawn(Block block, VolmitSender sender) {
|
||||
World world = block.getWorld();
|
||||
Engine access = IrisToolbelt.access(world).getEngine();
|
||||
PlatformChunkGenerator generator = IrisToolbelt.access(world);
|
||||
if (generator == null) {
|
||||
return;
|
||||
}
|
||||
Engine access = generator.getEngine();
|
||||
|
||||
if(access != null) {
|
||||
String a = access.getObjectPlacementKey(block.getX(), block.getY(), block.getZ());
|
||||
|
||||
if(a != null) {
|
||||
if (access != null) {
|
||||
String a = access.getObjectPlacementKey(block.getX(), block.getY() - block.getWorld().getMinHeight(), block.getZ());
|
||||
if (a != null) {
|
||||
world.playSound(block.getLocation(), Sound.ITEM_LODESTONE_COMPASS_LOCK, 1f, 0.1f);
|
||||
|
||||
sender.sendMessage("Found object " + a);
|
||||
@@ -112,7 +124,11 @@ public class DustRevealer {
|
||||
}
|
||||
|
||||
private boolean is(BlockPosition a) {
|
||||
if(isValidTry(a) && engine.getObjectPlacementKey(a.getX(), a.getY(), a.getZ()) != null && engine.getObjectPlacementKey(a.getX(), a.getY(), a.getZ()).equals(key)) {
|
||||
if (a.getY() < world.getMinHeight() || a.getY() >= world.getMaxHeight()) {
|
||||
return false;
|
||||
}
|
||||
int betterY = a.getY() - world.getMinHeight();
|
||||
if (isValidTry(a) && engine.getObjectPlacementKey(a.getX(), betterY, a.getZ()) != null && engine.getObjectPlacementKey(a.getX(), betterY, a.getZ()).equals(key)) {
|
||||
hits.add(a);
|
||||
new DustRevealer(engine, world, a, key, hits);
|
||||
return true;
|
||||
+2
-2
@@ -16,9 +16,9 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.events;
|
||||
package art.arcane.iris.core.events;
|
||||
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
+2
-2
@@ -16,9 +16,9 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.events;
|
||||
package art.arcane.iris.core.events;
|
||||
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
public class IrisEngineHotloadEvent extends IrisEngineEvent {
|
||||
@@ -0,0 +1,141 @@
|
||||
package art.arcane.iris.core.events;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.InventorySlotType;
|
||||
import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import lombok.Getter;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.world.LootGenerateEvent;
|
||||
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.loot.LootContext;
|
||||
import org.bukkit.loot.LootTable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
@Getter
|
||||
public class IrisLootEvent extends Event {
|
||||
private static final HandlerList handlers = new HandlerList();
|
||||
private static final LootTable EMPTY = new LootTable() {
|
||||
@NotNull
|
||||
@Override
|
||||
public NamespacedKey getKey() {
|
||||
return new NamespacedKey(Iris.instance, "empty");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Collection<ItemStack> populateLoot(@Nullable Random random, @NotNull LootContext context) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillInventory(@NotNull Inventory inventory, @Nullable Random random, @NotNull LootContext context) {
|
||||
}
|
||||
};
|
||||
|
||||
private final Engine engine;
|
||||
private final Block block;
|
||||
private final InventorySlotType slot;
|
||||
private final KList<IrisLootTable> tables;
|
||||
|
||||
/**
|
||||
* Constructor for IrisLootEvent with mode selection.
|
||||
*
|
||||
* @param engine The engine instance.
|
||||
* @param block The block associated with the event.
|
||||
* @param slot The inventory slot type.
|
||||
* @param tables The list of IrisLootTables. (mutable*)
|
||||
*/
|
||||
public IrisLootEvent(Engine engine, Block block, InventorySlotType slot, KList<IrisLootTable> tables) {
|
||||
this.engine = engine;
|
||||
this.block = block;
|
||||
this.slot = slot;
|
||||
this.tables = tables;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Required method to get the HandlerList for this event.
|
||||
*
|
||||
* @return The HandlerList.
|
||||
*/
|
||||
public static HandlerList getHandlerList() {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the corresponding Bukkit loot event.
|
||||
* This method integrates your custom IrisLootTables with Bukkit's LootGenerateEvent,
|
||||
* allowing other plugins to modify or cancel the loot generation.
|
||||
*
|
||||
* @return true when the event was canceled
|
||||
*/
|
||||
public static boolean callLootEvent(KList<ItemStack> loot, Inventory inv, World world, int x, int y, int z) {
|
||||
InventoryHolder holder = inv.getHolder();
|
||||
Location loc = new Location(world, x, y, z);
|
||||
if (holder == null) {
|
||||
holder = new InventoryHolder() {
|
||||
@NotNull
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inv;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
LootContext context = new LootContext.Builder(loc).build();
|
||||
LootGenerateEvent event = new LootGenerateEvent(world, null, holder, EMPTY, context, loot, true);
|
||||
if (!Bukkit.isPrimaryThread()) {
|
||||
Iris.warn("LootGenerateEvent was not called on the main thread, please report this issue.");
|
||||
Thread.dumpStack();
|
||||
J.sfut(() -> {
|
||||
try {
|
||||
Bukkit.getPluginManager().callEvent(event);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("LootGenerateEvent dispatch failed at "
|
||||
+ world.getName() + " [" + x + "," + y + "," + z + "].", e);
|
||||
if (e instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (e instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}).join();
|
||||
} else {
|
||||
try {
|
||||
Bukkit.getPluginManager().callEvent(event);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("LootGenerateEvent dispatch failed at "
|
||||
+ world.getName() + " [" + x + "," + y + "," + z + "].", e);
|
||||
if (e instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (e instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
return event.isCancelled();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
/*
|
||||
* 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.IrisSettings;
|
||||
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.function.Function2;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.math.RollingSequence;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.iris.util.common.parallel.BurstExecutor;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, Listener {
|
||||
|
||||
private static final long serialVersionUID = 2094606939770332040L;
|
||||
private static final Color BG = new Color(24, 24, 28);
|
||||
private static final Color SIDEBAR_BG = new Color(20, 20, 24);
|
||||
private static final Color SIDEBAR_SELECTED = new Color(40, 50, 70);
|
||||
private static final Color SIDEBAR_ITEM_COLOR = new Color(170, 170, 185);
|
||||
private static final Color SEARCH_BG = new Color(30, 30, 38);
|
||||
private static final Color SEARCH_FG = new Color(180, 180, 190);
|
||||
private static final Color STATUS_BG = new Color(32, 32, 38, 230);
|
||||
private static final Color STATUS_TEXT = new Color(180, 180, 190);
|
||||
private static final Color ACCENT = new Color(90, 140, 255);
|
||||
private static final Color SEPARATOR = new Color(40, 40, 50);
|
||||
private static final Font STATUS_FONT = new Font(Font.MONOSPACED, Font.PLAIN, 12);
|
||||
private static final Font SIDEBAR_HEADER_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 11);
|
||||
private static final Font SIDEBAR_ITEM_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
|
||||
private static final Font SEARCH_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
|
||||
private static final int SIDEBAR_WIDTH = 240;
|
||||
private static final int[] HSB_LUT = new int[256];
|
||||
|
||||
private static final String[] CATEGORY_ORDER = {
|
||||
"Pack Generators", "Simplex", "Perlin", "Cellular", "Iris", "Clover",
|
||||
"Hexagon", "Vascular", "Globe", "Cubic", "Fractal", "Static",
|
||||
"Nowhere", "Sierpinski", "Utility", "Other"
|
||||
};
|
||||
|
||||
static {
|
||||
for (int i = 0; i < 256; i++) {
|
||||
float n = i / 255f;
|
||||
HSB_LUT[i] = Color.HSBtoRGB(0.666f - n * 0.666f, 1f, 1f - n * 0.8f);
|
||||
}
|
||||
}
|
||||
|
||||
private final RollingSequence fpsHistory = new RollingSequence(60);
|
||||
private final boolean colorMode = IrisSettings.get().getGui().colorMode;
|
||||
private final MultiBurst gx = MultiBurst.burst;
|
||||
private double scale = 1;
|
||||
private double animScale = 10;
|
||||
private double ox = 0;
|
||||
private double oz = 0;
|
||||
private double animOx = 0;
|
||||
private double animOz = 0;
|
||||
private double lastMouseX = Double.MAX_VALUE;
|
||||
private double lastMouseZ = Double.MAX_VALUE;
|
||||
private double time = 0;
|
||||
private double animTime = 0;
|
||||
private int imgWidth = 0;
|
||||
private int imgHeight = 0;
|
||||
private BufferedImage img;
|
||||
private CNG cng = NoiseStyle.STATIC.create(new RNG(RNG.r.nextLong()));
|
||||
private Function2<Double, Double, Double> generator;
|
||||
private Supplier<Function2<Double, Double, Double>> loader;
|
||||
private String currentName = "STATIC";
|
||||
|
||||
public NoiseExplorerGUI() {
|
||||
Iris.instance.registerListener(this);
|
||||
setBackground(BG);
|
||||
addMouseWheelListener(this);
|
||||
addMouseMotionListener(new MouseMotionListener() {
|
||||
@Override
|
||||
public void mouseMoved(MouseEvent e) {
|
||||
Point cp = e.getPoint();
|
||||
lastMouseX = cp.getX();
|
||||
lastMouseZ = cp.getY();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
Point cp = e.getPoint();
|
||||
ox += (lastMouseX - cp.getX()) * scale;
|
||||
oz += (lastMouseZ - cp.getY()) * scale;
|
||||
lastMouseX = cp.getX();
|
||||
lastMouseZ = cp.getY();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void launch() {
|
||||
Engine engine = findActiveEngine();
|
||||
EventQueue.invokeLater(() -> {
|
||||
NoiseExplorerGUI nv = new NoiseExplorerGUI();
|
||||
buildFrame("Noise Explorer", nv, engine, null, null);
|
||||
});
|
||||
}
|
||||
|
||||
public static void launch(Supplier<Function2<Double, Double, Double>> gen, String genName) {
|
||||
Engine engine = findActiveEngine();
|
||||
EventQueue.invokeLater(() -> {
|
||||
NoiseExplorerGUI nv = new NoiseExplorerGUI();
|
||||
nv.loader = gen;
|
||||
nv.generator = gen.get();
|
||||
nv.currentName = genName;
|
||||
buildFrame("Noise Explorer: " + genName, nv, engine, gen, genName);
|
||||
});
|
||||
}
|
||||
|
||||
private static Engine findActiveEngine() {
|
||||
try {
|
||||
for (World w : new ArrayList<>(Bukkit.getWorlds())) {
|
||||
try {
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(w);
|
||||
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
|
||||
return access.getEngine();
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static JFrame buildFrame(String title, NoiseExplorerGUI nv, Engine engine,
|
||||
Supplier<Function2<Double, Double, Double>> customGen, String customName) {
|
||||
JFrame frame = new JFrame(title);
|
||||
frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
|
||||
frame.getContentPane().setBackground(BG);
|
||||
frame.setLayout(new BorderLayout());
|
||||
|
||||
JPanel sidebar = buildSidebar(nv, engine, customGen, customName);
|
||||
frame.add(sidebar, BorderLayout.WEST);
|
||||
frame.add(nv, BorderLayout.CENTER);
|
||||
|
||||
frame.setSize(1440, 820);
|
||||
frame.setMinimumSize(new Dimension(640, 480));
|
||||
frame.setLocationRelativeTo(null);
|
||||
frame.setVisible(true);
|
||||
frame.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
Iris.instance.unregisterListener(nv);
|
||||
}
|
||||
});
|
||||
return frame;
|
||||
}
|
||||
|
||||
private static JPanel buildSidebar(NoiseExplorerGUI nv, Engine engine,
|
||||
Supplier<Function2<Double, Double, Double>> customGen, String customName) {
|
||||
JPanel sidebar = new JPanel(new BorderLayout());
|
||||
sidebar.setPreferredSize(new Dimension(SIDEBAR_WIDTH, 0));
|
||||
sidebar.setBackground(SIDEBAR_BG);
|
||||
sidebar.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, SEPARATOR));
|
||||
|
||||
JTextField search = new JTextField();
|
||||
search.setBackground(SEARCH_BG);
|
||||
search.setForeground(SEARCH_FG);
|
||||
search.setCaretColor(SEARCH_FG);
|
||||
search.setFont(SEARCH_FONT);
|
||||
search.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 0, 1, 0, SEPARATOR),
|
||||
BorderFactory.createEmptyBorder(8, 10, 8, 10)
|
||||
));
|
||||
search.putClientProperty("JTextField.placeholderText", "Search...");
|
||||
|
||||
LinkedHashMap<String, List<ListItem>> categories = buildCategoryMap(nv, engine, customGen, customName);
|
||||
DefaultListModel<ListItem> model = new DefaultListModel<>();
|
||||
populateModel(model, categories, "");
|
||||
|
||||
JList<ListItem> list = new JList<>(model);
|
||||
list.setBackground(SIDEBAR_BG);
|
||||
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
list.setCellRenderer(new SidebarCellRenderer());
|
||||
list.setFixedCellHeight(-1);
|
||||
|
||||
list.addListSelectionListener(e -> {
|
||||
if (e.getValueIsAdjusting()) return;
|
||||
ListItem selected = list.getSelectedValue();
|
||||
if (selected != null && !selected.header && selected.action != null) {
|
||||
selected.action.run();
|
||||
}
|
||||
});
|
||||
|
||||
search.getDocument().addDocumentListener(new DocumentListener() {
|
||||
private void filter() {
|
||||
String text = search.getText().trim();
|
||||
populateModel(model, categories, text);
|
||||
}
|
||||
|
||||
public void insertUpdate(DocumentEvent e) { filter(); }
|
||||
public void removeUpdate(DocumentEvent e) { filter(); }
|
||||
public void changedUpdate(DocumentEvent e) { filter(); }
|
||||
});
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(list);
|
||||
scrollPane.setBorder(BorderFactory.createEmptyBorder());
|
||||
scrollPane.getVerticalScrollBar().setUnitIncrement(16);
|
||||
scrollPane.getVerticalScrollBar().setBackground(SIDEBAR_BG);
|
||||
|
||||
sidebar.add(search, BorderLayout.NORTH);
|
||||
sidebar.add(scrollPane, BorderLayout.CENTER);
|
||||
return sidebar;
|
||||
}
|
||||
|
||||
private static void populateModel(DefaultListModel<ListItem> model, LinkedHashMap<String, List<ListItem>> categories, String filter) {
|
||||
model.clear();
|
||||
String lower = filter.toLowerCase();
|
||||
for (Map.Entry<String, List<ListItem>> entry : categories.entrySet()) {
|
||||
List<ListItem> matching = new ArrayList<>();
|
||||
for (ListItem item : entry.getValue()) {
|
||||
if (lower.isEmpty() || item.text.toLowerCase().contains(lower) || item.rawName.toLowerCase().contains(lower)) {
|
||||
matching.add(item);
|
||||
}
|
||||
}
|
||||
if (!matching.isEmpty()) {
|
||||
model.addElement(new ListItem(entry.getKey(), entry.getKey(), true, null));
|
||||
for (ListItem item : matching) {
|
||||
model.addElement(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static LinkedHashMap<String, List<ListItem>> buildCategoryMap(NoiseExplorerGUI nv, Engine engine,
|
||||
Supplier<Function2<Double, Double, Double>> customGen, String customName) {
|
||||
LinkedHashMap<String, List<ListItem>> categories = new LinkedHashMap<>();
|
||||
|
||||
if (customGen != null && customName != null) {
|
||||
List<ListItem> custom = new ArrayList<>();
|
||||
custom.add(new ListItem(customName, customName, false, () -> {
|
||||
nv.generator = customGen.get();
|
||||
nv.loader = customGen;
|
||||
nv.currentName = customName;
|
||||
}));
|
||||
categories.put("Custom", custom);
|
||||
}
|
||||
|
||||
Map<String, List<NoiseStyle>> styleGroups = new LinkedHashMap<>();
|
||||
for (NoiseStyle style : NoiseStyle.values()) {
|
||||
String cat = categorize(style);
|
||||
styleGroups.computeIfAbsent(cat, k -> new ArrayList<>()).add(style);
|
||||
}
|
||||
|
||||
if (engine != null && !engine.isClosed()) {
|
||||
List<ListItem> genItems = new ArrayList<>();
|
||||
try {
|
||||
IrisData data = engine.getData();
|
||||
String[] keys = data.getGeneratorLoader().getPossibleKeys();
|
||||
Arrays.sort(keys);
|
||||
for (String key : keys) {
|
||||
IrisGenerator gen = data.getGeneratorLoader().load(key);
|
||||
if (gen != null) {
|
||||
long seed = new RNG(12345).nextParallelRNG(3245).lmax();
|
||||
genItems.add(new ListItem(formatName(key), key, false, () -> {
|
||||
nv.generator = (x, z) -> gen.getHeight(x, z, seed);
|
||||
nv.loader = null;
|
||||
nv.currentName = key;
|
||||
}));
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
if (!genItems.isEmpty()) {
|
||||
categories.put("Pack Generators", genItems);
|
||||
}
|
||||
}
|
||||
|
||||
for (String cat : CATEGORY_ORDER) {
|
||||
if ("Pack Generators".equals(cat)) continue;
|
||||
List<NoiseStyle> styles = styleGroups.get(cat);
|
||||
if (styles != null && !styles.isEmpty()) {
|
||||
List<ListItem> items = new ArrayList<>();
|
||||
for (NoiseStyle style : styles) {
|
||||
items.add(new ListItem(formatName(style.name()), style.name(), false, () -> {
|
||||
nv.cng = style.create(RNG.r.nextParallelRNG(RNG.r.imax()));
|
||||
nv.generator = null;
|
||||
nv.loader = null;
|
||||
nv.currentName = style.name();
|
||||
}));
|
||||
}
|
||||
categories.put(cat, items);
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<String, List<NoiseStyle>> entry : styleGroups.entrySet()) {
|
||||
if (!categories.containsKey(entry.getKey())) {
|
||||
List<ListItem> items = new ArrayList<>();
|
||||
for (NoiseStyle style : entry.getValue()) {
|
||||
items.add(new ListItem(formatName(style.name()), style.name(), false, () -> {
|
||||
nv.cng = style.create(RNG.r.nextParallelRNG(RNG.r.imax()));
|
||||
nv.generator = null;
|
||||
nv.loader = null;
|
||||
nv.currentName = style.name();
|
||||
}));
|
||||
}
|
||||
categories.put(entry.getKey(), items);
|
||||
}
|
||||
}
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
private static String categorize(NoiseStyle style) {
|
||||
String n = style.name();
|
||||
if (n.startsWith("STATIC")) return "Static";
|
||||
if (n.startsWith("IRIS")) return "Iris";
|
||||
if (n.startsWith("CLOVER")) return "Clover";
|
||||
if (n.startsWith("VASCULAR")) return "Vascular";
|
||||
if (n.equals("FLAT")) return "Utility";
|
||||
if (n.startsWith("CELLULAR")) return "Cellular";
|
||||
if (n.startsWith("HEX") || n.equals("HEXAGON")) return "Hexagon";
|
||||
if (n.startsWith("SIERPINSKI")) return "Sierpinski";
|
||||
if (n.startsWith("NOWHERE")) return "Nowhere";
|
||||
if (n.startsWith("GLOB")) return "Globe";
|
||||
if (n.startsWith("PERLIN")) return "Perlin";
|
||||
if (n.startsWith("CUBIC") || (n.startsWith("FRACTAL") && n.contains("CUBIC"))) return "Cubic";
|
||||
if (n.contains("SIMPLEX") && !n.startsWith("FRACTAL")) return "Simplex";
|
||||
if (n.startsWith("FRACTAL")) return "Fractal";
|
||||
return "Other";
|
||||
}
|
||||
|
||||
private static String formatName(String enumName) {
|
||||
String lower = enumName.toLowerCase().replace('_', ' ');
|
||||
return Character.toUpperCase(lower.charAt(0)) + lower.substring(1);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(IrisEngineHotloadEvent e) {
|
||||
if (generator != null && loader != null) {
|
||||
generator = loader.get();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseWheelMoved(MouseWheelEvent e) {
|
||||
int notches = e.getWheelRotation();
|
||||
if (e.isControlDown()) {
|
||||
time = time + ((0.0025 * time) * notches);
|
||||
return;
|
||||
}
|
||||
scale = scale + ((0.044 * scale) * notches);
|
||||
scale = Math.max(scale, 0.00001);
|
||||
}
|
||||
|
||||
private double lerp(double current, double target, double speed) {
|
||||
double diff = target - current;
|
||||
if (Math.abs(diff) < 0.001) return target;
|
||||
return current + diff * speed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paint(Graphics g) {
|
||||
animScale = lerp(animScale, scale, 0.16);
|
||||
animTime = lerp(animTime, time, 0.29);
|
||||
animOx = lerp(animOx, ox, 0.16);
|
||||
animOz = lerp(animOz, oz, 0.16);
|
||||
|
||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||
|
||||
if (g instanceof Graphics2D gg) {
|
||||
gg.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
|
||||
gg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
|
||||
int pw = getWidth();
|
||||
int ph = getHeight();
|
||||
|
||||
if (pw != imgWidth || ph != imgHeight || img == null) {
|
||||
imgWidth = pw;
|
||||
imgHeight = ph;
|
||||
img = null;
|
||||
}
|
||||
|
||||
int accuracy = M.clip((fpsHistory.getAverage() / 14D), 1D, 64D).intValue();
|
||||
int rw = Math.max(1, pw / accuracy);
|
||||
int rh = Math.max(1, ph / accuracy);
|
||||
|
||||
if (img == null || img.getWidth() != rw || img.getHeight() != rh) {
|
||||
img = new BufferedImage(rw, rh, BufferedImage.TYPE_INT_RGB);
|
||||
}
|
||||
|
||||
int[] pixels = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
|
||||
|
||||
BurstExecutor burst = gx.burst(rw);
|
||||
for (int x = 0; x < rw; x++) {
|
||||
int xx = x;
|
||||
burst.queue(() -> {
|
||||
for (int z = 0; z < rh; z++) {
|
||||
double worldX = (xx * accuracy * animScale) + animOx;
|
||||
double worldZ = (z * accuracy * animScale) + animOz;
|
||||
double n = generator != null
|
||||
? generator.apply(worldX, worldZ)
|
||||
: cng.noise(worldX, worldZ);
|
||||
n = Math.max(0, Math.min(1, n));
|
||||
|
||||
int rgb;
|
||||
if (colorMode) {
|
||||
rgb = HSB_LUT[(int) (n * 255)];
|
||||
} else {
|
||||
int v = (int) (n * 255);
|
||||
rgb = (v << 16) | (v << 8) | v;
|
||||
}
|
||||
pixels[z * rw + xx] = rgb;
|
||||
}
|
||||
});
|
||||
}
|
||||
burst.complete();
|
||||
|
||||
gg.setColor(BG);
|
||||
gg.fillRect(0, 0, pw, ph);
|
||||
gg.drawImage(img, 0, 0, pw, ph, null);
|
||||
|
||||
renderStatusBar(gg, pw, ph, p.getMilliseconds());
|
||||
renderCrosshair(gg, pw, ph);
|
||||
}
|
||||
|
||||
p.end();
|
||||
time += 1D;
|
||||
fpsHistory.put(p.getMilliseconds());
|
||||
|
||||
if (!isVisible() || !getParent().isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long sleepMs = Math.max(1, 16 - (long) p.getMilliseconds());
|
||||
EventQueue.invokeLater(() -> {
|
||||
J.sleep(sleepMs);
|
||||
repaint();
|
||||
});
|
||||
}
|
||||
|
||||
private void renderCrosshair(Graphics2D g, int w, int h) {
|
||||
int cx = w / 2;
|
||||
int cy = h / 2;
|
||||
g.setColor(new Color(255, 255, 255, 40));
|
||||
g.drawLine(cx - 8, cy, cx + 8, cy);
|
||||
g.drawLine(cx, cy - 8, cx, cy + 8);
|
||||
}
|
||||
|
||||
private void renderStatusBar(Graphics2D g, int w, int h, double frameMs) {
|
||||
int barHeight = 28;
|
||||
int y = h - barHeight;
|
||||
|
||||
g.setColor(STATUS_BG);
|
||||
g.fillRect(0, y, w, barHeight);
|
||||
g.setColor(new Color(50, 50, 60));
|
||||
g.drawLine(0, y, w, y);
|
||||
|
||||
g.setFont(STATUS_FONT);
|
||||
g.setColor(STATUS_TEXT);
|
||||
|
||||
double worldX = (w / 2.0 * animScale) + animOx;
|
||||
double worldZ = (h / 2.0 * animScale) + animOz;
|
||||
double noiseVal = generator != null
|
||||
? generator.apply(worldX, worldZ)
|
||||
: cng.noise(worldX, worldZ);
|
||||
noiseVal = Math.max(0, Math.min(1, noiseVal));
|
||||
|
||||
int fps = frameMs > 0 ? (int) (1000.0 / frameMs) : 0;
|
||||
|
||||
String status = String.format(" %s | X: %.1f Z: %.1f | Zoom: %.4f | Value: %.4f | %d FPS",
|
||||
currentName, worldX, worldZ, animScale, noiseVal, fps);
|
||||
g.drawString(status, 8, y + 18);
|
||||
|
||||
int barW = 60;
|
||||
int barX = w - barW - 12;
|
||||
int barY = y + 6;
|
||||
int barH = barHeight - 12;
|
||||
g.setColor(new Color(40, 40, 48));
|
||||
g.fillRoundRect(barX, barY, barW, barH, 4, 4);
|
||||
int fillW = (int) (noiseVal * (barW - 2));
|
||||
g.setColor(ACCENT);
|
||||
g.fillRoundRect(barX + 1, barY + 1, fillW, barH - 2, 3, 3);
|
||||
}
|
||||
|
||||
private static final class ListItem {
|
||||
final String text;
|
||||
final String rawName;
|
||||
final boolean header;
|
||||
final Runnable action;
|
||||
|
||||
ListItem(String text, String rawName, boolean header, Runnable action) {
|
||||
this.text = text;
|
||||
this.rawName = rawName;
|
||||
this.header = header;
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SidebarCellRenderer extends DefaultListCellRenderer {
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean selected, boolean focus) {
|
||||
ListItem item = (ListItem) value;
|
||||
super.getListCellRendererComponent(list, item.text, index, !item.header && selected, false);
|
||||
setOpaque(true);
|
||||
if (item.header) {
|
||||
setFont(SIDEBAR_HEADER_FONT);
|
||||
setForeground(ACCENT);
|
||||
setBackground(SIDEBAR_BG);
|
||||
setBorder(BorderFactory.createEmptyBorder(10, 10, 4, 10));
|
||||
} else {
|
||||
setFont(SIDEBAR_ITEM_FONT);
|
||||
setForeground(selected ? Color.WHITE : SIDEBAR_ITEM_COLOR);
|
||||
setBackground(selected ? SIDEBAR_SELECTED : SIDEBAR_BG);
|
||||
setBorder(BorderFactory.createEmptyBorder(3, 20, 3, 10));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
-93
@@ -16,35 +16,36 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.gui;
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.IrisSettings;
|
||||
import com.volmit.iris.core.pregenerator.IrisPregenerator;
|
||||
import com.volmit.iris.core.pregenerator.PregenListener;
|
||||
import com.volmit.iris.core.pregenerator.PregenTask;
|
||||
import com.volmit.iris.core.pregenerator.PregeneratorMethod;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.format.Form;
|
||||
import com.volmit.iris.util.format.MemoryMonitor;
|
||||
import com.volmit.iris.util.function.Consumer2;
|
||||
import com.volmit.iris.util.mantle.Mantle;
|
||||
import com.volmit.iris.util.math.M;
|
||||
import com.volmit.iris.util.math.Position2;
|
||||
import com.volmit.iris.util.scheduling.ChronoLatch;
|
||||
import com.volmit.iris.util.scheduling.J;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.pregenerator.IrisPregenerator;
|
||||
import art.arcane.iris.core.pregenerator.PregenListener;
|
||||
import art.arcane.iris.core.pregenerator.PregenTask;
|
||||
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.format.MemoryMonitor;
|
||||
import art.arcane.volmlib.util.function.Consumer2;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.World;
|
||||
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JPanel;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.KeyListener;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -57,7 +58,7 @@ public class PregeneratorJob implements PregenListener {
|
||||
private static final Color COLOR_NETWORK_GENERATING = parseColor("#836b8c");
|
||||
private static final Color COLOR_GENERATED = parseColor("#65c295");
|
||||
private static final Color COLOR_CLEANED = parseColor("#34eb93");
|
||||
public static PregeneratorJob instance;
|
||||
private static final AtomicReference<PregeneratorJob> instance = new AtomicReference<>();
|
||||
private final MemoryMonitor monitor;
|
||||
private final PregenTask task;
|
||||
private final boolean saving;
|
||||
@@ -66,81 +67,113 @@ public class PregeneratorJob implements PregenListener {
|
||||
private final IrisPregenerator pregenerator;
|
||||
private final Position2 min;
|
||||
private final Position2 max;
|
||||
private final ChronoLatch cl = new ChronoLatch(TimeUnit.MINUTES.toMillis(1));
|
||||
private final Engine engine;
|
||||
private final ExecutorService service;
|
||||
private JFrame frame;
|
||||
private PregenRenderer renderer;
|
||||
private int rgc = 0;
|
||||
private final ChronoLatch cl = new ChronoLatch(TimeUnit.MINUTES.toMillis(1));
|
||||
private String[] info;
|
||||
private final Engine engine;
|
||||
private volatile double lastChunksPerSecond = 0D;
|
||||
private volatile long lastChunksRemaining = 0L;
|
||||
|
||||
public PregeneratorJob(PregenTask task, PregeneratorMethod method, Engine engine) {
|
||||
instance.updateAndGet(old -> {
|
||||
if (old != null) {
|
||||
old.pregenerator.close();
|
||||
old.close();
|
||||
}
|
||||
return this;
|
||||
});
|
||||
this.engine = engine;
|
||||
instance = this;
|
||||
monitor = new MemoryMonitor(50);
|
||||
saving = false;
|
||||
info = new String[] {"Initializing..."};
|
||||
info = new String[]{"Initializing..."};
|
||||
this.task = task;
|
||||
this.pregenerator = new IrisPregenerator(task, method, this);
|
||||
max = new Position2(0, 0);
|
||||
min = new Position2(0, 0);
|
||||
KList<Runnable> draw = new KList<>();
|
||||
task.iterateRegions((xx, zz) -> {
|
||||
min.setX(Math.min(xx << 5, min.getX()));
|
||||
min.setZ(Math.min(zz << 5, min.getZ()));
|
||||
max.setX(Math.max((xx << 5) + 31, max.getX()));
|
||||
max.setZ(Math.max((zz << 5) + 31, max.getZ()));
|
||||
min = new Position2(Integer.MAX_VALUE, Integer.MAX_VALUE);
|
||||
task.iterateAllChunks((xx, zz) -> {
|
||||
min.setX(Math.min(xx, min.getX()));
|
||||
min.setZ(Math.min(zz, min.getZ()));
|
||||
max.setX(Math.max(xx, max.getX()));
|
||||
max.setZ(Math.max(zz, max.getZ()));
|
||||
});
|
||||
|
||||
if(IrisSettings.get().getGui().isUseServerLaunchedGuis()) {
|
||||
if (IrisSettings.get().getGui().isUseServerLaunchedGuis() && task.isGui()) {
|
||||
open();
|
||||
}
|
||||
|
||||
J.a(this.pregenerator::start, 20);
|
||||
}
|
||||
|
||||
public Mantle getMantle() {
|
||||
return pregenerator.getMantle();
|
||||
var t = new Thread(() -> {
|
||||
J.sleep(1000);
|
||||
this.pregenerator.start();
|
||||
}, "Iris Pregenerator");
|
||||
t.setPriority(Thread.MIN_PRIORITY);
|
||||
t.start();
|
||||
service = Executors.newVirtualThreadPerTaskExecutor();
|
||||
}
|
||||
|
||||
public static boolean shutdownInstance() {
|
||||
if(instance == null) {
|
||||
PregeneratorJob inst = instance.get();
|
||||
if (inst == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
J.a(() -> instance.pregenerator.close());
|
||||
J.a(inst.pregenerator::close);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static PregeneratorJob getInstance() {
|
||||
return instance;
|
||||
return instance.get();
|
||||
}
|
||||
|
||||
public static boolean pauseResume() {
|
||||
if(instance == null) {
|
||||
PregeneratorJob inst = instance.get();
|
||||
if (inst == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(isPaused()) {
|
||||
instance.pregenerator.resume();
|
||||
if (isPaused()) {
|
||||
inst.pregenerator.resume();
|
||||
} else {
|
||||
instance.pregenerator.pause();
|
||||
inst.pregenerator.pause();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isPaused() {
|
||||
if(instance == null) {
|
||||
PregeneratorJob inst = instance.get();
|
||||
if (inst == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return instance.paused();
|
||||
return inst.paused();
|
||||
}
|
||||
|
||||
public static double chunksPerSecond() {
|
||||
PregeneratorJob inst = instance.get();
|
||||
return inst == null ? 0D : Math.max(0D, inst.lastChunksPerSecond);
|
||||
}
|
||||
|
||||
public static long chunksRemaining() {
|
||||
PregeneratorJob inst = instance.get();
|
||||
return inst == null ? -1L : Math.max(0L, inst.lastChunksRemaining);
|
||||
}
|
||||
|
||||
public boolean targetsWorld(World world) {
|
||||
if (world == null || engine == null || engine.getWorld() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String targetName = engine.getWorld().name();
|
||||
return targetName != null && targetName.equalsIgnoreCase(world.getName());
|
||||
}
|
||||
|
||||
private static Color parseColor(String c) {
|
||||
String v = (c.startsWith("#") ? c : "#" + c).trim();
|
||||
try {
|
||||
return Color.decode(v);
|
||||
} catch(Throwable e) {
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Iris.error("Error Parsing 'color', (" + c + ")");
|
||||
}
|
||||
@@ -148,6 +181,10 @@ public class PregeneratorJob implements PregenListener {
|
||||
return Color.RED;
|
||||
}
|
||||
|
||||
public Mantle getMantle() {
|
||||
return pregenerator.getMantle();
|
||||
}
|
||||
|
||||
public PregeneratorJob onProgress(Consumer<Double> c) {
|
||||
onProgress.add(c);
|
||||
return this;
|
||||
@@ -159,21 +196,19 @@ public class PregeneratorJob implements PregenListener {
|
||||
}
|
||||
|
||||
public void drawRegion(int x, int z, Color color) {
|
||||
J.a(() -> {
|
||||
PregenTask.iterateRegion(x, z, (xx, zz) -> {
|
||||
draw(xx, zz, color);
|
||||
J.sleep(3);
|
||||
});
|
||||
});
|
||||
J.a(() -> task.iterateChunks(x, z, (xx, zz) -> {
|
||||
draw(xx, zz, color);
|
||||
J.sleep(3);
|
||||
}));
|
||||
}
|
||||
|
||||
public void draw(int x, int z, Color color) {
|
||||
try {
|
||||
if(renderer != null && frame != null && frame.isVisible()) {
|
||||
if (renderer != null && frame != null && frame.isVisible()) {
|
||||
renderer.func.accept(new Position2(x, z), color);
|
||||
}
|
||||
} catch(Throwable ignored) {
|
||||
|
||||
} catch (Throwable ignored) {
|
||||
Iris.error("Failed to draw pregen");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +216,7 @@ public class PregeneratorJob implements PregenListener {
|
||||
J.a(() -> {
|
||||
pregenerator.close();
|
||||
close();
|
||||
instance = null;
|
||||
instance.compareAndSet(this, null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -191,8 +226,8 @@ public class PregeneratorJob implements PregenListener {
|
||||
monitor.close();
|
||||
J.sleep(3000);
|
||||
frame.setVisible(false);
|
||||
} catch(Throwable e) {
|
||||
|
||||
} catch (Throwable ignored) {
|
||||
Iris.error("Error closing pregen gui");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -206,8 +241,7 @@ public class PregeneratorJob implements PregenListener {
|
||||
renderer.l = new ReentrantLock();
|
||||
renderer.frame = frame;
|
||||
renderer.job = this;
|
||||
renderer.func = (c, b) ->
|
||||
{
|
||||
renderer.func = (c, b) -> {
|
||||
renderer.l.lock();
|
||||
renderer.order.add(() -> renderer.draw(c, b, renderer.bg));
|
||||
renderer.l.unlock();
|
||||
@@ -215,45 +249,47 @@ public class PregeneratorJob implements PregenListener {
|
||||
frame.add(renderer);
|
||||
frame.setSize(1000, 1000);
|
||||
frame.setVisible(true);
|
||||
} catch(Throwable e) {
|
||||
|
||||
} catch (Throwable ignored) {
|
||||
Iris.error("Error opening pregen gui");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, int generated, int totalChunks, int chunksRemaining, long eta, long elapsed, String method) {
|
||||
info = new String[] {
|
||||
(paused() ? "PAUSED" : (saving ? "Saving... " : "Generating")) + " " + Form.f(generated) + " of " + Form.f(totalChunks) + " (" + Form.pc(percent, 0) + " Complete)",
|
||||
"Speed: " + Form.f(chunksPerSecond, 0) + " Chunks/s, " + Form.f(regionsPerMinute, 1) + " Regions/m, " + Form.f(chunksPerMinute, 0) + " Chunks/m",
|
||||
Form.duration(eta, 2) + " Remaining " + " (" + Form.duration(elapsed, 2) + " Elapsed)",
|
||||
"Generation Method: " + method,
|
||||
"Memory: " + Form.memSize(monitor.getUsedBytes(), 2) + " (" + Form.pc(monitor.getUsagePercent(), 0) + ") Pressure: " + Form.memSize(monitor.getPressure(), 0) + "/s",
|
||||
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
|
||||
lastChunksPerSecond = chunksPerSecond;
|
||||
lastChunksRemaining = chunksRemaining;
|
||||
|
||||
info = new String[]{
|
||||
(paused() ? "PAUSED" : (saving ? "Saving... " : "Generating")) + " " + Form.f(generated) + " of " + Form.f(totalChunks) + " (" + Form.pc(percent, 0) + " Complete)",
|
||||
"Speed: " + (cached ? "Cached " : "") + Form.f(chunksPerSecond, 0) + " Chunks/s, " + Form.f(regionsPerMinute, 1) + " Regions/m, " + Form.f(chunksPerMinute, 0) + " Chunks/m",
|
||||
Form.duration(eta, 2) + " Remaining " + " (" + Form.duration(elapsed, 2) + " Elapsed)",
|
||||
"Generation Method: " + method,
|
||||
"Memory: " + Form.memSize(monitor.getUsedBytes(), 2) + " (" + Form.pc(monitor.getUsagePercent(), 0) + ") Pressure: " + Form.memSize(monitor.getPressure(), 0) + "/s",
|
||||
|
||||
};
|
||||
|
||||
for(Consumer<Double> i : onProgress) {
|
||||
for (Consumer<Double> i : onProgress) {
|
||||
i.accept(percent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkGenerating(int x, int z) {
|
||||
if(engine != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
draw(x, z, COLOR_GENERATING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkGenerated(int x, int z) {
|
||||
if(engine != null) {
|
||||
draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8));
|
||||
return;
|
||||
}
|
||||
public void onChunkGenerated(int x, int z, boolean cached) {
|
||||
if (renderer == null || frame == null || !frame.isVisible()) return;
|
||||
service.submit(() -> {
|
||||
if (engine != null) {
|
||||
draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8));
|
||||
return;
|
||||
}
|
||||
|
||||
draw(x, z, COLOR_GENERATED);
|
||||
draw(x, z, COLOR_GENERATED);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -263,7 +299,7 @@ public class PregeneratorJob implements PregenListener {
|
||||
}
|
||||
|
||||
private void shouldGc() {
|
||||
if(cl.flip() && rgc > 16) {
|
||||
if (cl.flip() && rgc > 16) {
|
||||
System.gc();
|
||||
}
|
||||
}
|
||||
@@ -311,8 +347,9 @@ public class PregeneratorJob implements PregenListener {
|
||||
@Override
|
||||
public void onClose() {
|
||||
close();
|
||||
instance = null;
|
||||
instance.compareAndSet(this, null);
|
||||
whenDone.forEach(Runnable::run);
|
||||
service.shutdownNow();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -322,7 +359,7 @@ public class PregeneratorJob implements PregenListener {
|
||||
|
||||
@Override
|
||||
public void onChunkExistsInRegionGen(int x, int z) {
|
||||
if(engine != null) {
|
||||
if (engine != null) {
|
||||
draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8));
|
||||
return;
|
||||
}
|
||||
@@ -371,10 +408,10 @@ public class PregeneratorJob implements PregenListener {
|
||||
bg = (Graphics2D) image.getGraphics();
|
||||
l.lock();
|
||||
|
||||
while(order.isNotEmpty()) {
|
||||
while (order.isNotEmpty()) {
|
||||
try {
|
||||
order.pop().run();
|
||||
} catch(Throwable e) {
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
|
||||
}
|
||||
@@ -388,12 +425,12 @@ public class PregeneratorJob implements PregenListener {
|
||||
int h = g.getFontMetrics().getHeight() + 5;
|
||||
int hh = 20;
|
||||
|
||||
if(job.paused()) {
|
||||
if (job.paused()) {
|
||||
g.drawString("PAUSED", 20, hh += h);
|
||||
|
||||
g.drawString("Press P to Resume", 20, hh += h);
|
||||
} else {
|
||||
for(String i : prog) {
|
||||
for (String i : prog) {
|
||||
g.drawString(i, 20, hh += h);
|
||||
}
|
||||
|
||||
@@ -429,7 +466,7 @@ public class PregeneratorJob implements PregenListener {
|
||||
|
||||
@Override
|
||||
public void keyReleased(KeyEvent e) {
|
||||
if(e.getKeyCode() == KeyEvent.VK_P) {
|
||||
if (e.getKeyCode() == KeyEvent.VK_P) {
|
||||
PregeneratorJob.pauseResume();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,915 @@
|
||||
/*
|
||||
* 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.gui.components.IrisRenderer;
|
||||
import art.arcane.iris.core.gui.components.RenderType;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.RollingSequence;
|
||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
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.Location;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.MouseInputListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
|
||||
|
||||
public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener, MouseMotionListener, MouseInputListener {
|
||||
private static final long serialVersionUID = 2094606939770332040L;
|
||||
|
||||
private static final Color BG = new Color(18, 18, 22);
|
||||
private static final Color CARD_BG = new Color(28, 28, 36, 220);
|
||||
private static final Color CARD_BORDER = new Color(60, 60, 75, 180);
|
||||
private static final Color TEXT_PRIMARY = new Color(220, 220, 230);
|
||||
private static final Color TEXT_SECONDARY = new Color(140, 140, 155);
|
||||
private static final Color TEXT_DIM = new Color(90, 90, 105);
|
||||
private static final Color ACCENT = new Color(90, 140, 255);
|
||||
private static final Color ACCENT_DIM = new Color(60, 100, 200, 100);
|
||||
private static final Color PLAYER_COLOR = new Color(80, 200, 120);
|
||||
private static final Color MOB_COLOR = new Color(220, 80, 80);
|
||||
private static final Color STATUS_BG = new Color(24, 24, 30, 240);
|
||||
private static final Color GRID_COLOR = new Color(255, 255, 255, 12);
|
||||
private static final Font FONT_STATUS = new Font(Font.MONOSPACED, Font.PLAIN, 12);
|
||||
private static final Font FONT_CARD_TITLE = new Font(Font.SANS_SERIF, Font.BOLD, 13);
|
||||
private static final Font FONT_CARD_BODY = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
|
||||
private static final Font FONT_HELP_KEY = new Font(Font.MONOSPACED, Font.BOLD, 12);
|
||||
private static final Font FONT_NOTIFICATION = new Font(Font.SANS_SERIF, Font.BOLD, 14);
|
||||
private static final int CARD_RADIUS = 8;
|
||||
private static final int CARD_PAD = 12;
|
||||
private static final int STATUS_HEIGHT = 26;
|
||||
|
||||
private final KList<LivingEntity> lastEntities = new KList<>();
|
||||
private final KMap<String, Long> notifications = new KMap<>();
|
||||
private final ChronoLatch centities = new ChronoLatch(1000);
|
||||
private final RollingSequence rs = new RollingSequence(512);
|
||||
private final O<Integer> m = new O<>();
|
||||
private final KMap<BlockPosition, BufferedImage> positions = new KMap<>();
|
||||
private final KMap<BlockPosition, BufferedImage> fastpositions = new KMap<>();
|
||||
private final KSet<BlockPosition> working = new KSet<>();
|
||||
private final KSet<BlockPosition> workingfast = new KSet<>();
|
||||
|
||||
private RenderType currentType = RenderType.BIOME;
|
||||
private boolean help = true;
|
||||
private boolean helpIgnored = false;
|
||||
private boolean shift = false;
|
||||
private Player player = null;
|
||||
private boolean debug = false;
|
||||
private boolean control = false;
|
||||
private boolean eco = false;
|
||||
private boolean lowtile = false;
|
||||
private boolean follow = false;
|
||||
private boolean alt = false;
|
||||
private boolean grid = false;
|
||||
private IrisRenderer renderer;
|
||||
private IrisWorld world;
|
||||
private double velocity = 0;
|
||||
private int lowq = 12;
|
||||
private double scale = 128;
|
||||
private double mscale = 4D;
|
||||
private int w = 0;
|
||||
private int h = 0;
|
||||
private double lx = 0;
|
||||
private double lz = 0;
|
||||
private double ox = 0;
|
||||
private double oz = 0;
|
||||
private double hx = 0;
|
||||
private double hz = 0;
|
||||
private double oxp = 0;
|
||||
private double ozp = 0;
|
||||
private Engine engine;
|
||||
private int tid = 0;
|
||||
private Map<RenderType, JToggleButton> modeButtons;
|
||||
|
||||
private final ExecutorService e = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), r -> {
|
||||
tid++;
|
||||
Thread t = new Thread(r);
|
||||
t.setName("Iris HD Renderer " + tid);
|
||||
t.setPriority(Thread.MIN_PRIORITY);
|
||||
t.setUncaughtExceptionHandler((et, ex) -> {
|
||||
Iris.info("Exception encountered in " + et.getName());
|
||||
ex.printStackTrace();
|
||||
});
|
||||
return t;
|
||||
});
|
||||
|
||||
private final ExecutorService eh = Executors.newFixedThreadPool(3, r -> {
|
||||
tid++;
|
||||
Thread t = new Thread(r);
|
||||
t.setName("Iris Renderer " + tid);
|
||||
t.setPriority(Thread.NORM_PRIORITY);
|
||||
t.setUncaughtExceptionHandler((et, ex) -> {
|
||||
Iris.info("Exception encountered in " + et.getName());
|
||||
ex.printStackTrace();
|
||||
});
|
||||
return t;
|
||||
});
|
||||
|
||||
public VisionGUI(JFrame frame) {
|
||||
m.set(8);
|
||||
rs.put(1);
|
||||
setBackground(BG);
|
||||
addMouseWheelListener(this);
|
||||
addMouseMotionListener(this);
|
||||
addMouseListener(this);
|
||||
frame.addKeyListener(this);
|
||||
J.a(() -> {
|
||||
J.sleep(10000);
|
||||
if (!helpIgnored && help) {
|
||||
help = false;
|
||||
}
|
||||
});
|
||||
frame.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent windowEvent) {
|
||||
e.shutdown();
|
||||
eh.shutdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void createAndShowGUI(Engine r, int s, IrisWorld world) {
|
||||
JFrame frame = new JFrame("Iris Vision");
|
||||
VisionGUI nv = new VisionGUI(frame);
|
||||
nv.world = world;
|
||||
nv.engine = r;
|
||||
nv.renderer = new IrisRenderer(r);
|
||||
frame.getContentPane().setBackground(BG);
|
||||
frame.setLayout(new BorderLayout());
|
||||
frame.add(buildToolbar(nv), BorderLayout.NORTH);
|
||||
frame.add(nv, BorderLayout.CENTER);
|
||||
frame.setSize(1440, 820);
|
||||
frame.setMinimumSize(new Dimension(640, 480));
|
||||
frame.setLocationRelativeTo(null);
|
||||
frame.setVisible(true);
|
||||
}
|
||||
|
||||
private static JPanel buildToolbar(VisionGUI nv) {
|
||||
JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 3, 3));
|
||||
toolbar.setBackground(new Color(22, 22, 28));
|
||||
toolbar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(45, 45, 55)));
|
||||
|
||||
JLabel modeLabel = new JLabel("View:");
|
||||
modeLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 11));
|
||||
modeLabel.setForeground(TEXT_SECONDARY);
|
||||
modeLabel.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 2));
|
||||
toolbar.add(modeLabel);
|
||||
|
||||
ButtonGroup modeGroup = new ButtonGroup();
|
||||
Map<RenderType, JToggleButton> modeButtons = new LinkedHashMap<>();
|
||||
for (RenderType type : RenderType.values()) {
|
||||
JToggleButton btn = createToolbarToggle(modeName(type), type == nv.currentType);
|
||||
btn.addActionListener(e -> {
|
||||
nv.setRenderType(type);
|
||||
for (Map.Entry<RenderType, JToggleButton> entry : modeButtons.entrySet()) {
|
||||
entry.getValue().setSelected(entry.getKey() == type);
|
||||
}
|
||||
});
|
||||
modeGroup.add(btn);
|
||||
modeButtons.put(type, btn);
|
||||
toolbar.add(btn);
|
||||
}
|
||||
nv.modeButtons = modeButtons;
|
||||
|
||||
toolbar.add(createToolbarSeparator());
|
||||
|
||||
JToggleButton gridBtn = createToolbarToggle("Grid", nv.grid);
|
||||
gridBtn.addActionListener(e -> { nv.toggleGrid(); gridBtn.setSelected(nv.grid); });
|
||||
toolbar.add(gridBtn);
|
||||
|
||||
JToggleButton followBtn = createToolbarToggle("Follow", nv.follow);
|
||||
followBtn.addActionListener(e -> { nv.toggleFollow(); followBtn.setSelected(nv.follow); });
|
||||
toolbar.add(followBtn);
|
||||
|
||||
JToggleButton qualityBtn = createToolbarToggle("LQ", nv.lowtile);
|
||||
qualityBtn.addActionListener(e -> { nv.toggleQuality(); qualityBtn.setSelected(nv.lowtile); });
|
||||
toolbar.add(qualityBtn);
|
||||
|
||||
return toolbar;
|
||||
}
|
||||
|
||||
private static JToggleButton createToolbarToggle(String text, boolean selected) {
|
||||
JToggleButton btn = new JToggleButton(text, selected);
|
||||
btn.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 11));
|
||||
btn.setFocusable(false);
|
||||
btn.setForeground(new Color(170, 170, 185));
|
||||
btn.setBackground(new Color(32, 32, 40));
|
||||
btn.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(new Color(50, 50, 60)),
|
||||
BorderFactory.createEmptyBorder(3, 8, 3, 8)
|
||||
));
|
||||
btn.setOpaque(true);
|
||||
btn.addChangeListener(e -> {
|
||||
if (btn.isSelected()) {
|
||||
btn.setBackground(new Color(50, 60, 85));
|
||||
btn.setForeground(Color.WHITE);
|
||||
} else {
|
||||
btn.setBackground(new Color(32, 32, 40));
|
||||
btn.setForeground(new Color(170, 170, 185));
|
||||
}
|
||||
});
|
||||
return btn;
|
||||
}
|
||||
|
||||
private static JSeparator createToolbarSeparator() {
|
||||
JSeparator sep = new JSeparator(SwingConstants.VERTICAL);
|
||||
sep.setPreferredSize(new Dimension(1, 24));
|
||||
sep.setForeground(new Color(50, 50, 60));
|
||||
sep.setBackground(new Color(22, 22, 28));
|
||||
return sep;
|
||||
}
|
||||
|
||||
public static void launch(Engine g, int i) {
|
||||
J.a(() -> createAndShowGUI(g, i, g.getWorld()));
|
||||
}
|
||||
|
||||
public boolean updateEngine() {
|
||||
if (engine.isClosed()) {
|
||||
if (world.hasRealWorld()) {
|
||||
try {
|
||||
engine = IrisToolbelt.access(world.realWorld()).getEngine();
|
||||
return !engine.isClosed();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseMoved(MouseEvent e) {
|
||||
Point cp = e.getPoint();
|
||||
lx = cp.getX();
|
||||
lz = cp.getY();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
Point cp = e.getPoint();
|
||||
ox += (lx - cp.getX()) * scale;
|
||||
oz += (lz - cp.getY()) * scale;
|
||||
lx = cp.getX();
|
||||
lz = cp.getY();
|
||||
}
|
||||
|
||||
public void notify(String s) {
|
||||
notifications.put(s, M.ms() + 2500);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyTyped(KeyEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
if (e.getKeyCode() == KeyEvent.VK_SHIFT) shift = true;
|
||||
if (e.getKeyCode() == KeyEvent.VK_CONTROL) control = true;
|
||||
if (e.getKeyCode() == KeyEvent.VK_SEMICOLON) debug = true;
|
||||
if (e.getKeyCode() == KeyEvent.VK_SLASH) { help = true; helpIgnored = true; }
|
||||
if (e.getKeyCode() == KeyEvent.VK_ALT) alt = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyReleased(KeyEvent e) {
|
||||
if (e.getKeyCode() == KeyEvent.VK_SEMICOLON) debug = false;
|
||||
if (e.getKeyCode() == KeyEvent.VK_SHIFT) shift = false;
|
||||
if (e.getKeyCode() == KeyEvent.VK_CONTROL) control = false;
|
||||
if (e.getKeyCode() == KeyEvent.VK_SLASH) { help = false; helpIgnored = true; }
|
||||
if (e.getKeyCode() == KeyEvent.VK_ALT) alt = false;
|
||||
|
||||
if (e.getKeyCode() == KeyEvent.VK_F) { toggleFollow(); return; }
|
||||
if (e.getKeyCode() == KeyEvent.VK_R) { dump(); notify("Refreshing"); return; }
|
||||
if (e.getKeyCode() == KeyEvent.VK_P) { toggleQuality(); return; }
|
||||
if (e.getKeyCode() == KeyEvent.VK_E) { eco = !eco; dump(); notify((eco ? "30" : "60") + " FPS"); return; }
|
||||
if (e.getKeyCode() == KeyEvent.VK_G) { toggleGrid(); return; }
|
||||
|
||||
if (e.getKeyCode() == KeyEvent.VK_EQUALS) {
|
||||
mscale = mscale + ((0.044 * mscale) * -3);
|
||||
mscale = Math.max(mscale, 0.00001);
|
||||
dump();
|
||||
return;
|
||||
}
|
||||
if (e.getKeyCode() == KeyEvent.VK_MINUS) {
|
||||
mscale = mscale + ((0.044 * mscale) * 3);
|
||||
mscale = Math.max(mscale, 0.00001);
|
||||
dump();
|
||||
return;
|
||||
}
|
||||
if (e.getKeyCode() == KeyEvent.VK_BACK_SLASH) {
|
||||
mscale = 1D;
|
||||
dump();
|
||||
notify("Zoom Reset");
|
||||
return;
|
||||
}
|
||||
|
||||
int currentMode = currentType.ordinal();
|
||||
for (RenderType i : RenderType.values()) {
|
||||
if (e.getKeyChar() == String.valueOf(i.ordinal() + 1).charAt(0)) {
|
||||
if (i.ordinal() != currentMode) {
|
||||
setRenderType(i);
|
||||
syncModeButtons();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (e.getKeyCode() == KeyEvent.VK_M) {
|
||||
setRenderType(RenderType.values()[(currentMode + 1) % RenderType.values().length]);
|
||||
syncModeButtons();
|
||||
}
|
||||
}
|
||||
|
||||
private static String modeName(RenderType type) {
|
||||
return Form.capitalizeWords(type.name().toLowerCase().replaceAll("\\Q_\\E", " "));
|
||||
}
|
||||
|
||||
void setRenderType(RenderType type) {
|
||||
currentType = type;
|
||||
dump();
|
||||
notify(modeName(type));
|
||||
}
|
||||
|
||||
void toggleGrid() {
|
||||
grid = !grid;
|
||||
notify("Grid " + (grid ? "On" : "Off"));
|
||||
}
|
||||
|
||||
void toggleFollow() {
|
||||
follow = !follow;
|
||||
if (player != null && follow) {
|
||||
notify("Following " + player.getName());
|
||||
} else if (follow) {
|
||||
notify("No player in world");
|
||||
follow = false;
|
||||
} else {
|
||||
notify("Follow disabled");
|
||||
}
|
||||
}
|
||||
|
||||
void toggleQuality() {
|
||||
lowtile = !lowtile;
|
||||
dump();
|
||||
notify((lowtile ? "Low" : "High") + " Quality");
|
||||
}
|
||||
|
||||
private void syncModeButtons() {
|
||||
if (modeButtons == null) return;
|
||||
for (Map.Entry<RenderType, JToggleButton> entry : modeButtons.entrySet()) {
|
||||
entry.getValue().setSelected(entry.getKey() == currentType);
|
||||
}
|
||||
}
|
||||
|
||||
private void dump() {
|
||||
positions.clear();
|
||||
fastpositions.clear();
|
||||
}
|
||||
|
||||
public BufferedImage getTile(KSet<BlockPosition> fg, int div, int x, int z, O<Integer> m) {
|
||||
BlockPosition key = new BlockPosition((int) mscale, Math.floorDiv(x, div), Math.floorDiv(z, div));
|
||||
fg.add(key);
|
||||
|
||||
if (positions.containsKey(key)) {
|
||||
return positions.get(key);
|
||||
}
|
||||
|
||||
if (fastpositions.containsKey(key)) {
|
||||
if (!working.contains(key) && working.size() < 9) {
|
||||
m.set(m.get() - 1);
|
||||
if (m.get() >= 0 && velocity < 50) {
|
||||
working.add(key);
|
||||
double mk = mscale;
|
||||
double mkd = scale;
|
||||
e.submit(() -> {
|
||||
PrecisionStopwatch ps = PrecisionStopwatch.start();
|
||||
BufferedImage b = renderer.render(x * mscale, z * mscale, div * mscale, div / (lowtile ? 3 : 1), currentType);
|
||||
rs.put(ps.getMilliseconds());
|
||||
working.remove(key);
|
||||
if (mk == mscale && mkd == scale) {
|
||||
positions.put(key, b);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return fastpositions.get(key);
|
||||
}
|
||||
|
||||
if (workingfast.contains(key) || workingfast.size() > Runtime.getRuntime().availableProcessors()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
workingfast.add(key);
|
||||
double mk = mscale;
|
||||
double mkd = scale;
|
||||
eh.submit(() -> {
|
||||
PrecisionStopwatch ps = PrecisionStopwatch.start();
|
||||
BufferedImage b = renderer.render(x * mscale, z * mscale, div * mscale, div / lowq, currentType);
|
||||
rs.put(ps.getMilliseconds());
|
||||
workingfast.remove(key);
|
||||
if (mk == mscale && mkd == scale) {
|
||||
fastpositions.put(key, b);
|
||||
}
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
private double getWorldX(double screenX) {
|
||||
return (mscale * screenX) + ((oxp / scale) * mscale);
|
||||
}
|
||||
|
||||
private double getWorldZ(double screenZ) {
|
||||
return (mscale * screenZ) + ((ozp / scale) * mscale);
|
||||
}
|
||||
|
||||
private double getScreenX(double x) {
|
||||
return (x / mscale) - (oxp / scale);
|
||||
}
|
||||
|
||||
private double getScreenZ(double z) {
|
||||
return (z / mscale) - (ozp / scale);
|
||||
}
|
||||
|
||||
private double lerp(double current, double target, double speed) {
|
||||
double diff = target - current;
|
||||
if (Math.abs(diff) < 0.5) return target;
|
||||
return current + diff * speed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paint(Graphics gx) {
|
||||
if (engine.isClosed()) {
|
||||
EventQueue.invokeLater(() -> {
|
||||
try { setVisible(false); } catch (Throwable ignored) { }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (updateEngine()) {
|
||||
dump();
|
||||
}
|
||||
|
||||
velocity = Math.abs(ox - oxp) * 0.36 + Math.abs(oz - ozp) * 0.36;
|
||||
oxp = lerp(oxp, ox, 0.36);
|
||||
ozp = lerp(ozp, oz, 0.36);
|
||||
hx = lerp(hx, lx, 0.36);
|
||||
hz = lerp(hz, lz, 0.36);
|
||||
|
||||
if (centities.flip()) {
|
||||
J.s(() -> {
|
||||
synchronized (lastEntities) {
|
||||
lastEntities.clear();
|
||||
lastEntities.addAll(world.getEntitiesByClass(LivingEntity.class));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lowq = Math.max(Math.min((int) M.lerp(8, 28, velocity / 1000D), 28), 8);
|
||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||
Graphics2D g = (Graphics2D) gx;
|
||||
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
|
||||
w = getWidth();
|
||||
h = getHeight();
|
||||
double vscale = scale;
|
||||
scale = w / 12D;
|
||||
|
||||
if (scale != vscale) {
|
||||
positions.clear();
|
||||
}
|
||||
|
||||
KSet<BlockPosition> gg = new KSet<>();
|
||||
int iscale = (int) scale;
|
||||
g.setColor(BG);
|
||||
g.fillRect(0, 0, w, h);
|
||||
double offsetX = oxp / scale;
|
||||
double offsetZ = ozp / scale;
|
||||
m.set(3);
|
||||
|
||||
for (int r = 0; r < Math.max(w, h); r += iscale) {
|
||||
for (int i = -iscale; i < w + iscale; i += iscale) {
|
||||
for (int j = -iscale; j < h + iscale; j += iscale) {
|
||||
int a = i - (w / 2);
|
||||
int b = j - (h / 2);
|
||||
if (a * a + b * b <= r * r) {
|
||||
int tx = (int) (Math.floor((offsetX + i) / iscale) * iscale);
|
||||
int tz = (int) (Math.floor((offsetZ + j) / iscale) * iscale);
|
||||
BufferedImage t = getTile(gg, iscale, tx, tz, m);
|
||||
|
||||
if (t != null) {
|
||||
int rx = Math.floorMod((int) Math.floor(offsetX), iscale);
|
||||
int rz = Math.floorMod((int) Math.floor(offsetZ), iscale);
|
||||
g.drawImage(t, i - rx, j - rz, iscale, iscale, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (grid) {
|
||||
renderGrid(g, iscale, offsetX, offsetZ);
|
||||
}
|
||||
|
||||
p.end();
|
||||
|
||||
for (BlockPosition i : positions.k()) {
|
||||
if (!gg.contains(i)) {
|
||||
positions.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
handleFollow();
|
||||
renderOverlays(g, p.getMilliseconds());
|
||||
|
||||
if (!isVisible() || !getParent().isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long targetMs = eco ? 32 : 16;
|
||||
long sleepMs = Math.max(1, targetMs - (long) p.getMilliseconds());
|
||||
J.a(() -> {
|
||||
J.sleep(sleepMs);
|
||||
repaint();
|
||||
});
|
||||
}
|
||||
|
||||
private void renderGrid(Graphics2D g, int tileSize, double offsetX, double offsetZ) {
|
||||
g.setColor(GRID_COLOR);
|
||||
int rx = Math.floorMod((int) Math.floor(offsetX), tileSize);
|
||||
int rz = Math.floorMod((int) Math.floor(offsetZ), tileSize);
|
||||
for (int i = -tileSize; i < w + tileSize; i += tileSize) {
|
||||
g.drawLine(i - rx, 0, i - rx, h);
|
||||
}
|
||||
for (int j = -tileSize; j < h + tileSize; j += tileSize) {
|
||||
g.drawLine(0, j - rz, w, j - rz);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFollow() {
|
||||
if (follow && player != null) {
|
||||
animateTo(player.getLocation().getX(), player.getLocation().getZ());
|
||||
}
|
||||
}
|
||||
|
||||
private void renderOverlays(Graphics2D g, double frameMs) {
|
||||
renderEntities(g);
|
||||
|
||||
if (help) {
|
||||
renderOverlayHelp(g);
|
||||
} else if (debug) {
|
||||
renderOverlayDebug(g);
|
||||
}
|
||||
|
||||
renderStatusBar(g, frameMs);
|
||||
renderHoverOverlay(g, shift);
|
||||
|
||||
if (!notifications.isEmpty()) {
|
||||
renderNotification(g);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderStatusBar(Graphics2D g, double frameMs) {
|
||||
int y = h - STATUS_HEIGHT;
|
||||
g.setColor(STATUS_BG);
|
||||
g.fillRect(0, y, w, STATUS_HEIGHT);
|
||||
g.setColor(CARD_BORDER);
|
||||
g.drawLine(0, y, w, y);
|
||||
|
||||
g.setFont(FONT_STATUS);
|
||||
g.setColor(TEXT_SECONDARY);
|
||||
|
||||
double wx = getWorldX(w / 2.0);
|
||||
double wz = getWorldZ(h / 2.0);
|
||||
int fps = frameMs > 0 ? (int) (1000.0 / frameMs) : 0;
|
||||
|
||||
String left = String.format(" %s | %.1f bpp | %s x %s blocks",
|
||||
modeName(currentType), mscale,
|
||||
Form.f((int) (mscale * w)), Form.f((int) (mscale * h)));
|
||||
g.drawString(left, 8, y + 17);
|
||||
|
||||
String right = String.format("X: %s Z: %s | %d FPS ",
|
||||
Form.f((int) wx), Form.f((int) wz), fps);
|
||||
int rw = g.getFontMetrics().stringWidth(right);
|
||||
g.drawString(right, w - rw - 8, y + 17);
|
||||
|
||||
g.setColor(ACCENT);
|
||||
int modeW = g.getFontMetrics().stringWidth(" " + modeName(currentType));
|
||||
g.fillRect(0, y + 1, 3, STATUS_HEIGHT - 1);
|
||||
}
|
||||
|
||||
private void renderEntities(Graphics2D g) {
|
||||
Player b = null;
|
||||
|
||||
for (Player i : world.getPlayers()) {
|
||||
b = i;
|
||||
renderPlayerMarker(g, i.getLocation().getX(), i.getLocation().getZ(), i.getName());
|
||||
}
|
||||
|
||||
synchronized (lastEntities) {
|
||||
double dist = Double.MAX_VALUE;
|
||||
LivingEntity nearest = null;
|
||||
|
||||
for (LivingEntity i : lastEntities) {
|
||||
if (i instanceof Player) continue;
|
||||
renderMobMarker(g, i.getLocation().getX(), i.getLocation().getZ());
|
||||
if (shift) {
|
||||
double d = i.getLocation().distanceSquared(
|
||||
new Location(i.getWorld(), getWorldX(hx), i.getLocation().getY(), getWorldZ(hz)));
|
||||
if (d < dist) {
|
||||
dist = d;
|
||||
nearest = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nearest != null && shift) {
|
||||
double sx = getScreenX(nearest.getLocation().getX());
|
||||
double sz = getScreenZ(nearest.getLocation().getZ());
|
||||
g.setColor(MOB_COLOR);
|
||||
g.fillOval((int) sx - 6, (int) sz - 6, 12, 12);
|
||||
g.setColor(new Color(220, 80, 80, 60));
|
||||
g.fillOval((int) sx - 10, (int) sz - 10, 20, 20);
|
||||
|
||||
KList<String> k = new KList<>();
|
||||
k.add(Form.capitalizeWords(nearest.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " ")));
|
||||
k.add("Pos: " + nearest.getLocation().getBlockX() + ", " + nearest.getLocation().getBlockY() + ", " + nearest.getLocation().getBlockZ());
|
||||
k.add("HP: " + Form.f(nearest.getHealth(), 1) + " / " + Form.f(nearest.getAttribute(MAX_HEALTH).getValue(), 1));
|
||||
drawCard(w - CARD_PAD, CARD_PAD, 1, 0, g, k);
|
||||
}
|
||||
}
|
||||
|
||||
player = b;
|
||||
}
|
||||
|
||||
private void renderPlayerMarker(Graphics2D g, double x, double z, String name) {
|
||||
int sx = (int) getScreenX(x);
|
||||
int sz = (int) getScreenZ(z);
|
||||
g.setColor(new Color(80, 200, 120, 40));
|
||||
g.fillOval(sx - 12, sz - 12, 24, 24);
|
||||
g.setColor(PLAYER_COLOR);
|
||||
g.fillOval(sx - 5, sz - 5, 10, 10);
|
||||
g.setColor(new Color(40, 160, 80));
|
||||
g.drawOval(sx - 5, sz - 5, 10, 10);
|
||||
|
||||
g.setFont(FONT_CARD_BODY);
|
||||
g.setColor(TEXT_PRIMARY);
|
||||
int nw = g.getFontMetrics().stringWidth(name);
|
||||
g.drawString(name, sx - nw / 2, sz - 14);
|
||||
}
|
||||
|
||||
private void renderMobMarker(Graphics2D g, double x, double z) {
|
||||
int sx = (int) getScreenX(x);
|
||||
int sz = (int) getScreenZ(z);
|
||||
g.setColor(MOB_COLOR);
|
||||
g.fillRect(sx - 2, sz - 2, 4, 4);
|
||||
}
|
||||
|
||||
private void animateTo(double wx, double wz) {
|
||||
double cx = getWorldX(getWidth() / 2.0);
|
||||
double cz = getWorldZ(getHeight() / 2.0);
|
||||
ox += ((wx - cx) / mscale) * scale;
|
||||
oz += ((wz - cz) / mscale) * scale;
|
||||
}
|
||||
|
||||
private void renderHoverOverlay(Graphics2D g, boolean detailed) {
|
||||
IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(getWorldX(hx), getWorldZ(hz));
|
||||
IrisRegion region = engine.getComplex().getRegionStream().get(getWorldX(hx), getWorldZ(hz));
|
||||
KList<String> l = new KList<>();
|
||||
l.add(biome.getName());
|
||||
l.add(region.getName());
|
||||
l.add("Block " + (int) getWorldX(hx) + ", " + (int) getWorldZ(hz));
|
||||
if (detailed) {
|
||||
l.add("Chunk " + ((int) getWorldX(hx) >> 4) + ", " + ((int) getWorldZ(hz) >> 4));
|
||||
l.add("Region " + (((int) getWorldX(hx) >> 4) >> 5) + ", " + (((int) getWorldZ(hz) >> 4) >> 5));
|
||||
l.add("Key: " + biome.getLoadKey());
|
||||
l.add("File: " + biome.getLoadFile());
|
||||
}
|
||||
drawCard((float) hx + 16, (float) hz, 0, 0, g, l);
|
||||
}
|
||||
|
||||
private void renderOverlayDebug(Graphics2D g) {
|
||||
KList<String> l = new KList<>();
|
||||
l.add("Velocity: " + (int) velocity);
|
||||
l.add("Tiles: " + positions.size() + " HD / " + fastpositions.size() + " LQ");
|
||||
l.add("Workers: " + working.size() + " HD / " + workingfast.size() + " LQ");
|
||||
l.add("Center: " + Form.f((int) getWorldX(getWidth() / 2.0)) + ", " + Form.f((int) getWorldZ(getHeight() / 2.0)));
|
||||
drawCard(CARD_PAD, h - STATUS_HEIGHT - CARD_PAD, 0, 1, g, l);
|
||||
}
|
||||
|
||||
private void renderOverlayHelp(Graphics2D g) {
|
||||
KList<String> keys = new KList<>();
|
||||
KList<String> descs = new KList<>();
|
||||
keys.add("/"); descs.add("Toggle help");
|
||||
keys.add("R"); descs.add("Refresh tiles");
|
||||
keys.add("F"); descs.add("Follow player");
|
||||
keys.add("+/-"); descs.add("Zoom in/out");
|
||||
keys.add("\\"); descs.add("Reset zoom");
|
||||
keys.add("M"); descs.add("Cycle render mode");
|
||||
keys.add("P"); descs.add("Toggle tile quality");
|
||||
keys.add("E"); descs.add("Toggle 30/60 FPS");
|
||||
keys.add("G"); descs.add("Toggle grid");
|
||||
|
||||
int ff = 0;
|
||||
for (RenderType i : RenderType.values()) {
|
||||
ff++;
|
||||
keys.add(String.valueOf(ff));
|
||||
descs.add(modeName(i));
|
||||
}
|
||||
|
||||
keys.add("Shift"); descs.add("Detailed biome info");
|
||||
keys.add("Ctrl+Click"); descs.add("Teleport to cursor");
|
||||
keys.add("Alt+Click"); descs.add("Open biome in editor");
|
||||
|
||||
int maxKeyW = 0;
|
||||
g.setFont(FONT_HELP_KEY);
|
||||
for (String k : keys) {
|
||||
maxKeyW = Math.max(maxKeyW, g.getFontMetrics().stringWidth(k));
|
||||
}
|
||||
|
||||
int lineH = 20;
|
||||
int totalH = keys.size() * lineH + CARD_PAD * 2 + 4;
|
||||
int totalW = maxKeyW + 180 + CARD_PAD * 2;
|
||||
|
||||
drawCardBackground(g, CARD_PAD, CARD_PAD, totalW, totalH);
|
||||
|
||||
for (int i = 0; i < keys.size(); i++) {
|
||||
int y = CARD_PAD + 16 + i * lineH;
|
||||
|
||||
g.setFont(FONT_HELP_KEY);
|
||||
g.setColor(ACCENT);
|
||||
g.drawString(keys.get(i), CARD_PAD * 2, y);
|
||||
|
||||
g.setFont(FONT_CARD_BODY);
|
||||
g.setColor(TEXT_SECONDARY);
|
||||
g.drawString(descs.get(i), CARD_PAD * 2 + maxKeyW + 16, y);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderNotification(Graphics2D g) {
|
||||
int y = h - STATUS_HEIGHT - 50;
|
||||
g.setFont(FONT_NOTIFICATION);
|
||||
|
||||
KList<String> active = new KList<>();
|
||||
for (String i : notifications.k()) {
|
||||
if (M.ms() > notifications.get(i)) {
|
||||
notifications.remove(i);
|
||||
} else {
|
||||
active.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (active.isEmpty()) return;
|
||||
|
||||
String text = String.join(" | ", active);
|
||||
int tw = g.getFontMetrics().stringWidth(text);
|
||||
int th = g.getFontMetrics().getHeight();
|
||||
int px = (w - tw) / 2 - 16;
|
||||
int py = y - th / 2 - 8;
|
||||
int bw = tw + 32;
|
||||
int bh = th + 16;
|
||||
|
||||
drawCardBackground(g, px, py, bw, bh);
|
||||
g.setColor(TEXT_PRIMARY);
|
||||
g.drawString(text, px + 16, py + th + 4);
|
||||
}
|
||||
|
||||
private void drawCardBackground(Graphics2D g, int x, int y, int w, int h) {
|
||||
RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, w, h, CARD_RADIUS, CARD_RADIUS);
|
||||
g.setColor(CARD_BG);
|
||||
g.fill(rect);
|
||||
g.setColor(CARD_BORDER);
|
||||
g.draw(rect);
|
||||
}
|
||||
|
||||
private void drawCard(float x, float y, double pushX, double pushZ, Graphics2D g, KList<String> text) {
|
||||
g.setFont(FONT_CARD_BODY);
|
||||
int lineH = g.getFontMetrics().getHeight();
|
||||
int cardW = 0;
|
||||
for (String i : text) {
|
||||
cardW = Math.max(cardW, g.getFontMetrics().stringWidth(i));
|
||||
}
|
||||
cardW += CARD_PAD * 2;
|
||||
int cardH = text.size() * lineH + CARD_PAD * 2 - 4;
|
||||
|
||||
int cx = (int) (x - cardW * pushX);
|
||||
int cy = (int) (y - cardH * pushZ);
|
||||
|
||||
drawCardBackground(g, cx, cy, cardW, cardH);
|
||||
|
||||
int ty = cy + CARD_PAD + lineH - 4;
|
||||
for (int i = 0; i < text.size(); i++) {
|
||||
g.setColor(i == 0 ? TEXT_PRIMARY : TEXT_SECONDARY);
|
||||
g.setFont(i == 0 ? FONT_CARD_TITLE : FONT_CARD_BODY);
|
||||
g.drawString(text.get(i), cx + CARD_PAD, ty + i * lineH);
|
||||
}
|
||||
}
|
||||
|
||||
public void mouseWheelMoved(MouseWheelEvent e) {
|
||||
int notches = e.getWheelRotation();
|
||||
if (e.isControlDown()) return;
|
||||
|
||||
double m0 = mscale;
|
||||
double m1 = m0 + ((0.25 * m0) * notches);
|
||||
m1 = Math.max(m1, 0.00001);
|
||||
if (m1 == m0) return;
|
||||
|
||||
positions.clear();
|
||||
fastpositions.clear();
|
||||
|
||||
Point p = e.getPoint();
|
||||
double sx = p.getX();
|
||||
double sz = p.getY();
|
||||
|
||||
double newOxp = scale * ((m0 / m1) * (sx + (oxp / scale)) - sx);
|
||||
double newOzp = scale * ((m0 / m1) * (sz + (ozp / scale)) - sz);
|
||||
|
||||
mscale = m1;
|
||||
oxp = newOxp;
|
||||
ozp = newOzp;
|
||||
ox = oxp;
|
||||
oz = ozp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (control) teleport();
|
||||
else if (alt) open();
|
||||
}
|
||||
|
||||
@Override public void mousePressed(MouseEvent e) { }
|
||||
@Override public void mouseReleased(MouseEvent e) { }
|
||||
@Override public void mouseEntered(MouseEvent e) { }
|
||||
@Override public void mouseExited(MouseEvent e) { }
|
||||
|
||||
private void open() {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
File r = null;
|
||||
switch (currentType) {
|
||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
|
||||
r = complex.getTrueBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
|
||||
case BIOME_LAND -> r = complex.getLandBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
|
||||
case BIOME_SEA -> r = complex.getSeaBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
|
||||
case REGION -> r = complex.getRegionStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
|
||||
case CAVE_LAND -> r = complex.getCaveBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
|
||||
}
|
||||
if (r != null) {
|
||||
notify("Opened " + r.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void teleport() {
|
||||
J.s(() -> {
|
||||
if (player != null) {
|
||||
int xx = (int) getWorldX(hx);
|
||||
int zz = (int) getWorldZ(hz);
|
||||
int yy = player.getWorld().getHighestBlockYAt(xx, zz) + 1;
|
||||
player.teleport(new Location(player.getWorld(), xx, yy, zz));
|
||||
notify("Teleported to " + xx + ", " + yy + ", " + zz);
|
||||
} else {
|
||||
notify("No player in world");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.components;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
|
||||
import art.arcane.iris.util.project.interpolation.IrisInterpolation;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
public class IrisRenderer {
|
||||
private static final int BLUE = Color.BLUE.getRGB();
|
||||
private static final int YELLOW = Color.YELLOW.getRGB();
|
||||
private static final int GREEN = Color.GREEN.getRGB();
|
||||
|
||||
private final Engine renderer;
|
||||
|
||||
public IrisRenderer(Engine renderer) {
|
||||
this.renderer = renderer;
|
||||
}
|
||||
|
||||
public BufferedImage render(double sx, double sz, double size, int resolution, RenderType currentType) {
|
||||
BufferedImage image = new BufferedImage(resolution, resolution, BufferedImage.TYPE_INT_RGB);
|
||||
int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
|
||||
BiFunction<Double, Double, Integer> colorFunction = (d, dx) -> 0;
|
||||
|
||||
switch (currentType) {
|
||||
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD ->
|
||||
colorFunction = (x, z) -> renderer.getComplex().getTrueBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
|
||||
case BIOME_LAND ->
|
||||
colorFunction = (x, z) -> renderer.getComplex().getLandBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
|
||||
case BIOME_SEA ->
|
||||
colorFunction = (x, z) -> renderer.getComplex().getSeaBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
|
||||
case REGION ->
|
||||
colorFunction = (x, z) -> renderer.getComplex().getRegionStream().get(x, z).getColor(renderer.getComplex(), currentType).getRGB();
|
||||
case CAVE_LAND ->
|
||||
colorFunction = (x, z) -> renderer.getComplex().getCaveBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
|
||||
case HEIGHT ->
|
||||
colorFunction = (x, z) -> Color.getHSBColor(renderer.getComplex().getHeightStream().get(x, z).floatValue(), 1f, 1f).getRGB();
|
||||
case CONTINENT -> colorFunction = (x, z) -> {
|
||||
IrisBiome b = renderer.getBiome((int) Math.round(x), renderer.getMaxHeight() - 1, (int) Math.round(z));
|
||||
IrisBiomeGeneratorLink g = b.getGenerators().get(0);
|
||||
if (g.getMax() <= 0) return BLUE;
|
||||
if (g.getMin() < 0) return YELLOW;
|
||||
return GREEN;
|
||||
};
|
||||
}
|
||||
|
||||
double x, z;
|
||||
for (int i = 0; i < resolution; i++) {
|
||||
x = IrisInterpolation.lerp(sx, sx + size, (double) i / (double) resolution);
|
||||
for (int j = 0; j < resolution; j++) {
|
||||
z = IrisInterpolation.lerp(sz, sz + size, (double) j / (double) resolution);
|
||||
pixels[j * resolution + i] = colorFunction.apply(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -16,8 +16,8 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.gui.components;
|
||||
package art.arcane.iris.core.gui.components;
|
||||
|
||||
public enum RenderType {
|
||||
BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, HEIGHT, OBJECT_LOAD, DECORATOR_LOAD, LAYER_LOAD
|
||||
BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, HEIGHT, OBJECT_LOAD, DECORATOR_LOAD, CONTINENT, LAYER_LOAD
|
||||
}
|
||||
+2
-2
@@ -16,9 +16,9 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.gui.components;
|
||||
package art.arcane.iris.core.gui.components;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.*;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Renderer {
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.gui.components;
|
||||
package art.arcane.iris.core.gui.components;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
@@ -0,0 +1,58 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.WorldCreator;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
final class BukkitPublicBackend implements WorldLifecycleBackend {
|
||||
private final CapabilitySnapshot capabilities;
|
||||
|
||||
BukkitPublicBackend(CapabilitySnapshot capabilities) {
|
||||
this.capabilities = capabilities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<World> create(WorldLifecycleRequest request) {
|
||||
World existing = Bukkit.getWorld(request.worldName());
|
||||
if (existing != null) {
|
||||
return CompletableFuture.completedFuture(existing);
|
||||
}
|
||||
|
||||
WorldCreator creator = request.toWorldCreator();
|
||||
if (request.generator() != null) {
|
||||
WorldLifecycleStaging.stageGenerator(request.worldName(), request.generator(), request.biomeProvider());
|
||||
WorldLifecycleStaging.stageStemGenerator(request.worldName(), request.generator());
|
||||
}
|
||||
|
||||
try {
|
||||
World world = creator.createWorld();
|
||||
return CompletableFuture.completedFuture(world);
|
||||
} catch (Throwable e) {
|
||||
return CompletableFuture.failedFuture(WorldLifecycleSupport.unwrap(e));
|
||||
} finally {
|
||||
WorldLifecycleStaging.clearAll(request.worldName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unload(World world, boolean save) {
|
||||
return WorldLifecycleSupport.unloadWorld(capabilities, world, save);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String backendName() {
|
||||
return "bukkit_public";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String describeSelectionReason() {
|
||||
return "public Bukkit world lifecycle path";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
final class CapabilityResolution {
|
||||
private CapabilityResolution() {
|
||||
}
|
||||
|
||||
static Method resolveCreateLevelMethod(Class<?> owner) throws NoSuchMethodException {
|
||||
Method current = resolveMethod(owner, "createLevel", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 3
|
||||
&& "LevelStem".equals(params[0].getSimpleName())
|
||||
&& "WorldLoadingInfoAndData".equals(params[1].getSimpleName())
|
||||
&& "WorldDataAndGenSettings".equals(params[2].getSimpleName());
|
||||
});
|
||||
if (current != null) {
|
||||
return current;
|
||||
}
|
||||
|
||||
Method legacy = resolveMethod(owner, "createLevel", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 4
|
||||
&& "LevelStem".equals(params[0].getSimpleName())
|
||||
&& "WorldLoadingInfo".equals(params[1].getSimpleName())
|
||||
&& "LevelStorageAccess".equals(params[2].getSimpleName())
|
||||
&& "PrimaryLevelData".equals(params[3].getSimpleName());
|
||||
});
|
||||
if (legacy != null) {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
throw new NoSuchMethodException(owner.getName() + "#createLevel");
|
||||
}
|
||||
|
||||
static Method resolveLevelStorageAccessMethod(Class<?> owner) throws NoSuchMethodException {
|
||||
Method exactValidate = resolveMethod(owner, "validateAndCreateAccess", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 2
|
||||
&& String.class.equals(params[0])
|
||||
&& "ResourceKey".equals(params[1].getSimpleName())
|
||||
&& "LevelStorageAccess".equals(method.getReturnType().getSimpleName());
|
||||
});
|
||||
if (exactValidate != null) {
|
||||
return exactValidate;
|
||||
}
|
||||
|
||||
Method oneArgValidate = resolveMethod(owner, "validateAndCreateAccess", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 1
|
||||
&& String.class.equals(params[0])
|
||||
&& "LevelStorageAccess".equals(method.getReturnType().getSimpleName());
|
||||
});
|
||||
if (oneArgValidate != null) {
|
||||
return oneArgValidate;
|
||||
}
|
||||
|
||||
Method exactCreate = resolveMethod(owner, "createAccess", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 2
|
||||
&& String.class.equals(params[0])
|
||||
&& "ResourceKey".equals(params[1].getSimpleName())
|
||||
&& "LevelStorageAccess".equals(method.getReturnType().getSimpleName());
|
||||
});
|
||||
if (exactCreate != null) {
|
||||
return exactCreate;
|
||||
}
|
||||
|
||||
Method oneArgCreate = resolveMethod(owner, "createAccess", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 1
|
||||
&& String.class.equals(params[0])
|
||||
&& "LevelStorageAccess".equals(method.getReturnType().getSimpleName());
|
||||
});
|
||||
if (oneArgCreate != null) {
|
||||
return oneArgCreate;
|
||||
}
|
||||
|
||||
throw new NoSuchMethodException(owner.getName() + "#validateAndCreateAccess/createAccess");
|
||||
}
|
||||
|
||||
static Method resolvePaperWorldDataMethod(Class<?> owner) throws NoSuchMethodException {
|
||||
Method current = resolveMethod(owner, "loadWorldData", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 3
|
||||
&& "MinecraftServer".equals(params[0].getSimpleName())
|
||||
&& "ResourceKey".equals(params[1].getSimpleName())
|
||||
&& String.class.equals(params[2])
|
||||
&& "LoadedWorldData".equals(method.getReturnType().getSimpleName());
|
||||
});
|
||||
if (current != null) {
|
||||
return current;
|
||||
}
|
||||
|
||||
Method legacy = resolveMethod(owner, "getLevelData", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 1 && "LevelStorageAccess".equals(params[0].getSimpleName());
|
||||
});
|
||||
if (legacy != null) {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
throw new NoSuchMethodException(owner.getName() + "#loadWorldData/getLevelData");
|
||||
}
|
||||
|
||||
static Constructor<?> resolveWorldLoadingInfoConstructor(Class<?> owner) throws NoSuchMethodException {
|
||||
Constructor<?> current = resolveConstructor(owner, constructor -> {
|
||||
Class<?>[] params = constructor.getParameterTypes();
|
||||
return params.length == 4
|
||||
&& "Environment".equals(params[0].getSimpleName())
|
||||
&& "ResourceKey".equals(params[1].getSimpleName())
|
||||
&& "ResourceKey".equals(params[2].getSimpleName())
|
||||
&& boolean.class.equals(params[3]);
|
||||
});
|
||||
if (current != null) {
|
||||
return current;
|
||||
}
|
||||
|
||||
Constructor<?> legacy = resolveConstructor(owner, constructor -> {
|
||||
Class<?>[] params = constructor.getParameterTypes();
|
||||
return params.length == 5
|
||||
&& int.class.equals(params[0])
|
||||
&& String.class.equals(params[1])
|
||||
&& String.class.equals(params[2])
|
||||
&& "ResourceKey".equals(params[3].getSimpleName())
|
||||
&& boolean.class.equals(params[4]);
|
||||
});
|
||||
if (legacy != null) {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
throw new NoSuchMethodException(owner.getName() + "#<init>");
|
||||
}
|
||||
|
||||
static Constructor<?> resolveWorldLoadingInfoAndDataConstructor(Class<?> owner) throws NoSuchMethodException {
|
||||
Constructor<?> constructor = resolveConstructor(owner, candidate -> {
|
||||
Class<?>[] params = candidate.getParameterTypes();
|
||||
return params.length == 2
|
||||
&& "WorldLoadingInfo".equals(params[0].getSimpleName())
|
||||
&& "LoadedWorldData".equals(params[1].getSimpleName());
|
||||
});
|
||||
if (constructor == null) {
|
||||
throw new NoSuchMethodException(owner.getName() + "#<init>");
|
||||
}
|
||||
return constructor;
|
||||
}
|
||||
|
||||
static Method resolveCreateNewWorldDataMethod(Class<?> owner) throws NoSuchMethodException {
|
||||
Method method = resolveMethod(owner, "createNewWorldData", candidate -> {
|
||||
Class<?>[] params = candidate.getParameterTypes();
|
||||
return params.length == 5
|
||||
&& "DedicatedServerSettings".equals(params[0].getSimpleName())
|
||||
&& "DataLoadContext".equals(params[1].getSimpleName())
|
||||
&& "Registry".equals(params[2].getSimpleName())
|
||||
&& boolean.class.equals(params[3])
|
||||
&& boolean.class.equals(params[4]);
|
||||
});
|
||||
if (method == null) {
|
||||
throw new NoSuchMethodException(owner.getName() + "#createNewWorldData");
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
static Method resolveServerRegistryAccessMethod(Class<?> owner) throws NoSuchMethodException {
|
||||
Method method = resolveMethod(owner, "registryAccess", candidate -> candidate.getParameterCount() == 0
|
||||
&& !void.class.equals(candidate.getReturnType()));
|
||||
if (method == null) {
|
||||
throw new NoSuchMethodException(owner.getName() + "#registryAccess");
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
static Method resolveMethod(Class<?> owner, String name, Predicate<Method> predicate) {
|
||||
Method selected = scanMethods(owner.getMethods(), name, predicate);
|
||||
if (selected != null) {
|
||||
return selected;
|
||||
}
|
||||
|
||||
Class<?> current = owner;
|
||||
while (current != null) {
|
||||
selected = scanMethods(current.getDeclaredMethods(), name, predicate);
|
||||
if (selected != null) {
|
||||
selected.setAccessible(true);
|
||||
return selected;
|
||||
}
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static Field resolveField(Class<?> owner, String name) throws NoSuchFieldException {
|
||||
Class<?> current = owner;
|
||||
while (current != null) {
|
||||
try {
|
||||
Field field = current.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
}
|
||||
throw new NoSuchFieldException(owner.getName() + "#" + name);
|
||||
}
|
||||
|
||||
private static Method scanMethods(Method[] methods, String name, Predicate<Method> predicate) {
|
||||
for (Method method : methods) {
|
||||
if (!method.getName().equals(name)) {
|
||||
continue;
|
||||
}
|
||||
if (predicate.test(method)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Constructor<?> resolveConstructor(Class<?> owner, Predicate<Constructor<?>> predicate) {
|
||||
for (Constructor<?> constructor : owner.getConstructors()) {
|
||||
if (predicate.test(constructor)) {
|
||||
return constructor;
|
||||
}
|
||||
}
|
||||
for (Constructor<?> constructor : owner.getDeclaredConstructors()) {
|
||||
if (predicate.test(constructor)) {
|
||||
constructor.setAccessible(true);
|
||||
return constructor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class CapabilitySnapshot {
|
||||
public enum PaperLikeFlavor {
|
||||
CURRENT_INFO_AND_DATA,
|
||||
LEGACY_STORAGE_ACCESS,
|
||||
UNSUPPORTED
|
||||
}
|
||||
|
||||
private final ServerFamily serverFamily;
|
||||
private final boolean regionizedRuntime;
|
||||
private final Object worldsProvider;
|
||||
private final Class<?> worldsLevelStemClass;
|
||||
private final Class<?> worldsGeneratorTypeClass;
|
||||
private final String worldsProviderResolution;
|
||||
private final Object bukkitServer;
|
||||
private final Object minecraftServer;
|
||||
private final Method createLevelMethod;
|
||||
private final PaperLikeFlavor paperLikeFlavor;
|
||||
private final Class<?> paperWorldLoaderClass;
|
||||
private final Method paperWorldDataMethod;
|
||||
private final Constructor<?> worldLoadingInfoConstructor;
|
||||
private final Constructor<?> worldLoadingInfoAndDataConstructor;
|
||||
private final Method createNewWorldDataMethod;
|
||||
private final Method levelStorageAccessMethod;
|
||||
private final Field worldLoaderContextField;
|
||||
private final Method serverRegistryAccessMethod;
|
||||
private final Field settingsField;
|
||||
private final Field optionsField;
|
||||
private final Method isDemoMethod;
|
||||
private final Method unloadWorldAsyncMethod;
|
||||
private final Method chunkAtAsyncMethod;
|
||||
private final Method removeLevelMethod;
|
||||
private final String paperLikeResolution;
|
||||
|
||||
private CapabilitySnapshot(
|
||||
ServerFamily serverFamily,
|
||||
boolean regionizedRuntime,
|
||||
Object worldsProvider,
|
||||
Class<?> worldsLevelStemClass,
|
||||
Class<?> worldsGeneratorTypeClass,
|
||||
String worldsProviderResolution,
|
||||
Object bukkitServer,
|
||||
Object minecraftServer,
|
||||
Method createLevelMethod,
|
||||
PaperLikeFlavor paperLikeFlavor,
|
||||
Class<?> paperWorldLoaderClass,
|
||||
Method paperWorldDataMethod,
|
||||
Constructor<?> worldLoadingInfoConstructor,
|
||||
Constructor<?> worldLoadingInfoAndDataConstructor,
|
||||
Method createNewWorldDataMethod,
|
||||
Method levelStorageAccessMethod,
|
||||
Field worldLoaderContextField,
|
||||
Method serverRegistryAccessMethod,
|
||||
Field settingsField,
|
||||
Field optionsField,
|
||||
Method isDemoMethod,
|
||||
Method unloadWorldAsyncMethod,
|
||||
Method chunkAtAsyncMethod,
|
||||
Method removeLevelMethod,
|
||||
String paperLikeResolution
|
||||
) {
|
||||
this.serverFamily = serverFamily;
|
||||
this.regionizedRuntime = regionizedRuntime;
|
||||
this.worldsProvider = worldsProvider;
|
||||
this.worldsLevelStemClass = worldsLevelStemClass;
|
||||
this.worldsGeneratorTypeClass = worldsGeneratorTypeClass;
|
||||
this.worldsProviderResolution = worldsProviderResolution;
|
||||
this.bukkitServer = bukkitServer;
|
||||
this.minecraftServer = minecraftServer;
|
||||
this.createLevelMethod = createLevelMethod;
|
||||
this.paperLikeFlavor = paperLikeFlavor;
|
||||
this.paperWorldLoaderClass = paperWorldLoaderClass;
|
||||
this.paperWorldDataMethod = paperWorldDataMethod;
|
||||
this.worldLoadingInfoConstructor = worldLoadingInfoConstructor;
|
||||
this.worldLoadingInfoAndDataConstructor = worldLoadingInfoAndDataConstructor;
|
||||
this.createNewWorldDataMethod = createNewWorldDataMethod;
|
||||
this.levelStorageAccessMethod = levelStorageAccessMethod;
|
||||
this.worldLoaderContextField = worldLoaderContextField;
|
||||
this.serverRegistryAccessMethod = serverRegistryAccessMethod;
|
||||
this.settingsField = settingsField;
|
||||
this.optionsField = optionsField;
|
||||
this.isDemoMethod = isDemoMethod;
|
||||
this.unloadWorldAsyncMethod = unloadWorldAsyncMethod;
|
||||
this.chunkAtAsyncMethod = chunkAtAsyncMethod;
|
||||
this.removeLevelMethod = removeLevelMethod;
|
||||
this.paperLikeResolution = paperLikeResolution;
|
||||
}
|
||||
|
||||
public static CapabilitySnapshot probe() {
|
||||
Server server = Bukkit.getServer();
|
||||
Object bukkitServer = server;
|
||||
boolean regionizedRuntime = FoliaScheduler.isRegionizedRuntime(server);
|
||||
ServerFamily serverFamily = detectServerFamily(server, regionizedRuntime);
|
||||
|
||||
Object worldsProvider = null;
|
||||
Class<?> worldsLevelStemClass = null;
|
||||
Class<?> worldsGeneratorTypeClass = null;
|
||||
String worldsProviderResolution = "inactive";
|
||||
try {
|
||||
Object[] worldsProviderData = resolveWorldsProvider();
|
||||
worldsProvider = worldsProviderData[0];
|
||||
worldsLevelStemClass = (Class<?>) worldsProviderData[1];
|
||||
worldsGeneratorTypeClass = (Class<?>) worldsProviderData[2];
|
||||
worldsProviderResolution = (String) worldsProviderData[3];
|
||||
} catch (Throwable e) {
|
||||
worldsProviderResolution = e.getClass().getSimpleName() + ": " + String.valueOf(e.getMessage());
|
||||
}
|
||||
|
||||
Object minecraftServer = null;
|
||||
Method createLevelMethod = null;
|
||||
PaperLikeFlavor paperLikeFlavor = PaperLikeFlavor.UNSUPPORTED;
|
||||
Class<?> paperWorldLoaderClass = null;
|
||||
Method paperWorldDataMethod = null;
|
||||
Constructor<?> worldLoadingInfoConstructor = null;
|
||||
Constructor<?> worldLoadingInfoAndDataConstructor = null;
|
||||
Method createNewWorldDataMethod = null;
|
||||
Method levelStorageAccessMethod = null;
|
||||
Field worldLoaderContextField = null;
|
||||
Method serverRegistryAccessMethod = null;
|
||||
Field settingsField = null;
|
||||
Field optionsField = null;
|
||||
Method isDemoMethod = null;
|
||||
Method removeLevelMethod = null;
|
||||
String paperLikeResolution = "inactive";
|
||||
|
||||
try {
|
||||
if (bukkitServer != null) {
|
||||
Method getServerMethod = CapabilityResolution.resolveMethod(bukkitServer.getClass(), "getServer", method -> method.getParameterCount() == 0);
|
||||
if (getServerMethod != null) {
|
||||
minecraftServer = getServerMethod.invoke(bukkitServer);
|
||||
}
|
||||
}
|
||||
|
||||
if (minecraftServer != null) {
|
||||
Class<?> minecraftServerClass = Class.forName("net.minecraft.server.MinecraftServer");
|
||||
if (!minecraftServerClass.isInstance(minecraftServer)) {
|
||||
throw new IllegalStateException("resolved server is not a MinecraftServer: " + minecraftServer.getClass().getName());
|
||||
}
|
||||
|
||||
createLevelMethod = CapabilityResolution.resolveCreateLevelMethod(minecraftServer.getClass());
|
||||
removeLevelMethod = CapabilityResolution.resolveMethod(minecraftServer.getClass(), "removeLevel", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 1 && "ServerLevel".equals(params[0].getSimpleName());
|
||||
});
|
||||
worldLoaderContextField = CapabilityResolution.resolveField(minecraftServer.getClass(), "worldLoaderContext");
|
||||
serverRegistryAccessMethod = CapabilityResolution.resolveServerRegistryAccessMethod(minecraftServer.getClass());
|
||||
settingsField = CapabilityResolution.resolveField(minecraftServer.getClass(), "settings");
|
||||
optionsField = CapabilityResolution.resolveField(minecraftServer.getClass(), "options");
|
||||
isDemoMethod = CapabilityResolution.resolveMethod(minecraftServer.getClass(), "isDemo", method -> method.getParameterCount() == 0 && boolean.class.equals(method.getReturnType()));
|
||||
|
||||
Class<?> mainClass = Class.forName("net.minecraft.server.Main");
|
||||
createNewWorldDataMethod = CapabilityResolution.resolveCreateNewWorldDataMethod(mainClass);
|
||||
|
||||
Class<?> paperLoaderCandidate = Class.forName("io.papermc.paper.world.PaperWorldLoader");
|
||||
paperWorldLoaderClass = paperLoaderCandidate;
|
||||
paperWorldDataMethod = CapabilityResolution.resolvePaperWorldDataMethod(paperLoaderCandidate);
|
||||
Class<?> worldLoadingInfoClass = Class.forName("io.papermc.paper.world.PaperWorldLoader$WorldLoadingInfo");
|
||||
worldLoadingInfoConstructor = CapabilityResolution.resolveWorldLoadingInfoConstructor(worldLoadingInfoClass);
|
||||
|
||||
if (createLevelMethod.getParameterCount() == 3) {
|
||||
Class<?> worldLoadingInfoAndDataClass = Class.forName("io.papermc.paper.world.PaperWorldLoader$WorldLoadingInfoAndData");
|
||||
worldLoadingInfoAndDataConstructor = CapabilityResolution.resolveWorldLoadingInfoAndDataConstructor(worldLoadingInfoAndDataClass);
|
||||
paperLikeFlavor = PaperLikeFlavor.CURRENT_INFO_AND_DATA;
|
||||
} else {
|
||||
Class<?> levelStorageSourceClass = Class.forName("net.minecraft.world.level.storage.LevelStorageSource");
|
||||
levelStorageAccessMethod = CapabilityResolution.resolveLevelStorageAccessMethod(levelStorageSourceClass);
|
||||
paperLikeFlavor = PaperLikeFlavor.LEGACY_STORAGE_ACCESS;
|
||||
}
|
||||
|
||||
paperLikeResolution = "available(flavor=" + paperLikeFlavor.name().toLowerCase(Locale.ROOT)
|
||||
+ ", createLevel=" + createLevelMethod.toGenericString() + ")";
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
paperLikeResolution = e.getClass().getSimpleName() + ": " + String.valueOf(e.getMessage());
|
||||
createLevelMethod = null;
|
||||
paperLikeFlavor = PaperLikeFlavor.UNSUPPORTED;
|
||||
paperWorldLoaderClass = null;
|
||||
paperWorldDataMethod = null;
|
||||
worldLoadingInfoConstructor = null;
|
||||
worldLoadingInfoAndDataConstructor = null;
|
||||
createNewWorldDataMethod = null;
|
||||
levelStorageAccessMethod = null;
|
||||
worldLoaderContextField = null;
|
||||
serverRegistryAccessMethod = null;
|
||||
settingsField = null;
|
||||
optionsField = null;
|
||||
isDemoMethod = null;
|
||||
removeLevelMethod = null;
|
||||
}
|
||||
|
||||
Method unloadWorldAsyncMethod = null;
|
||||
try {
|
||||
if (bukkitServer != null) {
|
||||
unloadWorldAsyncMethod = CapabilityResolution.resolveMethod(bukkitServer.getClass(), "unloadWorldAsync", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 3
|
||||
&& World.class.equals(params[0])
|
||||
&& boolean.class.equals(params[1])
|
||||
&& "Consumer".equals(params[2].getSimpleName());
|
||||
});
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
unloadWorldAsyncMethod = null;
|
||||
}
|
||||
|
||||
Method chunkAtAsyncMethod = null;
|
||||
try {
|
||||
chunkAtAsyncMethod = CapabilityResolution.resolveMethod(World.class, "getChunkAtAsync", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 3
|
||||
&& int.class.equals(params[0])
|
||||
&& int.class.equals(params[1])
|
||||
&& boolean.class.equals(params[2]);
|
||||
});
|
||||
} catch (Throwable ignored) {
|
||||
chunkAtAsyncMethod = null;
|
||||
}
|
||||
|
||||
return new CapabilitySnapshot(
|
||||
serverFamily,
|
||||
regionizedRuntime,
|
||||
worldsProvider,
|
||||
worldsLevelStemClass,
|
||||
worldsGeneratorTypeClass,
|
||||
worldsProviderResolution,
|
||||
bukkitServer,
|
||||
minecraftServer,
|
||||
createLevelMethod,
|
||||
paperLikeFlavor,
|
||||
paperWorldLoaderClass,
|
||||
paperWorldDataMethod,
|
||||
worldLoadingInfoConstructor,
|
||||
worldLoadingInfoAndDataConstructor,
|
||||
createNewWorldDataMethod,
|
||||
levelStorageAccessMethod,
|
||||
worldLoaderContextField,
|
||||
serverRegistryAccessMethod,
|
||||
settingsField,
|
||||
optionsField,
|
||||
isDemoMethod,
|
||||
unloadWorldAsyncMethod,
|
||||
chunkAtAsyncMethod,
|
||||
removeLevelMethod,
|
||||
paperLikeResolution
|
||||
);
|
||||
}
|
||||
|
||||
public static CapabilitySnapshot forTesting(ServerFamily serverFamily, boolean regionizedRuntime, boolean worldsProviderHealthy, boolean paperLikeRuntimeHealthy) {
|
||||
Object minecraftServer = paperLikeRuntimeHealthy ? new TestingPaperLikeServer("datapack-registry", "server-registry") : null;
|
||||
Method createLevelMethod = null;
|
||||
Field worldLoaderContextField = null;
|
||||
Method serverRegistryAccessMethod = null;
|
||||
try {
|
||||
createLevelMethod = paperLikeRuntimeHealthy
|
||||
? TestingPaperLikeServer.class.getDeclaredMethod("createLevel", Object.class, Object.class, Object.class)
|
||||
: null;
|
||||
worldLoaderContextField = paperLikeRuntimeHealthy
|
||||
? CapabilityResolution.resolveField(TestingPaperLikeServer.class, "worldLoaderContext")
|
||||
: null;
|
||||
serverRegistryAccessMethod = paperLikeRuntimeHealthy
|
||||
? CapabilityResolution.resolveServerRegistryAccessMethod(TestingPaperLikeServer.class)
|
||||
: null;
|
||||
} catch (NoSuchMethodException | NoSuchFieldException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
return new CapabilitySnapshot(
|
||||
serverFamily,
|
||||
regionizedRuntime,
|
||||
worldsProviderHealthy ? new Object() : null,
|
||||
worldsProviderHealthy ? Object.class : null,
|
||||
worldsProviderHealthy ? Object.class : null,
|
||||
worldsProviderHealthy ? "test-provider" : "inactive",
|
||||
null,
|
||||
minecraftServer,
|
||||
createLevelMethod,
|
||||
paperLikeRuntimeHealthy ? PaperLikeFlavor.CURRENT_INFO_AND_DATA : PaperLikeFlavor.UNSUPPORTED,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
worldLoaderContextField,
|
||||
serverRegistryAccessMethod,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
paperLikeRuntimeHealthy ? "available(test)" : "unsupported(test)"
|
||||
);
|
||||
}
|
||||
|
||||
public static CapabilitySnapshot forTestingRuntimeRegistries(ServerFamily serverFamily, boolean regionizedRuntime, Object datapackDimensions, Object serverRegistryAccess) {
|
||||
TestingPaperLikeServer minecraftServer = new TestingPaperLikeServer(datapackDimensions, serverRegistryAccess);
|
||||
Method createLevelMethod;
|
||||
Field worldLoaderContextField;
|
||||
Method registryAccessMethod;
|
||||
try {
|
||||
createLevelMethod = TestingPaperLikeServer.class.getDeclaredMethod("createLevel", Object.class, Object.class, Object.class);
|
||||
worldLoaderContextField = CapabilityResolution.resolveField(TestingPaperLikeServer.class, "worldLoaderContext");
|
||||
registryAccessMethod = CapabilityResolution.resolveServerRegistryAccessMethod(TestingPaperLikeServer.class);
|
||||
} catch (NoSuchMethodException | NoSuchFieldException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
return new CapabilitySnapshot(
|
||||
serverFamily,
|
||||
regionizedRuntime,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"inactive",
|
||||
null,
|
||||
minecraftServer,
|
||||
createLevelMethod,
|
||||
PaperLikeFlavor.CURRENT_INFO_AND_DATA,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
worldLoaderContextField,
|
||||
registryAccessMethod,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"available(test-runtime-registries)"
|
||||
);
|
||||
}
|
||||
|
||||
public ServerFamily serverFamily() {
|
||||
return serverFamily;
|
||||
}
|
||||
|
||||
public boolean regionizedRuntime() {
|
||||
return regionizedRuntime;
|
||||
}
|
||||
|
||||
public Object worldsProvider() {
|
||||
return worldsProvider;
|
||||
}
|
||||
|
||||
public Class<?> worldsLevelStemClass() {
|
||||
return worldsLevelStemClass;
|
||||
}
|
||||
|
||||
public Class<?> worldsGeneratorTypeClass() {
|
||||
return worldsGeneratorTypeClass;
|
||||
}
|
||||
|
||||
public Object bukkitServer() {
|
||||
return bukkitServer;
|
||||
}
|
||||
|
||||
public Object minecraftServer() {
|
||||
return minecraftServer;
|
||||
}
|
||||
|
||||
public Method createLevelMethod() {
|
||||
return createLevelMethod;
|
||||
}
|
||||
|
||||
public PaperLikeFlavor paperLikeFlavor() {
|
||||
return paperLikeFlavor;
|
||||
}
|
||||
|
||||
public Class<?> paperWorldLoaderClass() {
|
||||
return paperWorldLoaderClass;
|
||||
}
|
||||
|
||||
public Method paperWorldDataMethod() {
|
||||
return paperWorldDataMethod;
|
||||
}
|
||||
|
||||
public Constructor<?> worldLoadingInfoConstructor() {
|
||||
return worldLoadingInfoConstructor;
|
||||
}
|
||||
|
||||
public Constructor<?> worldLoadingInfoAndDataConstructor() {
|
||||
return worldLoadingInfoAndDataConstructor;
|
||||
}
|
||||
|
||||
public Method createNewWorldDataMethod() {
|
||||
return createNewWorldDataMethod;
|
||||
}
|
||||
|
||||
public Method levelStorageAccessMethod() {
|
||||
return levelStorageAccessMethod;
|
||||
}
|
||||
|
||||
public Field worldLoaderContextField() {
|
||||
return worldLoaderContextField;
|
||||
}
|
||||
|
||||
public Method serverRegistryAccessMethod() {
|
||||
return serverRegistryAccessMethod;
|
||||
}
|
||||
|
||||
public Field settingsField() {
|
||||
return settingsField;
|
||||
}
|
||||
|
||||
public Field optionsField() {
|
||||
return optionsField;
|
||||
}
|
||||
|
||||
public Method isDemoMethod() {
|
||||
return isDemoMethod;
|
||||
}
|
||||
|
||||
public Method unloadWorldAsyncMethod() {
|
||||
return unloadWorldAsyncMethod;
|
||||
}
|
||||
|
||||
public Method chunkAtAsyncMethod() {
|
||||
return chunkAtAsyncMethod;
|
||||
}
|
||||
|
||||
public Method removeLevelMethod() {
|
||||
return removeLevelMethod;
|
||||
}
|
||||
|
||||
public boolean hasWorldsProvider() {
|
||||
return worldsProvider != null && worldsLevelStemClass != null && worldsGeneratorTypeClass != null;
|
||||
}
|
||||
|
||||
public boolean hasPaperLikeRuntime() {
|
||||
return minecraftServer != null
|
||||
&& createLevelMethod != null
|
||||
&& serverRegistryAccessMethod != null
|
||||
&& paperLikeFlavor != PaperLikeFlavor.UNSUPPORTED;
|
||||
}
|
||||
|
||||
public String worldsProviderResolution() {
|
||||
return worldsProviderResolution;
|
||||
}
|
||||
|
||||
public String paperLikeResolution() {
|
||||
return paperLikeResolution;
|
||||
}
|
||||
|
||||
public String describe() {
|
||||
return "family=" + serverFamily.id()
|
||||
+ ", regionizedRuntime=" + regionizedRuntime
|
||||
+ ", worldsProvider=" + worldsProviderResolution
|
||||
+ ", paperLike=" + paperLikeResolution
|
||||
+ ", serverRegistryAccess=" + (serverRegistryAccessMethod != null)
|
||||
+ ", unloadAsync=" + (unloadWorldAsyncMethod != null)
|
||||
+ ", chunkAsync=" + (chunkAtAsyncMethod != null);
|
||||
}
|
||||
|
||||
private static ServerFamily detectServerFamily(Server server, boolean regionizedRuntime) {
|
||||
String bukkitName = server == null ? "" : server.getName();
|
||||
String bukkitVersion = server == null ? "" : server.getVersion();
|
||||
String serverClassName = server == null ? "" : server.getClass().getName();
|
||||
boolean canvasRuntime = hasCanvasRuntime();
|
||||
|
||||
if (containsIgnoreCase(bukkitName, "folia")
|
||||
|| containsIgnoreCase(bukkitVersion, "folia")
|
||||
|| containsIgnoreCase(serverClassName, "folia")) {
|
||||
return ServerFamily.FOLIA;
|
||||
}
|
||||
|
||||
if (canvasRuntime
|
||||
|| containsIgnoreCase(bukkitName, "canvas")
|
||||
|| containsIgnoreCase(bukkitVersion, "canvas")
|
||||
|| containsIgnoreCase(serverClassName, "canvas")) {
|
||||
return regionizedRuntime ? ServerFamily.CANVAS : ServerFamily.CANVAS;
|
||||
}
|
||||
|
||||
if (containsIgnoreCase(bukkitName, "purpur")
|
||||
|| containsIgnoreCase(bukkitVersion, "purpur")
|
||||
|| containsIgnoreCase(serverClassName, "purpur")) {
|
||||
return ServerFamily.PURPUR;
|
||||
}
|
||||
|
||||
if (containsIgnoreCase(bukkitName, "paper")
|
||||
|| containsIgnoreCase(bukkitVersion, "paper")
|
||||
|| containsIgnoreCase(serverClassName, "paper")
|
||||
|| containsIgnoreCase(bukkitName, "pufferfish")
|
||||
|| containsIgnoreCase(bukkitVersion, "pufferfish")
|
||||
|| containsIgnoreCase(serverClassName, "pufferfish")) {
|
||||
return ServerFamily.PAPER;
|
||||
}
|
||||
|
||||
if (containsIgnoreCase(bukkitName, "spigot")
|
||||
|| containsIgnoreCase(bukkitVersion, "spigot")
|
||||
|| containsIgnoreCase(serverClassName, "spigot")) {
|
||||
return ServerFamily.SPIGOT;
|
||||
}
|
||||
|
||||
if (containsIgnoreCase(bukkitName, "craftbukkit")
|
||||
|| containsIgnoreCase(bukkitVersion, "craftbukkit")
|
||||
|| containsIgnoreCase(serverClassName, "craftbukkit")
|
||||
|| containsIgnoreCase(bukkitName, "bukkit")
|
||||
|| containsIgnoreCase(bukkitVersion, "bukkit")) {
|
||||
return ServerFamily.BUKKIT;
|
||||
}
|
||||
|
||||
if (regionizedRuntime || J.isFolia()) {
|
||||
return ServerFamily.FOLIA;
|
||||
}
|
||||
|
||||
return ServerFamily.UNKNOWN;
|
||||
}
|
||||
|
||||
private static boolean hasCanvasRuntime() {
|
||||
try {
|
||||
Class.forName("io.canvasmc.canvas.region.WorldRegionizer");
|
||||
return true;
|
||||
} catch (Throwable ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean containsIgnoreCase(String value, String needle) {
|
||||
if (value == null || needle == null || needle.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return value.toLowerCase(Locale.ROOT).contains(needle.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
private static Object[] resolveWorldsProvider() throws Throwable {
|
||||
try {
|
||||
Class<?> worldsProviderClass = Class.forName("net.thenextlvl.worlds.api.WorldsProvider");
|
||||
Class<?> levelStemClass = Class.forName("net.thenextlvl.worlds.api.generator.LevelStem");
|
||||
Class<?> generatorTypeClass = Class.forName("net.thenextlvl.worlds.api.generator.GeneratorType");
|
||||
Object provider = Bukkit.getServicesManager().load(worldsProviderClass);
|
||||
String resolution = provider == null ? "inactive(service not registered)" : "active(service=" + provider.getClass().getName() + ")";
|
||||
return new Object[]{provider, levelStemClass, generatorTypeClass, resolution};
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
Collection<Class<?>> knownServices = Bukkit.getServicesManager().getKnownServices();
|
||||
for (Class<?> serviceClass : knownServices) {
|
||||
if (!"net.thenextlvl.worlds.api.WorldsProvider".equals(serviceClass.getName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RegisteredServiceProvider<?> registration = Bukkit.getServicesManager().getRegistration(serviceClass);
|
||||
if (registration == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object provider = registration.getProvider();
|
||||
ClassLoader loader = serviceClass.getClassLoader();
|
||||
if (loader == null && provider != null) {
|
||||
loader = provider.getClass().getClassLoader();
|
||||
}
|
||||
if (loader == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Class<?> levelStemClass = Class.forName("net.thenextlvl.worlds.api.generator.LevelStem", false, loader);
|
||||
Class<?> generatorTypeClass = Class.forName("net.thenextlvl.worlds.api.generator.GeneratorType", false, loader);
|
||||
return new Object[]{provider, levelStemClass, generatorTypeClass, "active(service-scan=" + provider.getClass().getName() + ")"};
|
||||
}
|
||||
|
||||
return new Object[]{null, null, null, "inactive(service scan found nothing)"};
|
||||
}
|
||||
|
||||
private static final class TestingPaperLikeServer {
|
||||
private final TestingWorldLoaderContext worldLoaderContext;
|
||||
private final Object registryAccess;
|
||||
|
||||
private TestingPaperLikeServer(Object datapackDimensions, Object registryAccess) {
|
||||
this.worldLoaderContext = new TestingWorldLoaderContext(datapackDimensions);
|
||||
this.registryAccess = registryAccess;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private void createLevel(Object levelStem, Object worldLoadingInfoAndData, Object worldDataAndGenSettings) {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Object registryAccess() {
|
||||
return registryAccess;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TestingWorldLoaderContext {
|
||||
private final Object datapackDimensions;
|
||||
|
||||
private TestingWorldLoaderContext(Object datapackDimensions) {
|
||||
this.datapackDimensions = datapackDimensions;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Object datapackDimensions() {
|
||||
return datapackDimensions;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
final class PaperLikeRuntimeBackend implements WorldLifecycleBackend {
|
||||
private final CapabilitySnapshot capabilities;
|
||||
|
||||
PaperLikeRuntimeBackend(CapabilitySnapshot capabilities) {
|
||||
this.capabilities = capabilities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) {
|
||||
return request.studio()
|
||||
&& capabilities.serverFamily().isPaperLike()
|
||||
&& capabilities.hasPaperLikeRuntime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<World> create(WorldLifecycleRequest request) {
|
||||
Object legacyStorageAccess = null;
|
||||
try {
|
||||
World existing = Bukkit.getWorld(request.worldName());
|
||||
if (existing != null) {
|
||||
return CompletableFuture.completedFuture(existing);
|
||||
}
|
||||
|
||||
if (request.generator() == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("Runtime world creation requires a non-null chunk generator."));
|
||||
}
|
||||
|
||||
WorldLifecycleStaging.stageGenerator(request.worldName(), request.generator(), request.biomeProvider());
|
||||
WorldLifecycleSupport.stageRuntimeConfiguration(request.worldName());
|
||||
|
||||
Iris.info("WorldLifecycle runtime LevelStem: world=" + request.worldName()
|
||||
+ ", backend=paper_like_runtime, flavor=" + capabilities.paperLikeFlavor().name().toLowerCase(Locale.ROOT)
|
||||
+ ", registrySource=" + WorldLifecycleSupport.runtimeLevelStemRegistrySource(request));
|
||||
Object levelStem = WorldLifecycleSupport.resolveRuntimeLevelStem(capabilities, request);
|
||||
Object stemKey = WorldLifecycleSupport.createRuntimeLevelStemKey(request.worldName());
|
||||
|
||||
if (capabilities.paperLikeFlavor() == CapabilitySnapshot.PaperLikeFlavor.CURRENT_INFO_AND_DATA) {
|
||||
Object dimensionKey = WorldLifecycleSupport.createDimensionKey(stemKey);
|
||||
Object loadedWorldData = capabilities.paperWorldDataMethod().invoke(null, capabilities.minecraftServer(), dimensionKey, request.worldName());
|
||||
Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(request.environment(), stemKey, dimensionKey, !request.studio());
|
||||
Object worldLoadingInfoAndData = capabilities.worldLoadingInfoAndDataConstructor().newInstance(worldLoadingInfo, loadedWorldData);
|
||||
Object worldDataAndGenSettings = WorldLifecycleSupport.createCurrentWorldDataAndSettings(capabilities, request.worldName());
|
||||
capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfoAndData, worldDataAndGenSettings);
|
||||
} else {
|
||||
legacyStorageAccess = WorldLifecycleSupport.createLegacyStorageAccess(capabilities, request.worldName());
|
||||
Object primaryLevelData = WorldLifecycleSupport.createLegacyPrimaryLevelData(capabilities, legacyStorageAccess, request.worldName());
|
||||
Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(0, request.worldName(), request.environment().name().toLowerCase(Locale.ROOT), stemKey, !request.studio());
|
||||
capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfo, legacyStorageAccess, primaryLevelData);
|
||||
}
|
||||
|
||||
World loadedWorld = Bukkit.getWorld(request.worldName());
|
||||
if (loadedWorld == null) {
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("Paper-like runtime backend did not load world \"" + request.worldName() + "\"."));
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(loadedWorld);
|
||||
} catch (Throwable e) {
|
||||
return CompletableFuture.failedFuture(WorldLifecycleSupport.unwrap(e));
|
||||
} finally {
|
||||
WorldLifecycleStaging.clearGenerator(request.worldName());
|
||||
WorldLifecycleSupport.closeLevelStorageAccess(legacyStorageAccess);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unload(World world, boolean save) {
|
||||
return WorldLifecycleSupport.unloadWorld(capabilities, world, save);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String backendName() {
|
||||
return "paper_like_runtime";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String describeSelectionReason() {
|
||||
return "server family " + capabilities.serverFamily().id() + " exposes paper-like runtime world lifecycle capabilities";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum ServerFamily {
|
||||
BUKKIT,
|
||||
SPIGOT,
|
||||
PAPER,
|
||||
PURPUR,
|
||||
FOLIA,
|
||||
CANVAS,
|
||||
UNKNOWN;
|
||||
|
||||
public boolean isPaperLike() {
|
||||
return this == PAPER || this == PURPUR || this == FOLIA || this == CANVAS;
|
||||
}
|
||||
|
||||
public String id() {
|
||||
return name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import org.bukkit.World;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface WorldLifecycleBackend {
|
||||
boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities);
|
||||
|
||||
CompletableFuture<World> create(WorldLifecycleRequest request);
|
||||
|
||||
boolean unload(World world, boolean save);
|
||||
|
||||
String backendName();
|
||||
|
||||
String describeSelectionReason();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
public enum WorldLifecycleCaller {
|
||||
STUDIO,
|
||||
CREATE,
|
||||
BENCHMARK
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.WorldCreator;
|
||||
import org.bukkit.WorldType;
|
||||
import org.bukkit.generator.BiomeProvider;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
|
||||
public record WorldLifecycleRequest(
|
||||
String worldName,
|
||||
World.Environment environment,
|
||||
ChunkGenerator generator,
|
||||
BiomeProvider biomeProvider,
|
||||
WorldType worldType,
|
||||
boolean generateStructures,
|
||||
boolean hardcore,
|
||||
long seed,
|
||||
boolean studio,
|
||||
boolean benchmark,
|
||||
WorldLifecycleCaller callerKind
|
||||
) {
|
||||
public static WorldLifecycleRequest fromCreator(WorldCreator creator, boolean studio, boolean benchmark, WorldLifecycleCaller callerKind) {
|
||||
return new WorldLifecycleRequest(
|
||||
creator.name(),
|
||||
creator.environment(),
|
||||
creator.generator(),
|
||||
creator.biomeProvider(),
|
||||
creator.type(),
|
||||
creator.generateStructures(),
|
||||
creator.hardcore(),
|
||||
creator.seed(),
|
||||
studio,
|
||||
benchmark,
|
||||
callerKind
|
||||
);
|
||||
}
|
||||
|
||||
public WorldCreator toWorldCreator() {
|
||||
WorldCreator creator = new WorldCreator(worldName)
|
||||
.environment(environment)
|
||||
.generateStructures(generateStructures)
|
||||
.hardcore(hardcore)
|
||||
.type(worldType)
|
||||
.seed(seed)
|
||||
.generator(generator);
|
||||
if (biomeProvider != null) {
|
||||
creator.biomeProvider(biomeProvider);
|
||||
}
|
||||
return creator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.World;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class WorldLifecycleService {
|
||||
private static volatile WorldLifecycleService instance;
|
||||
|
||||
private final CapabilitySnapshot capabilities;
|
||||
private final WorldsProviderBackend worldsProviderBackend;
|
||||
private final PaperLikeRuntimeBackend paperLikeRuntimeBackend;
|
||||
private final BukkitPublicBackend bukkitPublicBackend;
|
||||
private final List<WorldLifecycleBackend> backends;
|
||||
private final Map<String, String> worldBackendByName;
|
||||
|
||||
public WorldLifecycleService(CapabilitySnapshot capabilities) {
|
||||
this.capabilities = capabilities;
|
||||
this.worldsProviderBackend = new WorldsProviderBackend(capabilities);
|
||||
this.paperLikeRuntimeBackend = new PaperLikeRuntimeBackend(capabilities);
|
||||
this.bukkitPublicBackend = new BukkitPublicBackend(capabilities);
|
||||
this.backends = List.of(worldsProviderBackend, paperLikeRuntimeBackend, bukkitPublicBackend);
|
||||
this.worldBackendByName = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
public static WorldLifecycleService get() {
|
||||
WorldLifecycleService current = instance;
|
||||
if (current != null) {
|
||||
return current;
|
||||
}
|
||||
|
||||
synchronized (WorldLifecycleService.class) {
|
||||
if (instance != null) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
CapabilitySnapshot capabilities = CapabilitySnapshot.probe();
|
||||
instance = new WorldLifecycleService(capabilities);
|
||||
Iris.info("WorldLifecycle capabilities: %s", capabilities.describe());
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
public CapabilitySnapshot capabilities() {
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
public CompletableFuture<World> create(WorldLifecycleRequest request) {
|
||||
WorldLifecycleBackend backend;
|
||||
try {
|
||||
backend = selectCreateBackend(request);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("WorldLifecycle create backend selection failed for world=\"" + request.worldName()
|
||||
+ "\", caller=" + request.callerKind().name().toLowerCase() + ".", e);
|
||||
return CompletableFuture.failedFuture(e);
|
||||
}
|
||||
Iris.info("WorldLifecycle create: world=%s, caller=%s, backend=%s, reason=%s",
|
||||
request.worldName(),
|
||||
request.callerKind().name().toLowerCase(),
|
||||
backend.backendName(),
|
||||
backend.describeSelectionReason());
|
||||
return backend.create(request).whenComplete((world, throwable) -> {
|
||||
if (throwable != null) {
|
||||
Throwable cause = WorldLifecycleSupport.unwrap(throwable);
|
||||
Iris.reportError("WorldLifecycle create failed: world=\"" + request.worldName()
|
||||
+ "\", caller=" + request.callerKind().name().toLowerCase()
|
||||
+ ", backend=" + backend.backendName()
|
||||
+ ", family=" + capabilities.serverFamily().id() + ".", cause);
|
||||
return;
|
||||
}
|
||||
if (world != null) {
|
||||
worldBackendByName.put(world.getName(), backend.backendName());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public World createBlocking(WorldLifecycleRequest request) {
|
||||
try {
|
||||
return create(request).join();
|
||||
} catch (CompletionException e) {
|
||||
throw new IllegalStateException(WorldLifecycleSupport.unwrap(e));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean unload(World world, boolean save) {
|
||||
if (!J.isPrimaryThread()) {
|
||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||
J.s(() -> {
|
||||
try {
|
||||
future.complete(unloadDirect(world, save));
|
||||
} catch (Throwable e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return future.join();
|
||||
}
|
||||
|
||||
return unloadDirect(world, save);
|
||||
}
|
||||
|
||||
private boolean unloadDirect(World world, boolean save) {
|
||||
WorldLifecycleBackend backend = selectUnloadBackend(world.getName());
|
||||
Iris.info("WorldLifecycle unload: world=%s, backend=%s, reason=%s",
|
||||
world.getName(),
|
||||
backend.backendName(),
|
||||
backend.describeSelectionReason());
|
||||
boolean unloaded;
|
||||
try {
|
||||
unloaded = backend.unload(world, save);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("WorldLifecycle unload failed: world=\"" + world.getName()
|
||||
+ "\", backend=" + backend.backendName()
|
||||
+ ", family=" + capabilities.serverFamily().id() + ".", e);
|
||||
if (e instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (e instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
if (unloaded) {
|
||||
worldBackendByName.remove(world.getName());
|
||||
}
|
||||
return unloaded;
|
||||
}
|
||||
|
||||
public String backendNameForWorld(String worldName) {
|
||||
return selectUnloadBackend(worldName).backendName();
|
||||
}
|
||||
|
||||
WorldLifecycleBackend selectCreateBackend(WorldLifecycleRequest request) {
|
||||
if (worldsProviderBackend.supports(request, capabilities)) {
|
||||
return worldsProviderBackend;
|
||||
}
|
||||
|
||||
if (request.studio() && capabilities.serverFamily().isPaperLike()) {
|
||||
if (!paperLikeRuntimeBackend.supports(request, capabilities)) {
|
||||
throw new IllegalStateException("World lifecycle backend paper_like_runtime is unavailable for studio create on "
|
||||
+ capabilities.serverFamily().id() + ": " + capabilities.paperLikeResolution());
|
||||
}
|
||||
return paperLikeRuntimeBackend;
|
||||
}
|
||||
|
||||
for (WorldLifecycleBackend backend : backends) {
|
||||
if (backend.supports(request, capabilities)) {
|
||||
return backend;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException("No world lifecycle backend supports request for \"" + request.worldName() + "\".");
|
||||
}
|
||||
|
||||
WorldLifecycleBackend selectUnloadBackend(String worldName) {
|
||||
String backendName = worldBackendByName.get(worldName);
|
||||
if (backendName == null) {
|
||||
return bukkitPublicBackend;
|
||||
}
|
||||
|
||||
for (WorldLifecycleBackend backend : backends) {
|
||||
if (backend.backendName().equals(backendName)) {
|
||||
return backend;
|
||||
}
|
||||
}
|
||||
|
||||
return bukkitPublicBackend;
|
||||
}
|
||||
|
||||
void rememberBackend(String worldName, String backendName) {
|
||||
worldBackendByName.put(worldName, backendName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import org.bukkit.generator.BiomeProvider;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class WorldLifecycleStaging {
|
||||
private static final Map<String, ChunkGenerator> stagedGenerators = new ConcurrentHashMap<>();
|
||||
private static final Map<String, BiomeProvider> stagedBiomeProviders = new ConcurrentHashMap<>();
|
||||
private static final Map<String, ChunkGenerator> stagedStemGenerators = new ConcurrentHashMap<>();
|
||||
|
||||
private WorldLifecycleStaging() {
|
||||
}
|
||||
|
||||
public static void stageGenerator(@NotNull String worldName, @NotNull ChunkGenerator generator, @Nullable BiomeProvider biomeProvider) {
|
||||
stagedGenerators.put(worldName, generator);
|
||||
if (biomeProvider != null) {
|
||||
stagedBiomeProviders.put(worldName, biomeProvider);
|
||||
} else {
|
||||
stagedBiomeProviders.remove(worldName);
|
||||
}
|
||||
}
|
||||
|
||||
public static void stageStemGenerator(@NotNull String worldName, @NotNull ChunkGenerator generator) {
|
||||
stagedStemGenerators.put(worldName, generator);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static ChunkGenerator consumeGenerator(@NotNull String worldName) {
|
||||
return stagedGenerators.remove(worldName);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static BiomeProvider consumeBiomeProvider(@NotNull String worldName) {
|
||||
return stagedBiomeProviders.remove(worldName);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static ChunkGenerator consumeStemGenerator(@NotNull String worldName) {
|
||||
return stagedStemGenerators.remove(worldName);
|
||||
}
|
||||
|
||||
public static void clearGenerator(@NotNull String worldName) {
|
||||
stagedGenerators.remove(worldName);
|
||||
stagedBiomeProviders.remove(worldName);
|
||||
}
|
||||
|
||||
public static void clearStem(@NotNull String worldName) {
|
||||
stagedStemGenerators.remove(worldName);
|
||||
}
|
||||
|
||||
public static void clearAll(@NotNull String worldName) {
|
||||
clearGenerator(worldName);
|
||||
clearStem(worldName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.INMSBinding;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
final class WorldLifecycleSupport {
|
||||
private WorldLifecycleSupport() {
|
||||
}
|
||||
|
||||
static Throwable unwrap(Throwable throwable) {
|
||||
if (throwable instanceof InvocationTargetException invocationTargetException && invocationTargetException.getCause() != null) {
|
||||
return unwrap(invocationTargetException.getCause());
|
||||
}
|
||||
if (throwable instanceof java.util.concurrent.CompletionException completionException && completionException.getCause() != null) {
|
||||
return unwrap(completionException.getCause());
|
||||
}
|
||||
if (throwable instanceof ExecutionException executionException && executionException.getCause() != null) {
|
||||
return unwrap(executionException.getCause());
|
||||
}
|
||||
return throwable;
|
||||
}
|
||||
|
||||
static Object invoke(Method method, Object target, Object... args) throws ReflectiveOperationException {
|
||||
return method.invoke(target, args);
|
||||
}
|
||||
|
||||
static Object invokeNamed(Object target, String methodName, Class<?>[] parameterTypes, Object... args) throws ReflectiveOperationException {
|
||||
Method method = target.getClass().getMethod(methodName, parameterTypes);
|
||||
return method.invoke(target, args);
|
||||
}
|
||||
|
||||
static Object read(Field field, Object target) throws IllegalAccessException {
|
||||
return field.get(target);
|
||||
}
|
||||
|
||||
static void stageRuntimeConfiguration(String worldName) throws ReflectiveOperationException {
|
||||
Object bukkitServer = Bukkit.getServer();
|
||||
if (bukkitServer == null) {
|
||||
throw new IllegalStateException("Bukkit server is unavailable.");
|
||||
}
|
||||
|
||||
Field configurationField = CapabilityResolution.resolveField(bukkitServer.getClass(), "configuration");
|
||||
Object rawConfiguration = configurationField.get(bukkitServer);
|
||||
if (!(rawConfiguration instanceof YamlConfiguration configuration)) {
|
||||
throw new IllegalStateException("CraftServer configuration field is unavailable.");
|
||||
}
|
||||
|
||||
ConfigurationSection worldsSection = configuration.getConfigurationSection("worlds");
|
||||
if (worldsSection == null) {
|
||||
worldsSection = configuration.createSection("worlds");
|
||||
}
|
||||
|
||||
ConfigurationSection worldSection = worldsSection.getConfigurationSection(worldName);
|
||||
if (worldSection == null) {
|
||||
worldSection = worldsSection.createSection(worldName);
|
||||
}
|
||||
|
||||
worldSection.set("generator", "Iris:runtime");
|
||||
}
|
||||
|
||||
static Object getRuntimeDatapackDimensions(CapabilitySnapshot capabilities) throws ReflectiveOperationException {
|
||||
Object worldLoaderContext = read(capabilities.worldLoaderContextField(), capabilities.minecraftServer());
|
||||
Method datapackDimensionsMethod = CapabilityResolution.resolveMethod(worldLoaderContext.getClass(), "datapackDimensions", method -> method.getParameterCount() == 0);
|
||||
if (datapackDimensionsMethod == null) {
|
||||
throw new IllegalStateException("DataLoadContext does not expose datapackDimensions().");
|
||||
}
|
||||
Object datapackDimensions = datapackDimensionsMethod.invoke(worldLoaderContext);
|
||||
if (datapackDimensions == null) {
|
||||
throw new IllegalStateException("DataLoadContext.datapackDimensions() returned null.");
|
||||
}
|
||||
return datapackDimensions;
|
||||
}
|
||||
|
||||
static Object getRuntimeServerRegistryAccess(CapabilitySnapshot capabilities) throws ReflectiveOperationException {
|
||||
Method registryAccessMethod = capabilities.serverRegistryAccessMethod();
|
||||
if (registryAccessMethod == null) {
|
||||
throw new IllegalStateException("MinecraftServer does not expose registryAccess().");
|
||||
}
|
||||
Object registryAccess = registryAccessMethod.invoke(capabilities.minecraftServer());
|
||||
if (registryAccess == null) {
|
||||
throw new IllegalStateException("MinecraftServer.registryAccess() returned null.");
|
||||
}
|
||||
return registryAccess;
|
||||
}
|
||||
|
||||
static Object getRuntimeLevelStemRegistry(CapabilitySnapshot capabilities) throws ReflectiveOperationException {
|
||||
Object datapackDimensions = getRuntimeDatapackDimensions(capabilities);
|
||||
Object levelStemRegistryKey = Class.forName("net.minecraft.core.registries.Registries")
|
||||
.getField("LEVEL_STEM")
|
||||
.get(null);
|
||||
Method lookupMethod = CapabilityResolution.resolveMethod(datapackDimensions.getClass(), "lookupOrThrow", method -> method.getParameterCount() == 1);
|
||||
if (lookupMethod == null) {
|
||||
throw new IllegalStateException("Registry access does not expose lookupOrThrow(...).");
|
||||
}
|
||||
return lookupMethod.invoke(datapackDimensions, levelStemRegistryKey);
|
||||
}
|
||||
|
||||
static Object createRuntimeLevelStemKey(String worldName) throws ReflectiveOperationException {
|
||||
String sanitized = worldName.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/_-]", "_");
|
||||
String path = "runtime/" + sanitized;
|
||||
Identifier identifier = new Identifier("iris", path);
|
||||
Object rawIdentifier = Class.forName("net.minecraft.resources.Identifier")
|
||||
.getMethod("fromNamespaceAndPath", String.class, String.class)
|
||||
.invoke(null, identifier.namespace(), identifier.key());
|
||||
Object registryKey = Class.forName("net.minecraft.core.registries.Registries")
|
||||
.getField("LEVEL_STEM")
|
||||
.get(null);
|
||||
Method createMethod = Class.forName("net.minecraft.resources.ResourceKey")
|
||||
.getMethod("create", registryKey.getClass(), rawIdentifier.getClass());
|
||||
return createMethod.invoke(null, registryKey, rawIdentifier);
|
||||
}
|
||||
|
||||
static Object createDimensionKey(Object stemKey) throws ReflectiveOperationException {
|
||||
Class<?> resourceKeyClass = Class.forName("net.minecraft.resources.ResourceKey");
|
||||
Method identifierMethod = CapabilityResolution.resolveMethod(resourceKeyClass, "identifier", method -> method.getParameterCount() == 0);
|
||||
Object identifier = identifierMethod.invoke(stemKey);
|
||||
Object dimensionRegistryKey = Class.forName("net.minecraft.core.registries.Registries")
|
||||
.getField("DIMENSION")
|
||||
.get(null);
|
||||
Method createMethod = resourceKeyClass.getMethod("create", dimensionRegistryKey.getClass(), identifier.getClass());
|
||||
return createMethod.invoke(null, dimensionRegistryKey, identifier);
|
||||
}
|
||||
|
||||
static Object resolveRuntimeLevelStem(CapabilitySnapshot capabilities, WorldLifecycleRequest request) throws ReflectiveOperationException {
|
||||
return resolveRuntimeLevelStem(capabilities, request, INMS.get());
|
||||
}
|
||||
|
||||
static Object resolveRuntimeLevelStem(CapabilitySnapshot capabilities, WorldLifecycleRequest request, INMSBinding binding) throws ReflectiveOperationException {
|
||||
ChunkGenerator generator = request.generator();
|
||||
if (generator instanceof PlatformChunkGenerator) {
|
||||
Object registryAccess = getRuntimeServerRegistryAccess(capabilities);
|
||||
try {
|
||||
Object levelStem = binding.createRuntimeLevelStem(registryAccess, generator);
|
||||
if (levelStem == null) {
|
||||
throw new IllegalStateException("Iris NMS binding returned null runtime LevelStem.");
|
||||
}
|
||||
return levelStem;
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException("Failed to create runtime LevelStem from full server registry access for world \"" + request.worldName() + "\".", unwrap(e));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Object levelStemRegistry = getRuntimeLevelStemRegistry(capabilities);
|
||||
Object overworldKey = Class.forName("net.minecraft.world.level.dimension.LevelStem")
|
||||
.getField("OVERWORLD")
|
||||
.get(null);
|
||||
Method getValueMethod = CapabilityResolution.resolveMethod(levelStemRegistry.getClass(), "getValue", method -> method.getParameterCount() == 1);
|
||||
if (getValueMethod != null) {
|
||||
Object resolved = getValueMethod.invoke(levelStemRegistry, overworldKey);
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
Method getMethod = CapabilityResolution.resolveMethod(levelStemRegistry.getClass(), "get", method -> method.getParameterCount() == 1);
|
||||
if (getMethod == null) {
|
||||
throw new IllegalStateException("Unable to resolve OVERWORLD LevelStem from registry.");
|
||||
}
|
||||
Object raw = getMethod.invoke(levelStemRegistry, overworldKey);
|
||||
return extractRegistryValue(raw);
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException("Failed to resolve fallback OVERWORLD LevelStem from datapack registry access for world \"" + request.worldName() + "\".", unwrap(e));
|
||||
}
|
||||
}
|
||||
|
||||
static String runtimeLevelStemRegistrySource(WorldLifecycleRequest request) {
|
||||
if (request.generator() instanceof PlatformChunkGenerator) {
|
||||
return "full_server_registry";
|
||||
}
|
||||
return "datapack_level_stem_registry";
|
||||
}
|
||||
|
||||
static Object extractRegistryValue(Object raw) throws ReflectiveOperationException {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
if (raw instanceof Optional<?> optional) {
|
||||
Object nested = optional.orElse(null);
|
||||
if (nested == null) {
|
||||
return null;
|
||||
}
|
||||
return extractRegistryValue(nested);
|
||||
}
|
||||
Method valueMethod = CapabilityResolution.resolveMethod(raw.getClass(), "value", method -> method.getParameterCount() == 0);
|
||||
if (valueMethod != null) {
|
||||
return valueMethod.invoke(raw);
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
static void applyWorldDataNameAndModInfo(CapabilitySnapshot capabilities, Object worldDataAndGenSettings, String worldName) throws ReflectiveOperationException {
|
||||
Method dataMethod = CapabilityResolution.resolveMethod(worldDataAndGenSettings.getClass(), "data", method -> method.getParameterCount() == 0);
|
||||
if (dataMethod == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object worldData = dataMethod.invoke(worldDataAndGenSettings);
|
||||
if (worldData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Method checkNameMethod = CapabilityResolution.resolveMethod(worldData.getClass(), "checkName", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 1 && String.class.equals(params[0]);
|
||||
});
|
||||
if (checkNameMethod != null) {
|
||||
checkNameMethod.invoke(worldData, worldName);
|
||||
}
|
||||
|
||||
Method getModdedStatusMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getModdedStatus", method -> method.getParameterCount() == 0);
|
||||
Method getServerModNameMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getServerModName", method -> method.getParameterCount() == 0);
|
||||
if (getModdedStatusMethod == null || getServerModNameMethod == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object modCheck = getModdedStatusMethod.invoke(capabilities.minecraftServer());
|
||||
Method shouldReportAsModifiedMethod = CapabilityResolution.resolveMethod(modCheck.getClass(), "shouldReportAsModified", method -> method.getParameterCount() == 0);
|
||||
Method setModdedInfoMethod = CapabilityResolution.resolveMethod(worldData.getClass(), "setModdedInfo", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 2 && String.class.equals(params[0]) && boolean.class.equals(params[1]);
|
||||
});
|
||||
if (shouldReportAsModifiedMethod == null || setModdedInfoMethod == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean modified = Boolean.TRUE.equals(shouldReportAsModifiedMethod.invoke(modCheck));
|
||||
String modName = (String) getServerModNameMethod.invoke(capabilities.minecraftServer());
|
||||
setModdedInfoMethod.invoke(worldData, modName, modified);
|
||||
}
|
||||
|
||||
static Object createCurrentWorldDataAndSettings(CapabilitySnapshot capabilities, String worldName) throws ReflectiveOperationException {
|
||||
Object settings = read(capabilities.settingsField(), capabilities.minecraftServer());
|
||||
Object worldLoaderContext = read(capabilities.worldLoaderContextField(), capabilities.minecraftServer());
|
||||
Object levelStemRegistry = getRuntimeLevelStemRegistry(capabilities);
|
||||
boolean demo = Boolean.TRUE.equals(capabilities.isDemoMethod().invoke(capabilities.minecraftServer()));
|
||||
Object options = read(capabilities.optionsField(), capabilities.minecraftServer());
|
||||
Method hasMethod = CapabilityResolution.resolveMethod(options.getClass(), "has", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 1 && String.class.equals(params[0]);
|
||||
});
|
||||
boolean bonusChest = hasMethod != null && Boolean.TRUE.equals(hasMethod.invoke(options, "bonusChest"));
|
||||
Object dataLoadOutput = capabilities.createNewWorldDataMethod().invoke(null, settings, worldLoaderContext, levelStemRegistry, demo, bonusChest);
|
||||
Method cookieMethod = CapabilityResolution.resolveMethod(dataLoadOutput.getClass(), "cookie", method -> method.getParameterCount() == 0);
|
||||
if (cookieMethod == null) {
|
||||
throw new IllegalStateException("WorldLoader.DataLoadOutput does not expose cookie().");
|
||||
}
|
||||
Object worldDataAndGenSettings = cookieMethod.invoke(dataLoadOutput);
|
||||
applyWorldDataNameAndModInfo(capabilities, worldDataAndGenSettings, worldName);
|
||||
return worldDataAndGenSettings;
|
||||
}
|
||||
|
||||
static Object createLegacyPrimaryLevelData(CapabilitySnapshot capabilities, Object levelStorageAccess, String worldName) throws ReflectiveOperationException {
|
||||
Object levelDataResult = capabilities.paperWorldDataMethod().invoke(null, levelStorageAccess);
|
||||
Method fatalErrorMethod = CapabilityResolution.resolveMethod(levelDataResult.getClass(), "fatalError", method -> method.getParameterCount() == 0);
|
||||
Method dataTagMethod = CapabilityResolution.resolveMethod(levelDataResult.getClass(), "dataTag", method -> method.getParameterCount() == 0);
|
||||
if (fatalErrorMethod != null && Boolean.TRUE.equals(fatalErrorMethod.invoke(levelDataResult))) {
|
||||
throw new IllegalStateException("Paper runtime world-data helper reported a fatal error for \"" + worldName + "\".");
|
||||
}
|
||||
if (dataTagMethod != null && dataTagMethod.invoke(levelDataResult) != null) {
|
||||
throw new IllegalStateException("Runtime world \"" + worldName + "\" already contains level data.");
|
||||
}
|
||||
|
||||
Object settings = read(capabilities.settingsField(), capabilities.minecraftServer());
|
||||
Object worldLoaderContext = read(capabilities.worldLoaderContextField(), capabilities.minecraftServer());
|
||||
Object levelStemRegistry = getRuntimeLevelStemRegistry(capabilities);
|
||||
boolean demo = Boolean.TRUE.equals(capabilities.isDemoMethod().invoke(capabilities.minecraftServer()));
|
||||
Object options = read(capabilities.optionsField(), capabilities.minecraftServer());
|
||||
Method hasMethod = CapabilityResolution.resolveMethod(options.getClass(), "has", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 1 && String.class.equals(params[0]);
|
||||
});
|
||||
boolean bonusChest = hasMethod != null && Boolean.TRUE.equals(hasMethod.invoke(options, "bonusChest"));
|
||||
Object dataLoadOutput = capabilities.createNewWorldDataMethod().invoke(null, settings, worldLoaderContext, levelStemRegistry, demo, bonusChest);
|
||||
Method cookieMethod = CapabilityResolution.resolveMethod(dataLoadOutput.getClass(), "cookie", method -> method.getParameterCount() == 0);
|
||||
if (cookieMethod == null) {
|
||||
throw new IllegalStateException("WorldLoader.DataLoadOutput does not expose cookie().");
|
||||
}
|
||||
Object primaryLevelData = cookieMethod.invoke(dataLoadOutput);
|
||||
|
||||
Method checkNameMethod = CapabilityResolution.resolveMethod(primaryLevelData.getClass(), "checkName", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 1 && String.class.equals(params[0]);
|
||||
});
|
||||
if (checkNameMethod != null) {
|
||||
checkNameMethod.invoke(primaryLevelData, worldName);
|
||||
}
|
||||
|
||||
Method getModdedStatusMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getModdedStatus", method -> method.getParameterCount() == 0);
|
||||
Method getServerModNameMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getServerModName", method -> method.getParameterCount() == 0);
|
||||
if (getModdedStatusMethod != null && getServerModNameMethod != null) {
|
||||
Object modCheck = getModdedStatusMethod.invoke(capabilities.minecraftServer());
|
||||
Method shouldReportAsModifiedMethod = CapabilityResolution.resolveMethod(modCheck.getClass(), "shouldReportAsModified", method -> method.getParameterCount() == 0);
|
||||
Method setModdedInfoMethod = CapabilityResolution.resolveMethod(primaryLevelData.getClass(), "setModdedInfo", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 2 && String.class.equals(params[0]) && boolean.class.equals(params[1]);
|
||||
});
|
||||
if (shouldReportAsModifiedMethod != null && setModdedInfoMethod != null) {
|
||||
boolean modified = Boolean.TRUE.equals(shouldReportAsModifiedMethod.invoke(modCheck));
|
||||
String modName = (String) getServerModNameMethod.invoke(capabilities.minecraftServer());
|
||||
setModdedInfoMethod.invoke(primaryLevelData, modName, modified);
|
||||
}
|
||||
}
|
||||
|
||||
return primaryLevelData;
|
||||
}
|
||||
|
||||
static Object createLegacyStorageAccess(CapabilitySnapshot capabilities, String worldName) throws ReflectiveOperationException {
|
||||
Class<?> levelStorageSourceClass = Class.forName("net.minecraft.world.level.storage.LevelStorageSource");
|
||||
Method createDefaultMethod = levelStorageSourceClass.getMethod("createDefault", Path.class);
|
||||
Object levelStorageSource = createDefaultMethod.invoke(null, Bukkit.getWorldContainer().toPath());
|
||||
Method storageAccessMethod = capabilities.levelStorageAccessMethod();
|
||||
if (storageAccessMethod.getParameterCount() == 1) {
|
||||
return storageAccessMethod.invoke(levelStorageSource, worldName);
|
||||
}
|
||||
Object overworldStemKey = Class.forName("net.minecraft.world.level.dimension.LevelStem")
|
||||
.getField("OVERWORLD")
|
||||
.get(null);
|
||||
return storageAccessMethod.invoke(levelStorageSource, worldName, overworldStemKey);
|
||||
}
|
||||
|
||||
static void closeLevelStorageAccess(Object levelStorageAccess) {
|
||||
if (levelStorageAccess == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Method closeMethod = levelStorageAccess.getClass().getMethod("close");
|
||||
closeMethod.invoke(levelStorageAccess);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
static boolean unloadWorld(CapabilitySnapshot capabilities, World world, boolean save) {
|
||||
if (world == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CompletableFuture<Boolean> asyncUnload = unloadWorldViaAsyncApi(capabilities, world, save);
|
||||
if (asyncUnload != null) {
|
||||
return resolveAsyncUnload(asyncUnload);
|
||||
}
|
||||
|
||||
try {
|
||||
return Bukkit.unloadWorld(world, save);
|
||||
} catch (UnsupportedOperationException unsupported) {
|
||||
if (capabilities.minecraftServer() == null || capabilities.removeLevelMethod() == null) {
|
||||
throw unsupported;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (save) {
|
||||
world.save();
|
||||
}
|
||||
|
||||
Method getHandleMethod = world.getClass().getMethod("getHandle");
|
||||
Object serverLevel = getHandleMethod.invoke(world);
|
||||
closeServerLevel(world, serverLevel);
|
||||
detachServerLevel(capabilities, serverLevel, world.getName());
|
||||
return Bukkit.getWorld(world.getName()) == null;
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException("Failed to unload world \"" + world.getName() + "\" through the selected world lifecycle backend.", unwrap(e));
|
||||
}
|
||||
}
|
||||
|
||||
private static CompletableFuture<Boolean> unloadWorldViaAsyncApi(CapabilitySnapshot capabilities, World world, boolean save) {
|
||||
if (capabilities.unloadWorldAsyncMethod() == null || capabilities.bukkitServer() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CompletableFuture<Boolean> callbackFuture = new CompletableFuture<>();
|
||||
Runnable invokeTask = () -> {
|
||||
Consumer<Boolean> callback = result -> callbackFuture.complete(Boolean.TRUE.equals(result));
|
||||
try {
|
||||
capabilities.unloadWorldAsyncMethod().invoke(capabilities.bukkitServer(), world, save, callback);
|
||||
} catch (Throwable e) {
|
||||
callbackFuture.completeExceptionally(unwrap(e));
|
||||
}
|
||||
};
|
||||
|
||||
if (J.isFolia() && !isGlobalTickThread()) {
|
||||
CompletableFuture<Void> scheduled = J.sfut(invokeTask);
|
||||
if (scheduled == null) {
|
||||
callbackFuture.completeExceptionally(new IllegalStateException("Failed to schedule global unload task."));
|
||||
return callbackFuture;
|
||||
}
|
||||
scheduled.whenComplete((unused, throwable) -> {
|
||||
if (throwable != null) {
|
||||
callbackFuture.completeExceptionally(unwrap(throwable));
|
||||
}
|
||||
});
|
||||
return callbackFuture;
|
||||
}
|
||||
|
||||
invokeTask.run();
|
||||
return callbackFuture;
|
||||
}
|
||||
|
||||
private static boolean resolveAsyncUnload(CompletableFuture<Boolean> asyncUnload) {
|
||||
if (J.isPrimaryThread()) {
|
||||
if (!asyncUnload.isDone()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
return Boolean.TRUE.equals(asyncUnload.join());
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException("Failed to consume async world unload result.", unwrap(e));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return Boolean.TRUE.equals(asyncUnload.get(120, TimeUnit.SECONDS));
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException("Failed while waiting for async world unload result.", unwrap(e));
|
||||
}
|
||||
}
|
||||
|
||||
private static void closeServerLevel(World world, Object serverLevel) throws Throwable {
|
||||
Method closeMethod = CapabilityResolution.resolveMethod(serverLevel.getClass(), "close", method -> method.getParameterCount() == 0);
|
||||
if (closeMethod == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!J.isFolia()) {
|
||||
closeMethod.invoke(serverLevel);
|
||||
return;
|
||||
}
|
||||
|
||||
Location spawn = world.getSpawnLocation();
|
||||
int chunkX = spawn == null ? 0 : spawn.getBlockX() >> 4;
|
||||
int chunkZ = spawn == null ? 0 : spawn.getBlockZ() >> 4;
|
||||
CompletableFuture<Void> closeFuture = new CompletableFuture<>();
|
||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
||||
try {
|
||||
closeMethod.invoke(serverLevel);
|
||||
closeFuture.complete(null);
|
||||
} catch (Throwable e) {
|
||||
closeFuture.completeExceptionally(unwrap(e));
|
||||
}
|
||||
});
|
||||
if (!scheduled) {
|
||||
throw new IllegalStateException("Failed to schedule region close task for world \"" + world.getName() + "\".");
|
||||
}
|
||||
closeFuture.get(90, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private static void removeWorldFromCraftServerMap(String worldName) throws ReflectiveOperationException {
|
||||
Object bukkitServer = Bukkit.getServer();
|
||||
if (bukkitServer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Field worldsField = CapabilityResolution.resolveField(bukkitServer.getClass(), "worlds");
|
||||
Object rawWorlds = worldsField.get(bukkitServer);
|
||||
if (rawWorlds instanceof Map map) {
|
||||
map.remove(worldName);
|
||||
map.remove(worldName.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
|
||||
private static void detachServerLevel(CapabilitySnapshot capabilities, Object serverLevel, String worldName) throws Throwable {
|
||||
Runnable detachTask = () -> {
|
||||
try {
|
||||
capabilities.removeLevelMethod().invoke(capabilities.minecraftServer(), serverLevel);
|
||||
removeWorldFromCraftServerMap(worldName);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
};
|
||||
|
||||
if (!J.isFolia() || isGlobalTickThread()) {
|
||||
detachTask.run();
|
||||
return;
|
||||
}
|
||||
|
||||
CompletableFuture<Void> detachFuture = J.sfut(detachTask);
|
||||
if (detachFuture == null) {
|
||||
throw new IllegalStateException("Failed to schedule global detach task for world \"" + worldName + "\".");
|
||||
}
|
||||
detachFuture.get(15, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
static boolean isGlobalTickThread() {
|
||||
Object server = Bukkit.getServer();
|
||||
if (server == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Method method = server.getClass().getMethod("isGlobalTickThread");
|
||||
return Boolean.TRUE.equals(method.invoke(server));
|
||||
} catch (Throwable ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.WorldType;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
final class WorldsProviderBackend implements WorldLifecycleBackend {
|
||||
private final CapabilitySnapshot capabilities;
|
||||
|
||||
WorldsProviderBackend(CapabilitySnapshot capabilities) {
|
||||
this.capabilities = capabilities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) {
|
||||
return request.studio() && capabilities.hasWorldsProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public CompletableFuture<World> create(WorldLifecycleRequest request) {
|
||||
try {
|
||||
Path worldPath = new File(Bukkit.getWorldContainer(), request.worldName()).toPath();
|
||||
Object builder = WorldLifecycleSupport.invokeNamed(capabilities.worldsProvider(), "levelBuilder", new Class[]{Path.class}, worldPath);
|
||||
builder = WorldLifecycleSupport.invokeNamed(builder, "name", new Class[]{String.class}, request.worldName());
|
||||
builder = WorldLifecycleSupport.invokeNamed(builder, "seed", new Class[]{long.class}, request.seed());
|
||||
builder = WorldLifecycleSupport.invokeNamed(builder, "levelStem", new Class[]{capabilities.worldsLevelStemClass()}, resolveLevelStem(request.environment()));
|
||||
builder = WorldLifecycleSupport.invokeNamed(builder, "chunkGenerator", new Class[]{org.bukkit.generator.ChunkGenerator.class}, request.generator());
|
||||
builder = WorldLifecycleSupport.invokeNamed(builder, "biomeProvider", new Class[]{org.bukkit.generator.BiomeProvider.class}, request.biomeProvider());
|
||||
builder = WorldLifecycleSupport.invokeNamed(builder, "generatorType", new Class[]{capabilities.worldsGeneratorTypeClass()}, resolveGeneratorType(request.worldType()));
|
||||
builder = WorldLifecycleSupport.invokeNamed(builder, "structures", new Class[]{boolean.class}, request.generateStructures());
|
||||
builder = WorldLifecycleSupport.invokeNamed(builder, "hardcore", new Class[]{boolean.class}, request.hardcore());
|
||||
Object levelBuilder = WorldLifecycleSupport.invokeNamed(builder, "build", new Class[0]);
|
||||
Object async = WorldLifecycleSupport.invokeNamed(levelBuilder, "createAsync", new Class[0]);
|
||||
if (async instanceof CompletableFuture<?> future) {
|
||||
return future.thenApply(world -> (World) world);
|
||||
}
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("Worlds provider createAsync did not return CompletableFuture."));
|
||||
} catch (Throwable e) {
|
||||
return CompletableFuture.failedFuture(WorldLifecycleSupport.unwrap(e));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unload(World world, boolean save) {
|
||||
return WorldLifecycleSupport.unloadWorld(capabilities, world, save);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String backendName() {
|
||||
return "worlds_provider";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String describeSelectionReason() {
|
||||
return "external Worlds provider is registered and healthy";
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private Object resolveLevelStem(World.Environment environment) {
|
||||
String key;
|
||||
if (environment == World.Environment.NETHER) {
|
||||
key = "NETHER";
|
||||
} else if (environment == World.Environment.THE_END) {
|
||||
key = "END";
|
||||
} else {
|
||||
key = "OVERWORLD";
|
||||
}
|
||||
Class<? extends Enum> enumClass = capabilities.worldsLevelStemClass().asSubclass(Enum.class);
|
||||
return Enum.valueOf(enumClass, key);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private Object resolveGeneratorType(WorldType worldType) {
|
||||
String typeName = worldType == null ? "NORMAL" : worldType.getName();
|
||||
String key;
|
||||
if ("FLAT".equalsIgnoreCase(typeName)) {
|
||||
key = "FLAT";
|
||||
} else if ("AMPLIFIED".equalsIgnoreCase(typeName)) {
|
||||
key = "AMPLIFIED";
|
||||
} else if ("LARGE_BIOMES".equalsIgnoreCase(typeName) || "LARGEBIOMES".equalsIgnoreCase(typeName)) {
|
||||
key = "LARGE_BIOMES";
|
||||
} else {
|
||||
key = "NORMAL";
|
||||
}
|
||||
Class<? extends Enum> enumClass = capabilities.worldsGeneratorTypeClass().asSubclass(Enum.class);
|
||||
return Enum.valueOf(enumClass, key.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//package art.arcane.iris.core.link;
|
||||
//
|
||||
//import com.jojodmo.customitems.api.CustomItemsAPI;
|
||||
//import com.jojodmo.customitems.item.custom.CustomItem;
|
||||
//import com.jojodmo.customitems.item.custom.block.CustomMushroomBlock;
|
||||
//import com.jojodmo.customitems.version.SafeMaterial;
|
||||
//import art.arcane.volmlib.util.collection.KList;
|
||||
//import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
//import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
|
||||
//import org.bukkit.block.BlockFace;
|
||||
//import org.bukkit.block.data.BlockData;
|
||||
//import org.bukkit.block.data.MultipleFacing;
|
||||
//import org.bukkit.inventory.ItemStack;
|
||||
//
|
||||
//import java.util.Map;
|
||||
//import java.util.MissingResourceException;
|
||||
//
|
||||
//public class CustomItemsDataProvider extends ExternalDataProvider {
|
||||
//
|
||||
// private static final String FIELD_FACES = "faces";
|
||||
// private static final String METHOD_GET_MATERIAL = "getMaterial";
|
||||
//
|
||||
// private WrappedField<CustomMushroomBlock, Map<Integer, boolean[]>> mushroomFaces;
|
||||
// private WrappedReturningMethod<CustomMushroomBlock, SafeMaterial> mushroomMaterial;
|
||||
//
|
||||
// public CustomItemsDataProvider() {
|
||||
// super("CustomItems");
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void init() {
|
||||
// this.mushroomFaces = new WrappedField<>(CustomMushroomBlock.class, FIELD_FACES);
|
||||
// this.mushroomMaterial = new WrappedReturningMethod<>(CustomMushroomBlock.class, METHOD_GET_MATERIAL);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public BlockData getBlockData(Identifier blockId) throws MissingResourceException {
|
||||
// CustomItem item = CustomItem.get(blockId.key());
|
||||
// if(item == null) {
|
||||
// throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
// } else if(item.getBlockTexture().isSpawner()) {
|
||||
// throw new MissingResourceException("Iris does not yet support SpawnerBlocks from CustomItems.", blockId.namespace(), blockId.key());
|
||||
// } else if(item.getBlockTexture() != null && item.getBlockTexture().isValid()) {
|
||||
// throw new MissingResourceException("Tried to fetch BlockData for a CustomItem that is not placeable!", blockId.namespace(), blockId.key());
|
||||
// }
|
||||
// return getMushroomData(item);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public ItemStack getItemStack(Identifier itemId) throws MissingResourceException {
|
||||
// ItemStack stack = CustomItemsAPI.getCustomItem(itemId.key());
|
||||
// if(stack == null) {
|
||||
// throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
// }
|
||||
// return stack;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public Identifier[] getBlockTypes() {
|
||||
// KList<Identifier> names = new KList<>();
|
||||
// for (String name : CustomItemsAPI.listBlockCustomItemIDs()) {
|
||||
// try {
|
||||
// Identifier key = new Identifier("cui", name);
|
||||
// if (getItemStack(key) != null)
|
||||
// names.add(key);
|
||||
// } catch (MissingResourceException ignored) { }
|
||||
// }
|
||||
//
|
||||
// return names.toArray(new Identifier[0]);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public Identifier[] getItemTypes() {
|
||||
// KList<Identifier> names = new KList<>();
|
||||
// for (String name : CustomItemsAPI.listCustomItemIDs()) {
|
||||
// try {
|
||||
// Identifier key = new Identifier("cui", name);
|
||||
// if (getItemStack(key) != null)
|
||||
// names.add(key);
|
||||
// } catch (MissingResourceException ignored) { }
|
||||
// }
|
||||
//
|
||||
// return names.toArray(new Identifier[0]);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public boolean isValidProvider(Identifier key, boolean isItem) {
|
||||
// return key.namespace().equalsIgnoreCase("cui");
|
||||
// }
|
||||
//
|
||||
// private BlockData getMushroomData(CustomItem item) {
|
||||
// MultipleFacing data = (MultipleFacing)mushroomMaterial.invoke(item.getBlockTexture().getMushroomId()).parseMaterial().createBlockData();
|
||||
// boolean[] values = mushroomFaces.get().get(item.getBlockTexture().getMushroomId());
|
||||
// data.setFace(BlockFace.DOWN, values[0]);
|
||||
// data.setFace(BlockFace.EAST, values[1]);
|
||||
// data.setFace(BlockFace.NORTH, values[2]);
|
||||
// data.setFace(BlockFace.SOUTH, values[3]);
|
||||
// data.setFace(BlockFace.UP, values[4]);
|
||||
// data.setFace(BlockFace.WEST, values[5]);
|
||||
// return data;
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,170 @@
|
||||
package art.arcane.iris.core.link;
|
||||
|
||||
import art.arcane.iris.core.link.data.DataType;
|
||||
import art.arcane.iris.core.nms.container.BiomeColor;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public abstract class ExternalDataProvider implements Listener {
|
||||
|
||||
@NonNull
|
||||
private final String pluginId;
|
||||
|
||||
@Nullable
|
||||
public Plugin getPlugin() {
|
||||
return Bukkit.getPluginManager().getPlugin(pluginId);
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return getPlugin() != null && getPlugin().isEnabled();
|
||||
}
|
||||
|
||||
public abstract void init();
|
||||
|
||||
/**
|
||||
* @see ExternalDataProvider#getBlockData(Identifier, KMap)
|
||||
*/
|
||||
@NotNull
|
||||
public BlockData getBlockData(@NotNull Identifier blockId) throws MissingResourceException {
|
||||
return getBlockData(blockId, new KMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns a {@link BlockData} corresponding to the blockID
|
||||
* it is used in any place Iris accepts {@link BlockData}
|
||||
*
|
||||
* @param blockId The id of the block to get
|
||||
* @param state The state of the block to get
|
||||
* @return Corresponding {@link BlockData} to the blockId
|
||||
* may return {@link IrisCustomData} for blocks that need a world for placement
|
||||
* @throws MissingResourceException when the blockId is invalid
|
||||
*/
|
||||
@NotNull
|
||||
public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of all {@link BlockProperty} objects associated with the specified block identifier.
|
||||
*
|
||||
* @param blockId The identifier of the block whose properties are to be retrieved. Must not be null.
|
||||
* @return A list of {@link BlockProperty} objects representing the properties of the block.
|
||||
* @throws MissingResourceException If the specified block identifier is invalid or cannot be found.
|
||||
*/
|
||||
@NotNull
|
||||
public List<BlockProperty> getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ExternalDataProvider#getItemStack(Identifier)
|
||||
*/
|
||||
@NotNull
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId) throws MissingResourceException {
|
||||
return getItemStack(itemId, new KMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns a {@link ItemStack} corresponding to the itemID
|
||||
* it is used in loot tables
|
||||
*
|
||||
* @param itemId The id of the item to get
|
||||
* @param customNbt Custom nbt to apply to the item
|
||||
* @return Corresponding {@link ItemStack} to the itemId
|
||||
* @throws MissingResourceException when the itemId is invalid
|
||||
*/
|
||||
@NotNull
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used for placing blocks that need to use the plugins api
|
||||
* it will only be called when the {@link ExternalDataProvider#getBlockData(Identifier, KMap)} returned a {@link IrisCustomData}
|
||||
*
|
||||
* @param engine The engine of the world the block is being placed in
|
||||
* @param block The block where the block should be placed
|
||||
* @param blockId The blockId to place
|
||||
*/
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {}
|
||||
|
||||
/**
|
||||
* Spawns a mob in the specified location using the given engine and entity identifier.
|
||||
*
|
||||
* @param location The location in the world where the mob should spawn. Must not be null.
|
||||
* @param entityId The identifier of the mob entity to spawn. Must not be null.
|
||||
* @return The spawned {@link Entity} if successful, or null if the mob could not be spawned.
|
||||
*/
|
||||
@Nullable
|
||||
public Entity spawnMob(@NotNull Location location, @NotNull Identifier entityId) throws MissingResourceException {
|
||||
throw new MissingResourceException("Failed to find Entity!", entityId.namespace(), entityId.key());
|
||||
}
|
||||
|
||||
public abstract @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType);
|
||||
|
||||
public abstract boolean isValidProvider(@NotNull Identifier id, DataType dataType);
|
||||
|
||||
protected static Pair<Float, BlockFace> parseYawAndFace(@NotNull Engine engine, @NotNull Block block, @NotNull KMap<@NotNull String, @NotNull String> state) {
|
||||
float yaw = 0;
|
||||
BlockFace face = BlockFace.NORTH;
|
||||
|
||||
long seed = engine.getSeedManager().getSeed() + Cache.key(block.getX(), block.getZ()) + block.getY();
|
||||
RNG rng = new RNG(seed);
|
||||
if ("true".equals(state.get("randomYaw"))) {
|
||||
yaw = rng.f(0, 360);
|
||||
} else if (state.containsKey("yaw")) {
|
||||
yaw = Float.parseFloat(state.get("yaw"));
|
||||
}
|
||||
if ("true".equals(state.get("randomFace"))) {
|
||||
BlockFace[] faces = BlockFace.values();
|
||||
face = faces[rng.i(0, faces.length - 1)];
|
||||
} else if (state.containsKey("face")) {
|
||||
face = BlockFace.valueOf(state.get("face").toUpperCase());
|
||||
}
|
||||
if (face == BlockFace.SELF) {
|
||||
face = BlockFace.NORTH;
|
||||
}
|
||||
|
||||
return new Pair<>(yaw, face);
|
||||
}
|
||||
|
||||
protected static List<BlockProperty> YAW_FACE_BIOME_PROPERTIES = List.of(
|
||||
BlockProperty.ofEnum(BiomeColor.class, "matchBiome", null),
|
||||
BlockProperty.ofBoolean("randomYaw", false),
|
||||
BlockProperty.ofDouble("yaw", 0, 0, 360f, false, true),
|
||||
BlockProperty.ofBoolean("randomFace", true),
|
||||
new BlockProperty(
|
||||
"face",
|
||||
BlockFace.class,
|
||||
BlockFace.NORTH,
|
||||
Arrays.asList(BlockFace.values()).subList(0, BlockFace.values().length - 1),
|
||||
BlockFace::name
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package art.arcane.iris.core.link;
|
||||
|
||||
import org.bukkit.NamespacedKey;
|
||||
|
||||
public record Identifier(String namespace, String key) {
|
||||
|
||||
private static final String DEFAULT_NAMESPACE = "minecraft";
|
||||
|
||||
public static Identifier fromNamespacedKey(NamespacedKey key) {
|
||||
return new Identifier(key.getNamespace(), key.getKey());
|
||||
}
|
||||
|
||||
public static Identifier fromString(String id) {
|
||||
String[] strings = id.split(":", 2);
|
||||
if (strings.length == 1) {
|
||||
return new Identifier(DEFAULT_NAMESPACE, strings[0]);
|
||||
} else {
|
||||
return new Identifier(strings[0], strings[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return namespace + ":" + key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Identifier i) {
|
||||
return i.namespace().equals(this.namespace) && i.key().equals(this.key);
|
||||
} else if (obj instanceof NamespacedKey i) {
|
||||
return i.getNamespace().equals(this.namespace) && i.getKey().equals(this.key);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
-37
@@ -16,35 +16,37 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.link;
|
||||
package art.arcane.iris.core.link;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||
import com.volmit.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
// See/update https://app.gitbook.com/@volmitsoftware/s/iris/compatability/papi/
|
||||
public class IrisPapiExpansion extends PlaceholderExpansion {
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
public @NotNull String getIdentifier() {
|
||||
return "iris";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthor() {
|
||||
public @NotNull String getAuthor() {
|
||||
return "Volmit Software";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
public @NotNull String getVersion() {
|
||||
return Iris.instance.getDescription().getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean persist() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -52,59 +54,63 @@ public class IrisPapiExpansion extends PlaceholderExpansion {
|
||||
Location l = null;
|
||||
PlatformChunkGenerator a = null;
|
||||
|
||||
if(player.isOnline()) {
|
||||
l = player.getPlayer().getLocation();
|
||||
if (player.isOnline() && player.getPlayer() != null) {
|
||||
l = player.getPlayer().getLocation().add(0, 2, 0);
|
||||
a = IrisToolbelt.access(l.getWorld());
|
||||
}
|
||||
|
||||
if(p.equalsIgnoreCase("biome_name")) {
|
||||
if(a != null) {
|
||||
return a.getEngine().getBiome(l).getName();
|
||||
if (p.equalsIgnoreCase("biome_name")) {
|
||||
if (a != null) {
|
||||
return getBiome(a, l).getName();
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("biome_id")) {
|
||||
if(a != null) {
|
||||
return a.getEngine().getBiome(l).getLoadKey();
|
||||
} else if (p.equalsIgnoreCase("biome_id")) {
|
||||
if (a != null) {
|
||||
return getBiome(a, l).getLoadKey();
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("biome_file")) {
|
||||
if(a != null) {
|
||||
return a.getEngine().getBiome(l).getLoadFile().getPath();
|
||||
} else if (p.equalsIgnoreCase("biome_file")) {
|
||||
if (a != null) {
|
||||
return getBiome(a, l).getLoadFile().getPath();
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("region_name")) {
|
||||
if(a != null) {
|
||||
} else if (p.equalsIgnoreCase("region_name")) {
|
||||
if (a != null) {
|
||||
return a.getEngine().getRegion(l).getName();
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("region_id")) {
|
||||
if(a != null) {
|
||||
} else if (p.equalsIgnoreCase("region_id")) {
|
||||
if (a != null) {
|
||||
return a.getEngine().getRegion(l).getLoadKey();
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("region_file")) {
|
||||
if(a != null) {
|
||||
} else if (p.equalsIgnoreCase("region_file")) {
|
||||
if (a != null) {
|
||||
return a.getEngine().getRegion(l).getLoadFile().getPath();
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("terrain_slope")) {
|
||||
if(a != null) {
|
||||
} else if (p.equalsIgnoreCase("terrain_slope")) {
|
||||
if (a != null) {
|
||||
return (a.getEngine())
|
||||
.getComplex().getSlopeStream()
|
||||
.get(l.getX(), l.getZ()) + "";
|
||||
.getComplex().getSlopeStream()
|
||||
.get(l.getX(), l.getZ()) + "";
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("terrain_height")) {
|
||||
if(a != null) {
|
||||
} else if (p.equalsIgnoreCase("terrain_height")) {
|
||||
if (a != null) {
|
||||
return Math.round(a.getEngine().getHeight(l.getBlockX(), l.getBlockZ())) + "";
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("world_mode")) {
|
||||
if(a != null) {
|
||||
} else if (p.equalsIgnoreCase("world_mode")) {
|
||||
if (a != null) {
|
||||
return a.isStudio() ? "Studio" : "Production";
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("world_seed")) {
|
||||
if(a != null) {
|
||||
} else if (p.equalsIgnoreCase("world_seed")) {
|
||||
if (a != null) {
|
||||
return a.getEngine().getSeedManager().getSeed() + "";
|
||||
}
|
||||
} else if(p.equalsIgnoreCase("world_speed")) {
|
||||
if(a != null) {
|
||||
} else if (p.equalsIgnoreCase("world_speed")) {
|
||||
if (a != null) {
|
||||
return a.getEngine().getGeneratedPerSecond() + "/s";
|
||||
}
|
||||
}
|
||||
|
||||
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,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.link;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.mvplugins.multiverse.core.MultiverseCoreApi;
|
||||
import org.mvplugins.multiverse.core.world.MultiverseWorld;
|
||||
import org.mvplugins.multiverse.core.world.WorldManager;
|
||||
import org.mvplugins.multiverse.core.world.options.ImportWorldOptions;
|
||||
|
||||
public class MultiverseCoreLink {
|
||||
private final boolean active;
|
||||
|
||||
public MultiverseCoreLink() {
|
||||
active = Bukkit.getPluginManager().getPlugin("Multiverse-Core") != null;
|
||||
}
|
||||
|
||||
public void removeFromConfig(World world) {
|
||||
removeFromConfig(world.getName());
|
||||
}
|
||||
|
||||
public void removeFromConfig(String world) {
|
||||
if (!active) return;
|
||||
var manager = worldManager();
|
||||
manager.removeWorld(world).onSuccess(manager::saveWorldsConfig);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void updateWorld(World bukkitWorld, String pack) {
|
||||
if (!active) return;
|
||||
var generator = "Iris:" + pack;
|
||||
var manager = worldManager();
|
||||
var world = manager.getWorld(bukkitWorld).getOrElse(() -> {
|
||||
var options = ImportWorldOptions.worldName(bukkitWorld.getName())
|
||||
.generator(generator)
|
||||
.environment(bukkitWorld.getEnvironment())
|
||||
.useSpawnAdjust(false);
|
||||
return manager.importWorld(options).get();
|
||||
});
|
||||
|
||||
world.setAutoLoad(false);
|
||||
if (!generator.equals(world.getGenerator())) {
|
||||
var field = MultiverseWorld.class.getDeclaredField("worldConfig");
|
||||
field.setAccessible(true);
|
||||
|
||||
var config = field.get(world);
|
||||
config.getClass()
|
||||
.getDeclaredMethod("setGenerator", String.class)
|
||||
.invoke(config, generator);
|
||||
}
|
||||
|
||||
manager.saveWorldsConfig();
|
||||
}
|
||||
|
||||
private WorldManager worldManager() {
|
||||
var api = MultiverseCoreApi.get();
|
||||
return api.getWorldManager();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package art.arcane.iris.core.link;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.volmlib.util.data.Cuboid;
|
||||
import art.arcane.volmlib.util.data.KCache;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.time.Duration;
|
||||
import java.util.UUID;
|
||||
|
||||
public class WorldEditLink {
|
||||
private static final AtomicCache<Boolean> active = new AtomicCache<>();
|
||||
|
||||
public static Cuboid getSelection(Player p) {
|
||||
if (!hasWorldEdit())
|
||||
return null;
|
||||
|
||||
try {
|
||||
Object instance = Class.forName("com.sk89q.worldedit.WorldEdit").getDeclaredMethod("getInstance").invoke(null);
|
||||
Object sessionManager = instance.getClass().getDeclaredMethod("getSessionManager").invoke(instance);
|
||||
Class<?> bukkitAdapter = Class.forName("com.sk89q.worldedit.bukkit.BukkitAdapter");
|
||||
Object world = bukkitAdapter.getDeclaredMethod("adapt", World.class).invoke(null, p.getWorld());
|
||||
Object player = bukkitAdapter.getDeclaredMethod("adapt", Player.class).invoke(null, p);
|
||||
Object localSession = sessionManager.getClass().getDeclaredMethod("getIfPresent", Class.forName("com.sk89q.worldedit.session.SessionOwner")).invoke(sessionManager, player);
|
||||
if (localSession == null) return null;
|
||||
|
||||
Object region = null;
|
||||
try {
|
||||
region = localSession.getClass().getDeclaredMethod("getSelection", Class.forName("com.sk89q.worldedit.world.World")).invoke(localSession, world);
|
||||
} catch (InvocationTargetException ignored) {}
|
||||
if (region == null) return null;
|
||||
|
||||
Object min = region.getClass().getDeclaredMethod("getMinimumPoint").invoke(region);
|
||||
Object max = region.getClass().getDeclaredMethod("getMaximumPoint").invoke(region);
|
||||
return new Cuboid(p.getWorld(),
|
||||
(int) min.getClass().getDeclaredMethod("x").invoke(min),
|
||||
(int) min.getClass().getDeclaredMethod("y").invoke(min),
|
||||
(int) min.getClass().getDeclaredMethod("z").invoke(min),
|
||||
(int) min.getClass().getDeclaredMethod("x").invoke(max),
|
||||
(int) min.getClass().getDeclaredMethod("y").invoke(max),
|
||||
(int) min.getClass().getDeclaredMethod("z").invoke(max)
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
Iris.error("Could not get selection");
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
active.reset();
|
||||
active.aquire(() -> false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean hasWorldEdit() {
|
||||
return active.aquire(() -> Bukkit.getPluginManager().isPluginEnabled("WorldEdit"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
|
||||
import net.momirealms.craftengine.core.block.ImmutableBlockState;
|
||||
import net.momirealms.craftengine.core.block.properties.BooleanProperty;
|
||||
import net.momirealms.craftengine.core.block.properties.IntegerProperty;
|
||||
import net.momirealms.craftengine.core.block.properties.Property;
|
||||
import net.momirealms.craftengine.core.util.Key;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class CraftEngineDataProvider extends ExternalDataProvider {
|
||||
private static final BlockProperty[] FURNITURE_PROPERTIES = new BlockProperty[]{
|
||||
BlockProperty.ofBoolean("randomYaw", false),
|
||||
BlockProperty.ofDouble("yaw", 0, 0, 360f, false, true),
|
||||
BlockProperty.ofBoolean("randomPitch", false),
|
||||
BlockProperty.ofDouble("pitch", 0, 0, 360f, false, true),
|
||||
};
|
||||
|
||||
public CraftEngineDataProvider() {
|
||||
super("CraftEngine");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<BlockProperty> getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException {
|
||||
Key key = Key.of(blockId.namespace(), blockId.key());
|
||||
net.momirealms.craftengine.core.block.CustomBlock block = CraftEngineBlocks.byId(key);
|
||||
if (block != null) {
|
||||
return block.properties().stream().map(CraftEngineDataProvider::convert).toList();
|
||||
}
|
||||
|
||||
net.momirealms.craftengine.core.entity.furniture.CustomFurniture furniture = CraftEngineFurniture.byId(key);
|
||||
if (furniture != null) {
|
||||
BlockProperty[] properties = Arrays.copyOf(FURNITURE_PROPERTIES, 5);
|
||||
properties[4] = new BlockProperty(
|
||||
"variant",
|
||||
String.class,
|
||||
furniture.anyVariantName(),
|
||||
furniture.variants().keySet(),
|
||||
Function.identity()
|
||||
);
|
||||
return List.of(properties);
|
||||
}
|
||||
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
net.momirealms.craftengine.core.item.CustomItem<ItemStack> item = CraftEngineItems.byId(Key.of(itemId.namespace(), itemId.key()));
|
||||
if (item == null) {
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
}
|
||||
|
||||
return item.buildItemStack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
Key key = Key.of(blockId.namespace(), blockId.key());
|
||||
if (CraftEngineBlocks.byId(key) == null && CraftEngineFurniture.byId(key) == null) {
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
|
||||
return IrisCustomData.of(B.getAir(), ExternalDataSVC.buildState(blockId, state));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
art.arcane.iris.core.nms.container.Pair<Identifier, KMap<String, String>> statePair = ExternalDataSVC.parseState(blockId);
|
||||
Identifier baseBlockId = statePair.getA();
|
||||
KMap<String, String> state = statePair.getB();
|
||||
Key key = Key.of(baseBlockId.namespace(), baseBlockId.key());
|
||||
|
||||
net.momirealms.craftengine.core.block.CustomBlock customBlock = CraftEngineBlocks.byId(key);
|
||||
if (customBlock != null) {
|
||||
ImmutableBlockState blockState = customBlock.defaultState();
|
||||
|
||||
for (Map.Entry<String, String> entry : state.entrySet()) {
|
||||
Property<?> property = customBlock.getProperty(entry.getKey());
|
||||
if (property == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Comparable<?> tag = property.optional(entry.getValue()).orElse(null);
|
||||
if (tag == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
blockState = ImmutableBlockState.with(blockState, property, tag);
|
||||
}
|
||||
|
||||
CraftEngineBlocks.place(block.getLocation(), blockState, false);
|
||||
return;
|
||||
}
|
||||
|
||||
net.momirealms.craftengine.core.entity.furniture.CustomFurniture furniture = CraftEngineFurniture.byId(key);
|
||||
if (furniture == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Location location = parseYawAndPitch(engine, block, state);
|
||||
String variant = state.getOrDefault("variant", furniture.anyVariantName());
|
||||
CraftEngineFurniture.place(location, furniture, variant, false);
|
||||
}
|
||||
|
||||
private static Location parseYawAndPitch(@NotNull Engine engine, @NotNull Block block, @NotNull Map<String, String> state) {
|
||||
Location location = block.getLocation();
|
||||
long seed = engine.getSeedManager().getSeed() + Cache.key(block.getX(), block.getZ()) + block.getY();
|
||||
RNG rng = new RNG(seed);
|
||||
|
||||
if ("true".equals(state.get("randomYaw"))) {
|
||||
location.setYaw(rng.f(0, 360));
|
||||
} else if (state.containsKey("yaw")) {
|
||||
location.setYaw(Float.parseFloat(state.get("yaw")));
|
||||
}
|
||||
|
||||
if ("true".equals(state.get("randomPitch"))) {
|
||||
location.setPitch(rng.f(0, 360));
|
||||
} else if (state.containsKey("pitch")) {
|
||||
location.setPitch(Float.parseFloat(state.get("pitch")));
|
||||
}
|
||||
|
||||
return location;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
Stream<Key> keys = switch (dataType) {
|
||||
case ENTITY -> Stream.<Key>empty();
|
||||
case ITEM -> CraftEngineItems.loadedItems().keySet().stream();
|
||||
case BLOCK -> Stream.concat(CraftEngineBlocks.loadedBlocks().keySet().stream(),
|
||||
CraftEngineFurniture.loadedFurniture().keySet().stream());
|
||||
};
|
||||
return keys.map(key -> new Identifier(key.namespace(), key.value())).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
Key key = Key.of(id.namespace(), id.key());
|
||||
return switch (dataType) {
|
||||
case ENTITY -> false;
|
||||
case ITEM -> CraftEngineItems.byId(key) != null;
|
||||
case BLOCK -> CraftEngineBlocks.byId(key) != null || CraftEngineFurniture.byId(key) != null;
|
||||
};
|
||||
}
|
||||
|
||||
private static <T extends Comparable<T>> BlockProperty convert(Property<T> raw) {
|
||||
return switch (raw) {
|
||||
case BooleanProperty property -> BlockProperty.ofBoolean(property.name(), property.defaultValue());
|
||||
case IntegerProperty property -> BlockProperty.ofLong(property.name(), property.defaultValue(), property.min, property.max, false, false);
|
||||
default -> new BlockProperty(raw.name(), raw.valueClass(), raw.defaultValue(), raw.possibleValues(), raw::valueName);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public enum DataType implements BiPredicate<ExternalDataProvider, Identifier> {
|
||||
ITEM,
|
||||
BLOCK,
|
||||
ENTITY;
|
||||
|
||||
@Override
|
||||
public boolean test(ExternalDataProvider dataProvider, Identifier identifier) {
|
||||
if (!dataProvider.isValidProvider(identifier, this)) return false;
|
||||
try {
|
||||
switch (this) {
|
||||
case ITEM -> dataProvider.getItemStack(identifier);
|
||||
case BLOCK -> dataProvider.getBlockData(identifier);
|
||||
case ENTITY -> {}
|
||||
}
|
||||
return true;
|
||||
} catch (MissingResourceException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Predicate<Identifier> asPredicate(ExternalDataProvider dataProvider) {
|
||||
return i -> test(dataProvider, i);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
import com.willfp.ecoitems.items.EcoItem;
|
||||
import com.willfp.ecoitems.items.EcoItems;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
|
||||
public class EcoItemsDataProvider extends ExternalDataProvider {
|
||||
private WrappedField<EcoItem, ItemStack> itemStack;
|
||||
private WrappedField<EcoItem, NamespacedKey> id;
|
||||
|
||||
public EcoItemsDataProvider() {
|
||||
super("EcoItems");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
Iris.info("Setting up EcoItems Link...");
|
||||
itemStack = new WrappedField<>(EcoItem.class, "_itemStack");
|
||||
if (this.itemStack.hasFailed()) {
|
||||
Iris.error("Failed to set up EcoItems Link: Unable to fetch ItemStack field!");
|
||||
}
|
||||
id = new WrappedField<>(EcoItem.class, "id");
|
||||
if (this.id.hasFailed()) {
|
||||
Iris.error("Failed to set up EcoItems Link: Unable to fetch id field!");
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
EcoItem item = EcoItems.INSTANCE.getByID(itemId.key());
|
||||
if (item == null) throw new MissingResourceException("Failed to find Item!", itemId.namespace(), itemId.key());
|
||||
return itemStack.get(item).clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
if (dataType != DataType.ITEM) return List.of();
|
||||
return EcoItems.INSTANCE.values()
|
||||
.stream()
|
||||
.map(x -> Identifier.fromNamespacedKey(id.get(x)))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
return id.namespace().equalsIgnoreCase("ecoitems") && dataType == DataType.ITEM;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import com.ssomar.score.api.executableitems.ExecutableItemsAPI;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Optional;
|
||||
|
||||
public class ExecutableItemsDataProvider extends ExternalDataProvider {
|
||||
public ExecutableItemsDataProvider() {
|
||||
super("ExecutableItems");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
Iris.info("Setting up ExecutableItems Link...");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
return ExecutableItemsAPI.getExecutableItemsManager().getExecutableItem(itemId.key())
|
||||
.map(item -> item.buildItem(1, Optional.empty()))
|
||||
.orElseThrow(() -> new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
if (dataType != DataType.ITEM) return List.of();
|
||||
return ExecutableItemsAPI.getExecutableItemsManager()
|
||||
.getExecutableItemIdsList()
|
||||
.stream()
|
||||
.map(name -> new Identifier("executable_items", name))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier key, DataType dataType) {
|
||||
return key.namespace().equalsIgnoreCase("executable_items") && dataType == DataType.ITEM;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.type.Leaves;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class HMCLeavesDataProvider extends ExternalDataProvider {
|
||||
private Object apiInstance;
|
||||
private WrappedReturningMethod<Object, Material> worldBlockType;
|
||||
private WrappedReturningMethod<Object, Boolean> setCustomBlock;
|
||||
private Map<String, Object> blockDataMap = Map.of();
|
||||
private Map<String, Supplier<ItemStack>> itemDataField = Map.of();
|
||||
|
||||
public HMCLeavesDataProvider() {
|
||||
super("HMCLeaves");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPluginId() {
|
||||
return "HMCLeaves";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
try {
|
||||
worldBlockType = new WrappedReturningMethod<>((Class<Object>) Class.forName("io.github.fisher2911.hmcleaves.data.BlockData"), "worldBlockType");
|
||||
apiInstance = getApiInstance(Class.forName("io.github.fisher2911.hmcleaves.api.HMCLeavesAPI"));
|
||||
setCustomBlock = new WrappedReturningMethod<>((Class<Object>) apiInstance.getClass(), "setCustomBlock", Location.class, String.class, boolean.class);
|
||||
Object config = getLeavesConfig(apiInstance.getClass());
|
||||
blockDataMap = getMap(config, "blockDataMap");
|
||||
itemDataField = getMap(config, "itemSupplierMap");
|
||||
} catch (Throwable e) {
|
||||
Iris.error("Failed to initialize HMCLeavesDataProvider: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
Object o = blockDataMap.get(blockId.key());
|
||||
if (o == null)
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
Material material = worldBlockType.invoke(o, new Object[0]);
|
||||
if (material == null)
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
BlockData blockData = Bukkit.createBlockData(material);
|
||||
if (IrisSettings.get().getGenerator().preventLeafDecay && blockData instanceof Leaves leaves)
|
||||
leaves.setPersistent(true);
|
||||
return IrisCustomData.of(blockData, ExternalDataSVC.buildState(blockId, state));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
if (!itemDataField.containsKey(itemId.key()))
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
return itemDataField.get(itemId.key()).get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
var pair = ExternalDataSVC.parseState(blockId);
|
||||
blockId = pair.getA();
|
||||
Boolean result = setCustomBlock.invoke(apiInstance, new Object[]{block.getLocation(), blockId.key(), false});
|
||||
if (result == null || !result)
|
||||
Iris.warn("Failed to set custom block! " + blockId.key() + " " + block.getX() + " " + block.getY() + " " + block.getZ());
|
||||
else if (IrisSettings.get().getGenerator().preventLeafDecay) {
|
||||
BlockData blockData = block.getBlockData();
|
||||
if (blockData instanceof Leaves leaves)
|
||||
leaves.setPersistent(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return List.of();
|
||||
return (dataType == DataType.BLOCK ? blockDataMap.keySet() : itemDataField.keySet())
|
||||
.stream()
|
||||
.map(x -> new Identifier("hmcleaves", x))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return (dataType == DataType.ITEM ? itemDataField.keySet() : blockDataMap.keySet()).contains(id.key());
|
||||
}
|
||||
|
||||
private <C, T> Map<String, T> getMap(C config, String name) {
|
||||
WrappedField<C, Map<String, T>> field = new WrappedField<>((Class<C>) config.getClass(), name);
|
||||
return field.get(config);
|
||||
}
|
||||
|
||||
private <A> A getApiInstance(Class<A> apiClass) {
|
||||
WrappedReturningMethod<A, A> instance = new WrappedReturningMethod<>(apiClass, "getInstance");
|
||||
return instance.invoke();
|
||||
}
|
||||
|
||||
private <A, C> C getLeavesConfig(Class<A> apiClass) {
|
||||
WrappedReturningMethod<A, A> instance = new WrappedReturningMethod<>(apiClass, "getInstance");
|
||||
WrappedField<A, C> config = new WrappedField<>(apiClass, "config");
|
||||
return config.get(instance.invoke());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import dev.lone.itemsadder.api.CustomBlock;
|
||||
import dev.lone.itemsadder.api.CustomStack;
|
||||
import dev.lone.itemsadder.api.Events.ItemsAdderLoadDataEvent;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ItemAdderDataProvider extends ExternalDataProvider {
|
||||
|
||||
private volatile Set<String> itemNamespaces = Set.of();
|
||||
private volatile Set<String> blockNamespaces = Set.of();
|
||||
|
||||
public ItemAdderDataProvider() {
|
||||
super("ItemsAdder");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
updateNamespaces();
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onLoadData(ItemsAdderLoadDataEvent event) {
|
||||
updateNamespaces();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
CustomBlock block = CustomBlock.getInstance(blockId.toString());
|
||||
if (block == null) {
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
return IrisCustomData.of(block.getBaseBlockData(), blockId);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
CustomStack stack = CustomStack.getInstance(itemId.toString());
|
||||
if (stack == null) {
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
}
|
||||
return stack.getItemStack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
CustomBlock custom;
|
||||
if ((custom = CustomBlock.place(blockId.toString(), block.getLocation())) == null)
|
||||
return;
|
||||
block.setBlockData(custom.getBaseBlockData(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
return switch (dataType) {
|
||||
case ENTITY -> List.of();
|
||||
case ITEM -> CustomStack.getNamespacedIdsInRegistry()
|
||||
.stream()
|
||||
.map(Identifier::fromString)
|
||||
.toList();
|
||||
case BLOCK -> CustomBlock.getNamespacedIdsInRegistry()
|
||||
.stream()
|
||||
.map(Identifier::fromString)
|
||||
.toList();
|
||||
};
|
||||
}
|
||||
|
||||
private void updateNamespaces() {
|
||||
try {
|
||||
updateNamespaces(DataType.ITEM);
|
||||
updateNamespaces(DataType.BLOCK);
|
||||
} catch (Throwable e) {
|
||||
Iris.warn("Failed to update ItemAdder namespaces: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateNamespaces(DataType dataType) {
|
||||
var namespaces = getTypes(dataType).stream().map(Identifier::namespace).collect(Collectors.toSet());
|
||||
if (dataType == DataType.ITEM) itemNamespaces = namespaces;
|
||||
else blockNamespaces = namespaces;
|
||||
Iris.debug("Updated ItemAdder namespaces: " + dataType + " - " + namespaces);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return dataType == DataType.ITEM ? itemNamespaces.contains(id.namespace()) : blockNamespaces.contains(id.namespace());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import me.kryniowesegryderiusz.kgenerators.Main;
|
||||
import me.kryniowesegryderiusz.kgenerators.api.KGeneratorsAPI;
|
||||
import me.kryniowesegryderiusz.kgenerators.generators.locations.objects.GeneratorLocation;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
|
||||
public class KGeneratorsDataProvider extends ExternalDataProvider {
|
||||
public KGeneratorsDataProvider() {
|
||||
super("KGenerators");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
if (Main.getGenerators().get(blockId.key()) == null) throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
return IrisCustomData.of(Material.STRUCTURE_VOID.createBlockData(), ExternalDataSVC.buildState(blockId, state));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
var gen = Main.getGenerators().get(itemId.key());
|
||||
if (gen == null) throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
return gen.getGeneratorItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
if (block.getType() != Material.STRUCTURE_VOID) return;
|
||||
var existing = KGeneratorsAPI.getLoadedGeneratorLocation(block.getLocation());
|
||||
if (existing != null) return;
|
||||
block.setBlockData(B.getAir(), false);
|
||||
var gen = Main.getGenerators().get(blockId.key());
|
||||
if (gen == null) return;
|
||||
var loc = new GeneratorLocation(-1, gen, block.getLocation(), Main.getPlacedGenerators().getChunkInfo(block.getChunk()), null, null);
|
||||
Main.getDatabases().getDb().saveGenerator(loc);
|
||||
Main.getPlacedGenerators().addLoaded(loc);
|
||||
Main.getSchedules().schedule(loc, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return List.of();
|
||||
return Main.getGenerators().getAll().stream()
|
||||
.map(gen -> new Identifier("kgenerators", gen.getId()))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return "kgenerators".equalsIgnoreCase(id.namespace());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import net.Indyuce.mmoitems.MMOItems;
|
||||
import net.Indyuce.mmoitems.api.ItemTier;
|
||||
import net.Indyuce.mmoitems.api.block.CustomBlock;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class MMOItemsDataProvider extends ExternalDataProvider {
|
||||
|
||||
public MMOItemsDataProvider() {
|
||||
super("MMOItems");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
Iris.info("Setting up MMOItems Link...");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
int id = -1;
|
||||
try {
|
||||
id = Integer.parseInt(blockId.key());
|
||||
} catch (NumberFormatException ignored) {}
|
||||
CustomBlock block = api().getCustomBlocks().getBlock(id);
|
||||
if (block == null) throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
return block.getState().getBlockData();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
String[] parts = itemId.namespace().split("_", 2);
|
||||
if (parts.length != 2)
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
CompletableFuture<ItemStack> future = new CompletableFuture<>();
|
||||
Runnable run = () -> {
|
||||
try {
|
||||
var type = api().getTypes().get(parts[1]);
|
||||
int level = -1;
|
||||
ItemTier tier = null;
|
||||
|
||||
if (customNbt != null) {
|
||||
level = (int) customNbt.getOrDefault("level", -1);
|
||||
tier = api().getTiers().get(String.valueOf(customNbt.get("tier")));
|
||||
}
|
||||
|
||||
ItemStack itemStack;
|
||||
if (type == null) {
|
||||
future.complete(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (level != -1 && tier != null) {
|
||||
itemStack = api().getItem(type, itemId.key(), level, tier);
|
||||
} else {
|
||||
itemStack = api().getItem(type, itemId.key());
|
||||
}
|
||||
future.complete(itemStack);
|
||||
} catch (Throwable e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
};
|
||||
if (Bukkit.isPrimaryThread()) run.run();
|
||||
else J.s(run);
|
||||
ItemStack item = null;
|
||||
try {
|
||||
item = future.get();
|
||||
} catch (InterruptedException | ExecutionException ignored) {}
|
||||
if (item == null)
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
return switch (dataType) {
|
||||
case ENTITY -> List.of();
|
||||
case BLOCK -> api().getCustomBlocks().getBlockIds().stream().map(id -> new Identifier("mmoitems", String.valueOf(id)))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
case ITEM -> {
|
||||
Supplier<Collection<Identifier>> supplier = () -> api().getTypes()
|
||||
.getAll()
|
||||
.stream()
|
||||
.flatMap(type -> api()
|
||||
.getTemplates()
|
||||
.getTemplateNames(type)
|
||||
.stream()
|
||||
.map(name -> new Identifier("mmoitems_" + type.getId(), name)))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
|
||||
if (Bukkit.isPrimaryThread()) yield supplier.get();
|
||||
else yield J.sfut(supplier).join();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return dataType == DataType.ITEM ? id.namespace().split("_", 2).length == 2 : id.namespace().equals("mmoitems");
|
||||
}
|
||||
|
||||
private MMOItems api() {
|
||||
return MMOItems.plugin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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.data;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.container.BiomeColor;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import io.lumine.mythic.bukkit.BukkitAdapter;
|
||||
import io.lumine.mythic.bukkit.utils.serialize.Chroma;
|
||||
import io.lumine.mythiccrucible.MythicCrucible;
|
||||
import io.lumine.mythiccrucible.items.CrucibleItem;
|
||||
import io.lumine.mythiccrucible.items.ItemManager;
|
||||
import io.lumine.mythiccrucible.items.blocks.CustomBlockItemContext;
|
||||
import io.lumine.mythiccrucible.items.furniture.FurnitureItemContext;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Optional;
|
||||
|
||||
public class MythicCrucibleDataProvider extends ExternalDataProvider {
|
||||
|
||||
private ItemManager itemManager;
|
||||
|
||||
public MythicCrucibleDataProvider() {
|
||||
super("MythicCrucible");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
Iris.info("Setting up MythicCrucible Link...");
|
||||
try {
|
||||
this.itemManager = MythicCrucible.inst().getItemManager();
|
||||
} catch (Exception e) {
|
||||
Iris.error("Failed to set up MythicCrucible Link: Unable to fetch MythicCrucible instance!");
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
CrucibleItem crucibleItem = this.itemManager.getItem(blockId.key())
|
||||
.orElseThrow(() -> new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()));
|
||||
CustomBlockItemContext blockItemContext = crucibleItem.getBlockData();
|
||||
FurnitureItemContext furnitureItemContext = crucibleItem.getFurnitureData();
|
||||
if (furnitureItemContext != null) {
|
||||
return IrisCustomData.of(B.getAir(), ExternalDataSVC.buildState(blockId, state));
|
||||
} else if (blockItemContext != null) {
|
||||
return blockItemContext.getBlockData();
|
||||
}
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<BlockProperty> getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException {
|
||||
CrucibleItem crucibleItem = this.itemManager.getItem(blockId.key())
|
||||
.orElseThrow(() -> new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key()));
|
||||
|
||||
if (crucibleItem.getFurnitureData() != null) {
|
||||
return YAW_FACE_BIOME_PROPERTIES;
|
||||
} else if (crucibleItem.getBlockData() != null) {
|
||||
return List.of();
|
||||
}
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
Optional<CrucibleItem> opt = this.itemManager.getItem(itemId.key());
|
||||
return BukkitAdapter.adapt(opt.orElseThrow(() ->
|
||||
new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key()))
|
||||
.getMythicItem()
|
||||
.generateItemStack(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
return itemManager.getItems()
|
||||
.stream()
|
||||
.map(i -> new Identifier("crucible", i.getInternalName()))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
var parsedState = ExternalDataSVC.parseState(blockId);
|
||||
var state = parsedState.getB();
|
||||
blockId = parsedState.getA();
|
||||
|
||||
Optional<CrucibleItem> item = itemManager.getItem(blockId.key());
|
||||
if (item.isEmpty()) return;
|
||||
FurnitureItemContext furniture = item.get().getFurnitureData();
|
||||
if (furniture == null) return;
|
||||
|
||||
var pair = parseYawAndFace(engine, block, state);
|
||||
BiomeColor type = null;
|
||||
Chroma color = null;
|
||||
try {
|
||||
type = BiomeColor.valueOf(state.get("matchBiome").toUpperCase());
|
||||
} catch (NullPointerException | IllegalArgumentException ignored) {}
|
||||
if (type != null) {
|
||||
var biomeColor = INMS.get().getBiomeColor(block.getLocation(), type);
|
||||
if (biomeColor == null) return;
|
||||
color = Chroma.of(biomeColor.getRGB());
|
||||
}
|
||||
furniture.place(block, pair.getB(), pair.getA(), color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier key, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return key.namespace().equalsIgnoreCase("crucible");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import io.lumine.mythic.api.adapters.AbstractLocation;
|
||||
import io.lumine.mythic.api.config.MythicLineConfig;
|
||||
import io.lumine.mythic.api.mobs.entities.SpawnReason;
|
||||
import io.lumine.mythic.api.skills.conditions.ILocationCondition;
|
||||
import io.lumine.mythic.bukkit.BukkitAdapter;
|
||||
import io.lumine.mythic.bukkit.MythicBukkit;
|
||||
import io.lumine.mythic.bukkit.adapters.BukkitWorld;
|
||||
import io.lumine.mythic.bukkit.events.MythicConditionLoadEvent;
|
||||
import io.lumine.mythic.core.mobs.ActiveMob;
|
||||
import io.lumine.mythic.core.mobs.MobStack;
|
||||
import io.lumine.mythic.core.skills.SkillCondition;
|
||||
import io.lumine.mythic.core.utils.annotations.MythicCondition;
|
||||
import io.lumine.mythic.core.utils.annotations.MythicField;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class MythicMobsDataProvider extends ExternalDataProvider {
|
||||
public MythicMobsDataProvider() {
|
||||
super("MythicMobs");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Entity spawnMob(@NotNull Location location, @NotNull Identifier entityId) throws MissingResourceException {
|
||||
var mm = spawnMob(BukkitAdapter.adapt(location), entityId);
|
||||
return mm == null ? null : mm.getEntity().getBukkitEntity();
|
||||
}
|
||||
|
||||
private ActiveMob spawnMob(AbstractLocation location, Identifier entityId) throws MissingResourceException {
|
||||
var manager = MythicBukkit.inst().getMobManager();
|
||||
var mm = manager.getMythicMob(entityId.key()).orElse(null);
|
||||
if (mm == null) {
|
||||
var stack = manager.getMythicMobStack(entityId.key());
|
||||
if (stack == null) throw new MissingResourceException("Failed to find Mob!", entityId.namespace(), entityId.key());
|
||||
return stack.spawn(location, 1d, SpawnReason.OTHER, null);
|
||||
}
|
||||
return mm.spawn(location, 1d, SpawnReason.OTHER, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
if (dataType != DataType.ENTITY) return List.of();
|
||||
var manager = MythicBukkit.inst().getMobManager();
|
||||
return Stream.concat(manager.getMobNames().stream(),
|
||||
manager.getMobStacks()
|
||||
.stream()
|
||||
.map(MobStack::getName)
|
||||
)
|
||||
.distinct()
|
||||
.map(name -> new Identifier("mythicmobs", name))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
return id.namespace().equalsIgnoreCase("mythicmobs") && dataType == DataType.ENTITY;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(MythicConditionLoadEvent event) {
|
||||
switch (event.getConditionName()) {
|
||||
case "irisbiome" -> event.register(new IrisBiomeCondition(event.getConditionName(), event.getConfig()));
|
||||
case "irisregion" -> event.register(new IrisRegionCondition(event.getConditionName(), event.getConfig()));
|
||||
}
|
||||
}
|
||||
|
||||
@MythicCondition(author = "CrazyDev22", name = "irisbiome", description = "Tests if the target is within the given list of biomes")
|
||||
public static class IrisBiomeCondition extends SkillCondition implements ILocationCondition {
|
||||
@MythicField(name = "biome", aliases = {"b"}, description = "A list of biomes to check")
|
||||
private Set<String> biomes = ConcurrentHashMap.newKeySet();
|
||||
@MythicField(name = "surface", aliases = {"s"}, description = "If the biome check should only be performed on the surface")
|
||||
private boolean surface;
|
||||
|
||||
public IrisBiomeCondition(String line, MythicLineConfig mlc) {
|
||||
super(line);
|
||||
String b = mlc.getString(new String[]{"biome", "b"}, "");
|
||||
biomes.addAll(Arrays.asList(b.split(",")));
|
||||
surface = mlc.getBoolean(new String[]{"surface", "s"}, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean check(AbstractLocation target) {
|
||||
var access = IrisToolbelt.access(((BukkitWorld) target.getWorld()).getBukkitWorld());
|
||||
if (access == null) return false;
|
||||
var engine = access.getEngine();
|
||||
if (engine == null) return false;
|
||||
var biome = surface ?
|
||||
engine.getSurfaceBiome(target.getBlockX(), target.getBlockZ()) :
|
||||
engine.getBiomeOrMantle(target.getBlockX(), target.getBlockY() - engine.getMinHeight(), target.getBlockZ());
|
||||
return biomes.contains(biome.getLoadKey());
|
||||
}
|
||||
}
|
||||
|
||||
@MythicCondition(author = "CrazyDev22", name = "irisregion", description = "Tests if the target is within the given list of biomes")
|
||||
public static class IrisRegionCondition extends SkillCondition implements ILocationCondition {
|
||||
@MythicField(name = "region", aliases = {"r"}, description = "A list of regions to check")
|
||||
private Set<String> regions = ConcurrentHashMap.newKeySet();
|
||||
|
||||
public IrisRegionCondition(String line, MythicLineConfig mlc) {
|
||||
super(line);
|
||||
String b = mlc.getString(new String[]{"region", "r"}, "");
|
||||
regions.addAll(Arrays.asList(b.split(",")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean check(AbstractLocation target) {
|
||||
var access = IrisToolbelt.access(((BukkitWorld) target.getWorld()).getBukkitWorld());
|
||||
if (access == null) return false;
|
||||
var engine = access.getEngine();
|
||||
if (engine == null) return false;
|
||||
var region = engine.getRegion(target.getBlockX(), target.getBlockZ());
|
||||
return regions.contains(region.getLoadKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package art.arcane.iris.core.link.data;
|
||||
|
||||
import com.nexomc.nexo.api.NexoBlocks;
|
||||
import com.nexomc.nexo.api.NexoFurniture;
|
||||
import com.nexomc.nexo.api.NexoItems;
|
||||
import com.nexomc.nexo.items.ItemBuilder;
|
||||
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.container.BiomeColor;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.ItemDisplay;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.LeatherArmorMeta;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.inventory.meta.PotionMeta;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.MissingResourceException;
|
||||
|
||||
public class NexoDataProvider extends ExternalDataProvider {
|
||||
public NexoDataProvider() {
|
||||
super("Nexo");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||
if (!NexoItems.exists(blockId.key())) {
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
|
||||
Identifier blockState = ExternalDataSVC.buildState(blockId, state);
|
||||
if (NexoBlocks.isCustomBlock(blockId.key())) {
|
||||
BlockData data = NexoBlocks.blockData(blockId.key());
|
||||
if (data == null)
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
return IrisCustomData.of(data, blockState);
|
||||
} else if (NexoFurniture.isFurniture(blockId.key())) {
|
||||
return IrisCustomData.of(B.getAir(), blockState);
|
||||
}
|
||||
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<BlockProperty> getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException {
|
||||
if (!NexoItems.exists(blockId.key())) {
|
||||
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||
}
|
||||
|
||||
return NexoFurniture.isFurniture(blockId.key()) ? YAW_FACE_BIOME_PROPERTIES : List.of();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||
ItemBuilder builder = NexoItems.itemFromId(itemId.key());
|
||||
if (builder == null) {
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
}
|
||||
try {
|
||||
return builder.build();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
var statePair = ExternalDataSVC.parseState(blockId);
|
||||
var state = statePair.getB();
|
||||
blockId = statePair.getA();
|
||||
|
||||
if (NexoBlocks.isCustomBlock(blockId.key())) {
|
||||
NexoBlocks.place(blockId.key(), block.getLocation());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NexoFurniture.isFurniture(blockId.key()))
|
||||
return;
|
||||
|
||||
var pair = parseYawAndFace(engine, block, state);
|
||||
ItemDisplay display = NexoFurniture.place(blockId.key(), block.getLocation(), pair.getA(), pair.getB());
|
||||
if (display == null) return;
|
||||
ItemStack itemStack = display.getItemStack();
|
||||
if (itemStack == null) return;
|
||||
|
||||
BiomeColor type = null;
|
||||
try {
|
||||
type = BiomeColor.valueOf(state.get("matchBiome").toUpperCase());
|
||||
} catch (NullPointerException | IllegalArgumentException ignored) {}
|
||||
|
||||
if (type != null) {
|
||||
var biomeColor = INMS.get().getBiomeColor(block.getLocation(), type);
|
||||
if (biomeColor == null) return;
|
||||
var potionColor = Color.fromARGB(biomeColor.getAlpha(), biomeColor.getRed(), biomeColor.getGreen(), biomeColor.getBlue());
|
||||
var meta = itemStack.getItemMeta();
|
||||
switch (meta) {
|
||||
case LeatherArmorMeta armor -> armor.setColor(potionColor);
|
||||
case PotionMeta potion -> potion.setColor(potionColor);
|
||||
case MapMeta map -> map.setColor(potionColor);
|
||||
case null, default -> {}
|
||||
}
|
||||
itemStack.setItemMeta(meta);
|
||||
}
|
||||
display.setItemStack(itemStack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return List.of();
|
||||
return NexoItems.itemNames()
|
||||
.stream()
|
||||
.map(i -> new Identifier("nexo", i))
|
||||
.filter(dataType.asPredicate(this))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||
if (dataType == DataType.ENTITY) return false;
|
||||
return "nexo".equalsIgnoreCase(id.namespace());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* 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.loader;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.object.IrisImage;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.volmlib.util.data.KCache;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class ImageResourceLoader extends ResourceLoader<IrisImage> {
|
||||
public ImageResourceLoader(File root, IrisData idm, String folderName, String resourceTypeName) {
|
||||
super(root, idm, folderName, resourceTypeName, IrisImage.class);
|
||||
loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getObjectLoaderCacheSize());
|
||||
}
|
||||
|
||||
public boolean supportsSchemas() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return loadCache.getSize();
|
||||
}
|
||||
|
||||
public long getTotalStorage() {
|
||||
return getSize();
|
||||
}
|
||||
|
||||
protected IrisImage loadFile(File j, String name) {
|
||||
try {
|
||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||
BufferedImage bu = ImageIO.read(j);
|
||||
IrisImage img = new IrisImage(bu);
|
||||
img.setLoadFile(j);
|
||||
img.setLoader(manager);
|
||||
img.setLoadKey(name);
|
||||
logLoad(j, img);
|
||||
tlt.addAndGet(p.getMilliseconds());
|
||||
return img;
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Iris.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath() + ": " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void getPNGFiles(File directory, Set<String> m, HashSet<String> visitedDirectories) {
|
||||
if (directory == null || !directory.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (directory.isDirectory()) {
|
||||
String canonicalDirectory = toCanonicalPath(directory);
|
||||
if (canonicalDirectory != null && !visitedDirectories.add(canonicalDirectory)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
File[] listedFiles = directory.listFiles();
|
||||
if (listedFiles == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (File file : listedFiles) {
|
||||
if (file.isFile() && file.getName().endsWith(".png")) {
|
||||
m.add(file.getName().replaceAll("\\Q.png\\E", ""));
|
||||
} else if (file.isDirectory()) {
|
||||
getPNGFiles(file, m, visitedDirectories);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public String[] getPossibleKeys() {
|
||||
if (possibleKeys != null) {
|
||||
return possibleKeys;
|
||||
}
|
||||
|
||||
Iris.debug("Building " + resourceTypeName + " Possibility Lists");
|
||||
KSet<String> m = new KSet<>();
|
||||
HashSet<String> visitedDirectories = new HashSet<>();
|
||||
|
||||
|
||||
for (File i : getFolders()) {
|
||||
getPNGFiles(i, m, visitedDirectories);
|
||||
}
|
||||
|
||||
// for (File i : getFolders()) {
|
||||
// for (File j : i.listFiles()) {
|
||||
// if (j.isFile() && j.getName().endsWith(".png")) {
|
||||
// m.add(j.getName().replaceAll("\\Q.png\\E", ""));
|
||||
// } else if (j.isDirectory()) {
|
||||
// for (File k : j.listFiles()) {
|
||||
// if (k.isFile() && k.getName().endsWith(".png")) {
|
||||
// m.add(j.getName() + "/" + k.getName().replaceAll("\\Q.png\\E", ""));
|
||||
// } else if (k.isDirectory()) {
|
||||
// for (File l : k.listFiles()) {
|
||||
// if (l.isFile() && l.getName().endsWith(".png")) {
|
||||
// m.add(j.getName() + "/" + k.getName() + "/" + l.getName().replaceAll("\\Q.png\\E", ""));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
KList<String> v = new KList<>(m);
|
||||
possibleKeys = v.toArray(new String[0]);
|
||||
return possibleKeys;
|
||||
}
|
||||
|
||||
private String toCanonicalPath(File file) {
|
||||
try {
|
||||
return file.getCanonicalPath();
|
||||
} catch (IOException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public File findFile(String name) {
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (name.equals("null")) {
|
||||
Iris.warn("Refusing " + resourceTypeName + " lookup for literal string \"null\" (called by " + callerHint() + ")");
|
||||
return null;
|
||||
}
|
||||
|
||||
for (File i : getFolders(name)) {
|
||||
for (File j : i.listFiles()) {
|
||||
if (j.isFile() && j.getName().endsWith(".png") && j.getName().split("\\Q.\\E")[0].equals(name)) {
|
||||
return j;
|
||||
}
|
||||
}
|
||||
|
||||
File file = new File(i, name + ".png");
|
||||
|
||||
if (file.exists()) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IrisImage load(String name) {
|
||||
return load(name, true);
|
||||
}
|
||||
|
||||
private IrisImage loadRaw(String name) {
|
||||
for (File i : getFolders(name)) {
|
||||
for (File j : i.listFiles()) {
|
||||
if (j.isFile() && j.getName().endsWith(".png") && j.getName().split("\\Q.\\E")[0].equals(name)) {
|
||||
return loadFile(j, name);
|
||||
}
|
||||
}
|
||||
|
||||
File file = new File(i, name + ".png");
|
||||
|
||||
if (file.exists()) {
|
||||
return loadFile(file, name);
|
||||
}
|
||||
}
|
||||
|
||||
Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IrisImage load(String name, boolean warn) {
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (name.equals("null") && warn) {
|
||||
Iris.warn("Refusing " + resourceTypeName + " load for literal string \"null\" (called by " + callerHint() + ")");
|
||||
return null;
|
||||
}
|
||||
return loadCache.get(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
/*
|
||||
* 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.loader;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.*;
|
||||
import art.arcane.iris.engine.object.annotations.Snippet;
|
||||
import art.arcane.iris.engine.object.matter.IrisMatterObject;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlagAdapter;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.iris.util.common.parallel.BurstExecutor;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.common.reflect.KeyedType;
|
||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import lombok.Data;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
@Data
|
||||
public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
private static final KMap<File, IrisData> dataLoaders = new KMap<>();
|
||||
private final File dataFolder;
|
||||
private final int id;
|
||||
private boolean closed = false;
|
||||
private ResourceLoader<IrisBiome> biomeLoader;
|
||||
private ResourceLoader<IrisLootTable> lootLoader;
|
||||
private ResourceLoader<IrisRegion> regionLoader;
|
||||
private ResourceLoader<IrisDimension> dimensionLoader;
|
||||
private ResourceLoader<IrisGenerator> generatorLoader;
|
||||
private ResourceLoader<IrisEntity> entityLoader;
|
||||
private ResourceLoader<IrisMarker> markerLoader;
|
||||
private ResourceLoader<IrisSpawner> spawnerLoader;
|
||||
private ResourceLoader<IrisMod> modLoader;
|
||||
private ResourceLoader<IrisBlockData> blockLoader;
|
||||
private ResourceLoader<IrisExpression> expressionLoader;
|
||||
private ResourceLoader<IrisObject> objectLoader;
|
||||
private ResourceLoader<IrisMatterObject> matterLoader;
|
||||
private ResourceLoader<IrisImage> imageLoader;
|
||||
private ResourceLoader<IrisMatterObject> matterObjectLoader;
|
||||
private KMap<String, KList<String>> possibleSnippets;
|
||||
private Gson gson;
|
||||
private Gson snippetLoader;
|
||||
private GsonBuilder builder;
|
||||
private KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
|
||||
private Engine engine;
|
||||
|
||||
private IrisData(File dataFolder) {
|
||||
this.engine = null;
|
||||
this.dataFolder = dataFolder;
|
||||
this.id = RNG.r.imax();
|
||||
hotloaded();
|
||||
}
|
||||
|
||||
public static IrisData get(File dataFolder) {
|
||||
return dataLoaders.computeIfAbsent(dataFolder, IrisData::new);
|
||||
}
|
||||
|
||||
public static Optional<IrisData> getLoaded(File dataFolder) {
|
||||
return Optional.ofNullable(dataLoaders.get(dataFolder));
|
||||
}
|
||||
|
||||
public static void dereference() {
|
||||
dataLoaders.values().forEach(IrisData::cleanupEngine);
|
||||
}
|
||||
|
||||
public static int cacheSize() {
|
||||
int m = 0;
|
||||
for (IrisData i : dataLoaders.values()) {
|
||||
for (ResourceLoader<?> j : i.getLoaders().values()) {
|
||||
m += j.getLoadCache().getSize();
|
||||
}
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
private static void printData(ResourceLoader<?> rl) {
|
||||
Iris.warn(" " + rl.getResourceTypeName() + " @ /" + rl.getFolderName() + ": Cache=" + rl.getLoadCache().getSize() + " Folders=" + rl.getFolders().size());
|
||||
}
|
||||
|
||||
public static IrisObject loadAnyObject(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisObject.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisMatterObject loadAnyMatter(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisMatterObject.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisBiome loadAnyBiome(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisBiome.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisExpression loadAnyExpression(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisExpression.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisMod loadAnyMod(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisMod.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisEntity loadAnyEntity(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisEntity.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisLootTable loadAnyLootTable(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisLootTable.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisBlockData loadAnyBlock(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisBlockData.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisSpawner loadAnySpaner(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisSpawner.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisRegion loadAnyRegion(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisRegion.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisMarker loadAnyMarker(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisMarker.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisImage loadAnyImage(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisImage.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisDimension loadAnyDimension(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisDimension.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisGenerator loadAnyGenerator(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisGenerator.class, key, nearest);
|
||||
}
|
||||
|
||||
public static <T extends IrisRegistrant> T loadAny(Class<T> type, String key, @Nullable IrisData nearest) {
|
||||
try {
|
||||
if (nearest != null) {
|
||||
T t = nearest.load(type, key, false);
|
||||
if (t != null) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
for (File i : Objects.requireNonNull(Iris.instance.getDataFolder("packs").listFiles())) {
|
||||
if (i.isDirectory()) {
|
||||
IrisData dm = get(i);
|
||||
if (dm == nearest) continue;
|
||||
T t = dm.load(type, key, false);
|
||||
|
||||
if (t != null) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public <T extends IrisRegistrant> T load(Class<T> type, String key, boolean warn) {
|
||||
var loader = getLoader(type);
|
||||
if (loader == null) return null;
|
||||
return loader.load(key, warn);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends IrisRegistrant> ResourceLoader<T> getLoader(Class<T> type) {
|
||||
return (ResourceLoader<T>) loaders.get(type);
|
||||
}
|
||||
|
||||
public ResourceLoader<?> getTypedLoaderFor(File f) {
|
||||
String[] k = f.getPath().split("\\Q" + File.separator + "\\E");
|
||||
|
||||
for (String i : k) {
|
||||
for (ResourceLoader<?> j : loaders.values()) {
|
||||
if (j.getFolderName().equals(i)) {
|
||||
return j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void cleanupEngine() {
|
||||
if (engine != null && engine.isClosed()) {
|
||||
engine = null;
|
||||
Iris.debug("Dereferenced Data<Engine> " + getId() + " " + getDataFolder());
|
||||
}
|
||||
}
|
||||
|
||||
public void preprocessObject(IrisRegistrant t) {
|
||||
}
|
||||
|
||||
public void close() {
|
||||
closed = true;
|
||||
dump();
|
||||
dataLoaders.remove(dataFolder);
|
||||
}
|
||||
|
||||
public IrisData copy() {
|
||||
return IrisData.get(dataFolder);
|
||||
}
|
||||
|
||||
private <T extends IrisRegistrant> ResourceLoader<T> registerLoader(Class<T> registrant) {
|
||||
try {
|
||||
IrisRegistrant rr = registrant.getConstructor().newInstance();
|
||||
ResourceLoader<T> r = null;
|
||||
if (registrant.equals(IrisObject.class)) {
|
||||
r = (ResourceLoader<T>) new ObjectResourceLoader(dataFolder, this, rr.getFolderName(),
|
||||
rr.getTypeName());
|
||||
} else if (registrant.equals(IrisMatterObject.class)) {
|
||||
r = (ResourceLoader<T>) new MatterObjectResourceLoader(dataFolder, this, rr.getFolderName(),
|
||||
rr.getTypeName());
|
||||
} else if (registrant.equals(IrisImage.class)) {
|
||||
r = (ResourceLoader<T>) new ImageResourceLoader(dataFolder, this, rr.getFolderName(),
|
||||
rr.getTypeName());
|
||||
} else {
|
||||
J.attempt(() -> registrant.getConstructor().newInstance().registerTypeAdapters(builder));
|
||||
r = new ResourceLoader<>(dataFolder, this, rr.getFolderName(), rr.getTypeName(), registrant);
|
||||
}
|
||||
|
||||
loaders.put(registrant, r);
|
||||
|
||||
return r;
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
Iris.error("Failed to create loader! " + registrant.getCanonicalName());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public synchronized void hotloaded() {
|
||||
closed = false;
|
||||
possibleSnippets = new KMap<>();
|
||||
builder = new GsonBuilder()
|
||||
.addDeserializationExclusionStrategy(this)
|
||||
.addSerializationExclusionStrategy(this)
|
||||
.setStrictness(Strictness.LENIENT)
|
||||
.registerTypeAdapterFactory(this)
|
||||
.registerTypeAdapter(MantleFlag.class, new MantleFlagAdapter())
|
||||
.setPrettyPrinting();
|
||||
loaders.clear();
|
||||
File packs = dataFolder;
|
||||
packs.mkdirs();
|
||||
this.lootLoader = registerLoader(IrisLootTable.class);
|
||||
this.spawnerLoader = registerLoader(IrisSpawner.class);
|
||||
this.entityLoader = registerLoader(IrisEntity.class);
|
||||
this.regionLoader = registerLoader(IrisRegion.class);
|
||||
this.biomeLoader = registerLoader(IrisBiome.class);
|
||||
this.modLoader = registerLoader(IrisMod.class);
|
||||
this.dimensionLoader = registerLoader(IrisDimension.class);
|
||||
this.generatorLoader = registerLoader(IrisGenerator.class);
|
||||
this.markerLoader = registerLoader(IrisMarker.class);
|
||||
this.blockLoader = registerLoader(IrisBlockData.class);
|
||||
this.expressionLoader = registerLoader(IrisExpression.class);
|
||||
this.objectLoader = registerLoader(IrisObject.class);
|
||||
this.imageLoader = registerLoader(IrisImage.class);
|
||||
this.matterObjectLoader = registerLoader(IrisMatterObject.class);
|
||||
builder.registerTypeAdapterFactory(KeyedType::createTypeAdapter);
|
||||
|
||||
gson = builder.create();
|
||||
|
||||
if (engine != null) {
|
||||
engine.hotload();
|
||||
}
|
||||
}
|
||||
|
||||
public void dump() {
|
||||
for (ResourceLoader<?> i : loaders.values()) {
|
||||
i.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
public void clearLists() {
|
||||
for (ResourceLoader<?> i : loaders.values()) {
|
||||
i.clearList();
|
||||
}
|
||||
possibleSnippets.clear();
|
||||
}
|
||||
|
||||
public Set<Class<?>> resolveSnippets() {
|
||||
var result = new HashSet<Class<?>>();
|
||||
var processed = new HashSet<Class<?>>();
|
||||
|
||||
var queue = new LinkedList<Class<?>>(loaders.keySet());
|
||||
while (!queue.isEmpty()) {
|
||||
var type = queue.poll();
|
||||
if (shouldSkipClass(type) || !processed.add(type))
|
||||
continue;
|
||||
if (type.isAnnotationPresent(Snippet.class))
|
||||
result.add(type);
|
||||
|
||||
try {
|
||||
for (var field : type.getDeclaredFields()) {
|
||||
if (shouldSkipField(new FieldAttributes(field)))
|
||||
continue;
|
||||
|
||||
queue.add(field.getType());
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public String toLoadKey(File f) {
|
||||
if (f.getPath().startsWith(getDataFolder().getPath())) {
|
||||
String[] full = f.getPath().split("\\Q" + File.separator + "\\E");
|
||||
String[] df = getDataFolder().getPath().split("\\Q" + File.separator + "\\E");
|
||||
StringBuilder g = new StringBuilder();
|
||||
boolean m = true;
|
||||
for (int i = 0; i < full.length; i++) {
|
||||
if (i >= df.length) {
|
||||
if (m) {
|
||||
m = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
g.append("/").append(full[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return g.substring(1).split("\\Q.\\E")[0];
|
||||
} else {
|
||||
Iris.error("Forign file from loader " + f.getPath() + " (loader realm: " + getDataFolder().getPath() + ")");
|
||||
}
|
||||
|
||||
Iris.error("Failed to load " + f.getPath() + " (loader realm: " + getDataFolder().getPath() + ")");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipField(FieldAttributes f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipClass(Class<?> c) {
|
||||
if (c.equals(AtomicCache.class)) {
|
||||
return true;
|
||||
} else return c.equals(ChronoLatch.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
|
||||
if (!typeToken.getRawType().isAnnotationPresent(Snippet.class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String snippetType = typeToken.getRawType().getDeclaredAnnotation(Snippet.class).value();
|
||||
String snippedBase = "snippet/" + snippetType + "/";
|
||||
|
||||
return new TypeAdapter<>() {
|
||||
@Override
|
||||
public void write(JsonWriter jsonWriter, T t) throws IOException {
|
||||
gson.getDelegateAdapter(IrisData.this, typeToken).write(jsonWriter, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T read(JsonReader reader) throws IOException {
|
||||
TypeAdapter<T> adapter = gson.getDelegateAdapter(IrisData.this, typeToken);
|
||||
|
||||
if (reader.peek().equals(JsonToken.STRING)) {
|
||||
String r = reader.nextString();
|
||||
if (!r.startsWith("snippet/"))
|
||||
return null;
|
||||
if (!r.startsWith(snippedBase))
|
||||
r = snippedBase + r.substring(8);
|
||||
|
||||
File f = new File(getDataFolder(), r + ".json");
|
||||
if (f.exists()) {
|
||||
try (JsonReader snippetReader = new JsonReader(new FileReader(f))){
|
||||
return adapter.read(snippetReader);
|
||||
} catch (Throwable e) {
|
||||
Iris.error("Couldn't read snippet " + r + " in " + reader.getPath() + " (" + e.getMessage() + ")");
|
||||
}
|
||||
} else {
|
||||
Iris.error("Couldn't find snippet " + r + " in " + reader.getPath());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return adapter.read(reader);
|
||||
} catch (Throwable e) {
|
||||
Iris.error("Failed to read " + typeToken.getRawType().getCanonicalName() + "... faking objects a little to load the file at least.");
|
||||
Iris.reportError(e);
|
||||
try {
|
||||
return (T) typeToken.getRawType().getConstructor().newInstance();
|
||||
} catch (Throwable ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public KList<String> getPossibleSnippets(String f) {
|
||||
return possibleSnippets.computeIfAbsent(f, (k) -> {
|
||||
KList<String> l = new KList<>();
|
||||
|
||||
File snippetFolder = new File(getDataFolder(), "snippet/" + f);
|
||||
if (!snippetFolder.exists()) return l;
|
||||
|
||||
String absPath = snippetFolder.getAbsolutePath();
|
||||
try (var stream = Files.walk(snippetFolder.toPath())) {
|
||||
stream.filter(Files::isRegularFile)
|
||||
.map(Path::toAbsolutePath)
|
||||
.map(Path::toString)
|
||||
.filter(s -> s.endsWith(".json"))
|
||||
.map(s -> s.substring(absPath.length() + 1))
|
||||
.map(s -> s.replace("\\", "/"))
|
||||
.map(s -> s.split("\\Q.\\E")[0])
|
||||
.forEach(s -> l.add("snippet/" + s));
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return l;
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
return closed;
|
||||
}
|
||||
|
||||
public void savePrefetch(Engine engine) {
|
||||
BurstExecutor b = MultiBurst.ioBurst.burst(loaders.size());
|
||||
|
||||
for (ResourceLoader<?> i : loaders.values()) {
|
||||
b.queue(() -> {
|
||||
try {
|
||||
i.saveFirstAccess(engine);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
b.complete();
|
||||
Iris.info("Saved Prefetch Cache to speed up future world startups");
|
||||
}
|
||||
|
||||
public void loadPrefetch(Engine engine) {
|
||||
BurstExecutor b = MultiBurst.ioBurst.burst(loaders.size());
|
||||
|
||||
for (ResourceLoader<?> i : loaders.values()) {
|
||||
b.queue(() -> {
|
||||
try {
|
||||
i.loadFirstAccess(engine);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
b.complete();
|
||||
Iris.info("Loaded Prefetch Cache to reduce generation disk use.");
|
||||
}
|
||||
}
|
||||
+8
-16
@@ -16,29 +16,21 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.loader;
|
||||
package art.arcane.iris.core.loader;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.engine.object.IrisScript;
|
||||
import com.volmit.iris.engine.object.annotations.ArrayType;
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
import com.volmit.iris.engine.object.annotations.RegistryListResource;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.json.JSONObject;
|
||||
import com.volmit.iris.util.plugin.VolmitSender;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.awt.Desktop;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
|
||||
@Data
|
||||
public abstract class IrisRegistrant {
|
||||
@Desc("Preprocess this object in-memory when it's loaded, run scripts using the variable 'Iris.getPreprocessorObject()' and modify properties about this object before it's used.")
|
||||
@RegistryListResource(IrisScript.class)
|
||||
@ArrayType(min = 1, type = String.class)
|
||||
private KList<String> preprocessors = new KList<>();
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
private transient IrisData loader;
|
||||
|
||||
private transient String loadKey;
|
||||
@@ -56,7 +48,7 @@ public abstract class IrisRegistrant {
|
||||
public File openInVSCode() {
|
||||
try {
|
||||
Desktop.getDesktop().open(getLoadFile());
|
||||
} catch(Throwable e) {
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
package art.arcane.iris.core.loader;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
final class JsonSchemaValidator {
|
||||
private static final ConcurrentHashMap<Class<?>, Set<String>> FIELD_CACHE = new ConcurrentHashMap<>();
|
||||
private static final int SUGGESTION_MAX_DISTANCE = 4;
|
||||
|
||||
private JsonSchemaValidator() {
|
||||
}
|
||||
|
||||
static void validateTopLevelKeys(JSONObject parsed, String rawText, File file, String resourceTypeName, Class<?> objectClass) {
|
||||
if (parsed == null || objectClass == null) {
|
||||
return;
|
||||
}
|
||||
Set<String> known = FIELD_CACHE.computeIfAbsent(objectClass, JsonSchemaValidator::collectFieldNames);
|
||||
for (String key : parsed.keySet()) {
|
||||
if (known.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
reportUnknownKey(key, rawText, file, resourceTypeName, known);
|
||||
}
|
||||
}
|
||||
|
||||
static void reportLoadFailure(File file, String rawText, String resourceTypeName, Throwable error) {
|
||||
String message = error.getMessage();
|
||||
if (message == null || message.isBlank()) {
|
||||
message = error.getClass().getSimpleName();
|
||||
}
|
||||
int line = extractLineFromMessage(message);
|
||||
String location = file.getPath();
|
||||
if (line > 0) {
|
||||
location = location + ":" + line;
|
||||
}
|
||||
StringBuilder out = new StringBuilder();
|
||||
out.append("Couldn't load ").append(resourceTypeName)
|
||||
.append(C.RED).append(" in ").append(C.WHITE).append(location).append(C.RED)
|
||||
.append(" -> ").append(message);
|
||||
String snippet = buildSnippet(rawText, line);
|
||||
if (snippet != null) {
|
||||
out.append('\n').append(snippet);
|
||||
}
|
||||
Iris.warn(out.toString());
|
||||
}
|
||||
|
||||
private static void reportUnknownKey(String key, String rawText, File file, String resourceTypeName, Set<String> known) {
|
||||
int line = findLineForKey(rawText, key);
|
||||
String suggestion = closestMatch(key, known);
|
||||
StringBuilder out = new StringBuilder();
|
||||
out.append("Unknown ").append(resourceTypeName).append(" field ")
|
||||
.append(C.WHITE).append('"').append(key).append('"').append(C.YELLOW)
|
||||
.append(" in ").append(C.WHITE).append(file.getPath());
|
||||
if (line > 0) {
|
||||
out.append(":").append(line);
|
||||
}
|
||||
out.append(C.YELLOW).append(" (Gson will silently ignore this)");
|
||||
if (suggestion != null) {
|
||||
out.append(". Did you mean ").append(C.WHITE).append('"').append(suggestion).append('"').append(C.YELLOW).append("?");
|
||||
}
|
||||
String snippet = buildSnippet(rawText, line);
|
||||
if (snippet != null) {
|
||||
out.append('\n').append(snippet);
|
||||
}
|
||||
Iris.warn(out.toString());
|
||||
}
|
||||
|
||||
private static Set<String> collectFieldNames(Class<?> cls) {
|
||||
Set<String> names = new LinkedHashSet<>();
|
||||
Class<?> c = cls;
|
||||
while (c != null && c != Object.class) {
|
||||
for (Field field : c.getDeclaredFields()) {
|
||||
int mods = field.getModifiers();
|
||||
if (Modifier.isStatic(mods) || Modifier.isTransient(mods)) {
|
||||
continue;
|
||||
}
|
||||
if (field.isSynthetic()) {
|
||||
continue;
|
||||
}
|
||||
SerializedName serialized = field.getAnnotation(SerializedName.class);
|
||||
if (serialized != null) {
|
||||
names.add(serialized.value());
|
||||
Collections.addAll(names, serialized.alternate());
|
||||
} else {
|
||||
names.add(field.getName());
|
||||
}
|
||||
}
|
||||
c = c.getSuperclass();
|
||||
}
|
||||
return Collections.unmodifiableSet(names);
|
||||
}
|
||||
|
||||
private static int findLineForKey(String rawText, String key) {
|
||||
if (rawText == null || key == null) {
|
||||
return -1;
|
||||
}
|
||||
Pattern pattern = Pattern.compile("\"" + Pattern.quote(key) + "\"\\s*:");
|
||||
Matcher matcher = pattern.matcher(rawText);
|
||||
if (!matcher.find()) {
|
||||
return -1;
|
||||
}
|
||||
int index = matcher.start();
|
||||
int line = 1;
|
||||
for (int i = 0; i < index; i++) {
|
||||
if (rawText.charAt(i) == '\n') {
|
||||
line++;
|
||||
}
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
private static int extractLineFromMessage(String message) {
|
||||
if (message == null) {
|
||||
return -1;
|
||||
}
|
||||
Matcher m = Pattern.compile("line\\s+(\\d+)").matcher(message);
|
||||
if (m.find()) {
|
||||
try {
|
||||
return Integer.parseInt(m.group(1));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static String buildSnippet(String rawText, int line) {
|
||||
if (rawText == null || line <= 0) {
|
||||
return null;
|
||||
}
|
||||
String[] lines = rawText.split("\n", -1);
|
||||
if (line > lines.length) {
|
||||
return null;
|
||||
}
|
||||
int from = Math.max(0, line - 2);
|
||||
int to = Math.min(lines.length, line + 1);
|
||||
StringBuilder out = new StringBuilder();
|
||||
int width = String.valueOf(to).length();
|
||||
for (int i = from; i < to; i++) {
|
||||
int n = i + 1;
|
||||
boolean focus = n == line;
|
||||
out.append(focus ? C.RED + "> " : C.GRAY + " ");
|
||||
out.append(String.format("%" + width + "d", n)).append(" | ");
|
||||
String content = lines[i];
|
||||
if (content.length() > 200) {
|
||||
content = content.substring(0, 200) + "...";
|
||||
}
|
||||
out.append(content);
|
||||
if (i < to - 1) {
|
||||
out.append('\n');
|
||||
}
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static String closestMatch(String key, Set<String> known) {
|
||||
String lowerKey = key.toLowerCase();
|
||||
String best = null;
|
||||
int bestDistance = Integer.MAX_VALUE;
|
||||
for (String candidate : known) {
|
||||
int d = levenshtein(lowerKey, candidate.toLowerCase());
|
||||
if (d < bestDistance) {
|
||||
bestDistance = d;
|
||||
best = candidate;
|
||||
}
|
||||
}
|
||||
if (best == null) {
|
||||
return null;
|
||||
}
|
||||
int threshold = Math.min(SUGGESTION_MAX_DISTANCE, Math.max(1, key.length() / 2));
|
||||
return bestDistance <= threshold ? best : null;
|
||||
}
|
||||
|
||||
private static int levenshtein(String a, String b) {
|
||||
int[] prev = new int[b.length() + 1];
|
||||
int[] curr = new int[b.length() + 1];
|
||||
for (int j = 0; j <= b.length(); j++) {
|
||||
prev[j] = j;
|
||||
}
|
||||
for (int i = 1; i <= a.length(); i++) {
|
||||
curr[0] = i;
|
||||
for (int j = 1; j <= b.length(); j++) {
|
||||
int cost = a.charAt(i - 1) == b.charAt(j - 1) ? 0 : 1;
|
||||
curr[j] = Math.min(Math.min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost);
|
||||
}
|
||||
int[] tmp = prev;
|
||||
prev = curr;
|
||||
curr = tmp;
|
||||
}
|
||||
return prev[b.length()];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* 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.loader;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.object.matter.IrisMatterObject;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.volmlib.util.data.KCache;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
|
||||
public class MatterObjectResourceLoader extends ResourceLoader<IrisMatterObject> {
|
||||
private String[] possibleKeys;
|
||||
|
||||
public MatterObjectResourceLoader(File root, IrisData idm, String folderName, String resourceTypeName) {
|
||||
super(root, idm, folderName, resourceTypeName, IrisMatterObject.class);
|
||||
loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getObjectLoaderCacheSize());
|
||||
}
|
||||
|
||||
public boolean supportsSchemas() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return loadCache.getSize();
|
||||
}
|
||||
|
||||
public long getTotalStorage() {
|
||||
return getSize();
|
||||
}
|
||||
|
||||
protected IrisMatterObject loadFile(File j, String name) {
|
||||
try {
|
||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||
IrisMatterObject t = IrisMatterObject.from(j);
|
||||
t.setLoadKey(name);
|
||||
t.setLoader(manager);
|
||||
t.setLoadFile(j);
|
||||
logLoad(j, t);
|
||||
tlt.addAndGet(p.getMilliseconds());
|
||||
return t;
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Iris.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath() + ": " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void findMatFiles(File dir, KSet<String> m, HashSet<String> visitedDirectories) {
|
||||
if (dir == null || !dir.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dir.isDirectory()) {
|
||||
String canonicalDirectory = toCanonicalPath(dir);
|
||||
if (canonicalDirectory != null && !visitedDirectories.add(canonicalDirectory)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
File[] listedFiles = dir.listFiles();
|
||||
if (listedFiles == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (File file : listedFiles) {
|
||||
if (file.isFile() && file.getName().endsWith(".mat")) {
|
||||
m.add(file.getName().replaceAll("\\Q.mat\\E", ""));
|
||||
} else if (file.isDirectory()) {
|
||||
findMatFiles(file, m, visitedDirectories);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String[] getPossibleKeys() {
|
||||
if (possibleKeys != null) {
|
||||
return possibleKeys;
|
||||
}
|
||||
|
||||
Iris.debug("Building " + resourceTypeName + " Possibility Lists");
|
||||
KSet<String> m = new KSet<>();
|
||||
HashSet<String> visitedDirectories = new HashSet<>();
|
||||
|
||||
for (File folder : getFolders()) {
|
||||
findMatFiles(folder, m, visitedDirectories);
|
||||
}
|
||||
|
||||
KList<String> v = new KList<>(m);
|
||||
possibleKeys = v.toArray(new String[0]);
|
||||
return possibleKeys;
|
||||
}
|
||||
|
||||
private String toCanonicalPath(File file) {
|
||||
try {
|
||||
return file.getCanonicalPath();
|
||||
} catch (IOException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// public String[] getPossibleKeys() {
|
||||
// if (possibleKeys != null) {
|
||||
// return possibleKeys;
|
||||
// }
|
||||
//
|
||||
// Iris.debug("Building " + resourceTypeName + " Possibility Lists");
|
||||
// KSet<String> m = new KSet<>();
|
||||
//
|
||||
// for (File i : getFolders()) {
|
||||
// for (File j : i.listFiles()) {
|
||||
// if (j.isFile() && j.getName().endsWith(".mat")) {
|
||||
// m.add(j.getName().replaceAll("\\Q.mat\\E", ""));
|
||||
// } else if (j.isDirectory()) {
|
||||
// for (File k : j.listFiles()) {
|
||||
// if (k.isFile() && k.getName().endsWith(".mat")) {
|
||||
// m.add(j.getName() + "/" + k.getName().replaceAll("\\Q.mat\\E", ""));
|
||||
// } else if (k.isDirectory()) {
|
||||
// for (File l : k.listFiles()) {
|
||||
// if (l.isFile() && l.getName().endsWith(".mat")) {
|
||||
// m.add(j.getName() + "/" + k.getName() + "/" + l.getName().replaceAll("\\Q.mat\\E", ""));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// KList<String> v = new KList<>(m);
|
||||
// possibleKeys = v.toArray(new String[0]);
|
||||
// return possibleKeys;
|
||||
// }
|
||||
|
||||
public File findFile(String name) {
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (name.equals("null")) {
|
||||
Iris.warn("Refusing " + resourceTypeName + " lookup for literal string \"null\" (called by " + callerHint() + ")");
|
||||
return null;
|
||||
}
|
||||
|
||||
for (File i : getFolders(name)) {
|
||||
for (File j : i.listFiles()) {
|
||||
if (j.isFile() && j.getName().endsWith(".mat") && j.getName().split("\\Q.\\E")[0].equals(name)) {
|
||||
return j;
|
||||
}
|
||||
}
|
||||
|
||||
File file = new File(i, name + ".mat");
|
||||
|
||||
if (file.exists()) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IrisMatterObject load(String name) {
|
||||
return load(name, true);
|
||||
}
|
||||
|
||||
private IrisMatterObject loadRaw(String name) {
|
||||
for (File i : getFolders(name)) {
|
||||
for (File j : i.listFiles()) {
|
||||
if (j.isFile() && j.getName().endsWith(".mat") && j.getName().split("\\Q.\\E")[0].equals(name)) {
|
||||
return loadFile(j, name);
|
||||
}
|
||||
}
|
||||
|
||||
File file = new File(i, name + ".mat");
|
||||
|
||||
if (file.exists()) {
|
||||
return loadFile(file, name);
|
||||
}
|
||||
}
|
||||
|
||||
Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IrisMatterObject load(String name, boolean warn) {
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (name.equals("null") && warn) {
|
||||
Iris.warn("Refusing " + resourceTypeName + " load for literal string \"null\" (called by " + callerHint() + ")");
|
||||
return null;
|
||||
}
|
||||
return loadCache.get(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* 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.loader;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.volmlib.util.data.KCache;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
|
||||
public class ObjectResourceLoader extends ResourceLoader<IrisObject> {
|
||||
public ObjectResourceLoader(File root, IrisData idm, String folderName, String resourceTypeName) {
|
||||
super(root, idm, folderName, resourceTypeName, IrisObject.class);
|
||||
loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getObjectLoaderCacheSize());
|
||||
}
|
||||
|
||||
public boolean supportsSchemas() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return loadCache.getSize();
|
||||
}
|
||||
|
||||
public long getTotalStorage() {
|
||||
return getSize();
|
||||
}
|
||||
|
||||
protected IrisObject loadFile(File j, String name) {
|
||||
try {
|
||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||
IrisObject t = new IrisObject(0, 0, 0);
|
||||
t.setLoadKey(name);
|
||||
t.setLoader(manager);
|
||||
t.setLoadFile(j);
|
||||
t.read(j);
|
||||
logLoad(j, t);
|
||||
tlt.addAndGet(p.getMilliseconds());
|
||||
return t;
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
String message = e.getMessage();
|
||||
String reason = e.getClass().getSimpleName();
|
||||
if (message != null && !message.isBlank()) {
|
||||
reason = reason + ": " + message;
|
||||
}
|
||||
Iris.warn("Couldn't read " + resourceTypeName + " file: " + j.getPath() + " (" + reason + ")");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String[] getPossibleKeys() {
|
||||
if (possibleKeys != null) {
|
||||
return possibleKeys;
|
||||
}
|
||||
Iris.debug("Building " + resourceTypeName + " Possibility Lists");
|
||||
KSet<String> m = new KSet<>();
|
||||
HashSet<String> visitedDirectories = new HashSet<>();
|
||||
for (File i : getFolders()) {
|
||||
m.addAll(getFiles(i, ".iob", true, visitedDirectories));
|
||||
}
|
||||
possibleKeys = m.toArray(new String[0]);
|
||||
return possibleKeys;
|
||||
}
|
||||
|
||||
private KList<String> getFiles(File dir, String ext, boolean skipDirName, HashSet<String> visitedDirectories) {
|
||||
KList<String> paths = new KList<>();
|
||||
if (dir == null || !dir.exists()) {
|
||||
return paths;
|
||||
}
|
||||
|
||||
if (dir.isDirectory()) {
|
||||
String canonicalDirectory = toCanonicalPath(dir);
|
||||
if (canonicalDirectory != null && !visitedDirectories.add(canonicalDirectory)) {
|
||||
return paths;
|
||||
}
|
||||
}
|
||||
|
||||
File[] listedFiles = dir.listFiles();
|
||||
if (listedFiles == null) {
|
||||
return paths;
|
||||
}
|
||||
|
||||
String name = skipDirName ? "" : dir.getName() + "/";
|
||||
for (File f : listedFiles) {
|
||||
if (f.isFile() && f.getName().endsWith(ext)) {
|
||||
paths.add(name + f.getName().replaceAll("\\Q" + ext + "\\E", ""));
|
||||
} else if (f.isDirectory()) {
|
||||
getFiles(f, ext, false, visitedDirectories).forEach(e -> paths.add(name + e));
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
private String toCanonicalPath(File file) {
|
||||
try {
|
||||
return file.getCanonicalPath();
|
||||
} catch (IOException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public File findFile(String name) {
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (name.equals("null")) {
|
||||
Iris.warn("Refusing " + resourceTypeName + " lookup for literal string \"null\" (called by " + callerHint() + ")");
|
||||
return null;
|
||||
}
|
||||
|
||||
for (File i : getFolders(name)) {
|
||||
for (File j : i.listFiles()) {
|
||||
if (j.isFile() && j.getName().endsWith(".iob") && j.getName().split("\\Q.\\E")[0].equals(name)) {
|
||||
return j;
|
||||
}
|
||||
}
|
||||
|
||||
File file = new File(i, name + ".iob");
|
||||
|
||||
if (file.exists()) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IrisObject load(String name) {
|
||||
return load(name, true);
|
||||
}
|
||||
|
||||
private IrisObject loadRaw(String name) {
|
||||
for (File i : getFolders(name)) {
|
||||
for (File j : i.listFiles()) {
|
||||
if (j.isFile() && j.getName().endsWith(".iob") && j.getName().split("\\Q.\\E")[0].equals(name)) {
|
||||
return loadFile(j, name);
|
||||
}
|
||||
}
|
||||
|
||||
File file = new File(i, name + ".iob");
|
||||
|
||||
if (file.exists()) {
|
||||
return loadFile(file, name);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IrisObject load(String name, boolean warn) {
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (name.equals("null") && warn) {
|
||||
Iris.warn("Refusing " + resourceTypeName + " load for literal string \"null\" (called by " + callerHint() + ")");
|
||||
return null;
|
||||
}
|
||||
IrisObject result = loadCache.get(name);
|
||||
if (result == null && warn) {
|
||||
Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
/*
|
||||
* 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.loader;
|
||||
|
||||
import com.google.common.util.concurrent.AtomicDouble;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.project.SchemaBuilder;
|
||||
import art.arcane.iris.core.service.PreservationSVC;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.MeteredCache;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.volmlib.util.data.KCache;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.io.CustomOutputStream;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.iris.util.common.parallel.BurstExecutor;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(exclude = "manager")
|
||||
@ToString(exclude = "manager")
|
||||
public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
||||
public static final AtomicDouble tlt = new AtomicDouble(0);
|
||||
private static final int CACHE_SIZE = 100000;
|
||||
private static final ExecutorService schemaBuildExecutor = Executors.newSingleThreadExecutor(runnable -> {
|
||||
Thread thread = new Thread(runnable, "Iris-Schema-Builder");
|
||||
thread.setDaemon(true);
|
||||
thread.setPriority(Thread.MIN_PRIORITY);
|
||||
return thread;
|
||||
});
|
||||
private static final Set<String> schemaBuildQueue = ConcurrentHashMap.newKeySet();
|
||||
protected final AtomicCache<KList<File>> folderCache;
|
||||
protected KSet<String> firstAccess;
|
||||
protected File root;
|
||||
protected String folderName;
|
||||
protected String resourceTypeName;
|
||||
protected KCache<String, T> loadCache;
|
||||
protected Class<? extends T> objectClass;
|
||||
protected String cname;
|
||||
protected String[] possibleKeys = null;
|
||||
protected IrisData manager;
|
||||
protected AtomicInteger loads;
|
||||
protected ChronoLatch sec;
|
||||
|
||||
public ResourceLoader(File root, IrisData manager, String folderName, String resourceTypeName, Class<? extends T> objectClass) {
|
||||
this.manager = manager;
|
||||
firstAccess = new KSet<>();
|
||||
folderCache = new AtomicCache<>();
|
||||
sec = new ChronoLatch(5000);
|
||||
loads = new AtomicInteger();
|
||||
this.objectClass = objectClass;
|
||||
cname = objectClass.getCanonicalName();
|
||||
this.resourceTypeName = resourceTypeName;
|
||||
this.root = root;
|
||||
this.folderName = folderName;
|
||||
loadCache = new KCache<>(this::loadRaw, IrisSettings.get().getPerformance().getResourceLoaderCacheSize());
|
||||
Iris.debug("Loader<" + C.GREEN + resourceTypeName + C.LIGHT_PURPLE + "> created in " + C.RED + "IDM/" + manager.getId() + C.LIGHT_PURPLE + " on " + C.GRAY + manager.getDataFolder().getPath());
|
||||
Iris.service(PreservationSVC.class).registerCache(this);
|
||||
}
|
||||
|
||||
public JSONObject buildSchema() {
|
||||
Iris.debug("Building Schema " + objectClass.getSimpleName() + " " + root.getPath());
|
||||
JSONObject o = new JSONObject();
|
||||
KList<String> fm = new KList<>();
|
||||
|
||||
for (int g = 1; g < 8; g++) {
|
||||
fm.add("/" + folderName + Form.repeat("/*", g) + ".json");
|
||||
}
|
||||
|
||||
o.put("fileMatch", new JSONArray(fm.toArray()));
|
||||
o.put("url", "./.iris/schema/" + getFolderName() + "-schema.json");
|
||||
File a = new File(getManager().getDataFolder(), ".iris/schema/" + getFolderName() + "-schema.json");
|
||||
String schemaPath = a.getAbsolutePath();
|
||||
if (!a.exists() && schemaBuildQueue.add(schemaPath)) {
|
||||
schemaBuildExecutor.execute(() -> {
|
||||
try {
|
||||
IO.writeAll(a, new SchemaBuilder(objectClass, manager).construct().toString(4));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
} finally {
|
||||
schemaBuildQueue.remove(schemaPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
public File findFile(String name) {
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (name.equals("null")) {
|
||||
Iris.warn("Refusing " + resourceTypeName + " lookup for literal string \"null\" (called by " + callerHint() + ")");
|
||||
return null;
|
||||
}
|
||||
|
||||
for (File i : getFolders(name)) {
|
||||
for (File j : i.listFiles()) {
|
||||
if (j.isFile() && j.getName().endsWith(".json") && j.getName().split("\\Q.\\E")[0].equals(name)) {
|
||||
return j;
|
||||
}
|
||||
}
|
||||
|
||||
File file = new File(i, name + ".json");
|
||||
|
||||
if (file.exists()) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static String describeName(String name) {
|
||||
if (name == null) return "<java null>";
|
||||
if (name.isEmpty()) return "<empty string>";
|
||||
if (name.equals("null")) return "\"null\" (literal string)";
|
||||
return "\"" + name + "\"";
|
||||
}
|
||||
|
||||
protected static String callerHint() {
|
||||
StackWalker walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
|
||||
return walker.walk(frames -> frames
|
||||
.filter(f -> {
|
||||
String cn = f.getClassName();
|
||||
return !cn.startsWith("art.arcane.iris.core.loader.")
|
||||
&& !cn.startsWith("art.arcane.volmlib.util.data.")
|
||||
&& !cn.startsWith("com.github.benmanes.caffeine.");
|
||||
})
|
||||
.limit(3)
|
||||
.map(f -> f.getClassName().substring(f.getClassName().lastIndexOf('.') + 1)
|
||||
+ "." + f.getMethodName() + ":" + f.getLineNumber())
|
||||
.reduce((a, b) -> a + " <- " + b)
|
||||
.orElse("<unknown>"));
|
||||
}
|
||||
|
||||
public void logLoad(File path, T t) {
|
||||
loads.getAndIncrement();
|
||||
|
||||
if (loads.get() == 1) {
|
||||
sec.flip();
|
||||
}
|
||||
|
||||
if (sec.flip()) {
|
||||
J.a(() -> {
|
||||
Iris.verbose("Loaded " + C.WHITE + loads.get() + " " + resourceTypeName + (loads.get() == 1 ? "" : "s") + C.GRAY + " (" + Form.f(getLoadCache().getSize()) + " " + resourceTypeName + (loadCache.getSize() == 1 ? "" : "s") + " Loaded)");
|
||||
loads.set(0);
|
||||
});
|
||||
}
|
||||
|
||||
Iris.debug("Loader<" + C.GREEN + resourceTypeName + C.LIGHT_PURPLE + "> iload " + C.YELLOW + t.getLoadKey() + C.LIGHT_PURPLE + " in " + C.GRAY + t.getLoadFile().getPath() + C.LIGHT_PURPLE + " TLT: " + C.RED + Form.duration(tlt.get(), 2));
|
||||
}
|
||||
|
||||
public void failLoad(File path, Throwable e) {
|
||||
failLoad(path, null, e);
|
||||
}
|
||||
|
||||
public void failLoad(File path, String rawText, Throwable e) {
|
||||
J.a(() -> JsonSchemaValidator.reportLoadFailure(path, rawText, resourceTypeName, e));
|
||||
}
|
||||
|
||||
private KList<File> matchAllFiles(File root, Predicate<File> f) {
|
||||
KList<File> files = new KList<>();
|
||||
HashSet<String> visitedDirectories = new HashSet<>();
|
||||
matchFiles(root, files, f, visitedDirectories);
|
||||
return files;
|
||||
}
|
||||
|
||||
private void matchFiles(File at, KList<File> files, Predicate<File> f, HashSet<String> visitedDirectories) {
|
||||
if (at == null || !at.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (at.isDirectory()) {
|
||||
String canonicalPath = toCanonicalPath(at);
|
||||
if (canonicalPath != null && !visitedDirectories.add(canonicalPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
File[] listedFiles = at.listFiles();
|
||||
if (listedFiles == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (File listedFile : listedFiles) {
|
||||
matchFiles(listedFile, files, f, visitedDirectories);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (f.test(at)) {
|
||||
files.add(at);
|
||||
}
|
||||
}
|
||||
|
||||
private String toCanonicalPath(File file) {
|
||||
try {
|
||||
return file.getCanonicalPath();
|
||||
} catch (IOException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String[] getPossibleKeys() {
|
||||
if (possibleKeys != null) {
|
||||
return possibleKeys;
|
||||
}
|
||||
|
||||
KList<File> files = getFolders();
|
||||
|
||||
if (files == null) {
|
||||
possibleKeys = new String[0];
|
||||
return possibleKeys;
|
||||
}
|
||||
|
||||
HashSet<String> m = new HashSet<>();
|
||||
for (File i : files) {
|
||||
for (File j : matchAllFiles(i, (f) -> f.getName().endsWith(".json"))) {
|
||||
m.add(i.toURI().relativize(j.toURI()).getPath().replaceAll("\\Q.json\\E", ""));
|
||||
}
|
||||
}
|
||||
|
||||
KList<String> v = new KList<>(m);
|
||||
possibleKeys = v.toArray(new String[0]);
|
||||
return possibleKeys;
|
||||
}
|
||||
|
||||
public long count() {
|
||||
return loadCache.getSize();
|
||||
}
|
||||
|
||||
protected T loadFile(File j, String name) {
|
||||
String rawText = null;
|
||||
try {
|
||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||
rawText = IO.readAll(j);
|
||||
JSONObject parsed = new JSONObject(rawText);
|
||||
JsonSchemaValidator.validateTopLevelKeys(parsed, rawText, j, resourceTypeName, objectClass);
|
||||
T t = getManager().getGson()
|
||||
.fromJson(preprocess(parsed).toString(0), objectClass);
|
||||
t.setLoadKey(name);
|
||||
t.setLoadFile(j);
|
||||
t.setLoader(manager);
|
||||
getManager().preprocessObject(t);
|
||||
logLoad(j, t);
|
||||
tlt.addAndGet(p.getMilliseconds());
|
||||
return t;
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
failLoad(j, rawText, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected JSONObject preprocess(JSONObject j) {
|
||||
return j;
|
||||
}
|
||||
|
||||
public Stream<T> streamAll() {
|
||||
return streamAll(Arrays.stream(getPossibleKeys()));
|
||||
}
|
||||
|
||||
public Stream<T> streamAll(Stream<String> s) {
|
||||
return s.map(this::load);
|
||||
}
|
||||
|
||||
public KList<T> loadAll(KList<String> s) {
|
||||
KList<T> m = new KList<>();
|
||||
|
||||
for (String i : s) {
|
||||
T t = load(i);
|
||||
|
||||
if (t != null) {
|
||||
m.add(t);
|
||||
}
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
public KList<T> loadAllParallel(KList<String> s) {
|
||||
KList<T> m = new KList<>();
|
||||
BurstExecutor burst = MultiBurst.ioBurst.burst(s.size());
|
||||
|
||||
for (String i : s) {
|
||||
burst.queue(() -> {
|
||||
T t = load(i);
|
||||
if (t == null)
|
||||
return;
|
||||
|
||||
synchronized (m) {
|
||||
m.add(t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
burst.complete();
|
||||
return m;
|
||||
}
|
||||
|
||||
public KList<T> loadAll(KList<String> s, Consumer<T> postLoad) {
|
||||
KList<T> m = new KList<>();
|
||||
|
||||
for (String i : s) {
|
||||
T t = load(i);
|
||||
|
||||
if (t != null) {
|
||||
m.add(t);
|
||||
postLoad.accept(t);
|
||||
}
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
public KList<T> loadAll(String[] s) {
|
||||
KList<T> m = new KList<>();
|
||||
|
||||
for (String i : s) {
|
||||
T t = load(i);
|
||||
|
||||
if (t != null) {
|
||||
m.add(t);
|
||||
}
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
public T load(String name) {
|
||||
return load(name, true);
|
||||
}
|
||||
|
||||
private T loadRaw(String name) {
|
||||
for (File i : getFolders(name)) {
|
||||
//noinspection ConstantConditions
|
||||
for (File j : i.listFiles()) {
|
||||
if (j.isFile() && j.getName().endsWith(".json") && j.getName().split("\\Q.\\E")[0].equals(name)) {
|
||||
return loadFile(j, name);
|
||||
}
|
||||
}
|
||||
|
||||
File file = new File(i, name + ".json");
|
||||
|
||||
if (file.exists()) {
|
||||
return loadFile(file, name);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public T load(String name, boolean warn) {
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (name.equals("null") && warn) {
|
||||
Iris.warn("Refusing " + resourceTypeName + " load for literal string \"null\" (called by " + callerHint() + ")");
|
||||
return null;
|
||||
}
|
||||
|
||||
var set = firstAccess;
|
||||
if (set != null) firstAccess.add(name);
|
||||
return loadCache.get(name);
|
||||
}
|
||||
|
||||
public void loadFirstAccess(Engine engine) throws IOException {
|
||||
String id = "DIM" + Math.abs(engine.getSeedManager().getSeed() + engine.getDimension().getVersion() + engine.getDimension().getLoadKey().hashCode());
|
||||
File file = Iris.instance.getDataFile("prefetch/" + id + "/" + Math.abs(getFolderName().hashCode()) + ".ipfch");
|
||||
|
||||
if (!file.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileInputStream fin = new FileInputStream(file);
|
||||
GZIPInputStream gzi = new GZIPInputStream(fin);
|
||||
DataInputStream din = new DataInputStream(gzi);
|
||||
int m = din.readInt();
|
||||
KList<String> s = new KList<>();
|
||||
|
||||
for (int i = 0; i < m; i++) {
|
||||
s.add(din.readUTF());
|
||||
}
|
||||
|
||||
din.close();
|
||||
Iris.info("Loading " + s.size() + " prefetch " + getFolderName());
|
||||
firstAccess = null;
|
||||
loadAllParallel(s);
|
||||
}
|
||||
|
||||
public void saveFirstAccess(Engine engine) throws IOException {
|
||||
if (firstAccess == null) return;
|
||||
String id = "DIM" + Math.abs(engine.getSeedManager().getSeed() + engine.getDimension().getVersion() + engine.getDimension().getLoadKey().hashCode());
|
||||
File file = Iris.instance.getDataFile("prefetch/" + id + "/" + Math.abs(getFolderName().hashCode()) + ".ipfch");
|
||||
file.getParentFile().mkdirs();
|
||||
FileOutputStream fos = new FileOutputStream(file);
|
||||
GZIPOutputStream gzo = new CustomOutputStream(fos, 9);
|
||||
DataOutputStream dos = new DataOutputStream(gzo);
|
||||
var set = firstAccess;
|
||||
firstAccess = null;
|
||||
dos.writeInt(set.size());
|
||||
|
||||
for (String i : set) {
|
||||
dos.writeUTF(i);
|
||||
}
|
||||
|
||||
dos.flush();
|
||||
dos.close();
|
||||
}
|
||||
|
||||
public KList<File> getFolders() {
|
||||
return folderCache.aquire(() -> {
|
||||
KList<File> fc = new KList<>();
|
||||
|
||||
File[] files = root.listFiles();
|
||||
if (files == null) {
|
||||
throw new IllegalStateException("Failed to list files in " + root);
|
||||
}
|
||||
|
||||
for (File i : files) {
|
||||
if (i.isDirectory()) {
|
||||
if (i.getName().equals(folderName)) {
|
||||
fc.add(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return fc;
|
||||
});
|
||||
}
|
||||
|
||||
public KList<File> getFolders(String rc) {
|
||||
KList<File> folders = getFolders().copy();
|
||||
|
||||
if (rc.contains(":")) {
|
||||
for (File i : folders.copy()) {
|
||||
if (!rc.startsWith(i.getName() + ":")) {
|
||||
folders.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return folders;
|
||||
}
|
||||
|
||||
public void clearCache() {
|
||||
possibleKeys = null;
|
||||
loadCache.invalidate();
|
||||
folderCache.reset();
|
||||
}
|
||||
|
||||
public File fileFor(T b) {
|
||||
for (File i : getFolders()) {
|
||||
for (File j : i.listFiles()) {
|
||||
if (j.isFile() && j.getName().endsWith(".json") && j.getName().split("\\Q.\\E")[0].equals(b.getLoadKey())) {
|
||||
return j;
|
||||
}
|
||||
}
|
||||
|
||||
File file = new File(i, b.getLoadKey() + ".json");
|
||||
|
||||
if (file.exists()) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isLoaded(String next) {
|
||||
return loadCache.contains(next);
|
||||
}
|
||||
|
||||
public void clearList() {
|
||||
folderCache.reset();
|
||||
possibleKeys = null;
|
||||
}
|
||||
|
||||
public KList<String> getPossibleKeys(String arg) {
|
||||
KList<String> f = new KList<>();
|
||||
|
||||
for (String i : getPossibleKeys()) {
|
||||
if (i.equalsIgnoreCase(arg) || i.toLowerCase(Locale.ROOT).startsWith(arg.toLowerCase(Locale.ROOT)) || i.toLowerCase(Locale.ROOT).contains(arg.toLowerCase(Locale.ROOT)) || arg.toLowerCase(Locale.ROOT).contains(i.toLowerCase(Locale.ROOT))) {
|
||||
f.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
public boolean supportsSchemas() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void clean() {
|
||||
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return loadCache.getSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KCache<?, ?> getRawCache() {
|
||||
return loadCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMaxSize() {
|
||||
return loadCache.getMaxSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return getManager().isClosed();
|
||||
}
|
||||
|
||||
public long getTotalStorage() {
|
||||
return getSize();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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.nms;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.nms.v1X.NMSBinding1X;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class INMS {
|
||||
private static final Version CURRENT = Boolean.getBoolean("iris.no-version-limit") ?
|
||||
new Version(Integer.MAX_VALUE, Integer.MAX_VALUE, null) :
|
||||
new Version(21, 11, null);
|
||||
|
||||
private static final List<Version> REVISION = List.of(
|
||||
new Version(21, 11, "v1_21_R7")
|
||||
);
|
||||
|
||||
private static final List<Version> PACKS = List.of(
|
||||
new Version(21, 11, "31100")
|
||||
);
|
||||
|
||||
//@done
|
||||
private static final INMSBinding binding = bind();
|
||||
public static final String OVERWORLD_TAG = getTag(PACKS, "31100");
|
||||
|
||||
public static INMSBinding get() {
|
||||
return binding;
|
||||
}
|
||||
|
||||
public static String getNMSTag() {
|
||||
if (IrisSettings.get().getGeneral().isDisableNMS()) {
|
||||
return "BUKKIT";
|
||||
}
|
||||
|
||||
try {
|
||||
String name = Bukkit.getServer().getClass().getCanonicalName();
|
||||
if (name.equals("org.bukkit.craftbukkit.CraftServer")) {
|
||||
return getTag(REVISION, "BUKKIT");
|
||||
} else {
|
||||
return name.split("\\Q.\\E")[3];
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Iris.error("Failed to determine server nms version!");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return "BUKKIT";
|
||||
}
|
||||
|
||||
private static INMSBinding bind() {
|
||||
String code = getNMSTag();
|
||||
boolean disableNms = IrisSettings.get().getGeneral().isDisableNMS();
|
||||
List<String> probeCodes = NmsBindingProbeSupport.getBindingProbeCodes(code, disableNms, getFallbackBindingCodes());
|
||||
if ("BUKKIT".equals(code) && !disableNms) {
|
||||
Iris.info("NMS tag resolution fell back to Bukkit; probing supported revision bindings.");
|
||||
}
|
||||
|
||||
for (int i = 0; i < probeCodes.size(); i++) {
|
||||
INMSBinding resolvedBinding = tryBind(probeCodes.get(i), i == 0);
|
||||
if (resolvedBinding != null) {
|
||||
return resolvedBinding;
|
||||
}
|
||||
}
|
||||
|
||||
if (disableNms) {
|
||||
Iris.info("Craftbukkit " + code + " <-> " + NMSBinding1X.class.getSimpleName() + " Successfully Bound");
|
||||
Iris.warn("Note: NMS support is disabled. Iris is running in limited Bukkit fallback mode.");
|
||||
return new NMSBinding1X();
|
||||
}
|
||||
|
||||
MinecraftVersion detectedVersion = getMinecraftVersion();
|
||||
String serverVersion = detectedVersion == null ? Bukkit.getServer().getVersion() : detectedVersion.value();
|
||||
throw new IllegalStateException("Iris requires Minecraft 1.21.11 or newer. Detected server version: " + serverVersion);
|
||||
}
|
||||
|
||||
private static String getTag(List<Version> versions, String def) {
|
||||
MinecraftVersion detectedVersion = getMinecraftVersion();
|
||||
if (detectedVersion == null) {
|
||||
return def;
|
||||
}
|
||||
|
||||
if (detectedVersion.isNewerThan(CURRENT.major, CURRENT.minor)) {
|
||||
return versions.getFirst().tag;
|
||||
}
|
||||
|
||||
for (Version p : versions) {
|
||||
if (!detectedVersion.isAtLeast(p.major, p.minor)) {
|
||||
continue;
|
||||
}
|
||||
return p.tag;
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
private static MinecraftVersion getMinecraftVersion() {
|
||||
try {
|
||||
return MinecraftVersion.detect(Bukkit.getServer());
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Iris.error("Failed to determine server minecraft version!");
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static INMSBinding tryBind(String code, boolean announce) {
|
||||
if (announce) {
|
||||
Iris.info("Locating NMS Binding for " + code);
|
||||
} else {
|
||||
Iris.info("Probing NMS Binding for " + code);
|
||||
}
|
||||
|
||||
try {
|
||||
Class<?> clazz = Class.forName("art.arcane.iris.core.nms." + code + ".NMSBinding");
|
||||
Object candidate = clazz.getConstructor().newInstance();
|
||||
if (candidate instanceof INMSBinding binding) {
|
||||
Iris.info("Craftbukkit " + code + " <-> " + candidate.getClass().getSimpleName() + " Successfully Bound");
|
||||
return binding;
|
||||
}
|
||||
} catch (ClassNotFoundException | NoClassDefFoundError classNotFoundException) {
|
||||
Iris.warn("Failed to load NMS binding class for " + code + ": " + classNotFoundException.getMessage());
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Set<String> getFallbackBindingCodes() {
|
||||
Set<String> codes = new LinkedHashSet<>();
|
||||
for (Version version : REVISION) {
|
||||
if (version.tag != null && !version.tag.isBlank()) {
|
||||
codes.add(version.tag);
|
||||
}
|
||||
}
|
||||
return codes;
|
||||
}
|
||||
|
||||
private record Version(int major, int minor, String tag) {}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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.nms;
|
||||
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleCaller;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleRequest;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||
import art.arcane.iris.core.nms.container.BiomeColor;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
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.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.math.Vector3d;
|
||||
import art.arcane.volmlib.util.nbt.mca.palette.MCABiomeContainer;
|
||||
import art.arcane.volmlib.util.nbt.mca.palette.MCAPaletteAccess;
|
||||
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface INMSBinding {
|
||||
boolean hasTile(Material material);
|
||||
|
||||
boolean hasTile(Location l);
|
||||
|
||||
KMap<String, Object> serializeTile(Location location);
|
||||
|
||||
void deserializeTile(KMap<String, Object> s, Location newPosition);
|
||||
|
||||
CompoundTag serializeEntity(Entity location);
|
||||
|
||||
Entity deserializeEntity(CompoundTag s, Location newPosition);
|
||||
|
||||
boolean supportsCustomHeight();
|
||||
|
||||
Object getBiomeBaseFromId(int id);
|
||||
|
||||
int getMinHeight(World world);
|
||||
|
||||
boolean supportsCustomBiomes();
|
||||
|
||||
int getTrueBiomeBaseId(Object biomeBase);
|
||||
|
||||
Object getTrueBiomeBase(Location location);
|
||||
|
||||
String getTrueBiomeBaseKey(Location location);
|
||||
|
||||
Object getCustomBiomeBaseFor(String mckey);
|
||||
|
||||
Object getCustomBiomeBaseHolderFor(String mckey);
|
||||
|
||||
int getBiomeBaseIdForKey(String key);
|
||||
|
||||
String getKeyForBiomeBase(Object biomeBase);
|
||||
|
||||
Object getBiomeBase(World world, Biome biome);
|
||||
|
||||
Object getBiomeBase(Object registry, Biome biome);
|
||||
|
||||
KList<Biome> getBiomes();
|
||||
|
||||
boolean isBukkit();
|
||||
|
||||
int getBiomeId(Biome biome);
|
||||
|
||||
MCABiomeContainer newBiomeContainer(int min, int max, int[] data);
|
||||
|
||||
MCABiomeContainer newBiomeContainer(int min, int max);
|
||||
|
||||
default World createWorld(WorldCreator c) {
|
||||
WorldLifecycleRequest request = WorldLifecycleRequest.fromCreator(c, false, false, WorldLifecycleCaller.CREATE);
|
||||
return createWorld(c, request);
|
||||
}
|
||||
|
||||
default CompletableFuture<World> createWorldAsync(WorldCreator c) {
|
||||
WorldLifecycleRequest request = WorldLifecycleRequest.fromCreator(c, false, false, WorldLifecycleCaller.CREATE);
|
||||
return createWorldAsync(c, request);
|
||||
}
|
||||
|
||||
default World createWorld(WorldCreator c, WorldLifecycleRequest request) {
|
||||
validateDimensionTypes(c);
|
||||
return WorldLifecycleService.get().createBlocking(request);
|
||||
}
|
||||
|
||||
default CompletableFuture<World> createWorldAsync(WorldCreator c, WorldLifecycleRequest request) {
|
||||
try {
|
||||
validateDimensionTypes(c);
|
||||
return WorldLifecycleService.get().create(request);
|
||||
} catch (Throwable e) {
|
||||
return CompletableFuture.failedFuture(e);
|
||||
}
|
||||
}
|
||||
|
||||
default Object createRuntimeLevelStem(Object registryAccess, ChunkGenerator raw) {
|
||||
throw new UnsupportedOperationException("Active NMS binding does not support runtime LevelStem creation.");
|
||||
}
|
||||
|
||||
int countCustomBiomes();
|
||||
|
||||
default boolean supportsDataPacks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
MCAPaletteAccess createPalette();
|
||||
|
||||
void injectBiomesFromMantle(Chunk e, Mantle<Matter> mantle);
|
||||
|
||||
ItemStack applyCustomNbt(ItemStack itemStack, KMap<String, Object> customNbt) throws IllegalArgumentException;
|
||||
|
||||
void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException;
|
||||
|
||||
Vector3d getBoundingbox(org.bukkit.entity.EntityType entity);
|
||||
|
||||
Entity spawnEntity(Location location, EntityType type, CreatureSpawnEvent.SpawnReason reason);
|
||||
|
||||
Color getBiomeColor(Location location, BiomeColor type);
|
||||
|
||||
default DataVersion getDataVersion() {
|
||||
return DataVersion.V1_19_2;
|
||||
}
|
||||
|
||||
default int getSpawnChunkCount(World world) {
|
||||
return 441;
|
||||
}
|
||||
|
||||
boolean missingDimensionTypes(String... keys);
|
||||
|
||||
default boolean injectBukkit() {
|
||||
return true;
|
||||
}
|
||||
|
||||
KMap<Material, List<BlockProperty>> getBlockProperties();
|
||||
|
||||
private void validateDimensionTypes(WorldCreator c) {
|
||||
if (c.generator() instanceof PlatformChunkGenerator gen
|
||||
&& missingDimensionTypes(gen.getTarget().getDimension().getDimensionTypeKey())) {
|
||||
throw new IllegalStateException("Missing dimension types to create world");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package art.arcane.iris.core.nms;
|
||||
|
||||
import org.bukkit.Server;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
final class MinecraftVersion {
|
||||
private static final Pattern DECORATED_VERSION_PATTERN = Pattern.compile("\\(MC: ([0-9]+(?:\\.[0-9]+){0,2})\\)");
|
||||
|
||||
private final String value;
|
||||
private final int major;
|
||||
private final int minor;
|
||||
|
||||
private MinecraftVersion(String value, int major, int minor) {
|
||||
this.value = value;
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
}
|
||||
|
||||
public static MinecraftVersion detect(Server server) {
|
||||
if (server == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
MinecraftVersion runtimeVersion = fromRuntimeMinecraftVersion(server);
|
||||
if (runtimeVersion != null) {
|
||||
return runtimeVersion;
|
||||
}
|
||||
|
||||
MinecraftVersion decoratedVersion = fromDecoratedVersion(server.getVersion());
|
||||
if (decoratedVersion != null) {
|
||||
return decoratedVersion;
|
||||
}
|
||||
|
||||
return fromBukkitVersion(server.getBukkitVersion());
|
||||
}
|
||||
|
||||
static MinecraftVersion fromRuntimeMinecraftVersion(Server server) {
|
||||
try {
|
||||
Method method = server.getClass().getMethod("getMinecraftVersion");
|
||||
Object value = method.invoke(server);
|
||||
if (value instanceof String version) {
|
||||
return fromVersionToken(version);
|
||||
}
|
||||
} catch (ReflectiveOperationException ignored) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static MinecraftVersion fromDecoratedVersion(String input) {
|
||||
if (input == null || input.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Matcher matcher = DECORATED_VERSION_PATTERN.matcher(input);
|
||||
if (!matcher.find()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fromVersionToken(matcher.group(1));
|
||||
}
|
||||
|
||||
static MinecraftVersion fromBukkitVersion(String input) {
|
||||
if (input == null || input.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String versionToken = input.split("-", 2)[0].trim();
|
||||
return fromVersionToken(versionToken);
|
||||
}
|
||||
|
||||
private static MinecraftVersion fromVersionToken(String input) {
|
||||
if (input == null || input.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String[] parts = input.split("\\.");
|
||||
if (parts.length < 2 || !"1".equals(parts[0])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
int major = Integer.parseInt(parts[1]);
|
||||
int minor = parts.length > 2 ? Integer.parseInt(parts[2]) : 0;
|
||||
return new MinecraftVersion(input, major, minor);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public int major() {
|
||||
return major;
|
||||
}
|
||||
|
||||
public int minor() {
|
||||
return minor;
|
||||
}
|
||||
|
||||
public boolean isAtLeast(int major, int minor) {
|
||||
return this.major > major || (this.major == major && this.minor >= minor);
|
||||
}
|
||||
|
||||
public boolean isNewerThan(int major, int minor) {
|
||||
return this.major > major || (this.major == major && this.minor > minor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package art.arcane.iris.core.nms;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
final class NmsBindingProbeSupport {
|
||||
private NmsBindingProbeSupport() {
|
||||
}
|
||||
|
||||
static List<String> getBindingProbeCodes(String code, boolean disableNms, Collection<String> fallbackCodes) {
|
||||
List<String> probeCodes = new ArrayList<>();
|
||||
if (code == null || code.isBlank()) {
|
||||
return probeCodes;
|
||||
}
|
||||
|
||||
if (!"BUKKIT".equals(code)) {
|
||||
probeCodes.add(code);
|
||||
return probeCodes;
|
||||
}
|
||||
|
||||
if (disableNms || fallbackCodes == null) {
|
||||
return probeCodes;
|
||||
}
|
||||
|
||||
probeCodes.addAll(fallbackCodes);
|
||||
return probeCodes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package art.arcane.iris.core.nms.container;
|
||||
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.function.NastyRunnable;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class AutoClosing implements AutoCloseable {
|
||||
private static final KMap<Thread, AutoClosing> CONTEXTS = new KMap<>();
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
private final NastyRunnable action;
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (closed.getAndSet(true)) return;
|
||||
try {
|
||||
removeContext();
|
||||
action.run();
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void storeContext() {
|
||||
CONTEXTS.put(Thread.currentThread(), this);
|
||||
}
|
||||
|
||||
public void removeContext() {
|
||||
CONTEXTS.values().removeIf(c -> c == this);
|
||||
}
|
||||
|
||||
public static void closeContext() {
|
||||
AutoClosing closing = CONTEXTS.remove(Thread.currentThread());
|
||||
if (closing == null) return;
|
||||
closing.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package art.arcane.iris.core.nms.container;
|
||||
|
||||
public enum BiomeColor {
|
||||
FOG,
|
||||
WATER,
|
||||
WATER_FOG,
|
||||
SKY,
|
||||
FOLIAGE,
|
||||
GRASS
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package art.arcane.iris.core.nms.container;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class BlockPos {
|
||||
private int x;
|
||||
private int y;
|
||||
private int z;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package art.arcane.iris.core.nms.container;
|
||||
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class BlockProperty {
|
||||
private static final Set<Class<?>> NATIVES = Set.of(Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Boolean.class, String.class);
|
||||
private final String name;
|
||||
private final Class<?> type;
|
||||
|
||||
private final Object defaultValue;
|
||||
private final Set<Object> values;
|
||||
private final Function<Object, String> nameFunction;
|
||||
private final Function<Object, Object> jsonFunction;
|
||||
|
||||
public <T extends Comparable<T>> BlockProperty(
|
||||
String name,
|
||||
Class<T> type,
|
||||
T defaultValue,
|
||||
Collection<T> values,
|
||||
Function<T, String> nameFunction
|
||||
) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.defaultValue = defaultValue;
|
||||
this.values = Collections.unmodifiableSet(new TreeSet<>(values));
|
||||
this.nameFunction = (Function<Object, String>) (Object) nameFunction;
|
||||
jsonFunction = NATIVES.contains(type) ? Function.identity() : this.nameFunction::apply;
|
||||
}
|
||||
|
||||
public static <T extends Enum<T>> BlockProperty ofEnum(Class<T> type, String name, T defaultValue) {
|
||||
return new BlockProperty(
|
||||
name,
|
||||
type,
|
||||
defaultValue,
|
||||
Arrays.asList(type.getEnumConstants()),
|
||||
val -> val == null ? "null" : val.name()
|
||||
);
|
||||
}
|
||||
|
||||
public static BlockProperty ofDouble(String name, float defaultValue, float min, float max, boolean exclusiveMin, boolean exclusiveMax) {
|
||||
return new BoundedDouble(
|
||||
name,
|
||||
defaultValue,
|
||||
min,
|
||||
max,
|
||||
exclusiveMin,
|
||||
exclusiveMax,
|
||||
(f) -> String.format("%.2f", f)
|
||||
);
|
||||
}
|
||||
|
||||
public static BlockProperty ofLong(String name, long defaultValue, long min, long max, boolean exclusiveMin, boolean exclusiveMax) {
|
||||
return new BoundedLong(
|
||||
name,
|
||||
defaultValue,
|
||||
min,
|
||||
max,
|
||||
exclusiveMin,
|
||||
exclusiveMax,
|
||||
value -> Long.toString(value)
|
||||
);
|
||||
}
|
||||
|
||||
public static BlockProperty ofBoolean(String name, boolean defaultValue) {
|
||||
return new BlockProperty(
|
||||
name,
|
||||
Boolean.class,
|
||||
defaultValue,
|
||||
List.of(true, false),
|
||||
(b) -> b ? "true" : "false"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return name + "=" + nameFunction.apply(defaultValue) + " [" + String.join(",", names()) + "]";
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String defaultValue() {
|
||||
return nameFunction.apply(defaultValue);
|
||||
}
|
||||
|
||||
public List<String> names() {
|
||||
return values.stream().map(nameFunction).toList();
|
||||
}
|
||||
|
||||
public Object defaultValueAsJson() {
|
||||
return jsonFunction.apply(defaultValue);
|
||||
}
|
||||
|
||||
public JSONArray valuesAsJson() {
|
||||
return new JSONArray(values.stream().map(jsonFunction).toList());
|
||||
}
|
||||
|
||||
public JSONObject buildJson() {
|
||||
var json = new JSONObject();
|
||||
json.put("type", jsonType());
|
||||
json.put("default", defaultValueAsJson());
|
||||
if (!values.isEmpty()) json.put("enum", valuesAsJson());
|
||||
return json;
|
||||
}
|
||||
|
||||
public String jsonType() {
|
||||
if (type == Boolean.class)
|
||||
return "boolean";
|
||||
if (type == Byte.class || type == Short.class || type == Integer.class || type == Long.class)
|
||||
return "integer";
|
||||
if (type == Float.class || type == Double.class)
|
||||
return "number";
|
||||
return "string";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
if (obj == null || obj.getClass() != this.getClass()) return false;
|
||||
var that = (BlockProperty) obj;
|
||||
return Objects.equals(this.name, that.name) &&
|
||||
Objects.equals(this.values, that.values) &&
|
||||
Objects.equals(this.type, that.type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, values, type);
|
||||
}
|
||||
|
||||
private static class BoundedLong extends BlockProperty {
|
||||
private final long min;
|
||||
private final long max;
|
||||
private final boolean exclusiveMin;
|
||||
private final boolean exclusiveMax;
|
||||
|
||||
public BoundedLong(
|
||||
String name,
|
||||
long defaultValue,
|
||||
long min,
|
||||
long max,
|
||||
boolean exclusiveMin,
|
||||
boolean exclusiveMax,
|
||||
Function<Long, String> nameFunction
|
||||
) {
|
||||
super(name, Long.class, defaultValue, List.of(), nameFunction);
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.exclusiveMin = exclusiveMin;
|
||||
this.exclusiveMax = exclusiveMax;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject buildJson() {
|
||||
return super.buildJson()
|
||||
.put("minimum", min)
|
||||
.put("maximum", max)
|
||||
.put("exclusiveMinimum", exclusiveMin)
|
||||
.put("exclusiveMaximum", exclusiveMax);
|
||||
}
|
||||
}
|
||||
|
||||
private static class BoundedDouble extends BlockProperty {
|
||||
private final double min, max;
|
||||
private final boolean exclusiveMin, exclusiveMax;
|
||||
|
||||
public BoundedDouble(
|
||||
String name,
|
||||
double defaultValue,
|
||||
double min,
|
||||
double max,
|
||||
boolean exclusiveMin,
|
||||
boolean exclusiveMax,
|
||||
Function<Double, String> nameFunction
|
||||
) {
|
||||
super(name, Double.class, defaultValue, List.of(), nameFunction);
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.exclusiveMin = exclusiveMin;
|
||||
this.exclusiveMax = exclusiveMax;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject buildJson() {
|
||||
return super.buildJson()
|
||||
.put("minimum", min)
|
||||
.put("maximum", max)
|
||||
.put("exclusiveMinimum", exclusiveMin)
|
||||
.put("exclusiveMaximum", exclusiveMax);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package art.arcane.iris.core.nms.container;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class Pair<A, B> {
|
||||
private A a;
|
||||
private B b;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package art.arcane.iris.core.nms.datapack;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.datapack.v1192.DataFixerV1192;
|
||||
import art.arcane.iris.core.nms.datapack.v1206.DataFixerV1206;
|
||||
import art.arcane.iris.core.nms.datapack.v1213.DataFixerV1213;
|
||||
import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
//https://minecraft.wiki/w/Pack_format
|
||||
@Getter
|
||||
public enum DataVersion {
|
||||
UNSUPPORTED("0.0.0", 0, () -> null),
|
||||
V1_19_2("1.19.2", 10, DataFixerV1192::new),
|
||||
V1_20_5("1.20.6", 41, DataFixerV1206::new),
|
||||
V1_21_3("1.21.3", 57, DataFixerV1213::new),
|
||||
V1_21_11("1.21.11", 75, DataFixerV1217::new);
|
||||
private static final KMap<DataVersion, IDataFixer> cache = new KMap<>();
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final Supplier<IDataFixer> constructor;
|
||||
private final String version;
|
||||
private final int packFormat;
|
||||
|
||||
DataVersion(String version, int packFormat, Supplier<IDataFixer> constructor) {
|
||||
this.constructor = constructor;
|
||||
this.packFormat = packFormat;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public IDataFixer get() {
|
||||
return cache.computeIfAbsent(this, k -> constructor.get());
|
||||
}
|
||||
|
||||
public static IDataFixer getDefault() {
|
||||
DataVersion version = INMS.get().getDataVersion();
|
||||
if (version == null || version == UNSUPPORTED) {
|
||||
DataVersion fallback = getLatest();
|
||||
Iris.warn("Unsupported datapack version mapping detected, falling back to latest fixer: " + fallback.getVersion());
|
||||
return fallback.get();
|
||||
}
|
||||
|
||||
IDataFixer fixer = version.get();
|
||||
if (fixer == null) {
|
||||
DataVersion fallback = getLatest();
|
||||
Iris.warn("Null datapack fixer for " + version.getVersion() + ", falling back to latest fixer: " + fallback.getVersion());
|
||||
return fallback.get();
|
||||
}
|
||||
|
||||
return fixer;
|
||||
}
|
||||
|
||||
public static DataVersion getLatest() {
|
||||
return values()[values().length - 1];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package art.arcane.iris.core.nms.datapack;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimensionTypeOptions;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public interface IDataFixer {
|
||||
default JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) {
|
||||
return json;
|
||||
}
|
||||
|
||||
JSONObject resolve(Dimension dimension, @Nullable IrisDimensionTypeOptions options);
|
||||
|
||||
void fixDimension(Dimension dimension, JSONObject json);
|
||||
|
||||
default JSONObject createDimension(Dimension base, int minY, int height, int logicalHeight, @Nullable IrisDimensionTypeOptions options) {
|
||||
JSONObject obj = resolve(base, options);
|
||||
obj.put("min_y", minY);
|
||||
obj.put("height", height);
|
||||
obj.put("logical_height", logicalHeight);
|
||||
fixDimension(base, obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
enum Dimension {
|
||||
OVERWORLD,
|
||||
NETHER,
|
||||
END
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package art.arcane.iris.core.nms.datapack.v1192;
|
||||
|
||||
import art.arcane.iris.core.nms.datapack.IDataFixer;
|
||||
import art.arcane.iris.engine.object.IrisDimensionTypeOptions;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.*;
|
||||
|
||||
public class DataFixerV1192 implements IDataFixer {
|
||||
private static final Map<Dimension, IrisDimensionTypeOptions> OPTIONS = Map.of(
|
||||
Dimension.OVERWORLD, new IrisDimensionTypeOptions(
|
||||
FALSE,
|
||||
TRUE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
TRUE,
|
||||
TRUE,
|
||||
TRUE,
|
||||
FALSE,
|
||||
1d,
|
||||
0f,
|
||||
null,
|
||||
192,
|
||||
0),
|
||||
Dimension.NETHER, new IrisDimensionTypeOptions(
|
||||
TRUE,
|
||||
FALSE,
|
||||
TRUE,
|
||||
TRUE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
TRUE,
|
||||
8d,
|
||||
0.1f,
|
||||
18000L,
|
||||
null,
|
||||
15),
|
||||
Dimension.END, new IrisDimensionTypeOptions(
|
||||
FALSE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
TRUE,
|
||||
FALSE,
|
||||
FALSE,
|
||||
1d,
|
||||
0f,
|
||||
6000L,
|
||||
null,
|
||||
0)
|
||||
);
|
||||
|
||||
private static final Map<Dimension, String> DIMENSIONS = Map.of(
|
||||
Dimension.OVERWORLD, """
|
||||
{
|
||||
"effects": "minecraft:overworld",
|
||||
"infiniburn": "#minecraft:infiniburn_overworld",
|
||||
"monster_spawn_light_level": {
|
||||
"type": "minecraft:uniform",
|
||||
"value": {
|
||||
"max_inclusive": 7,
|
||||
"min_inclusive": 0
|
||||
}
|
||||
}
|
||||
}""",
|
||||
Dimension.NETHER, """
|
||||
{
|
||||
"effects": "minecraft:the_nether",
|
||||
"infiniburn": "#minecraft:infiniburn_nether",
|
||||
"monster_spawn_light_level": 7,
|
||||
}""",
|
||||
Dimension.END, """
|
||||
{
|
||||
"effects": "minecraft:the_end",
|
||||
"infiniburn": "#minecraft:infiniburn_end",
|
||||
"monster_spawn_light_level": {
|
||||
"type": "minecraft:uniform",
|
||||
"value": {
|
||||
"max_inclusive": 7,
|
||||
"min_inclusive": 0
|
||||
}
|
||||
}
|
||||
}"""
|
||||
);
|
||||
|
||||
@Override
|
||||
public JSONObject resolve(Dimension dimension, @Nullable IrisDimensionTypeOptions options) {
|
||||
return options == null ? OPTIONS.get(dimension).toJson() : options.resolve(OPTIONS.get(dimension)).toJson();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fixDimension(Dimension dimension, JSONObject json) {
|
||||
var missing = new JSONObject(DIMENSIONS.get(dimension));
|
||||
for (String key : missing.keySet()) {
|
||||
if (json.has(key)) continue;
|
||||
json.put(key, missing.get(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package art.arcane.iris.core.nms.datapack.v1206;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.nms.datapack.v1192.DataFixerV1192;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustomSpawn;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustomSpawnType;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class DataFixerV1206 extends DataFixerV1192 {
|
||||
@Override
|
||||
public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) {
|
||||
int spawnRarity = biome.getSpawnRarity();
|
||||
if (spawnRarity > 0) {
|
||||
json.put("creature_spawn_probability", Math.min(spawnRarity/20d, 0.9999999));
|
||||
} else {
|
||||
json.remove("creature_spawn_probability");
|
||||
}
|
||||
|
||||
var spawns = biome.getSpawns();
|
||||
if (spawns != null && spawns.isNotEmpty()) {
|
||||
JSONObject spawners = new JSONObject();
|
||||
KMap<IrisBiomeCustomSpawnType, JSONArray> groups = new KMap<>();
|
||||
|
||||
for (IrisBiomeCustomSpawn i : spawns) {
|
||||
if (i == null) {
|
||||
continue;
|
||||
}
|
||||
EntityType type = i.getType();
|
||||
if (type == null) {
|
||||
Iris.warn("Skipping custom biome spawn with null entity type in biome " + biome.getId());
|
||||
continue;
|
||||
}
|
||||
IrisBiomeCustomSpawnType group = i.getGroup() == null ? IrisBiomeCustomSpawnType.MISC : i.getGroup();
|
||||
JSONArray g = groups.computeIfAbsent(group, (k) -> new JSONArray());
|
||||
JSONObject o = new JSONObject();
|
||||
NamespacedKey key = type.getKey();
|
||||
if (key == null) {
|
||||
Iris.warn("Skipping custom biome spawn with unresolved entity key in biome " + biome.getId());
|
||||
continue;
|
||||
}
|
||||
o.put("type", key.toString());
|
||||
o.put("weight", i.getWeight());
|
||||
o.put("minCount", i.getMinCount());
|
||||
o.put("maxCount", i.getMaxCount());
|
||||
g.put(o);
|
||||
}
|
||||
|
||||
for (IrisBiomeCustomSpawnType i : groups.k()) {
|
||||
spawners.put(i.name().toLowerCase(Locale.ROOT), groups.get(i));
|
||||
}
|
||||
|
||||
json.put("spawners", spawners);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fixDimension(Dimension dimension, JSONObject json) {
|
||||
super.fixDimension(dimension, json);
|
||||
if (!(json.get("monster_spawn_light_level") instanceof JSONObject lightLevel))
|
||||
return;
|
||||
var value = (JSONObject) lightLevel.remove("value");
|
||||
lightLevel.put("max_inclusive", value.get("max_inclusive"));
|
||||
lightLevel.put("min_inclusive", value.get("min_inclusive"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package art.arcane.iris.core.nms.datapack.v1213;
|
||||
|
||||
import art.arcane.iris.core.nms.datapack.v1206.DataFixerV1206;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
public class DataFixerV1213 extends DataFixerV1206 {
|
||||
|
||||
@Override
|
||||
public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) {
|
||||
json = super.fixCustomBiome(biome, json);
|
||||
json.put("carvers", new JSONArray());
|
||||
return json;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package art.arcane.iris.core.nms.datapack.v1217;
|
||||
|
||||
import art.arcane.iris.core.nms.datapack.v1213.DataFixerV1213;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class DataFixerV1217 extends DataFixerV1213 {
|
||||
private static final Map<Dimension, String> DIMENSIONS = Map.of(
|
||||
Dimension.OVERWORLD, """
|
||||
{
|
||||
"ambient_light": 0.0,
|
||||
"has_ender_dragon_fight": false,
|
||||
"attributes": {
|
||||
"minecraft:audio/ambient_sounds": {
|
||||
"mood": {
|
||||
"block_search_extent": 8,
|
||||
"offset": 2.0,
|
||||
"sound": "minecraft:ambient.cave",
|
||||
"tick_delay": 6000
|
||||
}
|
||||
},
|
||||
"minecraft:audio/background_music": {
|
||||
"creative": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.creative"
|
||||
},
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 12000,
|
||||
"sound": "minecraft:music.game"
|
||||
}
|
||||
},
|
||||
"minecraft:visual/cloud_color": "#ccffffff",
|
||||
"minecraft:visual/fog_color": "#c0d8ff",
|
||||
"minecraft:visual/sky_color": "#78a7ff"
|
||||
},
|
||||
"timelines": "#minecraft:in_overworld"
|
||||
}""",
|
||||
Dimension.NETHER, """
|
||||
{
|
||||
"ambient_light": 0.1,
|
||||
"has_ender_dragon_fight": false,
|
||||
"attributes": {
|
||||
"minecraft:gameplay/sky_light_level": 4.0,
|
||||
"minecraft:gameplay/snow_golem_melts": true,
|
||||
"minecraft:visual/fog_end_distance": 96.0,
|
||||
"minecraft:visual/fog_start_distance": 10.0,
|
||||
"minecraft:visual/sky_light_color": "#7a7aff",
|
||||
"minecraft:visual/sky_light_factor": 0.0
|
||||
},
|
||||
"cardinal_light": "nether",
|
||||
"skybox": "none",
|
||||
"timelines": "#minecraft:in_nether"
|
||||
}""",
|
||||
Dimension.END, """
|
||||
{
|
||||
"ambient_light": 0.25,
|
||||
"has_ender_dragon_fight": true,
|
||||
"attributes": {
|
||||
"minecraft:audio/ambient_sounds": {
|
||||
"mood": {
|
||||
"block_search_extent": 8,
|
||||
"offset": 2.0,
|
||||
"sound": "minecraft:ambient.cave",
|
||||
"tick_delay": 6000
|
||||
}
|
||||
},
|
||||
"minecraft:audio/background_music": {
|
||||
"default": {
|
||||
"max_delay": 24000,
|
||||
"min_delay": 6000,
|
||||
"replace_current_music": true,
|
||||
"sound": "minecraft:music.end"
|
||||
}
|
||||
},
|
||||
"minecraft:visual/fog_color": "#181318",
|
||||
"minecraft:visual/sky_color": "#000000",
|
||||
"minecraft:visual/sky_light_color": "#e580ff",
|
||||
"minecraft:visual/sky_light_factor": 0.0
|
||||
},
|
||||
"skybox": "end",
|
||||
"timelines": "#minecraft:in_end"
|
||||
}"""
|
||||
);
|
||||
|
||||
@Override
|
||||
public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) {
|
||||
json = super.fixCustomBiome(biome, json);
|
||||
var effects = json.getJSONObject("effects");
|
||||
var attributes = new JSONObject();
|
||||
|
||||
attributes.put("minecraft:visual/fog_color", effects.remove("fog_color"));
|
||||
attributes.put("minecraft:visual/sky_color", effects.remove("sky_color"));
|
||||
attributes.put("minecraft:visual/water_fog_color", effects.remove("water_fog_color"));
|
||||
|
||||
JSONObject particle = (JSONObject) effects.remove("particle");
|
||||
if (particle != null) {
|
||||
particle.put("particle", particle.remove("options"));
|
||||
attributes.put("minecraft:visual/ambient_particles", new JSONArray()
|
||||
.put(particle));
|
||||
}
|
||||
json.put("attributes", attributes);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fixDimension(Dimension dimension, JSONObject json) {
|
||||
super.fixDimension(dimension, json);
|
||||
|
||||
var attributes = new JSONObject();
|
||||
if ((Boolean) json.remove("ultrawarm")) {
|
||||
attributes.put("minecraft:gameplay/water_evaporates", true);
|
||||
attributes.put("minecraft:gameplay/fast_lava", true);
|
||||
attributes.put("minecraft:gameplay/snow_golem_melts", true);
|
||||
attributes.put("minecraft:visual/default_dripstone_particle", new JSONObject()
|
||||
.put("type", "minecraft:dripping_dripstone_lava"));
|
||||
}
|
||||
|
||||
if ((Boolean) json.remove("bed_works")) {
|
||||
attributes.put("minecraft:gameplay/bed_rule", new JSONObject()
|
||||
.put("can_set_spawn", "always")
|
||||
.put("can_sleep", "when_dark")
|
||||
.put("error_message", new JSONObject()
|
||||
.put("translate", "block.minecraft.bed.no_sleep")));
|
||||
} else {
|
||||
attributes.put("minecraft:gameplay/bed_rule", new JSONObject()
|
||||
.put("can_set_spawn", "never")
|
||||
.put("can_sleep", "never")
|
||||
.put("explodes", true));
|
||||
}
|
||||
|
||||
attributes.put("minecraft:gameplay/respawn_anchor_works", json.remove("respawn_anchor_works"));
|
||||
attributes.put("minecraft:gameplay/piglins_zombify", !(Boolean) json.remove("piglin_safe"));
|
||||
attributes.put("minecraft:gameplay/can_start_raid", json.remove("has_raids"));
|
||||
|
||||
var cloud_height = json.remove("cloud_height");
|
||||
if (cloud_height != null) attributes.put("minecraft:visual/cloud_height", cloud_height);
|
||||
|
||||
boolean natural = (Boolean) json.remove("natural");
|
||||
attributes.put("minecraft:gameplay/nether_portal_spawns_piglin", natural);
|
||||
if (natural != (dimension == Dimension.OVERWORLD)) {
|
||||
attributes.put("minecraft:gameplay/eyeblossom_open", natural);
|
||||
attributes.put("minecraft:gameplay/creaking_active", natural);
|
||||
}
|
||||
|
||||
//json.put("has_fixed_time", json.remove("fixed_time") != null); //TODO investigate
|
||||
json.put("attributes", attributes);
|
||||
|
||||
json.remove("effects");
|
||||
var defaults = new JSONObject(DIMENSIONS.get(dimension));
|
||||
merge(json, defaults);
|
||||
}
|
||||
|
||||
private void merge(JSONObject base, JSONObject override) {
|
||||
for (String key : override.keySet()) {
|
||||
switch (base.opt(key)) {
|
||||
case null -> base.put(key, override.opt(key));
|
||||
case JSONObject base1 when override.opt(key) instanceof JSONObject override1 -> merge(base1, override1);
|
||||
case JSONArray base1 when override.opt(key) instanceof JSONArray override1 -> {
|
||||
for (Object o : override1) {
|
||||
base1.put(o);
|
||||
}
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
-16
@@ -16,18 +16,33 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.nms.v1X;
|
||||
package art.arcane.iris.core.nms.v1X;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.nms.INMSBinding;
|
||||
import com.volmit.iris.util.nbt.mca.palette.MCABiomeContainer;
|
||||
import com.volmit.iris.util.nbt.mca.palette.MCAPaletteAccess;
|
||||
import com.volmit.iris.util.nbt.tag.CompoundTag;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.nms.INMSBinding;
|
||||
import art.arcane.iris.core.nms.container.BiomeColor;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.math.Vector3d;
|
||||
import art.arcane.volmlib.util.nbt.mca.palette.MCABiomeContainer;
|
||||
import art.arcane.volmlib.util.nbt.mca.palette.MCAPaletteAccess;
|
||||
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public class NMSBinding1X implements INMSBinding {
|
||||
private static final boolean supportsCustomHeight = testCustomHeight();
|
||||
@@ -35,33 +50,82 @@ public class NMSBinding1X implements INMSBinding {
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private static boolean testCustomHeight() {
|
||||
try {
|
||||
if(World.class.getDeclaredMethod("getMaxHeight") != null && World.class.getDeclaredMethod("getMinHeight") != null)
|
||||
if (World.class.getDeclaredMethod("getMaxHeight") != null && World.class.getDeclaredMethod("getMinHeight") != null)
|
||||
;
|
||||
{
|
||||
return true;
|
||||
}
|
||||
} catch(Throwable ignored) {
|
||||
} catch (Throwable ignored) {
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasTile(Material material) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasTile(Location l) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag serializeTile(Location location) {
|
||||
public KMap<String, Object> serializeTile(Location location) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deserializeTile(CompoundTag s, Location newPosition) {
|
||||
public void deserializeTile(KMap<String, Object> s, Location newPosition) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void injectBiomesFromMantle(Chunk e, Mantle<Matter> mantle) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack applyCustomNbt(ItemStack itemStack, KMap<String, Object> customNbt) throws IllegalArgumentException {
|
||||
return itemStack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException {
|
||||
|
||||
}
|
||||
|
||||
public Vector3d getBoundingbox() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entity spawnEntity(Location location, EntityType type, CreatureSpawnEvent.SpawnReason reason) {
|
||||
return location.getWorld().spawnEntity(location, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Color getBiomeColor(Location location, BiomeColor type) {
|
||||
return Color.GREEN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean missingDimensionTypes(String... keys) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMap<Material, List<BlockProperty>> getBlockProperties() {
|
||||
KMap<Material, List<BlockProperty>> map = new KMap<>();
|
||||
for (Material m : Material.values()) {
|
||||
if (m.isBlock()) map.put(m, List.of());
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag serializeEntity(Entity location) {
|
||||
return null;
|
||||
@@ -117,6 +181,11 @@ public class NMSBinding1X implements INMSBinding {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBiomeBaseIdForKey(String key) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeyForBiomeBase(Object biomeBase) {
|
||||
return null;
|
||||
@@ -131,14 +200,30 @@ public class NMSBinding1X implements INMSBinding {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<Biome> getBiomes() {
|
||||
KList<Biome> biomes = new KList<>();
|
||||
for (Biome biome : Registry.BIOME) {
|
||||
biomes.add(biome);
|
||||
}
|
||||
return biomes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBukkit() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataVersion getDataVersion() {
|
||||
return DataVersion.UNSUPPORTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBiomeId(Biome biome) {
|
||||
return biome.ordinal();
|
||||
List<Biome> biomes = StreamSupport.stream(Registry.BIOME.spliterator(), false).toList();
|
||||
int index = biomes.indexOf(biome);
|
||||
return Math.max(index, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -161,8 +246,8 @@ public class NMSBinding1X implements INMSBinding {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forceBiomeInto(int x, int y, int z, Object somethingVeryDirty, ChunkGenerator.BiomeGrid chunk) {
|
||||
|
||||
public Vector3d getBoundingbox(org.bukkit.entity.EntityType entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.pack;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class BrokenPackException extends RuntimeException {
|
||||
private final String packName;
|
||||
private final List<String> reasons;
|
||||
|
||||
public BrokenPackException(String packName, List<String> reasons) {
|
||||
super(buildMessage(packName, reasons));
|
||||
this.packName = packName;
|
||||
this.reasons = reasons == null ? new ArrayList<>() : new ArrayList<>(reasons);
|
||||
}
|
||||
|
||||
public String getPackName() {
|
||||
return packName;
|
||||
}
|
||||
|
||||
public List<String> getReasons() {
|
||||
return Collections.unmodifiableList(reasons);
|
||||
}
|
||||
|
||||
private static String buildMessage(String packName, List<String> reasons) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Iris pack '").append(packName).append("' is broken and cannot be used for world or studio creation.");
|
||||
if (reasons != null) {
|
||||
for (String reason : reasons) {
|
||||
if (reason == null || reason.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
sb.append(System.lineSeparator()).append(" - ").append(reason);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
+52
-68
@@ -16,22 +16,22 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.pack;
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.core.loader.ResourceLoader;
|
||||
import com.volmit.iris.core.service.StudioSVC;
|
||||
import com.volmit.iris.engine.object.IrisDimension;
|
||||
import com.volmit.iris.engine.object.IrisWorld;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.exceptions.IrisException;
|
||||
import com.volmit.iris.util.format.Form;
|
||||
import com.volmit.iris.util.io.IO;
|
||||
import com.volmit.iris.util.json.JSONArray;
|
||||
import com.volmit.iris.util.json.JSONObject;
|
||||
import com.volmit.iris.util.plugin.VolmitSender;
|
||||
import com.volmit.iris.util.scheduling.PrecisionStopwatch;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.exceptions.IrisException;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import lombok.Data;
|
||||
import org.bukkit.World;
|
||||
import org.zeroturnaround.zip.commons.FileUtils;
|
||||
@@ -54,8 +54,7 @@ public class IrisPack {
|
||||
* Create an iris pack backed by a data folder
|
||||
* the data folder is assumed to be in the Iris/packs/NAME folder
|
||||
*
|
||||
* @param name
|
||||
* the name
|
||||
* @param name the name
|
||||
*/
|
||||
public IrisPack(String name) {
|
||||
this(packsPack(name));
|
||||
@@ -64,17 +63,16 @@ public class IrisPack {
|
||||
/**
|
||||
* Create an iris pack backed by a data folder
|
||||
*
|
||||
* @param folder
|
||||
* the folder of the pack. Must be a directory
|
||||
* @param folder the folder of the pack. Must be a directory
|
||||
*/
|
||||
public IrisPack(File folder) {
|
||||
this.folder = folder;
|
||||
|
||||
if(!folder.exists()) {
|
||||
if (!folder.exists()) {
|
||||
throw new RuntimeException("Cannot open Pack " + folder.getPath() + " (directory doesnt exist)");
|
||||
}
|
||||
|
||||
if(!folder.isDirectory()) {
|
||||
if (!folder.isDirectory()) {
|
||||
throw new RuntimeException("Cannot open Pack " + folder.getPath() + " (not a directory)");
|
||||
}
|
||||
|
||||
@@ -84,23 +82,20 @@ public class IrisPack {
|
||||
/**
|
||||
* Create a new pack from the input url
|
||||
*
|
||||
* @param sender
|
||||
* the sender
|
||||
* @param url
|
||||
* the url, or name, or really anything see IrisPackRepository.from(String)
|
||||
* @param sender the sender
|
||||
* @param url the url, or name, or really anything see IrisPackRepository.from(String)
|
||||
* @return the iris pack
|
||||
* @throws IrisException
|
||||
* fails
|
||||
* @throws IrisException fails
|
||||
*/
|
||||
public static Future<IrisPack> from(VolmitSender sender, String url) throws IrisException {
|
||||
IrisPackRepository repo = IrisPackRepository.from(url);
|
||||
if(repo == null) {
|
||||
if (repo == null) {
|
||||
throw new IrisException("Null Repo");
|
||||
}
|
||||
|
||||
try {
|
||||
return from(sender, repo);
|
||||
} catch(MalformedURLException e) {
|
||||
} catch (MalformedURLException e) {
|
||||
throw new IrisException("Malformed URL " + e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -108,13 +103,10 @@ public class IrisPack {
|
||||
/**
|
||||
* Create a pack from a repo
|
||||
*
|
||||
* @param sender
|
||||
* the sender
|
||||
* @param repo
|
||||
* the repo
|
||||
* @param sender the sender
|
||||
* @param repo the repo
|
||||
* @return the pack
|
||||
* @throws MalformedURLException
|
||||
* shit happens
|
||||
* @throws MalformedURLException shit happens
|
||||
*/
|
||||
public static Future<IrisPack> from(VolmitSender sender, IrisPackRepository repo) throws MalformedURLException {
|
||||
CompletableFuture<IrisPack> pack = new CompletableFuture<>();
|
||||
@@ -127,16 +119,14 @@ public class IrisPack {
|
||||
/**
|
||||
* Create a blank pack with a given name
|
||||
*
|
||||
* @param name
|
||||
* the name of the pack
|
||||
* @param name the name of the pack
|
||||
* @return the pack
|
||||
* @throws IrisException
|
||||
* if the pack already exists or another error
|
||||
* @throws IrisException if the pack already exists or another error
|
||||
*/
|
||||
public static IrisPack blank(String name) throws IrisException {
|
||||
File f = packsPack(name);
|
||||
|
||||
if(f.exists()) {
|
||||
if (f.exists()) {
|
||||
throw new IrisException("Already exists");
|
||||
}
|
||||
|
||||
@@ -144,10 +134,10 @@ public class IrisPack {
|
||||
fd.getParentFile().mkdirs();
|
||||
try {
|
||||
IO.writeAll(fd, "{\n" +
|
||||
" \"name\": \"" + Form.capitalize(name) + "\",\n" +
|
||||
" \"version\": 1\n" +
|
||||
"}\n");
|
||||
} catch(IOException e) {
|
||||
" \"name\": \"" + Form.capitalize(name) + "\",\n" +
|
||||
" \"version\": 1\n" +
|
||||
"}\n");
|
||||
} catch (IOException e) {
|
||||
throw new IrisException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
@@ -159,8 +149,7 @@ public class IrisPack {
|
||||
/**
|
||||
* Get a packs pack folder for a name. Such that overworld would resolve as Iris/packs/overworld
|
||||
*
|
||||
* @param name
|
||||
* the name
|
||||
* @param name the name
|
||||
* @return the file path
|
||||
*/
|
||||
public static File packsPack(String name) {
|
||||
@@ -170,11 +159,11 @@ public class IrisPack {
|
||||
private static KList<File> collectFiles(File f, String fileExtension) {
|
||||
KList<File> l = new KList<>();
|
||||
|
||||
if(f.isDirectory()) {
|
||||
for(File i : f.listFiles()) {
|
||||
if (f.isDirectory()) {
|
||||
for (File i : f.listFiles()) {
|
||||
l.addAll(collectFiles(i, fileExtension));
|
||||
}
|
||||
} else if(f.getName().endsWith("." + fileExtension)) {
|
||||
} else if (f.getName().endsWith("." + fileExtension)) {
|
||||
l.add(f);
|
||||
}
|
||||
|
||||
@@ -225,13 +214,13 @@ public class IrisPack {
|
||||
p.end();
|
||||
Iris.debug("Building Workspace: " + ws.getPath() + " took " + Form.duration(p.getMilliseconds(), 2));
|
||||
return true;
|
||||
} catch(Throwable e) {
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Iris.warn("Pack invalid: " + ws.getAbsolutePath() + " Re-creating. You may loose some vs-code workspace settings! But not your actual project!");
|
||||
ws.delete();
|
||||
try {
|
||||
IO.writeAll(ws, generateWorkspaceConfig());
|
||||
} catch(IOException e1) {
|
||||
} catch (IOException e1) {
|
||||
Iris.reportError(e1);
|
||||
e1.printStackTrace();
|
||||
}
|
||||
@@ -243,8 +232,7 @@ public class IrisPack {
|
||||
/**
|
||||
* Install this pack into a world
|
||||
*
|
||||
* @param world
|
||||
* the world to install into (world/iris/pack)
|
||||
* @param world the world to install into (world/iris/pack)
|
||||
* @return the installed pack
|
||||
*/
|
||||
public IrisPack install(World world) throws IrisException {
|
||||
@@ -254,8 +242,7 @@ public class IrisPack {
|
||||
/**
|
||||
* Install this pack into a world
|
||||
*
|
||||
* @param world
|
||||
* the world to install into (world/iris/pack)
|
||||
* @param world the world to install into (world/iris/pack)
|
||||
* @return the installed pack
|
||||
*/
|
||||
public IrisPack install(IrisWorld world) throws IrisException {
|
||||
@@ -265,12 +252,11 @@ public class IrisPack {
|
||||
/**
|
||||
* Install this pack into a world
|
||||
*
|
||||
* @param folder
|
||||
* the folder to install this pack into
|
||||
* @param folder the folder to install this pack into
|
||||
* @return the installed pack
|
||||
*/
|
||||
public IrisPack install(File folder) throws IrisException {
|
||||
if(folder.exists()) {
|
||||
if (folder.exists()) {
|
||||
throw new IrisException("Cannot install new pack because the folder " + folder.getName() + " already exists!");
|
||||
}
|
||||
|
||||
@@ -278,7 +264,7 @@ public class IrisPack {
|
||||
|
||||
try {
|
||||
FileUtils.copyDirectory(getFolder(), folder);
|
||||
} catch(IOException e) {
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
@@ -289,20 +275,19 @@ public class IrisPack {
|
||||
* Create a new pack using this pack as a template. The new pack will be renamed & have a renamed dimension
|
||||
* to match it.
|
||||
*
|
||||
* @param newName
|
||||
* the new pack name
|
||||
* @param newName the new pack name
|
||||
* @return the new IrisPack
|
||||
*/
|
||||
public IrisPack install(String newName) throws IrisException {
|
||||
File newPack = packsPack(newName);
|
||||
|
||||
if(newPack.exists()) {
|
||||
if (newPack.exists()) {
|
||||
throw new IrisException("Cannot install new pack because the folder " + newName + " already exists!");
|
||||
}
|
||||
|
||||
try {
|
||||
FileUtils.copyDirectory(getFolder(), newPack);
|
||||
} catch(IOException e) {
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
@@ -314,7 +299,7 @@ public class IrisPack {
|
||||
try {
|
||||
FileUtils.moveFile(from, to);
|
||||
new File(newPack, getWorkspaceFile().getName()).delete();
|
||||
} catch(Throwable e) {
|
||||
} catch (Throwable e) {
|
||||
throw new IrisException(e);
|
||||
}
|
||||
|
||||
@@ -345,8 +330,7 @@ public class IrisPack {
|
||||
/**
|
||||
* Find all files in this pack with the given extension
|
||||
*
|
||||
* @param fileExtension
|
||||
* the extension
|
||||
* @param fileExtension the extension
|
||||
* @return the list of files
|
||||
*/
|
||||
public KList<File> collectFiles(String fileExtension) {
|
||||
@@ -386,8 +370,8 @@ public class IrisPack {
|
||||
JSONArray schemas = new JSONArray();
|
||||
IrisData dm = IrisData.get(getFolder());
|
||||
|
||||
for(ResourceLoader<?> r : dm.getLoaders().v()) {
|
||||
if(r.supportsSchemas()) {
|
||||
for (ResourceLoader<?> r : dm.getLoaders().v()) {
|
||||
if (r.supportsSchemas()) {
|
||||
schemas.put(r.buildSchema());
|
||||
}
|
||||
}
|
||||
+39
-39
@@ -16,15 +16,15 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.pack;
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.service.StudioSVC;
|
||||
import com.volmit.iris.util.format.Form;
|
||||
import com.volmit.iris.util.plugin.VolmitSender;
|
||||
import com.volmit.iris.util.scheduling.jobs.DownloadJob;
|
||||
import com.volmit.iris.util.scheduling.jobs.JobCollection;
|
||||
import com.volmit.iris.util.scheduling.jobs.SingleJob;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.jobs.DownloadJob;
|
||||
import art.arcane.iris.util.common.scheduling.jobs.JobCollection;
|
||||
import art.arcane.iris.util.common.scheduling.jobs.SingleJob;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import org.zeroturnaround.zip.ZipUtil;
|
||||
@@ -45,7 +45,7 @@ public class IrisPackRepository {
|
||||
private String repo = "overworld";
|
||||
|
||||
@Builder.Default
|
||||
private String branch = "master";
|
||||
private String branch = "stable";
|
||||
|
||||
@Builder.Default
|
||||
private String tag = "";
|
||||
@@ -55,34 +55,34 @@ public class IrisPackRepository {
|
||||
*/
|
||||
public static IrisPackRepository from(String g) {
|
||||
// https://github.com/IrisDimensions/overworld
|
||||
if(g.startsWith("https://github.com/")) {
|
||||
if (g.startsWith("https://github.com/")) {
|
||||
String sub = g.split("\\Qgithub.com/\\E")[1];
|
||||
IrisPackRepository r = IrisPackRepository.builder()
|
||||
.user(sub.split("\\Q/\\E")[0])
|
||||
.repo(sub.split("\\Q/\\E")[1]).build();
|
||||
.user(sub.split("\\Q/\\E")[0])
|
||||
.repo(sub.split("\\Q/\\E")[1]).build();
|
||||
|
||||
if(g.contains("/tree/")) {
|
||||
if (g.contains("/tree/")) {
|
||||
r.setBranch(g.split("/tree/")[1]);
|
||||
}
|
||||
|
||||
return r;
|
||||
} else if(g.contains("/")) {
|
||||
} else if (g.contains("/")) {
|
||||
String[] f = g.split("\\Q/\\E");
|
||||
|
||||
if(f.length == 1) {
|
||||
if (f.length == 1) {
|
||||
return from(g);
|
||||
} else if(f.length == 2) {
|
||||
} else if (f.length == 2) {
|
||||
return IrisPackRepository.builder()
|
||||
.user(f[0])
|
||||
.repo(f[1])
|
||||
.build();
|
||||
} else if(f.length >= 3) {
|
||||
.user(f[0])
|
||||
.repo(f[1])
|
||||
.build();
|
||||
} else if (f.length >= 3) {
|
||||
IrisPackRepository r = IrisPackRepository.builder()
|
||||
.user(f[0])
|
||||
.repo(f[1])
|
||||
.build();
|
||||
.user(f[0])
|
||||
.repo(f[1])
|
||||
.build();
|
||||
|
||||
if(f[2].startsWith("#")) {
|
||||
if (f[2].startsWith("#")) {
|
||||
r.setTag(f[2].substring(1));
|
||||
} else {
|
||||
r.setBranch(f[2]);
|
||||
@@ -92,17 +92,17 @@ public class IrisPackRepository {
|
||||
}
|
||||
} else {
|
||||
return IrisPackRepository.builder()
|
||||
.user("IrisDimensions")
|
||||
.repo(g)
|
||||
.branch(g.equals("overworld") ? "stable" : "master")
|
||||
.build();
|
||||
.user("IrisDimensions")
|
||||
.repo(g)
|
||||
.branch("stable")
|
||||
.build();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public String toURL() {
|
||||
if(!tag.trim().isEmpty()) {
|
||||
if (!tag.trim().isEmpty()) {
|
||||
return "https://codeload.github.com/" + user + "/" + repo + "/zip/refs/tags/" + tag;
|
||||
}
|
||||
|
||||
@@ -112,19 +112,19 @@ public class IrisPackRepository {
|
||||
public void install(VolmitSender sender, Runnable whenComplete) throws MalformedURLException {
|
||||
File pack = Iris.instance.getDataFolderNoCreate(StudioSVC.WORKSPACE_NAME, getRepo());
|
||||
|
||||
if(!pack.exists()) {
|
||||
if (!pack.exists()) {
|
||||
File dl = new File(Iris.getTemp(), "dltk-" + UUID.randomUUID() + ".zip");
|
||||
File work = new File(Iris.getTemp(), "extk-" + UUID.randomUUID());
|
||||
new JobCollection(Form.capitalize(getRepo()),
|
||||
new DownloadJob(toURL(), pack),
|
||||
new SingleJob("Extracting", () -> ZipUtil.unpack(dl, work)),
|
||||
new SingleJob("Installing", () -> {
|
||||
try {
|
||||
FileUtils.copyDirectory(work.listFiles()[0], pack);
|
||||
} catch(IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
})).execute(sender, whenComplete);
|
||||
new DownloadJob(toURL(), pack),
|
||||
new SingleJob("Extracting", () -> ZipUtil.unpack(dl, work)),
|
||||
new SingleJob("Installing", () -> {
|
||||
try {
|
||||
FileUtils.copyDirectory(work.listFiles()[0], pack);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
})).execute(sender, whenComplete);
|
||||
} else {
|
||||
sender.sendMessage("Pack already exists!");
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.pack;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class PackValidationRegistry {
|
||||
private static final Map<String, PackValidationResult> RESULTS = new ConcurrentHashMap<>();
|
||||
|
||||
private PackValidationRegistry() {
|
||||
}
|
||||
|
||||
public static void publish(PackValidationResult result) {
|
||||
if (result == null || result.getPackName() == null || result.getPackName().isBlank()) {
|
||||
return;
|
||||
}
|
||||
RESULTS.put(result.getPackName(), result);
|
||||
}
|
||||
|
||||
public static PackValidationResult get(String packName) {
|
||||
if (packName == null || packName.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return RESULTS.get(packName);
|
||||
}
|
||||
|
||||
public static boolean isBroken(String packName) {
|
||||
PackValidationResult result = get(packName);
|
||||
return result != null && !result.isLoadable();
|
||||
}
|
||||
|
||||
public static Map<String, PackValidationResult> snapshot() {
|
||||
return Collections.unmodifiableMap(RESULTS);
|
||||
}
|
||||
|
||||
public static void remove(String packName) {
|
||||
if (packName == null || packName.isBlank()) {
|
||||
return;
|
||||
}
|
||||
RESULTS.remove(packName);
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
RESULTS.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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.pack;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class PackValidationResult {
|
||||
private final String packName;
|
||||
private final List<String> blockingErrors;
|
||||
private final List<String> warnings;
|
||||
private final List<String> removedUnusedFiles;
|
||||
private final long validatedAtMillis;
|
||||
|
||||
public PackValidationResult(String packName,
|
||||
List<String> blockingErrors,
|
||||
List<String> warnings,
|
||||
List<String> removedUnusedFiles,
|
||||
long validatedAtMillis) {
|
||||
this.packName = packName;
|
||||
this.blockingErrors = blockingErrors == null ? new ArrayList<>() : new ArrayList<>(blockingErrors);
|
||||
this.warnings = warnings == null ? new ArrayList<>() : new ArrayList<>(warnings);
|
||||
this.removedUnusedFiles = removedUnusedFiles == null ? new ArrayList<>() : new ArrayList<>(removedUnusedFiles);
|
||||
this.validatedAtMillis = validatedAtMillis;
|
||||
}
|
||||
|
||||
public String getPackName() {
|
||||
return packName;
|
||||
}
|
||||
|
||||
public boolean isLoadable() {
|
||||
return blockingErrors.isEmpty();
|
||||
}
|
||||
|
||||
public List<String> getBlockingErrors() {
|
||||
return Collections.unmodifiableList(blockingErrors);
|
||||
}
|
||||
|
||||
public List<String> getWarnings() {
|
||||
return Collections.unmodifiableList(warnings);
|
||||
}
|
||||
|
||||
public List<String> getRemovedUnusedFiles() {
|
||||
return Collections.unmodifiableList(removedUnusedFiles);
|
||||
}
|
||||
|
||||
public long getValidatedAtMillis() {
|
||||
return validatedAtMillis;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
/*
|
||||
* 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.pack;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class PackValidator {
|
||||
private static final String TRASH_ROOT = ".iris-trash";
|
||||
private static final String DATAPACK_IMPORTS = "datapack-imports";
|
||||
private static final String EXTERNAL_DATAPACKS = "externaldatapacks";
|
||||
private static final String INTERNAL_DATAPACKS = "internaldatapacks";
|
||||
private static final String DATAPACKS_FOLDER = "datapacks";
|
||||
private static final String CACHE_FOLDER = "cache";
|
||||
private static final String OBJECTS_FOLDER = "objects";
|
||||
private static final String DIMENSIONS_FOLDER = "dimensions";
|
||||
private static final List<String> MANAGED_RESOURCE_FOLDERS = List.of(
|
||||
"biomes",
|
||||
"regions",
|
||||
"entities",
|
||||
"spawners",
|
||||
"loot",
|
||||
"generators",
|
||||
"expressions",
|
||||
"markers",
|
||||
"blocks",
|
||||
"mods"
|
||||
);
|
||||
private static final DateTimeFormatter TRASH_STAMP = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
|
||||
|
||||
private PackValidator() {
|
||||
}
|
||||
|
||||
public static PackValidationResult validate(File packFolder) {
|
||||
String packName = packFolder == null ? "<unknown>" : packFolder.getName();
|
||||
List<String> blockingErrors = new ArrayList<>();
|
||||
List<String> warnings = new ArrayList<>();
|
||||
List<String> removedUnusedFiles = new ArrayList<>();
|
||||
long validatedAt = System.currentTimeMillis();
|
||||
|
||||
if (packFolder == null || !packFolder.isDirectory()) {
|
||||
blockingErrors.add("Pack folder does not exist or is not a directory.");
|
||||
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
|
||||
}
|
||||
|
||||
File dimensionsFolder = new File(packFolder, DIMENSIONS_FOLDER);
|
||||
if (!dimensionsFolder.isDirectory()) {
|
||||
blockingErrors.add("Missing dimensions/ folder.");
|
||||
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
|
||||
}
|
||||
|
||||
File[] dimensionFiles = dimensionsFolder.listFiles(f -> f.isFile() && f.getName().endsWith(".json"));
|
||||
if (dimensionFiles == null || dimensionFiles.length == 0) {
|
||||
blockingErrors.add("No dimension JSON files under dimensions/.");
|
||||
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
|
||||
}
|
||||
|
||||
validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
|
||||
|
||||
try {
|
||||
String packTextCorpus = buildPackTextCorpus(packFolder);
|
||||
runUnusedResourceGc(packFolder, packTextCorpus, removedUnusedFiles, warnings);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("PackValidator GC pass failed for pack '" + packName + "'", e);
|
||||
warnings.add("Unused-resource GC pass failed: " + e.getMessage());
|
||||
}
|
||||
|
||||
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
|
||||
}
|
||||
|
||||
private static void validateDimensions(File packFolder, File[] dimensionFiles, List<String> blockingErrors, List<String> warnings) {
|
||||
File regionsFolder = new File(packFolder, "regions");
|
||||
File biomesFolder = new File(packFolder, "biomes");
|
||||
|
||||
for (File dimFile : dimensionFiles) {
|
||||
String dimensionKey = stripExtension(dimFile.getName());
|
||||
JSONObject dimJson;
|
||||
try {
|
||||
dimJson = new JSONObject(Files.readString(dimFile.toPath(), StandardCharsets.UTF_8));
|
||||
} catch (Throwable e) {
|
||||
blockingErrors.add("Dimension '" + dimensionKey + "' has invalid JSON: " + e.getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
JSONArray regionsArray = dimJson.optJSONArray("regions");
|
||||
if (regionsArray == null || regionsArray.length() == 0) {
|
||||
blockingErrors.add("Dimension '" + dimensionKey + "' declares no regions.");
|
||||
continue;
|
||||
}
|
||||
|
||||
int resolvedRegions = 0;
|
||||
for (int i = 0; i < regionsArray.length(); i++) {
|
||||
String regionKey = regionsArray.optString(i, null);
|
||||
if (regionKey == null || regionKey.isBlank()) {
|
||||
warnings.add("Dimension '" + dimensionKey + "' has a blank region entry at index " + i + ".");
|
||||
continue;
|
||||
}
|
||||
File regionFile = new File(regionsFolder, regionKey + ".json");
|
||||
if (!regionFile.isFile()) {
|
||||
blockingErrors.add("Dimension '" + dimensionKey + "' references missing region '" + regionKey + "'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
JSONObject regionJson;
|
||||
try {
|
||||
regionJson = new JSONObject(Files.readString(regionFile.toPath(), StandardCharsets.UTF_8));
|
||||
} catch (Throwable e) {
|
||||
blockingErrors.add("Region '" + regionKey + "' has invalid JSON: " + e.getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
int anyBiome = countBiomeRefs(regionJson, "landBiomes", biomesFolder, regionKey, warnings)
|
||||
+ countBiomeRefs(regionJson, "seaBiomes", biomesFolder, regionKey, warnings)
|
||||
+ countBiomeRefs(regionJson, "shoreBiomes", biomesFolder, regionKey, warnings)
|
||||
+ countBiomeRefs(regionJson, "caveBiomes", biomesFolder, regionKey, warnings);
|
||||
if (anyBiome == 0) {
|
||||
blockingErrors.add("Region '" + regionKey + "' has no resolvable biomes.");
|
||||
}
|
||||
resolvedRegions++;
|
||||
}
|
||||
|
||||
if (resolvedRegions == 0) {
|
||||
blockingErrors.add("Dimension '" + dimensionKey + "' has no resolvable regions.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int countBiomeRefs(JSONObject regionJson, String field, File biomesFolder, String regionKey, List<String> warnings) {
|
||||
JSONArray arr = regionJson.optJSONArray(field);
|
||||
if (arr == null) {
|
||||
return 0;
|
||||
}
|
||||
int resolved = 0;
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
String biomeKey = arr.optString(i, null);
|
||||
if (biomeKey == null || biomeKey.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
File biomeFile = new File(biomesFolder, biomeKey + ".json");
|
||||
if (!biomeFile.isFile()) {
|
||||
warnings.add("Region '" + regionKey + "' references missing biome '" + biomeKey + "' in " + field + ".");
|
||||
continue;
|
||||
}
|
||||
resolved++;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private static String buildPackTextCorpus(File packFolder) {
|
||||
StringBuilder sb = new StringBuilder(1 << 16);
|
||||
try (Stream<Path> stream = Files.walk(packFolder.toPath())) {
|
||||
stream.filter(Files::isRegularFile)
|
||||
.filter(PackValidator::isScannableJsonPath)
|
||||
.forEach(p -> {
|
||||
try {
|
||||
sb.append(Files.readString(p, StandardCharsets.UTF_8));
|
||||
sb.append('\n');
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("PackValidator failed to walk pack folder for corpus scan", e);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static boolean isScannableJsonPath(Path path) {
|
||||
String name = path.getFileName().toString();
|
||||
if (!name.endsWith(".json")) {
|
||||
return false;
|
||||
}
|
||||
String str = path.toString().replace(File.separatorChar, '/');
|
||||
if (str.contains("/" + TRASH_ROOT + "/")) {
|
||||
return false;
|
||||
}
|
||||
if (str.contains("/" + DATAPACK_IMPORTS + "/")) {
|
||||
return false;
|
||||
}
|
||||
if (str.contains("/" + EXTERNAL_DATAPACKS + "/")) {
|
||||
return false;
|
||||
}
|
||||
if (str.contains("/" + INTERNAL_DATAPACKS + "/")) {
|
||||
return false;
|
||||
}
|
||||
if (str.contains("/" + DATAPACKS_FOLDER + "/")) {
|
||||
return false;
|
||||
}
|
||||
if (str.contains("/" + CACHE_FOLDER + "/")) {
|
||||
return false;
|
||||
}
|
||||
if (str.contains("/" + OBJECTS_FOLDER + "/")) {
|
||||
return false;
|
||||
}
|
||||
if (str.contains("/.iris/")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void runUnusedResourceGc(File packFolder, String corpus, List<String> removedUnusedFiles, List<String> warnings) {
|
||||
if (corpus == null || corpus.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
File trashRoot = new File(packFolder, TRASH_ROOT + File.separator + LocalDateTime.now().format(TRASH_STAMP));
|
||||
Set<File> scheduledForTrash = new LinkedHashSet<>();
|
||||
|
||||
for (String folderName : MANAGED_RESOURCE_FOLDERS) {
|
||||
File resourceFolder = new File(packFolder, folderName);
|
||||
if (!resourceFolder.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<File> files = listJsonRecursive(resourceFolder);
|
||||
for (File resourceFile : files) {
|
||||
String key = deriveKey(resourceFolder, resourceFile);
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (isReferenced(corpus, key)) {
|
||||
continue;
|
||||
}
|
||||
scheduledForTrash.add(resourceFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (scheduledForTrash.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (File file : scheduledForTrash) {
|
||||
try {
|
||||
Path src = file.toPath();
|
||||
Path relative = packFolder.toPath().relativize(src);
|
||||
Path dest = trashRoot.toPath().resolve(relative);
|
||||
Files.createDirectories(dest.getParent());
|
||||
Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
|
||||
removedUnusedFiles.add(relative.toString().replace(File.separatorChar, '/'));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("PackValidator failed to move unused file " + file.getPath() + " to trash", e);
|
||||
warnings.add("Failed to quarantine unused file " + file.getName() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isReferenced(String corpus, String key) {
|
||||
String needleQuoted = "\"" + key + "\"";
|
||||
if (corpus.contains(needleQuoted)) {
|
||||
return true;
|
||||
}
|
||||
int slash = key.indexOf('/');
|
||||
if (slash > 0) {
|
||||
String tail = key.substring(slash + 1);
|
||||
if (!tail.isBlank() && corpus.contains("\"" + tail + "\"")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<File> listJsonRecursive(File root) {
|
||||
List<File> out = new ArrayList<>();
|
||||
try (Stream<Path> stream = Files.walk(root.toPath())) {
|
||||
stream.filter(Files::isRegularFile)
|
||||
.filter(p -> p.getFileName().toString().endsWith(".json"))
|
||||
.forEach(p -> out.add(p.toFile()));
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String deriveKey(File resourceFolder, File resourceFile) {
|
||||
Path relative = resourceFolder.toPath().relativize(resourceFile.toPath());
|
||||
String str = relative.toString().replace(File.separatorChar, '/');
|
||||
if (!str.endsWith(".json")) {
|
||||
return null;
|
||||
}
|
||||
return str.substring(0, str.length() - ".json".length());
|
||||
}
|
||||
|
||||
private static String stripExtension(String name) {
|
||||
int dot = name.lastIndexOf('.');
|
||||
return dot <= 0 ? name : name.substring(0, dot);
|
||||
}
|
||||
|
||||
public static int restoreTrash(File packFolder) {
|
||||
if (packFolder == null || !packFolder.isDirectory()) {
|
||||
return 0;
|
||||
}
|
||||
File trashRoot = new File(packFolder, TRASH_ROOT);
|
||||
if (!trashRoot.isDirectory()) {
|
||||
return 0;
|
||||
}
|
||||
File[] dumps = trashRoot.listFiles(File::isDirectory);
|
||||
if (dumps == null || dumps.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
Arrays.sort(dumps, Comparator.comparing(File::getName));
|
||||
File latestDump = dumps[dumps.length - 1];
|
||||
int restored = 0;
|
||||
try (Stream<Path> stream = Files.walk(latestDump.toPath())) {
|
||||
List<Path> files = stream.filter(Files::isRegularFile).toList();
|
||||
for (Path src : files) {
|
||||
Path relative = latestDump.toPath().relativize(src);
|
||||
Path dest = packFolder.toPath().resolve(relative);
|
||||
Files.createDirectories(dest.getParent());
|
||||
Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
|
||||
restored++;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("PackValidator failed to restore trash for pack " + packFolder.getName(), e);
|
||||
}
|
||||
deleteFolderQuiet(latestDump);
|
||||
return restored;
|
||||
}
|
||||
|
||||
private static void deleteFolderQuiet(File folder) {
|
||||
if (folder == null || !folder.exists()) {
|
||||
return;
|
||||
}
|
||||
try (Stream<Path> stream = Files.walk(folder.toPath())) {
|
||||
stream.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.forEach(File::delete);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static Set<String> listReferencedKeysFromCorpus(String corpus) {
|
||||
Set<String> keys = new HashSet<>();
|
||||
if (corpus == null) {
|
||||
return keys;
|
||||
}
|
||||
int i = 0;
|
||||
while (i < corpus.length()) {
|
||||
int start = corpus.indexOf('"', i);
|
||||
if (start < 0) {
|
||||
break;
|
||||
}
|
||||
int end = corpus.indexOf('"', start + 1);
|
||||
if (end < 0) {
|
||||
break;
|
||||
}
|
||||
keys.add(corpus.substring(start + 1, end));
|
||||
i = end + 1;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user