mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Compare commits
1633
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b18681a3e2 | ||
|
|
1e1eaa96aa | ||
|
|
dfb3748059 | ||
|
|
c14a972ab8 | ||
|
|
9a1e4b91d1 | ||
|
|
e3c6a32968 | ||
|
|
ef26f3f443 | ||
|
|
7a099cbd5e | ||
|
|
e832239730 | ||
|
|
bc9b7e86d7 | ||
|
|
3f57925443 | ||
|
|
251b0e250d | ||
|
|
29a31a69e0 | ||
|
|
bf12f30d26 | ||
|
|
577f924133 | ||
|
|
e4cdf2dcda | ||
|
|
5ba95388f8 | ||
|
|
45407a7e3f | ||
|
|
e33e6a632a | ||
|
|
62e61970f8 | ||
|
|
4de1aea8b9 | ||
|
|
0782bbf0b4 | ||
|
|
98e51cba8d | ||
|
|
c39a6403bc | ||
|
|
425d305d0b | ||
|
|
114cc68a08 | ||
|
|
3551614088 | ||
|
|
355d7f4132 | ||
|
|
00474252b4 | ||
|
|
cab35996f8 | ||
|
|
2451c89cee | ||
|
|
8b9d1fb2fb | ||
|
|
e7c6b6e08e | ||
|
|
a4e22006e0 | ||
|
|
85e12caf9b | ||
|
|
2fb1dc4fd6 | ||
|
|
a31d494f45 | ||
|
|
422618282e | ||
|
|
9775a50b26 | ||
|
|
290412b62a | ||
|
|
71e3af727f | ||
|
|
c3bc0f5d18 | ||
|
|
9dcaed5315 | ||
|
|
fd146de1ad | ||
|
|
486c6eeae9 | ||
|
|
6c6dba0232 | ||
|
|
f83cf71b11 | ||
|
|
45b9ff8e11 | ||
|
|
79af11c435 | ||
|
|
1be563bd28 | ||
|
|
e030ac3be5 | ||
|
|
9d22a18a3a | ||
|
|
e5b7cb97a8 | ||
|
|
bdb13d1647 | ||
|
|
42798c6340 | ||
|
|
c75b0e01af | ||
|
|
03c645b66f | ||
|
|
c332c86849 | ||
|
|
c75cfd387e | ||
|
|
7841efed54 | ||
|
|
89dc5d0447 | ||
|
|
5c84c6b8d8 | ||
|
|
0bdacc6fed | ||
|
|
6e3e1f3c64 | ||
|
|
c83d6964f1 | ||
|
|
b225c0b5be | ||
|
|
1913baed17 | ||
|
|
031d4ade2c | ||
|
|
9b9ca8de34 | ||
|
|
266fe0d71c | ||
|
|
d3245aa1c2 | ||
|
|
894cef73a6 | ||
|
|
0dcd1900be | ||
|
|
151677f92f | ||
|
|
083e689864 | ||
|
|
319f8aed47 | ||
|
|
0324b0c467 | ||
|
|
88008a7fd2 | ||
|
|
c8fa5d9d8b | ||
|
|
bbf14a7967 | ||
|
|
b4440463a5 | ||
|
|
04103bd15a | ||
|
|
bb967a673e | ||
|
|
634e6ffe57 | ||
|
|
be621c0074 | ||
|
|
2415825bb7 | ||
|
|
364d127658 | ||
|
|
48a81a3d2c | ||
|
|
763f9df2c2 | ||
|
|
21d6c2c9f9 | ||
|
|
28c715ff51 | ||
|
|
e2761af959 | ||
|
|
95323150c0 | ||
|
|
eb43cc44fa | ||
|
|
bfaacb4d38 | ||
|
|
af42c69824 | ||
|
|
a28614c04e | ||
|
|
0d2e8a45ff | ||
|
|
34c2e4e560 | ||
|
|
f3b6a0ec1b | ||
|
|
e5e3ae112d | ||
|
|
9fdb5615d6 | ||
|
|
3425c05e8b | ||
|
|
fb4585b62d | ||
|
|
e69140e752 | ||
|
|
6b450fdea4 | ||
|
|
fca4a58388 | ||
|
|
cb85a09c0a | ||
|
|
df717cf151 | ||
|
|
5570923d30 | ||
|
|
d093ff0e41 | ||
|
|
03b0f6e801 | ||
|
|
04471678b2 | ||
|
|
f916c81bc0 | ||
|
|
2b124a2a25 | ||
|
|
62f6a32362 | ||
|
|
c49a9ddba8 | ||
|
|
d454f25f4d | ||
|
|
723426177c | ||
|
|
0bfb4d6718 | ||
|
|
046b389925 | ||
|
|
fda24db789 | ||
|
|
2c66367606 | ||
|
|
9a1c3546ba | ||
|
|
0efe919ff4 | ||
|
|
ce431b524e | ||
|
|
2967efbf35 | ||
|
|
b3c3a0b183 | ||
|
|
420148fcc7 | ||
|
|
4c989fe1c0 | ||
|
|
1a2c8a8845 | ||
|
|
606aa2e704 | ||
|
|
5baea8dd5b | ||
|
|
75353d43c7 | ||
|
|
3f9759b05f | ||
|
|
b9b969f1f9 | ||
|
|
1e3831263c | ||
|
|
ebaa616536 | ||
|
|
79690edaaf | ||
|
|
6276bb7327 | ||
|
|
587e0fdee0 | ||
|
|
47f77df0e8 | ||
|
|
534c844a7d | ||
|
|
b7e7f64bcf | ||
|
|
20b9b3dce3 | ||
|
|
a68cad5893 | ||
|
|
3ac441484a | ||
|
|
c27a233a50 | ||
|
|
23bad0e1a2 | ||
|
|
dde3c149ad | ||
|
|
80155cef93 | ||
|
|
057d51f211 | ||
|
|
23316d2d5e | ||
|
|
d55c77572b | ||
|
|
942545dcb9 | ||
|
|
17c927acdd | ||
|
|
13263f1a03 | ||
|
|
3a0e4557b7 | ||
|
|
f8cbbde683 | ||
|
|
3f1448dc33 | ||
|
|
80843114fc | ||
|
|
b17fe811f3 | ||
|
|
27d511d99a | ||
|
|
c53e9f8261 | ||
|
|
064ec0cae9 | ||
|
|
14cfe3ed68 | ||
|
|
645edcff5c | ||
|
|
50894b90ce | ||
|
|
f2887632fe | ||
|
|
d10d70a894 | ||
|
|
5a5e8a8dc8 | ||
|
|
965606bbcc | ||
|
|
b1e1fbc15e | ||
|
|
73d640943c | ||
|
|
20ad49b9d2 | ||
|
|
56b5f4dd0b | ||
|
|
8247f167cf | ||
|
|
6549b40797 | ||
|
|
30113c4133 | ||
|
|
c1bcdd0a9d | ||
|
|
366033a535 | ||
|
|
a771fa6856 | ||
|
|
a368d42558 | ||
|
|
0510c108be | ||
|
|
e4ca7d4e70 | ||
|
|
fc3a26dd9e | ||
|
|
657aaeaccf | ||
|
|
b25937f986 | ||
|
|
5053a644c7 | ||
|
|
fd66b41bad | ||
|
|
a4fe527d65 | ||
|
|
e92c0efe09 | ||
|
|
0a791bb8bf | ||
|
|
0b543bf7ee | ||
|
|
0d464aabf3 | ||
|
|
516ef71c9c | ||
|
|
df34f328e7 | ||
|
|
88bd8feeeb | ||
|
|
e63fd9c448 | ||
|
|
1c577fc580 | ||
|
|
665b201e41 | ||
|
|
333b766d2d | ||
|
|
f6a7ad7472 | ||
|
|
d23d6d5c9b | ||
|
|
185e2fa4ae | ||
|
|
3ec5fab70b | ||
|
|
479b94d6a2 | ||
|
|
13bce7aa42 | ||
|
|
8848167c22 | ||
|
|
f4a76dfa85 | ||
|
|
4b497a7d20 | ||
|
|
6945cbfd9b | ||
|
|
d1c73025a0 | ||
|
|
896e58e212 | ||
|
|
639786537b | ||
|
|
1ad3a25cb6 | ||
|
|
507e241b2c | ||
|
|
3d96a8616a | ||
|
|
0c317929f8 | ||
|
|
4c4153debd | ||
|
|
124bcd9372 | ||
|
|
00f5a16c20 | ||
|
|
b57a2e2fc8 | ||
|
|
5b1b636539 | ||
|
|
2585b2105a | ||
|
|
676372efab | ||
|
|
8a256e3226 | ||
|
|
2c6792977f | ||
|
|
915cc1a2f0 | ||
|
|
48afa0d93f | ||
|
|
15bc52e9b5 | ||
|
|
9103796845 | ||
|
|
27799039d1 | ||
|
|
fd8c55076e | ||
|
|
95d8523d1d | ||
|
|
ecec18509c | ||
|
|
1ca943ab51 | ||
|
|
1a561da2cd | ||
|
|
2b92fbf825 | ||
|
|
f398c55091 | ||
|
|
bdaa84975a | ||
|
|
07cc76614f | ||
|
|
2797a5bf22 | ||
|
|
37b01b73c4 | ||
|
|
e20bb86f40 | ||
|
|
fe5651f854 | ||
|
|
dce7af1955 | ||
|
|
770240e4f7 | ||
|
|
fe684083e7 | ||
|
|
34092da6be | ||
|
|
f43df6c232 | ||
|
|
8eaafa3c69 | ||
|
|
5cacf6d435 | ||
|
|
fec655def4 | ||
|
|
41b2b53ac8 | ||
|
|
4adef4576f | ||
|
|
03746468ec | ||
|
|
a1cef36f10 | ||
|
|
7691d2df53 | ||
|
|
fc0fdf4ce4 | ||
|
|
ef2e1b8f58 | ||
|
|
8297e49ed2 | ||
|
|
4568b3fff1 | ||
|
|
33638eadd7 | ||
|
|
a7baa1c1b9 | ||
|
|
d196ffe853 | ||
|
|
4535bf3eed | ||
|
|
59f33bd921 | ||
|
|
57072d5bcd | ||
|
|
32beca236c | ||
|
|
1586b4bd1a | ||
|
|
14b6280668 | ||
|
|
13e02ecd2f | ||
|
|
c59592e448 | ||
|
|
365205ad0a | ||
|
|
12b97b7994 | ||
|
|
8a189195ad | ||
|
|
d9860782ef | ||
|
|
623a02025c | ||
|
|
82290a3090 | ||
|
|
dfae45663a | ||
|
|
998a5c9f5f | ||
|
|
ebfe278b3b | ||
|
|
506787f51a | ||
|
|
c40e1cf152 | ||
|
|
68b842d5dc | ||
|
|
42b8db4856 | ||
|
|
dda5ea397e | ||
|
|
967c082372 | ||
|
|
ce8c4ff578 | ||
|
|
70d355621c | ||
|
|
a66cba6418 | ||
|
|
cd217c05f9 | ||
|
|
aaccbacf32 | ||
|
|
10b2363226 | ||
|
|
a8217b42e9 | ||
|
|
324cf9e095 | ||
|
|
0afbfcb7a2 | ||
|
|
7ad4909518 | ||
|
|
74741ac83e | ||
|
|
d5a55ccfcf | ||
|
|
de25b7d7ab | ||
|
|
9e1e202f15 | ||
|
|
f6f06c1ab9 | ||
|
|
d76dadec30 | ||
|
|
1d194e880b | ||
|
|
ad41b6795c | ||
|
|
0193f75daa | ||
|
|
e8dd4e1127 | ||
|
|
699247b1de | ||
|
|
55b7460b9a | ||
|
|
45230c0689 | ||
|
|
4dea984d77 | ||
|
|
cd05fd1bca | ||
|
|
e42ab6fae6 | ||
|
|
402bcb04fe | ||
|
|
6111b5670a | ||
|
|
0a7a531646 | ||
|
|
d1fec88769 | ||
|
|
848e056bb8 | ||
|
|
6b64624233 | ||
|
|
d1cc83a40b | ||
|
|
927c48f8c7 | ||
|
|
29ff3f01a7 | ||
|
|
a8e46e3590 | ||
|
|
75e8c22268 | ||
|
|
f777ea6b18 | ||
|
|
8fb627f9c1 | ||
|
|
3cfbcbf8ed | ||
|
|
79e80bef33 | ||
|
|
796d81e58b | ||
|
|
b0a5d97e9c | ||
|
|
ad6732812e | ||
|
|
28c4941941 | ||
|
|
d452f10fd9 | ||
|
|
f482b8ef7e | ||
|
|
e2f6061f98 | ||
|
|
b98a341dc7 | ||
|
|
261a850af4 | ||
|
|
516537db72 | ||
|
|
921a98f210 | ||
|
|
2bac948edb | ||
|
|
26d9115904 | ||
|
|
763ff90a61 | ||
|
|
f32094a404 | ||
|
|
6b24105640 | ||
|
|
a3d7269a99 | ||
|
|
e1196b0364 | ||
|
|
4c462ba78a | ||
|
|
65d4abe5c0 | ||
|
|
5142c048c7 | ||
|
|
88c87175b3 | ||
|
|
56a0c75a39 | ||
|
|
704c94aab4 | ||
|
|
b5d08b6203 | ||
|
|
b584cb4193 | ||
|
|
af3035088a | ||
|
|
df888e5516 | ||
|
|
ca10edef0c | ||
|
|
986c7f12cd | ||
|
|
6c5e991051 | ||
|
|
1a834ed6f6 | ||
|
|
c9929e0880 | ||
|
|
3063801a3a | ||
|
|
13002b04df | ||
|
|
1c9f9d92bb | ||
|
|
4568f0b4b6 | ||
|
|
1c08eb23b2 | ||
|
|
7e224325f3 | ||
|
|
a1ff3b2c55 | ||
|
|
abfc122171 | ||
|
|
6a68884ce9 | ||
|
|
c5a2a57822 | ||
|
|
58d24ca55e | ||
|
|
c6c02a9040 | ||
|
|
a530a03459 | ||
|
|
a82b8f4a9c | ||
|
|
cf2e7df298 | ||
|
|
d20b3af9b9 | ||
|
|
01d98d2141 | ||
|
|
febe930db0 | ||
|
|
5c832ba128 | ||
|
|
03f9e233da | ||
|
|
11efae428f | ||
|
|
1a6fff35e8 | ||
|
|
540a416bc9 | ||
|
|
7eb6e87a81 | ||
|
|
9ff1c502ef | ||
|
|
08c6f7ddf9 | ||
|
|
4bc1913f03 | ||
|
|
821cc027db | ||
|
|
1968ef2f2c | ||
|
|
ce29b70618 | ||
|
|
3d128b70a7 | ||
|
|
23fad24fb7 | ||
|
|
6df718e6ca | ||
|
|
787c728060 | ||
|
|
88bbce82fe | ||
|
|
e6a8351e57 | ||
|
|
b82472d521 | ||
|
|
8c05f1bf1d | ||
|
|
6a5616abae | ||
|
|
6096c39192 | ||
|
|
b42d0e4710 | ||
|
|
dcb3306197 | ||
|
|
9a231b2bcf | ||
|
|
aa706d027b | ||
|
|
568fb07f66 | ||
|
|
e7d89cadd8 | ||
|
|
cddd6b16d6 | ||
|
|
b2ac4e7f93 | ||
|
|
c2bb32459f | ||
|
|
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 |
@@ -27,19 +27,40 @@ body:
|
||||
placeholder: The code to place a is missing b and c...
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Which server platform is Iris running on? Pick the loader or server software, not the launcher.
|
||||
options:
|
||||
- Paper
|
||||
- Purpur
|
||||
- Leaf
|
||||
- Canvas
|
||||
- Folia
|
||||
- Spigot / CraftBukkit
|
||||
- Fabric
|
||||
- Forge
|
||||
- NeoForge
|
||||
- Other (describe under Problem)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: mcversion
|
||||
attributes:
|
||||
label: Minecraft Version
|
||||
description: What version of Minecraft is the server on?
|
||||
description: What version of Minecraft is the server on? Iris 4.x targets 26.2 only; older versions are not supported.
|
||||
options:
|
||||
- 1.14.X
|
||||
- 1.15.X
|
||||
- 1.16.X
|
||||
- 1.17
|
||||
- 1.17.1
|
||||
- 1.18
|
||||
- 1.19
|
||||
- '26.2'
|
||||
- Other (unsupported)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: loaderversion
|
||||
attributes:
|
||||
label: Platform / Loader Version
|
||||
description: Exact server or loader build (see console). For example "Paper 26.2-60", "Fabric Loader 0.19.3", "NeoForge 26.2.0.12-beta".
|
||||
placeholder: DO NOT SAY "LATEST"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
||||
+67
-1
@@ -1,5 +1,6 @@
|
||||
|
||||
build/
|
||||
libs/
|
||||
|
||||
.gradle/
|
||||
|
||||
@@ -9,4 +10,69 @@ build/
|
||||
|
||||
collection/
|
||||
|
||||
src/main/java/com/volmit/iris/util/uniques/
|
||||
out/
|
||||
tools/simd-bench/out/
|
||||
tools/simd-bench/simd-bench.jar
|
||||
|
||||
DataPackExamples/
|
||||
|
||||
TreeGenStuff/
|
||||
dist/
|
||||
core/plugins/
|
||||
adapters/bukkit/plugin/plugins/
|
||||
|
||||
# Dev-run artifacts. `run/` is a loom/ForgeGradle/ModDevGradle working directory and `logs/` is
|
||||
# server output; neither is ever source. No source directory in this repo is named run or logs.
|
||||
run/
|
||||
logs/
|
||||
*.log
|
||||
*.log.gz
|
||||
crash-reports/
|
||||
hs_err_pid*.log
|
||||
replay_pid*.log
|
||||
|
||||
.codegraph/
|
||||
|
||||
plans/
|
||||
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
local.properties
|
||||
*.jks
|
||||
*.p12
|
||||
*.pfx
|
||||
*.pem
|
||||
*.key
|
||||
*.hprof
|
||||
credentials.json
|
||||
service-account*.json
|
||||
|
||||
# docs/ is hand-written and tracked (flat numbered product docs). Nothing under it is
|
||||
# generated, so there is nothing to ignore here.
|
||||
|
||||
.qa/
|
||||
.repro/
|
||||
.perf/
|
||||
.release-gate/
|
||||
|
||||
# Throwaway worktree copies used by the API / PlaceholderAPI rebuild lanes. Generated, never source.
|
||||
.apiwt/
|
||||
.papiwt/
|
||||
|
||||
__pycache__/
|
||||
|
||||
# Doc authoring scratch
|
||||
.docs-wip/
|
||||
|
||||
AGENTS.md
|
||||
|
||||
# Local build tooling, not part of the project
|
||||
build-lt.sh
|
||||
BUILDS/
|
||||
.run/
|
||||
|
||||
# Version-content compat lane worktree. Generated, never source.
|
||||
.compatwt/
|
||||
|
||||
.worktrees/
|
||||
|
||||
@@ -1,77 +1,320 @@
|
||||
# Iris
|
||||
|
||||
The master branch is for the latest version of minecraft.
|
||||
Iris is a world generation engine for Minecraft servers and mod loaders. It generates terrain,
|
||||
biomes, caves, structures, objects, and entities from editable JSON packs, with a full in-game
|
||||
studio authoring workflow. The same engine runs as a Bukkit-family plugin and as a Fabric, Forge,
|
||||
or NeoForge server mod. Cross-platform generation targets deterministic parity for identical
|
||||
artifacts, pack bytes, seeds, and test areas. The master branch targets Minecraft 26.2.
|
||||
|
||||
# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://docs.volmit.com/iris/) **|** [Git](https://github.com/IrisDimensions)
|
||||
# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://github.com/VolmitSoftware/docs/blob/master/iris/00-overview.md) **|** [Git](https://github.com/IrisDimensions)
|
||||
|
||||
# Building
|
||||
Authoritative docs live in the central [VolmitSoftware/docs](https://github.com/VolmitSoftware/docs/tree/master/iris) repository, which is the source for the hosted wiki.
|
||||
|
||||
Building Iris is fairly simple, though you will need to setup a few things if your system has never been used for java
|
||||
development.
|
||||
Consider supporting development by buying Iris on Spigot.
|
||||
|
||||
Consider supporting our development by buying Iris on spigot! We work hard to make Iris the best it can be for everyone.
|
||||
## Language and localization
|
||||
|
||||
### Command Line Builds
|
||||
Canonical English is defined in the typed Java catalogs under `core/src/main/java/art/arcane/iris/localization`, next to the command, Studio, runtime, and UI surfaces that use it. Iris does not ship an English server translation file; the required Minecraft client asset remains at `assets/irisworldgen/lang/en_us.json`. Complete server bundles and matching client assets are included for German, Spanish, Finnish, French, Hebrew, Italian, Japanese, Korean, Lithuanian, Dutch, Polish, Portuguese, Russian, Turkish, Vietnamese, Simplified Chinese, and Traditional Chinese. Set `language` in Iris settings to select one. A JSON file at `languages/overrides/<locale>.json` can override only selected server messages; omitted entries resolve from the bundle and then code-owned English.
|
||||
|
||||
1. Install [Java JDK 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
|
||||
2. Set the JDK installation path to `JAVA_HOME` as an environment variable.
|
||||
* Windows
|
||||
1. Start > Type `env` and press Enter
|
||||
2. Advanced > Environment Variables
|
||||
3. Under System Variables, click `New...`
|
||||
4. Variable Name: `JAVA_HOME`
|
||||
5. Variable Value: `C:\Program Files\Java\jdk-17.0.1` (verify this exists after installing java don't just copy
|
||||
the example text)
|
||||
* MacOS
|
||||
1. Run `/usr/libexec/java_home -V` and look for Java 17
|
||||
2. Run `sudo nano ~/.zshenv`
|
||||
3. Add `export JAVA_HOME=$(/usr/libexec/java_home)` as a new line
|
||||
4. Use `CTRL + X`, then Press `Y`, Then `ENTER`
|
||||
5. Quit & Reopen Terminal and verify with `echo $JAVA_HOME`. It should print a directory
|
||||
3. If this is your first time building Iris for MC 1.18+ run `gradlew setup` inside the root Iris project folder.
|
||||
Otherwise, skip this step. Grab a coffee, this may take up to 5 minutes depending on your cpu & internet connection.
|
||||
4. Once the project has setup, run `gradlew iris`
|
||||
5. The Iris jar will be placed in `Iris/build/Iris-XXX-XXX.jar` Enjoy! Consider supporting us by buying it on spigot!
|
||||
## Platforms
|
||||
|
||||
### IDE Builds (for development)
|
||||
| Platform | Artifact | Minecraft | Notes |
|
||||
|---|---|---|---|
|
||||
| Paper / Purpur / Leaf / Canvas | plugin jar | 26.1.2 - 26.2 | Full feature set |
|
||||
| Folia | plugin jar | 26.1.2 - 26.2 | Region-safe scheduling throughout |
|
||||
| Spigot / CraftBukkit | plugin jar | 26.1.2 - 26.2 | Managed `iris:*` creation and generation; exact vanilla-slot `/iris replace` is unavailable |
|
||||
| Fabric | mod jar | 26.2 | Server worldgen + client HUD; requires Fabric Loader 0.19.3+ |
|
||||
| Forge | mod jar | 26.2 | Server worldgen + client HUD; current target is Forge 26.2-65.1.1 |
|
||||
| NeoForge | mod jar | 26.2 | Server worldgen + client HUD; current target is NeoForge 26.2.0.59 |
|
||||
|
||||
* Run `gradlew setup` any time you get dependency issues with craftbukkit
|
||||
* Configure ITJ Gradle to use JDK 17 (in settings, search for gradle)
|
||||
* Add a build line in the build.gradle for your own build task to directly compile Iris into your plugins folder if you
|
||||
prefer.
|
||||
* Resync the project & run your newly created task (under the development folder in gradle tasks!)
|
||||
Java 25 is required on every platform.
|
||||
|
||||
# Iris Toolbelt
|
||||
The modded feature set matches the plugin wherever the operation is not Bukkit-bound: worldgen
|
||||
from Iris packs, authoring with mod blocks/items/entities (with validation suggestions), studio
|
||||
workspaces with schema autocomplete over the server's live registries, entity spawning parity
|
||||
including death loot, pregeneration with a boss bar or client HUD, the `/iris` command tree, and
|
||||
the goldenhash determinism gate, which is interchangeable across all four platforms.
|
||||
|
||||
Everyone needs a tool-belt.
|
||||
### Native worldgen over Iris terrain
|
||||
|
||||
```java
|
||||
package com.volmit.iris.core.tools;
|
||||
Iris replaces the chunk generator, so vanilla and mod worldgen only runs where Iris runs it. This
|
||||
is identical on every platform.
|
||||
|
||||
// Get IrisDataManager from a world
|
||||
IrisToolbelt.access(anyWorld).getCompound().getData();
|
||||
| Vanilla / mod worldgen | Over Iris terrain | Control |
|
||||
|---|---|---|
|
||||
| Structures (vanilla, datapack, mod) | Yes, on by default | `importedStructures.disabled` denies individual keys |
|
||||
| Placed features: ores, trees, plants, springs, geodes | Yes, **off by default** | `importedFeatures.enabled` per dimension, with per-step and per-key filters |
|
||||
| Carvers (caves, canyons, mod carvers) | Never - architectural | Iris has no `NoiseGeneratorSettings` for a carver to sample; use pack `caves`/`carvings` |
|
||||
| Surface builders and surface rules | Never | Iris builds its surface from pack palettes |
|
||||
| Mod biomes | Only as a `derivative`, `vanillaDerivative`, `biomeScatter` or `biomeSkyScatter` target | Iris chooses biomes from the pack, not from a biome source |
|
||||
| Mob spawning, including mod mobs | Yes | Biome spawn tables are merged with the vanilla derivative's |
|
||||
|
||||
// Get Default Engine from world
|
||||
IrisToolbelt.access(anyWorld).getCompound().getDefaultEngine();
|
||||
With `importedFeatures` off - the default - chunk output is byte-for-byte what Iris has always
|
||||
produced. See [94 - API - Modded](https://github.com/VolmitSoftware/docs/blob/master/iris/94-api-modded.md) and
|
||||
[01 - Installation & Platforms](https://github.com/VolmitSoftware/docs/blob/master/iris/01-installation-platforms.md) for the full control reference, including
|
||||
which `pointed_dripstone` keys the 26.2 `speleothem` rename does and does not affect.
|
||||
|
||||
// Get the engine at the given height
|
||||
IrisToolbelt.access(anyWorld).getCompound().getEngineForHeight(68);
|
||||
Independently of that flag, Iris custom biomes now inherit the biome tags of their vanilla
|
||||
derivative on every platform, so the emitted datapack tag files change. Anything driven by biome
|
||||
tags therefore applies to Iris custom biomes: mob variants, spawn rules, and any vanilla or mod
|
||||
content selecting on `#minecraft:is_overworld` and friends.
|
||||
|
||||
// IS THIS THING ON?
|
||||
boolean yes=IrisToolbelt.isIrisWorld(world);
|
||||
## Install
|
||||
|
||||
// GTFO for worlds (moves players to any other world, just not this one)
|
||||
IrisToolbelt.evacuate(world);
|
||||
**Plugin (Paper/Purpur/Leaf/Canvas/Folia/Spigot):** drop the plugin jar into `plugins/` and start
|
||||
the server. First boot performs no pack download. Run `/iris download pack=overworld`,
|
||||
`/iris download pack=underworld`, or `/iris download link=https://host/path/pack.zip`, waiting for
|
||||
each download to finish before starting another. The current built-in packs declare no external
|
||||
datapack imports. Restart after download so Minecraft loads their dimension types and biomes.
|
||||
Custom packs that declare `datapackImports` must complete their external-datapack installation and
|
||||
registry restart first. Bukkit accepts Modrinth, direct HTTP(S), and absolute local `file:` ZIP URLs;
|
||||
ZIPs under `plugins/Iris/datapacks/imports/` are discovered for every Iris dimension. Plain Spigot
|
||||
supports ordinary managed `/iris create`, but not
|
||||
the early-bootstrap `/iris replace` path for canonical Overworld, Nether, or End slots.
|
||||
|
||||
**Mod (Fabric/Forge/NeoForge):** drop the mod jar into `mods/` and start the server. The jar is
|
||||
self-contained (core, SPI, and required Fabric API modules are bundled). First boot compiles only
|
||||
packs already on disk and never accesses the network. `/iris download` installs a pack atomically
|
||||
without stopping the server. The current built-in packs declare no external datapack imports.
|
||||
Custom packs that declare them require compatible archives in that save's `datapacks/` directory;
|
||||
modded `/iris datapack ingest` is an explanatory stub and does not install them. Restart only after
|
||||
the Iris pack and all of its inputs are present. Packs register their custom dimension types,
|
||||
height ranges, biomes, and external structure keys during that boot; worlds created
|
||||
before it run with fallback registry data.
|
||||
|
||||
**Singleplayer (modded clients):** installed Iris packs appear as selectable World Types on the
|
||||
Create New World screen; the integrated server runs the same engine.
|
||||
|
||||
## The client mod
|
||||
|
||||
Installing the mod jar on a client adds a native pregeneration HUD: a top-left panel with a
|
||||
progress bar, chunks done/total, percent, chunks per second, and ETA, turning yellow while the
|
||||
pregen is paused. The `H` key (rebindable, under the "Iris" controls category) toggles it.
|
||||
|
||||
The HUD works against modded Iris servers and against Bukkit/Paper Iris servers, both over the
|
||||
`irisworldgen:main` channel (custom payloads on modded, plugin messaging on Bukkit). Vanilla
|
||||
clients are unaffected and get the server-side boss bar instead; on non-Iris servers the client
|
||||
mod is inert.
|
||||
|
||||
## Quickstart
|
||||
|
||||
Complete the platform's pack, external-datapack, and registry-restart workflow above first. Then
|
||||
create and enter an Iris world.
|
||||
|
||||
Plugin (optional arguments are keyed):
|
||||
|
||||
IrisAccess access=IrisToolbelt.createWorld() // If you like builders...
|
||||
.name("myWorld") // The world name
|
||||
.dimension("terrifyinghands")
|
||||
.seed(69133742) // The world seed
|
||||
.headless(true) // Headless make gen go fast
|
||||
.pregen(PregenTask // Define a pregen job to run
|
||||
.builder()
|
||||
.center(new Position2(0,0)) // REGION coords (1 region = 32x32 chunks)
|
||||
.radius(4) // Radius in REGIONS. Rad of 4 means a 9x9 Region map.
|
||||
.build())
|
||||
.create();
|
||||
```
|
||||
/iris create myworld type=overworld seed=1337
|
||||
/iris tp myworld
|
||||
```
|
||||
|
||||
Mod (positional arguments):
|
||||
|
||||
```
|
||||
/iris create myworld overworld 1337
|
||||
/iris tp irisworldgen:myworld
|
||||
```
|
||||
|
||||
On Paper-family servers with early bootstrap (not plain Spigot), the shipping pair can instead
|
||||
replace the canonical portal-linked slots after their registry workflow is complete:
|
||||
|
||||
```
|
||||
/iris replace minecraft:overworld type=overworld seed=123456789
|
||||
/iris replace minecraft:the_nether type=underworld seed=-987654321
|
||||
```
|
||||
|
||||
Restart once after both commands report staged. The independent seeds apply to their respective
|
||||
slots, and vanilla portals keep routing between the canonical Overworld and Nether identities.
|
||||
|
||||
Pregeneration requires a radius in blocks. On the plugin, optional arguments are keyed; on modded
|
||||
servers they are positional and composable:
|
||||
|
||||
```
|
||||
/iris pregen start 352 world=myworld center=0,0 gui=false
|
||||
/iris pregen start 352 irisworldgen:myworld at 0 0 sync
|
||||
```
|
||||
|
||||
Players with the client mod see the native HUD; everyone else gets a boss bar (modded) or
|
||||
console/status output. `/iris pregen status` reports progress on the plugin.
|
||||
|
||||
## Studio and VSCode workspace
|
||||
|
||||
The studio is the pack authoring environment, available on all platforms. Studio worlds are
|
||||
transient - they are deleted on close and purged at startup.
|
||||
|
||||
Plugin:
|
||||
|
||||
```
|
||||
/iris studio create name=<name> [template=<pack>]
|
||||
/iris studio open <pack> [seed=1337]
|
||||
/iris studio vscode [dimension=<pack>]
|
||||
/iris studio update [dimension=<pack>]
|
||||
/iris studio close
|
||||
```
|
||||
|
||||
Mod:
|
||||
|
||||
```
|
||||
/iris studio create [name] [template]
|
||||
/iris studio open <pack> [seed]
|
||||
/iris studio vscode [pack]
|
||||
/iris studio update [pack]
|
||||
/iris studio close
|
||||
```
|
||||
|
||||
The generated VSCode workspace wires per-type JSON schemas (dimensions, biomes, regions, objects,
|
||||
loot, entities, snippets) for full autocomplete. Schemas are generated from the server's live
|
||||
registries, so on modded servers block, item, entity, enchantment, and potion-effect completion
|
||||
includes installed mod content (for example `create:brass_ingot`). Editing an open studio's pack
|
||||
files hotloads the changes and regenerates the schemas.
|
||||
|
||||
## PlaceholderAPI
|
||||
|
||||
Iris registers the `iris` expansion when PlaceholderAPI is enabled. Paths are dot-separated,
|
||||
lowercase, and never contain an underscore. Every value is plain text: no colour codes, no unit
|
||||
suffixes, no `%` character, `.` as the decimal separator, and no thousands grouping.
|
||||
|
||||
Three answers are possible. A path that is not in the list below returns nothing, so PlaceholderAPI
|
||||
re-emits the literal `%iris_...%` and a typo stays visible. A known path with no value right now
|
||||
returns `---`. A real zero returns `0`.
|
||||
|
||||
| Placeholder | Value |
|
||||
|---|---|
|
||||
| `%iris_available%` | `true` when the Iris terrain service is live |
|
||||
| `%iris_world.available%` | `true` when the reading player is in an Iris world and a reading exists |
|
||||
| `%iris_world.biome%` | Surface biome display name at the player, e.g. `Hot Desert Dunes` |
|
||||
| `%iris_world.biome-key%` | Surface biome load key, e.g. `desert/hot-dunes` |
|
||||
| `%iris_world.biome-custom%` | `true` when the surface biome defines custom derivatives, otherwise `false` |
|
||||
| `%iris_world.biome-custom-id%` | Authored `customDerivitives[].id` when exactly one derivative is defined, for example `golden-dunes`; `---` for none or multiple |
|
||||
| `%iris_world.biome-custom-ids%` | All authored custom derivative IDs, comma-and-space separated in definition order; `---` for none |
|
||||
| `%iris_world.biome-custom-key%` | Namespaced Minecraft registry key for the sole custom derivative, such as `iris:biomes/<hash>`; `---` for none, multiple, or unavailable mapping |
|
||||
| `%iris_world.biome-custom-keys%` | Registry keys for all custom derivatives in definition order, comma-and-space separated; `---` for none or any unavailable mapping |
|
||||
| `%iris_world.biome-custom-count%` | Number of authored custom derivatives; `0` for a vanilla-only biome |
|
||||
| `%iris_world.biome-derivative%` | Configured base derivative registry key, for example `minecraft:desert` |
|
||||
| `%iris_world.biome-vanilla-derivative%` | Effective vanilla derivative registry key used when a vanilla biome is required |
|
||||
| `%iris_world.biome-type%` | Inferred biome category: `land`, `sea`, `shore`, or `cave`; `---` when unavailable |
|
||||
| `%iris_world.min-height%` | World minimum absolute Y, inclusive |
|
||||
| `%iris_world.max-height%` | World maximum absolute Y, exclusive |
|
||||
| `%iris_world.height%` | World vertical span, equal to maximum minus minimum height |
|
||||
| `%iris_world.fluid-height%` | Dimension fluid level in absolute world Y; this is configuration, not a measurement of water at the player |
|
||||
| `%iris_world.region%` | Region display name at the player |
|
||||
| `%iris_world.region-key%` | Region load key |
|
||||
| `%iris_world.dimension%` | Dimension (pack) load key of the player's world |
|
||||
| `%iris_pregen.available%` | `true` while a pregeneration job is running |
|
||||
| `%iris_pregen.world%` | World name the running job is pregenerating |
|
||||
| `%iris_pregen.percent%` | Completion, `0.00` to `100.00`, no `%` character |
|
||||
| `%iris_pregen.eta%` | Estimated seconds remaining, whole number |
|
||||
| `%iris_pregen.eta-text%` | Same estimate as `2m 5s` or `1h 30m` |
|
||||
| `%iris_pregen.chunks%` | Chunks generated so far |
|
||||
| `%iris_pregen.total%` | Chunks in the job |
|
||||
| `%iris_pregen.chunks-per-second%` | Current rate |
|
||||
| `%iris_pregen.paused%` | `true` while the job is paused |
|
||||
|
||||
Custom derivative IDs are authored metadata. A singular ID or registry key is available only when the biome defines one derivative; plural keys list all definitions. Saved columns retain their historical biome definitions.
|
||||
|
||||
Biome and region values describe the surface at the player's block column. Height values describe world configuration. Walking refreshes them at most
|
||||
once per second per player, so a whole board of `world.*` keys costs one refresh per player per
|
||||
second no matter how many of them are on it, and a value may lag a sprinting player by up to a
|
||||
second. A jump that is not walking - joining, respawning, changing worlds, stepping through a portal,
|
||||
or any teleport including `/iris goto`, `/tp`, an ender pearl and a random teleport - is published
|
||||
immediately, so a player who arrives somewhere and then stands still never keeps reading the biome,
|
||||
region or dimension of where they came from. `pregen.*` is global: there is one pregeneration job per
|
||||
server, and `%iris_pregen.world%` says which world it is.
|
||||
|
||||
### Migration from the pre-2.0 keys
|
||||
|
||||
The old underscore keys are gone. There is no alias and no dual-accept window; an old key now
|
||||
renders literally so it is visible rather than silently wrong.
|
||||
|
||||
| Old key | New key | Why |
|
||||
|---|---|---|
|
||||
| `%iris_biome_name%` | `%iris_world.biome%` | Renamed onto the dot grammar |
|
||||
| `%iris_biome_id%` | `%iris_world.biome-key%` | Renamed; `id` was always the load key |
|
||||
| `%iris_region_name%` | `%iris_world.region%` | Renamed onto the dot grammar |
|
||||
| `%iris_region_id%` | `%iris_world.region-key%` | Renamed; `id` was always the load key |
|
||||
| `%iris_biome_file%` | removed | Rendered an absolute server path into player-visible text, and threw on packs with no backing file |
|
||||
| `%iris_region_file%` | removed | Same as `biome_file` |
|
||||
| `%iris_world_seed%` | removed | Handed the world seed to anyone who could read a scoreboard, and a placeholder has no permission context to gate on |
|
||||
| `%iris_terrain_height%` | removed | Reported the *generated* height, before objects and player edits, so it disagreed with the block under the player's feet |
|
||||
| `%iris_terrain_slope%` | removed | Three extra noise samples per read for an unformatted pack-authoring diagnostic |
|
||||
| `%iris_world_mode%` | removed | Studio or Production; a studio world exists for seconds during authoring and is never on a live board |
|
||||
| `%iris_world_speed%` | removed | Mutated engine rate-window state every time it was read. `%iris_pregen.chunks-per-second%` answers the same question from a snapshot |
|
||||
|
||||
The old keys also read the *cave* biome for a player standing under an overhang, because they
|
||||
sampled two blocks above the player's feet. The new `world.biome` is always the surface biome,
|
||||
which is what a board reader means.
|
||||
|
||||
## Building from source
|
||||
|
||||
Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everything else.
|
||||
|
||||
```
|
||||
./gradlew buildAllToOut
|
||||
```
|
||||
|
||||
builds every platform artifact into the workspace-level `../PluginOuts/` directory:
|
||||
|
||||
```
|
||||
Iris v<version> [CraftBukkit] <mc>.jar
|
||||
Iris v<version> [Fabric] <mc>+<loader>.jar
|
||||
Iris v<version> [Forge] <mc>+<loader>.jar
|
||||
Iris v<version> [NeoForge] <mc>+<loader>.jar
|
||||
```
|
||||
|
||||
Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `buildNeoforge`. The
|
||||
SPI jar (the pure-JVM adapter/platform contract, not the stable plugin API) is built to `spi/build/libs/` by
|
||||
`./gradlew :spi:jar`.
|
||||
|
||||
`./gradlew buildAll` also runs `buildAllToOut` and copies the jars into a consumer dropin tree
|
||||
for a local test server. The consumer directory defaults to `../../[Minecraft Server]/consumers/`
|
||||
when it exists, otherwise `build/consumers/` inside the repo. `-Plocation=/path/to/consumers`
|
||||
changes only the consumer directory. Both tasks copy all four verified platform jars into
|
||||
`../PluginOuts/`.
|
||||
|
||||
If you need help compiling as a developer or contributor, ask in the Discord.
|
||||
|
||||
## Adapters / modded development
|
||||
|
||||
`core/` and `spi/` are pure JVM. `adapters/bukkit/` is part of the root Gradle build; the three
|
||||
modded adapters (`adapters/fabric`, `adapters/forge`, `adapters/neoforge`) are standalone builds
|
||||
with their own `settings.gradle`, which is what keeps Loom, ForgeGradle, and ModDevGradle off one
|
||||
plugin classpath. Drive them with `-p`:
|
||||
|
||||
```
|
||||
./gradlew -p adapters/fabric runServer # or runClient
|
||||
./gradlew -p adapters/forge runServer
|
||||
./gradlew -p adapters/neoforge runServer
|
||||
./gradlew -p adapters/fabric test # shared adapters/modded-common test suite
|
||||
```
|
||||
|
||||
Each `runServer` accepts determinism and world-integrity flags, forwarded to the game as system
|
||||
properties:
|
||||
|
||||
| Flag | System property | Purpose |
|
||||
|---|---|---|
|
||||
| `-PirisParity=<pack>` | `iris.parity` | Run the cross-platform parity harness for a pack |
|
||||
| `-PirisParityGolden=<file>` | `iris.parity.golden` | Compare against a captured golden-hash file |
|
||||
| `-PirisParityDeep=true` | `iris.parity.deep` | Deep (per-block) parity instead of hash-only |
|
||||
| `-PirisWorldCheck=<world>` | `iris.worldcheck` | Post-generation world integrity check |
|
||||
|
||||
Fabric additionally takes `-PirisClientRunDir=<dir>` to relocate the `runClient` working directory.
|
||||
Shared code lives in `adapters/minecraft-common` (all adapters), `adapters/modded-common`
|
||||
(loaders + the shared test suite), and `adapters/client-common` (client HUD and world-type
|
||||
screens); every adapter adds those source directories, so one edit reaches all three loaders.
|
||||
|
||||
For IDE import you can surface the three adapter builds in the root composite with
|
||||
`-PincludeModdedAdapters=true`. It is off by default: each adapter includes the root build back to
|
||||
substitute `art.arcane:core` and `art.arcane:spi`, so including them from the root closes a
|
||||
composite cycle. The build and release paths do not need it.
|
||||
|
||||
## Documentation
|
||||
|
||||
Full product docs live in the central [VolmitSoftware/docs](https://github.com/VolmitSoftware/docs/tree/master/iris) repository. Start with:
|
||||
|
||||
- [Overview and index](https://github.com/VolmitSoftware/docs/blob/master/iris/00-overview.md)
|
||||
- [Installation and platforms](https://github.com/VolmitSoftware/docs/blob/master/iris/01-installation-platforms.md)
|
||||
- [Getting started](https://github.com/VolmitSoftware/docs/blob/master/iris/02-getting-started.md)
|
||||
- [Structures overview](https://github.com/VolmitSoftware/docs/blob/master/iris/18-structures-overview.md)
|
||||
- [API — Getting Started](https://github.com/VolmitSoftware/docs/blob/master/iris/90-api-getting-started.md)
|
||||
- [Maintainer — MC version bump](https://github.com/VolmitSoftware/docs/blob/master/iris/85-maintainer-mc-version-bump.md)
|
||||
- [Maintainer — release checklist](https://github.com/VolmitSoftware/docs/blob/master/iris/86-maintainer-release-checklist.md)
|
||||
|
||||
+1280
File diff suppressed because it is too large
Load Diff
+509
@@ -0,0 +1,509 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureFrequencyScale;
|
||||
import art.arcane.iris.structure.nativegen.IrisImportedStructureControl;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderOwner;
|
||||
import net.minecraft.core.Vec3i;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
final class DatapackStructureStateFilter {
|
||||
private DatapackStructureStateFilter() {
|
||||
}
|
||||
|
||||
static Selection filter(
|
||||
List<Holder<StructureSet>> structureSets,
|
||||
DatapackStructureScopeIndex scopeIndex,
|
||||
Set<String> declaredSources
|
||||
) {
|
||||
return filter(structureSets, scopeIndex, declaredSources, new IrisImportedStructureControl());
|
||||
}
|
||||
|
||||
static Selection filter(
|
||||
List<Holder<StructureSet>> structureSets,
|
||||
DatapackStructureScopeIndex scopeIndex,
|
||||
Set<String> declaredSources,
|
||||
IrisImportedStructureControl importedStructures
|
||||
) {
|
||||
return filter(
|
||||
structureSets,
|
||||
scopeIndex,
|
||||
declaredSources,
|
||||
importedStructures,
|
||||
keyIndex(structureSets));
|
||||
}
|
||||
|
||||
static Selection filter(
|
||||
List<Holder<StructureSet>> structureSets,
|
||||
DatapackStructureScopeIndex scopeIndex,
|
||||
Set<String> declaredSources,
|
||||
IrisImportedStructureControl importedStructures,
|
||||
StructureSetKeyIndex keyIndex
|
||||
) {
|
||||
if (scopeIndex.isEmpty()) {
|
||||
return new Selection(
|
||||
scaleFrequencyOverrides(structureSets, importedStructures, keyIndex),
|
||||
0,
|
||||
0);
|
||||
}
|
||||
Map<String, Holder<StructureSet>> holdersByKey = new HashMap<>();
|
||||
for (Holder<StructureSet> holder : structureSets) {
|
||||
String key = structureSetKey(holder, keyIndex);
|
||||
if (key != null) {
|
||||
holdersByKey.putIfAbsent(key, holder);
|
||||
}
|
||||
}
|
||||
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity = new IdentityHashMap<>();
|
||||
Set<Holder<StructureSet>> visiting = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
List<Holder<StructureSet>> filteredSets = new ArrayList<>(structureSets.size());
|
||||
int retainedManagedSets = 0;
|
||||
int excludedManagedSets = 0;
|
||||
for (Holder<StructureSet> holder : structureSets) {
|
||||
String setKey = structureSetKey(holder, keyIndex);
|
||||
boolean managedSet = setKey != null && scopeIndex.isManagedStructureSet(setKey);
|
||||
Holder<StructureSet> scopedHolder = scopeHolder(
|
||||
holder,
|
||||
scopeIndex,
|
||||
declaredSources,
|
||||
holdersByKey,
|
||||
scopedByIdentity,
|
||||
visiting,
|
||||
keyIndex);
|
||||
if (scopedHolder == null) {
|
||||
if (managedSet) {
|
||||
excludedManagedSets++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (managedSet) {
|
||||
retainedManagedSets++;
|
||||
}
|
||||
filteredSets.add(scopedHolder);
|
||||
}
|
||||
return new Selection(
|
||||
scaleFrequencyOverrides(
|
||||
List.copyOf(filteredSets), importedStructures, keyIndex),
|
||||
retainedManagedSets,
|
||||
excludedManagedSets);
|
||||
}
|
||||
|
||||
static String structureSetKey(Holder<StructureSet> holder) {
|
||||
return structureSetKey(holder, keyIndex(List.of(holder)));
|
||||
}
|
||||
|
||||
static StructureSetKeyIndex keyIndex(
|
||||
Iterable<? extends Holder<StructureSet>> registeredStructureSets
|
||||
) {
|
||||
return StructureSetKeyIndex.create(registeredStructureSets);
|
||||
}
|
||||
|
||||
private static String structureSetKey(
|
||||
Holder<StructureSet> holder,
|
||||
StructureSetKeyIndex keyIndex
|
||||
) {
|
||||
return keyIndex.resolve(holder)
|
||||
.map(key -> key.identifier().toString())
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private static List<StructureSet.StructureSelectionEntry> filterEntries(
|
||||
List<StructureSet.StructureSelectionEntry> entries,
|
||||
DatapackStructureScopeIndex scopeIndex,
|
||||
Set<String> declaredSources
|
||||
) {
|
||||
List<StructureSet.StructureSelectionEntry> filtered = new ArrayList<>(entries.size());
|
||||
for (StructureSet.StructureSelectionEntry entry : entries) {
|
||||
String structureKey = entry.structure().unwrapKey()
|
||||
.map(key -> key.identifier().toString())
|
||||
.orElse(null);
|
||||
if (structureKey == null || scopeIndex.allowsStructure(structureKey, declaredSources)) {
|
||||
filtered.add(entry);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private static Holder<StructureSet> scopeHolder(
|
||||
Holder<StructureSet> holder,
|
||||
DatapackStructureScopeIndex scopeIndex,
|
||||
Set<String> declaredSources,
|
||||
Map<String, Holder<StructureSet>> holdersByKey,
|
||||
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity,
|
||||
Set<Holder<StructureSet>> visiting,
|
||||
StructureSetKeyIndex keyIndex
|
||||
) {
|
||||
if (scopedByIdentity.containsKey(holder)) {
|
||||
return scopedByIdentity.get(holder);
|
||||
}
|
||||
if (!visiting.add(holder)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String setKey = structureSetKey(holder, keyIndex);
|
||||
if (setKey != null && scopeIndex.isManagedStructureSet(setKey)
|
||||
&& !scopeIndex.allowsStructureSet(setKey, declaredSources)) {
|
||||
scopedByIdentity.put(holder, null);
|
||||
return null;
|
||||
}
|
||||
StructureSet originalSet = holder.value();
|
||||
List<StructureSet.StructureSelectionEntry> entries = filterEntries(
|
||||
originalSet.structures(), scopeIndex, declaredSources);
|
||||
if (entries.isEmpty()) {
|
||||
scopedByIdentity.put(holder, null);
|
||||
return null;
|
||||
}
|
||||
StructurePlacement placement = scopePlacement(
|
||||
originalSet.placement(),
|
||||
scopeIndex,
|
||||
declaredSources,
|
||||
holdersByKey,
|
||||
scopedByIdentity,
|
||||
visiting,
|
||||
keyIndex);
|
||||
if (entries.size() == originalSet.structures().size()
|
||||
&& placement == originalSet.placement()) {
|
||||
scopedByIdentity.put(holder, holder);
|
||||
return holder;
|
||||
}
|
||||
Holder<StructureSet> scoped = replacementHolder(
|
||||
holder, new StructureSet(entries, placement), keyIndex);
|
||||
scopedByIdentity.put(holder, scoped);
|
||||
return scoped;
|
||||
} finally {
|
||||
visiting.remove(holder);
|
||||
}
|
||||
}
|
||||
|
||||
static Optional<StructurePlacement.ExclusionZone> exclusionZone(StructurePlacement placement) {
|
||||
Object value = declaredFieldValue(StructurePlacement.class, placement, Optional.class);
|
||||
if (value instanceof Optional<?> optional && (optional.isEmpty()
|
||||
|| optional.get() instanceof StructurePlacement.ExclusionZone)) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Optional<StructurePlacement.ExclusionZone> resolved =
|
||||
(Optional<StructurePlacement.ExclusionZone>) optional;
|
||||
return resolved;
|
||||
}
|
||||
throw new IllegalStateException("Could not read structure placement exclusion zone from "
|
||||
+ placement.getClass().getName());
|
||||
}
|
||||
|
||||
private static StructurePlacement scopePlacement(
|
||||
StructurePlacement placement,
|
||||
DatapackStructureScopeIndex scopeIndex,
|
||||
Set<String> declaredSources,
|
||||
Map<String, Holder<StructureSet>> holdersByKey,
|
||||
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity,
|
||||
Set<Holder<StructureSet>> visiting,
|
||||
StructureSetKeyIndex keyIndex
|
||||
) {
|
||||
Optional<StructurePlacement.ExclusionZone> currentZone = exclusionZone(placement);
|
||||
if (currentZone.isEmpty()) {
|
||||
return placement;
|
||||
}
|
||||
|
||||
Holder<StructureSet> target = currentZone.get().otherSet();
|
||||
String targetKey = structureSetKey(target, keyIndex);
|
||||
if (targetKey != null) {
|
||||
target = holdersByKey.getOrDefault(targetKey, target);
|
||||
}
|
||||
Holder<StructureSet> scopedTarget = scopeHolder(
|
||||
target,
|
||||
scopeIndex,
|
||||
declaredSources,
|
||||
holdersByKey,
|
||||
scopedByIdentity,
|
||||
visiting,
|
||||
keyIndex);
|
||||
Optional<StructurePlacement.ExclusionZone> scopedZone = Optional.empty();
|
||||
if (scopedTarget != null) {
|
||||
scopedZone = Optional.of(new StructurePlacement.ExclusionZone(
|
||||
scopedTarget,
|
||||
currentZone.get().chunkCount()));
|
||||
}
|
||||
if (scopedTarget == currentZone.get().otherSet()) {
|
||||
return placement;
|
||||
}
|
||||
return copyPlacement(placement, scopedZone, 1D);
|
||||
}
|
||||
|
||||
private static List<Holder<StructureSet>> scaleFrequencyOverrides(
|
||||
List<Holder<StructureSet>> structureSets,
|
||||
IrisImportedStructureControl importedStructures,
|
||||
StructureSetKeyIndex keyIndex
|
||||
) {
|
||||
if (!importedStructures.hasFrequencyOverrides()) {
|
||||
return structureSets;
|
||||
}
|
||||
Map<String, Holder<StructureSet>> holdersByKey = new HashMap<>();
|
||||
for (Holder<StructureSet> holder : structureSets) {
|
||||
String key = structureSetKey(holder, keyIndex);
|
||||
if (key != null) {
|
||||
holdersByKey.putIfAbsent(key, holder);
|
||||
}
|
||||
}
|
||||
Map<Holder<StructureSet>, Holder<StructureSet>> scaledByIdentity = new IdentityHashMap<>();
|
||||
List<Holder<StructureSet>> scaledSets = new ArrayList<>(structureSets.size());
|
||||
boolean changed = false;
|
||||
for (Holder<StructureSet> holder : structureSets) {
|
||||
Holder<StructureSet> scaled = scaleFrequencyHolder(
|
||||
holder,
|
||||
importedStructures,
|
||||
holdersByKey,
|
||||
scaledByIdentity,
|
||||
keyIndex);
|
||||
scaledSets.add(scaled);
|
||||
changed |= scaled != holder;
|
||||
}
|
||||
return changed ? List.copyOf(scaledSets) : structureSets;
|
||||
}
|
||||
|
||||
private static Holder<StructureSet> scaleFrequencyHolder(
|
||||
Holder<StructureSet> holder,
|
||||
IrisImportedStructureControl importedStructures,
|
||||
Map<String, Holder<StructureSet>> holdersByKey,
|
||||
Map<Holder<StructureSet>, Holder<StructureSet>> scaledByIdentity,
|
||||
StructureSetKeyIndex keyIndex
|
||||
) {
|
||||
if (scaledByIdentity.containsKey(holder)) {
|
||||
return scaledByIdentity.get(holder);
|
||||
}
|
||||
if (!dependsOnFrequencyOverride(
|
||||
holder, importedStructures, holdersByKey, keyIndex)) {
|
||||
scaledByIdentity.put(holder, holder);
|
||||
return holder;
|
||||
}
|
||||
|
||||
ResourceKey<StructureSet> holderKey = keyIndex.resolve(holder).orElseThrow(() ->
|
||||
new IllegalStateException(
|
||||
"An affected native structure-set exclusion graph has an unkeyed holder"));
|
||||
ReboundStructureSetHolder scaledHolder = new ReboundStructureSetHolder(holderKey);
|
||||
scaledByIdentity.put(holder, scaledHolder);
|
||||
|
||||
StructureSet originalSet = holder.value();
|
||||
StructurePlacement originalPlacement = originalSet.placement();
|
||||
Optional<StructurePlacement.ExclusionZone> originalZone = exclusionZone(originalPlacement);
|
||||
Optional<StructurePlacement.ExclusionZone> scaledZone = originalZone;
|
||||
if (originalZone.isPresent()) {
|
||||
Holder<StructureSet> target = canonicalHolder(
|
||||
originalZone.get().otherSet(), holdersByKey, keyIndex);
|
||||
Holder<StructureSet> scaledTarget = scaleFrequencyHolder(
|
||||
target,
|
||||
importedStructures,
|
||||
holdersByKey,
|
||||
scaledByIdentity,
|
||||
keyIndex);
|
||||
if (scaledTarget != originalZone.get().otherSet()) {
|
||||
scaledZone = Optional.of(new StructurePlacement.ExclusionZone(
|
||||
scaledTarget, originalZone.get().chunkCount()));
|
||||
}
|
||||
}
|
||||
|
||||
double multiplier = importedStructures.frequencyMultiplier(
|
||||
structureSetKey(holder, keyIndex));
|
||||
StructurePlacement scaledPlacement = multiplier == 1D && scaledZone.equals(originalZone)
|
||||
? originalPlacement
|
||||
: copyPlacement(originalPlacement, scaledZone, multiplier);
|
||||
scaledHolder.bind(new StructureSet(originalSet.structures(), scaledPlacement));
|
||||
return scaledHolder;
|
||||
}
|
||||
|
||||
private static boolean dependsOnFrequencyOverride(
|
||||
Holder<StructureSet> holder,
|
||||
IrisImportedStructureControl importedStructures,
|
||||
Map<String, Holder<StructureSet>> holdersByKey,
|
||||
StructureSetKeyIndex keyIndex
|
||||
) {
|
||||
Set<Holder<StructureSet>> visited = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
Holder<StructureSet> current = holder;
|
||||
while (visited.add(current)) {
|
||||
if (importedStructures.frequencyMultiplier(
|
||||
structureSetKey(current, keyIndex)) != 1D) {
|
||||
return true;
|
||||
}
|
||||
Optional<StructurePlacement.ExclusionZone> zone =
|
||||
exclusionZone(current.value().placement());
|
||||
if (zone.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
current = canonicalHolder(zone.get().otherSet(), holdersByKey, keyIndex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Holder<StructureSet> canonicalHolder(
|
||||
Holder<StructureSet> holder,
|
||||
Map<String, Holder<StructureSet>> holdersByKey,
|
||||
StructureSetKeyIndex keyIndex
|
||||
) {
|
||||
String key = structureSetKey(holder, keyIndex);
|
||||
return key == null ? holder : holdersByKey.getOrDefault(key, holder);
|
||||
}
|
||||
|
||||
private static Holder<StructureSet> replacementHolder(
|
||||
Holder<StructureSet> original,
|
||||
StructureSet replacement,
|
||||
StructureSetKeyIndex keyIndex
|
||||
) {
|
||||
Optional<ResourceKey<StructureSet>> key = keyIndex.resolve(original);
|
||||
if (key.isEmpty()) {
|
||||
return Holder.direct(replacement);
|
||||
}
|
||||
ReboundStructureSetHolder holder = new ReboundStructureSetHolder(key.get());
|
||||
holder.bind(replacement);
|
||||
return holder;
|
||||
}
|
||||
|
||||
private static StructurePlacement copyPlacement(
|
||||
StructurePlacement placement,
|
||||
Optional<StructurePlacement.ExclusionZone> exclusionZone,
|
||||
double frequencyMultiplier
|
||||
) {
|
||||
Vec3i locateOffset = (Vec3i) declaredFieldValue(
|
||||
StructurePlacement.class, placement, Vec3i.class);
|
||||
StructurePlacement.FrequencyReductionMethod frequencyReductionMethod =
|
||||
(StructurePlacement.FrequencyReductionMethod) declaredFieldValue(
|
||||
StructurePlacement.class,
|
||||
placement,
|
||||
StructurePlacement.FrequencyReductionMethod.class);
|
||||
float frequency = (float) declaredFieldValue(
|
||||
StructurePlacement.class, placement, float.class);
|
||||
int salt = (int) declaredFieldValue(
|
||||
StructurePlacement.class, placement, int.class);
|
||||
|
||||
if (placement instanceof RandomSpreadStructurePlacement randomSpread) {
|
||||
NativeStructureFrequencyScale scale = NativeStructureFrequencyScale.randomSpread(
|
||||
frequency, randomSpread.spacing(), randomSpread.separation(), frequencyMultiplier);
|
||||
return new RandomSpreadStructurePlacement(
|
||||
locateOffset,
|
||||
frequencyReductionMethod,
|
||||
scale.frequency(),
|
||||
salt,
|
||||
exclusionZone,
|
||||
scale.spacing(),
|
||||
randomSpread.separation(),
|
||||
randomSpread.spreadType());
|
||||
}
|
||||
if (placement.getClass() == ConcentricRingsStructurePlacement.class) {
|
||||
ConcentricRingsStructurePlacement rings =
|
||||
(ConcentricRingsStructurePlacement) placement;
|
||||
float scaledFrequency = NativeStructureFrequencyScale.probability(
|
||||
frequency, frequencyMultiplier);
|
||||
return new ConcentricRingsStructurePlacement(
|
||||
locateOffset,
|
||||
frequencyReductionMethod,
|
||||
scaledFrequency,
|
||||
salt,
|
||||
exclusionZone,
|
||||
rings.distance(),
|
||||
rings.spread(),
|
||||
rings.count(),
|
||||
rings.preferredBiomes());
|
||||
}
|
||||
throw new IllegalStateException("Unsupported native structure placement in an affected "
|
||||
+ "frequency-override or datapack-scope graph: "
|
||||
+ placement.getClass().getName());
|
||||
}
|
||||
|
||||
private static Object declaredFieldValue(
|
||||
Class<?> declaringType,
|
||||
Object target,
|
||||
Class<?> fieldType
|
||||
) {
|
||||
Field match = null;
|
||||
for (Field field : declaringType.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(field.getModifiers()) || field.getType() != fieldType) {
|
||||
continue;
|
||||
}
|
||||
if (match != null) {
|
||||
throw new IllegalStateException("Ambiguous " + fieldType.getName() + " field on "
|
||||
+ declaringType.getName());
|
||||
}
|
||||
match = field;
|
||||
}
|
||||
if (match == null) {
|
||||
throw new IllegalStateException("Missing " + fieldType.getName() + " field on "
|
||||
+ declaringType.getName());
|
||||
}
|
||||
try {
|
||||
match.setAccessible(true);
|
||||
return match.get(target);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new IllegalStateException("Could not read " + fieldType.getName() + " field on "
|
||||
+ declaringType.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
static final class StructureSetKeyIndex {
|
||||
private final Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>>
|
||||
keysByEntries;
|
||||
|
||||
private StructureSetKeyIndex(
|
||||
Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>>
|
||||
keysByEntries
|
||||
) {
|
||||
this.keysByEntries = keysByEntries;
|
||||
}
|
||||
|
||||
private static StructureSetKeyIndex create(
|
||||
Iterable<? extends Holder<StructureSet>> registeredStructureSets
|
||||
) {
|
||||
Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>> keys =
|
||||
new IdentityHashMap<>();
|
||||
for (Holder<StructureSet> holder : registeredStructureSets) {
|
||||
Optional<ResourceKey<StructureSet>> key = holder.unwrapKey();
|
||||
if (key.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
List<StructureSet.StructureSelectionEntry> entries = holder.value().structures();
|
||||
ResourceKey<StructureSet> previous = keys.put(entries, key.get());
|
||||
if (previous != null && !previous.equals(key.get())) {
|
||||
throw new IllegalStateException(
|
||||
"Multiple registered structure sets share the same entry list");
|
||||
}
|
||||
}
|
||||
return new StructureSetKeyIndex(keys);
|
||||
}
|
||||
|
||||
private Optional<ResourceKey<StructureSet>> resolve(Holder<StructureSet> holder) {
|
||||
Optional<ResourceKey<StructureSet>> key = holder.unwrapKey();
|
||||
if (key.isPresent()) {
|
||||
return key;
|
||||
}
|
||||
return Optional.ofNullable(keysByEntries.get(holder.value().structures()));
|
||||
}
|
||||
}
|
||||
|
||||
record Selection(
|
||||
List<Holder<StructureSet>> structureSets,
|
||||
int retainedManagedSets,
|
||||
int excludedManagedSets
|
||||
) {
|
||||
}
|
||||
|
||||
private static final class ReboundStructureSetHolder extends Holder.Reference<StructureSet> {
|
||||
private ReboundStructureSetHolder(ResourceKey<StructureSet> key) {
|
||||
super(Type.STAND_ALONE, new HolderOwner<>() {
|
||||
}, key, null);
|
||||
}
|
||||
|
||||
private void bind(StructureSet structureSet) {
|
||||
bindValue(structureSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.generation.runtime.DimensionStackContext;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||
import art.arcane.iris.nativegen.NativeGenerationWriteGuard;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.nativegen.NativeFeatureGenerationPolicy;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
||||
import art.arcane.iris.generation.decoration.IrisDecorationStep;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl;
|
||||
import art.arcane.iris.structure.object.IrisStaticObjectLayer;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import it.unimi.dsi.fastutil.ints.IntArraySet;
|
||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeGenerationSettings;
|
||||
import net.minecraft.world.level.biome.FeatureSorter;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomSupport;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
||||
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
/**
|
||||
* Bukkit twin of the modded imported-feature stage. Same control, same semantics, same seeds: with
|
||||
* {@code importedFeatures.enabled} the vanilla placed-feature pass runs over Iris terrain, and with the
|
||||
* control off nothing here allocates or runs.
|
||||
*
|
||||
* <p>Iris does its own native structure pass and calls the delegate with {@code addVanillaDecorations=false},
|
||||
* so the vanilla feature half never runs on its own. This reproduces that half only - structures are never
|
||||
* placed twice.
|
||||
*
|
||||
* <p>Threading: called from {@code applyBiomeDecoration} on the worldgen thread that owns the chunk. The
|
||||
* FEATURES chunk step is not parallel-safe.
|
||||
*/
|
||||
final class ImportedFeatureStage {
|
||||
private static final String CYCLE_MARKER = "Feature order cycle found";
|
||||
|
||||
private final Engine engine;
|
||||
private final ConcurrentHashMap<Integer, RuntimeFeatureState> runtimeStates = new ConcurrentHashMap<>();
|
||||
|
||||
ImportedFeatureStage(Engine engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generation settings for one biome holder, mapping Iris custom biomes onto their vanilla derivative.
|
||||
* Pass-through while the control is off, which is what keeps {@code BiomeFilter} behaving exactly as it
|
||||
* does today.
|
||||
*/
|
||||
BiomeGenerationSettings generationSettings(Holder<Biome> biome) {
|
||||
RuntimeFeatureState state = currentState();
|
||||
FeatureTable table = state == null ? null : state.featureTable();
|
||||
if (table == null) {
|
||||
return null;
|
||||
}
|
||||
Holder<Biome> mapped = table.derivatives().get(holderKey(biome));
|
||||
return mapped == null ? null : mapped.value().getGenerationSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the table on the first decorated chunk, or leaves the stage inert. A feature-order cycle is
|
||||
* reported once here and degrades to features-off; it never reaches chunk generation as a crash.
|
||||
*/
|
||||
void prepare(WorldGenLevel level) {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
int runtimeId = engine.getCacheID();
|
||||
if (settled(runtimeStates.get(runtimeId), dimension)) {
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (settled(runtimeStates.get(runtimeId), dimension)) {
|
||||
return;
|
||||
}
|
||||
runtimeStates.put(runtimeId, build(level, dimension));
|
||||
}
|
||||
}
|
||||
|
||||
void evictRuntime(int runtimeId) {
|
||||
runtimeStates.remove(runtimeId);
|
||||
}
|
||||
|
||||
private RuntimeFeatureState currentState() {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
RuntimeFeatureState state = runtimeStates.get(engine.getCacheID());
|
||||
return settled(state, dimension) ? state : null;
|
||||
}
|
||||
|
||||
private static boolean settled(RuntimeFeatureState state, IrisDimension dimension) {
|
||||
return state != null && state.dimension() == dimension;
|
||||
}
|
||||
|
||||
private RuntimeFeatureState build(WorldGenLevel level, IrisDimension dimension) {
|
||||
IrisImportedFeatureControl control;
|
||||
try {
|
||||
control = NativeFeatureGenerationPolicy.control(engine);
|
||||
} catch (RuntimeException error) {
|
||||
IrisLogging.error("Iris could not read importedFeatures for this dimension; features off: "
|
||||
+ error);
|
||||
return new RuntimeFeatureState(dimension, null);
|
||||
}
|
||||
if (!control.shouldGenerateFeatures()) {
|
||||
return new RuntimeFeatureState(dimension, null);
|
||||
}
|
||||
FeatureTable built;
|
||||
try {
|
||||
built = buildTable(level, control, dimension);
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
|
||||
+ ": feature table construction failed: " + error);
|
||||
return new RuntimeFeatureState(dimension, null);
|
||||
}
|
||||
if (built == null) {
|
||||
return new RuntimeFeatureState(dimension, null);
|
||||
}
|
||||
IrisLogging.info("Iris importedFeatures on for " + dimensionKey() + ": " + built.biomes().size()
|
||||
+ " biomes, " + built.steps().size() + " steps, " + built.derivatives().size()
|
||||
+ " custom-biome derivative maps");
|
||||
return new RuntimeFeatureState(dimension, built);
|
||||
}
|
||||
|
||||
private FeatureTable buildTable(WorldGenLevel level, IrisImportedFeatureControl control,
|
||||
IrisDimension dimension) {
|
||||
Registry<Biome> registry = level.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
Set<String> visibleKeys = visibleBiomeKeys();
|
||||
// Registry-ordered walk, never a hash-ordered set: FeatureSorter's cycle detection walks this list and
|
||||
// an unordered walk makes detection depend on JVM hash order.
|
||||
List<Holder<Biome>> biomes = new ArrayList<>();
|
||||
Map<String, Holder<Biome>> byKey = new HashMap<>();
|
||||
registry.listElements().forEach((Holder.Reference<Biome> reference) -> {
|
||||
String key = holderKey(reference);
|
||||
if (key != null && visibleKeys.contains(key)) {
|
||||
biomes.add(reference);
|
||||
byKey.put(key, reference);
|
||||
}
|
||||
});
|
||||
if (biomes.isEmpty()) {
|
||||
IrisLogging.error("Iris importedFeatures is on but " + dimensionKey()
|
||||
+ " exposes no registered biomes; features off");
|
||||
return null;
|
||||
}
|
||||
Map<String, Holder<Biome>> derivatives = customBiomeDerivatives(registry, byKey);
|
||||
List<FeatureSorter.StepFeatureData> steps;
|
||||
try {
|
||||
steps = FeatureSorter.buildFeaturesPerStep(biomes,
|
||||
(Holder<Biome> biome) -> settingsFor(biome, derivatives).features(), true);
|
||||
} catch (IllegalStateException error) {
|
||||
String message = error.getMessage();
|
||||
if (message == null || !message.contains(CYCLE_MARKER)) {
|
||||
throw error;
|
||||
}
|
||||
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
|
||||
+ ": the registered placed features cannot be ordered. " + message
|
||||
+ ". Remove or reorder the conflicting content, or leave importedFeatures.enabled false.");
|
||||
return null;
|
||||
}
|
||||
boolean filtered = control.getDisabled() != null && !control.getDisabled().isEmpty();
|
||||
return new FeatureTable(dimension, control, List.copyOf(biomes), Set.copyOf(biomes),
|
||||
Map.copyOf(byKey), steps, Map.copyOf(derivatives), filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every biome key Iris can write into a chunk section: the structure derivative, the raw derivative, every
|
||||
* scatter entry, and every generated custom biome.
|
||||
*/
|
||||
private Set<String> visibleBiomeKeys() {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
|
||||
addKey(keys, irisBiome.getStructureDerivativeKey());
|
||||
addKey(keys, irisBiome.getDerivativeKey());
|
||||
addKey(keys, irisBiome.getVanillaDerivativeKey());
|
||||
for (String scatter : irisBiome.getBiomeScatter()) {
|
||||
addKey(keys, scatter);
|
||||
}
|
||||
for (String scatter : irisBiome.getBiomeSkyScatter()) {
|
||||
addKey(keys, scatter);
|
||||
}
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
addKey(keys, engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps every generated Iris custom biome key onto the registry holder of its Iris biome's vanilla
|
||||
* derivative. The custom biome's own datapack JSON carries no features by design.
|
||||
*/
|
||||
private Map<String, Holder<Biome>> customBiomeDerivatives(Registry<Biome> registry,
|
||||
Map<String, Holder<Biome>> byKey) {
|
||||
Map<String, Holder<Biome>> derivatives = new HashMap<>();
|
||||
for (IrisBiome irisBiome : engine.getDimension().getReachableBiomes(engine)) {
|
||||
if (!irisBiome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
String derivativeKey = normalizeKey(irisBiome.getVanillaDerivativeKey());
|
||||
// Resolve from the registry, not only from the visible biome set: a sea or shore biome's structure
|
||||
// derivative is rewritten away from its vanilla derivative, so the derivative whose features we
|
||||
// want is not always a biome Iris can emit.
|
||||
Holder<Biome> derivative = byKey.get(derivativeKey);
|
||||
if (derivative == null) {
|
||||
derivative = resolveHolder(registry, derivativeKey);
|
||||
}
|
||||
if (derivative == null) {
|
||||
IrisLogging.warn("Iris importedFeatures: vanilla derivative " + derivativeKey + " of biome "
|
||||
+ irisBiome.getLoadKey()
|
||||
+ " is not registered; its custom biomes generate no imported features");
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
derivatives.put(
|
||||
engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome),
|
||||
derivative
|
||||
);
|
||||
}
|
||||
}
|
||||
return derivatives;
|
||||
}
|
||||
|
||||
private static Holder<Biome> resolveHolder(Registry<Biome> registry, String key) {
|
||||
if (registry == null || key == null || key.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(key);
|
||||
if (identifier == null) {
|
||||
return null;
|
||||
}
|
||||
return registry.get(identifier).<Holder<Biome>>map((Holder.Reference<Biome> reference) -> reference)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private static BiomeGenerationSettings settingsFor(Holder<Biome> biome,
|
||||
Map<String, Holder<Biome>> derivatives) {
|
||||
Holder<Biome> mapped = derivatives.get(holderKey(biome));
|
||||
return mapped == null
|
||||
? biome.value().getGenerationSettings()
|
||||
: mapped.value().getGenerationSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the vanilla placed-feature pass for one chunk on the calling worldgen thread. A no-op while the
|
||||
* control is disabled or the table degraded.
|
||||
*/
|
||||
void run(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator owner) {
|
||||
RuntimeFeatureState state = currentState();
|
||||
FeatureTable table = state == null ? null : state.featureTable();
|
||||
if (table == null) {
|
||||
return;
|
||||
}
|
||||
ChunkPos centerPos = chunk.getPos();
|
||||
SectionPos sectionPos = SectionPos.of(centerPos, level.getMinSectionY());
|
||||
BlockPos origin = sectionPos.origin();
|
||||
Registry<PlacedFeature> featureRegistry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
|
||||
List<FeatureSorter.StepFeatureData> steps = table.steps();
|
||||
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ());
|
||||
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
||||
int staticMinY = engine.getMinHeight();
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
Set<Holder<Biome>> chunkBiomes = chunkBiomes(level, sectionPos, table, stackContext);
|
||||
IntBinaryOperator surfaceFirstFreeY = stackContext == null
|
||||
? (x, z) -> level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z)
|
||||
: (x, z) -> Engine.hostHeight(engine, x, z, false) + staticMinY + 1;
|
||||
IntBinaryOperator floorFirstFreeY = stackContext == null
|
||||
? (x, z) -> level.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z)
|
||||
: (x, z) -> Engine.hostHeight(engine, x, z, true) + staticMinY + 1;
|
||||
WorldGenLevel placementLevel = staticObjects.isEmpty() && stackContext == null
|
||||
? level
|
||||
: NativeStructureWorldgenAccess.create(
|
||||
level, centerPos,
|
||||
surfaceFirstFreeY,
|
||||
floorFirstFreeY,
|
||||
stackContext != null,
|
||||
NativeGenerationWriteGuard.protectedPositions(staticObjects, stackContext, staticMinY));
|
||||
|
||||
try {
|
||||
for (int stepIndex = 0; stepIndex < steps.size(); stepIndex++) {
|
||||
if (!table.control().shouldGenerateStep(IrisDecorationStep.byOrdinal(stepIndex))) {
|
||||
continue;
|
||||
}
|
||||
placeStep(placementLevel, table, steps.get(stepIndex), featureRegistry, chunkBiomes, owner,
|
||||
random, decorationSeed, origin, stepIndex);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
throw new IllegalStateException("Iris imported feature placement failed for chunk "
|
||||
+ centerPos.x() + "," + centerPos.z(), error);
|
||||
} finally {
|
||||
level.setCurrentlyGenerating(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
|
||||
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
|
||||
ChunkGenerator owner, WorldgenRandom random, long decorationSeed,
|
||||
BlockPos origin, int stepIndex) {
|
||||
IntSet stepFeatures = new IntArraySet();
|
||||
for (Holder<Biome> biome : chunkBiomes) {
|
||||
List<HolderSet<PlacedFeature>> biomeFeatures = settingsFor(biome, table.derivatives()).features();
|
||||
if (stepIndex >= biomeFeatures.size()) {
|
||||
continue;
|
||||
}
|
||||
for (Holder<PlacedFeature> feature : biomeFeatures.get(stepIndex)) {
|
||||
stepFeatures.add(stepData.indexMapping().applyAsInt(feature.value()));
|
||||
}
|
||||
}
|
||||
if (stepFeatures.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// Sorted global indices: identical ordering to vanilla, and each feature's seed comes from its own
|
||||
// global index, so denying one feature never shifts another.
|
||||
int[] featureIndices = stepFeatures.toIntArray();
|
||||
Arrays.sort(featureIndices);
|
||||
for (int globalIndex : featureIndices) {
|
||||
PlacedFeature feature = stepData.features().get(globalIndex);
|
||||
if (table.filtered()) {
|
||||
Identifier featureId = featureRegistry.getKey(feature);
|
||||
if (featureId != null && !table.control().shouldGenerate(featureId.toString())) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
random.setFeatureSeed(decorationSeed, globalIndex, stepIndex);
|
||||
level.setCurrentlyGenerating(() -> describeFeature(featureRegistry, feature));
|
||||
feature.placeWithBiomeCheck(level, owner, random, origin);
|
||||
}
|
||||
}
|
||||
|
||||
private static String describeFeature(Registry<PlacedFeature> registry, PlacedFeature feature) {
|
||||
Identifier id = registry.getKey(feature);
|
||||
return id == null ? feature.toString() : id.toString();
|
||||
}
|
||||
|
||||
private Set<Holder<Biome>> chunkBiomes(
|
||||
WorldGenLevel level,
|
||||
SectionPos sectionPos,
|
||||
FeatureTable table,
|
||||
DimensionStackContext stackContext
|
||||
) {
|
||||
List<Holder<Biome>> collected = new ArrayList<>();
|
||||
ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach((ChunkPos chunkPos) -> {
|
||||
ChunkAccess neighbour = level.getChunk(chunkPos.x(), chunkPos.z());
|
||||
LevelChunkSection[] sections = neighbour.getSections();
|
||||
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
|
||||
LevelChunkSection section = sections[sectionIndex];
|
||||
if (stackContext == null) {
|
||||
section.getBiomes().getAll(collected::add);
|
||||
continue;
|
||||
}
|
||||
int sectionMinimumY = neighbour.getSectionYFromSectionIndex(sectionIndex) << 4;
|
||||
for (int quartY = 0; quartY < 4; quartY++) {
|
||||
int internalY = sectionMinimumY + (quartY << 2) - level.getMinY();
|
||||
for (int quartX = 0; quartX < 4; quartX++) {
|
||||
int blockX = chunkPos.getMinBlockX() + (quartX << 2);
|
||||
for (int quartZ = 0; quartZ < 4; quartZ++) {
|
||||
int blockZ = chunkPos.getMinBlockZ() + (quartZ << 2);
|
||||
DimensionStackLayout.Layer layer = stackContext.getLayerAt(
|
||||
blockX, internalY, blockZ);
|
||||
if (layer.terrainContext().isSelfReferencing()) {
|
||||
collected.add(section.getBiomes().get(quartX, quartY, quartZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Set<Holder<Biome>> present = new LinkedHashSet<>();
|
||||
for (Holder<Biome> biome : collected) {
|
||||
if (table.biomeSet().contains(biome)) {
|
||||
present.add(biome);
|
||||
continue;
|
||||
}
|
||||
String key = holderKey(biome);
|
||||
Holder<Biome> canonical = key == null ? null : table.byKey().get(key);
|
||||
if (canonical != null) {
|
||||
present.add(canonical);
|
||||
}
|
||||
}
|
||||
return present;
|
||||
}
|
||||
|
||||
private static void addKey(Set<String> keys, String key) {
|
||||
String normalized = normalizeKey(key);
|
||||
if (normalized != null) {
|
||||
keys.add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
private String dimensionKey() {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
return dimension == null ? "<unbound>" : dimension.getLoadKey();
|
||||
}
|
||||
|
||||
private static String holderKey(Holder<Biome> holder) {
|
||||
return holder.unwrapKey()
|
||||
.map(key -> key.identifier().toString().toLowerCase(Locale.ROOT))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private static String normalizeKey(String key) {
|
||||
Identifier identifier = key == null ? null : Identifier.tryParse(key);
|
||||
return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private record FeatureTable(IrisDimension dimension, IrisImportedFeatureControl control,
|
||||
List<Holder<Biome>> biomes, Set<Holder<Biome>> biomeSet,
|
||||
Map<String, Holder<Biome>> byKey,
|
||||
List<FeatureSorter.StepFeatureData> steps,
|
||||
Map<String, Holder<Biome>> derivatives, boolean filtered) {
|
||||
}
|
||||
|
||||
private record RuntimeFeatureState(IrisDimension dimension, FeatureTable featureTable) {
|
||||
}
|
||||
}
|
||||
+1643
File diff suppressed because it is too large
Load Diff
+1644
File diff suppressed because it is too large
Load Diff
+142
@@ -0,0 +1,142 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import ca.spottedleaf.moonrise.common.util.TickThread;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiManager;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiSection;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiType;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ImposterProtoChunk;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
final class NativeStructurePoiUpdates {
|
||||
private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections");
|
||||
|
||||
private final int skipPoiFlag;
|
||||
|
||||
NativeStructurePoiUpdates(int skipPoiFlag) {
|
||||
this.skipPoiFlag = skipPoiFlag;
|
||||
}
|
||||
|
||||
static NativeStructurePoiUpdates create() {
|
||||
try {
|
||||
return new NativeStructurePoiUpdates(Block.class.getField("UPDATE_SKIP_POI").getInt(null));
|
||||
} catch (NoSuchFieldException e) {
|
||||
return null;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
}
|
||||
|
||||
boolean setBlock(WorldGenLevel world, BlockPos position, BlockState state, int flags, int limit) {
|
||||
if ((flags & skipPoiFlag) != 0) {
|
||||
return world.setBlock(position, state, flags, limit);
|
||||
}
|
||||
ChunkAccess chunk = world.getChunk(position);
|
||||
if (!(chunk instanceof ProtoChunk)) {
|
||||
return world.setBlock(position, state, flags, limit);
|
||||
}
|
||||
if (!(chunk instanceof ImposterProtoChunk)
|
||||
&& !PoiTypes.forState(chunk.getBlockState(position)).equals(PoiTypes.forState(state))) {
|
||||
markSection(chunk, chunk.getSectionIndex(position.getY()));
|
||||
}
|
||||
return world.setBlock(position, state, flags | skipPoiFlag, limit);
|
||||
}
|
||||
|
||||
static void reconcile(LevelChunk chunk) {
|
||||
long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
|
||||
if (sections == null) {
|
||||
return;
|
||||
}
|
||||
validateSections(chunk, sections);
|
||||
ServerLevel world = (ServerLevel) chunk.getLevel();
|
||||
TickThread.ensureTickThread(world, chunk.getPos(), "Native structure POIs require the chunk owning thread");
|
||||
PoiManager manager = world.getPoiManager();
|
||||
for (int word = 0; word < sections.length; word++) {
|
||||
long remaining = sections[word];
|
||||
while (remaining != 0) {
|
||||
int index = (word << 6) + Long.numberOfTrailingZeros(remaining);
|
||||
reconcileSection(chunk, manager, index);
|
||||
remaining &= remaining - 1;
|
||||
}
|
||||
}
|
||||
chunk.persistentDataContainer.remove(DIRTY_SECTIONS);
|
||||
chunk.markUnsaved();
|
||||
}
|
||||
|
||||
private static void markSection(ChunkAccess chunk, int index) {
|
||||
long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
|
||||
int word = index >>> 6;
|
||||
long bit = 1L << (index & 63);
|
||||
if (sections == null) {
|
||||
sections = new long[word + 1];
|
||||
} else {
|
||||
validateSections(chunk, sections);
|
||||
if (word < sections.length && (sections[word] & bit) != 0) {
|
||||
return;
|
||||
}
|
||||
if (word >= sections.length) {
|
||||
sections = Arrays.copyOf(sections, word + 1);
|
||||
}
|
||||
}
|
||||
sections[word] |= bit;
|
||||
chunk.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, sections);
|
||||
}
|
||||
|
||||
private static void validateSections(ChunkAccess chunk, long[] sections) {
|
||||
int count = chunk.getSectionsCount();
|
||||
int words = (count + 63) >>> 6;
|
||||
if (sections.length > words
|
||||
|| sections.length == words && (count & 63) != 0
|
||||
&& (sections[words - 1] >>> (count & 63)) != 0) {
|
||||
throw new IllegalArgumentException("Native POI repair sections exceed chunk height at " + chunk.getPos());
|
||||
}
|
||||
}
|
||||
|
||||
private static void reconcileSection(LevelChunk chunk, PoiManager manager, int index) {
|
||||
SectionPos sectionPosition = SectionPos.of(chunk.getPos(), chunk.getSectionYFromSectionIndex(index));
|
||||
Optional<PoiSection> existing = manager.getOrLoad(sectionPosition.asLong());
|
||||
if (existing.isPresent()) {
|
||||
for (PoiRecord record : existing.get().getRecords(type -> true, PoiManager.Occupancy.ANY).toList()) {
|
||||
if (!PoiTypes.forState(chunk.getBlockState(record.getPos())).equals(Optional.of(record.getPoiType()))) {
|
||||
manager.remove(record.getPos());
|
||||
}
|
||||
}
|
||||
}
|
||||
LevelChunkSection section = chunk.getSection(index);
|
||||
if (!section.maybeHas(PoiTypes::hasPoi)) {
|
||||
return;
|
||||
}
|
||||
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||
for (int y = 0; y < 16; y++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
Optional<Holder<PoiType>> type = PoiTypes.forState(section.getBlockState(x, y, z));
|
||||
if (type.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
position.set(sectionPosition.minBlockX() + x, sectionPosition.minBlockY() + y,
|
||||
sectionPosition.minBlockZ() + z);
|
||||
if (!manager.getType(position).equals(type)) {
|
||||
manager.add(position.immutable(), type.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+606
@@ -0,0 +1,606 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
|
||||
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.QuartPos;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.particles.ParticleOptions;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.DifficultyInstance;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributeReader;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.flag.FeatureFlagSet;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LightLayer;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.border.WorldBorder;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkSource;
|
||||
import net.minecraft.world.level.chunk.EmptyLevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.entity.EntityTypeTest;
|
||||
import net.minecraft.world.level.gameevent.GameEvent;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.lighting.LevelLightEngine;
|
||||
import net.minecraft.world.level.material.Fluid;
|
||||
import net.minecraft.world.level.material.FluidState;
|
||||
import net.minecraft.world.level.storage.LevelData;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraft.world.ticks.LevelTickAccess;
|
||||
import net.minecraft.world.ticks.ScheduledTick;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
private static final int WRITE_RADIUS = 1;
|
||||
private static final NativeStructurePoiUpdates POI_UPDATES = NativeStructurePoiUpdates.create();
|
||||
|
||||
private final WorldGenLevel delegate;
|
||||
private final ChunkPos generationCenter;
|
||||
private final IntBinaryOperator surfaceFirstFreeY;
|
||||
private final IntBinaryOperator floorFirstFreeY;
|
||||
private final boolean virtualizeHeights;
|
||||
private final Predicate<BlockPos> protectedPosition;
|
||||
private final Holder<Biome> fallbackBiome;
|
||||
private final BiomeManager biomeManager;
|
||||
private final AABB generationBounds;
|
||||
private final LevelTickAccess<Block> blockTicks;
|
||||
private final LevelTickAccess<Fluid> fluidTicks;
|
||||
private final Long2LongOpenHashMap terrainHeights;
|
||||
private final Long2ObjectOpenHashMap<ChunkAccess> outsideChunks;
|
||||
|
||||
private NativeStructureWorldgenAccess(WorldGenLevel delegate, Boundary boundary) {
|
||||
this.delegate = Objects.requireNonNull(delegate, "Native structure world access requires a delegate");
|
||||
this.generationCenter = Objects.requireNonNull(
|
||||
boundary.generationCenter(), "Native structure world access requires a generation center");
|
||||
this.surfaceFirstFreeY = Objects.requireNonNull(
|
||||
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
|
||||
this.floorFirstFreeY = Objects.requireNonNull(
|
||||
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
|
||||
this.virtualizeHeights = boundary.virtualizeHeights();
|
||||
this.protectedPosition = Objects.requireNonNull(
|
||||
boundary.protectedPosition(), "World access requires a protected-position predicate");
|
||||
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
|
||||
this.fallbackBiome = delegate.getBiome(biomeSample);
|
||||
this.biomeManager = delegate.getBiomeManager().withDifferentSource(this);
|
||||
this.generationBounds = new AABB(
|
||||
generationCenter.getMinBlockX() - 16,
|
||||
delegate.getMinY(),
|
||||
generationCenter.getMinBlockZ() - 16,
|
||||
generationCenter.getMaxBlockX() + 17,
|
||||
delegate.getMinY() + delegate.getHeight(),
|
||||
generationCenter.getMaxBlockZ() + 17);
|
||||
this.blockTicks = new BoundedTickAccess<>(delegate.getBlockTicks(), this::isWritable);
|
||||
this.fluidTicks = new BoundedTickAccess<>(delegate.getFluidTicks(), this::isWritable);
|
||||
this.terrainHeights = new Long2LongOpenHashMap();
|
||||
this.terrainHeights.defaultReturnValue(Long.MIN_VALUE);
|
||||
this.outsideChunks = new Long2ObjectOpenHashMap<>();
|
||||
}
|
||||
|
||||
static NativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
|
||||
IntBinaryOperator surfaceFirstFreeY,
|
||||
IntBinaryOperator floorFirstFreeY,
|
||||
boolean virtualizeHeights,
|
||||
Predicate<BlockPos> protectedPosition) {
|
||||
return new NativeStructureWorldgenAccess(delegate, new Boundary(
|
||||
generationCenter, surfaceFirstFreeY, floorFirstFreeY, virtualizeHeights, protectedPosition));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSeed() {
|
||||
return delegate.getSeed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ensureCanWrite(BlockPos position) {
|
||||
return isWritable(position) && delegate.ensureCanWrite(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentlyGenerating(Supplier<String> description) {
|
||||
delegate.setCurrentlyGenerating(description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerLevel getLevel() {
|
||||
return delegate.getLevel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DifficultyInstance getCurrentDifficultyAt(BlockPos position) {
|
||||
if (isReadable(position)) {
|
||||
return delegate.getCurrentDifficultyAt(position);
|
||||
}
|
||||
return delegate.getCurrentDifficultyAt(generationCenter.getMiddleBlockPosition(
|
||||
Mth.clamp(position.getY(), getMinY(), getMaxY() - 1)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long nextSubTickCount() {
|
||||
return delegate.nextSubTickCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LevelTickAccess<Block> getBlockTicks() {
|
||||
return blockTicks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LevelTickAccess<Fluid> getFluidTicks() {
|
||||
return fluidTicks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LevelData getLevelData() {
|
||||
return delegate.getLevelData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MinecraftServer getServer() {
|
||||
return delegate.getServer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkSource getChunkSource() {
|
||||
return delegate.getChunkSource();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RandomSource getRandom() {
|
||||
return delegate.getRandom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateNeighborsAt(BlockPos position, Block block) {
|
||||
if (isWritableNeighbourhood(position)) {
|
||||
delegate.updateNeighborsAt(position, block);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void neighborShapeChanged(Direction direction, BlockPos position,
|
||||
BlockPos neighbourPosition, BlockState neighbourState,
|
||||
int updateFlags, int updateLimit) {
|
||||
if (isWritable(position) && isWritable(neighbourPosition)) {
|
||||
delegate.neighborShapeChanged(
|
||||
direction, position, neighbourPosition, neighbourState, updateFlags, updateLimit);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playSound(Entity source, BlockPos position, SoundEvent sound,
|
||||
SoundSource soundSource, float volume, float pitch) {
|
||||
if (isWritable(position)) {
|
||||
delegate.playSound(source, position, sound, soundSource, volume, pitch);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addParticle(ParticleOptions particle, double x, double y, double z,
|
||||
double velocityX, double velocityY, double velocityZ) {
|
||||
if (isWritable(BlockPos.containing(x, y, z))) {
|
||||
delegate.addParticle(particle, x, y, z, velocityX, velocityY, velocityZ);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void levelEvent(Entity source, int eventId, BlockPos position, int data) {
|
||||
if (isWritable(position)) {
|
||||
delegate.levelEvent(source, eventId, position, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void gameEvent(Holder<GameEvent> event, Vec3 position, GameEvent.Context context) {
|
||||
if (isWritable(BlockPos.containing(position))) {
|
||||
delegate.gameEvent(event, position, context);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkAccess getChunk(int chunkX, int chunkZ) {
|
||||
if (isInsideGenerationRegion(chunkX, chunkZ)) {
|
||||
return delegate.getChunk(chunkX, chunkZ);
|
||||
}
|
||||
return outsideChunk(chunkX, chunkZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus status, boolean create) {
|
||||
if (isInsideGenerationRegion(chunkX, chunkZ)) {
|
||||
return delegate.getChunk(chunkX, chunkZ, status, create);
|
||||
}
|
||||
return create ? outsideChunk(chunkX, chunkZ) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkAccess getChunkIfLoadedImmediately(int chunkX, int chunkZ) {
|
||||
if (!isInsideGenerationRegion(chunkX, chunkZ)) {
|
||||
return null;
|
||||
}
|
||||
return delegate.getChunkIfLoadedImmediately(chunkX, chunkZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasChunk(int chunkX, int chunkZ) {
|
||||
return isInsideGenerationRegion(chunkX, chunkZ) && delegate.hasChunk(chunkX, chunkZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight(Heightmap.Types type, int x, int z) {
|
||||
if (!virtualizeHeights && isInsideGenerationRegion(x >> 4, z >> 4)) {
|
||||
return delegate.getHeight(type, x, z);
|
||||
}
|
||||
return height(type, x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSkyDarken() {
|
||||
return delegate.getSkyDarken();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiomeManager getBiomeManager() {
|
||||
return biomeManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ) {
|
||||
return getUncachedNoiseBiome(quartX, quartY, quartZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getUncachedNoiseBiome(int quartX, int quartY, int quartZ) {
|
||||
if (isInsideGenerationRegion(QuartPos.toSection(quartX), QuartPos.toSection(quartZ))) {
|
||||
return delegate.getUncachedNoiseBiome(quartX, quartY, quartZ);
|
||||
}
|
||||
return fallbackBiome;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClientSide() {
|
||||
return delegate.isClientSide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSeaLevel() {
|
||||
return delegate.getSeaLevel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DimensionType dimensionType() {
|
||||
return delegate.dimensionType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinY() {
|
||||
return delegate.getMinY();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return delegate.getHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistryAccess registryAccess() {
|
||||
return delegate.registryAccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FeatureFlagSet enabledFeatures() {
|
||||
return delegate.enabledFeatures();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EnvironmentAttributeReader environmentAttributes() {
|
||||
return delegate.environmentAttributes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LevelLightEngine getLightEngine() {
|
||||
return delegate.getLightEngine();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBrightness(LightLayer layer, BlockPos position) {
|
||||
if (isReadable(position)) {
|
||||
return delegate.getBrightness(layer, position);
|
||||
}
|
||||
return layer == LightLayer.SKY && canSeeSky(position) ? 15 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRawBrightness(BlockPos position, int ambientDarkening) {
|
||||
if (isReadable(position)) {
|
||||
return delegate.getRawBrightness(position, ambientDarkening);
|
||||
}
|
||||
return Math.max(0, getBrightness(LightLayer.SKY, position) - ambientDarkening);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSeeSky(BlockPos position) {
|
||||
if (isReadable(position)) {
|
||||
return delegate.canSeeSky(position);
|
||||
}
|
||||
return position.getY() >= height(Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ());
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorldBorder getWorldBorder() {
|
||||
return delegate.getWorldBorder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockGetter getChunkForCollisions(int chunkX, int chunkZ) {
|
||||
if (isInsideGenerationRegion(chunkX, chunkZ)) {
|
||||
return delegate.getChunkForCollisions(chunkX, chunkZ);
|
||||
}
|
||||
return outsideChunk(chunkX, chunkZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockEntity getBlockEntity(BlockPos position) {
|
||||
return isWritable(position) ? delegate.getBlockEntity(position) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends BlockEntity> Optional<T> getBlockEntity(
|
||||
BlockPos position, BlockEntityType<T> type) {
|
||||
return isWritable(position) ? delegate.getBlockEntity(position, type) : Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockState getBlockState(BlockPos position) {
|
||||
return isReadable(position) ? delegate.getBlockState(position) : terrainState(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockState getBlockStateIfLoaded(BlockPos position) {
|
||||
return isReadable(position) ? delegate.getBlockStateIfLoaded(position) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FluidState getFluidState(BlockPos position) {
|
||||
return isReadable(position) ? delegate.getFluidState(position) : terrainState(position).getFluidState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FluidState getFluidIfLoaded(BlockPos position) {
|
||||
return isReadable(position) ? delegate.getFluidIfLoaded(position) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Entity> getEntities(Entity source, AABB area, Predicate<? super Entity> predicate) {
|
||||
AABB boundedArea = boundedArea(area);
|
||||
return boundedArea == null ? List.of() : delegate.getEntities(source, boundedArea, predicate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Entity> List<T> getEntities(
|
||||
EntityTypeTest<Entity, T> type,
|
||||
AABB area, Predicate<? super T> predicate) {
|
||||
AABB boundedArea = boundedArea(area);
|
||||
return boundedArea == null ? List.of() : delegate.getEntities(type, boundedArea, predicate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends Player> players() {
|
||||
return delegate.players();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStateAtPosition(BlockPos position, Predicate<BlockState> predicate) {
|
||||
return predicate.test(getBlockState(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFluidAtPosition(BlockPos position, Predicate<FluidState> predicate) {
|
||||
return predicate.test(getFluidState(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockPos getHeightmapPos(Heightmap.Types type, BlockPos position) {
|
||||
return position.atY(getHeight(type, position.getX(), position.getZ()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setBlock(BlockPos position, BlockState state, int updateFlags, int updateLimit) {
|
||||
return isWritable(position)
|
||||
&& (POI_UPDATES != null
|
||||
? POI_UPDATES.setBlock(delegate, position, state, updateFlags, updateLimit)
|
||||
: delegate.setBlock(position, state, updateFlags, updateLimit));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeBlock(BlockPos position, boolean move) {
|
||||
if (!isWritable(position)) {
|
||||
return false;
|
||||
}
|
||||
return POI_UPDATES != null && delegate instanceof WorldGenRegion
|
||||
? setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, Block.UPDATE_LIMIT)
|
||||
: delegate.removeBlock(position, move);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean destroyBlock(BlockPos position, boolean drop, Entity source, int updateLimit) {
|
||||
if (!isWritable(position)) {
|
||||
return false;
|
||||
}
|
||||
return POI_UPDATES != null && delegate instanceof WorldGenRegion
|
||||
? !delegate.getBlockState(position).isAir()
|
||||
&& setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, updateLimit)
|
||||
: delegate.destroyBlock(position, drop, source, updateLimit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addFreshEntity(Entity entity) {
|
||||
return isWritable(entity.blockPosition()) && delegate.addFreshEntity(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addFreshEntity(Entity entity, CreatureSpawnEvent.SpawnReason reason) {
|
||||
return isWritable(entity.blockPosition()) && delegate.addFreshEntity(entity, reason);
|
||||
}
|
||||
|
||||
private boolean isReadable(BlockPos position) {
|
||||
return isInsideGenerationRegion(position.getX() >> 4, position.getZ() >> 4)
|
||||
&& !isOutsideBuildHeight(position);
|
||||
}
|
||||
|
||||
private boolean isWritable(BlockPos position) {
|
||||
return isReadable(position) && !protectedPosition.test(position);
|
||||
}
|
||||
|
||||
private boolean isWritableNeighbourhood(BlockPos position) {
|
||||
return isWritable(position)
|
||||
&& isWritable(position.north())
|
||||
&& isWritable(position.south())
|
||||
&& isWritable(position.east())
|
||||
&& isWritable(position.west())
|
||||
&& isWritable(position.above())
|
||||
&& isWritable(position.below());
|
||||
}
|
||||
|
||||
private AABB boundedArea(AABB area) {
|
||||
double minX = Math.max(area.minX, generationBounds.minX);
|
||||
double minY = Math.max(area.minY, generationBounds.minY);
|
||||
double minZ = Math.max(area.minZ, generationBounds.minZ);
|
||||
double maxX = Math.min(area.maxX, generationBounds.maxX);
|
||||
double maxY = Math.min(area.maxY, generationBounds.maxY);
|
||||
double maxZ = Math.min(area.maxZ, generationBounds.maxZ);
|
||||
if (minX >= maxX || minY >= maxY || minZ >= maxZ) {
|
||||
return null;
|
||||
}
|
||||
if (minX == area.minX && minY == area.minY && minZ == area.minZ
|
||||
&& maxX == area.maxX && maxY == area.maxY && maxZ == area.maxZ) {
|
||||
return area;
|
||||
}
|
||||
return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
}
|
||||
|
||||
boolean isInsideGenerationRegion(int chunkX, int chunkZ) {
|
||||
return Math.abs(chunkX - generationCenter.x()) <= WRITE_RADIUS
|
||||
&& Math.abs(chunkZ - generationCenter.z()) <= WRITE_RADIUS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOutsideBuildHeight(BlockPos position) {
|
||||
return position.getY() < getMinY() || position.getY() >= getMaxY();
|
||||
}
|
||||
|
||||
private BlockState terrainState(BlockPos position) {
|
||||
if (isOutsideBuildHeight(position)) {
|
||||
return Blocks.VOID_AIR.defaultBlockState();
|
||||
}
|
||||
long heights = heights(position.getX(), position.getZ());
|
||||
int surface = (int) (heights >> 32);
|
||||
int floor = (int) heights;
|
||||
if (position.getY() < floor) {
|
||||
return Blocks.STONE.defaultBlockState();
|
||||
}
|
||||
if (position.getY() < surface) {
|
||||
return Blocks.WATER.defaultBlockState();
|
||||
}
|
||||
return Blocks.AIR.defaultBlockState();
|
||||
}
|
||||
|
||||
private int height(Heightmap.Types type, int x, int z) {
|
||||
long heights = heights(x, z);
|
||||
if (type == Heightmap.Types.OCEAN_FLOOR || type == Heightmap.Types.OCEAN_FLOOR_WG) {
|
||||
return (int) heights;
|
||||
}
|
||||
return (int) (heights >> 32);
|
||||
}
|
||||
|
||||
private long heights(int x, int z) {
|
||||
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
long cached = terrainHeights.get(key);
|
||||
if (cached != Long.MIN_VALUE) {
|
||||
return cached;
|
||||
}
|
||||
int floor = Mth.clamp(floorFirstFreeY.applyAsInt(x, z), getMinY(), getMaxY());
|
||||
int surface = Mth.clamp(surfaceFirstFreeY.applyAsInt(x, z), floor, getMaxY());
|
||||
long heights = ((long) surface << 32) | (floor & 0xffffffffL);
|
||||
terrainHeights.put(key, heights);
|
||||
return heights;
|
||||
}
|
||||
|
||||
private ChunkAccess outsideChunk(int chunkX, int chunkZ) {
|
||||
long key = ChunkPos.pack(chunkX, chunkZ);
|
||||
ChunkAccess cached = outsideChunks.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
EmptyLevelChunk chunk = new EmptyLevelChunk(getLevel(), new ChunkPos(chunkX, chunkZ), fallbackBiome);
|
||||
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY, floorFirstFreeY);
|
||||
outsideChunks.put(key, chunk);
|
||||
return chunk;
|
||||
}
|
||||
|
||||
private record Boundary(ChunkPos generationCenter,
|
||||
IntBinaryOperator surfaceFirstFreeY,
|
||||
IntBinaryOperator floorFirstFreeY,
|
||||
boolean virtualizeHeights,
|
||||
Predicate<BlockPos> protectedPosition) {
|
||||
}
|
||||
|
||||
private static final class BoundedTickAccess<T> implements LevelTickAccess<T> {
|
||||
private final LevelTickAccess<T> delegate;
|
||||
private final Predicate<BlockPos> writable;
|
||||
|
||||
private BoundedTickAccess(LevelTickAccess<T> delegate, Predicate<BlockPos> writable) {
|
||||
this.delegate = delegate;
|
||||
this.writable = writable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void schedule(ScheduledTick<T> tick) {
|
||||
if (writable.test(tick.pos())) {
|
||||
delegate.schedule(tick);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasScheduledTick(BlockPos position, T type) {
|
||||
return writable.test(position) && delegate.hasScheduledTick(position, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int count() {
|
||||
return delegate.count();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean willTickThisTick(BlockPos position, T type) {
|
||||
return writable.test(position) && delegate.willTickThisTick(position, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.platform.agent.Agent;
|
||||
import it.unimi.dsi.fastutil.shorts.ShortList;
|
||||
import net.bytebuddy.ByteBuddy;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import net.bytebuddy.matcher.ElementMatchers;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import java.util.function.LongPredicate;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
final class NmsGenerationHooks {
|
||||
private NmsGenerationHooks() {
|
||||
}
|
||||
|
||||
static void install() {
|
||||
ByteBuddy buddy = new ByteBuddy();
|
||||
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
|
||||
buddy.redefine(clazz)
|
||||
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
|
||||
.make()
|
||||
.load(clazz.getClassLoader(), Agent.installed());
|
||||
}
|
||||
buddy.redefine(WorldGenRegion.class)
|
||||
.visit(Advice.to(WorldGenerationWriteAdvice.class).on(ElementMatchers.named("ensureCanWrite")
|
||||
.and(ElementMatchers.takesArguments(BlockPos.class))))
|
||||
.make()
|
||||
.load(WorldGenRegion.class.getClassLoader(), Agent.installed());
|
||||
}
|
||||
|
||||
private static class WorldGenerationWriteAdvice {
|
||||
@Advice.OnMethodExit
|
||||
static void exit(@Advice.This WorldGenRegion region, @Advice.Argument(0) BlockPos position,
|
||||
@Advice.Return(readOnly = false) boolean allowed) {
|
||||
if (!allowed) {
|
||||
return;
|
||||
}
|
||||
ChunkGenerator generator = region.getLevel().getChunkSource().getGenerator();
|
||||
if (generator instanceof LongPredicate predicate
|
||||
&& generator.getClass().getName().equals("art.arcane.iris.platform.bukkit.nms.v26_2_R1.IrisChunkGenerator")
|
||||
&& region.getChunk(position.getX() >> 4, position.getZ() >> 4)
|
||||
.getPersistedStatus().isOrAfter(ChunkStatus.FULL)) {
|
||||
allowed = predicate.test(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ChunkAccessAdvice {
|
||||
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||
static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) {
|
||||
return index >= access.getPostProcessing().length;
|
||||
}
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
final class NmsGenerationRegistry implements PlatformGenerationRegistry {
|
||||
private static final String BIOME_REGISTRY = "minecraft:worldgen/biome";
|
||||
private static final String STRUCTURE_REGISTRY = "minecraft:worldgen/structure";
|
||||
private static final String DIMENSION_TYPE_REGISTRY = "minecraft:dimension_type";
|
||||
private static final String BLOCK_REGISTRY = "minecraft:block";
|
||||
private static final String ENTITY_TYPE_REGISTRY = "minecraft:entity_type";
|
||||
|
||||
private final Supplier<RegistryAccess> registryAccess;
|
||||
private final String runtimeIdentity;
|
||||
private final String rendererIdentity;
|
||||
|
||||
NmsGenerationRegistry(
|
||||
Supplier<RegistryAccess> registryAccess,
|
||||
String runtimeIdentity,
|
||||
String rendererIdentity
|
||||
) {
|
||||
this.registryAccess = Objects.requireNonNull(registryAccess, "registryAccess");
|
||||
this.runtimeIdentity = requireText(runtimeIdentity, "runtimeIdentity");
|
||||
this.rendererIdentity = requireText(rendererIdentity, "rendererIdentity");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String runtimeIdentity() {
|
||||
return runtimeIdentity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generatedDefinitionRendererIdentity() {
|
||||
return rendererIdentity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String customBiomeResourceKey(String identitySha256) {
|
||||
return PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(identitySha256);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> legacyCustomBiomeResourceKeys(
|
||||
String packName,
|
||||
String dimensionKey,
|
||||
String customBiomeId
|
||||
) {
|
||||
return List.of(requireText(dimensionKey, "dimensionKey").toLowerCase(Locale.ROOT)
|
||||
+ ":" + requireText(customBiomeId, "customBiomeId").toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) {
|
||||
return "iris:" + requireText(dimensionTypeKey, "dimensionTypeKey").toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Definition definition(String registryKey, String resourceKey) {
|
||||
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
|
||||
return switch (registryKey) {
|
||||
case BIOME_REGISTRY -> presence(access, Registries.BIOME, registryKey, resourceKey);
|
||||
case STRUCTURE_REGISTRY -> presence(access, Registries.STRUCTURE, registryKey, resourceKey);
|
||||
case DIMENSION_TYPE_REGISTRY -> presence(
|
||||
access,
|
||||
Registries.DIMENSION_TYPE,
|
||||
registryKey,
|
||||
resourceKey
|
||||
);
|
||||
case BLOCK_REGISTRY -> presence(access, Registries.BLOCK, registryKey, resourceKey);
|
||||
case ENTITY_TYPE_REGISTRY -> presence(access, Registries.ENTITY_TYPE, registryKey, resourceKey);
|
||||
default -> throw new IllegalArgumentException("Unsupported generation registry: " + registryKey);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Definition canonicalDefinition(String registryKey, String resourceKey, String sourceJson) {
|
||||
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
|
||||
return switch (registryKey) {
|
||||
case BIOME_REGISTRY -> canonicalize(access, Biome.DIRECT_CODEC, sourceJson);
|
||||
case DIMENSION_TYPE_REGISTRY -> canonicalize(access, DimensionType.DIRECT_CODEC, sourceJson);
|
||||
default -> throw new IllegalArgumentException(
|
||||
"Registry does not accept generated JSON definitions: " + registryKey
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Definition generatedDefinition(String registryKey, String resourceKey) {
|
||||
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
|
||||
return switch (registryKey) {
|
||||
case BIOME_REGISTRY -> exactRegistered(
|
||||
access,
|
||||
Registries.BIOME,
|
||||
Biome.DIRECT_CODEC,
|
||||
resourceKey
|
||||
);
|
||||
case DIMENSION_TYPE_REGISTRY -> exactRegistered(
|
||||
access,
|
||||
Registries.DIMENSION_TYPE,
|
||||
DimensionType.DIRECT_CODEC,
|
||||
resourceKey
|
||||
);
|
||||
default -> throw new IllegalArgumentException(
|
||||
"Registry does not contain Iris-generated definitions: " + registryKey
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private static <T> Definition presence(
|
||||
RegistryAccess access,
|
||||
ResourceKey<? extends Registry<T>> registryKey,
|
||||
String registryResourceKey,
|
||||
String resourceKey
|
||||
) {
|
||||
Registry<T> registry = access.lookupOrThrow(registryKey);
|
||||
Identifier identifier = requireIdentifier(resourceKey);
|
||||
T value = registry.getValue(identifier);
|
||||
if (value == null) {
|
||||
throw new IllegalStateException("Missing registry definition " + registryKey.identifier()
|
||||
+ " / " + resourceKey + ".");
|
||||
}
|
||||
return Definition.resourceIdentity(registryResourceKey, resourceKey);
|
||||
}
|
||||
|
||||
private static <T> Definition canonicalize(RegistryAccess access, Codec<T> codec, String sourceJson) {
|
||||
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||
JsonElement source = JsonParser.parseString(Objects.requireNonNull(sourceJson, "sourceJson"));
|
||||
T decoded = codec.parse(ops, source).getOrThrow(IllegalArgumentException::new);
|
||||
JsonElement encoded = codec.encodeStart(ops, decoded).getOrThrow(IllegalStateException::new);
|
||||
return Definition.exactJson(encoded.toString());
|
||||
}
|
||||
|
||||
private static <T> Definition exactRegistered(
|
||||
RegistryAccess access,
|
||||
ResourceKey<? extends Registry<T>> registryKey,
|
||||
Codec<T> codec,
|
||||
String resourceKey
|
||||
) {
|
||||
Registry<T> registry = access.lookupOrThrow(registryKey);
|
||||
Identifier identifier = requireIdentifier(resourceKey);
|
||||
T value = registry.getValue(identifier);
|
||||
if (value == null) {
|
||||
throw new IllegalStateException("Missing generated registry definition "
|
||||
+ registryKey.identifier() + " / " + resourceKey + ".");
|
||||
}
|
||||
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||
JsonElement encoded = codec.encodeStart(ops, value).getOrThrow(IllegalStateException::new);
|
||||
return Definition.exactJson(encoded.toString());
|
||||
}
|
||||
|
||||
private static Identifier requireIdentifier(String resourceKey) {
|
||||
Identifier identifier = Identifier.tryParse(resourceKey);
|
||||
if (identifier == null) {
|
||||
throw new IllegalArgumentException("Invalid registry resource key: " + resourceKey);
|
||||
}
|
||||
return identifier;
|
||||
}
|
||||
|
||||
private static String requireText(String value, String label) {
|
||||
String required = Objects.requireNonNull(value, label).trim();
|
||||
if (required.isEmpty()) {
|
||||
throw new IllegalArgumentException(label + " must not be blank.");
|
||||
}
|
||||
return required;
|
||||
}
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.world.history.NativeTerrainReceipt;
|
||||
import art.arcane.iris.world.history.SavedTerrainChunk;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import ca.spottedleaf.concurrentutil.completable.Completable;
|
||||
import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
|
||||
import ca.spottedleaf.concurrentutil.util.Priority;
|
||||
import ca.spottedleaf.moonrise.common.PlatformHooks;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtIo;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
final class NmsSavedTerrainCapture {
|
||||
private static final long WRITE_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(30L);
|
||||
private static final int SNAPSHOT_BATCH_SIZE = 64;
|
||||
|
||||
private NmsSavedTerrainCapture() {
|
||||
}
|
||||
|
||||
static CompletableFuture<SavedTerrainChunk> capture(World world, int chunkX, int chunkZ, int minimumY, int height) {
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
CompletableFuture<CapturedChunk> snapshot = new CompletableFuture<>();
|
||||
J.runRegionFuture(world, chunkX, chunkZ, () -> {
|
||||
try {
|
||||
NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
|
||||
ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk();
|
||||
if (chunk == null) {
|
||||
snapshot.complete(new CapturedChunk(new ChunkPos(chunkX, chunkZ), CompletableFuture.completedFuture(null)));
|
||||
return;
|
||||
}
|
||||
String status = BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString();
|
||||
if (!SavedTerrainChunk.hasTerrain(status)) {
|
||||
throw new IOException("Native chunk " + chunkX + "," + chunkZ + " has no completed terrain at " + status);
|
||||
}
|
||||
snapshot.complete(scheduleSnapshot(level, chunk));
|
||||
} catch (Throwable failure) {
|
||||
snapshot.completeExceptionally(failure);
|
||||
}
|
||||
}).whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
snapshot.completeExceptionally(failure);
|
||||
}
|
||||
});
|
||||
return snapshot.thenApplyAsync(captured -> {
|
||||
try {
|
||||
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
|
||||
CompoundTag data = awaitSnapshot(level, captured, deadline);
|
||||
Checkpoint checkpoint = checkpoint(chunkX, chunkZ, data);
|
||||
awaitWrite(level, checkpoint.chunk(), deadline);
|
||||
if (data == null) {
|
||||
return SavedTerrainChunk.read(world.getWorldFolder().toPath(), chunkX, chunkZ, minimumY, height);
|
||||
}
|
||||
verifyCheckpoint(world.getWorldFolder().toPath(), checkpoint);
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
try (DataOutputStream output = new DataOutputStream(bytes)) {
|
||||
NbtIo.write(data, output);
|
||||
}
|
||||
return SavedTerrainChunk.readNbt(bytes.toByteArray(), chunkX, chunkZ, minimumY, height);
|
||||
} catch (IOException failure) {
|
||||
throw new CompletionException(failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static CompletableFuture<Void> flush(World world) {
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
List<NewChunkHolder> holders = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders();
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
flushSnapshots(world, level, holders);
|
||||
} catch (IOException failure) {
|
||||
throw new CompletionException(failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void flushSnapshots(World world, ServerLevel level, List<NewChunkHolder> holders) throws IOException {
|
||||
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
|
||||
List<Checkpoint> checkpoints = new ArrayList<>(holders.size());
|
||||
for (int offset = 0; offset < holders.size(); offset += SNAPSHOT_BATCH_SIZE) {
|
||||
int end = Math.min(holders.size(), offset + SNAPSHOT_BATCH_SIZE);
|
||||
List<CompletableFuture<CapturedChunk>> snapshots = requestSnapshots(world, level, holders.subList(offset, end));
|
||||
awaitSnapshots(snapshots, deadline);
|
||||
for (CompletableFuture<CapturedChunk> future : snapshots) {
|
||||
CapturedChunk captured = future.join();
|
||||
CompoundTag data = awaitSnapshot(level, captured, deadline);
|
||||
checkpoints.add(checkpoint(captured.chunk().x(), captured.chunk().z(), data));
|
||||
}
|
||||
for (int index = offset; index < end; index++) {
|
||||
awaitWrite(level, checkpoints.get(index).chunk(), deadline);
|
||||
}
|
||||
}
|
||||
finishCheckpoint(level, world.getWorldFolder().toPath(), checkpoints);
|
||||
}
|
||||
|
||||
private static List<CompletableFuture<CapturedChunk>> requestSnapshots(
|
||||
World world, ServerLevel level, List<NewChunkHolder> holders) {
|
||||
List<CompletableFuture<CapturedChunk>> snapshots = new ArrayList<>(holders.size());
|
||||
for (NewChunkHolder original : holders) {
|
||||
ChunkPos position = new ChunkPos(original.chunkX, original.chunkZ);
|
||||
CompletableFuture<CapturedChunk> snapshot = new CompletableFuture<>();
|
||||
snapshots.add(snapshot);
|
||||
J.runRegionFuture(world, position.x(), position.z(), () -> {
|
||||
try {
|
||||
NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager
|
||||
.getChunkHolder(position.x(), position.z());
|
||||
ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk();
|
||||
snapshot.complete(chunk != null && chunk.isUnsaved() ? scheduleSnapshot(level, chunk)
|
||||
: new CapturedChunk(position, CompletableFuture.completedFuture(null)));
|
||||
} catch (Throwable failure) {
|
||||
snapshot.completeExceptionally(failure);
|
||||
}
|
||||
}).whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
snapshot.completeExceptionally(failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
private static void awaitSnapshots(List<CompletableFuture<CapturedChunk>> snapshots, long deadlineNanos)
|
||||
throws IOException {
|
||||
try {
|
||||
CompletableFuture.allOf(snapshots.toArray(CompletableFuture[]::new))
|
||||
.get(Math.max(0L, deadlineNanos - System.nanoTime()), TimeUnit.NANOSECONDS);
|
||||
} catch (InterruptedException failure) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Interrupted while copying terrain checkpoint chunks.", failure);
|
||||
} catch (ExecutionException | TimeoutException failure) {
|
||||
throw new IOException("Unable to copy terrain checkpoint chunks.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
static void finishCheckpoint(ServerLevel level, Path worldFolder, List<Checkpoint> checkpoints) throws IOException {
|
||||
long deadline = System.nanoTime() + WRITE_TIMEOUT_NANOS;
|
||||
for (Checkpoint checkpoint : checkpoints) {
|
||||
awaitWrite(level, checkpoint.chunk(), deadline);
|
||||
}
|
||||
MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
|
||||
for (Checkpoint checkpoint : checkpoints) {
|
||||
verifyCheckpoint(worldFolder, checkpoint);
|
||||
}
|
||||
}
|
||||
|
||||
static Checkpoint checkpoint(int chunkX, int chunkZ, CompoundTag data) {
|
||||
if (data == null) {
|
||||
return new Checkpoint(new ChunkPos(chunkX, chunkZ), null);
|
||||
}
|
||||
byte[] receipt = data.getCompound("ChunkBukkitValues")
|
||||
.flatMap(values -> values.getByteArray(NativeTerrainReceipt.NBT_KEY)).orElse(null);
|
||||
long structureActivation = data.getCompound("ChunkBukkitValues").map(values -> values.getLongOr(
|
||||
NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY, 0)).orElse(0L);
|
||||
return new Checkpoint(new ChunkPos(chunkX, chunkZ), new Verification(
|
||||
data.getStringOr("Status", ""), receipt, structureActivation));
|
||||
}
|
||||
|
||||
private static void verifyCheckpoint(Path worldFolder, Checkpoint checkpoint) throws IOException {
|
||||
Verification expected = checkpoint.verification();
|
||||
if (expected == null) {
|
||||
return;
|
||||
}
|
||||
SavedTerrainChunk.verifyCheckpoint(worldFolder, checkpoint.chunk().x(), checkpoint.chunk().z(),
|
||||
expected.status(), expected.receipt(), expected.structureActivation());
|
||||
}
|
||||
|
||||
static void awaitWrite(ServerLevel level, ChunkPos chunk, long deadlineNanos) throws IOException {
|
||||
ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler();
|
||||
while (true) {
|
||||
checkWriteWait(chunk, deadlineNanos);
|
||||
if (MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(),
|
||||
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA) == Priority.COMPLETING) {
|
||||
return;
|
||||
}
|
||||
helpSaveTasks(scheduler);
|
||||
}
|
||||
}
|
||||
|
||||
static CapturedChunk scheduleSnapshot(ServerLevel level, ChunkAccess chunk) {
|
||||
SerializableChunkData snapshot = SerializableChunkData.copyOf(level, chunk);
|
||||
PlatformHooks.get().chunkSyncSave(level, chunk, snapshot);
|
||||
Completable<CompoundTag> nativeData = new Completable<>();
|
||||
CompletableFuture<CompoundTag> data = new CompletableFuture<>();
|
||||
PrioritisedExecutor.PrioritisedTask task = level.moonrise$getChunkTaskScheduler().saveExecutor
|
||||
.createTask(() -> serializeSnapshot(snapshot, nativeData, data));
|
||||
task.queue();
|
||||
ChunkPos position = chunk.getPos();
|
||||
MoonriseRegionFileIO.scheduleSave(level, position.x(), position.z(), nativeData, task,
|
||||
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, Priority.NORMAL);
|
||||
return new CapturedChunk(position, data);
|
||||
}
|
||||
|
||||
static CompoundTag awaitSnapshot(ServerLevel level, CapturedChunk captured, long deadlineNanos) throws IOException {
|
||||
ChunkTaskScheduler scheduler = level.moonrise$getChunkTaskScheduler();
|
||||
while (!captured.data().isDone()) {
|
||||
checkWriteWait(captured.chunk(), deadlineNanos);
|
||||
helpSaveTasks(scheduler);
|
||||
}
|
||||
try {
|
||||
return captured.data().join();
|
||||
} catch (CompletionException failure) {
|
||||
throw new IOException("Unable to serialize terrain checkpoint chunk " + captured.chunk(), failure.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
private static void serializeSnapshot(SerializableChunkData snapshot, Completable<CompoundTag> nativeData,
|
||||
CompletableFuture<CompoundTag> data) {
|
||||
try {
|
||||
CompoundTag serialized = snapshot.write();
|
||||
nativeData.complete(serialized);
|
||||
data.complete(serialized);
|
||||
} catch (Throwable failure) {
|
||||
nativeData.completeExceptionally(failure);
|
||||
data.completeExceptionally(failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkWriteWait(ChunkPos chunk, long deadlineNanos) throws IOException {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new IOException("Interrupted while saving terrain checkpoint chunk " + chunk);
|
||||
}
|
||||
if (System.nanoTime() - deadlineNanos >= 0L) {
|
||||
throw new IOException("Timed out saving terrain checkpoint chunk " + chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private static void helpSaveTasks(ChunkTaskScheduler scheduler) {
|
||||
boolean helped = scheduler.saveExecutor.executeTask();
|
||||
helped |= scheduler.compressionExecutor.executeTask();
|
||||
if (!helped) {
|
||||
LockSupport.parkNanos(1_000_000L);
|
||||
}
|
||||
}
|
||||
|
||||
record CapturedChunk(ChunkPos chunk, CompletableFuture<CompoundTag> data) {
|
||||
}
|
||||
|
||||
record Checkpoint(ChunkPos chunk, Verification verification) {
|
||||
}
|
||||
|
||||
record Verification(String status, byte[] receipt, long structureActivation) {
|
||||
}
|
||||
}
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.nms.ServerShutdownBoundary;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.agent.Agent;
|
||||
import net.bytebuddy.agent.builder.AgentBuilder;
|
||||
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import net.bytebuddy.description.type.TypeDescription;
|
||||
import net.bytebuddy.dynamic.DynamicType;
|
||||
import net.bytebuddy.matcher.ElementMatchers;
|
||||
import net.bytebuddy.utility.JavaModule;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.craftbukkit.CraftServer;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
final class NmsWorldLifecycle {
|
||||
private final AtomicBoolean injected = new AtomicBoolean();
|
||||
private volatile ResettableClassFileTransformer levelStorageAccessTransformer;
|
||||
private volatile ResettableClassFileTransformer serverLevelTransformer;
|
||||
private volatile ResettableClassFileTransformer pluginClassLoaderTransformer;
|
||||
private boolean pluginClassLoaderCloseDeferred;
|
||||
|
||||
public boolean injectBukkit() {
|
||||
synchronized (injected) {
|
||||
if (injected.get()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
IrisLogging.info("Injecting Bukkit");
|
||||
Agent.requireClassLoaderCloseDeferral();
|
||||
Class<?> loaderCloseType = getClass().getClassLoader().getClass()
|
||||
.getMethod("close").getDeclaringClass();
|
||||
PluginClassLoaderInjectionListener loaderListener = new PluginClassLoaderInjectionListener();
|
||||
pluginClassLoaderTransformer = new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.with(AgentBuilder.RedefinitionStrategy.Listener.ErrorEscalating.FAIL_FAST)
|
||||
.with(loaderListener)
|
||||
.type(ElementMatchers.is(loaderCloseType))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(PluginClassLoaderCloseAdvice.class)
|
||||
.on(ElementMatchers.named("close")
|
||||
.and(ElementMatchers.takesArguments(0))
|
||||
.and(ElementMatchers.returns(void.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
loaderListener.requireInstalled();
|
||||
levelStorageAccessTransformer = new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.type(ElementMatchers.is(LevelStorageSource.LevelStorageAccess.class))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(LevelStorageAccessAdvice.class).on(ElementMatchers.isConstructor()
|
||||
.and(ElementMatchers.takesArguments(4))
|
||||
.and(ElementMatchers.takesArgument(0, LevelStorageSource.class))
|
||||
.and(ElementMatchers.takesArgument(1, String.class))
|
||||
.and(ElementMatchers.takesArgument(2, Path.class))
|
||||
.and(ElementMatchers.takesArgument(3, ResourceKey.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
serverLevelTransformer = new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.type(ElementMatchers.is(ServerLevel.class))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
|
||||
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
|
||||
.and(ElementMatchers.takesArgument(5, LevelStem.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
NmsGenerationHooks.install();
|
||||
|
||||
injected.set(true);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e);
|
||||
ResettableClassFileTransformer partialServerLevel = serverLevelTransformer;
|
||||
ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer;
|
||||
ResettableClassFileTransformer partialPluginClassLoader = pluginClassLoaderTransformer;
|
||||
serverLevelTransformer = null;
|
||||
levelStorageAccessTransformer = null;
|
||||
pluginClassLoaderTransformer = null;
|
||||
for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{
|
||||
partialServerLevel,
|
||||
partialStorageAccess,
|
||||
partialPluginClassLoader
|
||||
}) {
|
||||
if (partial == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
||||
} catch (Throwable cleanupFailure) {
|
||||
IrisLogging.reportError("Failed to remove partial Bukkit lifecycle injection", cleanupFailure);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ensureServerLevelInjection() {
|
||||
if (!injected.get()) {
|
||||
throw new IllegalStateException("Iris world lifecycle injection is unavailable. Fix the Java Agent or Code Injection startup failure and restart before creating worlds.");
|
||||
}
|
||||
try {
|
||||
Agent.getInstrumentation().retransformClasses(
|
||||
LevelStorageSource.LevelStorageAccess.class,
|
||||
ServerLevel.class
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException("Failed to re-apply Bukkit world lifecycle injection. World creation was stopped before loading the world.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void uninjectBukkit() {
|
||||
synchronized (injected) {
|
||||
ResettableClassFileTransformer activeServerLevel = serverLevelTransformer;
|
||||
ResettableClassFileTransformer activeStorageAccess = levelStorageAccessTransformer;
|
||||
serverLevelTransformer = null;
|
||||
levelStorageAccessTransformer = null;
|
||||
injected.set(false);
|
||||
for (ResettableClassFileTransformer transformer : new ResettableClassFileTransformer[]{
|
||||
activeServerLevel,
|
||||
activeStorageAccess
|
||||
}) {
|
||||
if (transformer == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(C.RED + "Failed to remove Bukkit world lifecycle injection", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isServerStopping() {
|
||||
return ((CraftServer) Bukkit.getServer()).getServer().hasStopped();
|
||||
}
|
||||
|
||||
public ServerShutdownBoundary createServerShutdownBoundary() {
|
||||
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
|
||||
return new ServerShutdownBoundary(
|
||||
() -> server.hasFullyShutdown,
|
||||
server.getRunningThread()
|
||||
);
|
||||
}
|
||||
|
||||
public void deferPluginClassLoaderClose() {
|
||||
synchronized (injected) {
|
||||
if (pluginClassLoaderTransformer == null || pluginClassLoaderCloseDeferred) {
|
||||
return;
|
||||
}
|
||||
Agent.retainClassLoader(getClass().getClassLoader());
|
||||
pluginClassLoaderCloseDeferred = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void releasePluginClassLoaderClose() {
|
||||
ResettableClassFileTransformer transformer;
|
||||
boolean releaseLoader;
|
||||
synchronized (injected) {
|
||||
transformer = pluginClassLoaderTransformer;
|
||||
pluginClassLoaderTransformer = null;
|
||||
releaseLoader = pluginClassLoaderCloseDeferred;
|
||||
pluginClassLoaderCloseDeferred = false;
|
||||
}
|
||||
try {
|
||||
if (transformer != null
|
||||
&& !transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)) {
|
||||
throw new IllegalStateException("Failed to remove Iris plugin class loader lifecycle injection");
|
||||
}
|
||||
} finally {
|
||||
if (releaseLoader) {
|
||||
Agent.releaseClassLoader(getClass().getClassLoader());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PluginClassLoaderInjectionListener extends AgentBuilder.Listener.Adapter {
|
||||
private volatile boolean transformed;
|
||||
private volatile Throwable failure;
|
||||
|
||||
@Override
|
||||
public void onTransformation(TypeDescription type, ClassLoader loader, JavaModule module,
|
||||
boolean loaded, DynamicType dynamicType) {
|
||||
transformed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String typeName, ClassLoader loader, JavaModule module,
|
||||
boolean loaded, Throwable throwable) {
|
||||
failure = throwable;
|
||||
}
|
||||
|
||||
private void requireInstalled() {
|
||||
if (!transformed || failure != null) {
|
||||
throw new IllegalStateException("Iris plugin class loader lifecycle injection failed", failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class PluginClassLoaderCloseAdvice {
|
||||
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||
static boolean enter(@Advice.This ClassLoader loader) throws ReflectiveOperationException {
|
||||
Class<?> installer = Class.forName(
|
||||
"art.arcane.iris.platform.agent.Installer", true, ClassLoader.getSystemClassLoader());
|
||||
return Boolean.TRUE.equals(installer.getMethod("deferClassLoaderClose", ClassLoader.class)
|
||||
.invoke(null, loader));
|
||||
}
|
||||
}
|
||||
|
||||
private static class LevelStorageAccessAdvice {
|
||||
@Advice.OnMethodEnter
|
||||
static void enter(
|
||||
@Advice.Argument(1) String levelId,
|
||||
@Advice.Argument(value = 3, readOnly = false) ResourceKey<LevelStem> dimensionType
|
||||
) {
|
||||
if (levelId == null || levelId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClassLoader pluginClassLoader;
|
||||
Class<?> generatorType;
|
||||
Class<?> stagingType;
|
||||
try {
|
||||
Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
|
||||
if (irisPlugin == null) {
|
||||
return;
|
||||
}
|
||||
pluginClassLoader = irisPlugin.getClass().getClassLoader();
|
||||
generatorType = Class.forName("art.arcane.iris.platform.generation.PlatformChunkGenerator", true, pluginClassLoader);
|
||||
stagingType = Class.forName("art.arcane.iris.world.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
|
||||
} catch (Throwable ignored) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object generator;
|
||||
try {
|
||||
generator = stagingType
|
||||
.getDeclaredMethod("peekStemGenerator", String.class)
|
||||
.invoke(null, levelId);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to inspect the staged world generator",
|
||||
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
if (generator == null || !generatorType.isInstance(generator)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Object target = generatorType.getMethod("getTarget").invoke(generator);
|
||||
if (target == null) {
|
||||
throw new IllegalStateException("Iris generator has no engine target.");
|
||||
}
|
||||
Object world = target.getClass().getMethod("getWorld").invoke(target);
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris generator target has no world identity.");
|
||||
}
|
||||
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
|
||||
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
|
||||
Identifier worldIdentifier = Identifier.parse(worldIdentity);
|
||||
if (!"iris".equals(worldIdentifier.getNamespace())
|
||||
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
|
||||
throw new IllegalStateException(
|
||||
"Iris generator has an unmanaged world identity: " + worldIdentity);
|
||||
}
|
||||
dimensionType = ResourceKey.create(Registries.LEVEL_STEM, worldIdentifier);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to bind the staged world storage identity",
|
||||
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ServerLevelAdvice {
|
||||
@Advice.OnMethodEnter
|
||||
static void enter(
|
||||
@Advice.Argument(0) MinecraftServer server,
|
||||
@Advice.Argument(value = 4, readOnly = false) ResourceKey<Level> dimensionKey,
|
||||
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
|
||||
@Advice.AllArguments Object[] constructorArguments
|
||||
) {
|
||||
if (dimensionKey == null)
|
||||
return;
|
||||
|
||||
// This advice is inlined into every ServerLevel construction on the server. Until a
|
||||
// world is proven Iris-owned, every failure must fail OPEN (keep the vanilla stem):
|
||||
// Iris being unloaded or half-loaded must never break other plugins' world creation.
|
||||
String levelId;
|
||||
ClassLoader pluginClassLoader;
|
||||
Class<?> generatorType;
|
||||
Class<?> stagingType;
|
||||
try {
|
||||
levelId = dimensionKey.identifier().getPath();
|
||||
if (levelId == null || levelId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
|
||||
if (irisPlugin == null) {
|
||||
return;
|
||||
}
|
||||
pluginClassLoader = irisPlugin.getClass().getClassLoader();
|
||||
generatorType = Class.forName("art.arcane.iris.platform.generation.PlatformChunkGenerator", true, pluginClassLoader);
|
||||
stagingType = Class.forName("art.arcane.iris.world.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
|
||||
} catch (Throwable ignored) {
|
||||
// Iris absent or half-loaded: fail OPEN for a world we cannot prove ours.
|
||||
return;
|
||||
}
|
||||
|
||||
// From here Iris is present and its classes resolve; a failure resolving the
|
||||
// staged generator must fail LOUD — silently handing a possibly-staged Iris world
|
||||
// the vanilla stem corrupts generation.
|
||||
ChunkGenerator gen = null;
|
||||
try {
|
||||
ChunkGenerator constructorGenerator = null;
|
||||
for (Object argument : constructorArguments) {
|
||||
if (argument instanceof ChunkGenerator candidate) {
|
||||
constructorGenerator = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
|
||||
if (generator == null) {
|
||||
generator = stagingType
|
||||
.getDeclaredMethod("consumeStemGenerator", String.class)
|
||||
.invoke(null, levelId);
|
||||
}
|
||||
if (generator instanceof ChunkGenerator owned && owned.getClass().getPackageName().startsWith("art.arcane.iris")) {
|
||||
gen = owned;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to resolve the staged world generator",
|
||||
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
if (gen == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Past the ownership gate the world is Iris-owned; silently handing back the
|
||||
// vanilla stem would corrupt generation, so failures from here rethrow.
|
||||
try {
|
||||
Object bindings = Class.forName("art.arcane.iris.platform.bukkit.nms.INMS", true, pluginClassLoader)
|
||||
.getDeclaredMethod("get")
|
||||
.invoke(null);
|
||||
if (bindings == null) {
|
||||
throw new IllegalStateException("Iris failed to resolve an INMSBinding instance.");
|
||||
}
|
||||
|
||||
Method stemMethod = null;
|
||||
for (Method candidate : bindings.getClass().getMethods()) {
|
||||
if (candidate.getName().equals("createRuntimeLevelStem") && candidate.getParameterCount() == 2) {
|
||||
stemMethod = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stemMethod == null) {
|
||||
throw new IllegalStateException("Iris binding is missing createRuntimeLevelStem.");
|
||||
}
|
||||
Object target = generatorType.getMethod("getTarget").invoke(gen);
|
||||
if (target == null) {
|
||||
throw new IllegalStateException("Iris generator has no engine target.");
|
||||
}
|
||||
Object world = target.getClass().getMethod("getWorld").invoke(target);
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris generator target has no world identity.");
|
||||
}
|
||||
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
|
||||
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
|
||||
Identifier worldIdentifier = Identifier.parse(worldIdentity);
|
||||
if (!"iris".equals(worldIdentifier.getNamespace())
|
||||
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
|
||||
throw new IllegalStateException(
|
||||
"Iris generator has an unmanaged world identity: " + worldIdentity);
|
||||
}
|
||||
Object resolvedStem = stemMethod.invoke(bindings, server.registryAccess(), gen);
|
||||
if (!(resolvedStem instanceof LevelStem runtimeStem)) {
|
||||
throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + ".");
|
||||
}
|
||||
dimensionKey = ResourceKey.create(Registries.DIMENSION, worldIdentifier);
|
||||
levelStem = runtimeStem;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.nativegen.NativeStructureGenerationKeys;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
final class VanillaStructureBiomes {
|
||||
private VanillaStructureBiomes() {
|
||||
}
|
||||
|
||||
static Set<String> possibleBiomeKeys(BiomeSource source) {
|
||||
if (source == null) {
|
||||
throw new IllegalStateException("Minecraft chunk generator has no biome source");
|
||||
}
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
Set<Holder<Biome>> possibleBiomes = source instanceof CustomBiomeSource customBiomeSource
|
||||
? customBiomeSource.possibleStructureBiomes()
|
||||
: source.possibleBiomes();
|
||||
for (Holder<Biome> holder : possibleBiomes) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent()) {
|
||||
keys.add(key.get().identifier().toString());
|
||||
}
|
||||
}
|
||||
if (keys.isEmpty()) {
|
||||
throw new IllegalStateException("Minecraft biome source exposes no registered possible biomes");
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
static Set<String> structureBiomeKeys(RegistryAccess access, String structureKey) {
|
||||
if (access == null) {
|
||||
throw new IllegalStateException("Minecraft registry access is unavailable");
|
||||
}
|
||||
if (structureKey == null || structureKey.isBlank()) {
|
||||
throw new IllegalArgumentException("Registered structure key must not be blank");
|
||||
}
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
Registry<Structure> registry = access.lookupOrThrow(Registries.STRUCTURE);
|
||||
Structure structure = registry.getValue(Identifier.parse(structureKey));
|
||||
if (structure == null) {
|
||||
throw new IllegalArgumentException("Registered structure does not exist: " + structureKey);
|
||||
}
|
||||
boolean hasFilterEntries = false;
|
||||
for (Holder<Biome> holder : structure.biomes()) {
|
||||
hasFilterEntries = true;
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent()) {
|
||||
keys.add(key.get().identifier().toString());
|
||||
}
|
||||
}
|
||||
// An empty biome filter is legal datapack content (opt-in structures whose tags only
|
||||
// reference absent modded biomes); the structure is unreachable, not an error state.
|
||||
if (keys.isEmpty() && hasFilterEntries) {
|
||||
throw new IllegalStateException("Registered structure '" + structureKey
|
||||
+ "' has biome filter entries but none resolve to registered biome keys");
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
static Set<String> reachableStructureKeys(ServerLevel level, BiomeSource source) {
|
||||
if (level == null) {
|
||||
throw new IllegalStateException("Minecraft server level is unavailable");
|
||||
}
|
||||
Set<String> possible = possibleBiomeKeys(source);
|
||||
return NativeStructureGenerationKeys.reachable(level, possible);
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.nms.v26_2_R1.IrisChunkGenerator;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public final class IrisChunkGeneratorHeightCacheTest {
|
||||
@Rule
|
||||
public final PlatformLeakGuard leakGuard = PlatformLeakGuard.clean();
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedNativeHeightSurvivesResolvedTerrainEvictionAndRuntimeChange() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
when(fixture.complex().resolvedTerrainColumn(-1, -33))
|
||||
.thenThrow(new IllegalStateException("Full terrain was evicted"));
|
||||
|
||||
for (int index = 0; index < 100; index++) {
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(
|
||||
-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
}
|
||||
verify(fixture.complex(), times(1)).resolvedTerrainColumn(-1, -33);
|
||||
|
||||
when(fixture.engine().getCacheID()).thenReturn(2);
|
||||
doReturn(Optional.of(signature())).when(fixture.complex()).resolvedTerrainColumn(-1, -33);
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(-1, -33, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
verify(fixture.complex(), times(2)).resolvedTerrainColumn(-1, -33);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ordinaryHeightRemainsLiveUntilResolvedTransitionTerrainExists() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||
when(fixture.complex().resolvedTerrainColumn(0, 0))
|
||||
.thenReturn(Optional.empty(), Optional.empty(), Optional.of(signature()));
|
||||
when(fixture.engine().getHeight(0, 0, false)).thenReturn(20, 25);
|
||||
|
||||
assertEquals(5, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(10, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
verify(fixture.engine(), times(2)).getHeight(0, 0, false);
|
||||
verify(fixture.complex(), times(3)).resolvedTerrainColumn(0, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativePredicatesAndAccessorBoundsRemainExact() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
LevelHeightAccessor bounds = LevelHeightAccessor.create(-16, 32);
|
||||
for (int repeat = 0; repeat < 2; repeat++) {
|
||||
assertEquals(-11, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE, bounds, null));
|
||||
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.OCEAN_FLOOR, bounds, null));
|
||||
assertEquals(-13, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING, bounds, null));
|
||||
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, bounds, null));
|
||||
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.WORLD_SURFACE,
|
||||
LevelHeightAccessor.create(-16, 2), null));
|
||||
assertEquals(-14, fixture.generator().getBaseHeight(0, 0, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
|
||||
LevelHeightAccessor.create(-14, 3), null));
|
||||
}
|
||||
verify(fixture.complex(), times(6)).resolvedTerrainColumn(0, 0);
|
||||
}
|
||||
|
||||
private static Fixture fixture() throws Exception {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getCacheID()).thenReturn(1);
|
||||
when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop());
|
||||
when(complex.resolvedTerrainColumn(anyInt(), anyInt())).thenReturn(Optional.of(signature()));
|
||||
IrisChunkGenerator generator = mock(IrisChunkGenerator.class, CALLS_REAL_METHODS);
|
||||
set(generator, "engine", engine);
|
||||
set(generator, "terrainHeights", new NativeTerrainHeightCache());
|
||||
return new Fixture(generator, engine, complex);
|
||||
}
|
||||
|
||||
private static void set(IrisChunkGenerator generator, String name, Object value) throws Exception {
|
||||
Field field = IrisChunkGenerator.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(generator, value);
|
||||
}
|
||||
|
||||
private static TerrainBoundarySignature signature() {
|
||||
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-16, List.of(
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:water", BoundaryColumnGeometry.Phase.FLUID, "minecraft:water", false),
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:oak_leaves[persistent=true]", BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:short_grass", BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:light[level=15]", BoundaryColumnGeometry.Phase.SOLID, "", false)));
|
||||
return new TerrainBoundarySignature(
|
||||
new TerrainBoundarySignature.Column(0, 0, 4, 4, OptionalInt.of(1), OptionalInt.empty()),
|
||||
new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(-16, 5, 1),
|
||||
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})), geometry);
|
||||
}
|
||||
|
||||
private record Fixture(IrisChunkGenerator generator, Engine engine, IrisComplex complex) {
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.generation.runtime.DimensionStackContext;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.world.history.GenerationActivation;
|
||||
import art.arcane.iris.world.history.GenerationHistory;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.object.IrisStaticObjectLayer;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public final class NativeGenerationWriteGuardTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void staticObjectsProtectRelativeHeightBeforeStackSampling() {
|
||||
IrisStaticObjectLayer objects = mock(IrisStaticObjectLayer.class);
|
||||
DimensionStackContext stack = mock(DimensionStackContext.class);
|
||||
when(objects.contains(-3, 74, -7)).thenReturn(true);
|
||||
|
||||
Predicate<BlockPos> protectedPositions = NativeGenerationWriteGuard.protectedPositions(objects, stack, -64);
|
||||
|
||||
assertTrue(protectedPositions.test(new BlockPos(-3, 10, -7)));
|
||||
verifyNoInteractions(stack);
|
||||
assertFalse(NativeGenerationWriteGuard.protectedPositions(objects, null, -64)
|
||||
.test(new BlockPos(-3, 11, -7)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyStaticLayerSkipsContainsAndCachesEachSignedColumn() {
|
||||
IrisStaticObjectLayer objects = mock(IrisStaticObjectLayer.class);
|
||||
DimensionStackContext stack = mock(DimensionStackContext.class);
|
||||
DimensionStackLayout negative = mock(DimensionStackLayout.class);
|
||||
DimensionStackLayout positive = mock(DimensionStackLayout.class);
|
||||
when(objects.isEmpty()).thenReturn(true);
|
||||
when(stack.sample(-1, -1)).thenReturn(negative);
|
||||
when(stack.sample(-1, 1)).thenReturn(positive);
|
||||
when(negative.isHostFeatureProtectedY(64)).thenReturn(true);
|
||||
when(positive.isHostFeatureProtectedY(65)).thenReturn(true);
|
||||
|
||||
Predicate<BlockPos> protectedPositions = NativeGenerationWriteGuard.protectedPositions(objects, stack, -64);
|
||||
|
||||
assertTrue(protectedPositions.test(new BlockPos(-1, 0, -1)));
|
||||
assertFalse(protectedPositions.test(new BlockPos(-1, 1, -1)));
|
||||
assertTrue(protectedPositions.test(new BlockPos(-1, 1, 1)));
|
||||
verify(stack, times(1)).sample(-1, -1);
|
||||
verify(stack, times(1)).sample(-1, 1);
|
||||
verify(objects, never()).contains(anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preTerrainSavedStampDistinguishesEarlierNativeStarts() {
|
||||
IrisEngine engine = mock(IrisEngine.class);
|
||||
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||
GenerationHistory history = mock(GenerationHistory.class);
|
||||
GenerationActivation active = mock(GenerationActivation.class);
|
||||
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
|
||||
when(router.history()).thenReturn(history);
|
||||
when(history.activeActivation()).thenReturn(active);
|
||||
when(active.activationId()).thenReturn(4L);
|
||||
assertTrue(NativeGenerationWriteGuard.isHistoricalStructure(engine, 3));
|
||||
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 0));
|
||||
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 4));
|
||||
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void partialNeighborsMayFinishButCompletedHistoricalNeighborRejectsPass() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
WorldGenLevel region = mock(WorldGenLevel.class);
|
||||
ChunkAccess partial = mock(ChunkAccess.class);
|
||||
when(partial.getPersistedStatus()).thenReturn(ChunkStatus.NOISE);
|
||||
when(region.getChunk(anyInt(), anyInt())).thenReturn(partial);
|
||||
assertTrue(NativeGenerationWriteGuard.allowsDecoration(engine, region, new ChunkPos(0, 0)));
|
||||
ChunkAccess complete = mock(ChunkAccess.class);
|
||||
when(complete.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
|
||||
when(region.getChunk(1, -1)).thenReturn(complete);
|
||||
assertFalse(NativeGenerationWriteGuard.allowsDecoration(engine, region, new ChunkPos(0, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void historicalNoiseMayFinishFeaturesButNeverReplaysNoiseOrCompletedStage() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
ChunkAccess chunk = mock(ChunkAccess.class);
|
||||
when(chunk.getPos()).thenReturn(new ChunkPos(-2, 3));
|
||||
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.NOISE);
|
||||
assertTrue(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.FEATURES));
|
||||
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.NOISE));
|
||||
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FEATURES);
|
||||
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.FEATURES));
|
||||
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
|
||||
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.SPAWN));
|
||||
}
|
||||
}
|
||||
+471
@@ -0,0 +1,471 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.structure.jigsaw.IrisJigsawConfiguration;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import com.mojang.datafixers.util.Either;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderOwner;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.Vec3i;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.IntTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.biome.FixedBiomeSource;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElementType;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructureFactoryTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void genericSourceDoesNotRequireJigsawType() {
|
||||
Structure source = new SwampHutStructure(
|
||||
new Structure.StructureSettings(HolderSet.empty()));
|
||||
|
||||
Structure configured = NativeStructureFactory.configure(
|
||||
null, source, null, false, 80);
|
||||
|
||||
assertSame(source, configured);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jigsawOptionsFailClosedForNonJigsawSource() {
|
||||
Structure source = new SwampHutStructure(
|
||||
new Structure.StructureSettings(HolderSet.empty()));
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> NativeStructureFactory.configure(
|
||||
null, source, new IrisJigsawConfiguration(), false, 80));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceMetadataReadsScalarAndAxisSpecificJigsawRanges() {
|
||||
CompoundTag scalar = new CompoundTag();
|
||||
scalar.putInt("max_distance_from_center", 80);
|
||||
CompoundTag axisSpecific = new CompoundTag();
|
||||
CompoundTag distance = new CompoundTag();
|
||||
distance.putInt("horizontal", 96);
|
||||
distance.putInt("vertical", 64);
|
||||
axisSpecific.put("max_distance_from_center", distance);
|
||||
|
||||
assertEquals(80, NativeStructureFactory.sourceDistance(scalar, "horizontal"));
|
||||
assertEquals(80, NativeStructureFactory.sourceDistance(scalar, "vertical"));
|
||||
assertEquals(96, NativeStructureFactory.sourceDistance(axisSpecific, "horizontal"));
|
||||
assertEquals(64, NativeStructureFactory.sourceDistance(axisSpecific, "vertical"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceMetadataIncludesAdjustBoundingBoxExpansion() {
|
||||
Structure none = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||
Structure beardBox = new SwampHutStructure(new Structure.StructureSettings.Builder(HolderSet.empty())
|
||||
.terrainAdapation(TerrainAdjustment.BEARD_BOX)
|
||||
.build());
|
||||
|
||||
assertEquals(0, NativeStructureFactory.horizontalReferenceExpansion(none));
|
||||
assertEquals(12, NativeStructureFactory.horizontalReferenceExpansion(beardBox));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void templatePoolSpanIncludesWeightedListsFallbacksAndRotations() {
|
||||
StructurePoolElement narrow = inlineElement(30, 8, 70);
|
||||
StructurePoolElement wide = inlineElement(96, 8, 12);
|
||||
ListPoolElement list = new ListPoolElement(
|
||||
List.of(narrow, wide, EmptyPoolElement.INSTANCE),
|
||||
StructureTemplatePool.Projection.RIGID);
|
||||
BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:fallback");
|
||||
StructureTemplatePool fallback = new StructureTemplatePool(
|
||||
fallbackHolder,
|
||||
List.of(Pair.of(inlineElement(110, 8, 20), 1)));
|
||||
fallbackHolder.bind(fallback);
|
||||
BoundPoolHolder startHolder = new BoundPoolHolder("test:start");
|
||||
StructureTemplatePool start = new StructureTemplatePool(
|
||||
fallbackHolder,
|
||||
List.of(Pair.of(list, 5), Pair.of(EmptyPoolElement.INSTANCE, 1)));
|
||||
startHolder.bind(start);
|
||||
|
||||
assertEquals(110, NativeStructureTemplatePoolBounds.horizontalSpan(start, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oversizedInlineStartTemplateReportsItsFullSpan() {
|
||||
BoundPoolHolder holder = new BoundPoolHolder("test:oversized");
|
||||
StructureTemplatePool pool = new StructureTemplatePool(
|
||||
holder,
|
||||
List.of(Pair.of(inlineElement(160, 8, 9), 1)));
|
||||
holder.bind(pool);
|
||||
|
||||
assertEquals(160, NativeStructureTemplatePoolBounds.horizontalSpan(pool, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceSpanIncludesOnlyAliasesThatCanReplaceTheStartPool() {
|
||||
BoundPoolHolder emptyHolder = new BoundPoolHolder("test:empty");
|
||||
StructureTemplatePool empty = new StructureTemplatePool(emptyHolder, List.of());
|
||||
emptyHolder.bind(empty);
|
||||
BoundPoolHolder startHolder = new BoundPoolHolder("test:start");
|
||||
StructureTemplatePool start = new StructureTemplatePool(
|
||||
emptyHolder, List.of(Pair.of(inlineElement(20, 8, 12), 1)));
|
||||
startHolder.bind(start);
|
||||
BoundPoolHolder replacementHolder = new BoundPoolHolder("test:replacement");
|
||||
StructureTemplatePool replacement = new StructureTemplatePool(
|
||||
emptyHolder, List.of(Pair.of(inlineElement(30, 8, 12), 1)));
|
||||
replacementHolder.bind(replacement);
|
||||
BoundPoolHolder unrelatedHolder = new BoundPoolHolder("test:unrelated");
|
||||
StructureTemplatePool unrelated = new StructureTemplatePool(
|
||||
emptyHolder, List.of(Pair.of(inlineElement(200, 8, 12), 1)));
|
||||
unrelatedHolder.bind(unrelated);
|
||||
ResourceKey<StructureTemplatePool> startKey = startHolder.unwrapKey().orElseThrow();
|
||||
ResourceKey<StructureTemplatePool> replacementKey = replacementHolder.unwrapKey().orElseThrow();
|
||||
ResourceKey<StructureTemplatePool> unrelatedKey = unrelatedHolder.unwrapKey().orElseThrow();
|
||||
ResourceKey<StructureTemplatePool> childAlias = ResourceKey.create(
|
||||
Registries.TEMPLATE_POOL, Identifier.parse("test:child_alias"));
|
||||
List<PoolAliasBinding> aliases = List.of(
|
||||
PoolAliasBinding.direct(startKey, replacementKey),
|
||||
PoolAliasBinding.direct(childAlias, unrelatedKey));
|
||||
Map<ResourceKey<StructureTemplatePool>, StructureTemplatePool> pools = Map.of(
|
||||
replacementKey, replacement,
|
||||
unrelatedKey, unrelated);
|
||||
|
||||
assertEquals(30, NativeStructureTemplatePoolBounds.sourceHorizontalSpan(
|
||||
null, start, startKey, aliases, pools::get));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceStartSpanDoesNotIncludeItsFallbackPool() {
|
||||
BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:large_fallback");
|
||||
StructureTemplatePool fallback = new StructureTemplatePool(
|
||||
fallbackHolder, List.of(Pair.of(inlineElement(180, 8, 10), 1)));
|
||||
fallbackHolder.bind(fallback);
|
||||
BoundPoolHolder startHolder = new BoundPoolHolder("test:small_start");
|
||||
StructureTemplatePool start = new StructureTemplatePool(
|
||||
fallbackHolder, List.of(Pair.of(inlineElement(24, 8, 10), 1)));
|
||||
startHolder.bind(start);
|
||||
|
||||
assertEquals(180, NativeStructureTemplatePoolBounds.horizontalSpan(start, null));
|
||||
assertEquals(24, NativeStructureTemplatePoolBounds.sourceHorizontalSpan(
|
||||
null, start, startHolder.unwrapKey().orElseThrow(), List.of(), ignored -> null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customPoolElementUsesItsBoundingBoxCapability() {
|
||||
BoundPoolHolder fallbackHolder = new BoundPoolHolder("test:custom_fallback");
|
||||
StructureTemplatePool fallback = new StructureTemplatePool(fallbackHolder, List.of());
|
||||
fallbackHolder.bind(fallback);
|
||||
BoundPoolHolder poolHolder = new BoundPoolHolder("test:custom");
|
||||
StructureTemplatePool pool = new StructureTemplatePool(
|
||||
fallbackHolder, List.of(Pair.of(new BoundingPoolElement(37, 11), 1)));
|
||||
poolHolder.bind(pool);
|
||||
|
||||
assertEquals(37, NativeStructureTemplatePoolBounds.horizontalSpan(pool, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forcedGeneratorBypassesBiomeEligibilityWithoutFabricatingTerrain() {
|
||||
ChunkGenerator delegate = new TestChunkGenerator();
|
||||
Holder<Biome> biome = Holder.direct((Biome) null);
|
||||
LevelHeightAccessor heightAccessor = LevelHeightAccessor.create(-64, 384);
|
||||
ForcedStructureChunkGenerator generator = new ForcedStructureChunkGenerator(
|
||||
delegate, biome);
|
||||
|
||||
assertEquals(63, generator.getSeaLevel());
|
||||
assertEquals(64, generator.getBaseHeight(
|
||||
0, 0, Heightmap.Types.WORLD_SURFACE_WG, heightAccessor, null));
|
||||
NoiseColumn column = generator.getBaseColumn(
|
||||
0, 0, heightAccessor, null);
|
||||
assertSame(Blocks.STONE, column.getBlock(80).getBlock());
|
||||
assertSame(Blocks.STONE, column.getBlock(81).getBlock());
|
||||
assertNotSame(delegate.getBiomeSource(), generator.getBiomeSource());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void undergroundRelocationMovesGenericPiecesAndReturnsFreshStart() {
|
||||
Structure source = new SwampHutStructure(
|
||||
new Structure.StructureSettings(HolderSet.empty()));
|
||||
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), 0, 0);
|
||||
StructureStart start = new StructureStart(
|
||||
source,
|
||||
new ChunkPos(0, 0),
|
||||
0,
|
||||
new PiecesContainer(List.of(piece))
|
||||
);
|
||||
|
||||
StructureStart relocated = NativeStructureVerticalPlacer.relocateToMinY(
|
||||
start, source, -20, LevelHeightAccessor.create(-64, 384));
|
||||
|
||||
assertNotSame(start, relocated);
|
||||
assertEquals(-20, relocated.getPieces().getFirst().getBoundingBox().minY());
|
||||
assertEquals(-20, relocated.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidGeneratedStartIsANormalSkippedCandidate() {
|
||||
Structure source = new SwampHutStructure(
|
||||
new Structure.StructureSettings(HolderSet.empty()));
|
||||
StructureStart valid = new StructureStart(
|
||||
source,
|
||||
new ChunkPos(0, 0),
|
||||
0,
|
||||
new PiecesContainer(List.of(
|
||||
new SwampHutPiece(RandomSource.create(23L), 0, 0)))
|
||||
);
|
||||
|
||||
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(
|
||||
StructureStart.INVALID_START));
|
||||
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(null));
|
||||
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(
|
||||
new StructureStart(source, new ChunkPos(0, 0), 0,
|
||||
new PiecesContainer(List.of()))));
|
||||
assertTrue(NativeStructureStartInjector.isUsableGeneratedStart(valid));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unrepresentableGeneratedContentBecomesAnUnpublishedInvalidStart() {
|
||||
Structure source = new SwampHutStructure(
|
||||
new Structure.StructureSettings(HolderSet.empty()));
|
||||
SwampHutPiece boundaryPiece = new SwampHutPiece(RandomSource.create(29L), 0, 0);
|
||||
boundaryPiece.move(143 - boundaryPiece.getBoundingBox().maxX(), 0, 0);
|
||||
StructureStart boundary = new StructureStart(
|
||||
source,
|
||||
new ChunkPos(0, 0),
|
||||
0,
|
||||
new PiecesContainer(List.of(boundaryPiece))
|
||||
);
|
||||
SwampHutPiece oversizedPiece = new SwampHutPiece(RandomSource.create(31L), 0, 0);
|
||||
oversizedPiece.move(144 - oversizedPiece.getBoundingBox().maxX(), 0, 0);
|
||||
StructureStart oversized = new StructureStart(
|
||||
source,
|
||||
new ChunkPos(0, 0),
|
||||
0,
|
||||
new PiecesContainer(List.of(oversizedPiece))
|
||||
);
|
||||
|
||||
StructureStart boundaryResult = NativeStructureReferenceEnvelope.wrapForPublication(
|
||||
boundary, source, 0, new IrisStructureTerrain(), "test:boundary_content");
|
||||
|
||||
StructureStart result = NativeStructureReferenceEnvelope.wrapForPublication(
|
||||
oversized, source, 0,
|
||||
new IrisStructureTerrain(),
|
||||
"test:oversized_content");
|
||||
|
||||
assertTrue(boundaryResult.isValid());
|
||||
assertSame(StructureStart.INVALID_START, result);
|
||||
assertFalse(NativeStructureStartInjector.isUsableGeneratedStart(result));
|
||||
assertEquals(1, oversized.getPieces().size());
|
||||
}
|
||||
|
||||
private static final class TestChunkGenerator extends ChunkGenerator {
|
||||
private TestChunkGenerator() {
|
||||
super(new FixedBiomeSource(Holder.direct((Biome) null)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends ChunkGenerator> codec() {
|
||||
return MapCodec.unit(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyCarvers(WorldGenRegion region, long seed, RandomState randomState,
|
||||
BiomeManager biomeManager, StructureManager structureManager,
|
||||
ChunkAccess chunk) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildSurface(WorldGenRegion level, StructureManager structureManager,
|
||||
RandomState randomState, ChunkAccess protoChunk) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spawnOriginalMobs(WorldGenRegion worldGenRegion) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGenDepth() {
|
||||
return 384;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> fillFromNoise(
|
||||
Blender blender, RandomState randomState,
|
||||
StructureManager structureManager, ChunkAccess centerChunk) {
|
||||
return CompletableFuture.completedFuture(centerChunk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSeaLevel() {
|
||||
return 63;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinY() {
|
||||
return -64;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBaseHeight(int x, int z, Heightmap.Types type,
|
||||
LevelHeightAccessor heightAccessor, RandomState randomState) {
|
||||
return 64;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor,
|
||||
RandomState randomState) {
|
||||
BlockState[] states = new BlockState[heightAccessor.getHeight()];
|
||||
for (int index = 0; index < states.length; index++) {
|
||||
states[index] = Blocks.STONE.defaultBlockState();
|
||||
}
|
||||
return new NoiseColumn(heightAccessor.getMinY(), states);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addDebugScreenInfo(List<String> result, RandomState randomState,
|
||||
BlockPos feetPos) {
|
||||
}
|
||||
}
|
||||
|
||||
private static StructurePoolElement inlineElement(int sizeX, int sizeY, int sizeZ) {
|
||||
StructureTemplate template = new StructureTemplate();
|
||||
CompoundTag tag = new CompoundTag();
|
||||
ListTag size = new ListTag();
|
||||
size.add(IntTag.valueOf(sizeX));
|
||||
size.add(IntTag.valueOf(sizeY));
|
||||
size.add(IntTag.valueOf(sizeZ));
|
||||
tag.put("size", size);
|
||||
template.load(BuiltInRegistries.BLOCK, tag);
|
||||
return new InlinePoolElement(template);
|
||||
}
|
||||
|
||||
private static final class InlinePoolElement extends SinglePoolElement {
|
||||
private final StructureTemplate inlineTemplate;
|
||||
|
||||
private InlinePoolElement(StructureTemplate template) {
|
||||
super(Either.right(template), Holder.direct(new StructureProcessorList(List.of())),
|
||||
StructureTemplatePool.Projection.RIGID, Optional.<LiquidSettings>empty());
|
||||
inlineTemplate = template;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
|
||||
BlockPos position, Rotation rotation) {
|
||||
return inlineTemplate.getBoundingBox(
|
||||
new StructurePlaceSettings().setRotation(rotation), position);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BoundingPoolElement extends StructurePoolElement {
|
||||
private final int width;
|
||||
private final int depth;
|
||||
|
||||
private BoundingPoolElement(int width, int depth) {
|
||||
super(StructureTemplatePool.Projection.RIGID);
|
||||
this.width = width;
|
||||
this.depth = depth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3i getSize(StructureTemplateManager templateManager, Rotation rotation) {
|
||||
return new Vec3i(width, 1, depth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StructureTemplate.JigsawBlockInfo> getShuffledJigsawBlocks(
|
||||
StructureTemplateManager templateManager, BlockPos position,
|
||||
Rotation rotation, RandomSource random) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
|
||||
BlockPos position, Rotation rotation) {
|
||||
return new BoundingBox(
|
||||
position.getX(), position.getY(), position.getZ(),
|
||||
position.getX() + width - 1, position.getY(),
|
||||
position.getZ() + depth - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean place(StructureTemplateManager templateManager,
|
||||
WorldGenLevel world,
|
||||
StructureManager structureManager, ChunkGenerator chunkGenerator,
|
||||
BlockPos position, BlockPos pivot, Rotation rotation,
|
||||
BoundingBox area, RandomSource random,
|
||||
LiquidSettings liquidSettings, boolean keepJigsaws) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StructurePoolElementType<?> getType() {
|
||||
return StructurePoolElementType.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BoundPoolHolder extends Holder.Reference<StructureTemplatePool> {
|
||||
private BoundPoolHolder(String key) {
|
||||
super(Type.STAND_ALONE, new HolderOwner<>() {
|
||||
}, ResourceKey.create(Registries.TEMPLATE_POOL, Identifier.parse(key)), null);
|
||||
}
|
||||
|
||||
private void bind(StructureTemplatePool pool) {
|
||||
bindValue(pool);
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.Vec3i;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructureGenerationKeysContractTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraft() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void frequencyGateUsesMappedAccessorAndRejectsNonPositivePlacements() {
|
||||
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.0F)));
|
||||
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(-0.1F)));
|
||||
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(Float.NaN)));
|
||||
assertTrue(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.25F)));
|
||||
}
|
||||
|
||||
private static RandomSpreadStructurePlacement placement(float frequency) {
|
||||
return new RandomSpreadStructurePlacement(
|
||||
Vec3i.ZERO,
|
||||
StructurePlacement.FrequencyReductionMethod.DEFAULT,
|
||||
frequency,
|
||||
1,
|
||||
Optional.empty(),
|
||||
2,
|
||||
1,
|
||||
RandomSpreadType.LINEAR);
|
||||
}
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructureLocatePersistenceTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void storageProbeScansEachCandidateOnlyOnce() {
|
||||
AtomicInteger reads = new AtomicInteger();
|
||||
CompoundTag chunk = new CompoundTag();
|
||||
chunk.putInt("DataVersion", SharedConstants.getCurrentVersion().dataVersion().version());
|
||||
chunk.putString("Status", "full");
|
||||
CompoundTag start = new CompoundTag();
|
||||
start.putString("id", "minecraft:village_plains");
|
||||
start.putInt("references", 0);
|
||||
CompoundTag starts = new CompoundTag();
|
||||
starts.put("minecraft:village_plains", start);
|
||||
CompoundTag structures = new CompoundTag();
|
||||
structures.put("starts", starts);
|
||||
chunk.put("structures", structures);
|
||||
NativeStructureLocatePersistence.ProbeBudget budget =
|
||||
new NativeStructureLocatePersistence.ProbeBudget(
|
||||
512,
|
||||
(chunkPos, visitor) -> {
|
||||
reads.incrementAndGet();
|
||||
chunk.acceptAsRoot(visitor);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
});
|
||||
ChunkPos chunkPos = new ChunkPos(7, -11);
|
||||
|
||||
assertTrue(budget.acceptsStored(
|
||||
null, chunkPos, "minecraft:village_plains", true));
|
||||
assertTrue(budget.acceptsStored(
|
||||
null, chunkPos, "minecraft:village_plains", true));
|
||||
assertEquals(1, reads.get());
|
||||
assertEquals(1, budget.used());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legacyStoredChunkIsDatafixedBeforeItsStartIsClassified() {
|
||||
AtomicBoolean datafixed = new AtomicBoolean();
|
||||
CompoundTag legacy = new CompoundTag();
|
||||
legacy.putInt("DataVersion", 100);
|
||||
legacy.putString("Status", "full");
|
||||
NativeStructureLocatePersistence.ProbeBudget budget =
|
||||
new NativeStructureLocatePersistence.ProbeBudget(
|
||||
512,
|
||||
(chunkPos, visitor) -> {
|
||||
legacy.acceptAsRoot(visitor);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
},
|
||||
(level, storedChunk) -> {
|
||||
datafixed.set(true);
|
||||
CompoundTag fixed = storedChunk.copy();
|
||||
fixed.putInt("DataVersion",
|
||||
SharedConstants.getCurrentVersion().dataVersion().version());
|
||||
CompoundTag start = new CompoundTag();
|
||||
start.putString("id", "minecraft:village_plains");
|
||||
start.putInt("references", 1);
|
||||
CompoundTag starts = new CompoundTag();
|
||||
starts.put("minecraft:village_plains", start);
|
||||
CompoundTag structures = new CompoundTag();
|
||||
structures.put("starts", starts);
|
||||
fixed.put("structures", structures);
|
||||
return fixed;
|
||||
});
|
||||
|
||||
assertFalse(budget.acceptsStored(
|
||||
null, new ChunkPos(3, 4), "minecraft:village_plains", true));
|
||||
assertTrue(datafixed.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void partialDatafixFailureFallsBackToSelectedChunkVerification() {
|
||||
CompoundTag legacy = new CompoundTag();
|
||||
legacy.putInt("DataVersion", 100);
|
||||
NativeStructureLocatePersistence.ProbeBudget budget =
|
||||
new NativeStructureLocatePersistence.ProbeBudget(
|
||||
512,
|
||||
(chunkPos, visitor) -> {
|
||||
legacy.acceptAsRoot(visitor);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
},
|
||||
(level, storedChunk) -> {
|
||||
throw new IllegalStateException("broken partial datafix");
|
||||
});
|
||||
|
||||
assertTrue(budget.acceptsStored(
|
||||
null, new ChunkPos(9, -2), "minecraft:village_plains", true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingStartsContainerFallsBackToSelectedChunkVerification() {
|
||||
CompoundTag chunk = currentChunk();
|
||||
NativeStructureLocatePersistence.ProbeBudget budget = budget(chunk);
|
||||
|
||||
assertTrue(budget.acceptsStored(
|
||||
null, new ChunkPos(5, 8), "minecraft:village_plains", false));
|
||||
assertTrue(budget.acceptsStored(
|
||||
null, new ChunkPos(5, 8), "minecraft:village_plains", true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void presentEmptyStartsContainerRejectsTheCandidateWithoutLoading() {
|
||||
CompoundTag chunk = currentChunk();
|
||||
CompoundTag structures = new CompoundTag();
|
||||
structures.put("starts", new CompoundTag());
|
||||
chunk.put("structures", structures);
|
||||
NativeStructureLocatePersistence.ProbeBudget budget = budget(chunk);
|
||||
|
||||
assertFalse(budget.acceptsStored(
|
||||
null, new ChunkPos(-3, 2), "minecraft:village_plains", false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collisionTombstoneIsRejectedForNormalAndUnexploredLocate() {
|
||||
CompoundTag chunk = currentChunk();
|
||||
CompoundTag tombstone = new CompoundTag();
|
||||
tombstone.putString("id", "INVALID");
|
||||
CompoundTag starts = new CompoundTag();
|
||||
starts.put("minecraft:village_plains", tombstone);
|
||||
CompoundTag structures = new CompoundTag();
|
||||
structures.put("starts", starts);
|
||||
chunk.put("structures", structures);
|
||||
ChunkPos chunkPos = new ChunkPos(12, -9);
|
||||
|
||||
assertFalse(budget(chunk).acceptsStored(
|
||||
null, chunkPos, "minecraft:village_plains", false));
|
||||
assertFalse(budget(chunk).acceptsStored(
|
||||
null, chunkPos, "minecraft:village_plains", true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void malformedNonCompoundStartIsRejectedWithoutLoading() {
|
||||
CompoundTag chunk = currentChunk();
|
||||
CompoundTag starts = new CompoundTag();
|
||||
starts.putInt("minecraft:village_plains", 1);
|
||||
CompoundTag structures = new CompoundTag();
|
||||
structures.put("starts", starts);
|
||||
chunk.put("structures", structures);
|
||||
|
||||
assertFalse(budget(chunk).acceptsStored(
|
||||
null, new ChunkPos(1, 1), "minecraft:village_plains", false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void referencedStoredStartIsOnlyRejectedForUnexploredLocate() {
|
||||
CompoundTag chunk = currentChunk();
|
||||
CompoundTag start = new CompoundTag();
|
||||
start.putString("id", "minecraft:village_plains");
|
||||
start.putInt("references", 1);
|
||||
CompoundTag starts = new CompoundTag();
|
||||
starts.put("minecraft:village_plains", start);
|
||||
CompoundTag structures = new CompoundTag();
|
||||
structures.put("starts", starts);
|
||||
chunk.put("structures", structures);
|
||||
ChunkPos chunkPos = new ChunkPos(-4, -7);
|
||||
|
||||
assertTrue(budget(chunk).acceptsStored(
|
||||
null, chunkPos, "minecraft:village_plains", false));
|
||||
assertFalse(budget(chunk).acceptsStored(
|
||||
null, chunkPos, "minecraft:village_plains", true));
|
||||
}
|
||||
|
||||
private static CompoundTag currentChunk() {
|
||||
CompoundTag chunk = new CompoundTag();
|
||||
chunk.putInt("DataVersion", SharedConstants.getCurrentVersion().dataVersion().version());
|
||||
return chunk;
|
||||
}
|
||||
|
||||
private static NativeStructureLocatePersistence.ProbeBudget budget(CompoundTag chunk) {
|
||||
return new NativeStructureLocatePersistence.ProbeBudget(
|
||||
512,
|
||||
(chunkPos, visitor) -> {
|
||||
chunk.acceptAsRoot(visitor);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructure;
|
||||
import art.arcane.iris.structure.placement.IrisStructurePlacement;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.Vec3i;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.LegacyRandomSource;
|
||||
import net.minecraft.world.level.levelgen.RandomSupport;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElementType;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructureOwnershipFingerprintTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void monumentFingerprintSurvivesSourceYRegenerationWithoutSyntheticPieces() {
|
||||
long seed = 8372619L;
|
||||
ChunkPos origin = new ChunkPos(9, -4);
|
||||
OceanMonumentStructure structure = new OceanMonumentStructure(
|
||||
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
|
||||
StructureStart generated = monumentStart(structure, origin, seed);
|
||||
NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel(
|
||||
generated, 0, 80, -64, 320);
|
||||
int alignedLocatorY = NativeStructureOwnershipFingerprint.locatorY(generated);
|
||||
IrisStructureTerrain terrain = new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
|
||||
.setHorizontalPadding(24);
|
||||
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
|
||||
generated,
|
||||
structure,
|
||||
0,
|
||||
terrain);
|
||||
String beforeReload = NativeStructureOwnershipFingerprint.fingerprint(
|
||||
"minecraft:monument", wrapped);
|
||||
|
||||
ListTag serializedPieces = (ListTag) new PiecesContainer(wrapped.getPieces()).save(null);
|
||||
PiecesContainer loadedPieces = PiecesContainer.load(serializedPieces, null);
|
||||
assertEquals(1, loadedPieces.pieces().size());
|
||||
assertTrue(loadedPieces.pieces().getFirst()
|
||||
instanceof OceanMonumentPieces.MonumentBuilding);
|
||||
PiecesContainer regeneratedPieces = OceanMonumentStructure.regeneratePiecesAfterLoad(
|
||||
origin, seed, loadedPieces);
|
||||
StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces);
|
||||
String afterReload = NativeStructureOwnershipFingerprint.fingerprint(
|
||||
"minecraft:monument", reloaded);
|
||||
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
|
||||
"minecraft:monument",
|
||||
wrapped,
|
||||
plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()),
|
||||
NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain));
|
||||
|
||||
assertEquals(beforeReload, afterReload);
|
||||
assertTrue(NativeStructureOwnershipFingerprint.matches(ownership, reloaded));
|
||||
assertNotEquals(alignedLocatorY, NativeStructureOwnershipFingerprint.locatorY(reloaded));
|
||||
assertEquals(alignedLocatorY, ownership.locatorY());
|
||||
assertEquals(1, reloaded.getPieces().size());
|
||||
assertEquals(1, wrapped.getPieces().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sameKeyAndOriginStillRequiresMatchingContentIdentity() {
|
||||
long seed = 18273L;
|
||||
ChunkPos origin = new ChunkPos(2, 3);
|
||||
OceanMonumentStructure structure = new OceanMonumentStructure(
|
||||
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
|
||||
StructureStart expected = monumentStart(structure, origin, seed);
|
||||
StructureStart moved = monumentStart(structure, origin, seed);
|
||||
for (StructurePiece piece : moved.getPieces()) {
|
||||
piece.move(1, 0, 0);
|
||||
}
|
||||
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
|
||||
"minecraft:monument",
|
||||
expected,
|
||||
plan(origin, NativeStructureReferenceEnvelope.contentBounds(expected).minY()),
|
||||
expected.getBoundingBox());
|
||||
|
||||
assertFalse(NativeStructureOwnershipFingerprint.matches(ownership, moved));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistedEnvelopeRepairsReloadedMonumentReferenceOutsideLiveBounds() {
|
||||
long seed = 772931L;
|
||||
ChunkPos origin = new ChunkPos(4, -7);
|
||||
OceanMonumentStructure structure = new OceanMonumentStructure(
|
||||
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
|
||||
StructureStart generated = monumentStart(structure, origin, seed);
|
||||
NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel(
|
||||
generated, 0, 80, -64, 320);
|
||||
IrisStructureTerrain terrain = new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
|
||||
.setHorizontalPadding(24);
|
||||
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
|
||||
generated,
|
||||
structure,
|
||||
0,
|
||||
terrain);
|
||||
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
|
||||
"minecraft:monument",
|
||||
wrapped,
|
||||
plan(origin, NativeStructureReferenceEnvelope.contentBounds(wrapped).minY()),
|
||||
NativeStructureReferenceEnvelope.referenceBounds(wrapped, structure, terrain));
|
||||
PiecesContainer regeneratedPieces = OceanMonumentStructure.regeneratePiecesAfterLoad(
|
||||
origin, seed, new PiecesContainer(wrapped.getPieces()));
|
||||
StructureStart reloaded = new StructureStart(structure, origin, 0, regeneratedPieces);
|
||||
ChunkPos target = outsideLiveBounds(ownership, reloaded);
|
||||
|
||||
assertNotNull(target);
|
||||
assertTrue(NativeStructureReferenceRepair.requiresReference(
|
||||
target, "minecraft:monument", reloaded, ownership));
|
||||
assertFalse(NativeStructureReferenceRepair.requiresReference(
|
||||
target, "minecraft:monument", reloaded, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overlappingManualStartsSixteenChunksApartRemainIndependentlyReferenced() {
|
||||
OceanMonumentStructure structure = new OceanMonumentStructure(
|
||||
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
|
||||
ChunkPos westOrigin = new ChunkPos(0, 0);
|
||||
ChunkPos eastOrigin = new ChunkPos(16, 0);
|
||||
StructureStart west = monumentStart(structure, westOrigin, 317L);
|
||||
StructureStart east = monumentStart(structure, eastOrigin, 941L);
|
||||
west.getPieces().getFirst().move(112, 0, 0);
|
||||
east.getPieces().getFirst().move(-96, 0, 0);
|
||||
NativeStructureOwnershipRecord westOwnership = ownership(westOrigin, west, structure);
|
||||
NativeStructureOwnershipRecord eastOwnership = ownership(eastOrigin, east, structure);
|
||||
ChunkPos shared = new ChunkPos(8, 0);
|
||||
|
||||
assertTrue(NativeStructureReferenceEnvelope.contentBounds(west).intersects(
|
||||
NativeStructureReferenceEnvelope.contentBounds(east)));
|
||||
assertTrue(NativeStructureReferenceRepair.requiresReference(
|
||||
shared, "minecraft:monument", west, westOwnership));
|
||||
assertTrue(NativeStructureReferenceRepair.requiresReference(
|
||||
shared, "minecraft:monument", east, eastOwnership));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void denseAdjacentManualStartsDoNotSuppressEachOther() {
|
||||
OceanMonumentStructure structure = new OceanMonumentStructure(
|
||||
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
|
||||
ChunkPos firstOrigin = new ChunkPos(0, 0);
|
||||
ChunkPos secondOrigin = new ChunkPos(1, 0);
|
||||
StructureStart first = monumentStart(structure, firstOrigin, 17L);
|
||||
StructureStart second = monumentStart(structure, secondOrigin, 23L);
|
||||
NativeStructureOwnershipRecord firstOwnership = ownership(firstOrigin, first, structure);
|
||||
NativeStructureOwnershipRecord secondOwnership = ownership(secondOrigin, second, structure);
|
||||
ChunkPos shared = new ChunkPos(0, 0);
|
||||
|
||||
assertTrue(NativeStructureReferenceEnvelope.contentBounds(first).intersects(
|
||||
NativeStructureReferenceEnvelope.contentBounds(second)));
|
||||
assertTrue(NativeStructureReferenceRepair.requiresReference(
|
||||
shared, "minecraft:monument", first, firstOwnership));
|
||||
assertTrue(NativeStructureReferenceRepair.requiresReference(
|
||||
shared, "minecraft:monument", second, secondOwnership));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customPoolElementIdentityDoesNotDependOnProcessLocalToString() {
|
||||
OceanMonumentStructure structure = new OceanMonumentStructure(
|
||||
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
|
||||
ChunkPos origin = new ChunkPos(3, -2);
|
||||
StructureStart first = poolStart(structure, origin, new IdentityStringPoolElement());
|
||||
StructureStart reloaded = poolStart(structure, origin, new IdentityStringPoolElement());
|
||||
|
||||
assertEquals(
|
||||
NativeStructureOwnershipFingerprint.fingerprint("test:custom_pool", first),
|
||||
NativeStructureOwnershipFingerprint.fingerprint("test:custom_pool", reloaded));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void namedPoolTemplateRemainsPartOfTheStableFingerprint() {
|
||||
OceanMonumentStructure structure = new OceanMonumentStructure(
|
||||
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
|
||||
ChunkPos origin = new ChunkPos(3, -2);
|
||||
StructurePoolElement firstElement = StructurePoolElement.single("test:first")
|
||||
.apply(StructureTemplatePool.Projection.RIGID);
|
||||
StructurePoolElement secondElement = StructurePoolElement.single("test:second")
|
||||
.apply(StructureTemplatePool.Projection.RIGID);
|
||||
|
||||
assertNotEquals(
|
||||
NativeStructureOwnershipFingerprint.fingerprint(
|
||||
"test:named_pool", poolStart(structure, origin, firstElement)),
|
||||
NativeStructureOwnershipFingerprint.fingerprint(
|
||||
"test:named_pool", poolStart(structure, origin, secondElement)));
|
||||
}
|
||||
|
||||
private static StructureStart monumentStart(OceanMonumentStructure structure,
|
||||
ChunkPos origin, long seed) {
|
||||
WorldgenRandom random = new WorldgenRandom(
|
||||
new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
random.setLargeFeatureSeed(seed, origin.x(), origin.z());
|
||||
Direction orientation = Direction.Plane.HORIZONTAL.getRandomDirection(random);
|
||||
OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding(
|
||||
random,
|
||||
origin.getMinBlockX() - 29,
|
||||
origin.getMinBlockZ() - 29,
|
||||
orientation
|
||||
);
|
||||
return new StructureStart(
|
||||
structure,
|
||||
origin,
|
||||
0,
|
||||
new PiecesContainer(List.of(building))
|
||||
);
|
||||
}
|
||||
|
||||
private static NativeStructureStartPlan plan(ChunkPos origin, int baseY) {
|
||||
IrisNativeStructure source = new IrisNativeStructure()
|
||||
.setStructure("minecraft:monument")
|
||||
.setWeight(1);
|
||||
IrisStructurePlacement placement = new IrisStructurePlacement()
|
||||
.setPlacementId("monument-test")
|
||||
.setNativeStructures(new KList<IrisNativeStructure>().qadd(source));
|
||||
return new NativeStructureStartPlan(
|
||||
placement,
|
||||
source,
|
||||
origin.x(),
|
||||
origin.z(),
|
||||
baseY
|
||||
);
|
||||
}
|
||||
|
||||
private static NativeStructureOwnershipRecord ownership(
|
||||
ChunkPos origin, StructureStart start, OceanMonumentStructure structure) {
|
||||
return NativeStructureOwnershipFingerprint.capture(
|
||||
"minecraft:monument",
|
||||
start,
|
||||
plan(origin, NativeStructureReferenceEnvelope.contentBounds(start).minY()),
|
||||
NativeStructureReferenceEnvelope.referenceBounds(
|
||||
start, structure, new IrisStructureTerrain())
|
||||
);
|
||||
}
|
||||
|
||||
private static StructureStart poolStart(
|
||||
OceanMonumentStructure structure, ChunkPos origin, StructurePoolElement element) {
|
||||
BlockPos position = new BlockPos(
|
||||
origin.getMiddleBlockX(), 64, origin.getMiddleBlockZ());
|
||||
PoolElementStructurePiece piece = new PoolElementStructurePiece(
|
||||
null, element, position, 0, Rotation.NONE,
|
||||
new BoundingBox(position), LiquidSettings.APPLY_WATERLOGGING);
|
||||
return new StructureStart(
|
||||
structure, origin, 0, new PiecesContainer(List.of(piece)));
|
||||
}
|
||||
|
||||
private static final class IdentityStringPoolElement extends StructurePoolElement {
|
||||
private IdentityStringPoolElement() {
|
||||
super(StructureTemplatePool.Projection.RIGID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3i getSize(StructureTemplateManager templateManager, Rotation rotation) {
|
||||
return new Vec3i(1, 1, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StructureTemplate.JigsawBlockInfo> getShuffledJigsawBlocks(
|
||||
StructureTemplateManager templateManager, BlockPos position,
|
||||
Rotation rotation, RandomSource random) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BoundingBox getBoundingBox(StructureTemplateManager templateManager,
|
||||
BlockPos position, Rotation rotation) {
|
||||
return new BoundingBox(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean place(StructureTemplateManager templateManager,
|
||||
WorldGenLevel world, StructureManager structureManager,
|
||||
ChunkGenerator chunkGenerator, BlockPos position, BlockPos pivot,
|
||||
Rotation rotation, BoundingBox area, RandomSource random,
|
||||
LiquidSettings liquidSettings, boolean keepJigsaws) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StructurePoolElementType<?> getType() {
|
||||
return StructurePoolElementType.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "IdentityStringPoolElement@"
|
||||
+ Integer.toHexString(System.identityHashCode(this));
|
||||
}
|
||||
}
|
||||
|
||||
private static ChunkPos outsideLiveBounds(NativeStructureOwnershipRecord ownership,
|
||||
StructureStart start) {
|
||||
for (int chunkX = ownership.referenceMinChunkX();
|
||||
chunkX <= ownership.referenceMaxChunkX(); chunkX++) {
|
||||
for (int chunkZ = ownership.referenceMinChunkZ();
|
||||
chunkZ <= ownership.referenceMaxChunkZ(); chunkZ++) {
|
||||
ChunkPos candidate = new ChunkPos(chunkX, chunkZ);
|
||||
if (!start.getBoundingBox().intersects(
|
||||
candidate.getMinBlockX(), candidate.getMinBlockZ(),
|
||||
candidate.getMaxBlockX(), candidate.getMaxBlockZ())) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureOwnershipRecord;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
|
||||
import art.arcane.iris.structure.placement.StructurePlacementGrid;
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructure;
|
||||
import art.arcane.iris.structure.placement.IrisStructurePlacement;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.levelgen.LegacyRandomSource;
|
||||
import net.minecraft.world.level.levelgen.RandomSupport;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructureOwnershipRecoveryTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingSidecarRecoversExactPolicyEnvelopeAndLocatorAfterReload() {
|
||||
long seed = 5519231L;
|
||||
ChunkPos origin = new ChunkPos(7, -5);
|
||||
OceanMonumentStructure structure = structure();
|
||||
StructureStart expected = monumentStart(structure, origin, seed);
|
||||
NativeStructureVerticalPlacer.alignOceanMonumentToSeaLevel(
|
||||
expected, 0, 80, -64, 320);
|
||||
int expectedLocatorY = NativeStructureOwnershipFingerprint.locatorY(expected);
|
||||
NativeStructureStartPlan plan = plan(origin, "recovery-exact", 24);
|
||||
StructureStart reloaded = new StructureStart(
|
||||
structure,
|
||||
origin,
|
||||
expected.getReferences(),
|
||||
OceanMonumentStructure.regeneratePiecesAfterLoad(
|
||||
origin, seed, new PiecesContainer(expected.getPieces()))
|
||||
);
|
||||
|
||||
NativeStructureOwnershipRecord recovered =
|
||||
NativeStructureOwnershipRecovery.proveCandidate(
|
||||
"minecraft:monument", structure, reloaded,
|
||||
plan, expected, false);
|
||||
|
||||
assertNotNull(recovered);
|
||||
assertEquals(StructurePlacementGrid.placementIdentity(plan.placement()),
|
||||
recovered.placementIdentity());
|
||||
assertEquals(expectedLocatorY, recovered.locatorY());
|
||||
assertNotEquals(NativeStructureOwnershipFingerprint.locatorY(reloaded),
|
||||
recovered.locatorY());
|
||||
assertEquals(IrisStructureTerrainMode.FORCE_CARVE,
|
||||
recovered.restoredDecision().terrain().resolvedMode());
|
||||
assertEquals(24, recovered.restoredDecision().terrain().getHorizontalPadding());
|
||||
assertTrue(recovered.referenceMinChunkX()
|
||||
< NativeStructureReferenceEnvelope.contentBounds(reloaded).minX() >> 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void naturalSameKeyStartRemainsUnownedWhenOriginIsAmbiguous() {
|
||||
long seed = 22817L;
|
||||
ChunkPos origin = new ChunkPos(-3, 6);
|
||||
OceanMonumentStructure structure = structure();
|
||||
StructureStart natural = monumentStart(structure, origin, seed);
|
||||
NativeStructureStartPlan plan = plan(origin, "natural-ambiguous", 0);
|
||||
|
||||
NativeStructureOwnershipRecord recovered =
|
||||
NativeStructureOwnershipRecovery.proveCandidate(
|
||||
"minecraft:monument", structure, natural,
|
||||
plan, natural, true);
|
||||
|
||||
assertNull(recovered);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changedPlacementIdentityOrGeometryCannotRecoverOwnership() {
|
||||
long seed = 991723L;
|
||||
ChunkPos origin = new ChunkPos(2, 4);
|
||||
OceanMonumentStructure structure = structure();
|
||||
StructureStart expected = monumentStart(structure, origin, seed);
|
||||
StructureStart changedGeometry = monumentStart(structure, origin, seed);
|
||||
for (StructurePiece piece : changedGeometry.getPieces()) {
|
||||
piece.move(1, 0, 0);
|
||||
}
|
||||
NativeStructureStartPlan originalIdentity = plan(
|
||||
origin, "original-identity", 0);
|
||||
NativeStructureStartPlan changedIdentity = plan(
|
||||
new ChunkPos(origin.x() + 1, origin.z()), "changed-identity", 0);
|
||||
|
||||
assertNotEquals(
|
||||
StructurePlacementGrid.placementIdentity(originalIdentity.placement()),
|
||||
StructurePlacementGrid.placementIdentity(changedIdentity.placement()));
|
||||
assertNull(NativeStructureOwnershipRecovery.proveCandidate(
|
||||
"minecraft:monument", structure, expected,
|
||||
changedIdentity, expected, false));
|
||||
assertNull(NativeStructureOwnershipRecovery.proveCandidate(
|
||||
"minecraft:monument", structure, changedGeometry,
|
||||
plan(origin, "geometry-check", 0), expected, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recoveredEnvelopeStillRequiresExactFingerprint() {
|
||||
long seed = 81337L;
|
||||
ChunkPos origin = new ChunkPos(-8, -9);
|
||||
OceanMonumentStructure structure = structure();
|
||||
StructureStart expected = monumentStart(structure, origin, seed);
|
||||
NativeStructureOwnershipRecord recovered =
|
||||
NativeStructureOwnershipRecovery.proveCandidate(
|
||||
"minecraft:monument", structure, expected,
|
||||
plan(origin, "fingerprint-check", 16), expected, false);
|
||||
assertNotNull(recovered);
|
||||
|
||||
StructureStart moved = monumentStart(structure, origin, seed);
|
||||
for (StructurePiece piece : moved.getPieces()) {
|
||||
piece.move(0, 0, 1);
|
||||
}
|
||||
|
||||
assertFalse(NativeStructureOwnershipFingerprint.matches(recovered, moved));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void staleVacuumEnvelopeRefreshesWithoutReplacingOwnershipIdentity() {
|
||||
String structureKey = "minecraft:monument";
|
||||
long seed = 648231L;
|
||||
ChunkPos origin = new ChunkPos(5, -6);
|
||||
OceanMonumentStructure structure = structure();
|
||||
StructureStart start = monumentStart(structure, origin, seed);
|
||||
NativeStructureStartPlan plan = plan(
|
||||
origin, "vacuum-envelope-refresh", IrisStructureTerrainMode.VACUUM, 0);
|
||||
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(start);
|
||||
NativeStructureOwnershipRecord stale = NativeStructureOwnershipFingerprint.capture(
|
||||
structureKey, start, plan, content);
|
||||
|
||||
NativeStructureOwnershipRecord refreshed =
|
||||
NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
|
||||
structureKey, structure, start, stale);
|
||||
|
||||
assertNotNull(refreshed);
|
||||
assertNotEquals(stale, refreshed);
|
||||
assertEquals(stale.schema(), refreshed.schema());
|
||||
assertEquals(stale.ownershipKey(), refreshed.ownershipKey());
|
||||
assertEquals(stale.placementIdentity(), refreshed.placementIdentity());
|
||||
assertEquals(stale.baseY(), refreshed.baseY());
|
||||
assertEquals(stale.locatorY(), refreshed.locatorY());
|
||||
assertEquals(stale.contentFingerprint(), refreshed.contentFingerprint());
|
||||
assertEquals(stale.decision(), refreshed.decision());
|
||||
assertEquals(IrisStructureTerrainMode.VACUUM,
|
||||
refreshed.restoredDecision().terrain().resolvedMode());
|
||||
BoundingBox expected = NativeStructureReferenceEnvelope.referenceBounds(
|
||||
start, structure, plan.placement().resolvedTerrain(), structureKey);
|
||||
assertEquals(expected.minX() >> 4, refreshed.referenceMinChunkX());
|
||||
assertEquals(expected.maxX() >> 4, refreshed.referenceMaxChunkX());
|
||||
assertEquals(expected.minZ() >> 4, refreshed.referenceMinChunkZ());
|
||||
assertEquals(expected.maxZ() >> 4, refreshed.referenceMaxChunkZ());
|
||||
assertTrue(hasExpandedCoverage(stale, refreshed));
|
||||
assertSame(refreshed, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
|
||||
structureKey, structure, start, refreshed));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void staleEnvelopeCannotRefreshAgainstDifferentContent() {
|
||||
String structureKey = "minecraft:monument";
|
||||
long seed = 412987L;
|
||||
ChunkPos origin = new ChunkPos(-2, 7);
|
||||
OceanMonumentStructure structure = structure();
|
||||
StructureStart expected = monumentStart(structure, origin, seed);
|
||||
NativeStructureStartPlan plan = plan(
|
||||
origin, "vacuum-content-check", IrisStructureTerrainMode.VACUUM, 0);
|
||||
NativeStructureOwnershipRecord stale = NativeStructureOwnershipFingerprint.capture(
|
||||
structureKey, expected, plan,
|
||||
NativeStructureReferenceEnvelope.contentBounds(expected));
|
||||
StructureStart moved = monumentStart(structure, origin, seed);
|
||||
for (StructurePiece piece : moved.getPieces()) {
|
||||
piece.move(1, 0, 0);
|
||||
}
|
||||
|
||||
assertNull(NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
|
||||
structureKey, structure, moved, stale));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void currentNonVacuumEnvelopeRemainsThePersistedAuthority() {
|
||||
String structureKey = "minecraft:monument";
|
||||
long seed = 927451L;
|
||||
ChunkPos origin = new ChunkPos(3, 8);
|
||||
OceanMonumentStructure structure = structure();
|
||||
StructureStart start = monumentStart(structure, origin, seed);
|
||||
NativeStructureStartPlan plan = plan(origin, "current-force-carve", 24);
|
||||
BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds(
|
||||
start, structure, plan.placement().resolvedTerrain(), structureKey);
|
||||
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
|
||||
structureKey, start, plan, envelope);
|
||||
|
||||
assertSame(ownership, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
|
||||
structureKey, structure, start, ownership));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clippedVacuumEnvelopeRemainsStableAtTheReferenceLimit() {
|
||||
String structureKey = "minecraft:monument";
|
||||
long seed = 381729L;
|
||||
ChunkPos origin = new ChunkPos(0, 0);
|
||||
OceanMonumentStructure structure = structure();
|
||||
StructureStart start = monumentStart(structure, origin, seed);
|
||||
BoundingBox initial = NativeStructureReferenceEnvelope.contentBounds(start);
|
||||
int maximumReferenceBlockX = ((origin.x()
|
||||
+ NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS) << 4) + 15;
|
||||
int shiftX = maximumReferenceBlockX - initial.maxX();
|
||||
for (StructurePiece piece : start.getPieces()) {
|
||||
piece.move(shiftX, 0, 0);
|
||||
}
|
||||
NativeStructureStartPlan plan = plan(
|
||||
origin, "clipped-vacuum", IrisStructureTerrainMode.VACUUM, 0);
|
||||
BoundingBox envelope = NativeStructureReferenceEnvelope.referenceBounds(
|
||||
start, structure, plan.placement().resolvedTerrain(), structureKey);
|
||||
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipFingerprint.capture(
|
||||
structureKey, start, plan, envelope);
|
||||
|
||||
assertEquals(origin.x() + NativeStructureOwnershipRecord.MAX_REFERENCE_DISTANCE_CHUNKS,
|
||||
ownership.referenceMaxChunkX());
|
||||
assertSame(ownership, NativeStructureOwnershipRecovery.refreshReferenceEnvelope(
|
||||
structureKey, structure, start, ownership));
|
||||
}
|
||||
|
||||
private static OceanMonumentStructure structure() {
|
||||
return new OceanMonumentStructure(
|
||||
new OceanMonumentStructure.StructureSettings(HolderSet.empty()));
|
||||
}
|
||||
|
||||
private static StructureStart monumentStart(OceanMonumentStructure structure,
|
||||
ChunkPos origin, long seed) {
|
||||
WorldgenRandom random = new WorldgenRandom(
|
||||
new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
random.setLargeFeatureSeed(seed, origin.x(), origin.z());
|
||||
Direction orientation = Direction.Plane.HORIZONTAL.getRandomDirection(random);
|
||||
OceanMonumentPieces.MonumentBuilding building =
|
||||
new OceanMonumentPieces.MonumentBuilding(
|
||||
random,
|
||||
origin.getMinBlockX() - 29,
|
||||
origin.getMinBlockZ() - 29,
|
||||
orientation
|
||||
);
|
||||
return new StructureStart(
|
||||
structure, origin, 0, new PiecesContainer(List.of(building)));
|
||||
}
|
||||
|
||||
private static NativeStructureStartPlan plan(ChunkPos origin,
|
||||
String placementId,
|
||||
int horizontalPadding) {
|
||||
return plan(origin, placementId,
|
||||
IrisStructureTerrainMode.FORCE_CARVE, horizontalPadding);
|
||||
}
|
||||
|
||||
private static NativeStructureStartPlan plan(ChunkPos origin,
|
||||
String placementId,
|
||||
IrisStructureTerrainMode terrainMode,
|
||||
int horizontalPadding) {
|
||||
IrisNativeStructure source = new IrisNativeStructure()
|
||||
.setStructure("minecraft:monument")
|
||||
.setWeight(1);
|
||||
IrisStructureTerrain terrain = new IrisStructureTerrain()
|
||||
.setMode(terrainMode)
|
||||
.setHorizontalPadding(horizontalPadding);
|
||||
IrisStructurePlacement placement = new IrisStructurePlacement()
|
||||
.setPlacementId(placementId)
|
||||
.setTerrain(terrain)
|
||||
.setNativeStructures(new KList<IrisNativeStructure>().qadd(source));
|
||||
return new NativeStructureStartPlan(
|
||||
placement,
|
||||
source,
|
||||
origin.x(),
|
||||
origin.z(),
|
||||
NativeStructureReferenceEnvelope.contentBounds(
|
||||
monumentStart(structure(), origin, 1L)).minY()
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean hasExpandedCoverage(
|
||||
NativeStructureOwnershipRecord stale,
|
||||
NativeStructureOwnershipRecord refreshed) {
|
||||
for (int chunkX = refreshed.referenceMinChunkX();
|
||||
chunkX <= refreshed.referenceMaxChunkX(); chunkX++) {
|
||||
for (int chunkZ = refreshed.referenceMinChunkZ();
|
||||
chunkZ <= refreshed.referenceMaxChunkZ(); chunkZ++) {
|
||||
if (!stale.covers(chunkX, chunkZ)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+532
@@ -0,0 +1,532 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||
import art.arcane.iris.structure.placement.IrisStructureYBand;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.BitSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructurePostProcessorEncaseTest {
|
||||
private static final long TEST_SEED = 1234L;
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraft() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encaseFillsAirAndLiquidWithoutOverwritingExistingTerrain() {
|
||||
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||
BoundingBox area = new BoundingBox(
|
||||
bounds.minX() - 1, bounds.minY() - 1, bounds.minZ() - 1,
|
||||
bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1);
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.DIRT.defaultBlockState());
|
||||
put(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ(), Blocks.WATER.defaultBlockState());
|
||||
|
||||
NativeStructureTerrainIntegrator.integrateTerrain(
|
||||
world(blocks), area, "minecraft:stronghold", start,
|
||||
new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.ENCASE)
|
||||
.setHorizontalPadding(1)
|
||||
.setCeilingPadding(1)
|
||||
.setFloorPadding(1),
|
||||
null);
|
||||
|
||||
assertEquals(Blocks.DIRT.defaultBlockState(),
|
||||
state(blocks, bounds.minX(), bounds.minY(), bounds.minZ()));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||
state(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ()));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||
state(blocks, bounds.minX() - 1, bounds.minY() - 1, bounds.minZ() - 1));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||
state(blocks, bounds.maxX() + 1, bounds.maxY() + 1, bounds.maxZ() + 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultEncasePaletteSplitsStoneAboveZeroAndDeepslateBelow() {
|
||||
StructureStart start = start(TerrainAdjustment.BURY, -1);
|
||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
|
||||
NativeStructureTerrainIntegrator.integrateTerrain(
|
||||
world(blocks), bounds, "minecraft:stronghold", start,
|
||||
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.ENCASE),
|
||||
null);
|
||||
|
||||
assertEquals(-1, bounds.minY());
|
||||
assertEquals(Blocks.DEEPSLATE.defaultBlockState(),
|
||||
state(blocks, bounds.minX(), -1, bounds.minZ()));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||
state(blocks, bounds.minX(), 0, bounds.minZ()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultEncaseMaterialFollowsVanillaDimensionTerrain() {
|
||||
assertEquals(Blocks.NETHERRACK.defaultBlockState(),
|
||||
NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.NETHER, 64));
|
||||
assertEquals(Blocks.END_STONE.defaultBlockState(),
|
||||
NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.END, 64));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||
NativeStructureTerrainIntegrator.defaultEncaseBlock(Level.OVERWORLD, 64));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceEncaseMaterialFollowsNearbyIrisTerrain() {
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
BlockPos fill = new BlockPos(4, 64, 8);
|
||||
put(blocks, fill.getX() + 3, fill.getY(), fill.getZ(), Blocks.TUFF.defaultBlockState());
|
||||
|
||||
assertEquals(Blocks.TUFF.defaultBlockState(),
|
||||
NativeStructureTerrainIntegrator.sourceEncaseBlock(world(blocks), fill));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceMaterialSnapshotIgnoresPreviouslyDecoratedAdjacentChunks() {
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
BlockPos fill = new BlockPos(0, 64, 8);
|
||||
put(blocks, -1, 64, 8, Blocks.OAK_PLANKS.defaultBlockState());
|
||||
put(blocks, 3, 64, 8, Blocks.TUFF.defaultBlockState());
|
||||
WorldGenLevel world = world(blocks);
|
||||
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
|
||||
sourceSnapshot(world, new BoundingBox(0, 64, 0, 15, 64, 15));
|
||||
|
||||
assertEquals(Blocks.TUFF.defaultBlockState(),
|
||||
NativeStructureTerrainIntegrator.sourceEncaseBlock(
|
||||
world, fill, sourceTerrain));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceMaterialSnapshotIsStableAcrossEarlierFillsInTheSameChunk() {
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
BlockPos fill = new BlockPos(0, 64, 8);
|
||||
put(blocks, 3, 64, 8, Blocks.TUFF.defaultBlockState());
|
||||
WorldGenLevel world = world(blocks);
|
||||
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
|
||||
sourceSnapshot(world, new BoundingBox(0, 64, 0, 15, 64, 15));
|
||||
put(blocks, 1, 64, 8, Blocks.OAK_PLANKS.defaultBlockState());
|
||||
|
||||
assertEquals(Blocks.TUFF.defaultBlockState(),
|
||||
NativeStructureTerrainIntegrator.sourceEncaseBlock(
|
||||
world, fill, sourceTerrain));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceMaterialRejectsStructureBlocksAndOresBeforeDimensionFallback() {
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
BlockPos fill = new BlockPos(4, -20, 8);
|
||||
put(blocks, 3, -20, 8, Blocks.OAK_PLANKS.defaultBlockState());
|
||||
put(blocks, 5, -20, 8, Blocks.STONE_BRICKS.defaultBlockState());
|
||||
put(blocks, 4, -20, 7, Blocks.IRON_ORE.defaultBlockState());
|
||||
|
||||
assertEquals(Blocks.DEEPSLATE.defaultBlockState(),
|
||||
NativeStructureTerrainIntegrator.sourceEncaseBlock(world(blocks), fill));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceMaterialSnapshotOnlyAllocatesActualFillLayers() {
|
||||
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||
BoundingBox area = new BoundingBox(0, -2032, 0, 15, 2030, 15);
|
||||
NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceTerrain =
|
||||
NativeStructureTerrainIntegrator.captureSourceTerrain(
|
||||
world(new HashMap<>()), area,
|
||||
List.of(new NativeStructureTerrainIntegrator.TerrainTarget(
|
||||
"test:bounded-snapshot", start,
|
||||
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE))));
|
||||
|
||||
assertNotNull(sourceTerrain);
|
||||
assertEquals(23 * 16 * 16, sourceTerrain.sampledCells());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customDimensionKeyUsesRegisteredVanillaDimensionType() {
|
||||
ResourceKey<Level> customDimension = ResourceKey.create(
|
||||
Registries.DIMENSION, Identifier.parse("iris:custom_nether"));
|
||||
|
||||
assertEquals(Blocks.NETHERRACK.defaultBlockState(),
|
||||
NativeStructureTerrainIntegrator.defaultEncaseBlock(
|
||||
customDimension, BuiltinDimensionTypes.NETHER, null, 64));
|
||||
assertEquals(Blocks.END_STONE.defaultBlockState(),
|
||||
NativeStructureTerrainIntegrator.defaultEncaseBlock(
|
||||
customDimension, BuiltinDimensionTypes.END, null, 64));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredEncasePaletteReplacesTheDefaultMaterial() {
|
||||
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
IrisMaterialPalette palette = new IrisMaterialPalette().qclear().qadd("minecraft:tuff");
|
||||
|
||||
NativeStructureTerrainIntegrator.integrateTerrain(
|
||||
world(blocks), bounds, "minecraft:stronghold", start,
|
||||
new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.ENCASE)
|
||||
.setEncasePalette(palette),
|
||||
NativeStructurePostProcessorEncaseTest::tuffBlock);
|
||||
|
||||
assertEquals(Blocks.TUFF.defaultBlockState(),
|
||||
state(blocks, bounds.minX(), bounds.minY(), bounds.minZ()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void omittedTerrainConfigurationRetainsSourceSemantics() {
|
||||
for (TerrainAdjustment adjustment : TerrainAdjustment.values()) {
|
||||
IrisStructureTerrain resolved = NativeStructureTerrainIntegrator.resolveNativeTerrain(
|
||||
start(adjustment, 64), null);
|
||||
|
||||
assertEquals(IrisStructureTerrainMode.SOURCE, resolved.resolvedMode());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceBuryUsesTheVanillaSixByTwelveInfluenceEnvelope() {
|
||||
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||
int groundY = bounds.minY();
|
||||
BoundingBox area = new BoundingBox(
|
||||
bounds.minX() - 7, groundY - 13, bounds.minZ(),
|
||||
bounds.maxX(), groundY + 13, bounds.maxZ());
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
|
||||
NativeStructureTerrainIntegrator.integrateTerrain(
|
||||
world(blocks), area, "test:bury", start,
|
||||
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE), null);
|
||||
|
||||
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||
state(blocks, bounds.minX() - 5, groundY, bounds.minZ()));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||
state(blocks, bounds.minX(), groundY + 11, bounds.minZ()));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||
state(blocks, bounds.minX() - 6, groundY, bounds.minZ()));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||
state(blocks, bounds.minX(), groundY + 12, bounds.minZ()));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||
state(blocks, bounds.minX() - 7, groundY, bounds.minZ()));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||
state(blocks, bounds.minX(), groundY + 13, bounds.minZ()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceEncapsulateWrapsTheFullPieceBounds() {
|
||||
StructureStart start = start(TerrainAdjustment.ENCAPSULATE, 64);
|
||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||
BoundingBox area = new BoundingBox(
|
||||
bounds.minX() - 13, bounds.minY(), bounds.minZ(),
|
||||
bounds.maxX(), bounds.maxY() + 13, bounds.maxZ());
|
||||
Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
|
||||
NativeStructureTerrainIntegrator.integrateTerrain(
|
||||
world(blocks), area, "test:encapsulate", start,
|
||||
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE), null);
|
||||
|
||||
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||
state(blocks, bounds.minX() - 11, bounds.minY(), bounds.minZ()));
|
||||
assertEquals(Blocks.STONE.defaultBlockState(),
|
||||
state(blocks, bounds.minX(), bounds.maxY() + 11, bounds.minZ()));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||
state(blocks, bounds.minX() - 12, bounds.minY(), bounds.minZ()));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||
state(blocks, bounds.minX(), bounds.maxY() + 12, bounds.minZ()));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||
state(blocks, bounds.minX() - 13, bounds.minY(), bounds.minZ()));
|
||||
assertEquals(Blocks.AIR.defaultBlockState(),
|
||||
state(blocks, bounds.minX(), bounds.maxY() + 13, bounds.minZ()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceTerrainDensityOnlyUsesRigidPoolPieces() {
|
||||
PoolElementStructurePiece terrainMatching = poolPiece(StructureTemplatePool.Projection.TERRAIN_MATCHING);
|
||||
PoolElementStructurePiece rigid = poolPiece(StructureTemplatePool.Projection.RIGID);
|
||||
|
||||
assertFalse(NativeStructureTerrainIntegrator.isSourceRigidPiece(terrainMatching));
|
||||
assertTrue(NativeStructureTerrainIntegrator.isSourceRigidPiece(rigid));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceJunctionTerrainUsesTheStrictLowerTwelveBlockEnvelope() {
|
||||
assertTrue(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -1, 0));
|
||||
assertTrue(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -11, 0));
|
||||
assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, 0, 0));
|
||||
assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(0, -12, 0));
|
||||
assertFalse(NativeStructureTerrainIntegrator.insideSourceJunctionEnvelope(12, -1, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitTerrainConfigurationWinsOverAutoEncase() {
|
||||
IrisStructureTerrain configured = new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.PRESERVE);
|
||||
|
||||
assertSame(configured, NativeStructureTerrainIntegrator.resolveNativeTerrain(
|
||||
start(TerrainAdjustment.BURY, 64), configured));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legacyTemplateAirIsClearedAfterEveryTerrainFillPath() {
|
||||
IrisStructureTerrain source = new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.SOURCE);
|
||||
IrisStructureTerrain preserve = new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.PRESERVE);
|
||||
IrisStructureTerrain encase = new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.ENCASE);
|
||||
|
||||
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
|
||||
start(TerrainAdjustment.BURY, 64), source));
|
||||
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
|
||||
start(TerrainAdjustment.BEARD_BOX, 64), source));
|
||||
assertTrue(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
|
||||
start(TerrainAdjustment.NONE, 64), encase));
|
||||
assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
|
||||
start(TerrainAdjustment.NONE, 64), source));
|
||||
assertFalse(NativeStructureTerrainIntegrator.clearsLegacyTemplateAir(
|
||||
start(TerrainAdjustment.BURY, 64), preserve));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encaseReservesTheNeighborChunkEnvelope() {
|
||||
StructureStart generated = start(TerrainAdjustment.BURY, 64);
|
||||
BoundingBox sourceBounds = generated.getBoundingBox();
|
||||
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated);
|
||||
IrisStructureTerrain terrain = new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.ENCASE)
|
||||
.setHorizontalPadding(4);
|
||||
|
||||
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
|
||||
generated,
|
||||
generated.getStructure(),
|
||||
0,
|
||||
terrain);
|
||||
BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds(
|
||||
wrapped, wrapped.getStructure(), terrain);
|
||||
|
||||
assertEquals(sourceBounds, wrapped.getBoundingBox());
|
||||
assertEquals(content.minX() - 4, referenceBounds.minX());
|
||||
assertEquals(content.maxX() + 4, referenceBounds.maxX());
|
||||
assertEquals(content.minZ() - 4, referenceBounds.minZ());
|
||||
assertEquals(content.maxZ() + 4, referenceBounds.maxZ());
|
||||
assertEquals(generated.getPieces().size(), wrapped.getPieces().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void referenceEnvelopeRejectsChunkCoordinateOverflow() {
|
||||
StructureStart generated = start(TerrainAdjustment.NONE, 64);
|
||||
StructureStart extreme = new StructureStart(
|
||||
generated.getStructure(), new ChunkPos(Integer.MAX_VALUE, 0), 0,
|
||||
new PiecesContainer(generated.getPieces()));
|
||||
|
||||
assertThrows(IllegalStateException.class, () ->
|
||||
NativeStructureReferenceEnvelope.referenceBounds(
|
||||
extreme, extreme.getStructure(), new IrisStructureTerrain()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitTerrainEnvelopeDoesNotInheritSourceAdjustment() {
|
||||
StructureStart generated = start(TerrainAdjustment.BURY, 64);
|
||||
BoundingBox content = NativeStructureReferenceEnvelope.contentBounds(generated);
|
||||
IrisStructureTerrain terrain = new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.ENCASE)
|
||||
.setHorizontalPadding(112);
|
||||
|
||||
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
|
||||
generated, generated.getStructure(), 0, terrain);
|
||||
BoundingBox referenceBounds = NativeStructureReferenceEnvelope.referenceBounds(
|
||||
wrapped, wrapped.getStructure(), terrain);
|
||||
|
||||
assertEquals(content.minX() - 112, referenceBounds.minX());
|
||||
assertEquals(content.maxX() + 112, referenceBounds.maxX());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void yBandRelocatesTheStartMidpointDeterministically() {
|
||||
IrisStructureYBand band = new IrisStructureYBand().setMin(-120).setMax(-20);
|
||||
StructureStart first = start(TerrainAdjustment.BURY, 64);
|
||||
StructureStart second = start(TerrainAdjustment.BURY, 64);
|
||||
|
||||
NativeStructureVerticalPlacer.applyVerticalShift(
|
||||
first, -64, -256, 320, true, false, band, (x, z) -> 40);
|
||||
NativeStructureVerticalPlacer.applyVerticalShift(
|
||||
second, -64, -256, 320, true, false, band, (x, z) -> 40);
|
||||
|
||||
BoundingBox bounds = first.getBoundingBox();
|
||||
assertEquals(bounds.minY(), second.getBoundingBox().minY());
|
||||
assertTrue(bounds.minY() >= -120);
|
||||
assertTrue(bounds.maxY() <= -20);
|
||||
|
||||
int repeated = NativeStructureVerticalPlacer.applyVerticalShift(
|
||||
first, -64, -256, 320, true, false, band, (x, z) -> 40);
|
||||
assertEquals(0, repeated);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void yBandClampsWhenTheBandCannotContainTheStructure() {
|
||||
IrisStructureYBand band = new IrisStructureYBand().setMin(-50).setMax(-45);
|
||||
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||
|
||||
NativeStructureVerticalPlacer.applyVerticalShift(
|
||||
start, 0, -256, 320, true, false, band, (x, z) -> 40);
|
||||
|
||||
BoundingBox bounds = start.getBoundingBox();
|
||||
int midpoint = bounds.minY() + (bounds.maxY() - bounds.minY()) / 2;
|
||||
assertTrue(midpoint >= -50);
|
||||
assertTrue(midpoint <= -45);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void yBandReplacesBothTheBlindShiftAndBurial() {
|
||||
IrisStructureYBand band = new IrisStructureYBand().setMin(-64).setMax(-64);
|
||||
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||
BoundingBox bounds = start.getBoundingBox();
|
||||
int height = bounds.maxY() - bounds.minY();
|
||||
|
||||
NativeStructureVerticalPlacer.applyVerticalShift(
|
||||
start, -200, -256, 320, true, false, band, (x, z) -> 40);
|
||||
|
||||
assertEquals(-64 - height / 2, start.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preserveSourceYWinsOverTheConfiguredBand() {
|
||||
IrisStructureYBand band = new IrisStructureYBand().setMin(-120).setMax(-20);
|
||||
StructureStart start = start(TerrainAdjustment.BURY, 64);
|
||||
int minY = start.getBoundingBox().minY();
|
||||
|
||||
int offset = NativeStructureVerticalPlacer.applyVerticalShift(
|
||||
start, 0, -256, 320, true, true, band, (x, z) -> 40);
|
||||
|
||||
assertEquals(0, offset);
|
||||
assertEquals(minY, start.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
private static BlockState tuffBlock(IrisMaterialPalette palette, RNG rng, int x, int y, int z) {
|
||||
assertNotNull(palette);
|
||||
assertNotNull(rng);
|
||||
return Blocks.TUFF.defaultBlockState();
|
||||
}
|
||||
|
||||
private static StructureStart start(TerrainAdjustment adjustment, int minY) {
|
||||
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(
|
||||
HolderSet.empty(), Map.of(), GenerationStep.Decoration.STRONGHOLDS, adjustment));
|
||||
StructurePiece piece = new DesertPyramidPiece(RandomSource.create(7L), 0, 0);
|
||||
piece.move(0, minY - piece.getBoundingBox().minY(), 0);
|
||||
return new StructureStart(
|
||||
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
|
||||
}
|
||||
|
||||
private static PoolElementStructurePiece poolPiece(StructureTemplatePool.Projection projection) {
|
||||
BlockPos position = new BlockPos(0, 64, 0);
|
||||
StructurePoolElement element = StructurePoolElement.single(
|
||||
"minecraft:empty",
|
||||
Holder.direct(new StructureProcessorList(List.of(
|
||||
BlockIgnoreProcessor.STRUCTURE_AND_AIR))),
|
||||
LiquidSettings.APPLY_WATERLOGGING
|
||||
).apply(projection);
|
||||
return new PoolElementStructurePiece(
|
||||
null,
|
||||
element,
|
||||
position,
|
||||
0,
|
||||
Rotation.NONE,
|
||||
new BoundingBox(position),
|
||||
LiquidSettings.APPLY_WATERLOGGING);
|
||||
}
|
||||
|
||||
private static WorldGenLevel world(Map<BlockPos, BlockState> blocks) {
|
||||
InvocationHandler handler = (proxy, method, arguments) -> {
|
||||
String methodName = method.getName();
|
||||
if (methodName.equals("getBlockState")) {
|
||||
BlockPos position = (BlockPos) arguments[0];
|
||||
return state(blocks, position.getX(), position.getY(), position.getZ());
|
||||
}
|
||||
if (methodName.equals("setBlock")) {
|
||||
BlockPos position = (BlockPos) arguments[0];
|
||||
BlockState blockState = (BlockState) arguments[1];
|
||||
put(blocks, position.getX(), position.getY(), position.getZ(), blockState);
|
||||
return true;
|
||||
}
|
||||
if (methodName.equals("getSeed")) {
|
||||
return TEST_SEED;
|
||||
}
|
||||
if (methodName.equals("getLevel")) {
|
||||
return null;
|
||||
}
|
||||
if (methodName.equals("hashCode")) {
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
if (methodName.equals("equals")) {
|
||||
return proxy == arguments[0];
|
||||
}
|
||||
if (methodName.equals("toString")) {
|
||||
return "encase-test-world";
|
||||
}
|
||||
throw new UnsupportedOperationException(method.toString());
|
||||
};
|
||||
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
|
||||
}
|
||||
|
||||
private static NativeStructureTerrainIntegrator.SourceTerrainSnapshot sourceSnapshot(
|
||||
WorldGenLevel world, BoundingBox area) {
|
||||
BitSet requiredLayers = new BitSet(area.getYSpan());
|
||||
requiredLayers.set(0, area.getYSpan());
|
||||
return NativeStructureTerrainIntegrator.SourceTerrainSnapshot.capture(
|
||||
world, area, requiredLayers);
|
||||
}
|
||||
|
||||
private static void put(Map<BlockPos, BlockState> blocks,
|
||||
int x, int y, int z, BlockState state) {
|
||||
blocks.put(new BlockPos(x, y, z), state);
|
||||
}
|
||||
|
||||
private static BlockState state(Map<BlockPos, BlockState> blocks, int x, int y, int z) {
|
||||
return blocks.getOrDefault(new BlockPos(x, y, z), Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentStructure;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructurePostProcessorMonumentTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void vanillaSeaLevelKeepsTheVanillaMonumentHeight() {
|
||||
StructureStart start = monumentStart(1337L);
|
||||
|
||||
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
start, "minecraft:monument", 0, 63, -64, 320, false, false, null, (x, z) -> 0);
|
||||
|
||||
assertEquals(0, offset);
|
||||
assertEquals(39, start.getBoundingBox().minY());
|
||||
assertEquals(61, start.getBoundingBox().maxY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shiftedSeaLevelMovesTheShellAndEveryRoomTogether() {
|
||||
StructureStart start = monumentStart(1337L);
|
||||
OceanMonumentPieces.MonumentBuilding building = monumentBuilding(start);
|
||||
List<StructurePiece> children = NativeStructureVerticalPlacer.monumentChildPieces(building);
|
||||
int[] childMinY = minimumYs(children);
|
||||
BoundingBox cachedBounds = start.getBoundingBox();
|
||||
|
||||
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
|
||||
|
||||
assertEquals(-13, offset);
|
||||
assertSame(cachedBounds, start.getBoundingBox());
|
||||
assertEquals(26, building.getBoundingBox().minY());
|
||||
assertEquals(48, building.getBoundingBox().maxY());
|
||||
assertEquals(26, start.getBoundingBox().minY());
|
||||
assertEquals(48, start.getBoundingBox().maxY());
|
||||
assertTrue(children.size() > 20);
|
||||
for (int i = 0; i < children.size(); i++) {
|
||||
assertEquals(childMinY[i] - 13, children.get(i).getBoundingBox().minY());
|
||||
}
|
||||
|
||||
int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
|
||||
assertEquals(0, repeatedOffset);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredOffsetIsRelativeToTheActualSeaLevel() {
|
||||
StructureStart start = monumentStart(1337L);
|
||||
|
||||
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
start, "minecraft:monument", 3, 50, -256, 512, false, false, null, (x, z) -> 0);
|
||||
|
||||
assertEquals(-10, offset);
|
||||
assertEquals(29, start.getBoundingBox().minY());
|
||||
assertEquals(51, start.getBoundingBox().maxY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void referenceEnvelopeDoesNotAddMonumentPieces() {
|
||||
StructureStart start = monumentStart(1337L);
|
||||
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
|
||||
start,
|
||||
start.getStructure(),
|
||||
0,
|
||||
new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
|
||||
.setHorizontalPadding(4));
|
||||
|
||||
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
wrapped, "minecraft:monument", 0, 63, -64, 320,
|
||||
false, false, null, (x, z) -> 0);
|
||||
|
||||
assertEquals(0, offset);
|
||||
assertEquals(1, wrapped.getPieces().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void vanillaReloadRegenerationIsRealignedBeforePlacement() {
|
||||
long seed = 1337L;
|
||||
ChunkPos chunkPos = new ChunkPos(0, 0);
|
||||
StructureStart initial = monumentStart(seed);
|
||||
NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
initial, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
|
||||
|
||||
PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad(
|
||||
chunkPos, seed, new PiecesContainer(initial.getPieces()));
|
||||
StructureStart reloaded = new StructureStart(
|
||||
monumentStructure(), chunkPos, 0, regenerated);
|
||||
|
||||
assertEquals(39, reloaded.getBoundingBox().minY());
|
||||
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
reloaded, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
|
||||
assertEquals(-13, offset);
|
||||
assertEquals(26, reloaded.getBoundingBox().minY());
|
||||
assertEquals(48, reloaded.getBoundingBox().maxY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void neighboringChunkPlacementRealignsReloadedMonumentOnce() throws Exception {
|
||||
long seed = 1337L;
|
||||
ChunkPos chunkPos = new ChunkPos(0, 0);
|
||||
StructureStart initial = monumentStart(seed);
|
||||
NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
initial, "minecraft:monument", 0, 50, -256, 512,
|
||||
false, false, null, (x, z) -> 0);
|
||||
PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad(
|
||||
chunkPos, seed, new PiecesContainer(initial.getPieces()));
|
||||
StructureStart reloaded = new StructureStart(
|
||||
monumentStructure(), chunkPos, 0, regenerated);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(8);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
List<Future<?>> futures = new ArrayList<>();
|
||||
try {
|
||||
for (int task = 0; task < 16; task++) {
|
||||
futures.add(executor.submit(() -> {
|
||||
release.await();
|
||||
NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment(
|
||||
reloaded, "minecraft:monument", 0, 50, -256, 512);
|
||||
return null;
|
||||
}));
|
||||
}
|
||||
release.countDown();
|
||||
for (Future<?> future : futures) {
|
||||
future.get();
|
||||
}
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
assertEquals(26, reloaded.getBoundingBox().minY());
|
||||
assertEquals(48, reloaded.getBoundingBox().maxY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void impossibleSeaLevelAlignmentFailsInsteadOfClippingTheMonument() {
|
||||
StructureStart start = monumentStart(1337L);
|
||||
try {
|
||||
NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
start, "minecraft:monument", 0, -50, -64, 320, false, false, null, (x, z) -> 0);
|
||||
} catch (IllegalStateException error) {
|
||||
assertTrue(error.getMessage().contains("cannot align"));
|
||||
return;
|
||||
}
|
||||
throw new AssertionError("Expected an out-of-bounds monument alignment to fail");
|
||||
}
|
||||
|
||||
private static StructureStart monumentStart(long seed) {
|
||||
ChunkPos chunkPos = new ChunkPos(0, 0);
|
||||
OceanMonumentPieces.MonumentBuilding building = new OceanMonumentPieces.MonumentBuilding(
|
||||
RandomSource.create(seed), -29, -29, Direction.NORTH);
|
||||
return new StructureStart(
|
||||
monumentStructure(), chunkPos, 0, new PiecesContainer(List.of(building)));
|
||||
}
|
||||
|
||||
private static OceanMonumentStructure monumentStructure() {
|
||||
return new OceanMonumentStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||
}
|
||||
|
||||
private static OceanMonumentPieces.MonumentBuilding monumentBuilding(StructureStart start) {
|
||||
return (OceanMonumentPieces.MonumentBuilding) start.getPieces().get(0);
|
||||
}
|
||||
|
||||
private static int[] minimumYs(List<StructurePiece> pieces) {
|
||||
int[] ys = new int[pieces.size()];
|
||||
for (int i = 0; i < pieces.size(); i++) {
|
||||
ys[i] = pieces.get(i).getBoundingBox().minY();
|
||||
}
|
||||
return ys;
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelAccessor;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.JungleTemplePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.JungleTempleStructure;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructurePostProcessorScatteredFeatureTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void desertPyramidUsesTheLowestFootprintSurfaceAndLocksVanillaReanchor() {
|
||||
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(11L), 0, 0);
|
||||
StructureStart start = desertStart(piece);
|
||||
BoundingBox cachedBounds = start.getBoundingBox();
|
||||
BoundingBox footprint = piece.getBoundingBox();
|
||||
|
||||
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null,
|
||||
(x, z) -> x == footprint.maxX() && z == footprint.maxZ() ? 64 : 92);
|
||||
|
||||
assertEquals(1, offset);
|
||||
assertSame(cachedBounds, start.getBoundingBox());
|
||||
assertEquals(65, piece.getBoundingBox().minY());
|
||||
assertEquals(65, start.getBoundingBox().minY());
|
||||
assertTrue(piece.attemptVanillaAlignment(-2));
|
||||
assertEquals(65, piece.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jungleTempleUsesAverageFootprintSurfaceAndHonorsConfiguredShift() {
|
||||
TestJungleTemplePiece piece = new TestJungleTemplePiece(RandomSource.create(19L), 32, -16);
|
||||
StructureStart start = jungleStart(piece);
|
||||
BoundingBox footprint = piece.getBoundingBox();
|
||||
|
||||
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
start, "minecraft:jungle_pyramid", 4, 63, -64, 320, false, false, null,
|
||||
(x, z) -> x == footprint.minX() && z == footprint.minZ() ? 88 : 70);
|
||||
|
||||
assertEquals(11, offset);
|
||||
assertEquals(75, piece.getBoundingBox().minY());
|
||||
assertTrue(piece.attemptVanillaAlignment());
|
||||
assertEquals(75, piece.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedAlignmentIsIdempotent() {
|
||||
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(23L), 0, 0);
|
||||
StructureStart start = desertStart(piece);
|
||||
|
||||
int initialOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80);
|
||||
int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80);
|
||||
|
||||
assertEquals(17, initialOffset);
|
||||
assertEquals(0, repeatedOffset);
|
||||
assertEquals(81, piece.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scatteredAlignmentStaysInsideWorldBounds() {
|
||||
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(29L), 0, 0);
|
||||
StructureStart start = desertStart(piece);
|
||||
|
||||
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 318);
|
||||
|
||||
assertEquals(241, offset);
|
||||
assertEquals(305, piece.getBoundingBox().minY());
|
||||
assertEquals(319, start.getBoundingBox().maxY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scatteredStructuresDoNotOptIntoFullFootprintSurfaceFitting() {
|
||||
TestDesertPyramidPiece desert = new TestDesertPyramidPiece(RandomSource.create(31L), 0, 0);
|
||||
TestJungleTemplePiece jungle = new TestJungleTemplePiece(RandomSource.create(37L), 0, 0);
|
||||
SwampHutPiece swamp = new SwampHutPiece(RandomSource.create(41L), 0, 0);
|
||||
StructureStart desertStart = desertStart(desert);
|
||||
StructureStart jungleStart = jungleStart(jungle);
|
||||
StructureStart swampStart = swampStart(swamp);
|
||||
|
||||
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(desertStart));
|
||||
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(jungleStart));
|
||||
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(swampStart));
|
||||
|
||||
AtomicInteger terrainQueries = new AtomicInteger();
|
||||
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
swampStart, "minecraft:swamp_hut", 0, 63, -64, 320, false, false, null,
|
||||
(x, z) -> terrainQueries.incrementAndGet());
|
||||
assertEquals(0, offset);
|
||||
assertEquals(0, terrainQueries.get());
|
||||
assertEquals(64, swamp.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scatteredHeightFieldMatchesTheRuntimeContract() {
|
||||
Field field = NativeStructureReflection.resolveScatteredHeightPositionField();
|
||||
|
||||
assertEquals(ScatteredFeaturePiece.class, field.getDeclaringClass());
|
||||
assertEquals(int.class, field.getType());
|
||||
assertTrue(Modifier.isProtected(field.getModifiers()));
|
||||
assertFalse(Modifier.isFinal(field.getModifiers()));
|
||||
assertFalse(Modifier.isStatic(field.getModifiers()));
|
||||
assertTrue(field.trySetAccessible());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void datapackOverridesOfSpecialVanillaIdsUseGenericPlacement() {
|
||||
StructureStart pyramidOverride = swampStart(
|
||||
new SwampHutPiece(RandomSource.create(43L), 0, 0));
|
||||
int pyramidMinY = pyramidOverride.getBoundingBox().minY();
|
||||
int pyramidOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
|
||||
pyramidOverride, "minecraft:desert_pyramid", 5,
|
||||
63, -64, 320, false, false, null, (x, z) -> 200);
|
||||
|
||||
assertEquals(5, pyramidOffset);
|
||||
assertEquals(pyramidMinY + 5, pyramidOverride.getBoundingBox().minY());
|
||||
|
||||
StructureStart monumentOverride = swampStart(
|
||||
new SwampHutPiece(RandomSource.create(47L), 0, 0));
|
||||
int monumentMinY = monumentOverride.getBoundingBox().minY();
|
||||
NativeStructureVerticalPlacer.ensureMonumentSeaLevelAlignment(
|
||||
monumentOverride, "minecraft:monument", 0, 40, -64, 320);
|
||||
|
||||
assertEquals(monumentMinY, monumentOverride.getBoundingBox().minY());
|
||||
}
|
||||
|
||||
private static StructureStart desertStart(StructurePiece piece) {
|
||||
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||
return start(structure, piece);
|
||||
}
|
||||
|
||||
private static StructureStart jungleStart(StructurePiece piece) {
|
||||
Structure structure = new JungleTempleStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||
return start(structure, piece);
|
||||
}
|
||||
|
||||
private static StructureStart swampStart(StructurePiece piece) {
|
||||
Structure structure = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||
return start(structure, piece);
|
||||
}
|
||||
|
||||
private static StructureStart start(Structure structure, StructurePiece piece) {
|
||||
return new StructureStart(
|
||||
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
|
||||
}
|
||||
|
||||
private static final class TestDesertPyramidPiece extends DesertPyramidPiece {
|
||||
private TestDesertPyramidPiece(RandomSource random, int west, int north) {
|
||||
super(random, west, north);
|
||||
}
|
||||
|
||||
private boolean attemptVanillaAlignment(int offset) {
|
||||
return updateHeightPositionToLowestGroundHeight((LevelAccessor) null, offset);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TestJungleTemplePiece extends JungleTemplePiece {
|
||||
private TestJungleTemplePiece(RandomSource random, int west, int north) {
|
||||
super(random, west, north);
|
||||
}
|
||||
|
||||
private boolean attemptVanillaAlignment() {
|
||||
return updateAverageGroundHeight((LevelAccessor) null, null, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2900
File diff suppressed because it is too large
Load Diff
+354
@@ -0,0 +1,354 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.IdMapper;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.PalettedContainer;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.Strategy;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructurePostProcessorVegetationTest {
|
||||
private static final long TEST_SEED = 1234L;
|
||||
private static final int MIN_Y = -64;
|
||||
private static final int MAX_Y = 319;
|
||||
private static final int GROUND_Y = 64;
|
||||
private static final BoundingBox AREA = new BoundingBox(0, MIN_Y, 0, 15, MAX_Y, 15);
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canopyAboveTheStructureRoofSurvivesVegetationClearing() {
|
||||
ProtoChunk chunk = chunk();
|
||||
StructureStart start = desertStart(0, 0, GROUND_Y + 15);
|
||||
BoundingBox bounds = start.getBoundingBox();
|
||||
for (int y = bounds.maxY() - 1; y <= GROUND_Y + 40; y++) {
|
||||
fillLayer(chunk, y, leaves());
|
||||
}
|
||||
write(chunk, 4, bounds.minY() - 1, 4, log());
|
||||
|
||||
clearVegetation(chunk, start);
|
||||
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
assertEquals(leaves(), chunk.getBlockState(new BlockPos(x, GROUND_Y + 40, z)));
|
||||
assertEquals(leaves(), chunk.getBlockState(new BlockPos(x, bounds.maxY() + 1, z)));
|
||||
assertEquals(air(), chunk.getBlockState(new BlockPos(x, bounds.maxY(), z)));
|
||||
assertEquals(air(), chunk.getBlockState(new BlockPos(x, bounds.maxY() - 1, z)));
|
||||
}
|
||||
}
|
||||
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 1, 4)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buriedStructuresPreserveTheForestAboveThem() {
|
||||
ProtoChunk chunk = chunk();
|
||||
StructureStart start = desertStart(0, 0, -20);
|
||||
write(chunk, 8, GROUND_Y + 1, 8, log());
|
||||
write(chunk, 8, GROUND_Y + 2, 8, log());
|
||||
write(chunk, 8, GROUND_Y + 3, 8, log());
|
||||
write(chunk, 8, GROUND_Y + 4, 8, leaves());
|
||||
|
||||
clearVegetation(chunk, start);
|
||||
|
||||
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 1, 8)));
|
||||
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 2, 8)));
|
||||
assertEquals(log(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 3, 8)));
|
||||
assertEquals(leaves(), chunk.getBlockState(new BlockPos(8, GROUND_Y + 4, 8)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void columnsOutsideThePieceFootprintKeepEveryTreeBlock() {
|
||||
ProtoChunk chunk = chunk();
|
||||
StructureStart start = desertStart(8, 8, GROUND_Y + 15);
|
||||
BoundingBox bounds = start.getBoundingBox();
|
||||
int inside = bounds.minY() + 4;
|
||||
write(chunk, 0, inside, 0, log());
|
||||
write(chunk, 0, GROUND_Y + 40, 0, leaves());
|
||||
write(chunk, 12, inside, 12, log());
|
||||
write(chunk, 12, GROUND_Y + 40, 12, leaves());
|
||||
|
||||
clearVegetation(chunk, start);
|
||||
|
||||
assertEquals(log(), chunk.getBlockState(new BlockPos(0, inside, 0)));
|
||||
assertEquals(leaves(), chunk.getBlockState(new BlockPos(0, GROUND_Y + 40, 0)));
|
||||
assertEquals(air(), chunk.getBlockState(new BlockPos(12, inside, 12)));
|
||||
assertEquals(leaves(), chunk.getBlockState(new BlockPos(12, GROUND_Y + 40, 12)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearingStopsAtThePieceEnvelopeBoundaries() {
|
||||
ProtoChunk chunk = chunk();
|
||||
StructureStart start = desertStart(0, 0, GROUND_Y + 15);
|
||||
BoundingBox bounds = start.getBoundingBox();
|
||||
write(chunk, 4, bounds.maxY() + 1, 4, log());
|
||||
write(chunk, 4, bounds.maxY(), 4, log());
|
||||
write(chunk, 4, bounds.minY() - 1, 4, log());
|
||||
write(chunk, 4, bounds.minY() - 2, 4, log());
|
||||
|
||||
clearVegetation(chunk, start);
|
||||
|
||||
assertEquals(log(), chunk.getBlockState(new BlockPos(4, bounds.maxY() + 1, 4)));
|
||||
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.maxY(), 4)));
|
||||
assertEquals(air(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 1, 4)));
|
||||
assertEquals(log(), chunk.getBlockState(new BlockPos(4, bounds.minY() - 2, 4)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overlappingPieceEnvelopesMergeIntoOneClearedRange() {
|
||||
ProtoChunk chunk = chunk();
|
||||
StructureStart low = desertStart(0, 0, GROUND_Y);
|
||||
StructureStart high = desertStart(0, 0, GROUND_Y + 40);
|
||||
int between = GROUND_Y + 20;
|
||||
write(chunk, 4, between, 4, log());
|
||||
write(chunk, 4, high.getBoundingBox().maxY() + 1, 4, log());
|
||||
write(chunk, 4, low.getBoundingBox().minY() - 2, 4, log());
|
||||
|
||||
NativeStructureVegetationClearer.clearIntersectingVegetation(
|
||||
world(chunk), chunk, AREA, List.of(low, high));
|
||||
|
||||
assertEquals(air(), chunk.getBlockState(new BlockPos(4, between, 4)));
|
||||
assertEquals(log(), chunk.getBlockState(
|
||||
new BlockPos(4, high.getBoundingBox().maxY() + 1, 4)));
|
||||
assertEquals(log(), chunk.getBlockState(
|
||||
new BlockPos(4, low.getBoundingBox().minY() - 2, 4)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validStartsReachIntersectionProcessing() {
|
||||
assertTrue(NativeStructureVegetationClearer.shouldProcessStart(
|
||||
desertStart(0, 0, GROUND_Y + 15)));
|
||||
assertFalse(NativeStructureVegetationClearer.shouldProcessStart(
|
||||
StructureStart.INVALID_START));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allUndergroundGenerationStepsShareOneClassification() {
|
||||
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
|
||||
GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
|
||||
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
|
||||
GenerationStep.Decoration.UNDERGROUND_DECORATION));
|
||||
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
|
||||
GenerationStep.Decoration.STRONGHOLDS));
|
||||
assertFalse(NativeStructureVegetationClearer.isUndergroundStep(
|
||||
GenerationStep.Decoration.SURFACE_STRUCTURES));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void undergroundStructuresUseTheLowestTerrainColumn() {
|
||||
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
|
||||
int offset = NativeStructureVerticalPlacer.resolveBuriedOffset(
|
||||
bounds, 0, -64, 320, (x, z) -> x == 1 ? 76 : 100);
|
||||
assertEquals(-5, offset);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void undergroundBurialClampsToTheWorldFloorInsteadOfFailing() {
|
||||
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
|
||||
assertEquals(-2, NativeStructureVerticalPlacer.resolveBuriedOffset(
|
||||
bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loweringSurfaceRemovesCactusStacksAndTheirFlowers() {
|
||||
ProtoChunk chunk = chunk();
|
||||
for (int y = 80; y <= 102; y++) {
|
||||
write(chunk, 4, y, 4, Blocks.SAND.defaultBlockState());
|
||||
}
|
||||
for (int y = 103; y <= 105; y++) {
|
||||
write(chunk, 4, y, 4, Blocks.CACTUS.defaultBlockState());
|
||||
}
|
||||
write(chunk, 4, 106, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
|
||||
|
||||
NativeStructureSurfaceFitter.applySurfaceColumn(
|
||||
world(chunk), new BlockPos.MutableBlockPos(), 4, 4, 102, 93, MIN_Y, MAX_Y);
|
||||
|
||||
assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 93, 4)));
|
||||
for (int y = 94; y <= 106; y++) {
|
||||
assertTrue("Unsupported decoration at " + y, chunk.getBlockState(new BlockPos(4, y, 4)).isAir());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loweringSurfacePreservesCactusOnUnchangedColumns() {
|
||||
ProtoChunk chunk = chunk();
|
||||
for (int x : List.of(4, 5)) {
|
||||
for (int y = 60; y <= 64; y++) {
|
||||
write(chunk, x, y, 4, Blocks.SAND.defaultBlockState());
|
||||
}
|
||||
write(chunk, x, 65, 4, Blocks.CACTUS.defaultBlockState());
|
||||
write(chunk, x, 66, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
|
||||
}
|
||||
WorldGenLevel world = world(chunk);
|
||||
|
||||
NativeStructureSurfaceFitter.applySurfaceColumn(
|
||||
world, new BlockPos.MutableBlockPos(), 4, 4, 64, 60, MIN_Y, MAX_Y);
|
||||
NativeStructureSurfaceFitter.applySurfaceColumn(
|
||||
world, new BlockPos.MutableBlockPos(), 5, 4, 64, 64, MIN_Y, MAX_Y);
|
||||
|
||||
assertTrue(chunk.getBlockState(new BlockPos(4, 65, 4)).isAir());
|
||||
assertTrue(chunk.getBlockState(new BlockPos(4, 66, 4)).isAir());
|
||||
assertEquals(Blocks.SAND.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 64, 4)));
|
||||
assertEquals(Blocks.CACTUS.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 65, 4)));
|
||||
assertEquals(Blocks.CACTUS_FLOWER.defaultBlockState(), chunk.getBlockState(new BlockPos(5, 66, 4)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loweringSurfaceKeepsTreeAndSolidObjectBoundaries() {
|
||||
for (BlockState boundary : List.of(Blocks.OAK_LOG.defaultBlockState(), Blocks.OAK_PLANKS.defaultBlockState())) {
|
||||
ProtoChunk chunk = chunk();
|
||||
write(chunk, 4, 63, 4, Blocks.SAND.defaultBlockState());
|
||||
write(chunk, 4, 64, 4, Blocks.SAND.defaultBlockState());
|
||||
write(chunk, 4, 65, 4, boundary);
|
||||
write(chunk, 4, 66, 4, Blocks.CACTUS.defaultBlockState());
|
||||
write(chunk, 4, 67, 4, Blocks.CACTUS_FLOWER.defaultBlockState());
|
||||
|
||||
NativeStructureSurfaceFitter.applySurfaceColumn(
|
||||
world(chunk), new BlockPos.MutableBlockPos(), 4, 4, 64, 60, MIN_Y, MAX_Y);
|
||||
|
||||
assertEquals(boundary, chunk.getBlockState(new BlockPos(4, 65, 4)));
|
||||
assertEquals(Blocks.CACTUS.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 66, 4)));
|
||||
assertEquals(Blocks.CACTUS_FLOWER.defaultBlockState(), chunk.getBlockState(new BlockPos(4, 67, 4)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearVegetation(ProtoChunk chunk, StructureStart start) {
|
||||
NativeStructureVegetationClearer.clearIntersectingVegetation(
|
||||
world(chunk), chunk, AREA, List.of(start));
|
||||
}
|
||||
|
||||
private static StructureStart desertStart(int originX, int originZ, int topY) {
|
||||
Structure structure = new DesertPyramidStructure(
|
||||
new Structure.StructureSettings(
|
||||
HolderSet.empty(), Map.of(),
|
||||
GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.NONE));
|
||||
DesertPyramidPiece piece = new DesertPyramidPiece(RandomSource.create(7L), originX, originZ);
|
||||
piece.move(0, topY - piece.getBoundingBox().maxY(), 0);
|
||||
return new StructureStart(
|
||||
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
|
||||
}
|
||||
|
||||
private static ProtoChunk chunk() {
|
||||
return new ProtoChunk(
|
||||
new ChunkPos(0, 0), UpgradeData.EMPTY,
|
||||
LevelHeightAccessor.create(MIN_Y, MAX_Y - MIN_Y + 1), containerFactory(), null);
|
||||
}
|
||||
|
||||
private static void fillLayer(ProtoChunk chunk, int y, BlockState state) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
write(chunk, x, y, z, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void write(ProtoChunk chunk, int x, int y, int z, BlockState state) {
|
||||
LevelChunkSection section = chunk.getSection(chunk.getSectionIndex(y));
|
||||
section.setBlockState(x, y & 15, z, state, false);
|
||||
}
|
||||
|
||||
private static BlockState log() {
|
||||
return Blocks.OAK_LOG.defaultBlockState();
|
||||
}
|
||||
|
||||
private static BlockState leaves() {
|
||||
return Blocks.OAK_LEAVES.defaultBlockState();
|
||||
}
|
||||
|
||||
private static BlockState air() {
|
||||
return Blocks.AIR.defaultBlockState();
|
||||
}
|
||||
|
||||
private static WorldGenLevel world(ChunkAccess chunk) {
|
||||
InvocationHandler handler = (proxy, method, arguments) -> {
|
||||
String methodName = method.getName();
|
||||
if (methodName.equals("getBlockState")) {
|
||||
return chunk.getBlockState((BlockPos) arguments[0]);
|
||||
}
|
||||
if (methodName.equals("setBlock")) {
|
||||
return chunk.setBlockState(
|
||||
((BlockPos) arguments[0]).immutable(),
|
||||
(BlockState) arguments[1], (int) arguments[2]) != null;
|
||||
}
|
||||
if (methodName.equals("getSeed")) {
|
||||
return TEST_SEED;
|
||||
}
|
||||
if (methodName.equals("getLevel")) {
|
||||
return null;
|
||||
}
|
||||
if (methodName.equals("holderLookup")) {
|
||||
return BuiltInRegistries.BLOCK;
|
||||
}
|
||||
if (methodName.equals("hashCode")) {
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
if (methodName.equals("equals")) {
|
||||
return proxy == arguments[0];
|
||||
}
|
||||
if (methodName.equals("toString")) {
|
||||
return "vegetation-test-world";
|
||||
}
|
||||
throw new UnsupportedOperationException(method.toString());
|
||||
};
|
||||
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
|
||||
}
|
||||
|
||||
private static PalettedContainerFactory containerFactory() {
|
||||
Strategy<BlockState> blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
|
||||
BlockState air = Blocks.AIR.defaultBlockState();
|
||||
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
|
||||
Holder<Biome> defaultBiome = Holder.direct((Biome) null);
|
||||
biomeIds.add(defaultBiome);
|
||||
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
|
||||
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(
|
||||
name -> defaultBiome, holder -> "iris-test-biome");
|
||||
return new PalettedContainerFactory(
|
||||
blockStates,
|
||||
air,
|
||||
PalettedContainer.codecRW(BlockState.CODEC, blockStates, air),
|
||||
biomes,
|
||||
defaultBiome,
|
||||
PalettedContainer.codecRO(biomeCodec, biomes, defaultBiome),
|
||||
PalettedContainer.codecRW(biomeCodec, biomes, defaultBiome));
|
||||
}
|
||||
}
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import com.mojang.datafixers.util.Either;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.util.valueproviders.ConstantInt;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.BlackstoneReplaceProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockAgeProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockIgnoreProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.CappedProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.GravityProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.JigsawReplacementProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.LavaSubmergedBlockProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.NopProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.ProtectedBlockProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.RuleProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import org.junit.Assume;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructureTemplateOccupancyTest {
|
||||
private static final BoundingBox PROCESSING_AREA = new BoundingBox(0, 56, 0, 15, 80, 0);
|
||||
private static final int TEMPLATE_WIDTH = 32;
|
||||
private static final int TERRAIN_HEIGHT = 64;
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraft() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void straddlingGravityPieceHeightReadsHonorTheVanillaClipContract() throws Exception {
|
||||
ColumnRecorder recorder = new ColumnRecorder();
|
||||
|
||||
resolve(straddlingPiece(
|
||||
List.of(new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0))), recorder);
|
||||
|
||||
// 26.2 vanilla clips processor evaluation to the placement bounding box; 26.1.2 vanilla
|
||||
// runs processors across the whole piece. Iris mirrors the pinned version's semantics.
|
||||
assertEquals(expectedClippedColumns(), recorder.heightColumns);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cappedStraddlingPieceRunsProcessorsAcrossTheWholePiece() throws Exception {
|
||||
ColumnRecorder recorder = new ColumnRecorder();
|
||||
|
||||
resolve(straddlingPiece(List.of(
|
||||
new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0),
|
||||
new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(0)))), recorder);
|
||||
|
||||
assertEquals(columns(0, TEMPLATE_WIDTH - 1), recorder.heightColumns);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cappedProcessorIsTheOnlyProcessorThatDisablesTheProcessingAreaClip() throws Exception {
|
||||
Method contract = clipContractMethod();
|
||||
Assume.assumeTrue("evaluatesEntirePieceState only exists on 26.2+", contract != null);
|
||||
|
||||
for (StructureProcessor processor : List.of(
|
||||
BlockIgnoreProcessor.STRUCTURE_BLOCK,
|
||||
JigsawReplacementProcessor.INSTANCE,
|
||||
LavaSubmergedBlockProcessor.INSTANCE,
|
||||
NopProcessor.INSTANCE,
|
||||
BlackstoneReplaceProcessor.INSTANCE,
|
||||
new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, 0),
|
||||
new RuleProcessor(List.of()),
|
||||
new BlockAgeProcessor(0.5F),
|
||||
protectedBlockProcessor())) {
|
||||
assertFalse(processor.getClass().getName(),
|
||||
(Boolean) contract.invoke(processor));
|
||||
}
|
||||
assertTrue((Boolean) contract.invoke(
|
||||
new CappedProcessor(NopProcessor.INSTANCE, ConstantInt.of(4))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void placementSettingsCarryTheProcessingAreaAsTheirClipBox() throws Exception {
|
||||
PoolElementStructurePiece piece = straddlingPiece(List.of());
|
||||
|
||||
StructurePlaceSettings settings = NativeStructureReflection.resolvePlacementSettings(
|
||||
(SinglePoolElement) piece.getElement(), piece, PROCESSING_AREA);
|
||||
|
||||
assertEquals(PROCESSING_AREA, settings.getBoundingBox());
|
||||
}
|
||||
|
||||
private static void resolve(PoolElementStructurePiece piece, ColumnRecorder recorder) {
|
||||
NativeStructureTemplateOccupancy.resolve(
|
||||
world(recorder), piece, piece.getPosition(), PROCESSING_AREA,
|
||||
NativeStructureTemplateOccupancyTest::forbiddenTemplateManager,
|
||||
PROCESSING_AREA::isInside, amount -> {
|
||||
});
|
||||
}
|
||||
|
||||
private static PoolElementStructurePiece straddlingPiece(
|
||||
List<StructureProcessor> processors) throws Exception {
|
||||
BoundingBox bounds = new BoundingBox(0, 70, 0, TEMPLATE_WIDTH - 1, 76, 0);
|
||||
return new PoolElementStructurePiece(
|
||||
null, new InlineSinglePoolElement(template(), processors),
|
||||
new BlockPos(bounds.minX(), bounds.minY(), bounds.minZ()),
|
||||
1, Rotation.NONE, bounds, LiquidSettings.APPLY_WATERLOGGING);
|
||||
}
|
||||
|
||||
private static Set<Integer> columns(int minimumX, int maximumX) {
|
||||
Set<Integer> columns = new TreeSet<>();
|
||||
for (int x = minimumX; x <= maximumX; x++) {
|
||||
columns.add(x);
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
private static Method clipContractMethod() {
|
||||
try {
|
||||
return StructureProcessor.class.getMethod("evaluatesEntirePieceState");
|
||||
} catch (NoSuchMethodException absent) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<Integer> expectedClippedColumns() {
|
||||
return clipContractMethod() != null
|
||||
? columns(PROCESSING_AREA.minX(), PROCESSING_AREA.maxX())
|
||||
: columns(0, TEMPLATE_WIDTH - 1);
|
||||
}
|
||||
|
||||
private static StructureProcessor protectedBlockProcessor() throws Exception {
|
||||
// Constructed reflectively: the constructor takes HolderSet on 26.2 and TagKey on 26.1.2.
|
||||
Constructor<?> constructor = ProtectedBlockProcessor.class.getConstructors()[0];
|
||||
Class<?> parameter = constructor.getParameterTypes()[0];
|
||||
Object argument = parameter == HolderSet.class
|
||||
? HolderSet.empty()
|
||||
: TagKey.create(Registries.BLOCK, Identifier.fromNamespaceAndPath("minecraft", "air"));
|
||||
return (StructureProcessor) constructor.newInstance(argument);
|
||||
}
|
||||
|
||||
private static StructureTemplateManager forbiddenTemplateManager() {
|
||||
throw new AssertionError("Inline templates must not resolve a template manager");
|
||||
}
|
||||
|
||||
private static StructureTemplate template() throws Exception {
|
||||
List<StructureTemplate.StructureBlockInfo> blocks = new ArrayList<>();
|
||||
for (int x = 0; x < TEMPLATE_WIDTH; x++) {
|
||||
blocks.add(new StructureTemplate.StructureBlockInfo(
|
||||
new BlockPos(x, 0, 0), Blocks.COBBLESTONE.defaultBlockState(), null));
|
||||
}
|
||||
Constructor<StructureTemplate.Palette> constructor =
|
||||
StructureTemplate.Palette.class.getDeclaredConstructor(List.class);
|
||||
assertTrue(constructor.trySetAccessible());
|
||||
StructureTemplate template = new StructureTemplate();
|
||||
template.palettes.add(constructor.newInstance(blocks));
|
||||
return template;
|
||||
}
|
||||
|
||||
private static WorldGenLevel world(ColumnRecorder recorder) {
|
||||
InvocationHandler handler = (proxy, method, arguments) -> {
|
||||
String methodName = method.getName();
|
||||
if (methodName.equals("getHeight") && arguments.length == 3) {
|
||||
recorder.heightColumns.add((Integer) arguments[1]);
|
||||
return TERRAIN_HEIGHT;
|
||||
}
|
||||
if (methodName.equals("getBlockState")) {
|
||||
recorder.stateColumns.add(((BlockPos) arguments[0]).getX());
|
||||
return Blocks.STONE.defaultBlockState();
|
||||
}
|
||||
if (methodName.equals("getLevel")) {
|
||||
return null;
|
||||
}
|
||||
if (methodName.equals("holderLookup")) {
|
||||
return BuiltInRegistries.BLOCK;
|
||||
}
|
||||
if (methodName.equals("hashCode")) {
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
if (methodName.equals("equals")) {
|
||||
return proxy == arguments[0];
|
||||
}
|
||||
if (methodName.equals("toString")) {
|
||||
return "occupancy-test-world";
|
||||
}
|
||||
throw new UnsupportedOperationException(method.toString());
|
||||
};
|
||||
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||
WorldGenLevel.class.getClassLoader(),
|
||||
new Class<?>[]{WorldGenLevel.class}, handler);
|
||||
}
|
||||
|
||||
private static final class ColumnRecorder {
|
||||
private final Set<Integer> heightColumns = new TreeSet<>();
|
||||
private final Set<Integer> stateColumns = new TreeSet<>();
|
||||
}
|
||||
|
||||
private static final class InlineSinglePoolElement extends SinglePoolElement {
|
||||
private InlineSinglePoolElement(
|
||||
StructureTemplate template, List<StructureProcessor> processors) {
|
||||
super(Either.right(template),
|
||||
Holder.direct(new StructureProcessorList(processors)),
|
||||
StructureTemplatePool.Projection.RIGID,
|
||||
Optional.<LiquidSettings>empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
+561
@@ -0,0 +1,561 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureVolume;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NativeStructureVolumeIndexTest {
|
||||
private static final String STRUCTURE_KEY = "minecraft:swamp_hut";
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void assembledPiecesBecomeWorldSpacePieceVolumes() {
|
||||
StructureStart start = swampHut(0, 0, 4, 6);
|
||||
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
|
||||
|
||||
KList<NativeStructureVolume> volumes = NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, start);
|
||||
|
||||
assertEquals(1, volumes.size());
|
||||
NativeStructureVolume volume = volumes.getFirst();
|
||||
assertEquals(STRUCTURE_KEY, volume.structure());
|
||||
assertEquals(bounds.minX(), volume.minX());
|
||||
assertEquals(bounds.minY(), volume.minY());
|
||||
assertEquals(bounds.minZ(), volume.minZ());
|
||||
assertEquals(bounds.maxX(), volume.maxX());
|
||||
assertEquals(bounds.maxY(), volume.maxY());
|
||||
assertEquals(bounds.maxZ(), volume.maxZ());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidStartsContributeNoVolumes() {
|
||||
assertNull(NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START));
|
||||
assertNull(NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coldAndCachedQueriesResolveIdenticalVolumes() {
|
||||
CountingResolver resolver = new CountingResolver(0, 0);
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
|
||||
KList<NativeStructureVolume> cold = index.resolve(null, 0, 0, 15, 15);
|
||||
int coldResolutions = resolver.resolutions();
|
||||
KList<NativeStructureVolume> cached = index.resolve(null, 0, 0, 15, 15);
|
||||
|
||||
assertTrue(coldResolutions > 0);
|
||||
assertFalse(cold.isEmpty());
|
||||
assertEquals(cold, cached);
|
||||
assertEquals(coldResolutions, resolver.resolutions());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runtimeBucketsStayIsolatedAndRetirementEvictsOnlyTheirEntries() {
|
||||
AtomicInteger runtimeId = new AtomicInteger(1);
|
||||
Engine engine = runtimeEngine(runtimeId);
|
||||
CountingResolver resolver = new CountingResolver(0, 0);
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
|
||||
index.resolve(engine, 0, 0, 15, 15);
|
||||
assertEquals(289, resolver.resolutions());
|
||||
index.resolve(engine, 0, 0, 15, 15);
|
||||
assertEquals(289, resolver.resolutions());
|
||||
|
||||
runtimeId.set(2);
|
||||
index.resolve(engine, 0, 0, 15, 15);
|
||||
assertEquals(578, resolver.resolutions());
|
||||
index.resolve(engine, 0, 0, 15, 15);
|
||||
assertEquals(578, resolver.resolutions());
|
||||
|
||||
index.evictRuntime(1);
|
||||
runtimeId.set(1);
|
||||
index.resolve(engine, 0, 0, 15, 15);
|
||||
assertEquals(867, resolver.resolutions());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pinnedRuntimeReusesCachedOriginsWithoutOpeningCoordinateScopes() throws Exception {
|
||||
ScopedEngine scoped = scopedEngine(true, 1);
|
||||
CountingResolver resolver = new CountingResolver(0, 0);
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
|
||||
KList<NativeStructureVolume> first = index.resolve(scoped.engine(), 0, 0, 15, 15);
|
||||
assertEquals(289, scoped.scopes().get());
|
||||
index.resolve(scoped.engine(), 16, 0, 31, 15);
|
||||
assertEquals(306, resolver.resolutions());
|
||||
assertEquals(306, scoped.scopes().get());
|
||||
assertEquals(first, index.resolve(scoped.engine(), 0, 0, 15, 15));
|
||||
assertEquals(306, scoped.scopes().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void absentOrMismatchedRuntimeOwnershipKeepsCoordinateRouting() throws Exception {
|
||||
ScopedEngine detached = scopedEngine(true, 1);
|
||||
when(detached.engine().getGenerationHistoryRuntimeRouter()).thenReturn(Optional.empty());
|
||||
for (ScopedEngine scoped : List.of(scopedEngine(false, 1), scopedEngine(true, 0),
|
||||
scopedEngine(true, 2), detached)) {
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
|
||||
index.resolve(scoped.engine(), 0, 0, 15, 15);
|
||||
index.resolve(scoped.engine(), 16, 0, 31, 15);
|
||||
assertEquals(578, scoped.scopes().get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retiredPinnedOriginsRebuildThroughCoordinateScopes() throws Exception {
|
||||
ScopedEngine scoped = scopedEngine(true, 1);
|
||||
CountingResolver resolver = new CountingResolver(0, 0);
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
|
||||
KList<NativeStructureVolume> first = index.resolve(scoped.engine(), 0, 0, 15, 15);
|
||||
index.evictRuntime(1);
|
||||
assertEquals(first, index.resolve(scoped.engine(), 0, 0, 15, 15));
|
||||
assertEquals(578, resolver.resolutions());
|
||||
assertEquals(578, scoped.scopes().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidatedIndexReceivesRuntimeRetirementAndDetachesOnUninstall() throws Exception {
|
||||
IrisEngine engine = mock(IrisEngine.class);
|
||||
Set<IntConsumer> listeners = new CopyOnWriteArraySet<>();
|
||||
doAnswer(invocation -> {
|
||||
listeners.add(invocation.getArgument(0));
|
||||
return null;
|
||||
}).when(engine).addGenerationRuntimeRetirementListener(any());
|
||||
doAnswer(invocation -> {
|
||||
listeners.remove(invocation.getArgument(0));
|
||||
return null;
|
||||
}).when(engine).removeGenerationRuntimeRetirementListener(any());
|
||||
NativeStructureVolumeIndex.Context context = new NativeStructureVolumeIndex.Context(
|
||||
null, null, null, null, () -> null, () -> null, () -> null);
|
||||
NativeStructureVolumeIndex.install(engine, context);
|
||||
try {
|
||||
assertEquals(1, listeners.size());
|
||||
IntConsumer original = listeners.iterator().next();
|
||||
NativeStructureVolumeIndex.invalidate(engine);
|
||||
assertEquals(1, listeners.size());
|
||||
assertFalse(listeners.contains(original));
|
||||
|
||||
NativeStructureVolumeIndex.volumes(engine, 0, 0, 15, 15);
|
||||
NativeStructureVolumeIndex current = installedIndexes().get(engine);
|
||||
assertEquals(289, cache(current, "originCache").size());
|
||||
assertEquals(1, cache(current, "queryCache").size());
|
||||
for (IntConsumer listener : listeners) {
|
||||
listener.accept(0);
|
||||
}
|
||||
assertTrue(cache(current, "originCache").isEmpty());
|
||||
assertTrue(cache(current, "queryCache").isEmpty());
|
||||
} finally {
|
||||
NativeStructureVolumeIndex.uninstall(engine);
|
||||
}
|
||||
assertTrue(listeners.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retiringOriginBuildCannotRepublishWhileQueryEvictionWaits() throws Exception {
|
||||
BlockingResolver resolver = new BlockingResolver();
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
Map<?, ?> origins = cache(index, "originCache");
|
||||
Map<?, ?> queries = cache(index, "queryCache");
|
||||
try {
|
||||
Future<KList<NativeStructureVolume>> builder = executor.submit(() -> index.originVolumes(null, 0, 0));
|
||||
assertTrue(resolver.awaitFirstEntry());
|
||||
index.originVolumes(null, 1, 0);
|
||||
assertEquals(1, origins.size());
|
||||
Future<?> retirement;
|
||||
synchronized (queries) {
|
||||
retirement = executor.submit(() -> index.evictRuntime(0));
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
|
||||
boolean cleared = false;
|
||||
while (System.nanoTime() < deadline) {
|
||||
synchronized (origins) {
|
||||
cleared = origins.isEmpty();
|
||||
}
|
||||
if (cleared) {
|
||||
break;
|
||||
}
|
||||
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10L));
|
||||
}
|
||||
assertTrue("Origin eviction did not complete before the query cache lock", cleared);
|
||||
resolver.release();
|
||||
assertTrue(builder.get(5, TimeUnit.SECONDS).isEmpty());
|
||||
synchronized (origins) {
|
||||
assertTrue("The retired in-flight origin was cached again", origins.isEmpty());
|
||||
}
|
||||
}
|
||||
retirement.get(5, TimeUnit.SECONDS);
|
||||
index.originVolumes(null, 0, 0);
|
||||
assertEquals(3, resolver.resolutions());
|
||||
} finally {
|
||||
resolver.release();
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void independentIndexesResolveIdenticalVolumesFromTheSameSeed() {
|
||||
NativeStructureVolumeIndex first = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
|
||||
NativeStructureVolumeIndex second = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
|
||||
|
||||
assertEquals(first.resolve(null, 0, 0, 15, 15), second.resolve(null, 0, 0, 15, 15));
|
||||
assertEquals(first.resolve(null, -32, -32, 47, 47), second.resolve(null, -32, -32, 47, 47));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void volumesOutsideTheQueryRectAreExcluded() {
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
|
||||
BoundingBox bounds = swampHut(0, 0, 4, 6).getPieces().getFirst().getBoundingBox();
|
||||
|
||||
KList<NativeStructureVolume> hit = index.resolve(null, bounds.minX(), bounds.minZ(), bounds.maxX(), bounds.maxZ());
|
||||
KList<NativeStructureVolume> miss = index.resolve(null,
|
||||
bounds.maxX() + 1, bounds.minZ(), bounds.maxX() + 8, bounds.maxZ());
|
||||
|
||||
assertEquals(1, hit.size());
|
||||
assertTrue(miss.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startsWithinTheOriginReachContributeAndBeyondItDoNot() {
|
||||
int reach = NativeStructureVolumeIndex.originReachChunks();
|
||||
NativeStructureVolumeIndex reachable = NativeStructureVolumeIndex.forTesting(new CountingResolver(reach, 0));
|
||||
NativeStructureVolumeIndex unreachable = NativeStructureVolumeIndex.forTesting(new CountingResolver(reach + 1, 0));
|
||||
|
||||
assertFalse(reachable.resolve(null, 0, 0, 15, 15).isEmpty());
|
||||
assertTrue(unreachable.resolve(null, 0, 0, 15, 15).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overlappingColdQueriesCoordinateBeforeResolvingOrigins() throws Exception {
|
||||
BlockingResolver resolver = new BlockingResolver();
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<KList<NativeStructureVolume>> first = executor.submit(() -> index.resolve(null, 0, 0, 15, 15));
|
||||
assertTrue(resolver.awaitFirstEntry());
|
||||
Future<KList<NativeStructureVolume>> adjacent = executor.submit(() -> index.resolve(null, 16, 0, 31, 15));
|
||||
|
||||
assertFalse(resolver.awaitSecondEntry(250));
|
||||
resolver.release();
|
||||
assertTrue(first.get(5, TimeUnit.SECONDS).isEmpty());
|
||||
assertTrue(adjacent.get(5, TimeUnit.SECONDS).isEmpty());
|
||||
assertEquals(306, resolver.resolutions());
|
||||
} finally {
|
||||
resolver.release();
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contendedNativeWindowsAllowQueuedTerrainWorkToRun() throws Exception {
|
||||
assertQueuedTerrainCanComplete(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interruptedNativeWindowWaitStillAcquiresAndReleasesOnce() throws Exception {
|
||||
assertQueuedTerrainCanComplete(true);
|
||||
}
|
||||
|
||||
private static void assertQueuedTerrainCanComplete(boolean interruptWaiter) throws Exception {
|
||||
CountDownLatch ownerEntered = new CountDownLatch(1);
|
||||
CompletableFuture<Void> terrain = new CompletableFuture<>();
|
||||
AtomicBoolean firstOrigin = new AtomicBoolean(true);
|
||||
AtomicBoolean interruptedAfterQuery = new AtomicBoolean();
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
|
||||
if (firstOrigin.compareAndSet(true, false)) {
|
||||
ownerEntered.countDown();
|
||||
terrain.join();
|
||||
}
|
||||
return new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
|
||||
-256, 0, -256, 256, 100, 256));
|
||||
});
|
||||
ExecutorService owner = Executors.newSingleThreadExecutor();
|
||||
ForkJoinPool workers = new ForkJoinPool(2);
|
||||
AtomicReference<Thread> firstWaiter = new AtomicReference<>();
|
||||
AtomicReference<Thread> secondWaiter = new AtomicReference<>();
|
||||
try {
|
||||
Future<KList<NativeStructureVolume>> original = owner.submit(
|
||||
() -> index.resolve(null, 0, 0, 15, 15));
|
||||
assertTrue(ownerEntered.await(5, TimeUnit.SECONDS));
|
||||
Future<KList<NativeStructureVolume>> adjacent = workers.submit(() -> {
|
||||
firstWaiter.set(Thread.currentThread());
|
||||
KList<NativeStructureVolume> result = index.resolve(null, 16, 0, 31, 15);
|
||||
interruptedAfterQuery.set(Thread.currentThread().isInterrupted());
|
||||
return result;
|
||||
});
|
||||
Future<KList<NativeStructureVolume>> next = workers.submit(() -> {
|
||||
secondWaiter.set(Thread.currentThread());
|
||||
return index.resolve(null, 32, 0, 47, 15);
|
||||
});
|
||||
awaitOriginWindowWaiter(firstWaiter);
|
||||
awaitOriginWindowWaiter(secondWaiter);
|
||||
if (interruptWaiter) {
|
||||
firstWaiter.get().interrupt();
|
||||
}
|
||||
|
||||
Future<?> terrainTask = workers.submit(() -> terrain.complete(null));
|
||||
terrainTask.get(3, TimeUnit.SECONDS);
|
||||
assertEquals(289, original.get(3, TimeUnit.SECONDS).size());
|
||||
KList<NativeStructureVolume> adjacentVolumes = adjacent.get(3, TimeUnit.SECONDS);
|
||||
assertEquals(289, adjacentVolumes.size());
|
||||
assertEquals(adjacentVolumes, index.resolve(null, 16, 0, 31, 15));
|
||||
assertEquals(289, next.get(3, TimeUnit.SECONDS).size());
|
||||
assertEquals(interruptWaiter, interruptedAfterQuery.get());
|
||||
} finally {
|
||||
terrain.complete(null);
|
||||
owner.shutdownNow();
|
||||
workers.shutdownNow();
|
||||
assertTrue(owner.awaitTermination(5, TimeUnit.SECONDS));
|
||||
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
private static void awaitOriginWindowWaiter(AtomicReference<Thread> reference) {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
|
||||
while (System.nanoTime() < deadline) {
|
||||
Thread thread = reference.get();
|
||||
if (thread != null && thread.getState() == Thread.State.WAITING) {
|
||||
for (StackTraceElement frame : thread.getStackTrace()) {
|
||||
if (frame.getClassName().equals(NativeStructureVolumeIndex.class.getName())
|
||||
&& frame.getMethodName().equals("lockOriginWindow")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
|
||||
}
|
||||
throw new AssertionError("Native query did not wait for the occupied origin window");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void distantColdQueriesCanResolveOriginsConcurrently() throws Exception {
|
||||
BlockingResolver resolver = new BlockingResolver();
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
int distantChunkX = findDisjointWindow(0, 0);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<KList<NativeStructureVolume>> first = executor.submit(() -> index.resolve(null, 0, 0, 15, 15));
|
||||
assertTrue(resolver.awaitFirstEntry());
|
||||
Future<KList<NativeStructureVolume>> distant = executor.submit(
|
||||
() -> index.resolve(null, distantChunkX << 4, 0, (distantChunkX << 4) + 15, 15));
|
||||
|
||||
assertTrue(resolver.awaitSecondEntry(5_000));
|
||||
resolver.release();
|
||||
assertTrue(first.get(5, TimeUnit.SECONDS).isEmpty());
|
||||
assertTrue(distant.get(5, TimeUnit.SECONDS).isEmpty());
|
||||
} finally {
|
||||
resolver.release();
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coordinatedResolutionPreservesCanonicalOriginOrder() {
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new OrderedResolver());
|
||||
|
||||
KList<NativeStructureVolume> volumes = index.resolve(null, 0, 0, 15, 15);
|
||||
|
||||
assertEquals(289, volumes.size());
|
||||
assertEquals("-8:-8", volumes.getFirst().structure());
|
||||
assertEquals("0:0", volumes.get(144).structure());
|
||||
assertEquals("8:8", volumes.getLast().structure());
|
||||
}
|
||||
|
||||
private static ScopedEngine scopedEngine(boolean scoped, int ownerRuntimeId) throws Exception {
|
||||
IrisEngine engine = mock(IrisEngine.class);
|
||||
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||
when(engine.hasGenerationRuntimeScope()).thenReturn(scoped);
|
||||
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
|
||||
when(engine.getCacheID()).thenReturn(1);
|
||||
if (ownerRuntimeId != 0) {
|
||||
IrisEngine.GenerationRuntimeBinding binding = mock(IrisEngine.GenerationRuntimeBinding.class);
|
||||
when(binding.runtimeId()).thenReturn(ownerRuntimeId);
|
||||
when(router.currentRuntimeOwnership()).thenReturn(Optional.of(
|
||||
new GenerationHistoryRuntimeRouter.RuntimeOwnership(1L, binding)));
|
||||
}
|
||||
AtomicInteger scopes = new AtomicInteger();
|
||||
when(engine.openGenerationHistoryCoordinateScope(anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||
scopes.incrementAndGet();
|
||||
return null;
|
||||
});
|
||||
return new ScopedEngine(engine, scopes);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<Engine, NativeStructureVolumeIndex> installedIndexes() throws Exception {
|
||||
Field indexes = NativeStructureVolumeIndex.class.getDeclaredField("INDEXES");
|
||||
indexes.setAccessible(true);
|
||||
return (Map<Engine, NativeStructureVolumeIndex>) indexes.get(null);
|
||||
}
|
||||
|
||||
private static Map<?, ?> cache(NativeStructureVolumeIndex index, String name) throws Exception {
|
||||
Field field = NativeStructureVolumeIndex.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return (Map<?, ?>) field.get(index);
|
||||
}
|
||||
|
||||
private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) {
|
||||
Structure source = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), x, z);
|
||||
return new StructureStart(source, new ChunkPos(chunkX, chunkZ), 0, new PiecesContainer(List.of(piece)));
|
||||
}
|
||||
|
||||
private static Engine runtimeEngine(AtomicInteger runtimeId) {
|
||||
return (Engine) Proxy.newProxyInstance(
|
||||
Engine.class.getClassLoader(),
|
||||
new Class<?>[]{Engine.class},
|
||||
(proxy, method, arguments) -> {
|
||||
if (method.getName().equals("getCacheID")) {
|
||||
return runtimeId.get();
|
||||
}
|
||||
if (method.getName().equals("hashCode")) {
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
if (method.getName().equals("equals")) {
|
||||
return proxy == arguments[0];
|
||||
}
|
||||
if (method.getName().equals("toString")) {
|
||||
return "runtime-engine-" + runtimeId.get();
|
||||
}
|
||||
throw new UnsupportedOperationException(method.getName());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static int findDisjointWindow(int chunkX, int chunkZ) {
|
||||
long sourceMask = NativeStructureVolumeIndex.originWindowStripeMask(chunkX, chunkZ);
|
||||
for (int candidate = 64; candidate < 16_384; candidate += 17) {
|
||||
if ((sourceMask & NativeStructureVolumeIndex.originWindowStripeMask(candidate, chunkZ)) == 0L) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Unable to find a disjoint native structure window stripe");
|
||||
}
|
||||
|
||||
private static final class CountingResolver implements NativeStructureVolumeIndex.OriginResolver {
|
||||
private final AtomicInteger resolutions = new AtomicInteger();
|
||||
private final int originChunkX;
|
||||
private final int originChunkZ;
|
||||
|
||||
private CountingResolver(int originChunkX, int originChunkZ) {
|
||||
this.originChunkX = originChunkX;
|
||||
this.originChunkZ = originChunkZ;
|
||||
}
|
||||
|
||||
private int resolutions() {
|
||||
return resolutions.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
|
||||
resolutions.incrementAndGet();
|
||||
if (chunkX != originChunkX || chunkZ != originChunkZ) {
|
||||
return NativeStructureVolume.NONE;
|
||||
}
|
||||
return NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BlockingResolver implements NativeStructureVolumeIndex.OriginResolver {
|
||||
private final AtomicInteger resolutions = new AtomicInteger();
|
||||
private final CountDownLatch firstEntry = new CountDownLatch(1);
|
||||
private final CountDownLatch secondEntry = new CountDownLatch(1);
|
||||
private final CountDownLatch release = new CountDownLatch(1);
|
||||
|
||||
private int resolutions() {
|
||||
return resolutions.get();
|
||||
}
|
||||
|
||||
private boolean awaitFirstEntry() throws InterruptedException {
|
||||
return firstEntry.await(5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private boolean awaitSecondEntry(long timeoutMillis) throws InterruptedException {
|
||||
return secondEntry.await(timeoutMillis, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void release() {
|
||||
release.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
|
||||
int resolution = resolutions.incrementAndGet();
|
||||
if (resolution == 1) {
|
||||
firstEntry.countDown();
|
||||
try {
|
||||
if (!release.await(5, TimeUnit.SECONDS)) {
|
||||
throw new IllegalStateException("Timed out waiting to release native structure resolution");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Interrupted while coordinating native structure resolution", e);
|
||||
}
|
||||
} else if (resolution == 2) {
|
||||
secondEntry.countDown();
|
||||
}
|
||||
return NativeStructureVolume.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class OrderedResolver implements NativeStructureVolumeIndex.OriginResolver {
|
||||
@Override
|
||||
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
|
||||
return new KList<>(new NativeStructureVolume(
|
||||
chunkX + ":" + chunkZ,
|
||||
0, 0, 0,
|
||||
15, 255, 15));
|
||||
}
|
||||
}
|
||||
private record ScopedEngine(IrisEngine engine, AtomicInteger scopes) {
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.OptionalInt;
|
||||
import java.util.function.IntSupplier;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class NativeTerrainHeightCacheTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coordinatesRuntimePredicatesAndBoundsHaveSeparateEntries() {
|
||||
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
NativeTerrainHeightCache.Query[] queries = {
|
||||
query(1, -1, 1),
|
||||
query(1, 1, -1),
|
||||
query(2, -1, 1),
|
||||
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.OCEAN_FLOOR, -256, 768),
|
||||
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.WORLD_SURFACE, -64, 768),
|
||||
new NativeTerrainHeightCache.Query(1, -1, 1, Heightmap.Types.WORLD_SURFACE, -256, 256)
|
||||
};
|
||||
for (int index = 0; index < queries.length; index++) {
|
||||
assertEquals(index + 1, cached(cache, queries[index], calls::incrementAndGet));
|
||||
}
|
||||
for (int index = 0; index < queries.length; index++) {
|
||||
assertEquals(index + 1, cached(cache, queries[index], calls::incrementAndGet));
|
||||
}
|
||||
assertEquals(queries.length, calls.get());
|
||||
cache.evictRuntime(1);
|
||||
assertEquals(3, cached(cache, queries[2], calls::incrementAndGet));
|
||||
assertEquals(7, cached(cache, queries[0], calls::incrementAndGet));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allIntegerResultsAreCacheable() {
|
||||
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||
NativeTerrainHeightCache.Query query = query(1, 0, 0);
|
||||
assertEquals(Integer.MIN_VALUE, cached(cache, query, () -> Integer.MIN_VALUE));
|
||||
assertEquals(Integer.MIN_VALUE, cached(cache, query, () -> 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedResolutionCanBeRetried() {
|
||||
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||
NativeTerrainHeightCache.Query query = query(1, 0, 0);
|
||||
assertThrows(IllegalStateException.class, () -> cached(cache, query, () -> {
|
||||
throw new IllegalStateException("terrain unavailable");
|
||||
}));
|
||||
assertEquals(42, cached(cache, query, () -> 42));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coldResolutionDoesNotHoldCacheMonitor() throws Exception {
|
||||
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<Integer> slow = executor.submit(() -> cached(cache, query(1, 0, 0), () -> {
|
||||
entered.countDown();
|
||||
try {
|
||||
assertTrue(release.await(5, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException interruption) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(interruption);
|
||||
}
|
||||
return 9;
|
||||
}));
|
||||
assertTrue(entered.await(5, TimeUnit.SECONDS));
|
||||
Future<Integer> independent = executor.submit(() -> cached(cache, query(1, 1, 0), () -> 17));
|
||||
assertEquals(Integer.valueOf(17), independent.get(1, TimeUnit.SECONDS));
|
||||
release.countDown();
|
||||
assertEquals(Integer.valueOf(9), slow.get(5, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
release.countDown();
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void boundedCacheKeepsRecentlyReadCoordinates() {
|
||||
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||
for (int x = 0; x < 65_536; x++) {
|
||||
cached(cache, query(1, x, 0), () -> 8);
|
||||
}
|
||||
assertEquals(8, cached(cache, query(1, 0, 0), () -> 99));
|
||||
cached(cache, query(1, 65_536, 0), () -> 9);
|
||||
assertEquals(8, cached(cache, query(1, 0, 0), () -> 99));
|
||||
assertEquals(99, cached(cache, query(1, 1, 0), () -> 99));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unavailableResolvedHeightIsNotCached() {
|
||||
NativeTerrainHeightCache cache = new NativeTerrainHeightCache();
|
||||
NativeTerrainHeightCache.Query query = query(1, 0, 0);
|
||||
assertTrue(cache.resolvedHeight(query, OptionalInt::empty).isEmpty());
|
||||
assertEquals(42, cache.resolvedHeight(query, () -> OptionalInt.of(42)).orElseThrow());
|
||||
assertEquals(42, cache.resolvedHeight(query, OptionalInt::empty).orElseThrow());
|
||||
}
|
||||
|
||||
private static int cached(NativeTerrainHeightCache cache, NativeTerrainHeightCache.Query query,
|
||||
IntSupplier resolver) {
|
||||
return cache.resolvedHeight(query, () -> OptionalInt.of(resolver.getAsInt())).orElseThrow();
|
||||
}
|
||||
|
||||
private static NativeTerrainHeightCache.Query query(int runtimeId, int x, int z) {
|
||||
return new NativeTerrainHeightCache.Query(runtimeId, x, z, Heightmap.Types.WORLD_SURFACE, -256, 768);
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.world.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.world.history.TerrainBoundarySignature;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.RotatedPillarBlock;
|
||||
import net.minecraft.world.level.block.LiquidBlock;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public final class NativeTransitionColumnTest {
|
||||
@Rule
|
||||
public final PlatformLeakGuard leakGuard = PlatformLeakGuard.clean();
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customKeysUseCarrierForNativeColumnsAndHeightmaps() {
|
||||
String key = "itemsadder:rocks/ruby_ore";
|
||||
PlatformBlockState custom = mock(PlatformBlockState.class);
|
||||
PlatformBlockState carrier = mock(PlatformBlockState.class);
|
||||
when(custom.placementBaseState()).thenReturn(carrier);
|
||||
when(carrier.key()).thenReturn("minecraft:oak_log[axis=z]");
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
when(registries.blockOrNull(key)).thenReturn(custom);
|
||||
LevelHeightAccessor height = LevelHeightAccessor.create(-16, 32);
|
||||
TerrainBoundarySignature signature = signature(key);
|
||||
|
||||
try (MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class)) {
|
||||
platforms.when(IrisPlatforms::isBound).thenReturn(true);
|
||||
platforms.when(IrisPlatforms::get).thenReturn(platform);
|
||||
NoiseColumn column = NativeTransitionColumn.column(signature, height);
|
||||
|
||||
assertEquals(Direction.Axis.Z, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS));
|
||||
assertEquals(-14, NativeTransitionColumn.height(signature, Heightmap.Types.OCEAN_FLOOR_WG, height));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preservesNativePropertiesCavesAndFluids() {
|
||||
NoiseColumn column = NativeTransitionColumn.column(signature("minecraft:oak_log[axis=x]"),
|
||||
LevelHeightAccessor.create(-16, 32));
|
||||
assertEquals(Direction.Axis.X, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS));
|
||||
assertTrue(column.getBlock(-14).is(Blocks.CAVE_AIR));
|
||||
assertEquals(Integer.valueOf(3), column.getBlock(-13).getValue(LiquidBlock.LEVEL));
|
||||
assertTrue(column.getBlock(0).isAir());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void usesNativeHeightmapPredicatesForWaterAndOpenSpace() {
|
||||
TerrainBoundarySignature signature = signature("minecraft:oak_log[axis=x]");
|
||||
LevelHeightAccessor height = LevelHeightAccessor.create(-16, 32);
|
||||
assertEquals(-12, NativeTransitionColumn.height(signature, Heightmap.Types.WORLD_SURFACE_WG, height));
|
||||
assertEquals(-14, NativeTransitionColumn.height(signature, Heightmap.Types.OCEAN_FLOOR_WG, height));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsUnknownSavedStatesInsteadOfReplacingTerrain() {
|
||||
assertThrows(IllegalArgumentException.class, () -> NativeTransitionColumn.column(
|
||||
signature("missing:removed_block"), LevelHeightAccessor.create(-16, 32)));
|
||||
}
|
||||
|
||||
private static TerrainBoundarySignature signature(String solidState) {
|
||||
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-16, List.of(
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||
new BoundaryColumnGeometry.Voxel(solidState, BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:cave_air", BoundaryColumnGeometry.Phase.AIR, "", false),
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:water[level=3]", BoundaryColumnGeometry.Phase.FLUID,
|
||||
"minecraft:water[level=3]", false)));
|
||||
return new TerrainBoundarySignature(
|
||||
new TerrainBoundarySignature.Column(0, 0, 3, 1, OptionalInt.of(3), OptionalInt.empty()),
|
||||
new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(-16, 4, 1),
|
||||
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})),
|
||||
geometry);
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class CustomBiomeSourceStructureContractTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stackedCustomBiomesUseTheirOwningDimensionNamespace() {
|
||||
IrisDimension host = new IrisDimension();
|
||||
host.setLoadKey("Host");
|
||||
IrisDimension upper = new IrisDimension();
|
||||
upper.setLoadKey("Layers/Upper");
|
||||
|
||||
assertEquals("layers:upper/aurora", CustomBiomeSource.customBiomeKey(upper, "Aurora"));
|
||||
assertNotEquals(
|
||||
CustomBiomeSource.customBiomeKey(host, "Aurora"),
|
||||
CustomBiomeSource.customBiomeKey(upper, "Aurora")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void temporaryNaturalBiomeAnswersAreNotMemoized() {
|
||||
Engine temporary = engineAnsweringNaturalFallback(true, 12, -8);
|
||||
Engine stable = engineAnsweringNaturalFallback(false, 12, -8);
|
||||
assertFalse(CustomBiomeSource.isBiomeCacheable(temporary, 12, -8));
|
||||
assertTrue(CustomBiomeSource.isBiomeCacheable(stable, 12, -8));
|
||||
}
|
||||
|
||||
private static Engine engineAnsweringNaturalFallback(
|
||||
boolean naturalFallback,
|
||||
int expectedX,
|
||||
int expectedZ
|
||||
) {
|
||||
return (Engine) Proxy.newProxyInstance(
|
||||
Engine.class.getClassLoader(),
|
||||
new Class<?>[]{Engine.class},
|
||||
(proxy, method, arguments) -> {
|
||||
if (!method.getName().equals("answersFromNaturalTerrain")) {
|
||||
throw new UnsupportedOperationException(method.getName());
|
||||
}
|
||||
assertEquals(Integer.valueOf(expectedX), arguments[0]);
|
||||
assertEquals(Integer.valueOf(expectedZ), arguments[1]);
|
||||
return naturalFallback;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisChunkGeneratorFailureContractTest {
|
||||
@Test
|
||||
public void releasedNoiseAdmissionLetsSynchronousDependentWaitForQueuedExclusive() throws Exception {
|
||||
Semaphore admission = new Semaphore(1, true);
|
||||
admission.acquire();
|
||||
CountDownLatch exclusiveEntered = new CountDownLatch(1);
|
||||
CountDownLatch releaseExclusive = new CountDownLatch(1);
|
||||
AtomicBoolean dependentFinished = new AtomicBoolean(false);
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
boolean stageReleased = false;
|
||||
|
||||
try {
|
||||
Future<?> exclusive = executor.submit(() -> {
|
||||
boolean acquired = false;
|
||||
try {
|
||||
admission.acquire();
|
||||
acquired = true;
|
||||
exclusiveEntered.countDown();
|
||||
assertTrue(releaseExclusive.await(2, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(e);
|
||||
} finally {
|
||||
if (acquired) {
|
||||
admission.release();
|
||||
}
|
||||
}
|
||||
});
|
||||
awaitQueueLength(admission, 1);
|
||||
CompletableFuture<Void> outward = new CompletableFuture<>();
|
||||
outward.thenRun(() -> {
|
||||
try {
|
||||
assertTrue(exclusiveEntered.await(2, TimeUnit.SECONDS));
|
||||
dependentFinished.set(true);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
});
|
||||
|
||||
admission.release();
|
||||
stageReleased = true;
|
||||
assertTrue(outward.complete(null));
|
||||
assertTrue(dependentFinished.get());
|
||||
releaseExclusive.countDown();
|
||||
exclusive.get(2, TimeUnit.SECONDS);
|
||||
assertEquals(1, admission.availablePermits());
|
||||
} finally {
|
||||
if (!stageReleased) {
|
||||
admission.release();
|
||||
}
|
||||
releaseExclusive.countDown();
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedDelegateCancellationIsDetectedThroughItsCompletionFailure() {
|
||||
CompletableFuture<Void> delegate = new CompletableFuture<>();
|
||||
CompletableFuture<Void> transformed = delegate.thenApply(value -> value);
|
||||
|
||||
assertTrue(delegate.cancel(false));
|
||||
assertFalse(transformed.isCancelled());
|
||||
CompletionException failure = assertThrows(CompletionException.class, transformed::join);
|
||||
assertTrue(failure.getCause() instanceof CancellationException);
|
||||
}
|
||||
|
||||
private static void awaitQueueLength(Semaphore semaphore, int expected) {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
|
||||
while (semaphore.getQueueLength() < expected && System.nanoTime() < deadline) {
|
||||
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
|
||||
}
|
||||
assertTrue("Expected at least " + expected + " queued semaphore threads",
|
||||
semaphore.getQueueLength() >= expected);
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.nativegen.NativeStructureLocateResults;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
public class IrisChunkGeneratorMonumentLocateContractTest {
|
||||
@Test
|
||||
public void mixedLocateSelectsNearestProviderAndPrefersNativeOnTie() {
|
||||
BlockPos origin = BlockPos.ZERO;
|
||||
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeTie = Pair.of(new BlockPos(0, 70, 4), null);
|
||||
|
||||
assertSame(irisNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeFar));
|
||||
assertSame(nativeNear, NativeStructureLocateResults.nearest(origin, irisNear, nativeNear));
|
||||
assertSame(nativeTie, NativeStructureLocateResults.nearest(origin, irisNear, nativeTie));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mixedUnexploredLocateReferencesOnlyTheSelectedProvider() {
|
||||
BlockPos origin = BlockPos.ZERO;
|
||||
Pair<BlockPos, Holder<Structure>> irisNear = Pair.of(new BlockPos(4, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeFar = Pair.of(new BlockPos(8, 70, 0), null);
|
||||
AtomicInteger irisReferences = new AtomicInteger();
|
||||
AtomicInteger nativeReferences = new AtomicInteger();
|
||||
|
||||
Pair<BlockPos, Holder<Structure>> irisSelected =
|
||||
NativeStructureLocateResults.selectAndReference(
|
||||
origin,
|
||||
irisNear, () -> irisReferences.incrementAndGet(),
|
||||
nativeFar, () -> nativeReferences.incrementAndGet());
|
||||
|
||||
assertSame(irisNear, irisSelected);
|
||||
assertEquals(1, irisReferences.get());
|
||||
assertEquals(0, nativeReferences.get());
|
||||
|
||||
Pair<BlockPos, Holder<Structure>> nativeNear = Pair.of(new BlockPos(2, 70, 0), null);
|
||||
Pair<BlockPos, Holder<Structure>> nativeSelected =
|
||||
NativeStructureLocateResults.selectAndReference(
|
||||
origin,
|
||||
irisNear, () -> irisReferences.incrementAndGet(),
|
||||
nativeNear, () -> nativeReferences.incrementAndGet());
|
||||
|
||||
assertSame(nativeNear, nativeSelected);
|
||||
assertEquals(1, irisReferences.get());
|
||||
assertEquals(1, nativeReferences.get());
|
||||
}
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionManager;
|
||||
import art.arcane.iris.generation.runtime.GenerationTransitionGate;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.world.history.NativeBiomeSpawnSelection;
|
||||
import art.arcane.iris.world.history.SavedBiomeRuntime;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.random.WeightedList;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisChunkGeneratorSpawnAdmissionTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tickSpawnQueryLeavesOwnerThreadAvailableForBoundaryCheckpoint() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
ExecutorService owner = Executors.newSingleThreadExecutor();
|
||||
ExecutorService reloader = Executors.newSingleThreadExecutor();
|
||||
AtomicInteger snapshots = new AtomicInteger();
|
||||
long previousSession = fixture.sessions.currentSessionId();
|
||||
try {
|
||||
assertSame(fixture.expected, owner.submit(fixture::query).get(2L, TimeUnit.SECONDS));
|
||||
assertEquals(previousSession, fixture.readSession.get());
|
||||
CompletableFuture<Void> cutover = CompletableFuture.runAsync(() -> {
|
||||
try (GenerationTransitionGate.Transition ignored = fixture.sessions.transitionGate().beginTransition(2_000L)) {
|
||||
fixture.sessions.sealAndAwait("Studio generation cutover", 2_000L);
|
||||
Future<?> query = owner.submit(() -> {
|
||||
assertTrue(fixture.query().isEmpty());
|
||||
assertNull(IrisContext.get());
|
||||
});
|
||||
owner.submit(snapshots::incrementAndGet).get(2L, TimeUnit.SECONDS);
|
||||
query.get(2L, TimeUnit.SECONDS);
|
||||
assertEquals(1, fixture.savedReads.get());
|
||||
assertEquals(1, fixture.routes.get());
|
||||
fixture.sessions.activateNextSession();
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException(failure);
|
||||
}
|
||||
}, reloader);
|
||||
|
||||
cutover.get(3L, TimeUnit.SECONDS);
|
||||
assertEquals(1, snapshots.get());
|
||||
assertEquals(0, fixture.sessions.activeLeases());
|
||||
assertSame(fixture.expected, owner.submit(fixture::query).get(2L, TimeUnit.SECONDS));
|
||||
assertEquals(fixture.sessions.currentSessionId(), fixture.readSession.get());
|
||||
assertTrue(fixture.readSession.get() > previousSession);
|
||||
assertEquals(2, fixture.savedReads.get());
|
||||
assertEquals(2, fixture.routes.get());
|
||||
assertEquals(0, fixture.sessions.activeLeases());
|
||||
} finally {
|
||||
owner.shutdownNow();
|
||||
reloader.shutdownNow();
|
||||
assertTrue(owner.awaitTermination(2L, TimeUnit.SECONDS));
|
||||
assertTrue(reloader.awaitTermination(2L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
private static void setField(IrisChunkGenerator generator, String name, Object value) throws Exception {
|
||||
Field field = IrisChunkGenerator.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(generator, value);
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final GenerationSessionManager sessions = new GenerationSessionManager(true);
|
||||
private final IrisEngine engine = mock(IrisEngine.class);
|
||||
private final IrisChunkGenerator generator = mock(IrisChunkGenerator.class, CALLS_REAL_METHODS);
|
||||
private final Holder<Biome> biome = Holder.direct(mock(Biome.class));
|
||||
private final StructureManager structures = mock(StructureManager.class);
|
||||
private final WeightedList<MobSpawnSettings.SpawnerData> expected = mock(WeightedList.class);
|
||||
private final AtomicInteger savedReads = new AtomicInteger();
|
||||
private final AtomicInteger routes = new AtomicInteger();
|
||||
private final AtomicLong readSession = new AtomicLong();
|
||||
|
||||
private Fixture() throws Exception {
|
||||
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
|
||||
ChunkGenerator delegate = mock(ChunkGenerator.class);
|
||||
CustomBiomeSource source = mock(CustomBiomeSource.class);
|
||||
when(engine.getGenerationSessions()).thenReturn(sessions);
|
||||
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
|
||||
when(router.biomes()).thenReturn(saved);
|
||||
when(saved.nativeSpawnSelection(0, 64, 0, "")).thenAnswer(invocation -> {
|
||||
assertAdmitted();
|
||||
savedReads.incrementAndGet();
|
||||
return new NativeBiomeSpawnSelection(NativeBiomeSpawnSelection.Mode.RETAINED, "minecraft:plains");
|
||||
});
|
||||
when(engine.openGenerationHistoryCoordinateScope(0, 0)).thenAnswer(invocation -> {
|
||||
assertAdmitted();
|
||||
routes.incrementAndGet();
|
||||
return null;
|
||||
});
|
||||
when(delegate.getMobsAt(biome, structures, MobCategory.MONSTER, new BlockPos(0, 64, 0)))
|
||||
.thenReturn(expected);
|
||||
setField(generator, "engine", engine);
|
||||
setField(generator, "delegate", delegate);
|
||||
setField(generator, "customBiomeSource", source);
|
||||
}
|
||||
|
||||
private WeightedList<MobSpawnSettings.SpawnerData> query() {
|
||||
return generator.getMobsAt(biome, structures, MobCategory.MONSTER, new BlockPos(0, 64, 0));
|
||||
}
|
||||
|
||||
private void assertAdmitted() {
|
||||
assertEquals(1, sessions.activeLeases());
|
||||
assertSame(engine, IrisContext.get().getEngine());
|
||||
readSession.set(IrisContext.get().getGenerationSessionId());
|
||||
}
|
||||
}
|
||||
}
|
||||
+509
@@ -0,0 +1,509 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.pack.datapack.DatapackStructureScopeIndex;
|
||||
import art.arcane.iris.structure.nativegen.IrisImportedStructureControl;
|
||||
import art.arcane.iris.structure.placement.IrisStructureSetFrequencyOverride;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import com.mojang.datafixers.util.Either;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderOwner;
|
||||
import net.minecraft.core.Vec3i;
|
||||
import net.minecraft.core.component.DataComponentMap;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacementType;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import org.junit.Test;
|
||||
import org.junit.BeforeClass;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
public class NMSBindingDatapackStructureScopeTest {
|
||||
private static final String SOURCE = "https://example.test/managed.zip";
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraft() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void managedSetContainingVanillaStructureIsAbsentOutsideDeclaringDimension() {
|
||||
Holder<Structure> vanillaStructure = structureHolder("minecraft:pillager_outpost");
|
||||
Holder<StructureSet> managedSet = structureSetHolder(
|
||||
"managed:illager_barracks", vanillaStructure);
|
||||
DatapackStructureScopeIndex index = index(
|
||||
List.of(),
|
||||
List.of("managed:illager_barracks"));
|
||||
|
||||
DatapackStructureStateFilter.Selection vanilla = DatapackStructureStateFilter.filter(
|
||||
List.of(managedSet), index, Set.of());
|
||||
DatapackStructureStateFilter.Selection declaring = DatapackStructureStateFilter.filter(
|
||||
List.of(managedSet), index, index.declaredSources(List.of(SOURCE)));
|
||||
|
||||
assertEquals(0, vanilla.structureSets().size());
|
||||
assertEquals(1, vanilla.excludedManagedSets());
|
||||
assertEquals(1, declaring.structureSets().size());
|
||||
assertSame(managedSet, declaring.structureSets().getFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unmanagedSetRetainsOnlyDefinitionsAllowedInTheWorld() {
|
||||
Holder<Structure> vanillaStructure = structureHolder("minecraft:village_plains");
|
||||
Holder<Structure> managedStructure = structureHolder("managed:tavern");
|
||||
Holder<StructureSet> vanillaSet = structureSetHolder(
|
||||
"minecraft:villages", vanillaStructure, managedStructure);
|
||||
DatapackStructureScopeIndex index = index(
|
||||
List.of("managed:tavern"),
|
||||
List.of());
|
||||
|
||||
DatapackStructureStateFilter.Selection vanilla = DatapackStructureStateFilter.filter(
|
||||
List.of(vanillaSet), index, Set.of());
|
||||
DatapackStructureStateFilter.Selection declaring = DatapackStructureStateFilter.filter(
|
||||
List.of(vanillaSet), index, index.declaredSources(List.of(SOURCE)));
|
||||
|
||||
assertEquals(1, vanilla.structureSets().size());
|
||||
assertEquals(1, vanilla.structureSets().getFirst().value().structures().size());
|
||||
assertSame(vanillaStructure,
|
||||
vanilla.structureSets().getFirst().value().structures().getFirst().structure());
|
||||
assertSame(vanillaSet.value().placement(),
|
||||
vanilla.structureSets().getFirst().value().placement());
|
||||
assertSame(vanillaSet, declaring.structureSets().getFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setWithNoAllowedDefinitionsIsRemoved() {
|
||||
Holder<StructureSet> unmanagedSet = structureSetHolder(
|
||||
"minecraft:custom", structureHolder("managed:only"));
|
||||
DatapackStructureScopeIndex index = index(List.of("managed:only"), List.of());
|
||||
|
||||
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
|
||||
List.of(unmanagedSet), index, Set.of());
|
||||
|
||||
assertEquals(0, selection.structureSets().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void spigotDirectHolderRecoversItsRegistryKeyFromSharedEntries() {
|
||||
ResourceKey<StructureSet> key = ResourceKey.create(
|
||||
Registries.STRUCTURE_SET,
|
||||
Identifier.parse("managed:illager_barracks"));
|
||||
List<StructureSet.StructureSelectionEntry> entries = List.of(
|
||||
new StructureSet.StructureSelectionEntry(
|
||||
structureHolder("minecraft:pillager_outpost"), 1));
|
||||
Holder<StructureSet> registered = new KeyedHolder<>(key, new StructureSet(
|
||||
entries,
|
||||
new RandomSpreadStructurePlacement(
|
||||
34, 8, RandomSpreadType.LINEAR, 10387312)));
|
||||
Holder<StructureSet> spigotDirect = Holder.direct(new StructureSet(
|
||||
entries,
|
||||
new RandomSpreadStructurePlacement(
|
||||
34, 8, RandomSpreadType.LINEAR, 14357620)));
|
||||
DatapackStructureScopeIndex scopeIndex = index(
|
||||
List.of(), List.of("managed:illager_barracks"));
|
||||
DatapackStructureStateFilter.StructureSetKeyIndex keyIndex =
|
||||
DatapackStructureStateFilter.keyIndex(List.of(registered));
|
||||
|
||||
DatapackStructureStateFilter.Selection excluded =
|
||||
DatapackStructureStateFilter.filter(
|
||||
List.of(spigotDirect),
|
||||
scopeIndex,
|
||||
Set.of(),
|
||||
new IrisImportedStructureControl(),
|
||||
keyIndex);
|
||||
DatapackStructureStateFilter.Selection retained =
|
||||
DatapackStructureStateFilter.filter(
|
||||
List.of(spigotDirect),
|
||||
scopeIndex,
|
||||
scopeIndex.declaredSources(List.of(SOURCE)),
|
||||
new IrisImportedStructureControl(),
|
||||
keyIndex);
|
||||
|
||||
assertEquals(0, excluded.structureSets().size());
|
||||
assertEquals(1, excluded.excludedManagedSets());
|
||||
assertSame(spigotDirect, retained.structureSets().getFirst());
|
||||
assertEquals(1, retained.retainedManagedSets());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureScopeDoesNotLinkPaperOnlyPlacementClasses() throws IOException {
|
||||
InputStream classResource = NMSBindingDatapackStructureScopeTest.class
|
||||
.getResourceAsStream("DatapackStructureStateFilter.class");
|
||||
assertNotNull(classResource);
|
||||
try (InputStream input = classResource) {
|
||||
String classFile = new String(input.readAllBytes(), StandardCharsets.ISO_8859_1);
|
||||
assertFalse(classFile.contains("KeyedRandomSpreadStructurePlacement"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exactFrequencyOverridesScaleOnlyTheNamedNativeStructureSet() {
|
||||
Holder<StructureSet> complexes = structureSetHolder(
|
||||
"minecraft:nether_complexes",
|
||||
new RandomSpreadStructurePlacement(27, 4, RandomSpreadType.LINEAR, 30084232),
|
||||
structureHolder("minecraft:fortress"));
|
||||
Holder<StructureSet> fossils = structureSetHolder(
|
||||
"minecraft:nether_fossils",
|
||||
new RandomSpreadStructurePlacement(2, 1, RandomSpreadType.LINEAR, 14357921),
|
||||
structureHolder("minecraft:nether_fossil"));
|
||||
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
|
||||
overrides.add(new IrisStructureSetFrequencyOverride()
|
||||
.setStructureSet("minecraft:nether_complexes")
|
||||
.setMultiplier(1.1D));
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setFrequencyOverrides(overrides);
|
||||
|
||||
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
|
||||
List.of(complexes, fossils), index(List.of(), List.of()), Set.of(), control);
|
||||
|
||||
RandomSpreadStructurePlacement scaledComplexes =
|
||||
(RandomSpreadStructurePlacement) selection.structureSets().get(0).value().placement();
|
||||
RandomSpreadStructurePlacement unchangedFossils =
|
||||
(RandomSpreadStructurePlacement) selection.structureSets().get(1).value().placement();
|
||||
assertEquals(26, scaledComplexes.spacing());
|
||||
assertEquals(4, scaledComplexes.separation());
|
||||
assertEquals(2, unchangedFossils.spacing());
|
||||
assertSame(fossils, selection.structureSets().get(1));
|
||||
assertEquals(27, ((RandomSpreadStructurePlacement) complexes.value().placement()).spacing());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void frequencyOnlyPassLeavesUnrelatedCustomPlacementUntouched() {
|
||||
Holder<StructureSet> exclusionTarget = structureSetHolder(
|
||||
"example:target", structureHolder("example:target"));
|
||||
Holder<StructureSet> custom = structureSetHolder(
|
||||
"example:custom",
|
||||
new UnsupportedPlacement(Optional.of(
|
||||
new StructurePlacement.ExclusionZone(exclusionTarget, 1))),
|
||||
structureHolder("example:custom"));
|
||||
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
|
||||
overrides.add(new IrisStructureSetFrequencyOverride()
|
||||
.setStructureSet("minecraft:nether_complexes")
|
||||
.setMultiplier(1.1D));
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setFrequencyOverrides(overrides);
|
||||
|
||||
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
|
||||
List.of(custom), index(List.of(), List.of()), Set.of(), control);
|
||||
|
||||
assertSame(custom, selection.structureSets().getFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void frequencyOnlyPassRebindsDependentExclusionTarget() {
|
||||
Holder<StructureSet> target = structureSetHolder(
|
||||
"example:target",
|
||||
new RandomSpreadStructurePlacement(27, 4, RandomSpreadType.LINEAR, 30084232),
|
||||
structureHolder("example:target"));
|
||||
RandomSpreadStructurePlacement dependentPlacement = new RandomSpreadStructurePlacement(
|
||||
Vec3i.ZERO,
|
||||
StructurePlacement.FrequencyReductionMethod.DEFAULT,
|
||||
1.0F,
|
||||
4567,
|
||||
Optional.of(new StructurePlacement.ExclusionZone(target, 1)),
|
||||
32,
|
||||
8,
|
||||
RandomSpreadType.LINEAR);
|
||||
Holder<StructureSet> dependent = structureSetHolder(
|
||||
"example:dependent",
|
||||
dependentPlacement,
|
||||
structureHolder("example:dependent"));
|
||||
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
|
||||
overrides.add(new IrisStructureSetFrequencyOverride()
|
||||
.setStructureSet("example:target")
|
||||
.setMultiplier(1.1D));
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setFrequencyOverrides(overrides);
|
||||
|
||||
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
|
||||
List.of(dependent, target), index(List.of(), List.of()), Set.of(), control);
|
||||
|
||||
Holder<StructureSet> scaledDependent = selection.structureSets().get(0);
|
||||
Holder<StructureSet> scaledTarget = selection.structureSets().get(1);
|
||||
assertNotSame(dependent, scaledDependent);
|
||||
assertNotSame(target, scaledTarget);
|
||||
assertSame(scaledTarget, DatapackStructureStateFilter.exclusionZone(
|
||||
scaledDependent.value().placement()).orElseThrow().otherSet());
|
||||
assertEquals(26, ((RandomSpreadStructurePlacement)
|
||||
scaledTarget.value().placement()).spacing());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void frequencyOnlyPassRebindsAffectedExclusionCycle() {
|
||||
MutableStructureSetHolder first = new MutableStructureSetHolder("example:first");
|
||||
MutableStructureSetHolder second = new MutableStructureSetHolder("example:second");
|
||||
first.bind(structureSet(
|
||||
new RandomSpreadStructurePlacement(
|
||||
Vec3i.ZERO,
|
||||
StructurePlacement.FrequencyReductionMethod.DEFAULT,
|
||||
1.0F,
|
||||
101,
|
||||
Optional.of(new StructurePlacement.ExclusionZone(second, 1)),
|
||||
32,
|
||||
8,
|
||||
RandomSpreadType.LINEAR),
|
||||
"example:first"));
|
||||
second.bind(structureSet(
|
||||
new RandomSpreadStructurePlacement(
|
||||
Vec3i.ZERO,
|
||||
StructurePlacement.FrequencyReductionMethod.DEFAULT,
|
||||
1.0F,
|
||||
102,
|
||||
Optional.of(new StructurePlacement.ExclusionZone(first, 1)),
|
||||
27,
|
||||
4,
|
||||
RandomSpreadType.LINEAR),
|
||||
"example:second"));
|
||||
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
|
||||
overrides.add(new IrisStructureSetFrequencyOverride()
|
||||
.setStructureSet("example:second")
|
||||
.setMultiplier(1.1D));
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setFrequencyOverrides(overrides);
|
||||
|
||||
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
|
||||
List.of(first, second), index(List.of(), List.of()), Set.of(), control);
|
||||
|
||||
Holder<StructureSet> scaledFirst = selection.structureSets().get(0);
|
||||
Holder<StructureSet> scaledSecond = selection.structureSets().get(1);
|
||||
assertSame(scaledSecond, DatapackStructureStateFilter.exclusionZone(
|
||||
scaledFirst.value().placement()).orElseThrow().otherSet());
|
||||
assertSame(scaledFirst, DatapackStructureStateFilter.exclusionZone(
|
||||
scaledSecond.value().placement()).orElseThrow().otherSet());
|
||||
assertEquals(26, ((RandomSpreadStructurePlacement)
|
||||
scaledSecond.value().placement()).spacing());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void randomSpreadSubtypeUsesTheCanonicalPlacementContract() {
|
||||
Holder<StructureSet> custom = structureSetHolder(
|
||||
"example:custom",
|
||||
new CustomRandomSpreadPlacement(),
|
||||
structureHolder("example:custom"));
|
||||
KList<IrisStructureSetFrequencyOverride> overrides = new KList<>();
|
||||
overrides.add(new IrisStructureSetFrequencyOverride()
|
||||
.setStructureSet("example:custom")
|
||||
.setMultiplier(1.1D));
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setFrequencyOverrides(overrides);
|
||||
|
||||
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
|
||||
List.of(custom), index(List.of(), List.of()), Set.of(), control);
|
||||
StructurePlacement placement = selection.structureSets().getFirst().value().placement();
|
||||
|
||||
assertEquals(RandomSpreadStructurePlacement.class, placement.getClass());
|
||||
assertEquals(26, ((RandomSpreadStructurePlacement) placement).spacing());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void excludedManagedSetCannotSuppressAnAllowedSetThroughExclusionZone() {
|
||||
Holder<StructureSet> managedSet = structureSetHolder(
|
||||
"managed:blocked",
|
||||
structureHolder("managed:blocked"));
|
||||
RandomSpreadStructurePlacement originalPlacement = new RandomSpreadStructurePlacement(
|
||||
Vec3i.ZERO,
|
||||
StructurePlacement.FrequencyReductionMethod.DEFAULT,
|
||||
1.0F,
|
||||
4567,
|
||||
Optional.of(new StructurePlacement.ExclusionZone(managedSet, 1)),
|
||||
32,
|
||||
8,
|
||||
RandomSpreadType.LINEAR);
|
||||
Holder<StructureSet> vanillaSet = structureSetHolder(
|
||||
"minecraft:allowed",
|
||||
originalPlacement,
|
||||
structureHolder("minecraft:village_plains"));
|
||||
DatapackStructureScopeIndex index = index(
|
||||
List.of("managed:blocked"),
|
||||
List.of("managed:blocked"));
|
||||
|
||||
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
|
||||
List.of(vanillaSet, managedSet), index, Set.of());
|
||||
|
||||
assertEquals(1, selection.structureSets().size());
|
||||
StructurePlacement scopedPlacement = selection.structureSets().getFirst().value().placement();
|
||||
assertEquals(0, DatapackStructureStateFilter.exclusionZone(scopedPlacement).stream().count());
|
||||
}
|
||||
|
||||
private static DatapackStructureScopeIndex index(
|
||||
List<String> structureKeys,
|
||||
List<String> structureSetKeys
|
||||
) {
|
||||
return DatapackStructureScopeIndex.create(List.of(
|
||||
new DatapackIngestService.StructureScopeResources(
|
||||
SOURCE,
|
||||
structureKeys,
|
||||
structureSetKeys)));
|
||||
}
|
||||
|
||||
private static Holder<Structure> structureHolder(String key) {
|
||||
return new KeyedHolder<>(ResourceKey.create(Registries.STRUCTURE, Identifier.parse(key)), null);
|
||||
}
|
||||
|
||||
private static Holder<StructureSet> structureSetHolder(
|
||||
String key,
|
||||
Holder<Structure>... structures
|
||||
) {
|
||||
return structureSetHolder(
|
||||
key,
|
||||
new RandomSpreadStructurePlacement(32, 8, RandomSpreadType.LINEAR, 12345),
|
||||
structures);
|
||||
}
|
||||
|
||||
private static Holder<StructureSet> structureSetHolder(
|
||||
String key,
|
||||
StructurePlacement placement,
|
||||
Holder<Structure>... structures
|
||||
) {
|
||||
List<StructureSet.StructureSelectionEntry> entries = Stream.of(structures)
|
||||
.map(structure -> new StructureSet.StructureSelectionEntry(structure, 1))
|
||||
.toList();
|
||||
StructureSet value = new StructureSet(entries, placement);
|
||||
return new KeyedHolder<>(
|
||||
ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)),
|
||||
value);
|
||||
}
|
||||
|
||||
private static StructureSet structureSet(StructurePlacement placement, String structureKey) {
|
||||
return new StructureSet(
|
||||
List.of(new StructureSet.StructureSelectionEntry(
|
||||
structureHolder(structureKey), 1)),
|
||||
placement);
|
||||
}
|
||||
|
||||
private static final class KeyedHolder<T> implements Holder<T> {
|
||||
private final ResourceKey<T> key;
|
||||
private final T value;
|
||||
|
||||
private KeyedHolder(ResourceKey<T> key, T value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBound() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areComponentsBound() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean is(Identifier identifier) {
|
||||
return key.identifier().equals(identifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean is(ResourceKey<T> candidate) {
|
||||
return key.equals(candidate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean is(Predicate<ResourceKey<T>> predicate) {
|
||||
return predicate.test(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean is(TagKey<T> tag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean is(Holder<T> holder) {
|
||||
return holder == this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<TagKey<T>> tags() {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataComponentMap components() {
|
||||
return DataComponentMap.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Either<ResourceKey<T>, T> unwrap() {
|
||||
return Either.left(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ResourceKey<T>> unwrapKey() {
|
||||
return Optional.of(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Kind kind() {
|
||||
return Kind.REFERENCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSerializeIn(HolderOwner<T> owner) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class UnsupportedPlacement extends StructurePlacement {
|
||||
private UnsupportedPlacement(Optional<ExclusionZone> exclusionZone) {
|
||||
super(Vec3i.ZERO, FrequencyReductionMethod.DEFAULT, 1F, 1, exclusionZone);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isPlacementChunk(ChunkGeneratorStructureState state, int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StructurePlacementType<?> type() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MutableStructureSetHolder extends Holder.Reference<StructureSet> {
|
||||
private MutableStructureSetHolder(String key) {
|
||||
super(Type.STAND_ALONE, new HolderOwner<>() {
|
||||
}, ResourceKey.create(Registries.STRUCTURE_SET, Identifier.parse(key)), null);
|
||||
}
|
||||
|
||||
private void bind(StructureSet structureSet) {
|
||||
bindValue(structureSet);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CustomRandomSpreadPlacement extends RandomSpreadStructurePlacement {
|
||||
private CustomRandomSpreadPlacement() {
|
||||
super(27, 4, RandomSpreadType.LINEAR, 30084232);
|
||||
}
|
||||
}
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import ca.spottedleaf.moonrise.common.util.TickThread;
|
||||
import com.mojang.serialization.Codec;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.IdMapper;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiManager;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiSection;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiType;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ImposterProtoChunk;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.PalettedContainer;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.Strategy;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry;
|
||||
import org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NativeStructurePoiUpdatesTest {
|
||||
private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections");
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generationRecordsIntentBeforeSuppressingNativePoiCallbacks() {
|
||||
Fixture fixture = new Fixture();
|
||||
BlockPos position = new BlockPos(-31, 5, 49);
|
||||
doAnswer(call -> {
|
||||
assertNotNull(fixture.proto.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY));
|
||||
assertEquals(Block.UPDATE_CLIENTS | 4096, (int) call.getArgument(2));
|
||||
fixture.proto.setBlockState(position, call.getArgument(1), 0);
|
||||
return true;
|
||||
}).when(fixture.world).setBlock(any(), any(), anyInt(), anyInt());
|
||||
|
||||
assertTrue(fixture.updates.setBlock(fixture.world, position,
|
||||
Blocks.COMPOSTER.defaultBlockState(), Block.UPDATE_CLIENTS, 17));
|
||||
assertArrayEquals(new long[]{16L}, fixture.marker());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void denseWritesStoreOneSectionBitAndSurvivePersistentDataRoundTrip() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
fixture.write(new BlockPos(-32 + x, 5, 48 + z), Blocks.COMPOSTER.defaultBlockState());
|
||||
}
|
||||
}
|
||||
assertArrayEquals(new long[]{16L}, fixture.marker());
|
||||
DirtyCraftPersistentDataContainer restored = new DirtyCraftPersistentDataContainer(new CraftPersistentDataTypeRegistry());
|
||||
restored.putAll(fixture.proto.persistentDataContainer.toTagCompound());
|
||||
fixture.loaded.persistentDataContainer = restored;
|
||||
|
||||
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
|
||||
|
||||
assertEquals(256L, fixture.records().getRecords(type -> true, PoiManager.Occupancy.ANY).count());
|
||||
assertFalse(restored.has(DIRTY_SECTIONS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validSectionsReconcileFinalStatesAndKeepMatchingClaimedTickets() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
BlockPos retained = new BlockPos(-31, 5, 49);
|
||||
BlockPos replaced = new BlockPos(-30, 5, 49);
|
||||
BlockPos removed = new BlockPos(-29, 5, 49);
|
||||
Holder<PoiType> farmer = PoiTypes.forState(Blocks.COMPOSTER.defaultBlockState()).orElseThrow();
|
||||
PoiSection existing = new PoiSection.Packed(true, List.of(
|
||||
new PoiRecord.Packed(retained, farmer, 0),
|
||||
new PoiRecord.Packed(replaced, farmer, 1),
|
||||
new PoiRecord.Packed(removed, farmer, 1))).unpack(() -> {});
|
||||
fixture.pois.put(SectionPos.of(retained).asLong(), existing);
|
||||
PoiRecord retainedRecord = existing.getRecords(type -> true, PoiManager.Occupancy.ANY)
|
||||
.filter(record -> record.getPos().equals(retained)).findFirst().orElseThrow();
|
||||
for (BlockPos position : List.of(retained, replaced, removed)) {
|
||||
fixture.proto.setBlockState(position, Blocks.COMPOSTER.defaultBlockState(), 0);
|
||||
}
|
||||
fixture.write(replaced, Blocks.BARREL.defaultBlockState());
|
||||
fixture.write(removed, Blocks.AIR.defaultBlockState());
|
||||
|
||||
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
|
||||
|
||||
assertEquals(PoiTypes.forState(Blocks.BARREL.defaultBlockState()), existing.getType(replaced));
|
||||
assertTrue(existing.getType(removed).isEmpty());
|
||||
assertSame(retainedRecord, existing.getRecords(type -> true, PoiManager.Occupancy.ANY)
|
||||
.filter(record -> record.getPos().equals(retained)).findFirst().orElseThrow());
|
||||
assertEquals(0, existing.getFreeTickets(retained));
|
||||
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
|
||||
assertEquals(0, existing.getFreeTickets(retained));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedReconciliationRetainsIntentForRetry() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.write(new BlockPos(-31, 5, 49), Blocks.COMPOSTER.defaultBlockState());
|
||||
IllegalStateException failure = new IllegalStateException("POI storage failure");
|
||||
doThrow(failure).when(fixture.manager).add(any(), any());
|
||||
|
||||
onOwner(() -> assertSame(failure, assertThrows(IllegalStateException.class,
|
||||
() -> NativeStructurePoiUpdates.reconcile(fixture.loaded))));
|
||||
|
||||
assertArrayEquals(new long[]{16L}, fixture.marker());
|
||||
fixture.allowPoiAdds();
|
||||
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
|
||||
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitPoiSuppressionAndReadOnlyFullChunksDoNotCreateIntent() {
|
||||
Fixture fixture = new Fixture();
|
||||
BlockPos position = new BlockPos(-31, 5, 49);
|
||||
fixture.updates.setBlock(fixture.world, position, Blocks.COMPOSTER.defaultBlockState(),
|
||||
4096, 17);
|
||||
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
|
||||
ImposterProtoChunk full = mock(ImposterProtoChunk.class);
|
||||
when(fixture.world.getChunk(any(BlockPos.class))).thenReturn(full);
|
||||
fixture.updates.setBlock(fixture.world, position, Blocks.BARREL.defaultBlockState(), 2, 17);
|
||||
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidSectionBitsRetainTheStoredRecord() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.proto.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, new long[]{1L << 24});
|
||||
onOwner(() -> assertThrows(IllegalArgumentException.class,
|
||||
() -> NativeStructurePoiUpdates.reconcile(fixture.loaded)));
|
||||
assertArrayEquals(new long[]{1L << 24}, fixture.marker());
|
||||
}
|
||||
|
||||
private static void onOwner(Runnable action) throws Exception {
|
||||
FutureTask<Void> task = new FutureTask<>(action, null);
|
||||
new TickThread(task, "Native POI test").start();
|
||||
task.get(5L, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private static PalettedContainerFactory containerFactory() {
|
||||
Strategy<BlockState> blocks = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
|
||||
BlockState air = Blocks.AIR.defaultBlockState();
|
||||
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
|
||||
Holder<Biome> biome = Holder.direct((Biome) null);
|
||||
biomeIds.add(biome);
|
||||
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
|
||||
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(name -> biome, holder -> "iris-test-biome");
|
||||
return new PalettedContainerFactory(blocks, air, PalettedContainer.codecRW(BlockState.CODEC, blocks, air),
|
||||
biomes, biome, PalettedContainer.codecRO(biomeCodec, biomes, biome),
|
||||
PalettedContainer.codecRW(biomeCodec, biomes, biome));
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final NativeStructurePoiUpdates updates = new NativeStructurePoiUpdates(4096);
|
||||
private final ProtoChunk proto = new ProtoChunk(new ChunkPos(-2, 3), UpgradeData.EMPTY,
|
||||
LevelHeightAccessor.create(-64, 384), containerFactory(), null);
|
||||
private final LevelChunk loaded = mock(LevelChunk.class);
|
||||
private final WorldGenLevel world = mock(WorldGenLevel.class);
|
||||
private final PoiManager manager = mock(PoiManager.class);
|
||||
private final Map<Long, PoiSection> pois = new HashMap<>();
|
||||
|
||||
private Fixture() {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
when(level.getPoiManager()).thenReturn(manager);
|
||||
loaded.persistentDataContainer = proto.persistentDataContainer;
|
||||
when(loaded.getLevel()).thenReturn(level);
|
||||
when(loaded.getPos()).thenReturn(proto.getPos());
|
||||
when(loaded.getSectionsCount()).thenReturn(proto.getSectionsCount());
|
||||
when(loaded.getSectionYFromSectionIndex(anyInt())).thenAnswer(call -> proto.getSectionYFromSectionIndex(call.getArgument(0)));
|
||||
when(loaded.getSection(anyInt())).thenAnswer(call -> proto.getSection(call.getArgument(0)));
|
||||
when(loaded.getBlockState(any())).thenAnswer(call -> proto.getBlockState(call.getArgument(0)));
|
||||
when(world.getChunk(any(BlockPos.class))).thenReturn(proto);
|
||||
when(world.setBlock(any(), any(), anyInt(), anyInt())).thenAnswer(call -> {
|
||||
proto.setBlockState(call.getArgument(0), call.getArgument(1), call.getArgument(2));
|
||||
return true;
|
||||
});
|
||||
when(manager.getOrLoad(anyLong())).thenAnswer(call -> Optional.ofNullable(pois.get((long) call.getArgument(0))));
|
||||
when(manager.getType(any())).thenAnswer(call -> {
|
||||
BlockPos position = call.getArgument(0);
|
||||
PoiSection section = pois.get(SectionPos.of(position).asLong());
|
||||
return section == null ? Optional.empty() : section.getType(position);
|
||||
});
|
||||
doAnswer(call -> {
|
||||
BlockPos position = call.getArgument(0);
|
||||
pois.get(SectionPos.of(position).asLong()).remove(position);
|
||||
return null;
|
||||
}).when(manager).remove(any());
|
||||
allowPoiAdds();
|
||||
}
|
||||
|
||||
private void allowPoiAdds() {
|
||||
doAnswer(call -> {
|
||||
BlockPos position = call.getArgument(0);
|
||||
return pois.computeIfAbsent(SectionPos.of(position).asLong(), ignored -> new PoiSection(() -> {}))
|
||||
.add(position, call.getArgument(1));
|
||||
}).when(manager).add(any(), any());
|
||||
}
|
||||
|
||||
private void write(BlockPos position, BlockState state) {
|
||||
updates.setBlock(world, position, state, Block.UPDATE_CLIENTS, Block.UPDATE_LIMIT);
|
||||
}
|
||||
|
||||
private long[] marker() {
|
||||
return proto.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
|
||||
}
|
||||
|
||||
private PoiSection records() {
|
||||
return pois.get(SectionPos.of(new BlockPos(-31, 5, 49)).asLong());
|
||||
}
|
||||
}
|
||||
}
|
||||
+464
@@ -0,0 +1,464 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrainMode;
|
||||
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
|
||||
import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.entity.EntityTypeTest;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidStructure;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.GravityProcessor;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||
import net.minecraft.world.level.material.Fluid;
|
||||
import net.minecraft.world.level.material.FluidState;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraft.world.ticks.LevelTickAccess;
|
||||
import net.minecraft.world.ticks.ScheduledTick;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructureWorldgenAccessTest {
|
||||
private static final int GENERATION_CENTER_X = 60;
|
||||
private static final int GENERATION_CENTER_Z = 15;
|
||||
private static final int SURFACE_FIRST_FREE_Y = 80;
|
||||
private static final int FLOOR_FIRST_FREE_Y = 70;
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ordinaryWorldFlattenPreparationHandlesReferencedVillagePiecesBeyondEightChunks() {
|
||||
int farX = (GENERATION_CENTER_X - 9) << 4;
|
||||
int farZ = (GENERATION_CENTER_Z + 4) << 4;
|
||||
BoundingBox bounds = new BoundingBox(farX, 64, farZ, farX + 4, 90, farZ + 4);
|
||||
PoolElementStructurePiece piece = new PoolElementStructurePiece(
|
||||
null, StructurePoolElement.single("minecraft:empty").apply(StructureTemplatePool.Projection.RIGID),
|
||||
new BlockPos(farX, 64, farZ), 1, Rotation.NONE, bounds, LiquidSettings.APPLY_WATERLOGGING);
|
||||
Structure structure = new DesertPyramidStructure(new Structure.StructureSettings(
|
||||
HolderSet.empty(), Map.of(), GenerationStep.Decoration.SURFACE_STRUCTURES, TerrainAdjustment.BEARD_BOX));
|
||||
StructureStart start = new StructureStart(structure, generationCenter(), 0, new PiecesContainer(List.of(piece)));
|
||||
List<NativeStructureTerrainIntegrator.TerrainTarget> targets = List.of(
|
||||
new NativeStructureTerrainIntegrator.TerrainTarget("minecraft:village_plains", start,
|
||||
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FLATTEN)));
|
||||
BoundingBox area = new BoundingBox(generationCenter().getMinBlockX(), -64, generationCenter().getMinBlockZ(),
|
||||
generationCenter().getMaxBlockX(), 319, generationCenter().getMaxBlockZ());
|
||||
RecordingDelegate unbounded = new RecordingDelegate();
|
||||
unbounded.rejectDistantTerrainReads = true;
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> NativeStructurePostProcessor.prepareTerrain(
|
||||
unbounded.world(), area, targets, (palette, rng, x, y, z) -> Blocks.STONE.defaultBlockState()));
|
||||
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
recording.rejectDistantTerrainReads = true;
|
||||
NativeStructureWorldgenAccess access = access(recording);
|
||||
NativeStructurePostProcessor.prepareTerrain(
|
||||
access, area, targets, (palette, rng, x, y, z) -> Blocks.STONE.defaultBlockState());
|
||||
|
||||
assertEquals(0, recording.heightReads);
|
||||
assertEquals(0, recording.chunkReads);
|
||||
assertEquals(0, recording.terrainReads);
|
||||
assertEquals(0, recording.mutations);
|
||||
assertEquals(91, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG,
|
||||
generationCenter().getMinBlockX(), generationCenter().getMinBlockZ()));
|
||||
assertEquals(1, recording.heightReads);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void projectedTemplatePrefiltersUseRemoteTerrainAndRetainLocalHeightmaps() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
recording.rejectDistantTerrainReads = true;
|
||||
NativeStructureWorldgenAccess access = access(recording);
|
||||
GravityProcessor gravity = new GravityProcessor(Heightmap.Types.WORLD_SURFACE_WG, -1);
|
||||
BlockPos remote = new BlockPos((GENERATION_CENTER_X - 9) << 4, 64, (GENERATION_CENTER_Z + 4) << 4);
|
||||
StructureTemplate.StructureBlockInfo raw = new StructureTemplate.StructureBlockInfo(
|
||||
new BlockPos(0, 4, 0), Blocks.DIRT_PATH.defaultBlockState(), null);
|
||||
StructurePlaceSettings settings = new StructurePlaceSettings().addProcessor(gravity);
|
||||
|
||||
StructureTemplate.StructureBlockInfo projected = StructureTemplate.processBlockInfos(
|
||||
access, remote, BlockPos.ZERO, settings, List.of(raw)).getFirst();
|
||||
|
||||
assertEquals(remote.atY(SURFACE_FIRST_FREE_Y + 3), projected.pos());
|
||||
assertEquals(0, recording.heightReads);
|
||||
BlockPos local = generationCenter().getMiddleBlockPosition(64);
|
||||
StructureTemplate.StructureBlockInfo localBlock = new StructureTemplate.StructureBlockInfo(
|
||||
BlockPos.ZERO, Blocks.DIRT_PATH.defaultBlockState(), null);
|
||||
StructureTemplate.StructureBlockInfo localProjection = StructureTemplate.processBlockInfos(
|
||||
access, local, BlockPos.ZERO, settings, List.of(localBlock)).getFirst();
|
||||
assertEquals(local.atY(90), localProjection.pos());
|
||||
assertEquals(1, recording.heightReads);
|
||||
assertEquals(0, recording.chunkReads);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void distanceTwoTerrainReadsNeverReachTheDelegate() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
NativeStructureWorldgenAccess access = access(recording);
|
||||
ChunkPos generationCenter = generationCenter();
|
||||
int x = generationCenter.getMiddleBlockX();
|
||||
int z = (generationCenter.z() + 2) << 4;
|
||||
|
||||
assertSame(Blocks.STONE, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y - 1, z)).getBlock());
|
||||
assertSame(Blocks.WATER, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)).getBlock());
|
||||
assertSame(Blocks.AIR, access.getBlockState(new BlockPos(x, SURFACE_FIRST_FREE_Y, z)).getBlock());
|
||||
assertSame(Blocks.WATER.defaultBlockState().getFluidState(),
|
||||
access.getFluidState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)));
|
||||
assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z));
|
||||
assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z));
|
||||
assertNull(access.getBlockStateIfLoaded(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)));
|
||||
assertNull(access.getChunk(generationCenter.x(), generationCenter.z() + 2, ChunkStatus.FEATURES, false));
|
||||
assertNull(access.getChunkIfLoadedImmediately(
|
||||
generationCenter.x(), generationCenter.z() + 2));
|
||||
List<BlockState> streamed = access.getBlockStates(new AABB(
|
||||
x, FLOOR_FIRST_FREE_Y, z, x, FLOOR_FIRST_FREE_Y, z)).toList();
|
||||
|
||||
assertEquals(1, streamed.size());
|
||||
assertSame(Blocks.WATER, streamed.getFirst().getBlock());
|
||||
assertEquals(0, recording.terrainReads);
|
||||
assertEquals(0, recording.heightReads);
|
||||
assertEquals(0, recording.chunkReads);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void distanceTwoMutationsAndSideEffectsNeverReachTheDelegate() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
NativeStructureWorldgenAccess access = access(recording);
|
||||
ChunkPos generationCenter = generationCenter();
|
||||
BlockPos position = new BlockPos(
|
||||
generationCenter.getMiddleBlockX(),
|
||||
FLOOR_FIRST_FREE_Y,
|
||||
(generationCenter.z() + 2) << 4);
|
||||
|
||||
assertFalse(access.ensureCanWrite(position));
|
||||
assertFalse(access.setBlock(position, Blocks.DIRT.defaultBlockState(), 2));
|
||||
assertFalse(access.removeBlock(position, false));
|
||||
assertFalse(access.destroyBlock(position, false));
|
||||
access.updateNeighborsAt(position, Blocks.DIRT);
|
||||
access.neighborShapeChanged(
|
||||
Direction.NORTH, position, position.north(),
|
||||
Blocks.DIRT.defaultBlockState(), 2, 512);
|
||||
access.levelEvent(null, 2001, position, 0);
|
||||
access.gameEvent(null, Vec3.atCenterOf(position), null);
|
||||
access.addParticle(null, position.getX(), position.getY(), position.getZ(), 0, 0, 0);
|
||||
access.getBlockTicks().schedule(new ScheduledTick<>(
|
||||
Blocks.DIRT, position, 1L, 0L));
|
||||
|
||||
assertEquals(0, recording.mutations);
|
||||
assertEquals(0, recording.events);
|
||||
assertEquals(0, recording.blockTicks.count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void radiusOneReadsWritesAndTicksRemainUnchanged() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
NativeStructureWorldgenAccess access = access(recording);
|
||||
ChunkPos generationCenter = generationCenter();
|
||||
BlockPos position = new BlockPos(
|
||||
(generationCenter.x() + 1) << 4,
|
||||
FLOOR_FIRST_FREE_Y,
|
||||
(generationCenter.z() - 1) << 4);
|
||||
|
||||
assertTrue(access.isInsideGenerationRegion(
|
||||
generationCenter.x() + 1, generationCenter.z() - 1));
|
||||
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||
assertEquals(91, access.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ()));
|
||||
assertTrue(access.ensureCanWrite(position));
|
||||
assertTrue(access.setBlock(position, Blocks.STONE.defaultBlockState(), 2));
|
||||
assertTrue(access.removeBlock(position, false));
|
||||
assertTrue(access.destroyBlock(position, false));
|
||||
access.getBlockTicks().schedule(new ScheduledTick<>(
|
||||
Blocks.DIRT, position, 1L, 0L));
|
||||
|
||||
assertEquals(1, recording.terrainReads);
|
||||
assertEquals(1, recording.heightReads);
|
||||
assertEquals(4, recording.mutations);
|
||||
assertEquals(1, recording.blockTicks.count());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void virtualizedHeightsHideInRegionTerrainFromTheDelegate() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
ChunkPos generationCenter = generationCenter();
|
||||
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
|
||||
recording.world(), generationCenter,
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
true,
|
||||
position -> false);
|
||||
int x = generationCenter.getMiddleBlockX();
|
||||
int z = generationCenter.getMiddleBlockZ();
|
||||
|
||||
assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z));
|
||||
assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z));
|
||||
assertEquals(0, recording.heightReads);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void protectedCellsRemainReadableButRejectWritesTilesAndTicks() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
BlockPos position = generationCenter().getMiddleBlockPosition(70);
|
||||
NativeStructureWorldgenAccess access = NativeStructureWorldgenAccess.create(
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position::equals);
|
||||
|
||||
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||
assertFalse(access.ensureCanWrite(position));
|
||||
assertFalse(access.setBlock(position, Blocks.STONE.defaultBlockState(), 2));
|
||||
assertFalse(access.removeBlock(position, false));
|
||||
assertFalse(access.destroyBlock(position, false));
|
||||
assertNull(access.getBlockEntity(position));
|
||||
assertTrue(access.getBlockEntity(position, null).isEmpty());
|
||||
access.getBlockTicks().schedule(new ScheduledTick<>(Blocks.DIRT, position, 1L, 0L));
|
||||
access.updateNeighborsAt(position.east(), Blocks.DIRT);
|
||||
access.neighborShapeChanged(
|
||||
Direction.EAST, position, position.east(), Blocks.DIRT.defaultBlockState(), 2, 512);
|
||||
|
||||
assertEquals(1, recording.terrainReads);
|
||||
assertEquals(0, recording.mutations);
|
||||
assertEquals(0, recording.blockTicks.count());
|
||||
assertTrue(access.setBlock(position.above(), Blocks.STONE.defaultBlockState(), 2));
|
||||
assertEquals(1, recording.mutations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
NativeStructureWorldgenAccess access = access(recording);
|
||||
ChunkPos generationCenter = generationCenter();
|
||||
|
||||
assertNull(access.getChunk(generationCenter.getWorldPosition()));
|
||||
|
||||
assertEquals(1, recording.statuslessChunkReads);
|
||||
assertEquals(0, recording.statusChunkReads);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void entityQueriesRejectDisjointAreasAndClampOverlaps() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
NativeStructureWorldgenAccess access = access(recording);
|
||||
ChunkPos generationCenter = generationCenter();
|
||||
int safeMaxX = generationCenter.getMaxBlockX() + 17;
|
||||
int safeMinZ = generationCenter.getMinBlockZ() - 16;
|
||||
int safeMaxZ = generationCenter.getMaxBlockZ() + 17;
|
||||
AABB disjoint = new AABB(
|
||||
safeMaxX, -64, safeMinZ,
|
||||
safeMaxX + 16, 320, safeMaxZ);
|
||||
AABB overlap = new AABB(
|
||||
safeMaxX - 8, -128, safeMinZ - 8,
|
||||
safeMaxX + 16, 400, safeMaxZ + 8);
|
||||
EntityTypeTest<Entity, Entity> type = EntityTypeTest.forClass(Entity.class);
|
||||
|
||||
assertTrue(access.getEntities((Entity) null, disjoint, entity -> true).isEmpty());
|
||||
assertTrue(access.getEntities(type, disjoint, entity -> true).isEmpty());
|
||||
access.getEntities((Entity) null, overlap, entity -> true);
|
||||
access.getEntities(type, overlap, entity -> true);
|
||||
|
||||
assertEquals(2, recording.entityAreas.size());
|
||||
for (AABB delegatedArea : recording.entityAreas) {
|
||||
assertEquals(safeMaxX - 8, delegatedArea.minX, 0D);
|
||||
assertEquals(-64, delegatedArea.minY, 0D);
|
||||
assertEquals(safeMinZ, delegatedArea.minZ, 0D);
|
||||
assertEquals(safeMaxX, delegatedArea.maxX, 0D);
|
||||
assertEquals(320, delegatedArea.maxY, 0D);
|
||||
assertEquals(safeMaxZ, delegatedArea.maxZ, 0D);
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeStructureWorldgenAccess access(RecordingDelegate recording) {
|
||||
return NativeStructureWorldgenAccess.create(
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position -> false);
|
||||
}
|
||||
|
||||
private static ChunkPos generationCenter() {
|
||||
return new ChunkPos(GENERATION_CENTER_X, GENERATION_CENTER_Z);
|
||||
}
|
||||
|
||||
private static final class RecordingDelegate implements InvocationHandler {
|
||||
private final Holder<Biome> biome;
|
||||
private final BiomeManager biomeManager;
|
||||
private final RecordingTicks<Block> blockTicks;
|
||||
private final RecordingTicks<Fluid> fluidTicks;
|
||||
private final List<AABB> entityAreas;
|
||||
private int terrainReads;
|
||||
private int heightReads;
|
||||
private int chunkReads;
|
||||
private int statuslessChunkReads;
|
||||
private int statusChunkReads;
|
||||
private int mutations;
|
||||
private int events;
|
||||
private boolean rejectDistantTerrainReads;
|
||||
|
||||
private RecordingDelegate() {
|
||||
this.biome = Holder.direct((Biome) null);
|
||||
this.biomeManager = new BiomeManager((x, y, z) -> biome, 13L);
|
||||
this.blockTicks = new RecordingTicks<>();
|
||||
this.fluidTicks = new RecordingTicks<>();
|
||||
this.entityAreas = new ArrayList<>();
|
||||
}
|
||||
|
||||
private WorldGenLevel world() {
|
||||
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||
WorldGenLevel.class.getClassLoader(),
|
||||
new Class<?>[]{WorldGenLevel.class}, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] arguments) {
|
||||
String name = method.getName();
|
||||
if (name.equals("getSeaLevel")) {
|
||||
return 63;
|
||||
}
|
||||
if (name.equals("getBiome")) {
|
||||
return biome;
|
||||
}
|
||||
if (name.equals("getBiomeManager")) {
|
||||
return biomeManager;
|
||||
}
|
||||
if (name.equals("getBlockTicks")) {
|
||||
return blockTicks;
|
||||
}
|
||||
if (name.equals("getFluidTicks")) {
|
||||
return fluidTicks;
|
||||
}
|
||||
if (name.equals("getMinY")) {
|
||||
return -64;
|
||||
}
|
||||
if (name.equals("getHeight") && arguments == null) {
|
||||
return 384;
|
||||
}
|
||||
if (name.equals("getHeight")) {
|
||||
if (rejectDistantTerrainReads
|
||||
&& (Math.abs(((int) arguments[1] >> 4) - GENERATION_CENTER_X) > 1
|
||||
|| Math.abs(((int) arguments[2] >> 4) - GENERATION_CENTER_Z) > 1)) {
|
||||
throw new IllegalStateException("Requested chunk unavailable during world generation");
|
||||
}
|
||||
heightReads++;
|
||||
return 91;
|
||||
}
|
||||
if (name.equals("getBlockState")) {
|
||||
terrainReads++;
|
||||
return Blocks.DIRT.defaultBlockState();
|
||||
}
|
||||
if (name.equals("getFluidState")) {
|
||||
terrainReads++;
|
||||
return Blocks.WATER.defaultBlockState().getFluidState();
|
||||
}
|
||||
if (name.equals("getChunk")) {
|
||||
chunkReads++;
|
||||
if (arguments.length == 2) {
|
||||
statuslessChunkReads++;
|
||||
} else {
|
||||
statusChunkReads++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (name.equals("getChunkIfLoadedImmediately")) {
|
||||
chunkReads++;
|
||||
return null;
|
||||
}
|
||||
if (name.equals("getEntities")) {
|
||||
entityAreas.add((AABB) arguments[1]);
|
||||
return List.of();
|
||||
}
|
||||
if (name.equals("ensureCanWrite") || name.equals("setBlock")
|
||||
|| name.equals("removeBlock") || name.equals("destroyBlock")) {
|
||||
mutations++;
|
||||
return true;
|
||||
}
|
||||
if (name.equals("updateNeighborsAt") || name.equals("neighborShapeChanged")) {
|
||||
mutations++;
|
||||
return null;
|
||||
}
|
||||
if (name.equals("levelEvent") || name.equals("gameEvent") || name.equals("addParticle")) {
|
||||
events++;
|
||||
return null;
|
||||
}
|
||||
if (name.equals("hashCode")) {
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
if (name.equals("equals")) {
|
||||
return proxy == arguments[0];
|
||||
}
|
||||
if (name.equals("toString")) {
|
||||
return "native-structure-worldgen-test-delegate";
|
||||
}
|
||||
throw new UnsupportedOperationException(method.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingTicks<T> implements LevelTickAccess<T> {
|
||||
private int count;
|
||||
|
||||
@Override
|
||||
public void schedule(ScheduledTick<T> tick) {
|
||||
count++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasScheduledTick(BlockPos position, T type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int count() {
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean willTickThisTick(BlockPos position, T type) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NmsChunkIOFlushTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeDrainCanWaitOffOwnerUntilChunkPoiAndEntityIoFinish() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
AtomicBoolean chunkPending = new AtomicBoolean(true);
|
||||
AtomicBoolean poiPending = new AtomicBoolean(true);
|
||||
AtomicBoolean entityPending = new AtomicBoolean(true);
|
||||
CountDownLatch chunkDrain = new CountDownLatch(1);
|
||||
CountDownLatch poiDrain = new CountDownLatch(1);
|
||||
CountDownLatch entityDrain = new CountDownLatch(1);
|
||||
when(fixture.chunks.hasTasks()).thenAnswer(invocation -> {
|
||||
chunkDrain.countDown();
|
||||
return chunkPending.get();
|
||||
});
|
||||
when(fixture.pois.hasTasks()).thenAnswer(invocation -> {
|
||||
poiDrain.countDown();
|
||||
return poiPending.get();
|
||||
});
|
||||
when(fixture.entities.hasTasks()).thenAnswer(invocation -> {
|
||||
entityDrain.countDown();
|
||||
return entityPending.get();
|
||||
});
|
||||
ExecutorService io = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
Future<?> drain = io.submit(() -> fixture.binding.flushChunkIO(fixture.world));
|
||||
assertTrue(chunkDrain.await(5L, TimeUnit.SECONDS));
|
||||
assertFalse(drain.isDone());
|
||||
chunkPending.set(false);
|
||||
assertTrue(poiDrain.await(5L, TimeUnit.SECONDS));
|
||||
assertFalse(drain.isDone());
|
||||
poiPending.set(false);
|
||||
assertTrue(entityDrain.await(5L, TimeUnit.SECONDS));
|
||||
assertFalse(drain.isDone());
|
||||
entityPending.set(false);
|
||||
drain.get(5L, TimeUnit.SECONDS);
|
||||
} finally {
|
||||
chunkPending.set(false);
|
||||
poiPending.set(false);
|
||||
entityPending.set(false);
|
||||
io.shutdownNow();
|
||||
assertTrue(io.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeDrainFailurePropagatesToItsCompletionReceipt() {
|
||||
Fixture fixture = new Fixture();
|
||||
IllegalStateException failure = new IllegalStateException("native IO failed");
|
||||
when(fixture.chunks.hasTasks()).thenThrow(failure);
|
||||
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||
() -> fixture.binding.flushChunkIO(fixture.world)));
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||
private final CraftWorld world = mock(CraftWorld.class);
|
||||
private final ServerLevel level = mock(ServerLevel.class);
|
||||
private final RegionDataController chunks = mock(RegionDataController.class);
|
||||
private final RegionDataController pois = mock(RegionDataController.class);
|
||||
private final RegionDataController entities = mock(RegionDataController.class);
|
||||
|
||||
private Fixture() {
|
||||
when(world.getHandle()).thenReturn(level);
|
||||
when(level.moonrise$getChunkDataController()).thenReturn(chunks);
|
||||
when(level.moonrise$getPoiChunkDataController()).thenReturn(pois);
|
||||
when(level.moonrise$getEntityChunkDataController()).thenReturn(entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerChunkCache;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NmsChunkTaskPollingTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serverOwnerPumpsOnlyTheTargetWorldQueue() {
|
||||
Server bukkitServer = mock(Server.class);
|
||||
CraftWorld world = mock(CraftWorld.class);
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
MinecraftServer server = mock(MinecraftServer.class);
|
||||
ServerChunkCache chunks = mock(ServerChunkCache.class);
|
||||
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||
CompletableFuture<Void> completed = new CompletableFuture<>();
|
||||
when(world.getHandle()).thenReturn(level);
|
||||
when(level.getServer()).thenReturn(server);
|
||||
when(server.getRunningThread()).thenReturn(Thread.currentThread());
|
||||
when(level.getChunkSource()).thenReturn(chunks);
|
||||
when(chunks.pollTask()).thenAnswer(invocation -> completed.complete(null)).thenReturn(false);
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
|
||||
bukkit.when(Bukkit::getServer).thenReturn(bukkitServer);
|
||||
folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(false);
|
||||
assertTrue(binding.pollChunkTask(world));
|
||||
assertTrue(completed.isDone());
|
||||
assertFalse(binding.pollChunkTask(world));
|
||||
}
|
||||
verify(chunks, times(2)).pollTask();
|
||||
verify(server, never()).executeAllRecentInternalTasks();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonOwnerIsRejectedBeforeAccessingTheChunkQueue() {
|
||||
Server bukkitServer = mock(Server.class);
|
||||
CraftWorld world = mock(CraftWorld.class);
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
MinecraftServer server = mock(MinecraftServer.class);
|
||||
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||
when(world.getHandle()).thenReturn(level);
|
||||
when(level.getServer()).thenReturn(server);
|
||||
when(server.getRunningThread()).thenReturn(new Thread("Other server owner"));
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
|
||||
bukkit.when(Bukkit::getServer).thenReturn(bukkitServer);
|
||||
folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(false);
|
||||
assertThrows(IllegalStateException.class, () -> binding.pollChunkTask(world));
|
||||
}
|
||||
verify(level, never()).getChunkSource();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void foliaNeverUsesTheWorldWideTaskPump() {
|
||||
Server bukkitServer = mock(Server.class);
|
||||
CraftWorld world = mock(CraftWorld.class);
|
||||
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||
MockedStatic<FoliaScheduler> folia = mockStatic(FoliaScheduler.class)) {
|
||||
bukkit.when(Bukkit::getServer).thenReturn(bukkitServer);
|
||||
folia.when(() -> FoliaScheduler.isRegionizedRuntime(bukkitServer)).thenReturn(true);
|
||||
assertFalse(binding.pollChunkTask(world));
|
||||
}
|
||||
verifyNoInteractions(world);
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import org.bukkit.Difficulty;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NmsEntitySpawnDifficultyTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void peacefulRejectsEveryNativeForbiddenTypeBeforeBukkitCreatesIt() {
|
||||
CraftWorld world = mock(CraftWorld.class);
|
||||
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
|
||||
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||
Location location = new Location(world, 0.5, 80, 0.5);
|
||||
int rejected = 0;
|
||||
|
||||
for (EntityType type : EntityType.values()) {
|
||||
if (type.getEntityClass() == null) {
|
||||
continue;
|
||||
}
|
||||
if (BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse(type.getKey().toString())).isAllowedInPeaceful()) {
|
||||
continue;
|
||||
}
|
||||
assertNull(type.name(), binding.spawnEntity(location, type, CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||
rejected++;
|
||||
}
|
||||
|
||||
assertTrue(rejected > 20);
|
||||
verify(world, never()).spawn(any(Location.class), any(Class.class), isNull(), any(CreatureSpawnEvent.SpawnReason.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void peacefulPreservesPassiveAndNativeMonsterCategoryExceptions() {
|
||||
CraftWorld world = mock(CraftWorld.class);
|
||||
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
|
||||
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||
Location location = new Location(world, 0.5, 80, 0.5);
|
||||
Entity created = mock(Entity.class);
|
||||
doReturn(created).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||
|
||||
for (EntityType type : List.of(EntityType.COW, EntityType.VILLAGER, EntityType.WOLF, EntityType.PIGLIN, EntityType.SHULKER)) {
|
||||
assertSame(type.name(), created, binding.spawnEntity(location, type, CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonPeacefulDifficultiesPreserveHostileSpawns() {
|
||||
CraftWorld world = mock(CraftWorld.class);
|
||||
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||
Location location = new Location(world, 0.5, 80, 0.5);
|
||||
Entity created = mock(Entity.class);
|
||||
doReturn(created).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||
|
||||
for (Difficulty difficulty : List.of(Difficulty.EASY, Difficulty.NORMAL, Difficulty.HARD)) {
|
||||
when(world.getDifficulty()).thenReturn(difficulty);
|
||||
assertSame(created, binding.spawnEntity(location, EntityType.ZOMBIE, CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unrelatedBukkitSpawnFailuresStillPropagate() {
|
||||
CraftWorld world = mock(CraftWorld.class);
|
||||
when(world.getDifficulty()).thenReturn(Difficulty.PEACEFUL);
|
||||
NMSBinding binding = mock(NMSBinding.class, CALLS_REAL_METHODS);
|
||||
Location location = new Location(world, 0.5, 80, 0.5);
|
||||
IllegalStateException failure = new IllegalStateException("Spawn rejected by platform");
|
||||
doThrow(failure).when(world).spawn(eq(location), any(Class.class), isNull(), eq(CreatureSpawnEvent.SpawnReason.NATURAL));
|
||||
|
||||
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||
() -> binding.spawnEntity(location, EntityType.COW, CreatureSpawnEvent.SpawnReason.NATURAL)));
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.world.history.SavedTerrainChunk;
|
||||
import ca.spottedleaf.concurrentutil.util.Priority;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
||||
public class NmsSavedTerrainCheckpointTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkpointWaitsForDirtyAndUnloadedChunkWritesBeforeFlushAndVerification() throws Exception {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
Path root = Path.of("checkpoint-world");
|
||||
byte[] receipt = new byte[]{1, 2, 3};
|
||||
NmsSavedTerrainCapture.Checkpoint saved = new NmsSavedTerrainCapture.Checkpoint(new ChunkPos(4, -5),
|
||||
new NmsSavedTerrainCapture.Verification("minecraft:full", receipt, 7L));
|
||||
NmsSavedTerrainCapture.Checkpoint unloaded = NmsSavedTerrainCapture.checkpoint(-8, 9, null);
|
||||
Set<ChunkPos> completed = new HashSet<>();
|
||||
AtomicBoolean flushed = new AtomicBoolean();
|
||||
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
|
||||
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
|
||||
io.when(() -> MoonriseRegionFileIO.getPriority(level, 4, -5, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||
.thenAnswer(invocation -> {
|
||||
completed.add(saved.chunk());
|
||||
return Priority.COMPLETING;
|
||||
});
|
||||
io.when(() -> MoonriseRegionFileIO.getPriority(level, -8, 9, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||
.thenAnswer(invocation -> {
|
||||
completed.add(unloaded.chunk());
|
||||
return Priority.COMPLETING;
|
||||
});
|
||||
io.when(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||
.thenAnswer(invocation -> {
|
||||
assertEquals(Set.of(saved.chunk(), unloaded.chunk()), completed);
|
||||
flushed.set(true);
|
||||
return null;
|
||||
});
|
||||
terrain.when(() -> SavedTerrainChunk.verifyCheckpoint(root, 4, -5, "minecraft:full", receipt, 7L))
|
||||
.thenAnswer(invocation -> {
|
||||
assertTrue(flushed.get());
|
||||
return null;
|
||||
});
|
||||
|
||||
NmsSavedTerrainCapture.finishCheckpoint(level, root, List.of(saved, unloaded));
|
||||
|
||||
terrain.verify(() -> SavedTerrainChunk.verifyCheckpoint(root, 4, -5, "minecraft:full", receipt, 7L));
|
||||
io.verify(() -> MoonriseRegionFileIO.flush(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), never());
|
||||
io.verify(() -> MoonriseRegionFileIO.getPriority(level, 100, 100, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), never());
|
||||
terrain.verifyNoMoreInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void storageFlushFailureCannotCompleteTheCheckpoint() throws Exception {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
IOException failure = new IOException("storage flush failed");
|
||||
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
|
||||
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
|
||||
io.when(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||
.thenThrow(failure);
|
||||
|
||||
assertSame(failure, assertThrows(IOException.class,
|
||||
() -> NmsSavedTerrainCapture.finishCheckpoint(level, Path.of("checkpoint-world"), List.of())));
|
||||
terrain.verifyNoInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistedReceiptMismatchCannotCompleteTheCheckpoint() throws Exception {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
Path root = Path.of("checkpoint-world");
|
||||
byte[] receipt = new byte[]{4, 5};
|
||||
IOException failure = new IOException("saved terrain receipt mismatch");
|
||||
NmsSavedTerrainCapture.Checkpoint saved = new NmsSavedTerrainCapture.Checkpoint(new ChunkPos(1, 2),
|
||||
new NmsSavedTerrainCapture.Verification("minecraft:noise", receipt, 3L));
|
||||
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class);
|
||||
MockedStatic<SavedTerrainChunk> terrain = mockStatic(SavedTerrainChunk.class)) {
|
||||
io.when(() -> MoonriseRegionFileIO.getPriority(level, 1, 2, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||
.thenReturn(Priority.COMPLETING);
|
||||
terrain.when(() -> SavedTerrainChunk.verifyCheckpoint(root, 1, 2, "minecraft:noise", receipt, 3L))
|
||||
.thenThrow(failure);
|
||||
|
||||
assertSame(failure, assertThrows(IOException.class,
|
||||
() -> NmsSavedTerrainCapture.finishCheckpoint(level, root, List.of(saved))));
|
||||
io.verify(() -> MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA));
|
||||
}
|
||||
}
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.world.task.J;
|
||||
import ca.spottedleaf.concurrentutil.completable.Completable;
|
||||
import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
|
||||
import ca.spottedleaf.concurrentutil.executor.thread.BalancedPrioritisedThreadPool;
|
||||
import ca.spottedleaf.concurrentutil.util.Priority;
|
||||
import ca.spottedleaf.moonrise.common.PlatformHooks;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
|
||||
import org.bukkit.World;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NmsSavedTerrainSnapshotTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ownerReservesSaveOrderBeforeOffOwnerSerializationAndLaterAutosave() throws Exception {
|
||||
try (SaveFixture fixture = new SaveFixture();
|
||||
ExecutorService checkpoint = Executors.newSingleThreadExecutor();
|
||||
MockedStatic<SerializableChunkData> snapshots = mockStatic(SerializableChunkData.class);
|
||||
MockedStatic<PlatformHooks> hooks = mockStatic(PlatformHooks.class);
|
||||
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
|
||||
Thread owner = Thread.currentThread();
|
||||
ChunkAccess chunk = mock(ChunkAccess.class);
|
||||
SerializableChunkData snapshot = mock(SerializableChunkData.class);
|
||||
PlatformHooks platform = mock(PlatformHooks.class);
|
||||
CompoundTag capturedData = new CompoundTag();
|
||||
CompoundTag autosave = new CompoundTag();
|
||||
List<String> submissions = new ArrayList<>();
|
||||
when(chunk.getPos()).thenReturn(new ChunkPos(4, -9));
|
||||
snapshots.when(() -> SerializableChunkData.copyOf(fixture.level, chunk)).thenAnswer(invocation -> {
|
||||
assertSame(owner, Thread.currentThread());
|
||||
return snapshot;
|
||||
});
|
||||
hooks.when(PlatformHooks::get).thenReturn(platform);
|
||||
doAnswer(invocation -> {
|
||||
assertSame(owner, Thread.currentThread());
|
||||
return null;
|
||||
}).when(platform).chunkSyncSave(fixture.level, chunk, snapshot);
|
||||
when(snapshot.write()).thenAnswer(invocation -> {
|
||||
assertNotSame(owner, Thread.currentThread());
|
||||
assertEquals(List.of("checkpoint", "autosave"), submissions);
|
||||
return capturedData;
|
||||
});
|
||||
io.when(() -> MoonriseRegionFileIO.scheduleSave(eq(fixture.level), eq(4), eq(-9),
|
||||
any(Completable.class), any(PrioritisedExecutor.PrioritisedTask.class),
|
||||
eq(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), eq(Priority.NORMAL)))
|
||||
.thenAnswer(invocation -> {
|
||||
assertSame(owner, Thread.currentThread());
|
||||
submissions.add("checkpoint");
|
||||
return null;
|
||||
});
|
||||
io.when(() -> MoonriseRegionFileIO.scheduleSave(fixture.level, 4, -9, autosave,
|
||||
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA)).thenAnswer(invocation -> {
|
||||
submissions.add("autosave");
|
||||
return null;
|
||||
});
|
||||
|
||||
NmsSavedTerrainCapture.CapturedChunk captured = NmsSavedTerrainCapture.scheduleSnapshot(fixture.level, chunk);
|
||||
|
||||
assertEquals(List.of("checkpoint"), submissions);
|
||||
assertFalse(captured.data().isDone());
|
||||
verify(snapshot, never()).write();
|
||||
verify(platform).chunkSyncSave(fixture.level, chunk, snapshot);
|
||||
MoonriseRegionFileIO.scheduleSave(fixture.level, 4, -9, autosave,
|
||||
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
|
||||
Future<CompoundTag> result = checkpoint.submit(() -> NmsSavedTerrainCapture.awaitSnapshot(
|
||||
fixture.level, captured, System.nanoTime() + TimeUnit.SECONDS.toNanos(3)));
|
||||
|
||||
assertSame(capturedData, result.get(5, TimeUnit.SECONDS));
|
||||
verify(snapshot).write();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedOffOwnerSerializationCannotSucceedAsACheckpoint() throws Exception {
|
||||
try (SaveFixture fixture = new SaveFixture();
|
||||
ExecutorService checkpoint = Executors.newSingleThreadExecutor();
|
||||
MockedStatic<SerializableChunkData> snapshots = mockStatic(SerializableChunkData.class);
|
||||
MockedStatic<PlatformHooks> hooks = mockStatic(PlatformHooks.class);
|
||||
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
|
||||
ChunkAccess chunk = mock(ChunkAccess.class);
|
||||
SerializableChunkData snapshot = mock(SerializableChunkData.class);
|
||||
IllegalStateException failure = new IllegalStateException("serialization failed");
|
||||
when(chunk.getPos()).thenReturn(new ChunkPos(4, -9));
|
||||
snapshots.when(() -> SerializableChunkData.copyOf(fixture.level, chunk)).thenReturn(snapshot);
|
||||
hooks.when(PlatformHooks::get).thenReturn(mock(PlatformHooks.class));
|
||||
when(snapshot.write()).thenThrow(failure);
|
||||
NmsSavedTerrainCapture.CapturedChunk captured = NmsSavedTerrainCapture.scheduleSnapshot(fixture.level, chunk);
|
||||
|
||||
Future<IOException> result = checkpoint.submit(() -> assertThrows(IOException.class,
|
||||
() -> NmsSavedTerrainCapture.awaitSnapshot(fixture.level, captured,
|
||||
System.nanoTime() + TimeUnit.SECONDS.toNanos(3))));
|
||||
|
||||
assertSame(failure, result.get(5, TimeUnit.SECONDS).getCause());
|
||||
assertTrue(captured.data().isCompletedExceptionally());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkpointBoundsOwnerRequestsUntilPriorNativeWritesFinish() throws Exception {
|
||||
try (SaveFixture fixture = new SaveFixture();
|
||||
ExecutorService owner = Executors.newSingleThreadExecutor();
|
||||
ExecutorService checkpoint = Executors.newSingleThreadExecutor()) {
|
||||
World world = mock(World.class);
|
||||
when(world.getWorldFolder()).thenReturn(new File("checkpoint-world"));
|
||||
List<NewChunkHolder> holders = new ArrayList<>(129);
|
||||
for (int index = 0; index < 129; index++) {
|
||||
NewChunkHolder holder = mock(NewChunkHolder.class);
|
||||
setField(holder, "chunkX", index);
|
||||
holders.add(holder);
|
||||
}
|
||||
AtomicInteger requested = new AtomicInteger();
|
||||
AtomicBoolean writesBlocked = new AtomicBoolean(true);
|
||||
CountDownLatch firstBatchWaiting = new CountDownLatch(1);
|
||||
Future<?> result = checkpoint.submit(() -> {
|
||||
try (MockedStatic<J> scheduling = mockStatic(J.class);
|
||||
MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
|
||||
scheduling.when(() -> J.runRegionFuture(eq(world), anyInt(), anyInt(), any(Runnable.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
requested.incrementAndGet();
|
||||
Runnable action = invocation.getArgument(3);
|
||||
return CompletableFuture.runAsync(action, owner);
|
||||
});
|
||||
io.when(() -> MoonriseRegionFileIO.getPriority(eq(fixture.level), anyInt(), anyInt(),
|
||||
eq(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))).thenAnswer(invocation -> {
|
||||
firstBatchWaiting.countDown();
|
||||
return writesBlocked.get() ? Priority.NORMAL : Priority.COMPLETING;
|
||||
});
|
||||
NmsSavedTerrainCapture.flushSnapshots(world, fixture.level, holders);
|
||||
io.verify(() -> MoonriseRegionFileIO.getPriority(fixture.level, 128, 0,
|
||||
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA), times(2));
|
||||
return null;
|
||||
}
|
||||
});
|
||||
try {
|
||||
assertTrue(firstBatchWaiting.await(5, TimeUnit.SECONDS));
|
||||
assertEquals(64, requested.get());
|
||||
assertTrue(owner.submit(() -> true).get(5, TimeUnit.SECONDS));
|
||||
assertFalse(result.isDone());
|
||||
writesBlocked.set(false);
|
||||
result.get(5, TimeUnit.SECONDS);
|
||||
assertEquals(129, requested.get());
|
||||
} finally {
|
||||
writesBlocked.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void setField(Object instance, String name, Object value) throws Exception {
|
||||
Field field = instance.getClass().getField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(instance, value);
|
||||
}
|
||||
|
||||
private static final class SaveFixture implements AutoCloseable {
|
||||
private final BalancedPrioritisedThreadPool pool = new BalancedPrioritisedThreadPool(
|
||||
TimeUnit.MILLISECONDS.toNanos(1), Executors.defaultThreadFactory());
|
||||
private final ServerLevel level = mock(ServerLevel.class);
|
||||
|
||||
private SaveFixture() throws Exception {
|
||||
BalancedPrioritisedThreadPool.OrderedStreamGroup group = pool.createOrderedStreamGroup();
|
||||
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
|
||||
setField(scheduler, "saveExecutor", group.createExecutor());
|
||||
setField(scheduler, "compressionExecutor", group.createExecutor());
|
||||
setField(scheduler, "chunkHolderManager", mock(ChunkHolderManager.class));
|
||||
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
pool.halt(true);
|
||||
assertTrue(pool.joinInterruptable(5_000));
|
||||
}
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import ca.spottedleaf.concurrentutil.executor.thread.BalancedPrioritisedThreadPool;
|
||||
import ca.spottedleaf.concurrentutil.util.Priority;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NmsSavedTerrainWriteBarrierTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkpointHelpsSaveAndCompressionWhileNativeGenerationOccupiesTheWorker() throws Exception {
|
||||
BalancedPrioritisedThreadPool pool = new BalancedPrioritisedThreadPool(
|
||||
TimeUnit.MILLISECONDS.toNanos(1), Executors.defaultThreadFactory());
|
||||
BalancedPrioritisedThreadPool.OrderedStreamGroup group = pool.createOrderedStreamGroup();
|
||||
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue generation = group.createExecutor();
|
||||
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue saves = group.createExecutor();
|
||||
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue compression = group.createExecutor();
|
||||
ServerLevel level = level(saves, compression);
|
||||
ChunkPos chunk = new ChunkPos(4, -9);
|
||||
CountDownLatch generationEntered = new CountDownLatch(1);
|
||||
CountDownLatch releaseGeneration = new CountDownLatch(1);
|
||||
AtomicBoolean pendingWrite = new AtomicBoolean(true);
|
||||
AtomicInteger serialized = new AtomicInteger();
|
||||
AtomicInteger compressed = new AtomicInteger();
|
||||
AtomicReference<Thread> saveThread = new AtomicReference<>();
|
||||
AtomicReference<Thread> compressionThread = new AtomicReference<>();
|
||||
try (MockedStatic<MoonriseRegionFileIO> io = mockStatic(MoonriseRegionFileIO.class)) {
|
||||
io.when(() -> MoonriseRegionFileIO.getPriority(level, chunk.x(), chunk.z(),
|
||||
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA))
|
||||
.thenAnswer(invocation -> pendingWrite.get() ? Priority.NORMAL : Priority.COMPLETING);
|
||||
generation.queueTask(() -> {
|
||||
generationEntered.countDown();
|
||||
try {
|
||||
assertTrue(releaseGeneration.await(5, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException interruption) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AssertionError(interruption);
|
||||
}
|
||||
});
|
||||
pool.adjustThreadCount(1);
|
||||
assertTrue(generationEntered.await(5, TimeUnit.SECONDS));
|
||||
saves.queueTask(() -> {
|
||||
saveThread.set(Thread.currentThread());
|
||||
serialized.incrementAndGet();
|
||||
compression.queueTask(() -> {
|
||||
compressionThread.set(Thread.currentThread());
|
||||
compressed.incrementAndGet();
|
||||
pendingWrite.set(false);
|
||||
});
|
||||
});
|
||||
|
||||
NmsSavedTerrainCapture.awaitWrite(level, chunk, System.nanoTime() + TimeUnit.SECONDS.toNanos(3));
|
||||
|
||||
assertEquals(1L, releaseGeneration.getCount());
|
||||
assertEquals(1, serialized.get());
|
||||
assertEquals(1, compressed.get());
|
||||
assertFalse(pendingWrite.get());
|
||||
assertSame(Thread.currentThread(), saveThread.get());
|
||||
assertSame(Thread.currentThread(), compressionThread.get());
|
||||
} finally {
|
||||
releaseGeneration.countDown();
|
||||
pool.halt(true);
|
||||
assertTrue(pool.joinInterruptable(5_000));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void waitingForNativeWriteHonorsTheSharedDeadline() throws Exception {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
ChunkPos chunk = new ChunkPos(2, 3);
|
||||
IOException failure = assertThrows(IOException.class,
|
||||
() -> NmsSavedTerrainCapture.awaitWrite(level, chunk, System.nanoTime() - 1L));
|
||||
assertTrue(failure.getMessage().contains("Timed out"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interruptedNativeWriteWaitPreservesInterruption() throws Exception {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
ChunkPos chunk = new ChunkPos(2, 3);
|
||||
Thread.currentThread().interrupt();
|
||||
try {
|
||||
IOException failure = assertThrows(IOException.class,
|
||||
() -> NmsSavedTerrainCapture.awaitWrite(level, chunk,
|
||||
System.nanoTime() + TimeUnit.SECONDS.toNanos(3)));
|
||||
assertTrue(failure.getMessage().contains("Interrupted"));
|
||||
assertTrue(Thread.currentThread().isInterrupted());
|
||||
} finally {
|
||||
Thread.interrupted();
|
||||
}
|
||||
}
|
||||
|
||||
private static ServerLevel level(BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue saves,
|
||||
BalancedPrioritisedThreadPool.OrderedStreamGroup.Queue compression) throws Exception {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
|
||||
Field savesField = ChunkTaskScheduler.class.getField("saveExecutor");
|
||||
Field compressionField = ChunkTaskScheduler.class.getField("compressionExecutor");
|
||||
savesField.setAccessible(true);
|
||||
compressionField.setAccessible(true);
|
||||
savesField.set(scheduler, saves);
|
||||
compressionField.set(scheduler, compression);
|
||||
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
|
||||
return level;
|
||||
}
|
||||
}
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
|
||||
import com.mojang.serialization.Codec;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.IdMapper;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.PalettedContainer;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.Strategy;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.IglooPieces;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
|
||||
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class WorldgenTerrainHeightmapsTest {
|
||||
private static final int MIN_Y = -64;
|
||||
private static final int HEIGHT = 384;
|
||||
private static final int LAND_TERRAIN_TOP = 70;
|
||||
private static final int LAND_CANOPY_TOP = 80;
|
||||
private static final int OCEAN_FLOOR_TOP = 60;
|
||||
private static final int OCEAN_FLUID_TOP = 64;
|
||||
private static final int LAND_X = 3;
|
||||
private static final int LAND_Z = 5;
|
||||
private static final int OCEAN_X = 8;
|
||||
private static final int OCEAN_Z = 8;
|
||||
private static final int TERRAIN_SLAB_DEPTH = 4;
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
|
||||
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||
|
||||
IrisChunkGenerator.primeAuthoringHeightmaps(chunk);
|
||||
|
||||
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
|
||||
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, LAND_X, LAND_Z));
|
||||
assertEquals(LAND_TERRAIN_TOP + 6,
|
||||
chunk.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, LAND_X, LAND_Z));
|
||||
assertEquals(OCEAN_FLUID_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, OCEAN_X, OCEAN_Z));
|
||||
assertEquals(OCEAN_FLOOR_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, OCEAN_X, OCEAN_Z));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void primeTerrainWritesEngineSurfaceAndOceanFloorHeights() {
|
||||
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||
|
||||
assertFalse(chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||
assertFalse(chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
|
||||
|
||||
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
|
||||
|
||||
assertTrue(chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||
assertTrue(chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
|
||||
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||
Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
|
||||
assertEquals(OCEAN_FLUID_TOP, chunk.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, OCEAN_X, OCEAN_Z));
|
||||
assertEquals(OCEAN_FLOOR_TOP, chunk.getHeight(
|
||||
Heightmap.Types.OCEAN_FLOOR_WG, OCEAN_X, OCEAN_Z));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void finalHeightmapPrimingDoesNotClobberTheWorldgenHeightmaps() {
|
||||
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||
|
||||
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
|
||||
Heightmap.primeHeightmaps(chunk, ChunkStatus.FINAL_HEIGHTMAPS);
|
||||
|
||||
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||
Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
|
||||
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE, LAND_X, LAND_Z));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unprimedWorldgenHeightmapResolvesTheIrisCanopyInstead() {
|
||||
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||
|
||||
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void primeTerrainOverwritesAnAlreadyLazyPrimedCanopyHeight() {
|
||||
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||
|
||||
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY(), floorFirstFreeY());
|
||||
|
||||
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structurePlacementPrimesTheWestNeighbourAnIglooEntranceReads() {
|
||||
Map<Long, ChunkAccess> chunks = new HashMap<>();
|
||||
chunks.put(ChunkPos.pack(0, 0), terrainChunk(new ChunkPos(0, 0)));
|
||||
chunks.put(ChunkPos.pack(-1, 0), terrainChunk(new ChunkPos(-1, 0)));
|
||||
WorldGenLevel world = world(chunks);
|
||||
StructureStart start = startInOrigin();
|
||||
|
||||
int canopySnap = IglooPieces.GENERATION_HEIGHT + iglooSnapOffset(world);
|
||||
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||
world, new ChunkPos(0, 0), List.of(start), surfaceFirstFreeY(), floorFirstFreeY());
|
||||
int terrainSnap = IglooPieces.GENERATION_HEIGHT + iglooSnapOffset(world);
|
||||
|
||||
assertEquals(LAND_TERRAIN_TOP + 1, world.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, -2, 5));
|
||||
assertEquals(LAND_TERRAIN_TOP, terrainSnap);
|
||||
assertEquals(LAND_CANOPY_TOP, canopySnap);
|
||||
assertEquals(LAND_CANOPY_TOP - LAND_TERRAIN_TOP, canopySnap - terrainSnap);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structurePlacementPrimesEveryChunkTheStartFootprintTouches() {
|
||||
Map<Long, ChunkAccess> chunks = new HashMap<>();
|
||||
for (int chunkX = -1; chunkX <= 1; chunkX++) {
|
||||
for (int chunkZ = -1; chunkZ <= 1; chunkZ++) {
|
||||
chunks.put(ChunkPos.pack(chunkX, chunkZ), terrainChunk(new ChunkPos(chunkX, chunkZ)));
|
||||
}
|
||||
}
|
||||
WorldGenLevel world = world(chunks);
|
||||
|
||||
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||
world, new ChunkPos(0, 0), List.of(startInOrigin()),
|
||||
surfaceFirstFreeY(), floorFirstFreeY());
|
||||
|
||||
for (ChunkAccess chunk : chunks.values()) {
|
||||
assertTrue(chunk.getPos().toString(),
|
||||
chunk.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||
assertTrue(chunk.getPos().toString(),
|
||||
chunk.hasPrimedHeightmap(Heightmap.Types.OCEAN_FLOOR_WG));
|
||||
assertEquals(LAND_TERRAIN_TOP, chunk.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structurePlacementDoesNotPrimeALoadedDistanceTwoChunk() {
|
||||
Map<Long, ChunkAccess> chunks = new HashMap<>();
|
||||
ProtoChunk origin = terrainChunk(new ChunkPos(0, 0));
|
||||
ProtoChunk neighbour = terrainChunk(new ChunkPos(1, 0));
|
||||
ProtoChunk distanceTwo = terrainChunk(new ChunkPos(2, 0));
|
||||
chunks.put(ChunkPos.pack(0, 0), origin);
|
||||
chunks.put(ChunkPos.pack(1, 0), neighbour);
|
||||
chunks.put(ChunkPos.pack(2, 0), distanceTwo);
|
||||
WorldGenLevel world = world(chunks);
|
||||
StructureStart shifted = startInOrigin();
|
||||
shifted.getPieces().forEach(piece -> piece.move(16, 0, 0));
|
||||
|
||||
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||
world, new ChunkPos(0, 0), List.of(shifted),
|
||||
surfaceFirstFreeY(), floorFirstFreeY());
|
||||
|
||||
assertTrue(origin.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||
assertTrue(neighbour.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||
assertFalse(distanceTwo.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structurePlacementSkipsChunksOutsideTheGenerationRegion() {
|
||||
Map<Long, ChunkAccess> chunks = new HashMap<>();
|
||||
ProtoChunk origin = terrainChunk(new ChunkPos(0, 0));
|
||||
chunks.put(ChunkPos.pack(0, 0), origin);
|
||||
WorldGenLevel world = world(chunks);
|
||||
|
||||
WorldgenTerrainHeightmaps.primeStructurePlacement(
|
||||
world, new ChunkPos(0, 0), List.of(startInOrigin()),
|
||||
surfaceFirstFreeY(), floorFirstFreeY());
|
||||
|
||||
assertTrue(origin.hasPrimedHeightmap(Heightmap.Types.WORLD_SURFACE_WG));
|
||||
assertEquals(LAND_TERRAIN_TOP, origin.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||
}
|
||||
|
||||
private static int iglooSnapOffset(WorldGenLevel world) {
|
||||
return world.getHeight(Heightmap.Types.WORLD_SURFACE_WG, -2, 5)
|
||||
- IglooPieces.GENERATION_HEIGHT - 1;
|
||||
}
|
||||
|
||||
private static StructureStart startInOrigin() {
|
||||
Structure structure = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(13L), 0, 0);
|
||||
return new StructureStart(
|
||||
structure, new ChunkPos(0, 0), 0, new PiecesContainer(List.of(piece)));
|
||||
}
|
||||
|
||||
private static IntBinaryOperator surfaceFirstFreeY() {
|
||||
return (x, z) -> isOcean(x, z) ? OCEAN_FLUID_TOP + 1 : LAND_TERRAIN_TOP + 1;
|
||||
}
|
||||
|
||||
private static IntBinaryOperator floorFirstFreeY() {
|
||||
return (x, z) -> isOcean(x, z) ? OCEAN_FLOOR_TOP + 1 : LAND_TERRAIN_TOP + 1;
|
||||
}
|
||||
|
||||
private static boolean isOcean(int x, int z) {
|
||||
return Math.floorMod(x, 16) == OCEAN_X && Math.floorMod(z, 16) == OCEAN_Z;
|
||||
}
|
||||
|
||||
private static ProtoChunk terrainChunk(ChunkPos pos) {
|
||||
ProtoChunk chunk = new ProtoChunk(
|
||||
pos, UpgradeData.EMPTY, LevelHeightAccessor.create(MIN_Y, HEIGHT),
|
||||
containerFactory(), null);
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
boolean ocean = isOcean(pos.getMinBlockX() + x, pos.getMinBlockZ() + z);
|
||||
int stoneTop = ocean ? OCEAN_FLOOR_TOP : LAND_TERRAIN_TOP;
|
||||
for (int y = stoneTop - TERRAIN_SLAB_DEPTH; y <= stoneTop; y++) {
|
||||
write(chunk, x, y, z, Blocks.STONE.defaultBlockState());
|
||||
}
|
||||
if (ocean) {
|
||||
for (int y = OCEAN_FLOOR_TOP + 1; y <= OCEAN_FLUID_TOP; y++) {
|
||||
write(chunk, x, y, z, Blocks.WATER.defaultBlockState());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (int y = LAND_TERRAIN_TOP + 1; y <= LAND_TERRAIN_TOP + 6; y++) {
|
||||
write(chunk, x, y, z, Blocks.SPRUCE_LOG.defaultBlockState());
|
||||
}
|
||||
for (int y = LAND_TERRAIN_TOP + 7; y <= LAND_CANOPY_TOP; y++) {
|
||||
write(chunk, x, y, z, Blocks.SPRUCE_LEAVES.defaultBlockState());
|
||||
}
|
||||
}
|
||||
}
|
||||
return chunk;
|
||||
}
|
||||
|
||||
private static void write(ProtoChunk chunk, int x, int y, int z, BlockState state) {
|
||||
LevelChunkSection section = chunk.getSection(chunk.getSectionIndex(y));
|
||||
section.setBlockState(x, y & 15, z, state, false);
|
||||
}
|
||||
|
||||
private static PalettedContainerFactory containerFactory() {
|
||||
Strategy<BlockState> blockStates = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
|
||||
BlockState air = Blocks.AIR.defaultBlockState();
|
||||
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
|
||||
Holder<Biome> defaultBiome = Holder.direct((Biome) null);
|
||||
biomeIds.add(defaultBiome);
|
||||
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
|
||||
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(
|
||||
name -> defaultBiome, holder -> "iris-test-biome");
|
||||
return new PalettedContainerFactory(
|
||||
blockStates,
|
||||
air,
|
||||
PalettedContainer.codecRW(BlockState.CODEC, blockStates, air),
|
||||
biomes,
|
||||
defaultBiome,
|
||||
PalettedContainer.codecRO(biomeCodec, biomes, defaultBiome),
|
||||
PalettedContainer.codecRW(biomeCodec, biomes, defaultBiome));
|
||||
}
|
||||
|
||||
private static WorldGenLevel world(Map<Long, ChunkAccess> chunks) {
|
||||
InvocationHandler handler = (proxy, method, arguments) -> {
|
||||
String methodName = method.getName();
|
||||
if (methodName.equals("hasChunk")) {
|
||||
return chunks.containsKey(ChunkPos.pack((int) arguments[0], (int) arguments[1]));
|
||||
}
|
||||
if (methodName.equals("getChunk") && arguments.length == 2) {
|
||||
ChunkAccess chunk = chunks.get(ChunkPos.pack((int) arguments[0], (int) arguments[1]));
|
||||
if (chunk == null) {
|
||||
throw new AssertionError("Heightmap priming requested an unavailable chunk "
|
||||
+ arguments[0] + "," + arguments[1]);
|
||||
}
|
||||
return chunk;
|
||||
}
|
||||
if (methodName.equals("getHeight") && arguments.length == 3) {
|
||||
Heightmap.Types type = (Heightmap.Types) arguments[0];
|
||||
int x = (int) arguments[1];
|
||||
int z = (int) arguments[2];
|
||||
ChunkAccess chunk = chunks.get(ChunkPos.pack(x >> 4, z >> 4));
|
||||
if (chunk == null) {
|
||||
throw new AssertionError("Heightmap read requested an unavailable chunk at "
|
||||
+ x + "," + z);
|
||||
}
|
||||
return chunk.getHeight(type, x & 15, z & 15) + 1;
|
||||
}
|
||||
if (methodName.equals("getBlockState")) {
|
||||
BlockPos position = (BlockPos) arguments[0];
|
||||
ChunkAccess chunk = chunks.get(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4));
|
||||
if (chunk == null) {
|
||||
throw new AssertionError("Block read requested an unavailable chunk at " + position);
|
||||
}
|
||||
return chunk.getBlockState(position);
|
||||
}
|
||||
if (methodName.equals("hashCode")) {
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
if (methodName.equals("equals")) {
|
||||
return proxy == arguments[0];
|
||||
}
|
||||
if (methodName.equals("toString")) {
|
||||
return "worldgen-heightmap-test-world";
|
||||
}
|
||||
throw new UnsupportedOperationException(method.toString());
|
||||
};
|
||||
return (WorldGenLevel) Proxy.newProxyInstance(
|
||||
WorldGenLevel.class.getClassLoader(), new Class<?>[]{WorldGenLevel.class}, handler);
|
||||
}
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
/*
|
||||
* 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.structure;
|
||||
|
||||
import art.arcane.iris.structure.authoring.StructureCapability;
|
||||
import art.arcane.iris.structure.authoring.StructureLoss;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
|
||||
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class VillageImporterListPoolElementRuntimeContractTest {
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraft() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pillagerOutpostTowerCompositeProducesOnePhysicalTowerChoice() throws Exception {
|
||||
StructurePoolElement tower = StructurePoolElement.legacy("minecraft:pillager_outpost/watchtower")
|
||||
.apply(StructureTemplatePool.Projection.RIGID);
|
||||
StructurePoolElement overgrown = StructurePoolElement.legacy("minecraft:pillager_outpost/watchtower_overgrown")
|
||||
.apply(StructureTemplatePool.Projection.RIGID);
|
||||
ListPoolElement towers = new ListPoolElement(
|
||||
List.of(tower, overgrown),
|
||||
StructureTemplatePool.Projection.RIGID
|
||||
);
|
||||
|
||||
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(towers);
|
||||
Map<String, Object> entry = VillageImporter.piecePoolEntry(
|
||||
"qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower",
|
||||
1
|
||||
);
|
||||
StructureLoss loss = VillageImporter.listElementFallbackLoss(
|
||||
resolution,
|
||||
"minecraft:pillager_outpost/towers"
|
||||
);
|
||||
Map<String, Object> basePlatePiece = VillageImporter.pieceJson(
|
||||
"qa/pillager_outpost/piece/minecraft/pillager_outpost/base_plate",
|
||||
List.of(Map.of("name", "minecraft:bottom")),
|
||||
0
|
||||
);
|
||||
Map<String, Object> watchtowerPiece = VillageImporter.pieceJson(
|
||||
"qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower",
|
||||
List.of(),
|
||||
1155
|
||||
);
|
||||
VillageImporter.PoolMemberNormalization connectorScaffold = VillageImporter.normalizePoolMember(
|
||||
"minecraft:pillager_outpost/features",
|
||||
"qa/pillager_outpost/pool/minecraft/pillager_outpost/features",
|
||||
false,
|
||||
2,
|
||||
"minecraft:empty",
|
||||
"minecraft:pillager_outpost/feature_plate",
|
||||
"qa/pillager_outpost/piece/minecraft/pillager_outpost/feature_plate",
|
||||
1,
|
||||
0,
|
||||
List.of(Map.of("name", "minecraft:bottom")));
|
||||
|
||||
assertSame(tower, resolution.physicalElement());
|
||||
assertEquals("minecraft:pillager_outpost/watchtower", resolution.templateLocation());
|
||||
assertEquals(1, resolution.omittedElements());
|
||||
assertEquals("qa/pillager_outpost/piece/minecraft/pillager_outpost/watchtower", entry.get("piece"));
|
||||
assertEquals(1, entry.get("weight"));
|
||||
assertEquals(StructureCapability.LIST_ELEMENTS, loss.capability());
|
||||
assertEquals("list_pool_overlays_not_imported", loss.code());
|
||||
assertEquals(false, basePlatePiece.get("collidable"));
|
||||
assertFalse(watchtowerPiece.containsKey("collidable"));
|
||||
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, connectorScaffold.disposition());
|
||||
assertTrue(connectorScaffold.losses().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void actualWaystoneLikeAllAirMemberNormalizesToEmpty() throws Exception {
|
||||
StructurePoolElement waystone = StructurePoolElement
|
||||
.legacy("dungeons_and_taverns:waystones/waystone_scaffold")
|
||||
.apply(StructureTemplatePool.Projection.RIGID);
|
||||
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(waystone);
|
||||
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
|
||||
"dungeons_and_taverns:waystones",
|
||||
"qa/pool/dungeons_and_taverns/waystones",
|
||||
false,
|
||||
1,
|
||||
"dungeons_and_taverns:waystones",
|
||||
resolution.templateLocation(),
|
||||
"qa/piece/dungeons_and_taverns/waystones/waystone_scaffold",
|
||||
2,
|
||||
0,
|
||||
List.of());
|
||||
|
||||
assertSame(waystone, resolution.physicalElement());
|
||||
assertEquals("dungeons_and_taverns:waystones/waystone_scaffold", resolution.templateLocation());
|
||||
assertEquals(VillageImporter.PoolMemberDisposition.EMPTY, normalization.disposition());
|
||||
assertEquals(true, normalization.poolEntry().get("empty"));
|
||||
assertEquals(2, normalization.poolEntry().get("weight"));
|
||||
assertEquals("connectorless_all_air_member_normalized_empty",
|
||||
normalization.losses().getFirst().code());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void actualNonAirOrphanMemberIsOmitted() throws Exception {
|
||||
StructurePoolElement orphan = StructurePoolElement
|
||||
.legacy("test:orphan_house")
|
||||
.apply(StructureTemplatePool.Projection.RIGID);
|
||||
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(orphan);
|
||||
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
|
||||
"test:orphan_pool",
|
||||
"qa/pool/test/orphan_pool",
|
||||
false,
|
||||
1,
|
||||
null,
|
||||
resolution.templateLocation(),
|
||||
"qa/piece/test/orphan_house",
|
||||
4,
|
||||
37,
|
||||
List.of());
|
||||
|
||||
assertSame(orphan, resolution.physicalElement());
|
||||
assertEquals("test:orphan_house", resolution.templateLocation());
|
||||
assertEquals(VillageImporter.PoolMemberDisposition.OMITTED, normalization.disposition());
|
||||
assertTrue(normalization.poolEntry().isEmpty());
|
||||
assertEquals("connectorless_non_air_member_omitted",
|
||||
normalization.losses().getFirst().code());
|
||||
assertTrue(normalization.losses().getFirst().detail().contains("37 non-air block(s)"));
|
||||
assertTrue(normalization.losses().getFirst().detail().contains("no source fallback"));
|
||||
assertTrue(normalization.losses().getFirst().detail().contains("selection weights"));
|
||||
assertTrue(normalization.losses().getFirst().detail().contains("RNG consumption"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void actualConnectorlessStartMemberRemainsPhysical() throws Exception {
|
||||
StructurePoolElement start = StructurePoolElement
|
||||
.legacy("test:air_start")
|
||||
.apply(StructureTemplatePool.Projection.RIGID);
|
||||
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(start);
|
||||
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
|
||||
"test:start",
|
||||
"qa/pool/test/start",
|
||||
true,
|
||||
2,
|
||||
"test:fallback",
|
||||
resolution.templateLocation(),
|
||||
"qa/piece/test/air_start",
|
||||
6,
|
||||
0,
|
||||
List.of());
|
||||
|
||||
assertSame(start, resolution.physicalElement());
|
||||
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, normalization.disposition());
|
||||
assertEquals("qa/piece/test/air_start", normalization.poolEntry().get("piece"));
|
||||
assertTrue(normalization.losses().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void actualMixedPoolAllAirMemberIsOmittedWithoutBecomingEmpty() throws Exception {
|
||||
StructurePoolElement inert = StructurePoolElement
|
||||
.legacy("test:mixed_air_inert")
|
||||
.apply(StructureTemplatePool.Projection.RIGID);
|
||||
VillageImporter.PoolElementResolution resolution = VillageImporter.resolvePoolElement(inert);
|
||||
VillageImporter.PoolMemberNormalization normalization = VillageImporter.normalizePoolMember(
|
||||
"test:mixed",
|
||||
"qa/pool/test/mixed",
|
||||
false,
|
||||
4,
|
||||
"test:mixed",
|
||||
resolution.templateLocation(),
|
||||
"qa/piece/test/mixed_air_inert",
|
||||
9,
|
||||
0,
|
||||
List.of());
|
||||
|
||||
assertSame(inert, resolution.physicalElement());
|
||||
assertEquals(VillageImporter.PoolMemberDisposition.OMITTED, normalization.disposition());
|
||||
assertTrue(normalization.poolEntry().isEmpty());
|
||||
assertEquals("connectorless_all_air_mixed_member_omitted",
|
||||
normalization.losses().getFirst().code());
|
||||
assertTrue(normalization.losses().getFirst().detail().contains("selection weights"));
|
||||
assertTrue(normalization.losses().getFirst().detail().contains("RNG consumption"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void actualMixedDistinctFallbackRetainsAllConnectorlessMembers() throws Exception {
|
||||
StructurePoolElement allAirElement = StructurePoolElement
|
||||
.legacy("test:mixed_all_air_primary")
|
||||
.apply(StructureTemplatePool.Projection.RIGID);
|
||||
StructurePoolElement nonAirElement = StructurePoolElement
|
||||
.legacy("test:mixed_non_air_primary")
|
||||
.apply(StructureTemplatePool.Projection.RIGID);
|
||||
VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(allAirElement);
|
||||
VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(nonAirElement);
|
||||
VillageImporter.PoolMemberNormalization allAir = VillageImporter.normalizePoolMember(
|
||||
"test:mixed",
|
||||
"qa/pool/test/mixed",
|
||||
false,
|
||||
4,
|
||||
"test:fallback",
|
||||
allAirResolution.templateLocation(),
|
||||
"qa/piece/test/mixed_all_air_primary",
|
||||
9,
|
||||
0,
|
||||
List.of());
|
||||
VillageImporter.PoolMemberNormalization nonAir = VillageImporter.normalizePoolMember(
|
||||
"test:mixed",
|
||||
"qa/pool/test/mixed",
|
||||
false,
|
||||
4,
|
||||
"test:fallback",
|
||||
nonAirResolution.templateLocation(),
|
||||
"qa/piece/test/mixed_non_air_primary",
|
||||
12,
|
||||
41,
|
||||
List.of());
|
||||
|
||||
assertSame(allAirElement, allAirResolution.physicalElement());
|
||||
assertSame(nonAirElement, nonAirResolution.physicalElement());
|
||||
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, allAir.disposition());
|
||||
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, nonAir.disposition());
|
||||
assertEquals("qa/piece/test/mixed_all_air_primary", allAir.poolEntry().get("piece"));
|
||||
assertEquals("qa/piece/test/mixed_non_air_primary", nonAir.poolEntry().get("piece"));
|
||||
assertTrue(allAir.losses().isEmpty());
|
||||
assertTrue(nonAir.losses().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void actualSingletonDistinctFallbackRetainsAllAirAndNonAirMembers() throws Exception {
|
||||
StructurePoolElement allAirElement = StructurePoolElement
|
||||
.legacy("test:all_air_primary")
|
||||
.apply(StructureTemplatePool.Projection.RIGID);
|
||||
StructurePoolElement nonAirElement = StructurePoolElement
|
||||
.legacy("test:non_air_primary")
|
||||
.apply(StructureTemplatePool.Projection.RIGID);
|
||||
VillageImporter.PoolElementResolution allAirResolution = VillageImporter.resolvePoolElement(allAirElement);
|
||||
VillageImporter.PoolElementResolution nonAirResolution = VillageImporter.resolvePoolElement(nonAirElement);
|
||||
VillageImporter.PoolMemberNormalization allAir = VillageImporter.normalizePoolMember(
|
||||
"test:primary",
|
||||
"qa/pool/test/primary",
|
||||
false,
|
||||
1,
|
||||
"test:fallback",
|
||||
allAirResolution.templateLocation(),
|
||||
"qa/piece/test/all_air_primary",
|
||||
5,
|
||||
0,
|
||||
List.of());
|
||||
VillageImporter.PoolMemberNormalization nonAir = VillageImporter.normalizePoolMember(
|
||||
"test:primary",
|
||||
"qa/pool/test/primary",
|
||||
false,
|
||||
1,
|
||||
"test:fallback",
|
||||
nonAirResolution.templateLocation(),
|
||||
"qa/piece/test/non_air_primary",
|
||||
7,
|
||||
29,
|
||||
List.of());
|
||||
|
||||
assertSame(allAirElement, allAirResolution.physicalElement());
|
||||
assertSame(nonAirElement, nonAirResolution.physicalElement());
|
||||
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, allAir.disposition());
|
||||
assertEquals(VillageImporter.PoolMemberDisposition.PHYSICAL, nonAir.disposition());
|
||||
assertEquals("qa/piece/test/all_air_primary", allAir.poolEntry().get("piece"));
|
||||
assertEquals("qa/piece/test/non_air_primary", nonAir.poolEntry().get("piece"));
|
||||
assertTrue(allAir.losses().isEmpty());
|
||||
assertTrue(nonAir.losses().isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
String apiVersion = providers.gradleProperty('apiVersion').get()
|
||||
def mainClass = 'art.arcane.iris.Iris'
|
||||
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
|
||||
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
||||
.orElse('com.github.VolmitSoftware:VolmLib:master-SNAPSHOT')
|
||||
.get()
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(':core'))
|
||||
compileOnly(libs.lombok)
|
||||
annotationProcessor(libs.lombok)
|
||||
compileOnly(volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
compileOnly(libs.paper.api)
|
||||
// LogFilterSVC (constructed in Iris.enable's explicit service list) implements the
|
||||
// log4j-core Filter interface, so its constructor reference needs the type resolvable.
|
||||
compileOnly(libs.log4j.core)
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.paper.api)
|
||||
testImplementation(libs.bstats)
|
||||
testImplementation(project(':core'))
|
||||
testImplementation(testFixtures(project(':core')))
|
||||
testImplementation(volmLibCoordinate) {
|
||||
transitive = false
|
||||
}
|
||||
compileOnly(libs.placeholderApi)
|
||||
testImplementation(libs.placeholderApi) {
|
||||
transitive = false
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('processResources').configure {
|
||||
def pluginProperties = [
|
||||
name : rootProject.name,
|
||||
version : rootProject.version,
|
||||
apiVersion: apiVersion,
|
||||
main : mainClass,
|
||||
bootstrapper: bootstrapperClass,
|
||||
]
|
||||
inputs.properties(pluginProperties)
|
||||
filesMatching(['**/paper-plugin.yml', '**/plugin.yml']) {
|
||||
expand(pluginProperties)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('compileJava', JavaCompile).configure {
|
||||
// lombok.config decides how equals/hashCode/toString are generated (fields, never getters), so editing it
|
||||
// changes this module's bytecode. Gradle does not know that on its own and would serve a stale up-to-date
|
||||
// build until something else in the module changed.
|
||||
inputs.file(rootProject.file('lombok.config'))
|
||||
.withPropertyName('lombokConfig')
|
||||
.withPathSensitivity(PathSensitivity.NONE)
|
||||
}
|
||||
|
||||
tasks.named('jar', Jar).configure {
|
||||
archiveBaseName.set('iris-bukkit-plugin')
|
||||
}
|
||||
|
||||
tasks.named('test').configure {
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,196 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.world.lifecycle.BukkitStartupPaths;
|
||||
import art.arcane.iris.world.lifecycle.BukkitWorldConfiguration;
|
||||
import art.arcane.iris.world.lifecycle.BukkitWorldConfiguration.IrisWorldStorageEntry;
|
||||
import art.arcane.iris.world.lifecycle.HuskWorldQuarantine;
|
||||
import art.arcane.iris.world.lifecycle.MissingWorldStorageLog;
|
||||
import art.arcane.iris.world.lifecycle.WorldReplacementBootstrap;
|
||||
import art.arcane.iris.world.lifecycle.WorldReplacementBootstrapMarker;
|
||||
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner;
|
||||
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
|
||||
import art.arcane.iris.platform.bootstrap.SlimJar;
|
||||
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
|
||||
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
|
||||
import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
|
||||
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEventType;
|
||||
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public final class IrisBootstrap implements PluginBootstrap {
|
||||
@Override
|
||||
public void bootstrap(BootstrapContext context) {
|
||||
WorldReplacementBootstrapMarker.markBootstrapped();
|
||||
try {
|
||||
loadRuntimeLibraries(context);
|
||||
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolveCurrent();
|
||||
reconcilePendingWorldReplacements(context, startupPaths);
|
||||
quarantineWorthlessHusks(startupPaths, message -> context.getLogger().warn(message));
|
||||
requireUsableWorldStorage(startupPaths);
|
||||
ProvisionResult provisioned = provision(context, startupPaths);
|
||||
Path datapackRoot = provisioned.datapackRoot();
|
||||
context.getLogger().info("Iris startup datapack is {} at {}", provisioned.status(), datapackRoot);
|
||||
} catch (Throwable failure) {
|
||||
armStartupFailure(context, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadRuntimeLibraries(BootstrapContext context) {
|
||||
SlimJar.loadBootstrap(
|
||||
context.getDataDirectory().resolve("cache").resolve("libraries"),
|
||||
new SlimJar.BootstrapLogger() {
|
||||
@Override
|
||||
public void info(String message) {
|
||||
context.getLogger().info(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(String message) {
|
||||
context.getLogger().error(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(String message) {
|
||||
context.getLogger().debug(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void reconcilePendingWorldReplacements(
|
||||
BootstrapContext context,
|
||||
BukkitStartupPaths startupPaths
|
||||
) {
|
||||
try {
|
||||
WorldReplacementBootstrap.ReconcileResult result = WorldReplacementBootstrap.reconcile(
|
||||
context.getDataDirectory(),
|
||||
startupPaths.levelRoot(),
|
||||
startupPaths.bukkitConfiguration(),
|
||||
message -> context.getLogger().info(message)
|
||||
);
|
||||
if (result.transactions() > 0) {
|
||||
context.getLogger().info(
|
||||
"Reconciled {} pending Iris world replacement(s): {} published, {} rolled back, {} retained",
|
||||
result.transactions(),
|
||||
result.published(),
|
||||
result.rolledBack(),
|
||||
result.retained()
|
||||
);
|
||||
}
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException(
|
||||
"Unable to reconcile pending Iris world replacements before registry bootstrap",
|
||||
failure
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a hot-deleted world's husk out of {@code <levelRoot>/dimensions/iris} before anything enumerates
|
||||
* it.
|
||||
* <p>
|
||||
* Deleting a loaded world's folder and letting the server save the level back leaves a directory holding
|
||||
* only the server's own {@code data/} skeleton. Left there it stops the boot whichever way it is
|
||||
* classified: as unusable storage it trips the guard below, and as an empty folder it is excluded from
|
||||
* the dimension registry, which trips Paper's interactive world-migration gate - a prompt that consumes
|
||||
* no console input and hangs startup forever. The folder is moved rather than deleted, so it stays
|
||||
* recoverable; {@link HuskWorldQuarantine} refuses anything that is not provably worthless.
|
||||
*/
|
||||
static void quarantineWorthlessHusks(BukkitStartupPaths startupPaths, Consumer<String> warn) {
|
||||
HuskWorldQuarantine.quarantineWorthlessHusks(startupPaths.levelRoot(), warn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops startup when a world the server is about to load carries Iris storage Iris cannot generate from.
|
||||
* <p>
|
||||
* The server enumerates levels from {@code <levelRoot>/dimensions/<namespace>/<key>} on disk, so a world
|
||||
* whose folder is present is going to be loaded whatever Iris does; and CraftBukkit swallows whatever
|
||||
* {@code getDefaultWorldGenerator} throws and falls back to the vanilla generator, which writes vanilla
|
||||
* terrain straight into that world's region files. A pack snapshot that has gone missing therefore has
|
||||
* to be found here, before any level is created, exactly as a corrupt {@code dimensions/<id>.json} is.
|
||||
* <p>
|
||||
* A world with no folder at all is not a startup failure: nothing enumerates it, nothing loads it, and
|
||||
* its bukkit.yml and Multiverse entries are what make restoring the folder a complete recovery. Neither
|
||||
* is a folder that holds no pack snapshot and no world data - {@link #quarantineWorthlessHusks} has
|
||||
* already moved those out of the dimensions tree, and one that survives it (a Spigot-layout world folder,
|
||||
* or a move that failed) owns nothing a vanilla generator could destroy. Both are only reported.
|
||||
*/
|
||||
static void requireUsableWorldStorage(BukkitStartupPaths startupPaths) throws IOException {
|
||||
Path levelRoot = startupPaths.levelRoot();
|
||||
Path levelName = levelRoot.getFileName();
|
||||
if (levelName == null || levelName.toString().isBlank()) {
|
||||
throw new IOException("Configured level root has no startup level id: " + levelRoot);
|
||||
}
|
||||
List<IrisWorldStorageEntry> entries = BukkitWorldConfiguration.auditIrisWorldStorage(
|
||||
startupPaths.bukkitConfiguration().toFile(),
|
||||
levelName.toString(),
|
||||
levelRoot
|
||||
);
|
||||
StringBuilder unusable = new StringBuilder();
|
||||
for (IrisWorldStorageEntry entry : entries) {
|
||||
switch (entry.state()) {
|
||||
case PRESENT -> {
|
||||
}
|
||||
case MISSING -> MissingWorldStorageLog.warnOnce(
|
||||
entry.configuredWorldName(),
|
||||
entry.storagePath());
|
||||
case EMPTY -> MissingWorldStorageLog.warnEmptyOnce(
|
||||
entry.configuredWorldName(),
|
||||
entry.storagePath());
|
||||
case UNUSABLE -> unusable.append(unusable.isEmpty() ? "" : "; ")
|
||||
.append(entry.configuredWorldName())
|
||||
.append(" (")
|
||||
.append(entry.storagePath())
|
||||
.append(": ")
|
||||
.append(entry.detail() == null ? "storage is unusable" : entry.detail())
|
||||
.append(')');
|
||||
}
|
||||
}
|
||||
if (unusable.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
throw new IllegalStateException("Iris world storage is unusable and the server would generate vanilla"
|
||||
+ " terrain over it: " + unusable
|
||||
+ ". Restore each world folder from a backup, or move it aside; a world with no folder is"
|
||||
+ " reported and skipped, and the server starts.");
|
||||
}
|
||||
|
||||
static void armStartupFailure(BootstrapContext context, Throwable failure) {
|
||||
armStartupFailure(context, failure, LifecycleEvents.DATAPACK_DISCOVERY);
|
||||
}
|
||||
|
||||
static <E extends LifecycleEvent> void armStartupFailure(
|
||||
BootstrapContext context,
|
||||
Throwable failure,
|
||||
LifecycleEventType<? super BootstrapContext, ? extends E, ?> eventType
|
||||
) {
|
||||
context.getLifecycleManager().registerEventHandler(eventType, event -> {
|
||||
throw new IllegalStateException("Iris bootstrap did not establish safe world-generation state.", failure);
|
||||
});
|
||||
try {
|
||||
context.getLogger().error(
|
||||
"Iris bootstrap failed; registry and world startup will be stopped at datapack discovery.",
|
||||
failure
|
||||
);
|
||||
} catch (Throwable loggingFailure) {
|
||||
failure.addSuppressed(loggingFailure);
|
||||
failure.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
private static ProvisionResult provision(BootstrapContext context, BukkitStartupPaths startupPaths) {
|
||||
try {
|
||||
return DefaultPackBootstrapProvisioner.provision(
|
||||
context.getDataDirectory(),
|
||||
message -> context.getLogger().info(message),
|
||||
startupPaths
|
||||
);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Unable to provision the Iris startup datapack", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.platform.bootstrap.SlimJar;
|
||||
import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
|
||||
import io.papermc.paper.plugin.loader.PluginClasspathBuilder;
|
||||
import io.papermc.paper.plugin.loader.PluginLoader;
|
||||
import io.papermc.paper.plugin.loader.library.impl.JarLibrary;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public final class IrisPluginLoader implements PluginLoader {
|
||||
@Override
|
||||
public void classloader(PluginClasspathBuilder classpathBuilder) {
|
||||
PluginProviderContext context = classpathBuilder.getContext();
|
||||
Path libraryRoot = context.getDataDirectory().resolve("cache").resolve("libraries");
|
||||
SlimJar.loadBootstrap(libraryRoot, new SlimJar.BootstrapLogger() {
|
||||
@Override
|
||||
public void info(String message) {
|
||||
context.getLogger().info(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(String message) {
|
||||
context.getLogger().error(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(String message) {
|
||||
context.getLogger().debug(message);
|
||||
}
|
||||
});
|
||||
for (Path library : relocatedLibraries(libraryRoot)) {
|
||||
classpathBuilder.addLibrary(new JarLibrary(library));
|
||||
}
|
||||
}
|
||||
|
||||
static List<Path> relocatedLibraries(Path libraryRoot) {
|
||||
try (Stream<Path> paths = Files.walk(libraryRoot)) {
|
||||
List<Path> libraries = paths
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(path -> path.getFileName().toString().endsWith(".jar"))
|
||||
.filter(IrisPluginLoader::isRelocatedLibrary)
|
||||
.sorted()
|
||||
.toList();
|
||||
if (libraries.isEmpty()) {
|
||||
throw new IllegalStateException("Iris runtime library provisioning produced no relocated libraries.");
|
||||
}
|
||||
return libraries;
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Unable to enumerate provisioned Iris runtime libraries.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isRelocatedLibrary(Path path) {
|
||||
int count = path.getNameCount();
|
||||
for (int i = 0; i < count - 2; i++) {
|
||||
if ("relocated".equals(path.getName(i).toString())
|
||||
&& "Iris".equals(path.getName(i + 1).toString())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package art.arcane.iris.api.pregen;
|
||||
|
||||
public enum IrisPregenPhase {
|
||||
STARTED,
|
||||
TICK,
|
||||
PAUSED,
|
||||
RESUMED,
|
||||
SAVING,
|
||||
COMPLETED,
|
||||
CANCELLED
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package art.arcane.iris.api.pregen;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record IrisPregenProgress(
|
||||
String worldName,
|
||||
String worldIdentity,
|
||||
double percent,
|
||||
long generatedChunks,
|
||||
long totalChunks,
|
||||
long remainingChunks,
|
||||
long failedChunks,
|
||||
double chunksPerSecond,
|
||||
long etaMillis,
|
||||
long elapsedMillis,
|
||||
String method,
|
||||
boolean paused) {
|
||||
public IrisPregenProgress {
|
||||
Objects.requireNonNull(worldIdentity, "worldIdentity");
|
||||
worldName = worldName == null ? worldIdentity : worldName;
|
||||
method = method == null ? "" : method;
|
||||
percent = Double.isFinite(percent) ? Math.clamp(percent, 0D, 100D) : 0D;
|
||||
generatedChunks = Math.max(0L, generatedChunks);
|
||||
totalChunks = Math.max(0L, totalChunks);
|
||||
remainingChunks = Math.max(0L, remainingChunks);
|
||||
failedChunks = Math.max(0L, failedChunks);
|
||||
chunksPerSecond = Double.isFinite(chunksPerSecond) ? Math.max(0D, chunksPerSecond) : 0D;
|
||||
etaMillis = Math.max(0L, etaMillis);
|
||||
elapsedMillis = Math.max(0L, elapsedMillis);
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package art.arcane.iris.api.pregen;
|
||||
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class IrisPregenerationEvent extends Event {
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
private final IrisPregenPhase phase;
|
||||
private final IrisPregenProgress progress;
|
||||
|
||||
public IrisPregenerationEvent(IrisPregenPhase phase, IrisPregenProgress progress) {
|
||||
this.phase = Objects.requireNonNull(phase, "phase");
|
||||
this.progress = Objects.requireNonNull(progress, "progress");
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
public IrisPregenPhase getPhase() {
|
||||
return phase;
|
||||
}
|
||||
|
||||
public IrisPregenProgress getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record IrisBiomeInfo(
|
||||
String key,
|
||||
String name,
|
||||
String regionKey,
|
||||
String regionName,
|
||||
String derivativeKey,
|
||||
String vanillaDerivativeKey,
|
||||
String type,
|
||||
List<IrisCustomBiomeInfo> customDerivatives) {
|
||||
public IrisBiomeInfo {
|
||||
key = key == null ? "" : key;
|
||||
name = name == null ? "" : name;
|
||||
regionKey = regionKey == null ? "" : regionKey;
|
||||
regionName = regionName == null ? "" : regionName;
|
||||
derivativeKey = derivativeKey == null ? "" : derivativeKey;
|
||||
vanillaDerivativeKey = vanillaDerivativeKey == null ? "" : vanillaDerivativeKey;
|
||||
type = type == null ? "" : type;
|
||||
customDerivatives = List.copyOf(customDerivatives);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
public enum IrisColumnField {
|
||||
SURFACE_HEIGHT,
|
||||
NATURAL_HEIGHT,
|
||||
SURFACE_KIND,
|
||||
BIOME_KEY,
|
||||
RIVER_STATE,
|
||||
RIVER_DISTANCE,
|
||||
RIVER_FLOW,
|
||||
RIVER_WATER_SURFACE_Y
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Objects;
|
||||
|
||||
public record IrisColumnQuery(
|
||||
int minBlockX,
|
||||
int minBlockZ,
|
||||
int maxBlockX,
|
||||
int maxBlockZ,
|
||||
int strideBlocks,
|
||||
EnumSet<IrisColumnField> fields) {
|
||||
public IrisColumnQuery {
|
||||
Objects.requireNonNull(fields, "fields");
|
||||
if (fields.isEmpty()) {
|
||||
throw new IllegalArgumentException("at least one field is required");
|
||||
}
|
||||
if (maxBlockX < minBlockX || maxBlockZ < minBlockZ) {
|
||||
throw new IllegalArgumentException("query bounds are inverted");
|
||||
}
|
||||
if (strideBlocks < 1) {
|
||||
throw new IllegalArgumentException("strideBlocks must be at least 1");
|
||||
}
|
||||
fields = EnumSet.copyOf(fields);
|
||||
}
|
||||
|
||||
public static IrisColumnQuery rect(
|
||||
int minBlockX,
|
||||
int minBlockZ,
|
||||
int maxBlockX,
|
||||
int maxBlockZ,
|
||||
int strideBlocks,
|
||||
EnumSet<IrisColumnField> fields) {
|
||||
return new IrisColumnQuery(minBlockX, minBlockZ, maxBlockX, maxBlockZ, strideBlocks, fields);
|
||||
}
|
||||
|
||||
public long columnCount() {
|
||||
long columnsX = (((long) maxBlockX - (long) minBlockX) / strideBlocks) + 1L;
|
||||
long columnsZ = (((long) maxBlockZ - (long) minBlockZ) / strideBlocks) + 1L;
|
||||
return saturatedProduct(columnsX, columnsZ);
|
||||
}
|
||||
|
||||
public long chunkCount() {
|
||||
long chunksX = ((long) (maxBlockX >> 4) - (long) (minBlockX >> 4)) + 1L;
|
||||
long chunksZ = ((long) (maxBlockZ >> 4) - (long) (minBlockZ >> 4)) + 1L;
|
||||
return saturatedProduct(chunksX, chunksZ);
|
||||
}
|
||||
|
||||
private static long saturatedProduct(long left, long right) {
|
||||
try {
|
||||
return Math.multiplyExact(left, right);
|
||||
} catch (ArithmeticException overflow) {
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public EnumSet<IrisColumnField> fields() {
|
||||
return EnumSet.copyOf(fields);
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record IrisColumnSample(
|
||||
int blockX,
|
||||
int blockZ,
|
||||
int surfaceHeight,
|
||||
int naturalHeight,
|
||||
IrisSurfaceKind surfaceKind,
|
||||
String biomeKey,
|
||||
IrisRiverState riverState,
|
||||
double riverDistance,
|
||||
int riverFlow,
|
||||
int riverWaterSurfaceY
|
||||
) {
|
||||
public static final int UNAVAILABLE_HEIGHT = Integer.MIN_VALUE;
|
||||
public static final double UNAVAILABLE_RIVER_DISTANCE = Double.NaN;
|
||||
public static final int UNAVAILABLE_RIVER_FLOW = -1;
|
||||
|
||||
public IrisColumnSample {
|
||||
Objects.requireNonNull(surfaceKind, "surfaceKind");
|
||||
Objects.requireNonNull(riverState, "riverState");
|
||||
biomeKey = biomeKey == null || biomeKey.isBlank() ? null : biomeKey;
|
||||
if (!Double.isNaN(riverDistance) && (!Double.isFinite(riverDistance) || riverDistance < 0D)) {
|
||||
throw new IllegalArgumentException("riverDistance must be non-negative, finite, or unavailable");
|
||||
}
|
||||
if (riverFlow < UNAVAILABLE_RIVER_FLOW) {
|
||||
throw new IllegalArgumentException("riverFlow must be non-negative or unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSurfaceHeight() {
|
||||
return surfaceHeight != UNAVAILABLE_HEIGHT;
|
||||
}
|
||||
|
||||
public boolean hasNaturalHeight() {
|
||||
return naturalHeight != UNAVAILABLE_HEIGHT;
|
||||
}
|
||||
|
||||
public boolean hasSurfaceKind() {
|
||||
return surfaceKind != IrisSurfaceKind.UNKNOWN;
|
||||
}
|
||||
|
||||
public boolean hasBiomeKey() {
|
||||
return biomeKey != null;
|
||||
}
|
||||
|
||||
public boolean hasRiverState() {
|
||||
return riverState != IrisRiverState.NONE;
|
||||
}
|
||||
|
||||
public boolean hasRiverDistance() {
|
||||
return !Double.isNaN(riverDistance);
|
||||
}
|
||||
|
||||
public boolean hasRiverFlow() {
|
||||
return riverFlow != UNAVAILABLE_RIVER_FLOW;
|
||||
}
|
||||
|
||||
public boolean hasRiverWaterSurfaceY() {
|
||||
return riverWaterSurfaceY != UNAVAILABLE_HEIGHT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface IrisColumnSink {
|
||||
void accept(IrisColumnSample sample);
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
public record IrisCustomBiomeInfo(String id, String registryKey) {
|
||||
public IrisCustomBiomeInfo {
|
||||
id = id == null ? "" : id;
|
||||
registryKey = registryKey == null ? "" : registryKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
public enum IrisRiverState {
|
||||
NONE,
|
||||
WET,
|
||||
DRY
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
public enum IrisSurfaceKind {
|
||||
UNKNOWN,
|
||||
LAND,
|
||||
SHORE,
|
||||
RIVER,
|
||||
RIVER_SHORE,
|
||||
DRY_CHANNEL,
|
||||
OCEAN,
|
||||
VOID
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
import org.bukkit.World;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
public interface IrisTerrainService {
|
||||
boolean isIrisWorld(World world);
|
||||
|
||||
Optional<IrisWorldInfo> worldInfo(World world);
|
||||
|
||||
OptionalInt surfaceHeight(World world, int blockX, int blockZ);
|
||||
|
||||
IrisSurfaceKind surfaceKind(World world, int blockX, int blockZ);
|
||||
|
||||
Optional<String> surfaceBiomeKey(World world, int blockX, int blockZ);
|
||||
|
||||
Optional<String> surfaceBiomeName(World world, int blockX, int blockZ);
|
||||
|
||||
Optional<IrisBiomeInfo> surfaceBiomeInfo(World world, int blockX, int blockZ);
|
||||
|
||||
Optional<String> biomeKey(World world, int blockX, int blockY, int blockZ);
|
||||
|
||||
Optional<String> regionKey(World world, int blockX, int blockZ);
|
||||
|
||||
Optional<String> regionName(World world, int blockX, int blockZ);
|
||||
|
||||
int maxSampleColumns();
|
||||
|
||||
int maxSampleChunks();
|
||||
|
||||
boolean sampleColumns(World world, IrisColumnQuery query, IrisColumnSink sink);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package art.arcane.iris.api.terrain;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record IrisWorldInfo(
|
||||
String dimensionKey,
|
||||
String worldIdentity,
|
||||
long seed,
|
||||
int minHeight,
|
||||
int maxHeight,
|
||||
int fluidHeight,
|
||||
boolean studio) {
|
||||
public IrisWorldInfo {
|
||||
Objects.requireNonNull(dimensionKey, "dimensionKey");
|
||||
Objects.requireNonNull(worldIdentity, "worldIdentity");
|
||||
if (maxHeight <= minHeight) {
|
||||
throw new IllegalArgumentException("maxHeight must exceed minHeight");
|
||||
}
|
||||
}
|
||||
|
||||
public int height() {
|
||||
return maxHeight - minHeight;
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package art.arcane.iris.api.tree;
|
||||
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
|
||||
public interface IrisTreeFellerService {
|
||||
boolean tryFell(BlockBreakEvent event, TreeFellerOptions options);
|
||||
|
||||
boolean isManagedBreak(BlockBreakEvent event);
|
||||
|
||||
boolean isTreeBlock(Block block);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package art.arcane.iris.api.tree;
|
||||
|
||||
public enum TreeFellerAccess {
|
||||
STANDALONE,
|
||||
INTEGRATION_OVERRIDE
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package art.arcane.iris.api.tree;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record TreeFellerOptions(
|
||||
TreeFellerAccess access,
|
||||
int durabilityPreservationChance,
|
||||
TreeFellerRunHooks runHooks
|
||||
) {
|
||||
public TreeFellerOptions {
|
||||
Objects.requireNonNull(access, "access");
|
||||
Objects.requireNonNull(runHooks, "runHooks");
|
||||
if (durabilityPreservationChance < 0 || durabilityPreservationChance > 100) {
|
||||
throw new IllegalArgumentException("durabilityPreservationChance must be between 0 and 100");
|
||||
}
|
||||
}
|
||||
|
||||
public static TreeFellerOptions standalone() {
|
||||
return new TreeFellerOptions(TreeFellerAccess.STANDALONE, 0, TreeFellerRunHooks.NONE);
|
||||
}
|
||||
|
||||
public static TreeFellerOptions integrationOverride(
|
||||
int durabilityPreservationChance,
|
||||
TreeFellerRunHooks runHooks
|
||||
) {
|
||||
return new TreeFellerOptions(
|
||||
TreeFellerAccess.INTEGRATION_OVERRIDE,
|
||||
durabilityPreservationChance,
|
||||
runHooks
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package art.arcane.iris.api.tree;
|
||||
|
||||
public interface TreeFellerRunHooks {
|
||||
TreeFellerRunHooks NONE = new TreeFellerRunHooks() {
|
||||
@Override
|
||||
public void onActivationAccepted() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean reserveLogCost() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commitLogCost() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refundLogCost() {
|
||||
}
|
||||
};
|
||||
|
||||
void onActivationAccepted();
|
||||
|
||||
boolean reserveLogCost();
|
||||
|
||||
void commitLogCost();
|
||||
|
||||
void refundLogCost();
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package art.arcane.iris.api.world;
|
||||
|
||||
import art.arcane.iris.api.terrain.IrisWorldInfo;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public class IrisWorldEngineEvent extends Event {
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
private final World world;
|
||||
private final IrisWorldPhase phase;
|
||||
private final IrisWorldInfo info;
|
||||
|
||||
public IrisWorldEngineEvent(World world, IrisWorldPhase phase, IrisWorldInfo info) {
|
||||
this.world = Objects.requireNonNull(world, "world");
|
||||
this.phase = Objects.requireNonNull(phase, "phase");
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
public World getWorld() {
|
||||
return world;
|
||||
}
|
||||
|
||||
public IrisWorldPhase getPhase() {
|
||||
return phase;
|
||||
}
|
||||
|
||||
public Optional<IrisWorldInfo> getInfo() {
|
||||
return Optional.ofNullable(info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.api.world;
|
||||
|
||||
public enum IrisWorldPhase {
|
||||
ENGINE_READY,
|
||||
ENGINE_HOTLOADED,
|
||||
ENGINE_CLOSING
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports", descriptionKey = "iris.director.commanddatapack.director.download_manage_external_datapack_imports_modrinth")
|
||||
public class CommandDatapack implements DirectorExecutor {
|
||||
@Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", descriptionKey = "iris.director.commanddatapack.director.download_update_every_datapack_listed_pack_dimension_s_datapackimports_install_it_into", aliases = {"pull"})
|
||||
public void ingest(
|
||||
@Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", descriptionKey = "iris.director.commanddatapack.param.restart_server_when_new_datapacks_are_installed_required_new_structures_register_generate", defaultValue = "false")
|
||||
boolean restart
|
||||
) {
|
||||
VolmitSender sender = sender();
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_STARTING_DATAPACK_INGEST));
|
||||
J.a(() -> DatapackIngestService.ingestAll(sender, restart));
|
||||
}
|
||||
|
||||
@Director(description = "List configured datapack imports and their installed versions", descriptionKey = "iris.director.commanddatapack.director.list_configured_datapack_imports_their_installed_versions", aliases = {"ls"})
|
||||
public void list() {
|
||||
VolmitSender sender = sender();
|
||||
KList<String> configured = DatapackIngestService.collectConfiguredImports();
|
||||
List<DatapackIngestService.Entry> installed = DatapackIngestService.installed();
|
||||
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_CONFIGURED_DATAPACK_IMPORTS, MessageArgument.untrusted("value", configured.size())));
|
||||
for (String url : configured) {
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_MESSAGE, MessageArgument.untrusted("url", url)));
|
||||
}
|
||||
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_INSTALLED_DATAPACKS, MessageArgument.untrusted("value", installed.size())));
|
||||
for (DatapackIngestService.Entry entry : installed) {
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_MESSAGE_2, MessageArgument.untrusted("value", entry.id), MessageArgument.untrusted("value2", (entry.versionNumber == null ? "?" : entry.versionNumber))));
|
||||
}
|
||||
|
||||
if (configured.isEmpty()) {
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DATAPACK_ADD_MODRINTH_URLS_DIMENSION_S_DATAPACKIMPORTS_LIST_THEN_RUN_IRIS));
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Remove an installed datapack by id (also delete its URL from datapackImports to keep it gone)", descriptionKey = "iris.director.commanddatapack.director.remove_installed_datapack_by_id_also_delete_its_url_from_datapackimports_keep", aliases = {"rm"})
|
||||
public void remove(
|
||||
@Param(description = "The datapack id (folder name) shown by /iris datapack list", descriptionKey = "iris.director.commanddatapack.param.datapack_id_folder_name_shown_by_iris_datapack_list")
|
||||
String id
|
||||
) {
|
||||
DatapackIngestService.remove(sender(), id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.command.DirectorExecutor;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.volmlib.util.plugin.ComponentMessenger;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
@Director(name = "debug", description = "Iris diagnostic tools", descriptionKey = "iris.director.commanddebug.description")
|
||||
public final class CommandDebug implements DirectorExecutor {
|
||||
@Director(name = "dump", sync = true, description = "Create and optionally upload a diagnostic report", descriptionKey = "iris.director.commandiris.director.debugdump")
|
||||
public void dump(
|
||||
@Param(name = "upload", defaultValue = "true", description = "Upload the report to mclo.gs", descriptionKey = "iris.director.commandiris.param.debugdump_upload") boolean upload,
|
||||
@Param(name = "sender", contextual = true) CommandSender sender
|
||||
) {
|
||||
Iris.instance.debugDump().request(sender, upload);
|
||||
}
|
||||
|
||||
@Director(sync = true, description = "Print version information", descriptionKey = "iris.director.commandiris.director.print_version_information")
|
||||
public void version(@Param(name = "sender", contextual = true) CommandSender sender) {
|
||||
ComponentMessenger.sendMarkup(sender, DirectorMiniMenu.version(
|
||||
"Iris", Iris.instance.getDescription().getVersion(), DirectorMiniMenu.Theme.irisGreen()));
|
||||
}
|
||||
|
||||
@Director(description = "Toggle debug", descriptionKey = "iris.director.commandiris.director.toggle_debug")
|
||||
public void toggle() {
|
||||
boolean enabled = !IrisSettings.get().getGeneral().isDebug();
|
||||
IrisSettings.get().getGeneral().setDebug(enabled);
|
||||
IrisSettings.get().forceSave();
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SET_DEBUG,
|
||||
MessageArgument.untrusted("to", enabled)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
||||
import art.arcane.iris.world.lifecycle.LifecycleOperationCoordinator;
|
||||
import art.arcane.iris.platform.bukkit.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.world.runtime.ChunkClearer;
|
||||
import art.arcane.iris.generation.validation.GoldenHashScanner;
|
||||
import art.arcane.iris.world.runtime.InPlaceChunkRegenerator;
|
||||
import art.arcane.iris.generation.runtime.IrisEngineSVC;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.pack.IrisPackBenchmarking;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.generation.runtime.IrisEngineMantle;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.world.storage.matter.IrisMatterContext;
|
||||
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.localization.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.io.CountingDataInputStream;
|
||||
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.iris.world.storage.region.MCAFile;
|
||||
import art.arcane.iris.world.storage.region.MCAUtil;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.BukkitRuntimeMessages;
|
||||
@Director(name = "Developer", origin = DirectorOrigin.BOTH, description = "Iris World Manager", descriptionKey = "iris.director.commanddeveloper.director.iris_world_manager", aliases = {"dev"})
|
||||
public class CommandDeveloper implements DirectorExecutor {
|
||||
@Director(description = "Get Loaded TectonicPlates Count", descriptionKey = "iris.director.commanddeveloper.director.get_loaded_tectonicplates_count", origin = DirectorOrigin.BOTH, sync = true)
|
||||
public void EngineStatus() {
|
||||
Iris.service(IrisEngineSVC.class)
|
||||
.engineStatus(sender());
|
||||
}
|
||||
|
||||
@Director(description = "Hash generated block output of a fixed area for determinism/identity testing", descriptionKey = "iris.director.commanddeveloper.director.hash_generated_block_output_fixed_area_determinism_identity_testing", origin = DirectorOrigin.BOTH)
|
||||
public void genhash(
|
||||
@Param(description = "The world to hash", descriptionKey = "iris.director.commanddeveloper.param.world_hash", contextual = true, contextualOverride = true)
|
||||
World world,
|
||||
@Param(description = "Radius in chunks around the center", descriptionKey = "iris.director.commanddeveloper.param.radius_chunks_around_center", defaultValue = "4")
|
||||
int radius,
|
||||
@Param(description = "Center chunk X", descriptionKey = "iris.director.commanddeveloper.param.center_chunk_x", defaultValue = "0")
|
||||
int centerX,
|
||||
@Param(description = "Center chunk Z", descriptionKey = "iris.director.commanddeveloper.param.center_chunk_z", defaultValue = "0")
|
||||
int centerZ) {
|
||||
if (world == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_WORLD_IS_NULL));
|
||||
return;
|
||||
}
|
||||
|
||||
VolmitSender sender = sender();
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DEVELOPER_GENHASH_STARTED_CHUNKS, MessageArgument.untrusted("value", ((radius * 2 + 1) * (radius * 2 + 1)))));
|
||||
J.a(() -> runGenhash(sender, world, radius, centerX, centerZ));
|
||||
}
|
||||
|
||||
private void runGenhash(VolmitSender sender, World world, int radius, int centerX, int centerZ) {
|
||||
long startMs = M.ms();
|
||||
int minY = world.getMinHeight();
|
||||
int maxY = world.getMaxHeight();
|
||||
long globalHash = 0L;
|
||||
long solidBlocks = 0L;
|
||||
Map<String, Long> histogram = new TreeMap<>();
|
||||
JsonObject chunkHashes = new JsonObject();
|
||||
|
||||
for (int rx = centerX - radius; rx <= centerX + radius; rx++) {
|
||||
for (int rz = centerZ - radius; rz <= centerZ + radius; rz++) {
|
||||
org.bukkit.ChunkSnapshot snapshot;
|
||||
try {
|
||||
org.bukkit.Chunk loaded = art.arcane.iris.platform.bukkit.BukkitPlatform.chunkAtAsync(world, rx, rz, true).get();
|
||||
snapshot = loaded.getChunkSnapshot(false, false, false);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DEVELOPER_GENHASH_FAILED_AT_CHUNK, MessageArgument.untrusted("rx", rx), MessageArgument.untrusted("rz", rz), MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
||||
return;
|
||||
}
|
||||
long chunkHash = 0L;
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
int worldX = (rx << 4) + x;
|
||||
int worldZ = (rz << 4) + z;
|
||||
for (int y = minY; y < maxY; y++) {
|
||||
org.bukkit.Material material = snapshot.getBlockType(x, y, z);
|
||||
long positionSeed = ((long) worldX * 0x9E3779B97F4A7C15L)
|
||||
^ ((long) y * 0xC2B2AE3D27D4EB4FL)
|
||||
^ ((long) worldZ * 0x165667B19E3779F9L);
|
||||
long blockHash = genHashMix(positionSeed ^ ((long) (material.ordinal() + 1) * 0xD6E8FEB86659FD93L));
|
||||
chunkHash ^= blockHash;
|
||||
if (material != org.bukkit.Material.AIR
|
||||
&& material != org.bukkit.Material.CAVE_AIR
|
||||
&& material != org.bukkit.Material.VOID_AIR) {
|
||||
histogram.merge(material.name(), 1L, Long::sum);
|
||||
solidBlocks++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
globalHash ^= chunkHash;
|
||||
chunkHashes.addProperty(rx + "," + rz, Long.toHexString(chunkHash));
|
||||
}
|
||||
}
|
||||
|
||||
int side = radius * 2 + 1;
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("global", Long.toHexString(globalHash));
|
||||
result.addProperty("chunks", side * side);
|
||||
result.addProperty("solidBlocks", solidBlocks);
|
||||
result.addProperty("minY", minY);
|
||||
result.addProperty("maxY", maxY);
|
||||
JsonObject hist = new JsonObject();
|
||||
for (Map.Entry<String, Long> entry : histogram.entrySet()) {
|
||||
hist.addProperty(entry.getKey(), entry.getValue());
|
||||
}
|
||||
result.add("histogram", hist);
|
||||
result.add("chunkHashes", chunkHashes);
|
||||
|
||||
File out = new File(Iris.instance.getDataFolder(), "genhash.json");
|
||||
try {
|
||||
Files.writeString(out.toPath(), result.toString());
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_DEVELOPER_GENHASH_GLOBAL_CHUNKS_SOLID, MessageArgument.untrusted("value", Long.toHexString(globalHash)), MessageArgument.untrusted("value2", (side * side)), MessageArgument.untrusted("solidBlocks", solidBlocks), MessageArgument.untrusted("value3", Form.duration((long) (M.ms() - startMs), 1))));
|
||||
Iris.info("genhash world=" + world.getName() + " global=" + Long.toHexString(globalHash)
|
||||
+ " chunks=" + (side * side) + " solidBlocks=" + solidBlocks + " -> " + out.getAbsolutePath());
|
||||
}
|
||||
|
||||
private static long genHashMix(long z) {
|
||||
z = (z ^ (z >>> 30)) * 0xBF58476D1CE4E5B9L;
|
||||
z = (z ^ (z >>> 27)) * 0x94D049BB133111EBL;
|
||||
return z ^ (z >>> 31);
|
||||
}
|
||||
|
||||
@Director(description = "Stage a world generation update", descriptionKey = "iris.director.commanddeveloper.director.stage_world_generation_update", name = "update-world", aliases = "^world")
|
||||
public void updateWorld(
|
||||
@Param(description = "The world to update", descriptionKey = "iris.director.commanddeveloper.param.world_update", contextual = true, contextualOverride = true)
|
||||
World world,
|
||||
@Param(description = "The pack to install into the world", descriptionKey = "iris.director.commanddeveloper.param.pack_install_into_world", contextual = true, contextualOverride = true, aliases = "dimension")
|
||||
IrisDimension pack,
|
||||
@Param(description = "Make sure to make a backup & read the warnings first!", descriptionKey = "iris.director.commanddeveloper.param.make_sure_make_backup_read_warnings_first", defaultValue = "false", aliases = "c")
|
||||
boolean confirm
|
||||
) {
|
||||
if (!confirm) {
|
||||
sender().sendMessage(IrisLanguage.text(
|
||||
BukkitRuntimeMessages.COMMAND_DEVELOPER_UPDATE_WORLD_WARNING,
|
||||
MessageArgument.untrusted("world", world.getName()),
|
||||
MessageArgument.untrusted("pack", pack.getLoadKey())
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
File folder = world.getWorldFolder();
|
||||
folder.mkdirs();
|
||||
|
||||
try (LifecycleOperationCoordinator.Lease lease = LifecycleOperationCoordinator.get().acquire(
|
||||
LifecycleOperationCoordinator.Domain.PACK_MUTATION,
|
||||
LifecycleOperationCoordinator.OperationKind.PACK_PUBLISH,
|
||||
pack.getLoadKey()
|
||||
)) {
|
||||
Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder, world.getSeed());
|
||||
} catch (LifecycleOperationCoordinator.BusyException e) {
|
||||
sender().sendMessage(C.YELLOW + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Test", descriptionKey = "iris.director.commanddeveloper.director.test")
|
||||
public void mantle(
|
||||
@Param(name = "plate", description = "Dump the whole tectonic plate instead of a single section", descriptionKey = "iris.director.commanddeveloper.param.dump_whole_tectonic_plate_instead_single_section", defaultValue = "false")
|
||||
boolean plate,
|
||||
@Param(name = "name", description = "The dump file id under plugins/Iris/dump (pv.<id>.*)", descriptionKey = "iris.director.commanddeveloper.param.dump_file_id_under_plugins_iris_dump_pv_id", defaultValue = "21474836474")
|
||||
String name
|
||||
) throws Throwable {
|
||||
Engine activeEngine = engine();
|
||||
if (activeEngine == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_TARGET_IRIS_WORLD_BEFORE_READING_MANTLE_DUMP));
|
||||
return;
|
||||
}
|
||||
File base = Iris.instance.getDataFile("dump", "pv." + name + ".ttp.lz4b.bin");
|
||||
File section = Iris.instance.getDataFile("dump", "pv." + name + ".section.bin");
|
||||
|
||||
try (IrisMatterContext.Scope scope = IrisMatterContext.open(activeEngine.getData())) {
|
||||
if (plate) {
|
||||
try (CountingDataInputStream in = CountingDataInputStream.wrap(new BufferedInputStream(new FileInputStream(base)))) {
|
||||
TectonicPlate.read(1088, in, true, IrisEngineMantle.createRuntimeDataAdapter(activeEngine.getData()), IrisEngineMantle.createRuntimeHooks());
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to inspect the Iris tectonic plate.", e);
|
||||
}
|
||||
} else {
|
||||
Matter.read(section);
|
||||
}
|
||||
}
|
||||
if (!TectonicPlate.hasError()) {
|
||||
Iris.info("Read " + (plate ? base : section).length() + " bytes from " + (plate ? base : section).getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Test", descriptionKey = "iris.director.commanddeveloper.director.test_2")
|
||||
public void packBenchmark(
|
||||
@Param(description = "The pack to bench", descriptionKey = "iris.director.commanddeveloper.param.pack_bench", aliases = {"pack"}, defaultValue = "overworld")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "Radius in regions", descriptionKey = "iris.director.commanddeveloper.param.radius_regions", defaultValue = "2048")
|
||||
int radius,
|
||||
@Param(description = "Open GUI while benchmarking", descriptionKey = "iris.director.commanddeveloper.param.open_gui_while_benchmarking", defaultValue = "false")
|
||||
boolean gui
|
||||
) {
|
||||
new IrisPackBenchmarking(dimension, radius, gui);
|
||||
}
|
||||
|
||||
@Director(description = "Upgrade to another Minecraft version", descriptionKey = "iris.director.commanddeveloper.director.upgrade_another_minecraft_version")
|
||||
public void upgrade(
|
||||
@Param(description = "The version to upgrade to", descriptionKey = "iris.director.commanddeveloper.param.version_upgrade", defaultValue = "latest") DataVersion version) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_UPGRADING, MessageArgument.untrusted("value", version.getVersion())));
|
||||
ServerConfigurator.installDataPacks(version.get(), false);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_DONE_UPGRADING_YOU_CAN_NOW_UPDATE_YOUR_SERVER_VERSION, MessageArgument.untrusted("value", version.getVersion())));
|
||||
}
|
||||
|
||||
@Director(description = "test", descriptionKey = "iris.director.commanddeveloper.director.test_3")
|
||||
public void mca (
|
||||
@Param(description = "The world folder to scan for .mca region files", descriptionKey = "iris.director.commanddeveloper.param.world_folder_scan_mca_region_files") String world) {
|
||||
try {
|
||||
File[] McaFiles = new File(world, "region").listFiles((dir, name) -> name.endsWith(".mca"));
|
||||
for (File mca : McaFiles) {
|
||||
MCAFile MCARegion = MCAUtil.read(mca);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Iris.reportError("Failed to inspect Minecraft region files.", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Director(description = "Delete nearby chunk blocks for regen testing", descriptionKey = "iris.director.commanddeveloper.director.delete_nearby_chunk_blocks_regen_testing", name = "delete-chunk", aliases = {"dc"}, origin = DirectorOrigin.PLAYER)
|
||||
public void deleteChunk(
|
||||
@Param(description = "Radius in chunks around your current chunk", descriptionKey = "iris.director.commanddeveloper.param.radius_chunks_around_your_current_chunk", defaultValue = "0")
|
||||
int radius
|
||||
) {
|
||||
if (radius < 0) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_RADIUS_MUST_BE_0_GREATER));
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = player();
|
||||
VolmitSender commandSender = sender();
|
||||
World world = player.getWorld();
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_THIS_IS_NOT_IRIS_WORLD));
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access == null || access.getEngine() == null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL));
|
||||
return;
|
||||
}
|
||||
|
||||
Engine engine = access.getEngine();
|
||||
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
|
||||
|
||||
// The player position must be read on the thread owning the player; ChunkClearer hops per chunk itself.
|
||||
if (!J.runEntity(player, () -> {
|
||||
int centerX = player.getLocation().getBlockX() >> 4;
|
||||
int centerZ = player.getLocation().getBlockZ() >> 4;
|
||||
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_DELETE_STARTED_CHUNK_S_AROUND_CLEARING_BLOCKS_AIR, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
|
||||
|
||||
new ChunkClearer(world, engine, commandSender, centerX, centerZ, radius).start();
|
||||
})) {
|
||||
Iris.warn("Could not schedule delete-chunk on the thread owning " + player.getName() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Test", descriptionKey = "iris.director.commanddeveloper.director.test_4", aliases = {"ip"})
|
||||
public void network() {
|
||||
try {
|
||||
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
|
||||
for (NetworkInterface ni : Collections.list(networkInterfaces)) {
|
||||
Iris.info("Display Name: %s", ni.getDisplayName());
|
||||
Enumeration<InetAddress> inetAddresses = ni.getInetAddresses();
|
||||
for (InetAddress ia : Collections.list(inetAddresses)) {
|
||||
Iris.info("IP: %s", ia.getHostAddress());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Iris.reportError("Failed to inspect server network interfaces.", e);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Regen ---
|
||||
|
||||
@Director(name = "regen", aliases = {"rg"}, description = "Delete and regenerate nearby chunks in place using Iris generation", descriptionKey = "iris.director.commanddeveloper.director.delete_regenerate_nearby_chunks_place_using_iris_generation", origin = DirectorOrigin.PLAYER)
|
||||
public void regen(
|
||||
@Param(name = "radius", description = "The radius of nearby chunks", descriptionKey = "iris.director.commanddeveloper.param.radius_nearby_chunks", defaultValue = "5")
|
||||
int radius
|
||||
) {
|
||||
if (radius < 0) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_RADIUS_MUST_BE_0_GREATER_2));
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = player();
|
||||
VolmitSender commandSender = sender();
|
||||
World world = player.getWorld();
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_YOU_MUST_BE_IRIS_WORLD_USE_REGEN));
|
||||
return;
|
||||
}
|
||||
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
if (engine == null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_GENERATE_NEARBY_CHUNKS_FIRST));
|
||||
return;
|
||||
}
|
||||
|
||||
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
|
||||
|
||||
// The player position must be read on the thread owning the player; the regenerator hops per chunk itself.
|
||||
if (!J.runEntity(player, () -> {
|
||||
int centerX = player.getLocation().getBlockX() >> 4;
|
||||
int centerZ = player.getLocation().getBlockZ() >> 4;
|
||||
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_REGEN_STARTED_CHUNK_S_AROUND_DELETING_REGENERATING_PLACE, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
|
||||
Iris.info("Regen run start: world=" + world.getName()
|
||||
+ " center=" + centerX + "," + centerZ
|
||||
+ " radius=" + radius
|
||||
+ " chunks=" + chunks);
|
||||
|
||||
new InPlaceChunkRegenerator(world, engine, commandSender, centerX, centerZ, radius).start();
|
||||
})) {
|
||||
Iris.warn("Could not schedule regen on the thread owning " + player.getName() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(name = "goldenhash", aliases = {"gold"}, description = "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Deletes the world's entire mantle - use on disposable test worlds.", descriptionKey = "iris.director.commanddeveloper.director.generate_chunks_into_buffers_no_world_writes_hash_blocks_biomes_captures_golden", origin = DirectorOrigin.BOTH)
|
||||
public void goldenhash(
|
||||
@Param(description = "The world to scan", descriptionKey = "iris.director.commanddeveloper.param.world_scan", contextual = true, contextualOverride = true)
|
||||
World world,
|
||||
@Param(name = "radius", description = "Radius in chunks around the center", descriptionKey = "iris.director.commanddeveloper.param.radius_chunks_around_center_2", defaultValue = "8")
|
||||
int radius,
|
||||
@Param(name = "center-x", description = "Center chunk X", descriptionKey = "iris.director.commanddeveloper.param.center_chunk_x_2", defaultValue = "0")
|
||||
int centerX,
|
||||
@Param(name = "center-z", description = "Center chunk Z", descriptionKey = "iris.director.commanddeveloper.param.center_chunk_z_2", defaultValue = "0")
|
||||
int centerZ,
|
||||
@Param(name = "reset-mantle", description = "Delete the world's entire mantle folder first for full regeneration from scratch", descriptionKey = "iris.director.commanddeveloper.param.delete_mantle_data_scan_area_first_full_regeneration_from_scratch", defaultValue = "true")
|
||||
boolean resetMantle,
|
||||
@Param(name = "threads", description = "Concurrent chunk generations; 1 = strictly serial for order-dependence testing", descriptionKey = "iris.director.commanddeveloper.param.concurrent_chunk_generations_1_strictly_serial_order_dependence_testing", defaultValue = "8")
|
||||
int threads,
|
||||
@Param(name = "deep", description = "Also dump full per-chunk non-air blockstates for offline diffing", descriptionKey = "iris.director.commanddeveloper.param.also_dump_full_per_chunk_non_air_blockstates_offline_diffing", defaultValue = "false")
|
||||
boolean deep
|
||||
) {
|
||||
if (radius < 0) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_RADIUS_MUST_BE_0_GREATER_3));
|
||||
return;
|
||||
}
|
||||
|
||||
if (world == null || !IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_TARGET_MUST_BE_IRIS_WORLD));
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access == null || access.getEngine() == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_2));
|
||||
return;
|
||||
}
|
||||
|
||||
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_GOLDENHASH_STARTED_CHUNK_S_AROUND_BUFFERS_WORLD_UNTOUCHED, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
|
||||
Iris.info("goldenhash start: world=" + world.getName()
|
||||
+ " center=" + centerX + "," + centerZ
|
||||
+ " radius=" + radius
|
||||
+ " chunks=" + chunks);
|
||||
|
||||
new GoldenHashScanner(world, access.getEngine(), sender(), centerX, centerZ, radius, resetMantle, threads, deep).start();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
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.localization.C;
|
||||
|
||||
import java.awt.Desktop;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
@Director(name = "edit", origin = DirectorOrigin.PLAYER, description = "Edit something", descriptionKey = "iris.director.commandedit.director.edit_something")
|
||||
public class CommandEdit implements DirectorExecutor {
|
||||
private boolean noStudio() {
|
||||
if (!sender().isPlayer()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_PLAYERS_ONLY));
|
||||
return true;
|
||||
}
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_NO_STUDIO_WORLD_IS_OPEN));
|
||||
return true;
|
||||
}
|
||||
if (!engine().isStudio()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_YOU_MUST_BE_STUDIO_WORLD));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (GraphicsEnvironment.isHeadless()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANNOT_OPEN_FILES_HEADLESS_ENVIRONMENTS));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Desktop.isDesktopSupported()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_DESKTOP_IS_NOT_SUPPORTED_BY_THIS_ENVIRONMENT));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Director(description = "Edit the biome you specified", descriptionKey = "iris.director.commandedit.director.edit_biome_you_specified", aliases = {"b"}, origin = DirectorOrigin.PLAYER)
|
||||
public void biome(@Param(contextual = false, description = "The biome to edit", descriptionKey = "iris.director.commandedit.param.biome_edit") IrisBiome biome) {
|
||||
if (noStudio()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (biome == null || biome.getLoadFile() == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM));
|
||||
return;
|
||||
}
|
||||
Desktop.getDesktop().open(biome.getLoadFile());
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_OPENING_VSCODE, MessageArgument.untrusted("value", biome.getTypeName()), MessageArgument.untrusted("value2", biome.getLoadFile().getName().split("\\Q.\\E")[0])));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANT_FIND_FILE_REGISTRANT_DOES_NOT_EXIST));
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Edit the region you specified", descriptionKey = "iris.director.commandedit.director.edit_region_you_specified", aliases = {"r"}, origin = DirectorOrigin.PLAYER)
|
||||
public void region(@Param(contextual = false, description = "The region to edit", descriptionKey = "iris.director.commandedit.param.region_edit") IrisRegion region) {
|
||||
if (noStudio()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (region == null || region.getLoadFile() == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM_2));
|
||||
return;
|
||||
}
|
||||
Desktop.getDesktop().open(region.getLoadFile());
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_OPENING_VSCODE_2, MessageArgument.untrusted("value", region.getTypeName()), MessageArgument.untrusted("value2", region.getLoadFile().getName().split("\\Q.\\E")[0])));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANT_FIND_FILE_REGISTRANT_DOES_NOT_EXIST_2));
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Edit the dimension you specified", descriptionKey = "iris.director.commandedit.director.edit_dimension_you_specified", aliases = {"d"}, origin = DirectorOrigin.PLAYER)
|
||||
public void dimension(@Param(contextual = false, description = "The dimension to edit", descriptionKey = "iris.director.commandedit.param.dimension_edit") IrisDimension dimension) {
|
||||
if (noStudio()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (dimension == null || dimension.getLoadFile() == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM_3));
|
||||
return;
|
||||
}
|
||||
Desktop.getDesktop().open(dimension.getLoadFile());
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_OPENING_VSCODE_3, MessageArgument.untrusted("value", dimension.getTypeName()), MessageArgument.untrusted("value2", dimension.getLoadFile().getName().split("\\Q.\\E")[0])));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_EDIT_CANT_FIND_FILE_REGISTRANT_DOES_NOT_EXIST_3));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,863 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.studio.object.ObjectStudioSaveService;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.structure.placement.StructureReachability;
|
||||
import art.arcane.iris.world.history.GenerationSemanticQueries;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.platform.generation.EngineBukkitOps;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import art.arcane.iris.command.specialhandlers.HydrologyTypeHandler;
|
||||
import art.arcane.iris.world.history.GenerationFindCatalog;
|
||||
import art.arcane.iris.command.specialhandlers.LocatableObjectHandler;
|
||||
import art.arcane.iris.command.specialhandlers.ReachableBiomeHandler;
|
||||
import art.arcane.iris.command.specialhandlers.ReachableRegionHandler;
|
||||
import art.arcane.iris.command.specialhandlers.StructureHandler;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.generator.structure.Structure;
|
||||
import org.bukkit.util.StructureSearchResult;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", descriptionKey = "iris.director.commandfind.director.iris_find_commands", aliases = "goto")
|
||||
public class CommandFind implements DirectorExecutor {
|
||||
@Director(description = "Find a biome", descriptionKey = "iris.director.commandfind.director.find_biome")
|
||||
public void biome(
|
||||
@Param(description = "The biome to look for", descriptionKey = "iris.director.commandfind.param.biome_look", customHandler = ReachableBiomeHandler.class)
|
||||
IrisBiome biome,
|
||||
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
|
||||
if (e == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_NOT_IRIS_WORLD));
|
||||
return;
|
||||
}
|
||||
|
||||
EngineBukkitOps.gotoBiome(e, biome, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
|
||||
public void region(
|
||||
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look", customHandler = ReachableRegionHandler.class)
|
||||
IrisRegion region,
|
||||
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_2", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
|
||||
if (e == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_NOT_IRIS_WORLD_2));
|
||||
return;
|
||||
}
|
||||
|
||||
EngineBukkitOps.gotoRegion(e, region, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find a point of interest.", descriptionKey = "iris.director.commandfind.director.find_point_interest")
|
||||
public void poi(
|
||||
@Param(description = "The type of PoI to look for.", descriptionKey = "iris.director.commandfind.param.type_poi_look")
|
||||
String type,
|
||||
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_3", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
if (e == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_NOT_IRIS_WORLD_3));
|
||||
return;
|
||||
}
|
||||
|
||||
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
|
||||
}
|
||||
|
||||
@Director(description = "Find an accepted hydrology feature", sync = true)
|
||||
public void river(
|
||||
@Param(description = "Feature type: surface, waterfall, underground, grotto, mouth, or a deep-fluid id", customHandler = HydrologyTypeHandler.class)
|
||||
String type,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine activeEngine = engine();
|
||||
VolmitSender commandSender = sender();
|
||||
Player target = player();
|
||||
if (activeEngine == null || commandSender == null || target == null) {
|
||||
if (commandSender != null) {
|
||||
commandSender.sendMessage(C.RED + "Run this command from an Iris world.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
|
||||
HydrologyFeatureQuery query;
|
||||
try {
|
||||
query = HydrologyFeatureQuery.parse(type);
|
||||
} catch (IllegalArgumentException error) {
|
||||
commandSender.sendMessage(C.RED + error.getMessage());
|
||||
return;
|
||||
}
|
||||
Location origin = target.getLocation();
|
||||
World originWorld = origin.getWorld();
|
||||
int requestedDistance = runtime == null
|
||||
? 8192
|
||||
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
||||
int maximumDistance = runtime == null
|
||||
? requestedDistance
|
||||
: runtime.maximumFeatureSearchDistance(
|
||||
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
|
||||
commandSender.sendMessage(C.GRAY + "Searching accepted hydrology plans for " + type + "...");
|
||||
J.a(() -> {
|
||||
try {
|
||||
GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver(
|
||||
activeEngine,
|
||||
query,
|
||||
origin.getBlockX(),
|
||||
origin.getBlockZ(),
|
||||
maximumDistance,
|
||||
(int visited) -> sendStructureMessage(target, commandSender,
|
||||
C.GRAY + "Searched " + visited + " hydrology tiles for " + type + "...")
|
||||
).orElse(null);
|
||||
if (feature == null) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "No accepted " + type + " hydrology feature found within "
|
||||
+ maximumDistance + " blocks.");
|
||||
return;
|
||||
}
|
||||
String label = type + " hydrology feature";
|
||||
if (!teleport) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.GREEN + "Found " + label + " at " + feature.x() + ", " + feature.y() + ", " + feature.z() + ".");
|
||||
return;
|
||||
}
|
||||
Location destination = new Location(
|
||||
originWorld, feature.x(), feature.y(), feature.z());
|
||||
prepareStructureTeleport(
|
||||
target, originWorld, commandSender, label, destination, true);
|
||||
} catch (Throwable error) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.RED + "Could not locate " + type + " hydrology: " + error.getClass().getSimpleName());
|
||||
Iris.reportError("Could not locate accepted hydrology feature '" + type + "'.", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Find a structure (a vanilla key like minecraft:village_plains or minecraft:stronghold, or an imported iris structure key)", descriptionKey = "iris.director.commandfind.director.find_structure_vanilla_key_like_minecraft_village_plains_minecraft_stronghold_imported_iris", sync = true)
|
||||
public void structure(
|
||||
@Param(description = "The structure to look for (e.g. minecraft:village_plains, minecraft:stronghold, minecraft_ancient_city)", descriptionKey = "iris.director.commandfind.param.structure_look_e_g_minecraft_village_plains_minecraft_stronghold_minecraft_ancient_city", customHandler = StructureHandler.class)
|
||||
String structure
|
||||
) {
|
||||
VolmitSender commandSender = sender();
|
||||
if (commandSender == null) {
|
||||
Iris.reportError("Structure lookup started without a command sender context.", new IllegalStateException("Missing command sender context"));
|
||||
return;
|
||||
}
|
||||
|
||||
Engine e = engine();
|
||||
|
||||
if (e == null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_NOT_IRIS_WORLD));
|
||||
return;
|
||||
}
|
||||
|
||||
Player target = player();
|
||||
if (target == null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_RUN_THIS_GAME_TELEPORT_STRUCTURE));
|
||||
return;
|
||||
}
|
||||
|
||||
String structureKey = structure == null ? "" : structure.trim();
|
||||
Structure nativeStructure = resolveNativeStructure(structureKey);
|
||||
boolean registered = nativeStructure != null;
|
||||
IrisNativeStructureDecision decision = registered
|
||||
? NativeStructureGenerationPolicy.resolve(e, structureKey, false)
|
||||
: null;
|
||||
boolean nativePlacement = IrisStructureLocator.hasNativePlacement(e, structureKey);
|
||||
boolean locatableNativePlacement = IrisStructureLocator.hasLocatableNativePlacement(e, structureKey);
|
||||
boolean locatableEditablePlacement = IrisStructureLocator.hasLocatableEditablePlacement(e, structureKey);
|
||||
World targetWorld = target.getWorld();
|
||||
boolean nativeGenerationEnabled = targetWorld.canGenerateStructures();
|
||||
boolean requiresReachability = registered && decision.generate()
|
||||
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS
|
||||
&& nativeGenerationEnabled;
|
||||
boolean reachable = !requiresReachability || StructureReachability.isReachable(e, structureKey);
|
||||
StructureLookupRoute route = selectStructureLookupRoute(
|
||||
registered, decision, nativePlacement, locatableNativePlacement,
|
||||
locatableEditablePlacement, nativeGenerationEnabled, reachable);
|
||||
|
||||
if (route != StructureLookupRoute.NATIVE && route != StructureLookupRoute.IRIS
|
||||
&& GenerationFindCatalog.hasRetainedStructurePlacement(e, structureKey)) {
|
||||
locateIrisStructure(e, structureKey, commandSender);
|
||||
return;
|
||||
}
|
||||
|
||||
if (route == StructureLookupRoute.UNKNOWN) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_UNKNOWN_STRUCTURE, MessageArgument.untrusted("structureKey", structureKey)));
|
||||
return;
|
||||
}
|
||||
if (route == StructureLookupRoute.POLICY_DISABLED) {
|
||||
commandSender.sendMessage(C.RED + NativeStructureGenerationPolicy.generationStatusMessage(
|
||||
structureKey, decision.status()));
|
||||
return;
|
||||
}
|
||||
if (route == StructureLookupRoute.NO_ACTIVE_PLACEMENT) {
|
||||
commandSender.sendMessage(C.YELLOW + structureKey
|
||||
+ " has no active placement in this world.");
|
||||
return;
|
||||
}
|
||||
if (route == StructureLookupRoute.WORLD_DISABLED) {
|
||||
commandSender.sendMessage(C.YELLOW + structureKey
|
||||
+ " cannot generate because native structure generation is disabled for this world.");
|
||||
return;
|
||||
}
|
||||
if (route == StructureLookupRoute.UNREACHABLE) {
|
||||
KList<String> miss = StructureReachability.missingBiomeKeys(e, structureKey);
|
||||
commandSender.sendMessage(C.YELLOW + structureKey
|
||||
+ " cannot generate in this world (its required biomes are not produced by this pack"
|
||||
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
|
||||
return;
|
||||
}
|
||||
if (route == StructureLookupRoute.IRIS) {
|
||||
locateIrisStructure(e, structureKey, commandSender);
|
||||
return;
|
||||
}
|
||||
|
||||
Location origin = target.getLocation();
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_LOCATING, MessageArgument.untrusted("structureKey", structureKey)));
|
||||
J.s(() -> {
|
||||
try {
|
||||
StructureSearchResult result = targetWorld.locateNearestStructure(
|
||||
origin, nativeStructure, 100, false);
|
||||
if (result == null || result.getLocation() == null) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "No " + structureKey + " found within range of you.");
|
||||
return;
|
||||
}
|
||||
prepareStructureTeleport(
|
||||
target, targetWorld, commandSender, structureKey, result.getLocation(), false);
|
||||
} catch (Throwable t) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.RED + "Could not locate " + structureKey + ": " + t.getClass().getSimpleName());
|
||||
Iris.reportError("Could not locate structure '" + structureKey + "'.", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Print every structure excluded from /iris goto and its rejection reason to the server console", sync = true)
|
||||
public void unregistered() {
|
||||
VolmitSender commandSender = sender();
|
||||
if (commandSender == null) {
|
||||
Iris.reportError("Structure exclusion report started without a command sender context.",
|
||||
new IllegalStateException("Missing command sender context"));
|
||||
return;
|
||||
}
|
||||
Engine activeEngine = engine();
|
||||
if (activeEngine == null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_NOT_IRIS_WORLD));
|
||||
return;
|
||||
}
|
||||
Player target = player();
|
||||
if (target == null) {
|
||||
commandSender.sendMessage(C.RED + "Run this command from the Iris world to inspect its structures.");
|
||||
return;
|
||||
}
|
||||
|
||||
StructureExclusionReport snapshot;
|
||||
try {
|
||||
snapshot = collectStructureExclusions(
|
||||
activeEngine, target.getWorld().canGenerateStructures());
|
||||
} catch (Throwable error) {
|
||||
commandSender.sendMessage(C.RED + "Could not build the structure exclusion report; see the server console.");
|
||||
Iris.reportError("Could not snapshot /iris goto unregistered report for world '"
|
||||
+ target.getWorld().getName() + "'.", error);
|
||||
return;
|
||||
}
|
||||
|
||||
String worldName = target.getWorld().getName();
|
||||
J.a(() -> {
|
||||
try {
|
||||
StructureExclusionReport report = includeManagedDatapackStructures(
|
||||
snapshot, DatapackIngestService.installed());
|
||||
printStructureExclusionReport(worldName, report);
|
||||
sendStructureMessage(target, commandSender, C.GREEN + "Printed " + report.entries().size()
|
||||
+ " non-generating structure candidate(s) and their reasons to the server console. "
|
||||
+ "This was an eligibility check; no chunks were searched.");
|
||||
} catch (Throwable error) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.RED + "Could not finish the structure exclusion report; see the server console.");
|
||||
Iris.reportError("Could not finish /iris goto unregistered report for world '"
|
||||
+ worldName + "'.", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static StructureExclusionReport collectStructureExclusions(
|
||||
Engine engine, boolean nativeGenerationEnabled) {
|
||||
PlatformStructureHooks structureHooks = IrisPlatforms.get().structureHooks();
|
||||
Map<String, String> registeredKeys = distinctStructureKeys(structureHooks.structureKeys());
|
||||
Set<String> reachableKeys = StructureReachability.reachableKeys(engine);
|
||||
Set<String> possibleBiomeKeys = normalizedStructureKeys(
|
||||
structureHooks.possibleBiomeKeys(engine.getWorld().platformWorld()));
|
||||
List<StructureExclusion> exclusions = new ArrayList<>();
|
||||
int registeredExcluded = 0;
|
||||
|
||||
for (Map.Entry<String, String> entry : registeredKeys.entrySet()) {
|
||||
String normalizedKey = entry.getKey();
|
||||
String key = entry.getValue();
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false);
|
||||
boolean nativePlacement = IrisStructureLocator.hasNativePlacement(engine, key);
|
||||
boolean locatableNativePlacement = IrisStructureLocator.hasLocatableNativePlacement(engine, key);
|
||||
boolean locatableEditablePlacement = IrisStructureLocator.hasLocatableEditablePlacement(engine, key);
|
||||
StructureLookupRoute route = selectStructureLookupRoute(
|
||||
true, decision, nativePlacement, locatableNativePlacement,
|
||||
locatableEditablePlacement, nativeGenerationEnabled,
|
||||
reachableKeys.contains(normalizedKey));
|
||||
if (route == StructureLookupRoute.IRIS || route == StructureLookupRoute.NATIVE) {
|
||||
continue;
|
||||
}
|
||||
List<String> requiredBiomes = needsNativeReachabilityReason(route, decision)
|
||||
? structureHooks.structureBiomeKeys(key)
|
||||
: List.of();
|
||||
exclusions.add(new StructureExclusion(
|
||||
StructureExclusionKind.EXCLUDED, key, null,
|
||||
describeStructureExclusion(
|
||||
route, key, decision.status(), nativePlacement,
|
||||
requiredBiomes, possibleBiomeKeys)));
|
||||
registeredExcluded++;
|
||||
}
|
||||
|
||||
int configuredUnregistered = 0;
|
||||
Set<String> configuredUnregisteredKeys = new LinkedHashSet<>();
|
||||
for (String configuredKey : IrisStructureLocator.placedKeys(engine)) {
|
||||
String normalizedKey = normalizeStructureKey(configuredKey);
|
||||
if (normalizedKey.isEmpty()
|
||||
|| registeredKeys.containsKey(normalizedKey)
|
||||
|| !IrisStructureLocator.hasNativePlacement(engine, configuredKey)
|
||||
|| !configuredUnregisteredKeys.add(normalizedKey)) {
|
||||
continue;
|
||||
}
|
||||
exclusions.add(new StructureExclusion(
|
||||
StructureExclusionKind.UNREGISTERED, configuredKey, null,
|
||||
describeConfiguredUnregisteredNative(
|
||||
IrisStructureLocator.hasLocatableNativePlacement(engine, configuredKey))));
|
||||
configuredUnregistered++;
|
||||
}
|
||||
|
||||
int editableUnplaced = 0;
|
||||
Set<String> unplacedEditableKeys = new LinkedHashSet<>();
|
||||
Set<String> locatableEditableKeys = normalizedStructureKeys(
|
||||
IrisStructureLocator.locatableEditableKeys(engine));
|
||||
for (String editableKey : engine.getData().getStructureLoader().getPossibleKeys()) {
|
||||
String normalizedKey = normalizeStructureKey(editableKey);
|
||||
if (!isUnplacedEditableCandidate(
|
||||
normalizedKey, registeredKeys.keySet(), locatableEditableKeys,
|
||||
IrisStructureLocator.hasNativePlacement(engine, editableKey))
|
||||
|| !unplacedEditableKeys.add(normalizedKey)) {
|
||||
continue;
|
||||
}
|
||||
boolean configuredPlacement = IrisStructureLocator.hasEditablePlacement(engine, editableKey);
|
||||
String reason = configuredPlacement
|
||||
? "editable Iris structure has a configured placement, but no matching placement is active "
|
||||
+ "because its density is not positive or its Y band does not intersect this world"
|
||||
: "editable Iris structure exists in this pack, but no structure placement references it";
|
||||
exclusions.add(new StructureExclusion(
|
||||
StructureExclusionKind.UNPLACED, editableKey, null, reason));
|
||||
editableUnplaced++;
|
||||
}
|
||||
|
||||
Set<String> configuredImportUrls = normalizedImportUrls(
|
||||
DatapackIngestService.configuredImports(engine.getDimension()));
|
||||
return new StructureExclusionReport(
|
||||
List.copyOf(exclusions), Set.copyOf(registeredKeys.keySet()),
|
||||
configuredImportUrls, registeredExcluded, configuredUnregistered,
|
||||
editableUnplaced, 0);
|
||||
}
|
||||
|
||||
static boolean isUnplacedEditableCandidate(
|
||||
String normalizedKey, Set<String> registeredKeys,
|
||||
Set<String> locatableEditableKeys, boolean nativePlacement) {
|
||||
return normalizedKey != null
|
||||
&& !normalizedKey.isEmpty()
|
||||
&& !registeredKeys.contains(normalizedKey)
|
||||
&& !locatableEditableKeys.contains(normalizedKey)
|
||||
&& !nativePlacement;
|
||||
}
|
||||
|
||||
private static StructureExclusionReport includeManagedDatapackStructures(
|
||||
StructureExclusionReport snapshot, List<DatapackIngestService.Entry> installedEntries) {
|
||||
Map<String, ManagedDatapackStructure> unregisteredStructures = new LinkedHashMap<>();
|
||||
for (DatapackIngestService.Entry entry : installedEntries) {
|
||||
String importUrl = normalizeImportUrl(entry.url);
|
||||
if (importUrl.isEmpty() || !snapshot.configuredImportUrls().contains(importUrl)) {
|
||||
continue;
|
||||
}
|
||||
String sourceId = entry.id == null || entry.id.isBlank() ? "unknown" : entry.id.trim();
|
||||
List<String> structureKeys = entry.structureKeys == null ? List.of() : entry.structureKeys;
|
||||
for (String key : structureKeys) {
|
||||
String normalizedKey = normalizeStructureKey(key);
|
||||
if (normalizedKey.isEmpty() || snapshot.registeredKeys().contains(normalizedKey)) {
|
||||
continue;
|
||||
}
|
||||
ManagedDatapackStructure managed = unregisteredStructures.computeIfAbsent(
|
||||
normalizedKey, ignored -> new ManagedDatapackStructure(key.trim()));
|
||||
managed.sourceIds().add(sourceId);
|
||||
}
|
||||
}
|
||||
|
||||
List<StructureExclusion> entries = new ArrayList<>();
|
||||
int configuredUnregistered = snapshot.configuredUnregistered();
|
||||
for (StructureExclusion exclusion : snapshot.entries()) {
|
||||
boolean replacedByManagedSource = exclusion.kind() == StructureExclusionKind.UNREGISTERED
|
||||
&& unregisteredStructures.containsKey(normalizeStructureKey(exclusion.key()));
|
||||
if (replacedByManagedSource) {
|
||||
configuredUnregistered--;
|
||||
continue;
|
||||
}
|
||||
entries.add(exclusion);
|
||||
}
|
||||
for (ManagedDatapackStructure managed : unregisteredStructures.values()) {
|
||||
entries.add(new StructureExclusion(
|
||||
StructureExclusionKind.UNREGISTERED, managed.key(),
|
||||
String.join(",", managed.sourceIds()),
|
||||
"declared by an Iris-managed datapack but absent from the live registry; "
|
||||
+ "restart, enablement, or datapack validation may be required"));
|
||||
}
|
||||
sortStructureExclusions(entries);
|
||||
return new StructureExclusionReport(
|
||||
List.copyOf(entries), snapshot.registeredKeys(), snapshot.configuredImportUrls(),
|
||||
snapshot.registeredExcluded(), Math.max(0, configuredUnregistered),
|
||||
snapshot.editableUnplaced(), unregisteredStructures.size());
|
||||
}
|
||||
|
||||
private static void printStructureExclusionReport(
|
||||
String worldName, StructureExclusionReport report) {
|
||||
Iris.info("Iris goto unregistered report for world '%s': %d non-generating structure candidate(s).",
|
||||
worldName, report.entries().size());
|
||||
for (StructureExclusion exclusion : report.entries()) {
|
||||
String source = exclusion.source() == null
|
||||
? ""
|
||||
: " (source " + exclusion.source() + ")";
|
||||
Iris.info("[%s] %s%s: %s",
|
||||
exclusion.kind().label(), exclusion.key(), source, exclusion.reason());
|
||||
}
|
||||
Iris.info("Iris goto unregistered summary: %d registered key(s) excluded, "
|
||||
+ "%d managed datapack key(s) unregistered, %d configured native key(s) unregistered, "
|
||||
+ "%d editable Iris structure(s) unplaced.",
|
||||
report.registeredExcluded(), report.managedUnregistered(),
|
||||
report.configuredUnregistered(), report.editableUnplaced());
|
||||
Iris.info("Eligibility report only; no chunks were searched and existing generated starts were not scanned.");
|
||||
}
|
||||
|
||||
private static boolean needsNativeReachabilityReason(
|
||||
StructureLookupRoute route, IrisNativeStructureDecision decision) {
|
||||
return route == StructureLookupRoute.UNREACHABLE
|
||||
|| route == StructureLookupRoute.NO_ACTIVE_PLACEMENT && decision.generate();
|
||||
}
|
||||
|
||||
static String describeStructureExclusion(
|
||||
StructureLookupRoute route, String key, NativeStructureGenerationStatus status,
|
||||
boolean nativePlacement, List<String> requiredBiomes, Set<String> possibleBiomeKeys) {
|
||||
return switch (route) {
|
||||
case UNKNOWN -> "the key is not registered by the active server/datapack";
|
||||
case POLICY_DISABLED -> NativeStructureGenerationPolicy.generationStatusMessage(key, status);
|
||||
case WORLD_DISABLED -> "native structure generation is disabled for this world";
|
||||
case UNREACHABLE -> nativeReachabilityReason(requiredBiomes, possibleBiomeKeys);
|
||||
case NO_ACTIVE_PLACEMENT -> {
|
||||
String placementType = nativePlacement
|
||||
? "configured nativeStructures placement"
|
||||
: "configured Iris replacement";
|
||||
String reason = placementType + " is inactive: no matching placement has positive density "
|
||||
+ "when density-based and a Y band intersecting this world";
|
||||
if (status == NativeStructureGenerationStatus.GENERATE_NATIVE) {
|
||||
reason += "; the native route is also inactive because "
|
||||
+ nativeReachabilityReason(requiredBiomes, possibleBiomeKeys);
|
||||
}
|
||||
yield reason;
|
||||
}
|
||||
case IRIS, NATIVE -> throw new IllegalArgumentException(
|
||||
"Active structure route cannot be described as excluded: " + route);
|
||||
};
|
||||
}
|
||||
|
||||
static String nativeReachabilityReason(
|
||||
List<String> requiredBiomes, Set<String> possibleBiomeKeys) {
|
||||
if (requiredBiomes == null || requiredBiomes.isEmpty()) {
|
||||
return "its resolved biome filter is empty";
|
||||
}
|
||||
Set<String> possible = possibleBiomeKeys == null ? Set.of() : possibleBiomeKeys;
|
||||
for (String requiredBiome : requiredBiomes) {
|
||||
if (possible.contains(normalizeStructureKey(requiredBiome))) {
|
||||
return "no active positive-weight, positive-frequency structure-set entry includes it in this world";
|
||||
}
|
||||
}
|
||||
return "this pack does not produce any of its required biome(s): "
|
||||
+ String.join("/", requiredBiomes);
|
||||
}
|
||||
|
||||
static String describeConfiguredUnregisteredNative(boolean locatablePlacement) {
|
||||
String reason = "configured in nativeStructures, but the key is not registered by the active "
|
||||
+ "server/datapack, so Minecraft cannot create its native structure start";
|
||||
return locatablePlacement
|
||||
? reason
|
||||
: reason + "; its Iris placement is also inactive because no matching placement has positive "
|
||||
+ "density when density-based and a Y band intersecting this world";
|
||||
}
|
||||
|
||||
private static Map<String, String> distinctStructureKeys(List<String> keys) {
|
||||
Map<String, String> distinctKeys = new LinkedHashMap<>();
|
||||
for (String key : keys) {
|
||||
String normalizedKey = normalizeStructureKey(key);
|
||||
if (!normalizedKey.isEmpty()) {
|
||||
distinctKeys.putIfAbsent(normalizedKey, key.trim());
|
||||
}
|
||||
}
|
||||
return distinctKeys;
|
||||
}
|
||||
|
||||
private static Set<String> normalizedStructureKeys(Iterable<String> keys) {
|
||||
Set<String> normalizedKeys = new LinkedHashSet<>();
|
||||
if (keys == null) {
|
||||
return normalizedKeys;
|
||||
}
|
||||
for (String key : keys) {
|
||||
String normalizedKey = normalizeStructureKey(key);
|
||||
if (!normalizedKey.isEmpty()) {
|
||||
normalizedKeys.add(normalizedKey);
|
||||
}
|
||||
}
|
||||
return normalizedKeys;
|
||||
}
|
||||
|
||||
private static Set<String> normalizedImportUrls(Iterable<String> urls) {
|
||||
Set<String> normalizedUrls = new LinkedHashSet<>();
|
||||
if (urls == null) {
|
||||
return normalizedUrls;
|
||||
}
|
||||
for (String url : urls) {
|
||||
String normalizedUrl = normalizeImportUrl(url);
|
||||
if (!normalizedUrl.isEmpty()) {
|
||||
normalizedUrls.add(normalizedUrl);
|
||||
}
|
||||
}
|
||||
return Set.copyOf(normalizedUrls);
|
||||
}
|
||||
|
||||
private static String normalizeImportUrl(String url) {
|
||||
return url == null ? "" : url.trim();
|
||||
}
|
||||
|
||||
private static void sortStructureExclusions(List<StructureExclusion> exclusions) {
|
||||
exclusions.sort((left, right) -> {
|
||||
int kindComparison = left.kind().compareTo(right.kind());
|
||||
return kindComparison == 0
|
||||
? left.key().compareToIgnoreCase(right.key())
|
||||
: kindComparison;
|
||||
});
|
||||
}
|
||||
|
||||
private static String normalizeStructureKey(String key) {
|
||||
return key == null ? "" : key.trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
static StructureLookupRoute selectStructureLookupRoute(
|
||||
boolean registered, IrisNativeStructureDecision decision, boolean nativePlacement,
|
||||
boolean locatableNativePlacement, boolean locatableEditablePlacement,
|
||||
boolean nativeGenerationEnabled, boolean reachable) {
|
||||
if (!registered) {
|
||||
return !nativePlacement && locatableEditablePlacement
|
||||
? StructureLookupRoute.IRIS
|
||||
: StructureLookupRoute.UNKNOWN;
|
||||
}
|
||||
if (decision == null) {
|
||||
throw new IllegalArgumentException("Registered structure lookup requires a generation decision");
|
||||
}
|
||||
if (!decision.generate()
|
||||
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||
return StructureLookupRoute.POLICY_DISABLED;
|
||||
}
|
||||
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||
if (!nativePlacement) {
|
||||
return locatableEditablePlacement
|
||||
? StructureLookupRoute.IRIS
|
||||
: StructureLookupRoute.NO_ACTIVE_PLACEMENT;
|
||||
}
|
||||
if (!nativeGenerationEnabled) {
|
||||
return StructureLookupRoute.WORLD_DISABLED;
|
||||
}
|
||||
return locatableNativePlacement
|
||||
? StructureLookupRoute.NATIVE
|
||||
: StructureLookupRoute.NO_ACTIVE_PLACEMENT;
|
||||
}
|
||||
if (!nativeGenerationEnabled) {
|
||||
return StructureLookupRoute.WORLD_DISABLED;
|
||||
}
|
||||
if (nativePlacement && locatableNativePlacement) {
|
||||
return StructureLookupRoute.NATIVE;
|
||||
}
|
||||
if (reachable) {
|
||||
return StructureLookupRoute.NATIVE;
|
||||
}
|
||||
return nativePlacement
|
||||
? StructureLookupRoute.NO_ACTIVE_PLACEMENT
|
||||
: StructureLookupRoute.UNREACHABLE;
|
||||
}
|
||||
|
||||
private static Structure resolveNativeStructure(String structureKey) {
|
||||
Registry<Structure> structureRegistry = Bukkit.getRegistry(Structure.class);
|
||||
if (structureRegistry == null) {
|
||||
return null;
|
||||
}
|
||||
for (Structure candidate : structureRegistry) {
|
||||
NamespacedKey key = structureRegistry.getKey(candidate);
|
||||
if (key != null && key.toString().equalsIgnoreCase(structureKey)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void locateIrisStructure(Engine engine, String structure, VolmitSender commandSender) {
|
||||
Player target = player();
|
||||
if (target == null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_RUN_THIS_GAME_TELEPORT_STRUCTURE_2));
|
||||
return;
|
||||
}
|
||||
World targetWorld = target.getWorld();
|
||||
Location origin = target.getLocation();
|
||||
int blockX = origin.getBlockX();
|
||||
int blockZ = origin.getBlockZ();
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_LOCATING_2, MessageArgument.untrusted("structure", structure)));
|
||||
J.a(() -> {
|
||||
try {
|
||||
IrisStructureLocator.LocateResult result =
|
||||
GenerationSemanticQueries.nearestStructure(engine, structure, blockX, blockZ, 1024);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "Unable to locate " + structure
|
||||
+ ": the density search safety limit was reached before the full 1024-chunk radius was searched.");
|
||||
return;
|
||||
}
|
||||
if (!result.found()) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "No " + structure + " found within 1024 chunks of you.");
|
||||
return;
|
||||
}
|
||||
Location destination = new Location(
|
||||
targetWorld, result.originX(), result.baseY(), result.originZ());
|
||||
prepareStructureTeleport(target, targetWorld, commandSender, structure, destination, true);
|
||||
} catch (Throwable t) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.RED + "Could not locate " + structure + ": " + t.getClass().getSimpleName());
|
||||
Iris.reportError("Could not locate Iris-placed structure '" + structure + "'.", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Find an object", descriptionKey = "iris.director.commandfind.director.find_object")
|
||||
public void object(
|
||||
@Param(description = "The object to look for", descriptionKey = "iris.director.commandfind.param.object_look", customHandler = LocatableObjectHandler.class)
|
||||
String object,
|
||||
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_4", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
|
||||
if (e == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_NOT_IRIS_WORLD_4));
|
||||
return;
|
||||
}
|
||||
|
||||
Player studioPlayer = player();
|
||||
if (studioPlayer != null) {
|
||||
try {
|
||||
if (ObjectStudioSaveService.get().teleportTo(studioPlayer, object)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_OBJECT_STUDIO_TELEPORTING, MessageArgument.untrusted("object", object)));
|
||||
return;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
Iris.reportError(t);
|
||||
}
|
||||
}
|
||||
|
||||
if (GenerationFindCatalog.hasObjectPlacement(e, object)) {
|
||||
EngineBukkitOps.gotoObject(e, object, player(), teleport);
|
||||
return;
|
||||
}
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_FIND_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS, MessageArgument.untrusted("object", object)));
|
||||
}
|
||||
|
||||
private void prepareStructureTeleport(Player target, World world, VolmitSender commandSender, String structure,
|
||||
Location at, boolean useLocatedY) {
|
||||
J.runEntity(target, () -> {
|
||||
if (!target.isOnline()) {
|
||||
return;
|
||||
}
|
||||
if (!world.equals(target.getWorld())) {
|
||||
commandSender.sendMessage(C.YELLOW + "Cancelled teleport to " + structure + " because your world changed.");
|
||||
return;
|
||||
}
|
||||
loadStructureDestination(target, world, commandSender, structure, at, useLocatedY);
|
||||
});
|
||||
}
|
||||
|
||||
private void loadStructureDestination(Player target, World world, VolmitSender commandSender, String structure,
|
||||
Location at, boolean useLocatedY) {
|
||||
int chunkX = at.getBlockX() >> 4;
|
||||
int chunkZ = at.getBlockZ() >> 4;
|
||||
try {
|
||||
BukkitPlatform.chunkAtAsync(world, chunkX, chunkZ, true).whenComplete((chunk, error) -> {
|
||||
if (error != null) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
|
||||
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
|
||||
return;
|
||||
}
|
||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ,
|
||||
() -> teleportToStructure(target, world, commandSender, structure, at, useLocatedY));
|
||||
if (!scheduled) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not schedule the destination lookup for " + structure + ".");
|
||||
}
|
||||
});
|
||||
} catch (Throwable error) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
|
||||
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
|
||||
}
|
||||
}
|
||||
|
||||
private void teleportToStructure(Player target, World world, VolmitSender commandSender, String structure,
|
||||
Location at, boolean useLocatedY) {
|
||||
try {
|
||||
int y = useLocatedY
|
||||
? Math.max(world.getMinHeight() + 1, Math.min(world.getMaxHeight() - 1, at.getBlockY() + 2))
|
||||
: world.getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2;
|
||||
Location destination = new Location(world, at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5);
|
||||
J.runEntity(target, () -> completeStructureTeleport(target, world, commandSender, structure, destination));
|
||||
} catch (Throwable t) {
|
||||
sendStructureMessage(target, commandSender, C.RED + "Could not prepare the destination for " + structure + ".");
|
||||
Iris.reportError("Could not prepare structure destination '" + structure + "'.", t);
|
||||
}
|
||||
}
|
||||
|
||||
private void completeStructureTeleport(Player target, World world, VolmitSender commandSender, String structure,
|
||||
Location destination) {
|
||||
if (!target.isOnline()) {
|
||||
return;
|
||||
}
|
||||
if (!world.equals(target.getWorld())) {
|
||||
commandSender.sendMessage(C.YELLOW + "Cancelled teleport to " + structure + " because your world changed.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
BukkitPlatform.teleportAsync(target, destination).whenComplete((teleported, error) -> {
|
||||
if (error != null) {
|
||||
Iris.reportError("Could not teleport to '" + structure + "'.", error);
|
||||
}
|
||||
sendStructureMessage(target, commandSender, error == null && Boolean.TRUE.equals(teleported)
|
||||
? IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_TELEPORTED,
|
||||
MessageArgument.untrusted("structure", structure),
|
||||
MessageArgument.untrusted("value", destination.getBlockX()),
|
||||
MessageArgument.untrusted("y", destination.getBlockY()),
|
||||
MessageArgument.untrusted("value2", destination.getBlockZ()))
|
||||
: C.RED + "Could not teleport to " + structure + ".");
|
||||
});
|
||||
} catch (Throwable error) {
|
||||
commandSender.sendMessage(C.RED + "Could not teleport to " + structure + ".");
|
||||
Iris.reportError("Could not teleport to '" + structure + "'.", error);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendStructureMessage(Player target, VolmitSender commandSender, String message) {
|
||||
J.runEntity(target, () -> commandSender.sendMessage(message));
|
||||
}
|
||||
|
||||
enum StructureLookupRoute {
|
||||
UNKNOWN,
|
||||
POLICY_DISABLED,
|
||||
NO_ACTIVE_PLACEMENT,
|
||||
WORLD_DISABLED,
|
||||
UNREACHABLE,
|
||||
IRIS,
|
||||
NATIVE
|
||||
}
|
||||
|
||||
private enum StructureExclusionKind {
|
||||
UNREGISTERED("unregistered"),
|
||||
EXCLUDED("excluded"),
|
||||
UNPLACED("unplaced");
|
||||
|
||||
private final String label;
|
||||
|
||||
StructureExclusionKind(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
private String label() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
|
||||
private record StructureExclusion(
|
||||
StructureExclusionKind kind, String key, String source, String reason) {
|
||||
}
|
||||
|
||||
private record StructureExclusionReport(
|
||||
List<StructureExclusion> entries, Set<String> registeredKeys,
|
||||
Set<String> configuredImportUrls, int registeredExcluded,
|
||||
int configuredUnregistered, int editableUnplaced, int managedUnregistered) {
|
||||
}
|
||||
|
||||
private record ManagedDatapackStructure(String key, Set<String> sourceIds) {
|
||||
private ManagedDatapackStructure(String key) {
|
||||
this(key, new LinkedHashSet<>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,890 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.world.BukkitWorldReconciler;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.world.IrisWorldStorage;
|
||||
import art.arcane.iris.world.IrisWorlds;
|
||||
import art.arcane.iris.world.PendingWorldReplacementManager;
|
||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
||||
import art.arcane.iris.world.lifecycle.IrisWorldRemovalService;
|
||||
import art.arcane.iris.world.lifecycle.LifecycleOperationCoordinator;
|
||||
import art.arcane.iris.world.lifecycle.WorldLifecycleService;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.PackDownloader;
|
||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import art.arcane.volmlib.util.director.DirectorParameterHandler;
|
||||
import art.arcane.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.command.specialhandlers.NullablePlayerHandler;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static art.arcane.iris.platform.bootstrap.ServerProperties.BUKKIT_YML;
|
||||
import static org.bukkit.Bukkit.getServer;
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.IrisMessages;
|
||||
import art.arcane.iris.localization.PackDownloadMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.RuntimeUiMessages;
|
||||
@Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command", descriptionKey = "iris.director.commandiris.director.basic_command")
|
||||
public class CommandIris implements DirectorExecutor {
|
||||
private static final String NO_DOWNLOAD_SOURCE = "__none__";
|
||||
private static final String PRESERVE_REPLACEMENT_SEED = "preserve";
|
||||
private static final long WORLD_UNLOAD_TIMEOUT_SECONDS = 150L;
|
||||
|
||||
private CommandStudio studio;
|
||||
private CommandPregen pregen;
|
||||
private CommandObject object;
|
||||
private CommandStructure structure;
|
||||
private CommandJigsaw jigsaw;
|
||||
private CommandWhat what;
|
||||
private CommandEdit edit;
|
||||
private CommandDeveloper developer;
|
||||
private CommandPack pack;
|
||||
private CommandFind find;
|
||||
private CommandDatapack datapack;
|
||||
private CommandDebug debug;
|
||||
VolmitSender sender = Iris.getSender();
|
||||
|
||||
@Director(description = "Choose your language or the server default")
|
||||
public void language() {
|
||||
Iris.instance.selectLanguage(sender().getS(), new String[0]);
|
||||
}
|
||||
|
||||
@Director(description = "Create a new world", descriptionKey = "iris.director.commandiris.director.create_new_world", aliases = {"c"})
|
||||
public void create(
|
||||
@Param(aliases = "world-name", description = "The name of the world to create", descriptionKey = "iris.director.commandiris.param.name_world_create")
|
||||
String name,
|
||||
@Param(
|
||||
aliases = {"dimension", "pack"},
|
||||
description = "The dimension/pack to create the world with", descriptionKey = "iris.director.commandiris.param.dimension_pack_create_world_with",
|
||||
defaultValue = "default",
|
||||
customHandler = PackDimensionTypeHandler.class
|
||||
)
|
||||
String type,
|
||||
@Param(description = "The seed to generate the world with", descriptionKey = "iris.director.commandiris.param.seed_generate_world_with", defaultValue = "1337")
|
||||
long seed
|
||||
) {
|
||||
NamespacedKey worldKey;
|
||||
try {
|
||||
worldKey = IrisWorldStorage.managedKeyFromName(name);
|
||||
IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey);
|
||||
} catch (IllegalArgumentException e) {
|
||||
sender().sendMessage(C.RED + e.getMessage());
|
||||
return;
|
||||
}
|
||||
String worldName = IrisWorldStorage.logicalName(worldKey);
|
||||
if (worldName.equalsIgnoreCase("iris")) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_YOU_CANNOT_USE_WORLD_NAME_IRIS_CREATING_WORLDS_AS_IRIS));
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD));
|
||||
return;
|
||||
}
|
||||
|
||||
if (worldName.equalsIgnoreCase("benchmark")) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_YOU_CANNOT_USE_WORLD_NAME_BENCHMARK_CREATING_WORLDS_AS_IRIS));
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_MAY_WE_SUGGEST_NAME_IRISWORLD_INSTEAD_2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (IrisWorldStorage.dimensionRoot(worldName).exists()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_THAT_FOLDER_ALREADY_EXISTS));
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<String> startupDenial = ServerConfigurator.worldCreationDenialReason(false);
|
||||
if (startupDenial.isPresent()) {
|
||||
sender().sendMessage(C.YELLOW + startupDenial.get());
|
||||
return;
|
||||
}
|
||||
|
||||
String resolvedType = type.equalsIgnoreCase("default")
|
||||
? IrisSettings.get().getGenerator().getDefaultWorldType()
|
||||
: type;
|
||||
|
||||
IrisDimension dimension = IrisToolbelt.getDimension(resolvedType);
|
||||
if (dimension == null) {
|
||||
sender().sendMessage(IrisLanguage.text(
|
||||
BukkitCommandMessagesExtended.COMMAND_IRIS_DIMENSION_NOT_FOUND,
|
||||
MessageArgument.untrusted("dimension", resolvedType)
|
||||
));
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND));
|
||||
sender().sendMessage(IrisLanguage.text(
|
||||
BukkitCommandMessagesExtended.COMMAND_IRIS_INSTALL_PACK_AND_RESTART,
|
||||
MessageArgument.untrusted("command", PackDownloader.downloadCommandFor(resolvedType))
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
IrisToolbelt.createWorld()
|
||||
.dimension(resolvedType)
|
||||
.name(worldName)
|
||||
.seed(seed)
|
||||
.sender(sender())
|
||||
.studio(false)
|
||||
.create();
|
||||
} catch (Throwable e) {
|
||||
if (reportExpectedCreationInterruption(e)) {
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_EXCEPTION_RAISED_DURING_CREATION_SEE_CONSOLE_MORE_DETAILS));
|
||||
Iris.reportError("Exception raised during world creation for \"" + worldName + "\".", e);
|
||||
return;
|
||||
}
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SUCCESSFULLY_CREATED_YOUR_WORLD));
|
||||
}
|
||||
|
||||
@Director(
|
||||
description = "Replace an existing world with Iris generation on the next restart",
|
||||
descriptionKey = "iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart",
|
||||
aliases = {"override", "overwrite"}
|
||||
)
|
||||
public void replace(
|
||||
@Param(
|
||||
name = "target",
|
||||
aliases = "world-name",
|
||||
description = "The exact existing world slot to replace",
|
||||
descriptionKey = "iris.director.commandiris.param.replace_exact_existing_world_slot_next_restart"
|
||||
)
|
||||
String target,
|
||||
@Param(
|
||||
aliases = {"dimension", "pack"},
|
||||
description = "The dimension/pack to replace the world with",
|
||||
defaultValue = "default",
|
||||
customHandler = PackDimensionTypeHandler.class
|
||||
)
|
||||
String type,
|
||||
@Param(
|
||||
name = "seed",
|
||||
aliases = "s",
|
||||
description = "The replacement seed; omit to preserve the target world's seed",
|
||||
defaultValue = PRESERVE_REPLACEMENT_SEED,
|
||||
customHandler = ReplacementSeedHandler.class
|
||||
)
|
||||
Long seed
|
||||
) {
|
||||
NamespacedKey worldKey;
|
||||
try {
|
||||
worldKey = Iris.instance.pendingWorldReplacements().resolveRequestedWorldKey(target);
|
||||
} catch (IllegalArgumentException e) {
|
||||
sender().sendMessage(C.RED + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
String resolvedType = type.equalsIgnoreCase("default")
|
||||
? IrisSettings.get().getGenerator().getDefaultWorldType()
|
||||
: type;
|
||||
IrisDimension dimension = IrisToolbelt.getDimension(resolvedType);
|
||||
if (dimension == null) {
|
||||
sender().sendMessage(IrisLanguage.text(
|
||||
BukkitCommandMessagesExtended.COMMAND_IRIS_DIMENSION_NOT_FOUND,
|
||||
MessageArgument.untrusted("dimension", resolvedType)
|
||||
));
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TRY_ONE_OVERWORLD_VANILLA_FLAT_THEEND));
|
||||
sender().sendMessage(IrisLanguage.text(
|
||||
BukkitCommandMessagesExtended.COMMAND_IRIS_INSTALL_PACK_AND_RESTART,
|
||||
MessageArgument.untrusted("command", PackDownloader.downloadCommandFor(resolvedType))
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
PendingWorldReplacementManager.StagedReplacement staged = Iris.instance
|
||||
.pendingWorldReplacements()
|
||||
.stageReplacement(sender(), worldKey, dimension, seed);
|
||||
String seedDetail = seed == null
|
||||
? " preserving seed " + staged.seed()
|
||||
: " using seed " + staged.seed();
|
||||
sender().sendMessage(C.GREEN + "Staged Iris replacement for " + staged.worldKey()
|
||||
+ seedDetail + ". Restart once to publish it. The current dimension is retained until Iris "
|
||||
+ "verifies the replacement.");
|
||||
} catch (LifecycleOperationCoordinator.BusyException busy) {
|
||||
sender().sendMessage(C.YELLOW + busy.getMessage());
|
||||
} catch (Throwable failure) {
|
||||
Iris.reportError("Failed to stage Iris world replacement for " + worldKey + ".", failure);
|
||||
String detail = failure.getMessage() == null || failure.getMessage().isBlank()
|
||||
? failure.getClass().getSimpleName()
|
||||
: failure.getMessage();
|
||||
sender().sendMessage(C.RED + "Could not stage the world replacement: " + detail);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean reportExpectedCreationInterruption(Throwable failure) {
|
||||
Throwable current = failure;
|
||||
while (current != null) {
|
||||
if (current instanceof LifecycleOperationCoordinator.BusyException) {
|
||||
sender().sendMessage(C.YELLOW + current.getMessage());
|
||||
return true;
|
||||
}
|
||||
current = current.getCause();
|
||||
}
|
||||
String message = failure.getMessage();
|
||||
if (message != null && message.contains("queued a restart")) {
|
||||
sender().sendMessage(C.YELLOW + message);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Director(description = "Teleport to another world", descriptionKey = "iris.director.commandiris.director.teleport_another_world", aliases = {"tp"}, sync = true)
|
||||
public void teleport(
|
||||
@Param(description = "World to teleport to", descriptionKey = "iris.director.commandiris.param.world_teleport")
|
||||
World world,
|
||||
@Param(description = "Player to teleport", descriptionKey = "iris.director.commandiris.param.player_teleport", defaultValue = "---", customHandler = NullablePlayerHandler.class)
|
||||
Player player
|
||||
) {
|
||||
if (player == null && sender().isPlayer())
|
||||
player = sender().player();
|
||||
|
||||
final Player target = player;
|
||||
if (target == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SPECIFIED_PLAYER_DOES_NOT_EXIST));
|
||||
return;
|
||||
}
|
||||
|
||||
final Location spawn = world.getSpawnLocation();
|
||||
final Runnable teleportTask = () -> {
|
||||
BukkitPlatform.teleportAsync(target, spawn);
|
||||
new VolmitSender(target).sendMessage(C.GREEN + IrisLanguage.text(
|
||||
RuntimeUiMessages.TELEPORTED_TO_WORLD,
|
||||
MessageArgument.untrusted("world", world.getName())
|
||||
));
|
||||
};
|
||||
if (!J.runEntity(target, teleportTask)) {
|
||||
teleportTask.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Director(hidden = true, sync = true, description = "Print version information", descriptionKey = "iris.director.commandiris.director.print_version_information")
|
||||
public void version(@Param(name = "sender", contextual = true) CommandSender sender) {
|
||||
debug.version(sender);
|
||||
}
|
||||
|
||||
@Director(description = "Print world height information", descriptionKey = "iris.director.commandiris.director.print_world_height_information", origin = DirectorOrigin.PLAYER)
|
||||
public void height() {
|
||||
if (sender().isPlayer()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TO, MessageArgument.untrusted("value", sender().player().getWorld().getMinHeight()), MessageArgument.untrusted("value2", sender().player().getWorld().getMaxHeight())));
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_TOTAL_HEIGHT, MessageArgument.untrusted("value", (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.", descriptionKey = "iris.director.commandiris.director.check_access_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(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_IRIS_WORLDS));
|
||||
for (World IrisWorld : IrisWorlds.copy()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_MESSAGE, MessageArgument.untrusted("value", IrisWorld.getName())));
|
||||
}
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_BUKKIT_WORLDS));
|
||||
for (World BukkitWorld : BukkitWorlds.copy()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_MESSAGE_2, MessageArgument.untrusted("value", 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", descriptionKey = "iris.director.commandiris.director.remove_iris_world", aliases = {"rm"}, sync = true)
|
||||
public void remove(
|
||||
@Param(description = "The loaded or disk-only Iris world to remove", descriptionKey = "iris.director.commandiris.param.world_remove", customHandler = ManagedWorldNameHandler.class)
|
||||
String world,
|
||||
@Param(description = "Whether to also remove the folder (if set to false, just does not load the world)", descriptionKey = "iris.director.commandiris.param.whether_also_remove_folder_if_set_false_just_does_not_load_world", defaultValue = "true")
|
||||
boolean delete
|
||||
) {
|
||||
VolmitSender responseSender = sender();
|
||||
responseSender.sendMessage(C.GRAY + "Removing Iris world '" + world + "'...");
|
||||
IrisWorldRemovalService.get().remove(world, delete).whenComplete((result, throwable) -> {
|
||||
Runnable response = () -> reportRemovalResult(responseSender, world, result, throwable);
|
||||
if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) {
|
||||
return;
|
||||
}
|
||||
J.s(response);
|
||||
});
|
||||
}
|
||||
|
||||
private void reportRemovalResult(
|
||||
VolmitSender responseSender,
|
||||
String requestedWorld,
|
||||
IrisWorldRemovalService.RemovalResult result,
|
||||
Throwable throwable
|
||||
) {
|
||||
if (throwable != null || result == null) {
|
||||
Throwable failure = throwable == null
|
||||
? new IllegalStateException("World removal returned no result.")
|
||||
: throwable;
|
||||
responseSender.sendMessage(C.RED + "World removal failed unexpectedly; nothing further was deleted.");
|
||||
Iris.reportError("Unexpected world removal failure for \"" + requestedWorld + "\".", failure);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (result.status()) {
|
||||
case UNREGISTERED -> responseSender.sendMessage(C.GREEN + "Unloaded and unregistered '"
|
||||
+ result.target().logicalName() + "'; its files were preserved.");
|
||||
case DELETED -> responseSender.sendMessage(C.GREEN + "Removed Iris world '"
|
||||
+ result.target().logicalName() + "' and deleted its folder.");
|
||||
case DELETE_QUEUED -> responseSender.sendMessage(C.YELLOW + "Removed Iris world '"
|
||||
+ result.target().logicalName() + "'; its quarantined folder will be deleted at startup.");
|
||||
case BUSY -> responseSender.sendMessage(C.YELLOW + "World changes are busy with "
|
||||
+ result.blockingOperation().kind().name().toLowerCase(Locale.ROOT) + " for '"
|
||||
+ result.blockingOperation().target() + "'. Try again when it completes.");
|
||||
case INVALID_IDENTIFIER, PROTECTED_WORLD, NOT_IRIS_WORLD, UNSAFE_PATH, NOT_FOUND ->
|
||||
responseSender.sendMessage(C.RED + removalFailureDetail(result));
|
||||
default -> {
|
||||
responseSender.sendMessage(C.RED + "World removal stopped at "
|
||||
+ result.status().name().toLowerCase(Locale.ROOT) + ": " + removalFailureDetail(result));
|
||||
if (result.quarantineDirectory() != null) {
|
||||
responseSender.sendMessage(C.YELLOW + "The recoverable world folder is "
|
||||
+ result.quarantineDirectory().toAbsolutePath() + ".");
|
||||
} else if (result.configurationChanged() || result.registryChanged()) {
|
||||
responseSender.sendMessage(C.YELLOW + "Removal changed registration state before stopping; "
|
||||
+ "the original world folder was not deleted.");
|
||||
}
|
||||
if (result.failure() != null) {
|
||||
Iris.reportError("World removal failed for \"" + requestedWorld + "\" at "
|
||||
+ result.status().name() + ".", result.failure());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String removalFailureDetail(IrisWorldRemovalService.RemovalResult result) {
|
||||
Throwable failure = result.failure();
|
||||
if (failure == null || failure.getMessage() == null || failure.getMessage().isBlank()) {
|
||||
return result.status().name().toLowerCase(Locale.ROOT).replace('_', ' ');
|
||||
}
|
||||
return failure.getMessage();
|
||||
}
|
||||
|
||||
@Director(description = "Download a project.", descriptionKey = "iris.director.commandiris.director.download_project", aliases = "dl")
|
||||
public void download(
|
||||
@Param(name = "pack", description = "The built-in pack to download", descriptionKey = "iris.director.commandiris.param.pack_download", defaultValue = NO_DOWNLOAD_SOURCE, customHandler = DownloadPackHandler.class)
|
||||
String pack,
|
||||
@Param(name = "link", description = "A direct HTTP or HTTPS zip link", defaultValue = NO_DOWNLOAD_SOURCE)
|
||||
String link
|
||||
) {
|
||||
String builtInPack = NO_DOWNLOAD_SOURCE.equals(pack) ? null : pack;
|
||||
String directLink = NO_DOWNLOAD_SOURCE.equals(link) ? null : link;
|
||||
if ((builtInPack == null) == (directLink == null)) {
|
||||
sender().sendMessage(IrisLanguage.text(PackDownloadMessages.INVALID_SOURCE));
|
||||
return;
|
||||
}
|
||||
if (builtInPack != null) {
|
||||
Iris.service(StudioSVC.class).downloadBuiltIn(sender(), builtInPack);
|
||||
return;
|
||||
}
|
||||
if (!PackDownloader.isDirectZipUrl(directLink)) {
|
||||
sender().sendMessage(IrisLanguage.text(PackDownloadMessages.INVALID_URL));
|
||||
return;
|
||||
}
|
||||
Iris.service(StudioSVC.class).downloadUrl(sender(), directLink);
|
||||
}
|
||||
|
||||
@Director(description = "Get metrics for your world", descriptionKey = "iris.director.commandiris.director.get_metrics_your_world", aliases = "measure", origin = DirectorOrigin.PLAYER)
|
||||
public void metrics() {
|
||||
if (!IrisToolbelt.isIrisWorld(world())) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_YOU_MUST_BE_IRIS_WORLD));
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_SENDING_METRICS));
|
||||
engine().printMetrics(sender()::sendMessage);
|
||||
}
|
||||
|
||||
@Director(description = "Reload configuration file (this is also done automatically)", descriptionKey = "iris.director.commandiris.director.reload_configuration_file_this_is_also_done_automatically")
|
||||
public void reload() {
|
||||
IrisSettings.invalidate();
|
||||
IrisSettings.get();
|
||||
boolean localeLoaded = IrisLanguage.reload();
|
||||
if (localeLoaded) {
|
||||
sender().sendMessage(C.GREEN + IrisLanguage.text(
|
||||
IrisMessages.COMMAND_RELOAD_SUCCESS,
|
||||
MessageArgument.trusted("locale", IrisLanguage.activeLocale())
|
||||
));
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(C.YELLOW + IrisLanguage.text(
|
||||
IrisMessages.COMMAND_RELOAD_FAILED,
|
||||
MessageArgument.untrusted("locale", IrisSettings.get().getGeneral().getLanguage()),
|
||||
MessageArgument.trusted("activeLocale", IrisLanguage.activeLocale())
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@Director(description = "Unload an Iris World", descriptionKey = "iris.director.commandiris.director.unload_iris_world", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void unloadWorld(
|
||||
@Param(description = "The world to unload", descriptionKey = "iris.director.commandiris.param.world_unload")
|
||||
World world
|
||||
) {
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_THIS_IS_NOT_IRIS_WORLD_IRIS_WORLDS_2, MessageArgument.untrusted("value", String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()))));
|
||||
return;
|
||||
}
|
||||
VolmitSender responseSender = sender();
|
||||
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_UNLOADING_WORLD, MessageArgument.untrusted("value", world.getName())));
|
||||
LifecycleOperationCoordinator.Lease lease;
|
||||
try {
|
||||
lease = LifecycleOperationCoordinator.get().acquire(
|
||||
LifecycleOperationCoordinator.Domain.WORLD_MUTATION,
|
||||
LifecycleOperationCoordinator.OperationKind.WORLD_UNLOAD,
|
||||
WorldIdentity.serialize(world)
|
||||
);
|
||||
} catch (LifecycleOperationCoordinator.BusyException e) {
|
||||
responseSender.sendMessage(C.YELLOW + e.getMessage());
|
||||
return;
|
||||
}
|
||||
PlatformChunkGenerator generator = IrisToolbelt.access(world);
|
||||
IrisToolbelt.beginWorldMaintenance(world, "world-unload", true);
|
||||
try {
|
||||
AtomicBoolean terminalTimeout = new AtomicBoolean(false);
|
||||
CompletableFuture<Boolean> sequence = IrisToolbelt.evacuateAsync(world)
|
||||
.thenCompose(evacuated -> {
|
||||
if (terminalTimeout.get()) {
|
||||
return CompletableFuture.failedFuture(new TimeoutException(
|
||||
"World unload stopped after its terminal timeout."));
|
||||
}
|
||||
if (!Boolean.TRUE.equals(evacuated)) {
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
return WorldLifecycleService.get().unloadAsync(world, true);
|
||||
})
|
||||
.thenCompose(unloaded -> {
|
||||
if (terminalTimeout.get()) {
|
||||
return CompletableFuture.failedFuture(new TimeoutException(
|
||||
"World unload stopped after its terminal timeout."));
|
||||
}
|
||||
if (!Boolean.TRUE.equals(unloaded) || generator == null) {
|
||||
return CompletableFuture.completedFuture(Boolean.TRUE.equals(unloaded));
|
||||
}
|
||||
return generator.closeAsync().thenApply(ignored -> true);
|
||||
});
|
||||
guardUnloadCompletion(sequence, terminalTimeout, world.getName())
|
||||
.whenComplete((unloaded, throwable) -> {
|
||||
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
||||
lease.close();
|
||||
Runnable response = () -> reportUnloadResult(responseSender, world, unloaded, throwable);
|
||||
if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) {
|
||||
return;
|
||||
}
|
||||
J.s(response);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
||||
lease.close();
|
||||
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UNLOAD_WORLD_3, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
||||
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e);
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<Boolean> guardUnloadCompletion(
|
||||
CompletableFuture<Boolean> source,
|
||||
AtomicBoolean terminalTimeout,
|
||||
String worldName
|
||||
) {
|
||||
CompletableFuture<Boolean> guarded = new CompletableFuture<>();
|
||||
AtomicBoolean settled = new AtomicBoolean(false);
|
||||
source.whenComplete((unloaded, throwable) -> {
|
||||
if (!settled.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
if (throwable == null) {
|
||||
guarded.complete(Boolean.TRUE.equals(unloaded));
|
||||
} else {
|
||||
guarded.completeExceptionally(throwable);
|
||||
}
|
||||
});
|
||||
CompletableFuture.delayedExecutor(WORLD_UNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS).execute(() -> {
|
||||
if (!settled.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
terminalTimeout.set(true);
|
||||
TimeoutException timeout = new TimeoutException(
|
||||
"World unload did not settle within " + WORLD_UNLOAD_TIMEOUT_SECONDS
|
||||
+ " seconds for \"" + worldName + "\".");
|
||||
ServerConfigurator.restart("World unload timed out for \"" + worldName + "\".");
|
||||
guarded.completeExceptionally(timeout);
|
||||
});
|
||||
return guarded;
|
||||
}
|
||||
|
||||
private void reportUnloadResult(VolmitSender responseSender, World world, Boolean unloaded, Throwable throwable) {
|
||||
if (throwable != null) {
|
||||
responseSender.sendMessage(IrisLanguage.text(
|
||||
BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UNLOAD_WORLD_3,
|
||||
MessageArgument.untrusted("value", String.valueOf(throwable.getMessage()))
|
||||
));
|
||||
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", throwable);
|
||||
return;
|
||||
}
|
||||
if (Boolean.TRUE.equals(unloaded)) {
|
||||
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_WORLD_UNLOADED_SUCCESSFULLY));
|
||||
} else {
|
||||
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UNLOAD_WORLD_2));
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Load an Iris World", descriptionKey = "iris.director.commandiris.director.load_iris_world", origin = DirectorOrigin.PLAYER, sync = true, aliases = {"import"})
|
||||
public void loadWorld(
|
||||
@Param(
|
||||
description = "The name of the world to load",
|
||||
descriptionKey = "iris.director.commandiris.param.name_world_load",
|
||||
customHandler = ManagedWorldNameHandler.class)
|
||||
String world
|
||||
) {
|
||||
NamespacedKey worldKey;
|
||||
try {
|
||||
worldKey = IrisWorldStorage.managedKeyFromName(world);
|
||||
IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey);
|
||||
} catch (IllegalArgumentException failure) {
|
||||
sender().sendMessage(C.RED + failure.getMessage());
|
||||
return;
|
||||
}
|
||||
String logicalWorldName = IrisWorldStorage.logicalName(worldKey);
|
||||
boolean worldExists = doesWorldExist(logicalWorldName);
|
||||
|
||||
if (!worldExists) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_DOESNT_EXIST_ON_SERVER, MessageArgument.untrusted("logicalWorldName", logicalWorldName)));
|
||||
return;
|
||||
}
|
||||
|
||||
VolmitSender responseSender = sender();
|
||||
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_LOADING_WORLD, MessageArgument.untrusted("logicalWorldName", logicalWorldName)));
|
||||
Iris.instance.worldReconciler()
|
||||
.loadWorld(BUKKIT_YML, worldKey.toString())
|
||||
.whenComplete((result, failure) -> {
|
||||
Runnable response = () -> reportLoadWorldResult(
|
||||
responseSender,
|
||||
logicalWorldName,
|
||||
result,
|
||||
failure);
|
||||
if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) {
|
||||
return;
|
||||
}
|
||||
J.s(response);
|
||||
});
|
||||
}
|
||||
|
||||
private void reportLoadWorldResult(
|
||||
VolmitSender responseSender,
|
||||
String logicalWorldName,
|
||||
BukkitWorldReconciler.LoadResult result,
|
||||
Throwable failure
|
||||
) {
|
||||
if (failure != null) {
|
||||
responseSender.sendMessage(C.RED + "Failed to load Iris world \"" + logicalWorldName + "\": " + failure.getMessage());
|
||||
Iris.reportError("Failed to load Iris world \"" + logicalWorldName + "\".", failure);
|
||||
return;
|
||||
}
|
||||
if (result == null) {
|
||||
IllegalStateException missingResult = new IllegalStateException("World load completed without a result.");
|
||||
responseSender.sendMessage(C.RED + missingResult.getMessage());
|
||||
Iris.reportError("Failed to load Iris world \"" + logicalWorldName + "\".", missingResult);
|
||||
return;
|
||||
}
|
||||
if (result.succeeded()) {
|
||||
responseSender.sendMessage(IrisLanguage.text(
|
||||
BukkitCommandMessagesExtended.COMMAND_IRIS_LOADED_SUCCESSFULLY,
|
||||
MessageArgument.untrusted("logicalWorldName", logicalWorldName)));
|
||||
return;
|
||||
}
|
||||
|
||||
C color = result.status() == BukkitWorldReconciler.ReconciliationStatus.BUSY
|
||||
|| result.status() == BukkitWorldReconciler.ReconciliationStatus.RESTART_REQUIRED
|
||||
? C.YELLOW
|
||||
: C.RED;
|
||||
responseSender.sendMessage(color + result.message());
|
||||
Throwable resultFailure = result.failure();
|
||||
if (resultFailure != null
|
||||
&& result.status() != BukkitWorldReconciler.ReconciliationStatus.BUSY
|
||||
&& result.status() != BukkitWorldReconciler.ReconciliationStatus.RESTART_REQUIRED) {
|
||||
Iris.reportError("Failed to load Iris world \"" + logicalWorldName + "\".", resultFailure);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Evacuate an iris world", descriptionKey = "iris.director.commandiris.director.evacuate_iris_world", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void evacuate(
|
||||
@Param(description = "Evacuate the world", descriptionKey = "iris.director.commandiris.param.evacuate_world")
|
||||
World world
|
||||
) {
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_THIS_IS_NOT_IRIS_WORLD_IRIS_WORLDS_3, MessageArgument.untrusted("value", String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()))));
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_EVACUATING_WORLD, MessageArgument.untrusted("value", world.getName())));
|
||||
IrisToolbelt.evacuate(world);
|
||||
}
|
||||
|
||||
boolean doesWorldExist(String worldName) {
|
||||
NamespacedKey worldKey = IrisWorldStorage.managedKeyFromName(worldName);
|
||||
File levelRoot = IrisWorldStorage.levelRoot();
|
||||
return IrisWorldStorage.frozenDimensionRoot(
|
||||
Bukkit.getWorldContainer(),
|
||||
levelRoot,
|
||||
IrisWorldStorage.configuredWorldName(worldKey, levelRoot.getName()),
|
||||
worldKey
|
||||
).isPresent();
|
||||
}
|
||||
|
||||
public static class ManagedWorldNameHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
Set<String> options = new LinkedHashSet<>();
|
||||
for (World world : Bukkit.getWorlds()) {
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
continue;
|
||||
}
|
||||
options.add(IrisWorldStorage.logicalName(world));
|
||||
String startupName = configuredStartupName(world);
|
||||
if (startupName != null) {
|
||||
options.add(startupName);
|
||||
}
|
||||
}
|
||||
for (String identity : IrisWorlds.get().getWorlds().keySet()) {
|
||||
try {
|
||||
options.add(IrisWorldStorage.logicalName(WorldIdentity.parse(identity)));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
File namespace = new File(IrisWorldStorage.levelRoot(), "dimensions/iris");
|
||||
File[] diskWorlds = namespace.listFiles(File::isDirectory);
|
||||
if (diskWorlds != null) {
|
||||
for (File diskWorld : diskWorlds) {
|
||||
if (!Files.isSymbolicLink(diskWorld.toPath())
|
||||
&& diskWorld.getName().matches("[a-z0-9_-]+")) {
|
||||
options.add(diskWorld.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
return new KList<>(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* "iris worlds" prints the Bukkit startup name, so it has to be offered and accepted here too.
|
||||
*/
|
||||
private static String configuredStartupName(World world) {
|
||||
try {
|
||||
String configured = IrisWorldStorage.configuredWorldName(
|
||||
WorldIdentity.key(world),
|
||||
IrisWorldStorage.levelRoot().getName()
|
||||
);
|
||||
return configured.equals(world.getName()) ? configured : null;
|
||||
} catch (RuntimeException failure) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(String in, boolean force) throws DirectorParsingException {
|
||||
if (in == null || in.isBlank()) {
|
||||
throw new DirectorParsingException("World identifier cannot be empty");
|
||||
}
|
||||
return in.trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> type) {
|
||||
return type == String.class;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PackDimensionTypeHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
return packDimensionOptions(Iris.instance.getDataFolder("packs"));
|
||||
}
|
||||
|
||||
static KList<String> packDimensionOptions(File packsFolder) {
|
||||
Set<String> options = new LinkedHashSet<>();
|
||||
|
||||
for (File pack : PackDirectoryResolver.listVisiblePackDirectories(packsFolder)) {
|
||||
options.add(pack.getName());
|
||||
|
||||
try {
|
||||
IrisData data = IrisData.get(pack);
|
||||
for (String key : data.getDimensionLoader().getPossibleKeys()) {
|
||||
options.add(packDimensionOption(pack.getName(), key));
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Iris.warn("Failed to read dimension keys from pack %s: %s%s",
|
||||
pack.getName(),
|
||||
ex.getClass().getSimpleName(),
|
||||
ex.getMessage() == null ? "" : " - " + ex.getMessage());
|
||||
Iris.reportError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return new KList<>(options);
|
||||
}
|
||||
|
||||
static String packDimensionOption(String packName, String dimensionKey) {
|
||||
return packName.equalsIgnoreCase(dimensionKey)
|
||||
? packName
|
||||
: packName + ":" + dimensionKey;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ReplacementSeedHandler implements DirectorParameterHandler<Long> {
|
||||
@Override
|
||||
public KList<Long> getPossibilities() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Long value) {
|
||||
return value == null ? PRESERVE_REPLACEMENT_SEED : Long.toString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long parse(String in, boolean force) throws DirectorParsingException {
|
||||
String seed = in == null ? "" : in.trim();
|
||||
if (PRESERVE_REPLACEMENT_SEED.equalsIgnoreCase(seed)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(seed);
|
||||
} catch (NumberFormatException failure) {
|
||||
throw new DirectorParsingException("Seed must be a signed 64-bit integer");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> type) {
|
||||
return type == Long.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRandomDefault() {
|
||||
return "1337";
|
||||
}
|
||||
}
|
||||
|
||||
public static class DownloadPackHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
return new KList<>(PackDownloader.builtInPacks());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(String in, boolean force) throws DirectorParsingException {
|
||||
if (NO_DOWNLOAD_SOURCE.equals(in)) {
|
||||
return null;
|
||||
}
|
||||
String pack = in == null ? "" : in.trim().toLowerCase(Locale.ROOT);
|
||||
if (!PackDownloader.isBuiltInPack(pack)) {
|
||||
throw new DirectorParsingException("Pack must be 'overworld' or 'underworld'");
|
||||
}
|
||||
return pack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> type) {
|
||||
return type == String.class;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,815 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.studio.object.StudioOpenCoordinator;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawPlanarArchetype;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioActivation;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCompatibilityTarget;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphMapper;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioLayout;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioMode;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioProjectCreator;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioSession;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioVariant;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphEvaluation;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.structure.StructureImporter;
|
||||
import art.arcane.iris.structure.VillageImporter;
|
||||
import art.arcane.iris.structure.authoring.StructureKey;
|
||||
import art.arcane.iris.structure.authoring.StructureWriteResult;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportSource;
|
||||
import art.arcane.iris.structure.graph.PlanarJigsawWorkcellResolver;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.structure.jigsaw.IrisJigsawCompatibility;
|
||||
import art.arcane.iris.structure.jigsaw.IrisJigsawPiece;
|
||||
import art.arcane.iris.structure.jigsaw.IrisJigsawThemeSet;
|
||||
import art.arcane.iris.structure.jigsaw.IrisJigsawWorkcellArchetype;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
import art.arcane.iris.structure.placement.IrisStructure;
|
||||
import art.arcane.iris.command.specialhandlers.IrisStructureHandler;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.DirectorParameterHandler;
|
||||
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 org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Director(name = "jigsaw", aliases = {"jig", "jgs"}, description = "Iris Jigsaw Studio tools",
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsaw implements DirectorExecutor {
|
||||
static final long DEFAULT_STUDIO_SEED = 1337L;
|
||||
static final StudioOpenCoordinator.StudioOpenKind STUDIO_OPEN_KIND =
|
||||
StudioOpenCoordinator.StudioOpenKind.JIGSAW;
|
||||
static final Map<UUID, SessionBinding> PLAYER_PACKS = new ConcurrentHashMap<>();
|
||||
private CommandJigsawPiece piece;
|
||||
private CommandJigsawPool pool;
|
||||
private CommandJigsawConnector connector;
|
||||
private CommandJigsawVariant variant;
|
||||
private CommandJigsawWorkcell workcell;
|
||||
private CommandJigsawRules rules;
|
||||
private CommandJigsawPreview preview;
|
||||
private CommandJigsawAdopt adopt;
|
||||
|
||||
@Director(description = "Create and open a new atomic Jigsaw Studio project", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void create(
|
||||
@Param(description = "Pack dimension") IrisDimension dimension,
|
||||
@Param(name = "key", aliases = {"structure", "name"},
|
||||
description = "New key written as structures/<key>.json") String structure,
|
||||
@Param(description = "planar or spatial", defaultValue = "planar",
|
||||
customHandler = JigsawModeHandler.class) String mode,
|
||||
@Param(description = "iris or vanilla", defaultValue = "iris",
|
||||
customHandler = JigsawCompatibilityHandler.class) String compatibility,
|
||||
@Param(description = "Cell width", defaultValue = "15") int width,
|
||||
@Param(description = "Cell height", defaultValue = "15") int height,
|
||||
@Param(description = "Cell depth", defaultValue = "15") int depth,
|
||||
@Param(description = "Studio world seed", defaultValue = "1337") long seed
|
||||
) {
|
||||
IrisData data = requireData(dimension);
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
JigsawStudioMode studioMode;
|
||||
JigsawStudioCompatibilityTarget target;
|
||||
JigsawStudioCellDimensions dimensions;
|
||||
try {
|
||||
studioMode = parseMode(mode);
|
||||
target = parseCompatibility(compatibility);
|
||||
dimensions = new JigsawStudioCellDimensions(width, height, depth);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
sendError(exception.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
StructureWriteResult result;
|
||||
try {
|
||||
result = JigsawStudioProjectCreator.create(
|
||||
data.getDataFolder().toPath(),
|
||||
new JigsawStudioProjectCreator.Options(structure, studioMode, target, dimensions));
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
Iris.reportError("Failed to create Jigsaw Studio project '" + structure + "'.", exception);
|
||||
sendError("Jigsaw project creation failed: " + exception.getMessage());
|
||||
return;
|
||||
}
|
||||
if (!result.successful()) {
|
||||
sendError("Jigsaw project was not created: " + result.status());
|
||||
if (!result.conflicts().isEmpty()) {
|
||||
sendError(result.conflicts().getFirst().relativePath() + ": "
|
||||
+ result.conflicts().getFirst().reason());
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JigsawStudioService.clearAutosaveHistory(
|
||||
data.getDataFolder().toPath(),
|
||||
structure);
|
||||
} catch (IOException exception) {
|
||||
Iris.reportError("Failed to clear stale Jigsaw Studio history for '" + structure + "'.", exception);
|
||||
sendError("Jigsaw project was created, but stale autosave history could not be cleared: "
|
||||
+ exception.getMessage());
|
||||
return;
|
||||
}
|
||||
data.invalidateStructureResources();
|
||||
sender().sendMessage(C.GREEN + "Created Jigsaw project '" + structure + "' atomically.");
|
||||
open(dimension, structure, seed);
|
||||
}
|
||||
|
||||
@Director(description = "Import a registered vanilla or datapack jigsaw graph into Iris Studio",
|
||||
aliases = {"import", "import-vanilla"}, sync = true, origin = DirectorOrigin.PLAYER)
|
||||
public void convert(
|
||||
@Param(description = "Pack dimension") IrisDimension dimension,
|
||||
@Param(description = "Registered structure key such as minecraft:village_plains") String source,
|
||||
@Param(description = "New Iris structure key, or auto", defaultValue = "auto") String target,
|
||||
@Param(description = "Studio world seed", defaultValue = "1337") long seed
|
||||
) {
|
||||
if (studioIsActiveOrOpening()) {
|
||||
sendError("Close the active or opening Jigsaw Studio before importing another graph.");
|
||||
return;
|
||||
}
|
||||
IrisData data = requireData(dimension);
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
NamespacedKey sourceKey;
|
||||
String targetKey;
|
||||
try {
|
||||
sourceKey = parseRegisteredStructureKey(source);
|
||||
targetKey = resolveConversionTarget(sourceKey, target);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
sendError(exception.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
VillageImporter.Result result = VillageImporter.importVillage(
|
||||
data,
|
||||
sourceKey,
|
||||
targetKey,
|
||||
StructureImporter.Mode.ADD_ONLY);
|
||||
if (!result.success()) {
|
||||
sendError(conversionFailureMessage(result));
|
||||
return;
|
||||
}
|
||||
data.invalidateStructureResources();
|
||||
VolmitSender commandSender = sender();
|
||||
commandSender.sendMessage(C.GREEN + result.message());
|
||||
CommandJigsawOpen.openProject(player(), commandSender, dimension, targetKey, seed);
|
||||
}
|
||||
|
||||
@Director(description = "Open an existing Iris jigsaw graph in transient Studio",
|
||||
aliases = {"edit", "reopen"}, sync = true, origin = DirectorOrigin.PLAYER)
|
||||
public void open(
|
||||
@Param(description = "Pack dimension") IrisDimension dimension,
|
||||
@Param(name = "key", aliases = {"structure", "name"},
|
||||
description = "Existing key loaded from structures/<key>.json",
|
||||
customHandler = IrisStructureHandler.class) String structure,
|
||||
@Param(description = "Studio world seed", defaultValue = "1337") long seed
|
||||
) {
|
||||
CommandJigsawOpen.openProject(player(), sender(), dimension, structure, seed);
|
||||
}
|
||||
|
||||
@Director(description = "Close the active Jigsaw Studio", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void close(
|
||||
@Param(description = "Discard unsaved Studio layout changes", defaultValue = "false") boolean discard
|
||||
) {
|
||||
Player targetPlayer = player();
|
||||
ActiveContext context = active(targetPlayer);
|
||||
if (context == null) {
|
||||
sendError("You do not have an active Jigsaw Studio session.");
|
||||
return;
|
||||
}
|
||||
if (context.session().isDirty() && !discard) {
|
||||
sendError("This Jigsaw Studio is waiting for autosave. Wait for it to finish or run close with discard=true.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService studioService = JigsawStudioService.get();
|
||||
UUID requestId = context.request().requestId();
|
||||
JigsawStudioService.CloseStart closeStart = studioService.tryBeginClose(
|
||||
requestId,
|
||||
targetPlayer.getUniqueId(),
|
||||
discard);
|
||||
if (closeStart != JigsawStudioService.CloseStart.STARTED) {
|
||||
sendError(closeStart == JigsawStudioService.CloseStart.SAVE_IN_PROGRESS
|
||||
? "This Jigsaw Studio is saving. Wait for the save to finish before closing it."
|
||||
: closeStart == JigsawStudioService.CloseStart.OPERATION_IN_PROGRESS
|
||||
? "This Jigsaw Studio is loading a variant. Wait for it to finish before closing it."
|
||||
: closeStart == JigsawStudioService.CloseStart.DIRTY
|
||||
? "This Jigsaw Studio is waiting for autosave. Wait for it to finish or run close with discard=true."
|
||||
: closeStart == JigsawStudioService.CloseStart.NOT_OWNER
|
||||
? "This Jigsaw Studio is owned by another player session."
|
||||
: "This Jigsaw Studio session is no longer active.");
|
||||
return;
|
||||
}
|
||||
VolmitSender commandSender = sender();
|
||||
try {
|
||||
Iris.service(StudioSVC.class).close().whenComplete((result, throwable) -> J.s(() -> {
|
||||
if (throwable != null || result == null || result.failureCause() != null) {
|
||||
studioService.cancelClose(requestId);
|
||||
Throwable failure = throwable != null ? throwable : result == null ? null : result.failureCause();
|
||||
commandSender.sendMessage(C.RED + "Jigsaw Studio close failed: "
|
||||
+ (failure == null ? "unknown failure" : failure.getMessage()));
|
||||
return;
|
||||
}
|
||||
JigsawStudioActivation.deactivate(context.packKey(), requestId);
|
||||
PLAYER_PACKS.remove(targetPlayer.getUniqueId(),
|
||||
new SessionBinding(context.packKey(), requestId));
|
||||
commandSender.sendMessage(C.GREEN + "Jigsaw Studio closed.");
|
||||
}));
|
||||
} catch (Throwable exception) {
|
||||
studioService.cancelClose(requestId);
|
||||
Iris.reportError("Failed to close Jigsaw Studio '" + context.request().structureKey() + "'.", exception);
|
||||
sendError("Jigsaw Studio close failed: " + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Director(name = "delete", aliases = "remove", description = "Delete the active owned jigsaw project",
|
||||
sync = true, origin = DirectorOrigin.PLAYER)
|
||||
public void delete(
|
||||
@Param(description = "Confirm permanent project deletion", defaultValue = "false") boolean confirm
|
||||
) {
|
||||
if (requireActive() == null) {
|
||||
return;
|
||||
}
|
||||
if (!confirm) {
|
||||
sendError("Project deletion removes every resource owned by this jigsaw. Run again with confirm=true.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().deleteProject(player());
|
||||
}
|
||||
|
||||
@Director(description = "Show active Jigsaw Studio and dynamic evaluation state", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void status() {
|
||||
ActiveContext context = requireActive();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
JigsawStudioLayout layout = context.session().layout();
|
||||
String selection = context.session().selectedBayId().orElse("none");
|
||||
JigsawStudioVariant activeVariant = "none".equals(selection)
|
||||
? null
|
||||
: context.session().activeVariant(selection).orElse(null);
|
||||
JigsawStudioBay selectedBay = "none".equals(selection) ? null : layout.get(selection);
|
||||
JigsawStudioCellDimensions selectedDimensions = selectedBay == null
|
||||
? null
|
||||
: selectedBay.bounds().dimensions();
|
||||
Optional<JigsawStudioGraphEvaluation> evaluation = JigsawStudioService.get().evaluation(player());
|
||||
sender().sendMessage(C.AQUA + "Jigsaw Studio: " + context.request().structureKey());
|
||||
sender().sendMessage(C.GRAY + "mode=" + layout.mode()
|
||||
+ ", compatibility=" + context.request().compatibilityTarget()
|
||||
+ ", defaultCell=" + layout.cellDimensions().width() + "x"
|
||||
+ layout.cellDimensions().height() + "x" + layout.cellDimensions().depth()
|
||||
+ ", workcells=" + layout.bays().size()
|
||||
+ ", variants=" + layout.variantCatalog().size());
|
||||
sender().sendMessage(C.GRAY + "selected=" + selection
|
||||
+ ", activeVariant=" + (activeVariant == null ? "none" : activeVariant.pieceKey())
|
||||
+ ", size=" + (selectedDimensions == null ? "none" : selectedDimensions.width() + "x"
|
||||
+ selectedDimensions.height() + "x" + selectedDimensions.depth())
|
||||
+ ", enabled=" + (selectedBay == null ? "n/a" : selectedBay.enabled())
|
||||
+ ", autosavePending=" + context.session().isDirty());
|
||||
if (evaluation.isPresent()) {
|
||||
JigsawStudioGraphEvaluation current = evaluation.get();
|
||||
sender().sendMessage(C.GRAY + "evaluation=" + current.state()
|
||||
+ ", seed=" + current.seed()
|
||||
+ ", theme=" + (current.selectedTheme().isBlank() ? "unthemed" : current.selectedTheme())
|
||||
+ ", pieces=" + current.pieceCount()
|
||||
+ ", detail=" + current.detail());
|
||||
} else {
|
||||
sender().sendMessage(C.GRAY + "evaluation=PENDING, seed=" + DEFAULT_STUDIO_SEED);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Open the Jigsaw Studio workcell and variant menu", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void menu() {
|
||||
if (requireActive() != null) {
|
||||
JigsawStudioService.get().openControlMenu(player());
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Select the Jigsaw Studio workcell containing you", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void select() {
|
||||
ActiveContext context = requireActive();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
Location location = player().getLocation();
|
||||
JigsawStudioBay bay = context.session().layout().findAt(
|
||||
location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
||||
if (bay == null) {
|
||||
sendError("Stand inside a Jigsaw Studio workcell before selecting it.");
|
||||
return;
|
||||
}
|
||||
context.session().selectBay(bay.stableId());
|
||||
sender().sendMessage(C.GREEN + "Selected " + bay.stableId() + " (" + bay.kind() + ").");
|
||||
}
|
||||
|
||||
@Director(name = "bounds", aliases = {"cell", "resize"},
|
||||
description = "Set the selected Studio workcell capacity",
|
||||
sync = true, origin = DirectorOrigin.PLAYER)
|
||||
public void bounds(
|
||||
@Param(description = "Cell width") int width,
|
||||
@Param(description = "Cell height") int height,
|
||||
@Param(description = "Cell depth") int depth
|
||||
) {
|
||||
ActiveContext context = requireActive();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay selected = selectedBay(context);
|
||||
if (selected == null) {
|
||||
sendError("Select a Jigsaw Studio workcell before changing its capacity.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioCellDimensions dimensions;
|
||||
try {
|
||||
dimensions = new JigsawStudioCellDimensions(width, height, depth);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
sendError(exception.getMessage());
|
||||
return;
|
||||
}
|
||||
if (dimensions.equals(selected.capacity())) {
|
||||
sender().sendMessage(C.YELLOW + "Workcell capacity is unchanged.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().updateWorkcellDimensions(player(), selected.stableId(), dimensions);
|
||||
}
|
||||
|
||||
@Director(description = "Flush the automatic save for a workcell now", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void save(
|
||||
@Param(description = "Workcell ID or selected", defaultValue = "selected") String bay
|
||||
) {
|
||||
ActiveContext context = requireActive();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
if ("selected".equalsIgnoreCase(bay)) {
|
||||
JigsawStudioBay selected = selectedBay(context);
|
||||
if (selected == null) {
|
||||
sendError("Select a Jigsaw Studio workcell before flushing its autosave.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().flushAutosave(player(), selected.stableId());
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().flushAutosave(player(), bay);
|
||||
}
|
||||
|
||||
@Director(name = "goto", aliases = "teleport", description = "Select and teleport to a Studio workcell",
|
||||
sync = true, origin = DirectorOrigin.PLAYER)
|
||||
public void gotoBay(
|
||||
@Param(description = "Workcell ID") String bay
|
||||
) {
|
||||
if (requireActive() != null) {
|
||||
JigsawStudioService.get().teleportTo(player(), bay);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Enable or disable Jigsaw Studio particles", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void particles(
|
||||
@Param(description = "Whether particles are visible") boolean visible
|
||||
) {
|
||||
if (requireActive() != null) {
|
||||
JigsawStudioService.get().setParticles(player(), visible);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Export the clean active graph as a vanilla datapack", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void export(
|
||||
@Param(description = "Datapack namespace", defaultValue = "iris") String namespace,
|
||||
@Param(description = "Output name under the Studio exports folder", defaultValue = "jigsaw-export")
|
||||
String output,
|
||||
@Param(description = "directory or zip", defaultValue = "zip") String format,
|
||||
@Param(description = "Replace an existing export", defaultValue = "false") boolean replace
|
||||
) {
|
||||
ActiveContext context = requireActive();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
if (context.session().isDirty()) {
|
||||
sendError("Wait for the pending autosave or discard the edits before exporting the on-disk graph.");
|
||||
return;
|
||||
}
|
||||
VanillaJigsawExportFormat exportFormat;
|
||||
try {
|
||||
exportFormat = CommandJigsawExports.parseFormat(format);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
sendError(exception.getMessage());
|
||||
return;
|
||||
}
|
||||
Path exportRoot = new File(Iris.service(StudioSVC.class).getWorkspaceFolder(), "exports")
|
||||
.toPath().toAbsolutePath().normalize();
|
||||
Path destination;
|
||||
try {
|
||||
destination = CommandJigsawExports.resolveExportDestination(exportRoot, output, exportFormat);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
sendError(exception.getMessage());
|
||||
return;
|
||||
}
|
||||
VanillaJigsawExportRequest request;
|
||||
try {
|
||||
request = VanillaJigsawExportRequest.builder(
|
||||
VanillaJigsawExportSource.forData(
|
||||
context.request().source(), context.request().structureKey()), destination)
|
||||
.namespace(namespace)
|
||||
.resourcePath(context.request().structureKey())
|
||||
.format(exportFormat)
|
||||
.replaceExisting(replace)
|
||||
.build();
|
||||
} catch (IllegalArgumentException exception) {
|
||||
sendError(exception.getMessage());
|
||||
return;
|
||||
}
|
||||
Player targetPlayer = player();
|
||||
UUID playerId = targetPlayer.getUniqueId();
|
||||
UUID requestId = context.request().requestId();
|
||||
JigsawStudioService studioService = JigsawStudioService.get();
|
||||
JigsawStudioService.ExportStart exportStart = studioService.tryBeginExport(requestId, playerId);
|
||||
if (exportStart != JigsawStudioService.ExportStart.STARTED) {
|
||||
sendError(CommandJigsawExports.exportStartError(exportStart));
|
||||
return;
|
||||
}
|
||||
if (!CommandJigsawExports.beginExport(playerId, destination)) {
|
||||
studioService.finishExport(requestId);
|
||||
sendError("A vanilla Jigsaw export for this player or output is already in progress.");
|
||||
return;
|
||||
}
|
||||
CommandJigsawExports.ExportLease lease = new CommandJigsawExports.ExportLease(() -> {
|
||||
CommandJigsawExports.finishExport(playerId, destination);
|
||||
studioService.finishExport(requestId);
|
||||
});
|
||||
CommandJigsawExports.ExportOperation operation = new CommandJigsawExports.ExportOperation(
|
||||
targetPlayer,
|
||||
sender(),
|
||||
requestId,
|
||||
context.request().structureKey(),
|
||||
request,
|
||||
lease);
|
||||
sender().sendMessage(C.GRAY + "Exporting '" + operation.structureKey()
|
||||
+ "' in the background to " + destination + "...");
|
||||
try {
|
||||
J.a(() -> CommandJigsawExports.runExport(operation));
|
||||
} catch (RuntimeException exception) {
|
||||
operation.lease().release();
|
||||
Iris.reportError("Failed to schedule vanilla Jigsaw export for '"
|
||||
+ operation.structureKey() + "'.", exception);
|
||||
sendError("Vanilla export could not be scheduled: " + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
static boolean studioIsActiveOrOpening() {
|
||||
return JigsawStudioActivation.activeOwnerId() != null
|
||||
|| JigsawStudioActivation.openingOwnerId() != null;
|
||||
}
|
||||
|
||||
static ActiveContext active(Player player) {
|
||||
if (player == null) {
|
||||
return null;
|
||||
}
|
||||
SessionBinding binding = PLAYER_PACKS.get(player.getUniqueId());
|
||||
if (binding == null) {
|
||||
return null;
|
||||
}
|
||||
ActiveContext context = context(binding);
|
||||
if (context == null) {
|
||||
PLAYER_PACKS.remove(player.getUniqueId(), binding);
|
||||
return null;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
private static ActiveContext context(SessionBinding binding) {
|
||||
JigsawStudioActivation.Request request = JigsawStudioActivation.getRequest(binding.packKey());
|
||||
JigsawStudioSession session = JigsawStudioActivation.getSession(binding.packKey());
|
||||
if (request == null || session == null
|
||||
|| !binding.requestId().equals(request.requestId())
|
||||
|| !request.requestId().equals(session.sessionId())) {
|
||||
return null;
|
||||
}
|
||||
return new ActiveContext(binding.packKey(), request, session);
|
||||
}
|
||||
|
||||
static ActiveContext findDirtyContext() {
|
||||
for (SessionBinding binding : PLAYER_PACKS.values()) {
|
||||
JigsawStudioActivation.Request request = JigsawStudioActivation.getRequest(binding.packKey());
|
||||
JigsawStudioSession session = JigsawStudioActivation.getSession(binding.packKey());
|
||||
if (request != null && session != null
|
||||
&& binding.requestId().equals(request.requestId())
|
||||
&& session.isDirty()) {
|
||||
return new ActiveContext(binding.packKey(), request, session);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ActiveContext requireActive() {
|
||||
ActiveContext context = active(player());
|
||||
if (context == null) {
|
||||
sendError("Open a Jigsaw Studio project first.");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
private IrisData requireData(IrisDimension dimension) {
|
||||
return requireData(dimension, sender());
|
||||
}
|
||||
|
||||
static IrisData requireData(IrisDimension dimension, VolmitSender commandSender) {
|
||||
IrisData data = dimension == null ? null : dimension.getLoader();
|
||||
if (data == null) {
|
||||
sendError(commandSender, "Could not resolve the requested Iris pack dimension.");
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private void sendError(String message) {
|
||||
sender().sendMessage(C.RED + message);
|
||||
}
|
||||
|
||||
static void sendError(VolmitSender commandSender, String message) {
|
||||
commandSender.sendMessage(C.RED + message);
|
||||
}
|
||||
|
||||
private static JigsawStudioMode parseMode(String value) {
|
||||
return switch (value.trim().toLowerCase(Locale.ROOT)) {
|
||||
case "planar", "2d", "planar_jigsaw" -> JigsawStudioMode.PLANAR_JIGSAW;
|
||||
case "spatial", "3d", "spatial_jigsaw" -> JigsawStudioMode.SPATIAL_JIGSAW;
|
||||
default -> throw new IllegalArgumentException("Mode must be planar or spatial.");
|
||||
};
|
||||
}
|
||||
|
||||
private static JigsawStudioCompatibilityTarget parseCompatibility(String value) {
|
||||
return switch (value.trim().toLowerCase(Locale.ROOT)) {
|
||||
case "extended", "iris", "iris_extended" -> JigsawStudioCompatibilityTarget.IRIS_EXTENDED;
|
||||
case "vanilla", "portable", "vanilla_portable" -> JigsawStudioCompatibilityTarget.VANILLA_PORTABLE;
|
||||
default -> throw new IllegalArgumentException("Compatibility must be iris or vanilla.");
|
||||
};
|
||||
}
|
||||
|
||||
static JigsawStudioCompatibilityTarget compatibilityOf(IrisStructure structure) {
|
||||
return structure.resolvedCompatibility() == IrisJigsawCompatibility.VANILLA_PORTABLE
|
||||
? JigsawStudioCompatibilityTarget.VANILLA_PORTABLE
|
||||
: JigsawStudioCompatibilityTarget.IRIS_EXTENDED;
|
||||
}
|
||||
|
||||
static NamespacedKey parseRegisteredStructureKey(String value) {
|
||||
String normalized = Objects.requireNonNull(value, "Registered structure key").trim()
|
||||
.toLowerCase(Locale.ROOT);
|
||||
NamespacedKey key = NamespacedKey.fromString(normalized);
|
||||
if (key == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Registered structure source must use a valid namespace:path key.");
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
static String resolveConversionTarget(NamespacedKey source, String value) {
|
||||
Objects.requireNonNull(source, "Registered structure key");
|
||||
String normalized = Objects.requireNonNull(value, "Conversion target").trim()
|
||||
.toLowerCase(Locale.ROOT);
|
||||
if (normalized.isEmpty() || "auto".equals(normalized)) {
|
||||
return StructureImporter.deriveName(source);
|
||||
}
|
||||
StructureKey target = StructureKey.parse(normalized, "iris");
|
||||
if (!"iris".equals(target.namespace())) {
|
||||
throw new IllegalArgumentException("Converted Studio targets must use the iris namespace.");
|
||||
}
|
||||
return target.path();
|
||||
}
|
||||
|
||||
static String conversionFailureMessage(VillageImporter.Result result) {
|
||||
String message = Objects.requireNonNull(result, "Village import result").message();
|
||||
String obsoleteInstruction = "; use 'import' for single-template structures";
|
||||
int instructionIndex = message.indexOf(obsoleteInstruction);
|
||||
if (instructionIndex < 0) {
|
||||
return message;
|
||||
}
|
||||
return message.substring(0, instructionIndex)
|
||||
+ ". This Studio converter accepts registered jigsaw structures only.";
|
||||
}
|
||||
|
||||
static JigsawStudioBay selectedBay(ActiveContext context) {
|
||||
String selected = context.session().selectedBayId().orElse(null);
|
||||
return selected == null ? null : context.session().layout().get(selected);
|
||||
}
|
||||
|
||||
static JigsawStudioVariant selectedVariant(ActiveContext context) {
|
||||
JigsawStudioBay selected = selectedBay(context);
|
||||
return selected == null
|
||||
? null
|
||||
: context.session().activeVariant(selected.stableId()).orElse(null);
|
||||
}
|
||||
|
||||
static boolean runGraphMutation(
|
||||
Player player,
|
||||
ActiveContext context,
|
||||
JigsawStudioService.CommandGraphMutation mutation
|
||||
) {
|
||||
return JigsawStudioService.get().runCommandGraphMutation(
|
||||
player,
|
||||
context.request().requestId(),
|
||||
mutation);
|
||||
}
|
||||
|
||||
static JigsawStudioLayout loadPersistedLayout(ActiveContext context) throws IOException {
|
||||
IrisData source = context.request().source();
|
||||
source.invalidateStructureResources();
|
||||
IrisStructure structure = source.load(IrisStructure.class, context.request().structureKey(), false);
|
||||
if (structure == null) {
|
||||
throw new IOException("The structure could not be reloaded after the graph transaction");
|
||||
}
|
||||
return JigsawStudioGraphMapper.map(source, structure);
|
||||
}
|
||||
|
||||
static JigsawStudioService.CommandGraphMutationResult mappedMutationResult(
|
||||
ActiveContext context,
|
||||
String activateWorkcellId,
|
||||
String activatePieceKey,
|
||||
String message
|
||||
) throws IOException {
|
||||
return new JigsawStudioService.CommandGraphMutationResult(
|
||||
loadPersistedLayout(context),
|
||||
activateWorkcellId,
|
||||
activatePieceKey,
|
||||
message);
|
||||
}
|
||||
|
||||
static String workcellIdForVariant(JigsawStudioLayout layout, String pieceKey) throws IOException {
|
||||
JigsawStudioVariant variant = layout.variantCatalog().find(pieceKey).orElse(null);
|
||||
if (variant == null) {
|
||||
throw new IOException("Persisted variant '" + pieceKey + "' did not map to a Studio workcell");
|
||||
}
|
||||
return variant.archetype()
|
||||
.map(archetype -> archetype.stableId())
|
||||
.orElse(JigsawStudioLayout.SPATIAL_WORKCELL_ID);
|
||||
}
|
||||
|
||||
static PieceWorkcellResolution resolvePieceWorkcell(
|
||||
JigsawStudioLayout layout,
|
||||
IrisJigsawPiece piece,
|
||||
IrisObject object
|
||||
) {
|
||||
JigsawStudioLayout activeLayout = Objects.requireNonNull(layout, "Jigsaw Studio layout");
|
||||
IrisJigsawPiece activePiece = Objects.requireNonNull(piece, "Jigsaw piece");
|
||||
IrisObject activeObject = Objects.requireNonNull(object, "Jigsaw piece object");
|
||||
String workcellId;
|
||||
IrisPosition requiredDimensions;
|
||||
if (activeLayout.mode() == JigsawStudioMode.PLANAR_JIGSAW) {
|
||||
IrisJigsawWorkcellArchetype modelArchetype = IrisJigsawWorkcellArchetype.fromPiece(activePiece);
|
||||
workcellId = JigsawPlanarArchetype.fromModel(modelArchetype).stableId();
|
||||
requiredDimensions = PlanarJigsawWorkcellResolver.canonicalDimensions(activePiece, activeObject);
|
||||
} else {
|
||||
workcellId = JigsawStudioLayout.SPATIAL_WORKCELL_ID;
|
||||
requiredDimensions = new IrisPosition(
|
||||
activeObject.getW(),
|
||||
activeObject.getH(),
|
||||
activeObject.getD());
|
||||
}
|
||||
JigsawStudioBay workcell = activeLayout.get(workcellId);
|
||||
if (workcell == null) {
|
||||
throw new IllegalArgumentException("Workcell '" + workcellId + "' is not configured");
|
||||
}
|
||||
return new PieceWorkcellResolution(workcell, requiredDimensions);
|
||||
}
|
||||
|
||||
static String nextThemeKey(ActiveContext context) throws IOException {
|
||||
IrisStructure structure = context.request().source().load(
|
||||
IrisStructure.class,
|
||||
context.request().structureKey(),
|
||||
false);
|
||||
if (structure == null) {
|
||||
throw new IOException("The active jigsaw structure could not be loaded");
|
||||
}
|
||||
Set<String> declared = new HashSet<>();
|
||||
if (structure.getThemeSets() != null) {
|
||||
for (IrisJigsawThemeSet themeSet : structure.getThemeSets()) {
|
||||
if (themeSet != null && themeSet.getKey() != null) {
|
||||
declared.add(themeSet.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
int candidate = 1;
|
||||
while (candidate > 0) {
|
||||
String key = "variant-" + candidate;
|
||||
if (!declared.contains(key)) {
|
||||
return key;
|
||||
}
|
||||
candidate = Math.incrementExact(candidate);
|
||||
}
|
||||
throw new IOException("Jigsaw Studio cannot allocate another numbered family key");
|
||||
}
|
||||
|
||||
static void sendError(DirectorExecutor executor, String message) {
|
||||
executor.sender().sendMessage(C.RED + message);
|
||||
}
|
||||
|
||||
public record ActiveContext(
|
||||
String packKey,
|
||||
JigsawStudioActivation.Request request,
|
||||
JigsawStudioSession session
|
||||
) {
|
||||
}
|
||||
|
||||
record PieceWorkcellResolution(
|
||||
JigsawStudioBay workcell,
|
||||
IrisPosition requiredDimensions
|
||||
) {
|
||||
PieceWorkcellResolution {
|
||||
workcell = Objects.requireNonNull(workcell, "Jigsaw Studio workcell");
|
||||
requiredDimensions = Objects.requireNonNull(requiredDimensions, "Jigsaw piece dimensions");
|
||||
}
|
||||
|
||||
boolean fits() {
|
||||
JigsawStudioCellDimensions capacity = workcell.capacity();
|
||||
return requiredDimensions.getX() <= capacity.width()
|
||||
&& requiredDimensions.getY() <= capacity.height()
|
||||
&& requiredDimensions.getZ() <= capacity.depth();
|
||||
}
|
||||
}
|
||||
|
||||
record SessionBinding(String packKey, UUID requestId) {
|
||||
}
|
||||
|
||||
public static final class JigsawModeHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
return new KList<>("planar", "spatial");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(String input, boolean force) throws DirectorParsingException {
|
||||
try {
|
||||
return parseMode(input) == JigsawStudioMode.PLANAR_JIGSAW ? "planar" : "spatial";
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new DirectorParsingException(exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> type) {
|
||||
return type == String.class;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class JigsawCompatibilityHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
return new KList<>("iris", "vanilla");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(String input, boolean force) throws DirectorParsingException {
|
||||
try {
|
||||
return parseCompatibility(input) == JigsawStudioCompatibilityTarget.IRIS_EXTENDED
|
||||
? "iris" : "vanilla";
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new DirectorParsingException(exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> type) {
|
||||
return type == String.class;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.structure.authoring.StructureKey;
|
||||
import art.arcane.iris.structure.authoring.StructureOwnershipManifest;
|
||||
import art.arcane.iris.structure.authoring.StructureTransactionWriter;
|
||||
import art.arcane.iris.structure.conversion.IrisStructureAdoptionDiagnostic;
|
||||
import art.arcane.iris.structure.conversion.IrisStructureAdoptionInputKind;
|
||||
import art.arcane.iris.structure.conversion.IrisStructureAdoptionPlan;
|
||||
import art.arcane.iris.structure.conversion.IrisStructureAdoptionRequest;
|
||||
import art.arcane.iris.structure.conversion.IrisStructureAdoptionResult;
|
||||
import art.arcane.iris.structure.conversion.IrisStructureAdoptionService;
|
||||
import art.arcane.iris.structure.conversion.IrisStructureAdoptionStrategy;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.DirectorParameterHandler;
|
||||
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 org.bukkit.entity.Player;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Director(name = "adopt", description = "Inspect and apply safe ownership adoption plans",
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsawAdopt implements DirectorExecutor {
|
||||
private static final Map<UUID, AdoptionPlanBinding> ADOPTION_PLANS = new ConcurrentHashMap<>();
|
||||
private static final Map<Path, IrisStructureAdoptionService> ADOPTION_SERVICES = new ConcurrentHashMap<>();
|
||||
|
||||
@Director(description = "Inspect an Iris graph for safe Studio ownership adoption", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void inspect(
|
||||
@Param(description = "Pack dimension") IrisDimension dimension,
|
||||
@Param(description = "Existing Iris structure key") String source,
|
||||
@Param(description = "New owned Iris key, or auto", defaultValue = "auto") String target,
|
||||
@Param(description = "auto, in-place, or clone", defaultValue = "auto",
|
||||
customHandler = JigsawAdoptionStrategyHandler.class) String strategy
|
||||
) {
|
||||
VolmitSender commandSender = sender();
|
||||
IrisData data = CommandJigsaw.requireData(dimension, commandSender);
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
String sourceKey;
|
||||
Optional<StructureKey> targetKey;
|
||||
IrisStructureAdoptionStrategy adoptionStrategy;
|
||||
Path root;
|
||||
try {
|
||||
sourceKey = parseAdoptionSource(source);
|
||||
targetKey = parseAdoptionTarget(target);
|
||||
adoptionStrategy = parseAdoptionStrategy(strategy);
|
||||
root = canonicalAdoptionRoot(data.getDataFolder().toPath());
|
||||
} catch (IOException | IllegalArgumentException exception) {
|
||||
CommandJigsaw.sendError(commandSender, "Could not inspect adoption source: " + exception.getMessage());
|
||||
return;
|
||||
}
|
||||
Player targetPlayer = player();
|
||||
UUID ownerId = targetPlayer.getUniqueId();
|
||||
commandSender.sendMessage(C.GRAY + "Inspecting Jigsaw adoption source '" + sourceKey
|
||||
+ "' asynchronously...");
|
||||
try {
|
||||
J.a(() -> runAdoptionInspect(
|
||||
targetPlayer,
|
||||
commandSender,
|
||||
ownerId,
|
||||
dimension,
|
||||
data,
|
||||
root,
|
||||
sourceKey,
|
||||
targetKey,
|
||||
adoptionStrategy));
|
||||
} catch (RuntimeException exception) {
|
||||
Iris.reportError("Failed to schedule Jigsaw adoption inspection for '"
|
||||
+ sourceKey + "'.", exception);
|
||||
CommandJigsaw.sendError(commandSender, "Jigsaw adoption inspection could not be scheduled: "
|
||||
+ exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Apply one inspected adoption plan without overwriting", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void apply(
|
||||
@Param(description = "Plan UUID returned by adopt inspect",
|
||||
customHandler = JigsawAdoptionPlanHandler.class) String planId
|
||||
) {
|
||||
VolmitSender commandSender = sender();
|
||||
UUID parsedPlanId;
|
||||
try {
|
||||
parsedPlanId = UUID.fromString(planId);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
CommandJigsaw.sendError(commandSender, "Adoption plan ID must be a UUID.");
|
||||
return;
|
||||
}
|
||||
if (CommandJigsaw.studioIsActiveOrOpening()) {
|
||||
CommandJigsaw.sendError(commandSender,
|
||||
"Close the active or opening Jigsaw Studio before applying an adoption plan.");
|
||||
return;
|
||||
}
|
||||
pruneAdoptionPlanBindings();
|
||||
AdoptionPlanBinding binding = ADOPTION_PLANS.get(parsedPlanId);
|
||||
UUID playerId = player().getUniqueId();
|
||||
if (binding == null || !binding.ownerId().equals(playerId)) {
|
||||
CommandJigsaw.sendError(commandSender, "No active adoption plan '" + parsedPlanId
|
||||
+ "' belongs to your player session. Run adopt inspect again.");
|
||||
return;
|
||||
}
|
||||
if (binding.service().plan(parsedPlanId).isEmpty()) {
|
||||
ADOPTION_PLANS.remove(parsedPlanId, binding);
|
||||
CommandJigsaw.sendError(commandSender, "Adoption plan '" + parsedPlanId
|
||||
+ "' expired or was already consumed. Run adopt inspect again.");
|
||||
return;
|
||||
}
|
||||
if (!ADOPTION_PLANS.remove(parsedPlanId, binding)) {
|
||||
CommandJigsaw.sendError(commandSender, "Adoption plan '" + parsedPlanId + "' is already being applied.");
|
||||
return;
|
||||
}
|
||||
Player targetPlayer = player();
|
||||
commandSender.sendMessage(C.GRAY + "Applying adoption plan '" + parsedPlanId
|
||||
+ "' asynchronously without overwrite...");
|
||||
try {
|
||||
J.a(() -> runAdoptionApply(targetPlayer, commandSender, parsedPlanId, binding));
|
||||
} catch (RuntimeException exception) {
|
||||
ADOPTION_PLANS.putIfAbsent(parsedPlanId, binding);
|
||||
Iris.reportError("Failed to schedule Jigsaw adoption plan '" + parsedPlanId + "'.", exception);
|
||||
CommandJigsaw.sendError(commandSender, "Jigsaw adoption could not be scheduled: " + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void pruneAdoptionPlanBindings() {
|
||||
for (Map.Entry<UUID, AdoptionPlanBinding> entry : ADOPTION_PLANS.entrySet()) {
|
||||
AdoptionPlanBinding binding = entry.getValue();
|
||||
if (binding.service().plan(entry.getKey()).isEmpty()) {
|
||||
ADOPTION_PLANS.remove(entry.getKey(), binding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void runAdoptionApply(
|
||||
Player targetPlayer,
|
||||
VolmitSender commandSender,
|
||||
UUID planId,
|
||||
AdoptionPlanBinding binding
|
||||
) {
|
||||
if (CommandJigsaw.studioIsActiveOrOpening()) {
|
||||
ADOPTION_PLANS.putIfAbsent(planId, binding);
|
||||
J.runEntity(targetPlayer, () -> CommandJigsaw.sendError(commandSender,
|
||||
"Close the active or opening Jigsaw Studio before applying an adoption plan."));
|
||||
return;
|
||||
}
|
||||
IrisStructureAdoptionResult result;
|
||||
Throwable failure = null;
|
||||
try {
|
||||
result = binding.service().apply(binding.plan());
|
||||
} catch (Throwable throwable) {
|
||||
result = null;
|
||||
failure = throwable;
|
||||
Iris.reportError("Failed to apply Jigsaw adoption plan '" + planId + "'.", throwable);
|
||||
}
|
||||
IrisStructureAdoptionResult completedResult = result;
|
||||
Throwable completedFailure = failure;
|
||||
J.runEntity(targetPlayer, () -> finishAdoptionApply(
|
||||
targetPlayer,
|
||||
commandSender,
|
||||
binding,
|
||||
completedResult,
|
||||
completedFailure));
|
||||
}
|
||||
|
||||
private static void runAdoptionInspect(
|
||||
Player targetPlayer,
|
||||
VolmitSender commandSender,
|
||||
UUID ownerId,
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
Path root,
|
||||
String sourceKey,
|
||||
Optional<StructureKey> targetKey,
|
||||
IrisStructureAdoptionStrategy strategy
|
||||
) {
|
||||
IrisStructureAdoptionService service = null;
|
||||
IrisStructureAdoptionPlan plan = null;
|
||||
Throwable failure = null;
|
||||
try {
|
||||
IrisStructureAdoptionInputKind inputKind = adoptionInputKind(root, sourceKey);
|
||||
service = ADOPTION_SERVICES.computeIfAbsent(root, IrisStructureAdoptionService::new);
|
||||
plan = service.inspect(new IrisStructureAdoptionRequest(
|
||||
sourceKey,
|
||||
targetKey,
|
||||
strategy,
|
||||
inputKind));
|
||||
} catch (Throwable throwable) {
|
||||
failure = throwable;
|
||||
Iris.reportError("Failed to inspect Jigsaw adoption source '" + sourceKey + "'.", throwable);
|
||||
}
|
||||
IrisStructureAdoptionService completedService = service;
|
||||
IrisStructureAdoptionPlan completedPlan = plan;
|
||||
Throwable completedFailure = failure;
|
||||
J.runEntity(targetPlayer, () -> finishAdoptionInspect(
|
||||
commandSender,
|
||||
ownerId,
|
||||
dimension,
|
||||
data,
|
||||
completedService,
|
||||
completedPlan,
|
||||
completedFailure));
|
||||
}
|
||||
|
||||
private static void finishAdoptionInspect(
|
||||
VolmitSender commandSender,
|
||||
UUID ownerId,
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
IrisStructureAdoptionService service,
|
||||
IrisStructureAdoptionPlan plan,
|
||||
Throwable failure
|
||||
) {
|
||||
if (failure != null || service == null || plan == null) {
|
||||
CommandJigsaw.sendError(commandSender, "Jigsaw adoption inspection failed: "
|
||||
+ (failure == null ? "no result" : failure.getMessage()));
|
||||
return;
|
||||
}
|
||||
pruneAdoptionPlanBindings();
|
||||
if (service.plan(plan.planId()).isPresent()) {
|
||||
ADOPTION_PLANS.put(plan.planId(), new AdoptionPlanBinding(
|
||||
ownerId, dimension, data, service, plan));
|
||||
}
|
||||
reportAdoptionPlan(commandSender, plan);
|
||||
}
|
||||
|
||||
private static void finishAdoptionApply(
|
||||
Player targetPlayer,
|
||||
VolmitSender commandSender,
|
||||
AdoptionPlanBinding binding,
|
||||
IrisStructureAdoptionResult result,
|
||||
Throwable failure
|
||||
) {
|
||||
if (failure != null || result == null) {
|
||||
CommandJigsaw.sendError(commandSender, "Jigsaw adoption failed: "
|
||||
+ (failure == null ? "no result" : failure.getMessage()));
|
||||
return;
|
||||
}
|
||||
reportAdoptionResult(commandSender, result);
|
||||
if (!result.successful()) {
|
||||
return;
|
||||
}
|
||||
StructureKey target = result.receipt().orElseThrow().targetStructure();
|
||||
binding.data().invalidateStructureResources();
|
||||
CommandJigsawOpen.openProject(targetPlayer, commandSender, binding.dimension(), target.path(), CommandJigsaw.DEFAULT_STUDIO_SEED);
|
||||
}
|
||||
|
||||
private static void reportAdoptionPlan(VolmitSender commandSender, IrisStructureAdoptionPlan plan) {
|
||||
C statusColor = plan.canApply() ? C.GREEN : C.RED;
|
||||
commandSender.sendMessage(statusColor + "Adoption plan " + plan.planId() + " -> "
|
||||
+ plan.disposition() + " for " + plan.targetStructure().value() + ".");
|
||||
commandSender.sendMessage(C.GRAY + "Resources=" + plan.resourceCount()
|
||||
+ ", bytes=" + plan.totalSourceBytes()
|
||||
+ ", errors=" + plan.errorCount()
|
||||
+ ", warnings=" + plan.warningCount() + ".");
|
||||
for (IrisStructureAdoptionDiagnostic diagnostic : plan.diagnostics()) {
|
||||
commandSender.sendMessage(adoptionColor(diagnostic).toString() + diagnostic.summary());
|
||||
}
|
||||
if (plan.canApply()) {
|
||||
commandSender.sendMessage(C.AQUA + "Apply with /iris jigsaw adopt apply " + plan.planId());
|
||||
}
|
||||
}
|
||||
|
||||
private static void reportAdoptionResult(VolmitSender commandSender, IrisStructureAdoptionResult result) {
|
||||
commandSender.sendMessage((result.successful() ? C.GREEN : C.RED)
|
||||
+ "Adoption plan " + result.planId() + ": " + result.status() + ".");
|
||||
for (IrisStructureAdoptionDiagnostic diagnostic : result.diagnostics()) {
|
||||
commandSender.sendMessage(adoptionColor(diagnostic).toString() + diagnostic.summary());
|
||||
}
|
||||
}
|
||||
|
||||
private static C adoptionColor(IrisStructureAdoptionDiagnostic diagnostic) {
|
||||
return switch (diagnostic.severity()) {
|
||||
case ERROR -> C.RED;
|
||||
case WARNING -> C.YELLOW;
|
||||
case INFO -> C.GRAY;
|
||||
};
|
||||
}
|
||||
|
||||
static String parseAdoptionSource(String value) {
|
||||
String normalized = Objects.requireNonNull(value, "Adoption source").trim()
|
||||
.toLowerCase(Locale.ROOT);
|
||||
StructureKey source = StructureKey.parse(normalized, "iris");
|
||||
if (!"iris".equals(source.namespace())) {
|
||||
throw new IllegalArgumentException("Adoption sources must use the iris namespace.");
|
||||
}
|
||||
return source.path();
|
||||
}
|
||||
|
||||
static Optional<StructureKey> parseAdoptionTarget(String value) {
|
||||
String normalized = Objects.requireNonNull(value, "Adoption target").trim()
|
||||
.toLowerCase(Locale.ROOT);
|
||||
if (normalized.isEmpty() || "auto".equals(normalized)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
StructureKey target = StructureKey.parse(normalized, "iris");
|
||||
if (!"iris".equals(target.namespace())) {
|
||||
throw new IllegalArgumentException("Adoption targets must use the iris namespace.");
|
||||
}
|
||||
return Optional.of(target);
|
||||
}
|
||||
|
||||
private static IrisStructureAdoptionStrategy parseAdoptionStrategy(String value) {
|
||||
return switch (Objects.requireNonNull(value, "Adoption strategy").trim().toLowerCase(Locale.ROOT)) {
|
||||
case "auto" -> IrisStructureAdoptionStrategy.AUTO;
|
||||
case "in-place", "in_place", "inplace", "claim" -> IrisStructureAdoptionStrategy.IN_PLACE;
|
||||
case "clone", "copy" -> IrisStructureAdoptionStrategy.CLONE;
|
||||
default -> throw new IllegalArgumentException("Adoption strategy must be auto, in-place, or clone.");
|
||||
};
|
||||
}
|
||||
|
||||
static IrisStructureAdoptionInputKind adoptionInputKind(Path packRoot, String source) throws IOException {
|
||||
Path root = canonicalAdoptionRoot(packRoot);
|
||||
StructureKey sourceKey = new StructureKey("iris", parseAdoptionSource(source));
|
||||
Path manifestPath = new StructureTransactionWriter(root).ownershipManifestPath(sourceKey);
|
||||
if (!Files.exists(manifestPath, LinkOption.NOFOLLOW_LINKS)) {
|
||||
return IrisStructureAdoptionInputKind.UNOWNED_IRIS;
|
||||
}
|
||||
if (!Files.isRegularFile(manifestPath, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("Ownership manifest is not a regular file: " + manifestPath);
|
||||
}
|
||||
StructureOwnershipManifest manifest;
|
||||
try {
|
||||
manifest = StructureOwnershipManifest.fromJson(Files.readAllBytes(manifestPath));
|
||||
} catch (RuntimeException exception) {
|
||||
throw new IOException("Invalid ownership manifest at " + manifestPath, exception);
|
||||
}
|
||||
if (!manifest.structure().equals(sourceKey)) {
|
||||
throw new IOException("Ownership manifest at " + manifestPath
|
||||
+ " belongs to " + manifest.structure().value());
|
||||
}
|
||||
return manifest.provenance().origin() == StructureOwnershipManifest.Origin.MANAGED_DATAPACK
|
||||
? IrisStructureAdoptionInputKind.MANAGED_DATAPACK
|
||||
: IrisStructureAdoptionInputKind.UNOWNED_IRIS;
|
||||
}
|
||||
|
||||
private static Path canonicalAdoptionRoot(Path packRoot) throws IOException {
|
||||
Path normalized = Objects.requireNonNull(packRoot, "Adoption pack root").toAbsolutePath().normalize();
|
||||
return Files.exists(normalized, LinkOption.NOFOLLOW_LINKS) ? normalized.toRealPath() : normalized;
|
||||
}
|
||||
|
||||
private record AdoptionPlanBinding(
|
||||
UUID ownerId,
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
IrisStructureAdoptionService service,
|
||||
IrisStructureAdoptionPlan plan
|
||||
) {
|
||||
}
|
||||
|
||||
public static final class JigsawAdoptionStrategyHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
return new KList<>("auto", "in-place", "clone");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(String input, boolean force) throws DirectorParsingException {
|
||||
try {
|
||||
return switch (parseAdoptionStrategy(input)) {
|
||||
case AUTO -> "auto";
|
||||
case IN_PLACE -> "in-place";
|
||||
case CLONE -> "clone";
|
||||
};
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new DirectorParsingException(exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> type) {
|
||||
return type == String.class;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class JigsawAdoptionPlanHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
return new KList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(String input, boolean force) throws DirectorParsingException {
|
||||
try {
|
||||
return UUID.fromString(input).toString();
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new DirectorParsingException("Adoption plan ID must be a UUID.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> type) {
|
||||
return type == String.class;
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphEditor;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioVariant;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
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.block.Block;
|
||||
import org.bukkit.block.data.type.Jigsaw;
|
||||
|
||||
@Director(name = "connector", description = "Inspect and configure targeted jigsaw connectors",
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsawConnector implements DirectorExecutor {
|
||||
@Director(description = "Set the Iris-only channel on the targeted jigsaw marker", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void channel(
|
||||
@Param(description = "Exact channel, or none to clear") String channel
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
Block target = player().getTargetBlockExact(8);
|
||||
if (target == null || !(target.getBlockData() instanceof Jigsaw)) {
|
||||
CommandJigsaw.sendError(this, "Look directly at a jigsaw marker within eight blocks.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay bay = context.session().layout().findAt(
|
||||
target.getX(), target.getY(), target.getZ());
|
||||
JigsawStudioVariant activeVariant = bay == null
|
||||
? null
|
||||
: context.session().activeVariant(bay.stableId()).orElse(null);
|
||||
if (activeVariant == null) {
|
||||
CommandJigsaw.sendError(this, "The targeted marker must be inside a workcell with an active variant.");
|
||||
return;
|
||||
}
|
||||
if (!activeVariant.owned()) {
|
||||
CommandJigsaw.sendError(this, "The active variant is read-only. Adopt it into this project before editing it.");
|
||||
return;
|
||||
}
|
||||
IrisObject object = context.request().source().getObjectLoader().load(activeVariant.objectKey());
|
||||
if (object == null) {
|
||||
CommandJigsaw.sendError(this, "The active variant object '" + activeVariant.objectKey() + "' is missing.");
|
||||
return;
|
||||
}
|
||||
IrisPosition canonicalPosition = new IrisPosition(
|
||||
target.getX() - bay.bounds().originX(),
|
||||
target.getY() - bay.bounds().originY(),
|
||||
target.getZ() - bay.bounds().originZ());
|
||||
IrisPosition position;
|
||||
try {
|
||||
position = activeVariant.canonicalToSourcePosition(
|
||||
canonicalPosition,
|
||||
new JigsawStudioCellDimensions(object.getW(), object.getH(), object.getD()));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
CommandJigsaw.sendError(this, "Connector channel update failed: " + exception.getMessage());
|
||||
return;
|
||||
}
|
||||
String normalized = channel == null || channel.isBlank() || "none".equalsIgnoreCase(channel)
|
||||
? "none"
|
||||
: channel.trim();
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
JigsawStudioGraphEditor.updateConnectorChannel(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
activeVariant.pieceKey(),
|
||||
position,
|
||||
channel);
|
||||
return CommandJigsaw.mappedMutationResult(
|
||||
context,
|
||||
"",
|
||||
"",
|
||||
"Persisted connector channel '" + normalized + "' on piece '"
|
||||
+ activeVariant.pieceKey() + "' at " + position.getX() + "," + position.getY()
|
||||
+ "," + position.getZ() + ".");
|
||||
});
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawDatapackExporter;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportDiagnostic;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
|
||||
import art.arcane.iris.structure.export.VanillaJigsawExportResult;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
final class CommandJigsawExports {
|
||||
private static final Pattern EXPORT_OUTPUT_NAME = Pattern.compile("[a-zA-Z0-9_-][a-zA-Z0-9._-]{0,127}");
|
||||
private static final Set<UUID> EXPORTING_PLAYERS = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<Path> EXPORT_DESTINATIONS = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private CommandJigsawExports() {
|
||||
}
|
||||
|
||||
static String exportStartError(JigsawStudioService.ExportStart exportStart) {
|
||||
Objects.requireNonNull(exportStart, "Jigsaw Studio export start status");
|
||||
return switch (exportStart) {
|
||||
case NOT_ACTIVE -> "The active Jigsaw Studio is no longer available.";
|
||||
case NOT_OWNER -> "Only the Jigsaw Studio owner can export this project.";
|
||||
case DIRTY -> "Wait for the pending autosave or discard the edits before exporting the on-disk graph.";
|
||||
case CLOSING -> "The active Jigsaw Studio is closing and cannot be exported.";
|
||||
case SAVE_IN_PROGRESS -> "Wait for the current Jigsaw Studio save to finish before exporting.";
|
||||
case OPERATION_IN_PROGRESS -> "Wait for the current Jigsaw Studio operation to finish before exporting.";
|
||||
case IN_PROGRESS -> "A Jigsaw Studio export is already in progress.";
|
||||
case STARTED -> throw new IllegalArgumentException("STARTED is not an export failure");
|
||||
};
|
||||
}
|
||||
|
||||
static boolean beginExport(UUID playerId, Path destination) {
|
||||
Objects.requireNonNull(playerId);
|
||||
Path normalizedDestination = Objects.requireNonNull(destination).toAbsolutePath().normalize();
|
||||
if (!EXPORTING_PLAYERS.add(playerId)) {
|
||||
return false;
|
||||
}
|
||||
if (EXPORT_DESTINATIONS.add(normalizedDestination)) {
|
||||
return true;
|
||||
}
|
||||
EXPORTING_PLAYERS.remove(playerId);
|
||||
return false;
|
||||
}
|
||||
|
||||
static void finishExport(UUID playerId, Path destination) {
|
||||
EXPORTING_PLAYERS.remove(Objects.requireNonNull(playerId));
|
||||
EXPORT_DESTINATIONS.remove(Objects.requireNonNull(destination).toAbsolutePath().normalize());
|
||||
}
|
||||
|
||||
static void runExport(ExportOperation operation) {
|
||||
VanillaJigsawExportResult result = null;
|
||||
Throwable failure = null;
|
||||
try {
|
||||
result = new VanillaJigsawDatapackExporter().export(operation.request());
|
||||
} catch (Throwable throwable) {
|
||||
failure = throwable;
|
||||
Iris.reportError("Vanilla Jigsaw export failed for '" + operation.structureKey() + "'.", throwable);
|
||||
} finally {
|
||||
operation.lease().release();
|
||||
}
|
||||
VanillaJigsawExportResult completedResult = result;
|
||||
Throwable completedFailure = failure;
|
||||
Runnable response = () -> reportExport(operation, completedResult, completedFailure);
|
||||
J.runEntity(operation.player(), response);
|
||||
}
|
||||
|
||||
private static void reportExport(
|
||||
ExportOperation operation,
|
||||
VanillaJigsawExportResult result,
|
||||
Throwable failure
|
||||
) {
|
||||
if (failure != null || result == null) {
|
||||
operation.sender().sendMessage(C.RED + "Background vanilla export for '"
|
||||
+ operation.structureKey() + "' failed: "
|
||||
+ (failure == null ? "no result" : failure.getMessage()));
|
||||
return;
|
||||
}
|
||||
for (VanillaJigsawExportDiagnostic diagnostic : result.diagnostics()) {
|
||||
C color = diagnostic.isBlocking() ? C.RED : C.YELLOW;
|
||||
operation.sender().sendMessage(color.toString() + diagnostic.code() + ": " + diagnostic.message());
|
||||
}
|
||||
CommandJigsaw.ActiveContext current = CommandJigsaw.active(operation.player());
|
||||
boolean sameSession = current != null
|
||||
&& current.request().requestId().equals(operation.requestId());
|
||||
String contextLabel = sameSession ? "Vanilla export" : "Background vanilla export for '"
|
||||
+ operation.structureKey() + "'";
|
||||
operation.sender().sendMessage((result.isSuccess() ? C.GREEN : C.RED) + contextLabel + " "
|
||||
+ result.status() + ": " + result.output());
|
||||
}
|
||||
|
||||
static VanillaJigsawExportFormat parseFormat(String value) {
|
||||
return switch (value.trim().toLowerCase(Locale.ROOT)) {
|
||||
case "zip" -> VanillaJigsawExportFormat.ZIP;
|
||||
case "directory", "dir", "folder" -> VanillaJigsawExportFormat.DIRECTORY;
|
||||
default -> throw new IllegalArgumentException("Export format must be directory or zip.");
|
||||
};
|
||||
}
|
||||
|
||||
static Path resolveExportDestination(
|
||||
Path exportRoot,
|
||||
String output,
|
||||
VanillaJigsawExportFormat format
|
||||
) {
|
||||
Path root = Objects.requireNonNull(exportRoot, "Jigsaw export root").toAbsolutePath().normalize();
|
||||
String requested = Objects.requireNonNull(output, "Jigsaw export output name");
|
||||
String name = requested.trim();
|
||||
if (!name.equals(requested) || !EXPORT_OUTPUT_NAME.matcher(name).matches()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Export output must be one file name using letters, numbers, '.', '_', or '-'.");
|
||||
}
|
||||
String fileName = format == VanillaJigsawExportFormat.ZIP && !name.endsWith(".zip")
|
||||
? name + ".zip" : name;
|
||||
Path destination = root.resolve(fileName).normalize();
|
||||
if (destination.equals(root) || !root.equals(destination.getParent())) {
|
||||
throw new IllegalArgumentException("Export output must be one artifact inside the Studio exports folder.");
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
|
||||
record ExportOperation(
|
||||
Player player,
|
||||
VolmitSender sender,
|
||||
UUID requestId,
|
||||
String structureKey,
|
||||
VanillaJigsawExportRequest request,
|
||||
ExportLease lease
|
||||
) {
|
||||
}
|
||||
|
||||
static final class ExportLease {
|
||||
private final Runnable release;
|
||||
private final AtomicBoolean released;
|
||||
|
||||
ExportLease(Runnable release) {
|
||||
this.release = Objects.requireNonNull(release, "Jigsaw export lease release action");
|
||||
this.released = new AtomicBoolean();
|
||||
}
|
||||
|
||||
void release() {
|
||||
if (released.compareAndSet(false, true)) {
|
||||
release.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.BrokenPackException;
|
||||
import art.arcane.iris.world.runtime.WorldRuntimeControlService;
|
||||
import art.arcane.iris.studio.object.StudioOpenCoordinator;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioActivation;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphMapper;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioLayout;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioSession;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.structure.placement.IrisStructure;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
final class CommandJigsawOpen {
|
||||
private CommandJigsawOpen() {
|
||||
}
|
||||
|
||||
static void openProject(
|
||||
Player targetPlayer,
|
||||
VolmitSender commandSender,
|
||||
IrisDimension dimension,
|
||||
String structure,
|
||||
long seed
|
||||
) {
|
||||
UUID playerId = targetPlayer.getUniqueId();
|
||||
UUID ownerId = JigsawStudioActivation.activeOwnerId();
|
||||
if (ownerId != null && !ownerId.equals(playerId)) {
|
||||
CommandJigsaw.sendError(commandSender, "Jigsaw Studio is already owned by another player session.");
|
||||
return;
|
||||
}
|
||||
UUID openingOwner = JigsawStudioActivation.openingOwnerId();
|
||||
if (openingOwner != null) {
|
||||
CommandJigsaw.sendError(commandSender, openingOwner.equals(playerId)
|
||||
? "Your Jigsaw Studio is already opening."
|
||||
: "Jigsaw Studio is already opening for another player.");
|
||||
return;
|
||||
}
|
||||
CommandJigsaw.ActiveContext previous = CommandJigsaw.active(targetPlayer);
|
||||
if (ownerId != null && previous == null) {
|
||||
CommandJigsaw.sendError(commandSender, "Your active Jigsaw Studio binding is unavailable. Close it before opening another project.");
|
||||
return;
|
||||
}
|
||||
CommandJigsaw.ActiveContext dirty = CommandJigsaw.findDirtyContext();
|
||||
if (dirty != null) {
|
||||
CommandJigsaw.sendError(commandSender,
|
||||
"The current Jigsaw Studio is waiting for autosave. Wait for it to finish or close with discard=true.");
|
||||
return;
|
||||
}
|
||||
IrisData data = CommandJigsaw.requireData(dimension, commandSender);
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
IrisStructure irisStructure = data.load(IrisStructure.class, structure, false);
|
||||
if (irisStructure == null) {
|
||||
CommandJigsaw.sendError(commandSender, "No Iris jigsaw structure '" + structure + "' exists in this pack.");
|
||||
return;
|
||||
}
|
||||
|
||||
JigsawStudioLayout layout;
|
||||
try {
|
||||
layout = JigsawStudioGraphMapper.map(data, irisStructure);
|
||||
} catch (RuntimeException exception) {
|
||||
Iris.reportError("Failed to map Jigsaw Studio graph '" + structure + "'.", exception);
|
||||
CommandJigsaw.sendError(commandSender, "Could not map this jigsaw graph: " + exception.getMessage());
|
||||
return;
|
||||
}
|
||||
String packKey = dimension.getLoadKey();
|
||||
if (!JigsawStudioActivation.tryBeginOpen(playerId)) {
|
||||
CommandJigsaw.sendError(commandSender,
|
||||
"Jigsaw Studio began opening for another player. Try again after it finishes.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService studioService = JigsawStudioService.get();
|
||||
UUID closingRequestId = previous == null ? null : previous.request().requestId();
|
||||
if (closingRequestId != null) {
|
||||
JigsawStudioService.CloseStart closeStart = studioService.tryBeginClose(
|
||||
closingRequestId,
|
||||
playerId,
|
||||
false);
|
||||
if (closeStart != JigsawStudioService.CloseStart.STARTED) {
|
||||
JigsawStudioActivation.finishOpen(playerId);
|
||||
CommandJigsaw.sendError(commandSender, closeStart == JigsawStudioService.CloseStart.SAVE_IN_PROGRESS
|
||||
? "The current Jigsaw Studio is saving. Wait for it to finish before opening another project."
|
||||
: closeStart == JigsawStudioService.CloseStart.OPERATION_IN_PROGRESS
|
||||
? "The current Jigsaw Studio is loading a variant. Wait for it to finish before opening another project."
|
||||
: closeStart == JigsawStudioService.CloseStart.DIRTY
|
||||
? "The current Jigsaw Studio is waiting for autosave. Wait for it to finish before opening another project."
|
||||
: "The current Jigsaw Studio could not be reserved for replacement.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
JigsawStudioActivation.StagedActivation staged;
|
||||
try {
|
||||
staged = JigsawStudioActivation.stage(
|
||||
packKey,
|
||||
structure,
|
||||
layout.mode(),
|
||||
CommandJigsaw.compatibilityOf(irisStructure),
|
||||
layout.cellDimensions(),
|
||||
data,
|
||||
layout,
|
||||
playerId,
|
||||
closingRequestId);
|
||||
} catch (RuntimeException exception) {
|
||||
JigsawStudioActivation.finishOpen(playerId);
|
||||
studioService.cancelClose(closingRequestId);
|
||||
Iris.reportError("Failed to stage Jigsaw Studio for '" + structure + "'.", exception);
|
||||
CommandJigsaw.sendError(commandSender, "Jigsaw Studio staging failed: " + exception.getMessage());
|
||||
return;
|
||||
}
|
||||
commandSender.sendMessage(C.GREEN + "Opening transient Jigsaw Studio for '" + structure + "' with "
|
||||
+ layout.bays().size() + " workcells.");
|
||||
|
||||
try {
|
||||
Iris.service(StudioSVC.class).openTracked(
|
||||
commandSender,
|
||||
seed,
|
||||
packKey,
|
||||
CommandJigsaw.STUDIO_OPEN_KIND,
|
||||
() -> beginStagedOpen(staged),
|
||||
world -> finishOpen(targetPlayer, world, staged, studioService))
|
||||
.whenComplete((result, throwable) -> J.s(() -> finishOpenAttempt(
|
||||
commandSender,
|
||||
playerId,
|
||||
structure,
|
||||
closingRequestId,
|
||||
studioService,
|
||||
staged,
|
||||
result,
|
||||
throwable)));
|
||||
} catch (Throwable exception) {
|
||||
JigsawStudioActivation.finishOpen(playerId);
|
||||
studioService.cancelClose(closingRequestId);
|
||||
JigsawStudioActivation.rollback(staged);
|
||||
Iris.reportError("Failed to open Jigsaw Studio for '" + structure + "'.", exception);
|
||||
CommandJigsaw.sendError(commandSender, "Jigsaw Studio open failed: " + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void beginStagedOpen(JigsawStudioActivation.StagedActivation staged) {
|
||||
if (!JigsawStudioActivation.beginStagedGeneration(staged)) {
|
||||
throw new IllegalStateException("The staged Jigsaw Studio activation is no longer current");
|
||||
}
|
||||
}
|
||||
|
||||
private static void finishOpenAttempt(
|
||||
VolmitSender commandSender,
|
||||
UUID playerId,
|
||||
String structureKey,
|
||||
UUID closingRequestId,
|
||||
JigsawStudioService studioService,
|
||||
JigsawStudioActivation.StagedActivation staged,
|
||||
StudioOpenCoordinator.StudioOpenResult result,
|
||||
Throwable throwable
|
||||
) {
|
||||
JigsawStudioActivation.finishOpen(playerId);
|
||||
if (throwable == null && result != null) {
|
||||
studioService.cancelClose(closingRequestId);
|
||||
return;
|
||||
}
|
||||
studioService.cancelClose(closingRequestId);
|
||||
JigsawStudioActivation.rollback(staged);
|
||||
Throwable failure = unwrapCompletionFailure(throwable == null
|
||||
? new IllegalStateException("Studio open completed without a result")
|
||||
: throwable);
|
||||
if (isExpectedOpenDenial(failure)) {
|
||||
Iris.warn("Jigsaw Studio open for '%s' was denied: %s",
|
||||
structureKey, failure.getMessage());
|
||||
return;
|
||||
}
|
||||
Iris.reportError("Failed to open Jigsaw Studio for '" + structureKey + "'.", failure);
|
||||
CommandJigsaw.sendError(commandSender, "Jigsaw Studio open failed: " + failure.getMessage());
|
||||
}
|
||||
|
||||
private static void finishOpen(
|
||||
Player targetPlayer,
|
||||
World world,
|
||||
JigsawStudioActivation.StagedActivation staged,
|
||||
JigsawStudioService studioService
|
||||
) {
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Jigsaw Studio opened without a world");
|
||||
}
|
||||
JigsawStudioActivation.Request request = staged.request();
|
||||
JigsawStudioSession session = staged.session();
|
||||
try {
|
||||
WorldRuntimeControlService.get().applyObjectStudioWorldRules(world);
|
||||
} catch (Throwable exception) {
|
||||
Iris.reportError("Failed to apply Jigsaw Studio world rules for '" + world.getName() + "'.", exception);
|
||||
}
|
||||
JigsawStudioBay destination = initialBay(session.layout());
|
||||
Location target = destination == null
|
||||
? new Location(world, 0.5D, 66D, 0.5D)
|
||||
: new Location(
|
||||
world,
|
||||
destination.bounds().originX() + destination.bounds().dimensions().width() / 2.0D,
|
||||
destination.bounds().maxY() + 2.0D,
|
||||
destination.bounds().originZ() + destination.bounds().dimensions().depth() / 2.0D);
|
||||
if (destination != null) {
|
||||
session.selectBay(destination.stableId());
|
||||
}
|
||||
if (!JigsawStudioActivation.commit(staged)) {
|
||||
throw new IllegalStateException("The staged Jigsaw Studio activation could not be committed");
|
||||
}
|
||||
studioService.activationCommitted(world, request.requestId());
|
||||
CommandJigsaw.PLAYER_PACKS.put(targetPlayer.getUniqueId(), new CommandJigsaw.SessionBinding(
|
||||
request.packKey(), request.requestId()));
|
||||
try {
|
||||
J.runEntity(targetPlayer, () -> BukkitPlatform.teleportAsync(targetPlayer, target)
|
||||
.thenRun(() -> J.runEntity(targetPlayer, () -> targetPlayer.setGameMode(GameMode.CREATIVE))));
|
||||
} catch (RuntimeException exception) {
|
||||
Iris.reportError("Failed to move the player to the committed Jigsaw Studio workcell.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static JigsawStudioBay initialBay(JigsawStudioLayout layout) {
|
||||
for (JigsawStudioBay bay : layout.bays()) {
|
||||
if (layout.defaultVariant(bay).isPresent()) {
|
||||
return bay;
|
||||
}
|
||||
}
|
||||
return layout.bays().isEmpty() ? null : layout.bays().getFirst();
|
||||
}
|
||||
|
||||
private static Throwable unwrapCompletionFailure(Throwable failure) {
|
||||
Throwable current = failure;
|
||||
while (current instanceof CompletionException && current.getCause() != null) {
|
||||
current = current.getCause();
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
static boolean isExpectedOpenDenial(Throwable failure) {
|
||||
return unwrapCompletionFailure(failure) instanceof BrokenPackException;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.studio.jigsaw.JigsawPlanarTopology;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCompatibilityTarget;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphEditor;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioLayout;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioMode;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioPoolEditor;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioVariant;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||
import art.arcane.iris.structure.jigsaw.IrisJigsawPiece;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
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.Location;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Director(name = "piece", description = "Manage contextual Studio pieces", origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsawPiece implements DirectorExecutor {
|
||||
@Director(description = "Create an owned piece from the selected Studio topology", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void create(
|
||||
@Param(description = "Owned pool resource key") String poolKey,
|
||||
@Param(description = "New owned piece and object key") String pieceKey,
|
||||
@Param(description = "Positive selection weight", defaultValue = "1") int weight
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawPlanarTopology topology = null;
|
||||
JigsawStudioBay targetWorkcell = context.session().layout().get(JigsawStudioLayout.SPATIAL_WORKCELL_ID);
|
||||
if (context.session().layout().mode() == JigsawStudioMode.PLANAR_JIGSAW) {
|
||||
Location location = player().getLocation();
|
||||
JigsawStudioBay contextual = context.session().layout().findAt(
|
||||
location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
||||
if (contextual == null) {
|
||||
contextual = CommandJigsaw.selectedBay(context);
|
||||
} else {
|
||||
context.session().selectBay(contextual.stableId());
|
||||
}
|
||||
if (contextual == null || contextual.topology().isEmpty()) {
|
||||
CommandJigsaw.sendError(this, "Stand in or select a planar topology workcell before creating a variant.");
|
||||
return;
|
||||
}
|
||||
topology = contextual.topology().get();
|
||||
targetWorkcell = contextual;
|
||||
}
|
||||
if (targetWorkcell == null) {
|
||||
CommandJigsaw.sendError(this, "The selected Jigsaw Studio workcell is no longer available.");
|
||||
return;
|
||||
}
|
||||
JigsawPlanarTopology selectedTopology = topology;
|
||||
String activationWorkcellId = targetWorkcell.stableId();
|
||||
JigsawStudioCellDimensions dimensions = targetWorkcell.capacity();
|
||||
String topologyText = selectedTopology == null ? "blank spatial" : selectedTopology.name();
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
JigsawStudioGraphEditor.createPiece(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
poolKey,
|
||||
pieceKey,
|
||||
weight,
|
||||
dimensions,
|
||||
selectedTopology);
|
||||
return CommandJigsaw.mappedMutationResult(
|
||||
context,
|
||||
activationWorkcellId,
|
||||
pieceKey,
|
||||
"Created owned " + topologyText + " piece '" + pieceKey
|
||||
+ "' and added it to pool '" + poolKey + "' atomically.");
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Add an existing jigsaw piece to an owned pool transactionally", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void add(
|
||||
@Param(description = "Owned pool resource key") String poolKey,
|
||||
@Param(description = "Existing jigsaw piece key") String pieceKey,
|
||||
@Param(description = "Positive selection weight", defaultValue = "1") int weight
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
IrisJigsawPiece piece = context.request().source().getJigsawPieceLoader().load(pieceKey);
|
||||
if (piece == null) {
|
||||
CommandJigsaw.sendError(this, "No jigsaw piece '" + pieceKey + "' exists in this pack.");
|
||||
return;
|
||||
}
|
||||
if (piece.getObject() == null || piece.getObject().isBlank()) {
|
||||
CommandJigsaw.sendError(this, "Jigsaw piece '" + pieceKey + "' does not declare an object.");
|
||||
return;
|
||||
}
|
||||
IrisObject object = context.request().source().getObjectLoader().load(piece.getObject());
|
||||
if (object == null) {
|
||||
CommandJigsaw.sendError(this, "Jigsaw piece '" + pieceKey + "' has no loadable object.");
|
||||
return;
|
||||
}
|
||||
CommandJigsaw.PieceWorkcellResolution resolution;
|
||||
try {
|
||||
resolution = CommandJigsaw.resolvePieceWorkcell(context.session().layout(), piece, object);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
CommandJigsaw.sendError(this, "Jigsaw piece '" + pieceKey + "' cannot be assigned to a Studio workcell: "
|
||||
+ exception.getMessage());
|
||||
return;
|
||||
}
|
||||
if (!resolution.fits()) {
|
||||
IrisPosition required = resolution.requiredDimensions();
|
||||
JigsawStudioCellDimensions capacity = resolution.workcell().capacity();
|
||||
CommandJigsaw.sendError(this, "Jigsaw piece '" + pieceKey + "' requires " + required.getX() + "x"
|
||||
+ required.getY() + "x" + required.getZ() + " in " + resolution.workcell().displayName()
|
||||
+ ", but that workcell capacity is " + capacity.width() + "x" + capacity.height()
|
||||
+ "x" + capacity.depth() + ".");
|
||||
return;
|
||||
}
|
||||
String objectKey = piece.getObject();
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
if (!JigsawStudioGraphEditor.ownsPiece(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
pieceKey,
|
||||
objectKey)) {
|
||||
throw new IOException("Piece add accepts only piece/object resources already owned by this project; "
|
||||
+ "use piece create for a new owned piece");
|
||||
}
|
||||
JigsawStudioPoolEditor.PoolUpdate update = JigsawStudioPoolEditor.addPiece(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
poolKey,
|
||||
pieceKey,
|
||||
weight);
|
||||
if (!update.changed()) {
|
||||
throw new IOException("The owned pool is missing or already references '" + pieceKey + "'");
|
||||
}
|
||||
JigsawStudioLayout layout = CommandJigsaw.loadPersistedLayout(context);
|
||||
String activationWorkcellId = CommandJigsaw.workcellIdForVariant(layout, pieceKey);
|
||||
return new JigsawStudioService.CommandGraphMutationResult(
|
||||
layout,
|
||||
activationWorkcellId,
|
||||
pieceKey,
|
||||
"Added '" + pieceKey + "' to pool '" + poolKey + "' in one graph transaction.");
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Remove the selected piece from an owned pool transactionally", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void remove(
|
||||
@Param(description = "Owned pool resource key") String poolKey
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioVariant selected = CommandJigsaw.selectedVariant(context);
|
||||
if (selected == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell with an active variant before removing its pool entry.");
|
||||
return;
|
||||
}
|
||||
String pieceKey = selected.pieceKey();
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
JigsawStudioPoolEditor.PoolUpdate update = JigsawStudioPoolEditor.removePiece(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
poolKey,
|
||||
pieceKey);
|
||||
if (!update.changed()) {
|
||||
throw new IOException("The owned pool does not reference selected piece '" + pieceKey + "'");
|
||||
}
|
||||
return CommandJigsaw.mappedMutationResult(
|
||||
context,
|
||||
"",
|
||||
"",
|
||||
"Removed '" + pieceKey + "' from pool '" + poolKey + "' in one graph transaction.");
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Allow or forbid cardinal rotation of the selected piece", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void rotatable(
|
||||
@Param(description = "Whether the selected piece may rotate") boolean rotatable
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioVariant selected = CommandJigsaw.selectedVariant(context);
|
||||
if (selected == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell with an active variant before changing rotation.");
|
||||
return;
|
||||
}
|
||||
if (!rotatable
|
||||
&& context.request().compatibilityTarget()
|
||||
== JigsawStudioCompatibilityTarget.VANILLA_PORTABLE) {
|
||||
CommandJigsaw.sendError(this, "Vanilla-portable pieces must remain rotatable.");
|
||||
return;
|
||||
}
|
||||
String pieceKey = selected.pieceKey();
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
JigsawStudioGraphEditor.updateRotatable(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
pieceKey,
|
||||
rotatable);
|
||||
return CommandJigsaw.mappedMutationResult(
|
||||
context,
|
||||
"",
|
||||
"",
|
||||
"Persisted piece '" + pieceKey + "' rotatable=" + rotatable + ".");
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Resize the selected piece object exactly to workcell capacity", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void expand() {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay workcell = CommandJigsaw.selectedBay(context);
|
||||
if (workcell == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell before expanding its active variant.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().expandVariantToCell(player(), workcell.stableId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioGraphEditor;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
@Director(name = "pool", description = "Manage owned jigsaw pools", origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsawPool implements DirectorExecutor {
|
||||
@Director(description = "Create an empty owned jigsaw pool", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void create(
|
||||
@Param(description = "New owned pool resource key") String poolKey,
|
||||
@Param(description = "Owned fallback pool key, or none", defaultValue = "none")
|
||||
String fallbackPoolKey
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
String fallback = "none".equalsIgnoreCase(fallbackPoolKey) ? "" : fallbackPoolKey;
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
JigsawStudioGraphEditor.createPool(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
poolKey,
|
||||
fallback);
|
||||
return CommandJigsaw.mappedMutationResult(
|
||||
context,
|
||||
"",
|
||||
"",
|
||||
"Created owned pool '" + poolKey + "' with fallback '"
|
||||
+ (fallback.isBlank() ? "none" : fallback) + "'.");
|
||||
});
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
@Director(name = "preview", description = "Assemble a read-only jigsaw preview",
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsawPreview implements DirectorExecutor {
|
||||
@Director(name = "goto", aliases = "teleport", description = "Teleport to the live seed-1337 preview",
|
||||
sync = true, origin = DirectorOrigin.PLAYER)
|
||||
public void gotoPreview() {
|
||||
JigsawStudioService.get().goToPreview(player());
|
||||
}
|
||||
|
||||
@Director(description = "Assemble a deterministic preview at your location", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void assemble(
|
||||
@Param(description = "Assembly seed", defaultValue = "1337") long seed
|
||||
) {
|
||||
JigsawStudioService.get().previewStudio(player(), seed);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCompatibilityTarget;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioPoolEditor;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioStructureEditor;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
@Director(name = "rules", aliases = "rule", description = "Manage structure-wide jigsaw rules",
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsawRules implements DirectorExecutor {
|
||||
@Director(description = "Set maximum expansion depth and horizontal radius", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void limits(
|
||||
@Param(description = "Maximum expansion depth") int maxDepth,
|
||||
@Param(description = "Maximum radius in chunks") int maxSizeChunks
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
if (context.request().compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE
|
||||
&& (maxDepth > 20 || maxSizeChunks > 8)) {
|
||||
CommandJigsaw.sendError(this, "Vanilla-portable rules require maxDepth <= 20 and maxSizeChunks <= 8.");
|
||||
return;
|
||||
}
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
JigsawStudioStructureEditor.updateLimits(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
maxDepth,
|
||||
maxSizeChunks);
|
||||
return CommandJigsaw.mappedMutationResult(
|
||||
context,
|
||||
"",
|
||||
"",
|
||||
"Persisted jigsaw limits: maxDepth=" + maxDepth
|
||||
+ ", maxSizeChunks=" + maxSizeChunks + ".");
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Set or clear one owned pool's direct fallback", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void fallback(
|
||||
@Param(description = "Owned pool resource key") String poolKey,
|
||||
@Param(description = "Owned fallback pool key, or none") String fallbackPoolKey
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
String fallback = "none".equalsIgnoreCase(fallbackPoolKey) ? "" : fallbackPoolKey;
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
JigsawStudioPoolEditor.PoolUpdate update = JigsawStudioPoolEditor.updateFallback(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
poolKey,
|
||||
fallback);
|
||||
if (!update.changed()) {
|
||||
return CommandJigsaw.mappedMutationResult(context, "", "", "Pool fallback is unchanged.");
|
||||
}
|
||||
return CommandJigsaw.mappedMutationResult(
|
||||
context,
|
||||
"",
|
||||
"",
|
||||
"Persisted pool '" + poolKey + "' fallback as '"
|
||||
+ (fallback.isBlank() ? "none" : fallback) + "'.");
|
||||
});
|
||||
}
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioPoolEditor;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioVariant;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Director(name = "variant", description = "Manage selected jigsaw variants", origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsawVariant implements DirectorExecutor {
|
||||
@Director(description = "Set the selected piece weight in a pool", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void weight(
|
||||
@Param(description = "Pool resource key") String poolKey,
|
||||
@Param(description = "Positive selection weight") int weight
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioVariant selected = CommandJigsaw.selectedVariant(context);
|
||||
if (selected == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell with an active variant before changing its weight.");
|
||||
return;
|
||||
}
|
||||
String pieceKey = selected.pieceKey();
|
||||
CommandJigsaw.runGraphMutation(player(), context, () -> {
|
||||
JigsawStudioPoolEditor.WeightUpdate update = JigsawStudioPoolEditor.updateWeight(
|
||||
context.request().source().getDataFolder().toPath(),
|
||||
context.request().structureKey(),
|
||||
poolKey,
|
||||
pieceKey,
|
||||
weight);
|
||||
if (!update.changed()) {
|
||||
throw new IOException("Pool '" + poolKey + "' does not reference selected piece '"
|
||||
+ pieceKey + "'");
|
||||
}
|
||||
return CommandJigsaw.mappedMutationResult(
|
||||
context,
|
||||
"",
|
||||
"",
|
||||
"Updated " + update.changedEntries() + " pool entry weight atomically in "
|
||||
+ update.poolPath() + ".");
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Resize only the active variant object within workcell capacity", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void resize(
|
||||
@Param(description = "Variant width") int width,
|
||||
@Param(description = "Variant height") int height,
|
||||
@Param(description = "Variant depth") int depth
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay selectedWorkcell = CommandJigsaw.selectedBay(context);
|
||||
JigsawStudioVariant selectedVariant = CommandJigsaw.selectedVariant(context);
|
||||
if (selectedWorkcell == null || selectedVariant == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell with an active variant before resizing it.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioCellDimensions dimensions;
|
||||
try {
|
||||
dimensions = new JigsawStudioCellDimensions(width, height, depth);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
CommandJigsaw.sendError(this, exception.getMessage());
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().resizeVariant(
|
||||
player(),
|
||||
selectedWorkcell.stableId(),
|
||||
selectedVariant.pieceKey(),
|
||||
dimensions);
|
||||
}
|
||||
|
||||
@Director(description = "Set the active variant's author-facing label", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void label(
|
||||
@Param(description = "Display label; quote labels containing spaces") String displayName
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay selectedWorkcell = CommandJigsaw.selectedBay(context);
|
||||
JigsawStudioVariant selectedVariant = CommandJigsaw.selectedVariant(context);
|
||||
if (selectedWorkcell == null || selectedVariant == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell with an active variant before renaming it.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().updateVariantDisplayName(
|
||||
player(),
|
||||
selectedWorkcell.stableId(),
|
||||
selectedVariant.pieceKey(),
|
||||
displayName);
|
||||
}
|
||||
|
||||
@Director(name = "label-reset", aliases = "reset-label",
|
||||
description = "Reset the active variant to its resource-key label", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void labelReset() {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay selectedWorkcell = CommandJigsaw.selectedBay(context);
|
||||
JigsawStudioVariant selectedVariant = CommandJigsaw.selectedVariant(context);
|
||||
if (selectedWorkcell == null || selectedVariant == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell with an active variant before resetting its label.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().updateVariantDisplayName(
|
||||
player(),
|
||||
selectedWorkcell.stableId(),
|
||||
selectedVariant.pieceKey(),
|
||||
"");
|
||||
}
|
||||
|
||||
@Director(description = "Duplicate the active variant in only this workcell", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void duplicate() {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay selected = CommandJigsaw.selectedBay(context);
|
||||
if (selected == null || CommandJigsaw.selectedVariant(context) == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell with an active variant before duplicating it.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().createVariant(player(), selected.stableId(), true);
|
||||
}
|
||||
|
||||
@Director(name = "duplicate-family", aliases = "family",
|
||||
description = "Duplicate every enabled workcell's active variant as one coherent family",
|
||||
sync = true, origin = DirectorOrigin.PLAYER)
|
||||
public void duplicateFamily(
|
||||
@Param(description = "Family key or next", defaultValue = "next") String themeKey
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
String selectedTheme = themeKey;
|
||||
if ("next".equalsIgnoreCase(themeKey)) {
|
||||
try {
|
||||
selectedTheme = CommandJigsaw.nextThemeKey(context);
|
||||
} catch (IOException | ArithmeticException exception) {
|
||||
CommandJigsaw.sendError(this, exception.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
JigsawStudioService.get().duplicateActiveFamily(player(), selectedTheme);
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
@Director(name = "workcell", aliases = "cell", description = "Manage the selected Studio workcell",
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public class CommandJigsawWorkcell implements DirectorExecutor {
|
||||
@Director(description = "Set capacity without resizing any variant object", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void capacity(
|
||||
@Param(description = "Capacity width") int width,
|
||||
@Param(description = "Capacity height") int height,
|
||||
@Param(description = "Capacity depth") int depth
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay selected = CommandJigsaw.selectedBay(context);
|
||||
if (selected == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell before changing its capacity.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioCellDimensions dimensions;
|
||||
try {
|
||||
dimensions = new JigsawStudioCellDimensions(width, height, depth);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
CommandJigsaw.sendError(this, exception.getMessage());
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().updateWorkcellDimensions(
|
||||
player(),
|
||||
selected.stableId(),
|
||||
dimensions);
|
||||
}
|
||||
|
||||
@Director(description = "Set the selected workcell's author-facing label", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void label(
|
||||
@Param(description = "Display label; quote labels containing spaces") String displayName
|
||||
) {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay selected = CommandJigsaw.selectedBay(context);
|
||||
if (selected == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell before renaming it.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().updateWorkcellDisplayName(
|
||||
player(),
|
||||
selected.stableId(),
|
||||
displayName);
|
||||
}
|
||||
|
||||
@Director(name = "label-reset", aliases = "reset-label",
|
||||
description = "Reset the selected workcell to its canonical solver label", sync = true,
|
||||
origin = DirectorOrigin.PLAYER)
|
||||
public void labelReset() {
|
||||
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
|
||||
if (context == null) {
|
||||
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay selected = CommandJigsaw.selectedBay(context);
|
||||
if (selected == null) {
|
||||
CommandJigsaw.sendError(this, "Select a workcell before resetting its label.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioService.get().updateWorkcellDisplayName(player(), selected.stableId(), "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,859 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.integration.WorldEditLink;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.studio.object.ObjectStudioActivation;
|
||||
import art.arcane.iris.studio.object.StudioOpenCoordinator;
|
||||
import art.arcane.iris.world.runtime.WorldRuntimeControlService;
|
||||
import art.arcane.iris.integration.ExternalDataSVC;
|
||||
import art.arcane.iris.studio.object.ObjectSVC;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.studio.wand.WandSVC;
|
||||
import art.arcane.iris.pack.IrisConverter;
|
||||
import art.arcane.iris.studio.tree.TreePlausibilizeBatch;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacementScaleInterpolator;
|
||||
import art.arcane.iris.structure.object.IrisObjectRotation;
|
||||
import art.arcane.iris.structure.object.IrisObjectScale;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.data.Cuboid;
|
||||
import art.arcane.iris.generation.block.IrisCustomData;
|
||||
import art.arcane.iris.command.specialhandlers.NullableDimensionHandler;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.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.command.specialhandlers.ObjectHandler;
|
||||
import art.arcane.iris.command.specialhandlers.ObjectScaleHandler;
|
||||
import art.arcane.iris.command.specialhandlers.ObjectTargetHandler;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.volmlib.util.math.DirectionBasis;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.HeightMap;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.RuntimeUiMessages;
|
||||
@Director(name = "object", aliases = "o", origin = DirectorOrigin.PLAYER, description = "Iris object manipulation", descriptionKey = "iris.director.commandobject.director.iris_object_manipulation")
|
||||
public class CommandObject implements DirectorExecutor {
|
||||
static final Set<Material> PASTE_TRANSPARENT_BLOCKS = Set.of(
|
||||
Material.AIR,
|
||||
Material.CAVE_AIR,
|
||||
Material.VOID_AIR,
|
||||
Material.SHORT_GRASS,
|
||||
Material.SNOW,
|
||||
Material.VINE,
|
||||
Material.TORCH,
|
||||
Material.DEAD_BUSH,
|
||||
Material.POPPY,
|
||||
Material.DANDELION
|
||||
);
|
||||
|
||||
static boolean isPasteTarget(Material material) {
|
||||
return !PASTE_TRANSPARENT_BLOCKS.contains(material);
|
||||
}
|
||||
|
||||
@Director(description = "Open an object studio world (grid of every object; dimension optional, defaults to all packs)", descriptionKey = "iris.director.commandobject.director.open_object_studio_world_grid_every_object_dimension_optional_defaults_all_packs", sync = true)
|
||||
public void studio(
|
||||
@Param(defaultValue = "null", description = "Optional dimension whose object pack to lay out; omit to aggregate objects from every pack", descriptionKey = "iris.director.commandobject.param.optional_dimension_whose_object_pack_lay_out_omit_aggregate_objects_from_every", aliases = "dim", customHandler = NullableDimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(defaultValue = "1337", description = "The seed to generate the studio with", descriptionKey = "iris.director.commandobject.param.seed_generate_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();
|
||||
for (File pack : PackDirectoryResolver.listVisiblePackDirectories(workspace)) {
|
||||
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(IrisLanguage.text(BukkitCommandMessages.COMMAND_OBJECT_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(IrisLanguage.text(BukkitCommandMessages.COMMAND_OBJECT_NO_OBJECTS_PLACE_ACROSS_SELECTED_PACK_S));
|
||||
return;
|
||||
}
|
||||
|
||||
// ObjectStudioActivation carries the buffet state; mutating the shared cached
|
||||
// IrisDimension here leaked OBJECT_BUFFET into later pack exports of this dimension.
|
||||
ObjectStudioActivation.activate(hostDimension.getLoadKey());
|
||||
ObjectStudioActivation.setSources(hostDimension.getLoadKey(), sources);
|
||||
|
||||
String scope = dimension == null
|
||||
? ("all packs [" + sources.size() + "]")
|
||||
: ("\"" + hostDimension.getName() + "\"");
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_OBJECT_OPENING_OBJECT_STUDIO_OBJECTS, MessageArgument.untrusted("scope", scope), MessageArgument.untrusted("totalObjects", totalObjects)));
|
||||
|
||||
IrisDimension finalHost = hostDimension;
|
||||
try {
|
||||
Iris.service(StudioSVC.class).open(
|
||||
commandSender,
|
||||
seed,
|
||||
hostDimension.getLoadKey(),
|
||||
StudioOpenCoordinator.StudioOpenKind.OBJECT,
|
||||
world -> {
|
||||
if (world == null) return;
|
||||
try {
|
||||
WorldRuntimeControlService.get().applyObjectStudioWorldRules(world);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to apply object studio world rules for " + world.getName(), e);
|
||||
}
|
||||
|
||||
if (commandSender.isPlayer()) {
|
||||
Player p = commandSender.player();
|
||||
if (p != null) {
|
||||
Location target = new Location(world, 0.5D, 66D, 0.5D);
|
||||
J.runEntity(p, () -> {
|
||||
BukkitPlatform.teleportAsync(p, target).thenRun(() -> p.setGameMode(GameMode.CREATIVE));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to open object studio world \"" + finalHost.getLoadKey() + "\".", e);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_OBJECT_FAILED_OPEN_OBJECT_STUDIO, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
||||
}
|
||||
}
|
||||
|
||||
public static IObjectPlacer createPlacer(World world, Map<Block, BlockData> futureBlockChanges, Engine targetEngine) {
|
||||
return new IObjectPlacer() {
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data) {
|
||||
return world.getHighestBlockYAt(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||
return world.getHighestBlockYAt(x, z, ignoreFluid ? HeightMap.OCEAN_FLOOR : HeightMap.MOTION_BLOCKING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, PlatformBlockState s) {
|
||||
BlockData d = (BlockData) s.nativeHandle();
|
||||
Block block = world.getBlockAt(x, y, z);
|
||||
|
||||
//Prevent blocks being set in or bellow bedrock
|
||||
if (y <= world.getMinHeight() || block.getType() == Material.BEDROCK) return;
|
||||
|
||||
ExternalDataSVC external = IrisServices.getOrNull(ExternalDataSVC.class);
|
||||
futureBlockChanges.computeIfAbsent(block, target -> {
|
||||
BlockData previous = target.getBlockData();
|
||||
return external == null ? previous : external.captureBlockData(previous);
|
||||
});
|
||||
|
||||
if (d instanceof IrisCustomData data) {
|
||||
if (external != null && external.placeBlock(block, data.getCustom())) {
|
||||
return;
|
||||
}
|
||||
block.setBlockData(data.getBase(), false);
|
||||
IrisLogging.warnOnce("object-paste:external:" + data.getCustom(),
|
||||
"Direct placement of %s is unavailable; only its base block was placed.", data.getCustom());
|
||||
} else block.setBlockData(d, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState get(int x, int y, int z) {
|
||||
return BukkitBlockState.of(world.getBlockAt(x, y, z).getBlockData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPreventingDecay() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolid(int x, int y, int z) {
|
||||
return world.getBlockAt(x, y, z).getType().isSolid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUnderwater(int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFluidHeight() {
|
||||
return targetEngine == null
|
||||
? 63
|
||||
: targetEngine.getMinHeight() + targetEngine.getDimension().getFluidHeight();
|
||||
}
|
||||
|
||||
@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 targetEngine;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Director(description = "Check the composition of an object", descriptionKey = "iris.director.commandobject.director.check_composition_object")
|
||||
public void analyze(
|
||||
@Param(description = "The object to analyze", descriptionKey = "iris.director.commandobject.param.object_analyze", customHandler = ObjectHandler.class)
|
||||
String object
|
||||
) {
|
||||
IrisObject o = IrisData.loadAnyObject(object, data());
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_OBJECT_SIZE, MessageArgument.untrusted("value", o.getW()), MessageArgument.untrusted("value2", o.getH()), MessageArgument.untrusted("value3", o.getD())));
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_BLOCKS_USED, MessageArgument.untrusted("value", NumberFormat.getIntegerInstance().format(o.getBlocks().size()))));
|
||||
|
||||
var queue = o.getBlocks().values();
|
||||
Map<Material, Set<BlockData>> unsorted = new HashMap<>();
|
||||
Map<BlockData, Integer> amounts = new HashMap<>();
|
||||
Map<Material, Integer> materials = new HashMap<>();
|
||||
while (queue.hasNext()) {
|
||||
BlockData block = (BlockData) queue.next().nativeHandle();
|
||||
|
||||
//unsorted.put(block.getMaterial(), block);
|
||||
|
||||
if (!amounts.containsKey(block)) {
|
||||
amounts.put(block, 1);
|
||||
|
||||
|
||||
} else
|
||||
amounts.put(block, amounts.get(block) + 1);
|
||||
|
||||
if (!materials.containsKey(block.getMaterial())) {
|
||||
materials.put(block.getMaterial(), 1);
|
||||
unsorted.put(block.getMaterial(), new HashSet<>());
|
||||
unsorted.get(block.getMaterial()).add(block);
|
||||
} else {
|
||||
materials.put(block.getMaterial(), materials.get(block.getMaterial()) + 1);
|
||||
unsorted.get(block.getMaterial()).add(block);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
List<Material> sortedMatsList = amounts.keySet().stream().map(BlockData::getMaterial)
|
||||
.sorted().toList();
|
||||
Set<Material> sortedMats = new TreeSet<>(Comparator.comparingInt(materials::get).reversed());
|
||||
sortedMats.addAll(sortedMatsList);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_BLOCKS_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(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_OTHER_BLOCK_TYPES, MessageArgument.untrusted("value", (sortedMats.size() - n))));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Shrink an object to its minimum size", descriptionKey = "iris.director.commandobject.director.shrink_object_its_minimum_size")
|
||||
public void shrink(@Param(description = "The object to shrink", descriptionKey = "iris.director.commandobject.param.object_shrink", customHandler = ObjectHandler.class) String object) {
|
||||
IrisObject o = IrisData.loadAnyObject(object, data());
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_CURRENT_OBJECT_SIZE, MessageArgument.untrusted("value", o.getW()), MessageArgument.untrusted("value2", o.getH()), MessageArgument.untrusted("value3", o.getD())));
|
||||
o.shrinkwrap();
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NEW_OBJECT_SIZE, MessageArgument.untrusted("value", o.getW()), MessageArgument.untrusted("value2", o.getH()), MessageArgument.untrusted("value3", o.getD())));
|
||||
try {
|
||||
o.write(o.getLoadFile());
|
||||
} catch (IOException e) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FAILED_SAVE_OBJECT, MessageArgument.untrusted("value", o.getLoadFile()), MessageArgument.untrusted("value2", String.valueOf(e.getMessage()))));
|
||||
Iris.reportError("Failed to save object " + o.getLoadFile() + ".", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Grow organic branches through the canopy so every leaf survives vanilla decay", descriptionKey = "iris.director.commandobject.director.grow_organic_branches_through_canopy_so_every_leaf_survives_vanilla_decay",
|
||||
origin = DirectorOrigin.BOTH)
|
||||
public void plausibilize(
|
||||
@Param(description = "Object key, prefix (trees/), or filesystem path", descriptionKey = "iris.director.commandobject.param.object_key_prefix_trees_filesystem_path",
|
||||
customHandler = ObjectTargetHandler.class)
|
||||
String target,
|
||||
@Param(name = "dryrun", description = "dryrun=true analyzes only, writes nothing", descriptionKey = "iris.director.commandobject.param.dryrun_true_analyzes_only_writes_nothing", defaultValue = "false")
|
||||
boolean dryRun,
|
||||
@Param(name = "reach", description = "reach=N max branch length in blocks from existing wood; farther leaf clusters are pinned persistent instead. reach=0 grows unlimited", descriptionKey = "iris.director.commandobject.param.reach_n_max_branch_length_blocks_from_existing_wood_farther_leaf_clusters", defaultValue = "12")
|
||||
int reach
|
||||
) {
|
||||
IrisData nearest = data();
|
||||
List<TreePlausibilizeBatch.Target> targets = TreePlausibilizeBatch.resolve(target, nearest);
|
||||
if (targets.isEmpty() && nearest == null) {
|
||||
targets = resolveFromPacks(target);
|
||||
}
|
||||
if (targets.isEmpty()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_OBJECTS_MATCHED, MessageArgument.untrusted("target", target)));
|
||||
return;
|
||||
}
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(
|
||||
BukkitCommandMessagesExtended.COMMAND_OBJECT_PLAUSIBILIZE_REACH_QUEUED_OBJECT_S,
|
||||
MessageArgument.trusted("reach", reach),
|
||||
MessageArgument.trusted("value", dryRun ? IrisLanguage.text(RuntimeUiMessages.TREE_DRY_SUFFIX) : ""),
|
||||
MessageArgument.trusted("value2", targets.size())
|
||||
));
|
||||
|
||||
VolmitSender s = sender();
|
||||
List<TreePlausibilizeBatch.Target> queued = targets;
|
||||
J.a(() -> TreePlausibilizeBatch.run(queued, dryRun, reach, nearest, (TreePlausibilizeBatch.Output output) ->
|
||||
s.sendMessage(output.headline()
|
||||
? C.IRIS + output.text()
|
||||
: C.GRAY + " " + output.text())));
|
||||
}
|
||||
|
||||
private static List<TreePlausibilizeBatch.Target> resolveFromPacks(String target) {
|
||||
List<TreePlausibilizeBatch.Target> out = new ArrayList<>();
|
||||
File packsFolder = Iris.instance.getDataFolder("packs");
|
||||
for (File pack : PackDirectoryResolver.listVisiblePackDirectories(packsFolder)) {
|
||||
File objectsRoot = new File(pack, "objects");
|
||||
if (!objectsRoot.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
File candidate = new File(objectsRoot, target + ".iob");
|
||||
if (candidate.isFile()) {
|
||||
out.add(new TreePlausibilizeBatch.Target(pack.getName() + "/" + target, candidate));
|
||||
continue;
|
||||
}
|
||||
File candidateDir = new File(objectsRoot, target);
|
||||
if (candidateDir.isDirectory()) {
|
||||
TreePlausibilizeBatch.walkIob(candidateDir, objectsRoot, out);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Director(description = "Convert .schem files in the 'convert' folder to .iob files.", descriptionKey = "iris.director.commandobject.director.convert_schem_files_convert_folder_iob_files")
|
||||
public void convert () {
|
||||
try {
|
||||
IrisConverter.convertSchematics(sender());
|
||||
} catch (Exception e) {
|
||||
Iris.reportError("Failed to convert schematics to Iris objects.", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Director(description = "Get a powder that reveals objects", descriptionKey = "iris.director.commandobject.director.get_powder_that_reveals_objects", aliases = "d")
|
||||
public void dust() {
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
onPlayerThread(player, () -> {
|
||||
player.getInventory().addItem(WandSVC.createDust());
|
||||
commandSender.playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f);
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Contract a selection based on your looking direction", descriptionKey = "iris.director.commandobject.director.contract_selection_based_on_your_looking_direction", aliases = "-")
|
||||
public void contract(
|
||||
@Param(description = "The amount to inset by", descriptionKey = "iris.director.commandobject.param.amount_inset_by", defaultValue = "1")
|
||||
int amount
|
||||
) {
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
onPlayerThread(player, () -> {
|
||||
if (!WandSVC.isHoldingWand(player)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Location[] b = WandSVC.getCuboid(player);
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED));
|
||||
return;
|
||||
}
|
||||
Location a1 = b[0].clone();
|
||||
Location a2 = b[1].clone();
|
||||
Cuboid cursor = new Cuboid(a1, a2);
|
||||
DirectionBasis d = DirectionBasis.closest(player.getLocation().getDirection()).reverse();
|
||||
assert d != null;
|
||||
cursor = cursor.expand(d.getCuboidDirection(), -amount);
|
||||
b[0] = cursor.getLowerNE();
|
||||
b[1] = cursor.getUpperSW();
|
||||
player.getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
|
||||
player.updateInventory();
|
||||
commandSender.playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Set point 1 to look", descriptionKey = "iris.director.commandobject.director.set_point_1_look", aliases = "p1")
|
||||
public void position1(
|
||||
@Param(description = "Whether to use your current position, or where you look", descriptionKey = "iris.director.commandobject.param.whether_use_your_current_position_where_you_look", defaultValue = "true")
|
||||
boolean here
|
||||
) {
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
onPlayerThread(player, () -> {
|
||||
if (!WandSVC.isHoldingWand(player)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_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", descriptionKey = "iris.director.commandobject.director.set_point_2_look", aliases = "p2")
|
||||
public void position2(
|
||||
@Param(description = "Whether to use your current position, or where you look", descriptionKey = "iris.director.commandobject.param.whether_use_your_current_position_where_you_look_2", defaultValue = "true")
|
||||
boolean here
|
||||
) {
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
onPlayerThread(player, () -> {
|
||||
if (!WandSVC.isHoldingWand(player)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND_2));
|
||||
return;
|
||||
}
|
||||
|
||||
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", descriptionKey = "iris.director.commandobject.director.paste_object", sync = true)
|
||||
public void paste(
|
||||
@Param(description = "The object to paste", descriptionKey = "iris.director.commandobject.param.object_paste", customHandler = ObjectHandler.class)
|
||||
String object,
|
||||
@Param(description = "Whether or not to edit the object (need to hold wand)", descriptionKey = "iris.director.commandobject.param.whether_not_edit_object_need_hold_wand", defaultValue = "false")
|
||||
boolean edit,
|
||||
@Param(description = "The amount of degrees to rotate by", descriptionKey = "iris.director.commandobject.param.amount_degrees_rotate_by", defaultValue = "0")
|
||||
int rotate,
|
||||
@Param(description = "Explicit scale factor, or dimension to inherit the current Iris world's factor", descriptionKey = "iris.director.commandobject.param.factor_by_which_scale_object_placement", defaultValue = "dimension", customHandler = ObjectScaleHandler.class)
|
||||
Double scale
|
||||
// ,
|
||||
// @Param(description = "The scale interpolator to use", descriptionKey = "iris.director.commandobject.param.scale_interpolator_use", defaultValue = "none")
|
||||
// IrisObjectPlacementScaleInterpolator interpolator
|
||||
) {
|
||||
IrisObject o = IrisData.loadAnyObject(object, data());
|
||||
Engine placementEngine = engine();
|
||||
double factor = scale == null
|
||||
? placementEngine == null ? 1D : placementEngine.getDimension().getAllObjectScaleFactor()
|
||||
: IrisObjectScale.requireValidFactor(scale, "scale");
|
||||
double maxScale = Double.max(10 - o.getBlocks().size() / 10000d, 1);
|
||||
if (factor > maxScale) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_INDICATED_SCALE_EXCEEDS_MAXIMUM_DOWNSCALED_MAXIMUM, MessageArgument.untrusted("maxScale", maxScale)));
|
||||
factor = maxScale;
|
||||
}
|
||||
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
placement.setRotation(IrisObjectRotation.of(0, rotate, 0));
|
||||
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
ItemStack wand = player.getInventory().getItemInMainHand();
|
||||
Block targetBlock = player.getTargetBlock(PASTE_TRANSPARENT_BLOCKS, 256);
|
||||
if (!isPasteTarget(targetBlock.getType())) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY));
|
||||
return;
|
||||
}
|
||||
Location block = targetBlock.getLocation().clone().add(0, 1, 0);
|
||||
|
||||
commandSender.playSound(Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1f, 1.5f);
|
||||
|
||||
Map<Block, BlockData> futureChanges = new HashMap<>();
|
||||
|
||||
if (factor != 1D) {
|
||||
o = scale == null ? IrisObjectScale.getFixed(o, factor)
|
||||
: new IrisObjectScale().setSize(factor)
|
||||
.setInterpolation(IrisObjectPlacementScaleInterpolator.TRICUBIC).get(new RNG(), o);
|
||||
}
|
||||
|
||||
// Block writes must run on the thread owning the target chunk; the undo log stays global.
|
||||
final IrisObject placed = o;
|
||||
if (!J.runAt(block, () -> {
|
||||
placed.place(block.getBlockX(), block.getBlockY() + (int) placed.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges, null), placement, new RNG(), placed.getLoader());
|
||||
J.runGlobal(() -> Iris.service(ObjectSVC.class).addChanges(futureChanges));
|
||||
|
||||
if (!edit) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_PLACED, MessageArgument.untrusted("object", object)));
|
||||
return;
|
||||
}
|
||||
|
||||
onPlayerThread(player, () -> {
|
||||
ObjectPasteBounds bounds = ObjectPasteBounds.resolve(placed, placement.getRotation(), block.getBlockX(),
|
||||
block.getBlockY() + placed.getCenter().getBlockY(), block.getBlockZ());
|
||||
Location minimum = new Location(block.getWorld(), bounds.minX(), bounds.minY(), bounds.minZ());
|
||||
Location maximum = new Location(block.getWorld(), bounds.maxX(), bounds.maxY(), bounds.maxZ());
|
||||
ItemStack newWand = WandSVC.createWand(maximum, minimum);
|
||||
if (WandSVC.isWand(wand)) {
|
||||
player.getInventory().setItemInMainHand(newWand);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", placed.getLoadKey())));
|
||||
} else {
|
||||
int slot = WandSVC.findWand(player.getInventory());
|
||||
if (slot == -1) {
|
||||
player.getInventory().addItem(newWand);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_GIVEN_NEW_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", placed.getLoadKey())));
|
||||
} else {
|
||||
player.getInventory().setItem(slot, newWand);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB_2, MessageArgument.untrusted("value", placed.getLoadKey())));
|
||||
}
|
||||
}
|
||||
});
|
||||
})) {
|
||||
Iris.warn("Could not schedule the object paste at " + block.getBlockX() + ", " + block.getBlockY() + ", " + block.getBlockZ() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the body on the thread owning the player, reporting when the hop cannot be scheduled.
|
||||
*/
|
||||
private void onPlayerThread(Player player, Runnable body) {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!J.runEntity(player, body)) {
|
||||
Iris.warn("Could not schedule /iris object on the thread owning " + player.getName() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Save an object", descriptionKey = "iris.director.commandobject.director.save_object")
|
||||
public void save(
|
||||
@Param(description = "The dimension to store the object in", descriptionKey = "iris.director.commandobject.param.dimension_store_object", contextual = true, contextualOverride = true)
|
||||
IrisDimension dimension,
|
||||
@Param(description = "The file to store it in, can use / for subfolders", descriptionKey = "iris.director.commandobject.param.file_store_it_can_use_subfolders")
|
||||
String name,
|
||||
@Param(description = "Overwrite existing object files", descriptionKey = "iris.director.commandobject.param.overwrite_existing_object_files", defaultValue = "false", aliases = "force")
|
||||
boolean overwrite,
|
||||
@Param(description = "Use legacy TileState serialization if possible", descriptionKey = "iris.director.commandobject.param.use_legacy_tilestate_serialization_if_possible", defaultValue = "true")
|
||||
boolean legacy
|
||||
) {
|
||||
IrisObject o = WandSVC.createSchematic(player(), legacy);
|
||||
|
||||
if (o == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_NEED_HOLD_YOUR_WAND));
|
||||
return;
|
||||
}
|
||||
|
||||
File file = Iris.service(StudioSVC.class).getWorkspaceFile(dimension.getLoadKey(), "objects", name + ".iob");
|
||||
|
||||
if (file.exists() && !overwrite) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FILE_ALREADY_EXISTS_SET_OVERWRITE_TRUE_OVERWRITE_IT));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
o.write(file, sender());
|
||||
} catch (IOException e) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FAILED_SAVE_OBJECT_BECAUSE_IOEXCEPTION, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
||||
Iris.reportError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
sender().playSound(Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1f, 1.5f);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_SUCCESSFULLY_OBJECT_SAVED_OBJECTS, MessageArgument.untrusted("value", dimension.getLoadKey()), MessageArgument.untrusted("name", name)));
|
||||
}
|
||||
|
||||
@Director(description = "Shift a selection in your looking direction", descriptionKey = "iris.director.commandobject.director.shift_selection_your_looking_direction")
|
||||
public void shift(
|
||||
@Param(description = "The amount to shift by", descriptionKey = "iris.director.commandobject.param.amount_shift_by", defaultValue = "1")
|
||||
int amount
|
||||
) {
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
onPlayerThread(player, () -> {
|
||||
if (!WandSVC.isHoldingWand(player)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_2));
|
||||
return;
|
||||
}
|
||||
|
||||
Location[] b = WandSVC.getCuboid(player);
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_2));
|
||||
return;
|
||||
}
|
||||
Location a1 = b[0].clone();
|
||||
Location a2 = b[1].clone();
|
||||
DirectionBasis d = DirectionBasis.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();
|
||||
commandSender.playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Undo a number of pastes", descriptionKey = "iris.director.commandobject.director.undo_number_pastes", aliases = "u")
|
||||
public void undo(
|
||||
@Param(description = "The amount of pastes to undo", descriptionKey = "iris.director.commandobject.param.amount_pastes_undo", defaultValue = "1")
|
||||
int amount
|
||||
) {
|
||||
ObjectSVC service = Iris.service(ObjectSVC.class);
|
||||
int actualReverts = Math.min(service.getUndos().size(), amount);
|
||||
service.revertChanges(actualReverts);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_REVERTED_PASTES, MessageArgument.untrusted("actualReverts", actualReverts)));
|
||||
}
|
||||
|
||||
@Director(description = "Gets an object wand and grabs the current WorldEdit selection.", descriptionKey = "iris.director.commandobject.director.gets_object_wand_grabs_current_worldedit_selection", aliases = "we", origin = DirectorOrigin.PLAYER)
|
||||
public void we() {
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
if (!Bukkit.getPluginManager().isPluginEnabled("WorldEdit")) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_CAN_T_GET_WORLDEDIT_SELECTION_WITHOUT_WORLDEDIT_YOU_KNOW));
|
||||
return;
|
||||
}
|
||||
|
||||
Cuboid locs = WorldEditLink.getSelection(player);
|
||||
|
||||
if (locs == null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_DON_T_HAVE_WORLDEDIT_SELECTION_THIS_WORLD));
|
||||
return;
|
||||
}
|
||||
|
||||
onPlayerThread(player, () -> {
|
||||
player.getInventory().addItem(WandSVC.createWand(locs.getLowerNE(), locs.getUpperSW()));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FRESH_WAND_WITH_YOUR_CURRENT_WORLDEDIT_SELECTION_ON_IT));
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Get an object wand", descriptionKey = "iris.director.commandobject.director.get_object_wand", sync = true)
|
||||
public void wand() {
|
||||
player().getInventory().addItem(WandSVC.createWand());
|
||||
sender().playSound(Sound.ITEM_ARMOR_EQUIP_NETHERITE, 1f, 1.5f);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_POOF_GOOD_LUCK_BUILDING));
|
||||
}
|
||||
|
||||
@Director(name = "x&y", description = "Autoselect up, down & out", descriptionKey = "iris.director.commandobject.director.autoselect_up_down_out", sync = true)
|
||||
public void xay() {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_3));
|
||||
return;
|
||||
}
|
||||
|
||||
Location[] b = WandSVC.getCuboid(player());
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_3));
|
||||
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(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_AUTO_SELECT_COMPLETE));
|
||||
}
|
||||
|
||||
@Director(name = "x+y", description = "Autoselect up & out", descriptionKey = "iris.director.commandobject.director.autoselect_up_out", sync = true)
|
||||
public void xpy() {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_4));
|
||||
return;
|
||||
}
|
||||
|
||||
Location[] b = WandSVC.getCuboid(player());
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_4));
|
||||
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(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_AUTO_SELECT_COMPLETE_2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.pack.validation.PackCompatReport;
|
||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.pack.PackResourceCleanup;
|
||||
import art.arcane.iris.pack.PackValidationCache;
|
||||
import art.arcane.iris.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.pack.PackValidationResult;
|
||||
import art.arcane.iris.pack.PackValidator;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.command.handlers.DimensionHandler;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.volmlib.util.localization.TextKey;
|
||||
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.iris.localization.BukkitRuntimeMessages;
|
||||
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance", descriptionKey = "iris.director.commandpack.director.pack_validation_maintenance")
|
||||
public class CommandPack implements DirectorExecutor {
|
||||
/**
|
||||
* Director treats a blank defaultValue as "required", which made the all-packs branch
|
||||
* unreachable from chat. The "*" sentinel keeps the parameter genuinely optional; a blank
|
||||
* value (the documented "pack=" escape hatch) still means every pack.
|
||||
*/
|
||||
static boolean wantsAllPacks(String pack) {
|
||||
return pack == null || pack.isBlank() || "*".equals(pack.trim());
|
||||
}
|
||||
|
||||
@Director(description = "Validate a pack (or all packs) and re-publish results", descriptionKey = "iris.director.commandpack.director.validate_pack_all_packs_re_publish_results", aliases = {"v"})
|
||||
public void validate(
|
||||
@Param(description = "The pack folder name to validate, or * for every pack", descriptionKey = "iris.director.commandpack.param.pack_folder_name_validate_leave_empty_all", defaultValue = "*")
|
||||
String pack
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
File packsRoot = Iris.instance.getDataFolder("packs");
|
||||
if (!packsRoot.isDirectory()) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_PACKS_FOLDER_NOT_FOUND));
|
||||
return;
|
||||
}
|
||||
|
||||
if (wantsAllPacks(pack)) {
|
||||
List<File> dirs = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
||||
if (dirs.isEmpty()) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_PACKS_VALIDATE));
|
||||
return;
|
||||
}
|
||||
int broken = 0;
|
||||
for (File dir : dirs) {
|
||||
PackValidationResult result = runValidate(s, dir);
|
||||
if (result != null && !result.isLoadable()) {
|
||||
broken++;
|
||||
}
|
||||
}
|
||||
persistValidationCache(packsRoot);
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_VALIDATION_COMPLETE_BROKEN_PACKS, MessageArgument.untrusted("broken", String.valueOf(broken)), MessageArgument.untrusted("value", String.valueOf(dirs.size()))));
|
||||
return;
|
||||
}
|
||||
|
||||
File target = PackDirectoryResolver.resolveExisting(packsRoot, pack);
|
||||
if (target == null) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_PACK_NOT_FOUND_UNDER_PACKS, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||
return;
|
||||
}
|
||||
runValidate(s, target);
|
||||
persistValidationCache(packsRoot);
|
||||
}
|
||||
|
||||
@Director(description = "Package a validated dimension into a compressed format", descriptionKey = "iris.director.commandstudio.director.package_dimension_into_compressed_format", aliases = "package")
|
||||
public void pkg(
|
||||
@Param(name = "dimension", description = "The dimension pack to compress", descriptionKey = "iris.director.commandstudio.param.dimension_pack_compress", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(name = "obfuscate", description = "Whether or not to obfuscate the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_obfuscate_pack", defaultValue = "false")
|
||||
boolean obfuscate,
|
||||
@Param(name = "minify", description = "Whether or not to minify the pack", descriptionKey = "iris.director.commandstudio.param.whether_not_minify_pack", defaultValue = "true")
|
||||
boolean minify
|
||||
) {
|
||||
Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify);
|
||||
}
|
||||
|
||||
@Director(description = "Preview or apply unused-resource cleanup", descriptionKey = "iris.director.commandpack.director.preview_apply_unused_resource_cleanup", aliases = {"c"})
|
||||
public void cleanup(
|
||||
@Param(description = "The pack folder name to clean", descriptionKey = "iris.director.commandpack.param.pack_folder_name_clean")
|
||||
String pack,
|
||||
@Param(description = "preview or apply", descriptionKey = "iris.director.commandpack.param.preview_apply", defaultValue = "preview")
|
||||
String mode
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
File packFolder = findPack(s, pack);
|
||||
if (packFolder == null) {
|
||||
return;
|
||||
}
|
||||
if ("apply".equalsIgnoreCase(mode)) {
|
||||
PackResourceCleanup.ApplyResult result = PackResourceCleanup.apply(packFolder);
|
||||
if (!result.success()) {
|
||||
s.sendMessage(IrisLanguage.text(
|
||||
BukkitRuntimeMessages.COMMAND_PACK_CLEANUP_FAILED,
|
||||
MessageArgument.untrusted("error", String.valueOf(result.error()))
|
||||
));
|
||||
reportPaths(s, result.quarantinedPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_STILL_QUARANTINED);
|
||||
return;
|
||||
}
|
||||
if (!result.changed()) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||
return;
|
||||
}
|
||||
PackValidationRegistry.remove(packFolder.getName());
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_QUARANTINED_CLEANUP_CANDIDATE_S_UNDER, MessageArgument.untrusted("size", String.valueOf(result.quarantinedPaths().size())), MessageArgument.untrusted("quarantinePath", String.valueOf(result.quarantinePath()))));
|
||||
reportPaths(s, result.quarantinedPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_QUARANTINED);
|
||||
return;
|
||||
}
|
||||
if (!"preview".equalsIgnoreCase(mode)) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_CLEANUP_MODE_MUST_BE_PREVIEW_APPLY));
|
||||
return;
|
||||
}
|
||||
PackResourceCleanup.Preview preview = PackResourceCleanup.preview(packFolder);
|
||||
if (!preview.success()) {
|
||||
s.sendMessage(IrisLanguage.text(
|
||||
BukkitRuntimeMessages.COMMAND_PACK_CLEANUP_FAILED,
|
||||
MessageArgument.untrusted("error", String.valueOf(preview.error()))
|
||||
));
|
||||
return;
|
||||
}
|
||||
if (!preview.hasCandidates()) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_CLEANUP_CANDIDATES_FOUND_PACK_2, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||
return;
|
||||
}
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_CLEANUP_PREVIEW_PACK_CANDIDATE_S_NO_FILES_WERE_CHANGED, MessageArgument.untrusted("pack", String.valueOf(pack)), MessageArgument.untrusted("size", String.valueOf(preview.candidatePaths().size()))));
|
||||
reportPaths(s, preview.candidatePaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_CANDIDATE);
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RUN_IRIS_PACK_CLEANUP_MODE_APPLY_QUARANTINE_THESE_CANDIDATES_AFTER_FRESH_SCAN, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||
}
|
||||
|
||||
@Director(description = "Preview or apply restoration of the latest quarantine", descriptionKey = "iris.director.commandpack.director.preview_apply_restoration_latest_quarantine", aliases = {"r"})
|
||||
public void restore(
|
||||
@Param(description = "The pack folder name to restore", descriptionKey = "iris.director.commandpack.param.pack_folder_name_restore")
|
||||
String pack,
|
||||
@Param(description = "preview or apply", descriptionKey = "iris.director.commandpack.param.preview_apply_2", defaultValue = "preview")
|
||||
String mode
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
File packFolder = findPack(s, pack);
|
||||
if (packFolder == null) {
|
||||
return;
|
||||
}
|
||||
if ("apply".equalsIgnoreCase(mode)) {
|
||||
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(packFolder);
|
||||
if (!result.conflicts().isEmpty()) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORE_REFUSED_BECAUSE_DESTINATION_S_ALREADY_EXIST, MessageArgument.untrusted("size", String.valueOf(result.conflicts().size()))));
|
||||
reportPaths(s, result.conflicts(), BukkitRuntimeMessages.COMMAND_PACK_PATH_CONFLICT);
|
||||
return;
|
||||
}
|
||||
if (!result.success()) {
|
||||
s.sendMessage(IrisLanguage.text(
|
||||
BukkitRuntimeMessages.COMMAND_PACK_RESTORE_FAILED,
|
||||
MessageArgument.untrusted("error", String.valueOf(result.error()))
|
||||
));
|
||||
return;
|
||||
}
|
||||
if (!result.changed()) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NOTHING_RESTORE_PACK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||
return;
|
||||
}
|
||||
PackValidationRegistry.remove(packFolder.getName());
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORED_FILE_S_FROM, MessageArgument.untrusted("size", String.valueOf(result.restoredPaths().size())), MessageArgument.untrusted("dumpPath", String.valueOf(result.dumpPath()))));
|
||||
reportPaths(s, result.restoredPaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_RESTORED);
|
||||
return;
|
||||
}
|
||||
if (!"preview".equalsIgnoreCase(mode)) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORE_MODE_MUST_BE_PREVIEW_APPLY));
|
||||
return;
|
||||
}
|
||||
PackResourceCleanup.RestorePreview preview = PackResourceCleanup.previewRestore(packFolder);
|
||||
if (!preview.success()) {
|
||||
s.sendMessage(IrisLanguage.text(
|
||||
BukkitRuntimeMessages.COMMAND_PACK_RESTORE_FAILED,
|
||||
MessageArgument.untrusted("error", String.valueOf(preview.error()))
|
||||
));
|
||||
return;
|
||||
}
|
||||
if (!preview.hasFiles()) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NOTHING_RESTORE_PACK_2, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||
return;
|
||||
}
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORE_PREVIEW_FILE_S_NO_FILES_WERE_CHANGED, MessageArgument.untrusted("dumpPath", String.valueOf(preview.dumpPath())), MessageArgument.untrusted("size", String.valueOf(preview.filePaths().size()))));
|
||||
reportPaths(s, preview.filePaths(), BukkitRuntimeMessages.COMMAND_PACK_PATH_FILE);
|
||||
if (!preview.conflicts().isEmpty()) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RESTORE_IS_BLOCKED_BY_EXISTING_DESTINATION_S, MessageArgument.untrusted("size", String.valueOf(preview.conflicts().size()))));
|
||||
reportPaths(s, preview.conflicts(), BukkitRuntimeMessages.COMMAND_PACK_PATH_CONFLICT);
|
||||
return;
|
||||
}
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_RUN_IRIS_PACK_RESTORE_MODE_APPLY_RESTORE_AFTER_FRESH_CONFLICT_CHECK, MessageArgument.untrusted("pack", String.valueOf(pack))));
|
||||
}
|
||||
|
||||
@Director(description = "Show cached validation status for a pack", descriptionKey = "iris.director.commandpack.director.show_cached_validation_status_pack", aliases = {"s"})
|
||||
public void status(
|
||||
@Param(description = "The pack folder name, or * for every pack", descriptionKey = "iris.director.commandpack.param.pack_folder_name", defaultValue = "*")
|
||||
String pack
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
if (wantsAllPacks(pack)) {
|
||||
if (PackValidationRegistry.snapshot().isEmpty()) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST));
|
||||
return;
|
||||
}
|
||||
PackValidationRegistry.snapshot().forEach((name, result) -> {
|
||||
TextKey key = result.isLoadable()
|
||||
? BukkitRuntimeMessages.COMMAND_PACK_STATUS_OK
|
||||
: BukkitRuntimeMessages.COMMAND_PACK_STATUS_BROKEN;
|
||||
s.sendMessage(IrisLanguage.text(
|
||||
key,
|
||||
MessageArgument.untrusted("pack", name),
|
||||
MessageArgument.trusted("blocking", result.getBlockingErrors().size()),
|
||||
MessageArgument.trusted("warnings", result.getWarnings().size())
|
||||
));
|
||||
});
|
||||
return;
|
||||
}
|
||||
PackValidationResult result = PackValidationRegistry.get(pack);
|
||||
if (result == null) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULT_RUN_IRIS_PACK_VALIDATE, MessageArgument.untrusted("pack", String.valueOf(pack)), MessageArgument.untrusted("pack2", String.valueOf(pack))));
|
||||
return;
|
||||
}
|
||||
reportResult(s, result);
|
||||
}
|
||||
|
||||
@Director(description = "Show pack content that cannot generate on this Minecraft version", descriptionKey = "iris.director.commandpack.director.show_content_cannot_generate_this_minecraft_version", aliases = {"cp"})
|
||||
public void compat(
|
||||
@Param(description = "The pack folder name to report on, or * for every pack", descriptionKey = "iris.director.commandpack.param.pack_folder_name_compat", defaultValue = "*")
|
||||
String pack
|
||||
) {
|
||||
VolmitSender s = sender();
|
||||
if (wantsAllPacks(pack)) {
|
||||
Map<String, PackValidationResult> snapshot = PackValidationRegistry.snapshot();
|
||||
if (snapshot.isEmpty()) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULTS_RECORDED_RUN_IRIS_PACK_VALIDATE_FIRST));
|
||||
return;
|
||||
}
|
||||
for (Map.Entry<String, PackValidationResult> entry : snapshot.entrySet()) {
|
||||
for (String line : compatOutput(entry.getKey(), entry.getValue())) {
|
||||
s.sendMessage(line);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (String line : compatOutput(pack, PackValidationRegistry.get(pack))) {
|
||||
s.sendMessage(line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The compat listing for one pack, straight from the persisted validation so no pack is reloaded. Every key is
|
||||
* printed with every subject; the boot listing is the capped form of the same data.
|
||||
*/
|
||||
static List<String> compatOutput(String pack, PackValidationResult result) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
if (result == null) {
|
||||
lines.add(IrisLanguage.text(
|
||||
BukkitRuntimeMessages.COMMAND_PACK_NO_VALIDATION_RESULT_RUN_IRIS_PACK_VALIDATE,
|
||||
MessageArgument.untrusted("pack", String.valueOf(pack)),
|
||||
MessageArgument.untrusted("pack2", String.valueOf(pack))));
|
||||
return lines;
|
||||
}
|
||||
String version = compatVersion(result);
|
||||
if (result.getCompatFindings().isEmpty()) {
|
||||
lines.add(IrisLanguage.text(
|
||||
BukkitRuntimeMessages.COMMAND_PACK_COMPAT_NONE,
|
||||
MessageArgument.untrusted("pack", pack),
|
||||
MessageArgument.untrusted("version", version)));
|
||||
return lines;
|
||||
}
|
||||
lines.add(IrisLanguage.text(
|
||||
BukkitRuntimeMessages.COMMAND_PACK_COMPAT_HEADER,
|
||||
MessageArgument.untrusted("pack", pack),
|
||||
MessageArgument.untrusted("version", version)));
|
||||
for (String line : PackCompatReport.of(result.getCompatFindings()).keyLines(0)) {
|
||||
lines.add(IrisLanguage.text(
|
||||
BukkitRuntimeMessages.COMMAND_PACK_MESSAGE_2,
|
||||
MessageArgument.untrusted("value", line)));
|
||||
}
|
||||
lines.add(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_COMPAT_REMEDY));
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** The live Minecraft version, falling back to the one the validation ran against. */
|
||||
static String compatVersion(PackValidationResult result) {
|
||||
if (IrisPlatforms.isBound()) {
|
||||
String live = IrisPlatforms.get().minecraftVersion();
|
||||
if (live != null && !live.isBlank()) {
|
||||
return live;
|
||||
}
|
||||
}
|
||||
String persisted = result.getMinecraftVersion();
|
||||
return persisted == null || persisted.isBlank() ? "unknown" : persisted;
|
||||
}
|
||||
|
||||
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);
|
||||
String detail = e.getMessage() == null || e.getMessage().isBlank()
|
||||
? e.getClass().getSimpleName()
|
||||
: e.getMessage();
|
||||
PackValidationResult result = new PackValidationResult(
|
||||
packFolder.getName(),
|
||||
List.of("Pack validation failed with " + e.getClass().getSimpleName() + ": " + detail),
|
||||
List.of(),
|
||||
System.currentTimeMillis());
|
||||
PackValidationRegistry.publish(result);
|
||||
reportResult(s, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private void persistValidationCache(File packsRoot) {
|
||||
List<File> packDirectories = PackDirectoryResolver.listVisiblePackDirectories(packsRoot);
|
||||
List<PackValidationResult> results = new ArrayList<>(packDirectories.size());
|
||||
for (File packDirectory : packDirectories) {
|
||||
PackValidationResult result = PackValidationRegistry.get(packDirectory.getName());
|
||||
if (result == null) {
|
||||
// The boot cache only loads when it covers every pack, so a partial write is
|
||||
// pointless - but say so instead of silently skipping the persist forever.
|
||||
Iris.warn("Pack validation cache not written: \"" + packDirectory.getName()
|
||||
+ "\" has no registered result. Run /iris pack validate all to repopulate it.");
|
||||
return;
|
||||
}
|
||||
results.add(result);
|
||||
}
|
||||
try {
|
||||
PackValidationCache.save(
|
||||
Iris.instance.getDataFile("cache", "pack-validation.json").toPath(),
|
||||
PackValidationCache.contentFingerprint(packsRoot),
|
||||
PackValidationCache.contextFingerprint(),
|
||||
results);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
Iris.reportError("Could not persist refreshed pack-validation results", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void reportResult(VolmitSender s, PackValidationResult result) {
|
||||
if (result.isLoadable()) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_PACK_IS_LOADABLE_WARNINGS, MessageArgument.untrusted("packName", String.valueOf(result.getPackName())), MessageArgument.untrusted("size", String.valueOf(result.getWarnings().size()))));
|
||||
} else {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_PACK_IS_BROKEN, MessageArgument.untrusted("packName", String.valueOf(result.getPackName()))));
|
||||
for (String reason : result.getBlockingErrors()) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_MESSAGE, MessageArgument.untrusted("reason", String.valueOf(reason))));
|
||||
}
|
||||
}
|
||||
int wMax = Math.min(10, result.getWarnings().size());
|
||||
for (int i = 0; i < wMax; i++) {
|
||||
s.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.COMMAND_PACK_MESSAGE_2, MessageArgument.untrusted("value", String.valueOf(result.getWarnings().get(i)))));
|
||||
}
|
||||
if (result.getWarnings().size() > wMax) {
|
||||
s.sendMessage(IrisLanguage.text(
|
||||
BukkitRuntimeMessages.COMMAND_PACK_MORE_WARNING_S,
|
||||
MessageArgument.trusted("count", result.getWarnings().size() - wMax)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private File findPack(VolmitSender sender, String pack) {
|
||||
if (pack == null || pack.isBlank()) {
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_PACK_YOU_MUST_SPECIFY_PACK_NAME));
|
||||
return null;
|
||||
}
|
||||
File packFolder = PackDirectoryResolver.resolveExisting(Iris.instance.getDataFolder("packs"), pack);
|
||||
if (packFolder == null) {
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_PACK_PACK_NOT_FOUND_UNDER_PACKS, MessageArgument.untrusted("pack", pack)));
|
||||
return null;
|
||||
}
|
||||
return packFolder;
|
||||
}
|
||||
|
||||
private void reportPaths(VolmitSender sender, List<String> paths, TextKey label) {
|
||||
int max = Math.min(10, paths.size());
|
||||
for (int i = 0; i < max; i++) {
|
||||
sender.sendMessage(IrisLanguage.text(
|
||||
BukkitCommandMessages.COMMAND_PACK_MESSAGE,
|
||||
MessageArgument.untrusted("label", IrisLanguage.text(label)),
|
||||
MessageArgument.untrusted("value", paths.get(i))
|
||||
));
|
||||
}
|
||||
if (paths.size() > max) {
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_PACK_MORE, MessageArgument.untrusted("value", (paths.size() - max))));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import art.arcane.iris.localization.IrisMessages;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.studio.view.PregeneratorJob;
|
||||
import art.arcane.iris.world.pregen.PregenTask;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.RuntimeUiMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
@Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!", descriptionKey = "iris.director.commandpregen.director.pregenerate_your_iris_worlds")
|
||||
public class CommandPregen implements DirectorExecutor {
|
||||
@Director(description = "Pregenerate a world", descriptionKey = "iris.director.commandpregen.director.pregenerate_world")
|
||||
public void start(
|
||||
@Param(description = "The radius of the pregen in blocks", descriptionKey = "iris.director.commandpregen.param.radius_pregen_blocks", aliases = "size")
|
||||
int radius,
|
||||
@Param(description = "The world to pregen", descriptionKey = "iris.director.commandpregen.param.world_pregen", contextual = true, contextualOverride = true)
|
||||
World world,
|
||||
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", descriptionKey = "iris.director.commandpregen.param.center_location_pregen_use_me_your_current_location", defaultValue = "0,0")
|
||||
Vector center,
|
||||
@Param(description = "Open the Iris pregen gui", descriptionKey = "iris.director.commandpregen.param.open_iris_pregen_gui", defaultValue = "true")
|
||||
boolean gui,
|
||||
@Param(name = "serial", description = "Generate only one chunk at a time", descriptionKey = "iris.director.commandpregen.param.generate_only_one_chunk_at_time", defaultValue = "false")
|
||||
boolean serial
|
||||
) {
|
||||
if (radius <= 0) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_PREGEN_RADIUS_MUST_BE_GREATER_THAN_ZERO_BLOCKS));
|
||||
return;
|
||||
}
|
||||
if (serial && !IrisToolbelt.supportsStrictSerialPregeneration()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_STRICT_SERIAL_PREGENERATION_REQUIRES_PAPER_PAPER_COMPATIBLE_SERVER));
|
||||
return;
|
||||
}
|
||||
|
||||
if (PregeneratorJob.getInstance() != null) {
|
||||
// Reject like the modded adapter does; never silently kill a running job.
|
||||
sender().sendMessage(IrisLanguage.text(IrisMessages.PREGEN_ALREADY_RUNNING));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (sender().isPlayer() && access() == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_ENGINE_ACCESS_THIS_WORLD_IS_NULL));
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_PLEASE_MAKE_SURE_WORLD_IS_LOADED_ENGINE_IS_INITIALIZED_GENERATE));
|
||||
}
|
||||
PregenTask task = PregenTask
|
||||
.builder()
|
||||
.center(new Position2(center.getBlockX(), center.getBlockZ()))
|
||||
.gui(gui)
|
||||
.radiusX(radius)
|
||||
.radiusZ(radius)
|
||||
.build();
|
||||
if (serial) {
|
||||
IrisToolbelt.pregenerateSerial(task, world);
|
||||
} else {
|
||||
IrisToolbelt.pregenerate(task, world);
|
||||
}
|
||||
String msg = C.GREEN + "Pregen started in " + C.GOLD + world.getName() + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
|
||||
sender().sendMessage(msg);
|
||||
Iris.info(msg);
|
||||
} catch (Throwable e) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_FAILED_START_PREGENERATION_SEE_CONSOLE_DETAILS));
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Stop the active pregeneration task", descriptionKey = "iris.director.commandpregen.director.stop_active_pregeneration_task", aliases = "x")
|
||||
public void stop() {
|
||||
if (PregeneratorJob.shutdownInstance()) {
|
||||
String message = C.BLUE + "Pregen stop requested; finishing active work before cancellation.";
|
||||
sender().sendMessage(message);
|
||||
Iris.info(message);
|
||||
} else {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_NO_ACTIVE_PREGENERATION_TASKS_STOP));
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Pause / continue the active pregeneration task", descriptionKey = "iris.director.commandpregen.director.pause_continue_active_pregeneration_task", aliases = {"resume"})
|
||||
public void pause() {
|
||||
if (PregeneratorJob.pauseResume()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_PAUSED_UNPAUSED_PREGENERATION_TASK_NOW, MessageArgument.trusted("value", IrisLanguage.text(PregeneratorJob.isPaused() ? RuntimeUiMessages.STATUS_PAUSED : RuntimeUiMessages.STATUS_RUNNING))));
|
||||
} else {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_NO_ACTIVE_PREGENERATION_TASKS_PAUSE_UNPAUSE));
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Show the active pregeneration status", descriptionKey = "iris.director.commandpregen.director.show_active_pregeneration_status")
|
||||
public void status() {
|
||||
PregeneratorJob.PregenProgress progress = PregeneratorJob.progressSnapshot();
|
||||
if (progress == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_NO_ACTIVE_PREGENERATION_TASK));
|
||||
return;
|
||||
}
|
||||
|
||||
String world = progress.worldName() == null ? "?" : progress.worldName();
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_PREGEN, MessageArgument.untrusted("world", world), MessageArgument.untrusted("value", Form.f(progress.generated())), MessageArgument.untrusted("value2", Form.f(progress.totalChunks())), MessageArgument.untrusted("value3", String.format("%.1f", progress.percent())), MessageArgument.untrusted("value4", (progress.paused() ? C.YELLOW + " PAUSED" : ""))));
|
||||
sender().sendMessage(IrisLanguage.text(
|
||||
BukkitCommandMessagesExtended.COMMAND_PREGEN_SPEED_S_ETA_ELAPSED_METHOD,
|
||||
MessageArgument.untrusted("overall", Form.f(progress.overallChunksPerSecond(), 1)),
|
||||
MessageArgument.untrusted("tenSecond", Form.f(progress.chunksPerSecond(), 1)),
|
||||
MessageArgument.untrusted("thirtySecond", Form.f(progress.thirtySecondChunksPerSecond(), 1)),
|
||||
MessageArgument.untrusted("sixtySecond", Form.f(progress.sixtySecondChunksPerSecond(), 1)),
|
||||
MessageArgument.untrusted("eta", Form.duration(progress.eta(), 2)),
|
||||
MessageArgument.untrusted("elapsed", Form.duration(progress.elapsed(), 2)),
|
||||
MessageArgument.untrusted("method", progress.method()),
|
||||
MessageArgument.untrusted("failures", progress.failed() > 0 ? C.RED + " Failed: " + Form.f(progress.failed()) : "")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.cache.AtomicCache;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.director.DirectorEngineOptions;
|
||||
import art.arcane.volmlib.util.director.compat.DirectorEngineFactory;
|
||||
import art.arcane.volmlib.util.director.context.DirectorContextRegistry;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorExecutionMode;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorExecutionResult;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorInvocation;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorInvocationHook;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorRuntimeEngine;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorRuntimeNode;
|
||||
import art.arcane.volmlib.util.director.runtime.DirectorSender;
|
||||
import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
|
||||
import art.arcane.volmlib.util.director.help.DirectorMiniMenu.DirectorHelpPage;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.IrisMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||
import art.arcane.volmlib.util.plugin.ComponentMessenger;
|
||||
public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, DirectorInvocationHook {
|
||||
private static final String ROOT_COMMAND = "iris";
|
||||
private static final String ROOT_PERMISSION = "iris.all";
|
||||
|
||||
private final ThreadLocal<Deque<LanguageAudience.Scope>> languageScopes = ThreadLocal.withInitial(ArrayDeque::new);
|
||||
|
||||
private final transient AtomicCache<DirectorRuntimeEngine> directorCache = new AtomicCache<>();
|
||||
private final transient ThreadLocal<CommandSender> dispatchSenders = new ThreadLocal<>();
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
PluginCommand command = findBukkitCommand();
|
||||
if (command != null) {
|
||||
command.setExecutor(this);
|
||||
command.setTabCompleter(this);
|
||||
} else {
|
||||
registerPaperCommand();
|
||||
}
|
||||
J.a(this::getDirector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
|
||||
}
|
||||
|
||||
public DirectorRuntimeEngine getDirector() {
|
||||
return directorCache.aquireNastyPrint(() -> DirectorEngineFactory.create(
|
||||
new CommandIris(),
|
||||
DirectorEngineOptions.builder()
|
||||
.contexts(buildDirectorContexts())
|
||||
.dispatcher(this::dispatchDirector)
|
||||
.invocationHook(this)
|
||||
.legacyHandlers(DirectorSystem.handlers)
|
||||
.textResolver(IrisLanguage.directorResolver())
|
||||
.build()
|
||||
));
|
||||
}
|
||||
|
||||
private DirectorContextRegistry buildDirectorContexts() {
|
||||
DirectorContextRegistry contexts = new DirectorContextRegistry();
|
||||
contexts.register(CommandSender.class, (invocation, map) -> {
|
||||
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
|
||||
return sender.sender();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
for (Map.Entry<Class<?>, DirectorContextHandler<?>> entry : DirectorContextHandler.contextHandlers.entrySet()) {
|
||||
registerContextHandler(contexts, entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
return contexts;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private void registerContextHandler(DirectorContextRegistry contexts, Class<?> type, DirectorContextHandler<?> handler) {
|
||||
contexts.register((Class) type, (invocation, map) -> {
|
||||
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
|
||||
return ((DirectorContextHandler) handler).handle(new VolmitSender(sender.sender()));
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private void dispatchDirector(DirectorExecutionMode mode, Runnable runnable) {
|
||||
if (mode != DirectorExecutionMode.SYNC) {
|
||||
runnable.run();
|
||||
return;
|
||||
}
|
||||
CommandSender sender = dispatchSenders.get();
|
||||
if (sender instanceof Player player) {
|
||||
if (!J.runEntity(player, runnable)) {
|
||||
throw new IllegalStateException("Failed to schedule player command on its entity thread");
|
||||
}
|
||||
return;
|
||||
}
|
||||
J.s(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
|
||||
if (invocation.getSender() instanceof BukkitDirectorSender sender) {
|
||||
languageScopes.get().push(LanguageAudience.open(sender.sender() instanceof Player player ? player.getUniqueId() : null));
|
||||
DirectorContext.touch(new VolmitSender(sender.sender()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterInvoke(DirectorInvocation invocation, DirectorRuntimeNode node) {
|
||||
DirectorContext.remove();
|
||||
Deque<LanguageAudience.Scope> scopes = languageScopes.get();
|
||||
if (!scopes.isEmpty()) {
|
||||
scopes.pop().close();
|
||||
}
|
||||
if (scopes.isEmpty()) {
|
||||
languageScopes.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
|
||||
if (!command.getName().equalsIgnoreCase(ROOT_COMMAND)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return tabCompleteRoot(sender, alias, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
if (!command.getName().equalsIgnoreCase(ROOT_COMMAND)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
executeRoot(sender, label, args);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void executeRoot(CommandSender sender, String label, String[] args) {
|
||||
if (args.length > 0 && args[0].equalsIgnoreCase("language")) {
|
||||
Iris.instance.selectLanguage(sender, Arrays.copyOfRange(args, 1, args.length));
|
||||
return;
|
||||
}
|
||||
if (!(args.length > 1 && args[0].equalsIgnoreCase("debug") && args[1].equalsIgnoreCase("dump"))
|
||||
&& !sender.hasPermission(ROOT_PERMISSION)) {
|
||||
ComponentMessenger.sendSection(sender, IrisLanguage.text(
|
||||
IrisMessages.COMMAND_PERMISSION_DENIED,
|
||||
MessageArgument.trusted("permission", ROOT_PERMISSION)
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
J.aBukkit(() -> executeCommand(sender, label, args));
|
||||
}
|
||||
|
||||
public List<String> tabCompleteRoot(CommandSender sender, String alias, String[] args) {
|
||||
List<String> suggestions = args.length > 0 && args[0].equalsIgnoreCase("language")
|
||||
? Iris.instance.completeLanguage(sender, Arrays.copyOfRange(args, 1, args.length))
|
||||
: runDirectorTab(sender, alias, args);
|
||||
if (sender instanceof Player player && IrisSettings.get().getGeneral().isCommandSounds()) {
|
||||
player.playSound(player.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 0.25f, RNG.r.f(0.125f, 1.95f));
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
private PluginCommand findBukkitCommand() {
|
||||
try {
|
||||
return Iris.instance.getCommand(ROOT_COMMAND);
|
||||
} catch (UnsupportedOperationException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void registerPaperCommand() {
|
||||
try {
|
||||
Class<?> registrarType = Class.forName(
|
||||
"art.arcane.iris.command.PaperCommandRegistrar",
|
||||
true,
|
||||
getClass().getClassLoader()
|
||||
);
|
||||
Method register = registrarType.getDeclaredMethod("register", Iris.class, CommandSVC.class);
|
||||
register.invoke(null, Iris.instance, this);
|
||||
} catch (InvocationTargetException e) {
|
||||
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||
throw new IllegalStateException("Paper command registration failed", cause);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException("Paper command registrar is unavailable", e);
|
||||
} catch (LinkageError e) {
|
||||
throw new IllegalStateException("Paper command APIs are unavailable", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void executeCommand(CommandSender sender, String label, String[] args) {
|
||||
if (sendHelpIfRequested(sender, args)) {
|
||||
playSuccessSound(sender);
|
||||
return;
|
||||
}
|
||||
|
||||
DirectorExecutionResult result = runDirector(sender, label, args);
|
||||
|
||||
if (result.isSuccess()) {
|
||||
playSuccessSound(sender);
|
||||
return;
|
||||
}
|
||||
|
||||
playFailureSound(sender);
|
||||
if (result.getMessage() == null || result.getMessage().trim().isEmpty()) {
|
||||
new VolmitSender(sender).sendMessage(C.RED + IrisLanguage.text(IrisMessages.COMMAND_UNKNOWN));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean sendHelpIfRequested(CommandSender sender, String[] args) {
|
||||
try (LanguageAudience.Scope audience = LanguageAudience.open(sender instanceof Player player ? player.getUniqueId() : null)) {
|
||||
Optional<DirectorHelpPage> request = DirectorMiniMenu.resolveHelp(getDirector(), Arrays.asList(args));
|
||||
if (request.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DirectorMiniMenu.deliver(
|
||||
sender,
|
||||
request.get(),
|
||||
DirectorMiniMenu.Theme.irisGreen(),
|
||||
IrisLanguage.directorResolver()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) {
|
||||
dispatchSenders.set(sender);
|
||||
DirectorContext.touch(new VolmitSender(sender));
|
||||
try (LanguageAudience.Scope audience = LanguageAudience.open(sender instanceof Player player ? player.getUniqueId() : null)) {
|
||||
return getDirector().execute(new DirectorInvocation(new BukkitDirectorSender(sender), label, Arrays.asList(args)));
|
||||
} catch (Throwable e) {
|
||||
Iris.warn("Director command execution failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
|
||||
return DirectorExecutionResult.notHandled();
|
||||
} finally {
|
||||
DirectorContext.remove();
|
||||
dispatchSenders.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> runDirectorTab(CommandSender sender, String alias, String[] args) {
|
||||
DirectorContext.touch(new VolmitSender(sender));
|
||||
try (LanguageAudience.Scope audience = LanguageAudience.open(sender instanceof Player player ? player.getUniqueId() : null)) {
|
||||
return getDirector().tabComplete(new DirectorInvocation(new BukkitDirectorSender(sender), alias, Arrays.asList(args)));
|
||||
} catch (Throwable e) {
|
||||
Iris.warn("Director tab completion failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
|
||||
return List.of();
|
||||
} finally {
|
||||
DirectorContext.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private void playFailureSound(CommandSender sender) {
|
||||
if (!IrisSettings.get().getGeneral().isCommandSounds()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender instanceof Player player) {
|
||||
J.runEntity(player, () -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 0.25f,
|
||||
Sound.BLOCK_BEACON_DEACTIVATE, 0.2f, 0.45f));
|
||||
}
|
||||
}
|
||||
|
||||
private void playSuccessSound(CommandSender sender) {
|
||||
if (!IrisSettings.get().getGeneral().isCommandSounds()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender instanceof Player player) {
|
||||
J.runEntity(player, () -> playSounds(player, Sound.BLOCK_AMETHYST_CLUSTER_BREAK, 0.77f, 1.65f,
|
||||
Sound.BLOCK_RESPAWN_ANCHOR_CHARGE, 0.125f, 2.99f));
|
||||
}
|
||||
}
|
||||
|
||||
private static final java.util.concurrent.atomic.AtomicBoolean SOUND_LOGGED = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
|
||||
private void playSounds(Player player, Sound a, float av, float ap, Sound b, float bv, float bp) {
|
||||
try {
|
||||
player.playSound(player.getLocation(), a, av, ap);
|
||||
player.playSound(player.getLocation(), b, bv, bp);
|
||||
} catch (Throwable e) {
|
||||
if (SOUND_LOGGED.compareAndSet(false, true)) {
|
||||
try {
|
||||
java.io.File f = Iris.instance.getDataFile("adventure-debug.txt");
|
||||
java.nio.file.Files.writeString(f.toPath(), "SOUND FAIL: " + e.getClass().getName() + ": " + e.getMessage() + "\n", java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record BukkitDirectorSender(CommandSender sender) implements DirectorSender {
|
||||
@Override
|
||||
public String getName() {
|
||||
return sender.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlayer() {
|
||||
return sender instanceof Player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(String message) {
|
||||
if (message != null && !message.trim().isEmpty()) {
|
||||
ComponentMessenger.sendLiteral(sender, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.structure.BulkStructureImporter;
|
||||
import art.arcane.iris.structure.StructureCaptureImporter;
|
||||
import art.arcane.iris.structure.StructureImporter;
|
||||
import art.arcane.iris.structure.StructureIndexService;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.structure.placement.PlacedStructurePiece;
|
||||
import art.arcane.iris.structure.placement.StructureAssembler;
|
||||
import art.arcane.iris.structure.placement.StructureReachability;
|
||||
import art.arcane.iris.structure.graph.StructureAssemblyResult;
|
||||
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
import art.arcane.iris.structure.placement.IrisStructure;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureGenerationStatus;
|
||||
import art.arcane.iris.structure.object.ObjectPlaceMode;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.generator.structure.Structure;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
@Director(name = "structure", aliases = {"struct", "str"}, description = "Iris structure tools (index, import, info)", descriptionKey = "iris.director.commandstructure.director.iris_structure_tools_index_import_info")
|
||||
public class CommandStructure implements DirectorExecutor {
|
||||
@Director(description = "Regenerate structure-index.json listing all vanilla, datapack & iris structures", descriptionKey = "iris.director.commandstructure.director.regenerate_structure_index_json_listing_all_vanilla_datapack_iris_structures", aliases = {"ls"}, origin = DirectorOrigin.BOTH)
|
||||
public void list(
|
||||
@Param(description = "The dimension whose pack to index", descriptionKey = "iris.director.commandstructure.param.dimension_whose_pack_index", aliases = "dim")
|
||||
IrisDimension dimension
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION, MessageArgument.untrusted("value", dimension.getLoadKey())));
|
||||
return;
|
||||
}
|
||||
File file = StructureIndexService.write(data);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_WROTE_STRUCTURE_INDEX, MessageArgument.untrusted("value", file.getPath())));
|
||||
}
|
||||
|
||||
@Director(name = "import", description = "Import EVERY structure - vanilla AND ingested datapacks - into this pack as editable Iris resources, always overwriting. Rebuilds jigsaw structures (villages, outposts, datapack jigsaws) as editable pool/piece graphs, imports every structure template NBT as an object, and assembles the multi-template structures (shipwrecks, ruined portals, ocean ruins, nether fossils). Run after ingesting a datapack and restarting. Regenerate chunks or use a fresh world for the imported copies to place.", descriptionKey = "iris.director.commandstructure.director.import_every_structure_vanilla_ingested_datapacks_into_this_pack_as_editable_iris", aliases = {"import-all", "reimport", "imp", "all"}, origin = DirectorOrigin.BOTH)
|
||||
public void importAll(
|
||||
@Param(description = "The dimension whose pack to import into", descriptionKey = "iris.director.commandstructure.param.dimension_whose_pack_import_into", aliases = "dim")
|
||||
IrisDimension dimension
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_2, MessageArgument.untrusted("value", dimension.getLoadKey())));
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_IMPORTING_ALL_VANILLA_DATAPACK_STRUCTURES_INTO_OVERWRITE, MessageArgument.untrusted("value", dimension.getLoadKey())));
|
||||
BulkStructureImporter.Report jigsaws = BulkStructureImporter.importAllVanilla(data, StructureImporter.Mode.OVERWRITE, true, sender());
|
||||
BulkStructureImporter.Report templates = BulkStructureImporter.importAllTemplates(data, StructureImporter.Mode.OVERWRITE, sender());
|
||||
BulkStructureImporter.Report groups = BulkStructureImporter.importTemplateGroups(data, StructureImporter.Mode.OVERWRITE, sender());
|
||||
StructureCaptureImporter.Report captured = StructureCaptureImporter.importStructures(
|
||||
data, StructureImporter.Mode.OVERWRITE, sender(), jigsaws.captureCandidates());
|
||||
int imported = jigsaws.imported() + templates.imported() + groups.imported() + captured.imported();
|
||||
int failed = jigsaws.failed() + templates.failed() + groups.failed() + captured.failed();
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_IMPORT_COMPLETE_STRUCTURES_OBJECTS_WRITTEN_FAILED, MessageArgument.untrusted("imported", imported), MessageArgument.untrusted("failed", failed)));
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_REFERENCE_THEM_FROM_BIOME_REGION_DIMENSION_STRUCTURES_LIST_RUN_IRIS, MessageArgument.untrusted("value", dimension.getLoadKey())));
|
||||
}
|
||||
|
||||
@Director(name = "capture", description = "Capture live registered structures into editable Iris objects by generating each one in a throwaway scratch world and reading back its blocks. This standalone command overwrites its owned outputs; structures wider/taller than the capture cap and anything that will not generate in a flat overworld are skipped. Each captured structure becomes a single-piece Iris structure you can place from a biome/region/dimension 'structures' list. /iris structure import runs a restricted final capture pass only for non-jigsaw structures that have no loadable NBT template.", descriptionKey = "iris.director.commandstructure.director.capture_code_generated_structures_that_have_no_nbt_template_swamp_huts_igloos", aliases = {"cap"}, origin = DirectorOrigin.BOTH)
|
||||
public void capture(
|
||||
@Param(description = "The dimension whose pack to capture into", descriptionKey = "iris.director.commandstructure.param.dimension_whose_pack_capture_into", aliases = "dim")
|
||||
IrisDimension dimension
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_3, MessageArgument.untrusted("value", dimension.getLoadKey())));
|
||||
return;
|
||||
}
|
||||
StructureCaptureImporter.Report report = StructureCaptureImporter.importAllStructures(data, StructureImporter.Mode.OVERWRITE, sender());
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_CAPTURED_STRUCTURES_PLACE_THEM_FROM_STRUCTURES_LIST_REGENERATE_CHUNKS_DELETE, MessageArgument.untrusted("value", report.imported())));
|
||||
}
|
||||
|
||||
@Director(description = "Verify native structure eligibility and locate Iris-placed structures without running blocking native searches.", descriptionKey = "iris.director.commandstructure.director.verify_native_structure_eligibility_locate_iris_placed_structures_without_running_blocking_native", aliases = {"locateall"}, origin = DirectorOrigin.BOTH, sync = true)
|
||||
public void verify(
|
||||
@Param(description = "The dimension to verify", descriptionKey = "iris.director.commandstructure.param.dimension_verify", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "Search radius in chunks around the origin (larger is much slower)", descriptionKey = "iris.director.commandstructure.param.search_radius_chunks_around_origin_larger_is_much_slower", defaultValue = "48")
|
||||
int radius
|
||||
) {
|
||||
World world = resolveIrisWorld(dimension);
|
||||
if (world == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_NO_LOADED_IRIS_WORLD_FOUND_JOIN_CREATE_ONE_FIRST_SEARCH, MessageArgument.untrusted("value", dimension.getLoadKey())));
|
||||
return;
|
||||
}
|
||||
boolean senderIsPlayer = sender() != null && sender().isPlayer();
|
||||
Location center = senderIsPlayer && player().getWorld() == world
|
||||
? player().getLocation()
|
||||
: world.getSpawnLocation();
|
||||
int searchRadius = Math.max(1, Math.min(radius, 1000));
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
Engine engine = access == null ? null : access.getEngine();
|
||||
if (engine == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_SELECTED_IRIS_WORLD_HAS_NO_ACTIVE_GENERATOR_ENGINE));
|
||||
return;
|
||||
}
|
||||
KList<String> structureKeys = new KList<>();
|
||||
Registry<Structure> structureRegistry = Bukkit.getRegistry(Structure.class);
|
||||
if (structureRegistry != null) {
|
||||
for (Structure structure : structureRegistry) {
|
||||
NamespacedKey key = structureRegistry.getKey(structure);
|
||||
if (key != null) {
|
||||
structureKeys.add(key.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (String placedKey : IrisStructureLocator.placedKeys(engine)) {
|
||||
if (!structureKeys.contains(placedKey)) {
|
||||
structureKeys.add(placedKey);
|
||||
}
|
||||
}
|
||||
VolmitSender commandSender = sender();
|
||||
Player target = senderIsPlayer ? player() : null;
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STRUCTURE_VERIFYING_STRUCTURES_FROM_WITHIN_CHUNKS, MessageArgument.untrusted("value", world.getName()), MessageArgument.untrusted("value2", center.getBlockX()), MessageArgument.untrusted("value3", center.getBlockZ()), MessageArgument.untrusted("searchRadius", searchRadius)));
|
||||
int centerX = center.getBlockX();
|
||||
int centerZ = center.getBlockZ();
|
||||
J.a(() -> runVerification(engine, structureKeys, centerX, centerZ, searchRadius, commandSender, target));
|
||||
}
|
||||
|
||||
private void runVerification(Engine engine, KList<String> structureKeys, int centerX, int centerZ,
|
||||
int searchRadius, VolmitSender commandSender, Player target) {
|
||||
KList<String> messages = new KList<>();
|
||||
Map<String, IrisNativeStructureDecision> decisions = new HashMap<>(structureKeys.size());
|
||||
boolean requiresNativeReachability = false;
|
||||
for (String keyName : structureKeys) {
|
||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, keyName, false);
|
||||
decisions.put(keyName, decision);
|
||||
requiresNativeReachability |= !IrisStructureLocator.isPlaced(engine, keyName)
|
||||
&& decision.generate();
|
||||
}
|
||||
Set<String> reachable = Set.of();
|
||||
if (requiresNativeReachability) {
|
||||
try {
|
||||
reachable = StructureReachability.reachableKeys(engine);
|
||||
} catch (Throwable error) {
|
||||
messages.add(C.RED + "Structure verification could not resolve native biome reachability.");
|
||||
sendVerificationMessages(commandSender, target, messages);
|
||||
Iris.reportError("Could not resolve native structure biome reachability for verification.", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
int located = 0;
|
||||
int nativeEligible = 0;
|
||||
int disabled = 0;
|
||||
int unreachable = 0;
|
||||
int searchLimited = 0;
|
||||
int errors = 0;
|
||||
for (String keyName : structureKeys) {
|
||||
IrisNativeStructureDecision decision = decisions.get(keyName);
|
||||
if (IrisStructureLocator.isPlaced(engine, keyName)) {
|
||||
try {
|
||||
IrisStructureLocator.LocateResult result =
|
||||
IrisStructureLocator.locate(engine, keyName, centerX, centerZ, searchRadius);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
searchLimited++;
|
||||
messages.add(C.YELLOW + "[iris-search-limit] " + C.WHITE + keyName + C.YELLOW
|
||||
+ ": unable to complete the density search before its safety limit was reached");
|
||||
continue;
|
||||
}
|
||||
if (!result.found()) {
|
||||
messages.add(C.YELLOW + "[iris-not-found] " + C.WHITE + keyName);
|
||||
continue;
|
||||
}
|
||||
located++;
|
||||
messages.add(C.AQUA + "[iris-planned] " + C.WHITE + keyName + C.GREEN + " @ "
|
||||
+ result.originX() + "," + result.baseY() + "," + result.originZ());
|
||||
} catch (Throwable error) {
|
||||
errors++;
|
||||
messages.add(C.RED + "[error] " + C.WHITE + keyName + C.RED + ": "
|
||||
+ error.getClass().getSimpleName());
|
||||
Iris.reportError("Could not verify Iris-placed structure '" + keyName + "'.", error);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!decision.generate()) {
|
||||
disabled++;
|
||||
messages.add(C.GRAY + "[disabled] " + C.WHITE + keyName);
|
||||
continue;
|
||||
}
|
||||
if (!reachable.contains(keyName.toLowerCase(Locale.ROOT))) {
|
||||
unreachable++;
|
||||
KList<String> missing = StructureReachability.missingBiomeKeys(engine, keyName);
|
||||
messages.add(C.YELLOW + "[unreachable] " + C.WHITE + keyName
|
||||
+ (missing.isEmpty() ? "" : C.YELLOW + " needs " + String.join("/", missing)));
|
||||
continue;
|
||||
}
|
||||
nativeEligible++;
|
||||
messages.add(C.GREEN + "[native-eligible] " + C.WHITE + keyName);
|
||||
}
|
||||
messages.add(C.GREEN + "Structure verify: " + C.WHITE + located + C.GREEN + " Iris placement plans found, "
|
||||
+ C.WHITE + nativeEligible + C.GREEN + " native structures eligible, "
|
||||
+ C.WHITE + disabled + C.GREEN + " disabled by policy, "
|
||||
+ C.WHITE + unreachable + C.GREEN + " biome-unreachable, "
|
||||
+ C.WHITE + searchLimited + C.GREEN + " density searches safety-limited, "
|
||||
+ C.WHITE + errors + C.GREEN + " errors. Native eligibility is checked without running blocking live locates.");
|
||||
sendVerificationMessages(commandSender, target, messages);
|
||||
}
|
||||
|
||||
private void sendVerificationMessages(VolmitSender commandSender, Player target,
|
||||
KList<String> messages) {
|
||||
Runnable send = () -> {
|
||||
for (String message : messages) {
|
||||
commandSender.sendMessage(message);
|
||||
}
|
||||
};
|
||||
if (target != null) {
|
||||
J.runEntity(target, send);
|
||||
} else {
|
||||
J.s(send);
|
||||
}
|
||||
}
|
||||
|
||||
private World resolveIrisWorld(IrisDimension dimension) {
|
||||
if (sender() != null && sender().isPlayer() && IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
PlatformChunkGenerator playerGenerator = IrisToolbelt.access(player().getWorld());
|
||||
if (matchesDimension(playerGenerator, dimension.getLoadKey())) {
|
||||
return player().getWorld();
|
||||
}
|
||||
}
|
||||
for (World w : Bukkit.getWorlds()) {
|
||||
if (!IrisToolbelt.isIrisWorld(w)) {
|
||||
continue;
|
||||
}
|
||||
PlatformChunkGenerator gen = IrisToolbelt.access(w);
|
||||
if (matchesDimension(gen, dimension.getLoadKey())) {
|
||||
return w;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static boolean matchesDimension(PlatformChunkGenerator generator, String dimensionKey) {
|
||||
return dimensionKey != null
|
||||
&& generator != null
|
||||
&& generator.getEngine() != null
|
||||
&& generator.getEngine().getDimension() != null
|
||||
&& dimensionKey.equals(generator.getEngine().getDimension().getLoadKey());
|
||||
}
|
||||
|
||||
@Director(description = "Resolve an iris structure's jigsaw graph and report piece count & bounds", descriptionKey = "iris.director.commandstructure.director.resolve_iris_structure_s_jigsaw_graph_report_piece_count_bounds", origin = DirectorOrigin.BOTH)
|
||||
public void info(
|
||||
@Param(description = "The dimension whose pack holds the structure", descriptionKey = "iris.director.commandstructure.param.dimension_whose_pack_holds_structure", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "The iris structure key to inspect", descriptionKey = "iris.director.commandstructure.param.iris_structure_key_inspect")
|
||||
String structure
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_4, MessageArgument.untrusted("value", dimension.getLoadKey())));
|
||||
return;
|
||||
}
|
||||
IrisStructure s = data.load(IrisStructure.class, structure, false);
|
||||
if (s == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_NO_IRIS_STRUCTURE_THIS_PACK, MessageArgument.untrusted("structure", structure)));
|
||||
return;
|
||||
}
|
||||
StructureAssembler assembler = StructureAssembler.forData(
|
||||
data, s, new IrisPosition(0, 64, 0));
|
||||
StructureAssemblyResult assembly = assembler.assemble(new RNG(1234));
|
||||
List<PlacedStructurePiece> pieces = assembly.pieces();
|
||||
if (!assembly.hasOutput()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_STRUCTURE_ASSEMBLED_0_PIECES_CHECK_STARTPOOL, MessageArgument.untrusted("structure", structure), MessageArgument.untrusted("value", s.getStartPool())));
|
||||
return;
|
||||
}
|
||||
int minX = Integer.MAX_VALUE;
|
||||
int minZ = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE;
|
||||
int maxZ = Integer.MIN_VALUE;
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
minX = Math.min(minX, p.getMinX());
|
||||
minZ = Math.min(minZ, p.getMinZ());
|
||||
maxX = Math.max(maxX, p.getMaxX());
|
||||
maxZ = Math.max(maxZ, p.getMaxZ());
|
||||
}
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_STRUCTURE_PIECES_FOOTPRINT_X_BLOCKS_SAMPLE_SEED_1234, MessageArgument.untrusted("structure", structure), MessageArgument.untrusted("value", pieces.size()), MessageArgument.untrusted("value2", (maxX - minX + 1)), MessageArgument.untrusted("value3", (maxZ - minZ + 1))));
|
||||
}
|
||||
|
||||
@Director(description = "Assemble and place an iris structure at your location (studio testing)", descriptionKey = "iris.director.commandstructure.director.assemble_place_iris_structure_at_your_location_studio_testing", aliases = {"p"}, origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void place(
|
||||
@Param(description = "The dimension whose pack holds the structure", descriptionKey = "iris.director.commandstructure.param.dimension_whose_pack_holds_structure_2", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "The iris structure key to place", descriptionKey = "iris.director.commandstructure.param.iris_structure_key_place")
|
||||
String structure
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_COULD_NOT_RESOLVE_PACK_DIMENSION_5, MessageArgument.untrusted("value", dimension.getLoadKey())));
|
||||
return;
|
||||
}
|
||||
IrisStructure s = data.load(IrisStructure.class, structure, false);
|
||||
if (s == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_NO_IRIS_STRUCTURE_THIS_PACK_2, MessageArgument.untrusted("structure", structure)));
|
||||
return;
|
||||
}
|
||||
Location loc = player().getLocation();
|
||||
StructureAssembler assembler = StructureAssembler.forData(
|
||||
data, s, new IrisPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()));
|
||||
RNG rng = new RNG((long) loc.getBlockX() * 341873128712L + loc.getBlockZ());
|
||||
StructureAssemblyResult assembly = assembler.assemble(rng);
|
||||
List<PlacedStructurePiece> pieces = assembly.pieces();
|
||||
if (!assembly.hasOutput()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STRUCTURE_STRUCTURE_ASSEMBLED_0_PIECES, MessageArgument.untrusted("structure", structure)));
|
||||
return;
|
||||
}
|
||||
Map<Block, BlockData> future = new HashMap<>();
|
||||
World targetWorld = player().getWorld();
|
||||
PlatformChunkGenerator targetGenerator = IrisToolbelt.access(targetWorld);
|
||||
Engine targetEngine = targetGenerator == null ? null : targetGenerator.getEngine();
|
||||
IObjectPlacer placer = CommandObject.createPlacer(targetWorld, future, targetEngine);
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
IrisObjectPlacement config = new IrisObjectPlacement();
|
||||
config.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
|
||||
config.setRotation(p.getRotation());
|
||||
config.getPlace().add(p.getObject().getLoadKey());
|
||||
if (!s.getEdit().isEmpty()) {
|
||||
config.setEdit(s.getEdit());
|
||||
}
|
||||
p.getObject().place(p.getX(), p.getY(), p.getZ(), placer, config, rng, null, null, data);
|
||||
}
|
||||
int blockChanges = 0;
|
||||
for (Map.Entry<Block, BlockData> entry : future.entrySet()) {
|
||||
if (!entry.getKey().getBlockData().equals(entry.getValue())) {
|
||||
blockChanges++;
|
||||
}
|
||||
}
|
||||
if (blockChanges == 0) {
|
||||
sender().sendMessage(IrisLanguage.text(
|
||||
BukkitCommandMessagesExtended.COMMAND_STRUCTURE_PLACEMENT_CHANGED_NO_BLOCKS,
|
||||
MessageArgument.untrusted("structure", structure),
|
||||
MessageArgument.untrusted("value", pieces.size())));
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(IrisLanguage.text(
|
||||
BukkitCommandMessagesExtended.COMMAND_STRUCTURE_PLACED_PIECES_AT_YOUR_LOCATION,
|
||||
MessageArgument.untrusted("structure", structure),
|
||||
MessageArgument.untrusted("value", pieces.size()),
|
||||
MessageArgument.untrusted("value2", blockChanges)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,952 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.studio.view.GuiHost;
|
||||
import art.arcane.iris.studio.view.ImageMapStudioGUI;
|
||||
import art.arcane.iris.studio.view.NoiseExplorerGUI;
|
||||
import art.arcane.iris.studio.view.VisionGUI;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.studio.workspace.IrisProject;
|
||||
import art.arcane.iris.studio.workspace.IrisCodeWorkspace;
|
||||
import art.arcane.iris.localization.RuntimeUiMessages;
|
||||
import art.arcane.iris.platform.bukkit.BoardSVC;
|
||||
import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.structure.BulkStructureImporter;
|
||||
import art.arcane.iris.structure.FeatureImporter;
|
||||
import art.arcane.iris.structure.StructureImporter;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.world.loot.InventorySlotType;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.biome.IrisBiomePaletteLayer;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.world.entity.IrisEntity;
|
||||
import art.arcane.iris.generation.noise.IrisGenerator;
|
||||
import art.arcane.iris.generation.noise.IrisInterpolator;
|
||||
import art.arcane.iris.world.loot.IrisLootTable;
|
||||
import art.arcane.iris.generation.noise.IrisNoiseGenerator;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.platform.generation.EngineBukkitOps;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.iris.command.handlers.DimensionHandler;
|
||||
import art.arcane.iris.command.specialhandlers.NullableDimensionHandler;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.function.NoiseProvider;
|
||||
import art.arcane.volmlib.util.interpolation.InterpolationMethod;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.math.Spiraler;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.scheduling.O;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.FluidCollisionMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", descriptionKey = "iris.director.commandstudio.director.studio_commands")
|
||||
public class CommandStudio implements DirectorExecutor {
|
||||
private static final long CHUNK_SCAN_TIMEOUT_MS = 3_000;
|
||||
private CommandEdit edit;
|
||||
//private CommandDeepSearch deepSearch;
|
||||
|
||||
public static String hrf(Duration duration) {
|
||||
return duration.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase();
|
||||
}
|
||||
|
||||
@Director(description = "Open a new studio world", descriptionKey = "iris.director.commandstudio.director.open_new_studio_world", aliases = "o", sync = true)
|
||||
public void open(
|
||||
@Param(description = "The dimension pack to open a studio for", descriptionKey = "iris.director.commandstudio.param.dimension_pack_open_studio", aliases = "dim", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(defaultValue = "1337", description = "The seed to generate the studio with", descriptionKey = "iris.director.commandstudio.param.seed_generate_studio_with", aliases = "s")
|
||||
long seed,
|
||||
@Param(defaultValue = "false", description = "Attempt to open Studio before a required registry restart", aliases = "f")
|
||||
boolean force) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_STUDIO_PACK_SEED, MessageArgument.untrusted("value", dimension.getName()), MessageArgument.untrusted("seed", seed)));
|
||||
Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey(), force);
|
||||
}
|
||||
|
||||
@Director(description = "Import vanilla trees, mushrooms & objects (and structures/jigsaw) from the server into a pack's objects/vanilla folder", descriptionKey = "iris.director.commandstudio.director.import_vanilla_trees_mushrooms_objects_structures_jigsaw_from_server_into_pack_s", aliases = {"importv", "iv"}, origin = DirectorOrigin.BOTH)
|
||||
public void importvanilla(
|
||||
@Param(description = "The dimension pack to import vanilla content into", descriptionKey = "iris.director.commandstudio.param.dimension_pack_import_vanilla_content_into", aliases = {"pack", "dim"}, customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@Param(description = "How many variants to capture per tree/object feature", descriptionKey = "iris.director.commandstudio.param.how_many_variants_capture_per_tree_object_feature", defaultValue = "3")
|
||||
int variants,
|
||||
@Param(description = "Also import vanilla & datapack structures/jigsaw into the pack", descriptionKey = "iris.director.commandstudio.param.also_import_vanilla_datapack_structures_jigsaw_into_pack", defaultValue = "true")
|
||||
boolean structures
|
||||
) {
|
||||
if (dimension == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_PROVIDE_DIMENSION_PACK_IRIS_STD_IMPORTVANILLA_PACK_DIMENSION));
|
||||
return;
|
||||
}
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_COULD_NOT_RESOLVE_PACK_DIMENSION, MessageArgument.untrusted("value", dimension.getLoadKey())));
|
||||
return;
|
||||
}
|
||||
|
||||
VolmitSender sender = sender();
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_IMPORTING_VANILLA_CONTENT_INTO, MessageArgument.untrusted("value", dimension.getLoadKey())));
|
||||
|
||||
FeatureImporter.Report features = FeatureImporter.importAllObjectFeatures(data, variants, sender);
|
||||
int imported = features.imported();
|
||||
int failed = features.failed();
|
||||
|
||||
if (structures) {
|
||||
BulkStructureImporter.Report jigsaws = BulkStructureImporter.importAllVanilla(data, StructureImporter.Mode.OVERWRITE, true, sender);
|
||||
BulkStructureImporter.Report templates = BulkStructureImporter.importAllTemplates(data, StructureImporter.Mode.OVERWRITE, sender);
|
||||
BulkStructureImporter.Report groups = BulkStructureImporter.importTemplateGroups(data, StructureImporter.Mode.OVERWRITE, sender);
|
||||
imported += jigsaws.imported() + templates.imported() + groups.imported();
|
||||
failed += jigsaws.failed() + templates.failed() + groups.failed();
|
||||
}
|
||||
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_IMPORTVANILLA_COMPLETE_OBJECTS_STRUCTURES_WRITTEN_FAILED, MessageArgument.untrusted("imported", imported), MessageArgument.untrusted("failed", failed)));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_TREES_OBJECTS_ARE_UNDER_OBJECTS_VANILLA_REFERENCE_THEM_FROM_BIOME));
|
||||
}
|
||||
|
||||
@Director(description = "Open VSCode for a dimension", descriptionKey = "iris.director.commandstudio.director.open_vscode_dimension", aliases = {"vsc"})
|
||||
public void vscode(
|
||||
@Param(defaultValue = "default", description = "The dimension to open VSCode for", descriptionKey = "iris.director.commandstudio.param.dimension_open_vscode", aliases = "dim", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_VSCODE_PACK, MessageArgument.untrusted("value", dimension.getName())));
|
||||
Iris.service(StudioSVC.class).openVSCode(sender(), dimension.getLoadKey());
|
||||
}
|
||||
|
||||
@Director(description = "Close an open studio project", descriptionKey = "iris.director.commandstudio.director.close_open_studio_project", aliases = {"x"}, sync = true)
|
||||
public void close() {
|
||||
VolmitSender commandSender = sender();
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_NO_OPEN_STUDIO_PROJECTS));
|
||||
return;
|
||||
}
|
||||
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_CLOSING_STUDIO));
|
||||
Iris.service(StudioSVC.class).close().whenComplete((result, throwable) -> J.s(() -> {
|
||||
if (throwable != null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_STUDIO_CLOSE_FAILED, MessageArgument.untrusted("value", String.valueOf(throwable.getMessage()))));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result != null && result.failureCause() != null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_STUDIO_CLOSE_FAILED_2, MessageArgument.untrusted("value", String.valueOf(result.failureCause().getMessage()))));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result != null && result.startupCleanupQueued()) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_STUDIO_CLOSED_REMAINING_WORLD_FAMILY_CLEANUP_WAS_QUEUED_STARTUP_FALLBACK));
|
||||
return;
|
||||
}
|
||||
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_STUDIO_CLOSED));
|
||||
}));
|
||||
}
|
||||
|
||||
@Director(description = "Toggle your Studio debug scoreboard", descriptionKey = "iris.director.commandstudio.director.toggle_your_studio_debug_scoreboard", aliases = {"board", "sidebar", "sb"}, origin = DirectorOrigin.PLAYER)
|
||||
public void scoreboard() {
|
||||
Player target = player();
|
||||
VolmitSender commandSender = sender();
|
||||
if (!J.runEntity(target, () -> {
|
||||
PlatformChunkGenerator generator = IrisToolbelt.access(target.getWorld());
|
||||
if (generator == null || !generator.isStudio()) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_YOU_MUST_BE_STUDIO_WORLD_TOGGLE_DEBUG_SCOREBOARD));
|
||||
return;
|
||||
}
|
||||
|
||||
boolean visible = Iris.service(BoardSVC.class).toggle(target);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_STUDIO_DEBUG_SCOREBOARD, MessageArgument.trusted("value", visible ? C.GREEN : C.YELLOW), MessageArgument.trusted("value2", IrisLanguage.text(visible ? RuntimeUiMessages.STATUS_ENABLED : RuntimeUiMessages.STATUS_DISABLED))));
|
||||
})) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_COULD_NOT_UPDATE_STUDIO_DEBUG_SCOREBOARD_RIGHT_NOW));
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Create a new studio project", descriptionKey = "iris.director.commandstudio.director.create_new_studio_project", aliases = "+")
|
||||
public void create(
|
||||
@Param(description = "The name of this new Iris Project.", descriptionKey = "iris.director.commandstudio.param.name_this_new_iris_project", defaultValue = "studio")
|
||||
String name,
|
||||
@Param(
|
||||
description = "Copy the contents of an existing project in your packs folder and use it as a template in this new project.", descriptionKey = "iris.director.commandstudio.param.copy_contents_existing_project_your_packs_folder_use_it_as_template_this",
|
||||
defaultValue = "null",
|
||||
contextual = true,
|
||||
contextualOverride = true,
|
||||
customHandler = NullableDimensionHandler.class
|
||||
)
|
||||
IrisDimension template) {
|
||||
if (template != null) {
|
||||
Iris.service(StudioSVC.class).create(sender(), name, template);
|
||||
} else {
|
||||
Iris.service(StudioSVC.class).create(sender(), name);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Get the version of a pack", descriptionKey = "iris.director.commandstudio.director.get_version_pack")
|
||||
public void version(
|
||||
@Param(defaultValue = "default", description = "The dimension get the version of", descriptionKey = "iris.director.commandstudio.param.dimension_get_version", aliases = "dim", contextual = true, contextualOverride = true, customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_PACK_HAS_VERSION, MessageArgument.untrusted("value", dimension.getName()), MessageArgument.untrusted("value2", dimension.getVersion())));
|
||||
}
|
||||
|
||||
@Director(description = "Open the noise explorer (External GUI)", descriptionKey = "iris.director.commandstudio.director.open_noise_explorer_external_gui", aliases = {"nmap"})
|
||||
public void noise(
|
||||
@Param(description = "Optional pack generator to preview", descriptionKey = "iris.director.commandstudio.param.optional_pack_generator_preview", defaultValue = "null", contextual = true, contextualOverride = true)
|
||||
IrisGenerator generator,
|
||||
@Param(description = "The seed to preview the generator with", descriptionKey = "iris.director.commandstudio.param.seed_preview_generator_with", defaultValue = "12345")
|
||||
long seed
|
||||
) {
|
||||
if (noGUI()) return;
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_NOISE_EXPLORER));
|
||||
|
||||
if (generator == null) {
|
||||
NoiseExplorerGUI.launch();
|
||||
return;
|
||||
}
|
||||
|
||||
String generatorKey = generator.getLoadKey();
|
||||
NoiseExplorerGUI.launchGeneratorKey(generatorKey, generator, seed);
|
||||
}
|
||||
|
||||
@Director(description = "Open the image-map studio (External GUI)", descriptionKey = "iris.director.commandstudio.director.open_image_map_studio_external_gui", aliases = {"imap"})
|
||||
public void imagemap(
|
||||
@Param(name = "world", description = "The Iris world whose active pack receives image-map exports", descriptionKey = "iris.director.commandstudio.param.world_open_image_map_studio", contextual = true, contextualOverride = true)
|
||||
World world
|
||||
) {
|
||||
if (noGUI()) {
|
||||
return;
|
||||
}
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_NEED_BE_SPECIFY_IRIS_GENERATED_WORLD));
|
||||
return;
|
||||
}
|
||||
|
||||
Engine activeEngine = IrisToolbelt.access(world).getEngine();
|
||||
ImageMapStudioGUI.launch(activeEngine);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_IMAGE_MAP_STUDIO));
|
||||
}
|
||||
|
||||
@Director(description = "Show loot if a chest were right here", descriptionKey = "iris.director.commandstudio.director.show_loot_if_chest_were_right_here", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void loot(
|
||||
@Param(description = "Fast insertion of items in virtual inventory (may cause performance drop)", descriptionKey = "iris.director.commandstudio.param.fast_insertion_items_virtual_inventory_may_cause_performance_drop", defaultValue = "false")
|
||||
boolean fast,
|
||||
@Param(description = "Whether or not to append to the inventory currently open (if false, clears opened inventory)", descriptionKey = "iris.director.commandstudio.param.whether_not_append_inventory_currently_open_if_false_clears_opened_inventory", defaultValue = "true")
|
||||
boolean add
|
||||
) {
|
||||
if (noStudio()) return;
|
||||
|
||||
KList<IrisLootTable> tables = EngineBukkitOps.getLootTables(engine(), RNG.r, player().getLocation().getBlock());
|
||||
Inventory inv = Bukkit.createInventory(null, 27 * 2);
|
||||
|
||||
try {
|
||||
EngineBukkitOps.addItems(engine(), true, inv, tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ());
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CANNOT_ADD_ITEMS_VIRTUAL_INVENTORY_BECAUSE, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
O<Integer> ta = new O<>();
|
||||
ta.set(-1);
|
||||
|
||||
var sender = sender();
|
||||
var player = player();
|
||||
var engine = engine();
|
||||
|
||||
ta.set(J.sr(() ->
|
||||
{
|
||||
if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) {
|
||||
J.csr(ta.get());
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_OPENED_INVENTORY));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!add) {
|
||||
inv.clear();
|
||||
}
|
||||
|
||||
EngineBukkitOps.addItems(engine, true, inv, tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ());
|
||||
}, fast ? 5 : 35));
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_INVENTORY_NOW));
|
||||
player().openInventory(inv);
|
||||
}
|
||||
|
||||
@Director(description = "Calculate the chance for each region to generate", descriptionKey = "iris.director.commandstudio.director.calculate_chance_each_region_generate", origin = DirectorOrigin.PLAYER)
|
||||
public void regions(@Param(description = "The radius in chunks", descriptionKey = "iris.director.commandstudio.param.radius_chunks", defaultValue = "500") int radius) {
|
||||
var engine = engine();
|
||||
if (engine == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_ONLY_WORKS_IRIS_WORLD));
|
||||
return;
|
||||
}
|
||||
if (radius <= 0 || radius > 2048) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_REGIONS_RADIUS_OUT_OF_RANGE));
|
||||
return;
|
||||
}
|
||||
var sender = sender();
|
||||
var player = player();
|
||||
Thread.ofVirtual()
|
||||
.start(() -> {
|
||||
int d = radius * 2;
|
||||
KMap<String, AtomicInteger> data = new KMap<>();
|
||||
// Everything acquired below is released in the finally: a throw mid-scan
|
||||
// previously leaked the whole sampler ForkJoinPool, the self-rescheduling
|
||||
// progress task and both HUD claims for the rest of the server's uptime.
|
||||
MultiBurst multiBurst = null;
|
||||
boolean progressShown = false;
|
||||
int c = -1;
|
||||
try {
|
||||
engine.getDimension().getRegions().forEach(key -> data.put(key, new AtomicInteger(0)));
|
||||
multiBurst = new MultiBurst("Region Sampler");
|
||||
var executor = multiBurst.burst(Math.min(radius * radius, 1 << 20));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_GENERATING_DATA));
|
||||
var loc = player.getLocation();
|
||||
int totalTasks = d * d;
|
||||
AtomicInteger completedTasks = new AtomicInteger(0);
|
||||
progressShown = true;
|
||||
c = J.ar(() -> {
|
||||
double jobProgress = (double) completedTasks.get() / totalTasks;
|
||||
sender.sendProgress(jobProgress, IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS));
|
||||
BukkitPlatform.showProgressLane(player, "iris:job", IrisLanguage.text(RuntimeUiMessages.FINDING_REGIONS) + " " + Form.pc(jobProgress, 0), jobProgress, 4000L);
|
||||
}, 0);
|
||||
new Spiraler(d, d, (x, z) -> executor.queue(() -> {
|
||||
var region = engine.getRegion((x << 4) + 8, (z << 4) + 8);
|
||||
data.computeIfAbsent(region.getLoadKey(), (k) -> new AtomicInteger(0))
|
||||
.incrementAndGet();
|
||||
completedTasks.incrementAndGet();
|
||||
})).setOffset(loc.getBlockX(), loc.getBlockZ()).drain();
|
||||
executor.complete();
|
||||
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_DONE));
|
||||
var loader = engine.getData().getRegionLoader();
|
||||
data.forEach((k, v) -> sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_MESSAGE, MessageArgument.untrusted("k", k), MessageArgument.untrusted("value", loader.load(k).getRarity()), MessageArgument.untrusted("value2", Form.f((double) v.get() / totalTasks * 100, 2)))));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_REGIONS_SCAN_FAILED,
|
||||
MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
||||
} finally {
|
||||
if (c != -1) {
|
||||
J.car(c);
|
||||
}
|
||||
if (progressShown) {
|
||||
sender.sendAction(" ");
|
||||
BukkitPlatform.hudLanes().hide(player, "iris:job");
|
||||
}
|
||||
if (multiBurst != null) {
|
||||
multiBurst.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Render a world map (External GUI)", descriptionKey = "iris.director.commandstudio.director.render_world_map_external_gui", aliases = "render")
|
||||
public void map(
|
||||
@Param(name = "world", description = "The world to open the generator for", descriptionKey = "iris.director.commandstudio.param.world_open_generator", contextual = true, contextualOverride = true)
|
||||
World world
|
||||
) {
|
||||
if (noGUI()) return;
|
||||
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_NEED_BE_SPECIFY_IRIS_GENERATED_WORLD));
|
||||
return;
|
||||
}
|
||||
|
||||
UUID openerId = sender().isPlayer() ? player().getUniqueId() : null;
|
||||
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), openerId);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP));
|
||||
}
|
||||
|
||||
@Director(description = "Profiles the performance of a dimension", descriptionKey = "iris.director.commandstudio.director.profiles_performance_dimension", origin = DirectorOrigin.PLAYER)
|
||||
public void profile(
|
||||
@Param(description = "The dimension to profile", descriptionKey = "iris.director.commandstudio.param.dimension_profile", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
// Todo: Make this more accurate
|
||||
File pack = dimension.getLoadFile().getParentFile().getParentFile();
|
||||
File report = Iris.instance.getDataFile("profile.txt");
|
||||
IrisProject project = new IrisProject(pack);
|
||||
IrisData data = IrisData.get(pack);
|
||||
PlatformChunkGenerator activeGenerator = resolveProfileGenerator(dimension);
|
||||
Engine activeEngine = activeGenerator == null ? null : activeGenerator.getEngine();
|
||||
|
||||
if (activeEngine != null) {
|
||||
IrisToolbelt.applyPregenPerformanceProfile(activeEngine);
|
||||
} else {
|
||||
IrisToolbelt.applyPregenPerformanceProfile();
|
||||
}
|
||||
|
||||
KList<String> fileText = new KList<>();
|
||||
|
||||
KMap<NoiseStyle, Double> styleTimings = new KMap<>();
|
||||
KMap<InterpolationMethod, Double> interpolatorTimings = new KMap<>();
|
||||
KMap<String, Double> generatorTimings = new KMap<>();
|
||||
KMap<String, Double> biomeTimings = new KMap<>();
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CALCULATING_PERFORMANCE_METRICS_NOISE_GENERATORS));
|
||||
|
||||
for (NoiseStyle i : NoiseStyle.values()) {
|
||||
CNG c = i.create(new RNG(i.hashCode()));
|
||||
|
||||
for (int j = 0; j < 3000; j++) {
|
||||
c.noise(j, j + 1000, j * j);
|
||||
c.noise(j, -j);
|
||||
}
|
||||
|
||||
PrecisionStopwatch px = PrecisionStopwatch.start();
|
||||
|
||||
for (int j = 0; j < 100000; j++) {
|
||||
c.noise(j, j + 1000, j * j);
|
||||
c.noise(j, -j);
|
||||
}
|
||||
|
||||
styleTimings.put(i, px.getMilliseconds());
|
||||
}
|
||||
|
||||
fileText.add("Noise Style Performance Impacts: ");
|
||||
|
||||
for (NoiseStyle i : styleTimings.sortKNumber()) {
|
||||
fileText.add(i.name() + ": " + styleTimings.get(i));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CALCULATING_INTERPOLATOR_TIMINGS));
|
||||
|
||||
for (InterpolationMethod i : InterpolationMethod.values()) {
|
||||
IrisInterpolator in = new IrisInterpolator();
|
||||
in.setFunction(i);
|
||||
in.setHorizontalScale(8);
|
||||
|
||||
NoiseProvider np = (x, z) -> Math.random();
|
||||
|
||||
for (int j = 0; j < 3000; j++) {
|
||||
in.interpolate(j, -j, np);
|
||||
}
|
||||
|
||||
PrecisionStopwatch px = PrecisionStopwatch.start();
|
||||
|
||||
for (int j = 0; j < 100000; j++) {
|
||||
in.interpolate(j + 10000, -j - 100000, np);
|
||||
}
|
||||
|
||||
interpolatorTimings.put(i, px.getMilliseconds());
|
||||
}
|
||||
|
||||
fileText.add("Noise Interpolator Performance Impacts: ");
|
||||
|
||||
for (InterpolationMethod i : interpolatorTimings.sortKNumber()) {
|
||||
fileText.add(i.name() + ": " + interpolatorTimings.get(i));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_PROCESSING_GENERATOR_SCORES));
|
||||
|
||||
KMap<String, KList<String>> btx = new KMap<>();
|
||||
|
||||
for (String i : data.getGeneratorLoader().getPossibleKeys()) {
|
||||
KList<String> vv = new KList<>();
|
||||
IrisGenerator g = data.getGeneratorLoader().load(i);
|
||||
KList<IrisNoiseGenerator> composites = g.getAllComposites();
|
||||
double score = 0;
|
||||
int m = 0;
|
||||
for (IrisNoiseGenerator j : composites) {
|
||||
m++;
|
||||
score += styleTimings.get(j.getStyle().getStyle());
|
||||
vv.add("Composite Noise Style " + m + " " + j.getStyle().getStyle().name() + ": " + styleTimings.get(j.getStyle().getStyle()));
|
||||
}
|
||||
|
||||
score += interpolatorTimings.get(g.getInterpolator().getFunction());
|
||||
vv.add("Interpolator " + g.getInterpolator().getFunction().name() + ": " + interpolatorTimings.get(g.getInterpolator().getFunction()));
|
||||
generatorTimings.put(i, score);
|
||||
btx.put(i, vv);
|
||||
}
|
||||
|
||||
fileText.add("Project Generator Performance Impacts: ");
|
||||
|
||||
for (String i : generatorTimings.sortKNumber()) {
|
||||
fileText.add(i + ": " + generatorTimings.get(i));
|
||||
|
||||
btx.get(i).forEach((ii) -> fileText.add(" " + ii));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
KMap<String, KList<String>> bt = new KMap<>();
|
||||
|
||||
for (String i : data.getBiomeLoader().getPossibleKeys()) {
|
||||
KList<String> vv = new KList<>();
|
||||
IrisBiome b = data.getBiomeLoader().load(i);
|
||||
double score = 0;
|
||||
|
||||
int m = 0;
|
||||
for (IrisBiomePaletteLayer j : b.getLayers()) {
|
||||
m++;
|
||||
score += styleTimings.get(j.getStyle().getStyle());
|
||||
vv.add("Palette Layer " + m + ": " + styleTimings.get(j.getStyle().getStyle()));
|
||||
}
|
||||
|
||||
score += styleTimings.get(b.getBiomeStyle().getStyle());
|
||||
vv.add("Biome Style: " + styleTimings.get(b.getBiomeStyle().getStyle()));
|
||||
score += styleTimings.get(b.getChildStyle().getStyle());
|
||||
vv.add("Child Style: " + styleTimings.get(b.getChildStyle().getStyle()));
|
||||
biomeTimings.put(i, score);
|
||||
bt.put(i, vv);
|
||||
}
|
||||
|
||||
fileText.add("Project Biome Performance Impacts: ");
|
||||
|
||||
for (String i : biomeTimings.sortKNumber()) {
|
||||
fileText.add(i + ": " + biomeTimings.get(i));
|
||||
|
||||
bt.get(i).forEach((ff) -> fileText.add(" " + ff));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
double m = 0;
|
||||
for (double i : biomeTimings.v()) {
|
||||
m += i;
|
||||
}
|
||||
m /= biomeTimings.size();
|
||||
double mm = 0;
|
||||
for (double i : generatorTimings.v()) {
|
||||
mm += i;
|
||||
}
|
||||
mm /= generatorTimings.size();
|
||||
m += mm;
|
||||
|
||||
fileText.add("Average Score: " + m);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_SCORE, MessageArgument.untrusted("value", Form.duration(m, 0))));
|
||||
|
||||
try {
|
||||
IO.writeAll(report, fileText.toString("\n"));
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_DONE, MessageArgument.untrusted("value", report.getPath())));
|
||||
}
|
||||
|
||||
private PlatformChunkGenerator resolveProfileGenerator(IrisDimension dimension) {
|
||||
StudioSVC studioService = Iris.service(StudioSVC.class);
|
||||
if (studioService != null && studioService.isProjectOpen()) {
|
||||
IrisProject activeProject = studioService.getActiveProject();
|
||||
if (activeProject != null) {
|
||||
PlatformChunkGenerator activeProvider = activeProject.getActiveProvider();
|
||||
if (isGeneratorDimension(activeProvider, dimension)) {
|
||||
return activeProvider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sender().isPlayer()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Player player = sender().player();
|
||||
if (player == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PlatformChunkGenerator worldAccess = IrisToolbelt.access(player.getWorld());
|
||||
if (isGeneratorDimension(worldAccess, dimension)) {
|
||||
return worldAccess;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isGeneratorDimension(PlatformChunkGenerator generator, IrisDimension dimension) {
|
||||
if (generator == null || generator.getEngine() == null || dimension == null || dimension.getLoadKey() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
IrisDimension engineDimension = generator.getEngine().getDimension();
|
||||
if (engineDimension == null || engineDimension.getLoadKey() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return engineDimension.getLoadKey().equalsIgnoreCase(dimension.getLoadKey());
|
||||
}
|
||||
|
||||
@Director(description = "Spawn an Iris entity", descriptionKey = "iris.director.commandstudio.director.spawn_iris_entity", aliases = "summon", origin = DirectorOrigin.PLAYER)
|
||||
public void spawn(
|
||||
@Param(description = "The entity to spawn", descriptionKey = "iris.director.commandstudio.param.entity_spawn")
|
||||
IrisEntity entity,
|
||||
@Param(description = "The location to spawn the entity at", descriptionKey = "iris.director.commandstudio.param.location_spawn_entity_at", contextual = true, contextualOverride = true)
|
||||
Vector location
|
||||
) {
|
||||
VolmitSender commandSender = sender();
|
||||
Engine spawnEngine = engine();
|
||||
|
||||
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_HAVE_BE_IRIS_WORLD_SPAWN_ENTITIES_PROPERLY_TRYING_SPAWN));
|
||||
}
|
||||
|
||||
// Entity creation must run on the thread owning the destination chunk.
|
||||
Location at = new Location(world(), location.getX(), location.getY(), location.getZ());
|
||||
if (!J.runAt(at, () -> entity.spawn(spawnEngine, at))) {
|
||||
Iris.warn("Could not schedule the entity spawn at " + at.getBlockX() + ", " + at.getBlockY() + ", " + at.getBlockZ() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Teleport to the active studio world", descriptionKey = "iris.director.commandstudio.director.teleport_active_studio_world", aliases = "stp", origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void tpstudio() {
|
||||
StudioSVC studioService = Iris.service(StudioSVC.class);
|
||||
if (!studioService.isProjectOpen()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_NO_STUDIO_WORLD_IS_OPEN));
|
||||
return;
|
||||
}
|
||||
|
||||
if (IrisToolbelt.isIrisWorld(world()) && engine().isStudio()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_ARE_ALREADY_STUDIO_WORLD));
|
||||
return;
|
||||
}
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_SENDING_YOU_STUDIO_WORLD));
|
||||
Player player = player();
|
||||
studioService.teleportToActiveProject(player)
|
||||
.whenComplete((teleported, failure) -> {
|
||||
if (failure != null) {
|
||||
Iris.reportError("Studio teleport failed for player \"" + player.getName() + "\".", failure);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Update your dimension projects VSCode workspace", descriptionKey = "iris.director.commandstudio.director.update_your_dimension_projects_vscode_workspace")
|
||||
public void update(
|
||||
@Param(description = "The dimension to update the workspace of", descriptionKey = "iris.director.commandstudio.param.dimension_update_workspace", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_UPDATING_CODE_WORKSPACE, MessageArgument.untrusted("value", dimension.getName())));
|
||||
if (new IrisCodeWorkspace(new IrisProject(dimension.getLoader().getDataFolder())).updateWorkspace()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_UPDATED_CODE_WORKSPACE, MessageArgument.untrusted("value", dimension.getName())));
|
||||
} else {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_INVALID_PROJECT_TRY_DELETING_CODE_WORKSPACE_FILE_TRY_AGAIN, MessageArgument.untrusted("value", dimension.getName())));
|
||||
}
|
||||
}
|
||||
|
||||
@Director(aliases = "find-objects", description = "Capture an IGenData chunk report for nearby chunks", descriptionKey = "iris.director.commandstudio.director.capture_igendata_chunk_report_nearby_chunks", origin = DirectorOrigin.PLAYER)
|
||||
public void objects() {
|
||||
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_BE_IRIS_WORLD));
|
||||
return;
|
||||
}
|
||||
|
||||
World world = player().getWorld();
|
||||
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_BE_IRIS_WORLD_2));
|
||||
return;
|
||||
}
|
||||
KList<Chunk> chunks = new KList<>();
|
||||
Player reporter = player();
|
||||
CountDownLatch gathered = new CountDownLatch(1);
|
||||
|
||||
// The raycast and the chunk loads need the thread owning the player; the report itself stays off it.
|
||||
boolean scheduled = J.runEntity(reporter, () -> {
|
||||
try {
|
||||
int bx = reporter.getLocation().getChunk().getX();
|
||||
int bz = reporter.getLocation().getChunk().getZ();
|
||||
|
||||
try {
|
||||
Location l = reporter.getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation();
|
||||
|
||||
int cx = l.getChunk().getX();
|
||||
int cz = l.getChunk().getZ();
|
||||
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + cx, z + cz))).drain();
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + bx, z + bz))).drain();
|
||||
} finally {
|
||||
gathered.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
if (!scheduled) {
|
||||
Iris.warn("Could not schedule the chunk report scan on the thread owning " + reporter.getName() + ".");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!gathered.await(CHUNK_SCAN_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
|
||||
Iris.warn("Timed out waiting for the chunk report scan of " + reporter.getName() + ".");
|
||||
return;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CAPTURING_IGENDATA_FROM_NEARBY_CHUNKS, MessageArgument.untrusted("value", chunks.size())));
|
||||
File ff = Iris.instance.getDataFile("reports/" + M.ms() + ".txt");
|
||||
try (PrintWriter pw = new PrintWriter(ff)) {
|
||||
pw.println("=== Iris Chunk Report ===");
|
||||
pw.println("== General Info ==");
|
||||
pw.println("Iris Version: " + Iris.instance.getDescription().getVersion());
|
||||
pw.println("Bukkit Version: " + Bukkit.getBukkitVersion());
|
||||
pw.println("MC Version: " + Bukkit.getVersion());
|
||||
pw.println("PaperSpigot: " + (BukkitPlatform.isPaperServer() ? "Yup!" : "Nope!"));
|
||||
pw.println("Report Captured At: " + new Date());
|
||||
pw.println("Chunks: (" + chunks.size() + "): ");
|
||||
|
||||
for (Chunk i : chunks) {
|
||||
pw.println("- [" + i.getX() + ", " + i.getZ() + "]");
|
||||
}
|
||||
|
||||
int regions = 0;
|
||||
long size = 0;
|
||||
String age = "No idea...";
|
||||
|
||||
try {
|
||||
for (File i : Objects.requireNonNull(new File(world.getWorldFolder(), "region").listFiles())) {
|
||||
if (i.isFile()) {
|
||||
size += i.length();
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
FileTime creationTime = (FileTime) Files.getAttribute(world.getWorldFolder().toPath(), "creationTime");
|
||||
age = hrf(Duration.of(M.ms() - creationTime.toMillis(), ChronoUnit.MILLIS));
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
KList<String> biomes = new KList<>();
|
||||
KList<String> caveBiomes = new KList<>();
|
||||
KMap<String, KMap<String, KList<String>>> objects = new KMap<>();
|
||||
|
||||
for (Chunk i : chunks) {
|
||||
for (int j = 0; j < 16; j += 3) {
|
||||
|
||||
for (int k = 0; k < 16; k += 3) {
|
||||
|
||||
assert engine() != null;
|
||||
IrisBiome bb = engine().getSurfaceBiome((i.getX() * 16) + j, (i.getZ() * 16) + k);
|
||||
IrisBiome bxf = engine().getCaveBiome((i.getX() * 16) + j, (i.getZ() * 16) + k);
|
||||
biomes.addIfMissing(bb.getName() + " [" + Form.capitalize(bb.getInferredType().name().toLowerCase()) + "] " + " (" + bb.getLoadFile().getName() + ")");
|
||||
caveBiomes.addIfMissing(bxf.getName() + " (" + bxf.getLoadFile().getName() + ")");
|
||||
exportObjects(bb, pw, engine(), objects);
|
||||
exportObjects(bxf, pw, engine(), objects);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String[] regionFiles = new File(world.getWorldFolder(), "region").list();
|
||||
regions = regionFiles == null ? 0 : regionFiles.length;
|
||||
|
||||
pw.println();
|
||||
pw.println("== World Info ==");
|
||||
pw.println("World Name: " + world.getName());
|
||||
pw.println("Age: " + age);
|
||||
pw.println("Folder: " + world.getWorldFolder().getPath());
|
||||
pw.println("Regions: " + Form.f(regions));
|
||||
pw.println("Chunks: max. " + Form.f(regions * 32 * 32));
|
||||
pw.println("World Size: min. " + Form.fileSize(size));
|
||||
pw.println();
|
||||
pw.println("== Biome Info ==");
|
||||
pw.println("Found " + biomes.size() + " Biome(s): ");
|
||||
|
||||
for (String i : biomes) {
|
||||
pw.println("- " + i);
|
||||
}
|
||||
pw.println();
|
||||
|
||||
pw.println("== Object Info ==");
|
||||
|
||||
for (String i : objects.k()) {
|
||||
pw.println("- " + i);
|
||||
|
||||
for (String j : objects.get(i).k()) {
|
||||
pw.println(" @ " + j);
|
||||
|
||||
for (String k : objects.get(i).get(j)) {
|
||||
pw.println(" * " + k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pw.println();
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_REPORTED, MessageArgument.untrusted("value", ff.getPath())));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportObjects(IrisBiome bb, PrintWriter pw, Engine g, KMap<String, KMap<String, KList<String>>> objects) {
|
||||
String n1 = bb.getName() + " [" + Form.capitalize(bb.getInferredType().name().toLowerCase()) + "] " + " (" + bb.getLoadFile().getName() + ")";
|
||||
int m = 0;
|
||||
KSet<String> stop = new KSet<>();
|
||||
for (IrisObjectPlacement f : bb.getObjects()) {
|
||||
m++;
|
||||
String n2 = "Placement #" + m + " (" + f.getPlace().size() + " possible objects)";
|
||||
|
||||
for (String i : f.getPlace()) {
|
||||
String nn3 = i + ": [ERROR] Failed to find object!";
|
||||
|
||||
try {
|
||||
if (stop.contains(i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
File ff = g.getData().getObjectLoader().findFile(i);
|
||||
art.arcane.iris.generation.geometry.IrisBlockVector sz = IrisObject.sampleSize(ff);
|
||||
nn3 = i + ": size=[" + sz.getBlockX() + "," + sz.getBlockY() + "," + sz.getBlockZ() + "] location=[" + ff.getPath() + "]";
|
||||
stop.add(i);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
String n3 = nn3;
|
||||
objects.computeIfAbsent(n1, (k1) -> new KMap<>())
|
||||
.computeIfAbsent(n2, (k) -> new KList<>()).addIfMissing(n3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if server GUIs are not enabled
|
||||
*/
|
||||
private boolean noGUI() {
|
||||
if (!IrisSettings.get().getGui().isUseServerLaunchedGuis()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_HAVE_SERVER_LAUNCHED_GUIS_ENABLED_SETTINGS));
|
||||
return true;
|
||||
}
|
||||
if (!GuiHost.isAvailable()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_DESKTOP_UNAVAILABLE));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if no studio is open or the player is not in one
|
||||
*/
|
||||
private boolean noStudio() {
|
||||
if (!sender().isPlayer()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_PLAYERS_ONLY));
|
||||
return true;
|
||||
}
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_NO_STUDIO_WORLD_IS_OPEN_2));
|
||||
return true;
|
||||
}
|
||||
if (!IrisToolbelt.isStudio(world())) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_BE_STUDIO_WORLD));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public void files(File clean, KList<File> files) {
|
||||
if (clean.isDirectory()) {
|
||||
for (File i : clean.listFiles()) {
|
||||
files(i, files);
|
||||
}
|
||||
} else if (clean.getName().endsWith(".json")) {
|
||||
try {
|
||||
files.add(clean);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Iris.error("Failed to beautify " + clean.getAbsolutePath() + " You may have errors in your json!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fixBlocks(JSONObject obj) {
|
||||
for (String i : obj.keySet()) {
|
||||
Object o = obj.get(i);
|
||||
|
||||
if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) {
|
||||
obj.put(i, "minecraft:" + o);
|
||||
}
|
||||
|
||||
if (o instanceof JSONObject) {
|
||||
fixBlocks((JSONObject) o);
|
||||
} else if (o instanceof JSONArray) {
|
||||
fixBlocks((JSONArray) o);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fixBlocks(JSONArray obj) {
|
||||
for (int i = 0; i < obj.length(); i++) {
|
||||
Object o = obj.get(i);
|
||||
|
||||
if (o instanceof JSONObject) {
|
||||
fixBlocks((JSONObject) o);
|
||||
} else if (o instanceof JSONArray) {
|
||||
fixBlocks((JSONArray) o);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.studio.edit.BlockSignal;
|
||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionException;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||
import art.arcane.iris.platform.generation.EngineBukkitOps;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.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.localization.C;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.FluidCollisionMode;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
|
||||
import art.arcane.iris.localization.RuntimeUiMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
@Director(name = "what", origin = DirectorOrigin.PLAYER, description = "Iris What?", descriptionKey = "iris.director.commandwhat.director.iris_what")
|
||||
public class CommandWhat implements DirectorExecutor {
|
||||
@Director(description = "What is in my hand?", descriptionKey = "iris.director.commandwhat.director.what_is_my_hand", origin = DirectorOrigin.PLAYER)
|
||||
public void hand() {
|
||||
try {
|
||||
BlockData bd = player().getInventory().getItemInMainHand().getType().createBlockData();
|
||||
if (!bd.getMaterial().equals(Material.AIR)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL, MessageArgument.untrusted("value", bd.getMaterial().name())));
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FULL, MessageArgument.untrusted("value", bd.getAsString(true))));
|
||||
} else {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_HOLD_BLOCK_ITEM));
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Material bd = player().getInventory().getItemInMainHand().getType();
|
||||
if (!bd.equals(Material.AIR)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL_2, MessageArgument.untrusted("value", bd.name())));
|
||||
} else {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_HOLD_BLOCK_ITEM_2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "What biome am i in?", descriptionKey = "iris.director.commandwhat.director.what_biome_am_i", origin = DirectorOrigin.PLAYER)
|
||||
public void biome() {
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
onPlayerThread(player, () -> describeBiome(commandSender, player));
|
||||
}
|
||||
|
||||
private void describeBiome(VolmitSender commandSender, Player player) {
|
||||
Location location = player.getLocation();
|
||||
try {
|
||||
Engine engine = IrisToolbelt.access(location.getWorld()).getEngine();
|
||||
IrisBiome b = engine.getBiome(location.getBlockX(), location.getBlockY() - location.getWorld().getMinHeight(), location.getBlockZ());
|
||||
Biome derivative = b.getDerivative();
|
||||
NamespacedKey derivativeKey = resolveBiomeKey(derivative);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IBIOME, MessageArgument.untrusted("value", b.getLoadKey()), MessageArgument.untrusted("value2", derivativeKey == null ? IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED) : derivativeKey.getKey())));
|
||||
|
||||
} catch (SavedBiomeUnavailableException e) {
|
||||
if (e.getCause() != null) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
commandSender.sendMessage(e.isLoading()
|
||||
? "Saved biome data is loading. Run the command again shortly."
|
||||
: e.getMessage());
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Biome biome = location.getBlock().getBiome();
|
||||
NamespacedKey key = resolveBiomeKey(biome);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_NON_IRIS_BIOME, MessageArgument.untrusted("value", key == null ? IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED) : key)));
|
||||
|
||||
if (key == null || key.getKey().equals("custom")) {
|
||||
try {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DATA_PACK_BIOME_ID, MessageArgument.untrusted("value", INMS.get().getTrueBiomeBaseKey(location)), MessageArgument.untrusted("value2", INMS.get().getTrueBiomeBaseId(INMS.get().getTrueBiomeBase(location)))));
|
||||
} catch (Throwable ee) {
|
||||
Iris.reportError(ee);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "What region am i in?", descriptionKey = "iris.director.commandwhat.director.what_region_am_i", origin = DirectorOrigin.PLAYER)
|
||||
public void region() {
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
World world = world();
|
||||
Engine engine = engine();
|
||||
|
||||
// Chunk access must happen on the thread owning the player's chunk.
|
||||
onPlayerThread(player, () -> {
|
||||
try {
|
||||
Chunk chunk = world.getChunkAt(player.getLocation().getBlockX() >> 4, player.getLocation().getBlockZ() >> 4);
|
||||
IrisRegion r = EngineBukkitOps.getRegion(engine, chunk);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IREGION, MessageArgument.untrusted("value", r.getLoadKey()), MessageArgument.untrusted("value2", r.getName())));
|
||||
|
||||
} catch (SavedBiomeUnavailableException e) {
|
||||
if (e.getCause() != null) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
commandSender.sendMessage(e.isLoading()
|
||||
? "Saved biome data is loading. Run the command again shortly."
|
||||
: e.getMessage());
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "What block am i looking at?", descriptionKey = "iris.director.commandwhat.director.what_block_am_i_looking_at", origin = DirectorOrigin.PLAYER)
|
||||
public void block() {
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
// The raycast reads blocks, so it has to run on the thread owning the player.
|
||||
onPlayerThread(player, () -> {
|
||||
BlockData bd;
|
||||
try {
|
||||
bd = player.getTargetBlockExact(128, FluidCollisionMode.NEVER).getBlockData();
|
||||
} catch (NullPointerException e) {
|
||||
Iris.reportError(e);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY));
|
||||
bd = null;
|
||||
}
|
||||
|
||||
if (bd != null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL_3, MessageArgument.untrusted("value", bd.getMaterial().name())));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FULL_2, MessageArgument.untrusted("value", bd.getAsString(true))));
|
||||
|
||||
if (BukkitBlockResolution.isStorage(bd)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_STORAGE_BLOCK_LOOT_CAPABLE));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isLit(bd)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_LIT_BLOCK_LIGHT_CAPABLE));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isFoliage(bd)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOLIAGE_BLOCK));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isDecorant(bd)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DECORANT_BLOCK));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isFluid(bd)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FLUID_BLOCK));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isFoliagePlantable(bd)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLANTABLE_FOLIAGE_BLOCK));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isSolid(bd)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_SOLID_BLOCK));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Matches ModdedWhatCommands.MAX_MARKERS so both loaders truncate identically.
|
||||
private static final int MAX_MARKER_HITS = 8_192;
|
||||
private static final int MARKER_SIGNAL_BATCH = 128;
|
||||
|
||||
@Director(description = "Show markers in chunk", descriptionKey = "iris.director.commandwhat.director.show_markers_chunk", origin = DirectorOrigin.PLAYER)
|
||||
public void markers(@Param(description = "Marker name such as cave_floor or cave_ceiling", descriptionKey = "iris.director.commandwhat.param.marker_name_such_as_cave_floor_cave_ceiling") String marker) {
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only the chunk coordinates need the owning thread; the 81-chunk mantle sweep loads
|
||||
// tectonic plates from disk and must never run on the tick thread (the modded adapter
|
||||
// already scans async, leased and capped — mirror it).
|
||||
onPlayerThread(player, () -> {
|
||||
World world = player.getWorld();
|
||||
Chunk c = player.getLocation().getChunk();
|
||||
int chunkX = c.getX();
|
||||
int chunkZ = c.getZ();
|
||||
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2));
|
||||
return;
|
||||
}
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
|
||||
J.a(() -> {
|
||||
KList<Location> hits = new KList<>();
|
||||
MatterMarker matterMarker = new MatterMarker(marker);
|
||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_what_markers");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
scan:
|
||||
for (int xxx = chunkX - 4; xxx <= chunkX + 4; xxx++) {
|
||||
for (int zzz = chunkZ - 4; zzz <= chunkZ + 4; zzz++) {
|
||||
for (Location i : engine.getMantle().findMarkers(xxx, zzz, matterMarker)
|
||||
.convert((p) -> BukkitPlatform.toLocation(p, world))) {
|
||||
hits.add(i);
|
||||
if (hits.size() >= MAX_MARKER_HITS) {
|
||||
break scan;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (GenerationSessionException e) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2));
|
||||
return;
|
||||
}
|
||||
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOUND_NEARBY_MARKERS, MessageArgument.untrusted("value", hits.size()), MessageArgument.untrusted("marker", marker)));
|
||||
emitMarkerSignals(player, engine, hits, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void emitMarkerSignals(Player player, Engine engine, KList<Location> hits, int from) {
|
||||
if (from >= hits.size() || !player.isOnline() || engine.isClosed() || engine.isClosing()) {
|
||||
return;
|
||||
}
|
||||
int to = Math.min(from + MARKER_SIGNAL_BATCH, hits.size());
|
||||
for (int i = from; i < to; i++) {
|
||||
Location at = hits.get(i);
|
||||
BlockSignal.of(at.getWorld(), at.getBlockX(), at.getBlockY(), at.getBlockZ(), 100);
|
||||
}
|
||||
J.s(() -> emitMarkerSignals(player, engine, hits, to), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the body on the thread owning the player, reporting when the hop cannot be scheduled.
|
||||
*/
|
||||
private void onPlayerThread(Player player, Runnable body) {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!J.runEntity(player, body)) {
|
||||
Iris.warn("Could not schedule /iris what on the thread owning " + player.getName() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private NamespacedKey resolveBiomeKey(Biome biome) {
|
||||
Object keyOrNullValue = invokeNoThrow(biome, "getKeyOrNull");
|
||||
if (keyOrNullValue instanceof NamespacedKey namespacedKey) {
|
||||
return namespacedKey;
|
||||
}
|
||||
|
||||
Object keyOrThrowValue = invokeNoThrow(biome, "getKeyOrThrow");
|
||||
if (keyOrThrowValue instanceof NamespacedKey namespacedKey) {
|
||||
return namespacedKey;
|
||||
}
|
||||
|
||||
Object keyValue = invokeNoThrow(biome, "getKey");
|
||||
if (keyValue instanceof NamespacedKey namespacedKey) {
|
||||
return namespacedKey;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Object invokeNoThrow(Biome biome, String methodName) {
|
||||
if (biome == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Method method = biome.getClass().getMethod(methodName);
|
||||
return method.invoke(biome);
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectRotation;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
record ObjectPasteBounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
static ObjectPasteBounds resolve(
|
||||
IrisObject object,
|
||||
IrisObjectRotation rotation,
|
||||
int anchorX,
|
||||
int anchorY,
|
||||
int anchorZ
|
||||
) {
|
||||
IrisObject placedObject = Objects.requireNonNull(object, "object");
|
||||
IrisObjectRotation placementRotation = Objects.requireNonNull(rotation, "rotation");
|
||||
int[] xOffsets = {
|
||||
-placedObject.getCenter().getBlockX(),
|
||||
placedObject.getW() - 1 - placedObject.getCenter().getBlockX()
|
||||
};
|
||||
int[] yOffsets = {
|
||||
-placedObject.getCenter().getBlockY(),
|
||||
placedObject.getH() - 1 - placedObject.getCenter().getBlockY()
|
||||
};
|
||||
int[] zOffsets = {
|
||||
-placedObject.getCenter().getBlockZ(),
|
||||
placedObject.getD() - 1 - placedObject.getCenter().getBlockZ()
|
||||
};
|
||||
int minimumX = Integer.MAX_VALUE;
|
||||
int minimumY = Integer.MAX_VALUE;
|
||||
int minimumZ = Integer.MAX_VALUE;
|
||||
int maximumX = Integer.MIN_VALUE;
|
||||
int maximumY = Integer.MIN_VALUE;
|
||||
int maximumZ = Integer.MIN_VALUE;
|
||||
|
||||
for (int xOffset : xOffsets) {
|
||||
for (int yOffset : yOffsets) {
|
||||
for (int zOffset : zOffsets) {
|
||||
IrisBlockVector transformed = placementRotation.rotate(new IrisBlockVector(xOffset, yOffset, zOffset));
|
||||
int worldX = anchorX + (int) Math.round(transformed.getX());
|
||||
int worldY = anchorY + (int) Math.round(transformed.getY());
|
||||
int worldZ = anchorZ + (int) Math.round(transformed.getZ());
|
||||
minimumX = Math.min(minimumX, worldX);
|
||||
minimumY = Math.min(minimumY, worldY);
|
||||
minimumZ = Math.min(minimumZ, worldZ);
|
||||
maximumX = Math.max(maximumX, worldX);
|
||||
maximumY = Math.max(maximumY, worldY);
|
||||
maximumZ = Math.max(maximumZ, worldZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ObjectPasteBounds(minimumX, minimumY, minimumZ, maximumX, maximumY, maximumZ);
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import io.papermc.paper.command.brigadier.BasicCommand;
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Paper-only command registration. Lives outside {@code art.arcane.iris.generation.runtime} on purpose:
|
||||
* that package is eagerly Class.forName-scanned by JarScanner during enable, and resolving this
|
||||
* class on plain Spigot throws NoClassDefFoundError for the Paper lifecycle types. CommandSVC
|
||||
* loads it reflectively only when the Bukkit plugin.yml command is unavailable (Paper path).
|
||||
*/
|
||||
public final class PaperCommandRegistrar {
|
||||
private static final String ROOT_COMMAND = "iris";
|
||||
|
||||
private PaperCommandRegistrar() {
|
||||
}
|
||||
|
||||
public static void register(Iris plugin, CommandSVC commandService) {
|
||||
plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event ->
|
||||
event.registrar().register(
|
||||
ROOT_COMMAND,
|
||||
"Iris world generation command.",
|
||||
List.of("ir", "irs"),
|
||||
command(commandService)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
static BasicCommand command(CommandSVC commandService) {
|
||||
return new PaperCommand(commandService);
|
||||
}
|
||||
|
||||
private record PaperCommand(CommandSVC commandService) implements BasicCommand {
|
||||
@Override
|
||||
public void execute(CommandSourceStack source, String[] args) {
|
||||
commandService.executeRoot(source.getSender(), ROOT_COMMAND, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> suggest(CommandSourceStack source, String[] args) {
|
||||
return commandService.tabCompleteRoot(source.getSender(), ROOT_COMMAND, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse(CommandSender sender) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+797
@@ -0,0 +1,797 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.studio.view.PregeneratorJob;
|
||||
import art.arcane.iris.world.lifecycle.WorldUnloadBoundaryRegistry;
|
||||
import art.arcane.iris.pack.loading.ResourceLoader;
|
||||
import art.arcane.iris.world.pregen.MantleHeapPressure;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.localization.C;
|
||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
|
||||
import art.arcane.iris.generation.stream.CachedStream2D;
|
||||
import art.arcane.iris.generation.stream.CachedStream3D;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.world.WorldLoadEvent;
|
||||
import org.bukkit.event.world.WorldUnloadEvent;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class IrisEngineSVC implements IrisService {
|
||||
private static final long MAINTENANCE_PERIOD_MILLIS = 2_000L;
|
||||
private static final long METRICS_PERIOD_MILLIS = 1_000L;
|
||||
private static final long SHUTDOWN_TIMEOUT_SECONDS = 30L;
|
||||
|
||||
private final Object registrationLock = new Object();
|
||||
private final Set<ClosingGenerator> closingGenerators = new HashSet<>();
|
||||
private final AtomicReference<IrisEngineStatus.MaintenanceMetrics> metrics =
|
||||
new AtomicReference<>(IrisEngineStatus.MaintenanceMetrics.EMPTY);
|
||||
private final AtomicReference<IrisTelemetrySnapshot> telemetry =
|
||||
new AtomicReference<>(IrisTelemetrySnapshot.EMPTY);
|
||||
private final Map<World, CompletableFuture<Void>> pendingRegistrations = new HashMap<>();
|
||||
private final Map<World, Registered> worlds = new ConcurrentHashMap<>();
|
||||
private final IrisWorldPhaseLedger phases = new IrisWorldPhaseLedger();
|
||||
private volatile ScheduledThreadPoolExecutor service;
|
||||
private volatile ScheduledFuture<?> metricsTask;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
ScheduledThreadPoolExecutor current = service;
|
||||
if (current != null && !current.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
IrisSettings.IrisSettingsEngineSVC settings = IrisSettings.get().getPerformance().getEngineSVC();
|
||||
ThreadFactory threadFactory = (settings.isUseVirtualThreads()
|
||||
? Thread.ofVirtual()
|
||||
: Thread.ofPlatform().priority(settings.getPriority()))
|
||||
.name("Iris EngineSVC-", 0)
|
||||
.factory();
|
||||
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
|
||||
EngineMaintenance.workerParallelism(), threadFactory);
|
||||
executor.setRemoveOnCancelPolicy(true);
|
||||
executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
|
||||
executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
|
||||
service = executor;
|
||||
|
||||
for (World world : Bukkit.getWorlds()) {
|
||||
add(world);
|
||||
}
|
||||
metricsTask = executor.scheduleWithFixedDelay(
|
||||
this::updateMetricsSafely,
|
||||
0L,
|
||||
METRICS_PERIOD_MILLIS,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
ScheduledThreadPoolExecutor activeService = service;
|
||||
service = null;
|
||||
|
||||
ScheduledFuture<?> activeMetricsTask = metricsTask;
|
||||
metricsTask = null;
|
||||
if (activeMetricsTask != null) {
|
||||
activeMetricsTask.cancel(false);
|
||||
}
|
||||
|
||||
List<Teardown> teardowns = new ArrayList<>();
|
||||
List<CompletableFuture<Void>> generatorCloses;
|
||||
synchronized (registrationLock) {
|
||||
for (Map.Entry<World, Registered> entry : worlds.entrySet()) {
|
||||
Registered registered = entry.getValue();
|
||||
registered.close();
|
||||
teardowns.add(new Teardown(entry.getKey(), registered, reserveClose(registered)));
|
||||
}
|
||||
worlds.clear();
|
||||
pendingRegistrations.clear();
|
||||
generatorCloses = new ArrayList<>(closingGenerators.size());
|
||||
for (ClosingGenerator closing : closingGenerators) {
|
||||
generatorCloses.add(closing.completion());
|
||||
}
|
||||
}
|
||||
|
||||
for (Teardown teardown : teardowns) {
|
||||
phases.closing(teardown.world());
|
||||
}
|
||||
shutdownAndDrain(activeService);
|
||||
for (Teardown teardown : teardowns) {
|
||||
startClose(teardown.registered(), teardown.closing());
|
||||
}
|
||||
awaitGeneratorShutdown(generatorCloses);
|
||||
resetMetrics();
|
||||
}
|
||||
|
||||
public void engineStatus(VolmitSender sender) {
|
||||
ScheduledThreadPoolExecutor activeService = service;
|
||||
ScheduledFuture<?> activeMetricsTask = metricsTask;
|
||||
boolean serviceRunning = activeService != null && !activeService.isShutdown();
|
||||
boolean metricsRunning = activeMetricsTask != null
|
||||
&& !activeMetricsTask.isDone()
|
||||
&& !activeMetricsTask.isCancelled();
|
||||
|
||||
IrisEngineStatus.send(sender, new IrisEngineStatus.Snapshot(
|
||||
serviceRunning,
|
||||
metricsRunning,
|
||||
MAINTENANCE_PERIOD_MILLIS,
|
||||
serviceRunning ? activeService.getCorePoolSize() : 0,
|
||||
TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive()),
|
||||
MantleHeapPressure.usedFraction(),
|
||||
metrics.get()));
|
||||
}
|
||||
|
||||
public IrisTelemetrySnapshot telemetrySnapshot() {
|
||||
return telemetry.get();
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onWorldUnload(WorldUnloadEvent event) {
|
||||
World world = event.getWorld();
|
||||
CompletionStage<Boolean> unloadBoundary = WorldUnloadBoundaryRegistry.claim(
|
||||
WorldIdentity.serialize(world));
|
||||
if (remove(world, unloadBoundary) || unloadBoundary != null) {
|
||||
return;
|
||||
}
|
||||
closeUntrackedGenerator(world);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the engine of a world this service never registered.
|
||||
* <p>
|
||||
* Registration is skipped while a previous generator for the same identity is still closing and while
|
||||
* the world is not yet the server's world for its UID, and an unload that lands in that window would
|
||||
* otherwise leave a live engine bound to a dead world. Only an unload Iris did not drive is covered:
|
||||
* an Iris-driven unload carries a boundary and closes the generator in its own phase. The close is
|
||||
* CAS-idempotent, so a redundant call returns the in-flight completion.
|
||||
*/
|
||||
private void closeUntrackedGenerator(World world) {
|
||||
PlatformChunkGenerator generator = IrisToolbelt.access(world);
|
||||
if (generator == null || generator.isClosing()) {
|
||||
return;
|
||||
}
|
||||
IrisLogging.debug("EngineSVC: closing untracked generator for unloaded world " + world.getName());
|
||||
CompletableFuture<Void> close = generator.closeAsync();
|
||||
if (close == null) {
|
||||
return;
|
||||
}
|
||||
close.whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
Throwable cause = failure.getCause() == null ? failure : failure.getCause();
|
||||
reportFailure("Failed to close untracked generator for " + world.getName(), cause);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onWorldLoad(WorldLoadEvent event) {
|
||||
add(event.getWorld());
|
||||
}
|
||||
|
||||
private void add(World world) {
|
||||
ScheduledThreadPoolExecutor activeService = service;
|
||||
if (world == null || activeService == null || activeService.isShutdown() || !isCurrentWorld(world)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
RegistrationIdentity registrationIdentity = RegistrationIdentity.from(world);
|
||||
Registered replaced = null;
|
||||
ClosingGenerator replacementClose = null;
|
||||
CompletableFuture<Void> retryAfter = null;
|
||||
boolean registered = false;
|
||||
try {
|
||||
synchronized (registrationLock) {
|
||||
if (service != activeService || activeService.isShutdown() || !isCurrentWorld(world)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Registered current = worlds.get(world);
|
||||
if (current != null && !current.isClosed() && current.uses(access)) {
|
||||
pendingRegistrations.remove(world);
|
||||
return;
|
||||
}
|
||||
if (current != null) {
|
||||
worlds.remove(world, current);
|
||||
current.close();
|
||||
replaced = current;
|
||||
replacementClose = reserveClose(current);
|
||||
retryAfter = replacementClose.completion();
|
||||
} else {
|
||||
ClosingGenerator closing = findClosingGenerator(registrationIdentity);
|
||||
if (closing != null) {
|
||||
retryAfter = closing.completion();
|
||||
} else if (!access.isClosing() && !hasRegisteredConflict(registrationIdentity)) {
|
||||
Registration registration = new Registration(world.getName(), access, registrationIdentity);
|
||||
worlds.put(world, new Registered(registration, activeService));
|
||||
pendingRegistrations.remove(world);
|
||||
registered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (RejectedExecutionException exception) {
|
||||
if (!activeService.isShutdown()) {
|
||||
reportFailure("Failed to register engine maintenance for " + world.getName(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
if (registered) {
|
||||
phases.ready(world);
|
||||
}
|
||||
if (replacementClose != null) {
|
||||
phases.closing(world);
|
||||
retryRegistrationAfterClose(world, retryAfter);
|
||||
startClose(replaced, replacementClose);
|
||||
return;
|
||||
}
|
||||
if (retryAfter != null) {
|
||||
retryRegistrationAfterClose(world, retryAfter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true when a registration was found and its close was reserved
|
||||
*/
|
||||
private boolean remove(World world, CompletionStage<Boolean> unloadBoundary) {
|
||||
if (world == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Registered registered;
|
||||
ClosingGenerator closing = null;
|
||||
synchronized (registrationLock) {
|
||||
pendingRegistrations.remove(world);
|
||||
registered = worlds.remove(world);
|
||||
if (registered != null) {
|
||||
registered.close();
|
||||
closing = reserveClose(registered);
|
||||
}
|
||||
}
|
||||
if (closing == null) {
|
||||
return false;
|
||||
}
|
||||
phases.closing(world);
|
||||
deferCloseUntilWorldUnload(world, registered, closing, unloadBoundary);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void deferCloseUntilWorldUnload(
|
||||
World world,
|
||||
Registered registered,
|
||||
ClosingGenerator closing,
|
||||
CompletionStage<Boolean> unloadBoundary
|
||||
) {
|
||||
if (unloadBoundary == null) {
|
||||
J.sfut(() -> startClose(registered, closing), 1)
|
||||
.whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
Throwable cause = failure.getCause() == null ? failure : failure.getCause();
|
||||
reportFailure("Failed to defer generator close for " + registered.name(), cause);
|
||||
completeClose(closing, cause);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
unloadBoundary.whenComplete((unloaded, failure) -> {
|
||||
if (failure == null && Boolean.TRUE.equals(unloaded)) {
|
||||
startClose(registered, closing);
|
||||
return;
|
||||
}
|
||||
abandonClose(world, closing);
|
||||
});
|
||||
}
|
||||
|
||||
private void abandonClose(World world, ClosingGenerator closing) {
|
||||
synchronized (registrationLock) {
|
||||
closingGenerators.remove(closing);
|
||||
}
|
||||
closing.completion().complete(null);
|
||||
J.sfut(() -> {
|
||||
if (isCurrentWorld(world)) {
|
||||
add(world);
|
||||
}
|
||||
}, 1).whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
Throwable cause = failure.getCause() == null ? failure : failure.getCause();
|
||||
reportFailure("Failed to restore generator maintenance for " + world.getName(), cause);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ClosingGenerator reserveClose(Registered registered) {
|
||||
ClosingGenerator closing = new ClosingGenerator(
|
||||
registered.registrationIdentity(),
|
||||
new CompletableFuture<>());
|
||||
closingGenerators.add(closing);
|
||||
return closing;
|
||||
}
|
||||
|
||||
private void startClose(Registered registered, ClosingGenerator closing) {
|
||||
try {
|
||||
CompletableFuture<Void> close = registered.closeGenerator();
|
||||
if (close == null) {
|
||||
throw new IllegalStateException("Generator close returned no completion future for " + registered.name());
|
||||
}
|
||||
close.whenComplete((ignored, failure) -> completeClose(closing, failure));
|
||||
} catch (Throwable exception) {
|
||||
reportFailure("Failed to start generator close for " + registered.name(), exception);
|
||||
completeClose(closing, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void completeClose(ClosingGenerator closing, Throwable failure) {
|
||||
if (failure == null) {
|
||||
synchronized (registrationLock) {
|
||||
closingGenerators.remove(closing);
|
||||
}
|
||||
closing.completion().complete(null);
|
||||
} else {
|
||||
// A failed close must still stop conflicting with future registrations,
|
||||
// otherwise the world never regains its maintenance task after a reload.
|
||||
synchronized (registrationLock) {
|
||||
closingGenerators.remove(closing);
|
||||
}
|
||||
closing.completion().completeExceptionally(failure);
|
||||
}
|
||||
}
|
||||
|
||||
private ClosingGenerator findClosingGenerator(RegistrationIdentity registrationIdentity) {
|
||||
for (ClosingGenerator closing : closingGenerators) {
|
||||
if (closing.registrationIdentity().conflictsWith(registrationIdentity)) {
|
||||
return closing;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean hasRegisteredConflict(RegistrationIdentity registrationIdentity) {
|
||||
for (Registered registered : worlds.values()) {
|
||||
if (!registered.isClosed() && registered.registrationIdentity().conflictsWith(registrationIdentity)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void retryRegistrationAfterClose(World world, CompletableFuture<Void> completion) {
|
||||
synchronized (registrationLock) {
|
||||
if (service == null || pendingRegistrations.get(world) == completion) {
|
||||
return;
|
||||
}
|
||||
pendingRegistrations.put(world, completion);
|
||||
}
|
||||
|
||||
completion.whenComplete((ignored, failure) -> {
|
||||
synchronized (registrationLock) {
|
||||
if (pendingRegistrations.get(world) != completion) {
|
||||
return;
|
||||
}
|
||||
pendingRegistrations.remove(world);
|
||||
}
|
||||
if (failure != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScheduledThreadPoolExecutor activeService = service;
|
||||
if (activeService == null || activeService.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
J.s(() -> add(world), 1);
|
||||
} catch (Throwable exception) {
|
||||
if (service != null) {
|
||||
reportFailure("Failed to retry engine maintenance registration for " + world.getName(), exception);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean isCurrentWorld(World world) {
|
||||
return Bukkit.getWorld(world.getUID()) == world;
|
||||
}
|
||||
|
||||
private void updateMetricsSafely() {
|
||||
try {
|
||||
updateMetrics();
|
||||
} catch (Throwable exception) {
|
||||
reportFailure("Failed to update engine maintenance metrics", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMetrics() {
|
||||
long now = System.currentTimeMillis();
|
||||
List<EngineTelemetrySnapshot> worldSnapshots = new ArrayList<>(worlds.size());
|
||||
int maintenanceTasks = 0;
|
||||
int irisWorlds = 0;
|
||||
|
||||
for (Registered registered : worlds.values()) {
|
||||
if (registered.isClosed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
irisWorlds++;
|
||||
if (registered.maintenanceInFlight()) {
|
||||
maintenanceTasks++;
|
||||
}
|
||||
|
||||
try {
|
||||
Engine engine = registered.getEngine();
|
||||
if (engine == null) {
|
||||
continue;
|
||||
}
|
||||
if (engine.isClosing() && !engine.isClosed()) {
|
||||
continue;
|
||||
}
|
||||
if (engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
||||
registered.close();
|
||||
continue;
|
||||
}
|
||||
|
||||
double chunksPerSecond = registered.sampleChunksPerSecond(engine.getGenerated(), now);
|
||||
worldSnapshots.add(EngineTelemetrySnapshot.capture(engine, chunksPerSecond, now));
|
||||
registered.clearMetricsFailure();
|
||||
} catch (Throwable exception) {
|
||||
if (EngineMaintenance.isMantleClosed(exception)) {
|
||||
if (registered.generatorClosing()) {
|
||||
registered.close();
|
||||
}
|
||||
} else {
|
||||
registered.reportMetricsFailure(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EngineTelemetrySnapshot.Aggregate aggregate = EngineTelemetrySnapshot.aggregate(worldSnapshots);
|
||||
metrics.set(new IrisEngineStatus.MaintenanceMetrics(
|
||||
aggregate.mantleResidentPlates(),
|
||||
aggregate.mantleQueuedPlates(),
|
||||
aggregate.loadedChunks(),
|
||||
irisWorlds,
|
||||
maintenanceTasks,
|
||||
aggregate.mantleIdleAverageMs(),
|
||||
aggregate.mantleIdleMaxMs(),
|
||||
aggregate.mantleIdleMinMs()));
|
||||
|
||||
ScheduledThreadPoolExecutor activeService = service;
|
||||
int workers = activeService == null || activeService.isShutdown()
|
||||
? 0
|
||||
: activeService.getCorePoolSize();
|
||||
int closingCount;
|
||||
int pendingCount;
|
||||
synchronized (registrationLock) {
|
||||
closingCount = closingGenerators.size();
|
||||
pendingCount = pendingRegistrations.size();
|
||||
}
|
||||
double heapUsage = MantleHeapPressure.usedFraction();
|
||||
telemetry.set(new IrisTelemetrySnapshot(
|
||||
now,
|
||||
worldSnapshots,
|
||||
aggregate,
|
||||
maintenanceTasks,
|
||||
workers,
|
||||
closingCount,
|
||||
pendingCount,
|
||||
heapUsage,
|
||||
MantleHeapPressure.reclaimUrgency(heapUsage),
|
||||
summarizeCaches(),
|
||||
pregeneratorSnapshot()));
|
||||
}
|
||||
|
||||
private IrisTelemetrySnapshot.CacheSnapshot summarizeCaches() {
|
||||
IrisTelemetrySnapshot.CacheBucket resource = IrisTelemetrySnapshot.CacheBucket.EMPTY;
|
||||
IrisTelemetrySnapshot.CacheBucket stream2d = IrisTelemetrySnapshot.CacheBucket.EMPTY;
|
||||
IrisTelemetrySnapshot.CacheBucket stream3d = IrisTelemetrySnapshot.CacheBucket.EMPTY;
|
||||
IrisTelemetrySnapshot.CacheBucket other = IrisTelemetrySnapshot.CacheBucket.EMPTY;
|
||||
PreservationSVC preservation = IrisServices.get(PreservationSVC.class);
|
||||
List<MeteredCache> caches = preservation == null ? List.of() : preservation.getCaches();
|
||||
|
||||
for (MeteredCache cache : caches) {
|
||||
IrisTelemetrySnapshot.CacheBucket sample = new IrisTelemetrySnapshot.CacheBucket(
|
||||
1,
|
||||
cache.getSize(),
|
||||
cache.getMaxSize());
|
||||
if (cache instanceof ResourceLoader<?>) {
|
||||
resource = resource.plus(sample);
|
||||
} else if (cache instanceof CachedStream2D<?> || cache instanceof CachedDoubleStream2D) {
|
||||
stream2d = stream2d.plus(sample);
|
||||
} else if (cache instanceof CachedStream3D<?>) {
|
||||
stream3d = stream3d.plus(sample);
|
||||
} else {
|
||||
other = other.plus(sample);
|
||||
}
|
||||
}
|
||||
|
||||
IrisTelemetrySnapshot.CacheBucket total = resource.plus(stream2d).plus(stream3d).plus(other);
|
||||
return new IrisTelemetrySnapshot.CacheSnapshot(total, resource, stream2d, stream3d, other);
|
||||
}
|
||||
|
||||
private IrisTelemetrySnapshot.PregenSnapshot pregeneratorSnapshot() {
|
||||
PregeneratorJob.PregenProgress progress = PregeneratorJob.progressSnapshot();
|
||||
if (progress == null) {
|
||||
return IrisTelemetrySnapshot.PregenSnapshot.INACTIVE;
|
||||
}
|
||||
return new IrisTelemetrySnapshot.PregenSnapshot(
|
||||
true,
|
||||
progress.worldIdentity(),
|
||||
progress.worldName(),
|
||||
progress.paused(),
|
||||
progress.percent(),
|
||||
progress.generated(),
|
||||
progress.totalChunks(),
|
||||
progress.chunksRemaining(),
|
||||
progress.chunksPerSecond(),
|
||||
progress.eta(),
|
||||
progress.elapsed(),
|
||||
progress.failed());
|
||||
}
|
||||
|
||||
private void shutdownAndDrain(ScheduledThreadPoolExecutor activeService) {
|
||||
if (activeService == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeService.shutdown();
|
||||
try {
|
||||
if (!activeService.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||
activeService.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException exception) {
|
||||
activeService.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
reportFailure("Interrupted while draining engine maintenance", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void awaitGeneratorShutdown(List<CompletableFuture<Void>> closes) {
|
||||
if (closes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
CompletableFuture.allOf(closes.toArray(new CompletableFuture<?>[0]))
|
||||
.get(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
reportFailure("Interrupted while closing Iris generators", exception);
|
||||
} catch (ExecutionException exception) {
|
||||
Throwable cause = exception.getCause() == null ? exception : exception.getCause();
|
||||
reportFailure("Unexpected generator close aggregation failure", cause);
|
||||
} catch (TimeoutException exception) {
|
||||
reportFailure("Timed out while closing Iris generators", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetMetrics() {
|
||||
metrics.set(IrisEngineStatus.MaintenanceMetrics.EMPTY);
|
||||
telemetry.set(IrisTelemetrySnapshot.EMPTY);
|
||||
}
|
||||
|
||||
private static void reportFailure(String message, Throwable exception) {
|
||||
IrisLogging.reportError(exception);
|
||||
IrisLogging.error("EngineSVC: " + message);
|
||||
}
|
||||
|
||||
private final class Registered {
|
||||
private final String name;
|
||||
private final PlatformChunkGenerator access;
|
||||
private final RegistrationIdentity registrationIdentity;
|
||||
private final AtomicBoolean metricsFailureReported = new AtomicBoolean();
|
||||
private final AtomicReference<CompletableFuture<Void>> activeMaintenance = new AtomicReference<>();
|
||||
private volatile ScheduledFuture<?> maintenance;
|
||||
private volatile boolean closed;
|
||||
private long lastGeneratedSampleAtMs;
|
||||
private int lastGeneratedCount;
|
||||
|
||||
private Registered(Registration registration, ScheduledThreadPoolExecutor executor) {
|
||||
name = registration.name();
|
||||
access = registration.access();
|
||||
registrationIdentity = registration.registrationIdentity();
|
||||
long offset = ThreadLocalRandom.current().nextLong(MAINTENANCE_PERIOD_MILLIS);
|
||||
maintenance = executor.scheduleWithFixedDelay(
|
||||
this::maintain,
|
||||
offset,
|
||||
MAINTENANCE_PERIOD_MILLIS,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void maintain() {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
CompletableFuture<Void> completion = new CompletableFuture<>();
|
||||
if (!activeMaintenance.compareAndSet(null, completion)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
Engine engine = getEngine();
|
||||
if (engine == null) {
|
||||
return;
|
||||
}
|
||||
if (!EngineMaintenance.isAvailable(engine)) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean pregeneratorTargetsWorld = EngineMaintenance.pregeneratorTargets(engine);
|
||||
if (pregeneratorTargetsWorld || !EngineMaintenance.shouldRun(engine)) {
|
||||
return;
|
||||
}
|
||||
|
||||
EngineMaintenance.Outcome outcome = EngineMaintenance.run(engine);
|
||||
if (outcome.unloadedTectonicPlates() > 0) {
|
||||
IrisLogging.debug(C.GOLD + "Unloaded " + C.YELLOW
|
||||
+ outcome.unloadedTectonicPlates() + " TectonicPlates in " + C.RED
|
||||
+ Form.duration(outcome.unloadDurationMillis(), 2) + " for " + name);
|
||||
}
|
||||
} catch (Throwable exception) {
|
||||
if (EngineMaintenance.isMantleClosed(exception)) {
|
||||
if (generatorClosing()) {
|
||||
close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
reportFailure("Failed maintenance for " + name, exception);
|
||||
} finally {
|
||||
completion.complete(null);
|
||||
activeMaintenance.compareAndSet(completion, null);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean uses(PlatformChunkGenerator candidate) {
|
||||
return access == candidate;
|
||||
}
|
||||
|
||||
private String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
private RegistrationIdentity registrationIdentity() {
|
||||
return registrationIdentity;
|
||||
}
|
||||
|
||||
private boolean generatorClosing() {
|
||||
return access.isClosing();
|
||||
}
|
||||
|
||||
private boolean isClosed() {
|
||||
return closed;
|
||||
}
|
||||
|
||||
private boolean maintenanceInFlight() {
|
||||
return activeMaintenance.get() != null;
|
||||
}
|
||||
|
||||
private double sampleChunksPerSecond(int generatedCount, long sampledAtMs) {
|
||||
int safeGeneratedCount = Math.max(0, generatedCount);
|
||||
long previousAt = lastGeneratedSampleAtMs;
|
||||
int previousCount = lastGeneratedCount;
|
||||
lastGeneratedSampleAtMs = sampledAtMs;
|
||||
lastGeneratedCount = safeGeneratedCount;
|
||||
if (previousAt <= 0L || sampledAtMs <= previousAt) {
|
||||
return 0D;
|
||||
}
|
||||
int generatedDelta = Math.max(0, safeGeneratedCount - previousCount);
|
||||
return generatedDelta * 1000D / (sampledAtMs - previousAt);
|
||||
}
|
||||
|
||||
private void close() {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
|
||||
ScheduledFuture<?> activeMaintenance = maintenance;
|
||||
maintenance = null;
|
||||
if (activeMaintenance != null) {
|
||||
activeMaintenance.cancel(false);
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> closeGenerator() {
|
||||
CompletableFuture<Void> runningMaintenance = activeMaintenance.get();
|
||||
if (runningMaintenance == null) {
|
||||
return invokeGeneratorClose();
|
||||
}
|
||||
return runningMaintenance.thenCompose(ignored -> invokeGeneratorClose());
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> invokeGeneratorClose() {
|
||||
try {
|
||||
CompletableFuture<Void> future = access.closeAsync();
|
||||
if (future == null) {
|
||||
IllegalStateException exception = new IllegalStateException(
|
||||
"Generator close returned no completion future for " + name);
|
||||
reportFailure("Failed to close generator for " + name, exception);
|
||||
return CompletableFuture.failedFuture(exception);
|
||||
}
|
||||
return future.whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
Throwable cause = failure.getCause() == null ? failure : failure.getCause();
|
||||
reportFailure("Failed to close generator for " + name, cause);
|
||||
}
|
||||
});
|
||||
} catch (Throwable exception) {
|
||||
reportFailure("Failed to close generator for " + name, exception);
|
||||
return CompletableFuture.failedFuture(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private Engine getEngine() {
|
||||
return closed ? null : access.getEngine();
|
||||
}
|
||||
|
||||
private void clearMetricsFailure() {
|
||||
metricsFailureReported.set(false);
|
||||
}
|
||||
|
||||
private void reportMetricsFailure(Throwable exception) {
|
||||
if (metricsFailureReported.compareAndSet(false, true)) {
|
||||
reportFailure("Failed to sample metrics for " + name, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Registration(String name, PlatformChunkGenerator access,
|
||||
RegistrationIdentity registrationIdentity) {
|
||||
}
|
||||
|
||||
record RegistrationIdentity(String worldIdentity, Path worldFolder) {
|
||||
RegistrationIdentity {
|
||||
worldIdentity = Objects.requireNonNull(worldIdentity);
|
||||
worldFolder = Objects.requireNonNull(worldFolder).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private static RegistrationIdentity from(World world) {
|
||||
return new RegistrationIdentity(
|
||||
WorldIdentity.serialize(world),
|
||||
world.getWorldFolder().toPath());
|
||||
}
|
||||
|
||||
boolean conflictsWith(RegistrationIdentity candidate) {
|
||||
return worldIdentity.equals(candidate.worldIdentity)
|
||||
|| worldFolder.equals(candidate.worldFolder);
|
||||
}
|
||||
}
|
||||
|
||||
private record ClosingGenerator(RegistrationIdentity registrationIdentity,
|
||||
CompletableFuture<Void> completion) {
|
||||
}
|
||||
|
||||
private record Teardown(World world, Registered registered, ClosingGenerator closing) {
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.pack.loading.ResourceLoader;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
|
||||
import art.arcane.iris.generation.stream.CachedStream2D;
|
||||
import art.arcane.iris.generation.stream.CachedStream3D;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
import art.arcane.iris.localization.RuntimeUiMessages;
|
||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||
final class IrisEngineStatus {
|
||||
private IrisEngineStatus() {
|
||||
}
|
||||
|
||||
static void send(VolmitSender sender, Snapshot snapshot) {
|
||||
CacheSummary caches = summarizeCaches();
|
||||
MaintenanceMetrics metrics = snapshot.metrics();
|
||||
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_MESSAGE));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_STATUS));
|
||||
sender.sendMessage(IrisLanguage.text(
|
||||
BukkitCommandMessages.IRIS_ENGINE_STATUS_SERVICE,
|
||||
MessageArgument.trusted("value", status(snapshot.serviceRunning()))
|
||||
));
|
||||
sender.sendMessage(IrisLanguage.text(
|
||||
BukkitCommandMessages.IRIS_ENGINE_STATUS_METRICS,
|
||||
MessageArgument.trusted("value", status(snapshot.metricsRunning()))
|
||||
));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_MAINTENANCE_PERIOD, MessageArgument.untrusted("value", Form.duration(snapshot.maintenancePeriodMillis()))));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_WORKER_PARALLELISM, MessageArgument.untrusted("value", snapshot.workerParallelism())));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_ACTIVE_WORLD_TASKS, MessageArgument.untrusted("value", metrics.activeTasks())));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_TECTONIC_PLATES));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_CONFIGURED_RETENTION, MessageArgument.untrusted("value", Form.duration(snapshot.retentionMillis()))));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_HEAP_USAGE, MessageArgument.untrusted("value", Form.pc(snapshot.heapUsage()))));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_RESIDENT, MessageArgument.untrusted("value", metrics.residentTectonicPlates())));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_QUEUED, MessageArgument.untrusted("value", metrics.queuedTectonicPlates())));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_AVERAGE_IDLE_DURATION, MessageArgument.untrusted("value", Form.duration(metrics.averageIdleDuration(), 2))));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_MAX_IDLE_DURATION, MessageArgument.untrusted("value", Form.duration(metrics.maxIdleDuration(), 2))));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_MIN_IDLE_DURATION, MessageArgument.untrusted("value", Form.duration(metrics.minIdleDuration(), 2))));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_CACHES));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_RESOURCE, MessageArgument.untrusted("value", caches.sizes()[0]), MessageArgument.untrusted("value2", caches.counts()[0])));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_2D_STREAM, MessageArgument.untrusted("value", caches.sizes()[1]), MessageArgument.untrusted("value2", caches.counts()[1])));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_3D_STREAM, MessageArgument.untrusted("value", caches.sizes()[2]), MessageArgument.untrusted("value2", caches.counts()[2])));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_OTHER, MessageArgument.untrusted("value", caches.sizes()[3]), MessageArgument.untrusted("value2", caches.counts()[3])));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_OTHER_2));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_IRIS_WORLDS, MessageArgument.untrusted("value", metrics.worlds())));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_LOADED_CHUNKS, MessageArgument.untrusted("value", metrics.loadedChunks())));
|
||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.IRIS_ENGINE_STATUS_MESSAGE_2));
|
||||
}
|
||||
|
||||
private static String status(boolean running) {
|
||||
return IrisLanguage.text(running ? RuntimeUiMessages.STATUS_RUNNING : RuntimeUiMessages.STATUS_STOPPED);
|
||||
}
|
||||
|
||||
private static CacheSummary summarizeCaches() {
|
||||
long[] sizes = new long[4];
|
||||
long[] counts = new long[4];
|
||||
PreservationSVC preservation = IrisServices.get(PreservationSVC.class);
|
||||
List<MeteredCache> caches = preservation == null ? List.of() : preservation.getCaches();
|
||||
|
||||
for (MeteredCache cache : caches) {
|
||||
int type = switch (cache) {
|
||||
case ResourceLoader<?> ignored -> 0;
|
||||
case CachedStream2D<?> ignored -> 1;
|
||||
case CachedDoubleStream2D ignored -> 1;
|
||||
case CachedStream3D<?> ignored -> 2;
|
||||
default -> 3;
|
||||
};
|
||||
sizes[type] += cache.getSize();
|
||||
counts[type]++;
|
||||
}
|
||||
return new CacheSummary(sizes, counts);
|
||||
}
|
||||
|
||||
record Snapshot(boolean serviceRunning,
|
||||
boolean metricsRunning,
|
||||
long maintenancePeriodMillis,
|
||||
int workerParallelism,
|
||||
long retentionMillis,
|
||||
double heapUsage,
|
||||
MaintenanceMetrics metrics) {
|
||||
}
|
||||
|
||||
record MaintenanceMetrics(long residentTectonicPlates,
|
||||
long queuedTectonicPlates,
|
||||
long loadedChunks,
|
||||
int worlds,
|
||||
int activeTasks,
|
||||
double averageIdleDuration,
|
||||
double maxIdleDuration,
|
||||
double minIdleDuration) {
|
||||
static final MaintenanceMetrics EMPTY = new MaintenanceMetrics(0L, 0L, 0L, 0, 0, 0D, 0D, 0D);
|
||||
}
|
||||
|
||||
private record CacheSummary(long[] sizes, long[] counts) {
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record IrisTelemetrySnapshot(
|
||||
long sampledAtMs,
|
||||
List<EngineTelemetrySnapshot> worlds,
|
||||
EngineTelemetrySnapshot.Aggregate aggregate,
|
||||
int maintenanceActiveTasks,
|
||||
int maintenanceWorkers,
|
||||
int closingEngines,
|
||||
int pendingRegistrations,
|
||||
double heapUsage,
|
||||
double reclaimUrgency,
|
||||
CacheSnapshot caches,
|
||||
PregenSnapshot pregenerator
|
||||
) {
|
||||
public static final IrisTelemetrySnapshot EMPTY = new IrisTelemetrySnapshot(
|
||||
0L,
|
||||
List.of(),
|
||||
EngineTelemetrySnapshot.aggregate(List.of()),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0D,
|
||||
0D,
|
||||
CacheSnapshot.EMPTY,
|
||||
PregenSnapshot.INACTIVE
|
||||
);
|
||||
|
||||
public IrisTelemetrySnapshot {
|
||||
worlds = worlds == null ? List.of() : List.copyOf(worlds);
|
||||
aggregate = aggregate == null ? EngineTelemetrySnapshot.aggregate(worlds) : aggregate;
|
||||
maintenanceActiveTasks = Math.max(0, maintenanceActiveTasks);
|
||||
maintenanceWorkers = Math.max(0, maintenanceWorkers);
|
||||
closingEngines = Math.max(0, closingEngines);
|
||||
pendingRegistrations = Math.max(0, pendingRegistrations);
|
||||
heapUsage = finiteRatio(heapUsage);
|
||||
reclaimUrgency = finiteRatio(reclaimUrgency);
|
||||
caches = caches == null ? CacheSnapshot.EMPTY : caches;
|
||||
pregenerator = pregenerator == null ? PregenSnapshot.INACTIVE : pregenerator;
|
||||
}
|
||||
|
||||
private static double finiteRatio(double value) {
|
||||
return Double.isFinite(value) ? Math.max(0D, Math.min(1D, value)) : 0D;
|
||||
}
|
||||
|
||||
public record CacheSnapshot(
|
||||
CacheBucket total,
|
||||
CacheBucket resource,
|
||||
CacheBucket stream2d,
|
||||
CacheBucket stream3d,
|
||||
CacheBucket other
|
||||
) {
|
||||
public static final CacheSnapshot EMPTY = new CacheSnapshot(
|
||||
CacheBucket.EMPTY,
|
||||
CacheBucket.EMPTY,
|
||||
CacheBucket.EMPTY,
|
||||
CacheBucket.EMPTY,
|
||||
CacheBucket.EMPTY
|
||||
);
|
||||
|
||||
public CacheSnapshot {
|
||||
total = total == null ? CacheBucket.EMPTY : total;
|
||||
resource = resource == null ? CacheBucket.EMPTY : resource;
|
||||
stream2d = stream2d == null ? CacheBucket.EMPTY : stream2d;
|
||||
stream3d = stream3d == null ? CacheBucket.EMPTY : stream3d;
|
||||
other = other == null ? CacheBucket.EMPTY : other;
|
||||
}
|
||||
}
|
||||
|
||||
public record CacheBucket(int count, long entries, long capacity) {
|
||||
public static final CacheBucket EMPTY = new CacheBucket(0, 0L, 0L);
|
||||
|
||||
public CacheBucket {
|
||||
count = Math.max(0, count);
|
||||
entries = Math.max(0L, entries);
|
||||
capacity = Math.max(0L, capacity);
|
||||
}
|
||||
|
||||
public double usage() {
|
||||
return capacity <= 0L ? 0D : Math.min(1D, entries / (double) capacity);
|
||||
}
|
||||
|
||||
CacheBucket plus(CacheBucket other) {
|
||||
if (other == null) {
|
||||
return this;
|
||||
}
|
||||
return new CacheBucket(count + other.count, entries + other.entries, capacity + other.capacity);
|
||||
}
|
||||
}
|
||||
|
||||
public record PregenSnapshot(
|
||||
boolean active,
|
||||
String worldIdentity,
|
||||
String worldName,
|
||||
boolean paused,
|
||||
double progressPercent,
|
||||
long generated,
|
||||
long total,
|
||||
long remaining,
|
||||
double chunksPerSecond,
|
||||
long etaMs,
|
||||
long elapsedMs,
|
||||
long failed
|
||||
) {
|
||||
public static final PregenSnapshot INACTIVE = new PregenSnapshot(
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
0D,
|
||||
0L,
|
||||
0L,
|
||||
0L,
|
||||
0D,
|
||||
0L,
|
||||
0L,
|
||||
0L
|
||||
);
|
||||
|
||||
public PregenSnapshot {
|
||||
worldIdentity = worldIdentity == null ? "" : worldIdentity.trim();
|
||||
worldName = worldName == null ? "" : worldName.trim();
|
||||
progressPercent = Double.isFinite(progressPercent)
|
||||
? Math.max(0D, Math.min(100D, progressPercent))
|
||||
: 0D;
|
||||
generated = Math.max(0L, generated);
|
||||
total = Math.max(0L, total);
|
||||
remaining = Math.max(0L, remaining);
|
||||
chunksPerSecond = Double.isFinite(chunksPerSecond) ? Math.max(0D, chunksPerSecond) : 0D;
|
||||
etaMs = Math.max(0L, etaMs);
|
||||
elapsedMs = Math.max(0L, elapsedMs);
|
||||
failed = Math.max(0L, failed);
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.api.IrisApiEventSVC;
|
||||
|
||||
import art.arcane.iris.api.world.IrisWorldPhase;
|
||||
import org.bukkit.World;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
final class IrisWorldPhaseLedger {
|
||||
@FunctionalInterface
|
||||
interface Dispatch {
|
||||
void fire(World world, IrisWorldPhase phase);
|
||||
}
|
||||
|
||||
private final Set<UUID> announced = ConcurrentHashMap.newKeySet();
|
||||
private final Dispatch dispatch;
|
||||
|
||||
IrisWorldPhaseLedger() {
|
||||
this(IrisApiEventSVC::fireWorldPhase);
|
||||
}
|
||||
|
||||
IrisWorldPhaseLedger(Dispatch dispatch) {
|
||||
this.dispatch = Objects.requireNonNull(dispatch, "dispatch");
|
||||
}
|
||||
|
||||
void ready(World world) {
|
||||
UUID identity = identityOf(world);
|
||||
if (identity == null || !announced.add(identity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch.fire(world, IrisWorldPhase.ENGINE_READY);
|
||||
}
|
||||
|
||||
void closing(World world) {
|
||||
UUID identity = identityOf(world);
|
||||
if (identity == null || !announced.remove(identity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch.fire(world, IrisWorldPhase.ENGINE_CLOSING);
|
||||
}
|
||||
|
||||
private static UUID identityOf(World world) {
|
||||
return world == null ? null : world.getUID();
|
||||
}
|
||||
}
|
||||
+388
@@ -0,0 +1,388 @@
|
||||
package art.arcane.iris.integration;
|
||||
|
||||
import art.arcane.iris.generation.runtime.IrisEngineSVC;
|
||||
import art.arcane.iris.generation.runtime.IrisTelemetrySnapshot;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.generation.runtime.EngineTelemetrySnapshot;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||
import art.arcane.volmlib.integration.IntegrationHandshakeRequest;
|
||||
import art.arcane.volmlib.integration.IntegrationHandshakeResponse;
|
||||
import art.arcane.volmlib.integration.IntegrationHeartbeat;
|
||||
import art.arcane.volmlib.integration.IntegrationMetricDescriptor;
|
||||
import art.arcane.volmlib.integration.IntegrationMetricGroup;
|
||||
import art.arcane.volmlib.integration.IntegrationMetricSample;
|
||||
import art.arcane.volmlib.integration.IntegrationMetricSchema;
|
||||
import art.arcane.volmlib.integration.IntegrationProtocolNegotiator;
|
||||
import art.arcane.volmlib.integration.IntegrationProtocolVersion;
|
||||
import art.arcane.volmlib.integration.IntegrationServiceContract;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.ServicePriority;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class IrisIntegrationService implements IrisService, IntegrationServiceContract {
|
||||
private static final IntegrationProtocolVersion CURRENT_PROTOCOL = new IntegrationProtocolVersion(1, 2);
|
||||
private static final Set<IntegrationProtocolVersion> SUPPORTED_PROTOCOLS = Set.of(
|
||||
new IntegrationProtocolVersion(1, 0),
|
||||
new IntegrationProtocolVersion(1, 1),
|
||||
CURRENT_PROTOCOL
|
||||
);
|
||||
private static final Set<String> CAPABILITIES = Set.of(
|
||||
"handshake",
|
||||
"heartbeat",
|
||||
"metrics",
|
||||
"metric-groups",
|
||||
"iris-engine-metrics",
|
||||
"iris-world-metrics"
|
||||
);
|
||||
private static final Map<String, String> TIMING_KEYS = Map.ofEntries(
|
||||
Map.entry("total", IntegrationMetricSchema.IRIS_GENERATION_TOTAL_MS),
|
||||
Map.entry("updates", IntegrationMetricSchema.IRIS_GENERATION_UPDATES_MS),
|
||||
Map.entry("terrain", IntegrationMetricSchema.IRIS_GENERATION_TERRAIN_MS),
|
||||
Map.entry("biome", IntegrationMetricSchema.IRIS_GENERATION_BIOME_MS),
|
||||
Map.entry("post", IntegrationMetricSchema.IRIS_GENERATION_POST_MS),
|
||||
Map.entry("perfection", IntegrationMetricSchema.IRIS_GENERATION_PERFECTION_MS),
|
||||
Map.entry("decoration", IntegrationMetricSchema.IRIS_GENERATION_DECORATION_MS),
|
||||
Map.entry("cave", IntegrationMetricSchema.IRIS_GENERATION_CAVE_MS),
|
||||
Map.entry("deposit", IntegrationMetricSchema.IRIS_GENERATION_DEPOSIT_MS),
|
||||
Map.entry("carve.resolve", IntegrationMetricSchema.IRIS_GENERATION_CARVE_RESOLVE_MS),
|
||||
Map.entry("carve.apply", IntegrationMetricSchema.IRIS_GENERATION_CARVE_APPLY_MS),
|
||||
Map.entry("context.prefill", IntegrationMetricSchema.IRIS_GENERATION_CONTEXT_PREFILL_MS),
|
||||
Map.entry("pregen.wait.permit", IntegrationMetricSchema.IRIS_PREGEN_WAIT_PERMIT_MS),
|
||||
Map.entry("pregen.wait.adaptive", IntegrationMetricSchema.IRIS_PREGEN_WAIT_ADAPTIVE_MS)
|
||||
);
|
||||
|
||||
private final Supplier<IrisTelemetrySnapshot> telemetrySupplier;
|
||||
|
||||
public IrisIntegrationService() {
|
||||
this(IrisIntegrationService::currentTelemetry);
|
||||
}
|
||||
|
||||
IrisIntegrationService(Supplier<IrisTelemetrySnapshot> telemetrySupplier) {
|
||||
this.telemetrySupplier = telemetrySupplier == null
|
||||
? () -> IrisTelemetrySnapshot.EMPTY
|
||||
: telemetrySupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
Bukkit.getServicesManager().register(IntegrationServiceContract.class, this, Iris.instance, ServicePriority.Normal);
|
||||
Iris.verbose("Integration provider registered for Iris");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
Bukkit.getServicesManager().unregister(IntegrationServiceContract.class, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String pluginId() {
|
||||
return "iris";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String pluginVersion() {
|
||||
return Iris.instance.getDescription().getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<IntegrationProtocolVersion> supportedProtocols() {
|
||||
return SUPPORTED_PROTOCOLS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> capabilities() {
|
||||
return CAPABILITIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<IntegrationMetricDescriptor> metricDescriptors() {
|
||||
Set<IntegrationMetricDescriptor> descriptors = new LinkedHashSet<>();
|
||||
for (String key : IntegrationMetricSchema.irisKeys()) {
|
||||
descriptors.add(IntegrationMetricSchema.descriptor(key));
|
||||
}
|
||||
return Set.copyOf(descriptors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntegrationHandshakeResponse handshake(IntegrationHandshakeRequest request) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (request == null) {
|
||||
return new IntegrationHandshakeResponse(
|
||||
pluginId(),
|
||||
pluginVersion(),
|
||||
false,
|
||||
null,
|
||||
SUPPORTED_PROTOCOLS,
|
||||
CAPABILITIES,
|
||||
"missing request",
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
Optional<IntegrationProtocolVersion> negotiated = IntegrationProtocolNegotiator.negotiate(
|
||||
SUPPORTED_PROTOCOLS,
|
||||
request.supportedProtocols()
|
||||
);
|
||||
if (negotiated.isEmpty()) {
|
||||
return new IntegrationHandshakeResponse(
|
||||
pluginId(),
|
||||
pluginVersion(),
|
||||
false,
|
||||
null,
|
||||
SUPPORTED_PROTOCOLS,
|
||||
CAPABILITIES,
|
||||
"no-common-protocol",
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
return new IntegrationHandshakeResponse(
|
||||
pluginId(),
|
||||
pluginVersion(),
|
||||
true,
|
||||
negotiated.get(),
|
||||
SUPPORTED_PROTOCOLS,
|
||||
CAPABILITIES,
|
||||
"ok",
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntegrationHeartbeat heartbeat() {
|
||||
return new IntegrationHeartbeat(CURRENT_PROTOCOL, true, System.currentTimeMillis(), "ok");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, IntegrationMetricSample> sampleMetrics(Set<String> metricKeys) {
|
||||
Set<String> requested = metricKeys == null || metricKeys.isEmpty()
|
||||
? IntegrationMetricSchema.irisKeys()
|
||||
: Set.copyOf(metricKeys);
|
||||
IrisTelemetrySnapshot snapshot = safeSnapshot();
|
||||
long sampledAtMs = snapshot.sampledAtMs() > 0L
|
||||
? snapshot.sampledAtMs()
|
||||
: System.currentTimeMillis();
|
||||
if (snapshot.sampledAtMs() <= 0L) {
|
||||
return unavailableMetrics(requested, "telemetry-not-ready", sampledAtMs);
|
||||
}
|
||||
|
||||
Map<String, IntegrationMetricSample> available = globalSamples(snapshot);
|
||||
Map<String, IntegrationMetricSample> selected = new LinkedHashMap<>(requested.size());
|
||||
for (String key : requested) {
|
||||
IntegrationMetricSample sample = available.get(key);
|
||||
selected.put(key, sample == null
|
||||
? unavailable(key, "unsupported-key", sampledAtMs)
|
||||
: sample);
|
||||
}
|
||||
return Map.copyOf(selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IntegrationMetricGroup> metricGroups() {
|
||||
IrisTelemetrySnapshot snapshot = safeSnapshot();
|
||||
if (snapshot.sampledAtMs() <= 0L || snapshot.worlds().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<EngineTelemetrySnapshot> worlds = new ArrayList<>(snapshot.worlds());
|
||||
worlds.sort(Comparator.comparing(EngineTelemetrySnapshot::worldIdentity));
|
||||
List<IntegrationMetricGroup> groups = new ArrayList<>(worlds.size());
|
||||
for (EngineTelemetrySnapshot world : worlds) {
|
||||
groups.add(new IntegrationMetricGroup(
|
||||
"world",
|
||||
world.worldIdentity(),
|
||||
world.worldName(),
|
||||
Map.of(
|
||||
"plugin", "iris",
|
||||
"dimension", world.dimensionKey()
|
||||
),
|
||||
worldSamples(world, snapshot.pregenerator())
|
||||
));
|
||||
}
|
||||
return List.copyOf(groups);
|
||||
}
|
||||
|
||||
private Map<String, IntegrationMetricSample> globalSamples(IrisTelemetrySnapshot snapshot) {
|
||||
long sampledAtMs = snapshot.sampledAtMs();
|
||||
EngineTelemetrySnapshot.Aggregate aggregate = snapshot.aggregate();
|
||||
Map<String, IntegrationMetricSample> samples = new LinkedHashMap<>();
|
||||
put(samples, IntegrationMetricSchema.IRIS_WORLD_COUNT, aggregate.worldCount(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_ENGINE_ACTIVE, aggregate.active(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_ENGINE_CLOSING, aggregate.closing() + snapshot.closingEngines(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_ENGINE_FAILED, aggregate.failed(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_ENGINE_STUDIO, aggregate.studio(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_ENGINE_PENDING_REGISTRATIONS, snapshot.pendingRegistrations(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS, aggregate.loadedChunks(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_LOADED_ENTITIES, aggregate.loadedEntities(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_ENTITY_SATURATION, aggregate.entitySaturationMax(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_SESSION, aggregate.generatedSession(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL, aggregate.generatedTotal(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_PER_SECOND, aggregate.chunksPerSecond(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_BLOCK_UPDATES_PER_SECOND, aggregate.blockUpdatesPerSecond(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_ENGINE_PARALLELISM, aggregate.parallelism(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_GENERATION_ACTIVE_LEASES, aggregate.activeGenerationLeases(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_HOTLOADS_TOTAL, aggregate.hotloadsTotal(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_MAINTENANCE_ACTIVE_TASKS, snapshot.maintenanceActiveTasks(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_MAINTENANCE_WORKERS, snapshot.maintenanceWorkers(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_MANTLE_RESIDENT_PLATES, aggregate.mantleResidentPlates(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_MANTLE_QUEUED_PLATES, aggregate.mantleQueuedPlates(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_AVERAGE_MS, aggregate.mantleIdleAverageMs(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_MAX_MS, aggregate.mantleIdleMaxMs(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_MIN_MS, aggregate.mantleIdleMinMs(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_MANTLE_HEAP_USAGE, snapshot.heapUsage(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_MANTLE_RECLAIM_URGENCY, snapshot.reclaimUrgency(), sampledAtMs);
|
||||
putCacheSamples(samples, snapshot.caches(), sampledAtMs);
|
||||
putPregenSamples(samples, snapshot.pregenerator(), sampledAtMs);
|
||||
putTimingSamples(samples, aggregate.generationTimingMaximaMs(), sampledAtMs);
|
||||
return Map.copyOf(samples);
|
||||
}
|
||||
|
||||
private Map<String, IntegrationMetricSample> worldSamples(
|
||||
EngineTelemetrySnapshot world,
|
||||
IrisTelemetrySnapshot.PregenSnapshot pregenerator
|
||||
) {
|
||||
long sampledAtMs = world.sampledAtMs();
|
||||
Map<String, IntegrationMetricSample> samples = new LinkedHashMap<>();
|
||||
put(samples, IntegrationMetricSchema.IRIS_ENGINE_ACTIVE, world.active() ? 1 : 0, sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_ENGINE_CLOSING, world.closing() ? 1 : 0, sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_ENGINE_FAILED, world.failed() ? 1 : 0, sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_ENGINE_STUDIO, world.studio() ? 1 : 0, sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_LOADED_CHUNKS, world.loadedChunks(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_LOADED_ENTITIES, world.loadedEntities(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_ENTITY_SATURATION, world.entitySaturation(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_SESSION, world.generatedSession(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_GENERATED_TOTAL, world.generatedTotal(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_CHUNKS_PER_SECOND, world.chunksPerSecond(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_BLOCK_UPDATES_PER_SECOND, world.blockUpdatesPerSecond(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_ENGINE_PARALLELISM, world.parallelism(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_GENERATION_ACTIVE_LEASES, world.activeGenerationLeases(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_HOTLOADS_TOTAL, world.hotloadsTotal(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_MANTLE_RESIDENT_PLATES, world.mantleResidentPlates(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_MANTLE_QUEUED_PLATES, world.mantleQueuedPlates(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_MANTLE_IDLE_AVERAGE_MS, world.mantleIdleMs(), sampledAtMs);
|
||||
boolean targetsWorld = pregenerator.active()
|
||||
&& world.worldIdentity().equals(pregenerator.worldIdentity());
|
||||
putPregenSamples(samples, targetsWorld ? pregenerator : IrisTelemetrySnapshot.PregenSnapshot.INACTIVE, sampledAtMs);
|
||||
putTimingSamples(samples, world.generationTimingsMs(), sampledAtMs);
|
||||
samples.keySet().retainAll(IntegrationMetricSchema.irisWorldKeys());
|
||||
return Map.copyOf(samples);
|
||||
}
|
||||
|
||||
private void putCacheSamples(
|
||||
Map<String, IntegrationMetricSample> samples,
|
||||
IrisTelemetrySnapshot.CacheSnapshot caches,
|
||||
long sampledAtMs
|
||||
) {
|
||||
putCacheBucket(samples, caches.total(), IntegrationMetricSchema.IRIS_CACHE_COUNT, IntegrationMetricSchema.IRIS_CACHE_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_USAGE, sampledAtMs);
|
||||
putCacheBucket(samples, caches.resource(), IntegrationMetricSchema.IRIS_CACHE_RESOURCE_COUNT, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_RESOURCE_USAGE, sampledAtMs);
|
||||
putCacheBucket(samples, caches.stream2d(), IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_COUNT, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_STREAM_2D_USAGE, sampledAtMs);
|
||||
putCacheBucket(samples, caches.stream3d(), IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_COUNT, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_STREAM_3D_USAGE, sampledAtMs);
|
||||
putCacheBucket(samples, caches.other(), IntegrationMetricSchema.IRIS_CACHE_OTHER_COUNT, IntegrationMetricSchema.IRIS_CACHE_OTHER_ENTRIES, IntegrationMetricSchema.IRIS_CACHE_OTHER_CAPACITY, IntegrationMetricSchema.IRIS_CACHE_OTHER_USAGE, sampledAtMs);
|
||||
}
|
||||
|
||||
private void putCacheBucket(
|
||||
Map<String, IntegrationMetricSample> samples,
|
||||
IrisTelemetrySnapshot.CacheBucket bucket,
|
||||
String countKey,
|
||||
String entriesKey,
|
||||
String capacityKey,
|
||||
String usageKey,
|
||||
long sampledAtMs
|
||||
) {
|
||||
put(samples, countKey, bucket.count(), sampledAtMs);
|
||||
put(samples, entriesKey, bucket.entries(), sampledAtMs);
|
||||
put(samples, capacityKey, bucket.capacity(), sampledAtMs);
|
||||
put(samples, usageKey, bucket.usage(), sampledAtMs);
|
||||
}
|
||||
|
||||
private void putPregenSamples(
|
||||
Map<String, IntegrationMetricSample> samples,
|
||||
IrisTelemetrySnapshot.PregenSnapshot pregenerator,
|
||||
long sampledAtMs
|
||||
) {
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ACTIVE, pregenerator.active() ? 1 : 0, sampledAtMs);
|
||||
if (!pregenerator.active()) {
|
||||
for (String key : Set.of(
|
||||
IntegrationMetricSchema.IRIS_PREGEN_PAUSED,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_PROGRESS,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_GENERATED,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_TOTAL,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_QUEUE,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_ETA_MS,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_MS,
|
||||
IntegrationMetricSchema.IRIS_PREGEN_FAILED)) {
|
||||
samples.put(key, unavailable(key, "pregen-inactive", sampledAtMs));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_PAUSED, pregenerator.paused() ? 1 : 0, sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_PROGRESS, pregenerator.progressPercent(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_GENERATED, pregenerator.generated(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_TOTAL, pregenerator.total(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_QUEUE, pregenerator.remaining(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_THROUGHPUT, pregenerator.chunksPerSecond(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ETA_MS, pregenerator.etaMs(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_ELAPSED_MS, pregenerator.elapsedMs(), sampledAtMs);
|
||||
put(samples, IntegrationMetricSchema.IRIS_PREGEN_FAILED, pregenerator.failed(), sampledAtMs);
|
||||
}
|
||||
|
||||
private void putTimingSamples(
|
||||
Map<String, IntegrationMetricSample> samples,
|
||||
Map<String, Double> timings,
|
||||
long sampledAtMs
|
||||
) {
|
||||
for (Map.Entry<String, String> entry : TIMING_KEYS.entrySet()) {
|
||||
Double value = timings.get(entry.getKey());
|
||||
samples.put(entry.getValue(), value == null
|
||||
? unavailable(entry.getValue(), "timing-not-available", sampledAtMs)
|
||||
: available(entry.getValue(), value, sampledAtMs));
|
||||
}
|
||||
}
|
||||
|
||||
private void put(Map<String, IntegrationMetricSample> samples, String key, double value, long sampledAtMs) {
|
||||
samples.put(key, available(key, value, sampledAtMs));
|
||||
}
|
||||
|
||||
private IntegrationMetricSample available(String key, double value, long sampledAtMs) {
|
||||
return IntegrationMetricSample.available(IntegrationMetricSchema.descriptor(key), value, sampledAtMs);
|
||||
}
|
||||
|
||||
private IntegrationMetricSample unavailable(String key, String reason, long sampledAtMs) {
|
||||
return IntegrationMetricSample.unavailable(IntegrationMetricSchema.descriptor(key), reason, sampledAtMs);
|
||||
}
|
||||
|
||||
private Map<String, IntegrationMetricSample> unavailableMetrics(
|
||||
Set<String> keys,
|
||||
String reason,
|
||||
long sampledAtMs
|
||||
) {
|
||||
Map<String, IntegrationMetricSample> samples = new LinkedHashMap<>(keys.size());
|
||||
for (String key : keys) {
|
||||
samples.put(key, unavailable(key, reason, sampledAtMs));
|
||||
}
|
||||
return Map.copyOf(samples);
|
||||
}
|
||||
|
||||
private IrisTelemetrySnapshot safeSnapshot() {
|
||||
IrisTelemetrySnapshot snapshot = telemetrySupplier.get();
|
||||
return snapshot == null ? IrisTelemetrySnapshot.EMPTY : snapshot;
|
||||
}
|
||||
|
||||
private static IrisTelemetrySnapshot currentTelemetry() {
|
||||
IrisEngineSVC service = IrisServices.get(IrisEngineSVC.class);
|
||||
return service == null ? IrisTelemetrySnapshot.EMPTY : service.telemetrySnapshot();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user