mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
Compare commits
1176
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60ba82c6fb | ||
|
|
86ef09e768 | ||
|
|
97612385e5 | ||
|
|
3123ab3753 | ||
|
|
4ac268047e | ||
|
|
0b5b75e936 | ||
|
|
97dfb63741 | ||
|
|
29942fa1c2 | ||
|
|
ea42141550 | ||
|
|
9eb56543a3 | ||
|
|
93f5615f00 | ||
|
|
f30f761ae6 | ||
|
|
03fd5e4bf3 | ||
|
|
2659837007 | ||
|
|
9d7211378d | ||
|
|
dc7615219b | ||
|
|
8d12019246 | ||
|
|
26825869e9 | ||
|
|
59626d2434 | ||
|
|
46f5472081 | ||
|
|
2cd9167549 | ||
|
|
a648ef6622 | ||
|
|
51c9d01bf7 | ||
|
|
99258bfd98 | ||
|
|
db3d42df26 | ||
|
|
b97bd51f20 | ||
|
|
a02b4386a4 | ||
|
|
5a99091193 | ||
|
|
88c2688662 | ||
|
|
f73c78349b | ||
|
|
0a76208194 | ||
|
|
38b340ac14 | ||
|
|
bfeb017037 | ||
|
|
89b864a142 | ||
|
|
a49a4e0949 | ||
|
|
3c773e3e57 | ||
|
|
84ffce263a | ||
|
|
e4675ecab8 | ||
|
|
b24c38896d | ||
|
|
6270b62552 | ||
|
|
8a1c7ac998 | ||
|
|
8c39f9a01b | ||
|
|
74756dd19e | ||
|
|
5363cbbc02 | ||
|
|
19bbca0e4b | ||
|
|
ec0d2d816a | ||
|
|
340e66105c | ||
|
|
e58d065ca4 | ||
|
|
cebd582018 | ||
|
|
f44ce5b327 | ||
|
|
b1445430bb | ||
|
|
80e7e37791 | ||
|
|
8e71c14f65 | ||
|
|
eb1a244114 | ||
|
|
05eed1f6a5 | ||
|
|
6c3822cdaf | ||
|
|
13312e9182 | ||
|
|
674502eeda | ||
|
|
143ba4771b | ||
|
|
02956fd358 | ||
|
|
76860e94fa | ||
|
|
252facf41e | ||
|
|
ad88b00c2a | ||
|
|
6f930d1370 | ||
|
|
855ab90be4 | ||
|
|
8783fa09a5 | ||
|
|
87dc78934f | ||
|
|
d35fe93f7f | ||
|
|
d6d6fd031c | ||
|
|
7f34bb917f | ||
|
|
18e0834db8 | ||
|
|
6a85cb53a9 | ||
|
|
8a0fd9b9fc | ||
|
|
2fc4c2faf9 | ||
|
|
475f72a300 | ||
|
|
bc7a530486 | ||
|
|
b12b1ee304 | ||
|
|
196de7d858 | ||
|
|
8a18b6ff8b | ||
|
|
e1514503f6 | ||
|
|
f1c1869e43 | ||
|
|
31615dc052 | ||
|
|
4ddfd9549f | ||
|
|
ed67dd6058 | ||
|
|
ee7f54429a | ||
|
|
1e0628f768 | ||
|
|
72faf76269 | ||
|
|
28c4941941 | ||
|
|
d452f10fd9 | ||
|
|
e7d89cadd8 | ||
|
|
cddd6b16d6 | ||
|
|
b2ac4e7f93 | ||
|
|
c2bb32459f | ||
|
|
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 |
No files matched your search
@@ -40,6 +40,9 @@ body:
|
|||||||
- 1.17.1
|
- 1.17.1
|
||||||
- 1.18
|
- 1.18
|
||||||
- 1.19
|
- 1.19
|
||||||
|
- 1.20
|
||||||
|
- 1.21
|
||||||
|
- 1.22
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
|
|||||||
+1
-1
@@ -10,4 +10,4 @@ libs/
|
|||||||
|
|
||||||
collection/
|
collection/
|
||||||
|
|
||||||
src/main/java/com/volmit/iris/util/uniques/
|
/core/src/main/java/com/volmit/iris/util/uniques/
|
||||||
@@ -15,30 +15,27 @@ Consider supporting our development by buying Iris on spigot! We work hard to ma
|
|||||||
|
|
||||||
### Command Line Builds
|
### Command Line Builds
|
||||||
|
|
||||||
1. Install [Java JDK 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
|
1. Install [Java JDK 21](https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html)
|
||||||
2. Set the JDK installation path to `JAVA_HOME` as an environment variable.
|
2. Set the JDK installation path to `JAVA_HOME` as an environment variable.
|
||||||
* Windows
|
* Windows
|
||||||
1. Start > Type `env` and press Enter
|
1. Start > Type `env` and press Enter
|
||||||
2. Advanced > Environment Variables
|
2. Advanced > Environment Variables
|
||||||
3. Under System Variables, click `New...`
|
3. Under System Variables, click `New...`
|
||||||
4. Variable Name: `JAVA_HOME`
|
4. Variable Name: `JAVA_HOME`
|
||||||
5. Variable Value: `C:\Program Files\Java\jdk-17.0.1` (verify this exists after installing java don't just copy
|
5. Variable Value: `C:\Program Files\Java\jdk-21.0.1` (verify this exists after installing java don't just copy
|
||||||
the example text)
|
the example text)
|
||||||
* MacOS
|
* MacOS
|
||||||
1. Run `/usr/libexec/java_home -V` and look for Java 17
|
1. Run `/usr/libexec/java_home -V` and look for Java 21
|
||||||
2. Run `sudo nano ~/.zshenv`
|
2. Run `sudo nano ~/.zshenv`
|
||||||
3. Add `export JAVA_HOME=$(/usr/libexec/java_home)` as a new line
|
3. Add `export JAVA_HOME=$(/usr/libexec/java_home)` as a new line
|
||||||
4. Use `CTRL + X`, then Press `Y`, Then `ENTER`
|
4. Use `CTRL + X`, then Press `Y`, Then `ENTER`
|
||||||
5. Quit & Reopen Terminal and verify with `echo $JAVA_HOME`. It should print a directory
|
5. Quit & Reopen Terminal and verify with `echo $JAVA_HOME`. It should print a directory
|
||||||
3. If this is your first time building Iris for MC 1.18+ run `gradlew setup` inside the root Iris project folder.
|
3. Once the project has setup, run `gradlew iris`
|
||||||
Otherwise, skip this step. Grab a coffee, this may take up to 5 minutes depending on your cpu & internet connection.
|
4. The Iris jar will be placed in `Iris/build/Iris-XXX-XXX.jar` Enjoy! Consider supporting us by buying it on spigot!
|
||||||
4. Once the project has setup, run `gradlew iris`
|
|
||||||
5. The Iris jar will be placed in `Iris/build/Iris-XXX-XXX.jar` Enjoy! Consider supporting us by buying it on spigot!
|
|
||||||
|
|
||||||
### IDE Builds (for development)
|
### IDE Builds (for development)
|
||||||
|
|
||||||
* Run `gradlew setup` any time you get dependency issues with craftbukkit
|
* Configure ITJ Gradle to use JDK 21 (in settings, search for gradle)
|
||||||
* Configure ITJ Gradle to use JDK 17 (in settings, search for gradle)
|
|
||||||
* Add a build line in the build.gradle for your own build task to directly compile Iris into your plugins folder if you
|
* Add a build line in the build.gradle for your own build task to directly compile Iris into your plugins folder if you
|
||||||
prefer.
|
prefer.
|
||||||
* Resync the project & run your newly created task (under the development folder in gradle tasks!)
|
* Resync the project & run your newly created task (under the development folder in gradle tasks!)
|
||||||
@@ -69,7 +66,6 @@ IrisAccess access=IrisToolbelt.createWorld() // If you like builders...
|
|||||||
.name("myWorld") // The world name
|
.name("myWorld") // The world name
|
||||||
.dimension("terrifyinghands")
|
.dimension("terrifyinghands")
|
||||||
.seed(69133742) // The world seed
|
.seed(69133742) // The world seed
|
||||||
.headless(true) // Headless make gen go fast
|
|
||||||
.pregen(PregenTask // Define a pregen job to run
|
.pregen(PregenTask // Define a pregen job to run
|
||||||
.builder()
|
.builder()
|
||||||
.center(new Position2(0,0)) // REGION coords (1 region = 32x32 chunks)
|
.center(new Position2(0,0)) // REGION coords (1 region = 32x32 chunks)
|
||||||
|
|||||||
-332
@@ -1,332 +0,0 @@
|
|||||||
/*
|
|
||||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
|
||||||
* Copyright (c) 2021 Arcane Arts (Volmit Software)
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id 'java'
|
|
||||||
id 'java-library'
|
|
||||||
id "io.freefair.lombok" version "6.3.0"
|
|
||||||
id "com.github.johnrengelman.shadow" version "7.1.2"
|
|
||||||
id "de.undercouch.download" version "5.0.1"
|
|
||||||
}
|
|
||||||
|
|
||||||
version '2.7.0-1.20.0'
|
|
||||||
def nmsVersion = '1.20' //[NMS]
|
|
||||||
def apiVersion = '1.20'
|
|
||||||
def specialSourceVersion = '1.11.0' //[NMS]
|
|
||||||
def spigotJarVersion = '1.20-R0.1-SNAPSHOT' //[NMS]
|
|
||||||
def name = getRootProject().getName() // Defined in settings.gradle
|
|
||||||
def main = 'com.volmit.iris.Iris'
|
|
||||||
|
|
||||||
// ADD YOURSELF AS A NEW LINE IF YOU WANT YOUR OWN BUILD TASK GENERATED
|
|
||||||
// ======================== WINDOWS =============================
|
|
||||||
registerCustomOutputTask('Cyberpwn', 'C://Users/cyberpwn/Documents/development/server/plugins')
|
|
||||||
registerCustomOutputTask('Psycho', 'C://Dan/MinecraftDevelopment/Server/plugins')
|
|
||||||
registerCustomOutputTask('ArcaneArts', 'C://Users/arcane/Documents/development/server/plugins')
|
|
||||||
registerCustomOutputTask('Coco', 'D://mcsm/plugins')
|
|
||||||
registerCustomOutputTask('Strange', 'D://Servers/1.17 Test Server/plugins')
|
|
||||||
registerCustomOutputTask('Vatuu', 'D://Minecraft/Servers/1.19.4/plugins')
|
|
||||||
// ========================== UNIX ==============================
|
|
||||||
registerCustomOutputTaskUnix('CyberpwnLT', '/Users/danielmills/development/server/plugins')
|
|
||||||
registerCustomOutputTaskUnix('PsychoLT', '/Users/brianfopiano/Desktop/REMOTES/RemoteMinecraft/plugins')
|
|
||||||
// ==============================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gradle is weird sometimes, we need to delete the plugin yml from the build folder to actually filter properly.
|
|
||||||
*/
|
|
||||||
file(jar.archiveFile.get().getAsFile().getParentFile().getParentFile().getParentFile().getAbsolutePath() + '/build/resources/main/plugin.yml').delete()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expand properties into plugin yml
|
|
||||||
*/
|
|
||||||
processResources {
|
|
||||||
filesMatching('**/plugin.yml') {
|
|
||||||
expand(
|
|
||||||
'name': name.toString(),
|
|
||||||
'version': version.toString(),
|
|
||||||
'main': main.toString(),
|
|
||||||
'apiversion': apiVersion.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unified repo
|
|
||||||
*/
|
|
||||||
repositories {
|
|
||||||
mavenLocal {
|
|
||||||
content {
|
|
||||||
includeGroup("org.bukkit")
|
|
||||||
includeGroup("org.spigotmc")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mavenCentral()
|
|
||||||
maven { url "https://arcanearts.jfrog.io/artifactory/archives" }
|
|
||||||
maven { url "https://mvn.lumine.io/repository/maven-public/" }
|
|
||||||
maven { url "https://jitpack.io"}
|
|
||||||
|
|
||||||
maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots" }
|
|
||||||
maven { url "https://mvn.lumine.io/repository/maven/" }
|
|
||||||
maven { url "https://repo.triumphteam.dev/snapshots" }
|
|
||||||
maven { url "https://repo.mineinabyss.com/releases" }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We need parameter meta for the decree command system
|
|
||||||
*/
|
|
||||||
compileJava {
|
|
||||||
options.compilerArgs << '-parameters'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure Iris for shading
|
|
||||||
*/
|
|
||||||
shadowJar {
|
|
||||||
//minimize()
|
|
||||||
append("plugin.yml")
|
|
||||||
relocate 'com.dfsek.paralithic', 'com.volmit.iris.util.paralithic'
|
|
||||||
relocate 'io.papermc.lib', 'com.volmit.iris.util.paper'
|
|
||||||
relocate 'net.kyori', 'com.volmit.iris.util.kyori'
|
|
||||||
dependencies {
|
|
||||||
include(dependency('io.papermc:paperlib'))
|
|
||||||
include(dependency('com.dfsek:Paralithic'))
|
|
||||||
include(dependency('net.kyori:'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations.all {
|
|
||||||
resolutionStrategy.cacheChangingModulesFor 60, 'minutes'
|
|
||||||
resolutionStrategy.cacheDynamicVersionsFor 60, 'minutes'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependencies.
|
|
||||||
*
|
|
||||||
* Provided or classpath dependencies are not shaded and are available on the runtime classpath
|
|
||||||
*
|
|
||||||
* Shaded dependencies are not available at runtime, nor are they available on mvn central so they
|
|
||||||
* need to be shaded into the jar (increasing binary size)
|
|
||||||
*
|
|
||||||
* Dynamically loaded dependencies are defined in the plugin.yml (updating these must be updated in the
|
|
||||||
* plugin.yml also, otherwise they wont be available). These do not increase binary size). Only declare
|
|
||||||
* these dependencies if they are available on mvn central.
|
|
||||||
*/
|
|
||||||
dependencies {
|
|
||||||
// Provided or Classpath
|
|
||||||
compileOnly 'org.projectlombok:lombok:1.18.24'
|
|
||||||
annotationProcessor 'org.projectlombok:lombok:1.18.24'
|
|
||||||
implementation 'org.spigotmc:spigot-api:1.20-R0.1-SNAPSHOT'
|
|
||||||
implementation 'org.bukkit:craftbukkit:1.20-R0.1-SNAPSHOT:remapped-mojang' //[NMS]
|
|
||||||
|
|
||||||
// Third Party Integrations
|
|
||||||
implementation 'com.github.oraxen:oraxen:1.152.5'
|
|
||||||
implementation 'com.github.LoneDev6:api-itemsadder:3.4.1-r4'
|
|
||||||
implementation 'me.clip:placeholderapi:2.11.3'
|
|
||||||
//implementation files('libs/CustomItems.jar')
|
|
||||||
|
|
||||||
// Shaded
|
|
||||||
implementation 'com.dfsek:Paralithic:0.4.0'
|
|
||||||
implementation 'io.papermc:paperlib:1.0.5'
|
|
||||||
implementation "net.kyori:adventure-text-minimessage:4.13.1"
|
|
||||||
implementation 'net.kyori:adventure-platform-bukkit:4.3.0'
|
|
||||||
implementation 'net.kyori:adventure-api:4.13.1'
|
|
||||||
implementation 'io.lumine:Mythic-Dist:5.2.1'
|
|
||||||
|
|
||||||
// Dynamically Loaded
|
|
||||||
implementation 'io.timeandspace:smoothie-map:2.0.2'
|
|
||||||
implementation 'it.unimi.dsi:fastutil:8.5.8'
|
|
||||||
implementation 'com.googlecode.concurrentlinkedhashmap:concurrentlinkedhashmap-lru:1.4.2'
|
|
||||||
implementation 'org.zeroturnaround:zt-zip:1.14'
|
|
||||||
implementation 'com.google.code.gson:gson:2.9.0'
|
|
||||||
implementation 'org.ow2.asm:asm:9.2'
|
|
||||||
implementation 'com.google.guava:guava:31.1-jre'
|
|
||||||
implementation 'bsf:bsf:2.4.0'
|
|
||||||
implementation 'rhino:js:1.7R2'
|
|
||||||
implementation 'com.github.ben-manes.caffeine:caffeine:3.0.6'
|
|
||||||
implementation 'org.apache.commons:commons-lang3:3.12.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (JavaVersion.current().toString() != "17") {
|
|
||||||
System.err.println()
|
|
||||||
System.err.println("=========================================================================================================")
|
|
||||||
System.err.println("You must run gradle on Java 17. You are using " + JavaVersion.current())
|
|
||||||
System.err.println()
|
|
||||||
System.err.println("=== For IDEs ===")
|
|
||||||
System.err.println("1. Configure the project for Java 17")
|
|
||||||
System.err.println("2. Configure the bundled gradle to use Java 17 in settings")
|
|
||||||
System.err.println()
|
|
||||||
System.err.println("=== For Command Line (gradlew) ===")
|
|
||||||
System.err.println("1. Install JDK 17 from https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html")
|
|
||||||
System.err.println("2. Set JAVA_HOME environment variable to the new jdk installation folder such as C:\\Program Files\\Java\\jdk-17.0.1")
|
|
||||||
System.err.println("3. Open a new command prompt window to get the new environment variables if need be.")
|
|
||||||
System.err.println("=========================================================================================================")
|
|
||||||
System.err.println()
|
|
||||||
System.exit(69);
|
|
||||||
}
|
|
||||||
|
|
||||||
def buildToolsJar = new File(buildDir, "buildtools/BuildTools.jar");
|
|
||||||
def specialSourceJar = new File(buildDir, "specialsource/SpecialSource.jar");
|
|
||||||
def buildToolsFolder = new File(buildDir, "buildtools");
|
|
||||||
def specialSourceFolder = new File(buildDir, "specialsource");
|
|
||||||
def buildToolsHint = new File(buildDir, "buildtools/craftbukkit-" + nmsVersion + ".jar");
|
|
||||||
def outputShadeJar = new File(buildDir, "libs/Iris-" + version + "-all.jar");
|
|
||||||
def ssiJar = new File(buildDir, "specialsource/Iris-" + version + "-all.jar");
|
|
||||||
def ssobfJar = new File(buildDir, "specialsource/Iris-" + version + "-rmo.jar");
|
|
||||||
def ssJar = new File(buildDir, "specialsource/Iris-" + version + "-rma.jar");
|
|
||||||
def homePath = System.properties['user.home']
|
|
||||||
def m2 = new File(homePath + "/.m2/repository")
|
|
||||||
def m2s = m2.getAbsolutePath();
|
|
||||||
|
|
||||||
// ======================== Building Mapped Jars =============================
|
|
||||||
task downloadBuildtools(type: Download) {
|
|
||||||
group "remapping"
|
|
||||||
src 'https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar'
|
|
||||||
dest buildToolsJar
|
|
||||||
onlyIf {
|
|
||||||
!buildToolsJar.exists()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task downloadSpecialSource(type: Download) {
|
|
||||||
group "remapping"
|
|
||||||
src 'https://repo.maven.apache.org/maven2/net/md-5/SpecialSource/' + specialSourceVersion + '/SpecialSource-'+specialSourceVersion+'-shaded.jar'
|
|
||||||
dest specialSourceJar
|
|
||||||
onlyIf {
|
|
||||||
!specialSourceJar.exists()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task executeBuildTools(dependsOn: downloadBuildtools, type: JavaExec)
|
|
||||||
{
|
|
||||||
group "remapping"
|
|
||||||
classpath = files(buildToolsJar)
|
|
||||||
workingDir = buildToolsFolder
|
|
||||||
args = [
|
|
||||||
"--rev",
|
|
||||||
nmsVersion,
|
|
||||||
"--compile",
|
|
||||||
"craftbukkit",
|
|
||||||
"--remap"
|
|
||||||
]
|
|
||||||
onlyIf {
|
|
||||||
!buildToolsHint.exists()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task copyBuildToSpecialSource(type: Copy)
|
|
||||||
{
|
|
||||||
group "remapping"
|
|
||||||
from outputShadeJar
|
|
||||||
into specialSourceFolder
|
|
||||||
dependsOn(downloadSpecialSource, shadowJar)
|
|
||||||
}
|
|
||||||
|
|
||||||
task specialSourceRemapObfuscate(type: JavaExec)
|
|
||||||
{
|
|
||||||
group "remapping"
|
|
||||||
dependsOn(copyBuildToSpecialSource, downloadSpecialSource, shadowJar)
|
|
||||||
workingDir = specialSourceFolder
|
|
||||||
classpath = files(specialSourceJar,
|
|
||||||
new File(m2s + "/org/spigotmc/spigot/" + spigotJarVersion + "/spigot-" + spigotJarVersion + "-remapped-mojang.jar"))
|
|
||||||
mainClass = "net.md_5.specialsource.SpecialSource"
|
|
||||||
args = [
|
|
||||||
"--live",
|
|
||||||
"-i",
|
|
||||||
ssiJar.getName(),
|
|
||||||
"-o",
|
|
||||||
ssobfJar.getName(),
|
|
||||||
"-m",
|
|
||||||
m2s + "/org/spigotmc/minecraft-server/" + spigotJarVersion + "/minecraft-server-" + spigotJarVersion + "-maps-mojang.txt",
|
|
||||||
"--reverse",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
task specialSourceRemap(type: JavaExec)
|
|
||||||
{
|
|
||||||
group "remapping"
|
|
||||||
dependsOn(specialSourceRemapObfuscate)
|
|
||||||
workingDir = specialSourceFolder
|
|
||||||
classpath = files(specialSourceJar,
|
|
||||||
new File(m2s + "/org/spigotmc/spigot/" + spigotJarVersion + "/spigot-" + spigotJarVersion + "-remapped-obf.jar"))
|
|
||||||
mainClass = "net.md_5.specialsource.SpecialSource"
|
|
||||||
args = [
|
|
||||||
"--live",
|
|
||||||
"-i",
|
|
||||||
ssobfJar.getName(),
|
|
||||||
"-o",
|
|
||||||
ssJar.getName(),
|
|
||||||
"-m",
|
|
||||||
m2s + "/org/spigotmc/minecraft-server/" + spigotJarVersion + "/minecraft-server-" + spigotJarVersion + "-maps-spigot.csrg"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.compileJava.dependsOn(executeBuildTools)
|
|
||||||
|
|
||||||
compileJava {
|
|
||||||
options.encoding = "UTF-8"
|
|
||||||
}
|
|
||||||
|
|
||||||
task setup()
|
|
||||||
{
|
|
||||||
group("iris")
|
|
||||||
dependsOn(clean, executeBuildTools)
|
|
||||||
}
|
|
||||||
|
|
||||||
task iris(type: Copy)
|
|
||||||
{
|
|
||||||
group "iris"
|
|
||||||
from ssJar
|
|
||||||
into buildDir
|
|
||||||
rename { String fileName ->
|
|
||||||
fileName.replace('Iris-' + version + '-rma.jar', "Iris-" + version + ".jar")
|
|
||||||
}
|
|
||||||
dependsOn(specialSourceRemap)
|
|
||||||
}
|
|
||||||
|
|
||||||
def registerCustomOutputTask(name, path) {
|
|
||||||
if (!System.properties['os.name'].toLowerCase().contains('windows')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('build' + name, Copy) {
|
|
||||||
group('development')
|
|
||||||
outputs.upToDateWhen { false }
|
|
||||||
dependsOn(iris)
|
|
||||||
from(new File(buildDir, "Iris-" + version + ".jar"))
|
|
||||||
into(file(path))
|
|
||||||
rename { String fileName ->
|
|
||||||
fileName.replace("Iris-" + version + ".jar", "Iris.jar")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def registerCustomOutputTaskUnix(name, path) {
|
|
||||||
if (System.properties['os.name'].toLowerCase().contains('windows')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('build' + name, Copy) {
|
|
||||||
group('development')
|
|
||||||
outputs.upToDateWhen { false }
|
|
||||||
dependsOn(iris)
|
|
||||||
from(new File(buildDir, "Iris-" + version + ".jar"))
|
|
||||||
into(file(path))
|
|
||||||
rename { String fileName ->
|
|
||||||
fileName.replace("Iris-" + version + ".jar", "Iris.jar")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
import de.undercouch.gradle.tasks.download.Download
|
||||||
|
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||||
|
import xyz.jpenilla.runpaper.task.RunServer
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2021 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories.maven("https://jitpack.io")
|
||||||
|
dependencies.classpath("com.github.VolmitSoftware:NMSTools:c88961416f")
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
java
|
||||||
|
`java-library`
|
||||||
|
alias(libs.plugins.download)
|
||||||
|
alias(libs.plugins.runPaper)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "com.volmit"
|
||||||
|
version = "3.9.2-1.20.1-26.3"
|
||||||
|
|
||||||
|
apply<ApiGenerator>()
|
||||||
|
|
||||||
|
// ADD YOURSELF AS A NEW LINE IF YOU WANT YOUR OWN BUILD TASK GENERATED
|
||||||
|
// ======================== WINDOWS =============================
|
||||||
|
registerCustomOutputTask("Cyberpwn", "C://Users/cyberpwn/Documents/development/server/plugins")
|
||||||
|
registerCustomOutputTask("Psycho", "C://Dan/MinecraftDevelopment/Server/plugins")
|
||||||
|
registerCustomOutputTask("ArcaneArts", "C://Users/arcane/Documents/development/server/plugins")
|
||||||
|
registerCustomOutputTask("Coco", "D://mcsm/plugins")
|
||||||
|
registerCustomOutputTask("Strange", "D://Servers/1.17 Test Server/plugins")
|
||||||
|
registerCustomOutputTask("Vatuu", "D://Minecraft/Servers/1.19.4/plugins")
|
||||||
|
registerCustomOutputTask("CrazyDev22", "C://Users/Julian/Desktop/server/plugins")
|
||||||
|
registerCustomOutputTask("PixelFury", "C://Users/repix/workplace/Iris/1.21.3 - Development-Public-v3/plugins")
|
||||||
|
registerCustomOutputTask("PixelFuryDev", "C://Users/repix/workplace/Iris/1.21 - Development-v3/plugins")
|
||||||
|
// ========================== UNIX ==============================
|
||||||
|
registerCustomOutputTaskUnix("CyberpwnLT", "/Users/danielmills/development/server/plugins")
|
||||||
|
registerCustomOutputTaskUnix("PsychoLT", "/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/plugins")
|
||||||
|
registerCustomOutputTaskUnix("PixelMac", "/Users/test/Desktop/mcserver/plugins")
|
||||||
|
registerCustomOutputTaskUnix("CrazyDev22LT", "/home/julian/Desktop/server/plugins")
|
||||||
|
// ==============================================================
|
||||||
|
|
||||||
|
val serverMinHeap = "10G"
|
||||||
|
val serverMaxHeap = "10G"
|
||||||
|
val additionalFlags = "-XX:+AlwaysPreTouch"
|
||||||
|
//Valid values are: none, truecolor, indexed256, indexed16, indexed8
|
||||||
|
val color = "truecolor"
|
||||||
|
val errorReporting = "true" == findProperty("errorReporting")
|
||||||
|
|
||||||
|
val nmsBindings = mapOf(
|
||||||
|
"v26_3_R1" to "26.3.build.176-stable",
|
||||||
|
"v26_1_R1" to "26.1.2.build.74-stable",
|
||||||
|
"v1_21_R7" to "1.21.11-R0.1-SNAPSHOT",
|
||||||
|
"v1_21_R6" to "1.21.10-R0.1-SNAPSHOT",
|
||||||
|
"v1_21_R5" to "1.21.8-R0.1-SNAPSHOT",
|
||||||
|
"v1_21_R4" to "1.21.5-R0.1-SNAPSHOT",
|
||||||
|
"v1_21_R3" to "1.21.4-R0.1-SNAPSHOT",
|
||||||
|
"v1_21_R2" to "1.21.3-R0.1-SNAPSHOT",
|
||||||
|
"v1_21_R1" to "1.21.1-R0.1-SNAPSHOT",
|
||||||
|
"v1_20_R4" to "1.20.6-R0.1-SNAPSHOT",
|
||||||
|
"v1_20_R3" to "1.20.4-R0.1-SNAPSHOT",
|
||||||
|
"v1_20_R2" to "1.20.2-R0.1-SNAPSHOT",
|
||||||
|
"v1_20_R1" to "1.20.1-R0.1-SNAPSHOT",
|
||||||
|
)
|
||||||
|
val jvmVersion = mapOf("v26_3_R1" to 25, "v26_1_R1" to 25)
|
||||||
|
// From 26.1 on the server is not obfuscated, so these bindings compile against Paper's dev bundle as-is.
|
||||||
|
val userDevBindings = setOf("v26_3_R1", "v26_1_R1")
|
||||||
|
nmsBindings.forEach { (key, value) ->
|
||||||
|
project(":nms:$key") {
|
||||||
|
apply<JavaPlugin>()
|
||||||
|
|
||||||
|
nmsBinding {
|
||||||
|
jvm = jvmVersion.getOrDefault(key, 21)
|
||||||
|
version = value
|
||||||
|
type = if (key in userDevBindings) NMSBinding.Type.USER_DEV else NMSBinding.Type.DIRECT
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(project(":core"))
|
||||||
|
compileOnly(rootProject.libs.annotations)
|
||||||
|
compileOnly(rootProject.libs.byteBuddy.core)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<RunServer>("runServer-$key") {
|
||||||
|
group = "servers"
|
||||||
|
minecraftVersion(value.substringBefore("-").substringBefore(".build"))
|
||||||
|
minHeapSize = serverMinHeap
|
||||||
|
maxHeapSize = serverMaxHeap
|
||||||
|
pluginJars(tasks.jar.flatMap { it.archiveFile })
|
||||||
|
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(jvmVersion.getOrDefault(key, 21))}
|
||||||
|
runDirectory.convention(layout.buildDirectory.dir("run/$key"))
|
||||||
|
systemProperty("disable.watchdog", "true")
|
||||||
|
systemProperty("net.kyori.ansi.colorLevel", color)
|
||||||
|
systemProperty("com.mojang.eula.agree", true)
|
||||||
|
systemProperty("iris.suppressReporting", !errorReporting)
|
||||||
|
jvmArgs("-javaagent:${project(":core:agent").tasks.jar.flatMap { it.archiveFile }.get().asFile.absolutePath}")
|
||||||
|
jvmArgs(additionalFlags.split(' '))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val included: Configuration by configurations.creating
|
||||||
|
val jarJar: Configuration by configurations.creating
|
||||||
|
dependencies {
|
||||||
|
for (key in nmsBindings.keys) {
|
||||||
|
included(project(":nms:$key", if (key in userDevBindings) "runtimeElements" else "reobf"))
|
||||||
|
}
|
||||||
|
included(project(":core", "shadow"))
|
||||||
|
jarJar(project(":core:agent"))
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
jar {
|
||||||
|
inputs.files(included)
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
from(jarJar, provider { included.resolve().map(::zipTree) })
|
||||||
|
archiveFileName.set("Iris-${project.version}.jar")
|
||||||
|
}
|
||||||
|
|
||||||
|
register<Copy>("iris") {
|
||||||
|
group = "iris"
|
||||||
|
dependsOn("jar")
|
||||||
|
from(layout.buildDirectory.file("libs/Iris-${project.version}.jar"))
|
||||||
|
into(layout.buildDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
register<Copy>("irisDev") {
|
||||||
|
group = "iris"
|
||||||
|
from(project(":core").layout.buildDirectory.files("libs/core-javadoc.jar", "libs/core-sources.jar"))
|
||||||
|
rename { it.replace("core", "Iris-${project.version}") }
|
||||||
|
into(layout.buildDirectory)
|
||||||
|
dependsOn(":core:sourcesJar")
|
||||||
|
dependsOn(":core:javadocJar")
|
||||||
|
}
|
||||||
|
|
||||||
|
val cli = file("sentry-cli.exe")
|
||||||
|
register<Download>("downloadCli") {
|
||||||
|
group = "io.sentry"
|
||||||
|
src("https://release-registry.services.sentry.io/apps/sentry-cli/latest?response=download&arch=x86_64&platform=${System.getProperty("os.name")}&package=sentry-cli")
|
||||||
|
dest(cli)
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
cli.setExecutable(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
register("release") {
|
||||||
|
group = "io.sentry"
|
||||||
|
dependsOn("downloadCli")
|
||||||
|
doLast {
|
||||||
|
val url = "http://sentry.volmit.com:8080"
|
||||||
|
val authToken = project.findProperty("sentry.auth.token") ?: System.getenv("SENTRY_AUTH_TOKEN")
|
||||||
|
val org = "sentry"
|
||||||
|
val projectName = "iris"
|
||||||
|
exec(cli, "--url", url , "--auth-token", authToken, "releases", "new", "-o", org, "-p", projectName, version)
|
||||||
|
exec(cli, "--url", url , "--auth-token", authToken, "releases", "set-commits", "-o", org, "-p", projectName, version, "--auto", "--ignore-missing")
|
||||||
|
//exec(cli, "--url", url, "--auth-token", authToken, "releases", "finalize", "-o", org, "-p", projectName, version)
|
||||||
|
cli.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exec(vararg command: Any) {
|
||||||
|
val p = ProcessBuilder(command.map { it.toString() })
|
||||||
|
.start()
|
||||||
|
p.inputStream.reader().useLines { it.forEach(::println) }
|
||||||
|
p.errorStream.reader().useLines { it.forEach(::println) }
|
||||||
|
p.waitFor()
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations.configureEach {
|
||||||
|
resolutionStrategy.cacheChangingModulesFor(60, "minutes")
|
||||||
|
resolutionStrategy.cacheDynamicVersionsFor(60, "minutes")
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
apply<JavaPlugin>()
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion.set(JavaLanguageVersion.of(jvmVersion.getOrDefault(name, 21)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://repo.papermc.io/repository/maven-public/")
|
||||||
|
maven("https://repo.codemc.org/repository/maven-public/")
|
||||||
|
|
||||||
|
maven("https://jitpack.io") // EcoItems, score
|
||||||
|
maven("https://repo.nexomc.com/releases/") // nexo
|
||||||
|
maven("https://maven.devs.beer/") // itemsadder
|
||||||
|
maven("https://repo.extendedclip.com/releases/") // placeholderapi
|
||||||
|
maven("https://mvn.lumine.io/repository/maven-public/") // mythic
|
||||||
|
maven("https://nexus.phoenixdevt.fr/repository/maven-public/") //MMOItems
|
||||||
|
maven("https://repo.onarandombox.com/content/groups/public/") //Multiverse Core
|
||||||
|
maven("https://repo.momirealms.net/releases/") //CraftEngine
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Provided or Classpath
|
||||||
|
compileOnly(rootProject.libs.lombok)
|
||||||
|
annotationProcessor(rootProject.libs.lombok)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need parameter meta for the decree command system
|
||||||
|
*/
|
||||||
|
tasks {
|
||||||
|
compileJava {
|
||||||
|
options.compilerArgs.add("-parameters")
|
||||||
|
options.encoding = "UTF-8"
|
||||||
|
}
|
||||||
|
|
||||||
|
javadoc {
|
||||||
|
options.encoding = "UTF-8"
|
||||||
|
options.quiet()
|
||||||
|
//options.addStringOption("Xdoclint:none") // TODO: Re-enable this
|
||||||
|
}
|
||||||
|
|
||||||
|
register<Jar>("sourcesJar") {
|
||||||
|
archiveClassifier.set("sources")
|
||||||
|
from(sourceSets.main.map { it.allSource })
|
||||||
|
}
|
||||||
|
|
||||||
|
register<Jar>("javadocJar") {
|
||||||
|
archiveClassifier.set("javadoc")
|
||||||
|
from(javadoc.map { it.destinationDir!! })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)) {
|
||||||
|
System.err.println()
|
||||||
|
System.err.println("=========================================================================================================")
|
||||||
|
System.err.println("You must run gradle on Java 21 or newer. You are using " + JavaVersion.current())
|
||||||
|
System.err.println()
|
||||||
|
System.err.println("=== For IDEs ===")
|
||||||
|
System.err.println("1. Configure the project for Java 21 toolchain")
|
||||||
|
System.err.println("2. Configure the bundled gradle to use Java 21+ in settings")
|
||||||
|
System.err.println()
|
||||||
|
System.err.println("=== For Command Line (gradlew) ===")
|
||||||
|
System.err.println("1. Install JDK 21 from https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html")
|
||||||
|
System.err.println("2. Set JAVA_HOME environment variable to the new jdk installation folder such as C:\\Program Files\\Java\\jdk-21.0.4")
|
||||||
|
System.err.println("3. Open a new command prompt window to get the new environment variables if need be.")
|
||||||
|
System.err.println("=========================================================================================================")
|
||||||
|
System.err.println()
|
||||||
|
exitProcess(69)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun registerCustomOutputTask(name: String, path: String) {
|
||||||
|
if (!System.getProperty("os.name").lowercase().contains("windows")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Copy>("build$name") {
|
||||||
|
group = "development"
|
||||||
|
outputs.upToDateWhen { false }
|
||||||
|
dependsOn("iris")
|
||||||
|
from(layout.buildDirectory.file("Iris-${project.version}.jar"))
|
||||||
|
into(file(path))
|
||||||
|
rename { "Iris.jar" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerCustomOutputTaskUnix(name: String, path: String) {
|
||||||
|
if (System.getProperty("os.name").lowercase().contains("windows")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Copy>("build$name") {
|
||||||
|
group = "development"
|
||||||
|
outputs.upToDateWhen { false }
|
||||||
|
dependsOn("iris")
|
||||||
|
from(layout.buildDirectory.file("Iris-${project.version}.jar"))
|
||||||
|
into(file(path))
|
||||||
|
rename { "Iris.jar" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("jvm") version embeddedKotlinVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion.set(JavaLanguageVersion.of(21))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(21)
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(JvmTarget.JVM_21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
maven("https://jitpack.io")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("org.ow2.asm:asm:9.8")
|
||||||
|
implementation("com.github.VolmitSoftware:NMSTools:c88961416f")
|
||||||
|
implementation("io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import org.gradle.api.DefaultTask
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.api.publish.PublishingExtension
|
||||||
|
import org.gradle.api.publish.maven.MavenPublication
|
||||||
|
import org.gradle.api.tasks.InputFile
|
||||||
|
import org.gradle.api.tasks.OutputFile
|
||||||
|
import org.gradle.api.tasks.TaskAction
|
||||||
|
import org.gradle.jvm.tasks.Jar
|
||||||
|
import org.objectweb.asm.*
|
||||||
|
import java.io.File
|
||||||
|
import java.util.jar.JarFile
|
||||||
|
import java.util.jar.JarOutputStream
|
||||||
|
|
||||||
|
class ApiGenerator : Plugin<Project> {
|
||||||
|
override fun apply(target: Project): Unit = with(target) {
|
||||||
|
plugins.apply("maven-publish")
|
||||||
|
val task = tasks.register("irisApi", GenerateApiTask::class.java)
|
||||||
|
extensions.findByType(PublishingExtension::class.java)!!.apply {
|
||||||
|
repositories.maven {
|
||||||
|
it.name = "deployDir"
|
||||||
|
it.url = targetDirectory.toURI()
|
||||||
|
}
|
||||||
|
|
||||||
|
publications.create("maven", MavenPublication::class.java) {
|
||||||
|
it.groupId = name
|
||||||
|
it.version = version.toString()
|
||||||
|
it.artifact(task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class GenerateApiTask : DefaultTask() {
|
||||||
|
init {
|
||||||
|
group = "iris"
|
||||||
|
dependsOn("jar")
|
||||||
|
finalizedBy("publishMavenPublicationToDeployDirRepository")
|
||||||
|
doLast {
|
||||||
|
logger.lifecycle("The API is located at ${outputFile.absolutePath}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InputFile
|
||||||
|
val inputFile: File = project.tasks
|
||||||
|
.named("jar", Jar::class.java)
|
||||||
|
.get()
|
||||||
|
.archiveFile
|
||||||
|
.get()
|
||||||
|
.asFile
|
||||||
|
|
||||||
|
@OutputFile
|
||||||
|
val outputFile: File = project.targetDirectory.resolve(inputFile.name)
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun generate() {
|
||||||
|
JarFile(inputFile).use { jar ->
|
||||||
|
JarOutputStream(outputFile.apply { parentFile?.mkdirs() }.outputStream()).use { out ->
|
||||||
|
jar.stream()
|
||||||
|
.parallel()
|
||||||
|
.filter { !it.isDirectory }
|
||||||
|
.filter { it.name.endsWith(".class") }
|
||||||
|
.forEach {
|
||||||
|
val bytes = jar.getInputStream(it).use { input ->
|
||||||
|
val writer = ClassWriter(ClassWriter.COMPUTE_MAXS)
|
||||||
|
val visitor = MethodClearingVisitor(writer)
|
||||||
|
ClassReader(input).accept(visitor, 0)
|
||||||
|
writer.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(out) {
|
||||||
|
out.putNextEntry(it)
|
||||||
|
out.write(bytes)
|
||||||
|
out.closeEntry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val Project.targetDirectory: File get() {
|
||||||
|
val dir = System.getenv("DEPLOY_DIR") ?: return project.layout.buildDirectory.dir("api").get().asFile
|
||||||
|
return File(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MethodClearingVisitor(
|
||||||
|
cv: ClassVisitor
|
||||||
|
) : ClassVisitor(Opcodes.ASM9, cv) {
|
||||||
|
|
||||||
|
override fun visitMethod(
|
||||||
|
access: Int,
|
||||||
|
name: String?,
|
||||||
|
descriptor: String?,
|
||||||
|
signature: String?,
|
||||||
|
exceptions: Array<out String>?
|
||||||
|
) = ExceptionThrowingMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions))
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ExceptionThrowingMethodVisitor(
|
||||||
|
mv: MethodVisitor
|
||||||
|
) : MethodVisitor(Opcodes.ASM9, mv) {
|
||||||
|
|
||||||
|
override fun visitCode() {
|
||||||
|
if (mv == null) return
|
||||||
|
mv.visitCode()
|
||||||
|
|
||||||
|
mv.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalStateException")
|
||||||
|
mv.visitInsn(Opcodes.DUP)
|
||||||
|
mv.visitLdcInsn("Only API")
|
||||||
|
mv.visitMethodInsn(
|
||||||
|
Opcodes.INVOKESPECIAL,
|
||||||
|
"java/lang/IllegalStateException",
|
||||||
|
"<init>", "(Ljava/lang/String;)V", false
|
||||||
|
)
|
||||||
|
mv.visitInsn(Opcodes.ATHROW)
|
||||||
|
|
||||||
|
mv.visitMaxs(0, 0)
|
||||||
|
mv.visitEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import NMSBinding.Type
|
||||||
|
import com.volmit.nmstools.NMSToolsExtension
|
||||||
|
import com.volmit.nmstools.NMSToolsPlugin
|
||||||
|
import io.papermc.paperweight.userdev.PaperweightUser
|
||||||
|
import io.papermc.paperweight.userdev.PaperweightUserDependenciesExtension
|
||||||
|
import io.papermc.paperweight.userdev.PaperweightUserExtension
|
||||||
|
import io.papermc.paperweight.userdev.attribute.Obfuscation
|
||||||
|
import io.papermc.paperweight.util.constants.REOBF_CONFIG
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.gradle.api.*
|
||||||
|
import org.gradle.api.attributes.Bundling
|
||||||
|
import org.gradle.api.attributes.Category
|
||||||
|
import org.gradle.api.attributes.LibraryElements
|
||||||
|
import org.gradle.api.attributes.Usage
|
||||||
|
import org.gradle.api.model.ObjectFactory
|
||||||
|
import org.gradle.api.plugins.JavaPluginExtension
|
||||||
|
import org.gradle.api.tasks.TaskAction
|
||||||
|
import org.gradle.internal.extensions.core.extra
|
||||||
|
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||||
|
import org.gradle.jvm.toolchain.JavaToolchainService
|
||||||
|
import org.gradle.work.DisableCachingByDefault
|
||||||
|
import java.io.RandomAccessFile
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class NMSBinding : Plugin<Project> {
|
||||||
|
override fun apply(target: Project): Unit = with(target) {
|
||||||
|
val config = extra["nms"] as? Config ?: throw GradleException("No NMS binding configuration found")
|
||||||
|
val jvm = config.jvm
|
||||||
|
val type = config.type
|
||||||
|
|
||||||
|
if (type == Type.USER_DEV) {
|
||||||
|
plugins.apply(PaperweightUser::class.java)
|
||||||
|
dependencies.extensions.findByType(PaperweightUserDependenciesExtension::class.java)
|
||||||
|
?.paperDevBundle(config.version)
|
||||||
|
|
||||||
|
val java = extensions.findByType(JavaPluginExtension::class.java) ?: throw GradleException("Java plugin not found")
|
||||||
|
java.toolchain.languageVersion.set(JavaLanguageVersion.of(jvm))
|
||||||
|
|
||||||
|
val javaToolchains = project.extensions.getByType(JavaToolchainService::class.java) ?: throw GradleException("Java toolchain service not found")
|
||||||
|
extensions.configure(PaperweightUserExtension::class.java) {
|
||||||
|
it.javaLauncher.set(javaToolchains.launcherFor(java.toolchain))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
extra["nmsTools.useBuildTools"] = type == Type.BUILD_TOOLS
|
||||||
|
plugins.apply(NMSToolsPlugin::class.java)
|
||||||
|
extensions.configure(NMSToolsExtension::class.java) {
|
||||||
|
it.jvm.set(jvm)
|
||||||
|
it.version.set(config.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerReobfConfiguration("remap")
|
||||||
|
}
|
||||||
|
|
||||||
|
val (major, minor) = config.version.parseVersion()
|
||||||
|
if (major <= 20 && minor <= 4) return@with
|
||||||
|
tasks.register("convert", ConversionTask::class.java, type)
|
||||||
|
tasks.named("compileJava") { it.dependsOn("convert") }
|
||||||
|
rootProject.tasks.named("prepareKotlinBuildScriptModel") { it.dependsOn("$path:convert") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Project.registerReobfConfiguration(artifactTask: String) {
|
||||||
|
val conf = configurations.create(REOBF_CONFIG)
|
||||||
|
conf.isCanBeConsumed = true
|
||||||
|
conf.isCanBeResolved = false
|
||||||
|
conf.attributes {
|
||||||
|
it.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
|
||||||
|
it.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
|
||||||
|
it.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR))
|
||||||
|
it.attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
|
||||||
|
it.attribute(Obfuscation.OBFUSCATION_ATTRIBUTE, objects.named(Obfuscation.OBFUSCATED))
|
||||||
|
}
|
||||||
|
conf.outgoing.artifact(tasks.named(artifactTask))
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisableCachingByDefault
|
||||||
|
abstract class ConversionTask @Inject constructor(type: Type) : DefaultTask() {
|
||||||
|
private val pattern: Regex
|
||||||
|
private val replacement: String
|
||||||
|
|
||||||
|
init {
|
||||||
|
group = "nms"
|
||||||
|
inputs.property("type", type)
|
||||||
|
val java = project.extensions.findByType(JavaPluginExtension::class.java) ?: throw GradleException("Java plugin not found")
|
||||||
|
val source = java.sourceSets.named("main").map { it.allJava }
|
||||||
|
inputs.files(source)
|
||||||
|
outputs.files(source)
|
||||||
|
|
||||||
|
if (type == Type.USER_DEV) {
|
||||||
|
pattern = "org\\.bukkit\\.craftbukkit\\.${project.name}".toRegex()
|
||||||
|
replacement = "org.bukkit.craftbukkit"
|
||||||
|
} else {
|
||||||
|
pattern = "org\\.bukkit\\.craftbukkit\\.(?!${project.name})".toRegex()
|
||||||
|
replacement = "org.bukkit.craftbukkit.${project.name}."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun process() {
|
||||||
|
val dispatcher = Dispatchers.IO.limitedParallelism(16)
|
||||||
|
runBlocking {
|
||||||
|
for (file in inputs.files) {
|
||||||
|
if (file.extension !in listOf("java"))
|
||||||
|
continue
|
||||||
|
|
||||||
|
launch(dispatcher) {
|
||||||
|
val output = ArrayList<String>()
|
||||||
|
var changed = false
|
||||||
|
|
||||||
|
file.bufferedReader().use {
|
||||||
|
for (line in it.lines()) {
|
||||||
|
if (line.startsWith("package") || line.isBlank()) {
|
||||||
|
output += line
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!line.startsWith("import")) {
|
||||||
|
if (!changed) return@launch
|
||||||
|
else {
|
||||||
|
output += line
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!line.contains(pattern)) {
|
||||||
|
output += line
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
output += line.replace(pattern, replacement)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
RandomAccessFile(file, "r").use { raf ->
|
||||||
|
val bytes = ByteArray(NEW_LINE_BYTES.size)
|
||||||
|
raf.seek(raf.length() - bytes.size)
|
||||||
|
raf.readFully(bytes)
|
||||||
|
if (bytes.contentEquals(NEW_LINE_BYTES))
|
||||||
|
output += ""
|
||||||
|
}
|
||||||
|
|
||||||
|
file.writer().use {
|
||||||
|
val iterator = output.iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
it.append(iterator.next())
|
||||||
|
if (iterator.hasNext())
|
||||||
|
it.append(NEW_LINE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Type {
|
||||||
|
USER_DEV,
|
||||||
|
BUILD_TOOLS,
|
||||||
|
DIRECT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val NEW_LINE = System.lineSeparator()
|
||||||
|
private val NEW_LINE_BYTES = NEW_LINE.encodeToByteArray()
|
||||||
|
// "1.21.11-R0.1-SNAPSHOT" -> (21, 11), "26.1.2.build.74-stable" -> (26, 1)
|
||||||
|
private fun String.parseVersion() = substringBefore('-').split(".").let {
|
||||||
|
if (it[0] == "1") it[1].toInt() to it[2].toInt()
|
||||||
|
else it[0].toInt() to it[1].toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
class Config(
|
||||||
|
var jvm: Int = 21,
|
||||||
|
var type: Type = Type.DIRECT
|
||||||
|
) {
|
||||||
|
lateinit var version: String
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Project.nmsBinding(action: Config.() -> Unit) {
|
||||||
|
extra["nms"] = Config().apply(action)
|
||||||
|
plugins.apply(NMSBinding::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T : Named> ObjectFactory.named(name: String): T = named(T::class.java, name)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
plugins {
|
||||||
|
java
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.jar {
|
||||||
|
manifest.attributes(
|
||||||
|
"Agent-Class" to "com.volmit.iris.util.agent.Installer",
|
||||||
|
"Premain-Class" to "com.volmit.iris.util.agent.Installer",
|
||||||
|
"Can-Redefine-Classes" to true,
|
||||||
|
"Can-Retransform-Classes" to true
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.volmit.iris.util.agent;
|
||||||
|
|
||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
|
||||||
|
public class Installer {
|
||||||
|
private static volatile Instrumentation instrumentation;
|
||||||
|
|
||||||
|
public static Instrumentation getInstrumentation() {
|
||||||
|
Instrumentation instrumentation = Installer.instrumentation;
|
||||||
|
if (instrumentation == null) {
|
||||||
|
throw new IllegalStateException("The agent is not loaded or this method is not called via the system class loader");
|
||||||
|
}
|
||||||
|
return instrumentation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void premain(String arguments, Instrumentation instrumentation) {
|
||||||
|
doMain(instrumentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void agentmain(String arguments, Instrumentation instrumentation) {
|
||||||
|
doMain(instrumentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static synchronized void doMain(Instrumentation instrumentation) {
|
||||||
|
if (Installer.instrumentation != null)
|
||||||
|
return;
|
||||||
|
Installer.instrumentation = instrumentation;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import io.github.slimjar.func.slimjarHelper
|
||||||
|
import io.github.slimjar.resolver.data.Mirror
|
||||||
|
import org.ajoberstar.grgit.Grgit
|
||||||
|
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2021 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
java
|
||||||
|
`java-library`
|
||||||
|
alias(libs.plugins.shadow)
|
||||||
|
alias(libs.plugins.sentry)
|
||||||
|
alias(libs.plugins.slimjar)
|
||||||
|
alias(libs.plugins.grgit)
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
alias(libs.plugins.kotlin.lombok)
|
||||||
|
}
|
||||||
|
|
||||||
|
val apiVersion = "1.19"
|
||||||
|
val main = "com.volmit.iris.Iris"
|
||||||
|
val lib = "com.volmit.iris.util"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dependencies.
|
||||||
|
*
|
||||||
|
* Provided or classpath dependencies are not shaded and are available on the runtime classpath
|
||||||
|
*
|
||||||
|
* Shaded dependencies are not available at runtime, nor are they available on mvn central so they
|
||||||
|
* need to be shaded into the jar (increasing binary size)
|
||||||
|
*
|
||||||
|
* Dynamically loaded dependencies are defined in the plugin.yml (updating these must be updated in the
|
||||||
|
* plugin.yml also, otherwise they wont be available). These do not increase binary size). Only declare
|
||||||
|
* these dependencies if they are available on mvn central.
|
||||||
|
*/
|
||||||
|
dependencies {
|
||||||
|
// Provided or Classpath
|
||||||
|
compileOnly(libs.paper)
|
||||||
|
compileOnly(platform(libs.adventure.bom))
|
||||||
|
compileOnly(libs.log4j.api)
|
||||||
|
compileOnly(libs.log4j.core)
|
||||||
|
|
||||||
|
// Third Party Integrations
|
||||||
|
compileOnly(libs.nexo)
|
||||||
|
compileOnly(libs.itemsadder)
|
||||||
|
compileOnly(libs.placeholderApi)
|
||||||
|
compileOnly(libs.score)
|
||||||
|
compileOnly(libs.mmoitems)
|
||||||
|
compileOnly(libs.ecoitems)
|
||||||
|
compileOnly(libs.mythic)
|
||||||
|
compileOnly(libs.mythicChrucible)
|
||||||
|
compileOnly(libs.kgenerators) {
|
||||||
|
isTransitive = false
|
||||||
|
}
|
||||||
|
compileOnly(libs.multiverseCore)
|
||||||
|
compileOnly(libs.craftengine.core)
|
||||||
|
compileOnly(libs.craftengine.bukkit)
|
||||||
|
//compileOnly(libs.sparrowNbt)
|
||||||
|
|
||||||
|
// Shaded
|
||||||
|
implementation(slimjarHelper("spigot"))
|
||||||
|
|
||||||
|
// Dynamically Loaded
|
||||||
|
slim(libs.paralithic)
|
||||||
|
slim(libs.bstats)
|
||||||
|
slim(libs.sentry)
|
||||||
|
|
||||||
|
slim(libs.commons.io)
|
||||||
|
slim(libs.commons.lang)
|
||||||
|
slim(libs.commons.lang3)
|
||||||
|
slim(libs.commons.math3)
|
||||||
|
slim(libs.oshi)
|
||||||
|
slim(libs.lz4)
|
||||||
|
slim(libs.fastutil)
|
||||||
|
slim(libs.lru)
|
||||||
|
slim(libs.zip)
|
||||||
|
slim(libs.gson)
|
||||||
|
slim(libs.asm)
|
||||||
|
slim(libs.caffeine)
|
||||||
|
slim(libs.byteBuddy.core)
|
||||||
|
slim(libs.byteBuddy.agent)
|
||||||
|
slim(libs.dom4j)
|
||||||
|
slim(libs.jaxen)
|
||||||
|
slim(libs.flatlaf)
|
||||||
|
slim(libs.flatlaf.extras)
|
||||||
|
|
||||||
|
// Script Engine
|
||||||
|
slim(libs.kotlin.stdlib)
|
||||||
|
slim(libs.kotlin.coroutines)
|
||||||
|
slim(libs.kotlin.scripting.common)
|
||||||
|
slim(libs.kotlin.scripting.jvm)
|
||||||
|
slim(libs.kotlin.scripting.jvm.host)
|
||||||
|
slim(libs.kotlin.scripting.dependencies.maven) {
|
||||||
|
constraints {
|
||||||
|
slim(libs.mavenCore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
disableAutoTargetJvm()
|
||||||
|
toolchain {
|
||||||
|
languageVersion.set(JavaLanguageVersion.of(21))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(21)
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(JvmTarget.JVM_21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sentry {
|
||||||
|
url = "http://sentry.volmit.com:8080"
|
||||||
|
autoInstallation.enabled = false
|
||||||
|
includeSourceContext = true
|
||||||
|
|
||||||
|
org = "sentry"
|
||||||
|
projectName = "iris"
|
||||||
|
authToken = findProperty("sentry.auth.token") as String? ?: System.getenv("SENTRY_AUTH_TOKEN")
|
||||||
|
}
|
||||||
|
|
||||||
|
slimJar {
|
||||||
|
mirrors = listOf(Mirror(
|
||||||
|
URI.create("https://maven-central.storage-download.googleapis.com/maven2").toURL(),
|
||||||
|
URI.create("https://repo.maven.apache.org/maven2/").toURL()
|
||||||
|
))
|
||||||
|
|
||||||
|
relocate("com.dfsek.paralithic", "$lib.paralithic")
|
||||||
|
relocate("org.bstats", "$lib.metrics")
|
||||||
|
relocate("io.sentry", "$lib.sentry")
|
||||||
|
relocate("org.apache.maven", "$lib.maven")
|
||||||
|
relocate("org.codehaus.plexus", "$lib.plexus")
|
||||||
|
relocate("org.eclipse.sisu", "$lib.sisu")
|
||||||
|
relocate("org.eclipse.aether", "$lib.aether")
|
||||||
|
relocate("com.google.inject", "$lib.guice")
|
||||||
|
relocate("org.dom4j", "$lib.dom4j")
|
||||||
|
relocate("org.jaxen", "$lib.jaxen")
|
||||||
|
relocate("com.github.benmanes.caffeine", "$lib.caffeine")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
/**
|
||||||
|
* We need parameter meta for the decree command system
|
||||||
|
*/
|
||||||
|
compileJava {
|
||||||
|
options.compilerArgs.add("-parameters")
|
||||||
|
options.encoding = "UTF-8"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand properties into plugin yml
|
||||||
|
*/
|
||||||
|
processResources {
|
||||||
|
inputs.properties(
|
||||||
|
"name" to rootProject.name,
|
||||||
|
"version" to rootProject.version,
|
||||||
|
"apiVersion" to apiVersion,
|
||||||
|
"main" to main,
|
||||||
|
)
|
||||||
|
filesMatching("**/plugin.yml") {
|
||||||
|
expand(inputs.properties)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shadowJar {
|
||||||
|
mergeServiceFiles()
|
||||||
|
//minimize()
|
||||||
|
relocate("io.github.slimjar", "$lib.slimjar")
|
||||||
|
exclude("modules/loader-agent.isolated-jar")
|
||||||
|
}
|
||||||
|
|
||||||
|
sentryCollectSourcesJava {
|
||||||
|
dependsOn(generateTemplates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val templateSource = file("src/main/templates")
|
||||||
|
val templateDest = layout.buildDirectory.dir("generated/sources/templates")!!
|
||||||
|
val generateTemplates = tasks.register<Copy>("generateTemplates") {
|
||||||
|
inputs.properties(
|
||||||
|
"environment" to when {
|
||||||
|
project.hasProperty("release") -> "production"
|
||||||
|
project.hasProperty("argghh") -> "Argghh!"
|
||||||
|
else -> "development"
|
||||||
|
},
|
||||||
|
"commit" to provider {
|
||||||
|
val res = runCatching { project.extensions.getByType<Grgit>().head().id }
|
||||||
|
res.getOrDefault("")
|
||||||
|
.takeIf { it.length == 40 } ?: run {
|
||||||
|
this.logger.error("Git commit hash not found", res.exceptionOrNull())
|
||||||
|
"unknown"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
from(templateSource)
|
||||||
|
into(templateDest)
|
||||||
|
rename { "com/volmit/iris/$it" }
|
||||||
|
expand(inputs.properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.generateSentryBundleIdJava {
|
||||||
|
dependsOn(generateTemplates)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.tasks.named("prepareKotlinBuildScriptModel") {
|
||||||
|
dependsOn(generateTemplates)
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets.main {
|
||||||
|
java.srcDir(generateTemplates.map { it.outputs })
|
||||||
|
}
|
||||||
+189
-184
@@ -22,27 +22,27 @@ import com.google.gson.JsonObject;
|
|||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import com.google.gson.JsonParser;
|
import com.google.gson.JsonParser;
|
||||||
import com.volmit.iris.core.IrisSettings;
|
import com.volmit.iris.core.IrisSettings;
|
||||||
|
import com.volmit.iris.core.IrisWorlds;
|
||||||
import com.volmit.iris.core.ServerConfigurator;
|
import com.volmit.iris.core.ServerConfigurator;
|
||||||
import com.volmit.iris.core.link.IrisPapiExpansion;
|
import com.volmit.iris.core.link.IrisPapiExpansion;
|
||||||
import com.volmit.iris.core.link.MultiverseCoreLink;
|
import com.volmit.iris.core.link.MultiverseCoreLink;
|
||||||
import com.volmit.iris.core.link.MythicMobsLink;
|
|
||||||
import com.volmit.iris.core.loader.IrisData;
|
import com.volmit.iris.core.loader.IrisData;
|
||||||
import com.volmit.iris.core.nms.INMS;
|
import com.volmit.iris.core.nms.INMS;
|
||||||
import com.volmit.iris.core.nms.v20.NMSBinding1_20;
|
|
||||||
import com.volmit.iris.core.pregenerator.LazyPregenerator;
|
import com.volmit.iris.core.pregenerator.LazyPregenerator;
|
||||||
import com.volmit.iris.core.service.StudioSVC;
|
import com.volmit.iris.core.service.StudioSVC;
|
||||||
import com.volmit.iris.core.tools.IrisToolbelt;
|
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||||
|
import com.volmit.iris.core.world.WorldStorage;
|
||||||
import com.volmit.iris.engine.EnginePanic;
|
import com.volmit.iris.engine.EnginePanic;
|
||||||
import com.volmit.iris.engine.object.IrisCompat;
|
import com.volmit.iris.engine.object.IrisCompat;
|
||||||
import com.volmit.iris.engine.object.IrisDimension;
|
import com.volmit.iris.engine.object.IrisDimension;
|
||||||
import com.volmit.iris.engine.object.IrisWorld;
|
import com.volmit.iris.engine.object.IrisWorld;
|
||||||
import com.volmit.iris.engine.platform.BukkitChunkGenerator;
|
import com.volmit.iris.engine.platform.BukkitChunkGenerator;
|
||||||
import com.volmit.iris.engine.platform.DummyChunkGenerator;
|
import com.volmit.iris.core.safeguard.IrisSafeguard;
|
||||||
|
import com.volmit.iris.engine.platform.PlatformChunkGenerator;
|
||||||
import com.volmit.iris.util.collection.KList;
|
import com.volmit.iris.util.collection.KList;
|
||||||
import com.volmit.iris.util.collection.KMap;
|
import com.volmit.iris.util.collection.KMap;
|
||||||
import com.volmit.iris.util.exceptions.IrisException;
|
import com.volmit.iris.util.exceptions.IrisException;
|
||||||
import com.volmit.iris.util.format.C;
|
import com.volmit.iris.util.format.C;
|
||||||
import com.volmit.iris.util.format.Form;
|
|
||||||
import com.volmit.iris.util.function.NastyRunnable;
|
import com.volmit.iris.util.function.NastyRunnable;
|
||||||
import com.volmit.iris.util.io.FileWatcher;
|
import com.volmit.iris.util.io.FileWatcher;
|
||||||
import com.volmit.iris.util.io.IO;
|
import com.volmit.iris.util.io.IO;
|
||||||
@@ -50,32 +50,23 @@ import com.volmit.iris.util.io.InstanceState;
|
|||||||
import com.volmit.iris.util.io.JarScanner;
|
import com.volmit.iris.util.io.JarScanner;
|
||||||
import com.volmit.iris.util.math.M;
|
import com.volmit.iris.util.math.M;
|
||||||
import com.volmit.iris.util.math.RNG;
|
import com.volmit.iris.util.math.RNG;
|
||||||
|
import com.volmit.iris.util.misc.Bindings;
|
||||||
|
import com.volmit.iris.util.misc.SlimJar;
|
||||||
import com.volmit.iris.util.parallel.MultiBurst;
|
import com.volmit.iris.util.parallel.MultiBurst;
|
||||||
import com.volmit.iris.util.plugin.IrisService;
|
import com.volmit.iris.util.plugin.IrisService;
|
||||||
import com.volmit.iris.util.plugin.Metrics;
|
|
||||||
import com.volmit.iris.util.plugin.VolmitPlugin;
|
import com.volmit.iris.util.plugin.VolmitPlugin;
|
||||||
import com.volmit.iris.util.plugin.VolmitSender;
|
import com.volmit.iris.util.plugin.VolmitSender;
|
||||||
import com.volmit.iris.util.reflect.ShadeFix;
|
import com.volmit.iris.util.plugin.chunk.ChunkTickets;
|
||||||
import com.volmit.iris.util.scheduling.J;
|
import com.volmit.iris.util.scheduling.J;
|
||||||
import com.volmit.iris.util.scheduling.Queue;
|
import com.volmit.iris.util.scheduling.Queue;
|
||||||
import com.volmit.iris.util.scheduling.ShurikenQueue;
|
import com.volmit.iris.util.scheduling.ShurikenQueue;
|
||||||
import io.papermc.lib.PaperLib;
|
import lombok.NonNull;
|
||||||
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
|
import org.bukkit.*;
|
||||||
import net.kyori.adventure.text.serializer.ComponentSerializer;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.GameMode;
|
|
||||||
import org.bukkit.Location;
|
|
||||||
import org.bukkit.WorldCreator;
|
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
import org.bukkit.command.Command;
|
import org.bukkit.command.Command;
|
||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
import org.bukkit.configuration.ConfigurationSection;
|
|
||||||
import org.bukkit.configuration.file.FileConfiguration;
|
|
||||||
import org.bukkit.configuration.file.YamlConfiguration;
|
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.Event;
|
import org.bukkit.event.*;
|
||||||
import org.bukkit.event.HandlerList;
|
|
||||||
import org.bukkit.event.Listener;
|
|
||||||
import org.bukkit.generator.BiomeProvider;
|
import org.bukkit.generator.BiomeProvider;
|
||||||
import org.bukkit.generator.ChunkGenerator;
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
import org.bukkit.plugin.IllegalPluginAccessException;
|
import org.bukkit.plugin.IllegalPluginAccessException;
|
||||||
@@ -86,26 +77,27 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Date;
|
import java.util.*;
|
||||||
import java.util.Map;
|
import java.util.function.Predicate;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@SuppressWarnings("CanBeFinal")
|
@SuppressWarnings("CanBeFinal")
|
||||||
public class Iris extends VolmitPlugin implements Listener {
|
public class Iris extends VolmitPlugin implements Listener {
|
||||||
public static final String OVERWORLD_TAG = "3003";
|
|
||||||
|
|
||||||
private static final Queue<Runnable> syncJobs = new ShurikenQueue<>();
|
private static final Queue<Runnable> syncJobs = new ShurikenQueue<>();
|
||||||
|
|
||||||
public static Iris instance;
|
public static Iris instance;
|
||||||
public static BukkitAudiences audiences;
|
|
||||||
public static MultiverseCoreLink linkMultiverseCore;
|
public static MultiverseCoreLink linkMultiverseCore;
|
||||||
public static MythicMobsLink linkMythicMobs;
|
|
||||||
public static IrisCompat compat;
|
public static IrisCompat compat;
|
||||||
public static FileWatcher configWatcher;
|
public static FileWatcher configWatcher;
|
||||||
|
public static ChunkTickets tickets;
|
||||||
private static VolmitSender sender;
|
private static VolmitSender sender;
|
||||||
|
private static Thread shutdownHook;
|
||||||
|
private static final String STUDIO_WORLDS_FOLDER = "iris";
|
||||||
|
private static final String NMS_BINDINGS_PACKAGE = "com.volmit.iris.core.nms.v";
|
||||||
|
|
||||||
static {
|
static {
|
||||||
try {
|
try {
|
||||||
fixShading();
|
|
||||||
InstanceState.updateInstanceId();
|
InstanceState.updateInstanceId();
|
||||||
} catch (Throwable ignored) {
|
} catch (Throwable ignored) {
|
||||||
|
|
||||||
@@ -116,6 +108,10 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
private KMap<Class<? extends IrisService>, IrisService> services;
|
private KMap<Class<? extends IrisService>, IrisService> services;
|
||||||
|
|
||||||
public static VolmitSender getSender() {
|
public static VolmitSender getSender() {
|
||||||
|
if (sender == null) {
|
||||||
|
sender = new VolmitSender(Bukkit.getConsoleSender());
|
||||||
|
sender.setTag(instance.getTag());
|
||||||
|
}
|
||||||
return sender;
|
return sender;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,10 +178,10 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
|
|
||||||
public static void msg(String string) {
|
public static void msg(String string) {
|
||||||
try {
|
try {
|
||||||
sender.sendMessage(string);
|
getSender().sendMessage(string);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
try {
|
try {
|
||||||
System.out.println(instance.getTag() + string.replaceAll("(<([^>]+)>)", ""));
|
instance.getLogger().info(instance.getTag() + string.replaceAll("(<([^>]+)>)", ""));
|
||||||
} catch (Throwable ignored1) {
|
} catch (Throwable ignored1) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -336,7 +332,7 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getJavaVersion() {
|
public static int getJavaVersion() {
|
||||||
String version = System.getProperty("java.version");
|
String version = System.getProperty("java.version");
|
||||||
if (version.startsWith("1.")) {
|
if (version.startsWith("1.")) {
|
||||||
version = version.substring(2, 3);
|
version = version.substring(2, 3);
|
||||||
@@ -349,6 +345,13 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
return Integer.parseInt(version);
|
return Integer.parseInt(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getJava() {
|
||||||
|
String javaRuntimeName = System.getProperty("java.vm.name");
|
||||||
|
String javaRuntimeVendor = System.getProperty("java.vendor");
|
||||||
|
String javaRuntimeVersion = System.getProperty("java.vm.version");
|
||||||
|
return String.format("%s %s (build %s)", javaRuntimeName, javaRuntimeVendor, javaRuntimeVersion);
|
||||||
|
}
|
||||||
|
|
||||||
public static void reportErrorChunk(int x, int z, Throwable e, String extra) {
|
public static void reportErrorChunk(int x, int z, Throwable e, String extra) {
|
||||||
if (IrisSettings.get().getGeneral().isDebug()) {
|
if (IrisSettings.get().getGeneral().isDebug()) {
|
||||||
File f = instance.getDataFile("debug", "chunk-errors", "chunk." + x + "." + z + ".txt");
|
File f = instance.getDataFile("debug", "chunk-errors", "chunk." + x + "." + z + ".txt");
|
||||||
@@ -368,6 +371,7 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void reportError(Throwable e) {
|
public static void reportError(Throwable e) {
|
||||||
|
Bindings.capture(e);
|
||||||
if (IrisSettings.get().getGeneral().isDebug()) {
|
if (IrisSettings.get().getGeneral().isDebug()) {
|
||||||
String n = e.getClass().getCanonicalName() + "-" + e.getStackTrace()[0].getClassName() + "-" + e.getStackTrace()[0].getLineNumber();
|
String n = e.getClass().getCanonicalName() + "-" + e.getStackTrace()[0].getClassName() + "-" + e.getStackTrace()[0].getLineNumber();
|
||||||
|
|
||||||
@@ -409,9 +413,10 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
pw.println();
|
pw.println();
|
||||||
pw.println();
|
pw.println();
|
||||||
}
|
}
|
||||||
|
pw.println("[%%__USER__%%,%%__RESOURCE__%%,%%__PRODUCT__%%,%%__BUILTBYBIT__%%]");
|
||||||
|
|
||||||
pw.close();
|
pw.close();
|
||||||
System.out.println("DUMPED! See " + fi.getAbsolutePath());
|
Iris.info("DUMPED! See " + fi.getAbsolutePath());
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
@@ -425,28 +430,29 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
EnginePanic.add(s, v);
|
EnginePanic.add(s, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void fixShading() {
|
public Iris() {
|
||||||
ShadeFix.fix(ComponentSerializer.class);
|
instance = this;
|
||||||
|
SlimJar.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enable() {
|
private void enable() {
|
||||||
instance = this;
|
|
||||||
services = new KMap<>();
|
services = new KMap<>();
|
||||||
|
Bindings.setupSentry();
|
||||||
initialize("com.volmit.iris.core.service").forEach((i) -> services.put((Class<? extends IrisService>) i.getClass(), (IrisService) i));
|
initialize("com.volmit.iris.core.service").forEach((i) -> services.put((Class<? extends IrisService>) i.getClass(), (IrisService) i));
|
||||||
INMS.get();
|
IO.delete(WorldStorage.get().worldFolder(STUDIO_WORLDS_FOLDER));
|
||||||
IO.delete(new File("iris"));
|
|
||||||
setupAudience();
|
|
||||||
sender = new VolmitSender(Bukkit.getConsoleSender());
|
|
||||||
sender.setTag(getTag());
|
|
||||||
instance = this;
|
|
||||||
compat = IrisCompat.configured(getDataFile("compat.json"));
|
compat = IrisCompat.configured(getDataFile("compat.json"));
|
||||||
|
adoptLegacyWorldData();
|
||||||
|
ServerConfigurator.configure();
|
||||||
|
IrisSafeguard.execute();
|
||||||
|
getSender().setTag(getTag());
|
||||||
|
IrisSafeguard.splash();
|
||||||
|
tickets = new ChunkTickets();
|
||||||
linkMultiverseCore = new MultiverseCoreLink();
|
linkMultiverseCore = new MultiverseCoreLink();
|
||||||
linkMythicMobs = new MythicMobsLink();
|
|
||||||
configWatcher = new FileWatcher(getDataFile("settings.json"));
|
configWatcher = new FileWatcher(getDataFile("settings.json"));
|
||||||
services.values().forEach(IrisService::onEnable);
|
services.values().forEach(IrisService::onEnable);
|
||||||
services.values().forEach(this::registerListener);
|
services.values().forEach(this::registerListener);
|
||||||
|
addShutdownHook();
|
||||||
J.s(() -> {
|
J.s(() -> {
|
||||||
J.a(() -> PaperLib.suggestPaper(this));
|
|
||||||
J.a(() -> IO.delete(getTemp()));
|
J.a(() -> IO.delete(getTemp()));
|
||||||
J.a(LazyPregenerator::loadLazyGenerators, 100);
|
J.a(LazyPregenerator::loadLazyGenerators, 100);
|
||||||
J.a(this::bstats);
|
J.a(this::bstats);
|
||||||
@@ -454,53 +460,70 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
J.sr(this::tickQueue, 0);
|
J.sr(this::tickQueue, 0);
|
||||||
J.s(this::setupPapi);
|
J.s(this::setupPapi);
|
||||||
J.a(ServerConfigurator::configure, 20);
|
J.a(ServerConfigurator::configure, 20);
|
||||||
splash();
|
|
||||||
autoStartStudio();
|
autoStartStudio();
|
||||||
checkForBukkitWorlds();
|
checkForBukkitWorlds(s -> true);
|
||||||
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
|
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
|
||||||
IrisToolbelt.retainMantleDataForSlice(BlockData.class.getCanonicalName());
|
IrisToolbelt.retainMantleDataForSlice(BlockData.class.getCanonicalName());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkForBukkitWorlds() {
|
private void adoptLegacyWorldData() {
|
||||||
FileConfiguration fc = new YamlConfiguration();
|
WorldStorage storage = WorldStorage.get();
|
||||||
|
for (String worldName : IrisWorlds.readKnownWorldNames()) {
|
||||||
|
try {
|
||||||
|
storage.adoptLegacyData(worldName);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.error("Failed to move the Iris data of " + worldName + " to its new world folder!");
|
||||||
|
Iris.reportError(e);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addShutdownHook() {
|
||||||
|
if (shutdownHook != null) {
|
||||||
|
Runtime.getRuntime().removeShutdownHook(shutdownHook);
|
||||||
|
}
|
||||||
|
shutdownHook = new Thread(() -> {
|
||||||
|
Bukkit.getWorlds()
|
||||||
|
.stream()
|
||||||
|
.map(IrisToolbelt::access)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.forEach(PlatformChunkGenerator::close);
|
||||||
|
|
||||||
|
MultiBurst.burst.close();
|
||||||
|
MultiBurst.ioBurst.close();
|
||||||
|
services.clear();
|
||||||
|
});
|
||||||
|
Runtime.getRuntime().addShutdownHook(shutdownHook);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkForBukkitWorlds(Predicate<String> filter) {
|
||||||
try {
|
try {
|
||||||
fc.load(new File("bukkit.yml"));
|
IrisWorlds.readBukkitWorlds().forEach((s, generator) -> {
|
||||||
ConfigurationSection section = fc.getConfigurationSection("worlds");
|
try {
|
||||||
if (section == null) {
|
if (Bukkit.getWorld(s) != null || !filter.test(s)) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String s : section.getKeys(false)) {
|
Iris.info("Loading World: %s | Generator: %s", s, generator);
|
||||||
ConfigurationSection entry = section.getConfigurationSection(s);
|
var gen = getDefaultWorldGenerator(s, generator);
|
||||||
if (!entry.contains("generator", true)) {
|
var dim = loadDimension(s, generator);
|
||||||
continue;
|
assert dim != null && gen != null;
|
||||||
|
|
||||||
|
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "...");
|
||||||
|
WorldCreator c = new WorldCreator(s)
|
||||||
|
.generator(gen)
|
||||||
|
.environment(dim.getEnvironment());
|
||||||
|
INMS.get().createWorld(c);
|
||||||
|
Iris.info(C.LIGHT_PURPLE + "Loaded " + s + "!");
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.error("Failed to load world " + s + "!");
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
String generator = entry.getString("generator");
|
|
||||||
if (generator.startsWith("Iris:")) {
|
|
||||||
generator = generator.split("\\Q:\\E")[1];
|
|
||||||
} else if (generator.equalsIgnoreCase("Iris")) {
|
|
||||||
generator = IrisSettings.get().getGenerator().getDefaultWorldType();
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Iris.info("2 World: %s | Generator: %s", s, generator);
|
|
||||||
|
|
||||||
if (Bukkit.getWorlds().stream().anyMatch(w -> w.getName().equals(s))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "...");
|
|
||||||
new WorldCreator(s)
|
|
||||||
.generator(getDefaultWorldGenerator(s, generator))
|
|
||||||
.environment(IrisData.loadAnyDimension(generator).getEnvironment())
|
|
||||||
.createWorld();
|
|
||||||
Iris.info(C.LIGHT_PURPLE + "Loaded " + s + "!");
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
reportError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,50 +532,62 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
Iris.info("Starting up auto Studio!");
|
Iris.info("Starting up auto Studio!");
|
||||||
try {
|
try {
|
||||||
Player r = new KList<>(getServer().getOnlinePlayers()).getRandom();
|
Player r = new KList<>(getServer().getOnlinePlayers()).getRandom();
|
||||||
Iris.service(StudioSVC.class).open(r != null ? new VolmitSender(r) : sender, 1337, IrisSettings.get().getGenerator().getDefaultWorldType(), (w) -> {
|
Iris.service(StudioSVC.class).open(r != null ? new VolmitSender(r) : getSender(), 1337, IrisSettings.get().getGenerator().getDefaultWorldType(), (w) -> {
|
||||||
J.s(() -> {
|
J.s(() -> {
|
||||||
|
var spawn = w.getSpawnLocation();
|
||||||
for (Player i : getServer().getOnlinePlayers()) {
|
for (Player i : getServer().getOnlinePlayers()) {
|
||||||
i.setGameMode(GameMode.SPECTATOR);
|
i.setGameMode(GameMode.SPECTATOR);
|
||||||
i.teleport(new Location(w, 0, 200, 0));
|
i.teleport(spawn);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (IrisException e) {
|
} catch (IrisException e) {
|
||||||
e.printStackTrace();
|
reportError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupAudience() {
|
|
||||||
try {
|
|
||||||
audiences = BukkitAudiences.create(this);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
IrisSettings.get().getGeneral().setUseConsoleCustomColors(false);
|
|
||||||
IrisSettings.get().getGeneral().setUseCustomColorsIngame(false);
|
|
||||||
Iris.error("Failed to setup Adventure API... No custom colors :(");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void postShutdown(Runnable r) {
|
public void postShutdown(Runnable r) {
|
||||||
postShutdown.add(r);
|
postShutdown.add(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
|
if (!isPaperServer()) {
|
||||||
|
getLogger().severe("Iris requires Paper or a Paper fork (Purpur, Pufferfish, ...). Spigot and CraftBukkit are not supported.");
|
||||||
|
Bukkit.getPluginManager().disablePlugin(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
enable();
|
enable();
|
||||||
super.onEnable();
|
super.onEnable();
|
||||||
Bukkit.getPluginManager().registerEvents(this, this);
|
Bukkit.getPluginManager().registerEvents(this, this);
|
||||||
setupChecks();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
|
if (services == null || IrisSafeguard.isForceShutdown()) return;
|
||||||
services.values().forEach(IrisService::onDisable);
|
services.values().forEach(IrisService::onDisable);
|
||||||
Bukkit.getScheduler().cancelTasks(this);
|
Bukkit.getScheduler().cancelTasks(this);
|
||||||
HandlerList.unregisterAll((Plugin) this);
|
HandlerList.unregisterAll((Plugin) this);
|
||||||
postShutdown.forEach(Runnable::run);
|
postShutdown.forEach(Runnable::run);
|
||||||
services.clear();
|
|
||||||
MultiBurst.burst.close();
|
|
||||||
super.onDisable();
|
super.onDisable();
|
||||||
|
|
||||||
|
J.attempt(() -> new JarScanner(instance.getJarFile(), "", false).scanAll(Iris::isForRunningServer));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isForRunningServer(String className) {
|
||||||
|
// Older servers log errors for the bindings compiled for a newer Java version.
|
||||||
|
if (!className.startsWith(NMS_BINDINGS_PACKAGE)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return className.startsWith(INMS.get().getClass().getPackageName() + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isPaperServer() {
|
||||||
|
try {
|
||||||
|
World.class.getMethod("getChunkAtAsync", int.class, int.class, boolean.class, boolean.class);
|
||||||
|
return true;
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupPapi() {
|
private void setupPapi() {
|
||||||
@@ -573,32 +608,7 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getTag(String subTag) {
|
public String getTag(String subTag) {
|
||||||
return C.BOLD + "" + C.DARK_GRAY + "[" + C.BOLD + "" + C.IRIS + "Iris" + C.BOLD + C.DARK_GRAY + "]" + C.RESET + "" + C.GRAY + ": ";
|
return IrisSafeguard.mode().tag(subTag);
|
||||||
}
|
|
||||||
|
|
||||||
private boolean setupChecks() {
|
|
||||||
boolean passed = true;
|
|
||||||
Iris.info("Version Information: " + instance.getServer().getVersion() + " | " + instance.getServer().getBukkitVersion());
|
|
||||||
if (!instance.getServer().getBukkitVersion().contains(NMSBinding1_20.NMS_VERSION)) {
|
|
||||||
passed = false;
|
|
||||||
Iris.warn("============================================");
|
|
||||||
Iris.warn("=");
|
|
||||||
Iris.warn("=");
|
|
||||||
Iris.warn("=");
|
|
||||||
Iris.warn("Iris is not compatible with this version of Minecraft.\nPlease use " + NMSBinding1_20.NMS_VERSION + " or use an older version of Iris.");
|
|
||||||
Iris.warn("=");
|
|
||||||
Iris.warn("=");
|
|
||||||
Iris.warn("=");
|
|
||||||
Iris.warn("============================================");
|
|
||||||
}
|
|
||||||
if (!instance.getServer().getVersion().contains("Purpur")) {
|
|
||||||
passed = false;
|
|
||||||
Iris.info("We recommend using Purpur for the best experience with Iris.");
|
|
||||||
Iris.info("Purpur is a fork of Paper that is optimized for performance and stability.");
|
|
||||||
Iris.info("Plugins that work on Spigot / Paper work on Purpur.");
|
|
||||||
Iris.info("You can download it here: https://purpurmc.org");
|
|
||||||
}
|
|
||||||
return passed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkConfigHotload() {
|
private void checkConfigHotload() {
|
||||||
@@ -631,7 +641,7 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
|
|
||||||
private void bstats() {
|
private void bstats() {
|
||||||
if (IrisSettings.get().getGeneral().isPluginMetrics()) {
|
if (IrisSettings.get().getGeneral().isPluginMetrics()) {
|
||||||
J.s(() -> new Metrics(Iris.instance, 8757));
|
Bindings.setupBstats(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,37 +664,11 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
@Override
|
@Override
|
||||||
public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
|
public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
|
||||||
Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id);
|
Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id);
|
||||||
if (worldName.equals("test")) {
|
if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType();
|
||||||
try {
|
|
||||||
throw new RuntimeException();
|
|
||||||
} catch (Throwable e) {
|
|
||||||
Iris.info(e.getStackTrace()[1].getClassName());
|
|
||||||
if (e.getStackTrace()[1].getClassName().contains("com.onarandombox.MultiverseCore")) {
|
|
||||||
Iris.debug("MVC Test detected, Quick! Send them the dummy!");
|
|
||||||
return new DummyChunkGenerator();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IrisDimension dim;
|
|
||||||
if (id == null || id.isEmpty()) {
|
|
||||||
dim = IrisData.loadAnyDimension(IrisSettings.get().getGenerator().getDefaultWorldType());
|
|
||||||
} else {
|
|
||||||
dim = IrisData.loadAnyDimension(id);
|
|
||||||
}
|
|
||||||
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
|
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
|
||||||
|
IrisDimension dim = loadDimension(worldName, id);
|
||||||
if (dim == null) {
|
if (dim == null) {
|
||||||
Iris.warn("Unable to find dimension type " + id + " Looking for online packs...");
|
throw new RuntimeException("Can't find dimension " + id + "!");
|
||||||
|
|
||||||
service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, true);
|
|
||||||
dim = IrisData.loadAnyDimension(id);
|
|
||||||
|
|
||||||
if (dim == null) {
|
|
||||||
throw new RuntimeException("Can't find dimension " + id + "!");
|
|
||||||
} else {
|
|
||||||
Iris.info("Resolved missing dimension, proceeding with generation.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Iris.debug("Assuming IrisDimension: " + dim.getName());
|
Iris.debug("Assuming IrisDimension: " + dim.getName());
|
||||||
@@ -693,7 +677,7 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
.name(worldName)
|
.name(worldName)
|
||||||
.seed(1337)
|
.seed(1337)
|
||||||
.environment(dim.getEnvironment())
|
.environment(dim.getEnvironment())
|
||||||
.worldFolder(new File(Bukkit.getWorldContainer(), worldName))
|
.worldFolder(WorldStorage.get().worldFolder(worldName))
|
||||||
.minHeight(dim.getMinHeight())
|
.minHeight(dim.getMinHeight())
|
||||||
.maxHeight(dim.getMaxHeight())
|
.maxHeight(dim.getMaxHeight())
|
||||||
.build();
|
.build();
|
||||||
@@ -701,50 +685,42 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
Iris.debug("Generator Config: " + w.toString());
|
Iris.debug("Generator Config: " + w.toString());
|
||||||
|
|
||||||
File ff = new File(w.worldFolder(), "iris/pack");
|
File ff = new File(w.worldFolder(), "iris/pack");
|
||||||
if (!ff.exists() || ff.listFiles().length == 0) {
|
var files = ff.listFiles();
|
||||||
|
if (files == null || files.length == 0)
|
||||||
|
IO.delete(ff);
|
||||||
|
|
||||||
|
if (!ff.exists()) {
|
||||||
ff.mkdirs();
|
ff.mkdirs();
|
||||||
service(StudioSVC.class).installIntoWorld(sender, dim.getLoadKey(), ff.getParentFile());
|
service(StudioSVC.class).installIntoWorld(getSender(), dim.getLoadKey(), w.worldFolder());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey());
|
return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void splash() {
|
@Nullable
|
||||||
if (!IrisSettings.get().getGeneral().isSplashLogoStartup()) {
|
public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) {
|
||||||
return;
|
File pack = new File(WorldStorage.get().worldFolder(worldName), "iris/pack");
|
||||||
|
var dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
|
||||||
|
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
|
||||||
|
if (dimension == null) {
|
||||||
|
Iris.warn("Unable to find dimension type " + id + " Looking for online packs...");
|
||||||
|
Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, false);
|
||||||
|
dimension = IrisData.loadAnyDimension(id, null);
|
||||||
|
|
||||||
|
if (dimension != null) {
|
||||||
|
Iris.info("Resolved missing dimension, proceeding.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @NoArgsConstructor
|
return dimension;
|
||||||
String padd = Form.repeat(" ", 8);
|
}
|
||||||
String padd2 = Form.repeat(" ", 4);
|
|
||||||
String[] info = {"", "", "", "", "", padd2 + C.IRIS + " Iris", padd2 + C.GRAY + " by " + "<rainbow>Volmit Software", padd2 + C.GRAY + " v" + C.IRIS + getDescription().getVersion(),
|
public void splash() {
|
||||||
};
|
Iris.info("Server type & version: " + Bukkit.getName() + " v" + Bukkit.getVersion());
|
||||||
String[] splash = {
|
|
||||||
padd + C.GRAY + " @@@@@@@@@@@@@@" + C.DARK_GRAY + "@@@",
|
|
||||||
padd + C.GRAY + " @@&&&&&&&&&" + C.DARK_GRAY + "&&&&&&" + C.IRIS + " .(((()))). ",
|
|
||||||
padd + C.GRAY + "@@@&&&&&&&&" + C.DARK_GRAY + "&&&&&" + C.IRIS + " .((((((())))))). ",
|
|
||||||
padd + C.GRAY + "@@@&&&&&" + C.DARK_GRAY + "&&&&&&&" + C.IRIS + " ((((((((())))))))) " + C.GRAY + " @",
|
|
||||||
padd + C.GRAY + "@@@&&&&" + C.DARK_GRAY + "@@@@@&" + C.IRIS + " ((((((((-))))))))) " + C.GRAY + " @@",
|
|
||||||
padd + C.GRAY + "@@@&&" + C.IRIS + " ((((((({ })))))))) " + C.GRAY + " &&@@@",
|
|
||||||
padd + C.GRAY + "@@" + C.IRIS + " ((((((((-))))))))) " + C.DARK_GRAY + "&@@@@@" + C.GRAY + "&&&&@@@",
|
|
||||||
padd + C.GRAY + "@" + C.IRIS + " ((((((((())))))))) " + C.DARK_GRAY + "&&&&&" + C.GRAY + "&&&&&&&@@@",
|
|
||||||
padd + C.GRAY + "" + C.IRIS + " '((((((()))))))' " + C.DARK_GRAY + "&&&&&" + C.GRAY + "&&&&&&&&@@@",
|
|
||||||
padd + C.GRAY + "" + C.IRIS + " '(((())))' " + C.DARK_GRAY + "&&&&&&&&" + C.GRAY + "&&&&&&&@@",
|
|
||||||
padd + C.GRAY + " " + C.DARK_GRAY + "@@@" + C.GRAY + "@@@@@@@@@@@@@@"
|
|
||||||
};
|
|
||||||
//@done
|
|
||||||
Iris.info("Server type & version: " + Bukkit.getVersion());
|
|
||||||
Iris.info("Bukkit version: " + Bukkit.getBukkitVersion());
|
|
||||||
Iris.info("Java version: " + getJavaVersion());
|
|
||||||
Iris.info("Custom Biomes: " + INMS.get().countCustomBiomes());
|
Iris.info("Custom Biomes: " + INMS.get().countCustomBiomes());
|
||||||
setupChecks();
|
|
||||||
printPacks();
|
printPacks();
|
||||||
|
|
||||||
for (int i = 0; i < info.length; i++) {
|
IrisSafeguard.mode().trySplash();
|
||||||
splash[i] += info[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
Iris.info("\n\n " + new KList<>(splash).toString("\n") + "\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void printPacks() {
|
private void printPacks() {
|
||||||
@@ -768,4 +744,33 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
}
|
}
|
||||||
Iris.info(" " + dimName + " v" + version);
|
Iris.info(" " + dimName + " v" + version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getIrisVersion() {
|
||||||
|
String input = Iris.instance.getDescription().getVersion();
|
||||||
|
int hyphenIndex = input.indexOf('-');
|
||||||
|
if (hyphenIndex != -1) {
|
||||||
|
String result = input.substring(0, hyphenIndex);
|
||||||
|
result = result.replaceAll("\\.", "");
|
||||||
|
return Integer.parseInt(result);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMCVersion() {
|
||||||
|
try {
|
||||||
|
String version = Bukkit.getVersion();
|
||||||
|
Matcher matcher = Pattern.compile("\\(MC: ([\\d.]+)\\)").matcher(version);
|
||||||
|
if (matcher.find()) {
|
||||||
|
version = matcher.group(1).replaceAll("\\.", "");
|
||||||
|
long versionNumber = Long.parseLong(version);
|
||||||
|
if (versionNumber > Integer.MAX_VALUE) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return (int) versionNumber;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+117
-4
@@ -23,8 +23,11 @@ import com.volmit.iris.Iris;
|
|||||||
import com.volmit.iris.util.io.IO;
|
import com.volmit.iris.util.io.IO;
|
||||||
import com.volmit.iris.util.json.JSONException;
|
import com.volmit.iris.util.json.JSONException;
|
||||||
import com.volmit.iris.util.json.JSONObject;
|
import com.volmit.iris.util.json.JSONObject;
|
||||||
|
import com.volmit.iris.util.misc.getHardware;
|
||||||
import com.volmit.iris.util.plugin.VolmitSender;
|
import com.volmit.iris.util.plugin.VolmitSender;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -41,13 +44,15 @@ public class IrisSettings {
|
|||||||
private IrisSettingsConcurrency concurrency = new IrisSettingsConcurrency();
|
private IrisSettingsConcurrency concurrency = new IrisSettingsConcurrency();
|
||||||
private IrisSettingsStudio studio = new IrisSettingsStudio();
|
private IrisSettingsStudio studio = new IrisSettingsStudio();
|
||||||
private IrisSettingsPerformance performance = new IrisSettingsPerformance();
|
private IrisSettingsPerformance performance = new IrisSettingsPerformance();
|
||||||
|
private IrisSettingsUpdater updater = new IrisSettingsUpdater();
|
||||||
|
private IrisSettingsPregen pregen = new IrisSettingsPregen();
|
||||||
|
private IrisSettingsSentry sentry = new IrisSettingsSentry();
|
||||||
|
|
||||||
public static int getThreadCount(int c) {
|
public static int getThreadCount(int c) {
|
||||||
return switch (c) {
|
return Math.max(switch (c) {
|
||||||
case -1, -2, -4 -> Runtime.getRuntime().availableProcessors() / -c;
|
case -1, -2, -4 -> Runtime.getRuntime().availableProcessors() / -c;
|
||||||
case 0, 1, 2 -> 1;
|
|
||||||
default -> Math.max(c, 2);
|
default -> Math.max(c, 2);
|
||||||
};
|
}, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IrisSettings get() {
|
public static IrisSettings get() {
|
||||||
@@ -126,32 +131,116 @@ public class IrisSettings {
|
|||||||
public boolean markerEntitySpawningSystem = true;
|
public boolean markerEntitySpawningSystem = true;
|
||||||
public boolean effectSystem = true;
|
public boolean effectSystem = true;
|
||||||
public boolean worldEditWandCUI = true;
|
public boolean worldEditWandCUI = true;
|
||||||
|
public boolean globalPregenCache = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class IrisSettingsConcurrency {
|
public static class IrisSettingsConcurrency {
|
||||||
public int parallelism = -1;
|
public int parallelism = -1;
|
||||||
|
public int ioParallelism = -2;
|
||||||
|
public int worldGenParallelism = -1;
|
||||||
|
|
||||||
|
public int getWorldGenThreads() {
|
||||||
|
return getThreadCount(worldGenParallelism);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class IrisSettingsPregen {
|
||||||
|
private static final int MIN_RESIDENT_TECTONIC_PLATES = 16;
|
||||||
|
private static final double MANTLE_HEAP_FRACTION = 0.6D;
|
||||||
|
private static final int MEASURED_PLATE_MEGABYTES = 38;
|
||||||
|
private static final int MEASURED_WORLD_HEIGHT = 1088;
|
||||||
|
|
||||||
|
public boolean useCacheByDefault = true;
|
||||||
|
public boolean useHighPriority = false;
|
||||||
|
public boolean useVirtualThreads = false;
|
||||||
|
public boolean useTicketQueue = true;
|
||||||
|
public int maxConcurrency = 256;
|
||||||
|
public int maxResidentTectonicPlates = 96;
|
||||||
|
public int mantleBackpressureWaitMs = 25;
|
||||||
|
public int mantleBackpressureTimeoutMs = 60_000;
|
||||||
|
|
||||||
|
public int getEffectiveResidentTectonicPlates(int worldHeight) {
|
||||||
|
// Mantle chunks only hold the sections something was written to, so shorter worlds keep the measured size.
|
||||||
|
double heightScale = Math.max(1D, (double) worldHeight / MEASURED_WORLD_HEIGHT);
|
||||||
|
double plateMegabytes = MEASURED_PLATE_MEGABYTES * heightScale;
|
||||||
|
int heapBudget = (int) Math.floor(MANTLE_HEAP_FRACTION * getHardware.getProcessMemory() / plateMegabytes);
|
||||||
|
return Math.max(MIN_RESIDENT_TECTONIC_PLATES, Math.min(maxResidentTectonicPlates, heapBudget));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMantleBackpressureWaitMs() {
|
||||||
|
return Math.max(5, Math.min(mantleBackpressureWaitMs, 1_000));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMantleBackpressureTimeoutMs() {
|
||||||
|
return Math.max(5_000, Math.min(mantleBackpressureTimeoutMs, 600_000));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class IrisSettingsPerformance {
|
public static class IrisSettingsPerformance {
|
||||||
|
private IrisSettingsEngineSVC engineSVC = new IrisSettingsEngineSVC();
|
||||||
public boolean trimMantleInStudio = false;
|
public boolean trimMantleInStudio = false;
|
||||||
public int mantleKeepAlive = 30;
|
public int mantleKeepAlive = 30;
|
||||||
public int cacheSize = 4_096;
|
public int noiseCacheSize = 1_024;
|
||||||
public int resourceLoaderCacheSize = 1_024;
|
public int resourceLoaderCacheSize = 1_024;
|
||||||
public int objectLoaderCacheSize = 4_096;
|
public int objectLoaderCacheSize = 4_096;
|
||||||
public int scriptLoaderCacheSize = 512;
|
public int scriptLoaderCacheSize = 512;
|
||||||
|
public int tectonicPlateSize = -1;
|
||||||
|
public int mantleCleanupDelay = 200;
|
||||||
|
|
||||||
|
public int getTectonicPlateSize() {
|
||||||
|
if (tectonicPlateSize > 0)
|
||||||
|
return tectonicPlateSize;
|
||||||
|
|
||||||
|
return (int) (getHardware.getProcessMemory() / 512L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class IrisSettingsUpdater {
|
||||||
|
public int maxConcurrency = 256;
|
||||||
|
public boolean nativeThreads = false;
|
||||||
|
public double threadMultiplier = 2;
|
||||||
|
|
||||||
|
public double chunkLoadSensitivity = 0.7;
|
||||||
|
public MsRange emptyMsRange = new MsRange(80, 100);
|
||||||
|
public MsRange defaultMsRange = new MsRange(20, 40);
|
||||||
|
|
||||||
|
public int getMaxConcurrency() {
|
||||||
|
return Math.max(Math.abs(maxConcurrency), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getThreadMultiplier() {
|
||||||
|
return Math.min(Math.abs(threadMultiplier), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getChunkLoadSensitivity() {
|
||||||
|
return Math.min(chunkLoadSensitivity, 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class MsRange {
|
||||||
|
public int min = 20;
|
||||||
|
public int max = 40;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class IrisSettingsGeneral {
|
public static class IrisSettingsGeneral {
|
||||||
|
public boolean DoomsdayAnnihilationSelfDestructMode = false;
|
||||||
public boolean commandSounds = true;
|
public boolean commandSounds = true;
|
||||||
public boolean debug = false;
|
public boolean debug = false;
|
||||||
|
public boolean dumpMantleOnError = false;
|
||||||
public boolean disableNMS = false;
|
public boolean disableNMS = false;
|
||||||
public boolean pluginMetrics = true;
|
public boolean pluginMetrics = true;
|
||||||
public boolean splashLogoStartup = true;
|
public boolean splashLogoStartup = true;
|
||||||
public boolean useConsoleCustomColors = true;
|
public boolean useConsoleCustomColors = true;
|
||||||
public boolean useCustomColorsIngame = true;
|
public boolean useCustomColorsIngame = true;
|
||||||
|
public boolean adjustVanillaHeight = false;
|
||||||
public String forceMainWorld = "";
|
public String forceMainWorld = "";
|
||||||
public int spinh = -20;
|
public int spinh = -20;
|
||||||
public int spins = 7;
|
public int spins = 7;
|
||||||
@@ -165,10 +254,19 @@ public class IrisSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class IrisSettingsSentry {
|
||||||
|
public boolean includeServerId = true;
|
||||||
|
public boolean disableAutoReporting = false;
|
||||||
|
public boolean debug = false;
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class IrisSettingsGUI {
|
public static class IrisSettingsGUI {
|
||||||
public boolean useServerLaunchedGuis = true;
|
public boolean useServerLaunchedGuis = true;
|
||||||
public boolean maximumPregenGuiFPS = false;
|
public boolean maximumPregenGuiFPS = false;
|
||||||
|
public boolean colorMode = true;
|
||||||
|
public String theme = "auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -176,6 +274,10 @@ public class IrisSettings {
|
|||||||
public String defaultWorldType = "overworld";
|
public String defaultWorldType = "overworld";
|
||||||
public int maxBiomeChildDepth = 4;
|
public int maxBiomeChildDepth = 4;
|
||||||
public boolean preventLeafDecay = true;
|
public boolean preventLeafDecay = true;
|
||||||
|
public boolean useMulticore = false;
|
||||||
|
public boolean useMulticoreMantle = false;
|
||||||
|
public boolean offsetNoiseTypes = false;
|
||||||
|
public boolean earlyCustomBlocks = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -185,4 +287,15 @@ public class IrisSettings {
|
|||||||
public boolean disableTimeAndWeather = true;
|
public boolean disableTimeAndWeather = true;
|
||||||
public boolean autoStartDefaultStudio = false;
|
public boolean autoStartDefaultStudio = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class IrisSettingsEngineSVC {
|
||||||
|
public boolean useVirtualThreads = true;
|
||||||
|
public boolean forceMulticoreWrite = false;
|
||||||
|
public int priority = Thread.NORM_PRIORITY;
|
||||||
|
|
||||||
|
public int getPriority() {
|
||||||
|
return Math.max(Math.min(priority, Thread.MAX_PRIORITY), Thread.MIN_PRIORITY);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package com.volmit.iris.core;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import com.volmit.iris.Iris;
|
||||||
|
import com.volmit.iris.core.loader.IrisData;
|
||||||
|
import com.volmit.iris.core.world.WorldStorage;
|
||||||
|
import com.volmit.iris.engine.data.cache.AtomicCache;
|
||||||
|
import com.volmit.iris.engine.object.IrisDimension;
|
||||||
|
import com.volmit.iris.util.collection.KMap;
|
||||||
|
import com.volmit.iris.util.io.IO;
|
||||||
|
import com.volmit.iris.util.misc.ServerProperties;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class IrisWorlds {
|
||||||
|
private static final AtomicCache<IrisWorlds> cache = new AtomicCache<>();
|
||||||
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||||
|
private static final Type TYPE = TypeToken.getParameterized(KMap.class, String.class, String.class).getType();
|
||||||
|
private final KMap<String, String> worlds;
|
||||||
|
private volatile boolean dirty = false;
|
||||||
|
|
||||||
|
private IrisWorlds(KMap<String, String> worlds) {
|
||||||
|
this.worlds = worlds;
|
||||||
|
readBukkitWorlds().forEach(this::put0);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IrisWorlds get() {
|
||||||
|
return cache.aquire(() -> new IrisWorlds(readWorldsFile()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<String> readKnownWorldNames() {
|
||||||
|
Set<String> names = new HashSet<>(readWorldsFile().keySet());
|
||||||
|
names.addAll(readBukkitWorlds().keySet());
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KMap<String, String> readWorldsFile() {
|
||||||
|
File file = Iris.instance.getDataFile("worlds.json");
|
||||||
|
if (!file.exists()) {
|
||||||
|
return new KMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String json = IO.readAll(file);
|
||||||
|
KMap<String, String> worlds = GSON.fromJson(json, TYPE);
|
||||||
|
return Objects.requireNonNullElseGet(worlds, KMap::new);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.error("Failed to load worlds.json!");
|
||||||
|
e.printStackTrace();
|
||||||
|
Iris.reportError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new KMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void put(String name, String type) {
|
||||||
|
put0(name, type);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void put0(String name, String type) {
|
||||||
|
String old = worlds.put(name, type);
|
||||||
|
if (!type.equals(old))
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KMap<String, String> getWorlds() {
|
||||||
|
clean();
|
||||||
|
return readBukkitWorlds().put(worlds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream<IrisData> getPacks() {
|
||||||
|
return getDimensions()
|
||||||
|
.map(IrisDimension::getLoader)
|
||||||
|
.filter(Objects::nonNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream<IrisDimension> getDimensions() {
|
||||||
|
return getWorlds()
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(entry -> Iris.loadDimension(entry.getKey(), entry.getValue()))
|
||||||
|
.filter(Objects::nonNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clean() {
|
||||||
|
dirty = worlds.entrySet().removeIf(entry -> !new File(WorldStorage.get().worldFolder(entry.getKey()), "iris/pack/dimensions/" + entry.getValue() + ".json").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void save() {
|
||||||
|
clean();
|
||||||
|
if (!dirty) return;
|
||||||
|
try {
|
||||||
|
IO.write(Iris.instance.getDataFile("worlds.json"), OutputStreamWriter::new, writer -> GSON.toJson(worlds, TYPE, writer));
|
||||||
|
dirty = false;
|
||||||
|
} catch (IOException e) {
|
||||||
|
Iris.error("Failed to save worlds.json!");
|
||||||
|
e.printStackTrace();
|
||||||
|
Iris.reportError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static KMap<String, String> readBukkitWorlds() {
|
||||||
|
var bukkit = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
|
||||||
|
var worlds = bukkit.getConfigurationSection("worlds");
|
||||||
|
if (worlds == null) return new KMap<>();
|
||||||
|
|
||||||
|
var result = new KMap<String, String>();
|
||||||
|
for (String world : worlds.getKeys(false)) {
|
||||||
|
var gen = worlds.getString(world + ".generator");
|
||||||
|
if (gen == null) continue;
|
||||||
|
|
||||||
|
String loadKey;
|
||||||
|
if (gen.equalsIgnoreCase("iris")) {
|
||||||
|
loadKey = IrisSettings.get().getGenerator().getDefaultWorldType();
|
||||||
|
} else if (gen.startsWith("Iris:")) {
|
||||||
|
loadKey = gen.substring(5);
|
||||||
|
} else continue;
|
||||||
|
|
||||||
|
result.put(world, loadKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core;
|
||||||
|
|
||||||
|
import com.volmit.iris.Iris;
|
||||||
|
import com.volmit.iris.core.loader.IrisData;
|
||||||
|
import com.volmit.iris.core.nms.INMS;
|
||||||
|
import com.volmit.iris.core.nms.datapack.DataVersion;
|
||||||
|
import com.volmit.iris.core.nms.datapack.IDataFixer;
|
||||||
|
import com.volmit.iris.core.world.WorldStorage;
|
||||||
|
import com.volmit.iris.engine.object.*;
|
||||||
|
import com.volmit.iris.util.collection.KList;
|
||||||
|
import com.volmit.iris.util.collection.KMap;
|
||||||
|
import com.volmit.iris.util.collection.KSet;
|
||||||
|
import com.volmit.iris.util.format.C;
|
||||||
|
import com.volmit.iris.util.misc.ServerProperties;
|
||||||
|
import com.volmit.iris.util.plugin.VolmitSender;
|
||||||
|
import com.volmit.iris.util.scheduling.J;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.configuration.InvalidConfigurationException;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicIntegerArray;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class ServerConfigurator {
|
||||||
|
public static void configure() {
|
||||||
|
IrisSettings.IrisSettingsAutoconfiguration s = IrisSettings.get().getAutoConfiguration();
|
||||||
|
if (s.isConfigureSpigotTimeoutTime()) {
|
||||||
|
J.attempt(ServerConfigurator::increaseKeepAliveSpigot);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.isConfigurePaperWatchdogDelay()) {
|
||||||
|
J.attempt(ServerConfigurator::increasePaperWatchdog);
|
||||||
|
}
|
||||||
|
|
||||||
|
installDataPacks(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void increaseKeepAliveSpigot() throws IOException, InvalidConfigurationException {
|
||||||
|
File spigotConfig = new File("spigot.yml");
|
||||||
|
FileConfiguration f = new YamlConfiguration();
|
||||||
|
f.load(spigotConfig);
|
||||||
|
long tt = f.getLong("settings.timeout-time");
|
||||||
|
|
||||||
|
long spigotTimeout = TimeUnit.MINUTES.toSeconds(5);
|
||||||
|
|
||||||
|
if (tt < spigotTimeout) {
|
||||||
|
Iris.warn("Updating spigot.yml timeout-time: " + tt + " -> " + spigotTimeout + " (5 minutes)");
|
||||||
|
Iris.warn("You can disable this change (autoconfigureServer) in Iris settings, then change back the value.");
|
||||||
|
f.set("settings.timeout-time", spigotTimeout);
|
||||||
|
f.save(spigotConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static void increasePaperWatchdog() throws IOException, InvalidConfigurationException {
|
||||||
|
File spigotConfig = new File("config/paper-global.yml");
|
||||||
|
FileConfiguration f = new YamlConfiguration();
|
||||||
|
f.load(spigotConfig);
|
||||||
|
long tt = f.getLong("watchdog.early-warning-delay");
|
||||||
|
|
||||||
|
long watchdog = TimeUnit.MINUTES.toMillis(3);
|
||||||
|
if (tt < watchdog) {
|
||||||
|
Iris.warn("Updating paper.yml watchdog early-warning-delay: " + tt + " -> " + watchdog + " (3 minutes)");
|
||||||
|
Iris.warn("You can disable this change (autoconfigureServer) in Iris settings, then change back the value.");
|
||||||
|
f.set("watchdog.early-warning-delay", watchdog);
|
||||||
|
f.save(spigotConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KList<File> getDatapacksFolder() {
|
||||||
|
if (!IrisSettings.get().getGeneral().forceMainWorld.isEmpty()) {
|
||||||
|
return new KList<File>().qadd(new File(Bukkit.getWorldContainer(), IrisSettings.get().getGeneral().forceMainWorld + "/datapacks"));
|
||||||
|
}
|
||||||
|
return WorldStorage.get().datapackFolders();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean installDataPacks(boolean fullInstall) {
|
||||||
|
return installDataPacks(DataVersion.getDefault(), fullInstall);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean installDataPacks(IDataFixer fixer, boolean fullInstall) {
|
||||||
|
if (fixer == null) {
|
||||||
|
Iris.error("Unable to install datapacks, fixer is null!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Iris.info("Checking Data Packs...");
|
||||||
|
DimensionHeight height = new DimensionHeight(fixer);
|
||||||
|
KList<File> folders = getDatapacksFolder();
|
||||||
|
KMap<String, KSet<String>> biomes = new KMap<>();
|
||||||
|
|
||||||
|
try (Stream<IrisData> stream = allPacks()) {
|
||||||
|
stream.flatMap(height::merge)
|
||||||
|
.parallel()
|
||||||
|
.forEach(dim -> {
|
||||||
|
Iris.verbose(" Checking Dimension " + dim.getLoadFile().getPath());
|
||||||
|
dim.installBiomes(fixer, dim::getLoader, folders, biomes.computeIfAbsent(dim.getLoadKey(), k -> new KSet<>()));
|
||||||
|
dim.installDimensionType(fixer, folders);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
IrisDimension.writeShared(folders, height);
|
||||||
|
Iris.info("Data Packs Setup!");
|
||||||
|
|
||||||
|
return fullInstall && verifyDataPacksPost(IrisSettings.get().getAutoConfiguration().isAutoRestartOnCustomBiomeInstall());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean verifyDataPacksPost(boolean allowRestarting) {
|
||||||
|
try (Stream<IrisData> stream = allPacks()) {
|
||||||
|
boolean bad = stream
|
||||||
|
.map(data -> {
|
||||||
|
Iris.verbose("Checking Pack: " + data.getDataFolder().getPath());
|
||||||
|
var loader = data.getDimensionLoader();
|
||||||
|
return loader.loadAll(loader.getPossibleKeys())
|
||||||
|
.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(ServerConfigurator::verifyDataPackInstalled)
|
||||||
|
.toList()
|
||||||
|
.contains(false);
|
||||||
|
})
|
||||||
|
.toList()
|
||||||
|
.contains(true);
|
||||||
|
if (!bad) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (allowRestarting) {
|
||||||
|
restart();
|
||||||
|
} else if (INMS.get().supportsDataPacks()) {
|
||||||
|
Iris.error("============================================================================");
|
||||||
|
Iris.error(C.ITALIC + "You need to restart your server to properly generate custom biomes.");
|
||||||
|
Iris.error(C.ITALIC + "By continuing, Iris will use backup biomes in place of the custom biomes.");
|
||||||
|
Iris.error("----------------------------------------------------------------------------");
|
||||||
|
Iris.error(C.UNDERLINE + "IT IS HIGHLY RECOMMENDED YOU RESTART THE SERVER BEFORE GENERATING!");
|
||||||
|
Iris.error("============================================================================");
|
||||||
|
|
||||||
|
for (Player i : Bukkit.getOnlinePlayers()) {
|
||||||
|
if (i.isOp() || i.hasPermission("iris.all")) {
|
||||||
|
VolmitSender sender = new VolmitSender(i, Iris.instance.getTag("WARNING"));
|
||||||
|
sender.sendMessage("There are some Iris Packs that have custom biomes in them");
|
||||||
|
sender.sendMessage("You need to restart your server to use these packs.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
J.sleep(3000);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void restart() {
|
||||||
|
J.s(() -> {
|
||||||
|
Iris.warn("New data pack entries have been installed in Iris! Restarting server!");
|
||||||
|
Iris.warn("This will only happen when your pack changes (updates/first time setup)");
|
||||||
|
Iris.warn("(You can disable this auto restart in iris settings)");
|
||||||
|
J.s(() -> {
|
||||||
|
Iris.warn("Looks like the restart command didn't work. Stopping the server instead!");
|
||||||
|
Bukkit.shutdown();
|
||||||
|
}, 100);
|
||||||
|
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "restart");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean verifyDataPackInstalled(IrisDimension dimension) {
|
||||||
|
KSet<String> keys = new KSet<>();
|
||||||
|
boolean warn = false;
|
||||||
|
|
||||||
|
for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) {
|
||||||
|
if (i.isCustom()) {
|
||||||
|
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||||
|
keys.add(dimension.getLoadKey() + ":" + j.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String key = worldFolderNameOf(dimension.getLoader());
|
||||||
|
if (key == null) key = dimension.getLoadKey();
|
||||||
|
else key += "/" + dimension.getLoadKey();
|
||||||
|
|
||||||
|
if (!INMS.get().supportsDataPacks()) {
|
||||||
|
if (!keys.isEmpty()) {
|
||||||
|
Iris.warn("===================================================================================");
|
||||||
|
Iris.warn("Pack " + key + " has " + keys.size() + " custom biome(s). ");
|
||||||
|
Iris.warn("Your server version does not yet support datapacks for iris.");
|
||||||
|
Iris.warn("The world will generate these biomes as backup biomes.");
|
||||||
|
Iris.warn("====================================================================================");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String i : keys) {
|
||||||
|
Object o = INMS.get().getCustomBiomeBaseFor(i);
|
||||||
|
|
||||||
|
if (o == null) {
|
||||||
|
Iris.warn("The Biome " + i + " is not registered on the server.");
|
||||||
|
warn = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (INMS.get().missingDimensionTypes(dimension.getDimensionTypeKey())) {
|
||||||
|
Iris.warn("The Dimension Type for " + dimension.getLoadFile() + " is not registered on the server.");
|
||||||
|
warn = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (warn) {
|
||||||
|
Iris.error("The Pack " + key + " is INCAPABLE of generating custom biomes");
|
||||||
|
Iris.error("If not done automatically, restart your server before generating with this pack!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return !warn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Stream<IrisData> allPacks() {
|
||||||
|
return Stream.concat(listFiles(Iris.instance.getDataFolder("packs"))
|
||||||
|
.filter(File::isDirectory)
|
||||||
|
.filter( base -> {
|
||||||
|
var content = new File(base, "dimensions").listFiles();
|
||||||
|
return content != null && content.length > 0;
|
||||||
|
})
|
||||||
|
.map(IrisData::get), IrisWorlds.get().getPacks());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static String worldFolderNameOf(IrisData data) {
|
||||||
|
File pack = data.getDataFolder();
|
||||||
|
File iris = pack.getParentFile();
|
||||||
|
if (iris == null || iris.getParentFile() == null || !pack.getName().equals("pack") || !iris.getName().equals("iris")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return iris.getParentFile().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
private static Stream<File> listFiles(File parent) {
|
||||||
|
if (!parent.isDirectory()) return Stream.empty();
|
||||||
|
return Files.walk(parent.toPath()).map(Path::toFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DimensionHeight {
|
||||||
|
private final IDataFixer fixer;
|
||||||
|
private final AtomicIntegerArray[] dimensions = new AtomicIntegerArray[3];
|
||||||
|
|
||||||
|
public DimensionHeight(IDataFixer fixer) {
|
||||||
|
this.fixer = fixer;
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
dimensions[i] = new AtomicIntegerArray(new int[]{
|
||||||
|
Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream<IrisDimension> merge(IrisData data) {
|
||||||
|
Iris.verbose("Checking Pack: " + data.getDataFolder().getPath());
|
||||||
|
var loader = data.getDimensionLoader();
|
||||||
|
return loader.loadAll(loader.getPossibleKeys())
|
||||||
|
.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.peek(this::merge);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void merge(IrisDimension dimension) {
|
||||||
|
AtomicIntegerArray array = dimensions[dimension.getBaseDimension().ordinal()];
|
||||||
|
array.updateAndGet(0, min -> Math.min(min, dimension.getMinHeight()));
|
||||||
|
array.updateAndGet(1, max -> Math.max(max, dimension.getMaxHeight()));
|
||||||
|
array.updateAndGet(2, logical -> Math.max(logical, dimension.getLogicalHeight()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] jsonStrings() {
|
||||||
|
var dims = IDataFixer.Dimension.values();
|
||||||
|
var arr = new String[3];
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
arr[i] = jsonString(dims[i]);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String jsonString(IDataFixer.Dimension dimension) {
|
||||||
|
var data = dimensions[dimension.ordinal()];
|
||||||
|
int minY = data.get(0);
|
||||||
|
int maxY = data.get(1);
|
||||||
|
int logicalHeight = data.get(2);
|
||||||
|
if (minY == Integer.MAX_VALUE || maxY == Integer.MIN_VALUE || Integer.MIN_VALUE == logicalHeight)
|
||||||
|
return null;
|
||||||
|
return fixer.createDimension(dimension, minY, maxY - minY, logicalHeight, null).toString(4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,580 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.commands;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.volmit.iris.Iris;
|
||||||
|
import com.volmit.iris.core.ServerConfigurator;
|
||||||
|
import com.volmit.iris.core.loader.IrisData;
|
||||||
|
import com.volmit.iris.core.nms.INMS;
|
||||||
|
import com.volmit.iris.core.nms.datapack.DataVersion;
|
||||||
|
import com.volmit.iris.core.service.IrisEngineSVC;
|
||||||
|
import com.volmit.iris.core.tools.IrisPackBenchmarking;
|
||||||
|
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||||
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
|
import com.volmit.iris.engine.object.IrisDimension;
|
||||||
|
import com.volmit.iris.engine.object.IrisPosition;
|
||||||
|
import com.volmit.iris.engine.object.annotations.Snippet;
|
||||||
|
import com.volmit.iris.util.collection.KMap;
|
||||||
|
import com.volmit.iris.util.collection.KSet;
|
||||||
|
import com.volmit.iris.util.context.IrisContext;
|
||||||
|
import com.volmit.iris.engine.object.IrisJigsawStructurePlacement;
|
||||||
|
import com.volmit.iris.util.collection.KList;
|
||||||
|
import com.volmit.iris.util.decree.DecreeExecutor;
|
||||||
|
import com.volmit.iris.util.decree.DecreeOrigin;
|
||||||
|
import com.volmit.iris.util.decree.annotations.Decree;
|
||||||
|
import com.volmit.iris.util.decree.annotations.Param;
|
||||||
|
import com.volmit.iris.util.decree.specialhandlers.NullableDimensionHandler;
|
||||||
|
import com.volmit.iris.util.format.C;
|
||||||
|
import com.volmit.iris.util.format.Form;
|
||||||
|
import com.volmit.iris.util.io.CountingDataInputStream;
|
||||||
|
import com.volmit.iris.util.io.IO;
|
||||||
|
import com.volmit.iris.util.mantle.TectonicPlate;
|
||||||
|
import com.volmit.iris.util.math.M;
|
||||||
|
import com.volmit.iris.util.matter.Matter;
|
||||||
|
import com.volmit.iris.util.nbt.mca.MCAFile;
|
||||||
|
import com.volmit.iris.util.nbt.mca.MCAUtil;
|
||||||
|
import com.volmit.iris.util.parallel.MultiBurst;
|
||||||
|
import com.volmit.iris.util.plugin.VolmitSender;
|
||||||
|
import com.volmit.iris.util.scheduling.jobs.Job;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import net.jpountz.lz4.LZ4BlockInputStream;
|
||||||
|
import net.jpountz.lz4.LZ4BlockOutputStream;
|
||||||
|
import net.jpountz.lz4.LZ4FrameInputStream;
|
||||||
|
import net.jpountz.lz4.LZ4FrameOutputStream;
|
||||||
|
import org.apache.commons.lang.RandomStringUtils;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.World;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.zip.GZIPInputStream;
|
||||||
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
|
||||||
|
@Decree(name = "Developer", origin = DecreeOrigin.BOTH, description = "Iris World Manager", aliases = {"dev"})
|
||||||
|
public class CommandDeveloper implements DecreeExecutor {
|
||||||
|
private CommandTurboPregen turboPregen;
|
||||||
|
private CommandLazyPregen lazyPregen;
|
||||||
|
|
||||||
|
@Decree(description = "Get Loaded TectonicPlates Count", origin = DecreeOrigin.BOTH, sync = true)
|
||||||
|
public void EngineStatus() {
|
||||||
|
Iris.service(IrisEngineSVC.class)
|
||||||
|
.engineStatus(sender());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Send a test exception to sentry")
|
||||||
|
public void Sentry() {
|
||||||
|
Engine engine = engine();
|
||||||
|
if (engine != null) IrisContext.getOr(engine);
|
||||||
|
Iris.reportError(new Exception("This is a test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Dev cmd to fix all the broken objects caused by faulty shrinkwarp")
|
||||||
|
public void fixObjects(
|
||||||
|
@Param(aliases = "dimension", description = "The dimension type to create the world with")
|
||||||
|
IrisDimension type
|
||||||
|
) {
|
||||||
|
if (type == null) {
|
||||||
|
sender().sendMessage("Type cant be null?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisData dm = IrisData.get(Iris.instance.getDataFolder("packs", type.getLoadKey()));
|
||||||
|
var loader = dm.getObjectLoader();
|
||||||
|
var processed = new KMap<String, IrisPosition>();
|
||||||
|
|
||||||
|
var objects = loader.getPossibleKeys();
|
||||||
|
var pieces = dm.getJigsawPieceLoader().getPossibleKeys();
|
||||||
|
var sender = sender();
|
||||||
|
|
||||||
|
sender.sendMessage(C.IRIS + "Found " + objects.length + " objects in " + type.getLoadKey());
|
||||||
|
sender.sendMessage(C.IRIS + "Found " + pieces.length + " jigsaw pieces in " + type.getLoadKey());
|
||||||
|
|
||||||
|
final int total = objects.length;
|
||||||
|
final AtomicInteger completed = new AtomicInteger();
|
||||||
|
final AtomicInteger changed = new AtomicInteger();
|
||||||
|
|
||||||
|
new Job() {
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Fixing Objects";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute() {
|
||||||
|
Arrays.stream(pieces).parallel()
|
||||||
|
.map(dm.getJigsawPieceLoader()::load)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.forEach(piece -> {
|
||||||
|
var offset = processed.compute(piece.getObject(), (key, o) -> {
|
||||||
|
if (o != null) return o;
|
||||||
|
var obj = loader.load(key);
|
||||||
|
if (obj == null) return new IrisPosition();
|
||||||
|
|
||||||
|
obj.shrinkwrap();
|
||||||
|
try {
|
||||||
|
if (!obj.getShrinkOffset().isZero()) {
|
||||||
|
changed.incrementAndGet();
|
||||||
|
obj.write(obj.getLoadFile());
|
||||||
|
}
|
||||||
|
completeWork();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Iris.error("Failed to write object " + obj.getLoadKey());
|
||||||
|
e.printStackTrace();
|
||||||
|
return new IrisPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new IrisPosition(obj.getShrinkOffset());
|
||||||
|
});
|
||||||
|
if (offset.getX() == 0 && offset.getY() == 0 && offset.getZ() == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
piece.getConnectors().forEach(connector -> connector.setPosition(connector.getPosition().add(offset)));
|
||||||
|
|
||||||
|
try {
|
||||||
|
IO.writeAll(piece.getLoadFile(), dm.getGson().toJson(piece));
|
||||||
|
} catch (IOException e) {
|
||||||
|
Iris.error("Failed to write jigsaw piece " + piece.getLoadKey());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Arrays.stream(loader.getPossibleKeys()).parallel()
|
||||||
|
.filter(key -> !processed.containsKey(key))
|
||||||
|
.map(loader::load)
|
||||||
|
.forEach(obj -> {
|
||||||
|
if (obj == null) {
|
||||||
|
completeWork();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.shrinkwrap();
|
||||||
|
if (obj.getShrinkOffset().isZero()) {
|
||||||
|
completeWork();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
obj.write(obj.getLoadFile());
|
||||||
|
completeWork();
|
||||||
|
changed.incrementAndGet();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Iris.error("Failed to write object " + obj.getLoadKey());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void completeWork() {
|
||||||
|
completed.incrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getTotalWork() {
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getWorkCompleted() {
|
||||||
|
return completed.get();
|
||||||
|
}
|
||||||
|
}.execute(sender, () -> {
|
||||||
|
var failed = total - completed.get();
|
||||||
|
if (failed != 0) sender.sendMessage(C.IRIS + "" + failed + " objects failed!");
|
||||||
|
if (changed.get() != 0) sender.sendMessage(C.IRIS + "" + changed.get() + " objects had their offsets changed!");
|
||||||
|
else sender.sendMessage(C.IRIS + "No objects had their offsets changed!");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Test")
|
||||||
|
public void mantle(@Param(defaultValue = "false") boolean plate, @Param(defaultValue = "21474836474") String name) throws Throwable {
|
||||||
|
var base = Iris.instance.getDataFile("dump", "pv." + name + ".ttp.lz4b.bin");
|
||||||
|
var section = Iris.instance.getDataFile("dump", "pv." + name + ".section.bin");
|
||||||
|
|
||||||
|
//extractSection(base, section, 5604930, 4397);
|
||||||
|
|
||||||
|
if (plate) {
|
||||||
|
try (var in = CountingDataInputStream.wrap(new BufferedInputStream(new FileInputStream(base)))) {
|
||||||
|
new TectonicPlate(1088, in, true);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
} else Matter.read(section);
|
||||||
|
if (!TectonicPlate.hasError())
|
||||||
|
Iris.info("Read " + (plate ? base : section).length() + " bytes from " + (plate ? base : section).getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void extractSection(File source, File target, long offset, int length) throws IOException {
|
||||||
|
var raf = new RandomAccessFile(source, "r");
|
||||||
|
var bytes = new byte[length];
|
||||||
|
raf.seek(offset);
|
||||||
|
raf.readFully(bytes);
|
||||||
|
raf.close();
|
||||||
|
Files.write(target.toPath(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Test")
|
||||||
|
public void dumpThreads() {
|
||||||
|
try {
|
||||||
|
File fi = Iris.instance.getDataFile("dump", "td-" + new java.sql.Date(M.ms()) + ".txt");
|
||||||
|
FileOutputStream fos = new FileOutputStream(fi);
|
||||||
|
Map<Thread, StackTraceElement[]> f = Thread.getAllStackTraces();
|
||||||
|
PrintWriter pw = new PrintWriter(fos);
|
||||||
|
|
||||||
|
pw.println(Thread.activeCount() + "/" + f.size());
|
||||||
|
var run = Runtime.getRuntime();
|
||||||
|
pw.println("Memory:");
|
||||||
|
pw.println("\tMax: " + run.maxMemory());
|
||||||
|
pw.println("\tTotal: " + run.totalMemory());
|
||||||
|
pw.println("\tFree: " + run.freeMemory());
|
||||||
|
pw.println("\tUsed: " + (run.totalMemory() - run.freeMemory()));
|
||||||
|
|
||||||
|
for (Thread i : f.keySet()) {
|
||||||
|
pw.println("========================================");
|
||||||
|
pw.println("Thread: '" + i.getName() + "' ID: " + i.getId() + " STATUS: " + i.getState().name());
|
||||||
|
|
||||||
|
for (StackTraceElement j : f.get(i)) {
|
||||||
|
pw.println(" @ " + j.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.println("========================================");
|
||||||
|
pw.println();
|
||||||
|
pw.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.close();
|
||||||
|
Iris.info("DUMPED! See " + fi.getAbsolutePath());
|
||||||
|
} catch (Throwable e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
@Decree(description = "Generate Iris structures for all loaded datapack structures")
|
||||||
|
public void generateStructures(
|
||||||
|
@Param(description = "The pack to add the generated structures to", aliases = "pack", defaultValue = "null", customHandler = NullableDimensionHandler.class)
|
||||||
|
IrisDimension dimension,
|
||||||
|
@Param(description = "Ignore existing structures", defaultValue = "false")
|
||||||
|
boolean force
|
||||||
|
) {
|
||||||
|
var map = INMS.get().collectStructures();
|
||||||
|
if (map.isEmpty()) {
|
||||||
|
sender().sendMessage(C.IRIS + "No structures found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sender().sendMessage(C.IRIS + "Found " + map.size() + " structures");
|
||||||
|
|
||||||
|
final File dataDir;
|
||||||
|
final IrisData data;
|
||||||
|
final Set<String> existingStructures;
|
||||||
|
final Map<String, Set<String>> snippets;
|
||||||
|
final File dimensionFile;
|
||||||
|
final File structuresFolder;
|
||||||
|
final File snippetsFolder;
|
||||||
|
|
||||||
|
var dimensionObj = new JsonObject();
|
||||||
|
|
||||||
|
if (dimension == null) {
|
||||||
|
dataDir = Iris.instance.getDataFolder("structures");
|
||||||
|
IO.delete(dataDir);
|
||||||
|
data = IrisData.get(dataDir);
|
||||||
|
existingStructures = Set.of();
|
||||||
|
snippets = Map.of();
|
||||||
|
dimensionFile = new File(dataDir, "structures.json");
|
||||||
|
} else {
|
||||||
|
data = dimension.getLoader();
|
||||||
|
dataDir = data.getDataFolder();
|
||||||
|
existingStructures = new KSet<>(data.getJigsawStructureLoader().getPossibleKeys());
|
||||||
|
|
||||||
|
dimensionObj = data.getGson().fromJson(IO.readAll(dimension.getLoadFile()), JsonObject.class);
|
||||||
|
snippets = Optional.ofNullable(dimensionObj.getAsJsonArray("jigsawStructures"))
|
||||||
|
.map(array -> array.asList()
|
||||||
|
.stream()
|
||||||
|
.filter(JsonElement::isJsonPrimitive)
|
||||||
|
.collect(Collectors.toMap(element -> data.getGson()
|
||||||
|
.fromJson(element, IrisJigsawStructurePlacement.class)
|
||||||
|
.getStructure(),
|
||||||
|
element -> Set.of(element.getAsString()),
|
||||||
|
KSet::merge)))
|
||||||
|
.orElse(Map.of());
|
||||||
|
|
||||||
|
dimensionFile = dimension.getLoadFile();
|
||||||
|
}
|
||||||
|
structuresFolder = new File(dataDir, "jigsaw-structures");
|
||||||
|
snippetsFolder = new File(dataDir, "snippet" + "/" + IrisJigsawStructurePlacement.class.getAnnotation(Snippet.class).value());
|
||||||
|
|
||||||
|
var gson = data.getGson();
|
||||||
|
var jigsawStructures = Optional.ofNullable(dimensionObj.getAsJsonArray("jigsawStructures"))
|
||||||
|
.orElse(new JsonArray(map.size()));
|
||||||
|
|
||||||
|
map.forEach((key, placement) -> {
|
||||||
|
String loadKey = "datapack/" + key.namespace() + "/" + key.key();
|
||||||
|
if (existingStructures.contains(loadKey) && !force)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var structures = placement.structures();
|
||||||
|
var obj = placement.toJson(loadKey);
|
||||||
|
if (obj == null || structures.isEmpty()) {
|
||||||
|
sender().sendMessage(C.RED + "Failed to generate hook for " + key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
File snippetFile = new File(snippetsFolder, loadKey + ".json");
|
||||||
|
try {
|
||||||
|
IO.writeAll(snippetFile, gson.toJson(obj));
|
||||||
|
} catch (IOException e) {
|
||||||
|
sender().sendMessage(C.RED + "Failed to generate snippet for " + key);
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> loadKeys = snippets.getOrDefault(loadKey, Set.of(loadKey));
|
||||||
|
jigsawStructures.asList().removeIf(e -> loadKeys.contains((e.isJsonObject() ? e.getAsJsonObject().get("structure") : e).getAsString()));
|
||||||
|
jigsawStructures.add("snippet/" + loadKey);
|
||||||
|
|
||||||
|
String structureKey;
|
||||||
|
if (structures.size() > 1) {
|
||||||
|
KList<String> common = new KList<>();
|
||||||
|
for (int i = 0; i < structures.size(); i++) {
|
||||||
|
var tags = structures.get(i).tags();
|
||||||
|
if (i == 0) common.addAll(tags);
|
||||||
|
else common.removeIf(tag -> !tags.contains(tag));
|
||||||
|
}
|
||||||
|
structureKey = common.isNotEmpty() ? "#" + common.getFirst() : structures.getFirst().key();
|
||||||
|
} else structureKey = structures.getFirst().key();
|
||||||
|
|
||||||
|
JsonArray array = new JsonArray();
|
||||||
|
if (structures.size() > 1) {
|
||||||
|
structures.stream()
|
||||||
|
.flatMap(structure -> {
|
||||||
|
String[] arr = new String[structure.weight()];
|
||||||
|
Arrays.fill(arr, structure.key());
|
||||||
|
return Arrays.stream(arr);
|
||||||
|
})
|
||||||
|
.forEach(array::add);
|
||||||
|
} else array.add(structureKey);
|
||||||
|
|
||||||
|
obj = new JsonObject();
|
||||||
|
obj.addProperty("structureKey", structureKey);
|
||||||
|
obj.add("datapackStructures", array);
|
||||||
|
|
||||||
|
File out = new File(structuresFolder, loadKey + ".json");
|
||||||
|
out.getParentFile().mkdirs();
|
||||||
|
try {
|
||||||
|
IO.writeAll(out, gson.toJson(obj));
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dimensionObj.add("jigsawStructures", jigsawStructures);
|
||||||
|
IO.writeAll(dimensionFile, gson.toJson(dimensionObj));
|
||||||
|
|
||||||
|
data.hotloaded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Test")
|
||||||
|
public void packBenchmark(
|
||||||
|
@Param(description = "The pack to bench", aliases = {"pack"}, defaultValue = "overworld")
|
||||||
|
IrisDimension dimension,
|
||||||
|
@Param(description = "Radius in regions", defaultValue = "2048")
|
||||||
|
int radius,
|
||||||
|
@Param(description = "Open GUI while benchmarking", defaultValue = "false")
|
||||||
|
boolean gui
|
||||||
|
) {
|
||||||
|
new IrisPackBenchmarking(dimension, radius, gui);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Upgrade to another Minecraft version")
|
||||||
|
public void upgrade(
|
||||||
|
@Param(description = "The version to upgrade to", defaultValue = "latest") DataVersion version) {
|
||||||
|
sender().sendMessage(C.GREEN + "Upgrading to " + version.getVersion() + "...");
|
||||||
|
ServerConfigurator.installDataPacks(version.get(), false);
|
||||||
|
sender().sendMessage(C.GREEN + "Done upgrading! You can now update your server version to " + version.getVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "test")
|
||||||
|
public void mca (
|
||||||
|
@Param(description = "String") String world) {
|
||||||
|
try {
|
||||||
|
File[] McaFiles = new File(world, "region").listFiles((dir, name) -> name.endsWith(".mca"));
|
||||||
|
for (File mca : McaFiles) {
|
||||||
|
MCAFile MCARegion = MCAUtil.read(mca);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "UnloadChunks for good reasons.")
|
||||||
|
public void unloadchunks() {
|
||||||
|
List<World> IrisWorlds = new ArrayList<>();
|
||||||
|
int chunksUnloaded = 0;
|
||||||
|
for (World world : Bukkit.getWorlds()) {
|
||||||
|
try {
|
||||||
|
if (IrisToolbelt.access(world).getEngine() != null) {
|
||||||
|
IrisWorlds.add(world);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// no
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (World world : IrisWorlds) {
|
||||||
|
for (Chunk chunk : world.getLoadedChunks()) {
|
||||||
|
if (chunk.isLoaded()) {
|
||||||
|
chunk.unload();
|
||||||
|
chunksUnloaded++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Iris.info(C.IRIS + "Chunks Unloaded: " + chunksUnloaded);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree
|
||||||
|
public void objects(@Param(defaultValue = "overworld") IrisDimension dimension) {
|
||||||
|
var loader = dimension.getLoader().getObjectLoader();
|
||||||
|
var sender = sender();
|
||||||
|
var keys = loader.getPossibleKeys();
|
||||||
|
var burst = MultiBurst.burst.burst(keys.length);
|
||||||
|
AtomicInteger failed = new AtomicInteger();
|
||||||
|
for (String key : keys) {
|
||||||
|
burst.queue(() -> {
|
||||||
|
if (loader.load(key) == null)
|
||||||
|
failed.incrementAndGet();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
burst.complete();
|
||||||
|
sender.sendMessage(C.RED + "Failed to load " + failed.get() + " of " + keys.length + " objects");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Test", aliases = {"ip"})
|
||||||
|
public void network() {
|
||||||
|
try {
|
||||||
|
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
|
||||||
|
for (NetworkInterface ni : Collections.list(networkInterfaces)) {
|
||||||
|
Iris.info("Display Name: %s", ni.getDisplayName());
|
||||||
|
Enumeration<InetAddress> inetAddresses = ni.getInetAddresses();
|
||||||
|
for (InetAddress ia : Collections.list(inetAddresses)) {
|
||||||
|
Iris.info("IP: %s", ia.getHostAddress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Test the compression algorithms")
|
||||||
|
public void compression(
|
||||||
|
@Param(description = "base IrisWorld") World world,
|
||||||
|
@Param(description = "raw TectonicPlate File") String path,
|
||||||
|
@Param(description = "Algorithm to Test") String algorithm,
|
||||||
|
@Param(description = "Amount of Tests") int amount,
|
||||||
|
@Param(description = "Is versioned", defaultValue = "false") boolean versioned) {
|
||||||
|
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||||
|
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", Bukkit.getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = new File(path);
|
||||||
|
if (!file.exists()) return;
|
||||||
|
|
||||||
|
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||||
|
if(engine != null) {
|
||||||
|
int height = engine.getTarget().getHeight();
|
||||||
|
ExecutorService service = Executors.newFixedThreadPool(1);
|
||||||
|
VolmitSender sender = sender();
|
||||||
|
service.submit(() -> {
|
||||||
|
try {
|
||||||
|
CountingDataInputStream raw = CountingDataInputStream.wrap(new FileInputStream(file));
|
||||||
|
TectonicPlate plate = new TectonicPlate(height, raw, versioned);
|
||||||
|
raw.close();
|
||||||
|
|
||||||
|
double d1 = 0;
|
||||||
|
double d2 = 0;
|
||||||
|
long size = 0;
|
||||||
|
File folder = new File("tmp");
|
||||||
|
folder.mkdirs();
|
||||||
|
for (int i = 0; i < amount; i++) {
|
||||||
|
File tmp = new File(folder, RandomStringUtils.randomAlphanumeric(10) + "." + algorithm + ".bin");
|
||||||
|
DataOutputStream dos = createOutput(tmp, algorithm);
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
plate.write(dos);
|
||||||
|
dos.close();
|
||||||
|
d1 += System.currentTimeMillis() - start;
|
||||||
|
if (size == 0)
|
||||||
|
size = tmp.length();
|
||||||
|
start = System.currentTimeMillis();
|
||||||
|
CountingDataInputStream din = createInput(tmp, algorithm);
|
||||||
|
new TectonicPlate(height, din, true);
|
||||||
|
din.close();
|
||||||
|
d2 += System.currentTimeMillis() - start;
|
||||||
|
tmp.delete();
|
||||||
|
}
|
||||||
|
IO.delete(folder);
|
||||||
|
sender.sendMessage(algorithm + " is " + Form.fileSize(size) + " big after compression");
|
||||||
|
sender.sendMessage(algorithm + " Took " + d2/amount + "ms to read");
|
||||||
|
sender.sendMessage(algorithm + " Took " + d1/amount + "ms to write");
|
||||||
|
} catch (Throwable e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
service.shutdown();
|
||||||
|
} else {
|
||||||
|
Iris.info(C.RED + "Engine is null!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CountingDataInputStream createInput(File file, String algorithm) throws Throwable {
|
||||||
|
FileInputStream in = new FileInputStream(file);
|
||||||
|
|
||||||
|
return CountingDataInputStream.wrap(switch (algorithm) {
|
||||||
|
case "gzip" -> new GZIPInputStream(in);
|
||||||
|
case "lz4f" -> new LZ4FrameInputStream(in);
|
||||||
|
case "lz4b" -> new LZ4BlockInputStream(in);
|
||||||
|
default -> throw new IllegalStateException("Unexpected value: " + algorithm);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataOutputStream createOutput(File file, String algorithm) throws Throwable {
|
||||||
|
FileOutputStream out = new FileOutputStream(file);
|
||||||
|
|
||||||
|
return new DataOutputStream(switch (algorithm) {
|
||||||
|
case "gzip" -> new GZIPOutputStream(out);
|
||||||
|
case "lz4f" -> new LZ4FrameOutputStream(out);
|
||||||
|
case "lz4b" -> new LZ4BlockOutputStream(out);
|
||||||
|
default -> throw new IllegalStateException("Unexpected value: " + algorithm);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+10
@@ -46,6 +46,16 @@ public class CommandEdit implements DecreeExecutor {
|
|||||||
sender().sendMessage(C.RED + "You must be in a studio world!");
|
sender().sendMessage(C.RED + "You must be in a studio world!");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (GraphicsEnvironment.isHeadless()) {
|
||||||
|
sender().sendMessage(C.RED + "Cannot open files in headless environments!");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Desktop.isDesktopSupported()) {
|
||||||
|
sender().sendMessage(C.RED + "Desktop is not supported by this environment!");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
+20
-10
@@ -34,7 +34,9 @@ public class CommandFind implements DecreeExecutor {
|
|||||||
@Decree(description = "Find a biome")
|
@Decree(description = "Find a biome")
|
||||||
public void biome(
|
public void biome(
|
||||||
@Param(description = "The biome to look for")
|
@Param(description = "The biome to look for")
|
||||||
IrisBiome biome
|
IrisBiome biome,
|
||||||
|
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||||
|
boolean teleport
|
||||||
) {
|
) {
|
||||||
Engine e = engine();
|
Engine e = engine();
|
||||||
|
|
||||||
@@ -43,13 +45,15 @@ public class CommandFind implements DecreeExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.gotoBiome(biome, player());
|
e.gotoBiome(biome, player(), teleport);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Decree(description = "Find a region")
|
@Decree(description = "Find a region")
|
||||||
public void region(
|
public void region(
|
||||||
@Param(description = "The region to look for")
|
@Param(description = "The region to look for")
|
||||||
IrisRegion region
|
IrisRegion region,
|
||||||
|
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||||
|
boolean teleport
|
||||||
) {
|
) {
|
||||||
Engine e = engine();
|
Engine e = engine();
|
||||||
|
|
||||||
@@ -58,13 +62,15 @@ public class CommandFind implements DecreeExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.gotoRegion(region, player());
|
e.gotoRegion(region, player(), teleport);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Decree(description = "Find a structure")
|
@Decree(description = "Find a structure")
|
||||||
public void structure(
|
public void structure(
|
||||||
@Param(description = "The structure to look for")
|
@Param(description = "The structure to look for")
|
||||||
IrisJigsawStructure structure
|
IrisJigsawStructure structure,
|
||||||
|
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||||
|
boolean teleport
|
||||||
) {
|
) {
|
||||||
Engine e = engine();
|
Engine e = engine();
|
||||||
|
|
||||||
@@ -73,13 +79,15 @@ public class CommandFind implements DecreeExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.gotoJigsaw(structure, player());
|
e.gotoJigsaw(structure, player(), teleport);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Decree(description = "Find a point of interest.")
|
@Decree(description = "Find a point of interest.")
|
||||||
public void poi(
|
public void poi(
|
||||||
@Param(description = "The type of PoI to look for.")
|
@Param(description = "The type of PoI to look for.")
|
||||||
String type
|
String type,
|
||||||
|
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||||
|
boolean teleport
|
||||||
) {
|
) {
|
||||||
Engine e = engine();
|
Engine e = engine();
|
||||||
if (e == null) {
|
if (e == null) {
|
||||||
@@ -87,13 +95,15 @@ public class CommandFind implements DecreeExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.gotoPOI(type, player());
|
e.gotoPOI(type, player(), teleport);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Decree(description = "Find an object")
|
@Decree(description = "Find an object")
|
||||||
public void object(
|
public void object(
|
||||||
@Param(description = "The object to look for", customHandler = ObjectHandler.class)
|
@Param(description = "The object to look for", customHandler = ObjectHandler.class)
|
||||||
String object
|
String object,
|
||||||
|
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||||
|
boolean teleport
|
||||||
) {
|
) {
|
||||||
Engine e = engine();
|
Engine e = engine();
|
||||||
|
|
||||||
@@ -102,6 +112,6 @@ public class CommandFind implements DecreeExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.gotoObject(object, player());
|
e.gotoObject(object, player(), teleport);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,529 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.commands;
|
||||||
|
|
||||||
|
import com.volmit.iris.Iris;
|
||||||
|
import com.volmit.iris.core.IrisSettings;
|
||||||
|
import com.volmit.iris.core.nms.INMS;
|
||||||
|
import com.volmit.iris.core.service.StudioSVC;
|
||||||
|
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||||
|
import com.volmit.iris.core.world.WorldStorage;
|
||||||
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
|
import com.volmit.iris.engine.object.IrisDimension;
|
||||||
|
import com.volmit.iris.util.collection.KList;
|
||||||
|
import com.volmit.iris.util.decree.DecreeExecutor;
|
||||||
|
import com.volmit.iris.util.decree.DecreeOrigin;
|
||||||
|
import com.volmit.iris.util.decree.annotations.Decree;
|
||||||
|
import com.volmit.iris.util.decree.annotations.Param;
|
||||||
|
import com.volmit.iris.util.decree.specialhandlers.NullablePlayerHandler;
|
||||||
|
import com.volmit.iris.util.format.C;
|
||||||
|
import com.volmit.iris.util.plugin.VolmitSender;
|
||||||
|
import com.volmit.iris.util.scheduling.J;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static com.volmit.iris.core.service.EditSVC.deletingWorld;
|
||||||
|
import static com.volmit.iris.util.misc.ServerProperties.BUKKIT_YML;
|
||||||
|
import static org.bukkit.Bukkit.getServer;
|
||||||
|
|
||||||
|
@Decree(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command")
|
||||||
|
public class CommandIris implements DecreeExecutor {
|
||||||
|
private CommandUpdater updater;
|
||||||
|
private CommandStudio studio;
|
||||||
|
private CommandPregen pregen;
|
||||||
|
private CommandSettings settings;
|
||||||
|
private CommandObject object;
|
||||||
|
private CommandJigsaw jigsaw;
|
||||||
|
private CommandWhat what;
|
||||||
|
private CommandEdit edit;
|
||||||
|
private CommandFind find;
|
||||||
|
private CommandDeveloper developer;
|
||||||
|
public static boolean worldCreation = false;
|
||||||
|
private static final AtomicReference<Thread> mainWorld = new AtomicReference<>();
|
||||||
|
String WorldEngine;
|
||||||
|
String worldNameToCheck = "YourWorldName";
|
||||||
|
VolmitSender sender = Iris.getSender();
|
||||||
|
|
||||||
|
@Decree(description = "Create a new world", aliases = {"+", "c"})
|
||||||
|
public void create(
|
||||||
|
@Param(aliases = "world-name", description = "The name of the world to create")
|
||||||
|
String name,
|
||||||
|
@Param(aliases = "dimension", description = "The dimension type to create the world with", defaultValue = "default")
|
||||||
|
IrisDimension type,
|
||||||
|
@Param(description = "The seed to generate the world with", defaultValue = "1337")
|
||||||
|
long seed,
|
||||||
|
@Param(aliases = "main-world", description = "Whether or not to automatically use this world as the main world", defaultValue = "false")
|
||||||
|
boolean main
|
||||||
|
) {
|
||||||
|
if (name.equalsIgnoreCase("iris")) {
|
||||||
|
sender().sendMessage(C.RED + "You cannot use the world name \"iris\" for creating worlds as Iris uses this directory for studio worlds.");
|
||||||
|
sender().sendMessage(C.RED + "May we suggest the name \"IrisWorld\" instead?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.equalsIgnoreCase("benchmark")) {
|
||||||
|
sender().sendMessage(C.RED + "You cannot use the world name \"benchmark\" for creating worlds as Iris uses this directory for Benchmarking Packs.");
|
||||||
|
sender().sendMessage(C.RED + "May we suggest the name \"IrisWorld\" instead?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WorldStorage.get().worldFolder(name).exists()) {
|
||||||
|
sender().sendMessage(C.RED + "That folder already exists!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
worldCreation = true;
|
||||||
|
IrisToolbelt.createWorld()
|
||||||
|
.dimension(type.getLoadKey())
|
||||||
|
.name(name)
|
||||||
|
.seed(seed)
|
||||||
|
.sender(sender())
|
||||||
|
.studio(false)
|
||||||
|
.create();
|
||||||
|
if (main) {
|
||||||
|
WorldStorage storage = WorldStorage.get();
|
||||||
|
Runtime.getRuntime().addShutdownHook(mainWorld.updateAndGet(old -> {
|
||||||
|
if (old != null) Runtime.getRuntime().removeShutdownHook(old);
|
||||||
|
return new Thread(() -> makeMainWorld(storage, name));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
sender().sendMessage(C.RED + "Exception raised during creation. See the console for more details.");
|
||||||
|
Iris.error("Exception raised during world creation: " + e.getMessage());
|
||||||
|
Iris.reportError(e);
|
||||||
|
worldCreation = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
worldCreation = false;
|
||||||
|
sender().sendMessage(C.GREEN + "Successfully created your world!");
|
||||||
|
if (main) sender().sendMessage(C.GREEN + "Your world will automatically be set as the main world when the server restarts.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void makeMainWorld(WorldStorage storage, String worldName) {
|
||||||
|
try {
|
||||||
|
storage.makeMainWorld(worldName);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.error("Failed to make " + worldName + " the main world!");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Teleport to another world", aliases = {"tp"}, sync = true)
|
||||||
|
public void teleport(
|
||||||
|
@Param(description = "World to teleport to")
|
||||||
|
World world,
|
||||||
|
@Param(description = "Player to teleport", defaultValue = "---", customHandler = NullablePlayerHandler.class)
|
||||||
|
Player player
|
||||||
|
) {
|
||||||
|
if (player == null && sender().isPlayer())
|
||||||
|
player = sender().player();
|
||||||
|
|
||||||
|
final Player target = player;
|
||||||
|
if (target == null) {
|
||||||
|
sender().sendMessage(C.RED + "The specified player does not exist.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new BukkitRunnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
target.teleport(world.getSpawnLocation());
|
||||||
|
new VolmitSender(target).sendMessage(C.GREEN + "You have been teleported to " + world.getName() + ".");
|
||||||
|
}
|
||||||
|
}.runTask(Iris.instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Print version information")
|
||||||
|
public void version() {
|
||||||
|
sender().sendMessage(C.GREEN + "Iris v" + Iris.instance.getDescription().getVersion() + " by Volmit Software");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
/todo
|
||||||
|
@Decree(description = "Benchmark a pack", origin = DecreeOrigin.CONSOLE)
|
||||||
|
public void packbenchmark(
|
||||||
|
@Param(description = "Dimension to benchmark")
|
||||||
|
IrisDimension type
|
||||||
|
) throws InterruptedException {
|
||||||
|
|
||||||
|
BenchDimension = type.getLoadKey();
|
||||||
|
|
||||||
|
IrisPackBenchmarking.runBenchmark();
|
||||||
|
} */
|
||||||
|
|
||||||
|
@Decree(description = "Print world height information", origin = DecreeOrigin.PLAYER)
|
||||||
|
public void height() {
|
||||||
|
if (sender().isPlayer()) {
|
||||||
|
sender().sendMessage(C.GREEN + "" + sender().player().getWorld().getMinHeight() + " to " + sender().player().getWorld().getMaxHeight());
|
||||||
|
sender().sendMessage(C.GREEN + "Total Height: " + (sender().player().getWorld().getMaxHeight() - sender().player().getWorld().getMinHeight()));
|
||||||
|
} else {
|
||||||
|
World mainWorld = getServer().getWorlds().get(0);
|
||||||
|
Iris.info(C.GREEN + "" + mainWorld.getMinHeight() + " to " + mainWorld.getMaxHeight());
|
||||||
|
Iris.info(C.GREEN + "Total Height: " + (mainWorld.getMaxHeight() - mainWorld.getMinHeight()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "QOL command to open a overworld studio world.", sync = true)
|
||||||
|
public void so() {
|
||||||
|
sender().sendMessage(C.GREEN + "Opening studio for the \"Overworld\" pack (seed: 1337)");
|
||||||
|
Iris.service(StudioSVC.class).open(sender(), 1337, "overworld");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Check access of all worlds.", aliases = {"accesslist"})
|
||||||
|
public void worlds() {
|
||||||
|
KList<World> IrisWorlds = new KList<>();
|
||||||
|
KList<World> BukkitWorlds = new KList<>();
|
||||||
|
|
||||||
|
for (World w : Bukkit.getServer().getWorlds()) {
|
||||||
|
try {
|
||||||
|
Engine engine = IrisToolbelt.access(w).getEngine();
|
||||||
|
if (engine != null) {
|
||||||
|
IrisWorlds.add(w);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
BukkitWorlds.add(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sender().isPlayer()) {
|
||||||
|
sender().sendMessage(C.BLUE + "Iris Worlds: ");
|
||||||
|
for (World IrisWorld : IrisWorlds.copy()) {
|
||||||
|
sender().sendMessage(C.IRIS + "- " +IrisWorld.getName());
|
||||||
|
}
|
||||||
|
sender().sendMessage(C.GOLD + "Bukkit Worlds: ");
|
||||||
|
for (World BukkitWorld : BukkitWorlds.copy()) {
|
||||||
|
sender().sendMessage(C.GRAY + "- " +BukkitWorld.getName());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Iris.info(C.BLUE + "Iris Worlds: ");
|
||||||
|
for (World IrisWorld : IrisWorlds.copy()) {
|
||||||
|
Iris.info(C.IRIS + "- " +IrisWorld.getName());
|
||||||
|
}
|
||||||
|
Iris.info(C.GOLD + "Bukkit Worlds: ");
|
||||||
|
for (World BukkitWorld : BukkitWorlds.copy()) {
|
||||||
|
Iris.info(C.GRAY + "- " +BukkitWorld.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Remove an Iris world", aliases = {"del", "rm", "delete"}, sync = true)
|
||||||
|
public void remove(
|
||||||
|
@Param(description = "The world to remove")
|
||||||
|
World world,
|
||||||
|
@Param(description = "Whether to also remove the folder (if set to false, just does not load the world)", defaultValue = "true")
|
||||||
|
boolean delete
|
||||||
|
) {
|
||||||
|
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||||
|
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sender().sendMessage(C.GREEN + "Removing world: " + world.getName());
|
||||||
|
|
||||||
|
if (!IrisToolbelt.evacuate(world)) {
|
||||||
|
sender().sendMessage(C.RED + "Failed to evacuate world: " + world.getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Bukkit.unloadWorld(world, false)) {
|
||||||
|
sender().sendMessage(C.RED + "Failed to unload world: " + world.getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (IrisToolbelt.removeWorld(world)) {
|
||||||
|
sender().sendMessage(C.GREEN + "Successfully removed " + world.getName() + " from bukkit.yml");
|
||||||
|
} else {
|
||||||
|
sender().sendMessage(C.YELLOW + "Looks like the world was already removed from bukkit.yml");
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
sender().sendMessage(C.RED + "Failed to save bukkit.yml because of " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
IrisToolbelt.evacuate(world, "Deleting world");
|
||||||
|
deletingWorld = true;
|
||||||
|
if (!delete) {
|
||||||
|
deletingWorld = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
VolmitSender sender = sender();
|
||||||
|
J.a(() -> {
|
||||||
|
int retries = 12;
|
||||||
|
|
||||||
|
if (deleteDirectory(world.getWorldFolder())) {
|
||||||
|
sender.sendMessage(C.GREEN + "Successfully removed world folder");
|
||||||
|
} else {
|
||||||
|
while(true){
|
||||||
|
if (deleteDirectory(world.getWorldFolder())){
|
||||||
|
sender.sendMessage(C.GREEN + "Successfully removed world folder");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
retries--;
|
||||||
|
if (retries == 0){
|
||||||
|
sender.sendMessage(C.RED + "Failed to remove world folder");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
J.sleep(3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deletingWorld = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean deleteDirectory(File dir) {
|
||||||
|
if (dir.isDirectory()) {
|
||||||
|
File[] children = dir.listFiles();
|
||||||
|
for (int i = 0; i < children.length; i++) {
|
||||||
|
boolean success = deleteDirectory(children[i]);
|
||||||
|
if (!success) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dir.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Set aura spins")
|
||||||
|
public void aura(
|
||||||
|
@Param(description = "The h color value", defaultValue = "-20")
|
||||||
|
int h,
|
||||||
|
@Param(description = "The s color value", defaultValue = "7")
|
||||||
|
int s,
|
||||||
|
@Param(description = "The b color value", defaultValue = "8")
|
||||||
|
int b
|
||||||
|
) {
|
||||||
|
IrisSettings.get().getGeneral().setSpinh(h);
|
||||||
|
IrisSettings.get().getGeneral().setSpins(s);
|
||||||
|
IrisSettings.get().getGeneral().setSpinb(b);
|
||||||
|
IrisSettings.get().forceSave();
|
||||||
|
sender().sendMessage("<rainbow>Aura Spins updated to " + h + " " + s + " " + b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Bitwise calculations")
|
||||||
|
public void bitwise(
|
||||||
|
@Param(description = "The first value to run calculations on")
|
||||||
|
int value1,
|
||||||
|
@Param(description = "The operator: | & ^ ≺≺ ≻≻ %")
|
||||||
|
String operator,
|
||||||
|
@Param(description = "The second value to run calculations on")
|
||||||
|
int value2
|
||||||
|
) {
|
||||||
|
Integer v = null;
|
||||||
|
switch (operator) {
|
||||||
|
case "|" -> v = value1 | value2;
|
||||||
|
case "&" -> v = value1 & value2;
|
||||||
|
case "^" -> v = value1 ^ value2;
|
||||||
|
case "%" -> v = value1 % value2;
|
||||||
|
case ">>" -> v = value1 >> value2;
|
||||||
|
case "<<" -> v = value1 << value2;
|
||||||
|
}
|
||||||
|
if (v == null) {
|
||||||
|
sender().sendMessage(C.RED + "The operator you entered: (" + operator + ") is invalid!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sender().sendMessage(C.GREEN + "" + value1 + " " + C.GREEN + operator.replaceAll("<", "≺").replaceAll(">", "≻").replaceAll("%", "%") + " " + C.GREEN + value2 + C.GREEN + " returns " + C.GREEN + v);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Toggle debug")
|
||||||
|
public void debug(
|
||||||
|
@Param(name = "on", description = "Whether or not debug should be on", defaultValue = "other")
|
||||||
|
Boolean on
|
||||||
|
) {
|
||||||
|
boolean to = on == null ? !IrisSettings.get().getGeneral().isDebug() : on;
|
||||||
|
IrisSettings.get().getGeneral().setDebug(to);
|
||||||
|
IrisSettings.get().forceSave();
|
||||||
|
sender().sendMessage(C.GREEN + "Set debug to: " + to);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO fix pack trimming
|
||||||
|
@Decree(description = "Download a project.", aliases = "dl")
|
||||||
|
public void download(
|
||||||
|
@Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project")
|
||||||
|
String pack,
|
||||||
|
@Param(name = "branch", description = "The branch to download from", defaultValue = "main")
|
||||||
|
String branch,
|
||||||
|
//@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false")
|
||||||
|
//boolean trim,
|
||||||
|
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false")
|
||||||
|
boolean overwrite
|
||||||
|
) {
|
||||||
|
boolean trim = false;
|
||||||
|
sender().sendMessage(C.GREEN + "Downloading pack: " + pack + "/" + branch + (trim ? " trimmed" : "") + (overwrite ? " overwriting" : ""));
|
||||||
|
if (pack.equals("overworld")) {
|
||||||
|
String url = "https://github.com/IrisDimensions/overworld/releases/download/" + INMS.OVERWORLD_TAG + "/overworld.zip";
|
||||||
|
Iris.service(StudioSVC.class).downloadRelease(sender(), url, trim, overwrite);
|
||||||
|
} else {
|
||||||
|
Iris.service(StudioSVC.class).downloadSearch(sender(), "IrisDimensions/" + pack + "/" + branch, trim, overwrite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Get metrics for your world", aliases = "measure", origin = DecreeOrigin.PLAYER)
|
||||||
|
public void metrics() {
|
||||||
|
if (!IrisToolbelt.isIrisWorld(world())) {
|
||||||
|
sender().sendMessage(C.RED + "You must be in an Iris world");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sender().sendMessage(C.GREEN + "Sending metrics...");
|
||||||
|
engine().printMetrics(sender());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Reload configuration file (this is also done automatically)")
|
||||||
|
public void reload() {
|
||||||
|
IrisSettings.invalidate();
|
||||||
|
IrisSettings.get();
|
||||||
|
sender().sendMessage(C.GREEN + "Hotloaded settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Update the pack of a world (UNSAFE!)", name = "^world", aliases = "update-world")
|
||||||
|
public void updateWorld(
|
||||||
|
@Param(description = "The world to update", contextual = true)
|
||||||
|
World world,
|
||||||
|
@Param(description = "The pack to install into the world", contextual = true, aliases = "dimension")
|
||||||
|
IrisDimension pack,
|
||||||
|
@Param(description = "Make sure to make a backup & read the warnings first!", defaultValue = "false", aliases = "c")
|
||||||
|
boolean confirm,
|
||||||
|
@Param(description = "Should Iris download the pack again for you", defaultValue = "false", name = "fresh-download", aliases = {"fresh", "new"})
|
||||||
|
boolean freshDownload
|
||||||
|
) {
|
||||||
|
if (!confirm) {
|
||||||
|
sender().sendMessage(new String[]{
|
||||||
|
C.RED + "You should always make a backup before using this",
|
||||||
|
C.YELLOW + "Issues caused by this can be, but are not limited to:",
|
||||||
|
C.YELLOW + " - Broken chunks (cut-offs) between old and new chunks (before & after the update)",
|
||||||
|
C.YELLOW + " - Regenerated chunks that do not fit in with the old chunks",
|
||||||
|
C.YELLOW + " - Structures not spawning again when regenerating",
|
||||||
|
C.YELLOW + " - Caves not lining up",
|
||||||
|
C.YELLOW + " - Terrain layers not lining up",
|
||||||
|
C.RED + "Now that you are aware of the risks, and have made a back-up:",
|
||||||
|
C.RED + "/iris ^world " + world.getName() + " " + pack.getLoadKey() + " confirm=true"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File folder = world.getWorldFolder();
|
||||||
|
folder.mkdirs();
|
||||||
|
|
||||||
|
if (freshDownload) {
|
||||||
|
Iris.service(StudioSVC.class).downloadSearch(sender(), pack.getLoadKey(), false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iris.service(StudioSVC.class).installIntoWorld(sender(), pack.getLoadKey(), folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Unload an Iris World", origin = DecreeOrigin.PLAYER, sync = true)
|
||||||
|
public void unloadWorld(
|
||||||
|
@Param(description = "The world to unload")
|
||||||
|
World world
|
||||||
|
) {
|
||||||
|
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||||
|
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sender().sendMessage(C.GREEN + "Unloading world: " + world.getName());
|
||||||
|
try {
|
||||||
|
IrisToolbelt.evacuate(world);
|
||||||
|
Bukkit.unloadWorld(world, false);
|
||||||
|
sender().sendMessage(C.GREEN + "World unloaded successfully.");
|
||||||
|
} catch (Exception e) {
|
||||||
|
sender().sendMessage(C.RED + "Failed to unload the world: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Load an Iris World", origin = DecreeOrigin.PLAYER, sync = true, aliases = {"import"})
|
||||||
|
public void loadWorld(
|
||||||
|
@Param(description = "The name of the world to load")
|
||||||
|
String world
|
||||||
|
) {
|
||||||
|
World worldloaded = Bukkit.getWorld(world);
|
||||||
|
WorldStorage.get().adoptLegacyData(world);
|
||||||
|
worldNameToCheck = world;
|
||||||
|
boolean worldExists = doesWorldExist(worldNameToCheck);
|
||||||
|
WorldEngine = world;
|
||||||
|
|
||||||
|
if (!worldExists) {
|
||||||
|
sender().sendMessage(C.YELLOW + world + " Doesnt exist on the server.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File directory = new File(WorldStorage.get().worldFolder(world), "iris/pack/dimensions");
|
||||||
|
|
||||||
|
String dimension = null;
|
||||||
|
if (directory.exists() && directory.isDirectory()) {
|
||||||
|
File[] files = directory.listFiles();
|
||||||
|
if (files != null) {
|
||||||
|
for (File file : files) {
|
||||||
|
if (file.isFile()) {
|
||||||
|
String fileName = file.getName();
|
||||||
|
if (fileName.endsWith(".json")) {
|
||||||
|
dimension = fileName.substring(0, fileName.length() - 5);
|
||||||
|
sender().sendMessage(C.BLUE + "Generator: " + dimension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sender().sendMessage(C.GOLD + world + " is not an iris world.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sender().sendMessage(C.GREEN + "Loading world: " + world);
|
||||||
|
|
||||||
|
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
|
||||||
|
String gen = "Iris:" + dimension;
|
||||||
|
ConfigurationSection section = yml.contains("worlds") ? yml.getConfigurationSection("worlds") : yml.createSection("worlds");
|
||||||
|
if (!section.contains(world)) {
|
||||||
|
section.createSection(world).set("generator", gen);
|
||||||
|
try {
|
||||||
|
yml.save(BUKKIT_YML);
|
||||||
|
Iris.info("Registered \"" + world + "\" in bukkit.yml");
|
||||||
|
} catch (IOException e) {
|
||||||
|
Iris.error("Failed to update bukkit.yml!");
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Iris.instance.checkForBukkitWorlds(world::equals);
|
||||||
|
sender().sendMessage(C.GREEN + world + " loaded successfully.");
|
||||||
|
}
|
||||||
|
@Decree(description = "Evacuate an iris world", origin = DecreeOrigin.PLAYER, sync = true)
|
||||||
|
public void evacuate(
|
||||||
|
@Param(description = "Evacuate the world")
|
||||||
|
World world
|
||||||
|
) {
|
||||||
|
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||||
|
sender().sendMessage(C.RED + "This is not an Iris world. Iris worlds: " + String.join(", ", getServer().getWorlds().stream().filter(IrisToolbelt::isIrisWorld).map(World::getName).toList()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sender().sendMessage(C.GREEN + "Evacuating world" + world.getName());
|
||||||
|
IrisToolbelt.evacuate(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean doesWorldExist(String worldName) {
|
||||||
|
File worldDirectory = WorldStorage.get().worldFolder(worldName);
|
||||||
|
return worldDirectory.exists() && worldDirectory.isDirectory();
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-5
@@ -21,6 +21,7 @@ package com.volmit.iris.core.commands;
|
|||||||
import com.volmit.iris.Iris;
|
import com.volmit.iris.Iris;
|
||||||
import com.volmit.iris.core.edit.JigsawEditor;
|
import com.volmit.iris.core.edit.JigsawEditor;
|
||||||
import com.volmit.iris.core.loader.IrisData;
|
import com.volmit.iris.core.loader.IrisData;
|
||||||
|
import com.volmit.iris.engine.framework.placer.WorldObjectPlacer;
|
||||||
import com.volmit.iris.engine.jigsaw.PlannedStructure;
|
import com.volmit.iris.engine.jigsaw.PlannedStructure;
|
||||||
import com.volmit.iris.engine.object.IrisJigsawPiece;
|
import com.volmit.iris.engine.object.IrisJigsawPiece;
|
||||||
import com.volmit.iris.engine.object.IrisJigsawStructure;
|
import com.volmit.iris.engine.object.IrisJigsawStructure;
|
||||||
@@ -34,6 +35,7 @@ import com.volmit.iris.util.decree.specialhandlers.ObjectHandler;
|
|||||||
import com.volmit.iris.util.format.C;
|
import com.volmit.iris.util.format.C;
|
||||||
import com.volmit.iris.util.format.Form;
|
import com.volmit.iris.util.format.Form;
|
||||||
import com.volmit.iris.util.math.RNG;
|
import com.volmit.iris.util.math.RNG;
|
||||||
|
import com.volmit.iris.util.plugin.VolmitSender;
|
||||||
import com.volmit.iris.util.scheduling.PrecisionStopwatch;
|
import com.volmit.iris.util.scheduling.PrecisionStopwatch;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -46,7 +48,7 @@ public class CommandJigsaw implements DecreeExecutor {
|
|||||||
IrisJigsawPiece piece
|
IrisJigsawPiece piece
|
||||||
) {
|
) {
|
||||||
File dest = piece.getLoadFile();
|
File dest = piece.getLoadFile();
|
||||||
new JigsawEditor(player(), piece, IrisData.loadAnyObject(piece.getObject()), dest);
|
new JigsawEditor(player(), piece, IrisData.loadAnyObject(piece.getObject(), data()), dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Decree(description = "Place a jigsaw structure")
|
@Decree(description = "Place a jigsaw structure")
|
||||||
@@ -55,9 +57,16 @@ public class CommandJigsaw implements DecreeExecutor {
|
|||||||
IrisJigsawStructure structure
|
IrisJigsawStructure structure
|
||||||
) {
|
) {
|
||||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||||
PlannedStructure ps = new PlannedStructure(structure, new IrisPosition(player().getLocation()), new RNG());
|
try {
|
||||||
sender().sendMessage(C.GREEN + "Generated " + ps.getPieces().size() + " pieces in " + Form.duration(p.getMilliseconds(), 2));
|
var world = world();
|
||||||
ps.place(world());
|
WorldObjectPlacer placer = new WorldObjectPlacer(world);
|
||||||
|
PlannedStructure ps = new PlannedStructure(structure, new IrisPosition(player().getLocation().add(0, world.getMinHeight(), 0)), new RNG(), true);
|
||||||
|
VolmitSender sender = sender();
|
||||||
|
sender.sendMessage(C.GREEN + "Generated " + ps.getPieces().size() + " pieces in " + Form.duration(p.getMilliseconds(), 2));
|
||||||
|
ps.place(placer, failed -> sender.sendMessage(failed ? C.GREEN + "Placed the structure!" : C.RED + "Failed to place the structure!"));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
sender().sendMessage(C.RED + "Failed to place the structure: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Decree(description = "Create a jigsaw piece")
|
@Decree(description = "Create a jigsaw piece")
|
||||||
@@ -69,7 +78,7 @@ public class CommandJigsaw implements DecreeExecutor {
|
|||||||
@Param(description = "The object to use for this piece", customHandler = ObjectHandler.class)
|
@Param(description = "The object to use for this piece", customHandler = ObjectHandler.class)
|
||||||
String object
|
String object
|
||||||
) {
|
) {
|
||||||
IrisObject o = IrisData.loadAnyObject(object);
|
IrisObject o = IrisData.loadAnyObject(object, data());
|
||||||
|
|
||||||
if (object == null) {
|
if (object == null) {
|
||||||
sender().sendMessage(C.RED + "Failed to find existing object");
|
sender().sendMessage(C.RED + "Failed to find existing object");
|
||||||
@@ -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 com.volmit.iris.core.commands;
|
||||||
|
|
||||||
|
import com.volmit.iris.Iris;
|
||||||
|
import com.volmit.iris.core.IrisSettings;
|
||||||
|
import com.volmit.iris.core.gui.PregeneratorJob;
|
||||||
|
import com.volmit.iris.core.pregenerator.LazyPregenerator;
|
||||||
|
import com.volmit.iris.core.pregenerator.PregenTask;
|
||||||
|
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||||
|
import com.volmit.iris.util.decree.DecreeExecutor;
|
||||||
|
import com.volmit.iris.util.decree.annotations.Decree;
|
||||||
|
import com.volmit.iris.util.decree.annotations.Param;
|
||||||
|
import com.volmit.iris.util.format.C;
|
||||||
|
import com.volmit.iris.util.math.Position2;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.util.Vector;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Decree(name = "lazypregen", aliases = "lazy", description = "Pregenerate your Iris worlds!")
|
||||||
|
public class CommandLazyPregen implements DecreeExecutor {
|
||||||
|
public String worldName;
|
||||||
|
@Decree(description = "Pregenerate a world")
|
||||||
|
public void start(
|
||||||
|
@Param(description = "The radius of the pregen in blocks", aliases = "size")
|
||||||
|
int radius,
|
||||||
|
@Param(description = "The world to pregen", contextual = true)
|
||||||
|
World world,
|
||||||
|
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0")
|
||||||
|
Vector center,
|
||||||
|
@Param(aliases = "maxcpm", description = "Limit the chunks per minute the pregen will generate", defaultValue = "999999999")
|
||||||
|
int cpm,
|
||||||
|
@Param(aliases = "silent", description = "Silent generation", defaultValue = "false")
|
||||||
|
boolean silent
|
||||||
|
) {
|
||||||
|
|
||||||
|
worldName = world.getName();
|
||||||
|
File worldDirectory = world.getWorldFolder();
|
||||||
|
File lazyFile = new File(worldDirectory, "lazygen.json");
|
||||||
|
if (lazyFile.exists()) {
|
||||||
|
sender().sendMessage(C.BLUE + "Lazy pregen is already in progress");
|
||||||
|
Iris.info(C.YELLOW + "Lazy pregen is already in progress");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (sender().isPlayer() && access() == null) {
|
||||||
|
sender().sendMessage(C.RED + "The engine access for this world is null!");
|
||||||
|
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyPregenerator.LazyPregenJob pregenJob = LazyPregenerator.LazyPregenJob.builder()
|
||||||
|
.world(worldName)
|
||||||
|
.healingPosition(0)
|
||||||
|
.healing(false)
|
||||||
|
.chunksPerMinute(cpm)
|
||||||
|
.radiusBlocks(radius)
|
||||||
|
.position(0)
|
||||||
|
.silent(silent)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
File lazyGenFile = new File(worldDirectory, "lazygen.json");
|
||||||
|
LazyPregenerator pregenerator = new LazyPregenerator(pregenJob, lazyGenFile);
|
||||||
|
pregenerator.start();
|
||||||
|
|
||||||
|
String msg = C.GREEN + "LazyPregen started in " + C.GOLD + worldName + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
|
||||||
|
sender().sendMessage(msg);
|
||||||
|
Iris.info(msg);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
sender().sendMessage(C.RED + "Epic fail. See console.");
|
||||||
|
Iris.reportError(e);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Stop the active pregeneration task", aliases = "x")
|
||||||
|
public void stop(
|
||||||
|
@Param(aliases = "world", description = "The world to pause")
|
||||||
|
World world
|
||||||
|
) throws IOException {
|
||||||
|
if (LazyPregenerator.getInstance() != null) {
|
||||||
|
LazyPregenerator.getInstance().shutdownInstance(world);
|
||||||
|
sender().sendMessage(C.LIGHT_PURPLE + "Closed lazygen instance for " + world.getName());
|
||||||
|
} else {
|
||||||
|
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"})
|
||||||
|
public void pause(
|
||||||
|
@Param(aliases = "world", description = "The world to pause")
|
||||||
|
World world
|
||||||
|
) {
|
||||||
|
if (LazyPregenerator.getInstance() != null) {
|
||||||
|
LazyPregenerator.getInstance().setPausedLazy(world);
|
||||||
|
sender().sendMessage(C.GREEN + "Paused/unpaused Lazy Pregen, now: " + (LazyPregenerator.getInstance().isPausedLazy(world) ? "Paused" : "Running") + ".");
|
||||||
|
} else {
|
||||||
|
sender().sendMessage(C.YELLOW + "No active Lazy Pregen tasks to pause/unpause.");
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
-18
@@ -24,9 +24,12 @@ import com.volmit.iris.core.loader.IrisData;
|
|||||||
import com.volmit.iris.core.service.ObjectSVC;
|
import com.volmit.iris.core.service.ObjectSVC;
|
||||||
import com.volmit.iris.core.service.StudioSVC;
|
import com.volmit.iris.core.service.StudioSVC;
|
||||||
import com.volmit.iris.core.service.WandSVC;
|
import com.volmit.iris.core.service.WandSVC;
|
||||||
|
import com.volmit.iris.core.tools.IrisConverter;
|
||||||
import com.volmit.iris.engine.framework.Engine;
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
import com.volmit.iris.engine.object.*;
|
import com.volmit.iris.engine.object.*;
|
||||||
import com.volmit.iris.util.data.Cuboid;
|
import com.volmit.iris.util.data.Cuboid;
|
||||||
|
import com.volmit.iris.util.data.IrisCustomData;
|
||||||
|
import com.volmit.iris.util.data.registry.Materials;
|
||||||
import com.volmit.iris.util.decree.DecreeExecutor;
|
import com.volmit.iris.util.decree.DecreeExecutor;
|
||||||
import com.volmit.iris.util.decree.DecreeOrigin;
|
import com.volmit.iris.util.decree.DecreeOrigin;
|
||||||
import com.volmit.iris.util.decree.annotations.Decree;
|
import com.volmit.iris.util.decree.annotations.Decree;
|
||||||
@@ -35,11 +38,8 @@ import com.volmit.iris.util.decree.specialhandlers.ObjectHandler;
|
|||||||
import com.volmit.iris.util.format.C;
|
import com.volmit.iris.util.format.C;
|
||||||
import com.volmit.iris.util.math.Direction;
|
import com.volmit.iris.util.math.Direction;
|
||||||
import com.volmit.iris.util.math.RNG;
|
import com.volmit.iris.util.math.RNG;
|
||||||
import com.volmit.iris.util.scheduling.Queue;
|
|
||||||
import org.bukkit.*;
|
import org.bukkit.*;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.block.BlockState;
|
|
||||||
import org.bukkit.block.TileState;
|
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
@@ -52,7 +52,7 @@ import java.util.*;
|
|||||||
@Decree(name = "object", aliases = "o", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris object manipulation")
|
@Decree(name = "object", aliases = "o", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris object manipulation")
|
||||||
public class CommandObject implements DecreeExecutor {
|
public class CommandObject implements DecreeExecutor {
|
||||||
|
|
||||||
private static final Set<Material> skipBlocks = Set.of(Material.GRASS, Material.SNOW, Material.VINE, Material.TORCH, Material.DEAD_BUSH,
|
private static final Set<Material> skipBlocks = Set.of(Materials.GRASS, Material.SNOW, Material.VINE, Material.TORCH, Material.DEAD_BUSH,
|
||||||
Material.POPPY, Material.DANDELION);
|
Material.POPPY, Material.DANDELION);
|
||||||
|
|
||||||
public static IObjectPlacer createPlacer(World world, Map<Block, BlockData> futureBlockChanges) {
|
public static IObjectPlacer createPlacer(World world, Map<Block, BlockData> futureBlockChanges) {
|
||||||
@@ -77,7 +77,10 @@ public class CommandObject implements DecreeExecutor {
|
|||||||
|
|
||||||
futureBlockChanges.put(block, block.getBlockData());
|
futureBlockChanges.put(block, block.getBlockData());
|
||||||
|
|
||||||
block.setBlockData(d);
|
if (d instanceof IrisCustomData data) {
|
||||||
|
block.setBlockData(data.getBase(), false);
|
||||||
|
Iris.warn("Tried to place custom block at " + x + ", " + y + ", " + z + " which is not supported!");
|
||||||
|
} else block.setBlockData(d, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -116,10 +119,18 @@ public class CommandObject implements DecreeExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setTile(int xx, int yy, int zz, TileData<? extends TileState> tile) {
|
public void setTile(int xx, int yy, int zz, TileData tile) {
|
||||||
BlockState state = world.getBlockAt(xx, yy, zz).getState();
|
tile.toBukkitTry(world.getBlockAt(xx, yy, zz));
|
||||||
tile.toBukkitTry(state);
|
}
|
||||||
state.update();
|
|
||||||
|
@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
|
@Override
|
||||||
@@ -134,11 +145,11 @@ public class CommandObject implements DecreeExecutor {
|
|||||||
@Param(description = "The object to analyze", customHandler = ObjectHandler.class)
|
@Param(description = "The object to analyze", customHandler = ObjectHandler.class)
|
||||||
String object
|
String object
|
||||||
) {
|
) {
|
||||||
IrisObject o = IrisData.loadAnyObject(object);
|
IrisObject o = IrisData.loadAnyObject(object, data());
|
||||||
sender().sendMessage("Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD() + "");
|
sender().sendMessage("Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD() + "");
|
||||||
sender().sendMessage("Blocks Used: " + NumberFormat.getIntegerInstance().format(o.getBlocks().size()));
|
sender().sendMessage("Blocks Used: " + NumberFormat.getIntegerInstance().format(o.getBlocks().size()));
|
||||||
|
|
||||||
Queue<BlockData> queue = o.getBlocks().enqueueValues();
|
var queue = o.getBlocks().values();
|
||||||
Map<Material, Set<BlockData>> unsorted = new HashMap<>();
|
Map<Material, Set<BlockData>> unsorted = new HashMap<>();
|
||||||
Map<BlockData, Integer> amounts = new HashMap<>();
|
Map<BlockData, Integer> amounts = new HashMap<>();
|
||||||
Map<Material, Integer> materials = new HashMap<>();
|
Map<Material, Integer> materials = new HashMap<>();
|
||||||
@@ -197,6 +208,30 @@ public class CommandObject implements DecreeExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Shrink an object to its minimum size")
|
||||||
|
public void shrink(@Param(description = "The object to shrink", customHandler = ObjectHandler.class) String object) {
|
||||||
|
IrisObject o = IrisData.loadAnyObject(object, data());
|
||||||
|
sender().sendMessage("Current Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD());
|
||||||
|
o.shrinkwrap();
|
||||||
|
sender().sendMessage("New Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD());
|
||||||
|
try {
|
||||||
|
o.write(o.getLoadFile());
|
||||||
|
} catch (IOException e) {
|
||||||
|
sender().sendMessage("Failed to save object " + o.getLoadFile() + ": " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Convert .schem files in the 'convert' folder to .iob files.")
|
||||||
|
public void convert () {
|
||||||
|
try {
|
||||||
|
IrisConverter.convertSchematics(sender());
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Decree(description = "Get a powder that reveals objects", studio = true, aliases = "d")
|
@Decree(description = "Get a powder that reveals objects", studio = true, aliases = "d")
|
||||||
public void dust() {
|
public void dust() {
|
||||||
player().getInventory().addItem(WandSVC.createDust());
|
player().getInventory().addItem(WandSVC.createDust());
|
||||||
@@ -215,7 +250,8 @@ public class CommandObject implements DecreeExecutor {
|
|||||||
|
|
||||||
|
|
||||||
Location[] b = WandSVC.getCuboid(player());
|
Location[] b = WandSVC.getCuboid(player());
|
||||||
if (b == null) {
|
if (b == null || b[0] == null || b[1] == null) {
|
||||||
|
sender().sendMessage("No area selected.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Location a1 = b[0].clone();
|
Location a1 = b[0].clone();
|
||||||
@@ -298,7 +334,7 @@ public class CommandObject implements DecreeExecutor {
|
|||||||
// @Param(description = "The scale interpolator to use", defaultValue = "none")
|
// @Param(description = "The scale interpolator to use", defaultValue = "none")
|
||||||
// IrisObjectPlacementScaleInterpolator interpolator
|
// IrisObjectPlacementScaleInterpolator interpolator
|
||||||
) {
|
) {
|
||||||
IrisObject o = IrisData.loadAnyObject(object);
|
IrisObject o = IrisData.loadAnyObject(object, data());
|
||||||
double maxScale = Double.max(10 - o.getBlocks().size() / 10000d, 1);
|
double maxScale = Double.max(10 - o.getBlocks().size() / 10000d, 1);
|
||||||
if (scale > maxScale) {
|
if (scale > maxScale) {
|
||||||
sender().sendMessage(C.YELLOW + "Indicated scale exceeds maximum. Downscaled to maximum: " + maxScale);
|
sender().sendMessage(C.YELLOW + "Indicated scale exceeds maximum. Downscaled to maximum: " + maxScale);
|
||||||
@@ -341,7 +377,7 @@ public class CommandObject implements DecreeExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sender().sendMessage("Placed " + object);
|
sender().sendMessage(C.IRIS + "Placed " + object);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,9 +388,11 @@ public class CommandObject implements DecreeExecutor {
|
|||||||
@Param(description = "The file to store it in, can use / for subfolders")
|
@Param(description = "The file to store it in, can use / for subfolders")
|
||||||
String name,
|
String name,
|
||||||
@Param(description = "Overwrite existing object files", defaultValue = "false", aliases = "force")
|
@Param(description = "Overwrite existing object files", defaultValue = "false", aliases = "force")
|
||||||
boolean overwrite
|
boolean overwrite,
|
||||||
|
@Param(description = "Use legacy TileState serialization if possible", defaultValue = "true")
|
||||||
|
boolean legacy
|
||||||
) {
|
) {
|
||||||
IrisObject o = WandSVC.createSchematic(player());
|
IrisObject o = WandSVC.createSchematic(player(), legacy);
|
||||||
|
|
||||||
if (o == null) {
|
if (o == null) {
|
||||||
sender().sendMessage(C.YELLOW + "You need to hold your wand!");
|
sender().sendMessage(C.YELLOW + "You need to hold your wand!");
|
||||||
@@ -368,7 +406,7 @@ public class CommandObject implements DecreeExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
o.write(file);
|
o.write(file, sender());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
sender().sendMessage(C.RED + "Failed to save object because of an IOException: " + e.getMessage());
|
sender().sendMessage(C.RED + "Failed to save object because of an IOException: " + e.getMessage());
|
||||||
Iris.reportError(e);
|
Iris.reportError(e);
|
||||||
@@ -389,6 +427,10 @@ public class CommandObject implements DecreeExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Location[] b = WandSVC.getCuboid(player());
|
Location[] b = WandSVC.getCuboid(player());
|
||||||
|
if (b == null || b[0] == null || b[1] == null) {
|
||||||
|
sender().sendMessage("No area selected.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
Location a1 = b[0].clone();
|
Location a1 = b[0].clone();
|
||||||
Location a2 = b[1].clone();
|
Location a2 = b[1].clone();
|
||||||
Direction d = Direction.closest(player().getLocation().getDirection()).reverse();
|
Direction d = Direction.closest(player().getLocation().getDirection()).reverse();
|
||||||
@@ -413,7 +455,7 @@ public class CommandObject implements DecreeExecutor {
|
|||||||
ObjectSVC service = Iris.service(ObjectSVC.class);
|
ObjectSVC service = Iris.service(ObjectSVC.class);
|
||||||
int actualReverts = Math.min(service.getUndos().size(), amount);
|
int actualReverts = Math.min(service.getUndos().size(), amount);
|
||||||
service.revertChanges(actualReverts);
|
service.revertChanges(actualReverts);
|
||||||
sender().sendMessage("Reverted " + actualReverts + " pastes!");
|
sender().sendMessage(C.BLUE + "Reverted " + actualReverts + C.BLUE +" pastes!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Decree(description = "Gets an object wand and grabs the current WorldEdit selection.", aliases = "we", origin = DecreeOrigin.PLAYER, studio = true)
|
@Decree(description = "Gets an object wand and grabs the current WorldEdit selection.", aliases = "we", origin = DecreeOrigin.PLAYER, studio = true)
|
||||||
@@ -449,6 +491,10 @@ public class CommandObject implements DecreeExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Location[] b = WandSVC.getCuboid(player());
|
Location[] b = WandSVC.getCuboid(player());
|
||||||
|
if (b == null || b[0] == null || b[1] == null) {
|
||||||
|
sender().sendMessage("No area selected.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
Location a1 = b[0].clone();
|
Location a1 = b[0].clone();
|
||||||
Location a2 = b[1].clone();
|
Location a2 = b[1].clone();
|
||||||
Location a1x = b[0].clone();
|
Location a1x = b[0].clone();
|
||||||
@@ -496,6 +542,10 @@ public class CommandObject implements DecreeExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Location[] b = WandSVC.getCuboid(player());
|
Location[] b = WandSVC.getCuboid(player());
|
||||||
|
if (b == null || b[0] == null || b[1] == null) {
|
||||||
|
sender().sendMessage("No area selected.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
b[0].add(new Vector(0, 1, 0));
|
b[0].add(new Vector(0, 1, 0));
|
||||||
b[1].add(new Vector(0, 1, 0));
|
b[1].add(new Vector(0, 1, 0));
|
||||||
Location a1 = b[0].clone();
|
Location a1 = b[0].clone();
|
||||||
+9
-7
@@ -39,20 +39,22 @@ public class CommandPregen implements DecreeExecutor {
|
|||||||
@Param(description = "The world to pregen", contextual = true)
|
@Param(description = "The world to pregen", contextual = true)
|
||||||
World world,
|
World world,
|
||||||
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0")
|
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0")
|
||||||
Vector center
|
Vector center,
|
||||||
) {
|
@Param(description = "Open the Iris pregen gui", defaultValue = "true")
|
||||||
|
boolean gui
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
if (sender().isPlayer() && access() == null) {
|
if (sender().isPlayer() && access() == null) {
|
||||||
sender().sendMessage(C.RED + "The engine access for this world is null!");
|
sender().sendMessage(C.RED + "The engine access for this world is null!");
|
||||||
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
|
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
|
||||||
}
|
}
|
||||||
radius = Math.max(radius, 1024);
|
radius = Math.max(radius, 1024);
|
||||||
int w = (radius >> 9 + 1) * 2;
|
|
||||||
IrisToolbelt.pregenerate(PregenTask
|
IrisToolbelt.pregenerate(PregenTask
|
||||||
.builder()
|
.builder()
|
||||||
.center(new Position2(center))
|
.center(new Position2(center.getBlockX(), center.getBlockZ()))
|
||||||
.width(w)
|
.gui(gui)
|
||||||
.height(w)
|
.radiusX(radius)
|
||||||
|
.radiusZ(radius)
|
||||||
.build(), world);
|
.build(), world);
|
||||||
String msg = C.GREEN + "Pregen started in " + C.GOLD + world.getName() + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
|
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);
|
sender().sendMessage(msg);
|
||||||
@@ -67,7 +69,7 @@ public class CommandPregen implements DecreeExecutor {
|
|||||||
@Decree(description = "Stop the active pregeneration task", aliases = "x")
|
@Decree(description = "Stop the active pregeneration task", aliases = "x")
|
||||||
public void stop() {
|
public void stop() {
|
||||||
if (PregeneratorJob.shutdownInstance()) {
|
if (PregeneratorJob.shutdownInstance()) {
|
||||||
sender().sendMessage(C.GREEN + "Stopped pregeneration task");
|
Iris.info( C.BLUE + "Finishing up mca region...");
|
||||||
} else {
|
} else {
|
||||||
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
|
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
|
||||||
}
|
}
|
||||||
File renamed without changes.
+270
-29
@@ -20,8 +20,10 @@ package com.volmit.iris.core.commands;
|
|||||||
|
|
||||||
import com.volmit.iris.Iris;
|
import com.volmit.iris.Iris;
|
||||||
import com.volmit.iris.core.IrisSettings;
|
import com.volmit.iris.core.IrisSettings;
|
||||||
|
import com.volmit.iris.core.gui.GuiHost;
|
||||||
|
import com.volmit.iris.core.gui.MapWindow;
|
||||||
import com.volmit.iris.core.gui.NoiseExplorerGUI;
|
import com.volmit.iris.core.gui.NoiseExplorerGUI;
|
||||||
import com.volmit.iris.core.gui.VisionGUI;
|
import com.volmit.iris.core.gui.PaletteExplorerStart;
|
||||||
import com.volmit.iris.core.loader.IrisData;
|
import com.volmit.iris.core.loader.IrisData;
|
||||||
import com.volmit.iris.core.project.IrisProject;
|
import com.volmit.iris.core.project.IrisProject;
|
||||||
import com.volmit.iris.core.service.ConversionSVC;
|
import com.volmit.iris.core.service.ConversionSVC;
|
||||||
@@ -29,28 +31,35 @@ import com.volmit.iris.core.service.StudioSVC;
|
|||||||
import com.volmit.iris.core.tools.IrisToolbelt;
|
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||||
import com.volmit.iris.engine.framework.Engine;
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
import com.volmit.iris.engine.object.*;
|
import com.volmit.iris.engine.object.*;
|
||||||
|
import com.volmit.iris.engine.platform.PlatformChunkGenerator;
|
||||||
import com.volmit.iris.util.collection.KList;
|
import com.volmit.iris.util.collection.KList;
|
||||||
import com.volmit.iris.util.collection.KMap;
|
import com.volmit.iris.util.collection.KMap;
|
||||||
import com.volmit.iris.util.collection.KSet;
|
import com.volmit.iris.util.collection.KSet;
|
||||||
|
import com.volmit.iris.util.decree.DecreeContext;
|
||||||
import com.volmit.iris.util.decree.DecreeExecutor;
|
import com.volmit.iris.util.decree.DecreeExecutor;
|
||||||
import com.volmit.iris.util.decree.DecreeOrigin;
|
import com.volmit.iris.util.decree.DecreeOrigin;
|
||||||
import com.volmit.iris.util.decree.annotations.Decree;
|
import com.volmit.iris.util.decree.annotations.Decree;
|
||||||
import com.volmit.iris.util.decree.annotations.Param;
|
import com.volmit.iris.util.decree.annotations.Param;
|
||||||
import com.volmit.iris.util.format.C;
|
import com.volmit.iris.util.format.C;
|
||||||
import com.volmit.iris.util.format.Form;
|
import com.volmit.iris.util.format.Form;
|
||||||
import com.volmit.iris.util.function.Function2;
|
|
||||||
import com.volmit.iris.util.function.NoiseProvider;
|
import com.volmit.iris.util.function.NoiseProvider;
|
||||||
import com.volmit.iris.util.interpolation.InterpolationMethod;
|
import com.volmit.iris.util.interpolation.InterpolationMethod;
|
||||||
import com.volmit.iris.util.io.IO;
|
import com.volmit.iris.util.io.IO;
|
||||||
import com.volmit.iris.util.json.JSONArray;
|
import com.volmit.iris.util.json.JSONArray;
|
||||||
import com.volmit.iris.util.json.JSONObject;
|
import com.volmit.iris.util.json.JSONObject;
|
||||||
|
import com.volmit.iris.util.mantle.MantleChunk;
|
||||||
import com.volmit.iris.util.math.M;
|
import com.volmit.iris.util.math.M;
|
||||||
|
import com.volmit.iris.util.math.Position2;
|
||||||
import com.volmit.iris.util.math.RNG;
|
import com.volmit.iris.util.math.RNG;
|
||||||
import com.volmit.iris.util.math.Spiraler;
|
import com.volmit.iris.util.math.Spiraler;
|
||||||
import com.volmit.iris.util.noise.CNG;
|
import com.volmit.iris.util.noise.CNG;
|
||||||
|
import com.volmit.iris.util.parallel.MultiBurst;
|
||||||
|
import com.volmit.iris.util.parallel.SyncExecutor;
|
||||||
|
import com.volmit.iris.util.plugin.VolmitSender;
|
||||||
|
import com.volmit.iris.util.scheduling.J;
|
||||||
import com.volmit.iris.util.scheduling.O;
|
import com.volmit.iris.util.scheduling.O;
|
||||||
import com.volmit.iris.util.scheduling.PrecisionStopwatch;
|
import com.volmit.iris.util.scheduling.PrecisionStopwatch;
|
||||||
import io.papermc.lib.PaperLib;
|
import com.volmit.iris.util.scheduling.jobs.ParallelRadiusJob;
|
||||||
import org.bukkit.*;
|
import org.bukkit.*;
|
||||||
import org.bukkit.event.inventory.InventoryType;
|
import org.bukkit.event.inventory.InventoryType;
|
||||||
import org.bukkit.inventory.Inventory;
|
import org.bukkit.inventory.Inventory;
|
||||||
@@ -65,31 +74,36 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.attribute.FileTime;
|
import java.nio.file.attribute.FileTime;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Supplier;
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
@Decree(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", studio = true)
|
@Decree(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", studio = true)
|
||||||
public class CommandStudio implements DecreeExecutor {
|
public class CommandStudio implements DecreeExecutor {
|
||||||
private CommandFind find;
|
private CommandFind find;
|
||||||
private CommandEdit edit;
|
private CommandEdit edit;
|
||||||
|
//private CommandDeepSearch deepSearch;
|
||||||
|
|
||||||
public static String hrf(Duration duration) {
|
public static String hrf(Duration duration) {
|
||||||
return duration.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase();
|
return duration.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO fix pack trimming
|
||||||
@Decree(description = "Download a project.", aliases = "dl")
|
@Decree(description = "Download a project.", aliases = "dl")
|
||||||
public void download(
|
public void download(
|
||||||
@Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project")
|
@Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project")
|
||||||
String pack,
|
String pack,
|
||||||
@Param(name = "branch", description = "The branch to download from", defaultValue = "master")
|
@Param(name = "branch", description = "The branch to download from", defaultValue = "master")
|
||||||
String branch,
|
String branch,
|
||||||
@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false")
|
//@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false")
|
||||||
boolean trim,
|
//boolean trim,
|
||||||
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false")
|
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false")
|
||||||
boolean overwrite
|
boolean overwrite
|
||||||
) {
|
) {
|
||||||
new CommandIris().download(pack, branch, trim, overwrite);
|
new CommandIris().download(pack, branch, overwrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Decree(description = "Open a new studio world", aliases = "o", sync = true)
|
@Decree(description = "Open a new studio world", aliases = "o", sync = true)
|
||||||
@@ -143,12 +157,87 @@ public class CommandStudio implements DecreeExecutor {
|
|||||||
sender().sendMessage(C.GREEN + "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion());
|
sender().sendMessage(C.GREEN + "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Decree(name = "regen", description = "Regenerate nearby chunks.", aliases = "rg", sync = true, origin = DecreeOrigin.PLAYER)
|
||||||
|
public void regen(
|
||||||
|
@Param(name = "radius", description = "The radius of nearby cunks", defaultValue = "5")
|
||||||
|
int radius
|
||||||
|
) {
|
||||||
|
World world = player().getWorld();
|
||||||
|
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||||
|
sender().sendMessage(C.RED + "You must be in an Iris World to use regen!");
|
||||||
|
}
|
||||||
|
|
||||||
|
VolmitSender sender = sender();
|
||||||
|
var loc = player().getLocation().clone();
|
||||||
|
|
||||||
|
J.a(() -> {
|
||||||
|
PlatformChunkGenerator plat = IrisToolbelt.access(world);
|
||||||
|
Engine engine = plat.getEngine();
|
||||||
|
DecreeContext.touch(sender);
|
||||||
|
try (SyncExecutor executor = new SyncExecutor(20);
|
||||||
|
var service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
|
||||||
|
) {
|
||||||
|
int x = loc.getBlockX() >> 4;
|
||||||
|
int z = loc.getBlockZ() >> 4;
|
||||||
|
|
||||||
|
int rad = engine.getMantle().getRadius();
|
||||||
|
var mantle = engine.getMantle().getMantle();
|
||||||
|
var chunkMap = new KMap<Position2, MantleChunk>();
|
||||||
|
ParallelRadiusJob prep = new ParallelRadiusJob(Integer.MAX_VALUE, service) {
|
||||||
|
@Override
|
||||||
|
protected void execute(int rX, int rZ) {
|
||||||
|
if (Math.abs(rX) <= radius && Math.abs(rZ) <= radius) {
|
||||||
|
mantle.deleteChunk(rX + x, rZ + z);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rX += x;
|
||||||
|
rZ += z;
|
||||||
|
chunkMap.put(new Position2(rX, rZ), mantle.getChunk(rX, rZ));
|
||||||
|
mantle.deleteChunk(rX, rZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Preparing Mantle";
|
||||||
|
}
|
||||||
|
}.retarget(radius + rad, 0, 0);
|
||||||
|
CountDownLatch pLatch = new CountDownLatch(1);
|
||||||
|
prep.execute(sender(), pLatch::countDown);
|
||||||
|
pLatch.await();
|
||||||
|
|
||||||
|
|
||||||
|
ParallelRadiusJob job = new ParallelRadiusJob(Integer.MAX_VALUE, service) {
|
||||||
|
@Override
|
||||||
|
protected void execute(int x, int z) {
|
||||||
|
plat.injectChunkReplacement(world, x, z, executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Regenerating";
|
||||||
|
}
|
||||||
|
}.retarget(radius, x, z);
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
job.execute(sender(), latch::countDown);
|
||||||
|
latch.await();
|
||||||
|
|
||||||
|
chunkMap.forEach((pos, chunk) ->
|
||||||
|
mantle.getChunk(pos.getX(), pos.getZ()).copyFrom(chunk));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
sender().sendMessage("Error while regenerating chunks");
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
DecreeContext.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Decree(description = "Convert objects in the \"convert\" folder")
|
@Decree(description = "Convert objects in the \"convert\" folder")
|
||||||
public void convert() {
|
public void convert() {
|
||||||
Iris.service(ConversionSVC.class).check(sender());
|
Iris.service(ConversionSVC.class).check(sender());
|
||||||
|
//IrisConverter.convertSchematics(sender());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Decree(description = "Execute a script", aliases = "run", origin = DecreeOrigin.PLAYER)
|
@Decree(description = "Execute a script", aliases = "run", origin = DecreeOrigin.PLAYER)
|
||||||
public void execute(
|
public void execute(
|
||||||
@Param(description = "The script to run")
|
@Param(description = "The script to run")
|
||||||
@@ -157,11 +246,47 @@ public class CommandStudio implements DecreeExecutor {
|
|||||||
engine().getExecution().execute(script.getLoadKey());
|
engine().getExecution().execute(script.getLoadKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Decree(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n"})
|
@Decree(description = "Open the map on its noise explorer (External GUI)", aliases = {"nmap", "n"})
|
||||||
public void noise() {
|
public void noise() {
|
||||||
if (noGUI()) return;
|
if (noGUI()) return;
|
||||||
sender().sendMessage(C.GREEN + "Opening Noise Explorer!");
|
Engine playerEngine = engine();
|
||||||
NoiseExplorerGUI.launch();
|
Engine engine = playerEngine != null ? playerEngine : GuiHost.findActiveEngine();
|
||||||
|
World world = engine == null ? null : engine.getWorld().realWorld();
|
||||||
|
if (world == null) {
|
||||||
|
sender().sendMessage(C.RED + "Open a studio or stand in an Iris world to explore its noise!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Location center = playerEngine != null ? player().getLocation() : null;
|
||||||
|
sender().sendMessage(C.GREEN + "Opening the map on its noise!");
|
||||||
|
MapWindow.openNoise(new PaletteExplorerStart(
|
||||||
|
engine,
|
||||||
|
world.getUID(),
|
||||||
|
sender().isPlayer() ? player().getUniqueId() : null,
|
||||||
|
null,
|
||||||
|
center == null ? 0D : center.getX(),
|
||||||
|
center == null ? 0D : center.getZ()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Preview the palettes around you (External GUI)", aliases = "pal", origin = DecreeOrigin.PLAYER)
|
||||||
|
public void palette() {
|
||||||
|
if (noGUI()) return;
|
||||||
|
Engine engine = engine();
|
||||||
|
if (engine == null) {
|
||||||
|
sender().sendMessage(C.RED + "Stand in an Iris world to preview its palettes!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Location location = player().getLocation();
|
||||||
|
IrisBiome surfaceBiome = engine.getSurfaceBiome(location.getBlockX(), location.getBlockZ());
|
||||||
|
sender().sendMessage(C.GREEN + "Opening the map on its palettes!");
|
||||||
|
MapWindow.openPalettes(new PaletteExplorerStart(
|
||||||
|
engine,
|
||||||
|
location.getWorld().getUID(),
|
||||||
|
player().getUniqueId(),
|
||||||
|
surfaceBiome == null ? null : surfaceBiome.getLoadKey(),
|
||||||
|
location.getX(),
|
||||||
|
location.getZ()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Decree(description = "Charges all spawners in the area", aliases = "zzt", origin = DecreeOrigin.PLAYER)
|
@Decree(description = "Charges all spawners in the area", aliases = "zzt", origin = DecreeOrigin.PLAYER)
|
||||||
@@ -183,16 +308,7 @@ public class CommandStudio implements DecreeExecutor {
|
|||||||
) {
|
) {
|
||||||
if (noGUI()) return;
|
if (noGUI()) return;
|
||||||
sender().sendMessage(C.GREEN + "Opening Noise Explorer!");
|
sender().sendMessage(C.GREEN + "Opening Noise Explorer!");
|
||||||
|
NoiseExplorerGUI.launchGeneratorKey(engine(), generator == null ? null : generator.getLoadKey(), generator, seed);
|
||||||
Supplier<Function2<Double, Double, Double>> l = () -> {
|
|
||||||
|
|
||||||
if (generator == null) {
|
|
||||||
return (x, z) -> 0D;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (x, z) -> generator.getHeight(x, z, new RNG(seed).nextParallelRNG(3245).lmax());
|
|
||||||
};
|
|
||||||
NoiseExplorerGUI.launch(l, "Custom Generator");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Decree(description = "Hotload a studio", aliases = {"reload", "h"})
|
@Decree(description = "Hotload a studio", aliases = {"reload", "h"})
|
||||||
@@ -218,7 +334,7 @@ public class CommandStudio implements DecreeExecutor {
|
|||||||
Inventory inv = Bukkit.createInventory(null, 27 * 2);
|
Inventory inv = Bukkit.createInventory(null, 27 * 2);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
engine().addItems(true, inv, RNG.r, tables, InventorySlotType.STORAGE, player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ(), 1);
|
engine().addItems(true, inv, RNG.r, tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ(), 1);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Iris.reportError(e);
|
Iris.reportError(e);
|
||||||
sender().sendMessage(C.RED + "Cannot add items to virtual inventory because of: " + e.getMessage());
|
sender().sendMessage(C.RED + "Cannot add items to virtual inventory because of: " + e.getMessage());
|
||||||
@@ -229,11 +345,15 @@ public class CommandStudio implements DecreeExecutor {
|
|||||||
O<Integer> ta = new O<>();
|
O<Integer> ta = new O<>();
|
||||||
ta.set(-1);
|
ta.set(-1);
|
||||||
|
|
||||||
|
var sender = sender();
|
||||||
|
var player = player();
|
||||||
|
var engine = engine();
|
||||||
|
|
||||||
ta.set(Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, () ->
|
ta.set(Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, () ->
|
||||||
{
|
{
|
||||||
if (!player().getOpenInventory().getType().equals(InventoryType.CHEST)) {
|
if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) {
|
||||||
Bukkit.getScheduler().cancelTask(ta.get());
|
Bukkit.getScheduler().cancelTask(ta.get());
|
||||||
sender().sendMessage(C.GREEN + "Opened inventory!");
|
sender.sendMessage(C.GREEN + "Opened inventory!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,13 +361,116 @@ public class CommandStudio implements DecreeExecutor {
|
|||||||
inv.clear();
|
inv.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
engine().addItems(true, inv, new RNG(RNG.r.imax()), tables, InventorySlotType.STORAGE, player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ(), 1);
|
engine.addItems(true, inv, new RNG(RNG.r.imax()), tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ(), 1);
|
||||||
}, 0, fast ? 5 : 35));
|
}, 0, fast ? 5 : 35));
|
||||||
|
|
||||||
sender().sendMessage(C.GREEN + "Opening inventory now!");
|
sender().sendMessage(C.GREEN + "Opening inventory now!");
|
||||||
player().openInventory(inv);
|
player().openInventory(inv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Calculate the chance for each region to generate", origin = DecreeOrigin.PLAYER)
|
||||||
|
public void regions(@Param(description = "The radius in chunks", defaultValue = "500") int radius) {
|
||||||
|
var engine = engine();
|
||||||
|
if (engine == null) {
|
||||||
|
sender().sendMessage(C.RED + "Only works in an Iris world!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var sender = sender();
|
||||||
|
var player = player();
|
||||||
|
Thread.ofVirtual()
|
||||||
|
.start(() -> {
|
||||||
|
int d = radius * 2;
|
||||||
|
KMap<String, AtomicInteger> data = new KMap<>();
|
||||||
|
engine.getDimension().getRegions().forEach(key -> data.put(key, new AtomicInteger(0)));
|
||||||
|
var multiBurst = new MultiBurst("Region Sampler");
|
||||||
|
var executor = multiBurst.burst(radius * radius);
|
||||||
|
sender.sendMessage(C.GRAY + "Generating data...");
|
||||||
|
var loc = player.getLocation();
|
||||||
|
int totalTasks = d * d;
|
||||||
|
AtomicInteger completedTasks = new AtomicInteger(0);
|
||||||
|
int c = J.ar(() -> sender.sendProgress((double) completedTasks.get() / totalTasks, "Finding regions"), 0);
|
||||||
|
new Spiraler(d, d, (x, z) -> executor.queue(() -> {
|
||||||
|
var region = engine.getRegion((x << 4) + 8, (z << 4) + 8);
|
||||||
|
data.computeIfAbsent(region.getLoadKey(), (k) -> new AtomicInteger(0))
|
||||||
|
.incrementAndGet();
|
||||||
|
completedTasks.incrementAndGet();
|
||||||
|
})).setOffset(loc.getBlockX(), loc.getBlockZ()).drain();
|
||||||
|
executor.complete();
|
||||||
|
multiBurst.close();
|
||||||
|
J.car(c);
|
||||||
|
|
||||||
|
sender.sendMessage(C.GREEN + "Done!");
|
||||||
|
var loader = engine.getData().getRegionLoader();
|
||||||
|
data.forEach((k, v) -> sender.sendMessage(C.GREEN + k + ": " + loader.load(k).getRarity() + " / " + Form.f((double) v.get() / totalTasks * 100, 2) + "%"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Get all structures in a radius of chunks", aliases = "dist", origin = DecreeOrigin.PLAYER)
|
||||||
|
public void distances(@Param(description = "The radius in chunks") int radius) {
|
||||||
|
var engine = engine();
|
||||||
|
if (engine == null) {
|
||||||
|
sender().sendMessage(C.RED + "Only works in an Iris world!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var sender = sender();
|
||||||
|
int d = radius * 2;
|
||||||
|
KMap<String, KList<Position2>> data = new KMap<>();
|
||||||
|
var multiBurst = new MultiBurst("Distance Sampler");
|
||||||
|
var executor = multiBurst.burst(radius * radius);
|
||||||
|
|
||||||
|
sender.sendMessage(C.GRAY + "Generating data...");
|
||||||
|
var loc = player().getLocation();
|
||||||
|
int totalTasks = d * d;
|
||||||
|
AtomicInteger completedTasks = new AtomicInteger(0);
|
||||||
|
int c = J.ar(() -> {
|
||||||
|
sender.sendProgress((double) completedTasks.get() / totalTasks, "Finding structures");
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
new Spiraler(d, d, (x, z) -> executor.queue(() -> {
|
||||||
|
var struct = engine.getStructureAt(x, z);
|
||||||
|
if (struct != null) {
|
||||||
|
data.computeIfAbsent(struct.getLoadKey(), (k) -> new KList<>()).add(new Position2(x, z));
|
||||||
|
}
|
||||||
|
completedTasks.incrementAndGet();
|
||||||
|
})).setOffset(loc.getBlockX(), loc.getBlockZ()).drain();
|
||||||
|
|
||||||
|
executor.complete();
|
||||||
|
multiBurst.close();
|
||||||
|
J.car(c);
|
||||||
|
|
||||||
|
for (var key : data.keySet()) {
|
||||||
|
var list = data.get(key);
|
||||||
|
KList<Long> distances = new KList<>(list.size() - 1);
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
var pos = list.get(i);
|
||||||
|
double dist = Integer.MAX_VALUE;
|
||||||
|
for (var p : list) {
|
||||||
|
if (p.equals(pos)) continue;
|
||||||
|
dist = Math.min(dist, Math.sqrt(Math.pow(pos.getX() - p.getX(), 2) + Math.pow(pos.getZ() - p.getZ(), 2)));
|
||||||
|
}
|
||||||
|
if (dist == Integer.MAX_VALUE) continue;
|
||||||
|
distances.add(Math.round(dist * 16));
|
||||||
|
}
|
||||||
|
long[] array = new long[distances.size()];
|
||||||
|
for (int i = 0; i < distances.size(); i++) {
|
||||||
|
array[i] = distances.get(i);
|
||||||
|
}
|
||||||
|
Arrays.sort(array);
|
||||||
|
long min = array.length > 0 ? array[0] : 0;
|
||||||
|
long max = array.length > 0 ? array[array.length - 1] : 0;
|
||||||
|
long sum = Arrays.stream(array).sum();
|
||||||
|
long avg = array.length > 0 ? Math.round(sum / (double) array.length) : 0;
|
||||||
|
String msg = "%s: %s => min: %s/max: %s -> avg: %s".formatted(key, list.size(), min, max, avg);
|
||||||
|
sender.sendMessage(msg);
|
||||||
|
}
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
sender.sendMessage(C.RED + "No data found!");
|
||||||
|
} else {
|
||||||
|
sender.sendMessage(C.GREEN + "Done!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Decree(description = "Render a world map (External GUI)", aliases = "render")
|
@Decree(description = "Render a world map (External GUI)", aliases = "render")
|
||||||
public void map(
|
public void map(
|
||||||
@Param(name = "world", description = "The world to open the generator for", contextual = true)
|
@Param(name = "world", description = "The world to open the generator for", contextual = true)
|
||||||
@@ -260,7 +483,15 @@ public class CommandStudio implements DecreeExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), 0);
|
Location center = sender().isPlayer() && player().getWorld().equals(world) ? player().getLocation() : null;
|
||||||
|
MapWindow.openMap(new PaletteExplorerStart(
|
||||||
|
IrisToolbelt.access(world).getEngine(),
|
||||||
|
world.getUID(),
|
||||||
|
sender().isPlayer() ? player().getUniqueId() : null,
|
||||||
|
null,
|
||||||
|
center == null ? 0D : center.getX(),
|
||||||
|
center == null ? 0D : center.getZ()
|
||||||
|
));
|
||||||
sender().sendMessage(C.GREEN + "Opening map!");
|
sender().sendMessage(C.GREEN + "Opening map!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,6 +512,7 @@ public class CommandStudio implements DecreeExecutor {
|
|||||||
@Param(description = "The dimension to profile", contextual = true, defaultValue = "default")
|
@Param(description = "The dimension to profile", contextual = true, defaultValue = "default")
|
||||||
IrisDimension dimension
|
IrisDimension dimension
|
||||||
) {
|
) {
|
||||||
|
// Todo: Make this more accurate
|
||||||
File pack = dimension.getLoadFile().getParentFile().getParentFile();
|
File pack = dimension.getLoadFile().getParentFile().getParentFile();
|
||||||
File report = Iris.instance.getDataFile("profile.txt");
|
File report = Iris.instance.getDataFile("profile.txt");
|
||||||
IrisProject project = new IrisProject(pack);
|
IrisProject project = new IrisProject(pack);
|
||||||
@@ -490,8 +722,14 @@ public class CommandStudio implements DecreeExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sender().sendMessage(C.GREEN + "Sending you to the studio world!");
|
sender().sendMessage(C.GREEN + "Sending you to the studio world!");
|
||||||
player().teleport(Iris.service(StudioSVC.class).getActiveProject().getActiveProvider().getTarget().getWorld().spawnLocation());
|
var player = player();
|
||||||
player().setGameMode(GameMode.SPECTATOR);
|
player.teleportAsync(Iris.service(StudioSVC.class)
|
||||||
|
.getActiveProject()
|
||||||
|
.getActiveProvider()
|
||||||
|
.getTarget()
|
||||||
|
.getWorld()
|
||||||
|
.spawnLocation()
|
||||||
|
).thenRun(() -> player.setGameMode(GameMode.SPECTATOR));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Decree(description = "Update your dimension projects VSCode workspace")
|
@Decree(description = "Update your dimension projects VSCode workspace")
|
||||||
@@ -544,7 +782,6 @@ public class CommandStudio implements DecreeExecutor {
|
|||||||
pw.println("Iris Version: " + Iris.instance.getDescription().getVersion());
|
pw.println("Iris Version: " + Iris.instance.getDescription().getVersion());
|
||||||
pw.println("Bukkit Version: " + Bukkit.getBukkitVersion());
|
pw.println("Bukkit Version: " + Bukkit.getBukkitVersion());
|
||||||
pw.println("MC Version: " + Bukkit.getVersion());
|
pw.println("MC Version: " + Bukkit.getVersion());
|
||||||
pw.println("PaperSpigot: " + (PaperLib.isPaper() ? "Yup!" : "Nope!"));
|
|
||||||
pw.println("Report Captured At: " + new Date());
|
pw.println("Report Captured At: " + new Date());
|
||||||
pw.println("Chunks: (" + chunks.size() + "): ");
|
pw.println("Chunks: (" + chunks.size() + "): ");
|
||||||
|
|
||||||
@@ -675,6 +912,10 @@ public class CommandStudio implements DecreeExecutor {
|
|||||||
sender().sendMessage(C.RED + "You must have server launched GUIs enabled in the settings!");
|
sender().sendMessage(C.RED + "You must have server launched GUIs enabled in the settings!");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (!GuiHost.isAvailable()) {
|
||||||
|
sender().sendMessage(C.RED + "This server runs without a desktop, so it cannot open windows!");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.commands;
|
||||||
|
|
||||||
|
import com.volmit.iris.Iris;
|
||||||
|
import com.volmit.iris.core.pregenerator.LazyPregenerator;
|
||||||
|
import com.volmit.iris.core.pregenerator.TurboPregenerator;
|
||||||
|
import com.volmit.iris.core.pregenerator.TurboPregenerator;
|
||||||
|
import com.volmit.iris.util.decree.DecreeExecutor;
|
||||||
|
import com.volmit.iris.util.decree.annotations.Decree;
|
||||||
|
import com.volmit.iris.util.decree.annotations.Param;
|
||||||
|
import com.volmit.iris.util.format.C;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.util.Vector;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Decree(name = "turbopregen", aliases = "turbo", description = "Pregenerate your Iris worlds!")
|
||||||
|
public class CommandTurboPregen implements DecreeExecutor {
|
||||||
|
public String worldName;
|
||||||
|
@Decree(description = "Pregenerate a world")
|
||||||
|
public void start(
|
||||||
|
@Param(description = "The radius of the pregen in blocks", aliases = "size")
|
||||||
|
int radius,
|
||||||
|
@Param(description = "The world to pregen", contextual = true)
|
||||||
|
World world,
|
||||||
|
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0")
|
||||||
|
Vector center
|
||||||
|
) {
|
||||||
|
|
||||||
|
worldName = world.getName();
|
||||||
|
File worldDirectory = world.getWorldFolder();
|
||||||
|
File TurboFile = new File(worldDirectory, "turbogen.json");
|
||||||
|
if (TurboFile.exists()) {
|
||||||
|
if (TurboPregenerator.getInstance() != null) {
|
||||||
|
sender().sendMessage(C.BLUE + "Turbo pregen is already in progress");
|
||||||
|
Iris.info(C.YELLOW + "Turbo pregen is already in progress");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
TurboFile.delete();
|
||||||
|
} catch (Exception e){
|
||||||
|
Iris.error("Failed to delete the old instance file of Turbo Pregen!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (sender().isPlayer() && access() == null) {
|
||||||
|
sender().sendMessage(C.RED + "The engine access for this world is null!");
|
||||||
|
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
|
||||||
|
}
|
||||||
|
|
||||||
|
TurboPregenerator.TurboPregenJob pregenJob = TurboPregenerator.TurboPregenJob.builder()
|
||||||
|
.world(worldName)
|
||||||
|
.radiusBlocks(radius)
|
||||||
|
.position(0)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
File TurboGenFile = new File(worldDirectory, "turbogen.json");
|
||||||
|
TurboPregenerator pregenerator = new TurboPregenerator(pregenJob, TurboGenFile);
|
||||||
|
pregenerator.start();
|
||||||
|
|
||||||
|
String msg = C.GREEN + "TurboPregen started in " + C.GOLD + worldName + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
|
||||||
|
sender().sendMessage(msg);
|
||||||
|
Iris.info(msg);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
sender().sendMessage(C.RED + "Epic fail. See console.");
|
||||||
|
Iris.reportError(e);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Stop the active pregeneration task", aliases = "x")
|
||||||
|
public void stop(@Param(aliases = "world", description = "The world to pause") World world) throws IOException {
|
||||||
|
TurboPregenerator turboPregenInstance = TurboPregenerator.getInstance();
|
||||||
|
File worldDirectory = world.getWorldFolder();
|
||||||
|
File turboFile = new File(worldDirectory, "turbogen.json");
|
||||||
|
|
||||||
|
if (turboPregenInstance != null) {
|
||||||
|
turboPregenInstance.shutdownInstance(world);
|
||||||
|
sender().sendMessage(C.LIGHT_PURPLE + "Closed Turbogen instance for " + world.getName());
|
||||||
|
} else if (turboFile.exists() && turboFile.delete()) {
|
||||||
|
sender().sendMessage(C.LIGHT_PURPLE + "Closed Turbogen instance for " + world.getName());
|
||||||
|
} else if (turboFile.exists()) {
|
||||||
|
Iris.error("Failed to delete the old instance file of Turbo Pregen!");
|
||||||
|
} else {
|
||||||
|
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Decree(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"})
|
||||||
|
public void pause(
|
||||||
|
@Param(aliases = "world", description = "The world to pause")
|
||||||
|
World world
|
||||||
|
) {
|
||||||
|
if (TurboPregenerator.getInstance() != null) {
|
||||||
|
TurboPregenerator.setPausedTurbo(world);
|
||||||
|
sender().sendMessage(C.GREEN + "Paused/unpaused Turbo Pregen, now: " + (TurboPregenerator.isPausedTurbo(world) ? "Paused" : "Running") + ".");
|
||||||
|
} else {
|
||||||
|
File worldDirectory = world.getWorldFolder();
|
||||||
|
File TurboFile = new File(worldDirectory, "turbogen.json");
|
||||||
|
if (TurboFile.exists()){
|
||||||
|
TurboPregenerator.loadTurboGenerator(world.getName());
|
||||||
|
sender().sendMessage(C.YELLOW + "Started Turbo Pregen back up!");
|
||||||
|
} else {
|
||||||
|
sender().sendMessage(C.YELLOW + "No active Turbo Pregen tasks to pause/unpause.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.commands;
|
||||||
|
|
||||||
|
import lombok.Synchronized;
|
||||||
|
import org.bukkit.World;
|
||||||
|
|
||||||
|
import com.volmit.iris.Iris;
|
||||||
|
import com.volmit.iris.core.pregenerator.ChunkUpdater;
|
||||||
|
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||||
|
import com.volmit.iris.util.decree.DecreeExecutor;
|
||||||
|
import com.volmit.iris.util.decree.DecreeOrigin;
|
||||||
|
import com.volmit.iris.util.decree.annotations.Decree;
|
||||||
|
import com.volmit.iris.util.decree.annotations.Param;
|
||||||
|
import com.volmit.iris.util.format.C;
|
||||||
|
import com.volmit.iris.util.format.Form;
|
||||||
|
|
||||||
|
@Decree(name = "updater", origin = DecreeOrigin.BOTH, description = "Iris World Updater")
|
||||||
|
public class CommandUpdater implements DecreeExecutor {
|
||||||
|
private final Object lock = new Object();
|
||||||
|
private transient ChunkUpdater chunkUpdater;
|
||||||
|
|
||||||
|
@Decree(description = "Updates all chunk in the specified world")
|
||||||
|
public void start(
|
||||||
|
@Param(description = "World to update chunks at", contextual = true)
|
||||||
|
World world
|
||||||
|
) {
|
||||||
|
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||||
|
sender().sendMessage(C.GOLD + "This is not an Iris world");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (lock) {
|
||||||
|
if (chunkUpdater != null) {
|
||||||
|
chunkUpdater.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkUpdater = new ChunkUpdater(world);
|
||||||
|
if (sender().isPlayer()) {
|
||||||
|
sender().sendMessage(C.GREEN + "Updating " + world.getName() + C.GRAY + " Total chunks: " + Form.f(chunkUpdater.getChunks()));
|
||||||
|
} else {
|
||||||
|
Iris.info(C.GREEN + "Updating " + world.getName() + C.GRAY + " Total chunks: " + Form.f(chunkUpdater.getChunks()));
|
||||||
|
}
|
||||||
|
chunkUpdater.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized("lock")
|
||||||
|
@Decree(description = "Pause the updater")
|
||||||
|
public void pause( ) {
|
||||||
|
if (chunkUpdater == null) {
|
||||||
|
sender().sendMessage(C.GOLD + "You cant pause something that doesnt exist?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean status = chunkUpdater.pause();
|
||||||
|
if (sender().isPlayer()) {
|
||||||
|
if (status) {
|
||||||
|
sender().sendMessage(C.IRIS + "Paused task for: " + C.GRAY + chunkUpdater.getName());
|
||||||
|
} else {
|
||||||
|
sender().sendMessage(C.IRIS + "Unpause task for: " + C.GRAY + chunkUpdater.getName());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (status) {
|
||||||
|
Iris.info(C.IRIS + "Paused task for: " + C.GRAY + chunkUpdater.getName());
|
||||||
|
} else {
|
||||||
|
Iris.info(C.IRIS + "Unpause task for: " + C.GRAY + chunkUpdater.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized("lock")
|
||||||
|
@Decree(description = "Stops the updater")
|
||||||
|
public void stop() {
|
||||||
|
if (chunkUpdater == null) {
|
||||||
|
sender().sendMessage(C.GOLD + "You cant stop something that doesnt exist?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sender().isPlayer()) {
|
||||||
|
sender().sendMessage("Stopping Updater for: " + C.GRAY + chunkUpdater.getName());
|
||||||
|
} else {
|
||||||
|
Iris.info("Stopping Updater for: " + C.GRAY + chunkUpdater.getName());
|
||||||
|
}
|
||||||
|
chunkUpdater.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+17
-1
@@ -22,7 +22,9 @@ import com.volmit.iris.Iris;
|
|||||||
import com.volmit.iris.core.edit.BlockSignal;
|
import com.volmit.iris.core.edit.BlockSignal;
|
||||||
import com.volmit.iris.core.nms.INMS;
|
import com.volmit.iris.core.nms.INMS;
|
||||||
import com.volmit.iris.core.tools.IrisToolbelt;
|
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||||
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
import com.volmit.iris.engine.object.IrisBiome;
|
import com.volmit.iris.engine.object.IrisBiome;
|
||||||
|
import com.volmit.iris.engine.object.IrisRegion;
|
||||||
import com.volmit.iris.util.data.B;
|
import com.volmit.iris.util.data.B;
|
||||||
import com.volmit.iris.util.decree.DecreeExecutor;
|
import com.volmit.iris.util.decree.DecreeExecutor;
|
||||||
import com.volmit.iris.util.decree.DecreeOrigin;
|
import com.volmit.iris.util.decree.DecreeOrigin;
|
||||||
@@ -37,6 +39,7 @@ import org.bukkit.Material;
|
|||||||
import org.bukkit.block.Biome;
|
import org.bukkit.block.Biome;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
@Decree(name = "what", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris What?")
|
@Decree(name = "what", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris What?")
|
||||||
@@ -82,6 +85,19 @@ public class CommandWhat implements DecreeExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Decree(description = "What region am i in?", origin = DecreeOrigin.PLAYER)
|
||||||
|
public void region() {
|
||||||
|
try {
|
||||||
|
Chunk chunk = world().getChunkAt(player().getLocation().getBlockZ() / 16, player().getLocation().getBlockZ() / 16);
|
||||||
|
IrisRegion r = engine().getRegion(chunk);
|
||||||
|
sender().sendMessage("IRegion: " + r.getLoadKey() + " (" + r.getName() + ")");
|
||||||
|
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
sender().sendMessage(C.IRIS + "Iris worlds only.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Decree(description = "What block am i looking at?", origin = DecreeOrigin.PLAYER)
|
@Decree(description = "What block am i looking at?", origin = DecreeOrigin.PLAYER)
|
||||||
public void block() {
|
public void block() {
|
||||||
BlockData bd;
|
BlockData bd;
|
||||||
@@ -147,7 +163,7 @@ public class CommandWhat implements DecreeExecutor {
|
|||||||
|
|
||||||
sender().sendMessage("Found " + v.get() + " Nearby Markers (" + marker + ")");
|
sender().sendMessage("Found " + v.get() + " Nearby Markers (" + marker + ")");
|
||||||
} else {
|
} else {
|
||||||
sender().sendMessage("Iris worlds only.");
|
sender().sendMessage(C.IRIS + "Iris worlds only.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+7
-7
@@ -61,14 +61,14 @@ public class JigsawEditor implements Listener {
|
|||||||
private Location target;
|
private Location target;
|
||||||
|
|
||||||
public JigsawEditor(Player player, IrisJigsawPiece piece, IrisObject object, File saveLocation) {
|
public JigsawEditor(Player player, IrisJigsawPiece piece, IrisObject object, File saveLocation) {
|
||||||
if (editors.containsKey(player)) {
|
if (object == null) throw new RuntimeException("Object is null! " + piece.getObject());
|
||||||
editors.get(player).close();
|
editors.compute(player, ($, current) -> {
|
||||||
}
|
if (current != null) {
|
||||||
|
current.exit();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
});
|
||||||
|
|
||||||
editors.put(player, this);
|
|
||||||
if (object == null) {
|
|
||||||
throw new RuntimeException("Object is null! " + piece.getObject());
|
|
||||||
}
|
|
||||||
this.object = object;
|
this.object = object;
|
||||||
this.player = player;
|
this.player = player;
|
||||||
origin = player.getLocation().clone().add(0, 7, 0);
|
origin = player.getLocation().clone().add(0, 7, 0);
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,113 @@
|
|||||||
|
package com.volmit.iris.core.events;
|
||||||
|
|
||||||
|
import com.volmit.iris.Iris;
|
||||||
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
|
import com.volmit.iris.engine.object.InventorySlotType;
|
||||||
|
import com.volmit.iris.engine.object.IrisLootTable;
|
||||||
|
import com.volmit.iris.util.collection.KList;
|
||||||
|
import com.volmit.iris.util.scheduling.J;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.bukkit.*;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.event.Event;
|
||||||
|
import org.bukkit.event.HandlerList;
|
||||||
|
import org.bukkit.event.world.LootGenerateEvent;
|
||||||
|
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.InventoryHolder;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.loot.LootContext;
|
||||||
|
import org.bukkit.loot.LootTable;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class IrisLootEvent extends Event {
|
||||||
|
private static final HandlerList handlers = new HandlerList();
|
||||||
|
private static final LootTable EMPTY = new LootTable() {
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public NamespacedKey getKey() {
|
||||||
|
return new NamespacedKey(Iris.instance, "empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public Collection<ItemStack> populateLoot(@Nullable Random random, @NotNull LootContext context) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fillInventory(@NotNull Inventory inventory, @Nullable Random random, @NotNull LootContext context) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final Engine engine;
|
||||||
|
private final Block block;
|
||||||
|
private final InventorySlotType slot;
|
||||||
|
private final KList<IrisLootTable> tables;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for IrisLootEvent with mode selection.
|
||||||
|
*
|
||||||
|
* @param engine The engine instance.
|
||||||
|
* @param block The block associated with the event.
|
||||||
|
* @param slot The inventory slot type.
|
||||||
|
* @param tables The list of IrisLootTables. (mutable*)
|
||||||
|
*/
|
||||||
|
public IrisLootEvent(Engine engine, Block block, InventorySlotType slot, KList<IrisLootTable> tables) {
|
||||||
|
this.engine = engine;
|
||||||
|
this.block = block;
|
||||||
|
this.slot = slot;
|
||||||
|
this.tables = tables;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HandlerList getHandlers() {
|
||||||
|
return handlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required method to get the HandlerList for this event.
|
||||||
|
*
|
||||||
|
* @return The HandlerList.
|
||||||
|
*/
|
||||||
|
public static HandlerList getHandlerList() {
|
||||||
|
return handlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers the corresponding Bukkit loot event.
|
||||||
|
* This method integrates your custom IrisLootTables with Bukkit's LootGenerateEvent,
|
||||||
|
* allowing other plugins to modify or cancel the loot generation.
|
||||||
|
*
|
||||||
|
* @return true when the event was canceled
|
||||||
|
*/
|
||||||
|
public static boolean callLootEvent(KList<ItemStack> loot, Inventory inv, World world, int x, int y, int z) {
|
||||||
|
InventoryHolder holder = inv.getHolder();
|
||||||
|
Location loc = new Location(world, x, y, z);
|
||||||
|
if (holder == null) {
|
||||||
|
holder = new InventoryHolder() {
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inv;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
LootContext context = new LootContext.Builder(loc).build();
|
||||||
|
LootGenerateEvent event = new LootGenerateEvent(world, null, holder, EMPTY, context, loot, true);
|
||||||
|
if (!Bukkit.isPrimaryThread()) {
|
||||||
|
Iris.warn("LootGenerateEvent was not called on the main thread, please report this issue.");
|
||||||
|
Thread.dumpStack();
|
||||||
|
J.sfut(() -> Bukkit.getPluginManager().callEvent(event)).join();
|
||||||
|
} else Bukkit.getPluginManager().callEvent(event);
|
||||||
|
|
||||||
|
return event.isCancelled();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.PickMeasuring.TypedSpread;
|
||||||
|
import com.volmit.iris.core.gui.components.PickDisplay;
|
||||||
|
import com.volmit.iris.engine.IrisComplex;
|
||||||
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
|
import com.volmit.iris.engine.object.InferredType;
|
||||||
|
import com.volmit.iris.engine.object.IrisRegion;
|
||||||
|
import com.volmit.iris.util.stream.ProceduralStream;
|
||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The biome view's explorer: every region picks the biomes on its ground from its own biome lists, so for each region
|
||||||
|
* in view it shows how its noise, or in hybrid terrain the height, picks them.
|
||||||
|
*/
|
||||||
|
final class BiomeExplorer {
|
||||||
|
private final Supplier<Engine> engineSource;
|
||||||
|
private final PickInspectorPanel inspector;
|
||||||
|
private final LatestMeasurement<Measurement> measurer = new LatestMeasurement<>("Iris Biome Explorer");
|
||||||
|
private MapCensus.ViewArea latestArea;
|
||||||
|
private MapCensus.ViewArea measuredArea;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param onDisplayChosen receives how the map should draw the picks
|
||||||
|
*/
|
||||||
|
BiomeExplorer(Supplier<Engine> engineSource, Consumer<PickDisplay> onDisplayChosen) {
|
||||||
|
this.engineSource = engineSource;
|
||||||
|
this.inspector = new PickInspectorPanel("Biomes", onDisplayChosen);
|
||||||
|
}
|
||||||
|
|
||||||
|
PickInspectorPanel inspector() {
|
||||||
|
return inspector;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the regions in this area and how they pick their biomes, unless that was already done.
|
||||||
|
*/
|
||||||
|
void measure(MapCensus.ViewArea area) {
|
||||||
|
latestArea = area;
|
||||||
|
if (area == null || area.equals(measuredArea)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
measuredArea = area;
|
||||||
|
measurer.measure(() -> measurePicks(area), this::show);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pack changed, so the regions and their biomes are read again.
|
||||||
|
*/
|
||||||
|
void refresh() {
|
||||||
|
measuredArea = null;
|
||||||
|
measure(latestArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() {
|
||||||
|
measurer.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void show(Measurement measurement) {
|
||||||
|
List<PickInspectorPanel.Section> sections = new ArrayList<>();
|
||||||
|
for (RegionPicks region : measurement.regions()) {
|
||||||
|
List<JComponent> charts = region.coverage() != null
|
||||||
|
? List.of(PickInspectorPanel.heightChart(region.coverage()))
|
||||||
|
: region.spreads().stream()
|
||||||
|
.map(spread -> (JComponent) inspector.noiseChart(spread, "Biomes", region.key() + "/" + spread.title()))
|
||||||
|
.toList();
|
||||||
|
sections.add(new PickInspectorPanel.Section(region.key(), region.name(), region.color(), region.share(),
|
||||||
|
biomeCountText(region.biomeCount()), charts, region.notes()));
|
||||||
|
}
|
||||||
|
String detail = measurement.hybrid()
|
||||||
|
? "Hybrid terrain · each region picks biomes by height"
|
||||||
|
: "Each region splits its own biome noise by rarity";
|
||||||
|
inspector.showRegionSections(detail, sections, measurement.notes());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String biomeCountText(int count) {
|
||||||
|
return count == 1 ? "1 biome" : count + " biomes";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Measurement measurePicks(MapCensus.ViewArea area) {
|
||||||
|
Engine engine = engineSource.get();
|
||||||
|
IrisComplex complex = engine == null ? null : engine.getComplex();
|
||||||
|
if (complex == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Map<IrisRegion, RegionSamples> samplesByRegion = new IdentityHashMap<>();
|
||||||
|
int[] total = {0};
|
||||||
|
PickMeasuring.forEachSample(area, (x, z) -> {
|
||||||
|
IrisRegion region = complex.getRegionStream().get(x, z);
|
||||||
|
if (region == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
total[0]++;
|
||||||
|
samplesByRegion.computeIfAbsent(region, subject -> new RegionSamples(engine, complex, subject)).add(x, z);
|
||||||
|
});
|
||||||
|
List<RegionPicks> regions = samplesByRegion.values().stream()
|
||||||
|
.sorted(Comparator.comparingInt(RegionSamples::count).reversed())
|
||||||
|
.map(samples -> samples.picks(engine, total[0]))
|
||||||
|
.toList();
|
||||||
|
List<String> notes = new ArrayList<>();
|
||||||
|
if (complex.isHybridTerrain()) {
|
||||||
|
notes.add(PickMeasuring.hybridNoiseNote());
|
||||||
|
} else {
|
||||||
|
PickMeasuring.addRarityNotes(notes, complex, regions.stream().flatMap(region -> region.spreads().stream()).toList());
|
||||||
|
}
|
||||||
|
notes.add(PickMeasuring.measuredNote());
|
||||||
|
return new Measurement(complex.isHybridTerrain(), regions, notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The spots in view one region owns. The biome view shows each spot's base biome, which the region picks from the
|
||||||
|
* biome list of the spot's continent type.
|
||||||
|
*/
|
||||||
|
private static final class RegionSamples {
|
||||||
|
private final IrisComplex complex;
|
||||||
|
private final IrisRegion region;
|
||||||
|
private final List<TypedSpread> spreads;
|
||||||
|
private final Map<InferredType, ProceduralStream<Double>> noiseByType = new EnumMap<>(InferredType.class);
|
||||||
|
private final List<Integer> pickingHeights = new ArrayList<>();
|
||||||
|
private int count;
|
||||||
|
|
||||||
|
private RegionSamples(Engine engine, IrisComplex complex, IrisRegion region) {
|
||||||
|
this.complex = complex;
|
||||||
|
this.region = region;
|
||||||
|
this.spreads = complex.isHybridTerrain() ? List.of() : PickMeasuring.biomeSpreads(engine, complex, region);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void add(double x, double z) {
|
||||||
|
count++;
|
||||||
|
if (complex.isHybridTerrain()) {
|
||||||
|
pickingHeights.add((int) Math.round(complex.getPlacementHeightStream().get(x, z)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
InferredType type = complex.getBridgeStream().get(x, z);
|
||||||
|
for (TypedSpread typed : spreads) {
|
||||||
|
if (typed.type() == type) {
|
||||||
|
typed.spread().add(noiseByType.computeIfAbsent(type, shown -> complex.biomePickNoiseStream(region, shown)).get(x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int count() {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RegionPicks picks(Engine engine, int totalCount) {
|
||||||
|
double share = totalCount == 0 ? 0D : count / (double) totalCount;
|
||||||
|
String key = region.getLoadKey();
|
||||||
|
String name = region.getName();
|
||||||
|
int color = PickMeasuring.regionColor(engine, region);
|
||||||
|
List<String> notes = new ArrayList<>();
|
||||||
|
if (complex.isHybridTerrain()) {
|
||||||
|
List<RegionHeightCoverage.BiomeRange> ranges = PickMeasuring.biomeRanges(engine, region);
|
||||||
|
RegionHeightCoverage coverage = RegionHeightCoverage.measure(ranges,
|
||||||
|
pickingHeights.stream().mapToInt(Integer::intValue).toArray());
|
||||||
|
notes.addAll(coverage.notes());
|
||||||
|
return new RegionPicks(key, name, color, share, ranges.size(), List.of(), coverage, notes);
|
||||||
|
}
|
||||||
|
List<TypedSpread> inView = spreads.stream().filter(typed -> typed.spread().sampleCount() > 0).toList();
|
||||||
|
PickMeasuring.addBiomeSpreadNotes(notes, inView);
|
||||||
|
PickMeasuring.addChildBiomeNote(notes, complex, region);
|
||||||
|
int biomeCount = (int) inView.stream()
|
||||||
|
.flatMap(typed -> typed.spread().rows().stream())
|
||||||
|
.map(row -> row.option().key())
|
||||||
|
.distinct()
|
||||||
|
.count();
|
||||||
|
return new RegionPicks(key, name, color, share, biomeCount, inView.stream().map(TypedSpread::spread).toList(),
|
||||||
|
null, notes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param coverage how the region's biomes cover the heights in view in hybrid terrain, or null in biome terrain
|
||||||
|
*/
|
||||||
|
private record RegionPicks(String key, String name, int color, double share, int biomeCount,
|
||||||
|
List<NoisePickSpread> spreads, RegionHeightCoverage coverage, List<String> notes) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Measurement(boolean hybrid, List<RegionPicks> regions, List<String> notes) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
// A copy of Minecraft's Biome.BIOME_INFO_NOISE, the 2D simplex noise swamps use to pick between their two grass colours.
|
||||||
|
// Minecraft seeds it through a LegacyRandomSource, which draws the same numbers as java.util.Random.
|
||||||
|
final class BiomeInfoNoise {
|
||||||
|
private static final long SEED = 2345L;
|
||||||
|
private static final int PERMUTATION_SIZE = 256;
|
||||||
|
private static final int GRADIENT_COUNT = 12;
|
||||||
|
private static final double SKEW = 0.5D * (Math.sqrt(3D) - 1D);
|
||||||
|
private static final double UNSKEW = (3D - Math.sqrt(3D)) / 6D;
|
||||||
|
private static final double CORNER_RADIUS = 0.5D;
|
||||||
|
private static final double OUTPUT_SCALE = 70D;
|
||||||
|
private static final int[][] GRADIENTS = {
|
||||||
|
{1, 1}, {-1, 1}, {1, -1}, {-1, -1},
|
||||||
|
{1, 0}, {-1, 0}, {1, 0}, {-1, 0},
|
||||||
|
{0, 1}, {0, -1}, {0, 1}, {0, -1}
|
||||||
|
};
|
||||||
|
private static final int[] PERMUTATION = createPermutation();
|
||||||
|
|
||||||
|
private BiomeInfoNoise() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static float at(double x, double z) {
|
||||||
|
double skewOffset = (x + z) * SKEW;
|
||||||
|
int cellX = (int) Math.floor(x + skewOffset);
|
||||||
|
int cellZ = (int) Math.floor(z + skewOffset);
|
||||||
|
double unskewOffset = (cellX + cellZ) * UNSKEW;
|
||||||
|
double x0 = x - (cellX - unskewOffset);
|
||||||
|
double z0 = z - (cellZ - unskewOffset);
|
||||||
|
int stepX = x0 > z0 ? 1 : 0;
|
||||||
|
int stepZ = 1 - stepX;
|
||||||
|
double x1 = x0 - stepX + UNSKEW;
|
||||||
|
double z1 = z0 - stepZ + UNSKEW;
|
||||||
|
double x2 = x0 - 1D + 2D * UNSKEW;
|
||||||
|
double z2 = z0 - 1D + 2D * UNSKEW;
|
||||||
|
int wrappedX = cellX & 0xFF;
|
||||||
|
int wrappedZ = cellZ & 0xFF;
|
||||||
|
int gradient0 = permute(wrappedX + permute(wrappedZ)) % GRADIENT_COUNT;
|
||||||
|
int gradient1 = permute(wrappedX + stepX + permute(wrappedZ + stepZ)) % GRADIENT_COUNT;
|
||||||
|
int gradient2 = permute(wrappedX + 1 + permute(wrappedZ + 1)) % GRADIENT_COUNT;
|
||||||
|
return (float) (OUTPUT_SCALE * (corner(gradient0, x0, z0) + corner(gradient1, x1, z1) + corner(gradient2, x2, z2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double corner(int gradient, double x, double z) {
|
||||||
|
double falloff = CORNER_RADIUS - x * x - z * z;
|
||||||
|
if (falloff < 0D) {
|
||||||
|
return 0D;
|
||||||
|
}
|
||||||
|
falloff *= falloff;
|
||||||
|
return falloff * falloff * (GRADIENTS[gradient][0] * x + GRADIENTS[gradient][1] * z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int permute(int index) {
|
||||||
|
return PERMUTATION[index & 0xFF];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minecraft draws three offsets first and discards them for this noise, so they are drawn here too to stay in step.
|
||||||
|
private static int[] createPermutation() {
|
||||||
|
Random random = new Random(SEED);
|
||||||
|
for (int discardedOffset = 0; discardedOffset < 3; discardedOffset++) {
|
||||||
|
random.nextDouble();
|
||||||
|
}
|
||||||
|
int[] permutation = new int[PERMUTATION_SIZE];
|
||||||
|
for (int i = 0; i < PERMUTATION_SIZE; i++) {
|
||||||
|
permutation[i] = i;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < PERMUTATION_SIZE; i++) {
|
||||||
|
int swap = i + random.nextInt(PERMUTATION_SIZE - i);
|
||||||
|
int previous = permutation[i];
|
||||||
|
permutation[i] = permutation[swap];
|
||||||
|
permutation[swap] = previous;
|
||||||
|
}
|
||||||
|
return permutation;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.engine.object.IrisBiome;
|
||||||
|
import com.volmit.iris.engine.object.IrisBiomePaletteLayer;
|
||||||
|
import com.volmit.iris.engine.object.IrisBlockData;
|
||||||
|
import com.volmit.iris.engine.object.IrisDecorator;
|
||||||
|
import com.volmit.iris.util.collection.KList;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One of the block palettes a biome places: a surface, sea or cave ceiling layer, its wall or slab blocks, or a
|
||||||
|
* decorator.
|
||||||
|
*/
|
||||||
|
record BiomePalette(String biomeKey, Kind kind, int index) {
|
||||||
|
private static final String ID_PREFIX = "palette:";
|
||||||
|
private static final char ID_SEPARATOR = '#';
|
||||||
|
private static final String SNIPPET_PREFIX = "snippet/";
|
||||||
|
|
||||||
|
enum Kind {
|
||||||
|
LAYER("Layer", "layers", SnippetFolder.BLOCK_PALETTES, true, true),
|
||||||
|
SEA_LAYER("Sea layer", "seaLayers", SnippetFolder.BLOCK_PALETTES, true, true),
|
||||||
|
CAVE_CEILING_LAYER("Cave ceiling layer", "caveCeilingLayers", SnippetFolder.BLOCK_PALETTES, true, true),
|
||||||
|
WALL("Wall", "wall", SnippetFolder.BLOCK_PALETTES, false, false),
|
||||||
|
SLAB("Slab", "slab", SnippetFolder.BLOCK_PALETTES, false, false),
|
||||||
|
DECORATOR("Decorator", "decorators", SnippetFolder.DECORATORS, false, true);
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
private final String jsonKey;
|
||||||
|
private final SnippetFolder snippetFolder;
|
||||||
|
private final boolean stacked;
|
||||||
|
private final boolean numbered;
|
||||||
|
|
||||||
|
Kind(String label, String jsonKey, SnippetFolder snippetFolder, boolean stacked, boolean numbered) {
|
||||||
|
this.label = label;
|
||||||
|
this.jsonKey = jsonKey;
|
||||||
|
this.snippetFolder = snippetFolder;
|
||||||
|
this.stacked = stacked;
|
||||||
|
this.numbered = numbered;
|
||||||
|
}
|
||||||
|
|
||||||
|
SnippetFolder snippetFolder() {
|
||||||
|
return snippetFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the palette is a stack of blocks with a thickness, rather than single blocks on walls or slopes.
|
||||||
|
*/
|
||||||
|
boolean isStacked() {
|
||||||
|
return stacked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The biome file's key holding these palettes: a list of them, or a single one for walls and slabs.
|
||||||
|
*/
|
||||||
|
String jsonKey() {
|
||||||
|
return jsonKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A biome without wall or slab blocks still has those palettes, just empty.
|
||||||
|
List<IrisBiomePaletteLayer> layersOf(IrisBiome biome) {
|
||||||
|
return switch (this) {
|
||||||
|
case LAYER -> biome.getLayers();
|
||||||
|
case SEA_LAYER -> biome.getSeaLayers();
|
||||||
|
case CAVE_CEILING_LAYER -> biome.getCaveCeilingLayers();
|
||||||
|
case WALL -> biome.getWall().getPalette().isEmpty() ? List.of() : List.of(biome.getWall());
|
||||||
|
case SLAB -> biome.getSlab().getPalette().isEmpty() ? List.of() : List.of(biome.getSlab());
|
||||||
|
case DECORATOR -> List.of();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// A decorator snippet that couldn't be found loads as an empty entry, which places nothing.
|
||||||
|
List<KList<IrisBlockData>> blockListsOf(IrisBiome biome) {
|
||||||
|
if (this != DECORATOR) {
|
||||||
|
return layersOf(biome).stream().map(IrisBiomePaletteLayer::getPalette).toList();
|
||||||
|
}
|
||||||
|
return biome.getDecorators().stream()
|
||||||
|
.map(decorator -> decorator == null ? new KList<IrisBlockData>() : decorator.getPalette())
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The folders under snippet/ that palettes can come from, named after the Iris type they hold.
|
||||||
|
*/
|
||||||
|
enum SnippetFolder {
|
||||||
|
BLOCK_PALETTES("biome-palette", "Block palettes"),
|
||||||
|
DECORATORS("decorator", "Decorators");
|
||||||
|
|
||||||
|
private final String folder;
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
SnippetFolder(String folder, String label) {
|
||||||
|
this.folder = folder;
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
String folder() {
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
String label() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The snippet's path inside this folder. Packs may leave the folder out, the way the snippet loader allows.
|
||||||
|
*/
|
||||||
|
String pathOf(String reference) {
|
||||||
|
String path = reference.startsWith(SNIPPET_PREFIX) ? reference.substring(SNIPPET_PREFIX.length()) : reference;
|
||||||
|
return path.startsWith(folder + "/") ? path.substring(folder.length() + 1) : path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<BiomePalette> allOf(String biomeKey, IrisBiome biome) {
|
||||||
|
List<BiomePalette> palettes = new ArrayList<>();
|
||||||
|
for (Kind kind : Kind.values()) {
|
||||||
|
List<KList<IrisBlockData>> blockLists = kind.blockListsOf(biome);
|
||||||
|
for (int index = 0; index < blockLists.size(); index++) {
|
||||||
|
if (!blockLists.get(index).isEmpty()) {
|
||||||
|
palettes.add(new BiomePalette(biomeKey, kind, index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(palettes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BiomePalette fromId(String id) {
|
||||||
|
if (id == null || !id.startsWith(ID_PREFIX)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int indexSeparator = id.lastIndexOf(ID_SEPARATOR);
|
||||||
|
int kindSeparator = id.lastIndexOf(ID_SEPARATOR, indexSeparator - 1);
|
||||||
|
if (kindSeparator < ID_PREFIX.length()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new BiomePalette(
|
||||||
|
id.substring(ID_PREFIX.length(), kindSeparator),
|
||||||
|
Kind.valueOf(id.substring(kindSeparator + 1, indexSeparator)),
|
||||||
|
Integer.parseInt(id.substring(indexSeparator + 1))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean belongsTo(String id, String biomeKey) {
|
||||||
|
return id != null && id.startsWith(ID_PREFIX + biomeKey + ID_SEPARATOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
String id() {
|
||||||
|
return ID_PREFIX + biomeKey + ID_SEPARATOR + kind.name() + ID_SEPARATOR + index;
|
||||||
|
}
|
||||||
|
|
||||||
|
String label() {
|
||||||
|
return kind.numbered ? kind.label + " " + index : kind.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
String lowercaseLabel() {
|
||||||
|
return label().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The palette's blocks in the given copy of the biome, or null when a hotload removed it.
|
||||||
|
*/
|
||||||
|
KList<IrisBlockData> blocksIn(IrisBiome biome) {
|
||||||
|
List<KList<IrisBlockData>> blockLists = kind.blockListsOf(biome);
|
||||||
|
return index < blockLists.size() ? blockLists.get(index) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisBiomePaletteLayer layerIn(IrisBiome biome) {
|
||||||
|
return kind.layersOf(biome).get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisDecorator decoratorIn(IrisBiome biome) {
|
||||||
|
return biome.getDecorators().get(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.loader.IrisData;
|
||||||
|
import com.volmit.iris.engine.decorator.IrisEngineDecorator;
|
||||||
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
|
import com.volmit.iris.engine.object.IrisBiome;
|
||||||
|
import com.volmit.iris.engine.object.IrisBiomePaletteLayer;
|
||||||
|
import com.volmit.iris.engine.object.IrisBlockData;
|
||||||
|
import com.volmit.iris.engine.object.IrisDecorationPart;
|
||||||
|
import com.volmit.iris.engine.object.IrisDecorator;
|
||||||
|
import com.volmit.iris.engine.object.IrisDimension;
|
||||||
|
import com.volmit.iris.util.collection.KList;
|
||||||
|
import com.volmit.iris.util.math.RNG;
|
||||||
|
import com.volmit.iris.util.noise.CNG;
|
||||||
|
import org.bukkit.block.data.Bisected;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How thick one biome palette is at a spot and which noise picks its block there, sampled exactly the way the
|
||||||
|
* generator does for that kind of palette. The generator seeds each palette's noise from the first call it makes,
|
||||||
|
* so these use the same random sources the terrain, carving, post and decorator passes use.
|
||||||
|
*/
|
||||||
|
abstract class BiomePalettePlacement {
|
||||||
|
protected final IrisData data;
|
||||||
|
|
||||||
|
private BiomePalettePlacement(IrisData data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BiomePalettePlacement of(Engine engine, IrisBiome biome, BiomePalette palette) {
|
||||||
|
IrisData data = engine.getData();
|
||||||
|
RNG terrainRandom = new RNG(engine.getSeedManager().getTerrain());
|
||||||
|
RNG carveRandom = new RNG(engine.getSeedManager().getCarve());
|
||||||
|
RNG postRandom = new RNG(engine.getSeedManager().getPost());
|
||||||
|
int seaLevel = seaLevel(engine.getDimension());
|
||||||
|
return switch (palette.kind()) {
|
||||||
|
case LAYER -> new SurfaceLayer(biome, palette.index(), palette.layerIn(biome), terrainRandom, data);
|
||||||
|
case SEA_LAYER -> new LegacyStackedLayer(palette.layerIn(biome), data, terrainRandom.nextParallelRNG(palette.index()),
|
||||||
|
biome.getLayerSeaHeightGenerators(terrainRandom, data), palette.index());
|
||||||
|
case CAVE_CEILING_LAYER -> new LegacyStackedLayer(palette.layerIn(biome), data, carveRandom.nextParallelRNG(palette.index()),
|
||||||
|
biome.getLayerHeightGenerators(terrainRandom, data), palette.index());
|
||||||
|
case WALL -> new SingleBlocks(palette.layerIn(biome), data, carveRandom, seaLevel);
|
||||||
|
case SLAB -> new SingleBlocks(palette.layerIn(biome), data, postRandom, seaLevel);
|
||||||
|
case DECORATOR -> new Decorations(biome, palette.decoratorIn(biome), engine.getSeedManager().getComponent(),
|
||||||
|
seaLevel, data);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int seaLevel(IrisDimension dimension) {
|
||||||
|
return dimension == null ? 0 : dimension.getFluidHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract KList<IrisBlockData> blocks();
|
||||||
|
|
||||||
|
abstract double zoom();
|
||||||
|
|
||||||
|
int minThickness() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int maxThickness() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract boolean hasSlopeCondition();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many blocks of this palette are stacked at the spot.
|
||||||
|
*/
|
||||||
|
abstract int thickness(int blockX, int blockZ);
|
||||||
|
|
||||||
|
abstract double noise(int blockX, int blockZ, int depth);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What the preview simplifies about this palette, or null when it shows exactly what the world places.
|
||||||
|
*/
|
||||||
|
String simplification() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract static class LayerPlacement extends BiomePalettePlacement {
|
||||||
|
protected final IrisBiomePaletteLayer layer;
|
||||||
|
|
||||||
|
private LayerPlacement(IrisBiomePaletteLayer layer, IrisData data) {
|
||||||
|
super(data);
|
||||||
|
this.layer = layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
KList<IrisBlockData> blocks() {
|
||||||
|
return layer.getPalette();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
double zoom() {
|
||||||
|
return layer.getZoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
int minThickness() {
|
||||||
|
return layer.getMinHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
int maxThickness() {
|
||||||
|
return layer.getMaxHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean hasSlopeCondition() {
|
||||||
|
return !layer.getSlopeCondition().isDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class SurfaceLayer extends LayerPlacement {
|
||||||
|
private final IrisBiome biome;
|
||||||
|
private final int index;
|
||||||
|
private final RNG random;
|
||||||
|
|
||||||
|
private SurfaceLayer(IrisBiome biome, int index, IrisBiomePaletteLayer layer, RNG random, IrisData data) {
|
||||||
|
super(layer, data);
|
||||||
|
this.biome = biome;
|
||||||
|
this.index = index;
|
||||||
|
this.random = random;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
int thickness(int blockX, int blockZ) {
|
||||||
|
return biome.getLayerThickness(index, blockX, blockZ, random, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
double noise(int blockX, int blockZ, int depth) {
|
||||||
|
return biome.getLayerNoise(index, blockX, blockZ, depth, random, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sea and cave ceiling layers go through the older sampling path, which divides by the zoom twice and borrows
|
||||||
|
* its thickness noise from another list: a cave ceiling layer uses the surface layer at the same position's.
|
||||||
|
*/
|
||||||
|
private static final class LegacyStackedLayer extends LayerPlacement {
|
||||||
|
private final RNG random;
|
||||||
|
private final CNG thicknessNoise;
|
||||||
|
|
||||||
|
private LegacyStackedLayer(IrisBiomePaletteLayer layer, IrisData data, RNG random, KList<CNG> thicknessNoises,
|
||||||
|
int index) {
|
||||||
|
super(layer, data);
|
||||||
|
this.random = random;
|
||||||
|
this.thicknessNoise = index < thicknessNoises.size() ? thicknessNoises.get(index) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
int thickness(int blockX, int blockZ) {
|
||||||
|
if (thicknessNoise == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
double zoom = layer.getZoom();
|
||||||
|
return Math.max(0, thicknessNoise.fit(layer.getMinHeight(), layer.getMaxHeight(), blockX / zoom, blockZ / zoom));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
double noise(int blockX, int blockZ, int depth) {
|
||||||
|
double zoom = layer.getZoom();
|
||||||
|
return layer.getLayerGenerator(random, data)
|
||||||
|
.noise((blockX + depth) / zoom / zoom, depth / zoom, (blockZ - depth) / zoom / zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String simplification() {
|
||||||
|
return thicknessNoise == null
|
||||||
|
? "The generator takes this layer's thickness from the surface layer at the same position, which doesn't exist, so it is never placed."
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wall and slab blocks are picked one at a time with noise that also changes with height.
|
||||||
|
*/
|
||||||
|
private static final class SingleBlocks extends LayerPlacement {
|
||||||
|
private final RNG random;
|
||||||
|
private final int sampleHeight;
|
||||||
|
|
||||||
|
private SingleBlocks(IrisBiomePaletteLayer layer, IrisData data, RNG random, int sampleHeight) {
|
||||||
|
super(layer, data);
|
||||||
|
this.random = random;
|
||||||
|
this.sampleHeight = sampleHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
int minThickness() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
int maxThickness() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
int thickness(int blockX, int blockZ) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
double noise(int blockX, int blockZ, int depth) {
|
||||||
|
double zoom = layer.getZoom();
|
||||||
|
return layer.getLayerGenerator(random, data).noise(blockX / zoom, sampleHeight / zoom, blockZ / zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String simplification() {
|
||||||
|
return "These blocks also change with height; the preview shows them at sea level (Y " + sampleHeight + ").";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A decorator places where its style noise stays under its chance, and its variance noise picks the block there.
|
||||||
|
* The generator seeds the variance noise from whichever block it decorates first, so the preview reuses that noise
|
||||||
|
* once the world has generated some decorations.
|
||||||
|
*/
|
||||||
|
private static final class Decorations extends BiomePalettePlacement {
|
||||||
|
private final IrisDecorator decorator;
|
||||||
|
private final CNG placementNoise;
|
||||||
|
private final CNG blockNoise;
|
||||||
|
private final int sampleHeight;
|
||||||
|
private final boolean sharesSpotsWithOtherDecorators;
|
||||||
|
|
||||||
|
private Decorations(IrisBiome biome, IrisDecorator decorator, long componentSeed, int sampleHeight, IrisData data) {
|
||||||
|
super(data);
|
||||||
|
this.decorator = decorator;
|
||||||
|
RNG partRandom = new RNG(IrisEngineDecorator.partSeed(componentSeed, decorator.getPartOf()));
|
||||||
|
this.placementNoise = decorator.getGenerator(partRandom, data);
|
||||||
|
this.blockNoise = decorator.getVarianceGenerator(partRandom, data);
|
||||||
|
this.sampleHeight = sampleHeight;
|
||||||
|
this.sharesSpotsWithOtherDecorators = biome.getDecorators().stream()
|
||||||
|
.anyMatch(other -> other != null && other != decorator && other.getPartOf() == decorator.getPartOf());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
KList<IrisBlockData> blocks() {
|
||||||
|
return decorator.getPalette();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
double zoom() {
|
||||||
|
return decorator.getStyle().getZoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean hasSlopeCondition() {
|
||||||
|
return !decorator.getSlopeCondition().isDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
int thickness(int blockX, int blockZ) {
|
||||||
|
double zoom = zoom();
|
||||||
|
return placementNoise.fitDouble(0D, 1D, blockX / zoom, blockZ / zoom) <= decorator.getChance() ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The generator swaps X and Z when it picks the block.
|
||||||
|
@Override
|
||||||
|
double noise(int blockX, int blockZ, int depth) {
|
||||||
|
return blockNoise.noise(blockZ, sampleHeight, blockX);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String simplification() {
|
||||||
|
List<String> notes = new ArrayList<>();
|
||||||
|
String partNote = partNote(decorator.getPartOf());
|
||||||
|
if (partNote != null) {
|
||||||
|
notes.add(partNote);
|
||||||
|
}
|
||||||
|
if (sharesSpotsWithOtherDecorators) {
|
||||||
|
notes.add("Where other decorators of this biome could place too, the world picks one of them at random.");
|
||||||
|
}
|
||||||
|
if (placesTwoBlockTallPlants()) {
|
||||||
|
notes.add("Two block tall plants stand up on the map: their top half covers the block above them.");
|
||||||
|
}
|
||||||
|
if (decorator.isStacking()) {
|
||||||
|
notes.add("It stacks " + decorator.getStackMin() + "–" + decorator.getStackMax()
|
||||||
|
+ " blocks high; the preview shows the bottom block.");
|
||||||
|
}
|
||||||
|
if (blocks().size() > 1) {
|
||||||
|
notes.add("The block also changes with height; the preview picks it at sea level (Y " + sampleHeight + ").");
|
||||||
|
}
|
||||||
|
return notes.isEmpty() ? null : String.join(" ", notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The generator places any two-part block as a bottom half with its top half above it.
|
||||||
|
private boolean placesTwoBlockTallPlants() {
|
||||||
|
return !decorator.isStacking() && blocks().stream().anyMatch(block -> block.getBlockData(data) instanceof Bisected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String partNote(IrisDecorationPart part) {
|
||||||
|
return switch (part) {
|
||||||
|
case NONE -> null;
|
||||||
|
case SHORE_LINE -> "It only places along shore lines.";
|
||||||
|
case SEA_SURFACE -> "It only places on the sea surface.";
|
||||||
|
case SEA_FLOOR -> "It only places on the sea floor.";
|
||||||
|
case CEILING -> "It places on cave ceilings and the undersides of overhangs, not on the ground.";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-5
@@ -16,10 +16,12 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.volmit.iris.util.reflect;
|
|
||||||
|
|
||||||
public class ShadeFix {
|
package com.volmit.iris.core.gui;
|
||||||
public static void fix(Class<?> c) {
|
|
||||||
c.getCanonicalName();
|
enum BiomeTint {
|
||||||
}
|
GRASS,
|
||||||
|
FOLIAGE,
|
||||||
|
DRY_FOLIAGE,
|
||||||
|
WATER
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
// The colours one Minecraft biome multiplies onto tinted blocks, as worked out by Biome#getGrassColor and friends.
|
||||||
|
record BiomeTintColors(int grass, GrassColorModifier grassModifier, int foliage, int dryFoliage, int water) {
|
||||||
|
int colorAt(BiomeTint tint, int blockX, int blockZ) {
|
||||||
|
return switch (tint) {
|
||||||
|
case GRASS -> grassModifier.modify(blockX, blockZ, grass);
|
||||||
|
case FOLIAGE -> foliage;
|
||||||
|
case DRY_FOLIAGE -> dryFoliage;
|
||||||
|
case WATER -> water;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean variesByPosition() {
|
||||||
|
return grassModifier == GrassColorModifier.SWAMP;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GrassColorModifier {
|
||||||
|
NONE,
|
||||||
|
DARK_FOREST,
|
||||||
|
SWAMP;
|
||||||
|
|
||||||
|
private static final double SWAMP_NOISE_SCALE = 0.0225D;
|
||||||
|
private static final double SWAMP_WET_THRESHOLD = -0.1D;
|
||||||
|
private static final int SWAMP_WET_GRASS = 0x4C763C;
|
||||||
|
private static final int SWAMP_DRY_GRASS = 0x6A7039;
|
||||||
|
private static final int DARK_FOREST_SHIFT = 0x28340A;
|
||||||
|
|
||||||
|
static GrassColorModifier named(String name) {
|
||||||
|
return switch (name.toLowerCase(Locale.ROOT)) {
|
||||||
|
case "dark_forest" -> DARK_FOREST;
|
||||||
|
case "swamp" -> SWAMP;
|
||||||
|
default -> NONE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
int modify(int blockX, int blockZ, int baseColor) {
|
||||||
|
return switch (this) {
|
||||||
|
case NONE -> baseColor;
|
||||||
|
case DARK_FOREST -> ((baseColor & 0xFEFEFE) + DARK_FOREST_SHIFT) >> 1;
|
||||||
|
case SWAMP -> BiomeInfoNoise.at(blockX * SWAMP_NOISE_SCALE, blockZ * SWAMP_NOISE_SCALE) < SWAMP_WET_THRESHOLD
|
||||||
|
? SWAMP_WET_GRASS
|
||||||
|
: SWAMP_DRY_GRASS;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.engine.object.IrisBiome;
|
||||||
|
import com.volmit.iris.engine.object.IrisBiomeCustom;
|
||||||
|
import com.volmit.iris.util.collection.KList;
|
||||||
|
import com.volmit.iris.util.math.RNG;
|
||||||
|
import org.bukkit.block.Biome;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
// The tint Minecraft draws on each column of one Iris biome. Iris picks the Minecraft biome once per chunk
|
||||||
|
// (see IrisBiomeActuator), and the client averages the colours of the columns around each block.
|
||||||
|
final class BiomeTintField {
|
||||||
|
private static final int MINECRAFT_DEFAULT_BLEND_RADIUS = 2;
|
||||||
|
private static final int BLENDED_COLUMN_COUNT = (MINECRAFT_DEFAULT_BLEND_RADIUS * 2 + 1) * (MINECRAFT_DEFAULT_BLEND_RADIUS * 2 + 1);
|
||||||
|
private static final int BLENDED_TINT_CACHE_SIZE = 1 << 16;
|
||||||
|
private static final int CHUNK_COLORS_CACHE_SIZE = 1 << 10;
|
||||||
|
|
||||||
|
private final IrisBiome biome;
|
||||||
|
private final RNG biomeRandom;
|
||||||
|
private final MinecraftBiomeColors minecraftColors;
|
||||||
|
private final BiomeTintColors sameColorsEverywhere;
|
||||||
|
private final Map<IrisBiomeCustom, BiomeTintColors> colorsByCustomBiome = new ConcurrentHashMap<>();
|
||||||
|
private final BlendedTint[] blendedTintCache = new BlendedTint[BLENDED_TINT_CACHE_SIZE];
|
||||||
|
private final ChunkColors[] chunkColorsCache = new ChunkColors[CHUNK_COLORS_CACHE_SIZE];
|
||||||
|
|
||||||
|
BiomeTintField(IrisBiome biome, RNG biomeRandom, MinecraftBiomeColors minecraftColors) {
|
||||||
|
this.biome = biome;
|
||||||
|
this.biomeRandom = biomeRandom;
|
||||||
|
this.minecraftColors = minecraftColors;
|
||||||
|
BiomeTintColors onlyColors = onlyPossibleColors();
|
||||||
|
this.sameColorsEverywhere = onlyColors == null || onlyColors.variesByPosition() ? null : onlyColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
int colorAt(BiomeTint tint, int blockX, int blockZ) {
|
||||||
|
if (sameColorsEverywhere != null) {
|
||||||
|
return sameColorsEverywhere.colorAt(tint, blockX, blockZ);
|
||||||
|
}
|
||||||
|
int cacheSlot = cacheSlot(blockX, blockZ, tint.ordinal(), BLENDED_TINT_CACHE_SIZE);
|
||||||
|
BlendedTint cached = blendedTintCache[cacheSlot];
|
||||||
|
if (cached != null && cached.x() == blockX && cached.z() == blockZ && cached.tint() == tint) {
|
||||||
|
return cached.color();
|
||||||
|
}
|
||||||
|
int color = blendedColor(tint, blockX, blockZ);
|
||||||
|
blendedTintCache[cacheSlot] = new BlendedTint(blockX, blockZ, tint, color);
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minecraft's ClientLevel#calculateBlockTint at the default biome blend radius.
|
||||||
|
private int blendedColor(BiomeTint tint, int blockX, int blockZ) {
|
||||||
|
int red = 0;
|
||||||
|
int green = 0;
|
||||||
|
int blue = 0;
|
||||||
|
for (int x = blockX - MINECRAFT_DEFAULT_BLEND_RADIUS; x <= blockX + MINECRAFT_DEFAULT_BLEND_RADIUS; x++) {
|
||||||
|
for (int z = blockZ - MINECRAFT_DEFAULT_BLEND_RADIUS; z <= blockZ + MINECRAFT_DEFAULT_BLEND_RADIUS; z++) {
|
||||||
|
int color = chunkColors(x >> 4, z >> 4).colorAt(tint, x, z);
|
||||||
|
red += (color >>> 16) & 0xFF;
|
||||||
|
green += (color >>> 8) & 0xFF;
|
||||||
|
blue += color & 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (red / BLENDED_COLUMN_COUNT) << 16 | (green / BLENDED_COLUMN_COUNT) << 8 | (blue / BLENDED_COLUMN_COUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BiomeTintColors chunkColors(int chunkX, int chunkZ) {
|
||||||
|
int cacheSlot = cacheSlot(chunkX, chunkZ, 0, CHUNK_COLORS_CACHE_SIZE);
|
||||||
|
ChunkColors cached = chunkColorsCache[cacheSlot];
|
||||||
|
if (cached != null && cached.chunkX() == chunkX && cached.chunkZ() == chunkZ) {
|
||||||
|
return cached.colors();
|
||||||
|
}
|
||||||
|
BiomeTintColors colors = colorsForChunkAt(chunkX << 4, chunkZ << 4);
|
||||||
|
chunkColorsCache[cacheSlot] = new ChunkColors(chunkX, chunkZ, colors);
|
||||||
|
return colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BiomeTintColors colorsForChunkAt(int originX, int originZ) {
|
||||||
|
if (biome.isCustom()) {
|
||||||
|
return customColors(biome.getCustomBiome(biomeRandom, originX, 0, originZ));
|
||||||
|
}
|
||||||
|
return vanillaColors(biome.getSkyBiome(biomeRandom, originX, 0, originZ));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follows the same choices as IrisBiome#getSkyBiome and #getCustomBiome, returning null when chunks can differ.
|
||||||
|
private BiomeTintColors onlyPossibleColors() {
|
||||||
|
if (biome.isCustom()) {
|
||||||
|
KList<IrisBiomeCustom> variants = biome.getCustomDerivitives();
|
||||||
|
return variants.size() == 1 ? customColors(variants.get(0)) : null;
|
||||||
|
}
|
||||||
|
KList<Biome> skyBiomes = biome.getBiomeSkyScatter();
|
||||||
|
if (!skyBiomes.isEmpty()) {
|
||||||
|
return skyBiomes.size() == 1 ? vanillaColors(skyBiomes.get(0)) : null;
|
||||||
|
}
|
||||||
|
KList<Biome> groundBiomes = biome.getBiomeScatter();
|
||||||
|
if (groundBiomes.isEmpty()) {
|
||||||
|
return vanillaColors(biome.getDerivative());
|
||||||
|
}
|
||||||
|
return groundBiomes.size() == 1 ? vanillaColors(groundBiomes.get(0)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BiomeTintColors customColors(IrisBiomeCustom customBiome) {
|
||||||
|
return colorsByCustomBiome.computeIfAbsent(customBiome, minecraftColors::custom);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BiomeTintColors vanillaColors(Biome vanillaBiome) {
|
||||||
|
return minecraftColors.vanilla(vanillaBiome.getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int cacheSlot(int x, int z, int salt, int cacheSize) {
|
||||||
|
long hash = x * 0x9E3779B97F4A7C15L ^ z * 0xC2B2AE3D27D4EB4FL ^ salt * 0x165667B19E3779F9L;
|
||||||
|
return (int) ((hash ^ (hash >>> 29)) & (cacheSize - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private record BlendedTint(int x, int z, BiomeTint tint, int color) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ChunkColors(int chunkX, int chunkZ, BiomeTintColors colors) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A block as the object viewer draws it: the textured faces of its model, in block units, already turned the way the
|
||||||
|
* blockstate faces.
|
||||||
|
*
|
||||||
|
* @param faces every face of every box in the model
|
||||||
|
* @param coveredSides the block's sides that a solid face fills completely, which hide whatever touches them
|
||||||
|
* @param hidesSameBlockSides whether the model fills the whole block, so like glass it hides the sides between two of
|
||||||
|
* the same blocks
|
||||||
|
*/
|
||||||
|
record BlockModel(List<Face> faces, Set<CubeFace> coveredSides, boolean hidesSameBlockSides) {
|
||||||
|
static final BlockModel EMPTY = new BlockModel(List.of(), Set.of(), false);
|
||||||
|
private static final int SIDE_CELLS = 16;
|
||||||
|
private static final double ON_SIDE_TOLERANCE = 1e-4D;
|
||||||
|
|
||||||
|
static BlockModel of(List<Face> faces) {
|
||||||
|
Set<CubeFace> coveredSides = EnumSet.noneOf(CubeFace.class);
|
||||||
|
boolean fillsEverySide = true;
|
||||||
|
for (CubeFace side : CubeFace.values()) {
|
||||||
|
if (sideIsFilled(faces, side, true)) {
|
||||||
|
coveredSides.add(side);
|
||||||
|
}
|
||||||
|
fillsEverySide &= sideIsFilled(faces, side, false);
|
||||||
|
}
|
||||||
|
return new BlockModel(List.copyOf(faces), Set.copyOf(coveredSides), fillsEverySide);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BlockModel solidColor(int color) {
|
||||||
|
BlockTexture texture = BlockTexture.solid(color);
|
||||||
|
List<Face> faces = new ArrayList<>();
|
||||||
|
for (CubeFace side : CubeFace.values()) {
|
||||||
|
faces.add(Face.boxSide(Vec3.ZERO, new Vec3(1D, 1D, 1D), side, texture, side));
|
||||||
|
}
|
||||||
|
return of(faces);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isEmpty() {
|
||||||
|
return faces.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockModel withTextures(UnaryOperator<BlockTexture> change) {
|
||||||
|
Map<BlockTexture, BlockTexture> changed = new IdentityHashMap<>();
|
||||||
|
List<Face> changedFaces = new ArrayList<>(faces.size());
|
||||||
|
for (Face face : faces) {
|
||||||
|
changedFaces.add(face.withTexture(changed.computeIfAbsent(face.texture(), change)));
|
||||||
|
}
|
||||||
|
return new BlockModel(List.copyOf(changedFaces), coveredSides, hidesSameBlockSides);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The side is split into a grid, and every cell has to lie under a face pressed flat against it.
|
||||||
|
private static boolean sideIsFilled(List<Face> faces, CubeFace side, boolean onlySolidFaces) {
|
||||||
|
boolean[] filled = new boolean[SIDE_CELLS * SIDE_CELLS];
|
||||||
|
for (Face face : faces) {
|
||||||
|
if (face.liesOn(side) && (!onlySolidFaces || face.texture().isFullyOpaque())) {
|
||||||
|
face.fillCells(side, filled, SIDE_CELLS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (boolean cell : filled) {
|
||||||
|
if (!cell) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A parallelogram from {@code origin} along {@code edgeU} and {@code edgeV}. The texture point at the origin and
|
||||||
|
* how it moves along each edge are given as fractions of the texture.
|
||||||
|
*
|
||||||
|
* @param normal the way the face looks out; it is hidden when seen from behind
|
||||||
|
* @param cullFace the block side whose neighbour hides this face when it covers that side, or null
|
||||||
|
*/
|
||||||
|
record Face(Vec3 origin, Vec3 edgeU, Vec3 edgeV, Vec3 normal, BlockTexture texture, TextureMapping mapping,
|
||||||
|
double shade, CubeFace cullFace) {
|
||||||
|
|
||||||
|
// Laid out as seen from outside: u to the right and v downwards, matching Minecraft's UVs for a whole texture.
|
||||||
|
static Face boxSide(Vec3 from, Vec3 to, CubeFace side, BlockTexture texture, CubeFace cullFace) {
|
||||||
|
Vec3[] corners = BoxCorners.of(from, to, side);
|
||||||
|
return new Face(corners[0], corners[3].minus(corners[0]), corners[1].minus(corners[0]), side.direction(),
|
||||||
|
texture, TextureMapping.WHOLE, side.shade(), cullFace);
|
||||||
|
}
|
||||||
|
|
||||||
|
Face withTexture(BlockTexture changedTexture) {
|
||||||
|
return new Face(origin, edgeU, edgeV, normal, changedTexture, mapping, shade, cullFace);
|
||||||
|
}
|
||||||
|
|
||||||
|
Face transformed(Transform transform) {
|
||||||
|
Vec3 turnedNormal = transform.direction(normal);
|
||||||
|
double length = Math.sqrt(turnedNormal.x() * turnedNormal.x() + turnedNormal.y() * turnedNormal.y() + turnedNormal.z() * turnedNormal.z());
|
||||||
|
return new Face(transform.point(origin), transform.direction(edgeU), transform.direction(edgeV),
|
||||||
|
length == 0D ? turnedNormal : turnedNormal.times(1D / length), texture, mapping, shade,
|
||||||
|
cullFace == null ? null : CubeFace.nearest(transform.direction(cullFace.direction())));
|
||||||
|
}
|
||||||
|
|
||||||
|
Face withCullFace(CubeFace side) {
|
||||||
|
return new Face(origin, edgeU, edgeV, normal, texture, mapping, shade, side);
|
||||||
|
}
|
||||||
|
|
||||||
|
Face withShade(double changedShade) {
|
||||||
|
return new Face(origin, edgeU, edgeV, normal, texture, mapping, changedShade, cullFace);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean liesOn(CubeFace side) {
|
||||||
|
int axis = side.axis();
|
||||||
|
double plane = side.pointsPositive() ? 1D : 0D;
|
||||||
|
return CubeFace.nearest(normal) == side
|
||||||
|
&& Math.abs(normal.component(axis)) > 1D - ON_SIDE_TOLERANCE
|
||||||
|
&& Math.abs(origin.component(axis) - plane) < ON_SIDE_TOLERANCE
|
||||||
|
&& Math.abs(edgeU.component(axis)) < ON_SIDE_TOLERANCE
|
||||||
|
&& Math.abs(edgeV.component(axis)) < ON_SIDE_TOLERANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillCells(CubeFace side, boolean[] filled, int cells) {
|
||||||
|
int firstAxis = side.axis() == 0 ? 1 : 0;
|
||||||
|
int secondAxis = side.axis() == 2 ? 1 : 2;
|
||||||
|
double[] firstRange = range(firstAxis);
|
||||||
|
double[] secondRange = range(secondAxis);
|
||||||
|
for (int first = 0; first < cells; first++) {
|
||||||
|
double firstCenter = (first + 0.5D) / cells;
|
||||||
|
if (firstCenter < firstRange[0] || firstCenter > firstRange[1]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int second = 0; second < cells; second++) {
|
||||||
|
double secondCenter = (second + 0.5D) / cells;
|
||||||
|
if (secondCenter >= secondRange[0] && secondCenter <= secondRange[1]) {
|
||||||
|
filled[first * cells + second] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double[] range(int axis) {
|
||||||
|
double start = origin.component(axis);
|
||||||
|
double alongU = edgeU.component(axis);
|
||||||
|
double alongV = edgeV.component(axis);
|
||||||
|
double low = start + Math.min(0D, alongU) + Math.min(0D, alongV);
|
||||||
|
double high = start + Math.max(0D, alongU) + Math.max(0D, alongV);
|
||||||
|
return new double[]{low, high};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where a face's corner sits in its texture and how the texture point moves along the face's two edges, all as
|
||||||
|
* fractions of the texture's width and height.
|
||||||
|
*/
|
||||||
|
record TextureMapping(double u, double v, double uAlongU, double vAlongU, double uAlongV, double vAlongV) {
|
||||||
|
static final TextureMapping WHOLE = new TextureMapping(0D, 0D, 1D, 0D, 0D, 1D);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A box side's corners in Minecraft's order: top left, bottom left, bottom right and top right, as seen from
|
||||||
|
* outside the box.
|
||||||
|
*/
|
||||||
|
static final class BoxCorners {
|
||||||
|
private BoxCorners() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static Vec3[] of(Vec3 from, Vec3 to, CubeFace side) {
|
||||||
|
double minX = from.x();
|
||||||
|
double minY = from.y();
|
||||||
|
double minZ = from.z();
|
||||||
|
double maxX = to.x();
|
||||||
|
double maxY = to.y();
|
||||||
|
double maxZ = to.z();
|
||||||
|
return switch (side) {
|
||||||
|
case DOWN -> corners(minX, minY, maxZ, minX, minY, minZ, maxX, minY, minZ, maxX, minY, maxZ);
|
||||||
|
case UP -> corners(minX, maxY, minZ, minX, maxY, maxZ, maxX, maxY, maxZ, maxX, maxY, minZ);
|
||||||
|
case NORTH -> corners(maxX, maxY, minZ, maxX, minY, minZ, minX, minY, minZ, minX, maxY, minZ);
|
||||||
|
case SOUTH -> corners(minX, maxY, maxZ, minX, minY, maxZ, maxX, minY, maxZ, maxX, maxY, maxZ);
|
||||||
|
case WEST -> corners(minX, maxY, minZ, minX, minY, minZ, minX, minY, maxZ, minX, maxY, maxZ);
|
||||||
|
case EAST -> corners(maxX, maxY, maxZ, maxX, minY, maxZ, maxX, minY, minZ, maxX, maxY, minZ);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vec3[] corners(double... coordinates) {
|
||||||
|
Vec3[] corners = new Vec3[4];
|
||||||
|
for (int corner = 0; corner < 4; corner++) {
|
||||||
|
corners[corner] = new Vec3(coordinates[corner * 3], coordinates[corner * 3 + 1], coordinates[corner * 3 + 2]);
|
||||||
|
}
|
||||||
|
return corners;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.volmit.iris.core.gui.BlockModel.BoxCorners;
|
||||||
|
import com.volmit.iris.core.gui.BlockModel.Face;
|
||||||
|
import com.volmit.iris.core.gui.BlockModel.TextureMapping;
|
||||||
|
import com.volmit.iris.core.gui.BlockTextures.BlockStateId;
|
||||||
|
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
|
||||||
|
import com.volmit.iris.core.gui.BlockTextures.ModelDefinition;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a block's look the way Minecraft does: every model part the blockstate picks, every box with its own texture
|
||||||
|
* and UVs, tilted boxes, the blockstate's turn with or without locked textures, and which faces a neighbour hides.
|
||||||
|
*/
|
||||||
|
final class BlockModelBuilder {
|
||||||
|
private static final double MODEL_UNITS = 16D;
|
||||||
|
private static final double BLOCK_CENTER = 0.5D;
|
||||||
|
private static final int QUARTER_TURN = 90;
|
||||||
|
|
||||||
|
private final BlockTextures textures;
|
||||||
|
private final EntityBlockShapes entityShapes;
|
||||||
|
|
||||||
|
BlockModelBuilder(BlockTextures textures) {
|
||||||
|
this.textures = textures;
|
||||||
|
this.entityShapes = new EntityBlockShapes(textures);
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<BlockModel> build(String blockState) {
|
||||||
|
try {
|
||||||
|
BlockStateId state = BlockStateId.parse(blockState);
|
||||||
|
List<JsonObject> placements = placementsOf(state);
|
||||||
|
if (placements == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
List<Face> faces = new ArrayList<>();
|
||||||
|
for (JsonObject placement : placements) {
|
||||||
|
faces.addAll(placedFaces(state, placement));
|
||||||
|
}
|
||||||
|
faces.addAll(entityShapes.facesAddedTo(state));
|
||||||
|
return Optional.of(BlockModel.of(faces));
|
||||||
|
} catch (IOException | RuntimeException error) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A box with Minecraft's default UVs on each side, for shapes the game draws in code.
|
||||||
|
*/
|
||||||
|
static List<Face> box(Vec3 fromPixels, Vec3 toPixels, BlockTexture texture, boolean culledBySides) {
|
||||||
|
List<Face> faces = new ArrayList<>();
|
||||||
|
for (CubeFace side : CubeFace.values()) {
|
||||||
|
FaceUv uv = FaceUv.byDefault(side, fromPixels, toPixels);
|
||||||
|
Face face = boxFace(fromPixels, toPixels, side, texture, uv).withShade(side.shade());
|
||||||
|
faces.add(culledBySides ? face.withCullFace(side) : face);
|
||||||
|
}
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A variant names one model; a multipart blockstate, like a fence's, adds a part for every condition that holds.
|
||||||
|
private List<JsonObject> placementsOf(BlockStateId state) throws IOException {
|
||||||
|
JsonObject definition = textures.readJson(BlockTextures.assetPath(state.namespace() + ":" + state.name(), "blockstates", ".json"));
|
||||||
|
if (definition == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<JsonObject> placements = new ArrayList<>();
|
||||||
|
if (definition.has("variants")) {
|
||||||
|
for (Map.Entry<String, JsonElement> variant : definition.getAsJsonObject("variants").entrySet()) {
|
||||||
|
if (BlockTextures.variantMatches(variant.getKey(), state.properties())) {
|
||||||
|
addPlacement(placements, variant.getValue());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (definition.has("multipart")) {
|
||||||
|
for (JsonElement part : definition.getAsJsonArray("multipart")) {
|
||||||
|
JsonObject rule = part.getAsJsonObject();
|
||||||
|
if (!rule.has("when") || BlockTextures.conditionMatches(rule.getAsJsonObject("when"), state.properties())) {
|
||||||
|
addPlacement(placements, rule.get("apply"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return placements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addPlacement(List<JsonObject> placements, JsonElement models) {
|
||||||
|
JsonObject first = BlockTextures.firstModel(models);
|
||||||
|
if (first != null && first.has("model")) {
|
||||||
|
placements.add(first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Face> placedFaces(BlockStateId state, JsonObject placement) throws IOException {
|
||||||
|
ModelDefinition model = textures.readModel(placement.get("model").getAsString());
|
||||||
|
if (model.elements() == null) {
|
||||||
|
return entityShapes.facesOf(state, model);
|
||||||
|
}
|
||||||
|
BlockRotation rotation = BlockRotation.of(placement);
|
||||||
|
List<Face> faces = new ArrayList<>();
|
||||||
|
for (JsonElement element : model.elements()) {
|
||||||
|
addElementFaces(faces, state, model, element.getAsJsonObject(), rotation);
|
||||||
|
}
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addElementFaces(List<Face> faces, BlockStateId state, ModelDefinition model, JsonObject element, BlockRotation rotation) {
|
||||||
|
JsonObject elementFaces = element.getAsJsonObject("faces");
|
||||||
|
if (elementFaces == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Vec3 from = vector(element.getAsJsonArray("from"));
|
||||||
|
Vec3 to = vector(element.getAsJsonArray("to"));
|
||||||
|
Transform tilt = tiltOf(element);
|
||||||
|
boolean shaded = !element.has("shade") || element.get("shade").getAsBoolean();
|
||||||
|
CubeFace shadeSide = element.has("shade_direction_override")
|
||||||
|
? CubeFace.named(element.get("shade_direction_override").getAsString()) : null;
|
||||||
|
for (Map.Entry<String, JsonElement> entry : elementFaces.entrySet()) {
|
||||||
|
CubeFace side = CubeFace.named(entry.getKey());
|
||||||
|
JsonObject faceJson = entry.getValue().getAsJsonObject();
|
||||||
|
if (side == null || !faceJson.has("texture")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String textureName = BlockTextures.dereference(faceJson.get("texture").getAsString(), model.textures());
|
||||||
|
if (textureName == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Optional<BlockTexture> texture = textures.faceTexture(state, textureName, BlockTextures.tintIndexOf(faceJson));
|
||||||
|
if (texture.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FaceUv uv = FaceUv.of(faceJson, side, from, to);
|
||||||
|
if (rotation.locksTextures()) {
|
||||||
|
uv = uv.lockedTo(side, rotation);
|
||||||
|
}
|
||||||
|
Face face = boxFace(from, to, side, texture.get(), uv).transformed(tilt);
|
||||||
|
if (faceJson.has("cullface")) {
|
||||||
|
face = face.withCullFace(CubeFace.named(faceJson.get("cullface").getAsString()));
|
||||||
|
}
|
||||||
|
face = face.transformed(rotation.transform());
|
||||||
|
faces.add(face.withShade(shadeOf(face, shaded, shadeSide, rotation)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Face boxFace(Vec3 fromPixels, Vec3 toPixels, CubeFace side, BlockTexture texture, FaceUv uv) {
|
||||||
|
Vec3[] corners = BoxCorners.of(fromPixels.times(1D / MODEL_UNITS), toPixels.times(1D / MODEL_UNITS), side);
|
||||||
|
return new Face(corners[0], corners[3].minus(corners[0]), corners[1].minus(corners[0]), side.direction(),
|
||||||
|
texture, uv.mapping(), side.shade(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double shadeOf(Face face, boolean shaded, CubeFace shadeSide, BlockRotation rotation) {
|
||||||
|
if (!shaded) {
|
||||||
|
return CubeFace.UP.shade();
|
||||||
|
}
|
||||||
|
if (shadeSide != null) {
|
||||||
|
return CubeFace.nearest(rotation.transform().direction(shadeSide.direction())).shade();
|
||||||
|
}
|
||||||
|
return CubeFace.nearest(face.normal()).shade();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A box can be tilted around a point, on one axis or, since 1.21.?, on all three; rescaling stretches a tilted
|
||||||
|
// plant back to the full block width.
|
||||||
|
private static Transform tiltOf(JsonObject element) {
|
||||||
|
if (!element.has("rotation")) {
|
||||||
|
return Transform.IDENTITY;
|
||||||
|
}
|
||||||
|
JsonObject rotation = element.getAsJsonObject("rotation");
|
||||||
|
Vec3 origin = rotation.has("origin") ? vector(rotation.getAsJsonArray("origin")).times(1D / MODEL_UNITS) : new Vec3(BLOCK_CENTER, BLOCK_CENTER, BLOCK_CENTER);
|
||||||
|
Transform around = Transform.IDENTITY.translate(origin.x(), origin.y(), origin.z());
|
||||||
|
Transform back = Transform.IDENTITY.translate(-origin.x(), -origin.y(), -origin.z());
|
||||||
|
if (!rotation.has("axis")) {
|
||||||
|
return around
|
||||||
|
.rotateX(numberOr(rotation, "x", 0D))
|
||||||
|
.rotateY(numberOr(rotation, "y", 0D))
|
||||||
|
.rotateZ(numberOr(rotation, "z", 0D))
|
||||||
|
.then(back);
|
||||||
|
}
|
||||||
|
String axis = rotation.get("axis").getAsString();
|
||||||
|
double angle = numberOr(rotation, "angle", 0D);
|
||||||
|
boolean rescale = rotation.has("rescale") && rotation.get("rescale").getAsBoolean();
|
||||||
|
double stretch = rescale ? 1D / Math.cos(Math.toRadians(angle)) : 1D;
|
||||||
|
Transform stretched = around.scale(axis.equals("x") ? 1D : stretch, axis.equals("y") ? 1D : stretch, axis.equals("z") ? 1D : stretch);
|
||||||
|
Transform tilted = switch (axis) {
|
||||||
|
case "x" -> stretched.rotateX(angle);
|
||||||
|
case "y" -> stretched.rotateY(angle);
|
||||||
|
default -> stretched.rotateZ(angle);
|
||||||
|
};
|
||||||
|
return tilted.then(back);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double numberOr(JsonObject object, String key, double fallback) {
|
||||||
|
return object.has(key) ? object.get(key).getAsDouble() : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vec3 vector(JsonArray values) {
|
||||||
|
return new Vec3(values.get(0).getAsDouble(), values.get(1).getAsDouble(), values.get(2).getAsDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How a blockstate turns its model: around the x axis first, then the y axis, both in quarter turns against the
|
||||||
|
* right-hand rule. With uvlock the textures stay put while the shape turns.
|
||||||
|
*/
|
||||||
|
private record BlockRotation(int x, int y, boolean uvlock) {
|
||||||
|
static BlockRotation of(JsonObject placement) {
|
||||||
|
int x = placement.has("x") ? Math.floorMod(placement.get("x").getAsInt(), 360) : 0;
|
||||||
|
int y = placement.has("y") ? Math.floorMod(placement.get("y").getAsInt(), 360) : 0;
|
||||||
|
return new BlockRotation(x, y, placement.has("uvlock") && placement.get("uvlock").getAsBoolean());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean locksTextures() {
|
||||||
|
return uvlock && (x != 0 || y != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Transform transform() {
|
||||||
|
return Transform.IDENTITY
|
||||||
|
.translate(BLOCK_CENTER, BLOCK_CENTER, BLOCK_CENTER)
|
||||||
|
.then(turn())
|
||||||
|
.translate(-BLOCK_CENTER, -BLOCK_CENTER, -BLOCK_CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
Transform turn() {
|
||||||
|
return Transform.IDENTITY.rotateY(-y).rotateX(-x);
|
||||||
|
}
|
||||||
|
|
||||||
|
Transform inverseTurn() {
|
||||||
|
return Transform.IDENTITY.rotateX(x).rotateY(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A face's corner UVs in model pixels and the quarter turn of the texture on it.
|
||||||
|
*/
|
||||||
|
private record FaceUv(double u1, double v1, double u2, double v2, int rotation) {
|
||||||
|
static FaceUv of(JsonObject face, CubeFace side, Vec3 from, Vec3 to) {
|
||||||
|
int rotation = face.has("rotation") ? Math.floorMod(face.get("rotation").getAsInt(), 360) : 0;
|
||||||
|
if (!face.has("uv")) {
|
||||||
|
return byDefault(side, from, to).withRotation(rotation);
|
||||||
|
}
|
||||||
|
JsonArray uv = face.getAsJsonArray("uv");
|
||||||
|
return new FaceUv(uv.get(0).getAsDouble(), uv.get(1).getAsDouble(), uv.get(2).getAsDouble(), uv.get(3).getAsDouble(), rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The part of the texture the face would cover if the texture were laid over the whole block side.
|
||||||
|
static FaceUv byDefault(CubeFace side, Vec3 from, Vec3 to) {
|
||||||
|
double full = MODEL_UNITS;
|
||||||
|
return switch (side) {
|
||||||
|
case DOWN -> new FaceUv(from.x(), full - to.z(), to.x(), full - from.z(), 0);
|
||||||
|
case UP -> new FaceUv(from.x(), from.z(), to.x(), to.z(), 0);
|
||||||
|
case NORTH -> new FaceUv(full - to.x(), full - to.y(), full - from.x(), full - from.y(), 0);
|
||||||
|
case SOUTH -> new FaceUv(from.x(), full - to.y(), to.x(), full - from.y(), 0);
|
||||||
|
case WEST -> new FaceUv(from.z(), full - to.y(), to.z(), full - from.y(), 0);
|
||||||
|
case EAST -> new FaceUv(full - to.z(), full - to.y(), full - from.z(), full - from.y(), 0);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
FaceUv withRotation(int changedRotation) {
|
||||||
|
return new FaceUv(u1, v1, u2, v2, changedRotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each corner takes the UV a quarter turn further along for every quarter turn of the texture.
|
||||||
|
TextureMapping mapping() {
|
||||||
|
double[] origin = cornerUv(0);
|
||||||
|
double[] alongU = cornerUv(3);
|
||||||
|
double[] alongV = cornerUv(1);
|
||||||
|
return new TextureMapping(origin[0] / MODEL_UNITS, origin[1] / MODEL_UNITS,
|
||||||
|
(alongU[0] - origin[0]) / MODEL_UNITS, (alongU[1] - origin[1]) / MODEL_UNITS,
|
||||||
|
(alongV[0] - origin[0]) / MODEL_UNITS, (alongV[1] - origin[1]) / MODEL_UNITS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double[] cornerUv(int corner) {
|
||||||
|
return switch ((corner + rotation / QUARTER_TURN) % 4) {
|
||||||
|
case 0 -> new double[]{u1, v1};
|
||||||
|
case 1 -> new double[]{u1, v2};
|
||||||
|
case 2 -> new double[]{u2, v2};
|
||||||
|
default -> new double[]{u2, v1};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minecraft's uvlock: the UVs are carried through the reverse of the blockstate's turn, so the texture keeps
|
||||||
|
* its place in the world while the face moves to another side.
|
||||||
|
*/
|
||||||
|
FaceUv lockedTo(CubeFace side, BlockRotation blockRotation) {
|
||||||
|
CubeFace turnedSide = CubeFace.nearest(blockRotation.turn().direction(side.direction()));
|
||||||
|
Transform uvTurn = Transform.IDENTITY
|
||||||
|
.translate(BLOCK_CENTER, BLOCK_CENTER, BLOCK_CENTER)
|
||||||
|
.then(faceFromWorld(side))
|
||||||
|
.then(blockRotation.inverseTurn())
|
||||||
|
.then(worldFromFace(turnedSide))
|
||||||
|
.translate(-BLOCK_CENTER, -BLOCK_CENTER, -BLOCK_CENTER);
|
||||||
|
Vec3 first = uvTurn.point(new Vec3(u1 / MODEL_UNITS, v1 / MODEL_UNITS, 0D)).times(MODEL_UNITS);
|
||||||
|
Vec3 second = uvTurn.point(new Vec3(u2 / MODEL_UNITS, v2 / MODEL_UNITS, 0D)).times(MODEL_UNITS);
|
||||||
|
boolean keepsUOrder = Math.signum(u2 - u1) == Math.signum(second.x() - first.x());
|
||||||
|
boolean keepsVOrder = Math.signum(v2 - v1) == Math.signum(second.y() - first.y());
|
||||||
|
Vec3 textureRight = uvTurn.direction(new Vec3(Math.cos(Math.toRadians(rotation)), Math.sin(Math.toRadians(rotation)), 0D));
|
||||||
|
long quarterTurns = Math.round(Math.toDegrees(Math.atan2(textureRight.y(), textureRight.x())) / QUARTER_TURN);
|
||||||
|
return new FaceUv(
|
||||||
|
keepsUOrder ? first.x() : second.x(), keepsVOrder ? first.y() : second.y(),
|
||||||
|
keepsUOrder ? second.x() : first.x(), keepsVOrder ? second.y() : first.y(),
|
||||||
|
(int) Math.floorMod(-quarterTurns * QUARTER_TURN, 360L));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minecraft lays every side's texture out as if it were the south side, turned into place.
|
||||||
|
private static Transform worldFromFace(CubeFace side) {
|
||||||
|
return switch (side) {
|
||||||
|
case SOUTH -> Transform.IDENTITY;
|
||||||
|
case EAST -> Transform.IDENTITY.rotateY(90D);
|
||||||
|
case WEST -> Transform.IDENTITY.rotateY(-90D);
|
||||||
|
case NORTH -> Transform.IDENTITY.rotateY(180D);
|
||||||
|
case UP -> Transform.IDENTITY.rotateX(-90D);
|
||||||
|
case DOWN -> Transform.IDENTITY.rotateX(90D);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Transform faceFromWorld(CubeFace side) {
|
||||||
|
return switch (side) {
|
||||||
|
case SOUTH -> Transform.IDENTITY;
|
||||||
|
case EAST -> Transform.IDENTITY.rotateY(-90D);
|
||||||
|
case WEST -> Transform.IDENTITY.rotateY(90D);
|
||||||
|
case NORTH -> Transform.IDENTITY.rotateY(180D);
|
||||||
|
case UP -> Transform.IDENTITY.rotateX(90D);
|
||||||
|
case DOWN -> Transform.IDENTITY.rotateX(-90D);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
final class BlockPreviewColors {
|
||||||
|
private static final Method MAP_COLOR_METHOD = findMapColorMethod();
|
||||||
|
private static final double[] MAP_SHADES = {1D, 0.86D, 0.71D, 0.53D};
|
||||||
|
private static final int NO_MAP_COLOR = 0;
|
||||||
|
|
||||||
|
private BlockPreviewColors() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, Integer> colorsByBlock(List<BlockData> blocks) {
|
||||||
|
Map<String, Integer> colors = new HashMap<>();
|
||||||
|
List<Integer> usedColors = new ArrayList<>();
|
||||||
|
for (BlockData block : blocks) {
|
||||||
|
String key = block.getAsString(true);
|
||||||
|
if (colors.containsKey(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int baseColor = baseColor(block);
|
||||||
|
int color = firstUnusedShade(baseColor, usedColors);
|
||||||
|
usedColors.add(color);
|
||||||
|
colors.put(key, color);
|
||||||
|
}
|
||||||
|
return colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blocks that share a map colour, like sand and sandstone, get darker shades the way Minecraft maps shade them.
|
||||||
|
private static int firstUnusedShade(int baseColor, List<Integer> usedColors) {
|
||||||
|
for (double shade : MAP_SHADES) {
|
||||||
|
int shaded = shade(baseColor, shade);
|
||||||
|
if (!usedColors.contains(shaded)) {
|
||||||
|
return shaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return baseColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int baseColor(BlockData block) {
|
||||||
|
int mapColor = mapColor(block);
|
||||||
|
return mapColor == NO_MAP_COLOR ? colorFromName(block.getMaterial().getKey().getKey()) : mapColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int mapColor(BlockData block) {
|
||||||
|
if (MAP_COLOR_METHOD == null) {
|
||||||
|
return NO_MAP_COLOR;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return ((org.bukkit.Color) MAP_COLOR_METHOD.invoke(block)).asRGB();
|
||||||
|
} catch (ReflectiveOperationException | RuntimeException error) {
|
||||||
|
return NO_MAP_COLOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int colorFromName(String name) {
|
||||||
|
float hue = (name.hashCode() & 0xFFFF) / (float) 0xFFFF;
|
||||||
|
return Color.HSBtoRGB(hue, 0.45F, 0.75F) & 0xFFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int shade(int color, double shade) {
|
||||||
|
int red = (int) Math.min(255, ((color >>> 16) & 0xFF) * shade);
|
||||||
|
int green = (int) Math.min(255, ((color >>> 8) & 0xFF) * shade);
|
||||||
|
int blue = (int) Math.min(255, (color & 0xFF) * shade);
|
||||||
|
return (red << 16) | (green << 8) | blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iris compiles against the 1.20.1 API, which predates BlockData#getMapColor.
|
||||||
|
private static Method findMapColorMethod() {
|
||||||
|
try {
|
||||||
|
return BlockData.class.getMethod("getMapColor");
|
||||||
|
} catch (NoSuchMethodException error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,603 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import org.bukkit.block.data.Bisected;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
final class BlockTextures {
|
||||||
|
private static final String DEFAULT_NAMESPACE = "minecraft";
|
||||||
|
private static final int MAX_MODEL_PARENTS = 16;
|
||||||
|
private static final int MAX_TEXTURE_REFERENCES = 16;
|
||||||
|
static final int NO_TINT_INDEX = -1;
|
||||||
|
private static final Object SHARED_LOCK = new Object();
|
||||||
|
private static CompletableFuture<BlockTextures> shared;
|
||||||
|
|
||||||
|
private final ZipFile clientJar;
|
||||||
|
private final MinecraftBiomeColors biomeColors;
|
||||||
|
private final Map<String, Optional<ResolvedTexture>> resolvedTextures = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Optional<BlockModel>> resolvedModels = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Optional<BlockTexture>> faceTextures = new ConcurrentHashMap<>();
|
||||||
|
private final BlockModelBuilder modelBuilder = new BlockModelBuilder(this);
|
||||||
|
|
||||||
|
private BlockTextures(ZipFile clientJar, MinecraftBiomeColors biomeColors) {
|
||||||
|
this.clientJar = clientJar;
|
||||||
|
this.biomeColors = biomeColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CompletableFuture<BlockTextures> shared() {
|
||||||
|
synchronized (SHARED_LOCK) {
|
||||||
|
if (shared == null || shared.isCompletedExceptionally()) {
|
||||||
|
shared = MinecraftClientJar.open().thenApply(BlockTextures::load);
|
||||||
|
}
|
||||||
|
return shared;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockTextures load(ZipFile clientJar) {
|
||||||
|
try {
|
||||||
|
return new BlockTextures(clientJar, MinecraftBiomeColors.load(clientJar));
|
||||||
|
} catch (IOException error) {
|
||||||
|
throw new IllegalStateException("Could not read Minecraft's biome colour maps", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MinecraftBiomeColors biomeColors() {
|
||||||
|
return biomeColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What the block looks like from above. Plants and other side-on sprites become their average colour, because
|
||||||
|
* their sprite would read as stripes across a stack of ground blocks.
|
||||||
|
*/
|
||||||
|
Optional<BlockTexture> topTexture(BlockData block) {
|
||||||
|
return resolve(block).map(resolved -> resolved.isSideOnSprite() ? resolved.texture().flattened() : resolved.texture());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like {@link #topTexture}, but plants keep their sprite, so single decorations on the ground stay recognisable.
|
||||||
|
* Two block tall plants give their bottom half, the part standing on the ground.
|
||||||
|
*/
|
||||||
|
Optional<BlockTexture> iconTexture(BlockData block) {
|
||||||
|
BlockData standing = block instanceof Bisected ? withHalf(block, Bisected.Half.BOTTOM) : block;
|
||||||
|
return resolve(standing).map(ResolvedTexture::texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sprite of a two block tall plant's top half, or nothing for any other block.
|
||||||
|
*/
|
||||||
|
Optional<BlockTexture> upperHalfIcon(BlockData block) {
|
||||||
|
if (!(block instanceof Bisected)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return resolve(withHalf(block, Bisected.Half.TOP)).filter(ResolvedTexture::isSideOnSprite).map(ResolvedTexture::texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What the block looks like from every side, for drawing it in 3D.
|
||||||
|
*/
|
||||||
|
Optional<BlockModel> model(BlockData block) {
|
||||||
|
return resolvedModels.computeIfAbsent(block.getAsString(false), modelBuilder::build);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockData withHalf(BlockData block, Bisected.Half half) {
|
||||||
|
BlockData copy = block.clone();
|
||||||
|
((Bisected) copy).setHalf(half);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blockstate files match on every property, so defaults the pack left out have to be spelled out too.
|
||||||
|
private Optional<ResolvedTexture> resolve(BlockData block) {
|
||||||
|
return resolvedTextures.computeIfAbsent(block.getAsString(false), this::resolveTopTexture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ResolvedTexture> resolveTopTexture(String blockState) {
|
||||||
|
try {
|
||||||
|
BlockStateId state = BlockStateId.parse(blockState);
|
||||||
|
JsonObject variant = variantOf(state);
|
||||||
|
if (variant == null || !variant.has("model")) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
ModelFace face = readModel(variant.get("model").getAsString()).visibleFace(faceSeenFromAbove(variant));
|
||||||
|
if (face == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return readTexture(state, face).map(texture -> new ResolvedTexture(texture, face.isSideOnSprite()));
|
||||||
|
} catch (IOException | RuntimeException error) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject variantOf(BlockStateId state) throws IOException {
|
||||||
|
JsonObject definition = readJson(assetPath(state.namespace() + ":" + state.name(), "blockstates", ".json"));
|
||||||
|
return definition == null ? null : pickVariant(definition, state.properties());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int rotationOf(JsonObject variant, String axis) {
|
||||||
|
return variant.has(axis) ? Math.floorMod(variant.get(axis).getAsInt(), 360) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One model face's texture with the block's tint, shared by every block state that uses it.
|
||||||
|
*/
|
||||||
|
Optional<BlockTexture> faceTexture(BlockStateId state, String texture, int tintIndex) {
|
||||||
|
int fixedTint = MinecraftBlockTints.fixedTint(state.name(), state.properties(), tintIndex);
|
||||||
|
BiomeTint biomeTint = MinecraftBlockTints.biomeTint(state.name(), tintIndex).orElse(null);
|
||||||
|
return faceTextures.computeIfAbsent(texture + "|" + fixedTint + "|" + biomeTint, key -> {
|
||||||
|
try {
|
||||||
|
return readTexture(state, new ModelFace(List.of(texture), tintIndex, false));
|
||||||
|
} catch (IOException | RuntimeException error) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A texture drawn on a block entity, like a chest, whole and tinted with a fixed colour.
|
||||||
|
*/
|
||||||
|
Optional<BlockTexture> entityTexture(String texture, int tint) {
|
||||||
|
return faceTextures.computeIfAbsent(texture + "|entity|" + tint, key -> {
|
||||||
|
try {
|
||||||
|
BufferedImage image = readImage(assetPath(texture, "textures", ".png"));
|
||||||
|
return image == null ? Optional.empty() : Optional.of(BlockTexture.fromWholeImage(image, tint));
|
||||||
|
} catch (IOException | RuntimeException error) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<BlockTexture> readTexture(BlockStateId state, ModelFace face) throws IOException {
|
||||||
|
BufferedImage image = readLayeredImage(face.textures());
|
||||||
|
if (image == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Optional.of(BlockTexture.fromImage(
|
||||||
|
image,
|
||||||
|
MinecraftBlockTints.fixedTint(state.name(), state.properties(), face.tintIndex()),
|
||||||
|
MinecraftBlockTints.biomeTint(state.name(), face.tintIndex()).orElse(null)
|
||||||
|
));
|
||||||
|
} catch (IllegalArgumentException invisibleTexture) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A block rotated on its x axis, like a sideways log, shows a side face on top. Blockstates turn the model
|
||||||
|
// against the right-hand rule, so 90 degrees brings the south side up.
|
||||||
|
private static String faceSeenFromAbove(JsonObject variant) {
|
||||||
|
return switch (rotationOf(variant, "x")) {
|
||||||
|
case 90 -> "south";
|
||||||
|
case 270 -> "north";
|
||||||
|
case 180 -> "down";
|
||||||
|
default -> "up";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelDefinition readModel(String modelReference) throws IOException {
|
||||||
|
Map<String, String> textures = new HashMap<>();
|
||||||
|
JsonArray elements = null;
|
||||||
|
String current = modelReference;
|
||||||
|
for (int depth = 0; current != null && depth < MAX_MODEL_PARENTS; depth++) {
|
||||||
|
JsonObject model = readJson(assetPath(current, "models", ".json"));
|
||||||
|
if (model == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (model.has("textures")) {
|
||||||
|
for (Map.Entry<String, JsonElement> texture : model.getAsJsonObject("textures").entrySet()) {
|
||||||
|
textures.putIfAbsent(texture.getKey(), spriteName(texture.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (elements == null && model.has("elements")) {
|
||||||
|
elements = model.getAsJsonArray("elements");
|
||||||
|
}
|
||||||
|
current = model.has("parent") ? model.get("parent").getAsString() : null;
|
||||||
|
}
|
||||||
|
return new ModelDefinition(textures, elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since 1.21.? a texture can also be an object naming its sprite, like glass's {"sprite": ..., "force_translucent": true}.
|
||||||
|
private static String spriteName(JsonElement texture) {
|
||||||
|
return texture.isJsonObject() ? texture.getAsJsonObject().get("sprite").getAsString() : texture.getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some plants draw a second sprite over the first, like the glowing fireflies on a firefly bush.
|
||||||
|
private static List<String> spriteLayers(JsonArray elements, Map<String, String> textures) {
|
||||||
|
Set<String> layers = new LinkedHashSet<>();
|
||||||
|
for (JsonElement element : elements) {
|
||||||
|
JsonObject faces = element.getAsJsonObject().getAsJsonObject("faces");
|
||||||
|
if (faces == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, JsonElement> face : faces.entrySet()) {
|
||||||
|
String texture = dereference(face.getValue().getAsJsonObject().get("texture").getAsString(), textures);
|
||||||
|
if (texture != null) {
|
||||||
|
layers.add(texture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(layers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int firstTintIndex(JsonArray elements) {
|
||||||
|
for (JsonElement element : elements) {
|
||||||
|
JsonObject faces = element.getAsJsonObject().getAsJsonObject("faces");
|
||||||
|
if (faces == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, JsonElement> face : faces.entrySet()) {
|
||||||
|
int tintIndex = tintIndexOf(face.getValue().getAsJsonObject());
|
||||||
|
if (tintIndex != NO_TINT_INDEX) {
|
||||||
|
return tintIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NO_TINT_INDEX;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int tintIndexOf(JsonObject face) {
|
||||||
|
return face.has("tintindex") ? face.get("tintindex").getAsInt() : NO_TINT_INDEX;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject topmostFace(JsonArray elements, String faceName) {
|
||||||
|
JsonObject best = null;
|
||||||
|
double bestHeight = Double.NEGATIVE_INFINITY;
|
||||||
|
double bestArea = -1D;
|
||||||
|
for (JsonElement element : elements) {
|
||||||
|
JsonObject cuboid = element.getAsJsonObject();
|
||||||
|
JsonObject faces = cuboid.getAsJsonObject("faces");
|
||||||
|
if (faces == null || !faces.has(faceName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JsonArray from = cuboid.getAsJsonArray("from");
|
||||||
|
JsonArray to = cuboid.getAsJsonArray("to");
|
||||||
|
double height = to.get(1).getAsDouble();
|
||||||
|
double area = (to.get(0).getAsDouble() - from.get(0).getAsDouble()) * (to.get(2).getAsDouble() - from.get(2).getAsDouble());
|
||||||
|
if (height > bestHeight || (height == bestHeight && area > bestArea)) {
|
||||||
|
best = faces.getAsJsonObject(faceName);
|
||||||
|
bestHeight = height;
|
||||||
|
bestArea = area;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String dereference(String reference, Map<String, String> textures) {
|
||||||
|
String resolved = reference;
|
||||||
|
for (int hop = 0; resolved != null && resolved.startsWith("#") && hop < MAX_TEXTURE_REFERENCES; hop++) {
|
||||||
|
resolved = textures.get(resolved.substring(1));
|
||||||
|
}
|
||||||
|
return resolved == null || resolved.startsWith("#") ? null : resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject pickVariant(JsonObject definition, Map<String, String> properties) {
|
||||||
|
if (definition.has("variants")) {
|
||||||
|
for (Map.Entry<String, JsonElement> variant : definition.getAsJsonObject("variants").entrySet()) {
|
||||||
|
if (variantMatches(variant.getKey(), properties)) {
|
||||||
|
return firstModel(variant.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (definition.has("multipart")) {
|
||||||
|
for (JsonElement part : definition.getAsJsonArray("multipart")) {
|
||||||
|
JsonObject rule = part.getAsJsonObject();
|
||||||
|
if (!rule.has("when") || conditionMatches(rule.getAsJsonObject("when"), properties)) {
|
||||||
|
return firstModel(rule.get("apply"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean variantMatches(String variantKey, Map<String, String> properties) {
|
||||||
|
if (variantKey.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (String condition : variantKey.split(",")) {
|
||||||
|
String[] pair = condition.split("=", 2);
|
||||||
|
if (pair.length != 2 || !pair[1].equals(properties.get(pair[0]))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean conditionMatches(JsonObject condition, Map<String, String> properties) {
|
||||||
|
if (condition.has("OR")) {
|
||||||
|
for (JsonElement option : condition.getAsJsonArray("OR")) {
|
||||||
|
if (conditionMatches(option.getAsJsonObject(), properties)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (condition.has("AND")) {
|
||||||
|
for (JsonElement requirement : condition.getAsJsonArray("AND")) {
|
||||||
|
if (!conditionMatches(requirement.getAsJsonObject(), properties)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, JsonElement> requirement : condition.entrySet()) {
|
||||||
|
String actual = properties.get(requirement.getKey());
|
||||||
|
boolean anyAllowed = false;
|
||||||
|
for (String allowed : requirement.getValue().getAsString().split("\\|")) {
|
||||||
|
anyAllowed |= allowed.equals(actual);
|
||||||
|
}
|
||||||
|
if (!anyAllowed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static JsonObject firstModel(JsonElement models) {
|
||||||
|
if (models.isJsonArray()) {
|
||||||
|
JsonArray choices = models.getAsJsonArray();
|
||||||
|
return choices.isEmpty() ? null : choices.get(0).getAsJsonObject();
|
||||||
|
}
|
||||||
|
return models.getAsJsonObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String assetPath(String resource, String folder, String extension) {
|
||||||
|
int separator = resource.indexOf(':');
|
||||||
|
String namespace = separator < 0 ? DEFAULT_NAMESPACE : resource.substring(0, separator);
|
||||||
|
String path = separator < 0 ? resource : resource.substring(separator + 1);
|
||||||
|
return "assets/" + namespace + "/" + folder + "/" + path + extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject readJson(String path) throws IOException {
|
||||||
|
return ClientJarFiles.readJson(clientJar, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each layer is drawn over the ones before it, using only its first frame when it is animated.
|
||||||
|
private BufferedImage readLayeredImage(List<String> layers) throws IOException {
|
||||||
|
BufferedImage bottom = readImage(assetPath(layers.get(0), "textures", ".png"));
|
||||||
|
if (bottom == null || layers.size() == 1) {
|
||||||
|
return bottom;
|
||||||
|
}
|
||||||
|
int size = bottom.getWidth();
|
||||||
|
BufferedImage combined = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D canvas = combined.createGraphics();
|
||||||
|
try {
|
||||||
|
for (String layer : layers) {
|
||||||
|
BufferedImage image = readImage(assetPath(layer, "textures", ".png"));
|
||||||
|
if (image != null) {
|
||||||
|
int frameSize = image.getWidth();
|
||||||
|
canvas.drawImage(image, 0, 0, size, size, 0, 0, frameSize, frameSize, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
return combined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BufferedImage readImage(String path) throws IOException {
|
||||||
|
return ClientJarFiles.readImage(clientJar, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plants and other blocks without a top face only have a side-on sprite, which would read as stripes from above.
|
||||||
|
private record ModelFace(List<String> textures, int tintIndex, boolean isSideOnSprite) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A block model with its parents' textures merged in. Models drawn in code, like chests, have no elements.
|
||||||
|
*/
|
||||||
|
record ModelDefinition(Map<String, String> textures, JsonArray elements) {
|
||||||
|
ModelFace visibleFace(String faceName) {
|
||||||
|
ModelFace face = sideFace(faceName);
|
||||||
|
if (face != null) {
|
||||||
|
return face;
|
||||||
|
}
|
||||||
|
return elements != null && !spriteLayers(elements, textures).isEmpty() ? spriteFace() : particleFace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The face's texture on the model's topmost box, the particle texture for models without boxes, or null when
|
||||||
|
// the model leaves that side open.
|
||||||
|
ModelFace sideFace(String faceName) {
|
||||||
|
if (elements == null) {
|
||||||
|
return particleFace();
|
||||||
|
}
|
||||||
|
JsonObject face = topmostFace(elements, faceName);
|
||||||
|
if (face == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String texture = dereference(face.get("texture").getAsString(), textures);
|
||||||
|
return texture == null ? null : new ModelFace(List.of(texture), tintIndexOf(face), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelFace spriteFace() {
|
||||||
|
return new ModelFace(spriteLayers(elements, textures), firstTintIndex(elements), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModelFace particleFace() {
|
||||||
|
String particle = dereference("#particle", textures);
|
||||||
|
return particle == null ? null : new ModelFace(List.of(particle), NO_TINT_INDEX, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ResolvedTexture(BlockTexture texture, boolean isSideOnSprite) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record BlockStateId(String namespace, String name, Map<String, String> properties) {
|
||||||
|
static BlockStateId parse(String blockState) {
|
||||||
|
int propertiesStart = blockState.indexOf('[');
|
||||||
|
String id = propertiesStart < 0 ? blockState : blockState.substring(0, propertiesStart);
|
||||||
|
int separator = id.indexOf(':');
|
||||||
|
String namespace = separator < 0 ? DEFAULT_NAMESPACE : id.substring(0, separator);
|
||||||
|
String name = separator < 0 ? id : id.substring(separator + 1);
|
||||||
|
Map<String, String> properties = new HashMap<>();
|
||||||
|
if (propertiesStart >= 0) {
|
||||||
|
String body = blockState.substring(propertiesStart + 1, blockState.length() - 1);
|
||||||
|
for (String pair : body.split(",")) {
|
||||||
|
String[] keyValue = pair.split("=", 2);
|
||||||
|
if (keyValue.length == 2) {
|
||||||
|
properties.put(keyValue[0], keyValue[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new BlockStateId(namespace, name, properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Biome tinted textures keep their grey pixels, because the biome colour changes from column to column.
|
||||||
|
// See-through pixels get a darker shade of the average colour, and the opaque mask remembers which they were.
|
||||||
|
// Textures with partly see-through pixels, like stained glass, ice and water, keep each pixel's alpha too.
|
||||||
|
record BlockTexture(int width, int height, int[] pixels, boolean[] opaque, int[] alphas, int averageColor,
|
||||||
|
BiomeTint biomeTint) {
|
||||||
|
private static final double GAP_SHADE = 0.55D;
|
||||||
|
private static final int FULL_ALPHA = 255;
|
||||||
|
|
||||||
|
// Animated textures stack their frames vertically, so only the first square frame is used.
|
||||||
|
static BlockTexture fromImage(BufferedImage image, int fixedTint, BiomeTint biomeTint) {
|
||||||
|
return fromPixels(image, image.getWidth(), Math.min(image.getWidth(), image.getHeight()), fixedTint, biomeTint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entity textures, like a chest's, are laid out for a model and can be wider than they are tall.
|
||||||
|
static BlockTexture fromWholeImage(BufferedImage image, int fixedTint) {
|
||||||
|
return fromPixels(image, image.getWidth(), image.getHeight(), fixedTint, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BlockTexture solid(int color) {
|
||||||
|
return new BlockTexture(1, 1, new int[]{color}, new boolean[]{true}, null, color, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockTexture fromPixels(BufferedImage image, int width, int height, int fixedTint, BiomeTint biomeTint) {
|
||||||
|
int[] pixels = new int[width * height];
|
||||||
|
boolean[] opaque = new boolean[pixels.length];
|
||||||
|
int[] alphas = new int[pixels.length];
|
||||||
|
boolean translucent = false;
|
||||||
|
long red = 0;
|
||||||
|
long green = 0;
|
||||||
|
long blue = 0;
|
||||||
|
int opaqueCount = 0;
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
int argb = image.getRGB(x, y);
|
||||||
|
int index = y * width + x;
|
||||||
|
int alpha = argb >>> 24;
|
||||||
|
alphas[index] = alpha;
|
||||||
|
if (alpha == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
translucent |= alpha < FULL_ALPHA;
|
||||||
|
int color = multiply(argb & 0xFFFFFF, fixedTint);
|
||||||
|
pixels[index] = color;
|
||||||
|
opaque[index] = true;
|
||||||
|
red += (color >>> 16) & 0xFF;
|
||||||
|
green += (color >>> 8) & 0xFF;
|
||||||
|
blue += color & 0xFF;
|
||||||
|
opaqueCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (opaqueCount == 0) {
|
||||||
|
throw new IllegalArgumentException("Texture has no visible pixels");
|
||||||
|
}
|
||||||
|
int average = (int) (red / opaqueCount) << 16 | (int) (green / opaqueCount) << 8 | (int) (blue / opaqueCount);
|
||||||
|
int gapColor = BlockPreviewColors.shade(average, GAP_SHADE);
|
||||||
|
for (int index = 0; index < pixels.length; index++) {
|
||||||
|
if (!opaque[index]) {
|
||||||
|
pixels[index] = gapColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new BlockTexture(width, height, pixels, opaque, translucent ? alphas : null, average, biomeTint);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isTranslucent() {
|
||||||
|
return alphas != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isFullyOpaque() {
|
||||||
|
if (isTranslucent()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (boolean pixelOpaque : opaque) {
|
||||||
|
if (!pixelOpaque) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockTexture flattened() {
|
||||||
|
int[] flat = new int[pixels.length];
|
||||||
|
Arrays.fill(flat, averageColor);
|
||||||
|
boolean[] allOpaque = new boolean[pixels.length];
|
||||||
|
Arrays.fill(allOpaque, true);
|
||||||
|
return new BlockTexture(width, height, flat, allOpaque, null, averageColor, biomeTint);
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockTexture tinted(int tint) {
|
||||||
|
int[] tintedPixels = new int[pixels.length];
|
||||||
|
for (int index = 0; index < pixels.length; index++) {
|
||||||
|
tintedPixels[index] = multiply(pixels[index], tint);
|
||||||
|
}
|
||||||
|
return new BlockTexture(width, height, tintedPixels, opaque, alphas, multiply(averageColor, tint), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
int texelAt(double worldX, double worldZ) {
|
||||||
|
return pixels[texelIndex(worldX, worldZ)];
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isOpaqueAt(double worldX, double worldZ) {
|
||||||
|
return opaque[texelIndex(worldX, worldZ)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private int texelIndex(double worldX, double worldZ) {
|
||||||
|
int x = Math.min(width - 1, (int) ((worldX - Math.floor(worldX)) * width));
|
||||||
|
int z = Math.min(height - 1, (int) ((worldZ - Math.floor(worldZ)) * height));
|
||||||
|
return z * width + x;
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferedImage toImage() {
|
||||||
|
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||||
|
image.setRGB(0, 0, width, height, pixels, 0, width);
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The GPU multiplies the texture by the tint in floating point and rounds back to 8 bits per channel.
|
||||||
|
static int multiply(int color, int tint) {
|
||||||
|
return multiplyChannel(color, tint, 16) << 16 | multiplyChannel(color, tint, 8) << 8 | multiplyChannel(color, tint, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int multiplyChannel(int color, int tint, int shift) {
|
||||||
|
return (((color >>> shift) & 0xFF) * ((tint >>> shift) & 0xFF) + 127) / 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.color.ColorSpace;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.awt.image.Raster;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
final class ClientJarFiles {
|
||||||
|
private ClientJarFiles() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static JsonObject readJson(ZipFile clientJar, String path) throws IOException {
|
||||||
|
ZipEntry entry = clientJar.getEntry(path);
|
||||||
|
if (entry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try (InputStreamReader reader = new InputStreamReader(clientJar.getInputStream(entry), StandardCharsets.UTF_8)) {
|
||||||
|
return JsonParser.parseReader(reader).getAsJsonObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static BufferedImage readImage(ZipFile clientJar, String path) throws IOException {
|
||||||
|
ZipEntry entry = clientJar.getEntry(path);
|
||||||
|
if (entry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try (InputStream input = clientJar.getInputStream(entry)) {
|
||||||
|
BufferedImage image = ImageIO.read(input);
|
||||||
|
return image != null && isGrey(image) ? withGreyKept(image) : image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isGrey(BufferedImage image) {
|
||||||
|
return image.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_GRAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Java treats grey PNGs, like stone's, as linear light and brightens them when converting to RGB; Minecraft reads
|
||||||
|
// the stored values as they are.
|
||||||
|
private static BufferedImage withGreyKept(BufferedImage image) {
|
||||||
|
Raster raster = image.getRaster();
|
||||||
|
boolean hasAlpha = raster.getNumBands() > 1;
|
||||||
|
int greyMax = (1 << raster.getSampleModel().getSampleSize(0)) - 1;
|
||||||
|
int alphaMax = hasAlpha ? (1 << raster.getSampleModel().getSampleSize(1)) - 1 : 1;
|
||||||
|
BufferedImage converted = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||||
|
for (int y = 0; y < image.getHeight(); y++) {
|
||||||
|
for (int x = 0; x < image.getWidth(); x++) {
|
||||||
|
int grey = raster.getSample(x, y, 0) * 255 / greyMax;
|
||||||
|
int alpha = hasAlpha ? raster.getSample(x, y, 1) * 255 / alphaMax : 255;
|
||||||
|
converted.setRGB(x, y, alpha << 24 | grey << 16 | grey << 8 | grey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The six sides of a block, named as in Minecraft's model files, with the light Minecraft gives each side.
|
||||||
|
*/
|
||||||
|
enum CubeFace {
|
||||||
|
UP("up", 0, 1, 0, 1.0D),
|
||||||
|
DOWN("down", 0, -1, 0, 0.5D),
|
||||||
|
NORTH("north", 0, 0, -1, 0.8D),
|
||||||
|
SOUTH("south", 0, 0, 1, 0.8D),
|
||||||
|
EAST("east", 1, 0, 0, 0.6D),
|
||||||
|
WEST("west", -1, 0, 0, 0.6D);
|
||||||
|
|
||||||
|
private final String modelName;
|
||||||
|
private final int stepX;
|
||||||
|
private final int stepY;
|
||||||
|
private final int stepZ;
|
||||||
|
private final double shade;
|
||||||
|
|
||||||
|
CubeFace(String modelName, int stepX, int stepY, int stepZ, double shade) {
|
||||||
|
this.modelName = modelName;
|
||||||
|
this.stepX = stepX;
|
||||||
|
this.stepY = stepY;
|
||||||
|
this.stepZ = stepZ;
|
||||||
|
this.shade = shade;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CubeFace named(String modelName) {
|
||||||
|
for (CubeFace face : values()) {
|
||||||
|
if (face.modelName.equals(modelName)) {
|
||||||
|
return face;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The side a direction points at most, like Minecraft picks for a tilted face.
|
||||||
|
*/
|
||||||
|
static CubeFace nearest(Vec3 direction) {
|
||||||
|
CubeFace nearest = UP;
|
||||||
|
double best = Double.NEGATIVE_INFINITY;
|
||||||
|
for (CubeFace face : values()) {
|
||||||
|
double alignment = face.stepX * direction.x() + face.stepY * direction.y() + face.stepZ * direction.z();
|
||||||
|
if (alignment > best) {
|
||||||
|
best = alignment;
|
||||||
|
nearest = face;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
String modelName() {
|
||||||
|
return modelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
int stepX() {
|
||||||
|
return stepX;
|
||||||
|
}
|
||||||
|
|
||||||
|
int stepY() {
|
||||||
|
return stepY;
|
||||||
|
}
|
||||||
|
|
||||||
|
int stepZ() {
|
||||||
|
return stepZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
double shade() {
|
||||||
|
return shade;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 direction() {
|
||||||
|
return new Vec3(stepX, stepY, stepZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
CubeFace opposite() {
|
||||||
|
return switch (this) {
|
||||||
|
case UP -> DOWN;
|
||||||
|
case DOWN -> UP;
|
||||||
|
case NORTH -> SOUTH;
|
||||||
|
case SOUTH -> NORTH;
|
||||||
|
case EAST -> WEST;
|
||||||
|
case WEST -> EAST;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
int axis() {
|
||||||
|
return stepX != 0 ? 0 : stepY != 0 ? 1 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean pointsPositive() {
|
||||||
|
return stepX + stepY + stepZ > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import javax.swing.Icon;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.Font;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.geom.RoundRectangle2D;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What an empty view says, drawn in the middle of it the way IntelliJ fills an empty tool window: a large icon, what
|
||||||
|
* is missing, and where to add it.
|
||||||
|
*
|
||||||
|
* @param location the pack folder or file the hint points to, drawn as code
|
||||||
|
*/
|
||||||
|
record EmptyState(Icon icon, String title, String hint, String location, String footer) {
|
||||||
|
private static final int ICON_GAP = 18;
|
||||||
|
private static final int TITLE_GAP = 8;
|
||||||
|
private static final int LOCATION_GAP = 10;
|
||||||
|
private static final int FOOTER_GAP = 14;
|
||||||
|
private static final int CHIP_PADDING_X = 8;
|
||||||
|
private static final int CHIP_PADDING_Y = 4;
|
||||||
|
private static final int CHIP_ARC = 8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the empty state centred on the given point.
|
||||||
|
*/
|
||||||
|
void paint(Graphics2D graphics, Component owner, int centerX, int centerY) {
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
int top = centerY - height(canvas) / 2;
|
||||||
|
icon.paintIcon(owner, canvas, centerX - icon.getIconWidth() / 2, top);
|
||||||
|
top += icon.getIconHeight() + StudioTheme.scale(ICON_GAP);
|
||||||
|
top = drawCentered(canvas, title, StudioTheme.titleFont(), StudioTheme.TEXT, centerX, top);
|
||||||
|
top = drawCentered(canvas, hint, StudioTheme.font(), StudioTheme.SECONDARY_TEXT, centerX,
|
||||||
|
top + StudioTheme.scale(TITLE_GAP));
|
||||||
|
top = drawLocationChip(canvas, centerX, top + StudioTheme.scale(LOCATION_GAP));
|
||||||
|
drawCentered(canvas, footer, StudioTheme.smallFont(), StudioTheme.MUTED_TEXT, centerX, top + StudioTheme.scale(FOOTER_GAP));
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int height(Graphics2D canvas) {
|
||||||
|
return icon.getIconHeight() + StudioTheme.scale(ICON_GAP)
|
||||||
|
+ canvas.getFontMetrics(StudioTheme.titleFont()).getHeight()
|
||||||
|
+ StudioTheme.scale(TITLE_GAP) + canvas.getFontMetrics(StudioTheme.font()).getHeight()
|
||||||
|
+ StudioTheme.scale(LOCATION_GAP) + chipHeight(canvas)
|
||||||
|
+ StudioTheme.scale(FOOTER_GAP) + canvas.getFontMetrics(StudioTheme.smallFont()).getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int chipHeight(Graphics2D canvas) {
|
||||||
|
return canvas.getFontMetrics(StudioTheme.monospacedFont()).getHeight() + StudioTheme.scale(CHIP_PADDING_Y) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int drawLocationChip(Graphics2D canvas, int centerX, int top) {
|
||||||
|
canvas.setFont(StudioTheme.monospacedFont());
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics();
|
||||||
|
int width = metrics.stringWidth(location) + StudioTheme.scale(CHIP_PADDING_X) * 2;
|
||||||
|
int height = chipHeight(canvas);
|
||||||
|
int left = centerX - width / 2;
|
||||||
|
RoundRectangle2D chip = new RoundRectangle2D.Double(left + 0.5D, top + 0.5D, width - 1D, height - 1D,
|
||||||
|
StudioTheme.scale(CHIP_ARC), StudioTheme.scale(CHIP_ARC));
|
||||||
|
canvas.setColor(StudioTheme.PANEL_BACKGROUND);
|
||||||
|
canvas.fill(chip);
|
||||||
|
canvas.setColor(StudioTheme.CONTROL_BORDER);
|
||||||
|
canvas.draw(chip);
|
||||||
|
canvas.setColor(StudioTheme.TEXT);
|
||||||
|
canvas.drawString(location, left + StudioTheme.scale(CHIP_PADDING_X), top + StudioTheme.scale(CHIP_PADDING_Y) + metrics.getAscent());
|
||||||
|
return top + height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int drawCentered(Graphics2D canvas, String text, Font font, Color color, int centerX, int top) {
|
||||||
|
canvas.setFont(font);
|
||||||
|
canvas.setColor(color);
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics();
|
||||||
|
canvas.drawString(text, centerX - metrics.stringWidth(text) / 2, top + metrics.getAscent());
|
||||||
|
return top + metrics.getHeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.BlockModel.Face;
|
||||||
|
import com.volmit.iris.core.gui.BlockModel.TextureMapping;
|
||||||
|
import com.volmit.iris.core.gui.BlockTextures.BlockStateId;
|
||||||
|
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
|
||||||
|
import com.volmit.iris.core.gui.BlockTextures.ModelDefinition;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shapes for the blocks Minecraft draws in code instead of from a model file, like chests, heads, banners and
|
||||||
|
* fluids, which fill their block. Chests, heads, banners and the bell use their entity textures laid out the way Minecraft's model parts are;
|
||||||
|
* rarer ones are a simple box in their particle texture.
|
||||||
|
*/
|
||||||
|
final class EntityBlockShapes {
|
||||||
|
private static final Set<String> INVISIBLE = Set.of("air", "cave_air", "void_air", "barrier", "light",
|
||||||
|
"structure_void", "moving_piston", "pitcher_crop");
|
||||||
|
private static final int END_PORTAL_COLOR = 0x0E1219;
|
||||||
|
private static final double SKULL_WALL_OFFSET = 0.25D;
|
||||||
|
private static final double ROTATION_SEGMENT_DEGREES = 22.5D;
|
||||||
|
private static final double BANNER_SCALE = 2D / 3D;
|
||||||
|
private static final double MODEL_UNITS = 16D;
|
||||||
|
private static final Map<String, Integer> DYE_COLORS = Map.ofEntries(
|
||||||
|
Map.entry("white", 0xF9FFFE), Map.entry("orange", 0xF9801D), Map.entry("magenta", 0xC74EBD),
|
||||||
|
Map.entry("light_blue", 0x3AB3DA), Map.entry("yellow", 0xFED83D), Map.entry("lime", 0x80C71F),
|
||||||
|
Map.entry("pink", 0xF38BAA), Map.entry("gray", 0x474F52), Map.entry("light_gray", 0x9D9D97),
|
||||||
|
Map.entry("cyan", 0x169C9C), Map.entry("purple", 0x8932B8), Map.entry("blue", 0x3C44AA),
|
||||||
|
Map.entry("brown", 0x835432), Map.entry("green", 0x5E7C16), Map.entry("red", 0xB02E26),
|
||||||
|
Map.entry("black", 0x1D1D21));
|
||||||
|
|
||||||
|
private final BlockTextures textures;
|
||||||
|
|
||||||
|
EntityBlockShapes(BlockTextures textures) {
|
||||||
|
this.textures = textures;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The faces of a block whose model has no boxes of its own.
|
||||||
|
*/
|
||||||
|
List<Face> facesOf(BlockStateId state, ModelDefinition model) {
|
||||||
|
String name = state.name();
|
||||||
|
if (INVISIBLE.contains(name)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
if (name.endsWith("chest")) {
|
||||||
|
return chest(state);
|
||||||
|
}
|
||||||
|
if (name.endsWith("_skull") || name.endsWith("_head")) {
|
||||||
|
return head(state, model);
|
||||||
|
}
|
||||||
|
if (name.endsWith("banner")) {
|
||||||
|
return banner(state);
|
||||||
|
}
|
||||||
|
return switch (name) {
|
||||||
|
case "conduit" -> particleBox(state, model, new Vec3(5, 5, 5), new Vec3(11, 11, 11), false);
|
||||||
|
case "decorated_pot" -> decoratedPot(state, model);
|
||||||
|
case "end_portal" -> BlockModelBuilder.box(new Vec3(0, 0, 0), new Vec3(16, 12, 16), BlockTexture.solid(END_PORTAL_COLOR), true);
|
||||||
|
case "end_gateway" -> BlockModelBuilder.box(new Vec3(0, 0, 0), new Vec3(16, 16, 16), BlockTexture.solid(END_PORTAL_COLOR), true);
|
||||||
|
default -> name.endsWith("copper_golem_statue")
|
||||||
|
? particleBox(state, model, new Vec3(4, 0, 4), new Vec3(12, 14, 12), false)
|
||||||
|
: particleBox(state, model, new Vec3(0, 0, 0), new Vec3(16, 16, 16), true);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parts a block draws in code on top of its model, like the bell hanging in its frame.
|
||||||
|
*/
|
||||||
|
List<Face> facesAddedTo(BlockStateId state) {
|
||||||
|
if (!state.name().equals("bell")) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Optional<BlockTexture> texture = textures.entityTexture("entity/bell/bell_body", MinecraftBlockTints.NO_TINT);
|
||||||
|
if (texture.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<Face> faces = new ArrayList<>();
|
||||||
|
faces.addAll(modelPartBox(texture.get(), 0, 0, new Vec3(5, 6, 5), new Vec3(6, 7, 6), Transform.IDENTITY));
|
||||||
|
faces.addAll(modelPartBox(texture.get(), 0, 13, new Vec3(4, 4, 4), new Vec3(8, 2, 8), Transform.IDENTITY));
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Face> particleBox(BlockStateId state, ModelDefinition model, Vec3 from, Vec3 to, boolean culledBySides) {
|
||||||
|
String particle = BlockTextures.dereference("#particle", model.textures());
|
||||||
|
if (particle == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return textures.faceTexture(state, particle, BlockTextures.NO_TINT_INDEX)
|
||||||
|
.map(texture -> BlockModelBuilder.box(from, to, texture, culledBySides))
|
||||||
|
.orElse(List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Face> decoratedPot(BlockStateId state, ModelDefinition model) {
|
||||||
|
List<Face> faces = new ArrayList<>(particleBox(state, model, new Vec3(1, 0, 1), new Vec3(15, 16, 15), false));
|
||||||
|
faces.addAll(particleBox(state, model, new Vec3(4, 16, 4), new Vec3(12, 20, 12), false));
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single chests are 14 pixels wide; each half of a double chest is 15, reaching over to its partner.
|
||||||
|
private List<Face> chest(BlockStateId state) {
|
||||||
|
String type = state.properties().getOrDefault("type", "single");
|
||||||
|
String suffix = type.equals("single") ? "" : "_" + type;
|
||||||
|
Optional<BlockTexture> texture = textures.entityTexture("entity/chest/" + chestTextureName(state.name()) + suffix, MinecraftBlockTints.NO_TINT);
|
||||||
|
if (texture.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Transform place = Transform.IDENTITY
|
||||||
|
.translate(0.5D, 0.5D, 0.5D)
|
||||||
|
.rotateY(-yRotationOf(state.properties().getOrDefault("facing", "north")))
|
||||||
|
.translate(-0.5D, -0.5D, -0.5D);
|
||||||
|
double left = type.equals("left") ? 0D : 1D;
|
||||||
|
double width = type.equals("single") ? 14D : 15D;
|
||||||
|
double lockX = switch (type) {
|
||||||
|
case "left" -> 0D;
|
||||||
|
case "right" -> 15D;
|
||||||
|
default -> 7D;
|
||||||
|
};
|
||||||
|
double lockWidth = type.equals("single") ? 2D : 1D;
|
||||||
|
List<Face> faces = new ArrayList<>();
|
||||||
|
faces.addAll(modelPartBox(texture.get(), 0, 19, new Vec3(left, 0, 1), new Vec3(width, 10, 14), place));
|
||||||
|
faces.addAll(modelPartBox(texture.get(), 0, 0, new Vec3(left, 9, 1), new Vec3(width, 5, 14), place));
|
||||||
|
faces.addAll(modelPartBox(texture.get(), 0, 0, new Vec3(lockX, 7, 15), new Vec3(lockWidth, 4, 1), place));
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String chestTextureName(String blockName) {
|
||||||
|
String name = blockName.startsWith("waxed_") ? blockName.substring("waxed_".length()) : blockName;
|
||||||
|
return switch (name) {
|
||||||
|
case "trapped_chest" -> "trapped";
|
||||||
|
case "ender_chest" -> "ender";
|
||||||
|
case "copper_chest" -> "copper";
|
||||||
|
case "exposed_copper_chest" -> "copper_exposed";
|
||||||
|
case "weathered_copper_chest" -> "copper_weathered";
|
||||||
|
case "oxidized_copper_chest" -> "copper_oxidized";
|
||||||
|
default -> "normal";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heads are drawn upside down and mirrored in their model, and turn in sixteenths like signs.
|
||||||
|
private List<Face> head(BlockStateId state, ModelDefinition model) {
|
||||||
|
boolean onWall = state.name().contains("_wall_");
|
||||||
|
String kind = state.name().replace("_wall_", "_");
|
||||||
|
Optional<HeadLook> look = HeadLook.of(kind);
|
||||||
|
Optional<BlockTexture> texture = look.flatMap(found -> textures.entityTexture(found.texture(), MinecraftBlockTints.NO_TINT));
|
||||||
|
if (texture.isEmpty()) {
|
||||||
|
return onWall
|
||||||
|
? particleBox(state, model, new Vec3(4, 4, 8), new Vec3(12, 12, 16), false)
|
||||||
|
: particleBox(state, model, new Vec3(4, 0, 4), new Vec3(12, 8, 12), false);
|
||||||
|
}
|
||||||
|
Transform place;
|
||||||
|
if (onWall) {
|
||||||
|
CubeFace facing = CubeFace.named(state.properties().getOrDefault("facing", "north"));
|
||||||
|
place = Transform.IDENTITY
|
||||||
|
.translate(0.5D - facing.stepX() * SKULL_WALL_OFFSET, SKULL_WALL_OFFSET, 0.5D - facing.stepZ() * SKULL_WALL_OFFSET)
|
||||||
|
.scale(-1D, -1D, 1D)
|
||||||
|
.rotateY(yRotationOf(facing.opposite().modelName()));
|
||||||
|
} else {
|
||||||
|
int rotation = Integer.parseInt(state.properties().getOrDefault("rotation", "0"));
|
||||||
|
place = Transform.IDENTITY
|
||||||
|
.translate(0.5D, 0D, 0.5D)
|
||||||
|
.scale(-1D, -1D, 1D)
|
||||||
|
.rotateY(rotation * ROTATION_SEGMENT_DEGREES);
|
||||||
|
}
|
||||||
|
HeadLook found = look.get();
|
||||||
|
return modelPartBox(texture.get(), 0, 0, new Vec3(-found.width() / 2D, -8, -4), new Vec3(found.width(), 8, 8), place);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Face> banner(BlockStateId state) {
|
||||||
|
boolean onWall = state.name().endsWith("_wall_banner");
|
||||||
|
String color = state.name().substring(0, state.name().length() - (onWall ? "_wall_banner" : "_banner").length());
|
||||||
|
Optional<BlockTexture> frame = textures.entityTexture("entity/banner/banner_base", MinecraftBlockTints.NO_TINT);
|
||||||
|
Optional<BlockTexture> cloth = textures.entityTexture("entity/banner/base", DYE_COLORS.getOrDefault(color, MinecraftBlockTints.NO_TINT));
|
||||||
|
if (frame.isEmpty() || cloth.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Transform place;
|
||||||
|
if (onWall) {
|
||||||
|
CubeFace facing = CubeFace.named(state.properties().getOrDefault("facing", "north"));
|
||||||
|
place = Transform.IDENTITY
|
||||||
|
.translate(0.5D, -1D / 6D, 0.5D)
|
||||||
|
.rotateY(-yRotationOf(facing.modelName()))
|
||||||
|
.translate(0D, -0.3125D, -0.4375D)
|
||||||
|
.scale(BANNER_SCALE, -BANNER_SCALE, -BANNER_SCALE);
|
||||||
|
} else {
|
||||||
|
int rotation = Integer.parseInt(state.properties().getOrDefault("rotation", "0"));
|
||||||
|
place = Transform.IDENTITY
|
||||||
|
.translate(0.5D, 0.5D, 0.5D)
|
||||||
|
.rotateY(-rotation * ROTATION_SEGMENT_DEGREES)
|
||||||
|
.scale(BANNER_SCALE, -BANNER_SCALE, -BANNER_SCALE);
|
||||||
|
}
|
||||||
|
List<Face> faces = new ArrayList<>();
|
||||||
|
if (!onWall) {
|
||||||
|
faces.addAll(modelPartBox(frame.get(), 44, 0, new Vec3(-1, -30, -1), new Vec3(2, 42, 2), place));
|
||||||
|
}
|
||||||
|
faces.addAll(modelPartBox(frame.get(), 0, 42, new Vec3(-10, -32, -1), new Vec3(20, 2, 2), place));
|
||||||
|
faces.addAll(modelPartBox(cloth.get(), 0, 0, new Vec3(-10, -32, -2), new Vec3(20, 40, 1), place));
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minecraft's facing angle: south is 0 and each quarter turn clockwise seen from above adds 90 degrees.
|
||||||
|
private static double yRotationOf(String facing) {
|
||||||
|
return switch (facing) {
|
||||||
|
case "west" -> 90D;
|
||||||
|
case "north" -> 180D;
|
||||||
|
case "east" -> 270D;
|
||||||
|
default -> 0D;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A box of a block entity's model, in model pixels, with its sides laid out on the texture the way Minecraft's
|
||||||
|
* model parts are: the top and bottom in a row above the four sides.
|
||||||
|
*/
|
||||||
|
static List<Face> modelPartBox(BlockTexture texture, int textureU, int textureV, Vec3 from, Vec3 size, Transform place) {
|
||||||
|
double minX = from.x() / MODEL_UNITS;
|
||||||
|
double minY = from.y() / MODEL_UNITS;
|
||||||
|
double minZ = from.z() / MODEL_UNITS;
|
||||||
|
double maxX = (from.x() + size.x()) / MODEL_UNITS;
|
||||||
|
double maxY = (from.y() + size.y()) / MODEL_UNITS;
|
||||||
|
double maxZ = (from.z() + size.z()) / MODEL_UNITS;
|
||||||
|
Vec3 v0 = new Vec3(minX, minY, minZ);
|
||||||
|
Vec3 v1 = new Vec3(maxX, minY, minZ);
|
||||||
|
Vec3 v2 = new Vec3(maxX, maxY, minZ);
|
||||||
|
Vec3 v3 = new Vec3(minX, maxY, minZ);
|
||||||
|
Vec3 v4 = new Vec3(minX, minY, maxZ);
|
||||||
|
Vec3 v5 = new Vec3(maxX, minY, maxZ);
|
||||||
|
Vec3 v6 = new Vec3(maxX, maxY, maxZ);
|
||||||
|
Vec3 v7 = new Vec3(minX, maxY, maxZ);
|
||||||
|
double u = textureU;
|
||||||
|
double v = textureV;
|
||||||
|
double width = size.x();
|
||||||
|
double height = size.y();
|
||||||
|
double depth = size.z();
|
||||||
|
List<Face> faces = new ArrayList<>(6);
|
||||||
|
faces.add(polygon(texture, place, CubeFace.DOWN, v5, v4, v0, u + depth, v, u + depth + width, v + depth));
|
||||||
|
faces.add(polygon(texture, place, CubeFace.UP, v2, v3, v7, u + depth + width, v + depth, u + depth + width + width, v));
|
||||||
|
faces.add(polygon(texture, place, CubeFace.WEST, v0, v4, v7, u, v + depth, u + depth, v + depth + height));
|
||||||
|
faces.add(polygon(texture, place, CubeFace.NORTH, v1, v0, v3, u + depth, v + depth, u + depth + width, v + depth + height));
|
||||||
|
faces.add(polygon(texture, place, CubeFace.EAST, v5, v1, v2, u + depth + width, v + depth, u + depth + width + depth, v + depth + height));
|
||||||
|
faces.add(polygon(texture, place, CubeFace.SOUTH, v4, v5, v6, u + depth + width + depth, v + depth, u + depth + width + depth + width, v + depth + height));
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A model part's polygon gives its first corner (u2, v1), its second (u1, v1) and its third (u1, v2).
|
||||||
|
private static Face polygon(BlockTexture texture, Transform place, CubeFace side, Vec3 first, Vec3 second, Vec3 third,
|
||||||
|
double u1, double v1, double u2, double v2) {
|
||||||
|
double textureWidth = texture.width();
|
||||||
|
double textureHeight = texture.height();
|
||||||
|
TextureMapping mapping = new TextureMapping(u1 / textureWidth, v1 / textureHeight,
|
||||||
|
(u2 - u1) / textureWidth, 0D, 0D, (v2 - v1) / textureHeight);
|
||||||
|
Face face = new Face(second, first.minus(second), third.minus(second), side.direction(), texture, mapping, side.shade(), null)
|
||||||
|
.transformed(place);
|
||||||
|
return face.withShade(CubeFace.nearest(face.normal()).shade());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The entity texture of a mob head and how wide its head is.
|
||||||
|
*/
|
||||||
|
private record HeadLook(String texture, double width) {
|
||||||
|
private static final double HEAD_WIDTH = 8D;
|
||||||
|
private static final double PIGLIN_HEAD_WIDTH = 10D;
|
||||||
|
|
||||||
|
static Optional<HeadLook> of(String kind) {
|
||||||
|
return Optional.ofNullable(switch (kind) {
|
||||||
|
case "skeleton_skull" -> new HeadLook("entity/skeleton/skeleton", HEAD_WIDTH);
|
||||||
|
case "wither_skeleton_skull" -> new HeadLook("entity/skeleton/wither_skeleton", HEAD_WIDTH);
|
||||||
|
case "zombie_head" -> new HeadLook("entity/zombie/zombie", HEAD_WIDTH);
|
||||||
|
case "creeper_head" -> new HeadLook("entity/creeper/creeper", HEAD_WIDTH);
|
||||||
|
case "player_head" -> new HeadLook("entity/player/wide/steve", HEAD_WIDTH);
|
||||||
|
case "piglin_head" -> new HeadLook("entity/piglin/piglin", PIGLIN_HEAD_WIDTH);
|
||||||
|
default -> null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.Iris;
|
||||||
|
import com.volmit.iris.core.events.IrisEngineHotloadEvent;
|
||||||
|
import com.volmit.iris.core.project.IrisProject;
|
||||||
|
import com.volmit.iris.core.service.PreservationSVC;
|
||||||
|
import com.volmit.iris.core.service.StudioSVC;
|
||||||
|
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||||
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
|
import com.volmit.iris.engine.platform.PlatformChunkGenerator;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
|
||||||
|
import javax.swing.JFrame;
|
||||||
|
import java.awt.Desktop;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.EventQueue;
|
||||||
|
import java.awt.GraphicsEnvironment;
|
||||||
|
import java.awt.Rectangle;
|
||||||
|
import java.awt.desktop.QuitResponse;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.awt.event.WindowAdapter;
|
||||||
|
import java.awt.event.WindowEvent;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public final class GuiHost {
|
||||||
|
private static final AtomicBoolean DESKTOP_QUIT_GUARD_INSTALLED = new AtomicBoolean(false);
|
||||||
|
private static final Set<JFrame> MANAGED_FRAMES = ConcurrentHashMap.newKeySet();
|
||||||
|
private static final Map<Runnable, Listener> HOTLOAD_HOOKS = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private GuiHost() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAvailable() {
|
||||||
|
return !GraphicsEnvironment.isHeadless();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JFrame createFrame(String title) {
|
||||||
|
XwaylandWindowManagerHint.apply();
|
||||||
|
StudioTheme.install();
|
||||||
|
JFrame frame = new JFrame(title);
|
||||||
|
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||||
|
MANAGED_FRAMES.add(frame);
|
||||||
|
frame.addWindowListener(new WindowAdapter() {
|
||||||
|
@Override
|
||||||
|
public void windowClosed(WindowEvent event) {
|
||||||
|
MANAGED_FRAMES.remove(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void windowActivated(WindowEvent event) {
|
||||||
|
StudioTheme.followSystem();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
installMacQuitGuard();
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps rendered pixels in an image Java2D draws straight from memory. Left to itself Java2D copies every image it
|
||||||
|
* draws into a texture inside the display server; with a map's hundreds of tiles that crashed Xwayland's Intel driver
|
||||||
|
* and took every X window on the desktop down with it.
|
||||||
|
*/
|
||||||
|
public static BufferedImage uncachedImage(int[] pixels, int width, int height) {
|
||||||
|
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||||
|
image.getRaster().setDataElements(0, 0, width, height, pixels);
|
||||||
|
image.setAccelerationPriority(0F);
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A scaled-up default size can be bigger than a small or low resolution screen.
|
||||||
|
static Dimension fitToScreen(Dimension preferred) {
|
||||||
|
Rectangle usable = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
|
||||||
|
return new Dimension(Math.min(preferred.width, usable.width), Math.min(preferred.height, usable.height));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The engine of the open studio project, which reads the pack folder you edit, or else the first Iris world.
|
||||||
|
* A normal Iris world runs on its own copy of the pack, made when the world was created.
|
||||||
|
*/
|
||||||
|
public static Engine findActiveEngine() {
|
||||||
|
Engine studioEngine = findStudioEngine();
|
||||||
|
if (studioEngine != null) {
|
||||||
|
return studioEngine;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (World world : new ArrayList<>(Bukkit.getWorlds())) {
|
||||||
|
Engine engine = engineOf(world);
|
||||||
|
if (engine != null) {
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Engine findStudioEngine() {
|
||||||
|
try {
|
||||||
|
StudioSVC studio = Iris.service(StudioSVC.class);
|
||||||
|
IrisProject project = studio == null || !studio.isProjectOpen() ? null : studio.getActiveProject();
|
||||||
|
PlatformChunkGenerator provider = project == null ? null : project.getActiveProvider();
|
||||||
|
Engine engine = provider == null ? null : provider.getEngine();
|
||||||
|
return engine == null || engine.isClosed() ? null : engine;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Engine findEngine(UUID worldId) {
|
||||||
|
World world = Bukkit.getWorld(worldId);
|
||||||
|
return world == null ? null : engineOf(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerHotloadHook(Runnable onHotload) {
|
||||||
|
Listener listener = new HotloadListener(onHotload);
|
||||||
|
HOTLOAD_HOOKS.put(onHotload, listener);
|
||||||
|
Iris.instance.registerListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void unregisterHotloadHook(Runnable onHotload) {
|
||||||
|
Listener listener = HOTLOAD_HOOKS.remove(onHotload);
|
||||||
|
if (listener != null) {
|
||||||
|
Iris.instance.unregisterListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void interruptOnPluginShutdown(ExecutorService executor) {
|
||||||
|
PreservationSVC preservation = Iris.service(PreservationSVC.class);
|
||||||
|
if (preservation != null) {
|
||||||
|
preservation.register(executor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void interruptOnPluginShutdown(Thread thread) {
|
||||||
|
PreservationSVC preservation = Iris.service(PreservationSVC.class);
|
||||||
|
if (preservation != null) {
|
||||||
|
preservation.register(thread);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Engine engineOf(World world) {
|
||||||
|
try {
|
||||||
|
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||||
|
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
|
||||||
|
return access.getEngine();
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// macOS Quit in the dock menu would otherwise stop the whole server.
|
||||||
|
private static void installMacQuitGuard() {
|
||||||
|
if (!isAvailable()
|
||||||
|
|| !System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("mac")
|
||||||
|
|| !DESKTOP_QUIT_GUARD_INSTALLED.compareAndSet(false, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!Desktop.isDesktopSupported()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Desktop desktop = Desktop.getDesktop();
|
||||||
|
if (!desktop.isSupported(Desktop.Action.APP_QUIT_HANDLER)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
desktop.setQuitHandler((event, response) -> closeWindowsAndCancelQuit(response));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
Iris.info("Unable to install the Iris desktop quit guard; use the server stop command instead of macOS Quit");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void closeWindowsAndCancelQuit(QuitResponse response) {
|
||||||
|
response.cancelQuit();
|
||||||
|
EventQueue.invokeLater(() -> {
|
||||||
|
for (JFrame frame : MANAGED_FRAMES) {
|
||||||
|
frame.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class HotloadListener implements Listener {
|
||||||
|
private final Runnable onHotload;
|
||||||
|
|
||||||
|
private HotloadListener(Runnable onHotload) {
|
||||||
|
this.onHotload = onHotload;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void on(IrisEngineHotloadEvent event) {
|
||||||
|
onHotload.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
public record GuiMarker(String label, double worldX, double worldY, double worldZ, double health, double maxHealth) {
|
||||||
|
public static GuiMarker player(String name, double worldX, double worldZ) {
|
||||||
|
return new GuiMarker(name, worldX, 0, worldZ, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GuiMarker entity(String label, double worldX, double worldY, double worldZ, double health, double maxHealth) {
|
||||||
|
return new GuiMarker(label, worldX, worldY, worldZ, health, maxHealth);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JLayeredPane;
|
||||||
|
import java.awt.Cursor;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lays an island over content that fills the whole area behind it. Dragging the island's right edge resizes it,
|
||||||
|
* always leaving the content some room beside it.
|
||||||
|
*/
|
||||||
|
final class IslandOverlay extends JLayeredPane {
|
||||||
|
private static final long serialVersionUID = -4182209135360718342L;
|
||||||
|
|
||||||
|
private final IslandPanel island;
|
||||||
|
private final JComponent background;
|
||||||
|
private final int minimumIslandWidth;
|
||||||
|
private final int minimumUncoveredWidth;
|
||||||
|
private int islandWidth;
|
||||||
|
|
||||||
|
IslandOverlay(IslandPanel island, JComponent background, int islandWidth, int minimumIslandWidth, int minimumUncoveredWidth) {
|
||||||
|
this.island = island;
|
||||||
|
this.background = background;
|
||||||
|
this.islandWidth = islandWidth;
|
||||||
|
this.minimumIslandWidth = minimumIslandWidth;
|
||||||
|
this.minimumUncoveredWidth = minimumUncoveredWidth;
|
||||||
|
add(background, JLayeredPane.DEFAULT_LAYER);
|
||||||
|
add(island, JLayeredPane.PALETTE_LAYER);
|
||||||
|
installResizing();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doLayout() {
|
||||||
|
background.setBounds(0, 0, getWidth(), getHeight());
|
||||||
|
island.setBounds(0, 0, clampedIslandWidth(), getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int clampedIslandWidth() {
|
||||||
|
int widest = Math.max(minimumIslandWidth, getWidth() - minimumUncoveredWidth);
|
||||||
|
return Math.clamp(islandWidth, minimumIslandWidth, widest);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void installResizing() {
|
||||||
|
MouseAdapter resizer = new MouseAdapter() {
|
||||||
|
private boolean dragging;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseMoved(MouseEvent event) {
|
||||||
|
island.setCursor(island.isOnResizeEdge(event.getX(), event.getY())
|
||||||
|
? Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR)
|
||||||
|
: Cursor.getDefaultCursor());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseExited(MouseEvent event) {
|
||||||
|
if (!dragging) {
|
||||||
|
island.setCursor(Cursor.getDefaultCursor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mousePressed(MouseEvent event) {
|
||||||
|
dragging = island.isOnResizeEdge(event.getX(), event.getY());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseDragged(MouseEvent event) {
|
||||||
|
if (!dragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
islandWidth = event.getX();
|
||||||
|
islandWidth = clampedIslandWidth();
|
||||||
|
revalidate();
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent event) {
|
||||||
|
dragging = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
island.addMouseListener(resizer);
|
||||||
|
island.addMouseMotionListener(resizer);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.geom.RoundRectangle2D;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A rounded panel floating over the window content with a gap all around it, like the tool windows of IntelliJ's
|
||||||
|
* islands theme. The gap belongs to the content behind it, except for the strip along the right edge, which grabs the
|
||||||
|
* island to resize it.
|
||||||
|
*/
|
||||||
|
final class IslandPanel extends JPanel {
|
||||||
|
private static final long serialVersionUID = 7313084027614358519L;
|
||||||
|
private static final int GAP = 6;
|
||||||
|
private static final int CORNER_ARC = 12;
|
||||||
|
private static final int INNER_PADDING = 4;
|
||||||
|
|
||||||
|
IslandPanel(JComponent content) {
|
||||||
|
super(new BorderLayout());
|
||||||
|
setOpaque(false);
|
||||||
|
int inset = GAP + INNER_PADDING;
|
||||||
|
setBorder(StudioTheme.emptyBorder(inset, inset, inset, inset));
|
||||||
|
add(content, BorderLayout.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isOnResizeEdge(int x, int y) {
|
||||||
|
int edgeWidth = StudioTheme.scale(GAP + INNER_PADDING);
|
||||||
|
return x >= getWidth() - edgeWidth && x < getWidth() && y >= 0 && y < getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clicks in the gap reach the content behind the island instead of the island itself.
|
||||||
|
@Override
|
||||||
|
public boolean contains(int x, int y) {
|
||||||
|
return island().contains(x, y) || isOnResizeEdge(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics graphics) {
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
canvas.setColor(StudioTheme.PANEL_BACKGROUND);
|
||||||
|
canvas.fill(island());
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RoundRectangle2D island() {
|
||||||
|
double gap = StudioTheme.scale(GAP);
|
||||||
|
double arc = StudioTheme.scale(CORNER_ARC);
|
||||||
|
return new RoundRectangle2D.Double(gap, gap, getWidth() - gap * 2D, getHeight() - gap * 2D, arc, arc);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.Iris;
|
||||||
|
|
||||||
|
import java.awt.EventQueue;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures off the Swing thread, one measurement at a time, and only hands back the newest one asked for.
|
||||||
|
*/
|
||||||
|
final class LatestMeasurement<T> {
|
||||||
|
private final ThreadPoolExecutor worker;
|
||||||
|
private final AtomicLong generation = new AtomicLong();
|
||||||
|
|
||||||
|
LatestMeasurement(String threadName) {
|
||||||
|
this.worker = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(1), task -> {
|
||||||
|
Thread thread = new Thread(task, threadName);
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
}, new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the measurement and shows its result on the Swing thread, unless another was asked for meanwhile or it
|
||||||
|
* measured nothing.
|
||||||
|
*/
|
||||||
|
void measure(Supplier<T> measurement, Consumer<T> show) {
|
||||||
|
long requested = generation.incrementAndGet();
|
||||||
|
worker.execute(() -> {
|
||||||
|
T result = measureSafely(measurement);
|
||||||
|
EventQueue.invokeLater(() -> {
|
||||||
|
if (requested == generation.get() && result != null) {
|
||||||
|
show.accept(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops the measurement under way, whose result no longer fits.
|
||||||
|
*/
|
||||||
|
void discardPending() {
|
||||||
|
generation.incrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() {
|
||||||
|
worker.shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T measureSafely(Supplier<T> measurement) {
|
||||||
|
try {
|
||||||
|
return measurement.get();
|
||||||
|
} catch (Throwable error) {
|
||||||
|
Iris.reportError(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import java.awt.AlphaComposite;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.geom.Path2D;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A little cube of blocks building itself while something loads: the blocks drop in one by one, the cube holds a
|
||||||
|
* moment, fades, and starts over. Loads shorter than {@link #SHOW_AFTER_MILLIS} never show it, so they don't flash.
|
||||||
|
*/
|
||||||
|
final class LoadingAnimation {
|
||||||
|
static final int SHOW_AFTER_MILLIS = 300;
|
||||||
|
static final int FRAME_MILLIS = 33;
|
||||||
|
|
||||||
|
// Back to front, so each block lands on or beside ones already there.
|
||||||
|
private static final int[][] BUILD_ORDER = {
|
||||||
|
{0, 0, 0}, {1, 0, 0}, {0, 0, 1}, {1, 0, 1},
|
||||||
|
{0, 1, 0}, {1, 1, 0}, {0, 1, 1}, {1, 1, 1}};
|
||||||
|
private static final int STEP_MILLIS = 140;
|
||||||
|
private static final int DROP_MILLIS = 260;
|
||||||
|
private static final int HOLD_MILLIS = 450;
|
||||||
|
private static final int FADE_MILLIS = 320;
|
||||||
|
private static final int CYCLE_MILLIS = STEP_MILLIS * (BUILD_ORDER.length - 1) + DROP_MILLIS + HOLD_MILLIS + FADE_MILLIS;
|
||||||
|
private static final double DROP_HEIGHT_BLOCKS = 1.6D;
|
||||||
|
private static final double ISOMETRIC_WIDTH = Math.cos(Math.PI / 6D);
|
||||||
|
private static final double ISOMETRIC_DEPTH = 0.5D;
|
||||||
|
private static final float SIDE_SHADE = 0.78F;
|
||||||
|
private static final float FAR_SIDE_SHADE = 0.6F;
|
||||||
|
|
||||||
|
private LoadingAnimation() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a load that began at {@code loadingSinceMillis} has taken long enough to show the animation.
|
||||||
|
*/
|
||||||
|
static boolean isDue(long loadingSinceMillis) {
|
||||||
|
return System.currentTimeMillis() - loadingSinceMillis >= SHOW_AFTER_MILLIS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the cube centred on a point, {@code blockSize} pixels to a block edge, as it looks
|
||||||
|
* {@code elapsedMillis} after it first showed.
|
||||||
|
*/
|
||||||
|
static void paint(Graphics2D graphics, double centerX, double centerY, double blockSize, long elapsedMillis) {
|
||||||
|
long time = Math.floorMod(elapsedMillis, CYCLE_MILLIS);
|
||||||
|
long fadeStart = CYCLE_MILLIS - FADE_MILLIS;
|
||||||
|
if (time < fadeStart) {
|
||||||
|
drawCube(graphics, centerX, centerY, blockSize, time);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fadeCube(graphics, centerX, centerY, blockSize, time, 1F - (time - fadeStart) / (float) FADE_MILLIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Faded block by block, the blocks behind would show through; the whole cube is drawn first and faded as one.
|
||||||
|
private static void fadeCube(Graphics2D graphics, double centerX, double centerY, double blockSize, long time, float opacity) {
|
||||||
|
int extent = (int) Math.ceil(blockSize * (2D + DROP_HEIGHT_BLOCKS) * 2D);
|
||||||
|
BufferedImage cube = new BufferedImage(extent, extent, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D cubeCanvas = cube.createGraphics();
|
||||||
|
drawCube(cubeCanvas, extent / 2D, extent / 2D, blockSize, time);
|
||||||
|
cubeCanvas.dispose();
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
canvas.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, Math.max(0F, opacity)));
|
||||||
|
canvas.drawImage(cube, (int) Math.round(centerX - extent / 2D), (int) Math.round(centerY - extent / 2D), null);
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawCube(Graphics2D graphics, double centerX, double centerY, double blockSize, long time) {
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
for (int index = 0; index < BUILD_ORDER.length; index++) {
|
||||||
|
long sinceDropStart = time - (long) index * STEP_MILLIS;
|
||||||
|
if (sinceDropStart < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
double fallen = easeOut(Math.min(1D, sinceDropStart / (double) DROP_MILLIS));
|
||||||
|
int[] block = BUILD_ORDER[index];
|
||||||
|
drawBlock(canvas, centerX, centerY, blockSize, block[0], block[1] + DROP_HEIGHT_BLOCKS * (1D - fallen), block[2]);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double easeOut(double progress) {
|
||||||
|
double remaining = 1D - progress;
|
||||||
|
return 1D - remaining * remaining * remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cube's middle, (1, 1, 1), lands on the centre point; the top and the two sides facing the viewer show.
|
||||||
|
private static void drawBlock(Graphics2D canvas, double centerX, double centerY, double size, double x, double y, double z) {
|
||||||
|
Color top = StudioTheme.ACCENT;
|
||||||
|
canvas.setColor(top);
|
||||||
|
canvas.fill(face(centerX, centerY, size, x, y, z, new double[][]{{0, 1, 0}, {1, 1, 0}, {1, 1, 1}, {0, 1, 1}}));
|
||||||
|
canvas.setColor(shaded(top, SIDE_SHADE));
|
||||||
|
canvas.fill(face(centerX, centerY, size, x, y, z, new double[][]{{1, 0, 0}, {1, 1, 0}, {1, 1, 1}, {1, 0, 1}}));
|
||||||
|
canvas.setColor(shaded(top, FAR_SIDE_SHADE));
|
||||||
|
canvas.fill(face(centerX, centerY, size, x, y, z, new double[][]{{0, 0, 1}, {1, 0, 1}, {1, 1, 1}, {0, 1, 1}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path2D face(double centerX, double centerY, double size, double x, double y, double z, double[][] corners) {
|
||||||
|
Path2D path = new Path2D.Double();
|
||||||
|
for (int corner = 0; corner < corners.length; corner++) {
|
||||||
|
double cornerX = x + corners[corner][0] - 1D;
|
||||||
|
double cornerY = y + corners[corner][1] - 1D;
|
||||||
|
double cornerZ = z + corners[corner][2] - 1D;
|
||||||
|
double screenX = centerX + (cornerX - cornerZ) * ISOMETRIC_WIDTH * size;
|
||||||
|
double screenY = centerY + (cornerX + cornerZ) * ISOMETRIC_DEPTH * size - cornerY * size;
|
||||||
|
if (corner == 0) {
|
||||||
|
path.moveTo(screenX, screenY);
|
||||||
|
} else {
|
||||||
|
path.lineTo(screenX, screenY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path.closePath();
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color shaded(Color color, float shade) {
|
||||||
|
return new Color(Math.round(color.getRed() * shade), Math.round(color.getGreen() * shade), Math.round(color.getBlue() * shade));
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,183 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.components.MapLabels;
|
||||||
|
import com.volmit.iris.core.gui.components.MapStyle;
|
||||||
|
import com.volmit.iris.core.gui.components.MapTile;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.ToIntFunction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the visible part of the map from the tiles already drawn: how much each biome or region covers, and which
|
||||||
|
* heights it spans.
|
||||||
|
*/
|
||||||
|
final class MapCensus {
|
||||||
|
private static final long MAXIMUM_COUNTED_PIXELS = 250_000L;
|
||||||
|
private static final int MINIMUM_HEIGHT_SAMPLES = 64;
|
||||||
|
private static final double LOW_HEIGHT_PERCENTILE = 0.01D;
|
||||||
|
private static final double HIGH_HEIGHT_PERCENTILE = 0.99D;
|
||||||
|
private static final double HEIGHT_RANGE_STEP_BLOCKS = 8D;
|
||||||
|
private static final double MINIMUM_WATER_DEPTH_BLOCKS = 8D;
|
||||||
|
private static final double MINIMUM_LAND_HEIGHT_BLOCKS = 16D;
|
||||||
|
|
||||||
|
private MapCensus() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Share> count(VisionRenderController.Frame frame, ViewArea area, ToIntFunction<MapLabels.Label> colorOf) {
|
||||||
|
Map<Short, long[]> counts = new HashMap<>();
|
||||||
|
MapLabels[] labels = new MapLabels[1];
|
||||||
|
long counted = visitVisiblePixels(frame, area, (tile, pixelX, pixelZ) -> {
|
||||||
|
labels[0] = tile.labels();
|
||||||
|
counts.computeIfAbsent(tile.subjectAt(pixelX, pixelZ), id -> new long[1])[0]++;
|
||||||
|
});
|
||||||
|
return shares(counts, labels[0], counted, colorOf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The heights on screen, ignoring the highest and lowest few percent so one spike doesn't wash out every colour.
|
||||||
|
* Rounded to whole steps, so panning around doesn't recolour the map for every small change.
|
||||||
|
*
|
||||||
|
* @return the range, or null until enough of the map is drawn to tell
|
||||||
|
*/
|
||||||
|
static MapStyle.HeightRange heightRange(VisionRenderController.Frame frame, ViewArea area, double seaLevel) {
|
||||||
|
DoubleList heights = new DoubleList();
|
||||||
|
visitVisiblePixels(frame, area, (tile, pixelX, pixelZ) -> {
|
||||||
|
if (tile.hasHeights()) {
|
||||||
|
heights.add(tile.heightAt(pixelX, pixelZ));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (heights.size() < MINIMUM_HEIGHT_SAMPLES) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
double[] sorted = heights.sorted();
|
||||||
|
double low = sorted[(int) ((sorted.length - 1) * LOW_HEIGHT_PERCENTILE)];
|
||||||
|
double high = sorted[(int) ((sorted.length - 1) * HIGH_HEIGHT_PERCENTILE)];
|
||||||
|
double lowest = Math.floor(Math.min(low, seaLevel - MINIMUM_WATER_DEPTH_BLOCKS) / HEIGHT_RANGE_STEP_BLOCKS)
|
||||||
|
* HEIGHT_RANGE_STEP_BLOCKS;
|
||||||
|
double highest = Math.ceil(Math.max(high, seaLevel + MINIMUM_LAND_HEIGHT_BLOCKS) / HEIGHT_RANGE_STEP_BLOCKS)
|
||||||
|
* HEIGHT_RANGE_STEP_BLOCKS;
|
||||||
|
return new MapStyle.HeightRange(lowest, seaLevel, highest);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long visitVisiblePixels(VisionRenderController.Frame frame, ViewArea area, PixelVisitor visitor) {
|
||||||
|
if (frame == null) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
double blocksPerPixel = frame.renderBlocksPerPixel();
|
||||||
|
int stride = countingStride(area.maximumX() - area.minimumX(), area.maximumZ() - area.minimumZ(), blocksPerPixel);
|
||||||
|
double tileSpan = VisionRenderController.TILE_PIXELS * blocksPerPixel;
|
||||||
|
long visited = 0L;
|
||||||
|
for (VisionRenderController.VisibleTile visible : frame.tiles()) {
|
||||||
|
MapTile tile = visible.tile();
|
||||||
|
if (tile == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double originX = visible.tileX() * tileSpan;
|
||||||
|
double originZ = visible.tileZ() * tileSpan;
|
||||||
|
int firstX = firstPixel(area.minimumX() - originX, blocksPerPixel, stride, visible.tileX());
|
||||||
|
int firstZ = firstPixel(area.minimumZ() - originZ, blocksPerPixel, stride, visible.tileZ());
|
||||||
|
int endX = endPixel(area.maximumX() - originX, blocksPerPixel, tile.size());
|
||||||
|
int endZ = endPixel(area.maximumZ() - originZ, blocksPerPixel, tile.size());
|
||||||
|
for (int pixelZ = firstZ; pixelZ < endZ; pixelZ += stride) {
|
||||||
|
for (int pixelX = firstX; pixelX < endX; pixelX += stride) {
|
||||||
|
visitor.visit(tile, pixelX, pixelZ);
|
||||||
|
visited++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return visited;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Share> shares(Map<Short, long[]> counts, MapLabels labels, long counted,
|
||||||
|
ToIntFunction<MapLabels.Label> colorOf) {
|
||||||
|
if (labels == null || counted == 0L) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<Share> shares = new ArrayList<>(counts.size());
|
||||||
|
for (Map.Entry<Short, long[]> entry : counts.entrySet()) {
|
||||||
|
MapLabels.Label label = labels.label(entry.getKey());
|
||||||
|
if (label != null) {
|
||||||
|
shares.add(new Share(label, colorOf.applyAsInt(label), entry.getValue()[0] / (double) counted));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shares.sort(Comparator.comparingDouble(Share::fraction).reversed().thenComparing(share -> share.label().name()));
|
||||||
|
return List.copyOf(shares);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Counting every pixel of a big screen is wasted work; an evenly spread sample gives the same percentages.
|
||||||
|
private static int countingStride(double widthBlocks, double heightBlocks, double blocksPerPixel) {
|
||||||
|
double pixels = (widthBlocks / blocksPerPixel) * (heightBlocks / blocksPerPixel);
|
||||||
|
return Math.max(1, (int) Math.ceil(Math.sqrt(pixels / MAXIMUM_COUNTED_PIXELS)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starting on the global stride grid keeps neighbouring tiles from sampling the same columns twice as densely.
|
||||||
|
private static int firstPixel(double offsetBlocks, double blocksPerPixel, int stride, long tileIndex) {
|
||||||
|
int first = Math.max(0, (int) Math.ceil(offsetBlocks / blocksPerPixel));
|
||||||
|
long globalPixel = tileIndex * VisionRenderController.TILE_PIXELS + first;
|
||||||
|
int misalignment = (int) Math.floorMod(globalPixel, (long) stride);
|
||||||
|
return misalignment == 0 ? first : first + stride - misalignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int endPixel(double offsetBlocks, double blocksPerPixel, int size) {
|
||||||
|
return Math.min(size, (int) Math.ceil(offsetBlocks / blocksPerPixel));
|
||||||
|
}
|
||||||
|
|
||||||
|
record Share(MapLabels.Label label, int color, double fraction) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The world area the screen shows, in blocks.
|
||||||
|
*/
|
||||||
|
record ViewArea(double minimumX, double minimumZ, double maximumX, double maximumZ) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface PixelVisitor {
|
||||||
|
void visit(MapTile tile, int pixelX, int pixelZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class DoubleList {
|
||||||
|
private double[] values = new double[1024];
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
void add(double value) {
|
||||||
|
if (size == values.length) {
|
||||||
|
values = Arrays.copyOf(values, size * 2);
|
||||||
|
}
|
||||||
|
values[size++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int size() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
double[] sorted() {
|
||||||
|
double[] sorted = Arrays.copyOf(values, size);
|
||||||
|
Arrays.sort(sorted);
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,503 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.MapHoverProbe.PickStep;
|
||||||
|
import com.volmit.iris.core.gui.NoisePickSpread.Band;
|
||||||
|
import com.volmit.iris.core.gui.components.MapLabels;
|
||||||
|
import com.volmit.iris.core.gui.components.RenderType;
|
||||||
|
import com.volmit.iris.core.loader.IrisRegistrant;
|
||||||
|
import com.volmit.iris.engine.object.IrisBiome;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Font;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.Shape;
|
||||||
|
import java.awt.geom.RoundRectangle2D;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The card that follows the cursor, laid out like IntelliJ's documentation popup: the biome as the title, the steps
|
||||||
|
* that picked it as a small tree, and with Shift the terrain, location and pack files below.
|
||||||
|
*/
|
||||||
|
record MapHoverCard(Header header, String subtitle, String pathTitle, List<PickStep> path, boolean showsBands,
|
||||||
|
List<Group> groups, List<Hint> hints) {
|
||||||
|
private static final int MINIMUM_CONTENT_WIDTH = 260;
|
||||||
|
private static final int PADDING = 12;
|
||||||
|
private static final int HEADER_TOP = 10;
|
||||||
|
private static final int HEADER_ICON_SIZE = 14;
|
||||||
|
private static final int NODE_ICON_SIZE = 12;
|
||||||
|
private static final int ICON_GAP = 8;
|
||||||
|
private static final int KIND_GAP = 16;
|
||||||
|
private static final int GROUP_GAP = 8;
|
||||||
|
private static final int GROUP_TITLE_GAP = 4;
|
||||||
|
private static final int NODE_HEIGHT = 24;
|
||||||
|
private static final int NODE_INDENT = 18;
|
||||||
|
private static final int CHIP_GAP = 16;
|
||||||
|
private static final int CHIP_PADDING_X = 5;
|
||||||
|
private static final int CHIP_PADDING_Y = 1;
|
||||||
|
private static final int BAND_HEIGHT = 4;
|
||||||
|
private static final int BAND_GAP = 6;
|
||||||
|
private static final int MARKER_WIDTH = 2;
|
||||||
|
private static final int MARKER_HEIGHT = 10;
|
||||||
|
private static final int COLUMN_GAP = 14;
|
||||||
|
private static final int ROW_GAP = 3;
|
||||||
|
private static final int BOTTOM_PADDING = 10;
|
||||||
|
private static final int FOOTER_PADDING_Y = 7;
|
||||||
|
private static final int KEY_PADDING_X = 4;
|
||||||
|
private static final int KEY_GAP = 4;
|
||||||
|
private static final int HINT_GAP = 14;
|
||||||
|
private static final Color ICON_EDGE = new ThemeColor(new Color(0, 0, 0, 38), new Color(255, 255, 255, 38));
|
||||||
|
private static final Color MARKER_EDGE = new Color(0, 0, 0, 200);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param subject the biome, or region in the region view, the drawn tile shows; null while that tile is drawing
|
||||||
|
* @param region the region the drawn tile shows
|
||||||
|
* @param details the probed facts about the spot, or null until they arrive
|
||||||
|
* @param detailed whether Shift is held, which trades the noise bands for terrain, location and pack files
|
||||||
|
*/
|
||||||
|
static MapHoverCard of(MapLabels.Label subject, MapLabels.Label region, MapHoverProbe.Details details,
|
||||||
|
RenderType type, boolean detailed, double worldX, double worldZ) {
|
||||||
|
List<PickStep> path = details == null ? List.of() : details.pickPath();
|
||||||
|
IrisBiome biome = cardBiome(subject, details, type);
|
||||||
|
List<Group> groups = new ArrayList<>();
|
||||||
|
if (detailed) {
|
||||||
|
addTerrainGroup(groups, details, type);
|
||||||
|
groups.add(locationGroup(worldX, worldZ));
|
||||||
|
addFilesGroup(groups, biome, details != null ? details.region() : region == null ? null : region.subject());
|
||||||
|
}
|
||||||
|
return new MapHoverCard(header(subject, biome, type, path), subtitle(details), pathTitle(path), path, !detailed,
|
||||||
|
groups, hints(type, detailed));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the card with its top left corner as close to the given point as fits inside the bounds.
|
||||||
|
*/
|
||||||
|
void paint(Graphics2D canvas, int anchorX, int anchorY, int boundsWidth, int boundsHeight) {
|
||||||
|
Graphics2D card = (Graphics2D) canvas.create();
|
||||||
|
card.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
card.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
int padding = StudioTheme.scale(PADDING);
|
||||||
|
int contentWidth = contentWidth(card);
|
||||||
|
int width = contentWidth + padding * 2;
|
||||||
|
int footerHeight = footerHeight(card);
|
||||||
|
int height = StudioTheme.scale(HEADER_TOP) + headerHeight(card) + pathHeight(card) + groupsHeight(card)
|
||||||
|
+ StudioTheme.scale(BOTTOM_PADDING) + footerHeight;
|
||||||
|
int x = Math.max(padding, Math.min(boundsWidth - width - padding, anchorX));
|
||||||
|
int y = Math.max(padding, Math.min(boundsHeight - height - padding, anchorY));
|
||||||
|
StudioOverlay.drawCard(card, x, y, width, height, footerHeight);
|
||||||
|
|
||||||
|
int left = x + padding;
|
||||||
|
int top = y + StudioTheme.scale(HEADER_TOP);
|
||||||
|
top = drawHeader(card, left, top, contentWidth);
|
||||||
|
top = drawPath(card, left, top, contentWidth);
|
||||||
|
drawGroups(card, left, top);
|
||||||
|
drawFooter(card, left, y + height - footerHeight);
|
||||||
|
card.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int contentWidth(Graphics2D canvas) {
|
||||||
|
int width = Math.max(headerWidth(canvas), pathWidth(canvas));
|
||||||
|
width = Math.max(width, groupsWidth(canvas));
|
||||||
|
width = Math.max(width, footerWidth(canvas));
|
||||||
|
return Math.max(StudioTheme.scale(MINIMUM_CONTENT_WIDTH), width);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header: the biome's colour, its name in bold with its kind on the right, and the ground's height below.
|
||||||
|
private int headerTextOffset() {
|
||||||
|
return header.color() == null ? 0 : StudioTheme.scale(HEADER_ICON_SIZE + ICON_GAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int headerWidth(Graphics2D canvas) {
|
||||||
|
int width = headerTextOffset() + canvas.getFontMetrics(StudioTheme.boldFont()).stringWidth(header.name());
|
||||||
|
if (header.kind() != null) {
|
||||||
|
width += StudioTheme.scale(KIND_GAP) + canvas.getFontMetrics(StudioTheme.smallFont()).stringWidth(header.kind());
|
||||||
|
}
|
||||||
|
if (subtitle != null) {
|
||||||
|
width = Math.max(width, headerTextOffset() + canvas.getFontMetrics(StudioTheme.smallFont()).stringWidth(subtitle));
|
||||||
|
}
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int headerHeight(Graphics2D canvas) {
|
||||||
|
int height = canvas.getFontMetrics(StudioTheme.boldFont()).getHeight();
|
||||||
|
if (subtitle != null) {
|
||||||
|
height += canvas.getFontMetrics(StudioTheme.smallFont()).getHeight();
|
||||||
|
}
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int drawHeader(Graphics2D canvas, int left, int top, int contentWidth) {
|
||||||
|
FontMetrics bold = canvas.getFontMetrics(StudioTheme.boldFont());
|
||||||
|
if (header.color() != null) {
|
||||||
|
int iconSize = StudioTheme.scale(HEADER_ICON_SIZE);
|
||||||
|
drawIcon(canvas, header.color(), left, top + (bold.getHeight() - iconSize) / 2, iconSize);
|
||||||
|
}
|
||||||
|
int textLeft = left + headerTextOffset();
|
||||||
|
int baseline = top + bold.getAscent();
|
||||||
|
drawText(canvas, header.name(), StudioTheme.boldFont(), StudioTheme.TEXT, textLeft, baseline);
|
||||||
|
if (header.kind() != null) {
|
||||||
|
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
|
||||||
|
drawText(canvas, header.kind(), StudioTheme.smallFont(), StudioTheme.MUTED_TEXT,
|
||||||
|
left + contentWidth - small.stringWidth(header.kind()), baseline);
|
||||||
|
}
|
||||||
|
top += bold.getHeight();
|
||||||
|
if (subtitle != null) {
|
||||||
|
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
|
||||||
|
drawText(canvas, subtitle, StudioTheme.smallFont(), StudioTheme.SECONDARY_TEXT, textLeft, top + small.getAscent());
|
||||||
|
top += small.getHeight();
|
||||||
|
}
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path: one indented row per step, its value as a chip on the right and, without Shift, the noise bands below it.
|
||||||
|
private int pathWidth(Graphics2D canvas) {
|
||||||
|
int width = 0;
|
||||||
|
for (int level = 0; level < path.size(); level++) {
|
||||||
|
PickStep step = path.get(level);
|
||||||
|
int row = nodeTextLeft(level) + canvas.getFontMetrics(StudioTheme.font()).stringWidth(step.picked().name() + " ")
|
||||||
|
+ canvas.getFontMetrics(StudioTheme.smallFont()).stringWidth(step.reason());
|
||||||
|
if (step.value() != null) {
|
||||||
|
row += StudioTheme.scale(CHIP_GAP) + chipWidth(canvas, step.value());
|
||||||
|
}
|
||||||
|
width = Math.max(width, row);
|
||||||
|
}
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int pathHeight(Graphics2D canvas) {
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int height = groupTitleHeight(canvas);
|
||||||
|
for (PickStep step : path) {
|
||||||
|
height += nodeHeight(step);
|
||||||
|
}
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int nodeHeight(PickStep step) {
|
||||||
|
int height = StudioTheme.scale(NODE_HEIGHT);
|
||||||
|
return showsBands && step.showsBands() ? height + StudioTheme.scale(BAND_HEIGHT + BAND_GAP) : height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int nodeTextLeft(int level) {
|
||||||
|
return StudioTheme.scale(level * NODE_INDENT + NODE_ICON_SIZE + ICON_GAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int drawPath(Graphics2D canvas, int left, int top, int contentWidth) {
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
top = drawGroupTitle(canvas, pathTitle, left, top);
|
||||||
|
for (int level = 0; level < path.size(); level++) {
|
||||||
|
PickStep step = path.get(level);
|
||||||
|
drawNode(canvas, step, left, top, level, contentWidth);
|
||||||
|
if (showsBands && step.showsBands()) {
|
||||||
|
int bandsLeft = left + nodeTextLeft(level);
|
||||||
|
drawBands(canvas, step, bandsLeft, top + StudioTheme.scale(NODE_HEIGHT), left + contentWidth - bandsLeft);
|
||||||
|
}
|
||||||
|
top += nodeHeight(step);
|
||||||
|
}
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawNode(Graphics2D canvas, PickStep step, int left, int top, int level, int contentWidth) {
|
||||||
|
int rowHeight = StudioTheme.scale(NODE_HEIGHT);
|
||||||
|
int iconSize = StudioTheme.scale(NODE_ICON_SIZE);
|
||||||
|
drawIcon(canvas, step.picked().color(), left + StudioTheme.scale(level * NODE_INDENT), top + (rowHeight - iconSize) / 2,
|
||||||
|
iconSize);
|
||||||
|
FontMetrics regular = canvas.getFontMetrics(StudioTheme.font());
|
||||||
|
int baseline = top + (rowHeight - regular.getHeight()) / 2 + regular.getAscent();
|
||||||
|
int textLeft = left + nodeTextLeft(level);
|
||||||
|
String name = step.picked().name() + " ";
|
||||||
|
drawText(canvas, name, StudioTheme.font(), StudioTheme.TEXT, textLeft, baseline);
|
||||||
|
drawText(canvas, step.reason(), StudioTheme.smallFont(), StudioTheme.MUTED_TEXT, textLeft + regular.stringWidth(name),
|
||||||
|
baseline);
|
||||||
|
if (step.value() != null) {
|
||||||
|
drawChip(canvas, step.value(), left + contentWidth - chipWidth(canvas, step.value()), top, rowHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A strip with each option's band of noise in its colour, and a marker where this spot's noise falls.
|
||||||
|
private static void drawBands(Graphics2D canvas, PickStep step, int left, int top, int width) {
|
||||||
|
int height = StudioTheme.scale(BAND_HEIGHT);
|
||||||
|
Shape clip = canvas.getClip();
|
||||||
|
canvas.clip(new RoundRectangle2D.Double(left, top, width, height, height, height));
|
||||||
|
for (Band band : step.bands()) {
|
||||||
|
int from = left + (int) Math.round(band.fromNoise() * width);
|
||||||
|
int to = left + (int) Math.round(band.toNoise() * width);
|
||||||
|
canvas.setColor(new Color(band.option().color()));
|
||||||
|
canvas.fillRect(from, top, Math.max(1, to - from), height);
|
||||||
|
}
|
||||||
|
canvas.setClip(clip);
|
||||||
|
int markerWidth = StudioTheme.scale(MARKER_WIDTH);
|
||||||
|
int markerHeight = StudioTheme.scale(MARKER_HEIGHT);
|
||||||
|
int markerX = left + (int) Math.round(Math.clamp(step.noise(), 0D, 1D) * (width - markerWidth));
|
||||||
|
int markerY = top + (height - markerHeight) / 2;
|
||||||
|
canvas.setColor(MARKER_EDGE);
|
||||||
|
canvas.fillRect(markerX - 1, markerY - 1, markerWidth + 2, markerHeight + 2);
|
||||||
|
canvas.setColor(Color.WHITE);
|
||||||
|
canvas.fillRect(markerX, markerY, markerWidth, markerHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Font chipFont() {
|
||||||
|
return StudioTheme.monospacedFont().deriveFont(StudioTheme.smallFont().getSize2D());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int chipWidth(Graphics2D canvas, String value) {
|
||||||
|
return canvas.getFontMetrics(chipFont()).stringWidth(value) + StudioTheme.scale(CHIP_PADDING_X) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styled like an inlay hint in IntelliJ's editor.
|
||||||
|
private static void drawChip(Graphics2D canvas, String value, int left, int rowTop, int rowHeight) {
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics(chipFont());
|
||||||
|
int height = metrics.getHeight() + StudioTheme.scale(CHIP_PADDING_Y) * 2;
|
||||||
|
int top = rowTop + (rowHeight - height) / 2;
|
||||||
|
canvas.setColor(StudioTheme.HOVER_BACKGROUND);
|
||||||
|
int arc = StudioTheme.scale(8);
|
||||||
|
canvas.fillRoundRect(left, top, chipWidth(canvas, value), height, arc, arc);
|
||||||
|
drawText(canvas, value, chipFont(), StudioTheme.SECONDARY_TEXT, left + StudioTheme.scale(CHIP_PADDING_X),
|
||||||
|
top + StudioTheme.scale(CHIP_PADDING_Y) + metrics.getAscent());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawIcon(Graphics2D canvas, int rgb, int left, int top, int size) {
|
||||||
|
int arc = Math.max(2, size / 2);
|
||||||
|
canvas.setColor(new Color(rgb));
|
||||||
|
canvas.fillRoundRect(left, top, size, size, arc, arc);
|
||||||
|
canvas.setColor(ICON_EDGE);
|
||||||
|
canvas.drawRoundRect(left, top, size - 1, size - 1, arc, arc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Groups: sentence-case titles over rows of muted labels and values, all labels in one column.
|
||||||
|
private int labelColumnWidth(Graphics2D canvas) {
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics(StudioTheme.font());
|
||||||
|
return groups.stream().flatMap(group -> group.rows().stream())
|
||||||
|
.mapToInt(row -> metrics.stringWidth(row.label())).max().orElse(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int groupsWidth(Graphics2D canvas) {
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics(StudioTheme.font());
|
||||||
|
int valueWidth = groups.stream().flatMap(group -> group.rows().stream())
|
||||||
|
.mapToInt(row -> metrics.stringWidth(row.value())).max().orElse(0);
|
||||||
|
return labelColumnWidth(canvas) + StudioTheme.scale(COLUMN_GAP) + valueWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int groupsHeight(Graphics2D canvas) {
|
||||||
|
int rowHeight = canvas.getFontMetrics(StudioTheme.font()).getHeight() + StudioTheme.scale(ROW_GAP);
|
||||||
|
return groups.stream().mapToInt(group -> groupTitleHeight(canvas) + group.rows().size() * rowHeight).sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawGroups(Graphics2D canvas, int left, int top) {
|
||||||
|
FontMetrics regular = canvas.getFontMetrics(StudioTheme.font());
|
||||||
|
int valueLeft = left + labelColumnWidth(canvas) + StudioTheme.scale(COLUMN_GAP);
|
||||||
|
for (Group group : groups) {
|
||||||
|
top = drawGroupTitle(canvas, group.title(), left, top);
|
||||||
|
for (Row row : group.rows()) {
|
||||||
|
int baseline = top + regular.getAscent();
|
||||||
|
drawText(canvas, row.label(), StudioTheme.font(), StudioTheme.SECONDARY_TEXT, left, baseline);
|
||||||
|
drawText(canvas, row.value(), StudioTheme.font(), row.link() ? StudioTheme.LINK : StudioTheme.TEXT, valueLeft,
|
||||||
|
baseline);
|
||||||
|
top += regular.getHeight() + StudioTheme.scale(ROW_GAP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int groupTitleHeight(Graphics2D canvas) {
|
||||||
|
return StudioTheme.scale(GROUP_GAP) + canvas.getFontMetrics(StudioTheme.smallFont()).getHeight()
|
||||||
|
+ StudioTheme.scale(GROUP_TITLE_GAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int drawGroupTitle(Graphics2D canvas, String title, int left, int top) {
|
||||||
|
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
|
||||||
|
top += StudioTheme.scale(GROUP_GAP);
|
||||||
|
drawText(canvas, title, StudioTheme.smallFont(), StudioTheme.MUTED_TEXT, left, top + small.getAscent());
|
||||||
|
return top + small.getHeight() + StudioTheme.scale(GROUP_TITLE_GAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer: the shortcuts as key caps, each followed by what it does.
|
||||||
|
private int footerWidth(Graphics2D canvas) {
|
||||||
|
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
|
||||||
|
int width = 0;
|
||||||
|
for (Hint hint : hints) {
|
||||||
|
width += keyWidth(small, hint.key()) + StudioTheme.scale(KEY_GAP) + small.stringWidth(hint.action());
|
||||||
|
}
|
||||||
|
return width + StudioTheme.scale(HINT_GAP) * Math.max(0, hints.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int footerHeight(Graphics2D canvas) {
|
||||||
|
return canvas.getFontMetrics(StudioTheme.smallFont()).getHeight() + StudioTheme.scale(FOOTER_PADDING_Y) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int keyWidth(FontMetrics small, String key) {
|
||||||
|
return small.stringWidth(key) + StudioTheme.scale(KEY_PADDING_X) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawFooter(Graphics2D canvas, int left, int top) {
|
||||||
|
FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont());
|
||||||
|
int textTop = top + StudioTheme.scale(FOOTER_PADDING_Y);
|
||||||
|
int baseline = textTop + small.getAscent();
|
||||||
|
int arc = StudioTheme.scale(6);
|
||||||
|
for (Hint hint : hints) {
|
||||||
|
int keyWidth = keyWidth(small, hint.key());
|
||||||
|
canvas.setColor(StudioTheme.HOVER_BACKGROUND);
|
||||||
|
canvas.fillRoundRect(left, textTop, keyWidth, small.getHeight(), arc, arc);
|
||||||
|
drawText(canvas, hint.key(), StudioTheme.smallFont(), StudioTheme.SECONDARY_TEXT,
|
||||||
|
left + StudioTheme.scale(KEY_PADDING_X), baseline);
|
||||||
|
left += keyWidth + StudioTheme.scale(KEY_GAP);
|
||||||
|
drawText(canvas, hint.action(), StudioTheme.smallFont(), StudioTheme.MUTED_TEXT, left, baseline);
|
||||||
|
left += small.stringWidth(hint.action()) + StudioTheme.scale(HINT_GAP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawText(Graphics2D canvas, String text, Font font, Color color, int left, int baseline) {
|
||||||
|
canvas.setFont(font);
|
||||||
|
canvas.setColor(color);
|
||||||
|
canvas.drawString(text, left, baseline);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The card is about a biome; the region view's tiles only know the region until the probe finds the biome.
|
||||||
|
private static IrisBiome cardBiome(MapLabels.Label subject, MapHoverProbe.Details details, RenderType type) {
|
||||||
|
if (subject != null && subject.subject() instanceof IrisBiome biome) {
|
||||||
|
return biome;
|
||||||
|
}
|
||||||
|
if (details == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return type == RenderType.CAVE_LAND ? details.caveBiome() : details.surfaceBiome();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Header header(MapLabels.Label subject, IrisBiome biome, RenderType type, List<PickStep> path) {
|
||||||
|
if (biome != null) {
|
||||||
|
return new Header(biome.getName(), colorOf(biome, path), type == RenderType.CAVE_LAND ? "Cave biome" : "Biome");
|
||||||
|
}
|
||||||
|
if (subject != null) {
|
||||||
|
return new Header(subject.name(), null, subject.kind() == MapLabels.Kind.REGION ? "Region" : "Biome");
|
||||||
|
}
|
||||||
|
return new Header("Drawing…", null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer colorOf(IrisBiome biome, List<PickStep> path) {
|
||||||
|
return path.stream()
|
||||||
|
.filter(step -> step.picked().key().equals(biome.getLoadKey()))
|
||||||
|
.reduce((first, last) -> last)
|
||||||
|
.map(step -> step.picked().color())
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String subtitle(MapHoverProbe.Details details) {
|
||||||
|
if (details == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Math.round(details.height()) + " · " + heightAboveSea(details.height() - details.fluidHeight()) + " · slope "
|
||||||
|
+ Math.round(details.slopeDegrees()) + "°";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String pathTitle(List<PickStep> path) {
|
||||||
|
return path.size() > 1 ? "Picked by" : "Region";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addTerrainGroup(List<Group> groups, MapHoverProbe.Details details, RenderType type) {
|
||||||
|
if (details == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Row> rows = new ArrayList<>();
|
||||||
|
IrisBiome surface = details.surfaceBiome();
|
||||||
|
IrisBiome cave = details.caveBiome();
|
||||||
|
if (type == RenderType.CAVE_LAND && surface != null) {
|
||||||
|
rows.add(new Row("Surface biome", surface.getName(), false));
|
||||||
|
} else if (type != RenderType.CAVE_LAND && cave != null && cave != surface) {
|
||||||
|
rows.add(new Row("Cave biome", cave.getName(), false));
|
||||||
|
}
|
||||||
|
if (details.vanillaBiome() != null) {
|
||||||
|
rows.add(new Row("Minecraft", details.vanillaBiome().replace('_', ' '), false));
|
||||||
|
}
|
||||||
|
if (!rows.isEmpty()) {
|
||||||
|
groups.add(new Group("Terrain", rows));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Group locationGroup(double worldX, double worldZ) {
|
||||||
|
int blockX = MapCanvas.floorWorldCoordinate(worldX);
|
||||||
|
int blockZ = MapCanvas.floorWorldCoordinate(worldZ);
|
||||||
|
return new Group("Location", List.of(
|
||||||
|
new Row("Block", blockX + ", " + blockZ, false),
|
||||||
|
new Row("Chunk", (blockX >> 4) + ", " + (blockZ >> 4), false),
|
||||||
|
new Row("Region file", "r." + (blockX >> 9) + "." + (blockZ >> 9) + ".mca", false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addFilesGroup(List<Group> groups, IrisBiome biome, Object region) {
|
||||||
|
List<Row> rows = new ArrayList<>();
|
||||||
|
if (biome != null) {
|
||||||
|
rows.add(new Row("Biome", packPath(biome), true));
|
||||||
|
}
|
||||||
|
if (region instanceof IrisRegistrant registrant) {
|
||||||
|
rows.add(new Row("Region", packPath(registrant), true));
|
||||||
|
}
|
||||||
|
if (!rows.isEmpty()) {
|
||||||
|
groups.add(new Group("Files", rows));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The key is the path under the type's folder, so folder, key and extension give the file within the pack.
|
||||||
|
private static String packPath(IrisRegistrant registrant) {
|
||||||
|
File file = registrant.getLoadFile();
|
||||||
|
String fileName = file == null ? "" : file.getName();
|
||||||
|
int extensionStart = fileName.lastIndexOf('.');
|
||||||
|
String extension = extensionStart < 0 ? "" : fileName.substring(extensionStart);
|
||||||
|
return registrant.getFolderName() + "/" + registrant.getLoadKey() + extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Hint> hints(RenderType type, boolean detailed) {
|
||||||
|
List<Hint> hints = new ArrayList<>();
|
||||||
|
if (!detailed) {
|
||||||
|
hints.add(new Hint("Shift", "More"));
|
||||||
|
}
|
||||||
|
hints.add(new Hint("H", "Highlight"));
|
||||||
|
if (type != RenderType.CONTINENT) {
|
||||||
|
hints.add(new Hint("Alt+Click", "Open file"));
|
||||||
|
}
|
||||||
|
return hints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String heightAboveSea(double height) {
|
||||||
|
long rounded = Math.round(height);
|
||||||
|
return rounded < 0 ? -rounded + " below sea" : rounded + " above sea";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param color the biome's map colour, or null when no step of the path tells it
|
||||||
|
* @param kind what the title names, like "Biome", or null while drawing
|
||||||
|
*/
|
||||||
|
record Header(String name, Integer color, String kind) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Group(String title, List<Row> rows) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Row(String label, String value, boolean link) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Hint(String key, String action) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.NoisePickSpread.Band;
|
||||||
|
import com.volmit.iris.core.gui.NoisePickSpread.Option;
|
||||||
|
import com.volmit.iris.engine.HybridBiomeSelector;
|
||||||
|
import com.volmit.iris.engine.IrisComplex;
|
||||||
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
|
import com.volmit.iris.engine.object.InferredType;
|
||||||
|
import com.volmit.iris.engine.object.IrisBiome;
|
||||||
|
import com.volmit.iris.engine.object.IrisDimension;
|
||||||
|
import com.volmit.iris.engine.object.IrisRegion;
|
||||||
|
import org.bukkit.block.Biome;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up everything the hover card can tell about one spot of the world; runs off the Swing thread.
|
||||||
|
*/
|
||||||
|
final class MapHoverProbe {
|
||||||
|
// IrisComplex measures slope as the height change over this many blocks.
|
||||||
|
private static final double SLOPE_STREAM_RANGE_BLOCKS = 3D;
|
||||||
|
|
||||||
|
private MapHoverProbe() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param previewRegionKey the region the region preview treats every spot as part of, or null for the real world
|
||||||
|
* @param explainBiomePick whether the card shows the surface biome, so the whole way to it is worth looking up;
|
||||||
|
* otherwise only how the region was picked
|
||||||
|
*/
|
||||||
|
static Details sample(Engine engine, double worldX, double worldZ, String previewRegionKey, boolean explainBiomePick) {
|
||||||
|
IrisComplex complex = engine.getComplex();
|
||||||
|
IrisRegion previewRegion = previewRegionKey == null ? null : engine.getData().getRegionLoader().load(previewRegionKey);
|
||||||
|
IrisRegion region = previewRegion != null ? previewRegion : complex.getRegionStream().get(worldX, worldZ);
|
||||||
|
IrisComplex.BiomePick surfacePick = complex.surfacePickAsIfIn(region, worldX, worldZ);
|
||||||
|
Biome vanillaBiome = complex.getTrueBiomeDerivativeStream().get(worldX, worldZ);
|
||||||
|
double slope = complex.getSlopeStream().getDouble(worldX, worldZ);
|
||||||
|
IrisDimension dimension = engine.getDimension();
|
||||||
|
return new Details(
|
||||||
|
worldX,
|
||||||
|
worldZ,
|
||||||
|
surfacePick.biome(),
|
||||||
|
complex.getCaveBiomeStream().get(worldX, worldZ),
|
||||||
|
region,
|
||||||
|
vanillaBiome == null ? null : vanillaBiome.getKey().getKey(),
|
||||||
|
complex.getHeightStream().getDouble(worldX, worldZ),
|
||||||
|
dimension == null ? 0D : dimension.getFluidHeight(),
|
||||||
|
Math.toDegrees(Math.atan(slope / SLOPE_STREAM_RANGE_BLOCKS)),
|
||||||
|
pickPath(engine, region, previewRegion != null, explainBiomePick ? surfacePick : null, worldX, worldZ)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The region preview chose the region itself; the real map picks it by the region noise.
|
||||||
|
private static List<PickStep> pickPath(Engine engine, IrisRegion region, boolean previewed,
|
||||||
|
IrisComplex.BiomePick surfacePick, double x, double z) {
|
||||||
|
List<PickStep> path = new ArrayList<>();
|
||||||
|
path.add(previewed ? previewedRegionStep(engine, region) : regionStep(engine, region, x, z));
|
||||||
|
if (surfacePick == null || surfacePick.type() == null || engine.getComplex().getFocusBiome() != null) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
PickStep biomeStep = engine.getComplex().isHybridTerrain()
|
||||||
|
? heightStep(engine, region, surfacePick.type(), x, z)
|
||||||
|
: noiseStep(engine, region, surfacePick.type(), x, z);
|
||||||
|
if (biomeStep == null) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
path.add(biomeStep);
|
||||||
|
if (!biomeStep.picked().key().equals(surfacePick.biome().getLoadKey())) {
|
||||||
|
path.add(new PickStep(PickMeasuring.biomeOption(engine, surfacePick.biome()), "child", null, List.of(), Double.NaN));
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PickStep regionStep(Engine engine, IrisRegion region, double x, double z) {
|
||||||
|
IrisComplex complex = engine.getComplex();
|
||||||
|
double noise = complex.getRegionStyleStream().get(x, z);
|
||||||
|
List<Option> regions = complex.noisePickedRegions().stream()
|
||||||
|
.map(candidate -> PickMeasuring.regionOption(engine, candidate))
|
||||||
|
.toList();
|
||||||
|
return new PickStep(PickMeasuring.regionOption(engine, region), "region", noiseText(noise),
|
||||||
|
NoisePickSpread.splitNoise(regions, complex.isLegacyRarity()), noise);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PickStep previewedRegionStep(Engine engine, IrisRegion region) {
|
||||||
|
return new PickStep(PickMeasuring.regionOption(engine, region), "previewed", null, List.of(), Double.NaN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PickStep noiseStep(Engine engine, IrisRegion region, InferredType type, double x, double z) {
|
||||||
|
IrisComplex complex = engine.getComplex();
|
||||||
|
double noise = complex.biomePickNoise(region, type, x, z);
|
||||||
|
List<Option> biomes = complex.noisePickedBiomes(region, type).stream()
|
||||||
|
.map(biome -> PickMeasuring.biomeOption(engine, biome))
|
||||||
|
.toList();
|
||||||
|
IrisBiome picked = complex.biomePickedByNoise(region, type, x, z);
|
||||||
|
return new PickStep(PickMeasuring.biomeOption(engine, picked), typeName(type), noiseText(noise),
|
||||||
|
NoisePickSpread.splitNoise(biomes, complex.isLegacyRarity()), noise);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HYBRID terrain picks by the height a spot is placed at, and only competes where biome ranges share that height.
|
||||||
|
private static PickStep heightStep(Engine engine, IrisRegion region, InferredType type, double x, double z) {
|
||||||
|
HybridBiomeSelector.Choice choice = engine.getComplex().explainHybridChoice(region, type, x, z);
|
||||||
|
if (choice == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new PickStep(PickMeasuring.biomeOption(engine, choice.biome()), typeName(type) + ", " + competitionText(choice),
|
||||||
|
signedHeightText(choice.placementHeight()), List.of(), Double.NaN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String competitionText(HybridBiomeSelector.Choice choice) {
|
||||||
|
if (choice.fallback()) {
|
||||||
|
return "nearest range";
|
||||||
|
}
|
||||||
|
if (choice.rivalCount() == 0) {
|
||||||
|
return "only fit";
|
||||||
|
}
|
||||||
|
return "beat " + choice.rivalCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String typeName(InferredType type) {
|
||||||
|
return type.name().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String noiseText(double noise) {
|
||||||
|
return String.format(Locale.ROOT, "%.3f", noise);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String signedHeightText(double heightAboveSea) {
|
||||||
|
long rounded = Math.round(heightAboveSea);
|
||||||
|
return rounded < 0 ? "−" + -rounded : "+" + rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
record Details(
|
||||||
|
double worldX,
|
||||||
|
double worldZ,
|
||||||
|
IrisBiome surfaceBiome,
|
||||||
|
IrisBiome caveBiome,
|
||||||
|
IrisRegion region,
|
||||||
|
String vanillaBiome,
|
||||||
|
double height,
|
||||||
|
double fluidHeight,
|
||||||
|
double slopeDegrees,
|
||||||
|
List<PickStep> pickPath
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One step on the way to the spot's biome, starting with its region.
|
||||||
|
*
|
||||||
|
* @param reason what picked it: its kind, the biome list the noise picked from, or "child"
|
||||||
|
* @param value the noise value or placement height that picked it, or null
|
||||||
|
* @param bands the noise bands of all the options the noise picked from, empty when no noise picked it
|
||||||
|
*/
|
||||||
|
record PickStep(Option picked, String reason, String value, List<Band> bands, double noise) {
|
||||||
|
boolean showsBands() {
|
||||||
|
return !bands.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.components.MapStyle;
|
||||||
|
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.DefaultListCellRenderer;
|
||||||
|
import javax.swing.DefaultListModel;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JList;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.ListSelectionModel;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.Point;
|
||||||
|
import java.awt.Rectangle;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The biomes or regions on screen with how much of it each covers; clicking one highlights it on the map.
|
||||||
|
*/
|
||||||
|
final class MapLegendPanel extends JPanel {
|
||||||
|
private static final long serialVersionUID = 4471297416386034128L;
|
||||||
|
private static final int PANEL_WIDTH = 250;
|
||||||
|
private static final int ROW_HEIGHT = 26;
|
||||||
|
private static final int SWATCH_SIZE = 12;
|
||||||
|
private static final int SWATCH_GAP = 8;
|
||||||
|
private static final int ROW_PADDING = 10;
|
||||||
|
private static final int PERCENT_WIDTH = 42;
|
||||||
|
private static final String DRAWING_MESSAGE = "Drawing the map…";
|
||||||
|
|
||||||
|
private final JLabel title = new JLabel();
|
||||||
|
private final DefaultListModel<MapCensus.Share> rows = new DefaultListModel<>();
|
||||||
|
private final JList<MapCensus.Share> list = new JList<>(rows) {
|
||||||
|
// Long names shorten with an ellipsis instead of pushing the percentages out of sight.
|
||||||
|
@Override
|
||||||
|
public boolean getScrollableTracksViewportWidth() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private final Set<String> sharedNames = new HashSet<>();
|
||||||
|
private MapStyle.Highlight highlight;
|
||||||
|
|
||||||
|
MapLegendPanel(Consumer<MapCensus.Share> onRowClicked) {
|
||||||
|
super(new BorderLayout());
|
||||||
|
setPreferredSize(StudioTheme.scaledSize(PANEL_WIDTH, 0));
|
||||||
|
setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, StudioTheme.SEPARATOR));
|
||||||
|
setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||||
|
|
||||||
|
title.setFont(StudioTheme.sectionFont());
|
||||||
|
title.setForeground(StudioTheme.MUTED_TEXT);
|
||||||
|
title.setBorder(StudioTheme.emptyBorder(10, 16, 6, 16));
|
||||||
|
add(title, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||||
|
list.setFocusable(false);
|
||||||
|
list.setFixedCellHeight(StudioTheme.scale(ROW_HEIGHT));
|
||||||
|
list.setCellRenderer(new ShareRenderer());
|
||||||
|
list.setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||||
|
list.setBorder(StudioTheme.emptyBorder(0, 6, 4, 6));
|
||||||
|
list.addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent event) {
|
||||||
|
MapCensus.Share share = shareAt(event.getPoint());
|
||||||
|
if (share != null) {
|
||||||
|
onRowClicked.accept(share);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add(StudioToolbar.scroll(list), BorderLayout.CENTER);
|
||||||
|
|
||||||
|
JLabel hint = new JLabel("Click to highlight · Esc clears");
|
||||||
|
hint.setFont(StudioTheme.smallFont());
|
||||||
|
hint.setForeground(StudioTheme.MUTED_TEXT);
|
||||||
|
hint.setBorder(StudioTheme.emptyBorder(6, 16, 8, 16));
|
||||||
|
add(hint, BorderLayout.SOUTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
void show(String subjectTitle, List<MapCensus.Share> shares, MapStyle.Highlight nextHighlight) {
|
||||||
|
title.setText(subjectTitle.toUpperCase(Locale.ROOT));
|
||||||
|
highlight = nextHighlight;
|
||||||
|
rememberSharedNames(shares);
|
||||||
|
if (!sameRows(shares)) {
|
||||||
|
rows.clear();
|
||||||
|
rows.addAll(shares);
|
||||||
|
}
|
||||||
|
list.repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two biomes can share a display name; those rows show their key so they can be told apart.
|
||||||
|
private void rememberSharedNames(List<MapCensus.Share> shares) {
|
||||||
|
Set<String> seen = new HashSet<>();
|
||||||
|
sharedNames.clear();
|
||||||
|
for (MapCensus.Share share : shares) {
|
||||||
|
if (!seen.add(share.label().name())) {
|
||||||
|
sharedNames.add(share.label().name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String rowText(MapCensus.Share share) {
|
||||||
|
String name = share.label().name();
|
||||||
|
return sharedNames.contains(name) ? name + " (" + share.label().key() + ")" : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sameRows(List<MapCensus.Share> shares) {
|
||||||
|
if (rows.size() != shares.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < shares.size(); index++) {
|
||||||
|
if (!rows.get(index).equals(shares.get(index))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MapCensus.Share shareAt(Point point) {
|
||||||
|
int index = list.locationToIndex(point);
|
||||||
|
if (index < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Rectangle bounds = list.getCellBounds(index, index);
|
||||||
|
return bounds != null && bounds.contains(point) ? rows.get(index) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isHighlighted(MapCensus.Share share) {
|
||||||
|
return highlight != null
|
||||||
|
&& highlight.kind() == share.label().kind()
|
||||||
|
&& highlight.key().equals(share.label().key());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintChildren(Graphics graphics) {
|
||||||
|
super.paintChildren(graphics);
|
||||||
|
if (!rows.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
canvas.setFont(StudioTheme.font());
|
||||||
|
canvas.setColor(StudioTheme.MUTED_TEXT);
|
||||||
|
canvas.drawString(DRAWING_MESSAGE, StudioTheme.scale(16),
|
||||||
|
title.getHeight() + StudioTheme.scale(4) + canvas.getFontMetrics().getAscent());
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ShareRenderer extends DefaultListCellRenderer {
|
||||||
|
private static final long serialVersionUID = -2350212934617052618L;
|
||||||
|
private Color swatch = Color.BLACK;
|
||||||
|
private String percent = "";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Component getListCellRendererComponent(JList<?> owner, Object value, int index, boolean selected, boolean focused) {
|
||||||
|
MapCensus.Share share = (MapCensus.Share) value;
|
||||||
|
super.getListCellRendererComponent(owner, rowText(share), index, isHighlighted(share), false);
|
||||||
|
setFont(StudioTheme.font());
|
||||||
|
setBorder(StudioTheme.emptyBorder(0, ROW_PADDING + SWATCH_SIZE + SWATCH_GAP, 0, ROW_PADDING + PERCENT_WIDTH));
|
||||||
|
setToolTipText(share.label().key());
|
||||||
|
swatch = new Color(share.color());
|
||||||
|
percent = formatPercent(share.fraction());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics graphics) {
|
||||||
|
super.paintComponent(graphics);
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
int size = StudioTheme.scale(SWATCH_SIZE);
|
||||||
|
int swatchX = StudioTheme.scale(ROW_PADDING);
|
||||||
|
int swatchY = (getHeight() - size) / 2;
|
||||||
|
int arc = StudioTheme.scale(4);
|
||||||
|
canvas.setColor(swatch);
|
||||||
|
canvas.fillRoundRect(swatchX, swatchY, size, size, arc, arc);
|
||||||
|
canvas.setColor(StudioTheme.CONTROL_BORDER);
|
||||||
|
canvas.drawRoundRect(swatchX, swatchY, size - 1, size - 1, arc, arc);
|
||||||
|
canvas.setFont(StudioTheme.smallFont());
|
||||||
|
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
||||||
|
int textWidth = canvas.getFontMetrics().stringWidth(percent);
|
||||||
|
int baseline = (getHeight() + canvas.getFontMetrics().getAscent() - canvas.getFontMetrics().getDescent()) / 2;
|
||||||
|
canvas.drawString(percent, getWidth() - StudioTheme.scale(ROW_PADDING) - textWidth, baseline);
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatPercent(double fraction) {
|
||||||
|
double percent = fraction * 100D;
|
||||||
|
if (percent < 0.1D) {
|
||||||
|
return "<0.1%";
|
||||||
|
}
|
||||||
|
return percent < 10D
|
||||||
|
? String.format(Locale.ROOT, "%.1f%%", percent)
|
||||||
|
: String.format(Locale.ROOT, "%.0f%%", percent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.components.RenderType;
|
||||||
|
|
||||||
|
import java.awt.event.KeyEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Everything the map window can show, in the order and groups of its view list.
|
||||||
|
*/
|
||||||
|
enum MapView {
|
||||||
|
BIOMES("Biomes", "The biome at each spot, in the biome's colour", Group.WORLD, RenderType.BIOME, KeyEvent.VK_1),
|
||||||
|
REGIONS("Regions", "The region each spot belongs to; pick one to see which biome covers which height",
|
||||||
|
Group.WORLD, RenderType.REGION, KeyEvent.VK_2),
|
||||||
|
HEIGHT("Height", "Terrain height, from deep water to snowy peaks", Group.WORLD, RenderType.HEIGHT, KeyEvent.VK_3),
|
||||||
|
CONTINENTS("Continents", "Land, coast or sea: where hybrid terrain ends up, or each biome's first height generator otherwise",
|
||||||
|
Group.WORLD, RenderType.CONTINENT, KeyEvent.VK_4),
|
||||||
|
LAND_BIOMES("Land biomes", "The land biome each spot would get, also where the sea is",
|
||||||
|
Group.BIOME_STREAMS, RenderType.BIOME_LAND, KeyEvent.VK_5),
|
||||||
|
SEA_BIOMES("Sea biomes", "The sea biome each spot would get, also where the land is",
|
||||||
|
Group.BIOME_STREAMS, RenderType.BIOME_SEA, KeyEvent.VK_6),
|
||||||
|
CAVE_BIOMES("Cave biomes", "The cave biome under each spot", Group.BIOME_STREAMS, RenderType.CAVE_LAND, KeyEvent.VK_7),
|
||||||
|
PALETTES("Palettes", "The blocks each biome's palette layers place, with their textures",
|
||||||
|
Group.BLOCKS, null, KeyEvent.VK_P),
|
||||||
|
OBJECTS("Objects", "Every object in the pack in 3D, with the blocks it is made of",
|
||||||
|
Group.BLOCKS, null, KeyEvent.VK_O),
|
||||||
|
PROCEDURAL("Procedural", "Trees, rock formations and more that Iris generates from settings, every variant in 3D",
|
||||||
|
Group.BLOCKS, null, KeyEvent.VK_T),
|
||||||
|
NOISE("Noise", "The pack's noise generators and Iris' built-in noise styles",
|
||||||
|
Group.GENERATORS, null, KeyEvent.VK_N);
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
private final String description;
|
||||||
|
private final Group group;
|
||||||
|
private final RenderType renderType;
|
||||||
|
private final int shortcutKey;
|
||||||
|
|
||||||
|
MapView(String label, String description, Group group, RenderType renderType, int shortcutKey) {
|
||||||
|
this.label = label;
|
||||||
|
this.description = description;
|
||||||
|
this.group = group;
|
||||||
|
this.renderType = renderType;
|
||||||
|
this.shortcutKey = shortcutKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
String label() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
String description() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
Group group() {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tile renderer's mode for this view, or null for the palette, object, procedural and noise views, which draw themselves.
|
||||||
|
*/
|
||||||
|
RenderType renderType() {
|
||||||
|
return renderType;
|
||||||
|
}
|
||||||
|
|
||||||
|
int shortcutKey() {
|
||||||
|
return shortcutKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
String shortcutText() {
|
||||||
|
return KeyEvent.getKeyText(shortcutKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isTileMap() {
|
||||||
|
return renderType != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static MapView forShortcut(int keyCode) {
|
||||||
|
for (MapView view : values()) {
|
||||||
|
if (view.shortcutKey == keyCode) {
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Group {
|
||||||
|
WORLD("World"),
|
||||||
|
BIOME_STREAMS("Biome layers"),
|
||||||
|
BLOCKS("Blocks"),
|
||||||
|
GENERATORS("Generators");
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
Group(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
String label() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.extras.FlatSVGIcon;
|
||||||
|
|
||||||
|
import javax.swing.Icon;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The icon of each map view, taken from IntelliJ's new UI icon set (see the NOTICE next to the files).
|
||||||
|
*/
|
||||||
|
final class MapViewIcon {
|
||||||
|
private static final String ICON_FOLDER = "iris/icons/views/";
|
||||||
|
private static final int SIZE = 16;
|
||||||
|
private static final Map<MapView, Icon> LOADED = new EnumMap<>(MapView.class);
|
||||||
|
|
||||||
|
private MapViewIcon() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static Icon of(MapView view) {
|
||||||
|
return LOADED.computeIfAbsent(view, shown -> load(shown, SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view's icon drawn larger, for an empty view.
|
||||||
|
*/
|
||||||
|
static Icon large(MapView view, int size) {
|
||||||
|
return load(view, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Icon load(MapView view, int size) {
|
||||||
|
String fileName = view.name().toLowerCase(Locale.ROOT).replace('_', '-') + ".svg";
|
||||||
|
return new FlatSVGIcon(ICON_FOLDER + fileName, size, size, MapViewIcon.class.getClassLoader());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import javax.swing.DefaultListCellRenderer;
|
||||||
|
import javax.swing.DefaultListModel;
|
||||||
|
import javax.swing.JList;
|
||||||
|
import javax.swing.ListSelectionModel;
|
||||||
|
import javax.swing.UIManager;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.Insets;
|
||||||
|
import java.awt.Rectangle;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The map window's list of views, grouped under headers, each row with a picture of the view and the key that opens
|
||||||
|
* it.
|
||||||
|
*/
|
||||||
|
final class MapViewList extends JList<MapViewList.Row> {
|
||||||
|
private static final long serialVersionUID = -4130872409914437415L;
|
||||||
|
private static final int ROW_HEIGHT = 28;
|
||||||
|
private static final int NO_ROW = -1;
|
||||||
|
|
||||||
|
private final DefaultListModel<Row> rows = new DefaultListModel<>();
|
||||||
|
private int lastViewRow;
|
||||||
|
private int hoveredRow = NO_ROW;
|
||||||
|
|
||||||
|
MapViewList(Consumer<MapView> onViewChosen) {
|
||||||
|
fillRows();
|
||||||
|
setModel(rows);
|
||||||
|
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||||
|
setFixedCellHeight(StudioTheme.scale(ROW_HEIGHT));
|
||||||
|
setCellRenderer(new RowRenderer());
|
||||||
|
setBorder(StudioTheme.emptyBorder(4, 6, 4, 6));
|
||||||
|
addListSelectionListener(event -> {
|
||||||
|
if (event.getValueIsAdjusting() || skipHeaderRow()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Row selected = getSelectedValue();
|
||||||
|
if (selected == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastViewRow = getSelectedIndex();
|
||||||
|
onViewChosen.accept(selected.view());
|
||||||
|
});
|
||||||
|
MouseAdapter hover = new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseMoved(MouseEvent event) {
|
||||||
|
hover(viewRowAt(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseExited(MouseEvent event) {
|
||||||
|
hover(NO_ROW);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addMouseListener(hover);
|
||||||
|
addMouseMotionListener(hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int viewRowAt(MouseEvent event) {
|
||||||
|
int row = locationToIndex(event.getPoint());
|
||||||
|
if (row < 0 || rows.get(row).isHeader() || !getCellBounds(row, row).contains(event.getPoint())) {
|
||||||
|
return NO_ROW;
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hover(int row) {
|
||||||
|
if (row == hoveredRow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
repaintRow(hoveredRow);
|
||||||
|
hoveredRow = row;
|
||||||
|
repaintRow(hoveredRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void repaintRow(int row) {
|
||||||
|
Rectangle bounds = row == NO_ROW ? null : getCellBounds(row, row);
|
||||||
|
if (bounds != null) {
|
||||||
|
repaint(bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void select(MapView view) {
|
||||||
|
for (int row = 0; row < rows.size(); row++) {
|
||||||
|
if (rows.get(row).view() == view) {
|
||||||
|
lastViewRow = row;
|
||||||
|
setSelectedIndex(row);
|
||||||
|
ensureIndexIsVisible(row);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillRows() {
|
||||||
|
MapView.Group currentGroup = null;
|
||||||
|
for (MapView view : MapView.values()) {
|
||||||
|
if (view.group() != currentGroup) {
|
||||||
|
currentGroup = view.group();
|
||||||
|
rows.addElement(Row.header(currentGroup));
|
||||||
|
}
|
||||||
|
rows.addElement(Row.of(view));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers can't be chosen: clicking one keeps the current view, and the arrow keys carry on past it.
|
||||||
|
private boolean skipHeaderRow() {
|
||||||
|
int row = getSelectedIndex();
|
||||||
|
if (row < 0 || !rows.get(row).isHeader()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int step = row >= lastViewRow ? 1 : -1;
|
||||||
|
int next = nextViewRow(row, step);
|
||||||
|
if (next < 0) {
|
||||||
|
next = nextViewRow(row, -step);
|
||||||
|
}
|
||||||
|
setSelectedIndex(next < 0 ? lastViewRow : next);
|
||||||
|
ensureIndexIsVisible(getSelectedIndex());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int nextViewRow(int from, int step) {
|
||||||
|
for (int row = from + step; row >= 0 && row < rows.size(); row += step) {
|
||||||
|
if (!rows.get(row).isHeader()) {
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
record Row(MapView.Group group, MapView view) {
|
||||||
|
static Row header(MapView.Group group) {
|
||||||
|
return new Row(group, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Row of(MapView view) {
|
||||||
|
return new Row(view.group(), view);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isHeader() {
|
||||||
|
return view == null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RowRenderer extends DefaultListCellRenderer {
|
||||||
|
private static final long serialVersionUID = 6302156810211578474L;
|
||||||
|
private static final int ICON_TEXT_GAP = 8;
|
||||||
|
private static final int KEYCAP_SIZE = 17;
|
||||||
|
private static final int KEYCAP_ARC = 5;
|
||||||
|
private String shortcut;
|
||||||
|
private boolean hovered;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean selected, boolean focused) {
|
||||||
|
Row row = (Row) value;
|
||||||
|
super.getListCellRendererComponent(list, row.isHeader() ? "" : row.view().label(), index, selected && !row.isHeader(), false);
|
||||||
|
if (row.isHeader()) {
|
||||||
|
setText(row.group().label().toUpperCase(Locale.ROOT));
|
||||||
|
setFont(StudioTheme.sectionFont());
|
||||||
|
setForeground(StudioTheme.MUTED_TEXT);
|
||||||
|
setBorder(StudioTheme.emptyBorder(index == 0 ? 2 : 10, 8, 0, 6));
|
||||||
|
setIcon(null);
|
||||||
|
setToolTipText(null);
|
||||||
|
shortcut = null;
|
||||||
|
hovered = false;
|
||||||
|
setOpaque(true);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
setFont(StudioTheme.font());
|
||||||
|
setBorder(StudioTheme.emptyBorder(0, 8, 0, 8));
|
||||||
|
setIcon(MapViewIcon.of(row.view()));
|
||||||
|
setIconTextGap(StudioTheme.scale(ICON_TEXT_GAP));
|
||||||
|
setToolTipText(row.view().description());
|
||||||
|
shortcut = row.view().shortcutText();
|
||||||
|
hovered = !selected && list instanceof MapViewList viewList && viewList.hoveredRow == index;
|
||||||
|
setOpaque(!hovered);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics graphics) {
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
if (hovered) {
|
||||||
|
paintHoverBackground(canvas);
|
||||||
|
}
|
||||||
|
super.paintComponent(graphics);
|
||||||
|
if (shortcut != null) {
|
||||||
|
paintKeycap(canvas);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shaped like the selection the look and feel paints, so moving between them doesn't jump.
|
||||||
|
private void paintHoverBackground(Graphics2D canvas) {
|
||||||
|
Insets inset = UIManager.getInsets("List.selectionInsets");
|
||||||
|
int left = inset == null ? 0 : StudioTheme.scale(inset.left);
|
||||||
|
int right = inset == null ? 0 : StudioTheme.scale(inset.right);
|
||||||
|
int arc = StudioTheme.scale(UIManager.getInt("List.selectionArc"));
|
||||||
|
canvas.setColor(StudioTheme.HOVER_BACKGROUND);
|
||||||
|
canvas.fillRoundRect(left, 0, getWidth() - left - right, getHeight(), arc, arc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintKeycap(Graphics2D canvas) {
|
||||||
|
canvas.setFont(StudioTheme.smallFont());
|
||||||
|
int textWidth = canvas.getFontMetrics().stringWidth(shortcut);
|
||||||
|
int height = StudioTheme.scale(KEYCAP_SIZE);
|
||||||
|
int width = Math.max(height, textWidth + StudioTheme.scale(8));
|
||||||
|
int x = getWidth() - getInsets().right - width;
|
||||||
|
int y = (getHeight() - height) / 2;
|
||||||
|
int arc = StudioTheme.scale(KEYCAP_ARC);
|
||||||
|
canvas.setColor(StudioTheme.CONTROL_BORDER);
|
||||||
|
canvas.drawRoundRect(x, y, width - 1, height - 1, arc, arc);
|
||||||
|
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
||||||
|
int baseline = y + (height + canvas.getFontMetrics().getAscent() - canvas.getFontMetrics().getDescent()) / 2;
|
||||||
|
canvas.drawString(shortcut, x + (width - textWidth) / 2, baseline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,555 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.components.MapStyle;
|
||||||
|
|
||||||
|
import javax.swing.AbstractAction;
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.Box;
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JFrame;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JRootPane;
|
||||||
|
import javax.swing.JSplitPane;
|
||||||
|
import javax.swing.JToggleButton;
|
||||||
|
import javax.swing.JToolBar;
|
||||||
|
import javax.swing.KeyStroke;
|
||||||
|
import javax.swing.SwingConstants;
|
||||||
|
import javax.swing.text.JTextComponent;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.CardLayout;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.EventQueue;
|
||||||
|
import java.awt.KeyboardFocusManager;
|
||||||
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.awt.event.ComponentAdapter;
|
||||||
|
import java.awt.event.ComponentEvent;
|
||||||
|
import java.awt.event.KeyEvent;
|
||||||
|
import java.awt.event.WindowAdapter;
|
||||||
|
import java.awt.event.WindowEvent;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.IntConsumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The studio map: one window showing the world as tiles in several views, with the palette, object and noise explorers
|
||||||
|
* as its Palettes, Objects and Noise views.
|
||||||
|
*/
|
||||||
|
public final class MapWindow implements MapCanvas.Listener {
|
||||||
|
private static final String MAP_CARD = "map";
|
||||||
|
private static final String PALETTES_CARD = "palettes";
|
||||||
|
private static final String NOISE_CARD = "noise";
|
||||||
|
private static final int SIDEBAR_WIDTH = 270;
|
||||||
|
private static final int SIDEBAR_MINIMUM_WIDTH = 200;
|
||||||
|
private static final int CONTENT_MINIMUM_WIDTH = 320;
|
||||||
|
private static final double PALETTE_RECENTER_DISTANCE_BLOCKS = 1D;
|
||||||
|
private static final int VIEW_LIST_SCROLL_INSETS = 4;
|
||||||
|
private static final int EXPLORER_DIVIDER_HEIGHT = 7;
|
||||||
|
|
||||||
|
private final JFrame frame;
|
||||||
|
private final PaletteExplorerStart start;
|
||||||
|
private final MapCanvas canvas;
|
||||||
|
private final MapViewList viewList;
|
||||||
|
private final JLabel viewTitle = new JLabel();
|
||||||
|
private final JPanel content = new JPanel(new CardLayout());
|
||||||
|
private final JPanel statusBars = new JPanel(new CardLayout());
|
||||||
|
private final JPanel sidebar = new JPanel(new BorderLayout());
|
||||||
|
private final IslandPanel sidebarIsland = new IslandPanel(sidebar);
|
||||||
|
private final JPanel inspectorSlot = new JPanel(new BorderLayout());
|
||||||
|
private final JComponent viewListScroll;
|
||||||
|
private final JSplitPane explorerSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
|
||||||
|
private final JSplitPane pickInspectorSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
|
||||||
|
private final List<JComponent> mapControls = new ArrayList<>();
|
||||||
|
private final List<JComponent> explorerControls = new ArrayList<>();
|
||||||
|
private final List<JComponent> noiseControls = new ArrayList<>();
|
||||||
|
private final JToolBar toolbar = StudioToolbar.create();
|
||||||
|
private final JComponent resetViewButton = StudioToolbar.button("Reset view", "Go back to the starting position and zoom (R)",
|
||||||
|
this::resetExplorerView);
|
||||||
|
private final JToggleButton gridToggle = StudioToolbar.toggle("Grid", "Show chunk and region borders (G)");
|
||||||
|
private final JToggleButton entitiesToggle = StudioToolbar.toggle("Entities", "Show mobs and other entities (E)");
|
||||||
|
private final JToggleButton followToggle = StudioToolbar.toggle("Follow", "Keep your player in the middle (F)");
|
||||||
|
private final JToggleButton shadingToggle = StudioToolbar.toggle("Shading", "Shade hills and valleys on every view (S)");
|
||||||
|
private final JToggleButton bordersToggle = StudioToolbar.toggle("Borders", "Outline the regions on every view (B)");
|
||||||
|
private final MapLegendPanel legendPanel;
|
||||||
|
private final RegionExplorer regionExplorer;
|
||||||
|
private final BiomeExplorer biomeExplorer;
|
||||||
|
|
||||||
|
private NoiseExplorerGUI paletteExplorer;
|
||||||
|
private final Map<MapView, ObjectExplorer> objectExplorers = new EnumMap<>(MapView.class);
|
||||||
|
private NoiseExplorerGUI noiseExplorer;
|
||||||
|
private JComponent noiseSidebar;
|
||||||
|
private MapView currentView;
|
||||||
|
private double paletteCenterX;
|
||||||
|
private double paletteCenterZ;
|
||||||
|
|
||||||
|
private MapWindow(PaletteExplorerStart start) {
|
||||||
|
this.start = start;
|
||||||
|
this.frame = GuiHost.createFrame("Iris Map");
|
||||||
|
this.canvas = new MapCanvas(start.engine(), start.worldId(), start.playerId(), this);
|
||||||
|
this.viewList = new MapViewList(this::showView);
|
||||||
|
this.viewListScroll = StudioToolbar.scroll(viewList);
|
||||||
|
this.legendPanel = new MapLegendPanel(share -> canvas.toggleHighlight(share.label()));
|
||||||
|
this.regionExplorer = new RegionExplorer(canvas::engine, canvas::previewRegion, canvas::showPicksAs);
|
||||||
|
this.biomeExplorer = new BiomeExplorer(canvas::engine, canvas::showPicksAs);
|
||||||
|
canvas.centerOn(start.centerX(), start.centerZ());
|
||||||
|
buildFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openMap(PaletteExplorerStart start) {
|
||||||
|
EventQueue.invokeLater(() -> open(start, MapView.BIOMES));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openPalettes(PaletteExplorerStart start) {
|
||||||
|
EventQueue.invokeLater(() -> open(start, MapView.PALETTES));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openNoise(PaletteExplorerStart start) {
|
||||||
|
EventQueue.invokeLater(() -> open(start, MapView.NOISE));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void open(PaletteExplorerStart start, MapView initialView) {
|
||||||
|
StudioTheme.install();
|
||||||
|
MapWindow window = new MapWindow(start);
|
||||||
|
window.frame.setVisible(true);
|
||||||
|
window.canvas.start();
|
||||||
|
window.showView(initialView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildFrame() {
|
||||||
|
styleExplorerSplit();
|
||||||
|
stylePickInspectorSplit();
|
||||||
|
content.add(canvas, MAP_CARD);
|
||||||
|
statusBars.add(canvas.statusBar(), MAP_CARD);
|
||||||
|
sidebar.add(viewListScroll, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
frame.setLayout(new BorderLayout());
|
||||||
|
frame.add(buildToolbar(), BorderLayout.NORTH);
|
||||||
|
frame.add(new IslandOverlay(sidebarIsland, content, StudioTheme.scale(SIDEBAR_WIDTH),
|
||||||
|
StudioTheme.scale(SIDEBAR_MINIMUM_WIDTH), StudioTheme.scale(CONTENT_MINIMUM_WIDTH)), BorderLayout.CENTER);
|
||||||
|
frame.add(inspectorSlot, BorderLayout.EAST);
|
||||||
|
frame.add(statusBars, BorderLayout.SOUTH);
|
||||||
|
installShortcuts(frame.getRootPane());
|
||||||
|
keepClearOfIsland(canvas::setCoveredLeftWidth);
|
||||||
|
frame.setSize(GuiHost.fitToScreen(StudioTheme.scaledSize(1440, 880)));
|
||||||
|
frame.setMinimumSize(StudioTheme.scaledSize(820, 520));
|
||||||
|
frame.setLocationRelativeTo(null);
|
||||||
|
frame.addWindowListener(new WindowAdapter() {
|
||||||
|
@Override
|
||||||
|
public void windowClosed(WindowEvent event) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private JToolBar buildToolbar() {
|
||||||
|
viewTitle.setFont(StudioTheme.titleFont());
|
||||||
|
viewTitle.setBorder(StudioTheme.emptyBorder(0, 6, 0, 14));
|
||||||
|
toolbar.add(viewTitle);
|
||||||
|
|
||||||
|
mapControls.add(StudioToolbar.button("Refresh", "Redraw the map with the latest pack files (R)", canvas::refreshContent));
|
||||||
|
mapControls.add(StudioToolbar.button("Reset zoom", "Go back to the default zoom (\\)", canvas::resetZoom));
|
||||||
|
mapControls.add(gridToggle);
|
||||||
|
mapControls.add(entitiesToggle);
|
||||||
|
mapControls.add(followToggle);
|
||||||
|
mapControls.add(toolbarSeparator());
|
||||||
|
mapControls.add(shadingToggle);
|
||||||
|
mapControls.add(bordersToggle);
|
||||||
|
gridToggle.addActionListener(event -> canvas.toggleGrid());
|
||||||
|
entitiesToggle.addActionListener(event -> canvas.toggleEntities());
|
||||||
|
followToggle.addActionListener(event -> canvas.toggleFollow());
|
||||||
|
shadingToggle.addActionListener(event -> canvas.toggleShading());
|
||||||
|
bordersToggle.addActionListener(event -> canvas.toggleRegionBorders());
|
||||||
|
mapControls.forEach(toolbar::add);
|
||||||
|
|
||||||
|
explorerControls.add(resetViewButton);
|
||||||
|
explorerControls.forEach(toolbar::add);
|
||||||
|
|
||||||
|
toolbar.add(Box.createHorizontalGlue());
|
||||||
|
toolbar.add(StudioToolbar.themeButton());
|
||||||
|
JComponent help = StudioToolbar.button("Shortcuts", "Show every mouse and key shortcut (/)", canvas::toggleHelp);
|
||||||
|
mapControls.add(help);
|
||||||
|
toolbar.add(help);
|
||||||
|
return toolbar;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JComponent toolbarSeparator() {
|
||||||
|
JToolBar.Separator separator = new JToolBar.Separator();
|
||||||
|
separator.setOrientation(SwingConstants.VERTICAL);
|
||||||
|
return separator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showView(MapView view) {
|
||||||
|
if (view == null || view == currentView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MapView previousView = currentView;
|
||||||
|
currentView = view;
|
||||||
|
viewList.select(view);
|
||||||
|
viewTitle.setText(view.label());
|
||||||
|
viewTitle.setToolTipText(view.description());
|
||||||
|
if (view.isTileMap()) {
|
||||||
|
showMap(previousView, view);
|
||||||
|
} else if (view == MapView.PALETTES) {
|
||||||
|
showPalettes(previousView);
|
||||||
|
} else if (view == MapView.OBJECTS || view == MapView.PROCEDURAL) {
|
||||||
|
showObjectExplorer(view);
|
||||||
|
} else {
|
||||||
|
showNoise();
|
||||||
|
}
|
||||||
|
frame.getRootPane().revalidate();
|
||||||
|
frame.getRootPane().repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showMap(MapView previousView, MapView view) {
|
||||||
|
if (previousView == MapView.PALETTES && paletteExplorer != null) {
|
||||||
|
canvas.centerOn(paletteExplorer.centerX(), paletteExplorer.centerZ());
|
||||||
|
}
|
||||||
|
canvas.showView(view);
|
||||||
|
canvas.setActive(true);
|
||||||
|
showControls(mapControls);
|
||||||
|
inspectorSlot.removeAll();
|
||||||
|
if (view == MapView.REGIONS) {
|
||||||
|
showRegionExplorer(previousView);
|
||||||
|
} else if (view == MapView.BIOMES) {
|
||||||
|
showBiomeExplorer();
|
||||||
|
} else {
|
||||||
|
useSidebar(viewListScroll);
|
||||||
|
inspectorSlot.add(legendPanel, BorderLayout.CENTER);
|
||||||
|
}
|
||||||
|
showCard(MAP_CARD);
|
||||||
|
canvas.requestFocusInWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The region inspector sits above the legend, which then lists the chosen region's biomes on the map.
|
||||||
|
private void showRegionExplorer(MapView previousView) {
|
||||||
|
if (previousView != MapView.REGIONS) {
|
||||||
|
regionExplorer.loadRegions();
|
||||||
|
}
|
||||||
|
useSidebar(viewListAbove(regionExplorer.navigator()));
|
||||||
|
showPickInspector(regionExplorer.inspector());
|
||||||
|
EventQueue.invokeLater(() -> viewList.select(currentView));
|
||||||
|
regionExplorer.measure(canvas.viewArea());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showBiomeExplorer() {
|
||||||
|
useSidebar(viewListScroll);
|
||||||
|
showPickInspector(biomeExplorer.inspector());
|
||||||
|
biomeExplorer.measure(canvas.viewArea());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The region and biome views share how the map draws its picks, so their switches show the same choice.
|
||||||
|
private void showPickInspector(PickInspectorPanel inspector) {
|
||||||
|
inspector.selectDisplay(canvas.pickDisplay());
|
||||||
|
pickInspectorSplit.setTopComponent(inspector);
|
||||||
|
pickInspectorSplit.setBottomComponent(legendPanel);
|
||||||
|
inspectorSlot.add(pickInspectorSplit, BorderLayout.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stylePickInspectorSplit() {
|
||||||
|
pickInspectorSplit.setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, StudioTheme.SEPARATOR));
|
||||||
|
pickInspectorSplit.setResizeWeight(0.7D);
|
||||||
|
pickInspectorSplit.setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||||
|
pickInspectorSplit.setDividerSize(StudioTheme.scale(EXPLORER_DIVIDER_HEIGHT));
|
||||||
|
pickInspectorSplit.putClientProperty("FlatLaf.style", "style: grip");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showPalettes(MapView previousView) {
|
||||||
|
boolean firstOpen = paletteExplorer == null;
|
||||||
|
if (firstOpen) {
|
||||||
|
createPaletteExplorer(previousView == null);
|
||||||
|
} else {
|
||||||
|
followMapIntoPalettes();
|
||||||
|
}
|
||||||
|
canvas.setActive(false);
|
||||||
|
showControls(explorerControls);
|
||||||
|
useSidebar(viewListAbove(paletteExplorer.paletteSidebar()));
|
||||||
|
inspectorSlot.removeAll();
|
||||||
|
inspectorSlot.add(paletteExplorer.paletteInspector(), BorderLayout.CENTER);
|
||||||
|
showCard(PALETTES_CARD);
|
||||||
|
EventQueue.invokeLater(() -> viewList.select(currentView));
|
||||||
|
if (firstOpen) {
|
||||||
|
paletteExplorer.openViewer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showObjectExplorer(MapView view) {
|
||||||
|
boolean firstOpen = !objectExplorers.containsKey(view);
|
||||||
|
ObjectExplorer explorer = objectExplorers.computeIfAbsent(view, this::createObjectExplorer);
|
||||||
|
canvas.setActive(false);
|
||||||
|
showControls(explorerControls);
|
||||||
|
useSidebar(viewListAbove(explorer.browser()));
|
||||||
|
inspectorSlot.removeAll();
|
||||||
|
inspectorSlot.add(explorer.overview(), BorderLayout.CENTER);
|
||||||
|
showCard(view.name());
|
||||||
|
EventQueue.invokeLater(() -> viewList.select(currentView));
|
||||||
|
if (firstOpen) {
|
||||||
|
explorer.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void keepClearOfIsland(IntConsumer setCoveredLeftWidth) {
|
||||||
|
setCoveredLeftWidth.accept(sidebarIsland.getWidth());
|
||||||
|
sidebarIsland.addComponentListener(new ComponentAdapter() {
|
||||||
|
@Override
|
||||||
|
public void componentResized(ComponentEvent event) {
|
||||||
|
setCoveredLeftWidth.accept(sidebarIsland.getWidth());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectExplorer createObjectExplorer(MapView view) {
|
||||||
|
ObjectSource source = view == MapView.PROCEDURAL ? new ProceduralObjectSource() : new PackObjectSource();
|
||||||
|
ObjectExplorer explorer = new ObjectExplorer(start.engine(), start.worldId(), source);
|
||||||
|
keepClearOfIsland(explorer::setCoveredLeftWidth);
|
||||||
|
content.add(explorer.view(), view.name());
|
||||||
|
statusBars.add(explorer.statusBar(), view.name());
|
||||||
|
return explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showNoise() {
|
||||||
|
boolean firstOpen = noiseExplorer == null;
|
||||||
|
if (firstOpen) {
|
||||||
|
createNoiseExplorer();
|
||||||
|
}
|
||||||
|
canvas.setActive(false);
|
||||||
|
showControls(noiseControls, explorerControls);
|
||||||
|
useSidebar(viewListAbove(noiseSidebar));
|
||||||
|
inspectorSlot.removeAll();
|
||||||
|
showCard(NOISE_CARD);
|
||||||
|
EventQueue.invokeLater(() -> viewList.select(currentView));
|
||||||
|
if (firstOpen) {
|
||||||
|
noiseExplorer.openViewer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both explorers share one split, so the divider stays where it was dragged when switching between them.
|
||||||
|
private JSplitPane viewListAbove(JComponent explorerSidebar) {
|
||||||
|
boolean firstUse = explorerSplit.getBottomComponent() == null;
|
||||||
|
explorerSplit.setTopComponent(viewListScroll);
|
||||||
|
explorerSplit.setBottomComponent(explorerSidebar);
|
||||||
|
if (firstUse) {
|
||||||
|
EventQueue.invokeLater(this::fitViewListAboveExplorer);
|
||||||
|
}
|
||||||
|
return explorerSplit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void styleExplorerSplit() {
|
||||||
|
explorerSplit.setBorder(BorderFactory.createEmptyBorder());
|
||||||
|
explorerSplit.setResizeWeight(0D);
|
||||||
|
explorerSplit.setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||||
|
explorerSplit.setDividerSize(StudioTheme.scale(EXPLORER_DIVIDER_HEIGHT));
|
||||||
|
explorerSplit.putClientProperty("FlatLaf.style", "style: grip");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every view stays in sight, so the divider starts below the last one and can only be dragged further down. Measured
|
||||||
|
// once the sidebar is laid out, since the list's rows are taller before they know their width.
|
||||||
|
private void fitViewListAboveExplorer() {
|
||||||
|
int fullHeight = viewList.getPreferredSize().height + VIEW_LIST_SCROLL_INSETS;
|
||||||
|
viewListScroll.setMinimumSize(new Dimension(0, fullHeight));
|
||||||
|
explorerSplit.setDividerLocation(fullHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createNoiseExplorer() {
|
||||||
|
noiseExplorer = NoiseExplorerGUI.createNoiseExplorer(start.engine());
|
||||||
|
noiseSidebar = noiseExplorer.noiseSidebar();
|
||||||
|
noiseControls.addAll(noiseExplorer.noiseControls());
|
||||||
|
noiseControls.forEach(control -> toolbar.add(control, toolbar.getComponentIndex(resetViewButton)));
|
||||||
|
content.add(noiseExplorer, NOISE_CARD);
|
||||||
|
statusBars.add(noiseExplorer.statusBar(), NOISE_CARD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createPaletteExplorer(boolean openedOnPalettes) {
|
||||||
|
PaletteExplorerStart explorerStart = openedOnPalettes
|
||||||
|
? start
|
||||||
|
: new PaletteExplorerStart(start.engine(), start.worldId(), start.playerId(), null, canvas.centerX(), canvas.centerZ());
|
||||||
|
paletteExplorer = NoiseExplorerGUI.createPaletteExplorer(explorerStart);
|
||||||
|
content.add(paletteExplorer, PALETTES_CARD);
|
||||||
|
statusBars.add(paletteExplorer.statusBar(), PALETTES_CARD);
|
||||||
|
rememberPaletteCenter(explorerStart.centerX(), explorerStart.centerZ());
|
||||||
|
if (!openedOnPalettes) {
|
||||||
|
showSurfaceBiomePalettes(explorerStart.centerX(), explorerStart.centerZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coming back from the map only moves the palettes when the map was moved, so a chosen layer survives a quick look.
|
||||||
|
private void followMapIntoPalettes() {
|
||||||
|
double movedX = canvas.centerX() - paletteCenterX;
|
||||||
|
double movedZ = canvas.centerZ() - paletteCenterZ;
|
||||||
|
if (Math.hypot(movedX, movedZ) < PALETTE_RECENTER_DISTANCE_BLOCKS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
paletteExplorer.centerOn(canvas.centerX(), canvas.centerZ());
|
||||||
|
rememberPaletteCenter(canvas.centerX(), canvas.centerZ());
|
||||||
|
showSurfaceBiomePalettes(canvas.centerX(), canvas.centerZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShowPalettesAt(double worldX, double worldZ) {
|
||||||
|
canvas.centerOn(worldX, worldZ);
|
||||||
|
showView(MapView.PALETTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSurfaceBiomePalettes(double worldX, double worldZ) {
|
||||||
|
canvas.findSurfaceBiome(worldX, worldZ, biomeKey -> {
|
||||||
|
if (paletteExplorer != null) {
|
||||||
|
paletteExplorer.showPalettesAt(worldX, worldZ, biomeKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rememberPaletteCenter(double worldX, double worldZ) {
|
||||||
|
paletteCenterX = worldX;
|
||||||
|
paletteCenterZ = worldZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTogglesChanged() {
|
||||||
|
gridToggle.setSelected(canvas.isGridVisible());
|
||||||
|
entitiesToggle.setSelected(canvas.areEntitiesVisible());
|
||||||
|
followToggle.setSelected(canvas.isFollowing());
|
||||||
|
shadingToggle.setSelected(canvas.style().shading());
|
||||||
|
bordersToggle.setSelected(canvas.style().regionBorders());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLegendChanged(String title, List<MapCensus.Share> shares, MapStyle.Highlight highlight) {
|
||||||
|
legendPanel.show(title, shares, highlight);
|
||||||
|
if (currentView == MapView.REGIONS) {
|
||||||
|
regionExplorer.measure(canvas.viewArea());
|
||||||
|
} else if (currentView == MapView.BIOMES) {
|
||||||
|
biomeExplorer.measure(canvas.viewArea());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContentRefreshed() {
|
||||||
|
regionExplorer.refresh();
|
||||||
|
biomeExplorer.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetExplorerView() {
|
||||||
|
ObjectExplorer objectExplorer = objectExplorers.get(currentView);
|
||||||
|
if (objectExplorer != null) {
|
||||||
|
objectExplorer.resetCamera();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NoiseExplorerGUI explorer = currentView == MapView.NOISE ? noiseExplorer : paletteExplorer;
|
||||||
|
if (explorer != null) {
|
||||||
|
explorer.resetViewport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void useSidebar(JComponent component) {
|
||||||
|
sidebar.removeAll();
|
||||||
|
sidebar.add(component, BorderLayout.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showCard(String card) {
|
||||||
|
((CardLayout) content.getLayout()).show(content, card);
|
||||||
|
((CardLayout) statusBars.getLayout()).show(statusBars, card);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SafeVarargs
|
||||||
|
private void showControls(List<JComponent>... visibleControls) {
|
||||||
|
for (List<JComponent> controls : List.of(mapControls, explorerControls, noiseControls)) {
|
||||||
|
controls.forEach(control -> control.setVisible(false));
|
||||||
|
}
|
||||||
|
for (List<JComponent> controls : visibleControls) {
|
||||||
|
controls.forEach(control -> control.setVisible(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void installShortcuts(JRootPane rootPane) {
|
||||||
|
for (MapView view : MapView.values()) {
|
||||||
|
bind(rootPane, KeyStroke.getKeyStroke(view.shortcutKey(), 0), "view-" + view.name(), () -> showView(view));
|
||||||
|
}
|
||||||
|
bind(rootPane, KeyStroke.getKeyStroke(KeyEvent.VK_M, 0), "next-view", this::showNextView);
|
||||||
|
bind(rootPane, KeyStroke.getKeyStroke(KeyEvent.VK_R, 0), "refresh", this::refreshOrReset);
|
||||||
|
bindMap(rootPane, KeyEvent.VK_F, 0, "follow", canvas::toggleFollow);
|
||||||
|
bindMap(rootPane, KeyEvent.VK_G, 0, "grid", canvas::toggleGrid);
|
||||||
|
bindMap(rootPane, KeyEvent.VK_E, 0, "entities", canvas::toggleEntities);
|
||||||
|
bindMap(rootPane, KeyEvent.VK_F3, 0, "debug", canvas::toggleDebug);
|
||||||
|
bindMap(rootPane, KeyEvent.VK_H, 0, "highlight", canvas::highlightUnderCursor);
|
||||||
|
bindMap(rootPane, KeyEvent.VK_ESCAPE, 0, "dismiss", canvas::dismissEmphasis);
|
||||||
|
bindMap(rootPane, KeyEvent.VK_S, 0, "shading", canvas::toggleShading);
|
||||||
|
bindMap(rootPane, KeyEvent.VK_B, 0, "borders", canvas::toggleRegionBorders);
|
||||||
|
bindMap(rootPane, KeyEvent.VK_EQUALS, 0, "zoom-in", canvas::zoomIn);
|
||||||
|
bindMap(rootPane, KeyEvent.VK_EQUALS, KeyEvent.SHIFT_DOWN_MASK, "zoom-in-plus", canvas::zoomIn);
|
||||||
|
bindMap(rootPane, KeyEvent.VK_ADD, 0, "zoom-in-keypad", canvas::zoomIn);
|
||||||
|
bindMap(rootPane, KeyEvent.VK_MINUS, 0, "zoom-out", canvas::zoomOut);
|
||||||
|
bindMap(rootPane, KeyEvent.VK_SUBTRACT, 0, "zoom-out-keypad", canvas::zoomOut);
|
||||||
|
bindMap(rootPane, KeyEvent.VK_BACK_SLASH, 0, "reset-zoom", canvas::resetZoom);
|
||||||
|
bindMap(rootPane, KeyEvent.VK_SLASH, 0, "help", canvas::toggleHelp);
|
||||||
|
bindMap(rootPane, KeyEvent.VK_SLASH, KeyEvent.SHIFT_DOWN_MASK, "help-question", canvas::toggleHelp);
|
||||||
|
bind(rootPane, KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT, KeyEvent.SHIFT_DOWN_MASK, false), "details-on",
|
||||||
|
() -> canvas.setDetailedHover(true));
|
||||||
|
bind(rootPane, KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT, 0, true), "details-off", () -> canvas.setDetailedHover(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindMap(JRootPane rootPane, int keyCode, int modifiers, String name, Runnable action) {
|
||||||
|
bind(rootPane, KeyStroke.getKeyStroke(keyCode, modifiers), name, () -> {
|
||||||
|
if (currentView != null && currentView.isTileMap()) {
|
||||||
|
action.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys typed into a search box belong to that box, not to the window's shortcuts.
|
||||||
|
private static void bind(JRootPane rootPane, KeyStroke keyStroke, String name, Runnable action) {
|
||||||
|
rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(keyStroke, name);
|
||||||
|
rootPane.getActionMap().put(name, new AbstractAction() {
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent event) {
|
||||||
|
if (!(KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() instanceof JTextComponent)) {
|
||||||
|
action.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showNextView() {
|
||||||
|
MapView[] views = MapView.values();
|
||||||
|
showView(views[(currentView.ordinal() + 1) % views.length]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshOrReset() {
|
||||||
|
if (currentView.isTileMap()) {
|
||||||
|
canvas.refreshContent();
|
||||||
|
} else {
|
||||||
|
resetExplorerView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void close() {
|
||||||
|
canvas.close();
|
||||||
|
regionExplorer.close();
|
||||||
|
biomeExplorer.close();
|
||||||
|
if (paletteExplorer != null) {
|
||||||
|
paletteExplorer.close();
|
||||||
|
}
|
||||||
|
objectExplorers.values().forEach(ObjectExplorer::close);
|
||||||
|
if (noiseExplorer != null) {
|
||||||
|
noiseExplorer.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.volmit.iris.engine.object.IrisBiomeCustom;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
// Works out biome tint colours the way Minecraft does, from the colour maps and vanilla biome files in the client jar.
|
||||||
|
final class MinecraftBiomeColors {
|
||||||
|
private static final String COLOR_MAP_FOLDER = "assets/minecraft/textures/colormap/";
|
||||||
|
private static final int COLOR_MAP_SIZE = 256;
|
||||||
|
private static final int GRASS_FALLBACK = 0xFF00FF;
|
||||||
|
private static final int FOLIAGE_FALLBACK = 0x48B518;
|
||||||
|
private static final int DRY_FOLIAGE_FALLBACK = 0x5C3C32;
|
||||||
|
private static final float UNKNOWN_BIOME_TEMPERATURE = 0.5F;
|
||||||
|
private static final float UNKNOWN_BIOME_DOWNFALL = 0.5F;
|
||||||
|
private static final int UNKNOWN_BIOME_WATER = 0x3F76E4;
|
||||||
|
private static final int UNPARSEABLE_COLOR = 0x000000;
|
||||||
|
|
||||||
|
private final ZipFile clientJar;
|
||||||
|
private final ColorMap grassMap;
|
||||||
|
private final ColorMap foliageMap;
|
||||||
|
private final ColorMap dryFoliageMap;
|
||||||
|
private final Map<NamespacedKey, BiomeTintColors> vanillaBiomes = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private MinecraftBiomeColors(ZipFile clientJar, ColorMap grassMap, ColorMap foliageMap, ColorMap dryFoliageMap) {
|
||||||
|
this.clientJar = clientJar;
|
||||||
|
this.grassMap = grassMap;
|
||||||
|
this.foliageMap = foliageMap;
|
||||||
|
this.dryFoliageMap = dryFoliageMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
static MinecraftBiomeColors load(ZipFile clientJar) throws IOException {
|
||||||
|
return new MinecraftBiomeColors(
|
||||||
|
clientJar,
|
||||||
|
ColorMap.read(clientJar, "grass.png", GRASS_FALLBACK),
|
||||||
|
ColorMap.read(clientJar, "foliage.png", FOLIAGE_FALLBACK),
|
||||||
|
ColorMap.read(clientJar, "dry_foliage.png", DRY_FOLIAGE_FALLBACK)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BiomeTintColors vanilla(NamespacedKey biome) {
|
||||||
|
return vanillaBiomes.computeIfAbsent(biome, this::readVanillaBiome);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iris registers custom biomes as data pack biomes built from these fields, see IrisBiomeCustom#generateJson.
|
||||||
|
BiomeTintColors custom(IrisBiomeCustom biome) {
|
||||||
|
float temperature = (float) biome.getTemperature();
|
||||||
|
float downfall = (float) biome.getHumidity();
|
||||||
|
return new BiomeTintColors(
|
||||||
|
biome.getGrassColor().isEmpty() ? grassMap.colorAt(temperature, downfall) : parseIrisColor(biome.getGrassColor()),
|
||||||
|
BiomeTintColors.GrassColorModifier.NONE,
|
||||||
|
biome.getFoliageColor().isEmpty() ? foliageMap.colorAt(temperature, downfall) : parseIrisColor(biome.getFoliageColor()),
|
||||||
|
dryFoliageMap.colorAt(temperature, downfall),
|
||||||
|
parseIrisColor(biome.getWaterColor())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BiomeTintColors readVanillaBiome(NamespacedKey biome) {
|
||||||
|
JsonObject definition = readBiomeDefinition(biome);
|
||||||
|
if (definition == null) {
|
||||||
|
return fromClimate(UNKNOWN_BIOME_TEMPERATURE, UNKNOWN_BIOME_DOWNFALL, new JsonObject());
|
||||||
|
}
|
||||||
|
JsonObject effects = definition.has("effects") ? definition.getAsJsonObject("effects") : new JsonObject();
|
||||||
|
return fromClimate(definition.get("temperature").getAsFloat(), definition.get("downfall").getAsFloat(), effects);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BiomeTintColors fromClimate(float temperature, float downfall, JsonObject effects) {
|
||||||
|
return new BiomeTintColors(
|
||||||
|
effects.has("grass_color") ? parseVanillaColor(effects.get("grass_color")) : grassMap.colorAt(temperature, downfall),
|
||||||
|
effects.has("grass_color_modifier")
|
||||||
|
? BiomeTintColors.GrassColorModifier.named(effects.get("grass_color_modifier").getAsString())
|
||||||
|
: BiomeTintColors.GrassColorModifier.NONE,
|
||||||
|
effects.has("foliage_color") ? parseVanillaColor(effects.get("foliage_color")) : foliageMap.colorAt(temperature, downfall),
|
||||||
|
effects.has("dry_foliage_color") ? parseVanillaColor(effects.get("dry_foliage_color")) : dryFoliageMap.colorAt(temperature, downfall),
|
||||||
|
effects.has("water_color") ? parseVanillaColor(effects.get("water_color")) : UNKNOWN_BIOME_WATER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject readBiomeDefinition(NamespacedKey biome) {
|
||||||
|
try {
|
||||||
|
return ClientJarFiles.readJson(clientJar, "data/" + biome.getNamespace() + "/worldgen/biome/" + biome.getKey() + ".json");
|
||||||
|
} catch (IOException | RuntimeException error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Older versions store colours as plain numbers, newer ones as "#rrggbb" strings.
|
||||||
|
private static int parseVanillaColor(JsonElement color) {
|
||||||
|
if (color.getAsJsonPrimitive().isNumber()) {
|
||||||
|
return color.getAsInt() & 0xFFFFFF;
|
||||||
|
}
|
||||||
|
return Integer.parseInt(color.getAsString().substring(1), 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int parseIrisColor(String color) {
|
||||||
|
try {
|
||||||
|
return Color.decode((color.startsWith("#") ? color : "#" + color).trim()).getRGB() & 0xFFFFFF;
|
||||||
|
} catch (RuntimeException error) {
|
||||||
|
return UNPARSEABLE_COLOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minecraft's ColorMapColorUtil: temperature picks the column and temperature times downfall picks the row.
|
||||||
|
private record ColorMap(int[] pixels, int fallback) {
|
||||||
|
private static ColorMap read(ZipFile clientJar, String name, int fallback) throws IOException {
|
||||||
|
BufferedImage image = ClientJarFiles.readImage(clientJar, COLOR_MAP_FOLDER + name);
|
||||||
|
if (image == null) {
|
||||||
|
throw new IOException("Minecraft colour map " + name + " is missing");
|
||||||
|
}
|
||||||
|
int[] pixels = new int[COLOR_MAP_SIZE * COLOR_MAP_SIZE];
|
||||||
|
for (int y = 0; y < COLOR_MAP_SIZE; y++) {
|
||||||
|
for (int x = 0; x < COLOR_MAP_SIZE; x++) {
|
||||||
|
pixels[y * COLOR_MAP_SIZE + x] = image.getRGB(x, y) & 0xFFFFFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ColorMap(pixels, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int colorAt(float temperature, float downfall) {
|
||||||
|
double clampedTemperature = Math.clamp(temperature, 0F, 1F);
|
||||||
|
double clampedDownfall = Math.clamp(downfall, 0F, 1F) * clampedTemperature;
|
||||||
|
int x = (int) ((1D - clampedTemperature) * 255D);
|
||||||
|
int y = (int) ((1D - clampedDownfall) * 255D);
|
||||||
|
int index = y << 8 | x;
|
||||||
|
return index >= pixels.length ? fallback : pixels[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
// Mirrors Minecraft's BlockColors: which colour each tinted face of a block is multiplied with in the world.
|
||||||
|
final class MinecraftBlockTints {
|
||||||
|
static final int NO_TINT = 0xFFFFFF;
|
||||||
|
private static final Set<String> GRASS_TINTED = Set.of(
|
||||||
|
"grass_block", "short_grass", "fern", "potted_fern", "bush", "tall_grass", "large_fern", "sugar_cane");
|
||||||
|
private static final Set<String> GRASS_TINTED_ON_SECOND_LAYER = Set.of("pink_petals", "wildflowers");
|
||||||
|
private static final Set<String> FOLIAGE_TINTED = Set.of(
|
||||||
|
"oak_leaves", "jungle_leaves", "acacia_leaves", "dark_oak_leaves", "vine", "mangrove_leaves");
|
||||||
|
private static final Set<String> WATER_TINTED_BY_FLUID_RENDERER = Set.of("water", "bubble_column");
|
||||||
|
private static final Set<String> GROWING_STEMS = Set.of("melon_stem", "pumpkin_stem");
|
||||||
|
private static final Map<String, Integer> FIXED_TINTS = Map.of(
|
||||||
|
"spruce_leaves", 0x619961,
|
||||||
|
"birch_leaves", 0x80A755,
|
||||||
|
"attached_melon_stem", 0xE0C71C,
|
||||||
|
"attached_pumpkin_stem", 0xE0C71C,
|
||||||
|
"lily_pad", 0x208030
|
||||||
|
);
|
||||||
|
|
||||||
|
private MinecraftBlockTints() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static Optional<BiomeTint> biomeTint(String blockName, int tintIndex) {
|
||||||
|
if (WATER_TINTED_BY_FLUID_RENDERER.contains(blockName)) {
|
||||||
|
return Optional.of(BiomeTint.WATER);
|
||||||
|
}
|
||||||
|
if (GRASS_TINTED_ON_SECOND_LAYER.contains(blockName)) {
|
||||||
|
return tintIndex == 1 ? Optional.of(BiomeTint.GRASS) : Optional.empty();
|
||||||
|
}
|
||||||
|
if (tintIndex != 0) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
if (GRASS_TINTED.contains(blockName)) {
|
||||||
|
return Optional.of(BiomeTint.GRASS);
|
||||||
|
}
|
||||||
|
if (FOLIAGE_TINTED.contains(blockName)) {
|
||||||
|
return Optional.of(BiomeTint.FOLIAGE);
|
||||||
|
}
|
||||||
|
if (blockName.equals("leaf_litter")) {
|
||||||
|
return Optional.of(BiomeTint.DRY_FOLIAGE);
|
||||||
|
}
|
||||||
|
if (blockName.equals("water_cauldron")) {
|
||||||
|
return Optional.of(BiomeTint.WATER);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int fixedTint(String blockName, Map<String, String> properties, int tintIndex) {
|
||||||
|
if (tintIndex != 0) {
|
||||||
|
return NO_TINT;
|
||||||
|
}
|
||||||
|
if (GROWING_STEMS.contains(blockName)) {
|
||||||
|
return stemTint(Integer.parseInt(properties.getOrDefault("age", "0")));
|
||||||
|
}
|
||||||
|
return FIXED_TINTS.getOrDefault(blockName, NO_TINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int stemTint(int age) {
|
||||||
|
return (age * 32) << 16 | (255 - age * 8) << 8 | (age * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.volmit.iris.Iris;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.security.DigestInputStream;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
final class MinecraftClientJar {
|
||||||
|
private static final URI VERSION_MANIFEST = URI.create("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json");
|
||||||
|
private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(15);
|
||||||
|
private static final Duration DOWNLOAD_TIMEOUT = Duration.ofMinutes(10);
|
||||||
|
private static final HttpClient HTTP = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(CONNECT_TIMEOUT)
|
||||||
|
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private MinecraftClientJar() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isCached() {
|
||||||
|
return cachedJar().exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
static CompletableFuture<ZipFile> open() {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
File jar = cachedJar();
|
||||||
|
if (!jar.exists()) {
|
||||||
|
download(Bukkit.getMinecraftVersion(), jar.toPath());
|
||||||
|
}
|
||||||
|
return new ZipFile(jar);
|
||||||
|
} catch (IOException | InterruptedException error) {
|
||||||
|
throw new IllegalStateException(readableReason(error), error);
|
||||||
|
}
|
||||||
|
}, MinecraftClientJar::runInBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runInBackground(Runnable task) {
|
||||||
|
Thread thread = new Thread(task, "Iris Minecraft Client Download");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File cachedJar() {
|
||||||
|
return Iris.instance.getDataFile("cache", "minecraft-client", Bukkit.getMinecraftVersion() + ".jar");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void download(String version, Path destination) throws IOException, InterruptedException {
|
||||||
|
JsonObject versionInfo = fetchJson(findVersionUrl(version));
|
||||||
|
JsonObject client = versionInfo.getAsJsonObject("downloads").getAsJsonObject("client");
|
||||||
|
URI clientUrl = URI.create(client.get("url").getAsString());
|
||||||
|
String expectedSha1 = client.get("sha1").getAsString();
|
||||||
|
|
||||||
|
Path partial = destination.resolveSibling(destination.getFileName() + ".part");
|
||||||
|
HttpResponse<InputStream> response = HTTP.send(
|
||||||
|
HttpRequest.newBuilder(clientUrl).timeout(DOWNLOAD_TIMEOUT).build(),
|
||||||
|
HttpResponse.BodyHandlers.ofInputStream()
|
||||||
|
);
|
||||||
|
requireSuccess(response.statusCode(), clientUrl);
|
||||||
|
String actualSha1;
|
||||||
|
try (DigestInputStream body = new DigestInputStream(response.body(), sha1())) {
|
||||||
|
Files.copy(body, partial, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
actualSha1 = HexFormat.of().formatHex(body.getMessageDigest().digest());
|
||||||
|
}
|
||||||
|
if (!actualSha1.equalsIgnoreCase(expectedSha1)) {
|
||||||
|
Files.deleteIfExists(partial);
|
||||||
|
throw new IOException("the downloaded client jar was damaged (checksum mismatch)");
|
||||||
|
}
|
||||||
|
Files.move(partial, destination, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static URI findVersionUrl(String version) throws IOException, InterruptedException {
|
||||||
|
for (var entry : fetchJson(VERSION_MANIFEST).getAsJsonArray("versions")) {
|
||||||
|
JsonObject candidate = entry.getAsJsonObject();
|
||||||
|
if (candidate.get("id").getAsString().equals(version)) {
|
||||||
|
return URI.create(candidate.get("url").getAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IOException("Mojang doesn't list Minecraft " + version);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject fetchJson(URI url) throws IOException, InterruptedException {
|
||||||
|
HttpResponse<String> response = HTTP.send(
|
||||||
|
HttpRequest.newBuilder(url).timeout(CONNECT_TIMEOUT).build(),
|
||||||
|
HttpResponse.BodyHandlers.ofString()
|
||||||
|
);
|
||||||
|
requireSuccess(response.statusCode(), url);
|
||||||
|
return JsonParser.parseString(response.body()).getAsJsonObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireSuccess(int statusCode, URI url) throws IOException {
|
||||||
|
if (statusCode / 100 != 2) {
|
||||||
|
throw new IOException(url.getHost() + " answered HTTP " + statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MessageDigest sha1() {
|
||||||
|
try {
|
||||||
|
return MessageDigest.getInstance("SHA-1");
|
||||||
|
} catch (NoSuchAlgorithmException error) {
|
||||||
|
throw new IllegalStateException(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readableReason(Throwable error) {
|
||||||
|
String message = error.getMessage();
|
||||||
|
return message == null || message.isBlank() ? error.getClass().getSimpleName() : message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
|
||||||
|
interface NoiseColorScale {
|
||||||
|
int colorFinite(double value);
|
||||||
|
|
||||||
|
default int colorAt(double value, double worldX, double worldZ) {
|
||||||
|
return colorFinite(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
int colorInvalid();
|
||||||
|
|
||||||
|
default int colorInvalidAt(double worldX, double worldZ) {
|
||||||
|
return colorInvalid();
|
||||||
|
}
|
||||||
|
|
||||||
|
Color displayColorNormalized(double normalized);
|
||||||
|
|
||||||
|
String label();
|
||||||
|
|
||||||
|
double minimum();
|
||||||
|
|
||||||
|
double maximum();
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
|
||||||
|
enum NoisePalette implements NoiseColorScale {
|
||||||
|
TERRAIN("Terrain", 0D, 1D, new int[]{0x071A2F, 0x155E75, 0x2A9D8F, 0xE9C46A, 0xF4F1DE}),
|
||||||
|
SIGNED("Signed", -1D, 1D, new int[]{0x173B66, 0x4F86C6, 0xE7EDF3, 0xE89555, 0x9D3B35}),
|
||||||
|
GRAYSCALE("Grayscale", 0D, 1D, new int[]{0x050505, 0xFFFFFF});
|
||||||
|
|
||||||
|
static final int INVALID_COLOR = 0xFF2DAA;
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
private final double minimum;
|
||||||
|
private final double maximum;
|
||||||
|
private final int[] lookup;
|
||||||
|
private final Color[] displayColors;
|
||||||
|
|
||||||
|
NoisePalette(String label, double minimum, double maximum, int[] stops) {
|
||||||
|
this.label = label;
|
||||||
|
this.minimum = minimum;
|
||||||
|
this.maximum = maximum;
|
||||||
|
this.lookup = buildLookup(stops);
|
||||||
|
this.displayColors = buildDisplayColors(lookup);
|
||||||
|
}
|
||||||
|
|
||||||
|
int color(double value) {
|
||||||
|
if (!Double.isFinite(value)) {
|
||||||
|
return INVALID_COLOR;
|
||||||
|
}
|
||||||
|
return colorFinite(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int colorFinite(double value) {
|
||||||
|
double normalized = (value - minimum) / (maximum - minimum);
|
||||||
|
return colorNormalized(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
int colorNormalized(double normalized) {
|
||||||
|
double clipped = Math.max(0D, Math.min(1D, normalized));
|
||||||
|
return lookup[(int) Math.round(clipped * (lookup.length - 1))];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int colorInvalid() {
|
||||||
|
return INVALID_COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Color displayColorNormalized(double normalized) {
|
||||||
|
double clipped = Math.max(0D, Math.min(1D, normalized));
|
||||||
|
return displayColors[(int) Math.round(clipped * (displayColors.length - 1))];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String label() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double minimum() {
|
||||||
|
return minimum;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double maximum() {
|
||||||
|
return maximum;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] buildLookup(int[] stops) {
|
||||||
|
int[] values = new int[256];
|
||||||
|
for (int index = 0; index < values.length; index++) {
|
||||||
|
double position = (index / 255D) * (stops.length - 1);
|
||||||
|
int lowerIndex = Math.min(stops.length - 2, (int) position);
|
||||||
|
double fraction = position - lowerIndex;
|
||||||
|
values[index] = interpolate(stops[lowerIndex], stops[lowerIndex + 1], fraction);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color[] buildDisplayColors(int[] lookup) {
|
||||||
|
Color[] colors = new Color[lookup.length];
|
||||||
|
for (int index = 0; index < lookup.length; index++) {
|
||||||
|
colors[index] = new Color(lookup[index]);
|
||||||
|
}
|
||||||
|
return colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int interpolate(int from, int to, double fraction) {
|
||||||
|
int red = channel(from, 16, to, fraction);
|
||||||
|
int green = channel(from, 8, to, fraction);
|
||||||
|
int blue = channel(from, 0, to, fraction);
|
||||||
|
return (red << 16) | (green << 8) | blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int channel(int from, int shift, int to, double fraction) {
|
||||||
|
int fromChannel = (from >>> shift) & 0xFF;
|
||||||
|
int toChannel = (to >>> shift) & 0xFF;
|
||||||
|
return (int) Math.round(fromChannel + ((toChannel - fromChannel) * fraction));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Cursor;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.Rectangle;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One noise that picks regions or biomes: the spread of its values in view above the bands that pick each option,
|
||||||
|
* and a table of how much of the noise each option is given and how much it gets in view. A long table shows only
|
||||||
|
* the options with the most ground in view until asked for all; a noise with one option to pick is a single line.
|
||||||
|
*/
|
||||||
|
final class NoisePickChart extends JComponent {
|
||||||
|
private static final long serialVersionUID = 1873365510472853316L;
|
||||||
|
private static final int PREFERRED_WIDTH = 300;
|
||||||
|
private static final int MARGIN = 16;
|
||||||
|
private static final int HISTOGRAM_HEIGHT = 64;
|
||||||
|
private static final int BAND_HEIGHT = 12;
|
||||||
|
private static final int TITLE_GAP = 6;
|
||||||
|
private static final int SECTION_GAP = 12;
|
||||||
|
private static final int BOTTOM_GAP = 14;
|
||||||
|
private static final Color HISTOGRAM_BAR = new Color(0x9D, 0xA0, 0xA8, 150);
|
||||||
|
private static final Color BAND_DIVIDER = new Color(255, 255, 255, 70);
|
||||||
|
private static final String WIDEST_SHARE = "▲ 100.0%";
|
||||||
|
private static final int SHORT_TABLE_ROWS = 6;
|
||||||
|
|
||||||
|
private final NoisePickSpread spread;
|
||||||
|
private final List<NoisePickSpread.Row> rows;
|
||||||
|
private final String optionNoun;
|
||||||
|
private final Consumer<Boolean> onShowAllRowsChanged;
|
||||||
|
private boolean showsAllRows;
|
||||||
|
private Rectangle showAllToggleBounds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param optionNoun what the noise picks, as a table heading: "Regions" or "Biomes"
|
||||||
|
* @param showsAllRows whether a long table starts with all its rows
|
||||||
|
* @param onShowAllRowsChanged receives whether the table shows all its rows after the user asked for more or fewer
|
||||||
|
*/
|
||||||
|
NoisePickChart(NoisePickSpread spread, String optionNoun, boolean showsAllRows, Consumer<Boolean> onShowAllRowsChanged) {
|
||||||
|
this.spread = spread;
|
||||||
|
this.rows = spread.rows();
|
||||||
|
this.optionNoun = optionNoun;
|
||||||
|
this.showsAllRows = showsAllRows;
|
||||||
|
this.onShowAllRowsChanged = onShowAllRowsChanged;
|
||||||
|
setAlignmentX(LEFT_ALIGNMENT);
|
||||||
|
listenForShowAllToggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredSize() {
|
||||||
|
if (hasSingleOption()) {
|
||||||
|
return new Dimension(StudioTheme.scale(PREFERRED_WIDTH), StudioTheme.scale(SECTION_GAP) + tableHeight()
|
||||||
|
+ StudioTheme.scale(BOTTOM_GAP));
|
||||||
|
}
|
||||||
|
int height = StudioTheme.scale(SECTION_GAP) + spreadHeight() + StudioTheme.scale(SECTION_GAP) + tableHeight()
|
||||||
|
+ StudioTheme.scale(BOTTOM_GAP);
|
||||||
|
return new Dimension(StudioTheme.scale(PREFERRED_WIDTH), height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getMaximumSize() {
|
||||||
|
return new Dimension(Integer.MAX_VALUE, getPreferredSize().height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics graphics) {
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
int margin = StudioTheme.scale(MARGIN);
|
||||||
|
int width = getWidth() - margin * 2;
|
||||||
|
int top = StudioTheme.scale(SECTION_GAP);
|
||||||
|
if (hasSingleOption()) {
|
||||||
|
drawSingleOption(canvas, margin, top, width);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drawSpread(canvas, margin, top, width);
|
||||||
|
drawTable(canvas, margin, top + spreadHeight() + StudioTheme.scale(SECTION_GAP), width);
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int spreadHeight() {
|
||||||
|
return sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP) + StudioTheme.scale(HISTOGRAM_HEIGHT)
|
||||||
|
+ StudioTheme.scale(2) + StudioTheme.scale(BAND_HEIGHT) + StudioTheme.scale(3) + smallMetrics().getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasSingleOption() {
|
||||||
|
return rows.size() == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isLongTable() {
|
||||||
|
return rows.size() > SHORT_TABLE_ROWS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int tableHeight() {
|
||||||
|
int toggleRows = isLongTable() ? 1 : 0;
|
||||||
|
return sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP) + rowHeight() * (shownRows().size() + toggleRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A short table keeps the options with the most ground in view, still in the order the noise meets them.
|
||||||
|
private List<NoisePickSpread.Row> shownRows() {
|
||||||
|
if (showsAllRows || !isLongTable()) {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
Set<NoisePickSpread.Row> largest = rows.stream()
|
||||||
|
.sorted(Comparator.comparingDouble(NoisePickSpread.Row::actualShare).reversed())
|
||||||
|
.limit(SHORT_TABLE_ROWS)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
return rows.stream().filter(largest::contains).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void listenForShowAllToggle() {
|
||||||
|
MouseAdapter toggle = new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent event) {
|
||||||
|
if (showAllToggleBounds == null || !showAllToggleBounds.contains(event.getPoint())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showsAllRows = !showsAllRows;
|
||||||
|
onShowAllRowsChanged.accept(showsAllRows);
|
||||||
|
revalidate();
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseMoved(MouseEvent event) {
|
||||||
|
boolean overToggle = showAllToggleBounds != null && showAllToggleBounds.contains(event.getPoint());
|
||||||
|
setCursor(overToggle ? Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) : null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addMouseListener(toggle);
|
||||||
|
addMouseMotionListener(toggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawSingleOption(Graphics2D canvas, int x, int top, int width) {
|
||||||
|
drawSectionTitle(canvas, spread.title(), x, top + sectionMetrics().getAscent());
|
||||||
|
int rowTop = top + sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP);
|
||||||
|
NoisePickSpread.Row row = rows.getFirst();
|
||||||
|
String only = "picked everywhere";
|
||||||
|
int nameRight = x + width - textMetrics().stringWidth(only) - StudioTheme.scale(10);
|
||||||
|
int baseline = drawOptionName(canvas, row, x, rowTop, nameRight);
|
||||||
|
canvas.setColor(StudioTheme.MUTED_TEXT);
|
||||||
|
drawRightAligned(canvas, only, x + width, baseline);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawSpread(Graphics2D canvas, int x, int top, int width) {
|
||||||
|
drawSectionTitle(canvas, spread.title(), x, top + sectionMetrics().getAscent());
|
||||||
|
int histogramTop = top + sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP);
|
||||||
|
int histogramHeight = StudioTheme.scale(HISTOGRAM_HEIGHT);
|
||||||
|
drawHistogram(canvas, x, histogramTop, width, histogramHeight);
|
||||||
|
int bandTop = histogramTop + histogramHeight + StudioTheme.scale(2);
|
||||||
|
drawBands(canvas, x, width, histogramTop, bandTop);
|
||||||
|
|
||||||
|
canvas.setFont(StudioTheme.smallFont());
|
||||||
|
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics();
|
||||||
|
int labelY = bandTop + StudioTheme.scale(BAND_HEIGHT) + StudioTheme.scale(3) + metrics.getAscent();
|
||||||
|
canvas.drawString("0", x, labelY);
|
||||||
|
canvas.drawString("0.5", x + (width - metrics.stringWidth("0.5")) / 2, labelY);
|
||||||
|
canvas.drawString("1", x + width - metrics.stringWidth("1"), labelY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawHistogram(Graphics2D canvas, int x, int top, int width, int height) {
|
||||||
|
long[] histogram = spread.histogram();
|
||||||
|
long peak = 1L;
|
||||||
|
for (long count : histogram) {
|
||||||
|
peak = Math.max(peak, count);
|
||||||
|
}
|
||||||
|
canvas.setColor(HISTOGRAM_BAR);
|
||||||
|
for (int bin = 0; bin < histogram.length; bin++) {
|
||||||
|
int barLeft = x + (bin * width) / histogram.length;
|
||||||
|
int barRight = x + ((bin + 1) * width) / histogram.length;
|
||||||
|
int barHeight = (int) Math.round(height * (histogram[bin] / (double) peak));
|
||||||
|
canvas.fillRect(barLeft, top + height - barHeight, Math.max(1, barRight - barLeft - 1), barHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawBands(Graphics2D canvas, int x, int width, int histogramTop, int bandTop) {
|
||||||
|
int bandHeight = StudioTheme.scale(BAND_HEIGHT);
|
||||||
|
for (NoisePickSpread.Band band : spread.bands()) {
|
||||||
|
int bandLeft = x + (int) Math.round(band.fromNoise() * width);
|
||||||
|
int bandRight = x + (int) Math.round(band.toNoise() * width);
|
||||||
|
canvas.setColor(new Color(band.option().color()));
|
||||||
|
canvas.fillRect(bandLeft, bandTop, Math.max(1, bandRight - bandLeft), bandHeight);
|
||||||
|
if (band.toNoise() < 1D) {
|
||||||
|
canvas.setColor(BAND_DIVIDER);
|
||||||
|
canvas.drawLine(bandRight, histogramTop, bandRight, bandTop + bandHeight - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawTable(Graphics2D canvas, int x, int top, int width) {
|
||||||
|
int columnGap = StudioTheme.scale(10);
|
||||||
|
int actualRight = x + width;
|
||||||
|
int weightRight = actualRight - textMetrics().stringWidth(WIDEST_SHARE) - columnGap;
|
||||||
|
int nameRight = weightRight - textMetrics().stringWidth(WIDEST_SHARE) - columnGap;
|
||||||
|
int titleBaseline = top + sectionMetrics().getAscent();
|
||||||
|
drawSectionTitle(canvas, optionNoun, x, titleBaseline);
|
||||||
|
canvas.setFont(StudioTheme.sectionFont());
|
||||||
|
drawRightAligned(canvas, "WEIGHT", weightRight, titleBaseline);
|
||||||
|
drawRightAligned(canvas, "IN VIEW", actualRight, titleBaseline);
|
||||||
|
|
||||||
|
int rowHeight = rowHeight();
|
||||||
|
int rowTop = top + sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP);
|
||||||
|
for (NoisePickSpread.Row row : shownRows()) {
|
||||||
|
int baseline = drawOptionName(canvas, row, x, rowTop, nameRight);
|
||||||
|
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
||||||
|
drawRightAligned(canvas, percent(row.weightShare()), weightRight, baseline);
|
||||||
|
boolean farFromWeight = row.isFarBelowWeight() || row.isFarAboveWeight();
|
||||||
|
canvas.setColor(farFromWeight ? StudioTheme.WARNING : StudioTheme.TEXT);
|
||||||
|
drawRightAligned(canvas, actualText(row), actualRight, baseline);
|
||||||
|
rowTop += rowHeight;
|
||||||
|
}
|
||||||
|
showAllToggleBounds = isLongTable() ? drawShowAllToggle(canvas, x, rowTop) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the row's text baseline.
|
||||||
|
private int drawOptionName(Graphics2D canvas, NoisePickSpread.Row row, int x, int rowTop, int nameRight) {
|
||||||
|
FontMetrics text = textMetrics();
|
||||||
|
int rowHeight = rowHeight();
|
||||||
|
int swatchSize = text.getAscent() - text.getDescent();
|
||||||
|
int baseline = rowTop + (rowHeight + text.getAscent() - text.getDescent()) / 2;
|
||||||
|
int arc = StudioTheme.scale(3);
|
||||||
|
canvas.setColor(new Color(row.option().color()));
|
||||||
|
canvas.fillRoundRect(x, rowTop + (rowHeight - swatchSize) / 2, swatchSize, swatchSize, arc, arc);
|
||||||
|
canvas.setFont(StudioTheme.font());
|
||||||
|
canvas.setColor(StudioTheme.TEXT);
|
||||||
|
int nameLeft = x + swatchSize + StudioTheme.scale(7);
|
||||||
|
canvas.drawString(fit(canvas, distinctName(row, rows), nameRight - nameLeft), nameLeft, baseline);
|
||||||
|
return baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Rectangle drawShowAllToggle(Graphics2D canvas, int x, int rowTop) {
|
||||||
|
String text = showsAllRows
|
||||||
|
? "Show fewer"
|
||||||
|
: "Show all " + rows.size() + " " + optionNoun.toLowerCase(Locale.ROOT);
|
||||||
|
FontMetrics metrics = textMetrics();
|
||||||
|
int baseline = rowTop + (rowHeight() + metrics.getAscent() - metrics.getDescent()) / 2;
|
||||||
|
canvas.setFont(StudioTheme.font());
|
||||||
|
canvas.setColor(StudioTheme.LINK);
|
||||||
|
canvas.drawString(text, x, baseline);
|
||||||
|
return new Rectangle(x, rowTop, metrics.stringWidth(text), rowHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int rowHeight() {
|
||||||
|
return textMetrics().getHeight() + StudioTheme.scale(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FontMetrics textMetrics() {
|
||||||
|
return getFontMetrics(StudioTheme.font());
|
||||||
|
}
|
||||||
|
|
||||||
|
private FontMetrics smallMetrics() {
|
||||||
|
return getFontMetrics(StudioTheme.smallFont());
|
||||||
|
}
|
||||||
|
|
||||||
|
private FontMetrics sectionMetrics() {
|
||||||
|
return getFontMetrics(StudioTheme.sectionFont());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawSectionTitle(Graphics2D canvas, String title, int x, int y) {
|
||||||
|
canvas.setFont(StudioTheme.sectionFont());
|
||||||
|
canvas.setColor(StudioTheme.MUTED_TEXT);
|
||||||
|
canvas.drawString(title.toUpperCase(Locale.ROOT), x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawRightAligned(Graphics2D canvas, String text, int right, int y) {
|
||||||
|
canvas.drawString(text, right - canvas.getFontMetrics().stringWidth(text), y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two biomes in different folders can share a name, so their keys tell them apart.
|
||||||
|
private static String distinctName(NoisePickSpread.Row row, List<NoisePickSpread.Row> rows) {
|
||||||
|
String name = row.option().name();
|
||||||
|
boolean shared = rows.stream().anyMatch(other -> other != row && other.option().name().equals(name));
|
||||||
|
return shared ? name + " (" + row.option().key() + ")" : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String actualText(NoisePickSpread.Row row) {
|
||||||
|
if (row.isFarBelowWeight()) {
|
||||||
|
return "▼ " + percent(row.actualShare());
|
||||||
|
}
|
||||||
|
if (row.isFarAboveWeight()) {
|
||||||
|
return "▲ " + percent(row.actualShare());
|
||||||
|
}
|
||||||
|
return percent(row.actualShare());
|
||||||
|
}
|
||||||
|
|
||||||
|
static String percent(double share) {
|
||||||
|
if (share > 0D && share < 0.001D) {
|
||||||
|
return "<0.1%";
|
||||||
|
}
|
||||||
|
return String.format(Locale.ROOT, "%.1f%%", share * 100D);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fit(Graphics2D canvas, String text, int maxWidth) {
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics();
|
||||||
|
if (metrics.stringWidth(text) <= maxWidth) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
String shortened = text;
|
||||||
|
while (shortened.length() > 1 && metrics.stringWidth(shortened + "…") > maxWidth) {
|
||||||
|
shortened = shortened.substring(0, shortened.length() - 1);
|
||||||
|
}
|
||||||
|
return shortened + "…";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How a noise picks one of several regions or biomes, measured against the noise values in view: rarity splits the
|
||||||
|
* noise from 0 to 1 into one band per option, and each spot gets the option whose band its noise value falls in.
|
||||||
|
*/
|
||||||
|
final class NoisePickSpread {
|
||||||
|
static final int HISTOGRAM_BINS = 50;
|
||||||
|
|
||||||
|
private final String title;
|
||||||
|
private final List<Band> bands;
|
||||||
|
private final long[] histogram = new long[HISTOGRAM_BINS];
|
||||||
|
private final long[] samplesByBand;
|
||||||
|
private long sampleCount;
|
||||||
|
private long belowZeroCount;
|
||||||
|
private long aboveOneCount;
|
||||||
|
|
||||||
|
private NoisePickSpread(String title, List<Band> bands) {
|
||||||
|
this.title = title;
|
||||||
|
this.bands = List.copyOf(bands);
|
||||||
|
this.samplesByBand = new long[bands.size()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param legacyRarity whether the dimension splits the noise the legacy way
|
||||||
|
*/
|
||||||
|
static NoisePickSpread of(String title, List<Option> options, boolean legacyRarity) {
|
||||||
|
return new NoisePickSpread(title, splitNoise(options, legacyRarity));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The band of noise each option gets, in the order the noise meets them.
|
||||||
|
*/
|
||||||
|
static List<Band> splitNoise(List<Option> options, boolean legacyRarity) {
|
||||||
|
return legacyRarity ? legacyBands(options) : bands(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(double noise) {
|
||||||
|
if (bands.isEmpty() || !Double.isFinite(noise)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sampleCount++;
|
||||||
|
if (noise < 0D) {
|
||||||
|
belowZeroCount++;
|
||||||
|
} else if (noise > 1D) {
|
||||||
|
aboveOneCount++;
|
||||||
|
}
|
||||||
|
histogram[Math.clamp((int) (noise * HISTOGRAM_BINS), 0, HISTOGRAM_BINS - 1)]++;
|
||||||
|
samplesByBand[bandIndexOf(noise)]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
String title() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Band> bands() {
|
||||||
|
return bands;
|
||||||
|
}
|
||||||
|
|
||||||
|
long[] histogram() {
|
||||||
|
return histogram;
|
||||||
|
}
|
||||||
|
|
||||||
|
long sampleCount() {
|
||||||
|
return sampleCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
double belowZeroShare() {
|
||||||
|
return share(belowZeroCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
double aboveOneShare() {
|
||||||
|
return share(aboveOneCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every option once, with its bands' widths and samples added up, in the order the noise meets them.
|
||||||
|
*/
|
||||||
|
List<Row> rows() {
|
||||||
|
Map<String, Row> rows = new LinkedHashMap<>();
|
||||||
|
for (int index = 0; index < bands.size(); index++) {
|
||||||
|
Band band = bands.get(index);
|
||||||
|
Row row = new Row(band.option(), band.width(), share(samplesByBand[index]));
|
||||||
|
rows.merge(band.option().key(), row, Row::plus);
|
||||||
|
}
|
||||||
|
return new ArrayList<>(rows.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
private double share(long count) {
|
||||||
|
return sampleCount == 0 ? 0D : count / (double) sampleCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The engine gives values below 0 the first option and values above 1 the last, the same as these bands.
|
||||||
|
private int bandIndexOf(double noise) {
|
||||||
|
for (int index = 0; index < bands.size(); index++) {
|
||||||
|
if (noise <= bands.get(index).toNoise()) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bands.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors IRare.pick: each option gets a share of the noise in proportion to one over its rarity.
|
||||||
|
private static List<Band> bands(List<Option> options) {
|
||||||
|
double total = 0D;
|
||||||
|
for (Option option : options) {
|
||||||
|
total += 1D / option.rarity();
|
||||||
|
}
|
||||||
|
List<Band> bands = new ArrayList<>();
|
||||||
|
double from = 0D;
|
||||||
|
for (Option option : options) {
|
||||||
|
double to = options.size() == 1 ? 1D : from + (1D / option.rarity()) / total;
|
||||||
|
bands.add(new Band(option, from, to));
|
||||||
|
from = to;
|
||||||
|
}
|
||||||
|
return bands;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors IRare.pickLegacy: each option gets a share in proportion to the total rarity minus its own.
|
||||||
|
private static List<Band> legacyBands(List<Option> options) {
|
||||||
|
if (options.size() <= 1) {
|
||||||
|
return bands(options);
|
||||||
|
}
|
||||||
|
double totalRarity = 0D;
|
||||||
|
for (Option option : options) {
|
||||||
|
totalRarity += option.rarity();
|
||||||
|
}
|
||||||
|
double whole = totalRarity * (options.size() - 1);
|
||||||
|
List<Band> bands = new ArrayList<>();
|
||||||
|
double from = 0D;
|
||||||
|
double buffer = 0D;
|
||||||
|
for (Option option : options) {
|
||||||
|
buffer += totalRarity - option.rarity();
|
||||||
|
double to = buffer / whole;
|
||||||
|
bands.add(new Band(option, from, to));
|
||||||
|
from = to;
|
||||||
|
}
|
||||||
|
return bands;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A region or biome the noise can pick.
|
||||||
|
*/
|
||||||
|
record Option(String key, String name, int color, int rarity) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The noise values from {@code fromNoise} to {@code toNoise} that pick the option.
|
||||||
|
*/
|
||||||
|
record Band(Option option, double fromNoise, double toNoise) {
|
||||||
|
double width() {
|
||||||
|
return toNoise - fromNoise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param weightShare how much of the noise from 0 to 1 picks the option
|
||||||
|
* @param actualShare how much of the noise in view picks the option
|
||||||
|
*/
|
||||||
|
record Row(Option option, double weightShare, double actualShare) {
|
||||||
|
private static final double FAR_FROM_WEIGHT_FACTOR = 2D;
|
||||||
|
|
||||||
|
Row plus(Row other) {
|
||||||
|
return new Row(option, weightShare + other.weightShare, actualShare + other.actualShare);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isFarBelowWeight() {
|
||||||
|
return actualShare * FAR_FROM_WEIGHT_FACTOR < weightShare;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isFarAboveWeight() {
|
||||||
|
return actualShare > weightShare * FAR_FROM_WEIGHT_FACTOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.util.function.NoiseProvider;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
final class NoiseRenderCoordinator implements AutoCloseable {
|
||||||
|
private static final int BANDS_PER_WORKER = 4;
|
||||||
|
|
||||||
|
private final int workerCount;
|
||||||
|
private final Listener listener;
|
||||||
|
private final ThreadPoolExecutor workers;
|
||||||
|
private final AtomicReference<RenderGeneration> latestRequested = new AtomicReference<>();
|
||||||
|
private final AtomicLong latestRevision = new AtomicLong();
|
||||||
|
private final AtomicBoolean closed = new AtomicBoolean();
|
||||||
|
|
||||||
|
NoiseRenderCoordinator(Listener listener) {
|
||||||
|
this(Math.min(4, Math.max(1, Runtime.getRuntime().availableProcessors() / 2)), listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
NoiseRenderCoordinator(int workerCount, Listener listener) {
|
||||||
|
this.workerCount = Math.max(1, workerCount);
|
||||||
|
this.listener = Objects.requireNonNull(listener, "listener");
|
||||||
|
AtomicInteger threadIds = new AtomicInteger();
|
||||||
|
ThreadFactory workerThreadFactory = runnable -> {
|
||||||
|
Thread thread = new Thread(runnable, "Iris Noise Renderer " + threadIds.incrementAndGet());
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.setPriority(Thread.MIN_PRIORITY);
|
||||||
|
return thread;
|
||||||
|
};
|
||||||
|
workers = new ThreadPoolExecutor(this.workerCount, this.workerCount, 30L, TimeUnit.SECONDS,
|
||||||
|
new LinkedBlockingQueue<>(), workerThreadFactory);
|
||||||
|
workers.allowCoreThreadTimeOut(true);
|
||||||
|
GuiHost.interruptOnPluginShutdown(workers);
|
||||||
|
}
|
||||||
|
|
||||||
|
void request(Request request) {
|
||||||
|
Objects.requireNonNull(request, "request");
|
||||||
|
if (closed.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
latestRevision.accumulateAndGet(request.revision(), Math::max);
|
||||||
|
RenderGeneration generation = new RenderGeneration(request, Math.min(workerCount * BANDS_PER_WORKER, outputHeight(request)));
|
||||||
|
latestRequested.set(generation);
|
||||||
|
listener.onRenderStarted(request);
|
||||||
|
for (int band = 0; band < generation.bandCount(); band++) {
|
||||||
|
int bandIndex = band;
|
||||||
|
try {
|
||||||
|
workers.execute(() -> renderBand(generation, bandIndex));
|
||||||
|
} catch (RejectedExecutionException rejected) {
|
||||||
|
generation.fail(rejected);
|
||||||
|
finishBand(generation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancel(long revision) {
|
||||||
|
latestRevision.accumulateAndGet(revision, Math::max);
|
||||||
|
latestRequested.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isClosed() {
|
||||||
|
return closed.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sampleStepForBudget(int width, int height, long sampleBudget) {
|
||||||
|
if (width < 1 || height < 1 || sampleBudget < 1L) {
|
||||||
|
throw new IllegalArgumentException("Invalid noise sample budget");
|
||||||
|
}
|
||||||
|
long totalSamples = (long) width * height;
|
||||||
|
int sampleStep = Math.max(1, (int) Math.ceil(Math.sqrt(totalSamples / (double) sampleBudget)));
|
||||||
|
while (sampleCount(width, height, sampleStep) > sampleBudget) {
|
||||||
|
sampleStep++;
|
||||||
|
}
|
||||||
|
return sampleStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
static long sampleCount(int width, int height, int sampleStep) {
|
||||||
|
if (width < 1 || height < 1 || sampleStep < 1) {
|
||||||
|
throw new IllegalArgumentException("Invalid noise sample dimensions");
|
||||||
|
}
|
||||||
|
long outputWidth = ((long) width + sampleStep - 1L) / sampleStep;
|
||||||
|
long outputHeight = ((long) height + sampleStep - 1L) / sampleStep;
|
||||||
|
return outputWidth * outputHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int nextRefinementStep(int width, int height, int currentStep, long sampleBudget) {
|
||||||
|
if (currentStep <= 1) {
|
||||||
|
throw new IllegalArgumentException("Noise refinement requires a coarse input");
|
||||||
|
}
|
||||||
|
return Math.min(currentStep, sampleStepForBudget(width, height, sampleBudget));
|
||||||
|
}
|
||||||
|
|
||||||
|
static long timeBoundSampleBudget(long completedSamples, double milliseconds, double targetMilliseconds,
|
||||||
|
long minimumBudget, long maximumBudget) {
|
||||||
|
if (completedSamples < 1L
|
||||||
|
|| !Double.isFinite(milliseconds)
|
||||||
|
|| milliseconds <= 0D
|
||||||
|
|| !Double.isFinite(targetMilliseconds)
|
||||||
|
|| targetMilliseconds <= 0D
|
||||||
|
|| minimumBudget < 1L
|
||||||
|
|| maximumBudget < minimumBudget) {
|
||||||
|
throw new IllegalArgumentException("Invalid timed noise sample budget");
|
||||||
|
}
|
||||||
|
double projectedSamples = Math.ceil((completedSamples / milliseconds) * targetMilliseconds);
|
||||||
|
long timeBoundBudget = (long) Math.min(maximumBudget, projectedSamples);
|
||||||
|
return Math.max(minimumBudget, timeBoundBudget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
if (!closed.compareAndSet(false, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
latestRevision.incrementAndGet();
|
||||||
|
latestRequested.set(null);
|
||||||
|
workers.shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bands of a frame that was replaced are skipped, so a new frame only waits for the rows already being drawn.
|
||||||
|
private void renderBand(RenderGeneration generation, int bandIndex) {
|
||||||
|
try {
|
||||||
|
if (isCurrent(generation)) {
|
||||||
|
renderRows(generation, bandIndex);
|
||||||
|
}
|
||||||
|
} catch (Throwable error) {
|
||||||
|
generation.fail(error);
|
||||||
|
} finally {
|
||||||
|
finishBand(generation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishBand(RenderGeneration generation) {
|
||||||
|
if (!generation.completeBand() || !isCurrent(generation)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Throwable failure = generation.failure();
|
||||||
|
if (failure != null) {
|
||||||
|
listener.onRenderFailed(generation.request(), failure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
listener.onRenderCompleted(generation.result());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderRows(RenderGeneration generation, int bandIndex) {
|
||||||
|
Request request = generation.request();
|
||||||
|
int outputWidth = generation.outputWidth();
|
||||||
|
int outputHeight = generation.outputHeight();
|
||||||
|
int[] pixels = generation.pixels();
|
||||||
|
BandStats stats = generation.bandStats(bandIndex);
|
||||||
|
int fromY = (outputHeight * bandIndex) / generation.bandCount();
|
||||||
|
int toY = (outputHeight * (bandIndex + 1)) / generation.bandCount();
|
||||||
|
double sampleOffset = request.sampleStep() * 0.5D;
|
||||||
|
double startWorldX = request.viewport().worldX(sampleOffset, request.width());
|
||||||
|
double worldStep = request.viewport().blocksPerPixel() * request.sampleStep();
|
||||||
|
NoiseColorScale colorScale = request.colorScale();
|
||||||
|
double paletteMinimum = colorScale.minimum();
|
||||||
|
double paletteMaximum = colorScale.maximum();
|
||||||
|
for (int y = fromY; y < toY; y++) {
|
||||||
|
double screenY = (y * (double) request.sampleStep()) + sampleOffset;
|
||||||
|
double worldZ = request.viewport().worldZ(screenY, request.height());
|
||||||
|
double worldX = startWorldX;
|
||||||
|
int pixelIndex = y * outputWidth;
|
||||||
|
boolean centerRow = y == outputHeight / 2;
|
||||||
|
for (int x = 0; x < outputWidth; x++) {
|
||||||
|
if ((x & 31) == 0 && (!isCurrent(generation) || Thread.currentThread().isInterrupted())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double value = request.sampler().noise(worldX, worldZ);
|
||||||
|
boolean center = centerRow && x == outputWidth / 2;
|
||||||
|
if (Double.isFinite(value)) {
|
||||||
|
pixels[pixelIndex++] = colorScale.colorAt(value, worldX, worldZ);
|
||||||
|
stats.acceptFinite(value, center, paletteMinimum, paletteMaximum);
|
||||||
|
} else {
|
||||||
|
pixels[pixelIndex++] = colorScale.colorInvalidAt(worldX, worldZ);
|
||||||
|
stats.acceptInvalid(value, center);
|
||||||
|
}
|
||||||
|
worldX += worldStep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCurrent(RenderGeneration generation) {
|
||||||
|
return !closed.get()
|
||||||
|
&& latestRequested.get() == generation
|
||||||
|
&& generation.request().revision() >= latestRevision.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int outputWidth(Request request) {
|
||||||
|
return Math.max(1, (request.width() + request.sampleStep() - 1) / request.sampleStep());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int outputHeight(Request request) {
|
||||||
|
return Math.max(1, (request.height() + request.sampleStep() - 1) / request.sampleStep());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RenderStats combine(BandStats[] bands) {
|
||||||
|
long samples = 0L;
|
||||||
|
long underflow = 0L;
|
||||||
|
long overflow = 0L;
|
||||||
|
long invalid = 0L;
|
||||||
|
double minimum = Double.POSITIVE_INFINITY;
|
||||||
|
double maximum = Double.NEGATIVE_INFINITY;
|
||||||
|
double centerValue = Double.NaN;
|
||||||
|
for (BandStats band : bands) {
|
||||||
|
samples += band.samples;
|
||||||
|
underflow += band.underflow;
|
||||||
|
overflow += band.overflow;
|
||||||
|
invalid += band.invalid;
|
||||||
|
minimum = Math.min(minimum, band.minimum);
|
||||||
|
maximum = Math.max(maximum, band.maximum);
|
||||||
|
if (!Double.isNaN(band.centerValue)) {
|
||||||
|
centerValue = band.centerValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (minimum == Double.POSITIVE_INFINITY) {
|
||||||
|
minimum = Double.NaN;
|
||||||
|
maximum = Double.NaN;
|
||||||
|
}
|
||||||
|
return new RenderStats(samples, minimum, maximum, centerValue, underflow, overflow, invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
void onRenderStarted(Request request);
|
||||||
|
|
||||||
|
void onRenderCompleted(Result result);
|
||||||
|
|
||||||
|
void onRenderFailed(Request request, Throwable error);
|
||||||
|
}
|
||||||
|
|
||||||
|
record Request(long revision, NoiseProvider sampler, NoiseViewport viewport, NoiseColorScale colorScale,
|
||||||
|
int width, int height, int sampleStep) {
|
||||||
|
Request {
|
||||||
|
Objects.requireNonNull(sampler, "sampler");
|
||||||
|
Objects.requireNonNull(viewport, "viewport");
|
||||||
|
Objects.requireNonNull(colorScale, "colorScale");
|
||||||
|
if (revision < 0L || width < 1 || height < 1 || sampleStep < 1) {
|
||||||
|
throw new IllegalArgumentException("Invalid noise render request");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record Result(Request request, BufferedImage image, double milliseconds, long samples, double minimum,
|
||||||
|
double maximum, double centerValue, long underflow, long overflow, long invalid) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RenderGeneration {
|
||||||
|
private final Request request;
|
||||||
|
private final long startedNanos = System.nanoTime();
|
||||||
|
private final int outputWidth;
|
||||||
|
private final int outputHeight;
|
||||||
|
private final int[] pixels;
|
||||||
|
private final BandStats[] bandStats;
|
||||||
|
private final AtomicInteger remainingBands;
|
||||||
|
private final AtomicReference<Throwable> failure = new AtomicReference<>();
|
||||||
|
|
||||||
|
private RenderGeneration(Request request, int bandCount) {
|
||||||
|
this.request = request;
|
||||||
|
this.outputWidth = NoiseRenderCoordinator.outputWidth(request);
|
||||||
|
this.outputHeight = NoiseRenderCoordinator.outputHeight(request);
|
||||||
|
this.pixels = new int[outputWidth * outputHeight];
|
||||||
|
this.bandStats = new BandStats[Math.max(1, bandCount)];
|
||||||
|
for (int band = 0; band < bandStats.length; band++) {
|
||||||
|
bandStats[band] = new BandStats();
|
||||||
|
}
|
||||||
|
this.remainingBands = new AtomicInteger(bandStats.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Request request() {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int bandCount() {
|
||||||
|
return bandStats.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int outputWidth() {
|
||||||
|
return outputWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int outputHeight() {
|
||||||
|
return outputHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int[] pixels() {
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BandStats bandStats(int band) {
|
||||||
|
return bandStats[band];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fail(Throwable error) {
|
||||||
|
failure.compareAndSet(null, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Throwable failure() {
|
||||||
|
return failure.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// True for the band that finishes last, which is the one that publishes the frame.
|
||||||
|
private boolean completeBand() {
|
||||||
|
return remainingBands.decrementAndGet() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result result() {
|
||||||
|
BufferedImage image = GuiHost.uncachedImage(pixels, outputWidth, outputHeight);
|
||||||
|
RenderStats combined = combine(bandStats);
|
||||||
|
double milliseconds = (System.nanoTime() - startedNanos) / 1_000_000D;
|
||||||
|
return new Result(request, image, milliseconds, combined.samples, combined.minimum, combined.maximum,
|
||||||
|
combined.centerValue, combined.underflow, combined.overflow, combined.invalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BandStats {
|
||||||
|
private long samples;
|
||||||
|
private long underflow;
|
||||||
|
private long overflow;
|
||||||
|
private long invalid;
|
||||||
|
private double minimum = Double.POSITIVE_INFINITY;
|
||||||
|
private double maximum = Double.NEGATIVE_INFINITY;
|
||||||
|
private double centerValue = Double.NaN;
|
||||||
|
|
||||||
|
private void acceptFinite(double value, boolean center, double paletteMinimum, double paletteMaximum) {
|
||||||
|
samples++;
|
||||||
|
if (center) {
|
||||||
|
centerValue = value;
|
||||||
|
}
|
||||||
|
minimum = Math.min(minimum, value);
|
||||||
|
maximum = Math.max(maximum, value);
|
||||||
|
if (value < paletteMinimum) {
|
||||||
|
underflow++;
|
||||||
|
} else if (value > paletteMaximum) {
|
||||||
|
overflow++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acceptInvalid(double value, boolean center) {
|
||||||
|
samples++;
|
||||||
|
invalid++;
|
||||||
|
if (center) {
|
||||||
|
centerValue = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RenderStats(long samples, double minimum, double maximum, double centerValue,
|
||||||
|
long underflow, long overflow, long invalid) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
record NoiseViewport(double centerX, double centerZ, double blocksPerPixel) {
|
||||||
|
static final double MIN_BLOCKS_PER_PIXEL = 0.0001D;
|
||||||
|
static final double MAX_BLOCKS_PER_PIXEL = 1_000_000D;
|
||||||
|
|
||||||
|
NoiseViewport {
|
||||||
|
if (!Double.isFinite(centerX) || !Double.isFinite(centerZ)) {
|
||||||
|
throw new IllegalArgumentException("Viewport center must be finite");
|
||||||
|
}
|
||||||
|
if (!Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
|
||||||
|
throw new IllegalArgumentException("Viewport scale must be finite and positive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double worldX(double screenX, int width) {
|
||||||
|
return centerX + ((screenX - (width / 2D)) * blocksPerPixel);
|
||||||
|
}
|
||||||
|
|
||||||
|
double worldZ(double screenZ, int height) {
|
||||||
|
return centerZ + ((screenZ - (height / 2D)) * blocksPerPixel);
|
||||||
|
}
|
||||||
|
|
||||||
|
double screenX(double worldX, int width) {
|
||||||
|
return ((worldX - centerX) / blocksPerPixel) + (width / 2D);
|
||||||
|
}
|
||||||
|
|
||||||
|
double screenZ(double worldZ, int height) {
|
||||||
|
return ((worldZ - centerZ) / blocksPerPixel) + (height / 2D);
|
||||||
|
}
|
||||||
|
|
||||||
|
NoiseViewport panPixels(double deltaX, double deltaZ) {
|
||||||
|
return new NoiseViewport(
|
||||||
|
centerX - (deltaX * blocksPerPixel),
|
||||||
|
centerZ - (deltaZ * blocksPerPixel),
|
||||||
|
blocksPerPixel
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NoiseViewport zoomAt(double screenX, double screenZ, int width, int height, double factor) {
|
||||||
|
if (!Double.isFinite(factor) || factor <= 0D) {
|
||||||
|
throw new IllegalArgumentException("Zoom factor must be finite and positive");
|
||||||
|
}
|
||||||
|
double anchorX = worldX(screenX, width);
|
||||||
|
double anchorZ = worldZ(screenZ, height);
|
||||||
|
double nextScale = Math.max(MIN_BLOCKS_PER_PIXEL,
|
||||||
|
Math.min(MAX_BLOCKS_PER_PIXEL, blocksPerPixel * factor));
|
||||||
|
double nextCenterX = anchorX - ((screenX - (width / 2D)) * nextScale);
|
||||||
|
double nextCenterZ = anchorZ - ((screenZ - (height / 2D)) * nextScale);
|
||||||
|
return new NoiseViewport(nextCenterX, nextCenterZ, nextScale);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JTextField;
|
||||||
|
import javax.swing.JTree;
|
||||||
|
import javax.swing.ToolTipManager;
|
||||||
|
import javax.swing.event.DocumentEvent;
|
||||||
|
import javax.swing.event.DocumentListener;
|
||||||
|
import javax.swing.tree.DefaultMutableTreeNode;
|
||||||
|
import javax.swing.tree.DefaultTreeCellRenderer;
|
||||||
|
import javax.swing.tree.DefaultTreeModel;
|
||||||
|
import javax.swing.tree.TreePath;
|
||||||
|
import javax.swing.tree.TreeSelectionModel;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every object in the pack as a folder tree, with a search that matches every word typed anywhere in an object's path.
|
||||||
|
*/
|
||||||
|
final class ObjectBrowser extends JPanel {
|
||||||
|
private static final long serialVersionUID = 6795434561028735214L;
|
||||||
|
private static final int ROW_HEIGHT = 24;
|
||||||
|
|
||||||
|
private final Consumer<String> onObjectChosen;
|
||||||
|
private final JTextField searchField = new JTextField();
|
||||||
|
private final DefaultTreeModel treeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
|
||||||
|
private final JTree tree = new JTree(treeModel);
|
||||||
|
private final JLabel countLabel = new JLabel();
|
||||||
|
private final String noun;
|
||||||
|
private List<String> objectKeys = List.of();
|
||||||
|
private String chosenKey;
|
||||||
|
private boolean updatingSelection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param noun what one entry is called in the browser's texts, such as "object"
|
||||||
|
*/
|
||||||
|
ObjectBrowser(String noun, Consumer<String> onObjectChosen) {
|
||||||
|
super(new BorderLayout());
|
||||||
|
this.noun = noun;
|
||||||
|
this.onObjectChosen = onObjectChosen;
|
||||||
|
countLabel.setText("Loading " + noun + "s...");
|
||||||
|
StudioToolbar.makeSearchField(searchField, "Search " + noun + "s...");
|
||||||
|
searchField.getDocument().addDocumentListener(new DocumentListener() {
|
||||||
|
@Override
|
||||||
|
public void insertUpdate(DocumentEvent event) {
|
||||||
|
rebuildTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeUpdate(DocumentEvent event) {
|
||||||
|
rebuildTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void changedUpdate(DocumentEvent event) {
|
||||||
|
rebuildTree();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tree.setRootVisible(false);
|
||||||
|
tree.setShowsRootHandles(true);
|
||||||
|
tree.setRowHeight(StudioTheme.scale(ROW_HEIGHT));
|
||||||
|
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
|
||||||
|
tree.setCellRenderer(new EntryRenderer());
|
||||||
|
ToolTipManager.sharedInstance().registerComponent(tree);
|
||||||
|
tree.addTreeSelectionListener(event -> {
|
||||||
|
if (!updatingSelection && nodeAt(event.getPath()) instanceof Entry entry && entry.isObject()) {
|
||||||
|
chosenKey = entry.key();
|
||||||
|
onObjectChosen.accept(entry.key());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
countLabel.setFont(StudioTheme.smallFont());
|
||||||
|
countLabel.setForeground(StudioTheme.SECONDARY_TEXT);
|
||||||
|
add(StudioToolbar.padded(searchField, 8, 8, 8, 8), BorderLayout.NORTH);
|
||||||
|
add(StickyFolderHeaders.around(tree, StudioToolbar.scroll(tree)), BorderLayout.CENTER);
|
||||||
|
add(StudioToolbar.padded(countLabel, 6, 12, 8, 12), BorderLayout.SOUTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showObjects(List<String> keys) {
|
||||||
|
objectKeys = keys.stream().sorted(Comparator.comparing(key -> key.toLowerCase(Locale.ROOT))).toList();
|
||||||
|
countLabel.setText(objectKeys.size() == 1 ? "1 " + noun : objectKeys.size() + " " + noun + "s");
|
||||||
|
rebuildTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showError(String text) {
|
||||||
|
countLabel.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
String firstObject() {
|
||||||
|
return objectKeys.isEmpty() ? null : objectKeys.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rebuildTree() {
|
||||||
|
String[] words = searchField.getText().trim().toLowerCase(Locale.ROOT).split("\\s+");
|
||||||
|
boolean searching = !searchField.getText().isBlank();
|
||||||
|
DefaultMutableTreeNode root = searching ? searchResults(words) : folderTree();
|
||||||
|
treeModel.setRoot(root);
|
||||||
|
if (searching) {
|
||||||
|
expandAll();
|
||||||
|
}
|
||||||
|
selectChosen();
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultMutableTreeNode searchResults(String[] words) {
|
||||||
|
DefaultMutableTreeNode root = new DefaultMutableTreeNode();
|
||||||
|
for (String key : objectKeys) {
|
||||||
|
if (matchesEveryWord(key, words)) {
|
||||||
|
root.add(new DefaultMutableTreeNode(Entry.object(key, key)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root.getChildCount() == 0) {
|
||||||
|
root.add(new DefaultMutableTreeNode(Entry.note("No matching " + noun + "s")));
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean matchesEveryWord(String key, String[] words) {
|
||||||
|
String lowerKey = key.toLowerCase(Locale.ROOT);
|
||||||
|
for (String word : words) {
|
||||||
|
if (!lowerKey.contains(word)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Folders come before the objects beside them, like a file browser.
|
||||||
|
private DefaultMutableTreeNode folderTree() {
|
||||||
|
Folder top = new Folder();
|
||||||
|
for (String key : objectKeys) {
|
||||||
|
Folder folder = top;
|
||||||
|
String[] parts = key.split("/");
|
||||||
|
for (int index = 0; index < parts.length - 1; index++) {
|
||||||
|
folder = folder.subfolders.computeIfAbsent(parts[index], name -> new Folder());
|
||||||
|
}
|
||||||
|
folder.objects.add(key);
|
||||||
|
}
|
||||||
|
DefaultMutableTreeNode root = new DefaultMutableTreeNode();
|
||||||
|
addFolderContents(root, top, "");
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addFolderContents(DefaultMutableTreeNode node, Folder folder, String path) {
|
||||||
|
for (Map.Entry<String, Folder> subfolder : folder.subfolders.entrySet()) {
|
||||||
|
String subfolderPath = path + subfolder.getKey() + "/";
|
||||||
|
DefaultMutableTreeNode child = new DefaultMutableTreeNode(Entry.folder(subfolder.getKey(), subfolderPath));
|
||||||
|
addFolderContents(child, subfolder.getValue(), subfolderPath);
|
||||||
|
node.add(child);
|
||||||
|
}
|
||||||
|
for (String key : folder.objects) {
|
||||||
|
node.add(new DefaultMutableTreeNode(Entry.object(key.substring(key.lastIndexOf('/') + 1), key)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void select(String key) {
|
||||||
|
chosenKey = key;
|
||||||
|
selectChosen();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectChosen() {
|
||||||
|
DefaultMutableTreeNode node = chosenKey == null ? null : findObjectNode((DefaultMutableTreeNode) treeModel.getRoot());
|
||||||
|
updatingSelection = true;
|
||||||
|
try {
|
||||||
|
if (node == null) {
|
||||||
|
tree.clearSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TreePath path = new TreePath(node.getPath());
|
||||||
|
tree.setSelectionPath(path);
|
||||||
|
tree.scrollPathToVisible(path);
|
||||||
|
} finally {
|
||||||
|
updatingSelection = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultMutableTreeNode findObjectNode(DefaultMutableTreeNode node) {
|
||||||
|
if (node.getUserObject() instanceof Entry entry && entry.isObject() && entry.key().equals(chosenKey)) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < node.getChildCount(); index++) {
|
||||||
|
DefaultMutableTreeNode found = findObjectNode((DefaultMutableTreeNode) node.getChildAt(index));
|
||||||
|
if (found != null) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void expandAll() {
|
||||||
|
for (int row = 0; row < tree.getRowCount(); row++) {
|
||||||
|
tree.expandRow(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object nodeAt(TreePath path) {
|
||||||
|
return path == null ? null : ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Folder {
|
||||||
|
private final Map<String, Folder> subfolders = new LinkedHashMap<>();
|
||||||
|
private final List<String> objects = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A row of the tree: an object with its key, a folder with its path, or a note without either.
|
||||||
|
*/
|
||||||
|
private record Entry(String label, String key, String folderPath) {
|
||||||
|
static Entry object(String label, String key) {
|
||||||
|
return new Entry(label, key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Entry folder(String label, String path) {
|
||||||
|
return new Entry(label, null, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Entry note(String text) {
|
||||||
|
return new Entry(text, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isObject() {
|
||||||
|
return key != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class EntryRenderer extends DefaultTreeCellRenderer {
|
||||||
|
private static final long serialVersionUID = -4316652201793418740L;
|
||||||
|
|
||||||
|
private EntryRenderer() {
|
||||||
|
setLeafIcon(null);
|
||||||
|
setOpenIcon(null);
|
||||||
|
setClosedIcon(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded,
|
||||||
|
boolean leaf, int row, boolean focus) {
|
||||||
|
Object userObject = ((DefaultMutableTreeNode) value).getUserObject();
|
||||||
|
super.getTreeCellRendererComponent(tree, userObject == null ? "" : userObject.toString(), selected, expanded, leaf, row, false);
|
||||||
|
boolean isNote = userObject instanceof Entry entry && !entry.isObject() && entry.folderPath() == null;
|
||||||
|
setFont(StudioTheme.font());
|
||||||
|
setForeground(isNote ? StudioTheme.MUTED_TEXT : StudioTheme.TEXT);
|
||||||
|
setToolTipText(userObject instanceof Entry entry ? entry.isObject() ? entry.key() : entry.folderPath() : null);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where the object viewer looks from: turned by yaw around the vertical axis, tilted down by pitch, both in radians.
|
||||||
|
* Zoom 1 fits the object in the view at the starting angle. Pan moves the picture in fractions of the view's size.
|
||||||
|
*/
|
||||||
|
record ObjectCamera(double yaw, double pitch, double zoom, double panX, double panY) {
|
||||||
|
static final ObjectCamera START = new ObjectCamera(0.76D, 0.38D, 1.72D, 0D, 0.025D);
|
||||||
|
private static final double RADIANS_PER_DRAGGED_PIXEL = 0.008D;
|
||||||
|
private static final double MAXIMUM_PITCH = 1.3D;
|
||||||
|
private static final double MINIMUM_ZOOM = 0.4D;
|
||||||
|
private static final double MAXIMUM_ZOOM = 12D;
|
||||||
|
|
||||||
|
ObjectCamera orbitedBy(double draggedX, double draggedY) {
|
||||||
|
double nextPitch = Math.max(-MAXIMUM_PITCH, Math.min(MAXIMUM_PITCH, pitch + draggedY * RADIANS_PER_DRAGGED_PIXEL));
|
||||||
|
return new ObjectCamera(yaw + draggedX * RADIANS_PER_DRAGGED_PIXEL, nextPitch, zoom, panX, panY);
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectCamera zoomedBy(double factor) {
|
||||||
|
return new ObjectCamera(yaw, pitch, Math.max(MINIMUM_ZOOM, Math.min(MAXIMUM_ZOOM, zoom * factor)), panX, panY);
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectCamera pannedBy(double fractionX, double fractionY) {
|
||||||
|
return new ObjectCamera(yaw, pitch, zoom, panX + fractionX, panY + fractionY);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,548 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
|
||||||
|
import com.volmit.iris.core.loader.IrisData;
|
||||||
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
|
import com.volmit.iris.engine.object.IrisObject;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.util.BlockVector;
|
||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import java.awt.CardLayout;
|
||||||
|
import java.awt.EventQueue;
|
||||||
|
import java.awt.event.KeyEvent;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The map window's Objects and Procedural views: everything its source offers in a folder tree, the chosen one in 3D,
|
||||||
|
* and what it is made of. A procedural object's variants can also be compared side by side, baked from any seed.
|
||||||
|
* Inspired by the Procedural Objects workbench.
|
||||||
|
*/
|
||||||
|
final class ObjectExplorer {
|
||||||
|
private static final NamespacedKey TINT_BIOME = NamespacedKey.minecraft("plains");
|
||||||
|
private static final int UNKNOWN_BLOCK_COLOR = 0x8A8A8A;
|
||||||
|
private static final String CAMERA_HINT = "Drag to orbit · Shift-drag to pan · Scroll to zoom · Page Up/Down cuts layers · R resets";
|
||||||
|
private static final String ONE_VARIANT_CARD = "one variant";
|
||||||
|
private static final String ALL_VARIANTS_CARD = "all variants";
|
||||||
|
|
||||||
|
private final Engine startEngine;
|
||||||
|
private final UUID worldId;
|
||||||
|
private final ObjectSource source;
|
||||||
|
private final ObjectBrowser browser;
|
||||||
|
private final ObjectViewport viewport = new ObjectViewport();
|
||||||
|
private final ObjectLayerCut layerCut = new ObjectLayerCut(shownLayers -> showLayerCut());
|
||||||
|
private final ObjectVariantPicker variantPicker = new ObjectVariantPicker(this::showVariant, this::showSeed, this::toggleComparing);
|
||||||
|
private final ObjectVariantGallery gallery = new ObjectVariantGallery(this::openVariant, this::redrawThumbnails);
|
||||||
|
private final CardLayout viewCards = new CardLayout();
|
||||||
|
private final JPanel views = new JPanel(viewCards);
|
||||||
|
private final JComponent viewWithOverlays;
|
||||||
|
private final ObjectOverviewPanel overview = new ObjectOverviewPanel(this::showSeeThrough);
|
||||||
|
private final StudioStatusBar statusBar = new StudioStatusBar();
|
||||||
|
private final ExecutorService loader = Executors.newSingleThreadExecutor(runnable -> {
|
||||||
|
Thread thread = new Thread(runnable, "Iris Object Loader");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.setPriority(Thread.MIN_PRIORITY);
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
private final ExecutorService thumbnailPainter = Executors.newSingleThreadExecutor(runnable -> {
|
||||||
|
Thread thread = new Thread(runnable, "Iris Variant Thumbnails");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.setPriority(Thread.MIN_PRIORITY);
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
private final AtomicLong objectRevision = new AtomicLong();
|
||||||
|
private final AtomicLong variantListRevision = new AtomicLong();
|
||||||
|
private final AtomicLong meshRevision = new AtomicLong();
|
||||||
|
private final Map<String, BlockModel> models = new ConcurrentHashMap<>();
|
||||||
|
private final Runnable hotloadHook = this::reloadPack;
|
||||||
|
private volatile BlockTextures textures;
|
||||||
|
private volatile BiomeTintColors tintColors;
|
||||||
|
private String shownKey;
|
||||||
|
private List<IrisObject> shownVariants = List.of();
|
||||||
|
private int shownVariantIndex;
|
||||||
|
private OptionalLong packSeed = OptionalLong.empty();
|
||||||
|
private OptionalLong shownSeed = OptionalLong.empty();
|
||||||
|
private boolean comparingVariants;
|
||||||
|
private long thumbnailsDrawnForRevision = -1;
|
||||||
|
private LoadedObject shown;
|
||||||
|
private ObjectMesh wholeMesh;
|
||||||
|
private Set<String> seeThroughTypes = Set.of();
|
||||||
|
|
||||||
|
ObjectExplorer(Engine engine, UUID worldId, ObjectSource source) {
|
||||||
|
this.startEngine = engine;
|
||||||
|
this.worldId = worldId;
|
||||||
|
this.source = source;
|
||||||
|
this.browser = new ObjectBrowser(source.noun(), this::showObject);
|
||||||
|
views.add(layerCut.floatingOver(viewport), ONE_VARIANT_CARD);
|
||||||
|
views.add(gallery, ALL_VARIANTS_CARD);
|
||||||
|
this.viewWithOverlays = variantPicker.floatingOver(views);
|
||||||
|
GuiHost.interruptOnPluginShutdown(loader);
|
||||||
|
GuiHost.interruptOnPluginShutdown(thumbnailPainter);
|
||||||
|
GuiHost.registerHotloadHook(hotloadHook);
|
||||||
|
statusBar.showPosition(CAMERA_HINT);
|
||||||
|
viewport.bindKey(KeyEvent.VK_PAGE_UP, "show-layer-above", () -> layerCut.stepBy(1));
|
||||||
|
viewport.bindKey(KeyEvent.VK_PAGE_DOWN, "hide-top-layer", () -> layerCut.stepBy(-1));
|
||||||
|
viewport.bindKey(KeyEvent.VK_OPEN_BRACKET, "previous-variant", () -> variantPicker.stepBy(-1));
|
||||||
|
viewport.bindKey(KeyEvent.VK_CLOSE_BRACKET, "next-variant", () -> variantPicker.stepBy(1));
|
||||||
|
viewport.bindKey(KeyEvent.VK_G, "compare-variants", this::toggleComparing);
|
||||||
|
gallery.bindKey(KeyEvent.VK_OPEN_BRACKET, "previous-variant", () -> variantPicker.stepBy(-1));
|
||||||
|
gallery.bindKey(KeyEvent.VK_CLOSE_BRACKET, "next-variant", () -> variantPicker.stepBy(1));
|
||||||
|
gallery.bindKey(KeyEvent.VK_G, "show-one-variant", this::toggleComparing);
|
||||||
|
gallery.bindKey(KeyEvent.VK_ESCAPE, "close-comparison", this::toggleComparing);
|
||||||
|
}
|
||||||
|
|
||||||
|
JComponent browser() {
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 3D view with the slider that cuts away its top layers, or every variant side by side, under the bar that
|
||||||
|
* steps through variants and seeds.
|
||||||
|
*/
|
||||||
|
JComponent view() {
|
||||||
|
return viewWithOverlays;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCoveredLeftWidth(int width) {
|
||||||
|
viewport.setCoveredLeftWidth(width);
|
||||||
|
gallery.setCoveredLeftWidth(width);
|
||||||
|
variantPicker.setCoveredLeftWidth(width);
|
||||||
|
}
|
||||||
|
|
||||||
|
JComponent overview() {
|
||||||
|
return overview;
|
||||||
|
}
|
||||||
|
|
||||||
|
StudioStatusBar statusBar() {
|
||||||
|
return statusBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetCamera() {
|
||||||
|
viewport.resetCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Textures load first, since the first object would otherwise be drawn in flat colours.
|
||||||
|
void open() {
|
||||||
|
viewport.showLoading("Loading Minecraft textures");
|
||||||
|
loader.execute(() -> {
|
||||||
|
loadTextures();
|
||||||
|
List<String> keys = objectKeys();
|
||||||
|
EventQueue.invokeLater(() -> showCatalog(keys));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() {
|
||||||
|
GuiHost.unregisterHotloadHook(hotloadHook);
|
||||||
|
objectRevision.incrementAndGet();
|
||||||
|
loader.shutdownNow();
|
||||||
|
thumbnailPainter.shutdownNow();
|
||||||
|
viewport.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadTextures() {
|
||||||
|
try {
|
||||||
|
BlockTextures loaded = BlockTextures.shared().join();
|
||||||
|
tintColors = loaded.biomeColors().vanilla(TINT_BIOME);
|
||||||
|
textures = loaded;
|
||||||
|
} catch (RuntimeException unavailable) {
|
||||||
|
EventQueue.invokeLater(() -> statusBar.showError("Minecraft textures are unavailable, objects use plain colours"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> objectKeys() {
|
||||||
|
return source.keys(engine().getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showCatalog(List<String> keys) {
|
||||||
|
statusBar.showActivity("");
|
||||||
|
browser.showObjects(keys);
|
||||||
|
String key = shownKey != null && keys.contains(shownKey) ? shownKey : browser.firstObject();
|
||||||
|
if (key == null) {
|
||||||
|
viewport.showEmptyState(source.emptyState());
|
||||||
|
overview.clear();
|
||||||
|
layerCut.showHeight(1);
|
||||||
|
if (comparingVariants) {
|
||||||
|
toggleComparing();
|
||||||
|
}
|
||||||
|
showVariantList(List.of(), OptionalLong.empty(), OptionalLong.empty(), 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
browser.select(key);
|
||||||
|
showObject(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the first variant of the entry, baked from the pack's seed. The pack reloading the shown entry keeps the
|
||||||
|
* variant and the seed being looked at, unless the pack itself now sets another seed.
|
||||||
|
*/
|
||||||
|
private void showObject(String key) {
|
||||||
|
boolean reloadingShownObject = key.equals(shownKey);
|
||||||
|
OptionalLong previousPackSeed = packSeed;
|
||||||
|
OptionalLong exploredSeed = shownSeed;
|
||||||
|
int variantIndex = reloadingShownObject ? shownVariantIndex : 0;
|
||||||
|
shownKey = key;
|
||||||
|
if (!reloadingShownObject) {
|
||||||
|
variantPicker.forgetTriedSeeds();
|
||||||
|
showVariantList(List.of(), OptionalLong.empty(), OptionalLong.empty(), 0);
|
||||||
|
}
|
||||||
|
long revision = startBaking(key);
|
||||||
|
loader.execute(() -> {
|
||||||
|
if (revision != objectRevision.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
OptionalLong pack = source.seed(key);
|
||||||
|
OptionalLong seed = reloadingShownObject && pack.equals(previousPackSeed) ? exploredSeed : pack;
|
||||||
|
loadVariants(revision, key, pack, seed, variantIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The seed shows at once, so stepping again continues from it even while this one bakes or fails.
|
||||||
|
private void showSeed(long seed) {
|
||||||
|
String key = shownKey;
|
||||||
|
OptionalLong pack = packSeed;
|
||||||
|
int variantIndex = shownVariantIndex;
|
||||||
|
shownSeed = OptionalLong.of(seed);
|
||||||
|
variantPicker.showSeed(shownSeed, pack);
|
||||||
|
long revision = startBaking(key + " with seed " + seed);
|
||||||
|
loader.execute(() -> {
|
||||||
|
if (revision == objectRevision.get()) {
|
||||||
|
loadVariants(revision, key, pack, OptionalLong.of(seed), variantIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// On the loader thread.
|
||||||
|
private void loadVariants(long revision, String key, OptionalLong pack, OptionalLong seed, int preferredIndex) {
|
||||||
|
try {
|
||||||
|
IrisData data = engine().getData();
|
||||||
|
List<IrisObject> variants = seed.isPresent() ? source.variants(data, key, seed.getAsLong()) : source.variants(data, key);
|
||||||
|
if (variants.isEmpty()) {
|
||||||
|
throw new IllegalStateException("Nothing could be loaded for " + key);
|
||||||
|
}
|
||||||
|
int index = preferredIndex < variants.size() ? preferredIndex : 0;
|
||||||
|
LoadedObject loaded = load(variants.get(index));
|
||||||
|
EventQueue.invokeLater(() -> {
|
||||||
|
if (revision != objectRevision.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showVariantList(variants, pack, seed, index);
|
||||||
|
showLoaded(revision, loaded);
|
||||||
|
});
|
||||||
|
} catch (RuntimeException error) {
|
||||||
|
EventQueue.invokeLater(() -> showFailure(revision, key, error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showVariantList(List<IrisObject> variants, OptionalLong pack, OptionalLong seed, int index) {
|
||||||
|
shownVariants = variants;
|
||||||
|
shownVariantIndex = index;
|
||||||
|
packSeed = pack;
|
||||||
|
shownSeed = seed;
|
||||||
|
variantListRevision.incrementAndGet();
|
||||||
|
variantPicker.showVariants(variants.size(), index);
|
||||||
|
variantPicker.showSeed(seed, pack);
|
||||||
|
if (!comparingVariants) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (variants.isEmpty()) {
|
||||||
|
gallery.showLoading();
|
||||||
|
} else if (variants.size() == 1) {
|
||||||
|
toggleComparing();
|
||||||
|
} else {
|
||||||
|
drawThumbnails();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showVariant(int index) {
|
||||||
|
if (index >= shownVariants.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shownVariantIndex = index;
|
||||||
|
gallery.select(index);
|
||||||
|
IrisObject variant = shownVariants.get(index);
|
||||||
|
long revision = startLoading(variant.getLoadKey());
|
||||||
|
loader.execute(() -> {
|
||||||
|
if (revision != objectRevision.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LoadedObject loaded = load(variant);
|
||||||
|
EventQueue.invokeLater(() -> showLoaded(revision, loaded));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openVariant(int index) {
|
||||||
|
variantPicker.showVariants(shownVariants.size(), index);
|
||||||
|
showVariant(index);
|
||||||
|
toggleComparing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleComparing() {
|
||||||
|
if (!comparingVariants && shownVariants.size() < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
comparingVariants = !comparingVariants;
|
||||||
|
variantPicker.showComparing(comparingVariants);
|
||||||
|
viewCards.show(views, comparingVariants ? ALL_VARIANTS_CARD : ONE_VARIANT_CARD);
|
||||||
|
if (comparingVariants) {
|
||||||
|
drawThumbnails();
|
||||||
|
gallery.requestFocusInWindow();
|
||||||
|
} else {
|
||||||
|
viewport.requestFocusInWindow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void redrawThumbnails() {
|
||||||
|
thumbnailsDrawnForRevision = -1;
|
||||||
|
if (comparingVariants) {
|
||||||
|
drawThumbnails();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills the side-by-side view with every shown variant, all drawn at the scale that fits the largest, so their
|
||||||
|
* sizes compare. Thumbnails arrive one at a time on their own thread, leaving the loader free for the 3D view.
|
||||||
|
*/
|
||||||
|
private void drawThumbnails() {
|
||||||
|
long revision = variantListRevision.get();
|
||||||
|
if (revision == thumbnailsDrawnForRevision) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
thumbnailsDrawnForRevision = revision;
|
||||||
|
List<IrisObject> variants = shownVariants;
|
||||||
|
gallery.showVariants(galleryHeading(), variants.size(), shownVariantIndex);
|
||||||
|
int size = gallery.thumbnailSize();
|
||||||
|
thumbnailPainter.execute(() -> {
|
||||||
|
List<ObjectMesh> meshes = new ArrayList<>();
|
||||||
|
for (IrisObject variant : variants) {
|
||||||
|
if (revision != variantListRevision.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
meshes.add(ObjectMesh.build(visibleBlocks(variant), this::modelOf));
|
||||||
|
}
|
||||||
|
ObjectMesh.Bounds sizeToFit = largestSize(meshes);
|
||||||
|
for (int index = 0; index < meshes.size() && revision == variantListRevision.get(); index++) {
|
||||||
|
BufferedImage thumbnail = ObjectRenderer.renderThumbnail(meshes.get(index), sizeToFit, size, size);
|
||||||
|
int shownIndex = index;
|
||||||
|
EventQueue.invokeLater(() -> {
|
||||||
|
if (revision == variantListRevision.get()) {
|
||||||
|
gallery.showThumbnail(shownIndex, thumbnail);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String galleryHeading() {
|
||||||
|
return String.format(Locale.ROOT, "%s · %d variants%s", shownKey, shownVariants.size(), seedNote());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ObjectMesh.Bounds largestSize(List<ObjectMesh> meshes) {
|
||||||
|
int sizeX = 1;
|
||||||
|
int sizeY = 1;
|
||||||
|
int sizeZ = 1;
|
||||||
|
for (ObjectMesh mesh : meshes) {
|
||||||
|
sizeX = Math.max(sizeX, mesh.bounds().sizeX());
|
||||||
|
sizeY = Math.max(sizeY, mesh.bounds().sizeY());
|
||||||
|
sizeZ = Math.max(sizeZ, mesh.bounds().sizeZ());
|
||||||
|
}
|
||||||
|
return new ObjectMesh.Bounds(0, 0, 0, sizeX, sizeY, sizeZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A new list of variants is on its way, so every thumbnail is outdated, unlike when stepping to another variant.
|
||||||
|
private long startBaking(String key) {
|
||||||
|
if (comparingVariants) {
|
||||||
|
gallery.showLoading();
|
||||||
|
}
|
||||||
|
return startLoading(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long startLoading(String key) {
|
||||||
|
shown = null;
|
||||||
|
wholeMesh = null;
|
||||||
|
meshRevision.incrementAndGet();
|
||||||
|
viewport.showLoading("Loading " + key);
|
||||||
|
return objectRevision.incrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoadedObject load(IrisObject object) {
|
||||||
|
Map<BlockVector, BlockData> blocks = visibleBlocks(object);
|
||||||
|
ObjectMesh mesh = ObjectMesh.build(blocks, this::modelOf);
|
||||||
|
ObjectSummary summary = ObjectSummary.of(object.getLoadKey(), mesh.bounds(), blocks.values(), this::iconOf);
|
||||||
|
return new LoadedObject(blocks, mesh, summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<BlockVector, BlockData> visibleBlocks(IrisObject object) {
|
||||||
|
Map<BlockVector, BlockData> blocks = new HashMap<>();
|
||||||
|
object.getBlocks().forEach((position, block) -> {
|
||||||
|
if (!block.getMaterial().isAir()) {
|
||||||
|
blocks.put(position, block);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void showLoaded(long revision, LoadedObject loaded) {
|
||||||
|
if (revision != objectRevision.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shown = loaded;
|
||||||
|
wholeMesh = loaded.mesh();
|
||||||
|
seeThroughTypes = Set.of();
|
||||||
|
statusBar.showActivity("");
|
||||||
|
viewport.show(loaded.mesh());
|
||||||
|
overview.show(loaded.summary());
|
||||||
|
layerCut.showHeight(loaded.mesh().bounds().sizeY());
|
||||||
|
showMeshSummary(loaded.mesh());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showMeshSummary(ObjectMesh mesh) {
|
||||||
|
ObjectSummary summary = shown.summary();
|
||||||
|
String cut = layerCut.isCutting()
|
||||||
|
? String.format(Locale.ROOT, " · showing layers 1-%d of %d", layerCut.shownLayerCount(), layerCut.layerCount())
|
||||||
|
: "";
|
||||||
|
statusBar.showSummary(String.format(Locale.ROOT, "%s%s · %,d blocks · %,d faces drawn%s",
|
||||||
|
summary.key(), seedNote(), summary.blockCount(), mesh.faceCount(), cut));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A seed being tried is not in the pack until its "seed" field says so.
|
||||||
|
private String seedNote() {
|
||||||
|
if (shownSeed.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String note = " · seed " + shownSeed.getAsLong();
|
||||||
|
return shownSeed.equals(packSeed) ? note : note + " (the pack has " + packSeed.getAsLong() + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSeeThrough(Set<String> blockTypes) {
|
||||||
|
seeThroughTypes = blockTypes;
|
||||||
|
rebuildWholeMesh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blocks behind see-through ones get their hidden faces back, so the mesh is built again.
|
||||||
|
private void rebuildWholeMesh() {
|
||||||
|
LoadedObject object = shown;
|
||||||
|
if (object == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long revision = meshRevision.incrementAndGet();
|
||||||
|
Set<String> seeThrough = seeThroughTypes;
|
||||||
|
loader.execute(() -> {
|
||||||
|
if (revision != meshRevision.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ObjectMesh mesh = seeThrough.isEmpty()
|
||||||
|
? object.mesh()
|
||||||
|
: ObjectMesh.build(object.blocks(), this::modelOf, object.mesh().bounds(),
|
||||||
|
block -> seeThrough.contains(block.getMaterial().getKey().getKey()));
|
||||||
|
EventQueue.invokeLater(() -> {
|
||||||
|
if (revision == meshRevision.get() && shown == object) {
|
||||||
|
wholeMesh = mesh;
|
||||||
|
showLayerCut();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showLayerCut() {
|
||||||
|
if (wholeMesh == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int highestShownY = wholeMesh.bounds().minY() + layerCut.shownLayerCount() - 1;
|
||||||
|
ObjectMesh mesh = layerCut.isCutting() ? wholeMesh.cutAbove(highestShownY) : wholeMesh;
|
||||||
|
viewport.replaceMesh(mesh);
|
||||||
|
showMeshSummary(mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showFailure(long revision, String key, Throwable error) {
|
||||||
|
if (revision != objectRevision.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
statusBar.showActivity("");
|
||||||
|
viewport.showMessage("Could not load " + key);
|
||||||
|
gallery.showMessage("Could not load " + key);
|
||||||
|
thumbnailsDrawnForRevision = -1;
|
||||||
|
overview.clear();
|
||||||
|
layerCut.showHeight(1);
|
||||||
|
statusBar.showError(error.getMessage() == null ? error.getClass().getSimpleName() : error.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The pack's files may have changed, so the list and the shown object are read again.
|
||||||
|
private void reloadPack() {
|
||||||
|
loader.execute(() -> {
|
||||||
|
List<String> keys = objectKeys();
|
||||||
|
EventQueue.invokeLater(() -> showCatalog(keys));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Engine engine() {
|
||||||
|
Engine current = GuiHost.findEngine(worldId);
|
||||||
|
return current != null && !current.isClosed() ? current : startEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BlockModel modelOf(BlockData block) {
|
||||||
|
return models.computeIfAbsent(block.getAsString(false), state -> {
|
||||||
|
BlockTextures available = textures;
|
||||||
|
if (available == null) {
|
||||||
|
return BlockModel.solidColor(UNKNOWN_BLOCK_COLOR);
|
||||||
|
}
|
||||||
|
return available.model(block).map(this::withBiomeTint).orElseGet(() -> BlockModel.solidColor(UNKNOWN_BLOCK_COLOR));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private BufferedImage iconOf(BlockData block) {
|
||||||
|
BlockTextures available = textures;
|
||||||
|
if (available == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return available.iconTexture(block).map(this::withBiomeTint).map(BlockTexture::toImage).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Objects have no biome of their own, so grass and leaves get plains' colours.
|
||||||
|
private BlockModel withBiomeTint(BlockModel model) {
|
||||||
|
return model.withTextures(this::withBiomeTint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BlockTexture withBiomeTint(BlockTexture texture) {
|
||||||
|
BiomeTintColors colors = tintColors;
|
||||||
|
if (texture.biomeTint() == null || colors == null) {
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
return texture.tinted(colors.colorAt(texture.biomeTint(), 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private record LoadedObject(Map<BlockVector, BlockData> blocks, ObjectMesh mesh, ObjectSummary summary) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JSlider;
|
||||||
|
import javax.swing.SwingConstants;
|
||||||
|
import java.awt.AlphaComposite;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.util.function.IntConsumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A slim slider floating on the right of the 3D view that hides an object's top layers, to look inside buildings
|
||||||
|
* floor by floor. Most objects never need it, so it stays faint until the mouse comes near or a cut is active.
|
||||||
|
*/
|
||||||
|
final class ObjectLayerCut extends JPanel {
|
||||||
|
private static final long serialVersionUID = 4473820211915526120L;
|
||||||
|
private static final int PANEL_WIDTH = 30;
|
||||||
|
private static final int MAX_HEIGHT = 320;
|
||||||
|
private static final double HEIGHT_SHARE_OF_VIEW = 0.5D;
|
||||||
|
private static final int EDGE_MARGIN = 8;
|
||||||
|
private static final float FAINT_OPACITY = 0.28F;
|
||||||
|
|
||||||
|
private final JSlider slider = new JSlider(SwingConstants.VERTICAL, 1, 1, 1);
|
||||||
|
private final JLabel shownLayers = new JLabel(" ", SwingConstants.CENTER);
|
||||||
|
private final IntConsumer onLayersChanged;
|
||||||
|
private boolean resetting;
|
||||||
|
private boolean hovered;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param onLayersChanged told how many layers from the bottom stay visible whenever the user moves the slider
|
||||||
|
*/
|
||||||
|
ObjectLayerCut(IntConsumer onLayersChanged) {
|
||||||
|
super(new BorderLayout(0, StudioTheme.scale(4)));
|
||||||
|
this.onLayersChanged = onLayersChanged;
|
||||||
|
setOpaque(false);
|
||||||
|
setToolTipText("Hide the top layers to look inside · Page Up / Page Down");
|
||||||
|
shownLayers.setFont(StudioTheme.smallFont());
|
||||||
|
shownLayers.setForeground(StudioTheme.SECONDARY_TEXT);
|
||||||
|
slider.setOpaque(false);
|
||||||
|
slider.setFocusable(false);
|
||||||
|
slider.setToolTipText(getToolTipText());
|
||||||
|
slider.addChangeListener(event -> {
|
||||||
|
showLayerLabel();
|
||||||
|
if (!resetting) {
|
||||||
|
onLayersChanged.accept(slider.getValue());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
MouseAdapter hover = new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseEntered(MouseEvent event) {
|
||||||
|
setHovered(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseExited(MouseEvent event) {
|
||||||
|
setHovered(slider.getValueIsAdjusting());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent event) {
|
||||||
|
setHovered(slider.contains(event.getPoint()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
slider.addMouseListener(hover);
|
||||||
|
addMouseListener(hover);
|
||||||
|
add(shownLayers, BorderLayout.NORTH);
|
||||||
|
add(slider, BorderLayout.CENTER);
|
||||||
|
showHeight(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view with this slider floating over its right edge, halfway down.
|
||||||
|
*/
|
||||||
|
JComponent floatingOver(JComponent view) {
|
||||||
|
ObjectLayerCut cut = this;
|
||||||
|
JPanel stack = new JPanel(null) {
|
||||||
|
private static final long serialVersionUID = -1950381405658102347L;
|
||||||
|
|
||||||
|
// The slider overlaps the view, so the view may not repaint without the slider on top of it.
|
||||||
|
@Override
|
||||||
|
public boolean isOptimizedDrawingEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doLayout() {
|
||||||
|
view.setBounds(0, 0, getWidth(), getHeight());
|
||||||
|
int width = StudioTheme.scale(PANEL_WIDTH);
|
||||||
|
int height = Math.min(StudioTheme.scale(MAX_HEIGHT), (int) (getHeight() * HEIGHT_SHARE_OF_VIEW));
|
||||||
|
cut.setBounds(getWidth() - width - StudioTheme.scale(EDGE_MARGIN), (getHeight() - height) / 2, width, height);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
stack.add(cut);
|
||||||
|
stack.add(view);
|
||||||
|
return stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows every layer of a newly chosen object without telling anyone, since nothing is hidden yet.
|
||||||
|
*/
|
||||||
|
void showHeight(int layers) {
|
||||||
|
resetting = true;
|
||||||
|
try {
|
||||||
|
slider.setMaximum(Math.max(1, layers));
|
||||||
|
slider.setValue(slider.getMaximum());
|
||||||
|
} finally {
|
||||||
|
resetting = false;
|
||||||
|
}
|
||||||
|
showLayerLabel();
|
||||||
|
setVisible(layers > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void stepBy(int layers) {
|
||||||
|
if (isVisible()) {
|
||||||
|
slider.setValue(slider.getValue() + layers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isCutting() {
|
||||||
|
return slider.getValue() < slider.getMaximum();
|
||||||
|
}
|
||||||
|
|
||||||
|
int shownLayerCount() {
|
||||||
|
return slider.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
int layerCount() {
|
||||||
|
return slider.getMaximum();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintChildren(Graphics graphics) {
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
if (!hovered && !isCutting()) {
|
||||||
|
canvas.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, FAINT_OPACITY));
|
||||||
|
}
|
||||||
|
super.paintChildren(canvas);
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setHovered(boolean nowHovered) {
|
||||||
|
if (hovered != nowHovered) {
|
||||||
|
hovered = nowHovered;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showLayerLabel() {
|
||||||
|
shownLayers.setText(isCutting() ? Integer.toString(slider.getValue()) : " ");
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.BlockModel.Face;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.util.BlockVector;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The faces of an object's blocks that can be seen from outside. A face is left out when the neighbour on its side
|
||||||
|
* covers that side, which is most of them in a large object. See-through faces, like water and stained glass, are
|
||||||
|
* kept apart because they are drawn last, blended over everything behind them. Top faces covered only by the block
|
||||||
|
* above are kept aside too, so the top layers can be cut away without building the mesh again; what is cut away stays
|
||||||
|
* as a faint ghost of the object's shape.
|
||||||
|
*/
|
||||||
|
record ObjectMesh(List<Quad> solidQuads, List<Quad> translucentQuads, List<Quad> cutAwayQuads, List<Quad> topsUnderBlockAbove,
|
||||||
|
Bounds bounds) {
|
||||||
|
private static final double SEE_THROUGH_OPACITY = 0.22D;
|
||||||
|
private static final double FULL_OPACITY = 1D;
|
||||||
|
private static final double CUT_AWAY_OPACITY = 0.1D;
|
||||||
|
|
||||||
|
static ObjectMesh build(Map<BlockVector, BlockData> blocks, Function<BlockData, BlockModel> modelOf) {
|
||||||
|
return build(blocks, modelOf, Bounds.of(blocks.keySet()), block -> false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bounds the whole object's extent, kept when only part of it is drawn so the view does not jump
|
||||||
|
* @param isSeeThrough blocks drawn faintly, which hide nothing behind them, to look past them into the object
|
||||||
|
*/
|
||||||
|
static ObjectMesh build(Map<BlockVector, BlockData> blocks, Function<BlockData, BlockModel> modelOf, Bounds bounds,
|
||||||
|
Predicate<BlockData> isSeeThrough) {
|
||||||
|
List<Quad> solid = new ArrayList<>();
|
||||||
|
List<Quad> translucent = new ArrayList<>();
|
||||||
|
List<Quad> topsUnderBlockAbove = new ArrayList<>();
|
||||||
|
BlockGrid grid = new BlockGrid(blocks, modelOf, bounds, isSeeThrough);
|
||||||
|
for (BlockVector position : blocks.keySet()) {
|
||||||
|
int x = position.getBlockX();
|
||||||
|
int y = position.getBlockY();
|
||||||
|
int z = position.getBlockZ();
|
||||||
|
int cell = grid.cellAt(x, y, z);
|
||||||
|
for (Face face : grid.modelIn(cell).faces()) {
|
||||||
|
Quad quad = new Quad(face, x, y, z, grid.isSeeThroughIn(cell) ? SEE_THROUGH_OPACITY : FULL_OPACITY);
|
||||||
|
if (!grid.isHiddenByNeighbour(x, y, z, face)) {
|
||||||
|
(quad.isBlended() ? translucent : solid).add(quad);
|
||||||
|
} else if (face.cullFace() == CubeFace.UP) {
|
||||||
|
topsUnderBlockAbove.add(quad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ObjectMesh(List.copyOf(solid), List.copyOf(translucent), List.of(), List.copyOf(topsUnderBlockAbove), bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This mesh with the blocks above the given layer faded to a ghost of the object's shape, and the tops of that
|
||||||
|
* layer's blocks showing again. Quick enough to follow a slider as it moves.
|
||||||
|
*/
|
||||||
|
ObjectMesh cutAbove(int highestShownY) {
|
||||||
|
List<Quad> solid = new ArrayList<>();
|
||||||
|
List<Quad> translucent = new ArrayList<>();
|
||||||
|
List<Quad> cutAway = new ArrayList<>();
|
||||||
|
for (Quad quad : solidQuads) {
|
||||||
|
keepOrCutAway(quad, highestShownY, solid, cutAway);
|
||||||
|
}
|
||||||
|
for (Quad quad : translucentQuads) {
|
||||||
|
keepOrCutAway(quad, highestShownY, translucent, cutAway);
|
||||||
|
}
|
||||||
|
for (Quad quad : topsUnderBlockAbove) {
|
||||||
|
if (quad.y() == highestShownY) {
|
||||||
|
(quad.isBlended() ? translucent : solid).add(quad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ObjectMesh(solid, translucent, cutAway, List.of(), bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void keepOrCutAway(Quad quad, int highestShownY, List<Quad> kept, List<Quad> cutAway) {
|
||||||
|
if (quad.y() <= highestShownY) {
|
||||||
|
kept.add(quad);
|
||||||
|
} else {
|
||||||
|
cutAway.add(quad.faded(CUT_AWAY_OPACITY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isEmpty() {
|
||||||
|
return solidQuads.isEmpty() && translucentQuads.isEmpty() && cutAwayQuads.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
int faceCount() {
|
||||||
|
return solidQuads.size() + translucentQuads.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object's blocks laid out in an array over its bounds, so finding a block's neighbours is quick. Looking them
|
||||||
|
* up by {@link BlockVector} in a map is slow, since its hash codes collide a lot.
|
||||||
|
*/
|
||||||
|
private static final class BlockGrid {
|
||||||
|
private final Bounds bounds;
|
||||||
|
private final BlockData[] blocks;
|
||||||
|
private final BlockModel[] models;
|
||||||
|
private final boolean[] seeThrough;
|
||||||
|
|
||||||
|
BlockGrid(Map<BlockVector, BlockData> placed, Function<BlockData, BlockModel> modelOf, Bounds bounds,
|
||||||
|
Predicate<BlockData> isSeeThrough) {
|
||||||
|
this.bounds = bounds;
|
||||||
|
int cells = bounds.sizeX() * bounds.sizeY() * bounds.sizeZ();
|
||||||
|
this.blocks = new BlockData[cells];
|
||||||
|
this.models = new BlockModel[cells];
|
||||||
|
this.seeThrough = new boolean[cells];
|
||||||
|
placed.forEach((position, block) -> {
|
||||||
|
int cell = cellAt(position.getBlockX(), position.getBlockY(), position.getBlockZ());
|
||||||
|
blocks[cell] = block;
|
||||||
|
models[cell] = modelOf.apply(block);
|
||||||
|
seeThrough[cell] = isSeeThrough.test(block);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int cellAt(int x, int y, int z) {
|
||||||
|
return ((y - bounds.minY()) * bounds.sizeZ() + (z - bounds.minZ())) * bounds.sizeX() + (x - bounds.minX());
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockModel modelIn(int cell) {
|
||||||
|
return models[cell];
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isSeeThroughIn(int cell) {
|
||||||
|
return seeThrough[cell];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Glass next to glass and water next to water hide the side between them too, as they do in the game. A
|
||||||
|
// see-through block only hides sides of other see-through blocks, so whatever it covers shows through it.
|
||||||
|
boolean isHiddenByNeighbour(int x, int y, int z, Face face) {
|
||||||
|
CubeFace side = face.cullFace();
|
||||||
|
if (side == null || !contains(x + side.stepX(), y + side.stepY(), z + side.stepZ())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int cell = cellAt(x, y, z);
|
||||||
|
int neighbourCell = cellAt(x + side.stepX(), y + side.stepY(), z + side.stepZ());
|
||||||
|
BlockData neighbour = blocks[neighbourCell];
|
||||||
|
if (neighbour == null || (seeThrough[neighbourCell] && !seeThrough[cell])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
BlockModel neighbourModel = models[neighbourCell];
|
||||||
|
if (neighbourModel.coveredSides().contains(side.opposite())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return neighbour.getMaterial() == blocks[cell].getMaterial() && models[cell].hidesSameBlockSides()
|
||||||
|
&& neighbourModel.hidesSameBlockSides();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean contains(int x, int y, int z) {
|
||||||
|
return x >= bounds.minX() && x < bounds.maxX() && y >= bounds.minY() && y < bounds.maxY()
|
||||||
|
&& z >= bounds.minZ() && z < bounds.maxZ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A model face placed at a block in the object, drawn at the given opacity on top of its texture's own.
|
||||||
|
*/
|
||||||
|
record Quad(Face face, int x, int y, int z, double opacity) {
|
||||||
|
boolean isBlended() {
|
||||||
|
return opacity < FULL_OPACITY || face.texture().isTranslucent();
|
||||||
|
}
|
||||||
|
|
||||||
|
Quad faded(double factor) {
|
||||||
|
return new Quad(face, x, y, z, opacity * factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
double originX() {
|
||||||
|
return x + face.origin().x();
|
||||||
|
}
|
||||||
|
|
||||||
|
double originY() {
|
||||||
|
return y + face.origin().y();
|
||||||
|
}
|
||||||
|
|
||||||
|
double originZ() {
|
||||||
|
return z + face.origin().z();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The blocks' extent, with the maximum one past the last block so a single block spans one unit.
|
||||||
|
*/
|
||||||
|
record Bounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||||
|
private static final Bounds EMPTY = new Bounds(0, 0, 0, 1, 1, 1);
|
||||||
|
|
||||||
|
static Bounds of(Iterable<BlockVector> positions) {
|
||||||
|
int minX = Integer.MAX_VALUE;
|
||||||
|
int minY = Integer.MAX_VALUE;
|
||||||
|
int minZ = Integer.MAX_VALUE;
|
||||||
|
int maxX = Integer.MIN_VALUE;
|
||||||
|
int maxY = Integer.MIN_VALUE;
|
||||||
|
int maxZ = Integer.MIN_VALUE;
|
||||||
|
for (BlockVector position : positions) {
|
||||||
|
minX = Math.min(minX, position.getBlockX());
|
||||||
|
minY = Math.min(minY, position.getBlockY());
|
||||||
|
minZ = Math.min(minZ, position.getBlockZ());
|
||||||
|
maxX = Math.max(maxX, position.getBlockX() + 1);
|
||||||
|
maxY = Math.max(maxY, position.getBlockY() + 1);
|
||||||
|
maxZ = Math.max(maxZ, position.getBlockZ() + 1);
|
||||||
|
}
|
||||||
|
return minX == Integer.MAX_VALUE ? EMPTY : new Bounds(minX, minY, minZ, maxX, maxY, maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
int sizeX() {
|
||||||
|
return maxX - minX;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sizeY() {
|
||||||
|
return maxY - minY;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sizeZ() {
|
||||||
|
return maxZ - minZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
int largestSize() {
|
||||||
|
return Math.max(sizeX(), Math.max(sizeY(), sizeZ()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.BoxLayout;
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
|
import javax.swing.DefaultListCellRenderer;
|
||||||
|
import javax.swing.DefaultListModel;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JList;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JTextField;
|
||||||
|
import javax.swing.event.DocumentEvent;
|
||||||
|
import javax.swing.event.DocumentListener;
|
||||||
|
import java.awt.AlphaComposite;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.Composite;
|
||||||
|
import java.awt.Cursor;
|
||||||
|
import java.awt.Insets;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.GridLayout;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The chosen object's size and the blocks it is made of, most used first, like the overview in Procedural Objects.
|
||||||
|
* Clicking a block makes it see-through in the 3D view, to look past walls into the object.
|
||||||
|
*/
|
||||||
|
final class ObjectOverviewPanel extends JPanel {
|
||||||
|
private static final long serialVersionUID = -8811407613645718290L;
|
||||||
|
private static final int PANEL_WIDTH = 280;
|
||||||
|
private static final int ROW_HEIGHT = 26;
|
||||||
|
private static final int ICON_SIZE = 16;
|
||||||
|
private static final int ICON_GAP = 8;
|
||||||
|
private static final int ROW_PADDING = 10;
|
||||||
|
private static final int SHARE_WIDTH = 92;
|
||||||
|
private static final int SHARE_BAR_HEIGHT = 4;
|
||||||
|
private static final float SEE_THROUGH_ICON_OPACITY = 0.3F;
|
||||||
|
private static final String SEE_THROUGH_HINT = "Click a block to see through it";
|
||||||
|
|
||||||
|
private final JLabel title = new JLabel(" ");
|
||||||
|
private final JLabel path = new JLabel(" ");
|
||||||
|
private final JLabel size = statValue();
|
||||||
|
private final JLabel blockCount = statValue();
|
||||||
|
private final JLabel blockTypes = statValue();
|
||||||
|
private final JLabel filled = statValue();
|
||||||
|
private final JTextField filterField = new JTextField();
|
||||||
|
private final DefaultListModel<ObjectSummary.BlockUse> rows = new DefaultListModel<>();
|
||||||
|
private final JList<ObjectSummary.BlockUse> list = new JList<>(rows) {
|
||||||
|
@Override
|
||||||
|
public boolean getScrollableTracksViewportWidth() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private final JLabel seeThroughHint = new JLabel(SEE_THROUGH_HINT);
|
||||||
|
private final JButton showAllSolid = new JButton("Show all solid");
|
||||||
|
private final Set<String> seeThroughTypes = new LinkedHashSet<>();
|
||||||
|
private final Consumer<Set<String>> onSeeThroughChanged;
|
||||||
|
private ObjectSummary summary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param onSeeThroughChanged told which block types to draw see-through whenever the user changes them
|
||||||
|
*/
|
||||||
|
ObjectOverviewPanel(Consumer<Set<String>> onSeeThroughChanged) {
|
||||||
|
super(new BorderLayout());
|
||||||
|
this.onSeeThroughChanged = onSeeThroughChanged;
|
||||||
|
setPreferredSize(StudioTheme.scaledSize(PANEL_WIDTH, 0));
|
||||||
|
setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, StudioTheme.SEPARATOR));
|
||||||
|
setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||||
|
add(buildHeader(), BorderLayout.NORTH);
|
||||||
|
add(buildBlockList(), BorderLayout.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
void show(ObjectSummary nextSummary) {
|
||||||
|
summary = nextSummary;
|
||||||
|
title.setText(nextSummary.name());
|
||||||
|
path.setText(nextSummary.key());
|
||||||
|
path.setToolTipText(nextSummary.key());
|
||||||
|
size.setText(nextSummary.sizeX() + " × " + nextSummary.sizeY() + " × " + nextSummary.sizeZ());
|
||||||
|
blockCount.setText(formatCount(nextSummary.blockCount()));
|
||||||
|
blockTypes.setText(formatCount(nextSummary.blockUses().size()));
|
||||||
|
filled.setText(String.format(Locale.ROOT, "%.1f%%", nextSummary.filledFraction() * 100D));
|
||||||
|
seeThroughTypes.clear();
|
||||||
|
showSeeThroughState();
|
||||||
|
refilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
summary = null;
|
||||||
|
title.setText(" ");
|
||||||
|
path.setText(" ");
|
||||||
|
for (JLabel value : List.of(size, blockCount, blockTypes, filled)) {
|
||||||
|
value.setText("—");
|
||||||
|
}
|
||||||
|
rows.clear();
|
||||||
|
seeThroughTypes.clear();
|
||||||
|
showSeeThroughState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildHeader() {
|
||||||
|
JPanel header = new JPanel();
|
||||||
|
header.setLayout(new BoxLayout(header, BoxLayout.Y_AXIS));
|
||||||
|
header.setOpaque(false);
|
||||||
|
header.setBorder(StudioTheme.emptyBorder(12, 16, 8, 16));
|
||||||
|
title.setFont(StudioTheme.titleFont());
|
||||||
|
path.setFont(StudioTheme.smallFont());
|
||||||
|
path.setForeground(StudioTheme.SECONDARY_TEXT);
|
||||||
|
header.add(leftAligned(title));
|
||||||
|
header.add(leftAligned(path));
|
||||||
|
|
||||||
|
JPanel stats = new JPanel(new GridLayout(2, 2, StudioTheme.scale(12), StudioTheme.scale(8)));
|
||||||
|
stats.setOpaque(false);
|
||||||
|
stats.setBorder(StudioTheme.emptyBorder(12, 0, 4, 0));
|
||||||
|
stats.add(stat("Size · X × Y × Z", size));
|
||||||
|
stats.add(stat("Blocks", blockCount));
|
||||||
|
stats.add(stat("Block types", blockTypes));
|
||||||
|
stats.add(stat("Filled volume", filled));
|
||||||
|
filled.setToolTipText("How much of the object's bounding box its blocks fill");
|
||||||
|
header.add(leftAligned(stats));
|
||||||
|
clear();
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildBlockList() {
|
||||||
|
JPanel blocks = new JPanel(new BorderLayout());
|
||||||
|
blocks.setOpaque(false);
|
||||||
|
StudioToolbar.makeSearchField(filterField, "Find a block...");
|
||||||
|
filterField.getDocument().addDocumentListener(new DocumentListener() {
|
||||||
|
@Override
|
||||||
|
public void insertUpdate(DocumentEvent event) {
|
||||||
|
refilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeUpdate(DocumentEvent event) {
|
||||||
|
refilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void changedUpdate(DocumentEvent event) {
|
||||||
|
refilter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
list.setFocusable(false);
|
||||||
|
list.setFixedCellHeight(StudioTheme.scale(ROW_HEIGHT));
|
||||||
|
list.setCellRenderer(new BlockUseRenderer());
|
||||||
|
list.setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||||
|
list.setBorder(StudioTheme.emptyBorder(0, 6, 4, 6));
|
||||||
|
list.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||||
|
list.addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent event) {
|
||||||
|
int row = list.locationToIndex(event.getPoint());
|
||||||
|
if (SwingUtilities.isLeftMouseButton(event) && row >= 0 && list.getCellBounds(row, row).contains(event.getPoint())) {
|
||||||
|
toggleSeeThrough(rows.get(row).blockType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
JPanel top = new JPanel();
|
||||||
|
top.setLayout(new BoxLayout(top, BoxLayout.Y_AXIS));
|
||||||
|
top.setOpaque(false);
|
||||||
|
top.add(leftAligned(StudioToolbar.padded(filterField, 4, 12, 4, 12)));
|
||||||
|
top.add(leftAligned(StudioToolbar.padded(seeThroughRow(), 0, 14, 6, 12)));
|
||||||
|
blocks.add(top, BorderLayout.NORTH);
|
||||||
|
blocks.add(StudioToolbar.scroll(list), BorderLayout.CENTER);
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel seeThroughRow() {
|
||||||
|
seeThroughHint.setFont(StudioTheme.smallFont());
|
||||||
|
seeThroughHint.setForeground(StudioTheme.MUTED_TEXT);
|
||||||
|
showAllSolid.setFont(StudioTheme.smallFont());
|
||||||
|
showAllSolid.setForeground(StudioTheme.LINK);
|
||||||
|
showAllSolid.setBorderPainted(false);
|
||||||
|
showAllSolid.setContentAreaFilled(false);
|
||||||
|
showAllSolid.setFocusable(false);
|
||||||
|
showAllSolid.setMargin(new Insets(0, 0, 0, 0));
|
||||||
|
showAllSolid.setBorder(BorderFactory.createEmptyBorder());
|
||||||
|
showAllSolid.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||||
|
showAllSolid.addActionListener(event -> {
|
||||||
|
seeThroughTypes.clear();
|
||||||
|
seeThroughChanged();
|
||||||
|
});
|
||||||
|
JPanel row = new JPanel(new BorderLayout());
|
||||||
|
row.setOpaque(false);
|
||||||
|
row.add(seeThroughHint, BorderLayout.CENTER);
|
||||||
|
row.add(showAllSolid, BorderLayout.EAST);
|
||||||
|
showSeeThroughState();
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleSeeThrough(String blockType) {
|
||||||
|
if (!seeThroughTypes.remove(blockType)) {
|
||||||
|
seeThroughTypes.add(blockType);
|
||||||
|
}
|
||||||
|
seeThroughChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seeThroughChanged() {
|
||||||
|
showSeeThroughState();
|
||||||
|
list.repaint();
|
||||||
|
onSeeThroughChanged.accept(Set.copyOf(seeThroughTypes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSeeThroughState() {
|
||||||
|
int count = seeThroughTypes.size();
|
||||||
|
seeThroughHint.setText(count == 0 ? SEE_THROUGH_HINT : count == 1 ? "1 block see-through" : count + " blocks see-through");
|
||||||
|
showAllSolid.setVisible(count > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refilter() {
|
||||||
|
rows.clear();
|
||||||
|
if (summary == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String filter = filterField.getText().trim().toLowerCase(Locale.ROOT).replace(' ', '_');
|
||||||
|
for (ObjectSummary.BlockUse use : summary.blockUses()) {
|
||||||
|
if (use.blockType().contains(filter)) {
|
||||||
|
rows.addElement(use);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JPanel stat(String label, JLabel value) {
|
||||||
|
JPanel stat = new JPanel(new BorderLayout());
|
||||||
|
stat.setOpaque(false);
|
||||||
|
JLabel name = new JLabel(label);
|
||||||
|
name.setFont(StudioTheme.smallFont());
|
||||||
|
name.setForeground(StudioTheme.MUTED_TEXT);
|
||||||
|
stat.add(name, BorderLayout.NORTH);
|
||||||
|
stat.add(value, BorderLayout.CENTER);
|
||||||
|
return stat;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JLabel statValue() {
|
||||||
|
JLabel value = new JLabel("—");
|
||||||
|
value.setFont(StudioTheme.boldFont());
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Component leftAligned(JPanel panel) {
|
||||||
|
panel.setAlignmentX(LEFT_ALIGNMENT);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Component leftAligned(JLabel label) {
|
||||||
|
label.setAlignmentX(LEFT_ALIGNMENT);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatCount(int count) {
|
||||||
|
return String.format(Locale.ROOT, "%,d", count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class BlockUseRenderer extends DefaultListCellRenderer {
|
||||||
|
private static final long serialVersionUID = 2785337045812613941L;
|
||||||
|
private transient BufferedImage icon;
|
||||||
|
private boolean seeThrough;
|
||||||
|
private double share;
|
||||||
|
private String percent = "";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Component getListCellRendererComponent(JList<?> owner, Object value, int index, boolean selected, boolean focused) {
|
||||||
|
ObjectSummary.BlockUse use = (ObjectSummary.BlockUse) value;
|
||||||
|
super.getListCellRendererComponent(owner, use.blockType(), index, false, false);
|
||||||
|
seeThrough = seeThroughTypes.contains(use.blockType());
|
||||||
|
setFont(StudioTheme.font());
|
||||||
|
setForeground(seeThrough ? StudioTheme.MUTED_TEXT : StudioTheme.TEXT);
|
||||||
|
setBorder(StudioTheme.emptyBorder(0, ROW_PADDING + ICON_SIZE + ICON_GAP, 0, ROW_PADDING + SHARE_WIDTH));
|
||||||
|
setToolTipText(use.blockType() + " · " + formatCount(use.count()) + " blocks · click to "
|
||||||
|
+ (seeThrough ? "make solid again" : "see through it"));
|
||||||
|
icon = use.icon();
|
||||||
|
share = summary == null || summary.blockCount() == 0 ? 0D : use.count() / (double) summary.blockCount();
|
||||||
|
percent = share > 0D && share < 0.001D ? "<0.1%" : String.format(Locale.ROOT, "%.1f%%", share * 100D);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics graphics) {
|
||||||
|
super.paintComponent(graphics);
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
int iconSize = StudioTheme.scale(ICON_SIZE);
|
||||||
|
int iconX = StudioTheme.scale(ROW_PADDING);
|
||||||
|
int iconY = (getHeight() - iconSize) / 2;
|
||||||
|
if (icon != null) {
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
|
||||||
|
Composite solid = canvas.getComposite();
|
||||||
|
if (seeThrough) {
|
||||||
|
canvas.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, SEE_THROUGH_ICON_OPACITY));
|
||||||
|
}
|
||||||
|
canvas.drawImage(icon, iconX, iconY, iconSize, iconSize, null);
|
||||||
|
canvas.setComposite(solid);
|
||||||
|
} else {
|
||||||
|
canvas.setColor(StudioTheme.CONTROL_BORDER);
|
||||||
|
canvas.drawRect(iconX, iconY, iconSize - 1, iconSize - 1);
|
||||||
|
}
|
||||||
|
drawShare(canvas);
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawShare(Graphics2D canvas) {
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
canvas.setFont(StudioTheme.smallFont());
|
||||||
|
int padding = StudioTheme.scale(ROW_PADDING);
|
||||||
|
int percentWidth = canvas.getFontMetrics().stringWidth("100.0%");
|
||||||
|
int barWidth = StudioTheme.scale(SHARE_WIDTH) - percentWidth - StudioTheme.scale(6);
|
||||||
|
int barX = getWidth() - padding - StudioTheme.scale(SHARE_WIDTH);
|
||||||
|
int barHeight = StudioTheme.scale(SHARE_BAR_HEIGHT);
|
||||||
|
int barY = (getHeight() - barHeight) / 2;
|
||||||
|
canvas.setColor(StudioTheme.HOVER_BACKGROUND);
|
||||||
|
canvas.fillRoundRect(barX, barY, barWidth, barHeight, barHeight, barHeight);
|
||||||
|
canvas.setColor(seeThrough ? StudioTheme.CONTROL_BORDER : StudioTheme.ACCENT);
|
||||||
|
canvas.fillRoundRect(barX, barY, Math.max(barHeight, (int) Math.round(barWidth * share)), barHeight, barHeight, barHeight);
|
||||||
|
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
||||||
|
int textWidth = canvas.getFontMetrics().stringWidth(percent);
|
||||||
|
int baseline = (getHeight() + canvas.getFontMetrics().getAscent() - canvas.getFontMetrics().getDescent()) / 2;
|
||||||
|
canvas.drawString(percent, getWidth() - padding - textWidth, baseline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.BlockModel.Face;
|
||||||
|
import com.volmit.iris.core.gui.BlockModel.TextureMapping;
|
||||||
|
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.awt.image.DataBufferInt;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws an object mesh with an orthographic camera into a picture, without a graphics card: each quad is filled pixel
|
||||||
|
* by pixel, and a depth buffer keeps the nearest one. A cut-away part's faint ghost only fills pixels the object
|
||||||
|
* leaves empty, so it never hazes over what is shown. See-through quads come last, farthest first, each blended over
|
||||||
|
* what is already drawn.
|
||||||
|
*/
|
||||||
|
final class ObjectRenderer {
|
||||||
|
private static final Color GRID = new ThemeColor(0xE3E4E8, 0x34373C);
|
||||||
|
private static final int GRID_LINES_PER_SIDE = 25;
|
||||||
|
private static final double GRID_REACH = 0.7D;
|
||||||
|
private static final double GRID_BELOW_OBJECT = 0.035D;
|
||||||
|
// Shared quad edges land exactly on pixel centres now and then; a little overlap keeps them from leaving cracks.
|
||||||
|
private static final double EDGE_TOLERANCE = 1e-6D;
|
||||||
|
private static final double EDGE_ON_DETERMINANT = 1e-9D;
|
||||||
|
// A face's own texels end a hair inside its UV rectangle, so rounding never picks the texel next to it.
|
||||||
|
private static final double TEXEL_EDGE_MARGIN = 1e-6D;
|
||||||
|
private static final double FULL_ALPHA = 255D;
|
||||||
|
// Zoom 2 fills the picture with the object's bounding box; thumbnails keep a hair of margin around it.
|
||||||
|
private static final ObjectCamera THUMBNAIL_CAMERA = new ObjectCamera(ObjectCamera.START.yaw(), ObjectCamera.START.pitch(), 1.9D, 0D, 0D);
|
||||||
|
|
||||||
|
private final int width;
|
||||||
|
private final int height;
|
||||||
|
private final int[] pixels;
|
||||||
|
private final double[] depths;
|
||||||
|
private final boolean[] showsObject;
|
||||||
|
private final Projection projection;
|
||||||
|
private final int gridColor = GRID.getRGB() & 0xFFFFFF;
|
||||||
|
|
||||||
|
private ObjectRenderer(BufferedImage image, Projection projection) {
|
||||||
|
this.width = image.getWidth();
|
||||||
|
this.height = image.getHeight();
|
||||||
|
this.pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
|
||||||
|
this.depths = new double[pixels.length];
|
||||||
|
this.showsObject = new boolean[pixels.length];
|
||||||
|
this.projection = projection;
|
||||||
|
Arrays.fill(pixels, StudioTheme.EDITOR_BACKGROUND.getRGB() & 0xFFFFFF);
|
||||||
|
Arrays.fill(depths, Double.NEGATIVE_INFINITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param coveredLeftWidth how much of the picture's left side something floats over, so the object is centred and
|
||||||
|
* fitted in the rest
|
||||||
|
*/
|
||||||
|
static BufferedImage render(ObjectMesh mesh, ObjectCamera camera, int width, int height, int coveredLeftWidth) {
|
||||||
|
BufferedImage image = blankImage(width, height);
|
||||||
|
int covered = Math.max(0, Math.min(image.getWidth() - 1, coveredLeftWidth));
|
||||||
|
ObjectRenderer renderer = new ObjectRenderer(image, new Projection(mesh.bounds(), mesh.bounds(), camera, image.getWidth(), image.getHeight(), covered));
|
||||||
|
renderer.drawGrid(mesh.bounds());
|
||||||
|
renderer.drawMesh(mesh);
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws {@code mesh} from the starting angle, without the floor grid, at the scale that fits an object of
|
||||||
|
* {@code sizeToFit}. Objects drawn with the same size compare at their true sizes, standing on the same ground.
|
||||||
|
*/
|
||||||
|
static BufferedImage renderThumbnail(ObjectMesh mesh, ObjectMesh.Bounds sizeToFit, int width, int height) {
|
||||||
|
BufferedImage image = blankImage(width, height);
|
||||||
|
new ObjectRenderer(image, new Projection(mesh.bounds(), sizeToFit, THUMBNAIL_CAMERA, image.getWidth(), image.getHeight(), 0)).drawMesh(mesh);
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BufferedImage blankImage(int width, int height) {
|
||||||
|
return new BufferedImage(Math.max(1, width), Math.max(1, height), BufferedImage.TYPE_INT_RGB);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawMesh(ObjectMesh mesh) {
|
||||||
|
for (ObjectMesh.Quad quad : mesh.solidQuads()) {
|
||||||
|
drawQuad(quad, Pass.SOLID);
|
||||||
|
}
|
||||||
|
for (ObjectMesh.Quad quad : mesh.cutAwayQuads()) {
|
||||||
|
drawQuad(quad, Pass.CUT_AWAY_GHOST);
|
||||||
|
}
|
||||||
|
for (ObjectMesh.Quad quad : farthestFirst(mesh.translucentQuads())) {
|
||||||
|
drawQuad(quad, Pass.BLENDED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ObjectMesh.Quad> farthestFirst(List<ObjectMesh.Quad> quads) {
|
||||||
|
List<ObjectMesh.Quad> sorted = new ArrayList<>(quads);
|
||||||
|
sorted.sort(Comparator.comparingDouble(this::centerDepth));
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double centerDepth(ObjectMesh.Quad quad) {
|
||||||
|
Face face = quad.face();
|
||||||
|
return projection.depth(
|
||||||
|
quad.originX() + (face.edgeU().x() + face.edgeV().x()) / 2D,
|
||||||
|
quad.originY() + (face.edgeU().y() + face.edgeV().y()) / 2D,
|
||||||
|
quad.originZ() + (face.edgeU().z() + face.edgeV().z()) / 2D);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawGrid(ObjectMesh.Bounds bounds) {
|
||||||
|
int reach = (int) Math.ceil(bounds.largestSize() * GRID_REACH);
|
||||||
|
int step = Math.max(1, (int) Math.ceil(reach / (double) GRID_LINES_PER_SIDE));
|
||||||
|
double floor = bounds.minY() - GRID_BELOW_OBJECT;
|
||||||
|
int centerX = Math.floorDiv(bounds.minX() + bounds.maxX(), 2);
|
||||||
|
int centerZ = Math.floorDiv(bounds.minZ() + bounds.maxZ(), 2);
|
||||||
|
for (int offset = -reach; offset <= reach; offset += step) {
|
||||||
|
drawLine(centerX + offset, floor, centerZ - reach, centerX + offset, floor, centerZ + reach);
|
||||||
|
drawLine(centerX - reach, floor, centerZ + offset, centerX + reach, floor, centerZ + offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawLine(double fromX, double fromY, double fromZ, double toX, double toY, double toZ) {
|
||||||
|
double startX = projection.screenX(fromX, fromY, fromZ);
|
||||||
|
double startY = projection.screenY(fromX, fromY, fromZ);
|
||||||
|
double startDepth = projection.depth(fromX, fromY, fromZ);
|
||||||
|
double endX = projection.screenX(toX, toY, toZ);
|
||||||
|
double endY = projection.screenY(toX, toY, toZ);
|
||||||
|
double endDepth = projection.depth(toX, toY, toZ);
|
||||||
|
int steps = (int) Math.ceil(Math.max(Math.abs(endX - startX), Math.abs(endY - startY)));
|
||||||
|
for (int step = 0; step <= steps; step++) {
|
||||||
|
double progress = steps == 0 ? 0D : step / (double) steps;
|
||||||
|
int x = (int) (startX + (endX - startX) * progress);
|
||||||
|
int y = (int) (startY + (endY - startY) * progress);
|
||||||
|
if (x < 0 || y < 0 || x >= width || y >= height) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int index = y * width + x;
|
||||||
|
double depth = startDepth + (endDepth - startDepth) * progress;
|
||||||
|
if (depth > depths[index]) {
|
||||||
|
depths[index] = depth;
|
||||||
|
pixels[index] = gridColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Later faces win ties, so an overlay drawn on the same box, like grass on a grass block's side, shows on top.
|
||||||
|
private void drawQuad(ObjectMesh.Quad quad, Pass pass) {
|
||||||
|
Face face = quad.face();
|
||||||
|
if (!projection.facesCamera(face.normal().x(), face.normal().y(), face.normal().z())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Vec3 u = face.edgeU();
|
||||||
|
Vec3 v = face.edgeV();
|
||||||
|
double originX = projection.screenX(quad.originX(), quad.originY(), quad.originZ());
|
||||||
|
double originY = projection.screenY(quad.originX(), quad.originY(), quad.originZ());
|
||||||
|
double originDepth = projection.depth(quad.originX(), quad.originY(), quad.originZ());
|
||||||
|
double edgeUX = projection.screenStepX(u.x(), u.z());
|
||||||
|
double edgeUY = projection.screenStepY(u.x(), u.y(), u.z());
|
||||||
|
double edgeUDepth = projection.depthStep(u.x(), u.y(), u.z());
|
||||||
|
double edgeVX = projection.screenStepX(v.x(), v.z());
|
||||||
|
double edgeVY = projection.screenStepY(v.x(), v.y(), v.z());
|
||||||
|
double edgeVDepth = projection.depthStep(v.x(), v.y(), v.z());
|
||||||
|
double determinant = edgeUX * edgeVY - edgeUY * edgeVX;
|
||||||
|
if (Math.abs(determinant) < EDGE_ON_DETERMINANT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int left = Math.max(0, (int) Math.floor(min4(originX, originX + edgeUX, originX + edgeVX, originX + edgeUX + edgeVX)));
|
||||||
|
int right = Math.min(width - 1, (int) Math.ceil(max4(originX, originX + edgeUX, originX + edgeVX, originX + edgeUX + edgeVX)));
|
||||||
|
int top = Math.max(0, (int) Math.floor(min4(originY, originY + edgeUY, originY + edgeVY, originY + edgeUY + edgeVY)));
|
||||||
|
int bottom = Math.min(height - 1, (int) Math.ceil(max4(originY, originY + edgeUY, originY + edgeVY, originY + edgeUY + edgeVY)));
|
||||||
|
if (left > right || top > bottom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockTexture texture = face.texture();
|
||||||
|
TextureMapping mapping = face.mapping();
|
||||||
|
TexelRange columns = TexelRange.of(mapping.u(), mapping.uAlongU(), mapping.uAlongV(), texture.width());
|
||||||
|
TexelRange rows = TexelRange.of(mapping.v(), mapping.vAlongU(), mapping.vAlongV(), texture.height());
|
||||||
|
int[] texels = texture.pixels();
|
||||||
|
boolean[] opaque = texture.opaque();
|
||||||
|
int[] alphas = texture.alphas();
|
||||||
|
double shade = face.shade();
|
||||||
|
double alongUPerPixelX = edgeVY / determinant;
|
||||||
|
double alongVPerPixelX = -edgeUY / determinant;
|
||||||
|
for (int y = top; y <= bottom; y++) {
|
||||||
|
double offsetX = left + 0.5D - originX;
|
||||||
|
double offsetY = y + 0.5D - originY;
|
||||||
|
double alongU = (offsetX * edgeVY - offsetY * edgeVX) / determinant;
|
||||||
|
double alongV = (offsetY * edgeUX - offsetX * edgeUY) / determinant;
|
||||||
|
int rowStart = y * width;
|
||||||
|
for (int x = left; x <= right; x++, alongU += alongUPerPixelX, alongV += alongVPerPixelX) {
|
||||||
|
if (alongU < -EDGE_TOLERANCE || alongU > 1D + EDGE_TOLERANCE || alongV < -EDGE_TOLERANCE || alongV > 1D + EDGE_TOLERANCE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int index = rowStart + x;
|
||||||
|
double depth = originDepth + alongU * edgeUDepth + alongV * edgeVDepth;
|
||||||
|
if (depth < depths[index] || (pass == Pass.CUT_AWAY_GHOST && showsObject[index])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int texelX = columns.texelAt(mapping.u() + alongU * mapping.uAlongU() + alongV * mapping.uAlongV());
|
||||||
|
int texelY = rows.texelAt(mapping.v() + alongU * mapping.vAlongU() + alongV * mapping.vAlongV());
|
||||||
|
int texel = texelY * texture.width() + texelX;
|
||||||
|
if (!opaque[texel]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int color = BlockPreviewColors.shade(texels[texel], shade);
|
||||||
|
if (pass != Pass.SOLID) {
|
||||||
|
pixels[index] = blend(color, pixels[index], (alphas == null ? 1D : alphas[texel] / FULL_ALPHA) * quad.opacity());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
depths[index] = depth;
|
||||||
|
pixels[index] = color;
|
||||||
|
showsObject[index] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int blend(int over, int under, double coverage) {
|
||||||
|
return blendChannel(over, under, coverage, 16) << 16 | blendChannel(over, under, coverage, 8) << 8 | blendChannel(over, under, coverage, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int blendChannel(int over, int under, double coverage, int shift) {
|
||||||
|
return (int) Math.round(((over >>> shift) & 0xFF) * coverage + ((under >>> shift) & 0xFF) * (1D - coverage));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double min4(double a, double b, double c, double d) {
|
||||||
|
return Math.min(Math.min(a, b), Math.min(c, d));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double max4(double a, double b, double c, double d) {
|
||||||
|
return Math.max(Math.max(a, b), Math.max(c, d));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The texels a face covers along one texture axis, so a pixel on the face's edge never takes its neighbour's.
|
||||||
|
*/
|
||||||
|
private record TexelRange(int first, int last, int size) {
|
||||||
|
static TexelRange of(double start, double alongU, double alongV, int size) {
|
||||||
|
double low = start + Math.min(0D, alongU) + Math.min(0D, alongV);
|
||||||
|
double high = start + Math.max(0D, alongU) + Math.max(0D, alongV);
|
||||||
|
int first = clamp((int) Math.floor(low * size + TEXEL_EDGE_MARGIN), size);
|
||||||
|
int last = Math.max(first, clamp((int) Math.ceil(high * size - TEXEL_EDGE_MARGIN) - 1, size));
|
||||||
|
return new TexelRange(first, last, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
int texelAt(double fraction) {
|
||||||
|
int texel = (int) Math.floor(fraction * size);
|
||||||
|
return texel < first ? first : Math.min(texel, last);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int clamp(int texel, int size) {
|
||||||
|
return Math.max(0, Math.min(size - 1, texel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns block positions into pixels. Larger depth is nearer the camera.
|
||||||
|
*/
|
||||||
|
private static final class Projection {
|
||||||
|
private final double centerX;
|
||||||
|
private final double centerY;
|
||||||
|
private final double centerZ;
|
||||||
|
private final double cosYaw;
|
||||||
|
private final double sinYaw;
|
||||||
|
private final double cosPitch;
|
||||||
|
private final double sinPitch;
|
||||||
|
private final double pixelsPerBlock;
|
||||||
|
private final double screenCenterX;
|
||||||
|
private final double screenCenterY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param shown the object drawn, centred across and standing where an object of {@code scaledAs} would
|
||||||
|
* @param scaledAs the size that is fitted to the view
|
||||||
|
*/
|
||||||
|
private Projection(ObjectMesh.Bounds shown, ObjectMesh.Bounds scaledAs, ObjectCamera camera, int width, int height, int coveredLeftWidth) {
|
||||||
|
this.centerX = (shown.minX() + shown.maxX()) / 2D;
|
||||||
|
this.centerY = shown.minY() + scaledAs.sizeY() / 2D;
|
||||||
|
this.centerZ = (shown.minZ() + shown.maxZ()) / 2D;
|
||||||
|
this.cosYaw = Math.cos(camera.yaw());
|
||||||
|
this.sinYaw = Math.sin(camera.yaw());
|
||||||
|
this.cosPitch = Math.cos(camera.pitch());
|
||||||
|
this.sinPitch = Math.sin(camera.pitch());
|
||||||
|
int visibleWidth = width - coveredLeftWidth;
|
||||||
|
double fit = fitAtStartingAngle(scaledAs, visibleWidth / (double) height);
|
||||||
|
this.pixelsPerBlock = camera.zoom() * fit * height / 2D / scaledAs.largestSize();
|
||||||
|
this.screenCenterX = coveredLeftWidth + visibleWidth / 2D + camera.panX() * visibleWidth / 2D;
|
||||||
|
this.screenCenterY = height / 2D - camera.panY() * height / 2D;
|
||||||
|
}
|
||||||
|
|
||||||
|
// How far to scale the object down so it fits the view at the starting angle; orbiting keeps the scale.
|
||||||
|
private static double fitAtStartingAngle(ObjectMesh.Bounds bounds, double aspect) {
|
||||||
|
double size = bounds.largestSize();
|
||||||
|
double width = bounds.sizeX() / size;
|
||||||
|
double height = bounds.sizeY() / size;
|
||||||
|
double depth = bounds.sizeZ() / size;
|
||||||
|
double yaw = ObjectCamera.START.yaw();
|
||||||
|
double pitch = ObjectCamera.START.pitch();
|
||||||
|
double projectedWidth = Math.abs(Math.cos(yaw)) * width + Math.abs(Math.sin(yaw)) * depth;
|
||||||
|
double projectedDepth = Math.abs(Math.sin(yaw)) * width + Math.abs(Math.cos(yaw)) * depth;
|
||||||
|
double projectedHeight = Math.abs(Math.cos(pitch)) * height + Math.abs(Math.sin(pitch)) * projectedDepth;
|
||||||
|
return Math.min(1D, Math.min(aspect / projectedWidth, 1D / projectedHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
double screenX(double x, double y, double z) {
|
||||||
|
return screenCenterX + screenStepX(x - centerX, z - centerZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
double screenY(double x, double y, double z) {
|
||||||
|
return screenCenterY + screenStepY(x - centerX, y - centerY, z - centerZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
double depth(double x, double y, double z) {
|
||||||
|
return depthStep(x - centerX, y - centerY, z - centerZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
double screenStepX(double x, double z) {
|
||||||
|
return (cosYaw * x - sinYaw * z) * pixelsPerBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
double screenStepY(double x, double y, double z) {
|
||||||
|
double towardCamera = sinYaw * x + cosYaw * z;
|
||||||
|
return -(cosPitch * y - sinPitch * towardCamera) * pixelsPerBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
double depthStep(double x, double y, double z) {
|
||||||
|
double towardCamera = sinYaw * x + cosYaw * z;
|
||||||
|
return sinPitch * y + cosPitch * towardCamera;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean facesCamera(double normalX, double normalY, double normalZ) {
|
||||||
|
return depthStep(normalX, normalY, normalZ) > 0D;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Pass {
|
||||||
|
SOLID,
|
||||||
|
CUT_AWAY_GHOST,
|
||||||
|
BLENDED
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import javax.swing.DefaultComboBoxModel;
|
||||||
|
import javax.swing.DefaultListCellRenderer;
|
||||||
|
import javax.swing.JComboBox;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JList;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.UIManager;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.EventQueue;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import java.util.function.LongConsumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where the seed of a procedural object's variants is chosen: typed in, picked again from the seeds tried on this
|
||||||
|
* object with the pack's own first, or rerolled at random. It keeps its size whatever it shows, so the bar it sits in
|
||||||
|
* never shifts under the mouse.
|
||||||
|
*/
|
||||||
|
final class ObjectSeedChooser extends JPanel {
|
||||||
|
private static final long serialVersionUID = 3127745038254406118L;
|
||||||
|
private static final int FIELD_WIDTH = 120;
|
||||||
|
private static final int TRIED_SEEDS_KEPT = 12;
|
||||||
|
// Small enough to read back and type, and far more seeds than anyone tries.
|
||||||
|
private static final int RANDOM_SEED_BOUND = 1_000_000;
|
||||||
|
|
||||||
|
private final JComboBox<Long> seeds = new JComboBox<>();
|
||||||
|
private final LongConsumer onSeedChosen;
|
||||||
|
private final List<Long> triedSeedsNewestFirst = new ArrayList<>();
|
||||||
|
private OptionalLong packSeed = OptionalLong.empty();
|
||||||
|
private long shownSeed;
|
||||||
|
private boolean showingSeeds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param onSeedChosen told the seed to bake the variants from whenever the user chooses another one
|
||||||
|
*/
|
||||||
|
ObjectSeedChooser(LongConsumer onSeedChosen) {
|
||||||
|
super(new BorderLayout(StudioTheme.scale(4), 0));
|
||||||
|
this.onSeedChosen = onSeedChosen;
|
||||||
|
setOpaque(false);
|
||||||
|
JLabel label = new JLabel("Seed");
|
||||||
|
label.setFont(StudioTheme.smallFont());
|
||||||
|
label.setForeground(StudioTheme.SECONDARY_TEXT);
|
||||||
|
seeds.setEditable(true);
|
||||||
|
seeds.setFont(StudioTheme.smallFont());
|
||||||
|
seeds.setToolTipText("Type a seed and press Enter, or pick one tried before");
|
||||||
|
seeds.setRenderer(new PackSeedMarker());
|
||||||
|
seeds.setPreferredSize(new Dimension(StudioTheme.scale(FIELD_WIDTH), seeds.getPreferredSize().height));
|
||||||
|
seeds.addActionListener(event -> {
|
||||||
|
if (!showingSeeds) {
|
||||||
|
EventQueue.invokeLater(this::chooseEnteredSeed);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add(label, BorderLayout.WEST);
|
||||||
|
add(seeds, BorderLayout.CENTER);
|
||||||
|
add(StudioToolbar.button("Reroll", "Bake the variants from a random seed", this::reroll), BorderLayout.EAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Starts a new object's list of tried seeds. */
|
||||||
|
void forgetTriedSeeds() {
|
||||||
|
triedSeedsNewestFirst.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showSeed(long shown, OptionalLong pack) {
|
||||||
|
shownSeed = shown;
|
||||||
|
packSeed = pack;
|
||||||
|
if (!isPackSeed(shown)) {
|
||||||
|
rememberTried(shown);
|
||||||
|
}
|
||||||
|
DefaultComboBoxModel<Long> choices = new DefaultComboBoxModel<>();
|
||||||
|
pack.ifPresent(choices::addElement);
|
||||||
|
triedSeedsNewestFirst.forEach(choices::addElement);
|
||||||
|
showingSeeds = true;
|
||||||
|
try {
|
||||||
|
seeds.setModel(choices);
|
||||||
|
seeds.setSelectedItem(shown);
|
||||||
|
} finally {
|
||||||
|
showingSeeds = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rememberTried(long seed) {
|
||||||
|
triedSeedsNewestFirst.remove(seed);
|
||||||
|
triedSeedsNewestFirst.add(0, seed);
|
||||||
|
while (triedSeedsNewestFirst.size() > TRIED_SEEDS_KEPT) {
|
||||||
|
triedSeedsNewestFirst.remove(triedSeedsNewestFirst.size() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pick from the list arrives as a seed, anything typed as text.
|
||||||
|
private void chooseEnteredSeed() {
|
||||||
|
OptionalLong entered = parse(seeds.getSelectedItem());
|
||||||
|
if (entered.isEmpty()) {
|
||||||
|
UIManager.getLookAndFeel().provideErrorFeedback(seeds);
|
||||||
|
showSeed(shownSeed, packSeed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (entered.getAsLong() != shownSeed) {
|
||||||
|
onSeedChosen.accept(entered.getAsLong());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OptionalLong parse(Object entered) {
|
||||||
|
if (entered instanceof Long seed) {
|
||||||
|
return OptionalLong.of(seed);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return OptionalLong.of(Long.parseLong(String.valueOf(entered).trim()));
|
||||||
|
} catch (NumberFormatException notASeed) {
|
||||||
|
return OptionalLong.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reroll() {
|
||||||
|
long seed;
|
||||||
|
do {
|
||||||
|
seed = ThreadLocalRandom.current().nextInt(RANDOM_SEED_BOUND);
|
||||||
|
} while (seed == shownSeed);
|
||||||
|
onSeedChosen.accept(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPackSeed(Object seed) {
|
||||||
|
return packSeed.isPresent() && seed instanceof Long value && value == packSeed.getAsLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PackSeedMarker extends DefaultListCellRenderer {
|
||||||
|
private static final long serialVersionUID = -6061513307766393251L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean selected, boolean focused) {
|
||||||
|
super.getListCellRendererComponent(list, value, index, selected, focused);
|
||||||
|
if (index >= 0 && isPackSeed(value)) {
|
||||||
|
setText(value + " (pack)");
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.loader.IrisData;
|
||||||
|
import com.volmit.iris.engine.object.IrisObject;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where an object explorer gets what it shows: the pack's object files, or the objects Iris generates from settings.
|
||||||
|
* Both are called on the explorer's loader thread only.
|
||||||
|
*/
|
||||||
|
interface ObjectSource {
|
||||||
|
/** What one entry is called, such as "object", for the browser's texts. */
|
||||||
|
String noun();
|
||||||
|
|
||||||
|
/** What the view shows when the pack has no entries. */
|
||||||
|
EmptyState emptyState();
|
||||||
|
|
||||||
|
/** The key of every entry, where a slash separates folders. */
|
||||||
|
List<String> keys(IrisData data);
|
||||||
|
|
||||||
|
/** The objects of the entry, one for an object file and one per variant for a procedural object; empty when none load. */
|
||||||
|
List<IrisObject> variants(IrisData data, String key);
|
||||||
|
|
||||||
|
/** The seed the pack bakes the entry's variants from; empty for anything not generated from a seed. */
|
||||||
|
default OptionalLong seed(String key) {
|
||||||
|
return OptionalLong.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The entry's variants as {@code seed} would bake them; only asked of entries that have a seed. */
|
||||||
|
default List<IrisObject> variants(IrisData data, String key, long seed) {
|
||||||
|
throw new UnsupportedOperationException(key + " is not generated from a seed");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What an object is made of: its size and how many of each block type it uses, most used first.
|
||||||
|
*/
|
||||||
|
record ObjectSummary(String key, int sizeX, int sizeY, int sizeZ, int blockCount, List<BlockUse> blockUses) {
|
||||||
|
static ObjectSummary of(String key, ObjectMesh.Bounds bounds, Iterable<BlockData> blocks, Function<BlockData, BufferedImage> iconOf) {
|
||||||
|
Map<String, Integer> counts = new HashMap<>();
|
||||||
|
Map<String, BlockData> examples = new HashMap<>();
|
||||||
|
int blockCount = 0;
|
||||||
|
for (BlockData block : blocks) {
|
||||||
|
String blockType = block.getMaterial().getKey().getKey();
|
||||||
|
counts.merge(blockType, 1, Integer::sum);
|
||||||
|
examples.putIfAbsent(blockType, block);
|
||||||
|
blockCount++;
|
||||||
|
}
|
||||||
|
List<BlockUse> uses = counts.entrySet().stream()
|
||||||
|
.map(count -> new BlockUse(count.getKey(), count.getValue(), iconOf.apply(examples.get(count.getKey()))))
|
||||||
|
.sorted(Comparator.comparingInt(BlockUse::count).reversed().thenComparing(BlockUse::blockType))
|
||||||
|
.toList();
|
||||||
|
return new ObjectSummary(key, bounds.sizeX(), bounds.sizeY(), bounds.sizeZ(), blockCount, uses);
|
||||||
|
}
|
||||||
|
|
||||||
|
String name() {
|
||||||
|
return key.substring(key.lastIndexOf('/') + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
double filledFraction() {
|
||||||
|
long volume = (long) sizeX * sizeY * sizeZ;
|
||||||
|
return volume == 0 ? 0D : blockCount / (double) volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param icon the block's texture, or null when Minecraft's textures are not available
|
||||||
|
*/
|
||||||
|
record BlockUse(String blockType, int count, BufferedImage icon) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import javax.swing.AbstractAction;
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JScrollPane;
|
||||||
|
import javax.swing.KeyStroke;
|
||||||
|
import javax.swing.Scrollable;
|
||||||
|
import javax.swing.Timer;
|
||||||
|
import java.awt.AlphaComposite;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Cursor;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.Rectangle;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.awt.geom.RoundRectangle2D;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.IntConsumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every variant of a procedural object side by side, drawn at one scale so their sizes compare. Clicking one opens it
|
||||||
|
* in the 3D view. While new thumbnails are drawn, each card keeps its old one faded until its own arrives; cards
|
||||||
|
* still waiting after a moment play the loading animation, one after another like a wave.
|
||||||
|
*/
|
||||||
|
final class ObjectVariantGallery extends JPanel {
|
||||||
|
private static final long serialVersionUID = 6153024487310558262L;
|
||||||
|
private static final int THUMBNAIL_SIZE = 220;
|
||||||
|
private static final int CARD_PADDING = 6;
|
||||||
|
private static final int CARD_GAP = 12;
|
||||||
|
private static final int EDGE_MARGIN = 16;
|
||||||
|
// The variant bar floats over the bottom edge, so the last row scrolls clear of it.
|
||||||
|
private static final int VARIANT_BAR_CLEARANCE = 64;
|
||||||
|
private static final float OUTDATED_THUMBNAIL_OPACITY = 0.35F;
|
||||||
|
private static final int THUMBNAIL_SIZE_PER_LOADING_BLOCK = 14;
|
||||||
|
private static final int LOADING_WAVE_STEP_MILLIS = 90;
|
||||||
|
|
||||||
|
private final JLabel title = new JLabel(" ");
|
||||||
|
private final CardGrid grid = new CardGrid();
|
||||||
|
private final IntConsumer onVariantOpened;
|
||||||
|
private final Runnable onThemeChanged;
|
||||||
|
private final List<VariantCard> cards = new ArrayList<>();
|
||||||
|
private final Timer loadingTicker = new Timer(LoadingAnimation.FRAME_MILLIS, event -> repaintWaitingCards());
|
||||||
|
private int coveredLeftWidth;
|
||||||
|
private int selectedIndex;
|
||||||
|
private long waitingSinceMillis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param onVariantOpened told the index of a variant the user clicks
|
||||||
|
* @param onThemeChanged told when the theme changes, since thumbnails are drawn on the theme's background
|
||||||
|
*/
|
||||||
|
ObjectVariantGallery(IntConsumer onVariantOpened, Runnable onThemeChanged) {
|
||||||
|
super(new BorderLayout(0, StudioTheme.scale(EDGE_MARGIN)));
|
||||||
|
this.onVariantOpened = onVariantOpened;
|
||||||
|
this.onThemeChanged = onThemeChanged;
|
||||||
|
setOpaque(true);
|
||||||
|
setBackground(StudioTheme.EDITOR_BACKGROUND);
|
||||||
|
setFocusable(true);
|
||||||
|
title.setFont(StudioTheme.sectionFont());
|
||||||
|
title.setForeground(StudioTheme.TEXT);
|
||||||
|
JScrollPane scroll = StudioToolbar.scroll(grid);
|
||||||
|
scroll.setOpaque(false);
|
||||||
|
scroll.getViewport().setOpaque(false);
|
||||||
|
add(title, BorderLayout.NORTH);
|
||||||
|
add(scroll, BorderLayout.CENTER);
|
||||||
|
keepClearOfOverlays();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateUI() {
|
||||||
|
super.updateUI();
|
||||||
|
if (onThemeChanged != null) {
|
||||||
|
onThemeChanged.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void bindKey(int keyCode, String name, Runnable action) {
|
||||||
|
getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(keyCode, 0), name);
|
||||||
|
getActionMap().put(name, new AbstractAction() {
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent event) {
|
||||||
|
action.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCoveredLeftWidth(int width) {
|
||||||
|
coveredLeftWidth = width;
|
||||||
|
keepClearOfOverlays();
|
||||||
|
revalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
int thumbnailSize() {
|
||||||
|
return StudioTheme.scale(THUMBNAIL_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cards for {@code count} variants, waiting for their thumbnails. The cards already shown stay, with their old
|
||||||
|
* thumbnails faded, so a reroll never blanks the view.
|
||||||
|
*/
|
||||||
|
void showVariants(String heading, int count, int selected) {
|
||||||
|
title.setText(heading);
|
||||||
|
if (count != cards.size()) {
|
||||||
|
replaceCards(count);
|
||||||
|
}
|
||||||
|
startWaiting();
|
||||||
|
select(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fades every thumbnail while something new loads. */
|
||||||
|
void showLoading() {
|
||||||
|
startWaiting();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showThumbnail(int index, BufferedImage thumbnail) {
|
||||||
|
if (index < cards.size()) {
|
||||||
|
cards.get(index).showThumbnail(thumbnail);
|
||||||
|
}
|
||||||
|
if (cards.stream().noneMatch(VariantCard::isWaiting)) {
|
||||||
|
loadingTicker.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void replaceCards(int count) {
|
||||||
|
cards.clear();
|
||||||
|
grid.removeAll();
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
VariantCard card = new VariantCard(index);
|
||||||
|
cards.add(card);
|
||||||
|
grid.add(card);
|
||||||
|
}
|
||||||
|
grid.revalidate();
|
||||||
|
grid.repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startWaiting() {
|
||||||
|
if (!loadingTicker.isRunning()) {
|
||||||
|
waitingSinceMillis = System.currentTimeMillis();
|
||||||
|
loadingTicker.start();
|
||||||
|
}
|
||||||
|
cards.forEach(VariantCard::markOutdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void repaintWaitingCards() {
|
||||||
|
if (LoadingAnimation.isDue(waitingSinceMillis)) {
|
||||||
|
cards.stream().filter(VariantCard::isWaiting).forEach(VariantCard::repaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void select(int index) {
|
||||||
|
selectedIndex = index;
|
||||||
|
cards.forEach(VariantCard::repaint);
|
||||||
|
if (index < cards.size()) {
|
||||||
|
VariantCard card = cards.get(index);
|
||||||
|
grid.scrollRectToVisible(card.getBounds());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showMessage(String text) {
|
||||||
|
title.setText(text);
|
||||||
|
loadingTicker.stop();
|
||||||
|
cards.clear();
|
||||||
|
grid.removeAll();
|
||||||
|
grid.revalidate();
|
||||||
|
grid.repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void keepClearOfOverlays() {
|
||||||
|
int margin = StudioTheme.scale(EDGE_MARGIN);
|
||||||
|
setBorder(BorderFactory.createEmptyBorder(margin, coveredLeftWidth + margin, StudioTheme.scale(VARIANT_BAR_CLEARANCE), margin));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cards in as many columns as fit the width, wrapping onto more rows.
|
||||||
|
*/
|
||||||
|
private final class CardGrid extends JPanel implements Scrollable {
|
||||||
|
private static final long serialVersionUID = -3618705187215931408L;
|
||||||
|
|
||||||
|
CardGrid() {
|
||||||
|
super(null);
|
||||||
|
setOpaque(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doLayout() {
|
||||||
|
Dimension card = cardSize();
|
||||||
|
int gap = StudioTheme.scale(CARD_GAP);
|
||||||
|
int columns = columns();
|
||||||
|
for (int index = 0; index < getComponentCount(); index++) {
|
||||||
|
int column = index % columns;
|
||||||
|
int row = index / columns;
|
||||||
|
getComponent(index).setBounds(column * (card.width + gap), row * (card.height + gap), card.width, card.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredSize() {
|
||||||
|
Dimension card = cardSize();
|
||||||
|
int gap = StudioTheme.scale(CARD_GAP);
|
||||||
|
int rows = (getComponentCount() + columns() - 1) / columns();
|
||||||
|
return new Dimension(card.width, Math.max(0, rows * (card.height + gap) - gap));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int columns() {
|
||||||
|
int gap = StudioTheme.scale(CARD_GAP);
|
||||||
|
return Math.max(1, (getWidth() + gap) / (cardSize().width + gap));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredScrollableViewportSize() {
|
||||||
|
return getPreferredSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
|
||||||
|
return cardSize().height / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
|
||||||
|
return visibleRect.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getScrollableTracksViewportWidth() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getScrollableTracksViewportHeight() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dimension cardSize() {
|
||||||
|
int padding = StudioTheme.scale(CARD_PADDING);
|
||||||
|
int labelHeight = getFontMetrics(StudioTheme.smallFont()).getHeight();
|
||||||
|
return new Dimension(thumbnailSize() + 2 * padding, thumbnailSize() + labelHeight + 3 * padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class VariantCard extends JComponent {
|
||||||
|
private static final long serialVersionUID = 2296135546207834180L;
|
||||||
|
|
||||||
|
private final int index;
|
||||||
|
private BufferedImage thumbnail;
|
||||||
|
private boolean thumbnailOutdated;
|
||||||
|
private boolean hovered;
|
||||||
|
|
||||||
|
VariantCard(int index) {
|
||||||
|
this.index = index;
|
||||||
|
setToolTipText("Open variant " + (index + 1) + " in 3D");
|
||||||
|
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||||
|
addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent event) {
|
||||||
|
onVariantOpened.accept(VariantCard.this.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseEntered(MouseEvent event) {
|
||||||
|
setHovered(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseExited(MouseEvent event) {
|
||||||
|
setHovered(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void showThumbnail(BufferedImage image) {
|
||||||
|
thumbnail = image;
|
||||||
|
thumbnailOutdated = false;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void markOutdated() {
|
||||||
|
thumbnailOutdated = true;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isWaiting() {
|
||||||
|
return thumbnail == null || thumbnailOutdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setHovered(boolean nowHovered) {
|
||||||
|
hovered = nowHovered;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics graphics) {
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
double arc = StudioTheme.scale(StudioTheme.OVERLAY_ARC);
|
||||||
|
canvas.setColor(index == selectedIndex ? StudioTheme.SELECTION : hovered ? StudioTheme.HOVER_BACKGROUND : StudioTheme.PANEL_BACKGROUND);
|
||||||
|
canvas.fill(new RoundRectangle2D.Double(0, 0, getWidth(), getHeight(), arc, arc));
|
||||||
|
int padding = StudioTheme.scale(CARD_PADDING);
|
||||||
|
int size = thumbnailSize();
|
||||||
|
canvas.setFont(StudioTheme.smallFont());
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics();
|
||||||
|
drawThumbnail(canvas, padding, size);
|
||||||
|
canvas.setColor(StudioTheme.TEXT);
|
||||||
|
drawCentered(canvas, "Variant " + (index + 1), padding * 2 + size + metrics.getAscent());
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawThumbnail(Graphics2D canvas, int padding, int size) {
|
||||||
|
if (thumbnail != null) {
|
||||||
|
Graphics2D faded = (Graphics2D) canvas.create();
|
||||||
|
if (thumbnailOutdated) {
|
||||||
|
faded.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, OUTDATED_THUMBNAIL_OPACITY));
|
||||||
|
}
|
||||||
|
faded.drawImage(thumbnail, padding, padding, size, size, null);
|
||||||
|
faded.dispose();
|
||||||
|
}
|
||||||
|
if (isWaiting() && LoadingAnimation.isDue(waitingSinceMillis)) {
|
||||||
|
long shownMillis = System.currentTimeMillis() - waitingSinceMillis - LoadingAnimation.SHOW_AFTER_MILLIS;
|
||||||
|
LoadingAnimation.paint(canvas, padding + size / 2D, padding + size / 2D, size / (double) THUMBNAIL_SIZE_PER_LOADING_BLOCK,
|
||||||
|
shownMillis - (long) index * LOADING_WAVE_STEP_MILLIS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawCentered(Graphics2D canvas, String text, int baseline) {
|
||||||
|
canvas.drawString(text, (getWidth() - canvas.getFontMetrics().stringWidth(text)) / 2, baseline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import javax.swing.Box;
|
||||||
|
import javax.swing.BoxLayout;
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.SwingConstants;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.geom.RoundRectangle2D;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
import java.util.function.IntConsumer;
|
||||||
|
import java.util.function.LongConsumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A small bar floating at the bottom of the object view that steps through the variants of a procedural object and
|
||||||
|
* the seeds they are baked from, and switches between one variant in 3D and all of them side by side. It hides itself
|
||||||
|
* for anything with a single variant and no seed.
|
||||||
|
*/
|
||||||
|
final class ObjectVariantPicker extends JPanel {
|
||||||
|
private static final long serialVersionUID = -2716301839488217455L;
|
||||||
|
private static final int BOTTOM_MARGIN = 14;
|
||||||
|
private static final int VARIANT_LABEL_WIDTH = 110;
|
||||||
|
private static final int SECTION_GAP = 10;
|
||||||
|
private static final String VARIANT_SHORTCUT_HINT = " · [ and ]";
|
||||||
|
private static final String COMPARE_SHORTCUT_HINT = " · G";
|
||||||
|
|
||||||
|
private final JLabel shownVariant = label(VARIANT_LABEL_WIDTH);
|
||||||
|
private final JComponent variantStepper;
|
||||||
|
private final ObjectSeedChooser seedChooser;
|
||||||
|
private final JButton compareButton;
|
||||||
|
private final IntConsumer onVariantChosen;
|
||||||
|
private int variantCount;
|
||||||
|
private int shownIndex;
|
||||||
|
private boolean seeded;
|
||||||
|
private int coveredLeftWidth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param onVariantChosen told the index of the variant to show whenever the user steps to another one
|
||||||
|
* @param onSeedChosen told the seed to bake the variants from whenever the user chooses another one
|
||||||
|
* @param onCompareToggled told when the user switches between one variant and all of them
|
||||||
|
*/
|
||||||
|
ObjectVariantPicker(IntConsumer onVariantChosen, LongConsumer onSeedChosen, Runnable onCompareToggled) {
|
||||||
|
this.onVariantChosen = onVariantChosen;
|
||||||
|
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
|
||||||
|
setOpaque(false);
|
||||||
|
setBorder(StudioTheme.emptyBorder(3, 4, 3, 4));
|
||||||
|
variantStepper = stepper(shownVariant,
|
||||||
|
StudioToolbar.button("‹", "Previous variant" + VARIANT_SHORTCUT_HINT, () -> stepBy(-1)),
|
||||||
|
StudioToolbar.button("›", "Next variant" + VARIANT_SHORTCUT_HINT, () -> stepBy(1)));
|
||||||
|
seedChooser = new ObjectSeedChooser(onSeedChosen);
|
||||||
|
compareButton = StudioToolbar.button("", "", onCompareToggled);
|
||||||
|
add(variantStepper);
|
||||||
|
add(Box.createHorizontalStrut(StudioTheme.scale(SECTION_GAP)));
|
||||||
|
add(seedChooser);
|
||||||
|
add(Box.createHorizontalStrut(StudioTheme.scale(SECTION_GAP)));
|
||||||
|
add(compareButton);
|
||||||
|
showComparing(false);
|
||||||
|
showVariants(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JLabel label(int width) {
|
||||||
|
JLabel label = new JLabel(" ", SwingConstants.CENTER);
|
||||||
|
label.setFont(StudioTheme.smallFont());
|
||||||
|
label.setForeground(StudioTheme.TEXT);
|
||||||
|
label.setPreferredSize(new Dimension(StudioTheme.scale(width), label.getPreferredSize().height));
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JComponent stepper(JLabel shown, JButton previous, JButton next) {
|
||||||
|
JPanel stepper = new JPanel(new BorderLayout());
|
||||||
|
stepper.setOpaque(false);
|
||||||
|
stepper.add(previous, BorderLayout.WEST);
|
||||||
|
stepper.add(shown, BorderLayout.CENTER);
|
||||||
|
stepper.add(next, BorderLayout.EAST);
|
||||||
|
return stepper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view with this bar floating over its bottom edge, centred on the part the left island leaves uncovered.
|
||||||
|
*/
|
||||||
|
JComponent floatingOver(JComponent view) {
|
||||||
|
ObjectVariantPicker picker = this;
|
||||||
|
JPanel stack = new JPanel(null) {
|
||||||
|
private static final long serialVersionUID = 1609842937565014832L;
|
||||||
|
|
||||||
|
// The bar overlaps the view, so the view may not repaint without the bar on top of it.
|
||||||
|
@Override
|
||||||
|
public boolean isOptimizedDrawingEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doLayout() {
|
||||||
|
view.setBounds(0, 0, getWidth(), getHeight());
|
||||||
|
Dimension size = picker.getPreferredSize();
|
||||||
|
int centerX = coveredLeftWidth + (getWidth() - coveredLeftWidth) / 2;
|
||||||
|
picker.setBounds(centerX - size.width / 2, getHeight() - size.height - StudioTheme.scale(BOTTOM_MARGIN), size.width, size.height);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
stack.add(picker);
|
||||||
|
stack.add(view);
|
||||||
|
return stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCoveredLeftWidth(int width) {
|
||||||
|
coveredLeftWidth = width;
|
||||||
|
relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows variant {@code index} of {@code count} without telling anyone, since the caller already shows it.
|
||||||
|
*/
|
||||||
|
void showVariants(int count, int index) {
|
||||||
|
variantCount = count;
|
||||||
|
shownIndex = index;
|
||||||
|
shownVariant.setText("Variant " + (index + 1) + " of " + count);
|
||||||
|
variantStepper.setVisible(count > 1);
|
||||||
|
compareButton.setVisible(count > 1);
|
||||||
|
showWhenUseful();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the seed the variants are baked from; empty for objects not generated from a seed.
|
||||||
|
*/
|
||||||
|
void showSeed(OptionalLong shown, OptionalLong pack) {
|
||||||
|
seeded = shown.isPresent();
|
||||||
|
seedChooser.setVisible(seeded);
|
||||||
|
shown.ifPresent(seed -> seedChooser.showSeed(seed, pack));
|
||||||
|
showWhenUseful();
|
||||||
|
}
|
||||||
|
|
||||||
|
void forgetTriedSeeds() {
|
||||||
|
seedChooser.forgetTriedSeeds();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showComparing(boolean comparing) {
|
||||||
|
compareButton.setText(comparing ? "3D view" : "All variants");
|
||||||
|
compareButton.setToolTipText((comparing ? "Show the chosen variant in 3D" : "Show every variant side by side") + COMPARE_SHORTCUT_HINT);
|
||||||
|
relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void stepBy(int step) {
|
||||||
|
if (variantCount < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shownIndex = Math.floorMod(shownIndex + step, variantCount);
|
||||||
|
showVariants(variantCount, shownIndex);
|
||||||
|
onVariantChosen.accept(shownIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showWhenUseful() {
|
||||||
|
setVisible(variantCount > 1 || seeded);
|
||||||
|
relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void relayout() {
|
||||||
|
if (getParent() != null) {
|
||||||
|
getParent().revalidate();
|
||||||
|
getParent().repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics graphics) {
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
double arc = StudioTheme.scale(StudioTheme.OVERLAY_ARC);
|
||||||
|
RoundRectangle2D shape = new RoundRectangle2D.Double(0.5, 0.5, getWidth() - 1D, getHeight() - 1D, arc, arc);
|
||||||
|
canvas.setColor(StudioTheme.OVERLAY_BACKGROUND);
|
||||||
|
canvas.fill(shape);
|
||||||
|
canvas.setColor(StudioTheme.OVERLAY_BORDER);
|
||||||
|
canvas.draw(shape);
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import javax.swing.AbstractAction;
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.KeyStroke;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
|
import javax.swing.Timer;
|
||||||
|
import java.awt.AlphaComposite;
|
||||||
|
import java.awt.EventQueue;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.Point;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.awt.event.ComponentAdapter;
|
||||||
|
import java.awt.event.ComponentEvent;
|
||||||
|
import java.awt.event.KeyEvent;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.awt.event.MouseWheelEvent;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 3D view of one object. Drag to orbit, shift-drag or middle-drag to pan, scroll to zoom; arrow keys orbit and
|
||||||
|
* plus and minus zoom too. Frames are drawn on a background thread, one at a time, always for the latest camera.
|
||||||
|
* While the next object loads, the last picture stays up; only a slow load dims it under the loading animation.
|
||||||
|
*/
|
||||||
|
final class ObjectViewport extends JComponent {
|
||||||
|
private static final long serialVersionUID = -3175226517409913524L;
|
||||||
|
private static final double WHEEL_ZOOM_EXPONENT = 0.12D;
|
||||||
|
private static final double KEY_ZOOM_FACTOR = 1.1D;
|
||||||
|
private static final int KEY_ORBIT_PIXELS = 12;
|
||||||
|
private static final float LOADING_DIM_OPACITY = 0.88F;
|
||||||
|
private static final double LOADING_BLOCK_SIZE = 16D;
|
||||||
|
private static final int LOADING_TEXT_GAP = 46;
|
||||||
|
|
||||||
|
private final ExecutorService renderThread = Executors.newSingleThreadExecutor(runnable -> {
|
||||||
|
Thread thread = new Thread(runnable, "Iris Object Viewer");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
private ObjectMesh mesh;
|
||||||
|
private ObjectCamera camera = ObjectCamera.START;
|
||||||
|
private BufferedImage frame;
|
||||||
|
private String message = "Choose an object";
|
||||||
|
private EmptyState emptyState;
|
||||||
|
private boolean rendering;
|
||||||
|
private boolean renderAgain;
|
||||||
|
private Point dragPoint;
|
||||||
|
private int coveredLeftWidth;
|
||||||
|
private final Timer loadingTicker = new Timer(LoadingAnimation.FRAME_MILLIS, event -> repaint());
|
||||||
|
private String loadingText;
|
||||||
|
private long loadingSinceMillis;
|
||||||
|
|
||||||
|
ObjectViewport() {
|
||||||
|
setOpaque(true);
|
||||||
|
setBackground(StudioTheme.EDITOR_BACKGROUND);
|
||||||
|
setFocusable(true);
|
||||||
|
installMouseControls();
|
||||||
|
installKeyControls();
|
||||||
|
addComponentListener(new ComponentAdapter() {
|
||||||
|
@Override
|
||||||
|
public void componentResized(ComponentEvent event) {
|
||||||
|
requestFrame();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The theme changed, and the last frame was drawn on the old theme's background.
|
||||||
|
@Override
|
||||||
|
public void updateUI() {
|
||||||
|
super.updateUI();
|
||||||
|
requestFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The last picture stays up until the first one of this mesh is drawn.
|
||||||
|
void show(ObjectMesh nextMesh) {
|
||||||
|
mesh = nextMesh;
|
||||||
|
camera = ObjectCamera.START;
|
||||||
|
message = nextMesh.isEmpty() ? "No visible blocks" : null;
|
||||||
|
emptyState = null;
|
||||||
|
if (nextMesh.isEmpty()) {
|
||||||
|
frame = null;
|
||||||
|
finishLoading();
|
||||||
|
}
|
||||||
|
requestFrame();
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the view as loading something new. A load already under way keeps its start, so stepping quickly through
|
||||||
|
* objects still shows the animation once the wait adds up.
|
||||||
|
*/
|
||||||
|
void showLoading(String text) {
|
||||||
|
if (loadingText == null) {
|
||||||
|
loadingSinceMillis = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
loadingText = text;
|
||||||
|
message = null;
|
||||||
|
emptyState = null;
|
||||||
|
loadingTicker.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishLoading() {
|
||||||
|
loadingText = null;
|
||||||
|
loadingTicker.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swaps in another mesh of the same object, like one with its top layers cut away, keeping the camera.
|
||||||
|
*/
|
||||||
|
void replaceMesh(ObjectMesh nextMesh) {
|
||||||
|
mesh = nextMesh;
|
||||||
|
message = nextMesh.isEmpty() ? "No visible blocks" : null;
|
||||||
|
emptyState = null;
|
||||||
|
requestFrame();
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void bindKey(int keyCode, String name, Runnable action) {
|
||||||
|
getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(keyCode, 0), name);
|
||||||
|
getActionMap().put(name, new AbstractAction() {
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent event) {
|
||||||
|
action.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void showMessage(String text) {
|
||||||
|
mesh = null;
|
||||||
|
message = text;
|
||||||
|
emptyState = null;
|
||||||
|
frame = null;
|
||||||
|
finishLoading();
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showEmptyState(EmptyState state) {
|
||||||
|
mesh = null;
|
||||||
|
message = null;
|
||||||
|
emptyState = state;
|
||||||
|
frame = null;
|
||||||
|
finishLoading();
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps the object clear of a panel floating over the viewport's left side.
|
||||||
|
*/
|
||||||
|
void setCoveredLeftWidth(int width) {
|
||||||
|
if (width == coveredLeftWidth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
coveredLeftWidth = width;
|
||||||
|
requestFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetCamera() {
|
||||||
|
moveCamera(current -> ObjectCamera.START);
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() {
|
||||||
|
loadingTicker.stop();
|
||||||
|
renderThread.shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveCamera(UnaryOperator<ObjectCamera> move) {
|
||||||
|
camera = move.apply(camera);
|
||||||
|
requestFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestFrame() {
|
||||||
|
if (mesh == null || mesh.isEmpty() || getWidth() < 1 || getHeight() < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rendering) {
|
||||||
|
renderAgain = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rendering = true;
|
||||||
|
ObjectMesh frameMesh = mesh;
|
||||||
|
ObjectCamera frameCamera = camera;
|
||||||
|
int width = getWidth();
|
||||||
|
int height = getHeight();
|
||||||
|
int covered = coveredLeftWidth;
|
||||||
|
renderThread.execute(() -> {
|
||||||
|
BufferedImage image = ObjectRenderer.render(frameMesh, frameCamera, width, height, covered);
|
||||||
|
EventQueue.invokeLater(() -> finishFrame(frameMesh, image));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishFrame(ObjectMesh frameMesh, BufferedImage image) {
|
||||||
|
rendering = false;
|
||||||
|
if (frameMesh == mesh) {
|
||||||
|
frame = image;
|
||||||
|
finishLoading();
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
if (renderAgain) {
|
||||||
|
renderAgain = false;
|
||||||
|
requestFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics graphics) {
|
||||||
|
graphics.setColor(getBackground());
|
||||||
|
graphics.fillRect(0, 0, getWidth(), getHeight());
|
||||||
|
if (frame != null) {
|
||||||
|
graphics.drawImage(frame, 0, 0, null);
|
||||||
|
}
|
||||||
|
if (loadingText != null && LoadingAnimation.isDue(loadingSinceMillis)) {
|
||||||
|
drawLoading((Graphics2D) graphics);
|
||||||
|
}
|
||||||
|
if (message != null) {
|
||||||
|
drawMessage((Graphics2D) graphics);
|
||||||
|
}
|
||||||
|
if (emptyState != null) {
|
||||||
|
emptyState.paint((Graphics2D) graphics, this, coveredLeftWidth + (getWidth() - coveredLeftWidth) / 2, getHeight() / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawLoading(Graphics2D graphics) {
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
if (frame != null) {
|
||||||
|
canvas.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, LOADING_DIM_OPACITY));
|
||||||
|
canvas.setColor(getBackground());
|
||||||
|
canvas.fillRect(0, 0, getWidth(), getHeight());
|
||||||
|
canvas.setComposite(AlphaComposite.SrcOver);
|
||||||
|
}
|
||||||
|
int centerX = coveredLeftWidth + (getWidth() - coveredLeftWidth) / 2;
|
||||||
|
int centerY = getHeight() / 2;
|
||||||
|
long shownMillis = System.currentTimeMillis() - loadingSinceMillis - LoadingAnimation.SHOW_AFTER_MILLIS;
|
||||||
|
LoadingAnimation.paint(canvas, centerX, centerY, StudioTheme.scale(LOADING_BLOCK_SIZE), shownMillis);
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
canvas.setFont(StudioTheme.font());
|
||||||
|
canvas.setColor(StudioTheme.MUTED_TEXT);
|
||||||
|
String text = loadingText + "...";
|
||||||
|
int textWidth = canvas.getFontMetrics().stringWidth(text);
|
||||||
|
canvas.drawString(text, centerX - textWidth / 2, centerY + StudioTheme.scale(LOADING_TEXT_GAP));
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawMessage(Graphics2D canvas) {
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
canvas.setFont(StudioTheme.font());
|
||||||
|
canvas.setColor(StudioTheme.MUTED_TEXT);
|
||||||
|
int textWidth = canvas.getFontMetrics().stringWidth(message);
|
||||||
|
canvas.drawString(message, (getWidth() - textWidth) / 2, getHeight() / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void installMouseControls() {
|
||||||
|
MouseAdapter mouse = new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mousePressed(MouseEvent event) {
|
||||||
|
dragPoint = event.getPoint();
|
||||||
|
requestFocusInWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent event) {
|
||||||
|
dragPoint = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseDragged(MouseEvent event) {
|
||||||
|
if (dragPoint == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int draggedX = event.getX() - dragPoint.x;
|
||||||
|
int draggedY = event.getY() - dragPoint.y;
|
||||||
|
dragPoint = event.getPoint();
|
||||||
|
boolean panning = event.isShiftDown() || SwingUtilities.isMiddleMouseButton(event);
|
||||||
|
moveCamera(current -> panning
|
||||||
|
? current.pannedBy(2D * draggedX / Math.max(1, getWidth() - coveredLeftWidth), -2D * draggedY / getHeight())
|
||||||
|
: current.orbitedBy(draggedX, draggedY));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseWheelMoved(MouseWheelEvent event) {
|
||||||
|
moveCamera(current -> current.zoomedBy(Math.exp(-event.getPreciseWheelRotation() * WHEEL_ZOOM_EXPONENT)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addMouseListener(mouse);
|
||||||
|
addMouseMotionListener(mouse);
|
||||||
|
addMouseWheelListener(mouse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void installKeyControls() {
|
||||||
|
bindKey(KeyEvent.VK_LEFT, "orbit-left", current -> current.orbitedBy(-KEY_ORBIT_PIXELS, 0));
|
||||||
|
bindKey(KeyEvent.VK_RIGHT, "orbit-right", current -> current.orbitedBy(KEY_ORBIT_PIXELS, 0));
|
||||||
|
bindKey(KeyEvent.VK_UP, "orbit-up", current -> current.orbitedBy(0, KEY_ORBIT_PIXELS));
|
||||||
|
bindKey(KeyEvent.VK_DOWN, "orbit-down", current -> current.orbitedBy(0, -KEY_ORBIT_PIXELS));
|
||||||
|
bindKey(KeyEvent.VK_EQUALS, "zoom-in", current -> current.zoomedBy(KEY_ZOOM_FACTOR));
|
||||||
|
bindKey(KeyEvent.VK_ADD, "zoom-in-keypad", current -> current.zoomedBy(KEY_ZOOM_FACTOR));
|
||||||
|
bindKey(KeyEvent.VK_MINUS, "zoom-out", current -> current.zoomedBy(1D / KEY_ZOOM_FACTOR));
|
||||||
|
bindKey(KeyEvent.VK_SUBTRACT, "zoom-out-keypad", current -> current.zoomedBy(1D / KEY_ZOOM_FACTOR));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindKey(int keyCode, String name, UnaryOperator<ObjectCamera> move) {
|
||||||
|
bindKey(keyCode, name, () -> moveCamera(move));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.loader.IrisData;
|
||||||
|
import com.volmit.iris.engine.object.IrisObject;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pack's object files, by their load keys.
|
||||||
|
*/
|
||||||
|
final class PackObjectSource implements ObjectSource {
|
||||||
|
private static final int EMPTY_ICON_SIZE = 40;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String noun() {
|
||||||
|
return "object";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EmptyState emptyState() {
|
||||||
|
return new EmptyState(MapViewIcon.large(MapView.OBJECTS, EMPTY_ICON_SIZE), "No objects yet",
|
||||||
|
"Put the pack's .iob objects in", "objects/", "They appear here once the pack reloads.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> keys(IrisData data) {
|
||||||
|
return Arrays.asList(data.getObjectLoader().getPossibleKeys());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<IrisObject> variants(IrisData data, String key) {
|
||||||
|
IrisObject object = data.getObjectLoader().load(key);
|
||||||
|
return object == null ? List.of() : List.of(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.google.gson.stream.JsonReader;
|
||||||
|
import com.volmit.iris.core.loader.IrisData;
|
||||||
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
|
import com.volmit.iris.engine.object.IrisBiome;
|
||||||
|
import com.volmit.iris.engine.object.IrisBlockData;
|
||||||
|
import com.volmit.iris.engine.object.IrisRegion;
|
||||||
|
import com.volmit.iris.util.collection.KList;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
record PaletteCatalog(List<RegionEntry> regions, List<String> biomesOutsideRegions, Map<String, BiomeEntry> biomes,
|
||||||
|
List<SnippetEntry> snippets) {
|
||||||
|
private static final int SUMMARIZED_BLOCK_COUNT = 3;
|
||||||
|
|
||||||
|
static PaletteCatalog load(Engine engine) {
|
||||||
|
IrisData data = engine.getData();
|
||||||
|
Map<String, BiomeEntry> biomes = loadBiomes(data);
|
||||||
|
List<RegionEntry> regions = new ArrayList<>();
|
||||||
|
Set<String> biomesInRegions = new HashSet<>();
|
||||||
|
for (String regionKey : regionKeysInUse(engine)) {
|
||||||
|
IrisRegion region = data.getRegionLoader().load(regionKey);
|
||||||
|
if (region == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
RegionEntry entry = describeRegion(regionKey, region, biomes);
|
||||||
|
regions.add(entry);
|
||||||
|
entry.groups().forEach(group -> group.biomeKeys().forEach(key -> collectWithChildren(key, biomes, biomesInRegions)));
|
||||||
|
}
|
||||||
|
List<String> outside = biomes.keySet().stream().filter(key -> !biomesInRegions.contains(key)).toList();
|
||||||
|
return new PaletteCatalog(List.copyOf(regions), outside, Map.copyOf(biomes), loadSnippets(data, biomes));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snippet files no biome refers to are listed too, so a new snippet shows up before it is put to use.
|
||||||
|
private static List<SnippetEntry> loadSnippets(IrisData data, Map<String, BiomeEntry> biomes) {
|
||||||
|
Map<String, SnippetEntry> snippets = new LinkedHashMap<>();
|
||||||
|
for (BiomeEntry biome : biomes.values()) {
|
||||||
|
for (PaletteSummary summary : biome.palettes()) {
|
||||||
|
if (summary.snippet() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BiomePalette.SnippetFolder folder = summary.palette().kind().snippetFolder();
|
||||||
|
snippets.computeIfAbsent(folder.folder() + "/" + summary.snippet(),
|
||||||
|
key -> new SnippetEntry(folder, summary.snippet(), summary.blocks(), new ArrayList<>()))
|
||||||
|
.uses().add(summary.palette());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (BiomePalette.SnippetFolder folder : BiomePalette.SnippetFolder.values()) {
|
||||||
|
for (String reference : data.getPossibleSnippets(folder.folder())) {
|
||||||
|
String path = folder.pathOf(reference);
|
||||||
|
snippets.putIfAbsent(folder.folder() + "/" + path, new SnippetEntry(folder, path, null, List.of()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return snippets.values().stream()
|
||||||
|
.map(snippet -> new SnippetEntry(snippet.folder(), snippet.path(), snippet.blocks(), List.copyOf(snippet.uses())))
|
||||||
|
.sorted(Comparator.comparing(SnippetEntry::folder).thenComparing(SnippetEntry::path))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A studio focus region is generated even when the dimension's region list doesn't include it.
|
||||||
|
private static Set<String> regionKeysInUse(Engine engine) {
|
||||||
|
Set<String> keys = new LinkedHashSet<>();
|
||||||
|
if (engine.getDimension().hasFocusRegion()) {
|
||||||
|
keys.add(engine.getDimension().getFocusRegion());
|
||||||
|
}
|
||||||
|
keys.addAll(engine.getDimension().getRegions());
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
BiomeEntry biome(String key) {
|
||||||
|
return key == null ? null : biomes.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
RegionEntry region(String key) {
|
||||||
|
return regions.stream().filter(region -> region.key().equals(key)).findFirst().orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String summarizeBlocks(KList<IrisBlockData> blocks) {
|
||||||
|
List<String> names = blocks.stream()
|
||||||
|
.map(block -> block.getBlock().replace("minecraft:", "").toLowerCase(Locale.ROOT))
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
if (names.size() <= SUMMARIZED_BLOCK_COUNT) {
|
||||||
|
return String.join(", ", names);
|
||||||
|
}
|
||||||
|
return String.join(", ", names.subList(0, SUMMARIZED_BLOCK_COUNT))
|
||||||
|
+ " +" + (names.size() - SUMMARIZED_BLOCK_COUNT) + " more";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, BiomeEntry> loadBiomes(IrisData data) {
|
||||||
|
String[] keys = data.getBiomeLoader().getPossibleKeys();
|
||||||
|
Map<String, IrisBiome> loaded = new LinkedHashMap<>();
|
||||||
|
if (keys == null) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
Arrays.sort(keys);
|
||||||
|
for (String key : keys) {
|
||||||
|
IrisBiome biome = data.getBiomeLoader().load(key);
|
||||||
|
if (biome != null) {
|
||||||
|
loaded.put(key, biome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, List<String>> parentsByChild = parentsByChild(loaded);
|
||||||
|
Map<String, BiomeEntry> biomes = new LinkedHashMap<>();
|
||||||
|
loaded.forEach((key, biome) -> biomes.put(key, new BiomeEntry(
|
||||||
|
key,
|
||||||
|
biome.getName(),
|
||||||
|
summarizePalettes(key, biome),
|
||||||
|
biome.getChildren().stream().filter(loaded::containsKey).distinct().toList(),
|
||||||
|
parentsByChild.getOrDefault(key, List.of())
|
||||||
|
)));
|
||||||
|
return biomes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, List<String>> parentsByChild(Map<String, IrisBiome> biomes) {
|
||||||
|
Map<String, List<String>> parents = new LinkedHashMap<>();
|
||||||
|
biomes.forEach((parentKey, parent) -> parent.getChildren().stream().distinct().forEach(childKey ->
|
||||||
|
parents.computeIfAbsent(childKey, key -> new ArrayList<>()).add(parentKey)));
|
||||||
|
return parents;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<PaletteSummary> summarizePalettes(String key, IrisBiome biome) {
|
||||||
|
JsonObject biomeSource = readSource(biome.getLoadFile());
|
||||||
|
return BiomePalette.allOf(key, biome).stream()
|
||||||
|
.map(palette -> new PaletteSummary(palette, snippetName(biomeSource, palette),
|
||||||
|
summarizeBlocks(palette.blocksIn(biome))))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snippets are copied into the biome when it loads, so only its file still tells which palettes came from one.
|
||||||
|
private static String snippetName(JsonObject biomeSource, BiomePalette palette) {
|
||||||
|
JsonElement element = biomeSource == null ? null : biomeSource.get(palette.kind().jsonKey());
|
||||||
|
if (element instanceof JsonArray list) {
|
||||||
|
element = palette.index() < list.size() ? list.get(palette.index()) : null;
|
||||||
|
}
|
||||||
|
if (element == null || !element.isJsonPrimitive()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return palette.kind().snippetFolder().pathOf(element.getAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject readSource(File file) {
|
||||||
|
if (file == null || !file.isFile()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try (JsonReader reader = new JsonReader(new FileReader(file))) {
|
||||||
|
reader.setLenient(true);
|
||||||
|
JsonElement source = JsonParser.parseReader(reader);
|
||||||
|
return source.isJsonObject() ? source.getAsJsonObject() : null;
|
||||||
|
} catch (IOException | RuntimeException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RegionEntry describeRegion(String key, IrisRegion region, Map<String, BiomeEntry> biomes) {
|
||||||
|
List<BiomeGroup> groups = new ArrayList<>();
|
||||||
|
addGroup(groups, "Land", region.getLandBiomes(), biomes);
|
||||||
|
addGroup(groups, "Shore", region.getShoreBiomes(), biomes);
|
||||||
|
addGroup(groups, "Sea", region.getSeaBiomes(), biomes);
|
||||||
|
addGroup(groups, "Cave", region.getCaveBiomes(), biomes);
|
||||||
|
return new RegionEntry(key, region.getName(), List.copyOf(groups));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addGroup(List<BiomeGroup> groups, String label, List<String> keys, Map<String, BiomeEntry> biomes) {
|
||||||
|
List<String> known = keys.stream().filter(biomes::containsKey).distinct().toList();
|
||||||
|
if (!known.isEmpty()) {
|
||||||
|
groups.add(new BiomeGroup(label, known));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectWithChildren(String key, Map<String, BiomeEntry> biomes, Set<String> collected) {
|
||||||
|
BiomeEntry biome = biomes.get(key);
|
||||||
|
if (biome == null || !collected.add(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
biome.childKeys().forEach(child -> collectWithChildren(child, biomes, collected));
|
||||||
|
}
|
||||||
|
|
||||||
|
record RegionEntry(String key, String name, List<BiomeGroup> groups) {
|
||||||
|
List<String> allBiomeKeys() {
|
||||||
|
return groups.stream().flatMap(group -> group.biomeKeys().stream()).distinct().toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record BiomeGroup(String label, List<String> biomeKeys) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param parentKeys the biomes that list this one as a child, which it can appear inside of
|
||||||
|
*/
|
||||||
|
record BiomeEntry(String key, String name, List<PaletteSummary> palettes, List<String> childKeys,
|
||||||
|
List<String> parentKeys) {
|
||||||
|
boolean matches(String filter) {
|
||||||
|
if (key.toLowerCase().contains(filter) || name.toLowerCase().contains(filter)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return palettes.stream().anyMatch(summary -> summary.text().toLowerCase().contains(filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
BiomePalette firstPalette() {
|
||||||
|
return palettes.isEmpty() ? null : palettes.get(0).palette();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A snippet file palettes can come from.
|
||||||
|
*
|
||||||
|
* @param blocks a summary of its blocks, or null when no biome uses it
|
||||||
|
* @param uses the palettes made from it, in biome order
|
||||||
|
*/
|
||||||
|
record SnippetEntry(BiomePalette.SnippetFolder folder, String path, String blocks, List<BiomePalette> uses) {
|
||||||
|
boolean matches(String filter) {
|
||||||
|
return path.toLowerCase(Locale.ROOT).contains(filter)
|
||||||
|
|| (blocks != null && blocks.toLowerCase(Locale.ROOT).contains(filter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param snippet the path of the snippet file the palette comes from, or null when the biome defines it itself
|
||||||
|
*/
|
||||||
|
record PaletteSummary(BiomePalette palette, String snippet, String blocks) {
|
||||||
|
String text() {
|
||||||
|
return snippet == null
|
||||||
|
? palette.label() + " · " + blocks
|
||||||
|
: palette.label() + " · " + snippet + " snippet · " + blocks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-7
@@ -16,13 +16,11 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.volmit.iris.engine.mantle;
|
package com.volmit.iris.core.gui;
|
||||||
|
|
||||||
import com.volmit.iris.util.mantle.MantleFlag;
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
import java.util.UUID;
|
||||||
public abstract class IrisMantleComponent implements MantleComponent {
|
|
||||||
private final EngineMantle engineMantle;
|
public record PaletteExplorerStart(Engine engine, UUID worldId, UUID playerId, String biomeKey, double centerX, double centerZ) {
|
||||||
private final MantleFlag flag;
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,500 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.ButtonGroup;
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JToggleButton;
|
||||||
|
import javax.swing.SwingConstants;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.GridLayout;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
|
||||||
|
final class PaletteInspectorPanel extends JPanel {
|
||||||
|
private static final long serialVersionUID = 4473102938518712001L;
|
||||||
|
private static final int PANEL_WIDTH = 310;
|
||||||
|
private static final Color HISTOGRAM_BAR = new Color(0x9D, 0xA0, 0xA8, 150);
|
||||||
|
private static final Color BAND_DIVIDER = new Color(255, 255, 255, 70);
|
||||||
|
|
||||||
|
private final Listener listener;
|
||||||
|
private final JLabel titleLabel = new JLabel();
|
||||||
|
private final JLabel detailLabel = new JLabel();
|
||||||
|
private final JLabel depthLabel = new JLabel("", SwingConstants.CENTER);
|
||||||
|
private final JButton shallowerButton = new JButton("‹");
|
||||||
|
private final JButton deeperButton = new JButton("›");
|
||||||
|
private final JPanel depthRow = new JPanel(new BorderLayout(StudioTheme.scale(6), 0));
|
||||||
|
private final StatisticsView statisticsView = new StatisticsView();
|
||||||
|
private PaletteLayerScale.View selectedView = PaletteLayerScale.View.BLOCKS;
|
||||||
|
private int depth;
|
||||||
|
private int deepestDepth;
|
||||||
|
|
||||||
|
PaletteInspectorPanel(Listener listener) {
|
||||||
|
super(new BorderLayout());
|
||||||
|
this.listener = listener;
|
||||||
|
setPreferredSize(StudioTheme.scaledSize(PANEL_WIDTH, 0));
|
||||||
|
setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, StudioTheme.SEPARATOR));
|
||||||
|
add(buildControls(), BorderLayout.NORTH);
|
||||||
|
add(StudioToolbar.scroll(statisticsView), BorderLayout.CENTER);
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showPreview(PaletteLayerPreview preview) {
|
||||||
|
titleLabel.setText(preview.biomeKey() + " · " + preview.palette().lowercaseLabel());
|
||||||
|
detailLabel.setText(placementText(preview));
|
||||||
|
depth = preview.depth();
|
||||||
|
deepestDepth = Math.max(0, preview.maxThickness() - 1);
|
||||||
|
refreshDepthControls();
|
||||||
|
statisticsView.show(preview);
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showTextureStatus(String status) {
|
||||||
|
statisticsView.showTextureStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
void hideInspector() {
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
PaletteLayerScale.View selectedView() {
|
||||||
|
return selectedView;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildControls() {
|
||||||
|
JPanel controls = new JPanel(new GridLayout(0, 1, 0, StudioTheme.scale(6)));
|
||||||
|
controls.setBorder(BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createMatteBorder(0, 0, 1, 0, StudioTheme.SEPARATOR),
|
||||||
|
StudioTheme.emptyBorder(12, 14, 12, 14)
|
||||||
|
));
|
||||||
|
titleLabel.setFont(StudioTheme.titleFont());
|
||||||
|
detailLabel.setForeground(StudioTheme.SECONDARY_TEXT);
|
||||||
|
detailLabel.setFont(StudioTheme.smallFont());
|
||||||
|
controls.add(titleLabel);
|
||||||
|
controls.add(detailLabel);
|
||||||
|
|
||||||
|
controls.add(sectionLabel("Show"));
|
||||||
|
controls.add(buildViewButtons());
|
||||||
|
controls.add(sectionLabel("Depth"));
|
||||||
|
controls.add(buildDepthRow());
|
||||||
|
return controls;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildViewButtons() {
|
||||||
|
JPanel row = new JPanel(new GridLayout(1, 0, StudioTheme.scale(4), 0));
|
||||||
|
row.setOpaque(false);
|
||||||
|
ButtonGroup group = new ButtonGroup();
|
||||||
|
for (PaletteLayerScale.View view : PaletteLayerScale.View.values()) {
|
||||||
|
JToggleButton button = new JToggleButton(view.toString());
|
||||||
|
button.setFocusable(false);
|
||||||
|
button.setSelected(view == selectedView);
|
||||||
|
button.addActionListener(event -> {
|
||||||
|
selectedView = view;
|
||||||
|
listener.onViewChanged(view);
|
||||||
|
});
|
||||||
|
group.add(button);
|
||||||
|
row.add(button);
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildDepthRow() {
|
||||||
|
depthRow.setOpaque(false);
|
||||||
|
shallowerButton.setToolTipText("Show the block one higher up");
|
||||||
|
deeperButton.setToolTipText("Show the block one further down");
|
||||||
|
shallowerButton.addActionListener(event -> changeDepth(-1));
|
||||||
|
deeperButton.addActionListener(event -> changeDepth(1));
|
||||||
|
depthRow.add(shallowerButton, BorderLayout.WEST);
|
||||||
|
depthRow.add(depthLabel, BorderLayout.CENTER);
|
||||||
|
depthRow.add(deeperButton, BorderLayout.EAST);
|
||||||
|
return depthRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void changeDepth(int step) {
|
||||||
|
int nextDepth = Math.clamp(depth + step, 0, deepestDepth);
|
||||||
|
if (nextDepth == depth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
depth = nextDepth;
|
||||||
|
refreshDepthControls();
|
||||||
|
listener.onDepthChanged(depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshDepthControls() {
|
||||||
|
depthLabel.setText(depthText(depth, deepestDepth));
|
||||||
|
shallowerButton.setEnabled(depth > 0);
|
||||||
|
deeperButton.setEnabled(depth < deepestDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JLabel sectionLabel(String text) {
|
||||||
|
JLabel label = new JLabel(text.toUpperCase(Locale.ROOT));
|
||||||
|
label.setForeground(StudioTheme.MUTED_TEXT);
|
||||||
|
label.setFont(StudioTheme.sectionFont());
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stacked layers divide their coordinates by the zoom twice before sampling; wall and slab blocks only once.
|
||||||
|
private static String placementText(PaletteLayerPreview preview) {
|
||||||
|
if (!preview.palette().kind().isStacked()) {
|
||||||
|
return "Single blocks · zoom " + formatZoom(preview.zoom());
|
||||||
|
}
|
||||||
|
return thicknessText(preview) + " · zoom " + formatZoom(preview.zoom())
|
||||||
|
+ " (×" + formatZoom(preview.zoom() * preview.zoom()) + " effective)";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String thicknessText(PaletteLayerPreview preview) {
|
||||||
|
if (preview.minThickness() == preview.maxThickness()) {
|
||||||
|
return preview.maxThickness() + (preview.maxThickness() == 1 ? " block thick" : " blocks thick");
|
||||||
|
}
|
||||||
|
return preview.minThickness() + "–" + preview.maxThickness() + " blocks thick";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String depthText(int depth, int deepestDepth) {
|
||||||
|
if (deepestDepth == 0) {
|
||||||
|
return "Top block only";
|
||||||
|
}
|
||||||
|
return depth == 0 ? "Top block" : depth + (depth == 1 ? " block down" : " blocks down");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatZoom(double zoom) {
|
||||||
|
return zoom == Math.rint(zoom) ? Long.toString((long) zoom) : String.format(Locale.ROOT, "%.2f", zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String percent(double share) {
|
||||||
|
if (share > 0D && share < 0.001D) {
|
||||||
|
return "<0.1%";
|
||||||
|
}
|
||||||
|
return String.format(Locale.ROOT, "%.1f%%", share * 100D);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
void onViewChanged(PaletteLayerScale.View view);
|
||||||
|
|
||||||
|
void onDepthChanged(int depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class StatisticsView extends JComponent {
|
||||||
|
private static final long serialVersionUID = -2318840125766150871L;
|
||||||
|
private static final int MARGIN = 14;
|
||||||
|
private static final int HISTOGRAM_HEIGHT = 84;
|
||||||
|
private static final int BAND_HEIGHT = 12;
|
||||||
|
private static final int SECTION_GAP = 18;
|
||||||
|
private static final int TITLE_GAP = 6;
|
||||||
|
private static final int NOTE_GAP = 4;
|
||||||
|
private static final String WIDEST_WEIGHT = "100.0%";
|
||||||
|
private static final String WIDEST_ACTUAL = "▲ 100.0%";
|
||||||
|
|
||||||
|
private PaletteLayerPreview preview;
|
||||||
|
private String textureStatus;
|
||||||
|
|
||||||
|
private StatisticsView() {
|
||||||
|
setOpaque(true);
|
||||||
|
setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void show(PaletteLayerPreview nextPreview) {
|
||||||
|
preview = nextPreview;
|
||||||
|
revalidate();
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showTextureStatus(String status) {
|
||||||
|
textureStatus = status;
|
||||||
|
revalidate();
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredSize() {
|
||||||
|
int preferredWidth = StudioTheme.scale(PANEL_WIDTH - 20);
|
||||||
|
if (preview == null) {
|
||||||
|
return new Dimension(preferredWidth, 0);
|
||||||
|
}
|
||||||
|
int margin = StudioTheme.scale(MARGIN);
|
||||||
|
int width = Math.max(getWidth(), preferredWidth) - margin * 2;
|
||||||
|
int height = margin + valueSpreadHeight() + StudioTheme.scale(SECTION_GAP) + blockTableHeight()
|
||||||
|
+ StudioTheme.scale(SECTION_GAP) + notesHeight(width) + margin;
|
||||||
|
return new Dimension(preferredWidth, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics graphics) {
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
canvas.setColor(StudioTheme.PANEL_BACKGROUND);
|
||||||
|
canvas.fillRect(0, 0, getWidth(), getHeight());
|
||||||
|
if (preview == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int margin = StudioTheme.scale(MARGIN);
|
||||||
|
int width = getWidth() - margin * 2;
|
||||||
|
int top = margin;
|
||||||
|
drawValueSpread(canvas, margin, top, width);
|
||||||
|
top += valueSpreadHeight() + StudioTheme.scale(SECTION_GAP);
|
||||||
|
drawBlockTable(canvas, margin, top, width);
|
||||||
|
top += blockTableHeight() + StudioTheme.scale(SECTION_GAP);
|
||||||
|
drawNotes(canvas, margin, top, width);
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int valueSpreadHeight() {
|
||||||
|
return sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP) + StudioTheme.scale(HISTOGRAM_HEIGHT)
|
||||||
|
+ StudioTheme.scale(2) + StudioTheme.scale(BAND_HEIGHT) + StudioTheme.scale(3) + smallMetrics().getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int blockTableHeight() {
|
||||||
|
return sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP) + rowHeight() * preview.entries().size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int notesHeight(int width) {
|
||||||
|
FontMetrics metrics = smallMetrics();
|
||||||
|
int height = 0;
|
||||||
|
for (String note : notes()) {
|
||||||
|
height += wrap(note, metrics, width).size() * metrics.getHeight() + StudioTheme.scale(NOTE_GAP);
|
||||||
|
}
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int rowHeight() {
|
||||||
|
return textMetrics().getHeight() + StudioTheme.scale(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FontMetrics textMetrics() {
|
||||||
|
return getFontMetrics(StudioTheme.font());
|
||||||
|
}
|
||||||
|
|
||||||
|
private FontMetrics smallMetrics() {
|
||||||
|
return getFontMetrics(StudioTheme.smallFont());
|
||||||
|
}
|
||||||
|
|
||||||
|
private FontMetrics sectionMetrics() {
|
||||||
|
return getFontMetrics(StudioTheme.sectionFont());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawValueSpread(Graphics2D canvas, int x, int top, int width) {
|
||||||
|
drawSectionTitle(canvas, "Noise values this layer produces", x, top + sectionMetrics().getAscent());
|
||||||
|
int histogramTop = top + sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP);
|
||||||
|
int histogramHeight = StudioTheme.scale(HISTOGRAM_HEIGHT);
|
||||||
|
int bandHeight = StudioTheme.scale(BAND_HEIGHT);
|
||||||
|
long[] histogram = preview.histogram();
|
||||||
|
long peak = 1L;
|
||||||
|
for (long count : histogram) {
|
||||||
|
peak = Math.max(peak, count);
|
||||||
|
}
|
||||||
|
canvas.setColor(HISTOGRAM_BAR);
|
||||||
|
for (int bin = 0; bin < histogram.length; bin++) {
|
||||||
|
int barLeft = x + (bin * width) / histogram.length;
|
||||||
|
int barRight = x + ((bin + 1) * width) / histogram.length;
|
||||||
|
int barHeight = (int) Math.round(histogramHeight * (histogram[bin] / (double) peak));
|
||||||
|
canvas.fillRect(barLeft, histogramTop + histogramHeight - barHeight, Math.max(1, barRight - barLeft - 1), barHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
int bandTop = histogramTop + histogramHeight + StudioTheme.scale(2);
|
||||||
|
for (PaletteLayerPreview.Entry entry : preview.entries()) {
|
||||||
|
if (Double.isNaN(entry.lowestNoise())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int bandLeft = x + (int) Math.round(entry.lowestNoise() * width);
|
||||||
|
int bandRight = x + (int) Math.round(entry.highestNoise() * width);
|
||||||
|
canvas.setColor(new Color(entry.color()));
|
||||||
|
canvas.fillRect(bandLeft, bandTop, Math.max(1, bandRight - bandLeft), bandHeight);
|
||||||
|
if (entry.highestNoise() < 1D) {
|
||||||
|
canvas.setColor(BAND_DIVIDER);
|
||||||
|
canvas.drawLine(bandRight, histogramTop, bandRight, bandTop + bandHeight - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.setFont(StudioTheme.smallFont());
|
||||||
|
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics();
|
||||||
|
int labelY = bandTop + bandHeight + StudioTheme.scale(3) + metrics.getAscent();
|
||||||
|
canvas.drawString("0", x, labelY);
|
||||||
|
canvas.drawString("0.5", x + (width - metrics.stringWidth("0.5")) / 2, labelY);
|
||||||
|
canvas.drawString("1", x + width - metrics.stringWidth("1"), labelY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawBlockTable(Graphics2D canvas, int x, int top, int width) {
|
||||||
|
int columnGap = StudioTheme.scale(10);
|
||||||
|
int actualRight = x + width;
|
||||||
|
int weightRight = actualRight - textMetrics().stringWidth(WIDEST_ACTUAL) - columnGap;
|
||||||
|
int nameRight = weightRight - textMetrics().stringWidth(WIDEST_WEIGHT) - columnGap;
|
||||||
|
int titleBaseline = top + sectionMetrics().getAscent();
|
||||||
|
drawSectionTitle(canvas, "Blocks", x, titleBaseline);
|
||||||
|
drawRightAligned(canvas, "weight", weightRight, titleBaseline);
|
||||||
|
drawRightAligned(canvas, "actual", actualRight, titleBaseline);
|
||||||
|
|
||||||
|
int rowHeight = rowHeight();
|
||||||
|
FontMetrics text = textMetrics();
|
||||||
|
int swatchSize = text.getAscent() - text.getDescent();
|
||||||
|
int rowTop = top + sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP);
|
||||||
|
for (PaletteLayerPreview.Entry entry : preview.entries()) {
|
||||||
|
int baseline = rowTop + (rowHeight + text.getAscent() - text.getDescent()) / 2;
|
||||||
|
drawSwatch(canvas, entry, x, rowTop + (rowHeight - swatchSize) / 2, swatchSize);
|
||||||
|
canvas.setFont(StudioTheme.font());
|
||||||
|
canvas.setColor(entry.found() ? StudioTheme.TEXT : StudioTheme.ERROR);
|
||||||
|
String name = entry.found() ? shortBlockName(entry.block()) : shortBlockName(entry.block()) + " (not found)";
|
||||||
|
int nameLeft = x + swatchSize + StudioTheme.scale(7);
|
||||||
|
canvas.drawString(fit(canvas, name, nameRight - nameLeft), nameLeft, baseline);
|
||||||
|
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
||||||
|
drawRightAligned(canvas, percent(entry.weightShare()), weightRight, baseline);
|
||||||
|
canvas.setColor(actualColor(entry));
|
||||||
|
drawRightAligned(canvas, actualText(entry), actualRight, baseline);
|
||||||
|
rowTop += rowHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawSwatch(Graphics2D canvas, PaletteLayerPreview.Entry entry, int x, int y, int size) {
|
||||||
|
if (entry.texture() != null) {
|
||||||
|
canvas.drawImage(entry.texture().toImage(), x, y, size, size, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
canvas.setColor(new Color(entry.color()));
|
||||||
|
int arc = StudioTheme.scale(3);
|
||||||
|
canvas.fillRoundRect(x, y, size, size, arc, arc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawNotes(Graphics2D canvas, int x, int top, int width) {
|
||||||
|
canvas.setFont(StudioTheme.smallFont());
|
||||||
|
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics();
|
||||||
|
int lineY = top + metrics.getAscent();
|
||||||
|
for (String note : notes()) {
|
||||||
|
for (String line : wrap(note, metrics, width)) {
|
||||||
|
canvas.drawString(line, x, lineY);
|
||||||
|
lineY += metrics.getHeight();
|
||||||
|
}
|
||||||
|
lineY += StudioTheme.scale(NOTE_GAP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> notes() {
|
||||||
|
List<String> notes = new ArrayList<>();
|
||||||
|
if (textureStatus != null) {
|
||||||
|
notes.add(textureStatus);
|
||||||
|
}
|
||||||
|
notes.add("▼ gets under half its weight, ▲ gets over double.");
|
||||||
|
notes.add(String.format(Locale.ROOT, "Measured on a %d × %d grid spread over %,.0f blocks around 0, 0.",
|
||||||
|
PaletteLayerPreview.STATISTICS_GRID_SIZE, PaletteLayerPreview.STATISTICS_GRID_SIZE,
|
||||||
|
PaletteLayerPreview.statisticsAreaBlocks()));
|
||||||
|
if (preview.absentShare() > 0D) {
|
||||||
|
notes.add(absentText(preview) + " That ground is dark on the map and left out of the shares.");
|
||||||
|
}
|
||||||
|
if (preview.outOfRangeShare() > 0D) {
|
||||||
|
notes.add(percent(preview.outOfRangeShare()) + " of the noise values fall outside 0 to 1."
|
||||||
|
+ " Those spots get the first block.");
|
||||||
|
}
|
||||||
|
if (preview.simplification() != null) {
|
||||||
|
notes.add(preview.simplification());
|
||||||
|
}
|
||||||
|
if (preview.hasSlopeCondition()) {
|
||||||
|
notes.add("This " + paletteNoun(preview) + " has a slope condition, which the preview ignores.");
|
||||||
|
}
|
||||||
|
return notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String absentText(PaletteLayerPreview preview) {
|
||||||
|
if (isDecorator(preview)) {
|
||||||
|
return "The decorator leaves " + percent(preview.absentShare()) + " of the ground bare.";
|
||||||
|
}
|
||||||
|
return "The layer doesn't reach this depth on " + percent(preview.absentShare()) + " of the ground.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String paletteNoun(PaletteLayerPreview preview) {
|
||||||
|
return isDecorator(preview) ? "decorator" : "layer";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isDecorator(PaletteLayerPreview preview) {
|
||||||
|
return preview.palette().kind() == BiomePalette.Kind.DECORATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String actualText(PaletteLayerPreview.Entry entry) {
|
||||||
|
if (entry.isFarBelowWeight()) {
|
||||||
|
return "▼ " + percent(entry.actualShare());
|
||||||
|
}
|
||||||
|
if (entry.isFarAboveWeight()) {
|
||||||
|
return "▲ " + percent(entry.actualShare());
|
||||||
|
}
|
||||||
|
return percent(entry.actualShare());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color actualColor(PaletteLayerPreview.Entry entry) {
|
||||||
|
return entry.isFarBelowWeight() || entry.isFarAboveWeight() ? StudioTheme.WARNING : StudioTheme.TEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawSectionTitle(Graphics2D canvas, String title, int x, int y) {
|
||||||
|
canvas.setFont(StudioTheme.sectionFont());
|
||||||
|
canvas.setColor(StudioTheme.MUTED_TEXT);
|
||||||
|
canvas.drawString(title.toUpperCase(Locale.ROOT), x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawRightAligned(Graphics2D canvas, String text, int right, int y) {
|
||||||
|
canvas.drawString(text, right - canvas.getFontMetrics().stringWidth(text), y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String shortBlockName(String block) {
|
||||||
|
return block.startsWith("minecraft:") ? block.substring("minecraft:".length()) : block;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fit(Graphics2D canvas, String text, int maxWidth) {
|
||||||
|
FontMetrics metrics = canvas.getFontMetrics();
|
||||||
|
if (metrics.stringWidth(text) <= maxWidth) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
String shortened = text;
|
||||||
|
while (shortened.length() > 1 && metrics.stringWidth(shortened + "…") > maxWidth) {
|
||||||
|
shortened = shortened.substring(0, shortened.length() - 1);
|
||||||
|
}
|
||||||
|
return shortened + "…";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> wrap(String text, FontMetrics metrics, int maxWidth) {
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
StringBuilder line = new StringBuilder();
|
||||||
|
for (String word : text.split(" ")) {
|
||||||
|
String candidate = line.isEmpty() ? word : line + " " + word;
|
||||||
|
if (metrics.stringWidth(candidate) > maxWidth && !line.isEmpty()) {
|
||||||
|
lines.add(line.toString());
|
||||||
|
line = new StringBuilder(word);
|
||||||
|
} else {
|
||||||
|
line = new StringBuilder(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!line.isEmpty()) {
|
||||||
|
lines.add(line.toString());
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,457 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
|
||||||
|
import com.volmit.iris.core.loader.IrisData;
|
||||||
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
|
import com.volmit.iris.engine.object.IrisBiome;
|
||||||
|
import com.volmit.iris.engine.object.IrisBiomePaletteLayer;
|
||||||
|
import com.volmit.iris.engine.object.IrisBlockData;
|
||||||
|
import com.volmit.iris.util.collection.KList;
|
||||||
|
import com.volmit.iris.util.math.RNG;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
final class PaletteLayerPreview {
|
||||||
|
static final int HISTOGRAM_BINS = 50;
|
||||||
|
static final int STATISTICS_GRID_SIZE = 64;
|
||||||
|
static final double STATISTICS_SPACING_BLOCKS = 37D;
|
||||||
|
private static final int MISSING_BLOCK_COLOR = 0x5A5F6B;
|
||||||
|
private static final double FAR_FROM_WEIGHT_FACTOR = 2D;
|
||||||
|
private static final int BLOCK_CACHE_SIZE = 1 << 16;
|
||||||
|
private static final int NO_OVERHANG = -1;
|
||||||
|
|
||||||
|
private final BiomePalette palette;
|
||||||
|
private final KList<IrisBlockData> blocks;
|
||||||
|
private final BiomePalettePlacement placement;
|
||||||
|
private final PaletteLayerPreview ground;
|
||||||
|
private final boolean[] showsGroundByEntry;
|
||||||
|
private final int depth;
|
||||||
|
private final IrisData data;
|
||||||
|
private final int[] entryBySlot;
|
||||||
|
private final BiomeTintField biomeTints;
|
||||||
|
private final BlockTexture[] textureByEntry;
|
||||||
|
private final BlockTexture[] upperHalfByEntry;
|
||||||
|
private final boolean hasTallPlants;
|
||||||
|
private final BlockTexture[] legendTextureByEntry;
|
||||||
|
private final int[] legendColorByEntry;
|
||||||
|
private final List<Entry> entries;
|
||||||
|
private final SampledBlock[] blockCache = new SampledBlock[BLOCK_CACHE_SIZE];
|
||||||
|
private final long[] histogram;
|
||||||
|
private final double absentShare;
|
||||||
|
private final double outOfRangeShare;
|
||||||
|
|
||||||
|
private PaletteLayerPreview(BiomePalette palette, BiomePalettePlacement placement, PaletteLayerPreview ground, int depth,
|
||||||
|
IrisData data, BlockTextures textures, BiomeTintField biomeTints) {
|
||||||
|
this.palette = palette;
|
||||||
|
this.blocks = placement.blocks();
|
||||||
|
this.placement = placement;
|
||||||
|
this.ground = ground;
|
||||||
|
this.depth = depth;
|
||||||
|
this.data = data;
|
||||||
|
this.entryBySlot = IrisBiomePaletteLayer.paletteEntryPerSlot(blocks, data).stream().mapToInt(Integer::intValue).toArray();
|
||||||
|
if (entryBySlot.length == 0) {
|
||||||
|
throw new IllegalStateException("No block of " + palette.biomeKey() + " " + palette.lowercaseLabel() + " could be found");
|
||||||
|
}
|
||||||
|
this.biomeTints = biomeTints;
|
||||||
|
this.showsGroundByEntry = resolveGroundShowingEntries();
|
||||||
|
this.textureByEntry = resolveEntryTextures(textures);
|
||||||
|
this.upperHalfByEntry = resolveUpperHalves(textures);
|
||||||
|
this.hasTallPlants = Arrays.stream(upperHalfByEntry).anyMatch(Objects::nonNull);
|
||||||
|
this.legendTextureByEntry = tintForLegend(legendTextures());
|
||||||
|
this.legendColorByEntry = resolveLegendColors();
|
||||||
|
|
||||||
|
Statistics statistics = measure();
|
||||||
|
this.histogram = statistics.histogram();
|
||||||
|
this.absentShare = statistics.absentShare();
|
||||||
|
this.outOfRangeShare = statistics.outOfRangeShare();
|
||||||
|
this.entries = describeEntries(statistics.sharesByEntry());
|
||||||
|
}
|
||||||
|
|
||||||
|
static PaletteLayerPreview load(Engine engine, BiomePalette palette, int depth, BlockTextures textures) {
|
||||||
|
if (engine == null || engine.isClosed()) {
|
||||||
|
throw new IllegalStateException("Open a studio world to preview pack palettes");
|
||||||
|
}
|
||||||
|
IrisData data = engine.getData();
|
||||||
|
IrisBiome biome = data.getBiomeLoader().load(palette.biomeKey());
|
||||||
|
if (biome == null || palette.blocksIn(biome) == null) {
|
||||||
|
throw new IllegalStateException("Palette removed during hotload: " + palette.biomeKey() + " " + palette.lowercaseLabel());
|
||||||
|
}
|
||||||
|
BiomePalettePlacement placement = BiomePalettePlacement.of(engine, biome, palette);
|
||||||
|
BiomeTintField biomeTints = textures == null
|
||||||
|
? null
|
||||||
|
: new BiomeTintField(biome, new RNG(engine.getSeedManager().getBiome()), textures.biomeColors());
|
||||||
|
PaletteLayerPreview ground = palette.kind() == BiomePalette.Kind.DECORATOR
|
||||||
|
? groundUnder(engine, palette.biomeKey(), biome, data, textures, biomeTints)
|
||||||
|
: null;
|
||||||
|
return new PaletteLayerPreview(palette, placement, ground, depth, data, textures, biomeTints);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decorations stand on the biome's top surface layer, so that is what shows around and between them.
|
||||||
|
private static PaletteLayerPreview groundUnder(Engine engine, String biomeKey, IrisBiome biome, IrisData data,
|
||||||
|
BlockTextures textures, BiomeTintField biomeTints) {
|
||||||
|
BiomePalette topLayer = new BiomePalette(biomeKey, BiomePalette.Kind.LAYER, 0);
|
||||||
|
KList<IrisBlockData> blocks = topLayer.blocksIn(biome);
|
||||||
|
if (blocks == null || blocks.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new PaletteLayerPreview(topLayer, BiomePalettePlacement.of(engine, biome, topLayer), null, 0, data, textures,
|
||||||
|
biomeTints);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The world picks one block per whole coordinate, and zoomed in, many pixels fall on the same block.
|
||||||
|
double sample(double x, double z) {
|
||||||
|
int blockX = (int) Math.floor(x);
|
||||||
|
int blockZ = (int) Math.floor(z);
|
||||||
|
int cacheSlot = blockCacheSlot(blockX, blockZ);
|
||||||
|
SampledBlock cached = blockCache[cacheSlot];
|
||||||
|
if (cached != null && cached.x() == blockX && cached.z() == blockZ) {
|
||||||
|
return cached.noise();
|
||||||
|
}
|
||||||
|
double noise = sampleBlock(blockX, blockZ);
|
||||||
|
blockCache[cacheSlot] = new SampledBlock(blockX, blockZ, noise);
|
||||||
|
return noise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double sampleBlock(int blockX, int blockZ) {
|
||||||
|
if (placement.thickness(blockX, blockZ) <= depth) {
|
||||||
|
return Double.NaN;
|
||||||
|
}
|
||||||
|
return placement.noise(blockX, blockZ, depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int blockCacheSlot(int blockX, int blockZ) {
|
||||||
|
long hash = blockX * 0x9E3779B97F4A7C15L ^ blockZ * 0xC2B2AE3D27D4EB4FL;
|
||||||
|
return (int) ((hash ^ (hash >>> 29)) & (BLOCK_CACHE_SIZE - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
int colorForNoise(double noise) {
|
||||||
|
return legendColorByEntry[entryIndexForNoise(noise)];
|
||||||
|
}
|
||||||
|
|
||||||
|
int blockColorAt(double noise, double worldX, double worldZ, boolean showTexturePixels) {
|
||||||
|
int overhang = showTexturePixels ? overhangColorAt(worldX, worldZ) : NO_OVERHANG;
|
||||||
|
if (overhang != NO_OVERHANG) {
|
||||||
|
return overhang;
|
||||||
|
}
|
||||||
|
int entry = entryIndexForNoise(noise);
|
||||||
|
if (showsGroundByEntry[entry]) {
|
||||||
|
return bareColorAt(worldX, worldZ, showTexturePixels);
|
||||||
|
}
|
||||||
|
BlockTexture texture = textureByEntry[entry];
|
||||||
|
if (texture == null) {
|
||||||
|
return legendColorByEntry[entry];
|
||||||
|
}
|
||||||
|
if (ground != null && showTexturePixels && !texture.isOpaqueAt(worldX, worldZ)) {
|
||||||
|
return groundColorAt(worldX, worldZ, true);
|
||||||
|
}
|
||||||
|
int untintedColor = showTexturePixels ? texture.texelAt(worldX, worldZ) : texture.averageColor();
|
||||||
|
return tinted(texture, untintedColor, worldX, worldZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The colour where the palette places nothing: the ground under a decorator, or plain dark otherwise.
|
||||||
|
*/
|
||||||
|
int bareColorAt(double worldX, double worldZ, boolean showTexturePixels) {
|
||||||
|
if (ground == null) {
|
||||||
|
return PaletteLayerScale.ABSENT_COLOR;
|
||||||
|
}
|
||||||
|
int overhang = showTexturePixels ? overhangColorAt(worldX, worldZ) : NO_OVERHANG;
|
||||||
|
return overhang != NO_OVERHANG ? overhang : groundColorAt(worldX, worldZ, showTexturePixels);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A two block tall plant stands up on the map: its top half covers the block above it, which is one block north.
|
||||||
|
private int overhangColorAt(double worldX, double worldZ) {
|
||||||
|
if (!hasTallPlants) {
|
||||||
|
return NO_OVERHANG;
|
||||||
|
}
|
||||||
|
double plantZ = worldZ + 1D;
|
||||||
|
double noise = sample(worldX, plantZ);
|
||||||
|
if (Double.isNaN(noise)) {
|
||||||
|
return NO_OVERHANG;
|
||||||
|
}
|
||||||
|
BlockTexture upperHalf = upperHalfByEntry[entryIndexForNoise(noise)];
|
||||||
|
if (upperHalf == null || !upperHalf.isOpaqueAt(worldX, worldZ)) {
|
||||||
|
return NO_OVERHANG;
|
||||||
|
}
|
||||||
|
return tinted(upperHalf, upperHalf.texelAt(worldX, worldZ), worldX, plantZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int tinted(BlockTexture texture, int untintedColor, double worldX, double worldZ) {
|
||||||
|
if (texture.biomeTint() == null) {
|
||||||
|
return untintedColor;
|
||||||
|
}
|
||||||
|
int tint = biomeTints.colorAt(texture.biomeTint(), (int) Math.floor(worldX), (int) Math.floor(worldZ));
|
||||||
|
return BlockTexture.multiply(untintedColor, tint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int groundColorAt(double worldX, double worldZ, boolean showTexturePixels) {
|
||||||
|
double noise = ground.sample(worldX, worldZ);
|
||||||
|
return Double.isNaN(noise) ? PaletteLayerScale.ABSENT_COLOR : ground.blockColorAt(noise, worldX, worldZ, showTexturePixels);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A decorator places air to leave gaps, which looks the same as placing nothing.
|
||||||
|
private boolean[] resolveGroundShowingEntries() {
|
||||||
|
boolean[] showsGround = new boolean[blocks.size()];
|
||||||
|
for (int entry = 0; entry < blocks.size(); entry++) {
|
||||||
|
BlockData block = blocks.get(entry).getBlockData(data);
|
||||||
|
showsGround[entry] = ground != null && block != null && block.getMaterial().isAir();
|
||||||
|
}
|
||||||
|
return showsGround;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entry entryForNoise(double noise) {
|
||||||
|
return entries.get(entryIndexForNoise(noise));
|
||||||
|
}
|
||||||
|
|
||||||
|
String biomeKey() {
|
||||||
|
return palette.biomeKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
BiomePalette palette() {
|
||||||
|
return palette;
|
||||||
|
}
|
||||||
|
|
||||||
|
String simplification() {
|
||||||
|
return placement.simplification();
|
||||||
|
}
|
||||||
|
|
||||||
|
int depth() {
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
int minThickness() {
|
||||||
|
return placement.minThickness();
|
||||||
|
}
|
||||||
|
|
||||||
|
int maxThickness() {
|
||||||
|
return placement.maxThickness();
|
||||||
|
}
|
||||||
|
|
||||||
|
double zoom() {
|
||||||
|
return placement.zoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasSlopeCondition() {
|
||||||
|
return placement.hasSlopeCondition();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Entry> entries() {
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
long[] histogram() {
|
||||||
|
return histogram.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
double absentShare() {
|
||||||
|
return absentShare;
|
||||||
|
}
|
||||||
|
|
||||||
|
double outOfRangeShare() {
|
||||||
|
return outOfRangeShare;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double statisticsAreaBlocks() {
|
||||||
|
return STATISTICS_GRID_SIZE * STATISTICS_SPACING_BLOCKS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int entryIndexForNoise(double noise) {
|
||||||
|
return entryBySlot[IrisBiomePaletteLayer.slotForNoise(noise, entryBySlot.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private BlockTexture[] resolveEntryTextures(BlockTextures textures) {
|
||||||
|
BlockTexture[] resolved = new BlockTexture[blocks.size()];
|
||||||
|
if (textures == null) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
for (int entry = 0; entry < blocks.size(); entry++) {
|
||||||
|
BlockData block = blocks.get(entry).getBlockData(data);
|
||||||
|
resolved[entry] = block == null ? null : textureOf(block, textures).orElse(null);
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BlockTexture[] resolveUpperHalves(BlockTextures textures) {
|
||||||
|
BlockTexture[] resolved = new BlockTexture[blocks.size()];
|
||||||
|
if (textures == null || palette.kind() != BiomePalette.Kind.DECORATOR) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
for (int entry = 0; entry < blocks.size(); entry++) {
|
||||||
|
BlockData block = blocks.get(entry).getBlockData(data);
|
||||||
|
resolved[entry] = block == null ? null : textures.upperHalfIcon(block).orElse(null);
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tall plants are known by their top half, the way Minecraft draws them in the inventory.
|
||||||
|
private BlockTexture[] legendTextures() {
|
||||||
|
BlockTexture[] legend = textureByEntry.clone();
|
||||||
|
for (int entry = 0; entry < legend.length; entry++) {
|
||||||
|
if (upperHalfByEntry[entry] != null) {
|
||||||
|
legend[entry] = upperHalfByEntry[entry];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return legend;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<BlockTexture> textureOf(BlockData block, BlockTextures textures) {
|
||||||
|
return palette.kind() == BiomePalette.Kind.DECORATOR ? textures.iconTexture(block) : textures.topTexture(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The legend shows tinted blocks as they look at the world origin, where the statistics are sampled too.
|
||||||
|
private BlockTexture[] tintForLegend(BlockTexture[] textures) {
|
||||||
|
BlockTexture[] tinted = new BlockTexture[textures.length];
|
||||||
|
for (int entry = 0; entry < textures.length; entry++) {
|
||||||
|
BlockTexture texture = textures[entry];
|
||||||
|
tinted[entry] = texture == null || texture.biomeTint() == null
|
||||||
|
? texture
|
||||||
|
: texture.tinted(biomeTints.colorAt(texture.biomeTint(), 0, 0));
|
||||||
|
}
|
||||||
|
return tinted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blocks without a texture, like custom blocks or everything before the textures load, fall back to map colours.
|
||||||
|
private int[] resolveLegendColors() {
|
||||||
|
List<BlockData> untexturedBlocks = new ArrayList<>();
|
||||||
|
for (int entry = 0; entry < blocks.size(); entry++) {
|
||||||
|
BlockData block = blocks.get(entry).getBlockData(data);
|
||||||
|
if (block != null && textureByEntry[entry] == null) {
|
||||||
|
untexturedBlocks.add(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, Integer> mapColorsByBlock = BlockPreviewColors.colorsByBlock(untexturedBlocks);
|
||||||
|
int[] colors = new int[blocks.size()];
|
||||||
|
for (int entry = 0; entry < blocks.size(); entry++) {
|
||||||
|
BlockData block = blocks.get(entry).getBlockData(data);
|
||||||
|
if (block == null) {
|
||||||
|
colors[entry] = MISSING_BLOCK_COLOR;
|
||||||
|
} else if (legendTextureByEntry[entry] != null) {
|
||||||
|
colors[entry] = legendTextureByEntry[entry].averageColor();
|
||||||
|
} else {
|
||||||
|
colors[entry] = mapColorsByBlock.get(block.getAsString(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Statistics measure() {
|
||||||
|
long[] values = new long[HISTOGRAM_BINS];
|
||||||
|
long[] samplesByEntry = new long[blocks.size()];
|
||||||
|
long present = 0;
|
||||||
|
long absent = 0;
|
||||||
|
long outOfRange = 0;
|
||||||
|
double origin = -statisticsAreaBlocks() / 2D;
|
||||||
|
for (int gridX = 0; gridX < STATISTICS_GRID_SIZE; gridX++) {
|
||||||
|
for (int gridZ = 0; gridZ < STATISTICS_GRID_SIZE; gridZ++) {
|
||||||
|
double noise = sample(origin + gridX * STATISTICS_SPACING_BLOCKS, origin + gridZ * STATISTICS_SPACING_BLOCKS);
|
||||||
|
if (Double.isNaN(noise)) {
|
||||||
|
absent++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
present++;
|
||||||
|
if (noise < 0D || noise > 1D) {
|
||||||
|
outOfRange++;
|
||||||
|
}
|
||||||
|
values[Math.clamp((int) (noise * HISTOGRAM_BINS), 0, HISTOGRAM_BINS - 1)]++;
|
||||||
|
samplesByEntry[entryIndexForNoise(noise)]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double[] sharesByEntry = new double[samplesByEntry.length];
|
||||||
|
for (int entry = 0; entry < samplesByEntry.length; entry++) {
|
||||||
|
sharesByEntry[entry] = present == 0 ? 0D : samplesByEntry[entry] / (double) present;
|
||||||
|
}
|
||||||
|
long total = present + absent;
|
||||||
|
return new Statistics(values, sharesByEntry, absent / (double) total, present == 0 ? 0D : outOfRange / (double) present);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Entry> describeEntries(double[] sharesByEntry) {
|
||||||
|
int slotCount = entryBySlot.length;
|
||||||
|
List<Entry> described = new ArrayList<>(blocks.size());
|
||||||
|
for (int entry = 0; entry < blocks.size(); entry++) {
|
||||||
|
IrisBlockData block = blocks.get(entry);
|
||||||
|
int firstSlot = firstSlotOf(entry);
|
||||||
|
int lastSlot = lastSlotOf(entry);
|
||||||
|
boolean placed = firstSlot >= 0;
|
||||||
|
described.add(new Entry(
|
||||||
|
block.getBlock(),
|
||||||
|
block.getWeight(),
|
||||||
|
block.getBlockData(data) != null,
|
||||||
|
legendColorByEntry[entry],
|
||||||
|
legendTextureByEntry[entry],
|
||||||
|
placed ? (lastSlot - firstSlot + 1) / (double) slotCount : 0D,
|
||||||
|
sharesByEntry[entry],
|
||||||
|
placed ? slotLowerBound(firstSlot, slotCount) : Double.NaN,
|
||||||
|
placed ? slotUpperBound(lastSlot, slotCount) : Double.NaN
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return List.copyOf(described);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int firstSlotOf(int entry) {
|
||||||
|
for (int slot = 0; slot < entryBySlot.length; slot++) {
|
||||||
|
if (entryBySlot[slot] == entry) {
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int lastSlotOf(int entry) {
|
||||||
|
for (int slot = entryBySlot.length - 1; slot >= 0; slot--) {
|
||||||
|
if (entryBySlot[slot] == entry) {
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slots are picked by rounding, so the first and last slot only own half a step of the value range.
|
||||||
|
private static double slotLowerBound(int slot, int slotCount) {
|
||||||
|
return slot == 0 ? 0D : (slot - 0.5D) / (slotCount - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double slotUpperBound(int slot, int slotCount) {
|
||||||
|
return slot == slotCount - 1 ? 1D : (slot + 0.5D) / (slotCount - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
record Entry(String block, int weight, boolean found, int color, BlockTexture texture, double weightShare,
|
||||||
|
double actualShare, double lowestNoise, double highestNoise) {
|
||||||
|
boolean isFarBelowWeight() {
|
||||||
|
return found && actualShare < weightShare / FAR_FROM_WEIGHT_FACTOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isFarAboveWeight() {
|
||||||
|
return found && actualShare > weightShare * FAR_FROM_WEIGHT_FACTOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record SampledBlock(int x, int z, double noise) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Statistics(long[] histogram, double[] sharesByEntry, double absentShare, double outOfRangeShare) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
|
||||||
|
final class PaletteLayerScale implements NoiseColorScale {
|
||||||
|
static final int ABSENT_COLOR = 0x161A24;
|
||||||
|
private static final double OVERLAY_DARKEST_SHADE = 0.3D;
|
||||||
|
|
||||||
|
private final View view;
|
||||||
|
private final PaletteLayerPreview preview;
|
||||||
|
private final NoisePalette noisePalette;
|
||||||
|
private final boolean showTexturePixels;
|
||||||
|
|
||||||
|
PaletteLayerScale(View view, PaletteLayerPreview preview, NoisePalette noisePalette, boolean showTexturePixels) {
|
||||||
|
this.view = view;
|
||||||
|
this.preview = preview;
|
||||||
|
this.noisePalette = noisePalette;
|
||||||
|
this.showTexturePixels = showTexturePixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int colorAt(double value, double worldX, double worldZ) {
|
||||||
|
if (view == View.NOISE) {
|
||||||
|
return noisePalette.colorFinite(value);
|
||||||
|
}
|
||||||
|
int blockColor = preview.blockColorAt(value, worldX, worldZ, showTexturePixels);
|
||||||
|
return view == View.BLOCKS ? blockColor : BlockPreviewColors.shade(blockColor, overlayShade(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int colorFinite(double value) {
|
||||||
|
return switch (view) {
|
||||||
|
case BLOCKS -> preview.colorForNoise(value);
|
||||||
|
case NOISE -> noisePalette.colorFinite(value);
|
||||||
|
case BLOCKS_SHADED_BY_NOISE -> BlockPreviewColors.shade(preview.colorForNoise(value), overlayShade(value));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int colorInvalid() {
|
||||||
|
return ABSENT_COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int colorInvalidAt(double worldX, double worldZ) {
|
||||||
|
return view == View.NOISE ? ABSENT_COLOR : preview.bareColorAt(worldX, worldZ, showTexturePixels);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Color displayColorNormalized(double normalized) {
|
||||||
|
return new Color(colorFinite(minimum() + normalized * (maximum() - minimum())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String label() {
|
||||||
|
return view == View.NOISE ? noisePalette.label() : view.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double minimum() {
|
||||||
|
return view == View.NOISE ? noisePalette.minimum() : 0D;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double maximum() {
|
||||||
|
return view == View.NOISE ? noisePalette.maximum() : 1D;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double overlayShade(double value) {
|
||||||
|
return OVERLAY_DARKEST_SHADE + (1D - OVERLAY_DARKEST_SHADE) * Math.clamp(value, 0D, 1D);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum View {
|
||||||
|
BLOCKS("Blocks"),
|
||||||
|
NOISE("Noise"),
|
||||||
|
BLOCKS_SHADED_BY_NOISE("Both");
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
View(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,527 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.PaletteCatalog.BiomeEntry;
|
||||||
|
import com.volmit.iris.core.gui.PaletteCatalog.BiomeGroup;
|
||||||
|
import com.volmit.iris.core.gui.PaletteCatalog.RegionEntry;
|
||||||
|
import com.volmit.iris.core.gui.PlayerLocationTracker.PlayerLocation;
|
||||||
|
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.ButtonGroup;
|
||||||
|
import javax.swing.DefaultListCellRenderer;
|
||||||
|
import javax.swing.DefaultListModel;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JList;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JTextField;
|
||||||
|
import javax.swing.JToggleButton;
|
||||||
|
import javax.swing.JTree;
|
||||||
|
import javax.swing.ListSelectionModel;
|
||||||
|
import javax.swing.event.DocumentEvent;
|
||||||
|
import javax.swing.event.DocumentListener;
|
||||||
|
import javax.swing.tree.DefaultMutableTreeNode;
|
||||||
|
import javax.swing.tree.DefaultTreeCellRenderer;
|
||||||
|
import javax.swing.tree.DefaultTreeModel;
|
||||||
|
import javax.swing.tree.TreePath;
|
||||||
|
import javax.swing.tree.TreeSelectionModel;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.CardLayout;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.GridLayout;
|
||||||
|
import java.awt.Rectangle;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
|
||||||
|
final class PaletteNavigator extends JPanel {
|
||||||
|
private static final long serialVersionUID = -6402313718430917284L;
|
||||||
|
private static final String HERE_CARD = "here";
|
||||||
|
private static final String OVERVIEW_CARD = "overview";
|
||||||
|
private static final int MESSAGE_WRAP_WIDTH_PIXELS = 190;
|
||||||
|
|
||||||
|
private final Listener listener;
|
||||||
|
private final CardLayout cards = new CardLayout();
|
||||||
|
private final JPanel cardPanel = new JPanel(cards);
|
||||||
|
private final JLabel regionLabel = new JLabel();
|
||||||
|
private final DefaultListModel<HereRow> hereRows = new DefaultListModel<>();
|
||||||
|
private final JList<HereRow> hereList = new JList<>(hereRows);
|
||||||
|
private final JTextField searchField = new JTextField();
|
||||||
|
private final DefaultTreeModel treeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
|
||||||
|
private final JTree overviewTree = new JTree(treeModel);
|
||||||
|
|
||||||
|
private PaletteCatalog catalog;
|
||||||
|
private PlayerLocation location;
|
||||||
|
private BiomePalette selectedPalette;
|
||||||
|
private boolean updatingSelection;
|
||||||
|
private boolean untracked;
|
||||||
|
|
||||||
|
PaletteNavigator(Listener listener) {
|
||||||
|
super(new BorderLayout());
|
||||||
|
this.listener = listener;
|
||||||
|
cardPanel.add(buildHereCard(), HERE_CARD);
|
||||||
|
cardPanel.add(buildOverviewCard(), OVERVIEW_CARD);
|
||||||
|
add(buildTabBar(), BorderLayout.NORTH);
|
||||||
|
add(cardPanel, BorderLayout.CENTER);
|
||||||
|
refreshHere();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showCatalog(PaletteCatalog nextCatalog) {
|
||||||
|
catalog = nextCatalog;
|
||||||
|
rebuildOverview();
|
||||||
|
refreshHere();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showLocation(PlayerLocation nextLocation) {
|
||||||
|
location = nextLocation;
|
||||||
|
refreshHere();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without a player there is nowhere to call "here", so the Here tab explains how to get one instead.
|
||||||
|
void showUntracked() {
|
||||||
|
untracked = true;
|
||||||
|
refreshHere();
|
||||||
|
}
|
||||||
|
|
||||||
|
void markSelected(BiomePalette palette) {
|
||||||
|
selectedPalette = palette;
|
||||||
|
updatingSelection = true;
|
||||||
|
try {
|
||||||
|
selectInHereList();
|
||||||
|
selectInTree();
|
||||||
|
} finally {
|
||||||
|
updatingSelection = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildTabBar() {
|
||||||
|
JPanel bar = new JPanel(new GridLayout(1, 2, 0, 0));
|
||||||
|
bar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, StudioTheme.SEPARATOR));
|
||||||
|
ButtonGroup group = new ButtonGroup();
|
||||||
|
JToggleButton here = createTab("Here", HERE_CARD, group);
|
||||||
|
JToggleButton overview = createTab("Overview", OVERVIEW_CARD, group);
|
||||||
|
bar.add(here);
|
||||||
|
bar.add(overview);
|
||||||
|
here.doClick();
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JToggleButton createTab(String text, String card, ButtonGroup group) {
|
||||||
|
JToggleButton tab = StudioToolbar.tab(text);
|
||||||
|
tab.addActionListener(event -> cards.show(cardPanel, card));
|
||||||
|
group.add(tab);
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildHereCard() {
|
||||||
|
JPanel card = new JPanel(new BorderLayout());
|
||||||
|
JPanel location = new JPanel(new GridLayout(0, 1, 0, StudioTheme.scale(3)));
|
||||||
|
location.setBorder(StudioTheme.emptyBorder(9, 12, 5, 12));
|
||||||
|
location.add(regionLabel);
|
||||||
|
|
||||||
|
hereList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||||
|
hereList.setCellRenderer(new HereRowRenderer());
|
||||||
|
hereList.addListSelectionListener(event -> {
|
||||||
|
HereRow row = hereList.getSelectedValue();
|
||||||
|
if (!event.getValueIsAdjusting() && !updatingSelection && row != null && row.biomeKey() != null) {
|
||||||
|
choose(row.biomeKey(), row.palette());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
card.add(location, BorderLayout.NORTH);
|
||||||
|
card.add(StudioToolbar.scroll(hereList), BorderLayout.CENTER);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildOverviewCard() {
|
||||||
|
JPanel card = new JPanel(new BorderLayout());
|
||||||
|
StudioToolbar.makeSearchField(searchField, "Search biomes, snippets or blocks...");
|
||||||
|
searchField.getDocument().addDocumentListener(new DocumentListener() {
|
||||||
|
@Override
|
||||||
|
public void insertUpdate(DocumentEvent event) {
|
||||||
|
rebuildOverview();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeUpdate(DocumentEvent event) {
|
||||||
|
rebuildOverview();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void changedUpdate(DocumentEvent event) {
|
||||||
|
rebuildOverview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
overviewTree.setRootVisible(false);
|
||||||
|
overviewTree.setShowsRootHandles(true);
|
||||||
|
overviewTree.setRowHeight(StudioTheme.scale(24));
|
||||||
|
overviewTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
|
||||||
|
overviewTree.setCellRenderer(new OverviewRenderer());
|
||||||
|
overviewTree.addTreeSelectionListener(event -> {
|
||||||
|
if (!updatingSelection && nodeAt(event.getPath()) instanceof OverviewNode node && node.biomeKey() != null) {
|
||||||
|
choose(node.biomeKey(), node.palette());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
card.add(StudioToolbar.padded(searchField, 8, 8, 8, 8), BorderLayout.NORTH);
|
||||||
|
card.add(StudioToolbar.scroll(overviewTree), BorderLayout.CENTER);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choosing a biome rather than one of its palettes opens its first palette.
|
||||||
|
private void choose(String biomeKey, BiomePalette palette) {
|
||||||
|
BiomeEntry biome = catalog == null ? null : catalog.biome(biomeKey);
|
||||||
|
BiomePalette chosen = palette != null ? palette : biome == null ? null : biome.firstPalette();
|
||||||
|
if (chosen != null) {
|
||||||
|
listener.onPaletteChosen(chosen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshHere() {
|
||||||
|
updatingSelection = true;
|
||||||
|
try {
|
||||||
|
hereRows.clear();
|
||||||
|
if (untracked) {
|
||||||
|
showHereMessage("Run /iris std pal in-game to see the palettes where you stand.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (location == null || catalog == null) {
|
||||||
|
showHereMessage("Finding where you are...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (location.status()) {
|
||||||
|
case OFFLINE -> showHereMessage("You are offline.");
|
||||||
|
case IN_OTHER_WORLD -> showHereMessage("You left the world this window shows.");
|
||||||
|
case IN_REGION -> showHereRows(location);
|
||||||
|
}
|
||||||
|
selectInHereList();
|
||||||
|
} finally {
|
||||||
|
updatingSelection = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showHereMessage(String message) {
|
||||||
|
regionLabel.setText("<html><body style='width:" + StudioTheme.scale(MESSAGE_WRAP_WIDTH_PIXELS) + "px'>" + message + "</body></html>");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showHereRows(PlayerLocation here) {
|
||||||
|
RegionEntry region = catalog.region(here.regionKey());
|
||||||
|
regionLabel.setText(labeled("Region", region == null ? here.regionKey() : region.name(), here.regionKey()));
|
||||||
|
addBiomePalettes("Surface biome here", here.surfaceBiomeKey());
|
||||||
|
if (here.isBelowSurfaceBiome()) {
|
||||||
|
addBiomePalettes("Underground biome at your height", here.biomeAtPositionKey());
|
||||||
|
}
|
||||||
|
if (region == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hereRows.addElement(HereRow.header("Biomes in this region"));
|
||||||
|
for (String biomeKey : region.allBiomeKeys()) {
|
||||||
|
addRegionBiome(biomeKey, 0, new HashSet<>(), here);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addBiomePalettes(String title, String biomeKey) {
|
||||||
|
BiomeEntry biome = catalog.biome(biomeKey);
|
||||||
|
if (biome == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hereRows.addElement(HereRow.header(title));
|
||||||
|
hereRows.addElement(HereRow.biome(biomeText(biome, childOfText(biome)), biomeKey, 0));
|
||||||
|
addPaletteRows(biome, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addPaletteRows(BiomeEntry biome, int nesting) {
|
||||||
|
for (PaletteCatalog.PaletteSummary summary : biome.palettes()) {
|
||||||
|
hereRows.addElement(HereRow.palette(summary.text(), summary.palette(), nesting));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Children sit under the biome they grow inside of, so a child biome is found where it actually appears.
|
||||||
|
private void addRegionBiome(String biomeKey, int nesting, Set<String> ancestors, PlayerLocation here) {
|
||||||
|
BiomeEntry biome = catalog.biome(biomeKey);
|
||||||
|
if (biome == null || !ancestors.add(biomeKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean standingHere = biomeKey.equals(here.surfaceBiomeKey()) || biomeKey.equals(here.biomeAtPositionKey());
|
||||||
|
hereRows.addElement(HereRow.biome(biomeText(biome, standingHere ? "you are here" : null), biomeKey, nesting));
|
||||||
|
addPaletteRows(biome, nesting);
|
||||||
|
for (String childKey : biome.childKeys()) {
|
||||||
|
addRegionBiome(childKey, nesting + 1, new HashSet<>(ancestors), here);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String childOfText(BiomeEntry biome) {
|
||||||
|
if (biome.parentKeys().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<String> parentNames = biome.parentKeys().stream()
|
||||||
|
.map(key -> catalog.biome(key) == null ? key : catalog.biome(key).name())
|
||||||
|
.toList();
|
||||||
|
return "child of " + String.join(", ", parentNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rebuildOverview() {
|
||||||
|
if (catalog == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String filter = searchField.getText().trim().toLowerCase(Locale.ROOT);
|
||||||
|
DefaultMutableTreeNode root = new DefaultMutableTreeNode();
|
||||||
|
for (RegionEntry region : catalog.regions()) {
|
||||||
|
boolean regionMatches = !filter.isEmpty() && region.name().toLowerCase(Locale.ROOT).contains(filter);
|
||||||
|
DefaultMutableTreeNode regionNode = new DefaultMutableTreeNode(new OverviewNode(region.name(), region.key(), null, null));
|
||||||
|
for (BiomeGroup group : region.groups()) {
|
||||||
|
DefaultMutableTreeNode groupNode = new DefaultMutableTreeNode();
|
||||||
|
for (String biomeKey : group.biomeKeys()) {
|
||||||
|
addBiomeNode(groupNode, biomeKey, regionMatches ? "" : filter, new HashSet<>());
|
||||||
|
}
|
||||||
|
if (groupNode.getChildCount() > 0) {
|
||||||
|
groupNode.setUserObject(new OverviewNode(group.label(), groupNode.getChildCount() + " biomes", null, null));
|
||||||
|
regionNode.add(groupNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (regionNode.getChildCount() > 0) {
|
||||||
|
root.add(regionNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DefaultMutableTreeNode outside = new DefaultMutableTreeNode();
|
||||||
|
for (String biomeKey : catalog.biomesOutsideRegions()) {
|
||||||
|
addBiomeNode(outside, biomeKey, filter, new HashSet<>());
|
||||||
|
}
|
||||||
|
if (outside.getChildCount() > 0) {
|
||||||
|
outside.setUserObject(new OverviewNode("Not in any region", outside.getChildCount() + " biomes", null, null));
|
||||||
|
root.add(outside);
|
||||||
|
}
|
||||||
|
addSnippetNodes(root, filter);
|
||||||
|
updatingSelection = true;
|
||||||
|
try {
|
||||||
|
treeModel.setRoot(root);
|
||||||
|
if (!filter.isEmpty()) {
|
||||||
|
expandDownToBiomes(root);
|
||||||
|
}
|
||||||
|
selectInTree();
|
||||||
|
} finally {
|
||||||
|
updatingSelection = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addBiomeNode(DefaultMutableTreeNode parent, String biomeKey, String filter, Set<String> ancestors) {
|
||||||
|
BiomeEntry biome = catalog.biome(biomeKey);
|
||||||
|
if (biome == null || !ancestors.add(biomeKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DefaultMutableTreeNode biomeNode = new DefaultMutableTreeNode(new OverviewNode(biome.name(), biomeKey, biomeKey, null));
|
||||||
|
for (PaletteCatalog.PaletteSummary summary : biome.palettes()) {
|
||||||
|
biomeNode.add(new DefaultMutableTreeNode(new OverviewNode(summary.text(), null, biomeKey, summary.palette())));
|
||||||
|
}
|
||||||
|
for (String childKey : biome.childKeys()) {
|
||||||
|
addBiomeNode(biomeNode, childKey, filter, new HashSet<>(ancestors));
|
||||||
|
}
|
||||||
|
boolean hasMatchingChild = biomeNode.getChildCount() > biome.palettes().size();
|
||||||
|
if (filter.isEmpty() || biome.matches(filter) || hasMatchingChild) {
|
||||||
|
parent.add(biomeNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snippets sit below the regions, so a palette picked elsewhere is still found in its biome first.
|
||||||
|
private void addSnippetNodes(DefaultMutableTreeNode root, String filter) {
|
||||||
|
for (BiomePalette.SnippetFolder folder : BiomePalette.SnippetFolder.values()) {
|
||||||
|
DefaultMutableTreeNode folderNode = new DefaultMutableTreeNode();
|
||||||
|
catalog.snippets().stream()
|
||||||
|
.filter(snippet -> snippet.folder() == folder && (filter.isEmpty() || snippet.matches(filter)))
|
||||||
|
.forEach(snippet -> folderNode.add(snippetNode(snippet)));
|
||||||
|
if (folderNode.getChildCount() > 0) {
|
||||||
|
folderNode.setUserObject(new OverviewNode(folder.label() + " snippets", folderNode.getChildCount() + " files", null, null));
|
||||||
|
root.add(folderNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Picking a snippet opens it where the first biome using it places it; its children open the other uses.
|
||||||
|
private DefaultMutableTreeNode snippetNode(PaletteCatalog.SnippetEntry snippet) {
|
||||||
|
if (snippet.uses().isEmpty()) {
|
||||||
|
return new DefaultMutableTreeNode(new OverviewNode(snippet.path(), "not used by any biome", null, null));
|
||||||
|
}
|
||||||
|
BiomePalette firstUse = snippet.uses().get(0);
|
||||||
|
String usedBy = snippet.uses().size() == 1 ? "used once" : "used " + snippet.uses().size() + " times";
|
||||||
|
DefaultMutableTreeNode node = new DefaultMutableTreeNode(new OverviewNode(snippet.path() + " · " + snippet.blocks(),
|
||||||
|
usedBy, firstUse.biomeKey(), firstUse));
|
||||||
|
for (BiomePalette use : snippet.uses()) {
|
||||||
|
BiomeEntry biome = catalog.biome(use.biomeKey());
|
||||||
|
String biomeName = biome == null ? use.biomeKey() : biome.name();
|
||||||
|
node.add(new DefaultMutableTreeNode(new OverviewNode(biomeName + " · " + use.lowercaseLabel(), use.biomeKey(),
|
||||||
|
use.biomeKey(), use)));
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void expandDownToBiomes(DefaultMutableTreeNode root) {
|
||||||
|
Enumeration<?> nodes = root.breadthFirstEnumeration();
|
||||||
|
while (nodes.hasMoreElements()) {
|
||||||
|
DefaultMutableTreeNode node = (DefaultMutableTreeNode) nodes.nextElement();
|
||||||
|
if (node.getUserObject() instanceof OverviewNode overview && overview.biomeKey() != null) {
|
||||||
|
overviewTree.expandPath(new TreePath(((DefaultMutableTreeNode) node.getParent()).getPath()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectInTree() {
|
||||||
|
if (selectedNodeShowsSelectedPalette()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DefaultMutableTreeNode match = findLayerNode((DefaultMutableTreeNode) treeModel.getRoot());
|
||||||
|
if (match == null) {
|
||||||
|
overviewTree.clearSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TreePath path = new TreePath(match.getPath());
|
||||||
|
overviewTree.setSelectionPath(path);
|
||||||
|
overviewTree.makeVisible(path);
|
||||||
|
Rectangle row = overviewTree.getPathBounds(path);
|
||||||
|
if (row != null) {
|
||||||
|
overviewTree.scrollRectToVisible(new Rectangle(0, row.y, 1, row.height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same palette can be listed under its biome and under its snippet; the row the user picked stays picked.
|
||||||
|
private boolean selectedNodeShowsSelectedPalette() {
|
||||||
|
return nodeAt(overviewTree.getSelectionPath()) instanceof OverviewNode node
|
||||||
|
&& node.palette() != null
|
||||||
|
&& node.palette().equals(selectedPalette);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultMutableTreeNode findLayerNode(DefaultMutableTreeNode root) {
|
||||||
|
Enumeration<?> nodes = root.depthFirstEnumeration();
|
||||||
|
while (nodes.hasMoreElements()) {
|
||||||
|
DefaultMutableTreeNode node = (DefaultMutableTreeNode) nodes.nextElement();
|
||||||
|
if (node.getUserObject() instanceof OverviewNode overview
|
||||||
|
&& overview.palette() != null
|
||||||
|
&& overview.palette().equals(selectedPalette)) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectInHereList() {
|
||||||
|
for (int index = 0; index < hereRows.size(); index++) {
|
||||||
|
HereRow row = hereRows.get(index);
|
||||||
|
if (row.palette() != null && row.palette().equals(selectedPalette)) {
|
||||||
|
hereList.setSelectedIndex(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hereList.clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object nodeAt(TreePath path) {
|
||||||
|
return path == null ? null : ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String biomeText(BiomeEntry biome, String note) {
|
||||||
|
String noteText = note == null ? "" : " · " + escape(note);
|
||||||
|
return "<html>" + escape(biome.name()) + noteText + " <font color='" + StudioTheme.SECONDARY_TEXT.toHtml() + "'>" + escape(biome.key())
|
||||||
|
+ "</font></html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String labeled(String title, String name, String key) {
|
||||||
|
return "<html><font color='" + StudioTheme.SECONDARY_TEXT.toHtml() + "'>" + title + "</font> " + escape(name)
|
||||||
|
+ " <font color='" + StudioTheme.SECONDARY_TEXT.toHtml() + "'>" + escape(key) + "</font></html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escape(String text) {
|
||||||
|
return text.replace("&", "&").replace("<", "<").replace(">", ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
void onPaletteChosen(BiomePalette palette);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param nesting how many parent biomes deep the row's biome sits
|
||||||
|
*/
|
||||||
|
private record HereRow(String text, String biomeKey, BiomePalette palette, int nesting, boolean header) {
|
||||||
|
private static HereRow header(String text) {
|
||||||
|
return new HereRow(text, null, null, 0, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HereRow biome(String text, String biomeKey, int nesting) {
|
||||||
|
return new HereRow(text, biomeKey, null, nesting, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HereRow palette(String text, BiomePalette palette, int nesting) {
|
||||||
|
return new HereRow(text, palette.biomeKey(), palette, nesting, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record OverviewNode(String label, String detail, String biomeKey, BiomePalette palette) {
|
||||||
|
private String html() {
|
||||||
|
if (detail == null) {
|
||||||
|
return escape(label);
|
||||||
|
}
|
||||||
|
return "<html>" + escape(label) + " <font color='" + StudioTheme.SECONDARY_TEXT.toHtml() + "'>" + escape(detail) + "</font></html>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class HereRowRenderer extends DefaultListCellRenderer {
|
||||||
|
private static final long serialVersionUID = 2785341117408251320L;
|
||||||
|
private static final int HEADER_INDENT = 11;
|
||||||
|
private static final int BIOME_INDENT = 20;
|
||||||
|
private static final int PALETTE_INDENT = 34;
|
||||||
|
private static final int CHILD_INDENT = 16;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean selected, boolean focus) {
|
||||||
|
HereRow row = (HereRow) value;
|
||||||
|
boolean selectable = row.biomeKey() != null;
|
||||||
|
super.getListCellRendererComponent(list, row.header() ? row.text().toUpperCase(Locale.ROOT) : row.text(), index,
|
||||||
|
selected && selectable, false);
|
||||||
|
setFont(row.header() ? StudioTheme.sectionFont() : StudioTheme.font());
|
||||||
|
if (row.header()) {
|
||||||
|
setForeground(StudioTheme.MUTED_TEXT);
|
||||||
|
}
|
||||||
|
int indent = row.header() ? HEADER_INDENT
|
||||||
|
: (row.palette() != null ? PALETTE_INDENT : BIOME_INDENT) + row.nesting() * CHILD_INDENT;
|
||||||
|
setBorder(StudioTheme.emptyBorder(row.header() ? 11 : 4, indent, 4, 10));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class OverviewRenderer extends DefaultTreeCellRenderer {
|
||||||
|
private static final long serialVersionUID = -1950176034017718227L;
|
||||||
|
|
||||||
|
private OverviewRenderer() {
|
||||||
|
setLeafIcon(null);
|
||||||
|
setOpenIcon(null);
|
||||||
|
setClosedIcon(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded,
|
||||||
|
boolean leaf, int row, boolean focus) {
|
||||||
|
Object userObject = ((DefaultMutableTreeNode) value).getUserObject();
|
||||||
|
String text = userObject instanceof OverviewNode node ? node.html() : "";
|
||||||
|
super.getTreeCellRendererComponent(tree, text, selected, expanded, leaf, row, false);
|
||||||
|
boolean groupsOthers = userObject instanceof OverviewNode node && node.biomeKey() == null && !leaf;
|
||||||
|
setFont(groupsOthers ? StudioTheme.boldFont() : StudioTheme.font());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,451 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.components.PickDisplay;
|
||||||
|
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.BoxLayout;
|
||||||
|
import javax.swing.ButtonGroup;
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.Icon;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JScrollPane;
|
||||||
|
import javax.swing.JTextArea;
|
||||||
|
import javax.swing.JToggleButton;
|
||||||
|
import javax.swing.Scrollable;
|
||||||
|
import javax.swing.SwingConstants;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
|
import javax.swing.UIManager;
|
||||||
|
import javax.swing.text.DefaultCaret;
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.CardLayout;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Cursor;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.GridLayout;
|
||||||
|
import java.awt.Rectangle;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The inspector of the region and biome views: how noise picks the regions or biomes in view, measured against what
|
||||||
|
* is in view and put in words, with a switch for how the map draws the picks. What the user opens or closes stays so
|
||||||
|
* while the view is measured again.
|
||||||
|
*/
|
||||||
|
final class PickInspectorPanel extends JPanel {
|
||||||
|
private static final long serialVersionUID = -2874105263188240317L;
|
||||||
|
private static final String MESSAGE_CARD = "message";
|
||||||
|
private static final String DETAILS_CARD = "details";
|
||||||
|
private static final int PANEL_WIDTH = 330;
|
||||||
|
private static final String PARAGRAPH_BREAK = "\n\n";
|
||||||
|
|
||||||
|
private final JLabel title = new JLabel();
|
||||||
|
private final JLabel detail = new JLabel();
|
||||||
|
private final Map<PickDisplay, JToggleButton> displayButtons = new EnumMap<>(PickDisplay.class);
|
||||||
|
private final CardLayout cards = new CardLayout();
|
||||||
|
private final JPanel cardPanel = new JPanel(cards);
|
||||||
|
private final JTextArea message = wrappingText();
|
||||||
|
private final JPanel details = new ViewportWidthPanel();
|
||||||
|
private final JScrollPane detailsScroll = StudioToolbar.scroll(details);
|
||||||
|
private final Set<String> expandedSectionKeys = new HashSet<>();
|
||||||
|
private final Set<String> fullTableKeys = new HashSet<>();
|
||||||
|
private final String picksName;
|
||||||
|
private boolean sectionsChosenByUser;
|
||||||
|
private String shownDetailsTitle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param picksName what the map shows when it draws the picks: "Regions" or "Biomes"
|
||||||
|
* @param onDisplayChosen receives how the map should draw the picks
|
||||||
|
*/
|
||||||
|
PickInspectorPanel(String picksName, Consumer<PickDisplay> onDisplayChosen) {
|
||||||
|
super(new BorderLayout());
|
||||||
|
this.picksName = picksName;
|
||||||
|
setPreferredSize(StudioTheme.scaledSize(PANEL_WIDTH, 0));
|
||||||
|
setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||||
|
add(buildHeader(onDisplayChosen), BorderLayout.NORTH);
|
||||||
|
cardPanel.setOpaque(false);
|
||||||
|
cardPanel.add(buildMessageCard(), MESSAGE_CARD);
|
||||||
|
details.setLayout(new BoxLayout(details, BoxLayout.Y_AXIS));
|
||||||
|
details.setBackground(StudioTheme.PANEL_BACKGROUND);
|
||||||
|
cardPanel.add(detailsScroll, DETAILS_CARD);
|
||||||
|
add(cardPanel, BorderLayout.CENTER);
|
||||||
|
showMeasuring(picksName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectDisplay(PickDisplay display) {
|
||||||
|
displayButtons.get(display).setSelected(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showMeasuring(String name) {
|
||||||
|
showHeader(name, "Measuring what is in view…");
|
||||||
|
showMessage("");
|
||||||
|
}
|
||||||
|
|
||||||
|
void showRegionPicks(NoisePickSpread spread, List<String> notes) {
|
||||||
|
showHeader("Regions", "Region noise, split by rarity");
|
||||||
|
nameFirstDisplay("Regions");
|
||||||
|
showDetails(List.of(noiseChart(spread, "Regions", "regions")), notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showBiomePicks(String regionName, List<NoisePickSpread> spreads, List<String> notes) {
|
||||||
|
showHeader(regionName, "Biome terrain · biome noise, split by rarity");
|
||||||
|
nameFirstDisplay("Biomes");
|
||||||
|
showDetails(spreads.stream().map(spread -> (JComponent) noiseChart(spread, "Biomes", spread.title())).toList(), notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showCoverage(String regionName, RegionHeightCoverage coverage, List<String> notes) {
|
||||||
|
showHeader(regionName, "Hybrid terrain · biomes by height");
|
||||||
|
nameFirstDisplay("Biomes");
|
||||||
|
showDetails(List.of(heightChart(coverage)), notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One collapsible section per region in view, each with how it picks its biomes, then notes about all of them.
|
||||||
|
* Until the user opens or closes one, only the first is open.
|
||||||
|
*/
|
||||||
|
void showRegionSections(String detailText, List<Section> sections, List<String> notes) {
|
||||||
|
showHeader(picksName, detailText);
|
||||||
|
nameFirstDisplay(picksName);
|
||||||
|
if (!sectionsChosenByUser) {
|
||||||
|
expandedSectionKeys.clear();
|
||||||
|
sections.stream().findFirst().ifPresent(first -> expandedSectionKeys.add(first.key()));
|
||||||
|
}
|
||||||
|
List<JComponent> components = new ArrayList<>();
|
||||||
|
for (Section section : sections) {
|
||||||
|
components.add(new SectionPanel(section, expandedSectionKeys.contains(section.key())));
|
||||||
|
}
|
||||||
|
showDetails(components, notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A chart whose long table remembers, by this key, whether the user asked for all its rows.
|
||||||
|
*/
|
||||||
|
NoisePickChart noiseChart(NoisePickSpread spread, String optionNoun, String key) {
|
||||||
|
return new NoisePickChart(spread, optionNoun, fullTableKeys.contains(key), showsAll -> {
|
||||||
|
if (showsAll) {
|
||||||
|
fullTableKeys.add(key);
|
||||||
|
} else {
|
||||||
|
fullTableKeys.remove(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static RegionHeightChart heightChart(RegionHeightCoverage coverage) {
|
||||||
|
RegionHeightChart chart = new RegionHeightChart();
|
||||||
|
chart.setAlignmentX(LEFT_ALIGNMENT);
|
||||||
|
chart.show(coverage);
|
||||||
|
return chart;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildHeader(Consumer<PickDisplay> onDisplayChosen) {
|
||||||
|
JPanel header = new JPanel(new GridLayout(0, 1, 0, StudioTheme.scale(6)));
|
||||||
|
header.setOpaque(false);
|
||||||
|
header.setBorder(BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createMatteBorder(0, 0, 1, 0, StudioTheme.SEPARATOR),
|
||||||
|
StudioTheme.emptyBorder(10, 16, 12, 16)));
|
||||||
|
title.setFont(StudioTheme.titleFont());
|
||||||
|
detail.setFont(StudioTheme.smallFont());
|
||||||
|
detail.setForeground(StudioTheme.MUTED_TEXT);
|
||||||
|
header.add(title);
|
||||||
|
header.add(detail);
|
||||||
|
header.add(sectionLabel("Show"));
|
||||||
|
header.add(buildDisplayButtons(onDisplayChosen));
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildDisplayButtons(Consumer<PickDisplay> onDisplayChosen) {
|
||||||
|
JPanel row = new JPanel(new GridLayout(1, 0, StudioTheme.scale(4), 0));
|
||||||
|
row.setOpaque(false);
|
||||||
|
ButtonGroup group = new ButtonGroup();
|
||||||
|
for (PickDisplay display : PickDisplay.values()) {
|
||||||
|
JToggleButton button = new JToggleButton(displayName(display));
|
||||||
|
button.setFocusable(false);
|
||||||
|
button.setSelected(display == PickDisplay.PICKS);
|
||||||
|
button.addActionListener(event -> onDisplayChosen.accept(display));
|
||||||
|
group.add(button);
|
||||||
|
row.add(button);
|
||||||
|
displayButtons.put(display, button);
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String displayName(PickDisplay display) {
|
||||||
|
return switch (display) {
|
||||||
|
case PICKS -> picksName;
|
||||||
|
case NOISE -> "Noise";
|
||||||
|
case BOTH -> "Both";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void nameFirstDisplay(String name) {
|
||||||
|
displayButtons.get(PickDisplay.PICKS).setText(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildMessageCard() {
|
||||||
|
JPanel card = new JPanel(new BorderLayout());
|
||||||
|
card.setOpaque(false);
|
||||||
|
message.setBorder(StudioTheme.emptyBorder(12, 16, 12, 16));
|
||||||
|
card.add(message, BorderLayout.NORTH);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showHeader(String name, String detailText) {
|
||||||
|
title.setText(name);
|
||||||
|
detail.setText(detailText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rememberSectionExpanded(String key, boolean expanded) {
|
||||||
|
sectionsChosenByUser = true;
|
||||||
|
if (expanded) {
|
||||||
|
expandedSectionKeys.add(key);
|
||||||
|
} else {
|
||||||
|
expandedSectionKeys.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showMessage(String text) {
|
||||||
|
message.setText(text);
|
||||||
|
cards.show(cardPanel, MESSAGE_CARD);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measuring the same subject again keeps the scroll position, so panning the map doesn't jump the list.
|
||||||
|
private void showDetails(List<JComponent> charts, List<String> notes) {
|
||||||
|
boolean sameSubject = Objects.equals(title.getText(), shownDetailsTitle);
|
||||||
|
int scrollPosition = sameSubject ? detailsScroll.getVerticalScrollBar().getValue() : 0;
|
||||||
|
shownDetailsTitle = title.getText();
|
||||||
|
details.removeAll();
|
||||||
|
charts.forEach(details::add);
|
||||||
|
if (!notes.isEmpty()) {
|
||||||
|
details.add(notesText(notes));
|
||||||
|
}
|
||||||
|
details.revalidate();
|
||||||
|
details.repaint();
|
||||||
|
cards.show(cardPanel, DETAILS_CARD);
|
||||||
|
SwingUtilities.invokeLater(() -> detailsScroll.getVerticalScrollBar().setValue(scrollPosition));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JTextArea notesText(List<String> notes) {
|
||||||
|
JTextArea text = wrappingText();
|
||||||
|
text.setAlignmentX(LEFT_ALIGNMENT);
|
||||||
|
text.setBorder(StudioTheme.emptyBorder(4, 16, 12, 16));
|
||||||
|
text.setText(String.join(PARAGRAPH_BREAK, notes));
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JLabel sectionLabel(String text) {
|
||||||
|
JLabel label = new JLabel(text.toUpperCase(Locale.ROOT));
|
||||||
|
label.setForeground(StudioTheme.MUTED_TEXT);
|
||||||
|
label.setFont(StudioTheme.sectionFont());
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JTextArea wrappingText() {
|
||||||
|
JTextArea text = new JTextArea();
|
||||||
|
text.setEditable(false);
|
||||||
|
text.setFocusable(false);
|
||||||
|
text.setOpaque(false);
|
||||||
|
((DefaultCaret) text.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
|
||||||
|
text.setLineWrap(true);
|
||||||
|
text.setWrapStyleWord(true);
|
||||||
|
text.setFont(StudioTheme.font());
|
||||||
|
text.setForeground(StudioTheme.SECONDARY_TEXT);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One region of the biome view's inspector.
|
||||||
|
*
|
||||||
|
* @param share how much of the view the region covers
|
||||||
|
* @param summary what the closed section says about its content, like "7 biomes"
|
||||||
|
*/
|
||||||
|
record Section(String key, String name, int color, double share, String summary, List<JComponent> charts,
|
||||||
|
List<String> notes) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A region's header, which opens and closes the charts and notes below it.
|
||||||
|
*/
|
||||||
|
private final class SectionPanel extends JPanel {
|
||||||
|
private static final long serialVersionUID = 2318546098861530717L;
|
||||||
|
|
||||||
|
private final JPanel body = new JPanel();
|
||||||
|
|
||||||
|
private SectionPanel(Section section, boolean expanded) {
|
||||||
|
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
|
||||||
|
setOpaque(false);
|
||||||
|
setAlignmentX(LEFT_ALIGNMENT);
|
||||||
|
body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS));
|
||||||
|
body.setOpaque(false);
|
||||||
|
body.setAlignmentX(LEFT_ALIGNMENT);
|
||||||
|
section.charts().forEach(body::add);
|
||||||
|
if (!section.notes().isEmpty()) {
|
||||||
|
body.add(notesText(section.notes()));
|
||||||
|
}
|
||||||
|
body.setVisible(expanded);
|
||||||
|
add(new SectionHeader(section, body::isVisible, () -> toggle(section)));
|
||||||
|
add(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggle(Section section) {
|
||||||
|
body.setVisible(!body.isVisible());
|
||||||
|
rememberSectionExpanded(section.key(), body.isVisible());
|
||||||
|
revalidate();
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A region's name in its colour behind an open or closed chevron, with what it holds and how much of the view it
|
||||||
|
* covers; clicking it opens or closes the region.
|
||||||
|
*/
|
||||||
|
private static final class SectionHeader extends JComponent {
|
||||||
|
private static final long serialVersionUID = -4730226930154581123L;
|
||||||
|
private static final int HEIGHT = 32;
|
||||||
|
private static final int MARGIN = 16;
|
||||||
|
private static final int CHEVRON_WIDTH = 16;
|
||||||
|
private static final int SWATCH_SIZE = 12;
|
||||||
|
private static final int SWATCH_GAP = 8;
|
||||||
|
private static final int SUMMARY_GAP = 8;
|
||||||
|
|
||||||
|
private final Section section;
|
||||||
|
private final BooleanSupplier expanded;
|
||||||
|
|
||||||
|
private SectionHeader(Section section, BooleanSupplier expanded, Runnable onToggle) {
|
||||||
|
this.section = section;
|
||||||
|
this.expanded = expanded;
|
||||||
|
setAlignmentX(LEFT_ALIGNMENT);
|
||||||
|
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||||
|
addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent event) {
|
||||||
|
onToggle.run();
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredSize() {
|
||||||
|
return StudioTheme.scaledSize(PANEL_WIDTH, HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getMaximumSize() {
|
||||||
|
return new Dimension(Integer.MAX_VALUE, getPreferredSize().height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics graphics) {
|
||||||
|
Graphics2D canvas = (Graphics2D) graphics.create();
|
||||||
|
try {
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
canvas.setColor(StudioTheme.SEPARATOR);
|
||||||
|
canvas.fillRect(0, 0, getWidth(), 1);
|
||||||
|
int left = StudioTheme.scale(MARGIN) - StudioTheme.scale(4);
|
||||||
|
paintChevron(canvas, left);
|
||||||
|
left += StudioTheme.scale(CHEVRON_WIDTH);
|
||||||
|
int swatch = StudioTheme.scale(SWATCH_SIZE);
|
||||||
|
canvas.setColor(new Color(section.color()));
|
||||||
|
int arc = StudioTheme.scale(4);
|
||||||
|
canvas.fillRoundRect(left, (getHeight() - swatch) / 2, swatch, swatch, arc, arc);
|
||||||
|
left += swatch + StudioTheme.scale(SWATCH_GAP);
|
||||||
|
|
||||||
|
canvas.setFont(StudioTheme.smallFont());
|
||||||
|
FontMetrics small = canvas.getFontMetrics();
|
||||||
|
String share = NoisePickChart.percent(section.share());
|
||||||
|
int shareLeft = getWidth() - StudioTheme.scale(MARGIN) - small.stringWidth(share);
|
||||||
|
canvas.setFont(StudioTheme.boldFont());
|
||||||
|
FontMetrics bold = canvas.getFontMetrics();
|
||||||
|
int baseline = (getHeight() + bold.getAscent() - bold.getDescent()) / 2;
|
||||||
|
canvas.setColor(StudioTheme.TEXT);
|
||||||
|
canvas.drawString(section.name(), left, baseline);
|
||||||
|
left += bold.stringWidth(section.name()) + StudioTheme.scale(SUMMARY_GAP);
|
||||||
|
|
||||||
|
canvas.setFont(StudioTheme.smallFont());
|
||||||
|
canvas.setColor(StudioTheme.MUTED_TEXT);
|
||||||
|
if (section.summary() != null && left + small.stringWidth(section.summary()) < shareLeft - StudioTheme.scale(SUMMARY_GAP)) {
|
||||||
|
canvas.drawString(section.summary(), left, baseline);
|
||||||
|
}
|
||||||
|
canvas.setColor(StudioTheme.SECONDARY_TEXT);
|
||||||
|
canvas.drawString(share, shareLeft, baseline);
|
||||||
|
} finally {
|
||||||
|
canvas.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void paintChevron(Graphics2D canvas, int left) {
|
||||||
|
Icon chevron = UIManager.getIcon(expanded.getAsBoolean() ? "Tree.expandedIcon" : "Tree.collapsedIcon");
|
||||||
|
if (chevron == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chevron.paintIcon(this, canvas, left + (StudioTheme.scale(CHEVRON_WIDTH) - chevron.getIconWidth()) / 2,
|
||||||
|
(getHeight() - chevron.getIconHeight()) / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Follows the scroll pane's width, so the charts stretch to it and the notes wrap inside it.
|
||||||
|
*/
|
||||||
|
private static final class ViewportWidthPanel extends JPanel implements Scrollable {
|
||||||
|
private static final long serialVersionUID = 6086542174870301352L;
|
||||||
|
private static final int SCROLL_STEP = 18;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredScrollableViewportSize() {
|
||||||
|
return getPreferredSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
|
||||||
|
return StudioTheme.scale(SCROLL_STEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
|
||||||
|
return orientation == SwingConstants.VERTICAL ? visibleRect.height : visibleRect.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getScrollableTracksViewportWidth() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getScrollableTracksViewportHeight() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.core.gui.NoisePickSpread.Option;
|
||||||
|
import com.volmit.iris.core.gui.RegionHeightCoverage.BiomeRange;
|
||||||
|
import com.volmit.iris.core.gui.components.RenderType;
|
||||||
|
import com.volmit.iris.engine.IrisComplex;
|
||||||
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
|
import com.volmit.iris.engine.object.InferredType;
|
||||||
|
import com.volmit.iris.engine.object.IrisBiome;
|
||||||
|
import com.volmit.iris.engine.object.IrisRegion;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Samples the map in view to measure how regions and biomes are picked, and puts what stands out in words; shared by
|
||||||
|
* the region and biome views.
|
||||||
|
*/
|
||||||
|
final class PickMeasuring {
|
||||||
|
static final List<InferredType> PLACED_TYPES = List.of(InferredType.LAND, InferredType.SHORE, InferredType.SEA);
|
||||||
|
private static final int SAMPLES_PER_SIDE = 96;
|
||||||
|
private static final int NAMED_IN_NOTES = 3;
|
||||||
|
|
||||||
|
private PickMeasuring() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static void forEachSample(MapCensus.ViewArea area, SampleVisitor visitor) {
|
||||||
|
double stepX = (area.maximumX() - area.minimumX()) / SAMPLES_PER_SIDE;
|
||||||
|
double stepZ = (area.maximumZ() - area.minimumZ()) / SAMPLES_PER_SIDE;
|
||||||
|
for (int row = 0; row < SAMPLES_PER_SIDE; row++) {
|
||||||
|
for (int column = 0; column < SAMPLES_PER_SIDE; column++) {
|
||||||
|
visitor.visit(area.minimumX() + (column + 0.5D) * stepX, area.minimumZ() + (row + 0.5D) * stepZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Option regionOption(Engine engine, IrisRegion region) {
|
||||||
|
return new Option(region.getLoadKey(), region.getName(), regionColor(engine, region), region.getRarity());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Option biomeOption(Engine engine, IrisBiome biome) {
|
||||||
|
return new Option(biome.getLoadKey(), biome.getName(), biome.getColor(engine, RenderType.BIOME).getRGB() & 0xFFFFFF,
|
||||||
|
biome.getRarity());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int regionColor(Engine engine, IrisRegion region) {
|
||||||
|
return region.getColor(engine.getComplex(), RenderType.REGION).getRGB() & 0xFFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One spread per biome list of the region that has biomes, in land, shore, sea order.
|
||||||
|
*/
|
||||||
|
static List<TypedSpread> biomeSpreads(Engine engine, IrisComplex complex, IrisRegion region) {
|
||||||
|
List<TypedSpread> spreads = new ArrayList<>();
|
||||||
|
for (InferredType type : PLACED_TYPES) {
|
||||||
|
List<Option> options = complex.noisePickedBiomes(region, type).stream()
|
||||||
|
.map(biome -> biomeOption(engine, biome))
|
||||||
|
.toList();
|
||||||
|
if (!options.isEmpty()) {
|
||||||
|
spreads.add(new TypedSpread(type,
|
||||||
|
NoisePickSpread.of(typeName(type) + " biome noise in view", options, complex.isLegacyRarity())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return spreads;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<BiomeRange> biomeRanges(Engine engine, IrisRegion region) {
|
||||||
|
List<BiomeRange> ranges = new ArrayList<>();
|
||||||
|
for (InferredType type : PLACED_TYPES) {
|
||||||
|
for (IrisBiome biome : region.getBiomes(engine.getComplex(), type)) {
|
||||||
|
if (biome == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ranges.add(new BiomeRange(biome.getLoadKey(), biome.getName(),
|
||||||
|
biome.getColor(engine, RenderType.BIOME).getRGB() & 0xFFFFFF, type,
|
||||||
|
biome.getPlacement().getMinHeight(), biome.getPlacement().getMaxHeight()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void addSpreadNotes(List<String> notes, NoisePickSpread spread, String noun) {
|
||||||
|
List<NoisePickSpread.Row> rows = spread.rows();
|
||||||
|
if (spread.sampleCount() == 0) {
|
||||||
|
notes.add("No " + noun + " is picked in view.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rows.size() == 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (spread.belowZeroShare() > 0D) {
|
||||||
|
notes.add(NoisePickChart.percent(spread.belowZeroShare()) + " of the " + noun + " noise in view is below 0, so "
|
||||||
|
+ rows.getFirst().option().name() + " gets it.");
|
||||||
|
}
|
||||||
|
if (spread.aboveOneShare() > 0D) {
|
||||||
|
notes.add(NoisePickChart.percent(spread.aboveOneShare()) + " of the " + noun + " noise in view is above 1, so "
|
||||||
|
+ rows.getLast().option().name() + " gets it.");
|
||||||
|
}
|
||||||
|
List<String> starved = rows.stream().filter(NoisePickSpread.Row::isFarBelowWeight).map(row -> row.option().name()).toList();
|
||||||
|
if (!starved.isEmpty()) {
|
||||||
|
boolean single = starved.size() == 1;
|
||||||
|
notes.add(namedFew(starved) + (single ? " gets" : " get") + " under half " + (single ? "its" : "their")
|
||||||
|
+ " weight in view (▼): the " + noun + " noise rarely reaches " + (single ? "its band." : "their bands."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void addBiomeSpreadNotes(List<String> notes, List<TypedSpread> spreads) {
|
||||||
|
for (TypedSpread typed : spreads) {
|
||||||
|
addSpreadNotes(notes, typed.spread(), typeName(typed.type()).toLowerCase(Locale.ROOT) + " biome");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void addChildBiomeNote(List<String> notes, IrisComplex complex, IrisRegion region) {
|
||||||
|
List<String> parents = PLACED_TYPES.stream()
|
||||||
|
.flatMap(type -> complex.noisePickedBiomes(region, type).stream())
|
||||||
|
.filter(biome -> !biome.getChildren().isEmpty())
|
||||||
|
.map(IrisBiome::getName)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
if (!parents.isEmpty()) {
|
||||||
|
notes.add(namedFew(parents) + " then hand some spots to child biomes by a noise of their own.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void addRarityNotes(List<String> notes, IrisComplex complex, Collection<NoisePickSpread> spreads) {
|
||||||
|
if (spreads.stream().allMatch(spread -> spread.bands().size() <= 1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notes.add(complex.isLegacyRarity()
|
||||||
|
? "Legacy rarity: each weight is the total rarity minus the option's own."
|
||||||
|
: "Each weight is one over the rarity, so rarity 2 gets half the noise of rarity 1.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A long list of names buries the point of a note, so only the first few are named.
|
||||||
|
private static String namedFew(List<String> names) {
|
||||||
|
if (names.size() <= NAMED_IN_NOTES) {
|
||||||
|
return String.join(", ", names);
|
||||||
|
}
|
||||||
|
return String.join(", ", names.subList(0, NAMED_IN_NOTES)) + " and " + (names.size() - NAMED_IN_NOTES) + " more";
|
||||||
|
}
|
||||||
|
|
||||||
|
static String measuredNote() {
|
||||||
|
return "Measured on a " + SAMPLES_PER_SIDE + " × " + SAMPLES_PER_SIDE + " grid over the map in view.";
|
||||||
|
}
|
||||||
|
|
||||||
|
static String hybridNoiseNote() {
|
||||||
|
return "Noise shows the height biomes are picked at: the overlay noise plus the dimension's biome placement "
|
||||||
|
+ "jitter. Where ranges share a height, each biome's competition noise, divided by its rarity, decides.";
|
||||||
|
}
|
||||||
|
|
||||||
|
static String typeName(InferredType type) {
|
||||||
|
String name = type.name().toLowerCase(Locale.ROOT);
|
||||||
|
return Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface SampleVisitor {
|
||||||
|
void visit(double x, double z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The spread of the noise that picks from one of a region's biome lists.
|
||||||
|
*/
|
||||||
|
record TypedSpread(InferredType type, NoisePickSpread spread) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.volmit.iris.core.gui;
|
||||||
|
|
||||||
|
import com.volmit.iris.Iris;
|
||||||
|
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||||
|
import com.volmit.iris.engine.framework.Engine;
|
||||||
|
import com.volmit.iris.engine.object.IrisBiome;
|
||||||
|
import com.volmit.iris.engine.object.IrisRegion;
|
||||||
|
import com.volmit.iris.engine.platform.PlatformChunkGenerator;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
|
||||||
|
import java.awt.EventQueue;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
final class PlayerLocationTracker {
|
||||||
|
private static final long CHECK_INTERVAL_TICKS = 10L;
|
||||||
|
|
||||||
|
private final UUID playerId;
|
||||||
|
private final UUID worldId;
|
||||||
|
private final Listener listener;
|
||||||
|
private BukkitTask task;
|
||||||
|
private PlayerLocation lastLocation;
|
||||||
|
|
||||||
|
private PlayerLocationTracker(UUID playerId, UUID worldId, Listener listener) {
|
||||||
|
this.playerId = playerId;
|
||||||
|
this.worldId = worldId;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PlayerLocationTracker follow(UUID playerId, UUID worldId, Listener listener) {
|
||||||
|
PlayerLocationTracker tracker = new PlayerLocationTracker(playerId, worldId, listener);
|
||||||
|
tracker.task = Bukkit.getScheduler().runTaskTimer(Iris.instance, tracker::checkLocation, 0L, CHECK_INTERVAL_TICKS);
|
||||||
|
return tracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
if (task != null) {
|
||||||
|
task.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkLocation() {
|
||||||
|
PlayerLocation location = locate();
|
||||||
|
if (location.equals(lastLocation)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastLocation = location;
|
||||||
|
EventQueue.invokeLater(() -> listener.onLocationChanged(location));
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayerLocation locate() {
|
||||||
|
Player player = Bukkit.getPlayer(playerId);
|
||||||
|
if (player == null) {
|
||||||
|
return PlayerLocation.OFFLINE;
|
||||||
|
}
|
||||||
|
if (player.getWorld() == null || !player.getWorld().getUID().equals(worldId)) {
|
||||||
|
return PlayerLocation.IN_OTHER_WORLD;
|
||||||
|
}
|
||||||
|
Engine engine = engineAt(player.getLocation());
|
||||||
|
if (engine == null) {
|
||||||
|
return PlayerLocation.IN_OTHER_WORLD;
|
||||||
|
}
|
||||||
|
Location position = player.getLocation();
|
||||||
|
IrisRegion region = engine.getRegion(position);
|
||||||
|
String surfaceBiome = keyOf(engine.getSurfaceBiome(position.getBlockX(), position.getBlockZ()));
|
||||||
|
String biomeAtPosition = keyOf(engine.getBiomeOrMantle(position));
|
||||||
|
if (region == null || region.getLoadKey() == null || surfaceBiome == null) {
|
||||||
|
return PlayerLocation.IN_OTHER_WORLD;
|
||||||
|
}
|
||||||
|
return new PlayerLocation(Status.IN_REGION, region.getLoadKey(), surfaceBiome, biomeAtPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String keyOf(IrisBiome biome) {
|
||||||
|
return biome == null ? null : biome.getLoadKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Engine engineAt(Location location) {
|
||||||
|
if (location.getWorld() == null || !IrisToolbelt.isIrisWorld(location.getWorld())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
PlatformChunkGenerator generator = IrisToolbelt.access(location.getWorld());
|
||||||
|
return generator == null ? null : generator.getEngine();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
OFFLINE,
|
||||||
|
IN_OTHER_WORLD,
|
||||||
|
IN_REGION
|
||||||
|
}
|
||||||
|
|
||||||
|
// Biomes are 3D: underground, the biome at the player's position can differ from the surface biome above it.
|
||||||
|
record PlayerLocation(Status status, String regionKey, String surfaceBiomeKey, String biomeAtPositionKey) {
|
||||||
|
static final PlayerLocation OFFLINE = new PlayerLocation(Status.OFFLINE, null, null, null);
|
||||||
|
static final PlayerLocation IN_OTHER_WORLD = new PlayerLocation(Status.IN_OTHER_WORLD, null, null, null);
|
||||||
|
|
||||||
|
boolean isBelowSurfaceBiome() {
|
||||||
|
return biomeAtPositionKey != null && !biomeAtPositionKey.equals(surfaceBiomeKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
void onLocationChanged(PlayerLocation location);
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
-30
@@ -40,7 +40,10 @@ import java.awt.*;
|
|||||||
import java.awt.event.KeyEvent;
|
import java.awt.event.KeyEvent;
|
||||||
import java.awt.event.KeyListener;
|
import java.awt.event.KeyListener;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@@ -53,7 +56,7 @@ public class PregeneratorJob implements PregenListener {
|
|||||||
private static final Color COLOR_NETWORK_GENERATING = parseColor("#836b8c");
|
private static final Color COLOR_NETWORK_GENERATING = parseColor("#836b8c");
|
||||||
private static final Color COLOR_GENERATED = parseColor("#65c295");
|
private static final Color COLOR_GENERATED = parseColor("#65c295");
|
||||||
private static final Color COLOR_CLEANED = parseColor("#34eb93");
|
private static final Color COLOR_CLEANED = parseColor("#34eb93");
|
||||||
public static PregeneratorJob instance;
|
private static final AtomicReference<PregeneratorJob> instance = new AtomicReference<>();
|
||||||
private final MemoryMonitor monitor;
|
private final MemoryMonitor monitor;
|
||||||
private final PregenTask task;
|
private final PregenTask task;
|
||||||
private final boolean saving;
|
private final boolean saving;
|
||||||
@@ -64,67 +67,83 @@ public class PregeneratorJob implements PregenListener {
|
|||||||
private final Position2 max;
|
private final Position2 max;
|
||||||
private final ChronoLatch cl = new ChronoLatch(TimeUnit.MINUTES.toMillis(1));
|
private final ChronoLatch cl = new ChronoLatch(TimeUnit.MINUTES.toMillis(1));
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
|
private final ExecutorService service;
|
||||||
private JFrame frame;
|
private JFrame frame;
|
||||||
private PregenRenderer renderer;
|
private PregenRenderer renderer;
|
||||||
private int rgc = 0;
|
private int rgc = 0;
|
||||||
private String[] info;
|
private String[] info;
|
||||||
|
|
||||||
public PregeneratorJob(PregenTask task, PregeneratorMethod method, Engine engine) {
|
public PregeneratorJob(PregenTask task, PregeneratorMethod method, Engine engine) {
|
||||||
|
instance.updateAndGet(old -> {
|
||||||
|
if (old != null) {
|
||||||
|
old.pregenerator.close();
|
||||||
|
old.close();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
});
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
instance = this;
|
|
||||||
monitor = new MemoryMonitor(50);
|
monitor = new MemoryMonitor(50);
|
||||||
saving = false;
|
saving = false;
|
||||||
info = new String[]{"Initializing..."};
|
info = new String[]{"Initializing..."};
|
||||||
this.task = task;
|
this.task = task;
|
||||||
this.pregenerator = new IrisPregenerator(task, method, this);
|
this.pregenerator = new IrisPregenerator(task, method, this);
|
||||||
max = new Position2(0, 0);
|
max = new Position2(0, 0);
|
||||||
min = new Position2(0, 0);
|
min = new Position2(Integer.MAX_VALUE, Integer.MAX_VALUE);
|
||||||
task.iterateRegions((xx, zz) -> {
|
task.iterateAllChunks((xx, zz) -> {
|
||||||
min.setX(Math.min(xx << 5, min.getX()));
|
min.setX(Math.min(xx, min.getX()));
|
||||||
min.setZ(Math.min(zz << 5, min.getZ()));
|
min.setZ(Math.min(zz, min.getZ()));
|
||||||
max.setX(Math.max((xx << 5) + 31, max.getX()));
|
max.setX(Math.max(xx, max.getX()));
|
||||||
max.setZ(Math.max((zz << 5) + 31, max.getZ()));
|
max.setZ(Math.max(zz, max.getZ()));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (IrisSettings.get().getGui().isUseServerLaunchedGuis()) {
|
if (IrisSettings.get().getGui().isUseServerLaunchedGuis() && task.isGui()) {
|
||||||
open();
|
open();
|
||||||
}
|
}
|
||||||
|
|
||||||
J.a(this.pregenerator::start, 20);
|
var t = new Thread(() -> {
|
||||||
|
J.sleep(1000);
|
||||||
|
this.pregenerator.start();
|
||||||
|
}, "Iris Pregenerator");
|
||||||
|
t.setPriority(Thread.MIN_PRIORITY);
|
||||||
|
t.start();
|
||||||
|
service = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean shutdownInstance() {
|
public static boolean shutdownInstance() {
|
||||||
if (instance == null) {
|
PregeneratorJob inst = instance.get();
|
||||||
|
if (inst == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
J.a(() -> instance.pregenerator.close());
|
J.a(inst.pregenerator::close);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PregeneratorJob getInstance() {
|
public static PregeneratorJob getInstance() {
|
||||||
return instance;
|
return instance.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean pauseResume() {
|
public static boolean pauseResume() {
|
||||||
if (instance == null) {
|
PregeneratorJob inst = instance.get();
|
||||||
|
if (inst == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPaused()) {
|
if (isPaused()) {
|
||||||
instance.pregenerator.resume();
|
inst.pregenerator.resume();
|
||||||
} else {
|
} else {
|
||||||
instance.pregenerator.pause();
|
inst.pregenerator.pause();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isPaused() {
|
public static boolean isPaused() {
|
||||||
if (instance == null) {
|
PregeneratorJob inst = instance.get();
|
||||||
|
if (inst == null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance.paused();
|
return inst.paused();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Color parseColor(String c) {
|
private static Color parseColor(String c) {
|
||||||
@@ -154,7 +173,7 @@ public class PregeneratorJob implements PregenListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void drawRegion(int x, int z, Color color) {
|
public void drawRegion(int x, int z, Color color) {
|
||||||
J.a(() -> PregenTask.iterateRegion(x, z, (xx, zz) -> {
|
J.a(() -> task.iterateChunks(x, z, (xx, zz) -> {
|
||||||
draw(xx, zz, color);
|
draw(xx, zz, color);
|
||||||
J.sleep(3);
|
J.sleep(3);
|
||||||
}));
|
}));
|
||||||
@@ -174,7 +193,7 @@ public class PregeneratorJob implements PregenListener {
|
|||||||
J.a(() -> {
|
J.a(() -> {
|
||||||
pregenerator.close();
|
pregenerator.close();
|
||||||
close();
|
close();
|
||||||
instance = null;
|
instance.compareAndSet(this, null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +212,7 @@ public class PregeneratorJob implements PregenListener {
|
|||||||
public void open() {
|
public void open() {
|
||||||
J.a(() -> {
|
J.a(() -> {
|
||||||
try {
|
try {
|
||||||
frame = new JFrame("Pregen View");
|
frame = GuiHost.createFrame("Pregen View");
|
||||||
renderer = new PregenRenderer();
|
renderer = new PregenRenderer();
|
||||||
frame.addKeyListener(renderer);
|
frame.addKeyListener(renderer);
|
||||||
renderer.l = new ReentrantLock();
|
renderer.l = new ReentrantLock();
|
||||||
@@ -214,10 +233,10 @@ public class PregeneratorJob implements PregenListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, int generated, int totalChunks, int chunksRemaining, long eta, long elapsed, String method) {
|
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
|
||||||
info = new String[]{
|
info = new String[]{
|
||||||
(paused() ? "PAUSED" : (saving ? "Saving... " : "Generating")) + " " + Form.f(generated) + " of " + Form.f(totalChunks) + " (" + Form.pc(percent, 0) + " Complete)",
|
(paused() ? "PAUSED" : (saving ? "Saving... " : "Generating")) + " " + Form.f(generated) + " of " + Form.f(totalChunks) + " (" + Form.pc(percent, 0) + " Complete)",
|
||||||
"Speed: " + Form.f(chunksPerSecond, 0) + " Chunks/s, " + Form.f(regionsPerMinute, 1) + " Regions/m, " + Form.f(chunksPerMinute, 0) + " Chunks/m",
|
"Speed: " + (cached ? "Cached " : "") + Form.f(chunksPerSecond, 0) + " Chunks/s, " + Form.f(regionsPerMinute, 1) + " Regions/m, " + Form.f(chunksPerMinute, 0) + " Chunks/m",
|
||||||
Form.duration(eta, 2) + " Remaining " + " (" + Form.duration(elapsed, 2) + " Elapsed)",
|
Form.duration(eta, 2) + " Remaining " + " (" + Form.duration(elapsed, 2) + " Elapsed)",
|
||||||
"Generation Method: " + method,
|
"Generation Method: " + method,
|
||||||
"Memory: " + Form.memSize(monitor.getUsedBytes(), 2) + " (" + Form.pc(monitor.getUsagePercent(), 0) + ") Pressure: " + Form.memSize(monitor.getPressure(), 0) + "/s",
|
"Memory: " + Form.memSize(monitor.getUsedBytes(), 2) + " (" + Form.pc(monitor.getUsagePercent(), 0) + ") Pressure: " + Form.memSize(monitor.getPressure(), 0) + "/s",
|
||||||
@@ -235,13 +254,16 @@ public class PregeneratorJob implements PregenListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onChunkGenerated(int x, int z) {
|
public void onChunkGenerated(int x, int z, boolean cached) {
|
||||||
if (engine != null) {
|
if (renderer == null || frame == null || !frame.isVisible()) return;
|
||||||
draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8));
|
service.submit(() -> {
|
||||||
return;
|
if (engine != null) {
|
||||||
}
|
draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
draw(x, z, COLOR_GENERATED);
|
draw(x, z, COLOR_GENERATED);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -299,8 +321,9 @@ public class PregeneratorJob implements PregenListener {
|
|||||||
@Override
|
@Override
|
||||||
public void onClose() {
|
public void onClose() {
|
||||||
close();
|
close();
|
||||||
instance = null;
|
instance.compareAndSet(this, null);
|
||||||
whenDone.forEach(Runnable::run);
|
whenDone.forEach(Runnable::run);
|
||||||
|
service.shutdownNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
Loaded 100 of 1293 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user