mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-20 15:26:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
93db7ee0ee | ||
|
|
9ba4dd4099 | ||
|
|
36efacf43c | ||
|
|
d9bf91afa5 | ||
|
|
b0c96be841 | ||
|
|
673b08f174 | ||
|
|
989778af26 | ||
|
|
f5a3a7eb1a | ||
|
|
7a97928c21 | ||
|
|
12a6d022cf | ||
|
|
e3d2dfa99e | ||
|
|
35cc39e9e2 | ||
|
|
006b0b4a03 | ||
|
|
ec939b9f78 | ||
|
|
7a94f53735 | ||
|
|
a9efe146ba | ||
|
|
956e2f6b06 | ||
|
|
72623e0acf | ||
|
|
e0f673bc3c | ||
|
|
fe40f12d2e | ||
|
|
0fda7a8506 | ||
|
|
22887da769 | ||
|
|
a1e0a8ffc1 | ||
|
|
62010116d7 | ||
|
|
6ebcd02ae6 | ||
|
|
051015656a | ||
|
|
ee082762c6 | ||
|
|
e967b5e052 | ||
|
|
36505e2fa1 | ||
|
|
502aa054f6 | ||
|
|
cc5a880fd7 | ||
|
|
6f9ad8b0eb | ||
|
|
ac6ab74d48 | ||
|
|
807ed2b247 | ||
|
|
367de5a8fd | ||
|
|
5bf2da714a | ||
|
|
cc90f42deb | ||
|
|
20ceaead09 | ||
|
|
3ec2f8fb7b | ||
|
|
6c5fac154e | ||
|
|
540ab8f0d2 | ||
|
|
17d2ac8d70 | ||
|
|
a6ebdead19 | ||
|
|
aa7631ecd0 | ||
|
|
4e138cad9f | ||
|
|
7e55b5fcee | ||
|
|
e19e5278b4 | ||
|
|
dfcd2dc83e | ||
|
|
8775f842e6 | ||
|
|
7b07a4ba6c | ||
|
|
d78e5973e9 | ||
|
|
110d296184 | ||
|
|
23cd5c117b | ||
|
|
131c4692bc | ||
|
|
f6571367db | ||
|
|
f5c64c7480 | ||
|
|
085f63a915 | ||
|
|
3e022e1931 | ||
|
|
0dba3725ae | ||
|
|
8bb409df4e | ||
|
|
603168a147 | ||
|
|
2a95edd860 | ||
|
|
e94406fb45 | ||
|
|
e5a7b5d0c6 | ||
|
|
d6f816fe2f | ||
|
|
c15d4a349f | ||
|
|
3ce832583c | ||
|
|
5514fd2645 | ||
|
|
66d07dcaca | ||
|
|
c1cf8e88ee | ||
|
|
684bd739b9 | ||
|
|
d4c0e07b1d | ||
|
|
a6ea6fcfb2 | ||
|
|
c366ec0c40 | ||
|
|
8d715e2e4e | ||
|
|
dea3ec80ac | ||
|
|
4053f05ba9 | ||
|
|
ef07ec2c62 | ||
|
|
0ffc64231b | ||
|
|
fe112c3ba5 | ||
|
|
2fa03aac3b | ||
|
|
6ac5ad880d | ||
|
|
54f14392d9 | ||
|
|
c7ecdaf8a8 | ||
|
|
497c24a3b6 | ||
|
|
00aa26e602 | ||
|
|
3796381156 | ||
|
|
b0c5700cf5 | ||
|
|
78a4b1d2ce | ||
|
|
e5e6b9848f | ||
|
|
3e36f57f14 | ||
|
|
d794ede4d8 | ||
|
|
7a4d97d76a | ||
|
|
e76ab1d367 | ||
|
|
9731feff7f | ||
|
|
4ce790082d | ||
|
|
065b748036 | ||
|
|
76891d246f | ||
|
|
0832007991 | ||
|
|
87302a046c | ||
|
|
f0f7453b32 | ||
|
|
2991bea248 | ||
|
|
673b42c2f5 | ||
|
|
7d994e27e3 | ||
|
|
5f2b82aac7 | ||
|
|
af1a03cb67 | ||
|
|
85fbbeca9d | ||
|
|
a64272620f | ||
|
|
ef9966d02b | ||
|
|
a28e5bb38a | ||
|
|
34670759e9 | ||
|
|
7dd1d3881a | ||
|
|
7b93542014 | ||
|
|
ea1ac0a154 | ||
|
|
35c7068fa6 | ||
|
|
db4cbb2ea6 | ||
|
|
fa7c073999 | ||
|
|
72a077662d | ||
|
|
137abb04b2 | ||
|
|
211b15332d | ||
|
|
f6fbcade17 | ||
|
|
1ef115fee8 | ||
|
|
7973444fc5 | ||
|
|
2bdb0bfa69 | ||
|
|
bfb6f82ea3 | ||
|
|
7729ce3753 | ||
|
|
f6ea171669 | ||
|
|
ff081ebc11 | ||
|
|
0818f971fe | ||
|
|
27070f44c7 | ||
|
|
8331cbe375 | ||
|
|
f533ec34b0 | ||
|
|
23d4fcb827 | ||
|
|
a248962f1b | ||
|
|
afa8fad8e9 | ||
|
|
4c423bb493 | ||
|
|
e660fe9e1e | ||
|
|
23c1d12e73 | ||
|
|
9cf2785626 | ||
|
|
b8b65c7e3e | ||
|
|
dcb467280a | ||
|
|
6aa802b42d | ||
|
|
d78f6ca589 | ||
|
|
493f8e0cc0 | ||
|
|
119563c553 | ||
|
|
2d61e6af66 | ||
|
|
aa1be934a9 | ||
|
|
f9c98b0a60 | ||
|
|
bd66fa3bf5 | ||
|
|
380f191f6a | ||
|
|
7e217b5fba | ||
|
|
c4a516a858 | ||
|
|
bdf181e348 | ||
|
|
f4ea1343b4 | ||
|
|
a37ccddd38 | ||
|
|
243ef8c0be | ||
|
|
00d099383b | ||
|
|
dda3f0b8e9 | ||
|
|
00f4d8b1ee | ||
|
|
2f5198d533 | ||
|
|
148261f876 | ||
|
|
60843b3bb9 | ||
|
|
2ad9a525bd | ||
|
|
78e9f21439 | ||
|
|
a66d60eaea | ||
|
|
3aa3c13477 | ||
|
|
cc850522e6 | ||
|
|
6d28f57f88 | ||
|
|
563e0a7cd4 | ||
|
|
2f2f857e98 | ||
|
|
251c52a2ee | ||
|
|
7528bd343b | ||
|
|
06d9f279ac | ||
|
|
044403b829 | ||
|
|
5a24dd3b49 | ||
|
|
3189b3c7a5 | ||
|
|
3bf1c6a282 | ||
|
|
7c7309c3ab | ||
|
|
da777da476 | ||
|
|
3d0c0a11ed | ||
|
|
e545269b93 | ||
|
|
141ca76647 | ||
|
|
94557830f5 | ||
|
|
49acb7faba | ||
|
|
1904f67662 | ||
|
|
5b2bf38344 | ||
|
|
494c38a153 | ||
|
|
dd4b85cbfe | ||
|
|
8f7b54a5a4 | ||
|
|
cec502340e | ||
|
|
64e27c7fb0 | ||
|
|
b2bbd31548 | ||
|
|
17df92a07c | ||
|
|
2b749b5ab7 | ||
|
|
28e3402d88 | ||
|
|
d96bb061e0 | ||
|
|
e6def804f8 | ||
|
|
a5be48c07c | ||
|
|
fe3909f594 | ||
|
|
f37d91a530 | ||
|
|
614ef78771 | ||
|
|
fb294fc03c | ||
|
|
8c7e7c3d48 | ||
|
|
98b6280652 | ||
|
|
7c4c2d6382 | ||
|
|
1a29ea302e | ||
|
|
be39fce741 | ||
|
|
4fed2e6e5f | ||
|
|
14ae5809b0 | ||
|
|
0f66aa47c9 | ||
|
|
cf85d300f4 | ||
|
|
91d60e56df | ||
|
|
1d3681beb4 | ||
|
|
0b403cf329 | ||
|
|
8ba75899e3 | ||
|
|
4d4ed72159 | ||
|
|
b72bf072b3 | ||
|
|
4edcb54b31 | ||
|
|
eae7fc0e53 | ||
|
|
fddacd4410 | ||
|
|
de6c4b9a30 | ||
|
|
6990e34138 | ||
|
|
d9883697ef | ||
|
|
05a26ff8b2 | ||
|
|
84bb082d44 | ||
|
|
a0db94d84f | ||
|
|
0367b4ecd7 | ||
|
|
dbc425dce6 | ||
|
|
116c017c6d | ||
|
|
9c387475f0 | ||
|
|
0765c21cef | ||
|
|
2871038584 | ||
|
|
340885f939 | ||
|
|
1e32e47f54 | ||
|
|
3c759f3b01 | ||
|
|
6615f34d20 | ||
|
|
3a4aac1ee4 | ||
|
|
4d6c092615 | ||
|
|
93421a1dc9 | ||
|
|
209458a856 | ||
|
|
82bd94620e | ||
|
|
af9f017871 | ||
|
|
2f8922ea87 | ||
|
|
0ea936f3fe | ||
|
|
d1870ee0a8 | ||
|
|
d2151690ad | ||
|
|
dff373e7d5 | ||
|
|
0aec2a664f | ||
|
|
723b696393 | ||
|
|
f5f89eb4e4 | ||
|
|
df15332c7f | ||
|
|
61d9e5a869 | ||
|
|
9628b66a97 | ||
|
|
5e041366d4 | ||
|
|
3ecae8d72c | ||
|
|
5709ce9d82 | ||
|
|
ce3c334ac5 | ||
|
|
2ed5aa8730 | ||
|
|
8480a63dda | ||
|
|
132a1695f3 | ||
|
|
21775630c3 | ||
|
|
eac2ef7c6d | ||
|
|
386997f646 | ||
|
|
637b90ad62 | ||
|
|
95d755c2ec | ||
|
|
781aeebef6 | ||
|
|
5e133fd51a | ||
|
|
85f62a3b9d | ||
|
|
a32de58c8b | ||
|
|
1cb2ea6c17 | ||
|
|
6a37ab7af2 | ||
|
|
e0f0aaf767 | ||
|
|
3d7c5b050a | ||
|
|
6bc57c255f | ||
|
|
b82edfe688 | ||
|
|
6b32eb3441 | ||
|
|
23a07fa8a5 | ||
|
|
b019faedd2 | ||
|
|
4430433a10 | ||
|
|
f52cd29e7b | ||
|
|
6a44e593a6 | ||
|
|
f7065fe034 | ||
|
|
0a247956f7 | ||
|
|
03836acded | ||
|
|
b5fb277982 | ||
|
|
e523d3c166 | ||
|
|
41477e4aa6 | ||
|
|
fbaf42a8c4 | ||
|
|
1c3668047b | ||
|
|
7d78c69b6e |
No files matched your search
@@ -0,0 +1,67 @@
|
||||
name: Iris Bug Report
|
||||
description: File a bug report for Iris
|
||||
labels: [ bug ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to fill this out!
|
||||
If this does not work for you, feel free to use the [blank](https://github.com/VolmitSoftware/Iris/issues/new) format.
|
||||
- type: textarea
|
||||
id: how
|
||||
attributes:
|
||||
label: Problem
|
||||
description: Please give a text description of how you reached the problem
|
||||
value: |
|
||||
1. Install Iris...
|
||||
2. Do this...
|
||||
3. Do that...
|
||||
4. Observe the error...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
label: Solution
|
||||
description: Explain where you think the problem comes from (optional)
|
||||
placeholder: The code to place a is missing b and c...
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: mcversion
|
||||
attributes:
|
||||
label: Minecraft Version
|
||||
description: What version of Minecraft is the server on?
|
||||
options:
|
||||
- 1.14.X
|
||||
- 1.15.X
|
||||
- 1.16.X
|
||||
- 1.17
|
||||
- 1.17.1
|
||||
- 1.18
|
||||
- 1.19
|
||||
- 1.20
|
||||
- 1.21
|
||||
- 1.22
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: irisversion
|
||||
attributes:
|
||||
label: Iris Version
|
||||
description: What version of Iris are you running? (see console)
|
||||
placeholder: DO NOT SAY "LATEST"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: logs
|
||||
attributes:
|
||||
label: Log
|
||||
description: Paste a full log. Always use [mclogs](https://mclo.gs) Or [Pastebin](https://pastebin.com/). Must not be a crash report. Must be a full log. Must not be a screenshot of a log.
|
||||
placeholder: https://mslog.gs/...
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
id: thanks
|
||||
attributes:
|
||||
value: "Thank you for filling out the form! We will be with you soon. Please do not ask support to review your report."
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Iris Feature Request
|
||||
description: File a feature request for Iris. If you want to report a bug this is not the place.
|
||||
labels: [ feature ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to fill this out!
|
||||
If this does not work for you, feel free to use the [blank](https://github.com/VolmitSoftware/Iris/issues/new) format.
|
||||
- type: dropdown
|
||||
id: arc
|
||||
attributes:
|
||||
label: Adding, Removing, or Changing
|
||||
description: What are you doing
|
||||
options:
|
||||
- Adding
|
||||
- Removing
|
||||
- Changing
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: atype
|
||||
attributes:
|
||||
label: Type of Modification
|
||||
description: What is it for?
|
||||
value: I want to ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: desc
|
||||
attributes:
|
||||
label: What are you trying to modify
|
||||
description: Give as detailed of a description as you can for the modification that you want done (include pictures if applicable)
|
||||
value: The way I would implement this is ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternative
|
||||
attributes:
|
||||
label: Alternatives
|
||||
description: What alternatives have you considered?
|
||||
value: If this could not be implemented I would ...
|
||||
- type: markdown
|
||||
id: thanks
|
||||
attributes:
|
||||
value: "Thank you for filling out the form! We will be with you soon. Please do not ask support to review your report."
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
|
||||
build/
|
||||
libs/
|
||||
|
||||
.gradle/
|
||||
|
||||
@@ -9,4 +10,4 @@ build/
|
||||
|
||||
collection/
|
||||
|
||||
src/main/java/com/volmit/iris/util/uniques/
|
||||
/core/src/main/java/com/volmit/iris/util/uniques/
|
||||
+518
@@ -0,0 +1,518 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed to take away your freedom to share and change the
|
||||
works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all
|
||||
versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use
|
||||
the GNU General Public License for most of our software; it applies also to any other work released this way by its
|
||||
authors. You can apply it to your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make
|
||||
sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive
|
||||
source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and
|
||||
that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights.
|
||||
Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it:
|
||||
responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients
|
||||
the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must
|
||||
show them these terms so they know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute
|
||||
and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software.
|
||||
For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems
|
||||
will not be attributed erroneously to authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run modified versions of the software inside them, although
|
||||
the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the
|
||||
software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely
|
||||
where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we stand ready to extend this provision to those
|
||||
domains in future versions of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict
|
||||
development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger
|
||||
that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "
|
||||
Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission,
|
||||
other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a
|
||||
work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily
|
||||
liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy.
|
||||
Propagation includes copying, distribution (with or without modification), making available to the public, and in some
|
||||
countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction
|
||||
with a user through a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright
|
||||
notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the extent that warranties are provided), that
|
||||
licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a
|
||||
list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means
|
||||
any non-source form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body,
|
||||
or, in the case of interfaces specified for a particular programming language, one that is widely used among developers
|
||||
working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in
|
||||
the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to
|
||||
enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is
|
||||
available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used
|
||||
to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (
|
||||
for an executable work) run the object code and to modify the work, including scripts to control those activities.
|
||||
However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs
|
||||
which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding
|
||||
Source includes interface definition files associated with source files for the work, and the source code for shared
|
||||
libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data
|
||||
communication or control flow between those subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the
|
||||
Corresponding Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided
|
||||
the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program.
|
||||
The output from running a covered work is covered by this License only if the output, given its content, constitutes a
|
||||
covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not convey, without conditions so long as your license
|
||||
otherwise remains in force. You may convey covered works to others for the sole purpose of having them make
|
||||
modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do not control copyright. Those thus making or running
|
||||
the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that
|
||||
prohibit them from making any copies of your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not
|
||||
allowed; section 10 makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling
|
||||
obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or
|
||||
restricting circumvention of such measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the
|
||||
extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you
|
||||
disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users,
|
||||
your or third parties' legal rights to forbid circumvention of technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating
|
||||
that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices
|
||||
of the absence of any warranty; and give all recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for
|
||||
a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source
|
||||
code under the terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of
|
||||
the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or
|
||||
distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the
|
||||
compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause
|
||||
this License to apply to the other parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License, in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library,
|
||||
need not be included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used
|
||||
for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In
|
||||
determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a
|
||||
particular product received by a particular user, "normally used" refers to a typical or common use of that class of
|
||||
product, regardless of the status of the particular user or of the way in which the particular user actually uses, or
|
||||
expects or is expected to use, the product. A product is a consumer product regardless of whether the product has
|
||||
substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of
|
||||
the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information
|
||||
required to install and execute modified versions of a covered work in that User Product from a modified version of its
|
||||
Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code
|
||||
is in no case prevented or interfered with solely because modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the
|
||||
conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to
|
||||
the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding
|
||||
Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not
|
||||
apply if neither you nor any third party retains the ability to install modified object code on the User Product (for
|
||||
example, the work has been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a requirement to continue to provide support
|
||||
service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product
|
||||
in which it has been modified or installed. Access to a network may be denied when the modification itself materially
|
||||
and adversely affects the operation of the network or violates the rules and protocols for communication across the
|
||||
network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format
|
||||
that is publicly documented (and with an implementation available to the public in source code form), and must require
|
||||
no special password or key for unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of
|
||||
its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were
|
||||
included in this License, to the extent that they are valid under applicable law. If additional permissions apply only
|
||||
to part of the Program, that part may be used separately under those permissions, but the entire Program remains
|
||||
governed by this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or
|
||||
from any part of it. (Additional permissions may be written to require their own removal in certain cases when you
|
||||
modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have
|
||||
or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by
|
||||
the copyright holders of that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the
|
||||
Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with
|
||||
a term that is a further restriction, you may remove that term. If a license document contains a further restriction but
|
||||
permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of
|
||||
that license document, provided that the further restriction does not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a
|
||||
statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as
|
||||
exceptions; the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to
|
||||
propagate or modify it is void, and will automatically terminate your rights under this License (including any patent
|
||||
licenses granted under the third paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your license from a particular copyright holder is
|
||||
reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b)
|
||||
permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days
|
||||
after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you
|
||||
of the violation by some reasonable means, this is the first time you have received notice of violation of this
|
||||
License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the
|
||||
notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the licenses of parties who have received copies or
|
||||
rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not
|
||||
qualify to receive new licenses for the same material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a
|
||||
covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not
|
||||
require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered
|
||||
work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run,
|
||||
modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third
|
||||
parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or
|
||||
subdividing an organization, or merging organizations. If propagation of a covered work results from an entity
|
||||
transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work
|
||||
the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with
|
||||
reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For
|
||||
example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License,
|
||||
and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using,
|
||||
selling, offering for sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the
|
||||
Program is based. The work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already
|
||||
acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or
|
||||
selling its contributor version, but do not include claims that would be infringed only as a consequence of further
|
||||
modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent
|
||||
sublicenses in a manner consistent with the requirements of this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential
|
||||
patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its
|
||||
contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not
|
||||
to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a
|
||||
patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not
|
||||
available for anyone to copy, free of charge and under the terms of this License, through a publicly available network
|
||||
server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available,
|
||||
or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a
|
||||
manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "
|
||||
Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in
|
||||
a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in
|
||||
that country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring
|
||||
conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing
|
||||
them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is
|
||||
automatically extended to all recipients of the covered work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of,
|
||||
or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You
|
||||
may not convey a covered work if you are a party to an arrangement with a third party that is in the business of
|
||||
distributing software, under which you make payment to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a
|
||||
discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from
|
||||
those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered
|
||||
work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to
|
||||
infringement that may otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this
|
||||
License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to
|
||||
satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence
|
||||
you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further
|
||||
conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License
|
||||
would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work
|
||||
licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the
|
||||
resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special
|
||||
requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply
|
||||
to the combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to
|
||||
time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new
|
||||
problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the
|
||||
GNU General Public License "or any later version" applies to it, you have the option of following the terms and
|
||||
conditions either of that numbered version or of any later version published by the Free Software Foundation. If the
|
||||
Program does not specify a version number of the GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used,
|
||||
that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the
|
||||
Program.
|
||||
|
||||
Later license versions may give you additional or different permissions. However, no additional obligations are imposed
|
||||
on any author or copyright holder as a result of your choosing to follow a later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING
|
||||
THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR
|
||||
IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU
|
||||
ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO
|
||||
MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
|
||||
INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO
|
||||
LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
||||
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to
|
||||
their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil
|
||||
liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program
|
||||
in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve
|
||||
this is to make it free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to
|
||||
most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer
|
||||
to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of
|
||||
course, your program's commands might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for
|
||||
the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is
|
||||
a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If
|
||||
this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -1,53 +1,46 @@
|
||||
# IrisBukkit
|
||||
# Iris
|
||||
|
||||
The master branch is for the latest version of minecraft.
|
||||
|
||||
# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://docs.volmit.com/iris/) **
|
||||
|** [Git](https://github.com/IrisDimensions)
|
||||
# [Support](https://discord.gg/3xxPTpT) **|** [Documentation](https://docs.volmit.com/iris/) **|** [Git](https://github.com/IrisDimensions)
|
||||
|
||||
# Building
|
||||
|
||||
Building IrisBukkit is fairly simple, though you will need to setup a few things if your system has never been used for
|
||||
java
|
||||
Building Iris is fairly simple, though you will need to setup a few things if your system has never been used for java
|
||||
development.
|
||||
|
||||
Consider supporting our development by buying IrisBukkit on spigot! We work hard to make IrisBukkit the best it can be
|
||||
for everyone.
|
||||
Consider supporting our development by buying Iris on spigot! We work hard to make Iris the best it can be for everyone.
|
||||
|
||||
## Preface: if you need help compiling and you are a developer / intend to help out in the community or with development we would love to help you regardless in the discord! however do not come to the discord asking for free copies, or a tutorial on how to compile.
|
||||
|
||||
### Command Line Builds
|
||||
|
||||
1. Install [Java JDK 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
|
||||
1. Install [Java JDK 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.
|
||||
* Windows
|
||||
1. Start > Type `env` and press Enter
|
||||
2. Advanced > Environment Variables
|
||||
3. Under System Variables, click `New...`
|
||||
4. Variable Name: `JAVA_HOME`
|
||||
5. Variable Value: `C:\Program Files\Java\jdk-17.0.1` (verify this exists after installing java don't just copy
|
||||
5. Variable Value: `C:\Program Files\Java\jdk-21.0.1` (verify this exists after installing java don't just copy
|
||||
the example text)
|
||||
* MacOS
|
||||
1. Run `/usr/libexec/java_home -V` and look for Java 17
|
||||
1. Run `/usr/libexec/java_home -V` and look for Java 21
|
||||
2. Run `sudo nano ~/.zshenv`
|
||||
3. Add `export JAVA_HOME=$(/usr/libexec/java_home)` as a new line
|
||||
4. Use `CTRL + X`, then Press `Y`, Then `ENTER`
|
||||
5. Quit & Reopen Terminal and verify with `echo $JAVA_HOME`. It should print a directory
|
||||
3. If this is your first time building IrisBukkit for MC 1.18+ run `gradlew setup` inside the root IrisBukkit project
|
||||
folder.
|
||||
Otherwise, skip this step. Grab a coffee, this may take up to 5 minutes depending on your cpu & internet connection.
|
||||
4. Once the project has setup, run `gradlew iris`
|
||||
5. The IrisBukkit jar will be placed in `IrisBukkit/build/IrisBukkit-XXX-XXX.jar` Enjoy! Consider supporting us by
|
||||
buying it on spigot!
|
||||
3. Once the project has setup, run `gradlew iris`
|
||||
4. The Iris jar will be placed in `Iris/build/Iris-XXX-XXX.jar` Enjoy! Consider supporting us by buying it on spigot!
|
||||
|
||||
### IDE Builds (for development)
|
||||
|
||||
* Run `gradlew setup` any time you get dependency issues with craftbukkit
|
||||
* Configure ITJ Gradle to use JDK 17 (in settings, search for gradle)
|
||||
* Add a build line in the build.gradle for your own build task to directly compile IrisBukkit into your plugins folder
|
||||
if you
|
||||
* Configure ITJ Gradle to use JDK 21 (in settings, search for gradle)
|
||||
* Add a build line in the build.gradle for your own build task to directly compile Iris into your plugins folder if you
|
||||
prefer.
|
||||
* Resync the project & run your newly created task (under the development folder in gradle tasks!)
|
||||
|
||||
# IrisBukkit Toolbelt
|
||||
# Iris Toolbelt
|
||||
|
||||
Everyone needs a tool-belt.
|
||||
|
||||
@@ -73,7 +66,6 @@ IrisAccess access=IrisToolbelt.createWorld() // If you like builders...
|
||||
.name("myWorld") // The world name
|
||||
.dimension("terrifyinghands")
|
||||
.seed(69133742) // The world seed
|
||||
.headless(true) // Headless make gen go fast
|
||||
.pregen(PregenTask // Define a pregen job to run
|
||||
.builder()
|
||||
.center(new Position2(0,0)) // REGION coords (1 region = 32x32 chunks)
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id "io.freefair.lombok" version "6.3.0"
|
||||
}
|
||||
|
||||
group 'com.volmit.iris'
|
||||
version '1.0-SNAPSHOT'
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes('Contains-Sources': 'java,class')
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
apply plugin: 'java'
|
||||
|
||||
repositories {
|
||||
maven { url "https://arcanearts.jfrog.io/artifactory/archives" }
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'org.projectlombok:lombok:1.18.24'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.24'
|
||||
annotationProcessor 'systems.manifold:manifold-ext:2022.1.19'
|
||||
implementation 'art.arcane:Source:22.9.3'
|
||||
implementation 'art.arcane:Cram:22.9.3'
|
||||
implementation 'art.arcane:Amulet:22.9.5'
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.1'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
|
||||
testAnnotationProcessor 'systems.manifold:manifold-ext:2022.1.19'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -1,290 +0,0 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
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"
|
||||
}
|
||||
|
||||
group rootProject.group
|
||||
version rootProject.version
|
||||
|
||||
def nmsVersion = "1.19"
|
||||
def apiVersion = '1.19'
|
||||
def spigotJarVersion = '1.19-R0.1-SNAPSHOT'
|
||||
def name = getRootProject().getName() // Defined in settings.gradle
|
||||
def main = 'com.volmit.iris.platform.bukkit.IrisBukkit'
|
||||
|
||||
// ADD YOURSELF AS A NEW LINE IF YOU WANT YOUR OWN BUILD TASK GENERATED
|
||||
// ======================== WINDOWS =============================
|
||||
registerCustomOutputTask('Cyberpwn', 'C://Users/cyberpwn/Documents/development/server/plugins')
|
||||
registerCustomOutputTask('Psycho', 'D://Dan/MinecraftDevelopment/server/plugins')
|
||||
registerCustomOutputTask('ArcaneArts', 'C://Users/arcane/Documents/development/server/plugins')
|
||||
registerCustomOutputTask('Coco', 'D://Documents/MC/plugins')
|
||||
registerCustomOutputTask('Strange', 'D://Servers/1.17 Test Server/plugins')
|
||||
registerCustomOutputTask('Vatuu', 'D://Minecraft/Servers/1.19/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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 'art.arcane', 'com.volmit.iris.util.arcane'
|
||||
relocate 'manifold', 'com.volmit.iris.util.manifold'
|
||||
relocate 'art.arcane.source', 'com.volmit.iris.util.source'
|
||||
relocate 'ChumBukkit.extensions', 'com.volmit.iris.util.extensions'
|
||||
relocate 'IrisBukkit.extensions', 'com.volmit.iris.util.extensions'
|
||||
relocate 'Fukkit.extensions', 'com.volmit.iris.util.extensions'
|
||||
relocate 'Amulet.extensions', 'com.volmit.iris.util.extensions'
|
||||
relocate 'Iris.extensions', 'com.volmit.iris.util.extensions'
|
||||
dependencies {
|
||||
include(dependency('art.arcane:'))
|
||||
include(dependency('systems.manifold:'))
|
||||
include(dependency("com.dfsek:Paralithic:"))
|
||||
include(dependency(":engine"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Faster dependency caches
|
||||
*/
|
||||
configurations.all {
|
||||
resolutionStrategy.cacheChangingModulesFor 60, 'minutes'
|
||||
resolutionStrategy.cacheDynamicVersionsFor 60, 'minutes'
|
||||
}
|
||||
|
||||
configurations {
|
||||
testImplementation.extendsFrom annotationProcessor
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven { url "https://arcanearts.jfrog.io/artifactory/archives" }
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.spigotmc:spigot-api:1.19-R0.1-SNAPSHOT'
|
||||
implementation 'art.arcane:Fukkit:22.9.2'
|
||||
implementation project(":engine")
|
||||
}
|
||||
|
||||
if (JavaVersion.current() != JavaVersion.VERSION_1_8 &&
|
||||
sourceSets.main.allJava.files.any { it.name == "module-info.java" }) {
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs += ['-Xplugin:Manifold', '--module-path', it.classpath.asPath]
|
||||
}
|
||||
} else {
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs += ['-Xplugin:Manifold']
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
System.out.println(buildDir);
|
||||
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/bukkit-" + version + "-all.jar");
|
||||
def ssiJar = new File(buildDir, "specialsource/bukkit-" + 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/1.10.0/SpecialSource-1.10.0-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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
package com.volmit.iris.platform.bukkit;
|
||||
|
||||
import art.arcane.amulet.io.IO;
|
||||
import art.arcane.amulet.logging.LogListener;
|
||||
import com.volmit.iris.engine.EngineConfiguration;
|
||||
import com.volmit.iris.platform.IrisPlatform;
|
||||
import com.volmit.iris.platform.PlatformBiome;
|
||||
import com.volmit.iris.platform.block.PlatformBlock;
|
||||
import com.volmit.iris.platform.PlatformNamespaceKey;
|
||||
import com.volmit.iris.platform.PlatformWorld;
|
||||
import com.volmit.iris.platform.bukkit.wrapper.BukkitBiome;
|
||||
import com.volmit.iris.platform.bukkit.wrapper.BukkitBlock;
|
||||
import com.volmit.iris.platform.bukkit.wrapper.BukkitKey;
|
||||
import com.volmit.iris.platform.bukkit.wrapper.BukkitWorld;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Warning;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.WorldCreator;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.command.ConsoleCommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class IrisBukkit extends JavaPlugin implements IrisPlatform, LogListener {
|
||||
private static final String TAG = ChatColor.DARK_GRAY + "[" + ChatColor.GREEN + "Iris" + ChatColor.DARK_GREEN + "/";
|
||||
private static final String TAG_STRIPPED = ChatColor.stripColor(TAG);
|
||||
private static IrisBukkit instance;
|
||||
private ConsoleCommandSender consoleSender;
|
||||
|
||||
public void onEnable() {
|
||||
instance = this;
|
||||
consoleSender = Bukkit.getConsoleSender();
|
||||
LogListener.listener.set(this);
|
||||
|
||||
getServer().getScheduler().scheduleSyncDelayedTask(this, () -> {
|
||||
World world = Bukkit.createWorld(new WorldCreator("iristests/" + UUID.randomUUID()).generator(new IrisBukkitChunkGenerator(this, EngineConfiguration.builder()
|
||||
.timings(true).mutable(true)
|
||||
.build())));
|
||||
for(Player i : Bukkit.getOnlinePlayers())
|
||||
{
|
||||
i.teleport(world.getSpawnLocation());
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
public void onDisable() {
|
||||
World w = null;
|
||||
for(World i : Bukkit.getWorlds()) {
|
||||
if(!i.getName().startsWith("iristest")) {
|
||||
w = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for(World i : Bukkit.getWorlds()) {
|
||||
if(i.getName().startsWith("iristest"))
|
||||
{
|
||||
for(Player j : i.getPlayers())
|
||||
{
|
||||
j.teleport(w.getSpawnLocation());
|
||||
}
|
||||
|
||||
if(i.getGenerator() instanceof Closeable c)
|
||||
{
|
||||
try {
|
||||
c.close();
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
BukkitWorld.of(i).unloadChunks(false, true);
|
||||
File folder = i.getWorldFolder();
|
||||
Bukkit.unloadWorld(i, false);
|
||||
IO.delete(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IrisBukkit getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void messageConsole(String color, String key, Object o) {
|
||||
try {
|
||||
consoleSender.sendMessage(TAG + key + ChatColor.DARK_GRAY + "]: " + color + o.toString());
|
||||
}
|
||||
|
||||
catch(Throwable e) {
|
||||
System.out.println(TAG_STRIPPED + key + "]: " + o.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logError(String k, Object o) {
|
||||
messageConsole(ChatColor.RED.toString(), k, o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logInfo(String k, Object o) {
|
||||
messageConsole(ChatColor.WHITE.toString(), k, o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logWarning(String k, Object o) {
|
||||
messageConsole(ChatColor.YELLOW.toString(), k, o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logDebug(String k, Object o) {
|
||||
messageConsole(ChatColor.GRAY.toString(), k, o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPlatformName() {
|
||||
return "Bukkit";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<PlatformBlock> getBlocks() {
|
||||
//This is because it's a method extension
|
||||
//noinspection Convert2MethodRef
|
||||
return Arrays.stream(Material.values())
|
||||
.filter(i -> !i.isLegacy())
|
||||
.filter(Material::isBlock)
|
||||
.map(Material::createBlockData).map(i -> BukkitBlock.of(i));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<PlatformBiome> getBiomes() {
|
||||
//This is because it's a method extension
|
||||
//noinspection Convert2MethodRef
|
||||
return Arrays.stream(Biome.values()).parallel().filter((i) -> i != Biome.CUSTOM).map(i -> BukkitBiome.of(i));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWorldLoaded(String name) {
|
||||
return Bukkit.getWorlds().keepWhere(i -> i.getName().equals(name)).isNotEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformWorld getWorld(String name) {
|
||||
World w = Bukkit.getWorld(name);
|
||||
|
||||
if(w == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return BukkitWorld.of(w);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlock parseBlock(String raw) {
|
||||
return BukkitBlock.of(Bukkit.createBlockData(raw));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformNamespaceKey key(String namespace, String key) {
|
||||
return BukkitKey.of(namespace, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getStudioFolder(String dimension) {
|
||||
File f = new File(getDataFolder(), "packs/" + dimension);
|
||||
f.mkdirs();
|
||||
return f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getStudioFolder() {
|
||||
File f = new File(getDataFolder(), "packs/");
|
||||
f.mkdirs();
|
||||
return f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
|
||||
return new IrisBukkitChunkGenerator(this, EngineConfiguration.builder()
|
||||
.threads(4)
|
||||
.mutable(true)
|
||||
.timings(true)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void i(String s, Object o) {
|
||||
logInfo(s, o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void f(String s, Object o) {
|
||||
logError(s, o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void w(String s, Object o) {
|
||||
logWarning(s, o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void d(String s, Object o) {
|
||||
logDebug(s, o);
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package com.volmit.iris.platform.bukkit;
|
||||
|
||||
import art.arcane.chrono.Average;
|
||||
import art.arcane.chrono.PrecisionStopwatch;
|
||||
import art.arcane.spatial.hunk.Hunk;
|
||||
import com.volmit.iris.engine.EngineConfiguration;
|
||||
import com.volmit.iris.engine.Engine;
|
||||
import com.volmit.iris.engine.feature.FeatureSizedTarget;
|
||||
import com.volmit.iris.engine.feature.FeatureTarget;
|
||||
import com.volmit.iris.engine.pipeline.PipedHunkStack;
|
||||
import com.volmit.iris.platform.IrisPlatform;
|
||||
import com.volmit.iris.platform.block.PlatformBlock;
|
||||
import com.volmit.iris.platform.bukkit.util.StaticBiomeProvider;
|
||||
import com.volmit.iris.platform.bukkit.wrapper.BukkitWorld;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.generator.BiomeProvider;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import com.volmit.iris.platform.bukkit.util.ChunkDataHunkView;
|
||||
import org.bukkit.generator.WorldInfo;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class IrisBukkitChunkGenerator extends ChunkGenerator implements Closeable {
|
||||
private final IrisPlatform platform;
|
||||
private final EngineConfiguration configuration;
|
||||
private final AtomicReference<Engine> engine;
|
||||
private final ReentrantLock engineLock;
|
||||
private final AtomicInteger perSecond;
|
||||
private final PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||
private final Average a = new Average(128);
|
||||
private final StaticBiomeProvider staticBiomeProvider;
|
||||
|
||||
public IrisBukkitChunkGenerator(IrisPlatform platform, EngineConfiguration configuration) {
|
||||
this.perSecond = new AtomicInteger(0);
|
||||
this.platform = platform;
|
||||
this.configuration = configuration;
|
||||
this.staticBiomeProvider = new StaticBiomeProvider(Biome.PLAINS);
|
||||
this.engine = new AtomicReference<>();
|
||||
this.engineLock = new ReentrantLock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkData generateChunkData(World world, Random random, int x, int z, BiomeGrid biome) {
|
||||
PrecisionStopwatch pp = PrecisionStopwatch.start();
|
||||
initEngine(world);
|
||||
ChunkData data = Bukkit.createChunkData(world);
|
||||
Hunk<PlatformBlock> chunk = new ChunkDataHunkView(data);
|
||||
FeatureSizedTarget targetSize = FeatureSizedTarget.builder()
|
||||
.width(chunk.getWidth())
|
||||
.height(chunk.getHeight())
|
||||
.depth(chunk.getDepth())
|
||||
.offsetX(x << 4)
|
||||
.offsetZ(z << 4)
|
||||
.offsetY(0)
|
||||
.build();
|
||||
FeatureTarget<PlatformBlock> blockTarget = new FeatureTarget<>(chunk, targetSize);
|
||||
PipedHunkStack stack = new PipedHunkStack();
|
||||
stack.register(PlatformBlock.class, blockTarget);
|
||||
engine.get().getPlumbing().generate(engine.get(), targetSize, stack);
|
||||
perSecond.incrementAndGet();
|
||||
a.put(pp.getMilliseconds());
|
||||
|
||||
if(p.getMilliseconds() > 1000)
|
||||
{
|
||||
p.reset();
|
||||
p.begin();
|
||||
System.out.println("PERSECOND: " + perSecond.getAndSet(0) + " AMS: " + ((int)Math.round(a.getAverage())) + "ms");
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private void initEngine(World world) {
|
||||
if(engine.get() == null)
|
||||
{
|
||||
engineLock.lock();
|
||||
|
||||
if(engine.get() == null)
|
||||
{
|
||||
try {
|
||||
engine.set(new Engine(platform, BukkitWorld.of(world), configuration));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
engineLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSpawn(World world, int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiomeProvider getDefaultBiomeProvider(WorldInfo worldInfo) {
|
||||
return staticBiomeProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
engine.get().close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateNoise() {
|
||||
return super.shouldGenerateNoise();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateSurface() {
|
||||
return super.shouldGenerateSurface();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateBedrock() {
|
||||
return super.shouldGenerateBedrock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateCaves() {
|
||||
return super.shouldGenerateCaves();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateDecorations() {
|
||||
return super.shouldGenerateDecorations();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateMobs() {
|
||||
return super.shouldGenerateMobs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateStructures() {
|
||||
return super.shouldGenerateStructures();
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.volmit.iris.platform.bukkit.util;
|
||||
|
||||
import art.arcane.spatial.hunk.Hunk;
|
||||
import com.volmit.iris.platform.block.PlatformBlock;
|
||||
import com.volmit.iris.platform.bukkit.wrapper.BukkitBlock;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
|
||||
@SuppressWarnings("ClassCanBeRecord")
|
||||
public class ChunkDataHunkView implements Hunk<PlatformBlock> {
|
||||
private final ChunkGenerator.ChunkData chunk;
|
||||
|
||||
public ChunkDataHunkView(ChunkGenerator.ChunkData chunk) {
|
||||
this.chunk = chunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() {
|
||||
return 16;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDepth() {
|
||||
return 16;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return chunk.getMaxHeight() - chunk.getMinHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void set(int x1, int y1, int z1, int x2, int y2, int z2, PlatformBlock t) {
|
||||
if(t == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
chunk.setRegion(x1, y1 + chunk.getMinHeight(), z1, x2, y2 + chunk.getMinHeight(), z2, ((BukkitBlock)t).getDelegate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setRaw(int x, int y, int z, PlatformBlock t) {
|
||||
if(t == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
chunk.setBlock(x, y + chunk.getMinHeight(), z, ((BukkitBlock)t).getDelegate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlock getRaw(int x, int y, int z) {
|
||||
return BukkitBlock.of(chunk.getBlockData(x, y + chunk.getMinHeight(), z));
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.volmit.iris.platform.bukkit.util;
|
||||
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.generator.BiomeProvider;
|
||||
import org.bukkit.generator.WorldInfo;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class StaticBiomeProvider extends BiomeProvider {
|
||||
private static final List<Biome> ALL_BIOMES = Arrays.stream(Biome.values()).without((i) -> i.equals(Biome.CUSTOM)).toList().unmodifiable();
|
||||
private final Biome defaultBiome;
|
||||
|
||||
public StaticBiomeProvider(Biome defaultBiome) {
|
||||
this.defaultBiome = defaultBiome;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Biome getBiome(WorldInfo worldInfo, int x, int y, int z) {
|
||||
return defaultBiome;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Biome> getBiomes(WorldInfo worldInfo) {
|
||||
return ALL_BIOMES;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.volmit.iris.platform.bukkit.wrapper;
|
||||
|
||||
import com.volmit.iris.platform.PlatformBiome;
|
||||
import com.volmit.iris.platform.PlatformNamespaceKey;
|
||||
import lombok.Data;
|
||||
import org.bukkit.block.Biome;
|
||||
|
||||
@Data
|
||||
public class BukkitBiome implements PlatformBiome {
|
||||
private final Biome delegate;
|
||||
|
||||
private BukkitBiome(Biome delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformNamespaceKey getKey() {
|
||||
return BukkitKey.of(delegate.getKey());
|
||||
}
|
||||
|
||||
public static BukkitBiome of(Biome biome) {
|
||||
return new BukkitBiome(biome);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package com.volmit.iris.platform.bukkit.wrapper;
|
||||
|
||||
import com.volmit.iris.platform.block.PlatformBlock;
|
||||
import com.volmit.iris.platform.PlatformNamespaceKey;
|
||||
import com.volmit.iris.platform.block.PlatformBlock;
|
||||
import lombok.Data;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.data.Ageable;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class BukkitBlock implements PlatformBlock {
|
||||
private final BlockData delegate;
|
||||
private final Map<String, String> properties;
|
||||
|
||||
private BukkitBlock(BlockData delegate, Map<String, String> properties) {
|
||||
this.delegate = delegate;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
private BukkitBlock(BlockData delegate) {
|
||||
this.delegate = delegate;
|
||||
String s = delegate.getAsString(true);
|
||||
|
||||
if(s.contains("[")) {
|
||||
Map<String, String> properties = new HashMap<>();
|
||||
String[] props = new String[] {s.split("\\Q[\\E")[1].split("\\Q]\\E")[0]};
|
||||
String[] p;
|
||||
if(props[0].contains(",")) {
|
||||
for(String i : props[0].split("\\Q,\\E")) {
|
||||
p = i.split("\\Q=\\E");
|
||||
properties.put(p[0], p[1]);
|
||||
}
|
||||
} else {
|
||||
p = props[0].split("\\Q=\\E");
|
||||
properties.put(p[0], p[1]);
|
||||
}
|
||||
|
||||
this.properties = Collections.unmodifiableMap(properties);
|
||||
} else {
|
||||
this.properties = Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getProperties() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformNamespaceKey getKey() {
|
||||
return BukkitKey.of(delegate.getMaterial().getKey());
|
||||
}
|
||||
|
||||
public static BukkitBlock of(BlockData blockData) {
|
||||
return new BukkitBlock(blockData);
|
||||
}
|
||||
|
||||
public static BukkitBlock of(Material material) {
|
||||
return of(material.createBlockData());
|
||||
}
|
||||
|
||||
public static BukkitBlock of(String raw) {
|
||||
return of(Bukkit.createBlockData(raw));
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.volmit.iris.platform.bukkit.wrapper;
|
||||
|
||||
import com.volmit.iris.platform.PlatformChunk;
|
||||
import org.bukkit.Chunk;
|
||||
|
||||
public class BukkitChunk implements PlatformChunk {
|
||||
private final Chunk delegate;
|
||||
|
||||
private BukkitChunk(Chunk delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getX() {
|
||||
return delegate.getX();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getZ() {
|
||||
return delegate.getZ();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unload(boolean save, boolean force) {
|
||||
if(force) {
|
||||
delegate.getPluginChunkTickets().forEach(delegate::removePluginChunkTicket);
|
||||
}
|
||||
|
||||
delegate.unload(save);
|
||||
}
|
||||
|
||||
public static BukkitChunk of(Chunk chunk) {
|
||||
return new BukkitChunk(chunk);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package com.volmit.iris.platform.bukkit.wrapper;
|
||||
|
||||
import com.volmit.iris.platform.PlatformNamespaceKey;
|
||||
import lombok.Data;
|
||||
import org.bukkit.NamespacedKey;
|
||||
|
||||
@Data
|
||||
public class BukkitKey implements PlatformNamespaceKey {
|
||||
private final NamespacedKey delegate;
|
||||
|
||||
private BukkitKey(NamespacedKey delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNamespace() {
|
||||
return delegate.getNamespace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return delegate.getKey();
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return delegate.toString();
|
||||
}
|
||||
|
||||
public static BukkitKey of(String nsk) {
|
||||
if(nsk.contains(":")) {
|
||||
String[] f = nsk.split("\\Q:\\E");
|
||||
return of(f[0], f[1]);
|
||||
}
|
||||
|
||||
return of("minecraft", nsk);
|
||||
}
|
||||
|
||||
public static BukkitKey of(String namespace, String key) {
|
||||
return of(new NamespacedKey(namespace, key));
|
||||
}
|
||||
|
||||
public static BukkitKey of(NamespacedKey key) {
|
||||
return new BukkitKey(key);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package com.volmit.iris.platform.bukkit.wrapper;
|
||||
|
||||
import art.arcane.amulet.geometry.Vec;
|
||||
import com.volmit.iris.platform.PlatformPlayer;
|
||||
import com.volmit.iris.platform.PlatformWorld;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class BukkitPlayer implements PlatformPlayer {
|
||||
private final Player delegate;
|
||||
|
||||
private BukkitPlayer(Player delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getUUID() {
|
||||
return delegate.getUniqueId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return delegate.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec getLocation() {
|
||||
return delegate.getLocation().vec();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformWorld getWorld() {
|
||||
return BukkitWorld.of(delegate.getWorld());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUseIris() {
|
||||
return delegate.isOp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(String message) {
|
||||
delegate.sendMessage(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendActionBar(String message) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendTitleMessage(String title, String subtitle, int in, int stay, int out) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public static BukkitPlayer of(Player player) {
|
||||
return new BukkitPlayer(player);
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package com.volmit.iris.platform.bukkit.wrapper;
|
||||
|
||||
import com.volmit.iris.platform.PlatformBiome;
|
||||
import com.volmit.iris.platform.block.PlatformBlock;
|
||||
import com.volmit.iris.platform.PlatformChunk;
|
||||
import com.volmit.iris.platform.PlatformPlayer;
|
||||
import com.volmit.iris.platform.PlatformWorld;
|
||||
import com.volmit.iris.util.WorldHeight;
|
||||
import lombok.Data;
|
||||
import org.bukkit.World;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Data
|
||||
public class BukkitWorld implements PlatformWorld {
|
||||
private final World delegate;
|
||||
private final WorldHeight worldHeight;
|
||||
|
||||
private BukkitWorld(World delegate) {
|
||||
this.delegate = delegate;
|
||||
this.worldHeight = new WorldHeight(delegate.getMinHeight(), delegate.getMaxHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorldHeight getHeight() {
|
||||
return worldHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return delegate.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFolder() {
|
||||
return delegate.getWorldFolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<PlatformPlayer> getPlayers() {
|
||||
//noinspection Convert2MethodRef
|
||||
return delegate.getPlayers().stream().map(i -> BukkitPlayer.of(i)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<PlatformChunk> getLoadedChunks() {
|
||||
//noinspection Convert2MethodRef
|
||||
return Arrays.stream(delegate.getLoadedChunks()).map(i -> BukkitChunk.of(i)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformChunk getOrLoadChunk(int x, int z) {
|
||||
return BukkitChunk.of(delegate.getChunkAt(x, z));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlock getBlock(int x, int y, int z) {
|
||||
return BukkitBlock.of(delegate.getBlockAt(x, y, z).getBlockData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBiome getBiome(int x, int y, int z) {
|
||||
return BukkitBiome.of(delegate.getBiome(x, y, z));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSeed() {
|
||||
return delegate.getSeed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChunkLoaded(int x, int z) {
|
||||
return delegate.isChunkLoaded(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBlock(int x, int y, int z, PlatformBlock block) {
|
||||
delegate.setBlockData(x, y, z, ((BukkitBlock) block).getDelegate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBiome(int x, int y, int z, PlatformBiome biome) {
|
||||
delegate.setBiome(x, y, z, ((BukkitBiome) biome).getDelegate());
|
||||
}
|
||||
|
||||
public static BukkitWorld of(World world) {
|
||||
return new BukkitWorld(world);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
name: Iris
|
||||
main: com.volmit.iris.platform.bukkit.IrisBukkit
|
||||
version: 1.0.0
|
||||
api-version: 1.19
|
||||
libraries:
|
||||
- org.apache-extras.beanshell:bsh:2.0b6
|
||||
- com.google.code.gson:gson:2.8.9
|
||||
- com.github.ben-manes.caffeine:caffeine:3.1.1
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -0,0 +1,776 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.volmit.iris.core.IrisSettings;
|
||||
import com.volmit.iris.core.IrisWorlds;
|
||||
import com.volmit.iris.core.ServerConfigurator;
|
||||
import com.volmit.iris.core.link.IrisPapiExpansion;
|
||||
import com.volmit.iris.core.link.MultiverseCoreLink;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.core.nms.INMS;
|
||||
import com.volmit.iris.core.pregenerator.LazyPregenerator;
|
||||
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.EnginePanic;
|
||||
import com.volmit.iris.engine.object.IrisCompat;
|
||||
import com.volmit.iris.engine.object.IrisDimension;
|
||||
import com.volmit.iris.engine.object.IrisWorld;
|
||||
import com.volmit.iris.engine.platform.BukkitChunkGenerator;
|
||||
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.KMap;
|
||||
import com.volmit.iris.util.exceptions.IrisException;
|
||||
import com.volmit.iris.util.format.C;
|
||||
import com.volmit.iris.util.function.NastyRunnable;
|
||||
import com.volmit.iris.util.io.FileWatcher;
|
||||
import com.volmit.iris.util.io.IO;
|
||||
import com.volmit.iris.util.io.InstanceState;
|
||||
import com.volmit.iris.util.io.JarScanner;
|
||||
import com.volmit.iris.util.math.M;
|
||||
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.plugin.IrisService;
|
||||
import com.volmit.iris.util.plugin.VolmitPlugin;
|
||||
import com.volmit.iris.util.plugin.VolmitSender;
|
||||
import com.volmit.iris.util.plugin.chunk.ChunkTickets;
|
||||
import com.volmit.iris.util.scheduling.J;
|
||||
import com.volmit.iris.util.scheduling.Queue;
|
||||
import com.volmit.iris.util.scheduling.ShurikenQueue;
|
||||
import lombok.NonNull;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.*;
|
||||
import org.bukkit.generator.BiomeProvider;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.bukkit.plugin.IllegalPluginAccessException;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@SuppressWarnings("CanBeFinal")
|
||||
public class Iris extends VolmitPlugin implements Listener {
|
||||
private static final Queue<Runnable> syncJobs = new ShurikenQueue<>();
|
||||
|
||||
public static Iris instance;
|
||||
public static MultiverseCoreLink linkMultiverseCore;
|
||||
public static IrisCompat compat;
|
||||
public static FileWatcher configWatcher;
|
||||
public static ChunkTickets tickets;
|
||||
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 {
|
||||
try {
|
||||
InstanceState.updateInstanceId();
|
||||
} catch (Throwable ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private final KList<Runnable> postShutdown = new KList<>();
|
||||
private KMap<Class<? extends IrisService>, IrisService> services;
|
||||
|
||||
public static VolmitSender getSender() {
|
||||
if (sender == null) {
|
||||
sender = new VolmitSender(Bukkit.getConsoleSender());
|
||||
sender.setTag(instance.getTag());
|
||||
}
|
||||
return sender;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T service(Class<T> c) {
|
||||
return (T) instance.services.get(c);
|
||||
}
|
||||
|
||||
public static void callEvent(Event e) {
|
||||
if (!e.isAsynchronous()) {
|
||||
J.s(() -> Bukkit.getPluginManager().callEvent(e));
|
||||
} else {
|
||||
Bukkit.getPluginManager().callEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static KList<Object> initialize(String s, Class<? extends Annotation> slicedClass) {
|
||||
JarScanner js = new JarScanner(instance.getJarFile(), s);
|
||||
KList<Object> v = new KList<>();
|
||||
J.attempt(js::scan);
|
||||
for (Class<?> i : js.getClasses()) {
|
||||
if (slicedClass == null || i.isAnnotationPresent(slicedClass)) {
|
||||
try {
|
||||
v.add(i.getDeclaredConstructor().newInstance());
|
||||
} catch (Throwable ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public static KList<Class<?>> getClasses(String s, Class<? extends Annotation> slicedClass) {
|
||||
JarScanner js = new JarScanner(instance.getJarFile(), s);
|
||||
KList<Class<?>> v = new KList<>();
|
||||
J.attempt(js::scan);
|
||||
for (Class<?> i : js.getClasses()) {
|
||||
if (slicedClass == null || i.isAnnotationPresent(slicedClass)) {
|
||||
try {
|
||||
v.add(i);
|
||||
} catch (Throwable ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public static KList<Object> initialize(String s) {
|
||||
return initialize(s, null);
|
||||
}
|
||||
|
||||
public static void sq(Runnable r) {
|
||||
synchronized (syncJobs) {
|
||||
syncJobs.queue(r);
|
||||
}
|
||||
}
|
||||
|
||||
public static File getTemp() {
|
||||
return instance.getDataFolder("cache", "temp");
|
||||
}
|
||||
|
||||
public static void msg(String string) {
|
||||
try {
|
||||
getSender().sendMessage(string);
|
||||
} catch (Throwable e) {
|
||||
try {
|
||||
instance.getLogger().info(instance.getTag() + string.replaceAll("(<([^>]+)>)", ""));
|
||||
} catch (Throwable ignored1) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static File getCached(String name, String url) {
|
||||
String h = IO.hash(name + "@" + url);
|
||||
File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h);
|
||||
|
||||
if (!f.exists()) {
|
||||
try (BufferedInputStream in = new BufferedInputStream(new URL(url).openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) {
|
||||
byte[] dataBuffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
|
||||
fileOutputStream.write(dataBuffer, 0, bytesRead);
|
||||
Iris.verbose("Aquiring " + name);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return f.exists() ? f : null;
|
||||
}
|
||||
|
||||
public static String getNonCached(String name, String url) {
|
||||
String h = IO.hash(name + "*" + url);
|
||||
File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h);
|
||||
|
||||
try (BufferedInputStream in = new BufferedInputStream(new URL(url).openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) {
|
||||
byte[] dataBuffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
|
||||
fileOutputStream.write(dataBuffer, 0, bytesRead);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
return IO.readAll(f);
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public static File getNonCachedFile(String name, String url) {
|
||||
String h = IO.hash(name + "*" + url);
|
||||
File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h);
|
||||
Iris.verbose("Download " + name + " -> " + url);
|
||||
try (BufferedInputStream in = new BufferedInputStream(new URL(url).openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) {
|
||||
byte[] dataBuffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
|
||||
fileOutputStream.write(dataBuffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
fileOutputStream.flush();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
public static void warn(String format, Object... objs) {
|
||||
msg(C.YELLOW + String.format(format, objs));
|
||||
}
|
||||
|
||||
public static void error(String format, Object... objs) {
|
||||
msg(C.RED + String.format(format, objs));
|
||||
}
|
||||
|
||||
public static void debug(String string) {
|
||||
if (!IrisSettings.get().getGeneral().isDebug()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
throw new RuntimeException();
|
||||
} catch (Throwable e) {
|
||||
try {
|
||||
String[] cc = e.getStackTrace()[1].getClassName().split("\\Q.\\E");
|
||||
|
||||
if (cc.length > 5) {
|
||||
debug(cc[3] + "/" + cc[4] + "/" + cc[cc.length - 1], e.getStackTrace()[1].getLineNumber(), string);
|
||||
} else {
|
||||
debug(cc[3] + "/" + cc[4], e.getStackTrace()[1].getLineNumber(), string);
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
debug("Origin", -1, string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void debug(String category, int line, String string) {
|
||||
if (!IrisSettings.get().getGeneral().isDebug()) {
|
||||
return;
|
||||
}
|
||||
if (IrisSettings.get().getGeneral().isUseConsoleCustomColors()) {
|
||||
msg("<gradient:#095fe0:#a848db>" + category + " <#bf3b76>" + line + "<reset> " + C.LIGHT_PURPLE + string.replaceAll("\\Q<\\E", "[").replaceAll("\\Q>\\E", "]"));
|
||||
} else {
|
||||
msg(C.BLUE + category + ":" + C.AQUA + line + C.RESET + C.LIGHT_PURPLE + " " + string.replaceAll("\\Q<\\E", "[").replaceAll("\\Q>\\E", "]"));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static void verbose(String string) {
|
||||
debug(string);
|
||||
}
|
||||
|
||||
public static void success(String string) {
|
||||
msg(C.IRIS + string);
|
||||
}
|
||||
|
||||
public static void info(String format, Object... args) {
|
||||
msg(C.WHITE + String.format(format, args));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public static void later(NastyRunnable object) {
|
||||
try {
|
||||
Bukkit.getScheduler().scheduleAsyncDelayedTask(instance, () ->
|
||||
{
|
||||
try {
|
||||
object.run();
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}, RNG.r.i(100, 1200));
|
||||
} catch (IllegalPluginAccessException ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static int jobCount() {
|
||||
return syncJobs.size();
|
||||
}
|
||||
|
||||
public static void clearQueues() {
|
||||
synchronized (syncJobs) {
|
||||
syncJobs.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static int getJavaVersion() {
|
||||
String version = System.getProperty("java.version");
|
||||
if (version.startsWith("1.")) {
|
||||
version = version.substring(2, 3);
|
||||
} else {
|
||||
int dot = version.indexOf(".");
|
||||
if (dot != -1) {
|
||||
version = version.substring(0, dot);
|
||||
}
|
||||
}
|
||||
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) {
|
||||
if (IrisSettings.get().getGeneral().isDebug()) {
|
||||
File f = instance.getDataFile("debug", "chunk-errors", "chunk." + x + "." + z + ".txt");
|
||||
|
||||
if (!f.exists()) {
|
||||
J.attempt(() -> {
|
||||
PrintWriter pw = new PrintWriter(f);
|
||||
pw.println("Thread: " + Thread.currentThread().getName());
|
||||
pw.println("First: " + new Date(M.ms()));
|
||||
e.printStackTrace(pw);
|
||||
pw.close();
|
||||
});
|
||||
}
|
||||
|
||||
Iris.debug("Chunk " + x + "," + z + " Exception Logged: " + e.getClass().getSimpleName() + ": " + C.RESET + "" + C.LIGHT_PURPLE + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void reportError(Throwable e) {
|
||||
Bindings.capture(e);
|
||||
if (IrisSettings.get().getGeneral().isDebug()) {
|
||||
String n = e.getClass().getCanonicalName() + "-" + e.getStackTrace()[0].getClassName() + "-" + e.getStackTrace()[0].getLineNumber();
|
||||
|
||||
if (e.getCause() != null) {
|
||||
n += "-" + e.getCause().getStackTrace()[0].getClassName() + "-" + e.getCause().getStackTrace()[0].getLineNumber();
|
||||
}
|
||||
|
||||
File f = instance.getDataFile("debug", "caught-exceptions", n + ".txt");
|
||||
|
||||
if (!f.exists()) {
|
||||
J.attempt(() -> {
|
||||
PrintWriter pw = new PrintWriter(f);
|
||||
pw.println("Thread: " + Thread.currentThread().getName());
|
||||
pw.println("First: " + new Date(M.ms()));
|
||||
e.printStackTrace(pw);
|
||||
pw.close();
|
||||
});
|
||||
}
|
||||
|
||||
Iris.debug("Exception Logged: " + e.getClass().getSimpleName() + ": " + C.RESET + "" + C.LIGHT_PURPLE + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void dump() {
|
||||
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);
|
||||
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.println("[%%__USER__%%,%%__RESOURCE__%%,%%__PRODUCT__%%,%%__BUILTBYBIT__%%]");
|
||||
|
||||
pw.close();
|
||||
Iris.info("DUMPED! See " + fi.getAbsolutePath());
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void panic() {
|
||||
EnginePanic.panic();
|
||||
}
|
||||
|
||||
public static void addPanic(String s, String v) {
|
||||
EnginePanic.add(s, v);
|
||||
}
|
||||
|
||||
public Iris() {
|
||||
instance = this;
|
||||
SlimJar.load();
|
||||
}
|
||||
|
||||
private void enable() {
|
||||
services = new KMap<>();
|
||||
Bindings.setupSentry();
|
||||
initialize("com.volmit.iris.core.service").forEach((i) -> services.put((Class<? extends IrisService>) i.getClass(), (IrisService) i));
|
||||
IO.delete(WorldStorage.get().worldFolder(STUDIO_WORLDS_FOLDER));
|
||||
compat = IrisCompat.configured(getDataFile("compat.json"));
|
||||
adoptLegacyWorldData();
|
||||
ServerConfigurator.configure();
|
||||
IrisSafeguard.execute();
|
||||
getSender().setTag(getTag());
|
||||
IrisSafeguard.splash();
|
||||
tickets = new ChunkTickets();
|
||||
linkMultiverseCore = new MultiverseCoreLink();
|
||||
configWatcher = new FileWatcher(getDataFile("settings.json"));
|
||||
services.values().forEach(IrisService::onEnable);
|
||||
services.values().forEach(this::registerListener);
|
||||
addShutdownHook();
|
||||
J.s(() -> {
|
||||
J.a(() -> IO.delete(getTemp()));
|
||||
J.a(LazyPregenerator::loadLazyGenerators, 100);
|
||||
J.a(this::bstats);
|
||||
J.ar(this::checkConfigHotload, 60);
|
||||
J.sr(this::tickQueue, 0);
|
||||
J.s(this::setupPapi);
|
||||
J.a(ServerConfigurator::configure, 20);
|
||||
|
||||
autoStartStudio();
|
||||
checkForBukkitWorlds(s -> true);
|
||||
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
|
||||
IrisToolbelt.retainMantleDataForSlice(BlockData.class.getCanonicalName());
|
||||
});
|
||||
}
|
||||
|
||||
private void adoptLegacyWorldData() {
|
||||
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 {
|
||||
IrisWorlds.readBukkitWorlds().forEach((s, generator) -> {
|
||||
try {
|
||||
if (Bukkit.getWorld(s) != null || !filter.test(s)) return;
|
||||
|
||||
Iris.info("Loading World: %s | Generator: %s", s, generator);
|
||||
var gen = getDefaultWorldGenerator(s, generator);
|
||||
var dim = loadDimension(s, generator);
|
||||
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();
|
||||
}
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void autoStartStudio() {
|
||||
if (IrisSettings.get().getStudio().isAutoStartDefaultStudio()) {
|
||||
Iris.info("Starting up auto Studio!");
|
||||
try {
|
||||
Player r = new KList<>(getServer().getOnlinePlayers()).getRandom();
|
||||
Iris.service(StudioSVC.class).open(r != null ? new VolmitSender(r) : getSender(), 1337, IrisSettings.get().getGenerator().getDefaultWorldType(), (w) -> {
|
||||
J.s(() -> {
|
||||
var spawn = w.getSpawnLocation();
|
||||
for (Player i : getServer().getOnlinePlayers()) {
|
||||
i.setGameMode(GameMode.SPECTATOR);
|
||||
i.teleport(spawn);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (IrisException e) {
|
||||
reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void postShutdown(Runnable r) {
|
||||
postShutdown.add(r);
|
||||
}
|
||||
|
||||
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();
|
||||
super.onEnable();
|
||||
Bukkit.getPluginManager().registerEvents(this, this);
|
||||
}
|
||||
|
||||
public void onDisable() {
|
||||
if (services == null || IrisSafeguard.isForceShutdown()) return;
|
||||
services.values().forEach(IrisService::onDisable);
|
||||
Bukkit.getScheduler().cancelTasks(this);
|
||||
HandlerList.unregisterAll((Plugin) this);
|
||||
postShutdown.forEach(Runnable::run);
|
||||
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() {
|
||||
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
|
||||
new IrisPapiExpansion().register();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag(String subTag) {
|
||||
return IrisSafeguard.mode().tag(subTag);
|
||||
}
|
||||
|
||||
private void checkConfigHotload() {
|
||||
if (configWatcher.checkModified()) {
|
||||
IrisSettings.invalidate();
|
||||
IrisSettings.get();
|
||||
configWatcher.checkModified();
|
||||
Iris.info("Hotloaded settings.json ");
|
||||
}
|
||||
}
|
||||
|
||||
private void tickQueue() {
|
||||
synchronized (Iris.syncJobs) {
|
||||
if (!Iris.syncJobs.hasNext()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long ms = M.ms();
|
||||
|
||||
while (Iris.syncJobs.hasNext() && M.ms() - ms < 25) {
|
||||
try {
|
||||
Iris.syncJobs.next().run();
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void bstats() {
|
||||
if (IrisSettings.get().getGeneral().isPluginMetrics()) {
|
||||
Bindings.setupBstats(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
return super.onCommand(sender, command, label, args);
|
||||
}
|
||||
|
||||
public void imsg(CommandSender s, String msg) {
|
||||
s.sendMessage(C.IRIS + "[" + C.DARK_GRAY + "Iris" + C.IRIS + "]" + C.GRAY + ": " + msg);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) {
|
||||
Iris.debug("Biome Provider Called for " + worldName + " using ID: " + id);
|
||||
return super.getDefaultBiomeProvider(worldName, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
|
||||
Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id);
|
||||
if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType();
|
||||
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
|
||||
IrisDimension dim = loadDimension(worldName, id);
|
||||
if (dim == null) {
|
||||
throw new RuntimeException("Can't find dimension " + id + "!");
|
||||
}
|
||||
|
||||
Iris.debug("Assuming IrisDimension: " + dim.getName());
|
||||
|
||||
IrisWorld w = IrisWorld.builder()
|
||||
.name(worldName)
|
||||
.seed(1337)
|
||||
.environment(dim.getEnvironment())
|
||||
.worldFolder(WorldStorage.get().worldFolder(worldName))
|
||||
.minHeight(dim.getMinHeight())
|
||||
.maxHeight(dim.getMaxHeight())
|
||||
.build();
|
||||
|
||||
Iris.debug("Generator Config: " + w.toString());
|
||||
|
||||
File ff = new File(w.worldFolder(), "iris/pack");
|
||||
var files = ff.listFiles();
|
||||
if (files == null || files.length == 0)
|
||||
IO.delete(ff);
|
||||
|
||||
if (!ff.exists()) {
|
||||
ff.mkdirs();
|
||||
service(StudioSVC.class).installIntoWorld(getSender(), dim.getLoadKey(), w.worldFolder());
|
||||
}
|
||||
|
||||
return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) {
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
return dimension;
|
||||
}
|
||||
|
||||
public void splash() {
|
||||
Iris.info("Server type & version: " + Bukkit.getName() + " v" + Bukkit.getVersion());
|
||||
Iris.info("Custom Biomes: " + INMS.get().countCustomBiomes());
|
||||
printPacks();
|
||||
|
||||
IrisSafeguard.mode().trySplash();
|
||||
}
|
||||
|
||||
private void printPacks() {
|
||||
File packFolder = Iris.service(StudioSVC.class).getWorkspaceFolder();
|
||||
File[] packs = packFolder.listFiles(File::isDirectory);
|
||||
if (packs == null || packs.length == 0)
|
||||
return;
|
||||
Iris.info("Custom Dimensions: " + packs.length);
|
||||
for (File f : packs)
|
||||
printPack(f);
|
||||
}
|
||||
|
||||
private void printPack(File pack) {
|
||||
String dimName = pack.getName();
|
||||
String version = "???";
|
||||
try (FileReader r = new FileReader(new File(pack, "dimensions/" + dimName + ".json"))) {
|
||||
JsonObject json = JsonParser.parseReader(r).getAsJsonObject();
|
||||
if (json.has("version"))
|
||||
version = json.get("version").getAsString();
|
||||
} catch (IOException | JsonParseException ignored) {
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* 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.google.gson.Gson;
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.util.io.IO;
|
||||
import com.volmit.iris.util.json.JSONException;
|
||||
import com.volmit.iris.util.json.JSONObject;
|
||||
import com.volmit.iris.util.misc.getHardware;
|
||||
import com.volmit.iris.util.plugin.VolmitSender;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
@SuppressWarnings("SynchronizeOnNonFinalField")
|
||||
@Data
|
||||
public class IrisSettings {
|
||||
public static IrisSettings settings;
|
||||
private IrisSettingsGeneral general = new IrisSettingsGeneral();
|
||||
private IrisSettingsWorld world = new IrisSettingsWorld();
|
||||
private IrisSettingsGUI gui = new IrisSettingsGUI();
|
||||
private IrisSettingsAutoconfiguration autoConfiguration = new IrisSettingsAutoconfiguration();
|
||||
private IrisSettingsGenerator generator = new IrisSettingsGenerator();
|
||||
private IrisSettingsConcurrency concurrency = new IrisSettingsConcurrency();
|
||||
private IrisSettingsStudio studio = new IrisSettingsStudio();
|
||||
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) {
|
||||
return Math.max(switch (c) {
|
||||
case -1, -2, -4 -> Runtime.getRuntime().availableProcessors() / -c;
|
||||
default -> Math.max(c, 2);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
public static IrisSettings get() {
|
||||
if (settings != null) {
|
||||
return settings;
|
||||
}
|
||||
|
||||
settings = new IrisSettings();
|
||||
|
||||
File s = Iris.instance.getDataFile("settings.json");
|
||||
|
||||
if (!s.exists()) {
|
||||
try {
|
||||
IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4));
|
||||
} catch (JSONException | IOException e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
String ss = IO.readAll(s);
|
||||
settings = new Gson().fromJson(ss, IrisSettings.class);
|
||||
try {
|
||||
IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} catch (Throwable ee) {
|
||||
// Iris.reportError(ee); causes a self-reference & stackoverflow
|
||||
Iris.error("Configuration Error in settings.json! " + ee.getClass().getSimpleName() + ": " + ee.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
public static void invalidate() {
|
||||
synchronized (settings) {
|
||||
settings = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void forceSave() {
|
||||
File s = Iris.instance.getDataFile("settings.json");
|
||||
|
||||
try {
|
||||
IO.writeAll(s, new JSONObject(new Gson().toJson(settings)).toString(4));
|
||||
} catch (JSONException | IOException e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisSettingsAutoconfiguration {
|
||||
public boolean configureSpigotTimeoutTime = true;
|
||||
public boolean configurePaperWatchdogDelay = true;
|
||||
public boolean autoRestartOnCustomBiomeInstall = true;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisAsyncTeleport {
|
||||
public boolean enabled = false;
|
||||
public int loadViewDistance = 2;
|
||||
public boolean urgent = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisSettingsWorld {
|
||||
public IrisAsyncTeleport asyncTeleport = new IrisAsyncTeleport();
|
||||
public boolean postLoadBlockUpdates = true;
|
||||
public boolean forcePersistEntities = true;
|
||||
public boolean anbientEntitySpawningSystem = true;
|
||||
public long asyncTickIntervalMS = 700;
|
||||
public double targetSpawnEntitiesPerChunk = 0.95;
|
||||
public boolean markerEntitySpawningSystem = true;
|
||||
public boolean effectSystem = true;
|
||||
public boolean worldEditWandCUI = true;
|
||||
public boolean globalPregenCache = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisSettingsConcurrency {
|
||||
public int parallelism = -1;
|
||||
public int ioParallelism = -2;
|
||||
public int worldGenParallelism = -1;
|
||||
|
||||
public int getWorldGenThreads() {
|
||||
return getThreadCount(worldGenParallelism);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisSettingsPregen {
|
||||
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
|
||||
public static class IrisSettingsPerformance {
|
||||
private IrisSettingsEngineSVC engineSVC = new IrisSettingsEngineSVC();
|
||||
public boolean trimMantleInStudio = false;
|
||||
public int mantleKeepAlive = 30;
|
||||
public int noiseCacheSize = 1_024;
|
||||
public int resourceLoaderCacheSize = 1_024;
|
||||
public int objectLoaderCacheSize = 4_096;
|
||||
public int scriptLoaderCacheSize = 512;
|
||||
public int tectonicPlateSize = -1;
|
||||
public int mantleCleanupDelay = 200;
|
||||
|
||||
public int getTectonicPlateSize() {
|
||||
if (tectonicPlateSize > 0)
|
||||
return tectonicPlateSize;
|
||||
|
||||
return (int) (getHardware.getProcessMemory() / 512L);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class 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
|
||||
public static class IrisSettingsGeneral {
|
||||
public boolean DoomsdayAnnihilationSelfDestructMode = false;
|
||||
public boolean commandSounds = true;
|
||||
public boolean debug = false;
|
||||
public boolean dumpMantleOnError = false;
|
||||
public boolean disableNMS = false;
|
||||
public boolean pluginMetrics = true;
|
||||
public boolean splashLogoStartup = true;
|
||||
public boolean useConsoleCustomColors = true;
|
||||
public boolean useCustomColorsIngame = true;
|
||||
public boolean adjustVanillaHeight = false;
|
||||
public String forceMainWorld = "";
|
||||
public int spinh = -20;
|
||||
public int spins = 7;
|
||||
public int spinb = 8;
|
||||
public String cartographerMessage = "Iris does not allow cartographers in its world due to crashes.";
|
||||
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean canUseCustomColors(VolmitSender volmitSender) {
|
||||
return volmitSender.isPlayer() ? useCustomColorsIngame : useConsoleCustomColors;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisSettingsSentry {
|
||||
public boolean includeServerId = true;
|
||||
public boolean disableAutoReporting = false;
|
||||
public boolean debug = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisSettingsGUI {
|
||||
public boolean useServerLaunchedGuis = true;
|
||||
public boolean maximumPregenGuiFPS = false;
|
||||
public boolean colorMode = true;
|
||||
public String theme = "auto";
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisSettingsGenerator {
|
||||
public String defaultWorldType = "overworld";
|
||||
public int maxBiomeChildDepth = 4;
|
||||
public boolean preventLeafDecay = true;
|
||||
public boolean useMulticore = false;
|
||||
public boolean useMulticoreMantle = false;
|
||||
public boolean offsetNoiseTypes = false;
|
||||
public boolean earlyCustomBlocks = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisSettingsStudio {
|
||||
public boolean studio = true;
|
||||
public boolean openVSCode = true;
|
||||
public boolean disableTimeAndWeather = true;
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.commands;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.service.StudioSVC;
|
||||
import com.volmit.iris.engine.object.*;
|
||||
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 java.awt.*;
|
||||
|
||||
|
||||
@Decree(name = "edit", origin = DecreeOrigin.PLAYER, studio = true, description = "Edit something")
|
||||
public class CommandEdit implements DecreeExecutor {
|
||||
|
||||
private boolean noStudio() {
|
||||
if (!sender().isPlayer()) {
|
||||
sender().sendMessage(C.RED + "Players only!");
|
||||
return true;
|
||||
}
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
sender().sendMessage(C.RED + "No studio world is open!");
|
||||
return true;
|
||||
}
|
||||
if (!engine().isStudio()) {
|
||||
sender().sendMessage(C.RED + "You must be in a studio world!");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (GraphicsEnvironment.isHeadless()) {
|
||||
sender().sendMessage(C.RED + "Cannot open files in headless environments!");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Desktop.isDesktopSupported()) {
|
||||
sender().sendMessage(C.RED + "Desktop is not supported by this environment!");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Decree(description = "Edit the biome you specified", aliases = {"b"}, origin = DecreeOrigin.PLAYER)
|
||||
public void biome(@Param(contextual = false, description = "The biome to edit") IrisBiome biome) {
|
||||
if (noStudio()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (biome == null || biome.getLoadFile() == null) {
|
||||
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
|
||||
return;
|
||||
}
|
||||
Desktop.getDesktop().open(biome.getLoadFile());
|
||||
sender().sendMessage(C.GREEN + "Opening " + biome.getTypeName() + " " + biome.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Edit the region you specified", aliases = {"r"}, origin = DecreeOrigin.PLAYER)
|
||||
public void region(@Param(contextual = false, description = "The region to edit") IrisRegion region) {
|
||||
if (noStudio()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (region == null || region.getLoadFile() == null) {
|
||||
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
|
||||
return;
|
||||
}
|
||||
Desktop.getDesktop().open(region.getLoadFile());
|
||||
sender().sendMessage(C.GREEN + "Opening " + region.getTypeName() + " " + region.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Edit the dimension you specified", aliases = {"d"}, origin = DecreeOrigin.PLAYER)
|
||||
public void dimension(@Param(contextual = false, description = "The dimension to edit") IrisDimension dimension) {
|
||||
if (noStudio()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (dimension == null || dimension.getLoadFile() == null) {
|
||||
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
|
||||
return;
|
||||
}
|
||||
Desktop.getDesktop().open(dimension.getLoadFile());
|
||||
sender().sendMessage(C.GREEN + "Opening " + dimension.getTypeName() + " " + dimension.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Edit the cave file you specified", aliases = {"c"}, origin = DecreeOrigin.PLAYER)
|
||||
public void cave(@Param(contextual = false, description = "The cave to edit") IrisCave cave) {
|
||||
if (noStudio()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (cave == null || cave.getLoadFile() == null) {
|
||||
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
|
||||
return;
|
||||
}
|
||||
Desktop.getDesktop().open(cave.getLoadFile());
|
||||
sender().sendMessage(C.GREEN + "Opening " + cave.getTypeName() + " " + cave.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Edit the structure file you specified", aliases = {"jigsawstructure", "structure"}, origin = DecreeOrigin.PLAYER)
|
||||
public void jigsaw(@Param(contextual = false, description = "The jigsaw structure to edit") IrisJigsawStructure jigsaw) {
|
||||
if (noStudio()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (jigsaw == null || jigsaw.getLoadFile() == null) {
|
||||
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
|
||||
return;
|
||||
}
|
||||
Desktop.getDesktop().open(jigsaw.getLoadFile());
|
||||
sender().sendMessage(C.GREEN + "Opening " + jigsaw.getTypeName() + " " + jigsaw.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Edit the pool file you specified", aliases = {"jigsawpool", "pool"}, origin = DecreeOrigin.PLAYER)
|
||||
public void jigsawPool(@Param(contextual = false, description = "The jigsaw pool to edit") IrisJigsawPool pool) {
|
||||
if (noStudio()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (pool == null || pool.getLoadFile() == null) {
|
||||
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
|
||||
return;
|
||||
}
|
||||
Desktop.getDesktop().open(pool.getLoadFile());
|
||||
sender().sendMessage(C.GREEN + "Opening " + pool.getTypeName() + " " + pool.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Edit the jigsaw piece file you specified", aliases = {"jigsawpiece", "piece"}, origin = DecreeOrigin.PLAYER)
|
||||
public void jigsawPiece(@Param(contextual = false, description = "The jigsaw piece to edit") IrisJigsawPiece piece) {
|
||||
if (noStudio()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (piece == null || piece.getLoadFile() == null) {
|
||||
sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?");
|
||||
return;
|
||||
}
|
||||
Desktop.getDesktop().open(piece.getLoadFile());
|
||||
sender().sendMessage(C.GREEN + "Opening " + piece.getTypeName() + " " + piece.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! ");
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.commands;
|
||||
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.object.IrisBiome;
|
||||
import com.volmit.iris.engine.object.IrisJigsawStructure;
|
||||
import com.volmit.iris.engine.object.IrisRegion;
|
||||
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.ObjectHandler;
|
||||
import com.volmit.iris.util.format.C;
|
||||
|
||||
@Decree(name = "find", origin = DecreeOrigin.PLAYER, description = "Iris Find commands", aliases = "goto")
|
||||
public class CommandFind implements DecreeExecutor {
|
||||
@Decree(description = "Find a biome")
|
||||
public void biome(
|
||||
@Param(description = "The biome to look for")
|
||||
IrisBiome biome,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
|
||||
if (e == null) {
|
||||
sender().sendMessage(C.GOLD + "Not in an Iris World!");
|
||||
return;
|
||||
}
|
||||
|
||||
e.gotoBiome(biome, player(), teleport);
|
||||
}
|
||||
|
||||
@Decree(description = "Find a region")
|
||||
public void region(
|
||||
@Param(description = "The region to look for")
|
||||
IrisRegion region,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
|
||||
if (e == null) {
|
||||
sender().sendMessage(C.GOLD + "Not in an Iris World!");
|
||||
return;
|
||||
}
|
||||
|
||||
e.gotoRegion(region, player(), teleport);
|
||||
}
|
||||
|
||||
@Decree(description = "Find a structure")
|
||||
public void structure(
|
||||
@Param(description = "The structure to look for")
|
||||
IrisJigsawStructure structure,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
|
||||
if (e == null) {
|
||||
sender().sendMessage(C.GOLD + "Not in an Iris World!");
|
||||
return;
|
||||
}
|
||||
|
||||
e.gotoJigsaw(structure, player(), teleport);
|
||||
}
|
||||
|
||||
@Decree(description = "Find a point of interest.")
|
||||
public void poi(
|
||||
@Param(description = "The type of PoI to look for.")
|
||||
String type,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
if (e == null) {
|
||||
sender().sendMessage(C.GOLD + "Not in an Iris World!");
|
||||
return;
|
||||
}
|
||||
|
||||
e.gotoPOI(type, player(), teleport);
|
||||
}
|
||||
|
||||
@Decree(description = "Find an object")
|
||||
public void object(
|
||||
@Param(description = "The object to look for", customHandler = ObjectHandler.class)
|
||||
String object,
|
||||
@Param(description = "Should you be teleported", defaultValue = "true")
|
||||
boolean teleport
|
||||
) {
|
||||
Engine e = engine();
|
||||
|
||||
if (e == null) {
|
||||
sender().sendMessage(C.GOLD + "Not in an Iris World!");
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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.edit.JigsawEditor;
|
||||
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.object.IrisJigsawPiece;
|
||||
import com.volmit.iris.engine.object.IrisJigsawStructure;
|
||||
import com.volmit.iris.engine.object.IrisObject;
|
||||
import com.volmit.iris.engine.object.IrisPosition;
|
||||
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.ObjectHandler;
|
||||
import com.volmit.iris.util.format.C;
|
||||
import com.volmit.iris.util.format.Form;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
import com.volmit.iris.util.plugin.VolmitSender;
|
||||
import com.volmit.iris.util.scheduling.PrecisionStopwatch;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@Decree(name = "jigsaw", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris jigsaw commands")
|
||||
public class CommandJigsaw implements DecreeExecutor {
|
||||
@Decree(description = "Edit a jigsaw piece")
|
||||
public void edit(
|
||||
@Param(description = "The jigsaw piece to edit")
|
||||
IrisJigsawPiece piece
|
||||
) {
|
||||
File dest = piece.getLoadFile();
|
||||
new JigsawEditor(player(), piece, IrisData.loadAnyObject(piece.getObject(), data()), dest);
|
||||
}
|
||||
|
||||
@Decree(description = "Place a jigsaw structure")
|
||||
public void place(
|
||||
@Param(description = "The jigsaw structure to place")
|
||||
IrisJigsawStructure structure
|
||||
) {
|
||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||
try {
|
||||
var world = 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")
|
||||
public void create(
|
||||
@Param(description = "The name of the jigsaw piece")
|
||||
String piece,
|
||||
@Param(description = "The project to add the jigsaw piece to")
|
||||
String project,
|
||||
@Param(description = "The object to use for this piece", customHandler = ObjectHandler.class)
|
||||
String object
|
||||
) {
|
||||
IrisObject o = IrisData.loadAnyObject(object, data());
|
||||
|
||||
if (object == null) {
|
||||
sender().sendMessage(C.RED + "Failed to find existing object");
|
||||
return;
|
||||
}
|
||||
|
||||
File dest = Iris.instance.getDataFile("packs", project, "jigsaw-pieces", piece + ".json");
|
||||
new JigsawEditor(player(), null, o, dest);
|
||||
sender().sendMessage(C.GRAY + "* Right Click blocks to make them connectors");
|
||||
sender().sendMessage(C.GRAY + "* Right Click connectors to orient them");
|
||||
sender().sendMessage(C.GRAY + "* Shift + Right Click connectors to remove them");
|
||||
sender().sendMessage(C.GREEN + "Remember to use /iris jigsaw save");
|
||||
}
|
||||
|
||||
@Decree(description = "Exit the current jigsaw editor")
|
||||
public void exit() {
|
||||
JigsawEditor editor = JigsawEditor.editors.get(player());
|
||||
|
||||
if (editor == null) {
|
||||
sender().sendMessage(C.GOLD + "You don't have any pieces open to exit!");
|
||||
return;
|
||||
}
|
||||
|
||||
editor.exit();
|
||||
sender().sendMessage(C.GREEN + "Exited Jigsaw Editor");
|
||||
}
|
||||
|
||||
@Decree(description = "Save & Exit the current jigsaw editor")
|
||||
public void save() {
|
||||
JigsawEditor editor = JigsawEditor.editors.get(player());
|
||||
|
||||
if (editor == null) {
|
||||
sender().sendMessage(C.GOLD + "You don't have any pieces open to save!");
|
||||
return;
|
||||
}
|
||||
|
||||
editor.close();
|
||||
sender().sendMessage(C.GREEN + "Saved & Exited Jigsaw Editor");
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
/*
|
||||
* 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.link.WorldEditLink;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.core.service.ObjectSVC;
|
||||
import com.volmit.iris.core.service.StudioSVC;
|
||||
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.object.*;
|
||||
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.DecreeOrigin;
|
||||
import com.volmit.iris.util.decree.annotations.Decree;
|
||||
import com.volmit.iris.util.decree.annotations.Param;
|
||||
import com.volmit.iris.util.decree.specialhandlers.ObjectHandler;
|
||||
import com.volmit.iris.util.format.C;
|
||||
import com.volmit.iris.util.math.Direction;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.*;
|
||||
|
||||
@Decree(name = "object", aliases = "o", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris object manipulation")
|
||||
public class CommandObject implements DecreeExecutor {
|
||||
|
||||
private static final Set<Material> skipBlocks = Set.of(Materials.GRASS, Material.SNOW, Material.VINE, Material.TORCH, Material.DEAD_BUSH,
|
||||
Material.POPPY, Material.DANDELION);
|
||||
|
||||
public static IObjectPlacer createPlacer(World world, Map<Block, BlockData> futureBlockChanges) {
|
||||
|
||||
return new IObjectPlacer() {
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data) {
|
||||
return world.getHighestBlockYAt(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||
return world.getHighestBlockYAt(x, z, ignoreFluid ? HeightMap.OCEAN_FLOOR : HeightMap.MOTION_BLOCKING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, BlockData d) {
|
||||
Block block = world.getBlockAt(x, y, z);
|
||||
|
||||
//Prevent blocks being set in or bellow bedrock
|
||||
if (y <= world.getMinHeight() || block.getType() == Material.BEDROCK) return;
|
||||
|
||||
futureBlockChanges.put(block, block.getBlockData());
|
||||
|
||||
if (d instanceof IrisCustomData data) {
|
||||
block.setBlockData(data.getBase(), false);
|
||||
Iris.warn("Tried to place custom block at " + x + ", " + y + ", " + z + " which is not supported!");
|
||||
} else block.setBlockData(d, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockData get(int x, int y, int z) {
|
||||
return world.getBlockAt(x, y, z).getBlockData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPreventingDecay() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolid(int x, int y, int z) {
|
||||
return world.getBlockAt(x, y, z).getType().isSolid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUnderwater(int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFluidHeight() {
|
||||
return 63;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDebugSmartBore() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTile(int xx, int yy, int zz, TileData tile) {
|
||||
tile.toBukkitTry(world.getBlockAt(xx, yy, zz));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void setData(int xx, int yy, int zz, T data) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Engine getEngine() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Decree(description = "Check the composition of an object")
|
||||
public void analyze(
|
||||
@Param(description = "The object to analyze", customHandler = ObjectHandler.class)
|
||||
String object
|
||||
) {
|
||||
IrisObject o = IrisData.loadAnyObject(object, data());
|
||||
sender().sendMessage("Object Size: " + o.getW() + " * " + o.getH() + " * " + o.getD() + "");
|
||||
sender().sendMessage("Blocks Used: " + NumberFormat.getIntegerInstance().format(o.getBlocks().size()));
|
||||
|
||||
var queue = o.getBlocks().values();
|
||||
Map<Material, Set<BlockData>> unsorted = new HashMap<>();
|
||||
Map<BlockData, Integer> amounts = new HashMap<>();
|
||||
Map<Material, Integer> materials = new HashMap<>();
|
||||
while (queue.hasNext()) {
|
||||
BlockData block = queue.next();
|
||||
|
||||
//unsorted.put(block.getMaterial(), block);
|
||||
|
||||
if (!amounts.containsKey(block)) {
|
||||
amounts.put(block, 1);
|
||||
|
||||
|
||||
} else
|
||||
amounts.put(block, amounts.get(block) + 1);
|
||||
|
||||
if (!materials.containsKey(block.getMaterial())) {
|
||||
materials.put(block.getMaterial(), 1);
|
||||
unsorted.put(block.getMaterial(), new HashSet<>());
|
||||
unsorted.get(block.getMaterial()).add(block);
|
||||
} else {
|
||||
materials.put(block.getMaterial(), materials.get(block.getMaterial()) + 1);
|
||||
unsorted.get(block.getMaterial()).add(block);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
List<Material> sortedMatsList = amounts.keySet().stream().map(BlockData::getMaterial)
|
||||
.sorted().toList();
|
||||
Set<Material> sortedMats = new TreeSet<>(Comparator.comparingInt(materials::get).reversed());
|
||||
sortedMats.addAll(sortedMatsList);
|
||||
sender().sendMessage("== Blocks in object ==");
|
||||
|
||||
int n = 0;
|
||||
for (Material mat : sortedMats) {
|
||||
int amount = materials.get(mat);
|
||||
List<BlockData> set = new ArrayList<>(unsorted.get(mat));
|
||||
set.sort(Comparator.comparingInt(amounts::get).reversed());
|
||||
BlockData data = set.get(0);
|
||||
int dataAmount = amounts.get(data);
|
||||
|
||||
String string = " - " + mat.toString() + "*" + amount;
|
||||
if (data.getAsString(true).contains("[")) {
|
||||
string = string + " --> [" + data.getAsString(true).split("\\[")[1]
|
||||
.replaceAll("true", ChatColor.GREEN + "true" + ChatColor.GRAY)
|
||||
.replaceAll("false", ChatColor.RED + "false" + ChatColor.GRAY) + "*" + dataAmount;
|
||||
}
|
||||
|
||||
sender().sendMessage(string);
|
||||
|
||||
n++;
|
||||
|
||||
if (n >= 10) {
|
||||
sender().sendMessage(" + " + (sortedMats.size() - n) + " other block types");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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")
|
||||
public void dust() {
|
||||
player().getInventory().addItem(WandSVC.createDust());
|
||||
sender().playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f);
|
||||
}
|
||||
|
||||
@Decree(description = "Contract a selection based on your looking direction", aliases = "-")
|
||||
public void contract(
|
||||
@Param(description = "The amount to inset by", defaultValue = "1")
|
||||
int amount
|
||||
) {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage("Hold your wand.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Location[] b = WandSVC.getCuboid(player());
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
sender().sendMessage("No area selected.");
|
||||
return;
|
||||
}
|
||||
Location a1 = b[0].clone();
|
||||
Location a2 = b[1].clone();
|
||||
Cuboid cursor = new Cuboid(a1, a2);
|
||||
Direction d = Direction.closest(player().getLocation().getDirection()).reverse();
|
||||
assert d != null;
|
||||
cursor = cursor.expand(d, -amount);
|
||||
b[0] = cursor.getLowerNE();
|
||||
b[1] = cursor.getUpperSW();
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
|
||||
player().updateInventory();
|
||||
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
}
|
||||
|
||||
@Decree(description = "Set point 1 to look", aliases = "p1")
|
||||
public void position1(
|
||||
@Param(description = "Whether to use your current position, or where you look", defaultValue = "true")
|
||||
boolean here
|
||||
) {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage("Ready your Wand.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (WandSVC.isHoldingWand(player())) {
|
||||
Location[] g = WandSVC.getCuboid(player());
|
||||
|
||||
if (g == null) {
|
||||
return;
|
||||
}
|
||||
if (!here) {
|
||||
// TODO: WARNING HEIGHT
|
||||
g[1] = player().getTargetBlock(null, 256).getLocation().clone();
|
||||
} else {
|
||||
g[1] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0);
|
||||
}
|
||||
player().setItemInHand(WandSVC.createWand(g[0], g[1]));
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Set point 2 to look", aliases = "p2")
|
||||
public void position2(
|
||||
@Param(description = "Whether to use your current position, or where you look", defaultValue = "true")
|
||||
boolean here
|
||||
) {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage("Ready your Wand.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (WandSVC.isHoldingIrisWand(player())) {
|
||||
Location[] g = WandSVC.getCuboid(player());
|
||||
|
||||
if (g == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!here) {
|
||||
// TODO: WARNING HEIGHT
|
||||
g[0] = player().getTargetBlock(null, 256).getLocation().clone();
|
||||
} else {
|
||||
g[0] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0);
|
||||
}
|
||||
player().setItemInHand(WandSVC.createWand(g[0], g[1]));
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Paste an object", sync = true)
|
||||
public void paste(
|
||||
@Param(description = "The object to paste", customHandler = ObjectHandler.class)
|
||||
String object,
|
||||
@Param(description = "Whether or not to edit the object (need to hold wand)", defaultValue = "false")
|
||||
boolean edit,
|
||||
@Param(description = "The amount of degrees to rotate by", defaultValue = "0")
|
||||
int rotate,
|
||||
@Param(description = "The factor by which to scale the object placement", defaultValue = "1")
|
||||
double scale
|
||||
// ,
|
||||
// @Param(description = "The scale interpolator to use", defaultValue = "none")
|
||||
// IrisObjectPlacementScaleInterpolator interpolator
|
||||
) {
|
||||
IrisObject o = IrisData.loadAnyObject(object, data());
|
||||
double maxScale = Double.max(10 - o.getBlocks().size() / 10000d, 1);
|
||||
if (scale > maxScale) {
|
||||
sender().sendMessage(C.YELLOW + "Indicated scale exceeds maximum. Downscaled to maximum: " + maxScale);
|
||||
scale = maxScale;
|
||||
}
|
||||
|
||||
sender().playSound(Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1f, 1.5f);
|
||||
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
placement.setRotation(IrisObjectRotation.of(0, rotate, 0));
|
||||
|
||||
ItemStack wand = player().getInventory().getItemInMainHand();
|
||||
Location block = player().getTargetBlock(skipBlocks, 256).getLocation().clone().add(0, 1, 0);
|
||||
|
||||
Map<Block, BlockData> futureChanges = new HashMap<>();
|
||||
|
||||
if (scale != 1) {
|
||||
o = o.scaled(scale, IrisObjectPlacementScaleInterpolator.TRICUBIC);
|
||||
}
|
||||
|
||||
o.place(block.getBlockX(), block.getBlockY() + (int) o.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges), placement, new RNG(), null);
|
||||
|
||||
Iris.service(ObjectSVC.class).addChanges(futureChanges);
|
||||
|
||||
if (edit) {
|
||||
ItemStack newWand = WandSVC.createWand(block.clone().subtract(o.getCenter()).add(o.getW() - 1,
|
||||
o.getH() + o.getCenter().clone().getY() - 1, o.getD() - 1), block.clone().subtract(o.getCenter().clone().setY(0)));
|
||||
if (WandSVC.isWand(wand)) {
|
||||
wand = newWand;
|
||||
player().getInventory().setItemInMainHand(wand);
|
||||
sender().sendMessage("Updated wand for " + "objects/" + o.getLoadKey() + ".iob ");
|
||||
} else {
|
||||
int slot = WandSVC.findWand(player().getInventory());
|
||||
if (slot == -1) {
|
||||
player().getInventory().addItem(newWand);
|
||||
sender().sendMessage("Given new wand for " + "objects/" + o.getLoadKey() + ".iob ");
|
||||
} else {
|
||||
player().getInventory().setItem(slot, newWand);
|
||||
sender().sendMessage("Updated wand for " + "objects/" + o.getLoadKey() + ".iob ");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sender().sendMessage(C.IRIS + "Placed " + object);
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Save an object")
|
||||
public void save(
|
||||
@Param(description = "The dimension to store the object in", contextual = true)
|
||||
IrisDimension dimension,
|
||||
@Param(description = "The file to store it in, can use / for subfolders")
|
||||
String name,
|
||||
@Param(description = "Overwrite existing object files", defaultValue = "false", aliases = "force")
|
||||
boolean overwrite,
|
||||
@Param(description = "Use legacy TileState serialization if possible", defaultValue = "true")
|
||||
boolean legacy
|
||||
) {
|
||||
IrisObject o = WandSVC.createSchematic(player(), legacy);
|
||||
|
||||
if (o == null) {
|
||||
sender().sendMessage(C.YELLOW + "You need to hold your wand!");
|
||||
return;
|
||||
}
|
||||
|
||||
File file = Iris.service(StudioSVC.class).getWorkspaceFile(dimension.getLoadKey(), "objects", name + ".iob");
|
||||
|
||||
if (file.exists() && !overwrite) {
|
||||
sender().sendMessage(C.RED + "File already exists. Set overwrite=true to overwrite it.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
o.write(file, sender());
|
||||
} catch (IOException e) {
|
||||
sender().sendMessage(C.RED + "Failed to save object because of an IOException: " + e.getMessage());
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
sender().playSound(Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1f, 1.5f);
|
||||
sender().sendMessage(C.GREEN + "Successfully object to saved: " + dimension.getLoadKey() + "/objects/" + name);
|
||||
}
|
||||
|
||||
@Decree(description = "Shift a selection in your looking direction", aliases = "-")
|
||||
public void shift(
|
||||
@Param(description = "The amount to shift by", defaultValue = "1")
|
||||
int amount
|
||||
) {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage("Hold your wand.");
|
||||
return;
|
||||
}
|
||||
|
||||
Location[] b = WandSVC.getCuboid(player());
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
sender().sendMessage("No area selected.");
|
||||
return;
|
||||
}
|
||||
Location a1 = b[0].clone();
|
||||
Location a2 = b[1].clone();
|
||||
Direction d = Direction.closest(player().getLocation().getDirection()).reverse();
|
||||
if (d == null) {
|
||||
return; // HOW DID THIS HAPPEN
|
||||
}
|
||||
a1.add(d.toVector().multiply(amount));
|
||||
a2.add(d.toVector().multiply(amount));
|
||||
Cuboid cursor = new Cuboid(a1, a2);
|
||||
b[0] = cursor.getLowerNE();
|
||||
b[1] = cursor.getUpperSW();
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
|
||||
player().updateInventory();
|
||||
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
}
|
||||
|
||||
@Decree(description = "Undo a number of pastes", aliases = "-")
|
||||
public void undo(
|
||||
@Param(description = "The amount of pastes to undo", defaultValue = "1")
|
||||
int amount
|
||||
) {
|
||||
ObjectSVC service = Iris.service(ObjectSVC.class);
|
||||
int actualReverts = Math.min(service.getUndos().size(), amount);
|
||||
service.revertChanges(actualReverts);
|
||||
sender().sendMessage(C.BLUE + "Reverted " + actualReverts + C.BLUE +" pastes!");
|
||||
}
|
||||
|
||||
@Decree(description = "Gets an object wand and grabs the current WorldEdit selection.", aliases = "we", origin = DecreeOrigin.PLAYER, studio = true)
|
||||
public void we() {
|
||||
if (!Bukkit.getPluginManager().isPluginEnabled("WorldEdit")) {
|
||||
sender().sendMessage(C.RED + "You can't get a WorldEdit selection without WorldEdit, you know.");
|
||||
return;
|
||||
}
|
||||
|
||||
Cuboid locs = WorldEditLink.getSelection(sender().player());
|
||||
|
||||
if (locs == null) {
|
||||
sender().sendMessage(C.RED + "You don't have a WorldEdit selection in this world.");
|
||||
return;
|
||||
}
|
||||
|
||||
sender().player().getInventory().addItem(WandSVC.createWand(locs.getLowerNE(), locs.getUpperSW()));
|
||||
sender().sendMessage(C.GREEN + "A fresh wand with your current WorldEdit selection on it!");
|
||||
}
|
||||
|
||||
@Decree(description = "Get an object wand", sync = true)
|
||||
public void wand() {
|
||||
player().getInventory().addItem(WandSVC.createWand());
|
||||
sender().playSound(Sound.ITEM_ARMOR_EQUIP_NETHERITE, 1f, 1.5f);
|
||||
sender().sendMessage(C.GREEN + "Poof! Good luck building!");
|
||||
}
|
||||
|
||||
@Decree(name = "x&y", description = "Autoselect up, down & out", sync = true)
|
||||
public void xay() {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage(C.YELLOW + "Hold your wand!");
|
||||
return;
|
||||
}
|
||||
|
||||
Location[] b = WandSVC.getCuboid(player());
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
sender().sendMessage("No area selected.");
|
||||
return;
|
||||
}
|
||||
Location a1 = b[0].clone();
|
||||
Location a2 = b[1].clone();
|
||||
Location a1x = b[0].clone();
|
||||
Location a2x = b[1].clone();
|
||||
Cuboid cursor = new Cuboid(a1, a2);
|
||||
Cuboid cursorx = new Cuboid(a1, a2);
|
||||
|
||||
while (!cursor.containsOnly(Material.AIR)) {
|
||||
a1.add(new org.bukkit.util.Vector(0, 1, 0));
|
||||
a2.add(new org.bukkit.util.Vector(0, 1, 0));
|
||||
cursor = new Cuboid(a1, a2);
|
||||
}
|
||||
|
||||
a1.add(new org.bukkit.util.Vector(0, -1, 0));
|
||||
a2.add(new org.bukkit.util.Vector(0, -1, 0));
|
||||
|
||||
while (!cursorx.containsOnly(Material.AIR)) {
|
||||
a1x.add(new org.bukkit.util.Vector(0, -1, 0));
|
||||
a2x.add(new org.bukkit.util.Vector(0, -1, 0));
|
||||
cursorx = new Cuboid(a1x, a2x);
|
||||
}
|
||||
|
||||
a1x.add(new org.bukkit.util.Vector(0, 1, 0));
|
||||
a2x.add(new Vector(0, 1, 0));
|
||||
b[0] = a1;
|
||||
b[1] = a2x;
|
||||
cursor = new Cuboid(b[0], b[1]);
|
||||
cursor = cursor.contract(Cuboid.CuboidDirection.North);
|
||||
cursor = cursor.contract(Cuboid.CuboidDirection.South);
|
||||
cursor = cursor.contract(Cuboid.CuboidDirection.East);
|
||||
cursor = cursor.contract(Cuboid.CuboidDirection.West);
|
||||
b[0] = cursor.getLowerNE();
|
||||
b[1] = cursor.getUpperSW();
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
|
||||
player().updateInventory();
|
||||
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
sender().sendMessage(C.GREEN + "Auto-select complete!");
|
||||
}
|
||||
|
||||
@Decree(name = "x+y", description = "Autoselect up & out", sync = true)
|
||||
public void xpy() {
|
||||
if (!WandSVC.isHoldingWand(player())) {
|
||||
sender().sendMessage(C.YELLOW + "Hold your wand!");
|
||||
return;
|
||||
}
|
||||
|
||||
Location[] b = WandSVC.getCuboid(player());
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
sender().sendMessage("No area selected.");
|
||||
return;
|
||||
}
|
||||
b[0].add(new Vector(0, 1, 0));
|
||||
b[1].add(new Vector(0, 1, 0));
|
||||
Location a1 = b[0].clone();
|
||||
Location a2 = b[1].clone();
|
||||
Cuboid cursor = new Cuboid(a1, a2);
|
||||
|
||||
while (!cursor.containsOnly(Material.AIR)) {
|
||||
a1.add(new Vector(0, 1, 0));
|
||||
a2.add(new Vector(0, 1, 0));
|
||||
cursor = new Cuboid(a1, a2);
|
||||
}
|
||||
|
||||
a1.add(new Vector(0, -1, 0));
|
||||
a2.add(new Vector(0, -1, 0));
|
||||
b[0] = a1;
|
||||
a2 = b[1];
|
||||
cursor = new Cuboid(a1, a2);
|
||||
cursor = cursor.contract(Cuboid.CuboidDirection.North);
|
||||
cursor = cursor.contract(Cuboid.CuboidDirection.South);
|
||||
cursor = cursor.contract(Cuboid.CuboidDirection.East);
|
||||
cursor = cursor.contract(Cuboid.CuboidDirection.West);
|
||||
b[0] = cursor.getLowerNE();
|
||||
b[1] = cursor.getUpperSW();
|
||||
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
|
||||
player().updateInventory();
|
||||
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
sender().sendMessage(C.GREEN + "Auto-select complete!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.gui.PregeneratorJob;
|
||||
import com.volmit.iris.core.pregenerator.PregenTask;
|
||||
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||
import com.volmit.iris.util.decree.DecreeExecutor;
|
||||
import com.volmit.iris.util.decree.annotations.Decree;
|
||||
import com.volmit.iris.util.decree.annotations.Param;
|
||||
import com.volmit.iris.util.format.C;
|
||||
import com.volmit.iris.util.math.Position2;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
@Decree(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!")
|
||||
public class CommandPregen implements DecreeExecutor {
|
||||
@Decree(description = "Pregenerate a world")
|
||||
public void start(
|
||||
@Param(description = "The radius of the pregen in blocks", aliases = "size")
|
||||
int radius,
|
||||
@Param(description = "The world to pregen", contextual = true)
|
||||
World world,
|
||||
@Param(aliases = "middle", description = "The center location of the pregen. Use \"me\" for your current location", defaultValue = "0,0")
|
||||
Vector center,
|
||||
@Param(description = "Open the Iris pregen gui", defaultValue = "true")
|
||||
boolean gui
|
||||
) {
|
||||
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.");
|
||||
}
|
||||
radius = Math.max(radius, 1024);
|
||||
IrisToolbelt.pregenerate(PregenTask
|
||||
.builder()
|
||||
.center(new Position2(center.getBlockX(), center.getBlockZ()))
|
||||
.gui(gui)
|
||||
.radiusX(radius)
|
||||
.radiusZ(radius)
|
||||
.build(), world);
|
||||
String msg = C.GREEN + "Pregen started in " + C.GOLD + world.getName() + C.GREEN + " of " + C.GOLD + (radius * 2) + C.GREEN + " by " + C.GOLD + (radius * 2) + C.GREEN + " blocks from " + C.GOLD + center.getX() + "," + center.getZ();
|
||||
sender().sendMessage(msg);
|
||||
Iris.info(msg);
|
||||
} catch (Throwable e) {
|
||||
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() {
|
||||
if (PregeneratorJob.shutdownInstance()) {
|
||||
Iris.info( C.BLUE + "Finishing up mca region...");
|
||||
} else {
|
||||
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to stop");
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"})
|
||||
public void pause() {
|
||||
if (PregeneratorJob.pauseResume()) {
|
||||
sender().sendMessage(C.GREEN + "Paused/unpaused pregeneration task, now: " + (PregeneratorJob.isPaused() ? "Paused" : "Running") + ".");
|
||||
} else {
|
||||
sender().sendMessage(C.YELLOW + "No active pregeneration tasks to pause/unpause.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.util.decree.DecreeExecutor;
|
||||
|
||||
public class CommandSettings implements DecreeExecutor {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,984 @@
|
||||
/*
|
||||
* 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.GuiHost;
|
||||
import com.volmit.iris.core.gui.MapWindow;
|
||||
import com.volmit.iris.core.gui.NoiseExplorerGUI;
|
||||
import com.volmit.iris.core.gui.PaletteExplorerStart;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.core.project.IrisProject;
|
||||
import com.volmit.iris.core.service.ConversionSVC;
|
||||
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.object.*;
|
||||
import com.volmit.iris.engine.platform.PlatformChunkGenerator;
|
||||
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.decree.DecreeContext;
|
||||
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;
|
||||
import com.volmit.iris.util.function.NoiseProvider;
|
||||
import com.volmit.iris.util.interpolation.InterpolationMethod;
|
||||
import com.volmit.iris.util.io.IO;
|
||||
import com.volmit.iris.util.json.JSONArray;
|
||||
import com.volmit.iris.util.json.JSONObject;
|
||||
import com.volmit.iris.util.mantle.MantleChunk;
|
||||
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.Spiraler;
|
||||
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.PrecisionStopwatch;
|
||||
import com.volmit.iris.util.scheduling.jobs.ParallelRadiusJob;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.util.BlockVector;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
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)
|
||||
public class CommandStudio implements DecreeExecutor {
|
||||
private CommandFind find;
|
||||
private CommandEdit edit;
|
||||
//private CommandDeepSearch deepSearch;
|
||||
|
||||
public static String hrf(Duration duration) {
|
||||
return duration.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase();
|
||||
}
|
||||
|
||||
//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 = "master")
|
||||
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
|
||||
) {
|
||||
new CommandIris().download(pack, branch, overwrite);
|
||||
}
|
||||
|
||||
@Decree(description = "Open a new studio world", aliases = "o", sync = true)
|
||||
public void open(
|
||||
@Param(defaultValue = "default", description = "The dimension to open a studio for", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@Param(defaultValue = "1337", description = "The seed to generate the studio with", aliases = "s")
|
||||
long seed) {
|
||||
sender().sendMessage(C.GREEN + "Opening studio for the \"" + dimension.getName() + "\" pack (seed: " + seed + ")");
|
||||
Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey());
|
||||
}
|
||||
|
||||
@Decree(description = "Open VSCode for a dimension", aliases = {"vsc", "edit"})
|
||||
public void vscode(
|
||||
@Param(defaultValue = "default", description = "The dimension to open VSCode for", aliases = "dim")
|
||||
IrisDimension dimension
|
||||
) {
|
||||
sender().sendMessage(C.GREEN + "Opening VSCode for the \"" + dimension.getName() + "\" pack");
|
||||
Iris.service(StudioSVC.class).openVSCode(sender(), dimension.getLoadKey());
|
||||
}
|
||||
|
||||
@Decree(description = "Close an open studio project", aliases = {"x", "c"}, sync = true)
|
||||
public void close() {
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
sender().sendMessage(C.RED + "No open studio projects.");
|
||||
return;
|
||||
}
|
||||
|
||||
Iris.service(StudioSVC.class).close();
|
||||
sender().sendMessage(C.GREEN + "Project Closed.");
|
||||
}
|
||||
|
||||
@Decree(description = "Create a new studio project", aliases = "+", sync = true)
|
||||
public void create(
|
||||
@Param(description = "The name of this new Iris Project.")
|
||||
String name,
|
||||
@Param(description = "Copy the contents of an existing project in your packs folder and use it as a template in this new project.", contextual = true)
|
||||
IrisDimension template) {
|
||||
if (template != null) {
|
||||
Iris.service(StudioSVC.class).create(sender(), name, template.getLoadKey());
|
||||
} else {
|
||||
Iris.service(StudioSVC.class).create(sender(), name);
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Get the version of a pack")
|
||||
public void version(
|
||||
@Param(defaultValue = "default", description = "The dimension get the version of", aliases = "dim", contextual = true)
|
||||
IrisDimension dimension
|
||||
) {
|
||||
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")
|
||||
public void convert() {
|
||||
Iris.service(ConversionSVC.class).check(sender());
|
||||
//IrisConverter.convertSchematics(sender());
|
||||
}
|
||||
|
||||
@Decree(description = "Execute a script", aliases = "run", origin = DecreeOrigin.PLAYER)
|
||||
public void execute(
|
||||
@Param(description = "The script to run")
|
||||
IrisScript script
|
||||
) {
|
||||
engine().getExecution().execute(script.getLoadKey());
|
||||
}
|
||||
|
||||
@Decree(description = "Open the map on its noise explorer (External GUI)", aliases = {"nmap", "n"})
|
||||
public void noise() {
|
||||
if (noGUI()) return;
|
||||
Engine playerEngine = engine();
|
||||
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)
|
||||
public void charge() {
|
||||
if (!IrisToolbelt.isIrisWorld(world())) {
|
||||
sender().sendMessage(C.RED + "You must be in an Iris world to charge spawners!");
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "Charging spawners!");
|
||||
engine().getWorldManager().chargeEnergy();
|
||||
}
|
||||
|
||||
@Decree(description = "Preview noise gens (External GUI)", aliases = {"generator", "gen"})
|
||||
public void explore(
|
||||
@Param(description = "The generator to explore", contextual = true)
|
||||
IrisGenerator generator,
|
||||
@Param(description = "The seed to generate with", defaultValue = "12345")
|
||||
long seed
|
||||
) {
|
||||
if (noGUI()) return;
|
||||
sender().sendMessage(C.GREEN + "Opening Noise Explorer!");
|
||||
NoiseExplorerGUI.launchGeneratorKey(engine(), generator == null ? null : generator.getLoadKey(), generator, seed);
|
||||
}
|
||||
|
||||
@Decree(description = "Hotload a studio", aliases = {"reload", "h"})
|
||||
public void hotload() {
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
sender().sendMessage(C.RED + "No studio world open!");
|
||||
return;
|
||||
}
|
||||
Iris.service(StudioSVC.class).getActiveProject().getActiveProvider().getEngine().hotload();
|
||||
sender().sendMessage(C.GREEN + "Hotloaded");
|
||||
}
|
||||
|
||||
@Decree(description = "Show loot if a chest were right here", origin = DecreeOrigin.PLAYER, sync = true)
|
||||
public void loot(
|
||||
@Param(description = "Fast insertion of items in virtual inventory (may cause performance drop)", defaultValue = "false")
|
||||
boolean fast,
|
||||
@Param(description = "Whether or not to append to the inventory currently open (if false, clears opened inventory)", defaultValue = "true")
|
||||
boolean add
|
||||
) {
|
||||
if (noStudio()) return;
|
||||
|
||||
KList<IrisLootTable> tables = engine().getLootTables(RNG.r, player().getLocation().getBlock());
|
||||
Inventory inv = Bukkit.createInventory(null, 27 * 2);
|
||||
|
||||
try {
|
||||
engine().addItems(true, inv, RNG.r, tables, InventorySlotType.STORAGE, player().getWorld(), player().getLocation().getBlockX(), player().getLocation().getBlockY(), player().getLocation().getBlockZ(), 1);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage(C.RED + "Cannot add items to virtual inventory because of: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
O<Integer> ta = new O<>();
|
||||
ta.set(-1);
|
||||
|
||||
var sender = sender();
|
||||
var player = player();
|
||||
var engine = engine();
|
||||
|
||||
ta.set(Bukkit.getScheduler().scheduleSyncRepeatingTask(Iris.instance, () ->
|
||||
{
|
||||
if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) {
|
||||
Bukkit.getScheduler().cancelTask(ta.get());
|
||||
sender.sendMessage(C.GREEN + "Opened inventory!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!add) {
|
||||
inv.clear();
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
sender().sendMessage(C.GREEN + "Opening inventory now!");
|
||||
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")
|
||||
public void map(
|
||||
@Param(name = "world", description = "The world to open the generator for", contextual = true)
|
||||
World world
|
||||
) {
|
||||
if (noGUI()) return;
|
||||
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(C.RED + "You need to be in or specify an Iris-generated world!");
|
||||
return;
|
||||
}
|
||||
|
||||
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!");
|
||||
}
|
||||
|
||||
@Decree(description = "Package a dimension into a compressed format", aliases = "package")
|
||||
public void pkg(
|
||||
@Param(name = "dimension", description = "The dimension pack to compress", contextual = true, defaultValue = "default")
|
||||
IrisDimension dimension,
|
||||
@Param(name = "obfuscate", description = "Whether or not to obfuscate the pack", defaultValue = "false")
|
||||
boolean obfuscate,
|
||||
@Param(name = "minify", description = "Whether or not to minify the pack", defaultValue = "true")
|
||||
boolean minify
|
||||
) {
|
||||
Iris.service(StudioSVC.class).compilePackage(sender(), dimension.getLoadKey(), obfuscate, minify);
|
||||
}
|
||||
|
||||
@Decree(description = "Profiles the performance of a dimension", origin = DecreeOrigin.PLAYER)
|
||||
public void profile(
|
||||
@Param(description = "The dimension to profile", contextual = true, defaultValue = "default")
|
||||
IrisDimension dimension
|
||||
) {
|
||||
// Todo: Make this more accurate
|
||||
File pack = dimension.getLoadFile().getParentFile().getParentFile();
|
||||
File report = Iris.instance.getDataFile("profile.txt");
|
||||
IrisProject project = new IrisProject(pack);
|
||||
IrisData data = IrisData.get(pack);
|
||||
|
||||
KList<String> fileText = new KList<>();
|
||||
|
||||
KMap<NoiseStyle, Double> styleTimings = new KMap<>();
|
||||
KMap<InterpolationMethod, Double> interpolatorTimings = new KMap<>();
|
||||
KMap<String, Double> generatorTimings = new KMap<>();
|
||||
KMap<String, Double> biomeTimings = new KMap<>();
|
||||
KMap<String, Double> regionTimings = new KMap<>();
|
||||
|
||||
sender().sendMessage("Calculating Performance Metrics for Noise generators");
|
||||
|
||||
for (NoiseStyle i : NoiseStyle.values()) {
|
||||
CNG c = i.create(new RNG(i.hashCode()));
|
||||
|
||||
for (int j = 0; j < 3000; j++) {
|
||||
c.noise(j, j + 1000, j * j);
|
||||
c.noise(j, -j);
|
||||
}
|
||||
|
||||
PrecisionStopwatch px = PrecisionStopwatch.start();
|
||||
|
||||
for (int j = 0; j < 100000; j++) {
|
||||
c.noise(j, j + 1000, j * j);
|
||||
c.noise(j, -j);
|
||||
}
|
||||
|
||||
styleTimings.put(i, px.getMilliseconds());
|
||||
}
|
||||
|
||||
fileText.add("Noise Style Performance Impacts: ");
|
||||
|
||||
for (NoiseStyle i : styleTimings.sortKNumber()) {
|
||||
fileText.add(i.name() + ": " + styleTimings.get(i));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
sender().sendMessage("Calculating Interpolator Timings...");
|
||||
|
||||
for (InterpolationMethod i : InterpolationMethod.values()) {
|
||||
IrisInterpolator in = new IrisInterpolator();
|
||||
in.setFunction(i);
|
||||
in.setHorizontalScale(8);
|
||||
|
||||
NoiseProvider np = (x, z) -> Math.random();
|
||||
|
||||
for (int j = 0; j < 3000; j++) {
|
||||
in.interpolate(j, -j, np);
|
||||
}
|
||||
|
||||
PrecisionStopwatch px = PrecisionStopwatch.start();
|
||||
|
||||
for (int j = 0; j < 100000; j++) {
|
||||
in.interpolate(j + 10000, -j - 100000, np);
|
||||
}
|
||||
|
||||
interpolatorTimings.put(i, px.getMilliseconds());
|
||||
}
|
||||
|
||||
fileText.add("Noise Interpolator Performance Impacts: ");
|
||||
|
||||
for (InterpolationMethod i : interpolatorTimings.sortKNumber()) {
|
||||
fileText.add(i.name() + ": " + interpolatorTimings.get(i));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
sender().sendMessage("Processing Generator Scores: ");
|
||||
|
||||
KMap<String, KList<String>> btx = new KMap<>();
|
||||
|
||||
for (String i : data.getGeneratorLoader().getPossibleKeys()) {
|
||||
KList<String> vv = new KList<>();
|
||||
IrisGenerator g = data.getGeneratorLoader().load(i);
|
||||
KList<IrisNoiseGenerator> composites = g.getAllComposites();
|
||||
double score = 0;
|
||||
int m = 0;
|
||||
for (IrisNoiseGenerator j : composites) {
|
||||
m++;
|
||||
score += styleTimings.get(j.getStyle().getStyle());
|
||||
vv.add("Composite Noise Style " + m + " " + j.getStyle().getStyle().name() + ": " + styleTimings.get(j.getStyle().getStyle()));
|
||||
}
|
||||
|
||||
score += interpolatorTimings.get(g.getInterpolator().getFunction());
|
||||
vv.add("Interpolator " + g.getInterpolator().getFunction().name() + ": " + interpolatorTimings.get(g.getInterpolator().getFunction()));
|
||||
generatorTimings.put(i, score);
|
||||
btx.put(i, vv);
|
||||
}
|
||||
|
||||
fileText.add("Project Generator Performance Impacts: ");
|
||||
|
||||
for (String i : generatorTimings.sortKNumber()) {
|
||||
fileText.add(i + ": " + generatorTimings.get(i));
|
||||
|
||||
btx.get(i).forEach((ii) -> fileText.add(" " + ii));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
KMap<String, KList<String>> bt = new KMap<>();
|
||||
|
||||
for (String i : data.getBiomeLoader().getPossibleKeys()) {
|
||||
KList<String> vv = new KList<>();
|
||||
IrisBiome b = data.getBiomeLoader().load(i);
|
||||
double score = 0;
|
||||
|
||||
int m = 0;
|
||||
for (IrisBiomePaletteLayer j : b.getLayers()) {
|
||||
m++;
|
||||
score += styleTimings.get(j.getStyle().getStyle());
|
||||
vv.add("Palette Layer " + m + ": " + styleTimings.get(j.getStyle().getStyle()));
|
||||
}
|
||||
|
||||
score += styleTimings.get(b.getBiomeStyle().getStyle());
|
||||
vv.add("Biome Style: " + styleTimings.get(b.getBiomeStyle().getStyle()));
|
||||
score += styleTimings.get(b.getChildStyle().getStyle());
|
||||
vv.add("Child Style: " + styleTimings.get(b.getChildStyle().getStyle()));
|
||||
biomeTimings.put(i, score);
|
||||
bt.put(i, vv);
|
||||
}
|
||||
|
||||
fileText.add("Project Biome Performance Impacts: ");
|
||||
|
||||
for (String i : biomeTimings.sortKNumber()) {
|
||||
fileText.add(i + ": " + biomeTimings.get(i));
|
||||
|
||||
bt.get(i).forEach((ff) -> fileText.add(" " + ff));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
for (String i : data.getRegionLoader().getPossibleKeys()) {
|
||||
IrisRegion b = data.getRegionLoader().load(i);
|
||||
double score = 0;
|
||||
|
||||
score += styleTimings.get(b.getLakeStyle().getStyle());
|
||||
score += styleTimings.get(b.getRiverStyle().getStyle());
|
||||
regionTimings.put(i, score);
|
||||
}
|
||||
|
||||
fileText.add("Project Region Performance Impacts: ");
|
||||
|
||||
for (String i : regionTimings.sortKNumber()) {
|
||||
fileText.add(i + ": " + regionTimings.get(i));
|
||||
}
|
||||
|
||||
fileText.add("");
|
||||
|
||||
double m = 0;
|
||||
for (double i : biomeTimings.v()) {
|
||||
m += i;
|
||||
}
|
||||
m /= biomeTimings.size();
|
||||
double mm = 0;
|
||||
for (double i : generatorTimings.v()) {
|
||||
mm += i;
|
||||
}
|
||||
mm /= generatorTimings.size();
|
||||
m += mm;
|
||||
double mmm = 0;
|
||||
for (double i : regionTimings.v()) {
|
||||
mmm += i;
|
||||
}
|
||||
mmm /= regionTimings.size();
|
||||
m += mmm;
|
||||
|
||||
fileText.add("Average Score: " + m);
|
||||
sender().sendMessage("Score: " + Form.duration(m, 0));
|
||||
|
||||
try {
|
||||
IO.writeAll(report, fileText.toString("\n"));
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Done! " + report.getPath());
|
||||
}
|
||||
|
||||
@Decree(description = "Spawn an Iris entity", aliases = "summon", origin = DecreeOrigin.PLAYER)
|
||||
public void spawn(
|
||||
@Param(description = "The entity to spawn")
|
||||
IrisEntity entity,
|
||||
@Param(description = "The location to spawn the entity at", contextual = true)
|
||||
Vector location
|
||||
) {
|
||||
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
sender().sendMessage(C.RED + "You have to be in an Iris world to spawn entities properly. Trying to spawn the best we can do.");
|
||||
}
|
||||
entity.spawn(engine(), new Location(world(), location.getX(), location.getY(), location.getZ()));
|
||||
}
|
||||
|
||||
@Decree(description = "Teleport to the active studio world", aliases = "stp", origin = DecreeOrigin.PLAYER, sync = true)
|
||||
public void tpstudio() {
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
sender().sendMessage(C.RED + "No studio world is open!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IrisToolbelt.isIrisWorld(world()) && engine().isStudio()) {
|
||||
sender().sendMessage(C.RED + "You are already in a studio world!");
|
||||
return;
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Sending you to the studio world!");
|
||||
var player = player();
|
||||
player.teleportAsync(Iris.service(StudioSVC.class)
|
||||
.getActiveProject()
|
||||
.getActiveProvider()
|
||||
.getTarget()
|
||||
.getWorld()
|
||||
.spawnLocation()
|
||||
).thenRun(() -> player.setGameMode(GameMode.SPECTATOR));
|
||||
}
|
||||
|
||||
@Decree(description = "Update your dimension projects VSCode workspace")
|
||||
public void update(
|
||||
@Param(description = "The dimension to update the workspace of", contextual = true, defaultValue = "default")
|
||||
IrisDimension dimension
|
||||
) {
|
||||
sender().sendMessage(C.GOLD + "Updating Code Workspace for " + dimension.getName() + "...");
|
||||
if (new IrisProject(dimension.getLoader().getDataFolder()).updateWorkspace()) {
|
||||
sender().sendMessage(C.GREEN + "Updated Code Workspace for " + dimension.getName());
|
||||
} else {
|
||||
sender().sendMessage(C.RED + "Invalid project: " + dimension.getName() + ". Try deleting the code-workspace file and try again.");
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(aliases = "find-objects", description = "Get information about nearby structures")
|
||||
public void objects() {
|
||||
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
sender().sendMessage(C.RED + "You must be in an Iris world");
|
||||
return;
|
||||
}
|
||||
|
||||
World world = player().getWorld();
|
||||
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage("You must be in an iris world.");
|
||||
return;
|
||||
}
|
||||
KList<Chunk> chunks = new KList<>();
|
||||
int bx = player().getLocation().getChunk().getX();
|
||||
int bz = player().getLocation().getChunk().getZ();
|
||||
|
||||
try {
|
||||
Location l = player().getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation();
|
||||
|
||||
int cx = l.getChunk().getX();
|
||||
int cz = l.getChunk().getZ();
|
||||
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + cx, z + cz))).drain();
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + bx, z + bz))).drain();
|
||||
sender().sendMessage("Capturing IGenData from " + chunks.size() + " nearby chunks.");
|
||||
try {
|
||||
File ff = Iris.instance.getDataFile("reports/" + M.ms() + ".txt");
|
||||
PrintWriter pw = new PrintWriter(ff);
|
||||
pw.println("=== Iris Chunk Report ===");
|
||||
pw.println("== General Info ==");
|
||||
pw.println("Iris Version: " + Iris.instance.getDescription().getVersion());
|
||||
pw.println("Bukkit Version: " + Bukkit.getBukkitVersion());
|
||||
pw.println("MC Version: " + Bukkit.getVersion());
|
||||
pw.println("Report Captured At: " + new Date());
|
||||
pw.println("Chunks: (" + chunks.size() + "): ");
|
||||
|
||||
for (Chunk i : chunks) {
|
||||
pw.println("- [" + i.getX() + ", " + i.getZ() + "]");
|
||||
}
|
||||
|
||||
int regions = 0;
|
||||
long size = 0;
|
||||
String age = "No idea...";
|
||||
|
||||
try {
|
||||
for (File i : Objects.requireNonNull(new File(world.getWorldFolder(), "region").listFiles())) {
|
||||
if (i.isFile()) {
|
||||
size += i.length();
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
FileTime creationTime = (FileTime) Files.getAttribute(world.getWorldFolder().toPath(), "creationTime");
|
||||
age = hrf(Duration.of(M.ms() - creationTime.toMillis(), ChronoUnit.MILLIS));
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
KList<String> biomes = new KList<>();
|
||||
KList<String> caveBiomes = new KList<>();
|
||||
KMap<String, KMap<String, KList<String>>> objects = new KMap<>();
|
||||
|
||||
for (Chunk i : chunks) {
|
||||
for (int j = 0; j < 16; j += 3) {
|
||||
|
||||
for (int k = 0; k < 16; k += 3) {
|
||||
|
||||
assert engine() != null;
|
||||
IrisBiome bb = engine().getSurfaceBiome((i.getX() * 16) + j, (i.getZ() * 16) + k);
|
||||
IrisBiome bxf = engine().getCaveBiome((i.getX() * 16) + j, (i.getZ() * 16) + k);
|
||||
biomes.addIfMissing(bb.getName() + " [" + Form.capitalize(bb.getInferredType().name().toLowerCase()) + "] " + " (" + bb.getLoadFile().getName() + ")");
|
||||
caveBiomes.addIfMissing(bxf.getName() + " (" + bxf.getLoadFile().getName() + ")");
|
||||
exportObjects(bb, pw, engine(), objects);
|
||||
exportObjects(bxf, pw, engine(), objects);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
regions = Objects.requireNonNull(new File(world.getWorldFolder().getPath() + "/region").list()).length;
|
||||
|
||||
pw.println();
|
||||
pw.println("== World Info ==");
|
||||
pw.println("World Name: " + world.getName());
|
||||
pw.println("Age: " + age);
|
||||
pw.println("Folder: " + world.getWorldFolder().getPath());
|
||||
pw.println("Regions: " + Form.f(regions));
|
||||
pw.println("Chunks: max. " + Form.f(regions * 32 * 32));
|
||||
pw.println("World Size: min. " + Form.fileSize(size));
|
||||
pw.println();
|
||||
pw.println("== Biome Info ==");
|
||||
pw.println("Found " + biomes.size() + " Biome(s): ");
|
||||
|
||||
for (String i : biomes) {
|
||||
pw.println("- " + i);
|
||||
}
|
||||
pw.println();
|
||||
|
||||
pw.println("== Object Info ==");
|
||||
|
||||
for (String i : objects.k()) {
|
||||
pw.println("- " + i);
|
||||
|
||||
for (String j : objects.get(i).k()) {
|
||||
pw.println(" @ " + j);
|
||||
|
||||
for (String k : objects.get(i).get(j)) {
|
||||
pw.println(" * " + k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pw.println();
|
||||
pw.close();
|
||||
|
||||
sender().sendMessage("Reported to: " + ff.getPath());
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportObjects(IrisBiome bb, PrintWriter pw, Engine g, KMap<String, KMap<String, KList<String>>> objects) {
|
||||
String n1 = bb.getName() + " [" + Form.capitalize(bb.getInferredType().name().toLowerCase()) + "] " + " (" + bb.getLoadFile().getName() + ")";
|
||||
int m = 0;
|
||||
KSet<String> stop = new KSet<>();
|
||||
for (IrisObjectPlacement f : bb.getObjects()) {
|
||||
m++;
|
||||
String n2 = "Placement #" + m + " (" + f.getPlace().size() + " possible objects)";
|
||||
|
||||
for (String i : f.getPlace()) {
|
||||
String nn3 = i + ": [ERROR] Failed to find object!";
|
||||
|
||||
try {
|
||||
if (stop.contains(i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
File ff = g.getData().getObjectLoader().findFile(i);
|
||||
BlockVector sz = IrisObject.sampleSize(ff);
|
||||
nn3 = i + ": size=[" + sz.getBlockX() + "," + sz.getBlockY() + "," + sz.getBlockZ() + "] location=[" + ff.getPath() + "]";
|
||||
stop.add(i);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
String n3 = nn3;
|
||||
objects.computeIfAbsent(n1, (k1) -> new KMap<>())
|
||||
.computeIfAbsent(n2, (k) -> new KList<>()).addIfMissing(n3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if server GUIs are not enabled
|
||||
*/
|
||||
private boolean noGUI() {
|
||||
if (!IrisSettings.get().getGui().isUseServerLaunchedGuis()) {
|
||||
sender().sendMessage(C.RED + "You must have server launched GUIs enabled in the settings!");
|
||||
return true;
|
||||
}
|
||||
if (!GuiHost.isAvailable()) {
|
||||
sender().sendMessage(C.RED + "This server runs without a desktop, so it cannot open windows!");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if no studio is open or the player is not in one
|
||||
*/
|
||||
private boolean noStudio() {
|
||||
if (!sender().isPlayer()) {
|
||||
sender().sendMessage(C.RED + "Players only!");
|
||||
return true;
|
||||
}
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
sender().sendMessage(C.RED + "No studio world is open!");
|
||||
return true;
|
||||
}
|
||||
if (!engine().isStudio()) {
|
||||
sender().sendMessage(C.RED + "You must be in a studio world!");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public void files(File clean, KList<File> files) {
|
||||
if (clean.isDirectory()) {
|
||||
for (File i : clean.listFiles()) {
|
||||
files(i, files);
|
||||
}
|
||||
} else if (clean.getName().endsWith(".json")) {
|
||||
try {
|
||||
files.add(clean);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Iris.error("Failed to beautify " + clean.getAbsolutePath() + " You may have errors in your json!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fixBlocks(JSONObject obj) {
|
||||
for (String i : obj.keySet()) {
|
||||
Object o = obj.get(i);
|
||||
|
||||
if (i.equals("block") && o instanceof String && !o.toString().trim().isEmpty() && !o.toString().contains(":")) {
|
||||
obj.put(i, "minecraft:" + o);
|
||||
}
|
||||
|
||||
if (o instanceof JSONObject) {
|
||||
fixBlocks((JSONObject) o);
|
||||
} else if (o instanceof JSONArray) {
|
||||
fixBlocks((JSONArray) o);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fixBlocks(JSONArray obj) {
|
||||
for (int i = 0; i < obj.length(); i++) {
|
||||
Object o = obj.get(i);
|
||||
|
||||
if (o instanceof JSONObject) {
|
||||
fixBlocks((JSONObject) o);
|
||||
} else if (o instanceof JSONArray) {
|
||||
fixBlocks((JSONArray) o);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.commands;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.edit.BlockSignal;
|
||||
import com.volmit.iris.core.nms.INMS;
|
||||
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.util.data.B;
|
||||
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.matter.MatterMarker;
|
||||
import com.volmit.iris.util.scheduling.J;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.FluidCollisionMode;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Decree(name = "what", origin = DecreeOrigin.PLAYER, studio = true, description = "Iris What?")
|
||||
public class CommandWhat implements DecreeExecutor {
|
||||
@Decree(description = "What is in my hand?", origin = DecreeOrigin.PLAYER)
|
||||
public void hand() {
|
||||
try {
|
||||
BlockData bd = player().getInventory().getItemInMainHand().getType().createBlockData();
|
||||
if (!bd.getMaterial().equals(Material.AIR)) {
|
||||
sender().sendMessage("Material: " + C.GREEN + bd.getMaterial().name());
|
||||
sender().sendMessage("Full: " + C.WHITE + bd.getAsString(true));
|
||||
} else {
|
||||
sender().sendMessage("Please hold a block/item");
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Material bd = player().getInventory().getItemInMainHand().getType();
|
||||
if (!bd.equals(Material.AIR)) {
|
||||
sender().sendMessage("Material: " + C.GREEN + bd.name());
|
||||
} else {
|
||||
sender().sendMessage("Please hold a block/item");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "What biome am i in?", origin = DecreeOrigin.PLAYER)
|
||||
public void biome() {
|
||||
try {
|
||||
IrisBiome b = engine().getBiome(player().getLocation().getBlockX(), player().getLocation().getBlockY() - player().getWorld().getMinHeight(), player().getLocation().getBlockZ());
|
||||
sender().sendMessage("IBiome: " + b.getLoadKey() + " (" + b.getDerivative().name() + ")");
|
||||
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage("Non-Iris Biome: " + player().getLocation().getBlock().getBiome().name());
|
||||
|
||||
if (player().getLocation().getBlock().getBiome().equals(Biome.CUSTOM)) {
|
||||
try {
|
||||
sender().sendMessage("Data Pack Biome: " + INMS.get().getTrueBiomeBaseKey(player().getLocation()) + " (ID: " + INMS.get().getTrueBiomeBaseId(INMS.get().getTrueBiomeBase(player().getLocation())) + ")");
|
||||
} catch (Throwable ee) {
|
||||
Iris.reportError(ee);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
public void block() {
|
||||
BlockData bd;
|
||||
try {
|
||||
bd = player().getTargetBlockExact(128, FluidCollisionMode.NEVER).getBlockData();
|
||||
} catch (NullPointerException e) {
|
||||
Iris.reportError(e);
|
||||
sender().sendMessage("Please look at any block, not at the sky");
|
||||
bd = null;
|
||||
}
|
||||
|
||||
if (bd != null) {
|
||||
sender().sendMessage("Material: " + C.GREEN + bd.getMaterial().name());
|
||||
sender().sendMessage("Full: " + C.WHITE + bd.getAsString(true));
|
||||
|
||||
if (B.isStorage(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Storage Block (Loot Capable)");
|
||||
}
|
||||
|
||||
if (B.isLit(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Lit Block (Light Capable)");
|
||||
}
|
||||
|
||||
if (B.isFoliage(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Foliage Block");
|
||||
}
|
||||
|
||||
if (B.isDecorant(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Decorant Block");
|
||||
}
|
||||
|
||||
if (B.isFluid(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Fluid Block");
|
||||
}
|
||||
|
||||
if (B.isFoliagePlantable(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Plantable Foliage Block");
|
||||
}
|
||||
|
||||
if (B.isSolid(bd)) {
|
||||
sender().sendMessage(C.YELLOW + "* Solid Block");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Decree(description = "Show markers in chunk", origin = DecreeOrigin.PLAYER)
|
||||
public void markers(@Param(description = "Marker name such as cave_floor or cave_ceiling") String marker) {
|
||||
Chunk c = player().getLocation().getChunk();
|
||||
|
||||
if (IrisToolbelt.isIrisWorld(c.getWorld())) {
|
||||
int m = 1;
|
||||
AtomicInteger v = new AtomicInteger(0);
|
||||
|
||||
for (int xxx = c.getX() - 4; xxx <= c.getX() + 4; xxx++) {
|
||||
for (int zzz = c.getZ() - 4; zzz <= c.getZ() + 4; zzz++) {
|
||||
IrisToolbelt.access(c.getWorld()).getEngine().getMantle().findMarkers(xxx, zzz, new MatterMarker(marker))
|
||||
.convert((i) -> i.toLocation(c.getWorld())).forEach((i) -> {
|
||||
J.s(() -> BlockSignal.of(i.getBlock(), 100));
|
||||
v.incrementAndGet();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sender().sendMessage("Found " + v.get() + " Nearby Markers (" + marker + ")");
|
||||
} else {
|
||||
sender().sendMessage(C.IRIS + "Iris worlds only.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.edit;
|
||||
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
public interface BlockEditor extends Closeable {
|
||||
long last();
|
||||
|
||||
void set(int x, int y, int z, BlockData d);
|
||||
|
||||
BlockData get(int x, int y, int z);
|
||||
|
||||
void setBiome(int x, int z, Biome b);
|
||||
|
||||
void setBiome(int x, int y, int z, Biome b);
|
||||
|
||||
@Override
|
||||
void close();
|
||||
|
||||
Biome getBiome(int x, int y, int z);
|
||||
|
||||
Biome getBiome(int x, int z);
|
||||
}
|
||||
@@ -0,0 +1,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.edit;
|
||||
|
||||
import com.volmit.iris.util.parallel.MultiBurst;
|
||||
import com.volmit.iris.util.scheduling.J;
|
||||
import com.volmit.iris.util.scheduling.SR;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.FallingBlock;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@SuppressWarnings("InstantiationOfUtilityClass")
|
||||
public class BlockSignal {
|
||||
public static final AtomicInteger active = new AtomicInteger(0);
|
||||
|
||||
public BlockSignal(Block block, int ticks) {
|
||||
active.incrementAndGet();
|
||||
Location tg = block.getLocation().clone().add(0.5, 0, 0.5);
|
||||
FallingBlock e = block.getWorld().spawnFallingBlock(tg, block.getBlockData());
|
||||
e.setGravity(false);
|
||||
e.setInvulnerable(true);
|
||||
e.setGlowing(true);
|
||||
e.setDropItem(false);
|
||||
e.setHurtEntities(false);
|
||||
e.setSilent(true);
|
||||
e.setTicksLived(1);
|
||||
e.setVelocity(new Vector(0, 0, 0));
|
||||
J.s(() -> {
|
||||
e.remove();
|
||||
active.decrementAndGet();
|
||||
BlockData type = block.getBlockData();
|
||||
MultiBurst.burst.lazy(() -> {
|
||||
for (Player i : block.getWorld().getPlayers()) {
|
||||
i.sendBlockChange(block.getLocation(), block.getBlockData());
|
||||
}
|
||||
});
|
||||
}, ticks);
|
||||
}
|
||||
|
||||
public static void of(Block block, int ticks) {
|
||||
new BlockSignal(block, ticks);
|
||||
}
|
||||
|
||||
public static void of(Block block) {
|
||||
of(block, 100);
|
||||
}
|
||||
|
||||
public static Runnable forever(Block block) {
|
||||
Location tg = block.getLocation().clone().add(0.5, 0, 0.5).clone();
|
||||
FallingBlock e = block.getWorld().spawnFallingBlock(tg.clone(), block.getBlockData());
|
||||
e.setGravity(false);
|
||||
e.setInvulnerable(true);
|
||||
e.setGlowing(true);
|
||||
e.teleport(tg.clone());
|
||||
e.setDropItem(false);
|
||||
e.setHurtEntities(false);
|
||||
e.setSilent(true);
|
||||
e.setTicksLived(1);
|
||||
e.setVelocity(new Vector(0, 0, 0));
|
||||
|
||||
new SR(20) {
|
||||
@Override
|
||||
public void run() {
|
||||
if (e.isDead()) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
e.setTicksLived(1);
|
||||
e.teleport(tg.clone());
|
||||
e.setVelocity(new Vector(0, 0, 0));
|
||||
}
|
||||
};
|
||||
|
||||
return () -> {
|
||||
e.remove();
|
||||
BlockData type = block.getBlockData();
|
||||
|
||||
MultiBurst.burst.lazy(() -> {
|
||||
for (Player i : block.getWorld().getPlayers()) {
|
||||
i.sendBlockChange(block.getLocation(), block.getBlockData());
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.edit;
|
||||
|
||||
import com.volmit.iris.util.math.M;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
@SuppressWarnings("ClassCanBeRecord")
|
||||
public class BukkitBlockEditor implements BlockEditor {
|
||||
private final World world;
|
||||
|
||||
public BukkitBlockEditor(World world) {
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, BlockData d) {
|
||||
world.getBlockAt(x, y, z).setBlockData(d, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockData get(int x, int y, int z) {
|
||||
return world.getBlockAt(x, y, z).getBlockData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public long last() {
|
||||
return M.ms();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void setBiome(int x, int z, Biome b) {
|
||||
world.setBiome(x, z, b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBiome(int x, int y, int z, Biome b) {
|
||||
world.setBiome(x, y, z, b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Biome getBiome(int x, int y, int z) {
|
||||
return world.getBiome(x, y, z);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public Biome getBiome(int x, int z) {
|
||||
return world.getBiome(x, z);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.core.edit;
|
||||
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.tools.IrisToolbelt;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.math.BlockPosition;
|
||||
import com.volmit.iris.util.math.M;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
import com.volmit.iris.util.plugin.VolmitSender;
|
||||
import com.volmit.iris.util.scheduling.J;
|
||||
import lombok.Data;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
@Data
|
||||
public class DustRevealer {
|
||||
private final Engine engine;
|
||||
private final World world;
|
||||
private final BlockPosition block;
|
||||
private final String key;
|
||||
private final KList<BlockPosition> hits;
|
||||
|
||||
public DustRevealer(Engine engine, World world, BlockPosition block, String key, KList<BlockPosition> hits) {
|
||||
this.engine = engine;
|
||||
this.world = world;
|
||||
this.block = block;
|
||||
this.key = key;
|
||||
this.hits = hits;
|
||||
|
||||
J.s(() -> {
|
||||
new BlockSignal(world.getBlockAt(block.getX(), block.getY(), block.getZ()), 10);
|
||||
if (M.r(0.25)) {
|
||||
world.playSound(block.toBlock(world).getLocation(), Sound.BLOCK_AMETHYST_BLOCK_CHIME, 1f, RNG.r.f(0.2f, 2f));
|
||||
}
|
||||
J.a(() -> {
|
||||
while (BlockSignal.active.get() > 128) {
|
||||
J.sleep(5);
|
||||
}
|
||||
|
||||
try {
|
||||
is(new BlockPosition(block.getX() + 1, block.getY(), block.getZ()));
|
||||
is(new BlockPosition(block.getX() - 1, block.getY(), block.getZ()));
|
||||
is(new BlockPosition(block.getX(), block.getY() + 1, block.getZ()));
|
||||
is(new BlockPosition(block.getX(), block.getY() - 1, block.getZ()));
|
||||
is(new BlockPosition(block.getX(), block.getY(), block.getZ() + 1));
|
||||
is(new BlockPosition(block.getX(), block.getY(), block.getZ() - 1));
|
||||
is(new BlockPosition(block.getX() + 1, block.getY(), block.getZ() + 1));
|
||||
is(new BlockPosition(block.getX() + 1, block.getY(), block.getZ() - 1));
|
||||
is(new BlockPosition(block.getX() - 1, block.getY(), block.getZ() + 1));
|
||||
is(new BlockPosition(block.getX() - 1, block.getY(), block.getZ() - 1));
|
||||
is(new BlockPosition(block.getX() + 1, block.getY() + 1, block.getZ()));
|
||||
is(new BlockPosition(block.getX() + 1, block.getY() - 1, block.getZ()));
|
||||
is(new BlockPosition(block.getX() - 1, block.getY() + 1, block.getZ()));
|
||||
is(new BlockPosition(block.getX() - 1, block.getY() - 1, block.getZ()));
|
||||
is(new BlockPosition(block.getX(), block.getY() + 1, block.getZ() - 1));
|
||||
is(new BlockPosition(block.getX(), block.getY() + 1, block.getZ() + 1));
|
||||
is(new BlockPosition(block.getX(), block.getY() - 1, block.getZ() - 1));
|
||||
is(new BlockPosition(block.getX(), block.getY() - 1, block.getZ() + 1));
|
||||
is(new BlockPosition(block.getX() - 1, block.getY() + 1, block.getZ() - 1));
|
||||
is(new BlockPosition(block.getX() - 1, block.getY() + 1, block.getZ() + 1));
|
||||
is(new BlockPosition(block.getX() - 1, block.getY() - 1, block.getZ() - 1));
|
||||
is(new BlockPosition(block.getX() - 1, block.getY() - 1, block.getZ() + 1));
|
||||
is(new BlockPosition(block.getX() + 1, block.getY() + 1, block.getZ() - 1));
|
||||
is(new BlockPosition(block.getX() + 1, block.getY() + 1, block.getZ() + 1));
|
||||
is(new BlockPosition(block.getX() + 1, block.getY() - 1, block.getZ() - 1));
|
||||
is(new BlockPosition(block.getX() + 1, block.getY() - 1, block.getZ() + 1));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}, RNG.r.i(2, 8));
|
||||
}
|
||||
|
||||
public static void spawn(Block block, VolmitSender sender) {
|
||||
World world = block.getWorld();
|
||||
Engine access = IrisToolbelt.access(world).getEngine();
|
||||
|
||||
if (access != null) {
|
||||
String a = access.getObjectPlacementKey(block.getX(), block.getY() - block.getWorld().getMinHeight(), block.getZ());
|
||||
if (a != null) {
|
||||
world.playSound(block.getLocation(), Sound.ITEM_LODESTONE_COMPASS_LOCK, 1f, 0.1f);
|
||||
|
||||
sender.sendMessage("Found object " + a);
|
||||
J.a(() -> {
|
||||
new DustRevealer(access, world, new BlockPosition(block.getX(), block.getY(), block.getZ()), a, new KList<>());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean is(BlockPosition a) {
|
||||
int betterY = a.getY() - world.getMinHeight();
|
||||
if (isValidTry(a) && engine.getObjectPlacementKey(a.getX(), betterY, a.getZ()) != null && engine.getObjectPlacementKey(a.getX(), betterY, a.getZ()).equals(key)) {
|
||||
hits.add(a);
|
||||
new DustRevealer(engine, world, a, key, hits);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isValidTry(BlockPosition b) {
|
||||
return !hits.contains(b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* 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.edit;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.volmit.iris.Iris;
|
||||
import com.volmit.iris.core.service.WandSVC;
|
||||
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.data.Cuboid;
|
||||
import com.volmit.iris.util.io.IO;
|
||||
import com.volmit.iris.util.json.JSONObject;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
import com.volmit.iris.util.scheduling.ChronoLatch;
|
||||
import com.volmit.iris.util.scheduling.J;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class JigsawEditor implements Listener {
|
||||
public static final KMap<Player, JigsawEditor> editors = new KMap<>();
|
||||
private final Player player;
|
||||
private final IrisObject object;
|
||||
private final File targetSaveLocation;
|
||||
private final IrisJigsawPiece piece;
|
||||
private final Location origin;
|
||||
private final Cuboid cuboid;
|
||||
private final int ticker;
|
||||
private final KMap<IrisPosition, Runnable> falling = new KMap<>();
|
||||
private final ChronoLatch cl = new ChronoLatch(100);
|
||||
private Location target;
|
||||
|
||||
public JigsawEditor(Player player, IrisJigsawPiece piece, IrisObject object, File saveLocation) {
|
||||
if (object == null) throw new RuntimeException("Object is null! " + piece.getObject());
|
||||
editors.compute(player, ($, current) -> {
|
||||
if (current != null) {
|
||||
current.exit();
|
||||
}
|
||||
return this;
|
||||
});
|
||||
|
||||
this.object = object;
|
||||
this.player = player;
|
||||
origin = player.getLocation().clone().add(0, 7, 0);
|
||||
target = origin;
|
||||
this.targetSaveLocation = saveLocation;
|
||||
this.piece = piece == null ? new IrisJigsawPiece() : piece;
|
||||
this.piece.setObject(object.getLoadKey());
|
||||
cuboid = new Cuboid(origin.clone(), origin.clone().add(object.getW() - 1, object.getH() - 1, object.getD() - 1));
|
||||
ticker = J.sr(this::onTick, 0);
|
||||
J.s(() -> object.placeCenterY(origin));
|
||||
Iris.instance.registerListener(this);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(PlayerMoveEvent e) {
|
||||
if (e.getPlayer().equals(player)) {
|
||||
try {
|
||||
target = player.getTargetBlockExact(7).getLocation();
|
||||
} catch (Throwable ex) {
|
||||
Iris.reportError(ex);
|
||||
target = player.getLocation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cuboid.contains(target)) {
|
||||
for (IrisPosition i : falling.k()) {
|
||||
Location at = toLocation(i);
|
||||
|
||||
if (at.equals(target)) {
|
||||
falling.remove(i).run();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Location toLocation(IrisPosition i) {
|
||||
return origin.clone()
|
||||
.add(new Vector(i.getX(), i.getY(), i.getZ()))
|
||||
.add(object.getCenter())
|
||||
.getBlock()
|
||||
.getLocation();
|
||||
}
|
||||
|
||||
public IrisPosition toPosition(Location l) {
|
||||
return new IrisPosition(l.clone().getBlock().getLocation()
|
||||
.subtract(origin.clone())
|
||||
.subtract(object.getCenter())
|
||||
.add(1, 1, 1)
|
||||
.toVector());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(PlayerInteractEvent e) {
|
||||
if (e.getAction().equals(Action.RIGHT_CLICK_BLOCK)) {
|
||||
if (e.getClickedBlock() != null && cuboid.contains(e.getClickedBlock().getLocation()) && e.getPlayer().equals(player)) {
|
||||
IrisPosition pos = toPosition(e.getClickedBlock().getLocation());
|
||||
IrisJigsawPieceConnector connector = null;
|
||||
for (IrisJigsawPieceConnector i : piece.getConnectors()) {
|
||||
if (i.getPosition().equals(pos)) {
|
||||
connector = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!player.isSneaking() && connector == null) {
|
||||
connector = new IrisJigsawPieceConnector();
|
||||
connector.setDirection(IrisDirection.getDirection(e.getBlockFace()));
|
||||
connector.setPosition(pos);
|
||||
piece.getConnectors().add(connector);
|
||||
player.playSound(e.getClickedBlock().getLocation(), Sound.ENTITY_ITEM_FRAME_ADD_ITEM, 1f, 1f);
|
||||
} else if (player.isSneaking() && connector != null) {
|
||||
piece.getConnectors().remove(connector);
|
||||
player.playSound(e.getClickedBlock().getLocation(), Sound.ENTITY_ITEM_FRAME_REMOVE_ITEM, 1f, 1f);
|
||||
} else if (connector != null && !player.isSneaking()) {
|
||||
connector.setDirection(IrisDirection.getDirection(e.getBlockFace()));
|
||||
player.playSound(e.getClickedBlock().getLocation(), Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeKey(JSONObject o, String... path) {
|
||||
if (path.length == 1) {
|
||||
o.remove(path[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> s = new java.util.ArrayList<>(List.of(path));
|
||||
s.remove(0);
|
||||
removeKey(o.getJSONObject(path[0]), s.toArray(new String[0]));
|
||||
}
|
||||
|
||||
private List<JSONObject> getObjectsInArray(JSONObject a) { // This gets all the objects in an array that are connectors
|
||||
KList<JSONObject> o = new KList<>();
|
||||
|
||||
for (int i = 0; i < a.getJSONArray("connectors").length(); i++) {
|
||||
o.add(a.getJSONArray("connectors").getJSONObject(i));
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
exit();
|
||||
try {
|
||||
JSONObject j = new JSONObject(new Gson().toJson(piece));
|
||||
// Remove sub-key
|
||||
removeKey(j, "placementOptions", "translateCenter"); // should work
|
||||
J.attempt(() -> j.getJSONObject("placementOptions").remove("translateCenter")); // otherwise
|
||||
|
||||
// remove root key
|
||||
removeKey(j, "placementOptions"); // should work
|
||||
j.remove("placementOptions"); // otherwise
|
||||
|
||||
// Remove key in all objects in array
|
||||
for (JSONObject i : getObjectsInArray(j)) {
|
||||
removeKey(i, "rotateConnector");
|
||||
}
|
||||
|
||||
IO.writeAll(targetSaveLocation, j.toString(4));
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void exit() {
|
||||
J.car(ticker);
|
||||
Iris.instance.unregisterListener(this);
|
||||
try {
|
||||
J.sfut(() -> {
|
||||
object.unplaceCenterY(origin);
|
||||
falling.v().forEach(Runnable::run);
|
||||
}).get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
editors.remove(player);
|
||||
}
|
||||
|
||||
public void onTick() {
|
||||
if (cl.flip()) {
|
||||
Iris.service(WandSVC.class).draw(cuboid, player);
|
||||
|
||||
f:
|
||||
for (IrisPosition i : falling.k()) {
|
||||
for (IrisJigsawPieceConnector j : piece.getConnectors()) {
|
||||
if (j.getPosition().equals(i)) {
|
||||
continue f;
|
||||
}
|
||||
}
|
||||
|
||||
falling.remove(i).run();
|
||||
}
|
||||
|
||||
for (IrisJigsawPieceConnector i : piece.getConnectors()) {
|
||||
IrisPosition pos = i.getPosition();
|
||||
Location at = toLocation(pos);
|
||||
|
||||
Vector dir = i.getDirection().toVector().clone();
|
||||
|
||||
|
||||
for (int ix = 0; ix < RNG.r.i(1, 3); ix++) {
|
||||
at.getWorld().spawnParticle(Particle.SOUL_FIRE_FLAME, at.clone().getBlock().getLocation().add(0.25, 0.25, 0.25).add(RNG.r.d(0.5), RNG.r.d(0.5), RNG.r.d(0.5)), 0, dir.getX(), dir.getY(), dir.getZ(), 0.092 + RNG.r.d(-0.03, 0.08));
|
||||
}
|
||||
|
||||
if (at.getBlock().getLocation().equals(target)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!falling.containsKey(pos)) {
|
||||
if (at.getBlock().getType().isAir()) {
|
||||
at.getBlock().setType(Material.STONE);
|
||||
}
|
||||
|
||||
falling.put(pos, BlockSignal.forever(at.getBlock()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.events;
|
||||
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class IrisEngineEvent extends Event {
|
||||
private static final HandlerList handlers = new HandlerList();
|
||||
private Engine engine;
|
||||
|
||||
public IrisEngineEvent() {
|
||||
super(true);
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return handlers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.events;
|
||||
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
public class IrisEngineHotloadEvent extends IrisEngineEvent {
|
||||
private static final HandlerList handlers = new HandlerList();
|
||||
|
||||
public IrisEngineHotloadEvent(Engine engine) {
|
||||
super(engine);
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return handlers;
|
||||
}
|
||||
}
|
||||
@@ -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,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.";
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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,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);
|
||||
}
|
||||
}
|
||||
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,112 @@
|
||||
/*
|
||||
* 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.RenderType;
|
||||
import com.volmit.iris.core.loader.IrisRegistrant;
|
||||
import com.volmit.iris.engine.object.IrisBiome;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The text of the card that follows the cursor: what the drawn map shows there at once, and the rest once probed.
|
||||
*/
|
||||
final class MapHoverCard {
|
||||
private MapHoverCard() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param subject the biome, or region in the region view, the drawn tile shows; null while that tile is drawing
|
||||
* @param details the probed facts about the spot, or null until they arrive
|
||||
* @param detailed whether Shift is held, which adds keys and files
|
||||
*/
|
||||
static List<String> lines(MapLabels.Label subject, MapLabels.Label region, MapHoverProbe.Details details,
|
||||
RenderType type, boolean detailed, double worldX, double worldZ) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
lines.add(title(subject, details));
|
||||
if (type != RenderType.REGION) {
|
||||
String regionName = region != null ? region.name() : details != null ? details.region().getName() : null;
|
||||
if (regionName != null) {
|
||||
lines.add("Region " + regionName);
|
||||
}
|
||||
}
|
||||
if (details != null) {
|
||||
addTerrain(lines, details, type);
|
||||
}
|
||||
int blockX = MapCanvas.floorWorldCoordinate(worldX);
|
||||
int blockZ = MapCanvas.floorWorldCoordinate(worldZ);
|
||||
lines.add("Block " + blockX + ", " + blockZ + " · chunk " + (blockX >> 4) + ", " + (blockZ >> 4));
|
||||
if (!detailed) {
|
||||
lines.add("Shift for keys and files · H highlights");
|
||||
return lines;
|
||||
}
|
||||
addSource(lines, subject);
|
||||
if (subject != null && subject.kind() != MapLabels.Kind.REGION) {
|
||||
addSource(lines, region);
|
||||
}
|
||||
lines.add("Region file r." + (blockX >> 9) + "." + (blockZ >> 9) + ".mca");
|
||||
return lines;
|
||||
}
|
||||
|
||||
private static String title(MapLabels.Label subject, MapHoverProbe.Details details) {
|
||||
if (subject != null) {
|
||||
return subject.name();
|
||||
}
|
||||
if (details != null && details.surfaceBiome() != null) {
|
||||
return details.surfaceBiome().getName();
|
||||
}
|
||||
return "Drawing…";
|
||||
}
|
||||
|
||||
private static void addTerrain(List<String> lines, MapHoverProbe.Details details, RenderType type) {
|
||||
lines.add("Height " + Math.round(details.height()) + " · sea level " + Math.round(details.fluidHeight()));
|
||||
String slope = "slope " + Math.round(details.slopeDegrees()) + "°";
|
||||
lines.add(details.terrainKind() == null ? capitalize(slope) : details.terrainKind() + " · " + slope);
|
||||
IrisBiome surface = details.surfaceBiome();
|
||||
IrisBiome cave = details.caveBiome();
|
||||
if (type == RenderType.CAVE_LAND && surface != null) {
|
||||
lines.add("Surface biome " + surface.getName());
|
||||
} else if (type != RenderType.CAVE_LAND && cave != null && cave != surface) {
|
||||
lines.add("Cave biome " + cave.getName());
|
||||
}
|
||||
if (details.vanillaBiome() != null) {
|
||||
lines.add("Minecraft biome " + details.vanillaBiome().replace('_', ' '));
|
||||
}
|
||||
}
|
||||
|
||||
private static void addSource(List<String> lines, MapLabels.Label label) {
|
||||
if (label == null || !(label.subject() instanceof IrisRegistrant registrant)) {
|
||||
return;
|
||||
}
|
||||
String kind = label.kind() == MapLabels.Kind.REGION ? "Region" : "Biome";
|
||||
lines.add(kind + " key " + label.key());
|
||||
File file = registrant.getLoadFile();
|
||||
if (file != null) {
|
||||
File folder = file.getParentFile();
|
||||
lines.add(kind + " file " + (folder == null ? "" : folder.getName() + "/") + file.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private static String capitalize(String text) {
|
||||
return Character.toUpperCase(text.charAt(0)) + text.substring(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.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.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() {
|
||||
}
|
||||
|
||||
static Details sample(Engine engine, double worldX, double worldZ) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
IrisBiome surfaceBiome = complex.getBaseBiomeStream().get(worldX, worldZ);
|
||||
IrisBiome caveBiome = complex.getCaveBiomeStream().get(worldX, worldZ);
|
||||
IrisRegion region = complex.getRegionStream().get(worldX, worldZ);
|
||||
InferredType terrainKind = complex.getBridgeStream().get(worldX, worldZ);
|
||||
Biome vanillaBiome = complex.getTrueBiomeDerivativeStream().get(worldX, worldZ);
|
||||
double height = complex.getHeightStream().getDouble(worldX, worldZ);
|
||||
double slope = complex.getSlopeStream().getDouble(worldX, worldZ);
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
return new Details(
|
||||
worldX,
|
||||
worldZ,
|
||||
surfaceBiome,
|
||||
caveBiome,
|
||||
region,
|
||||
terrainKindName(surfaceBiome, terrainKind),
|
||||
vanillaBiome == null ? null : vanillaBiome.getKey().getKey(),
|
||||
height,
|
||||
dimension == null ? 0D : dimension.getFluidHeight(),
|
||||
Math.toDegrees(Math.atan(slope / SLOPE_STREAM_RANGE_BLOCKS))
|
||||
);
|
||||
}
|
||||
|
||||
// A shore biome sits on land the continent stream calls land or sea, so the biome's own type says it best.
|
||||
private static String terrainKindName(IrisBiome surfaceBiome, InferredType continentKind) {
|
||||
InferredType kind = surfaceBiome != null && surfaceBiome.getInferredType() != null
|
||||
? surfaceBiome.getInferredType()
|
||||
: continentKind;
|
||||
if (kind == null) {
|
||||
return null;
|
||||
}
|
||||
String name = kind.name().toLowerCase(Locale.ROOT);
|
||||
return Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
||||
}
|
||||
|
||||
record Details(
|
||||
double worldX,
|
||||
double worldZ,
|
||||
IrisBiome surfaceBiome,
|
||||
IrisBiome caveBiome,
|
||||
IrisRegion region,
|
||||
String terrainKind,
|
||||
String vanillaBiome,
|
||||
double height,
|
||||
double fluidHeight,
|
||||
double slopeDegrees
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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,116 @@
|
||||
/*
|
||||
* 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", 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, judged by each biome's first height generator",
|
||||
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),
|
||||
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 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,47 @@
|
||||
/*
|
||||
* 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, MapViewIcon::load);
|
||||
}
|
||||
|
||||
private static Icon load(MapView view) {
|
||||
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,497 @@
|
||||
/*
|
||||
* 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.List;
|
||||
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 OBJECTS_CARD = "objects";
|
||||
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 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 NoiseExplorerGUI paletteExplorer;
|
||||
private ObjectExplorer objectExplorer;
|
||||
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()));
|
||||
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();
|
||||
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) {
|
||||
showObjects();
|
||||
} 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);
|
||||
useSidebar(viewListScroll);
|
||||
inspectorSlot.removeAll();
|
||||
inspectorSlot.add(legendPanel, BorderLayout.CENTER);
|
||||
showCard(MAP_CARD);
|
||||
canvas.requestFocusInWindow();
|
||||
}
|
||||
|
||||
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 showObjects() {
|
||||
boolean firstOpen = objectExplorer == null;
|
||||
if (firstOpen) {
|
||||
createObjectExplorer();
|
||||
}
|
||||
canvas.setActive(false);
|
||||
showControls(explorerControls);
|
||||
useSidebar(viewListAbove(objectExplorer.browser()));
|
||||
inspectorSlot.removeAll();
|
||||
inspectorSlot.add(objectExplorer.overview(), BorderLayout.CENTER);
|
||||
showCard(OBJECTS_CARD);
|
||||
EventQueue.invokeLater(() -> viewList.select(currentView));
|
||||
if (firstOpen) {
|
||||
objectExplorer.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 void createObjectExplorer() {
|
||||
objectExplorer = new ObjectExplorer(start.engine(), start.worldId());
|
||||
keepClearOfIsland(objectExplorer.viewport()::setCoveredLeftWidth);
|
||||
content.add(objectExplorer.view(), OBJECTS_CARD);
|
||||
statusBars.add(objectExplorer.statusBar(), OBJECTS_CARD);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private void resetExplorerView() {
|
||||
if (currentView == MapView.OBJECTS) {
|
||||
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();
|
||||
if (paletteExplorer != null) {
|
||||
paletteExplorer.close();
|
||||
}
|
||||
if (objectExplorer != null) {
|
||||
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,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,270 @@
|
||||
/*
|
||||
* 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("Loading objects...");
|
||||
private List<String> objectKeys = List.of();
|
||||
private String chosenKey;
|
||||
private boolean updatingSelection;
|
||||
|
||||
ObjectBrowser(Consumer<String> onObjectChosen) {
|
||||
super(new BorderLayout());
|
||||
this.onObjectChosen = onObjectChosen;
|
||||
StudioToolbar.makeSearchField(searchField, "Search objects...");
|
||||
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 object" : objectKeys.size() + " objects");
|
||||
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 objects")));
|
||||
}
|
||||
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,317 @@
|
||||
/*
|
||||
* 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.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 java.awt.EventQueue;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
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.Executors;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* The map window's Objects view: every object in the pack in a folder tree, the chosen one in 3D, and what it is made
|
||||
* of. 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 final Engine startEngine;
|
||||
private final UUID worldId;
|
||||
private final ObjectBrowser browser = new ObjectBrowser(this::showObject);
|
||||
private final ObjectViewport viewport = new ObjectViewport();
|
||||
private final ObjectLayerCut layerCut = new ObjectLayerCut(shownLayers -> showLayerCut());
|
||||
private final JComponent viewWithLayerCut = layerCut.floatingOver(viewport);
|
||||
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 AtomicLong objectRevision = 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 LoadedObject shown;
|
||||
private ObjectMesh wholeMesh;
|
||||
private Set<String> seeThroughTypes = Set.of();
|
||||
|
||||
ObjectExplorer(Engine engine, UUID worldId) {
|
||||
this.startEngine = engine;
|
||||
this.worldId = worldId;
|
||||
GuiHost.interruptOnPluginShutdown(loader);
|
||||
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));
|
||||
}
|
||||
|
||||
JComponent browser() {
|
||||
return browser;
|
||||
}
|
||||
|
||||
ObjectViewport viewport() {
|
||||
return viewport;
|
||||
}
|
||||
|
||||
/**
|
||||
* The 3D view with the slider that cuts away its top layers.
|
||||
*/
|
||||
JComponent view() {
|
||||
return viewWithLayerCut;
|
||||
}
|
||||
|
||||
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() {
|
||||
statusBar.showActivity("Loading Minecraft textures...");
|
||||
loader.execute(() -> {
|
||||
loadTextures();
|
||||
List<String> keys = objectKeys();
|
||||
EventQueue.invokeLater(() -> showCatalog(keys));
|
||||
});
|
||||
}
|
||||
|
||||
void close() {
|
||||
GuiHost.unregisterHotloadHook(hotloadHook);
|
||||
objectRevision.incrementAndGet();
|
||||
loader.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 Arrays.asList(engine().getData().getObjectLoader().getPossibleKeys());
|
||||
}
|
||||
|
||||
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.showMessage("This pack has no objects");
|
||||
overview.clear();
|
||||
layerCut.showHeight(1);
|
||||
return;
|
||||
}
|
||||
browser.select(key);
|
||||
showObject(key);
|
||||
}
|
||||
|
||||
private void showObject(String key) {
|
||||
shownKey = key;
|
||||
shown = null;
|
||||
wholeMesh = null;
|
||||
meshRevision.incrementAndGet();
|
||||
long revision = objectRevision.incrementAndGet();
|
||||
viewport.showMessage("Loading " + key + "...");
|
||||
statusBar.showActivity("Loading " + key + "...");
|
||||
loader.execute(() -> {
|
||||
if (revision != objectRevision.get()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
LoadedObject loaded = load(key);
|
||||
EventQueue.invokeLater(() -> showLoaded(revision, loaded));
|
||||
} catch (RuntimeException error) {
|
||||
EventQueue.invokeLater(() -> showFailure(revision, key, error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private LoadedObject load(String key) {
|
||||
IrisObject object = engine().getData().getObjectLoader().load(key);
|
||||
if (object == null) {
|
||||
throw new IllegalStateException("The object file could not be read");
|
||||
}
|
||||
Map<BlockVector, BlockData> blocks = new HashMap<>();
|
||||
object.getBlocks().forEach((position, block) -> {
|
||||
if (!block.getMaterial().isAir()) {
|
||||
blocks.put(position, block);
|
||||
}
|
||||
});
|
||||
ObjectMesh mesh = ObjectMesh.build(blocks, this::modelOf);
|
||||
ObjectSummary summary = ObjectSummary.of(key, mesh.bounds(), blocks.values(), this::iconOf);
|
||||
return new LoadedObject(blocks, mesh, summary);
|
||||
}
|
||||
|
||||
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 · %,d blocks · %,d faces drawn%s",
|
||||
summary.key(), summary.blockCount(), mesh.faceCount(), cut));
|
||||
}
|
||||
|
||||
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);
|
||||
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,331 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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(ObjectMesh mesh, ObjectCamera camera, BufferedImage image, int coveredLeftWidth) {
|
||||
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 = new Projection(mesh.bounds(), camera, width, height, coveredLeftWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 = new BufferedImage(Math.max(1, width), Math.max(1, height), BufferedImage.TYPE_INT_RGB);
|
||||
ObjectRenderer renderer = new ObjectRenderer(mesh, camera, image, Math.max(0, Math.min(width - 1, coveredLeftWidth)));
|
||||
Arrays.fill(renderer.pixels, StudioTheme.EDITOR_BACKGROUND.getRGB() & 0xFFFFFF);
|
||||
Arrays.fill(renderer.depths, Double.NEGATIVE_INFINITY);
|
||||
renderer.drawGrid(mesh.bounds());
|
||||
for (ObjectMesh.Quad quad : mesh.solidQuads()) {
|
||||
renderer.drawQuad(quad, Pass.SOLID);
|
||||
}
|
||||
for (ObjectMesh.Quad quad : mesh.cutAwayQuads()) {
|
||||
renderer.drawQuad(quad, Pass.CUT_AWAY_GHOST);
|
||||
}
|
||||
for (ObjectMesh.Quad quad : renderer.farthestFirst(mesh.translucentQuads())) {
|
||||
renderer.drawQuad(quad, Pass.BLENDED);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
private Projection(ObjectMesh.Bounds bounds, ObjectCamera camera, int width, int height, int coveredLeftWidth) {
|
||||
this.centerX = (bounds.minX() + bounds.maxX()) / 2D;
|
||||
this.centerY = (bounds.minY() + bounds.maxY()) / 2D;
|
||||
this.centerZ = (bounds.minZ() + bounds.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(bounds, visibleWidth / (double) height);
|
||||
this.pixelsPerBlock = camera.zoom() * fit * height / 2D / bounds.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,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,250 @@
|
||||
/*
|
||||
* 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 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.
|
||||
*/
|
||||
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 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 boolean rendering;
|
||||
private boolean renderAgain;
|
||||
private Point dragPoint;
|
||||
private int coveredLeftWidth;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
void show(ObjectMesh nextMesh) {
|
||||
mesh = nextMesh;
|
||||
camera = ObjectCamera.START;
|
||||
message = nextMesh.isEmpty() ? "No visible blocks" : null;
|
||||
frame = null;
|
||||
requestFrame();
|
||||
repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
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;
|
||||
frame = null;
|
||||
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() {
|
||||
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;
|
||||
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 (message != null) {
|
||||
drawMessage((Graphics2D) graphics);
|
||||
}
|
||||
}
|
||||
|
||||
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,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.framework.Engine;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record PaletteExplorerStart(Engine engine, UUID worldId, UUID playerId, String biomeKey, double centerX, double centerZ) {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 1265 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user