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