mirror of
https://github.com/MinecraftForge/MinecraftForge.git
synced 2026-11-14 09:05:17 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
041084d1da | ||
|
|
e643d0a11f | ||
|
|
5efb8d3f12 | ||
|
|
70537542d1 | ||
|
|
bd4b059c2b | ||
|
|
e0d2c68c5b | ||
|
|
86d4bda32e | ||
|
|
98b90f26bc | ||
|
|
4e7c471fa3 | ||
|
|
ec6f9fc6e7 | ||
|
|
427fef1d4b | ||
|
|
83594b0b0a | ||
|
|
b76064e57a | ||
|
|
b2bf70c7f1 | ||
|
|
384286a453 | ||
|
|
d93b7db69a | ||
|
|
bdbde4bb0f | ||
|
|
e2a68ad903 | ||
|
|
46cfe50108 | ||
|
|
4b423a3087 | ||
|
|
7d0b07e858 | ||
|
|
40220600b0 | ||
|
|
0c7a0e927c | ||
|
|
60e8157b9a | ||
|
|
b8f5e4553b | ||
|
|
de587eb50a | ||
|
|
086bcb7148 | ||
|
|
c22589b5b3 | ||
|
|
e0fbab3c49 | ||
|
|
7cfed38194 | ||
|
|
b8105ca5a6 | ||
|
|
c3ee633897 | ||
|
|
23b41ce9b3 | ||
|
|
2c6156be25 | ||
|
|
eeb3eb3689 | ||
|
|
44a158a714 | ||
|
|
770298b1d5 | ||
|
|
7bd50903a6 | ||
|
|
64295d04ff | ||
|
|
f168ce4499 | ||
|
|
419c349a1f | ||
|
|
26d0f7282f | ||
|
|
93c72d2d92 | ||
|
|
f252871322 | ||
|
|
32c8597305 | ||
|
|
9714512c31 | ||
|
|
f106ff0c21 | ||
|
|
9c85a6bb64 | ||
|
|
dc594f196a | ||
|
|
db4d8a4729 | ||
|
|
0ca1d0ebd0 | ||
|
|
4995902bc0 | ||
|
|
28e55b2843 | ||
|
|
decaace84a | ||
|
|
72f98dc4a3 | ||
|
|
2951c1dac8 | ||
|
|
40a0a16db0 | ||
|
|
520680b037 | ||
|
|
2844bead6e | ||
|
|
d498ec4a45 | ||
|
|
e2d1ca7d0e | ||
|
|
a547327c61 | ||
|
|
e9a7ce9544 | ||
|
|
511be6415c | ||
|
|
4d85053a4d | ||
|
|
4fcf1877c1 | ||
|
|
41fd15fc57 | ||
|
|
8e0845d032 | ||
|
|
c04e533a61 | ||
|
|
54f4997f71 | ||
|
|
3272e995ab | ||
|
|
8edb551571 | ||
|
|
4bf0de4763 | ||
|
|
f6a54436b9 | ||
|
|
7a497cdbd6 | ||
|
|
06336a40d8 | ||
|
|
842c95cfb9 | ||
|
|
aa2f471e87 | ||
|
|
4209d1fc12 | ||
|
|
583f363b6b | ||
|
|
1c633b4624 | ||
|
|
2677511da6 | ||
|
|
387441c045 | ||
|
|
4a2db246e0 | ||
|
|
3d9e12a71d | ||
|
|
cdeb786877 | ||
|
|
ff7085b95b | ||
|
|
16dde4d551 | ||
|
|
60661350c6 | ||
|
|
7ee4a44c75 | ||
|
|
f22894cf70 | ||
|
|
a3fce36942 | ||
|
|
549dbe7df4 | ||
|
|
5505e472aa | ||
|
|
c2c7f9b656 | ||
|
|
fd8a0f9c8b | ||
|
|
0ac873e296 | ||
|
|
38b5b678cb | ||
|
|
9a2bc1e7c2 | ||
|
|
95b49bedbc | ||
|
|
7c36115e3d | ||
|
|
c037e6aca1 | ||
|
|
e8f1b7ed6d | ||
|
|
5da16e6719 | ||
|
|
f0a82d79a6 | ||
|
|
2b3a4137e1 | ||
|
|
640c206117 | ||
|
|
59a7bbdb57 | ||
|
|
be7ad14b4b | ||
|
|
c700b4ea9d | ||
|
|
dffa3dd4c9 | ||
|
|
4b934809c4 | ||
|
|
ddf76db9b3 | ||
|
|
64d6b4f76a | ||
|
|
58012bfb17 | ||
|
|
0cad6b14d9 | ||
|
|
8848294210 | ||
|
|
0e28336fc0 | ||
|
|
968fa9eeb5 | ||
|
|
43768ea328 | ||
|
|
0e829630da | ||
|
|
0467d71466 | ||
|
|
115eb5e4c8 | ||
|
|
a7345372fd | ||
|
|
bd714b2f76 | ||
|
|
7b782e5b05 | ||
|
|
e1fa61080a | ||
|
|
381998c35c | ||
|
|
11ed2500a6 | ||
|
|
a647237db4 | ||
|
|
e0dccae041 | ||
|
|
e7a5c58907 | ||
|
|
e094eee54d | ||
|
|
a6adc58655 | ||
|
|
d31ea640f9 | ||
|
|
8e16765438 | ||
|
|
73b0088cb4 | ||
|
|
203fbbb742 | ||
|
|
46d78c8210 | ||
|
|
cfba4381b6 | ||
|
|
7113701322 | ||
|
|
73e02c6add | ||
|
|
7f2b85d2ff | ||
|
|
099d121c2c | ||
|
|
a6978eed68 | ||
|
|
309116520b | ||
|
|
3e358acbe1 | ||
|
|
c243adf6aa | ||
|
|
5d0047753d | ||
|
|
aa6f268c74 | ||
|
|
d21556a4da | ||
|
|
d4c998e311 | ||
|
|
25727a0e93 | ||
|
|
80286594c5 | ||
|
|
870e3ebcab | ||
|
|
a4c26c9b2b | ||
|
|
45b00b0004 | ||
|
|
4b1c5ddb2c | ||
|
|
0966109602 | ||
|
|
b31006534a | ||
|
|
740a5d5039 | ||
|
|
817e20821d | ||
|
|
fdb22d02cc | ||
|
|
f09fd1f05b | ||
|
|
ed2ba0f52c | ||
|
|
b04118982f | ||
|
|
d44d0b8edc | ||
|
|
6d7fda6a9b | ||
|
|
0303ca651e | ||
|
|
8445f73b2d | ||
|
|
c7446a5180 | ||
|
|
5c15aa3322 | ||
|
|
588466f5e4 | ||
|
|
470f03b2f9 | ||
|
|
d2367d81fc | ||
|
|
6f9f0f0cfa | ||
|
|
16defac82e | ||
|
|
16483d4ea1 | ||
|
|
28909812bb | ||
|
|
e5580b84b7 | ||
|
|
607ca5beea | ||
|
|
744906e33a | ||
|
|
c1557ec3c2 | ||
|
|
4ac1a3dedf | ||
|
|
a26f952212 | ||
|
|
83ee68c0b0 | ||
|
|
a5ff932125 | ||
|
|
0d820eddd4 | ||
|
|
83c651ce39 | ||
|
|
7cd88944d4 | ||
|
|
dff52ecd71 | ||
|
|
da34d97d9d | ||
|
|
a27405b8d1 | ||
|
|
7a443276a5 | ||
|
|
e34090c809 | ||
|
|
72e8ae252c | ||
|
|
347c480bdc | ||
|
|
e0b9474621 | ||
|
|
8ad1e899a3 | ||
|
|
3b6bb5edbc | ||
|
|
c4e7d694c1 | ||
|
|
e00de1a326 | ||
|
|
e9ee999558 | ||
|
|
a1aacf25fe | ||
|
|
0c2f3e5523 | ||
|
|
a6978cffbf | ||
|
|
2332ecf984 | ||
|
|
9cd73c84a6 | ||
|
|
ab544a78d3 | ||
|
|
5b0abf97c8 | ||
|
|
587b684035 | ||
|
|
90dafb0df0 | ||
|
|
aff05a4cf3 | ||
|
|
1ddac54256 | ||
|
|
fcf9088aa5 | ||
|
|
374dbc7a4b | ||
|
|
d25ba7adc8 | ||
|
|
05c88ae33a | ||
|
|
e2d564d32a | ||
|
|
225092130e | ||
|
|
eefca22aa3 | ||
|
|
19bbb88e52 | ||
|
|
18886e5128 | ||
|
|
47dfa83cff | ||
|
|
0940c532f2 | ||
|
|
d80f106140 | ||
|
|
516b35ce57 | ||
|
|
be60ddbfca | ||
|
|
af6e848227 | ||
|
|
dd139115ed | ||
|
|
145df60bec | ||
|
|
f7cca8fb88 | ||
|
|
1f59b3f9fe | ||
|
|
084effdadf | ||
|
|
40a7998281 | ||
|
|
4786fa6f89 | ||
|
|
9291b865c7 | ||
|
|
52eca96fdc | ||
|
|
4649f7af36 | ||
|
|
3edefbe33e | ||
|
|
12d24df4f6 | ||
|
|
e69f5e262a | ||
|
|
23cee57d88 | ||
|
|
ab70bde1d6 | ||
|
|
f65381fbff | ||
|
|
b680d09359 | ||
|
|
7969daf81a | ||
|
|
f157567d2a | ||
|
|
93a615e900 | ||
|
|
7c84249996 | ||
|
|
881725c7d5 | ||
|
|
2745531f08 | ||
|
|
bcdd44aece | ||
|
|
bad5b1b5d4 | ||
|
|
30f255d76c | ||
|
|
ebb316a15c | ||
|
|
e6bb7eecbd | ||
|
|
c66f70d6c2 | ||
|
|
26adf4da44 | ||
|
|
30d0a630d6 | ||
|
|
00ad61b4ae | ||
|
|
09bc73d5cd | ||
|
|
1ee53c0464 | ||
|
|
16863c8d3b | ||
|
|
150e8281ae | ||
|
|
4385eb2155 | ||
|
|
5c40bef28e | ||
|
|
55aa419100 | ||
|
|
a4bef82836 | ||
|
|
b741df5f18 | ||
|
|
0c81a71308 | ||
|
|
ea7f755a11 | ||
|
|
d947d35eb1 | ||
|
|
5271ef79fe | ||
|
|
b7131824f0 | ||
|
|
3e624db4f8 | ||
|
|
fc7af96bde | ||
|
|
b1965db30b | ||
|
|
8df5a80725 | ||
|
|
d9043298d3 | ||
|
|
e91ff9a5a6 | ||
|
|
77fb08713f | ||
|
|
2c965fac90 | ||
|
|
2f229c5bb1 | ||
|
|
54b08d2711 | ||
|
|
46f641a502 | ||
|
|
6304cb655b | ||
|
|
27614b46a6 | ||
|
|
ca47813bfa | ||
|
|
b15b84fd59 | ||
|
|
97edac430d | ||
|
|
7b833faa46 | ||
|
|
93e4a7c649 | ||
|
|
ccbc697bad | ||
|
|
77cd7043c9 | ||
|
|
c0fe1688d8 | ||
|
|
72e2ae1d6f | ||
|
|
027f4d3d7b | ||
|
|
47ed2ad401 | ||
|
|
b34275988c | ||
|
|
47a2e137ed | ||
|
|
063546c37d | ||
|
|
3733005839 | ||
|
|
7d0330eb08 | ||
|
|
4c978a510c | ||
|
|
81f123fcab | ||
|
|
a8dce04c1b | ||
|
|
fa66faff6d | ||
|
|
18536c76ae | ||
|
|
ad9b14e59c | ||
|
|
45e27c62e9 | ||
|
|
f1939ef144 | ||
|
|
a43be39137 | ||
|
|
9450dfd67c | ||
|
|
a3af55f85b | ||
|
|
9f858ef931 | ||
|
|
b5f5d1d848 | ||
|
|
15878af658 | ||
|
|
2d8d6b2a0a | ||
|
|
7a25e83d34 | ||
|
|
8768c00f31 | ||
|
|
8c003dd965 | ||
|
|
dadc55cb2d | ||
|
|
83ab7656c9 | ||
|
|
d26cdf0b55 | ||
|
|
e382ed526f | ||
|
|
9cf1a77fdb | ||
|
|
5318d70c18 | ||
|
|
060889656b | ||
|
|
c43c15932f | ||
|
|
4d9df706b0 | ||
|
|
af8900a748 | ||
|
|
439d07e403 | ||
|
|
1ad8d48683 | ||
|
|
031d008826 | ||
|
|
551feaf019 | ||
|
|
032639b151 | ||
|
|
a32a06ce8a | ||
|
|
baa3098435 | ||
|
|
1a413eeed2 | ||
|
|
497274fd93 | ||
|
|
2eddcabdb7 | ||
|
|
ce84ba86f0 | ||
|
|
622c47c075 | ||
|
|
7614e024ed | ||
|
|
a4570cb22f | ||
|
|
76349744f7 | ||
|
|
75d647615e | ||
|
|
8482714375 | ||
|
|
2d8a935695 | ||
|
|
f6c13682d2 | ||
|
|
18c3ea9625 | ||
|
|
09de4a2dcc | ||
|
|
d8c84e2949 | ||
|
|
658e614dc1 | ||
|
|
9abfcbc90b | ||
|
|
6ee20899bf | ||
|
|
42115d37d6 | ||
|
|
8abe9b6b0c | ||
|
|
fc91dfd1d1 | ||
|
|
8cd6f2d237 | ||
|
|
f1702495ef | ||
|
|
2ab2ced589 | ||
|
|
c8f9e59fef | ||
|
|
9a25527a54 | ||
|
|
ad6d11671e | ||
|
|
3c93cd414d | ||
|
|
2c243626f6 | ||
|
|
ae05036bb3 | ||
|
|
3de22ffba3 | ||
|
|
e1bf07dece | ||
|
|
0ae63b0b07 | ||
|
|
d4f211b815 | ||
|
|
393b28d7bb | ||
|
|
47c807c0e3 | ||
|
|
b398f52a7f | ||
|
|
f2aeeeb1fe | ||
|
|
dcef406df8 | ||
|
|
01846c729a | ||
|
|
e6aaaff3f3 | ||
|
|
d4836bc769 | ||
|
|
b5655b0ddc | ||
|
|
d75d333e13 | ||
|
|
f5e16aa353 | ||
|
|
4e5eecac12 | ||
|
|
a111073181 | ||
|
|
a05f555ab8 | ||
|
|
df3db236e5 | ||
|
|
71b115c8fe | ||
|
|
3e1b01af2c | ||
|
|
1ab0685d04 | ||
|
|
5c44992d1c | ||
|
|
d448ffd539 | ||
|
|
9f895a79f2 | ||
|
|
6e1de81ff5 | ||
|
|
b493a63fb6 | ||
|
|
ae65f5a255 | ||
|
|
bb49b9e6dc | ||
|
|
2815e69407 | ||
|
|
b33efc0727 | ||
|
|
434c1ec4f8 | ||
|
|
c58d922324 | ||
|
|
331374dc7a | ||
|
|
c18eb23259 | ||
|
|
d0e2242043 | ||
|
|
684d36201f | ||
|
|
7b8ca60d32 | ||
|
|
576ff81856 | ||
|
|
73e68c9322 | ||
|
|
3cd758647d | ||
|
|
14bc8d6611 | ||
|
|
d552ce5eac | ||
|
|
f03adc840e | ||
|
|
31e73355c3 | ||
|
|
88368dfa1c | ||
|
|
7e6c8351c3 | ||
|
|
fb20e4fa90 | ||
|
|
94c5699a03 | ||
|
|
58626d1a22 | ||
|
|
631e72f99d | ||
|
|
3fffc18729 | ||
|
|
44c689f171 | ||
|
|
1f44339d6e | ||
|
|
001372bef2 | ||
|
|
8f72c143a1 | ||
|
|
3c75e1269d | ||
|
|
121120aee1 | ||
|
|
62ce55d8d7 | ||
|
|
183d43f5f8 | ||
|
|
79dfdb0ace | ||
|
|
bb4a0b7dd6 | ||
|
|
4cddc62be7 | ||
|
|
914b2fe984 | ||
|
|
308d4fa68e | ||
|
|
50f325c8fe | ||
|
|
57fd0ecb2e | ||
|
|
94af5cc43a | ||
|
|
3c19a0309e | ||
|
|
0eb9d94bf3 | ||
|
|
4d57a1bec6 | ||
|
|
59ab01a559 | ||
|
|
5946356f8e | ||
|
|
f0f7b18528 | ||
|
|
5311b0bc90 | ||
|
|
43580f5804 | ||
|
|
b0a68b8c79 | ||
|
|
f225d1f3fb | ||
|
|
15214a6e9a | ||
|
|
dfce6b62d4 | ||
|
|
0232ad7424 | ||
|
|
e6ff2e0c24 | ||
|
|
72980d2b03 | ||
|
|
6e3b51d81f | ||
|
|
6b1c628326 | ||
|
|
85f9b1012c | ||
|
|
7569de7004 | ||
|
|
5ccde7422c | ||
|
|
ad6bf9428e | ||
|
|
d176bab28e | ||
|
|
58d6f072e4 | ||
|
|
cc393818e4 | ||
|
|
e84098a011 | ||
|
|
f2db3941ea | ||
|
|
c2bfe12068 | ||
|
|
d9e3a0081e | ||
|
|
84d7a0facb | ||
|
|
9c66928854 | ||
|
|
4b494451db | ||
|
|
e9ea4ca169 | ||
|
|
c5ca054936 | ||
|
|
7d2c7c0f91 | ||
|
|
54b872af69 | ||
|
|
ae954bea17 | ||
|
|
ad59985a9b | ||
|
|
f8fdd43552 | ||
|
|
b269f7dbfd | ||
|
|
e688f136dd | ||
|
|
81276b341a | ||
|
|
f050617b47 | ||
|
|
e6c7958456 | ||
|
|
4802ceeccc | ||
|
|
2ea16d89ca | ||
|
|
e541201129 | ||
|
|
09aac3a4d3 | ||
|
|
dccb1ab3a4 | ||
|
|
8bf96d8738 | ||
|
|
069a94f205 | ||
|
|
332ed4ab76 | ||
|
|
030f2dbd38 | ||
|
|
ec3abdea14 | ||
|
|
4cf5508e04 | ||
|
|
d861f647d7 | ||
|
|
992f793695 | ||
|
|
69eb940764 | ||
|
|
c1f26d544a | ||
|
|
8255d7aed3 | ||
|
|
617ecb7ad3 | ||
|
|
dd8239c255 | ||
|
|
bdddc8c5d4 | ||
|
|
c1c619a109 | ||
|
|
f7eea35cb9 | ||
|
|
a2434e1607 | ||
|
|
b806ecd0ff | ||
|
|
8358b800e9 | ||
|
|
b25a092cf1 | ||
|
|
d812fdd0a3 | ||
|
|
2d3ee8a228 | ||
|
|
97a9ca65f1 | ||
|
|
f043fbbc51 | ||
|
|
a19ab3a8d4 | ||
|
|
bc0517a404 | ||
|
|
dc9c0d7e35 | ||
|
|
1a2207b3b2 | ||
|
|
84ab9764e3 | ||
|
|
bef68991f0 | ||
|
|
526c13d662 | ||
|
|
67ffb78c23 | ||
|
|
8f407c3a22 | ||
|
|
8980252e16 | ||
|
|
3baee80485 | ||
|
|
ca9599bb4d | ||
|
|
051db7907a | ||
|
|
bdba5ef2d3 | ||
|
|
838b9c0eda | ||
|
|
be584c54aa | ||
|
|
1b3640b42d | ||
|
|
be5dab5801 | ||
|
|
aae628cd18 | ||
|
|
e7bd08649f | ||
|
|
e1017a4c7e | ||
|
|
6f8ba9d295 | ||
|
|
ac94478ae7 | ||
|
|
515c6082e0 | ||
|
|
a753598113 | ||
|
|
bc6c4be4e4 | ||
|
|
98823a8f07 | ||
|
|
ecd8f1e54d | ||
|
|
b0caea8e20 | ||
|
|
e0477f8776 | ||
|
|
94e1e54b53 | ||
|
|
9ce72331b7 | ||
|
|
1c31f42dae | ||
|
|
0978a94358 | ||
|
|
e956a9c9f3 | ||
|
|
82026d11a4 | ||
|
|
6d233ac75c | ||
|
|
73c1934e7e | ||
|
|
bf1194ec75 | ||
|
|
22d2740725 | ||
|
|
02f26dc7a1 | ||
|
|
db14cbb0e0 | ||
|
|
a2b35cae92 | ||
|
|
aab0e4c82e | ||
|
|
6609708d14 | ||
|
|
ed198f062c | ||
|
|
4e2d656e9d | ||
|
|
2a4230868f | ||
|
|
cf778b1759 | ||
|
|
1b6daddd40 | ||
|
|
d7b137d144 | ||
|
|
b5d5855559 | ||
|
|
c0aa8dd808 | ||
|
|
4f2f5c02a0 | ||
|
|
7a6843fa4a | ||
|
|
f0ecf16c6b | ||
|
|
8998fa71b2 | ||
|
|
4e677a7558 | ||
|
|
07254b36d7 | ||
|
|
a51b8d256d | ||
|
|
75fcd9448e | ||
|
|
6b45999702 | ||
|
|
a4f52f4057 | ||
|
|
b8b3201372 | ||
|
|
41deadc1e2 | ||
|
|
82e8d06e54 | ||
|
|
51db5f2ff7 | ||
|
|
2ec9e515ff | ||
|
|
0579572148 | ||
|
|
83f4e6353c | ||
|
|
130953e519 | ||
|
|
7981093591 | ||
|
|
79112d6208 | ||
|
|
a3eb6263f4 | ||
|
|
d91bc9c73d | ||
|
|
c807665ab9 | ||
|
|
7b97c181b9 | ||
|
|
c42b0847d2 | ||
|
|
f182cb00c1 | ||
|
|
3181b5da23 | ||
|
|
983c119477 | ||
|
|
874ff1bba2 | ||
|
|
06d9d89f98 | ||
|
|
c713bad8c3 | ||
|
|
fe19491822 | ||
|
|
c80e8a1aca | ||
|
|
6845c094ab | ||
|
|
3c1126a7b4 | ||
|
|
326e0329b9 | ||
|
|
f9d0daa91c | ||
|
|
094200e687 | ||
|
|
bb4818630c | ||
|
|
7af3155b39 | ||
|
|
d988b4a811 | ||
|
|
18f5bdb7ca | ||
|
|
f41d493552 | ||
|
|
8cd2c9c63f | ||
|
|
427b335cac | ||
|
|
2049096c6f | ||
|
|
98d4fa6dc2 | ||
|
|
5306471aec | ||
|
|
aa8818b049 | ||
|
|
2d86f17363 | ||
|
|
3a36a01913 | ||
|
|
47d70a7f31 | ||
|
|
d5b5e6f4db | ||
|
|
e76d814439 | ||
|
|
b8b40e1f54 | ||
|
|
5e8c786406 | ||
|
|
5b8224f839 | ||
|
|
cd621eb081 | ||
|
|
b1113f0363 | ||
|
|
daf5933b79 | ||
|
|
47fd2e8604 | ||
|
|
05297fd72f | ||
|
|
f20cf19f94 | ||
|
|
061cd3da53 | ||
|
|
03202a8ab4 | ||
|
|
8a67be4afd | ||
|
|
4c1ab67f33 | ||
|
|
7e06a2105f | ||
|
|
4a0c8be742 | ||
|
|
0c030ee5e1 | ||
|
|
1134fac54c | ||
|
|
ac44d72530 | ||
|
|
12468bd8d2 | ||
|
|
a98eeb9d08 | ||
|
|
b558b132cd | ||
|
|
c102633566 | ||
|
|
cce002ad29 | ||
|
|
b1ed321438 | ||
|
|
c7c2478cb0 | ||
|
|
b70d99489b | ||
|
|
57f45eaca0 | ||
|
|
52fcc4d649 | ||
|
|
959ab9abc9 | ||
|
|
c3a91872fe | ||
|
|
4d074f0a1e | ||
|
|
c2225ab1da | ||
|
|
bb03b1a7d1 | ||
|
|
be7275443f | ||
|
|
dc1c973684 | ||
|
|
06d568408c | ||
|
|
2256e88fb4 | ||
|
|
9b962c8ff0 | ||
|
|
7656b75992 | ||
|
|
58a5449c2f | ||
|
|
702abe0665 | ||
|
|
2070b77fd4 | ||
|
|
fd00822015 | ||
|
|
17565c0ea2 | ||
|
|
22668ad2c2 | ||
|
|
0dcc3d700d | ||
|
|
2ab3d9b8a3 | ||
|
|
d578d8f6df | ||
|
|
fd61016a7e | ||
|
|
7f77d24f45 | ||
|
|
3ebdcb34d1 | ||
|
|
05240fecf7 | ||
|
|
d5860d7cfa | ||
|
|
7bb97dafda | ||
|
|
ca90babc4a | ||
|
|
4b813e4319 | ||
|
|
fa84c22662 | ||
|
|
2ae640b530 | ||
|
|
4f95a20c5e | ||
|
|
915d570b17 | ||
|
|
5979636ee5 | ||
|
|
5acfc6ae3c | ||
|
|
48341120fb | ||
|
|
ff45bb4c27 | ||
|
|
eabba46595 | ||
|
|
683bf99fe4 | ||
|
|
2a05c7c01d | ||
|
|
e386229b66 | ||
|
|
59b8ec5625 | ||
|
|
50ae5db357 | ||
|
|
3aa99a1250 | ||
|
|
00c8513129 | ||
|
|
9c1404fecc | ||
|
|
c11686ab6c | ||
|
|
2818984f6c | ||
|
|
db225ed04a | ||
|
|
0ec4c34a34 | ||
|
|
597ebc3267 | ||
|
|
e16f54eebe | ||
|
|
27480c15fa | ||
|
|
df0dea59f3 | ||
|
|
d7bc564512 | ||
|
|
e6a73ef36c | ||
|
|
1ba7c6aecd | ||
|
|
7c5e95222e | ||
|
|
38df3d2a15 | ||
|
|
3beaa817f3 | ||
|
|
3ebd7ba0ef | ||
|
|
1ba9c34650 | ||
|
|
2f9e726aea | ||
|
|
ade2b8cc80 | ||
|
|
9b144f4541 | ||
|
|
0c35436217 | ||
|
|
0e66b7b217 | ||
|
|
4ed9117799 | ||
|
|
270e778ac5 | ||
|
|
9528fd8bcc | ||
|
|
5a75314bb3 | ||
|
|
cceb8db74e | ||
|
|
8e7b08b96d | ||
|
|
194f11069f | ||
|
|
5ef0d667e0 | ||
|
|
8a1c1d7e53 | ||
|
|
1353c1d9fa | ||
|
|
70a8663605 | ||
|
|
f179eed971 | ||
|
|
d4c8708229 | ||
|
|
4008057803 | ||
|
|
997d8e0aa2 | ||
|
|
15bd5f0d30 | ||
|
|
ccaa20b006 | ||
|
|
c48698f680 | ||
|
|
4285c931e7 | ||
|
|
03c43cf3da | ||
|
|
8d14797416 | ||
|
|
e4837f65a3 | ||
|
|
ee46af66e5 | ||
|
|
2bec354317 | ||
|
|
727c7780b3 | ||
|
|
5410f9b532 | ||
|
|
4c09d8d774 | ||
|
|
aded6fb847 | ||
|
|
c2ad951336 | ||
|
|
7fb5dfa3e6 | ||
|
|
0f8c2fa454 | ||
|
|
dfc2aa9c6a | ||
|
|
8865092bb2 | ||
|
|
abc3353375 | ||
|
|
e7a8da4d0b | ||
|
|
f938051373 | ||
|
|
9db0d72232 | ||
|
|
9e6e789b73 | ||
|
|
bc0d7179cc | ||
|
|
aafa34439a | ||
|
|
168a3bc6fc | ||
|
|
2fb8b2eb18 | ||
|
|
da82705e86 | ||
|
|
a237a39d52 | ||
|
|
c042dc5542 | ||
|
|
1493d06b5a | ||
|
|
9a11201a9e | ||
|
|
c34094ea50 | ||
|
|
c531b2b5c7 |
No files matched your search
@@ -21,3 +21,6 @@ src/generated/**/*.json text eol=lf
|
||||
src/generated_test/**/.cache/cache text eol=lf
|
||||
src/generated_test/**/*.json text eol=lf
|
||||
|
||||
# We force eclipse to use unix line endings
|
||||
ide/eclipse/template/.settings/*.prefs text eol=lf
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# Configuration for support-requests - https://github.com/dessant/support-requests
|
||||
|
||||
# Label used to mark issues as support requests
|
||||
supportLabel: Forum
|
||||
# Comment to post on issues marked as support requests. Add a link
|
||||
# to a support page, or set to `false` to disable
|
||||
supportComment: >
|
||||
👋 We use the issue tracker exclusively for final bug reports and feature requests.
|
||||
However, this issue appears to be better suited for the
|
||||
[Forge Support Forums](http://www.minecraftforge.net/forum/forum/18-support-bug-reports/).
|
||||
Please create a new topic on the support forum with this issue, and the conversation can
|
||||
continue there.
|
||||
# Whether to close issues marked as support requests
|
||||
close: true
|
||||
# Whether to lock issues marked as support requests
|
||||
lock: true
|
||||
@@ -0,0 +1,16 @@
|
||||
name: 'Comment actions'
|
||||
|
||||
on: issue_comment
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
jobs:
|
||||
labels:
|
||||
uses: MinecraftForge/SharedActions/.github/workflows/comment_actions.yml@main
|
||||
secrets:
|
||||
GH_APP_KEY: ${{ secrets.GH_APP_KEY }}
|
||||
GH_APP_NAME: ${{ secrets.GH_APP_NAME }}
|
||||
@@ -0,0 +1,16 @@
|
||||
name: 'Issue actions'
|
||||
|
||||
on: issues
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
jobs:
|
||||
labels:
|
||||
uses: MinecraftForge/SharedActions/.github/workflows/issue_actions.yml@main
|
||||
secrets:
|
||||
GH_APP_KEY: ${{ secrets.GH_APP_KEY }}
|
||||
GH_APP_NAME: ${{ secrets.GH_APP_NAME }}
|
||||
@@ -0,0 +1,13 @@
|
||||
name: 'PR Review'
|
||||
|
||||
on: pull_request_review
|
||||
|
||||
jobs:
|
||||
upload_payload:
|
||||
name: Upload payload
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/upload-artifact@v3 # Payload artifact is consumed in Actionable
|
||||
with:
|
||||
name: payload
|
||||
path: ${{ github.event_path }}
|
||||
@@ -0,0 +1,13 @@
|
||||
name: 'PR Review Comment'
|
||||
|
||||
on: pull_request_review_comment
|
||||
|
||||
jobs:
|
||||
upload_payload:
|
||||
name: Upload payload
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/upload-artifact@v3 # Payload artifact is consumed in Actionable
|
||||
with:
|
||||
name: payload
|
||||
path: ${{ github.event_path }}
|
||||
@@ -0,0 +1,20 @@
|
||||
name: 'Pull Request Review Comment actions'
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [PR Review Comment]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
jobs:
|
||||
labels:
|
||||
uses: MinecraftForge/SharedActions/.github/workflows/pr_review_comment_actions.yml@main
|
||||
secrets:
|
||||
GH_APP_KEY: ${{ secrets.GH_APP_KEY }}
|
||||
GH_APP_NAME: ${{ secrets.GH_APP_NAME }}
|
||||
@@ -0,0 +1,20 @@
|
||||
name: 'Pull Request Review actions'
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [PR Review]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
jobs:
|
||||
labels:
|
||||
uses: MinecraftForge/SharedActions/.github/workflows/pr_review_actions.yml@main
|
||||
secrets:
|
||||
GH_APP_KEY: ${{ secrets.GH_APP_KEY }}
|
||||
GH_APP_NAME: ${{ secrets.GH_APP_NAME }}
|
||||
@@ -0,0 +1,18 @@
|
||||
name: 'Pull Request actions'
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [assigned, unassigned, labeled, unlabeled, opened, edited, closed, reopened, synchronize, converted_to_draft, ready_for_review, locked, unlocked, review_requested, review_request_removed, auto_merge_enabled, auto_merge_disabled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
jobs:
|
||||
labels:
|
||||
uses: MinecraftForge/SharedActions/.github/workflows/pr_actions.yml@main
|
||||
secrets:
|
||||
GH_APP_KEY: ${{ secrets.GH_APP_KEY }}
|
||||
GH_APP_NAME: ${{ secrets.GH_APP_NAME }}
|
||||
@@ -0,0 +1,16 @@
|
||||
name: 'Push actions'
|
||||
|
||||
on: push
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
jobs:
|
||||
labels:
|
||||
uses: MinecraftForge/SharedActions/.github/workflows/push_actions.yml@main
|
||||
secrets:
|
||||
GH_APP_KEY: ${{ secrets.GH_APP_KEY }}
|
||||
GH_APP_NAME: ${{ secrets.GH_APP_NAME }}
|
||||
+9
-13
@@ -13,6 +13,9 @@
|
||||
/classes
|
||||
*.iml
|
||||
|
||||
# We however WANT the ide folder as it has tempaltes/configs for developing Forge.
|
||||
!ide/**
|
||||
|
||||
#gradle
|
||||
/build
|
||||
/.gradle
|
||||
@@ -23,8 +26,6 @@
|
||||
/projects/mcp/
|
||||
/projects/clean/
|
||||
/projects/forge/
|
||||
/projects/fmlonly/
|
||||
#/patches/*
|
||||
|
||||
#occupational hazards
|
||||
/projects/**/build/
|
||||
@@ -32,16 +33,11 @@
|
||||
/projects/**/run/
|
||||
/projects/**/*.launch
|
||||
/repo/
|
||||
src/generated/resources/.cache/
|
||||
src/*/generated/**/.cache/
|
||||
|
||||
#Patch rejects
|
||||
/patches-/
|
||||
*.patch.rej
|
||||
/projects/*/rejects/
|
||||
|
||||
# FML changelog
|
||||
changelog.txt
|
||||
*.py
|
||||
*.7z
|
||||
/logs/
|
||||
# Generated by gradle every import
|
||||
**/src/main/resources/META-INF/MANIFEST.MF
|
||||
/fmlloader/src/main/resources/forge_version.json
|
||||
|
||||
/ForgeRoot.ipr
|
||||
/ForgeRoot.iws
|
||||
Vendored
+75
-18
@@ -1,5 +1,7 @@
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.*
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.projectFeatures.githubIssues
|
||||
import jetbrains.buildServer.configs.kotlin.*
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.gradle
|
||||
|
||||
/*
|
||||
The settings script is an entry point for defining a TeamCity
|
||||
@@ -30,11 +32,13 @@ project {
|
||||
buildType(Build)
|
||||
buildType(BuildSecondaryBranches)
|
||||
buildType(PullRequests)
|
||||
buildType(PullRequestChecks)
|
||||
buildType(PullRequestCompatibility)
|
||||
|
||||
params {
|
||||
text("docker_jdk_version", "17", label = "Gradle version", description = "The version of the JDK to use during execution of tasks in a JDK.", display = ParameterDisplay.HIDDEN, allowEmpty = false)
|
||||
text("docker_gradle_version", "7.4", label = "Gradle version", description = "The version of Gradle to use during execution of Gradle tasks.", display = ParameterDisplay.HIDDEN, allowEmpty = false)
|
||||
text("git_main_branch", "1.18.x", label = "Git Main Branch", description = "The git main or default branch to use in VCS operations.", display = ParameterDisplay.HIDDEN, allowEmpty = false)
|
||||
text("docker_jdk_version", "21", label = "Gradle version", description = "The version of the JDK to use during execution of tasks in a JDK.", display = ParameterDisplay.HIDDEN, allowEmpty = false)
|
||||
text("docker_gradle_version", "8.7", label = "Gradle version", description = "The version of Gradle to use during execution of Gradle tasks.", display = ParameterDisplay.HIDDEN, allowEmpty = false)
|
||||
text("git_main_branch", "1.21.x", label = "Git Main Branch", description = "The git main or default branch to use in VCS operations.", display = ParameterDisplay.HIDDEN, allowEmpty = false)
|
||||
text("git_branch_spec", """
|
||||
+:refs/heads/(main*)
|
||||
+:refs/heads/(master*)
|
||||
@@ -43,8 +47,6 @@ project {
|
||||
""".trimIndent(), label = "The branch specification of the repository", description = "By default all main branches are build by the configuration. Modify this value to adapt the branches build.", display = ParameterDisplay.HIDDEN, allowEmpty = true)
|
||||
text("github_repository_name", "MinecraftForge", label = "The github repository name. Used to connect to it in VCS Roots.", description = "This is the repository slug on github. So for example `MinecraftForge` or `MinecraftForge`. It is interpolated into the global VCS Roots.", display = ParameterDisplay.HIDDEN, allowEmpty = false)
|
||||
text("env.PUBLISHED_JAVA_ARTIFACT_ID", "forge", label = "Published artifact id", description = "The maven coordinate artifact id that has been published by this build. Can not be empty.", allowEmpty = false)
|
||||
text("env.PUBLISHED_JAVA_FML_ARTIFACT_ID", "fmlonly", label = "Published fmlonly artifact id", description = "The maven coordinate artifact id for fml only that has been published by this build. Can not be empty.", allowEmpty = false)
|
||||
text("env.PUBLISHED_JAVA_FML_ARTIFACT_VERSION", "0.0.0-SNAPSHOT", label = "Published fmlonly artifact version", description = "The version for fml only that has been published by this build. Can not be empty.", allowEmpty = false)
|
||||
text("env.PUBLISHED_JAVA_GROUP", "net.minecraftforge", label = "Published group", description = "The maven coordinate group that has been published by this build. Can not be empty.", allowEmpty = false)
|
||||
//These are references and not actually keys
|
||||
password("env.CROWDIN_KEY", "credentialsJSON:a3102dbe-805d-4177-9f54-3d2c2eb08fd5", display = ParameterDisplay.HIDDEN)
|
||||
@@ -72,19 +74,6 @@ object Build : BuildType({
|
||||
id("MinecraftForge_MinecraftForge__Build")
|
||||
name = "Build"
|
||||
description = "Builds and Publishes the main branches of the project."
|
||||
|
||||
features {
|
||||
feature {
|
||||
id = "trigger_fml_only_files_generator"
|
||||
type = "triggerBuildFeature"
|
||||
param("triggers", "MinecraftForge_FilesGenerator_GeneratePages")
|
||||
param("parameters", """
|
||||
env.PUBLISHED_JAVA_FML_ARTIFACT_ID~env.PUBLISHED_JAVA_ARTIFACT_ID
|
||||
env.PUBLISHED_JAVA_FML_ARTIFACT_VERSION~env.PUBLISHED_JAVA_ARTIFACT_VERSION
|
||||
env.PUBLISHED_JAVA_GROUP
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
object BuildSecondaryBranches : BuildType({
|
||||
@@ -134,3 +123,71 @@ object PullRequests : BuildType({
|
||||
""".trimIndent()
|
||||
}
|
||||
})
|
||||
|
||||
object PullRequestChecks : BuildType({
|
||||
templates(AbsoluteId("MinecraftForge_BuildPullRequests"), AbsoluteId("MinecraftForge_SetupGradleUtilsCiEnvironmen"), AbsoluteId("MinecraftForge_BuildWithDiscordNotifications"), AbsoluteId("MinecraftForge_SetupProjectUsingGradle"))
|
||||
id("MinecraftForge_MinecraftForge__PullRequestChecks")
|
||||
name = "Pull Requests (Checks)"
|
||||
description = "Checks pull requests for the project"
|
||||
|
||||
steps {
|
||||
gradle {
|
||||
name = "Check"
|
||||
id = "RUNNER_10_Check"
|
||||
|
||||
tasks = "checkAll"
|
||||
gradleParams = "--continue %gradle_custom_args%"
|
||||
enableStacktrace = true
|
||||
dockerImage = "%docker_gradle_image%"
|
||||
dockerRunParameters = """
|
||||
-v "/opt/cache/agent/gradle:/home/gradle/.gradle"
|
||||
-v "/opt/cache/shared/gradle:/home/gradle/rocache:ro"
|
||||
--network=host
|
||||
-u 1000:1000
|
||||
%docker_additional_args%
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
vcs {
|
||||
branchFilter = """
|
||||
+:*
|
||||
-:1.*
|
||||
-:<default>
|
||||
""".trimIndent()
|
||||
}
|
||||
})
|
||||
|
||||
object PullRequestCompatibility : BuildType({
|
||||
templates(AbsoluteId("MinecraftForge_BuildPullRequests"), AbsoluteId("MinecraftForge_SetupGradleUtilsCiEnvironmen"), AbsoluteId("MinecraftForge_BuildWithDiscordNotifications"), AbsoluteId("MinecraftForge_SetupProjectUsingGradle"))
|
||||
id("MinecraftForge_MinecraftForge__PullRequestCompatibility")
|
||||
name = "Pull Requests (Compatibility)"
|
||||
description = "Validates binary compatibility for pull requests made to the project"
|
||||
|
||||
steps {
|
||||
gradle {
|
||||
name = "Validate"
|
||||
id = "RUNNER_10_Compatibility"
|
||||
|
||||
tasks = "checkJarCompatibility"
|
||||
gradleParams = "--continue %gradle_custom_args%"
|
||||
enableStacktrace = true
|
||||
dockerImage = "%docker_gradle_image%"
|
||||
dockerRunParameters = """
|
||||
-v "/opt/cache/agent/gradle:/home/gradle/.gradle"
|
||||
-v "/opt/cache/shared/gradle:/home/gradle/rocache:ro"
|
||||
--network=host
|
||||
-u 1000:1000
|
||||
%docker_additional_args%
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
vcs {
|
||||
branchFilter = """
|
||||
+:*
|
||||
-:1.*
|
||||
-:<default>
|
||||
""".trimIndent()
|
||||
}
|
||||
})
|
||||
+4
-4
@@ -3,7 +3,7 @@ Minecraft Forge: Credits/Thank You
|
||||
Forge is a set of tools and modifications to the Minecraft base game code to assist
|
||||
mod developers in creating new and exciting content. It has been in development for
|
||||
several years now, but I would like to take this time thank a few people who have
|
||||
helped it along it's way.
|
||||
helped it along its way.
|
||||
|
||||
First, the people who originally created the Forge projects way back in Minecraft
|
||||
alpha. Eloraam of RedPower, and SpaceToad of Buildcraft, without their acceptiance
|
||||
@@ -11,7 +11,7 @@ of me taking over the project, who knows what Minecraft modding would be today.
|
||||
|
||||
Secondly, someone who has worked with me, and developed some of the core features
|
||||
that allow modding to be as functional, and as simple as it is, cpw. For developing
|
||||
FML, which stabelized the client and server modding ecosystem. As well as the base
|
||||
FML, which stabilized the client and server modding ecosystem. As well as the base
|
||||
loading system that allows us to modify Minecraft's code as elegently as possible.
|
||||
|
||||
Mezz, who has stepped up as the issue and pull request manager. Helping to keep me
|
||||
@@ -45,11 +45,11 @@ and minecraft clients.
|
||||
The code is authored by cpw.
|
||||
|
||||
It began by partially implementing an API defined by the client side ModLoader, authored by Risugami.
|
||||
http://www.minecraftforum.net/topic/75440-
|
||||
https://www.minecraftforum.net/topic/75440-
|
||||
This support has been dropped as of Minecraft release 1.7, as Risugami no longer maintains ModLoader.
|
||||
|
||||
It also contains suggestions and hints and generous helpings of code from LexManos, author of MinecraftForge.
|
||||
http://www.minecraftforge.net/
|
||||
https://minecraftforge.net/
|
||||
|
||||
Additionally, it contains an implementation of topological sort based on that
|
||||
published at http://keithschwarz.com/interesting/code/?dir=topological-sort
|
||||
|
||||
Vendored
-125
@@ -1,125 +0,0 @@
|
||||
@Library('forge-shared-library')_
|
||||
|
||||
pipeline {
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
}
|
||||
agent {
|
||||
docker {
|
||||
image 'gradle:7-jdk16'
|
||||
//args '-v forgegc:/home/gradle/.gradle_cache/'
|
||||
}
|
||||
}
|
||||
environment {
|
||||
GRADLE_ARGS = '--no-daemon --console=plain' // No daemon for now as FG3 kinda derps. //'-Dorg.gradle.daemon.idletimeout=5000'
|
||||
DISCORD_WEBHOOK = credentials('forge-discord-jenkins-webhook')
|
||||
DISCORD_PREFIX = "Job: Forge Branch: ${BRANCH_NAME} Build: #${BUILD_NUMBER}"
|
||||
JENKINS_HEAD = 'https://wiki.jenkins-ci.org/download/attachments/2916393/headshot.png'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('notify_start') {
|
||||
when {
|
||||
not {
|
||||
changeRequest()
|
||||
}
|
||||
}
|
||||
steps {
|
||||
discordSend(
|
||||
title: "${DISCORD_PREFIX} Started",
|
||||
successful: true,
|
||||
result: 'ABORTED', //White border
|
||||
thumbnail: JENKINS_HEAD,
|
||||
webhookURL: DISCORD_WEBHOOK
|
||||
)
|
||||
}
|
||||
}
|
||||
/*
|
||||
stage('pull_cache') {
|
||||
steps {
|
||||
sh 'cp -r /home/gradle/.gradle_cache/jdks/ /home/gradle/.gradle/jdks/'
|
||||
sh 'cp -r /home/gradle/.gradle_cache/wrapper/ /home/gradle/.gradle/wrapper/'
|
||||
sh 'cp -r /home/gradle/.gradle_cache/caches/ /home/gradle/.gradle/caches/'
|
||||
}
|
||||
}
|
||||
*/
|
||||
stage('setup') {
|
||||
steps {
|
||||
withGradle {
|
||||
sh './gradlew ${GRADLE_ARGS} --refresh-dependencies --continue setup'
|
||||
}
|
||||
script {
|
||||
env.MYVERSION = sh(returnStdout: true, script: './gradlew :forge:properties -q | grep "^version:" | cut -d" " -f2').trim()
|
||||
env.FMLONLY_VERSION = sh(returnStdout: true, script: './gradlew :fmlonly:properties -q | grep "^version:" | cut -d" " -f2').trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('changelog') {
|
||||
when {
|
||||
not {
|
||||
changeRequest()
|
||||
}
|
||||
}
|
||||
steps {
|
||||
writeChangelog(currentBuild, 'build/changelog.txt')
|
||||
}
|
||||
}
|
||||
stage('publish') {
|
||||
when {
|
||||
not {
|
||||
changeRequest()
|
||||
}
|
||||
}
|
||||
environment {
|
||||
CROWDIN = credentials('forge-crowdin')
|
||||
KEYSTORE = credentials('forge-jenkins-keystore-old')
|
||||
KEYSTORE_KEYPASS = credentials('forge-jenkins-keystore-old-keypass')
|
||||
KEYSTORE_STOREPASS = credentials('forge-jenkins-keystore-old-keypass')
|
||||
}
|
||||
steps {
|
||||
withCredentials([usernamePassword(credentialsId: 'maven-forge-user', usernameVariable: 'MAVEN_USER', passwordVariable: 'MAVEN_PASSWORD')]) {
|
||||
withGradle {
|
||||
sh './gradlew ${GRADLE_ARGS} publish -PkeystoreKeyPass=${KEYSTORE_KEYPASS} -PkeystoreStorePass=${KEYSTORE_STOREPASS} -Pkeystore=${KEYSTORE} -PcrowdinKey=${CROWDIN}'
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
success {
|
||||
build job: 'filegenerator', parameters: [string(name: 'COMMAND', value: "promote net.minecraftforge:forge ${env.MYVERSION} latest")], propagate: false, wait: false
|
||||
build job: 'filegenerator', parameters: [string(name: 'COMMAND', value: "promote net.minecraftforge:fmlonly ${env.FMLONLY_VERSION} latest")], propagate: false, wait: false
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('test_publish_pr') { //Publish to local repo to test full process, but don't include credentials so it can't sign/publish to maven
|
||||
when {
|
||||
changeRequest()
|
||||
}
|
||||
environment {
|
||||
CROWDIN = credentials('forge-crowdin')
|
||||
}
|
||||
steps {
|
||||
withGradle {
|
||||
sh './gradlew ${GRADLE_ARGS} publish -PcrowdinKey=${CROWDIN}'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
script {
|
||||
archiveArtifacts artifacts: 'projects/forge/build/libs/**/*.*', fingerprint: true, onlyIfSuccessful: true, allowEmptyArchive: true
|
||||
|
||||
if (env.CHANGE_ID == null) { // This is unset for non-PRs
|
||||
discordSend(
|
||||
title: "${DISCORD_PREFIX} Finished ${currentBuild.currentResult}",
|
||||
description: '```\n' + getChanges(currentBuild) + '\n```',
|
||||
successful: currentBuild.resultIsBetterOrEqualTo("SUCCESS"),
|
||||
result: currentBuild.currentResult,
|
||||
thumbnail: JENKINS_HEAD,
|
||||
webhookURL: DISCORD_WEBHOOK
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,9 @@ This software contains a partial repackaging of javaxdelta, a BSD licensed progr
|
||||
binary differences and applying them, sourced from the subversion at http://sourceforge.net/projects/javaxdelta/
|
||||
authored by genman, heikok, pivot.
|
||||
The only changes are to replace some Trove collection types with standard Java collections, and repackaged.
|
||||
|
||||
This software includes the Monocraft font from https://github.com/IdreesInc/Monocraft/ for use in the early loading
|
||||
display.
|
||||
=========================================================================
|
||||
|
||||
|
||||
|
||||
+19
-1528
File diff suppressed because it is too large.
Load diff
@@ -1,9 +1,13 @@
|
||||
repositories {
|
||||
maven { url = 'https://maven.minecraftforge.net/' }
|
||||
mavenCentral()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.ow2.asm:asm:9.1'
|
||||
implementation 'org.ow2.asm:asm-tree:9.1'
|
||||
implementation 'net.minecraftforge:srgutils:0.4.+'
|
||||
}
|
||||
implementation 'org.ow2.asm:asm:9.7'
|
||||
implementation 'org.ow2.asm:asm-tree:9.7'
|
||||
implementation 'net.minecraftforge:srgutils:0.5.10'
|
||||
implementation 'commons-io:commons-io:2.13.0'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation 'org.eclipse.jgit:org.eclipse.jgit:6.7.0.202309050840-r'
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import org.gradle.api.tasks.*
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.ConfigurableFileCollection
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.tasks.bundling.AbstractArchiveTask
|
||||
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
abstract class BundleList extends DefaultTask {
|
||||
@InputFiles abstract ConfigurableFileCollection getConfig()
|
||||
@InputFiles abstract ConfigurableFileCollection getConfigExtra()
|
||||
@InputFile abstract RegularFileProperty getServerBundle()
|
||||
@OutputFile abstract RegularFileProperty getOutput()
|
||||
|
||||
BundleList() {
|
||||
config.setFrom(project.configurations.installer)
|
||||
configExtra.setFrom(project.configurations.installerextra)
|
||||
output.convention(project.layout.buildDirectory.file("$name/output.list"))
|
||||
configure {
|
||||
dependsOn(project.tasks.universalJar)
|
||||
inputs.file(project.tasks.universalJar.archiveFile)
|
||||
}
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
void run() {
|
||||
def entries = [:] as TreeMap
|
||||
def resolved = project.configurations.installer.resolvedConfiguration.resolvedArtifacts
|
||||
for (def dep : resolved) {
|
||||
def info = Util.getMavenInfoFromDep(dep)
|
||||
//println("$dep.file.sha1\t$info.name\t$info.path")
|
||||
entries.put("$info.art.group:$info.art.name", "$dep.file.sha256\t$info.name\t$info.path")
|
||||
}
|
||||
|
||||
resolved = project.configurations.installerextra.resolvedConfiguration.resolvedArtifacts
|
||||
for (def dep : resolved) {
|
||||
def info = Util.getMavenInfoFromDep(dep)
|
||||
//println("$dep.file.sha1\t$info.name\t$info.path")
|
||||
entries.put("$info.art.group:$info.art.name", "$dep.file.sha256\t$info.name\t$info.path")
|
||||
}
|
||||
|
||||
var packed = (AbstractArchiveTask) project.tasks.universalJar
|
||||
var info = Util.getMavenInfoFromTask(packed)
|
||||
def file = packed.archiveFile.get().asFile
|
||||
entries.put("$info.art.group:$info.art.name:$info.art.classifier", "$file.sha256\t$info.name\t$info.path")
|
||||
|
||||
var classifier = 'server'
|
||||
var genned = project.tasks.applyServerBinPatches
|
||||
info = Util.getMavenInfoFromTask(genned, classifier)
|
||||
file = genned.output.get().asFile
|
||||
entries.put("$info.art.group:$info.art.name:$info.art.classifier", "$file.sha256\t$info.name\t$info.path")
|
||||
|
||||
try (def zip = new ZipFile(serverBundle.get().asFile)) {
|
||||
def entry = zip.getEntry('META-INF/libraries.list')
|
||||
def data = zip.getInputStream(entry).text.split('\n')
|
||||
for (def line : data) {
|
||||
def (sha, artifact, path) = line.split('\t')
|
||||
def (group, name, other) = artifact.split(':', 3)
|
||||
//println("Group: $group Name: $name")
|
||||
def key = "$group:$name"
|
||||
if (!entries.containsKey(key))
|
||||
entries.put(key, line)
|
||||
}
|
||||
}
|
||||
|
||||
output.get().asFile.text = entries.values().join('\n')
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,22 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import groovy.json.JsonBuilder
|
||||
import groovy.transform.CompileStatic
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.OutputFile
|
||||
import org.gradle.api.tasks.Internal
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.objectweb.asm.ClassReader
|
||||
import org.objectweb.asm.Opcodes
|
||||
import org.objectweb.asm.tree.ClassNode
|
||||
import org.objectweb.asm.tree.FieldNode
|
||||
import org.objectweb.asm.tree.MethodNode
|
||||
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
@CompileStatic
|
||||
abstract class BytecodeFinder extends DefaultTask {
|
||||
@InputFile abstract RegularFileProperty getJar()
|
||||
@OutputFile abstract RegularFileProperty getOutput()
|
||||
// It should be fine to mark the output as internal as we want to control when we run it anyways.
|
||||
// This also shuts Gradle 8 up about implicit task dependencies.
|
||||
@Internal abstract RegularFileProperty getOutput()
|
||||
|
||||
BytecodeFinder() {
|
||||
output.convention(project.layout.buildDirectory.dir(name).map { it.file("output.json") })
|
||||
@@ -27,24 +26,13 @@ abstract class BytecodeFinder extends DefaultTask {
|
||||
protected void exec() {
|
||||
Util.init()
|
||||
|
||||
def outputFile = output.get().asFile
|
||||
var outputFile = output.get().asFile
|
||||
if (outputFile.exists())
|
||||
outputFile.delete()
|
||||
|
||||
pre()
|
||||
|
||||
jar.get().asFile.withInputStream { i ->
|
||||
new ZipInputStream(i).withCloseable { zin ->
|
||||
ZipEntry zein
|
||||
while ((zein = zin.nextEntry) != null) {
|
||||
if (zein.name.endsWith('.class')) {
|
||||
def node = new ClassNode(Opcodes.ASM9)
|
||||
new ClassReader(zin).accept(node, 0)
|
||||
process(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Util.processClassNodes(jar.get().asFile, this.&process)
|
||||
|
||||
post()
|
||||
outputFile.text = new JsonBuilder(getData()).toPrettyString()
|
||||
@@ -52,8 +40,8 @@ abstract class BytecodeFinder extends DefaultTask {
|
||||
|
||||
|
||||
protected process(ClassNode node) {
|
||||
if (node.fields != null) node.fields.each { process(node, it) }
|
||||
if (node.methods != null) node.methods.each { process(node, it) }
|
||||
if (node.fields !== null) node.fields.each { process(node, it) }
|
||||
if (node.methods !== null) node.methods.each { process(node, it) }
|
||||
}
|
||||
|
||||
protected pre() {}
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.Internal
|
||||
import org.objectweb.asm.tree.ClassNode
|
||||
import org.objectweb.asm.tree.MethodNode
|
||||
|
||||
@CompileStatic
|
||||
abstract class BytecodePredicateFinder extends BytecodeFinder {
|
||||
|
||||
@Internal
|
||||
abstract Property<Closure<Boolean>> getPredicate()
|
||||
|
||||
private final Set<String> matches = new HashSet<>()
|
||||
|
||||
@Override
|
||||
protected process(ClassNode parent, MethodNode node) {
|
||||
for (final current : node.instructions) {
|
||||
if (predicate.get().call(parent, node, current)) {
|
||||
matches.add(parent.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Internal
|
||||
@Override
|
||||
protected Object getData() {
|
||||
return matches.toSorted(Comparator.naturalOrder()) ?: {throw new RuntimeException('Failed to find any targets, please ensure that method names and descriptors are correct.')}()
|
||||
}
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import net.minecraftforge.srgutils.IMappingFile
|
||||
import org.gradle.api.file.ConfigurableFileCollection
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.InputFiles
|
||||
import org.gradle.api.tasks.Optional
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.objectweb.asm.Opcodes
|
||||
|
||||
abstract class CheckATs extends DefaultTask {
|
||||
@InputFile abstract RegularFileProperty getInheritance()
|
||||
@InputFiles abstract ConfigurableFileCollection getAts()
|
||||
@InputFile @Optional abstract RegularFileProperty getMappings()
|
||||
|
||||
@TaskAction
|
||||
protected void exec() {
|
||||
def mappings = mappings.map { IMappingFile.load(it.asFile) }.getOrNull()
|
||||
Util.init()
|
||||
def parse = { line ->
|
||||
def idx = line.indexOf('#')
|
||||
def comment = idx == -1 ? null : line.substring(idx)
|
||||
if (idx != -1) line = line.substring(0, idx - 1)
|
||||
def (modifier, cls, desc) = (line.trim() + ' ').split(' ', -1)
|
||||
def key = cls + (desc.isEmpty() ? '' : ' ' + desc)
|
||||
return [modifier, cls, desc, comment, key]
|
||||
}
|
||||
def accessLevel = { access ->
|
||||
if ((access & Opcodes.ACC_PUBLIC) != 0) return 3
|
||||
if ((access & Opcodes.ACC_PROTECTED) != 0) return 2
|
||||
if ((access & Opcodes.ACC_PRIVATE) != 0) return 0
|
||||
return 1
|
||||
}
|
||||
def accessStr = { access ->
|
||||
if (access.endsWith('-f') || access.endsWith('+f'))
|
||||
return 4
|
||||
switch (access.toLowerCase()) {
|
||||
case 'public': return 3
|
||||
case 'protected': return 2
|
||||
case 'default': return 1
|
||||
case 'private': return 0
|
||||
default: return -1
|
||||
}
|
||||
}
|
||||
def json = inheritance.get().asFile.json()
|
||||
|
||||
ats.each { f ->
|
||||
TreeMap lines = [:]
|
||||
def group = null
|
||||
for (def line : f.readLines()) {
|
||||
if (line.isEmpty()) continue
|
||||
if (line.startsWith('#group ')) {
|
||||
def (modifier, cls, desc, comment, key) = parse.call(line.substring(7))
|
||||
|
||||
if (desc != '*' && desc != '*()' && desc != '<init>')
|
||||
throw new IllegalStateException('Invalid group: ' + line)
|
||||
|
||||
group = [modifier: modifier, cls: cls, desc: desc, comment: comment,
|
||||
existing: [] as Set,
|
||||
children: [] as TreeSet,
|
||||
group: true
|
||||
]
|
||||
if (lines.containsKey(key))
|
||||
throw new IllegalStateException('Duplicate group: ' + line)
|
||||
|
||||
lines[key] = group
|
||||
} else if (group != null) {
|
||||
if (line.startsWith('#endgroup')) {
|
||||
group = null
|
||||
} else {
|
||||
def (modifier, cls, desc, comment, key) = parse.call(line)
|
||||
group['existing'].add(key)
|
||||
}
|
||||
} else if (line.startsWith('#endgroup')) {
|
||||
throw new IllegalStateException('Invalid group ending: ' + line)
|
||||
} else if (line.startsWith('#')) {
|
||||
//Nom
|
||||
} else {
|
||||
def (modifier, cls, desc, comment, key) = parse.call(line)
|
||||
if (lines.containsKey(key)) {
|
||||
println('Duplicate: ' + line)
|
||||
continue
|
||||
}
|
||||
lines[key] = [modifier: modifier, cls: cls, desc: desc, comment: comment, group: false]
|
||||
}
|
||||
}
|
||||
|
||||
// Process Groups, this will remove any entries outside the group that is covered by the group
|
||||
for (def key : new ArrayList<>(lines.keySet())) {
|
||||
def entry = lines.get(key)
|
||||
if (entry != null && entry['group']) {
|
||||
def cls = entry['cls']
|
||||
def jcls = json.get(cls.replaceAll('\\.', '/'))
|
||||
if (jcls == null) {
|
||||
lines.remove(key)
|
||||
println('Invalid Group: ' + key)
|
||||
} else if ('*' == entry['desc']) {
|
||||
if (!jcls.containsKey('fields')) {
|
||||
lines.remove(key)
|
||||
println('Invalid Group, Class has no fields: ' + key)
|
||||
} else {
|
||||
jcls['fields'].each { field, value ->
|
||||
def fkey = cls + ' ' + field
|
||||
if (accessLevel.call(value['access']) < accessStr.call(entry['modifier'])) {
|
||||
if (lines.containsKey(fkey)) {
|
||||
lines.remove(fkey)
|
||||
} else if (!entry['existing'].contains(fkey)) {
|
||||
println('Added: ' + fkey)
|
||||
}
|
||||
entry['children'].add(fkey)
|
||||
} else if (lines.containsKey(fkey)) {
|
||||
lines.remove(fkey)
|
||||
println('Removed: ' + fkey)
|
||||
}
|
||||
}
|
||||
entry['existing'].stream().findAll{ !entry['children'].contains(it) }.each{ println('Removed: ' + it) }
|
||||
}
|
||||
} else if ('*()' == entry['desc']) {
|
||||
if (!jcls.containsKey('methods')) {
|
||||
lines.remove(key)
|
||||
println('Invalid Group, Class has no methods: ' + key)
|
||||
} else {
|
||||
jcls['methods'].each{ mtd, value ->
|
||||
if (mtd.startsWith('<clinit>') || mtd.startsWith('lambda$'))
|
||||
return
|
||||
key = cls + ' ' + mtd.replace(' ', '')
|
||||
if (accessLevel.call(value['access']) < accessStr.call(entry['modifier'])) {
|
||||
if (lines.containsKey(key)) {
|
||||
lines.remove(key)
|
||||
} else if (!entry['existing'].contains(key)) {
|
||||
println('Added: ' + key)
|
||||
}
|
||||
entry['children'].add(key)
|
||||
} else if (lines.containsKey(key)) {
|
||||
lines.remove(key)
|
||||
println('Removed: ' + key)
|
||||
}
|
||||
}
|
||||
entry['existing'].stream().findAll{ !entry['children'].contains(it) }.each{ println('Removed: ' + it) }
|
||||
}
|
||||
} else if ('<init>' == entry['desc']) { //Make all public non-abstract subclasses
|
||||
json.each{ tcls,value ->
|
||||
if (!value.containsKey('methods') || ((value['access'] & Opcodes.ACC_ABSTRACT) != 0))
|
||||
return
|
||||
def parents = [] as Set
|
||||
def parent = tcls
|
||||
while (parent != null && json.containsKey(parent)) {
|
||||
parents.add(parent)
|
||||
def p = json[parent]
|
||||
parent = p == null ? null : p['superName']
|
||||
}
|
||||
if (parents.contains(cls.replaceAll('\\.', '/'))) {
|
||||
value['methods'].each{ mtd, v ->
|
||||
if (mtd.startsWith('<init>')) {
|
||||
def child = tcls.replaceAll('/', '\\.') + ' ' + mtd.replace(' ', '')
|
||||
if (accessLevel.call(v['access']) < 3) {
|
||||
if (lines.containsKey(child)) {
|
||||
lines.remove(child)
|
||||
} else if (!entry['existing'].contains(child)) {
|
||||
println('Added: ' + child)
|
||||
}
|
||||
entry['children'].add(child)
|
||||
} else if (lines.containsKey(child)) {
|
||||
lines.remove(child)
|
||||
println('Removed: ' + child)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
entry['existing'].stream().findAll{ !entry['children'].contains(it) }.each{ println('Removed: ' + it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process normal lines, remove invalid and remove narrowing
|
||||
for (def key : new ArrayList<>(lines.keySet())) {
|
||||
def entry = lines.get(key)
|
||||
if (entry != null && !entry['group']) {
|
||||
def cls = entry['cls']
|
||||
def jcls = json.get(cls.replaceAll('\\.', '/'))
|
||||
if (jcls == null) {
|
||||
lines.remove(key)
|
||||
println('Invalid: ' + key)
|
||||
} else if (entry['desc'] == '') {
|
||||
if (accessLevel.call(jcls['access']) > accessStr.call(entry['modifier']) && (entry.comment == null || !entry.comment.startsWith('#force '))) {
|
||||
lines.remove(key)
|
||||
println('Invalid Narrowing: ' + key)
|
||||
}
|
||||
} else if (!entry['desc'].contains('(')) {
|
||||
if (!jcls.containsKey('fields') || !jcls['fields'].containsKey(entry['desc'])) {
|
||||
lines.remove(key)
|
||||
println('Invalid: ' + key)
|
||||
} else {
|
||||
def value = jcls['fields'][entry['desc']]
|
||||
if (accessLevel.call(value['access']) > accessStr.call(entry['modifier']) && (entry.comment == null || !entry.comment.startsWith('#force '))) {
|
||||
lines.remove(key)
|
||||
println('Invalid Narrowing: ' + key)
|
||||
println(entry.comment)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
def jdesc = entry['desc'].replace('(', ' (')
|
||||
if (!jcls.containsKey('methods') || !jcls['methods'].containsKey(jdesc)) {
|
||||
lines.remove(key)
|
||||
println('Invalid: ' + key)
|
||||
} else {
|
||||
def value = jcls['methods'][jdesc]
|
||||
if (accessLevel.call(value['access']) > accessStr.call(entry['modifier']) && (entry.comment == null || !entry.comment.startsWith('#force '))) {
|
||||
lines.remove(key)
|
||||
println('Invalid Narrowing: ' + key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def data = []
|
||||
def remapComment = { entry ->
|
||||
if (!mappings || !entry || !entry.desc) return null
|
||||
def comment = entry.comment?.substring(1)?.trim()
|
||||
def jsonCls = json.get(entry.cls.replaceAll('\\.', '/'))
|
||||
def mappingsClass = mappings?.getClass(jsonCls.name)
|
||||
if (!mappingsClass) return entry.comment
|
||||
def idx = entry.desc.indexOf('(')
|
||||
def mappedName = idx == -1
|
||||
? mappingsClass.remapField(entry.desc)
|
||||
: mappingsClass.remapMethod(entry.desc.substring(0, idx), entry.desc.substring(idx))
|
||||
if (!mappedName) return entry.comment
|
||||
if (mappedName == '<init>')
|
||||
mappedName = 'constructor'
|
||||
|
||||
if (comment?.startsWith(mappedName))
|
||||
return '# ' + comment
|
||||
if (comment && comment.indexOf(' ') != -1) {
|
||||
def split = comment.split(" - ").toList()
|
||||
if (split[0].indexOf(' ') != -1)
|
||||
// The first string is more than one word, so append before it
|
||||
return "# ${mappedName} - ${comment}"
|
||||
split.remove(0)
|
||||
return "# ${mappedName} - ${String.join(' - ', split)}"
|
||||
}
|
||||
return '# ' + mappedName
|
||||
}
|
||||
lines.each { key,value ->
|
||||
if (!value.group) {
|
||||
def comment = remapComment.call(value)
|
||||
data.add(value.modifier + ' ' + key + (comment ? ' ' + comment : ''))
|
||||
} else {
|
||||
data.add('#group ' + value.modifier + ' ' + key + (value.comment == null ? '' : ' ' + value.comment))
|
||||
value.children.each {
|
||||
def line = value.modifier + ' ' + it
|
||||
def (modifier, cls, desc, comment) = parse.call(line)
|
||||
comment = remapComment.call([modifier: modifier, cls: cls, desc: desc, comment: comment])
|
||||
data.add(line + (comment ? ' ' + comment : ''))
|
||||
}
|
||||
data.add('#endgroup')
|
||||
}
|
||||
}
|
||||
f.text = data.join('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import org.gradle.api.file.ConfigurableFileCollection
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.InputFiles
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
import org.objectweb.asm.ClassReader
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.Opcodes
|
||||
import org.objectweb.asm.Type
|
||||
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
abstract class CheckExcs extends DefaultTask {
|
||||
@InputFile abstract RegularFileProperty getBinary()
|
||||
@InputFiles abstract ConfigurableFileCollection getExcs()
|
||||
|
||||
@TaskAction
|
||||
protected void exec() {
|
||||
Util.init()
|
||||
def known = []
|
||||
binary.get().asFile.withInputStream { i ->
|
||||
new ZipInputStream(i).withCloseable { zin ->
|
||||
def visitor = new ClassVisitor(Opcodes.ASM9) {
|
||||
private String cls
|
||||
@Override
|
||||
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
|
||||
this.cls = name
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
|
||||
known.add(this.cls + '.' + name + descriptor)
|
||||
super.visitMethod(access, name, descriptor, signature, exceptions)
|
||||
}
|
||||
}
|
||||
ZipEntry zein
|
||||
while ((zein = zin.nextEntry) != null) {
|
||||
if (zein.name.endsWith('.class')) {
|
||||
ClassReader reader = new ClassReader(zin)
|
||||
reader.accept(visitor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
excs.each { f ->
|
||||
def lines = []
|
||||
f.eachLine { line ->
|
||||
def idx = line.indexOf('#')
|
||||
if (idx == 0 || line.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
def comment = idx == -1 ? null : line.substring(idx)
|
||||
if (idx != -1) line = line.substring(0, idx - 1)
|
||||
|
||||
if (!line.contains('=')) {
|
||||
println('Invalid: ' + line)
|
||||
return
|
||||
}
|
||||
|
||||
def (key, value) = line.split('=', 2)
|
||||
if (!known.contains(key)) {
|
||||
println('Unknown: ' + line)
|
||||
return
|
||||
}
|
||||
|
||||
def (cls, desc) = key.split('\\.', 2)
|
||||
if (!desc.contains('(')) {
|
||||
println('Invalid: ' + line)
|
||||
return
|
||||
}
|
||||
def name = desc.split('\\(', 2)[0]
|
||||
desc = '(' + desc.split('\\(', 2)[1]
|
||||
|
||||
def (exceptions, args) = value.contains('|') ? value.split('|', 2) : [value, '']
|
||||
|
||||
if (args.split(',').length != Type.getArgumentTypes(desc).length) {
|
||||
println('Invalid: ' + line)
|
||||
return
|
||||
}
|
||||
lines.add(line)
|
||||
}
|
||||
f.text = lines.sort().join('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.InputDirectory
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.util.regex.Pattern
|
||||
|
||||
abstract class CheckPatches extends DefaultTask {
|
||||
@InputDirectory abstract DirectoryProperty getPatchDir()
|
||||
@Input boolean autoFix = false
|
||||
|
||||
@TaskAction
|
||||
protected void exec() {
|
||||
def hasS2SArtifact = [
|
||||
Paths.get("patches/minecraft/net/minecraft/client/renderer/ViewArea.java.patch"),
|
||||
Paths.get("patches/minecraft/net/minecraft/data/models/blockstates/Variant.java.patch")
|
||||
]
|
||||
|
||||
def verified = true;
|
||||
patchDir.get().asFileTree.each { patch ->
|
||||
def patchPath = project.rootDir.toPath().relativize(patch.toPath())
|
||||
verified &= verifyPatch(patch, autoFix, patchPath.toString(), hasS2SArtifact.contains(patchPath))
|
||||
}
|
||||
|
||||
if (!verified)
|
||||
throw new RuntimeException('One or more patches failed verification. Check the log for errors.')
|
||||
}
|
||||
|
||||
def verifyPatch(patch, fix, patchPath, hasS2SArtifact) {
|
||||
def hunk_start_pattern = Pattern.compile('^@@ -[0-9,]* \\+[0-9,_]* @@$')
|
||||
def white_space_pattern = Pattern.compile('^[+\\-]\\s*$')
|
||||
def import_pattern = Pattern.compile('^[+\\-]\\s*import.*')
|
||||
def field_pattern = Pattern.compile('^[+\\-][\\s]*((public|protected|private)[\\s]*)?(static[\\s]*)?(final)?([^=;]*)(=.*)?;\\s*$')
|
||||
def method_pattern = Pattern.compile('^[+\\-][\\s]*((public|protected|private)[\\s]*)?(static[\\s]*)?(final)?([^(]*)[(]([^)]*)?[)]\\s*[{]\\s*$')
|
||||
def class_pattern = Pattern.compile('^[+\\-][\\s]*((public|protected|private)[\\s]*)?(static[\\s]*)?(final[\\s]*)?(class|interface)([^{]*)[{]\\s*$')
|
||||
|
||||
def accessMap = [("private"):0, (null):1, ("protected"):2, ("public"):3]
|
||||
|
||||
def hasProblem = false;
|
||||
def lines = patch.readLines()
|
||||
|
||||
def hunksStart = 0
|
||||
def onlyWhiteSpace = false
|
||||
|
||||
def didFix = false
|
||||
def newLines = []
|
||||
def whiteSpaceErrors = []
|
||||
|
||||
// First two lines are file name ++/-- and we do not care
|
||||
newLines.add(lines[0] + '\n')
|
||||
newLines.add(lines[1] + '\n')
|
||||
|
||||
int i = 2
|
||||
for (; i < lines.size(); ++i) {
|
||||
def line = lines[i]
|
||||
newLines.add(line + '\n')
|
||||
|
||||
if (hunk_start_pattern.matcher(line).find()) {
|
||||
if (onlyWhiteSpace) {
|
||||
if (fix || hasS2SArtifact) {
|
||||
logger.lifecycle("Removing white space hunk starting at line {}, file: {}", hunksStart + 1, patchPath)
|
||||
def toRemove = i - hunksStart;
|
||||
while (toRemove-- > 0)
|
||||
newLines.remove(newLines.size() - 1)
|
||||
didFix = true
|
||||
}
|
||||
else {
|
||||
logger.lifecycle("Patch contains only white space hunk starting at line {}, file: {}", hunksStart + 1, patchPath)
|
||||
hasProblem = true
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!whiteSpaceErrors.empty)
|
||||
hasProblem = true
|
||||
whiteSpaceErrors.each { error -> logger.lifecycle(error) }
|
||||
whiteSpaceErrors.clear()
|
||||
}
|
||||
hunksStart = i
|
||||
onlyWhiteSpace = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.startsWithAny('+','-')) {
|
||||
def prefixChange = false
|
||||
def prevLine = lines[i - 1]
|
||||
|
||||
if (line.charAt(0) == (char)'+' && prevLine.charAt(0) == (char)'-') {
|
||||
def prevTrim = prevLine.substring(1).replaceAll("\\s", "")
|
||||
def currTrim = line.substring(1).replaceAll("\\s", "")
|
||||
|
||||
if (prevTrim.equals(currTrim)) {
|
||||
prefixChange = true
|
||||
}
|
||||
|
||||
def pMatcher = field_pattern.matcher(prevLine)
|
||||
def cMatcher = field_pattern.matcher(line)
|
||||
|
||||
if (pMatcher.find() && cMatcher.find() &&
|
||||
pMatcher.group(6) == cMatcher.group(6) && // = ...
|
||||
pMatcher.group(5) == cMatcher.group(5) && // field name
|
||||
pMatcher.group(3) == cMatcher.group(3) && // static
|
||||
(accessMap[pMatcher.group(2)] < accessMap[cMatcher.group(2)] || pMatcher.group(4) != cMatcher.group(4))) {
|
||||
logger.lifecycle("Patch contains access changes or final removal at line {}, file: {}", i + 1, patchPath)
|
||||
hasProblem = true
|
||||
}
|
||||
|
||||
pMatcher = method_pattern.matcher(prevLine)
|
||||
cMatcher = method_pattern.matcher(line)
|
||||
|
||||
if (pMatcher.find() && cMatcher.find() &&
|
||||
pMatcher.group(6) == cMatcher.group(6) && // params
|
||||
pMatcher.group(5) == cMatcher.group(5) && // <T> void name
|
||||
pMatcher.group(3) == cMatcher.group(3) && // static
|
||||
(accessMap[pMatcher.group(2)] < accessMap[cMatcher.group(2)] || pMatcher.group(4) != cMatcher.group(4))) {
|
||||
logger.lifecycle("Patch contains access changes or final removal at line {}, file: {}", i + 1, patchPath)
|
||||
hasProblem = true
|
||||
}
|
||||
|
||||
pMatcher = class_pattern.matcher(prevLine)
|
||||
cMatcher = class_pattern.matcher(line)
|
||||
|
||||
if (pMatcher.find() && cMatcher.find() &&
|
||||
pMatcher.group(6) == cMatcher.group(6) && // ClassName<> extends ...
|
||||
pMatcher.group(5) == cMatcher.group(5) && // class | interface
|
||||
pMatcher.group(3) == cMatcher.group(3) && // static
|
||||
(accessMap[pMatcher.group(2)] < accessMap[cMatcher.group(2)] || pMatcher.group(4) != cMatcher.group(4))) {
|
||||
logger.lifecycle("Patch contains access changes or final removal at line {}, file: {}", i + 1, patchPath)
|
||||
hasProblem = true
|
||||
}
|
||||
}
|
||||
|
||||
if (line.charAt(0) == (char)'-' && i + 1 < lines.size()) {
|
||||
def nextLine = lines[i + 1]
|
||||
if (nextLine.charAt(0) == (char)'+') {
|
||||
def nextTrim = nextLine.substring(1).replaceAll("\\s", "")
|
||||
def currTrim = line.substring(1).replaceAll("\\s", "")
|
||||
|
||||
if (nextTrim.equals(currTrim)) {
|
||||
prefixChange = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def isWhiteSpaceChange = white_space_pattern.matcher(line).find()
|
||||
|
||||
if (!prefixChange && !isWhiteSpaceChange) {
|
||||
onlyWhiteSpace = hasS2SArtifact && import_pattern.matcher(line).find()
|
||||
}
|
||||
else if (isWhiteSpaceChange) {
|
||||
def prevLineChange = prevLine.startsWithAny('+','-')
|
||||
def nextLineChange = i + 1 < lines.size() && lines[i + 1].startsWithAny('+','-')
|
||||
|
||||
if (!prevLineChange && !nextLineChange) {
|
||||
whiteSpaceErrors.add(String.format("Patch contains white space change in valid hunk at line %d (cannot auto fix), file: %s", i + 1, patchPath))
|
||||
}
|
||||
}
|
||||
|
||||
if (line.contains("\t")) {
|
||||
if (!fix) {
|
||||
logger.lifecycle("Patch contains tabs on line {}, file: {}", i + 1, patchPath)
|
||||
hasProblem = true
|
||||
}
|
||||
else {
|
||||
logger.lifecycle("Fixing tabs on line {}, file: {}", i + 1, patchPath)
|
||||
line = line.replaceAll('\t', ' ')
|
||||
newLines.remove(newLines.size() - 1)
|
||||
newLines.add(line + '\n')
|
||||
didFix = true
|
||||
}
|
||||
}
|
||||
|
||||
if (import_pattern.matcher(line).find() && !hasS2SArtifact) {
|
||||
logger.lifecycle("Patch contains import change on line {}, file: {}", i + 1, patchPath)
|
||||
hasProblem = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (onlyWhiteSpace) {
|
||||
if (fix || hasS2SArtifact) {
|
||||
logger.lifecycle("Removing white space hunk starting at line {}, file: {}", hunksStart + 1, patchPath)
|
||||
def toRemove = i - hunksStart;
|
||||
while (toRemove-- > 0)
|
||||
newLines.remove(newLines.size() - 1)
|
||||
didFix = true
|
||||
}
|
||||
else {
|
||||
logger.lifecycle("Patch contains only white space hunk starting at line {}, file: {}", hunksStart + 1, patchPath)
|
||||
hasProblem = true
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!whiteSpaceErrors.empty)
|
||||
hasProblem = true
|
||||
whiteSpaceErrors.each { error -> logger.lifecycle(error) }
|
||||
}
|
||||
|
||||
if (didFix) {
|
||||
if (newLines.size() <= 2) {
|
||||
logger.lifecycle("Patch is now empty removing, file: {}", patchPath)
|
||||
Files.delete(patch.toPath())
|
||||
}
|
||||
else {
|
||||
if (!hasS2SArtifact)
|
||||
logger.lifecycle("*** Updating patch file. Please run setup then genPatches again. ***")
|
||||
patch.withWriter('UTF-8') { writer ->
|
||||
newLines.each { l -> writer.write(l) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !hasProblem
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import org.gradle.api.file.ConfigurableFileCollection
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.InputFiles
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
abstract class CheckSAS extends DefaultTask {
|
||||
@InputFile abstract RegularFileProperty getInheritance()
|
||||
@InputFiles abstract ConfigurableFileCollection getSass()
|
||||
|
||||
@TaskAction
|
||||
protected void exec() {
|
||||
Util.init()
|
||||
def json = inheritance.get().asFile.json()
|
||||
|
||||
sass.each { f ->
|
||||
def lines = []
|
||||
f.eachLine { line ->
|
||||
if (line[0] == '\t') return //Skip any tabbed lines, those are ones we add
|
||||
def idx = line.indexOf('#')
|
||||
if (idx == 0 || line.isEmpty()) {
|
||||
lines.add(line)
|
||||
return
|
||||
}
|
||||
def comment = idx == -1 ? null : line.substring(idx)
|
||||
if (idx != -1) line = line.substring(0, idx - 1)
|
||||
def (cls, desc) = (line.trim() + ' ').split(' ', -1)
|
||||
cls = cls.replaceAll('\\.', '/')
|
||||
desc = desc.replace('(', ' (')
|
||||
|
||||
if (json[cls] == null || (!desc.isEmpty() && (json[cls]['methods'] == null || json[cls]['methods'][desc] == null))) {
|
||||
println('Invalid: ' + line)
|
||||
return
|
||||
}
|
||||
|
||||
//Class SAS
|
||||
if (desc.isEmpty()) {
|
||||
lines.add(cls + (comment == null ? '' : ' ' + comment))
|
||||
if (json[cls]['methods'] != null)
|
||||
(json[cls]['methods'] as TreeMap).each {
|
||||
findChildMethods(json, cls, it.key).each { lines.add('\t' + it) }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//Method SAS
|
||||
lines.add(cls + ' ' + desc.replace(' ', '') + (comment == null ? '' : ' ' + comment))
|
||||
findChildMethods(json, cls, desc).each { lines.add('\t' + it) }
|
||||
}
|
||||
f.text = lines.join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
protected static findChildMethods(json, cls, desc)
|
||||
{
|
||||
return json.values().findAll{ it.methods != null && it.methods[desc] != null && it.methods[desc].override == cls}
|
||||
.collect { it.name + ' ' + desc.replace(' ', '') } as TreeSet
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package net.minecraftforge.forge.tasks;
|
||||
|
||||
import groovy.transform.CompileDynamic;
|
||||
import groovy.transform.CompileStatic;
|
||||
|
||||
import java.util.Properties;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Map;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.Reader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Eclipse config files are literally just java properties, with the header cleaned up.
|
||||
* https://github.com/eclipse/buildship/blob/5b2c7fca7fa86cd74d71b3c099c7b1559eba038e/org.eclipse.buildship.core/src/main/java/org/eclipse/buildship/core/internal/configuration/PreferenceStore.java#L238
|
||||
*
|
||||
* This does the same thing, as well as sorting alphabetically.
|
||||
* It also ignores all comments. We can add them latter if someone cares.
|
||||
*/
|
||||
@CompileStatic
|
||||
public class CleanProperties extends Properties {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String LINE_SEP = System.getProperty("line.separator");
|
||||
private static final String UNIX_LINE_SEP = "\n";
|
||||
private static final Charset ENCODING = StandardCharsets.UTF_8;
|
||||
|
||||
public CleanProperties load(File input) throws IOException {
|
||||
if (input.exists()) {
|
||||
try (Reader is = new InputStreamReader(new FileInputStream(input), ENCODING)) {
|
||||
super.load(is);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public void store(File out) throws IOException {
|
||||
if (!out.getParentFile().exists())
|
||||
out.getParentFile().mkdirs();
|
||||
|
||||
try (OutputStream os = new FileOutputStream(out)) {
|
||||
store(os, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Enumeration<Object> keys() {
|
||||
Set<Object> ret = new TreeSet<>();
|
||||
for (Enumeration<?> e = super.keys(); e.hasMoreElements();)
|
||||
ret.add(e.nextElement());
|
||||
return Collections.enumeration(ret);
|
||||
}
|
||||
|
||||
@CompileDynamic
|
||||
@Override
|
||||
public Set<Map.Entry<Object, Object>> entrySet() {
|
||||
Set<Map.Entry<Object, Object>> ret = new TreeSet<>((l, r) -> ((String)l.getKey()).compareTo((String)r.getKey()));
|
||||
ret.addAll(super.entrySet());
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void store(OutputStream out, String comments) throws IOException {
|
||||
out.write(clean().getBytes(ENCODING));
|
||||
out.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void store(Writer out, String comments) throws IOException {
|
||||
out.write(clean());
|
||||
out.flush();
|
||||
}
|
||||
|
||||
private String clean() throws IOException {
|
||||
ByteArrayOutputStream tmp = new ByteArrayOutputStream();
|
||||
try {
|
||||
super.store(tmp, null);
|
||||
} finally {
|
||||
tmp.close();
|
||||
}
|
||||
|
||||
String ret = tmp.toString(ENCODING).replace(LINE_SEP, UNIX_LINE_SEP);
|
||||
ret = ret.substring(ret.indexOf(UNIX_LINE_SEP) + 1);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import groovy.transform.stc.FirstParam
|
||||
|
||||
import java.util.function.BiConsumer
|
||||
|
||||
public class ClosureHelper {
|
||||
@@ -22,7 +24,7 @@ public class ClosureHelper {
|
||||
}
|
||||
}
|
||||
|
||||
static <T> T apply(T obj, Closure cl) {
|
||||
static <T> T apply(T obj, @DelegatesTo(value = FirstParam, strategy = Closure.DELEGATE_FIRST) Closure cl) {
|
||||
cl.delegate = obj
|
||||
cl.resolveStrategy = Closure.DELEGATE_FIRST
|
||||
cl()
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.Optional
|
||||
import org.gradle.api.tasks.OutputFile
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
abstract class Crowdin extends DefaultTask {
|
||||
@Input abstract Property<String> getId()
|
||||
@Input @Optional abstract Property<String> getKey()
|
||||
@Input boolean json = true
|
||||
@OutputFile abstract RegularFileProperty getOutput()
|
||||
@OutputFile abstract RegularFileProperty getExport()
|
||||
|
||||
Crowdin() {
|
||||
outputs.upToDateWhen{ false }
|
||||
id.convention('minecraft-forge')
|
||||
output.convention(project.layout.buildDirectory.dir(name).map {it.file("output.zip") })
|
||||
export.convention(project.layout.buildDirectory.dir(name).map {it.file("export.json") })
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
def run() {
|
||||
File outputFile = output.get().asFile
|
||||
File exportFile = export.get().asFile
|
||||
if (outputFile.exists())
|
||||
outputFile.delete()
|
||||
|
||||
if (!key.isPresent())
|
||||
return
|
||||
String key = this.key.get()
|
||||
String id = this.id.get()
|
||||
|
||||
// Force an export
|
||||
new URL("https://api.crowdin.com/api/project/${id}/export?key=${key}").withInputStream { i ->
|
||||
exportFile.withOutputStream { it << i }
|
||||
}
|
||||
|
||||
if (!exportFile.text.contains('success')) {
|
||||
throw new RuntimeException("Crowdin export failed, see ${exportFile} for more info")
|
||||
}
|
||||
|
||||
new URL("https://api.crowdin.com/api/project/${id}/download/all.zip?key=${key}").withInputStream { i ->
|
||||
new ZipInputStream(i).withCloseable { zin ->
|
||||
outputFile.withOutputStream { out ->
|
||||
new ZipOutputStream(out).withCloseable { zout ->
|
||||
ZipEntry zein
|
||||
while ((zein = zin.nextEntry) != null) {
|
||||
if (zein.isDirectory()) {
|
||||
zout.putNextEntry(new ZipEntry(zein.name))
|
||||
} else {
|
||||
// 1.13+ uses json
|
||||
if (zein.name.endsWith('.json') == json) {
|
||||
ZipEntry zeout = new ZipEntry(json ? zein.name.toLowerCase() : zein.name)
|
||||
zeout.time = 1
|
||||
zout.putNextEntry(zeout)
|
||||
zout << zin
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ abstract class DownloadLibraries extends DefaultTask {
|
||||
def run() {
|
||||
Util.init()
|
||||
File outputDir = output.get().asFile
|
||||
def libraries = new ArrayList()
|
||||
var libraries = new ArrayList<String>()
|
||||
|
||||
def json = input.get().asFile.json().libraries.each { lib ->
|
||||
//TODO: Thread?
|
||||
@@ -35,7 +35,7 @@ abstract class DownloadLibraries extends DefaultTask {
|
||||
artifacts.each{ art ->
|
||||
def target = new File(outputDir, art.path)
|
||||
libraries.add(target.absolutePath)
|
||||
if (!target.exists() || !art.sha1.equals(target.sha1())) {
|
||||
if (!target.exists() || art.sha1 != target.sha1()) {
|
||||
project.logger.lifecycle("Downloading ${art.url}")
|
||||
if (!target.parentFile.exists()) {
|
||||
target.parentFile.mkdirs()
|
||||
@@ -43,7 +43,7 @@ abstract class DownloadLibraries extends DefaultTask {
|
||||
new URL(art.url).withInputStream { i ->
|
||||
target.withOutputStream { it << i }
|
||||
}
|
||||
if (!art.sha1.equals(target.sha1())) {
|
||||
if (art.sha1 != target.sha1()) {
|
||||
throw new IllegalStateException("Failed to download ${art.url} to ${target.canonicalPath} SHA Mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.*
|
||||
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
@CompileStatic
|
||||
abstract class ExtractFile extends DefaultTask {
|
||||
@InputFile abstract RegularFileProperty getInput()
|
||||
@Input abstract Property<String> getTarget()
|
||||
@OutputFile abstract RegularFileProperty getOutput()
|
||||
|
||||
@TaskAction
|
||||
protected void exec() {
|
||||
var zip = new ZipFile(input.get().asFile);
|
||||
var entry = zip.getEntry(target.get())
|
||||
if (entry == null)
|
||||
throw new IllegalStateException(input.get().asFile.absolutePath + " does not contain " + target.get())
|
||||
var stream = zip.getInputStream(entry)
|
||||
output.get().asFile.withOutputStream { out ->
|
||||
stream.transferTo(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,11 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.EqualsAndHashCode
|
||||
|
||||
import java.util.ArrayList
|
||||
import java.util.HashMap
|
||||
import java.util.TreeMap
|
||||
import java.util.TreeSet
|
||||
import java.util.function.BiConsumer
|
||||
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.tasks.*
|
||||
|
||||
import org.objectweb.asm.Opcodes
|
||||
import org.objectweb.asm.Type
|
||||
import org.objectweb.asm.tree.AbstractInsnNode
|
||||
import org.objectweb.asm.tree.ClassNode
|
||||
import org.objectweb.asm.tree.FieldNode
|
||||
import org.objectweb.asm.tree.MethodNode
|
||||
|
||||
import static org.objectweb.asm.Opcodes.*
|
||||
@@ -34,17 +25,17 @@ abstract class FieldCompareFinder extends BytecodeFinder {
|
||||
|
||||
@Override
|
||||
protected process(ClassNode parent, MethodNode node) {
|
||||
def last = null
|
||||
AbstractInsnNode last = null
|
||||
def parentInstance = new ObjectTarget(owner: parent.name, name: '', desc: '')
|
||||
for (int x = 0; x < node.instructions.size(); x++) {
|
||||
def current = node.instructions.get(x)
|
||||
if (current.opcode == IF_ACMPEQ || current.opcode == IF_ACMPNE) {
|
||||
if (last != null && (last.opcode == GETSTATIC || last.opcode == GETFIELD)) {
|
||||
if (current.opcode === IF_ACMPEQ || current.opcode === IF_ACMPNE) {
|
||||
if (last !== null && (last.opcode === GETSTATIC || last.opcode === GETFIELD)) {
|
||||
def target = new Search(cls: last.owner, name: last.name)
|
||||
def wanted = fieldsReverse.get(target)
|
||||
def original = fields.get(wanted)
|
||||
def instance = new ObjectTarget(owner: parent.name, name: node.name, desc: node.desc)
|
||||
if (wanted != null && (original.blacklist == null || (!original.blacklist.contains(instance) && !original.blacklist.contains(parentInstance)))) {
|
||||
if (wanted !== null && (original.blacklist === null || (!original.blacklist.contains(instance) && !original.blacklist.contains(parentInstance)))) {
|
||||
targets.computeIfAbsent(wanted, { k -> new TreeSet() }).add(instance)
|
||||
}
|
||||
}
|
||||
@@ -68,9 +59,10 @@ abstract class FieldCompareFinder extends BytecodeFinder {
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
|
||||
@CompileStatic
|
||||
@EqualsAndHashCode(excludes = ['replacement', 'blacklist'])
|
||||
public static class Search {
|
||||
static class Search {
|
||||
@Input
|
||||
String cls
|
||||
|
||||
@@ -89,17 +81,17 @@ abstract class FieldCompareFinder extends BytecodeFinder {
|
||||
return cls + '.' + name
|
||||
}
|
||||
|
||||
def blacklist(def owner, def name, def desc) {
|
||||
if (blacklist == null)
|
||||
blacklist = new HashSet()
|
||||
def blacklist(String owner, String name, String desc) {
|
||||
if (blacklist === null)
|
||||
blacklist = new HashSet<>()
|
||||
blacklist.add(new ObjectTarget(owner: owner, name: name, desc: desc))
|
||||
}
|
||||
def blacklist(def owner) {
|
||||
def blacklist(String owner) {
|
||||
blacklist(owner, '', '')
|
||||
}
|
||||
}
|
||||
|
||||
def fields(Closure cl) {
|
||||
void fields(Closure cl) {
|
||||
new ClosureHelper(cl, {name, ccl ->
|
||||
def search = ClosureHelper.apply(new Search(), ccl)
|
||||
this.fields.put(name, search)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.TupleConstructor
|
||||
|
||||
@CompileStatic
|
||||
@TupleConstructor
|
||||
final class InheritanceData implements Annotatable {
|
||||
String name
|
||||
int access
|
||||
String superName
|
||||
List<String> interfaces = []
|
||||
Map<String, Method> methods = [:]
|
||||
Map<String, Field> fields = [:]
|
||||
List<Annotation> annotations = []
|
||||
|
||||
@TupleConstructor
|
||||
static final class Method implements Annotatable {
|
||||
int access
|
||||
String override
|
||||
List<Annotation> annotations = []
|
||||
}
|
||||
|
||||
@TupleConstructor
|
||||
static final class Field implements Annotatable {
|
||||
int access
|
||||
String desc
|
||||
List<Annotation> annotations = []
|
||||
}
|
||||
|
||||
@TupleConstructor
|
||||
static final class Annotation {
|
||||
String desc
|
||||
}
|
||||
|
||||
private static final Gson GSON = new Gson()
|
||||
static Map<String, InheritanceData> parse(File file) {
|
||||
try (final reader = file.newReader()) {
|
||||
return GSON.fromJson(reader, new TypeToken<Map<String, InheritanceData>>() {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CompileStatic
|
||||
interface Annotatable {
|
||||
List<InheritanceData.Annotation> getAnnotations()
|
||||
}
|
||||
@@ -1,44 +1,158 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import org.gradle.api.file.DuplicatesStrategy
|
||||
import org.gradle.api.tasks.bundling.AbstractArchiveTask
|
||||
import org.gradle.api.tasks.bundling.Zip
|
||||
import org.gradle.api.tasks.*
|
||||
import org.gradle.api.provider.SetProperty
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.provider.Property
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
abstract class InstallerJar extends Zip {
|
||||
@Input @Optional abstract SetProperty<String> getPackedDependencies()
|
||||
|
||||
@Input @Optional abstract Property<Boolean> getFat()
|
||||
@Input @Optional abstract Property<Boolean> getOffline()
|
||||
|
||||
InstallerJar() {
|
||||
archiveClassifier.set('installer')
|
||||
archiveExtension.set('jar') // Needs to be Zip task to not override Manifest, so set extension
|
||||
destinationDirectory.set(project.layout.buildDirectory.dir('libs'))
|
||||
fat.convention(false)
|
||||
offline.convention(false)
|
||||
|
||||
def installerJson = project.tasks.installerJson
|
||||
def launcherJson = project.tasks.launcherJson
|
||||
def downloadInstaller = project.tasks.downloadInstaller
|
||||
|
||||
dependsOn(installerJson, launcherJson, downloadInstaller)
|
||||
|
||||
dependsOn(installerJson, launcherJson, downloadInstaller, project.configurations.installer)
|
||||
from(installerJson, launcherJson)
|
||||
|
||||
from(project.rootProject.file('/src/main/resources/url.png'))
|
||||
project.afterEvaluate {
|
||||
from(project.zipTree(downloadInstaller.output)) {
|
||||
duplicatesStrategy = 'exclude'
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
if (!System.env.MAVEN_USER) {
|
||||
dependsOn(project.universalJar)
|
||||
from(project.universalJar) {
|
||||
into '/maven/' + project.group.replace('.', '/') + '/' + project.universalJar.archiveBaseName.get() + '/' + project.version + '/'
|
||||
if (fat.get() || offline.get()) {
|
||||
def cfg = project.tasks.register(name + "Config", Configure)
|
||||
cfg.get().configure {
|
||||
parent = this
|
||||
dependsOn(project.tasks.installerJson, project.tasks.launcherJson)
|
||||
}
|
||||
|
||||
packedDependencies.get().forEach {
|
||||
def jarTask = project.rootProject.getTasks().findByPath(it)
|
||||
dependsOn(jarTask)
|
||||
from(jarTask) {
|
||||
into '/maven/' + jarTask.project.group.replace('.', '/') + '/' + jarTask.archiveBaseName.get() + '/' + jarTask.project.version + '/'
|
||||
dependsOn(cfg)
|
||||
} else {
|
||||
// Things we ALWAYS bundle, this just the server shim jar, because the installer spec only says to extract the file.
|
||||
// I should make it allow downloads but thats a spec break, and this is just a ~14KB jar
|
||||
[
|
||||
project.tasks.serverShimJar // Server bootstrap executable jar
|
||||
].forEach { AbstractArchiveTask packed ->
|
||||
def path = Util.getMavenInfoFromTask(packed).path
|
||||
from(packed) {
|
||||
rename { "maven/$path" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static abstract class Configure extends DefaultTask {
|
||||
public Zip parent
|
||||
private int count = 0;
|
||||
|
||||
@TaskAction
|
||||
protected void exec() {
|
||||
def deps = [:] as java.util.TreeMap
|
||||
|
||||
// Gather all things that need downloading
|
||||
[
|
||||
project.tasks.installerJson,
|
||||
project.tasks.launcherJson
|
||||
].each { task ->
|
||||
def json = task.output.get().asFile.json
|
||||
json.libraries.each { lib ->
|
||||
if (lib.downloads?.artifact?.url !== null && !lib.downloads.artifact.url.isEmpty())
|
||||
deps.put(lib.name, lib.downloads.artifact)
|
||||
}
|
||||
}
|
||||
|
||||
// First find things we build in this project.
|
||||
[
|
||||
project.tasks.universalJar, // Forge runtime code
|
||||
project.tasks.serverShimJar // Shim jar for dedicated server
|
||||
].forEach { AbstractArchiveTask packed ->
|
||||
def name = Util.getMavenInfoFromTask(packed).name
|
||||
def info = deps.remove(name)
|
||||
if (info !== null) {
|
||||
println("Adding: $packed.path $name")
|
||||
parent.from(packed) {
|
||||
rename { "maven/$info.path" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find any artifacts from the 'installer' config
|
||||
// This config specifies the runtime files we intend for the interaller to have.
|
||||
// And are typically what we would be developing and testing alongside Forge.
|
||||
// So we may have local modified versions
|
||||
def cfg = project.configurations.installer
|
||||
while (cfg != null) {
|
||||
//println('')
|
||||
def resolved = cfg.resolvedConfiguration.resolvedArtifacts
|
||||
int found = 0
|
||||
for (def dep : resolved) {
|
||||
def name = Util.getMavenInfoFromDep(dep).name
|
||||
def info = deps.remove(name)
|
||||
if (info == null) {
|
||||
//println("Skipping: $name")
|
||||
continue
|
||||
}
|
||||
//println("-$name")
|
||||
found++
|
||||
addFile(dep.file, info)
|
||||
}
|
||||
|
||||
if (deps.isEmpty()) {
|
||||
cfg = null
|
||||
continue
|
||||
}
|
||||
|
||||
// Prevent infinite loops if something fucky happens
|
||||
if (found == 0)
|
||||
throw new IllegalStateException("Failed to find any installer dependencies")
|
||||
|
||||
def seen = [] as Set
|
||||
cfg = project.configurations.detachedConfiguration()
|
||||
cfg.transitive = false
|
||||
for (def key : deps.keySet()) {
|
||||
def (group, artifact, other) = key.split(':', 3)
|
||||
// Only resolve unique group:artifact so we don't get version overrides
|
||||
if (seen.add(group + ':' + artifact)) {
|
||||
//println("+$key")
|
||||
cfg.dependencies.add(project.dependencies.create(key))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addFile(file, info) {
|
||||
boolean pack = parent.offline.get() || info.url.isEmpty()
|
||||
|
||||
// If it's a offline jar just always pack
|
||||
if (!pack) {
|
||||
try {
|
||||
var remote = new URL("${info.url}.sha1").getText('UTF-8')
|
||||
pack = info.sha1 != remote
|
||||
} catch (FileNotFoundException e) {
|
||||
pack = !info.url.startsWith('https://libraries.minecraft.net/')
|
||||
// Oh noes its not there!
|
||||
}
|
||||
}
|
||||
|
||||
if (pack) {
|
||||
println("Adding: $file.absolutePath")
|
||||
parent.from(file) {
|
||||
rename { "maven/$info.path" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,13 @@ import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.provider.SetProperty
|
||||
import org.gradle.api.tasks.*
|
||||
import org.gradle.api.tasks.bundling.AbstractArchiveTask
|
||||
|
||||
import java.nio.file.Files
|
||||
|
||||
abstract class InstallerJson extends DefaultTask {
|
||||
@OutputFile abstract RegularFileProperty getOutput()
|
||||
@InputFiles abstract ConfigurableFileCollection getInput()
|
||||
@Input @Optional abstract SetProperty<String> getPackedDependencies()
|
||||
@Input @Optional final Map<String, Object> libraries = new LinkedHashMap<>()
|
||||
@Input Map<String, Object> json = new LinkedHashMap<>()
|
||||
@InputFile abstract RegularFileProperty getIcon()
|
||||
@@ -23,29 +23,19 @@ abstract class InstallerJson extends DefaultTask {
|
||||
@Input abstract Property<String> getWelcome()
|
||||
|
||||
InstallerJson() {
|
||||
getLauncherJsonName().convention('/version.json')
|
||||
getLogo().convention('/big_logo.png')
|
||||
getMirrors().convention('https://files.minecraftforge.net/mirrors-2.0.json')
|
||||
getWelcome().convention("Welcome to the simple ${project.name.capitalize()} installer.")
|
||||
|
||||
getOutput().convention(project.layout.buildDirectory.file('install_profile.json'))
|
||||
|
||||
['client', 'server'].each { side ->
|
||||
['slim', 'extra'].each { type ->
|
||||
def tsk = project.tasks.getByName("download${side.capitalize()}${type.capitalize()}")
|
||||
dependsOn(tsk)
|
||||
input.from(tsk.output)
|
||||
}
|
||||
def tsk = project.tasks.getByName("create${side.capitalize()}SRG")
|
||||
dependsOn(tsk)
|
||||
input.from(tsk.output)
|
||||
}
|
||||
|
||||
launcherJsonName.convention('/version.json')
|
||||
logo.convention('/big_logo.png')
|
||||
mirrors.convention('https://files.minecraftforge.net/mirrors-2.0.json')
|
||||
welcome.convention("Welcome to the ${project.name.capitalize()} installer.")
|
||||
output.convention(project.layout.buildDirectory.file('libs/install_profile.json'))
|
||||
|
||||
project.afterEvaluate {
|
||||
(packedDependencies.get().collect{ project.rootProject.tasks.findByPath(it) } + [project.universalJar]).forEach {
|
||||
dependsOn(it)
|
||||
input.from it.archiveFile
|
||||
[
|
||||
project.tasks.universalJar,
|
||||
project.tasks.serverShimJar
|
||||
].forEach { packed ->
|
||||
dependsOn(packed)
|
||||
input.from packed.archiveFile
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,17 +43,19 @@ abstract class InstallerJson extends DefaultTask {
|
||||
@TaskAction
|
||||
protected void exec() {
|
||||
def libs = libraries
|
||||
for (def child : packedDependencies.get().collect{ project.rootProject.tasks.findByPath(it) } + [project.universalJar]) {
|
||||
def dep = Util.getMavenDep(child)
|
||||
def path = Util.getMavenPath(child)
|
||||
libs.put(dep.toString(), [
|
||||
name: dep,
|
||||
[
|
||||
project.tasks.universalJar,
|
||||
project.tasks.serverShimJar
|
||||
].forEach { AbstractArchiveTask packed ->
|
||||
def info = Util.getMavenInfoFromTask(packed)
|
||||
libs.put(info.name, [
|
||||
name: info.name,
|
||||
downloads: [
|
||||
artifact: [
|
||||
path: path,
|
||||
url: "https://maven.minecraftforge.net/${path}",
|
||||
sha1: child.archiveFile.get().asFile.sha1(),
|
||||
size: child.archiveFile.get().asFile.length()
|
||||
path: info.path,
|
||||
url: "https://maven.minecraftforge.net/$info.path",
|
||||
sha1: packed.archiveFile.get().asFile.sha1(),
|
||||
size: packed.archiveFile.get().asFile.length()
|
||||
]
|
||||
]
|
||||
])
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.gradle.api.file.ConfigurableFileCollection
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.SetProperty
|
||||
import org.gradle.api.tasks.*
|
||||
import org.gradle.api.tasks.bundling.AbstractArchiveTask
|
||||
|
||||
import java.nio.file.Files
|
||||
|
||||
@@ -16,59 +17,80 @@ abstract class LauncherJson extends DefaultTask {
|
||||
@OutputFile abstract RegularFileProperty getOutput()
|
||||
@InputFiles abstract ConfigurableFileCollection getInput()
|
||||
@Input Map<String, Object> json = new LinkedHashMap<>()
|
||||
@Input @Optional abstract SetProperty<String> getPackedDependencies()
|
||||
|
||||
@Internal final vanilla = project.project(':mcp').file('build/mcp/downloadJson/version.json')
|
||||
@Internal final timestamp = iso8601Now()
|
||||
@Internal final comment = [
|
||||
"Please do not automate the download and installation of Forge.",
|
||||
"Our efforts are supported by ads from the download page.",
|
||||
"If you MUST automate this, please consider supporting the project through https://www.patreon.com/LexManos/"
|
||||
]
|
||||
@Internal final id = "${project.rootProject.ext.MC_VERSION}-${project.name}${project.version.substring(project.rootProject.ext.MC_VERSION.length())}"
|
||||
|
||||
LauncherJson() {
|
||||
getOutput().convention(project.layout.buildDirectory.file('version.json'))
|
||||
output.convention(project.layout.buildDirectory.file('libs/version.json'))
|
||||
|
||||
dependsOn(':fmlloader:jar', 'universalJar')
|
||||
getInput().from(project.tasks.universalJar.archiveFile,
|
||||
project.project(':fmlloader').jar.archiveFile,
|
||||
vanilla)
|
||||
|
||||
project.afterEvaluate {
|
||||
packedDependencies.get().forEach {
|
||||
def jarTask = project.rootProject.tasks.findByPath(it)
|
||||
dependsOn(jarTask)
|
||||
input.from jarTask.archiveFile
|
||||
dependsOn(project.tasks.universalJar)
|
||||
input.from(project.tasks.universalJar.archiveFile)
|
||||
input.from(project.configurations.installer)
|
||||
input.from(project.configurations.installerextra)
|
||||
configure {
|
||||
def mc = project.rootProject.ext.MC_VERSION
|
||||
def forge = project.rootProject.ext.FORGE_VERSION
|
||||
def timestamp = iso8601Now()
|
||||
json.putAll([
|
||||
_comment: [
|
||||
"Please do not automate the download and installation of Forge.",
|
||||
"Our efforts are supported by ads from the download page.",
|
||||
"If you MUST automate this, please consider supporting the project through https://www.patreon.com/LexManos/"
|
||||
],
|
||||
id: "$mc-$project.name-$forge",
|
||||
time: timestamp,
|
||||
releaseTime: timestamp,
|
||||
inheritsFrom: mc,
|
||||
type: 'release',
|
||||
logging: [:],
|
||||
mainClass: '',
|
||||
libraries: []
|
||||
] as LinkedHashMap)
|
||||
|
||||
[
|
||||
project.tasks.universalJar
|
||||
].forEach { packed ->
|
||||
dependsOn(packed)
|
||||
input.from packed.archiveFile
|
||||
}
|
||||
|
||||
def patched = project.tasks.applyClientBinPatches
|
||||
dependsOn(patched)
|
||||
input.from patched.output
|
||||
}
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
protected void exec() {
|
||||
if (!json.libraries)
|
||||
json.libraries = []
|
||||
def libs = [:]
|
||||
getArtifacts(project, project.configurations.installer, false).each { key, lib -> libs[key] = lib }
|
||||
getArtifacts(project, project.configurations.moduleonly, false).each { key, lib -> libs[key] = lib }
|
||||
|
||||
packedDependencies.get().collect{ project.rootProject.tasks.findByPath(it) }.forEach {
|
||||
def path = Util.getMavenPath(it)
|
||||
def key = Util.getMavenDep(it)
|
||||
|
||||
libs[key] = [
|
||||
name: key,
|
||||
downloads: [
|
||||
artifact: [
|
||||
path: path,
|
||||
url: "https://maven.minecraftforge.net/${path}",
|
||||
sha1: it.archiveFile.get().asFile.sha1(),
|
||||
size: it.archiveFile.get().asFile.length()
|
||||
]
|
||||
var packed = (AbstractArchiveTask) project.tasks.universalJar
|
||||
def info = Util.getMavenInfoFromTask(packed)
|
||||
json.libraries.add([
|
||||
name: info.name,
|
||||
downloads: [
|
||||
artifact: [
|
||||
path: info.path,
|
||||
url: "https://maven.minecraftforge.net/$info.path",
|
||||
sha1: packed.archiveFile.get().asFile.sha1(),
|
||||
size: packed.archiveFile.get().asFile.length()
|
||||
]
|
||||
]
|
||||
}
|
||||
libs.each { key, lib -> json.libraries.add(lib) }
|
||||
])
|
||||
|
||||
var classifier = 'client'
|
||||
var genned = project.tasks.applyClientBinPatches
|
||||
info = Util.getMavenInfoFromTask(genned, classifier)
|
||||
json.libraries.add([
|
||||
name: info.name,
|
||||
downloads: [
|
||||
artifact: [
|
||||
path: info.path,
|
||||
url: "",
|
||||
sha1: genned.output.get().asFile.sha1(),
|
||||
size: genned.output.get().asFile.length()
|
||||
]
|
||||
]
|
||||
])
|
||||
|
||||
json.libraries.addAll(getArtifacts(project.configurations.installerextra).values())
|
||||
json.libraries.addAll(getArtifacts(project.configurations.installer).values())
|
||||
Files.writeString(output.get().asFile.toPath(), new JsonBuilder(json).toPrettyString())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.ConfigurableFileCollection
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.tasks.InputFiles
|
||||
import org.gradle.api.tasks.OutputFile
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
@CompileStatic
|
||||
abstract class MergeJars extends DefaultTask {
|
||||
MergeJars() {
|
||||
output.convention(project.layout.buildDirectory.dir(name).map { it.file('output.jar') })
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
void run() {
|
||||
def jars = inputJars.files
|
||||
|
||||
try (ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(output.get().asFile))) {
|
||||
for (def jar : jars) {
|
||||
try (ZipInputStream zin = new ZipInputStream(new FileInputStream(jar))) {
|
||||
ZipEntry entry
|
||||
while ((entry = zin.getNextEntry()) !== null) {
|
||||
ZipEntry _new = new ZipEntry(entry.getName())
|
||||
_new.setTime(0) //SHOULD be the same time as the main entry, but NOOOO _new.setTime(entry.getTime()) throws DateTimeException, so you get 0, screw you!
|
||||
zout.putNextEntry(_new)
|
||||
IOUtils.copy(zin, zout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@InputFiles
|
||||
abstract ConfigurableFileCollection getInputJars()
|
||||
|
||||
@OutputFile
|
||||
abstract RegularFileProperty getOutput()
|
||||
}
|
||||
@@ -27,6 +27,6 @@ public class ObjectTarget implements Comparable<ObjectTarget> {
|
||||
|
||||
@Override
|
||||
int compareTo(ObjectTarget o) {
|
||||
return toString().compareTo(o.toString())
|
||||
return toString() <=> o.toString()
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.artifacts.Dependency
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.Optional
|
||||
import org.gradle.api.tasks.OutputFile
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
|
||||
abstract class SetupCheckJarCompatibility extends DefaultTask {
|
||||
SetupCheckJarCompatibility() {
|
||||
group = 'jar compatibility'
|
||||
onlyIf {
|
||||
inputVersion.getOrNull() !== null
|
||||
}
|
||||
outputs.upToDateWhen { false } // Never up to date, because this setup task should always run
|
||||
|
||||
baseBinPatchesOutput.convention(project.layout.buildDirectory.dir(name).map { it.file('joined.lzma') })
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
void run() {
|
||||
def inputVersion = inputVersion.get()
|
||||
|
||||
def baseForgeUserdev = project.layout.buildDirectory.dir(name).map { it.file("forge-${inputVersion}-userdev.jar") }.get().asFile
|
||||
project.rootProject.extensions.download.run {
|
||||
src "https://maven.minecraftforge.net/net/minecraftforge/forge/${inputVersion}/forge-${inputVersion}-userdev.jar"
|
||||
dest baseForgeUserdev
|
||||
}
|
||||
|
||||
def joinedLzma = project.zipTree(baseForgeUserdev).matching { it.include('joined.lzma') }.singleFile
|
||||
|
||||
Files.copy(joinedLzma.toPath(), baseBinPatchesOutput.get().asFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
|
||||
@Input
|
||||
@Optional
|
||||
abstract Property<String> getInputVersion()
|
||||
|
||||
@OutputFile
|
||||
abstract RegularFileProperty getBaseBinPatchesOutput()
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import groovy.transform.CompileStatic
|
||||
import org.eclipse.jgit.api.Git
|
||||
|
||||
import javax.annotation.Nullable
|
||||
import java.util.stream.Collectors
|
||||
|
||||
@CompileStatic
|
||||
class TeamcityRequests {
|
||||
public static final Gson GSON = new Gson()
|
||||
|
||||
@Nullable
|
||||
static <T> T jsonRequest(TypeToken<T> clazz, String url) throws Exception {
|
||||
final HttpURLConnection conn = (HttpURLConnection) URI.create(url).toURL().openConnection()
|
||||
conn.setRequestProperty('Accept', 'application/json')
|
||||
conn.setReadTimeout(5 * 1000)
|
||||
conn.setConnectTimeout(5 * 1000)
|
||||
conn.connect()
|
||||
|
||||
if (conn.responseCode !== 200) {
|
||||
return null
|
||||
}
|
||||
|
||||
try (final InputStream is = conn.getInputStream()) {
|
||||
GSON.fromJson(new InputStreamReader(is), clazz.getType())
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static Map<String, Build> buildsByCommit() throws IOException {
|
||||
final Map<String, Build> builds = [:]
|
||||
jsonRequest(new TypeToken<Builds>() {}, 'https://teamcity.minecraftforge.net/guestAuth/app/rest/builds?fields=build:(revisions,number)&locator=buildType:(id:MinecraftForge_MinecraftForge_MinecraftForge_MinecraftForge__Build),defaultFilter:false,count:300,status:SUCCESS')
|
||||
?.build?.forEach {
|
||||
it.revisions.revision.each { rev -> builds[rev.version] = it }
|
||||
}
|
||||
return builds
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static String attemptFindBase(File gitPath) {
|
||||
try (final git = Git.open(gitPath)) {
|
||||
final Map<String, Build> buildByCommit = buildsByCommit()
|
||||
for (final commit in git.log().setMaxCount(100).call()) {
|
||||
// Find the first commit which was built on CI
|
||||
final build = buildByCommit[commit.id.name]
|
||||
if (build) {
|
||||
return build.number
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
return null
|
||||
}
|
||||
|
||||
static final class Builds {
|
||||
public List<Build> build
|
||||
}
|
||||
|
||||
static final class Build {
|
||||
public String number
|
||||
public Revisions revisions
|
||||
}
|
||||
|
||||
static final class Revisions {
|
||||
public int count
|
||||
public List<Revision> revision
|
||||
}
|
||||
|
||||
static final class Revision {
|
||||
public String version
|
||||
}
|
||||
}
|
||||
@@ -2,144 +2,229 @@ package net.minecraftforge.forge.tasks
|
||||
|
||||
import groovy.json.JsonBuilder
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.stc.ClosureParams
|
||||
import groovy.transform.stc.SimpleType
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.Task
|
||||
import org.gradle.api.artifacts.Configuration
|
||||
import org.gradle.api.artifacts.ResolvedArtifact
|
||||
import org.gradle.api.tasks.bundling.AbstractArchiveTask
|
||||
import org.objectweb.asm.ClassReader
|
||||
import org.objectweb.asm.Opcodes
|
||||
import org.objectweb.asm.tree.ClassNode
|
||||
|
||||
import java.io.File
|
||||
import java.net.http.HttpClient
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
import java.security.MessageDigest
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Semaphore
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
public class Util {
|
||||
static void init() {
|
||||
File.metaClass.sha1 = { ->
|
||||
MessageDigest md = MessageDigest.getInstance('SHA-1')
|
||||
delegate.eachByte 4096, {bytes, size ->
|
||||
md.update(bytes, 0, size)
|
||||
}
|
||||
return md.digest().collect {String.format "%02x", it}.join()
|
||||
}
|
||||
final class Util {
|
||||
public static final int ASM_LEVEL = Opcodes.ASM9
|
||||
private static final HttpClient HTTP = HttpClient.newBuilder().build()
|
||||
|
||||
static void init() {
|
||||
File.metaClass.sha1 = { ->
|
||||
MessageDigest md = MessageDigest.getInstance('SHA-1')
|
||||
delegate.eachByte(4096) { byte[] bytes, int size ->
|
||||
md.update(bytes, 0, size)
|
||||
}
|
||||
return md.digest().collect(this.&toHex).join('')
|
||||
}
|
||||
File.metaClass.getSha1 = { !delegate.exists() ? null : delegate.sha1() }
|
||||
|
||||
File.metaClass.json = { -> new JsonSlurper().parseText(delegate.text) }
|
||||
File.metaClass.sha256 = { ->
|
||||
MessageDigest md = MessageDigest.getInstance('SHA-256')
|
||||
delegate.eachByte(4096) { byte[] bytes, int size ->
|
||||
md.update(bytes, 0, size)
|
||||
}
|
||||
return md.digest().collect(this.&toHex).join('')
|
||||
}
|
||||
File.metaClass.getSha256 = { !delegate.exists() ? null : delegate.sha256() }
|
||||
|
||||
File.metaClass.json = { -> new JsonSlurper().parseText(delegate.text) }
|
||||
File.metaClass.getJson = { return delegate.exists() ? new JsonSlurper().parse(delegate) : [:] }
|
||||
File.metaClass.setJson = { json -> delegate.text = new JsonBuilder(json).toPrettyString() }
|
||||
|
||||
Date.metaClass.iso8601 = { ->
|
||||
def format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
|
||||
def result = format.format(delegate)
|
||||
return result[0..21] + ':' + result[22..-1]
|
||||
}
|
||||
|
||||
|
||||
Date.metaClass.iso8601 = { ->
|
||||
var format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
|
||||
var result = format.format(delegate)
|
||||
return result[0..21] + ':' + result[22..-1]
|
||||
}
|
||||
|
||||
String.metaClass.rsplit = { String del, int limit = -1 ->
|
||||
def lst = new ArrayList()
|
||||
def x = 0, idx
|
||||
def tmp = delegate
|
||||
while ((idx = tmp.lastIndexOf(del)) != -1 && (limit == -1 || x++ < limit)) {
|
||||
var lst = new ArrayList<String>()
|
||||
int x = 0
|
||||
int idx
|
||||
String tmp = delegate
|
||||
while ((idx = tmp.lastIndexOf(del)) != -1 && (limit === -1 || x++ < limit)) {
|
||||
lst.add(0, tmp.substring(idx + del.length(), tmp.length()))
|
||||
tmp = tmp.substring(0, idx)
|
||||
}
|
||||
lst.add(0, tmp)
|
||||
return lst
|
||||
}
|
||||
}
|
||||
|
||||
public static String[] getClasspath(project, libs, artifact) {
|
||||
def ret = []
|
||||
artifactTree(project, artifact).each { key, lib ->
|
||||
libs[lib.name] = lib
|
||||
if (lib.name != artifact)
|
||||
ret.add(lib.name)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
public static def getArtifacts(project, config, classifiers) {
|
||||
def ret = [:]
|
||||
config.resolvedConfiguration.resolvedArtifacts.each {
|
||||
def art = [
|
||||
group: it.moduleVersion.id.group,
|
||||
name: it.moduleVersion.id.name,
|
||||
version: it.moduleVersion.id.version,
|
||||
classifier: it.classifier,
|
||||
extension: it.extension,
|
||||
file: it.file
|
||||
]
|
||||
def key = art.group + ':' + art.name
|
||||
def folder = "${art.group.replace('.', '/')}/${art.name}/${art.version}/"
|
||||
def filename = "${art.name}-${art.version}"
|
||||
if (art.classifier != null)
|
||||
filename += "-${art.classifier}"
|
||||
filename += ".${art.extension}"
|
||||
def path = "${folder}${filename}"
|
||||
def url = "https://libraries.minecraft.net/${path}"
|
||||
if (!checkExists(url)) {
|
||||
url = "https://maven.minecraftforge.net/${path}"
|
||||
}
|
||||
//TODO remove when Mojang launcher is updated
|
||||
if (!classifiers && art.classifier != null) {
|
||||
//Mojang launcher doesn't currently support classifiers, so... move it to part of the version, and force the extension to 'jar'
|
||||
// However, keep the path normal so that our mirror system works.
|
||||
art.version = "${art.version}-${art.classifier}"
|
||||
art.classifier = null
|
||||
art.extension = 'jar'
|
||||
}
|
||||
ret[key] = [
|
||||
name: "${art.group}:${art.name}:${art.version}" + (art.classifier == null ? '' : ":${art.classifier}") + (art.extension == 'jar' ? '' : "@${art.extension}"),
|
||||
downloads: [
|
||||
artifact: [
|
||||
path: path,
|
||||
url: url,
|
||||
sha1: sha1(art.file),
|
||||
size: art.file.length()
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
public static def getMavenPath(task) {
|
||||
def classifier = task.archiveClassifier.get()
|
||||
def dep = "${task.project.group}:${task.project.name}:${task.project.version}" + (classifier == '' ? '' : ':' + classifier)
|
||||
return "${task.project.group.replace('.', '/')}/${task.project.name}/${task.project.version}/${task.project.name}-${task.project.version}".toString() + (classifier == '' ? '' : '-' + classifier) + '.jar'
|
||||
}
|
||||
|
||||
public static def getMavenDep(task) {
|
||||
def classifier = task.archiveClassifier.get()
|
||||
return "${task.project.group}:${task.project.name}:${task.project.version}" + (classifier == '' ? '' : ':' + classifier)
|
||||
}
|
||||
|
||||
public static def iso8601Now() { new Date().iso8601() }
|
||||
static String[] getClasspath(Project project, Map libs, String artifact) {
|
||||
def ret = []
|
||||
artifactTree(project, artifact).each { key, lib ->
|
||||
libs[lib.name] = lib
|
||||
if (lib.name != artifact)
|
||||
ret.add(lib.name)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
public static def sha1(file) {
|
||||
MessageDigest md = MessageDigest.getInstance('SHA-1')
|
||||
file.eachByte 4096, {bytes, size ->
|
||||
md.update(bytes, 0, size)
|
||||
}
|
||||
return md.digest().collect {String.format "%02x", it}.join()
|
||||
}
|
||||
@CompileStatic
|
||||
static Map getArtifacts(Configuration config) {
|
||||
var ret = [:]
|
||||
var semaphore = new Semaphore(1, true)
|
||||
config.resolvedConfiguration.resolvedArtifacts.parallelStream().forEachOrdered(dep -> {
|
||||
var info = getMavenInfoFromDep(dep)
|
||||
var domain = 'libraries.minecraft.net'
|
||||
var url = "https://$domain/$info.path"
|
||||
if (!checkExists(url))
|
||||
url.values[0] = 'maven.minecraftforge.net'
|
||||
|
||||
private static def artifactTree(project, artifact) {
|
||||
if (!project.ext.has('tree_resolver'))
|
||||
project.ext.tree_resolver = 1
|
||||
def cfg = project.configurations.create('tree_resolver_' + project.ext.tree_resolver++)
|
||||
def dep = project.dependencies.create(artifact)
|
||||
cfg.dependencies.add(dep)
|
||||
def files = cfg.resolve()
|
||||
return getArtifacts(project, cfg, true)
|
||||
}
|
||||
|
||||
private static boolean checkExists(url) {
|
||||
try {
|
||||
def code = new URL(url).openConnection().with {
|
||||
requestMethod = 'HEAD'
|
||||
connect()
|
||||
responseCode
|
||||
}
|
||||
return code == 200
|
||||
} catch (Exception e) {
|
||||
if (e.toString().contains('unable to find valid certification path to requested target'))
|
||||
throw new RuntimeException('Failed to connect to ' + url + ': Missing certificate root authority, try updating java')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
var sha1 = sha1(dep.file)
|
||||
|
||||
semaphore.acquire()
|
||||
ret[info.key] = [
|
||||
name: info.name,
|
||||
downloads: [
|
||||
artifact: [
|
||||
path: info.path,
|
||||
url: url.toString(),
|
||||
sha1: sha1,
|
||||
size: dep.file.length()
|
||||
]
|
||||
]
|
||||
]
|
||||
semaphore.release()
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
@CompileStatic
|
||||
static Map getMavenInfoFromDep(ResolvedArtifact dep) {
|
||||
return getMavenInfoFromMap([
|
||||
group: dep.moduleVersion.id.group,
|
||||
name: dep.moduleVersion.id.name,
|
||||
version: dep.moduleVersion.id.version,
|
||||
classifier: dep.classifier,
|
||||
extension: dep.extension
|
||||
])
|
||||
}
|
||||
|
||||
@CompileStatic
|
||||
static Map getMavenInfoFromTask(AbstractArchiveTask task) {
|
||||
return getMavenInfoFromMap([
|
||||
group: task.project.group.toString(),
|
||||
name: task.project.name,
|
||||
version: task.project.version.toString(),
|
||||
classifier: task.archiveClassifier.get(),
|
||||
extension: task.archiveExtension.get()
|
||||
])
|
||||
}
|
||||
|
||||
@CompileStatic
|
||||
static Map getMavenInfoFromTask(Task task, String classifier) {
|
||||
return getMavenInfoFromMap([
|
||||
group: task.project.group.toString(),
|
||||
name: task.project.name,
|
||||
version: task.project.version.toString(),
|
||||
classifier: classifier,
|
||||
extension: 'jar'
|
||||
])
|
||||
}
|
||||
|
||||
@CompileStatic
|
||||
private static Map getMavenInfoFromMap(Map<String, String> art) {
|
||||
var key = "$art.group:$art.name"
|
||||
var name = "$art.group:$art.name:$art.version"
|
||||
var path = "${art.group.replace('.', '/')}/$art.name/$art.version/$art.name-$art.version"
|
||||
if (art.classifier !== null) {
|
||||
name += ":$art.classifier"
|
||||
path += "-$art.classifier"
|
||||
}
|
||||
if ('jar' != art.extension) {
|
||||
name += "@$art.extension"
|
||||
path += ".$art.extension"
|
||||
} else {
|
||||
path += ".jar"
|
||||
}
|
||||
return [
|
||||
key: key.toString(),
|
||||
name: name.toString(),
|
||||
path: path.toString(),
|
||||
art: art
|
||||
]
|
||||
}
|
||||
|
||||
static String iso8601Now() { new Date().iso8601() }
|
||||
|
||||
@CompileStatic
|
||||
static String sha1(File file) {
|
||||
MessageDigest md = MessageDigest.getInstance('SHA-1')
|
||||
file.eachByte(4096) { byte[] bytes, int size ->
|
||||
md.update(bytes, 0, size)
|
||||
}
|
||||
return md.digest().collect(this.&toHex).join('')
|
||||
}
|
||||
|
||||
@CompileStatic
|
||||
private static String toHex(byte bite) {
|
||||
return String.format('%02x', bite)
|
||||
}
|
||||
|
||||
private static Map artifactTree(Project project, String artifact, boolean transitive = true) {
|
||||
if (!project.ext.has('tree_resolver'))
|
||||
project.ext.tree_resolver = 1
|
||||
def cfg = project.configurations.create('tree_resolver_' + project.ext.tree_resolver++)
|
||||
cfg.transitive = transitive
|
||||
def dep = project.dependencies.create(artifact)
|
||||
cfg.dependencies.add(dep)
|
||||
def files = cfg.resolve()
|
||||
return getArtifacts(cfg)
|
||||
}
|
||||
|
||||
@CompileStatic
|
||||
static boolean checkExists(String url) {
|
||||
try {
|
||||
return HTTP.send(HttpRequest.newBuilder(new URI(url))
|
||||
.method('HEAD', HttpRequest.BodyPublishers.noBody()).build(), HttpResponse.BodyHandlers.discarding()
|
||||
).statusCode() === 200
|
||||
} catch (Exception e) {
|
||||
if (e.toString().contains('unable to find valid certification path to requested target'))
|
||||
throw new RuntimeException("Failed to connect to $url: Missing certificate root authority, try updating Java")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
static String getLatestForgeVersion(String mcVersion) {
|
||||
final json = new JsonSlurper().parseText(new URL('https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json').getText('UTF-8'))
|
||||
final ver = json.promos["$mcVersion-latest"]
|
||||
ver === null ? null : (mcVersion + '-' + ver)
|
||||
}
|
||||
|
||||
@CompileStatic
|
||||
static void processClassNodes(File file, @ClosureParams(value = SimpleType, options = 'org.objectweb.asm.tree.ClassNode') Closure process) {
|
||||
file.withInputStream { i ->
|
||||
new ZipInputStream(i).withCloseable { zin ->
|
||||
ZipEntry zein
|
||||
while ((zein = zin.nextEntry) !== null) {
|
||||
if (zein.name.endsWith('.class')) {
|
||||
var node = new ClassNode(ASM_LEVEL)
|
||||
new ClassReader(zin).accept(node, 0)
|
||||
process(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import net.minecraftforge.srgutils.MinecraftVersion
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.objectweb.asm.tree.AnnotationNode
|
||||
import org.objectweb.asm.tree.ClassNode
|
||||
|
||||
abstract class ValidateDeprecations extends DefaultTask {
|
||||
@InputFile
|
||||
abstract RegularFileProperty getInput()
|
||||
|
||||
@Input
|
||||
abstract Property<String> getMcVersion()
|
||||
|
||||
ValidateDeprecations() {
|
||||
this.onlyIf { !System.env.TEAMCITY_VERSION }
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
protected void exec() {
|
||||
var mcVer = MinecraftVersion.from(mcVersion.get())
|
||||
List<String> errors = []
|
||||
|
||||
Util.processClassNodes(input.get().asFile) {
|
||||
processNode(mcVer, errors, it)
|
||||
}
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
errors.forEach {
|
||||
this.logger.error("Deprecated ${it[0]} is marked for removal in ${it[1]} but is not yet removed")
|
||||
}
|
||||
throw new IllegalStateException("Found deprecated members marked for removal but not yet removed in ${mcVer}; see log for details")
|
||||
}
|
||||
}
|
||||
|
||||
protected processNode(MinecraftVersion mcVer, List<String> errors, ClassNode node) {
|
||||
node.visibleAnnotations?.each { annotation ->
|
||||
ValidateDeprecations.processAnnotations(annotation, mcVer, errors) {
|
||||
"class ${node.name}"
|
||||
}
|
||||
}
|
||||
node.fields?.each { field ->
|
||||
field.visibleAnnotations?.each { annotation ->
|
||||
ValidateDeprecations.processAnnotations(annotation, mcVer, errors) {
|
||||
"field ${node.name}#${field.name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
node.methods?.each { method ->
|
||||
method.visibleAnnotations?.each { annotation ->
|
||||
ValidateDeprecations.processAnnotations(annotation, mcVer, errors) {
|
||||
"method ${node.name}#${method.name}${method.desc}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void processAnnotations(AnnotationNode annotation, MinecraftVersion mcVer, List<String> errors, Closure<String> context) {
|
||||
def values = annotation.values
|
||||
if (values === null)
|
||||
return
|
||||
int forRemoval = values.indexOf('forRemoval')
|
||||
int since = values.indexOf('since')
|
||||
if (annotation.desc == 'Ljava/lang/Deprecated;' && forRemoval !== -1 && since !== -1 && values.size() >= 4 && values[forRemoval + 1] === true) {
|
||||
def oldVersion = MinecraftVersion.from(values[since + 1])
|
||||
int[] split = ValidateDeprecations.splitDots(oldVersion.toString())
|
||||
if (split.length < 2)
|
||||
return
|
||||
def removeVersion = MinecraftVersion.from("${split[0]}.${split[1] + 1}")
|
||||
if (removeVersion <= mcVer)
|
||||
errors.add([context(), removeVersion])
|
||||
}
|
||||
}
|
||||
|
||||
@CompileStatic
|
||||
private static int[] splitDots(String version) {
|
||||
String[] pts = version.split('\\.')
|
||||
int[] values = new int[pts.length]
|
||||
for (int x = 0; x < pts.length; x++)
|
||||
values[x] = Integer.parseInt(pts[x])
|
||||
return values
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
package net.minecraftforge.forge.tasks.checks
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.TupleConstructor
|
||||
import net.minecraftforge.forge.tasks.InheritanceData
|
||||
import net.minecraftforge.srgutils.IMappingFile
|
||||
import org.gradle.api.file.ConfigurableFileCollection
|
||||
import org.gradle.api.file.RegularFile
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.InputFiles
|
||||
import org.gradle.api.tasks.Optional
|
||||
import org.objectweb.asm.Opcodes
|
||||
|
||||
@CompileStatic
|
||||
abstract class CheckATs extends CheckTask {
|
||||
|
||||
@InputFile abstract RegularFileProperty getInheritance()
|
||||
@InputFiles abstract ConfigurableFileCollection getAts()
|
||||
@InputFile @Optional abstract RegularFileProperty getMappings()
|
||||
|
||||
@Override
|
||||
void check(Reporter reporter, boolean fix) {
|
||||
final IMappingFile mappings = mappings.map { RegularFile it -> IMappingFile.load(it.asFile) }.getOrNull()
|
||||
final inheritance = InheritanceData.parse(this.inheritance.get().asFile)
|
||||
|
||||
ats.each {
|
||||
final lines = process(it, reporter, inheritance)
|
||||
if (fix) {
|
||||
it.text = joinBack(lines, inheritance, mappings).join('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static TreeMap<String, ATParser.Entry> process(File file, Reporter reporter, Map<String, InheritanceData> inheritance) {
|
||||
final TreeMap<String, ATParser.Entry> lines = ATParser.parse(file.readLines(), reporter)
|
||||
final Map<String, ATParser.Entry> constructorGroups = [:]
|
||||
|
||||
final itr = lines.entrySet().iterator()
|
||||
final toRemove = []
|
||||
while (itr.hasNext()) {
|
||||
final next = itr.next()
|
||||
String key = next.key
|
||||
final entry = next.value
|
||||
if (entry === null) continue
|
||||
|
||||
final binaryName = entry.cls.replaceAll('\\.', '/')
|
||||
|
||||
// Process Groups, this will remove any entries outside the group that is covered by the group
|
||||
if (entry.group) {
|
||||
final jcls = inheritance[binaryName]
|
||||
if (jcls === null) {
|
||||
itr.remove()
|
||||
reporter.report("Invalid group: $key")
|
||||
} else if ('*' == entry.desc) {
|
||||
if (!jcls.fields) {
|
||||
itr.remove()
|
||||
reporter.report("Invalid group, class has no fields: $key")
|
||||
} else {
|
||||
jcls.fields.each { field, value ->
|
||||
final fkey = entry.cls + ' ' + field
|
||||
if (accessLevel(value.access) < accessStr(entry.modifier)) {
|
||||
if (lines.containsKey(fkey)) {
|
||||
toRemove.add(fkey)
|
||||
} else if (!entry.existing.contains(fkey)) {
|
||||
reporter.report("Missing group entry: $fkey")
|
||||
}
|
||||
entry.children.add(fkey)
|
||||
} else if (lines.containsKey(fkey)) {
|
||||
toRemove.add(fkey)
|
||||
reporter.report("Found invalid group entry: $fkey")
|
||||
}
|
||||
}
|
||||
entry.existing.findAll { it !in entry.children }.each { println('Removed: ' + it) }
|
||||
}
|
||||
} else if ('*()' == entry.desc) {
|
||||
if (!jcls.methods) {
|
||||
itr.remove()
|
||||
reporter.report("Invalid group, class has no methods: $key")
|
||||
} else {
|
||||
jcls.methods.each { mtd, value ->
|
||||
if (mtd.startsWith('<clinit>') || mtd.startsWith('lambda$')) return
|
||||
key = entry.cls + ' ' + mtd.replace(' ', '')
|
||||
if (accessLevel(value.access) < accessStr(entry.modifier)) {
|
||||
if (lines.containsKey(key)) {
|
||||
toRemove.add(key)
|
||||
} else if (!entry.existing.contains(key)) {
|
||||
reporter.report("Missing group entry: $key")
|
||||
}
|
||||
entry.children.add(key)
|
||||
} else if (lines.containsKey(key)) {
|
||||
toRemove.add(key)
|
||||
reporter.report("Found invalid group entry: $key")
|
||||
}
|
||||
}
|
||||
entry.existing.findAll { it !in entry.children }.each { println('Removed: ' + it) }
|
||||
}
|
||||
} else if ('<init>' == entry.desc) { //Make all public non-abstract subclasses
|
||||
constructorGroups.put(binaryName, entry)
|
||||
}
|
||||
}
|
||||
|
||||
// Process normal lines, remove invalid and remove narrowing
|
||||
else {
|
||||
def jcls = inheritance.get(binaryName)
|
||||
if (jcls === null) {
|
||||
itr.remove()
|
||||
reporter.report("Invalid: $key")
|
||||
} else if (entry.desc == '') {
|
||||
if (accessLevel(jcls.access) > accessStr(entry.modifier) && (entry.comment === null || !entry.comment.startsWith('#force '))) {
|
||||
itr.remove()
|
||||
reporter.report("Invalid Narrowing: $key")
|
||||
}
|
||||
} else if (!entry.desc.contains('(')) {
|
||||
if (!jcls.fields || !jcls.fields.containsKey(entry.desc)) {
|
||||
itr.remove()
|
||||
reporter.report("Invalid: $key")
|
||||
} else {
|
||||
final value = jcls.fields[entry.desc]
|
||||
if (accessLevel(value.access) > accessStr(entry.modifier) && (entry.comment === null || !entry.comment.startsWith('#force '))) {
|
||||
itr.remove()
|
||||
reporter.report("Invalid Narrowing: $key - ${entry.comment}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final jdesc = entry.desc.replace('(', ' (')
|
||||
if (!jcls.methods || !jcls.methods.containsKey(jdesc)) {
|
||||
itr.remove()
|
||||
reporter.report("Invalid: $key")
|
||||
} else {
|
||||
final value = jcls.methods[jdesc]
|
||||
if (accessLevel(value.access) > accessStr(entry.modifier) && (entry.comment === null || !entry.comment.startsWith('#force '))) {
|
||||
itr.remove()
|
||||
reporter.report("Invalid Narrowing: $key")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inheritance.each { tcls, value ->
|
||||
if (!value.methods || ((value.access & Opcodes.ACC_ABSTRACT) !== 0)) return
|
||||
String parent = tcls
|
||||
while (parent !== null) {
|
||||
constructorGroups[parent]?.tap { entry ->
|
||||
value.methods.each { mtd, v ->
|
||||
if (mtd.startsWith('<init>')) {
|
||||
final child = tcls.replaceAll('/', '\\.') + ' ' + mtd.replace(' ', '')
|
||||
if (accessLevel(v.access) < 3) {
|
||||
if (lines.containsKey(child)) {
|
||||
toRemove.add(child)
|
||||
} else if (child !in entry.existing) {
|
||||
reporter.report("Missing group entry: $child")
|
||||
}
|
||||
entry.children.add(child)
|
||||
} else if (lines.containsKey(child)) {
|
||||
toRemove.add(child)
|
||||
reporter.report("Found invalid group entry: $child")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
parent = inheritance[parent]?.superName
|
||||
}
|
||||
}
|
||||
constructorGroups.values().each { entry -> entry.existing.findAll { it !in entry.children }.each{ reporter.report("Found invalid group entry: $it") } }
|
||||
|
||||
toRemove.each(lines.&remove)
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
private static List<String> joinBack(TreeMap<String, ATParser.Entry> lines, Map<String, InheritanceData> inheritance, IMappingFile mappings) {
|
||||
final data = [] as List<String>
|
||||
final remapComment = { ATParser.Entry entry ->
|
||||
if (!mappings || !entry || !entry.desc) return null
|
||||
final comment = entry.comment?.substring(1)?.trim()
|
||||
final jsonCls = inheritance.get(entry.cls.replaceAll('\\.', '/'))
|
||||
final mappingsClass = mappings?.getClass(jsonCls.name)
|
||||
if (mappingsClass === null) return entry.comment
|
||||
final idx = entry.desc.indexOf('(')
|
||||
|
||||
String mappedName = idx == -1
|
||||
? mappingsClass.remapField(entry.desc)
|
||||
: mappingsClass.remapMethod(entry.desc.substring(0, idx), entry.desc.substring(idx))
|
||||
if (!mappedName) return entry.comment
|
||||
if (mappedName == '<init>')
|
||||
mappedName = 'constructor'
|
||||
|
||||
if (comment?.startsWith(mappedName))
|
||||
return '# ' + comment
|
||||
if (comment && comment.indexOf(' ') !== -1) {
|
||||
def split = comment.split(' - ').toList()
|
||||
if (split[0].indexOf(' ') !== -1)
|
||||
// The first string is more than one word, so append before it
|
||||
return "# ${mappedName} - ${comment}"
|
||||
split.remove(0)
|
||||
return "# ${mappedName} - ${String.join(' - ', split)}"
|
||||
}
|
||||
return '# ' + mappedName
|
||||
}
|
||||
lines.each { key, value ->
|
||||
if (!value.group) {
|
||||
def comment = remapComment.call(value)
|
||||
data.add(value.modifier + ' ' + key + (comment ? ' ' + comment : ''))
|
||||
} else {
|
||||
data.add(('#group ' + value.modifier + ' ' + key + ' ' + (value.comment ?: '')).trim())
|
||||
value.children.each {
|
||||
final line = value.modifier + ' ' + it
|
||||
final entry = ATParser.parseEntry(line)
|
||||
final comment = remapComment(entry)
|
||||
data.add(line + (comment ? ' ' + comment : ''))
|
||||
}
|
||||
data.add('#endgroup')
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
static int accessStr(String access) {
|
||||
if (access.endsWith('-f') || access.endsWith('+f')) return 4
|
||||
switch (access.toLowerCase()) {
|
||||
case 'public': return 3
|
||||
case 'protected': return 2
|
||||
case 'default': return 1
|
||||
case 'private': return 0
|
||||
default: return -1
|
||||
}
|
||||
}
|
||||
|
||||
static int accessLevel(int access) {
|
||||
if ((access & Opcodes.ACC_PUBLIC) !== 0) return 3
|
||||
if ((access & Opcodes.ACC_PROTECTED) !== 0) return 2
|
||||
if ((access & Opcodes.ACC_PRIVATE) !== 0) return 0
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
@CompileStatic
|
||||
class ATParser {
|
||||
static TreeMap<String, Entry> parse(List<String> lines, CheckTask.Reporter reporter) {
|
||||
TreeMap<String, Entry> outLines = new TreeMap<>()
|
||||
Entry group = null
|
||||
for (final line : lines) {
|
||||
if (line.isEmpty()) continue
|
||||
if (line.startsWith('#group ')) {
|
||||
final entry = parseEntry(line.substring(7))
|
||||
|
||||
if (entry.desc != '*' && entry.desc != '*()' && entry.desc != '<init>') {
|
||||
reporter.report("Invalid group: $line", false)
|
||||
}
|
||||
|
||||
entry.group = true
|
||||
entry.children = []
|
||||
entry.existing = []
|
||||
|
||||
group = entry
|
||||
|
||||
if (outLines.containsKey(entry.key)) {
|
||||
reporter.report("Duplicate group: $line", false)
|
||||
}
|
||||
|
||||
outLines[entry.key] = group
|
||||
} else if (group !== null) {
|
||||
if (line.startsWith('#endgroup')) {
|
||||
group = null
|
||||
} else {
|
||||
final key = parseEntry(line).key
|
||||
group.existing.add(key)
|
||||
}
|
||||
} else if (line.startsWith('#endgroup')) {
|
||||
reporter.report("Invalid group ending: $line", false)
|
||||
} else if (line.startsWith('#')) {
|
||||
//Nom
|
||||
} else {
|
||||
final entry = parseEntry(line)
|
||||
if (outLines.containsKey(entry.key)) {
|
||||
reporter.report("Found duplicate: $line")
|
||||
continue
|
||||
}
|
||||
outLines[entry.key] = entry
|
||||
}
|
||||
}
|
||||
return outLines
|
||||
}
|
||||
|
||||
static Entry parseEntry(String line) {
|
||||
final idx = line.indexOf('#')
|
||||
final String comment = idx === -1 ? null : line.substring(idx)
|
||||
if (idx !== -1) line = line.substring(0, idx - 1)
|
||||
final data = (line.trim() + ' ').split(' ', -1)
|
||||
new Entry(data[0], data[1], data[2], comment)
|
||||
}
|
||||
|
||||
@TupleConstructor
|
||||
static final class Entry {
|
||||
String modifier, cls, desc, comment
|
||||
|
||||
Set<String> existing
|
||||
TreeSet<String> children
|
||||
boolean group = false
|
||||
|
||||
@Lazy
|
||||
String key = {cls + (desc.isEmpty() ? '' : ' ' + desc)}()
|
||||
|
||||
Object getAt(String key) {
|
||||
return getProperty(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package net.minecraftforge.forge.tasks.checks
|
||||
|
||||
import net.minecraftforge.forge.tasks.Util
|
||||
import org.gradle.api.file.ConfigurableFileCollection
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.InputFiles
|
||||
import org.objectweb.asm.ClassReader
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.Type
|
||||
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
abstract class CheckExcs extends CheckTask {
|
||||
|
||||
@InputFile abstract RegularFileProperty getBinary()
|
||||
@InputFiles abstract ConfigurableFileCollection getExcs()
|
||||
|
||||
@Override
|
||||
void check(Reporter reporter, boolean fix) {
|
||||
final Set<String> known = []
|
||||
collectKnown(known)
|
||||
|
||||
excs.each { f ->
|
||||
final lines = []
|
||||
f.eachLine { line ->
|
||||
int idx = line.indexOf('#')
|
||||
if (idx === 0 || line.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (idx !== -1) line = line.substring(0, idx - 1)
|
||||
|
||||
if (!line.contains('=')) {
|
||||
reporter.report("Invalid: $line")
|
||||
return
|
||||
}
|
||||
|
||||
def (String key, String value) = line.split('=', 2)
|
||||
if (!known.contains(key)) {
|
||||
reporter.report("Unknown: $line")
|
||||
return
|
||||
}
|
||||
|
||||
String desc = key.split('\\.', 2)[1]
|
||||
if (!desc.contains('(')) {
|
||||
reporter.report("Invalid: $line")
|
||||
return
|
||||
}
|
||||
desc = '(' + desc.split('\\(', 2)[1]
|
||||
|
||||
def (exceptions, String args) = value.contains('|') ? value.split('|', 2) : [value, '']
|
||||
|
||||
if (args.split(',').length !== Type.getArgumentTypes(desc).length) {
|
||||
reporter.report("Invalid: $line")
|
||||
return
|
||||
}
|
||||
lines.add(line)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (fix) f.text = lines.sort().join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
private void collectKnown(Collection<String> known) {
|
||||
binary.get().asFile.withInputStream { i ->
|
||||
new ZipInputStream(i).withCloseable { zin ->
|
||||
final visitor = new ClassVisitor(Util.ASM_LEVEL) {
|
||||
private String cls
|
||||
@Override
|
||||
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
|
||||
this.cls = name
|
||||
}
|
||||
|
||||
@Override
|
||||
MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
|
||||
known.add(this.cls + '.' + name + descriptor)
|
||||
super.visitMethod(access, name, descriptor, signature, exceptions)
|
||||
}
|
||||
}
|
||||
ZipEntry zein
|
||||
while ((zein = zin.nextEntry) !== null) {
|
||||
if (zein.name.endsWith('.class')) {
|
||||
ClassReader reader = new ClassReader(zin)
|
||||
reader.accept(visitor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package net.minecraftforge.forge.tasks.checks
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
|
||||
@CompileStatic
|
||||
enum CheckMode {
|
||||
CHECK,
|
||||
FIX
|
||||
|
||||
CheckMode() {}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package net.minecraftforge.forge.tasks.checks
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.provider.ListProperty
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.InputDirectory
|
||||
import org.gradle.api.tasks.Optional
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@CompileStatic
|
||||
abstract class CheckPatches extends CheckTask {
|
||||
|
||||
private static final Pattern HUNK_START_PATTERN = Pattern.compile('^@@ -[0-9,]* \\+[0-9,_]* @@$')
|
||||
private static final Pattern WHITESPACE_PATTERN = Pattern.compile('^[+\\-]\\s*$')
|
||||
private static final Pattern IMPORT_PATTERN = Pattern.compile('^[+\\-]\\s*import.*')
|
||||
private static final Pattern FIELD_PATTERN = Pattern.compile('^[+\\-][\\s]*((public|protected|private)[\\s]*)?(static[\\s]*)?(final)?([^=;]*)(=.*)?;\\s*$')
|
||||
private static final Pattern METHOD_PATTERN = Pattern.compile('^[+\\-][\\s]*((public|protected|private)[\\s]*)?(static[\\s]*)?(final)?([^(]*)[(]([^)]*)?[)]\\s*[{]\\s*$')
|
||||
private static final Pattern CLASS_PATTERN = Pattern.compile('^[+\\-][\\s]*((public|protected|private)[\\s]*)?(static[\\s]*)?(final[\\s]*)?(class|interface)([^{]*)[{]\\s*$')
|
||||
private static final Map<String, Integer> ACCESS_MAP = [private: 0, protected: 2, public: 3].tap { it.put(null, 1) }
|
||||
|
||||
@InputDirectory abstract DirectoryProperty getPatchDir()
|
||||
@Input @Optional abstract ListProperty<String> getPatchesWithS2SArtifact()
|
||||
|
||||
@Override
|
||||
void check(Reporter reporter, boolean fix) {
|
||||
final patchDir = getPatchDir().get().asFile.toPath()
|
||||
Files.walk(patchDir).withCloseable {
|
||||
it.filter(Files.&isRegularFile).forEach { path ->
|
||||
final String relativeName = patchDir.relativize(path).toString()
|
||||
verifyPatch(path, reporter, fix, relativeName, patchesWithS2SArtifact.get().contains(relativeName.replace('\\', '/')))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean accessChange(String previous, String current) {
|
||||
//return ACCESS_MAP[previous] < ACCESS_MAP[current]
|
||||
return previous != current
|
||||
}
|
||||
|
||||
void verifyPatch(Path patch, Reporter reporter, boolean fix, String patchPath, boolean hasS2SArtifact) {
|
||||
int oldFixedErrors = reporter.fixed.size()
|
||||
|
||||
final lines = Files.readAllLines(patch)
|
||||
|
||||
int hunksStart = 0
|
||||
boolean onlyWhiteSpace = false
|
||||
|
||||
final List<String> newLines = []
|
||||
|
||||
// First two lines are file name ++/-- and we do not care
|
||||
newLines.add(lines[0] + '\n')
|
||||
newLines.add(lines[1] + '\n')
|
||||
|
||||
int i
|
||||
for (i = 2; i < lines.size(); ++i) {
|
||||
def line = lines[i]
|
||||
newLines.add(line + '\n')
|
||||
|
||||
if (HUNK_START_PATTERN.matcher(line).find()) {
|
||||
if (onlyWhiteSpace) {
|
||||
if (!hasS2SArtifact)
|
||||
reporter.report("Patch contains only white space hunk starting at line ${hunksStart + 1}, file: $patchPath")
|
||||
int toRemove = i - hunksStart
|
||||
while (toRemove-- > 0)
|
||||
newLines.remove(newLines.size() - 1)
|
||||
}
|
||||
hunksStart = i
|
||||
onlyWhiteSpace = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.startsWithAny('+','-')) {
|
||||
def prefixChange = false
|
||||
def prevLine = lines[i - 1]
|
||||
|
||||
if (line.charAt(0) == (char)'+' && prevLine.charAt(0) == (char)'-') {
|
||||
def prevTrim = prevLine.substring(1).replaceAll("\\s", "")
|
||||
def currTrim = line.substring(1).replaceAll("\\s", "")
|
||||
|
||||
if (prevTrim == currTrim) {
|
||||
prefixChange = true
|
||||
}
|
||||
|
||||
def pMatcher = FIELD_PATTERN.matcher(prevLine)
|
||||
def cMatcher = FIELD_PATTERN.matcher(line)
|
||||
|
||||
if (pMatcher.find() && cMatcher.find() &&
|
||||
pMatcher.group(6) == cMatcher.group(6) && // = ...
|
||||
pMatcher.group(5) == cMatcher.group(5) && // field name
|
||||
pMatcher.group(3) == cMatcher.group(3) && // static
|
||||
(accessChange(pMatcher.group(2), cMatcher.group(2)) || pMatcher.group(4) != cMatcher.group(4))) {
|
||||
reporter.report("Patch contains access changes or final removal at line ${i + 1}, file: $patchPath", false)
|
||||
}
|
||||
|
||||
pMatcher = METHOD_PATTERN.matcher(prevLine)
|
||||
cMatcher = METHOD_PATTERN.matcher(line)
|
||||
|
||||
if (pMatcher.find() && cMatcher.find() &&
|
||||
pMatcher.group(6) == cMatcher.group(6) && // params
|
||||
pMatcher.group(5) == cMatcher.group(5) && // <T> void name
|
||||
pMatcher.group(3) == cMatcher.group(3) && // static
|
||||
(accessChange(pMatcher.group(2), cMatcher.group(2)) || pMatcher.group(4) != cMatcher.group(4))) {
|
||||
reporter.report("Patch contains access changes or final removal at line ${i + 1}, file: $patchPath", false)
|
||||
}
|
||||
|
||||
pMatcher = CLASS_PATTERN.matcher(prevLine)
|
||||
cMatcher = CLASS_PATTERN.matcher(line)
|
||||
|
||||
if (pMatcher.find() && cMatcher.find() &&
|
||||
pMatcher.group(6) == cMatcher.group(6) && // ClassName<> extends ...
|
||||
pMatcher.group(5) == cMatcher.group(5) && // class | interface
|
||||
pMatcher.group(3) == cMatcher.group(3) && // static
|
||||
(accessChange(pMatcher.group(2), cMatcher.group(2)) || pMatcher.group(4) != cMatcher.group(4))) {
|
||||
reporter.report("Patch contains access changes or final removal at line ${i + 1}, file: $patchPath", false)
|
||||
}
|
||||
}
|
||||
|
||||
if (line.charAt(0) == (char)'-' && i + 1 < lines.size()) {
|
||||
final nextLine = lines[i + 1]
|
||||
if (nextLine.charAt(0) == (char)'+') {
|
||||
final nextTrim = nextLine.substring(1).replaceAll("\\s", "")
|
||||
final currTrim = line.substring(1).replaceAll("\\s", "")
|
||||
|
||||
if (nextTrim == currTrim) {
|
||||
prefixChange = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final isWhiteSpaceChange = WHITESPACE_PATTERN.matcher(line).find()
|
||||
|
||||
if (!prefixChange && !isWhiteSpaceChange) {
|
||||
onlyWhiteSpace = hasS2SArtifact && IMPORT_PATTERN.matcher(line).find()
|
||||
} else if (isWhiteSpaceChange) {
|
||||
final prevLineChange = prevLine.startsWithAny('+','-')
|
||||
final nextLineChange = i + 1 < lines.size() && lines[i + 1].startsWithAny('+','-')
|
||||
|
||||
if (!prevLineChange && !nextLineChange) {
|
||||
reporter.report("Patch contains white space change in valid hunk at line ${i + 1}, file: $patchPath \n$prevLine\n$line\n${lines[i+1]}", false)
|
||||
}
|
||||
}
|
||||
|
||||
if (line.contains('\t')) {
|
||||
reporter.report("Patch contains tabs on line ${i + 1}, file: $patchPath")
|
||||
line = line.replaceAll('\t', ' ')
|
||||
newLines.remove(newLines.size() - 1)
|
||||
newLines.add(line + '\n')
|
||||
}
|
||||
|
||||
if (IMPORT_PATTERN.matcher(line).find() && !hasS2SArtifact) {
|
||||
reporter.report("Patch contains import change on line ${i + 1}, file: $patchPath", false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (onlyWhiteSpace) {
|
||||
if (!hasS2SArtifact)
|
||||
reporter.report("Patch contains only white space hunk starting at line ${hunksStart + 1}, file: $patchPath")
|
||||
def toRemove = i - hunksStart;
|
||||
while (toRemove-- > 0)
|
||||
newLines.remove(newLines.size() - 1)
|
||||
}
|
||||
|
||||
if ((reporter.fixed.size() > oldFixedErrors && fix) || hasS2SArtifact) {
|
||||
if (newLines.size() <= 2) {
|
||||
logger.lifecycle("Patch is now empty removing, file: {}", patchPath)
|
||||
Files.delete(patch)
|
||||
}
|
||||
else {
|
||||
if (!hasS2SArtifact)
|
||||
logger.lifecycle("*** Updating patch file. Please run setup then genPatches again. ***")
|
||||
Files.newBufferedWriter(patch).withCloseable {
|
||||
newLines.each { l -> it.write(l) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package net.minecraftforge.forge.tasks.checks
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import net.minecraftforge.forge.tasks.Annotatable
|
||||
import net.minecraftforge.forge.tasks.InheritanceData
|
||||
import org.gradle.api.file.ConfigurableFileCollection
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.InputFiles
|
||||
|
||||
@CompileStatic
|
||||
abstract class CheckSAS extends CheckTask {
|
||||
|
||||
@InputFile abstract RegularFileProperty getInheritance()
|
||||
@InputFiles abstract ConfigurableFileCollection getSass()
|
||||
|
||||
@Override
|
||||
void check(Reporter reporter, boolean fix) {
|
||||
final inheritance = InheritanceData.parse(this.inheritance.get().asFile)
|
||||
|
||||
sass.each { f ->
|
||||
final lines = []
|
||||
f.eachLine { line ->
|
||||
if (line[0] == '\t') return // Skip any tabbed lines, those are ones we add
|
||||
final idx = line.indexOf('#')
|
||||
if (idx == 0 || line.isEmpty()) {
|
||||
lines.add(line)
|
||||
return
|
||||
}
|
||||
def comment = idx == -1 ? null : line.substring(idx)
|
||||
if (idx != -1) line = line.substring(0, idx - 1)
|
||||
final spl = (line.trim().replace('(', ' (') + ' ').split(' ', -1)
|
||||
def (String cls, String name, String desc) = [spl[0], spl[1], spl[2]]
|
||||
cls = cls.replaceAll('\\.', '/')
|
||||
|
||||
if (inheritance[cls] === null) {
|
||||
reporter.report("Invalid: $line")
|
||||
} else if (name.isEmpty()) { //Class SAS
|
||||
final toAdd = []
|
||||
final clsSided = isSided(inheritance[cls])
|
||||
boolean sided = clsSided
|
||||
|
||||
/* TODO: MergeTool doesn't do fields
|
||||
if (json[cls]['fields'] != null) {
|
||||
for (entry in json[cls]['fields']) {
|
||||
if (isSided(entry.value)) {
|
||||
sided = true
|
||||
toAdd.add('\t' + cls + ' ' + entry.key)
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
final clsInh = inheritance[cls]
|
||||
if (clsInh.methods) {
|
||||
for (entry in clsInh.methods) {
|
||||
if (isSided(entry.value)) {
|
||||
sided = true
|
||||
toAdd.add('\t' + cls + ' ' + entry.key.replaceAll(' ', ''))
|
||||
findChildMethods(inheritance, cls, entry.key).each { lines.add('\t' + it) }
|
||||
findChildMethods(inheritance, cls, entry.key).each { println(line + ' -- ' + it) }
|
||||
} else if (clsSided) {
|
||||
findChildMethods(inheritance, cls, entry.key).each { lines.add('\t' + it) }
|
||||
findChildMethods(inheritance, cls, entry.key).each { println(line + ' -- ' + it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sided) {
|
||||
lines.add(cls + (comment == null ? '' : ' ' + comment))
|
||||
lines.addAll(toAdd.sort())
|
||||
} else {
|
||||
reporter.report("Invalid: $line")
|
||||
}
|
||||
|
||||
} else if (desc.isEmpty()) { // Fields
|
||||
/* TODO: MergeTool doesn't do fields
|
||||
if (json[cls]['fields'] != null && isSided(json[cls]['fields'][name]))
|
||||
lines.add(cls + ' ' + name + (comment == null ? '' : ' ' + comment))
|
||||
else */
|
||||
reporter.report("Invalid: $line")
|
||||
} else { // Methods
|
||||
final clsInh = inheritance[cls]
|
||||
if (clsInh.methods === null || !isSided(clsInh.methods[name + ' ' + desc]))
|
||||
reporter.report("Invalid: $line")
|
||||
else {
|
||||
lines.add(cls + ' ' + name + desc + (comment == null ? '' : ' ' + comment))
|
||||
findChildMethods(inheritance, cls, name + ' ' + desc).each { println(line + ' -- ' + it) }
|
||||
findChildMethods(inheritance, cls, name + ' ' + desc).each { lines.add('\t' + it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fix) f.text = lines.join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
protected static boolean isSided(Annotatable annotatable) {
|
||||
if (annotatable === null) return false
|
||||
for (ann in annotatable.annotations) {
|
||||
if ('Lnet/minecraftforge/api/distmarker/OnlyIn;' == ann.desc)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
protected static findChildMethods(Map<String, InheritanceData> json, String cls, String desc) {
|
||||
return json.values().findAll{ it.methods !== null && it.methods[desc] !== null && it.methods[desc].override == cls && isSided(it.methods[desc]) }
|
||||
.collect { it.name + ' ' + desc.replace(' ', '') } as TreeSet
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package net.minecraftforge.forge.tasks.checks
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import groovy.transform.stc.ClosureParams
|
||||
import groovy.transform.stc.ThirdParam
|
||||
import net.minecraftforge.forge.tasks.Util
|
||||
import org.gradle.api.Action
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.logging.LogLevel
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.gradle.api.tasks.TaskContainer
|
||||
import org.gradle.api.tasks.VerificationTask
|
||||
|
||||
@CompileStatic
|
||||
abstract class CheckTask extends DefaultTask implements VerificationTask {
|
||||
@Input
|
||||
abstract Property<CheckMode> getMode()
|
||||
|
||||
private boolean ignoreFailures = false
|
||||
|
||||
@Input
|
||||
@Override
|
||||
boolean getIgnoreFailures() {
|
||||
return ignoreFailures
|
||||
}
|
||||
|
||||
@Override
|
||||
void setIgnoreFailures(boolean ignoreFailures) {
|
||||
this.ignoreFailures = ignoreFailures
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
void run() {
|
||||
Util.init()
|
||||
|
||||
boolean doFix = getMode().get() === CheckMode.FIX
|
||||
final Reporter reporter = new Reporter(doFix)
|
||||
check(reporter, doFix)
|
||||
|
||||
if (reporter.messages) {
|
||||
if (getMode().get() === CheckMode.CHECK) {
|
||||
logger.error("Check task '{}' found errors:\n{}", name, reporter.messages.join('\n'))
|
||||
if (!ignoreFailures) {
|
||||
throw new IllegalArgumentException("${reporter.messages.size()} errors were found!")
|
||||
}
|
||||
} else {
|
||||
if (logger.isEnabled(LogLevel.DEBUG)) {
|
||||
logger.warn("Check task '{}' found {} errors and fixed {}:\n{}", name, reporter.messages.size(), reporter.fixed.size(), reporter.fixed.join('\n'))
|
||||
} else {
|
||||
logger.warn("Check task '{}' found {} errors and fixed {}.", name, reporter.messages.size(), reporter.fixed.size())
|
||||
}
|
||||
|
||||
if (reporter.notFixed) {
|
||||
logger.error('{} errors could not be fixed:\n{}', reporter.notFixed.size(), reporter.notFixed.join('\n'))
|
||||
if (!ignoreFailures) {
|
||||
throw new IllegalArgumentException("${reporter.notFixed.size()} errors which cannot be fixed were found!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract void check(Reporter reporter, boolean fix)
|
||||
|
||||
@CompileStatic
|
||||
static final class Reporter {
|
||||
final boolean trackFixed
|
||||
Reporter(boolean trackFixed) {
|
||||
this.trackFixed = trackFixed
|
||||
}
|
||||
|
||||
public final List<String> messages = []
|
||||
|
||||
public final List<String> fixed = []
|
||||
public final List<String> notFixed = []
|
||||
|
||||
void report(String message, boolean canBeFixed = true) {
|
||||
messages.add(message)
|
||||
|
||||
if (trackFixed) {
|
||||
if (canBeFixed) {
|
||||
fixed.add(message)
|
||||
} else {
|
||||
notFixed.add(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static <T extends CheckTask> void registerTask(TaskContainer tasks, String taskName, @DelegatesTo.Target('type') Class<T> clazz,
|
||||
@DelegatesTo(genericTypeIndex = 0, target = 'type')
|
||||
@ClosureParams(ThirdParam.FirstGenericType) Closure configuration) {
|
||||
taskName = taskName.capitalize()
|
||||
tasks.register("check$taskName", clazz) { CheckTask task ->
|
||||
def castedTask = task as T
|
||||
configuration.setDelegate(castedTask)
|
||||
configuration.call(castedTask)
|
||||
castedTask.mode.set(CheckMode.CHECK)
|
||||
castedTask.group = 'checks'
|
||||
}
|
||||
tasks.named('checkAll').configure { it.dependsOn("check$taskName") }
|
||||
|
||||
tasks.register("checkAndFix$taskName", clazz) { CheckTask task ->
|
||||
def castedTask = task as T
|
||||
configuration.setDelegate(castedTask)
|
||||
configuration.call(castedTask)
|
||||
castedTask.mode.set(CheckMode.FIX)
|
||||
castedTask.group = 'checks'
|
||||
}
|
||||
tasks.named('checkAllAndFix').configure { it.dependsOn("checkAndFix$taskName") }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'net.minecraftforge.gradle.patcher'
|
||||
}
|
||||
|
||||
evaluationDependsOn(':mcp')
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven gradleutils.forgeMaven
|
||||
maven gradleutils.minecraftLibsMaven
|
||||
}
|
||||
|
||||
java.toolchain.languageVersion = JavaLanguageVersion.of(JAVA_VERSION)
|
||||
|
||||
dependencies {
|
||||
implementation(libs.forgespi)
|
||||
}
|
||||
|
||||
patcher {
|
||||
parent = project(':mcp')
|
||||
mcVersion = MC_VERSION
|
||||
patchedSrc = file('src/main/java')
|
||||
|
||||
mappings channel: MAPPING_CHANNEL, version: MAPPING_VERSION
|
||||
|
||||
runs {
|
||||
clean_client {
|
||||
client true
|
||||
taskName 'clean_client'
|
||||
ideaModule "${rootProject.name}.${project.name}.main"
|
||||
|
||||
main 'net.minecraft.client.main.Main'
|
||||
workingDirectory project.file('run/client')
|
||||
|
||||
args '--gameDir', '.'
|
||||
args '--version', MC_VERSION
|
||||
args '--assetsDir', downloadAssets.output
|
||||
args '--assetIndex', '{asset_index}'
|
||||
args '--accessToken', '0'
|
||||
}
|
||||
|
||||
clean_server {
|
||||
client false
|
||||
taskName 'clean_server'
|
||||
ideaModule "${rootProject.name}.${project.name}.main"
|
||||
|
||||
main 'net.minecraft.server.Main'
|
||||
workingDirectory project.file('run/server')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation
|
||||
options.warnings = false // Shutup deprecated for removal warnings
|
||||
}
|
||||
|
||||
eclipse.classpath.file.whenMerged {
|
||||
// Disable optional warnings on the minecraft decompiled source code. It just muddies the warning window and hides warnings in our own codebase
|
||||
def src = entries.find { it.path == 'src/main/java' }
|
||||
if (src == null) throw new IllegalStateException("You must run `setup` task before importing")
|
||||
src.entryAttributes['ignore_optional_problems'] = 'true'
|
||||
}
|
||||
|
||||
tasks.named('genPatches').configure {
|
||||
onlyIf { false }
|
||||
}
|
||||
|
||||
tasks.named('extractRangeMap').configure {
|
||||
onlyIf { false }
|
||||
tool = 'net.minecraftforge:Srg2Source:8.1.0:fatjar'
|
||||
}
|
||||
+1070
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,85 @@
|
||||
import net.minecraftforge.forge.tasks.CleanProperties
|
||||
|
||||
apply plugin: 'eclipse'
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'net.minecraftforge.gradleutils'
|
||||
|
||||
group = 'net.minecraftforge'
|
||||
version = VERSION
|
||||
println("Version: $version")
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven gradleutils.forgeMaven
|
||||
maven gradleutils.minecraftLibsMaven
|
||||
//mavenLocal()
|
||||
}
|
||||
|
||||
tasks.withType(Javadoc).configureEach {
|
||||
options.tags = [
|
||||
'apiNote:a:<em>API Note:</em>',
|
||||
'implSpec:a:<em>Implementation Requirements:</em>',
|
||||
'implNote:a:<em>Implementation Note:</em>'
|
||||
]
|
||||
options.addStringOption('Xdoclint:all,-missing', '-public')
|
||||
}
|
||||
|
||||
// We need to write the manifest to the binary file so we have properly versioned packaged at dev time.
|
||||
tasks.register('writeManifest') {
|
||||
doLast {
|
||||
if (plugins.findPlugin('net.minecraftforge.gradle.patcher')) // Forge project
|
||||
universalJar.manifest.writeTo(rootProject.file('src/main/resources/META-INF/MANIFEST.MF'))
|
||||
else
|
||||
jar.manifest.writeTo(project.file('src/main/resources/META-INF/MANIFEST.MF'))
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('generateResources') {
|
||||
dependsOn('writeManifest')
|
||||
}
|
||||
|
||||
// Make sure out manifests get written before compiling the code, IDEA calls this task if you tell it to use the gradle build.
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
dependsOn 'generateResources'
|
||||
dependsOn 'processResources' // Needed because we merge the output of this with the output of the compile task. And gradle detects downstream tasks using the output without a hard dep
|
||||
options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation
|
||||
options.warnings = false // Shutup deprecated for removal warnings
|
||||
options.forkOptions.jvmArgs += '-Xmx3G' // Needed to make compiling faster, and not run out of heap space in some cases.
|
||||
}
|
||||
|
||||
// Merge the resources and classes into the same directory. We'll need to split them at runtime because
|
||||
// Minecraft and Forge are in the same sourceSet as they are inter dependent.. for now..
|
||||
sourceSets.each {
|
||||
def dir = layout.buildDirectory.dir("classes/java/$it.name")
|
||||
it.output.resourcesDir = dir
|
||||
it.java.destinationDirectory = dir
|
||||
}
|
||||
|
||||
tasks.register('copyEclipseSettings') {
|
||||
doLast {
|
||||
rootProject.fileTree('ide/eclipse/template/.settings/').matching { include '**/*.prefs' }.each { file ->
|
||||
def target = project.file('.settings/' + file.name)
|
||||
def temp = new CleanProperties().load(file)
|
||||
def exst = new CleanProperties().load(target)
|
||||
exst.put('eclipse.preferences.version', '1')
|
||||
exst.putAll(temp)
|
||||
exst.store(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: [Gradle][IntelliJ] Auto trigger these tasks on import.
|
||||
eclipse {
|
||||
// Run everytime eclipse builds the code
|
||||
//autoBuildTasks writeManifest
|
||||
// Run when importing the project
|
||||
synchronizationTasks generateResources, copyEclipseSettings, eclipseClasspath, eclipseProject
|
||||
}
|
||||
|
||||
idea {
|
||||
module {
|
||||
// IntelliJ IDEA does not do this by itself anymore...
|
||||
downloadJavadoc = true
|
||||
downloadSources = true
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
Contributing to Forge
|
||||
=====================
|
||||
|
||||
1) Keep patches to Minecraft classes minimal. If you need a lot of things done, you may either add to relevant forge classes or make a new class.
|
||||
1) Keep patches to Minecraft classes together. If you need a lot of things done, you may either add to relevant forge classes or make a new class. Try not to spread out your patches across multiple disjoint lines, as this makes maintenance of your patches difficult.
|
||||
|
||||
2) An example mod must be provided for all PRs adding events/hooks to Forge. Whether it's a full mod using your proposed hooks, or just a pastebin containing a simple example, an example mod is required. Please also explain in detail what your new hook does.
|
||||
2) TODO: Test Mods
|
||||
|
||||
3) Follow Forge coding guidelines (braces on newlines, spaces instead of tabs, etc)
|
||||
3) Follow the code style of the class you're working in (braces on newlines & spaces instead of tabs in Forge classes, inline brackets in patches, etc).
|
||||
|
||||
For more information, refer to [the wiki](https://github.com/MinecraftForge/MinecraftForge/wiki/If-you-want-to-contribute-to-Forge)
|
||||
For more information, refer to [the wiki][Wiki].
|
||||
|
||||
|
||||
Contributor License Agreement
|
||||
@@ -16,3 +16,5 @@ Contributor License Agreement
|
||||
- You assign copyright ownership of your contributions to the patches codebase (everything under patches) to Forge, where it will be licensed under the LGPLv2.1 license.
|
||||
|
||||
"Forge" is Forge Development LLC, a legally incorporated entity in Oregon, USA.
|
||||
|
||||
[Wiki]: https://github.com/MinecraftForge/MinecraftForge/wiki/If-you-want-to-contribute-to-Forge
|
||||
+34
-23
@@ -2,16 +2,16 @@
|
||||
|
||||
MinecraftForge
|
||||
=============
|
||||
[](https://files.minecraftforge.net)
|
||||
[](https://files.minecraftforge.net) [](https://discord.gg/UvedJ9m) [](https://www.patreon.com/LexManos)
|
||||
[][Download]
|
||||
[][Download] [][Discord] [][Patreon]
|
||||
|
||||
Forge is a free, open-source modding API all of your favourite mods use!
|
||||
|
||||
| Version | Support |
|
||||
| ------------- | ------------- |
|
||||
| 1.18.x | Active |
|
||||
| 1.16.x | LTS |
|
||||
| Version | Support |
|
||||
|---------| ------------- |
|
||||
| 1.20.x | Active |
|
||||
| 1.19.x | Legacy |
|
||||
|
||||
* [Download]
|
||||
* [Forum]
|
||||
@@ -20,7 +20,7 @@ Forge is a free, open-source modding API all of your favourite mods use!
|
||||
|
||||
# Installing Forge
|
||||
|
||||
Go to [the Forge website](https://files.minecraftforge.net)
|
||||
Go to [the Forge website][Download]
|
||||
and select the Minecraft version you wish to get Forge for from the list.
|
||||
|
||||
You can download the installer for the *Recommended Build* or the
|
||||
@@ -29,37 +29,48 @@ You can download the installer for the *Recommended Build* or the
|
||||
into your vanilla launcher environment, where you can then create a new
|
||||
profile using that version and play the game!
|
||||
|
||||
For support and questions, visit [the Support Forum](https://www.minecraftforge.net/forum/forum/18-support-bug-reports/).
|
||||
|
||||
[Here is a short video from Rorax showing how to install and setup Forge.](https://www.youtube.com/watch?v=lB3ArN_-3Oc)
|
||||
For support and questions, visit [the Support Forum][Support-Forum] or [the Forge Discord server][Discord].
|
||||
|
||||
# Creating Mods
|
||||
|
||||
[See the "Getting Started" section in the Forge Documentation](https://mcforge.readthedocs.io/en/latest/gettingstarted/).
|
||||
[See the "Getting Started" section in the Forge Documentation][Getting-Started].
|
||||
|
||||
# Contribute to Forge
|
||||
|
||||
If you wish to actually inspect Forge, submit PRs or otherwise work
|
||||
with Forge itself, you're in the right place!
|
||||
|
||||
[See the guide to setting up a Forge workspace](http://mcforge.readthedocs.io/en/latest/forgedev/).
|
||||
|
||||
[See the guide to setting up a Forge workspace][ForgeDev].
|
||||
|
||||
### Pull requests
|
||||
|
||||
[See the "Making Changes and Pull Requests" section in the Forge documentation](https://mcforge.readthedocs.io/en/latest/forgedev/#making-changes-and-pull-requests).
|
||||
[See the "Making Changes and Pull Requests" section in the Forge documentation][Pull-Requests].
|
||||
|
||||
Please read the contributing guidelines found [here](CONTRIBUTING.md) before making a pull request.
|
||||
Please read the contributing guidelines found [here][Contributing] before making a pull request.
|
||||
|
||||
### Contributor License Agreement
|
||||
We require all contributors to acknowledge the [Forge Contributor
|
||||
License Agreement](https://cla-assistant.io/MinecraftForge/MinecraftForge). Please ensure you have a valid email address
|
||||
associated with your GitHub account to do this. If you have previously
|
||||
We require all contributors to acknowledge the [Forge Contributor License Agreement][CLA].
|
||||
Please ensure you have a valid email address associated with your GitHub account to do this. If you have previously
|
||||
signed it, you should be OK.
|
||||
|
||||
#### Donate
|
||||
*Forge is a large project with many collaborators working on it around the clock. Forge is and will always remain free to use and modify. However, it costs money to run such a large project as this, so please consider [becoming a patron](https://www.patreon.com/LexManos).*
|
||||
*Forge is a large project with many collaborators working on it around the clock. Forge is and will always remain free
|
||||
to use and modify. However, it costs money to run such a large project as this, so please consider
|
||||
[becoming a patron][Patreon].*
|
||||
|
||||
[Contributing]: ./CONTRIBUTING.md
|
||||
[CLA]: https://cla-assistant.io/MinecraftForge/MinecraftForge
|
||||
|
||||
[Download]: https://files.minecraftforge.net/
|
||||
[Forum]: https://www.minecraftforge.net/forum/
|
||||
[Discord]: https://discord.gg/UvedJ9m
|
||||
[Documentation]: https://mcforge.readthedocs.io
|
||||
|
||||
[Forum]: https://forums.minecraftforge.net/
|
||||
[Support-Forum]: https://forums.minecraftforge.net/forum/18-support-bug-reports/
|
||||
|
||||
[Discord]: https://discord.minecraftforge.net/
|
||||
|
||||
[Documentation]: https://docs.minecraftforge.net/
|
||||
[Getting-Started]: https://docs.minecraftforge.net/en/latest/gettingstarted/
|
||||
[ForgeDev]: https://docs.minecraftforge.net/en/latest/forgedev/
|
||||
[Pull-Requests]: https://docs.minecraftforge.net/en/latest/forgedev/#making-changes-and-pull-requests
|
||||
|
||||
[Patreon]: https://www.patreon.com/LexManos
|
||||
+17
-9
@@ -1,7 +1,7 @@
|
||||
Getting Support Surrounding Forge
|
||||
=================================
|
||||
|
||||
**If you do not want to read the following wall of text, no matter its relevance for helping get your issue resolved quickly, please refer to the [Forge Forums](http://www.minecraftforge.net/forum/forum/18-support-bug-reports/) for support.**
|
||||
**If you do not want to read the following wall of text, no matter its relevance for helping get your issue resolved quickly, please refer to the [Forge Forums][Bug-Reports] for support.**
|
||||
|
||||
Before reporting an issue on this repository's bug tracker, please carefully read this document. There are various resources that are more likely to be able to help you with certain kinds of issues. This document provides instructions on how to determine which place best to report to.
|
||||
|
||||
@@ -44,7 +44,7 @@ You can generally tell pretty quickly what caused the error at the head of your
|
||||
1. If you can find the name of a mod in the trace, that mod most likely is the culprit.
|
||||
- Be aware that you may find multiple mods as participants of a crash. In that case, you can either report to both or try to reason (using the names in the stacktrace) about which mod is at fault.
|
||||
2. If you can only find Forge (usually starting with `net.minecraftforge`) alongside Vanilla (usually starting with `net.minecraft`) references, chances are that you have found a bug in Forge. In this case, use one of the various [support channels](#forge-support) listed below.
|
||||
- The occurence of a single call to Forge somewhere in the stacktrace *does not* warrant a report to Forge if there are mods present in the crash report.
|
||||
- The occurrence of a single call to Forge somewhere in the stacktrace *does not* warrant a report to Forge if there are mods present in the crash report.
|
||||
3. If there's no mention of Forge or a mod, but any Vanilla class, you might have found a Vanilla bug. You should probably still report it via the Forge channels first, however, due the nature of modded environments and the rarity of Vanilla crash bugs.
|
||||
4. Always check whether there's a mention of coremods at the beginning of a crash report. If there is and you can't find any clear culprit, consider reporting to these coremods first, since they are free to change any of the code executed in your environment.
|
||||
|
||||
@@ -74,17 +74,18 @@ Please note that this list has to be considered incomplete as there is a large n
|
||||
|
||||
If you're certain you've found an issue that is directly caused by Forge, you have the following three platforms available for dealing with it, with decreasing relevance for end-users:
|
||||
|
||||
1. **Forge Forums:** There's a dedicated [Support & Bug Report](http://www.minecraftforge.net/forum/forum/18-support-bug-reports/) section on the Forge forums, which is led by team of moderators and volunteers who will try to deal with your reports as quickly as possible. *As a player you should probably report here first.*
|
||||
- For modders, there also is a separate [Modder Support](http://www.minecraftforge.net/forum/forum/70-modder-support/) section where you can ask all your development-related questions. *You should prefer this section for coding help.*
|
||||
2. **Forge IRC Channel:** You can find a lot of help on the official [`#minecraftforge`](irc://irc.esper.net:6667/minecraftforge) channel on EsperNet. Especially smaller issues that you quickly need help with could be reported there. *Be aware that the channel is more suited towards development questions, so while you may get support there as a player, you should almost always prefer the forums.*
|
||||
3. **Forge Issue Tracker:** If you're absolutely certain that you've found an issue in the Forge codebase (e.g. you're a modder yourself and have analyzed the stacktrace and relevant code), don't refrain from reporting it on this repository's [issue tracker](https://github.com/MinecraftForge/MinecraftForge/issues). *Please do not use the issues as a means of getting coding help. For developers, the IRC and the [official documentation](https://mcforge.readthedocs.io) are the best sources of information.*
|
||||
1. **Forge Forums:** There's a dedicated [Support & Bug Report][Bug-Reports] section on the Forge forums, which is led by team of moderators and volunteers who will try to deal with your reports as quickly as possible. *As a player you should probably report here first.*
|
||||
- For modders, there also is a separate [Modder Support][Modder-Support] section where you can ask all your development-related questions. *You should prefer this section for coding help.*
|
||||
2. **Forge Discord Server:** You can find a lot of help on the official [Discord Server][Discord], especially smaller issues that you quickly need help with. *Be aware that the channel is more suited towards development questions, so while you may get support there as a player, you should almost always prefer the forums.*
|
||||
3. **Forge Issue Tracker:** If you're absolutely certain that you've found an issue in the Forge codebase (e.g. you're a modder yourself and have analyzed the stacktrace and relevant code), don't refrain from reporting it on this repository's [issue tracker](https://github.com/MinecraftForge/MinecraftForge/issues). *Please do not use the issues as a means of getting coding help. For developers, the IRC and the [official documentation][Documentation] are the best sources of information.*
|
||||
|
||||
### Mod Support
|
||||
|
||||
A lot of mods also use GitHub or other source management systems and their integrated issue trackers. The following options will give you the best chances of reporting your issue in the right place:
|
||||
|
||||
- **CurseForge:** Many mods are hosted on the [CurseForge platform](https://minecraft.curseforge.com) nowadays. Chances are you already downloaded them from there or as a part of a modpack hosted there. Projects may specify an issue reporting link on their own page, so simply look for it on the mod's project page and you should be directed to the preferred means of error reporting.
|
||||
- **Searching for a repository directly:** You may need to try and find a repository directly on source control sites. The most common ones are [GitHub](https://github.com) and [BitBucket](https://bitbucket.org). Both come with search capabilities, so simply try to search for the mod's name.
|
||||
- **Modrinth:** Some modders use [Modrinth](https://modrinth.com/mods) to host their mods, but be mindful that not all mods on this platform are for the Forge modloader.
|
||||
- **Searching for a repository directly:** You may need to try and find a repository directly on source control sites. The most common ones are [GitHub](https://github.com), [GitLab](https://gitlab.com), and [BitBucket](https://bitbucket.org). Both come with search capabilities, so simply try to search for the mod's name.
|
||||
|
||||
### Vanilla Support
|
||||
|
||||
@@ -98,7 +99,7 @@ Now that you've identified the source of your issue and decided on the correct s
|
||||
### Preventing Duplicates and Unnecessary Reports
|
||||
Use the target issue tracker's search functionality to look for issues which have descriptions similar to yours, but make sure to also search for already closed issues. If you can find one, verify it is in fact the same (or a very similar issue) and simply comment on it if you have anything else to contribute to the discussion, such as other steps to reproduce it. *Do not create duplicates of an existing issue, since it only puts extra effort on maintainers to recognize and close/mark them.*
|
||||
|
||||
Additionally, always check the *latest* changelogs of Forge and participating mods to find out if the bug was already noticed and fixed by the developer. Often the maintainer may only support newer versions of the game, so also check the changelogs for versions of Minecraft you don't actually actively play. If the issue was adressed already, *do not report it*, even if the fix only exists for a newer version of the game, since the developer probably had a good reason to do so. If your issue only exists with versions of a mod that are for very old versions of the game, also refrain from reporting them, considering most modders only support the two latest versions of Minecraft, if at all.
|
||||
Additionally, always check the *latest* changelogs of Forge and participating mods to find out if the bug was already noticed and fixed by the developer. Often the maintainer may only support newer versions of the game, so also check the changelogs for versions of Minecraft you don't actually actively play. If the issue was addressed already, *do not report it*, even if the fix only exists for a newer version of the game, since the developer probably had a good reason to do so. If your issue only exists with versions of a mod that are for very old versions of the game, also refrain from reporting them, considering most modders only support the two latest versions of Minecraft, if at all.
|
||||
|
||||
### Required Information
|
||||
If you're certain that your issue went unnoticed up until now, you can get to actually writing up the report. Below you can find a list of the *required* information any bug report should contain:
|
||||
@@ -143,4 +144,11 @@ After you have written your bug report, it is time to *wait*. Please be *patient
|
||||
|
||||
Every project has a different way of dealing with issues, so don't wonder if there's no comment on it, but somebody "labels" it with certain markers. This helps developers to more easily keep track of bugs and priorities. When any further input from you is required, you will be asked specifically. Do not bug anyone through other channels about resolving your issue.
|
||||
|
||||
Once your issue is closed, you will know whether it was valid or turned out to be irreproducible. If you included all of the data from above in your report, chances are that the latter won't be the case.
|
||||
Once your issue is closed, you will know whether it was valid or turned out to be unreproducible. If you included all of the data from above in your report, chances are that the latter won't be the case.
|
||||
|
||||
[Bug-Reports]: https://forums.minecraftforge.net/forum/18-support-bug-reports/
|
||||
[Modder-Support]: https://forums.minecraftforge.net/forum/70-modder-support/
|
||||
|
||||
[Discord]: https://discord.minecraftforge.net/
|
||||
|
||||
[Documentation]: https://docs.minecraftforge.net/
|
||||
+46
-50
@@ -1,71 +1,67 @@
|
||||
plugins {
|
||||
id 'com.github.ben-manes.versions'
|
||||
id 'org.javamodularity.moduleplugin' version '1.8.3' apply false
|
||||
id 'org.cadixdev.licenser'
|
||||
}
|
||||
apply plugin: 'java-library'
|
||||
apply plugin: 'jacoco'
|
||||
apply plugin: 'org.javamodularity.moduleplugin'
|
||||
import net.minecraftforge.gradleutils.PomUtils
|
||||
|
||||
dependencyUpdates.rejectVersionIf { isNonStable(it.candidate.version) }
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'maven-publish'
|
||||
id 'net.minecraftforge.licenser'
|
||||
id 'net.minecraftforge.gradleutils'
|
||||
}
|
||||
|
||||
apply from: rootProject.file('build_shared.gradle')
|
||||
|
||||
dependencies {
|
||||
compileOnly('org.jetbrains:annotations:21.0.1')
|
||||
compileOnly(libs.nulls)
|
||||
|
||||
api(libs.eventbus)
|
||||
|
||||
implementation(project(':fmlloader'))
|
||||
api('net.minecraftforge:eventbus:5.0.+')
|
||||
implementation('commons-io:commons-io:2.5')
|
||||
implementation(libs.commons.io)
|
||||
}
|
||||
|
||||
task sourcesJar(type: Jar) {
|
||||
archiveClassifier = 'sources'
|
||||
from sourceSets.main.allSource
|
||||
java {
|
||||
toolchain.languageVersion = JavaLanguageVersion.of(JAVA_VERSION)
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
ext {
|
||||
MANIFESTS = [
|
||||
'': [
|
||||
'FMLModType': 'LIBRARY',
|
||||
'Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"),
|
||||
'Git-Commit': GIT_INFO.abbreviatedId,
|
||||
'Git-Branch': GIT_INFO.branch,
|
||||
'Build-Number': "${System.getenv('BUILD_NUMBER')?:0}"
|
||||
] as LinkedHashMap,
|
||||
'net/minecraftforge/fml/loading/': [
|
||||
'Specification-Title': 'FMLLoader',
|
||||
tasks.named('jar', Jar).configure {
|
||||
manifest {
|
||||
attributes([
|
||||
'Automatic-Module-Name': 'net.minecraftforge.fmlcore',
|
||||
'FMLModType': 'LIBRARY'
|
||||
] as LinkedHashMap)
|
||||
attributes([
|
||||
'Specification-Title': 'FML',
|
||||
'Specification-Vendor': 'Forge Development LLC',
|
||||
'Specification-Version': '1',
|
||||
'Implementation-Title': 'FML Loader',
|
||||
'Implementation-Title': 'FML',
|
||||
'Implementation-Version': '1.0',
|
||||
'Implementation-Vendor': 'Forge'
|
||||
] as LinkedHashMap
|
||||
]
|
||||
}
|
||||
|
||||
jar.doFirst {
|
||||
MANIFESTS.each { pkg, values ->
|
||||
if (pkg == '')
|
||||
manifest.attributes(values)
|
||||
else
|
||||
manifest.attributes(values, pkg)
|
||||
'Implementation-Vendor': 'Forge Development LLC'
|
||||
] as LinkedHashMap, 'net/minecraftforge/fml/')
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs << '-Xlint:unchecked'
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.compilerArgs << '-Xlint:-unchecked'
|
||||
}
|
||||
|
||||
license {
|
||||
header = rootProject.file('LICENSE-header.txt')
|
||||
|
||||
include 'net/minecraftforge/'
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives jar
|
||||
archives sourcesJar
|
||||
publishing {
|
||||
publications.register('mavenJava', MavenPublication).configure {
|
||||
from components.java
|
||||
artifactId = 'fmlcore'
|
||||
pom {
|
||||
name = project.name
|
||||
description = 'Modifactions to Minecraft to enable mod developers.'
|
||||
url = 'https://github.com/MinecraftForge/MinecraftForge'
|
||||
PomUtils.setGitHubDetails(pom, 'MinecraftForge')
|
||||
license PomUtils.Licenses.LGPLv2_1
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven gradleutils.publishingForgeMaven
|
||||
}
|
||||
}
|
||||
|
||||
publishing.publications.mavenJava(MavenPublication) {
|
||||
from components.java
|
||||
artifact sourcesJar
|
||||
}
|
||||
@@ -5,36 +5,39 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import cpw.mods.modlauncher.util.ServiceLoaderUtils;
|
||||
import net.minecraftforge.eventbus.api.IEventBus;
|
||||
import net.minecraftforge.fml.config.IConfigEvent;
|
||||
import net.minecraftforge.fml.loading.FMLLoader;
|
||||
|
||||
import java.util.ServiceConfigurationError;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Used to allow access to certain things from the game layer
|
||||
*/
|
||||
public class Bindings {
|
||||
private static final Bindings INSTANCE = new Bindings();
|
||||
private Bindings() {}
|
||||
|
||||
private final IBindingsProvider provider;
|
||||
|
||||
private Bindings() {
|
||||
final var providers = ServiceLoaderUtils.streamServiceLoader(()->ServiceLoader.load(FMLLoader.getGameLayer(), IBindingsProvider.class), sce->{}).toList();
|
||||
if (providers.size() != 1) {
|
||||
throw new IllegalStateException("Could not find bindings provider");
|
||||
}
|
||||
this.provider = providers.get(0);
|
||||
}
|
||||
/**
|
||||
* The provider of the bindings
|
||||
* @implNote May also throw a {@link ServiceConfigurationError}
|
||||
*/
|
||||
private static final IBindingsProvider PROVIDER = ServiceLoader.load(FMLLoader.getGameLayer(), IBindingsProvider.class)
|
||||
.findFirst().orElseThrow(() -> new IllegalStateException("Could not find bindings provider"));
|
||||
|
||||
/**
|
||||
* @return A supplier of net.minecraftforge.common.MinecraftForge#EVENT_BUS
|
||||
*/
|
||||
public static Supplier<IEventBus> getForgeBus() {
|
||||
return INSTANCE.provider.getForgeBusSupplier();
|
||||
return PROVIDER.getForgeBusSupplier();
|
||||
}
|
||||
|
||||
public static Supplier<I18NParser> getMessageParser() {
|
||||
return INSTANCE.provider.getMessageParser();
|
||||
return PROVIDER.getMessageParser();
|
||||
}
|
||||
|
||||
public static Supplier<IConfigEvent.ConfigConfig> getConfigConfiguration() {
|
||||
return INSTANCE.provider.getConfigConfiguration();
|
||||
return PROVIDER.getConfigConfiguration();
|
||||
}
|
||||
}
|
||||
@@ -5,33 +5,93 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class CrashReportCallables {
|
||||
public class CrashReportCallables
|
||||
{
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final List<ISystemReportExtender> crashCallables = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
/**
|
||||
* Register a custom {@link ISystemReportExtender}
|
||||
*/
|
||||
public static void registerCrashCallable(ISystemReportExtender callable)
|
||||
{
|
||||
crashCallables.add(callable);
|
||||
}
|
||||
|
||||
public static void registerCrashCallable(String headerName, Supplier<String> reportGenerator) {
|
||||
registerCrashCallable(new ISystemReportExtender() {
|
||||
/**
|
||||
* Register a {@link ISystemReportExtender system report extender} with the given header name and content
|
||||
* generator, which will always be appended to the system report
|
||||
* @param headerName The name of the system report entry
|
||||
* @param reportGenerator The report generator to be called when a crash report is built
|
||||
*/
|
||||
public static void registerCrashCallable(String headerName, Supplier<String> reportGenerator)
|
||||
{
|
||||
registerCrashCallable(new ISystemReportExtender()
|
||||
{
|
||||
@Override
|
||||
public String getLabel() {
|
||||
public String getLabel()
|
||||
{
|
||||
return headerName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get() {
|
||||
public String get()
|
||||
{
|
||||
return reportGenerator.get();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static List<ISystemReportExtender> allCrashCallables() {
|
||||
/**
|
||||
* Register a {@link ISystemReportExtender system report extender} with the given header name and content
|
||||
* generator, which will only be appended to the system report when the given {@link BooleanSupplier} returns true
|
||||
* @param headerName The name of the system report entry
|
||||
* @param reportGenerator The report generator to be called when a crash report is built
|
||||
* @param active The supplier of the flag to be checked when a crash report is built
|
||||
*/
|
||||
public static void registerCrashCallable(String headerName, Supplier<String> reportGenerator, BooleanSupplier active)
|
||||
{
|
||||
registerCrashCallable(new ISystemReportExtender()
|
||||
{
|
||||
@Override
|
||||
public String getLabel()
|
||||
{
|
||||
return headerName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get()
|
||||
{
|
||||
return reportGenerator.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive()
|
||||
{
|
||||
try
|
||||
{
|
||||
return active.getAsBoolean();
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
LOGGER.warn("CrashCallable '{}' threw an exception while checking the active flag, disabling", headerName, t);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static List<ISystemReportExtender> allCrashCallables()
|
||||
{
|
||||
return List.copyOf(crashCallables);
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,11 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import com.google.common.base.Stopwatch;
|
||||
@@ -53,17 +55,31 @@ public class DeferredWorkQueue
|
||||
public void runTasks() {
|
||||
if (tasks.isEmpty()) return;
|
||||
LOGGER.debug(LOADING, "Dispatching synchronous work for work queue {}: {} jobs", modLoadingStage, tasks.size());
|
||||
RuntimeException aggregate = new RuntimeException();
|
||||
Stopwatch timer = Stopwatch.createStarted();
|
||||
tasks.forEach(t->makeRunnable(t, Runnable::run));
|
||||
for (TaskInfo t : tasks) {
|
||||
makeRunnable(t, Runnable::run, aggregate);
|
||||
}
|
||||
timer.stop();
|
||||
LOGGER.debug(LOADING, "Synchronous work queue completed in {}", timer);
|
||||
if (aggregate.getSuppressed().length > 0) {
|
||||
LOGGER.fatal(
|
||||
LOADING,
|
||||
"Synchronous work queue completed exceptionally in {}, see suppressed exceptions for details:",
|
||||
timer,
|
||||
aggregate
|
||||
);
|
||||
throw aggregate;
|
||||
} else {
|
||||
LOGGER.debug(LOADING, "Synchronous work queue completed in {}", timer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void makeRunnable(TaskInfo ti, Executor executor) {
|
||||
private static void makeRunnable(TaskInfo ti, Executor executor, RuntimeException aggregate) {
|
||||
executor.execute(() -> {
|
||||
Stopwatch timer = Stopwatch.createStarted();
|
||||
ModLoadingContext.get().setActiveContainer(ti.owner);
|
||||
try {
|
||||
ti.future.exceptionally(t -> captureException(ti.owner.getModId(), aggregate, t));
|
||||
ti.task.run();
|
||||
} finally {
|
||||
ModLoadingContext.get().setActiveContainer(null);
|
||||
@@ -75,13 +91,38 @@ public class DeferredWorkQueue
|
||||
});
|
||||
}
|
||||
|
||||
private static <T> T captureException(String modId, RuntimeException aggregate, Throwable throwable) {
|
||||
if (throwable instanceof CompletionException ce) {
|
||||
throwable = ce.getCause();
|
||||
}
|
||||
aggregate.addSuppressed(throwable);
|
||||
LOGGER.error("Mod '{}' encountered an error in a deferred task:", modId, throwable);
|
||||
return null;
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> enqueueWork(final ModContainer modInfo, final Runnable work) {
|
||||
return CompletableFuture.runAsync(work, r->tasks.add(new TaskInfo(modInfo, r)));
|
||||
return enqueueWork(modInfo, taskInfo -> CompletableFuture.runAsync(work, r -> taskInfo.task = r));
|
||||
}
|
||||
|
||||
public <T> CompletableFuture<T> enqueueWork(final ModContainer modInfo, final Supplier<T> work) {
|
||||
return CompletableFuture.supplyAsync(work, r->tasks.add(new TaskInfo(modInfo, r)));
|
||||
return enqueueWork(modInfo, taskInfo -> CompletableFuture.supplyAsync(work, r -> taskInfo.task = r));
|
||||
}
|
||||
|
||||
record TaskInfo(ModContainer owner, Runnable task) {}
|
||||
private <T> CompletableFuture<T> enqueueWork(final ModContainer modInfo, Function<TaskInfo, CompletableFuture<T>> futureGen) {
|
||||
TaskInfo taskInfo = new TaskInfo(modInfo);
|
||||
CompletableFuture<T> future = futureGen.apply(taskInfo);
|
||||
taskInfo.future = future;
|
||||
tasks.add(taskInfo);
|
||||
return future;
|
||||
}
|
||||
|
||||
private static final class TaskInfo {
|
||||
private final ModContainer owner;
|
||||
private Runnable task;
|
||||
private CompletableFuture<?> future;
|
||||
|
||||
private TaskInfo(ModContainer owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,41 +8,114 @@ package net.minecraftforge.fml;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* An extension point for a mod container.
|
||||
*
|
||||
* <p>An extension point can be registered for a mod container using {@link ModContainer#registerExtensionPoint(Class, Supplier)}
|
||||
* and retrieved (if present) using {@link ModContainer#getCustomExtension(Class)}. An extension point allows a mod to
|
||||
* supply an arbitrary value as a record class to another mod or framework through their mod container class, avoiding
|
||||
* the use of {@link InterModComms} or other external frameworks to pass around these values.</p>
|
||||
*
|
||||
* <p>The usual way to declare an extension point is to implement this interface on a record class, with the type
|
||||
* parameter being a reference to the class itself. For example, {@code record MyExtension(...) extends
|
||||
* IExtensionPoint<MyExtension>} would declare an extension point which supplies a {@code MyExtension} object. However,
|
||||
* there is no hard requirement that an extension point's type parameter must be in reference to itself; the type
|
||||
* parameter may reference another record class instead.</p>
|
||||
*
|
||||
* @param <T> the type of the record which is held by the extension point
|
||||
*/
|
||||
@SuppressWarnings("unused") // Type parameter T
|
||||
// TODO: Complete re-write, this hasnt actually be used in a generic way by mods and we could make it a lot simpler.
|
||||
public interface IExtensionPoint<T extends Record>
|
||||
{
|
||||
/**
|
||||
* Compatibility display test for the mod.
|
||||
* Used for displaying compatibility with remote servers with the same mod, and on disk saves.
|
||||
* Extension point for the compatibility display test used on the server selection screen.
|
||||
*
|
||||
* The supplier provides my "local" version for sending across the impl or writing to disk
|
||||
* The predicate tests the version from a remote instance or save for acceptability (Boolean is true for impl, false for local save)
|
||||
* and returns true if the version is compatible.
|
||||
* <em>Note: "server" and "client" refers to the {@linkplain net.minecraftforge.api.distmarker.Dist#DEDICATED_SERVER dedicated server}
|
||||
* and {@linkplain net.minecraftforge.api.distmarker.Dist#CLIENT game client} physical distributions, rather than the
|
||||
* {@linkplain LogicalSide logical server and client}.</em>
|
||||
*
|
||||
* <p>Return {@code net.minecraftforge.impl.impl.FMLNetworkConstants#IGNORESERVERONLY} in the supplier, if you wish to be ignored
|
||||
* as a server side only mod.</p>
|
||||
* <p>Return true in the predicate for all values of the input string (when impl boolean is true) if you are client side,
|
||||
* and don't care about matching any potential server version.</p>
|
||||
* <p>The {@link Supplier} provides the local compatibility version, which is sent from the server to the client
|
||||
* for multiplayer connections or stored to disk for the world save. The {@link BiPredicate} accepts the remote
|
||||
* compatibility version and a boolean indicating whether the remote version is from the server or a world save,
|
||||
* where {@code true} means it is from the server and {@code false} means it is from the world save. The return
|
||||
* value of the predicate determines whether the remote version is "compatible" for the purposes of the display test.</p>
|
||||
*
|
||||
* <p>
|
||||
* Examples: A server only mod
|
||||
* </p>
|
||||
* <p>The local compatibility version may be of the value {@link DisplayTest#IGNORESERVERONLY},
|
||||
* in which case clients will ignore the mod's presence if it is present on the server but not on the client.
|
||||
* However, the remote version test predicate must still accept this value as a remote version in order to display
|
||||
* as compatible if the mod is present on the client.</p>
|
||||
*
|
||||
* <p><strong>The compatibility display test does not necessarily indicate the success or failure of an actual
|
||||
* connection attempt.</strong> Factors such as display test extension misconfiguration, truncation of ping data,
|
||||
* difference of registry data or network channels between server and client, and others may cause the result of the
|
||||
* compatibility test to not reflect the actual likelihood of a connection successfully being established between
|
||||
* the server and the client.</p>
|
||||
*
|
||||
* <p>An example declaration of a display test extension registration for a regular mod (requires to be present on
|
||||
* server and client) is as follows:</p>
|
||||
* <pre>{@code
|
||||
* registerExtensionPoint(DISPLAYTEST, ()->Pair.of(
|
||||
* ()->FMLNetworkConstants.IGNORESERVERONLY, // ignore me, I'm a server only mod
|
||||
* (s,b)->true // i accept anything from the server or the save, if I'm asked
|
||||
* )
|
||||
* String compatibilityVersion = "1"; // Could be linked with a network channel version or mod version
|
||||
* ModLoadingContext.get().registerExtensionPoint(IExtensionPoint.DisplayTest.class,
|
||||
* () -> new IExtensionPoint.DisplayTest(
|
||||
* () -> compatibilityVersion,
|
||||
* (remoteVersion, isFromServer) -> remoteVersion.equals(compatibilityVersion)
|
||||
* )
|
||||
* );
|
||||
* }</pre>
|
||||
*
|
||||
* <p>
|
||||
* Examples: A client only mod
|
||||
* </p>
|
||||
* <p>An example declaration of a display test extension registration for a <em>server-side-only</em> mod (does not
|
||||
* require to be present on the client) is as follows:</p>
|
||||
* <pre>{@code
|
||||
* registerExtensionPoint(DISPLAYTEST, ()->Pair.of(
|
||||
* ()->"anything. i don't care", // if i'm actually on the server, this string is sent but i'm a client only mod, so it won't be
|
||||
* (remoteversionstring,networkbool)->networkbool // i accept anything from the server, by returning true if it's asking about the server
|
||||
* )
|
||||
* ModLoadingContext.get().registerExtensionPoint(IExtensionPoint.DisplayTest.class,
|
||||
* () -> new IExtensionPoint.DisplayTest(
|
||||
* // Ignore this mod if not present on the client
|
||||
* () -> NetworkConstants.IGNORESERVERONLY,
|
||||
* // If present on the client, accept any version if from a server
|
||||
* (remoteVersion, isFromServer) -> isFromServer
|
||||
* )
|
||||
* );
|
||||
* }</pre>
|
||||
*
|
||||
* <p>An example declaration of a display test extension registration for a <em>client-side-only</em> mod (does not
|
||||
* require to be present on the server) is as follows:</p>
|
||||
* <pre>{@code
|
||||
* ModLoadingContext.get().registerExtensionPoint(IExtensionPoint.DisplayTest.class,
|
||||
* () -> new IExtensionPoint.DisplayTest(
|
||||
* // Send any version from server to client, since we will be accepting any version as well
|
||||
* () -> "dQw4w9WgXcQ",
|
||||
* // Accept any version on the client, from server or from save
|
||||
* (remoteVersion, isFromServer) -> true
|
||||
* )
|
||||
* );
|
||||
* }</pre>
|
||||
*
|
||||
* @see net.minecraftforge.network.ServerStatusPing
|
||||
* @see net.minecraftforge.client.ForgeHooksClient#processForgeListPingData(net.minecraft.network.protocol.status.ServerStatus, net.minecraft.client.multiplayer.ServerData)
|
||||
*/
|
||||
record DisplayTest(Supplier<String> suppliedVersion, BiPredicate<String, Boolean> remoteVersionTest) implements IExtensionPoint<DisplayTest> {}
|
||||
@SuppressWarnings("JavadocReference") // reference to NetworkConstants, ForgeHooksClient
|
||||
record DisplayTest(Supplier<String> suppliedVersion, BiPredicate<String, Boolean> remoteVersionTest) implements IExtensionPoint<DisplayTest> {
|
||||
public static final String IGNORESERVERONLY = "SERVER_ONLY";
|
||||
|
||||
/**
|
||||
* Ignores any version information coming from the server - use for server only mods
|
||||
*/
|
||||
public static final Supplier<DisplayTest> IGNORE_SERVER_VERSION = () -> new DisplayTest(IGNORESERVERONLY, (remoteVersion, isFromServer) -> true);
|
||||
|
||||
/**
|
||||
* Ignores all information and provides no information
|
||||
* <p>Note: If your mod is purely client-side and has no multiplayer functionality (be it dedicated servers or
|
||||
* Open to LAN), consider setting {@code clientSideOnly=true} in the root of your mods.toml.</p>
|
||||
*/
|
||||
public static final Supplier<DisplayTest> IGNORE_ALL_VERSION = () -> new DisplayTest("", (remoteVersion, isFromServer) -> true);
|
||||
|
||||
/**
|
||||
* An optional alternative to {@link #DisplayTest(Supplier, BiPredicate)} which accepts a constant version string
|
||||
* instead of a {@link Supplier}.
|
||||
* <p>Internally, the provided version string is wrapped in a Supplier for you.</p>
|
||||
*/
|
||||
public DisplayTest(String version, BiPredicate<String, Boolean> remoteVersionTest) {
|
||||
this(() -> version, remoteVersionTest);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,24 +7,94 @@ package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.ToIntFunction;
|
||||
|
||||
/**
|
||||
* A mod loading state. During mod loading, the mod loader transitions between states in a defined sorted list of states,
|
||||
* grouped into various {@link ModLoadingPhase phases}.
|
||||
*
|
||||
* @see IModStateProvider
|
||||
*/
|
||||
public interface IModLoadingState {
|
||||
/**
|
||||
* {@return the name of this state}
|
||||
*/
|
||||
String name();
|
||||
|
||||
/**
|
||||
* {@return the name of the state immediately previous to this state} This may be a blank name, which indicates this
|
||||
* is either the first mod loading state or an exceptional mod loading state (such as a situation where errors
|
||||
* prevent the loading process from continuing normally).
|
||||
*/
|
||||
String previous();
|
||||
|
||||
/**
|
||||
* {@return the mod loading phase this state belongs to} For exceptional mod loading states, this should be
|
||||
* {@link ModLoadingPhase#ERROR}.
|
||||
*/
|
||||
ModLoadingPhase phase();
|
||||
|
||||
/**
|
||||
* {@return a function returning a human-friendly message for this state}
|
||||
*/
|
||||
Function<ModList, String> message();
|
||||
|
||||
/**
|
||||
* @return a function that computes the size of this transition based on the size of the modlist.
|
||||
* Used to compute progress.
|
||||
*/
|
||||
ToIntFunction<ModList> size();
|
||||
|
||||
/**
|
||||
* {@return an optional runnable, which runs before starting the transition from this state to the next}
|
||||
* @see #buildTransition(Executor, Executor, ProgressMeter, Function, Function)
|
||||
*/
|
||||
Optional<Consumer<ModList>> inlineRunnable();
|
||||
|
||||
default <T extends Event & IModBusEvent> Optional<CompletableFuture<List<Throwable>>> buildTransition(final Executor syncExecutor, final Executor parallelExecutor) {
|
||||
return buildTransition(syncExecutor, parallelExecutor, e->CompletableFuture.runAsync(()->{}, e), e->CompletableFuture.runAsync(()->{}, e));
|
||||
/**
|
||||
* Builds the transition task for this state with a blank pre-sync and post-sync task.
|
||||
*
|
||||
* @param <T> a type of event fired on the mod-specific event bus
|
||||
* @param syncExecutor a synchronous executor
|
||||
* @param parallelExecutor a parallel executor
|
||||
* @param progressBar a progress meter for tracking progress
|
||||
* @return a transition task for this state
|
||||
* @see #buildTransition(Executor, Executor, ProgressMeter, Function, Function)
|
||||
*/
|
||||
default <T extends Event & IModBusEvent> Optional<CompletableFuture<Void>> buildTransition(
|
||||
final Executor syncExecutor,
|
||||
final Executor parallelExecutor,
|
||||
final ProgressMeter progressBar
|
||||
) {
|
||||
return buildTransition(syncExecutor, parallelExecutor, progressBar,
|
||||
e -> CompletableFuture.completedFuture(null),
|
||||
e -> CompletableFuture.completedFuture(null)
|
||||
);
|
||||
}
|
||||
|
||||
<T extends Event & IModBusEvent> Optional<CompletableFuture<List<Throwable>>> buildTransition(Executor syncExecutor, Executor parallelExecutor, Function<Executor, CompletableFuture<Void>> preSyncTask, Function<Executor, CompletableFuture<Void>> postSyncTask);
|
||||
/**
|
||||
* Builds the transition task for this state. The pre-sync and post-sync task functions allow the transition builder
|
||||
* to run these tasks on the same executor as the actual event dispatch and pre/post hooks.
|
||||
*
|
||||
* @param <T> a type of event fired on the mod-specific event bus
|
||||
* @param syncExecutor a synchronous executor
|
||||
* @param parallelExecutor a parallel executor
|
||||
* @param progressBar a progress meter for tracking progress
|
||||
* @param preSyncTask a function which returns a task to run before event pre-dispatch hook
|
||||
* @param postSyncTask a function which returns a task to run after event post-dispatch hook
|
||||
* @return a transition task for this state
|
||||
*/
|
||||
<T extends Event & IModBusEvent>
|
||||
Optional<CompletableFuture<Void>> buildTransition(final Executor syncExecutor,
|
||||
final Executor parallelExecutor,
|
||||
final ProgressMeter progressBar,
|
||||
final Function<Executor, CompletableFuture<Void>> preSyncTask,
|
||||
final Function<Executor, CompletableFuture<Void>> postSyncTask);
|
||||
}
|
||||
@@ -7,6 +7,15 @@ package net.minecraftforge.fml;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides a list of mod loading states which the mod loader may transition between.
|
||||
*
|
||||
* <p>There may be multiple mod state providers in a single application, where all states from each provider is
|
||||
* combined into a single list and ordered.</p>
|
||||
*/
|
||||
public interface IModStateProvider {
|
||||
/**
|
||||
* {@return the list of mod loading states known to this provider}
|
||||
*/
|
||||
List<IModLoadingState> getAllStates();
|
||||
}
|
||||
@@ -7,10 +7,8 @@ package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiFunction;
|
||||
@@ -18,75 +16,67 @@ import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public interface IModStateTransition {
|
||||
/** Magic value to allow me to optimize the futures list by ignoring the default value without making old methods nullable. */
|
||||
public static final BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> NULL_HOOK = (e, g) -> CompletableFuture.completedFuture(null);
|
||||
|
||||
static IModStateTransition buildNoopTransition() {
|
||||
return new NoopTransition();
|
||||
return ModStateTransitionHelper.NOOP;
|
||||
}
|
||||
|
||||
default <T extends Event & IModBusEvent> CompletableFuture<List<Throwable>> build(Executor syncExecutor, Executor parallelExecutor, Function<Executor, CompletableFuture<Void>> preSyncTask, Function<Executor, CompletableFuture<Void>> postSyncTask) {
|
||||
List<CompletableFuture<List<Throwable>>> cfs = new ArrayList<>();
|
||||
this.eventFunctionStream().get()
|
||||
.map(f->(EventGenerator<T>)f)
|
||||
.reduce((head, tail)-> addCompletableFutureTaskForModDispatch(syncExecutor, parallelExecutor, cfs, head, ModLoadingStage::currentState, tail))
|
||||
.ifPresent(last-> addCompletableFutureTaskForModDispatch(syncExecutor, parallelExecutor, cfs, last, nextModLoadingStage(), null));
|
||||
final CompletableFuture<Void> preSyncTaskCF = preSyncTask.apply(syncExecutor);
|
||||
final CompletableFuture<List<Throwable>> eventDispatchCF = ModList.gather(cfs).thenCompose(ModList::completableFutureFromExceptionList);
|
||||
final CompletableFuture<List<Throwable>> postEventDispatchCF = preSyncTaskCF.thenComposeAsync(n -> eventDispatchCF, parallelExecutor).thenApply(r -> {
|
||||
postSyncTask.apply(syncExecutor);
|
||||
return r;
|
||||
});
|
||||
return this.finalActivityGenerator().apply(syncExecutor, postEventDispatchCF);
|
||||
default CompletableFuture<Void> build(
|
||||
final String name,
|
||||
final Executor syncExecutor,
|
||||
final Executor parallelExecutor,
|
||||
final ProgressMeter progressBar,
|
||||
final Function<Executor, CompletableFuture<Void>> preSyncTask,
|
||||
final Function<Executor, CompletableFuture<Void>> postSyncTask
|
||||
) {
|
||||
return ModStateTransitionHelper.build(this, name, syncExecutor, parallelExecutor, progressBar, preSyncTask, postSyncTask);
|
||||
}
|
||||
|
||||
default BiFunction<ModLoadingStage, Throwable, ModLoadingStage> nextModLoadingStage() {
|
||||
return ModLoadingStage::nextState;
|
||||
}
|
||||
|
||||
private <T extends Event & IModBusEvent> EventGenerator<T> addCompletableFutureTaskForModDispatch(final Executor syncExecutor, final Executor parallelExecutor, final List<CompletableFuture<List<Throwable>>> completeableFutures, final EventGenerator<T> eventGenerator, BiFunction<ModLoadingStage, Throwable, ModLoadingStage> nextState, final EventGenerator<T> nextGenerator) {
|
||||
completeableFutures.add(((BiFunction<Executor, EventGenerator<T>, CompletableFuture<List<Throwable>>>)preDispatchHook()).apply(threadSelector().apply(syncExecutor, parallelExecutor), eventGenerator));
|
||||
completeableFutures.add(ModList.get().futureVisitor(eventGenerator, nextState).apply(threadSelector().apply(syncExecutor, parallelExecutor)));
|
||||
completeableFutures.add(((BiFunction<Executor, EventGenerator<T>, CompletableFuture<List<Throwable>>>)postDispatchHook()).apply(threadSelector().apply(syncExecutor, parallelExecutor), eventGenerator));
|
||||
return nextGenerator;
|
||||
/**
|
||||
* This used to allow you to fire multiple events during the transition. However, in doing so it would cause issues with the default
|
||||
* ModContainer's event handlers causing issues such as mod classes being constructed multiple times.
|
||||
*/
|
||||
@Deprecated(since = "1.21.3", forRemoval = true)
|
||||
default Supplier<Stream<EventGenerator<?>>> eventFunctionStream() {
|
||||
return () -> Stream.ofNullable(eventFunction());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
default <T extends Event & IModBusEvent> EventGenerator<T> eventFunction() {
|
||||
return null;
|
||||
}
|
||||
|
||||
Supplier<Stream<EventGenerator<?>>> eventFunctionStream();
|
||||
ThreadSelector threadSelector();
|
||||
BiFunction<Executor, CompletableFuture<List<Throwable>>, CompletableFuture<List<Throwable>>> finalActivityGenerator();
|
||||
BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<List<Throwable>>> preDispatchHook();
|
||||
BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<List<Throwable>>> postDispatchHook();
|
||||
BiFunction<Executor, CompletableFuture<Void>, CompletableFuture<Void>> finalActivityGenerator();
|
||||
|
||||
/**
|
||||
* I think this was meant as a way to do some things for each mod container beforge/after the transition had been sent to the container.
|
||||
* However, the Future returned by this was never linked to the main transition future in any way. Which means that it was run
|
||||
* in parallel and couldn't guarantee the state of the ModContainer.
|
||||
* <p>
|
||||
* Plus all existing code that I could find returned a completedFuture, so I don't think anyone ever used this.
|
||||
* <p>
|
||||
* If I were to add this back it would be a CompletableFuture wrap(ModContainer, CompletableFuture)
|
||||
* <p>
|
||||
* Added magic value NULL_HOOK to allow me to optimize the futures list by ignoring the default value without making this method nullable.
|
||||
*/
|
||||
@Deprecated(since = "1.21.3", forRemoval = true)
|
||||
default BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> preDispatchHook() { return NULL_HOOK; }
|
||||
@Deprecated(since = "1.21.3", forRemoval = true)
|
||||
default BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> postDispatchHook() { return NULL_HOOK; }
|
||||
|
||||
interface EventGenerator<T extends Event & IModBusEvent> extends Function<ModContainer, T> {
|
||||
static <FN extends Event & IModBusEvent> EventGenerator<FN> fromFunction(Function<ModContainer, FN> fn) {
|
||||
return fn::apply;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record NoopTransition() implements IModStateTransition {
|
||||
|
||||
@Override
|
||||
public Supplier<Stream<EventGenerator<?>>> eventFunctionStream() {
|
||||
return Stream::of;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThreadSelector threadSelector() {
|
||||
return ThreadSelector.SYNC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiFunction<Executor, CompletableFuture<List<Throwable>>, CompletableFuture<List<Throwable>>> finalActivityGenerator() {
|
||||
return (e, t) -> t.thenApplyAsync(Function.identity(), e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<List<Throwable>>> preDispatchHook() {
|
||||
return (t, f)-> CompletableFuture.completedFuture(Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<List<Throwable>>> postDispatchHook() {
|
||||
return (t, f)-> CompletableFuture.completedFuture(Collections.emptyList());
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,9 @@ import java.util.function.Supplier;
|
||||
public interface ISystemReportExtender extends Supplier<String>
|
||||
{
|
||||
String getLabel();
|
||||
|
||||
default boolean isActive()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ public class InterModComms
|
||||
}
|
||||
}
|
||||
|
||||
private static ConcurrentMap<String, ConcurrentLinkedQueue<IMCMessage>> containerQueues = new ConcurrentHashMap<>();
|
||||
private static final ConcurrentMap<String, ConcurrentLinkedQueue<IMCMessage>> containerQueues = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Send IMC to remote. Sender will default to the active modcontainer, or minecraft if not.
|
||||
@@ -115,16 +115,14 @@ public class InterModComms
|
||||
return getMessages(modId, s->Boolean.TRUE);
|
||||
}
|
||||
|
||||
private static class QueueFilteringSpliterator implements Spliterator<IMCMessage>
|
||||
{
|
||||
private final ConcurrentLinkedQueue<IMCMessage> queue;
|
||||
private final Predicate<String> methodFilter;
|
||||
private final Iterator<IMCMessage> iterator;
|
||||
private record QueueFilteringSpliterator(
|
||||
ConcurrentLinkedQueue<IMCMessage> queue,
|
||||
Predicate<String> methodFilter,
|
||||
Iterator<IMCMessage> iterator
|
||||
) implements Spliterator<IMCMessage> {
|
||||
|
||||
public QueueFilteringSpliterator(final ConcurrentLinkedQueue<IMCMessage> queue, final Predicate<String> methodFilter) {
|
||||
this.queue = queue;
|
||||
this.iterator = queue.iterator();
|
||||
this.methodFilter = methodFilter;
|
||||
this(queue, methodFilter, queue.iterator());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,23 +5,50 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
public enum LogicalSide
|
||||
{
|
||||
CLIENT, SERVER;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
|
||||
/**
|
||||
* A logical side of the Minecraft game.
|
||||
* <p>
|
||||
* The {@linkplain Dist#CLIENT client distribution} has a copy of the logical client and the logical server, while the
|
||||
* {@linkplain Dist#DEDICATED_SERVER dedicated server distribution} only holds
|
||||
* the logical server.
|
||||
*
|
||||
* @see Dist
|
||||
*/
|
||||
public enum LogicalSide {
|
||||
/**
|
||||
* The logical client of the Minecraft game, which interfaces with the player's inputs and renders the player's
|
||||
* viewpoint.
|
||||
* <p>
|
||||
* The logical client is only shipped with the client distribution of the game.
|
||||
*
|
||||
* @see Dist#CLIENT
|
||||
*/
|
||||
CLIENT,
|
||||
|
||||
/**
|
||||
* @return if the logical side is a server.
|
||||
* The logical server of the Minecraft game, responsible for connecting to clients and running the simulation logic
|
||||
* on the level.
|
||||
* <p>
|
||||
* The logical server is shipped with both client and dedicated server distributions. The client distribution runs
|
||||
* the logical server for singleplayer mode and LAN play.
|
||||
*
|
||||
* @see Dist#DEDICATED_SERVER
|
||||
*/
|
||||
public boolean isServer()
|
||||
{
|
||||
SERVER;
|
||||
|
||||
/**
|
||||
* {@return if this logical side is the server}
|
||||
*/
|
||||
public boolean isServer() {
|
||||
return !isClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the logical side is a client.
|
||||
* {@return if the logical side is the client}
|
||||
*/
|
||||
public boolean isClient()
|
||||
{
|
||||
public boolean isClient() {
|
||||
return this == CLIENT;
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,13 @@ import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.forgespi.language.IModInfo;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -31,89 +30,82 @@ import java.util.function.Supplier;
|
||||
* a mechanism by which we can wrap actual mod code so that the loader and other
|
||||
* facilities can treat mods at arms length.
|
||||
* </p>
|
||||
*
|
||||
* @author cpw
|
||||
*
|
||||
*/
|
||||
|
||||
public abstract class ModContainer
|
||||
{
|
||||
public abstract class ModContainer {
|
||||
protected final String modId;
|
||||
protected final String namespace;
|
||||
protected final IModInfo modInfo;
|
||||
protected ModLoadingStage modLoadingStage;
|
||||
protected Supplier<?> contextExtension;
|
||||
protected final Map<ModLoadingStage, Runnable> activityMap = new HashMap<>();
|
||||
protected final Map<ModLoadingStage, Runnable> activityMap = new EnumMap<>(ModLoadingStage.class);
|
||||
protected final Map<Class<? extends IExtensionPoint<?>>, Supplier<?>> extensionPoints = new IdentityHashMap<>();
|
||||
protected final EnumMap<ModConfig.Type, ModConfig> configs = new EnumMap<>(ModConfig.Type.class);
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
final Set<ModContainer> dependencies = new HashSet<>();
|
||||
/**
|
||||
* If you want to handle the event, override {@link #dispatchConfigEvent(IConfigEvent)}
|
||||
*/
|
||||
@Deprecated(since = "1.21.3", forRemoval = true)
|
||||
protected Optional<Consumer<IConfigEvent>> configHandler = Optional.empty();
|
||||
|
||||
public ModContainer(IModInfo info)
|
||||
{
|
||||
public ModContainer(IModInfo info) {
|
||||
this.modId = info.getModId();
|
||||
// TODO: Currently not reading namespace from configuration..
|
||||
this.namespace = this.modId;
|
||||
this.modInfo = info;
|
||||
this.modLoadingStage = ModLoadingStage.CONSTRUCT;
|
||||
// default displaytest extension checks for version string match
|
||||
registerExtensionPoint(IExtensionPoint.DisplayTest.class, () -> new IExtensionPoint.DisplayTest(()->this.modInfo.getVersion().toString(),
|
||||
(incoming, isNetwork)->Objects.equals(incoming, this.modInfo.getVersion().toString())));
|
||||
|
||||
final String displayTestString = info.getConfig().<String>getConfigElement("displayTest")
|
||||
.orElseGet(() -> info.getOwningFile().isClientSideOnly() ? "IGNORE_ALL_VERSION" : "MATCH_VERSION");
|
||||
Supplier<IExtensionPoint.DisplayTest> displayTestSupplier = switch (displayTestString) {
|
||||
case "MATCH_VERSION" -> // default displaytest checks for version string match
|
||||
() -> new IExtensionPoint.DisplayTest(() -> this.modInfo.getVersion().toString(),
|
||||
(incoming, isNetwork) -> Objects.equals(incoming, this.modInfo.getVersion().toString()));
|
||||
case "IGNORE_SERVER_VERSION" -> // Ignores any version information coming from the server - use for server only mods
|
||||
IExtensionPoint.DisplayTest.IGNORE_SERVER_VERSION;
|
||||
case "IGNORE_ALL_VERSION" -> // Ignores all information and provides no information
|
||||
IExtensionPoint.DisplayTest.IGNORE_ALL_VERSION;
|
||||
case "NONE" -> null; // NO display test at all - use this if you're going to do your own display test
|
||||
default -> // any other value throws an exception
|
||||
throw new IllegalArgumentException("Invalid displayTest value supplied in mods.toml");
|
||||
};
|
||||
if (displayTestSupplier != null)
|
||||
registerExtensionPoint(IExtensionPoint.DisplayTest.class, displayTestSupplier);
|
||||
else
|
||||
extensionPoints.remove(IExtensionPoint.DisplayTest.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Errored container state, used for filtering. Does nothing.
|
||||
*/
|
||||
ModContainer()
|
||||
{
|
||||
ModContainer() {
|
||||
this.modLoadingStage = ModLoadingStage.ERROR;
|
||||
modId = "BROKEN";
|
||||
namespace = "BROKEN";
|
||||
modInfo = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the modid for this mod
|
||||
*/
|
||||
public final String getModId()
|
||||
{
|
||||
public final String getModId() {
|
||||
return modId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the resource prefix for the mod
|
||||
*/
|
||||
public final String getNamespace()
|
||||
{
|
||||
public final String getNamespace() {
|
||||
return namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current loading stage for this mod
|
||||
*/
|
||||
public ModLoadingStage getCurrentState()
|
||||
{
|
||||
public ModLoadingStage getCurrentState() {
|
||||
return modLoadingStage;
|
||||
}
|
||||
|
||||
public static <T extends Event & IModBusEvent> CompletableFuture<Void> buildTransitionHandler(
|
||||
final ModContainer target,
|
||||
final IModStateTransition.EventGenerator<T> eventGenerator,
|
||||
final BiFunction<ModLoadingStage, Throwable, ModLoadingStage> stateChangeHandler,
|
||||
final Executor executor) {
|
||||
return CompletableFuture
|
||||
.runAsync(() -> {
|
||||
ModLoadingContext.get().setActiveContainer(target);
|
||||
target.activityMap.getOrDefault(target.modLoadingStage, ()->{}).run();
|
||||
target.acceptEvent(eventGenerator.apply(target));
|
||||
}, executor)
|
||||
.whenComplete((mc, exception) -> {
|
||||
target.modLoadingStage = stateChangeHandler.apply(target.modLoadingStage, exception);
|
||||
ModLoadingContext.get().setActiveContainer(null);
|
||||
});
|
||||
}
|
||||
|
||||
public IModInfo getModInfo()
|
||||
{
|
||||
public IModInfo getModInfo() {
|
||||
return modInfo;
|
||||
}
|
||||
|
||||
@@ -122,17 +114,35 @@ public abstract class ModContainer
|
||||
return Optional.ofNullable((T)extensionPoints.getOrDefault(point,()-> null).get());
|
||||
}
|
||||
|
||||
public <T extends Record & IExtensionPoint<T>> void registerExtensionPoint(Class<? extends IExtensionPoint<T>> point, Supplier<T> extension)
|
||||
{
|
||||
public <T extends Record & IExtensionPoint<T>> void registerExtensionPoint(Class<? extends IExtensionPoint<T>> point, Supplier<T> extension) {
|
||||
extensionPoints.put(point, extension);
|
||||
}
|
||||
|
||||
public void registerDisplayTest(IExtensionPoint.DisplayTest displayTest) {
|
||||
registerExtensionPoint(IExtensionPoint.DisplayTest.class, () -> displayTest);
|
||||
}
|
||||
|
||||
public void registerDisplayTest(Supplier<IExtensionPoint.DisplayTest> displayTest) {
|
||||
registerExtensionPoint(IExtensionPoint.DisplayTest.class, displayTest);
|
||||
}
|
||||
|
||||
public void registerDisplayTest(String version, BiPredicate<String, Boolean> remoteVersionTest) {
|
||||
registerDisplayTest(new IExtensionPoint.DisplayTest(version, remoteVersionTest));
|
||||
}
|
||||
|
||||
public void registerDisplayTest(Supplier<String> suppliedVersion, BiPredicate<String, Boolean> remoteVersionTest) {
|
||||
registerDisplayTest(new IExtensionPoint.DisplayTest(suppliedVersion, remoteVersionTest));
|
||||
}
|
||||
|
||||
public void addConfig(final ModConfig modConfig) {
|
||||
configs.put(modConfig.getType(), modConfig);
|
||||
}
|
||||
|
||||
public void dispatchConfigEvent(IConfigEvent event) {
|
||||
configHandler.ifPresent(configHandler->configHandler.accept(event));
|
||||
var handler = configHandler.orElse(null);
|
||||
if (handler != null) {
|
||||
handler.accept(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.forgespi.language.IModFileInfo;
|
||||
import net.minecraftforge.forgespi.language.IModInfo;
|
||||
import net.minecraftforge.forgespi.language.ModFileScanData;
|
||||
@@ -14,27 +12,15 @@ import net.minecraftforge.fml.loading.moddiscovery.ModFile;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.ModInfo;
|
||||
import net.minecraftforge.forgespi.locating.IModFile;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinWorkerThread;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -44,26 +30,25 @@ import java.util.stream.Stream;
|
||||
* Master list of all mods - game-side version. This is classloaded in the game scope and
|
||||
* can dispatch game level events as a result.
|
||||
*/
|
||||
public class ModList
|
||||
{
|
||||
private static Logger LOGGER = LogManager.getLogger();
|
||||
public class ModList {
|
||||
private static ModList INSTANCE;
|
||||
private final List<IModFileInfo> modFiles;
|
||||
private final List<IModInfo> sortedList;
|
||||
private final Map<String, ModFileInfo> fileById;
|
||||
private final Map<String, IModFileInfo> fileById;
|
||||
private List<ModContainer> mods;
|
||||
private Map<String, ModContainer> indexedMods;
|
||||
private List<ModFileScanData> modFileScanData;
|
||||
private List<ModContainer> sortedContainers;
|
||||
|
||||
private ModList(final List<ModFile> modFiles, final List<ModInfo> sortedList)
|
||||
{
|
||||
this.modFiles = modFiles.stream().map(ModFile::getModFileInfo).map(ModFileInfo.class::cast).collect(Collectors.toList());
|
||||
this.sortedList = sortedList.stream().
|
||||
map(ModInfo.class::cast).
|
||||
collect(Collectors.toList());
|
||||
this.fileById = this.modFiles.stream().map(IModFileInfo::getMods).flatMap(Collection::stream).
|
||||
map(ModInfo.class::cast).
|
||||
collect(Collectors.toMap(ModInfo::getModId, ModInfo::getOwningFile));
|
||||
private ModList(final List<ModFile> modFiles, final List<ModInfo> sortedList) {
|
||||
this.modFiles = modFiles.stream().map(ModFile::getModFileInfo).toList();
|
||||
this.sortedList = sortedList.stream().map(IModInfo.class::cast).toList();
|
||||
var byId = new HashMap<String, IModFileInfo>();
|
||||
for (var file : this.modFiles) {
|
||||
for (var mod : file.getMods())
|
||||
byId.put(mod.getModId(), mod.getOwningFile());
|
||||
}
|
||||
this.fileById = Collections.unmodifiableMap(byId);
|
||||
CrashReportCallables.registerCrashCallable("Mod List", this::crashReport);
|
||||
}
|
||||
|
||||
@@ -72,19 +57,20 @@ public class ModList
|
||||
}
|
||||
|
||||
private String fileToLine(IModFile mf) {
|
||||
var mainMod = mf.getModInfos().getFirst();
|
||||
return String.format(Locale.ENGLISH, "%-50.50s|%-30.30s|%-30.30s|%-20.20s|%-10.10s|Manifest: %s", mf.getFileName(),
|
||||
mf.getModInfos().get(0).getDisplayName(),
|
||||
mf.getModInfos().get(0).getModId(),
|
||||
mf.getModInfos().get(0).getVersion(),
|
||||
getModContainerState(mf.getModInfos().get(0).getModId()),
|
||||
mainMod.getDisplayName(),
|
||||
mainMod.getModId(),
|
||||
mainMod.getVersion(),
|
||||
getModContainerState(mainMod.getModId()),
|
||||
((ModFileInfo)mf.getModFileInfo()).getCodeSigningFingerprint().orElse("NOSIGNATURE"));
|
||||
}
|
||||
|
||||
private String crashReport() {
|
||||
return "\n"+applyForEachModFile(this::fileToLine).collect(Collectors.joining("\n\t\t", "\t\t", ""));
|
||||
}
|
||||
|
||||
public static ModList of(List<ModFile> modFiles, List<ModInfo> sortedList)
|
||||
{
|
||||
public static ModList of(List<ModFile> modFiles, List<ModInfo> sortedList) {
|
||||
INSTANCE = new ModList(modFiles, sortedList);
|
||||
return INSTANCE;
|
||||
}
|
||||
@@ -93,107 +79,47 @@ public class ModList
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private static ForkJoinWorkerThread newForkJoinWorkerThread(ForkJoinPool pool) {
|
||||
ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
|
||||
thread.setName("modloading-worker-" + thread.getPoolIndex());
|
||||
// The default sets it to the SystemClassloader, so copy the current one.
|
||||
thread.setContextClassLoader(Thread.currentThread().getContextClassLoader());
|
||||
return thread;
|
||||
}
|
||||
|
||||
public List<IModFileInfo> getModFiles()
|
||||
{
|
||||
public List<IModFileInfo> getModFiles() {
|
||||
return modFiles;
|
||||
}
|
||||
|
||||
public IModFileInfo getModFileById(String modid)
|
||||
{
|
||||
public IModFileInfo getModFileById(String modid) {
|
||||
return this.fileById.get(modid);
|
||||
}
|
||||
|
||||
<T extends Event & IModBusEvent> Function<Executor, CompletableFuture<List<Throwable>>> futureVisitor(
|
||||
final IModStateTransition.EventGenerator<T> eventGenerator,
|
||||
final BiFunction<ModLoadingStage, Throwable, ModLoadingStage> stateChange) {
|
||||
return executor -> gather(
|
||||
this.mods.stream()
|
||||
.map(mod -> ModContainer.buildTransitionHandler(mod, eventGenerator, stateChange, executor))
|
||||
.collect(Collectors.toList()))
|
||||
.thenComposeAsync(ModList::completableFutureFromExceptionList, executor);
|
||||
}
|
||||
static CompletionStage<List<Throwable>> completableFutureFromExceptionList(List<? extends Map.Entry<?, Throwable>> t) {
|
||||
if (t.stream().noneMatch(e->e.getValue()!=null)) {
|
||||
return CompletableFuture.completedFuture(Collections.emptyList());
|
||||
} else {
|
||||
final List<Throwable> throwables = t.stream().filter(e -> e.getValue() != null).map(Map.Entry::getValue).collect(Collectors.toList());
|
||||
CompletableFuture<List<Throwable>> cf = new CompletableFuture<>();
|
||||
final RuntimeException accumulator = new RuntimeException();
|
||||
cf.completeExceptionally(accumulator);
|
||||
throwables.forEach(exception -> {
|
||||
if (exception instanceof CompletionException) {
|
||||
exception = exception.getCause();
|
||||
}
|
||||
if (exception.getSuppressed().length!=0) {
|
||||
Arrays.stream(exception.getSuppressed()).forEach(accumulator::addSuppressed);
|
||||
} else {
|
||||
accumulator.addSuppressed(exception);
|
||||
}
|
||||
});
|
||||
return cf;
|
||||
}
|
||||
}
|
||||
|
||||
static <V> CompletableFuture<List<Map.Entry<V, Throwable>>> gather(List<? extends CompletableFuture<? extends V>> futures) {
|
||||
List<Map.Entry<V, Throwable>> list = new ArrayList<>(futures.size());
|
||||
CompletableFuture<?>[] results = new CompletableFuture[futures.size()];
|
||||
futures.forEach(future -> {
|
||||
int i = list.size();
|
||||
list.add(null);
|
||||
results[i] = future.whenComplete((result, exception) -> list.set(i, new AbstractMap.SimpleImmutableEntry<>(result, exception)));
|
||||
});
|
||||
return CompletableFuture.allOf(results).handle((r, th)->null).thenApply(res -> list);
|
||||
}
|
||||
|
||||
void setLoadedMods(final List<ModContainer> modContainers)
|
||||
{
|
||||
void setLoadedMods(final List<ModContainer> modContainers) {
|
||||
this.mods = modContainers;
|
||||
this.sortedContainers = modContainers.stream().sorted(Comparator.comparingInt(c -> sortedList.indexOf(c.getModInfo()))).toList();
|
||||
this.indexedMods = modContainers.stream().collect(Collectors.toMap(ModContainer::getModId, Function.identity()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Optional<T> getModObjectById(String modId)
|
||||
{
|
||||
public <T> Optional<T> getModObjectById(String modId) {
|
||||
return getModContainerById(modId).map(ModContainer::getMod).map(o -> (T) o);
|
||||
}
|
||||
|
||||
public Optional<? extends ModContainer> getModContainerById(String modId)
|
||||
{
|
||||
public Optional<? extends ModContainer> getModContainerById(String modId) {
|
||||
return Optional.ofNullable(this.indexedMods.get(modId));
|
||||
}
|
||||
|
||||
public Optional<? extends ModContainer> getModContainerByObject(Object obj)
|
||||
{
|
||||
public Optional<? extends ModContainer> getModContainerByObject(Object obj) {
|
||||
return mods.stream().filter(mc -> mc.getMod() == obj).findFirst();
|
||||
}
|
||||
|
||||
public List<IModInfo> getMods()
|
||||
{
|
||||
public List<IModInfo> getMods() {
|
||||
return this.sortedList;
|
||||
}
|
||||
|
||||
public boolean isLoaded(String modTarget)
|
||||
{
|
||||
public boolean isLoaded(String modTarget) {
|
||||
return this.indexedMods.containsKey(modTarget);
|
||||
}
|
||||
|
||||
public int size()
|
||||
{
|
||||
public int size() {
|
||||
return mods.size();
|
||||
}
|
||||
|
||||
public List<ModFileScanData> getAllScanData()
|
||||
{
|
||||
if (modFileScanData == null)
|
||||
{
|
||||
public List<ModFileScanData> getAllScanData() {
|
||||
if (modFileScanData == null) {
|
||||
modFileScanData = this.sortedList.stream().
|
||||
map(IModInfo::getOwningFile).
|
||||
filter(Objects::nonNull).
|
||||
@@ -206,8 +132,7 @@ public class ModList
|
||||
|
||||
}
|
||||
|
||||
public void forEachModFile(Consumer<IModFile> fileConsumer)
|
||||
{
|
||||
public void forEachModFile(Consumer<IModFile> fileConsumer) {
|
||||
modFiles.stream().map(IModFileInfo::getFile).forEach(fileConsumer);
|
||||
}
|
||||
|
||||
@@ -219,13 +144,15 @@ public class ModList
|
||||
indexedMods.forEach(modContainerConsumer);
|
||||
}
|
||||
|
||||
public void forEachModInOrder(Consumer<ModContainer> containerConsumer) {
|
||||
this.sortedContainers.forEach(containerConsumer);
|
||||
}
|
||||
|
||||
public List<ModContainer> getLoadedMods() {
|
||||
return this.sortedContainers;
|
||||
}
|
||||
|
||||
public <T> Stream<T> applyForEachModContainer(Function<ModContainer, T> function) {
|
||||
return indexedMods.values().stream().map(function);
|
||||
}
|
||||
|
||||
private static class UncaughtModLoadingException extends ModLoadingException {
|
||||
public UncaughtModLoadingException(ModLoadingStage stage, Throwable originalException) {
|
||||
super(null, stage, "fml.modloading.uncaughterror", originalException);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,30 +8,30 @@ package net.minecraftforge.fml;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.fml.loading.FMLEnvironment;
|
||||
import net.minecraftforge.fml.loading.FMLLoader;
|
||||
import net.minecraftforge.fml.loading.ImmediateWindowHandler;
|
||||
import net.minecraftforge.fml.loading.LoadingModList;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.InvalidModIdentifier;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.ModFile;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo;
|
||||
import net.minecraftforge.fml.loading.progress.StartupMessageManager;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.ModInfo;
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
import net.minecraftforge.fml.loading.progress.StartupNotificationManager;
|
||||
import net.minecraftforge.forgespi.language.IModInfo;
|
||||
import net.minecraftforge.forgespi.language.IModLanguageProvider;
|
||||
import net.minecraftforge.forgespi.locating.ForgeFeature;
|
||||
import net.minecraftforge.forgespi.locating.IModFile;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static net.minecraftforge.fml.Logging.CORE;
|
||||
import static net.minecraftforge.fml.Logging.LOADING;
|
||||
|
||||
@@ -71,48 +71,53 @@ import static net.minecraftforge.fml.Logging.LOADING;
|
||||
* and completes the mod loading sequence.</dd>
|
||||
* </dl>
|
||||
*/
|
||||
public class ModLoader
|
||||
{
|
||||
public class ModLoader {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
private static ModLoader INSTANCE;
|
||||
private final LoadingModList loadingModList;
|
||||
|
||||
private final List<ModLoadingException> loadingExceptions;
|
||||
private final Set<IModInfo> erroredModInfos;
|
||||
private final List<ModLoadingWarning> loadingWarnings;
|
||||
private final ModStateManager stateManager;
|
||||
private boolean loadingStateValid;
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private final Optional<Consumer<String>> statusConsumer = StartupMessageManager.modLoaderConsumer();
|
||||
private static boolean loadingStateValid;
|
||||
private final Consumer<String> statusConsumer = StartupNotificationManager.modLoaderConsumer().orElse(msg -> {});
|
||||
private final Set<IModLoadingState> completedStates = new HashSet<>();
|
||||
private ModList modList;
|
||||
|
||||
private ModLoader()
|
||||
{
|
||||
INSTANCE = this;
|
||||
private ModLoader() {
|
||||
this.loadingModList = FMLLoader.getLoadingModList();
|
||||
this.loadingExceptions = FMLLoader.getLoadingModList().getErrors().stream()
|
||||
this.loadingExceptions = this.loadingModList.getErrors().stream()
|
||||
.flatMap(ModLoadingException::fromEarlyException)
|
||||
.collect(Collectors.toList());
|
||||
this.loadingWarnings = FMLLoader.getLoadingModList().getBrokenFiles().stream()
|
||||
.toList();
|
||||
this.loadingWarnings = this.loadingModList.getBrokenFiles().stream()
|
||||
.map(file -> new ModLoadingWarning(null, ModLoadingStage.VALIDATE, InvalidModIdentifier.identifyJarProblem(file.getFilePath()).orElse("fml.modloading.brokenfile"), file.getFileName()))
|
||||
.collect(Collectors.toList());
|
||||
FMLLoader.getLoadingModList().getModFiles().stream()
|
||||
.filter(ModFileInfo::missingLicense) //Search for files with missing licenses
|
||||
.filter(modFileInfo -> modFileInfo.getMods().stream().noneMatch(thisModInfo -> this.loadingExceptions.stream().map(ModLoadingException::getModInfo).anyMatch(otherInfo -> otherInfo == thisModInfo))) //Ignore files where any other mod already encountered an error
|
||||
if (this.loadingExceptions.isEmpty()) {
|
||||
this.erroredModInfos = Collections.emptySet();
|
||||
} else {
|
||||
this.erroredModInfos = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
this.erroredModInfos.addAll(this.loadingExceptions.stream().map(ModLoadingException::getModInfo).toList());
|
||||
}
|
||||
this.loadingModList.getModFiles().stream()
|
||||
.filter(ModFileInfo::missingLicense)
|
||||
.filter(modFileInfo -> modFileInfo.getMods().stream().noneMatch(this.erroredModInfos::contains)) //Ignore files where any other mod already encountered an error
|
||||
.map(modFileInfo -> new ModLoadingException(null, ModLoadingStage.VALIDATE, "fml.modloading.missinglicense", null, modFileInfo.getFile()))
|
||||
.forEach(this.loadingExceptions::add);
|
||||
this.stateManager = new ModStateManager();
|
||||
CrashReportCallables.registerCrashCallable("ModLauncher", FMLLoader::getLauncherInfo);
|
||||
CrashReportCallables.registerCrashCallable("ModLauncher launch target", FMLLoader::launcherHandlerName);
|
||||
CrashReportCallables.registerCrashCallable("ModLauncher naming", FMLLoader::getNaming);
|
||||
CrashReportCallables.registerCrashCallable("ModLauncher services", this::computeModLauncherServiceList);
|
||||
CrashReportCallables.registerCrashCallable("FML Language Providers", this::computeLanguageList);
|
||||
CrashReportCallables.registerCrashCallable("ModLauncher services", ModLoader::computeModLauncherServiceList);
|
||||
CrashReportCallables.registerCrashCallable("FML Language Providers", ModLoader::computeLanguageList);
|
||||
}
|
||||
|
||||
private String computeLanguageList() {
|
||||
return "\n"+FMLLoader.getLanguageLoadingProvider().applyForEach(lp->lp.name() +"@"+ lp.getClass().getPackage().getImplementationVersion()).collect(Collectors.joining("\n\t\t", "\t\t", ""));
|
||||
private static String computeLanguageList() {
|
||||
return "\n" + FMLLoader.getLanguageLoadingProvider()
|
||||
.applyForEach(lp -> lp.name() + "@" + lp.getClass().getPackage().getImplementationVersion())
|
||||
.collect(Collectors.joining("\n\t\t", "\t\t", ""));
|
||||
}
|
||||
|
||||
private String computeModLauncherServiceList() {
|
||||
private static String computeModLauncherServiceList() {
|
||||
final List<Map<String, String>> mods = FMLLoader.modLauncherModList();
|
||||
return "\n"+mods.stream().map(mod->mod.getOrDefault("file","nofile")+
|
||||
" "+mod.getOrDefault("name", "missing")+
|
||||
@@ -121,9 +126,13 @@ public class ModLoader
|
||||
collect(Collectors.joining("\n\t\t","\t\t",""));
|
||||
}
|
||||
|
||||
public static ModLoader get()
|
||||
{
|
||||
return INSTANCE == null ? INSTANCE = new ModLoader() : INSTANCE;
|
||||
public static ModLoader get() {
|
||||
return LazyInit.INSTANCE;
|
||||
}
|
||||
|
||||
private static final class LazyInit {
|
||||
private static final ModLoader INSTANCE = new ModLoader();
|
||||
private LazyInit() {}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,124 +142,199 @@ public class ModLoader
|
||||
* @param periodicTask Optional periodic task to perform on the main thread while other activities run
|
||||
*/
|
||||
public void gatherAndInitializeMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
|
||||
ForgeFeature.registerFeature("javaVersion", ForgeFeature.VersionFeatureTest.forVersionString(IModInfo.DependencySide.BOTH, System.getProperty("java.version")));
|
||||
ForgeFeature.registerFeature("openGLVersion", ForgeFeature.VersionFeatureTest.forVersionString(IModInfo.DependencySide.CLIENT, ImmediateWindowHandler.getGLVersion()));
|
||||
loadingStateValid = true;
|
||||
statusConsumer.ifPresent(c->c.accept("Waiting for scan to complete"));
|
||||
FMLLoader.backgroundScanHandler.waitForScanToComplete(periodicTask);
|
||||
statusConsumer.ifPresent(c->c.accept("Loading mods"));
|
||||
final ModList modList = ModList.of(loadingModList.getModFiles().stream().map(ModFileInfo::getFile).toList(),
|
||||
loadingModList.getMods());
|
||||
if (!this.loadingExceptions.isEmpty()) {
|
||||
LOGGER.fatal(CORE, "Error during pre-loading phase", loadingExceptions.get(0));
|
||||
LOGGER.fatal(CORE, "Error during pre-loading phase", loadingExceptions.getFirst());
|
||||
statusConsumer.accept("ERROR DURING MOD LOADING");
|
||||
modList.setLoadedMods(Collections.emptyList());
|
||||
loadingStateValid = false;
|
||||
throw new LoadingFailedException(loadingExceptions);
|
||||
}
|
||||
statusConsumer.ifPresent(c->c.accept("Building Mod List"));
|
||||
final List<ModContainer> modContainers = loadingModList.getModFiles().stream()
|
||||
.map(ModFileInfo::getFile)
|
||||
.map(this::buildMods)
|
||||
.<ModContainer>mapMulti(Iterable::forEach)
|
||||
List<? extends ForgeFeature.Bound> failedBounds = loadingModList.getMods().stream()
|
||||
.map(ModInfo::getForgeFeatures)
|
||||
.flatMap(List::stream)
|
||||
.filter(bound -> !ForgeFeature.testFeature(FMLEnvironment.dist, bound))
|
||||
.toList();
|
||||
|
||||
if (!failedBounds.isEmpty()) {
|
||||
LOGGER.fatal(CORE, "Failed to validate feature bounds for mods: {}", failedBounds);
|
||||
statusConsumer.accept("ERROR DURING MOD LOADING");
|
||||
modList.setLoadedMods(Collections.emptyList());
|
||||
loadingStateValid = false;
|
||||
throw new LoadingFailedException(failedBounds.stream()
|
||||
.map(fb -> new ModLoadingException(fb.modInfo(), ModLoadingStage.CONSTRUCT, "fml.modloading.feature.missing", null, fb, ForgeFeature.featureValue(fb)))
|
||||
.toList());
|
||||
}
|
||||
|
||||
final var modContainers = new HashMap<String, ModContainer>();
|
||||
for (var file : loadingModList.getModFiles()) {
|
||||
var containers = this.buildMods(file.getFile());
|
||||
for (var container : containers)
|
||||
modContainers.put(container.getModId(), container);
|
||||
}
|
||||
|
||||
if (!loadingExceptions.isEmpty()) {
|
||||
LOGGER.fatal(CORE, "Failed to initialize mod containers", loadingExceptions.get(0));
|
||||
LOGGER.fatal(CORE, "Failed to initialize mod containers", loadingExceptions.getFirst());
|
||||
statusConsumer.accept("ERROR DURING MOD LOADING");
|
||||
modList.setLoadedMods(Collections.emptyList());
|
||||
loadingStateValid = false;
|
||||
throw new LoadingFailedException(loadingExceptions);
|
||||
}
|
||||
modList.setLoadedMods(modContainers);
|
||||
|
||||
// Gather all dependencies, so we can make sure they have finished their transition events before we fire ours
|
||||
for (var mod : modContainers.values()) {
|
||||
for (var dep : mod.getModInfo().getDependencies()) {
|
||||
var target = modContainers.get(dep.getModId());
|
||||
if (target == null)
|
||||
continue;
|
||||
|
||||
switch (dep.getOrdering()) {
|
||||
case AFTER:
|
||||
mod.dependencies.add(target);
|
||||
break;
|
||||
case BEFORE:
|
||||
target.dependencies.add(mod);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modList.setLoadedMods(modContainers.values().stream().toList());
|
||||
this.modList = modList;
|
||||
statusConsumer.ifPresent(c->c.accept("Dispatching gathering events"));
|
||||
stateManager.getStates(ModLoadingPhase.GATHER).forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask));
|
||||
statusConsumer.ifPresent(c->c.accept("Gathering phase complete"));
|
||||
var stateList = stateManager.getStates(ModLoadingPhase.GATHER);
|
||||
var progress = StartupMessageManager.addProgressBar("Mod Gather", stateList.stream().mapToInt(mls -> mls.size().applyAsInt(this.modList)).sum());
|
||||
for (IModLoadingState mls : stateList) {
|
||||
dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress);
|
||||
}
|
||||
progress.complete();
|
||||
}
|
||||
|
||||
public void loadMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
|
||||
statusConsumer.ifPresent(c->c.accept("Performing load phase"));
|
||||
stateManager.getStates(ModLoadingPhase.LOAD).forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask));
|
||||
statusConsumer.ifPresent(c->c.accept("Mod setup complete"));
|
||||
var stateList = stateManager.getStates(ModLoadingPhase.LOAD);
|
||||
var progress = StartupMessageManager.addProgressBar("Mod Loading", stateList.stream().mapToInt(mls -> mls.size().applyAsInt(modList)).sum());
|
||||
for (IModLoadingState mls : stateList) {
|
||||
dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress);
|
||||
}
|
||||
progress.complete();
|
||||
}
|
||||
|
||||
public void finishMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
|
||||
statusConsumer.ifPresent(c->c.accept("Performing completion phase"));
|
||||
stateManager.getStates(ModLoadingPhase.COMPLETE).forEach(mls->dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask));
|
||||
statusConsumer.ifPresent(c->c.accept(String.format("Mod loading complete - %d mods loaded", this.modList.size())));
|
||||
var stateList = stateManager.getStates(ModLoadingPhase.COMPLETE);
|
||||
var progress = StartupMessageManager.addProgressBar("Mod Complete", stateList.stream().mapToInt(mls -> mls.size().applyAsInt(modList)).sum());
|
||||
for (IModLoadingState mls : stateList) {
|
||||
dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress);
|
||||
}
|
||||
statusConsumer.accept(String.format("Mod loading complete - %d mods loaded", this.modList.size()));
|
||||
progress.complete();
|
||||
}
|
||||
|
||||
private void dispatchAndHandleError(IModLoadingState state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker) {
|
||||
private void dispatchAndHandleError(IModLoadingState state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker, final ProgressMeter progressBar) {
|
||||
if (!isLoadingStateValid()) {
|
||||
LOGGER.error("Cowardly refusing to process mod state change request from {}", state);
|
||||
return;
|
||||
}
|
||||
statusConsumer.ifPresent(c->c.accept(state.message().apply(this.modList)));
|
||||
state.inlineRunnable().ifPresent(a->a.accept(this.modList));
|
||||
state.buildTransition(syncExecutor, parallelExecutor).ifPresent(t->waitForTransition(state, syncExecutor, ticker, t));
|
||||
progressBar.label(progressBar.name() + " working");
|
||||
syncExecutor.drive(ticker);
|
||||
|
||||
var inlineRunnable = state.inlineRunnable().orElse(null);
|
||||
if (inlineRunnable != null) handleInlineTransition(inlineRunnable, state, syncExecutor, ticker);
|
||||
|
||||
var transition = state.buildTransition(syncExecutor, parallelExecutor, progressBar).orElse(null);
|
||||
if (transition != null) waitForTransition(state, syncExecutor, ticker, transition);
|
||||
|
||||
completedStates.add(state);
|
||||
}
|
||||
|
||||
private void dispatchAndHandleError(IModLoadingState state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker, Function<Executor, CompletableFuture<Void>> preSyncTask, Function<Executor, CompletableFuture<Void>> postSyncTask) {
|
||||
if (!isLoadingStateValid()) {
|
||||
LOGGER.error("Cowardly refusing to process mod state change request from {}", state);
|
||||
return;
|
||||
}
|
||||
statusConsumer.ifPresent(c->c.accept(state.message().apply(this.modList)));
|
||||
state.inlineRunnable().ifPresent(a->a.accept(this.modList));
|
||||
state.buildTransition(syncExecutor, parallelExecutor, preSyncTask, postSyncTask).ifPresent(t->waitForTransition(state, syncExecutor, ticker, t));
|
||||
private void handleInlineTransition(final Consumer<ModList> transition, final IModLoadingState state, final ModWorkManager.DrivenExecutor syncExecutor, final Runnable ticker) {
|
||||
var pb = StartupMessageManager.addProgressBar("State transition " +state.name()+" running", 0);
|
||||
syncExecutor.drive(ticker);
|
||||
transition.accept(this.modList);
|
||||
syncExecutor.drive(ticker);
|
||||
pb.complete();
|
||||
syncExecutor.drive(ticker);
|
||||
}
|
||||
|
||||
private void waitForTransition(final IModLoadingState state, final ModWorkManager.DrivenExecutor syncExecutor, final Runnable ticker, final CompletableFuture<List<Throwable>> transition) {
|
||||
private void waitForTransition(final IModLoadingState state, final ModWorkManager.DrivenExecutor syncExecutor, final Runnable ticker, final CompletableFuture<Void> transition) {
|
||||
while (!transition.isDone()) {
|
||||
syncExecutor.drive(ticker);
|
||||
}
|
||||
|
||||
try {
|
||||
transition.join();
|
||||
} catch (CompletionException e) {
|
||||
loadingStateValid = false;
|
||||
Throwable t = e.getCause();
|
||||
final List<Throwable> notModLoading = Arrays.stream(t.getSuppressed())
|
||||
.filter(obj -> !(obj instanceof ModLoadingException))
|
||||
.collect(Collectors.toList());
|
||||
if (!notModLoading.isEmpty()) {
|
||||
boolean hasNotModLoadingEx = Arrays.stream(t.getSuppressed())
|
||||
.anyMatch(obj -> !(obj instanceof ModLoadingException));
|
||||
if (hasNotModLoadingEx) {
|
||||
LOGGER.fatal("Encountered non-modloading exceptions!", e);
|
||||
statusConsumer.accept("ERROR DURING MOD LOADING");
|
||||
throw e;
|
||||
}
|
||||
|
||||
final List<ModLoadingException> modLoadingExceptions = Arrays.stream(t.getSuppressed())
|
||||
.filter(ModLoadingException.class::isInstance)
|
||||
.map(ModLoadingException.class::cast)
|
||||
.collect(Collectors.toList());
|
||||
.toList();
|
||||
LOGGER.fatal(LOADING,"Failed to complete lifecycle event {}, {} errors found", state.name(), modLoadingExceptions.size());
|
||||
statusConsumer.accept("ERROR DURING MOD LOADING");
|
||||
throw new LoadingFailedException(modLoadingExceptions);
|
||||
}
|
||||
}
|
||||
|
||||
private List<ModContainer> buildMods(final ModFile modFile)
|
||||
{
|
||||
final Map<String, IModInfo> modInfoMap = modFile.getModFileInfo().getMods().stream().collect(Collectors.toMap(IModInfo::getModId, Function.identity()));
|
||||
private List<ModContainer> buildMods(final IModFile modFile) {
|
||||
final Map<String, IModInfo> modInfoMap = new HashMap<>();
|
||||
for (var mod : modFile.getModFileInfo().getMods())
|
||||
modInfoMap.put(mod.getModId(), mod);
|
||||
|
||||
LOGGER.trace(LOADING, "ModContainer is {}", ModContainer.class.getClassLoader());
|
||||
final List<ModContainer> containers = modFile.getScanResult().getTargets()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(e -> buildModContainerFromTOML(modFile, modInfoMap, e))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
final List<ModContainer> containers = new ArrayList<>();
|
||||
for (var entry : modFile.getScanResult().getTargets().entrySet()) {
|
||||
var container = buildModContainerFromTOML(modFile, modInfoMap, entry.getKey(), entry.getValue());
|
||||
if (container != null)
|
||||
containers.add(container);
|
||||
}
|
||||
|
||||
if (containers.size() != modInfoMap.size()) {
|
||||
LOGGER.fatal(LOADING,"File {} constructed {} mods: {}, but had {} mods specified: {}",
|
||||
var modIds = modInfoMap.values().stream().map(IModInfo::getModId).sorted().toList();
|
||||
var containerIds = containers.stream().map(c -> c != null ? c.getModId() : "(null)").sorted().toList();
|
||||
|
||||
LOGGER.fatal(LOADING, "File {} constructed {} mods: {}, but had {} mods specified: {}",
|
||||
modFile.getFilePath(),
|
||||
containers.size(), containers.stream().map(c -> c != null ? c.getModId() : "(null)").sorted().collect(Collectors.toList()),
|
||||
modInfoMap.size(), modInfoMap.values().stream().map(IModInfo::getModId).sorted().collect(Collectors.toList()));
|
||||
containers.size(), containerIds,
|
||||
modInfoMap.size(), modIds);
|
||||
|
||||
var missingClasses = new ArrayList<>(modIds);
|
||||
missingClasses.removeAll(containerIds);
|
||||
LOGGER.fatal(LOADING, "The following classes are missing, but are reported in the mods.toml: {}", missingClasses);
|
||||
|
||||
var missingMods = new ArrayList<>(containerIds);
|
||||
missingMods.removeAll(modIds);
|
||||
LOGGER.fatal(LOADING, "The following mods are missing, but have classes in the jar: {}", missingMods);
|
||||
|
||||
loadingExceptions.add(new ModLoadingException(null, ModLoadingStage.CONSTRUCT, "fml.modloading.missingclasses", null, modFile.getFilePath()));
|
||||
}
|
||||
|
||||
// remove errored mod containers
|
||||
return containers.stream().filter(mc -> mc.modLoadingStage != ModLoadingStage.ERROR).collect(Collectors.toList());
|
||||
return containers.stream().filter(mc -> mc.modLoadingStage != ModLoadingStage.ERROR).toList();
|
||||
}
|
||||
|
||||
private ModContainer buildModContainerFromTOML(final ModFile modFile, final Map<String, IModInfo> modInfoMap, final Map.Entry<String, ? extends IModLanguageProvider.IModLanguageLoader> idToProviderEntry) {
|
||||
private ModContainer buildModContainerFromTOML(final IModFile modFile, final Map<String, IModInfo> modInfoMap,
|
||||
final String modId, final IModLanguageProvider.IModLanguageLoader languageLoader
|
||||
) {
|
||||
try {
|
||||
final String modId = idToProviderEntry.getKey();
|
||||
final IModLanguageProvider.IModLanguageLoader languageLoader = idToProviderEntry.getValue();
|
||||
IModInfo info = Optional.ofNullable(modInfoMap.get(modId)).
|
||||
// throw a missing metadata error if there is no matching modid in the modInfoMap from the mods.toml file
|
||||
orElseThrow(()->new ModLoadingException(null, ModLoadingStage.CONSTRUCT, "fml.modloading.missingmetadata", null, modId));
|
||||
IModInfo info = modInfoMap.get(modId);
|
||||
// throw a missing metadata error if there is no matching modid in the modInfoMap from the mods.toml file
|
||||
if (info == null)
|
||||
throw new ModLoadingException(null, ModLoadingStage.CONSTRUCT, "fml.modloading.missingmetadata", null, modId);
|
||||
|
||||
return languageLoader.loadMod(info, modFile.getScanResult(), FMLLoader.getGameLayer());
|
||||
} catch (ModLoadingException mle) {
|
||||
// exceptions are caught and added to the error list for later handling
|
||||
@@ -265,7 +349,12 @@ public class ModLoader
|
||||
* and don't want to cause extraneous crashes due to trying to do things that aren't possible in a "broken load"
|
||||
*/
|
||||
public static boolean isLoadingStateValid() {
|
||||
return get().loadingStateValid;
|
||||
return loadingStateValid;
|
||||
}
|
||||
|
||||
public boolean hasCompletedState(final String stateName) {
|
||||
IModLoadingState state = stateManager.findState(stateName);
|
||||
return completedStates.contains(state);
|
||||
}
|
||||
|
||||
public <T extends Event & IModBusEvent> void runEventGenerator(Function<ModContainer, T> generator) {
|
||||
@@ -273,7 +362,7 @@ public class ModLoader
|
||||
LOGGER.error("Cowardly refusing to send event generator to a broken mod state");
|
||||
return;
|
||||
}
|
||||
ModList.get().forEachModContainer((id, mc) -> mc.acceptEvent(generator.apply(mc)));
|
||||
ModList.get().forEachModInOrder(mc -> mc.acceptEvent(generator.apply(mc)));
|
||||
}
|
||||
|
||||
public <T extends Event & IModBusEvent> void postEvent(T e) {
|
||||
@@ -281,16 +370,43 @@ public class ModLoader
|
||||
LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName());
|
||||
return;
|
||||
}
|
||||
ModList.get().forEachModContainer((id, mc) -> mc.acceptEvent(e));
|
||||
ModList.get().forEachModInOrder(mc -> mc.acceptEvent(e));
|
||||
}
|
||||
|
||||
public List<ModLoadingWarning> getWarnings()
|
||||
{
|
||||
public <T extends Event & IModBusEvent> T postEventWithReturn(T e) {
|
||||
if (!loadingStateValid) {
|
||||
LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName());
|
||||
return e;
|
||||
}
|
||||
ModList.get().forEachModInOrder(mc -> mc.acceptEvent(e));
|
||||
return e;
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
public <T extends Event & IModBusEvent> void postEventWrapContainerInModOrder(T event) {
|
||||
postEventWithWrapInModOrder(event,
|
||||
(mc, e) -> ModLoadingContext.get().setActiveContainer(mc),
|
||||
(mc, e) -> ModLoadingContext.get().setActiveContainer(null)
|
||||
);
|
||||
}
|
||||
|
||||
public <T extends Event & IModBusEvent> void postEventWithWrapInModOrder(T e, BiConsumer<ModContainer, T> pre, BiConsumer<ModContainer, T> post) {
|
||||
if (!loadingStateValid) {
|
||||
LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName());
|
||||
return;
|
||||
}
|
||||
ModList.get().forEachModInOrder(mc -> {
|
||||
pre.accept(mc, e);
|
||||
mc.acceptEvent(e);
|
||||
post.accept(mc, e);
|
||||
});
|
||||
}
|
||||
|
||||
public List<ModLoadingWarning> getWarnings() {
|
||||
return ImmutableList.copyOf(this.loadingWarnings);
|
||||
}
|
||||
|
||||
public void addWarning(ModLoadingWarning warning)
|
||||
{
|
||||
public void addWarning(ModLoadingWarning warning) {
|
||||
this.loadingWarnings.add(warning);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,36 +5,60 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import net.minecraftforge.fml.config.IConfigSpec;
|
||||
import net.minecraftforge.fml.config.ModConfig;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class ModLoadingContext
|
||||
{
|
||||
public class ModLoadingContext {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final ThreadLocal<ModLoadingContext> context = ThreadLocal.withInitial(ModLoadingContext::new);
|
||||
private ModContainer activeContainer;
|
||||
private Object languageExtension;
|
||||
private ModLoadingStage stage;
|
||||
|
||||
/**
|
||||
* @deprecated Use the context provided by your language loader in your mod's constructor
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since="1.21.1")
|
||||
public static ModLoadingContext get() {
|
||||
return context.get();
|
||||
}
|
||||
|
||||
private ModContainer activeContainer;
|
||||
|
||||
/**
|
||||
* @deprecated Going to be moved to ForgeHooks for Internal use.
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since="1.21.1")
|
||||
public void setActiveContainer(final ModContainer container) {
|
||||
this.activeContainer = container;
|
||||
this.languageExtension = container == null ? null : container.contextExtension.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Going to be moved to ForgeHooks for Internal use.
|
||||
* @deprecated Override/Use {@link ModLoadingContext#getContainer()}
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since="1.21.1")
|
||||
public ModContainer getActiveContainer() {
|
||||
return activeContainer == null ? ModList.get().getModContainerById("minecraft").orElseThrow(()->new RuntimeException("Where is minecraft???!")) : activeContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Going to be moved to ForgeHooks for Internal use.
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since="1.21.1")
|
||||
public String getActiveNamespace() {
|
||||
return activeContainer == null ? "minecraft" : activeContainer.getNamespace();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@link ModLoadingContext#getActiveContainer()} by default.
|
||||
*/
|
||||
public ModContainer getContainer() {
|
||||
return getActiveContainer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an {@link IExtensionPoint} with the mod container.
|
||||
* @param point The extension point to register
|
||||
@@ -42,15 +66,67 @@ public class ModLoadingContext
|
||||
* @param <T> The type signature of the extension operator
|
||||
*/
|
||||
public <T extends Record & IExtensionPoint<T>> void registerExtensionPoint(Class<? extends IExtensionPoint<T>> point, Supplier<T> extension) {
|
||||
getActiveContainer().registerExtensionPoint(point, extension);
|
||||
getContainer().registerExtensionPoint(point, extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a {@link IExtensionPoint.DisplayTest} with the mod container.
|
||||
* <p>A shorthand for registering a DisplayTest with {@link #registerExtensionPoint(Class, Supplier)}.</p>
|
||||
* @param displayTest The {@link IExtensionPoint.DisplayTest} to register
|
||||
*/
|
||||
public void registerDisplayTest(IExtensionPoint.DisplayTest displayTest) {
|
||||
getContainer().registerDisplayTest(() -> displayTest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a {@link IExtensionPoint.DisplayTest} with the mod container.
|
||||
* <p>A shorthand for registering a DisplayTest supplier with {@link #registerExtensionPoint(Class, Supplier)}.</p>
|
||||
* @param displayTest The {@link Supplier<IExtensionPoint.DisplayTest>} to register
|
||||
*/
|
||||
public void registerDisplayTest(Supplier<IExtensionPoint.DisplayTest> displayTest) {
|
||||
getContainer().registerDisplayTest(displayTest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a {@link IExtensionPoint.DisplayTest} with the mod container.
|
||||
* <p>A shorthand for registering a DisplayTest with {@link #registerExtensionPoint(Class, Supplier)} that also
|
||||
* creates the DisplayTest instance for you using the provided parameters.</p>
|
||||
* @see IExtensionPoint.DisplayTest#DisplayTest(String, BiPredicate)
|
||||
*/
|
||||
public void registerDisplayTest(String version, BiPredicate<String, Boolean> remoteVersionTest) {
|
||||
getContainer().registerDisplayTest(new IExtensionPoint.DisplayTest(version, remoteVersionTest));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a {@link IExtensionPoint.DisplayTest} with the mod container.
|
||||
* <p>A shorthand for registering a DisplayTest with {@link #registerExtensionPoint(Class, Supplier)} that also
|
||||
* creates the DisplayTest instance for you using the provided parameters.</p>
|
||||
* @see IExtensionPoint.DisplayTest#DisplayTest(Supplier, BiPredicate)
|
||||
*/
|
||||
public void registerDisplayTest(Supplier<String> suppliedVersion, BiPredicate<String, Boolean> remoteVersionTest) {
|
||||
getContainer().registerDisplayTest(new IExtensionPoint.DisplayTest(suppliedVersion, remoteVersionTest));
|
||||
}
|
||||
|
||||
public void registerConfig(ModConfig.Type type, IConfigSpec<?> spec) {
|
||||
getActiveContainer().addConfig(new ModConfig(type, spec, getActiveContainer()));
|
||||
if (spec.isEmpty())
|
||||
{
|
||||
// This handles the case where a mod tries to register a config, without any options configured inside it.
|
||||
LOGGER.debug("Attempted to register an empty config for type {} on mod {}", type, getContainer().getModId());
|
||||
return;
|
||||
}
|
||||
|
||||
getContainer().addConfig(new ModConfig(type, spec, getContainer()));
|
||||
}
|
||||
|
||||
public void registerConfig(ModConfig.Type type, IConfigSpec<?> spec, String fileName) {
|
||||
getActiveContainer().addConfig(new ModConfig(type, spec, getActiveContainer(), fileName));
|
||||
if (spec.isEmpty())
|
||||
{
|
||||
// This handles the case where a mod tries to register a config, without any options configured inside it.
|
||||
LOGGER.debug("Attempted to register an empty config for type {} on mod {} using file name {}", type, getContainer().getModId(), fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
getContainer().addConfig(new ModConfig(type, spec, getContainer(), fileName));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,10 +5,34 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
/**
|
||||
* Phases of mod loading, for grouping mod loading states.
|
||||
*/
|
||||
public enum ModLoadingPhase {
|
||||
/**
|
||||
* Special phase for exceptional situations and error handling, where mod loading cannot continue normally.
|
||||
*
|
||||
* <p>There is conventionally only one state with this phase.</p>
|
||||
*/
|
||||
ERROR, // Special state for error handling
|
||||
/**
|
||||
* Phase for discovering and gathering mods for loading.
|
||||
*/
|
||||
GATHER,
|
||||
/**
|
||||
* Phase for the loading of mods found from mod discovery.
|
||||
*
|
||||
* @see #GATHER
|
||||
*/
|
||||
LOAD,
|
||||
/**
|
||||
* Phase after mod loading has completed, for post-loading tasks.
|
||||
*
|
||||
* @see #LOAD
|
||||
*/
|
||||
COMPLETE,
|
||||
DONE;
|
||||
/**
|
||||
* Marker phase for the last state in the full mod loading process.
|
||||
*/
|
||||
DONE
|
||||
}
|
||||
@@ -5,16 +5,53 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
/**
|
||||
* Mod loading stage of mod containers during the mod loading process. These will have a corresponding {@link ModLoadingState}
|
||||
* in the basic mod loading process provided by FML.
|
||||
*
|
||||
* <p>Each mod loading stage has a global {@link DeferredWorkQueue}, which is populated during the execution of the state
|
||||
* associated with this stage and emptied at the end of the state's execution.</p>
|
||||
*/
|
||||
public enum ModLoadingStage
|
||||
{
|
||||
/**
|
||||
* Special stage for exceptional situations and error handling.
|
||||
*/
|
||||
ERROR,
|
||||
/**
|
||||
* Validation of the mod list.
|
||||
* TODO: figure out where this is used and why this exists instead of CONSTRUCT being the first normal stage
|
||||
*/
|
||||
VALIDATE,
|
||||
/**
|
||||
* Default stage of mod containers after construction.
|
||||
*/
|
||||
CONSTRUCT,
|
||||
/**
|
||||
* Common (non-side-specific) setup and initialization.
|
||||
*/
|
||||
COMMON_SETUP,
|
||||
/**
|
||||
* Side-specific setup and initialization.
|
||||
*
|
||||
* @see net.minecraftforge.api.distmarker.Dist
|
||||
*/
|
||||
SIDED_SETUP,
|
||||
/**
|
||||
* Stage for enqueuing {@link net.minecraftforge.fml.InterModComms} messages for later processing.
|
||||
*/
|
||||
ENQUEUE_IMC,
|
||||
/**
|
||||
* Stage for processing received messages though {@link net.minecraftforge.fml.InterModComms}.
|
||||
*/
|
||||
PROCESS_IMC,
|
||||
/**
|
||||
* Marks the completion of mod loading for this container.
|
||||
*/
|
||||
COMPLETE,
|
||||
/**
|
||||
* Marks the completion of the full mod loading process.
|
||||
*/
|
||||
DONE;
|
||||
|
||||
private final DeferredWorkQueue deferredWorkQueue;
|
||||
@@ -23,14 +60,27 @@ public enum ModLoadingStage
|
||||
deferredWorkQueue = new DeferredWorkQueue(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return the next stage after this stage, or {@link #ERROR} if the exception is not {@code null}}
|
||||
*
|
||||
* @param exception the exception that occurred during this stage, may be {@code null}
|
||||
*/
|
||||
ModLoadingStage nextState(Throwable exception) {
|
||||
return exception != null ? ERROR : values()[this.ordinal()+1];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return this stage, or {@link #ERROR} if the exception is not {@code null}}
|
||||
*
|
||||
* @param exception the exception that occurred during this stage, may be {@code null}
|
||||
*/
|
||||
public ModLoadingStage currentState(Throwable exception) {
|
||||
return exception != null ? ERROR : this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return the deferred work queue for this stage}
|
||||
*/
|
||||
public DeferredWorkQueue getDeferredWorkQueue() {
|
||||
return deferredWorkQueue;
|
||||
}
|
||||
|
||||
@@ -7,32 +7,161 @@ package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.ToIntFunction;
|
||||
|
||||
public record ModLoadingState(String name, String previous, Function<ModList, String> message, ModLoadingPhase phase, Optional<Consumer<ModList>> inlineRunnable, Optional<IModStateTransition> transition) implements IModLoadingState {
|
||||
/**
|
||||
* Implementation of the {@link IModLoadingState} interface.
|
||||
*
|
||||
* @param name the name of this state
|
||||
* @param previous the name of the state immediately previous to this state
|
||||
* @param message a function returning a human-friendly message for this state
|
||||
* @param phase the mod loading phase this state belongs to
|
||||
* @param inlineRunnable an optional runnable, which runs before starting the transition from this state to the next
|
||||
* @param transition optional state transition information
|
||||
*/
|
||||
public record ModLoadingState(
|
||||
String name,
|
||||
String previous,
|
||||
Function<ModList, String> message,
|
||||
ToIntFunction<ModList> size,
|
||||
ModLoadingPhase phase,
|
||||
Optional<Consumer<ModList>> inlineRunnable,
|
||||
Optional<IModStateTransition> transition
|
||||
) implements IModLoadingState {
|
||||
@Override
|
||||
public <T extends Event & IModBusEvent> Optional<CompletableFuture<List<Throwable>>> buildTransition(final Executor syncExecutor, final Executor parallelExecutor, Function<Executor, CompletableFuture<Void>> preSyncTask, Function<Executor, CompletableFuture<Void>> postSyncTask) {
|
||||
return transition.map(t->t.build(syncExecutor, parallelExecutor, preSyncTask, postSyncTask));
|
||||
public <T extends Event & IModBusEvent> Optional<CompletableFuture<Void>> buildTransition(
|
||||
final Executor syncExecutor,
|
||||
final Executor parallelExecutor,
|
||||
final ProgressMeter progressBar,
|
||||
final Function<Executor, CompletableFuture<Void>> preSyncTask,
|
||||
final Function<Executor, CompletableFuture<Void>> postSyncTask
|
||||
) {
|
||||
var transition = this.transition.orElse(null);
|
||||
return transition == null
|
||||
? Optional.empty()
|
||||
: Optional.ofNullable(transition.build(name, syncExecutor, parallelExecutor, progressBar, preSyncTask, postSyncTask));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return an empty mod loading state} The mod loading state has a blank human-readable message, no inline runnable,
|
||||
* and no state transition information.
|
||||
*
|
||||
* @param name the name of the state
|
||||
* @param previous the name of the immediately previous state to this state
|
||||
* @param phase the mod loading phase the state belongs to
|
||||
*
|
||||
* @deprecated Use the builder
|
||||
*/
|
||||
@Deprecated(since = "1.21.3", forRemoval = true)
|
||||
public static ModLoadingState empty(final String name, final String previous, final ModLoadingPhase phase) {
|
||||
return new ModLoadingState(name, previous, ml->"", phase, Optional.empty(), Optional.empty());
|
||||
return of(name, phase).after(previous).empty();
|
||||
}
|
||||
|
||||
public static ModLoadingState withTransition(final String name, final String previous, final ModLoadingPhase phase, IModStateTransition transition) {
|
||||
return new ModLoadingState(name, previous, ml->"Processing transition "+name, phase, Optional.empty(), Optional.of(transition));
|
||||
/**
|
||||
* Returns a mod loading state with state transition information and a default human-friendly message of
|
||||
* {@code Processing transition [name]}.
|
||||
*
|
||||
* @param name the name of the state
|
||||
* @param previous the name of the immediately previous state to this state
|
||||
* @param phase the mod loading phase the state belongs to
|
||||
* @param transition the state transition information
|
||||
*
|
||||
* @deprecated Use the builder
|
||||
*/
|
||||
@Deprecated(since = "1.21.3", forRemoval = true)
|
||||
public static ModLoadingState withTransition(final String name, final String previous, final ModLoadingPhase phase,
|
||||
final IModStateTransition transition) {
|
||||
return of(name, phase).after(previous).withTransition(transition);
|
||||
}
|
||||
|
||||
public static ModLoadingState withTransition(final String name, final String previous, final Function<ModList, String> message, final ModLoadingPhase phase, IModStateTransition transition) {
|
||||
return new ModLoadingState(name, previous, message, phase, Optional.empty(), Optional.of(transition));
|
||||
/**
|
||||
* Returns a mod loading state with state transition information and a custom human-friendly message function.
|
||||
*
|
||||
* @param name the name of the state
|
||||
* @param previous the name of the immediately previous state to this state
|
||||
* @param message a function returning a human-friendly message for this state
|
||||
* @param phase the mod loading phase the state belongs to
|
||||
* @param transition the state transition information
|
||||
* @return a mod loading state with state transition information and a custom message
|
||||
*
|
||||
* @deprecated Use the builder
|
||||
*/
|
||||
@Deprecated(since = "1.21.3", forRemoval = true)
|
||||
public static ModLoadingState withTransition(final String name, final String previous,
|
||||
final Function<ModList, String> message, final ModLoadingPhase phase,
|
||||
final IModStateTransition transition) {
|
||||
return of(name, phase).after(previous).message(message).withTransition(transition);
|
||||
}
|
||||
public static ModLoadingState withInline(final String name, final String previous, final ModLoadingPhase phase, Consumer<ModList> inline) {
|
||||
return new ModLoadingState(name, previous, ml->"Processing work "+name, phase, Optional.of(inline), Optional.empty());
|
||||
|
||||
/**
|
||||
* Returns a mod loading state with an inline runnable and a default human-friendly message of {@code Processing
|
||||
* work [name]}.
|
||||
*
|
||||
* @param name the name of the state
|
||||
* @param previous the name of the immediately previous state to this state
|
||||
* @param phase the mod loading phase the state belongs to
|
||||
* @param inline an optional runnable, which runs before starting the transition from this state to the next
|
||||
* @return a mod loading state with an inline runnable and default message
|
||||
*
|
||||
* @deprecated Use the builder
|
||||
*/
|
||||
@Deprecated(since = "1.21.3", forRemoval = true)
|
||||
public static ModLoadingState withInline(final String name, final String previous, final ModLoadingPhase phase,
|
||||
final Consumer<ModList> inline) {
|
||||
return of(name, phase).after(previous).withInline(inline);
|
||||
}
|
||||
|
||||
public static Builder of(final String name, final ModLoadingPhase phase) {
|
||||
return new Builder(name, phase);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private final String name;
|
||||
private final ModLoadingPhase phase;
|
||||
private String after;
|
||||
private Function<ModList, String> message = null;
|
||||
private ToIntFunction<ModList> size = null;
|
||||
|
||||
private Builder(final String name, final ModLoadingPhase phase) {
|
||||
this.name = name;
|
||||
this.phase = phase;
|
||||
}
|
||||
|
||||
public Builder after(final ModLoadingState value) { return after(value.name()); }
|
||||
public Builder after(final String value) {
|
||||
this.after = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder message(final String value) { return message(ml -> value); }
|
||||
public Builder message(final Function<ModList, String> value) {
|
||||
this.message = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder size(final int value) { return size(ml -> value); }
|
||||
public Builder size(final ToIntFunction<ModList> value) {
|
||||
this.size = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ModLoadingState empty() {
|
||||
return new ModLoadingState(name, after, message != null ? message : ml -> "", size != null ? size : ml -> 0, phase, Optional.empty(), Optional.empty());
|
||||
}
|
||||
|
||||
public ModLoadingState withTransition(final IModStateTransition transition) {
|
||||
return new ModLoadingState(name, after, message != null ? message : ml -> "Processing transition " + name, size != null ? size : ModList::size, phase, Optional.empty(), Optional.of(transition));
|
||||
}
|
||||
|
||||
public ModLoadingState withInline(final Consumer<ModList> inline) {
|
||||
return new ModLoadingState(name, after, message != null ? message : ml -> "Processing work " + name, size != null ? size : ml -> 0, phase, Optional.of(inline), Optional.empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,7 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ModLoadingWarning
|
||||
{
|
||||
public class ModLoadingWarning {
|
||||
/**
|
||||
* Mod Info for mod with warning
|
||||
*/
|
||||
|
||||
@@ -16,15 +16,13 @@ import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public class ModStateManager {
|
||||
static ModStateManager INSTANCE;
|
||||
private final EnumMap<ModLoadingPhase, List<IModLoadingState>> stateMap;
|
||||
|
||||
public ModStateManager() {
|
||||
INSTANCE = this;
|
||||
final var sp = ServiceLoader.load(FMLLoader.getGameLayer(), IModStateProvider.class);
|
||||
this.stateMap = ServiceLoaderUtils.streamWithErrorHandling(sp, sce->{})
|
||||
.map(IModStateProvider::getAllStates)
|
||||
.<IModLoadingState>mapMulti(Iterable::forEach)
|
||||
.flatMap(List::stream)
|
||||
.collect(Collectors.groupingBy(IModLoadingState::phase, ()->new EnumMap<>(ModLoadingPhase.class), Collectors.toUnmodifiableList()));
|
||||
}
|
||||
|
||||
@@ -36,7 +34,18 @@ public class ModStateManager {
|
||||
var dummy = ModLoadingState.empty("", "", phase);
|
||||
nodes.forEach(graph::addNode);
|
||||
graph.addNode(dummy);
|
||||
nodes.forEach(n->graph.putEdge(lookup.getOrDefault(n.previous(), dummy), n));
|
||||
for (IModLoadingState node : nodes) {
|
||||
graph.putEdge(lookup.getOrDefault(node.previous(), dummy), node);
|
||||
}
|
||||
return TopologicalSort.topologicalSort(graph, Comparator.comparingInt(nodes::indexOf)).stream().filter(st->st!=dummy).toList();
|
||||
}
|
||||
|
||||
public IModLoadingState findState(final String stateName) {
|
||||
return stateMap.values()
|
||||
.stream()
|
||||
.flatMap(List::stream)
|
||||
.filter(mls -> mls.name().equals(stateName))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown IModLoadingState: " + stateName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.fml.IModStateTransition.EventGenerator;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
|
||||
@ApiStatus.Internal
|
||||
class ModStateTransitionHelper {
|
||||
static final IModStateTransition NOOP = new NoopTransition();
|
||||
record NoopTransition() implements IModStateTransition {
|
||||
@Override
|
||||
public ThreadSelector threadSelector() {
|
||||
return ThreadSelector.SYNC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiFunction<Executor, CompletableFuture<Void>, CompletableFuture<Void>> finalActivityGenerator() {
|
||||
return (e, t) -> t.thenApplyAsync(Function.identity(), e);
|
||||
}
|
||||
}
|
||||
|
||||
static <V> CompletionStage<Void> completableFutureFromExceptionList(List<FutureResult<V>> t) {
|
||||
if (t.stream().noneMatch(e -> e.exception() != null)) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
} else {
|
||||
final var throwables = t.stream().map(FutureResult::exception).filter(e -> e != null).toList();
|
||||
CompletableFuture<Void> cf = new CompletableFuture<>();
|
||||
final RuntimeException accumulator = new RuntimeException();
|
||||
cf.completeExceptionally(accumulator);
|
||||
for (Throwable exception : throwables) {
|
||||
if (exception instanceof CompletionException) {
|
||||
exception = exception.getCause();
|
||||
}
|
||||
if (exception.getSuppressed().length != 0) {
|
||||
for (Throwable throwable : exception.getSuppressed()) {
|
||||
accumulator.addSuppressed(throwable);
|
||||
}
|
||||
} else {
|
||||
accumulator.addSuppressed(exception);
|
||||
}
|
||||
}
|
||||
return cf;
|
||||
}
|
||||
}
|
||||
|
||||
record FutureResult<V>(V value, Throwable exception){}
|
||||
|
||||
static <V> CompletableFuture<List<FutureResult<V>>> gather(Collection<? extends CompletableFuture<? extends V>> futures) {
|
||||
var list = new ArrayList<FutureResult<V>>(futures.size());
|
||||
var results = new CompletableFuture[futures.size()];
|
||||
|
||||
for (var future : futures) {
|
||||
int i = list.size();
|
||||
list.add(null);
|
||||
results[i] = future.whenComplete((result, exception) -> list.set(i, new FutureResult<>(result, exception)));
|
||||
}
|
||||
|
||||
return CompletableFuture.allOf(results).handle((r, th)->null).thenApply(res -> list);
|
||||
}
|
||||
|
||||
private static <T extends Event & IModBusEvent> void addCompletableFutureTaskForModDispatch(
|
||||
final IModStateTransition transition,
|
||||
final Executor executor,
|
||||
final List<CompletableFuture<Void>> completableFutures,
|
||||
final ProgressMeter progressBar,
|
||||
final EventGenerator<T> eventGenerator,
|
||||
final BiFunction<ModLoadingStage, Throwable, ModLoadingStage> nextState
|
||||
) {
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
var preDispatchHook = getHook(transition.preDispatchHook(), executor, eventGenerator);
|
||||
if (preDispatchHook != null)
|
||||
completableFutures.add(preDispatchHook);
|
||||
|
||||
var modFutures = new LinkedHashMap<String, CompletableFuture<Void>>();
|
||||
for (var mod : ModList.get().getLoadedMods()) {
|
||||
|
||||
CompletableFuture<Void> parent = null;
|
||||
if (mod.dependencies.isEmpty()) {
|
||||
parent = CompletableFuture.allOf();
|
||||
} else {
|
||||
var deps = new CompletableFuture[mod.dependencies.size()];
|
||||
int idx = 0;
|
||||
for (var depContainer : mod.dependencies) {
|
||||
var future = modFutures.get(depContainer.getModId());
|
||||
if (future == null)
|
||||
throw new IllegalStateException("Could not find dependency future " + depContainer.getModId() + " for " + mod.getModId());
|
||||
deps[idx++] = future;
|
||||
}
|
||||
parent = CompletableFuture.allOf(deps);
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
var dispatch = parent
|
||||
.thenRunAsync(() -> {
|
||||
ModLoadingContext.get().setActiveContainer(mod);
|
||||
var handler = mod.activityMap.get(mod.modLoadingStage);
|
||||
if (handler != null)
|
||||
handler.run();
|
||||
mod.acceptEvent(eventGenerator.apply(mod));
|
||||
}, executor)
|
||||
.whenComplete((mc, exception) -> {
|
||||
mod.modLoadingStage = nextState.apply(mod.modLoadingStage, exception);
|
||||
progressBar.increment();
|
||||
ModLoadingContext.get().setActiveContainer(null);
|
||||
});
|
||||
|
||||
modFutures.put(mod.getModId(), dispatch);
|
||||
}
|
||||
|
||||
var dispatch = gather(modFutures.values()).thenComposeAsync(ModStateTransitionHelper::completableFutureFromExceptionList, executor);
|
||||
completableFutures.add(dispatch);
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
var postDispatchHook = getHook(transition.preDispatchHook(), executor, eventGenerator);
|
||||
if (postDispatchHook != null)
|
||||
completableFutures.add(postDispatchHook);
|
||||
}
|
||||
|
||||
private static <T extends Event & IModBusEvent> CompletableFuture<Void> getHook(BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> hook, Executor executor, EventGenerator<T> eventGenerator) {
|
||||
if (hook == null || hook == IModStateTransition.NULL_HOOK) return null;
|
||||
@SuppressWarnings("unchecked")
|
||||
var hookTyped = (BiFunction<Executor, EventGenerator<T>, CompletableFuture<Void>>)hook;
|
||||
return hookTyped.apply(executor, eventGenerator);
|
||||
}
|
||||
|
||||
static <T extends Event & IModBusEvent> CompletableFuture<Void> build(
|
||||
final IModStateTransition transition,
|
||||
final String name,
|
||||
final Executor syncExecutor,
|
||||
final Executor parallelExecutor,
|
||||
final ProgressMeter progressBar,
|
||||
final Function<Executor, CompletableFuture<Void>> preSyncTask,
|
||||
final Function<Executor, CompletableFuture<Void>> postSyncTask
|
||||
) {
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
||||
final var executor = transition.threadSelector().apply(syncExecutor, parallelExecutor);
|
||||
|
||||
@SuppressWarnings({ "removal", "unchecked" })
|
||||
var events = transition.eventFunctionStream().get().map(f -> (EventGenerator<T>)f).toList();
|
||||
for (int x = 0; x < events.size(); x++) {
|
||||
var gen = events.get(x);
|
||||
BiFunction<ModLoadingStage, Throwable, ModLoadingStage> state = x == events.size() - 1
|
||||
? transition.nextModLoadingStage()
|
||||
: ModLoadingStage::currentState;
|
||||
addCompletableFutureTaskForModDispatch(transition, executor, futures, progressBar, gen, state);
|
||||
}
|
||||
|
||||
final CompletableFuture<Void> preSyncTaskCF = preSyncTask.apply(syncExecutor);
|
||||
final CompletableFuture<Void> eventDispatchCF = gather(futures).thenCompose(ModStateTransitionHelper::completableFutureFromExceptionList);
|
||||
final CompletableFuture<Void> postEventDispatchCF = preSyncTaskCF
|
||||
.thenApplyAsync(n -> {
|
||||
progressBar.label(progressBar.name() + ": dispatching " + name);
|
||||
return null;
|
||||
}, parallelExecutor)
|
||||
.thenComposeAsync(n -> eventDispatchCF, parallelExecutor)
|
||||
.thenApply(r -> {
|
||||
postSyncTask.apply(syncExecutor);
|
||||
return null;
|
||||
});
|
||||
return transition.finalActivityGenerator().apply(syncExecutor, postEventDispatchCF);
|
||||
}
|
||||
}
|
||||
@@ -17,14 +17,16 @@ import static net.minecraftforge.fml.Logging.LOADING;
|
||||
public class ModWorkManager {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
private static final long PARK_TIME = TimeUnit.MILLISECONDS.toNanos(1);
|
||||
|
||||
public interface DrivenExecutor extends Executor {
|
||||
boolean selfDriven();
|
||||
boolean driveOne();
|
||||
|
||||
default void drive(Runnable ticker) {
|
||||
if (!selfDriven()) {
|
||||
while (driveOne()) {
|
||||
ticker.run();
|
||||
ticker.run();
|
||||
while (true) {
|
||||
if (!driveOne()) break;
|
||||
}
|
||||
} else {
|
||||
// park for a bit so other threads can schedule
|
||||
@@ -32,8 +34,11 @@ public class ModWorkManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
private static class SyncExecutor implements DrivenExecutor {
|
||||
private ConcurrentLinkedDeque<Runnable> tasks = new ConcurrentLinkedDeque<>();
|
||||
|
||||
private record SyncExecutor(ConcurrentLinkedDeque<Runnable> tasks) implements DrivenExecutor {
|
||||
public SyncExecutor() {
|
||||
this(new ConcurrentLinkedDeque<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean driveOne() {
|
||||
@@ -55,13 +60,7 @@ public class ModWorkManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static class WrappingExecutor implements DrivenExecutor {
|
||||
private final Executor wrapped;
|
||||
|
||||
public WrappingExecutor(final Executor executor) {
|
||||
this.wrapped = executor;
|
||||
}
|
||||
|
||||
private record WrappingExecutor(Executor wrapped) implements DrivenExecutor {
|
||||
@Override
|
||||
public boolean selfDriven() {
|
||||
return true;
|
||||
@@ -78,11 +77,9 @@ public class ModWorkManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static SyncExecutor syncExecutor;
|
||||
private static final SyncExecutor syncExecutor = new SyncExecutor();
|
||||
|
||||
public static DrivenExecutor syncExecutor() {
|
||||
if (syncExecutor == null)
|
||||
syncExecutor = new SyncExecutor();
|
||||
return syncExecutor;
|
||||
}
|
||||
|
||||
@@ -90,14 +87,8 @@ public class ModWorkManager {
|
||||
return new WrappingExecutor(executor);
|
||||
}
|
||||
|
||||
private static ForkJoinPool parallelThreadPool;
|
||||
public static Executor parallelExecutor() {
|
||||
if (parallelThreadPool == null) {
|
||||
final int loadingThreadCount = FMLConfig.loadingThreadCount();
|
||||
LOGGER.debug(LOADING, "Using {} threads for parallel mod-loading", loadingThreadCount);
|
||||
parallelThreadPool = new ForkJoinPool(loadingThreadCount, ModWorkManager::newForkJoinWorkerThread, null, false);
|
||||
}
|
||||
return parallelThreadPool;
|
||||
return LazyInit.PARALLEL_EXECUTOR;
|
||||
}
|
||||
|
||||
private static ForkJoinWorkerThread newForkJoinWorkerThread(ForkJoinPool pool) {
|
||||
@@ -108,4 +99,14 @@ public class ModWorkManager {
|
||||
return thread;
|
||||
}
|
||||
|
||||
private static final class LazyInit {
|
||||
private LazyInit() {}
|
||||
private static final ForkJoinPool PARALLEL_EXECUTOR;
|
||||
|
||||
static {
|
||||
final int loadingThreadCount = FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.MAX_THREADS);
|
||||
LOGGER.debug(LOADING, "Using {} threads for parallel mod-loading", loadingThreadCount);
|
||||
PARALLEL_EXECUTOR = new ForkJoinPool(loadingThreadCount, ModWorkManager::newForkJoinWorkerThread, null, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ public final class OptionalMod<T>
|
||||
return new OptionalMod<>(modId);
|
||||
}
|
||||
|
||||
private static OptionalMod<?> EMPTY = new OptionalMod<>(true);
|
||||
private static final OptionalMod<?> EMPTY = new OptionalMod<>(true);
|
||||
private static <T> OptionalMod<T> empty() {
|
||||
@SuppressWarnings("unchecked")
|
||||
OptionalMod<T> t = (OptionalMod<T>) EMPTY;
|
||||
@@ -213,8 +213,8 @@ public final class OptionalMod<T>
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (this == obj) return true;
|
||||
if (obj instanceof OptionalMod) {
|
||||
return Objects.equals(((OptionalMod)obj).modId, modId);
|
||||
if (obj instanceof OptionalMod<?> optionalMod) {
|
||||
return Objects.equals(optionalMod.modId, modId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5,19 +5,30 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
import net.minecraftforge.fml.loading.progress.StartupNotificationManager;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class StartupMessageManager {
|
||||
public static void addModMessage(final String message) {
|
||||
net.minecraftforge.fml.loading.progress.StartupMessageManager.addModMessage(message);
|
||||
StartupNotificationManager.addModMessage(message);
|
||||
}
|
||||
|
||||
public static Optional<Consumer<String>> modLoaderConsumer() {
|
||||
return net.minecraftforge.fml.loading.progress.StartupMessageManager.modLoaderConsumer();
|
||||
return StartupNotificationManager.modLoaderConsumer();
|
||||
}
|
||||
|
||||
public static Optional<Consumer<String>> mcLoaderConsumer() {
|
||||
return net.minecraftforge.fml.loading.progress.StartupMessageManager.mcLoaderConsumer();
|
||||
return StartupNotificationManager.mcLoaderConsumer();
|
||||
}
|
||||
|
||||
public static ProgressMeter addProgressBar(final String barName, final int count) {
|
||||
return StartupNotificationManager.addProgressBar(barName, count);
|
||||
}
|
||||
|
||||
public static ProgressMeter prependProgressBar(final String barName, final int count) {
|
||||
return StartupNotificationManager.prependProgressBar(barName, count);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ package net.minecraftforge.fml;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import net.minecraftforge.fml.loading.FMLConfig;
|
||||
import net.minecraftforge.fml.loading.FMLLoader;
|
||||
import net.minecraftforge.forgespi.language.IModInfo;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -35,81 +34,72 @@ import java.util.zip.GZIPInputStream;
|
||||
|
||||
import static net.minecraftforge.fml.VersionChecker.Status.*;
|
||||
|
||||
public class VersionChecker
|
||||
{
|
||||
public class VersionChecker {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
private static final int MAX_HTTP_REDIRECTS = Integer.getInteger("http.maxRedirects", 20);
|
||||
private static final int HTTP_TIMEOUT_SECS = Integer.getInteger("http.timeoutSecs", 15);
|
||||
|
||||
public enum Status
|
||||
{
|
||||
public enum Status {
|
||||
PENDING(),
|
||||
FAILED(),
|
||||
UP_TO_DATE(),
|
||||
OUTDATED(3, true),
|
||||
OUTDATED(3, true, true),
|
||||
AHEAD(),
|
||||
BETA(),
|
||||
BETA_OUTDATED(6, true);
|
||||
BETA_OUTDATED(6, true, true);
|
||||
|
||||
final int sheetOffset;
|
||||
final boolean draw, animated;
|
||||
private final int sheetOffset;
|
||||
private final boolean draw, animated;
|
||||
|
||||
Status()
|
||||
{
|
||||
Status() {
|
||||
this(0, false, false);
|
||||
}
|
||||
|
||||
Status(int sheetOffset)
|
||||
{
|
||||
this(sheetOffset, true, false);
|
||||
}
|
||||
|
||||
Status(int sheetOffset, boolean animated)
|
||||
{
|
||||
this(sheetOffset, true, animated);
|
||||
}
|
||||
|
||||
Status(int sheetOffset, boolean draw, boolean animated)
|
||||
{
|
||||
Status(int sheetOffset, boolean draw, boolean animated) {
|
||||
this.sheetOffset = sheetOffset;
|
||||
this.draw = draw;
|
||||
this.animated = animated;
|
||||
}
|
||||
|
||||
public int getSheetOffset()
|
||||
{
|
||||
public int getSheetOffset() {
|
||||
return sheetOffset;
|
||||
}
|
||||
|
||||
public boolean shouldDraw()
|
||||
{
|
||||
public boolean shouldDraw() {
|
||||
return draw;
|
||||
}
|
||||
|
||||
public boolean isAnimated()
|
||||
{
|
||||
public boolean isAnimated() {
|
||||
return animated;
|
||||
}
|
||||
|
||||
public boolean isOutdated() {
|
||||
return this == OUTDATED || this == BETA_OUTDATED;
|
||||
}
|
||||
}
|
||||
|
||||
public record CheckResult(VersionChecker.Status status, ComparableVersion target, Map<ComparableVersion, String> changes, String url) {}
|
||||
|
||||
public static void startVersionCheck()
|
||||
{
|
||||
new Thread("Forge Version Check")
|
||||
{
|
||||
public static void startVersionCheck() {
|
||||
new Thread("Forge Version Check") {
|
||||
private HttpClient client;
|
||||
private String mcVersion;
|
||||
private String agent;
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if (!FMLConfig.runVersionCheck())
|
||||
{
|
||||
public void run() {
|
||||
if (!FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.VERSION_CHECK)) {
|
||||
LOGGER.info("Global Forge version check system disabled, no further processing.");
|
||||
return;
|
||||
}
|
||||
|
||||
var mc = ModList.get().getModContainerById("minecraft");
|
||||
var forge = ModList.get().getModContainerById("forge");
|
||||
this.mcVersion = mc.get().getModInfo().getVersion().toString();
|
||||
|
||||
this.agent = "Java-http-client/" + System.getProperty("java.version")
|
||||
+ " MinecraftForge/" + mcVersion + '-' + forge.get().getModInfo().getVersion();
|
||||
|
||||
client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(HTTP_TIMEOUT_SECS)).build();
|
||||
gatherMods().forEach(this::process);
|
||||
}
|
||||
@@ -117,22 +107,23 @@ public class VersionChecker
|
||||
/**
|
||||
* Returns the response body as a String for the given URL while following redirects
|
||||
*/
|
||||
private String openUrlString(URL url) throws IOException, URISyntaxException, InterruptedException {
|
||||
private String openUrlString(URL url, IModInfo mod) throws IOException, URISyntaxException, InterruptedException {
|
||||
URL currentUrl = url;
|
||||
for (int redirects = 0; redirects < MAX_HTTP_REDIRECTS; redirects++)
|
||||
{
|
||||
var userAgent = this.agent + ' ' + mod.getModId() + '/' + mod.getVersion();
|
||||
|
||||
for (int redirects = 0; redirects < MAX_HTTP_REDIRECTS; redirects++) {
|
||||
var request = HttpRequest.newBuilder()
|
||||
.uri(currentUrl.toURI())
|
||||
.timeout(Duration.ofSeconds(HTTP_TIMEOUT_SECS))
|
||||
.setHeader("Accept-Encoding", "gzip")
|
||||
.setHeader("User-Agent", userAgent)
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
final HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
||||
|
||||
int responseCode = response.statusCode();
|
||||
if (responseCode >= 300 && responseCode <= 399)
|
||||
{
|
||||
if (responseCode >= 300 && responseCode <= 399) {
|
||||
String newLocation = response.headers().firstValue("Location")
|
||||
.orElseThrow(() -> new IOException("Got a 3xx response code but Location header was null while trying to fetch " + url));
|
||||
currentUrl = new URL(currentUrl, newLocation);
|
||||
@@ -142,82 +133,64 @@ public class VersionChecker
|
||||
final boolean isGzipEncoded = response.headers().firstValue("Content-Encoding").orElse("").equals("gzip");
|
||||
|
||||
final String bodyStr;
|
||||
try (InputStream inStream = isGzipEncoded ? new GZIPInputStream(response.body()) : response.body())
|
||||
{
|
||||
try (var bufferedReader = new BufferedReader(new InputStreamReader(inStream)))
|
||||
{
|
||||
bodyStr = bufferedReader.lines().collect(Collectors.joining("\n"));
|
||||
}
|
||||
try (var inStream = isGzipEncoded ? new GZIPInputStream(response.body()) : response.body();
|
||||
var bufferedReader = new BufferedReader(new InputStreamReader(inStream))
|
||||
) {
|
||||
bodyStr = bufferedReader.lines().collect(Collectors.joining("\n"));
|
||||
}
|
||||
return bodyStr;
|
||||
}
|
||||
throw new IOException("Too many redirects while trying to fetch " + url);
|
||||
}
|
||||
|
||||
private void process(IModInfo mod)
|
||||
{
|
||||
private void process(IModInfo mod) {
|
||||
Status status = PENDING;
|
||||
ComparableVersion target = null;
|
||||
Map<ComparableVersion, String> changes = null;
|
||||
String display_url = null;
|
||||
try
|
||||
{
|
||||
try {
|
||||
if (mod.getUpdateURL().isEmpty()) return;
|
||||
URL url = mod.getUpdateURL().get();
|
||||
LOGGER.info("[{}] Starting version check at {}", mod.getModId(), url.toString());
|
||||
|
||||
String data = openUrlString(url);
|
||||
String data = openUrlString(url, mod);
|
||||
|
||||
LOGGER.debug("[{}] Received version check data:\n{}", mod.getModId(), data);
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> json = new Gson().fromJson(data, Map.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> promos = (Map<String, String>)json.get("promos");
|
||||
display_url = (String)json.get("homepage");
|
||||
|
||||
var mcVersion = FMLLoader.versionInfo().mcVersion();
|
||||
String rec = promos.get(mcVersion + "-recommended");
|
||||
String lat = promos.get(mcVersion + "-latest");
|
||||
ComparableVersion current = new ComparableVersion(mod.getVersion().toString());
|
||||
|
||||
if (rec != null)
|
||||
{
|
||||
if (rec != null) {
|
||||
ComparableVersion recommended = new ComparableVersion(rec);
|
||||
int diff = recommended.compareTo(current);
|
||||
|
||||
if (diff == 0)
|
||||
status = UP_TO_DATE;
|
||||
else if (diff < 0)
|
||||
{
|
||||
else if (diff < 0) {
|
||||
status = AHEAD;
|
||||
if (lat != null)
|
||||
{
|
||||
if (lat != null) {
|
||||
ComparableVersion latest = new ComparableVersion(lat);
|
||||
if (current.compareTo(latest) < 0)
|
||||
{
|
||||
if (current.compareTo(latest) < 0) {
|
||||
status = OUTDATED;
|
||||
target = latest;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
status = OUTDATED;
|
||||
target = recommended;
|
||||
}
|
||||
}
|
||||
else if (lat != null)
|
||||
{
|
||||
} else if (lat != null) {
|
||||
ComparableVersion latest = new ComparableVersion(lat);
|
||||
if (current.compareTo(latest) < 0)
|
||||
status = BETA_OUTDATED;
|
||||
else
|
||||
status = BETA;
|
||||
status = current.compareTo(latest) < 0 ? BETA_OUTDATED : BETA;
|
||||
target = latest;
|
||||
}
|
||||
else
|
||||
} else
|
||||
status = BETA;
|
||||
|
||||
LOGGER.info("[{}] Found status: {} Current: {} Target: {}", mod.getModId(), status, current, target);
|
||||
@@ -225,38 +198,30 @@ public class VersionChecker
|
||||
changes = new LinkedHashMap<>();
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> tmp = (Map<String, String>)json.get(mcVersion);
|
||||
if (tmp != null)
|
||||
{
|
||||
if (tmp != null) {
|
||||
List<ComparableVersion> ordered = new ArrayList<>();
|
||||
for (String key : tmp.keySet())
|
||||
{
|
||||
for (String key : tmp.keySet()) {
|
||||
ComparableVersion ver = new ComparableVersion(key);
|
||||
if (ver.compareTo(current) > 0 && (target == null || ver.compareTo(target) < 1))
|
||||
{
|
||||
ordered.add(ver);
|
||||
}
|
||||
}
|
||||
Collections.sort(ordered);
|
||||
|
||||
for (ComparableVersion ver : ordered)
|
||||
{
|
||||
for (ComparableVersion ver : ordered) {
|
||||
changes.put(ver, tmp.get(ver.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to process update information", e);
|
||||
status = FAILED;
|
||||
}
|
||||
results.put(mod, new CheckResult(status, target, changes, display_url));
|
||||
RESULTS.put(mod, new CheckResult(status, target, changes, display_url));
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
// Gather a list of mods that have opted in to this update system by providing a URL.
|
||||
private static List<IModInfo> gatherMods()
|
||||
{
|
||||
private static List<IModInfo> gatherMods() {
|
||||
List<IModInfo> ret = new LinkedList<>();
|
||||
for (IModInfo info : ModList.get().getMods()) {
|
||||
if (info.getUpdateURL().isPresent())
|
||||
@@ -265,12 +230,10 @@ public class VersionChecker
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static Map<IModInfo, CheckResult> results = new ConcurrentHashMap<>();
|
||||
private static final Map<IModInfo, CheckResult> RESULTS = new ConcurrentHashMap<>();
|
||||
private static final CheckResult PENDING_CHECK = new CheckResult(PENDING, null, null, null);
|
||||
|
||||
public static CheckResult getResult(IModInfo mod)
|
||||
{
|
||||
return results.getOrDefault(mod, PENDING_CHECK);
|
||||
public static CheckResult getResult(IModInfo mod) {
|
||||
return RESULTS.getOrDefault(mod, PENDING_CHECK);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,11 +10,11 @@ import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import com.electronwill.nightconfig.core.file.FileWatcher;
|
||||
import com.electronwill.nightconfig.core.io.ParsingException;
|
||||
import com.electronwill.nightconfig.core.io.WritingMode;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import net.minecraftforge.fml.loading.FMLConfig;
|
||||
import net.minecraftforge.fml.loading.FMLPaths;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -24,9 +24,9 @@ import java.util.function.Function;
|
||||
import static net.minecraftforge.fml.config.ConfigTracker.CONFIG;
|
||||
|
||||
public class ConfigFileTypeHandler {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
static ConfigFileTypeHandler TOML = new ConfigFileTypeHandler();
|
||||
private static final Path defaultConfigPath = FMLPaths.GAMEDIR.get().resolve(FMLConfig.defaultConfigPath());
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
static final ConfigFileTypeHandler TOML = new ConfigFileTypeHandler();
|
||||
private static final Path defaultConfigPath = FMLPaths.GAMEDIR.get().resolve(FMLConfig.getConfigValue(FMLConfig.ConfigValue.DEFAULT_CONFIG_PATH));
|
||||
|
||||
public Function<ModConfig, CommentedFileConfig> reader(Path configBasePath) {
|
||||
return (c) -> {
|
||||
@@ -47,12 +47,8 @@ public class ConfigFileTypeHandler {
|
||||
throw new ConfigLoadingException(c, ex);
|
||||
}
|
||||
LOGGER.debug(CONFIG, "Loaded TOML config file {}", configPath.toString());
|
||||
try {
|
||||
FileWatcher.defaultInstance().addWatch(configPath, new ConfigWatcher(c, configData, Thread.currentThread().getContextClassLoader()));
|
||||
LOGGER.debug(CONFIG, "Watching TOML config file {} for changes", configPath.toString());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Couldn't watch config file", e);
|
||||
}
|
||||
FileWatcher.defaultInstance().addWatch(configPath, new ConfigWatcher(c, configData, Thread.currentThread().getContextClassLoader()));
|
||||
LOGGER.debug(CONFIG, "Watching TOML config file {} for changes", configPath.toString());
|
||||
return configData;
|
||||
};
|
||||
}
|
||||
@@ -66,8 +62,10 @@ public class ConfigFileTypeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean setupConfigFile(final ModConfig modConfig, final Path file, final ConfigFormat<?> conf) throws IOException {
|
||||
Files.createDirectories(file.getParent());
|
||||
private static boolean setupConfigFile(final ModConfig modConfig, final Path file, final ConfigFormat<?> conf) throws IOException {
|
||||
if (!Files.isDirectory(file.getParent())) {
|
||||
Files.createDirectories(file.getParent());
|
||||
}
|
||||
Path p = defaultConfigPath.resolve(modConfig.getFileName());
|
||||
if (Files.exists(p)) {
|
||||
LOGGER.info(CONFIG, "Loading default config file from path {}", p);
|
||||
@@ -111,17 +109,11 @@ public class ConfigFileTypeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private static class ConfigWatcher implements Runnable {
|
||||
private final ModConfig modConfig;
|
||||
private final CommentedFileConfig commentedFileConfig;
|
||||
private final ClassLoader realClassLoader;
|
||||
|
||||
ConfigWatcher(final ModConfig modConfig, final CommentedFileConfig commentedFileConfig, final ClassLoader classLoader) {
|
||||
this.modConfig = modConfig;
|
||||
this.commentedFileConfig = commentedFileConfig;
|
||||
this.realClassLoader = classLoader;
|
||||
}
|
||||
|
||||
private record ConfigWatcher(
|
||||
ModConfig modConfig,
|
||||
CommentedFileConfig commentedFileConfig,
|
||||
ClassLoader realClassLoader
|
||||
) implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
// Force the regular classloader onto the special thread
|
||||
|
||||
@@ -7,18 +7,18 @@ package net.minecraftforge.fml.config;
|
||||
|
||||
import com.electronwill.nightconfig.core.CommentedConfig;
|
||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.Marker;
|
||||
import org.apache.logging.log4j.MarkerManager;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.Marker;
|
||||
import org.slf4j.MarkerFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class ConfigTracker {
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
static final Marker CONFIG = MarkerManager.getMarker("CONFIG");
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
static final Marker CONFIG = MarkerFactory.getMarker("CONFIG");
|
||||
public static final ConfigTracker INSTANCE = new ConfigTracker();
|
||||
private final ConcurrentHashMap<String, ModConfig> fileMap;
|
||||
private final EnumMap<ModConfig.Type, Set<ModConfig>> configSets;
|
||||
@@ -47,15 +47,19 @@ public class ConfigTracker {
|
||||
|
||||
public void loadConfigs(ModConfig.Type type, Path configBasePath) {
|
||||
LOGGER.debug(CONFIG, "Loading configs type {}", type);
|
||||
this.configSets.get(type).forEach(config -> openConfig(config, configBasePath));
|
||||
for (ModConfig config : this.configSets.get(type)) {
|
||||
openConfig(config, configBasePath);
|
||||
}
|
||||
}
|
||||
|
||||
public void unloadConfigs(ModConfig.Type type, Path configBasePath) {
|
||||
LOGGER.debug(CONFIG, "Unloading configs type {}", type);
|
||||
this.configSets.get(type).forEach(config -> closeConfig(config, configBasePath));
|
||||
for (ModConfig config : this.configSets.get(type)) {
|
||||
closeConfig(config, configBasePath);
|
||||
}
|
||||
}
|
||||
|
||||
private void openConfig(final ModConfig config, final Path configBasePath) {
|
||||
private static void openConfig(final ModConfig config, final Path configBasePath) {
|
||||
LOGGER.trace(CONFIG, "Loading config file type {} at {} for {}", config.getType(), config.getFileName(), config.getModId());
|
||||
final CommentedFileConfig configData = config.getHandler().reader(configBasePath).apply(config);
|
||||
config.setConfigData(configData);
|
||||
@@ -63,11 +67,15 @@ public class ConfigTracker {
|
||||
config.save();
|
||||
}
|
||||
|
||||
private void closeConfig(final ModConfig config, final Path configBasePath) {
|
||||
private static void closeConfig(final ModConfig config, final Path configBasePath) {
|
||||
if (config.getConfigData() != null) {
|
||||
LOGGER.trace(CONFIG, "Closing config file type {} at {} for {}", config.getType(), config.getFileName(), config.getModId());
|
||||
config.save();
|
||||
// stop the filewatcher before we save the file and close it, so reload doesn't fire
|
||||
config.getHandler().unload(configBasePath, config);
|
||||
var unloading = IConfigEvent.unloading(config);
|
||||
if (unloading != null)
|
||||
config.fireEvent(unloading);
|
||||
config.save();
|
||||
config.setConfigData(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,11 @@ import net.minecraftforge.fml.Bindings;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public interface IConfigEvent {
|
||||
record ConfigConfig(Function<ModConfig, IConfigEvent> loading, Function<ModConfig, IConfigEvent> reloading) {}
|
||||
record ConfigConfig(Function<ModConfig, IConfigEvent> loading, Function<ModConfig, IConfigEvent> reloading, @Nullable Function<ModConfig, IConfigEvent> unloading) {}
|
||||
|
||||
ConfigConfig CONFIGCONFIG = Bindings.getConfigConfiguration().get();
|
||||
|
||||
static IConfigEvent reloading(ModConfig modConfig) {
|
||||
@@ -20,6 +23,9 @@ public interface IConfigEvent {
|
||||
static IConfigEvent loading(ModConfig modConfig) {
|
||||
return CONFIGCONFIG.loading().apply(modConfig);
|
||||
}
|
||||
@Nullable static IConfigEvent unloading(ModConfig modConfig) {
|
||||
return CONFIGCONFIG.unloading() == null ? null : CONFIGCONFIG.unloading().apply(modConfig);
|
||||
}
|
||||
ModConfig getConfig();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -24,7 +24,6 @@ public class ModConfig
|
||||
private final ModContainer container;
|
||||
private final ConfigFileTypeHandler configHandler;
|
||||
private CommentedConfig configData;
|
||||
private Callable<Void> saveHandler;
|
||||
|
||||
public ModConfig(final Type type, final IConfigSpec<?> spec, final ModContainer container, final String fileName) {
|
||||
this.type = type;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import net.minecraftforge.gradleutils.PomUtils
|
||||
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'maven-publish'
|
||||
id 'net.minecraftforge.licenser'
|
||||
id 'net.minecraftforge.gradleutils'
|
||||
}
|
||||
|
||||
apply from: rootProject.file('build_shared.gradle')
|
||||
|
||||
import org.gradle.internal.os.OperatingSystem
|
||||
switch (OperatingSystem.current()) {
|
||||
case OperatingSystem.LINUX:
|
||||
project.ext.lwjglNatives = "natives-linux"
|
||||
break
|
||||
case OperatingSystem.MAC_OS:
|
||||
project.ext.lwjglNatives = "natives-macos"
|
||||
break
|
||||
case OperatingSystem.WINDOWS:
|
||||
project.ext.lwjglNatives = "natives-windows"
|
||||
break
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain.languageVersion = JavaLanguageVersion.of(JAVA_VERSION)
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.nulls)
|
||||
implementation(project(':fmlloader'))
|
||||
implementation(project(':fmlcore'))
|
||||
implementation(libs.bundles.lwjgl)
|
||||
implementation('org.slf4j:slf4j-api:1.8.0-beta4')
|
||||
implementation(libs.jopt.simple)
|
||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.8.2')
|
||||
testImplementation('org.powermock:powermock-core:2.0.9')
|
||||
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.8.2')
|
||||
testRuntimeOnly('org.slf4j:slf4j-jdk14:1.8.0-beta4')
|
||||
testRuntimeOnly("org.lwjgl:lwjgl::$lwjglNatives")
|
||||
testRuntimeOnly("org.lwjgl:lwjgl-glfw::$lwjglNatives")
|
||||
testRuntimeOnly("org.lwjgl:lwjgl-opengl::$lwjglNatives")
|
||||
testRuntimeOnly("org.lwjgl:lwjgl-stb::$lwjglNatives")
|
||||
}
|
||||
|
||||
tasks.named('test', Test).configure {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
tasks.named('jar', Jar).configure {
|
||||
manifest {
|
||||
attributes([
|
||||
'Automatic-Module-Name': 'net.minecraftforge.earlydisplay',
|
||||
'Forge-Module-Layer': 'boot'
|
||||
] as LinkedHashMap)
|
||||
attributes([
|
||||
'Specification-Title': 'FML Early Display',
|
||||
'Specification-Vendor': 'Forge Development LLC',
|
||||
'Specification-Version': '1',
|
||||
'Implementation-Title': 'FML Early Display',
|
||||
'Implementation-Vendor': 'Forge Development LLC',
|
||||
'Implementation-Version': '1.0'
|
||||
] as LinkedHashMap, 'net/minecraftforge/fml/earlydisplay/')
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.compilerArgs << '-Xlint:unchecked'
|
||||
}
|
||||
|
||||
license {
|
||||
header = rootProject.file('LICENSE-header.txt')
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications.register('mavenJava', MavenPublication).configure {
|
||||
from components.java
|
||||
artifactId = 'fmlearlydisplay'
|
||||
pom {
|
||||
name = project.name
|
||||
description = 'A pretty looking, but prone to erroring screen displayed during Forge loading.'
|
||||
url = 'https://github.com/MinecraftForge/MinecraftForge'
|
||||
PomUtils.setGitHubDetails(pom, 'MinecraftForge')
|
||||
license PomUtils.Licenses.LGPLv2_1
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven gradleutils.publishingForgeMaven
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.fml.earlydisplay;
|
||||
|
||||
public enum ColourScheme {
|
||||
RED(new Colour(239, 50, 61), new Colour(255, 255, 255)),
|
||||
BLACK(new Colour(0, 0, 0), new Colour(255, 255, 255));
|
||||
|
||||
private final Colour background;
|
||||
private final Colour foreground;
|
||||
|
||||
ColourScheme(final Colour background, final Colour foreground) {
|
||||
this.background = background;
|
||||
this.foreground = foreground;
|
||||
}
|
||||
|
||||
public Colour background() {
|
||||
return background;
|
||||
}
|
||||
|
||||
public Colour foreground() {
|
||||
return foreground;
|
||||
}
|
||||
|
||||
public record Colour(int red, int green, int blue) {
|
||||
public float redf() {
|
||||
return ((float)red)/255f;
|
||||
}
|
||||
public float greenf() {
|
||||
return ((float)green)/255f;
|
||||
}
|
||||
public float bluef() {
|
||||
return ((float)blue)/255f;
|
||||
}
|
||||
|
||||
public int packedint(int a) {
|
||||
return ((a & 0xff) << 24) | ((blue & 0xff) << 16) | ((green & 0xff) << 8) | (red & 0xff);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,653 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.fml.earlydisplay;
|
||||
|
||||
import joptsimple.OptionParser;
|
||||
import net.minecraftforge.fml.loading.FMLConfig;
|
||||
import net.minecraftforge.fml.loading.FMLLoader;
|
||||
import net.minecraftforge.fml.loading.FMLPaths;
|
||||
import net.minecraftforge.fml.loading.ImmediateWindowHandler;
|
||||
import net.minecraftforge.fml.loading.ImmediateWindowProvider;
|
||||
import net.minecraftforge.fml.loading.progress.StartupNotificationManager;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.lwjgl.PointerBuffer;
|
||||
import org.lwjgl.glfw.GLFWVidMode;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
import org.lwjgl.util.tinyfd.TinyFileDialogs;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.Desktop;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.IntConsumer;
|
||||
import java.util.function.IntSupplier;
|
||||
import java.util.function.LongSupplier;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.lwjgl.glfw.GLFW.*;
|
||||
import static org.lwjgl.opengl.GL.createCapabilities;
|
||||
import static org.lwjgl.opengl.GL32C.*;
|
||||
|
||||
/**
|
||||
* The Loading Window that is opened Immediately after Forge starts.
|
||||
* It is called from the ModDirTransformerDiscoverer, the soonest method that ModLauncher calls into Forge code.
|
||||
* In this way, we can be sure that this will not run before any transformer or injection.
|
||||
*
|
||||
* The window itself is spun off into a secondary thread, and is handed off to the main game by Forge.
|
||||
*
|
||||
* Because it is created so early, this thread will "absorb" the context from OpenGL.
|
||||
* Therefore, it is of utmost importance that the Context is made Current for the main thread before handoff,
|
||||
* otherwise OS X will crash out.
|
||||
*
|
||||
* Based on the prior ClientVisualization, with some personal touches.
|
||||
*/
|
||||
public class DisplayWindow implements ImmediateWindowProvider {
|
||||
private static final int[][] GL_VERSIONS = new int[][] {{4,6}, {4,5}, {4,4}, {4,3}, {4,2}, {4,1}, {4,0}, {3,3}, {3,2}};
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("EARLYDISPLAY");
|
||||
private final AtomicBoolean animationTimerTrigger = new AtomicBoolean(true);
|
||||
|
||||
private ColourScheme colourScheme = ColourScheme.RED;
|
||||
private ElementShader elementShader;
|
||||
|
||||
private RenderElement.DisplayContext context;
|
||||
private List<RenderElement> elements;
|
||||
private int framecount;
|
||||
private EarlyFramebuffer framebuffer;
|
||||
private ScheduledFuture<?> windowTick;
|
||||
private ScheduledFuture<?> initializationFuture;
|
||||
|
||||
private PerformanceInfo performanceInfo;
|
||||
@SuppressWarnings("unused")
|
||||
private ScheduledFuture<?> performanceTick;
|
||||
// The GL ID of the window. Used for all operations
|
||||
private long window;
|
||||
// The thread that contains and ticks the window while Forge is loading mods
|
||||
private ScheduledExecutorService renderScheduler;
|
||||
private int fbWidth;
|
||||
private int fbHeight;
|
||||
private int fbScale;
|
||||
private int winWidth;
|
||||
private int winHeight;
|
||||
private int winX;
|
||||
private int winY;
|
||||
|
||||
private final Semaphore renderLock = new Semaphore(1);
|
||||
private boolean maximized;
|
||||
private String glVersion;
|
||||
private SimpleFont font;
|
||||
private Runnable repaintTick = ()->{};
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "fmlearlywindow";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Runnable initialize(String[] arguments) {
|
||||
String mcVersion = FMLLoader.versionInfo().mcVersion();
|
||||
String forgeVersion = FMLLoader.versionInfo().forgeVersion();
|
||||
|
||||
final OptionParser parser = new OptionParser();
|
||||
var widthopt = parser.accepts("width")
|
||||
.withRequiredArg().ofType(Integer.class)
|
||||
.defaultsTo(FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_WIDTH));
|
||||
var heightopt = parser.accepts("height")
|
||||
.withRequiredArg().ofType(Integer.class)
|
||||
.defaultsTo(FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_HEIGHT));
|
||||
var maximizedopt = parser.accepts("earlywindow.maximized");
|
||||
parser.allowsUnrecognizedOptions();
|
||||
var parsed = parser.parse(arguments);
|
||||
winWidth = parsed.valueOf(widthopt);
|
||||
winHeight = parsed.valueOf(heightopt);
|
||||
FMLConfig.updateConfig(FMLConfig.ConfigValue.EARLY_WINDOW_WIDTH, winWidth);
|
||||
FMLConfig.updateConfig(FMLConfig.ConfigValue.EARLY_WINDOW_HEIGHT, winHeight);
|
||||
fbScale = FMLConfig.getIntConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_FBSCALE);
|
||||
if (System.getenv("FML_EARLY_WINDOW_DARK") != null) {
|
||||
this.colourScheme = ColourScheme.BLACK;
|
||||
} else {
|
||||
try {
|
||||
// check the options file for the colour scheme
|
||||
var optionLines = Files.readAllLines(FMLPaths.GAMEDIR.get().resolve(Path.of("options.txt")));
|
||||
var keyName = "darkMojangStudiosBackground:";
|
||||
for (String line : optionLines) {
|
||||
if (line.startsWith(keyName)) {
|
||||
this.colourScheme = line.startsWith("true", keyName.length()) ? ColourScheme.BLACK : ColourScheme.RED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
this.colourScheme = ColourScheme.RED; // fallback to red colourScheme
|
||||
}
|
||||
}
|
||||
this.maximized = parsed.has(maximizedopt) || FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_MAXIMIZED);
|
||||
|
||||
StartupNotificationManager.modLoaderConsumer().ifPresent(c->c.accept("Forge loading " + forgeVersion));
|
||||
performanceInfo = new PerformanceInfo();
|
||||
return start(mcVersion, forgeVersion);
|
||||
}
|
||||
|
||||
private static final long MINFRAMETIME = TimeUnit.MILLISECONDS.toNanos(10); // This is the FPS cap on the window - note animation is capped at 20FPS via the tickTimer
|
||||
private long nextFrameTime = 0;
|
||||
/**
|
||||
* The main render loop.
|
||||
* renderThread executes this.
|
||||
*
|
||||
* Performs initialization and then ticks the screen at 20 fps.
|
||||
* When the thread is killed, context is destroyed.
|
||||
*/
|
||||
private void renderThreadFunc() {
|
||||
if (!renderLock.tryAcquire()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
long nt;
|
||||
if ((nt = System.nanoTime()) < nextFrameTime) {
|
||||
return;
|
||||
}
|
||||
nextFrameTime = nt + MINFRAMETIME;
|
||||
glfwMakeContextCurrent(window);
|
||||
framebuffer.activate();
|
||||
glViewport(0, 0, this.context.scaledWidth(), this.context.scaledHeight());
|
||||
this.context.elementShader().activate();
|
||||
this.context.elementShader().updateScreenSizeUniform(this.context.scaledWidth(), this.context.scaledHeight());
|
||||
glClearColor(colourScheme.background().redf(), colourScheme.background().greenf(), colourScheme.background().bluef(), 1f);
|
||||
paintFramebuffer();
|
||||
this.context.elementShader().clear();
|
||||
framebuffer.deactivate();
|
||||
glViewport(0, 0, fbWidth, fbHeight);
|
||||
framebuffer.draw(this.fbWidth, this.fbHeight);
|
||||
// Swap buffers; we're done
|
||||
glfwSwapBuffers(window);
|
||||
} catch (Throwable t) {
|
||||
LOGGER.error("BARF", t);
|
||||
} finally {
|
||||
if (this.windowTick != null) glfwMakeContextCurrent(0); // we release the gl context IF we're running off the main thread
|
||||
renderLock.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render initialization methods called by the Render Thread.
|
||||
* It compiles the fragment and vertex shaders for rendering text with STB, and sets up basic render framework.
|
||||
*
|
||||
* Nothing fancy, we just want to draw and render text.
|
||||
*/
|
||||
private void initRender(final @Nullable String mcVersion, final String forgeVersion) {
|
||||
// This thread owns the GL render context now. We should make a note of that.
|
||||
glfwMakeContextCurrent(window);
|
||||
// Wait for one frame to be complete before swapping; enable vsync in other words.
|
||||
glfwSwapInterval(1);
|
||||
createCapabilities();
|
||||
LOGGER.info("GL info: "+ glGetString(GL_RENDERER) + " GL version " + glGetString(GL_VERSION) + ", " + glGetString(GL_VENDOR));
|
||||
|
||||
elementShader = new ElementShader();
|
||||
try {
|
||||
elementShader.init();
|
||||
} catch (Throwable t) {
|
||||
LOGGER.error("Crash during shader initialization", t);
|
||||
crashElegantly("An error occurred initializing shaders.");
|
||||
}
|
||||
|
||||
// Set the clear color based on the colour scheme
|
||||
glClearColor(colourScheme.background().redf(), colourScheme.background().greenf(), colourScheme.background().bluef(), 1f);
|
||||
|
||||
// we always render to an 854x480 texture and then fit that to the screen - with a scale factor
|
||||
this.context = new RenderElement.DisplayContext(854, 480, fbScale, elementShader, colourScheme, performanceInfo);
|
||||
framebuffer = new EarlyFramebuffer(this.context);
|
||||
try {
|
||||
this.font = new SimpleFont("Monocraft.ttf", fbScale, 200000, 1 + RenderElement.INDEX_TEXTURE_OFFSET);
|
||||
} catch (Throwable t) {
|
||||
LOGGER.error("Crash during font initialization", t);
|
||||
crashElegantly("An error occurred initializing a font for rendering. "+t.getMessage());
|
||||
}
|
||||
this.elements = new ArrayList<>(List.of(
|
||||
RenderElement.anvil(font),
|
||||
RenderElement.logMessageOverlay(font),
|
||||
RenderElement.forgeVersionOverlay(font, mcVersion + "-" + forgeVersion),
|
||||
RenderElement.performanceBar(font),
|
||||
RenderElement.progressBars(font)
|
||||
));
|
||||
|
||||
var date = Calendar.getInstance();
|
||||
if (FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_SQUIR) || (date.get(Calendar.MONTH) == Calendar.APRIL && date.get(Calendar.DAY_OF_MONTH) == 1))
|
||||
this.elements.add(0, RenderElement.squir());
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
glfwMakeContextCurrent(0);
|
||||
this.windowTick = renderScheduler.scheduleAtFixedRate(this::renderThreadFunc, 50, 50, TimeUnit.MILLISECONDS);
|
||||
this.performanceTick = renderScheduler.scheduleAtFixedRate(performanceInfo::update, 0, 500, TimeUnit.MILLISECONDS);
|
||||
// schedule a 50 ms ticker to try and smooth out the rendering
|
||||
renderScheduler.scheduleAtFixedRate(()-> animationTimerTrigger.set(true), 1, 50, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every frame by the Render Thread to draw to the screen.
|
||||
*/
|
||||
void paintFramebuffer() {
|
||||
// Clear the screen to our color
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
for (var itr = this.elements.iterator(); itr.hasNext(); ) {
|
||||
var element = itr.next();
|
||||
if (!element.render(context, framecount))
|
||||
itr.remove();
|
||||
}
|
||||
if (animationTimerTrigger.compareAndSet(true, false)) // we only increment the framecount on a periodic basis
|
||||
framecount++;
|
||||
}
|
||||
|
||||
|
||||
public void render(int alpha) {
|
||||
var currentVAO = glGetInteger(GL_VERTEX_ARRAY_BINDING);
|
||||
var currentFB = glGetInteger(GL_READ_FRAMEBUFFER_BINDING);
|
||||
glViewport(0, 0, this.context.scaledWidth(), this.context.scaledHeight());
|
||||
RenderElement.globalAlpha = alpha;
|
||||
framebuffer.activate();
|
||||
glClearColor(colourScheme.background().redf(), colourScheme.background().greenf(), colourScheme.background().bluef(), alpha / 255f);
|
||||
elementShader.activate();
|
||||
elementShader.updateScreenSizeUniform(this.context.scaledWidth(), this.context.scaledHeight());
|
||||
paintFramebuffer();
|
||||
elementShader.clear();
|
||||
framebuffer.deactivate();
|
||||
glBindVertexArray(currentVAO);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, currentFB);
|
||||
}
|
||||
/**
|
||||
* Start the window and Render Thread; we're ready to go.
|
||||
*/
|
||||
public Runnable start(@Nullable String mcVersion, final String forgeVersion) {
|
||||
renderScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
final var thread = Executors.defaultThreadFactory().newThread(r);
|
||||
thread.setName("EarlyDisplay");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
initWindow(mcVersion);
|
||||
this.initializationFuture = renderScheduler.schedule(() -> initRender(mcVersion, forgeVersion), 1, TimeUnit.MILLISECONDS);
|
||||
return this::periodicTick;
|
||||
}
|
||||
|
||||
private static final String ERROR_URL = "https://links.minecraftforge.net/early-display-errors";
|
||||
@Override
|
||||
public String getGLVersion() {
|
||||
return this.glVersion;
|
||||
}
|
||||
|
||||
private void crashElegantly(String errorDetails) {
|
||||
StringBuilder msgBuilder = new StringBuilder(2000);
|
||||
msgBuilder.append("Failed to initialize graphics window with current settings.\n");
|
||||
msgBuilder.append("\n\n");
|
||||
msgBuilder.append("Failure details:\n");
|
||||
msgBuilder.append(errorDetails);
|
||||
msgBuilder.append("\n\n");
|
||||
msgBuilder.append("If you click yes, we will try and open " + ERROR_URL + " in your default browser");
|
||||
LOGGER.error("ERROR DISPLAY\n"+msgBuilder);
|
||||
// we show the display on a new dedicated thread
|
||||
Executors.newSingleThreadExecutor().submit(()-> {
|
||||
var res = TinyFileDialogs.tinyfd_messageBox("Minecraft: Forge", msgBuilder.toString(), "yesno", "error", false);
|
||||
if (res) {
|
||||
try {
|
||||
Desktop.getDesktop().browse(URI.create(ERROR_URL));
|
||||
} catch (IOException ioe) {
|
||||
TinyFileDialogs.tinyfd_messageBox("Minecraft: Forge", "Sadly, we couldn't open your browser.\nVisit " + ERROR_URL, "ok", "error", false);
|
||||
}
|
||||
}
|
||||
System.exit(1);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Called to initialize the window when preparing for the Render Thread.
|
||||
*
|
||||
* The act of calling glfwInit here creates a concurrency issue; GL doesn't know whether we're gonna call any
|
||||
* GL functions from the secondary thread and the main thread at the same time.
|
||||
*
|
||||
* It's then our job to make sure this doesn't happen, only calling GL functions where the Context is Current.
|
||||
* As long as we can verify that, then GL (and things like OS X) have no complaints with doing this.
|
||||
*
|
||||
* @param mcVersion Minecraft Version
|
||||
* @return The selected GL profile as an integer pair
|
||||
*/
|
||||
public void initWindow(@Nullable String mcVersion) {
|
||||
// Initialize GLFW with a time guard, in case something goes wrong
|
||||
long glfwInitBegin = System.nanoTime();
|
||||
if (!glfwInit()) {
|
||||
crashElegantly("We are unable to initialize the graphics system.\nglfwInit failed.\n");
|
||||
throw new IllegalStateException("Unable to initialize GLFW");
|
||||
}
|
||||
long glfwInitEnd = System.nanoTime();
|
||||
|
||||
if (glfwInitEnd - glfwInitBegin > 1e9) {
|
||||
LOGGER.error("WARNING : glfwInit took {} seconds to start.", (glfwInitEnd - glfwInitBegin) / 1.0e9);
|
||||
}
|
||||
|
||||
// Clear the Last Exception (#7285 - Prevent Vanilla throwing an IllegalStateException due to invalid controller mappings)
|
||||
handleLastGLFWError((error, description) -> LOGGER.error(String.format("Suppressing Last GLFW error: [0x%X]%s", error, description)));
|
||||
|
||||
// Set window hints for the new window we're gonna create.
|
||||
glfwDefaultWindowHints();
|
||||
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
|
||||
glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_NATIVE_CONTEXT_API);
|
||||
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
||||
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
|
||||
|
||||
String vanillaWindowTitle = "Minecraft* ";
|
||||
if (mcVersion != null) vanillaWindowTitle += mcVersion;
|
||||
|
||||
// this emulates what we would get without early progress window
|
||||
// as vanilla never sets these, so GLFW uses the first window title
|
||||
// set them explicitly to avoid it using "FML early loading progress" as the class
|
||||
glfwWindowHintString(GLFW_X11_CLASS_NAME, vanillaWindowTitle);
|
||||
glfwWindowHintString(GLFW_X11_INSTANCE_NAME, vanillaWindowTitle);
|
||||
|
||||
long primaryMonitor = glfwGetPrimaryMonitor();
|
||||
if (primaryMonitor == 0) {
|
||||
LOGGER.error("Failed to find a primary monitor - this means LWJGL isn't working properly");
|
||||
crashElegantly("Failed to locate a primary monitor.\nglfwGetPrimaryMonitor failed.\n");
|
||||
throw new IllegalStateException("Can't find a primary monitor");
|
||||
}
|
||||
GLFWVidMode vidmode = glfwGetVideoMode(primaryMonitor);
|
||||
|
||||
if (vidmode == null) {
|
||||
LOGGER.error("Failed to get the current display video mode.");
|
||||
crashElegantly("Failed to get current display resolution.\nglfwGetVideoMode failed.\n");
|
||||
throw new IllegalStateException("Can't get a resolution");
|
||||
}
|
||||
long window = 0;
|
||||
var successfulWindow = new AtomicBoolean(false);
|
||||
var windowFailFuture = renderScheduler.schedule(()->{
|
||||
if (!successfulWindow.get()) crashElegantly("Timed out trying to setup the Game Window.");
|
||||
}, 10, TimeUnit.SECONDS);
|
||||
int versidx = 0;
|
||||
var skipVersions = FMLConfig.<String>getListConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_SKIP_GL_VERSIONS);
|
||||
boolean showHelpLog = FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_LOG_HELP_MSG);
|
||||
final String[] lastGLError = new String[GL_VERSIONS.length];
|
||||
do {
|
||||
final var glVersionToTry = GL_VERSIONS[versidx][0] + "." + GL_VERSIONS[versidx][1];
|
||||
if (skipVersions.contains(glVersionToTry)) {
|
||||
LOGGER.info("Skipping GL version "+ glVersionToTry+" because of configuration");
|
||||
versidx++;
|
||||
continue;
|
||||
}
|
||||
LOGGER.info("Trying GL version " + glVersionToTry);
|
||||
if (showHelpLog && versidx == 0) {
|
||||
LOGGER.info("""
|
||||
If this message is the only thing at the bottom of your log before a crash, you probably have a driver issue.
|
||||
|
||||
Possible solutions:
|
||||
A) Make sure Minecraft is set to prefer high performance graphics in the OS and/or driver control panel
|
||||
B) Check for driver updates on the graphics brand's website
|
||||
C) Try reinstalling your graphics drivers
|
||||
D) If still not working after trying all of the above, ask for further help on the Forge forums or Discord
|
||||
|
||||
You can safely ignore this message if the game starts up successfully.""");
|
||||
}
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, GL_VERSIONS[versidx][0]); // we try our versions one at a time
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, GL_VERSIONS[versidx][1]);
|
||||
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
|
||||
window = glfwCreateWindow(winWidth, winHeight, vanillaWindowTitle, 0L, 0L);
|
||||
var erridx = versidx;
|
||||
handleLastGLFWError((error, description) -> lastGLError[erridx] = String.format("Trying %d.%d: GLFW error: [0x%X]%s", GL_VERSIONS[erridx][0], GL_VERSIONS[erridx][1], error, description));
|
||||
if (lastGLError[versidx] != null) {
|
||||
LOGGER.trace(lastGLError[versidx]);
|
||||
}
|
||||
versidx++;
|
||||
} while (window == 0 && versidx < GL_VERSIONS.length);
|
||||
// LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(12));
|
||||
if (versidx== GL_VERSIONS.length && window == 0) {
|
||||
LOGGER.error("Failed to find any valid GLFW profile. "+lastGLError[0]);
|
||||
|
||||
crashElegantly("Failed to find a valid GLFW profile.\nWe tried "+
|
||||
Arrays.stream(GL_VERSIONS).map(p->p[0]+"."+p[1]).filter(o -> !skipVersions.contains(o))
|
||||
.collect(Collector.of(()->new StringJoiner(", ").setEmptyValue("no versions"), StringJoiner::add, StringJoiner::merge, StringJoiner::toString))+
|
||||
" but none of them worked.\n"+ Arrays.stream(lastGLError).filter(Objects::nonNull).collect(Collectors.joining("\n")));
|
||||
throw new IllegalStateException("Failed to create a GLFW window with any profile");
|
||||
}
|
||||
successfulWindow.set(true);
|
||||
if (!windowFailFuture.cancel(true)) throw new IllegalStateException("We died but didn't somehow?");
|
||||
var requestedVersion = GL_VERSIONS[versidx-1][0]+"."+GL_VERSIONS[versidx-1][1];
|
||||
var maj = glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MAJOR);
|
||||
var min = glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MINOR);
|
||||
var gotVersion = maj+"."+min;
|
||||
LOGGER.info("Requested GL version "+requestedVersion+" got version "+gotVersion);
|
||||
this.glVersion = gotVersion;
|
||||
this.window = window;
|
||||
|
||||
if (showHelpLog)
|
||||
FMLConfig.updateConfig(FMLConfig.ConfigValue.EARLY_WINDOW_LOG_HELP_MSG, false);
|
||||
|
||||
int[] x = new int[1];
|
||||
int[] y = new int[1];
|
||||
glfwGetMonitorPos(primaryMonitor, x, y);
|
||||
int monitorX = x[0];
|
||||
int monitorY = y[0];
|
||||
// glfwSetWindowSizeLimits(window, 854, 480, GLFW_DONT_CARE, GLFW_DONT_CARE);
|
||||
if (this.maximized) {
|
||||
glfwMaximizeWindow(window);
|
||||
}
|
||||
|
||||
glfwGetWindowSize(window, x, y);
|
||||
this.winWidth = x[0];
|
||||
this.winHeight = y[0];
|
||||
|
||||
glfwSetWindowPos(window, (vidmode.width() - this.winWidth) / 2 + monitorX, (vidmode.height() - this.winHeight) / 2 + monitorY);
|
||||
|
||||
// Attempt setting the icon
|
||||
// int[] channels = new int[1];
|
||||
// try (var glfwImgBuffer = GLFWImage.create(MemoryUtil.getAllocator().malloc(GLFWImage.SIZEOF), 1)) {
|
||||
// final ByteBuffer imgBuffer;
|
||||
// try (GLFWImage glfwImages = GLFWImage.malloc()) {
|
||||
// imgBuffer = STBHelper.loadImageFromClasspath("forge_logo.png", 20000, x, y, channels);
|
||||
// glfwImgBuffer.put(glfwImages.set(x[0], y[0], imgBuffer));
|
||||
// glfwSetWindowIcon(window, glfwImgBuffer);
|
||||
// STBImage.stbi_image_free(imgBuffer);
|
||||
// }
|
||||
// } catch (NullPointerException e) {
|
||||
// System.err.println("Failed to load forge logo");
|
||||
// }
|
||||
// handleLastGLFWError((error, description) -> LOGGER.debug(String.format("Suppressing GLFW icon error: [0x%X]%s", error, description)));
|
||||
|
||||
glfwSetFramebufferSizeCallback(window, this::fbResize);
|
||||
glfwSetWindowPosCallback(window, this::winMove);
|
||||
glfwSetWindowSizeCallback(window, this::winResize);
|
||||
|
||||
// Show the window
|
||||
glfwShowWindow(window);
|
||||
glfwGetWindowPos(window, x, y);
|
||||
this.winX = x[0];
|
||||
this.winY = y[0];
|
||||
glfwGetFramebufferSize(window, x, y);
|
||||
this.fbWidth = x[0];
|
||||
this.fbHeight = y[0];
|
||||
glfwPollEvents();
|
||||
}
|
||||
|
||||
private void winResize(long window, int width, int height) {
|
||||
if (window == this.window && width != 0 && height != 0) {
|
||||
this.winWidth = width;
|
||||
this.winHeight = height;
|
||||
}
|
||||
}
|
||||
private void fbResize(long window, int width, int height) {
|
||||
if (window == this.window && width != 0 && height != 0) {
|
||||
this.fbWidth = width;
|
||||
this.fbHeight = height;
|
||||
}
|
||||
}
|
||||
|
||||
private void winMove(long window, int x, int y) {
|
||||
if (window == this.window) {
|
||||
this.winX = x;
|
||||
this.winY = y;
|
||||
}
|
||||
}
|
||||
private void handleLastGLFWError(BiConsumer<Integer, String> handler) {
|
||||
try (MemoryStack memorystack = MemoryStack.stackPush()) {
|
||||
PointerBuffer pointerbuffer = memorystack.mallocPointer(1);
|
||||
int error = glfwGetError(pointerbuffer);
|
||||
if (error != GLFW_NO_ERROR) {
|
||||
long pDescription = pointerbuffer.get();
|
||||
String description = pDescription == 0L ? "" : MemoryUtil.memUTF8(pDescription);
|
||||
handler.accept(error, description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hand-off the window to the vanilla game.
|
||||
* Called on the main thread instead of the game's initialization.
|
||||
*
|
||||
* @return the Window we own.
|
||||
*/
|
||||
public long setupMinecraftWindow(final IntSupplier width, final IntSupplier height, final Supplier<String> title, final LongSupplier monitorSupplier) {
|
||||
// wait for the window to actually be initialized
|
||||
try {
|
||||
this.initializationFuture.get(30, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (TimeoutException e) {
|
||||
Thread.dumpStack();
|
||||
crashElegantly("We seem to be having trouble initializing the window, waited for 30 seconds");
|
||||
}
|
||||
|
||||
// we have to spin wait for the window ticker
|
||||
ImmediateWindowHandler.updateProgress("Initializing Game Graphics");
|
||||
while (!this.windowTick.isDone()) {
|
||||
this.windowTick.cancel(false);
|
||||
}
|
||||
var tries = 0;
|
||||
var renderlockticket = false;
|
||||
do {
|
||||
try {
|
||||
renderlockticket = renderLock.tryAcquire(100, TimeUnit.MILLISECONDS);
|
||||
if (++tries > 9) {
|
||||
Thread.dumpStack();
|
||||
crashElegantly("We seem to be having trouble handing off the window, tried for 1 second");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.interrupted();
|
||||
}
|
||||
} while (!renderlockticket);
|
||||
// we don't want the lock, just making sure it's back on the main thread
|
||||
renderLock.release();
|
||||
|
||||
glfwMakeContextCurrent(window);
|
||||
// Set the title to what the game wants
|
||||
glfwSetWindowTitle(window, title.get());
|
||||
glfwSwapInterval(0);
|
||||
// Clean up our hooks
|
||||
glfwSetFramebufferSizeCallback(window, null).free();
|
||||
glfwSetWindowPosCallback(window, null).free();
|
||||
glfwSetWindowSizeCallback(window, null).free();
|
||||
this.repaintTick = this::renderThreadFunc; // the repaint will continue to be called until the overlay takes over
|
||||
this.windowTick = null; // this tells the render thread that the async ticker is done
|
||||
return window;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean positionWindow(final Optional<Object> monitor, final IntConsumer widthSetter, final IntConsumer heightSetter, final IntConsumer xSetter, final IntConsumer ySetter) {
|
||||
widthSetter.accept(this.winWidth);
|
||||
heightSetter.accept(this.winHeight);
|
||||
xSetter.accept(this.winX);
|
||||
ySetter.accept(this.winY);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFramebufferSize(final IntConsumer width, final IntConsumer height) {
|
||||
width.accept(this.fbWidth);
|
||||
height.accept(this.fbHeight);
|
||||
}
|
||||
|
||||
private Method loadingOverlay;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> Supplier<T> loadingOverlay(final Supplier<?> mc, final Supplier<?> ri, final Consumer<Optional<Throwable>> ex, final boolean fade) {
|
||||
try {
|
||||
return (Supplier<T>)loadingOverlay.invoke(null, mc, ri, ex, this);
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException("How did you get here?", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateModuleReads(final ModuleLayer layer) {
|
||||
final var FORGE_MODULE = "net.minecraftforge.forge";
|
||||
var forge_module = layer.findModule(FORGE_MODULE).orElse(null);
|
||||
if (forge_module == null)
|
||||
throw new IllegalStateException("Could not find " + FORGE_MODULE + " in " + layer);
|
||||
|
||||
getClass().getModule().addReads(forge_module);
|
||||
|
||||
|
||||
var clz = Class.forName(forge_module, "net.minecraftforge.client.loading.ForgeLoadingOverlay");
|
||||
|
||||
for (var mtd : clz.getDeclaredMethods()) {
|
||||
if (Modifier.isStatic(mtd.getModifiers()) && "newInstance".equals(mtd.getName())) {
|
||||
this.loadingOverlay = mtd;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (loadingOverlay == null)
|
||||
throw new IllegalStateException("Could not find static newInstace method in " + clz.getName());
|
||||
}
|
||||
|
||||
public int getFramebufferTextureId() {
|
||||
return framebuffer.getTexture();
|
||||
}
|
||||
|
||||
public RenderElement.DisplayContext context() {
|
||||
return this.context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void periodicTick() {
|
||||
glfwPollEvents();
|
||||
repaintTick.run();
|
||||
}
|
||||
|
||||
public void addMojangTexture(final int textureId) {
|
||||
this.elements.add(0, RenderElement.mojang(textureId, framecount));
|
||||
}
|
||||
|
||||
public void close() {
|
||||
// Close the Render Scheduler thread
|
||||
renderScheduler.shutdown();
|
||||
this.framebuffer.close();
|
||||
this.context.elementShader().close();
|
||||
SimpleBufferBuilder.destroy();
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.fml.earlydisplay;
|
||||
|
||||
import java.nio.IntBuffer;
|
||||
|
||||
import static net.minecraftforge.fml.earlydisplay.RenderElement.clamp;
|
||||
import static org.lwjgl.opengl.GL32C.*;
|
||||
|
||||
public class EarlyFramebuffer {
|
||||
private final int framebuffer;
|
||||
private final int texture;
|
||||
|
||||
private final RenderElement.DisplayContext context;
|
||||
|
||||
EarlyFramebuffer(final RenderElement.DisplayContext context) {
|
||||
this.context = context;
|
||||
this.framebuffer = glGenFramebuffers();
|
||||
this.texture = glGenTextures();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, this.framebuffer);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, this.texture);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, context.width() * context.scale(), context.height() * context.scale(), 0, GL_RGBA, GL_UNSIGNED_BYTE, (IntBuffer)null);
|
||||
glTexParameterIi(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameterIi(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, this.texture, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
void activate() {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, this.framebuffer);
|
||||
}
|
||||
|
||||
void deactivate() {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
void draw(int windowFBWidth, int windowFBHeight) {
|
||||
var wscale = ((float)windowFBWidth / this.context.width());
|
||||
var hscale = ((float)windowFBHeight / this.context.height());
|
||||
var scale = this.context.scale() * Math.min(wscale, hscale) / 2f;
|
||||
var wleft = (int)(windowFBWidth * 0.5f - scale * this.context.width());
|
||||
var wtop = (int)(windowFBHeight * 0.5f - scale * this.context.height());
|
||||
var wright = (int)(windowFBWidth * 0.5f + scale * this.context.width());
|
||||
var wbottom = (int)(windowFBHeight * 0.5f + scale * this.context.height());
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, this.framebuffer);
|
||||
final var colour = this.context.colourScheme().background();
|
||||
glClearColor(colour.redf(), colour.greenf(), colour.bluef(), 1f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
// src Y are flipped, since our FB is flipped
|
||||
glBlitFramebuffer(0, this.context.height() * this.context.scale(), this.context.width() * this.context.scale(), 0, clamp(wleft, 0, windowFBWidth), clamp(wtop, 0, windowFBHeight), clamp(wright, 0, windowFBWidth), clamp(wbottom, 0, windowFBHeight), GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
int getTexture() {
|
||||
return this.texture;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
glDeleteTextures(this.texture);
|
||||
glDeleteFramebuffers(this.framebuffer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.fml.earlydisplay;
|
||||
|
||||
|
||||
import static org.lwjgl.opengl.GL32C.*;
|
||||
|
||||
public class ElementShader {
|
||||
private int program;
|
||||
private int textureUniform;
|
||||
private int screenSizeUniform;
|
||||
private int renderTypeUniform;
|
||||
|
||||
public void init() {
|
||||
int vertexShader = glCreateShader(GL_VERTEX_SHADER);
|
||||
int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
|
||||
// Bind the source of our shaders to the ones created above
|
||||
glShaderSource(fragmentShader, """
|
||||
#version 150 core
|
||||
uniform sampler2D tex;
|
||||
uniform int rendertype;
|
||||
in vec2 fTex;
|
||||
in vec4 fColour;
|
||||
out vec4 fragColor;
|
||||
|
||||
void main() {
|
||||
if (rendertype == 0)
|
||||
fragColor = vec4(1,1,1,texture(tex, fTex).r) * fColour;
|
||||
if (rendertype == 1)
|
||||
fragColor = texture(tex, fTex) * fColour;
|
||||
if (rendertype == 2)
|
||||
fragColor = fColour;
|
||||
}
|
||||
""");
|
||||
glShaderSource(vertexShader, """
|
||||
#version 150 core
|
||||
in vec2 position;
|
||||
in vec2 tex;
|
||||
in vec4 colour;
|
||||
uniform vec2 screenSize;
|
||||
out vec2 fTex;
|
||||
out vec4 fColour;
|
||||
void main() {
|
||||
fTex = tex;
|
||||
fColour = colour;
|
||||
gl_Position = vec4((position/screenSize) * 2 - 1, 0.0, 1.0);
|
||||
}
|
||||
""");
|
||||
|
||||
// Compile the vertex and fragment elementShader so that we can use them
|
||||
glCompileShader(vertexShader);
|
||||
if (glGetShaderi(vertexShader, GL_COMPILE_STATUS) == GL_FALSE) {
|
||||
throw new IllegalStateException("VertexShader linkage failure. \n" + glGetShaderInfoLog(vertexShader));
|
||||
}
|
||||
glCompileShader(fragmentShader);
|
||||
if (glGetShaderi(fragmentShader, GL_COMPILE_STATUS) == GL_FALSE) {
|
||||
throw new IllegalStateException("FragmentShader linkage failure. \n" + glGetShaderInfoLog(fragmentShader));
|
||||
}
|
||||
|
||||
var program = glCreateProgram();
|
||||
glBindAttribLocation(program, 0, "position");
|
||||
glBindAttribLocation(program, 1, "tex");
|
||||
glBindAttribLocation(program, 2, "colour");
|
||||
glAttachShader(program, vertexShader);
|
||||
glAttachShader(program, fragmentShader);
|
||||
glLinkProgram(program);
|
||||
if (glGetProgrami(program, GL_LINK_STATUS) == GL_FALSE) {
|
||||
throw new RuntimeException("ShaderProgram linkage failure. \n" + glGetProgramInfoLog(program));
|
||||
}
|
||||
this.program = program;
|
||||
|
||||
glDetachShader(program, vertexShader);
|
||||
glDetachShader(program, fragmentShader);
|
||||
glDeleteShader(vertexShader);
|
||||
glDeleteShader(fragmentShader);
|
||||
textureUniform = glGetUniformLocation(program, "tex");
|
||||
screenSizeUniform = glGetUniformLocation(program, "screenSize");
|
||||
renderTypeUniform = glGetUniformLocation(program, "rendertype");
|
||||
|
||||
activate();
|
||||
}
|
||||
|
||||
public void activate() {
|
||||
glUseProgram(program);
|
||||
}
|
||||
public void updateTextureUniform(int textureNumber) {
|
||||
glUniform1i(textureUniform, textureNumber);
|
||||
}
|
||||
|
||||
public void updateScreenSizeUniform(int width, int height) {
|
||||
glUniform2f(screenSizeUniform, width, height);
|
||||
}
|
||||
|
||||
public void updateRenderTypeUniform(RenderType type) {
|
||||
glUniform1i(renderTypeUniform, type.ordinal());
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
glDeleteProgram(program);
|
||||
}
|
||||
|
||||
public enum RenderType {
|
||||
FONT, TEXTURE, BAR;
|
||||
}
|
||||
|
||||
public int program() {
|
||||
return program;
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.fml.earlydisplay;
|
||||
|
||||
import com.sun.management.OperatingSystemMXBean;
|
||||
import net.minecraftforge.fml.loading.FMLConfig;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryMXBean;
|
||||
import java.lang.management.MemoryUsage;
|
||||
|
||||
public class PerformanceInfo {
|
||||
|
||||
private final boolean showCPUUsage;
|
||||
private final OperatingSystemMXBean osBean;
|
||||
private final MemoryMXBean memoryBean;
|
||||
|
||||
private float memory;
|
||||
private String text;
|
||||
|
||||
PerformanceInfo() {
|
||||
showCPUUsage = FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_SHOW_CPU);
|
||||
osBean = showCPUUsage ? ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class) : null;
|
||||
memoryBean = ManagementFactory.getMemoryMXBean();
|
||||
}
|
||||
|
||||
void update() {
|
||||
final MemoryUsage heapusage = memoryBean.getHeapMemoryUsage();
|
||||
memory = (float) heapusage.getUsed() / heapusage.getMax();
|
||||
|
||||
if (!showCPUUsage) {
|
||||
text = "Heap: %d/%d MB (%.1f%%) OffHeap: %d MB".formatted(heapusage.getUsed() >> 20, heapusage.getMax() >> 20, memory * 100.0, memoryBean.getNonHeapMemoryUsage().getUsed() >> 20);
|
||||
return;
|
||||
}
|
||||
|
||||
var cpuLoad = osBean.getProcessCpuLoad();
|
||||
String cpuText;
|
||||
if (cpuLoad == -1) {
|
||||
cpuText = "*CPU: %.1f%%".formatted(osBean.getCpuLoad() * 100f);
|
||||
} else {
|
||||
cpuText = "CPU: %.1f%%".formatted(cpuLoad * 100f);
|
||||
}
|
||||
|
||||
text = "Heap: %d/%d MB (%.1f%%) OffHeap: %d MB %s".formatted(heapusage.getUsed() >> 20, heapusage.getMax() >> 20, memory * 100.0, memoryBean.getNonHeapMemoryUsage().getUsed() >> 20, cpuText);
|
||||
}
|
||||
|
||||
String text() {
|
||||
return text;
|
||||
}
|
||||
|
||||
float memory() {
|
||||
return memory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.fml.earlydisplay;
|
||||
|
||||
public class QuadHelper {
|
||||
public static void loadQuad(SimpleBufferBuilder bb, float x0, float x1, float y0, float y1, float u0, float u1, float v0, float v1, int colour) {
|
||||
bb.pos(x0, y0).tex(u0, v0).colour(colour).endVertex();
|
||||
bb.pos(x1, y0).tex(u1, v0).colour(colour).endVertex();
|
||||
bb.pos(x0, y1).tex(u0, v1).colour(colour).endVertex();
|
||||
bb.pos(x1, y1).tex(u1, v1).colour(colour).endVertex();
|
||||
}
|
||||
|
||||
public static void loadQuad(SimpleBufferBuilder bb, float x0, float x1, float y0, float y1, float u0, float u1, float v0, float v1) {
|
||||
bb.pos(x0, y0).tex(u0, v0).endVertex();
|
||||
bb.pos(x1, y0).tex(u1, v0).endVertex();
|
||||
bb.pos(x0, y1).tex(u0, v1).endVertex();
|
||||
bb.pos(x1, y1).tex(u1, v1).endVertex();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.fml.earlydisplay;
|
||||
|
||||
import net.minecraftforge.fml.loading.progress.Message;
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
import net.minecraftforge.fml.loading.progress.StartupNotificationManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.lwjgl.opengl.GL32C.*;
|
||||
|
||||
public class RenderElement {
|
||||
static final int INDEX_TEXTURE_OFFSET = 5;
|
||||
private final SimpleBufferBuilder bb;
|
||||
private final Renderer renderer;
|
||||
static int globalAlpha = 255;
|
||||
private int retireCount;
|
||||
|
||||
interface Renderer {
|
||||
|
||||
void accept(SimpleBufferBuilder bb, DisplayContext context, int frame);
|
||||
|
||||
default Renderer then(Renderer r) {
|
||||
if (r == null) return this;
|
||||
return (bb, ctx, frame) -> {
|
||||
r.accept(bb, ctx, frame);
|
||||
this.accept(bb, ctx, frame);
|
||||
};
|
||||
}
|
||||
}
|
||||
@FunctionalInterface
|
||||
interface TextureRenderer {
|
||||
void accept(SimpleBufferBuilder bb, DisplayContext context, int[] size, int frame);
|
||||
}
|
||||
@FunctionalInterface
|
||||
interface Initializer extends Supplier<Renderer> {}
|
||||
|
||||
@FunctionalInterface
|
||||
interface TextGenerator {
|
||||
void accept(SimpleBufferBuilder bb, SimpleFont fh, DisplayContext ctx);
|
||||
}
|
||||
|
||||
public record DisplayContext(int width, int height, int scale, ElementShader elementShader, ColourScheme colourScheme, PerformanceInfo performance) {
|
||||
public int scaledWidth() {
|
||||
return scale() * width();
|
||||
}
|
||||
|
||||
public int scaledHeight() {
|
||||
return scale() * height();
|
||||
}
|
||||
}
|
||||
|
||||
public RenderElement(final Initializer rendererInitializer) {
|
||||
this.bb = new SimpleBufferBuilder(1);
|
||||
this.renderer = rendererInitializer.get();
|
||||
}
|
||||
|
||||
public boolean render(DisplayContext ctx, int count) {
|
||||
this.renderer.accept(bb, ctx, count);
|
||||
return this.retireCount == 0 || this.retireCount < count;
|
||||
}
|
||||
|
||||
public void retire(final int frame) {
|
||||
this.retireCount = frame;
|
||||
}
|
||||
|
||||
private static void startupLogMessages(SimpleBufferBuilder bb, SimpleFont font, DisplayContext context) {
|
||||
List<StartupNotificationManager.AgeMessage> messages = StartupNotificationManager.getMessages();
|
||||
List<SimpleFont.DisplayText> texts = new ArrayList<>();
|
||||
for (int i = messages.size() - 1; i >= 0; i--) {
|
||||
final StartupNotificationManager.AgeMessage pair = messages.get(i);
|
||||
final float fade = clamp((4000.0f - (float) pair.age() - ( i - 4 ) * 1000.0f) / 5000.0f, 0.0f, 1.0f);
|
||||
if (fade <0.01f) continue;
|
||||
Message msg = pair.message();
|
||||
int colour = Math.min((int)(fade * 255f), globalAlpha) << 24 | 0xFFFFFF;
|
||||
texts.add(new SimpleFont.DisplayText(msg.getText()+"\n", colour));
|
||||
}
|
||||
|
||||
font.generateVerticesForTexts(10, context.scaledHeight() - texts.size() * font.lineSpacing() + font.descent() - 10, bb, texts.toArray(SimpleFont.DisplayText[]::new));
|
||||
}
|
||||
public static RenderElement monag() {
|
||||
return new RenderElement(RenderElement.initializeTexture("monagstudios.png", 45000, 4, (bb, ctx, sz, frame) -> {
|
||||
var size = 256;
|
||||
var x0 = (ctx.width() - 2 * size) / 2;
|
||||
var y0 = 64;
|
||||
QuadHelper.loadQuad(bb, x0, x0+size, y0, y0+size/2f, 0f, 1f, 0f, 0.5f, 0xFFFFFFFF);
|
||||
QuadHelper.loadQuad(bb, x0+size, x0+2*size, y0, y0+size/2f, 0f, 1f, 0.5f, 1f, 0xFFFFFFFF);
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
public static RenderElement mojang(final int textureId, final int frameStart) {
|
||||
return new RenderElement(()->(bb, ctx, frame) -> {
|
||||
var size = 256 * ctx.scale();
|
||||
var x0 = (ctx.scaledWidth() - 2 * size) / 2;
|
||||
var y0 = 64 * ctx.scale() + 32;
|
||||
ctx.elementShader().updateTextureUniform(0);
|
||||
ctx.elementShader().updateRenderTypeUniform(ElementShader.RenderType.TEXTURE);
|
||||
var fade = Math.min((frame - frameStart) * 10, 255);
|
||||
glBindTexture(GL_TEXTURE_2D, textureId);
|
||||
bb.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS);
|
||||
QuadHelper.loadQuad(bb, x0, x0+size, y0, y0+size/2f, 0f, 1f, 0f, 0.5f, (fade << 24) | 0xFFFFFF);
|
||||
QuadHelper.loadQuad(bb, x0+size, x0+2*size, y0, y0+size/2f, 0f, 1f, 0.5f, 1f, (fade << 24) | 0xFFFFFF);
|
||||
bb.draw();
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
});
|
||||
}
|
||||
public static RenderElement logMessageOverlay(SimpleFont font) {
|
||||
return new RenderElement(RenderElement.initializeText(font, RenderElement::startupLogMessages));
|
||||
}
|
||||
|
||||
public static RenderElement forgeVersionOverlay(SimpleFont font, String version) {
|
||||
return new RenderElement(RenderElement.initializeText(font, (bb, fnt, ctx)->
|
||||
font.generateVerticesForTexts(ctx.scaledWidth() - font.stringWidth(version) - 10,
|
||||
ctx.scaledHeight() - font.lineSpacing() + font.descent() - 10, bb,
|
||||
new SimpleFont.DisplayText(version, ctx.colourScheme.foreground().packedint(RenderElement.globalAlpha)))));
|
||||
}
|
||||
public static RenderElement squir() {
|
||||
return new RenderElement(RenderElement.initializeTexture("squirrel.png", 45000, 3, (bb, context, size, frame) -> {
|
||||
var inset = 5f;
|
||||
var x0 = inset;
|
||||
var x1 = inset + size[0] * context.scale();
|
||||
var y0 = inset;
|
||||
var y1 = inset + size[1] * context.scale();
|
||||
int fade = (int) (Math.cos(frame * Math.PI / 16) * 16) + 16;
|
||||
// int fade = 0xff;
|
||||
var colour = (Math.min(fade, globalAlpha) & 0xff) << 24 | 0xffffff;
|
||||
QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 1f, 0f, 1f, colour);
|
||||
}));
|
||||
}
|
||||
|
||||
public static RenderElement anvil(SimpleFont font) {
|
||||
return new RenderElement(RenderElement.initializeTexture("forge_anvil.png", 20000, 2, (bb, context, size, frame) -> {
|
||||
var x0 = context.scaledWidth() - size[0] * context.scale();
|
||||
var x1 = context.scaledWidth();
|
||||
var y0 = context.scaledHeight() - size[0] * context.scale() - font.descent() - font.lineSpacing();
|
||||
var y1 = context.scaledHeight() - font.descent() - font.lineSpacing();
|
||||
int frameidx = frame % 32;
|
||||
float framepos = (frameidx * (float)size[0]) / size[1];
|
||||
float framesize = size[0] / (float)size[1];
|
||||
QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 1f, framepos, framepos+framesize, globalAlpha << 24 | 0xFFFFFF);
|
||||
}));
|
||||
}
|
||||
public static RenderElement progressBars(SimpleFont font) {
|
||||
return new RenderElement(() -> (bb, ctx, frame) -> RenderElement.startupProgressBars(font, bb, ctx, frame));
|
||||
}
|
||||
|
||||
public static RenderElement performanceBar(SimpleFont font) {
|
||||
return new RenderElement(() -> (bb, ctx, frame) -> RenderElement.memoryInfo(font, bb, ctx, frame));
|
||||
}
|
||||
|
||||
public static void startupProgressBars(SimpleFont font, final SimpleBufferBuilder buffer, final DisplayContext context, final int frameNumber) {
|
||||
Renderer acc = null;
|
||||
var barCount = 2;
|
||||
List<ProgressMeter> currentProgress = StartupNotificationManager.getCurrentProgress();
|
||||
var size = currentProgress.size();
|
||||
var alpha = 0xFF;
|
||||
for (int i = 0; i < barCount && i < size; i++) {
|
||||
final ProgressMeter pm = currentProgress.get(i);
|
||||
Renderer barRenderer = barRenderer(i, alpha, font, pm, context);
|
||||
acc = barRenderer.then(acc);
|
||||
alpha >>= 1;
|
||||
}
|
||||
if (acc != null)
|
||||
acc.accept(buffer, context, frameNumber);
|
||||
}
|
||||
private static final int BAR_HEIGHT = 20;
|
||||
private static final int BAR_WIDTH = 400;
|
||||
private static Renderer barRenderer(int cnt, int alpha, SimpleFont font, ProgressMeter pm, DisplayContext context) {
|
||||
var barSpacing = font.lineSpacing() - font.descent() + BAR_HEIGHT;
|
||||
var y = 250 * context.scale() + cnt * barSpacing;
|
||||
var colour = (alpha << 24) | 0xFFFFFF;
|
||||
Renderer bar;
|
||||
if (pm.steps() == 0) {
|
||||
bar = progressBar(ctx->new int[] {(ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y + font.lineSpacing() - font.descent(), BAR_WIDTH * ctx.scale()}, f->colour, frame -> indeterminateBar(frame, cnt == 0));
|
||||
} else {
|
||||
bar = progressBar(ctx -> new int[]{(ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y + font.lineSpacing() - font.descent(), BAR_WIDTH * ctx.scale()}, f -> colour, f -> new float[]{0f, pm.progress()});
|
||||
}
|
||||
Renderer label = (bb, ctx, frame) -> renderText(font, text((ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y, pm.label().getText(), colour), bb, ctx);
|
||||
return bar.then(label);
|
||||
}
|
||||
private static float[] indeterminateBar(int frame, boolean isActive) {
|
||||
if (RenderElement.globalAlpha != 0xFF || !isActive) {
|
||||
return new float[] {0f,1f};
|
||||
} else {
|
||||
var progress = frame % 100;
|
||||
return new float[]{clamp((progress - 2) / 100f, 0f, 1f), clamp((progress + 2) / 100f, 0f, 1f)};
|
||||
}
|
||||
}
|
||||
|
||||
private static void memoryInfo(SimpleFont font, final SimpleBufferBuilder buffer, final DisplayContext context, final int frameNumber) {
|
||||
var y = 10 * context.scale();
|
||||
PerformanceInfo pi = context.performance();
|
||||
final int colour = hsvToRGB((1.0f - (float)Math.pow(pi.memory(), 1.5f)) / 3f, 1.0f, 0.5f);
|
||||
var bar = progressBar(ctx -> new int[]{(ctx.scaledWidth() - BAR_WIDTH * ctx.scale()) / 2, y, BAR_WIDTH * ctx.scale()}, f -> colour, f -> new float[]{0f, pi.memory()});
|
||||
var width = font.stringWidth(pi.text());
|
||||
Renderer label = (bb, ctx, frame) -> renderText(font, text(ctx.scaledWidth() / 2 - width / 2, y + 18, pi.text(), context.colourScheme.foreground().packedint(globalAlpha)), bb, ctx);
|
||||
bar.then(label).accept(buffer, context, frameNumber);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface ColourFunction {
|
||||
int colour(int frame);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface ProgressDisplay {
|
||||
float[] progress(int frame);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface BarPosition {
|
||||
int[] location(DisplayContext context);
|
||||
}
|
||||
public static Renderer progressBar(BarPosition position, ColourFunction colourFunction, ProgressDisplay progressDisplay) {
|
||||
return (bb, context, frame) -> {
|
||||
var colour = colourFunction.colour(frame);
|
||||
var alpha = (colour & 0xFF000000) >> 24;
|
||||
context.elementShader().updateTextureUniform(0);
|
||||
context.elementShader().updateRenderTypeUniform(ElementShader.RenderType.BAR);
|
||||
var progress = progressDisplay.progress(frame);
|
||||
bb.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS);
|
||||
var inset = 2;
|
||||
var pos = position.location(context);
|
||||
var x0 = pos[0];
|
||||
var x1 = pos[0] + pos[2] + 4 * inset;
|
||||
var y0 = pos[1];
|
||||
var y1 = y0 + BAR_HEIGHT;
|
||||
QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 0f, 0f, 0f, context.colourScheme().foreground().packedint(alpha));
|
||||
|
||||
x0 += inset;
|
||||
x1 -= inset;
|
||||
y0 += inset;
|
||||
y1 -= inset;
|
||||
QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 0f, 0f, 0f, context.colourScheme().background().packedint(RenderElement.globalAlpha));
|
||||
|
||||
x1 = x0 + inset + (int)(progress[1] * pos[2]);
|
||||
x0 += inset + progress[0] * pos[2];
|
||||
y0 += inset;
|
||||
y1 -= inset;
|
||||
QuadHelper.loadQuad(bb, x0, x1, y0, y1, 0f, 0f, 0f, 0f, colour);
|
||||
bb.draw();
|
||||
};
|
||||
}
|
||||
|
||||
private static Initializer initializeText(SimpleFont font, TextGenerator textGenerator) {
|
||||
return () -> (bb, context, frame) -> renderText(font, textGenerator, bb, context);
|
||||
}
|
||||
|
||||
private static void renderText(final SimpleFont font, final TextGenerator textGenerator, final SimpleBufferBuilder bb, final DisplayContext context) {
|
||||
context.elementShader().updateTextureUniform(font.textureNumber());
|
||||
context.elementShader().updateRenderTypeUniform(ElementShader.RenderType.FONT);
|
||||
bb.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS);
|
||||
textGenerator.accept(bb, font, context);
|
||||
bb.draw();
|
||||
}
|
||||
|
||||
private static TextGenerator text(int x, int y, String text, int colour) {
|
||||
return (bb, font, context) -> font.generateVerticesForTexts(x, y, bb, new SimpleFont.DisplayText(text, colour));
|
||||
}
|
||||
private static Initializer initializeTexture(final String textureFileName, int size, int textureNumber, TextureRenderer positionAndColour) {
|
||||
return ()->{
|
||||
int[] imgSize = STBHelper.loadTextureFromClasspath(textureFileName, size, GL_TEXTURE0 + textureNumber + INDEX_TEXTURE_OFFSET);
|
||||
return (bb, ctx, frame) -> {
|
||||
ctx.elementShader().updateTextureUniform(textureNumber + INDEX_TEXTURE_OFFSET);
|
||||
ctx.elementShader().updateRenderTypeUniform(ElementShader.RenderType.TEXTURE);
|
||||
renderTexture(bb, ctx, frame, imgSize, positionAndColour);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
private static void renderTexture(SimpleBufferBuilder bb, DisplayContext context, int frame, int[] size, TextureRenderer positionAndColour) {
|
||||
bb.begin(SimpleBufferBuilder.Format.POS_TEX_COLOR, SimpleBufferBuilder.Mode.QUADS);
|
||||
positionAndColour.accept(bb, context, size, frame);
|
||||
bb.draw();
|
||||
}
|
||||
|
||||
|
||||
public static float clamp(float num, float min, float max) {
|
||||
if (num < min) {
|
||||
return min;
|
||||
} else {
|
||||
return Math.min(num, max);
|
||||
}
|
||||
}
|
||||
|
||||
public static int clamp(int num, int min, int max) {
|
||||
if (num < min) {
|
||||
return min;
|
||||
} else {
|
||||
return Math.min(num, max);
|
||||
}
|
||||
}
|
||||
|
||||
public static int hsvToRGB(float hue, float saturation, float value) {
|
||||
int i = (int)(hue * 6.0F) % 6;
|
||||
float f = hue * 6.0F - (float)i;
|
||||
float f1 = value * (1.0F - saturation);
|
||||
float f2 = value * (1.0F - f * saturation);
|
||||
float f3 = value * (1.0F - (1.0F - f) * saturation);
|
||||
float f4;
|
||||
float f5;
|
||||
float f6;
|
||||
switch(i) {
|
||||
case 0:
|
||||
f4 = value;
|
||||
f5 = f3;
|
||||
f6 = f1;
|
||||
break;
|
||||
case 1:
|
||||
f4 = f2;
|
||||
f5 = value;
|
||||
f6 = f1;
|
||||
break;
|
||||
case 2:
|
||||
f4 = f1;
|
||||
f5 = value;
|
||||
f6 = f3;
|
||||
break;
|
||||
case 3:
|
||||
f4 = f1;
|
||||
f5 = f2;
|
||||
f6 = value;
|
||||
break;
|
||||
case 4:
|
||||
f4 = f3;
|
||||
f5 = f1;
|
||||
f6 = value;
|
||||
break;
|
||||
case 5:
|
||||
f4 = value;
|
||||
f5 = f1;
|
||||
f6 = f2;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Something went wrong when converting from HSV to RGB. Input was " + hue + ", " + saturation + ", " + value);
|
||||
}
|
||||
|
||||
int j = clamp((int)(f4 * 255.0F), 0, 255);
|
||||
int k = clamp((int)(f5 * 255.0F), 0, 255);
|
||||
int l = clamp((int)(f6 * 255.0F), 0, 255);
|
||||
return 0xFF << 24 | j << 16 | k << 8 | l;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.fml.earlydisplay;
|
||||
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.stb.STBImage;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.Channels;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.lwjgl.opengl.GL32C.*;
|
||||
|
||||
public class STBHelper {
|
||||
public static ByteBuffer readFromClasspath(final String name, int initialCapacity) {
|
||||
ByteBuffer buf;
|
||||
try (var channel = Channels.newChannel(
|
||||
Objects.requireNonNull(STBHelper.class.getClassLoader().getResourceAsStream(name), "The resource "+name+" cannot be found"))) {
|
||||
buf = BufferUtils.createByteBuffer(initialCapacity);
|
||||
while (true) {
|
||||
var readbytes = channel.read(buf);
|
||||
if (readbytes == -1) break;
|
||||
if (buf.remaining() == 0) { // extend the buffer by 50%
|
||||
var newBuf = BufferUtils.createByteBuffer(buf.capacity() * 3 / 2);
|
||||
buf.flip();
|
||||
newBuf.put(buf);
|
||||
buf = newBuf;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
buf.flip();
|
||||
return MemoryUtil.memSlice(buf); // we trim the final buffer to the size of the content
|
||||
}
|
||||
|
||||
public static int[] loadTextureFromClasspath(String file, int size, int textureNumber) {
|
||||
int[] lw = new int[1];
|
||||
int[] lh = new int[1];
|
||||
int[] lc = new int[1];
|
||||
var img = loadImageFromClasspath(file, size, lw, lh, lc);
|
||||
var texid = glGenTextures();
|
||||
glActiveTexture(textureNumber);
|
||||
glBindTexture(GL_TEXTURE_2D, texid);
|
||||
// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, lw[0], lh[0], 0, GL_RGBA, GL_UNSIGNED_BYTE, img);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
MemoryUtil.memFree(img);
|
||||
return new int[] {lw[0], lh[0]};
|
||||
}
|
||||
|
||||
public static ByteBuffer loadImageFromClasspath(String file, int size, int[] width, int[] height, int[] channels) {
|
||||
ByteBuffer buf = STBHelper.readFromClasspath(file, size);
|
||||
return STBImage.stbi_load_from_memory(buf, width, height, channels, 4);
|
||||
}
|
||||
}
|
||||
+517
@@ -0,0 +1,517 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.fml.earlydisplay;
|
||||
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.lwjgl.opengl.GL32C.*;
|
||||
|
||||
/**
|
||||
* A very simple, Mojang inspired BufferBuilder.
|
||||
* <em>This has been customized for 2d rendering such as text and simple planar textures</em>
|
||||
* <p>
|
||||
* Not bound to any specific format, ideally should be held onto for re-use.
|
||||
* <p>
|
||||
* Can be used for 'immediate mode' style rendering using {@link #draw()}, or
|
||||
* upload to external vertex arrays for proper instancing using {@link #finishAndUpload()}.
|
||||
* <p>
|
||||
* This is a Triangles only buffer, all data uploaded is in Triangles.
|
||||
* Quads are converted to triangles using {@code 0, 1, 2, 0, 2, 3}.
|
||||
* <p>
|
||||
* Any given {@link Format} should have its individual {@link Element} components
|
||||
* buffered in the order specified by the {@link Format},
|
||||
* followed by an {@link #endVertex()} call to prepare for the next vertex.
|
||||
* <p>
|
||||
* It is illegal to buffer primitives in any format other than the one specified to
|
||||
* {@link #begin(Format, Mode)}.
|
||||
*
|
||||
* @author covers1624
|
||||
*/
|
||||
public class SimpleBufferBuilder implements Closeable {
|
||||
|
||||
private static final MemoryUtil.MemoryAllocator ALLOCATOR = MemoryUtil.getAllocator(false);
|
||||
|
||||
private static final int[] VERTEX_ARRAYS = new int[Format.values().length];
|
||||
private static final int[] VERTEX_BUFFERS = new int[Format.values().length];
|
||||
private static final int[] VERTEX_BUFFER_LENGTHS = new int[Format.values().length];
|
||||
private static int elementBuffer = 0;
|
||||
private static int elementBufferVertexLength = 0;
|
||||
|
||||
static {
|
||||
Arrays.fill(VERTEX_ARRAYS, 0);
|
||||
Arrays.fill(VERTEX_BUFFERS, 0);
|
||||
Arrays.fill(VERTEX_BUFFER_LENGTHS, 0);
|
||||
}
|
||||
|
||||
private long bufferAddr; // Pointer to the backing buffer.
|
||||
private ByteBuffer buffer; // ByteBuffer view of the backing buffer.
|
||||
|
||||
private Format format; // The current format we are buffering.
|
||||
private Mode mode; // The current mode we are buffering.
|
||||
private boolean building; // If we are building the buffer.
|
||||
private int elementIndex; // The current element index we are buffering. if elementIndex == format.types.length, we expect 'endVertex'
|
||||
private int index; // The current index into the buffer we are writing to.
|
||||
private int vertices; // The number of complete vertices we have buffered.
|
||||
|
||||
/**
|
||||
* Create a new SimpleBufferBuilder with an initial capacity.
|
||||
* <p>
|
||||
* The buffer will be doubled as required.
|
||||
* <p>
|
||||
* Generally picking a small number, around 128/256 should be a
|
||||
* safe bet. Provided you cache your buffers, it should not mean much overall.
|
||||
*
|
||||
* @param capacity The initial capacity in bytes.
|
||||
*/
|
||||
public SimpleBufferBuilder(int capacity) {
|
||||
bufferAddr = ALLOCATOR.malloc(capacity);
|
||||
buffer = MemoryUtil.memByteBuffer(bufferAddr, capacity);
|
||||
}
|
||||
|
||||
public static void destroy() {
|
||||
glDeleteBuffers(VERTEX_BUFFERS);
|
||||
glDeleteBuffers(elementBuffer);
|
||||
glDeleteVertexArrays(VERTEX_ARRAYS);
|
||||
}
|
||||
|
||||
private static void ensureElementBufferLength(int vertices) {
|
||||
if (elementBufferVertexLength >= vertices) {
|
||||
return;
|
||||
}
|
||||
|
||||
// treating it as immutable storage, even though it's not
|
||||
final var newElementBuffer = glGenBuffers();
|
||||
var newElementBufferVertexLength = Math.max(1024, elementBufferVertexLength);
|
||||
while (newElementBufferVertexLength < vertices) {
|
||||
newElementBufferVertexLength *= 2;
|
||||
}
|
||||
|
||||
final var oldIndexCount = elementBufferVertexLength + elementBufferVertexLength / 2;
|
||||
final var newIndexCount = newElementBufferVertexLength + newElementBufferVertexLength / 2;
|
||||
|
||||
// allocate new buffer
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, newElementBuffer);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, newIndexCount * 4L, GL_STATIC_DRAW);
|
||||
|
||||
// mapping avoids creating additional CPU copies of the data
|
||||
// unsynchronized is fine because this is a brand-new buffer, and the old contents will be copied in afterward
|
||||
// also can invalidate the whole buffer too, similarly because brand new, don't care what was there before
|
||||
final var mappingOffset = oldIndexCount * 4;
|
||||
final var mappingSize = (newIndexCount - oldIndexCount) * 4;
|
||||
final var mappedBuffer = glMapBufferRange(GL_ELEMENT_ARRAY_BUFFER, mappingOffset, mappingSize, GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
|
||||
|
||||
if(mappedBuffer == null){
|
||||
throw new NullPointerException("OpenGL buffer mapping failed");
|
||||
}
|
||||
|
||||
final int quads = newElementBufferVertexLength / 4;
|
||||
final int oldQuads = elementBufferVertexLength / 4;
|
||||
// generate indices for the extension to the buffer
|
||||
for (int i = oldQuads; i < quads; i++) {
|
||||
// Quads are a bit different, we need to emit 2 triangles such that
|
||||
// when combined they make up a single quad.
|
||||
mappedBuffer.putInt(i * 4 + 0).putInt(i * 4 + 1).putInt(i * 4 + 2);
|
||||
mappedBuffer.putInt(i * 4 + 1).putInt(i * 4 + 3).putInt(i * 4 + 2);
|
||||
}
|
||||
|
||||
glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
|
||||
|
||||
if (elementBuffer != 0) {
|
||||
// copy old data from previous element buffer
|
||||
glBindBuffer(GL_COPY_READ_BUFFER, elementBuffer);
|
||||
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_ELEMENT_ARRAY_BUFFER, 0, 0, mappingOffset);
|
||||
glBindBuffer(GL_COPY_READ_BUFFER, 0);
|
||||
}
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
|
||||
glDeleteBuffers(elementBuffer);
|
||||
elementBuffer = newElementBuffer;
|
||||
elementBufferVertexLength = newElementBufferVertexLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start building a new set of vertex data in the
|
||||
* given format and mode.
|
||||
*
|
||||
* @param format The format to start building in.
|
||||
* @param mode The mode to start building in.
|
||||
*/
|
||||
public SimpleBufferBuilder begin(Format format, Mode mode) {
|
||||
if (bufferAddr == MemoryUtil.NULL) {
|
||||
throw new IllegalStateException("Buffer has been freed."); // You already free'd the buffer
|
||||
}
|
||||
if (building) {
|
||||
throw new IllegalStateException("Already building."); // Your already building verticies.
|
||||
}
|
||||
this.format = format;
|
||||
this.mode = mode;
|
||||
building = true;
|
||||
elementIndex = 0;
|
||||
ensureSpace(format.stride);
|
||||
// Rewind ready for new data.
|
||||
buffer.rewind();
|
||||
buffer.limit(buffer.capacity());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buffer a position element.
|
||||
*
|
||||
* @param x The x.
|
||||
* @param y The y.
|
||||
* @param z The z.
|
||||
* @return The same builder.
|
||||
*/
|
||||
public SimpleBufferBuilder pos(float x, float y) {
|
||||
if (!building) throw new IllegalStateException("Not building."); // You did not call begin.
|
||||
|
||||
if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call.
|
||||
if (format.types[elementIndex] != Element.POS) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order.
|
||||
|
||||
// Assumes that our POS element specifies the FLOAT data type.
|
||||
buffer.putFloat(index + 0, x);
|
||||
buffer.putFloat(index + 4, y);
|
||||
|
||||
// Increment index for the number of bytes we wrote and increment the element index.
|
||||
index += format.types[elementIndex].width;
|
||||
elementIndex++;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buffer a texture element.
|
||||
*
|
||||
* @param u The u.
|
||||
* @param v The v.
|
||||
* @return The same builder.
|
||||
*/
|
||||
public SimpleBufferBuilder tex(float u, float v) {
|
||||
if (!building) throw new IllegalStateException("Not building."); // You did not call begin.
|
||||
|
||||
if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call.
|
||||
if (format.types[elementIndex] != Element.TEX) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order.
|
||||
|
||||
// Assumes our TEX element specifies the FLOAT data type.
|
||||
buffer.putFloat(index + 0, u);
|
||||
buffer.putFloat(index + 4, v);
|
||||
|
||||
// Increment index for the number of bytes we wrote and increment the element index.
|
||||
index += format.types[elementIndex].width;
|
||||
elementIndex++;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buffer a color element.
|
||||
*
|
||||
* @param r The red component. (0-1)
|
||||
* @param g The green component. (0-1)
|
||||
* @param b The blue component. (0-1)
|
||||
* @param a The alpha component. (0-1)
|
||||
* @return The same buffer.
|
||||
*/
|
||||
public SimpleBufferBuilder colour(float r, float g, float b, float a) {
|
||||
// Expand floats to 0-255 and forward.
|
||||
return colour((byte) (r * 255F), (byte) (g * 255F), (byte) (b * 255F), (byte) (a * 255F));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ColourScheme.Colour#packedint(int)
|
||||
* @param packedColor an ABGR packed int
|
||||
* @return the same buffer.
|
||||
*/
|
||||
public SimpleBufferBuilder colour(int packedColor) {
|
||||
if (!building) throw new IllegalStateException("Not building."); // You did not call begin.
|
||||
|
||||
if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call.
|
||||
if (format.types[elementIndex] != Element.COLOR) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order.
|
||||
|
||||
// Assumes our COLOR element specifies the UNSIGNED_BYTE data type.
|
||||
buffer.putInt(index + 0, packedColor);
|
||||
|
||||
// Increment index for the number of bytes we wrote and increment the element index.
|
||||
index += format.types[elementIndex].width;
|
||||
elementIndex++;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Buffer a color element.
|
||||
*
|
||||
* @param r The red component. (0-255)
|
||||
* @param g The green component. (0-255)
|
||||
* @param b The blue component. (0-255)
|
||||
* @param a The alpha component. (0-255)
|
||||
* @return The same buffer.
|
||||
*/
|
||||
public SimpleBufferBuilder colour(byte r, byte g, byte b, byte a) {
|
||||
if (!building) throw new IllegalStateException("Not building."); // You did not call begin.
|
||||
|
||||
if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // we have reached the end of elements to buffer for this vertex, we expected an endVertex call.
|
||||
if (format.types[elementIndex] != Element.COLOR) throw new IllegalArgumentException("Expected " + format.types[elementIndex]); // You called the wrong method for the format order.
|
||||
|
||||
// Assumes our COLOR element specifies the UNSIGNED_BYTE data type.
|
||||
buffer.put(index + 0, r);
|
||||
buffer.put(index + 1, g);
|
||||
buffer.put(index + 2, b);
|
||||
buffer.put(index + 3, a);
|
||||
|
||||
// Increment index for the number of bytes we wrote and increment the element index.
|
||||
index += format.types[elementIndex].width;
|
||||
elementIndex++;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* End building the current vertex and prepare for the next.
|
||||
*
|
||||
* @return The same builder.
|
||||
*/
|
||||
public SimpleBufferBuilder endVertex() {
|
||||
if (!building) throw new IllegalStateException("Not building."); // You did not call begin.
|
||||
|
||||
if (elementIndex != format.types.length) throw new IllegalStateException("Expected " + format.types[elementIndex]); // You did not finish building the vertex.
|
||||
|
||||
// Reset elementIndex
|
||||
elementIndex = 0;
|
||||
// Increment the number of vertices we have so far buffered.
|
||||
vertices++;
|
||||
// Make sure there is space for the next vertex.
|
||||
ensureSpace(format.stride);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Checks there is enough space in the buffer for specified number of bytes.
|
||||
// If there is not enough space, the buffer is increased by 50%.
|
||||
private void ensureSpace(int newBytes) {
|
||||
int cap = buffer.capacity();
|
||||
if (index + newBytes > cap) {
|
||||
int newCap = Math.max(3 * cap / 2, 3 * newBytes / 2);
|
||||
bufferAddr = ALLOCATOR.realloc(bufferAddr, newCap);
|
||||
buffer = MemoryUtil.memByteBuffer(bufferAddr, newCap);
|
||||
buffer.rewind();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload the current buffer.
|
||||
* <p>
|
||||
* This will bind a {@link org.lwjgl.opengl.GL32C#GL_ARRAY_BUFFER} and {@link org.lwjgl.opengl.GL32C#GL_ELEMENT_ARRAY_BUFFER}
|
||||
* <p>
|
||||
* The vertex data and index data is uploaded to their respective buffers.
|
||||
* <p>
|
||||
* Uploading the buffers finishes drawing and resets for the next buffer operation.
|
||||
* <p>
|
||||
* This should not be called in conjunction with {@link #draw()}
|
||||
*
|
||||
* @return The number of indexes that were uploaded.
|
||||
*/
|
||||
public int finishAndUpload() {
|
||||
if (!building) throw new IllegalStateException("Not building.");
|
||||
|
||||
int indices;
|
||||
try {
|
||||
if (elementIndex == format.types.length) throw new IllegalStateException("Expected endVertex"); // You didn't finish building your vertex.
|
||||
if (elementIndex != 0) throw new IllegalStateException("Not finished building vertex, Expected: " + format.types[elementIndex]); // You didn't finish building your vertex data.
|
||||
if (vertices == 0) return 0; // No vertices buffered, lets not do anything.
|
||||
if (vertices % mode.vertices != 0) throw new IllegalStateException("Does not contain vertices aligned to " + mode); // You did not put in enough vertices to cleanly slice the data into TRIANGLES/QUADS
|
||||
|
||||
// Reset position to 0, limit the buffer to our index.
|
||||
buffer.position(0);
|
||||
buffer.limit(index);
|
||||
|
||||
// Upload the raw vertex data in dynamic mode.
|
||||
final int vbo = VERTEX_BUFFERS[format.ordinal()];
|
||||
final int vboSize = VERTEX_BUFFER_LENGTHS[format.ordinal()];
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
if (vboSize < index) {
|
||||
// expand buffer, it's not big enough
|
||||
var newVBOSize = Math.max(1024, vboSize);
|
||||
while (newVBOSize < index){
|
||||
newVBOSize *= 2;
|
||||
}
|
||||
// because everything is overwritten anyway, we can do an in-place reallocation
|
||||
glBufferData(GL_ARRAY_BUFFER, newVBOSize, GL_DYNAMIC_DRAW);
|
||||
VERTEX_BUFFER_LENGTHS[format.ordinal()] = newVBOSize;
|
||||
}
|
||||
glBufferSubData(GL_ARRAY_BUFFER, 0, buffer);
|
||||
|
||||
// The number of indices for triangles is equal to our vertex count, as that is
|
||||
// what we operate in. However, for Quads, we have exactly vertices + vertices / 2
|
||||
// vertices once we convert the quads to triangles.
|
||||
indices = mode == Mode.TRIANGLES ? vertices : vertices + vertices / 2;
|
||||
|
||||
if (mode == Mode.QUADS) {
|
||||
ensureElementBufferLength(vertices);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementBuffer);
|
||||
}
|
||||
|
||||
return indices;
|
||||
} finally {
|
||||
// Reset builder state for next begin call.
|
||||
building = false;
|
||||
vertices = 0;
|
||||
index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload and draw this buffer using one of a number of re-usable set of buffers.
|
||||
* <p>
|
||||
* This will immediately upload the buffer, resetting this builder for the next
|
||||
* buffer operation, and draw the uploaded data.
|
||||
* <p>
|
||||
* You will need to bind shaders, textures, etc, before calling this function.
|
||||
*/
|
||||
public void draw() {
|
||||
if (!building) throw new IllegalStateException("Not building.");
|
||||
|
||||
int vao = VERTEX_ARRAYS[format.ordinal()];
|
||||
int vbo = VERTEX_BUFFERS[format.ordinal()];
|
||||
|
||||
if (vao == 0) {
|
||||
// These 3 buffers are paired, you can't allocate one without the others.
|
||||
assert vbo == 0;
|
||||
|
||||
// Make new vertex array and buffers!
|
||||
vao = glGenVertexArrays();
|
||||
vbo = glGenBuffers();
|
||||
|
||||
// Cache the vertex array and buffers for future re-use.
|
||||
VERTEX_ARRAYS[format.ordinal()] = vao;
|
||||
VERTEX_BUFFERS[format.ordinal()] = vbo;
|
||||
|
||||
// Ask our Format to set up its data layout for the vertex array.
|
||||
// but only once, the VAO saves this state
|
||||
glBindVertexArray(vao);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
format.bind();
|
||||
format.enable();
|
||||
}
|
||||
// Bind the vertex array and buffers!
|
||||
glBindVertexArray(vao);
|
||||
|
||||
// Upload the data.
|
||||
int indices = finishAndUpload();
|
||||
|
||||
if (mode == Mode.QUADS) {
|
||||
glDrawElements(GL_TRIANGLES, indices, GL_UNSIGNED_INT, 0);
|
||||
} else {
|
||||
glDrawArrays(GL_TRIANGLES, 0, indices);
|
||||
}
|
||||
|
||||
// Unbind the vertex array.
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear this builder's cached buffer.
|
||||
* <p>
|
||||
* If you are completely done, call {@link #destroy()}
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
ALLOCATOR.free(bufferAddr);
|
||||
bufferAddr = MemoryUtil.NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a primitive mode that this builder is capable of buffering in.
|
||||
*/
|
||||
public enum Mode {
|
||||
TRIANGLES(3),
|
||||
QUADS(4),
|
||||
;
|
||||
|
||||
public final int vertices;
|
||||
|
||||
Mode(int vertices) {
|
||||
this.vertices = vertices;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies a vertex element with a specific data type, number of primitives and a size in bytes.
|
||||
*/
|
||||
public enum Element {
|
||||
POS(GL_FLOAT, 2, 2 * 4),
|
||||
TEX(GL_FLOAT, 2, 2 * 4),
|
||||
COLOR(GL_UNSIGNED_BYTE, 4, 4);
|
||||
|
||||
public final int glType;
|
||||
public final int count;
|
||||
public final int width;
|
||||
|
||||
Element(int glType, int count, int width) {
|
||||
this.glType = glType;
|
||||
this.count = count;
|
||||
this.width = width;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies a combination of vertex elements.
|
||||
*/
|
||||
public enum Format {
|
||||
POS(Element.POS),
|
||||
POS_TEX(Element.POS, Element.TEX),
|
||||
POS_COLOR(Element.POS, Element.COLOR),
|
||||
POS_TEX_COLOR(Element.POS, Element.TEX, Element.COLOR);
|
||||
|
||||
private final Element[] types;
|
||||
public final int stride;
|
||||
|
||||
Format(Element... types) {
|
||||
this.types = types;
|
||||
|
||||
// Stride is the width of each vertex in bytes.
|
||||
stride = Arrays.stream(types).mapToInt(e -> e.width).sum();
|
||||
}
|
||||
|
||||
/**
|
||||
* set up the attribute pointers for this format.
|
||||
* <p>
|
||||
* Assumes that an array buffer is already bound and ready to go.
|
||||
*/
|
||||
public void bind() {
|
||||
int offset = 0;
|
||||
|
||||
// Set up the pointers that tell GL where our interleaved
|
||||
// vertex data is in the buffers.
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
Element type = types[i];
|
||||
switch (type.glType) {
|
||||
case GL_FLOAT -> glVertexAttribPointer(i, type.count, GL_FLOAT, false, stride, offset);
|
||||
case GL_UNSIGNED_BYTE -> glVertexAttribPointer(i, type.count, GL_UNSIGNED_BYTE, true, stride, offset);
|
||||
default -> throw new IllegalStateException("Unknown glType, I don't know how to bind this vertex element: " + type);
|
||||
}
|
||||
// add to the offset for the next element.
|
||||
offset += type.width;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the vertex attributes this format contains.
|
||||
*/
|
||||
public void enable() {
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
glEnableVertexAttribArray(i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the vertex attributes this format contains.
|
||||
*/
|
||||
public void disable() {
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
glDisableVertexAttribArray(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.fml.earlydisplay;
|
||||
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.stb.STBTTAlignedQuad;
|
||||
import org.lwjgl.stb.STBTTFontinfo;
|
||||
import org.lwjgl.stb.STBTTPackContext;
|
||||
import org.lwjgl.stb.STBTTPackRange;
|
||||
import org.lwjgl.stb.STBTTPackedchar;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.lwjgl.opengl.GL32C.*;
|
||||
import static org.lwjgl.stb.STBTruetype.*;
|
||||
import static org.lwjgl.system.MemoryUtil.NULL;
|
||||
|
||||
public class SimpleFont {
|
||||
private final int textureNumber;
|
||||
private final int lineSpacing;
|
||||
private final int descent;
|
||||
private final int GLYPH_COUNT = 127-32;
|
||||
private final Glyph[] glyphs;
|
||||
|
||||
private record Glyph(char c, int charwidth, int[] pos, float[] uv) {
|
||||
Pos loadQuad(Pos pos, int colour, SimpleBufferBuilder bb) {
|
||||
final var x0 = pos.x() + pos()[0];
|
||||
final var y0 = pos.y() + pos()[1];
|
||||
final var x1 = pos.x() + pos()[2];
|
||||
final var y1 = pos.y() + pos()[3];
|
||||
bb.pos(x0, y0).tex(uv()[0], uv()[1]).colour(colour).endVertex();
|
||||
bb.pos(x1, y0).tex(uv()[2], uv()[1]).colour(colour).endVertex();
|
||||
bb.pos(x0, y1).tex(uv()[0], uv()[3]).colour(colour).endVertex();
|
||||
bb.pos(x1, y1).tex(uv()[2], uv()[3]).colour(colour).endVertex();
|
||||
return new Pos(pos.x()+charwidth(), pos.y(), pos.minx());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the font and store it in the textureNumber location
|
||||
*/
|
||||
public SimpleFont(String fontName, int scale, int bufferSize, int textureNumber) {
|
||||
ByteBuffer buf = STBHelper.readFromClasspath(fontName, bufferSize);
|
||||
var info = STBTTFontinfo.create();
|
||||
if (!stbtt_InitFont(info, buf)) {
|
||||
throw new IllegalStateException("Bad font");
|
||||
}
|
||||
|
||||
var ascent = new float[1];
|
||||
var descent = new float[1];
|
||||
var lineGap = new float[1];
|
||||
int fontSize = 24;
|
||||
stbtt_GetScaledFontVMetrics(buf, 0, fontSize, ascent, descent, lineGap);
|
||||
this.lineSpacing = (int)(ascent[0] - descent[0] + lineGap[0]);
|
||||
this.descent = (int)Math.floor(descent[0]);
|
||||
int fontTextureId = glGenTextures();
|
||||
glActiveTexture(GL_TEXTURE0+textureNumber);
|
||||
this.textureNumber = textureNumber;
|
||||
glBindTexture(GL_TEXTURE_2D, fontTextureId);
|
||||
try (var packedchars = STBTTPackedchar.malloc(GLYPH_COUNT)) {
|
||||
int texwidth = 256;
|
||||
int texheight = 128;
|
||||
try (STBTTPackRange.Buffer packRanges = STBTTPackRange.malloc(1)) {
|
||||
var bitmap = BufferUtils.createByteBuffer(texwidth * texheight);
|
||||
try (STBTTPackRange packRange = STBTTPackRange.malloc()) {
|
||||
packRanges.put(packRange.set(fontSize, 32, null, GLYPH_COUNT, packedchars, (byte) 1, (byte) 1));
|
||||
packRanges.flip();
|
||||
}
|
||||
|
||||
try (STBTTPackContext pc = STBTTPackContext.malloc()) {
|
||||
stbtt_PackBegin(pc, bitmap, texwidth, texheight, 0, 1, NULL);
|
||||
stbtt_PackSetOversampling(pc, 1, 1);
|
||||
stbtt_PackSetSkipMissingCodepoints(pc, true);
|
||||
stbtt_PackFontRanges(pc, buf, 0, packRanges);
|
||||
stbtt_PackEnd(pc);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, texwidth, texheight, 0, GL_RED, GL_UNSIGNED_BYTE, bitmap);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
}
|
||||
}
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
try (var q = STBTTAlignedQuad.malloc()) {
|
||||
float[] x = new float[1];
|
||||
float[] y = new float[1];
|
||||
glyphs = new Glyph[GLYPH_COUNT];
|
||||
|
||||
for (int i = 0; i < GLYPH_COUNT; i++) {
|
||||
x[0] = 0f;
|
||||
y[0] = fontSize;
|
||||
stbtt_GetPackedQuad(packedchars, texwidth, texheight, i, x, y, q, true);
|
||||
glyphs[i] = new Glyph((char) (i + 32), (int) (x[0] - 0f), new int[]{(int) q.x0(), (int) q.y0(), (int) q.x1(), (int) q.y1()}, new float[]{q.s0(), q.t0(), q.s1(), q.t1()});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int lineSpacing() {
|
||||
return lineSpacing;
|
||||
}
|
||||
|
||||
int textureNumber() {
|
||||
return textureNumber;
|
||||
}
|
||||
|
||||
int descent() {
|
||||
return descent;
|
||||
}
|
||||
|
||||
public int stringWidth(String text) {
|
||||
var bytes = text.getBytes(StandardCharsets.US_ASCII);
|
||||
int len = 0;
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
final byte c = bytes[i];
|
||||
len += switch (c) {
|
||||
case '\n', '\t' -> 0;
|
||||
case ' ' -> glyphs[0].charwidth();
|
||||
default -> {
|
||||
if (c - 32 < this.GLYPH_COUNT && c > 32) {
|
||||
yield this.glyphs[c - 32].charwidth();
|
||||
} else {
|
||||
yield 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return len;
|
||||
}
|
||||
private record Pos(int x, int y, int minx) {}
|
||||
|
||||
/**
|
||||
* A piece of text to display
|
||||
*
|
||||
* @param string The text
|
||||
* @param colour The colour of the text as an RGBA packed int
|
||||
*/
|
||||
public record DisplayText(String string, int colour) {
|
||||
private byte[] asBytes() {
|
||||
return string.getBytes(StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
Pos generateStringArray(SimpleFont font, Pos pos, SimpleBufferBuilder bb) {
|
||||
for (int i = 0; i < asBytes().length; i++) {
|
||||
byte c = asBytes()[i];
|
||||
pos = switch (c) {
|
||||
case '\n' -> new Pos(pos.minx(), pos.y()+font.lineSpacing(), pos.minx());
|
||||
case '\t' -> new Pos(pos.x()+font.glyphs[0].charwidth() * 4, pos.y(), pos.minx());
|
||||
case ' ' -> new Pos(pos.x()+font.glyphs[0].charwidth(), pos.y(), pos.minx());
|
||||
default -> {
|
||||
if (c-32 < font.GLYPH_COUNT && c > 32) {
|
||||
pos = font.glyphs[c - 32].loadQuad(pos, colour(), bb);
|
||||
}
|
||||
yield pos;
|
||||
}
|
||||
};
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate vertices for a set of display texts
|
||||
* @param x The starting screen x coordinate
|
||||
* @param y The starting screen y coordinate
|
||||
* @param texts Some {@link DisplayText} to display
|
||||
* @return a {@link SimpleBufferBuilder} that can draw the texts
|
||||
*/
|
||||
public SimpleBufferBuilder generateVerticesForTexts(int x, int y, SimpleBufferBuilder textBB, DisplayText... texts) {
|
||||
var pos = new Pos(x, y, x);
|
||||
for (DisplayText text : texts) {
|
||||
pos = text.generateStringArray(this, pos, textBB);
|
||||
}
|
||||
return textBB;
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
net.minecraftforge.fml.earlydisplay.DisplayWindow
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,13 @@
|
||||
█▀▀▀▀▀█ ▀▄▄▄▀█ █ █▀▀▀▀▀█
|
||||
█ ███ █ ███ ▀███▀ █ ███ █
|
||||
█ ▀▀▀ █ ▀ ▄ ███▀█ █ ▀▀▀ █
|
||||
▀▀▀▀▀▀▀ ▀ █ ▀ ▀▄█ ▀▀▀▀▀▀▀
|
||||
█▀█▀▄▄▀▄▀ █▄▀▄ ▀▀▀ ▄▀▀▀▄▀
|
||||
▄█▄▄ ▀▀███▀██▄ █▀ ▀▄▄
|
||||
▀█▄▀ ▀▀▄▄▀▀ █▀█▄▄████ ▀▀█
|
||||
▄▀▀▄▀ ▀▄▀▄█ ▀ ▀▀▀▄█ ▀▀▄
|
||||
▀ ▀▀ ▄ ▄██▀ ▄█▀▀▀█▀█▀█
|
||||
█▀▀▀▀▀█ ▄█▄▄▀▀▄█ ▀ █ ▀▀▀
|
||||
█ ███ █ ▄▄ ▄ █ ▀██▀██▄██
|
||||
█ ▀▀▀ █ ██▄███▀█ █ █ █▀
|
||||
▀▀▀▀▀▀▀ ▀▀ ▀▀▀ ▀▀▀▀▀▀▀▀
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
+86
-68
@@ -1,88 +1,106 @@
|
||||
plugins {
|
||||
id 'com.github.ben-manes.versions'
|
||||
id 'org.javamodularity.moduleplugin' version '1.8.3' apply false
|
||||
id 'org.cadixdev.licenser'
|
||||
}
|
||||
apply plugin: 'java-library'
|
||||
apply plugin: 'jacoco'
|
||||
apply plugin: 'org.javamodularity.moduleplugin'
|
||||
import net.minecraftforge.gradleutils.PomUtils
|
||||
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'maven-publish'
|
||||
id 'net.minecraftforge.licenser'
|
||||
id 'net.minecraftforge.gradleutils'
|
||||
}
|
||||
|
||||
apply from: rootProject.file('build_shared.gradle')
|
||||
|
||||
dependencyUpdates.rejectVersionIf { isNonStable(it.candidate.version) }
|
||||
configurations.forEach{ it.transitive = false }
|
||||
|
||||
dependencies {
|
||||
implementation('net.sf.jopt-simple:jopt-simple:5.0.4')
|
||||
compileOnly('org.jetbrains:annotations:23.0.0')
|
||||
api("org.ow2.asm:asm:${ASM_VERSION}")
|
||||
api("org.ow2.asm:asm-tree:${ASM_VERSION}")
|
||||
api("org.ow2.asm:asm-commons:${ASM_VERSION}")
|
||||
api('net.minecraftforge:forgespi:4.0.+')
|
||||
api('org.apache.logging.log4j:log4j-api:2.17.0')
|
||||
api('org.slf4j:slf4j-api:1.8.0-beta4')
|
||||
api('com.google.guava:guava:21.0')
|
||||
api('com.google.code.gson:gson:2.8.9')
|
||||
api('org.apache.maven:maven-artifact:3.8.1')
|
||||
api('org.apache.commons:commons-lang3:3.12.0')
|
||||
api('com.electronwill.night-config:core:3.6.4')
|
||||
api('com.electronwill.night-config:toml:3.6.4')
|
||||
api('cpw.mods:modlauncher:9.0.+')
|
||||
api('net.minecraftforge:coremods:5.0.+')
|
||||
implementation("cpw.mods:securejarhandler:${SECUREJARHANDLER_VERSION}")
|
||||
implementation('org.apache.logging.log4j:log4j-core:2.17.0')
|
||||
annotationProcessor('org.apache.logging.log4j:log4j-core:2.17.0')
|
||||
implementation('net.minecraftforge:accesstransformers:8.0.4')
|
||||
implementation('net.minecrell:terminalconsoleappender:1.2.0')
|
||||
// implementation('org.jline:jline:3.12.+')
|
||||
testCompileOnly('org.jetbrains:annotations:21.0.0')
|
||||
testRuntimeOnly("cpw.mods:bootstraplauncher:${BOOTSTRAPLAUNCHER_VERSION}")
|
||||
compileOnly(libs.nulls)
|
||||
|
||||
api(libs.bundles.asm) // Needed by all the black magic
|
||||
api(libs.forgespi)
|
||||
api(libs.mergetool.api)
|
||||
api(libs.log4j.api)
|
||||
api(libs.slf4j.api)
|
||||
api(libs.guava)
|
||||
api(libs.gson)
|
||||
api(libs.maven.artifact)
|
||||
api(libs.apache.commons)
|
||||
api(libs.bundles.night.config)
|
||||
api(libs.modlauncher)
|
||||
api(libs.coremods)
|
||||
api(libs.mojang.logging)
|
||||
api(libs.jarjar.selector)
|
||||
api(libs.jarjar.meta)
|
||||
|
||||
implementation(libs.jopt.simple)
|
||||
implementation(libs.securemodules)
|
||||
implementation(libs.accesstransformers)
|
||||
implementation(libs.terminalconsoleappender)
|
||||
implementation(libs.jimfs)
|
||||
|
||||
implementation(libs.log4j.core)
|
||||
annotationProcessor(libs.bundles.log4j)
|
||||
|
||||
testCompileOnly(libs.nulls)
|
||||
testRuntimeOnly(libs.bootstrap)
|
||||
}
|
||||
|
||||
task sourcesJar(type: Jar) {
|
||||
archiveClassifier = 'sources'
|
||||
from sourceSets.main.allSource
|
||||
java {
|
||||
toolchain.languageVersion = JavaLanguageVersion.of(JAVA_VERSION)
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
ext {
|
||||
MANIFESTS = [
|
||||
'/': [
|
||||
'Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"),
|
||||
'Git-Commit': GIT_INFO.abbreviatedId,
|
||||
'Git-Branch': GIT_INFO.branch,
|
||||
'Build-Number': "${System.getenv('BUILD_NUMBER')?:0}",
|
||||
] as LinkedHashMap,
|
||||
'net/minecraftforge/fml/loading/': [
|
||||
'Specification-Title': 'FMLLoader',
|
||||
'Specification-Vendor': 'Forge Development LLC',
|
||||
'Specification-Version': '1',
|
||||
'Implementation-Title': 'FML Loader',
|
||||
'Implementation-Version': '1.0',
|
||||
'Implementation-Vendor': 'Forge'
|
||||
] as LinkedHashMap
|
||||
]
|
||||
}
|
||||
|
||||
jar.doFirst {
|
||||
MANIFESTS.each { pkg, values ->
|
||||
manifest.attributes(values, pkg)
|
||||
tasks.named('jar', Jar).configure {
|
||||
manifest {
|
||||
attributes([
|
||||
'Automatic-Module-Name': 'net.minecraftforge.fmlloader',
|
||||
'Forge-Module-Layer': 'boot'
|
||||
] as LinkedHashMap)
|
||||
attributes([
|
||||
'Specification-Title': 'FMLLoader',
|
||||
'Specification-Vendor': 'Forge Development LLC',
|
||||
'Specification-Version': '1',
|
||||
'Implementation-Title': 'FMLLoader',
|
||||
'Implementation-Vendor': 'Forge Development LLC',
|
||||
'Implementation-Version': FORGE_VERSION
|
||||
] as LinkedHashMap, 'net/minecraftforge/fml/loading/')
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.compilerArgs << '-Xlint:unchecked'
|
||||
}
|
||||
|
||||
license {
|
||||
header = rootProject.file('LICENSE-header.txt')
|
||||
|
||||
include 'net/minecraftforge/'
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives jar
|
||||
archives sourcesJar
|
||||
publishing {
|
||||
publications.register('mavenJava', MavenPublication).configure {
|
||||
from components.java
|
||||
artifactId = 'fmlloader'
|
||||
pom {
|
||||
name = project.name
|
||||
description = 'Modifactions to Minecraft to enable mod developers.'
|
||||
url = 'https://github.com/MinecraftForge/MinecraftForge'
|
||||
PomUtils.setGitHubDetails(pom, 'MinecraftForge')
|
||||
license PomUtils.Licenses.LGPLv2_1
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven gradleutils.publishingForgeMaven
|
||||
}
|
||||
}
|
||||
|
||||
publishing.publications.mavenJava(MavenPublication) {
|
||||
from components.java
|
||||
artifact sourcesJar
|
||||
}
|
||||
tasks.register('writeForgeVersionJson') {
|
||||
doLast {
|
||||
file('src/main/resources/forge_version.json').json = [
|
||||
forge: FORGE_VERSION,
|
||||
mc: MC_VERSION,
|
||||
mcp: MCP_VERSION
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('generateResources').configure {
|
||||
dependsOn('writeForgeVersionJson')
|
||||
}
|
||||
+16
-30
@@ -36,9 +36,9 @@ import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
|
||||
*/
|
||||
public class CapabilityTokenSubclass implements ILaunchPluginService {
|
||||
|
||||
private final String FUNC_NAME = "getType";
|
||||
private final String FUNC_DESC = "()Ljava/lang/String;";
|
||||
private final String CAP_INJECT = "net/minecraftforge/common/capabilities/CapabilityToken"; //Don't directly reference this to prevent class loading.
|
||||
private static final String FUNC_NAME = "getType";
|
||||
private static final String FUNC_DESC = "()Ljava/lang/String;";
|
||||
private static final String CAP_INJECT = "net/minecraftforge/common/capabilities/CapabilityToken"; //Don't directly reference this to prevent class loading.
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
@@ -49,49 +49,38 @@ public class CapabilityTokenSubclass implements ILaunchPluginService {
|
||||
private static final EnumSet<Phase> NAY = EnumSet.noneOf(Phase.class);
|
||||
|
||||
@Override
|
||||
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty)
|
||||
{
|
||||
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty) {
|
||||
return isEmpty ? NAY : YAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int processClassWithFlags(final Phase phase, final ClassNode classNode, final Type classType, final String reason)
|
||||
{
|
||||
if (CAP_INJECT.equals(classNode.name))
|
||||
{
|
||||
for (MethodNode mtd : classNode.methods)
|
||||
{
|
||||
if (FUNC_NAME.equals(mtd.name) && FUNC_DESC.equals(mtd.desc))
|
||||
{
|
||||
public int processClassWithFlags(final Phase phase, final ClassNode classNode, final Type classType, final String reason) {
|
||||
if (CAP_INJECT.equals(classNode.name)) {
|
||||
for (MethodNode mtd : classNode.methods) {
|
||||
if (FUNC_NAME.equals(mtd.name) && FUNC_DESC.equals(mtd.desc)) {
|
||||
mtd.access &= ~Opcodes.ACC_FINAL; // We have it final in code so people don't override it, cuz that'd be stupid, and make our transformer more complicated.
|
||||
}
|
||||
}
|
||||
return ComputeFlags.SIMPLE_REWRITE;
|
||||
}
|
||||
else if (CAP_INJECT.equals(classNode.superName))
|
||||
{
|
||||
} else if (CAP_INJECT.equals(classNode.superName)) {
|
||||
Holder cls = new Holder();
|
||||
|
||||
SignatureReader reader = new SignatureReader(classNode.signature); // Having a node version of this would probably be useful.
|
||||
reader.accept(new SignatureVisitor(Opcodes.ASM9)
|
||||
{
|
||||
Deque<String> stack = new ArrayDeque<>();
|
||||
reader.accept(new SignatureVisitor(Opcodes.ASM9) {
|
||||
final Deque<String> stack = new ArrayDeque<>();
|
||||
|
||||
@Override
|
||||
public void visitClassType(final String name)
|
||||
{
|
||||
public void visitClassType(final String name) {
|
||||
stack.push(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitInnerClassType(final String name)
|
||||
{
|
||||
public void visitInnerClassType(final String name) {
|
||||
stack.push(stack.pop() + '$' + name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitEnd()
|
||||
{
|
||||
public void visitEnd() {
|
||||
var val = stack.pop();
|
||||
if (!stack.isEmpty() && CAP_INJECT.equals(stack.peek()))
|
||||
cls.value = val;
|
||||
@@ -106,15 +95,12 @@ public class CapabilityTokenSubclass implements ILaunchPluginService {
|
||||
mtd.visitInsn(Opcodes.ARETURN);
|
||||
mtd.visitEnd();
|
||||
return ComputeFlags.COMPUTE_MAXS;
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
return ComputeFlags.NO_REWRITE;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Holder {
|
||||
private static final class Holder {
|
||||
String value;
|
||||
}
|
||||
|
||||
}
|
||||
Loaded 100 of 3942 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user