mirror of
https://github.com/MinecraftForge/MinecraftForge.git
synced 2026-11-14 01:04:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ec8685098 | ||
|
|
f375683bcd | ||
|
|
9cd61b99f1 | ||
|
|
ad5ca0b67d | ||
|
|
b76746e62f | ||
|
|
4cfa1f83c4 | ||
|
|
da7e5f8bfa | ||
|
|
cd8eccc29c | ||
|
|
7d0bd1898f | ||
|
|
55f03a2d86 | ||
|
|
c328912074 | ||
|
|
aa4e31beab | ||
|
|
4249a25ec7 | ||
|
|
939fb13d3b | ||
|
|
932c6e9649 | ||
|
|
ed6d9ff0e4 | ||
|
|
851a57e808 | ||
|
|
49aac2c496 | ||
|
|
26eeb1cc70 | ||
|
|
5afe3c050c | ||
|
|
839fd66e52 | ||
|
|
fea644f6d2 | ||
|
|
2bc9649bd6 | ||
|
|
41dfdc8f8b | ||
|
|
4dae990880 | ||
|
|
f0f144156b | ||
|
|
d17cfd0b4b | ||
|
|
d697be351f | ||
|
|
703195cd36 | ||
|
|
3f1e9a90c9 | ||
|
|
da353a52fe | ||
|
|
3bac35089f | ||
|
|
cb2e205867 | ||
|
|
c5d015e60a | ||
|
|
72be0c9ccd | ||
|
|
dabe919492 | ||
|
|
3da1b644d0 | ||
|
|
d3476f3f80 | ||
|
|
6665cdcf4e | ||
|
|
d9665f6447 | ||
|
|
ec73fd5240 | ||
|
|
3bbdbacb81 | ||
|
|
478d8b3f18 | ||
|
|
03f928a3de | ||
|
|
4834f16aef | ||
|
|
7846d25606 | ||
|
|
77cb0c5c4e | ||
|
|
5e421e8b8b | ||
|
|
5f37e8af53 | ||
|
|
5ab792f1d7 | ||
|
|
38ebbf189c | ||
|
|
f247eebd55 | ||
|
|
555989cf10 | ||
|
|
49fe93b1b5 | ||
|
|
48167e93b4 | ||
|
|
2bd132a6f8 | ||
|
|
b8168a12df | ||
|
|
df271e0265 | ||
|
|
d7687a72d5 | ||
|
|
047e5fe252 | ||
|
|
5ac2675f87 | ||
|
|
b544849f7d | ||
|
|
39683f28dd | ||
|
|
da4fd10ee8 | ||
|
|
e1c2add370 | ||
|
|
c71ba862cf | ||
|
|
3c057d277b | ||
|
|
b99abff6ce | ||
|
|
558e7d754e | ||
|
|
d29524bac3 | ||
|
|
929064ee69 | ||
|
|
b4024a7884 | ||
|
|
07f6499143 | ||
|
|
236842f210 | ||
|
|
ef811aebe8 | ||
|
|
d61de63482 | ||
|
|
8368a1d20a | ||
|
|
8b8d3fdef7 | ||
|
|
f5d4fbbee5 | ||
|
|
6f4f60e213 | ||
|
|
01bfc33b28 | ||
|
|
17e1002e86 | ||
|
|
da1b8f4efa | ||
|
|
42bbe44a34 | ||
|
|
f095474adf | ||
|
|
1b5ca64f20 | ||
|
|
b518c804fb | ||
|
|
e50a0ffc6c | ||
|
|
7ea0a41fe2 | ||
|
|
f3d16621c0 | ||
|
|
aa55a6a831 | ||
|
|
3449217c65 | ||
|
|
41609a03d2 | ||
|
|
9b8e5c7109 | ||
|
|
f05ad3afd0 | ||
|
|
6a615f1a4b | ||
|
|
4376cd6b85 | ||
|
|
d17a111dc0 | ||
|
|
1b99b2cea4 | ||
|
|
4ef6695a30 | ||
|
|
0e6341a23e | ||
|
|
17ce723edf | ||
|
|
fc80e30289 | ||
|
|
ecd1491f96 | ||
|
|
8f7fbc76e1 | ||
|
|
ec405958c9 | ||
|
|
e2ff990601 | ||
|
|
8443262e39 | ||
|
|
8e09d47cd6 | ||
|
|
0404d2d776 | ||
|
|
aea804cb6e | ||
|
|
0bffcb83c5 | ||
|
|
9a022d1451 | ||
|
|
58aa3b276c | ||
|
|
b0ccf1a480 | ||
|
|
c1d703e395 | ||
|
|
728fd6ff24 | ||
|
|
0a885a77e7 | ||
|
|
15dbadaae6 | ||
|
|
4c1c3aafdb | ||
|
|
f06727a205 | ||
|
|
ef0e7bc17f | ||
|
|
ec7a098f81 | ||
|
|
ff25961a05 | ||
|
|
c999a2d4eb | ||
|
|
ce79df9716 | ||
|
|
eae9b9145d | ||
|
|
7f56986027 | ||
|
|
c6874bce67 | ||
|
|
aa11175f41 | ||
|
|
2730559ff3 | ||
|
|
e80c3402b7 | ||
|
|
837c259668 | ||
|
|
2e48910b4c | ||
|
|
70dd556db6 | ||
|
|
e44a558f37 | ||
|
|
1e154cbe3e | ||
|
|
d5c5df1b64 | ||
|
|
5e75de5eb7 | ||
|
|
ddb1e52e99 | ||
|
|
9e55363e2c | ||
|
|
219abfed22 | ||
|
|
6915753b5a | ||
|
|
1527738ddc | ||
|
|
18c806f1d0 | ||
|
|
47f7c68ed5 | ||
|
|
5e8942e0a7 | ||
|
|
a02f3f9841 | ||
|
|
c5e462f7d2 | ||
|
|
d1f79d6655 | ||
|
|
9d5e81b1a9 | ||
|
|
a1c275df46 | ||
|
|
4ef7fe8bd8 | ||
|
|
ce8838e721 | ||
|
|
b7e2593d48 | ||
|
|
33d177f02e | ||
|
|
5d2186df63 | ||
|
|
77af36b807 | ||
|
|
54eeca0aed | ||
|
|
3de0566425 | ||
|
|
a5ebf3b645 | ||
|
|
63d0496cc4 | ||
|
|
31f623e794 | ||
|
|
eb001a6382 | ||
|
|
504dcad849 | ||
|
|
81f7f029c3 | ||
|
|
207c1997c9 | ||
|
|
e348ae5b84 | ||
|
|
0f3b8ee830 | ||
|
|
b4b6e48a7a | ||
|
|
d50a3b5ab7 | ||
|
|
88904a1d09 | ||
|
|
b864faf0ef | ||
|
|
c97eabc0c4 | ||
|
|
6ae39c8597 | ||
|
|
dcafb4f633 | ||
|
|
1ea7ce9831 | ||
|
|
674f2efd54 | ||
|
|
02e82c672d | ||
|
|
b25f6e3a7c | ||
|
|
e69b07a54a | ||
|
|
79efe2ac36 | ||
|
|
464483db5b | ||
|
|
f49adb285f | ||
|
|
55862dfd1d | ||
|
|
124b9bd8d3 | ||
|
|
bae3915728 | ||
|
|
b3c2c62c7e | ||
|
|
68698d70c1 | ||
|
|
d55dee9394 | ||
|
|
2d114f2719 | ||
|
|
3e6583af01 | ||
|
|
762f6a7d78 | ||
|
|
3c5a534b7a | ||
|
|
bc62d1693a | ||
|
|
554ead8d4a | ||
|
|
ec5ac91389 | ||
|
|
f63813d929 | ||
|
|
5d4825ac4b | ||
|
|
3024ef7707 | ||
|
|
0d3b488728 | ||
|
|
a01012f20c | ||
|
|
d4e3ef4a22 | ||
|
|
d7293a78dc | ||
|
|
8e4e02e258 | ||
|
|
2f5a6fee48 | ||
|
|
0e74886f8d | ||
|
|
07977ba242 | ||
|
|
a894c244fe | ||
|
|
cf32a799a4 | ||
|
|
0fda1d9393 | ||
|
|
c7f0acbe38 | ||
|
|
9c06c20023 | ||
|
|
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 |
No files matched your search
@@ -3,6 +3,7 @@
|
||||
*.patch text eol=lf
|
||||
*.java text eol=lf
|
||||
*.gradle text eol=crlf
|
||||
**/MANIFEST.MF text eol=crlf
|
||||
*.png binary
|
||||
*.gif binary
|
||||
*.exe binary
|
||||
@@ -21,3 +22,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
|
||||
|
||||
@@ -10,4 +10,4 @@ assignees: ''
|
||||
<!-- Please note, feature requests that are submitted as GitHub issues here are rarely implemented. -->
|
||||
<!-- If you'd like to see something in Forge, you should be ready to implement it as a PR. -->
|
||||
<!-- If you're not sure, open a Draft PR or discuss it in the Discord server. -->
|
||||
<!-- Discord: https://discord.gg/UvedJ9m -->
|
||||
<!-- Discord: https://discord..minecraftforge.net/ -->
|
||||
@@ -11,6 +11,7 @@ assignees: ''
|
||||
|
||||
<!-- For support with crashes and issues, please create a post on the forum instead: -->
|
||||
<!-- https://www.minecraftforge.net/forum/forum/18-support-bug-reports/ -->
|
||||
<!-- You can also use the discord for support, https://discord.minecraftforge.net/ -->
|
||||
|
||||
**Minecraft Version:** {Minecraft version}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ '26.3' ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: MinecraftForge/SharedActions/.github/workflows/gradle.yml@main
|
||||
with:
|
||||
java: 21
|
||||
gradle_setup: './gradlew setup'
|
||||
gradle_tasks: 'build publish'
|
||||
artifact_name: 'forge'
|
||||
secrets:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
PROMOTE_ARTIFACT_WEBHOOK: ${{ secrets.PROMOTE_ARTIFACT_WEBHOOK }}
|
||||
PROMOTE_ARTIFACT_USERNAME: ${{ secrets.PROMOTE_ARTIFACT_USERNAME }}
|
||||
PROMOTE_ARTIFACT_PASSWORD: ${{ secrets.PROMOTE_ARTIFACT_PASSWORD }}
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
SIGN_KEY_ALIAS: ${{ secrets.SIGN_KEY_ALIAS }}
|
||||
SIGN_KEY_PASSWORD: ${{ secrets.SIGN_KEY_PASSWORD }}
|
||||
SIGN_KEYSTORE_PASSWORD: ${{ secrets.SIGN_KEYSTORE_PASSWORD }}
|
||||
SIGN_KEYSTORE_DATA: ${{ secrets.SIGN_KEYSTORE_DATA }}
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
name: Upload payload
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/upload-artifact@v3 # Payload artifact is consumed in Actionable
|
||||
- uses: actions/upload-artifact@v4 # Payload artifact is consumed in Actionable
|
||||
with:
|
||||
name: payload
|
||||
path: ${{ github.event_path }}
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
name: Upload payload
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/upload-artifact@v3 # Payload artifact is consumed in Actionable
|
||||
- uses: actions/upload-artifact@v4 # Payload artifact is consumed in Actionable
|
||||
with:
|
||||
name: payload
|
||||
path: ${{ github.event_path }}
|
||||
+32
-21
@@ -1,3 +1,6 @@
|
||||
#forgedev dev if the folder exists in here
|
||||
forgedev
|
||||
|
||||
#eclipse
|
||||
**/bin
|
||||
**/.settings
|
||||
@@ -8,41 +11,49 @@
|
||||
/eclipse/
|
||||
|
||||
#idea
|
||||
/.idea
|
||||
**/.idea
|
||||
/*/out
|
||||
/classes
|
||||
*.iml
|
||||
|
||||
# We however WANT the ide folder as it has tempaltes/configs for developing Forge.
|
||||
!ide/**
|
||||
|
||||
#gradle
|
||||
/build
|
||||
/.gradle
|
||||
/*/build
|
||||
/*/.gradle
|
||||
|
||||
# Minecraft
|
||||
/run
|
||||
/*.launch
|
||||
|
||||
# Projects repo, either ignore it, or ignore patches.
|
||||
/projects/mcp/
|
||||
/projects/clean/
|
||||
/projects/forge/
|
||||
/projects/fmlonly/
|
||||
#/patches/*
|
||||
src/minecraft
|
||||
|
||||
#occupational hazards
|
||||
/projects/**/build/
|
||||
/projects/**/out/
|
||||
/projects/**/run/
|
||||
/projects/**/*.launch
|
||||
/*/runs/
|
||||
/repo/
|
||||
src/generated/resources/.cache/
|
||||
src/generated_test/resources/.cache/
|
||||
/rejects-*/
|
||||
**/src/*/generated/**/.cache/
|
||||
**/*.launch
|
||||
**/.project_info.properties
|
||||
|
||||
#Patch rejects
|
||||
/patches-/
|
||||
*.patch.rej
|
||||
/projects/*/rejects/
|
||||
# Generated by gradle every import
|
||||
**/src/main/resources/META-INF/MANIFEST.MF
|
||||
/fmlloader/src/main/resources/forge_version.json
|
||||
|
||||
# FML changelog
|
||||
changelog.txt
|
||||
*.py
|
||||
*.7z
|
||||
/logs/
|
||||
/ForgeRoot.ipr
|
||||
/ForgeRoot.iws
|
||||
|
||||
# APT stuff
|
||||
/*/.gitignore
|
||||
/*/.factorypath
|
||||
/*/.apt_generated/
|
||||
/fmlcore/logs/
|
||||
/lib/
|
||||
/_actual/
|
||||
/runs/
|
||||
/forge.ipr
|
||||
/forge.iws
|
||||
Vendored
-104
@@ -1,104 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<project>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<name>MinecraftForge_Forge Config DSL Script</name>
|
||||
<groupId>MinecraftForge_Forge</groupId>
|
||||
<artifactId>MinecraftForge_Forge_dsl</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<parent>
|
||||
<groupId>org.jetbrains.teamcity</groupId>
|
||||
<artifactId>configs-dsl-kotlin-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>jetbrains-all</id>
|
||||
<url>https://download.jetbrains.com/teamcity-repository</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>teamcity-server</id>
|
||||
<url>https://teamcity.minecraftforge.net/app/dsl-plugins-repository</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>JetBrains</id>
|
||||
<url>https://download.jetbrains.com/teamcity-repository</url>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>${basedir}</sourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<version>${kotlin.version}</version>
|
||||
|
||||
<configuration/>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>process-sources</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>process-test-sources</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.teamcity</groupId>
|
||||
<artifactId>teamcity-configs-maven-plugin</artifactId>
|
||||
<version>${teamcity.dsl.version}</version>
|
||||
<configuration>
|
||||
<format>kotlin</format>
|
||||
<dstDir>target/generated-configs</dstDir>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.teamcity</groupId>
|
||||
<artifactId>configs-dsl-kotlin</artifactId>
|
||||
<version>${teamcity.dsl.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.teamcity</groupId>
|
||||
<artifactId>configs-dsl-kotlin-plugins</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<type>pom</type>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-script-runtime</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
Vendored
-208
@@ -1,208 +0,0 @@
|
||||
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
|
||||
project hierarchy. The script should contain a single call to the
|
||||
project() function with a Project instance or an init function as
|
||||
an argument.
|
||||
|
||||
VcsRoots, BuildTypes, Templates, and subprojects can be
|
||||
registered inside the project using the vcsRoot(), buildType(),
|
||||
template(), and subProject() methods respectively.
|
||||
|
||||
To debug settings scripts in command-line, run the
|
||||
|
||||
mvnDebug org.jetbrains.teamcity:teamcity-configs-maven-plugin:generate
|
||||
|
||||
command and attach your debugger to the port 8000.
|
||||
|
||||
To debug in IntelliJ Idea, open the 'Maven Projects' tool window (View
|
||||
-> Tool Windows -> Maven Projects), find the generate task node
|
||||
(Plugins -> teamcity-configs -> teamcity-configs:generate), the
|
||||
'Debug' option is available in the context menu for the task.
|
||||
*/
|
||||
|
||||
version = "2021.2"
|
||||
|
||||
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.5.1", 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.19.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*)
|
||||
+:refs/heads/(develop|release|staging|main|master)
|
||||
+:refs/heads/(1.*)
|
||||
""".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)
|
||||
password("env.KEYSTORE_URL", "credentialsJSON:a7ae1c82-8058-4061-8d12-7f6bc2618d2e", display = ParameterDisplay.HIDDEN)
|
||||
password("env.KEYSTORE_PASSWORD", "credentialsJSON:d7b964e3-a1fd-47a8-b892-6f601fe47479", display = ParameterDisplay.HIDDEN)
|
||||
text("additional_publishing_gradle_parameters", "-PcrowdinKey=%env.CROWDIN_KEY% -PkeystoreKeyPass=%env.KEYSTORE_PASSWORD% -PkeystoreStorePass=%env.KEYSTORE_PASSWORD% -Pkeystore=%env.KEYSTORE_URL%", label = "Additional gradle parameters for publish", description = "Contains the additional gradle parameters used during publishing.", display = ParameterDisplay.HIDDEN, allowEmpty = true)
|
||||
|
||||
checkbox("should_execute_build", "false", label = "Should build", description = "Indicates if the build task should be executed.", display = ParameterDisplay.HIDDEN,
|
||||
checked = "true", unchecked = "false")
|
||||
checkbox("should_execute_test", "false", label = "Should build", description = "Indicates if the build task should be executed.", display = ParameterDisplay.HIDDEN,
|
||||
checked = "true", unchecked = "false")
|
||||
}
|
||||
|
||||
features {
|
||||
githubIssues {
|
||||
id = "MinecraftForge_MinecraftForge__IssueTracker"
|
||||
displayName = "MinecraftForge/MinecraftForge"
|
||||
repositoryURL = "https://github.com/MinecraftForge/MinecraftForge"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Build : BuildType({
|
||||
templates(AbsoluteId("MinecraftForge_SetupGradleUtilsCiEnvironmen"), AbsoluteId("MinecraftForge_BuildWithDiscordNotifications"), AbsoluteId("MinecraftForge_BuildMainBranches"), AbsoluteId("MinecraftForge_SetupProjectUsingGradle"), AbsoluteId("MinecraftForge_BuildUsingGradle"), AbsoluteId("MinecraftForge_PublishProjectUsingGradle"), AbsoluteId("MinecraftForge_TriggersStaticFilesWebpageGenerator"))
|
||||
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({
|
||||
templates(AbsoluteId("MinecraftForge_ExcludesBuildingDefaultBranch"), AbsoluteId("MinecraftForge_SetupGradleUtilsCiEnvironmen"), AbsoluteId("MinecraftForge_BuildWithDiscordNotifications"), AbsoluteId("MinecraftForge_BuildMainBranches"), AbsoluteId("MinecraftForge_SetupProjectUsingGradle"), AbsoluteId("MinecraftForge_BuildUsingGradle"))
|
||||
id("MinecraftForge_MinecraftForge__BuildSecondaryBranches")
|
||||
name = "Build - Secondary Branches"
|
||||
description = "Builds and Publishes the secondary branches of the project."
|
||||
params {
|
||||
text("git_branch_spec", """
|
||||
+:refs/heads/(*)
|
||||
-:refs/heads/noci*
|
||||
""".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)
|
||||
}
|
||||
vcs {
|
||||
branchFilter = """
|
||||
+:*
|
||||
-:1.*
|
||||
-:<default>
|
||||
""".trimIndent()
|
||||
}
|
||||
})
|
||||
|
||||
object PullRequests : BuildType({
|
||||
templates(AbsoluteId("MinecraftForge_BuildPullRequests"), AbsoluteId("MinecraftForge_SetupGradleUtilsCiEnvironmen"), AbsoluteId("MinecraftForge_BuildWithDiscordNotifications"), AbsoluteId("MinecraftForge_SetupProjectUsingGradle"), AbsoluteId("MinecraftForge_BuildUsingGradle"))
|
||||
id("MinecraftForge_MinecraftForge__PullRequests")
|
||||
name = "Pull Requests"
|
||||
description = "Builds pull requests for the project"
|
||||
|
||||
params {
|
||||
checkbox("should_execute_build", "true", label = "Should build", description = "Indicates if the build task should be executed.", display = ParameterDisplay.HIDDEN,
|
||||
checked = "true", unchecked = "false")
|
||||
text(
|
||||
"gradle_build_task",
|
||||
"assemble",
|
||||
label = "Gradle build task to execute during build",
|
||||
description = "Determines the build task that is executed to build the project.",
|
||||
display = ParameterDisplay.HIDDEN,
|
||||
allowEmpty = false
|
||||
)
|
||||
}
|
||||
|
||||
vcs {
|
||||
branchFilter = """
|
||||
+:*
|
||||
-:1.*
|
||||
-:<default>
|
||||
""".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()
|
||||
}
|
||||
})
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
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.
|
||||
|
||||
First, the people who originally created the Forge projects way back in Minecraft
|
||||
alpha. Eloraam of RedPower, and SpaceToad of Buildcraft, without their acceptiance
|
||||
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
|
||||
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
|
||||
sane as well as guiding the community into creating better additions to Forge.
|
||||
|
||||
Searge, Bspks, Fesh0r, ProfMobious, and all the rest over on the MCP team {of which
|
||||
I am a part}. For creating some of the core tools needed to make Minecraft modding
|
||||
both possible, and as stable as can be.
|
||||
On that note, here is some specific information of the MCP data we use:
|
||||
* Minecraft Coder Pack (MCP) *
|
||||
Forge Mod Loader and Minecraft Forge have permission to distribute and automatically
|
||||
download components of MCP and distribute MCP data files. This permission is not
|
||||
transitive and others wishing to redistribute the Minecraft Forge source independently
|
||||
should seek permission of MCP or remove the MCP data files and request their users
|
||||
to download MCP separately.
|
||||
|
||||
And lastly, the countless community members who have spent time submitting bug reports,
|
||||
pull requests, and just helping out the community in general. Thank you.
|
||||
|
||||
--LexManos
|
||||
|
||||
=========================================================================
|
||||
|
||||
This is Forge Mod Loader.
|
||||
|
||||
You can find the source code at all times at https://github.com/MinecraftForge/MinecraftForge/tree/1.12.x/src/main/java/net/minecraftforge/fml
|
||||
|
||||
This minecraft mod is a clean open source implementation of a mod loader for minecraft servers
|
||||
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-
|
||||
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/
|
||||
|
||||
Additionally, it contains an implementation of topological sort based on that
|
||||
published at http://keithschwarz.com/interesting/code/?dir=topological-sort
|
||||
|
||||
It also contains code from the Maven project for performing versioned dependency
|
||||
resolution. http://maven.apache.org/
|
||||
|
||||
It also contains a partial repackaging of the javaxdelta library from http://sourceforge.net/projects/javaxdelta/
|
||||
with credit to it's authors.
|
||||
|
||||
Forge Mod Loader downloads components from the Minecraft Coder Pack
|
||||
(http://mcp.ocean-labs.de/index.php/Main_Page) with kind permission from the MCP team.
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -3,7 +3,7 @@ parts herein are licensed under the terms of the LGPL 2.1 found
|
||||
here http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt and
|
||||
copied below.
|
||||
|
||||
Homepage: http://minecraftforge.net/
|
||||
Homepage: https://minecraftforge.net/
|
||||
https://github.com/MinecraftForge/MinecraftForge
|
||||
|
||||
|
||||
@@ -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.
|
||||
=========================================================================
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
plugins {
|
||||
id("groovy-gradle-plugin")
|
||||
alias(libs.plugins.licenser)
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven { url = 'https://maven.minecraftforge.net/' }
|
||||
//mavenLocal()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly gradleApi()
|
||||
|
||||
// Manually import plugins so that we can apply them by id.
|
||||
implementation plugin('licenser')
|
||||
implementation plugin('gradleutils')
|
||||
implementation plugin('gitversion')
|
||||
implementation plugin('changelog')
|
||||
implementation plugin('forgedev')
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
register('convention') {
|
||||
id = 'net.minecraftforge.forge.build.convention'
|
||||
implementationClass = 'net.minecraftforge.forge.build.ConventionPlugin'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final Provider<String> plugin(String alias) {
|
||||
return (Provider<String>)project.extensions.getByType(VersionCatalogsExtension)
|
||||
.named("libs")
|
||||
.findPlugin(alias).get()
|
||||
.map { it.pluginId + ":" + it.pluginId + ".gradle.plugin:" + it.version }
|
||||
}
|
||||
|
||||
license {
|
||||
header = file('../LICENSE-header.txt')
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
Vendored
+251
@@ -0,0 +1,251 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,30 @@
|
||||
pluginManagement {
|
||||
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
maven { url = 'https://maven.minecraftforge.net/' }
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||
}
|
||||
|
||||
//enableFeaturePreview 'TYPESAFE_PROJECT_ACCESSORS'
|
||||
|
||||
dependencyResolutionManagement.versionCatalogs {
|
||||
register('bootLibs') {
|
||||
description = 'Important version numbers that are externally declared for easy modification.'
|
||||
from(files('../minecraft.versions.toml'))
|
||||
}
|
||||
|
||||
create('libs') {
|
||||
description = 'Libraries used by Forge and its subprojects.'
|
||||
from(files("../gradle/libs.versions.toml"))
|
||||
}
|
||||
}
|
||||
|
||||
if (new File('forgedev/settings.gradle').exists())
|
||||
includeBuild '../forgedev'
|
||||
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.forge.build
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import org.gradle.api.Action
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.Task
|
||||
import org.gradle.api.artifacts.ExternalModuleDependencyBundle
|
||||
import org.gradle.api.artifacts.MinimalExternalModuleDependency
|
||||
import org.gradle.api.model.ObjectFactory
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.gradle.api.provider.ProviderFactory
|
||||
import org.gradle.api.publish.maven.MavenPublication
|
||||
import org.gradle.api.tasks.TaskProvider
|
||||
import org.gradle.jvm.tasks.Jar
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
@CompileStatic
|
||||
abstract class ConventionExtension implements Meta {
|
||||
private final Project project;
|
||||
private final Runs runs
|
||||
|
||||
protected abstract @Inject ObjectFactory getObjects()
|
||||
protected abstract @Inject ProviderFactory getProviders()
|
||||
|
||||
@Inject
|
||||
ConventionExtension(Project project) {
|
||||
this.project = project
|
||||
this.runs = objects.newInstance(Runs, this)
|
||||
}
|
||||
|
||||
@Override
|
||||
Project getProject() {
|
||||
this.project
|
||||
}
|
||||
|
||||
void manifest(String pkg) {
|
||||
this.project.tasks.named('jar', Jar).configure {
|
||||
this.gradleutils.manifestDefaults(it.manifest, pkg)
|
||||
}
|
||||
}
|
||||
void manifest(String pkg, Map<String, String> info) {
|
||||
manifest(pkg)
|
||||
this.project.tasks.named('jar', Jar).configure {
|
||||
it.manifest.getAttributes().with {
|
||||
for (var entry : info.entrySet())
|
||||
put(entry.key, entry.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Maven publishing ==============================================================
|
||||
void publish() {
|
||||
publish(pub -> pub.from(project.components.getByName('java')))
|
||||
}
|
||||
void publish(Action<MavenPublication> config) {
|
||||
project.with {
|
||||
publishing.with {
|
||||
repositories.with {
|
||||
maven gradleutils.publishingForgeMaven
|
||||
}
|
||||
publications.register('mavenJava', MavenPublication) {
|
||||
changelog.publish(it)
|
||||
gradleutils.promote(it)
|
||||
|
||||
// The actual artifacts to be published should be configured, so run the config
|
||||
config.execute(it)
|
||||
|
||||
it.pom.with {
|
||||
description = project.description
|
||||
gradleutils.pom.addRemoteDetails(it)
|
||||
license.with {
|
||||
gradleutils.pom.licenses.LGPLv2_1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Runs ================================================================
|
||||
private TaskProvider<Task> genAllData = null
|
||||
TaskProvider<Task> getGenAllData() {
|
||||
if (genAllData == null)
|
||||
genAllData = this.project.tasks.register('genAllData')
|
||||
return genAllData
|
||||
}
|
||||
TaskProvider<Task> getGenAllData(Action<Task> config) {
|
||||
getGenAllData().configure(config)
|
||||
return getGenAllData()
|
||||
}
|
||||
|
||||
Runs getRuns() {
|
||||
return this.runs
|
||||
}
|
||||
Runs runs(Action<Runs> config) {
|
||||
config.execute(this.runs)
|
||||
return this.runs
|
||||
}
|
||||
Runs basicRuns() {
|
||||
return basicRuns({})
|
||||
}
|
||||
Runs basicRuns(Action<Runs> config) {
|
||||
this.runs.main(null, null)
|
||||
config.execute(this.runs)
|
||||
return getRuns()
|
||||
}
|
||||
Runs basicRuns(String mods, File output) {
|
||||
return basicRuns(mods, output, {})
|
||||
}
|
||||
Runs basicRuns(String mods, File output, Action<Runs> config) {
|
||||
this.runs.main(mods, output)
|
||||
config.execute(this.runs)
|
||||
return getRuns()
|
||||
}
|
||||
Runs testRuns(String mods, File output) {
|
||||
return testRuns(mods, output, {})
|
||||
}
|
||||
Runs testRuns(String mods, File output, Action<Runs> config) {
|
||||
this.runs.test(mods, output)
|
||||
config.execute(this.runs)
|
||||
return getRuns()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Forge Version Json ===================================================
|
||||
private TaskProvider<WriteForgeVersionJson> writeForgeVersionJson = null
|
||||
TaskProvider<WriteForgeVersionJson> getForgeVersionJson() {
|
||||
if (writeForgeVersionJson == null) {
|
||||
writeForgeVersionJson = this.project.tasks.register('writeForgeVersionJson', WriteForgeVersionJson)
|
||||
final versions = this.versions
|
||||
writeForgeVersionJson.configure {
|
||||
it.forgeVersion.set(versions.forge)
|
||||
it.minecraftVersion.set(versions.minecraft)
|
||||
it.mcpVersion.set(versions.mcp)
|
||||
}
|
||||
}
|
||||
return writeForgeVersionJson
|
||||
}
|
||||
TaskProvider<WriteForgeVersionJson> forgeVersionJson(Action<WriteForgeVersionJson> config) {
|
||||
forgeVersionJson.configure(config)
|
||||
return forgeVersionJson
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Natives helper =========================================================
|
||||
// See gradle/gradle#35070 (https://github.com/gradle/gradle/issues/35070)
|
||||
// We are making the list of providers first BEFORE mapping them to a provider of the resolved objects so
|
||||
// that they can be finalized after configuration.
|
||||
List<MinimalExternalModuleDependency> nativeVariants(Provider<ExternalModuleDependencyBundle> bundle) {
|
||||
final natives = "natives-" + forgedev.os
|
||||
final deps = this.project.dependencies
|
||||
bundle.get().collect { dep ->
|
||||
(deps.variantOf(providers.provider { (MinimalExternalModuleDependency)dep }) {
|
||||
it.classifier(natives)
|
||||
}).get()
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.forge.build
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.tasks.compile.JavaCompile
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
|
||||
@CompileStatic
|
||||
class ConventionPlugin implements Plugin<Project>, Meta {
|
||||
Project project
|
||||
@Override
|
||||
Project getProject() {
|
||||
this.project
|
||||
}
|
||||
|
||||
@Override
|
||||
void apply(Project project) {
|
||||
this.project = project
|
||||
project.with {
|
||||
pluginManager.with {
|
||||
apply 'java-library'
|
||||
apply 'maven-publish'
|
||||
apply pluginId('licenser')
|
||||
apply pluginId('gradleutils')
|
||||
apply pluginId('gitversion')
|
||||
apply pluginId('changelog')
|
||||
apply pluginId('forgedev')
|
||||
}
|
||||
|
||||
final versions = extensions.create("versions", VersionsExtension, project)
|
||||
final convention = extensions.create('convention', ConventionExtension, project)
|
||||
|
||||
logger.lifecycle(name + " " + version)
|
||||
|
||||
gradleutils.with {
|
||||
vendor.set('Forge Development LLC')
|
||||
version.set(gitversion.version)
|
||||
}
|
||||
project.setGroup('net.minecraftforge')
|
||||
|
||||
java.with {
|
||||
toolchain.languageVersion.set(JavaLanguageVersion.of(versions.java))
|
||||
// This is needed for IDE runtime classpath to be correct/not have multiple of the same artifact
|
||||
consistentResolution {
|
||||
it.useCompileClasspathVersions()
|
||||
}
|
||||
// Forge needs to build its own sources jar due to patching, everything else should use the default
|
||||
if (project != rootProject)
|
||||
it.withSourcesJar()
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
it.options.with {
|
||||
encoding = 'UTF-8'
|
||||
compilerArgs << '-Xlint:-unchecked' // This is mainly for vanilla code, but just makes it quiet down outside the IDE
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: We are adding dependencies to projects instead of through settings, so we don't have to apply forgedev in settings.
|
||||
// Applying forgedev in settings will break IDE linting support, which makes it very hard to work with.
|
||||
// We can reconsider this if/when JetBrains gets their shit together.
|
||||
// - I doubt they will ever fix their shit 8/28/2006 - Lex
|
||||
repositories.with {
|
||||
// Libraries has to be before maven central because Mojang hosts a classifer that central doesn't (org.lwjgl:lwjgl-freetype:3.3.3:natives-macos-patch)
|
||||
maven { url = 'https://libraries.minecraft.net/' }
|
||||
mavenCentral()
|
||||
maven gradleutils.forgeMaven
|
||||
project.pluginManager.withPlugin('net.minecraftforge.forgedev') {
|
||||
maven forgedev.mavenizer
|
||||
}
|
||||
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
changelog.with {
|
||||
from versions.changelogBase
|
||||
}
|
||||
|
||||
license.with {
|
||||
header = rootProject.file('LICENSE-header.txt')
|
||||
exclude('.project_info.properties')
|
||||
}
|
||||
|
||||
// Common dependencies,
|
||||
project.dependencies.with {
|
||||
add('compileOnly', libs.findLibrary('jetbrains.annotations').get())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.forge.build
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import net.minecraftforge.forgedev.ForgeDevExtension
|
||||
import net.minecraftforge.gitversion.gradle.GitVersionExtension
|
||||
import net.minecraftforge.gitversion.gradle.changelog.ChangelogExtension
|
||||
import net.minecraftforge.gradleutils.GradleUtilsExtensionForProject
|
||||
import net.minecraftforge.licenser.LicenseExtension
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.artifacts.VersionCatalog
|
||||
import org.gradle.api.artifacts.VersionCatalogsExtension
|
||||
import org.gradle.api.plugins.ExtraPropertiesExtension
|
||||
import org.gradle.api.plugins.JavaPluginExtension
|
||||
import org.gradle.api.publish.PublishingExtension
|
||||
|
||||
@CompileStatic
|
||||
interface Meta {
|
||||
Project getProject();
|
||||
|
||||
default VersionCatalog getLibs() {
|
||||
project.extensions.getByType(VersionCatalogsExtension).named("libs")
|
||||
}
|
||||
|
||||
default ExtraPropertiesExtension getExt() {
|
||||
project.extensions.getByType(ExtraPropertiesExtension)
|
||||
}
|
||||
|
||||
default JavaPluginExtension getJava() {
|
||||
project.extensions.getByType(JavaPluginExtension)
|
||||
}
|
||||
|
||||
default GradleUtilsExtensionForProject getGradleutils() {
|
||||
project.extensions.getByType(GradleUtilsExtensionForProject)
|
||||
}
|
||||
|
||||
default GitVersionExtension getGitversion() {
|
||||
project.extensions.getByType(GitVersionExtension)
|
||||
}
|
||||
|
||||
default ForgeDevExtension getForgedev() {
|
||||
project.extensions.getByType(ForgeDevExtension)
|
||||
}
|
||||
|
||||
default ChangelogExtension getChangelog() {
|
||||
project.extensions.getByType(ChangelogExtension)
|
||||
}
|
||||
|
||||
default LicenseExtension getLicense() {
|
||||
project.extensions.getByType(LicenseExtension)
|
||||
}
|
||||
|
||||
default PublishingExtension getPublishing() {
|
||||
project.extensions.getByType(PublishingExtension)
|
||||
}
|
||||
|
||||
default VersionsExtension getVersions() {
|
||||
project.extensions.getByType(VersionsExtension)
|
||||
}
|
||||
|
||||
default ConventionExtension getConvention() {
|
||||
project.extensions.getByType(ConventionExtension)
|
||||
}
|
||||
|
||||
default String pluginId(String alias) {
|
||||
getLibs().findPlugin(alias).orElseThrow { new IllegalStateException("Could not find plugin " + alias) }.get().pluginId
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.forge.build
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
import net.minecraftforge.licenser.LicenseExtension
|
||||
import net.minecraftforge.licenser.tasks.LicenseCheck
|
||||
import org.gradle.api.Action
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.tasks.SourceSet
|
||||
import org.gradle.api.tasks.TaskProvider
|
||||
import org.gradle.api.tasks.WriteProperties
|
||||
import org.jspecify.annotations.Nullable
|
||||
|
||||
import javax.inject.Inject
|
||||
import javax.naming.spi.ObjectFactory
|
||||
|
||||
@CompileStatic
|
||||
class Runs implements Meta {
|
||||
private final ConventionExtension ext
|
||||
private boolean hasDependency = false
|
||||
|
||||
@Inject
|
||||
Runs(ConventionExtension ext) {
|
||||
this.ext = ext
|
||||
}
|
||||
|
||||
@Override
|
||||
Project getProject() {
|
||||
this.ext.project
|
||||
}
|
||||
|
||||
void main(@Nullable String mods, @Nullable File output) {
|
||||
final project = this.project
|
||||
final genAllData = this.ext.genAllData
|
||||
|
||||
project.with {
|
||||
final SourceSet sourceSet = getJava().getSourceSets().named("main").get()
|
||||
if (!hasDependency && project != project.rootProject) {
|
||||
dependencies.with {
|
||||
add('implementation', it.project(':')) // Root project is forge itself
|
||||
}
|
||||
hasDependency = true
|
||||
}
|
||||
if (output != null)
|
||||
sourceSet.resources.with { srcDir output }
|
||||
|
||||
forgedev.runs.with {
|
||||
final File existing = sourceSet.getResources().getSrcDirs()[0]
|
||||
|
||||
configureEach {
|
||||
it.with {
|
||||
cache.set(project.layout.projectDirectory.dir('runs/cache'))
|
||||
eclipsePrefix = project.name
|
||||
|
||||
// If we're not forge, we need to get the metadata from the forge project
|
||||
if (project != project.rootProject)
|
||||
metadata = project.files(getForgedev().consumeFile('metadata', project.rootProject))
|
||||
|
||||
var runName = it.name.toLowerCase(Locale.ENGLISH)
|
||||
|
||||
if (runName.contains('client') || runName.contains('data')) {
|
||||
options.with {
|
||||
// LWJGL needs native access, Mojang gets away with just saying 'ALL-UNNAMED' because it doesnt boot into module land
|
||||
// We need a system to translate that to module names
|
||||
//jvmArgs '--enable-native-access=ALL-UNNAMED', '--add-exports', 'java.base/jdk.internal.misc=ALL-UNNAMED'
|
||||
args '--assetsDir', '{assets_root}', '--assetIndex', '{asset_index}'
|
||||
systemProperty 'org.lwjgl.system.SharedLibraryExtractDirectory', 'lwjgl_dll'
|
||||
}
|
||||
}
|
||||
|
||||
if (runName.contains('data')) {
|
||||
options.with(sourceSet).with {
|
||||
systemProperties.put("forge.project.list", getProjectList().flatMap { it.destinationFile.map{ it.asFile.absolutePath } })
|
||||
if (mods != null) args('--mod', mods)
|
||||
if (output != null) args('--output', output)
|
||||
}
|
||||
}
|
||||
|
||||
options.with {
|
||||
getWorkingDir().set(project.layout.projectDirectory.dir("runs/$name"))
|
||||
getMainClass().set('net.minecraftforge.bootstrap.ForgeBootstrap')
|
||||
args '--gameDir', '.'
|
||||
jvmArgs '-Djava.net.preferIPv6Addresses=system', '-XX:+UseCompactObjectHeaders', '-XX:StackShadowPages=32'
|
||||
|
||||
//systemProperty 'forge.logging.marker.registries', 'ACCEPT'
|
||||
//systemProperty 'forge.logging.console.level', 'debug'
|
||||
systemProperty 'bsl.debug', 'true'
|
||||
systemProperty 'terminal.jline', 'true'
|
||||
systemProperty 'forge.enableGameTest', 'true'
|
||||
systemProperty 'eventbus.api.strictRuntimeChecks', 'true'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maybeCreate('client').with {
|
||||
options.with {
|
||||
args '--launchTarget', 'forge_dev_client',
|
||||
'--username', 'Dev',
|
||||
'--version', project.name,
|
||||
'--accessToken', '0',
|
||||
'--userType', 'mojang',
|
||||
'--versionType', 'release'
|
||||
}
|
||||
}
|
||||
|
||||
maybeCreate('server').with {
|
||||
options.with {
|
||||
args '--launchTarget', 'forge_dev_server'
|
||||
}
|
||||
}
|
||||
|
||||
maybeCreate('gameTestServer').with {
|
||||
options.with {
|
||||
args '--launchTarget', 'forge_dev_server_gametest',
|
||||
'--uniqueWorld' // Unique world is used so that the world regenerates, as well as the world config isn't influenced by other runs
|
||||
}
|
||||
}
|
||||
|
||||
maybeCreate('data').with {
|
||||
options.with {
|
||||
args '--launchTarget', 'forge_dev_data',
|
||||
'--all',
|
||||
'--existing', existing
|
||||
}
|
||||
}
|
||||
|
||||
maybeCreate('clientData').with {
|
||||
options.with {
|
||||
args '--launchTarget', 'forge_dev_client_data',
|
||||
'--all',
|
||||
'--existing', existing
|
||||
}
|
||||
}
|
||||
}
|
||||
genAllData.configure {
|
||||
it.dependsOn forgedev.runs.getByName('data').with(sourceSet).run, forgedev.runs.getByName('clientData').with(sourceSet).run
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void test(@Nullable String mods, @Nullable File output) {
|
||||
final project = this.project
|
||||
final genAllData = this.ext.genAllData
|
||||
|
||||
project.with {
|
||||
final SourceSet sourceSet = getJava().getSourceSets().named("test").get()
|
||||
forgedev.runs.with {
|
||||
final File existing = sourceSet.getResources().getSrcDirs()[0]
|
||||
|
||||
configureEach {
|
||||
it.with {
|
||||
var runName = it.name.toLowerCase(Locale.ENGLISH)
|
||||
options.with(sourceSet).with {
|
||||
systemProperty 'forgedev.enableTestMods', 'true'
|
||||
}
|
||||
|
||||
if (runName.contains('data')) {
|
||||
options.with(sourceSet).with {
|
||||
if (mods != null) args('--mod', mods)
|
||||
if (output != null) args('--output', output)
|
||||
args('--existing', existing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
genAllData.configure {
|
||||
it.dependsOn forgedev.runs.getByName('data').with(sourceSet).run, forgedev.runs.getByName('clientData').with(sourceSet).run
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TaskProvider<WriteProperties> projectInfo = null
|
||||
TaskProvider<WriteProperties> getProjectInfo() {
|
||||
if (projectInfo == null) {
|
||||
projectInfo = this.project.tasks.register("writeProjectInfo", WriteProperties)
|
||||
projectInfo.configure {
|
||||
it.destinationFile.set(this.project.file('src/main/resources/.project_info.properties'))
|
||||
it.property('name', this.project.rootProject.name + this.project.isolated.path.replaceAll(':', '.'))
|
||||
it.property('output', 'src/main/generated')
|
||||
}
|
||||
this.project.tasks.named("generateResources").configure { it.dependsOn(projectInfo) }
|
||||
this.project.tasks.withType(LicenseCheck).configureEach {
|
||||
it.dependsOn(projectInfo)
|
||||
}
|
||||
}
|
||||
return projectInfo
|
||||
}
|
||||
TaskProvider<WriteProperties> projectInfo(Action<? extends WriteProperties> action) {
|
||||
getProjectInfo().configure(action)
|
||||
return getProjectInfo()
|
||||
}
|
||||
void setFlat(boolean value) {
|
||||
projectInfo(task -> task.property('flat', value))
|
||||
}
|
||||
|
||||
private TaskProvider<WriteProperties> projectList = null
|
||||
TaskProvider<WriteProperties> getProjectList() {
|
||||
if (projectList == null) {
|
||||
projectList = this.project.tasks.register("writeProjectList", WriteProperties)
|
||||
projectList.configure { task ->
|
||||
task.destinationFile.set(this.project.layout.buildDirectory.file('project_list.properties'))
|
||||
task.encoding = 'UTF-8'
|
||||
final root = this.project.rootProject.name
|
||||
this.project.rootProject.allprojects.forEach { p ->
|
||||
task.property(root + p.isolated.path.replaceAll(':', '.'), p.isolated.projectDirectory.asFile.absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
return projectList
|
||||
}
|
||||
TaskProvider<WriteProperties> projectList(Action<? extends WriteProperties> action) {
|
||||
getProjectList().configure(action)
|
||||
return getProjectList()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.forge.build;
|
||||
|
||||
import net.minecraftforge.gitversion.gradle.GitVersionExtension;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.VersionCatalog;
|
||||
import org.gradle.api.artifacts.VersionCatalogsExtension;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public abstract class VersionsExtension {
|
||||
private final VersionCatalog libs;
|
||||
|
||||
public final String java, minecraft, forge, forgeSpec, minecraftNext, mcp, changelogBase;
|
||||
|
||||
@Inject
|
||||
public VersionsExtension(Project project) {
|
||||
final var extensions = project.getExtensions();
|
||||
final var gitversion = extensions.getByType(GitVersionExtension.class);
|
||||
libs = extensions.getByType(VersionCatalogsExtension.class).named("bootLibs");
|
||||
java = get("java");
|
||||
minecraft = get("minecraft");
|
||||
minecraftNext = get("minecraft.next");
|
||||
mcp = get("mcp");
|
||||
changelogBase = get("changelog.base");
|
||||
forge = gitversion.of(project.getRootProject()).getVersion();
|
||||
forgeSpec = forge.split("\\.")[0];
|
||||
project.setVersion(minecraft + '-' + gitversion.getVersion());
|
||||
}
|
||||
|
||||
private String get(String name) {
|
||||
var ver = this.libs.findVersion(name);
|
||||
if (ver.isEmpty())
|
||||
throw new IllegalStateException("No version found for " + name);
|
||||
return ver.get().toString();
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.forge.build
|
||||
|
||||
import groovy.json.JsonBuilder
|
||||
import groovy.transform.CompileDynamic
|
||||
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.Input
|
||||
import org.gradle.api.tasks.OutputFile
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
@CompileStatic
|
||||
abstract class WriteForgeVersionJson extends DefaultTask {
|
||||
abstract @Input Property<String> getForgeVersion()
|
||||
abstract @Input Property<String> getMinecraftVersion()
|
||||
abstract @Input Property<String> getMcpVersion()
|
||||
|
||||
abstract @OutputFile RegularFileProperty getOutputFile()
|
||||
|
||||
@Inject
|
||||
WriteForgeVersionJson() {}
|
||||
|
||||
@TaskAction
|
||||
@CompileDynamic
|
||||
void exec() {
|
||||
var json = new JsonBuilder()
|
||||
json {
|
||||
forge this.forgeVersion.get()
|
||||
mc this.minecraftVersion.get()
|
||||
mcp this.mcpVersion.get()
|
||||
}
|
||||
this.outputFile.asFile.get().text = json.toPrettyString()
|
||||
}
|
||||
}
|
||||
+524
-1672
File diff suppressed because it is too large.
Load diff
@@ -1,3 +0,0 @@
|
||||
/.gradle/
|
||||
/build/
|
||||
/out/
|
||||
@@ -1,12 +0,0 @@
|
||||
repositories {
|
||||
maven { url = 'https://maven.minecraftforge.net/' }
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.ow2.asm:asm:9.3'
|
||||
implementation 'org.ow2.asm:asm-tree:9.3'
|
||||
implementation 'net.minecraftforge:srgutils:0.4.+'
|
||||
implementation 'commons-io:commons-io:2.8.0'
|
||||
implementation 'com.google.code.gson:gson:2.10'
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import groovy.json.JsonBuilder
|
||||
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.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
|
||||
|
||||
abstract class BytecodeFinder extends DefaultTask {
|
||||
@InputFile abstract RegularFileProperty getJar()
|
||||
@OutputFile abstract RegularFileProperty getOutput()
|
||||
|
||||
BytecodeFinder() {
|
||||
output.convention(project.layout.buildDirectory.dir(name).map { it.file("output.json") })
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
protected void exec() {
|
||||
Util.init()
|
||||
|
||||
def 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post()
|
||||
outputFile.text = new JsonBuilder(getData()).toPrettyString()
|
||||
}
|
||||
|
||||
|
||||
protected process(ClassNode node) {
|
||||
if (node.fields != null) node.fields.each { process(node, it) }
|
||||
if (node.methods != null) node.methods.each { process(node, it) }
|
||||
}
|
||||
|
||||
protected pre() {}
|
||||
protected process(ClassNode parent, FieldNode node) {}
|
||||
protected process(ClassNode parent, MethodNode node) {}
|
||||
protected post() {}
|
||||
protected abstract Object getData()
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import java.util.function.BiConsumer
|
||||
|
||||
public class ClosureHelper {
|
||||
BiConsumer<String, Closure> callback
|
||||
|
||||
public ClosureHelper(Closure cl, BiConsumer<String, Closure> callback) {
|
||||
this.callback = callback
|
||||
apply(this, cl)
|
||||
}
|
||||
|
||||
|
||||
def methodMissing(String name, Object args) {
|
||||
if (!args.class.isArray()) return
|
||||
Object[] aargs = (Object[])args
|
||||
|
||||
if (aargs.length == 1 && aargs[0] instanceof Closure) {
|
||||
this.callback.accept(name, (Closure)aargs[0])
|
||||
} else {
|
||||
throw new IllegalArgumentException('Unknown method: "' + name + '" with arguments ' + args + ' for ' + this)
|
||||
}
|
||||
}
|
||||
|
||||
static <T> T apply(T obj, Closure cl) {
|
||||
cl.delegate = obj
|
||||
cl.resolveStrategy = Closure.DELEGATE_FIRST
|
||||
cl()
|
||||
return obj
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.ConfigurableFileCollection
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.Internal
|
||||
import org.gradle.api.tasks.OutputFile
|
||||
import org.gradle.api.tasks.OutputFiles
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.gradle.api.tasks.OutputDirectory
|
||||
|
||||
import java.nio.file.Files
|
||||
|
||||
abstract class DownloadLibraries extends DefaultTask {
|
||||
@InputFile abstract RegularFileProperty getInput()
|
||||
@OutputDirectory abstract DirectoryProperty getOutput()
|
||||
@OutputFile abstract RegularFileProperty getLibrariesOutput()
|
||||
|
||||
DownloadLibraries() {
|
||||
output.convention(project.layout.buildDirectory.dir(name))
|
||||
librariesOutput.convention(project.layout.buildDirectory.dir(name).map(d -> d.file('libraries.txt')))
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
def run() {
|
||||
Util.init()
|
||||
File outputDir = output.get().asFile
|
||||
def libraries = new ArrayList()
|
||||
|
||||
def json = input.get().asFile.json().libraries.each { lib ->
|
||||
//TODO: Thread?
|
||||
def artifacts = [lib.downloads.artifact] + lib.downloads.get('classifiers', [:]).values()
|
||||
artifacts.each{ art ->
|
||||
def target = new File(outputDir, art.path)
|
||||
libraries.add(target.absolutePath)
|
||||
if (!target.exists() || !art.sha1.equals(target.sha1())) {
|
||||
project.logger.lifecycle("Downloading ${art.url}")
|
||||
if (!target.parentFile.exists()) {
|
||||
target.parentFile.mkdirs()
|
||||
}
|
||||
new URL(art.url).withInputStream { i ->
|
||||
target.withOutputStream { it << i }
|
||||
}
|
||||
if (!art.sha1.equals(target.sha1())) {
|
||||
throw new IllegalStateException("Failed to download ${art.url} to ${target.canonicalPath} SHA Mismatch")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Files.write(librariesOutput.get().asFile.toPath(), libraries)
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
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.ClassNode
|
||||
import org.objectweb.asm.tree.FieldNode
|
||||
import org.objectweb.asm.tree.MethodNode
|
||||
|
||||
import static org.objectweb.asm.Opcodes.*
|
||||
|
||||
abstract class FieldCompareFinder extends BytecodeFinder {
|
||||
@Nested
|
||||
Map<String, Search> fields = [:] as HashMap
|
||||
@Internal
|
||||
Map<Search, String> fieldsReverse = [:] as HashMap
|
||||
@Internal
|
||||
Map<String, Set<ObjectTarget>> targets = [:] as TreeMap
|
||||
|
||||
@Override
|
||||
protected pre() {
|
||||
//fields.each{ k,v -> logger.lifecycle("Fields: " + k + ' ' + v) }
|
||||
}
|
||||
|
||||
@Override
|
||||
protected process(ClassNode parent, MethodNode node) {
|
||||
def 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)) {
|
||||
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)))) {
|
||||
targets.computeIfAbsent(wanted, { k -> new TreeSet() }).add(instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
last = current
|
||||
}
|
||||
}
|
||||
|
||||
@Internal
|
||||
@Override
|
||||
protected Object getData() {
|
||||
def ret = [:] as HashMap
|
||||
targets.forEach{ k, v ->
|
||||
def e = fields.get(k)
|
||||
ret[k] = [
|
||||
cls: e.cls,
|
||||
name: e.name,
|
||||
replacement: e.replacement,
|
||||
targets: v
|
||||
]
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@EqualsAndHashCode(excludes = ['replacement', 'blacklist'])
|
||||
public static class Search {
|
||||
@Input
|
||||
String cls
|
||||
|
||||
@Input
|
||||
String name
|
||||
|
||||
@Input
|
||||
String replacement
|
||||
|
||||
@Nested
|
||||
@Optional
|
||||
Set<ObjectTarget> blacklist
|
||||
|
||||
@Override
|
||||
String toString() {
|
||||
return cls + '.' + name
|
||||
}
|
||||
|
||||
def blacklist(def owner, def name, def desc) {
|
||||
if (blacklist == null)
|
||||
blacklist = new HashSet()
|
||||
blacklist.add(new ObjectTarget(owner: owner, name: name, desc: desc))
|
||||
}
|
||||
def blacklist(def owner) {
|
||||
blacklist(owner, '', '')
|
||||
}
|
||||
}
|
||||
|
||||
def fields(Closure cl) {
|
||||
new ClosureHelper(cl, {name, ccl ->
|
||||
def search = ClosureHelper.apply(new Search(), ccl)
|
||||
this.fields.put(name, search)
|
||||
this.fieldsReverse.put(search, name)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
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 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import org.gradle.api.tasks.bundling.Zip
|
||||
import org.gradle.api.tasks.*
|
||||
import org.gradle.api.provider.SetProperty
|
||||
|
||||
abstract class InstallerJar extends Zip {
|
||||
@Input @Optional abstract SetProperty<String> getPackedDependencies()
|
||||
|
||||
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'))
|
||||
|
||||
def installerJson = project.tasks.installerJson
|
||||
def launcherJson = project.tasks.launcherJson
|
||||
def downloadInstaller = project.tasks.downloadInstaller
|
||||
|
||||
dependsOn(installerJson, launcherJson, downloadInstaller)
|
||||
from(installerJson, launcherJson)
|
||||
|
||||
from(project.rootProject.file('/src/main/resources/url.png'))
|
||||
project.afterEvaluate {
|
||||
from(project.zipTree(downloadInstaller.output)) {
|
||||
duplicatesStrategy = 'exclude'
|
||||
}
|
||||
|
||||
if (!System.env.MAVEN_USER) {
|
||||
dependsOn(project.universalJar)
|
||||
from(project.universalJar) {
|
||||
into '/maven/' + project.group.replace('.', '/') + '/' + project.universalJar.archiveBaseName.get() + '/' + project.version + '/'
|
||||
}
|
||||
|
||||
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 + '/'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import groovy.json.JsonBuilder
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.ConfigurableFileCollection
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.provider.SetProperty
|
||||
import org.gradle.api.tasks.*
|
||||
|
||||
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()
|
||||
@Input abstract Property<String> getLauncherJsonName()
|
||||
@Input abstract Property<String> getLogo()
|
||||
@Input abstract Property<String> getMirrors()
|
||||
@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)
|
||||
}
|
||||
|
||||
|
||||
project.afterEvaluate {
|
||||
(packedDependencies.get().collect{ project.rootProject.tasks.findByPath(it) } + [project.universalJar]).forEach {
|
||||
dependsOn(it)
|
||||
input.from it.archiveFile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
downloads: [
|
||||
artifact: [
|
||||
path: path,
|
||||
url: "https://maven.minecraftforge.net/${path}",
|
||||
sha1: child.archiveFile.get().asFile.sha1(),
|
||||
size: child.archiveFile.get().asFile.length()
|
||||
]
|
||||
]
|
||||
])
|
||||
}
|
||||
json.libraries = libs.values().sort{a,b -> a.name.compareTo(b.name)}
|
||||
json.icon = "data:image/png;base64," + new String(Base64.getEncoder().encode(Files.readAllBytes(icon.get().asFile.toPath())))
|
||||
json.json = launcherJsonName.get()
|
||||
json.logo = logo.get()
|
||||
if (!mirrors.get().isEmpty())
|
||||
json.mirrorList = mirrors.get()
|
||||
json.welcome = welcome.get()
|
||||
|
||||
Files.writeString(output.get().getAsFile().toPath(), new JsonBuilder(json).toPrettyString())
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import groovy.json.JsonBuilder
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.ConfigurableFileCollection
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.SetProperty
|
||||
import org.gradle.api.tasks.*
|
||||
|
||||
import java.nio.file.Files
|
||||
|
||||
import static net.minecraftforge.forge.tasks.Util.getArtifacts
|
||||
import static net.minecraftforge.forge.tasks.Util.iso8601Now
|
||||
|
||||
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'))
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
libs.each { key, lib -> json.libraries.add(lib) }
|
||||
Files.writeString(output.get().asFile.toPath(), new JsonBuilder(json).toPrettyString())
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
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
|
||||
|
||||
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))) {
|
||||
def 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()
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import groovy.transform.EqualsAndHashCode
|
||||
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.Nested
|
||||
import org.gradle.api.tasks.Optional
|
||||
|
||||
@EqualsAndHashCode
|
||||
public class ObjectTarget implements Comparable<ObjectTarget> {
|
||||
@Input
|
||||
String owner
|
||||
|
||||
@Input
|
||||
String name
|
||||
|
||||
@Input
|
||||
@Optional
|
||||
String desc
|
||||
|
||||
@Override
|
||||
String toString() {
|
||||
if (desc == null)
|
||||
return owner + '.' + name
|
||||
return owner + '.' + name + desc
|
||||
}
|
||||
|
||||
@Override
|
||||
int compareTo(ObjectTarget o) {
|
||||
return toString().compareTo(o.toString())
|
||||
}
|
||||
}
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
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 fmlLibs = project.configurations.detachedConfiguration(project.PACKED_DEPS.collect {
|
||||
def artifactId = it.split(':')[1]
|
||||
return project.dependencies.create("net.minecraftforge:${artifactId}:${inputVersion}")
|
||||
}.toArray(Dependency[]::new))
|
||||
|
||||
project.tasks.named('checkJarCompatibility') {
|
||||
baseLibraries.from(project.provider {
|
||||
fmlLibs.resolvedConfiguration.lenientConfiguration.files
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks
|
||||
|
||||
import groovy.json.JsonBuilder
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
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()
|
||||
}
|
||||
File.metaClass.getSha1 = { !delegate.exists() ? null : delegate.sha1() }
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
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)) {
|
||||
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() }
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
public static String getLatestForgeVersion(mcVersion) {
|
||||
def json = new JsonSlurper().parseText(new URL("https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json").getText("UTF-8"))
|
||||
return json.promos["$mcVersion-latest"]
|
||||
}
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
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 = []
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks.checks
|
||||
|
||||
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.Opcodes
|
||||
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 ->
|
||||
def 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)
|
||||
}
|
||||
|
||||
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(Opcodes.ASM9) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks.checks
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
|
||||
@CompileStatic
|
||||
enum CheckMode {
|
||||
CHECK,
|
||||
FIX
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
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('\\', '/')))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void verifyPatch(Path patch, Reporter reporter, boolean fix, String patchPath, boolean hasS2SArtifact) {
|
||||
final 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
|
||||
(ACCESS_MAP[pMatcher.group(2)] < ACCESS_MAP[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
|
||||
(ACCESS_MAP[pMatcher.group(2)] < ACCESS_MAP[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
|
||||
(ACCESS_MAP[pMatcher.group(2)] < ACCESS_MAP[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", 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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
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 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
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package net.minecraftforge.forge.tasks.checks
|
||||
|
||||
import groovy.transform.CompileStatic
|
||||
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()
|
||||
|
||||
final 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') Closure configuration) {
|
||||
taskName = taskName.capitalize()
|
||||
tasks.register("check$taskName", clazz) { CheckTask task ->
|
||||
configuration.setDelegate((T) task)
|
||||
configuration.call(task)
|
||||
task.mode.set(CheckMode.CHECK)
|
||||
task.group = 'checks'
|
||||
}
|
||||
tasks.named('checkAll').configure { it.dependsOn("check$taskName") }
|
||||
|
||||
tasks.register("checkAndFix$taskName", clazz) { CheckTask task ->
|
||||
configuration.setDelegate((T) task)
|
||||
configuration.call(task)
|
||||
task.mode.set(CheckMode.FIX)
|
||||
task.group = 'checks'
|
||||
}
|
||||
tasks.named('checkAllAndFix').configure { it.dependsOn("checkAndFix$taskName") }
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ Contributing to Forge
|
||||
|
||||
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
|
||||
+56
-19
@@ -2,16 +2,15 @@
|
||||
|
||||
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.19.x | Active |
|
||||
| 1.18.x | LTS |
|
||||
| Version | Support |
|
||||
|--------------|-----------------------------|
|
||||
| All versions | [Supported][Support-Policy] |
|
||||
|
||||
* [Download]
|
||||
* [Forum]
|
||||
@@ -20,7 +19,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,38 +28,76 @@ 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/) or [the Forge Discord server](https://discord.gg/forge).
|
||||
For support and questions, visit [the Support Forum][Support-Forum] or [the Forge Discord server][Discord].
|
||||
|
||||
[Here is a short video from Rorax showing how to install and setup Forge.](https://www.youtube.com/watch?v=lB3ArN_-3Oc)
|
||||
## Required Java Versions
|
||||
|
||||
We require the version of Java that Minecraft requires, which is currently:
|
||||
|
||||
| Minecraft | Java |
|
||||
|------------------|-------------------|
|
||||
| 26.1+ | [25][Adoptium-25] |
|
||||
| 1.20.6 - 1.21.11 | [21][Adoptium-21] |
|
||||
| 1.18 - 1.20.4 | [17][Adoptium-17] |
|
||||
| 1.17.1 | [16][Adoptium-16] |
|
||||
| 1.16.5- | [8][Adoptium-8] |
|
||||
|
||||
**If you are on Windows and have difficulty using Java to launch JAR files, consider using [Jarfix by Johann Löfflmann][Jarfix].**
|
||||
|
||||
# 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).
|
||||
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).*
|
||||
[becoming a patron][Patreon].*
|
||||
|
||||
### Thanks
|
||||
[](https://www.yourkit.com/)
|
||||
|
||||
YourKit for providing us accesss to their [YourKit Java Profiler] which helps identify bottlenecks and places for improvement.
|
||||
|
||||
[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
|
||||
|
||||
[Adoptium-25]: https://adoptium.net/temurin/releases/?version=25
|
||||
[Adoptium-21]: https://adoptium.net/temurin/releases/?version=21
|
||||
[Adoptium-17]: https://adoptium.net/temurin/releases/?version=17
|
||||
[Adoptium-16]: https://adoptium.net/temurin/releases/?version=16
|
||||
[Adoptium-8]: https://adoptium.net/temurin/releases/?version=8
|
||||
[Jarfix]: https://johann.loefflmann.net/en/software/jarfix/index.html
|
||||
|
||||
[Forum]: https://forums.minecraftforge.net/
|
||||
[Support-Forum]: https://forums.minecraftforge.net/forum/18-support-bug-reports/
|
||||
[Support-Policy]: https://forums.minecraftforge.net/topic/144690-new-tiered-support-policy
|
||||
|
||||
[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
|
||||
[YourKit Java Profiler]: https://www.yourkit.com/java/profiler/
|
||||
+15
-8
@@ -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,10 +74,10 @@ 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 Discord Server:** You can find a lot of help on the official [Discord Server](https://discord.gg/forge), 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](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
|
||||
|
||||
@@ -99,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:
|
||||
@@ -144,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/
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
for file in $(find repo | grep --invert-match -e md5 -e sha | grep -e .jar -e .zip); do
|
||||
file_path=$(realpath $file)
|
||||
file_name=$(basename $file)
|
||||
|
||||
echo "$file_name"
|
||||
files=$(unzip -l $file | awk '{print $4}')
|
||||
for f in $files; do
|
||||
out="_actual/$file_name/$f"
|
||||
parent=$(dirname "$out")
|
||||
echo $out
|
||||
|
||||
if [ ! -d "$parent" ]; then
|
||||
mkdir -p "$parent"
|
||||
fi
|
||||
|
||||
if [[ $f == *.class ]]; then
|
||||
javap -v -p jar:file://$file_path!/$f | tail -n +4 >$out.txt
|
||||
elif [[ $f != */ && $f != "Name" && $f != "----" ]]; then
|
||||
unzip -o -q $file $f -d _actual/$file_name
|
||||
fi
|
||||
done
|
||||
done
|
||||
+11
-64
@@ -1,73 +1,20 @@
|
||||
plugins {
|
||||
id 'com.github.ben-manes.versions'
|
||||
id 'org.javamodularity.moduleplugin' version '1.8.3' apply false
|
||||
id 'org.cadixdev.licenser'
|
||||
id 'net.minecraftforge.forge.build.convention'
|
||||
}
|
||||
apply plugin: 'java-library'
|
||||
apply plugin: 'jacoco'
|
||||
apply plugin: 'org.javamodularity.moduleplugin'
|
||||
|
||||
dependencyUpdates.rejectVersionIf { isNonStable(it.candidate.version) }
|
||||
gradleutils.displayName = 'FML'
|
||||
description = 'Core library used for Forge ModLoader.'
|
||||
|
||||
dependencies {
|
||||
compileOnly("org.jetbrains:annotations:${JETBRAINS_ANNOTATIONS_VERSION}")
|
||||
api libs.eventbus
|
||||
|
||||
api("net.minecraftforge:eventbus:${EVENTBUS_VERSION}")
|
||||
|
||||
implementation(project(':fmlloader'))
|
||||
implementation("commons-io:commons-io:${COMMONS_IO_VERSION}")
|
||||
implementation projects.fmlloader
|
||||
implementation libs.commons.io
|
||||
}
|
||||
|
||||
task sourcesJar(type: Jar) {
|
||||
archiveClassifier = 'sources'
|
||||
from sourceSets.main.allSource
|
||||
}
|
||||
convention.manifest('net/minecraftforge/fml/', [
|
||||
'Automatic-Module-Name': 'net.minecraftforge.fmlcore',
|
||||
'FMLModType' : 'LIBRARY'
|
||||
])
|
||||
|
||||
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',
|
||||
'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 ->
|
||||
if (pkg == '')
|
||||
manifest.attributes(values)
|
||||
else
|
||||
manifest.attributes(values, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs << '-Xlint:unchecked'
|
||||
}
|
||||
|
||||
license {
|
||||
header = rootProject.file('LICENSE-header.txt')
|
||||
|
||||
include 'net/minecraftforge/'
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives jar
|
||||
archives sourcesJar
|
||||
}
|
||||
|
||||
publishing.publications.mavenJava(MavenPublication) {
|
||||
from components.java
|
||||
artifact sourcesJar
|
||||
}
|
||||
convention.publish()
|
||||
@@ -5,36 +5,39 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import cpw.mods.modlauncher.util.ServiceLoaderUtils;
|
||||
import net.minecraftforge.eventbus.api.IEventBus;
|
||||
import net.minecraftforge.eventbus.api.bus.BusGroup;
|
||||
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;
|
||||
|
||||
public class Bindings {
|
||||
private static final Bindings INSTANCE = new Bindings();
|
||||
/**
|
||||
* Used to allow access to certain things from the game layer
|
||||
*/
|
||||
public final class Bindings {
|
||||
private Bindings() {}
|
||||
|
||||
private final IBindingsProvider provider;
|
||||
/**
|
||||
* 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"));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public static Supplier<IEventBus> getForgeBus() {
|
||||
return INSTANCE.provider.getForgeBusSupplier();
|
||||
/**
|
||||
* @return A supplier of {@link BusGroup#DEFAULT}
|
||||
*/
|
||||
public static Supplier<BusGroup> getForgeBus() {
|
||||
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,25 +5,39 @@
|
||||
|
||||
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 {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final List<ISystemReportExtender> crashCallables = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
public static void registerCrashCallable(ISystemReportExtender callable)
|
||||
{
|
||||
/**
|
||||
* Register a custom {@link ISystemReportExtender}
|
||||
*/
|
||||
public static void registerCrashCallable(ISystemReportExtender callable) {
|
||||
crashCallables.add(callable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
return headerName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get() {
|
||||
return reportGenerator.get();
|
||||
@@ -31,6 +45,37 @@ public class CrashReportCallables {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Forge Development LLC and contributors
|
||||
* SPDX-License-Identifier: LGPL-2.1-only
|
||||
*/
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.fml.loading.FMLEnvironment;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.invoke.SerializedLambda;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Use to execute code conditionally based on sidedness.
|
||||
* <ul>
|
||||
* <li>When you want to call something on one side and return a result {@link #safeCallWhenOn(Dist, Supplier)}</li>
|
||||
* <li>When you want to call one thing on one side, another thing on the other and return a result {@link #safeRunForDist(Supplier, Supplier)}</li>
|
||||
* <li>When you want to run something on one side {@link #safeRunWhenOn(Dist, Supplier)}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class DistExecutor
|
||||
{
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
private DistExecutor() {}
|
||||
|
||||
/**
|
||||
* Run the callable in the supplier only on the specified {@link Dist}.
|
||||
* This method is NOT sided-safe and special care needs to be taken in code using this method that implicit class
|
||||
* loading is not triggered by the Callable.
|
||||
*
|
||||
* This method can cause unexpected ClassNotFound exceptions.
|
||||
*
|
||||
* Use {@link #safeCallWhenOn(Dist, Supplier)} where possible.
|
||||
*
|
||||
* @param dist The dist to run on
|
||||
* @param toRun A supplier of the callable to run (Supplier wrapper to ensure classloading only on the appropriate dist)
|
||||
* @param <T> The return type from the callable
|
||||
* @return The callable's result
|
||||
* @deprecated use {@link #safeCallWhenOn(Dist, Supplier)} instead. This remains for advanced use cases.
|
||||
*/
|
||||
@Deprecated
|
||||
public static <T> T callWhenOn(Dist dist, Supplier<Callable<T>> toRun) {
|
||||
return unsafeCallWhenOn(dist, toRun);
|
||||
}
|
||||
|
||||
public static <T> T unsafeCallWhenOn(Dist dist, Supplier<Callable<T>> toRun) {
|
||||
if (dist == FMLEnvironment.dist) {
|
||||
try
|
||||
{
|
||||
return toRun.get().call();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the SafeCallable when on the correct {@link Dist}.
|
||||
*
|
||||
* <strong>The lambda supplied here is required to be a method reference to a method defined in
|
||||
* another class, otherwise an invalid SafeReferent error will be thrown</strong>
|
||||
* @param dist the dist which this will run on
|
||||
* @param toRun the SafeCallable to run and return the result from
|
||||
* @param <T> The type of the SafeCallable
|
||||
* @return the result of the SafeCallable or null if on the wrong side
|
||||
*/
|
||||
public static <T> T safeCallWhenOn(Dist dist, Supplier<SafeCallable<T>> toRun) {
|
||||
validateSafeReferent(toRun);
|
||||
return callWhenOn(dist, toRun::get);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the supplied Runnable on the speicified side. Same warnings apply as {@link #callWhenOn(Dist, Supplier)}.
|
||||
*
|
||||
* This method can cause unexpected ClassNotFound exceptions.
|
||||
*
|
||||
* @see #callWhenOn(Dist, Supplier)
|
||||
* @param dist Dist to run this code on
|
||||
* @param toRun The code to run
|
||||
* @deprecated use {@link #safeRunWhenOn(Dist, Supplier)} where possible. Advanced uses only.
|
||||
*/
|
||||
@Deprecated
|
||||
public static void runWhenOn(Dist dist, Supplier<Runnable> toRun) {
|
||||
unsafeRunWhenOn(dist, toRun);
|
||||
}
|
||||
/**
|
||||
* Runs the supplied Runnable on the speicified side. Same warnings apply as {@link #unsafeCallWhenOn(Dist, Supplier)}.
|
||||
*
|
||||
* This method can cause unexpected ClassNotFoundException problems in common scenarios. Understand the pitfalls of
|
||||
* the way the class verifier works to load classes before using this.
|
||||
*
|
||||
* Use {@link #safeRunWhenOn(Dist, Supplier)} if you can.
|
||||
*
|
||||
* @see #unsafeCallWhenOn(Dist, Supplier)
|
||||
* @param dist Dist to run this code on
|
||||
* @param toRun The code to run
|
||||
*/
|
||||
public static void unsafeRunWhenOn(Dist dist, Supplier<Runnable> toRun) {
|
||||
if (dist == FMLEnvironment.dist) {
|
||||
toRun.get().run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the supplied SafeRunnable when on the correct Dist.
|
||||
* @param dist The dist to run on
|
||||
* @param toRun The code to run
|
||||
*/
|
||||
public static void safeRunWhenOn(Dist dist, Supplier<SafeRunnable> toRun) {
|
||||
validateSafeReferent(toRun);
|
||||
if (dist == FMLEnvironment.dist) {
|
||||
toRun.get().run();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Executes one of the two suppliers, based on which side is active.
|
||||
*
|
||||
* <p>
|
||||
* Example (replacement for old SidedProxy):<br/>
|
||||
* {@code Proxy p = DistExecutor.runForDist(()->ClientProxy::new, ()->ServerProxy::new);}
|
||||
*
|
||||
* NOTE: the double supplier is required to avoid classloading the secondary target.
|
||||
*
|
||||
* @param clientTarget The supplier supplier to run when on the {@link Dist#CLIENT}
|
||||
* @param serverTarget The supplier supplier to run when on the {@link Dist#DEDICATED_SERVER}
|
||||
* @param <T> The common type to return
|
||||
* @return The returned instance
|
||||
* @deprecated Use {@link #safeRunForDist(Supplier, Supplier)}
|
||||
*/
|
||||
@Deprecated
|
||||
public static <T> T runForDist(Supplier<Supplier<T>> clientTarget, Supplier<Supplier<T>> serverTarget) {
|
||||
return unsafeRunForDist(clientTarget, serverTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsafe version of {@link #safeRunForDist(Supplier, Supplier)}. Use only when you know what you're doing
|
||||
* and understand why the verifier can cause unexpected ClassNotFoundException crashes even when code is apparently
|
||||
* not sided. Ensure you test both sides fully to be confident in using this.
|
||||
*
|
||||
* @param clientTarget The supplier supplier to run when on the {@link Dist#CLIENT}
|
||||
* @param serverTarget The supplier supplier to run when on the {@link Dist#DEDICATED_SERVER}
|
||||
* @param <T> The common type to return
|
||||
* @return The returned instance
|
||||
*/
|
||||
public static <T> T unsafeRunForDist(Supplier<Supplier<T>> clientTarget, Supplier<Supplier<T>> serverTarget) {
|
||||
switch (FMLEnvironment.dist)
|
||||
{
|
||||
case CLIENT:
|
||||
return clientTarget.get().get();
|
||||
case DEDICATED_SERVER:
|
||||
return serverTarget.get().get();
|
||||
default:
|
||||
throw new IllegalArgumentException("UNSIDED?");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Executes one of the two suppliers, based on which side is active.
|
||||
*
|
||||
* <p>
|
||||
* Example (replacement for old SidedProxy):<br/>
|
||||
* {@code Proxy p = DistExecutor.safeRunForDist(()->ClientProxy::new, ()->ServerProxy::new);}
|
||||
*
|
||||
* NOTE: the double supplier is required to avoid classloading the secondary target.
|
||||
*
|
||||
* @param clientTarget The supplier supplier to run when on the {@link Dist#CLIENT}
|
||||
* @param serverTarget The supplier supplier to run when on the {@link Dist#DEDICATED_SERVER}
|
||||
* @param <T> The common type to return
|
||||
* @return The returned instance
|
||||
*/
|
||||
public static <T> T safeRunForDist(Supplier<SafeSupplier<T>> clientTarget, Supplier<SafeSupplier<T>> serverTarget) {
|
||||
validateSafeReferent(clientTarget);
|
||||
validateSafeReferent(serverTarget);
|
||||
switch (FMLEnvironment.dist)
|
||||
{
|
||||
case CLIENT:
|
||||
return clientTarget.get().get();
|
||||
case DEDICATED_SERVER:
|
||||
return serverTarget.get().get();
|
||||
default:
|
||||
throw new IllegalArgumentException("UNSIDED?");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A safe referent. This will assert that it is being called via a separated class method reference. This will
|
||||
* avoid the common pitfalls of {@link #callWhenOn(Dist, Supplier)} above.
|
||||
*
|
||||
* SafeReferents assert that they are defined as a separate method outside the scope of the calling class.
|
||||
*
|
||||
* <strong>Implementations need to be defined in a separate class to the referring site, with appropriate
|
||||
* visibility to be accessible at the callsite (generally, avoid private methods).</strong>
|
||||
*
|
||||
* <p>
|
||||
* Valid:<br/>
|
||||
*
|
||||
* {@code DistExecutor.safeCallWhenOn(Dist.CLIENT, ()->AnotherClass::clientOnlyMethod);}
|
||||
*
|
||||
* <p>
|
||||
* Invalid:<br/>
|
||||
*
|
||||
* {@code DistExecutor.safeCallWhenOn(Dist.CLIENT, ()->()->Minecraft.getInstance().world);}
|
||||
*/
|
||||
public interface SafeReferent {}
|
||||
|
||||
/**
|
||||
* SafeCallable version of {@link SafeReferent}.
|
||||
* @see SafeReferent
|
||||
* @param <T> The return type of the Callable
|
||||
*/
|
||||
public interface SafeCallable<T> extends SafeReferent, Callable<T>, Serializable {}
|
||||
|
||||
/**
|
||||
* SafeSupplier version of {@link SafeReferent}
|
||||
* @param <T> The return type of the Supplier
|
||||
*/
|
||||
public interface SafeSupplier<T> extends SafeReferent, Supplier<T>, Serializable {}
|
||||
|
||||
/**
|
||||
* SafeRunnable version of {@link SafeReferent}
|
||||
*/
|
||||
public interface SafeRunnable extends SafeReferent, Runnable, Serializable {}
|
||||
|
||||
private static final void validateSafeReferent(Supplier<? extends SafeReferent> safeReferentSupplier) {
|
||||
if (FMLEnvironment.production) return;
|
||||
final SafeReferent setter;
|
||||
try {
|
||||
setter = safeReferentSupplier.get();
|
||||
} catch (Exception e) {
|
||||
// Typically a class cast exception, just return out, expected.
|
||||
return;
|
||||
}
|
||||
|
||||
for (Class<?> cl = setter.getClass(); cl != null; cl = cl.getSuperclass()) {
|
||||
try {
|
||||
Method m = cl.getDeclaredMethod("writeReplace");
|
||||
m.setAccessible(true);
|
||||
Object replacement = m.invoke(setter);
|
||||
if (!(replacement instanceof SerializedLambda))
|
||||
break;// custom interface implementation
|
||||
SerializedLambda l = (SerializedLambda) replacement;
|
||||
if (Objects.equals(l.getCapturingClass(), l.getImplClass())) {
|
||||
LOGGER.fatal("Detected unsafe referent usage, please view the code at {}",Thread.currentThread().getStackTrace()[3]);
|
||||
throw new RuntimeException("Unsafe Referent usage found in safe referent method");
|
||||
}
|
||||
} catch (NoSuchMethodException e) {
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,13 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.eventbus.api.IEventBus;
|
||||
import net.minecraftforge.eventbus.api.bus.BusGroup;
|
||||
import net.minecraftforge.fml.config.IConfigEvent;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public interface IBindingsProvider {
|
||||
Supplier<IEventBus> getForgeBusSupplier();
|
||||
Supplier<BusGroup> getForgeBusSupplier();
|
||||
Supplier<I18NParser> getMessageParser();
|
||||
Supplier<IConfigEvent.ConfigConfig> getConfigConfiguration();
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import java.util.function.Supplier;
|
||||
* @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>
|
||||
{
|
||||
/**
|
||||
@@ -40,7 +41,7 @@ public interface IExtensionPoint<T extends Record>
|
||||
* 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>The local compatibility version may be of the value {@link net.minecraftforge.network.NetworkConstants#IGNORESERVERONLY},
|
||||
* <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>
|
||||
@@ -94,6 +95,27 @@ public interface IExtensionPoint<T extends Record>
|
||||
*/
|
||||
@SuppressWarnings("JavadocReference") // reference to NetworkConstants, ForgeHooksClient
|
||||
record DisplayTest(Supplier<String> suppliedVersion, BiPredicate<String, Boolean> remoteVersionTest) implements IExtensionPoint<DisplayTest> {
|
||||
public static final String IGNORESERVERONLY = "OHNOES\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31";
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,15 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
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.IntSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* A mod loading state. During mod loading, the mod loader transitions between states in a defined sorted list of states,
|
||||
@@ -20,6 +21,7 @@ import java.util.function.Function;
|
||||
*
|
||||
* @see IModStateProvider
|
||||
*/
|
||||
@NullMarked
|
||||
public interface IModLoadingState {
|
||||
/**
|
||||
* {@return the name of this state}
|
||||
@@ -42,45 +44,56 @@ public interface IModLoadingState {
|
||||
/**
|
||||
* {@return a function returning a human-friendly message for this state}
|
||||
*/
|
||||
Function<ModList, String> message();
|
||||
Supplier<String> message();
|
||||
|
||||
/**
|
||||
* @return a function that computes the size of this transition based on the size of the modlist.
|
||||
* Used to compute progress.
|
||||
*/
|
||||
default IntSupplier size() {
|
||||
return ModList::size;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return an optional runnable, which runs before starting the transition from this state to the next}
|
||||
* @see #buildTransition(Executor, Executor, Function, Function)
|
||||
* @see #buildTransition(Executor, Executor, ProgressMeter, Function, Function)
|
||||
*/
|
||||
Optional<Consumer<ModList>> inlineRunnable();
|
||||
@Nullable Runnable inlineRunnable();
|
||||
|
||||
/**
|
||||
* Builds the transition task for this state with a blank pre-sync and post-sync task.
|
||||
*
|
||||
* @param syncExecutor a synchronous executor
|
||||
* @param syncExecutor a synchronous executor
|
||||
* @param parallelExecutor a parallel executor
|
||||
* @param <T> a type of event fired on the mod-specific event bus
|
||||
* @param progressBar a progress meter for tracking progress
|
||||
* @return a transition task for this state
|
||||
* @see #buildTransition(Executor, Executor, Function, Function)
|
||||
* @see #buildTransition(Executor, Executor, ProgressMeter, Function, Function)
|
||||
*/
|
||||
default <T extends Event & IModBusEvent>
|
||||
Optional<CompletableFuture<Void>> buildTransition(final Executor syncExecutor,
|
||||
final Executor parallelExecutor) {
|
||||
return buildTransition(syncExecutor, parallelExecutor,
|
||||
e -> CompletableFuture.runAsync(() -> {}, e),
|
||||
e -> CompletableFuture.runAsync(() -> {}, e));
|
||||
default @Nullable CompletableFuture<Void> buildTransition(
|
||||
final Executor syncExecutor,
|
||||
final Executor parallelExecutor,
|
||||
final ProgressMeter progressBar
|
||||
) {
|
||||
return buildTransition(syncExecutor, parallelExecutor, progressBar,
|
||||
_ -> CompletableFuture.completedFuture(null),
|
||||
_ -> CompletableFuture.completedFuture(null)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 syncExecutor a synchronous executor
|
||||
* @param syncExecutor a synchronous executor
|
||||
* @param parallelExecutor a parallel executor
|
||||
* @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
|
||||
* @param <T> a type of event fired on the mod-specific event bus
|
||||
* @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,
|
||||
@Nullable CompletableFuture<Void> buildTransition(final Executor syncExecutor,
|
||||
final Executor parallelExecutor,
|
||||
final ProgressMeter progressBar,
|
||||
final Function<Executor, CompletableFuture<Void>> preSyncTask,
|
||||
final Function<Executor, CompletableFuture<Void>> postSyncTask);
|
||||
}
|
||||
@@ -5,108 +5,47 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public interface IModStateTransition {
|
||||
static IModStateTransition buildNoopTransition() {
|
||||
return new NoopTransition();
|
||||
return ModStateTransitionHelper.NOOP;
|
||||
}
|
||||
|
||||
default <T extends Event & IModBusEvent>
|
||||
CompletableFuture<Void> build(final Executor syncExecutor,
|
||||
final Executor parallelExecutor,
|
||||
final Function<Executor, CompletableFuture<Void>> preSyncTask,
|
||||
final Function<Executor, CompletableFuture<Void>> postSyncTask) {
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
||||
|
||||
this.eventFunctionStream().get()
|
||||
.map(f -> (EventGenerator<T>) f)
|
||||
.reduce((head, tail) -> addCompletableFutureTaskForModDispatch(syncExecutor, parallelExecutor, futures, head, ModLoadingStage::currentState, tail))
|
||||
.ifPresent(last -> addCompletableFutureTaskForModDispatch(syncExecutor, parallelExecutor, futures, last, nextModLoadingStage(), null));
|
||||
|
||||
final CompletableFuture<Void> preSyncTaskCF = preSyncTask.apply(syncExecutor);
|
||||
final CompletableFuture<Void> eventDispatchCF = ModList.gather(futures).thenCompose(ModList::completableFutureFromExceptionList);
|
||||
final CompletableFuture<Void> postEventDispatchCF = preSyncTaskCF
|
||||
.thenComposeAsync(n -> eventDispatchCF, parallelExecutor)
|
||||
.thenApply(r -> {
|
||||
postSyncTask.apply(syncExecutor);
|
||||
return null;
|
||||
});
|
||||
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<Void>> completableFutures,
|
||||
final EventGenerator<T> eventGenerator,
|
||||
final BiFunction<ModLoadingStage, Throwable, ModLoadingStage> nextState,
|
||||
final EventGenerator<T> nextGenerator) {
|
||||
final Executor selectedExecutor = threadSelector().apply(syncExecutor, parallelExecutor);
|
||||
|
||||
var preDispatchHook = (BiFunction<Executor, EventGenerator<T>, CompletableFuture<Void>>) preDispatchHook();
|
||||
completableFutures.add(preDispatchHook.apply(selectedExecutor, eventGenerator));
|
||||
|
||||
completableFutures.add(ModList.get().futureVisitor(eventGenerator, nextState).apply(threadSelector().apply(syncExecutor, parallelExecutor)));
|
||||
|
||||
var postDispatchHook = (BiFunction<Executor, EventGenerator<T>, CompletableFuture<Void>>) postDispatchHook();
|
||||
completableFutures.add(postDispatchHook.apply(selectedExecutor, eventGenerator));
|
||||
|
||||
return nextGenerator;
|
||||
@Nullable
|
||||
default <T extends IModBusEvent> EventGenerator<T> eventFunction() {
|
||||
return null;
|
||||
}
|
||||
|
||||
Supplier<Stream<EventGenerator<?>>> eventFunctionStream();
|
||||
ThreadSelector threadSelector();
|
||||
BiFunction<Executor, CompletableFuture<Void>, CompletableFuture<Void>> finalActivityGenerator();
|
||||
BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> preDispatchHook();
|
||||
BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> postDispatchHook();
|
||||
|
||||
interface EventGenerator<T extends Event & IModBusEvent> extends Function<ModContainer, T> {
|
||||
static <FN extends Event & IModBusEvent> EventGenerator<FN> fromFunction(Function<ModContainer, FN> fn) {
|
||||
interface EventGenerator<T extends IModBusEvent> extends Function<ModContainer, T> {
|
||||
static <FN extends 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<Void>, CompletableFuture<Void>> finalActivityGenerator() {
|
||||
return (e, t) -> t.thenApplyAsync(Function.identity(), e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> preDispatchHook() {
|
||||
return (t, f)-> CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiFunction<Executor, ? extends EventGenerator<?>, CompletableFuture<Void>> postDispatchHook() {
|
||||
return (t, f)-> CompletableFuture.completedFuture(null);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,10 @@ package net.minecraftforge.fml;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public interface ISystemReportExtender extends Supplier<String>
|
||||
{
|
||||
public interface ISystemReportExtender extends Supplier<String> {
|
||||
String getLabel();
|
||||
|
||||
default boolean isActive() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -5,63 +5,20 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Spliterator;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public class InterModComms
|
||||
{
|
||||
public record IMCMessage(String senderModId, String modId, String method, Supplier<?> messageSupplier) {
|
||||
public final class InterModComms {
|
||||
public record IMCMessage(String senderModId, String modId, String method, Supplier<?> messageSupplier) {}
|
||||
|
||||
/**
|
||||
* Deprecated: use {@link #senderModId()}
|
||||
* @return The modid of the sender. This is supplied by the caller, or by the active mod container context.
|
||||
* Consider it unreliable.
|
||||
*/
|
||||
@Deprecated
|
||||
public final String getSenderModId() {
|
||||
return this.senderModId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated: use {@link #modId()}
|
||||
* @return The modid being sent to.
|
||||
*/
|
||||
@Deprecated
|
||||
public final String getModId() {
|
||||
return this.modId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated: use {@link #method()}
|
||||
* @return The method being sent to.
|
||||
*/
|
||||
@Deprecated
|
||||
public final String getMethod() {
|
||||
return this.method;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param <T> The type of the message.
|
||||
* @return A {@link Supplier} of the message.
|
||||
* Use {@link #messageSupplier()}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Deprecated
|
||||
public final <T> Supplier<T> getMessageSupplier() {
|
||||
return (Supplier<T>) this.messageSupplier;
|
||||
}
|
||||
}
|
||||
|
||||
private static ConcurrentMap<String, ConcurrentLinkedQueue<IMCMessage>> containerQueues = new ConcurrentHashMap<>();
|
||||
private static final ConcurrentHashMap<String, ConcurrentLinkedQueue<IMCMessage>> containerQueues = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Send IMC to remote. Sender will default to the active modcontainer, or minecraft if not.
|
||||
@@ -72,8 +29,9 @@ public class InterModComms
|
||||
* @return true if the message was enqueued for sending (the target modid is loaded)
|
||||
*/
|
||||
public static boolean sendTo(final String modId, final String method, final Supplier<?> thing) {
|
||||
if (!ModList.get().isLoaded(modId)) return false;
|
||||
containerQueues.computeIfAbsent(modId, k->new ConcurrentLinkedQueue<>()).add(new IMCMessage(ModLoadingContext.get().getActiveContainer().getModId(), modId, method, thing));
|
||||
if (!ModList.isLoaded(modId)) return false;
|
||||
containerQueues.computeIfAbsent(modId, _ -> new ConcurrentLinkedQueue<>())
|
||||
.add(new IMCMessage(ModLoadingContext.get().getContainer().getModId(), modId, method, thing));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -87,8 +45,9 @@ public class InterModComms
|
||||
* @return true if the message was enqueued for sending (the target modid is loaded)
|
||||
*/
|
||||
public static boolean sendTo(final String senderModId, final String modId, final String method, final Supplier<?> thing) {
|
||||
if (!ModList.get().isLoaded(modId)) return false;
|
||||
containerQueues.computeIfAbsent(modId, k->new ConcurrentLinkedQueue<>()).add(new IMCMessage(senderModId, modId, method, thing));
|
||||
if (!ModList.isLoaded(modId)) return false;
|
||||
containerQueues.computeIfAbsent(modId, _ -> new ConcurrentLinkedQueue<>())
|
||||
.add(new IMCMessage(senderModId, modId, method, thing));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -112,19 +71,17 @@ public class InterModComms
|
||||
* @return All messages
|
||||
*/
|
||||
public static Stream<IMCMessage> getMessages(final String modId) {
|
||||
return getMessages(modId, s->Boolean.TRUE);
|
||||
return getMessages(modId, _ -> 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
|
||||
@@ -138,13 +95,10 @@ public class InterModComms
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryAdvance(final Consumer<? super IMCMessage> action)
|
||||
{
|
||||
public boolean tryAdvance(final Consumer<? super IMCMessage> action) {
|
||||
IMCMessage next;
|
||||
do
|
||||
{
|
||||
if (!iterator.hasNext())
|
||||
{
|
||||
do {
|
||||
if (!iterator.hasNext()) {
|
||||
return false;
|
||||
}
|
||||
next = this.iterator.next();
|
||||
@@ -159,6 +113,5 @@ public class InterModComms
|
||||
public Spliterator<IMCMessage> trySplit() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,7 @@ import net.minecraftforge.api.distmarker.Dist;
|
||||
*
|
||||
* @see Dist
|
||||
*/
|
||||
public enum LogicalSide
|
||||
{
|
||||
public enum LogicalSide {
|
||||
/**
|
||||
* The logical client of the Minecraft game, which interfaces with the player's inputs and renders the player's
|
||||
* viewpoint.
|
||||
@@ -27,6 +26,7 @@ public enum LogicalSide
|
||||
* @see Dist#CLIENT
|
||||
*/
|
||||
CLIENT,
|
||||
|
||||
/**
|
||||
* The logical server of the Minecraft game, responsible for connecting to clients and running the simulation logic
|
||||
* on the level.
|
||||
@@ -41,16 +41,14 @@ public enum LogicalSide
|
||||
/**
|
||||
* {@return if this logical side is the server}
|
||||
*/
|
||||
public boolean isServer()
|
||||
{
|
||||
public boolean isServer() {
|
||||
return !isClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return if the logical side is the client}
|
||||
*/
|
||||
public boolean isClient()
|
||||
{
|
||||
public boolean isClient() {
|
||||
return this == CLIENT;
|
||||
}
|
||||
}
|
||||
@@ -5,22 +5,20 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.eventbus.api.bus.BusGroup;
|
||||
import net.minecraftforge.fml.config.IConfigEvent;
|
||||
import net.minecraftforge.fml.config.ModConfig;
|
||||
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.function.Consumer;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
@@ -31,41 +29,35 @@ 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")
|
||||
protected Optional<Consumer<IConfigEvent>> configHandler = Optional.empty();
|
||||
final Set<ModContainer> dependencies = new HashSet<>();
|
||||
|
||||
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;
|
||||
|
||||
final String displayTestString = info.getConfig().<String>getConfigElement("displayTest").orElse("MATCH_VERSION"); // missing defaults to DEFAULT type
|
||||
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()));
|
||||
(incoming, _) -> Objects.equals(incoming, this.modInfo.getVersion().toString()));
|
||||
case "IGNORE_SERVER_VERSION" -> // Ignores any version information coming from the server - use for server only mods
|
||||
() -> new IExtensionPoint.DisplayTest(() -> IExtensionPoint.DisplayTest.IGNORESERVERONLY, (incoming, isNetwork) -> true);
|
||||
IExtensionPoint.DisplayTest.IGNORE_SERVER_VERSION;
|
||||
case "IGNORE_ALL_VERSION" -> // Ignores all information and provides no information
|
||||
() -> new IExtensionPoint.DisplayTest(() -> "", (incoming, isNetwork) -> true);
|
||||
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");
|
||||
@@ -79,56 +71,35 @@ public abstract class ModContainer
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
@@ -137,18 +108,31 @@ 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));
|
||||
}
|
||||
public void dispatchConfigEvent(IConfigEvent event) {}
|
||||
|
||||
/**
|
||||
* Does this mod match the supplied mod?
|
||||
@@ -163,9 +147,17 @@ public abstract class ModContainer
|
||||
*/
|
||||
public abstract Object getMod();
|
||||
|
||||
/**
|
||||
* @return the mod bus group for this specific mod, or null if there is none.
|
||||
* Leave null if you use {@link BusGroup#DEFAULT} for your mod lifecycle events.
|
||||
*/
|
||||
public BusGroup getModBusGroup() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept an arbitrary event for processing by the mod. Probably posted to an event bus in the lower level container.
|
||||
* @param e Event to accept
|
||||
*/
|
||||
protected <T extends Event & IModBusEvent> void acceptEvent(T e) {}
|
||||
protected <T extends IModBusEvent> void acceptEvent(T e) {}
|
||||
}
|
||||
@@ -5,36 +5,22 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.fml.loading.LoadingModList;
|
||||
import net.minecraftforge.forgespi.language.IModFileInfo;
|
||||
import net.minecraftforge.forgespi.language.IModInfo;
|
||||
import net.minecraftforge.forgespi.language.ModFileScanData;
|
||||
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.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,194 +30,131 @@ 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();
|
||||
private static ModList INSTANCE;
|
||||
private final List<IModFileInfo> modFiles;
|
||||
private final List<IModInfo> sortedList;
|
||||
private final Map<String, ModFileInfo> fileById;
|
||||
private List<ModContainer> mods;
|
||||
private Map<String, ModContainer> indexedMods;
|
||||
private List<ModFileScanData> modFileScanData;
|
||||
private List<ModContainer> sortedContainers;
|
||||
public final class ModList {
|
||||
private ModList() {}
|
||||
|
||||
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));
|
||||
CrashReportCallables.registerCrashCallable("Mod List", this::crashReport);
|
||||
private static final List<IModFileInfo> MOD_FILES;
|
||||
private static final List<IModInfo> SORTED_LIST;
|
||||
private static final Map<String, IModFileInfo> FILE_BY_ID;
|
||||
private static List<ModContainer> mods;
|
||||
private static Map<String, ModContainer> indexedMods;
|
||||
private static List<ModContainer> sortedContainers;
|
||||
|
||||
static {
|
||||
var loadingModFiles = LoadingModList.getModFiles().stream().map(ModFileInfo::getFile).toList();
|
||||
var loadingSortedList = LoadingModList.getMods();
|
||||
|
||||
var modFileInfos = MOD_FILES = loadingModFiles.stream().map(ModFile::getModFileInfo).toList();
|
||||
SORTED_LIST = loadingSortedList.stream().map(IModInfo.class::cast).toList();
|
||||
var byId = new HashMap<String, IModFileInfo>();
|
||||
for (var file : modFileInfos) {
|
||||
for (var mod : file.getMods())
|
||||
byId.put(mod.getModId(), mod.getOwningFile());
|
||||
}
|
||||
FILE_BY_ID = Map.copyOf(byId);
|
||||
CrashReportCallables.registerCrashCallable("Mod List", ModList::crashReport);
|
||||
}
|
||||
|
||||
private String getModContainerState(String modId) {
|
||||
private static String getModContainerState(String modId) {
|
||||
return getModContainerById(modId).map(ModContainer::getCurrentState).map(Object::toString).orElse("NONE");
|
||||
}
|
||||
|
||||
private String fileToLine(IModFile mf) {
|
||||
private static 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", ""));
|
||||
|
||||
private static String crashReport() {
|
||||
return "\n" + applyForEachModFile(ModList::fileToLine).collect(Collectors.joining("\n\t\t", "\t\t", ""));
|
||||
}
|
||||
|
||||
public static ModList of(List<ModFile> modFiles, List<ModInfo> sortedList)
|
||||
{
|
||||
INSTANCE = new ModList(modFiles, sortedList);
|
||||
return INSTANCE;
|
||||
public static void init() {}
|
||||
|
||||
public static List<IModFileInfo> getModFiles() {
|
||||
return MOD_FILES;
|
||||
}
|
||||
|
||||
public static ModList get() {
|
||||
return INSTANCE;
|
||||
public static IModFileInfo getModFileById(String modid) {
|
||||
return FILE_BY_ID.get(modid);
|
||||
}
|
||||
|
||||
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;
|
||||
static void setLoadedMods(final List<ModContainer> modContainers) {
|
||||
mods = modContainers;
|
||||
sortedContainers = modContainers.stream().sorted(Comparator.comparingInt(c -> SORTED_LIST.indexOf(c.getModInfo()))).toList();
|
||||
indexedMods = modContainers.stream().collect(Collectors.toUnmodifiableMap(ModContainer::getModId, Function.identity()));
|
||||
}
|
||||
|
||||
public List<IModFileInfo> getModFiles()
|
||||
{
|
||||
return modFiles;
|
||||
}
|
||||
|
||||
public IModFileInfo getModFileById(String modid)
|
||||
{
|
||||
return this.fileById.get(modid);
|
||||
}
|
||||
|
||||
<T extends Event & IModBusEvent> Function<Executor, CompletableFuture<Void>> 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<Void> completableFutureFromExceptionList(List<? extends Map.Entry<?, Throwable>> t) {
|
||||
if (t.stream().noneMatch(e->e.getValue()!=null)) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
} else {
|
||||
final List<Throwable> throwables = t.stream().filter(e -> e.getValue() != null).map(Map.Entry::getValue).collect(Collectors.toList());
|
||||
CompletableFuture<Void> 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)
|
||||
{
|
||||
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()));
|
||||
static void clearLoadedMods() {
|
||||
mods = List.of();
|
||||
indexedMods = Map.of();
|
||||
sortedContainers = List.of();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Optional<T> getModObjectById(String modId)
|
||||
{
|
||||
public static <T> Optional<T> getModObjectById(String modId) {
|
||||
return getModContainerById(modId).map(ModContainer::getMod).map(o -> (T) o);
|
||||
}
|
||||
|
||||
public Optional<? extends ModContainer> getModContainerById(String modId)
|
||||
{
|
||||
return Optional.ofNullable(this.indexedMods.get(modId));
|
||||
public static Optional<? extends ModContainer> getModContainerById(String modId) {
|
||||
return Optional.ofNullable(indexedMods == null ? null : indexedMods.get(modId));
|
||||
}
|
||||
|
||||
public Optional<? extends ModContainer> getModContainerByObject(Object obj)
|
||||
{
|
||||
public static Optional<? extends ModContainer> getModContainerByObject(Object obj) {
|
||||
return mods.stream().filter(mc -> mc.getMod() == obj).findFirst();
|
||||
}
|
||||
|
||||
public List<IModInfo> getMods()
|
||||
{
|
||||
return this.sortedList;
|
||||
public static List<IModInfo> getMods() {
|
||||
return SORTED_LIST;
|
||||
}
|
||||
|
||||
public boolean isLoaded(String modTarget)
|
||||
{
|
||||
return this.indexedMods.containsKey(modTarget);
|
||||
public static boolean isLoaded(String modTarget) {
|
||||
return indexedMods.containsKey(modTarget);
|
||||
}
|
||||
|
||||
public int size()
|
||||
{
|
||||
public static int size() {
|
||||
return mods.size();
|
||||
}
|
||||
|
||||
public List<ModFileScanData> getAllScanData()
|
||||
{
|
||||
if (modFileScanData == null)
|
||||
{
|
||||
modFileScanData = this.sortedList.stream().
|
||||
map(IModInfo::getOwningFile).
|
||||
filter(Objects::nonNull).
|
||||
map(IModFileInfo::getFile).
|
||||
distinct().
|
||||
map(IModFile::getScanResult).
|
||||
collect(Collectors.toList());
|
||||
public static List<ModFileScanData> getAllScanData() {
|
||||
final class LazyInit {
|
||||
private LazyInit() {}
|
||||
private static final List<ModFileScanData> MOD_FILE_SCAN_DATA = SORTED_LIST.stream()
|
||||
.map(IModInfo::getOwningFile)
|
||||
.filter(Objects::nonNull)
|
||||
.map(IModFileInfo::getFile)
|
||||
.distinct()
|
||||
.map(IModFile::getScanResult)
|
||||
.toList();
|
||||
}
|
||||
return modFileScanData;
|
||||
|
||||
return LazyInit.MOD_FILE_SCAN_DATA;
|
||||
}
|
||||
|
||||
public void forEachModFile(Consumer<IModFile> fileConsumer)
|
||||
{
|
||||
modFiles.stream().map(IModFileInfo::getFile).forEach(fileConsumer);
|
||||
public static void forEachModFile(Consumer<IModFile> fileConsumer) {
|
||||
MOD_FILES.stream().map(IModFileInfo::getFile).forEach(fileConsumer);
|
||||
}
|
||||
|
||||
public <T> Stream<T> applyForEachModFile(Function<IModFile, T> function) {
|
||||
return modFiles.stream().map(IModFileInfo::getFile).map(function);
|
||||
public static <T> Stream<T> applyForEachModFile(Function<IModFile, T> function) {
|
||||
return MOD_FILES.stream().map(IModFileInfo::getFile).map(function);
|
||||
}
|
||||
|
||||
public void forEachModContainer(BiConsumer<String, ModContainer> modContainerConsumer) {
|
||||
public static void forEachModContainer(BiConsumer<String, ModContainer> modContainerConsumer) {
|
||||
indexedMods.forEach(modContainerConsumer);
|
||||
}
|
||||
|
||||
public void forEachModInOrder(Consumer<ModContainer> containerConsumer) {
|
||||
this.sortedContainers.forEach(containerConsumer);
|
||||
public static void forEachModInOrder(Consumer<ModContainer> containerConsumer) {
|
||||
sortedContainers.forEach(containerConsumer);
|
||||
}
|
||||
|
||||
public <T> Stream<T> applyForEachModContainer(Function<ModContainer, T> function) {
|
||||
public static List<ModContainer> getLoadedMods() {
|
||||
return sortedContainers;
|
||||
}
|
||||
|
||||
public static <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,13 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.eventbus.api.bus.BusGroup;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.fml.loading.FMLEnvironment;
|
||||
import net.minecraftforge.fml.loading.FMLLoader;
|
||||
import net.minecraftforge.fml.loading.LoadingModList;
|
||||
import net.minecraftforge.fml.loading.*;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.InvalidModIdentifier;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo;
|
||||
import net.minecraftforge.fml.loading.moddiscovery.ModInfo;
|
||||
import net.minecraftforge.fml.loading.progress.StartupMessageManager;
|
||||
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;
|
||||
@@ -30,7 +27,6 @@ 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;
|
||||
|
||||
@@ -70,190 +66,239 @@ import static net.minecraftforge.fml.Logging.LOADING;
|
||||
* and completes the mod loading sequence.</dd>
|
||||
* </dl>
|
||||
*/
|
||||
public class ModLoader
|
||||
{
|
||||
public final class ModLoader {
|
||||
private ModLoader() {}
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
private static ModLoader INSTANCE;
|
||||
private final LoadingModList loadingModList;
|
||||
private static final ArrayList<ModLoadingException> LOADING_EXCEPTIONS = new ArrayList<>();
|
||||
private static final ArrayList<ModLoadingWarning> LOADING_WARNINGS = new ArrayList<>();
|
||||
private static final Consumer<String> STATUS_CONSUMER = StartupNotificationManager.modLoaderConsumer().orElse(_ -> {});
|
||||
private static final HashSet<IModLoadingState> COMPLETED_STATES = new HashSet<>();
|
||||
private static boolean loadingStateValid;
|
||||
|
||||
private final List<ModLoadingException> loadingExceptions;
|
||||
private final List<ModLoadingWarning> loadingWarnings;
|
||||
private final ModStateManager stateManager;
|
||||
private boolean loadingStateValid;
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private final Optional<Consumer<String>> statusConsumer = StartupMessageManager.modLoaderConsumer();
|
||||
private ModList modList;
|
||||
|
||||
private ModLoader()
|
||||
{
|
||||
INSTANCE = this;
|
||||
this.loadingModList = FMLLoader.getLoadingModList();
|
||||
this.loadingExceptions = FMLLoader.getLoadingModList().getErrors().stream()
|
||||
.flatMap(ModLoadingException::fromEarlyException)
|
||||
.collect(Collectors.toList());
|
||||
this.loadingWarnings = FMLLoader.getLoadingModList().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)
|
||||
.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
|
||||
.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);
|
||||
private static String computeLanguageList() {
|
||||
return "\n" + LanguageLoadingProvider
|
||||
.applyForEach(lp -> lp.name() + '@' + lp.getClass().getPackage().getImplementationVersion())
|
||||
.collect(Collectors.joining("\n\t\t", "\t\t", ""));
|
||||
}
|
||||
|
||||
private 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")+
|
||||
" "+mod.getOrDefault("type","NOTYPE")+
|
||||
" "+mod.getOrDefault("description", "")).
|
||||
return "\n" + mods.stream().map(mod -> mod.getOrDefault("file","nofile") +
|
||||
' ' + mod.getOrDefault("name", "missing") +
|
||||
' ' + mod.getOrDefault("type","NOTYPE") +
|
||||
' ' + mod.getOrDefault("description", "")).
|
||||
collect(Collectors.joining("\n\t\t","\t\t",""));
|
||||
}
|
||||
|
||||
public static ModLoader get()
|
||||
{
|
||||
return INSTANCE == null ? INSTANCE = new ModLoader() : INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run on the primary starting thread by ClientModLoader and ServerModLoader
|
||||
* @param syncExecutor An executor to run tasks on the main thread
|
||||
* @param parallelExecutor An executor to run tasks on a parallel loading thread pool
|
||||
* @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("java_version", ForgeFeature.VersionFeatureTest.forVersionString(IModInfo.DependencySide.SERVER, System.getProperty("java.version")));
|
||||
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));
|
||||
modList.setLoadedMods(Collections.emptyList());
|
||||
loadingStateValid = false;
|
||||
throw new LoadingFailedException(loadingExceptions);
|
||||
}
|
||||
statusConsumer.ifPresent(c->c.accept("Validating features"));
|
||||
List<? extends ForgeFeature.Bound> failedBounds = loadingModList.getMods().stream()
|
||||
.map(ModInfo::getForgeFeatures)
|
||||
.flatMap(Collection::stream)
|
||||
.filter(bound -> !ForgeFeature.testFeature(FMLEnvironment.dist, bound))
|
||||
public static void gatherAndInitializeMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
|
||||
var loadingExceptions = LoadingModList.getErrors().stream()
|
||||
.flatMap(ModLoadingException::fromEarlyException)
|
||||
.toList();
|
||||
ModLoader.LOADING_EXCEPTIONS.addAll(loadingExceptions);
|
||||
LOADING_WARNINGS.addAll(LoadingModList.getBrokenFiles().stream()
|
||||
.map(file -> new ModLoadingWarning(null, ModLoadingStage.VALIDATE, InvalidModIdentifier.identifyJarProblem(file.getFilePath()).orElse("fml.modloading.brokenfile"), file.getFileName()))
|
||||
.toList());
|
||||
Set<IModInfo> erroredModInfos;
|
||||
if (loadingExceptions.isEmpty()) {
|
||||
erroredModInfos = Collections.emptySet();
|
||||
} else {
|
||||
erroredModInfos = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
erroredModInfos.addAll(loadingExceptions.stream().map(ModLoadingException::getModInfo).toList());
|
||||
}
|
||||
LoadingModList.getModFiles().stream()
|
||||
.filter(ModFileInfo::missingLicense)
|
||||
.filter(modFileInfo -> modFileInfo.getMods().stream().noneMatch(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(LOADING_EXCEPTIONS::add);
|
||||
CrashReportCallables.registerCrashCallable("ModLauncher", FMLLoader::getLauncherInfo);
|
||||
CrashReportCallables.registerCrashCallable("ModLauncher launch target", FMLLoader::launcherHandlerName);
|
||||
CrashReportCallables.registerCrashCallable("ModLauncher naming", FMLLoader::getNaming);
|
||||
CrashReportCallables.registerCrashCallable("ModLauncher services", ModLoader::computeModLauncherServiceList);
|
||||
CrashReportCallables.registerCrashCallable("FML Language Providers", ModLoader::computeLanguageList);
|
||||
|
||||
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;
|
||||
FMLLoader.backgroundScanHandler.waitForScanToComplete(periodicTask);
|
||||
if (!LOADING_EXCEPTIONS.isEmpty()) {
|
||||
LOGGER.fatal(CORE, "Error during pre-loading phase", LOADING_EXCEPTIONS.getFirst());
|
||||
STATUS_CONSUMER.accept("ERROR DURING MOD LOADING");
|
||||
ModList.clearLoadedMods();
|
||||
loadingStateValid = false;
|
||||
throw new LoadingFailedException(LOADING_EXCEPTIONS);
|
||||
}
|
||||
var failedBounds = new ArrayList<ForgeFeature.Bound>();
|
||||
for (var mod : LoadingModList.getMods()) {
|
||||
for (var feature : mod.getForgeFeatures()) {
|
||||
if (!ForgeFeature.testFeature(FMLEnvironment.dist, feature))
|
||||
failedBounds.add(feature);
|
||||
}
|
||||
}
|
||||
|
||||
if (!failedBounds.isEmpty()) {
|
||||
LOGGER.fatal(CORE, "Failed to validate feature bounds for mods");
|
||||
modList.setLoadedMods(Collections.emptyList());
|
||||
LOGGER.fatal(CORE, "Failed to validate feature bounds for mods: {}", failedBounds);
|
||||
STATUS_CONSUMER.accept("ERROR DURING MOD LOADING");
|
||||
ModList.clearLoadedMods();
|
||||
loadingStateValid = false;
|
||||
throw new LoadingFailedException(failedBounds.stream()
|
||||
.map(fb -> new ModLoadingException(fb.modInfo(), ModLoadingStage.CONSTRUCT, "fml.modloading.feature.missing", null, fb.featureName(), fb.featureBound(), ForgeFeature.featureValue(fb)))
|
||||
.map(fb -> new ModLoadingException(fb.modInfo(), ModLoadingStage.CONSTRUCT, "fml.modloading.feature.missing", null, fb, ForgeFeature.featureValue(fb)))
|
||||
.toList());
|
||||
}
|
||||
|
||||
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)
|
||||
.toList();
|
||||
if (!loadingExceptions.isEmpty()) {
|
||||
LOGGER.fatal(CORE, "Failed to initialize mod containers", loadingExceptions.get(0));
|
||||
modList.setLoadedMods(Collections.emptyList());
|
||||
final var modContainers = new HashMap<String, ModContainer>();
|
||||
for (var file : LoadingModList.getModFiles()) {
|
||||
var containers = buildMods(file.getFile());
|
||||
for (var container : containers)
|
||||
modContainers.put(container.getModId(), container);
|
||||
}
|
||||
|
||||
if (!LOADING_EXCEPTIONS.isEmpty()) {
|
||||
LOGGER.fatal(CORE, "Failed to initialize mod containers", LOADING_EXCEPTIONS.getFirst());
|
||||
STATUS_CONSUMER.accept("ERROR DURING MOD LOADING");
|
||||
ModList.clearLoadedMods();
|
||||
loadingStateValid = false;
|
||||
throw new LoadingFailedException(loadingExceptions);
|
||||
throw new LoadingFailedException(LOADING_EXCEPTIONS);
|
||||
}
|
||||
modList.setLoadedMods(modContainers);
|
||||
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"));
|
||||
|
||||
// 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());
|
||||
var stateList = ModStateManager.getStates(ModLoadingPhase.GATHER);
|
||||
var progress = StartupMessageManager.addProgressBar("Mod Gather", stateList.stream().mapToInt(mls -> mls.size().getAsInt()).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"));
|
||||
public static void loadMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
|
||||
var stateList = ModStateManager.getStates(ModLoadingPhase.LOAD);
|
||||
var progress = StartupMessageManager.addProgressBar("Mod Loading", stateList.stream().mapToInt(mls -> mls.size().getAsInt()).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())));
|
||||
public static void finishMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) {
|
||||
var stateList = ModStateManager.getStates(ModLoadingPhase.COMPLETE);
|
||||
var progress = StartupMessageManager.addProgressBar("Mod Complete", stateList.stream().mapToInt(mls -> mls.size().getAsInt()).sum());
|
||||
for (IModLoadingState mls : stateList) {
|
||||
dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress);
|
||||
}
|
||||
|
||||
var modBusGroups = Set.of(
|
||||
ModList.getLoadedMods().stream()
|
||||
.map(ModContainer::getModBusGroup)
|
||||
.filter(Objects::nonNull)
|
||||
.toArray(BusGroup[]::new)
|
||||
);
|
||||
for (var modBusGroup : modBusGroups) {
|
||||
parallelExecutor.execute(modBusGroup::trim);
|
||||
}
|
||||
parallelExecutor.execute(BusGroup.DEFAULT::trim);
|
||||
|
||||
STATUS_CONSUMER.accept("Mod loading complete - %d mods loaded".formatted(ModList.size()));
|
||||
progress.complete();
|
||||
}
|
||||
|
||||
private void dispatchAndHandleError(IModLoadingState state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker) {
|
||||
private static 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();
|
||||
if (inlineRunnable != null) handleInlineTransition(inlineRunnable, state, syncExecutor, ticker);
|
||||
|
||||
var transition = state.buildTransition(syncExecutor, parallelExecutor, progressBar);
|
||||
if (transition != null) waitForTransition(state, syncExecutor, ticker, transition);
|
||||
|
||||
COMPLETED_STATES.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 static void handleInlineTransition(final Runnable 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.run();
|
||||
syncExecutor.drive(ticker);
|
||||
pb.complete();
|
||||
syncExecutor.drive(ticker);
|
||||
}
|
||||
|
||||
private void waitForTransition(final IModLoadingState state, final ModWorkManager.DrivenExecutor syncExecutor, final Runnable ticker, final CompletableFuture<Void> transition) {
|
||||
private static 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);
|
||||
STATUS_CONSUMER.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());
|
||||
STATUS_CONSUMER.accept("ERROR DURING MOD LOADING");
|
||||
throw new LoadingFailedException(modLoadingExceptions);
|
||||
}
|
||||
}
|
||||
|
||||
private List<ModContainer> buildMods(final IModFile modFile)
|
||||
{
|
||||
final Map<String, IModInfo> modInfoMap = modFile.getModFileInfo().getMods().stream().collect(Collectors.toMap(IModInfo::getModId, Function.identity()));
|
||||
private static 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());
|
||||
if (containers.size() != modInfoMap.size()) {
|
||||
var modIds = modInfoMap.values().stream().map(IModInfo::getModId).sorted().collect(Collectors.toList());
|
||||
var containerIds = containers.stream().map(c -> c != null ? c.getModId() : "(null)").sorted().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);
|
||||
}
|
||||
|
||||
LOGGER.fatal(LOADING,"File {} constructed {} mods: {}, but had {} mods specified: {}",
|
||||
if (containers.size() != modInfoMap.size()) {
|
||||
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(), containerIds,
|
||||
modInfoMap.size(), modIds);
|
||||
@@ -266,23 +311,26 @@ public class ModLoader
|
||||
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()));
|
||||
LOADING_EXCEPTIONS.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 IModFile modFile, final Map<String, IModInfo> modInfoMap, final Map.Entry<String, ? extends IModLanguageProvider.IModLanguageLoader> idToProviderEntry) {
|
||||
private static 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
|
||||
loadingExceptions.add(mle);
|
||||
LOADING_EXCEPTIONS.add(mle);
|
||||
// return an errored container instance here, because we tried and failed building a container.
|
||||
return new ErroredModContainer();
|
||||
}
|
||||
@@ -293,52 +341,71 @@ 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 <T extends Event & IModBusEvent> void runEventGenerator(Function<ModContainer, T> generator) {
|
||||
public static boolean hasCompletedState(IModLoadingState state) {
|
||||
return COMPLETED_STATES.contains(state);
|
||||
}
|
||||
|
||||
public static <T extends IModBusEvent> void runEventGenerator(Function<ModContainer, T> generator) {
|
||||
if (!loadingStateValid) {
|
||||
LOGGER.error("Cowardly refusing to send event generator to a broken mod state");
|
||||
return;
|
||||
}
|
||||
ModList.get().forEachModInOrder(mc -> mc.acceptEvent(generator.apply(mc)));
|
||||
for (var mod : ModList.getLoadedMods())
|
||||
mod.acceptEvent(generator.apply(mod));
|
||||
}
|
||||
|
||||
public <T extends Event & IModBusEvent> void postEvent(T e) {
|
||||
public static <T extends IModBusEvent> void postEvent(T e) {
|
||||
if (!loadingStateValid) {
|
||||
LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName());
|
||||
return;
|
||||
}
|
||||
ModList.get().forEachModInOrder(mc -> mc.acceptEvent(e));
|
||||
for (var mod : ModList.getLoadedMods())
|
||||
mod.acceptEvent(e);
|
||||
}
|
||||
public <T extends Event & IModBusEvent> T postEventWithReturn(T e) {
|
||||
|
||||
public static <T extends IModBusEvent> void postEvent(ModContainer mod, T e) {
|
||||
mod.acceptEvent(e);
|
||||
}
|
||||
|
||||
public static <T extends 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));
|
||||
for (var mod : ModList.getLoadedMods())
|
||||
mod.acceptEvent(e);
|
||||
return e;
|
||||
}
|
||||
public <T extends Event & IModBusEvent> void postEventWithWrapInModOrder(T e, BiConsumer<ModContainer, T> pre, BiConsumer<ModContainer, T> post) {
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
public static <T extends IModBusEvent> void postEventWrapContainerInModOrder(T event) {
|
||||
postEventWithWrapInModOrder(event,
|
||||
(mc, _) -> ModLoadingContext.get().setActiveContainer(mc),
|
||||
(_, _) -> ModLoadingContext.get().setActiveContainer(null)
|
||||
);
|
||||
}
|
||||
|
||||
public static <T extends 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);
|
||||
});
|
||||
for (var mod : ModList.getLoadedMods()) {
|
||||
pre.accept(mod, e);
|
||||
mod.acceptEvent(e);
|
||||
post.accept(mod, e);
|
||||
}
|
||||
}
|
||||
|
||||
public List<ModLoadingWarning> getWarnings()
|
||||
{
|
||||
return ImmutableList.copyOf(this.loadingWarnings);
|
||||
public static List<ModLoadingWarning> getWarnings() {
|
||||
return List.copyOf(LOADING_WARNINGS);
|
||||
}
|
||||
|
||||
public void addWarning(ModLoadingWarning warning)
|
||||
{
|
||||
this.loadingWarnings.add(warning);
|
||||
public static void addWarning(ModLoadingWarning warning) {
|
||||
LOADING_WARNINGS.add(warning);
|
||||
}
|
||||
|
||||
private static boolean runningDataGen = false;
|
||||
@@ -347,7 +414,7 @@ public class ModLoader
|
||||
return runningDataGen;
|
||||
}
|
||||
|
||||
private static class ErroredModContainer extends ModContainer {
|
||||
private static final class ErroredModContainer extends ModContainer {
|
||||
public ErroredModContainer() {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -8,38 +8,49 @@ package net.minecraftforge.fml;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import net.minecraftforge.fml.config.IConfigSpec;
|
||||
import net.minecraftforge.fml.config.ModConfig;
|
||||
import net.minecraftforge.fml.loading.FMLLoader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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. Example for Java mods: {@snippet :
|
||||
* @Mod("examplemod")
|
||||
* public final class ExampleMod {
|
||||
* public ExampleMod(FMLJavaModLoadingContext context) {
|
||||
* // Use context from the param instead of FMLJavaModLoadingContext.get() or ModLoadingContext.get()
|
||||
* }
|
||||
* }}
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since="1.21.1")
|
||||
public static ModLoadingContext get() {
|
||||
return context.get();
|
||||
}
|
||||
|
||||
private ModContainer activeContainer;
|
||||
|
||||
public void setActiveContainer(final ModContainer container) {
|
||||
void setActiveContainer(final ModContainer container) {
|
||||
this.activeContainer = container;
|
||||
this.languageExtension = container == null ? null : container.contextExtension.get();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public ModContainer getContainer() {
|
||||
return activeContainer == null
|
||||
? ModList.getModContainerById("minecraft").orElseThrow(()->new RuntimeException("Where is minecraft???!"))
|
||||
: activeContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an {@link IExtensionPoint} with the mod container.
|
||||
* @param point The extension point to register
|
||||
@@ -47,32 +58,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) {
|
||||
if (spec.isEmpty())
|
||||
{
|
||||
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, getActiveContainer().getModId());
|
||||
LOGGER.debug("Attempted to register an empty config for type {} on mod {}", type, getContainer().getModId());
|
||||
return;
|
||||
}
|
||||
|
||||
getActiveContainer().addConfig(new ModConfig(type, spec, getActiveContainer()));
|
||||
getContainer().addConfig(new ModConfig(type, spec, getContainer()));
|
||||
}
|
||||
|
||||
public void registerConfig(ModConfig.Type type, IConfigSpec<?> spec, String fileName) {
|
||||
if (spec.isEmpty())
|
||||
{
|
||||
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, getActiveContainer().getModId(), fileName);
|
||||
LOGGER.debug("Attempted to register an empty config for type {} on mod {} using file name {}", type, getContainer().getModId(), fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
getActiveContainer().addConfig(new ModConfig(type, spec, getActiveContainer(), fileName));
|
||||
getContainer().addConfig(new ModConfig(type, spec, getContainer(), fileName));
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T extension() {
|
||||
return (T)languageExtension;
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
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.IntSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link IModLoadingState} interface.
|
||||
@@ -24,18 +25,26 @@ import java.util.function.Function;
|
||||
* @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,
|
||||
ModLoadingPhase phase,
|
||||
Optional<Consumer<ModList>> inlineRunnable,
|
||||
Optional<IModStateTransition> transition) implements IModLoadingState {
|
||||
@NullMarked
|
||||
public record ModLoadingState(
|
||||
String name,
|
||||
String previous,
|
||||
Supplier<String> message,
|
||||
IntSupplier size,
|
||||
ModLoadingPhase phase,
|
||||
@Nullable Runnable inlineRunnable,
|
||||
@Nullable IModStateTransition transition
|
||||
) implements IModLoadingState {
|
||||
@Override
|
||||
public <T extends Event & IModBusEvent>
|
||||
Optional<CompletableFuture<Void>> buildTransition(final Executor syncExecutor,
|
||||
final Executor parallelExecutor,
|
||||
final Function<Executor, CompletableFuture<Void>> preSyncTask,
|
||||
final Function<Executor, CompletableFuture<Void>> postSyncTask) {
|
||||
return transition.map(t -> t.build(syncExecutor, parallelExecutor, preSyncTask, postSyncTask));
|
||||
public @Nullable CompletableFuture<Void> buildTransition(
|
||||
final Executor syncExecutor,
|
||||
final Executor parallelExecutor,
|
||||
final ProgressMeter progressBar,
|
||||
final Function<Executor, CompletableFuture<Void>> preSyncTask,
|
||||
final Function<Executor, CompletableFuture<Void>> postSyncTask
|
||||
) {
|
||||
if (transition == null) return null;
|
||||
return transition.build(name, syncExecutor, parallelExecutor, progressBar, preSyncTask, postSyncTask);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,9 +54,12 @@ public record ModLoadingState(String name, String previous,
|
||||
* @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 new ModLoadingState(name, previous, () -> "", () -> 0, phase, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,11 +70,13 @@ public record ModLoadingState(String name, String previous,
|
||||
* @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
|
||||
* @return a mod loading state with state transition information and a default message
|
||||
*
|
||||
* @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 new ModLoadingState(name, previous, ml -> "Processing transition " + name, phase, Optional.empty(), Optional.of(transition));
|
||||
return of(name, phase).after(previous).withTransition(transition);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,11 +88,14 @@ public record ModLoadingState(String name, String previous,
|
||||
* @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 Supplier<String> message, final ModLoadingPhase phase,
|
||||
final IModStateTransition transition) {
|
||||
return new ModLoadingState(name, previous, message, phase, Optional.empty(), Optional.of(transition));
|
||||
return of(name, phase).after(previous).message(message).withTransition(transition);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,9 +107,75 @@ public record ModLoadingState(String name, String previous,
|
||||
* @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 new ModLoadingState(name, previous, ml -> "Processing work " + name, phase, Optional.of(inline), Optional.empty());
|
||||
final Runnable inline) {
|
||||
return of(name, phase).after(previous).withInline(inline);
|
||||
}
|
||||
}
|
||||
|
||||
public static Builder of(final String name, final ModLoadingPhase phase) {
|
||||
return new Builder(name, phase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof ModLoadingState that
|
||||
&& this.phase == that.phase
|
||||
&& this.name.equals(that.name)
|
||||
&& this.previous.equals(that.previous);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = phase.hashCode();
|
||||
result = 31 * result + name.hashCode();
|
||||
result = 31 * result + previous.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final String name;
|
||||
private final ModLoadingPhase phase;
|
||||
private String after = "";
|
||||
private @Nullable Supplier<String> message = null;
|
||||
private @Nullable IntSupplier 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(() -> value); }
|
||||
public Builder message(final Supplier<String> value) {
|
||||
this.message = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder size(final int value) { return size(() -> value); }
|
||||
public Builder size(final IntSupplier value) {
|
||||
this.size = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ModLoadingState empty() {
|
||||
return new ModLoadingState(name, after, message != null ? message : () -> "", size != null ? size : () -> 0, phase, null, null);
|
||||
}
|
||||
|
||||
public ModLoadingState withTransition(final IModStateTransition transition) {
|
||||
return new ModLoadingState(name, after, message != null ? message : () -> "Processing transition " + name, size != null ? size : ModList::size, phase, null, transition);
|
||||
}
|
||||
|
||||
public ModLoadingState withInline(final Runnable inline) {
|
||||
return new ModLoadingState(name, after, message != null ? message : () -> "Processing work " + name, size != null ? size : () -> 0, phase, inline, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,42 +5,27 @@
|
||||
|
||||
package net.minecraftforge.fml;
|
||||
|
||||
import com.google.common.collect.Streams;
|
||||
import net.minecraftforge.forgespi.language.IModInfo;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ModLoadingWarning
|
||||
{
|
||||
/**
|
||||
* Mod Info for mod with warning
|
||||
*/
|
||||
private final IModInfo modInfo;
|
||||
/**
|
||||
* The stage where this warning was encountered
|
||||
*/
|
||||
private final ModLoadingStage warningStage;
|
||||
|
||||
/**
|
||||
* I18N message to use for display
|
||||
*/
|
||||
private final String i18nMessage;
|
||||
|
||||
/**
|
||||
* Context for message display
|
||||
*/
|
||||
private final List<Object> context;
|
||||
|
||||
/**
|
||||
* @param modInfo Mod info for mod with warning
|
||||
* @param warningStage The stage where this warning was encountered
|
||||
* @param i18nMessage I18N message to use for display
|
||||
* @param context Context for message display
|
||||
*/
|
||||
public record ModLoadingWarning(IModInfo modInfo, ModLoadingStage warningStage, String i18nMessage, List<Object> context) {
|
||||
public ModLoadingWarning(final IModInfo modInfo, final ModLoadingStage warningStage, final String i18nMessage, Object... context) {
|
||||
this.modInfo = modInfo;
|
||||
this.warningStage = warningStage;
|
||||
this.i18nMessage = i18nMessage;
|
||||
this.context = Arrays.asList(context);
|
||||
this(modInfo, warningStage, i18nMessage, List.of(context));
|
||||
}
|
||||
|
||||
public ModLoadingWarning {
|
||||
context = List.copyOf(context);
|
||||
}
|
||||
|
||||
public String formatToString() {
|
||||
return Bindings.getMessageParser().get().parseMessage(i18nMessage, Streams.concat(Stream.of(modInfo, warningStage), context.stream()).toArray());
|
||||
return Bindings.getMessageParser().get().parseMessage(i18nMessage, Stream.concat(Stream.of(modInfo, warningStage), context.stream()).toArray());
|
||||
}
|
||||
}
|
||||
@@ -15,20 +15,19 @@ import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public class ModStateManager {
|
||||
static ModStateManager INSTANCE;
|
||||
private final EnumMap<ModLoadingPhase, List<IModLoadingState>> stateMap;
|
||||
public final class ModStateManager {
|
||||
private ModStateManager() {}
|
||||
private static final EnumMap<ModLoadingPhase, List<IModLoadingState>> stateMap;
|
||||
|
||||
public ModStateManager() {
|
||||
INSTANCE = this;
|
||||
static {
|
||||
final var sp = ServiceLoader.load(FMLLoader.getGameLayer(), IModStateProvider.class);
|
||||
this.stateMap = ServiceLoaderUtils.streamWithErrorHandling(sp, sce->{})
|
||||
stateMap = ServiceLoaderUtils.streamWithErrorHandling(sp, _ -> {})
|
||||
.map(IModStateProvider::getAllStates)
|
||||
.<IModLoadingState>mapMulti(Iterable::forEach)
|
||||
.collect(Collectors.groupingBy(IModLoadingState::phase, ()->new EnumMap<>(ModLoadingPhase.class), Collectors.toUnmodifiableList()));
|
||||
.flatMap(List::stream)
|
||||
.collect(Collectors.groupingBy(IModLoadingState::phase, () -> new EnumMap<>(ModLoadingPhase.class), Collectors.toUnmodifiableList()));
|
||||
}
|
||||
|
||||
public List<IModLoadingState> getStates(final ModLoadingPhase phase) {
|
||||
public static List<IModLoadingState> getStates(final ModLoadingPhase phase) {
|
||||
var nodes = stateMap.get(phase);
|
||||
var lookup = nodes.stream().collect(Collectors.toMap(IModLoadingState::name, Function.identity()));
|
||||
|
||||
@@ -36,7 +35,20 @@ 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 static IModLoadingState findState(final String stateName) {
|
||||
var result = stateMap.values()
|
||||
.stream()
|
||||
.flatMap(List::stream)
|
||||
.filter(mls -> mls.name().equals(stateName))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (result != null) return result;
|
||||
else throw new IllegalArgumentException("Unknown IModLoadingState: " + stateName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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.Objects;
|
||||
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.fml.IModStateTransition.EventGenerator;
|
||||
import net.minecraftforge.fml.event.IModBusEvent;
|
||||
import net.minecraftforge.fml.loading.progress.ProgressMeter;
|
||||
|
||||
@ApiStatus.Internal
|
||||
final 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(Objects::nonNull).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;
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as [#completableFutureFromExceptionList(List)] but specialised for a single future
|
||||
static <V> CompletionStage<Void> completableFutureFromException(FutureResult<V> t) {
|
||||
if (t.exception() == null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
} else {
|
||||
var exception = t.exception();
|
||||
CompletableFuture<Void> cf = new CompletableFuture<>();
|
||||
RuntimeException accumulator = new RuntimeException();
|
||||
cf.completeExceptionally(accumulator);
|
||||
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);
|
||||
@SuppressWarnings("unchecked") // Bypass a weird javac error with generic by forcing it to be the exact type we want.
|
||||
var raw = ((CompletableFuture<V>)future);
|
||||
results[i] = raw.whenComplete((result, exception) -> list.set(i, new FutureResult<>(result, exception)));
|
||||
}
|
||||
|
||||
return CompletableFuture.allOf(results).handle((_, _)->null).thenApply(_ -> list);
|
||||
}
|
||||
|
||||
private static <T extends IModBusEvent> CompletableFuture<Void> addCompletableFutureTaskForModDispatch(
|
||||
final Executor executor,
|
||||
final ProgressMeter progressBar,
|
||||
final EventGenerator<T> eventGenerator,
|
||||
final BiFunction<ModLoadingStage, Throwable, ModLoadingStage> nextState
|
||||
) {
|
||||
var modFutures = new LinkedHashMap<String, CompletableFuture<Void>>();
|
||||
for (var mod : ModList.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((_, exception) -> {
|
||||
mod.modLoadingStage = nextState.apply(mod.modLoadingStage, exception);
|
||||
progressBar.increment();
|
||||
ModLoadingContext.get().setActiveContainer(null);
|
||||
});
|
||||
|
||||
modFutures.put(mod.getModId(), dispatch);
|
||||
}
|
||||
|
||||
return gather(modFutures.values()).thenComposeAsync(ModStateTransitionHelper::completableFutureFromExceptionList, executor);
|
||||
}
|
||||
|
||||
static <T extends 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
|
||||
) {
|
||||
CompletableFuture<Void> future;
|
||||
final var executor = transition.threadSelector().apply(syncExecutor, parallelExecutor);
|
||||
|
||||
final CompletableFuture<Void> preSyncTaskCF = preSyncTask.apply(syncExecutor);
|
||||
CompletableFuture<Void> postEventDispatchCF = preSyncTaskCF
|
||||
.thenRunAsync(() -> progressBar.label(progressBar.name() + ": dispatching " + name), parallelExecutor);
|
||||
|
||||
var eventGenerator = transition.eventFunction();
|
||||
if (eventGenerator != null) {
|
||||
BiFunction<ModLoadingStage, Throwable, ModLoadingStage> state = transition.nextModLoadingStage();
|
||||
future = addCompletableFutureTaskForModDispatch(executor, progressBar, eventGenerator, state);
|
||||
|
||||
final CompletableFuture<Void> eventDispatchCF = future.handle(FutureResult::new)
|
||||
.thenCompose(ModStateTransitionHelper::completableFutureFromException);
|
||||
postEventDispatchCF = postEventDispatchCF.thenComposeAsync(_ -> eventDispatchCF, parallelExecutor);
|
||||
}
|
||||
|
||||
postEventDispatchCF = postEventDispatchCF.thenRun(() -> postSyncTask.apply(syncExecutor));
|
||||
|
||||
return transition.finalActivityGenerator().apply(syncExecutor, postEventDispatchCF);
|
||||
}
|
||||
}
|
||||
@@ -14,29 +14,20 @@ import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import static net.minecraftforge.fml.Logging.LOADING;
|
||||
|
||||
public class ModWorkManager {
|
||||
public final 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();
|
||||
}
|
||||
} else {
|
||||
// park for a bit so other threads can schedule
|
||||
LockSupport.parkNanos(PARK_TIME);
|
||||
}
|
||||
}
|
||||
public sealed interface DrivenExecutor extends Executor {
|
||||
void drive(Runnable ticker);
|
||||
}
|
||||
private static class SyncExecutor implements DrivenExecutor {
|
||||
private ConcurrentLinkedDeque<Runnable> tasks = new ConcurrentLinkedDeque<>();
|
||||
|
||||
@Override
|
||||
public boolean driveOne() {
|
||||
private record SyncExecutor(ConcurrentLinkedDeque<Runnable> tasks) implements DrivenExecutor {
|
||||
public SyncExecutor() {
|
||||
this(new ConcurrentLinkedDeque<>());
|
||||
}
|
||||
|
||||
private boolean driveOne() {
|
||||
final Runnable task = tasks.pollFirst();
|
||||
if (task != null) {
|
||||
task.run();
|
||||
@@ -44,45 +35,36 @@ public class ModWorkManager {
|
||||
return task != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean selfDriven() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final Runnable command) {
|
||||
tasks.addLast(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drive(Runnable ticker) {
|
||||
ticker.run();
|
||||
while (true) {
|
||||
if (!driveOne()) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class WrappingExecutor implements DrivenExecutor {
|
||||
private final Executor wrapped;
|
||||
|
||||
public WrappingExecutor(final Executor executor) {
|
||||
this.wrapped = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean selfDriven() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean driveOne() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private record WrappingExecutor(Executor wrapped) implements DrivenExecutor {
|
||||
@Override
|
||||
public void execute(final Runnable command) {
|
||||
wrapped.execute(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drive(Runnable ticker) {
|
||||
// Park for a bit so other threads can schedule
|
||||
LockSupport.parkNanos(PARK_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
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 +72,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 +84,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;
|
||||
@@ -43,7 +43,7 @@ public final class OptionalMod<T>
|
||||
private T getValue()
|
||||
{
|
||||
if (!searched) {
|
||||
this.value = ModList.get().<T>getModObjectById(this.modId).orElse(null);
|
||||
this.value = ModList.<T>getModObjectById(this.modId).orElse(null);
|
||||
searched = true;
|
||||
}
|
||||
return this.value;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import java.util.concurrent.Executor;
|
||||
import java.util.function.BinaryOperator;
|
||||
|
||||
public enum ThreadSelector implements BinaryOperator<Executor> {
|
||||
SYNC((sync, parallel) -> sync),
|
||||
PARALLEL((sync, parallel) -> parallel);
|
||||
SYNC((sync, _) -> sync),
|
||||
PARALLEL((_, parallel) -> parallel);
|
||||
|
||||
private final BinaryOperator<Executor> selector;
|
||||
|
||||
|
||||
@@ -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.getModContainerById("minecraft");
|
||||
var forge = ModList.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);
|
||||
}
|
||||
@@ -119,15 +109,9 @@ public class VersionChecker
|
||||
*/
|
||||
private String openUrlString(URL url, IModInfo mod) throws IOException, URISyntaxException, InterruptedException {
|
||||
URL currentUrl = url;
|
||||
var userAgent = this.agent + ' ' + mod.getModId() + '/' + mod.getVersion();
|
||||
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append("Java-http-client/").append(System.getProperty("java.version")).append(' ');
|
||||
sb.append("MinecraftForge/").append(FMLLoader.versionInfo().mcAndForgeVersion()).append(' ');
|
||||
sb.append(mod.getModId()).append('/').append(mod.getVersion());
|
||||
final String userAgent = sb.toString();
|
||||
|
||||
for (int redirects = 0; redirects < MAX_HTTP_REDIRECTS; redirects++)
|
||||
{
|
||||
for (int redirects = 0; redirects < MAX_HTTP_REDIRECTS; redirects++) {
|
||||
var request = HttpRequest.newBuilder()
|
||||
.uri(currentUrl.toURI())
|
||||
.timeout(Duration.ofSeconds(HTTP_TIMEOUT_SECS))
|
||||
@@ -139,8 +123,7 @@ public class VersionChecker
|
||||
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);
|
||||
@@ -150,26 +133,22 @@ 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());
|
||||
@@ -178,54 +157,40 @@ public class VersionChecker
|
||||
|
||||
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);
|
||||
@@ -233,52 +198,42 @@ 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()) {
|
||||
for (IModInfo info : ModList.getMods()) {
|
||||
if (info.getUpdateURL().isPresent())
|
||||
ret.add(info);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,6 +14,7 @@ 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.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -25,8 +26,65 @@ import static net.minecraftforge.fml.config.ConfigTracker.CONFIG;
|
||||
|
||||
public class ConfigFileTypeHandler {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
static ConfigFileTypeHandler TOML = new ConfigFileTypeHandler();
|
||||
private static final Path defaultConfigPath = FMLPaths.GAMEDIR.get().resolve(FMLConfig.defaultConfigPath());
|
||||
private static final Path defaultConfigPath = FMLPaths.GAMEDIR.get().resolve(FMLConfig.getConfigValue(FMLConfig.ConfigValue.DEFAULT_CONFIG_PATH));
|
||||
|
||||
private static final ConfigFileTypeHandler CLIENT = new ConfigFileTypeHandler(ModConfig.Type.CLIENT);
|
||||
private static final ConfigFileTypeHandler COMMON = new ConfigFileTypeHandler(ModConfig.Type.COMMON);
|
||||
private static final ConfigFileTypeHandler SERVER = new ConfigFileTypeHandler(ModConfig.Type.SERVER);
|
||||
|
||||
private final @Nullable ModConfig.Type type;
|
||||
private @Nullable FileWatcher watcher;
|
||||
|
||||
// exists for bin compat
|
||||
public ConfigFileTypeHandler() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
private ConfigFileTypeHandler(@Nullable ModConfig.Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the handler for the given {@link ModConfig.Type}.
|
||||
*
|
||||
* @param type The type to get the handler for
|
||||
* @return The handler
|
||||
*/
|
||||
static ConfigFileTypeHandler get(ModConfig.Type type) {
|
||||
return switch (type) {
|
||||
case CLIENT -> CLIENT;
|
||||
case COMMON -> COMMON;
|
||||
case SERVER -> SERVER;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link FileWatcher} for this handler, creating it if it doesn't exist and/or has been stopped.
|
||||
*
|
||||
* @return The watcher
|
||||
*
|
||||
* @apiNote This is package-private so modders can't just call {@link FileWatcher#stop()} and fuck up everything.
|
||||
*/
|
||||
FileWatcher getWatcher() {
|
||||
if (this.watcher == null) {
|
||||
LOGGER.debug(CONFIG, "Starting watcher for handler: {}", this);
|
||||
this.watcher = new FileWatcher();
|
||||
}
|
||||
|
||||
return this.watcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the {@link FileWatcher} for this handler, and sets it to null afterward. Use this instead of
|
||||
* {@link FileWatcher#stop()}.
|
||||
*/
|
||||
void stopWatcher() {
|
||||
if (this.watcher == null) return;
|
||||
|
||||
LOGGER.debug(CONFIG, "Stopping watcher for hander: {}", this);
|
||||
this.watcher.stop();
|
||||
this.watcher = null;
|
||||
}
|
||||
|
||||
public Function<ModConfig, CommentedFileConfig> reader(Path configBasePath) {
|
||||
return (c) -> {
|
||||
@@ -37,7 +95,7 @@ public class ConfigFileTypeHandler {
|
||||
onFileNotFound((newfile, configFormat)-> setupConfigFile(c, newfile, configFormat)).
|
||||
writingMode(WritingMode.REPLACE).
|
||||
build();
|
||||
LOGGER.debug(CONFIG, "Built TOML config for {}", configPath.toString());
|
||||
LOGGER.debug(CONFIG, "Built TOML config for {}", configPath);
|
||||
try
|
||||
{
|
||||
configData.load();
|
||||
@@ -46,13 +104,9 @@ 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);
|
||||
}
|
||||
LOGGER.debug(CONFIG, "Loaded TOML config file {}", configPath);
|
||||
this.getWatcher().addWatch(configPath, new ConfigWatcher(c, configData, Thread.currentThread().getContextClassLoader()));
|
||||
LOGGER.debug(CONFIG, "Watching TOML config file {} for changes", configPath);
|
||||
return configData;
|
||||
};
|
||||
}
|
||||
@@ -60,14 +114,16 @@ public class ConfigFileTypeHandler {
|
||||
public void unload(Path configBasePath, ModConfig config) {
|
||||
Path configPath = configBasePath.resolve(config.getFileName());
|
||||
try {
|
||||
FileWatcher.defaultInstance().removeWatch(configBasePath.resolve(config.getFileName()));
|
||||
this.getWatcher().removeWatch(configPath);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.error("Failed to remove config {} from tracker!", configPath.toString(), e);
|
||||
LOGGER.error("Failed to remove config {} from tracker!", configPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
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 +167,16 @@ 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;
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ConfigFileTypeHandler[" + (type != null ? type : "UNKNOWN") + "]";
|
||||
}
|
||||
|
||||
private record ConfigWatcher(
|
||||
ModConfig modConfig,
|
||||
CommentedFileConfig commentedFileConfig,
|
||||
ClassLoader realClassLoader
|
||||
) implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
// Force the regular classloader onto the special thread
|
||||
|
||||
@@ -8,6 +8,8 @@ package net.minecraftforge.fml.config;
|
||||
import com.electronwill.nightconfig.core.CommentedConfig;
|
||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import net.minecraftforge.fml.loading.FMLEnvironment;
|
||||
import net.minecraftforge.fml.loading.FMLPaths;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.Marker;
|
||||
import org.slf4j.MarkerFactory;
|
||||
@@ -16,46 +18,60 @@ import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class ConfigTracker {
|
||||
public final class ConfigTracker {
|
||||
private ConfigTracker() {}
|
||||
|
||||
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;
|
||||
private final ConcurrentHashMap<String, Map<ModConfig.Type, ModConfig>> configsByMod;
|
||||
private static final ConcurrentHashMap<String, ModConfig> FILE_MAP = new ConcurrentHashMap<>();
|
||||
private static final Map<ModConfig.Type, Set<ModConfig>> CONFIG_SETS = Map.of(
|
||||
ModConfig.Type.CLIENT, Collections.synchronizedSet(new LinkedHashSet<>()),
|
||||
ModConfig.Type.COMMON, Collections.synchronizedSet(new LinkedHashSet<>()),
|
||||
ModConfig.Type.SERVER, Collections.synchronizedSet(new LinkedHashSet<>())
|
||||
);
|
||||
private static final ConcurrentHashMap<String, Map<ModConfig.Type, ModConfig>> CONFIGS_BY_MOD = new ConcurrentHashMap<>();
|
||||
|
||||
private ConfigTracker() {
|
||||
this.fileMap = new ConcurrentHashMap<>();
|
||||
this.configSets = new EnumMap<>(ModConfig.Type.class);
|
||||
this.configsByMod = new ConcurrentHashMap<>();
|
||||
this.configSets.put(ModConfig.Type.CLIENT, Collections.synchronizedSet(new LinkedHashSet<>()));
|
||||
this.configSets.put(ModConfig.Type.COMMON, Collections.synchronizedSet(new LinkedHashSet<>()));
|
||||
// this.configSets.put(ModConfig.Type.PLAYER, new ConcurrentSkipListSet<>());
|
||||
this.configSets.put(ModConfig.Type.SERVER, Collections.synchronizedSet(new LinkedHashSet<>()));
|
||||
}
|
||||
|
||||
void trackConfig(final ModConfig config) {
|
||||
if (this.fileMap.containsKey(config.getFileName())) {
|
||||
LOGGER.error(CONFIG,"Detected config file conflict {} between {} and {}", config.getFileName(), this.fileMap.get(config.getFileName()).getModId(), config.getModId());
|
||||
static void trackConfig(final ModConfig config) {
|
||||
if (FILE_MAP.containsKey(config.getFileName())) {
|
||||
LOGGER.error(CONFIG,"Detected config file conflict {} between {} and {}", config.getFileName(), FILE_MAP.get(config.getFileName()).getModId(), config.getModId());
|
||||
throw new RuntimeException("Config conflict detected!");
|
||||
}
|
||||
this.fileMap.put(config.getFileName(), config);
|
||||
this.configSets.get(config.getType()).add(config);
|
||||
this.configsByMod.computeIfAbsent(config.getModId(), (k)->new EnumMap<>(ModConfig.Type.class)).put(config.getType(), config);
|
||||
FILE_MAP.put(config.getFileName(), config);
|
||||
CONFIG_SETS.get(config.getType()).add(config);
|
||||
CONFIGS_BY_MOD.computeIfAbsent(config.getModId(), _ -> new EnumMap<>(ModConfig.Type.class)).put(config.getType(), config);
|
||||
LOGGER.debug(CONFIG, "Config file {} for {} tracking", config.getFileName(), config.getModId());
|
||||
}
|
||||
|
||||
public void loadConfigs(ModConfig.Type type, Path configBasePath) {
|
||||
public static 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 : CONFIG_SETS.get(type)) {
|
||||
openConfig(config, configBasePath);
|
||||
}
|
||||
}
|
||||
|
||||
public void unloadConfigs(ModConfig.Type type, Path configBasePath) {
|
||||
// TODO: [FML] This is only called for the server (outside of forceUnload)
|
||||
// rethink config implementation for eventual FML rewrite
|
||||
public static 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 : CONFIG_SETS.get(type)) {
|
||||
closeConfig(config, configBasePath);
|
||||
}
|
||||
ConfigFileTypeHandler.get(type).stopWatcher();
|
||||
}
|
||||
|
||||
private void openConfig(final ModConfig config, final Path configBasePath) {
|
||||
// If there is a better way to do this, please tell me. Because this is FUCKED
|
||||
public static void forceUnload() {
|
||||
// This is how ModStateProvider handles loading configs. So we're doing the same but with unloadConfigs instead
|
||||
if (FMLEnvironment.dist.isClient())
|
||||
unloadConfigs(ModConfig.Type.CLIENT, FMLPaths.CONFIGDIR.get());
|
||||
|
||||
unloadConfigs(ModConfig.Type.COMMON, FMLPaths.CONFIGDIR.get());
|
||||
|
||||
// just in case server watcher is still alive somehow...
|
||||
ConfigFileTypeHandler.get(ModConfig.Type.SERVER).stopWatcher();
|
||||
}
|
||||
|
||||
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,7 +79,7 @@ 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());
|
||||
// stop the filewatcher before we save the file and close it, so reload doesn't fire
|
||||
@@ -76,8 +92,8 @@ public class ConfigTracker {
|
||||
}
|
||||
}
|
||||
|
||||
public void loadDefaultServerConfigs() {
|
||||
configSets.get(ModConfig.Type.SERVER).forEach(modConfig -> {
|
||||
public static void loadDefaultServerConfigs() {
|
||||
CONFIG_SETS.get(ModConfig.Type.SERVER).forEach(modConfig -> {
|
||||
final CommentedConfig commentedConfig = CommentedConfig.inMemory();
|
||||
modConfig.getSpec().correct(commentedConfig);
|
||||
modConfig.setConfigData(commentedConfig);
|
||||
@@ -85,16 +101,16 @@ public class ConfigTracker {
|
||||
});
|
||||
}
|
||||
|
||||
public String getConfigFileName(String modId, ModConfig.Type type) {
|
||||
return Optional.ofNullable(configsByMod.getOrDefault(modId, Collections.emptyMap()).getOrDefault(type, null)).
|
||||
public static String getConfigFileName(String modId, ModConfig.Type type) {
|
||||
return Optional.ofNullable(CONFIGS_BY_MOD.getOrDefault(modId, Collections.emptyMap()).getOrDefault(type, null)).
|
||||
map(ModConfig::getFullPath).map(Object::toString).orElse(null);
|
||||
}
|
||||
|
||||
public Map<ModConfig.Type, Set<ModConfig>> configSets() {
|
||||
return configSets;
|
||||
public static Map<ModConfig.Type, Set<ModConfig>> configSets() {
|
||||
return CONFIG_SETS;
|
||||
}
|
||||
|
||||
public ConcurrentHashMap<String, ModConfig> fileMap() {
|
||||
return fileMap;
|
||||
public static ConcurrentHashMap<String, ModConfig> fileMap() {
|
||||
return FILE_MAP;
|
||||
}
|
||||
}
|
||||
@@ -5,22 +5,17 @@
|
||||
|
||||
package net.minecraftforge.fml.config;
|
||||
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.eventbus.api.event.InheritableEvent;
|
||||
import net.minecraftforge.fml.Bindings;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public interface IConfigEvent {
|
||||
record ConfigConfig(Function<ModConfig, IConfigEvent> loading, Function<ModConfig, IConfigEvent> reloading, @Nullable Function<ModConfig, IConfigEvent> unloading) {
|
||||
@Deprecated(since = "1.19.3", forRemoval = true)
|
||||
@ApiStatus.Internal
|
||||
ConfigConfig(Function<ModConfig, IConfigEvent> loading, Function<ModConfig, IConfigEvent> reloading) {
|
||||
this(loading, reloading, null);
|
||||
}
|
||||
}
|
||||
// TODO: [FML] Get rid of this indirection somehow
|
||||
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) {
|
||||
@@ -35,7 +30,7 @@ public interface IConfigEvent {
|
||||
ModConfig getConfig();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
default <T extends Event & IConfigEvent> T self() {
|
||||
default <T extends InheritableEvent & IConfigEvent> T self() {
|
||||
return (T) this;
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,6 @@ import net.minecraftforge.fml.loading.StringUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class ModConfig
|
||||
{
|
||||
@@ -24,15 +22,14 @@ 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;
|
||||
this.spec = spec;
|
||||
this.fileName = fileName;
|
||||
this.container = container;
|
||||
this.configHandler = ConfigFileTypeHandler.TOML;
|
||||
ConfigTracker.INSTANCE.trackConfig(this);
|
||||
this.configHandler = ConfigFileTypeHandler.get(type);
|
||||
ConfigTracker.trackConfig(this);
|
||||
}
|
||||
|
||||
public ModConfig(final Type type, final IConfigSpec<?> spec, final ModContainer activeContainer) {
|
||||
@@ -41,8 +38,9 @@ public class ModConfig
|
||||
|
||||
private static String defaultConfigName(Type type, String modId) {
|
||||
// config file name would be "forge-client.toml" and "forge-server.toml"
|
||||
return String.format(Locale.ROOT, "%s-%s.toml", modId, type.extension());
|
||||
return modId + '-' + type.extension() + ".toml";
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,33 @@
|
||||
|
||||
package net.minecraftforge.fml.event;
|
||||
|
||||
import net.minecraftforge.eventbus.api.bus.BusGroup;
|
||||
import net.minecraftforge.eventbus.api.bus.EventBus;
|
||||
import net.minecraftforge.eventbus.api.event.InheritableEvent;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* Marker interface for events dispatched on the ModLifecycle event bus instead of the primary event bus
|
||||
* Marker interface for mod lifecycle events dispatched on the
|
||||
* {@linkplain net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext#getModBusGroup() mod BusGroup} instead
|
||||
* of the {@linkplain BusGroup#DEFAULT default BusGroup}.
|
||||
*
|
||||
* @apiNote Each mod gets its own unique mod BusGroup instance, obtained from the mod loading context. Most events
|
||||
* should go on the default BusGroup.
|
||||
*/
|
||||
public interface IModBusEvent {
|
||||
public interface IModBusEvent extends InheritableEvent {
|
||||
/**
|
||||
* @param modEventBusGroup Obtained from {@link net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext#getModBusGroup()} or your language provider's equivalent.
|
||||
* @param eventClass The event type you want to get the associated bus for.
|
||||
*/
|
||||
static <T extends IModBusEvent> @NonNull EventBus<@NonNull T> getBus(@NonNull BusGroup modEventBusGroup, @NonNull Class<T> eventClass) {
|
||||
// Do not copy! Temporary solution until the FML rewrite is complete. May throw in future
|
||||
// without prior notice.
|
||||
//
|
||||
// Mods creating their own events should directly assign the result of the create method to a
|
||||
// static final field in their event class, without wrapping or calling multiple times.
|
||||
//
|
||||
// Mods using existing events should instead use the existing static final BUS field in the
|
||||
// event class (for most events) or getBus(BusGroup) method (for mod bus events).
|
||||
return EventBus.create(modEventBusGroup, eventClass);
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,11 @@ package net.minecraftforge.fml.util.thread;
|
||||
|
||||
import net.minecraftforge.fml.LogicalSide;
|
||||
|
||||
public class EffectiveSide
|
||||
{
|
||||
/**
|
||||
* Attempts to determine the effect side of the current thread, based on know threads in networking.
|
||||
* This is not guaranteed to be correct, and modders should use other methods to determine the logical side when possible.
|
||||
*/
|
||||
public class EffectiveSide {
|
||||
public static LogicalSide get() {
|
||||
final ThreadGroup group = Thread.currentThread().getThreadGroup();
|
||||
return group instanceof SidedThreadGroup ? ((SidedThreadGroup) group).getSide() : LogicalSide.CLIENT;
|
||||
|
||||
@@ -7,11 +7,13 @@ package net.minecraftforge.fml.util.thread;
|
||||
|
||||
import net.minecraftforge.fml.LogicalSide;
|
||||
|
||||
public final class SidedThreadGroups
|
||||
{
|
||||
public final class SidedThreadGroups {
|
||||
public static final SidedThreadGroup CLIENT = new SidedThreadGroup(LogicalSide.CLIENT);
|
||||
public static final SidedThreadGroup SERVER = new SidedThreadGroup(LogicalSide.SERVER);
|
||||
|
||||
private SidedThreadGroups() {
|
||||
public static SidedThreadGroup get(boolean client) {
|
||||
return client ? CLIENT : SERVER;
|
||||
}
|
||||
|
||||
private SidedThreadGroups() { }
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import org.gradle.api.plugins.jvm.JvmTestSuite
|
||||
|
||||
plugins {
|
||||
id 'jvm-test-suite'
|
||||
id 'net.minecraftforge.forge.build.convention'
|
||||
}
|
||||
|
||||
gradleutils.displayName = 'FML Early Display'
|
||||
description = 'A pretty looking, but prone to erroring screen displayed during Forge loading.'
|
||||
|
||||
dependencies {
|
||||
implementation projects.fmlloader
|
||||
implementation projects.fmlcore
|
||||
implementation libs.bundles.lwjgl
|
||||
implementation earlyDisplayLibs.slf4j.api
|
||||
implementation libs.jopt.simple
|
||||
}
|
||||
|
||||
testing.suites.named('test', JvmTestSuite) {
|
||||
useJUnitJupiter('5.8.2')
|
||||
|
||||
dependencies {
|
||||
implementation earlyDisplayTestLibs.powermock.core
|
||||
runtimeOnly earlyDisplayLibs.slf4j.jdk14
|
||||
runtimeOnly.bundle convention.nativeVariants(earlyDisplayTestLibs.bundles.lwjgl)
|
||||
}
|
||||
}
|
||||
|
||||
convention.manifest('net/minecraftforge/fml/earlydisplay/', [
|
||||
'Automatic-Module-Name': 'net.minecraftforge.earlydisplay',
|
||||
'Forge-Module-Layer' : 'boot'
|
||||
])
|
||||
|
||||
convention.publish()
|
||||
@@ -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,716 @@
|
||||
/*
|
||||
* 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.opengl.GL;
|
||||
import org.lwjgl.sdl.SDLError;
|
||||
import org.lwjgl.sdl.SDLVideo;
|
||||
import org.lwjgl.sdl.SDL_Event;
|
||||
import org.lwjgl.sdl.SDL_Rect;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
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.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
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.Consumer;
|
||||
import java.util.function.IntConsumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.lwjgl.opengl.GL32C.*;
|
||||
|
||||
import static org.lwjgl.sdl.SDLError.*;
|
||||
import static org.lwjgl.sdl.SDLEvents.*;
|
||||
import static org.lwjgl.sdl.SDLHints.*;
|
||||
import static org.lwjgl.sdl.SDLInit.*;
|
||||
import static org.lwjgl.sdl.SDLVideo.*;
|
||||
|
||||
/**
|
||||
* 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}};
|
||||
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;
|
||||
// The GL ID of the window. Used for all operations
|
||||
private long window;
|
||||
// The SDL Context we use for switching between threads
|
||||
private long sdlContext;
|
||||
// The thread that contains and ticks the window while Forge is loading mods
|
||||
private static final ScheduledExecutorService renderScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
final var thread = Executors.defaultThreadFactory().newThread(r);
|
||||
thread.setName("EarlyDisplay");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
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 ImmediateWindowProvider selectBackend(String backend) {
|
||||
// We only support opengl
|
||||
//if ("default".equals(backend) || "opengl".equals(backend))
|
||||
// return this;
|
||||
return ImmediateWindowProvider.getFallbackHandler();
|
||||
}
|
||||
|
||||
@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 color 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 " + FMLLoader.versionInfo().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;
|
||||
SDL_GL_MakeCurrent(window, sdlContext);
|
||||
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
|
||||
SDL_GL_SwapWindow(window);
|
||||
} catch (Throwable t) {
|
||||
LOGGER.error("BARF", t);
|
||||
} finally {
|
||||
if (this.windowTick != null)
|
||||
SDL_GL_MakeCurrent(window, 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.sdlContext = SDL_GL_CreateContext(window);
|
||||
if (sdlContext == 0)
|
||||
throw new IllegalStateException("Failed to create OpenGL context: " + SDL_GetError());
|
||||
|
||||
// This thread owns the GL render context now. We should make a note of that.
|
||||
bindGlContext();
|
||||
GL.createCapabilities();
|
||||
|
||||
// Wait for one frame to be complete before swapping; enable vsync in other words.
|
||||
SDL_GL_SetSwapInterval(1);
|
||||
|
||||
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 color 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.addFirst(RenderElement.squir());
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
releaseGlContext();
|
||||
|
||||
this.windowTick = renderScheduler.scheduleAtFixedRate(this::renderThreadFunc, 50, 50, TimeUnit.MILLISECONDS);
|
||||
|
||||
// Update the performance information every 1/2 second
|
||||
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) {
|
||||
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 static 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", 0);
|
||||
if (res != 0) {
|
||||
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", 0);
|
||||
}
|
||||
}
|
||||
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
|
||||
*/
|
||||
public void initWindow(@Nullable String mcVersion) {
|
||||
// Initialize a time guard, in case something goes wrong
|
||||
long initBegin = System.nanoTime();
|
||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
crashElegantly("We are unable to initialize the graphics system.\nSDL_Init failed.\n");
|
||||
throw new IllegalStateException("Unable to initialize SDL");
|
||||
}
|
||||
long initEnd = System.nanoTime();
|
||||
|
||||
if (initEnd - initBegin > 1e9)
|
||||
LOGGER.error("WARNING : Window init took {} seconds to start.", (initEnd - initBegin) / 1.0e9);
|
||||
|
||||
// Set window hints for the new window we're gonna create.
|
||||
SDL_ResetHints();
|
||||
|
||||
String vanillaWindowTitle = "Minecraft* ";
|
||||
if (mcVersion != null)
|
||||
vanillaWindowTitle += mcVersion;
|
||||
|
||||
var primaryMonitor = SDL_GetPrimaryDisplay();
|
||||
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.\nSDL_GetPrimaryDisplay failed.\n");
|
||||
throw new IllegalStateException("Can't find a primary monitor");
|
||||
}
|
||||
|
||||
var vidmode = SDL_GetDesktopDisplayMode(primaryMonitor);
|
||||
if (vidmode == null) {
|
||||
LOGGER.error("Failed to get the current display video mode.");
|
||||
crashElegantly("Failed to get current display resolution.\nSDL_GetDesktopDisplayMode 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);
|
||||
|
||||
var skipVersions = FMLConfig.<String>getListConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_SKIP_GL_VERSIONS);
|
||||
boolean showHelpLog = FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_LOG_HELP_MSG);
|
||||
|
||||
record Attempt(String vesion, String error) {}
|
||||
final List<Attempt> attempts = new ArrayList<>();
|
||||
String requestedVersion = null;
|
||||
|
||||
for (int x = 0; x < GL_VERSIONS.length; x++) {
|
||||
var version = GL_VERSIONS[x];
|
||||
requestedVersion = version[0] + "." + version[1];
|
||||
|
||||
if (skipVersions.contains(requestedVersion)) {
|
||||
LOGGER.info("Skipping GL version " + requestedVersion + " because of configuration");
|
||||
continue;
|
||||
}
|
||||
|
||||
LOGGER.info("Trying GL version " + requestedVersion);
|
||||
if (showHelpLog && x == 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.""");
|
||||
}
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, version[0]);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, version[1]);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
|
||||
SDL_GL_SetAttribute(SDL_GL_FRAMEBUFFER_SRGB_CAPABLE, GL_TRUE);
|
||||
window = SDL_CreateWindow(vanillaWindowTitle, winWidth, winHeight,
|
||||
SDL_WINDOW_OPENGL
|
||||
| SDL_WINDOW_RESIZABLE
|
||||
| SDL_WINDOW_HIGH_PIXEL_DENSITY
|
||||
);
|
||||
|
||||
if (window == 0) {
|
||||
var error = Objects.requireNonNullElse(SDLError.SDL_GetError(), "<no error>");
|
||||
attempts.add(new Attempt(requestedVersion, error));
|
||||
LOGGER.trace("Failed to create window using %d.%d: %s", version[0], version[1], error);
|
||||
} else {
|
||||
SDLVideo.SDL_SetWindowMinimumSize(window, 320, 240);
|
||||
LOGGER.info("Created window using SDL video driver: {}", SDLVideo.SDL_GetCurrentVideoDriver());
|
||||
break; // We got a valid window!
|
||||
}
|
||||
}
|
||||
|
||||
if (window == 0) {
|
||||
LOGGER.error("Failed to find any valid GLFW profile.");
|
||||
var message = new StringBuilder()
|
||||
.append("Failed to find a valid GLFW profile.\n");
|
||||
|
||||
if (attempts.isEmpty()) {
|
||||
message.append("All GL Versions were skipped");
|
||||
} else {
|
||||
message.append("We tried:");
|
||||
for (var attempt : attempts)
|
||||
message.append("\n\t").append(attempt.vesion).append(':').append(attempt.error);
|
||||
}
|
||||
|
||||
crashElegantly(message.toString());
|
||||
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?");
|
||||
|
||||
try (var stack = org.lwjgl.system.MemoryStack.stackPush()) {
|
||||
var majBuf = stack.mallocInt(1);
|
||||
var minBuf = stack.mallocInt(1);
|
||||
SDL_GL_GetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, majBuf);
|
||||
SDL_GL_GetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, minBuf);
|
||||
this.glVersion = majBuf.get(0) + "." + minBuf.get(0);
|
||||
}
|
||||
|
||||
LOGGER.info("Requested GL version " + requestedVersion + " got version " + this.glVersion);
|
||||
this.window = window;
|
||||
|
||||
if (showHelpLog)
|
||||
FMLConfig.updateConfig(FMLConfig.ConfigValue.EARLY_WINDOW_LOG_HELP_MSG, false);
|
||||
|
||||
if (this.maximized)
|
||||
SDL_MaximizeWindow(window);
|
||||
|
||||
var size = getWindowSize(window);
|
||||
this.onWindowResize(size.x, size.y);
|
||||
|
||||
var monitor = getMonitorSize(primaryMonitor);
|
||||
if (monitor != null)
|
||||
SDL_SetWindowPosition(window, (monitor.width - size.x) / 2 + monitor.x, (monitor.height - size.y) / 2 + monitor.y);
|
||||
|
||||
pollEvents();
|
||||
|
||||
SDL_ShowWindow(window);
|
||||
}
|
||||
|
||||
private record Point(int x, int y) {}
|
||||
private record Rect(int x, int y, int width, int height) {}
|
||||
|
||||
private static @Nullable Rect getMonitorSize(int monitor) {
|
||||
try (var bounds = SDL_Rect.malloc()) {
|
||||
// Fetch the display bounds for the current index
|
||||
if (SDL_GetDisplayBounds(monitor, bounds)) {
|
||||
int x = bounds.x();
|
||||
int y = bounds.y();
|
||||
int width = bounds.w();
|
||||
int height = bounds.h();
|
||||
LOGGER.info("Display {} Bounds -> X: {}, Y: {}, Width: {}, Height: {}", monitor, x, y, width, height);
|
||||
return new Rect(x, y, width, height);
|
||||
} else {
|
||||
LOGGER.warn("Could not get bounds for display: " + SDL_GetError());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Point getWindowSize(long window) {
|
||||
try (var stack = MemoryStack.stackPush()) {
|
||||
var width = stack.mallocInt(1);
|
||||
var height = stack.mallocInt(1);
|
||||
SDL_GetWindowSize(window, width, height);
|
||||
return new Point(width.get(0), height.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hand-off the window to the vanilla game.
|
||||
* Called on the main thread instead of the game's initialization.
|
||||
*
|
||||
* @return the Window we own.
|
||||
*/
|
||||
@Override
|
||||
public long setupMinecraftWindow(final int width, final int height, final String title, final Supplier<Object> backend) {
|
||||
// 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();
|
||||
|
||||
SDL_GL_MakeCurrent(window, sdlContext);
|
||||
// Set the title to what the game wants
|
||||
SDL_SetWindowTitle(window, title);
|
||||
SDL_GL_SetSwapInterval(0); // Disable vsync, let Minecraft control it
|
||||
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() {
|
||||
pollEvents();
|
||||
repaintTick.run();
|
||||
}
|
||||
|
||||
public void addMojangTexture(final int textureId) {
|
||||
this.elements.addFirst(RenderElement.mojang(textureId, framecount));
|
||||
}
|
||||
|
||||
public void close() {
|
||||
// Close the Render Scheduler thread
|
||||
renderScheduler.shutdown();
|
||||
this.framebuffer.close();
|
||||
this.context.elementShader().close();
|
||||
SimpleBufferBuilder.destroy();
|
||||
}
|
||||
|
||||
private void bindGlContext() {
|
||||
SDL_GL_MakeCurrent(window, sdlContext);
|
||||
//if (!SDL_GL_MakeCurrent(window, sdlContext))
|
||||
// throw new IllegalStateException("Failed to make OpenGL context current: " + SDL_GetError());
|
||||
}
|
||||
|
||||
private void releaseGlContext() {
|
||||
SDL_GL_MakeCurrent(window, 0);
|
||||
}
|
||||
|
||||
|
||||
private void pollEvents() {
|
||||
try (var event = SDL_Event.malloc()) {
|
||||
while (SDL_PollEvent(event)) {
|
||||
switch (event.type()) {
|
||||
case SDL_EVENT_WINDOW_MOVED:
|
||||
this.onWindowMove(event.window().data1(), event.window().data2());
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_RESIZED:
|
||||
this.onWindowResize(event.window().data1(), event.window().data2());
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
|
||||
this.onFrameBufferResize(event.window().data1(), event.window().data2());
|
||||
break;
|
||||
|
||||
/*
|
||||
We were told to quit for some reason, find a way to abort?
|
||||
case SDL_EVENT_QUIT:
|
||||
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
||||
case SDL_EVENT_TERMINATING:
|
||||
|
||||
Window was minimized or restored, maybe stop rendering?
|
||||
case SDL_EVENT_WINDOW_MINIMIZED:
|
||||
case SDL_EVENT_WINDOW_MAXIMIZED:
|
||||
case SDL_EVENT_WINDOW_RESTORED:
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onWindowResize(int width, int height) {
|
||||
if (width == 0 || height == 0)
|
||||
return;
|
||||
|
||||
this.winWidth = width;
|
||||
this.winHeight = height;
|
||||
}
|
||||
|
||||
private void onFrameBufferResize(int width, int height) {
|
||||
if (width == 0 || height == 0)
|
||||
return;
|
||||
|
||||
this.fbWidth = width;
|
||||
this.fbHeight = height;
|
||||
}
|
||||
|
||||
private void onWindowMove(int x, int y) {
|
||||
this.winX = x;
|
||||
this.winY = y;
|
||||
}
|
||||
|
||||
}
|
||||
+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;
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 static final MemoryMXBean MEMORY_BEAN = ManagementFactory.getMemoryMXBean();
|
||||
|
||||
private final boolean showCPUUsage;
|
||||
private final OperatingSystemMXBean osBean;
|
||||
|
||||
private float memory;
|
||||
private String text;
|
||||
|
||||
PerformanceInfo() {
|
||||
showCPUUsage = FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.EARLY_WINDOW_SHOW_CPU);
|
||||
osBean = showCPUUsage ? ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class) : null;
|
||||
}
|
||||
|
||||
void update() {
|
||||
final MemoryUsage heapusage = MEMORY_BEAN.getHeapMemoryUsage();
|
||||
memory = (float) heapusage.getUsed() / heapusage.getMax();
|
||||
|
||||
if (!showCPUUsage) {
|
||||
text = "Memory: %d/%dMB (%.1f%%)".formatted(heapusage.getUsed() >>> 20, heapusage.getMax() >>> 20, memory * 100f);
|
||||
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 = "Memory: %d/%dMB (%.1f%%) %s".formatted(heapusage.getUsed() >>> 20, heapusage.getMax() >>> 20, memory * 100f, 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,327 @@
|
||||
/*
|
||||
* 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 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;
|
||||
|
||||
@FunctionalInterface
|
||||
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 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(Renderer renderer) {
|
||||
this.bb = new SimpleBufferBuilder(1);
|
||||
this.renderer = renderer;
|
||||
}
|
||||
|
||||
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 = Math.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 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, _, 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()}, _->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()}, _ -> colour, _ -> new float[]{0f, pm.progress()});
|
||||
}
|
||||
Renderer label = (bb, ctx, _) -> 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 {
|
||||
int progress = frame % 100;
|
||||
return new float[] { Math.clamp((progress - 2) / 100f, 0f, 1f), Math.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()}, _ -> colour, _ -> new float[]{0f, pi.memory()});
|
||||
var width = font.stringWidth(pi.text());
|
||||
Renderer label = (bb, ctx, _) -> 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 Renderer initializeText(SimpleFont font, TextGenerator textGenerator) {
|
||||
return (bb, context, _) -> 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, _) -> font.generateVerticesForTexts(x, y, bb, new SimpleFont.DisplayText(text, colour));
|
||||
}
|
||||
|
||||
private static Renderer initializeTexture(final String textureFileName, int size, int textureNumber, TextureRenderer positionAndColour) {
|
||||
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();
|
||||
}
|
||||
|
||||
@Deprecated(since = "1.21.5", forRemoval = true)
|
||||
public static float clamp(float num, float min, float max) {
|
||||
return Math.clamp(num, min, max);
|
||||
}
|
||||
|
||||
public static int clamp(int num, int min, int max) {
|
||||
return Math.min(max, Math.max(num, min));
|
||||
}
|
||||
|
||||
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 static 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 < 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 < 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 |
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id 'net.minecraftforge.forge.build.convention'
|
||||
id 'jvm-test-suite'
|
||||
}
|
||||
|
||||
gradleutils.displayName = 'FMLLoader-Tests'
|
||||
description = 'JUnit tests for FMLLoader, not meant for distribution'
|
||||
|
||||
dependencies {
|
||||
testImplementation(project(':fmlloader'))
|
||||
testImplementation(libs.guava)
|
||||
testImplementation(libs.junit.api)
|
||||
testRuntimeOnly(libs.bundles.junit.runtime)
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
Loaded 100 of 3874 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user